Skip to content

Commit f0e5521

Browse files
feat: support configuring database settings (#56)
* feat: support configuring database settings * chore: reuse config update methods * docs: update json schema file --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 4272699 commit f0e5521

File tree

5 files changed

+111
-22
lines changed

5 files changed

+111
-22
lines changed

docs/resources/settings.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Settings resource
1616
resource "supabase_settings" "production" {
1717
project_ref = "mayuaycdtijbctgqbycg"
1818
19+
database = jsonencode({
20+
statement_timeout = "10s"
21+
})
22+
1923
api = jsonencode({
2024
db_schema = "public,storage,graphql_public"
2125
db_extra_search_path = "public,extensions"
@@ -39,7 +43,7 @@ resource "supabase_settings" "production" {
3943

4044
- `api` (String) API settings as [serialised JSON](https://api.supabase.com/api/v1#/services/updatePostgRESTConfig)
4145
- `auth` (String) Auth settings as [serialised JSON](https://api.supabase.com/api/v1#/projects%20config/updateV1AuthConfig)
42-
- `database` (String) Database settings as serialised JSON
46+
- `database` (String) Database settings as [serialised JSON](https://api.supabase.com/api/v1#/projects%20config/updateConfig)
4347
- `network` (String) Network settings as serialised JSON
4448
- `pooler` (String) Pooler settings as serialised JSON
4549
- `storage` (String) Storage settings as serialised JSON

docs/schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@
175175
},
176176
"database": {
177177
"type": "string",
178-
"description": "Database settings as serialised JSON",
178+
"description": "Database settings as [serialised JSON](https://api.supabase.com/api/v1#/projects%20config/updateConfig)",
179179
"description_kind": "markdown",
180180
"optional": true
181181
},

examples/resources/supabase_settings/resource.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
resource "supabase_settings" "production" {
22
project_ref = "mayuaycdtijbctgqbycg"
33

4+
database = jsonencode({
5+
statement_timeout = "10s"
6+
})
7+
48
api = jsonencode({
59
db_schema = "public,storage,graphql_public"
610
db_extra_search_path = "public,extensions"

internal/provider/settings_resource.go

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (r *SettingsResource) Schema(ctx context.Context, req resource.SchemaReques
6363
},
6464
"database": schema.StringAttribute{
6565
CustomType: jsontypes.NormalizedType{},
66-
MarkdownDescription: "Database settings as serialised JSON",
66+
MarkdownDescription: "Database settings as [serialised JSON](https://api.supabase.com/api/v1#/projects%20config/updateConfig)",
6767
Optional: true,
6868
},
6969
"pooler": schema.StringAttribute{
@@ -132,6 +132,9 @@ func (r *SettingsResource) Create(ctx context.Context, req resource.CreateReques
132132

133133
// Initial settings are always created together with the project resource.
134134
// We can simply apply partial updates here based on the given TF plan.
135+
if !data.Database.IsNull() {
136+
resp.Diagnostics.Append(updateDatabaseConfig(ctx, &data, r.client)...)
137+
}
135138
if !data.Api.IsNull() {
136139
resp.Diagnostics.Append(updateApiConfig(ctx, &data, r.client)...)
137140
}
@@ -164,6 +167,9 @@ func (r *SettingsResource) Read(ctx context.Context, req resource.ReadRequest, r
164167

165168
// If an existing state has not been imported or created from a TF plan before,
166169
// skip loading them because we are not interested in managing them through TF.
170+
if !data.Database.IsNull() {
171+
resp.Diagnostics.Append(readDatabaseConfig(ctx, &data, r.client)...)
172+
}
167173
if !data.Api.IsNull() {
168174
resp.Diagnostics.Append(readApiConfig(ctx, &data, r.client)...)
169175
}
@@ -191,6 +197,9 @@ func (r *SettingsResource) Update(ctx context.Context, req resource.UpdateReques
191197
}
192198

193199
// Ignore any states not specified in the TF plan.
200+
if !data.Database.IsNull() {
201+
resp.Diagnostics.Append(updateDatabaseConfig(ctx, &data, r.client)...)
202+
}
194203
if !data.Api.IsNull() {
195204
resp.Diagnostics.Append(updateApiConfig(ctx, &data, r.client)...)
196205
}
@@ -223,6 +232,7 @@ func (r *SettingsResource) ImportState(ctx context.Context, req resource.ImportS
223232

224233
// Read all configs from API when importing so it's easier to pick
225234
// individual fields to manage through TF.
235+
resp.Diagnostics.Append(readDatabaseConfig(ctx, &data, r.client)...)
226236
resp.Diagnostics.Append(readApiConfig(ctx, &data, r.client)...)
227237
resp.Diagnostics.Append(readAuthConfig(ctx, &data, r.client)...)
228238

@@ -269,20 +279,10 @@ func updateApiConfig(ctx context.Context, plan *SettingsResourceModel, client *a
269279
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
270280
}
271281

272-
partial := make(map[string]interface{})
273-
if diags := plan.Api.Unmarshal(&partial); diags.HasError() {
274-
// Unreachable because unmarshalling to request body returned no error
275-
return diags
276-
}
277-
pickConfig(*httpResp.JSON200, partial)
278-
279-
value, err := json.Marshal(partial)
280-
if err != nil {
281-
msg := fmt.Sprintf("Unable to update api settings, got marshal error: %s", err)
282+
if plan.Api, err = parseConfig(plan.Api, *httpResp.JSON200); err != nil {
283+
msg := fmt.Sprintf("Unable to update api settings, got error: %s", err)
282284
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
283285
}
284-
285-
plan.Api = jsontypes.NewNormalizedValue(string(value))
286286
return nil
287287
}
288288

@@ -324,20 +324,55 @@ func updateAuthConfig(ctx context.Context, plan *SettingsResourceModel, client *
324324
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
325325
}
326326

327-
partial := make(map[string]interface{})
328-
if diags := plan.Auth.Unmarshal(&partial); diags.HasError() {
329-
// Unreachable because unmarshalling to request body returned no error
327+
if plan.Auth, err = parseConfig(plan.Auth, *httpResp.JSON200); err != nil {
328+
msg := fmt.Sprintf("Unable to update auth settings, got error: %s", err)
329+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
330+
}
331+
return nil
332+
}
333+
334+
func readDatabaseConfig(ctx context.Context, state *SettingsResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
335+
httpResp, err := client.GetConfigWithResponse(ctx, state.Id.ValueString())
336+
if err != nil {
337+
msg := fmt.Sprintf("Unable to read database settings, got error: %s", err)
338+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
339+
}
340+
// Deleted project is an orphan resource, not returning error so it can be destroyed.
341+
switch httpResp.StatusCode() {
342+
case http.StatusNotFound, http.StatusNotAcceptable:
343+
return nil
344+
}
345+
if httpResp.JSON200 == nil {
346+
msg := fmt.Sprintf("Unable to read database settings, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
347+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
348+
}
349+
if state.Database, err = parseConfig(state.Database, *httpResp.JSON200); err != nil {
350+
msg := fmt.Sprintf("Unable to read database settings, got error: %s", err)
351+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
352+
}
353+
return nil
354+
}
355+
356+
func updateDatabaseConfig(ctx context.Context, plan *SettingsResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
357+
var body api.UpdatePostgresConfigBody
358+
if diags := plan.Database.Unmarshal(&body); diags.HasError() {
330359
return diags
331360
}
332-
pickConfig(*httpResp.JSON200, partial)
333361

334-
value, err := json.Marshal(partial)
362+
httpResp, err := client.UpdateConfigWithResponse(ctx, plan.ProjectRef.ValueString(), body)
335363
if err != nil {
336-
msg := fmt.Sprintf("Unable to update auth settings, got marshal error: %s", err)
364+
msg := fmt.Sprintf("Unable to update database settings, got error: %s", err)
365+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
366+
}
367+
if httpResp.JSON200 == nil {
368+
msg := fmt.Sprintf("Unable to update database settings, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
337369
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
338370
}
339371

340-
plan.Auth = jsontypes.NewNormalizedValue(string(value))
372+
if plan.Database, err = parseConfig(plan.Database, *httpResp.JSON200); err != nil {
373+
msg := fmt.Sprintf("Unable to update database settings, got error: %s", err)
374+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
375+
}
341376
return nil
342377
}
343378

internal/provider/settings_resource_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ func TestAccSettingsResource(t *testing.T) {
2121
// Setup mock api
2222
defer gock.OffAll()
2323
// Step 1: create
24+
gock.New("https://api.supabase.com").
25+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/database/postgres").
26+
Reply(http.StatusOK).
27+
JSON(api.PostgresConfigResponse{
28+
StatementTimeout: Ptr("10s"),
29+
})
30+
gock.New("https://api.supabase.com").
31+
Put("/v1/projects/mayuaycdtijbctgqbycg/config/database/postgres").
32+
Reply(http.StatusOK).
33+
JSON(api.PostgresConfigResponse{
34+
StatementTimeout: Ptr("10s"),
35+
})
2436
gock.New("https://api.supabase.com").
2537
Get("/v1/projects/mayuaycdtijbctgqbycg/postgrest").
2638
Reply(http.StatusOK).
@@ -50,6 +62,18 @@ func TestAccSettingsResource(t *testing.T) {
5062
SiteUrl: Ptr("http://localhost:3000"),
5163
})
5264
// Step 2: read
65+
gock.New("https://api.supabase.com").
66+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/database/postgres").
67+
Reply(http.StatusOK).
68+
JSON(api.PostgresConfigResponse{
69+
StatementTimeout: Ptr("10s"),
70+
})
71+
gock.New("https://api.supabase.com").
72+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/database/postgres").
73+
Reply(http.StatusOK).
74+
JSON(api.PostgresConfigResponse{
75+
StatementTimeout: Ptr("10s"),
76+
})
5377
gock.New("https://api.supabase.com").
5478
Get("/v1/projects/mayuaycdtijbctgqbycg/postgrest").
5579
Reply(http.StatusOK).
@@ -79,6 +103,24 @@ func TestAccSettingsResource(t *testing.T) {
79103
SiteUrl: Ptr("http://localhost:3000"),
80104
})
81105
// Step 3: update
106+
gock.New("https://api.supabase.com").
107+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/database/postgres").
108+
Reply(http.StatusOK).
109+
JSON(api.PostgresConfigResponse{
110+
StatementTimeout: Ptr("10s"),
111+
})
112+
gock.New("https://api.supabase.com").
113+
Put("/v1/projects/mayuaycdtijbctgqbycg/config/database/postgres").
114+
Reply(http.StatusOK).
115+
JSON(api.PostgresConfigResponse{
116+
StatementTimeout: Ptr("20s"),
117+
})
118+
gock.New("https://api.supabase.com").
119+
Get("/v1/projects/mayuaycdtijbctgqbycg/config/database/postgres").
120+
Reply(http.StatusOK).
121+
JSON(api.PostgresConfigResponse{
122+
StatementTimeout: Ptr("20s"),
123+
})
82124
gock.New("https://api.supabase.com").
83125
Get("/v1/projects/mayuaycdtijbctgqbycg/postgrest").
84126
Reply(http.StatusOK).
@@ -158,6 +200,10 @@ const testAccSettingsResourceConfig = `
158200
resource "supabase_settings" "production" {
159201
project_ref = "mayuaycdtijbctgqbycg"
160202
203+
database = jsonencode({
204+
statement_timeout = "20s"
205+
})
206+
161207
api = jsonencode({
162208
db_schema = "public,storage,graphql_public"
163209
db_extra_search_path = "public,extensions"

0 commit comments

Comments
 (0)