Skip to content
Merged
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
5 changes: 1 addition & 4 deletions docs/resources/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ resource "supabase_project" "test" {
instance_size = "micro"
lifecycle {
ignore_changes = [
database_password,
instance_size,
]
ignore_changes = [database_password]
}
}
```
Expand Down
3 changes: 2 additions & 1 deletion docs/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@
"type": "string",
"description": "Desired instance size of the project",
"description_kind": "markdown",
"optional": true
"optional": true,
"computed": true
},
"name": {
"type": "string",
Expand Down
5 changes: 1 addition & 4 deletions examples/resources/supabase_project/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ resource "supabase_project" "test" {
instance_size = "micro"

lifecycle {
ignore_changes = [
database_password,
instance_size,
]
ignore_changes = [database_password]
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/hashicorp/terraform-plugin-docs v0.24.0
github.com/hashicorp/terraform-plugin-framework v1.16.1
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0
github.com/hashicorp/terraform-plugin-go v0.29.0
github.com/hashicorp/terraform-plugin-log v0.10.0
github.com/hashicorp/terraform-plugin-testing v1.13.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ github.com/hashicorp/terraform-plugin-framework v1.16.1 h1:1+zwFm3MEqd/0K3YBB2v9
github.com/hashicorp/terraform-plugin-framework v1.16.1/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y=
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 h1:SJXL5FfJJm17554Kpt9jFXngdM6fXbnUnZ6iT2IeiYA=
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0/go.mod h1:p0phD0IYhsu9bR4+6OetVvvH59I6LwjXGnTVEr8ox6E=
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0 h1:Zz3iGgzxe/1XBkooZCewS0nJAaCFPFPHdNJd8FgE4Ow=
github.com/hashicorp/terraform-plugin-framework-validators v0.19.0/go.mod h1:GBKTNGbGVJohU03dZ7U8wHqc2zYnMUawgCN+gC0itLc=
github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU=
github.com/hashicorp/terraform-plugin-go v0.29.0/go.mod h1:vYZbIyvxyy0FWSmDHChCqKvI40cFTDGSb3D8D70i9GM=
github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g=
Expand Down
151 changes: 132 additions & 19 deletions internal/provider/project_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import (
"context"
"fmt"
"net/http"
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/supabase/cli/pkg/api"
Expand Down Expand Up @@ -71,6 +74,34 @@ func (r *ProjectResource) Schema(ctx context.Context, req resource.SchemaRequest
"instance_size": schema.StringAttribute{
MarkdownDescription: "Desired instance size of the project",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
stringvalidator.OneOf(
string(api.V1CreateProjectBodyDesiredInstanceSizeLarge),
string(api.V1CreateProjectBodyDesiredInstanceSizeMedium),
string(api.V1CreateProjectBodyDesiredInstanceSizeMicro),
string(api.V1CreateProjectBodyDesiredInstanceSizeN12xlarge),
string(api.V1CreateProjectBodyDesiredInstanceSizeN16xlarge),
string(api.V1CreateProjectBodyDesiredInstanceSizeN24xlarge),
string(api.V1CreateProjectBodyDesiredInstanceSizeN24xlargeHighMemory),
string(api.V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedCpu),
string(api.V1CreateProjectBodyDesiredInstanceSizeN24xlargeOptimizedMemory),
string(api.V1CreateProjectBodyDesiredInstanceSizeN2xlarge),
string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlarge),
string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlargeHighMemory),
string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedCpu),
string(api.V1CreateProjectBodyDesiredInstanceSizeN48xlargeOptimizedMemory),
string(api.V1CreateProjectBodyDesiredInstanceSizeN4xlarge),
string(api.V1CreateProjectBodyDesiredInstanceSizeN8xlarge),
string(api.V1CreateProjectBodyDesiredInstanceSizeNano),
string(api.V1CreateProjectBodyDesiredInstanceSizePico),
string(api.V1CreateProjectBodyDesiredInstanceSizeSmall),
string(api.V1CreateProjectBodyDesiredInstanceSizeXlarge),
),
},
},
"id": schema.StringAttribute{
MarkdownDescription: "Project identifier",
Expand Down Expand Up @@ -110,12 +141,17 @@ func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest
return
}

tflog.Trace(ctx, "create project")
resp.Diagnostics.Append(createProject(ctx, &data, r.client)...)
if resp.Diagnostics.HasError() {
return
}

tflog.Trace(ctx, "create project")
tflog.Trace(ctx, "read up to date project")
resp.Diagnostics.Append(readProject(ctx, &data, r.client)...)
if resp.Diagnostics.HasError() {
return
}

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

func (r *ProjectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data ProjectResourceModel
var plan, state ProjectResourceModel

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

// TODO: allow api to update project resource
msg := fmt.Sprintf("Update is not supported for project resource: %s", data.Id.ValueString())
resp.Diagnostics.Append(diag.NewErrorDiagnostic("Client Error", msg))
// required attributes
if !plan.Name.Equal(state.Name) {
resp.Diagnostics.AddAttributeError(path.Root("name"), "Client Error", "Update is not supported for this attribute")
return
}
if !plan.DatabasePassword.Equal(state.DatabasePassword) {
resp.Diagnostics.AddAttributeError(path.Root("database_password"), "Client Error", "Update is not supported for this attribute")
return
}
if !plan.Region.Equal(state.Region) {
resp.Diagnostics.AddAttributeError(path.Root("region"), "Client Error", "Update is not supported for this attribute")
return
}
if !plan.OrganizationId.Equal(state.OrganizationId) {
resp.Diagnostics.AddAttributeError(path.Root("organization_id"), "Client Error", "Update is not supported for this attribute")
return
}

// optional attributes
if !plan.InstanceSize.IsNull() && !plan.InstanceSize.Equal(state.InstanceSize) {
resp.Diagnostics.Append(updateInstanceSize(ctx, &plan, r.client)...)
}

if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (r *ProjectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
Expand Down Expand Up @@ -198,7 +258,7 @@ func createProject(ctx context.Context, data *ProjectResourceModel, client *api.
DbPass: data.DatabasePassword.ValueString(),
RegionSelection: &region,
}
if !data.InstanceSize.IsNull() {
if !data.InstanceSize.IsUnknown() && !data.InstanceSize.IsNull() {
body.DesiredInstanceSize = Ptr(api.V1CreateProjectBodyDesiredInstanceSize(data.InstanceSize.ValueString()))
}

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

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

if httpResp.JSON200 == nil {
msg := fmt.Sprintf("Unable to read project, got status %d: %s", httpResp.StatusCode(), httpResp.Body)
if projectResp.StatusCode() == http.StatusNotFound {
return nil
}

if projectResp.JSON200 == nil {
msg := fmt.Sprintf("Unable to read project, got status %d: %s", projectResp.StatusCode(), projectResp.Body)
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
}

project := projectResp.JSON200
data.OrganizationId = types.StringValue(project.OrganizationId)
data.Name = types.StringValue(project.Name)
data.Region = types.StringValue(project.Region)
data.InstanceSize = types.StringNull()

addonsResp, err := client.V1ListProjectAddonsWithResponse(ctx, project.Id)
if err != nil {
msg := fmt.Sprintf("Unable to read project addons, got error: %s", err)
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
}

for _, project := range *httpResp.JSON200 {
if project.Id == data.Id.ValueString() {
data.OrganizationId = types.StringValue(project.OrganizationId)
data.Name = types.StringValue(project.Name)
data.Region = types.StringValue(project.Region)
return nil
if addonsResp.JSON200 == nil {
msg := fmt.Sprintf("Unable to read project addons, got error: %s", string(addonsResp.Body))
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
}

for _, addon := range addonsResp.JSON200.SelectedAddons {
if addon.Type != api.ComputeInstance {
continue
}

val, err := addon.Variant.Id.AsListProjectAddonsResponseSelectedAddonsVariantId0()
if err != nil {
msg := fmt.Sprintf("Unable to read compute instance addon, got error: %s", err)
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
}

data.InstanceSize = types.StringValue(strings.TrimPrefix(string(val), "ci_"))
break
}

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

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

return nil
}

func updateInstanceSize(ctx context.Context, plan *ProjectResourceModel, client *api.ClientWithResponses) diag.Diagnostics {
addon := api.ApplyProjectAddonBody_AddonVariant{}
variant := api.ApplyProjectAddonBodyAddonVariant0("ci_" + plan.InstanceSize.ValueString())
if err := addon.FromApplyProjectAddonBodyAddonVariant0(variant); err != nil {
return diag.Diagnostics{diag.NewErrorDiagnostic(
"Internal Error",
fmt.Sprintf("Failed to configure instance size: %s", err),
)}
}
body := api.V1ApplyProjectAddonJSONRequestBody{
AddonType: api.ApplyProjectAddonBodyAddonTypeComputeInstance,
AddonVariant: addon,
}

httpResp, err := client.V1ApplyProjectAddonWithResponse(ctx, plan.Id.ValueString(), body)
if err != nil {
msg := fmt.Sprintf("Unable to update project, got error: %s", err)
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
}

if httpResp.StatusCode() != http.StatusOK {
msg := fmt.Sprintf("Unable to update project, got error: %s", string(httpResp.Body))
return diag.Diagnostics{diag.NewErrorDiagnostic("Client Error", msg)}
}

return nil
}
Loading