diff --git a/.changelog/18564.txt b/.changelog/18564.txt new file mode 100644 index 00000000000..33d6bc7eec0 --- /dev/null +++ b/.changelog/18564.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_codedeploy_app: Add `arn`, `linked_to_github`, `github_account_name`, `application_id` attributes +``` + +```release-note:enhancement +resource/aws_codedeploy_app: Add `tags` argument +``` + +```release-note:enhancement +resource/aws_codedeploy_app: Add plan time validation for `name` +``` \ No newline at end of file diff --git a/aws/resource_aws_codedeploy_app.go b/aws/resource_aws_codedeploy_app.go index 251ab724852..d57cf3f37e4 100644 --- a/aws/resource_aws_codedeploy_app.go +++ b/aws/resource_aws_codedeploy_app.go @@ -6,10 +6,11 @@ import ( "strings" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/codedeploy" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) func resourceAwsCodeDeployApp() *schema.Resource { @@ -52,30 +53,36 @@ func resourceAwsCodeDeployApp() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "name": { + "arn": { Type: schema.TypeString, - Required: true, - ForceNew: true, + Computed: true, }, - - "compute_platform": { + "application_id": { Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateFunc: validation.StringInSlice([]string{ - codedeploy.ComputePlatformEcs, - codedeploy.ComputePlatformLambda, - codedeploy.ComputePlatformServer, - }, false), - Default: codedeploy.ComputePlatformServer, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 100), }, - // The unique ID is set by AWS on create. - "unique_id": { + "compute_platform": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(codedeploy.ComputePlatform_Values(), false), + Default: codedeploy.ComputePlatformServer, + }, + "github_account_name": { Type: schema.TypeString, - Optional: true, Computed: true, }, + "linked_to_github": { + Type: schema.TypeBool, + Computed: true, + }, + "tags": tagsSchema(), }, } } @@ -90,6 +97,7 @@ func resourceAwsCodeDeployAppCreate(d *schema.ResourceData, meta interface{}) er resp, err := conn.CreateApplication(&codedeploy.CreateApplicationInput{ ApplicationName: aws.String(application), ComputePlatform: aws.String(computePlatform), + Tags: keyvaluetags.New(d.Get("tags").(map[string]interface{})).IgnoreAws().CodedeployTags(), }) if err != nil { return err @@ -101,31 +109,66 @@ func resourceAwsCodeDeployAppCreate(d *schema.ResourceData, meta interface{}) er // the state file. This allows us to reliably detect both when the TF // config file changes and when the user deletes the app without removing // it first from the TF config. - d.SetId(fmt.Sprintf("%s:%s", *resp.ApplicationId, application)) + d.SetId(fmt.Sprintf("%s:%s", aws.StringValue(resp.ApplicationId), application)) return resourceAwsCodeDeployAppRead(d, meta) } func resourceAwsCodeDeployAppRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).codedeployconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig application := resourceAwsCodeDeployAppParseId(d.Id()) + name := d.Get("name").(string) + if name != "" && application != name { + application = name + } log.Printf("[DEBUG] Reading CodeDeploy application %s", application) resp, err := conn.GetApplication(&codedeploy.GetApplicationInput{ ApplicationName: aws.String(application), }) if err != nil { - if codedeployerr, ok := err.(awserr.Error); ok && codedeployerr.Code() == "ApplicationDoesNotExistException" { + if isAWSErr(err, codedeploy.ErrCodeApplicationDoesNotExistException, "") { d.SetId("") + log.Printf("[WARN] CodeDeploy Application (%s) not found, removing from state", d.Id()) return nil - } else { - log.Printf("[ERROR] Error finding CodeDeploy application: %s", err) - return err } + + log.Printf("[ERROR] Error finding CodeDeploy application: %s", err) + return err + } + + app := resp.Application + appName := aws.StringValue(app.ApplicationName) + + if !strings.Contains(d.Id(), appName) { + d.SetId(fmt.Sprintf("%s:%s", aws.StringValue(app.ApplicationId), appName)) + } + + appArn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: "codedeploy", + Region: meta.(*AWSClient).region, + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("application:%s", appName), + }.String() + + d.Set("arn", appArn) + d.Set("application_id", app.ApplicationId) + d.Set("compute_platform", app.ComputePlatform) + d.Set("name", appName) + d.Set("github_account_name", app.GitHubAccountName) + d.Set("linked_to_github", app.LinkedToGitHub) + + tags, err := keyvaluetags.CodedeployListTags(conn, appArn) + + if err != nil { + return fmt.Errorf("error listing tags for CodeDeploy application (%s): %w", d.Id(), err) } - d.Set("compute_platform", resp.Application.ComputePlatform) - d.Set("name", resp.Application.ApplicationName) + if err := d.Set("tags", tags.IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } return nil } @@ -133,20 +176,28 @@ func resourceAwsCodeDeployAppRead(d *schema.ResourceData, meta interface{}) erro func resourceAwsCodeDeployUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).codedeployconn - o, n := d.GetChange("name") + if d.HasChange("name") { + o, n := d.GetChange("name") - _, err := conn.UpdateApplication(&codedeploy.UpdateApplicationInput{ - ApplicationName: aws.String(o.(string)), - NewApplicationName: aws.String(n.(string)), - }) - if err != nil { - return err + _, err := conn.UpdateApplication(&codedeploy.UpdateApplicationInput{ + ApplicationName: aws.String(o.(string)), + NewApplicationName: aws.String(n.(string)), + }) + + if err != nil { + return fmt.Errorf("error updating CodeDeploy Application (%s) name: %w", d.Id(), err) + } } - log.Printf("[DEBUG] CodeDeploy application %s updated", n) - d.Set("name", n) + if d.HasChange("tags") { + o, n := d.GetChange("tags") - return nil + if err := keyvaluetags.CodedeployUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating CodeDeploy Application (%s) tags: %w", d.Get("arn").(string), err) + } + } + + return resourceAwsCodeDeployAppRead(d, meta) } func resourceAwsCodeDeployAppDelete(d *schema.ResourceData, meta interface{}) error { @@ -156,12 +207,12 @@ func resourceAwsCodeDeployAppDelete(d *schema.ResourceData, meta interface{}) er ApplicationName: aws.String(d.Get("name").(string)), }) if err != nil { - if cderr, ok := err.(awserr.Error); ok && cderr.Code() == "InvalidApplicationNameException" { + if isAWSErr(err, codedeploy.ErrCodeApplicationDoesNotExistException, "") { return nil - } else { - log.Printf("[ERROR] Error deleting CodeDeploy application: %s", err) - return err } + + log.Printf("[ERROR] Error deleting CodeDeploy application: %s", err) + return err } return nil diff --git a/aws/resource_aws_codedeploy_app_test.go b/aws/resource_aws_codedeploy_app_test.go index 61db3b4c141..1c5fcdf7c28 100644 --- a/aws/resource_aws_codedeploy_app_test.go +++ b/aws/resource_aws_codedeploy_app_test.go @@ -3,15 +3,67 @@ package aws import ( "errors" "fmt" + "log" "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/codedeploy" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) +func init() { + resource.AddTestSweepers("aws_codedeploy_app", &resource.Sweeper{ + Name: "aws_codedeploy_app", + F: testSweepCodeDeployApps, + }) +} + +func testSweepCodeDeployApps(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).codedeployconn + input := &codedeploy.ListApplicationsInput{} + var sweeperErrs *multierror.Error + + err = conn.ListApplicationsPages(input, func(page *codedeploy.ListApplicationsOutput, lastPage bool) bool { + for _, app := range page.Applications { + if app == nil { + continue + } + + appName := aws.StringValue(app) + r := resourceAwsCodeDeployApp() + d := r.Data(nil) + d.SetId(fmt.Sprintf("%s:%s", "xxxx", appName)) + err = r.Delete(d, client) + + if err != nil { + sweeperErr := fmt.Errorf("error deleting CodeDeploy Application (%s): %w", appName, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping CodeDeploy Application sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing CodeDeploy Applications: %w", err) + } + + return sweeperErrs.ErrorOrNil() +} + func TestAccAWSCodeDeployApp_basic(t *testing.T) { var application1 codedeploy.ApplicationInfo rName := acctest.RandomWithPrefix("tf-acc-test") @@ -27,8 +79,12 @@ func TestAccAWSCodeDeployApp_basic(t *testing.T) { Config: testAccAWSCodeDeployAppConfigName(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSCodeDeployAppExists(resourceName, &application1), + testAccCheckResourceAttrRegionalARN(resourceName, "arn", "codedeploy", fmt.Sprintf(`application:%s`, rName)), resource.TestCheckResourceAttr(resourceName, "compute_platform", "Server"), resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "linked_to_github", "false"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttrSet(resourceName, "application_id"), ), }, // Import by ID @@ -155,7 +211,6 @@ func TestAccAWSCodeDeployApp_name(t *testing.T) { Config: testAccAWSCodeDeployAppConfigName(rName2), Check: resource.ComposeTestCheckFunc( testAccCheckAWSCodeDeployAppExists(resourceName, &application2), - testAccCheckAWSCodeDeployAppRecreated(&application1, &application2), resource.TestCheckResourceAttr(resourceName, "name", rName2), ), }, @@ -168,6 +223,74 @@ func TestAccAWSCodeDeployApp_name(t *testing.T) { }) } +func TestAccAWSCodeDeployApp_tags(t *testing.T) { + var application codedeploy.ApplicationInfo + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_codedeploy_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, codedeploy.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCodeDeployAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCodeDeployAppConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCodeDeployAppExists(resourceName, &application), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSCodeDeployAppConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCodeDeployAppExists(resourceName, &application), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSCodeDeployAppConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCodeDeployAppExists(resourceName, &application), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccAWSCodeDeployApp_disappears(t *testing.T) { + var application1 codedeploy.ApplicationInfo + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_codedeploy_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, codedeploy.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSCodeDeployAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSCodeDeployAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSCodeDeployAppExists(resourceName, &application1), + testAccCheckResourceDisappears(testAccProvider, resourceAwsCodeDeployApp(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckAWSCodeDeployAppDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).codedeployconn @@ -249,3 +372,28 @@ resource "aws_codedeploy_app" "test" { } `, rName) } + +func testAccAWSCodeDeployAppConfigTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_codedeploy_app" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSCodeDeployAppConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_codedeploy_app" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 44d0fec791d..a81e54a0c20 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -254,6 +254,7 @@ This functionality is only supported in the following resources: - [`aws_apigatewayv2_stage` resource](/docs/providers/aws/r/apigatewayv2_stage.html) - [`aws_athena_workgroup` resource](/docs/providers/aws/r/athena_workgroup.html) - [`aws_budgets_budget` resource](/docs/providers/aws/r/budgets_budget.html) + - [`aws_codedeploy_app` resource](/docs/providers/aws/r/codedeploy_app.html) - [`aws_cognito_identity_pool` resource](/docs/providers/aws/r/cognito_identity_pool.html) - [`aws_cognito_user_pools` data source](/docs/providers/aws/d/cognito_user_pools.html) - [`aws_default_vpc_dhcp_options`](/docs/providers/aws/r/default_vpc_dhcp_options.html) diff --git a/website/docs/r/codedeploy_app.html.markdown b/website/docs/r/codedeploy_app.html.markdown index 1003a2d9930..54d2dff8190 100644 --- a/website/docs/r/codedeploy_app.html.markdown +++ b/website/docs/r/codedeploy_app.html.markdown @@ -45,13 +45,18 @@ The following arguments are supported: * `name` - (Required) The name of the application. * `compute_platform` - (Optional) The compute platform can either be `ECS`, `Lambda`, or `Server`. Default is `Server`. +* `tags` - (Optional) Key-value map of resource tags ## Attributes Reference In addition to all arguments above, the following attributes are exported: +* `arn` - The ARN of the CodeDeploy application. +* `application_id` - The application ID. * `id` - Amazon's assigned ID for the application. * `name` - The application's name. +* `github_account_name` - The name for a connection to a GitHub account. +* `linked_to_github` - Whether the user has authenticated with GitHub for the specified application. ## Import