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

r/aws_fms_policy: Suppress diffs on null and [] JSON fields #32860

Merged
merged 21 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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/32860.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_fms_admin_account: Add configurable timeouts
```

```release-note:bug
resource/aws_fms_policy: Prevent erroneous diffs on `security_service_policy_data.managed_service_data`
```
219 changes: 131 additions & 88 deletions internal/service/fms/admin_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/internal/verify"
)

// @SDKResource("aws_fms_admin_account")
func ResourceAdminAccount() *schema.Resource {
func resourceAdminAccount() *schema.Resource {
return &schema.Resource{
CreateWithoutTimeout: resourceAdminAccountCreate,
ReadWithoutTimeout: resourceAdminAccountRead,
Expand All @@ -30,6 +31,11 @@ func ResourceAdminAccount() *schema.Resource {
StateContext: schema.ImportStatePassthroughContext,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(30 * time.Minute),
Delete: schema.DefaultTimeout(10 * time.Minute),
},

Schema: map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Expand All @@ -46,101 +52,41 @@ func resourceAdminAccountCreate(ctx context.Context, d *schema.ResourceData, met
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).FMSConn(ctx)

// Ensure there is not an existing FMS Admin Account
output, err := conn.GetAdminAccountWithContext(ctx, &fms.GetAdminAccountInput{})

if err != nil && !tfawserr.ErrCodeEquals(err, fms.ErrCodeResourceNotFoundException) {
return sdkdiag.AppendErrorf(diags, "getting FMS Admin Account: %s", err)
}
// Ensure there is not an existing FMS Admin Account.
output, err := findAdminAccount(ctx, conn)

if output != nil && output.AdminAccount != nil && aws.StringValue(output.RoleStatus) == fms.AccountRoleStatusReady {
if !tfresource.NotFound(err) {
return sdkdiag.AppendErrorf(diags, "FMS Admin Account (%s) already associated: import this Terraform resource to manage", aws.StringValue(output.AdminAccount))
}

accountID := meta.(*conns.AWSClient).AccountID

if v, ok := d.GetOk("account_id"); ok {
accountID = v.(string)
}

stateConf := &retry.StateChangeConf{
Pending: []string{
fms.AccountRoleStatusDeleted, // Recreating association can return this status
fms.AccountRoleStatusCreating,
},
Target: []string{fms.AccountRoleStatusReady},
Refresh: associateAdminAccountRefreshFunc(ctx, conn, accountID),
Timeout: 30 * time.Minute,
Delay: 10 * time.Second,
}

if _, err := stateConf.WaitForStateContext(ctx); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for FMS Admin Account (%s) association: %s", accountID, err)
if _, err := waitAdminAccountCreated(ctx, conn, accountID, d.Timeout(schema.TimeoutCreate)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for FMS Admin Account (%s) create: %s", d.Id(), err)
}

d.SetId(accountID)

return append(diags, resourceAdminAccountRead(ctx, d, meta)...)
}

func associateAdminAccountRefreshFunc(ctx context.Context, conn *fms.FMS, accountId string) retry.StateRefreshFunc {
// This is all wrapped in a refresh func since AssociateAdminAccount returns
// success even though it failed if called too quickly after creating an organization
return func() (interface{}, string, error) {
req := &fms.AssociateAdminAccountInput{
AdminAccount: aws.String(accountId),
}

_, aserr := conn.AssociateAdminAccountWithContext(ctx, req)
if aserr != nil {
return nil, "", aserr
}

res, err := conn.GetAdminAccountWithContext(ctx, &fms.GetAdminAccountInput{})
if err != nil {
// FMS returns an AccessDeniedException if no account is associated,
// but does not define this in its error codes
if tfawserr.ErrMessageContains(err, "AccessDeniedException", "is not currently delegated by AWS FM") {
return nil, "", nil
}
if tfawserr.ErrCodeEquals(err, fms.ErrCodeResourceNotFoundException) {
return nil, "", nil
}
return nil, "", err
}

if aws.StringValue(res.AdminAccount) != accountId {
return nil, "", nil
}

return res, aws.StringValue(res.RoleStatus), err
}
}

func resourceAdminAccountRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).FMSConn(ctx)

output, err := conn.GetAdminAccountWithContext(ctx, &fms.GetAdminAccountInput{})
output, err := findAdminAccount(ctx, conn)

if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, fms.ErrCodeResourceNotFoundException) {
if !d.IsNewResource() && tfresource.NotFound(err) {
log.Printf("[WARN] FMS Admin Account (%s) not found, removing from state", d.Id())
d.SetId("")
return diags
}

if err != nil {
return sdkdiag.AppendErrorf(diags, "getting FMS Admin Account (%s): %s", d.Id(), err)
}

if aws.StringValue(output.RoleStatus) == fms.AccountRoleStatusDeleted {
if d.IsNewResource() {
return sdkdiag.AppendErrorf(diags, "getting FMS Admin Account (%s): %s after creation", d.Id(), aws.StringValue(output.RoleStatus))
}

log.Printf("[WARN] FMS Admin Account (%s) not found, removing from state", d.Id())
d.SetId("")
return diags
return sdkdiag.AppendErrorf(diags, "reading FMS Admin Account (%s): %s", d.Id(), err)
}

d.Set("account_id", output.AdminAccount)
Expand All @@ -158,39 +104,136 @@ func resourceAdminAccountDelete(ctx context.Context, d *schema.ResourceData, met
return sdkdiag.AppendErrorf(diags, "disassociating FMS Admin Account (%s): %s", d.Id(), err)
}

if err := waitForAdminAccountDeletion(ctx, conn); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for FMS Admin Account (%s) disassociation: %s", d.Id(), err)
if _, err := waitAdminAccountDeleted(ctx, conn, d.Timeout(schema.TimeoutDelete)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for FMS Admin Account (%s) delete: %s", d.Id(), err)
}

return diags
}

func waitForAdminAccountDeletion(ctx context.Context, conn *fms.FMS) error {
func findAdminAccount(ctx context.Context, conn *fms.FMS) (*fms.GetAdminAccountOutput, error) {
input := &fms.GetAdminAccountInput{}

output, err := conn.GetAdminAccountWithContext(ctx, input)

if tfawserr.ErrCodeEquals(err, fms.ErrCodeResourceNotFoundException) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

if output == nil {
return nil, tfresource.NewEmptyResultError(input)
}

if status := aws.StringValue(output.RoleStatus); status == fms.AccountRoleStatusDeleted {
return nil, &retry.NotFoundError{
Message: status,
LastRequest: input,
}
}

return output, nil
}

func statusAssociateAdminAccount(ctx context.Context, conn *fms.FMS, accountID string) retry.StateRefreshFunc {
// This is all wrapped in a StateRefreshFunc since AssociateAdminAccount returns
// success even though it failed if called too quickly after creating an Organization.
return func() (interface{}, string, error) {
input := &fms.AssociateAdminAccountInput{
AdminAccount: aws.String(accountID),
}

_, err := conn.AssociateAdminAccountWithContext(ctx, input)

if err != nil {
return nil, "", err
}

output, err := conn.GetAdminAccountWithContext(ctx, &fms.GetAdminAccountInput{})

// FMS returns an AccessDeniedException if no account is associated,
// but does not define this in its error codes.
if tfawserr.ErrMessageContains(err, "AccessDeniedException", "is not currently delegated by AWS FM") {
return nil, "", nil
}

if tfawserr.ErrCodeEquals(err, fms.ErrCodeResourceNotFoundException) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

if aws.StringValue(output.AdminAccount) != accountID {
return nil, "", nil
}

return output, aws.StringValue(output.RoleStatus), err
}
}

func statusAdminAccount(ctx context.Context, conn *fms.FMS) retry.StateRefreshFunc {
return func() (interface{}, string, error) {
output, err := findAdminAccount(ctx, conn)

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

if err != nil {
return nil, "", err
}

return output, aws.StringValue(output.RoleStatus), nil
}
}

func waitAdminAccountCreated(ctx context.Context, conn *fms.FMS, accountID string, timeout time.Duration) (*fms.GetAdminAccountOutput, error) {
stateConf := &retry.StateChangeConf{
Pending: []string{
fms.AccountRoleStatusDeleting,
fms.AccountRoleStatusPendingDeletion,
fms.AccountRoleStatusReady,
fms.AccountRoleStatusDeleted, // Recreating association can return this status.
fms.AccountRoleStatusCreating,
},
Target: []string{fms.AccountRoleStatusDeleted},
Refresh: func() (interface{}, string, error) {
output, err := conn.GetAdminAccountWithContext(ctx, &fms.GetAdminAccountInput{})
Target: []string{fms.AccountRoleStatusReady},
Refresh: statusAssociateAdminAccount(ctx, conn, accountID),
Timeout: timeout,
Delay: 10 * time.Second,
}

if tfawserr.ErrCodeEquals(err, fms.ErrCodeResourceNotFoundException) {
return output, fms.AccountRoleStatusDeleted, nil
}
outputRaw, err := stateConf.WaitForStateContext(ctx)

if err != nil {
return nil, "", err
}
if output, ok := outputRaw.(*fms.GetAdminAccountOutput); ok {
return output, err
}

return output, aws.StringValue(output.RoleStatus), nil
return nil, err
}

func waitAdminAccountDeleted(ctx context.Context, conn *fms.FMS, timeout time.Duration) (*fms.GetAdminAccountOutput, error) {
stateConf := &retry.StateChangeConf{
Pending: []string{
fms.AccountRoleStatusDeleting,
fms.AccountRoleStatusPendingDeletion,
fms.AccountRoleStatusReady,
},
Timeout: 10 * time.Minute,
Target: []string{},
Refresh: statusAdminAccount(ctx, conn),
Timeout: timeout,
Delay: 10 * time.Second,
}

_, err := stateConf.WaitForStateContext(ctx)
outputRaw, err := stateConf.WaitForStateContext(ctx)

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

return err
return nil, err
}
Loading
Loading