Skip to content

Commit a273130

Browse files
authored
feat: Long-running operation improvements for mongodbatlas_privatelink_endpoint_service resource (#3545)
* renames and timeout fixes * changelog * changelog * wait for delete to complete * Revert "wait for delete to complete" This reverts commit e994e9f. * use existing privatelink_endpoint and clean up
1 parent e29fda3 commit a273130

File tree

9 files changed

+186
-44
lines changed

9 files changed

+186
-44
lines changed

.changelog/3545.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/mongodbatlas_privatelink_endpoint_service: Adds `delete_on_create_timeout` attribute to indicate whether to delete the resource if its creation times out
3+
```

docs/resources/privatelink_endpoint_service.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ resource "mongodbatlas_privatelink_endpoint_service" "test" {
153153
* `gcp_project_id` - (Optional) Unique identifier of the GCP project in which you created your endpoints. Only for `GCP`.
154154
* `endpoints` - (Optional) Collection of individual private endpoints that comprise your endpoint group. Only for `GCP`. See below.
155155
* `timeouts`- (Optional) The duration of time to wait for Private Endpoint Service to be created or deleted. The timeout value is defined by a signed sequence of decimal numbers with a time unit suffix such as: `1h45m`, `300s`, `10m`, etc. The valid time units are: `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. The default timeout for Private Endpoint create & delete is `2h`. Learn more about timeouts [here](https://www.terraform.io/plugin/sdkv2/resources/retries-and-customizable-timeouts).
156+
* `delete_on_create_timeout`- (Optional) Flag that 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.
156157

157158
### `endpoints`
158159
* `ip_address` - (Optional) Private IP address of the endpoint you created in GCP.

internal/service/privatelinkendpoint/resource.go

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const (
2424
errorPrivateLinkEndpointsRead = "error reading MongoDB Private Endpoints Connection(%s): %s"
2525
errorPrivateLinkEndpointsDelete = "error deleting MongoDB Private Endpoints Connection(%s): %s"
2626
ErrorPrivateLinkEndpointsSetting = "error setting `%s` for MongoDB Private Endpoints Connection(%s): %s"
27-
oneMinute = 1 * time.Minute
2827
delayAndMinTimeout = 5 * time.Second
2928
)
3029

@@ -139,16 +138,7 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.
139138
return diag.FromErr(fmt.Errorf(errorPrivateLinkEndpointsCreate, err))
140139
}
141140

142-
stateConf := &retry.StateChangeConf{
143-
Pending: []string{"INITIATING", "DELETING"},
144-
Target: []string{"WAITING_FOR_USER", "FAILED", "DELETED", "AVAILABLE"},
145-
Refresh: refreshFunc(ctx, connV2, projectID, providerName, privateEndpoint.GetId()),
146-
Timeout: d.Timeout(schema.TimeoutCreate) - oneMinute, // If using a CRUD function with a timeout, any StateChangeConf timeouts should be configured below that duration to avoid returning the SDK context: deadline exceeded error instead of the retry logic error.
147-
MinTimeout: delayAndMinTimeout,
148-
Delay: delayAndMinTimeout,
149-
}
150-
151-
// Wait, catching any errors
141+
stateConf := CreateStateChangeConfig(ctx, connV2, projectID, providerName, privateEndpoint.GetId(), d.Timeout(schema.TimeoutCreate))
152142
_, errWait := stateConf.WaitForStateContext(ctx)
153143
deleteOnCreateTimeout := true // default value when not set
154144
if v, ok := d.GetOkExists("delete_on_create_timeout"); ok {
@@ -267,15 +257,7 @@ func resourceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.
267257

268258
log.Println("[INFO] Waiting for MongoDB Private Endpoints Connection to be destroyed")
269259

270-
stateConf := &retry.StateChangeConf{
271-
Pending: []string{"DELETING"},
272-
Target: []string{"DELETED", "FAILED"},
273-
Refresh: refreshFunc(ctx, connV2, projectID, providerName, privateLinkID),
274-
Timeout: d.Timeout(schema.TimeoutDelete),
275-
MinTimeout: delayAndMinTimeout,
276-
Delay: delayAndMinTimeout,
277-
}
278-
260+
stateConf := DeleteStateChangeConfig(ctx, connV2, projectID, providerName, privateLinkID, d.Timeout(schema.TimeoutDelete))
279261
_, err = stateConf.WaitForStateContext(ctx)
280262
if err != nil {
281263
return diag.FromErr(fmt.Errorf(errorPrivateLinkEndpointsDelete, privateLinkID, err))
@@ -346,3 +328,25 @@ func refreshFunc(ctx context.Context, client *admin.APIClient, projectID, provid
346328
return p, status, nil
347329
}
348330
}
331+
332+
func CreateStateChangeConfig(ctx context.Context, connV2 *admin.APIClient, projectID, providerName, privateLinkID string, timeout time.Duration) retry.StateChangeConf {
333+
return retry.StateChangeConf{
334+
Pending: []string{"INITIATING", "DELETING"},
335+
Target: []string{"WAITING_FOR_USER", "FAILED", "DELETED", "AVAILABLE"},
336+
Refresh: refreshFunc(ctx, connV2, projectID, providerName, privateLinkID),
337+
Timeout: timeout,
338+
MinTimeout: delayAndMinTimeout,
339+
Delay: delayAndMinTimeout,
340+
}
341+
}
342+
343+
func DeleteStateChangeConfig(ctx context.Context, connV2 *admin.APIClient, projectID, providerName, privateLinkID string, timeout time.Duration) retry.StateChangeConf {
344+
return retry.StateChangeConf{
345+
Pending: []string{"DELETING"},
346+
Target: []string{"DELETED", "FAILED"},
347+
Refresh: refreshFunc(ctx, connV2, projectID, providerName, privateLinkID),
348+
Timeout: timeout,
349+
MinTimeout: delayAndMinTimeout,
350+
Delay: delayAndMinTimeout,
351+
}
352+
}
Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
1313
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1414
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
15+
16+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/cleanup"
1517
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
1618
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate"
1719
"github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
@@ -24,13 +26,15 @@ const (
2426
ErrorServiceEndpointRead = "error reading MongoDB Private Service Endpoint Connection(%s): %s"
2527
errorEndpointDelete = "error deleting MongoDB Private Service Endpoint Connection(%s): %s"
2628
ErrorEndpointSetting = "error setting `%s` for MongoDB Private Service Endpoint Connection(%s): %s"
29+
oneMinute = 1 * time.Minute
30+
delayAndMinTimeout = 10 * time.Second
2731
)
2832

2933
func Resource() *schema.Resource {
3034
return &schema.Resource{
31-
CreateContext: resourceCreate,
32-
ReadWithoutTimeout: resourceRead,
33-
DeleteContext: resourceDelete,
35+
CreateWithoutTimeout: resourceCreate,
36+
ReadWithoutTimeout: resourceRead,
37+
DeleteWithoutTimeout: resourceDelete,
3438
Importer: &schema.ResourceImporter{
3539
StateContext: resourceImportState,
3640
},
@@ -100,6 +104,12 @@ func Resource() *schema.Resource {
100104
ForceNew: true,
101105
ConflictsWith: []string{"private_endpoint_ip_address"},
102106
},
107+
"delete_on_create_timeout": { // Don't use Default: true to avoid unplanned changes when upgrading from previous versions.
108+
Type: schema.TypeBool,
109+
Optional: true,
110+
ForceNew: true,
111+
Description: "Flag that indicates whether to delete the resource if creation times out. Default is true.",
112+
},
103113
"endpoints": {
104114
Type: schema.TypeList,
105115
Optional: true,
@@ -173,23 +183,31 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.
173183
Pending: []string{"NONE", "INITIATING", "PENDING_ACCEPTANCE", "PENDING", "DELETING", "VERIFIED"},
174184
Target: []string{"AVAILABLE", "REJECTED", "DELETED", "FAILED"},
175185
Refresh: resourceRefreshFunc(ctx, connV2, projectID, providerName, privateLinkID, endpointServiceID),
176-
Timeout: d.Timeout(schema.TimeoutCreate) - time.Minute, // When using a CRUD function with a timeout, any StateChangeConf timeouts must be configured below that duration to avoid returning the SDK context: deadline exceeded error instead of the retry logic error.
177-
MinTimeout: 5 * time.Second,
178-
Delay: 1 * time.Minute,
186+
Timeout: d.Timeout(schema.TimeoutCreate) - oneMinute, // When using a CRUD function with a timeout, any StateChangeConf timeouts must be configured below that duration to avoid returning the SDK context: deadline exceeded error instead of the retry logic error.
187+
MinTimeout: delayAndMinTimeout,
188+
Delay: delayAndMinTimeout,
179189
}
180190
// Wait, catching any errors
181-
_, err = stateConf.WaitForStateContext(ctx)
182-
if err != nil {
183-
return diag.FromErr(fmt.Errorf(errorServiceEndpointAdd, endpointServiceID, privateLinkID, err))
191+
_, errWait := stateConf.WaitForStateContext(ctx)
192+
deleteOnCreateTimeout := true // default value when not set
193+
if v, ok := d.GetOkExists("delete_on_create_timeout"); ok {
194+
deleteOnCreateTimeout = v.(bool)
195+
}
196+
errWait = cleanup.HandleCreateTimeout(deleteOnCreateTimeout, errWait, func(ctxCleanup context.Context) error {
197+
_, errCleanup := connV2.PrivateEndpointServicesApi.DeletePrivateEndpoint(ctxCleanup, projectID, providerName, endpointServiceID, privateLinkID).Execute()
198+
return errCleanup
199+
})
200+
if errWait != nil {
201+
return diag.FromErr(fmt.Errorf(errorServiceEndpointAdd, endpointServiceID, privateLinkID, errWait))
184202
}
185203

186204
clusterConf := &retry.StateChangeConf{
187205
Pending: []string{"REPEATING", "PENDING"},
188206
Target: []string{"IDLE", "DELETED"},
189207
Refresh: advancedcluster.ResourceClusterListAdvancedRefreshFunc(ctx, projectID, connV2.ClustersApi),
190-
Timeout: d.Timeout(schema.TimeoutCreate) - time.Minute, // When using a CRUD function with a timeout, any StateChangeConf timeouts must be configured below that duration to avoid returning the SDK context: deadline exceeded error instead of the retry logic error.
191-
MinTimeout: 5 * time.Second,
192-
Delay: 1 * time.Minute,
208+
Timeout: d.Timeout(schema.TimeoutCreate) - oneMinute, // When using a CRUD function with a timeout, any StateChangeConf timeouts must be configured below that duration to avoid returning the SDK context: deadline exceeded error instead of the retry logic error.
209+
MinTimeout: delayAndMinTimeout,
210+
Delay: delayAndMinTimeout,
193211
}
194212

195213
if _, err = clusterConf.WaitForStateContext(ctx); err != nil {
@@ -299,8 +317,8 @@ func resourceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.
299317
Target: []string{"REJECTED", "DELETED", "FAILED"},
300318
Refresh: resourceRefreshFunc(ctx, connV2, projectID, providerName, privateLinkID, endpointServiceID),
301319
Timeout: d.Timeout(schema.TimeoutDelete),
302-
MinTimeout: 5 * time.Second,
303-
Delay: 3 * time.Second,
320+
MinTimeout: delayAndMinTimeout,
321+
Delay: delayAndMinTimeout,
304322
}
305323

306324
// Wait, catching any errors
@@ -314,8 +332,8 @@ func resourceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.
314332
Target: []string{"IDLE", "DELETED"},
315333
Refresh: advancedcluster.ResourceClusterListAdvancedRefreshFunc(ctx, projectID, connV2.ClustersApi),
316334
Timeout: d.Timeout(schema.TimeoutDelete),
317-
MinTimeout: 5 * time.Second,
318-
Delay: 1 * time.Minute,
335+
MinTimeout: delayAndMinTimeout,
336+
Delay: delayAndMinTimeout,
319337
}
320338

321339
if _, err = clusterConf.WaitForStateContext(ctx); err != nil {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,30 @@ func TestAccNetworkRSPrivateLinkEndpointServiceAWS_Failed(t *testing.T) {
4242
})
4343
}
4444

45+
func TestAccNetworkRSPrivateLinkEndpointService_deleteOnCreateTimeout(t *testing.T) {
46+
var (
47+
resourceSuffix = "test"
48+
providerName = "AWS"
49+
region = os.Getenv("AWS_REGION")
50+
// Create private link endpoint outside of test configuration to avoid cleanup issues
51+
projectID, privateLinkEndpointID = acc.PrivateLinkEndpointIDExecution(t, providerName, region)
52+
)
53+
54+
resource.Test(t, resource.TestCase{
55+
PreCheck: func() { acc.PreCheckBasic(t) },
56+
CheckDestroy: checkDestroy,
57+
ProtoV6ProviderFactories: acc.TestAccProviderV6Factories,
58+
Steps: []resource.TestStep{
59+
{
60+
Config: configDeleteOnCreateTimeoutWithExistingEndpoint(
61+
projectID, providerName, privateLinkEndpointID, resourceSuffix, "1s", true,
62+
),
63+
ExpectError: regexp.MustCompile("will run cleanup because delete_on_create_timeout is true"),
64+
},
65+
},
66+
})
67+
}
68+
4569
func basicAWSTestCase(tb testing.TB) *resource.TestCase {
4670
tb.Helper()
4771
acc.SkipTestForCI(tb) // needs AWS configuration
@@ -189,3 +213,19 @@ func configFailAWS(projectID, providerName, region, resourceSuffix string) strin
189213
}
190214
`, projectID, providerName, region, resourceSuffix)
191215
}
216+
217+
func configDeleteOnCreateTimeoutWithExistingEndpoint(projectID, providerName, privateLinkEndpointID, resourceSuffix, timeout string, deleteOnTimeout bool) string {
218+
return fmt.Sprintf(`
219+
resource "mongodbatlas_privatelink_endpoint_service" %[4]q {
220+
project_id = %[1]q
221+
private_link_id = %[3]q
222+
endpoint_service_id = "vpce-11111111111111111"
223+
provider_name = %[2]q
224+
delete_on_create_timeout = %[6]t
225+
226+
timeouts {
227+
create = %[5]q
228+
}
229+
}
230+
`, projectID, providerName, privateLinkEndpointID, resourceSuffix, timeout, deleteOnTimeout)
231+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package acc
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/privatelinkendpoint"
10+
"github.com/stretchr/testify/require"
11+
"go.mongodb.org/atlas-sdk/v20250312005/admin"
12+
)
13+
14+
func createPrivateLinkEndpoint(tb testing.TB, projectID, providerName, region string) string {
15+
tb.Helper()
16+
17+
request := &admin.CloudProviderEndpointServiceRequest{
18+
ProviderName: providerName,
19+
Region: region,
20+
}
21+
22+
privateEndpoint, _, err := ConnV2().PrivateEndpointServicesApi.CreatePrivateEndpointService(tb.Context(), projectID, request).Execute()
23+
require.NoError(tb, err)
24+
25+
stateConf := privatelinkendpoint.CreateStateChangeConfig(tb.Context(), ConnV2(), projectID, providerName, privateEndpoint.GetId(), 1*time.Hour)
26+
_, err = stateConf.WaitForStateContext(tb.Context())
27+
require.NoError(tb, err, "Private link endpoint creation failed: %s, err: %s", privateEndpoint.GetId(), err)
28+
29+
return privateEndpoint.GetId()
30+
}
31+
32+
func deletePrivateLinkEndpoint(projectID, providerName, privateLinkEndpointID string) {
33+
_, err := ConnV2().PrivateEndpointServicesApi.DeletePrivateEndpointService(context.Background(), projectID, providerName, privateLinkEndpointID).Execute()
34+
if err != nil {
35+
fmt.Printf("Failed to delete private link endpoint %s: %s\n", privateLinkEndpointID, err)
36+
return
37+
}
38+
stateConf := privatelinkendpoint.DeleteStateChangeConfig(context.Background(), ConnV2(), projectID, providerName, privateLinkEndpointID, 1*time.Hour)
39+
_, err = stateConf.WaitForStateContext(context.Background())
40+
if err != nil {
41+
fmt.Printf("Failed to delete private link endpoint %s: %s\n", privateLinkEndpointID, err)
42+
}
43+
}

internal/testutil/acc/shared_resource.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@ func cleanupSharedResources() {
4444
fmt.Printf("Failed to delete stream instances: for execution project %s, error: %s\n", projectID, err)
4545
}
4646
}
47+
if sharedInfo.privateLinkEndpointID != "" {
48+
projectID := sharedInfo.projectID
49+
if projectID == "" {
50+
projectID = projectIDLocal()
51+
}
52+
fmt.Printf("Deleting execution private link endpoint: %s, project id: %s, provider: %s\n", sharedInfo.privateLinkEndpointID, projectID, sharedInfo.privateLinkProviderName)
53+
deletePrivateLinkEndpoint(projectID, sharedInfo.privateLinkProviderName, sharedInfo.privateLinkEndpointID)
54+
}
4755
if sharedInfo.projectID != "" {
4856
fmt.Printf("Deleting execution project: %s, id: %s\n", sharedInfo.projectName, sharedInfo.projectID)
4957
deleteProject(sharedInfo.projectID)
@@ -170,21 +178,46 @@ func SerialSleep(tb testing.TB) {
170178
time.Sleep(5 * time.Second)
171179
}
172180

181+
// PrivateLinkEndpointIDExecution returns a private link endpoint id created for the execution of the tests.
182+
// The endpoint is created with provider "AWS" and region from environment variable.
183+
// When `MONGODB_ATLAS_PROJECT_ID` is defined, it is used instead of creating a project.
184+
func PrivateLinkEndpointIDExecution(tb testing.TB, providerName, region string) (projectID, privateLinkEndpointID string) {
185+
tb.Helper()
186+
SkipInUnitTest(tb)
187+
require.True(tb, sharedInfo.init, "SetupSharedResources must called from TestMain test package")
188+
189+
projectID = ProjectIDExecution(tb) // ensure the execution project is created before endpoint creation
190+
191+
sharedInfo.mu.Lock()
192+
defer sharedInfo.mu.Unlock()
193+
194+
// lazy creation so it's only done if really needed
195+
if sharedInfo.privateLinkEndpointID == "" {
196+
tb.Logf("Creating execution private link endpoint for provider: %s, region: %s\n", providerName, region)
197+
sharedInfo.privateLinkEndpointID = createPrivateLinkEndpoint(tb, projectID, providerName, region)
198+
sharedInfo.privateLinkProviderName = providerName
199+
}
200+
201+
return projectID, sharedInfo.privateLinkEndpointID
202+
}
203+
173204
type projectInfo struct {
174205
id string
175206
name string
176207
nodeCount int
177208
}
178209

179210
var sharedInfo = struct {
180-
projectID string
181-
projectName string
182-
clusterName string
183-
streamInstanceName string
184-
projects []projectInfo
185-
mu sync.Mutex
186-
muSleep sync.Mutex
187-
init bool
211+
projectID string
212+
projectName string
213+
clusterName string
214+
streamInstanceName string
215+
privateLinkEndpointID string
216+
privateLinkProviderName string
217+
projects []projectInfo
218+
mu sync.Mutex
219+
muSleep sync.Mutex
220+
init bool
188221
}{
189222
projects: []projectInfo{},
190223
}

0 commit comments

Comments
 (0)