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

provider/aws: Elastic Beanstalk Application Version #5770

Merged
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
1 change: 1 addition & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ func Provider() terraform.ResourceProvider {
"aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(),
"aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(),
"aws_elastic_beanstalk_application": resourceAwsElasticBeanstalkApplication(),
"aws_elastic_beanstalk_application_version": resourceAwsElasticBeanstalkApplicationVersion(),
"aws_elastic_beanstalk_configuration_template": resourceAwsElasticBeanstalkConfigurationTemplate(),
"aws_elastic_beanstalk_environment": resourceAwsElasticBeanstalkEnvironment(),
"aws_elasticsearch_domain": resourceAwsElasticSearchDomain(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package aws

import (
"fmt"
"log"

"github.com/hashicorp/terraform/helper/schema"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elasticbeanstalk"
"time"
)

func resourceAwsElasticBeanstalkApplicationVersion() *schema.Resource {
return &schema.Resource{
Create: resourceAwsElasticBeanstalkApplicationVersionCreate,
Read: resourceAwsElasticBeanstalkApplicationVersionRead,
Update: resourceAwsElasticBeanstalkApplicationVersionUpdate,
Delete: resourceAwsElasticBeanstalkApplicationVersionDelete,

Schema: map[string]*schema.Schema{
"application": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"bucket": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"key": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"force_delete": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
}
}

func resourceAwsElasticBeanstalkApplicationVersionCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticbeanstalkconn

application := d.Get("application").(string)
description := d.Get("description").(string)
bucket := d.Get("bucket").(string)
key := d.Get("key").(string)
name := d.Get("name").(string)

s3Location := elasticbeanstalk.S3Location{
S3Bucket: aws.String(bucket),
S3Key: aws.String(key),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

If bucket is optional, what happens here if the User omits it from the config? I assume a failure in the CreateApplicationVersion call. We need to conditionally added this, if that is the case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to make bucket and key required for the reasons discussed below on the documentation note.


createOpts := elasticbeanstalk.CreateApplicationVersionInput{
ApplicationName: aws.String(application),
Description: aws.String(description),
SourceBundle: &s3Location,
VersionLabel: aws.String(name),
}

log.Printf("[DEBUG] Elastic Beanstalk Application Version create opts: %s", createOpts)
_, err := conn.CreateApplicationVersion(&createOpts)
if err != nil {
return err
}

d.SetId(name)
log.Printf("[INFO] Elastic Beanstalk Application Version Label: %s", name)

return resourceAwsElasticBeanstalkApplicationVersionRead(d, meta)
}

func resourceAwsElasticBeanstalkApplicationVersionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticbeanstalkconn

resp, err := conn.DescribeApplicationVersions(&elasticbeanstalk.DescribeApplicationVersionsInput{
VersionLabels: []*string{aws.String(d.Id())},
})

if err != nil {
return err
}

if len(resp.ApplicationVersions) == 0 {
log.Printf("[DEBUG] Elastic Beanstalk application version read: application version not found")

d.SetId("")

return nil
} else if len(resp.ApplicationVersions) != 1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious, it's my understanding that users will have a unique aws_elastic_beanstalk_application_version per version, right?

If I name one version test-version and another test-version2, will the conn.DescribeApplicationVersions just return one of them, if I give it test-version? If so, then this is fine, but I'm wondering if it will return both here. Do you know by chance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In that case test-version would be the only application version returned. In this context it should only ever return one version, if not something is very wrong 😄

As for your first question here, it would either be a unique application version per version or per release. In thinking about this question I realized that in its current state this may not work in some cases. I'm going to add a comment describing this a bit more in the pr, as it may take a bit of work to fix.

return fmt.Errorf("Error reading application version properties: found %d application versions, expected 1", len(resp.ApplicationVersions))
}

if err := d.Set("description", resp.ApplicationVersions[0].Description); err != nil {
return err
}

return nil
}

func resourceAwsElasticBeanstalkApplicationVersionUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticbeanstalkconn

if d.HasChange("description") {
if err := resourceAwsElasticBeanstalkApplicationVersionDescriptionUpdate(conn, d); err != nil {
return err
}
}

return resourceAwsElasticBeanstalkApplicationVersionRead(d, meta)

}

func resourceAwsElasticBeanstalkApplicationVersionDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticbeanstalkconn

application := d.Get("application").(string)
name := d.Id()

if d.Get("force_delete").(bool) == false {
environments, err := versionUsedBy(application, name, conn)
if err != nil {
return err
}

if len(environments) > 1 {
return fmt.Errorf("Unable to delete Application Version, it is currently in use by the following environments: %s.", environments)
}
}
_, err := conn.DeleteApplicationVersion(&elasticbeanstalk.DeleteApplicationVersionInput{
ApplicationName: aws.String(application),
VersionLabel: aws.String(name),
DeleteSourceBundle: aws.Bool(false),
})

if err != nil {
if awserr, ok := err.(awserr.Error); ok {
// application version is pending delete, or no longer exists.
if awserr.Code() == "InvalidParameterValue" {
d.SetId("")
return nil
}
}
return err
}

d.SetId("")
return nil
}

func resourceAwsElasticBeanstalkApplicationVersionDescriptionUpdate(conn *elasticbeanstalk.ElasticBeanstalk, d *schema.ResourceData) error {
application := d.Get("application").(string)
description := d.Get("description").(string)
name := d.Get("name").(string)

log.Printf("[DEBUG] Elastic Beanstalk application version: %s, update description: %s", name, description)

_, err := conn.UpdateApplicationVersion(&elasticbeanstalk.UpdateApplicationVersionInput{
ApplicationName: aws.String(application),
Description: aws.String(description),
VersionLabel: aws.String(name),
})

return err
}

func versionUsedBy(applicationName, versionLabel string, conn *elasticbeanstalk.ElasticBeanstalk) ([]string, error) {
now := time.Now()
resp, err := conn.DescribeEnvironments(&elasticbeanstalk.DescribeEnvironmentsInput{
ApplicationName: aws.String(applicationName),
VersionLabel: aws.String(versionLabel),
IncludeDeleted: aws.Bool(true),
IncludedDeletedBackTo: aws.Time(now.Add(-1 * time.Minute)),
})

if err != nil {
return nil, err
}

var environmentIDs []string
for _, environment := range resp.Environments {
environmentIDs = append(environmentIDs, *environment.EnvironmentId)
}

return environmentIDs, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package aws

import (
"fmt"
"log"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elasticbeanstalk"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccAWSBeanstalkAppVersion_basic(t *testing.T) {

var appVersion elasticbeanstalk.ApplicationVersionDescription

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckApplicationVersionDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccBeanstalkApplicationVersionConfig(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckApplicationVersionExists("aws_elastic_beanstalk_application_version.default", &appVersion),
),
},
},
})
}

func testAccCheckApplicationVersionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).elasticbeanstalkconn

for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_elastic_beanstalk_application_version" {
continue
}

describeApplicationVersionOpts := &elasticbeanstalk.DescribeApplicationVersionsInput{
VersionLabels: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribeApplicationVersions(describeApplicationVersionOpts)
if err == nil {
if len(resp.ApplicationVersions) > 0 {
return fmt.Errorf("Elastic Beanstalk Application Verson still exists.")
}

return nil
}
ec2err, ok := err.(awserr.Error)
if !ok {
return err
}
if ec2err.Code() != "InvalidParameterValue" {
return err
}
}

return nil
}

func testAccCheckApplicationVersionExists(n string, app *elasticbeanstalk.ApplicationVersionDescription) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}

if rs.Primary.ID == "" {
return fmt.Errorf("Elastic Beanstalk Application Version is not set")
}

conn := testAccProvider.Meta().(*AWSClient).elasticbeanstalkconn
describeApplicationVersionOpts := &elasticbeanstalk.DescribeApplicationVersionsInput{
VersionLabels: []*string{aws.String(rs.Primary.ID)},
}

log.Printf("[DEBUG] Elastic Beanstalk Application Version TEST describe opts: %s", describeApplicationVersionOpts)

resp, err := conn.DescribeApplicationVersions(describeApplicationVersionOpts)
if err != nil {
return err
}
if len(resp.ApplicationVersions) == 0 {
return fmt.Errorf("Elastic Beanstalk Application Version not found.")
}

*app = *resp.ApplicationVersions[0]

return nil
}
}

func testAccBeanstalkApplicationVersionConfig(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "default" {
bucket = "tftest.applicationversion.bucket-%d"
}

resource "aws_s3_bucket_object" "default" {
bucket = "${aws_s3_bucket.default.id}"
key = "beanstalk/python-v1.zip"
source = "test-fixtures/python-v1.zip"
}

resource "aws_elastic_beanstalk_application" "default" {
name = "tf-test-name"
description = "tf-test-desc"
}

resource "aws_elastic_beanstalk_application_version" "default" {
application = "tf-test-name"
name = "tf-test-version-label"
bucket = "${aws_s3_bucket.default.id}"
key = "${aws_s3_bucket_object.default.id}"
}
`, randInt)
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func resourceAwsElasticBeanstalkEnvironment() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"version_label": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Copy link
Contributor

Choose a reason for hiding this comment

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

When you apply your example above, does this field show up in future diffs? It looks like it would need to be Computed: true as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe it does, at least when I change the value in my Terraform document, it is aware of the change when running terraform plan

Related to this, I did see a case where the version field wasn't being managed by Terraform. Subsequent plans would show the value changing from the external value to "". To work around that ignore_changes was used for that field. Changing Computed: true also fixed the issue (and removed the need for ignore_changes), but I wasn't sure if that was the intended behavior of that option.

Not sure I completely understand the purpose of Computed yet. Based on the comments for Computed and some of the documentation (https://www.terraform.io/intro/getting-started/build.html) I thought it was for values that wouldn't be known until the resource was created. In this case we are providing the value in the Terraform document, so why would it be computed?

Computed: true,
},
"cname": &schema.Schema{
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -196,6 +201,7 @@ func resourceAwsElasticBeanstalkEnvironmentCreate(d *schema.ResourceData, meta i
tier := d.Get("tier").(string)
app := d.Get("application").(string)
desc := d.Get("description").(string)
version := d.Get("version_label").(string)
settings := d.Get("setting").(*schema.Set)
solutionStack := d.Get("solution_stack_name").(string)
templateName := d.Get("template_name").(string)
Expand Down Expand Up @@ -245,6 +251,10 @@ func resourceAwsElasticBeanstalkEnvironmentCreate(d *schema.ResourceData, meta i
createOpts.TemplateName = aws.String(templateName)
}

if version != "" {
createOpts.VersionLabel = aws.String(version)
}

// Get the current time to filter describeBeanstalkEvents messages
t := time.Now()
log.Printf("[DEBUG] Elastic Beanstalk Environment create opts: %s", createOpts)
Expand Down Expand Up @@ -387,6 +397,11 @@ func resourceAwsElasticBeanstalkEnvironmentUpdate(d *schema.ResourceData, meta i
}
}

if d.HasChange("version_label") {
hasChange = true
updateOpts.VersionLabel = aws.String(d.Get("version_label").(string))
}

if hasChange {
// Get the current time to filter describeBeanstalkEvents messages
t := time.Now()
Expand Down Expand Up @@ -489,6 +504,10 @@ func resourceAwsElasticBeanstalkEnvironmentRead(d *schema.ResourceData, meta int
return err
}

if err := d.Set("version_label", env.VersionLabel); err != nil {
return err
}

if err := d.Set("tier", *env.Tier.Name); err != nil {
return err
}
Expand Down
Loading