diff --git a/builtin/providers/aws/resource_aws_kms_key.go b/builtin/providers/aws/resource_aws_kms_key.go index 0ccd72e75..a95ab25be 100644 --- a/builtin/providers/aws/resource_aws_kms_key.go +++ b/builtin/providers/aws/resource_aws_kms_key.go @@ -80,6 +80,7 @@ func resourceAwsKmsKey() *schema.Resource { return }, }, + "tags": tagsSchema(), }, } } @@ -98,6 +99,9 @@ func resourceAwsKmsKeyCreate(d *schema.ResourceData, meta interface{}) error { if v, exists := d.GetOk("policy"); exists { req.Policy = aws.String(v.(string)) } + if v, exists := d.GetOk("tags"); exists { + req.Tags = tagsFromMapKMS(v.(map[string]interface{})) + } var resp *kms.CreateKeyOutput // AWS requires any principal in the policy to exist before the key is created. @@ -170,6 +174,14 @@ func resourceAwsKmsKeyRead(d *schema.ResourceData, meta interface{}) error { } d.Set("enable_key_rotation", krs.KeyRotationEnabled) + tagList, err := conn.ListResourceTags(&kms.ListResourceTagsInput{ + KeyId: metadata.KeyId, + }) + if err != nil { + return fmt.Errorf("Failed to get KMS key tags (key: %s): %s", metadata.KeyId, err) + } + d.Set("tags", tagsToMapKMS(tagList.Tags)) + return nil } @@ -215,6 +227,10 @@ func _resourceAwsKmsKeyUpdate(d *schema.ResourceData, meta interface{}, isFresh } } + if err := setTagsKMS(conn, d, d.Id()); err != nil { + return err + } + return resourceAwsKmsKeyRead(d, meta) } diff --git a/builtin/providers/aws/resource_aws_kms_key_test.go b/builtin/providers/aws/resource_aws_kms_key_test.go index cd686c7e3..86cabe4ed 100644 --- a/builtin/providers/aws/resource_aws_kms_key_test.go +++ b/builtin/providers/aws/resource_aws_kms_key_test.go @@ -1,3 +1,4 @@ +// make testacc TEST=./builtin/providers/aws/ TESTARGS='-run=TestAccAWSKmsKey_' package aws import ( @@ -95,6 +96,25 @@ func TestAccAWSKmsKey_isEnabled(t *testing.T) { }) } +func TestAccAWSKmsKey_tags(t *testing.T) { + var keyBefore kms.KeyMetadata + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSKmsKeyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSKmsKey_tags, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSKmsKeyExists("aws_kms_key.foo", &keyBefore), + resource.TestCheckResourceAttr("aws_kms_key.foo", "tags.%", "2"), + ), + }, + }, + }) +} + func testAccCheckAWSKmsKeyHasPolicy(name string, expectedPolicyText string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] @@ -244,3 +264,12 @@ resource "aws_kms_key" "bar" { enable_key_rotation = true is_enabled = true }`, kmsTimestamp) + +var testAccAWSKmsKey_tags = fmt.Sprintf(` +resource "aws_kms_key" "foo" { + description = "Terraform acc test %s" + tags { + Key1 = "Value One" + Description = "Very interesting" + } +}`, kmsTimestamp) diff --git a/builtin/providers/aws/tagsKMS.go b/builtin/providers/aws/tagsKMS.go new file mode 100644 index 000000000..d4d2eca1c --- /dev/null +++ b/builtin/providers/aws/tagsKMS.go @@ -0,0 +1,115 @@ +package aws + +import ( + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kms" + "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 setTagsKMS(conn *kms.KMS, d *schema.ResourceData, keyId string) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsKMS(tagsFromMapKMS(o), tagsFromMapKMS(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.TagKey + } + + _, err := conn.UntagResource(&kms.UntagResourceInput{ + KeyId: aws.String(keyId), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + _, err := conn.TagResource(&kms.TagResourceInput{ + KeyId: aws.String(keyId), + 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 diffTagsKMS(oldTags, newTags []*kms.Tag) ([]*kms.Tag, []*kms.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[aws.StringValue(t.TagKey)] = aws.StringValue(t.TagValue) + } + + // Build the list of what to remove + var remove []*kms.Tag + for _, t := range oldTags { + old, ok := create[aws.StringValue(t.TagKey)] + if !ok || old != aws.StringValue(t.TagValue) { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapKMS(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapKMS(m map[string]interface{}) []*kms.Tag { + result := make([]*kms.Tag, 0, len(m)) + for k, v := range m { + t := &kms.Tag{ + TagKey: aws.String(k), + TagValue: aws.String(v.(string)), + } + if !tagIgnoredKMS(t) { + result = append(result, t) + } + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapKMS(ts []*kms.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + if !tagIgnoredKMS(t) { + result[aws.StringValue(t.TagKey)] = aws.StringValue(t.TagValue) + } + } + + return result +} + +// compare a tag against a list of strings and checks if it should +// be ignored or not +func tagIgnoredKMS(t *kms.Tag) bool { + filter := []string{"^aws:*"} + for _, v := range filter { + log.Printf("[DEBUG] Matching %v with %v\n", v, *t.TagKey) + if r, _ := regexp.MatchString(v, *t.TagKey); r == true { + log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", *t.TagKey, *t.TagValue) + return true + } + } + return false +} diff --git a/builtin/providers/aws/tagsKMS_test.go b/builtin/providers/aws/tagsKMS_test.go new file mode 100644 index 000000000..a1d7a770e --- /dev/null +++ b/builtin/providers/aws/tagsKMS_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/kms" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +// go test -v -run="TestDiffKMSTags" +func TestDiffKMSTags(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 := diffTagsKMS(tagsFromMapKMS(tc.Old), tagsFromMapKMS(tc.New)) + cm := tagsToMapKMS(c) + rm := tagsToMapKMS(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="TestIgnoringTagsKMS" +func TestIgnoringTagsKMS(t *testing.T) { + var ignoredTags []*kms.Tag + ignoredTags = append(ignoredTags, &kms.Tag{ + TagKey: aws.String("aws:cloudformation:logical-id"), + TagValue: aws.String("foo"), + }) + ignoredTags = append(ignoredTags, &kms.Tag{ + TagKey: aws.String("aws:foo:bar"), + TagValue: aws.String("baz"), + }) + for _, tag := range ignoredTags { + if !tagIgnoredKMS(tag) { + t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.TagKey, *tag.TagValue) + } + } +} + +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckKMSTags( + ts []*kms.Tag, key string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := tagsToMapKMS(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/source/docs/providers/aws/r/kms_key.html.markdown b/website/source/docs/providers/aws/r/kms_key.html.markdown index d0c89be02..9a494977f 100644 --- a/website/source/docs/providers/aws/r/kms_key.html.markdown +++ b/website/source/docs/providers/aws/r/kms_key.html.markdown @@ -32,6 +32,7 @@ The following arguments are supported: * `is_enabled` - (Optional) Specifies whether the key is enabled. Defaults to true. * `enable_key_rotation` - (Optional) Specifies whether [key rotation](http://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html) is enabled. Defaults to false. +* `tags` - (Optional) A mapping of tags to assign to the object. ## Attributes Reference