Skip to content

Commit 47f642f

Browse files
lantolixargom
andauthored
feat: Network_peering long-running operation improvements (#3515)
* remove logs * use WithoutTimeout funcs * timeouts * delete_on_create_timeout * changelog * more generic doc so can be used for any resource * rename to errWait * TestAccNetworkNetworkPeering_timeouts * change doc * add clarification * also change error message * fix TestAccNetworkRSNetworkPeering_Azure * don't use default * Revert "fix TestAccNetworkRSNetworkPeering_Azure" This reverts commit ca9f4e0. * no need to exclude delete_on_create_timeout in import if Default is not used * use d.GetOkExists to distinguish empty from zero (false) value * Update .changelog/3515.txt Co-authored-by: Javier Armendáriz <[email protected]> * Update internal/common/cleanup/handle_timeout.go Co-authored-by: Javier Armendáriz <[email protected]> * improve changelog * use new context in cleanup --------- Co-authored-by: Javier Armendáriz <[email protected]>
1 parent cc7b67e commit 47f642f

File tree

6 files changed

+112
-38
lines changed

6 files changed

+112
-38
lines changed

.changelog/3515.txt

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

docs/resources/network_peering.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ resource "mongodbatlas_network_peering" "test" {
330330
* `project_id` - (Required) The unique ID for the MongoDB Atlas project.
331331
* `container_id` - (Required) Unique identifier of the MongoDB Atlas container for the provider (GCP) or provider/region (AWS, AZURE). You can create an MongoDB Atlas container using the network_container resource or it can be obtained from the cluster returned values if a cluster has been created before the first container.
332332
* `provider_name` - (Required) Cloud provider to whom the peering connection is being made. (Possible Values `AWS`, `AZURE`, `GCP`).
333+
* `timeouts`- (Optional) The duration of time to wait for the resource to be created, updated, or deleted. The default timeout is `1h`. 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`. Learn more about timeouts [here](https://www.terraform.io/plugin/sdkv2/resources/retries-and-customizable-timeouts).
334+
* `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.
333335

334336
**AWS ONLY:**
335337

internal/common/cleanup/on_timeout.go renamed to internal/common/cleanup/handle_timeout.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,31 @@ import (
77
"time"
88

99
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
1011
)
1112

1213
const (
1314
CleanupWarning = "Failed to create resource. Will run cleanup due to the operation timing out"
1415
)
1516

17+
// HandleCreateTimeout helps to implement Create in long-running operations.
18+
// It deletes the resource if the creation times out and `delete_on_create_timeout` is enabled.
19+
// It returns an error with additional information which should be used instead of the original error.
20+
func HandleCreateTimeout(deleteOnCreateTimeout bool, errWait error, cleanup func(context.Context) error) error {
21+
if _, isTimeoutErr := errWait.(*retry.TimeoutError); !isTimeoutErr {
22+
return errWait
23+
}
24+
if !deleteOnCreateTimeout {
25+
return errors.Join(errWait, errors.New("cleanup won't be run because delete_on_create_timeout is false"))
26+
}
27+
errWait = errors.Join(errWait, errors.New("will run cleanup because delete_on_create_timeout is true. If you suspect a transient error, wait before retrying to allow resource deletion to finish"))
28+
// cleanup uses a new context as existing one is expired.
29+
if errCleanup := cleanup(context.Background()); errCleanup != nil {
30+
errWait = errors.Join(errWait, errors.New("cleanup failed: "+errCleanup.Error()))
31+
}
32+
return errWait
33+
}
34+
1635
// OnTimeout creates a new context with a timeout and a deferred function that will run `cleanup` when the context hit the timeout (no timeout=no-op).
1736
// Remember to always call the returned `deferCall` function: `defer deferCall()`.
1837
// `warningDetail` should have resource identifiable information, for example cluster name and project ID.

internal/service/networkpeering/resource_network_peering.go

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"log"
87
"strings"
98
"time"
109

1110
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1211
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
1312
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1413
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
14+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/cleanup"
1515
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
1616
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate"
1717
"github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
@@ -24,17 +24,24 @@ const (
2424
errorPeersRead = "error reading MongoDB Network Peering Connection (%s): %s"
2525
errorPeersDelete = "error deleting MongoDB Network Peering Connection (%s): %s"
2626
errorPeersUpdate = "error updating MongoDB Network Peering Connection (%s): %s"
27+
28+
minTimeout = 10 * time.Second
2729
)
2830

2931
func Resource() *schema.Resource {
3032
return &schema.Resource{
31-
CreateContext: resourceCreate,
32-
ReadContext: resourceRead,
33-
UpdateContext: resourceUpdate,
34-
DeleteContext: resourceDelete,
33+
CreateWithoutTimeout: resourceCreate,
34+
ReadWithoutTimeout: resourceRead,
35+
UpdateWithoutTimeout: resourceUpdate,
36+
DeleteWithoutTimeout: resourceDelete,
3537
Importer: &schema.ResourceImporter{
3638
StateContext: resourceImportState,
3739
},
40+
Timeouts: &schema.ResourceTimeout{
41+
Create: schema.DefaultTimeout(1 * time.Hour),
42+
Update: schema.DefaultTimeout(1 * time.Hour),
43+
Delete: schema.DefaultTimeout(1 * time.Hour),
44+
},
3845
Schema: map[string]*schema.Schema{
3946
"project_id": {
4047
Type: schema.TypeString,
@@ -92,7 +99,6 @@ func Resource() *schema.Resource {
9299
Type: schema.TypeString,
93100
Computed: true,
94101
},
95-
96102
"atlas_cidr_block": {
97103
Type: schema.TypeString,
98104
Optional: true,
@@ -157,6 +163,11 @@ func Resource() *schema.Resource {
157163
Type: schema.TypeString,
158164
Computed: true,
159165
},
166+
"delete_on_create_timeout": { // Don't use Default: true to avoid unplanned changes when upgrading from previous versions.
167+
Type: schema.TypeBool,
168+
Optional: true,
169+
Description: "Flag that indicates whether to delete the resource if creation times out. Default is true.",
170+
},
160171
},
161172
}
162173
}
@@ -244,19 +255,27 @@ func resourceCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.
244255
if err != nil {
245256
return diag.FromErr(fmt.Errorf(errorPeersCreate, err))
246257
}
258+
peerID := peer.GetId()
247259

248260
stateConf := &retry.StateChangeConf{
249261
Pending: []string{"INITIATING", "FINALIZING", "ADDING_PEER", "WAITING_FOR_USER"},
250262
Target: []string{"FAILED", "AVAILABLE", "PENDING_ACCEPTANCE"},
251-
Refresh: resourceRefreshFunc(ctx, peer.GetId(), projectID, peerRequest.GetContainerId(), conn.NetworkPeeringApi),
252-
Timeout: 1 * time.Hour,
253-
MinTimeout: 10 * time.Second,
254-
Delay: 30 * time.Second,
263+
Refresh: resourceRefreshFunc(ctx, peerID, projectID, peerRequest.GetContainerId(), conn.NetworkPeeringApi),
264+
Timeout: d.Timeout(schema.TimeoutCreate),
265+
MinTimeout: minTimeout,
266+
Delay: minTimeout,
255267
}
256-
257-
_, err = stateConf.WaitForStateContext(ctx)
258-
if err != nil {
259-
return diag.FromErr(fmt.Errorf(errorPeersCreate, err))
268+
_, errWait := stateConf.WaitForStateContext(ctx)
269+
deleteOnCreateTimeout := true // default value when not set
270+
if v, ok := d.GetOkExists("delete_on_create_timeout"); ok {
271+
deleteOnCreateTimeout = v.(bool)
272+
}
273+
errWait = cleanup.HandleCreateTimeout(deleteOnCreateTimeout, errWait, func(ctxCleanup context.Context) error {
274+
_, _, errCleanup := conn.NetworkPeeringApi.DeletePeeringConnection(ctxCleanup, projectID, peerID).Execute()
275+
return errCleanup
276+
})
277+
if errWait != nil {
278+
return diag.Errorf(errorPeersCreate, errWait)
260279
}
261280

262281
d.SetId(conversion.EncodeStateID(map[string]string{
@@ -459,9 +478,9 @@ func resourceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.
459478
Pending: []string{"INITIATING", "FINALIZING", "ADDING_PEER", "WAITING_FOR_USER"},
460479
Target: []string{"FAILED", "AVAILABLE", "PENDING_ACCEPTANCE"},
461480
Refresh: resourceRefreshFunc(ctx, peerID, projectID, "", conn.NetworkPeeringApi),
462-
Timeout: d.Timeout(schema.TimeoutCreate),
463-
MinTimeout: 30 * time.Second,
464-
Delay: 1 * time.Minute,
481+
Timeout: d.Timeout(schema.TimeoutUpdate),
482+
MinTimeout: minTimeout,
483+
Delay: minTimeout,
465484
}
466485

467486
_, err = stateConf.WaitForStateContext(ctx)
@@ -483,15 +502,13 @@ func resourceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.
483502
return diag.FromErr(fmt.Errorf(errorPeersDelete, peerID, err))
484503
}
485504

486-
log.Println("[INFO] Waiting for MongoDB Network Peering Connection to be destroyed")
487-
488505
stateConf := &retry.StateChangeConf{
489506
Pending: []string{"AVAILABLE", "INITIATING", "PENDING_ACCEPTANCE", "FINALIZING", "ADDING_PEER", "WAITING_FOR_USER", "TERMINATING", "DELETING"},
490507
Target: []string{"DELETED"},
491508
Refresh: resourceRefreshFunc(ctx, peerID, projectID, "", conn.NetworkPeeringApi),
492-
Timeout: 1 * time.Hour,
493-
MinTimeout: 30 * time.Second,
494-
Delay: 10 * time.Second, // Wait 10 secs before starting
509+
Timeout: d.Timeout(schema.TimeoutDelete),
510+
MinTimeout: minTimeout,
511+
Delay: minTimeout,
495512
}
496513

497514
_, err = stateConf.WaitForStateContext(ctx)
@@ -516,19 +533,19 @@ func resourceImportState(ctx context.Context, d *schema.ResourceData, meta any)
516533

517534
peer, _, err := conn.NetworkPeeringApi.GetPeeringConnection(ctx, projectID, peerID).Execute()
518535
if err != nil {
519-
return nil, fmt.Errorf("couldn't import peer %s in project %s, error: %s", peerID, projectID, err)
536+
return nil, fmt.Errorf("couldn't import peer %s in project %s, error: %w", peerID, projectID, err)
520537
}
521538

522539
if err := d.Set("project_id", projectID); err != nil {
523-
log.Printf("[WARN] Error setting project_id for (%s): %s", peerID, err)
540+
return nil, fmt.Errorf("error setting project_id while importing peer %s in project %s, error: %w", peerID, projectID, err)
524541
}
525542

526543
if err := d.Set("container_id", peer.GetContainerId()); err != nil {
527-
log.Printf("[WARN] Error setting container_id for (%s): %s", peerID, err)
544+
return nil, fmt.Errorf("error setting container_id while importing peer %s in project %s, error: %w", peerID, projectID, err)
528545
}
529546

530547
if err := d.Set("provider_name", providerName); err != nil {
531-
log.Printf("[WARN] Error setting provider_name for (%s): %s", peerID, err)
548+
return nil, fmt.Errorf("error setting provider_name while importing peer %s in project %s, error: %w", peerID, projectID, err)
532549
}
533550

534551
d.SetId(conversion.EncodeStateID(map[string]string{
@@ -547,9 +564,6 @@ func resourceRefreshFunc(ctx context.Context, peerID, projectID, containerID str
547564
if validate.StatusNotFound(resp) {
548565
return "", "DELETED", nil
549566
}
550-
551-
log.Printf("error reading MongoDB Network Peering Connection %s: %s", peerID, err)
552-
553567
return nil, "", err
554568
}
555569

@@ -559,8 +573,6 @@ func resourceRefreshFunc(ctx context.Context, peerID, projectID, containerID str
559573
status = c.GetStatusName()
560574
}
561575

562-
log.Printf("[DEBUG] status for MongoDB Network Peering Connection: %s: %s", peerID, status)
563-
564576
/* We need to get the provisioned status from Mongo container that contains the peering connection
565577
* to validate if it has changed to true. This means that the reciprocal connection in Mongo side
566578
* is right, and the Mongo parameters used on the Google side to configure the reciprocal connection

internal/service/networkpeering/resource_network_peering_test.go

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package networkpeering_test
33
import (
44
"context"
55
"fmt"
6-
"log"
76
"os"
87
"regexp"
98
"strings"
@@ -191,13 +190,37 @@ func TestAccNetworkRSNetworkPeering_AWSDifferentRegionName(t *testing.T) {
191190
CheckDestroy: acc.CheckDestroyNetworkPeering,
192191
Steps: []resource.TestStep{
193192
{
194-
Config: configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, containerRegion, peerRegion),
193+
Config: configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, containerRegion, peerRegion, false),
195194
Check: resource.ComposeAggregateTestCheckFunc(checks...),
196195
},
197196
},
198197
})
199198
}
200199

200+
func TestAccNetworkNetworkPeering_timeouts(t *testing.T) {
201+
var (
202+
orgID = os.Getenv("MONGODB_ATLAS_ORG_ID")
203+
vpcID = os.Getenv("AWS_VPC_ID")
204+
vpcCIDRBlock = os.Getenv("AWS_VPC_CIDR_BLOCK")
205+
awsAccountID = os.Getenv("AWS_ACCOUNT_ID")
206+
containerRegion = os.Getenv("AWS_REGION")
207+
peerRegion = conversion.MongoDBRegionToAWSRegion(containerRegion)
208+
providerName = "AWS"
209+
projectName = acc.RandomProjectName()
210+
)
211+
resource.ParallelTest(t, resource.TestCase{
212+
PreCheck: func() { acc.PreCheckPeeringEnvAWS(t) },
213+
ProtoV6ProviderFactories: acc.TestAccProviderV6Factories,
214+
CheckDestroy: acc.CheckDestroyNetworkPeering, // resource is deleted when creation times out
215+
Steps: []resource.TestStep{
216+
{
217+
Config: configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, containerRegion, peerRegion, true),
218+
ExpectError: regexp.MustCompile("will run cleanup because delete_on_create_timeout is true"),
219+
},
220+
},
221+
})
222+
}
223+
201224
func basicAWSTestCase(tb testing.TB) *resource.TestCase {
202225
tb.Helper()
203226
var (
@@ -213,12 +236,12 @@ func basicAWSTestCase(tb testing.TB) *resource.TestCase {
213236
checks := commonChecksAWS(vpcID, providerName, awsAccountID, vpcCIDRBlock, peerRegion)
214237

215238
return &resource.TestCase{
216-
PreCheck: func() { acc.PreCheckBasic(tb); acc.PreCheckPeeringEnvAWS(tb) },
239+
PreCheck: func() { acc.PreCheckPeeringEnvAWS(tb) },
217240
ProtoV6ProviderFactories: acc.TestAccProviderV6Factories,
218241
CheckDestroy: acc.CheckDestroyNetworkPeering,
219242
Steps: []resource.TestStep{
220243
{
221-
Config: configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, containerRegion, peerRegion),
244+
Config: configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, containerRegion, peerRegion, false),
222245
Check: resource.ComposeAggregateTestCheckFunc(checks...),
223246
},
224247
{
@@ -272,15 +295,25 @@ func checkExists(resourceName string) resource.TestCheckFunc {
272295
return fmt.Errorf("no ID is set")
273296
}
274297
ids := conversion.DecodeStateID(rs.Primary.ID)
275-
log.Printf("[DEBUG] projectID: %s", ids["project_id"])
276298
if _, _, err := acc.ConnV2().NetworkPeeringApi.GetPeeringConnection(context.Background(), ids["project_id"], ids["peer_id"]).Execute(); err == nil {
277299
return nil
278300
}
279301
return fmt.Errorf("peer(%s:%s) does not exist", rs.Primary.Attributes["project_id"], rs.Primary.Attributes["peer_id"])
280302
}
281303
}
282304

283-
func configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, awsRegionContainer, awsRegionPeer string) string {
305+
func configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, awsRegionContainer, awsRegionPeer string, forceTimeout bool) string {
306+
var extraConfig string
307+
if forceTimeout {
308+
extraConfig = `
309+
delete_on_create_timeout = true # default value
310+
timeouts {
311+
create = "10s"
312+
update = "10s"
313+
delete = "10s"
314+
}
315+
`
316+
}
284317
return fmt.Sprintf(`
285318
resource "mongodbatlas_project" "my_project" {
286319
name = %[2]q
@@ -301,6 +334,7 @@ func configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlo
301334
route_table_cidr_block = %[6]q
302335
vpc_id = %[4]q
303336
aws_account_id = %[5]q
337+
%[9]s
304338
}
305339
306340
data "mongodbatlas_network_peering" "test" {
@@ -311,7 +345,7 @@ func configAWS(orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlo
311345
data "mongodbatlas_network_peerings" "test" {
312346
project_id = mongodbatlas_network_peering.test.project_id
313347
}
314-
`, orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, awsRegionContainer, awsRegionPeer)
348+
`, orgID, projectName, providerName, vpcID, awsAccountID, vpcCIDRBlock, awsRegionContainer, awsRegionPeer, extraConfig)
315349
}
316350

317351
func configAzure(projectID, providerName, directoryID, subscriptionID, resourceGroupName, vNetName string) string {

0 commit comments

Comments
 (0)