Skip to content

Commit

Permalink
Merge pull request #38227 from hashicorp/b-aws_db_instance-read-repli…
Browse files Browse the repository at this point in the history
…ca-parameter-group-postgres

r/aws_db_instance: Handle `InvalidParameterCombination: A parameter group can't be specified during Read Replica creation for the following DB engine: postgres`
  • Loading branch information
ewbankkit authored Jul 9, 2024
2 parents e2010bd + a11a132 commit 3a10ac6
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 92 deletions.
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)
d.SetId(resourceID)

Expand Down Expand Up @@ -2200,6 +2203,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 @@ -2467,91 +2569,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

0 comments on commit 3a10ac6

Please sign in to comment.