diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain.go b/builtin/providers/aws/resource_aws_elasticsearch_domain.go index c5666424b..d09cec4f4 100644 --- a/builtin/providers/aws/resource_aws_elasticsearch_domain.go +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain.go @@ -37,13 +37,9 @@ func resourceAwsElasticSearchDomain() *schema.Resource { ForceNew: true, ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { value := v.(string) - if !regexp.MustCompile(`^[0-9A-Za-z]+`).MatchString(value) { + if !regexp.MustCompile(`^[a-z][0-9a-z\-]{2,27}$`).MatchString(value) { errors = append(errors, fmt.Errorf( - "%q must start with a letter or number", k)) - } - if !regexp.MustCompile(`^[0-9A-Za-z][0-9a-z-]+$`).MatchString(value) { - errors = append(errors, fmt.Errorf( - "%q can only contain lowercase characters, numbers and hyphens", k)) + "%q must start with a lowercase alphabet and be at least 3 and no more than 28 characters long. Valid characters are a-z (lowercase letters), 0-9, and - (hyphen).", k)) } return }, @@ -133,6 +129,7 @@ func resourceAwsElasticSearchDomain() *schema.Resource { }, }, }, + "tags": tagsSchema(), }, } } @@ -228,6 +225,16 @@ func resourceAwsElasticSearchDomainCreate(d *schema.ResourceData, meta interface return err } + tags := tagsFromMapElasticsearchService(d.Get("tags").(map[string]interface{})) + + if err := setTagsElasticsearchService(conn, d, *out.DomainStatus.ARN); err != nil { + return err + } + + d.Set("tags", tagsToMapElasticsearchService(tags)) + d.SetPartial("tags") + d.Partial(false) + log.Printf("[DEBUG] ElasticSearch domain %q created", d.Id()) return resourceAwsElasticSearchDomainRead(d, meta) @@ -276,12 +283,32 @@ func resourceAwsElasticSearchDomainRead(d *schema.ResourceData, meta interface{} d.Set("arn", *ds.ARN) + listOut, err := conn.ListTags(&elasticsearch.ListTagsInput{ + ARN: ds.ARN, + }) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Received ElasticSearch tags: %s", out) + + d.Set("tags", listOut) + return nil } func resourceAwsElasticSearchDomainUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).esconn + d.Partial(true) + + if err := setTagsElasticsearchService(conn, d, d.Id()); err != nil { + return err + } else { + d.SetPartial("tags") + } + input := elasticsearch.UpdateElasticsearchDomainConfigInput{ DomainName: aws.String(d.Get("domain_name").(string)), } @@ -355,6 +382,8 @@ func resourceAwsElasticSearchDomainUpdate(d *schema.ResourceData, meta interface return err } + d.Partial(false) + return resourceAwsElasticSearchDomainRead(d, meta) } diff --git a/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go b/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go index e17c0c0e8..8c960f5b2 100644 --- a/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go +++ b/builtin/providers/aws/resource_aws_elasticsearch_domain_test.go @@ -20,7 +20,7 @@ func TestAccAWSElasticSearchDomain_basic(t *testing.T) { CheckDestroy: testAccCheckESDomainDestroy, Steps: []resource.TestStep{ resource.TestStep{ - Config: testAccESDomainConfig_basic, + Config: testAccESDomainConfig, Check: resource.ComposeTestCheckFunc( testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), ), @@ -47,6 +47,55 @@ func TestAccAWSElasticSearchDomain_complex(t *testing.T) { }) } +func TestAccAWSElasticSearch_tags(t *testing.T) { + var domain elasticsearch.ElasticsearchDomainStatus + var td elasticsearch.ListTagsOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSELBDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccESDomainConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + testAccLoadESTags(&domain, &td), + testAccCheckElasticsearchServiceTags(&td.TagList, "bar", "baz"), + ), + }, + + resource.TestStep{ + Config: testAccESDomainConfig_TagUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckESDomainExists("aws_elasticsearch_domain.example", &domain), + testAccLoadESTags(&domain, &td), + testAccCheckElasticsearchServiceTags(&td.TagList, "foo", "bar"), + testAccCheckElasticsearchServiceTags(&td.TagList, "new", "type"), + ), + }, + }, + }) +} + +func testAccLoadESTags(conf *elasticsearch.ElasticsearchDomainStatus, td *elasticsearch.ListTagsOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).esconn + + describe, err := conn.ListTags(&elasticsearch.ListTagsInput{ + ARN: conf.ARN, + }) + + if err != nil { + return err + } + if len(describe.TagList) > 0 { + *td = *describe + } + return nil + } +} + func testAccCheckESDomainExists(n string, domain *elasticsearch.ElasticsearchDomainStatus) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -97,31 +146,50 @@ func testAccCheckESDomainDestroy(s *terraform.State) error { return nil } -const testAccESDomainConfig_basic = ` +const testAccESDomainConfig = ` resource "aws_elasticsearch_domain" "example" { - domain_name = "tf-test-1" + domain_name = "tf-test-1" + + tags { + bar = "baz" + } +} +` + +const testAccESDomainConfig_TagUpdate = ` +resource "aws_elasticsearch_domain" "example" { + domain_name = "tf-test-1" + + tags { + foo = "bar" + new = "type" + } } ` const testAccESDomainConfig_complex = ` resource "aws_elasticsearch_domain" "example" { - domain_name = "tf-test-2" + domain_name = "tf-test-2" - advanced_options { - "indices.fielddata.cache.size" = 80 - } + advanced_options { + "indices.fielddata.cache.size" = 80 + } - ebs_options { - ebs_enabled = false - } + ebs_options { + ebs_enabled = false + } - cluster_config { - instance_count = 2 - zone_awareness_enabled = true - } + cluster_config { + instance_count = 2 + zone_awareness_enabled = true + } - snapshot_options { - automated_snapshot_start_hour = 23 - } + snapshot_options { + automated_snapshot_start_hour = 23 + } + + tags { + bar = "complex" + } } ` diff --git a/builtin/providers/aws/tags_elasticsearchservice.go b/builtin/providers/aws/tags_elasticsearchservice.go new file mode 100644 index 000000000..9fc664343 --- /dev/null +++ b/builtin/providers/aws/tags_elasticsearchservice.go @@ -0,0 +1,94 @@ +package aws + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "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 setTagsElasticsearchService(conn *elasticsearch.ElasticsearchService, d *schema.ResourceData, arn string) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsElasticsearchService(tagsFromMapElasticsearchService(o), tagsFromMapElasticsearchService(n)) + + // Set tags + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + k := make([]*string, 0, len(remove)) + for _, t := range remove { + k = append(k, t.Key) + } + _, err := conn.RemoveTags(&elasticsearch.RemoveTagsInput{ + ARN: aws.String(arn), + TagKeys: k, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + _, err := conn.AddTags(&elasticsearch.AddTagsInput{ + ARN: aws.String(arn), + TagList: 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 diffTagsElasticsearchService(oldTags, newTags []*elasticsearch.Tag) ([]*elasticsearch.Tag, []*elasticsearch.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 []*elasticsearch.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapElasticsearchService(create), remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapElasticsearchService(m map[string]interface{}) []*elasticsearch.Tag { + var result []*elasticsearch.Tag + for k, v := range m { + result = append(result, &elasticsearch.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +// tagsToMap turns the list of tags into a map. +func tagsToMapElasticsearchService(ts []*elasticsearch.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/tags_elasticsearchservice_test.go b/builtin/providers/aws/tags_elasticsearchservice_test.go new file mode 100644 index 000000000..3d367ed9a --- /dev/null +++ b/builtin/providers/aws/tags_elasticsearchservice_test.go @@ -0,0 +1,85 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + elasticsearch "github.com/aws/aws-sdk-go/service/elasticsearchservice" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestDiffElasticsearchServiceTags(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 := diffTagsElasticsearchService(tagsFromMapElasticsearchService(tc.Old), tagsFromMapElasticsearchService(tc.New)) + cm := tagsToMapElasticsearchService(c) + rm := tagsToMapElasticsearchService(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) + } + } +} + +// testAccCheckTags can be used to check the tags on a resource. +func testAccCheckElasticsearchServiceTags( + ts *[]*elasticsearch.Tag, key string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + m := tagsToMapElasticsearchService(*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 + } +}