From 955b974a210debb0717e7810bbf8615d07927860 Mon Sep 17 00:00:00 2001 From: "marcin.janas" Date: Thu, 5 Aug 2021 15:51:22 +0200 Subject: [PATCH] feat: add aws_db_instance_automated_backups_replication Signed-off-by: marcin.janas --- aws/internal/service/rds/finder/finder.go | 28 ++++ aws/internal/service/rds/waiter/status.go | 19 +++ aws/internal/service/rds/waiter/waiter.go | 46 +++++ aws/provider.go | 1 + ..._instance_automated_backups_replication.go | 150 +++++++++++++++++ ...ance_automated_backups_replication_test.go | 158 ++++++++++++++++++ ...nce_automated_backups_replication.markdown | 46 +++++ 7 files changed, 448 insertions(+) create mode 100644 aws/resource_aws_db_instance_automated_backups_replication.go create mode 100644 aws/resource_aws_db_instance_automated_backups_replication_test.go create mode 100644 website/docs/r/db_instance_automated_backups_replication.markdown diff --git a/aws/internal/service/rds/finder/finder.go b/aws/internal/service/rds/finder/finder.go index a401d39b5b22..434f6fd1116c 100644 --- a/aws/internal/service/rds/finder/finder.go +++ b/aws/internal/service/rds/finder/finder.go @@ -1,6 +1,8 @@ package finder import ( + "context" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/aws-sdk-go-base/tfawserr" @@ -120,3 +122,29 @@ func DBClusterByID(conn *rds.RDS, id string) (*rds.DBCluster, error) { return dbCluster, nil } + +// DBInstanceAutomatedBackup returns matching DBInstanceAutomatedBackup +func DBInstanceAutomatedBackup(ctx context.Context, conn *rds.RDS, dbInstanceAutomatedBackupsArn string) (*rds.DBInstanceAutomatedBackup, error) { + input := &rds.DescribeDBInstanceAutomatedBackupsInput{ + DBInstanceAutomatedBackupsArn: aws.String(dbInstanceAutomatedBackupsArn), + } + + var dbInstanceAutomatedBackup *rds.DBInstanceAutomatedBackup + + err := conn.DescribeDBInstanceAutomatedBackupsPagesWithContext(ctx, input, func(page *rds.DescribeDBInstanceAutomatedBackupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, backup := range page.DBInstanceAutomatedBackups { + if aws.StringValue(backup.DBInstanceAutomatedBackupsArn) == dbInstanceAutomatedBackupsArn { + dbInstanceAutomatedBackup = backup + return false + } + } + + return !lastPage + }) + + return dbInstanceAutomatedBackup, err +} diff --git a/aws/internal/service/rds/waiter/status.go b/aws/internal/service/rds/waiter/status.go index 870f1b58e8e5..9ae1f2f17b1a 100644 --- a/aws/internal/service/rds/waiter/status.go +++ b/aws/internal/service/rds/waiter/status.go @@ -1,6 +1,8 @@ package waiter import ( + "context" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -75,3 +77,20 @@ func DBClusterRoleStatus(conn *rds.RDS, dbClusterID, roleARN string) resource.St return output, aws.StringValue(output.Status), nil } } + +// DBInstanceAutomatedBackupsReplicationStatus fetches the DBInstanceAutomatedBackup and its Status +func DBInstanceAutomatedBackupsReplicationStatus(ctx context.Context, conn *rds.RDS, dbInstanceAutomatedBackupsArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.DBInstanceAutomatedBackup(ctx, conn, dbInstanceAutomatedBackupsArn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} diff --git a/aws/internal/service/rds/waiter/waiter.go b/aws/internal/service/rds/waiter/waiter.go index d3ad5da52d03..db329bdfab7c 100644 --- a/aws/internal/service/rds/waiter/waiter.go +++ b/aws/internal/service/rds/waiter/waiter.go @@ -1,6 +1,7 @@ package waiter import ( + "context" "time" "github.com/aws/aws-sdk-go/service/rds" @@ -15,6 +16,15 @@ const ( DBClusterRoleAssociationCreatedTimeout = 5 * time.Minute DBClusterRoleAssociationDeletedTimeout = 5 * time.Minute + + // DB Instance Automated Backups Replication timeouts + DBInstanceAutomatedBackupsReplicationStartedTimeout = 30 * time.Minute + DBInstanceAutomatedBackupsReplicationDeletedTimeout = 5 * time.Minute + + // DB Instance Automated Backups Replication states + DBInstanceAutomatedBackupsPending = "pending" + DBInstanceAutomatedBackupsReplicating = "replicating" + DBInstanceAutomatedBackupsDeleting = "deleting" ) // EventSubscriptionDeleted waits for a EventSubscription to return Deleted @@ -107,3 +117,39 @@ func DBClusterRoleAssociationDeleted(conn *rds.RDS, dbClusterID, roleARN string) return nil, err } + +// DBInstanceAutomatedBackupsReplicationStarted waits for a DBInstanceAutomatedBackup to return replicating +func DBInstanceAutomatedBackupsReplicationStarted(ctx context.Context, conn *rds.RDS, dbInstanceAutomatedBackupsArn string) (*rds.DBInstanceAutomatedBackup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{DBInstanceAutomatedBackupsPending}, + Target: []string{DBInstanceAutomatedBackupsReplicating}, + Refresh: DBInstanceAutomatedBackupsReplicationStatus(ctx, conn, dbInstanceAutomatedBackupsArn), + Timeout: DBInstanceAutomatedBackupsReplicationStartedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.DBInstanceAutomatedBackup); ok { + return output, err + } + + return nil, err +} + +// DBInstanceAutomatedBackupsReplicationDeleted waits for a DBInstanceAutomatedBackup to return deleting +func DBInstanceAutomatedBackupsReplicationDeleted(ctx context.Context, conn *rds.RDS, dbInstanceAutomatedBackupsArn string) (*rds.DBInstanceAutomatedBackup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{DBInstanceAutomatedBackupsReplicating}, + Target: []string{DBInstanceAutomatedBackupsDeleting}, + Refresh: DBInstanceAutomatedBackupsReplicationStatus(ctx, conn, dbInstanceAutomatedBackupsArn), + Timeout: DBInstanceAutomatedBackupsReplicationDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*rds.DBInstanceAutomatedBackup); ok { + return output, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index 872662fcedc6..feeffb30e2b5 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -639,6 +639,7 @@ func Provider() *schema.Provider { "aws_db_cluster_snapshot": resourceAwsDbClusterSnapshot(), "aws_db_event_subscription": resourceAwsDbEventSubscription(), "aws_db_instance": resourceAwsDbInstance(), + "aws_db_instance_automated_backups_replication": resourceAwsDbInstanceAutomatedBackupsReplication(), "aws_db_instance_role_association": resourceAwsDbInstanceRoleAssociation(), "aws_db_option_group": resourceAwsDbOptionGroup(), "aws_db_parameter_group": resourceAwsDbParameterGroup(), diff --git a/aws/resource_aws_db_instance_automated_backups_replication.go b/aws/resource_aws_db_instance_automated_backups_replication.go new file mode 100644 index 000000000000..4176e46933e5 --- /dev/null +++ b/aws/resource_aws_db_instance_automated_backups_replication.go @@ -0,0 +1,150 @@ +package aws + +import ( + "context" + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/waiter" +) + +func resourceAwsDbInstanceAutomatedBackupsReplication() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAwsDbInstanceAutomatedBackupsReplicationCreate, + ReadContext: resourceAwsDbInstanceAutomatedBackupsReplicationRead, + DeleteContext: resourceAwsDbInstanceAutomatedBackupsReplicationDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "backup_retention_period": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 1, + }, + "kms_key_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "pre_signed_url": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "source_db_instance_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArn, + }, + }, + } +} + +func resourceAwsDbInstanceAutomatedBackupsReplicationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).rdsconn + + input := &rds.StartDBInstanceAutomatedBackupsReplicationInput{ + SourceDBInstanceArn: aws.String(d.Get("source_db_instance_arn").(string)), + } + + if attr, ok := d.GetOk("backup_retention_period"); ok { + input.BackupRetentionPeriod = aws.Int64(int64(attr.(int))) + } + + if attr, ok := d.GetOk("kms_key_id"); ok { + input.KmsKeyId = aws.String(attr.(string)) + } + + if attr, ok := d.GetOk("pre_signed_url"); ok { + input.PreSignedUrl = aws.String(attr.(string)) + } + + log.Printf("[DEBUG] RDS DB Instance Start Automated Backups Replication: (%s)", input) + output, err := conn.StartDBInstanceAutomatedBackupsReplicationWithContext(ctx, input) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to Start Automated Backup Replication: %s", err)) + } + + if output == nil || output.DBInstanceAutomatedBackup == nil { + return diag.FromErr(fmt.Errorf("error starting RDS DB Instance Start Automated Backups Replication: empty output")) + } + + dbInstanceAutomatedBackupsArn := aws.StringValue(output.DBInstanceAutomatedBackup.DBInstanceAutomatedBackupsArn) + d.SetId(dbInstanceAutomatedBackupsArn) + + if _, err := waiter.DBInstanceAutomatedBackupsReplicationStarted(ctx, conn, dbInstanceAutomatedBackupsArn); err != nil { + return diag.FromErr(fmt.Errorf("error waiting to RDS DB Instance Start Automated Backups Replication: %s", err)) + } + + return resourceAwsDbInstanceAutomatedBackupsReplicationRead(ctx, d, meta) +} + +func resourceAwsDbInstanceAutomatedBackupsReplicationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).rdsconn + + dbInstanceAutomatedBackup, err := finder.DBInstanceAutomatedBackup(ctx, conn, d.Id()) + if isAWSErr(err, rds.ErrCodeDBInstanceAutomatedBackupNotFoundFault, "") { + log.Printf("[WARN] RDS DB Instance Automated Backups Replication not found (%s), removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error describe RDS DB Instance Automated Backups Replication: %s", err)) + } + + if err := d.Set("backup_retention_period", dbInstanceAutomatedBackup.BackupRetentionPeriod); err != nil { + return diag.FromErr(fmt.Errorf("error setting backup retention period for RDS DB Instance: %s", err)) + } + if err := d.Set("kms_key_id", dbInstanceAutomatedBackup.KmsKeyId); err != nil { + return diag.FromErr(fmt.Errorf("error setting kms key id for RDS DB Instance: %s", err)) + } + if err := d.Set("source_db_instance_arn", dbInstanceAutomatedBackup.DBInstanceArn); err != nil { + return diag.FromErr(fmt.Errorf("error setting source db instance arn for RDS DB Instance: (%s)", err)) + } + + return nil +} + +func resourceAwsDbInstanceAutomatedBackupsReplicationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*AWSClient).rdsconn + + input := &rds.DeleteDBInstanceAutomatedBackupInput{ + DBInstanceAutomatedBackupsArn: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Delete RDS DB Instance Automated Backups Replication: %s", d.Id()) + _, err := conn.DeleteDBInstanceAutomatedBackupWithContext(ctx, input) + + if isAWSErr(err, rds.ErrCodeDBInstanceAutomatedBackupNotFoundFault, "") { + return nil + } + + if isAWSErr(err, rds.ErrCodeInvalidDBInstanceAutomatedBackupStateFault, "") { + return nil + } + + if isAWSErr(err, rds.ErrCodeInvalidDBInstanceStateFault, "") { + return nil + } + + if err != nil { + return diag.FromErr(fmt.Errorf("error delete RDS DB Instance Automated Backups Replication: %s", err)) + } + + if _, err = waiter.DBInstanceAutomatedBackupsReplicationDeleted(ctx, conn, d.Id()); err != nil { + return diag.FromErr(fmt.Errorf("error waiting to delete RDS DB Instance Automated Backups Replication: %s", err)) + } + + return nil +} diff --git a/aws/resource_aws_db_instance_automated_backups_replication_test.go b/aws/resource_aws_db_instance_automated_backups_replication_test.go new file mode 100644 index 000000000000..a05950951fb4 --- /dev/null +++ b/aws/resource_aws_db_instance_automated_backups_replication_test.go @@ -0,0 +1,158 @@ +package aws + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/rds/finder" +) + +func TestAccAWSDbInstanceAutomatedBackupsReplication_basic(t *testing.T) { + var providers []*schema.Provider + var dbInstanceAutomatedBackup rds.DBInstanceAutomatedBackup + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_db_instance_automated_backups_replication.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccMultipleRegionPreCheck(t, 2) + }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + ProviderFactories: testAccProviderFactoriesMultipleRegion(&providers, 2), + CheckDestroy: testAccCheckAWSDbInstanceAutomatedBackupsReplicationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSDbInstanceAutomatedBackupsReplicationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDbInstanceAutomatedBackupsReplicationExists(resourceName, &dbInstanceAutomatedBackup), + testAccCheckResourceAttrRegionalARNIgnoreRegionAndAccount(resourceName, "source_db_instance_arn", "rds", regexp.MustCompile(`db:.+`).String()), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSDbInstanceAutomatedBackupsReplication_disappears(t *testing.T) { + var providers []*schema.Provider + var dbInstanceAutomatedBackup rds.DBInstanceAutomatedBackup + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_db_instance_automated_backups_replication.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccMultipleRegionPreCheck(t, 2) + testAccAlternateAccountPreCheck(t) + }, + ErrorCheck: testAccErrorCheck(t, rds.EndpointsID), + ProviderFactories: testAccProviderFactoriesMultipleRegion(&providers, 2), + CheckDestroy: testAccCheckAWSDbInstanceAutomatedBackupsReplicationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSDbInstanceAutomatedBackupsReplicationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDbInstanceAutomatedBackupsReplicationExists(resourceName, &dbInstanceAutomatedBackup), + testAccCheckResourceDisappears(testAccProvider, resourceAwsDbInstanceAutomatedBackupsReplication(), resourceName), + ), + ExpectNonEmptyPlan: false, + }, + }, + }) +} + +func testAccCheckAWSDbInstanceAutomatedBackupsReplicationExists(resourceName string, dbInstanceAutomatedBackup *rds.DBInstanceAutomatedBackup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("Resource not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + backup, err := finder.DBInstanceAutomatedBackup(context.Background(), conn, rs.Primary.ID) + if err != nil { + return err + } + + if backup == nil { + return fmt.Errorf("RDS DB Instance Automated Backup not found (%s)", rs.Primary.ID) + } + + *dbInstanceAutomatedBackup = *backup + + return nil + } +} + +func testAccCheckAWSDbInstanceAutomatedBackupsReplicationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_db_instance_automated_backups_replication" { + continue + } + + backup, err := finder.DBInstanceAutomatedBackup(context.Background(), conn, rs.Primary.ID) + if isAWSErr(err, rds.ErrCodeDBInstanceAutomatedBackupNotFoundFault, "") { + continue + } + + if isAWSErr(err, rds.ErrCodeInvalidDBInstanceAutomatedBackupStateFault, "") { + continue + } + + if isAWSErr(err, rds.ErrCodeInvalidDBInstanceStateFault, "") { + return nil + } + + if err != nil { + return err + } + + if backup == nil { + continue + } + + return fmt.Errorf("RDS DB Instance Automated Backups Replication still exists %s", rs.Primary.ID) + } + + return nil +} + +func testAccAWSDbInstanceAutomatedBackupsReplicationConfig(rName string) string { + return composeConfig( + testAccMultipleRegionProviderConfig(2), fmt.Sprintf(` +resource "aws_db_instance" "test" { + allocated_storage = 10 + engine = "postgres" + identifier = %[1]q + instance_class = "db.t3.micro" + password = "avoid-plaintext-passwords" + username = "tfacctest" + skip_final_snapshot = true + delete_automated_backups = true + backup_retention_period = 1 + provider = awsalternate +} + +resource "aws_db_instance_automated_backups_replication" "test" { + source_db_instance_arn = aws_db_instance.test.arn +} +`, rName)) +} diff --git a/website/docs/r/db_instance_automated_backups_replication.markdown b/website/docs/r/db_instance_automated_backups_replication.markdown new file mode 100644 index 000000000000..3753d57f3b8e --- /dev/null +++ b/website/docs/r/db_instance_automated_backups_replication.markdown @@ -0,0 +1,46 @@ +--- +subcategory: "RDS" +layout: "aws" +page_title: "AWS: aws_db_instance_automated_backups_replication" +description: |- + Manages an RDS DB Instance Automated Backups Replication. +--- + +# Resource: aws_db_instance_automated_backups_replication + +Manages an RDS DB Instance Automated Backups Replication. + +~> **NOTE:** This resource requires a second AWS provider to be defined in another region. + +* [Replicating automated backups to another AWS Region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReplicateBackups.html) + +## Example Usage + +```terraform +resource "aws_db_instance_automated_backups_replication" "example" { + source_db_instance_arn = aws_db_instance.example.arn +} +``` + +## Argument Reference + +The following arguments are supported: + +* `backup_retention_period` - (Optional) The retention period for the replicated automated backups. +* `kms_key_id` - (Optional) The AWS KMS key identifier for encryption of the replicated automated backups. +* `pre_signed_url` - (Optional) A URL that contains a Signature Version 4 signed request for the [StartDBInstanceAutomatedBackupsReplication](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_StartDBInstanceAutomatedBackupsReplication.html) action to be called in the AWS Region of the source DB instance. +* `source_db_instance_arn` - (Optional) The Amazon Resource Name (ARN) of the source DB instance for the replicated automated backups. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - DB Instance Automated Backup Replication ARN + +## Import + +`aws_db_instance_automated_backups_replication` can be imported using the DB Instance Automated Backup Replication ARN, e.g. + +``` +$ terraform import aws_db_instance_automated_backups_replication.example arn:aws:rds:eu-west-1:123456789012:auto-backup:ab-lrg8qb6qtarwcfvoto3so53igbdulp3xjs8xeym +```