Skip to content

Commit

Permalink
Merge pull request #32860 from hashicorp/b-aws_fms_policy-diffs
Browse files Browse the repository at this point in the history
r/aws_fms_policy: Suppress diffs on `null` and `[]` JSON fields
  • Loading branch information
ewbankkit authored Aug 4, 2023
2 parents 1123a63 + 9b8bc04 commit 3d88c83
Show file tree
Hide file tree
Showing 16 changed files with 1,464 additions and 145 deletions.
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

0 comments on commit 3d88c83

Please sign in to comment.