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_db_instance: Handle InvalidParameterCombination: A parameter group can't be specified during Read Replica creation for the following DB engine: postgres #38227

Merged
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
3 changes: 3 additions & 0 deletions .changelog/38227.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_db_instance: Fix `InvalidParameterCombination: A parameter group can't be specified during Read Replica creation for the following DB engine: postgres` errors
```
201 changes: 109 additions & 92 deletions internal/service/rds/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,17 +866,20 @@ func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, meta in
input.VpcSecurityGroupIds = flex.ExpandStringSet(v.(*schema.Set))
}

outputRaw, err := tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout,
func() (interface{}, error) {
return conn.CreateDBInstanceReadReplicaWithContext(ctx, input)
},
errCodeInvalidParameterValue, "ENHANCED_MONITORING")
output, err := dbInstanceCreateReadReplica(ctx, conn, input)

// Some engines (e.g. PostgreSQL) you cannot specify a custom parameter group for the read replica during creation.
// See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReadRepl.html#USER_ReadRepl.XRgn.Cnsdr.
if input.DBParameterGroupName != nil && tfawserr.ErrMessageContains(err, "InvalidParameterCombination", "A parameter group can't be specified during Read Replica creation for the following DB engine") {
input.DBParameterGroupName = nil

output, err = dbInstanceCreateReadReplica(ctx, conn, input)
}

if err != nil {
return sdkdiag.AppendErrorf(diags, "creating RDS DB Instance (read replica) (%s): %s", identifier, err)
}

output := outputRaw.(*rds.CreateDBInstanceReadReplicaOutput)

resourceID = aws.StringValue(output.DBInstance.DbiResourceId)

if v, ok := d.GetOk(names.AttrAllowMajorVersionUpgrade); ok {
Expand Down Expand Up @@ -2189,6 +2192,105 @@ func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta in
return append(diags, resourceInstanceRead(ctx, d, meta)...)
}

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

input := &rds.DeleteDBInstanceInput{
DBInstanceIdentifier: aws.String(d.Get(names.AttrIdentifier).(string)),
DeleteAutomatedBackups: aws.Bool(d.Get("delete_automated_backups").(bool)),
}

if d.Get("skip_final_snapshot").(bool) {
input.SkipFinalSnapshot = aws.Bool(true)
} else {
input.SkipFinalSnapshot = aws.Bool(false)

if v, ok := d.GetOk(names.AttrFinalSnapshotIdentifier); ok {
input.FinalDBSnapshotIdentifier = aws.String(v.(string))
} else {
return sdkdiag.AppendErrorf(diags, "final_snapshot_identifier is required when skip_final_snapshot is false")
}
}

log.Printf("[DEBUG] Deleting RDS DB Instance: %s", d.Get(names.AttrIdentifier).(string))
_, err := conn.DeleteDBInstanceWithContext(ctx, input)

if tfawserr.ErrMessageContains(err, errCodeInvalidParameterCombination, "disable deletion pro") {
if v, ok := d.GetOk(names.AttrDeletionProtection); (!ok || !v.(bool)) && d.Get(names.AttrApplyImmediately).(bool) {
_, ierr := tfresource.RetryWhen(ctx, d.Timeout(schema.TimeoutUpdate),
func() (interface{}, error) {
return conn.ModifyDBInstanceWithContext(ctx, &rds.ModifyDBInstanceInput{
ApplyImmediately: aws.Bool(true),
DBInstanceIdentifier: aws.String(d.Get(names.AttrIdentifier).(string)),
DeletionProtection: aws.Bool(false),
})
},
func(err error) (bool, error) {
// Retry for IAM eventual consistency.
if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "IAM role ARN value is invalid or") {
return true, err
}

// "InvalidDBInstanceState: RDS is configuring Enhanced Monitoring or Performance Insights for this DB instance. Try your request later."
if tfawserr.ErrMessageContains(err, rds.ErrCodeInvalidDBInstanceStateFault, "your request later") {
return true, err
}

return false, err
},
)

if ierr != nil {
return sdkdiag.AppendErrorf(diags, "updating RDS DB Instance (%s): %s", d.Get(names.AttrIdentifier).(string), err)
}

if _, ierr := waitDBInstanceAvailableSDKv1(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); ierr != nil {
return sdkdiag.AppendErrorf(diags, "waiting for RDS DB Instance (%s) update: %s", d.Get(names.AttrIdentifier).(string), ierr)
}

_, err = conn.DeleteDBInstanceWithContext(ctx, input)
}
}

if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBInstanceNotFoundFault) {
return diags
}

if err != nil && !tfawserr.ErrMessageContains(err, rds.ErrCodeInvalidDBInstanceStateFault, "is already being deleted") {
return sdkdiag.AppendErrorf(diags, "deleting RDS DB Instance (%s): %s", d.Get(names.AttrIdentifier).(string), err)
}

if _, err := waitDBInstanceDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for RDS DB Instance (%s) delete: %s", d.Get(names.AttrIdentifier).(string), err)
}

return diags
}

func resourceInstanceImport(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// Neither skip_final_snapshot nor final_snapshot_identifier can be fetched
// from any API call, so we need to default skip_final_snapshot to true so
// that final_snapshot_identifier is not required.
d.Set("skip_final_snapshot", true)
d.Set("delete_automated_backups", true)
return []*schema.ResourceData{d}, nil
}

func dbInstanceCreateReadReplica(ctx context.Context, conn *rds.RDS, input *rds.CreateDBInstanceReadReplicaInput) (*rds.CreateDBInstanceReadReplicaOutput, error) {
outputRaw, err := tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout,
func() (interface{}, error) {
return conn.CreateDBInstanceReadReplicaWithContext(ctx, input)
},
errCodeInvalidParameterValue, "ENHANCED_MONITORING")

if err != nil {
return nil, err
}

return outputRaw.(*rds.CreateDBInstanceReadReplicaOutput), nil
}

func dbInstancePopulateModify(input *rds_sdkv2.ModifyDBInstanceInput, d *schema.ResourceData) bool {
needsModify := false

Expand Down Expand Up @@ -2456,91 +2558,6 @@ func dbInstanceModify(ctx context.Context, conn *rds_sdkv2.Client, resourceID st
return nil
}

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

input := &rds.DeleteDBInstanceInput{
DBInstanceIdentifier: aws.String(d.Get(names.AttrIdentifier).(string)),
DeleteAutomatedBackups: aws.Bool(d.Get("delete_automated_backups").(bool)),
}

if d.Get("skip_final_snapshot").(bool) {
input.SkipFinalSnapshot = aws.Bool(true)
} else {
input.SkipFinalSnapshot = aws.Bool(false)

if v, ok := d.GetOk(names.AttrFinalSnapshotIdentifier); ok {
input.FinalDBSnapshotIdentifier = aws.String(v.(string))
} else {
return sdkdiag.AppendErrorf(diags, "final_snapshot_identifier is required when skip_final_snapshot is false")
}
}

log.Printf("[DEBUG] Deleting RDS DB Instance: %s", d.Get(names.AttrIdentifier).(string))
_, err := conn.DeleteDBInstanceWithContext(ctx, input)

if tfawserr.ErrMessageContains(err, errCodeInvalidParameterCombination, "disable deletion pro") {
if v, ok := d.GetOk(names.AttrDeletionProtection); (!ok || !v.(bool)) && d.Get(names.AttrApplyImmediately).(bool) {
_, ierr := tfresource.RetryWhen(ctx, d.Timeout(schema.TimeoutUpdate),
func() (interface{}, error) {
return conn.ModifyDBInstanceWithContext(ctx, &rds.ModifyDBInstanceInput{
ApplyImmediately: aws.Bool(true),
DBInstanceIdentifier: aws.String(d.Get(names.AttrIdentifier).(string)),
DeletionProtection: aws.Bool(false),
})
},
func(err error) (bool, error) {
// Retry for IAM eventual consistency.
if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "IAM role ARN value is invalid or") {
return true, err
}

// "InvalidDBInstanceState: RDS is configuring Enhanced Monitoring or Performance Insights for this DB instance. Try your request later."
if tfawserr.ErrMessageContains(err, rds.ErrCodeInvalidDBInstanceStateFault, "your request later") {
return true, err
}

return false, err
},
)

if ierr != nil {
return sdkdiag.AppendErrorf(diags, "updating RDS DB Instance (%s): %s", d.Get(names.AttrIdentifier).(string), err)
}

if _, ierr := waitDBInstanceAvailableSDKv1(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); ierr != nil {
return sdkdiag.AppendErrorf(diags, "waiting for RDS DB Instance (%s) update: %s", d.Get(names.AttrIdentifier).(string), ierr)
}

_, err = conn.DeleteDBInstanceWithContext(ctx, input)
}
}

if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBInstanceNotFoundFault) {
return diags
}

if err != nil && !tfawserr.ErrMessageContains(err, rds.ErrCodeInvalidDBInstanceStateFault, "is already being deleted") {
return sdkdiag.AppendErrorf(diags, "deleting RDS DB Instance (%s): %s", d.Get(names.AttrIdentifier).(string), err)
}

if _, err := waitDBInstanceDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for RDS DB Instance (%s) delete: %s", d.Get(names.AttrIdentifier).(string), err)
}

return diags
}

func resourceInstanceImport(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// Neither skip_final_snapshot nor final_snapshot_identifier can be fetched
// from any API call, so we need to default skip_final_snapshot to true so
// that final_snapshot_identifier is not required.
d.Set("skip_final_snapshot", true)
d.Set("delete_automated_backups", true)
return []*schema.ResourceData{d}, nil
}

// See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html#gp3-storage.
func isStorageTypeGP3BelowAllocatedStorageThreshold(d *schema.ResourceData) bool {
if storageType := d.Get(names.AttrStorageType).(string); storageType != storageTypeGP3 {
Expand Down
104 changes: 104 additions & 0 deletions internal/service/rds/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2213,6 +2213,50 @@ func TestAccRDSInstance_ReplicateSourceDB_CrossRegion_parameterGroupNameEquivale
})
}

func TestAccRDSInstance_ReplicateSourceDB_CrossRegion_parameterGroupNamePostgres(t *testing.T) {
ctx := acctest.Context(t)
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}

var dbInstance, sourceDbInstance rds.DBInstance
var providers []*schema.Provider

rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
sourceResourceName := "aws_db_instance.source"
resourceName := "aws_db_instance.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.RDSServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5FactoriesPlusProvidersAlternate(ctx, t, &providers),
CheckDestroy: testAccCheckDBInstanceDestroy(ctx),
Steps: []resource.TestStep{
{
Config: testAccInstanceConfig_ReplicateSourceDB_CrossRegion_ParameterGroupName_postgres(rName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckDBInstanceExistsWithProvider(ctx, sourceResourceName, &sourceDbInstance, acctest.RegionProviderFunc(acctest.AlternateRegion(), &providers)),
resource.TestCheckResourceAttr(sourceResourceName, names.AttrParameterGroupName, fmt.Sprintf("%s-source", rName)),
testAccCheckDBInstanceExistsWithProvider(ctx, resourceName, &dbInstance, acctest.RegionProviderFunc(acctest.Region(), &providers)),
resource.TestCheckResourceAttrPair(resourceName, "replicate_source_db", sourceResourceName, names.AttrARN),
resource.TestCheckResourceAttrPair(resourceName, names.AttrParameterGroupName, "aws_db_parameter_group.test", names.AttrName),
testAccCheckInstanceParameterApplyStatusInSync(&dbInstance),
testAccCheckInstanceParameterApplyStatusInSync(&sourceDbInstance),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
names.AttrApplyImmediately,
names.AttrPassword,
},
},
},
})
}

func TestAccRDSInstance_ReplicateSourceDB_CrossRegion_characterSet(t *testing.T) {
t.Skip("Skipping due to upstream error")
ctx := acctest.Context(t)
Expand Down Expand Up @@ -9961,6 +10005,66 @@ data "aws_rds_orderable_db_instance" "test" {
`, rName, tfrds.InstanceEngineOracleEnterprise, strings.Replace(mainInstanceClasses, "db.t3.small", "frodo", 1), parameters))
}

func testAccInstanceConfig_ReplicateSourceDB_CrossRegion_ParameterGroupName_postgres(rName string) string {
parameters := `
parameter {
name = "client_encoding"
value = "UTF8"
apply_method = "pending-reboot"
}
`
return acctest.ConfigCompose(
acctest.ConfigMultipleRegionProvider(2),
testAccInstanceConfig_orderableClassPostgres(), fmt.Sprintf(`
resource "aws_db_instance" "test" {
provider = "aws"

identifier = %[1]q
replicate_source_db = aws_db_instance.source.arn
instance_class = data.aws_rds_orderable_db_instance.test.instance_class
skip_final_snapshot = true
apply_immediately = true
parameter_group_name = aws_db_parameter_group.test.name
}

resource "aws_db_parameter_group" "test" {
provider = "aws"

family = data.aws_rds_engine_version.default.parameter_group_family
name = %[1]q

%[2]s
}

resource "aws_db_instance" "source" {
provider = "awsalternate"

identifier = "%[1]s-source"
allocated_storage = 20
engine = data.aws_rds_orderable_db_instance.test.engine
engine_version = data.aws_rds_orderable_db_instance.test.engine_version
instance_class = data.aws_rds_orderable_db_instance.test.instance_class
storage_type = data.aws_rds_orderable_db_instance.test.storage_type
db_name = "MAINDB"
username = "oadmin"
password = "avoid-plaintext-passwords"
skip_final_snapshot = true
apply_immediately = true
backup_retention_period = 3
parameter_group_name = aws_db_parameter_group.source.name
}

resource "aws_db_parameter_group" "source" {
provider = "awsalternate"

family = data.aws_rds_engine_version.default.parameter_group_family
name = "%[1]s-source"

%[2]s
}
`, rName, parameters))
}

func testAccInstanceConfig_ReplicateSourceDB_CrossRegion_CharacterSet(rName string) string {
return acctest.ConfigCompose(
acctest.ConfigMultipleRegionProvider(2),
Expand Down
Loading