From 24a16cbbca2b228486640afa8189a8a0ad72deeb Mon Sep 17 00:00:00 2001 From: Kash Date: Fri, 16 Nov 2018 13:02:09 -0500 Subject: [PATCH 1/5] support tags for iam user --- aws/resource_aws_iam_user.go | 33 ++++++++++++ aws/resource_aws_iam_user_test.go | 55 ++++++++++++++++++++ aws/tagsIAM.go | 83 +++++++++++++++++++++++++++++++ aws/tagsIAM_test.go | 79 +++++++++++++++++++++++++++++ 4 files changed, 250 insertions(+) create mode 100644 aws/tagsIAM.go create mode 100644 aws/tagsIAM_test.go diff --git a/aws/resource_aws_iam_user.go b/aws/resource_aws_iam_user.go index 830674ebe05..99ebdb2aca3 100644 --- a/aws/resource_aws_iam_user.go +++ b/aws/resource_aws_iam_user.go @@ -62,6 +62,7 @@ func resourceAwsIamUser() *schema.Resource { Default: false, Description: "Delete user even if it has non-Terraform-managed IAM access keys, login profile or MFA devices", }, + "tags": tagsSchema(), }, } } @@ -80,6 +81,11 @@ func resourceAwsIamUserCreate(d *schema.ResourceData, meta interface{}) error { request.PermissionsBoundary = aws.String(v.(string)) } + if v, ok := d.GetOk("tags"); ok { + tags := tagsFromMapIAM(v.(map[string]interface{})) + request.Tags = tags + } + log.Println("[DEBUG] Create IAM User request:", request) createResp, err := iamconn.CreateUser(request) if err != nil { @@ -121,6 +127,7 @@ func resourceAwsIamUserRead(d *schema.ResourceData, meta interface{}) error { d.Set("permissions_boundary", output.User.PermissionsBoundary.PermissionsBoundaryArn) } d.Set("unique_id", output.User.UserId) + d.Set("tags", tagsToMapIAM(output.User.Tags)) return nil } @@ -174,6 +181,32 @@ func resourceAwsIamUserUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChange("tags") { + // Reset all tags to empty set + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + _, r := diffTagsIAM(tagsFromMapIAM(o), tagsFromMapIAM(n)) + + _, untagErr := iamconn.UntagUser(&iam.UntagUserInput{ + UserName: aws.String(d.Id()), + TagKeys: tagKeysIam(r), + }) + if untagErr != nil { + return fmt.Errorf("error deleting IAM user tags: %s", untagErr) + } + + tags := tagsFromMapIAM(d.Get("tags").(map[string]interface{})) + input := &iam.TagUserInput{ + UserName: aws.String(d.Id()), + Tags: tags, + } + _, tagErr := iamconn.TagUser(input) + if tagErr != nil { + return fmt.Errorf("error update IAM user tags: %s", tagErr) + } + } + return resourceAwsIamUserRead(d, meta) } diff --git a/aws/resource_aws_iam_user_test.go b/aws/resource_aws_iam_user_test.go index 7ff2d308a96..3156afc4ba3 100644 --- a/aws/resource_aws_iam_user_test.go +++ b/aws/resource_aws_iam_user_test.go @@ -345,6 +345,38 @@ func TestAccAWSUser_permissionsBoundary(t *testing.T) { }) } +func TestAccAWSUser_tags(t *testing.T) { + var user iam.GetUserOutput + + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_user.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSUserConfig_tags(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSUserExists(resourceName, &user), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", "test-Name"), + resource.TestCheckResourceAttr(resourceName, "tags.tag2", "test-tag2"), + ), + }, + { + Config: testAccAWSUserConfig_tagsUpdate(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSUserExists(resourceName, &user), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.tag2", "test-tagUpdate"), + ), + }, + }, + }) +} + func testAccCheckAWSUserDestroy(s *terraform.State) error { iamconn := testAccProvider.Meta().(*AWSClient).iamconn @@ -566,3 +598,26 @@ resource "aws_iam_user" "test" { } `, rName) } + +func testAccAWSUserConfig_tags(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_user" "test" { + name = %q + tags { + Name = "test-Name" + tag2 = "test-tag2" + } +} +`, rName) +} + +func testAccAWSUserConfig_tagsUpdate(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_user" "test" { + name = %q + tags { + tag2 = "test-tagUpdate" + } +} +`, rName) +} diff --git a/aws/tagsIAM.go b/aws/tagsIAM.go new file mode 100644 index 00000000000..9a305af9c94 --- /dev/null +++ b/aws/tagsIAM.go @@ -0,0 +1,83 @@ +package aws + +import ( + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" +) + +// 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 diffTagsIAM(oldTags, newTags []*iam.Tag) ([]*iam.Tag, []*iam.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + + // Build the list of what to remove + var remove []*iam.Tag + for _, t := range oldTags { + old, ok := create[aws.StringValue(t.Key)] + if !ok || old != aws.StringValue(t.Value) { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapIAM(create), remove +} + +// tagsFromMapIAM returns the tags for the given map of data for IAM. +func tagsFromMapIAM(m map[string]interface{}) []*iam.Tag { + result := make([]*iam.Tag, 0, len(m)) + for k, v := range m { + t := &iam.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + } + if !tagIgnoredIAM(t) { + result = append(result, t) + } + } + + return result +} + +// tagsToMapIAM turns the list of IAM tags into a map. +func tagsToMapIAM(ts []*iam.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + if !tagIgnoredIAM(t) { + result[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + } + + return result +} + +// compare a tag against a list of strings and checks if it should +// be ignored or not +func tagIgnoredIAM(t *iam.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 +} + +// tagKeysIam returns the keys for the list of IAM tags +func tagKeysIam(ts []*iam.Tag) []*string { + result := make([]*string, 0, len(ts)) + for _, t := range ts { + result = append(result, t.Key) + } + return result +} diff --git a/aws/tagsIAM_test.go b/aws/tagsIAM_test.go new file mode 100644 index 00000000000..d2a8545b0d4 --- /dev/null +++ b/aws/tagsIAM_test.go @@ -0,0 +1,79 @@ +package aws + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/iam" +) + +// go test -v -run="TestDiffIAMTags" +func TestDiffIAMTags(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 := diffTagsIAM(tagsFromMapIAM(tc.Old), tagsFromMapIAM(tc.New)) + cm := tagsToMapIAM(c) + rm := tagsToMapIAM(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="TestIgnoringTagsIAM" +func TestIgnoringTagsIAM(t *testing.T) { + var ignoredTags []*iam.Tag + ignoredTags = append(ignoredTags, &iam.Tag{ + Key: aws.String("aws:cloudformation:logical-id"), + Value: aws.String("foo"), + }) + ignoredTags = append(ignoredTags, &iam.Tag{ + Key: aws.String("aws:foo:bar"), + Value: aws.String("baz"), + }) + for _, tag := range ignoredTags { + if !tagIgnoredIAM(tag) { + t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.Key, *tag.Value) + } + } +} From dd9ae3b8373a9dcfcbc9011e85d4e3a815616c1a Mon Sep 17 00:00:00 2001 From: Kash Date: Fri, 16 Nov 2018 13:03:54 -0500 Subject: [PATCH 2/5] update docs --- website/docs/r/iam_user.html.markdown | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/r/iam_user.html.markdown b/website/docs/r/iam_user.html.markdown index 3c6a29f58d8..0ed5a43445f 100644 --- a/website/docs/r/iam_user.html.markdown +++ b/website/docs/r/iam_user.html.markdown @@ -16,6 +16,9 @@ Provides an IAM user. resource "aws_iam_user" "lb" { name = "loadbalancer" path = "/system/" + tags { + tag-key = "tag-value" + } } resource "aws_iam_access_key" "lb" { @@ -53,6 +56,7 @@ The following arguments are supported: * `force_destroy` - (Optional, default false) When destroying this user, destroy even if it has non-Terraform-managed IAM access keys, login profile or MFA devices. Without `force_destroy` a user with non-Terraform-managed access keys and login profile will fail to be destroyed. +* `tags` - Tags for the IAM user ## Attributes Reference From ac26c14482965dbb3224cbaf05d889a6e3ae4fb7 Mon Sep 17 00:00:00 2001 From: Kash Date: Fri, 16 Nov 2018 14:09:44 -0500 Subject: [PATCH 3/5] fix linter --- aws/resource_aws_iam_user.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/aws/resource_aws_iam_user.go b/aws/resource_aws_iam_user.go index 99ebdb2aca3..61fa242c7e8 100644 --- a/aws/resource_aws_iam_user.go +++ b/aws/resource_aws_iam_user.go @@ -186,7 +186,7 @@ func resourceAwsIamUserUpdate(d *schema.ResourceData, meta interface{}) error { oraw, nraw := d.GetChange("tags") o := oraw.(map[string]interface{}) n := nraw.(map[string]interface{}) - _, r := diffTagsIAM(tagsFromMapIAM(o), tagsFromMapIAM(n)) + c, r := diffTagsIAM(tagsFromMapIAM(o), tagsFromMapIAM(n)) _, untagErr := iamconn.UntagUser(&iam.UntagUserInput{ UserName: aws.String(d.Id()), @@ -196,10 +196,9 @@ func resourceAwsIamUserUpdate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error deleting IAM user tags: %s", untagErr) } - tags := tagsFromMapIAM(d.Get("tags").(map[string]interface{})) input := &iam.TagUserInput{ UserName: aws.String(d.Id()), - Tags: tags, + Tags: c, } _, tagErr := iamconn.TagUser(input) if tagErr != nil { From 63250117be4fd640f0f7437b5b45bf23ab0f34c6 Mon Sep 17 00:00:00 2001 From: Kash Date: Mon, 19 Nov 2018 09:54:08 -0500 Subject: [PATCH 4/5] skip existing tags --- aws/tagsIAM.go | 2 ++ aws/tagsIAM_test.go | 40 ++++++++++++++++++++++++--- website/docs/r/iam_user.html.markdown | 4 +-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/aws/tagsIAM.go b/aws/tagsIAM.go index 9a305af9c94..17a547c5420 100644 --- a/aws/tagsIAM.go +++ b/aws/tagsIAM.go @@ -25,6 +25,8 @@ func diffTagsIAM(oldTags, newTags []*iam.Tag) ([]*iam.Tag, []*iam.Tag) { if !ok || old != aws.StringValue(t.Value) { // Delete it! remove = append(remove, t) + } else if ok { + delete(create, aws.StringValue(t.Key)) } } diff --git a/aws/tagsIAM_test.go b/aws/tagsIAM_test.go index d2a8545b0d4..4a74062f5e9 100644 --- a/aws/tagsIAM_test.go +++ b/aws/tagsIAM_test.go @@ -14,20 +14,19 @@ func TestDiffIAMTags(t *testing.T) { Old, New map[string]interface{} Create, Remove map[string]string }{ - // Basic add/remove + // Add { Old: map[string]interface{}{ "foo": "bar", }, New: map[string]interface{}{ + "foo": "bar", "bar": "baz", }, Create: map[string]string{ "bar": "baz", }, - Remove: map[string]string{ - "foo": "bar", - }, + Remove: map[string]string{}, }, // Modify @@ -45,6 +44,39 @@ func TestDiffIAMTags(t *testing.T) { "foo": "bar", }, }, + + // Overlap + { + Old: map[string]interface{}{ + "foo": "bar", + "hello": "world", + }, + New: map[string]interface{}{ + "foo": "baz", + "hello": "world", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Remove + { + Old: map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + New: map[string]interface{}{ + "foo": "bar", + }, + Create: map[string]string{}, + Remove: map[string]string{ + "bar": "baz", + }, + }, } for i, tc := range cases { diff --git a/website/docs/r/iam_user.html.markdown b/website/docs/r/iam_user.html.markdown index 0ed5a43445f..14d76280f09 100644 --- a/website/docs/r/iam_user.html.markdown +++ b/website/docs/r/iam_user.html.markdown @@ -16,7 +16,7 @@ Provides an IAM user. resource "aws_iam_user" "lb" { name = "loadbalancer" path = "/system/" - tags { + tags = { tag-key = "tag-value" } } @@ -56,7 +56,7 @@ The following arguments are supported: * `force_destroy` - (Optional, default false) When destroying this user, destroy even if it has non-Terraform-managed IAM access keys, login profile or MFA devices. Without `force_destroy` a user with non-Terraform-managed access keys and login profile will fail to be destroyed. -* `tags` - Tags for the IAM user +* `tags` - Key-value mapping of tags for the IAM user ## Attributes Reference From 74562b3329f3043886144647de92b6fe9b8a0e89 Mon Sep 17 00:00:00 2001 From: Kash Date: Mon, 19 Nov 2018 09:57:20 -0500 Subject: [PATCH 5/5] api call when count > 0 --- aws/resource_aws_iam_user.go | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/aws/resource_aws_iam_user.go b/aws/resource_aws_iam_user.go index 61fa242c7e8..c3a622a9214 100644 --- a/aws/resource_aws_iam_user.go +++ b/aws/resource_aws_iam_user.go @@ -127,7 +127,9 @@ func resourceAwsIamUserRead(d *schema.ResourceData, meta interface{}) error { d.Set("permissions_boundary", output.User.PermissionsBoundary.PermissionsBoundaryArn) } d.Set("unique_id", output.User.UserId) - d.Set("tags", tagsToMapIAM(output.User.Tags)) + if err := d.Set("tags", tagsToMapIAM(output.User.Tags)); err != nil { + return fmt.Errorf("error setting tags: %s", err) + } return nil } @@ -188,21 +190,25 @@ func resourceAwsIamUserUpdate(d *schema.ResourceData, meta interface{}) error { n := nraw.(map[string]interface{}) c, r := diffTagsIAM(tagsFromMapIAM(o), tagsFromMapIAM(n)) - _, untagErr := iamconn.UntagUser(&iam.UntagUserInput{ - UserName: aws.String(d.Id()), - TagKeys: tagKeysIam(r), - }) - if untagErr != nil { - return fmt.Errorf("error deleting IAM user tags: %s", untagErr) + if len(r) > 0 { + _, err := iamconn.UntagUser(&iam.UntagUserInput{ + UserName: aws.String(d.Id()), + TagKeys: tagKeysIam(r), + }) + if err != nil { + return fmt.Errorf("error deleting IAM user tags: %s", err) + } } - input := &iam.TagUserInput{ - UserName: aws.String(d.Id()), - Tags: c, - } - _, tagErr := iamconn.TagUser(input) - if tagErr != nil { - return fmt.Errorf("error update IAM user tags: %s", tagErr) + if len(c) > 0 { + input := &iam.TagUserInput{ + UserName: aws.String(d.Id()), + Tags: c, + } + _, err := iamconn.TagUser(input) + if err != nil { + return fmt.Errorf("error update IAM user tags: %s", err) + } } }