From cddcf578c1a40b86387cc842037048e2a9199bf4 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Wed, 6 Aug 2025 12:13:22 +0200 Subject: [PATCH 01/19] implement timeout for create and delete --- .../resource.go | 15 ++++++++++++-- .../resource_schema.go | 20 ++++++++++++------- .../state_transition.go | 20 +++++++++---------- .../state_transition_test.go | 4 ++-- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/internal/service/encryptionatrestprivateendpoint/resource.go b/internal/service/encryptionatrestprivateendpoint/resource.go index 2032bec2df..aecfb7bbe6 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource.go +++ b/internal/service/encryptionatrestprivateendpoint/resource.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/cleanup" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/retrystrategy" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate" @@ -63,7 +64,12 @@ func (r *encryptionAtRestPrivateEndpointRS) Create(ctx context.Context, req reso return } - finalResp, err := waitStateTransition(ctx, projectID, cloudProvider, createResp.GetId(), connV2.EncryptionAtRestUsingCustomerKeyManagementApi) + createTimeout := cleanup.ResolveTimeout(ctx, &earPrivateEndpointPlan.Timeouts, cleanup.OperationCreate, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + finalResp, err := waitStateTransition(ctx, projectID, cloudProvider, createResp.GetId(), connV2.EncryptionAtRestUsingCustomerKeyManagementApi, createTimeout) if err != nil { resp.Diagnostics.AddError("error when waiting for status transition in creation", err.Error()) return @@ -124,7 +130,12 @@ func (r *encryptionAtRestPrivateEndpointRS) Delete(ctx context.Context, req reso return } - model, err := WaitDeleteStateTransition(ctx, projectID, cloudProvider, endpointID, connV2.EncryptionAtRestUsingCustomerKeyManagementApi) + deleteTimeout := cleanup.ResolveTimeout(ctx, &earPrivateEndpointState.Timeouts, cleanup.OperationDelete, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + model, err := WaitDeleteStateTransition(ctx, projectID, cloudProvider, endpointID, connV2.EncryptionAtRestUsingCustomerKeyManagementApi, deleteTimeout) if err != nil { resp.Diagnostics.AddError("error when waiting for status transition in delete", err.Error()) return diff --git a/internal/service/encryptionatrestprivateendpoint/resource_schema.go b/internal/service/encryptionatrestprivateendpoint/resource_schema.go index fe8ad6fdc6..c48d06f0bf 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_schema.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_schema.go @@ -3,6 +3,7 @@ package encryptionatrestprivateendpoint import ( "context" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -38,16 +39,21 @@ func ResourceSchema(ctx context.Context) schema.Schema { Computed: true, MarkdownDescription: "State of the Encryption At Rest private endpoint.", }, + "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ + Create: true, + Delete: true, + }), }, } } type TFEarPrivateEndpointModel struct { - CloudProvider types.String `tfsdk:"cloud_provider"` - ErrorMessage types.String `tfsdk:"error_message"` - ProjectID types.String `tfsdk:"project_id"` - ID types.String `tfsdk:"id"` - PrivateEndpointConnectionName types.String `tfsdk:"private_endpoint_connection_name"` - RegionName types.String `tfsdk:"region_name"` - Status types.String `tfsdk:"status"` + CloudProvider types.String `tfsdk:"cloud_provider"` + ErrorMessage types.String `tfsdk:"error_message"` + ProjectID types.String `tfsdk:"project_id"` + ID types.String `tfsdk:"id"` + PrivateEndpointConnectionName types.String `tfsdk:"private_endpoint_connection_name"` + RegionName types.String `tfsdk:"region_name"` + Status types.String `tfsdk:"status"` + Timeouts timeouts.Value `tfsdk:"timeouts"` } diff --git a/internal/service/encryptionatrestprivateendpoint/state_transition.go b/internal/service/encryptionatrestprivateendpoint/state_transition.go index e3360450d7..ac9a202b5e 100644 --- a/internal/service/encryptionatrestprivateendpoint/state_transition.go +++ b/internal/service/encryptionatrestprivateendpoint/state_transition.go @@ -18,36 +18,36 @@ const ( defaultMinTimeout = 30 * time.Second // Smallest time to wait before refreshes ) -func waitStateTransition(ctx context.Context, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { - return WaitStateTransitionWithMinTimeout(ctx, defaultMinTimeout, projectID, cloudProvider, endpointID, client) +func waitStateTransition(ctx context.Context, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi, timeout time.Duration) (*admin.EARPrivateEndpoint, error) { + return WaitStateTransitionWithMinTimeoutAndTimeout(ctx, defaultMinTimeout, timeout, projectID, cloudProvider, endpointID, client) } -func WaitStateTransitionWithMinTimeout(ctx context.Context, minTimeout time.Duration, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { +func WaitStateTransitionWithMinTimeoutAndTimeout(ctx context.Context, minTimeout, timeout time.Duration, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { return waitStateTransitionForStates( ctx, []string{retrystrategy.RetryStrategyInitiatingState}, []string{retrystrategy.RetryStrategyPendingAcceptanceState, retrystrategy.RetryStrategyActiveState, retrystrategy.RetryStrategyFailedState}, - minTimeout, projectID, cloudProvider, endpointID, client) + minTimeout, timeout, projectID, cloudProvider, endpointID, client) } -func WaitDeleteStateTransition(ctx context.Context, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { - return WaitDeleteStateTransitionWithMinTimeout(ctx, defaultMinTimeout, projectID, cloudProvider, endpointID, client) +func WaitDeleteStateTransition(ctx context.Context, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi, timeout time.Duration) (*admin.EARPrivateEndpoint, error) { + return WaitDeleteStateTransitionWithMinTimeout(ctx, defaultMinTimeout, timeout, projectID, cloudProvider, endpointID, client) } -func WaitDeleteStateTransitionWithMinTimeout(ctx context.Context, minTimeout time.Duration, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { +func WaitDeleteStateTransitionWithMinTimeout(ctx context.Context, minTimeout, timeout time.Duration, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { return waitStateTransitionForStates( ctx, []string{retrystrategy.RetryStrategyDeletingState}, []string{retrystrategy.RetryStrategyDeletedState, retrystrategy.RetryStrategyFailedState}, - minTimeout, projectID, cloudProvider, endpointID, client) + minTimeout, timeout, projectID, cloudProvider, endpointID, client) } -func waitStateTransitionForStates(ctx context.Context, pending, target []string, minTimeout time.Duration, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { +func waitStateTransitionForStates(ctx context.Context, pending, target []string, minTimeout, timeout time.Duration, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { stateConf := &retry.StateChangeConf{ Pending: pending, Target: target, Refresh: refreshFunc(ctx, projectID, cloudProvider, endpointID, client), - Timeout: defaultTimeout, + Timeout: timeout, MinTimeout: minTimeout, Delay: 0, } diff --git a/internal/service/encryptionatrestprivateendpoint/state_transition_test.go b/internal/service/encryptionatrestprivateendpoint/state_transition_test.go index a579f63abc..fb7580159e 100644 --- a/internal/service/encryptionatrestprivateendpoint/state_transition_test.go +++ b/internal/service/encryptionatrestprivateendpoint/state_transition_test.go @@ -67,7 +67,7 @@ func TestStateTransition(t *testing.T) { modelResp, httpResp, err := resp.get() m.EXPECT().GetEncryptionAtRestPrivateEndpointExecute(mock.Anything).Return(modelResp, httpResp, err).Once() } - resp, err := encryptionatrestprivateendpoint.WaitStateTransitionWithMinTimeout(t.Context(), 1*time.Second, "project-id", "cloud-provider", "endpoint-id", m) + resp, err := encryptionatrestprivateendpoint.WaitStateTransitionWithMinTimeoutAndTimeout(t.Context(), 1*time.Second, 20*time.Minute, "project-id", "cloud-provider", "endpoint-id", m) assert.Equal(t, tc.expectedError, err != nil) if resp != nil { assert.Equal(t, tc.expectedState, resp.Status) @@ -111,7 +111,7 @@ func TestDeleteStateTransition(t *testing.T) { modelResp, httpResp, err := resp.get() m.EXPECT().GetEncryptionAtRestPrivateEndpointExecute(mock.Anything).Return(modelResp, httpResp, err).Once() } - resp, err := encryptionatrestprivateendpoint.WaitDeleteStateTransitionWithMinTimeout(t.Context(), 1*time.Second, "project-id", "cloud-provider", "endpoint-id", m) + resp, err := encryptionatrestprivateendpoint.WaitStateTransitionWithMinTimeoutAndTimeout(t.Context(), 1*time.Second, 20*time.Minute, "project-id", "cloud-provider", "endpoint-id", m) assert.Equal(t, tc.expectedError, err != nil) if resp != nil { assert.Equal(t, tc.expectedState, resp.Status) From 5d6e1f9853080b04009f442c9c7b3efd1ab25e19 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Wed, 6 Aug 2025 12:28:35 +0200 Subject: [PATCH 02/19] implement delete_on_create_timeout --- internal/common/cleanup/handle_timeout.go | 12 ++++++++++++ .../encryptionatrestprivateendpoint/resource.go | 15 ++++++++++++++- .../resource_schema.go | 10 ++++++++++ internal/service/flexcluster/resource.go | 13 +------------ 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/internal/common/cleanup/handle_timeout.go b/internal/common/cleanup/handle_timeout.go index 9004e5085a..3c51c9bb5d 100644 --- a/internal/common/cleanup/handle_timeout.go +++ b/internal/common/cleanup/handle_timeout.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/constant" ) @@ -99,3 +100,14 @@ func ResolveTimeout(ctx context.Context, t *timeouts.Value, operationName string } return timeoutDuration } + +// ResolveDeleteOnCreateTimeout returns true if delete_on_create_timeout should be enabled. +// Default behavior is true when not explicitly set to false. +func ResolveDeleteOnCreateTimeout(deleteOnCreateTimeout types.Bool) bool { + // If null or unknown, default to true + if deleteOnCreateTimeout.IsNull() || deleteOnCreateTimeout.IsUnknown() { + return true + } + // Otherwise use the explicit value + return deleteOnCreateTimeout.ValueBool() +} diff --git a/internal/service/encryptionatrestprivateendpoint/resource.go b/internal/service/encryptionatrestprivateendpoint/resource.go index aecfb7bbe6..87d8b699ae 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource.go +++ b/internal/service/encryptionatrestprivateendpoint/resource.go @@ -70,12 +70,22 @@ func (r *encryptionAtRestPrivateEndpointRS) Create(ctx context.Context, req reso } finalResp, err := waitStateTransition(ctx, projectID, cloudProvider, createResp.GetId(), connV2.EncryptionAtRestUsingCustomerKeyManagementApi, createTimeout) + err = cleanup.HandleCreateTimeout(cleanup.ResolveDeleteOnCreateTimeout(earPrivateEndpointPlan.DeleteOnCreateTimeout), err, func(ctxCleanup context.Context) error { + cleanResp, cleanErr := connV2.EncryptionAtRestUsingCustomerKeyManagementApi.RequestEncryptionAtRestPrivateEndpointDeletion(ctxCleanup, projectID, cloudProvider, createResp.GetId()).Execute() + if validate.StatusNotFound(cleanResp) { + return nil + } + return cleanErr + }) + if err != nil { resp.Diagnostics.AddError("error when waiting for status transition in creation", err.Error()) return } privateEndpointModel := NewTFEarPrivateEndpoint(*finalResp, projectID) + privateEndpointModel.Timeouts = earPrivateEndpointPlan.Timeouts + privateEndpointModel.DeleteOnCreateTimeout = earPrivateEndpointPlan.DeleteOnCreateTimeout resp.Diagnostics.Append(resp.State.Set(ctx, privateEndpointModel)...) diags := CheckErrorMessageAndStatus(finalResp) @@ -104,7 +114,10 @@ func (r *encryptionAtRestPrivateEndpointRS) Read(ctx context.Context, req resour return } - resp.Diagnostics.Append(resp.State.Set(ctx, NewTFEarPrivateEndpoint(*endpointModel, projectID))...) + privateEndpointModel := NewTFEarPrivateEndpoint(*endpointModel, projectID) + privateEndpointModel.Timeouts = earPrivateEndpointState.Timeouts + privateEndpointModel.DeleteOnCreateTimeout = earPrivateEndpointState.DeleteOnCreateTimeout + resp.Diagnostics.Append(resp.State.Set(ctx, privateEndpointModel)...) diags := CheckErrorMessageAndStatus(endpointModel) resp.Diagnostics.Append(diags...) diff --git a/internal/service/encryptionatrestprivateendpoint/resource_schema.go b/internal/service/encryptionatrestprivateendpoint/resource_schema.go index c48d06f0bf..6808d01307 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_schema.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_schema.go @@ -5,7 +5,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/customplanmodifier" ) func ResourceSchema(ctx context.Context) schema.Schema { @@ -43,6 +45,13 @@ func ResourceSchema(ctx context.Context) schema.Schema { Create: true, Delete: true, }), + "delete_on_create_timeout": schema.BoolAttribute{ + Optional: true, + PlanModifiers: []planmodifier.Bool{ + customplanmodifier.CreateOnlyBoolPlanModifier(), + }, + MarkdownDescription: "Indicates whether to delete the created resource if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the cleanup 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`.", + }, }, } } @@ -56,4 +65,5 @@ type TFEarPrivateEndpointModel struct { RegionName types.String `tfsdk:"region_name"` Status types.String `tfsdk:"status"` Timeouts timeouts.Value `tfsdk:"timeouts"` + DeleteOnCreateTimeout types.Bool `tfsdk:"delete_on_create_timeout"` } diff --git a/internal/service/flexcluster/resource.go b/internal/service/flexcluster/resource.go index 71967cc498..10e1d6eb72 100644 --- a/internal/service/flexcluster/resource.go +++ b/internal/service/flexcluster/resource.go @@ -80,7 +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 := resolveDeleteOnCreateTimeout(tfModel.DeleteOnCreateTimeout) + deleteOnCreateTimeout := cleanup.ResolveDeleteOnCreateTimeout(tfModel.DeleteOnCreateTimeout) err = cleanup.HandleCreateTimeout(deleteOnCreateTimeout, err, func(ctxCleanup context.Context) error { cleanResp, cleanErr := r.Client.AtlasV2.FlexClustersApi.DeleteFlexCluster(ctxCleanup, projectID, clusterName).Execute() if validate.StatusNotFound(cleanResp) { @@ -241,17 +241,6 @@ func splitFlexClusterImportID(id string) (projectID, clusterName *string, err er return } -// resolveDeleteOnCreateTimeout returns true if delete_on_create_timeout should be enabled. -// Default behavior is true when not explicitly set to false. -func resolveDeleteOnCreateTimeout(deleteOnCreateTimeout types.Bool) bool { - // If null or unknown, default to true - if deleteOnCreateTimeout.IsNull() || deleteOnCreateTimeout.IsUnknown() { - return true - } - // Otherwise use the explicit value - return deleteOnCreateTimeout.ValueBool() -} - func CreateFlexCluster(ctx context.Context, projectID, clusterName string, flexClusterReq *admin.FlexClusterDescriptionCreate20241113, client admin.FlexClustersApi, timeout *time.Duration) (*admin.FlexClusterDescription20241113, error) { _, _, err := client.CreateFlexCluster(ctx, projectID, flexClusterReq).Execute() if err != nil { From df219b9c48b2a9880e0a3903bd96bcd8c4b80199 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Wed, 6 Aug 2025 12:39:24 +0200 Subject: [PATCH 03/19] implement test --- .../resource_test.go | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/internal/service/encryptionatrestprivateendpoint/resource_test.go b/internal/service/encryptionatrestprivateendpoint/resource_test.go index e6ddbedc88..160771bb4a 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_test.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "regexp" "testing" "time" @@ -32,6 +33,33 @@ func TestAccEncryptionAtRestPrivateEndpoint_Azure_basic(t *testing.T) { resource.Test(t, *basicTestCaseAzure(t)) } +func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *testing.T) { + var ( + projectID = os.Getenv("MONGODB_ATLAS_PROJECT_EAR_PE_AWS_ID") + createTimeout = "1s" + deleteOnCreateTimeout = true + awsKms = &admin.AWSKMSConfiguration{ + Enabled: conversion.Pointer(true), + RequirePrivateNetworking: conversion.Pointer(true), + AccessKeyID: conversion.StringPtr(os.Getenv("AWS_ACCESS_KEY_ID")), + SecretAccessKey: conversion.StringPtr(os.Getenv("AWS_SECRET_ACCESS_KEY")), + CustomerMasterKeyID: conversion.StringPtr(os.Getenv("AWS_CUSTOMER_MASTER_KEY_ID")), + Region: conversion.StringPtr(os.Getenv("AWS_REGION")), + } + region = conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION")) + ) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.PreCheckEncryptionAtRestEnvAWS(t) }, + ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, + Steps: []resource.TestStep{ + { + Config: configAWSBasicWithTimeout(projectID, awsKms, region, acc.TimeoutConfig(&createTimeout, nil, nil, true), &deleteOnCreateTimeout), + ExpectError: regexp.MustCompile("will run cleanup because delete_on_create_timeout is true"), + }, + }, + }) +} + func basicTestCaseAzure(tb testing.TB) *resource.TestCase { tb.Helper() var ( @@ -316,7 +344,19 @@ func checkBasic(projectID, cloudProvider, region string, expectApproved bool) re } func configAWSBasic(projectID string, awsKms *admin.AWSKMSConfiguration, region string) string { + return configAWSBasicWithTimeout(projectID, awsKms, region, "", nil) +} + +func configAWSBasicWithTimeout(projectID string, awsKms *admin.AWSKMSConfiguration, region, timeoutConfig string, deleteOnCreateTimeout *bool) string { encryptionAtRestConfig := acc.ConfigAwsKms(projectID, awsKms, false, true, false) + + deleteOnCreateTimeoutConfig := "" + if deleteOnCreateTimeout != nil { + deleteOnCreateTimeoutConfig = fmt.Sprintf(` + delete_on_create_timeout = %[1]t + `, *deleteOnCreateTimeout) + } + config := fmt.Sprintf(` %[1]s @@ -324,11 +364,13 @@ func configAWSBasic(projectID string, awsKms *admin.AWSKMSConfiguration, region project_id = mongodbatlas_encryption_at_rest.test.project_id cloud_provider = "AWS" region_name = %[2]q + %[3]s + %[4]s } - %[3]s + %[5]s - `, encryptionAtRestConfig, region, configDS()) + `, encryptionAtRestConfig, region, deleteOnCreateTimeoutConfig, timeoutConfig, configDS()) return config } From 695283b68f094e29528b535f1e252de93977d43d Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Wed, 6 Aug 2025 12:48:08 +0200 Subject: [PATCH 04/19] data sources fix --- .../data_source.go | 4 ++-- .../data_source_schema.go | 17 ++++++++++++++--- .../encryptionatrestprivateendpoint/model.go | 17 +++++++++++++++-- .../model_test.go | 2 +- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/internal/service/encryptionatrestprivateendpoint/data_source.go b/internal/service/encryptionatrestprivateendpoint/data_source.go index d7116fc134..cde16dee69 100644 --- a/internal/service/encryptionatrestprivateendpoint/data_source.go +++ b/internal/service/encryptionatrestprivateendpoint/data_source.go @@ -31,7 +31,7 @@ func (d *encryptionAtRestPrivateEndpointDS) Schema(ctx context.Context, req data } func (d *encryptionAtRestPrivateEndpointDS) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var earPrivateEndpointConfig TFEarPrivateEndpointModel + var earPrivateEndpointConfig TFEarPrivateEndpointModelDS resp.Diagnostics.Append(req.Config.Get(ctx, &earPrivateEndpointConfig)...) if resp.Diagnostics.HasError() { return @@ -48,5 +48,5 @@ func (d *encryptionAtRestPrivateEndpointDS) Read(ctx context.Context, req dataso return } - resp.Diagnostics.Append(resp.State.Set(ctx, NewTFEarPrivateEndpoint(*endpointModel, projectID))...) + resp.Diagnostics.Append(resp.State.Set(ctx, NewTFEarPrivateEndpointDS(*endpointModel, projectID))...) } diff --git a/internal/service/encryptionatrestprivateendpoint/data_source_schema.go b/internal/service/encryptionatrestprivateendpoint/data_source_schema.go index 841373c896..77222b4355 100644 --- a/internal/service/encryptionatrestprivateendpoint/data_source_schema.go +++ b/internal/service/encryptionatrestprivateendpoint/data_source_schema.go @@ -41,8 +41,19 @@ func DSAttributes(withArguments bool) map[string]schema.Attribute { } } +// TFEarPrivateEndpointModelDS represents the model for data sources (without timeout fields) +type TFEarPrivateEndpointModelDS struct { + CloudProvider types.String `tfsdk:"cloud_provider"` + ErrorMessage types.String `tfsdk:"error_message"` + ProjectID types.String `tfsdk:"project_id"` + ID types.String `tfsdk:"id"` + PrivateEndpointConnectionName types.String `tfsdk:"private_endpoint_connection_name"` + RegionName types.String `tfsdk:"region_name"` + Status types.String `tfsdk:"status"` +} + type TFEncryptionAtRestPrivateEndpointsDSModel struct { - CloudProvider types.String `tfsdk:"cloud_provider"` - ProjectID types.String `tfsdk:"project_id"` - Results []TFEarPrivateEndpointModel `tfsdk:"results"` + CloudProvider types.String `tfsdk:"cloud_provider"` + ProjectID types.String `tfsdk:"project_id"` + Results []TFEarPrivateEndpointModelDS `tfsdk:"results"` } diff --git a/internal/service/encryptionatrestprivateendpoint/model.go b/internal/service/encryptionatrestprivateendpoint/model.go index 60f0cd8c4f..10846c1480 100644 --- a/internal/service/encryptionatrestprivateendpoint/model.go +++ b/internal/service/encryptionatrestprivateendpoint/model.go @@ -31,9 +31,9 @@ func NewEarPrivateEndpointReq(tfPlan *TFEarPrivateEndpointModel) *admin.EARPriva } func NewTFEarPrivateEndpoints(projectID, cloudProvider string, sdkResults []admin.EARPrivateEndpoint) *TFEncryptionAtRestPrivateEndpointsDSModel { - results := make([]TFEarPrivateEndpointModel, len(sdkResults)) + results := make([]TFEarPrivateEndpointModelDS, len(sdkResults)) for i := range sdkResults { - result := NewTFEarPrivateEndpoint(sdkResults[i], projectID) + result := NewTFEarPrivateEndpointDS(sdkResults[i], projectID) results[i] = result } return &TFEncryptionAtRestPrivateEndpointsDSModel{ @@ -42,3 +42,16 @@ func NewTFEarPrivateEndpoints(projectID, cloudProvider string, sdkResults []admi Results: results, } } + +// NewTFEarPrivateEndpointDS creates a new data source model without timeout fields +func NewTFEarPrivateEndpointDS(apiResp admin.EARPrivateEndpoint, projectID string) TFEarPrivateEndpointModelDS { + return TFEarPrivateEndpointModelDS{ + ProjectID: types.StringValue(projectID), + CloudProvider: conversion.StringNullIfEmpty(apiResp.GetCloudProvider()), + ErrorMessage: conversion.StringNullIfEmpty(apiResp.GetErrorMessage()), + ID: conversion.StringNullIfEmpty(apiResp.GetId()), + RegionName: conversion.StringNullIfEmpty(apiResp.GetRegionName()), + Status: conversion.StringNullIfEmpty(apiResp.GetStatus()), + PrivateEndpointConnectionName: conversion.StringNullIfEmpty(apiResp.GetPrivateEndpointConnectionName()), + } +} diff --git a/internal/service/encryptionatrestprivateendpoint/model_test.go b/internal/service/encryptionatrestprivateendpoint/model_test.go index 818bdf14e8..d204ed61ee 100644 --- a/internal/service/encryptionatrestprivateendpoint/model_test.go +++ b/internal/service/encryptionatrestprivateendpoint/model_test.go @@ -174,7 +174,7 @@ func TestEncryptionAtRestPrivateEndpointPluralDSSDKToTFModel(t *testing.T) { expectedTFModel: &encryptionatrestprivateendpoint.TFEncryptionAtRestPrivateEndpointsDSModel{ CloudProvider: types.StringValue(testCloudProvider), ProjectID: types.StringValue(testProjectID), - Results: []encryptionatrestprivateendpoint.TFEarPrivateEndpointModel{ + Results: []encryptionatrestprivateendpoint.TFEarPrivateEndpointModelDS{ { CloudProvider: types.StringValue(testCloudProvider), ErrorMessage: types.StringNull(), From 43444798890def937ddcbef8f546d2f64604d241 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Wed, 6 Aug 2025 13:03:43 +0200 Subject: [PATCH 05/19] docs and changelog --- .changelog/3561.txt | 7 +++++++ .../encryption_at_rest_private_endpoint.md | 13 +++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 .changelog/3561.txt diff --git a/.changelog/3561.txt b/.changelog/3561.txt new file mode 100644 index 0000000000..e0f4688dbf --- /dev/null +++ b/.changelog/3561.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/mongodbatlas_encryption_at_rest_private_endpoint: Adds `timeouts` attribute for create, update and delete operations +``` + +```release-note:enhancement +resource/mongodbatlas_encryption_at_rest_private_endpoint: Adds `delete_on_create_timeout` attribute to indicate whether to delete the resource if its creation times out +``` diff --git a/docs/resources/encryption_at_rest_private_endpoint.md b/docs/resources/encryption_at_rest_private_endpoint.md index 0accc86d81..fbaaa1d56e 100644 --- a/docs/resources/encryption_at_rest_private_endpoint.md +++ b/docs/resources/encryption_at_rest_private_endpoint.md @@ -99,6 +99,11 @@ resource "mongodbatlas_encryption_at_rest_private_endpoint" "endpoint" { - `project_id` (String) Unique 24-hexadecimal digit string that identifies your project. - `region_name` (String) Cloud provider region in which the Encryption At Rest private endpoint is located. +### Optional + +- `delete_on_create_timeout` (Boolean) Indicates whether to delete the created resource if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the cleanup 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`. +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) + ### Read-Only - `error_message` (String) Error message for failures associated with the Encryption At Rest private endpoint. @@ -106,6 +111,14 @@ resource "mongodbatlas_encryption_at_rest_private_endpoint" "endpoint" { - `private_endpoint_connection_name` (String) Connection name of the Azure Private Endpoint. - `status` (String) State of the Encryption At Rest private endpoint. + +### 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. + ## Import Encryption At Rest Private Endpoint resource can be imported using the project ID, cloud provider, and private endpoint ID. The format must be `{project_id}-{cloud_provider}-{private_endpoint_id}` e.g. From 5858275ff27ed0cf277afe893393f00d6e067210 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Wed, 6 Aug 2025 13:28:05 +0200 Subject: [PATCH 06/19] fix unit test --- .../encryptionatrestprivateendpoint/state_transition.go | 4 ++-- .../encryptionatrestprivateendpoint/state_transition_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/encryptionatrestprivateendpoint/state_transition.go b/internal/service/encryptionatrestprivateendpoint/state_transition.go index ac9a202b5e..7bd21c520f 100644 --- a/internal/service/encryptionatrestprivateendpoint/state_transition.go +++ b/internal/service/encryptionatrestprivateendpoint/state_transition.go @@ -31,10 +31,10 @@ func WaitStateTransitionWithMinTimeoutAndTimeout(ctx context.Context, minTimeout } func WaitDeleteStateTransition(ctx context.Context, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi, timeout time.Duration) (*admin.EARPrivateEndpoint, error) { - return WaitDeleteStateTransitionWithMinTimeout(ctx, defaultMinTimeout, timeout, projectID, cloudProvider, endpointID, client) + return WaitDeleteStateTransitionWithMinTimeoutAndTimeout(ctx, defaultMinTimeout, timeout, projectID, cloudProvider, endpointID, client) } -func WaitDeleteStateTransitionWithMinTimeout(ctx context.Context, minTimeout, timeout time.Duration, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { +func WaitDeleteStateTransitionWithMinTimeoutAndTimeout(ctx context.Context, minTimeout, timeout time.Duration, projectID, cloudProvider, endpointID string, client admin.EncryptionAtRestUsingCustomerKeyManagementApi) (*admin.EARPrivateEndpoint, error) { return waitStateTransitionForStates( ctx, []string{retrystrategy.RetryStrategyDeletingState}, diff --git a/internal/service/encryptionatrestprivateendpoint/state_transition_test.go b/internal/service/encryptionatrestprivateendpoint/state_transition_test.go index fb7580159e..8bf4974d58 100644 --- a/internal/service/encryptionatrestprivateendpoint/state_transition_test.go +++ b/internal/service/encryptionatrestprivateendpoint/state_transition_test.go @@ -111,7 +111,7 @@ func TestDeleteStateTransition(t *testing.T) { modelResp, httpResp, err := resp.get() m.EXPECT().GetEncryptionAtRestPrivateEndpointExecute(mock.Anything).Return(modelResp, httpResp, err).Once() } - resp, err := encryptionatrestprivateendpoint.WaitStateTransitionWithMinTimeoutAndTimeout(t.Context(), 1*time.Second, 20*time.Minute, "project-id", "cloud-provider", "endpoint-id", m) + resp, err := encryptionatrestprivateendpoint.WaitDeleteStateTransitionWithMinTimeoutAndTimeout(t.Context(), 1*time.Second, 20*time.Minute, "project-id", "cloud-provider", "endpoint-id", m) assert.Equal(t, tc.expectedError, err != nil) if resp != nil { assert.Equal(t, tc.expectedState, resp.Status) From 37c287e4229dca3e4a83565dc1f30a1c3497e8cd Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Wed, 6 Aug 2025 14:04:58 +0200 Subject: [PATCH 07/19] test fixes --- .../encryptionatrestprivateendpoint/resource_test.go | 11 +++++------ internal/service/flexcluster/resource_test.go | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/service/encryptionatrestprivateendpoint/resource_test.go b/internal/service/encryptionatrestprivateendpoint/resource_test.go index 160771bb4a..8648740ca3 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_test.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_test.go @@ -38,13 +38,12 @@ func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *t projectID = os.Getenv("MONGODB_ATLAS_PROJECT_EAR_PE_AWS_ID") createTimeout = "1s" deleteOnCreateTimeout = true - awsKms = &admin.AWSKMSConfiguration{ + awsKms = admin.AWSKMSConfiguration{ Enabled: conversion.Pointer(true), - RequirePrivateNetworking: conversion.Pointer(true), - AccessKeyID: conversion.StringPtr(os.Getenv("AWS_ACCESS_KEY_ID")), - SecretAccessKey: conversion.StringPtr(os.Getenv("AWS_SECRET_ACCESS_KEY")), CustomerMasterKeyID: conversion.StringPtr(os.Getenv("AWS_CUSTOMER_MASTER_KEY_ID")), - Region: conversion.StringPtr(os.Getenv("AWS_REGION")), + Region: conversion.StringPtr(conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION"))), + RoleId: conversion.StringPtr(os.Getenv("AWS_EAR_ROLE_ID")), + RequirePrivateNetworking: conversion.Pointer(false), } region = conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION")) ) @@ -53,7 +52,7 @@ func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *t ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, Steps: []resource.TestStep{ { - Config: configAWSBasicWithTimeout(projectID, awsKms, region, acc.TimeoutConfig(&createTimeout, nil, nil, true), &deleteOnCreateTimeout), + Config: configAWSBasicWithTimeout(projectID, &awsKms, region, acc.TimeoutConfig(&createTimeout, nil, nil, true), &deleteOnCreateTimeout), ExpectError: regexp.MustCompile("will run cleanup because delete_on_create_timeout is true"), }, }, diff --git a/internal/service/flexcluster/resource_test.go b/internal/service/flexcluster/resource_test.go index c112e90455..f6a81878a2 100644 --- a/internal/service/flexcluster/resource_test.go +++ b/internal/service/flexcluster/resource_test.go @@ -48,6 +48,7 @@ func TestAccFlexClusterRS_createTimeoutWithDeleteOnCreateFlex(t *testing.T) { } func TestAccFlexClusterRS_updateDeleteTimeout(t *testing.T) { + acc.SkipTestForCI(t) // Update is consistently too fast and it does not time out, making the test flaky var ( projectID = acc.ProjectIDExecution(t) clusterName = acc.RandomName() From 72aca36e3a08c159dcf1fb01e049d9d7fbc30eff Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Thu, 7 Aug 2025 08:46:08 +0200 Subject: [PATCH 08/19] fix test --- .../resource_test.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/service/encryptionatrestprivateendpoint/resource_test.go b/internal/service/encryptionatrestprivateendpoint/resource_test.go index 8648740ca3..6f359f3033 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_test.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_test.go @@ -45,6 +45,13 @@ func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *t RoleId: conversion.StringPtr(os.Getenv("AWS_EAR_ROLE_ID")), RequirePrivateNetworking: conversion.Pointer(false), } + awsKmsPrivateNetworking = admin.AWSKMSConfiguration{ + Enabled: conversion.Pointer(true), + CustomerMasterKeyID: conversion.StringPtr(os.Getenv("AWS_CUSTOMER_MASTER_KEY_ID")), + Region: conversion.StringPtr(conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION"))), + RoleId: conversion.StringPtr(os.Getenv("AWS_EAR_ROLE_ID")), + RequirePrivateNetworking: conversion.Pointer(true), + } region = conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION")) ) resource.ParallelTest(t, resource.TestCase{ @@ -52,7 +59,14 @@ func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *t ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, Steps: []resource.TestStep{ { - Config: configAWSBasicWithTimeout(projectID, &awsKms, region, acc.TimeoutConfig(&createTimeout, nil, nil, true), &deleteOnCreateTimeout), + Config: acc.ConfigAwsKms(projectID, &awsKms, false, true, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(earResourceName, "aws_kms_config.0.enabled", "true"), + resource.TestCheckResourceAttr(earResourceName, "aws_kms_config.0.require_private_networking", "false"), + ), + }, + { + Config: configAWSBasicWithTimeout(projectID, &awsKmsPrivateNetworking, region, acc.TimeoutConfig(&createTimeout, nil, nil, true), &deleteOnCreateTimeout), ExpectError: regexp.MustCompile("will run cleanup because delete_on_create_timeout is true"), }, }, From d77b3286fdf71ff722bab664c66d9a8c6ea62052 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Thu, 7 Aug 2025 09:35:21 +0200 Subject: [PATCH 09/19] use shared resources to avoid CANNOT_DISABLE_ENCRYPTION_AT_REST_DUE_TO_PRIVATE_ENDPOINTS --- .../resource_test.go | 52 ++++++++------- internal/testutil/acc/encryption_at_rest.go | 66 +++++++++++++++++++ internal/testutil/acc/shared_resource.go | 9 +++ 3 files changed, 103 insertions(+), 24 deletions(-) diff --git a/internal/service/encryptionatrestprivateendpoint/resource_test.go b/internal/service/encryptionatrestprivateendpoint/resource_test.go index 6f359f3033..120866ebc2 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_test.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_test.go @@ -35,38 +35,18 @@ func TestAccEncryptionAtRestPrivateEndpoint_Azure_basic(t *testing.T) { func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *testing.T) { var ( - projectID = os.Getenv("MONGODB_ATLAS_PROJECT_EAR_PE_AWS_ID") createTimeout = "1s" deleteOnCreateTimeout = true - awsKms = admin.AWSKMSConfiguration{ - Enabled: conversion.Pointer(true), - CustomerMasterKeyID: conversion.StringPtr(os.Getenv("AWS_CUSTOMER_MASTER_KEY_ID")), - Region: conversion.StringPtr(conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION"))), - RoleId: conversion.StringPtr(os.Getenv("AWS_EAR_ROLE_ID")), - RequirePrivateNetworking: conversion.Pointer(false), - } - awsKmsPrivateNetworking = admin.AWSKMSConfiguration{ - Enabled: conversion.Pointer(true), - CustomerMasterKeyID: conversion.StringPtr(os.Getenv("AWS_CUSTOMER_MASTER_KEY_ID")), - Region: conversion.StringPtr(conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION"))), - RoleId: conversion.StringPtr(os.Getenv("AWS_EAR_ROLE_ID")), - RequirePrivateNetworking: conversion.Pointer(true), - } - region = conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION")) + region = conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION")) + // Create encryption at rest configuration outside of test configuration to avoid cleanup issues + projectID = acc.EncryptionAtRestExecution(t) ) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acc.PreCheckEncryptionAtRestEnvAWS(t) }, ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, Steps: []resource.TestStep{ { - Config: acc.ConfigAwsKms(projectID, &awsKms, false, true, false), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(earResourceName, "aws_kms_config.0.enabled", "true"), - resource.TestCheckResourceAttr(earResourceName, "aws_kms_config.0.require_private_networking", "false"), - ), - }, - { - Config: configAWSBasicWithTimeout(projectID, &awsKmsPrivateNetworking, region, acc.TimeoutConfig(&createTimeout, nil, nil, true), &deleteOnCreateTimeout), + Config: configEARPrivateEndpointWithTimeout(projectID, region, acc.TimeoutConfig(&createTimeout, nil, nil, true), &deleteOnCreateTimeout), ExpectError: regexp.MustCompile("will run cleanup because delete_on_create_timeout is true"), }, }, @@ -388,6 +368,30 @@ func configAWSBasicWithTimeout(projectID string, awsKms *admin.AWSKMSConfigurati return config } +func configEARPrivateEndpointWithTimeout(projectID, region, timeoutConfig string, deleteOnCreateTimeout *bool) string { + deleteOnCreateTimeoutConfig := "" + if deleteOnCreateTimeout != nil { + deleteOnCreateTimeoutConfig = fmt.Sprintf(` + delete_on_create_timeout = %[1]t + `, *deleteOnCreateTimeout) + } + + config := fmt.Sprintf(` + resource "mongodbatlas_encryption_at_rest_private_endpoint" "test" { + project_id = %[1]q + cloud_provider = "AWS" + region_name = %[2]q + %[3]s + %[4]s + } + + %[5]s + + `, projectID, region, deleteOnCreateTimeoutConfig, timeoutConfig, configDS()) + + return config +} + func configDS() string { return ` data "mongodbatlas_encryption_at_rest_private_endpoint" "test" { diff --git a/internal/testutil/acc/encryption_at_rest.go b/internal/testutil/acc/encryption_at_rest.go index ed25a27f34..79fdc2cc12 100644 --- a/internal/testutil/acc/encryption_at_rest.go +++ b/internal/testutil/acc/encryption_at_rest.go @@ -3,12 +3,16 @@ package acc import ( "context" "fmt" + "os" "strconv" + "testing" "go.mongodb.org/atlas-sdk/v20250312005/admin" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion" + "github.com/stretchr/testify/require" ) func ConfigEARAzureKeyVault(projectID string, azure *admin.AzureKeyVault, useRequirePrivateNetworking, useDatasource bool) string { @@ -153,3 +157,65 @@ func EARImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { return rs.Primary.ID, nil } } + +// EncryptionAtRestExecution creates an encryption at rest configuration for test execution. +func EncryptionAtRestExecution(tb testing.TB) string { + tb.Helper() + SkipInUnitTest(tb) + require.True(tb, sharedInfo.init, "SetupSharedResources must called from TestMain test package") + + projectID := ProjectIDExecution(tb) + + sharedInfo.mu.Lock() + defer sharedInfo.mu.Unlock() + + // lazy creation so it's only done if really needed + if !sharedInfo.encryptionAtRestEnabled { + tb.Logf("Creating execution encryption at rest configuration for project: %s\n", projectID) + + // Create encryption at rest configuration using environment variables + awsKms := &admin.AWSKMSConfiguration{ + Enabled: conversion.Pointer(true), + CustomerMasterKeyID: conversion.StringPtr(os.Getenv("AWS_CUSTOMER_MASTER_KEY_ID")), + Region: conversion.StringPtr(conversion.AWSRegionToMongoDBRegion(os.Getenv("AWS_REGION"))), + RoleId: conversion.StringPtr(os.Getenv("AWS_EAR_ROLE_ID")), + RequirePrivateNetworking: conversion.Pointer(true), + } + + createEncryptionAtRest(tb, projectID, awsKms) + sharedInfo.encryptionAtRestEnabled = true + } + + return projectID +} + +func createEncryptionAtRest(tb testing.TB, projectID string, aws *admin.AWSKMSConfiguration) { + tb.Helper() + + encryptionAtRestReq := &admin.EncryptionAtRest{ + AwsKms: aws, + } + + _, _, err := ConnV2().EncryptionAtRestUsingCustomerKeyManagementApi.UpdateEncryptionAtRest(tb.Context(), projectID, encryptionAtRestReq).Execute() + require.NoError(tb, err, "Failed to create encryption at rest configuration for project: %s", projectID) +} + +func deleteEncryptionAtRest(projectID string) { + // Disable encryption at rest by setting all providers to disabled + encryptionAtRestReq := &admin.EncryptionAtRest{ + AwsKms: &admin.AWSKMSConfiguration{ + Enabled: conversion.Pointer(false), + }, + AzureKeyVault: &admin.AzureKeyVault{ + Enabled: conversion.Pointer(false), + }, + GoogleCloudKms: &admin.GoogleCloudKMS{ + Enabled: conversion.Pointer(false), + }, + } + + _, _, err := ConnV2().EncryptionAtRestUsingCustomerKeyManagementApi.UpdateEncryptionAtRest(context.Background(), projectID, encryptionAtRestReq).Execute() + if err != nil { + fmt.Printf("Failed to delete encryption at rest for project %s: %s\n", projectID, err) + } +} diff --git a/internal/testutil/acc/shared_resource.go b/internal/testutil/acc/shared_resource.go index 99106c21d7..cc7310db78 100644 --- a/internal/testutil/acc/shared_resource.go +++ b/internal/testutil/acc/shared_resource.go @@ -52,6 +52,14 @@ func cleanupSharedResources() { fmt.Printf("Deleting execution private link endpoint: %s, project id: %s, provider: %s\n", sharedInfo.privateLinkEndpointID, projectID, sharedInfo.privateLinkProviderName) deletePrivateLinkEndpoint(projectID, sharedInfo.privateLinkProviderName, sharedInfo.privateLinkEndpointID) } + if sharedInfo.encryptionAtRestEnabled { + projectID := sharedInfo.projectID + if projectID == "" { + projectID = projectIDLocal() + } + fmt.Printf("Deleting execution encryption at rest: project id: %s\n", projectID) + deleteEncryptionAtRest(projectID) + } if sharedInfo.projectID != "" { fmt.Printf("Deleting execution project: %s, id: %s\n", sharedInfo.projectName, sharedInfo.projectID) deleteProject(sharedInfo.projectID) @@ -217,6 +225,7 @@ var sharedInfo = struct { projects []projectInfo mu sync.Mutex muSleep sync.Mutex + encryptionAtRestEnabled bool init bool }{ projects: []projectInfo{}, From 6fe734567924a062b82bd6aee8c316874b8507af Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Thu, 7 Aug 2025 10:01:26 +0200 Subject: [PATCH 10/19] non parallel test --- .../service/encryptionatrestprivateendpoint/resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/encryptionatrestprivateendpoint/resource_test.go b/internal/service/encryptionatrestprivateendpoint/resource_test.go index 120866ebc2..2b9287dc75 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_test.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_test.go @@ -41,7 +41,7 @@ func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *t // Create encryption at rest configuration outside of test configuration to avoid cleanup issues projectID = acc.EncryptionAtRestExecution(t) ) - resource.ParallelTest(t, resource.TestCase{ + resource.Test(t, resource.TestCase{ PreCheck: func() { acc.PreCheckEncryptionAtRestEnvAWS(t) }, ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, Steps: []resource.TestStep{ From 90bb920c2069f8f41373c18a8f9da6f21b8b1112 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Thu, 7 Aug 2025 10:29:07 +0200 Subject: [PATCH 11/19] try new project for ear --- internal/testutil/acc/encryption_at_rest.go | 2 +- internal/testutil/acc/shared_resource.go | 37 +++++++++++++-------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/internal/testutil/acc/encryption_at_rest.go b/internal/testutil/acc/encryption_at_rest.go index 79fdc2cc12..65d3da7320 100644 --- a/internal/testutil/acc/encryption_at_rest.go +++ b/internal/testutil/acc/encryption_at_rest.go @@ -164,7 +164,7 @@ func EncryptionAtRestExecution(tb testing.TB) string { SkipInUnitTest(tb) require.True(tb, sharedInfo.init, "SetupSharedResources must called from TestMain test package") - projectID := ProjectIDExecution(tb) + projectID := createProject(tb, "encryption-at-rest-delete-on-create-timeout-test") sharedInfo.mu.Lock() defer sharedInfo.mu.Unlock() diff --git a/internal/testutil/acc/shared_resource.go b/internal/testutil/acc/shared_resource.go index cc7310db78..38cde261d8 100644 --- a/internal/testutil/acc/shared_resource.go +++ b/internal/testutil/acc/shared_resource.go @@ -53,12 +53,21 @@ func cleanupSharedResources() { deletePrivateLinkEndpoint(projectID, sharedInfo.privateLinkProviderName, sharedInfo.privateLinkEndpointID) } if sharedInfo.encryptionAtRestEnabled { - projectID := sharedInfo.projectID + projectID := sharedInfo.encryptionAtRestProjectID if projectID == "" { - projectID = projectIDLocal() + projectID = sharedInfo.projectID + if projectID == "" { + projectID = projectIDLocal() + } } fmt.Printf("Deleting execution encryption at rest: project id: %s\n", projectID) deleteEncryptionAtRest(projectID) + + // If we created a dedicated project, delete it + if sharedInfo.encryptionAtRestProjectID != "" && sharedInfo.encryptionAtRestProjectID != sharedInfo.projectID { + fmt.Printf("Deleting execution encryption at rest project: %s, id: %s\n", sharedInfo.encryptionAtRestProjectName, sharedInfo.encryptionAtRestProjectID) + deleteProject(sharedInfo.encryptionAtRestProjectID) + } } if sharedInfo.projectID != "" { fmt.Printf("Deleting execution project: %s, id: %s\n", sharedInfo.projectName, sharedInfo.projectID) @@ -216,17 +225,19 @@ type projectInfo struct { } var sharedInfo = struct { - projectID string - projectName string - clusterName string - streamInstanceName string - privateLinkEndpointID string - privateLinkProviderName string - projects []projectInfo - mu sync.Mutex - muSleep sync.Mutex - encryptionAtRestEnabled bool - init bool + encryptionAtRestProjectName string + projectName string + clusterName string + streamInstanceName string + privateLinkEndpointID string + privateLinkProviderName string + projectID string + encryptionAtRestProjectID string + projects []projectInfo + mu sync.Mutex + muSleep sync.Mutex + encryptionAtRestEnabled bool + init bool }{ projects: []projectInfo{}, } From 26b8dc7703f33abfdd3d997d4a88b6da4733b2b5 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Thu, 7 Aug 2025 11:23:17 +0200 Subject: [PATCH 12/19] create project --- internal/testutil/acc/encryption_at_rest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/testutil/acc/encryption_at_rest.go b/internal/testutil/acc/encryption_at_rest.go index 65d3da7320..79fdc2cc12 100644 --- a/internal/testutil/acc/encryption_at_rest.go +++ b/internal/testutil/acc/encryption_at_rest.go @@ -164,7 +164,7 @@ func EncryptionAtRestExecution(tb testing.TB) string { SkipInUnitTest(tb) require.True(tb, sharedInfo.init, "SetupSharedResources must called from TestMain test package") - projectID := createProject(tb, "encryption-at-rest-delete-on-create-timeout-test") + projectID := ProjectIDExecution(tb) sharedInfo.mu.Lock() defer sharedInfo.mu.Unlock() From 06aad46f349a8ddae591fa9bd28649f8e48aa829 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Thu, 7 Aug 2025 12:14:26 +0200 Subject: [PATCH 13/19] change description --- docs/resources/encryption_at_rest_private_endpoint.md | 2 +- .../service/encryptionatrestprivateendpoint/resource_schema.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/encryption_at_rest_private_endpoint.md b/docs/resources/encryption_at_rest_private_endpoint.md index fbaaa1d56e..0e6636e334 100644 --- a/docs/resources/encryption_at_rest_private_endpoint.md +++ b/docs/resources/encryption_at_rest_private_endpoint.md @@ -101,7 +101,7 @@ resource "mongodbatlas_encryption_at_rest_private_endpoint" "endpoint" { ### Optional -- `delete_on_create_timeout` (Boolean) Indicates whether to delete the created resource if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the cleanup 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`. +- `delete_on_create_timeout` (Boolean) 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`. - `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) ### Read-Only diff --git a/internal/service/encryptionatrestprivateendpoint/resource_schema.go b/internal/service/encryptionatrestprivateendpoint/resource_schema.go index 6808d01307..e04b0ee1fa 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_schema.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_schema.go @@ -50,7 +50,7 @@ func ResourceSchema(ctx context.Context) schema.Schema { PlanModifiers: []planmodifier.Bool{ customplanmodifier.CreateOnlyBoolPlanModifier(), }, - MarkdownDescription: "Indicates whether to delete the created resource if a timeout is reached when waiting for completion. When set to `true` and timeout occurs, it triggers the cleanup 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`.", + 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`.", }, }, } From 4aa5b4107f7e27d129dafcff0134c13f263812fa Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Fri, 8 Aug 2025 11:15:56 +0200 Subject: [PATCH 14/19] use configured project --- internal/testutil/acc/encryption_at_rest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/testutil/acc/encryption_at_rest.go b/internal/testutil/acc/encryption_at_rest.go index f535e35519..e40f8272ef 100644 --- a/internal/testutil/acc/encryption_at_rest.go +++ b/internal/testutil/acc/encryption_at_rest.go @@ -164,7 +164,7 @@ func EncryptionAtRestExecution(tb testing.TB) string { SkipInUnitTest(tb) require.True(tb, sharedInfo.init, "SetupSharedResources must called from TestMain test package") - projectID := ProjectIDExecution(tb) + projectID := os.Getenv("MONGODB_ATLAS_PROJECT_EAR_PE_ID") sharedInfo.mu.Lock() defer sharedInfo.mu.Unlock() From 622b1a12180549fcaac27180f65260102f41f2cc Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Fri, 8 Aug 2025 14:31:09 +0200 Subject: [PATCH 15/19] use correct projectn --- internal/testutil/acc/encryption_at_rest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/testutil/acc/encryption_at_rest.go b/internal/testutil/acc/encryption_at_rest.go index e40f8272ef..c1a1ebcb47 100644 --- a/internal/testutil/acc/encryption_at_rest.go +++ b/internal/testutil/acc/encryption_at_rest.go @@ -164,7 +164,7 @@ func EncryptionAtRestExecution(tb testing.TB) string { SkipInUnitTest(tb) require.True(tb, sharedInfo.init, "SetupSharedResources must called from TestMain test package") - projectID := os.Getenv("MONGODB_ATLAS_PROJECT_EAR_PE_ID") + projectID := os.Getenv("MONGODB_ATLAS_PROJECT_EAR_PE_AWS_ID") sharedInfo.mu.Lock() defer sharedInfo.mu.Unlock() From bf15a871a5fc03c96aef6001e8cc1c1dd005f343 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Fri, 8 Aug 2025 14:45:19 +0200 Subject: [PATCH 16/19] clean up shared resources --- internal/testutil/acc/shared_resource.go | 37 +++++++++--------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/internal/testutil/acc/shared_resource.go b/internal/testutil/acc/shared_resource.go index 38cde261d8..337a8a6de3 100644 --- a/internal/testutil/acc/shared_resource.go +++ b/internal/testutil/acc/shared_resource.go @@ -53,21 +53,12 @@ func cleanupSharedResources() { deletePrivateLinkEndpoint(projectID, sharedInfo.privateLinkProviderName, sharedInfo.privateLinkEndpointID) } if sharedInfo.encryptionAtRestEnabled { - projectID := sharedInfo.encryptionAtRestProjectID + projectID := sharedInfo.projectID if projectID == "" { - projectID = sharedInfo.projectID - if projectID == "" { - projectID = projectIDLocal() - } + projectID = projectIDLocal() } fmt.Printf("Deleting execution encryption at rest: project id: %s\n", projectID) deleteEncryptionAtRest(projectID) - - // If we created a dedicated project, delete it - if sharedInfo.encryptionAtRestProjectID != "" && sharedInfo.encryptionAtRestProjectID != sharedInfo.projectID { - fmt.Printf("Deleting execution encryption at rest project: %s, id: %s\n", sharedInfo.encryptionAtRestProjectName, sharedInfo.encryptionAtRestProjectID) - deleteProject(sharedInfo.encryptionAtRestProjectID) - } } if sharedInfo.projectID != "" { fmt.Printf("Deleting execution project: %s, id: %s\n", sharedInfo.projectName, sharedInfo.projectID) @@ -225,19 +216,17 @@ type projectInfo struct { } var sharedInfo = struct { - encryptionAtRestProjectName string - projectName string - clusterName string - streamInstanceName string - privateLinkEndpointID string - privateLinkProviderName string - projectID string - encryptionAtRestProjectID string - projects []projectInfo - mu sync.Mutex - muSleep sync.Mutex - encryptionAtRestEnabled bool - init bool + projectName string + clusterName string + streamInstanceName string + privateLinkEndpointID string + privateLinkProviderName string + projectID string + projects []projectInfo + mu sync.Mutex + muSleep sync.Mutex + encryptionAtRestEnabled bool + init bool }{ projects: []projectInfo{}, } From 7547e48d282385d4452a700cd73de126c0737bb6 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Fri, 8 Aug 2025 15:29:34 +0200 Subject: [PATCH 17/19] skip test --- docs/guides/2.0.0-upgrade-guide.md | 1 + .../encryptionatrestprivateendpoint/resource_test.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/docs/guides/2.0.0-upgrade-guide.md b/docs/guides/2.0.0-upgrade-guide.md index 2d9c05fc26..5b5e2449c7 100644 --- a/docs/guides/2.0.0-upgrade-guide.md +++ b/docs/guides/2.0.0-upgrade-guide.md @@ -16,6 +16,7 @@ The Terraform MongoDB Atlas Provider version 2.0.0 has the following new feature - `mongodbatlas_advanced_cluster` - `mongodbatlas_cloud_backup_snapshot` - `mongodbatlas_cluster_outage_simulation` + - `mongodbatlas_encryption_at_rest_private_endpoint` - `mongodbatlas_flex_cluster` - `mongodbatlas_network_peering` - `mongodbatlas_online_archive` diff --git a/internal/service/encryptionatrestprivateendpoint/resource_test.go b/internal/service/encryptionatrestprivateendpoint/resource_test.go index 2dce729aeb..615be893e7 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_test.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_test.go @@ -34,6 +34,14 @@ func TestAccEncryptionAtRestPrivateEndpoint_Azure_basic(t *testing.T) { } func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *testing.T) { + // This test is skipped because it creates a race condition with other tests: + // 1. This test creates an encryption at rest private endpoint with a 1s timeout, causing it to fail and trigger cleanup + // 2. The private endpoint deletion doesn't complete immediately + // 3. Other tests share the same project and attempt to disable encryption at rest during cleanup + // 4. MongoDB Atlas returns "CANNOT_DISABLE_ENCRYPTION_AT_REST_REQUIRE_PRIVATE_NETWORKING_WHILE_PRIVATE_ENDPOINTS_EXIST" + // because the private endpoint from this test is still being deleted + // This race condition occurs even when tests don't run in parallel due to the async nature of private endpoint deletion. + acc.SkipTestForCI(t) var ( createTimeout = "1s" deleteOnCreateTimeout = true From c24628295ddd55ccfee62bf1e6f14c04f76d0b5d Mon Sep 17 00:00:00 2001 From: Oriol Date: Mon, 11 Aug 2025 08:33:03 +0200 Subject: [PATCH 18/19] Update .changelog/3561.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .changelog/3561.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/3561.txt b/.changelog/3561.txt index e0f4688dbf..a9827c16f5 100644 --- a/.changelog/3561.txt +++ b/.changelog/3561.txt @@ -1,5 +1,5 @@ ```release-note:enhancement -resource/mongodbatlas_encryption_at_rest_private_endpoint: Adds `timeouts` attribute for create, update and delete operations +resource/mongodbatlas_encryption_at_rest_private_endpoint: Adds `timeouts` attribute for create and delete operations ``` ```release-note:enhancement From bb81f45b4fede6a47758c8aebf8a62e56e2f7c79 Mon Sep 17 00:00:00 2001 From: Oriol Arbusi Abadal Date: Mon, 11 Aug 2025 10:18:26 +0200 Subject: [PATCH 19/19] changes from dev --- .../service/encryptionatrestprivateendpoint/resource_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/encryptionatrestprivateendpoint/resource_test.go b/internal/service/encryptionatrestprivateendpoint/resource_test.go index 615be893e7..ee36f56a3c 100644 --- a/internal/service/encryptionatrestprivateendpoint/resource_test.go +++ b/internal/service/encryptionatrestprivateendpoint/resource_test.go @@ -54,7 +54,7 @@ func TestAccEncryptionAtRestPrivateEndpoint_createTimeoutWithDeleteOnCreate(t *t ProtoV6ProviderFactories: acc.TestAccProviderV6Factories, Steps: []resource.TestStep{ { - Config: configEARPrivateEndpointWithTimeout(projectID, region, acc.TimeoutConfig(&createTimeout, nil, nil, true), &deleteOnCreateTimeout), + Config: configEARPrivateEndpointWithTimeout(projectID, region, acc.TimeoutConfig(&createTimeout, nil, nil), &deleteOnCreateTimeout), ExpectError: regexp.MustCompile("will run cleanup because delete_on_create_timeout is true"), }, },