Skip to content

Commit 14c8a10

Browse files
authored
feat: allow updating project's instance size (#255)
1 parent 0209b09 commit 14c8a10

File tree

7 files changed

+268
-37
lines changed

7 files changed

+268
-37
lines changed

docs/resources/project.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ resource "supabase_project" "test" {
2121
instance_size = "micro"
2222
2323
lifecycle {
24-
ignore_changes = [
25-
database_password,
26-
instance_size,
27-
]
24+
ignore_changes = [database_password]
2825
}
2926
}
3027
```

docs/schema.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@
138138
"type": "string",
139139
"description": "Desired instance size of the project",
140140
"description_kind": "markdown",
141-
"optional": true
141+
"optional": true,
142+
"computed": true
142143
},
143144
"name": {
144145
"type": "string",

examples/resources/supabase_project/resource.tf

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ resource "supabase_project" "test" {
66
instance_size = "micro"
77

88
lifecycle {
9-
ignore_changes = [
10-
database_password,
11-
instance_size,
12-
]
9+
ignore_changes = [database_password]
1310
}
1411
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/hashicorp/terraform-plugin-docs v0.24.0
88
github.com/hashicorp/terraform-plugin-framework v1.16.1
99
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0
10+
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0
1011
github.com/hashicorp/terraform-plugin-go v0.29.0
1112
github.com/hashicorp/terraform-plugin-log v0.10.0
1213
github.com/hashicorp/terraform-plugin-testing v1.13.3

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ github.com/hashicorp/terraform-plugin-framework v1.16.1 h1:1+zwFm3MEqd/0K3YBB2v9
117117
github.com/hashicorp/terraform-plugin-framework v1.16.1/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y=
118118
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 h1:SJXL5FfJJm17554Kpt9jFXngdM6fXbnUnZ6iT2IeiYA=
119119
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0/go.mod h1:p0phD0IYhsu9bR4+6OetVvvH59I6LwjXGnTVEr8ox6E=
120+
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 h1:Zz3iGgzxe/1XBkooZCewS0nJAaCFPFPHdNJd8FgE4Ow=
121+
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0/go.mod h1:GBKTNGbGVJohU03dZ7U8wHqc2zYnMUawgCN+gC0itLc=
120122
github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
121123
github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
122124
github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g=

internal/provider/project_resource.go

Lines changed: 132 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import (
77
"context"
88
"fmt"
99
"net/http"
10+
"strings"
1011

12+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1113
"github.com/hashicorp/terraform-plugin-framework/diag"
1214
"github.com/hashicorp/terraform-plugin-framework/path"
1315
"github.com/hashicorp/terraform-plugin-framework/resource"
1416
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1517
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1618
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
19+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
1720
"github.com/hashicorp/terraform-plugin-framework/types"
1821
"github.com/hashicorp/terraform-plugin-log/tflog"
1922
"github.com/supabase/cli/pkg/api"
@@ -71,6 +74,34 @@ func (r *ProjectResource) Schema(ctx context.Context, req resource.SchemaRequest
7174
"instance_size": schema.StringAttribute{
7275
MarkdownDescription: "Desired instance size of the project",
7376
Optional: true,
77+
Computed: true,
78+
PlanModifiers: []planmodifier.String{
79+
stringplanmodifier.UseStateForUnknown(),
80+
},
81+
Validators: []validator.String{
82+
stringvalidator.OneOf(
83+
string(api.V1CreateProjectBodyDesiredInstanceSizeLarge),
84+
string(api.V1CreateProjectBodyDesiredInstanceSizeMedium),
85+
string(api.V1CreateProjectBodyDesiredInstanceSizeMicro),
86+
string(api.V1CreateProjectBodyDesiredInstanceSizeN12xlarge),
87+
string(api.V1CreateProjectBodyDesiredInstanceSizeN16xlarge),
88+
string(api.V1CreateProjectBodyDesiredInstanceSizeN24xlarge),
89+
string(api.V1CreateProjectBodyDesiredInstanceSizeN24xlargeHighMemory),
90+
string(api.V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedCpu),
91+
string(api.V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedMemory),
92+
string(api.V1CreateProjectBodyDesiredInstanceSizeN2xlarge),
93+
string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlarge),
94+
string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlargeHighMemory),
95+
string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedCpu),
96+
string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedMemory),
97+
string(api.V1CreateProjectBodyDesiredInstanceSizeN4xlarge),
98+
string(api.V1CreateProjectBodyDesiredInstanceSizeN8xlarge),
99+
string(api.V1CreateProjectBodyDesiredInstanceSizeNano),
100+
string(api.V1CreateProjectBodyDesiredInstanceSizePico),
101+
string(api.V1CreateProjectBodyDesiredInstanceSizeSmall),
102+
string(api.V1CreateProjectBodyDesiredInstanceSizeXlarge),
103+
),
104+
},
74105
},
75106
"id": schema.StringAttribute{
76107
MarkdownDescription: "Project identifier",
@@ -110,12 +141,17 @@ func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest
110141
return
111142
}
112143

144+
tflog.Trace(ctx, "create project")
113145
resp.Diagnostics.Append(createProject(ctx, &data, r.client)...)
114146
if resp.Diagnostics.HasError() {
115147
return
116148
}
117149

118-
tflog.Trace(ctx, "create project")
150+
tflog.Trace(ctx, "read up to date project")
151+
resp.Diagnostics.Append(readProject(ctx, &data, r.client)...)
152+
if resp.Diagnostics.HasError() {
153+
return
154+
}
119155

120156
// Save data into Terraform state
121157
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
@@ -142,17 +178,41 @@ func (r *ProjectResource) Read(ctx context.Context, req resource.ReadRequest, re
142178
}
143179

144180
func (r *ProjectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
145-
var data ProjectResourceModel
181+
var plan, state ProjectResourceModel
146182

147-
// Read Terraform plan data into the model
148-
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
183+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
184+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
149185
if resp.Diagnostics.HasError() {
150186
return
151187
}
152188

153-
// TODO: allow api to update project resource
154-
msg := fmt.Sprintf("Update is not supported for project resource: %s", data.Id.ValueString())
155-
resp.Diagnostics.Append(diag.NewErrorDiagnostic("Client Error", msg))
189+
// required attributes
190+
if !plan.Name.Equal(state.Name) {
191+
resp.Diagnostics.AddAttributeError(path.Root("name"), "Client Error", "Update is not supported for this attribute")
192+
return
193+
}
194+
if !plan.DatabasePassword.Equal(state.DatabasePassword) {
195+
resp.Diagnostics.AddAttributeError(path.Root("database_password"), "Client Error", "Update is not supported for this attribute")
196+
return
197+
}
198+
if !plan.Region.Equal(state.Region) {
199+
resp.Diagnostics.AddAttributeError(path.Root("region"), "Client Error", "Update is not supported for this attribute")
200+
return
201+
}
202+
if !plan.OrganizationId.Equal(state.OrganizationId) {
203+
resp.Diagnostics.AddAttributeError(path.Root("organization_id"), "Client Error", "Update is not supported for this attribute")
204+
return
205+
}
206+
207+
// optional attributes
208+
if !plan.InstanceSize.IsNull() && !plan.InstanceSize.Equal(state.InstanceSize) {
209+
resp.Diagnostics.Append(updateInstanceSize(ctx, &plan, r.client)...)
210+
}
211+
212+
if resp.Diagnostics.HasError() {
213+
return
214+
}
215+
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
156216
}
157217

158218
func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
@@ -198,7 +258,7 @@ func createProject(ctx context.Context, data *ProjectResourceModel, client *api.
198258
DbPass: data.DatabasePassword.ValueString(),
199259
RegionSelection: &region,
200260
}
201-
if !data.InstanceSize.IsNull() {
261+
if !data.InstanceSize.IsUnknown() && !data.InstanceSize.IsNull() {
202262
body.DesiredInstanceSize = Ptr(api.V1CreateProjectBodyDesiredInstanceSize(data.InstanceSize.ValueString()))
203263
}
204264

@@ -218,28 +278,53 @@ func createProject(ctx context.Context, data *ProjectResourceModel, client *api.
218278
}
219279

220280
func readProject(ctx context.Context, data *ProjectResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
221-
httpResp, err := client.V1ListAllProjectsWithResponse(ctx)
281+
projectResp, err := client.V1GetProjectWithResponse(ctx, data.Id.ValueString())
222282
if err != nil {
223283
msg := fmt.Sprintf("Unable to read project, got error: %s", err)
224284
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
225285
}
226286

227-
if httpResp.JSON200 == nil {
228-
msg := fmt.Sprintf("Unable to read project, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
287+
if projectResp.StatusCode() == http.StatusNotFound {
288+
return nil
289+
}
290+
291+
if projectResp.JSON200 == nil {
292+
msg := fmt.Sprintf("Unable to read project, got status %d: %s", projectResp.StatusCode(), projectResp.Body)
293+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
294+
}
295+
296+
project := projectResp.JSON200
297+
data.OrganizationId = types.StringValue(project.OrganizationId)
298+
data.Name = types.StringValue(project.Name)
299+
data.Region = types.StringValue(project.Region)
300+
data.InstanceSize = types.StringNull()
301+
302+
addonsResp, err := client.V1ListProjectAddonsWithResponse(ctx, project.Id)
303+
if err != nil {
304+
msg := fmt.Sprintf("Unable to read project addons, got error: %s", err)
229305
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
230306
}
231307

232-
for _, project := range *httpResp.JSON200 {
233-
if project.Id == data.Id.ValueString() {
234-
data.OrganizationId = types.StringValue(project.OrganizationId)
235-
data.Name = types.StringValue(project.Name)
236-
data.Region = types.StringValue(project.Region)
237-
return nil
308+
if addonsResp.JSON200 == nil {
309+
msg := fmt.Sprintf("Unable to read project addons, got error: %s", string(addonsResp.Body))
310+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
311+
}
312+
313+
for _, addon := range addonsResp.JSON200.SelectedAddons {
314+
if addon.Type != api.ComputeInstance {
315+
continue
316+
}
317+
318+
val, err := addon.Variant.Id.AsListProjectAddonsResponseSelectedAddonsVariantId0()
319+
if err != nil {
320+
msg := fmt.Sprintf("Unable to read compute instance addon, got error: %s", err)
321+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
238322
}
323+
324+
data.InstanceSize = types.StringValue(strings.TrimPrefix(string(val), "ci_"))
325+
break
239326
}
240327

241-
// Not finding a project means our local state is stale. Return no error to allow TF to refresh its state.
242-
tflog.Trace(ctx, fmt.Sprintf("project not found: %s", data.Id.ValueString()))
243328
return nil
244329
}
245330

@@ -262,3 +347,31 @@ func deleteProject(ctx context.Context, data *ProjectResourceModel, client *api.
262347

263348
return nil
264349
}
350+
351+
func updateInstanceSize(ctx context.Context, plan *ProjectResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
352+
addon := api.ApplyProjectAddonBody_AddonVariant{}
353+
variant := api.ApplyProjectAddonBodyAddonVariant0("ci_" + plan.InstanceSize.ValueString())
354+
if err := addon.FromApplyProjectAddonBodyAddonVariant0(variant); err != nil {
355+
return diag.Diagnostics{diag.NewErrorDiagnostic(
356+
"Internal Error",
357+
fmt.Sprintf("Failed to configure instance size: %s", err),
358+
)}
359+
}
360+
body := api.V1ApplyProjectAddonJSONRequestBody{
361+
AddonType: api.ApplyProjectAddonBodyAddonTypeComputeInstance,
362+
AddonVariant: addon,
363+
}
364+
365+
httpResp, err := client.V1ApplyProjectAddonWithResponse(ctx, plan.Id.ValueString(), body)
366+
if err != nil {
367+
msg := fmt.Sprintf("Unable to update project, got error: %s", err)
368+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
369+
}
370+
371+
if httpResp.StatusCode() != http.StatusOK {
372+
msg := fmt.Sprintf("Unable to update project, got error: %s", string(httpResp.Body))
373+
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
374+
}
375+
376+
return nil
377+
}

0 commit comments

Comments
 (0)