Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions internal/provider/settings_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,34 +195,41 @@ func (r *SettingsResource) Read(ctx context.Context, req resource.ReadRequest, r
}

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

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...)
if resp.Diagnostics.HasError() {
return
}

// Ignore any states not specified in the TF plan.
if !data.Database.IsNull() {
resp.Diagnostics.Append(updateDatabaseConfig(ctx, &data, r.client)...)
// Read Terraform state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...)
if resp.Diagnostics.HasError() {
return
}
if !data.Network.IsNull() {
resp.Diagnostics.Append(updateNetworkConfig(ctx, &data, r.client)...)

// Only update settings that are present in the plan and have actually changed.
// This respects lifecycle.ignore_changes and avoids no-op API calls.
if !planData.Database.IsNull() && !planData.Database.Equal(stateData.Database) {
resp.Diagnostics.Append(updateDatabaseConfig(ctx, &planData, r.client)...)
}
if !data.Api.IsNull() {
resp.Diagnostics.Append(updateApiConfig(ctx, &data, r.client)...)
if !planData.Network.IsNull() && !planData.Network.Equal(stateData.Network) {
resp.Diagnostics.Append(updateNetworkConfig(ctx, &planData, r.client)...)
}
if !data.Auth.IsNull() {
resp.Diagnostics.Append(updateAuthConfig(ctx, &data, r.client)...)
if !planData.Api.IsNull() && !planData.Api.Equal(stateData.Api) {
resp.Diagnostics.Append(updateApiConfig(ctx, &planData, r.client)...)
}
if !planData.Auth.IsNull() && !planData.Auth.Equal(stateData.Auth) {
resp.Diagnostics.Append(updateAuthConfig(ctx, &planData, r.client)...)
}
// TODO: update all settings above concurrently
if resp.Diagnostics.HasError() {
return
}

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
resp.Diagnostics.Append(resp.State.Set(ctx, &planData)...)
}

func (r *SettingsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
Expand Down
254 changes: 254 additions & 0 deletions internal/provider/settings_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
package provider

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"testing"

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

func TestAccSettingsResource_SmtpPass(t *testing.T) {
// Setup mock api
defer gock.OffAll()
gock.New("https://api.supabase.com").
Get("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
Reply(http.StatusOK).
JSON(api.AuthConfigResponse{
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
MailerOtpExp: 3600,
MfaPhoneOtpLength: 6,
SmsOtpLength: 6,
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
})
gock.New("https://api.supabase.com").
Patch("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
AddMatcher(func(req *http.Request, _ *gock.Request) (bool, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return false, err
}
req.Body = io.NopCloser(bytes.NewBuffer(body))
bodyStr := string(body)
return strings.Contains(bodyStr, `"smtp_pass"`) &&
strings.Contains(bodyStr, `"secret_password_123"`), nil
}).
Reply(http.StatusOK).
JSON(api.AuthConfigResponse{
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
MailerOtpExp: 3600,
MfaPhoneOtpLength: 6,
SmsOtpLength: 6,
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
})
gock.New("https://api.supabase.com").
Get("/v1/projects/mayuaycdtijbctgqbycg/config/auth").
Reply(http.StatusOK).
JSON(api.AuthConfigResponse{
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
MailerOtpExp: 3600,
MfaPhoneOtpLength: 6,
SmsOtpLength: 6,
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
})

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "supabase_settings" "test" {
project_ref = "mayuaycdtijbctgqbycg"

auth = jsonencode({
site_url = "http://localhost:3000"
smtp_pass = "secret_password_123"
})
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("supabase_settings.test", "id", "mayuaycdtijbctgqbycg"),
),
},
},
})
}

func TestAccSettingsResource_IgnoreChanges(t *testing.T) {
defer gock.OffAll()

projectRef := "mayuaycdtijbctgqbycg"

gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/config/database/postgres").
Reply(http.StatusOK).
JSON(api.PostgresConfigResponse{})
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/network-restrictions").
Reply(http.StatusOK).
JSON(api.NetworkRestrictionsResponse{
Config: api.NetworkRestrictionsRequest{
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32"}),
},
})
gock.New("https://api.supabase.com").
Post("/v1/projects/" + projectRef + "/network-restrictions").
Reply(http.StatusCreated).
JSON(api.NetworkRestrictionsResponse{
Config: api.NetworkRestrictionsRequest{
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32"}),
},
})
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/postgrest").
Reply(http.StatusOK).
JSON(api.V1PostgrestConfigResponse{})
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/config/auth").
Reply(http.StatusOK).
JSON(api.AuthConfigResponse{
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
MailerOtpExp: 3600,
MfaPhoneOtpLength: 6,
SmsOtpLength: 6,
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
})
gock.New("https://api.supabase.com").
Patch("/v1/projects/" + projectRef + "/config/auth").
Reply(http.StatusOK).
JSON(api.AuthConfigResponse{
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
MailerOtpExp: 3600,
MfaPhoneOtpLength: 6,
SmsOtpLength: 6,
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
})
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/network-restrictions").
Reply(http.StatusOK).
JSON(api.NetworkRestrictionsResponse{
Config: api.NetworkRestrictionsRequest{
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32"}),
},
})
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/config/auth").
Reply(http.StatusOK).
JSON(api.AuthConfigResponse{
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
MailerOtpExp: 3600,
MfaPhoneOtpLength: 6,
SmsOtpLength: 6,
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
})
gock.New("https://api.supabase.com").
Post("/v1/projects/" + projectRef + "/network-restrictions").
Reply(http.StatusCreated).
JSON(api.NetworkRestrictionsResponse{
Config: api.NetworkRestrictionsRequest{
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32", "198.51.100.1/32"}),
},
})
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/network-restrictions").
Reply(http.StatusOK).
JSON(api.NetworkRestrictionsResponse{
Config: api.NetworkRestrictionsRequest{
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32", "198.51.100.1/32"}),
},
})
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/config/auth").
Reply(http.StatusOK).
JSON(api.AuthConfigResponse{
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
MailerOtpExp: 3600,
MfaPhoneOtpLength: 6,
SmsOtpLength: 6,
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
})
for range 5 {
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/network-restrictions").
Reply(http.StatusOK).
JSON(api.NetworkRestrictionsResponse{
Config: api.NetworkRestrictionsRequest{
DbAllowedCidrs: Ptr([]string{"203.0.113.1/32", "198.51.100.1/32"}),
},
})
gock.New("https://api.supabase.com").
Get("/v1/projects/" + projectRef + "/config/auth").
Reply(http.StatusOK).
JSON(api.AuthConfigResponse{
SiteUrl: nullable.NewNullableWithValue("http://localhost:3000"),
MailerOtpExp: 3600,
MfaPhoneOtpLength: 6,
SmsOtpLength: 6,
SmtpAdminEmail: nullable.NewNullNullable[openapi_types.Email](),
})
}

authPatchCalled := false
gock.New("https://api.supabase.com").
Patch("/v1/projects/" + projectRef + "/config/auth").
AddMatcher(func(req *http.Request, _ *gock.Request) (bool, error) {
authPatchCalled = true
return true, nil
}).
Reply(http.StatusBadRequest).
JSON(map[string]any{
"message": "Should not be called",
})

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "supabase_settings" "test" {
project_ref = "mayuaycdtijbctgqbycg"

network = jsonencode({
restrictions = ["203.0.113.1/32"]
})

auth = jsonencode({
site_url = "http://localhost:3000"
})

lifecycle {
ignore_changes = [auth]
}
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("supabase_settings.test", "id", projectRef),
),
},
{
Config: `
resource "supabase_settings" "test" {
project_ref = "mayuaycdtijbctgqbycg"

network = jsonencode({
restrictions = ["203.0.113.1/32", "198.51.100.1/32"]
})

auth = jsonencode({
site_url = "http://localhost:3000"
})

lifecycle {
ignore_changes = [auth]
}
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("supabase_settings.test", "id", projectRef),
func(s *terraform.State) error {
if authPatchCalled {
return fmt.Errorf("auth PATCH was called despite lifecycle.ignore_changes")
}
return nil
},
),
},
},
})
}

const testAccSettingsResourceConfig = `
resource "supabase_settings" "production" {
project_ref = "mayuaycdtijbctgqbycg"
Expand Down