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

resource/db_instance: add restore to point in time support #15969

Merged
merged 7 commits into from
Nov 12, 2020
Merged
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
167 changes: 167 additions & 0 deletions aws/resource_aws_db_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ func resourceAwsDbInstance() *schema.Resource {
Optional: true,
},

"latest_restorable_time": {
Type: schema.TypeString,
Computed: true,
},

"license_model": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -274,6 +279,44 @@ func resourceAwsDbInstance() *schema.Resource {
},
},

"restore_to_point_in_time": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
ForceNew: true,
ConflictsWith: []string{
"s3_import",
"snapshot_identifier",
"replicate_source_db",
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"restore_time": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateUTCTimestamp,
ConflictsWith: []string{"restore_to_point_in_time.0.use_latest_restorable_time"},
},

"source_db_instance_identifier": {
anGie44 marked this conversation as resolved.
Show resolved Hide resolved
Type: schema.TypeString,
Optional: true,
},
anGie44 marked this conversation as resolved.
Show resolved Hide resolved

"source_dbi_resource_id": {
Type: schema.TypeString,
Optional: true,
},
anGie44 marked this conversation as resolved.
Show resolved Hide resolved

"use_latest_restorable_time": {
Type: schema.TypeBool,
Optional: true,
ConflictsWith: []string{"restore_to_point_in_time.0.restore_time"},
},
},
},
},

"s3_import": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -1036,6 +1079,95 @@ func resourceAwsDbInstanceCreate(d *schema.ResourceData, meta interface{}) error
if err != nil {
return fmt.Errorf("Error creating DB Instance: %s", err)
}
} else if v, ok := d.GetOk("restore_to_point_in_time"); ok {
if input := expandRestoreToPointInTime(v.([]interface{})); input != nil {
input.AutoMinorVersionUpgrade = aws.Bool(d.Get("auto_minor_version_upgrade").(bool))
input.CopyTagsToSnapshot = aws.Bool(d.Get("copy_tags_to_snapshot").(bool))
input.DBInstanceClass = aws.String(d.Get("instance_class").(string))
input.DeletionProtection = aws.Bool(d.Get("deletion_protection").(bool))
input.PubliclyAccessible = aws.Bool(d.Get("publicly_accessible").(bool))
input.Tags = tags
input.TargetDBInstanceIdentifier = aws.String(d.Get("identifier").(string))

if v, ok := d.GetOk("availability_zone"); ok {
input.AvailabilityZone = aws.String(v.(string))
}

if v, ok := d.GetOk("domain"); ok {
input.Domain = aws.String(v.(string))
}

if v, ok := d.GetOk("domain_iam_role_name"); ok {
input.DomainIAMRoleName = aws.String(v.(string))
}

if v, ok := d.GetOk("enabled_cloudwatch_logs_exports"); ok && v.(*schema.Set).Len() > 0 {
input.EnableCloudwatchLogsExports = expandStringSet(v.(*schema.Set))
}

if v, ok := d.GetOk("engine"); ok {
input.Engine = aws.String(v.(string))
}

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

if v, ok := d.GetOk("iops"); ok {
input.Iops = aws.Int64(int64(v.(int)))
}

if v, ok := d.GetOk("license_model"); ok {
input.LicenseModel = aws.String(v.(string))
}

if v, ok := d.GetOk("max_allocated_storage"); ok {
input.MaxAllocatedStorage = aws.Int64(int64(v.(int)))
}

if v, ok := d.GetOk("multi_az"); ok {
input.MultiAZ = aws.Bool(v.(bool))
}

if v, ok := d.GetOk("name"); ok {
input.DBName = aws.String(v.(string))
}

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

if v, ok := d.GetOk("parameter_group_name"); ok {
input.DBParameterGroupName = aws.String(v.(string))
}

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

if v, ok := d.GetOk("storage_type"); ok {
input.StorageType = aws.String(v.(string))
}

if v, ok := d.GetOk("subnet_group_name"); ok {
input.DBSubnetGroupName = aws.String(v.(string))
}

if v, ok := d.GetOk("tde_credential_arn"); ok {
input.TdeCredentialArn = aws.String(v.(string))
}

if v, ok := d.GetOk("vpc_security_group_ids"); ok && v.(*schema.Set).Len() > 0 {
input.VpcSecurityGroupIds = expandStringSet(v.(*schema.Set))
}

log.Printf("[DEBUG] DB Instance restore to point in time configuration: %s", input)

_, err := conn.RestoreDBInstanceToPointInTime(input)
if err != nil {
return fmt.Errorf("error creating DB Instance: %w", err)
}
}
} else {
if _, ok := d.GetOk("allocated_storage"); !ok {
return fmt.Errorf(`provider.aws: aws_db_instance: %s: "allocated_storage": required field is not set`, d.Get("name").(string))
Expand Down Expand Up @@ -1287,6 +1419,7 @@ func resourceAwsDbInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("availability_zone", v.AvailabilityZone)
d.Set("backup_retention_period", v.BackupRetentionPeriod)
d.Set("backup_window", v.PreferredBackupWindow)
d.Set("latest_restorable_time", aws.TimeValue(v.LatestRestorableTime).Format(time.RFC3339))
d.Set("license_model", v.LicenseModel)
d.Set("maintenance_window", v.PreferredMaintenanceWindow)
d.Set("max_allocated_storage", v.MaxAllocatedStorage)
Expand Down Expand Up @@ -1821,3 +1954,37 @@ var resourceAwsDbInstanceUpdatePendingStates = []string{
"storage-full",
"upgrading",
}

func expandRestoreToPointInTime(l []interface{}) *rds.RestoreDBInstanceToPointInTimeInput {
if len(l) == 0 || l[0] == nil {
return nil
}

tfMap, ok := l[0].(map[string]interface{})
anGie44 marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return nil
}

input := &rds.RestoreDBInstanceToPointInTimeInput{}
anGie44 marked this conversation as resolved.
Show resolved Hide resolved

if v, ok := tfMap["restore_time"].(string); ok && v != "" {
parsedTime, err := time.Parse(time.RFC3339, v)
if err == nil {
input.RestoreTime = aws.Time(parsedTime)
}
}

if v, ok := tfMap["source_db_instance_identifier"].(string); ok && v != "" {
anGie44 marked this conversation as resolved.
Show resolved Hide resolved
input.SourceDBInstanceIdentifier = aws.String(v)
}

if v, ok := tfMap["source_dbi_resource_id"].(string); ok && v != "" {
input.SourceDbiResourceId = aws.String(v)
}

if v, ok := tfMap["use_latest_restorable_time"].(bool); ok && v {
input.UseLatestRestorableTime = aws.Bool(v)
}

return input
}
127 changes: 127 additions & 0 deletions aws/resource_aws_db_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2984,6 +2984,76 @@ func TestAccAWSDBInstance_CACertificateIdentifier(t *testing.T) {
})
}

func TestAccAWSDBInstance_RestoreToPointInTime_SourceIdentifier(t *testing.T) {
var dbInstance, sourceDbInstance rds.DBInstance
sourceName := "aws_db_instance.test"
resourceName := "aws_db_instance.restore"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSDBInstanceConfig_RestoreToPointInTime_SourceIdentifier(),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists(sourceName, &sourceDbInstance),
testAccCheckAWSDBInstanceExists(resourceName, &dbInstance),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"apply_immediately",
"delete_automated_backups",
"final_snapshot_identifier",
"latest_restorable_time", // dynamic value of a DBInstance
"password",
"restore_to_point_in_time",
"skip_final_snapshot",
},
},
},
})
}

func TestAccAWSDBInstance_RestoreToPointInTime_SourceResourceID(t *testing.T) {
var dbInstance, sourceDbInstance rds.DBInstance
sourceName := "aws_db_instance.test"
resourceName := "aws_db_instance.restore"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSDBInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSDBInstanceConfig_RestoreToPointInTime_SourceResourceID(),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSDBInstanceExists(sourceName, &sourceDbInstance),
testAccCheckAWSDBInstanceExists(resourceName, &dbInstance),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"apply_immediately",
"delete_automated_backups",
"final_snapshot_identifier",
"latest_restorable_time", // dynamic value of a DBInstance
"password",
"restore_to_point_in_time",
"skip_final_snapshot",
},
},
},
})
}

func testAccAWSDBInstanceConfig_orderableClass(engine, version, license string) string {
return fmt.Sprintf(`
data "aws_rds_orderable_db_instance" "test" {
Expand Down Expand Up @@ -3546,6 +3616,63 @@ resource "aws_db_instance" "test" {
`, rName)
}

const testAccAWSDBInstanceBaseConfig = `
data "aws_rds_engine_version" "test" {
engine = "mysql"
}

data "aws_rds_orderable_db_instance" "test" {
engine = data.aws_rds_engine_version.test.engine
engine_version = data.aws_rds_engine_version.test.version
preferred_instance_classes = ["db.t3.micro", "db.t2.micro", "db.t3.small"]
}

resource "aws_db_instance" "test" {
allocated_storage = 10
backup_retention_period = 1
engine = data.aws_rds_orderable_db_instance.test.engine
engine_version = data.aws_rds_orderable_db_instance.test.engine_version
instance_class = data.aws_rds_orderable_db_instance.test.instance_class
name = "baz"
parameter_group_name = "default.${data.aws_rds_engine_version.test.parameter_group_family}"
password = "barbarbarbar"
skip_final_snapshot = true
username = "foo"
}
`

func testAccAWSDBInstanceConfig_RestoreToPointInTime_SourceIdentifier() string {
return composeConfig(
testAccAWSDBInstanceBaseConfig,
fmt.Sprintf(`
resource "aws_db_instance" "restore" {
identifier = "tf-acc-test-point-in-time-restore-%d"
instance_class = aws_db_instance.test.instance_class
restore_to_point_in_time {
source_db_instance_identifier = aws_db_instance.test.identifier
use_latest_restorable_time = true
}
skip_final_snapshot = true
}
`, acctest.RandInt()))
}

func testAccAWSDBInstanceConfig_RestoreToPointInTime_SourceResourceID() string {
return composeConfig(
testAccAWSDBInstanceBaseConfig,
fmt.Sprintf(`
resource "aws_db_instance" "restore" {
identifier = "tf-acc-test-point-in-time-restore-%d"
instance_class = aws_db_instance.test.instance_class
restore_to_point_in_time {
source_dbi_resource_id = aws_db_instance.test.resource_id
use_latest_restorable_time = true
}
skip_final_snapshot = true
}
`, acctest.RandInt()))
}

func testAccAWSDBInstanceConfig_SnapshotInstanceConfig_iopsUpdate(rName string, iops int) string {
return fmt.Sprintf(`
data "aws_rds_engine_version" "default" {
Expand Down
4 changes: 2 additions & 2 deletions aws/resource_aws_iot_topic_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func resourceAwsIotTopicRule() *schema.Resource {
"metric_timestamp": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateIoTTopicRuleCloudWatchMetricTimestamp,
ValidateFunc: validateUTCTimestamp,
},
"metric_unit": {
Type: schema.TypeString,
Expand Down Expand Up @@ -489,7 +489,7 @@ func resourceAwsIotTopicRule() *schema.Resource {
"metric_timestamp": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateIoTTopicRuleCloudWatchMetricTimestamp,
ValidateFunc: validateUTCTimestamp,
},
"metric_unit": {
Type: schema.TypeString,
Expand Down
13 changes: 3 additions & 10 deletions aws/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,9 @@ func validateOnceADayWindowFormat(v interface{}, k string) (ws []string, errors
return
}

// validateUTCTimestamp validates a string in UTC Format required by APIs including:
// https://docs.aws.amazon.com/iot/latest/apireference/API_CloudwatchMetricAction.html
// https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_RestoreDBInstanceToPointInTime.html
func validateUTCTimestamp(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
_, err := time.Parse(time.RFC3339, value)
Expand Down Expand Up @@ -1965,16 +1968,6 @@ func validateIoTTopicRuleCloudWatchAlarmStateValue(v interface{}, s string) ([]s
return nil, []error{fmt.Errorf("State must be one of OK, ALARM, or INSUFFICIENT_DATA")}
}

func validateIoTTopicRuleCloudWatchMetricTimestamp(v interface{}, s string) ([]string, []error) {
dateString := v.(string)

// https://docs.aws.amazon.com/iot/latest/apireference/API_CloudwatchMetricAction.html
if _, err := time.Parse(time.RFC3339, dateString); err != nil {
return nil, []error{err}
}
return nil, nil
}

func validateIoTTopicRuleElasticSearchEndpoint(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)

Expand Down
Loading