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

Closes #3004: adds support for modifying Beanstalk tags #3513

Merged
merged 4 commits into from
Mar 1, 2018
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
75 changes: 75 additions & 0 deletions aws/resource_aws_elastic_beanstalk_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ func resourceAwsElasticBeanstalkEnvironment() *schema.Resource {
MigrateState: resourceAwsElasticBeanstalkEnvironmentMigrateState,

Schema: map[string]*schema.Schema{
"arn": {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should ensure this new attribute has an associated test of some sort in TestAccAWSBeanstalkEnv_basic:

resource.TestCheckResourceAttrSet("aws_elastic_beanstalk_environment.tfenvtest", "arn"),
# or preferably
resource.TestMatchResourceAttr("aws_elastic_beanstalk_environment.tfenvtest", "arn", regexp.MustCompile(fmt.Sprintf("^arn:[^:]+:elasticbeanstalk:[^:]+:[^:]+:environment/%s/%s$", appName, envName)),

Type: schema.TypeString,
Computed: true,
},
"name": {
Type: schema.TypeString,
Required: true,
Expand Down Expand Up @@ -452,6 +456,63 @@ func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta i
}
}

if d.HasChange("tags") {
o, n := d.GetChange("tags")
oldTags := tagsFromMapBeanstalk(o.(map[string]interface{}))
newTags := tagsFromMapBeanstalk(n.(map[string]interface{}))

tagsToAdd, tagNamesToRemove := diffTagsBeanstalk(oldTags, newTags)

updateTags := elasticbeanstalk.UpdateTagsForResourceInput{
ResourceArn: aws.String(d.Get("arn").(string)),
TagsToAdd: tagsToAdd,
TagsToRemove: tagNamesToRemove,
}

// Get the current time to filter getBeanstalkEnvironmentErrors messages
t := time.Now()
log.Printf("[DEBUG] Elastic Beanstalk Environment update tags: %s", updateTags)
_, err := conn.UpdateTagsForResource(&updateTags)
if err != nil {
return err
}

waitForReadyTimeOut, err := time.ParseDuration(d.Get("wait_for_ready_timeout").(string))
if err != nil {
return err
}
pollInterval, err := time.ParseDuration(d.Get("poll_interval").(string))
if err != nil {
pollInterval = 0
log.Printf("[WARN] Error parsing poll_interval, using default backoff")
}

stateConf := &resource.StateChangeConf{
Pending: []string{"Launching", "Updating"},
Target: []string{"Ready"},
Refresh: environmentStateRefreshFunc(conn, d.Id(), t),
Timeout: waitForReadyTimeOut,
Delay: 10 * time.Second,
PollInterval: pollInterval,
MinTimeout: 3 * time.Second,
}

_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf(
"Error waiting for Elastic Beanstalk Environment (%s) to become ready: %s",
d.Id(), err)
}

envErrors, err := getBeanstalkEnvironmentErrors(conn, d.Id(), t)
if err != nil {
return err
}
if envErrors != nil {
return envErrors
}
}

return resourceAwsElasticBeanstalkEnvironmentRead(d, meta)
}

Expand Down Expand Up @@ -496,6 +557,8 @@ func resourceAwsElasticBeanstalkEnvironmentRead(d *schema.ResourceData, meta int
return err
}

d.Set("arn", env.EnvironmentArn)

if err := d.Set("name", env.EnvironmentName); err != nil {
return err
}
Expand Down Expand Up @@ -564,6 +627,18 @@ func resourceAwsElasticBeanstalkEnvironmentRead(d *schema.ResourceData, meta int
return err
}

tags, err := conn.ListTagsForResource(&elasticbeanstalk.ListTagsForResourceInput{
ResourceArn: aws.String(d.Get("arn").(string)),
})

if err != nil {
return err
}

if err := d.Set("tags", tagsToMapBeanstalk(tags.ResourceTags)); err != nil {
return err
}

return resourceAwsElasticBeanstalkEnvironmentSettingsRead(d, meta)
}

Expand Down
98 changes: 98 additions & 0 deletions aws/resource_aws_elastic_beanstalk_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,14 @@ func TestAccAWSBeanstalkEnv_basic(t *testing.T) {
Config: testAccBeanstalkEnvConfig(appName, envName),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.tfenvtest", &app),
resource.TestMatchResourceAttr(
"aws_elastic_beanstalk_environment.tfenvtest", "arn",
regexp.MustCompile(fmt.Sprintf("^arn:[^:]+:elasticbeanstalk:[^:]+:[^:]+:environment/%s/%s$", appName, envName))),
),
},
},
})

}

func TestAccAWSBeanstalkEnv_tier(t *testing.T) {
Expand Down Expand Up @@ -283,6 +287,53 @@ func TestAccAWSBeanstalkEnv_resource(t *testing.T) {
})
}

func TestAccAWSBeanstalkEnv_tags(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is currently failing with:

=== RUN   TestAccAWSBeanstalkEnv_tags
--- FAIL: TestAccAWSBeanstalkEnv_tags (474.54s)
    testing.go:513: Step 0 error: Check failed: Check 1/1 error: InvalidParameter: 1 validation error(s) found.
        - missing required field, ListTagsForResourceInput.ResourceArn.

The first test step needs to include this as the first check so app is correctly populated: testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.tfenvtest", &app),

FYI you can run the acceptance test locally with make testacc TEST=./aws TESTARGS='-run=TestAccAWSBeanstalkEnv_tags to verify

var app elasticbeanstalk.EnvironmentDescription

rString := acctest.RandString(8)
appName := fmt.Sprintf("tf_acc_app_env_resource_%s", rString)
envName := fmt.Sprintf("tf-acc-env-resource-%s", rString)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckBeanstalkEnvDestroy,
Steps: []resource.TestStep{
{
Config: testAccBeanstalkEnvConfig_empty_settings(appName, envName),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.tfenvtest", &app),
testAccCheckBeanstalkEnvTagsMatch(&app, map[string]string{}),
),
},

{
Config: testAccBeanstalkTagsTemplate(appName, envName, "test1", "test2"),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.tfenvtest", &app),
testAccCheckBeanstalkEnvTagsMatch(&app, map[string]string{"firstTag": "test1", "secondTag": "test2"}),
),
},

{
Config: testAccBeanstalkTagsTemplate(appName, envName, "test2", "test1"),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.tfenvtest", &app),
testAccCheckBeanstalkEnvTagsMatch(&app, map[string]string{"firstTag": "test2", "secondTag": "test1"}),
),
},

{
Config: testAccBeanstalkEnvConfig_empty_settings(appName, envName),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkEnvExists("aws_elastic_beanstalk_environment.tfenvtest", &app),
testAccCheckBeanstalkEnvTagsMatch(&app, map[string]string{}),
),
},
},
})
}

func TestAccAWSBeanstalkEnv_vpc(t *testing.T) {
var app elasticbeanstalk.EnvironmentDescription

Expand Down Expand Up @@ -629,6 +680,32 @@ func testAccCheckBeanstalkEnvConfigValue(n string, expectedValue string) resourc
}
}

func testAccCheckBeanstalkEnvTagsMatch(env *elasticbeanstalk.EnvironmentDescription, expectedValue map[string]string) resource.TestCheckFunc {
return func(s *terraform.State) error {
if env == nil {
return fmt.Errorf("Nil environment in testAccCheckBeanstalkEnvTagsMatch")
}

conn := testAccProvider.Meta().(*AWSClient).elasticbeanstalkconn

tags, err := conn.ListTagsForResource(&elasticbeanstalk.ListTagsForResourceInput{
ResourceArn: env.EnvironmentArn,
})

if err != nil {
return err
}

foundTags := tagsToMapBeanstalk(tags.ResourceTags)

if !reflect.DeepEqual(foundTags, expectedValue) {
return fmt.Errorf("Tag value: %s. Expected %s", foundTags, expectedValue)
}

return nil
}
}

func testAccCheckBeanstalkApplicationVersionDeployed(n string, app *elasticbeanstalk.EnvironmentDescription) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
Expand Down Expand Up @@ -933,6 +1010,27 @@ resource "aws_elastic_beanstalk_environment" "tfenvtest" {
}`, appName, envName)
}

func testAccBeanstalkTagsTemplate(appName, envName, firstTag, secondTag string) string {
return fmt.Sprintf(`
resource "aws_elastic_beanstalk_application" "tftest" {
name = "%s"
description = "tf-test-desc"
}

resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "%s"
application = "${aws_elastic_beanstalk_application.tftest.name}"
solution_stack_name = "64bit Amazon Linux running Python"

wait_for_ready_timeout = "15m"

tags {
firstTag = "%s"
secondTag = "%s"
}
}`, appName, envName, firstTag, secondTag)
}

func testAccBeanstalkEnv_VPC(sgName, appName, envName string) string {
return fmt.Sprintf(`
resource "aws_vpc" "tf_b_test" {
Expand Down
11 changes: 5 additions & 6 deletions aws/tagsBeanstalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,19 @@ import (
// 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 diffTagsBeanstalk(oldTags, newTags []*elasticbeanstalk.Tag) ([]*elasticbeanstalk.Tag, []*elasticbeanstalk.Tag) {
func diffTagsBeanstalk(oldTags, newTags []*elasticbeanstalk.Tag) ([]*elasticbeanstalk.Tag, []*string) {
// 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 []*elasticbeanstalk.Tag
var remove []*string
for _, t := range oldTags {
old, ok := create[*t.Key]
if !ok || old != *t.Value {
if _, ok := create[*t.Key]; !ok {
// Delete it!
remove = append(remove, t)
remove = append(remove, t.Key)
}
}

Expand Down Expand Up @@ -62,7 +61,7 @@ func tagsToMapBeanstalk(ts []*elasticbeanstalk.Tag) map[string]string {
// compare a tag against a list of strings and checks if it should
// be ignored or not
func tagIgnoredBeanstalk(t *elasticbeanstalk.Tag) bool {
filter := []string{"^aws:"}
filter := []string{"^aws:", "^elasticbeanstalk:", "Name"}
for _, v := range filter {
log.Printf("[DEBUG] Matching %v with %v\n", v, *t.Key)
if r, _ := regexp.MatchString(v, *t.Key); r == true {
Expand Down
22 changes: 12 additions & 10 deletions aws/tagsBeanstalk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (

func TestDiffBeanstalkTags(t *testing.T) {
cases := []struct {
Old, New map[string]interface{}
Create, Remove map[string]string
Old, New map[string]interface{}
Create map[string]string
Remove []string
}{
// Basic add/remove
{
Expand All @@ -27,8 +28,8 @@ func TestDiffBeanstalkTags(t *testing.T) {
Create: map[string]string{
"bar": "baz",
},
Remove: map[string]string{
"foo": "bar",
Remove: []string{
"foo",
},
},

Expand All @@ -43,21 +44,22 @@ func TestDiffBeanstalkTags(t *testing.T) {
Create: map[string]string{
"foo": "baz",
},
Remove: map[string]string{
"foo": "bar",
},
Remove: []string{},
},
}

for i, tc := range cases {
c, r := diffTagsBeanstalk(tagsFromMapBeanstalk(tc.Old), tagsFromMapBeanstalk(tc.New))
cm := tagsToMapBeanstalk(c)
rm := tagsToMapBeanstalk(r)
rl := []string{}
for _, tagName := range r {
rl = append(rl, *tagName)
}
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)
if !reflect.DeepEqual(rl, tc.Remove) {
t.Fatalf("%d: bad remove: %#v", i, rl)
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions website/docs/r/elastic_beanstalk_environment.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ for any `create` or `update` action. Minimum `10s`, maximum `180s`. Omit this to
use the default behavior, which is an exponential backoff
* `version_label` - (Optional) The name of the Elastic Beanstalk Application Version
to use in deployment.
* `tags` – (Optional) A set of tags to apply to the Environment. **Note:** at
this time the Elastic Beanstalk API does not provide a programatic way of
changing these tags after initial application
* `tags` – (Optional) A set of tags to apply to the Environment.


## Option Settings
Expand Down