diff --git a/pkg/api/cluster_client.go b/pkg/api/cluster_client.go index bd688a7b..1ace84fb 100644 --- a/pkg/api/cluster_client.go +++ b/pkg/api/cluster_client.go @@ -40,7 +40,6 @@ func (c ClusterClient) Create(ctx context.Context, projectId string, model any) url := fmt.Sprintf("projects/%s/clusters", projectId) body, err := c.doRequest(ctx, http.MethodPost, url, bytes.NewBuffer(b)) - if err != nil { return "", err } @@ -49,6 +48,22 @@ func (c ClusterClient) Create(ctx context.Context, projectId string, model any) return response.Data.ClusterId, err } +func (c ClusterClient) ReadDeletedCluster(ctx context.Context, projectId, id string) (*models.Cluster, error) { + response := struct { + Data models.Cluster `json:"data"` + }{} + + url := fmt.Sprintf("projects/%s/deleted-clusters/%s", projectId, id) + body, err := c.doRequest(ctx, http.MethodGet, url, nil) + if err != nil { + return &response.Data, err + } + + err = json.Unmarshal(body, &response) + + return &response.Data, err +} + func (c ClusterClient) Read(ctx context.Context, projectId, id string) (*models.Cluster, error) { response := struct { Data models.Cluster `json:"data"` @@ -73,18 +88,18 @@ func (c ClusterClient) ReadByName(ctx context.Context, projectId, name string, m url := fmt.Sprintf("projects/%s/clusters?name=%s", projectId, name) body, err := c.doRequest(ctx, http.MethodGet, url, nil) if err != nil { - return &models.Cluster{}, err + return nil, err } if err := json.Unmarshal(body, &clusters); err != nil { - return &models.Cluster{}, err + return nil, err } if len(clusters.Data) != 1 { if most_recent { sort.Slice(clusters.Data, func(i, j int) bool { return clusters.Data[i].CreatedAt.Seconds > clusters.Data[j].CreatedAt.Seconds }) } else { - return &models.Cluster{}, ErrorClustersSameName + return nil, ErrorClustersSameName } } @@ -175,3 +190,47 @@ func (c ClusterClient) GetPeAllowedPrincipalIds(ctx context.Context, projectID s } return &response.Data, nil } + +func (c ClusterClient) RestoreCluster(ctx context.Context, projectId, clusterId string, model models.RestoreCluster) (string, error) { + response := struct { + Data struct { + ClusterId string `json:"clusterId"` + } `json:"data"` + }{} + + b, err := json.Marshal(model) + if err != nil { + return "", err + } + + url := fmt.Sprintf("projects/%s/clusters/%s/restore", projectId, clusterId) + body, err := c.doRequest(ctx, http.MethodPost, url, bytes.NewBuffer(b)) + if err != nil { + return "", err + } + + err = json.Unmarshal(body, &response) + return response.Data.ClusterId, err +} + +func (c ClusterClient) RestoreClusterFromDeleted(ctx context.Context, projectId, clusterId string, model models.RestoreCluster) (string, error) { + response := struct { + Data struct { + ClusterId string `json:"clusterId"` + } `json:"data"` + }{} + + b, err := json.Marshal(model) + if err != nil { + return "", err + } + + url := fmt.Sprintf("projects/%s/deleted-clusters/%s/restore", projectId, clusterId) + body, err := c.doRequest(ctx, http.MethodPost, url, bytes.NewBuffer(b)) + if err != nil { + return "", err + } + + err = json.Unmarshal(body, &response) + return response.Data.ClusterId, err +} diff --git a/pkg/models/restore_cluster.go b/pkg/models/restore_cluster.go new file mode 100644 index 00000000..8dc0f476 --- /dev/null +++ b/pkg/models/restore_cluster.go @@ -0,0 +1,27 @@ +package models + +import ( + commonApi "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/models/common/api" +) + +type RestoreCluster struct { + AllowedIpRanges *[]AllowedIpRange `json:"allowedIpRanges,omitempty"` + BackupRetentionPeriod *string `json:"backupRetentionPeriod,omitempty"` + ClusterArchitecture *Architecture `json:"clusterArchitecture,omitempty" mapstructure:"cluster_architecture"` + ClusterName *string `json:"clusterName,omitempty"` + ClusterType *string `json:"clusterType,omitempty"` + CSPAuth *bool `json:"cspAuth,omitempty"` + InstanceType *InstanceType `json:"instanceType,omitempty"` + Password *string `json:"password,omitempty"` + PgConfig *[]KeyValue `json:"pgConfig,omitempty"` + Phase *string `json:"phase,omitempty"` + ReadOnlyConnections *bool `json:"readOnlyConnections,omitempty"` + Region *Region `json:"region,omitempty"` + ResizingPvc []string `json:"resizingPvc,omitempty"` + Storage *Storage `json:"storage,omitempty"` + MaintenanceWindow *commonApi.MaintenanceWindow `json:"maintenanceWindow,omitempty"` + ServiceAccountIds *[]string `json:"serviceAccountIds,omitempty"` + PeAllowedPrincipalIds *[]string `json:"peAllowedPrincipalIds,omitempty"` + SuperuserAccess *bool `json:"superuserAccess,omitempty"` + RestorePoint *string `json:"selectedRestorePointInTime,omitempty"` +} diff --git a/pkg/plan_modifier/restore_cluster_id.go b/pkg/plan_modifier/restore_cluster_id.go new file mode 100644 index 00000000..967ee212 --- /dev/null +++ b/pkg/plan_modifier/restore_cluster_id.go @@ -0,0 +1,43 @@ +package plan_modifier + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" +) + +// CustomRestoreClusterId returns a plan modifier that copies a known prior state +// value into the planned value. Use this when it is known that an unconfigured +// value will remain the same after a resource update. +// +// To prevent Terraform errors, the framework automatically sets unconfigured +// and Computed attributes to an unknown value "(known after apply)" on update. +// Using this plan modifier will instead display the prior state value in the +// plan, unless a prior plan modifier adjusts the value. +func CustomRestoreClusterId() planmodifier.String { + return customRestoreClusterIdModifier{} +} + +// customRestoreClusterIdModifier implements the plan modifier. +type customRestoreClusterIdModifier struct{} + +// Description returns a human-readable description of the plan modifier. +func (m customRestoreClusterIdModifier) Description(_ context.Context) string { + return "Once set, the value of this attribute in state will not change." +} + +// MarkdownDescription returns a markdown description of the plan modifier. +func (m customRestoreClusterIdModifier) MarkdownDescription(_ context.Context) string { + return "Once set, the value of this attribute in state will not change." +} + +// PlanModifyString implements the plan modification logic. +func (m customRestoreClusterIdModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if !req.PlanValue.IsNull() { + resp.Diagnostics.AddWarning( + "You are restoring cluster", + fmt.Sprint("You are restoring a cluster. After restoring the cluster, please remove field 'restore_cluster_id' and optionally remove fields 'restore_from_deleted' and 'restore_point' from the config"), + ) + } +} diff --git a/pkg/provider/resource_cluster.go b/pkg/provider/resource_cluster.go index d6b4cee4..826b9e15 100644 --- a/pkg/provider/resource_cluster.go +++ b/pkg/provider/resource_cluster.go @@ -69,8 +69,12 @@ type ClusterResourceModel struct { ServiceAccountIds types.Set `tfsdk:"service_account_ids"` PeAllowedPrincipalIds types.Set `tfsdk:"pe_allowed_principal_ids"` SuperuserAccess types.Bool `tfsdk:"superuser_access"` - FromDeleted *bool `tfsdk:"from_deleted"` + ImportFromDeleted types.Bool `tfsdk:"import_from_deleted"` + RestoreFromDeleted *bool `tfsdk:"restore_from_deleted"` + RestoreClusterId *string `tfsdk:"restore_cluster_id"` + RestorePoint types.String `tfsdk:"restore_point"` Pgvector types.Bool `tfsdk:"pgvector"` + MostRecent types.Bool `tfsdk:"most_recent"` Timeouts timeouts.Value `tfsdk:"timeouts"` } @@ -238,6 +242,10 @@ func (c *clusterResource) Schema(ctx context.Context, req resource.SchemaRequest MarkdownDescription: "Name of the cluster.", Required: true, }, + "most_recent": schema.BoolAttribute{ + MarkdownDescription: "Show the most recent cluster when there are multiple clusters with the same name.", + Optional: true, + }, "phase": schema.StringAttribute{ MarkdownDescription: "Current phase of the cluster.", Computed: true, @@ -393,10 +401,23 @@ func (c *clusterResource) Schema(ctx context.Context, req resource.SchemaRequest Optional: true, Computed: true, }, - "from_deleted": schema.BoolAttribute{ + "import_from_deleted": schema.BoolAttribute{ + Description: "Used by import function only to import a deleted cluster", + Optional: true, + }, + "restore_from_deleted": schema.BoolAttribute{ Description: "For restoring a cluster. Specifies if the cluster you want to restore is deleted", Optional: true, }, + "restore_cluster_id": schema.StringAttribute{ + Description: "For restoring a cluster. Specifies the cluster id to restore", + Optional: true, + PlanModifiers: []planmodifier.String{plan_modifier.CustomRestoreClusterId()}, + }, + "restore_point": schema.StringAttribute{ + Description: "For restoring a cluster. Specifies restore point e.g. 2006-01-02T15:04:05-0700. Leave empty to restore from latest point", + Optional: true, + }, "pgvector": schema.BoolAttribute{ MarkdownDescription: "Is pgvector extension enabled. Adds support for vector storage and vector similarity search to Postgres.", Optional: true, @@ -424,12 +445,45 @@ func (c *clusterResource) Create(ctx context.Context, req resource.CreateRequest return } - clusterId, err := c.client.Create(ctx, config.ProjectId, clusterModel) - if err != nil { - if !appendDiagFromBAErr(err, &resp.Diagnostics) { - resp.Diagnostics.AddError("Error creating cluster API request", err.Error()) + var clusterId string + + if config.RestoreClusterId != nil { + var restoreModel models.RestoreCluster + err := utils.CopyObjectJson(clusterModel, &restoreModel) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error copy object json in restore cluster API request", err.Error()) + } + return } - return + + if config.RestoreFromDeleted != nil && *config.RestoreFromDeleted { + clusterId, err = c.client.RestoreClusterFromDeleted(ctx, config.ProjectId, *config.RestoreClusterId, restoreModel) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error restore cluster API request", err.Error()) + } + return + } + } else { + clusterId, err = c.client.RestoreCluster(ctx, config.ProjectId, *config.RestoreClusterId, restoreModel) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error restore cluster API request", err.Error()) + } + return + } + } + + } else { + clusterId, err = c.client.Create(ctx, config.ProjectId, clusterModel) + if err != nil { + if !appendDiagFromBAErr(err, &resp.Diagnostics) { + resp.Diagnostics.AddError("Error creating cluster API request", err.Error()) + } + return + } + } config.ClusterId = &clusterId @@ -537,7 +591,7 @@ func (c *clusterResource) Delete(ctx context.Context, req resource.DeleteRequest func (c *clusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, "/") - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + if len(idParts) < 2 || idParts[0] == "" || idParts[1] == "" { resp.Diagnostics.AddError( "Unexpected Import Identifier", fmt.Sprintf("Expected import identifier with format: project_id/cluster_id. Got: %q", req.ID), @@ -547,12 +601,27 @@ func (c *clusterResource) ImportState(ctx context.Context, req resource.ImportSt resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cluster_id"), idParts[1])...) + + if len(idParts) > 2 && idParts[2] == "import-from-deleted" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("import_from_deleted"), true)...) + } } func (c *clusterResource) read(ctx context.Context, clusterResource *ClusterResourceModel) error { - cluster, err := c.client.Read(ctx, clusterResource.ProjectId, *clusterResource.ClusterId) - if err != nil { - return err + var cluster *models.Cluster + + if clusterResource.ImportFromDeleted.ValueBool() { + deletedCluster, err := c.client.ReadDeletedCluster(ctx, clusterResource.ProjectId, *clusterResource.ClusterId) + if err != nil { + return err + } + cluster = deletedCluster + } else { + existingCluster, err := c.client.Read(ctx, clusterResource.ProjectId, *clusterResource.ClusterId) + if err != nil { + return err + } + cluster = existingCluster } connection, err := c.client.ConnectionString(ctx, clusterResource.ProjectId, *clusterResource.ClusterId)