diff --git a/.changelog/31600.txt b/.changelog/31600.txt new file mode 100644 index 00000000000..8de19ef7780 --- /dev/null +++ b/.changelog/31600.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +data-source/aws_db_snapshot: Add `tags` argument +``` + +```release-note:bug +resource/aws_db_instance_automated_backups_replication: Fix `unexpected state 'Pending'` errors on resource Create +``` \ No newline at end of file diff --git a/internal/errs/sdkdiag/diags.go b/internal/errs/sdkdiag/diags.go index 483266e7448..143bc173738 100644 --- a/internal/errs/sdkdiag/diags.go +++ b/internal/errs/sdkdiag/diags.go @@ -24,7 +24,7 @@ func Warnings(diags diag.Diagnostics) diag.Diagnostics { return tfslices.Filter(diags, severityFilter(diag.Warning)) } -func severityFilter(s diag.Severity) tfslices.FilterFunc[diag.Diagnostic] { +func severityFilter(s diag.Severity) tfslices.Predicate[diag.Diagnostic] { return func(d diag.Diagnostic) bool { return d.Severity == s } diff --git a/internal/service/cloudformation/stack.go b/internal/service/cloudformation/stack.go index bb2a3d289fe..ebd7f3d862d 100644 --- a/internal/service/cloudformation/stack.go +++ b/internal/service/cloudformation/stack.go @@ -598,7 +598,7 @@ func WaitStackDeleted(ctx context.Context, conn *cloudformation.CloudFormation, return output, err } -func findStackEventsForOperation(ctx context.Context, conn *cloudformation.CloudFormation, name, requestToken string, f slices.FilterFunc[*cloudformation.StackEvent]) ([]*cloudformation.StackEvent, error) { +func findStackEventsForOperation(ctx context.Context, conn *cloudformation.CloudFormation, name, requestToken string, filter slices.Predicate[*cloudformation.StackEvent]) ([]*cloudformation.StackEvent, error) { input := &cloudformation.DescribeStackEventsInput{ StackName: aws.String(name), } @@ -611,6 +611,10 @@ func findStackEventsForOperation(ctx context.Context, conn *cloudformation.Cloud } for _, v := range page.StackEvents { + if v == nil { + continue + } + if currentToken := aws.StringValue(v.ClientRequestToken); !tokenSeen { if currentToken != requestToken { continue @@ -622,7 +626,7 @@ func findStackEventsForOperation(ctx context.Context, conn *cloudformation.Cloud } } - if f(v) { + if filter(v) { output = append(output, v) } } diff --git a/internal/service/inspector2/enabler.go b/internal/service/inspector2/enabler.go index b93ccec55d7..257f096bfdf 100644 --- a/internal/service/inspector2/enabler.go +++ b/internal/service/inspector2/enabler.go @@ -471,7 +471,7 @@ func statusEnablerAccountAndResourceTypes(ctx context.Context, conn *inspector2. }) { return true } - if v.Status == types.StatusEnabled && tfslices.All(maps.Values(v.ResourceStatuses), tfslices.FilterEquals(types.StatusDisabled)) { + if v.Status == types.StatusEnabled && tfslices.All(maps.Values(v.ResourceStatuses), tfslices.PredicateEquals(types.StatusDisabled)) { return true } return false diff --git a/internal/service/rds/consts.go b/internal/service/rds/consts.go index d262f5df063..98c3e0b0ae2 100644 --- a/internal/service/rds/consts.go +++ b/internal/service/rds/consts.go @@ -100,12 +100,6 @@ const ( InstanceStatusUpgrading = "upgrading" ) -const ( - InstanceAutomatedBackupStatusPending = "pending" - InstanceAutomatedBackupStatusReplicating = "replicating" - InstanceAutomatedBackupStatusRetained = "retained" -) - const ( EventSubscriptionStatusActive = "active" EventSubscriptionStatusCreating = "creating" diff --git a/internal/service/rds/find.go b/internal/service/rds/find.go index 69639fe0762..9e6871f8ec7 100644 --- a/internal/service/rds/find.go +++ b/internal/service/rds/find.go @@ -195,82 +195,6 @@ func FindEventSubscriptionByID(ctx context.Context, conn *rds.RDS, id string) (* return output.EventSubscriptionsList[0], nil } -func FindDBInstanceAutomatedBackupByARN(ctx context.Context, conn *rds.RDS, arn string) (*rds.DBInstanceAutomatedBackup, error) { - input := &rds.DescribeDBInstanceAutomatedBackupsInput{ - DBInstanceAutomatedBackupsArn: aws.String(arn), - } - - output, err := findDBInstanceAutomatedBackup(ctx, conn, input) - if err != nil { - return nil, err - } - - if status := aws.StringValue(output.Status); status == InstanceAutomatedBackupStatusRetained { - // If the automated backup is retained, the replication is stopped. - return nil, &retry.NotFoundError{ - Message: status, - LastRequest: input, - } - } - - // Eventual consistency check. - if aws.StringValue(output.DBInstanceAutomatedBackupsArn) != arn { - return nil, &retry.NotFoundError{ - LastRequest: input, - } - } - - return output, nil -} - -func findDBInstanceAutomatedBackup(ctx context.Context, conn *rds.RDS, input *rds.DescribeDBInstanceAutomatedBackupsInput) (*rds.DBInstanceAutomatedBackup, error) { - output, err := findDBInstanceAutomatedBackups(ctx, conn, input) - if err != nil { - return nil, err - } - - if len(output) == 0 || output[0] == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - if count := len(output); count > 1 { - return nil, tfresource.NewTooManyResultsError(count, input) - } - - return output[0], nil -} - -func findDBInstanceAutomatedBackups(ctx context.Context, conn *rds.RDS, input *rds.DescribeDBInstanceAutomatedBackupsInput) ([]*rds.DBInstanceAutomatedBackup, error) { - var output []*rds.DBInstanceAutomatedBackup - - err := conn.DescribeDBInstanceAutomatedBackupsPagesWithContext(ctx, input, func(page *rds.DescribeDBInstanceAutomatedBackupsOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, v := range page.DBInstanceAutomatedBackups { - if v != nil { - output = append(output, v) - } - } - - return !lastPage - }) - - if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBInstanceAutomatedBackupNotFoundFault) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - return output, nil -} - func FindGlobalClusterByDBClusterARN(ctx context.Context, conn *rds.RDS, dbClusterARN string) (*rds.GlobalCluster, error) { input := &rds.DescribeGlobalClustersInput{} globalClusters, err := findGlobalClusters(ctx, conn, input) diff --git a/internal/service/rds/instance_automated_backups_replication.go b/internal/service/rds/instance_automated_backups_replication.go index 11a08290709..d9403f1cd82 100644 --- a/internal/service/rds/instance_automated_backups_replication.go +++ b/internal/service/rds/instance_automated_backups_replication.go @@ -6,6 +6,7 @@ package rds import ( "context" "log" + "strconv" "time" "github.com/aws/aws-sdk-go/aws" @@ -13,16 +14,19 @@ import ( "github.com/aws/aws-sdk-go/service/rds" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/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" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) const ( - InstanceAutomatedBackupsReplicationCreateTimeout = 75 * time.Minute - InstanceAutomatedBackupsReplicationDeleteTimeout = 75 * time.Minute + InstanceAutomatedBackupStatusPending = "Pending" + InstanceAutomatedBackupStatusReplicating = "Replicating" + InstanceAutomatedBackupStatusRetained = "Retained" ) // @SDKResource("aws_db_instance_automated_backups_replication") @@ -33,8 +37,8 @@ func ResourceInstanceAutomatedBackupsReplication() *schema.Resource { DeleteWithoutTimeout: resourceInstanceAutomatedBackupsReplicationDelete, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(InstanceAutomatedBackupsReplicationCreateTimeout), - Delete: schema.DefaultTimeout(InstanceAutomatedBackupsReplicationDeleteTimeout), + Create: schema.DefaultTimeout(75 * time.Minute), + Delete: schema.DefaultTimeout(75 * time.Minute), }, Importer: &schema.ResourceImporter{ @@ -87,7 +91,6 @@ func resourceInstanceAutomatedBackupsReplicationCreate(ctx context.Context, d *s input.PreSignedUrl = aws.String(v.(string)) } - log.Printf("[DEBUG] Starting RDS instance automated backups replication: %s", input) output, err := conn.StartDBInstanceAutomatedBackupsReplicationWithContext(ctx, input) if err != nil { return sdkdiag.AppendErrorf(diags, "starting RDS instance automated backups replication: %s", err) @@ -137,13 +140,13 @@ func resourceInstanceAutomatedBackupsReplicationDelete(ctx context.Context, d *s } if err != nil { - return sdkdiag.AppendErrorf(diags, "deleting RDS DB Instance Automated Backup (%s): %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "reading RDS instance automated backup (%s): %s", d.Id(), err) } dbInstanceID := aws.StringValue(backup.DBInstanceIdentifier) sourceDatabaseARN, err := arn.Parse(aws.StringValue(backup.DBInstanceArn)) if err != nil { - return sdkdiag.AppendErrorf(diags, "deleting RDS DB Instance Automated Backup (%s): %s", d.Id(), err) + return sdkdiag.AppendFromErr(diags, err) } log.Printf("[DEBUG] Stopping RDS Instance Automated Backups Replication: %s", d.Id()) @@ -155,6 +158,10 @@ func resourceInstanceAutomatedBackupsReplicationDelete(ctx context.Context, d *s return diags } + if tfawserr.ErrMessageContains(err, rds.ErrCodeInvalidDBInstanceStateFault, "not replicating to the current region") { + return diags + } + if err != nil { return sdkdiag.AppendErrorf(diags, "deleting RDS DB Instance Automated Backup (%s): %s", d.Id(), err) } @@ -166,8 +173,153 @@ func resourceInstanceAutomatedBackupsReplicationDelete(ctx context.Context, d *s } if _, err := waitDBInstanceAutomatedBackupDeleted(ctx, sourceDatabaseConn, dbInstanceID, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { - return sdkdiag.AppendErrorf(diags, "deleting RDS DB Instance Automated Backup (%s): waiting for completion: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "waiting for DB instance automated backup (%s) delete: %s", d.Id(), err) } return diags } + +func FindDBInstanceAutomatedBackupByARN(ctx context.Context, conn *rds.RDS, arn string) (*rds.DBInstanceAutomatedBackup, error) { + input := &rds.DescribeDBInstanceAutomatedBackupsInput{ + DBInstanceAutomatedBackupsArn: aws.String(arn), + } + output, err := findDBInstanceAutomatedBackup(ctx, conn, input) + + if err != nil { + return nil, err + } + + if status := aws.StringValue(output.Status); status == InstanceAutomatedBackupStatusRetained { + // If the automated backup is retained, the replication is stopped. + return nil, &retry.NotFoundError{ + Message: status, + LastRequest: input, + } + } + + // Eventual consistency check. + if aws.StringValue(output.DBInstanceAutomatedBackupsArn) != arn { + return nil, &retry.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + +func findDBInstanceAutomatedBackup(ctx context.Context, conn *rds.RDS, input *rds.DescribeDBInstanceAutomatedBackupsInput) (*rds.DBInstanceAutomatedBackup, error) { + output, err := findDBInstanceAutomatedBackups(ctx, conn, input, tfslices.PredicateTrue[*rds.DBInstanceAutomatedBackup]()) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findDBInstanceAutomatedBackups(ctx context.Context, conn *rds.RDS, input *rds.DescribeDBInstanceAutomatedBackupsInput, filter tfslices.Predicate[*rds.DBInstanceAutomatedBackup]) ([]*rds.DBInstanceAutomatedBackup, error) { + var output []*rds.DBInstanceAutomatedBackup + + err := conn.DescribeDBInstanceAutomatedBackupsPagesWithContext(ctx, input, func(page *rds.DescribeDBInstanceAutomatedBackupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DBInstanceAutomatedBackups { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBInstanceAutomatedBackupNotFoundFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func statusDBInstanceAutomatedBackup(ctx context.Context, conn *rds.RDS, arn string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindDBInstanceAutomatedBackupByARN(ctx, conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func waitDBInstanceAutomatedBackupCreated(ctx context.Context, conn *rds.RDS, arn string, timeout time.Duration) (*rds.DBInstanceAutomatedBackup, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{InstanceAutomatedBackupStatusPending}, + Target: []string{InstanceAutomatedBackupStatusReplicating}, + Refresh: statusDBInstanceAutomatedBackup(ctx, conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*rds.DBInstanceAutomatedBackup); ok { + return output, err + } + + return nil, err +} + +// statusDBInstanceHasAutomatedBackup returns whether or not a database instance has a specified automated backup. +// The connection must be valid for the database instance's Region. +func statusDBInstanceHasAutomatedBackup(ctx context.Context, conn *rds.RDS, dbInstanceID, dbInstanceAutomatedBackupsARN string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findDBInstanceByIDSDKv1(ctx, conn, dbInstanceID) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + for _, v := range output.DBInstanceAutomatedBackupsReplications { + if aws.StringValue(v.DBInstanceAutomatedBackupsArn) == dbInstanceAutomatedBackupsARN { + return output, strconv.FormatBool(true), nil + } + } + + return output, strconv.FormatBool(false), nil + } +} + +// waitDBInstanceAutomatedBackupDeleted waits for a specified automated backup to be deleted from a database instance. +// The connection must be valid for the database instance's Region. +func waitDBInstanceAutomatedBackupDeleted(ctx context.Context, conn *rds.RDS, dbInstanceID, dbInstanceAutomatedBackupsARN string, timeout time.Duration) (*rds.DBInstance, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{strconv.FormatBool(true)}, + Target: []string{strconv.FormatBool(false)}, + Refresh: statusDBInstanceHasAutomatedBackup(ctx, conn, dbInstanceID, dbInstanceAutomatedBackupsARN), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*rds.DBInstance); ok { + return output, err + } + + return nil, err +} diff --git a/internal/service/rds/instance_automated_backups_replication_test.go b/internal/service/rds/instance_automated_backups_replication_test.go index 7daafd3502a..39ed1f943d4 100644 --- a/internal/service/rds/instance_automated_backups_replication_test.go +++ b/internal/service/rds/instance_automated_backups_replication_test.go @@ -52,6 +52,36 @@ func TestAccRDSInstanceAutomatedBackupsReplication_basic(t *testing.T) { }) } +func TestAccRDSInstanceAutomatedBackupsReplication_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_db_instance_automated_backups_replication.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckMultipleRegion(t, 2) + }, + ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesAlternate(ctx, t), + CheckDestroy: testAccCheckInstanceAutomatedBackupsReplicationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccInstanceAutomatedBackupsReplicationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceAutomatedBackupsReplicationExist(ctx, resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfrds.ResourceInstanceAutomatedBackupsReplication(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func TestAccRDSInstanceAutomatedBackupsReplication_retentionPeriod(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -120,6 +150,51 @@ func TestAccRDSInstanceAutomatedBackupsReplication_kmsEncrypted(t *testing.T) { }) } +func testAccCheckInstanceAutomatedBackupsReplicationExist(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No RDS instance automated backups replication ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) + + _, err := tfrds.FindDBInstanceAutomatedBackupByARN(ctx, conn, rs.Primary.ID) + + return err + } +} + +func testAccCheckInstanceAutomatedBackupsReplicationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_db_instance_automated_backups_replication" { + continue + } + + _, err := tfrds.FindDBInstanceAutomatedBackupByARN(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("RDS instance automated backups replication %s still exists", rs.Primary.ID) + } + + return nil + } +} + func testAccInstanceAutomatedBackupsReplicationConfig_base(rName string, storageEncrypted bool) string { return acctest.ConfigCompose(acctest.ConfigMultipleRegionProvider(2), fmt.Sprintf(` data "aws_availability_zones" "available" { @@ -168,15 +243,30 @@ resource "aws_db_subnet_group" "test" { provider = "awsalternate" } +data "aws_rds_engine_version" "default" { + engine = "postgres" + + provider = "awsalternate" +} + +data "aws_rds_orderable_db_instance" "test" { + engine = data.aws_rds_engine_version.default.engine + engine_version = data.aws_rds_engine_version.default.version + license_model = "postgresql-license" + storage_type = "standard" + + preferred_instance_classes = [%[3]s] + + provider = "awsalternate" +} + resource "aws_db_instance" "test" { allocated_storage = 10 identifier = %[1]q - engine = "postgres" - engine_version = "13.4" - instance_class = "db.t3.micro" - db_name = "mydb" - username = "masterusername" - password = "mustbeeightcharacters" + engine = data.aws_rds_engine_version.default.engine + instance_class = data.aws_rds_orderable_db_instance.test.instance_class + password = "avoid-plaintext-passwords" + username = "tfacctest" backup_retention_period = 7 skip_final_snapshot = true storage_encrypted = %[2]t @@ -184,7 +274,7 @@ resource "aws_db_instance" "test" { provider = "awsalternate" } -`, rName, storageEncrypted)) +`, rName, storageEncrypted, postgresPreferredInstanceClasses)) } func testAccInstanceAutomatedBackupsReplicationConfig_basic(rName string) string { @@ -216,48 +306,3 @@ resource "aws_db_instance_automated_backups_replication" "test" { } `, rName)) } - -func testAccCheckInstanceAutomatedBackupsReplicationExist(ctx context.Context, n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("No RDS instance automated backups replication ID is set") - } - - conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) - - _, err := tfrds.FindDBInstanceAutomatedBackupByARN(ctx, conn, rs.Primary.ID) - - return err - } -} - -func testAccCheckInstanceAutomatedBackupsReplicationDestroy(ctx context.Context) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) - - for _, rs := range s.RootModule().Resources { - if rs.Type != "aws_db_instance_automated_backups_replication" { - continue - } - - _, err := tfrds.FindDBInstanceAutomatedBackupByARN(ctx, conn, rs.Primary.ID) - - if tfresource.NotFound(err) { - continue - } - - if err != nil { - return err - } - - return fmt.Errorf("RDS instance automated backups replication %s still exists", rs.Primary.ID) - } - - return nil - } -} diff --git a/internal/service/rds/service_package_gen.go b/internal/service/rds/service_package_gen.go index 09a27cab8fb..f0c082acef9 100644 --- a/internal/service/rds/service_package_gen.go +++ b/internal/service/rds/service_package_gen.go @@ -54,6 +54,8 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac { Factory: DataSourceSnapshot, TypeName: "aws_db_snapshot", + Name: "DB Snapshot", + Tags: &types.ServicePackageResourceTags{}, }, { Factory: DataSourceSubnetGroup, diff --git a/internal/service/rds/snapshot.go b/internal/service/rds/snapshot.go index d8b711fdb88..55d653ad72c 100644 --- a/internal/service/rds/snapshot.go +++ b/internal/service/rds/snapshot.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" "github.com/hashicorp/terraform-provider-aws/internal/flex" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" @@ -276,36 +277,59 @@ func FindDBSnapshotByID(ctx context.Context, conn *rds.RDS, id string) (*rds.DBS input := &rds.DescribeDBSnapshotsInput{ DBSnapshotIdentifier: aws.String(id), } + output, err := findDBSnapshot(ctx, conn, input) - output, err := conn.DescribeDBSnapshotsWithContext(ctx, input) + if err != nil { + return nil, err + } - if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBSnapshotNotFoundFault) { + // Eventual consistency check. + if aws.StringValue(output.DBSnapshotIdentifier) != id { return nil, &retry.NotFoundError{ - LastError: err, LastRequest: input, } } + return output, nil +} + +func findDBSnapshot(ctx context.Context, conn *rds.RDS, input *rds.DescribeDBSnapshotsInput) (*rds.DBSnapshot, error) { + output, err := findDBSnapshots(ctx, conn, input, tfslices.PredicateTrue[*rds.DBSnapshot]()) + if err != nil { return nil, err } - if output == nil || len(output.DBSnapshots) == 0 || output.DBSnapshots[0] == nil { - return nil, tfresource.NewEmptyResultError(input) - } + return tfresource.AssertSinglePtrResult(output) +} - if count := len(output.DBSnapshots); count > 1 { - return nil, tfresource.NewTooManyResultsError(count, input) - } +func findDBSnapshots(ctx context.Context, conn *rds.RDS, input *rds.DescribeDBSnapshotsInput, filter tfslices.Predicate[*rds.DBSnapshot]) ([]*rds.DBSnapshot, error) { + var output []*rds.DBSnapshot - dbSnapshot := output.DBSnapshots[0] + err := conn.DescribeDBSnapshotsPagesWithContext(ctx, input, func(page *rds.DescribeDBSnapshotsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - // Eventual consistency check. - if aws.StringValue(dbSnapshot.DBSnapshotIdentifier) != id { + for _, v := range page.DBSnapshots { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, rds.ErrCodeDBSnapshotNotFoundFault) { return nil, &retry.NotFoundError{ + LastError: err, LastRequest: input, } } - return dbSnapshot, nil + if err != nil { + return nil, err + } + + return output, nil } diff --git a/internal/service/rds/snapshot_data_source.go b/internal/service/rds/snapshot_data_source.go index b287856ff54..627ad5d3142 100644 --- a/internal/service/rds/snapshot_data_source.go +++ b/internal/service/rds/snapshot_data_source.go @@ -14,9 +14,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/slices" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" ) -// @SDKDataSource("aws_db_snapshot") +// @SDKDataSource("aws_db_snapshot", name="DB Snapshot") +// @Tags func DataSourceSnapshot() *schema.Resource { return &schema.Resource{ ReadWithoutTimeout: dataSourceSnapshotRead, @@ -31,18 +34,16 @@ func DataSourceSnapshot() *schema.Resource { Computed: true, }, "db_instance_identifier": { - Type: schema.TypeString, - Optional: true, - AtLeastOneOf: []string{"db_instance_identifier", "db_snapshot_identifier"}, + Type: schema.TypeString, + Optional: true, }, "db_snapshot_arn": { Type: schema.TypeString, Computed: true, }, "db_snapshot_identifier": { - Type: schema.TypeString, - Optional: true, - AtLeastOneOf: []string{"db_instance_identifier", "db_snapshot_identifier"}, + Type: schema.TypeString, + Optional: true, }, "encrypted": { Type: schema.TypeBool, @@ -115,6 +116,7 @@ func DataSourceSnapshot() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "tags": tftags.TagsSchemaComputed(), "vpc_id": { Type: schema.TypeString, Computed: true, @@ -144,25 +146,32 @@ func dataSourceSnapshotRead(ctx context.Context, d *schema.ResourceData, meta in input.SnapshotType = aws.String(v.(string)) } - output, err := conn.DescribeDBSnapshotsWithContext(ctx, input) + f := slices.PredicateTrue[*rds.DBSnapshot]() + if tags := getTagsIn(ctx); len(tags) > 0 { + f = func(v *rds.DBSnapshot) bool { + return KeyValueTags(ctx, v.TagList).ContainsAll(KeyValueTags(ctx, tags)) + } + } + + snapshots, err := findDBSnapshots(ctx, conn, input, f) if err != nil { return sdkdiag.AppendErrorf(diags, "reading RDS DB Snapshots: %s", err) } - if len(output.DBSnapshots) < 1 { + if len(snapshots) < 1 { return sdkdiag.AppendErrorf(diags, "Your query returned no results. Please change your search criteria and try again.") } var snapshot *rds.DBSnapshot - if len(output.DBSnapshots) > 1 { + if len(snapshots) > 1 { if d.Get("most_recent").(bool) { - snapshot = mostRecentDBSnapshot(output.DBSnapshots) + snapshot = mostRecentDBSnapshot(snapshots) } else { return sdkdiag.AppendErrorf(diags, "Your query returned more than one result. Please try a more specific search criteria.") } } else { - snapshot = output.DBSnapshots[0] + snapshot = snapshots[0] } d.SetId(aws.StringValue(snapshot.DBSnapshotIdentifier)) @@ -189,6 +198,8 @@ func dataSourceSnapshotRead(ctx context.Context, d *schema.ResourceData, meta in d.Set("storage_type", snapshot.StorageType) d.Set("vpc_id", snapshot.VpcId) + setTagsOut(ctx, snapshot.TagList) + return diags } diff --git a/internal/service/rds/snapshot_data_source_test.go b/internal/service/rds/snapshot_data_source_test.go index 7782ce0d462..7bdc823898b 100644 --- a/internal/service/rds/snapshot_data_source_test.go +++ b/internal/service/rds/snapshot_data_source_test.go @@ -10,7 +10,6 @@ import ( "github.com/aws/aws-sdk-go/service/rds" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" ) @@ -20,76 +19,74 @@ func TestAccRDSSnapshotDataSource_basic(t *testing.T) { t.Skip("skipping long-running test in short mode") } - rInt := sdkacctest.RandInt() + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_db_snapshot.test" + ds1Name := "data.aws_db_snapshot.by_id" + ds2Name := "data.aws_db_snapshot.by_tags" + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccSnapshotDataSourceConfig_basic(rInt), - Check: resource.ComposeTestCheckFunc( - testAccCheckSnapshotIDDataSource("data.aws_db_snapshot.snapshot"), + Config: testAccSnapshotDataSourceConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(ds1Name, "db_instance_identifier", resourceName, "db_instance_identifier"), + resource.TestCheckResourceAttrPair(ds1Name, "db_snapshot_arn", resourceName, "db_snapshot_arn"), + resource.TestCheckResourceAttrPair(ds1Name, "db_snapshot_identifier", resourceName, "db_snapshot_identifier"), + resource.TestCheckResourceAttrPair(ds1Name, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttr(ds1Name, "tags.Name", rName), + + resource.TestCheckResourceAttrPair(ds2Name, "db_instance_identifier", resourceName, "db_instance_identifier"), + resource.TestCheckResourceAttrPair(ds2Name, "db_snapshot_arn", resourceName, "db_snapshot_arn"), + resource.TestCheckResourceAttrPair(ds2Name, "db_snapshot_identifier", resourceName, "db_snapshot_identifier"), + resource.TestCheckResourceAttrPair(ds1Name, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttr(ds2Name, "tags.Name", rName), ), }, }, }) } -func testAccCheckSnapshotIDDataSource(n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Can't find Volume data source: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("Snapshot data source ID not set") - } - return nil - } -} - -func testAccSnapshotDataSourceConfig_basic(rInt int) string { - return fmt.Sprintf(` -data "aws_rds_engine_version" "default" { - engine = "mysql" -} +func testAccSnapshotDataSourceConfig_basic(rName string) string { + return acctest.ConfigCompose(testAccSnapshotConfig_base(rName), fmt.Sprintf(` +resource "aws_db_snapshot" "test" { + db_instance_identifier = aws_db_instance.test.identifier + db_snapshot_identifier = %[1]q -data "aws_rds_orderable_db_instance" "test" { - engine = data.aws_rds_engine_version.default.engine - engine_version = data.aws_rds_engine_version.default.version - preferred_instance_classes = [%[1]s] + tags = { + Name = %[1]q + Test = "true" + } } -resource "aws_db_instance" "bar" { - allocated_storage = 10 - engine = data.aws_rds_engine_version.default.engine - engine_version = data.aws_rds_engine_version.default.version - instance_class = data.aws_rds_orderable_db_instance.test.instance_class - db_name = "baz" - password = "barbarbarbar" - username = "foo" - skip_final_snapshot = true +resource "aws_db_snapshot_copy" "test" { + source_db_snapshot_identifier = aws_db_snapshot.test.db_snapshot_arn + target_db_snapshot_identifier = "%[1]s-copy" - # Maintenance Window is stored in lower case in the API, though not strictly - # documented. Terraform will downcase this to match (as opposed to throw a - # validation error). - maintenance_window = "Fri:09:00-Fri:09:30" - - backup_retention_period = 0 - - parameter_group_name = "default.${data.aws_rds_engine_version.default.parameter_group_family}" + tags = { + Name = "%[1]s-copy" + Test = "true" + } } -data "aws_db_snapshot" "snapshot" { +data "aws_db_snapshot" "by_id" { most_recent = "true" db_snapshot_identifier = aws_db_snapshot.test.id + + depends_on = [aws_db_snapshot_copy.test] } -resource "aws_db_snapshot" "test" { - db_instance_identifier = aws_db_instance.bar.identifier - db_snapshot_identifier = "testsnapshot%[2]d" +data "aws_db_snapshot" "by_tags" { + most_recent = "true" + + tags = { + Name = %[1]q + } + + depends_on = [aws_db_snapshot.test, aws_db_snapshot_copy.test] } -`, mySQLPreferredInstanceClasses, rInt) + +`, rName)) } diff --git a/internal/service/rds/status.go b/internal/service/rds/status.go index 1a0655b5110..20fc1da7ca4 100644 --- a/internal/service/rds/status.go +++ b/internal/service/rds/status.go @@ -5,7 +5,6 @@ package rds import ( "context" - "strconv" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rds" @@ -69,46 +68,6 @@ func statusDBClusterRole(ctx context.Context, conn *rds.RDS, dbClusterID, roleAR } } -func statusDBInstanceAutomatedBackup(ctx context.Context, conn *rds.RDS, arn string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - output, err := FindDBInstanceAutomatedBackupByARN(ctx, conn, arn) - - if tfresource.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - return output, aws.StringValue(output.Status), nil - } -} - -// statusDBInstanceHasAutomatedBackup returns whether or not a database instance has a specified automated backup. -// The connection must be valid for the database instance's Region. -func statusDBInstanceHasAutomatedBackup(ctx context.Context, conn *rds.RDS, dbInstanceID, dbInstanceAutomatedBackupsARN string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - output, err := findDBInstanceByIDSDKv1(ctx, conn, dbInstanceID) - - if tfresource.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - for _, v := range output.DBInstanceAutomatedBackupsReplications { - if aws.StringValue(v.DBInstanceAutomatedBackupsArn) == dbInstanceAutomatedBackupsARN { - return output, strconv.FormatBool(true), nil - } - } - - return output, strconv.FormatBool(false), nil - } -} - func statusDBProxy(ctx context.Context, conn *rds.RDS, name string) retry.StateRefreshFunc { return func() (interface{}, string, error) { output, err := FindDBProxyByName(ctx, conn, name) diff --git a/internal/service/rds/wait.go b/internal/service/rds/wait.go index 771b3d30447..77c36a77444 100644 --- a/internal/service/rds/wait.go +++ b/internal/service/rds/wait.go @@ -5,7 +5,6 @@ package rds import ( "context" - "strconv" "time" "github.com/aws/aws-sdk-go/service/rds" @@ -237,42 +236,6 @@ func waitDBClusterInstanceDeleted(ctx context.Context, conn *rds.RDS, id string, return nil, err } -func waitDBInstanceAutomatedBackupCreated(ctx context.Context, conn *rds.RDS, arn string, timeout time.Duration) (*rds.DBInstanceAutomatedBackup, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{InstanceAutomatedBackupStatusPending}, - Target: []string{InstanceAutomatedBackupStatusReplicating}, - Refresh: statusDBInstanceAutomatedBackup(ctx, conn, arn), - Timeout: timeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*rds.DBInstanceAutomatedBackup); ok { - return output, err - } - - return nil, err -} - -// waitDBInstanceAutomatedBackupDeleted waits for a specified automated backup to be deleted from a database instance. -// The connection must be valid for the database instance's Region. -func waitDBInstanceAutomatedBackupDeleted(ctx context.Context, conn *rds.RDS, dbInstanceID, dbInstanceAutomatedBackupsARN string, timeout time.Duration) (*rds.DBInstance, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{strconv.FormatBool(true)}, - Target: []string{strconv.FormatBool(false)}, - Refresh: statusDBInstanceHasAutomatedBackup(ctx, conn, dbInstanceID, dbInstanceAutomatedBackupsARN), - Timeout: timeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*rds.DBInstance); ok { - return output, err - } - - return nil, err -} - func waitDBProxyCreated(ctx context.Context, conn *rds.RDS, name string, timeout time.Duration) (*rds.DBProxy, error) { stateConf := &retry.StateChangeConf{ Pending: []string{rds.DBProxyStatusCreating}, diff --git a/internal/slices/filters.go b/internal/slices/filters.go deleted file mode 100644 index 3ca377bde10..00000000000 --- a/internal/slices/filters.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package slices - -func FilterEquals[T comparable](v T) FilterFunc[T] { - return func(x T) bool { - return x == v - } -} diff --git a/internal/slices/predicates.go b/internal/slices/predicates.go new file mode 100644 index 00000000000..b184e6237d7 --- /dev/null +++ b/internal/slices/predicates.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package slices + +// PredicateEquals returns a Predicate that evaluates to true if the predicate's argument equals `v`. +func PredicateEquals[T comparable](v T) Predicate[T] { + return func(x T) bool { + return x == v + } +} + +// PredicateTrue returns a Predicate that always evaluates to true. +func PredicateTrue[T any]() Predicate[T] { + return func(T) bool { + return true + } +} diff --git a/internal/slices/slices.go b/internal/slices/slices.go index f21ffcd4164..298e52135f2 100644 --- a/internal/slices/slices.go +++ b/internal/slices/slices.go @@ -41,10 +41,11 @@ func ApplyToAll[T, U any](s []T, f func(T) U) []U { return v } -type FilterFunc[T any] func(T) bool +// Predicate represents a predicate (boolean-valued function) of one argument. +type Predicate[T any] func(T) bool // Filter returns a new slice containing all values that return `true` for the filter function `f` -func Filter[T any](s []T, f FilterFunc[T]) []T { +func Filter[T any](s []T, f Predicate[T]) []T { v := make([]T, 0, len(s)) for _, e := range s { @@ -57,7 +58,7 @@ func Filter[T any](s []T, f FilterFunc[T]) []T { } // All returns `true` if the filter function `f` retruns `true` for all items -func All[T any](s []T, f FilterFunc[T]) bool { +func All[T any](s []T, f Predicate[T]) bool { for _, e := range s { if !f(e) { return false @@ -67,7 +68,7 @@ func All[T any](s []T, f FilterFunc[T]) bool { } // Any returns `true` if the filter function `f` retruns `true` for any item -func Any[T any](s []T, f FilterFunc[T]) bool { +func Any[T any](s []T, f Predicate[T]) bool { for _, e := range s { if f(e) { return true diff --git a/internal/slices/slices_test.go b/internal/slices/slices_test.go index cd08f2c8d76..87b6a9a37f2 100644 --- a/internal/slices/slices_test.go +++ b/internal/slices/slices_test.go @@ -169,3 +169,41 @@ func TestChunk(t *testing.T) { }) } } + +func TestFilter(t *testing.T) { + t.Parallel() + + type testCase struct { + input []string + expected []string + } + tests := map[string]testCase{ + "three elements": { + input: []string{"one", "two", "3", "a0"}, + expected: []string{"a0"}, + }, + "one element": { + input: []string{"abcdEFGH"}, + expected: []string{"abcdEFGH"}, + }, + "zero elements": { + input: []string{}, + expected: []string{}, + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := Filter(test.input, func(v string) bool { + return strings.HasPrefix(v, "a") + }) + + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("unexpected diff (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/website/docs/d/db_snapshot.html.markdown b/website/docs/d/db_snapshot.html.markdown index 6dac756de1a..608abd9e726 100644 --- a/website/docs/d/db_snapshot.html.markdown +++ b/website/docs/d/db_snapshot.html.markdown @@ -53,21 +53,18 @@ This argument supports the following arguments: * `most_recent` - (Optional) If more than one result is returned, use the most recent Snapshot. - * `db_instance_identifier` - (Optional) Returns the list of snapshots created by the specific db_instance - * `db_snapshot_identifier` - (Optional) Returns information on a specific snapshot_id. - * `snapshot_type` - (Optional) Type of snapshots to be returned. If you don't specify a SnapshotType value, then both automated and manual snapshots are returned. Shared and public DB snapshots are not included in the returned results by default. Possible values are, `automated`, `manual`, `shared`, `public` and `awsbackup`. - * `include_shared` - (Optional) Set this value to true to include shared manual DB snapshots from other AWS accounts that this AWS account has been given permission to copy or restore, otherwise set this value to false. The default is `false`. - * `include_public` - (Optional) Set this value to true to include manual DB snapshots that are public and can be copied or restored by any AWS account, otherwise set this value to false. The default is `false`. +`tags` - (Optional) Mapping of tags, each pair of which must exactly match + a pair on the desired DB snapshot. ## Attribute Reference