diff --git a/aws/resource_aws_ssm_parameter.go b/aws/resource_aws_ssm_parameter.go index d22668cb428..3f91cee44f3 100644 --- a/aws/resource_aws_ssm_parameter.go +++ b/aws/resource_aws_ssm_parameter.go @@ -18,6 +18,7 @@ func resourceAwsSsmParameter() *schema.Resource { Read: resourceAwsSsmParameterRead, Update: resourceAwsSsmParameterPut, Delete: resourceAwsSsmParameterDelete, + Exists: resourceAwsSmmParameterExists, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, @@ -28,10 +29,14 @@ func resourceAwsSsmParameter() *schema.Resource { Required: true, ForceNew: true, }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, "type": { Type: schema.TypeString, Required: true, - ForceNew: true, ValidateFunc: validateSsmParameterType, }, "value": { @@ -47,45 +52,88 @@ func resourceAwsSsmParameter() *schema.Resource { "key_id": { Type: schema.TypeString, Optional: true, - ForceNew: true, }, "overwrite": { Type: schema.TypeBool, Optional: true, - Default: false, }, + "allowed_pattern": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "tags": tagsSchema(), }, } } +func resourceAwsSmmParameterExists(d *schema.ResourceData, meta interface{}) (bool, error) { + ssmconn := meta.(*AWSClient).ssmconn + + resp, err := ssmconn.GetParameters(&ssm.GetParametersInput{ + Names: []*string{aws.String(d.Id())}, + WithDecryption: aws.Bool(true), + }) + + if err != nil { + return false, err + } + return len(resp.InvalidParameters) == 0, nil +} + func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error { ssmconn := meta.(*AWSClient).ssmconn log.Printf("[DEBUG] Reading SSM Parameter: %s", d.Id()) - paramInput := &ssm.GetParametersInput{ - Names: []*string{ - aws.String(d.Id()), - }, + resp, err := ssmconn.GetParameters(&ssm.GetParametersInput{ + Names: []*string{aws.String(d.Id())}, WithDecryption: aws.Bool(true), + }) + if err != nil { + return errwrap.Wrapf("[ERROR] Error getting SSM parameter: {{err}}", err) } - resp, err := ssmconn.GetParameters(paramInput) + param := resp.Parameters[0] + d.Set("name", param.Name) + d.Set("type", param.Type) + d.Set("value", param.Value) + respDetailed, err := ssmconn.DescribeParameters(&ssm.DescribeParametersInput{ + Filters: []*ssm.ParametersFilter{ + &ssm.ParametersFilter{ + Key: aws.String("Name"), + Values: []*string{aws.String(d.Get("name").(string))}, + }, + }, + }) if err != nil { return errwrap.Wrapf("[ERROR] Error describing SSM parameter: {{err}}", err) } - if len(resp.Parameters) == 0 { - log.Printf("[WARN] SSM Param %q not found, removing from state", d.Id()) - d.SetId("") - return nil + detail := respDetailed.Parameters[0] + if detail.Description != nil { + // Trailing spaces are not considered as a difference + *detail.Description = strings.TrimSpace(*detail.Description) + } + if _, ok := d.GetOk("key_id"); !ok && detail.KeyId != nil && *detail.KeyId == "alias/aws/ssm" { + // If the key_id is not specified and the actual key is set to the AWS default key, we set + // the key to nil to ensure that terraform does not consider that the actual key has changed. + detail.KeyId = nil } - param := resp.Parameters[0] - d.Set("name", param.Name) - d.Set("type", param.Type) - d.Set("value", param.Value) + d.Set("key_id", detail.KeyId) + d.Set("description", detail.Description) + d.Set("allowed_pattern", detail.AllowedPattern) + + if tagList, err := ssmconn.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)) + } arn := arn.ARN{ Partition: meta.(*AWSClient).partition, @@ -104,15 +152,12 @@ func resourceAwsSsmParameterDelete(d *schema.ResourceData, meta interface{}) err log.Printf("[INFO] Deleting SSM Parameter: %s", d.Id()) - paramInput := &ssm.DeleteParameterInput{ + _, err := ssmconn.DeleteParameter(&ssm.DeleteParameterInput{ Name: aws.String(d.Get("name").(string)), - } - - _, err := ssmconn.DeleteParameter(paramInput) + }) if err != nil { return err } - d.SetId("") return nil @@ -124,24 +169,47 @@ func resourceAwsSsmParameterPut(d *schema.ResourceData, meta interface{}) error 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)), + Type: aws.String(d.Get("type").(string)), + Value: aws.String(d.Get("value").(string)), + Overwrite: aws.Bool(shouldUpdateSsmParameter(d)), + AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), } + + if description, ok := d.GetOk("description"); ok { + paramInput.SetDescription(description.(string)) + } else if d.HasChange("description") { + // There is a "bug" in the AWS API and is it not possible to unset a description once + // it has been initially set + paramInput.SetDescription(" ") + } + 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 := ssmconn.PutParameter(paramInput); err != nil { return errwrap.Wrapf("[ERROR] Error creating SSM parameter: {{err}}", err) } + if err := setTagsSSM(ssmconn, 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) } + +func shouldUpdateSsmParameter(d *schema.ResourceData) bool { + // If the user has specified a preference, return their preference + if value, ok := d.GetOkExists("overwrite"); ok { + return value.(bool) + } + + // Since the user has not specified a preference, obey lifecycle rules + // if it is not a new resource, otherwise overwrite should be set to false. + return !d.IsNewResource() +} diff --git a/aws/resource_aws_ssm_parameter_test.go b/aws/resource_aws_ssm_parameter_test.go index 194b1c2b7db..fcc465d2737 100644 --- a/aws/resource_aws_ssm_parameter_test.go +++ b/aws/resource_aws_ssm_parameter_test.go @@ -273,18 +273,31 @@ resource "aws_ssm_parameter" "foo" { func testAccAWSSSMParameterBasicConfigOverwrite(rName, pType, value string) string { return fmt.Sprintf(` resource "aws_ssm_parameter" "foo" { - name = "%s" - type = "%s" + name = "test_parameter-%s" + description = "description for parameter %s" + type = "String" value = "%s" overwrite = true } -`, rName, pType, 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, 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}" @@ -294,5 +307,37 @@ resource "aws_kms_key" "test_key" { description = "KMS key 1" deletion_window_in_days = 7 } -`, rName, value) +`, rName, rName, value) +} + +func TestAWSSSMParameterShouldUpdate(t *testing.T) { + data := resourceAwsSsmParameter().TestResourceData() + failure := false + + if !shouldUpdateSsmParameter(data) { + t.Logf("Existing resources should be overwritten if the values don't match!") + failure = true + } + + data.MarkNewResource() + if shouldUpdateSsmParameter(data) { + t.Logf("New resources must never be overwritten, this will overwrite parameters created outside of the system") + failure = true + } + + data = resourceAwsSsmParameter().TestResourceData() + data.Set("overwrite", true) + if !shouldUpdateSsmParameter(data) { + t.Logf("Resources should always be overwritten if the user requests it") + failure = true + } + + data.Set("overwrite", false) + if shouldUpdateSsmParameter(data) { + t.Logf("Resources should never be overwritten if the user requests it") + failure = true + } + if failure { + t.Fail() + } } 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 fe9e5d22650..d03284ef866 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,8 +60,11 @@ 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 `false` if the resource has not been created by terraform to avoid overwrite of existing resource and will default to `true` otherwise (terraform lifecycle rules should then be used to manage the update behavior). +* `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 @@ -64,5 +72,14 @@ The following attributes are exported: * `arn` - The ARN of the parameter. * `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. + +## Import + +SSM Parameters can be imported using the `parameter store name`, e.g. + +``` +$ terraform import aws_ssm_parameter.my_param /my_path/my_paramname +```