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

New resource: aws_dlm_lifecycle_policy #5558

Merged
merged 1 commit into from
Nov 6, 2018
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
3 changes: 3 additions & 0 deletions aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/aws/aws-sdk-go/service/devicefarm"
"github.com/aws/aws-sdk-go/service/directconnect"
"github.com/aws/aws-sdk-go/service/directoryservice"
"github.com/aws/aws-sdk-go/service/dlm"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ecr"
Expand Down Expand Up @@ -171,6 +172,7 @@ type AWSClient struct {
configconn *configservice.ConfigService
daxconn *dax.DAX
devicefarmconn *devicefarm.DeviceFarm
dlmconn *dlm.DLM
dmsconn *databasemigrationservice.DatabaseMigrationService
dsconn *directoryservice.DirectoryService
dynamodbconn *dynamodb.DynamoDB
Expand Down Expand Up @@ -514,6 +516,7 @@ func (c *Config) Client() (interface{}, error) {
client.cognitoidpconn = cognitoidentityprovider.New(sess)
client.codepipelineconn = codepipeline.New(sess)
client.daxconn = dax.New(awsDynamoSess)
client.dlmconn = dlm.New(sess)
client.dmsconn = databasemigrationservice.New(sess)
client.dsconn = directoryservice.New(sess)
client.dynamodbconn = dynamodb.New(awsDynamoSess)
Expand Down
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ func Provider() terraform.ResourceProvider {
"aws_devicefarm_project": resourceAwsDevicefarmProject(),
"aws_directory_service_directory": resourceAwsDirectoryServiceDirectory(),
"aws_directory_service_conditional_forwarder": resourceAwsDirectoryServiceConditionalForwarder(),
"aws_dlm_lifecycle_policy": resourceAwsDlmLifecyclePolicy(),
"aws_dms_certificate": resourceAwsDmsCertificate(),
"aws_dms_endpoint": resourceAwsDmsEndpoint(),
"aws_dms_replication_instance": resourceAwsDmsReplicationInstance(),
Expand Down
344 changes: 344 additions & 0 deletions aws/resource_aws_dlm_lifecycle_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
package aws

import (
"fmt"
"log"
"regexp"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/dlm"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
)

func resourceAwsDlmLifecyclePolicy() *schema.Resource {
return &schema.Resource{
Create: resourceAwsDlmLifecyclePolicyCreate,
Read: resourceAwsDlmLifecyclePolicyRead,
Update: resourceAwsDlmLifecyclePolicyUpdate,
Delete: resourceAwsDlmLifecyclePolicyDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"description": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringMatch(regexp.MustCompile("^[0-9A-Za-z _-]+$"), "see https://docs.aws.amazon.com/cli/latest/reference/dlm/create-lifecycle-policy.html"),
bflad marked this conversation as resolved.
Show resolved Hide resolved
// TODO: https://docs.aws.amazon.com/dlm/latest/APIReference/API_LifecyclePolicy.html#dlm-Type-LifecyclePolicy-Description says it has max length of 500 but doesn't mention the regex but SDK and CLI docs only mention the regex and not max length. Check this
},
"execution_role_arn": {
// TODO: Make this not required and if it's not provided then use the default service role, creating it if necessary
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this want doing in first pass or should it be done later? I'm tempted to do the work for this if AWS take a long time fixing the API response which is blocking this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for starters that we can omit the handling in the code (so people can try this out) and document the solution on the website.

Type: schema.TypeString,
Required: true,
ValidateFunc: validateArn,
},
"policy_details": {
Type: schema.TypeList,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"resource_types": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"schedule": {
Type: schema.TypeList,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"create_rule": {
Type: schema.TypeList,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"interval": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validateIntegerInSlice([]int{
12,
24,
}),
},
"interval_unit": {
Type: schema.TypeString,
Optional: true,
Default: dlm.IntervalUnitValuesHours,
ValidateFunc: validation.StringInSlice([]string{
dlm.IntervalUnitValuesHours,
}, false),
},
"times": {
Type: schema.TypeList,
Optional: true,
Computed: true,
MaxItems: 1,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringMatch(regexp.MustCompile("^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$"), "see https://docs.aws.amazon.com/dlm/latest/APIReference/API_CreateRule.html#dlm-Type-CreateRule-Times"),
},
},
},
},
},
"name": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringLenBetween(0, 500),
},
"retain_rule": {
Type: schema.TypeList,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"count": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntBetween(1, 1000),
},
},
},
},
"tags_to_add": {
Type: schema.TypeMap,
Optional: true,
},
},
},
},
"target_tags": {
Type: schema.TypeMap,
Required: true,
},
},
},
},
"state": {
Type: schema.TypeString,
Optional: true,
Default: dlm.SettablePolicyStateValuesEnabled,
ValidateFunc: validation.StringInSlice([]string{
dlm.SettablePolicyStateValuesDisabled,
dlm.SettablePolicyStateValuesEnabled,
}, false),
},
},
}
}

func resourceAwsDlmLifecyclePolicyCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).dlmconn

input := dlm.CreateLifecyclePolicyInput{
Description: aws.String(d.Get("description").(string)),
ExecutionRoleArn: aws.String(d.Get("execution_role_arn").(string)),
PolicyDetails: expandDlmPolicyDetails(d.Get("policy_details").([]interface{})),
State: aws.String(d.Get("state").(string)),
}

log.Printf("[INFO] Creating DLM lifecycle policy: %s", input)
out, err := conn.CreateLifecyclePolicy(&input)
if err != nil {
return err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We should return context about errors so operators and code maintainers can more easily troubleshoot issues, e.g.

return fmt.Errorf("error creating DLM Lifecycle Policy: %s", err)

Same with other similar messaging during Read/Update/Delete. 👍

}

d.SetId(*out.PolicyId)

return resourceAwsDlmLifecyclePolicyRead(d, meta)
}

func resourceAwsDlmLifecyclePolicyRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).dlmconn

log.Printf("[INFO] Reading DLM lifecycle policy: %s", d.Id())
out, err := conn.GetLifecyclePolicy(&dlm.GetLifecyclePolicyInput{
PolicyId: aws.String(d.Id()),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" && !d.IsNewResource() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things here:

  • We have a helper for handling this type of logic, e.g. isAWSErr(err, dlm.ErrCodeResourceNotFoundException, "")
  • We should not create a special case during Read after Create that returns an error -- instead if this resource has eventual consistency issues, we should implement a resource.Retry() loop that retries for a minute or two (then can use d.IsNewResource() to determine if its a retryable error).

d.SetId("")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: When triggering resource recreation, we should return a warning message in the logs so operators and code maintainers can more easily diagnose why it occurred, e.g.

log.Printf("[WARN] DLM Lifecycle Policy (%s) not found, removing from state", d.Id())
d.SetId("")

return nil
}
return err
}

d.Set("description", out.Policy.Description)
d.Set("execution_role_arn", out.Policy.ExecutionRoleArn)
d.Set("state", out.Policy.State)
if err := d.Set("policy_details", flattenDlmPolicyDetails(out.Policy.PolicyDetails)); err != nil {
return fmt.Errorf("error setting policy details %s", err)
}

return nil
}

func resourceAwsDlmLifecyclePolicyUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).dlmconn

input := dlm.UpdateLifecyclePolicyInput{
PolicyId: aws.String(d.Id()),
}

if d.HasChange("description") {
input.Description = aws.String(d.Get("description").(string))
}
if d.HasChange("execution_role_arn") {
input.ExecutionRoleArn = aws.String(d.Get("execution_role_arn").(string))
}
if d.HasChange("state") {
input.State = aws.String(d.Get("state").(string))
}
if d.HasChange("policy_details") {
input.PolicyDetails = expandDlmPolicyDetails(d.Get("policy_details").([]interface{}))
}

log.Printf("[INFO] Updating lifecycle policy %s", d.Id())
_, err := conn.UpdateLifecyclePolicy(&input)
if err != nil {
return err
}

return resourceAwsDlmLifecyclePolicyRead(d, meta)
}

func resourceAwsDlmLifecyclePolicyDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).dlmconn

log.Printf("[INFO] Deleting DLM lifecycle policy: %s", d.Id())
_, err := conn.DeleteLifecyclePolicy(&dlm.DeleteLifecyclePolicyInput{
PolicyId: aws.String(d.Id()),
})
if err != nil {
return err
}

return nil
}

func expandDlmPolicyDetails(cfg []interface{}) *dlm.PolicyDetails {
policyDetails := &dlm.PolicyDetails{}
m := cfg[0].(map[string]interface{})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent potential panics, we should length and nil check the slice before accessing its first element, e.g.

if len(cfg) == 0 || cfg[0] == nil {
  return nil
}
m := cfg[0].(map[string]interface{})

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack yeah, I keep missing these. Is there a linting rule or something we can use to check for this?

if v, ok := m["resource_types"]; ok {
policyDetails.ResourceTypes = expandStringList(v.([]interface{}))
}
if v, ok := m["schedule"]; ok {
policyDetails.Schedules = expandDlmSchedules(v.([]interface{}))
}
if v, ok := m["target_tags"]; ok {
policyDetails.TargetTags = expandDlmTags(v.(map[string]interface{}))
}

return policyDetails
}

func flattenDlmPolicyDetails(policyDetails *dlm.PolicyDetails) []map[string]interface{} {
result := make(map[string]interface{}, 0)
result["resource_types"] = flattenStringList(policyDetails.ResourceTypes)
result["schedule"] = flattenDlmSchedules(policyDetails.Schedules)
result["target_tags"] = flattenDlmTags(policyDetails.TargetTags)

return []map[string]interface{}{result}
}

func expandDlmSchedules(cfg []interface{}) []*dlm.Schedule {
schedules := make([]*dlm.Schedule, len(cfg))
for i, c := range cfg {
schedule := &dlm.Schedule{}
m := c.(map[string]interface{})
if v, ok := m["create_rule"]; ok {
schedule.CreateRule = expandDlmCreateRule(v.([]interface{}))
}
if v, ok := m["name"]; ok {
schedule.Name = aws.String(v.(string))
}
if v, ok := m["retain_rule"]; ok {
schedule.RetainRule = expandDlmRetainRule(v.([]interface{}))
}
if v, ok := m["tags_to_add"]; ok {
schedule.TagsToAdd = expandDlmTags(v.(map[string]interface{}))
}
schedules[i] = schedule
}

return schedules
}

func flattenDlmSchedules(schedules []*dlm.Schedule) []map[string]interface{} {
result := make([]map[string]interface{}, len(schedules))
for i, s := range schedules {
m := make(map[string]interface{})
m["create_rule"] = flattenDlmCreateRule(s.CreateRule)
m["name"] = aws.StringValue(s.Name)
m["retain_rule"] = flattenDlmRetainRule(s.RetainRule)
m["tags_to_add"] = flattenDlmTags(s.TagsToAdd)
result[i] = m
}

return result
}

func expandDlmCreateRule(cfg []interface{}) *dlm.CreateRule {
c := cfg[0].(map[string]interface{})
createRule := &dlm.CreateRule{
Interval: aws.Int64(int64(c["interval"].(int))),
IntervalUnit: aws.String(c["interval_unit"].(string)),
}
if v, ok := c["times"]; ok {
createRule.Times = expandStringList(v.([]interface{}))
}

return createRule
}

func flattenDlmCreateRule(createRule *dlm.CreateRule) []map[string]interface{} {
if createRule == nil {
return []map[string]interface{}{}
}

result := make(map[string]interface{})
result["interval"] = aws.Int64Value(createRule.Interval)
result["interval_unit"] = aws.StringValue(createRule.IntervalUnit)
result["times"] = flattenStringList(createRule.Times)

return []map[string]interface{}{result}
}

func expandDlmRetainRule(cfg []interface{}) *dlm.RetainRule {
return &dlm.RetainRule{
Count: aws.Int64(int64(cfg[0].(map[string]interface{})["count"].(int))),
}
}

func flattenDlmRetainRule(retainRule *dlm.RetainRule) []map[string]interface{} {
result := make(map[string]interface{})
result["count"] = aws.Int64Value(retainRule.Count)

return []map[string]interface{}{result}
}

func expandDlmTags(m map[string]interface{}) []*dlm.Tag {
var result []*dlm.Tag
for k, v := range m {
result = append(result, &dlm.Tag{
Key: aws.String(k),
Value: aws.String(v.(string)),
})
}

return result
}

func flattenDlmTags(tags []*dlm.Tag) map[string]string {
result := make(map[string]string)
for _, t := range tags {
result[*t.Key] = *t.Value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent potential panics, we should prefer to use the SDK helpers in this case, e.g.

result[aws.StringValue(t.Key)] = aws.StringValue(t.Value)

}

return result
}
Loading