From 3eb13601cedd7c620fe214b83f9077b3f919a7e6 Mon Sep 17 00:00:00 2001 From: Tom Elliff Date: Mon, 5 Nov 2018 15:53:08 +0000 Subject: [PATCH] Add DLM Lifecycle Policy resource Fixes #5176 --- aws/config.go | 3 + aws/provider.go | 1 + aws/resource_aws_dlm_lifecycle_policy.go | 344 ++++++++++++++++++ aws/resource_aws_dlm_lifecycle_policy_test.go | 292 +++++++++++++++ website/docs/r/dlm_lifecycle_policy.markdown | 113 ++++++ 5 files changed, 753 insertions(+) create mode 100644 aws/resource_aws_dlm_lifecycle_policy.go create mode 100644 aws/resource_aws_dlm_lifecycle_policy_test.go create mode 100644 website/docs/r/dlm_lifecycle_policy.markdown diff --git a/aws/config.go b/aws/config.go index 7e7f4e53cb1..d55d25e9619 100644 --- a/aws/config.go +++ b/aws/config.go @@ -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" @@ -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 @@ -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) diff --git a/aws/provider.go b/aws/provider.go index 36d77a71951..ea06e64ab6c 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -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(), diff --git a/aws/resource_aws_dlm_lifecycle_policy.go b/aws/resource_aws_dlm_lifecycle_policy.go new file mode 100644 index 00000000000..d0722dcb920 --- /dev/null +++ b/aws/resource_aws_dlm_lifecycle_policy.go @@ -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"), + // 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 + 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 + } + + 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() { + 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{}) + 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 + } + + return result +} diff --git a/aws/resource_aws_dlm_lifecycle_policy_test.go b/aws/resource_aws_dlm_lifecycle_policy_test.go new file mode 100644 index 00000000000..1693cfb6321 --- /dev/null +++ b/aws/resource_aws_dlm_lifecycle_policy_test.go @@ -0,0 +1,292 @@ +package aws + +import ( + "fmt" + "testing" + + "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/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSDlmLifecyclePolicyBasic(t *testing.T) { + resourceName := "aws_dlm_lifecycle_policy.basic" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: dlmLifecyclePolicyDestroy, + Steps: []resource.TestStep{ + { + Config: dlmLifecyclePolicyBasicConfig(), + Check: resource.ComposeTestCheckFunc( + checkDlmLifecyclePolicyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", "tf-acc-basic"), + resource.TestCheckResourceAttrSet(resourceName, "execution_role_arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.resource_types.0", "VOLUME"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.name", "tf-acc-basic"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.create_rule.0.interval", "12"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.create_rule.0.interval_unit", "HOURS"), + resource.TestCheckResourceAttrSet(resourceName, "policy_details.0.schedule.0.create_rule.0.times.0"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.retain_rule.0.count", "10"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.target_tags.tf-acc-test", "basic"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSDlmLifecyclePolicyFull(t *testing.T) { + resourceName := "aws_dlm_lifecycle_policy.full" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: dlmLifecyclePolicyDestroy, + Steps: []resource.TestStep{ + { + Config: dlmLifecyclePolicyFullConfig(), + Check: resource.ComposeTestCheckFunc( + checkDlmLifecyclePolicyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", "tf-acc-full"), + resource.TestCheckResourceAttrSet(resourceName, "execution_role_arn"), + resource.TestCheckResourceAttr(resourceName, "state", "ENABLED"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.resource_types.0", "VOLUME"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.name", "tf-acc-full"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.create_rule.0.interval", "12"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.create_rule.0.interval_unit", "HOURS"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.create_rule.0.times.0", "21:42"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.retain_rule.0.count", "10"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.tags_to_add.tf-acc-test-added", "full"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.target_tags.tf-acc-test", "full"), + ), + }, + { + Config: dlmLifecyclePolicyFullUpdateConfig(), + Check: resource.ComposeTestCheckFunc( + checkDlmLifecyclePolicyExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "description", "tf-acc-full-updated"), + resource.TestCheckResourceAttrSet(resourceName, "execution_role_arn"), + resource.TestCheckResourceAttr(resourceName, "state", "DISABLED"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.resource_types.0", "VOLUME"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.name", "tf-acc-full-updated"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.create_rule.0.interval", "24"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.create_rule.0.interval_unit", "HOURS"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.create_rule.0.times.0", "09:42"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.retain_rule.0.count", "100"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.schedule.0.tags_to_add.tf-acc-test-added", "full-updated"), + resource.TestCheckResourceAttr(resourceName, "policy_details.0.target_tags.tf-acc-test", "full-updated"), + ), + }, + }, + }) +} + +func dlmLifecyclePolicyDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).dlmconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_dlm_lifecycle_policy" { + continue + } + + input := dlm.GetLifecyclePolicyInput{ + PolicyId: aws.String(rs.Primary.Attributes["name"]), + } + + out, err := conn.GetLifecyclePolicy(&input) + + if err != nil { + if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" { + return nil + } + return err + } + + if out.Policy != nil { + return fmt.Errorf("DLM lifecycle policy still exists: %#v", out) + } + } + + return nil +} + +func checkDlmLifecyclePolicyExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + return nil + } +} + +func dlmLifecyclePolicyBasicConfig() string { + return fmt.Sprint(` +resource "aws_iam_role" "dlm_lifecycle_role" { + name = "tf-acc-basic-dlm-lifecycle-role" + + assume_role_policy = < Note: You cannot have overlapping lifecycle policies that share the same `target_tags`. Terraform is unable to detect this at plan time but it will fail during apply. + +#### Schedule arguments + +* `create_rule` - (Required) See the [`create_rule`](#create-rule-arguments) block. Max of 1 per schedule. +* `name` - (Required) A name for the schedule. +* `retain_rule` - (Required) See the [`create_rule`](#create-rule-arguments) block. Max of 1 per schedule. +* `tags_to_add` - (Optional) A mapping of tag keys and their values. DLM lifecycle policies will already tag the snapshot with the tags on the volume. This configuration adds extra tags on top of these. + +#### Create Rule arguments + +* `interval` - (Required) How often this lifecycle policy should be evaluated. `12` or `24` are valid values. +* `interval_unit` - (Optional) The unit for how often the lifecycle policy should be evaluated. `HOURS` is currently the only allowed value and also the default value. +* `times` - (Optional) A list of times in 24 hour clock format that sets when the lifecycle policy should be evaluated. Max of 1. + +#### Retain Rule arguments + +* `count` - (Required) How many snapshots to keep. Must be an integer between 1 and 1000. + +## Attributes Reference + +All of the arguments above are exported as attributes. + +## Import + +DLM lifecyle policies can be imported by their policy ID: + +``` +$ terraform import aws_dlm_lifecycle_policy.example policy-abcdef12345678901 +``` \ No newline at end of file