Skip to content

feat: Long-running operation improvements for mongodbatlas_flex_cluster resource #3525

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: CLOUDP-320243-dev-2.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
7 changes: 7 additions & 0 deletions .changelog/3525.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/mongodbatlas_flex_cluster: Adds `timeouts` attribute for create, update and delete operations
```

```release-note:enhancement
resource/mongodbatlas_flex_cluster: Adds `delete_on_create_timeout` attribute to indicate whether to delete the resource if its creation times out
```
12 changes: 12 additions & 0 deletions docs/resources/flex_cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,10 @@ output "mongodbatlas_flex_clusters_names" {

### Optional

- `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.
- `tags` (Map of String) Map that contains key-value pairs between 1 to 255 characters in length for tagging and categorizing the instance.
- `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.
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

### Read-Only

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


<a id="nestedatt--timeouts"></a>
### Nested Schema for `timeouts`

Optional:

- `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).
- `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.
- `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).


<a id="nestedatt--backup_settings"></a>
### Nested Schema for `backup_settings`

Expand Down
51 changes: 51 additions & 0 deletions internal/common/customplanmodifier/create_only.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package customplanmodifier

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
planmodifier "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)

// CreateOnlyStringPlanModifier creates a plan modifier that prevents updates to string attributes.
func CreateOnlyStringPlanModifier() planmodifier.String {
return &createOnlyAttributePlanModifier{}
}

// CreateOnlyBoolPlanModifier creates a plan modifier that prevents updates to boolean attributes.
func CreateOnlyBoolPlanModifier() planmodifier.Bool {
return &createOnlyAttributePlanModifier{}
}

// Plan modifier that implements create-only behavior for multiple attribute types
type createOnlyAttributePlanModifier struct{}

func (d *createOnlyAttributePlanModifier) Description(ctx context.Context) string {
return d.MarkdownDescription(ctx)
}

func (d *createOnlyAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
return "Ensures that update operations fail when attempting to modify a create-only attribute."
}

func (d *createOnlyAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
validateCreateOnly(req.PlanValue, req.StateValue, req.Path, &resp.Diagnostics)
}

func (d *createOnlyAttributePlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
validateCreateOnly(req.PlanValue, req.StateValue, req.Path, &resp.Diagnostics)
}

// validateCreateOnly checks if an attribute value has changed and adds an error if it has
func validateCreateOnly(planValue, stateValue attr.Value, attrPath path.Path, diagnostics *diag.Diagnostics,
) {
if !stateValue.IsNull() && !stateValue.Equal(planValue) {
diagnostics.AddError(
fmt.Sprintf("%s cannot be updated", attrPath),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Would improve the details by showing the plan & stateValue.

fmt.Sprintf("%s cannot be updated", attrPath),
)
}
}
36 changes: 0 additions & 36 deletions internal/common/customplanmodifier/non_updatable.go

This file was deleted.

10 changes: 9 additions & 1 deletion internal/service/flexcluster/data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/hashicorp/terraform-plugin-framework/datasource"
dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
)
Expand All @@ -25,10 +26,17 @@ type ds struct {

func (d *ds) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = conversion.DataSourceSchemaFromResource(ResourceSchema(ctx), &conversion.DataSourceSchemaRequest{
RequiredFields: []string{"project_id", "name"},
RequiredFields: []string{"project_id", "name"},
OverridenFields: dataSourceOverridenFields(),
})
}

func dataSourceOverridenFields() map[string]dsschema.Attribute {
return map[string]dsschema.Attribute{
"delete_on_create_timeout": nil,
}
}

func (d *ds) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var tfModel TFModel
resp.Diagnostics.Append(req.Config.Get(ctx, &tfModel)...)
Expand Down
3 changes: 2 additions & 1 deletion internal/service/flexcluster/plural_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ type pluralDS struct {

func (d *pluralDS) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = conversion.PluralDataSourceSchemaFromResource(ResourceSchema(ctx), &conversion.PluralDataSourceSchemaRequest{
RequiredFields: []string{"project_id"},
RequiredFields: []string{"project_id"},
OverridenFields: dataSourceOverridenFields(),
})
}

Expand Down
13 changes: 11 additions & 2 deletions internal/service/flexcluster/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou
clusterName := tfModel.Name.ValueString()

connV2 := r.Client.AtlasV2
flexClusterResp, err := CreateFlexCluster(ctx, projectID, clusterName, flexClusterReq, connV2.FlexClustersApi)
flexClusterResp, err := CreateFlexClusterNew(ctx, projectID, clusterName, flexClusterReq, connV2.FlexClustersApi)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf(ErrorCreateFlex, err.Error()), fmt.Sprintf("Name: %s, Project ID: %s", clusterName, projectID))
return
Expand All @@ -80,6 +80,7 @@ func (r *rs) Create(ctx context.Context, req resource.CreateRequest, resp *resou
resp.Diagnostics.Append(diags...)
return
}
newFlexClusterModel.Timeouts = tfModel.Timeouts

if conversion.UseNilForEmpty(tfModel.Tags, newFlexClusterModel.Tags) {
newFlexClusterModel.Tags = types.MapNull(types.StringType)
Expand Down Expand Up @@ -113,6 +114,7 @@ func (r *rs) Read(ctx context.Context, req resource.ReadRequest, resp *resource.
resp.Diagnostics.Append(diags...)
return
}
newFlexClusterModel.Timeouts = flexClusterState.Timeouts

if conversion.UseNilForEmpty(flexClusterState.Tags, newFlexClusterModel.Tags) {
newFlexClusterModel.Tags = types.MapNull(types.StringType)
Expand Down Expand Up @@ -150,6 +152,7 @@ func (r *rs) Update(ctx context.Context, req resource.UpdateRequest, resp *resou
resp.Diagnostics.Append(diags...)
return
}
newFlexClusterModel.Timeouts = plan.Timeouts

if conversion.UseNilForEmpty(plan.Tags, newFlexClusterModel.Tags) {
newFlexClusterModel.Tags = types.MapNull(types.StringType)
Expand Down Expand Up @@ -203,7 +206,8 @@ func splitFlexClusterImportID(id string) (projectID, clusterName *string, err er
return
}

func CreateFlexCluster(ctx context.Context, projectID, clusterName string, flexClusterReq *admin.FlexClusterDescriptionCreate20241113, client admin.FlexClustersApi) (*admin.FlexClusterDescription20241113, error) {
// TODO: using CreateFlexClusterNew to avoid changes in adv_cluster and running their acc tests while doing this PR, rename to CreateFlexCluster and update adv_cluster call params before merging.
func CreateFlexClusterNew(ctx context.Context, projectID, clusterName string, flexClusterReq *admin.FlexClusterDescriptionCreate20241113, client admin.FlexClustersApi) (*admin.FlexClusterDescription20241113, error) {
_, _, err := client.CreateFlexCluster(ctx, projectID, flexClusterReq).Execute()
if err != nil {
return nil, err
Expand All @@ -221,6 +225,11 @@ func CreateFlexCluster(ctx context.Context, projectID, clusterName string, flexC
return flexClusterResp, nil
}

// TODO: keeping CreateFlexCluster to avoid changes in adv_cluster and running their acc tests while doing this PR, remove before merging.
func CreateFlexCluster(ctx context.Context, projectID, clusterName string, flexClusterReq *admin.FlexClusterDescriptionCreate20241113, client admin.FlexClustersApi) (*admin.FlexClusterDescription20241113, error) {
return CreateFlexClusterNew(ctx, projectID, clusterName, flexClusterReq, client)
}

func GetFlexCluster(ctx context.Context, projectID, clusterName string, client admin.FlexClustersApi) (*admin.FlexClusterDescription20241113, error) {
flexCluster, _, err := client.GetFlexCluster(ctx, projectID, clusterName).Execute()
if err != nil {
Expand Down
50 changes: 33 additions & 17 deletions internal/service/flexcluster/resource_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package flexcluster
import (
"context"

"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier"
Expand All @@ -21,14 +22,14 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"project_id": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
customplanmodifier.NonUpdatableStringAttributePlanModifier(),
customplanmodifier.CreateOnlyStringPlanModifier(),
},
MarkdownDescription: "Unique 24-hexadecimal character string that identifies the project.",
},
"name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
customplanmodifier.NonUpdatableStringAttributePlanModifier(),
customplanmodifier.CreateOnlyStringPlanModifier(),
},
MarkdownDescription: "Human-readable label that identifies the instance.",
},
Expand All @@ -37,7 +38,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"backing_provider_name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
customplanmodifier.NonUpdatableStringAttributePlanModifier(),
customplanmodifier.CreateOnlyStringPlanModifier(),
},
MarkdownDescription: "Cloud service provider on which MongoDB Cloud provisioned the flex cluster.",
},
Expand All @@ -58,7 +59,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
"region_name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
customplanmodifier.NonUpdatableStringAttributePlanModifier(),
customplanmodifier.CreateOnlyStringPlanModifier(),
},
MarkdownDescription: "Human-readable label that identifies the geographic location of your MongoDB flex cluster. The region you choose can affect network latency for clients accessing your databases. For a complete list of region names, see [AWS](https://docs.atlas.mongodb.com/reference/amazon-aws/#std-label-amazon-aws), [GCP](https://docs.atlas.mongodb.com/reference/google-gcp/), and [Azure](https://docs.atlas.mongodb.com/reference/microsoft-azure/).",
},
Expand Down Expand Up @@ -145,24 +146,39 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
MarkdownDescription: "Method by which the cluster maintains the MongoDB versions.",
},
"delete_on_create_timeout": schema.BoolAttribute{
Optional: true,
PlanModifiers: []planmodifier.Bool{
customplanmodifier.CreateOnlyBoolPlanModifier(),
},

MarkdownDescription: "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.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
MarkdownDescription: "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.",
MarkdownDescription: "Indicates whether to delete the created resource if a timeout is reached when waiting for completion. Default is `true`. If you suspect a transient error, wait before retrying to allow resource deletion to finish.",

The 3rd sentence was confusing to me, would just leave as one description.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is following the description in https://github.com/mongodb/terraform-provider-mongodbatlas/pull/3515/files#diff-e9d48cdf6bde5c047a1a852d6c3275175fe0a3d47f7ab543facdf8663b57fc57R334 which was reviewed by docs team.
I am open to changing it, but I would loop in docs team to review this and then change it in all resources.

},
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
Create: true,
Update: true,
Delete: true,
}),
},
}
}

type TFModel struct {
ProviderSettings types.Object `tfsdk:"provider_settings"`
ConnectionStrings types.Object `tfsdk:"connection_strings"`
Tags types.Map `tfsdk:"tags"`
CreateDate types.String `tfsdk:"create_date"`
ProjectId types.String `tfsdk:"project_id"`
Id types.String `tfsdk:"id"`
MongoDbversion types.String `tfsdk:"mongo_db_version"`
Name types.String `tfsdk:"name"`
ClusterType types.String `tfsdk:"cluster_type"`
StateName types.String `tfsdk:"state_name"`
VersionReleaseSystem types.String `tfsdk:"version_release_system"`
BackupSettings types.Object `tfsdk:"backup_settings"`
TerminationProtectionEnabled types.Bool `tfsdk:"termination_protection_enabled"`
Tags types.Map `tfsdk:"tags"`
MongoDbversion types.String `tfsdk:"mongo_db_version"`
ClusterType types.String `tfsdk:"cluster_type"`
CreateDate types.String `tfsdk:"create_date"`
ProjectId types.String `tfsdk:"project_id"`
Id types.String `tfsdk:"id"`
ProviderSettings types.Object `tfsdk:"provider_settings"`
Name types.String `tfsdk:"name"`
ConnectionStrings types.Object `tfsdk:"connection_strings"`
StateName types.String `tfsdk:"state_name"`
VersionReleaseSystem types.String `tfsdk:"version_release_system"`
BackupSettings types.Object `tfsdk:"backup_settings"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
DeleteOnCreateTimeout types.Bool `tfsdk:"delete_on_create_timeout"`
TerminationProtectionEnabled types.Bool `tfsdk:"termination_protection_enabled"`
}

type TFBackupSettings struct {
Expand Down
Loading