diff --git a/.changelog/40333.txt b/.changelog/40333.txt new file mode 100644 index 000000000000..966bc667a9ac --- /dev/null +++ b/.changelog/40333.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_rds_cluster: Fix `InvalidDBClusterStateFault` errors when deleting clusters that are members of a global cluster +``` \ No newline at end of file diff --git a/internal/service/docdb/sweep.go b/internal/service/docdb/sweep.go index 5499dc68c492..c0ec260e1607 100644 --- a/internal/service/docdb/sweep.go +++ b/internal/service/docdb/sweep.go @@ -221,6 +221,10 @@ func sweepClusterInstances(region string) error { } for _, v := range page.DBInstances { + if engine := aws.ToString(v.Engine); engine != engineDocDB { + continue + } + r := resourceClusterInstance() d := r.Data(nil) d.SetId(aws.ToString(v.DBInstanceIdentifier)) diff --git a/internal/service/neptune/sweep.go b/internal/service/neptune/sweep.go index 41f2cfbc43e9..4efb701c6df1 100644 --- a/internal/service/neptune/sweep.go +++ b/internal/service/neptune/sweep.go @@ -290,6 +290,10 @@ func sweepClusterInstances(region string) error { } for _, v := range page.DBInstances { + if aws.ToString(v.Engine) != engineNeptune { + continue + } + id := aws.ToString(v.DBInstanceIdentifier) if state := aws.ToString(v.DBInstanceStatus); state == dbInstanceStatusDeleting { diff --git a/internal/service/rds/cluster.go b/internal/service/rds/cluster.go index 050293fdc366..ce0a34823d5c 100644 --- a/internal/service/rds/cluster.go +++ b/internal/service/rds/cluster.go @@ -1644,6 +1644,11 @@ func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta int if err != nil && !errs.IsA[*types.GlobalClusterNotFoundFault](err) && !tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "is not found in global cluster") { return sdkdiag.AppendErrorf(diags, "removing RDS Cluster (%s) from RDS Global Cluster: %s", d.Id(), err) } + + // Removal from a global cluster puts the cluster into 'promoting' state. Wait for it to become available again. + if _, err := waitDBClusterAvailable(ctx, conn, d.Id(), true, d.Timeout(schema.TimeoutUpdate)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for RDS Cluster (%s) available: %s", d.Id(), err) + } } if d.HasChange("iam_roles") { @@ -1666,14 +1671,14 @@ func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta int return append(diags, resourceClusterRead(ctx, d, meta)...) } -func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { +func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics conn := meta.(*conns.AWSClient).RDSClient(ctx) // Automatically remove from global cluster to bypass this error on deletion: // InvalidDBClusterStateFault: This cluster is a part of a global cluster, please remove it from globalcluster first - if d.Get("global_cluster_identifier").(string) != "" { + if globalClusterID := d.Get("global_cluster_identifier").(string); globalClusterID != "" { clusterARN := d.Get(names.AttrARN).(string) - globalClusterID := d.Get("global_cluster_identifier").(string) input := &rds.RemoveFromGlobalClusterInput{ DbClusterIdentifier: aws.String(clusterARN), GlobalClusterIdentifier: aws.String(globalClusterID), @@ -1684,6 +1689,10 @@ func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta int if err != nil && !errs.IsA[*types.GlobalClusterNotFoundFault](err) && !tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "is not found in global cluster") { return sdkdiag.AppendErrorf(diags, "removing RDS Cluster (%s) from RDS Global Cluster (%s): %s", d.Id(), globalClusterID, err) } + + if _, err := waitDBClusterAvailable(ctx, conn, d.Id(), true, d.Timeout(schema.TimeoutCreate)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for RDS Cluster (%s) available: %s", d.Id(), err) + } } skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) @@ -1939,14 +1948,15 @@ func statusDBCluster(ctx context.Context, conn *rds.Client, id string, waitNoPen func waitDBClusterAvailable(ctx context.Context, conn *rds.Client, id string, waitNoPendingModifiedValues bool, timeout time.Duration) (*types.DBCluster, error) { //nolint:unparam pendingStatuses := []string{ - clusterStatusCreating, - clusterStatusMigrating, - clusterStatusPreparingDataMigration, - clusterStatusRebooting, clusterStatusBackingUp, clusterStatusConfiguringIAMDatabaseAuth, clusterStatusConfiguringEnhancedMonitoring, + clusterStatusCreating, + clusterStatusMigrating, clusterStatusModifying, + clusterStatusPreparingDataMigration, + clusterStatusPromoting, + clusterStatusRebooting, clusterStatusRenaming, clusterStatusResettingMasterCredentials, clusterStatusScalingCompute, diff --git a/internal/service/rds/cluster_test.go b/internal/service/rds/cluster_test.go index 9804d6cd76f2..3b2e96eea5b7 100644 --- a/internal/service/rds/cluster_test.go +++ b/internal/service/rds/cluster_test.go @@ -1017,6 +1017,28 @@ func TestAccRDSCluster_takeFinalSnapshot(t *testing.T) { }) } +func TestAccRDSCluster_GlobalClusterIdentifierTakeFinalSnapshot(t *testing.T) { + ctx := acctest.Context(t) + var v types.DBCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_rds_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.RDSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterDestroyWithFinalSnapshot(ctx), + Steps: []resource.TestStep{ + { + Config: testAccClusterConfig_GlobalClusterID_finalSnapshot(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &v), + ), + }, + }, + }) +} + // This is a regression test to make sure that we always cover the scenario as highlighted in // https://github.com/hashicorp/terraform/issues/11568 // Expected error updated to match API response @@ -3705,6 +3727,28 @@ resource "aws_rds_cluster" "test" { `, rName, tfrds.ClusterEngineAuroraMySQL) } +func testAccClusterConfig_GlobalClusterID_finalSnapshot(rName string) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + global_cluster_identifier = %[1]q + engine = "aurora-postgresql" + engine_version = "11.9" +} + +resource "aws_rds_cluster" "test" { + cluster_identifier = %[1]q + database_name = "test" + engine = aws_rds_global_cluster.test.engine + engine_version = aws_rds_global_cluster.test.engine_version + master_username = "tfacctest" + master_password = "avoid-plaintext-passwords" + final_snapshot_identifier = %[1]q + skip_final_snapshot = false + global_cluster_identifier = aws_rds_global_cluster.test.id +} +`, rName) +} + func testAccClusterConfig_withoutUserNameAndPassword(n int) string { return fmt.Sprintf(` resource "aws_rds_cluster" "default" { diff --git a/internal/service/rds/global_cluster.go b/internal/service/rds/global_cluster.go index 5777288899ea..a808d6df6d56 100644 --- a/internal/service/rds/global_cluster.go +++ b/internal/service/rds/global_cluster.go @@ -350,12 +350,7 @@ func resourceGlobalClusterDelete(ctx context.Context, d *schema.ResourceData, me // This operation will be quick if successful globalClusterClusterDeleteTimeout = 5 * time.Minute ) - var timeout time.Duration - if x, y := deadline.Remaining(), globalClusterClusterDeleteTimeout; x < y { - timeout = x - } else { - timeout = y - } + timeout := max(deadline.Remaining(), globalClusterClusterDeleteTimeout) _, err := tfresource.RetryWhenIsAErrorMessageContains[*types.InvalidGlobalClusterStateFault](ctx, timeout, func() (interface{}, error) { return conn.DeleteGlobalCluster(ctx, &rds.DeleteGlobalClusterInput{ GlobalClusterIdentifier: aws.String(d.Id()), diff --git a/internal/service/rds/sweep.go b/internal/service/rds/sweep.go index 2b2ff4ffac84..28d6deecbd30 100644 --- a/internal/service/rds/sweep.go +++ b/internal/service/rds/sweep.go @@ -241,11 +241,10 @@ func sweepClusters(region string) error { if engineMode := aws.ToString(v.EngineMode); engineMode == engineModeGlobal || engineMode == engineModeProvisioned { globalCluster, err := findGlobalClusterByDBClusterARN(ctx, conn, arn) - if err != nil { - if !tfresource.NotFound(err) { - log.Printf("[WARN] Reading RDS Global Cluster information for DB Cluster (%s): %s", id, err) - continue - } + + if err != nil && !tfresource.NotFound(err) { + log.Printf("[WARN] Reading RDS Global Cluster information for DB Cluster (%s): %s", id, err) + continue } if globalCluster != nil && globalCluster.GlobalClusterIdentifier != nil { @@ -374,9 +373,23 @@ func sweepInstances(region string) error { } for _, v := range page.DBInstances { + id := aws.ToString(v.DbiResourceId) + + switch engine := aws.ToString(v.Engine); engine { + case "docdb", "neptune": + // These engines are handled by their respective services' sweepers. + continue + default: + // "InvalidParameterValue: Deleting cluster instances isn't supported for DB engine mysql". + if v.DBClusterIdentifier != nil { + log.Printf("[INFO] Skipping RDS DB Instance %s", id) + continue + } + } + r := resourceInstance() d := r.Data(nil) - d.SetId(aws.ToString(v.DbiResourceId)) + d.SetId(id) d.Set(names.AttrApplyImmediately, true) d.Set("delete_automated_backups", true) d.Set(names.AttrDeletionProtection, false)