Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS - Tag support for Elasticsearch #4973

Merged
merged 2 commits into from
Feb 17, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions builtin/providers/aws/resource_aws_elasticsearch_domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paultyng why has this ValidateFunc changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are the actual rules for es domain names, I had it failing in the API call locally instead of in validation if I didn't meet those. Let me dig up the docs, one sec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http://docs.aws.amazon.com/sdk-for-go/api/service/elasticsearchservice.html#type-CreateElasticsearchDomainInput

Domain names must start with a letter or number and can contain the following characters: a-z (lowercase), 0-9, and - (hyphen).

The text from the actual console is:

The name 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).

And when I try to start with a number it doesn't work.

Feel free to cherry pick around this, or I can rip it out, I just hit this bug as well.

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
},
Expand Down Expand Up @@ -133,6 +129,7 @@ func resourceAwsElasticSearchDomain() *schema.Resource {
},
},
},
"tags": tagsSchema(),
},
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)),
}
Expand Down Expand Up @@ -355,6 +382,8 @@ func resourceAwsElasticSearchDomainUpdate(d *schema.ResourceData, meta interface
return err
}

d.Partial(false)

return resourceAwsElasticSearchDomainRead(d, meta)
}

Expand Down
102 changes: 85 additions & 17 deletions builtin/providers/aws/resource_aws_elasticsearch_domain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
),
Expand All @@ -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]
Expand Down Expand Up @@ -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"
}
}
`
94 changes: 94 additions & 0 deletions builtin/providers/aws/tags_elasticsearchservice.go
Original file line number Diff line number Diff line change
@@ -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
}
85 changes: 85 additions & 0 deletions builtin/providers/aws/tags_elasticsearchservice_test.go
Original file line number Diff line number Diff line change
@@ -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
}
}