diff --git a/.changelog/40004.txt b/.changelog/40004.txt new file mode 100644 index 00000000000..52d99688db7 --- /dev/null +++ b/.changelog/40004.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_api_gateway_account: Add attribute `reset_on_delete` to properly reset CloudWatch Role ARN on deletion. +``` diff --git a/internal/framework/flex/auto_flatten.go b/internal/framework/flex/auto_flatten.go index 0e7c346120e..2bc9cd4bc55 100644 --- a/internal/framework/flex/auto_flatten.go +++ b/internal/framework/flex/auto_flatten.go @@ -684,6 +684,8 @@ func (flattener autoFlattener) struct_(ctx context.Context, sourcePath path.Path return diags } + tflog.SubsystemError(ctx, subsystemName, "Flattening incompatible types") + return diags } diff --git a/internal/service/apigateway/account.go b/internal/service/apigateway/account.go index 329d6d6a2e5..fa431e794cc 100644 --- a/internal/service/apigateway/account.go +++ b/internal/service/apigateway/account.go @@ -5,139 +5,300 @@ package apigateway import ( "context" - "log" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/apigateway" - "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-provider-aws/internal/conns" + awstypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-provider-aws/internal/errs" - "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/framework/validators" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" - "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" ) -// @SDKResource("aws_api_gateway_account", name="Account") -func resourceAccount() *schema.Resource { - return &schema.Resource{ - CreateWithoutTimeout: resourceAccountUpdate, - ReadWithoutTimeout: resourceAccountRead, - UpdateWithoutTimeout: resourceAccountUpdate, - DeleteWithoutTimeout: schema.NoopContext, +// @FrameworkResource("aws_api_gateway_account") +func newResourceAccount(context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceAccount{} - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, + return r, nil +} - Schema: map[string]*schema.Schema{ - "api_key_version": { - Type: schema.TypeString, +type resourceAccount struct { + framework.ResourceWithConfigure +} + +func (r *resourceAccount) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_api_gateway_account" +} + +func (r *resourceAccount) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) { + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "api_key_version": schema.StringAttribute{ Computed: true, }, - "cloudwatch_role_arn": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: verify.ValidARN, - }, - "features": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, + "cloudwatch_role_arn": schema.StringAttribute{ + Optional: true, Computed: true, + Validators: []validator.String{ + stringvalidator.Any( + validators.ARN(), + stringvalidator.OneOf(""), + ), + }, + Default: stringdefault.StaticString(""), // Needed for backwards compatibility with SDK resource }, - "throttle_settings": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "burst_limit": { - Type: schema.TypeInt, - Computed: true, - }, - "rate_limit": { - Type: schema.TypeFloat, - Computed: true, - }, - }, + "features": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, + names.AttrID: schema.StringAttribute{ + Computed: true, + DeprecationMessage: `The "id" attribute is unused and will be removed in a future version of the provider`, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "reset_on_delete": schema.BoolAttribute{ + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), }, + DeprecationMessage: `The "reset_on_delete" attribute will be removed in a future version of the provider`, }, + "throttle_settings": framework.DataSourceComputedListOfObjectAttribute[throttleSettingsModel](ctx), }, } + + response.Schema = s } -func resourceAccountUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).APIGatewayClient(ctx) +func (r *resourceAccount) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + var data resourceAccountModel + response.Diagnostics.Append(request.Plan.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().APIGatewayClient(ctx) input := &apigateway.UpdateAccountInput{} - if v, ok := d.GetOk("cloudwatch_role_arn"); ok { - input.PatchOperations = []types.PatchOperation{{ - Op: types.OpReplace, - Path: aws.String("/cloudwatchRoleArn"), - Value: aws.String(v.(string)), - }} + if data.CloudwatchRoleARN.IsNull() || data.CloudwatchRoleARN.ValueString() == "" { + input.PatchOperations = []awstypes.PatchOperation{ + { + Op: awstypes.OpReplace, + Path: aws.String("/cloudwatchRoleArn"), + Value: nil, + }, + } } else { - input.PatchOperations = []types.PatchOperation{{ - Op: types.OpReplace, - Path: aws.String("/cloudwatchRoleArn"), - Value: aws.String(""), - }} + input.PatchOperations = []awstypes.PatchOperation{ + { + Op: awstypes.OpReplace, + Path: aws.String("/cloudwatchRoleArn"), + Value: data.CloudwatchRoleARN.ValueStringPointer(), + }, + } } - _, err := tfresource.RetryWhen(ctx, propagationTimeout, - func() (interface{}, error) { + output, err := tfresource.RetryGWhen(ctx, propagationTimeout, + func() (*apigateway.UpdateAccountOutput, error) { return conn.UpdateAccount(ctx, input) }, func(err error) (bool, error) { - if errs.IsAErrorMessageContains[*types.BadRequestException](err, "The role ARN does not have required permissions") { + if errs.IsAErrorMessageContains[*awstypes.BadRequestException](err, "The role ARN does not have required permissions") { return true, err } - - if errs.IsAErrorMessageContains[*types.BadRequestException](err, "API Gateway could not successfully write to CloudWatch Logs using the ARN specified") { + if errs.IsAErrorMessageContains[*awstypes.BadRequestException](err, "API Gateway could not successfully write to CloudWatch Logs using the ARN specified") { return true, err } - return false, err }, ) - if err != nil { - return sdkdiag.AppendErrorf(diags, "updating API Gateway Account: %s", err) + response.Diagnostics.AddError("creating API Gateway Account", err.Error()) + return } - if d.IsNewResource() { - d.SetId("api-gateway-account") - } + response.Diagnostics.Append(flex.Flatten(ctx, output, &data)...) + data.ID = types.StringValue("api-gateway-account") - return append(diags, resourceAccountRead(ctx, d, meta)...) + response.Diagnostics.Append(response.State.Set(ctx, &data)...) } -func resourceAccountRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).APIGatewayClient(ctx) +func (r *resourceAccount) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + var data resourceAccountModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + conn := r.Meta().APIGatewayClient(ctx) account, err := findAccount(ctx, conn) + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + return + } + if err != nil { + response.Diagnostics.AddError("reading API Gateway Account", err.Error()) + return + } - if !d.IsNewResource() && tfresource.NotFound(err) { - log.Printf("[WARN] API Gateway Account (%s) not found, removing from state", d.Id()) - d.SetId("") - return diags + response.Diagnostics.Append(flex.Flatten(ctx, account, &data)...) + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *resourceAccount) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + var state, plan resourceAccountModel + response.Diagnostics.Append(request.State.Get(ctx, &state)...) + if response.Diagnostics.HasError() { + return } - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading API Gateway Account: %s", err) + response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) + if response.Diagnostics.HasError() { + return } - d.Set("api_key_version", account.ApiKeyVersion) - d.Set("cloudwatch_role_arn", account.CloudwatchRoleArn) - d.Set("features", account.Features) - if err := d.Set("throttle_settings", flattenThrottleSettings(account.ThrottleSettings)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting throttle_settings: %s", err) + diff, d := flex.Calculate(ctx, plan, state) + response.Diagnostics.Append(d...) + if response.Diagnostics.HasError() { + return } - return diags + if diff.HasChanges() { + conn := r.Meta().APIGatewayClient(ctx) + + input := &apigateway.UpdateAccountInput{} + + if plan.CloudwatchRoleARN.IsNull() || plan.CloudwatchRoleARN.ValueString() == "" { + input.PatchOperations = []awstypes.PatchOperation{ + { + Op: awstypes.OpReplace, + Path: aws.String("/cloudwatchRoleArn"), + Value: nil, + }, + } + } else { + input.PatchOperations = []awstypes.PatchOperation{ + { + Op: awstypes.OpReplace, + Path: aws.String("/cloudwatchRoleArn"), + Value: plan.CloudwatchRoleARN.ValueStringPointer(), + }, + } + } + + output, err := tfresource.RetryGWhen(ctx, propagationTimeout, + func() (*apigateway.UpdateAccountOutput, error) { + return conn.UpdateAccount(ctx, input) + }, + func(err error) (bool, error) { + if errs.IsAErrorMessageContains[*awstypes.BadRequestException](err, "The role ARN does not have required permissions") { + return true, err + } + if errs.IsAErrorMessageContains[*awstypes.BadRequestException](err, "API Gateway could not successfully write to CloudWatch Logs using the ARN specified") { + return true, err + } + return false, err + }, + ) + if err != nil { + response.Diagnostics.AddError("updating API Gateway Account", err.Error()) + return + } + + response.Diagnostics.Append(flex.Flatten(ctx, output, &plan)...) + } + + response.Diagnostics.Append(response.State.Set(ctx, &plan)...) +} + +func (r *resourceAccount) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + var data resourceAccountModel + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + if data.ResetOnDelete.ValueBool() { + conn := r.Meta().APIGatewayClient(ctx) + + input := &apigateway.UpdateAccountInput{} + + input.PatchOperations = []awstypes.PatchOperation{{ + Op: awstypes.OpReplace, + Path: aws.String("/cloudwatchRoleArn"), + Value: nil, + }} + + _, err := conn.UpdateAccount(ctx, input) + if err != nil { + response.Diagnostics.AddError("resetting API Gateway Account", err.Error()) + } + } else { + response.Diagnostics.AddWarning( + "Resource Destruction", + "This resource has only been removed from Terraform state. "+ + "Manually use the AWS Console to fully destroy this resource. "+ + "Setting the attribute \"reset_on_delete\" will also fully destroy resources of this type.", + ) + } +} + +func (r *resourceAccount) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrID), request, response) +} + +type resourceAccountModel struct { + ApiKeyVersion types.String `tfsdk:"api_key_version"` + CloudwatchRoleARN types.String `tfsdk:"cloudwatch_role_arn" autoflex:",legacy"` + Features types.Set `tfsdk:"features"` + ID types.String `tfsdk:"id"` + ResetOnDelete types.Bool `tfsdk:"reset_on_delete"` + ThrottleSettings fwtypes.ListNestedObjectValueOf[throttleSettingsModel] `tfsdk:"throttle_settings"` +} + +type throttleSettingsModel struct { + BurstLimit types.Int32 `tfsdk:"burst_limit"` + RateLimit types.Float64 `tfsdk:"rate_limit"` +} + +func (r *resourceAccount) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + // If the entire plan is null, the resource is planned for destruction. + if request.Plan.Raw.IsNull() { + var resetOnDelete types.Bool + response.Diagnostics.Append(request.State.GetAttribute(ctx, path.Root("reset_on_delete"), &resetOnDelete)...) + if response.Diagnostics.HasError() { + return + } + + if !resetOnDelete.ValueBool() { + response.Diagnostics.AddWarning( + "Resource Destruction", + "Applying this resource destruction will only remove the resource from Terraform state and will not reset account settings. "+ + "Either manually use the AWS Console to fully destroy this resource or "+ + "update the resource with \"reset_on_delete\" set to true.", + ) + } + } } func findAccount(ctx context.Context, conn *apigateway.Client) (*apigateway.GetAccountOutput, error) { diff --git a/internal/service/apigateway/account_test.go b/internal/service/apigateway/account_test.go index 2e81280fb17..c63e66ce9c7 100644 --- a/internal/service/apigateway/account_test.go +++ b/internal/service/apigateway/account_test.go @@ -4,34 +4,88 @@ package apigateway_test import ( + "context" + "errors" "fmt" "testing" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + awstypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/hashicorp/terraform-plugin-testing/compare" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfapigateway "github.com/hashicorp/terraform-provider-aws/internal/service/apigateway" "github.com/hashicorp/terraform-provider-aws/names" ) func testAccAccount_basic(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_api_gateway_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + t.Cleanup(accountCleanup(ctx, t)) + }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAccountDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAccountConfig_basic, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("api_key_version"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("cloudwatch_role_arn"), knownvalue.StringExact("")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("features"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.StringExact("api-gateway-account")), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("reset_on_delete"), knownvalue.Null()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("throttle_settings"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "burst_limit": knownvalue.Int32Exact(5000), + "rate_limit": knownvalue.Float64Exact(10000), + }), + })), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAccount_cloudwatchRoleARN_value(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_api_gateway_account.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(ctx, t) }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + t.Cleanup(accountCleanup(ctx, t)) + }, ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: acctest.CheckDestroyNoop, + CheckDestroy: testAccCheckAccountDestroy(ctx), Steps: []resource.TestStep{ { Config: testAccAccountConfig_role0(rName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair(resourceName, "cloudwatch_role_arn", "aws_iam_role.test.0", names.AttrARN), - resource.TestCheckResourceAttr(resourceName, "throttle_settings.#", "1"), - resource.TestCheckResourceAttrSet(resourceName, "api_key_version"), - resource.TestCheckResourceAttrSet(resourceName, "features.#"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + resourceName, tfjsonpath.New("cloudwatch_role_arn"), + "aws_iam_role.test[0]", tfjsonpath.New(names.AttrARN), + compare.ValuesSame(), + ), + }, }, { ResourceName: resourceName, @@ -40,23 +94,266 @@ func testAccAccount_basic(t *testing.T) { }, { Config: testAccAccountConfig_role1(rName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair(resourceName, "cloudwatch_role_arn", "aws_iam_role.test.1", names.AttrARN), - resource.TestCheckResourceAttr(resourceName, "throttle_settings.#", "1"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + resourceName, tfjsonpath.New("cloudwatch_role_arn"), + "aws_iam_role.test[1]", tfjsonpath.New(names.AttrARN), + compare.ValuesSame(), + ), + }, }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAccountConfig_basic, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("cloudwatch_role_arn"), knownvalue.StringExact("")), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAccount_cloudwatchRoleARN_empty(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_api_gateway_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + t.Cleanup(accountCleanup(ctx, t)) + }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAccountDestroy(ctx), + Steps: []resource.TestStep{ { Config: testAccAccountConfig_empty, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "cloudwatch_role_arn", ""), - resource.TestCheckResourceAttr(resourceName, "throttle_settings.#", "1"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("cloudwatch_role_arn"), knownvalue.StringExact("")), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) } -const testAccAccountConfig_empty = ` +func testAccAccount_resetOnDelete_false(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_api_gateway_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + t.Cleanup(accountCleanup(ctx, t)) + }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAccountNotDestroyed(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAccountConfig_resetOnDelete(rName, false), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + resourceName, tfjsonpath.New("cloudwatch_role_arn"), + "aws_iam_role.test[0]", tfjsonpath.New(names.AttrARN), + compare.ValuesSame(), + ), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("reset_on_delete"), knownvalue.Bool(false)), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "reset_on_delete", + }, + }, + }, + }) +} + +func testAccAccount_resetOnDelete_true(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_api_gateway_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + t.Cleanup(accountCleanup(ctx, t)) + }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAccountDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccAccountConfig_resetOnDelete(rName, true), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + resourceName, tfjsonpath.New("cloudwatch_role_arn"), + "aws_iam_role.test[0]", tfjsonpath.New(names.AttrARN), + compare.ValuesSame(), + ), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("reset_on_delete"), knownvalue.Bool(true)), + }, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "reset_on_delete", + }, + }, + }, + }) +} + +func testAccAccount_frameworkMigration_basic(t *testing.T) { + ctx := acctest.Context(t) + resourceName := "aws_api_gateway_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + t.Cleanup(accountCleanup(ctx, t)) + }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + CheckDestroy: testAccCheckAccountDestroy(ctx), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "aws": { + Source: "hashicorp/aws", + VersionConstraint: "5.74.0", + }, + }, + Config: testAccAccountConfig_basic, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("cloudwatch_role_arn"), knownvalue.StringExact("")), + }, + }, + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Config: testAccAccountConfig_basic, + PlanOnly: true, + }, + }, + }) +} + +func testAccAccount_frameworkMigration_cloudwatchRoleARN(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_api_gateway_account.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + t.Cleanup(accountCleanup(ctx, t)) + }, + ErrorCheck: acctest.ErrorCheck(t, names.APIGatewayServiceID), + CheckDestroy: testAccCheckAccountNotDestroyed(ctx), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "aws": { + Source: "hashicorp/aws", + VersionConstraint: "5.74.0", + }, + }, + Config: testAccAccountConfig_role0(rName), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + resourceName, tfjsonpath.New("cloudwatch_role_arn"), + "aws_iam_role.test[0]", tfjsonpath.New(names.AttrARN), + compare.ValuesSame(), + ), + }, + }, + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Config: testAccAccountConfig_role0(rName), + PlanOnly: true, + }, + }, + }) +} + +func testAccCheckAccountDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).APIGatewayClient(ctx) + + account, err := tfapigateway.FindAccount(ctx, conn) + if err != nil { + return err + } + + if account.CloudwatchRoleArn == nil { + // Settings have been reset + return nil + } + + return errors.New("API Gateway Account still exists") + } +} + +func testAccCheckAccountNotDestroyed(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).APIGatewayClient(ctx) + + account, err := tfapigateway.FindAccount(ctx, conn) + if err != nil { + return err + } + + if account.CloudwatchRoleArn != nil { + // Settings have not been reset + return nil + } + + return errors.New("API Gateway Account was reset") + } +} + +func accountCleanup(ctx context.Context, t *testing.T) func() { + return func() { + t.Helper() + + conn := acctest.Provider.Meta().(*conns.AWSClient).APIGatewayClient(ctx) + + input := &apigateway.UpdateAccountInput{ + PatchOperations: []awstypes.PatchOperation{ + { + Op: awstypes.OpReplace, + Path: aws.String("/cloudwatchRoleArn"), + Value: nil, + }, + }, + } + + if _, err := conn.UpdateAccount(ctx, input); err != nil { + t.Errorf("API Gateway Account cleanup: %s", err) + } + } +} + +const testAccAccountConfig_basic = ` resource "aws_api_gateway_account" "test" {} ` @@ -86,7 +383,8 @@ resource "aws_iam_role" "test" { } func testAccAccountConfig_role0(rName string) string { - return acctest.ConfigCompose(testAccAccountConfig_base(rName), ` + return acctest.ConfigCompose( + testAccAccountConfig_base(rName), ` resource "aws_api_gateway_account" "test" { cloudwatch_role_arn = aws_iam_role.test[0].arn } @@ -94,9 +392,27 @@ resource "aws_api_gateway_account" "test" { } func testAccAccountConfig_role1(rName string) string { - return acctest.ConfigCompose(testAccAccountConfig_base(rName), ` + return acctest.ConfigCompose( + testAccAccountConfig_base(rName), ` resource "aws_api_gateway_account" "test" { cloudwatch_role_arn = aws_iam_role.test[1].arn } `) } + +const testAccAccountConfig_empty = ` +resource "aws_api_gateway_account" "test" { + cloudwatch_role_arn = "" +} +` + +func testAccAccountConfig_resetOnDelete(rName string, reset bool) string { + return acctest.ConfigCompose( + testAccAccountConfig_base(rName), + fmt.Sprintf(` +resource "aws_api_gateway_account" "test" { + cloudwatch_role_arn = aws_iam_role.test[0].arn + reset_on_delete = %[1]t +} +`, reset)) +} diff --git a/internal/service/apigateway/apigateway_test.go b/internal/service/apigateway/apigateway_test.go index f34ce15901e..b83d53d7532 100644 --- a/internal/service/apigateway/apigateway_test.go +++ b/internal/service/apigateway/apigateway_test.go @@ -25,7 +25,13 @@ func testAccErrorCheckSkip(t *testing.T) resource.ErrorCheckFunc { func TestAccAPIGateway_serial(t *testing.T) { testCases := map[string]map[string]func(t *testing.T){ "Account": { - acctest.CtBasic: testAccAccount_basic, + acctest.CtBasic: testAccAccount_basic, + "CloudwatchRoleARN_Value": testAccAccount_cloudwatchRoleARN_value, + "CloudwatchRoleARN_Empty": testAccAccount_cloudwatchRoleARN_empty, + "FrameworkMigration_Basic": testAccAccount_frameworkMigration_basic, + "FrameworkMigration_CloudwatchRoleARN": testAccAccount_frameworkMigration_cloudwatchRoleARN, + "ResetOnDelete_false": testAccAccount_resetOnDelete_false, + "ResetOnDelete_true": testAccAccount_resetOnDelete_true, }, // Some aws_api_gateway_method_settings tests require the account-level CloudWatch Logs role ARN to be set. // Serialize all this resource's acceptance tests. diff --git a/internal/service/apigateway/exports_test.go b/internal/service/apigateway/exports_test.go index a0ad73271ce..8933b86d518 100644 --- a/internal/service/apigateway/exports_test.go +++ b/internal/service/apigateway/exports_test.go @@ -5,7 +5,6 @@ package apigateway // Exports for use in tests only. var ( - ResourceAccount = resourceAccount ResourceAPIKey = resourceAPIKey ResourceAuthorizer = resourceAuthorizer ResourceBasePathMapping = resourceBasePathMapping @@ -31,6 +30,7 @@ var ( ResourceVPCLink = resourceVPCLink DefaultAuthorizerTTL = defaultAuthorizerTTL + FindAccount = findAccount FindAPIKeyByID = findAPIKeyByID FindAuthorizerByTwoPartKey = findAuthorizerByTwoPartKey FindBasePathMappingByTwoPartKey = findBasePathMappingByTwoPartKey diff --git a/internal/service/apigateway/method_settings_test.go b/internal/service/apigateway/method_settings_test.go index ee807ac65e5..fbb1fba5968 100644 --- a/internal/service/apigateway/method_settings_test.go +++ b/internal/service/apigateway/method_settings_test.go @@ -619,7 +619,9 @@ func testAccMethodSettingsImportStateIdFunc(resourceName string) resource.Import } func testAccMethodSettingsConfig_base(rName string) string { - return acctest.ConfigCompose(testAccAccountConfig_role0(rName), fmt.Sprintf(` + return acctest.ConfigCompose( + testAccAccountConfig_resetOnDelete(rName, true), + fmt.Sprintf(` resource "aws_api_gateway_rest_api" "test" { name = %[1]q } diff --git a/internal/service/apigateway/service_package_gen.go b/internal/service/apigateway/service_package_gen.go index 0c9dbd98479..ae94df64315 100644 --- a/internal/service/apigateway/service_package_gen.go +++ b/internal/service/apigateway/service_package_gen.go @@ -17,7 +17,11 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { - return []*types.ServicePackageFrameworkResource{} + return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceAccount, + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { @@ -76,11 +80,6 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePackageSDKResource { return []*types.ServicePackageSDKResource{ - { - Factory: resourceAccount, - TypeName: "aws_api_gateway_account", - Name: "Account", - }, { Factory: resourceAPIKey, TypeName: "aws_api_gateway_api_key", diff --git a/internal/service/apigateway/stage_test.go b/internal/service/apigateway/stage_test.go index 906af2e8daa..9508db920d8 100644 --- a/internal/service/apigateway/stage_test.go +++ b/internal/service/apigateway/stage_test.go @@ -551,7 +551,9 @@ func testAccStageImportStateIdFunc(resourceName string) resource.ImportStateIdFu } func testAccStageConfig_base(rName string) string { - return acctest.ConfigCompose(testAccAccountConfig_role0(rName), fmt.Sprintf(` + return acctest.ConfigCompose( + testAccAccountConfig_resetOnDelete(rName, true), + fmt.Sprintf(` resource "aws_api_gateway_rest_api" "test" { name = %[1]q } @@ -672,6 +674,10 @@ resource "aws_api_gateway_stage" "test" { destination_arn = aws_cloudwatch_log_group.test.arn format = %[2]q } + + depends_on = [ + aws_api_gateway_account.test + ] } `, rName, format)) } diff --git a/internal/service/apigateway/sweep.go b/internal/service/apigateway/sweep.go index 7501f7eeeda..939b3b9241d 100644 --- a/internal/service/apigateway/sweep.go +++ b/internal/service/apigateway/sweep.go @@ -4,6 +4,7 @@ package apigateway import ( + "context" "fmt" "log" @@ -11,11 +12,17 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/sweep" "github.com/hashicorp/terraform-provider-aws/internal/sweep/awsv2" + "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" ) func RegisterSweepers() { + awsv2.Register("aws_api_gateway_account", sweepAccounts, + "aws_api_gateway_rest_api", + ) + resource.AddTestSweepers("aws_api_gateway_rest_api", &resource.Sweeper{ Name: "aws_api_gateway_rest_api", F: sweepRestAPIs, @@ -50,6 +57,14 @@ func RegisterSweepers() { }) } +func sweepAccounts(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { + return []sweep.Sweepable{ + framework.NewSweepResource(newResourceAccount, client, + framework.NewAttribute("reset_on_delete", true), + ), + }, nil +} + func sweepRestAPIs(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) diff --git a/tools/tfsdk2fw/main.go b/tools/tfsdk2fw/main.go index 37c55c5f895..b9c99be197a 100644 --- a/tools/tfsdk2fw/main.go +++ b/tools/tfsdk2fw/main.go @@ -11,7 +11,6 @@ import ( "io" "os" "path" - "sort" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" diff --git a/website/docs/r/api_gateway_account.html.markdown b/website/docs/r/api_gateway_account.html.markdown index 010b038fa65..44996a8cf79 100644 --- a/website/docs/r/api_gateway_account.html.markdown +++ b/website/docs/r/api_gateway_account.html.markdown @@ -10,7 +10,7 @@ description: |- Provides a settings of an API Gateway Account. Settings is applied region-wide per `provider` block. --> **Note:** As there is no API method for deleting account settings or resetting it to defaults, destroying this resource will keep your account settings intact +-> **Note:** By default, destroying this resource will keep your account settings intact. Set `reset_on_delete` to `true` to reset the account setttings to default. In a future major version of the provider, destroying the resource will reset account settings. ## Example Usage @@ -66,6 +66,9 @@ resource "aws_iam_role_policy" "cloudwatch" { This resource supports the following arguments: * `cloudwatch_role_arn` - (Optional) ARN of an IAM role for CloudWatch (to allow logging & monitoring). See more [in AWS Docs](https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-stage-settings.html#how-to-stage-settings-console). Logging & monitoring can be enabled/disabled and otherwise tuned on the API Gateway Stage level. +* `reset_on_delete` - (Optional) If `true`, destroying the resource will reset account settings to default, otherwise account settings are not modified. + Defaults to `false`. + Will be removed in a future major version of the provider. ## Attribute Reference