From 80ea2ae6d2d48dfa9419cbb8974c5233a0dce0a1 Mon Sep 17 00:00:00 2001 From: Jocelyn Giroux Date: Mon, 28 Aug 2017 15:06:24 -0400 Subject: [PATCH] Add support for description, allowed_pattern and tags for ssm parameters Allow detection of key_id change Fixing problems with ssm-parameter. The resource should not return an error if the parameter no longer exist. If a user manually delete a parameter, Terraform is no longer able to recreate it or destroy it. The defautt value for overwrite should be true. Otherwise, this is causing a breaking change with previously written code where overwrite is not set and the behaviour was to overwrite the parameter by default. Moreover, terraform already provides a mechanism to handle the lifecycle of the values. --- aws/resource_aws_ssm_parameter.go | 99 +++++++++++------ aws/resource_aws_ssm_parameter_test.go | 9 +- aws/tagsSSM.go | 117 +++++++++++++++++++++ aws/tagsSSM_test.go | 105 ++++++++++++++++++ website/docs/r/ssm_parameter.html.markdown | 11 +- 5 files changed, 305 insertions(+), 36 deletions(-) create mode 100644 aws/tagsSSM.go create mode 100644 aws/tagsSSM_test.go diff --git a/aws/resource_aws_ssm_parameter.go b/aws/resource_aws_ssm_parameter.go index 72669bfc72c..5c1e5bf9534 100644 --- a/aws/resource_aws_ssm_parameter.go +++ b/aws/resource_aws_ssm_parameter.go @@ -23,6 +23,11 @@ func resourceAwsSsmParameter() *schema.Resource { Required: true, ForceNew: true, }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, "type": { Type: schema.TypeString, Required: true, @@ -42,84 +47,114 @@ func resourceAwsSsmParameter() *schema.Resource { "overwrite": { Type: schema.TypeBool, Optional: true, - Default: false, + // The default should be set to true, terraform lifecycle should take care of not overriding the value if it is manually set by the user. + // Otherwise, it is causing a breaking change because the first version did not allow overwrite parameter and overwrite was allowed. + Default: true, + }, + "allowed_pattern": { + Type: schema.TypeString, + Optional: true, + Default: "", }, + "tags": tagsSchema(), }, } } func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error { - ssmconn := meta.(*AWSClient).ssmconn + conn := meta.(*AWSClient).ssmconn log.Printf("[DEBUG] Reading SSM Parameter: %s", d.Id()) - paramInput := &ssm.GetParametersInput{ - Names: []*string{ - aws.String(d.Get("name").(string)), - }, + if resp, err := conn.GetParameters(&ssm.GetParametersInput{ + Names: []*string{aws.String(d.Get("name").(string))}, WithDecryption: aws.Bool(true), + }); err != nil { + return errwrap.Wrapf("[ERROR] Error getting SSM parameter: {{err}}", err) + } else { + if len(resp.InvalidParameters) > 0 { + log.Print("[INFO] The resource no longer exists, marking it for recreation:", d.Id()) + d.SetId("") + return nil + } + param := resp.Parameters[0] + d.Set("name", param.Name) + d.Set("type", param.Type) + d.Set("value", param.Value) } - resp, err := ssmconn.GetParameters(paramInput) - - if err != nil { + if resp, err := conn.DescribeParameters(&ssm.DescribeParametersInput{ + Filters: []*ssm.ParametersFilter{ + &ssm.ParametersFilter{ + Key: aws.String("Name"), + Values: []*string{aws.String(d.Get("name").(string))}, + }, + }, + }); err != nil { return errwrap.Wrapf("[ERROR] Error describing SSM parameter: {{err}}", err) + } else { + param := resp.Parameters[0] + d.Set("key_id", param.KeyId) + d.Set("description", param.Description) + d.Set("allowed_pattern", param.AllowedPattern) } - if len(resp.InvalidParameters) > 0 { - return fmt.Errorf("[ERROR] SSM Parameter %s is invalid", d.Id()) + if tagList, err := conn.ListTagsForResource(&ssm.ListTagsForResourceInput{ + ResourceId: aws.String(d.Get("name").(string)), + ResourceType: aws.String("Parameter"), + }); err != nil { + return fmt.Errorf("Failed to get SSM parameter tags for %s: %s", d.Get("name"), err) + } else { + d.Set("tags", tagsToMapSSM(tagList.TagList)) } - param := resp.Parameters[0] - d.Set("name", param.Name) - d.Set("type", param.Type) - d.Set("value", param.Value) - return nil } func resourceAwsSsmParameterDelete(d *schema.ResourceData, meta interface{}) error { - ssmconn := meta.(*AWSClient).ssmconn + conn := meta.(*AWSClient).ssmconn log.Printf("[INFO] Deleting SSM Parameter: %s", d.Id()) - paramInput := &ssm.DeleteParameterInput{ + _, err := conn.DeleteParameter(&ssm.DeleteParameterInput{ Name: aws.String(d.Get("name").(string)), - } - - _, err := ssmconn.DeleteParameter(paramInput) + }) if err != nil { return err } - d.SetId("") return nil } func resourceAwsSsmParameterPut(d *schema.ResourceData, meta interface{}) error { - ssmconn := meta.(*AWSClient).ssmconn + conn := meta.(*AWSClient).ssmconn log.Printf("[INFO] Creating SSM Parameter: %s", d.Get("name").(string)) paramInput := &ssm.PutParameterInput{ - Name: aws.String(d.Get("name").(string)), - Type: aws.String(d.Get("type").(string)), - Value: aws.String(d.Get("value").(string)), - Overwrite: aws.Bool(d.Get("overwrite").(bool)), + Name: aws.String(d.Get("name").(string)), + Description: aws.String(d.Get("description").(string)), + Type: aws.String(d.Get("type").(string)), + Value: aws.String(d.Get("value").(string)), + Overwrite: aws.Bool(d.Get("overwrite").(bool)), + AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), } + if keyID, ok := d.GetOk("key_id"); ok { - log.Printf("[DEBUG] Setting key_id for SSM Parameter %s: %s", d.Get("name").(string), keyID.(string)) + log.Printf("[DEBUG] Setting key_id for SSM Parameter %v: %s", d.Get("name"), keyID) paramInput.SetKeyId(keyID.(string)) } - log.Printf("[DEBUG] Waiting for SSM Parameter %q to be updated", d.Get("name").(string)) - _, err := ssmconn.PutParameter(paramInput) - - if err != nil { + log.Printf("[DEBUG] Waiting for SSM Parameter %v to be updated", d.Get("name")) + if _, err := conn.PutParameter(paramInput); err != nil { return errwrap.Wrapf("[ERROR] Error creating SSM parameter: {{err}}", err) } + if err := setTagsSSM(conn, d, d.Get("name").(string), "Parameter"); err != nil { + return errwrap.Wrapf("[ERROR] Error creating SSM parameter tags: {{err}}", err) + } + d.SetId(d.Get("name").(string)) return resourceAwsSsmParameterRead(d, meta) diff --git a/aws/resource_aws_ssm_parameter_test.go b/aws/resource_aws_ssm_parameter_test.go index 7ed5d223738..df03c61a6ac 100644 --- a/aws/resource_aws_ssm_parameter_test.go +++ b/aws/resource_aws_ssm_parameter_test.go @@ -231,27 +231,30 @@ func testAccAWSSSMParameterBasicConfigOverwrite(rName string, value string) stri return fmt.Sprintf(` resource "aws_ssm_parameter" "foo" { name = "test_parameter-%s" + description = "description for parameter %s" type = "String" value = "%s" overwrite = true } -`, rName, value) +`, rName, rName, value) } func testAccAWSSSMParameterSecureConfig(rName string, value string) string { return fmt.Sprintf(` resource "aws_ssm_parameter" "secret_foo" { name = "test_secure_parameter-%s" + description = "description for parameter %s" type = "SecureString" value = "%s" } -`, rName, value) +`, rName, rName, value) } func testAccAWSSSMParameterSecureConfigWithKey(rName string, value string) string { return fmt.Sprintf(` resource "aws_ssm_parameter" "secret_foo" { name = "test_secure_parameter-%s" + description = "description for parameter %s" type = "SecureString" value = "%s" key_id = "${aws_kms_key.test_key.id}" @@ -261,5 +264,5 @@ resource "aws_kms_key" "test_key" { description = "KMS key 1" deletion_window_in_days = 7 } -`, rName, value) +`, rName, rName, value) } diff --git a/aws/tagsSSM.go b/aws/tagsSSM.go new file mode 100644 index 00000000000..c26eafc5308 --- /dev/null +++ b/aws/tagsSSM.go @@ -0,0 +1,117 @@ +package aws + +import ( + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform/helper/schema" +) + +// setTags is a helper to set the tags for a resource. It expects the +// tags field to be named "tags" +func setTagsSSM(conn *ssm.SSM, d *schema.ResourceData, id, resourceType string) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsSSM(tagsFromMapSSM(o), tagsFromMapSSM(n)) + + // Set tags + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + k := make([]*string, len(remove), len(remove)) + for i, t := range remove { + k[i] = t.Key + } + + _, err := conn.RemoveTagsFromResource(&ssm.RemoveTagsFromResourceInput{ + ResourceId: aws.String(id), + ResourceType: aws.String(resourceType), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + _, err := conn.AddTagsToResource(&ssm.AddTagsToResourceInput{ + ResourceId: aws.String(id), + ResourceType: aws.String(resourceType), + Tags: create, + }) + if err != nil { + return err + } + } + } + + return nil +} + +// diffTags takes our tags locally and the ones remotely and returns +// the set of tags that must be created, and the set of tags that must +// be destroyed. +func diffTagsSSM(oldTags, newTags []*ssm.Tag) ([]*ssm.Tag, []*ssm.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[*t.Key] = *t.Value + } + + // Build the list of what to remove + var remove []*ssm.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapSSM(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapSSM(m map[string]interface{}) []*ssm.Tag { + result := make([]*ssm.Tag, 0, len(m)) + for k, v := range m { + t := &ssm.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + } + if !tagIgnoredSSM(t) { + result = append(result, t) + } + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapSSM(ts []*ssm.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + if !tagIgnoredSSM(t) { + result[*t.Key] = *t.Value + } + } + + return result +} + +// compare a tag against a list of strings and checks if it should +// be ignored or not +func tagIgnoredSSM(t *ssm.Tag) bool { + filter := []string{"^aws:"} + for _, v := range filter { + log.Printf("[DEBUG] Matching %v with %v\n", v, *t.Key) + if r, _ := regexp.MatchString(v, *t.Key); r == true { + log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", *t.Key, *t.Value) + return true + } + } + return false +} diff --git a/aws/tagsSSM_test.go b/aws/tagsSSM_test.go new file mode 100644 index 00000000000..33792ae6985 --- /dev/null +++ b/aws/tagsSSM_test.go @@ -0,0 +1,105 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +// go test -v -run="TestDiffSSMTags" +func TestDiffSSMTags(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]string + }{ + // Basic add/remove + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "bar": "baz", + }, + Create: map[string]string{ + "bar": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Modify + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "baz", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + } + + for i, tc := range cases { + c, r := diffTagsSSM(tagsFromMapSSM(tc.Old), tagsFromMapSSM(tc.New)) + cm := tagsToMapSSM(c) + rm := tagsToMapSSM(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: %#v", i, cm) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: %#v", i, rm) + } + } +} + +// go test -v -run="TestIgnoringTagsSSM" +func TestIgnoringTagsSSM(t *testing.T) { + var ignoredTags []*ssm.Tag + ignoredTags = append(ignoredTags, &ssm.Tag{ + Key: aws.String("aws:cloudformation:logical-id"), + Value: aws.String("foo"), + }) + ignoredTags = append(ignoredTags, &ssm.Tag{ + Key: aws.String("aws:foo:bar"), + Value: aws.String("baz"), + }) + for _, tag := range ignoredTags { + if !tagIgnoredSSM(tag) { + t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.Key, *tag.Value) + } + } +} + +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckSSMTags( + ts []*ssm.Tag, key string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := tagsToMapSSM(ts) + v, ok := m[key] + if value != "" && !ok { + return fmt.Errorf("Missing tag: %s", key) + } else if value == "" && ok { + return fmt.Errorf("Extra tag: %s", key) + } + if value == "" { + return nil + } + + if v != value { + return fmt.Errorf("%s: bad value: %s", key, v) + } + + return nil + } +} diff --git a/website/docs/r/ssm_parameter.html.markdown b/website/docs/r/ssm_parameter.html.markdown index b0b991e844d..799c119233e 100644 --- a/website/docs/r/ssm_parameter.html.markdown +++ b/website/docs/r/ssm_parameter.html.markdown @@ -40,8 +40,13 @@ resource "aws_db_instance" "default" { resource "aws_ssm_parameter" "secret" { name = "${var.environment}/database/password/master" + description = "The parameter description" type = "SecureString" value = "${var.database_master_password}" + + tags { + environment = "${var.environment}" + } } ``` @@ -55,13 +60,17 @@ The following arguments are supported: * `name` - (Required) The name of the parameter. * `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`. * `value` - (Required) The value of the parameter. +* `description` - (Optional) The description of the parameter. * `key_id` - (Optional) The KMS key id or arn for encrypting a SecureString. -* `overwrite` - (Optional) Overwrite an existing parameter. If not specified, will default to `false`. +* `overwrite` - (Optional) Overwrite an existing parameter. If not specified, will default to `true`. +* `allowed_pattern` - (Optional) A regular expression used to validate the parameter value. +* `tags` - (Optional) A mapping of tags to assign to the object. ## Attributes Reference The following attributes are exported: * `name` - (Required) The name of the parameter. +* `description` - (Required) The description of the parameter. * `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`. * `value` - (Required) The value of the parameter.