diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 308d01a27..dca9a0c0e 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -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(), diff --git a/builtin/providers/aws/resource_aws_elastic_beanstalk_application_version.go b/builtin/providers/aws/resource_aws_elastic_beanstalk_application_version.go new file mode 100644 index 000000000..9125225a3 --- /dev/null +++ b/builtin/providers/aws/resource_aws_elastic_beanstalk_application_version.go @@ -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 +} diff --git a/builtin/providers/aws/resource_aws_elastic_beanstalk_application_version_test.go b/builtin/providers/aws/resource_aws_elastic_beanstalk_application_version_test.go new file mode 100644 index 000000000..c7bfe97c6 --- /dev/null +++ b/builtin/providers/aws/resource_aws_elastic_beanstalk_application_version_test.go @@ -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) +} diff --git a/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go b/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go index 114056166..0d0548ad4 100644 --- a/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go +++ b/builtin/providers/aws/resource_aws_elastic_beanstalk_environment.go @@ -67,6 +67,11 @@ func resourceAwsElasticBeanstalkEnvironment() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "version_label": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, "cname": &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -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) @@ -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) @@ -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() @@ -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 } diff --git a/builtin/providers/aws/resource_aws_elastic_beanstalk_environment_test.go b/builtin/providers/aws/resource_aws_elastic_beanstalk_environment_test.go index f0bf0bdb0..305ab9c4a 100644 --- a/builtin/providers/aws/resource_aws_elastic_beanstalk_environment_test.go +++ b/builtin/providers/aws/resource_aws_elastic_beanstalk_environment_test.go @@ -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 { return func(s *terraform.State) error { 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, envID *string) (*elasticbeanstalk.EnvironmentDescription, error) { describeBeanstalkEnvOpts := &elasticbeanstalk.DescribeEnvironmentsInput{ @@ -873,3 +923,69 @@ resource "aws_elastic_beanstalk_configuration_template" "template" { } `, 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) +} diff --git a/builtin/providers/aws/test-fixtures/python-v1.zip b/builtin/providers/aws/test-fixtures/python-v1.zip new file mode 100644 index 000000000..f7d68b1fc Binary files /dev/null and b/builtin/providers/aws/test-fixtures/python-v1.zip differ diff --git a/website/source/docs/providers/aws/r/elastic_beanstalk_application_version.html.markdown b/website/source/docs/providers/aws/r/elastic_beanstalk_application_version.html.markdown new file mode 100644 index 000000000..08683de12 --- /dev/null +++ b/website/source/docs/providers/aws/r/elastic_beanstalk_application_version.html.markdown @@ -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\_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: +
    +
  1. Create each environment in a separate AWS account
  2. +
  3. Create your `aws_elastic_beanstalk_application_version` resources with a unique names in your +Elastic Beanstalk Application. For example <revision>-<environment>.
  4. +
+ +## 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. diff --git a/website/source/docs/providers/aws/r/elastic_beanstalk_environment.html.markdown b/website/source/docs/providers/aws/r/elastic_beanstalk_environment.html.markdown index 9970ca821..17c3df326 100644 --- a/website/source/docs/providers/aws/r/elastic_beanstalk_environment.html.markdown +++ b/website/source/docs/providers/aws/r/elastic_beanstalk_environment.html.markdown @@ -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 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 diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index cb3bb07e6..b362270ff 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -606,7 +606,9 @@ > aws_elastic_beanstalk_application - + > + aws_elastic_beanstalk_application_version + > aws_elastic_beanstalk_configuration_template