Skip to content

Commit

Permalink
add point in time restore argument and refactor timestamp validator
Browse files Browse the repository at this point in the history
  • Loading branch information
anGie44 committed Nov 2, 2020
1 parent 9f4423f commit 291851c
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 13 deletions.
141 changes: 141 additions & 0 deletions aws/resource_aws_db_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,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 @@ -273,6 +278,42 @@ 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": {
Type: schema.TypeString,
Optional: true,
},
"source_dbi_resource_id": {
Type: schema.TypeString,
Optional: true,
},
"use_latest_restorable_time": {
Type: schema.TypeBool,
Optional: true,
Default: false,
ConflictsWith: []string{"restore_to_point_in_time.0.restore_time"},
},
},
},
},

"s3_import": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -1035,6 +1076,77 @@ 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 @@ -1286,6 +1398,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", 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 @@ -1816,3 +1929,31 @@ 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{})
if !ok {
return nil
}
input := &rds.RestoreDBInstanceToPointInTimeInput{}
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 != "" {
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 {
input.UseLatestRestorableTime = aws.Bool(v)
}

return input
}
119 changes: 119 additions & 0 deletions aws/resource_aws_db_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2990,6 +2990,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 @@ -3552,6 +3622,55 @@ resource "aws_db_instance" "test" {
`, rName)
}

const testAccAWSDBInstanceBaseConfig = `
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.mysql5.6"
password = "barbarbarbar"
skip_final_snapshot = true
username = "foo"
}
`

func testAccAWSDBInstanceConfig_RestoreToPointInTime_SourceIdentifier() string {
return composeConfig(
testAccAWSDBInstanceConfig_orderableClassMysql(),
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(
testAccAWSDBInstanceConfig_orderableClassMysql(),
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
14 changes: 8 additions & 6 deletions aws/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -1956,14 +1956,16 @@ 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) {
// 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) {
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}
_, err := time.Parse(time.RFC3339, dateString)
if err != nil {
errors = append(errors, fmt.Errorf("%q cannot be parsed in UTC format e.g. %q", k, time.RFC3339))
}
return nil, nil
return
}

func validateIoTTopicRuleElasticSearchEndpoint(v interface{}, k string) (ws []string, errors []error) {
Expand Down
26 changes: 26 additions & 0 deletions aws/validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3220,3 +3220,29 @@ func TestValidateServiceDiscoveryNamespaceName(t *testing.T) {
}
}
}

func TestValidateUTCTimestamp(t *testing.T) {
validT := []string{
"2006-01-02T15:04:05Z",
}

invalidT := []string{
"2015-03-07 23:45:00",
"27-03-2019 23:45:00",
"Mon, 02 Jan 2006 15:04:05 -0700",
}

for _, f := range validT {
_, errors := validateUTCTimestamp(f, "UTC timestamp")
if len(errors) > 0 {
t.Fatalf("Expected the time %q to be in valid format, got error %q", f, errors)
}
}

for _, f := range invalidT {
_, errors := validateUTCTimestamp(f, "invalid UTC timestamp")
if len(errors) == 0 {
t.Fatalf("Expected the time %q to fail validation", f)
}
}
}
Loading

0 comments on commit 291851c

Please sign in to comment.