diff --git a/builtin/providers/aws/resource_aws_cloudtrail.go b/builtin/providers/aws/resource_aws_cloudtrail.go index ed7480625b..c8c639f802 100644 --- a/builtin/providers/aws/resource_aws_cloudtrail.go +++ b/builtin/providers/aws/resource_aws_cloudtrail.go @@ -74,6 +74,7 @@ func resourceAwsCloudTrail() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "tags": tagsSchema(), }, } } @@ -118,6 +119,7 @@ func resourceAwsCloudTrailCreate(d *schema.ResourceData, meta interface{}) error log.Printf("[DEBUG] CloudTrail created: %s", t) + d.Set("arn", *t.TrailARN) d.SetId(*t.Name) // AWS CloudTrail sets newly-created trails to false. @@ -128,7 +130,7 @@ func resourceAwsCloudTrailCreate(d *schema.ResourceData, meta interface{}) error } } - return resourceAwsCloudTrailRead(d, meta) + return resourceAwsCloudTrailUpdate(d, meta) } func resourceAwsCloudTrailRead(d *schema.ResourceData, meta interface{}) error { @@ -169,6 +171,26 @@ func resourceAwsCloudTrailRead(d *schema.ResourceData, meta interface{}) error { d.Set("arn", trail.TrailARN) d.Set("home_region", trail.HomeRegion) + // Get tags + req := &cloudtrail.ListTagsInput{ + ResourceIdList: []*string{trail.TrailARN}, + } + + tagsOut, err := conn.ListTags(req) + if err != nil { + return err + } + log.Printf("[DEBUG] Received CloudTrail tags: %s", tagsOut) + + var tags []*cloudtrail.Tag + if tagsOut.ResourceTagList != nil && len(tagsOut.ResourceTagList) > 0 { + tags = tagsOut.ResourceTagList[0].TagsList + } + + if err := d.Set("tags", tagsToMapCloudtrail(tags)); err != nil { + return err + } + logstatus, err := cloudTrailGetLoggingStatus(conn, trail.Name) if err != nil { return err @@ -219,6 +241,13 @@ func resourceAwsCloudTrailUpdate(d *schema.ResourceData, meta interface{}) error return err } + if d.HasChange("tags") { + err := setTagsCloudtrail(conn, d) + if err != nil { + return err + } + } + if d.HasChange("enable_logging") { log.Printf("[DEBUG] Updating logging on CloudTrail: %s", input) err := cloudTrailSetLogging(conn, d.Get("enable_logging").(bool), *input.Name) diff --git a/builtin/providers/aws/resource_aws_cloudtrail_test.go b/builtin/providers/aws/resource_aws_cloudtrail_test.go index 77c0ed244e..722bcaab1c 100644 --- a/builtin/providers/aws/resource_aws_cloudtrail_test.go +++ b/builtin/providers/aws/resource_aws_cloudtrail_test.go @@ -2,6 +2,7 @@ package aws import ( "fmt" + "log" "math/rand" "testing" "time" @@ -157,6 +158,53 @@ func TestAccAWSCloudTrail_logValidation(t *testing.T) { }) } +func TestAccAWSCloudTrail_tags(t *testing.T) { + var trail cloudtrail.Trail + var trailTags []*cloudtrail.Tag + var trailTagsModified []*cloudtrail.Tag + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCloudTrailDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSCloudTrailConfig_tags, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudTrailExists("aws_cloudtrail.foobar", &trail), + resource.TestCheckResourceAttr("aws_cloudtrail.foobar", "tags.#", "2"), + testAccCheckCloudTrailLoadTags(&trail, &trailTags), + testAccCheckCloudTrailCheckTags(&trailTags, map[string]string{"Foo": "moo", "Pooh": "hi"}), + testAccCheckCloudTrailLogValidationEnabled("aws_cloudtrail.foobar", false, &trail), + testAccCheckCloudTrailKmsKeyIdEquals("aws_cloudtrail.foobar", "", &trail), + ), + }, + resource.TestStep{ + Config: testAccAWSCloudTrailConfig_tagsModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudTrailExists("aws_cloudtrail.foobar", &trail), + resource.TestCheckResourceAttr("aws_cloudtrail.foobar", "tags.#", "3"), + testAccCheckCloudTrailLoadTags(&trail, &trailTagsModified), + testAccCheckCloudTrailCheckTags(&trailTagsModified, map[string]string{"Foo": "moo", "Moo": "boom", "Pooh": "hi"}), + testAccCheckCloudTrailLogValidationEnabled("aws_cloudtrail.foobar", false, &trail), + testAccCheckCloudTrailKmsKeyIdEquals("aws_cloudtrail.foobar", "", &trail), + ), + }, + resource.TestStep{ + Config: testAccAWSCloudTrailConfig_tagsModifiedAgain, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudTrailExists("aws_cloudtrail.foobar", &trail), + resource.TestCheckResourceAttr("aws_cloudtrail.foobar", "tags.#", "0"), + testAccCheckCloudTrailLoadTags(&trail, &trailTagsModified), + testAccCheckCloudTrailCheckTags(&trailTagsModified, map[string]string{}), + testAccCheckCloudTrailLogValidationEnabled("aws_cloudtrail.foobar", false, &trail), + testAccCheckCloudTrailKmsKeyIdEquals("aws_cloudtrail.foobar", "", &trail), + ), + }, + }, + }) +} + func testAccCheckCloudTrailExists(n string, trail *cloudtrail.Trail) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -299,6 +347,25 @@ func testAccCheckAWSCloudTrailDestroy(s *terraform.State) error { return nil } +func testAccCheckCloudTrailLoadTags(trail *cloudtrail.Trail, tags *[]*cloudtrail.Tag) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).cloudtrailconn + input := cloudtrail.ListTagsInput{ + ResourceIdList: []*string{trail.TrailARN}, + } + out, err := conn.ListTags(&input) + if err != nil { + return err + } + log.Printf("[DEBUG] Received CloudTrail tags during test: %s", out) + if len(out.ResourceTagList) > 0 { + *tags = out.ResourceTagList[0].TagsList + } + log.Printf("[DEBUG] Loading CloudTrail tags into a var: %s", *tags) + return nil + } +} + var cloudTrailRandInt = rand.New(rand.NewSource(time.Now().UnixNano())).Int() var testAccAWSCloudTrailConfig = fmt.Sprintf(` @@ -498,3 +565,56 @@ resource "aws_s3_bucket" "foo" { POLICY } `, cloudTrailRandInt, cloudTrailRandInt, cloudTrailRandInt) + +var testAccAWSCloudTrailConfig_tags_tpl = ` +resource "aws_cloudtrail" "foobar" { + name = "tf-acc-trail-log-validation-test" + s3_bucket_name = "${aws_s3_bucket.foo.id}" + %s +} + +resource "aws_s3_bucket" "foo" { + bucket = "tf-test-trail-%d" + force_destroy = true + policy = < 0 { + input := cloudtrail.RemoveTagsInput{ + ResourceId: aws.String(d.Get("arn").(string)), + TagsList: remove, + } + log.Printf("[DEBUG] Removing CloudTrail tags: %s", input) + _, err := conn.RemoveTags(&input) + if err != nil { + return err + } + } + if len(create) > 0 { + input := cloudtrail.AddTagsInput{ + ResourceId: aws.String(d.Get("arn").(string)), + TagsList: create, + } + log.Printf("[DEBUG] Adding CloudTrail tags: %s", input) + _, err := conn.AddTags(&input) + 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 diffTagsCloudtrail(oldTags, newTags []*cloudtrail.Tag) ([]*cloudtrail.Tag, []*cloudtrail.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 []*cloudtrail.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapCloudtrail(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapCloudtrail(m map[string]interface{}) []*cloudtrail.Tag { + var result []*cloudtrail.Tag + for k, v := range m { + result = append(result, &cloudtrail.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapCloudtrail(ts []*cloudtrail.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} diff --git a/builtin/providers/aws/tagsCloudtrail_test.go b/builtin/providers/aws/tagsCloudtrail_test.go new file mode 100644 index 0000000000..860a9bd3de --- /dev/null +++ b/builtin/providers/aws/tagsCloudtrail_test.go @@ -0,0 +1,73 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudtrail" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffCloudtrailTags(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 := diffTagsCloudtrail(tagsFromMapCloudtrail(tc.Old), tagsFromMapCloudtrail(tc.New)) + cm := tagsToMapCloudtrail(c) + rm := tagsToMapCloudtrail(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) + } + } +} + +// testAccCheckCloudTrailCheckTags can be used to check the tags on a trail +func testAccCheckCloudTrailCheckTags(tags *[]*cloudtrail.Tag, expectedTags map[string]string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if !reflect.DeepEqual(expectedTags, tagsToMapCloudtrail(*tags)) { + return fmt.Errorf("Tags mismatch.\nExpected: %#v\nGiven: %#v", + expectedTags, tagsToMapCloudtrail(*tags)) + } + return nil + } +} diff --git a/website/source/docs/providers/aws/r/cloudtrail.html.markdown b/website/source/docs/providers/aws/r/cloudtrail.html.markdown index 030bccc9d4..2afe727bfe 100644 --- a/website/source/docs/providers/aws/r/cloudtrail.html.markdown +++ b/website/source/docs/providers/aws/r/cloudtrail.html.markdown @@ -78,6 +78,7 @@ The following arguments are supported: * `enable_log_file_validation` - (Optional) Specifies whether log file integrity validation is enabled. Defaults to `false`. * `kms_key_id` - (Optional) Specifies the KMS key ID to use to encrypt the logs delivered by CloudTrail. +* `tags` - (Optional) A mapping of tags to assign to the trail ## Attribute Reference