diff --git a/.changelog/15966.txt b/.changelog/15966.txt new file mode 100644 index 00000000000..feafab97221 --- /dev/null +++ b/.changelog/15966.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_amplify_app +``` \ No newline at end of file diff --git a/aws/internal/service/amplify/consts.go b/aws/internal/service/amplify/consts.go new file mode 100644 index 00000000000..3049f2bcfc2 --- /dev/null +++ b/aws/internal/service/amplify/consts.go @@ -0,0 +1,5 @@ +package amplify + +const ( + StageNone = "NONE" +) diff --git a/aws/internal/service/amplify/finder/finder.go b/aws/internal/service/amplify/finder/finder.go new file mode 100644 index 00000000000..8f67fbb18f1 --- /dev/null +++ b/aws/internal/service/amplify/finder/finder.go @@ -0,0 +1,36 @@ +package finder + +import ( + "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/resource" +) + +func AppByID(conn *amplify.Amplify, id string) (*amplify.App, error) { + input := &lify.GetAppInput{ + AppId: aws.String(id), + } + + output, err := conn.GetApp(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.App == nil { + return nil, &resource.NotFoundError{ + Message: "Empty result", + LastRequest: input, + } + } + + return output.App, nil +} diff --git a/aws/internal/service/amplify/lister/list.go b/aws/internal/service/amplify/lister/list.go new file mode 100644 index 00000000000..3b7007c9979 --- /dev/null +++ b/aws/internal/service/amplify/lister/list.go @@ -0,0 +1,3 @@ +//go:generate go run ../../../generators/listpages/main.go -function=ListApps github.com/aws/aws-sdk-go/service/amplify + +package lister diff --git a/aws/internal/service/amplify/lister/list_pages_gen.go b/aws/internal/service/amplify/lister/list_pages_gen.go new file mode 100644 index 00000000000..30fa4b12930 --- /dev/null +++ b/aws/internal/service/amplify/lister/list_pages_gen.go @@ -0,0 +1,31 @@ +// Code generated by "aws/internal/generators/listpages/main.go -function=ListApps github.com/aws/aws-sdk-go/service/amplify"; DO NOT EDIT. + +package lister + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" +) + +func ListAppsPages(conn *amplify.Amplify, input *amplify.ListAppsInput, fn func(*amplify.ListAppsOutput, bool) bool) error { + return ListAppsPagesWithContext(context.Background(), conn, input, fn) +} + +func ListAppsPagesWithContext(ctx context.Context, conn *amplify.Amplify, input *amplify.ListAppsInput, fn func(*amplify.ListAppsOutput, bool) bool) error { + for { + output, err := conn.ListAppsWithContext(ctx, input) + if err != nil { + return err + } + + lastPage := aws.StringValue(output.NextToken) == "" + if !fn(output, lastPage) || lastPage { + break + } + + input.NextToken = output.NextToken + } + return nil +} diff --git a/aws/provider.go b/aws/provider.go index b0cc423bedc..a66754c0123 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -450,6 +450,7 @@ func Provider() *schema.Provider { "aws_ami_copy": resourceAwsAmiCopy(), "aws_ami_from_instance": resourceAwsAmiFromInstance(), "aws_ami_launch_permission": resourceAwsAmiLaunchPermission(), + "aws_amplify_app": resourceAwsAmplifyApp(), "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 new file mode 100644 index 00000000000..963fffb1269 --- /dev/null +++ b/aws/resource_aws_amplify_app.go @@ -0,0 +1,817 @@ +package aws + +import ( + "context" + "fmt" + "log" + "time" + + "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/customdiff" + "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 resourceAwsAmplifyApp() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAmplifyAppCreate, + Read: resourceAwsAmplifyAppRead, + Update: resourceAwsAmplifyAppUpdate, + Delete: resourceAwsAmplifyAppDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + CustomizeDiff: customdiff.Sequence( + SetTagsDiff, + customdiff.ForceNewIfChange("description", func(_ context.Context, old, new, meta interface{}) bool { + // Any existing value cannot be cleared. + return new.(string) == "" + }), + customdiff.ForceNewIfChange("iam_service_role_arn", func(_ context.Context, old, new, meta interface{}) bool { + // Any existing value cannot be cleared. + return new.(string) == "" + }), + ), + + Schema: map[string]*schema.Schema{ + "access_token": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "arn": { + Type: schema.TypeString, + Computed: true, + }, + + "auto_branch_creation_config": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "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("auto_branch_creation_config.0.enable_basic_auth").(bool) { + return old == new + } + + return true + }, + }, + + "build_spec": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 25000), + }, + + "enable_auto_build": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_basic_auth": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_performance_mode": { + Type: schema.TypeBool, + Optional: 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, 255), + }, + + "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 + }, + }, + }, + }, + }, + + "auto_branch_creation_patterns": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // These patterns are ignored if branch auto-creation is not enabled. + if d.Get("enable_auto_branch_creation").(bool) { + return old == new + } + + return true + }, + }, + + "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 + }, + }, + + "build_spec": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringLenBetween(1, 25000), + }, + + "custom_rule": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "condition": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 2048), + }, + + "source": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 2048), + }, + + "status": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "200", + "301", + "302", + "404", + "404-200", + }, false), + }, + + "target": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 2048), + }, + }, + }, + }, + + "default_domain": { + Type: schema.TypeString, + Computed: true, + }, + + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + + "enable_auto_branch_creation": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_basic_auth": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_branch_auto_build": { + Type: schema.TypeBool, + Optional: true, + }, + + "enable_branch_auto_deletion": { + Type: schema.TypeBool, + Optional: true, + }, + + "environment_variables": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "iam_service_role_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateArn, + }, + + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + + "oauth_token": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + + "platform": { + Type: schema.TypeString, + Optional: true, + Default: amplify.PlatformWeb, + ValidateFunc: validation.StringInSlice(amplify.Platform_Values(), false), + }, + + "production_branch": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "branch_name": { + Type: schema.TypeString, + Computed: true, + }, + + "last_deploy_time": { + Type: schema.TypeString, + Computed: true, + }, + + "status": { + Type: schema.TypeString, + Computed: true, + }, + + "thumbnail_url": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "repository": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + + "tags": tagsSchema(), + "tags_all": tagsSchemaComputed(), + }, + } +} + +func resourceAwsAmplifyAppCreate(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{}))) + + name := d.Get("name").(string) + + input := &lify.CreateAppInput{ + Name: aws.String(name), + } + + if v, ok := d.GetOk("access_token"); ok { + input.AccessToken = aws.String(v.(string)) + } + + if v, ok := d.GetOk("auto_branch_creation_config"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.AutoBranchCreationConfig = expandAmplifyAutoBranchCreationConfig(v.([]interface{})[0].(map[string]interface{})) + } + + if v, ok := d.GetOk("auto_branch_creation_patterns"); ok && v.(*schema.Set).Len() > 0 { + input.AutoBranchCreationPatterns = expandStringSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("basic_auth_credentials"); ok { + input.BasicAuthCredentials = aws.String(v.(string)) + } + + if v, ok := d.GetOk("build_spec"); ok { + input.BuildSpec = aws.String(v.(string)) + } + + if v, ok := d.GetOk("custom_rule"); ok && len(v.([]interface{})) > 0 { + input.CustomRules = expandAmplifyCustomRules(v.([]interface{})) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("enable_auto_branch_creation"); ok { + input.EnableAutoBranchCreation = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_basic_auth"); ok { + input.EnableBasicAuth = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_branch_auto_build"); ok { + input.EnableBranchAutoBuild = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("enable_branch_auto_deletion"); ok { + input.EnableBranchAutoDeletion = 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("iam_service_role_arn"); ok { + input.IamServiceRoleArn = aws.String(v.(string)) + } + + if v, ok := d.GetOk("oauth_token"); ok { + input.OauthToken = aws.String(v.(string)) + } + + if v, ok := d.GetOk("platform"); ok { + input.Platform = aws.String(v.(string)) + } + + if v, ok := d.GetOk("repository"); ok { + input.Repository = aws.String(v.(string)) + } + + if len(tags) > 0 { + input.Tags = tags.IgnoreAws().AmplifyTags() + } + + log.Printf("[DEBUG] Creating Amplify App: %s", input) + output, err := conn.CreateApp(input) + + if err != nil { + return fmt.Errorf("error creating Amplify App (%s): %w", name, err) + } + + d.SetId(aws.StringValue(output.App.AppId)) + + return resourceAwsAmplifyAppRead(d, meta) +} + +func resourceAwsAmplifyAppRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + defaultTagsConfig := meta.(*AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + app, err := finder.AppByID(conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Amplify App (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Amplify App (%s): %w", d.Id(), err) + } + + d.Set("arn", app.AppArn) + if app.AutoBranchCreationConfig != nil { + if err := d.Set("auto_branch_creation_config", []interface{}{flattenAmplifyAutoBranchCreationConfig(app.AutoBranchCreationConfig)}); err != nil { + return fmt.Errorf("error setting auto_branch_creation_config: %w", err) + } + } else { + d.Set("auto_branch_creation_config", nil) + } + d.Set("auto_branch_creation_patterns", aws.StringValueSlice(app.AutoBranchCreationPatterns)) + d.Set("basic_auth_credentials", app.BasicAuthCredentials) + d.Set("build_spec", app.BuildSpec) + if err := d.Set("custom_rule", flattenAmplifyCustomRules(app.CustomRules)); err != nil { + return fmt.Errorf("error setting custom_rule: %w", err) + } + d.Set("default_domain", app.DefaultDomain) + d.Set("description", app.Description) + d.Set("enable_auto_branch_creation", app.EnableAutoBranchCreation) + d.Set("enable_basic_auth", app.EnableBasicAuth) + d.Set("enable_branch_auto_build", app.EnableBranchAutoBuild) + d.Set("enable_branch_auto_deletion", app.EnableBranchAutoDeletion) + d.Set("environment_variables", aws.StringValueMap(app.EnvironmentVariables)) + d.Set("iam_service_role_arn", app.IamServiceRoleArn) + d.Set("name", app.Name) + d.Set("platform", app.Platform) + if app.ProductionBranch != nil { + if err := d.Set("production_branch", []interface{}{flattenAmplifyProductionBranch(app.ProductionBranch)}); err != nil { + return fmt.Errorf("error setting production_branch: %w", err) + } + } else { + d.Set("production_branch", nil) + } + d.Set("repository", app.Repository) + + tags := keyvaluetags.AmplifyKeyValueTags(app.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 resourceAwsAmplifyAppUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + if d.HasChangesExcept("tags", "tags_all") { + input := &lify.UpdateAppInput{ + AppId: aws.String(d.Id()), + } + + if d.HasChange("access_token") { + input.AccessToken = aws.String(d.Get("access_token").(string)) + } + + if d.HasChange("auto_branch_creation_config") { + input.AutoBranchCreationConfig = expandAmplifyAutoBranchCreationConfig(d.Get("auto_branch_creation_config").([]interface{})[0].(map[string]interface{})) + + if d.HasChange("auto_branch_creation_config.0.environment_variables") { + if v := d.Get("auto_branch_creation_config.0.environment_variables").(map[string]interface{}); len(v) == 0 { + input.AutoBranchCreationConfig.EnvironmentVariables = aws.StringMap(map[string]string{"": ""}) + } + } + } + + if d.HasChange("auto_branch_creation_patterns") { + input.AutoBranchCreationPatterns = expandStringSet(d.Get("auto_branch_creation_patterns").(*schema.Set)) + } + + if d.HasChange("basic_auth_credentials") { + input.BasicAuthCredentials = aws.String(d.Get("basic_auth_credentials").(string)) + } + + if d.HasChange("build_spec") { + input.BuildSpec = aws.String(d.Get("build_spec").(string)) + } + + if d.HasChange("custom_rule") { + if v := d.Get("custom_rule").([]interface{}); len(v) > 0 { + input.CustomRules = expandAmplifyCustomRules(v) + } else { + input.CustomRules = []*amplify.CustomRule{} + } + } + + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChange("enable_auto_branch_creation") { + input.EnableAutoBranchCreation = aws.Bool(d.Get("enable_auto_branch_creation").(bool)) + } + + if d.HasChange("enable_basic_auth") { + input.EnableBasicAuth = aws.Bool(d.Get("enable_basic_auth").(bool)) + } + + if d.HasChange("enable_branch_auto_build") { + input.EnableBranchAutoBuild = aws.Bool(d.Get("enable_branch_auto_build").(bool)) + } + + if d.HasChange("enable_branch_auto_deletion") { + input.EnableBranchAutoDeletion = aws.Bool(d.Get("enable_branch_auto_deletion").(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("iam_service_role_arn") { + input.IamServiceRoleArn = aws.String(d.Get("iam_service_role_arn").(string)) + } + + if d.HasChange("name") { + input.Name = aws.String(d.Get("name").(string)) + } + + if d.HasChange("oauth_token") { + input.OauthToken = aws.String(d.Get("oauth_token").(string)) + } + + if d.HasChange("platform") { + input.Platform = aws.String(d.Get("platform").(string)) + } + + if d.HasChange("repository") { + input.Repository = aws.String(d.Get("repository").(string)) + } + + _, err := conn.UpdateApp(input) + + if err != nil { + return fmt.Errorf("error updating Amplify App (%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 resourceAwsAmplifyAppRead(d, meta) +} + +func resourceAwsAmplifyAppDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).amplifyconn + + log.Printf("[DEBUG] Deleting Amplify App (%s)", d.Id()) + _, err := conn.DeleteApp(&lify.DeleteAppInput{ + AppId: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, amplify.ErrCodeNotFoundException) { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting Amplify App (%s): %w", d.Id(), err) + } + + return nil +} + +func expandAmplifyAutoBranchCreationConfig(tfMap map[string]interface{}) *amplify.AutoBranchCreationConfig { + if tfMap == nil { + return nil + } + + apiObject := &lify.AutoBranchCreationConfig{} + + if v, ok := tfMap["basic_auth_credentials"].(string); ok && v != "" { + apiObject.BasicAuthCredentials = aws.String(v) + } + + if v, ok := tfMap["build_spec"].(string); ok && v != "" { + apiObject.BuildSpec = aws.String(v) + } + + if v, ok := tfMap["enable_auto_build"].(bool); ok { + apiObject.EnableAutoBuild = aws.Bool(v) + } + + if v, ok := tfMap["enable_basic_auth"].(bool); ok { + apiObject.EnableBasicAuth = aws.Bool(v) + } + + if v, ok := tfMap["enable_performance_mode"].(bool); ok { + apiObject.EnablePerformanceMode = aws.Bool(v) + } + + if v, ok := tfMap["enable_pull_request_preview"].(bool); ok { + apiObject.EnablePullRequestPreview = aws.Bool(v) + } + + if v, ok := tfMap["environment_variables"].(map[string]interface{}); ok && len(v) > 0 { + apiObject.EnvironmentVariables = expandStringMap(v) + } + + if v, ok := tfMap["framework"].(string); ok && v != "" { + apiObject.Framework = aws.String(v) + } + + if v, ok := tfMap["pull_request_environment_name"].(string); ok && v != "" { + apiObject.PullRequestEnvironmentName = aws.String(v) + } + + if v, ok := tfMap["stage"].(string); ok && v != "" && v != tfamplify.StageNone { + apiObject.Stage = aws.String(v) + } + + return apiObject +} + +func flattenAmplifyAutoBranchCreationConfig(apiObject *amplify.AutoBranchCreationConfig) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.BasicAuthCredentials; v != nil { + tfMap["basic_auth_credentials"] = aws.StringValue(v) + } + + if v := apiObject.BuildSpec; v != nil { + tfMap["build_spec"] = aws.StringValue(v) + } + + if v := apiObject.EnableAutoBuild; v != nil { + tfMap["enable_auto_build"] = aws.BoolValue(v) + } + + if v := apiObject.EnableBasicAuth; v != nil { + tfMap["enable_basic_auth"] = aws.BoolValue(v) + } + + if v := apiObject.EnablePerformanceMode; v != nil { + tfMap["enable_performance_mode"] = aws.BoolValue(v) + } + + if v := apiObject.EnablePullRequestPreview; v != nil { + tfMap["enable_pull_request_preview"] = aws.BoolValue(v) + } + + if v := apiObject.EnvironmentVariables; v != nil { + tfMap["environment_variables"] = aws.StringValueMap(v) + } + + if v := apiObject.Framework; v != nil { + tfMap["framework"] = aws.StringValue(v) + } + + if v := apiObject.PullRequestEnvironmentName; v != nil { + tfMap["pull_request_environment_name"] = aws.StringValue(v) + } + + if v := apiObject.Stage; v != nil { + tfMap["stage"] = aws.StringValue(v) + } + + return tfMap +} + +func expandAmplifyCustomRule(tfMap map[string]interface{}) *amplify.CustomRule { + if tfMap == nil { + return nil + } + + apiObject := &lify.CustomRule{} + + if v, ok := tfMap["condition"].(string); ok && v != "" { + apiObject.Condition = aws.String(v) + } + + if v, ok := tfMap["source"].(string); ok && v != "" { + apiObject.Source = aws.String(v) + } + + if v, ok := tfMap["status"].(string); ok && v != "" { + apiObject.Status = aws.String(v) + } + + if v, ok := tfMap["target"].(string); ok && v != "" { + apiObject.Target = aws.String(v) + } + + return apiObject +} + +func expandAmplifyCustomRules(tfList []interface{}) []*amplify.CustomRule { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*amplify.CustomRule + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandAmplifyCustomRule(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func flattenAmplifyCustomRule(apiObject *amplify.CustomRule) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.Condition; v != nil { + tfMap["condition"] = aws.StringValue(v) + } + + if v := apiObject.Source; v != nil { + tfMap["source"] = aws.StringValue(v) + } + + if v := apiObject.Status; v != nil { + tfMap["status"] = aws.StringValue(v) + } + + if v := apiObject.Target; v != nil { + tfMap["target"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenAmplifyCustomRules(apiObjects []*amplify.CustomRule) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenAmplifyCustomRule(apiObject)) + } + + return tfList +} + +func flattenAmplifyProductionBranch(apiObject *amplify.ProductionBranch) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.BranchName; v != nil { + tfMap["branch_name"] = aws.StringValue(v) + } + + if v := apiObject.LastDeployTime; v != nil { + tfMap["last_deploy_time"] = aws.TimeValue(v).Format(time.RFC3339) + } + + if v := apiObject.Status; v != nil { + tfMap["status"] = aws.StringValue(v) + } + + if v := apiObject.ThumbnailUrl; v != nil { + tfMap["thumbnail_url"] = aws.StringValue(v) + } + + return tfMap +} diff --git a/aws/resource_aws_amplify_app_test.go b/aws/resource_aws_amplify_app_test.go new file mode 100644 index 00000000000..94b49a37ee8 --- /dev/null +++ b/aws/resource_aws_amplify_app_test.go @@ -0,0 +1,970 @@ +package aws + +import ( + "encoding/base64" + "fmt" + "log" + "os" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/amplify" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/amplify/lister" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" +) + +func init() { + resource.AddTestSweepers("aws_amplify_app", &resource.Sweeper{ + Name: "aws_amplify_app", + F: testSweepAmplifyApps, + }) +} + +func testSweepAmplifyApps(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).amplifyconn + input := &lify.ListAppsInput{} + var sweeperErrs *multierror.Error + + err = lister.ListAppsPages(conn, input, func(page *amplify.ListAppsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, app := range page.Apps { + r := resourceAwsAmplifyApp() + d := r.Data(nil) + d.SetId(aws.StringValue(app.AppId)) + err = r.Delete(d, client) + + if err != nil { + log.Printf("[ERROR] %s", err) + sweeperErrs = multierror.Append(sweeperErrs, err) + continue + } + } + + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Amplify Apps sweep for %s: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors + } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Amplify Apps: %w", err)) + } + + return sweeperErrs.ErrorOrNil() +} + +func testAccAWSAmplifyApp_basic(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckNoResourceAttr(resourceName, "access_token"), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "amplify", regexp.MustCompile(`apps/.+`)), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "0"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "0"), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "build_spec", ""), + resource.TestCheckResourceAttr(resourceName, "custom_rule.#", "0"), + resource.TestMatchResourceAttr(resourceName, "default_domain", regexp.MustCompile(`\.amplifyapp\.com$`)), + resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_branch_auto_build", "false"), + resource.TestCheckResourceAttr(resourceName, "enable_branch_auto_deletion", "false"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "0"), + resource.TestCheckResourceAttr(resourceName, "iam_service_role_arn", ""), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckNoResourceAttr(resourceName, "oauth_token"), + resource.TestCheckResourceAttr(resourceName, "platform", "WEB"), + resource.TestCheckResourceAttr(resourceName, "production_branch.#", "0"), + resource.TestCheckResourceAttr(resourceName, "repository", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAWSAmplifyApp_disappears(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + testAccCheckResourceDisappears(testAccProvider, resourceAwsAmplifyApp(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccAWSAmplifyApp_Tags(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_AutoBranchCreationConfig(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + credentials := base64.StdEncoding.EncodeToString([]byte("username1:password1")) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigAutoBranchCreationConfigNoAutoBranchCreationConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.build_spec", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_auto_build", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_basic_auth", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_performance_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_pull_request_preview", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.environment_variables.%", "0"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.framework", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.pull_request_environment_name", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.stage", "NONE"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "2"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.0", "*"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.1", "*/**"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "true"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigAutoBranchCreationConfigAutoBranchCreationConfig(rName, credentials), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.basic_auth_credentials", credentials), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.build_spec", "version: 0.1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_auto_build", "true"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_basic_auth", "true"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_performance_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_pull_request_preview", "true"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.environment_variables.%", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.environment_variables.ENVVAR1", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.framework", "React"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.pull_request_environment_name", "test1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.stage", "DEVELOPMENT"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.0", "feature/*"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigAutoBranchCreationConfigAutoBranchCreationConfigUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "1"), + // Clearing basic_auth_credentials not reflected in API. + // resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.build_spec", "version: 0.2"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_auto_build", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_basic_auth", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_performance_mode", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.enable_pull_request_preview", "false"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.environment_variables.%", "0"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.framework", "React"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.pull_request_environment_name", "test2"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.0.stage", "EXPERIMENTAL"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.0", "feature/*"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "true"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + // No change is reflected in API. + // resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_config.#", "0"), + // resource.TestCheckResourceAttr(resourceName, "auto_branch_creation_patterns.#", "0"), + resource.TestCheckResourceAttr(resourceName, "enable_auto_branch_creation", "false"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_BasicAuthCredentials(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.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: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigBasicAuthCredentials(rName, credentials1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", credentials1), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigBasicAuthCredentials(rName, credentials2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", credentials2), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "true"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + // Clearing basic_auth_credentials not reflected in API. + // resource.TestCheckResourceAttr(resourceName, "basic_auth_credentials", ""), + resource.TestCheckResourceAttr(resourceName, "enable_basic_auth", "false"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_BuildSpec(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigBuildSpec(rName, "version: 0.1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "build_spec", "version: 0.1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigBuildSpec(rName, "version: 0.2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "build_spec", "version: 0.2"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + // build_spec is Computed. + resource.TestCheckResourceAttr(resourceName, "build_spec", "version: 0.2"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_CustomRules(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigCustomRules(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "custom_rule.#", "1"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.source", "/<*>"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.status", "404"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.target", "/index.html"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigCustomRulesUpdated(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "custom_rule.#", "2"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.condition", ""), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.source", "/documents"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.status", "302"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.0.target", "/documents/us"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.1.source", "/<*>"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.1.status", "200"), + resource.TestCheckResourceAttr(resourceName, "custom_rule.1.target", "/index.html"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "custom_rule.#", "0"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_Description(t *testing.T) { + var app1, app2, app3 amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigDescription(rName, "description 1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app1), + resource.TestCheckResourceAttr(resourceName, "description", "description 1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigDescription(rName, "description 2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app2), + testAccCheckAWSAmplifyAppNotRecreated(&app1, &app2), + resource.TestCheckResourceAttr(resourceName, "description", "description 2"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app3), + testAccCheckAWSAmplifyAppRecreated(&app2, &app3), + resource.TestCheckResourceAttr(resourceName, "description", ""), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_EnvironmentVariables(t *testing.T) { + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigEnvironmentVariables(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "1"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR1", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigEnvironmentVariablesUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "2"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR1", "2"), + resource.TestCheckResourceAttr(resourceName, "environment_variables.ENVVAR2", "2"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "environment_variables.%", "0"), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_IamServiceRole(t *testing.T) { + var app1, app2, app3 amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + iamRole1ResourceName := "aws_iam_role.test1" + iamRole2ResourceName := "aws_iam_role.test2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigIAMServiceRoleArn(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app1), + resource.TestCheckResourceAttrPair(resourceName, "iam_service_role_arn", iamRole1ResourceName, "arn")), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigIAMServiceRoleArnUpdated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app2), + testAccCheckAWSAmplifyAppNotRecreated(&app1, &app2), + resource.TestCheckResourceAttrPair(resourceName, "iam_service_role_arn", iamRole2ResourceName, "arn"), + ), + }, + { + Config: testAccAWSAmplifyAppConfigName(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app3), + testAccCheckAWSAmplifyAppRecreated(&app2, &app3), + resource.TestCheckResourceAttr(resourceName, "iam_service_role_arn", ""), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_Name(t *testing.T) { + var app amplify.App + rName1 := acctest.RandomWithPrefix("tf-acc-test") + rName2 := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigName(rName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "name", rName1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSAmplifyAppConfigName(rName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "name", rName2), + ), + }, + }, + }) +} + +func testAccAWSAmplifyApp_Repository(t *testing.T) { + key := "AMPLIFY_GITHUB_ACCESS_TOKEN" + accessToken := os.Getenv(key) + if accessToken == "" { + t.Skipf("Environment variable %s is not set", key) + } + + key = "AMPLIFY_GITHUB_REPOSITORY" + repository := os.Getenv(key) + if repository == "" { + t.Skipf("Environment variable %s is not set", key) + } + + var app amplify.App + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_amplify_app.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSAmplify(t) }, + ErrorCheck: testAccErrorCheck(t, amplify.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSAmplifyAppDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSAmplifyAppConfigRepository(rName, repository, accessToken), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSAmplifyAppExists(resourceName, &app), + resource.TestCheckResourceAttr(resourceName, "access_token", accessToken), + resource.TestCheckResourceAttr(resourceName, "repository", repository), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // access_token is ignored because AWS does not store access_token and oauth_token + // See https://docs.aws.amazon.com/sdk-for-go/api/service/amplify/#CreateAppInput + ImportStateVerifyIgnore: []string{"access_token"}, + }, + }, + }) +} + +func testAccCheckAWSAmplifyAppExists(n string, v *amplify.App) 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("No Amplify App ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + output, err := finder.AppByID(conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckAWSAmplifyAppDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).amplifyconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_amplify_app" { + continue + } + + _, err := finder.AppByID(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Amplify App %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccPreCheckAWSAmplify(t *testing.T) { + if testAccGetPartition() == "aws-us-gov" { + t.Skip("AWS Amplify is not supported in GovCloud partition") + } +} + +func testAccCheckAWSAmplifyAppNotRecreated(before, after *amplify.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.StringValue(before.AppId), aws.StringValue(after.AppId); before != after { + return fmt.Errorf("Amplify App (%s/%s) recreated", before, after) + } + + return nil + } +} + +func testAccCheckAWSAmplifyAppRecreated(before, after *amplify.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.StringValue(before.AppId), aws.StringValue(after.AppId); before == after { + return fmt.Errorf("Amplify App (%s) not recreated", before) + } + + return nil + } +} + +func testAccAWSAmplifyAppConfigName(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q +} +`, rName) +} + +func testAccAWSAmplifyAppConfigTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccAWSAmplifyAppConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccAWSAmplifyAppConfigAutoBranchCreationConfigNoAutoBranchCreationConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + enable_auto_branch_creation = true + + auto_branch_creation_patterns = [ + "*", + "*/**", + ] +} +`, rName) +} + +func testAccAWSAmplifyAppConfigAutoBranchCreationConfigAutoBranchCreationConfig(rName, basicAuthCredentials string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + enable_auto_branch_creation = true + + auto_branch_creation_patterns = [ + "feature/*", + ] + + auto_branch_creation_config { + build_spec = "version: 0.1" + framework = "React" + stage = "DEVELOPMENT" + + enable_basic_auth = true + basic_auth_credentials = %[2]q + + enable_auto_build = true + enable_pull_request_preview = true + pull_request_environment_name = "test1" + + environment_variables = { + ENVVAR1 = "1" + } + } +} + +`, rName, basicAuthCredentials) +} + +func testAccAWSAmplifyAppConfigAutoBranchCreationConfigAutoBranchCreationConfigUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + enable_auto_branch_creation = true + + auto_branch_creation_patterns = [ + "feature/*", + ] + + auto_branch_creation_config { + build_spec = "version: 0.2" + framework = "React" + stage = "EXPERIMENTAL" + + enable_basic_auth = false + + enable_auto_build = false + enable_pull_request_preview = false + + pull_request_environment_name = "test2" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigBasicAuthCredentials(rName, basicAuthCredentials string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + basic_auth_credentials = %[2]q + enable_basic_auth = true +} +`, rName, basicAuthCredentials) +} + +func testAccAWSAmplifyAppConfigBuildSpec(rName, buildSpec string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + build_spec = %[2]q +} +`, rName, buildSpec) +} + +func testAccAWSAmplifyAppConfigCustomRules(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + custom_rule { + source = "/<*>" + status = "404" + target = "/index.html" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigCustomRulesUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + custom_rule { + condition = "" + source = "/documents" + status = "302" + target = "/documents/us" + } + + custom_rule { + source = "/<*>" + status = "200" + target = "/index.html" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigDescription(rName, description string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + description = %[2]q +} +`, rName, description) +} + +func testAccAWSAmplifyAppConfigEnvironmentVariables(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + environment_variables = { + ENVVAR1 = "1" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigEnvironmentVariablesUpdated(rName string) string { + return fmt.Sprintf(` +resource "aws_amplify_app" "test" { + name = %[1]q + + environment_variables = { + ENVVAR1 = "2", + ENVVAR2 = "2" + } +} +`, rName) +} + +func testAccAWSAmplifyAppConfigIAMServiceRoleBase(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_role" "test1" { + name = "%[1]s-1" + + assume_role_policy = < **Note:** When you create/update an Amplify App from Terraform, you may end up with the error "BadRequestException: You should at least provide one valid token" because of authentication issues. See the section "Repository with Tokens" below. + +## Example Usage + +```terraform +resource "aws_amplify_app" "example" { + name = "example" + repository = "https://github.com/example/app" + + # The default build_spec added by the Amplify Console for React. + build_spec = <<-EOT + version: 0.1 + frontend: + phases: + preBuild: + commands: + - yarn install + build: + commands: + - yarn run build + artifacts: + baseDirectory: build + files: + - '**/*' + cache: + paths: + - node_modules/**/* + EOT + + # The default rewrites and redirects added by the Amplify Console. + custom_rule { + source = "/<*>" + status = "404" + target = "/index.html" + } + + environment_variables = { + ENV = "test" + } +} +``` + +### Repository with Tokens + +If you create a new Amplify App with the `repository` argument, you also need to set `oauth_token` or `access_token` for authentication. For GitHub, get a [personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line) and set `access_token` as follows: + +```terraform +resource "aws_amplify_app" "example" { + name = "example" + repository = "https://github.com/example/app" + + # GitHub personal access token + access_token = "..." +} +``` + +You can omit `access_token` if you import an existing Amplify App created by the Amplify Console (using OAuth for authentication). + +### Auto Branch Creation + +```terraform +resource "aws_amplify_app" "example" { + name = "example" + + enable_auto_branch_creation = true + + # The default patterns added by the Amplify Console. + auto_branch_creation_patterns = [ + "*", + "*/**", + ] + + auto_branch_creation_config { + # Enable auto build for the created branch. + enable_auto_build = true + } +} +``` + +### Basic Authorization + +```terraform +resource "aws_amplify_app" "example" { + name = "example" + + enable_basic_auth = true + basic_auth_credentials = base64encode("username1:password1") +} +``` + +### Rewrites and Redirects + +```terraform +resource "aws_amplify_app" "example" { + name = "example" + + # Reverse Proxy Rewrite for API requests + # https://docs.aws.amazon.com/amplify/latest/userguide/redirects.html#reverse-proxy-rewrite + custom_rule { + source = "/api/<*>" + status = "200" + target = "https://api.example.com/api/<*>" + } + + # Redirects for Single Page Web Apps (SPA) + # https://docs.aws.amazon.com/amplify/latest/userguide/redirects.html#redirects-for-single-page-web-apps-spa + custom_rule { + source = "" + status = "200" + target = "/index.html" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name for an Amplify app. +* `access_token` - (Optional) The personal access token for a third-party source control system for an Amplify app. The personal access token is used to create a webhook and a read-only deploy key. The token is not stored. +* `auto_branch_creation_config` - (Optional) The automated branch creation configuration for an Amplify app. An `auto_branch_creation_config` block is documented below. +* `auto_branch_creation_patterns` - (Optional) The automated branch creation glob patterns for an Amplify app. +* `basic_auth_credentials` - (Optional) The credentials for basic authorization for an Amplify app. +* `build_spec` - (Optional) The [build specification](https://docs.aws.amazon.com/amplify/latest/userguide/build-settings.html) (build spec) for an Amplify app. +* `custom_rule` - (Optional) The custom rewrite and redirect rules for an Amplify app. A `custom_rule` block is documented below. +* `description` - (Optional) The description for an Amplify app. +* `enable_auto_branch_creation` - (Optional) Enables automated branch creation for an Amplify app. +* `enable_basic_auth` - (Optional) Enables basic authorization for an Amplify app. This will apply to all branches that are part of this app. +* `enable_branch_auto_build` - (Optional) Enables auto-building of branches for the Amplify App. +* `enable_branch_auto_deletion` - (Optional) Automatically disconnects a branch in the Amplify Console when you delete a branch from your Git repository. +* `environment_variables` - (Optional) The environment variables map for an Amplify app. +* `iam_service_role_arn` - (Optional) The AWS Identity and Access Management (IAM) service role for an Amplify app. +* `oauth_token` - (Optional) The OAuth token for a third-party source control system for an Amplify app. The OAuth token is used to create a webhook and a read-only deploy key. The OAuth token is not stored. +* `platform` - (Optional) The platform or framework for an Amplify app. Valid values: `WEB`. +* `repository` - (Optional) The repository for an Amplify app. +* `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. + + +An `auto_branch_creation_config` block supports the following arguments: + +* `basic_auth_credentials` - (Optional) The basic authorization credentials for the autocreated branch. +* `build_spec` - (Optional) The build specification (build spec) for the autocreated branch. +* `enable_auto_build` - (Optional) Enables auto building for the autocreated branch. +* `enable_basic_auth` - (Optional) Enables basic authorization for the autocreated branch. +* `enable_performance_mode` - (Optional) Enables performance mode for the branch. +* `enable_pull_request_preview` - (Optional) Enables pull request previews for the autocreated branch. +* `environment_variables` - (Optional) The environment variables for the autocreated branch. +* `framework` - (Optional) The framework for the autocreated branch. +* `pull_request_environment_name` - (Optional) The Amplify environment name for the pull request. +* `stage` - (Optional) Describes the current stage for the autocreated branch. Valid values: `PRODUCTION`, `BETA`, `DEVELOPMENT`, `EXPERIMENTAL`, `PULL_REQUEST`. + +A `custom_rule` block supports the following arguments: + +* `condition` - (Optional) The condition for a URL rewrite or redirect rule, such as a country code. +* `source` - (Required) The source pattern for a URL rewrite or redirect rule. +* `status` - (Optional) The status code for a URL rewrite or redirect rule. Valid values: `200`, `301`, `302`, `404`, `404-200`. +* `target` - (Required) The target pattern for a URL rewrite or redirect rule. + +## Attributes Reference + +The following attributes are exported: + +* `arn` - The Amazon Resource Name (ARN) of the Amplify app. +* `default_domain` - The default domain for the Amplify app. +* `id` - The unique ID of the Amplify app. +* `production_branch` - Describes the information about a production branch for an Amplify app. A `production_branch` block is documented below. +* `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). + +A `production_branch` block supports the following attributes: + +* `branch_name` - The branch name for the production branch. +* `last_deploy_time` - The last deploy time of the production branch. +* `status` - The status of the production branch. +* `thumbnail_url` - The thumbnail URL for the production branch. + +## Import + +Amplify App can be imported using Amplify App ID (appId), e.g. + +``` +$ terraform import aws_amplify_app.app d2ypk4k47z8u6 +``` + +App ID can be obtained from App ARN (e.g. `arn:aws:amplify:us-east-1:12345678:apps/d2ypk4k47z8u6`).