provider/aws: Elastic Beanstalk Application Version (#5770)

* Added new resource aws_elastic_beanstalk_application_version.

* Changing bucket and key to required.

* Update to use d.Id() directly in DescribeApplicationVersions.

* Checking err to make sure that the application version is successfully deleted.

* Update `version_label` to `Computed: true`.

* provider/aws: Updating to python solution stack

* provider/aws: Beanstalk App Version delete source

The Elastic Beanstalk API call to delete `application_version` resource
should not delete the s3 bundle, as this object is managed by another
Terraform resource

* provider/aws: Update application version docs

* Fix application version test

* Add `version_label` update test

Adds test that fails after rebasing branch onto v0.8.x. `version_label`
changes do not update the `aws_elastic_beanstalk_environment` resource.

* `version_label` changes to update environment

* Prevent unintended delete of `application_version`

Prevents an `application_version` used by multiple environments from
being deleted.

* Add `force_delete` attribute

* Update documentation
This commit is contained in:
David Harris 2017-02-17 08:54:07 -07:00 committed by Paul Stack
parent 5b7e3701cb
commit 2ab6fcc16b
9 changed files with 536 additions and 1 deletions

View File

@ -279,6 +279,7 @@ func Provider() terraform.ResourceProvider {
"aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(), "aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(),
"aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(), "aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(),
"aws_elastic_beanstalk_application": resourceAwsElasticBeanstalkApplication(), "aws_elastic_beanstalk_application": resourceAwsElasticBeanstalkApplication(),
"aws_elastic_beanstalk_application_version": resourceAwsElasticBeanstalkApplicationVersion(),
"aws_elastic_beanstalk_configuration_template": resourceAwsElasticBeanstalkConfigurationTemplate(), "aws_elastic_beanstalk_configuration_template": resourceAwsElasticBeanstalkConfigurationTemplate(),
"aws_elastic_beanstalk_environment": resourceAwsElasticBeanstalkEnvironment(), "aws_elastic_beanstalk_environment": resourceAwsElasticBeanstalkEnvironment(),
"aws_elasticsearch_domain": resourceAwsElasticSearchDomain(), "aws_elasticsearch_domain": resourceAwsElasticSearchDomain(),

View File

@ -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),
}
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 {
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
}

View File

@ -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)
}

View File

@ -67,6 +67,11 @@ func resourceAwsElasticBeanstalkEnvironment() *schema.Resource {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
}, },
"version_label": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"cname": &schema.Schema{ "cname": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
@ -196,6 +201,7 @@ func resourceAwsElasticBeanstalkEnvironmentCreate(d *schema.ResourceData, meta i
tier := d.Get("tier").(string) tier := d.Get("tier").(string)
app := d.Get("application").(string) app := d.Get("application").(string)
desc := d.Get("description").(string) desc := d.Get("description").(string)
version := d.Get("version_label").(string)
settings := d.Get("setting").(*schema.Set) settings := d.Get("setting").(*schema.Set)
solutionStack := d.Get("solution_stack_name").(string) solutionStack := d.Get("solution_stack_name").(string)
templateName := d.Get("template_name").(string) templateName := d.Get("template_name").(string)
@ -245,6 +251,10 @@ func resourceAwsElasticBeanstalkEnvironmentCreate(d *schema.ResourceData, meta i
createOpts.TemplateName = aws.String(templateName) createOpts.TemplateName = aws.String(templateName)
} }
if version != "" {
createOpts.VersionLabel = aws.String(version)
}
// Get the current time to filter describeBeanstalkEvents messages // Get the current time to filter describeBeanstalkEvents messages
t := time.Now() t := time.Now()
log.Printf("[DEBUG] Elastic Beanstalk Environment create opts: %s", createOpts) log.Printf("[DEBUG] Elastic Beanstalk Environment create opts: %s", createOpts)
@ -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 { if hasChange {
// Get the current time to filter describeBeanstalkEvents messages // Get the current time to filter describeBeanstalkEvents messages
t := time.Now() t := time.Now()
@ -489,6 +504,10 @@ func resourceAwsElasticBeanstalkEnvironmentRead(d *schema.ResourceData, meta int
return err return err
} }
if err := d.Set("version_label", env.VersionLabel); err != nil {
return err
}
if err := d.Set("tier", *env.Tier.Name); err != nil { if err := d.Set("tier", *env.Tier.Name); err != nil {
return err return err
} }

View File

@ -263,6 +263,30 @@ func TestAccAWSBeanstalkEnv_basic_settings_update(t *testing.T) {
}) })
} }
func TestAccAWSBeanstalkEnv_version_label(t *testing.T) {
var app elasticbeanstalk.EnvironmentDescription
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckBeanstalkEnvDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccBeanstalkEnvApplicationVersionConfig(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkApplicationVersionDeployed("aws_elastic_beanstalk_environment.default", &app),
),
},
resource.TestStep{
Config: testAccBeanstalkEnvApplicationVersionConfigUpdate(acctest.RandInt()),
Check: resource.ComposeTestCheckFunc(
testAccCheckBeanstalkApplicationVersionDeployed("aws_elastic_beanstalk_environment.default", &app),
),
},
},
})
}
func testAccVerifyBeanstalkConfig(env *elasticbeanstalk.EnvironmentDescription, expected []string) resource.TestCheckFunc { func testAccVerifyBeanstalkConfig(env *elasticbeanstalk.EnvironmentDescription, expected []string) resource.TestCheckFunc {
return func(s *terraform.State) error { return func(s *terraform.State) error {
if env == nil { if env == nil {
@ -445,6 +469,32 @@ func testAccCheckBeanstalkEnvConfigValue(n string, expectedValue string) resourc
} }
} }
func testAccCheckBeanstalkApplicationVersionDeployed(n string, app *elasticbeanstalk.EnvironmentDescription) 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 ENV is not set")
}
env, err := describeBeanstalkEnv(testAccProvider.Meta().(*AWSClient).elasticbeanstalkconn, aws.String(rs.Primary.ID))
if err != nil {
return err
}
if *env.VersionLabel != rs.Primary.Attributes["version_label"] {
return fmt.Errorf("Elastic Beanstalk version deployed %s. Expected %s", *env.VersionLabel, rs.Primary.Attributes["version_label"])
}
*app = *env
return nil
}
}
func describeBeanstalkEnv(conn *elasticbeanstalk.ElasticBeanstalk, func describeBeanstalkEnv(conn *elasticbeanstalk.ElasticBeanstalk,
envID *string) (*elasticbeanstalk.EnvironmentDescription, error) { envID *string) (*elasticbeanstalk.EnvironmentDescription, error) {
describeBeanstalkEnvOpts := &elasticbeanstalk.DescribeEnvironmentsInput{ describeBeanstalkEnvOpts := &elasticbeanstalk.DescribeEnvironmentsInput{
@ -873,3 +923,69 @@ resource "aws_elastic_beanstalk_configuration_template" "template" {
} }
`, r, r, r) `, r, r, r)
} }
func testAccBeanstalkEnvApplicationVersionConfig(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "default" {
bucket = "tftest.applicationversion.buckets-%d"
}
resource "aws_s3_bucket_object" "default" {
bucket = "${aws_s3_bucket.default.id}"
key = "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}"
}
resource "aws_elastic_beanstalk_environment" "default" {
name = "tf-test-name"
application = "${aws_elastic_beanstalk_application.default.name}"
version_label = "${aws_elastic_beanstalk_application_version.default.name}"
solution_stack_name = "64bit Amazon Linux running Python"
}
`, randInt)
}
func testAccBeanstalkEnvApplicationVersionConfigUpdate(randInt int) string {
return fmt.Sprintf(`
resource "aws_s3_bucket" "default" {
bucket = "tftest.applicationversion.buckets-%d"
}
resource "aws_s3_bucket_object" "default" {
bucket = "${aws_s3_bucket.default.id}"
key = "python-v2.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-v2"
bucket = "${aws_s3_bucket.default.id}"
key = "${aws_s3_bucket_object.default.id}"
}
resource "aws_elastic_beanstalk_environment" "default" {
name = "tf-test-name"
application = "${aws_elastic_beanstalk_application.default.name}"
version_label = "${aws_elastic_beanstalk_application_version.default.name}"
solution_stack_name = "64bit Amazon Linux running Python"
}
`, randInt)
}

Binary file not shown.

View File

@ -0,0 +1,71 @@
---
layout: "aws"
page_title: "AWS: aws_elastic_beanstalk_application_version"
sidebar_current: "docs-aws-resource-elastic-beanstalk-application-version"
description: |-
Provides an Elastic Beanstalk Application Version Resource
---
# aws\_elastic\_beanstalk\_application\_<wbr>version
Provides an Elastic Beanstalk Application Version Resource. Elastic Beanstalk allows
you to deploy and manage applications in the AWS cloud without worrying about
the infrastructure that runs those applications.
This resource creates a Beanstalk Application Version that can be deployed to a Beanstalk
Environment.
~> **NOTE on Application Version Resource:** When using the Application Version resource with multiple
[Elastic Beanstalk Environments](elastic_beanstalk_environment.html) it is possible that an error may be returned
when attempting to delete an Application Version while it is still in use by a different environment.
To work around this you can:
<ol>
<li>Create each environment in a separate AWS account</li>
<li>Create your `aws_elastic_beanstalk_application_version` resources with a unique names in your
Elastic Beanstalk Application. For example &lt;revision&gt;-&lt;environment&gt;.</li>
</ol>
## Example Usage
```
resource "aws_s3_bucket" "default" {
bucket = "tftest.applicationversion.bucket"
}
resource "aws_s3_bucket_object" "default" {
bucket = "${aws_s3_bucket.default.id}"
key = "beanstalk/go-v1.zip"
source = "go-v1.zip"
}
resource "aws_elastic_beanstalk_application" "default" {
name = "tf-test-name"
description = "tf-test-desc"
}
resource "aws_elastic_beanstalk_application_version" "default" {
name = "tf-test-version-label"
application = "tf-test-name"
description = "application version created by terraform"
bucket = "${aws_s3_bucket.default.id}"
key = "${aws_s3_bucket_object.default.id}"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) A unique name for the this Application Version.
* `application` - (Required) Name of the Beanstalk Application the version is associated with.
* `description` - (Optional) Short description of the Application Version.
* `bucket` - (Required) S3 bucket that contains the Application Version source bundle.
* `key` - (Required) S3 object that is the Application Version source bundle.
* `force_delete` - (Optional) On delete, force an Application Version to be deleted when it may be in use
by multiple Elastic Beanstalk Environments.
## Attributes Reference
The following attributes are exported:
* `name` - The Application Version name.

View File

@ -59,6 +59,8 @@ off of. Example stacks can be found in the [Amazon API documentation][1]
check if changes have been applied. Use this to adjust the rate of API calls check if changes have been applied. Use this to adjust the rate of API calls
for any `create` or `update` action. Minimum `10s`, maximum `180s`. Omit this to for any `create` or `update` action. Minimum `10s`, maximum `180s`. Omit this to
use the default behavior, which is an exponential backoff 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 * `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 this time the Elastic Beanstalk API does not provide a programatic way of
changing these tags after initial application changing these tags after initial application

View File

@ -606,7 +606,9 @@
<li<%= sidebar_current("docs-aws-resource-elastic-beanstalk-application") %>> <li<%= sidebar_current("docs-aws-resource-elastic-beanstalk-application") %>>
<a href="/docs/providers/aws/r/elastic_beanstalk_application.html">aws_elastic_beanstalk_application</a> <a href="/docs/providers/aws/r/elastic_beanstalk_application.html">aws_elastic_beanstalk_application</a>
</li> </li>
<li<%= sidebar_current("docs-aws-resource-elastic-beanstalk-application-version") %>>
<a href="/docs/providers/aws/r/elastic_beanstalk_application_version.html">aws_elastic_beanstalk_application_version</a>
</li>
<li<%= sidebar_current("docs-aws-resource-elastic-beanstalk-configuration-template") %>> <li<%= sidebar_current("docs-aws-resource-elastic-beanstalk-configuration-template") %>>
<a href="/docs/providers/aws/r/elastic_beanstalk_configuration_template.html">aws_elastic_beanstalk_configuration_template</a> <a href="/docs/providers/aws/r/elastic_beanstalk_configuration_template.html">aws_elastic_beanstalk_configuration_template</a>
</li> </li>