diff --git a/.changelog/36980.txt b/.changelog/36980.txt new file mode 100644 index 00000000000..d2df435fa7c --- /dev/null +++ b/.changelog/36980.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_appconfig_deployment: Fix `ConflictException` errors on resource Create +``` \ No newline at end of file diff --git a/internal/conns/apiretry_test.go b/internal/conns/apiretry_test.go index fec1d6b10f9..8b4d50e874c 100644 --- a/internal/conns/apiretry_test.go +++ b/internal/conns/apiretry_test.go @@ -5,10 +5,15 @@ package conns import ( "errors" + "net/http" "testing" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/retry" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + appconfigtypes "github.com/aws/aws-sdk-go-v2/service/appconfig/types" + smithy "github.com/aws/smithy-go" + smithyhttp "github.com/aws/smithy-go/transport/http" "github.com/hashicorp/terraform-provider-aws/internal/errs" ) @@ -24,18 +29,55 @@ func TestAddIsErrorRetryables(t *testing.T) { testCases := []struct { name string err error + f retry.IsErrorRetryableFunc expected bool }{ { name: "no error", + f: f, }, { name: "non-retryable", err: errors.New(`this is not retryable`), + f: f, }, { name: "retryable", err: errors.New(`this is testing`), + f: f, + expected: true, + }, + { + // https://github.com/hashicorp/terraform-provider-aws/issues/36975. + name: "appconfig ConflictException", + err: &smithy.OperationError{ + ServiceID: "AppConfig", + OperationName: "StartDeployment", + Err: &awshttp.ResponseError{ + ResponseError: &smithyhttp.ResponseError{ + Response: &smithyhttp.Response{ + Response: &http.Response{ + StatusCode: http.StatusConflict, + }, + }, + Err: &appconfigtypes.ConflictException{ + Message: aws.String("Deployment number 1 already exists"), + }, + }, + RequestID: "43e844da-818b-458e-aae2-553960ccc4d6", + }, + }, + f: func(err error) aws.Ternary { + if err, ok := errs.As[*smithy.OperationError](err); ok { + switch err.OperationName { + case "StartDeployment": + if errs.IsA[*appconfigtypes.ConflictException](err) { + return aws.TrueTernary + } + } + } + return aws.UnknownTernary + }, expected: true, }, } @@ -45,7 +87,7 @@ func TestAddIsErrorRetryables(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - got := AddIsErrorRetryables(retry.NewStandard(), retry.IsErrorRetryableFunc(f)).IsErrorRetryable(testCase.err) + got := AddIsErrorRetryables(retry.NewStandard(), testCase.f).IsErrorRetryable(testCase.err) if got, want := got, testCase.expected; got != want { t.Errorf("IsErrorRetryable(%q) = %v, want %v", testCase.err, got, want) } diff --git a/internal/service/appconfig/deployment.go b/internal/service/appconfig/deployment.go index 07844d6b163..87d0f291011 100644 --- a/internal/service/appconfig/deployment.go +++ b/internal/service/appconfig/deployment.go @@ -9,6 +9,7 @@ import ( "log" "strconv" "strings" + "time" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" @@ -22,6 +23,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -124,16 +126,18 @@ func resourceDeploymentCreate(ctx context.Context, d *schema.ResourceData, meta input.KmsKeyIdentifier = aws.String(v.(string)) } - output, err := conn.StartDeployment(ctx, input) + const ( + timeout = 30 * time.Minute // AWS SDK for Go v1 compatibility. + ) + outputRaw, err := tfresource.RetryWhenIsA[*awstypes.ConflictException](ctx, timeout, func() (interface{}, error) { + return conn.StartDeployment(ctx, input) + }) if err != nil { return sdkdiag.AppendErrorf(diags, "starting AppConfig Deployment: %s", err) } - if output == nil { - return sdkdiag.AppendErrorf(diags, "starting AppConfig Deployment: empty response") - } - + output := outputRaw.(*appconfig.StartDeploymentOutput) appID := aws.ToString(output.ApplicationId) envID := aws.ToString(output.EnvironmentId) diff --git a/internal/service/appconfig/deployment_test.go b/internal/service/appconfig/deployment_test.go index a801ab316fc..a97e22bf2f8 100644 --- a/internal/service/appconfig/deployment_test.go +++ b/internal/service/appconfig/deployment_test.go @@ -34,9 +34,7 @@ func TestAccAppConfigDeployment_basic(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.AppConfigServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - // AppConfig Deployments cannot be destroyed, but we want to ensure - // the Application and its dependents are removed. - CheckDestroy: testAccCheckApplicationDestroy(ctx), + CheckDestroy: acctest.CheckDestroyNoop, Steps: []resource.TestStep{ { Config: testAccDeploymentConfig_name(rName), @@ -77,9 +75,7 @@ func TestAccAppConfigDeployment_kms(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.AppConfigServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - // AppConfig Deployments cannot be destroyed, but we want to ensure - // the Application and its dependents are removed. - CheckDestroy: testAccCheckApplicationDestroy(ctx), + CheckDestroy: acctest.CheckDestroyNoop, Steps: []resource.TestStep{ { Config: testAccDeploymentConfig_kms(rName), @@ -112,9 +108,7 @@ func TestAccAppConfigDeployment_predefinedStrategy(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.AppConfigServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - // AppConfig Deployments cannot be destroyed, but we want to ensure - // the Application and its dependents are removed. - CheckDestroy: testAccCheckApplicationDestroy(ctx), + CheckDestroy: acctest.CheckDestroyNoop, Steps: []resource.TestStep{ { Config: testAccDeploymentConfig_predefinedStrategy(rName, strategy), @@ -146,7 +140,7 @@ func TestAccAppConfigDeployment_tags(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.AppConfigServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, + CheckDestroy: acctest.CheckDestroyNoop, Steps: []resource.TestStep{ { Config: testAccDeploymentConfig_tags1(rName, "key1", "value1"), @@ -182,6 +176,31 @@ func TestAccAppConfigDeployment_tags(t *testing.T) { }) } +func TestAccAppConfigDeployment_multiple(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resource1Name := "aws_appconfig_deployment.test.0" + resource2Name := "aws_appconfig_deployment.test.1" + resource3Name := "aws_appconfig_deployment.test.2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.AppConfigServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccDeploymentConfig_multiple(rName, 3), + Check: resource.ComposeTestCheckFunc( + testAccCheckDeploymentExists(ctx, resource1Name), + testAccCheckDeploymentExists(ctx, resource2Name), + testAccCheckDeploymentExists(ctx, resource3Name), + ), + }, + }, + }) +} + func testAccCheckDeploymentExists(ctx context.Context, resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -221,7 +240,7 @@ func testAccCheckDeploymentExists(ctx context.Context, resourceName string) reso } } -func testAccDeploymentBaseConfig(rName string) string { +func testAccDeploymentConfig_base(rName string) string { return fmt.Sprintf(` resource "aws_appconfig_application" "test" { name = %[1]q @@ -259,7 +278,7 @@ resource "aws_appconfig_hosted_configuration_version" "test" { `, rName) } -func testAccDeploymentKMSConfig(rName string) string { +func testAccDeploymentConfig_baseKMS(rName string) string { return fmt.Sprintf(` resource "aws_kms_key" "test" { description = %[1]q @@ -304,9 +323,7 @@ resource "aws_appconfig_hosted_configuration_version" "test" { } func testAccDeploymentConfig_name(rName string) string { - return acctest.ConfigCompose( - testAccDeploymentBaseConfig(rName), - fmt.Sprintf(` + return acctest.ConfigCompose(testAccDeploymentConfig_base(rName), fmt.Sprintf(` resource "aws_appconfig_deployment" "test"{ application_id = aws_appconfig_application.test.id configuration_profile_id = aws_appconfig_configuration_profile.test.configuration_profile_id @@ -319,9 +336,7 @@ resource "aws_appconfig_deployment" "test"{ } func testAccDeploymentConfig_kms(rName string) string { - return acctest.ConfigCompose( - testAccDeploymentKMSConfig(rName), - fmt.Sprintf(` + return acctest.ConfigCompose(testAccDeploymentConfig_baseKMS(rName), fmt.Sprintf(` resource "aws_appconfig_deployment" "test"{ application_id = aws_appconfig_application.test.id configuration_profile_id = aws_appconfig_configuration_profile.test.configuration_profile_id @@ -335,9 +350,7 @@ resource "aws_appconfig_deployment" "test"{ } func testAccDeploymentConfig_predefinedStrategy(rName, strategy string) string { - return acctest.ConfigCompose( - testAccDeploymentBaseConfig(rName), - fmt.Sprintf(` + return acctest.ConfigCompose(testAccDeploymentConfig_base(rName), fmt.Sprintf(` resource "aws_appconfig_deployment" "test"{ application_id = aws_appconfig_application.test.id configuration_profile_id = aws_appconfig_configuration_profile.test.configuration_profile_id @@ -350,9 +363,7 @@ resource "aws_appconfig_deployment" "test"{ } func testAccDeploymentConfig_tags1(rName, tagKey1, tagValue1 string) string { - return acctest.ConfigCompose( - testAccDeploymentBaseConfig(rName), - fmt.Sprintf(` + return acctest.ConfigCompose(testAccDeploymentConfig_base(rName), fmt.Sprintf(` resource "aws_appconfig_deployment" "test"{ application_id = aws_appconfig_application.test.id configuration_profile_id = aws_appconfig_configuration_profile.test.configuration_profile_id @@ -368,9 +379,7 @@ resource "aws_appconfig_deployment" "test"{ } func testAccDeploymentConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { - return acctest.ConfigCompose( - testAccDeploymentBaseConfig(rName), - fmt.Sprintf(` + return acctest.ConfigCompose(testAccDeploymentConfig_base(rName), fmt.Sprintf(` resource "aws_appconfig_deployment" "test"{ application_id = aws_appconfig_application.test.id configuration_profile_id = aws_appconfig_configuration_profile.test.configuration_profile_id @@ -385,3 +394,56 @@ resource "aws_appconfig_deployment" "test"{ } `, rName, tagKey1, tagValue1, tagKey2, tagValue2)) } + +func testAccDeploymentConfig_multiple(rName string, n int) string { + return fmt.Sprintf(` +resource "aws_appconfig_application" "test" { + name = %[1]q +} + +resource "aws_appconfig_environment" "test" { + name = %[1]q + application_id = aws_appconfig_application.test.id +} + +resource "aws_appconfig_configuration_profile" "test" { + count = %[2]d + + application_id = aws_appconfig_application.test.id + name = "%[1]s-${count.index}" + location_uri = "hosted" +} + +resource "aws_appconfig_deployment_strategy" "test" { + name = %[1]q + deployment_duration_in_minutes = 3 + growth_factor = 10 + replicate_to = "NONE" +} + +resource "aws_appconfig_hosted_configuration_version" "test" { + count = %[2]d + + application_id = aws_appconfig_application.test.id + configuration_profile_id = aws_appconfig_configuration_profile.test[count.index].configuration_profile_id + content_type = "application/json" + + content = jsonencode({ + foo = "bar" + }) + + description = "%[1]s-${count.index}" +} + +resource "aws_appconfig_deployment" "test" { + count = %[2]d + + application_id = aws_appconfig_application.test.id + configuration_profile_id = aws_appconfig_configuration_profile.test[count.index].configuration_profile_id + configuration_version = aws_appconfig_hosted_configuration_version.test[count.index].version_number + description = "%[1]s-${count.index}" + deployment_strategy_id = aws_appconfig_deployment_strategy.test.id + environment_id = aws_appconfig_environment.test.environment_id +} +`, rName, n) +} diff --git a/internal/service/appconfig/service_package.go b/internal/service/appconfig/service_package.go deleted file mode 100644 index 4bcca3d4550..00000000000 --- a/internal/service/appconfig/service_package.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package appconfig - -import ( - "context" - - aws_sdkv2 "github.com/aws/aws-sdk-go-v2/aws" - retry_sdkv2 "github.com/aws/aws-sdk-go-v2/aws/retry" - appconfig_sdkv2 "github.com/aws/aws-sdk-go-v2/service/appconfig" - awstypes "github.com/aws/aws-sdk-go-v2/service/appconfig/types" - "github.com/aws/smithy-go" - "github.com/hashicorp/terraform-provider-aws/internal/conns" - "github.com/hashicorp/terraform-provider-aws/internal/errs" -) - -func (p *servicePackage) NewClient(ctx context.Context, config map[string]any) (*appconfig_sdkv2.Client, error) { - cfg := *(config["aws_sdkv2_config"].(*aws_sdkv2.Config)) - - return appconfig_sdkv2.NewFromConfig(cfg, func(o *appconfig_sdkv2.Options) { - if endpoint := config["endpoint"].(string); endpoint != "" { - o.BaseEndpoint = aws_sdkv2.String(endpoint) - } - - // StartDeployment operations can return a ConflictException - // if ongoing deployments are in-progress, thus we handle them - // here for the service client. - o.Retryer = conns.AddIsErrorRetryables(cfg.Retryer().(aws_sdkv2.RetryerV2), retry_sdkv2.IsErrorRetryableFunc(func(err error) aws_sdkv2.Ternary { - if v, ok := errs.As[*smithy.OperationError](err); ok { - switch v.OperationName { - case "StartDeployment": - if errs.IsA[*awstypes.ConflictException](err) { - return aws_sdkv2.TrueTernary - } - } - } - return aws_sdkv2.UnknownTernary - })) - }), nil -} diff --git a/internal/service/appconfig/service_package_gen.go b/internal/service/appconfig/service_package_gen.go index 36a5da52582..b8aa48189fd 100644 --- a/internal/service/appconfig/service_package_gen.go +++ b/internal/service/appconfig/service_package_gen.go @@ -5,6 +5,8 @@ package appconfig import ( "context" + aws_sdkv2 "github.com/aws/aws-sdk-go-v2/aws" + appconfig_sdkv2 "github.com/aws/aws-sdk-go-v2/service/appconfig" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/types" "github.com/hashicorp/terraform-provider-aws/names" @@ -105,6 +107,17 @@ func (p *servicePackage) ServicePackageName() string { return names.AppConfig } +// NewClient returns a new AWS SDK for Go v2 client for this service package's AWS API. +func (p *servicePackage) NewClient(ctx context.Context, config map[string]any) (*appconfig_sdkv2.Client, error) { + cfg := *(config["aws_sdkv2_config"].(*aws_sdkv2.Config)) + + return appconfig_sdkv2.NewFromConfig(cfg, func(o *appconfig_sdkv2.Options) { + if endpoint := config["endpoint"].(string); endpoint != "" { + o.BaseEndpoint = aws_sdkv2.String(endpoint) + } + }), nil +} + func ServicePackage(ctx context.Context) conns.ServicePackage { return &servicePackage{} } diff --git a/names/data/names_data.csv b/names/data/names_data.csv index 6368a1fb2b0..48bab774dd8 100644 --- a/names/data/names_data.csv +++ b/names/data/names_data.csv @@ -16,7 +16,7 @@ appfabric,appfabric,appfabric,appfabric,,appfabric,,,AppFabric,AppFabric,,,2,,aw appmesh,appmesh,appmesh,appmesh,,appmesh,,,AppMesh,AppMesh,,1,,,aws_appmesh_,,appmesh_,App Mesh,AWS,,,,,,,App Mesh,ListMeshes,, apprunner,apprunner,apprunner,apprunner,,apprunner,,,AppRunner,AppRunner,,,2,,aws_apprunner_,,apprunner_,App Runner,AWS,,,,,,,AppRunner,ListConnections,, ,,,,,,,,,,,,,,,,,App2Container,AWS,x,,,,,,,,,No SDK support -appconfig,appconfig,appconfig,appconfig,,appconfig,,,AppConfig,AppConfig,x,,2,,aws_appconfig_,,appconfig_,AppConfig,AWS,,,,,,,AppConfig,ListApplications,, +appconfig,appconfig,appconfig,appconfig,,appconfig,,,AppConfig,AppConfig,,,2,,aws_appconfig_,,appconfig_,AppConfig,AWS,,,,,,,AppConfig,ListApplications,, appconfigdata,appconfigdata,appconfigdata,appconfigdata,,appconfigdata,,,AppConfigData,AppConfigData,,1,,,aws_appconfigdata_,,appconfigdata_,AppConfig Data,AWS,,x,,,,,AppConfigData,,, appflow,appflow,appflow,appflow,,appflow,,,AppFlow,Appflow,,,2,,aws_appflow_,,appflow_,AppFlow,Amazon,,,,,,,Appflow,ListFlows,, appintegrations,appintegrations,appintegrationsservice,appintegrations,,appintegrations,,appintegrationsservice,AppIntegrations,AppIntegrationsService,,1,,,aws_appintegrations_,,appintegrations_,AppIntegrations,Amazon,,,,,,,AppIntegrations,ListApplications,,