diff --git a/.changelog/34309.txt b/.changelog/34309.txt new file mode 100644 index 00000000000..1888ee95983 --- /dev/null +++ b/.changelog/34309.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_rds_cluster: Add `delete_automated_backups` argument +``` \ No newline at end of file diff --git a/internal/service/rds/cluster.go b/internal/service/rds/cluster.go index 51c5db656a4..9f2e8f4cea8 100644 --- a/internal/service/rds/cluster.go +++ b/internal/service/rds/cluster.go @@ -56,6 +56,15 @@ func ResourceCluster() *schema.Resource { Delete: schema.DefaultTimeout(120 * time.Minute), }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceClusterResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: clusterStateUpgradeV0, + Version: 0, + }, + }, + Schema: map[string]*schema.Schema{ "allocated_storage": { Type: schema.TypeInt, @@ -157,6 +166,11 @@ func ResourceCluster() *schema.Resource { ForceNew: true, Computed: true, }, + "delete_automated_backups": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, "deletion_protection": { Type: schema.TypeBool, Optional: true, @@ -1219,6 +1233,7 @@ func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta int if d.HasChangesExcept( "allow_major_version_upgrade", + "delete_automated_backups", "final_snapshot_identifier", "global_cluster_identifier", "iam_roles", @@ -1470,8 +1485,9 @@ func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta int skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) input := &rds.DeleteDBClusterInput{ - DBClusterIdentifier: aws.String(d.Id()), - SkipFinalSnapshot: aws.Bool(skipFinalSnapshot), + DBClusterIdentifier: aws.String(d.Id()), + DeleteAutomatedBackups: aws.Bool(d.Get("delete_automated_backups").(bool)), + SkipFinalSnapshot: aws.Bool(skipFinalSnapshot), } if !skipFinalSnapshot { @@ -1554,6 +1570,7 @@ func resourceClusterImport(_ context.Context, d *schema.ResourceData, meta inter // from any API call, so we need to default skip_final_snapshot to true so // that final_snapshot_identifier is not required d.Set("skip_final_snapshot", true) + d.Set("delete_automated_backups", true) return []*schema.ResourceData{d}, nil } diff --git a/internal/service/rds/cluster_migrate.go b/internal/service/rds/cluster_migrate.go new file mode 100644 index 00000000000..24e9f58b474 --- /dev/null +++ b/internal/service/rds/cluster_migrate.go @@ -0,0 +1,418 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package rds + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// Resource schema as at v5.25.0 without validators etc. +func resourceClusterResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allocated_storage": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "allow_major_version_upgrade": { + Type: schema.TypeBool, + Optional: true, + }, + // apply_immediately is used to determine when the update modifications take place. + // See http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html + "apply_immediately": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "availability_zones": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "backup_retention_period": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "backtrack_window": { + Type: schema.TypeInt, + Optional: true, + }, + "cluster_identifier": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "cluster_identifier_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "cluster_members": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "cluster_resource_id": { + Type: schema.TypeString, + Computed: true, + }, + "copy_tags_to_snapshot": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "database_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "db_cluster_instance_class": { + Type: schema.TypeString, + Optional: true, + }, + "db_cluster_parameter_group_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "db_instance_parameter_group_name": { + Type: schema.TypeString, + Optional: true, + }, + "db_subnet_group_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "db_system_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "deletion_protection": { + Type: schema.TypeBool, + Optional: true, + }, + "enable_global_write_forwarding": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "enabled_cloudwatch_logs_exports": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "enable_http_endpoint": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "endpoint": { + Type: schema.TypeString, + Computed: true, + }, + "engine": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "engine_mode": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: EngineModeProvisioned, + }, + "engine_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "engine_version_actual": { + Type: schema.TypeString, + Computed: true, + }, + "final_snapshot_identifier": { + Type: schema.TypeString, + Optional: true, + }, + "global_cluster_identifier": { + Type: schema.TypeString, + Optional: true, + }, + "hosted_zone_id": { + Type: schema.TypeString, + Computed: true, + }, + "iam_database_authentication_enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "iam_roles": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "iops": { + Type: schema.TypeInt, + Optional: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "manage_master_user_password": { + Type: schema.TypeBool, + Optional: true, + }, + "master_user_secret": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "kms_key_id": { + Type: schema.TypeString, + Computed: true, + }, + "secret_arn": { + Type: schema.TypeString, + Computed: true, + }, + "secret_status": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "master_user_secret_kms_key_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "master_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + }, + "master_username": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "network_type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "port": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "preferred_backup_window": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "preferred_maintenance_window": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "reader_endpoint": { + Type: schema.TypeString, + Computed: true, + }, + "replication_source_identifier": { + Type: schema.TypeString, + Optional: true, + }, + "restore_to_point_in_time": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "restore_to_time": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "restore_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "source_cluster_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "use_latest_restorable_time": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + "s3_import": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bucket_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "bucket_prefix": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "ingestion_role": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "source_engine": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "source_engine_version": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + }, + "scaling_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "auto_pause": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "max_capacity": { + Type: schema.TypeInt, + Optional: true, + Default: clusterScalingConfiguration_DefaultMaxCapacity, + }, + "min_capacity": { + Type: schema.TypeInt, + Optional: true, + Default: clusterScalingConfiguration_DefaultMinCapacity, + }, + "seconds_until_auto_pause": { + Type: schema.TypeInt, + Optional: true, + Default: 300, + }, + "timeout_action": { + Type: schema.TypeString, + Optional: true, + Default: TimeoutActionRollbackCapacityChange, + }, + }, + }, + }, + "serverlessv2_scaling_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_capacity": { + Type: schema.TypeFloat, + Required: true, + }, + "min_capacity": { + Type: schema.TypeFloat, + Required: true, + }, + }, + }, + }, + "skip_final_snapshot": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "snapshot_identifier": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "source_region": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "storage_encrypted": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + ForceNew: true, + }, + "storage_type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + "vpc_security_group_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + } +} + +func clusterStateUpgradeV0(_ context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + if rawState == nil { + return nil, nil + } + + rawState["delete_automated_backups"] = true + + return rawState, nil +} diff --git a/internal/service/rds/cluster_test.go b/internal/service/rds/cluster_test.go index f712ce1979c..ab6c427f671 100644 --- a/internal/service/rds/cluster_test.go +++ b/internal/service/rds/cluster_test.go @@ -9,6 +9,7 @@ import ( "fmt" "strings" "testing" + "time" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go/aws" @@ -79,6 +80,7 @@ func TestAccRDSCluster_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "copy_tags_to_snapshot", "false"), resource.TestCheckResourceAttrSet(resourceName, "db_cluster_parameter_group_name"), resource.TestCheckResourceAttr(resourceName, "db_system_id", ""), + resource.TestCheckResourceAttr(resourceName, "delete_automated_backups", "true"), resource.TestCheckResourceAttr(resourceName, "enabled_cloudwatch_logs_exports.#", "0"), resource.TestCheckResourceAttr(resourceName, "engine", "aurora-mysql"), resource.TestCheckResourceAttrSet(resourceName, "engine_version"), @@ -2461,12 +2463,105 @@ func TestAccRDSCluster_password(t *testing.T) { }) } +func TestAccRDSCluster_NoDeleteAutomatedBackups(t *testing.T) { + ctx := acctest.Context(t) + var dbCluster rds.DBCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_rds_cluster.test" + + backupWindowLength := 30 * time.Minute + clusterProvisionTime := 5 * time.Minute + + // Unlike rds instance, there is no automated backup created when the cluster is deployed. + // However, we can forcefully create one by moving maintenance window behind the deployment. + now := time.Now().UTC() + backupWindowStart := now.Add(clusterProvisionTime) + backupWindowEnd := backupWindowStart.Add(backupWindowLength) + preferredBackupWindow := fmt.Sprintf( + "%02d:%02d-%02d:%02d", + backupWindowStart.Hour(), + backupWindowStart.Minute(), + backupWindowEnd.Hour(), + backupWindowEnd.Minute(), + ) + + waitUntilAutomatedBackupCreated := func(*terraform.State) error { + ticker := time.NewTicker(1 * time.Minute) + for { + select { + case <-time.After(backupWindowLength): + return nil + case <-ticker.C: + conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) + output, _ := conn.DescribeDBClusterSnapshotsWithContext(ctx, &rds.DescribeDBClusterSnapshotsInput{ + DBClusterIdentifier: aws.String(rName), + SnapshotType: aws.String("automated"), + }) + if output != nil && len(output.DBClusterSnapshots) > 0 { + snapshot := output.DBClusterSnapshots[0] + if aws.StringValue(snapshot.Status) == "available" { + return nil + } + } + } + } + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterAutomatedBackupsDelete(ctx), + Steps: []resource.TestStep{ + { + Config: testAccClusterConfig_noDeleteAutomatedBackups(rName, preferredBackupWindow), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &dbCluster), + waitUntilAutomatedBackupCreated, + ), + }, + }, + }) +} + func testAccCheckClusterDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { return testAccCheckClusterDestroyWithProvider(ctx)(s, acctest.Provider) } } +func testAccCheckClusterAutomatedBackupsDelete(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_rds_cluster" { + continue + } + describeOutput, err := conn.DescribeDBClusterSnapshotsWithContext(ctx, &rds.DescribeDBClusterSnapshotsInput{ + DBClusterIdentifier: aws.String(rs.Primary.Attributes["cluster_identifier"]), + SnapshotType: aws.String("automated"), + }) + if err != nil { + return err + } + + if describeOutput == nil || len(describeOutput.DBClusterSnapshots) == 0 { + return fmt.Errorf("Automated backup for %s not found", rs.Primary.Attributes["cluster_identifier"]) + } + + _, err = conn.DeleteDBClusterAutomatedBackupWithContext(ctx, &rds.DeleteDBClusterAutomatedBackupInput{ + DbClusterResourceId: aws.String(rs.Primary.Attributes["cluster_resource_id"]), + }) + if err != nil { + return err + } + } + + return testAccCheckClusterDestroyWithProvider(ctx)(s, acctest.Provider) + } +} + func testAccCheckClusterDestroyWithProvider(ctx context.Context) acctest.TestCheckWithProviderFunc { return func(s *terraform.State, provider *schema.Provider) error { conn := provider.Meta().(*conns.AWSClient).RDSConn(ctx) @@ -4702,3 +4797,18 @@ resource "aws_db_subnet_group" "test" { `, rName), ) } + +func testAccClusterConfig_noDeleteAutomatedBackups(rName, preferredBackupWindow string) string { + return fmt.Sprintf(` +resource "aws_rds_cluster" "test" { + cluster_identifier = %[1]q + database_name = "test" + engine = "aurora-mysql" + master_username = "tfacctest" + master_password = "avoid-plaintext-passwords" + preferred_backup_window = %[2]q + skip_final_snapshot = true + delete_automated_backups = false +} +`, rName, preferredBackupWindow) +} diff --git a/internal/service/rds/sweep.go b/internal/service/rds/sweep.go index 0e8c28ea297..22dfe68160f 100644 --- a/internal/service/rds/sweep.go +++ b/internal/service/rds/sweep.go @@ -233,6 +233,7 @@ func sweepClusters(region string) error { d.SetId(id) d.Set("apply_immediately", true) d.Set("arn", arn) + d.Set("delete_automated_backups", true) d.Set("deletion_protection", false) d.Set("skip_final_snapshot", true) diff --git a/website/docs/cdktf/python/r/rds_cluster.html.markdown b/website/docs/cdktf/python/r/rds_cluster.html.markdown index ce61f049ddb..e235b7192aa 100644 --- a/website/docs/cdktf/python/r/rds_cluster.html.markdown +++ b/website/docs/cdktf/python/r/rds_cluster.html.markdown @@ -345,6 +345,7 @@ This argument supports the following arguments: * `db_subnet_group_name` - (Optional) DB subnet group to associate with this DB cluster. **NOTE:** This must match the `db_subnet_group_name` specified on every [`aws_rds_cluster_instance`](/docs/providers/aws/r/rds_cluster_instance.html) in the cluster. * `db_system_id` - (Optional) For use with RDS Custom. +* `delete_automated_backups` - (Optional) Specifies whether to remove automated backups immediately after the DB cluster is deleted. Default is `true`. * `deletion_protection` - (Optional) If the DB cluster should have deletion protection enabled. The database can't be deleted when this value is set to `true`. The default is `false`. diff --git a/website/docs/cdktf/typescript/r/rds_cluster.html.markdown b/website/docs/cdktf/typescript/r/rds_cluster.html.markdown index b372c853a40..63e77b5d5e4 100644 --- a/website/docs/cdktf/typescript/r/rds_cluster.html.markdown +++ b/website/docs/cdktf/typescript/r/rds_cluster.html.markdown @@ -387,6 +387,7 @@ This argument supports the following arguments: * `dbSubnetGroupName` - (Optional) DB subnet group to associate with this DB cluster. **NOTE:** This must match the `db_subnet_group_name` specified on every [`aws_rds_cluster_instance`](/docs/providers/aws/r/rds_cluster_instance.html) in the cluster. * `dbSystemId` - (Optional) For use with RDS Custom. +* `deleteAutomatedBackups` - (Optional) Specifies whether to remove automated backups immediately after the DB cluster is deleted. Default is `true`. * `deletionProtection` - (Optional) If the DB cluster should have deletion protection enabled. The database can't be deleted when this value is set to `true`. The default is `false`. diff --git a/website/docs/r/rds_cluster.html.markdown b/website/docs/r/rds_cluster.html.markdown index 668db491bf7..635aedea00c 100644 --- a/website/docs/r/rds_cluster.html.markdown +++ b/website/docs/r/rds_cluster.html.markdown @@ -239,6 +239,7 @@ This argument supports the following arguments: * `db_subnet_group_name` - (Optional) DB subnet group to associate with this DB cluster. **NOTE:** This must match the `db_subnet_group_name` specified on every [`aws_rds_cluster_instance`](/docs/providers/aws/r/rds_cluster_instance.html) in the cluster. * `db_system_id` - (Optional) For use with RDS Custom. +* `delete_automated_backups` - (Optional) Specifies whether to remove automated backups immediately after the DB cluster is deleted. Default is `true`. * `deletion_protection` - (Optional) If the DB cluster should have deletion protection enabled. The database can't be deleted when this value is set to `true`. The default is `false`.