diff --git a/.changelog/11937.txt b/.changelog/11937.txt new file mode 100644 index 00000000000..106a416f8af --- /dev/null +++ b/.changelog/11937.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_amplify_branch +``` + +```release-note:bug +resource/aws_amplify_app: Mark the `enable_performance_mode` argumnet in the `auto_branch_creation_config` configuration block as `ForceNew` +``` \ No newline at end of file diff --git a/aws/internal/service/amplify/finder/finder.go b/aws/internal/service/amplify/finder/finder.go index 46c91eac887..7074fabb4c2 100644 --- a/aws/internal/service/amplify/finder/finder.go +++ b/aws/internal/service/amplify/finder/finder.go @@ -63,3 +63,32 @@ func BackendEnvironmentByAppIDAndEnvironmentName(conn *amplify.Amplify, appID, e return output.BackendEnvironment, nil } + +func BranchByAppIDAndBranchName(conn *amplify.Amplify, appID, branchName string) (*amplify.Branch, error) { + input := &lify.GetBranchInput{ + AppId: aws.String(appID), + BranchName: aws.String(branchName), + } + + output, err := conn.GetBranch(input) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Branch == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.Branch, nil +} diff --git a/aws/internal/service/amplify/id.go b/aws/internal/service/amplify/id.go index a6c3210b0f2..0306a36111d 100644 --- a/aws/internal/service/amplify/id.go +++ b/aws/internal/service/amplify/id.go @@ -23,3 +23,22 @@ func BackendEnvironmentParseResourceID(id string) (string, string, error) { return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected APPID%[2]sENVIRONMENTNAME", id, backendEnvironmentResourceIDSeparator) } + +const branchResourceIDSeparator = "/" + +func BranchCreateResourceID(appID, branchName string) string { + parts := []string{appID, branchName} + id := strings.Join(parts, branchResourceIDSeparator) + + return id +} + +func BranchParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, branchResourceIDSeparator) + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected APPID%[2]sBRANCHNAME", id, branchResourceIDSeparator) +} diff --git a/aws/provider.go b/aws/provider.go index 8cdb9f54066..81e43f04776 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -455,6 +455,7 @@ func Provider() *schema.Provider { "aws_ami_launch_permission": resourceAwsAmiLaunchPermission(), "aws_amplify_app": resourceAwsAmplifyApp(), "aws_amplify_backend_environment": resourceAwsAmplifyBackendEnvironment(), + "aws_amplify_branch": resourceAwsAmplifyBranch(), "aws_api_gateway_account": resourceAwsApiGatewayAccount(), "aws_api_gateway_api_key": resourceAwsApiGatewayApiKey(), "aws_api_gateway_authorizer": resourceAwsApiGatewayAuthorizer(), diff --git a/aws/resource_aws_amplify_app.go b/aws/resource_aws_amplify_app.go index 963fffb1269..b28d2ba0120 100644 --- a/aws/resource_aws_amplify_app.go +++ b/aws/resource_aws_amplify_app.go @@ -94,6 +94,7 @@ func resourceAwsAmplifyApp() *schema.Resource { "enable_performance_mode": { Type: schema.TypeBool, Optional: true, + ForceNew: true, }, "enable_pull_request_preview": { diff --git a/aws/resource_aws_amplify_backend_environment_test.go b/aws/resource_aws_amplify_backend_environment_test.go index 8d3b590463b..5d5e6c63596 100644 --- a/aws/resource_aws_amplify_backend_environment_test.go +++ b/aws/resource_aws_amplify_backend_environment_test.go @@ -103,8 +103,6 @@ func testAccAWSAmplifyBackendEnvironment_DeploymentArtifacts_StackName(t *testin }) } -// testAccAWSAmplifyBackendEnvironmentConfigDeploymentArtifactsAndStackName(rname, environmentName) - func testAccCheckAWSAmplifyBackendEnvironmentExists(resourceName string, v *amplify.BackendEnvironment) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -160,7 +158,7 @@ func testAccCheckAWSAmplifyBackendEnvironmentDestroy(s *terraform.State) error { return err } - return fmt.Errorf("Amplify BackendEnvironment %s still exists", rs.Primary.ID) + return fmt.Errorf("Amplify Backend Environment %s still exists", rs.Primary.ID) } return nil diff --git a/aws/resource_aws_amplify_branch.go b/aws/resource_aws_amplify_branch.go new file mode 100644 index 00000000000..d63bc5a2dbe --- /dev/null +++ b/aws/resource_aws_amplify_branch.go @@ -0,0 +1,441 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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" + tfamplify "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func resourceAwsAmplifyBranch() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAmplifyBranchCreate, + Read: resourceAwsAmplifyBranchRead, + Update: resourceAwsAmplifyBranchUpdate, + Delete: resourceAwsAmplifyBranchDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + CustomizeDiff: SetTagsDiff, + + Schema: map[string]*schema.Schema{ + "app_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "associated_resources": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "backend_environment_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + + "basic_auth_credentials": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(1, 2000), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // These credentials are ignored if basic auth is not enabled. + if d.Get("enable_basic_auth").(bool) { + return old == new + } + + return true + }, + }, + + "branch_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[0-9A-Za-z/_.-]{1,255}$`), "should be not be more than 255 letters, numbers, and the symbols /_.-"), + }, + + "custom_domains": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + + "destination_branch": { + Type: schema.TypeString, + Computed: true, + }, + + "display_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[0-9a-z-]{1,255}$`), "should be not be more than 255 lowercase alphanumeric or hyphen characters"), + }, + + "enable_auto_build": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "enable_basic_auth": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_notification": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_performance_mode": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "enable_pull_request_preview": { + Type: schema.TypeBool, + Optional: true, + }, + + "environment_variables": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "framework": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "pull_request_environment_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 20), + }, + + "source_branch": { + Type: schema.TypeString, + Computed: true, + }, + + "stage": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(amplify.Stage_Values(), false), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // API returns "NONE" by default. + if old == tfamplify.StageNone && new == "" { + return true + } + + return old == new + }, + }, + + "ttl": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // API returns "5" by default. + if old == "5" && new == "" { + return true + } + + return old == new + }, + }, + + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + } +} + +func resourceAwsAmplifyBranchCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(keyvaluetags.New(d.Get("tags").(map[string]interface{}))) + + appID := d.Get("app_id").(string) + branchName := d.Get("branch_name").(string) + id := tfamplify.BranchCreateResourceID(appID, branchName) + + input := &lify.CreateBranchInput{ + AppId: aws.String(appID), + BranchName: aws.String(branchName), + EnableAutoBuild: aws.Bool(d.Get("enable_auto_build").(bool)), + } + + if v, ok := d.GetOk("backend_environment_arn"); ok { + input.BackendEnvironmentArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("basic_auth_credentials"); ok { + input.BasicAuthCredentials = aws.String(v.(string)) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("display_name"); ok { + input.DisplayName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("enable_basic_auth"); ok { + input.EnableBasicAuth = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_notification"); ok { + input.EnableNotification = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_performance_mode"); ok { + input.EnablePerformanceMode = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_pull_request_preview"); ok { + input.EnablePullRequestPreview = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("environment_variables"); ok && len(v.(map[string]interface{})) > 0 { + input.EnvironmentVariables = expandStringMap(v.(map[string]interface{})) + } + + if v, ok := d.GetOk("framework"); ok { + input.Framework = aws.String(v.(string)) + } + + if v, ok := d.GetOk("pull_request_environment_name"); ok { + input.PullRequestEnvironmentName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("stage"); ok { + input.Stage = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ttl"); ok { + input.Ttl = aws.String(v.(string)) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AmplifyTags() + } + + log.Printf("[DEBUG] Creating Amplify Branch: %s", input) + _, err := conn.CreateBranch(input) + + if err != nil { + return fmt.Errorf("error creating Amplify Branch (%s): %w", id, err) + } + + d.SetId(id) + + return resourceAwsAmplifyBranchRead(d, meta) +} + +func resourceAwsAmplifyBranchRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + appID, branchName, err := tfamplify.BranchParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Branch ID: %w", err) + } + + branch, err := finder.BranchByAppIDAndBranchName(conn, appID, branchName) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Amplify Branch (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Amplify Branch (%s): %w", d.Id(), err) + } + + d.Set("app_id", appID) + d.Set("arn", branch.BranchArn) + d.Set("associated_resources", aws.StringValueSlice(branch.AssociatedResources)) + d.Set("backend_environment_arn", branch.BackendEnvironmentArn) + d.Set("basic_auth_credentials", branch.BasicAuthCredentials) + d.Set("branch_name", branch.BranchName) + d.Set("custom_domains", aws.StringValueSlice(branch.CustomDomains)) + d.Set("description", branch.Description) + d.Set("destination_branch", branch.DestinationBranch) + d.Set("display_name", branch.DisplayName) + d.Set("enable_auto_build", branch.EnableAutoBuild) + d.Set("enable_basic_auth", branch.EnableBasicAuth) + d.Set("enable_notification", branch.EnableNotification) + d.Set("enable_performance_mode", branch.EnablePerformanceMode) + d.Set("enable_pull_request_preview", branch.EnablePullRequestPreview) + d.Set("environment_variables", aws.StringValueMap(branch.EnvironmentVariables)) + d.Set("framework", branch.Framework) + d.Set("pull_request_environment_name", branch.PullRequestEnvironmentName) + d.Set("source_branch", branch.SourceBranch) + d.Set("stage", branch.Stage) + d.Set("ttl", branch.Ttl) + + tags := keyvaluetags.AmplifyKeyValueTags(branch.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func resourceAwsAmplifyBranchUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + if d.HasChangesExcept("tags", "tags_all") { + appID, branchName, err := tfamplify.BranchParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Branch ID: %w", err) + } + + input := &lify.UpdateBranchInput{ + AppId: aws.String(appID), + BranchName: aws.String(branchName), + } + + if d.HasChange("backend_environment_arn") { + input.BackendEnvironmentArn = aws.String(d.Get("backend_environment_arn").(string)) + } + + if d.HasChange("basic_auth_credentials") { + input.BasicAuthCredentials = aws.String(d.Get("basic_auth_credentials").(string)) + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("display_name") { + input.DisplayName = aws.String(d.Get("display_name").(string)) + } + + if d.HasChange("enable_auto_build") { + input.EnableAutoBuild = aws.Bool(d.Get("enable_auto_build").(bool)) + } + + if d.HasChange("enable_basic_auth") { + input.EnableBasicAuth = aws.Bool(d.Get("enable_basic_auth").(bool)) + } + + if d.HasChange("enable_notification") { + input.EnableNotification = aws.Bool(d.Get("enable_notification").(bool)) + } + + if d.HasChange("enable_performance_mode") { + input.EnablePullRequestPreview = aws.Bool(d.Get("enable_performance_mode").(bool)) + } + + if d.HasChange("enable_pull_request_preview") { + input.EnablePullRequestPreview = aws.Bool(d.Get("enable_pull_request_preview").(bool)) + } + + if d.HasChange("environment_variables") { + if v := d.Get("environment_variables").(map[string]interface{}); len(v) > 0 { + input.EnvironmentVariables = expandStringMap(v) + } else { + input.EnvironmentVariables = aws.StringMap(map[string]string{"": ""}) + } + } + + if d.HasChange("framework") { + input.Framework = aws.String(d.Get("framework").(string)) + } + + if d.HasChange("pull_request_environment_name") { + input.PullRequestEnvironmentName = aws.String(d.Get("pull_request_environment_name").(string)) + } + + if d.HasChange("stage") { + input.Stage = aws.String(d.Get("stage").(string)) + } + + if d.HasChange("ttl") { + input.Ttl = aws.String(d.Get("ttl").(string)) + } + + _, err = conn.UpdateBranch(input) + + if err != nil { + return fmt.Errorf("error updating Amplify Branch (%s): %w", d.Id(), err) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + if err := keyvaluetags.AmplifyUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating tags: %w", err) + } + } + + return resourceAwsAmplifyBranchRead(d, meta) +} + +func resourceAwsAmplifyBranchDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + appID, branchName, err := tfamplify.BranchParseResourceID(d.Id()) + + if err != nil { + return fmt.Errorf("error parsing Amplify Branch ID: %w", err) + } + + log.Printf("[DEBUG] Deleting Amplify Branch: %s", d.Id()) + _, err = conn.DeleteBranch(&lify.DeleteBranchInput{ + AppId: aws.String(appID), + BranchName: aws.String(branchName), + }) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Amplify Branch (%s): %w", d.Id(), err) + } + + return nil +} diff --git a/aws/resource_aws_amplify_branch_test.go b/aws/resource_aws_amplify_branch_test.go new file mode 100644 index 00000000000..3ba7103ddfd --- /dev/null +++ b/aws/resource_aws_amplify_branch_test.go @@ -0,0 +1,510 @@ +package aws + +import ( + "encoding/base64" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/amplify" + "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" + tfamplify "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func testAccAWSAmplifyBranch_basic(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigName(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+/branches/.+`)), + resource.TestCheckResourceAttr(resourceName, "associated_resources.#", "0"), + resource.TestCheckResourceAttr(resourceName, "backend_environment_arn", ""), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "branch_name", rName), + resource.TestCheckResourceAttr(resourceName, "custom_domains.#", "0"), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "destination_branch", ""), + resource.TestCheckResourceAttr(resourceName, "display_name", rName), + resource.TestCheckResourceAttr(resourceName, "enable_auto_build", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_notification", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_performance_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_pull_request_preview", "false"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "0"), + resource.TestCheckResourceAttr(resourceName, "framework", ""), + resource.TestCheckResourceAttr(resourceName, "pull_request_environment_name", ""), + resource.TestCheckResourceAttr(resourceName, "source_branch", ""), + resource.TestCheckResourceAttr(resourceName, "stage", "NONE"), + resource.TestCheckResourceAttr(resourceName, "ttl", "5"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSAmplifyBranch_disappears(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAmplifyBranch(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSAmplifyBranch_Tags(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyBranchConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSAmplifyBranchConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyBranch_BasicAuthCredentials(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + credentials1 := base64.StdEncoding.EncodeToString([]byte("username1:password1")) + credentials2 := base64.StdEncoding.EncodeToString([]byte("username2:password2")) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigBasicAuthCredentials(rName, credentials1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", credentials1), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyBranchConfigBasicAuthCredentials(rName, credentials2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", credentials2), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "true"), + ), + }, + { + Config: testAccAWSAmplifyBranchConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + // Clearing basic_auth_credentials not reflected in API. + // resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "false"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyBranch_EnvironmentVariables(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_branch.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigEnvironmentVariables(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "1"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR1", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyBranchConfigEnvironmentVariablesUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "2"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR1", "2"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR2", "2"), + ), + }, + { + Config: testAccAWSAmplifyBranchConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "0"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyBranch_OptionalArguments(t *testing.T) { + var branch amplify.Branch + rName := acctest.RandomWithPrefix("tf-acc-test") + environmentName := acctest.RandStringFromCharSet(9, acctest.CharSetAlpha) + resourceName := "aws_amplify_branch.test" + backendEnvironment1ResourceName := "aws_amplify_backend_environment.test1" + backendEnvironment2ResourceName := "aws_amplify_backend_environment.test2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyBranchDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyBranchConfigOptionalArguments(rName, environmentName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttrPair(resourceName, "backend_environment_arn", backendEnvironment1ResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "description", "testdescription1"), + resource.TestCheckResourceAttr(resourceName, "display_name", "testdisplayname1"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_build", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_notification", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_performance_mode", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_pull_request_preview", "false"), + resource.TestCheckResourceAttr(resourceName, "framework", "React"), + resource.TestCheckResourceAttr(resourceName, "pull_request_environment_name", "testpr1"), + resource.TestCheckResourceAttr(resourceName, "stage", "DEVELOPMENT"), + resource.TestCheckResourceAttr(resourceName, "ttl", "10"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyBranchConfigOptionalArgumentsUpdated(rName, environmentName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyBranchExists(resourceName, &branch), + resource.TestCheckResourceAttrPair(resourceName, "backend_environment_arn", backendEnvironment2ResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "description", "testdescription2"), + resource.TestCheckResourceAttr(resourceName, "display_name", "testdisplayname2"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_build", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_notification", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_performance_mode", "true"), + resource.TestCheckResourceAttr(resourceName, "enable_pull_request_preview", "true"), + resource.TestCheckResourceAttr(resourceName, "framework", "Angular"), + resource.TestCheckResourceAttr(resourceName, "pull_request_environment_name", "testpr2"), + resource.TestCheckResourceAttr(resourceName, "stage", "EXPERIMENTAL"), + resource.TestCheckResourceAttr(resourceName, "ttl", "15"), + ), + }, + }, + }) +} + +func testAccCheckAWSAmplifyBranchExists(resourceName string, v *amplify.Branch) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Amplify Branch ID is set") + } + + appID, branchName, err := tfamplify.BranchParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + branch, err := finder.BranchByAppIDAndBranchName(conn, appID, branchName) + + if err != nil { + return err + } + + *v = *branch + + return nil + } +} + +func testAccCheckAWSAmplifyBranchDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_amplify_branch" { + continue + } + + appID, branchName, err := tfamplify.BranchParseResourceID(rs.Primary.ID) + + if err != nil { + return err + } + + _, err = finder.BranchByAppIDAndBranchName(conn, appID, branchName) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Amplify Branch %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccAWSAmplifyBranchConfigName(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q +} +`, rName) +} + +func testAccAWSAmplifyBranchConfigTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSAmplifyBranchConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccAWSAmplifyBranchConfigBasicAuthCredentials(rName, basicAuthCredentials string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + basic_auth_credentials = %[2]q + enable_basic_auth = true +} +`, rName, basicAuthCredentials) +} + +func testAccAWSAmplifyBranchConfigEnvironmentVariables(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + environment_variables = { + ENVVAR1 = "1" + } +} +`, rName) +} + +func testAccAWSAmplifyBranchConfigEnvironmentVariablesUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + environment_variables = { + ENVVAR1 = "2", + ENVVAR2 = "2" + } +} +`, rName) +} + +func testAccAWSAmplifyBranchConfigOptionalArguments(rName, environmentName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_backend_environment" "test1" { + app_id = aws_amplify_app.test.id + environment_name = "%[2]sa" +} + +resource "aws_amplify_backend_environment" "test2" { + app_id = aws_amplify_app.test.id + environment_name = "%[2]sb" +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + backend_environment_arn = aws_amplify_backend_environment.test1.arn + description = "testdescription1" + display_name = "testdisplayname1" + enable_auto_build = false + enable_notification = true + enable_performance_mode = true + enable_pull_request_preview = false + framework = "React" + pull_request_environment_name = "testpr1" + stage = "DEVELOPMENT" + ttl = "10" +} +`, rName, environmentName) +} + +func testAccAWSAmplifyBranchConfigOptionalArgumentsUpdated(rName, environmentName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} + +resource "aws_amplify_backend_environment" "test1" { + app_id = aws_amplify_app.test.id + environment_name = "%[2]sa" +} + +resource "aws_amplify_backend_environment" "test2" { + app_id = aws_amplify_app.test.id + environment_name = "%[2]sb" +} + +resource "aws_amplify_branch" "test" { + app_id = aws_amplify_app.test.id + branch_name = %[1]q + + backend_environment_arn = aws_amplify_backend_environment.test2.arn + description = "testdescription2" + display_name = "testdisplayname2" + enable_auto_build = true + enable_notification = false + enable_performance_mode = true + enable_pull_request_preview = true + framework = "Angular" + pull_request_environment_name = "testpr2" + stage = "EXPERIMENTAL" + ttl = "15" +} +`, rName, environmentName) +} diff --git a/aws/resource_aws_amplify_test.go b/aws/resource_aws_amplify_test.go index 46f5fab6279..03e39da1c05 100644 --- a/aws/resource_aws_amplify_test.go +++ b/aws/resource_aws_amplify_test.go @@ -27,6 +27,14 @@ func TestAccAWSAmplify_serial(t *testing.T) { "disappears": testAccAWSAmplifyBackendEnvironment_disappears, "DeploymentArtifacts_StackName": testAccAWSAmplifyBackendEnvironment_DeploymentArtifacts_StackName, }, + "Branch": { + "basic": testAccAWSAmplifyBranch_basic, + "disappears": testAccAWSAmplifyBranch_disappears, + "Tags": testAccAWSAmplifyBranch_Tags, + "BasicAuthCredentials": testAccAWSAmplifyBranch_BasicAuthCredentials, + "EnvironmentVariables": testAccAWSAmplifyBranch_EnvironmentVariables, + "OptionalArguments": testAccAWSAmplifyBranch_OptionalArguments, + }, } for group, m := range testCases { diff --git a/website/docs/r/amplify_branch.html.markdown b/website/docs/r/amplify_branch.html.markdown new file mode 100644 index 00000000000..38a78617f55 --- /dev/null +++ b/website/docs/r/amplify_branch.html.markdown @@ -0,0 +1,192 @@ +--- +subcategory: "Amplify Console" +layout: "aws" +page_title: "AWS: aws_amplify_branch" +description: |- + Provides an Amplify Branch resource. +--- + +# Resource: aws_amplify_branch + +Provides an Amplify Branch resource. + +## Example Usage + +```terraform +resource "aws_amplify_app" "example" { + name = "app" +} + +resource "aws_amplify_branch" "master" { + app_id = aws_amplify_app.example.id + branch_name = "master" + + framework = "React" + stage = "PRODUCTION" + + environment_variables = { + REACT_APP_API_SERVER = "https://api.example.com" + } +} +``` + +### Basic Authentication + +```terraform +resource "aws_amplify_app" "example" { + name = "app" +} + +resource "aws_amplify_branch" "master" { + app_id = aws_amplify_app.example.id + branch_name = "master" + + basic_auth_config { + # Enable basic authentication. + enable_basic_auth = true + + username = "username" + password = "password" + } +} +``` + +### Notifications + +Amplify Console uses CloudWatch Events and SNS for email notifications. To implement the same functionality, you need to set `enable_notification` in a `aws_amplify_branch` resource, as well as creating a CloudWatch Events Rule, a SNS topic, and SNS subscriptions. + +```terraform +resource "aws_amplify_app" "example" { + name = "app" +} + +resource "aws_amplify_branch" "master" { + app_id = aws_amplify_app.example.id + branch_name = "master" + + # Enable SNS notifications. + enable_notification = true +} + +# CloudWatch Events Rule for Amplify notifications + +resource "aws_cloudwatch_event_rule" "amplify_app_master" { + name = "amplify-${aws_amplify_app.app.id}-${aws_amplify_branch.master.branch_name}-branch-notification" + description = "AWS Amplify build notifications for : App: ${aws_amplify_app.app.id} Branch: ${aws_amplify_branch.master.branch_name}" + + event_pattern = jsonencode({ + "detail" = { + "appId" = [ + aws_amplify_app.example.id + ] + "branchName" = [ + aws_amplify_branch.master.branch_name + ], + "jobStatus" = [ + "SUCCEED", + "FAILED", + "STARTED" + ] + } + "detail-type" = [ + "Amplify Deployment Status Change" + ] + "source" = [ + "aws.amplify" + ] + }) +} + +resource "aws_cloudwatch_event_target" "amplify_app_master" { + rule = aws_cloudwatch_event_rule.amplify_app_master.name + target_id = aws_amplify_branch.master.branch_name + arn = aws_sns_topic.amplify_app_master.arn + + input_transformer { + input_paths = { + jobId = "$.detail.jobId" + appId = "$.detail.appId" + region = "$.region" + branch = "$.detail.branchName" + status = "$.detail.jobStatus" + } + + input_template = "\"Build notification from the AWS Amplify Console for app: https://..amplifyapp.com/. Your build status is . Go to https://console.aws.amazon.com/amplify/home?region=#// to view details on your build. \"" + } +} + +# SNS Topic for Amplify notifications + +resource "aws_sns_topic" "amplify_app_master" { + name = "amplify-${aws_amplify_app.app.id}_${aws_amplify_branch.master.branch_name}" +} + +data "aws_iam_policy_document" "amplify_app_master" { + statement { + sid = "Allow_Publish_Events ${aws_amplify_branch.master.arn}" + + effect = "Allow" + + actions = [ + "SNS:Publish", + ] + + principals { + type = "Service" + identifiers = [ + "events.amazonaws.com", + ] + } + + resources = [ + aws_sns_topic.amplify_app_master.arn, + ] + } +} + +resource "aws_sns_topic_policy" "amplify_app_master" { + arn = aws_sns_topic.amplify_app_master.arn + policy = data.aws_iam_policy_document.amplify_app_master.json +} +``` + +## Argument Reference + +The following arguments are supported: + +* `app_id` - (Required) The unique ID for an Amplify app. +* `branch_name` - (Required) The name for the branch. +* `backend_environment_arn` - (Optional) The Amazon Resource Name (ARN) for a backend environment that is part of an Amplify app. +* `basic_auth_credentials` - (Optional) The basic authorization credentials for the branch. +* `description` - (Optional) The description for the branch. +* `display_name` - (Optional) The display name for a branch. This is used as the default domain prefix. +* `enable_auto_build` - (Optional) Enables auto building for the branch. +* `enable_basic_auth` - (Optional) Enables basic authorization for the branch. +* `enable_notifications` - (Optional) Enables notifications for the branch. +* `enable_performance_mode` - (Optional) Enables performance mode for the branch. +* `enable_pull_request_preview` - (Optional) Enables pull request previews for this branch. +* `environment_variables` - (Optional) The environment variables for the branch. +* `framework` - (Optional) The framework for the branch. +* `pull_request_environment_name` - (Optional) The Amplify environment name for the pull request. +* `stage` - (Optional) Describes the current stage for the branch. Valid values: `PRODUCTION`, `BETA`, `DEVELOPMENT`, `EXPERIMENTAL`, `PULL_REQUEST`. +* `tags` - (Optional) Key-value mapping of resource tags. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `ttl` - (Optional) The content Time To Live (TTL) for the website in seconds. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - The Amazon Resource Name (ARN) for the branch. +* `associated_resources` - A list of custom resources that are linked to this branch. +* `custom_domains` - The custom domains for the branch. +* `destination_branch` - The destination branch if the branch is a pull request branch. +* `source_branch` - The source branch if the branch is a pull request branch. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Import + +Amplify branch can be imported using `app_id` and `branch_name`, e.g. + +``` +$ terraform import aws_amplify_branch.master d2ypk4k47z8u6/master +```