diff --git a/internal/common/customplanmodifier/create_only_default_bool.go b/internal/common/customplanmodifier/create_only_default_bool.go index 4452686b44..eb3cc53d7c 100644 --- a/internal/common/customplanmodifier/create_only_default_bool.go +++ b/internal/common/customplanmodifier/create_only_default_bool.go @@ -12,38 +12,36 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -// CreateOnlyAttributePlanModifierWithBoolDefault sets a default value on create operation that will show in the plan. +// CreateOnlyBoolWithDefault sets a default value on create operation that will show in the plan. // This avoids any custom logic in the resource "Create" handler. // On update the default has no impact and the UseStateForUnknown behavior is observed instead. // Always use Optional+Computed when using a default value. -func CreateOnlyAttributePlanModifierWithBoolDefault(b bool) planmodifier.Bool { - return &createOnlyAttributePlanModifierWithBoolDefault{defaultBool: &b} +// If the attribute is not in the API Response implement CopyFromPlan behavior when converting API Model to TF Model. +func CreateOnlyBoolWithDefault(b bool) planmodifier.Bool { + return &createOnlyBoolPlanModifier{defaultBool: b} } -type createOnlyAttributePlanModifierWithBoolDefault struct { - defaultBool *bool +type createOnlyBoolPlanModifier struct { + defaultBool bool } -func (d *createOnlyAttributePlanModifierWithBoolDefault) Description(ctx context.Context) string { +func (d *createOnlyBoolPlanModifier) Description(ctx context.Context) string { return d.MarkdownDescription(ctx) } -func (d *createOnlyAttributePlanModifierWithBoolDefault) MarkdownDescription(ctx context.Context) string { - return "Ensures the update operation fails when updating an attribute. If the read after import don't equal the configuration value it will also raise an error." +func (d *createOnlyBoolPlanModifier) MarkdownDescription(ctx context.Context) string { + return "Ensures the update operation fails when updating an attribute. If the read after import doesn't equal the configuration value it will also raise an error." } +// isCreate uses the full state to check if this is a create operation func isCreate(t *tfsdk.State) bool { return t.Raw.IsNull() } -func (d *createOnlyAttributePlanModifierWithBoolDefault) UseDefault() bool { - return d.defaultBool != nil -} - -func (d *createOnlyAttributePlanModifierWithBoolDefault) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { +func (d *createOnlyBoolPlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { if isCreate(&req.State) { - if !IsKnown(req.PlanValue) && d.UseDefault() { - resp.PlanValue = types.BoolPointerValue(d.defaultBool) + if !IsKnown(req.PlanValue) { + resp.PlanValue = types.BoolPointerValue(&d.defaultBool) } return } @@ -55,6 +53,10 @@ func (d *createOnlyAttributePlanModifierWithBoolDefault) PlanModifyBool(ctx cont } } +// isUpdated checks if the attribute was updated. +// Special case when the attribute is removed/set to null in the plan: +// Computed Attribute: returns false (unknown in the plan) +// Optional Attribute: returns true if the state has a value func isUpdated(state, plan attr.Value) bool { if !IsKnown(plan) { return false @@ -62,7 +64,7 @@ func isUpdated(state, plan attr.Value) bool { return !state.Equal(plan) } -func (d *createOnlyAttributePlanModifierWithBoolDefault) addDiags(diags *diag.Diagnostics, attrPath path.Path, stateValue attr.Value) { +func (d *createOnlyBoolPlanModifier) addDiags(diags *diag.Diagnostics, attrPath path.Path, stateValue attr.Value) { message := fmt.Sprintf("%s cannot be updated or set after import, remove it from the configuration or use the state value (see below).", attrPath) detail := fmt.Sprintf("The current state value is %s", stateValue) diags.AddError(message, detail) diff --git a/internal/service/flexcluster/resource.go b/internal/service/flexcluster/resource.go index f81318e60b..c82e36bbfb 100644 --- a/internal/service/flexcluster/resource.go +++ b/internal/service/flexcluster/resource.go @@ -80,8 +80,7 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou flexClusterResp, err := CreateFlexCluster(ctx, projectID, clusterName, flexClusterReq, connV2.FlexClustersApi, &createTimeout) // Handle timeout with cleanup logic - deleteOnCreateTimeout := cleanup.ResolveDeleteOnCreateTimeout(tfModel.DeleteOnCreateTimeout) - err = cleanup.HandleCreateTimeout(deleteOnCreateTimeout, err, func(ctxCleanup context.Context) error { + err = cleanup.HandleCreateTimeout(tfModel.DeleteOnCreateTimeout.ValueBool(), err, func(ctxCleanup context.Context) error { cleanResp, cleanErr := r.Client.AtlasV2.FlexClustersApi.DeleteFlexCluster(ctxCleanup, projectID, clusterName).Execute() if validate.StatusNotFound(cleanResp) { return nil diff --git a/internal/service/flexcluster/resource_schema.go b/internal/service/flexcluster/resource_schema.go index 2eeb16cdf2..a2864e14cf 100644 --- a/internal/service/flexcluster/resource_schema.go +++ b/internal/service/flexcluster/resource_schema.go @@ -148,8 +148,9 @@ func ResourceSchema(ctx context.Context) schema.Schema { }, "delete_on_create_timeout": schema.BoolAttribute{ Optional: true, + Computed: true, PlanModifiers: []planmodifier.Bool{ - customplanmodifier.CreateOnly(), + customplanmodifier.CreateOnlyBoolWithDefault(true), }, MarkdownDescription: "Indicates whether to delete the resource being created if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the deletion and returns immediately without waiting for deletion to complete. When set to `false`, the timeout will not trigger resource deletion. If you suspect a transient error when the value is `true`, wait before retrying to allow resource deletion to finish. Default is `true`.", }, diff --git a/internal/service/flexcluster/resource_test.go b/internal/service/flexcluster/resource_test.go index 2cef4ca1cc..b787ada201 100644 --- a/internal/service/flexcluster/resource_test.go +++ b/internal/service/flexcluster/resource_test.go @@ -104,10 +104,11 @@ func basicTestCase(t *testing.T) *resource.TestCase { Check: checksFlexCluster(projectID, clusterName, false, true), }, { - ResourceName: resourceName, - ImportStateIdFunc: acc.ImportStateIDFuncProjectIDClusterName(resourceName, "project_id", "name"), - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportStateIdFunc: acc.ImportStateIDFuncProjectIDClusterName(resourceName, "project_id", "name"), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"delete_on_create_timeout"}, }, }, } diff --git a/internal/service/project/resource_project_schema.go b/internal/service/project/resource_project_schema.go index b78b33b936..f4189110d2 100644 --- a/internal/service/project/resource_project_schema.go +++ b/internal/service/project/resource_project_schema.go @@ -61,7 +61,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { // Provider produced invalid plan: planned an invalid value for a non-computed attribute. Optional: true, Computed: true, - PlanModifiers: []planmodifier.Bool{customplanmodifier.CreateOnlyAttributePlanModifierWithBoolDefault(true)}, + PlanModifiers: []planmodifier.Bool{customplanmodifier.CreateOnlyBoolWithDefault(true)}, }, "is_collect_database_specifics_statistics_enabled": schema.BoolAttribute{ Computed: true,