Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource/aws_api_gateway_account: Properly support unregistration #40004

Merged
merged 13 commits into from
Nov 7, 2024
3 changes: 3 additions & 0 deletions .changelog/40004.txt
Original file line number Diff line number Diff line change
@@ -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.
```
2 changes: 2 additions & 0 deletions internal/framework/flex/auto_flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
327 changes: 244 additions & 83 deletions internal/service/apigateway/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
jar-b marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down
Loading
Loading