Skip to content

Commit eafa7da

Browse files
committed
fix: respect lifecycle.ignore_changes in settings resource
1 parent eb42f5e commit eafa7da

File tree

2 files changed

+273
-12
lines changed

2 files changed

+273
-12
lines changed

internal/provider/settings_resource.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -195,34 +195,41 @@ func (r *SettingsResource) Read(ctx context.Context, req resource.ReadRequest, r
195195
}
196196

197197
func (r *SettingsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
198-
var data SettingsResourceModel
198+
var planData, stateData SettingsResourceModel
199199

200200
// Read Terraform plan data into the model
201-
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
201+
resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...)
202202
if resp.Diagnostics.HasError() {
203203
return
204204
}
205205

206-
// Ignore any states not specified in the TF plan.
207-
if !data.Database.IsNull() {
208-
resp.Diagnostics.Append(updateDatabaseConfig(ctx, &data, r.client)...)
206+
// Read Terraform state data into the model
207+
resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...)
208+
if resp.Diagnostics.HasError() {
209+
return
209210
}
210-
if !data.Network.IsNull() {
211-
resp.Diagnostics.Append(updateNetworkConfig(ctx, &data, r.client)...)
211+
212+
// Only update settings that are present in the plan and have actually changed.
213+
// This respects lifecycle.ignore_changes and avoids no-op API calls.
214+
if !planData.Database.IsNull() && !planData.Database.Equal(stateData.Database) {
215+
resp.Diagnostics.Append(updateDatabaseConfig(ctx, &planData, r.client)...)
212216
}
213-
if !data.Api.IsNull() {
214-
resp.Diagnostics.Append(updateApiConfig(ctx, &data, r.client)...)
217+
if !planData.Network.IsNull() && !planData.Network.Equal(stateData.Network) {
218+
resp.Diagnostics.Append(updateNetworkConfig(ctx, &planData, r.client)...)
215219
}
216-
if !data.Auth.IsNull() {
217-
resp.Diagnostics.Append(updateAuthConfig(ctx, &data, r.client)...)
220+
if !planData.Api.IsNull() && !planData.Api.Equal(stateData.Api) {
221+
resp.Diagnostics.Append(updateApiConfig(ctx, &planData, r.client)...)
222+
}
223+
if !planData.Auth.IsNull() && !planData.Auth.Equal(stateData.Auth) {
224+
resp.Diagnostics.Append(updateAuthConfig(ctx, &planData, r.client)...)
218225
}
219226
// TODO: update all settings above concurrently
220227
if resp.Diagnostics.HasError() {
221228
return
222229
}
223230

224231
// Save updated data into Terraform state
225-
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
232+
resp.Diagnostics.Append(resp.State.Set(ctx, &planData)...)
226233
}
227234

228235
func (r *SettingsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {

internal/provider/settings_resource_test.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
package provider
55

66
import (
7+
"bytes"
78
"encoding/json"
89
"errors"
910
"fmt"
11+
"io"
1012
"net/http"
13+
"strings"
1114
"testing"
1215

1316
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -338,6 +341,257 @@ func unmarshalStateAttr(state *terraform.InstanceState, attr string) (map[string
338341
return out, nil
339342
}
340343

344+
func TestAccSettingsResource_SmtpPass(t *testing.T) {
345+
// Setup mock api
346+
defer gock.OffAll()
347+
gock.New("https://api.supabase.com").
348+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
349+
Reply(http.StatusOK).
350+
JSON(api.AuthConfigResponse{
351+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
352+
MailerOtpExp: 3600,
353+
MfaPhoneOtpLength: 6,
354+
SmsOtpLength: 6,
355+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
356+
})
357+
gock.New("https://api.supabase.com").
358+
Patch("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
359+
AddMatcher(func(req *http.Request, _ *gock.Request) (bool, error) {
360+
body, err := io.ReadAll(req.Body)
361+
if err != nil {
362+
return false, err
363+
}
364+
req.Body = io.NopCloser(bytes.NewBuffer(body))
365+
bodyStr := string(body)
366+
return strings.Contains(bodyStr, `"smtp_pass"`) &&
367+
strings.Contains(bodyStr, `"secret_password_123"`), nil
368+
}).
369+
Reply(http.StatusOK).
370+
JSON(api.AuthConfigResponse{
371+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
372+
MailerOtpExp: 3600,
373+
MfaPhoneOtpLength: 6,
374+
SmsOtpLength: 6,
375+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
376+
})
377+
gock.New("https://api.supabase.com").
378+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
379+
Reply(http.StatusOK).
380+
JSON(api.AuthConfigResponse{
381+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
382+
MailerOtpExp: 3600,
383+
MfaPhoneOtpLength: 6,
384+
SmsOtpLength: 6,
385+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
386+
})
387+
388+
resource.Test(t, resource.TestCase{
389+
PreCheck: func() { testAccPreCheck(t) },
390+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
391+
Steps: []resource.TestStep{
392+
{
393+
Config: `
394+
resource "supabase_settings" "test" {
395+
project_ref = "mayuaycdtijbctgqbycg"
396+
397+
auth = jsonencode({
398+
site_url = "http://localhost:3000"
399+
smtp_pass = "secret_password_123"
400+
})
401+
}
402+
`,
403+
Check: resource.ComposeAggregateTestCheckFunc(
404+
resource.TestCheckResourceAttr("supabase_settings.test", "id", "mayuaycdtijbctgqbycg"),
405+
),
406+
},
407+
},
408+
})
409+
}
410+
411+
func TestAccSettingsResource_IgnoreChanges(t *testing.T) {
412+
defer gock.OffAll()
413+
414+
projectRef := "mayuaycdtijbctgqbycg"
415+
416+
gock.New("https://api.supabase.com").
417+
Get("/v1/projects/" + projectRef + "/config/database/postgres").
418+
Reply(http.StatusOK).
419+
JSON(api.PostgresConfigResponse{})
420+
gock.New("https://api.supabase.com").
421+
Get("/v1/projects/" + projectRef + "/network-restrictions").
422+
Reply(http.StatusOK).
423+
JSON(api.NetworkRestrictionsResponse{
424+
Config: api.NetworkRestrictionsRequest{
425+
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32"}),
426+
},
427+
})
428+
gock.New("https://api.supabase.com").
429+
Post("/v1/projects/" + projectRef + "/network-restrictions").
430+
Reply(http.StatusCreated).
431+
JSON(api.NetworkRestrictionsResponse{
432+
Config: api.NetworkRestrictionsRequest{
433+
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32"}),
434+
},
435+
})
436+
gock.New("https://api.supabase.com").
437+
Get("/v1/projects/" + projectRef + "/postgrest").
438+
Reply(http.StatusOK).
439+
JSON(api.V1PostgrestConfigResponse{})
440+
gock.New("https://api.supabase.com").
441+
Get("/v1/projects/" + projectRef + "/config/auth").
442+
Reply(http.StatusOK).
443+
JSON(api.AuthConfigResponse{
444+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
445+
MailerOtpExp: 3600,
446+
MfaPhoneOtpLength: 6,
447+
SmsOtpLength: 6,
448+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
449+
})
450+
gock.New("https://api.supabase.com").
451+
Patch("/v1/projects/" + projectRef + "/config/auth").
452+
Reply(http.StatusOK).
453+
JSON(api.AuthConfigResponse{
454+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
455+
MailerOtpExp: 3600,
456+
MfaPhoneOtpLength: 6,
457+
SmsOtpLength: 6,
458+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
459+
})
460+
gock.New("https://api.supabase.com").
461+
Get("/v1/projects/" + projectRef + "/network-restrictions").
462+
Reply(http.StatusOK).
463+
JSON(api.NetworkRestrictionsResponse{
464+
Config: api.NetworkRestrictionsRequest{
465+
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32"}),
466+
},
467+
})
468+
gock.New("https://api.supabase.com").
469+
Get("/v1/projects/" + projectRef + "/config/auth").
470+
Reply(http.StatusOK).
471+
JSON(api.AuthConfigResponse{
472+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
473+
MailerOtpExp: 3600,
474+
MfaPhoneOtpLength: 6,
475+
SmsOtpLength: 6,
476+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
477+
})
478+
gock.New("https://api.supabase.com").
479+
Post("/v1/projects/" + projectRef + "/network-restrictions").
480+
Reply(http.StatusCreated).
481+
JSON(api.NetworkRestrictionsResponse{
482+
Config: api.NetworkRestrictionsRequest{
483+
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32", "198.51.100.1/32"}),
484+
},
485+
})
486+
gock.New("https://api.supabase.com").
487+
Get("/v1/projects/" + projectRef + "/network-restrictions").
488+
Reply(http.StatusOK).
489+
JSON(api.NetworkRestrictionsResponse{
490+
Config: api.NetworkRestrictionsRequest{
491+
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32", "198.51.100.1/32"}),
492+
},
493+
})
494+
gock.New("https://api.supabase.com").
495+
Get("/v1/projects/" + projectRef + "/config/auth").
496+
Reply(http.StatusOK).
497+
JSON(api.AuthConfigResponse{
498+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
499+
MailerOtpExp: 3600,
500+
MfaPhoneOtpLength: 6,
501+
SmsOtpLength: 6,
502+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
503+
})
504+
for range 5 {
505+
gock.New("https://api.supabase.com").
506+
Get("/v1/projects/" + projectRef + "/network-restrictions").
507+
Reply(http.StatusOK).
508+
JSON(api.NetworkRestrictionsResponse{
509+
Config: api.NetworkRestrictionsRequest{
510+
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32", "198.51.100.1/32"}),
511+
},
512+
})
513+
gock.New("https://api.supabase.com").
514+
Get("/v1/projects/" + projectRef + "/config/auth").
515+
Reply(http.StatusOK).
516+
JSON(api.AuthConfigResponse{
517+
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
518+
MailerOtpExp: 3600,
519+
MfaPhoneOtpLength: 6,
520+
SmsOtpLength: 6,
521+
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
522+
})
523+
}
524+
525+
authPatchCalled := false
526+
gock.New("https://api.supabase.com").
527+
Patch("/v1/projects/" + projectRef + "/config/auth").
528+
AddMatcher(func(req *http.Request, _ *gock.Request) (bool, error) {
529+
authPatchCalled = true
530+
return true, nil
531+
}).
532+
Reply(http.StatusBadRequest).
533+
JSON(map[string]any{
534+
"message": "Should not be called",
535+
})
536+
537+
resource.Test(t, resource.TestCase{
538+
PreCheck: func() { testAccPreCheck(t) },
539+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
540+
Steps: []resource.TestStep{
541+
{
542+
Config: `
543+
resource "supabase_settings" "test" {
544+
project_ref = "mayuaycdtijbctgqbycg"
545+
546+
network = jsonencode({
547+
restrictions = ["203.0.113.1/32"]
548+
})
549+
550+
auth = jsonencode({
551+
site_url = "http://localhost:3000"
552+
})
553+
554+
lifecycle {
555+
ignore_changes = [auth]
556+
}
557+
}
558+
`,
559+
Check: resource.ComposeAggregateTestCheckFunc(
560+
resource.TestCheckResourceAttr("supabase_settings.test", "id", projectRef),
561+
),
562+
},
563+
{
564+
Config: `
565+
resource "supabase_settings" "test" {
566+
project_ref = "mayuaycdtijbctgqbycg"
567+
568+
network = jsonencode({
569+
restrictions = ["203.0.113.1/32", "198.51.100.1/32"]
570+
})
571+
572+
auth = jsonencode({
573+
site_url = "http://localhost:3000"
574+
})
575+
576+
lifecycle {
577+
ignore_changes = [auth]
578+
}
579+
}
580+
`,
581+
Check: resource.ComposeAggregateTestCheckFunc(
582+
resource.TestCheckResourceAttr("supabase_settings.test", "id", projectRef),
583+
func(s *terraform.State) error {
584+
if authPatchCalled {
585+
return fmt.Errorf("auth PATCH was called despite lifecycle.ignore_changes")
586+
}
587+
return nil
588+
},
589+
),
590+
},
591+
},
592+
})
593+
}
594+
341595
const testAccSettingsResourceConfig = `
342596
resource "supabase_settings" "production" {
343597
project_ref = "mayuaycdtijbctgqbycg"

0 commit comments

Comments
 (0)