Skip to content

Commit

Permalink
Allow engine_version upgrades
Browse files Browse the repository at this point in the history
  • Loading branch information
bill-rich committed Apr 6, 2021
1 parent af0e82b commit 8b3c2f8
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .changelog/18598.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_rds_global_cluster: Allow `engine_version` to be upgraded.
```
118 changes: 115 additions & 3 deletions aws/resource_aws_rds_global_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"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/helper/validation"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

const (
Expand Down Expand Up @@ -58,7 +59,6 @@ func resourceAwsRDSGlobalCluster() *schema.Resource {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"force_destroy": {
Type: schema.TypeBool,
Expand Down Expand Up @@ -211,6 +211,12 @@ func resourceAwsRDSGlobalClusterUpdate(d *schema.ResourceData, meta interface{})
GlobalClusterIdentifier: aws.String(d.Id()),
}

if d.HasChange("engine_version") {
if err := resourceAwsRDSGlobalClusterUpgradeEngineVersion(d, conn); err != nil {
return err
}
}

log.Printf("[DEBUG] Updating RDS Global Cluster (%s): %s", d.Id(), input)
_, err := conn.ModifyGlobalCluster(input)

Expand All @@ -226,7 +232,20 @@ func resourceAwsRDSGlobalClusterUpdate(d *schema.ResourceData, meta interface{})
return fmt.Errorf("error waiting for RDS Global Cluster (%s) update: %s", d.Id(), err)
}

return nil
return resourceAwsRDSGlobalClusterRead(d, meta)
}

func resourceAwsRDSGlobalClusterGetIdByArn(conn *rds.RDS, arn string) string {
result, err := conn.DescribeDBClusters(&rds.DescribeDBClustersInput{})
if err != nil {
return ""
}
for _, cluster := range result.DBClusters {
if aws.StringValue(cluster.DBClusterArn) == arn {
return aws.StringValue(cluster.DBClusterIdentifier)
}
}
return ""
}

func resourceAwsRDSGlobalClusterDelete(d *schema.ResourceData, meta interface{}) error {
Expand Down Expand Up @@ -430,10 +449,11 @@ func waitForRdsGlobalClusterCreation(conn *rds.RDS, globalClusterID string) erro

func waitForRdsGlobalClusterUpdate(conn *rds.RDS, globalClusterID string) error {
stateConf := &resource.StateChangeConf{
Pending: []string{"modifying"},
Pending: []string{"modifying", "upgrading"},
Target: []string{"available"},
Refresh: rdsGlobalClusterRefreshFunc(conn, globalClusterID),
Timeout: 10 * time.Minute,
Delay: 30 * time.Second,
}

log.Printf("[DEBUG] Waiting for RDS Global Cluster (%s) availability", globalClusterID)
Expand Down Expand Up @@ -498,3 +518,95 @@ func waitForRdsGlobalClusterRemoval(conn *rds.RDS, dbClusterIdentifier string) e

return nil
}

func resourceAwsRDSGlobalClusterUpgradeMajorEngineVersion(clusterId string, engineVersion string, conn *rds.RDS) error {
input := &rds.ModifyGlobalClusterInput{
GlobalClusterIdentifier: aws.String(clusterId),
}
input.AllowMajorVersionUpgrade = aws.Bool(true)
input.EngineVersion = aws.String(engineVersion)
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err := conn.ModifyGlobalCluster(input)
if err != nil {
if isAWSErr(err, rds.ErrCodeGlobalClusterNotFoundFault, "") {
return resource.NonRetryableError(err)
}
if isAWSErr(err, "InvalidParameterValue", "ModifyGlobalCluster only supports Major Version Upgrades. To patch the members of your global cluster to a newer minor version you need to call ModifyDbCluster in each one of them.") {
return resource.NonRetryableError(err)
}

return resource.RetryableError(err)
}

return nil
})
if tfresource.TimedOut(err) {
_, err = conn.ModifyGlobalCluster(input)
}
return err
}

func resourceAwsRDSGlobalClusterUpgradeMinorEngineVersion(clusterMembers *schema.Set, engineVersion string, conn *rds.RDS) error {
for _, clusterMemberRaw := range clusterMembers.List() {
clusterMember := clusterMemberRaw.(map[string]interface{})
if clusterMemberArn, ok := clusterMember["db_cluster_arn"]; ok && clusterMemberArn.(string) != "" {
modInput := &rds.ModifyDBClusterInput{
ApplyImmediately: aws.Bool(true),
DBClusterIdentifier: aws.String(clusterMemberArn.(string)),
EngineVersion: aws.String(engineVersion),
}
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err := conn.ModifyDBCluster(modInput)
if err != nil {
if isAWSErr(err, "InvalidParameterValue", "IAM role ARN value is invalid or does not include the required permissions") {
return resource.RetryableError(err)
}

if isAWSErr(err, rds.ErrCodeInvalidDBClusterStateFault, "Cannot modify engine version without a primary instance in DB cluster") {
return resource.NonRetryableError(err)
}

if isAWSErr(err, rds.ErrCodeInvalidDBClusterStateFault, "") {
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
}
return nil
})
if tfresource.TimedOut(err) {
_, err := conn.ModifyDBCluster(modInput)
if err != nil {
return err
}
}
if err != nil {
return fmt.Errorf("Failed to update engine_version on global cluster member (%s): %s", clusterMemberArn, err)
}
}
}
return nil
}

func resourceAwsRDSGlobalClusterUpgradeEngineVersion(d *schema.ResourceData, conn *rds.RDS) error {
log.Printf("[DEBUG] Upgrading RDS Global Cluster (%s) engine version: %s", d.Id(), d.Get("engine_version"))
err := resourceAwsRDSGlobalClusterUpgradeMajorEngineVersion(d.Id(), d.Get("engine_version").(string), conn)
if isAWSErr(err, "InvalidParameterValue", "ModifyGlobalCluster only supports Major Version Upgrades. To patch the members of your global cluster to a newer minor version you need to call ModifyDbCluster in each one of them.") {
err = resourceAwsRDSGlobalClusterUpgradeMinorEngineVersion(d.Get("global_cluster_members").(*schema.Set), d.Get("engine_version").(string), conn)
if err != nil {
return err
}
} else if err != nil {
return err
}
globalCluster, err := rdsDescribeGlobalCluster(conn, d.Id())
if err != nil {
return err
}
for _, clusterMember := range globalCluster.GlobalClusterMembers {
err := waitForRDSClusterUpdate(conn, resourceAwsRDSGlobalClusterGetIdByArn(conn, aws.StringValue(clusterMember.DBClusterArn)), d.Timeout(schema.TimeoutUpdate))
if err != nil {
return err
}
}
return nil
}
109 changes: 107 additions & 2 deletions aws/resource_aws_rds_global_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,75 @@ func TestAccAWSRdsGlobalCluster_EngineVersion_Aurora(t *testing.T) {
})
}

func TestAccAWSRdsGlobalCluster_EngineVersionUpdateMinor(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); testAccPreCheckAWSRdsGlobalCluster(t) },
ErrorCheck: testAccErrorCheck(t, rds.EndpointsID),
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSRdsGlobalClusterWithPrimaryConfigEngineVersion(rName, "aurora", "5.6.mysql_aurora.1.22.2"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1),
),
},
{
Config: testAccAWSRdsGlobalClusterWithPrimaryConfigEngineVersion(rName, "aurora", "5.6.mysql_aurora.1.23.2"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster2),
testAccCheckAWSRdsGlobalClusterNotRecreated(&globalCluster1, &globalCluster2),
resource.TestCheckResourceAttr(resourceName, "engine_version", "5.6.mysql_aurora.1.23.2"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccAWSRdsGlobalCluster_EngineVersionUpdateMajor(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); testAccPreCheckAWSRdsGlobalCluster(t) },
ErrorCheck: testAccErrorCheck(t, rds.EndpointsID),
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSRdsGlobalClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSRdsGlobalClusterWithPrimaryConfigEngineVersion(rName, "aurora", "5.6.mysql_aurora.1.22.2"),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster1),
),
},
{
Config: testAccAWSRdsGlobalClusterWithPrimaryConfigEngineVersion(rName, "aurora", "5.7.mysql_aurora.2.07.2"),
ExpectNonEmptyPlan: true,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSRdsGlobalClusterExists(resourceName, &globalCluster2),
testAccCheckAWSRdsGlobalClusterNotRecreated(&globalCluster1, &globalCluster2),
resource.TestCheckResourceAttr(resourceName, "engine_version", "5.7.mysql_aurora.2.07.2"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccAWSRdsGlobalCluster_EngineVersion_AuroraMySQL(t *testing.T) {
var globalCluster1 rds.GlobalCluster
rName := acctest.RandomWithPrefix("tf-acc-test")
Expand Down Expand Up @@ -478,8 +547,8 @@ func testAccCheckAWSRdsGlobalClusterDisappears(globalCluster *rds.GlobalCluster)

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")
if aws.StringValue(i.GlobalClusterArn) != aws.StringValue(j.GlobalClusterArn) {
return fmt.Errorf("RDS Global Cluster was recreated. got: %s, expected: %s", aws.StringValue(i.GlobalClusterArn), aws.StringValue(j.GlobalClusterArn))
}

return nil
Expand Down Expand Up @@ -557,6 +626,42 @@ resource "aws_rds_global_cluster" "test" {
`, engine, engineVersion, rName)
}

func testAccAWSRdsGlobalClusterWithPrimaryConfigEngineVersion(rName, engine, engineVersion string) string {
return fmt.Sprintf(`
resource "aws_rds_global_cluster" "test" {
engine = %[1]q
engine_version = %[2]q
global_cluster_identifier = %[3]q
}
resource "aws_rds_cluster" "test" {
apply_immediately = true
allow_major_version_upgrade = true
cluster_identifier = %[3]q
master_password = "mustbeeightcharacters"
master_username = "test"
skip_final_snapshot = true
global_cluster_identifier = aws_rds_global_cluster.test.global_cluster_identifier
lifecycle {
ignore_changes = [global_cluster_identifier]
}
}
resource "aws_rds_cluster_instance" "test" {
apply_immediately = true
cluster_identifier = aws_rds_cluster.test.id
identifier = %[3]q
instance_class = "db.r3.large"
lifecycle {
ignore_changes = [engine_version]
}
}
`, engine, engineVersion, rName)
}

func testAccAWSRdsGlobalClusterConfigSourceDbClusterIdentifier(rName string) string {
return fmt.Sprintf(`
resource "aws_rds_cluster" "test" {
Expand Down
2 changes: 1 addition & 1 deletion website/docs/r/rds_global_cluster.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ The following arguments are supported:
* `database_name` - (Optional, Forces new resources) 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, Forces new resources) Name of the database engine to be used for this DB cluster. Terraform will only perform drift detection if a configuration value is provided. Valid values: `aurora`, `aurora-mysql`, `aurora-postgresql`. Defaults to `aurora`. Conflicts with `source_db_cluster_identifier`.
* `engine_version` - (Optional, Forces new resources) Engine version of the Aurora global database.
* `engine_version` - (Optional) Engine version of the Aurora global database. Upgrading the engine version will result in all cluster members being immediately updated.
* **NOTE:** When the engine is set to `aurora-mysql`, an engine version compatible with global database is required. The earliest available version is `5.7.mysql_aurora.2.06.0`.
* `force_destroy` - (Optional) Enable to remove DB Cluster members from Global Cluster on destroy. Required with `source_db_cluster_identifier`.
* `source_db_cluster_identifier` - (Optional) Amazon Resource Name (ARN) to use as the primary DB Cluster of the Global Cluster on creation. Terraform cannot perform drift detection of this value.
Expand Down

0 comments on commit 8b3c2f8

Please sign in to comment.