Skip to content

Commit e2e475b

Browse files
lantolioarbusi
andauthored
feat: Long-running operation improvements for mongodbatlas_flex_cluster resource (#3525)
* CreateFlexClusterNew * changelog * schema, model & auto-generated doc * non-updatable for multiple types like string and bool * rename to create only * fill Timeouts in model * remove delete_on_create_timeout by default in DataSourceSchemaFromResource * implement timeout and delete_on_create * resolve timeout refactor * remove unnecessary update and delete timeouts in test * pass timeout for update and delete * explicitly check that cluster does not exist due to delete_on_create_timeout * timeout as value instead of pointer * remove redunadnt check * final fixes for timeout * improve testing * remove config on import step * remove redundant checkDestroy * remove redundant checkDestroy in adv_cluster test --------- Co-authored-by: Oriol Arbusi Abadal <[email protected]>
1 parent 0ef10fc commit e2e475b

File tree

21 files changed

+427
-177
lines changed

21 files changed

+427
-177
lines changed

.changelog/3525.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:enhancement
2+
resource/mongodbatlas_flex_cluster: Adds `timeouts` attribute for create, update and delete operations
3+
```
4+
5+
```release-note:enhancement
6+
resource/mongodbatlas_flex_cluster: Adds `delete_on_create_timeout` attribute to indicate whether to delete the resource if its creation times out
7+
```

docs/resources/flex_cluster.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ output "mongodbatlas_flex_clusters_names" {
4646

4747
### Optional
4848

49+
- `delete_on_create_timeout` (Boolean) Indicates whether to delete the resource if creation times out. Default is `true`. When Terraform apply fails, it returns immediately without waiting for cleanup to complete. If you suspect a transient error, wait before retrying to allow resource deletion to finish.
4950
- `tags` (Map of String) Map that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the instance.
5051
- `termination_protection_enabled` (Boolean) Flag that indicates whether termination protection is enabled on the cluster. If set to `true`, MongoDB Cloud won't delete the cluster. If set to `false`, MongoDB Cloud will delete the cluster.
52+
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))
5153

5254
### Read-Only
5355

@@ -74,6 +76,16 @@ Read-Only:
7476
- `provider_name` (String) Human-readable label that identifies the cloud service provider.
7577

7678

79+
<a id="nestedatt--timeouts"></a>
80+
### Nested Schema for `timeouts`
81+
82+
Optional:
83+
84+
- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
85+
- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs.
86+
- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
87+
88+
7789
<a id="nestedatt--backup_settings"></a>
7890
### Nested Schema for `backup_settings`
7991

internal/common/cleanup/handle_timeout.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"strings"
77
"time"
88

9+
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
910
"github.com/hashicorp/terraform-plugin-framework/diag"
1011
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
12+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/constant"
1113
)
1214

1315
const (
@@ -69,3 +71,31 @@ func ReplaceContextDeadlineExceededDiags(diags *diag.Diagnostics, duration time.
6971
}
7072
}
7173
}
74+
75+
const (
76+
OperationCreate = "create"
77+
OperationUpdate = "update"
78+
OperationDelete = "delete"
79+
)
80+
81+
// ResolveTimeout extracts the appropriate timeout duration from the model for the given operation
82+
func ResolveTimeout(ctx context.Context, t *timeouts.Value, operationName string, diags *diag.Diagnostics) time.Duration {
83+
var (
84+
timeoutDuration time.Duration
85+
localDiags diag.Diagnostics
86+
)
87+
switch operationName {
88+
case OperationCreate:
89+
timeoutDuration, localDiags = t.Create(ctx, constant.DefaultTimeout)
90+
diags.Append(localDiags...)
91+
case OperationUpdate:
92+
timeoutDuration, localDiags = t.Update(ctx, constant.DefaultTimeout)
93+
diags.Append(localDiags...)
94+
case OperationDelete:
95+
timeoutDuration, localDiags = t.Delete(ctx, constant.DefaultTimeout)
96+
diags.Append(localDiags...)
97+
default:
98+
timeoutDuration = constant.DefaultTimeout
99+
}
100+
return timeoutDuration
101+
}

internal/common/conversion/schema_generation.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ var convertNestedMappings = map[string]reflect.Type{
9191
}
9292

9393
func convertAttrs(rsAttrs map[string]schema.Attribute, requiredFields []string) map[string]dsschema.Attribute {
94-
const ignoreField = "timeouts"
94+
ignoreFields := []string{"timeouts", "delete_on_create_timeout"}
9595
dsAttrs := make(map[string]dsschema.Attribute, len(rsAttrs))
9696
for name, attr := range rsAttrs {
97-
if name == ignoreField {
97+
if slices.Contains(ignoreFields, name) {
9898
continue
9999
}
100100
dsAttrs[name] = convertElement(name, attr, requiredFields).(dsschema.Attribute)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package customplanmodifier
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/attr"
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/path"
10+
planmodifier "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
11+
)
12+
13+
// CreateOnlyStringPlanModifier creates a plan modifier that prevents updates to string attributes.
14+
func CreateOnlyStringPlanModifier() planmodifier.String {
15+
return &createOnlyAttributePlanModifier{}
16+
}
17+
18+
// CreateOnlyBoolPlanModifier creates a plan modifier that prevents updates to boolean attributes.
19+
func CreateOnlyBoolPlanModifier() planmodifier.Bool {
20+
return &createOnlyAttributePlanModifier{}
21+
}
22+
23+
// Plan modifier that implements create-only behavior for multiple attribute types
24+
type createOnlyAttributePlanModifier struct{}
25+
26+
func (d *createOnlyAttributePlanModifier) Description(ctx context.Context) string {
27+
return d.MarkdownDescription(ctx)
28+
}
29+
30+
func (d *createOnlyAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
31+
return "Ensures that update operations fail when attempting to modify a create-only attribute."
32+
}
33+
34+
func (d *createOnlyAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
35+
validateCreateOnly(req.PlanValue, req.StateValue, req.Path, &resp.Diagnostics)
36+
}
37+
38+
func (d *createOnlyAttributePlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
39+
validateCreateOnly(req.PlanValue, req.StateValue, req.Path, &resp.Diagnostics)
40+
}
41+
42+
// validateCreateOnly checks if an attribute value has changed and adds an error if it has
43+
func validateCreateOnly(planValue, stateValue attr.Value, attrPath path.Path, diagnostics *diag.Diagnostics,
44+
) {
45+
if !stateValue.IsNull() && !stateValue.Equal(planValue) {
46+
diagnostics.AddError(
47+
fmt.Sprintf("%s cannot be updated", attrPath),
48+
fmt.Sprintf("%s cannot be updated", attrPath),
49+
)
50+
}
51+
}

internal/common/customplanmodifier/non_updatable.go

Lines changed: 0 additions & 36 deletions
This file was deleted.

internal/service/advancedcluster/resource_advanced_cluster.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.
462462

463463
if isFlex {
464464
flexClusterReq := advancedclustertpf.NewFlexCreateReq(clusterName, d.Get("termination_protection_enabled").(bool), conversion.ExpandTagsFromSetSchema(d), replicationSpecs)
465-
flexClusterResp, err := flexcluster.CreateFlexCluster(ctx, projectID, clusterName, flexClusterReq, connV2.FlexClustersApi)
465+
flexClusterResp, err := flexcluster.CreateFlexCluster(ctx, projectID, clusterName, flexClusterReq, connV2.FlexClustersApi, &timeout)
466466
if err != nil {
467467
return diag.FromErr(fmt.Errorf(flexcluster.ErrorCreateFlex, err))
468468
}
@@ -1326,9 +1326,10 @@ func resourceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.
13261326
}
13271327

13281328
replicationSpecs := expandAdvancedReplicationSpecs(d.Get("replication_specs").([]any), nil)
1329+
timeout := d.Timeout(schema.TimeoutDelete)
13291330

13301331
if advancedclustertpf.IsFlex(replicationSpecs) {
1331-
err := flexcluster.DeleteFlexCluster(ctx, projectID, clusterName, connV2.FlexClustersApi)
1332+
err := flexcluster.DeleteFlexCluster(ctx, projectID, clusterName, connV2.FlexClustersApi, timeout)
13321333
if err != nil {
13331334
return diag.FromErr(fmt.Errorf(flexcluster.ErrorDeleteFlex, clusterName, err))
13341335
}
@@ -1433,7 +1434,7 @@ func waitStateTransitionFlexUpgrade(ctx context.Context, client admin.FlexCluste
14331434
GroupId: projectID,
14341435
Name: name,
14351436
}
1436-
flexClusterResp, err := flexcluster.WaitStateTransition(ctx, flexClusterParams, client, []string{retrystrategy.RetryStrategyUpdatingState}, []string{retrystrategy.RetryStrategyIdleState}, true, &timeout)
1437+
flexClusterResp, err := flexcluster.WaitStateTransition(ctx, flexClusterParams, client, []string{retrystrategy.RetryStrategyUpdatingState}, []string{retrystrategy.RetryStrategyIdleState}, true, timeout)
14371438
if err != nil {
14381439
return nil, err
14391440
}
@@ -1539,8 +1540,9 @@ func resourceUpdateFlexCluster(ctx context.Context, flexUpdateRequest *admin.Fle
15391540
ids := conversion.DecodeStateID(d.Id())
15401541
projectID := ids["project_id"]
15411542
clusterName := ids["cluster_name"]
1543+
timeout := d.Timeout(schema.TimeoutUpdate)
15421544

1543-
_, err := flexcluster.UpdateFlexCluster(ctx, projectID, clusterName, flexUpdateRequest, connV2.FlexClustersApi)
1545+
_, err := flexcluster.UpdateFlexCluster(ctx, projectID, clusterName, flexUpdateRequest, connV2.FlexClustersApi, timeout)
15441546
if err != nil {
15451547
return diag.FromErr(fmt.Errorf(flexcluster.ErrorUpdateFlex, err))
15461548
}

0 commit comments

Comments
 (0)