@@ -7,13 +7,17 @@ import (
77 "context"
88 "fmt"
99 "net/http"
10+ "slices"
11+ "strings"
1012
13+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
1114 "github.com/hashicorp/terraform-plugin-framework/diag"
1215 "github.com/hashicorp/terraform-plugin-framework/path"
1316 "github.com/hashicorp/terraform-plugin-framework/resource"
1417 "github.com/hashicorp/terraform-plugin-framework/resource/schema"
1518 "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1619 "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
20+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
1721 "github.com/hashicorp/terraform-plugin-framework/types"
1822 "github.com/hashicorp/terraform-plugin-log/tflog"
1923 "github.com/supabase/cli/pkg/api"
@@ -71,6 +75,30 @@ func (r *ProjectResource) Schema(ctx context.Context, req resource.SchemaRequest
7175 "instance_size" : schema.StringAttribute {
7276 MarkdownDescription : "Desired instance size of the project" ,
7377 Optional : true ,
78+ Validators : []validator.String {
79+ stringvalidator .OneOf (
80+ string (api .V1CreateProjectBodyDesiredInstanceSizeLarge ),
81+ string (api .V1CreateProjectBodyDesiredInstanceSizeMedium ),
82+ string (api .V1CreateProjectBodyDesiredInstanceSizeMicro ),
83+ string (api .V1CreateProjectBodyDesiredInstanceSizeN12xlarge ),
84+ string (api .V1CreateProjectBodyDesiredInstanceSizeN16xlarge ),
85+ string (api .V1CreateProjectBodyDesiredInstanceSizeN24xlarge ),
86+ string (api .V1CreateProjectBodyDesiredInstanceSizeN24xlargeHighMemory ),
87+ string (api .V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedCpu ),
88+ string (api .V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedMemory ),
89+ string (api .V1CreateProjectBodyDesiredInstanceSizeN2xlarge ),
90+ string (api .V1CreateProjectBodyDesiredInstanceSizeN48xlarge ),
91+ string (api .V1CreateProjectBodyDesiredInstanceSizeN48xlargeHighMemory ),
92+ string (api .V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedCpu ),
93+ string (api .V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedMemory ),
94+ string (api .V1CreateProjectBodyDesiredInstanceSizeN4xlarge ),
95+ string (api .V1CreateProjectBodyDesiredInstanceSizeN8xlarge ),
96+ string (api .V1CreateProjectBodyDesiredInstanceSizeNano ),
97+ string (api .V1CreateProjectBodyDesiredInstanceSizePico ),
98+ string (api .V1CreateProjectBodyDesiredInstanceSizeSmall ),
99+ string (api .V1CreateProjectBodyDesiredInstanceSizeXlarge ),
100+ ),
101+ },
74102 },
75103 "id" : schema.StringAttribute {
76104 MarkdownDescription : "Project identifier" ,
@@ -142,17 +170,41 @@ func (r *ProjectResource) Read(ctx context.Context, req resource.ReadRequest, re
142170}
143171
144172func (r * ProjectResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
145- var data ProjectResourceModel
173+ var plan , state ProjectResourceModel
146174
147- // Read Terraform plan data into the model
148- resp .Diagnostics .Append (req .Plan .Get (ctx , & data )... )
175+ resp .Diagnostics .Append (req .Plan .Get (ctx , & plan )... )
176+ resp .Diagnostics .Append (req .State .Get (ctx , & state )... )
177+ if resp .Diagnostics .HasError () {
178+ return
179+ }
180+
181+ if ! plan .Name .Equal (state .Name ) {
182+ resp .Diagnostics .AddAttributeError (path .Root ("name" ), "Client Error" , "Update is not supported for this attribute" )
183+ return
184+ }
185+ if ! plan .DatabasePassword .Equal (state .DatabasePassword ) {
186+ resp .Diagnostics .AddAttributeError (path .Root ("database_password" ), "Client Error" , "Update is not supported for this attribute" )
187+ return
188+ }
189+ if ! plan .Region .Equal (state .Region ) {
190+ resp .Diagnostics .AddAttributeError (path .Root ("region" ), "Client Error" , "Update is not supported for this attribute" )
191+ return
192+ }
193+ if ! plan .OrganizationId .Equal (state .OrganizationId ) {
194+ resp .Diagnostics .AddAttributeError (path .Root ("organization_id" ), "Client Error" , "Update is not supported for this attribute" )
195+ return
196+ }
197+ if ! plan .InstanceSize .Equal (state .InstanceSize ) {
198+ resp .Diagnostics .Append (updateInstanceSize (ctx , & plan , & state , r .client )... )
199+ return
200+ }
201+
202+ resp .Diagnostics .Append (updateInstanceSize (ctx , & plan , & state , r .client )... )
149203 if resp .Diagnostics .HasError () {
150204 return
151205 }
152206
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 ))
207+ resp .Diagnostics .Append (resp .State .Set (ctx , & plan )... )
156208}
157209
158210func (r * ProjectResource ) Delete (ctx context.Context , req resource.DeleteRequest , resp * resource.DeleteResponse ) {
@@ -218,28 +270,46 @@ func createProject(ctx context.Context, data *ProjectResourceModel, client *api.
218270}
219271
220272func readProject (ctx context.Context , data * ProjectResourceModel , client * api.ClientWithResponses ) diag.Diagnostics {
221- httpResp , err := client .V1ListAllProjectsWithResponse (ctx )
273+ projectResp , err := client .V1GetProjectWithResponse (ctx , data . Id . ValueString () )
222274 if err != nil {
223275 msg := fmt .Sprintf ("Unable to read project, got error: %s" , err )
224276 return diag.Diagnostics {diag .NewErrorDiagnostic ("Client Error" , msg )}
225277 }
226278
227- if httpResp .JSON200 == nil {
228- msg := fmt .Sprintf ("Unable to read project, got status %d: %s" , httpResp .StatusCode (), httpResp .Body )
279+ if projectResp .JSON200 == nil {
280+ msg := fmt .Sprintf ("Unable to read project, got status %d: %s" , projectResp .StatusCode (), projectResp .Body )
229281 return diag.Diagnostics {diag .NewErrorDiagnostic ("Client Error" , msg )}
230282 }
231283
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
284+ project := projectResp .JSON200
285+ data .OrganizationId = types .StringValue (project .OrganizationId )
286+ data .Name = types .StringValue (project .Name )
287+ data .Region = types .StringValue (project .Region )
288+
289+ addonsResp , err := client .V1ListProjectAddonsWithResponse (ctx , project .Id )
290+ if err != nil {
291+ msg := fmt .Sprintf ("Unable to read project addons, got error: %s" , err )
292+ return diag.Diagnostics {diag .NewErrorDiagnostic ("Client Error" , msg )}
293+ }
294+
295+ if addonsResp .JSON200 != nil {
296+ for _ , addon := range addonsResp .JSON200 .SelectedAddons {
297+ if addon .Type != api .ComputeInstance {
298+ continue
299+ }
300+
301+ val , err := addon .Variant .Id .AsListProjectAddonsResponseSelectedAddonsVariantId0 ()
302+ if err != nil {
303+ tflog .Warn (ctx , "failed to get addon ID as compute_instance" , map [string ]any {
304+ "addon" : addon ,
305+ })
306+ continue
307+ }
308+
309+ data .InstanceSize = types .StringValue (strings .TrimPrefix (string (val ), "ci_" ))
238310 }
239311 }
240312
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 ()))
243313 return nil
244314}
245315
@@ -262,3 +332,48 @@ func deleteProject(ctx context.Context, data *ProjectResourceModel, client *api.
262332
263333 return nil
264334}
335+
336+ var freeInstanceSizes = []string {
337+ string (api .V1CreateProjectBodyDesiredInstanceSizeNano ),
338+ string (api .V1CreateProjectBodyDesiredInstanceSizePico ),
339+ }
340+
341+ func updateInstanceSize (ctx context.Context , plan * ProjectResourceModel , state * ProjectResourceModel , client * api.ClientWithResponses ) diag.Diagnostics {
342+ if plan .InstanceSize .Equal (state .InstanceSize ) {
343+ return diag.Diagnostics {}
344+ }
345+
346+ requestedInstanceSize := plan .InstanceSize .ValueString ()
347+ if requestedInstanceSize != "" &&
348+ ! slices .Contains (freeInstanceSizes , state .InstanceSize .ValueString ()) &&
349+ slices .Contains (freeInstanceSizes , requestedInstanceSize ) {
350+ msg := fmt .Sprintf ("A paid project cannot be downgraded to %s instance size" , requestedInstanceSize )
351+ return diag.Diagnostics {diag .NewErrorDiagnostic ("Configuration Error" , msg )}
352+ }
353+
354+ addon := api.ApplyProjectAddonBody_AddonVariant {}
355+ variant := api .ApplyProjectAddonBodyAddonVariant0 ("ci_" + plan .InstanceSize .ValueString ())
356+ if err := addon .FromApplyProjectAddonBodyAddonVariant0 (variant ); err != nil {
357+ return diag.Diagnostics {diag .NewErrorDiagnostic (
358+ "Internal Error" ,
359+ fmt .Sprintf ("Failed to configure instance size: %s" , err ),
360+ )}
361+ }
362+ body := api.V1ApplyProjectAddonJSONRequestBody {
363+ AddonType : api .ApplyProjectAddonBodyAddonTypeComputeInstance ,
364+ AddonVariant : addon ,
365+ }
366+
367+ httpResp , err := client .V1ApplyProjectAddonWithResponse (ctx , plan .Id .ValueString (), body )
368+ if err != nil {
369+ msg := fmt .Sprintf ("Unable to update project, got error: %s" , err )
370+ return diag.Diagnostics {diag .NewErrorDiagnostic ("Client Error" , msg )}
371+ }
372+
373+ if httpResp .StatusCode () != http .StatusOK {
374+ msg := fmt .Sprintf ("Unable to update project, got error: %s" , string (httpResp .Body ))
375+ return diag.Diagnostics {diag .NewErrorDiagnostic ("Client Error" , msg )}
376+ }
377+
378+ return nil
379+ }
0 commit comments