Skip to content

Tech debt: Use AutoFlEx for auditmanager #42741

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

Merged
merged 13 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/42741.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_auditmanager_framework_share: Add plan-time validation of `destination_account`
```

```release-note:enhancement
resource/aws_auditmanager_organization_admin_account_registration: Add plan-time validation of `admin_account_id`
```
248 changes: 160 additions & 88 deletions internal/service/auditmanager/account_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ package auditmanager

import (
"context"
"fmt"
"time"

"github.com/aws/aws-sdk-go-v2/service/auditmanager"
awstypes "github.com/aws/aws-sdk-go-v2/service/auditmanager/types"
"github.com/hashicorp/terraform-plugin-framework/diag"
"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/types"
"github.com/hashicorp/terraform-provider-aws/internal/create"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-provider-aws/internal/enum"
"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"
fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)

Expand All @@ -24,17 +30,13 @@ func newAccountRegistrationResource(_ context.Context) (resource.ResourceWithCon
return &accountRegistrationResource{}, nil
}

const (
ResNameAccountRegistration = "AccountRegistration"
)

type accountRegistrationResource struct {
framework.ResourceWithModel[accountRegistrationResourceModel]
framework.WithImportRegionalSingleton
}

func (r *accountRegistrationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
func (r *accountRegistrationResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
response.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"delegated_admin_account": schema.StringAttribute{
Optional: true,
Expand All @@ -53,133 +55,203 @@ func (r *accountRegistrationResource) Schema(ctx context.Context, req resource.S
}
}

func (r *accountRegistrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
conn := r.Meta().AuditManagerClient(ctx)
// Registration is applied per region, so use this as the ID
id := r.Meta().Region(ctx)

var plan accountRegistrationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
func (r *accountRegistrationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
var data accountRegistrationResourceModel
response.Diagnostics.Append(request.Plan.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

in := auditmanager.RegisterAccountInput{}
if !plan.DelegatedAdminAccount.IsNull() {
in.DelegatedAdminAccount = plan.DelegatedAdminAccount.ValueStringPointer()
}
if !plan.KmsKey.IsNull() {
in.KmsKey = plan.KmsKey.ValueStringPointer()
}
out, err := conn.RegisterAccount(ctx, &in)
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.AuditManager, create.ErrActionCreating, ResNameAccountRegistration, id, nil),
err.Error(),
)
output, diags := r.registerAccount(ctx, data)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}

state := plan
state.ID = types.StringValue(id)
state.Status = flex.StringValueToFramework(ctx, out.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}
// Set values for unknowns.
data.ID = fwflex.StringValueToFramework(ctx, r.Meta().Region(ctx)) // Registration is applied per region, so use this as the ID.
data.Status = fwflex.StringValueToFramework(ctx, output.Status)

func (r *accountRegistrationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
conn := r.Meta().AuditManagerClient(ctx)
response.Diagnostics.Append(response.State.Set(ctx, data)...)
}

var state accountRegistrationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
func (r *accountRegistrationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
var data accountRegistrationResourceModel
response.Diagnostics.Append(request.State.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

conn := r.Meta().AuditManagerClient(ctx)

// There is no API to get account registration attributes like delegated admin account
// and KMS key. Read will instead call the GetAccountStatus API to confirm an active
// account status.
out, err := conn.GetAccountStatus(ctx, &auditmanager.GetAccountStatusInput{})
if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.AuditManager, create.ErrActionReading, ResNameAccountRegistration, state.ID.String(), nil),
err.Error(),
)
output, err := findAccountRegistration(ctx, conn)

if tfresource.NotFound(err) {
response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err))
response.State.RemoveResource(ctx)

return
}
if out.Status == awstypes.AccountStatusInactive {
resp.State.RemoveResource(ctx)

if err != nil {
response.Diagnostics.AddError(fmt.Sprintf("reading Audit Manager Account Registration (%s)", data.ID.ValueString()), err.Error())

return
}

state.Status = flex.StringValueToFramework(ctx, out.Status)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
data.Status = fwflex.StringValueToFramework(ctx, output.Status)

func (r *accountRegistrationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
conn := r.Meta().AuditManagerClient(ctx)
response.Diagnostics.Append(response.State.Set(ctx, &data)...)
}

var plan, state accountRegistrationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
func (r *accountRegistrationResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
var new, old accountRegistrationResourceModel
response.Diagnostics.Append(request.Plan.Get(ctx, &new)...)
if response.Diagnostics.HasError() {
return
}
response.Diagnostics.Append(request.State.Get(ctx, &old)...)
if response.Diagnostics.HasError() {
return
}

if !plan.DelegatedAdminAccount.Equal(state.DelegatedAdminAccount) ||
!plan.KmsKey.Equal(state.KmsKey) {
in := auditmanager.RegisterAccountInput{}
if !plan.DelegatedAdminAccount.IsNull() {
in.DelegatedAdminAccount = plan.DelegatedAdminAccount.ValueStringPointer()
}
if !plan.KmsKey.IsNull() {
in.KmsKey = plan.KmsKey.ValueStringPointer()
if !new.DelegatedAdminAccount.Equal(old.DelegatedAdminAccount) ||
!new.KMSKey.Equal(old.KMSKey) {
output, diags := r.registerAccount(ctx, new)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
out, err := conn.RegisterAccount(ctx, &in)

// Set values for unknowns.
new.Status = fwflex.StringValueToFramework(ctx, output.Status)
} else {
new.Status = old.Status
}

new.DeregisterOnDestroy = old.DeregisterOnDestroy

response.Diagnostics.Append(response.State.Set(ctx, &new)...)
}

func (r *accountRegistrationResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
var data accountRegistrationResourceModel
response.Diagnostics.Append(request.State.Get(ctx, &data)...)
if response.Diagnostics.HasError() {
return
}

conn := r.Meta().AuditManagerClient(ctx)

if data.DeregisterOnDestroy.ValueBool() {
input := auditmanager.DeregisterAccountInput{}
_, err := conn.DeregisterAccount(ctx, &input)

if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.AuditManager, create.ErrActionUpdating, ResNameAccountRegistration, state.ID.String(), nil),
err.Error(),
)
response.Diagnostics.AddError(fmt.Sprintf("deregistering Audit Manager Account (%s)", data.ID.ValueString()), err.Error())

return
}
}
}

func (r *accountRegistrationResource) registerAccount(ctx context.Context, data accountRegistrationResourceModel) (*auditmanager.GetAccountStatusOutput, diag.Diagnostics) {
var diags diag.Diagnostics

state.DelegatedAdminAccount = plan.DelegatedAdminAccount
state.KmsKey = plan.KmsKey
state.Status = flex.StringValueToFramework(ctx, out.Status)
var input auditmanager.RegisterAccountInput
diags.Append(fwflex.Expand(ctx, data, &input)...)
if diags.HasError() {
return nil, diags
}

if !plan.DeregisterOnDestroy.Equal(state.DeregisterOnDestroy) {
state.DeregisterOnDestroy = plan.DeregisterOnDestroy
conn := r.Meta().AuditManagerClient(ctx)
id := r.Meta().Region(ctx)

_, err := conn.RegisterAccount(ctx, &input)

if err != nil {
diags.AddError(fmt.Sprintf("registering Audit Manager Account (%s)", id), err.Error())

return nil, diags
}

resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
output, err := waitAccountRegistered(ctx, conn)

if err != nil {
diags.AddError(fmt.Sprintf("waiting for Audit Manager Account (%s) registered", id), err.Error())

return nil, diags
}

return output, diags
}

func (r *accountRegistrationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
conn := r.Meta().AuditManagerClient(ctx)
func findAccountRegistration(ctx context.Context, conn *auditmanager.Client) (*auditmanager.GetAccountStatusOutput, error) {
input := auditmanager.GetAccountStatusInput{}
output, err := conn.GetAccountStatus(ctx, &input)

var state accountRegistrationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
if err != nil {
return nil, err
}

if state.DeregisterOnDestroy.ValueBool() {
_, err := conn.DeregisterAccount(ctx, &auditmanager.DeregisterAccountInput{})
if output == nil {
return nil, tfresource.NewEmptyResultError(input)
}

if status := output.Status; status == awstypes.AccountStatusInactive {
return nil, &retry.NotFoundError{
Message: string(status),
LastRequest: &input,
}
}

return output, nil
}

func statusAccountRegistration(ctx context.Context, conn *auditmanager.Client) retry.StateRefreshFunc {
return func() (any, string, error) {
output, err := findAccountRegistration(ctx, conn)

if tfresource.NotFound(err) {
return nil, "", nil
}

if err != nil {
resp.Diagnostics.AddError(
create.ProblemStandardMessage(names.AuditManager, create.ErrActionDeleting, ResNameAccountRegistration, state.ID.String(), nil),
err.Error(),
)
return nil, "", err
}

return output, string(output.Status), nil
}
}

func waitAccountRegistered(ctx context.Context, conn *auditmanager.Client) (*auditmanager.GetAccountStatusOutput, error) {
const (
timeout = 5 * time.Minute
)
stateConf := &retry.StateChangeConf{
Pending: enum.Slice(awstypes.AccountStatusPendingActivation),
Target: enum.Slice(awstypes.AccountStatusActive),
Refresh: statusAccountRegistration(ctx, conn),
Timeout: timeout,
}

outputRaw, err := stateConf.WaitForStateContext(ctx)

if output, ok := outputRaw.(*auditmanager.GetAccountStatusOutput); ok {
return output, err
}

return nil, err
}

type accountRegistrationResourceModel struct {
framework.WithRegionModel
DelegatedAdminAccount types.String `tfsdk:"delegated_admin_account"`
DeregisterOnDestroy types.Bool `tfsdk:"deregister_on_destroy"`
KmsKey types.String `tfsdk:"kms_key"`
KMSKey types.String `tfsdk:"kms_key"`
ID types.String `tfsdk:"id"`
Status types.String `tfsdk:"status"`
}
Loading
Loading