From cef5c96a427c22b18a6cc0419160991de54185de Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 23 Sep 2024 13:59:36 -0500 Subject: [PATCH 1/6] aws_rds_cluster_role_association: wait for cluster to be available --- internal/service/rds/cluster.go | 34 +++++++++++++++++++ .../service/rds/cluster_role_association.go | 20 +++++++++-- internal/service/rds/errors.go | 4 +++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/internal/service/rds/cluster.go b/internal/service/rds/cluster.go index 3fb3b1f7f398..f748b027c6a7 100644 --- a/internal/service/rds/cluster.go +++ b/internal/service/rds/cluster.go @@ -1919,6 +1919,40 @@ func statusDBCluster(ctx context.Context, conn *rds.Client, id string, waitNoPen } } +func waitDBClusterAvailable(ctx context.Context, conn *rds.Client, id string, waitNoPendingModifiedValues bool, timeout time.Duration) (*types.DBCluster, error) { //nolint:unparam + pendingStatuses := []string{ + clusterStatusCreating, + clusterStatusMigrating, + clusterStatusPreparingDataMigration, + clusterStatusRebooting, + clusterStatusBackingUp, + clusterStatusConfiguringIAMDatabaseAuth, + clusterStatusConfiguringEnhancedMonitoring, + clusterStatusModifying, + clusterStatusRenaming, + clusterStatusResettingMasterCredentials, + clusterStatusScalingCompute, + clusterStatusUpgrading, + } + + stateConf := &retry.StateChangeConf{ + Pending: pendingStatuses, + Target: []string{clusterStatusAvailable}, + Refresh: statusDBCluster(ctx, conn, id, waitNoPendingModifiedValues), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*types.DBCluster); ok { + return output, err + } + + return nil, err +} + func waitDBClusterCreated(ctx context.Context, conn *rds.Client, id string, timeout time.Duration) (*types.DBCluster, error) { stateConf := &retry.StateChangeConf{ Pending: []string{ diff --git a/internal/service/rds/cluster_role_association.go b/internal/service/rds/cluster_role_association.go index c9477700f9e8..88ad2af54e24 100644 --- a/internal/service/rds/cluster_role_association.go +++ b/internal/service/rds/cluster_role_association.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" + "github.com/hashicorp/aws-sdk-go-base/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -75,9 +76,22 @@ func resourceClusterRoleAssociationCreate(ctx context.Context, d *schema.Resourc RoleArn: aws.String(roleARN), } - _, err := tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (interface{}, error) { - return conn.AddRoleToDBCluster(ctx, input) - }, errCodeInvalidParameterValue, "IAM role ARN value is invalid or does not include the required permissions") + _, err := conn.AddRoleToDBCluster(ctx, input) + + // check if the cluster is in a valid state to add the role association + if errs.IsA[*types.InvalidDBClusterStateFault](err) { + if _, err := waitDBClusterAvailable(ctx, conn, dbClusterID, true, d.Timeout(schema.TimeoutCreate)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for RDS Cluster (%s) available: %s", dbClusterID, err) + } + + _, err = conn.AddRoleToDBCluster(ctx, input) + } + + if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, errIamRolePropagationMessage) { + _, err = tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (interface{}, error) { + return conn.AddRoleToDBCluster(ctx, input) + }, errCodeInvalidParameterValue, errIamRolePropagationMessage) + } if err != nil { return sdkdiag.AppendErrorf(diags, "creating RDS Cluster IAM Role Association (%s): %s", id, err) diff --git a/internal/service/rds/errors.go b/internal/service/rds/errors.go index 8bee522bc8ba..68c6d6c8c7e3 100644 --- a/internal/service/rds/errors.go +++ b/internal/service/rds/errors.go @@ -9,3 +9,7 @@ const ( errCodeInvalidParameterValue = "InvalidParameterValue" errCodeValidationError = "ValidationError" ) + +const ( + errIamRolePropagationMessage = "IAM role ARN value is invalid or does not include the required permissions" +) From a064c6d21e73bf27b9a9943acb26a06ca7cfc223 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 23 Sep 2024 16:31:43 -0500 Subject: [PATCH 2/6] aws_db_instance_role_association: wait for instance to be available --- .../service/rds/instance_role_association.go | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/internal/service/rds/instance_role_association.go b/internal/service/rds/instance_role_association.go index 4b9baa638180..3ef4d82d846d 100644 --- a/internal/service/rds/instance_role_association.go +++ b/internal/service/rds/instance_role_association.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/rds" "github.com/aws/aws-sdk-go-v2/service/rds/types" + "github.com/hashicorp/aws-sdk-go-base/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -42,6 +43,11 @@ func resourceInstanceRoleAssociation() *schema.Resource { StateContext: schema.ImportStatePassthroughContext, }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + Schema: map[string]*schema.Schema{ "db_instance_identifier": { Type: schema.TypeString, @@ -76,9 +82,22 @@ func resourceInstanceRoleAssociationCreate(ctx context.Context, d *schema.Resour RoleArn: aws.String(roleARN), } - _, err := tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (interface{}, error) { - return conn.AddRoleToDBInstance(ctx, input) - }, errCodeInvalidParameterValue, "IAM role ARN value is invalid or does not include the required permissions") + _, err := conn.AddRoleToDBInstance(ctx, input) + + // check if the instance is in a valid state to add the role association + if errs.IsA[*types.InvalidDBInstanceStateFault](err) { + if _, err := waitDBInstanceAvailable(ctx, conn, dbInstanceIdentifier, d.Timeout(schema.TimeoutCreate)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for RDS DB Instance (%s) available: %s", dbInstanceIdentifier, err) + } + + _, err = conn.AddRoleToDBInstance(ctx, input) + } + + if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, errIamRolePropagationMessage) { + _, err = tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (interface{}, error) { + return conn.AddRoleToDBInstance(ctx, input) + }, errCodeInvalidParameterValue, errIamRolePropagationMessage) + } if err != nil { return sdkdiag.AppendErrorf(diags, "creating RDS DB Instance IAM Role Association (%s): %s", id, err) @@ -86,10 +105,7 @@ func resourceInstanceRoleAssociationCreate(ctx context.Context, d *schema.Resour d.SetId(id) - const ( - timeout = 10 * time.Minute - ) - if _, err := waitDBInstanceRoleAssociationCreated(ctx, conn, dbInstanceIdentifier, roleARN, timeout); err != nil { + if _, err := waitDBInstanceRoleAssociationCreated(ctx, conn, dbInstanceIdentifier, roleARN, d.Timeout(schema.TimeoutCreate)); err != nil { return sdkdiag.AppendErrorf(diags, "waiting for RDS DB Instance IAM Role Association (%s) create: %s", d.Id(), err) } @@ -148,10 +164,7 @@ func resourceInstanceRoleAssociationDelete(ctx context.Context, d *schema.Resour return sdkdiag.AppendErrorf(diags, "deleting RDS DB Instance IAM Role Association (%s): %s", d.Id(), err) } - const ( - timeout = 10 * time.Minute - ) - if _, err := waitDBInstanceRoleAssociationDeleted(ctx, conn, dbInstanceIdentifier, roleARN, timeout); err != nil { + if _, err := waitDBInstanceRoleAssociationDeleted(ctx, conn, dbInstanceIdentifier, roleARN, d.Timeout(schema.TimeoutDelete)); err != nil { return sdkdiag.AppendErrorf(diags, "waiting for RDS DB Instance IAM Role Association (%s) delete: %s", d.Id(), err) } From a39e3303a4c32e66f6239c19dd7a46802780141c Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 23 Sep 2024 16:32:50 -0500 Subject: [PATCH 3/6] aws_db_instance_role_association: update docs with configuarable timeouts --- website/docs/r/db_instance_role_association.html.markdown | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/docs/r/db_instance_role_association.html.markdown b/website/docs/r/db_instance_role_association.html.markdown index bc868a1dffa0..0afdfedd9a6f 100644 --- a/website/docs/r/db_instance_role_association.html.markdown +++ b/website/docs/r/db_instance_role_association.html.markdown @@ -46,6 +46,13 @@ This resource exports the following attributes in addition to the arguments abov * `id` - DB Instance Identifier and IAM Role ARN separated by a comma (`,`) +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +- `create` - (Default `10m`) +- `delete` - (Default `10m`) + ## Import In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import `aws_db_instance_role_association` using the DB Instance Identifier and IAM Role ARN separated by a comma (`,`). For example: From d9a392377e430d5f24b740644d73f2b5995ac8ce Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 23 Sep 2024 16:37:49 -0500 Subject: [PATCH 4/6] add CHANGELOG entry --- .changelog/39457.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changelog/39457.txt diff --git a/.changelog/39457.txt b/.changelog/39457.txt new file mode 100644 index 000000000000..49455d0ebaa5 --- /dev/null +++ b/.changelog/39457.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_rds_cluster_role_association: Fix intermittent failure when cluster is not in an available state +``` + +```release-note:bug +resource/aws_db_instance_role_association: Fix intermittent failure when instance is not in an available state +``` \ No newline at end of file From 1710338e8f366ba750dc013dc2253428c1044350 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 23 Sep 2024 16:39:06 -0500 Subject: [PATCH 5/6] chore: linter --- internal/service/rds/cluster_role_association.go | 4 ++-- internal/service/rds/errors.go | 2 +- internal/service/rds/instance_role_association.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/service/rds/cluster_role_association.go b/internal/service/rds/cluster_role_association.go index 88ad2af54e24..21b69abe5366 100644 --- a/internal/service/rds/cluster_role_association.go +++ b/internal/service/rds/cluster_role_association.go @@ -87,10 +87,10 @@ func resourceClusterRoleAssociationCreate(ctx context.Context, d *schema.Resourc _, err = conn.AddRoleToDBCluster(ctx, input) } - if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, errIamRolePropagationMessage) { + if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, errIAMRolePropagationMessage) { _, err = tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (interface{}, error) { return conn.AddRoleToDBCluster(ctx, input) - }, errCodeInvalidParameterValue, errIamRolePropagationMessage) + }, errCodeInvalidParameterValue, errIAMRolePropagationMessage) } if err != nil { diff --git a/internal/service/rds/errors.go b/internal/service/rds/errors.go index 68c6d6c8c7e3..10eb1ff3db67 100644 --- a/internal/service/rds/errors.go +++ b/internal/service/rds/errors.go @@ -11,5 +11,5 @@ const ( ) const ( - errIamRolePropagationMessage = "IAM role ARN value is invalid or does not include the required permissions" + errIAMRolePropagationMessage = "IAM role ARN value is invalid or does not include the required permissions" ) diff --git a/internal/service/rds/instance_role_association.go b/internal/service/rds/instance_role_association.go index 3ef4d82d846d..3f893ef786da 100644 --- a/internal/service/rds/instance_role_association.go +++ b/internal/service/rds/instance_role_association.go @@ -93,10 +93,10 @@ func resourceInstanceRoleAssociationCreate(ctx context.Context, d *schema.Resour _, err = conn.AddRoleToDBInstance(ctx, input) } - if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, errIamRolePropagationMessage) { + if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, errIAMRolePropagationMessage) { _, err = tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (interface{}, error) { return conn.AddRoleToDBInstance(ctx, input) - }, errCodeInvalidParameterValue, errIamRolePropagationMessage) + }, errCodeInvalidParameterValue, errIAMRolePropagationMessage) } if err != nil { From 857546d299f918a782d2411b150b1b422922d280 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Mon, 23 Sep 2024 16:58:10 -0500 Subject: [PATCH 6/6] chore: golangci-lint --- internal/service/rds/cluster.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/rds/cluster.go b/internal/service/rds/cluster.go index f748b027c6a7..3e0954f904ba 100644 --- a/internal/service/rds/cluster.go +++ b/internal/service/rds/cluster.go @@ -1919,7 +1919,7 @@ func statusDBCluster(ctx context.Context, conn *rds.Client, id string, waitNoPen } } -func waitDBClusterAvailable(ctx context.Context, conn *rds.Client, id string, waitNoPendingModifiedValues bool, timeout time.Duration) (*types.DBCluster, error) { //nolint:unparam +func waitDBClusterAvailable(ctx context.Context, conn *rds.Client, id string, waitNoPendingModifiedValues bool, timeout time.Duration) (*types.DBCluster, error) { pendingStatuses := []string{ clusterStatusCreating, clusterStatusMigrating,