From a4ff48119ab2e07d8e0aefd92c3adb06a47dc712 Mon Sep 17 00:00:00 2001 From: harshitha-yb Date: Sun, 26 Jan 2025 17:02:02 +0000 Subject: [PATCH] Support CRUD operations for PITR configs, PITR restore and clone --- go.mod | 4 +- go.sum | 4 +- managed/fflags/feature_flags.go | 6 + managed/models.go | 37 +++ managed/provider.go | 15 + managed/resource_pitr_clone.go | 329 ++++++++++++++++++++++ managed/resource_pitr_config.go | 464 +++++++++++++++++++++++++++++++ managed/resource_pitr_restore.go | 240 ++++++++++++++++ samples/cluster-single-region.tf | 28 ++ 9 files changed, 1123 insertions(+), 4 deletions(-) create mode 100644 managed/resource_pitr_clone.go create mode 100644 managed/resource_pitr_config.go create mode 100644 managed/resource_pitr_restore.go diff --git a/go.mod b/go.mod index 76e5ccb..0b18cce 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/yugabyte/terraform-provider-ybm go 1.22.7 -toolchain go1.23.1 +toolchain go1.23.4 require ( github.com/golang/mock v1.6.0 @@ -10,7 +10,7 @@ require ( github.com/hashicorp/terraform-plugin-framework-validators v0.4.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/sethvargo/go-retry v0.2.3 - github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250111091824-e051c979c3dd + github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250204064923-ec656335987b ) require github.com/stretchr/testify v1.8.2 // indirect diff --git a/go.sum b/go.sum index fb6efaf..cf3cdef 100644 --- a/go.sum +++ b/go.sum @@ -169,8 +169,8 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250111091824-e051c979c3dd h1:vuMsPasq6Uzoy5fBJ0HJz7/jiCuLhKRj+LOyNVolqCk= -github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250111091824-e051c979c3dd/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik= +github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250204064923-ec656335987b h1:yvhcGfFhG06y3I6KOJBogTkCwZasKZiroRjpVbXYa1s= +github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250204064923-ec656335987b/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/managed/fflags/feature_flags.go b/managed/fflags/feature_flags.go index 4171f15..59854ab 100644 --- a/managed/fflags/feature_flags.go +++ b/managed/fflags/feature_flags.go @@ -15,11 +15,17 @@ type FeatureFlag string const ( CONNECTION_POOLING FeatureFlag = "CONNECTION_POOLING" DR FeatureFlag = "DR" + PITR FeatureFlag = "PITR" + PITR_RESTORE FeatureFlag = "PITR_RESTORE" + PITR_CLONE FeatureFlag = "PITR_CLONE" ) var flagEnabled = map[FeatureFlag]bool{ CONNECTION_POOLING: false, DR: false, + PITR: false, + PITR_RESTORE: false, + PITR_CLONE: false, } func (f FeatureFlag) String() string { diff --git a/managed/models.go b/managed/models.go index 8d9f7ad..180a2e7 100644 --- a/managed/models.go +++ b/managed/models.go @@ -402,3 +402,40 @@ type DrConfig struct { TargetClusterId types.String `tfsdk:"target_cluster_id"` Databases []types.String `tfsdk:"databases"` } + +type PitrConfig struct { + AccountId types.String `tfsdk:"account_id"` + ProjectId types.String `tfsdk:"project_id"` + ClusterId types.String `tfsdk:"cluster_id"` + PitrConfigId types.String `tfsdk:"pitr_config_id"` + NamespaceId types.String `tfsdk:"namespace_id"` + NamespaceName types.String `tfsdk:"namespace_name"` + NamespaceType types.String `tfsdk:"namespace_type"` + RetentionPeriodInDays types.Int64 `tfsdk:"retention_period_in_days"` + State types.String `tfsdk:"state"` + EarliestRecoveryTimeMillis types.Int64 `tfsdk:"earliest_recovery_time_millis"` + LatestRecoveryTimeMillis types.Int64 `tfsdk:"latest_recovery_time_millis"` +} + +type PitrRestore struct { + AccountId types.String `tfsdk:"account_id"` + ProjectId types.String `tfsdk:"project_id"` + ClusterId types.String `tfsdk:"cluster_id"` + PitrConfigId types.String `tfsdk:"pitr_config_id"` + PitrRestoreId types.String `tfsdk:"pitr_restore_id"` + RestoreAtMillis types.Int64 `tfsdk:"restore_at_millis"` + State types.String `tfsdk:"state"` +} + +type PitrClone struct { + AccountId types.String `tfsdk:"account_id"` + ProjectId types.String `tfsdk:"project_id"` + ClusterId types.String `tfsdk:"cluster_id"` + CloneNamespaceId types.String `tfsdk:"clone_namespace_id"` + CloneAs types.String `tfsdk:"clone_as"` + SourceNamespaceId types.String `tfsdk:"source_namespace_id"` + NamespaceName types.String `tfsdk:"namespace_name"` + NamespaceType types.String `tfsdk:"namespace_type"` + CloneAtMillis types.Int64 `tfsdk:"clone_at_millis"` + State types.String `tfsdk:"state"` +} diff --git a/managed/provider.go b/managed/provider.go index 529040e..57cf280 100644 --- a/managed/provider.go +++ b/managed/provider.go @@ -166,6 +166,21 @@ func (p *provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceTyp resources["ybm_dr_config"] = resourceDrConfigType{} } + // Add PITR config resource only if the feature flag is enabled + if fflags.IsFeatureFlagEnabled(fflags.PITR) { + resources["ybm_pitr_config"] = resourcePitrConfigType{} + } + + // Add PITR restore resource only if the feature flag is enabled + if fflags.IsFeatureFlagEnabled(fflags.PITR_RESTORE) { + resources["ybm_pitr_restore"] = resourcePitrRestoreType{} + } + + // Add PITR clone resource only if the feature flag is enabled + if fflags.IsFeatureFlagEnabled(fflags.PITR_CLONE) { + resources["ybm_pitr_clone"] = resourcePitrCloneType{} + } + return resources, nil } diff --git a/managed/resource_pitr_clone.go b/managed/resource_pitr_clone.go new file mode 100644 index 0000000..5d9497c --- /dev/null +++ b/managed/resource_pitr_clone.go @@ -0,0 +1,329 @@ +/* + * Copyright © 2022-present Yugabyte, Inc. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package managed + +import ( + "context" + "errors" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + retry "github.com/sethvargo/go-retry" + openapiclient "github.com/yugabyte/yugabytedb-managed-go-client-internal" +) + +type resourcePitrCloneType struct{} + +func (r resourcePitrCloneType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Description: `The resource to clone a namespace in YugabyteDB Aeon.`, + Attributes: map[string]tfsdk.Attribute{ + "account_id": { + Description: "The ID of the account.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "project_id": { + Description: "The ID of the project.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "cluster_id": { + Description: "The ID of the cluster.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "clone_namespace_id": { + Description: "The ID of the namespace clone.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "clone_as": { + Description: "The name for new cloned namespace.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "source_namespace_id": { + Description: "The ID of the namespace to be cloned.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "namespace_name": { + Description: "The source namespace name to be cloned.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "namespace_type": { + Description: "The namespace type.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "clone_at_millis": { + Description: "The time in UNIX millis to clone to via PITR Config.", + Type: types.Int64Type, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "state": { + Description: "The status of the namespace cloning.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + }, + }, nil +} + +func (r resourcePitrCloneType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return resourcePitrClone{ + p: *(p.(*provider)), + }, nil +} + +type resourcePitrClone struct { + p provider +} + +func getPitrClonePlan(ctx context.Context, plan tfsdk.Plan, pitrClone *PitrClone) diag.Diagnostics { + var diags diag.Diagnostics + + diags.Append(plan.GetAttribute(ctx, path.Root("cluster_id"), &pitrClone.ClusterId)...) + diags.Append(plan.GetAttribute(ctx, path.Root("namespace_name"), &pitrClone.NamespaceName)...) + diags.Append(plan.GetAttribute(ctx, path.Root("namespace_type"), &pitrClone.NamespaceType)...) + diags.Append(plan.GetAttribute(ctx, path.Root("clone_at_millis"), &pitrClone.CloneAtMillis)...) + + return diags +} + +func getPitrCloneState(ctx context.Context, state tfsdk.State, pitrClone *PitrClone) { + state.GetAttribute(ctx, path.Root("account_id"), &pitrClone.AccountId) + state.GetAttribute(ctx, path.Root("project_id"), &pitrClone.ProjectId) + state.GetAttribute(ctx, path.Root("cluster_id"), &pitrClone.ClusterId) + state.GetAttribute(ctx, path.Root("clone_namespace_id"), &pitrClone.CloneNamespaceId) + state.GetAttribute(ctx, path.Root("clone_as"), &pitrClone.CloneAs) + state.GetAttribute(ctx, path.Root("namespace_name"), &pitrClone.NamespaceName) + state.GetAttribute(ctx, path.Root("namespace_type"), &pitrClone.NamespaceType) + state.GetAttribute(ctx, path.Root("source_namespace_id"), &pitrClone.SourceNamespaceId) + state.GetAttribute(ctx, path.Root("clone_at_millis"), &pitrClone.CloneAtMillis) + state.GetAttribute(ctx, path.Root("state"), &pitrClone.State) +} + +// Create PITR clone +func (r resourcePitrClone) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + + if !r.p.configured { + resp.Diagnostics.AddError( + "Provider not configured", + "The provider wasn't configured before being applied, likely because it depends on an unknown value from another resource.", + ) + return + } + + var plan PitrClone + var accountId, message string + var getAccountOK bool + resp.Diagnostics.Append(getPitrClonePlan(ctx, req.Plan, &plan)...) + if resp.Diagnostics.HasError() { + tflog.Debug(ctx, "Error while getting the plan for the PITR Clone") + return + } + + if (!plan.CloneNamespaceId.Unknown && !plan.CloneNamespaceId.Null) || plan.CloneNamespaceId.Value != "" { + resp.Diagnostics.AddError( + "Cloned namespace ID provided for new clone", + "The clone_namespace_id was provided even though a new clone creation is being requested. Do not include this field in the provider when cloning a namespace.", + ) + return + } + + apiClient := r.p.client + + accountId, getAccountOK, message = getAccountId(ctx, apiClient) + if !getAccountOK { + resp.Diagnostics.AddError("Unable to get account ID", message) + return + } + + projectId, getProjectOK, message := getProjectId(ctx, apiClient, accountId) + if !getProjectOK { + resp.Diagnostics.AddError("Unable to get project ID ", message) + return + } + + clusterId := plan.ClusterId.Value + namespaceName := plan.NamespaceName.Value + namespaceType := plan.NamespaceType.Value + + namespacesResp, response, err := apiClient.ClusterApi.GetClusterNamespaces(ctx, accountId, projectId, clusterId).Execute() + if err != nil { + errMsg := getErrorMessage(response, err) + resp.Diagnostics.AddError("Unable to clone namespace", errMsg) + return + } + + var namespaceId string + for _, namespace := range namespacesResp.Data { + if namespace.GetName() == namespaceName && namespace.GetTableType() == GetNamespaceTypeMap()[namespaceType] { + namespaceId = namespace.GetId() + } + } + if len(namespaceId) == 0 { + msg := "No" + namespaceType + "namespace found with name" + namespaceName + resp.Diagnostics.AddError("Unable to clone namespace:", msg) + } + + pitrConfigsResp, response, err := apiClient.ClusterApi.ListClusterPitrConfigs(ctx, accountId, projectId, clusterId).Execute() + if err != nil { + errMsg := getErrorMessage(response, err) + resp.Diagnostics.AddError("Unable to clone namespace", errMsg) + return + } + + var pitrConfigId string + for _, pitrConfig := range pitrConfigsResp.GetData() { + if pitrConfig.Spec.DatabaseId == namespaceId { + pitrConfigId = *pitrConfig.Info.Id + break + } + } + + cloneSpec := openapiclient.NewDatabaseCloneSpec() + + if len(pitrConfigId) == 0 { + // No PITR config exists, so we create one and clone to current time. + // "clone_at_millis" should not be provided in this case. + if (!plan.CloneAtMillis.Unknown && !plan.CloneAtMillis.Null) || plan.CloneAtMillis.Value != 0 { + resp.Diagnostics.AddError( + "Clone time provided for cloning a namespace without a pre exsiting PITR config", + "The clone_at_millis was provided even though cloning of a namespace that is not assocaited to a PITR config is being requested. Do not include this field in the provider.", + ) + return + } + cloneSpec.SetCloneNow(*openapiclient.NewDatabaseCloneNowSpec(namespaceId, plan.CloneAs.Value)) + } else { + // We can only clone to PIT if a config is present for the {namespaceName, namespaceType} + if (!plan.CloneAtMillis.Unknown && !plan.CloneAtMillis.Null) || plan.CloneAtMillis.Value != 0 { + cloneSpec.SetClonePointInTime(*openapiclient.NewDatabaseClonePITSpec(plan.CloneAtMillis.Value, pitrConfigId, plan.CloneAs.Value)) + } else { + resp.Diagnostics.AddError( + "Clone time was not provided for cloning a namespace with a pre exsiting PITR config", + "The clone_at_millis was not provided even though cloning of a namespace that is assocaited to a pre existing PITR config is being requested. Do specify this field in the provider.", + ) + return + } + } + + pitrCloneResp, response, err := apiClient.ClusterApi.CloneDatabase(context.Background(), accountId, projectId, clusterId).DatabaseCloneSpec(*cloneSpec).Execute() + if err != nil { + errMsg := getErrorMessage(response, err) + resp.Diagnostics.AddError("Unable to clone namespace ", errMsg) + return + } + + readClusterRetries := 0 + retryPolicy := retry.NewConstant(10 * time.Second) + retryPolicy = retry.WithMaxDuration(3600*time.Second, retryPolicy) + err = retry.Do(ctx, retryPolicy, func(ctx context.Context) error { + asState, readInfoOK, message := getTaskState(accountId, projectId, clusterId, openapiclient.ENTITYTYPEENUM_CLUSTER, openapiclient.TASKTYPEENUM_CLONE_DB_PITR, apiClient, ctx) + + tflog.Info(ctx, "Cloning namespace in progress, state: "+asState) + + if readInfoOK { + if asState == string(openapiclient.TASKACTIONSTATEENUM_SUCCEEDED) { + return nil + } + if asState == string(openapiclient.TASKACTIONSTATEENUM_FAILED) { + return ErrFailedTask + } + } else { + return handleReadFailureWithRetries(ctx, &readClusterRetries, 2, message) + } + return retry.RetryableError(errors.New("Cloning namespace in progress")) + }) + + if err != nil { + msg := "The operation timed out waiting for namespace cloning to complete." + if errors.Is(err, ErrFailedTask) { + msg = "Cloning namespace failed" + } + resp.Diagnostics.AddError("Unable to clone namespace:", msg) + return + } + + // Set the computed fields + plan.AccountId = types.String{Value: accountId} + plan.ProjectId = types.String{Value: projectId} + plan.CloneNamespaceId = types.String{Value: pitrCloneResp.Data.Info.Id} + plan.SourceNamespaceId = types.String{Value: namespaceId} + plan.State = types.String{Value: *&pitrCloneResp.Data.Info.State} + + diags := resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read PITR clone +func (r resourcePitrClone) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { + var state PitrClone + getPitrCloneState(ctx, req.State, &state) + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update PITR clone +func (r resourcePitrClone) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { + resp.Diagnostics.AddError("Unable to update PITR clone.", "Updating PITR clones is not supported.") + return +} + +// Delete PITR clone +func (r resourcePitrClone) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { + resp.Diagnostics.AddError("Unable to delete PITR clone.", "Deleting PITR clones is not supported.") + return +} + +// Import PITR clone +func (r resourcePitrClone) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + // Save the import identifier in the id attribute + tfsdk.ResourceImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/managed/resource_pitr_config.go b/managed/resource_pitr_config.go new file mode 100644 index 0000000..71ab9a1 --- /dev/null +++ b/managed/resource_pitr_config.go @@ -0,0 +1,464 @@ +/* + * Copyright © 2022-present Yugabyte, Inc. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package managed + +import ( + "context" + "errors" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + retry "github.com/sethvargo/go-retry" + openapiclient "github.com/yugabyte/yugabytedb-managed-go-client-internal" +) + +type resourcePitrConfigType struct{} + +func (r resourcePitrConfigType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Description: `The resource to create a PITR Config for a namespace in YugabyteDB Aeon.`, + Attributes: map[string]tfsdk.Attribute{ + "account_id": { + Description: "The ID of the account this PITR Config belongs to.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "project_id": { + Description: "The ID of the project this PITR Config belongs to.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "cluster_id": { + Description: "The ID of the cluster this PITR Config belongs to.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "pitr_config_id": { + Description: "The ID of the PITR Config.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "namespace_id": { + Description: "The ID of the namespace that this PITR Config is associated to.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "namespace_name": { + Description: "The namespace name for the PITR Config.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "namespace_type": { + Description: "The namespace type.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "retention_period_in_days": { + Description: "The retention period of the PITR Config.", + Type: types.Int64Type, + Required: true, + }, + "state": { + Description: "The status of the PITR config.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "earliest_recovery_time_millis": { + Description: "The earliest recovery time in milliseconds to which the namespace can be restored.", + Type: types.Int64Type, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "latest_recovery_time_millis": { + Description: "The latest recovery time in milliseconds to which the namespace can be restored.", + Type: types.Int64Type, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + }, + }, nil +} + +func (r resourcePitrConfigType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return resourcePitrConfig{ + p: *(p.(*provider)), + }, nil +} + +type resourcePitrConfig struct { + p provider +} + +func GetNamespaceTypeMap() map[string]string { + return map[string]string{ + "YSQL": "PGSQL_TABLE_TYPE", + "YCQL": "YQL_TABLE_TYPE", + } +} + +func createBulkPitrConfigRequest(apiClient *openapiclient.APIClient, namespaceId string, retentionPeriod int32) (pitrConfigsRequest *openapiclient.BulkCreateDatabasePitrConfigSpec) { + + pitrConfigSpecs := []openapiclient.DatabasePitrConfigSpec{} + pitrConfigSpecs = append(pitrConfigSpecs, *openapiclient.NewDatabasePitrConfigSpec(namespaceId, retentionPeriod)) + + pitrConfigsRequest = openapiclient.NewBulkCreateDatabasePitrConfigSpec() + pitrConfigsRequest.SetPitrConfigSpecs(pitrConfigSpecs) + + return pitrConfigsRequest +} + +func getPitrConfigPlan(ctx context.Context, plan tfsdk.Plan, pitrConfig *PitrConfig) diag.Diagnostics { + var diags diag.Diagnostics + + diags.Append(plan.GetAttribute(ctx, path.Root("cluster_id"), &pitrConfig.ClusterId)...) + diags.Append(plan.GetAttribute(ctx, path.Root("namespace_name"), &pitrConfig.NamespaceName)...) + diags.Append(plan.GetAttribute(ctx, path.Root("namespace_type"), &pitrConfig.NamespaceType)...) + diags.Append(plan.GetAttribute(ctx, path.Root("retention_period_in_days"), &pitrConfig.RetentionPeriodInDays)...) + + return diags +} + +func getPitrConfigState(ctx context.Context, state tfsdk.State, pitrConfig *PitrConfig) { + state.GetAttribute(ctx, path.Root("account_id"), &pitrConfig.AccountId) + state.GetAttribute(ctx, path.Root("project_id"), &pitrConfig.ProjectId) + state.GetAttribute(ctx, path.Root("cluster_id"), &pitrConfig.ClusterId) + state.GetAttribute(ctx, path.Root("pitr_config_id"), &pitrConfig.PitrConfigId) + state.GetAttribute(ctx, path.Root("namespace_id"), &pitrConfig.NamespaceId) + state.GetAttribute(ctx, path.Root("namespace_name"), &pitrConfig.NamespaceName) + state.GetAttribute(ctx, path.Root("namespace_type"), &pitrConfig.NamespaceType) + state.GetAttribute(ctx, path.Root("retention_period_in_days"), &pitrConfig.RetentionPeriodInDays) + state.GetAttribute(ctx, path.Root("state"), &pitrConfig.State) + state.GetAttribute(ctx, path.Root("earliest_recovery_time_millis"), &pitrConfig.EarliestRecoveryTimeMillis) + state.GetAttribute(ctx, path.Root("latest_recovery_time_millis"), &pitrConfig.LatestRecoveryTimeMillis) +} + +// Create PITR Config +func (r resourcePitrConfig) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + + if !r.p.configured { + resp.Diagnostics.AddError( + "Provider not configured", + "The provider wasn't configured before being applied, likely because it depends on an unknown value from another resource.", + ) + return + } + + var plan PitrConfig + var accountId, message string + var getAccountOK bool + resp.Diagnostics.Append(getPitrConfigPlan(ctx, req.Plan, &plan)...) + if resp.Diagnostics.HasError() { + tflog.Debug(ctx, "Error while getting the plan for the PITR Configs") + return + } + + if (!plan.PitrConfigId.Unknown && !plan.PitrConfigId.Null) || plan.PitrConfigId.Value != "" { + resp.Diagnostics.AddError( + "PITR Config ID provided for new PITR Config", + "The pitr_config_id was provided even though a new PITR Config is being created. Do not include this field in the provider when creating a PITR Config.", + ) + return + } + + apiClient := r.p.client + + accountId, getAccountOK, message = getAccountId(ctx, apiClient) + if !getAccountOK { + resp.Diagnostics.AddError("Unable to get account ID", message) + return + } + + projectId, getProjectOK, message := getProjectId(ctx, apiClient, accountId) + if !getProjectOK { + resp.Diagnostics.AddError("Unable to get project ID ", message) + return + } + + clusterId := plan.ClusterId.Value + namespaceName := plan.NamespaceName.Value + namespaceType := plan.NamespaceType.Value + + namespacesResp, response, err := apiClient.ClusterApi.GetClusterNamespaces(ctx, accountId, projectId, clusterId).Execute() + if err != nil { + errMsg := getErrorMessage(response, err) + resp.Diagnostics.AddError("Unable to create PITR configuration", errMsg) + return + } + + var namespaceId string + + for _, namespace := range namespacesResp.Data { + if namespace.GetName() == namespaceName && namespace.GetTableType() == GetNamespaceTypeMap()[namespaceType] { + namespaceId = namespace.GetId() + } + } + if len(namespaceId) == 0 { + msg := "No" + namespaceType + "namespace found with name" + namespaceName + resp.Diagnostics.AddError("Unable to create PITR config:", msg) + } + + createPitrConfigsRequest := createBulkPitrConfigRequest(apiClient, namespaceId, int32(plan.RetentionPeriodInDays.Value)) + + pitrConfigsResp, response, err := apiClient.ClusterApi.CreateDatabasePitrConfig(context.Background(), accountId, projectId, clusterId).BulkCreateDatabasePitrConfigSpec(*createPitrConfigsRequest).Execute() + if err != nil { + errMsg := getErrorMessage(response, err) + resp.Diagnostics.AddError("Unable to create PITR Config ", errMsg) + return + } + + readClusterRetries := 0 + retryPolicy := retry.NewConstant(10 * time.Second) + retryPolicy = retry.WithMaxDuration(3600*time.Second, retryPolicy) + err = retry.Do(ctx, retryPolicy, func(ctx context.Context) error { + asState, readInfoOK, message := getTaskState(accountId, projectId, clusterId, openapiclient.ENTITYTYPEENUM_CLUSTER, openapiclient.TASKTYPEENUM_BULK_ENABLE_DB_PITR, apiClient, ctx) + + tflog.Info(ctx, "PITR config creation operation in progress, state: "+asState) + + if readInfoOK { + if asState == string(openapiclient.TASKACTIONSTATEENUM_SUCCEEDED) { + return nil + } + if asState == string(openapiclient.TASKACTIONSTATEENUM_FAILED) { + return ErrFailedTask + } + } else { + return handleReadFailureWithRetries(ctx, &readClusterRetries, 2, message) + } + return retry.RetryableError(errors.New("PITR config creation operation in progress")) + }) + + if err != nil { + msg := "The operation timed out waiting for PITR config creation to complete." + if errors.Is(err, ErrFailedTask) { + msg = "PITR config creation operation failed" + } + resp.Diagnostics.AddError("Unable to create PITR config:", msg) + return + } + + // Set the computed fields + plan.AccountId = types.String{Value: accountId} + plan.ProjectId = types.String{Value: projectId} + plan.NamespaceId = types.String{Value: namespaceId} + plan.PitrConfigId = types.String{Value: pitrConfigsResp.GetData()[0].Info.GetId()} + plan.State = types.String{Value: pitrConfigsResp.GetData()[0].Info.GetState()} + plan.State = types.String{Value: pitrConfigsResp.GetData()[0].Info.GetState()} + plan.EarliestRecoveryTimeMillis = types.Int64{Value: pitrConfigsResp.GetData()[0].Info.GetEarliestRecoveryTimeMillis()} + plan.LatestRecoveryTimeMillis = types.Int64{Value: pitrConfigsResp.GetData()[0].Info.GetLatestRecoveryTimeMillis()} + + diags := resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read PITR configuration +func (r resourcePitrConfig) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { + var state PitrConfig + getPitrConfigState(ctx, req.State, &state) + + apiClient := r.p.client + accountId := state.AccountId.Value + projectId := state.ProjectId.Value + clusterId := state.ClusterId.Value + pitrConfigId := state.PitrConfigId.Value + + pitrConfigResp, response, err := apiClient.ClusterApi.GetDatabasePitrConfig(ctx, accountId, projectId, clusterId, pitrConfigId).Execute() + if err != nil { + if response != nil && response.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } + errMsg := getErrorMessage(response, err) + resp.Diagnostics.AddError("Unable to read PITR configuration", errMsg) + return + } + + // Update state with the current values + state.NamespaceId = types.String{Value: pitrConfigResp.Data.Spec.DatabaseId} + state.NamespaceName = types.String{Value: pitrConfigResp.Data.Info.GetDatabaseName()} + state.NamespaceType = types.String{Value: string(pitrConfigResp.Data.Info.GetDatabaseType())} + state.RetentionPeriodInDays = types.Int64{Value: int64(pitrConfigResp.Data.Spec.GetRetentionPeriod())} + state.State = types.String{Value: pitrConfigResp.Data.Info.GetState()} + state.EarliestRecoveryTimeMillis = types.Int64{Value: pitrConfigResp.Data.Info.GetEarliestRecoveryTimeMillis()} + state.LatestRecoveryTimeMillis = types.Int64{Value: pitrConfigResp.Data.Info.GetLatestRecoveryTimeMillis()} + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update PITR Config +func (r resourcePitrConfig) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { + var plan PitrConfig + var state PitrConfig + + // Get current state + getPitrConfigState(ctx, req.State, &state) + + // Get planned changes + resp.Diagnostics.Append(getPitrConfigPlan(ctx, req.Plan, &plan)...) + if resp.Diagnostics.HasError() { + tflog.Debug(ctx, "Error while getting the plan for the PITR config edit") + return + } + + // Verify that only retention period field is being changed + if plan.ClusterId.Value != state.ClusterId.Value || plan.NamespaceName.Value != state.NamespaceName.Value || plan.NamespaceType.Value != state.NamespaceType.Value { + resp.Diagnostics.AddError( + "Invalid edit to PITR configuration", + "Only the retention period field can be modified in PITR configurations. Other fields cannot be changed.", + ) + return + } + + apiClient := r.p.client + accountId := state.AccountId.Value + projectId := state.ProjectId.Value + clusterId := state.ClusterId.Value + pitrConfigId := state.PitrConfigId.Value + + // Create edit request with new retention period + _, response, err := apiClient.ClusterApi.UpdateDatabasePitrConfig(ctx, accountId, projectId, clusterId, pitrConfigId).UpdateDatabasePitrConfigSpec(*openapiclient.NewUpdateDatabasePitrConfigSpec(int32(plan.RetentionPeriodInDays.Value))).Execute() + if err != nil { + errMsg := getErrorMessage(response, err) + resp.Diagnostics.AddError("Unable to edit PITR configuration", errMsg) + return + } + + // Wait for to complete + readClusterRetries := 0 + retryPolicy := retry.NewConstant(10 * time.Second) + retryPolicy = retry.WithMaxDuration(3600*time.Second, retryPolicy) + err = retry.Do(ctx, retryPolicy, func(ctx context.Context) error { + asState, readInfoOK, message := getTaskState(accountId, projectId, clusterId, openapiclient.ENTITYTYPEENUM_CLUSTER, openapiclient.TASKTYPEENUM_UPDATE_DB_PITR, apiClient, ctx) + + tflog.Info(ctx, "PITR config edit operation in progress, state: "+asState) + + if readInfoOK { + if asState == string(openapiclient.TASKACTIONSTATEENUM_SUCCEEDED) { + return nil + } + if asState == string(openapiclient.TASKACTIONSTATEENUM_FAILED) { + return ErrFailedTask + } + } else { + return handleReadFailureWithRetries(ctx, &readClusterRetries, 2, message) + } + return retry.RetryableError(errors.New("PITR config edit operation in progress")) + }) + + if err != nil { + msg := "The operation timed out waiting for PITR config edit to complete." + if errors.Is(err, ErrFailedTask) { + msg = "PITR config edit operation failed" + } + resp.Diagnostics.AddError("Unable to edit PITR config:", msg) + return + } + + // Set state to planned new state + plan.AccountId = state.AccountId + plan.ProjectId = state.ProjectId + plan.PitrConfigId = state.PitrConfigId + plan.NamespaceId = state.NamespaceId + + diags := resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete PITR configuration +func (r resourcePitrConfig) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { + var state PitrConfig + getPitrConfigState(ctx, req.State, &state) + accountId := state.AccountId.Value + projectId := state.ProjectId.Value + clusterId := state.ClusterId.Value + pitrConfigId := state.PitrConfigId.Value + + apiClient := r.p.client + + _, err := apiClient.ClusterApi.RemoveDatabasePitrConfig(ctx, accountId, projectId, clusterId, pitrConfigId).Execute() + if err != nil { + resp.Diagnostics.AddError("Unable to delete PITR configuration", GetApiErrorDetails(err)) + return + } + + readClusterRetries := 0 + retryPolicy := retry.NewConstant(10 * time.Second) + retryPolicy = retry.WithMaxDuration(3600*time.Second, retryPolicy) + err = retry.Do(ctx, retryPolicy, func(ctx context.Context) error { + asState, readInfoOK, message := getTaskState(accountId, projectId, clusterId, openapiclient.ENTITYTYPEENUM_CLUSTER, openapiclient.TASKTYPEENUM_DISABLE_DB_PITR, apiClient, ctx) + + tflog.Info(ctx, "PITR config delete operation in progress, state: "+asState) + + if readInfoOK { + if asState == string(openapiclient.TASKACTIONSTATEENUM_SUCCEEDED) { + return nil + } + if asState == string(openapiclient.TASKACTIONSTATEENUM_FAILED) { + return ErrFailedTask + } + } else { + return handleReadFailureWithRetries(ctx, &readClusterRetries, 2, message) + } + return retry.RetryableError(errors.New("PITR config deletion operation in progress")) + }) + + if err != nil { + msg := "The operation timed out waiting for PITR config deletion to complete." + if errors.Is(err, ErrFailedTask) { + msg = "PITR config deletion operation failed" + } + resp.Diagnostics.AddError("Unable to delete PITR config:", msg) + return + } + + resp.State.RemoveResource(ctx) +} + +// Import PITR Config +func (r resourcePitrConfig) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + // Save the import identifier in the id attribute + tfsdk.ResourceImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/managed/resource_pitr_restore.go b/managed/resource_pitr_restore.go new file mode 100644 index 0000000..dfe8a70 --- /dev/null +++ b/managed/resource_pitr_restore.go @@ -0,0 +1,240 @@ +/* + * Copyright © 2022-present Yugabyte, Inc. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package managed + +import ( + "context" + "errors" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + retry "github.com/sethvargo/go-retry" + openapiclient "github.com/yugabyte/yugabytedb-managed-go-client-internal" +) + +type resourcePitrRestoreType struct{} + +func (r resourcePitrRestoreType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Description: `The resource to restore a namespace via PITR Config in YugabyteDB Aeon.`, + Attributes: map[string]tfsdk.Attribute{ + "account_id": { + Description: "The ID of the account.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "project_id": { + Description: "The ID of the project.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "cluster_id": { + Description: "The ID of the cluster.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "pitr_config_id": { + Description: "The ID of the PITR config to be used to perform the restore.", + Type: types.StringType, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "pitr_restore_id": { + Description: "The ID of the restore op via PITR Config.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + "restore_at_millis": { + Description: "The time in UNIX millis to restore to via PITR Config.", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + ignoreChangesModifier{}, + }, + }, + "state": { + Description: "The status of the restoration via PITR config.", + Type: types.StringType, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.UseStateForUnknown(), + }, + }, + }, + }, nil +} + +func (r resourcePitrRestoreType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return resourcePitrRestore{ + p: *(p.(*provider)), + }, nil +} + +type resourcePitrRestore struct { + p provider +} + +func getPitrRestorePlan(ctx context.Context, plan tfsdk.Plan, pitrRestore *PitrRestore) diag.Diagnostics { + var diags diag.Diagnostics + + diags.Append(plan.GetAttribute(ctx, path.Root("cluster_id"), &pitrRestore.ClusterId)...) + diags.Append(plan.GetAttribute(ctx, path.Root("pitr_config_id"), &pitrRestore.PitrConfigId)...) + diags.Append(plan.GetAttribute(ctx, path.Root("restore_at_millis"), &pitrRestore.RestoreAtMillis)...) + + return diags +} + +func getPitrRestoreState(ctx context.Context, state tfsdk.State, pitrRestore *PitrRestore) { + state.GetAttribute(ctx, path.Root("account_id"), &pitrRestore.AccountId) + state.GetAttribute(ctx, path.Root("project_id"), &pitrRestore.ProjectId) + state.GetAttribute(ctx, path.Root("cluster_id"), &pitrRestore.ClusterId) + state.GetAttribute(ctx, path.Root("pitr_config_id"), &pitrRestore.PitrConfigId) + state.GetAttribute(ctx, path.Root("pitr_restore_id"), &pitrRestore.PitrRestoreId) + state.GetAttribute(ctx, path.Root("restore_at_millis"), &pitrRestore.RestoreAtMillis) + state.GetAttribute(ctx, path.Root("state"), &pitrRestore.State) +} + +// Restore via PITR Config +func (r resourcePitrRestore) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + + if !r.p.configured { + resp.Diagnostics.AddError( + "Provider not configured", + "The provider wasn't configured before being applied, likely because it depends on an unknown value from another resource.", + ) + return + } + + var plan PitrRestore + var accountId, message string + var getAccountOK bool + resp.Diagnostics.Append(getPitrRestorePlan(ctx, req.Plan, &plan)...) + if resp.Diagnostics.HasError() { + tflog.Debug(ctx, "Error while getting the plan for the PITR Restore") + return + } + + if (!plan.PitrRestoreId.Unknown && !plan.PitrRestoreId.Null) || plan.PitrRestoreId.Value != "" { + resp.Diagnostics.AddError( + "PITR Restore ID provided for new restore via PITR Config", + "The pitr_restore_id was provided even though a new restore via PITR Config is being requested. Do not include this field in the provider when restoring via PITR Config.", + ) + return + } + + apiClient := r.p.client + + accountId, getAccountOK, message = getAccountId(ctx, apiClient) + if !getAccountOK { + resp.Diagnostics.AddError("Unable to get account ID", message) + return + } + + projectId, getProjectOK, message := getProjectId(ctx, apiClient, accountId) + if !getProjectOK { + resp.Diagnostics.AddError("Unable to get project ID ", message) + return + } + + clusterId := plan.ClusterId.Value + pitrConfigId := plan.PitrConfigId.Value + + pitrRestoreResp, response, err := apiClient.ClusterApi.RestoreDatabaseViaPitr(context.Background(), accountId, projectId, clusterId, pitrConfigId).DatabaseRestoreViaPitrSpec(*openapiclient.NewDatabaseRestoreViaPitrSpec(plan.RestoreAtMillis.Value)).Execute() + if err != nil { + errMsg := getErrorMessage(response, err) + resp.Diagnostics.AddError("Unable to restore via PITR Config ", errMsg) + return + } + + readClusterRetries := 0 + retryPolicy := retry.NewConstant(10 * time.Second) + retryPolicy = retry.WithMaxDuration(3600*time.Second, retryPolicy) + err = retry.Do(ctx, retryPolicy, func(ctx context.Context) error { + asState, readInfoOK, message := getTaskState(accountId, projectId, clusterId, openapiclient.ENTITYTYPEENUM_CLUSTER, openapiclient.TASKTYPEENUM_RESTORE_DB_PITR, apiClient, ctx) + + tflog.Info(ctx, "Restoration via PITR config in progress, state: "+asState) + + if readInfoOK { + if asState == string(openapiclient.TASKACTIONSTATEENUM_SUCCEEDED) { + return nil + } + if asState == string(openapiclient.TASKACTIONSTATEENUM_FAILED) { + return ErrFailedTask + } + } else { + return handleReadFailureWithRetries(ctx, &readClusterRetries, 2, message) + } + return retry.RetryableError(errors.New("Restoration via PITR config in progress")) + }) + + if err != nil { + msg := "The operation timed out waiting for restoration via PITR config to complete." + if errors.Is(err, ErrFailedTask) { + msg = "Restoration via PITR config failed" + } + resp.Diagnostics.AddError("Unable to restore via PITR config:", msg) + return + } + + // Set the computed fields + plan.AccountId = types.String{Value: accountId} + plan.ProjectId = types.String{Value: projectId} + plan.PitrRestoreId = types.String{Value: pitrRestoreResp.Data.Info.GetId()} + plan.State = types.String{Value: *&pitrRestoreResp.Data.Info.State} + + diags := resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read PITR restore +func (r resourcePitrRestore) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { + var state PitrRestore + getPitrRestoreState(ctx, req.State, &state) + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update PITR restore +func (r resourcePitrRestore) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { + resp.Diagnostics.AddError("Unable to update PITR restore.", "Updating PITR restores is not supported.") + return +} + +// Delete PITR restore +func (r resourcePitrRestore) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { + resp.Diagnostics.AddError("Unable to delete PITR restore.", "Deleting PITR restores is not supported.") + return +} + +// Import PITR restore +func (r resourcePitrRestore) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + // Save the import identifier in the id attribute + tfsdk.ResourceImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/samples/cluster-single-region.tf b/samples/cluster-single-region.tf index c4f8eb5..190426c 100644 --- a/samples/cluster-single-region.tf +++ b/samples/cluster-single-region.tf @@ -140,3 +140,31 @@ resource "ybm_read_replicas" "myrr" { # target_cluster_id = "e35dbf4d-cfd7-4e17-b9de-7d4ebd56a0e0" # databases = ["test1", "test2"] # } + +# resource "ybm_pitr_config" "sample_pitr" { +# cluster_id = ybm_cluster.single_region.cluster_id +# namespace_name = "test-PITR-DB" +# namespace_type = "YSQL" +# retention_period_in_days = 7 +# } + +# resource "ybm_pitr_restore" "sample_pitr_restore" { +# cluster_id = ybm_cluster.single_region.cluster_id +# pitr_config_id = "e35dbf4d-cfd7-4e17-b9de-7d4ebd56a0e0" +# restore_at_millis = "1234567889" +# } + +# resource "ybm_pitr_clone" "sample_pitr_clone_now" { +# cluster_id = ybm_cluster.single_region.cluster_id +# clone_as = "test-clone-now-db-clone" +# namespace_name = "test-clone-now-DB" +# namespace_type = "YSQL" +# } + +# resource "ybm_pitr_clone" "sample_pitr_clone_PIT" { +# cluster_id = ybm_cluster.single_region.cluster_id +# clone_as = "test-clone-PIT-db-clone" +# namespace_name = "test-clone-PIT-DB" +# namespace_type = "YSQL" +# clone_at_millis = "1234567889" +# }