diff --git a/.changelog/40409.txt b/.changelog/40409.txt new file mode 100644 index 00000000000..4a202a0418b --- /dev/null +++ b/.changelog/40409.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_rds_cluster_instance: Fix error when destroying from a read replica cluster +``` \ No newline at end of file diff --git a/internal/service/rds/cluster_instance.go b/internal/service/rds/cluster_instance.go index a14be2aac36..abaa8432482 100644 --- a/internal/service/rds/cluster_instance.go +++ b/internal/service/rds/cluster_instance.go @@ -129,6 +129,11 @@ func resourceClusterInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, + names.AttrForceDestroy: { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, names.AttrIdentifier: { Type: schema.TypeString, Optional: true, @@ -543,6 +548,21 @@ func resourceClusterInstanceDelete(ctx context.Context, d *schema.ResourceData, }, "Delete the replica cluster before deleting") + if errs.IsAErrorMessageContains[*types.InvalidDBClusterStateFault](err, "Cannot delete the last instance of the read replica DB cluster") && d.Get(names.AttrForceDestroy).(bool) { + _, err = conn.PromoteReadReplicaDBCluster(ctx, &rds.PromoteReadReplicaDBClusterInput{ + DBClusterIdentifier: aws.String(d.Get(names.AttrClusterIdentifier).(string)), + }) + if err != nil { + return sdkdiag.AppendErrorf(diags, "promoting read replica to primary for RDS Cluster (%s): %s", d.Id(), err) + } + + if _, err := waitDBClusterAvailable(ctx, conn, d.Id(), false, d.Timeout(schema.TimeoutDelete)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for RDS Cluster (%s) update: %s", d.Id(), err) + } + + _, err = conn.DeleteDBInstance(ctx, input) + } + if errs.IsA[*types.DBInstanceNotFoundFault](err) { return diags } diff --git a/internal/service/rds/cluster_instance_test.go b/internal/service/rds/cluster_instance_test.go index db26d310215..34b91832dce 100644 --- a/internal/service/rds/cluster_instance_test.go +++ b/internal/service/rds/cluster_instance_test.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/hashicorp/aws-sdk-go-base/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -65,6 +66,7 @@ func TestAccRDSClusterInstance_basic(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -136,6 +138,7 @@ func TestAccRDSClusterInstance_identifierGenerated(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -172,6 +175,7 @@ func TestAccRDSClusterInstance_identifierPrefix(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -208,6 +212,7 @@ func TestAccRDSClusterInstance_tags(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -303,6 +308,7 @@ func TestAccRDSClusterInstance_az(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -339,6 +345,7 @@ func TestAccRDSClusterInstance_kmsKey(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -374,6 +381,7 @@ func TestAccRDSClusterInstance_publiclyAccessible(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -389,6 +397,7 @@ func TestAccRDSClusterInstance_publiclyAccessible(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -431,6 +440,7 @@ func TestAccRDSClusterInstance_copyTagsToSnapshot(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -474,6 +484,7 @@ func TestAccRDSClusterInstance_caCertificateIdentifier(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -509,6 +520,7 @@ func TestAccRDSClusterInstance_monitoringInterval(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -566,6 +578,7 @@ func TestAccRDSClusterInstance_MonitoringRoleARN_enabledToDisabled(t *testing.T) ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -609,6 +622,7 @@ func TestAccRDSClusterInstance_MonitoringRoleARN_enabledToRemoved(t *testing.T) ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -650,6 +664,7 @@ func TestAccRDSClusterInstance_MonitoringRoleARN_removedToEnabled(t *testing.T) ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -693,6 +708,7 @@ func TestAccRDSClusterInstance_PerformanceInsightsEnabled_auroraMySQL1(t *testin ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -729,6 +745,7 @@ func TestAccRDSClusterInstance_PerformanceInsightsEnabled_auroraPostgresql(t *te ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -767,6 +784,7 @@ func TestAccRDSClusterInstance_PerformanceInsightsKMSKeyID_auroraMySQL1(t *testi ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -803,6 +821,7 @@ func TestAccRDSClusterInstance_PerformanceInsightsKMSKeyIDAuroraMySQL1_defaultKe ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -846,6 +865,7 @@ func TestAccRDSClusterInstance_performanceInsightsRetentionPeriod(t *testing.T) ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -900,6 +920,7 @@ func TestAccRDSClusterInstance_PerformanceInsightsKMSKeyID_auroraPostgresql(t *t ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, }, @@ -936,6 +957,7 @@ func TestAccRDSClusterInstance_PerformanceInsightsKMSKeyIDAuroraPostgresql_defau ImportStateVerify: true, ImportStateVerifyIgnore: []string{ names.AttrApplyImmediately, + names.AttrForceDestroy, }, }, { @@ -946,6 +968,88 @@ func TestAccRDSClusterInstance_PerformanceInsightsKMSKeyIDAuroraPostgresql_defau }) } +func TestAccRDSClusterInstance_Replica_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var primaryInstance types.DBInstance + var replicaInstance types.DBInstance + resourceName := "aws_rds_cluster_instance.test" + resourceName2 := "aws_rds_cluster_instance.alternate" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + // record the initialized providers so that we can use them to + // check for the cluster in each region + var providers []*schema.Provider + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckMultipleRegion(t, 2) + }, + ErrorCheck: acctest.ErrorCheck(t, names.RDSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesPlusProvidersAlternate(ctx, t, &providers), + CheckDestroy: acctest.CheckWithProviders(testAccCheckClusterInstanceDestroyWithProvider(ctx), &providers), + Steps: []resource.TestStep{ + { + Config: testAccClusterInstanceConfig_replicationSource_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterInstanceExistsWithProvider(ctx, resourceName, &primaryInstance, acctest.RegionProviderFunc(ctx, acctest.Region(), &providers)), + testAccCheckClusterInstanceExistsWithProvider(ctx, resourceName2, &replicaInstance, acctest.RegionProviderFunc(ctx, acctest.AlternateRegion(), &providers)), + ), + }, + }, + }) +} + +func testAccCheckClusterInstanceExistsWithProvider(ctx context.Context, n string, v *types.DBInstance, providerF func() *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := providerF().Meta().(*conns.AWSClient).RDSClient(ctx) + + output, err := tfrds.FindDBInstanceByID(ctx, conn, rs.Primary.ID) + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckClusterInstanceDestroyWithProvider(ctx context.Context) acctest.TestCheckWithProviderFunc { + return func(s *terraform.State, provider *schema.Provider) error { + conn := provider.Meta().(*conns.AWSClient).RDSClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_rds_cluster_instance" { + continue + } + + _, err := tfrds.FindDBInstanceByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("RDS Cluster Instance %s still exists", rs.Primary.ID) + } + + return nil + } +} + func testAccPerformanceInsightsDefaultVersionPreCheck(ctx context.Context, t *testing.T, engine string) { conn := acctest.Provider.Meta().(*conns.AWSClient).RDSClient(ctx) @@ -1545,6 +1649,41 @@ resource "aws_rds_cluster_instance" "test" { `, rName)) } +func testAccClusterInstanceConfig_replicationSource_basic(rName string) string { + return acctest.ConfigCompose( + testAccClusterConfig_replicationSource_base(rName), + fmt.Sprintf(` +resource "aws_rds_cluster" "alternate" { + provider = "awsalternate" + + cluster_identifier = "%[1]s-replica" + db_subnet_group_name = aws_db_subnet_group.test.name + engine = %[2]q + kms_key_id = aws_kms_key.test.arn + storage_encrypted = true + skip_final_snapshot = true + replication_source_identifier = aws_rds_cluster.test.arn + source_region = data.aws_region.current.name + + depends_on = [ + aws_rds_cluster_instance.test, + ] +} + +resource "aws_rds_cluster_instance" "alternate" { + provider = "awsalternate" + + identifier = "%[1]s-replica" + cluster_identifier = aws_rds_cluster.alternate.id + instance_class = data.aws_rds_orderable_db_instance.test.instance_class + engine = aws_rds_cluster.alternate.engine + engine_version = aws_rds_cluster.alternate.engine_version + + force_destroy = true +} +`, rName, tfrds.ClusterEngineAuroraMySQL)) +} + func testAccClusterInstanceConfig_performanceInsightsEnabledAuroraPostgresql(rName, engine string) string { return acctest.ConfigCompose( testAccClusterInstanceConfig_orderableEngineBase(engine, true), diff --git a/website/docs/r/rds_cluster_instance.html.markdown b/website/docs/r/rds_cluster_instance.html.markdown index 7012c54e459..c1020045d75 100644 --- a/website/docs/r/rds_cluster_instance.html.markdown +++ b/website/docs/r/rds_cluster_instance.html.markdown @@ -65,6 +65,7 @@ This resource supports the following arguments: * `engine_version` - (Optional) Database engine version. Please note that to upgrade the `engine_version` of the instance, it must be done on the `aws_rds_cluster` `engine_version`. Trying to upgrade in `aws_cluster_instance` will not update the `engine_version`. * `engine` - (Required, Forces new resource) Name of the database engine to be used for the RDS cluster instance. Valid Values: `aurora-mysql`, `aurora-postgresql`, `mysql`, `postgres`.(Note that `mysql` and `postgres` are Multi-AZ RDS clusters). +* `force_destroy` - (Optional) Forces an instance to be destroyed when a part of a read replica cluster. **Note:** will promote the read replica to a standalone cluster before instance deletion. * `identifier_prefix` - (Optional, Forces new resource) Creates a unique identifier beginning with the specified prefix. Conflicts with `identifier`. * `identifier` - (Optional, Forces new resource) Identifier for the RDS instance, if omitted, Terraform will assign a random, unique identifier. * `instance_class` - (Required) Instance class to use. For details on CPU and memory, see [Scaling Aurora DB Instances][4]. Aurora uses `db.*` instance classes/types. Please see [AWS Documentation][7] for currently available instance classes and complete details. For Aurora Serverless v2 use `db.serverless`.