Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added point_in_time_restore to aws_rds_cluster #8534

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 180 additions & 1 deletion aws/resource_aws_rds_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ func resourceAwsRDSCluster() *schema.Resource {
MaxItems: 1,
ConflictsWith: []string{
"snapshot_identifier",
"replication_source_identifier",
"point_in_time_restore",
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Expand Down Expand Up @@ -290,7 +292,11 @@ func resourceAwsRDSCluster() *schema.Resource {
Type: schema.TypeString,
Computed: false,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
ConflictsWith: []string{
"s3_import",
"replication_source_identifier",
"point_in_time_restore",
},
},

"port": {
Expand Down Expand Up @@ -355,6 +361,11 @@ func resourceAwsRDSCluster() *schema.Resource {
"replication_source_identifier": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{
"s3_import",
"snapshot_identifier",
"point_in_time_restore",
},
},

"iam_roles": {
Expand Down Expand Up @@ -394,6 +405,54 @@ func resourceAwsRDSCluster() *schema.Resource {
}, false),
},
},
"point_in_time_restore": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
ConflictsWith: []string{
"s3_import",
"snapshot_identifier",
"replication_source_identifier",
// We can't set the following values when point-in-time restore nor modification after restore.
"availability_zones",
"database_name",
"engine",
"engine_mode",
"master_username",
"storage_encrypted",
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"source_db_cluster_identifier": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"restore_type": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{
"full-copy",
"copy-on-write",
}, false),
Default: "full-copy",
},
"use_latest_restorable_time": {
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Default: false,
},
"restore_to_time": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
ValidateFunc: validation.ValidateRFC3339TimeString,
},
},
},
},

"tags": tagsSchema(),
},
Expand Down Expand Up @@ -519,6 +578,126 @@ func resourceAwsRDSClusterCreate(d *schema.ResourceData, meta interface{}) error
if err != nil {
return fmt.Errorf("Error creating RDS Cluster: %s", err)
}
} else if v, ok := d.GetOk("point_in_time_restore"); ok {

pointInTimeRestore := v.([]interface{})[0].(map[string]interface{})

restoreOpts := &rds.RestoreDBClusterToPointInTimeInput{
CopyTagsToSnapshot: aws.Bool(d.Get("copy_tags_to_snapshot").(bool)),
DBClusterIdentifier: aws.String(identifier),
DeletionProtection: aws.Bool(d.Get("deletion_protection").(bool)),
SourceDBClusterIdentifier: aws.String(pointInTimeRestore["source_db_cluster_identifier"].(string)),
UseLatestRestorableTime: aws.Bool(pointInTimeRestore["use_latest_restorable_time"].(bool)),
Tags: tags,
}

if v, ok := pointInTimeRestore["restore_type"].(string); ok && v != "" {
restoreOpts.RestoreType = aws.String(v)
}

if attr, ok := pointInTimeRestore["restore_to_time"].(string); ok && attr != "" {
if v, ok := pointInTimeRestore["use_latest_restorable_time"].(bool); ok && v {
return fmt.Errorf(`"point_in_time_restore.restore_to_time" can't be used when "point_in_time_restore.use_latest_restorable_time" is true`)
}
if v, ok := pointInTimeRestore["restore_type"].(string); ok && v == "copy-on-write" {
return fmt.Errorf(`"point_in_time_restore.restore_to_time" can't be used when "point_in_time_restore.restore_to_time" is "copy-on-write"`)
}

restoreToTime, _ := time.Parse(time.RFC3339, attr)
restoreOpts.RestoreToTime = aws.Time(restoreToTime)
} else if v, ok := pointInTimeRestore["use_latest_restorable_time"].(bool); !ok || !v {
return fmt.Errorf(`"point_in_time_restore.restore_to_time" must be set when "point_in_time_restore.use_latest_restorable_time" is false`)
}

// Need to check value > 0 due to:
// InvalidParameterValue: Backtrack is not enabled for the aurora-postgresql engine.
if v, ok := d.GetOk("backtrack_window"); ok && v.(int) > 0 {
restoreOpts.BacktrackWindow = aws.Int64(int64(v.(int)))
}

if attr, ok := d.GetOk("db_cluster_parameter_group_name"); ok {
restoreOpts.DBClusterParameterGroupName = aws.String(attr.(string))
}

if attr, ok := d.GetOk("db_subnet_group_name"); ok {
restoreOpts.DBSubnetGroupName = aws.String(attr.(string))
}

if attr, ok := d.GetOk("enabled_cloudwatch_logs_exports"); ok && len(attr.([]interface{})) > 0 {
restoreOpts.EnableCloudwatchLogsExports = expandStringList(attr.([]interface{}))
}

if attr, ok := d.GetOk("iam_database_authentication_enabled"); ok {
restoreOpts.EnableIAMDatabaseAuthentication = aws.Bool(attr.(bool))
}

if attr, ok := d.GetOk("kms_key_id"); ok {
restoreOpts.KmsKeyId = aws.String(attr.(string))
}

if attr, ok := d.GetOk("option_group_name"); ok {
restoreOpts.OptionGroupName = aws.String(attr.(string))
}

if attr, ok := d.GetOk("port"); ok {
restoreOpts.Port = aws.Int64(int64(attr.(int)))
}

if attr := d.Get("vpc_security_group_ids").(*schema.Set); attr.Len() > 0 {
restoreOpts.VpcSecurityGroupIds = expandStringList(attr.List())
}

// modification options after restore

if attr, ok := d.GetOk("backup_retention_period"); ok {
modifyDbClusterInput.BackupRetentionPeriod = aws.Int64(int64(attr.(int)))
requiresModifyDbCluster = true
}

if attr, ok := d.GetOk("preferred_backup_window"); ok {
modifyDbClusterInput.PreferredBackupWindow = aws.String(attr.(string))
requiresModifyDbCluster = true
}

if attr, ok := d.GetOk("preferred_maintenance_window"); ok {
modifyDbClusterInput.PreferredMaintenanceWindow = aws.String(attr.(string))
requiresModifyDbCluster = true
}

if attr, ok := d.GetOk("engine_version"); ok {
modifyDbClusterInput.EngineVersion = aws.String(attr.(string))
requiresModifyDbCluster = true
}

if attr, ok := d.GetOk("master_password"); ok {
modifyDbClusterInput.MasterUserPassword = aws.String(attr.(string))
requiresModifyDbCluster = true
}

if scalingConfiguration := expandRdsScalingConfiguration(d.Get("scaling_configuration").([]interface{})); scalingConfiguration != nil {
modifyDbClusterInput.ScalingConfiguration = scalingConfiguration
requiresModifyDbCluster = true
}

log.Printf("[DEBUG] RDS Cluster restore to point in time: %s", restoreOpts)
var resp *rds.RestoreDBClusterToPointInTimeOutput
err := resource.Retry(1*time.Minute, func() *resource.RetryError {
var err error
resp, err = conn.RestoreDBClusterToPointInTime(restoreOpts)
if err != nil {
if isAWSErr(err, "InvalidParameterValue", "IAM role ARN value is invalid or does not include the required permissions") {
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
}
return nil
})
if err != nil {
return fmt.Errorf("error restoring RDS cluster: %s", err)
}

log.Printf("[DEBUG]: RDS Cluster restore point in time response: %s", resp)

} else if _, ok := d.GetOk("replication_source_identifier"); ok {
createOpts := &rds.CreateDBClusterInput{
CopyTagsToSnapshot: aws.Bool(d.Get("copy_tags_to_snapshot").(bool)),
Expand Down
74 changes: 74 additions & 0 deletions aws/resource_aws_rds_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func TestAccAWSRDSCluster_basic(t *testing.T) {
resource.TestCheckResourceAttr(resourceName,
"enabled_cloudwatch_logs_exports.1", "error"),
resource.TestCheckResourceAttr(resourceName, "scaling_configuration.#", "0"),
resource.TestCheckResourceAttr(resourceName, "point_in_time_restore.#", "0"),
),
},
},
Expand Down Expand Up @@ -1447,6 +1448,33 @@ func TestAccAWSRDSCluster_SnapshotIdentifier_EncryptedRestore(t *testing.T) {
})
}

func TestAccAWSRDSCluster_pointInTimeRestore(t *testing.T) {
var srcCluster, dstCluster rds.DBCluster

rInt := acctest.RandInt()
srcClusterName := "aws_rds_cluster.src"
dstClusterName := "aws_rds_cluster.dst"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSRDSClusterConfig_pointInTimeRestore(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSClusterExists(srcClusterName, &srcCluster),
testAccCheckAWSClusterExists(dstClusterName, &dstCluster),
resource.TestCheckResourceAttr(dstClusterName, "point_in_time_restore.#", "1"),
resource.TestCheckResourceAttr(dstClusterName, "point_in_time_restore.0.restore_type", "full-copy"),
resource.TestCheckResourceAttr(dstClusterName, "point_in_time_restore.0.use_latest_restorable_time", "true"),
resource.TestCheckResourceAttr(dstClusterName, "backup_retention_period", "5"),
),
},
},
})
}

func testAccCheckAWSClusterDestroy(s *terraform.State) error {
return testAccCheckAWSClusterDestroyWithProvider(s, testAccProvider)
}
Expand Down Expand Up @@ -2711,3 +2739,49 @@ resource "aws_rds_cluster" "default" {
skip_final_snapshot = true
}`, n, f)
}

func testAccAWSRDSClusterConfig_pointInTimeRestore(n int) string {
return fmt.Sprintf(`
data "aws_availability_zones" "available" {}

resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "pitr-%d"
}
}

resource "aws_subnet" "test" {
count = 2
cidr_block = "${cidrsubnet(aws_vpc.test.cidr_block, 8, count.index)}"
availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
vpc_id = "${aws_vpc.test.id}"
tags = {
Name = "pitr-%d"
}
}

resource "aws_db_subnet_group" "test" {
name = "pitr-%d"
subnet_ids = "${aws_subnet.test.*.id}"
}

resource "aws_rds_cluster" "src" {
cluster_identifier = "pitr-src-%d"
master_username = "root"
master_password = "password"
skip_final_snapshot = true
db_subnet_group_name = "${aws_db_subnet_group.test.name}"
}

resource "aws_rds_cluster" "dst" {
cluster_identifier = "pitr-dest-%d"
skip_final_snapshot = true
backup_retention_period = 5
point_in_time_restore {
source_db_cluster_identifier = "${aws_rds_cluster.src.cluster_identifier}"
use_latest_restorable_time = true
}
}
`, n, n, n, n, n)
}
20 changes: 20 additions & 0 deletions website/docs/r/rds_cluster.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Default: A 30-minute window selected at random from an 8-hour block of time per
* `enabled_cloudwatch_logs_exports` - (Optional) List of log types to export to cloudwatch. If omitted, no logs will be exported.
The following log types are supported: `audit`, `error`, `general`, `slowquery`.
* `scaling_configuration` - (Optional) Nested attribute with scaling properties. Only valid when `engine_mode` is set to `serverless`. More details below.
* `point_in_time_restore` - (Optional) Restore to point in time. More details below.
* `tags` - (Optional) A mapping of tags to assign to the DB cluster.

### S3 Import Options
Expand Down Expand Up @@ -152,6 +153,25 @@ resource "aws_rds_cluster" "db" {

This will not recreate the resource if the S3 object changes in some way. It's only used to initialize the database. This only works currently with the aurora engine. See AWS for currently supported engines and options. See [Aurora S3 Migration Docs](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/AuroraMySQL.Migrating.ExtMySQL.html#AuroraMySQL.Migrating.ExtMySQL.S3).

### Point In Time Restore Options

Full details on the core parameters and impacts are in the API Docs: [RestoreDBClusterToPointInTime](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_RestoreDBClusterToPointInTime.html).


```hcl
resource "aws_rds_cluster" "db" {
point_in_time_restore {
source_db_cluster_identifier = "source-cluster"
use_latest_restorable_time = true
}
}
```

* `source_db_cluster_identifier` - (Required) The identifier of the source DB cluster from which to restore.
* `restore_type` - (Optional) The identifier of the source DB cluster from which to restore. Valid values: `full-copy`, `copy-on-write`. Defaults to: `full-copy`.
* `use_latest_restorable_time` - (Optional) A value that is set to true to restore the DB cluster to the latest restorable backup time, and false otherwise. Defaults to: `false`.
* `restore_to_time` - (Optional) The date and time to restore the DB cluster to, in UTC [RFC3339](https://tools.ietf.org/html/rfc3339#section-5.8) format(for example, YYYY-MM-DDTHH:MM:SSZ).

### scaling_configuration Argument Reference

~> **NOTE:** `scaling_configuration` configuration is only valid when `engine_mode` is set to `serverless`.
Expand Down