diff --git a/aws/provider.go b/aws/provider.go index 7d107c649a4c..6e7fc4e8de98 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -584,6 +584,7 @@ func Provider() terraform.ResourceProvider { "aws_rds_cluster_endpoint": resourceAwsRDSClusterEndpoint(), "aws_rds_cluster_instance": resourceAwsRDSClusterInstance(), "aws_rds_cluster_parameter_group": resourceAwsRDSClusterParameterGroup(), + "aws_rds_global_cluster": resourceAwsRDSGlobalCluster(), "aws_redshift_cluster": resourceAwsRedshiftCluster(), "aws_redshift_security_group": resourceAwsRedshiftSecurityGroup(), "aws_redshift_parameter_group": resourceAwsRedshiftParameterGroup(), diff --git a/aws/resource_aws_rds_cluster.go b/aws/resource_aws_rds_cluster.go index 2f46a87272b4..6e43ebf1071e 100644 --- a/aws/resource_aws_rds_cluster.go +++ b/aws/resource_aws_rds_cluster.go @@ -1,6 +1,7 @@ package aws import ( + "errors" "fmt" "log" "regexp" @@ -106,6 +107,11 @@ func resourceAwsRDSCluster() *schema.Resource { Computed: true, }, + "global_cluster_identifier": { + Type: schema.TypeString, + Optional: true, + }, + "reader_endpoint": { Type: schema.TypeString, Computed: true, @@ -130,6 +136,7 @@ func resourceAwsRDSCluster() *schema.Resource { ForceNew: true, Default: "provisioned", ValidateFunc: validation.StringInSlice([]string{ + "global", "parallelquery", "provisioned", "serverless", @@ -750,6 +757,10 @@ func resourceAwsRDSClusterCreate(d *schema.ResourceData, meta interface{}) error createOpts.EngineVersion = aws.String(attr.(string)) } + if attr, ok := d.GetOk("global_cluster_identifier"); ok { + createOpts.GlobalClusterIdentifier = aws.String(attr.(string)) + } + if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 { createOpts.VpcSecurityGroupIds = expandStringList(attr.List()) } @@ -975,6 +986,21 @@ func resourceAwsRDSClusterRead(d *schema.ResourceData, meta interface{}) error { log.Printf("[WARN] Failed to save tags for RDS Cluster (%s): %s", aws.StringValue(dbc.DBClusterIdentifier), err) } + // Fetch and save Global Cluster if engine mode global + d.Set("global_cluster_identifier", "") + + if aws.StringValue(dbc.EngineMode) == "global" { + globalCluster, err := rdsDescribeGlobalClusterFromDbClusterARN(conn, aws.StringValue(dbc.DBClusterArn)) + + if err != nil { + return fmt.Errorf("error reading RDS Global Cluster information for DB Cluster (%s): %s", d.Id(), err) + } + + if globalCluster != nil { + d.Set("global_cluster_identifier", globalCluster.GlobalClusterIdentifier) + } + } + return nil } @@ -1084,6 +1110,30 @@ func resourceAwsRDSClusterUpdate(d *schema.ResourceData, meta interface{}) error } } + if d.HasChange("global_cluster_identifier") { + oRaw, nRaw := d.GetChange("global_cluster_identifier") + o := oRaw.(string) + n := nRaw.(string) + + if o == "" { + return errors.New("Existing RDS Clusters cannot be added to an existing RDS Global Cluster") + } + + if n != "" { + return errors.New("Existing RDS Clusters cannot be migrated between existing RDS Global Clusters") + } + + input := &rds.RemoveFromGlobalClusterInput{ + DbClusterIdentifier: aws.String(d.Get("arn").(string)), + GlobalClusterIdentifier: aws.String(o), + } + + log.Printf("[DEBUG] Removing RDS Cluster from RDS Global Cluster: %s", input) + if _, err := conn.RemoveFromGlobalCluster(input); err != nil { + return fmt.Errorf("error removing RDS Cluster (%s) from RDS Global Cluster: %s", d.Id(), err) + } + } + if d.HasChange("iam_roles") { oraw, nraw := d.GetChange("iam_roles") if oraw == nil { @@ -1128,6 +1178,22 @@ func resourceAwsRDSClusterDelete(d *schema.ResourceData, meta interface{}) error conn := meta.(*AWSClient).rdsconn log.Printf("[DEBUG] Destroying RDS Cluster (%s)", d.Id()) + // 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) != "" { + input := &rds.RemoveFromGlobalClusterInput{ + DbClusterIdentifier: aws.String(d.Get("arn").(string)), + GlobalClusterIdentifier: aws.String(d.Get("global_cluster_identifier").(string)), + } + + log.Printf("[DEBUG] Removing RDS Cluster from RDS Global Cluster: %s", input) + _, err := conn.RemoveFromGlobalCluster(input) + + if err != nil && !isAWSErr(err, rds.ErrCodeGlobalClusterNotFoundFault, "") { + return fmt.Errorf("error removing RDS Cluster (%s) from RDS Global Cluster: %s", d.Id(), err) + } + } + deleteOpts := rds.DeleteDBClusterInput{ DBClusterIdentifier: aws.String(d.Id()), } diff --git a/aws/resource_aws_rds_cluster_test.go b/aws/resource_aws_rds_cluster_test.go index a2c22cf7f423..f30a34fef6b9 100644 --- a/aws/resource_aws_rds_cluster_test.go +++ b/aws/resource_aws_rds_cluster_test.go @@ -63,6 +63,7 @@ func TestAccAWSRDSCluster_basic(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "cluster_resource_id"), resource.TestCheckResourceAttr(resourceName, "engine", "aurora"), resource.TestCheckResourceAttrSet(resourceName, "engine_version"), + resource.TestCheckResourceAttr(resourceName, "global_cluster_identifier", ""), resource.TestCheckResourceAttrSet(resourceName, "hosted_zone_id"), resource.TestCheckResourceAttr(resourceName, "enabled_cloudwatch_logs_exports.0", "audit"), @@ -529,6 +530,40 @@ func TestAccAWSRDSCluster_EngineMode(t *testing.T) { }) } +func TestAccAWSRDSCluster_EngineMode_Global(t *testing.T) { + var dbCluster1 rds.DBCluster + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterConfig_EngineMode(rName, "global"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSClusterExists(resourceName, &dbCluster1), + resource.TestCheckResourceAttr(resourceName, "engine_mode", "global"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "apply_immediately", + "cluster_identifier_prefix", + "master_password", + "skip_final_snapshot", + "snapshot_identifier", + }, + }, + }, + }) +} + func TestAccAWSRDSCluster_EngineMode_ParallelQuery(t *testing.T) { var dbCluster1 rds.DBCluster @@ -619,6 +654,125 @@ func TestAccAWSRDSCluster_EngineVersionWithPrimaryInstance(t *testing.T) { }) } +func TestAccAWSRDSCluster_GlobalClusterIdentifier(t *testing.T) { + var dbCluster1 rds.DBCluster + + rName := acctest.RandomWithPrefix("tf-acc-test") + globalClusterResourceName := "aws_rds_global_cluster.test" + resourceName := "aws_rds_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterConfig_GlobalClusterIdentifier(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSClusterExists(resourceName, &dbCluster1), + resource.TestCheckResourceAttrPair(resourceName, "global_cluster_identifier", globalClusterResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "apply_immediately", + "cluster_identifier_prefix", + "master_password", + "skip_final_snapshot", + "snapshot_identifier", + }, + }, + }, + }) +} + +func TestAccAWSRDSCluster_GlobalClusterIdentifier_Add(t *testing.T) { + var dbCluster1 rds.DBCluster + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterConfig_EngineMode(rName, "global"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSClusterExists(resourceName, &dbCluster1), + resource.TestCheckResourceAttr(resourceName, "global_cluster_identifier", ""), + ), + }, + { + Config: testAccAWSRDSClusterConfig_GlobalClusterIdentifier(rName), + ExpectError: regexp.MustCompile(`Existing RDS Clusters cannot be added to an existing RDS Global Cluster`), + }, + }, + }) +} + +func TestAccAWSRDSCluster_GlobalClusterIdentifier_Remove(t *testing.T) { + var dbCluster1 rds.DBCluster + + rName := acctest.RandomWithPrefix("tf-acc-test") + globalClusterResourceName := "aws_rds_global_cluster.test" + resourceName := "aws_rds_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterConfig_GlobalClusterIdentifier(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSClusterExists(resourceName, &dbCluster1), + resource.TestCheckResourceAttrPair(resourceName, "global_cluster_identifier", globalClusterResourceName, "id"), + ), + }, + { + Config: testAccAWSRDSClusterConfig_EngineMode(rName, "global"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSClusterExists(resourceName, &dbCluster1), + resource.TestCheckResourceAttr(resourceName, "global_cluster_identifier", ""), + ), + }, + }, + }) +} + +func TestAccAWSRDSCluster_GlobalClusterIdentifier_Update(t *testing.T) { + var dbCluster1 rds.DBCluster + + rName := acctest.RandomWithPrefix("tf-acc-test") + globalClusterResourceName1 := "aws_rds_global_cluster.test.0" + globalClusterResourceName2 := "aws_rds_global_cluster.test.1" + resourceName := "aws_rds_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRDSClusterConfig_GlobalClusterIdentifier_Update(rName, globalClusterResourceName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSClusterExists(resourceName, &dbCluster1), + resource.TestCheckResourceAttrPair(resourceName, "global_cluster_identifier", globalClusterResourceName1, "id"), + ), + }, + { + Config: testAccAWSRDSClusterConfig_GlobalClusterIdentifier_Update(rName, globalClusterResourceName2), + ExpectError: regexp.MustCompile(`Existing RDS Clusters cannot be migrated between existing RDS Global Clusters`), + }, + }, + }) +} + func TestAccAWSRDSCluster_Port(t *testing.T) { var dbCluster1, dbCluster2 rds.DBCluster rInt := acctest.RandInt() @@ -2118,6 +2272,42 @@ resource "aws_rds_cluster" "test" { `, rName, engineMode) } +func testAccAWSRDSClusterConfig_GlobalClusterIdentifier(rName string) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + global_cluster_identifier = %q +} + +resource "aws_rds_cluster" "test" { + cluster_identifier = %q + global_cluster_identifier = "${aws_rds_global_cluster.test.id}" + engine_mode = "global" + master_password = "barbarbarbar" + master_username = "foo" + skip_final_snapshot = true +} +`, rName, rName) +} + +func testAccAWSRDSClusterConfig_GlobalClusterIdentifier_Update(rName, globalClusterIdentifierResourceName string) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + count = 2 + + global_cluster_identifier = "%s-${count.index}" +} + +resource "aws_rds_cluster" "test" { + cluster_identifier = %q + global_cluster_identifier = "${%s.id}" + engine_mode = "global" + master_password = "barbarbarbar" + master_username = "foo" + skip_final_snapshot = true +} +`, rName, rName, globalClusterIdentifierResourceName) +} + func testAccAWSRDSClusterConfig_ScalingConfiguration(rName string, autoPause bool, maxCapacity, minCapacity, secondsUntilAutoPause int) string { return fmt.Sprintf(` resource "aws_rds_cluster" "test" { diff --git a/aws/resource_aws_rds_global_cluster.go b/aws/resource_aws_rds_global_cluster.go new file mode 100644 index 000000000000..119415992c0e --- /dev/null +++ b/aws/resource_aws_rds_global_cluster.go @@ -0,0 +1,349 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceAwsRDSGlobalCluster() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRDSGlobalClusterCreate, + Read: resourceAwsRDSGlobalClusterRead, + Update: resourceAwsRDSGlobalClusterUpdate, + Delete: resourceAwsRDSGlobalClusterDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "database_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "deletion_protection": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "engine": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "aurora", + ValidateFunc: validation.StringInSlice([]string{ + "aurora", + }, false), + }, + "engine_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "global_cluster_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "global_cluster_resource_id": { + Type: schema.TypeString, + Computed: true, + }, + "storage_encrypted": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsRDSGlobalClusterCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + input := &rds.CreateGlobalClusterInput{ + DeletionProtection: aws.Bool(d.Get("deletion_protection").(bool)), + GlobalClusterIdentifier: aws.String(d.Get("global_cluster_identifier").(string)), + StorageEncrypted: aws.Bool(d.Get("storage_encrypted").(bool)), + } + + if v, ok := d.GetOk("database_name"); ok { + input.DatabaseName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("engine"); ok { + input.Engine = aws.String(v.(string)) + } + + if v, ok := d.GetOk("engine_version"); ok { + input.EngineVersion = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating RDS Global Cluster: %s", input) + output, err := conn.CreateGlobalCluster(input) + if err != nil { + return fmt.Errorf("error creating RDS Global Cluster: %s", err) + } + + d.SetId(aws.StringValue(output.GlobalCluster.GlobalClusterIdentifier)) + + if err := waitForRdsGlobalClusterCreation(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for RDS Global Cluster (%s) availability: %s", d.Id(), err) + } + + return resourceAwsRDSGlobalClusterRead(d, meta) +} + +func resourceAwsRDSGlobalClusterRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + globalCluster, err := rdsDescribeGlobalCluster(conn, d.Id()) + + if isAWSErr(err, rds.ErrCodeGlobalClusterNotFoundFault, "") { + log.Printf("[WARN] RDS Global Cluster (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading RDS Global Cluster: %s", err) + } + + if globalCluster == nil { + log.Printf("[WARN] RDS Global Cluster (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if aws.StringValue(globalCluster.Status) == "deleting" || aws.StringValue(globalCluster.Status) == "deleted" { + log.Printf("[WARN] RDS Global Cluster (%s) in deleted state (%s), removing from state", d.Id(), aws.StringValue(globalCluster.Status)) + d.SetId("") + return nil + } + + d.Set("arn", globalCluster.GlobalClusterArn) + d.Set("database_name", globalCluster.DatabaseName) + d.Set("deletion_protection", globalCluster.DeletionProtection) + d.Set("engine", globalCluster.Engine) + d.Set("engine_version", globalCluster.EngineVersion) + d.Set("global_cluster_identifier", globalCluster.GlobalClusterIdentifier) + d.Set("global_cluster_resource_id", globalCluster.GlobalClusterResourceId) + d.Set("storage_encrypted", globalCluster.StorageEncrypted) + + return nil +} + +func resourceAwsRDSGlobalClusterUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + input := &rds.ModifyGlobalClusterInput{ + DeletionProtection: aws.Bool(d.Get("deletion_protection").(bool)), + GlobalClusterIdentifier: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Updating RDS Global Cluster (%s): %s", d.Id(), input) + _, err := conn.ModifyGlobalCluster(input) + + if isAWSErr(err, rds.ErrCodeGlobalClusterNotFoundFault, "") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting RDS Global Cluster: %s", err) + } + + if err := waitForRdsGlobalClusterUpdate(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for RDS Global Cluster (%s) update: %s", d.Id(), err) + } + + return nil +} + +func resourceAwsRDSGlobalClusterDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).rdsconn + + input := &rds.DeleteGlobalClusterInput{ + GlobalClusterIdentifier: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting RDS Global Cluster (%s): %s", d.Id(), input) + + // Allow for eventual consistency + // InvalidGlobalClusterStateFault: Global Cluster arn:aws:rds::123456789012:global-cluster:tf-acc-test-5618525093076697001-0 is not empty + err := resource.Retry(1*time.Minute, func() *resource.RetryError { + _, err := conn.DeleteGlobalCluster(input) + + if isAWSErr(err, rds.ErrCodeInvalidGlobalClusterStateFault, "is not empty") { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if isAWSErr(err, rds.ErrCodeGlobalClusterNotFoundFault, "") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting RDS Global Cluster: %s", err) + } + + if err := waitForRdsGlobalClusterDeletion(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for RDS Global Cluster (%s) deletion: %s", d.Id(), err) + } + + return nil +} + +func rdsDescribeGlobalCluster(conn *rds.RDS, globalClusterID string) (*rds.GlobalCluster, error) { + var globalCluster *rds.GlobalCluster + + input := &rds.DescribeGlobalClustersInput{ + GlobalClusterIdentifier: aws.String(globalClusterID), + } + + log.Printf("[DEBUG] Reading RDS Global Cluster (%s): %s", globalClusterID, input) + err := conn.DescribeGlobalClustersPages(input, func(page *rds.DescribeGlobalClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, gc := range page.GlobalClusters { + if gc == nil { + continue + } + + if aws.StringValue(gc.GlobalClusterIdentifier) == globalClusterID { + globalCluster = gc + return false + } + } + + return !lastPage + }) + + return globalCluster, err +} + +func rdsDescribeGlobalClusterFromDbClusterARN(conn *rds.RDS, dbClusterARN string) (*rds.GlobalCluster, error) { + var globalCluster *rds.GlobalCluster + + input := &rds.DescribeGlobalClustersInput{ + Filters: []*rds.Filter{ + { + Name: aws.String("db-cluster-id"), + Values: []*string{aws.String(dbClusterARN)}, + }, + }, + } + + log.Printf("[DEBUG] Reading RDS Global Clusters: %s", input) + err := conn.DescribeGlobalClustersPages(input, func(page *rds.DescribeGlobalClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, gc := range page.GlobalClusters { + if gc == nil { + continue + } + + for _, globalClusterMember := range gc.GlobalClusterMembers { + if aws.StringValue(globalClusterMember.DBClusterArn) == dbClusterARN { + globalCluster = gc + return false + } + } + } + + return !lastPage + }) + + return globalCluster, err +} + +func rdsGlobalClusterRefreshFunc(conn *rds.RDS, globalClusterID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + globalCluster, err := rdsDescribeGlobalCluster(conn, globalClusterID) + + if isAWSErr(err, rds.ErrCodeGlobalClusterNotFoundFault, "") { + return nil, "deleted", nil + } + + if err != nil { + return nil, "", fmt.Errorf("error reading RDS Global Cluster (%s): %s", globalClusterID, err) + } + + if globalCluster == nil { + return nil, "deleted", nil + } + + return globalCluster, aws.StringValue(globalCluster.Status), nil + } +} + +func waitForRdsGlobalClusterCreation(conn *rds.RDS, globalClusterID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating"}, + Target: []string{"available"}, + Refresh: rdsGlobalClusterRefreshFunc(conn, globalClusterID), + Timeout: 10 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for RDS Global Cluster (%s) availability", globalClusterID) + _, err := stateConf.WaitForState() + + return err +} + +func waitForRdsGlobalClusterUpdate(conn *rds.RDS, globalClusterID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"modifying"}, + Target: []string{"available"}, + Refresh: rdsGlobalClusterRefreshFunc(conn, globalClusterID), + Timeout: 10 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for RDS Global Cluster (%s) availability", globalClusterID) + _, err := stateConf.WaitForState() + + return err +} + +func waitForRdsGlobalClusterDeletion(conn *rds.RDS, globalClusterID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "available", + "deleting", + }, + Target: []string{"deleted"}, + Refresh: rdsGlobalClusterRefreshFunc(conn, globalClusterID), + Timeout: 10 * time.Minute, + NotFoundChecks: 1, + } + + log.Printf("[DEBUG] Waiting for RDS Global Cluster (%s) deletion", globalClusterID) + _, err := stateConf.WaitForState() + + if isResourceNotFoundError(err) { + return nil + } + + return err +} diff --git a/aws/resource_aws_rds_global_cluster_test.go b/aws/resource_aws_rds_global_cluster_test.go new file mode 100644 index 000000000000..1c45f0beef25 --- /dev/null +++ b/aws/resource_aws_rds_global_cluster_test.go @@ -0,0 +1,376 @@ +package aws + +import ( + "errors" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSRdsGlobalCluster_basic(t *testing.T) { + var globalCluster1 rds.GlobalCluster + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_global_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRdsGlobalClusterConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1), + testAccCheckResourceAttrGlobalARN(resourceName, "arn", "rds", fmt.Sprintf("global-cluster:%s", rName)), + resource.TestCheckResourceAttr(resourceName, "database_name", ""), + resource.TestCheckResourceAttr(resourceName, "deletion_protection", "false"), + resource.TestCheckResourceAttrSet(resourceName, "engine"), + resource.TestCheckResourceAttrSet(resourceName, "engine_version"), + resource.TestCheckResourceAttr(resourceName, "global_cluster_identifier", rName), + resource.TestMatchResourceAttr(resourceName, "global_cluster_resource_id", regexp.MustCompile(`cluster-.+`)), + resource.TestCheckResourceAttr(resourceName, "storage_encrypted", "false"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSRdsGlobalCluster_disappears(t *testing.T) { + var globalCluster1 rds.GlobalCluster + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_global_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRdsGlobalClusterConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1), + testAccCheckAWSRdsGlobalClusterDisappears(&globalCluster1), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccAWSRdsGlobalCluster_DatabaseName(t *testing.T) { + var globalCluster1, globalCluster2 rds.GlobalCluster + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_global_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRdsGlobalClusterConfigDatabaseName(rName, "database1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1), + resource.TestCheckResourceAttr(resourceName, "database_name", "database1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSRdsGlobalClusterConfigDatabaseName(rName, "database2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster2), + testAccCheckAWSRdsGlobalClusterRecreated(&globalCluster1, &globalCluster2), + resource.TestCheckResourceAttr(resourceName, "database_name", "database2"), + ), + }, + }, + }) +} + +func TestAccAWSRdsGlobalCluster_DeletionProtection(t *testing.T) { + var globalCluster1, globalCluster2 rds.GlobalCluster + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_global_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRdsGlobalClusterConfigDeletionProtection(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1), + resource.TestCheckResourceAttr(resourceName, "deletion_protection", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSRdsGlobalClusterConfigDeletionProtection(rName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster2), + testAccCheckAWSRdsGlobalClusterNotRecreated(&globalCluster1, &globalCluster2), + resource.TestCheckResourceAttr(resourceName, "deletion_protection", "false"), + ), + }, + }, + }) +} + +func TestAccAWSRdsGlobalCluster_Engine_Aurora(t *testing.T) { + var globalCluster1 rds.GlobalCluster + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_global_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRdsGlobalClusterConfigEngine(rName, "aurora"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1), + resource.TestCheckResourceAttr(resourceName, "engine", "aurora"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSRdsGlobalCluster_EngineVersion_Aurora(t *testing.T) { + var globalCluster1 rds.GlobalCluster + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_global_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRdsGlobalClusterConfigEngineVersion(rName, "aurora", "5.6.10a"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1), + resource.TestCheckResourceAttr(resourceName, "engine_version", "5.6.10a"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSRdsGlobalCluster_StorageEncrypted(t *testing.T) { + var globalCluster1, globalCluster2 rds.GlobalCluster + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_rds_global_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRdsGlobalClusterConfigStorageEncrypted(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1), + resource.TestCheckResourceAttr(resourceName, "storage_encrypted", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSRdsGlobalClusterConfigStorageEncrypted(rName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster2), + testAccCheckAWSRdsGlobalClusterRecreated(&globalCluster1, &globalCluster2), + resource.TestCheckResourceAttr(resourceName, "storage_encrypted", "false"), + ), + }, + }, + }) +} + +func testAccCheckAWSRdsGlobalClusterExists(resourceName string, globalCluster *rds.GlobalCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No RDS Global Cluster ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + cluster, err := rdsDescribeGlobalCluster(conn, rs.Primary.ID) + + if err != nil { + return err + } + + if cluster == nil { + return fmt.Errorf("RDS Global Cluster not found") + } + + if aws.StringValue(cluster.Status) != "available" { + return fmt.Errorf("RDS Global Cluster (%s) exists in non-available (%s) state", rs.Primary.ID, aws.StringValue(cluster.Status)) + } + + *globalCluster = *cluster + + return nil + } +} + +func testAccCheckAWSRdsGlobalClusterDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_rds_global_cluster" { + continue + } + + globalCluster, err := rdsDescribeGlobalCluster(conn, rs.Primary.ID) + + if isAWSErr(err, rds.ErrCodeGlobalClusterNotFoundFault, "") { + continue + } + + if err != nil { + return err + } + + if globalCluster == nil { + continue + } + + return fmt.Errorf("RDS Global Cluster (%s) still exists in non-deleted (%s) state", rs.Primary.ID, aws.StringValue(globalCluster.Status)) + } + + return nil +} + +func testAccCheckAWSRdsGlobalClusterDisappears(globalCluster *rds.GlobalCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).rdsconn + + input := &rds.DeleteGlobalClusterInput{ + GlobalClusterIdentifier: globalCluster.GlobalClusterIdentifier, + } + + _, err := conn.DeleteGlobalCluster(input) + + if err != nil { + return err + } + + return waitForRdsGlobalClusterDeletion(conn, aws.StringValue(globalCluster.GlobalClusterIdentifier)) + } +} + +func testAccCheckAWSRdsGlobalClusterNotRecreated(i, j *rds.GlobalCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.StringValue(i.GlobalClusterResourceId) != aws.StringValue(j.GlobalClusterResourceId) { + return errors.New("RDS Global Cluster was recreated") + } + + return nil + } +} + +func testAccCheckAWSRdsGlobalClusterRecreated(i, j *rds.GlobalCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.StringValue(i.GlobalClusterResourceId) == aws.StringValue(j.GlobalClusterResourceId) { + return errors.New("RDS Global Cluster was not recreated") + } + + return nil + } +} + +func testAccAWSRdsGlobalClusterConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + global_cluster_identifier = %q +} +`, rName) +} + +func testAccAWSRdsGlobalClusterConfigDatabaseName(rName, databaseName string) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + database_name = %q + global_cluster_identifier = %q +} +`, databaseName, rName) +} + +func testAccAWSRdsGlobalClusterConfigDeletionProtection(rName string, deletionProtection bool) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + deletion_protection = %t + global_cluster_identifier = %q +} +`, deletionProtection, rName) +} + +func testAccAWSRdsGlobalClusterConfigEngine(rName, engine string) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + engine = %q + global_cluster_identifier = %q +} +`, engine, rName) +} + +func testAccAWSRdsGlobalClusterConfigEngineVersion(rName, engine, engineVersion string) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + engine = %q + engine_version = %q + global_cluster_identifier = %q +} +`, engine, engineVersion, rName) +} + +func testAccAWSRdsGlobalClusterConfigStorageEncrypted(rName string, storageEncrypted bool) string { + return fmt.Sprintf(` +resource "aws_rds_global_cluster" "test" { + global_cluster_identifier = %q + storage_encrypted = %t +} +`, rName, storageEncrypted) +} diff --git a/website/aws.erb b/website/aws.erb index 7bde4379f4c1..6069a59efdd0 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1918,6 +1918,9 @@ aws_rds_cluster_parameter_group + > + aws_rds_global_cluster + diff --git a/website/docs/r/rds_cluster.html.markdown b/website/docs/r/rds_cluster.html.markdown index 0419b9bf49d5..748a56ca3cbc 100644 --- a/website/docs/r/rds_cluster.html.markdown +++ b/website/docs/r/rds_cluster.html.markdown @@ -114,7 +114,7 @@ Default: A 30-minute window selected at random from an 8-hour block of time per * `iam_roles` - (Optional) A List of ARNs for the IAM roles to associate to the RDS Cluster. * `iam_database_authentication_enabled` - (Optional) Specifies whether or mappings of AWS Identity and Access Management (IAM) accounts to database accounts is enabled. Please see [AWS Documentation][6] for availability and limitations. * `engine` - (Optional) The name of the database engine to be used for this DB cluster. Defaults to `aurora`. Valid Values: `aurora`, `aurora-mysql`, `aurora-postgresql` -* `engine_mode` - (Optional) The database engine mode. Valid values: `parallelquery`, `provisioned`, `serverless`. Defaults to: `provisioned`. See the [RDS User Guide](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/aurora-serverless.html) for limitations when using `serverless`. +* `engine_mode` - (Optional) The database engine mode. Valid values: `global`, `parallelquery`, `provisioned`, `serverless`. Defaults to: `provisioned`. See the [RDS User Guide](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/aurora-serverless.html) for limitations when using `serverless`. * `engine_version` - (Optional) The database engine version. Updating this argument results in an outage. * `source_region` - (Optional) The source region for an encrypted replica DB cluster. * `enabled_cloudwatch_logs_exports` - (Optional) List of log types to export to cloudwatch. If omitted, no logs will be exported. diff --git a/website/docs/r/rds_global_cluster.html.markdown b/website/docs/r/rds_global_cluster.html.markdown new file mode 100644 index 000000000000..b5b4ec71055c --- /dev/null +++ b/website/docs/r/rds_global_cluster.html.markdown @@ -0,0 +1,92 @@ +--- +layout: "aws" +page_title: "AWS: aws_rds_global_cluster" +sidebar_current: "docs-aws-resource-ec2-transit-gateway-x" +description: |- + Manages a RDS Global Cluster +--- + +# aws_rds_global_cluster + +Manages a RDS Global Cluster, which is an Aurora global database spread across multiple regions. The global database contains a single primary cluster with read-write capability, and a read-only secondary cluster that receives data from the primary cluster through high-speed replication performed by the Aurora storage subsystem. + +More information about Aurora global databases can be found in the [Aurora User Guide](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-global-database.html#aurora-global-database-creating). + +~> **NOTE:** RDS only supports the `aurora` engine (MySQL 5.6 compatible) for Global Clusters at this time. + +## Example Usage + +```hcl +provider "aws" { + alias = "primary" + region = "us-east-2" +} + +provider "aws" { + alias = "secondary" + region = "us-west-2" +} + +resource "aws_rds_global_cluster" "example" { + provider = "aws.primary" + + global_cluster_identifier = "example" +} + +resource "aws_rds_cluster" "primary" { + provider = "aws.primary" + + # ... other configuration ... + engine_mode = "global" + global_cluster_identifier = "${aws_rds_global_cluster.example.id}" +} + +resource "aws_rds_cluster_instance" "primary" { + provider = "aws.primary" + + # ... other configuration ... + cluster_identifier = "${aws_rds_cluster.primary.id}" +} + +resource "aws_rds_cluster" "secondary" { + depends_on = ["aws_rds_cluster_instance.primary"] + provider = "aws.secondary" + + # ... other configuration ... + engine_mode = "global" + global_cluster_identifier = "${aws_rds_global_cluster.example.id}" +} + +resource "aws_rds_cluster_instance" "secondary" { + provider = "aws.secondary" + + # ... other configuration ... + cluster_identifier = "${aws_rds_cluster.secondary.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `database_name` - (Optional) Name for an automatically created database on cluster creation. +* `deletion_protection` - (Optional) If the Global Cluster should have deletion protection enabled. The database can't be deleted when this value is set to `true`. The default is `false`. +* `engine` - (Optional) Name of the database engine to be used for this DB cluster. Valid values: `aurora`. Defaults to `aurora`. +* `engine_version` - (Optional) Engine version of the Aurora global database. +* `storage_encrypted` - (Optional) Specifies whether the DB cluster is encrypted. The default is `false`. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - RDS Global Cluster Amazon Resource Name (ARN) +* `global_cluster_resource_id` - AWS Region-unique, immutable identifier for the global database cluster. This identifier is found in AWS CloudTrail log entries whenever the AWS KMS key for the DB cluster is accessed +* `id` - RDS Global Cluster identifier + +## Import + +`aws_rds_global_cluster` can be imported by using the RDS Global Cluster identifier, e.g. + +``` +$ terraform import aws_rds_global_cluster.example example +```