diff --git a/aws/resource_aws_codepipeline.go b/aws/resource_aws_codepipeline.go index 9bd8a0d46a4..444e2dc7dd9 100644 --- a/aws/resource_aws_codepipeline.go +++ b/aws/resource_aws_codepipeline.go @@ -161,6 +161,7 @@ func resourceAwsCodePipeline() *schema.Resource { }, }, }, + "tags": tagsSchema(), }, } } @@ -169,6 +170,7 @@ func resourceAwsCodePipelineCreate(d *schema.ResourceData, meta interface{}) err conn := meta.(*AWSClient).codepipelineconn params := &codepipeline.CreatePipelineInput{ Pipeline: expandAwsCodePipeline(d), + Tags: tagsFromMapCodePipeline(d.Get("tags").(map[string]interface{})), } var resp *codepipeline.CreatePipelineOutput @@ -447,6 +449,11 @@ func resourceAwsCodePipelineRead(d *schema.ResourceData, meta interface{}) error d.Set("arn", metadata.PipelineArn) d.Set("name", pipeline.Name) d.Set("role_arn", pipeline.RoleArn) + + if err := saveTagsCodePipeline(conn, d); err != nil { + return err + } + return nil } @@ -465,6 +472,10 @@ func resourceAwsCodePipelineUpdate(d *schema.ResourceData, meta interface{}) err d.Id(), err) } + if err := setTagsCodePipeline(conn, d); err != nil { + return fmt.Errorf("Error updating CodePipeline tags: %s", d.Id()) + } + return resourceAwsCodePipelineRead(d, meta) } diff --git a/aws/resource_aws_codepipeline_test.go b/aws/resource_aws_codepipeline_test.go index 5ab7d5c3038..bf7b30c7870 100644 --- a/aws/resource_aws_codepipeline_test.go +++ b/aws/resource_aws_codepipeline_test.go @@ -135,6 +135,55 @@ func TestAccAWSCodePipeline_deployWithServiceRole(t *testing.T) { }) } +func TestAccAWSCodePipeline_tags(t *testing.T) { + if os.Getenv("GITHUB_TOKEN") == "" { + t.Skip("Environment variable GITHUB_TOKEN is not set") + } + + name := acctest.RandString(10) + resourceName := "aws_codepipeline.bar" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCodePipelineDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCodePipelineConfigWithTags(name, "tag1value", "tag2value"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCodePipelineExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "3"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", fmt.Sprintf("test-pipeline-%s", name)), + resource.TestCheckResourceAttr(resourceName, "tags.tag1", "tag1value"), + resource.TestCheckResourceAttr(resourceName, "tags.tag2", "tag2value"), + ), + }, + { + Config: testAccAWSCodePipelineConfigWithTags(name, "tag1valueUpdate", "tag2valueUpdate"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCodePipelineExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "3"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", fmt.Sprintf("test-pipeline-%s", name)), + resource.TestCheckResourceAttr(resourceName, "tags.tag1", "tag1valueUpdate"), + resource.TestCheckResourceAttr(resourceName, "tags.tag2", "tag2valueUpdate"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSCodePipelineConfig_basic(name), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCodePipelineExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + }, + }) +} + func testAccCheckAWSCodePipelineExists(n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -695,3 +744,121 @@ resource "aws_codepipeline" "bar" { } `, rName, rName, rName, rName) } + +func testAccAWSCodePipelineConfigWithTags(rName, tag1, tag2 string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "foo" { + bucket = "tf-test-pipeline-%[1]s" + acl = "private" +} + +resource "aws_iam_role" "codepipeline_role" { + name = "codepipeline-role-%[1]s" + + assume_role_policy = < 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + k := make([]*string, len(remove)) + for i, t := range remove { + k[i] = t.Key + } + + _, err := conn.UntagResource(&codepipeline.UntagResourceInput{ + ResourceArn: aws.String(d.Get("arn").(string)), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + _, err := conn.TagResource(&codepipeline.TagResourceInput{ + ResourceArn: aws.String(d.Get("arn").(string)), + 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 diffTagsCodePipeline(oldTags, newTags []*codepipeline.Tag) ([]*codepipeline.Tag, []*codepipeline.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 []*codepipeline.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) + } else if ok { + // already present so remove from new + delete(create, aws.StringValue(t.Key)) + } + } + + return tagsFromMapCodePipeline(create), remove +} + +func saveTagsCodePipeline(conn *codepipeline.CodePipeline, d *schema.ResourceData) error { + resp, err := conn.ListTagsForResource(&codepipeline.ListTagsForResourceInput{ + ResourceArn: aws.String(d.Get("arn").(string)), + }) + + if err != nil { + return fmt.Errorf("Error retreiving tags for ARN: %s", d.Get("arn").(string)) + } + + var dt []*codepipeline.Tag + if len(resp.Tags) > 0 { + dt = resp.Tags + } + + return d.Set("tags", tagsToMapCodePipeline(dt)) +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapCodePipeline(m map[string]interface{}) []*codepipeline.Tag { + result := make([]*codepipeline.Tag, 0, len(m)) + for k, v := range m { + t := &codepipeline.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + } + if !tagIgnoredCodePipeline(t) { + result = append(result, t) + } + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapCodePipeline(ts []*codepipeline.Tag) map[string]string { + result := make(map[string]string) + for _, t := range ts { + if !tagIgnoredCodePipeline(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 tagIgnoredCodePipeline(t *codepipeline.Tag) bool { + filter := []string{"^aws:"} + for _, v := range filter { + log.Printf("[DEBUG] Matching %v with %v\n", v, aws.StringValue(t.Key)) + r, _ := regexp.MatchString(v, aws.StringValue(t.Key)) + if r { + log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", aws.StringValue(t.Key), aws.StringValue(t.Value)) + return true + } + } + return false +} diff --git a/aws/tagsCodePipeline_test.go b/aws/tagsCodePipeline_test.go new file mode 100644 index 00000000000..a47c839cd45 --- /dev/null +++ b/aws/tagsCodePipeline_test.go @@ -0,0 +1,112 @@ +package aws + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/codepipeline" +) + +// go test -v -run="TestDiffCodePipelineTags" +func TestDiffCodePipelineTags(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", + }, + }, + + // 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 { + c, r := diffTagsCodePipeline(tagsFromMapCodePipeline(tc.Old), tagsFromMapCodePipeline(tc.New)) + cm := tagsToMapCodePipeline(c) + rm := tagsToMapCodePipeline(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="TestIgnoringTagsCodePipeline" +func TestIgnoringTagsCodePipeline(t *testing.T) { + var ignoredTags []*codepipeline.Tag + ignoredTags = append(ignoredTags, &codepipeline.Tag{ + Key: aws.String("aws:cloudformation:logical-id"), + Value: aws.String("foo"), + }) + ignoredTags = append(ignoredTags, &codepipeline.Tag{ + Key: aws.String("aws:foo:bar"), + Value: aws.String("baz"), + }) + for _, tag := range ignoredTags { + if !tagIgnoredCodePipeline(tag) { + t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.Key, *tag.Value) + } + } +} diff --git a/website/docs/r/codepipeline.markdown b/website/docs/r/codepipeline.markdown index 036b1b2da34..a2365c7fb67 100644 --- a/website/docs/r/codepipeline.markdown +++ b/website/docs/r/codepipeline.markdown @@ -158,6 +158,7 @@ The following arguments are supported: * `role_arn` - (Required) A service role Amazon Resource Name (ARN) that grants AWS CodePipeline permission to make calls to AWS services on your behalf. * `artifact_store` (Required) An artifact_store block. Artifact stores are documented below. * `stage` (Minimum of at least two `stage` blocks is required) A stage block. Stages are documented below. +* `tags` - (Optional) A mapping of tags to assign to the resource. An `artifact_store` block supports the following arguments: diff --git a/website/docs/r/codepipeline_webhook.markdown b/website/docs/r/codepipeline_webhook.markdown index 071da22e1cf..0798e4fe0f9 100644 --- a/website/docs/r/codepipeline_webhook.markdown +++ b/website/docs/r/codepipeline_webhook.markdown @@ -115,6 +115,7 @@ The following arguments are supported: * `filter` (Required) One or more `filter` blocks. Filter blocks are documented below. * `target_action` - (Required) The name of the action in a pipeline you want to connect to the webhook. The action must be from the source (first) stage of the pipeline. * `target_pipeline` - (Required) The name of the pipeline. +* `tags` - (Optional) A mapping of tags to assign to the resource. An `authentication_configuration` block supports the following arguments: