diff --git a/.changelog/34500.txt b/.changelog/34500.txt new file mode 100644 index 00000000000..772933021b1 --- /dev/null +++ b/.changelog/34500.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_neptune_parameter_group: Add `name_prefix` argument +``` \ No newline at end of file diff --git a/.changelog/34637.txt b/.changelog/34637.txt new file mode 100644 index 00000000000..d75cfd9b4f5 --- /dev/null +++ b/.changelog/34637.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_docdb_cluster: Add `storage_type` argument +``` \ No newline at end of file diff --git a/internal/service/docdb/cluster.go b/internal/service/docdb/cluster.go index ef2b55740c2..9901904f413 100644 --- a/internal/service/docdb/cluster.go +++ b/internal/service/docdb/cluster.go @@ -241,6 +241,20 @@ func ResourceCluster() *schema.Resource { Optional: true, ForceNew: true, }, + "storage_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(storageType_Values(), false), + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // When you create a DocumentDB DB cluster with the storage type set to "iopt1", + // the storage type is returned in the response. + // The storage type isn't returned when you set it to "standard". + if old == "" && new == storageTypeStandard { + return true + } + return old == new + }, + }, names.AttrTags: tftags.TagsSchema(), names.AttrTagsAll: tftags.TagsSchemaComputed(), "vpc_security_group_ids": { @@ -271,7 +285,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int // ModifyDBInstance afterwadocdb to prevent Terraform operators from API // errors or needing to double apply. var requiresModifyDbCluster bool - modifyDbClusterInput := &docdb.ModifyDBClusterInput{ + inputM := &docdb.ModifyDBClusterInput{ ApplyImmediately: aws.Bool(true), } @@ -289,12 +303,12 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int } if v, ok := d.GetOk("backup_retention_period"); ok { - modifyDbClusterInput.BackupRetentionPeriod = aws.Int64(int64(v.(int))) + inputM.BackupRetentionPeriod = aws.Int64(int64(v.(int))) requiresModifyDbCluster = true } if v, ok := d.GetOk("db_cluster_parameter_group_name"); ok { - modifyDbClusterInput.DBClusterParameterGroupName = aws.String(v.(string)) + inputM.DBClusterParameterGroupName = aws.String(v.(string)) requiresModifyDbCluster = true } @@ -319,15 +333,19 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int } if v, ok := d.GetOk("preferred_backup_window"); ok { - modifyDbClusterInput.PreferredBackupWindow = aws.String(v.(string)) + inputM.PreferredBackupWindow = aws.String(v.(string)) requiresModifyDbCluster = true } if v, ok := d.GetOk("preferred_maintenance_window"); ok { - modifyDbClusterInput.PreferredMaintenanceWindow = aws.String(v.(string)) + inputM.PreferredMaintenanceWindow = aws.String(v.(string)) requiresModifyDbCluster = true } + if v, ok := d.GetOk("storage_type"); ok { + input.StorageType = aws.String(v.(string)) + } + if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { input.VpcSecurityGroupIds = flex.ExpandStringSet(v) } @@ -407,10 +425,14 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int input.PreferredMaintenanceWindow = aws.String(v.(string)) } - if v, ok := d.GetOkExists("storage_encrypted"); ok { + if v, ok := d.GetOk("storage_encrypted"); ok { input.StorageEncrypted = aws.Bool(v.(bool)) } + if v, ok := d.GetOk("storage_type"); ok { + input.StorageType = aws.String(v.(string)) + } + if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 { input.VpcSecurityGroupIds = flex.ExpandStringSet(v) } @@ -426,20 +448,20 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int d.SetId(identifier) - if _, err := waitDBClusterCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + if _, err := waitDBClusterAvailable(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { return sdkdiag.AppendErrorf(diags, "waiting for DocumentDB Cluster (%s) create: %s", d.Id(), err) } if requiresModifyDbCluster { - modifyDbClusterInput.DBClusterIdentifier = aws.String(d.Id()) + inputM.DBClusterIdentifier = aws.String(d.Id()) - _, err := conn.ModifyDBClusterWithContext(ctx, modifyDbClusterInput) + _, err := conn.ModifyDBClusterWithContext(ctx, inputM) if err != nil { return sdkdiag.AppendErrorf(diags, "modifying DocumentDB Cluster (%s): %s", d.Id(), err) } - if _, err := waitDBClusterUpdated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + if _, err := waitDBClusterAvailable(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { return sdkdiag.AppendErrorf(diags, "waiting for DocumentDB Cluster (%s) update: %s", d.Id(), err) } } @@ -463,18 +485,14 @@ func resourceClusterRead(ctx context.Context, d *schema.ResourceData, meta inter return sdkdiag.AppendErrorf(diags, "reading DocumentDB Cluster (%s): %s", d.Id(), err) } - globalCluster, err := findGlobalClusterByARN(ctx, conn, aws.StringValue(dbc.DBClusterArn)) - // Ignore the following API error for regions/partitions that do not support DocDB Global Clusters: // InvalidParameterValue: Access Denied to API Version: APIGlobalDatabases - if err != nil && !tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "Access Denied to API Version: APIGlobalDatabases") { - return sdkdiag.AppendErrorf(diags, "reading DocumentDB Cluster (%s) Global Cluster information: %s", d.Id(), err) - } - - if globalCluster != nil { - d.Set("global_cluster_identifier", globalCluster.GlobalClusterIdentifier) - } else { + if globalCluster, err := findGlobalClusterByClusterARN(ctx, conn, aws.StringValue(dbc.DBClusterArn)); tfresource.NotFound(err) || tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "Access Denied to API Version: APIGlobalDatabases") { d.Set("global_cluster_identifier", "") + } else if err != nil { + return sdkdiag.AppendErrorf(diags, "reading DocumentDB Global Cluster information for DocumentDB Cluster (%s): %s", d.Id(), err) + } else { + d.Set("global_cluster_identifier", globalCluster.GlobalClusterIdentifier) } d.Set("arn", dbc.DBClusterArn) @@ -482,11 +500,11 @@ func resourceClusterRead(ctx context.Context, d *schema.ResourceData, meta inter d.Set("backup_retention_period", dbc.BackupRetentionPeriod) d.Set("cluster_identifier", dbc.DBClusterIdentifier) d.Set("cluster_identifier_prefix", create.NamePrefixFromName(aws.StringValue(dbc.DBClusterIdentifier))) - var cm []string - for _, m := range dbc.DBClusterMembers { - cm = append(cm, aws.StringValue(m.DBInstanceIdentifier)) + var clusterMembers []string + for _, v := range dbc.DBClusterMembers { + clusterMembers = append(clusterMembers, aws.StringValue(v.DBInstanceIdentifier)) } - d.Set("cluster_members", cm) + d.Set("cluster_members", clusterMembers) d.Set("cluster_resource_id", dbc.DbClusterResourceId) d.Set("db_cluster_parameter_group_name", dbc.DBClusterParameterGroup) d.Set("db_subnet_group_name", dbc.DBSubnetGroup) @@ -503,11 +521,12 @@ func resourceClusterRead(ctx context.Context, d *schema.ResourceData, meta inter d.Set("preferred_maintenance_window", dbc.PreferredMaintenanceWindow) d.Set("reader_endpoint", dbc.ReaderEndpoint) d.Set("storage_encrypted", dbc.StorageEncrypted) - var vpcg []string - for _, g := range dbc.VpcSecurityGroups { - vpcg = append(vpcg, aws.StringValue(g.VpcSecurityGroupId)) + d.Set("storage_type", dbc.StorageType) + var securityGroupIDs []string + for _, v := range dbc.VpcSecurityGroups { + securityGroupIDs = append(securityGroupIDs, aws.StringValue(v.VpcSecurityGroupId)) } - d.Set("vpc_security_group_ids", vpcg) + d.Set("vpc_security_group_ids", securityGroupIDs) return diags } @@ -554,6 +573,10 @@ func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta int input.PreferredBackupWindow = aws.String(d.Get("preferred_backup_window").(string)) } + if d.HasChange("storage_type") { + input.StorageType = aws.String(d.Get("storage_type").(string)) + } + if d.HasChange("preferred_maintenance_window") { input.PreferredMaintenanceWindow = aws.String(d.Get("preferred_maintenance_window").(string)) } @@ -574,9 +597,11 @@ func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta int if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "IAM role ARN value is invalid or does not include the required permissions") { return true, err } + if tfawserr.ErrMessageContains(err, docdb.ErrCodeInvalidDBClusterStateFault, "is not currently in the available state") { return true, err } + if tfawserr.ErrMessageContains(err, docdb.ErrCodeInvalidDBClusterStateFault, "cluster is a part of a global cluster") { return true, err } @@ -589,7 +614,7 @@ func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta int return sdkdiag.AppendErrorf(diags, "modifying DocumentDB Cluster (%s): %s", d.Id(), err) } - if _, err := waitDBClusterUpdated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { + if _, err := waitDBClusterAvailable(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { return sdkdiag.AppendErrorf(diags, "waiting for DocumentDB Cluster (%s) update: %s", d.Id(), err) } } @@ -606,15 +631,8 @@ func resourceClusterUpdate(ctx context.Context, d *schema.ResourceData, meta int return sdkdiag.AppendErrorf(diags, "existing DocumentDB Clusters cannot be migrated between existing DocumentDB Global Clusters") } - input := &docdb.RemoveFromGlobalClusterInput{ - DbClusterIdentifier: aws.String(d.Get("arn").(string)), - GlobalClusterIdentifier: aws.String(o), - } - - _, err := conn.RemoveFromGlobalClusterWithContext(ctx, input) - - if err != nil && !tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) && !tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "is not found in global cluster") { - return sdkdiag.AppendErrorf(diags, "removing DocumentDB Cluster (%s) from DocumentDB Global Cluster: %s", d.Id(), err) + if err := removeClusterFromGlobalCluster(ctx, conn, d.Get("arn").(string), o, d.Timeout(schema.TimeoutUpdate)); err != nil { + return append(diags, diag.FromErr(err)...) } } @@ -625,30 +643,12 @@ func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta int var diags diag.Diagnostics conn := meta.(*conns.AWSClient).DocDBConn(ctx) - log.Printf("[DEBUG] Deleting DocumentDB Cluster: %s", d.Id()) - - // Automatically remove from global cluster to bypass this error on deletion: - // InvalidDBClusterStateFault: This cluster is a part of a global cluster, please remove it from globalcluster first - if d.Get("global_cluster_identifier").(string) != "" { - input := &docdb.RemoveFromGlobalClusterInput{ - DbClusterIdentifier: aws.String(d.Get("arn").(string)), - GlobalClusterIdentifier: aws.String(d.Get("global_cluster_identifier").(string)), - } - - _, err := conn.RemoveFromGlobalClusterWithContext(ctx, input) - - if err != nil && !tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) && !tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "is not found in global cluster") { - return sdkdiag.AppendErrorf(diags, "removing DocumentDB Cluster (%s) from Global Cluster: %s", d.Id(), err) - } - } - + skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) input := &docdb.DeleteDBClusterInput{ DBClusterIdentifier: aws.String(d.Id()), + SkipFinalSnapshot: aws.Bool(skipFinalSnapshot), } - skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) - input.SkipFinalSnapshot = aws.Bool(skipFinalSnapshot) - if !skipFinalSnapshot { if v, ok := d.GetOk("final_snapshot_identifier"); ok { input.FinalDBSnapshotIdentifier = aws.String(v.(string)) @@ -657,7 +657,14 @@ func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta int } } - _, err := tfresource.RetryWhen(ctx, 5*time.Minute, + if v, ok := d.GetOk("global_cluster_identifier"); ok { + if err := removeClusterFromGlobalCluster(ctx, conn, d.Get("arn").(string), v.(string), d.Timeout(schema.TimeoutDelete)); err != nil { + return append(diags, diag.FromErr(err)...) + } + } + + log.Printf("[DEBUG] Deleting DocumentDB Cluster: %s", d.Id()) + _, err := tfresource.RetryWhen(ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { return conn.DeleteDBClusterWithContext(ctx, input) }, @@ -665,6 +672,7 @@ func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta int if tfawserr.ErrMessageContains(err, docdb.ErrCodeInvalidDBClusterStateFault, "is not currently in the available state") { return true, err } + if tfawserr.ErrMessageContains(err, docdb.ErrCodeInvalidDBClusterStateFault, "cluster is a part of a global cluster") { return true, err } @@ -720,11 +728,38 @@ func diffCloudWatchLogsExportConfiguration(old, new []interface{}) ([]interface{ return add, disable } +func removeClusterFromGlobalCluster(ctx context.Context, conn *docdb.DocDB, clusterARN, globalClusterID string, timeout time.Duration) error { + input := &docdb.RemoveFromGlobalClusterInput{ + DbClusterIdentifier: aws.String(clusterARN), + GlobalClusterIdentifier: aws.String(globalClusterID), + } + + _, err := conn.RemoveFromGlobalClusterWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBClusterNotFoundFault, docdb.ErrCodeGlobalClusterNotFoundFault) || tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "is not found in global cluster") { + return nil + } + + if err != nil { + return fmt.Errorf("removing DocumentDB Cluster (%s) from DocumentDB Global Cluster (%s): %w", clusterARN, globalClusterID, err) + } + + _, err = tfresource.RetryUntilNotFound(ctx, timeout, func() (interface{}, error) { + return findGlobalClusterByClusterARN(ctx, conn, clusterARN) + }) + + if err != nil { + return fmt.Errorf("waiting for DocumentDB Cluster (%s) removal from DocumentDB Global Cluster (%s): %w", clusterARN, globalClusterID, err) + } + + return nil +} + func FindDBClusterByID(ctx context.Context, conn *docdb.DocDB, id string) (*docdb.DBCluster, error) { input := &docdb.DescribeDBClustersInput{ DBClusterIdentifier: aws.String(id), } - output, err := findDBCluster(ctx, conn, input) + output, err := findDBCluster(ctx, conn, input, tfslices.PredicateTrue[*docdb.DBCluster]()) if err != nil { return nil, err @@ -740,8 +775,16 @@ func FindDBClusterByID(ctx context.Context, conn *docdb.DocDB, id string) (*docd return output, nil } -func findDBCluster(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBClustersInput) (*docdb.DBCluster, error) { - output, err := findDBClusters(ctx, conn, input) +func findClusterByARN(ctx context.Context, conn *docdb.DocDB, arn string) (*docdb.DBCluster, error) { + input := &docdb.DescribeDBClustersInput{} + + return findDBCluster(ctx, conn, input, func(v *docdb.DBCluster) bool { + return aws.StringValue(v.DBClusterArn) == arn + }) +} + +func findDBCluster(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBClustersInput, filter tfslices.Predicate[*docdb.DBCluster]) (*docdb.DBCluster, error) { + output, err := findDBClusters(ctx, conn, input, filter) if err != nil { return nil, err @@ -750,7 +793,7 @@ func findDBCluster(ctx context.Context, conn *docdb.DocDB, input *docdb.Describe return tfresource.AssertSinglePtrResult(output) } -func findDBClusters(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBClustersInput) ([]*docdb.DBCluster, error) { +func findDBClusters(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBClustersInput, filter tfslices.Predicate[*docdb.DBCluster]) ([]*docdb.DBCluster, error) { var output []*docdb.DBCluster err := conn.DescribeDBClustersPagesWithContext(ctx, input, func(page *docdb.DescribeDBClustersOutput, lastPage bool) bool { @@ -759,7 +802,7 @@ func findDBClusters(ctx context.Context, conn *docdb.DocDB, input *docdb.Describ } for _, v := range page.DBClusters { - if v != nil { + if v != nil && filter(v) { output = append(output, v) } } @@ -797,41 +840,18 @@ func statusDBCluster(ctx context.Context, conn *docdb.DocDB, id string) retry.St } } -func waitDBClusterCreated(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.DBCluster, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{ - "creating", - "backing-up", - "modifying", - "preparing-data-migration", - "migrating", - "resetting-master-credentials", - }, - Target: []string{"available"}, - Refresh: statusDBCluster(ctx, conn, id), - Timeout: timeout, - MinTimeout: 10 * time.Second, - Delay: 30 * time.Second, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*docdb.DBCluster); ok { - return output, err - } - - return nil, err -} - -func waitDBClusterUpdated(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.DBCluster, error) { //nolint:unparam +func waitDBClusterAvailable(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.DBCluster, error) { //nolint:unparam stateConf := &retry.StateChangeConf{ Pending: []string{ - "backing-up", - "modifying", - "resetting-master-credentials", - "upgrading", + clusterStatusCreating, + clusterStatusBackingUp, + clusterStatusModifying, + clusterStatusPreparingDataMigration, + clusterStatusMigrating, + clusterStatusResettingMasterCredentials, + clusterStatusUpgrading, }, - Target: []string{"available"}, + Target: []string{clusterStatusAvailable}, Refresh: statusDBCluster(ctx, conn, id), Timeout: timeout, MinTimeout: 10 * time.Second, @@ -850,10 +870,10 @@ func waitDBClusterUpdated(ctx context.Context, conn *docdb.DocDB, id string, tim func waitDBClusterDeleted(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.DBCluster, error) { stateConf := &retry.StateChangeConf{ Pending: []string{ - "available", - "deleting", - "backing-up", - "modifying", + clusterStatusAvailable, + clusterStatusDeleting, + clusterStatusBackingUp, + clusterStatusModifying, }, Target: []string{}, Refresh: statusDBCluster(ctx, conn, id), diff --git a/internal/service/docdb/cluster_parameter_group.go b/internal/service/docdb/cluster_parameter_group.go index 435e1396746..a1af860ca73 100644 --- a/internal/service/docdb/cluster_parameter_group.go +++ b/internal/service/docdb/cluster_parameter_group.go @@ -26,8 +26,6 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) -const clusterParameterGroupMaxParamsBulkEdit = 20 - // @SDKResource("aws_docdb_cluster_parameter_group", name="Cluster Parameter Group") // @Tags(identifierAttribute="arn") func ResourceClusterParameterGroup() *schema.Resource { @@ -159,21 +157,8 @@ func resourceClusterParameterGroupRead(ctx context.Context, d *schema.ResourceDa input := &docdb.DescribeDBClusterParametersInput{ DBClusterParameterGroupName: aws.String(d.Id()), } - var parameters []*docdb.Parameter - - err = conn.DescribeDBClusterParametersPagesWithContext(ctx, input, func(page *docdb.DescribeDBClusterParametersOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - for _, v := range page.Parameters { - if v != nil { - parameters = append(parameters, v) - } - } - - return !lastPage - }) + parameters, err := findDBClusterParameters(ctx, conn, input) if err != nil { return sdkdiag.AppendErrorf(diags, "reading DocumentDB Cluster Parameter Group (%s) parameters: %s", d.Id(), err) @@ -192,15 +177,7 @@ func resourceClusterParameterGroupUpdate(ctx context.Context, d *schema.Resource if d.HasChange("parameter") { o, n := d.GetChange("parameter") - if o == nil { - o = new(schema.Set) - } - if n == nil { - n = new(schema.Set) - } - - os := o.(*schema.Set) - ns := n.(*schema.Set) + os, ns := o.(*schema.Set), n.(*schema.Set) if parameters := expandParameters(ns.Difference(os).List()); len(parameters) > 0 { err := modifyClusterParameterGroupParameters(ctx, conn, d.Id(), parameters) @@ -243,6 +220,9 @@ func resourceClusterParameterGroupDelete(ctx context.Context, d *schema.Resource } func modifyClusterParameterGroupParameters(ctx context.Context, conn *docdb.DocDB, name string, parameters []*docdb.Parameter) error { + const ( + clusterParameterGroupMaxParamsBulkEdit = 20 + ) // We can only modify 20 parameters at a time, so chunk them until we've got them all. for _, chunk := range tfslices.Chunks(parameters, clusterParameterGroupMaxParamsBulkEdit) { input := &docdb.ModifyDBClusterParameterGroupInput{ @@ -320,3 +300,34 @@ func findDBClusterParameterGroups(ctx context.Context, conn *docdb.DocDB, input return output, nil } + +func findDBClusterParameters(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBClusterParametersInput) ([]*docdb.Parameter, error) { + var output []*docdb.Parameter + + err := conn.DescribeDBClusterParametersPagesWithContext(ctx, input, func(page *docdb.DescribeDBClusterParametersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.Parameters { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBParameterGroupNotFoundFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} diff --git a/internal/service/docdb/cluster_snapshot.go b/internal/service/docdb/cluster_snapshot.go index a101ab6e18e..767e3cc3800 100644 --- a/internal/service/docdb/cluster_snapshot.go +++ b/internal/service/docdb/cluster_snapshot.go @@ -5,7 +5,6 @@ package docdb import ( "context" - "fmt" "log" "time" @@ -17,7 +16,7 @@ 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/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) // @SDKResource("aws_docdb_cluster_snapshot") @@ -26,6 +25,7 @@ func ResourceClusterSnapshot() *schema.Resource { CreateWithoutTimeout: resourceClusterSnapshotCreate, ReadWithoutTimeout: resourceClusterSnapshotRead, DeleteWithoutTimeout: resourceClusterSnapshotDelete, + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -35,31 +35,26 @@ func ResourceClusterSnapshot() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "db_cluster_snapshot_identifier": { - Type: schema.TypeString, - ValidateFunc: validClusterSnapshotIdentifier, - Required: true, - ForceNew: true, + "availability_zones": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, }, "db_cluster_identifier": { Type: schema.TypeString, - ValidateFunc: validClusterIdentifier, Required: true, ForceNew: true, - }, - - "availability_zones": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - Computed: true, + ValidateFunc: validClusterIdentifier, }, "db_cluster_snapshot_arn": { Type: schema.TypeString, Computed: true, }, - "storage_encrypted": { - Type: schema.TypeBool, - Computed: true, + "db_cluster_snapshot_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validClusterSnapshotIdentifier, }, "engine": { Type: schema.TypeString, @@ -77,11 +72,11 @@ func ResourceClusterSnapshot() *schema.Resource { Type: schema.TypeInt, Computed: true, }, - "source_db_cluster_snapshot_arn": { + "snapshot_type": { Type: schema.TypeString, Computed: true, }, - "snapshot_type": { + "source_db_cluster_snapshot_arn": { Type: schema.TypeString, Computed: true, }, @@ -89,6 +84,10 @@ func ResourceClusterSnapshot() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "storage_encrypted": { + Type: schema.TypeBool, + Computed: true, + }, "vpc_id": { Type: schema.TypeString, Computed: true, @@ -101,30 +100,22 @@ func resourceClusterSnapshotCreate(ctx context.Context, d *schema.ResourceData, var diags diag.Diagnostics conn := meta.(*conns.AWSClient).DocDBConn(ctx) - params := &docdb.CreateDBClusterSnapshotInput{ + clusterSnapshotID := d.Get("db_cluster_snapshot_identifier").(string) + input := &docdb.CreateDBClusterSnapshotInput{ DBClusterIdentifier: aws.String(d.Get("db_cluster_identifier").(string)), - DBClusterSnapshotIdentifier: aws.String(d.Get("db_cluster_snapshot_identifier").(string)), + DBClusterSnapshotIdentifier: aws.String(clusterSnapshotID), } - _, err := conn.CreateDBClusterSnapshotWithContext(ctx, params) + _, err := conn.CreateDBClusterSnapshotWithContext(ctx, input) + if err != nil { - return sdkdiag.AppendErrorf(diags, "creating DocumentDB Cluster Snapshot: %s", err) + return sdkdiag.AppendErrorf(diags, "creating DocumentDB Cluster Snapshot (%s): %s", clusterSnapshotID, err) } - d.SetId(d.Get("db_cluster_snapshot_identifier").(string)) - stateConf := &retry.StateChangeConf{ - Pending: []string{"creating"}, - Target: []string{"available"}, - Refresh: resourceClusterSnapshotStateRefreshFunc(ctx, d.Id(), conn), - Timeout: d.Timeout(schema.TimeoutCreate), - MinTimeout: 10 * time.Second, - Delay: 5 * time.Second, - } + d.SetId(clusterSnapshotID) - // Wait, catching any errors - _, err = stateConf.WaitForStateContext(ctx) - if err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for DocumentDB Cluster Snapshot %q to create: %s", d.Id(), err) + if _, err := waitClusterSnapshotCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for DocumentDB Cluster Snapshot (%s) create: %s", d.Id(), err) } return append(diags, resourceClusterSnapshotRead(ctx, d, meta)...) @@ -134,30 +125,19 @@ func resourceClusterSnapshotRead(ctx context.Context, d *schema.ResourceData, me var diags diag.Diagnostics conn := meta.(*conns.AWSClient).DocDBConn(ctx) - params := &docdb.DescribeDBClusterSnapshotsInput{ - DBClusterSnapshotIdentifier: aws.String(d.Id()), - } - resp, err := conn.DescribeDBClusterSnapshotsWithContext(ctx, params) - if err != nil { - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBClusterSnapshotNotFoundFault) { - log.Printf("[WARN] DocumentDB Cluster Snapshot (%s) not found, removing from state", d.Id()) - d.SetId("") - return diags - } - return sdkdiag.AppendErrorf(diags, "reading DocumentDB Cluster Snapshot %q: %s", d.Id(), err) - } + snapshot, err := FindClusterSnapshotByID(ctx, conn, d.Id()) - if !d.IsNewResource() && (resp == nil || len(resp.DBClusterSnapshots) == 0 || resp.DBClusterSnapshots[0] == nil || aws.StringValue(resp.DBClusterSnapshots[0].DBClusterSnapshotIdentifier) != d.Id()) { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] DocumentDB Cluster Snapshot (%s) not found, removing from state", d.Id()) d.SetId("") return diags } - snapshot := resp.DBClusterSnapshots[0] - - if err := d.Set("availability_zones", flex.FlattenStringList(snapshot.AvailabilityZones)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting availability_zones: %s", err) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading DocumentDB Cluster Snapshot (%s): %s", d.Id(), err) } + + d.Set("availability_zones", aws.StringValueSlice(snapshot.AvailabilityZones)) d.Set("db_cluster_identifier", snapshot.DBClusterIdentifier) d.Set("db_cluster_snapshot_arn", snapshot.DBClusterSnapshotArn) d.Set("db_cluster_snapshot_identifier", snapshot.DBClusterSnapshotIdentifier) @@ -178,40 +158,114 @@ func resourceClusterSnapshotDelete(ctx context.Context, d *schema.ResourceData, var diags diag.Diagnostics conn := meta.(*conns.AWSClient).DocDBConn(ctx) - params := &docdb.DeleteDBClusterSnapshotInput{ + log.Printf("[DEBUG] Deleting DocumentDB Cluster Snapshot: %s", d.Id()) + _, err := conn.DeleteDBClusterSnapshotWithContext(ctx, &docdb.DeleteDBClusterSnapshotInput{ DBClusterSnapshotIdentifier: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBClusterSnapshotNotFoundFault) { + return diags } - _, err := conn.DeleteDBClusterSnapshotWithContext(ctx, params) + if err != nil { - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBClusterSnapshotNotFoundFault) { - return diags - } - return sdkdiag.AppendErrorf(diags, "deleting DocumentDB Cluster Snapshot %q: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "deleting DocumentDB Cluster Snapshot (%s): %s", d.Id(), err) } return diags } -func resourceClusterSnapshotStateRefreshFunc(ctx context.Context, dbClusterSnapshotIdentifier string, conn *docdb.DocDB) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - opts := &docdb.DescribeDBClusterSnapshotsInput{ - DBClusterSnapshotIdentifier: aws.String(dbClusterSnapshotIdentifier), +func FindClusterSnapshotByID(ctx context.Context, conn *docdb.DocDB, id string) (*docdb.DBClusterSnapshot, error) { + input := &docdb.DescribeDBClusterSnapshotsInput{ + DBClusterSnapshotIdentifier: aws.String(id), + } + output, err := findClusterSnapshot(ctx, conn, input) + + if err != nil { + return nil, err + } + + // Eventual consistency check. + if aws.StringValue(output.DBClusterSnapshotIdentifier) != id { + return nil, &retry.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + +func findClusterSnapshot(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBClusterSnapshotsInput) (*docdb.DBClusterSnapshot, error) { + output, err := findClusterSnapshots(ctx, conn, input) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findClusterSnapshots(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBClusterSnapshotsInput) ([]*docdb.DBClusterSnapshot, error) { + var output []*docdb.DBClusterSnapshot + + err := conn.DescribeDBClusterSnapshotsPagesWithContext(ctx, input, func(page *docdb.DescribeDBClusterSnapshotsOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - resp, err := conn.DescribeDBClusterSnapshotsWithContext(ctx, opts) - if err != nil { - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBClusterSnapshotNotFoundFault) { - return nil, "", nil + for _, v := range page.DBClusterSnapshots { + if v != nil { + output = append(output, v) } - return nil, "", fmt.Errorf("retrieving DocumentDB Cluster Snapshots: %s", err) } - if resp == nil || len(resp.DBClusterSnapshots) == 0 || resp.DBClusterSnapshots[0] == nil { - return nil, "", fmt.Errorf("No snapshots returned for %s", dbClusterSnapshotIdentifier) + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBClusterSnapshotNotFoundFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func statusClusterSnapshot(ctx context.Context, conn *docdb.DocDB, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindClusterSnapshotByID(ctx, conn, id) - snapshot := resp.DBClusterSnapshots[0] + if tfresource.NotFound(err) { + return nil, "", nil + } - return resp, aws.StringValue(snapshot.Status), nil + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil } } + +func waitClusterSnapshotCreated(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.DBClusterSnapshot, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{clusterSnapshotStatusCreating}, + Target: []string{clusterSnapshotStatusAvailable}, + Refresh: statusClusterSnapshot(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 5 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*docdb.DBClusterSnapshot); ok { + return output, err + } + + return nil, err +} diff --git a/internal/service/docdb/cluster_snapshot_test.go b/internal/service/docdb/cluster_snapshot_test.go index b88dca38ec8..2674aa298dd 100644 --- a/internal/service/docdb/cluster_snapshot_test.go +++ b/internal/service/docdb/cluster_snapshot_test.go @@ -9,14 +9,14 @@ import ( "testing" "github.com/YakDriver/regexache" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/docdb" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" 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" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfdocdb "github.com/hashicorp/terraform-provider-aws/internal/service/docdb" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccDocDBClusterSnapshot_basic(t *testing.T) { @@ -57,6 +57,30 @@ func TestAccDocDBClusterSnapshot_basic(t *testing.T) { }) } +func TestAccDocDBClusterSnapshot_disappears(t *testing.T) { + ctx := acctest.Context(t) + var dbClusterSnapshot docdb.DBClusterSnapshot + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_docdb_cluster_snapshot.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, docdb.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterSnapshotDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccClusterSnapshotConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterSnapshotExists(ctx, resourceName, &dbClusterSnapshot), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfdocdb.ResourceClusterSnapshot(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckClusterSnapshotDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).DocDBConn(ctx) @@ -66,106 +90,62 @@ func testAccCheckClusterSnapshotDestroy(ctx context.Context) resource.TestCheckF continue } - input := &docdb.DescribeDBClusterSnapshotsInput{ - DBClusterSnapshotIdentifier: aws.String(rs.Primary.ID), + _, err := tfdocdb.FindClusterSnapshotByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue } - output, err := conn.DescribeDBClusterSnapshotsWithContext(ctx, input) if err != nil { - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBClusterSnapshotNotFoundFault) { - continue - } return err } - if output != nil && len(output.DBClusterSnapshots) > 0 && output.DBClusterSnapshots[0] != nil && aws.StringValue(output.DBClusterSnapshots[0].DBClusterSnapshotIdentifier) == rs.Primary.ID { - return fmt.Errorf("DocumentDB Cluster Snapshot %q still exists", rs.Primary.ID) - } + return fmt.Errorf("DocumentDB Cluster Snapshot %s still exists", rs.Primary.ID) } return nil } } -func testAccCheckClusterSnapshotExists(ctx context.Context, resourceName string, dbClusterSnapshot *docdb.DBClusterSnapshot) resource.TestCheckFunc { +func testAccCheckClusterSnapshotExists(ctx context.Context, n string, v *docdb.DBClusterSnapshot) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] + rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("Not found: %s", resourceName) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set for %s", resourceName) + return fmt.Errorf("Not found: %s", n) } conn := acctest.Provider.Meta().(*conns.AWSClient).DocDBConn(ctx) - request := &docdb.DescribeDBClusterSnapshotsInput{ - DBClusterSnapshotIdentifier: aws.String(rs.Primary.ID), - } + output, err := tfdocdb.FindClusterSnapshotByID(ctx, conn, rs.Primary.ID) - response, err := conn.DescribeDBClusterSnapshotsWithContext(ctx, request) if err != nil { return err } - if response == nil || len(response.DBClusterSnapshots) == 0 || response.DBClusterSnapshots[0] == nil || aws.StringValue(response.DBClusterSnapshots[0].DBClusterSnapshotIdentifier) != rs.Primary.ID { - return fmt.Errorf("DocumentDB Cluster Snapshot %q not found", rs.Primary.ID) - } - - *dbClusterSnapshot = *response.DBClusterSnapshots[0] + *v = *output return nil } } func testAccClusterSnapshotConfig_basic(rName string) string { - return fmt.Sprintf(` -data "aws_availability_zones" "available" { - state = "available" - - filter { - name = "opt-in-status" - values = ["opt-in-not-required"] - } -} - -resource "aws_vpc" "test" { - cidr_block = "192.168.0.0/16" - - tags = { - Name = %q - } -} - -resource "aws_subnet" "test" { - count = 2 - - availability_zone = data.aws_availability_zones.available.names[count.index] - cidr_block = "192.168.${count.index}.0/24" - vpc_id = aws_vpc.test.id - - tags = { - Name = %q - } -} - + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 2), fmt.Sprintf(` resource "aws_docdb_subnet_group" "test" { - name = %q + name = %[1]q subnet_ids = aws_subnet.test[*].id } resource "aws_docdb_cluster" "test" { - cluster_identifier = %q + cluster_identifier = %[1]q db_subnet_group_name = aws_docdb_subnet_group.test.name - master_password = "barbarbarbar" - master_username = "foo" + master_password = "avoid-plaintext-passwords" + master_username = "tfacctest" skip_final_snapshot = true } resource "aws_docdb_cluster_snapshot" "test" { db_cluster_identifier = aws_docdb_cluster.test.id - db_cluster_snapshot_identifier = %q + db_cluster_snapshot_identifier = %[1]q } -`, rName, rName, rName, rName, rName) +`, rName)) } diff --git a/internal/service/docdb/cluster_test.go b/internal/service/docdb/cluster_test.go index 9f247ea5a66..8d92751cfb5 100644 --- a/internal/service/docdb/cluster_test.go +++ b/internal/service/docdb/cluster_test.go @@ -79,6 +79,7 @@ func TestAccDocDBCluster_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "skip_final_snapshot", "true"), resource.TestCheckNoResourceAttr(resourceName, "snapshot_identifier"), resource.TestCheckResourceAttr(resourceName, "storage_encrypted", "false"), + resource.TestCheckResourceAttr(resourceName, "storage_type", ""), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "vpc_security_group_ids.#", "1"), ), @@ -759,6 +760,9 @@ func TestAccDocDBCluster_GlobalClusterIdentifier_PrimarySecondaryClusters(t *tes } func TestAccDocDBCluster_updateEngineMajorVersion(t *testing.T) { + // https://docs.aws.amazon.com/documentdb/latest/developerguide/docdb-mvu.html. + acctest.Skip(t, "Amazon DocumentDB has identified an issue and is temporarily disallowing major version upgrades (MVU) in all regions.") + ctx := acctest.Context(t) var dbCluster docdb.DBCluster rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -803,6 +807,7 @@ func TestAccDocDBCluster_updateEngineMajorVersion(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "skip_final_snapshot", "true"), resource.TestCheckNoResourceAttr(resourceName, "snapshot_identifier"), resource.TestCheckResourceAttr(resourceName, "storage_encrypted", "false"), + resource.TestCheckResourceAttr(resourceName, "storage_type", ""), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "vpc_security_group_ids.#", "1"), ), @@ -832,6 +837,55 @@ func TestAccDocDBCluster_updateEngineMajorVersion(t *testing.T) { }) } +func TestAccDocDBCluster_storageType(t *testing.T) { + ctx := acctest.Context(t) + var dbCluster docdb.DBCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_docdb_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, docdb.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccClusterConfig_storageType(rName, "standard"), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &dbCluster), + resource.TestCheckResourceAttr(resourceName, "storage_type", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "allow_major_version_upgrade", + "apply_immediately", + "final_snapshot_identifier", + "master_password", + "skip_final_snapshot", + }, + }, + { + Config: testAccClusterConfig_storageType(rName, "iopt1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &dbCluster), + resource.TestCheckResourceAttr(resourceName, "storage_type", "iopt1"), + ), + }, + { + Config: testAccClusterConfig_storageType(rName, "standard"), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &dbCluster), + resource.TestCheckResourceAttr(resourceName, "storage_type", ""), + ), + }, + }, + }) +} + func testAccCheckClusterDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).DocDBConn(ctx) @@ -1347,3 +1401,23 @@ resource "aws_docdb_cluster_instance" "test" { } `, rName, engineVersion) } + +func testAccClusterConfig_storageType(rName, storageType string) string { + return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` +resource "aws_docdb_cluster" "test" { + availability_zones = [ + data.aws_availability_zones.available.names[0], + data.aws_availability_zones.available.names[1], + data.aws_availability_zones.available.names[2] + ] + + cluster_identifier = %[1]q + engine = "docdb" + master_password = "avoid-plaintext-passwords" + master_username = "tfacctest" + storage_type = %[2]q + apply_immediately = true + skip_final_snapshot = true +} +`, rName, storageType)) +} diff --git a/internal/service/docdb/consts.go b/internal/service/docdb/consts.go index 3b4099b476e..dadd4fa3bbd 100644 --- a/internal/service/docdb/consts.go +++ b/internal/service/docdb/consts.go @@ -24,3 +24,48 @@ func engine_Values() []string { const ( errCodeInvalidParameterValue = "InvalidParameterValue" ) + +const ( + clusterStatusAvailable = "available" + clusterStatusBackingUp = "backing-up" + clusterStatusCreating = "creating" + clusterStatusDeleting = "deleting" + clusterStatusMigrating = "migrating" + clusterStatusModifying = "modifying" + clusterStatusPreparingDataMigration = "preparing-data-migration" + clusterStatusResettingMasterCredentials = "resetting-master-credentials" + clusterStatusUpgrading = "upgrading" +) + +const ( + clusterSnapshotStatusAvailable = "available" + clusterSnapshotStatusCreating = "creating" +) + +const ( + eventSubscriptionStatusActive = "active" + eventSubscriptionStatusCreating = "creating" + eventSubscriptionStatusDeleting = "deleting" + eventSubscriptionStatusModifying = "modifying" +) + +const ( + globalClusterStatusAvailable = "available" + globalClusterStatusCreating = "creating" + globalClusterStatusDeleted = "deleted" + globalClusterStatusDeleting = "deleting" + globalClusterStatusModifying = "modifying" + globalClusterStatusUpgrading = "upgrading" +) + +const ( + storageTypeIOpt1 = "iopt1" + storageTypeStandard = "standard" +) + +func storageType_Values() []string { + return []string{ + storageTypeIOpt1, + storageTypeStandard, + } +} diff --git a/internal/service/docdb/engine_version_data_source.go b/internal/service/docdb/engine_version_data_source.go index 56bbb430501..05e07e818ad 100644 --- a/internal/service/docdb/engine_version_data_source.go +++ b/internal/service/docdb/engine_version_data_source.go @@ -5,7 +5,6 @@ package docdb import ( "context" - "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/docdb" @@ -13,6 +12,9 @@ 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/flex" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) // @SDKDataSource("aws_docdb_engine_version") @@ -23,53 +25,43 @@ func DataSourceEngineVersion() *schema.Resource { "engine": { Type: schema.TypeString, Optional: true, - Default: "docdb", + Default: engineDocDB, }, - "engine_description": { Type: schema.TypeString, Computed: true, }, - "exportable_log_types": { Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, Computed: true, - Set: schema.HashString, + Elem: &schema.Schema{Type: schema.TypeString}, }, - "parameter_group_family": { Type: schema.TypeString, Computed: true, Optional: true, }, - "preferred_versions": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, ConflictsWith: []string{"version"}, }, - "supports_log_exports_to_cloudwatch": { Type: schema.TypeBool, Computed: true, }, - "valid_upgrade_targets": { Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, Computed: true, - Set: schema.HashString, + Elem: &schema.Schema{Type: schema.TypeString}, }, - "version": { Type: schema.TypeString, Computed: true, Optional: true, ConflictsWith: []string{"preferred_versions"}, }, - "version_description": { Type: schema.TypeString, Computed: true, @@ -94,89 +86,89 @@ func dataSourceEngineVersionRead(ctx context.Context, d *schema.ResourceData, me if v, ok := d.GetOk("version"); ok { input.EngineVersion = aws.String(v.(string)) - } - - if _, ok := d.GetOk("version"); !ok { - if _, ok := d.GetOk("preferred_versions"); !ok { - if _, ok := d.GetOk("parameter_group_family"); !ok { - input.DefaultOnly = aws.Bool(true) - } + } else if _, ok := d.GetOk("preferred_versions"); !ok { + if _, ok := d.GetOk("parameter_group_family"); !ok { + input.DefaultOnly = aws.Bool(true) } } - log.Printf("[DEBUG] Reading DocumentDB engine versions: %v", input) - var engineVersions []*docdb.DBEngineVersion + var engineVersion *docdb.DBEngineVersion + var err error + if preferredVersions := flex.ExpandStringValueList(d.Get("preferred_versions").([]interface{})); len(preferredVersions) > 0 { + var engineVersions []*docdb.DBEngineVersion + + engineVersions, err = findEngineVersions(ctx, conn, input) + + if err == nil { + PreferredVersionLoop: + // Return the first matching version. + for _, preferredVersion := range preferredVersions { + for _, v := range engineVersions { + if preferredVersion == aws.StringValue(v.EngineVersion) { + engineVersion = v + break PreferredVersionLoop + } + } + } - err := conn.DescribeDBEngineVersionsPagesWithContext(ctx, input, func(resp *docdb.DescribeDBEngineVersionsOutput, lastPage bool) bool { - for _, engineVersion := range resp.DBEngineVersions { if engineVersion == nil { - continue + err = tfresource.NewEmptyResultError(input) } - - engineVersions = append(engineVersions, engineVersion) } - return !lastPage - }) + } else { + engineVersion, err = findEngineVersion(ctx, conn, input) + } if err != nil { - return sdkdiag.AppendErrorf(diags, "reading DocumentDB engine versions: %s", err) + return sdkdiag.AppendFromErr(diags, tfresource.SingularDataSourceFindError("DocumentDB Engine Version", err)) } - if len(engineVersions) == 0 { - return sdkdiag.AppendErrorf(diags, "no DocumentDB engine versions found") - } + d.SetId(aws.StringValue(engineVersion.EngineVersion)) + d.Set("engine", engineVersion.Engine) + d.Set("engine_description", engineVersion.DBEngineDescription) + d.Set("exportable_log_types", engineVersion.ExportableLogTypes) + d.Set("parameter_group_family", engineVersion.DBParameterGroupFamily) + d.Set("supports_log_exports_to_cloudwatch", engineVersion.SupportsLogExportsToCloudwatchLogs) + d.Set("valid_upgrade_targets", tfslices.ApplyToAll(engineVersion.ValidUpgradeTarget, func(v *docdb.UpgradeTarget) string { + return aws.StringValue(v.EngineVersion) + })) - // preferred versions - var found *docdb.DBEngineVersion - if l := d.Get("preferred_versions").([]interface{}); len(l) > 0 { - for _, elem := range l { - preferredVersion, ok := elem.(string) + d.Set("version", engineVersion.EngineVersion) + d.Set("version_description", engineVersion.DBEngineVersionDescription) - if !ok { - continue - } + return diags +} - for _, engineVersion := range engineVersions { - if preferredVersion == aws.StringValue(engineVersion.EngineVersion) { - found = engineVersion - break - } - } +func findEngineVersion(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBEngineVersionsInput) (*docdb.DBEngineVersion, error) { + output, err := findEngineVersions(ctx, conn, input) - if found != nil { - break - } - } + if err != nil { + return nil, err } - if found == nil && len(engineVersions) > 1 { - return sdkdiag.AppendErrorf(diags, "multiple DocumentDB engine versions (%v) match the criteria", engineVersions) - } + return tfresource.AssertSinglePtrResult(output) +} - if found == nil && len(engineVersions) == 1 { - found = engineVersions[0] - } +func findEngineVersions(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeDBEngineVersionsInput) ([]*docdb.DBEngineVersion, error) { + var output []*docdb.DBEngineVersion - if found == nil { - return sdkdiag.AppendErrorf(diags, "no DocumentDB engine versions match the criteria") - } + err := conn.DescribeDBEngineVersionsPagesWithContext(ctx, input, func(page *docdb.DescribeDBEngineVersionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - d.SetId(aws.StringValue(found.EngineVersion)) + for _, v := range page.DBEngineVersions { + if v != nil { + output = append(output, v) + } + } - d.Set("engine", found.Engine) - d.Set("engine_description", found.DBEngineDescription) - d.Set("exportable_log_types", found.ExportableLogTypes) - d.Set("parameter_group_family", found.DBParameterGroupFamily) - d.Set("supports_log_exports_to_cloudwatch", found.SupportsLogExportsToCloudwatchLogs) + return !lastPage + }) - var upgradeTargets []string - for _, ut := range found.ValidUpgradeTarget { - upgradeTargets = append(upgradeTargets, aws.StringValue(ut.EngineVersion)) + if err != nil { + return nil, err } - d.Set("valid_upgrade_targets", upgradeTargets) - d.Set("version", found.EngineVersion) - d.Set("version_description", found.DBEngineVersionDescription) - - return diags + return output, nil } diff --git a/internal/service/docdb/engine_version_data_source_test.go b/internal/service/docdb/engine_version_data_source_test.go index 30a09e3bc19..4d3c23d43f4 100644 --- a/internal/service/docdb/engine_version_data_source_test.go +++ b/internal/service/docdb/engine_version_data_source_test.go @@ -26,14 +26,12 @@ func TestAccDocDBEngineVersionDataSource_basic(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccEngineVersionPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, docdb.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccEngineVersionDataSourceConfig_basic(engine, version), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "engine", engine), resource.TestCheckResourceAttr(dataSourceName, "version", version), - resource.TestCheckResourceAttrSet(dataSourceName, "engine_description"), resource.TestMatchResourceAttr(dataSourceName, "exportable_log_types.#", regexache.MustCompile(`^[1-9][0-9]*`)), resource.TestCheckResourceAttrSet(dataSourceName, "parameter_group_family"), @@ -53,7 +51,6 @@ func TestAccDocDBEngineVersionDataSource_preferred(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccEngineVersionPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, docdb.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccEngineVersionDataSourceConfig_preferred(), @@ -73,7 +70,6 @@ func TestAccDocDBEngineVersionDataSource_defaultOnly(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccEngineVersionPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, docdb.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccEngineVersionDataSourceConfig_defaultOnly(), @@ -108,8 +104,8 @@ func testAccEngineVersionPreCheck(ctx context.Context, t *testing.T) { func testAccEngineVersionDataSourceConfig_basic(engine, version string) string { return fmt.Sprintf(` data "aws_docdb_engine_version" "test" { - engine = %q - version = %q + engine = %[1]q + version = %[2]q } `, engine, version) } diff --git a/internal/service/docdb/event_subscription.go b/internal/service/docdb/event_subscription.go index 98e009e5205..d34b862c0d0 100644 --- a/internal/service/docdb/event_subscription.go +++ b/internal/service/docdb/event_subscription.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go/service/docdb" "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/create" @@ -129,8 +130,8 @@ func resourceEventSubscriptionCreate(ctx context.Context, d *schema.ResourceData d.SetId(aws.StringValue(output.EventSubscription.CustSubscriptionId)) - if _, err := waitEventSubscriptionActive(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { - return diag.Errorf("waiting for DocumentDB Event Subscription (%s) to become active: %s", d.Id(), err) + if _, err := waitEventSubscriptionCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return diag.Errorf("waiting for DocumentDB Event Subscription (%s) create: %s", d.Id(), err) } return resourceEventSubscriptionRead(ctx, d, meta) @@ -139,7 +140,7 @@ func resourceEventSubscriptionCreate(ctx context.Context, d *schema.ResourceData func resourceEventSubscriptionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DocDBConn(ctx) - output, err := FindEventSubscriptionByID(ctx, conn, d.Id()) + output, err := FindEventSubscriptionByName(ctx, conn, d.Id()) if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] DocumentDB Event Subscription (%s) not found, removing from state", d.Id()) @@ -195,8 +196,8 @@ func resourceEventSubscriptionUpdate(ctx context.Context, d *schema.ResourceData return diag.Errorf("updating DocumentDB Event Subscription (%s): %s", d.Id(), err) } - if _, err := waitEventSubscriptionActive(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { - return diag.Errorf("waiting for DocumentDB Event Subscription (%s) to become active: %s", d.Id(), err) + if _, err := waitEventSubscriptionUpdated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { + return diag.Errorf("waiting for DocumentDB Event Subscription (%s) update: %s", d.Id(), err) } } @@ -266,3 +267,137 @@ func resourceEventSubscriptionDelete(ctx context.Context, d *schema.ResourceData return nil } + +func FindEventSubscriptionByName(ctx context.Context, conn *docdb.DocDB, name string) (*docdb.EventSubscription, error) { + input := &docdb.DescribeEventSubscriptionsInput{ + SubscriptionName: aws.String(name), + } + output, err := findEventSubscription(ctx, conn, input) + + if err != nil { + return nil, err + } + + // Eventual consistency check. + if aws.StringValue(output.CustSubscriptionId) != name { + return nil, &retry.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + +func findEventSubscription(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeEventSubscriptionsInput) (*docdb.EventSubscription, error) { + output, err := findEventSubscriptions(ctx, conn, input) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findEventSubscriptions(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeEventSubscriptionsInput) ([]*docdb.EventSubscription, error) { + var output []*docdb.EventSubscription + + err := conn.DescribeEventSubscriptionsPagesWithContext(ctx, input, func(page *docdb.DescribeEventSubscriptionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.EventSubscriptionsList { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, docdb.ErrCodeSubscriptionNotFoundFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func statusEventSubscription(ctx context.Context, conn *docdb.DocDB, name string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindEventSubscriptionByName(ctx, conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } +} + +func waitEventSubscriptionCreated(ctx context.Context, conn *docdb.DocDB, name string, timeout time.Duration) (*docdb.EventSubscription, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{eventSubscriptionStatusCreating}, + Target: []string{eventSubscriptionStatusActive}, + Refresh: statusEventSubscription(ctx, conn, name), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*docdb.EventSubscription); ok { + return output, err + } + + return nil, err +} + +func waitEventSubscriptionUpdated(ctx context.Context, conn *docdb.DocDB, name string, timeout time.Duration) (*docdb.EventSubscription, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{eventSubscriptionStatusModifying}, + Target: []string{eventSubscriptionStatusActive}, + Refresh: statusEventSubscription(ctx, conn, name), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*docdb.EventSubscription); ok { + return output, err + } + + return nil, err +} + +func waitEventSubscriptionDeleted(ctx context.Context, conn *docdb.DocDB, name string, timeout time.Duration) (*docdb.EventSubscription, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{eventSubscriptionStatusDeleting}, + Target: []string{}, + Refresh: statusEventSubscription(ctx, conn, name), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*docdb.EventSubscription); ok { + return output, err + } + + return nil, err +} diff --git a/internal/service/docdb/event_subscription_test.go b/internal/service/docdb/event_subscription_test.go index a015dc93c85..292a1bbf88f 100644 --- a/internal/service/docdb/event_subscription_test.go +++ b/internal/service/docdb/event_subscription_test.go @@ -245,7 +245,7 @@ func testAccCheckEventSubscriptionDestroy(ctx context.Context) resource.TestChec conn := acctest.Provider.Meta().(*conns.AWSClient).DocDBConn(ctx) - _, err := tfdocdb.FindEventSubscriptionByID(ctx, conn, rs.Primary.ID) + _, err := tfdocdb.FindEventSubscriptionByName(ctx, conn, rs.Primary.ID) if tfresource.NotFound(err) { continue @@ -275,7 +275,7 @@ func testAccCheckEventSubscriptionExists(ctx context.Context, n string, eventSub conn := acctest.Provider.Meta().(*conns.AWSClient).DocDBConn(ctx) - res, err := tfdocdb.FindEventSubscriptionByID(ctx, conn, rs.Primary.ID) + res, err := tfdocdb.FindEventSubscriptionByName(ctx, conn, rs.Primary.ID) if err != nil { return err diff --git a/internal/service/docdb/find.go b/internal/service/docdb/find.go deleted file mode 100644 index c7fe5849847..00000000000 --- a/internal/service/docdb/find.go +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package docdb - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/docdb" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-provider-aws/internal/tfresource" -) - -func findGlobalClusterByARN(ctx context.Context, conn *docdb.DocDB, dbClusterARN string) (*docdb.GlobalCluster, error) { - var globalCluster *docdb.GlobalCluster - - input := &docdb.DescribeGlobalClustersInput{ - Filters: []*docdb.Filter{ - { - Name: aws.String("db-cluster-id"), - Values: []*string{aws.String(dbClusterARN)}, - }, - }, - } - - err := conn.DescribeGlobalClustersPagesWithContext(ctx, input, func(page *docdb.DescribeGlobalClustersOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, gc := range page.GlobalClusters { - if gc == nil { - continue - } - - for _, globalClusterMember := range gc.GlobalClusterMembers { - if aws.StringValue(globalClusterMember.DBClusterArn) == dbClusterARN { - globalCluster = gc - return false - } - } - } - - return !lastPage - }) - - return globalCluster, err -} - -func findGlobalClusterIDByARN(ctx context.Context, conn *docdb.DocDB, arn string) string { - result, err := conn.DescribeDBClustersWithContext(ctx, &docdb.DescribeDBClustersInput{}) - if err != nil { - return "" - } - for _, cluster := range result.DBClusters { - if aws.StringValue(cluster.DBClusterArn) == arn { - return aws.StringValue(cluster.DBClusterIdentifier) - } - } - return "" -} - -func FindDBClusterSnapshotById(ctx context.Context, conn *docdb.DocDB, dBClusterSnapshotID string) (*docdb.DBClusterSnapshot, error) { - var dBClusterSnapshot *docdb.DBClusterSnapshot - - input := &docdb.DescribeDBClusterSnapshotsInput{ - DBClusterIdentifier: aws.String(dBClusterSnapshotID), - } - - err := conn.DescribeDBClusterSnapshotsPagesWithContext(ctx, input, func(page *docdb.DescribeDBClusterSnapshotsOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, dbcss := range page.DBClusterSnapshots { - if dbcss == nil { - continue - } - - if aws.StringValue(dbcss.DBClusterIdentifier) == dBClusterSnapshotID { - dBClusterSnapshot = dbcss - return false - } - } - - return !lastPage - }) - - return dBClusterSnapshot, err -} - -func FindGlobalClusterById(ctx context.Context, conn *docdb.DocDB, globalClusterID string) (*docdb.GlobalCluster, error) { - var globalCluster *docdb.GlobalCluster - - input := &docdb.DescribeGlobalClustersInput{ - GlobalClusterIdentifier: aws.String(globalClusterID), - } - - err := conn.DescribeGlobalClustersPagesWithContext(ctx, input, func(page *docdb.DescribeGlobalClustersOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, gc := range page.GlobalClusters { - if gc == nil { - continue - } - - if aws.StringValue(gc.GlobalClusterIdentifier) == globalClusterID { - globalCluster = gc - return false - } - } - - return !lastPage - }) - - return globalCluster, err -} - -func FindEventSubscriptionByID(ctx context.Context, conn *docdb.DocDB, id string) (*docdb.EventSubscription, error) { - var eventSubscription *docdb.EventSubscription - - input := &docdb.DescribeEventSubscriptionsInput{ - SubscriptionName: aws.String(id), - } - - err := conn.DescribeEventSubscriptionsPagesWithContext(ctx, input, func(page *docdb.DescribeEventSubscriptionsOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, es := range page.EventSubscriptionsList { - if es == nil { - continue - } - - if aws.StringValue(es.CustSubscriptionId) == id { - eventSubscription = es - return false - } - } - - return !lastPage - }) - - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeSubscriptionNotFoundFault) { - return nil, &retry.NotFoundError{ - LastError: err, - LastRequest: input, - } - } - - if err != nil { - return nil, err - } - - if eventSubscription == nil { - return nil, tfresource.NewEmptyResultError(input) - } - - return eventSubscription, nil -} diff --git a/internal/service/docdb/global_cluster.go b/internal/service/docdb/global_cluster.go index d64be95ff9a..7a4ecb59c07 100644 --- a/internal/service/docdb/global_cluster.go +++ b/internal/service/docdb/global_cluster.go @@ -5,7 +5,6 @@ package docdb import ( "context" - "fmt" "log" "time" @@ -17,7 +16,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "golang.org/x/exp/slices" ) // @SDKResource("aws_docdb_global_cluster") @@ -32,11 +33,10 @@ func ResourceGlobalCluster() *schema.Resource { StateContext: schema.ImportStatePassthroughContext, }, - //Timeouts will scale per number of resources in the cluster. Timeouts implemented on each resource action. Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(GlobalClusterCreateTimeout), - Update: schema.DefaultTimeout(GlobalClusterUpdateTimeout), - Delete: schema.DefaultTimeout(GlobalClusterDeleteTimeout), + Create: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), }, Schema: map[string]*schema.Schema{ @@ -59,9 +59,9 @@ func ResourceGlobalCluster() *schema.Resource { Optional: true, Computed: true, ForceNew: true, + ValidateFunc: validation.StringInSlice(engine_Values(), false), AtLeastOneOf: []string{"engine", "source_db_cluster_identifier"}, ConflictsWith: []string{"source_db_cluster_identifier"}, - ValidateFunc: validation.StringInSlice(engine_Values(), false), }, "engine_version": { Type: schema.TypeString, @@ -119,8 +119,9 @@ func ResourceGlobalCluster() *schema.Resource { func resourceGlobalClusterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DocDBConn(ctx) + globalClusterID := d.Get("global_cluster_identifier").(string) input := &docdb.CreateGlobalClusterInput{ - GlobalClusterIdentifier: aws.String(d.Get("global_cluster_identifier").(string)), + GlobalClusterIdentifier: aws.String(globalClusterID), } if v, ok := d.GetOk("database_name"); ok { @@ -148,14 +149,15 @@ func resourceGlobalClusterCreate(ctx context.Context, d *schema.ResourceData, me } output, err := conn.CreateGlobalClusterWithContext(ctx, input) + if err != nil { - return diag.Errorf("creating DocumentDB Global Cluster: %s", err) + return diag.Errorf("creating DocumentDB Global Cluster (%s): %s", globalClusterID, err) } d.SetId(aws.StringValue(output.GlobalCluster.GlobalClusterIdentifier)) - if err := waitForGlobalClusterCreation(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { - return diag.Errorf("waiting for DocumentDB Global Cluster (%s) availability: %s", d.Id(), err) + if _, err := waitGlobalClusterCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return diag.Errorf("waiting for DocumentDB Global Cluster (%s) create: %s", d.Id(), err) } return resourceGlobalClusterRead(ctx, d, meta) @@ -164,28 +166,16 @@ func resourceGlobalClusterCreate(ctx context.Context, d *schema.ResourceData, me func resourceGlobalClusterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DocDBConn(ctx) - globalCluster, err := FindGlobalClusterById(ctx, conn, d.Id()) + globalCluster, err := FindGlobalClusterByID(ctx, conn, d.Id()) - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] DocumentDB Global Cluster (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return diag.Errorf("reading DocumentDB Global Cluster: %s", err) - } - - if !d.IsNewResource() && globalCluster == nil { - log.Printf("[WARN] DocumentDB Global Cluster (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - - if !d.IsNewResource() && (aws.StringValue(globalCluster.Status) == GlobalClusterStatusDeleting || aws.StringValue(globalCluster.Status) == GlobalClusterStatusDeleted) { - log.Printf("[WARN] DocumentDB Global Cluster (%s) in deleted state (%s), removing from state", d.Id(), aws.StringValue(globalCluster.Status)) - d.SetId("") - return nil + return diag.Errorf("reading DocumentDB Global Cluster (%s): %s", d.Id(), err) } d.Set("arn", globalCluster.GlobalClusterArn) @@ -194,11 +184,9 @@ func resourceGlobalClusterRead(ctx context.Context, d *schema.ResourceData, meta d.Set("engine", globalCluster.Engine) d.Set("engine_version", globalCluster.EngineVersion) d.Set("global_cluster_identifier", globalCluster.GlobalClusterIdentifier) - if err := d.Set("global_cluster_members", flattenGlobalClusterMembers(globalCluster.GlobalClusterMembers)); err != nil { return diag.Errorf("setting global_cluster_members: %s", err) } - d.Set("global_cluster_resource_id", globalCluster.GlobalClusterResourceId) d.Set("storage_encrypted", globalCluster.StorageEncrypted) @@ -208,31 +196,64 @@ func resourceGlobalClusterRead(ctx context.Context, d *schema.ResourceData, meta func resourceGlobalClusterUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DocDBConn(ctx) - input := &docdb.ModifyGlobalClusterInput{ - DeletionProtection: aws.Bool(d.Get("deletion_protection").(bool)), - GlobalClusterIdentifier: aws.String(d.Id()), - } + if d.HasChange("deletion_protection") { + input := &docdb.ModifyGlobalClusterInput{ + DeletionProtection: aws.Bool(d.Get("deletion_protection").(bool)), + GlobalClusterIdentifier: aws.String(d.Id()), + } - log.Printf("[DEBUG] Updating DocumentDB Global Cluster (%s): %s", d.Id(), input) + _, err := conn.ModifyGlobalClusterWithContext(ctx, input) - if d.HasChange("engine_version") { - if err := resourceGlobalClusterUpgradeEngineVersion(ctx, d, conn); err != nil { - return diag.FromErr(err) + if tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) { + return nil } - } - _, err := conn.ModifyGlobalClusterWithContext(ctx, input) + if err != nil { + return diag.Errorf("updating DocumentDB Global Cluster: %s", err) + } - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) { - return nil + if _, err := waitGlobalClusterUpdated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { + return diag.Errorf("waiting for DocumentDB Global Cluster (%s) update: %s", d.Id(), err) + } } - if err != nil { - return diag.Errorf("updating DocumentDB Global Cluster: %s", err) - } + if d.HasChange("engine_version") { + engineVersion := d.Get("engine_version").(string) + + for _, tfMapRaw := range d.Get("global_cluster_members").(*schema.Set).List() { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + if clusterARN, ok := tfMap["db_cluster_arn"].(string); ok && clusterARN != "" { + cluster, err := findClusterByARN(ctx, conn, clusterARN) + + if err != nil { + return diag.Errorf("reading DocumentDB Cluster (%s): %s", clusterARN, err) + } - if err := waitForGlobalClusterUpdate(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { - return diag.Errorf("waiting for DocumentDB Global Cluster (%s) update: %s", d.Id(), err) + clusterID := aws.StringValue(cluster.DBClusterIdentifier) + input := &docdb.ModifyDBClusterInput{ + ApplyImmediately: aws.Bool(true), + DBClusterIdentifier: aws.String(clusterID), + EngineVersion: aws.String(engineVersion), + } + + _, err = tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (interface{}, error) { + return conn.ModifyDBClusterWithContext(ctx, input) + }, "InvalidParameterValue", "IAM role ARN value is invalid or does not include the required permissions") + + if err != nil { + return diag.Errorf("modifying DocumentDB Cluster (%s) engine version: %s", clusterID, err) + } + + if _, err := waitDBClusterAvailable(ctx, conn, clusterID, d.Timeout(schema.TimeoutUpdate)); err != nil { + return diag.Errorf("waiting for DocumentDB Cluster (%s) update: %s", clusterID, err) + } + } + } } return resourceGlobalClusterRead(ctx, d, meta) @@ -241,71 +262,188 @@ func resourceGlobalClusterUpdate(ctx context.Context, d *schema.ResourceData, me func resourceGlobalClusterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).DocDBConn(ctx) - for _, globalClusterMemberRaw := range d.Get("global_cluster_members").(*schema.Set).List() { - globalClusterMember, ok := globalClusterMemberRaw.(map[string]interface{}) + // Remove any members from the global cluster. + for _, tfMapRaw := range d.Get("global_cluster_members").(*schema.Set).List() { + tfMap, ok := tfMapRaw.(map[string]interface{}) + if !ok { continue } - dbClusterArn, ok := globalClusterMember["db_cluster_arn"].(string) - if !ok { - continue + if clusterARN, ok := tfMap["db_cluster_arn"].(string); ok && clusterARN != "" { + if err := removeClusterFromGlobalCluster(ctx, conn, clusterARN, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return diag.FromErr(err) + } } + } - input := &docdb.RemoveFromGlobalClusterInput{ - DbClusterIdentifier: aws.String(dbClusterArn), + log.Printf("[DEBUG] Deleting DocumentDB Global Cluster: %s", d.Id()) + _, err := tfresource.RetryWhenAWSErrMessageContains(ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { + return conn.DeleteGlobalClusterWithContext(ctx, &docdb.DeleteGlobalClusterInput{ GlobalClusterIdentifier: aws.String(d.Id()), + }) + }, docdb.ErrCodeInvalidGlobalClusterStateFault, "is not empty") + + if tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) { + return nil + } + + if err != nil { + return diag.Errorf("deleting DocumentDB Global Cluster (%s): %s", d.Id(), err) + } + + if _, err := waitGlobalClusterDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return diag.Errorf("waiting for DocumentDB Global Cluster (%s) delete: %s", d.Id(), err) + } + + return nil +} + +func FindGlobalClusterByID(ctx context.Context, conn *docdb.DocDB, id string) (*docdb.GlobalCluster, error) { + input := &docdb.DescribeGlobalClustersInput{ + GlobalClusterIdentifier: aws.String(id), + } + output, err := findGlobalCluster(ctx, conn, input, tfslices.PredicateTrue[*docdb.GlobalCluster]()) + + if err != nil { + return nil, err + } + + if status := aws.StringValue(output.Status); status == globalClusterStatusDeleted { + return nil, &retry.NotFoundError{ + Message: status, + LastRequest: input, } + } - _, err := conn.RemoveFromGlobalClusterWithContext(ctx, input) - if tfawserr.ErrMessageContains(err, "InvalidParameterValue", "is not found in global cluster") { - continue + // Eventual consistency check. + if aws.StringValue(output.GlobalClusterIdentifier) != id { + return nil, &retry.NotFoundError{ + LastRequest: input, } - if err != nil { - return diag.Errorf("removing DocumentDB Cluster (%s) from Global Cluster (%s): %s", dbClusterArn, d.Id(), err) + } + + return output, nil +} + +func findGlobalClusterByClusterARN(ctx context.Context, conn *docdb.DocDB, arn string) (*docdb.GlobalCluster, error) { + input := &docdb.DescribeGlobalClustersInput{} + + return findGlobalCluster(ctx, conn, input, func(v *docdb.GlobalCluster) bool { + return slices.ContainsFunc(v.GlobalClusterMembers, func(v *docdb.GlobalClusterMember) bool { + return aws.StringValue(v.DBClusterArn) == arn + }) + }) +} + +func findGlobalCluster(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeGlobalClustersInput, filter tfslices.Predicate[*docdb.GlobalCluster]) (*docdb.GlobalCluster, error) { + output, err := findGlobalClusters(ctx, conn, input, filter) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findGlobalClusters(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeGlobalClustersInput, filter tfslices.Predicate[*docdb.GlobalCluster]) ([]*docdb.GlobalCluster, error) { + var output []*docdb.GlobalCluster + + err := conn.DescribeGlobalClustersPagesWithContext(ctx, input, func(page *docdb.DescribeGlobalClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - if err := waitForGlobalClusterRemoval(ctx, conn, dbClusterArn, d.Timeout(schema.TimeoutDelete)); err != nil { - return diag.Errorf("waiting for DocumentDB Cluster (%s) removal from DocumentDB Global Cluster (%s): %s", dbClusterArn, d.Id(), err) + for _, v := range page.GlobalClusters { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, } } - input := &docdb.DeleteGlobalClusterInput{ - GlobalClusterIdentifier: aws.String(d.Id()), + if err != nil { + return nil, err } - // Allow for eventual consistency - err := retry.RetryContext(ctx, d.Timeout(schema.TimeoutCreate), func() *retry.RetryError { - _, err := conn.DeleteGlobalClusterWithContext(ctx, input) + return output, nil +} + +func statusGlobalCluster(ctx context.Context, conn *docdb.DocDB, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindGlobalClusterByID(ctx, conn, id) - if tfawserr.ErrMessageContains(err, docdb.ErrCodeInvalidGlobalClusterStateFault, "is not empty") { - return retry.RetryableError(err) + if tfresource.NotFound(err) { + return nil, "", nil } if err != nil { - return retry.NonRetryableError(err) + return nil, "", err } - return nil - }) + return output, aws.StringValue(output.Status), nil + } +} - if tfresource.TimedOut(err) { - _, err = conn.DeleteGlobalClusterWithContext(ctx, input) +func waitGlobalClusterCreated(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.GlobalCluster, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{globalClusterStatusCreating}, + Target: []string{globalClusterStatusAvailable}, + Refresh: statusGlobalCluster(ctx, conn, id), + Timeout: timeout, } - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) { - return nil + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*docdb.GlobalCluster); ok { + return output, err } - if err != nil { - return diag.Errorf("deleting DocumentDB Global Cluster: %s", err) + return nil, err +} + +func waitGlobalClusterUpdated(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.GlobalCluster, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{globalClusterStatusModifying, globalClusterStatusUpgrading}, + Target: []string{globalClusterStatusAvailable}, + Refresh: statusGlobalCluster(ctx, conn, id), + Timeout: timeout, + Delay: 30 * time.Second, } - if err := WaitForGlobalClusterDeletion(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { - return diag.Errorf("waiting for DocumentDB Global Cluster (%s) deletion: %s", d.Id(), err) + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*docdb.GlobalCluster); ok { + return output, err } - return nil + return nil, err +} + +func waitGlobalClusterDeleted(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.GlobalCluster, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{globalClusterStatusAvailable, globalClusterStatusDeleting}, + Target: []string{}, + Refresh: statusGlobalCluster(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 1, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*docdb.GlobalCluster); ok { + return output, err + } + + return nil, err } func flattenGlobalClusterMembers(apiObjects []*docdb.GlobalClusterMember) []interface{} { @@ -326,56 +464,3 @@ func flattenGlobalClusterMembers(apiObjects []*docdb.GlobalClusterMember) []inte return tfList } - -// Updating major versions is not supported by documentDB -// To support minor version upgrades, we will upgrade all cluster members -func resourceGlobalClusterUpgradeEngineVersion(ctx context.Context, d *schema.ResourceData, conn *docdb.DocDB) error { - log.Printf("[DEBUG] Upgrading DocumentDB Global Cluster (%s) engine version: %s", d.Id(), d.Get("engine_version")) - err := resourceGlobalClusterUpgradeMinorEngineVersion(ctx, d.Get("global_cluster_members").(*schema.Set), d.Get("engine_version").(string), conn, d.Timeout(schema.TimeoutUpdate)) - if err != nil { - return err - } - globalCluster, err := FindGlobalClusterById(ctx, conn, d.Id()) - if err != nil { - return err - } - for _, clusterMember := range globalCluster.GlobalClusterMembers { - if _, err := waitDBClusterUpdated(ctx, conn, findGlobalClusterIDByARN(ctx, conn, aws.StringValue(clusterMember.DBClusterArn)), d.Timeout(schema.TimeoutUpdate)); err != nil { - return err - } - } - return nil -} - -func resourceGlobalClusterUpgradeMinorEngineVersion(ctx context.Context, clusterMembers *schema.Set, engineVersion string, conn *docdb.DocDB, timeout time.Duration) error { - for _, clusterMemberRaw := range clusterMembers.List() { - clusterMember := clusterMemberRaw.(map[string]interface{}) - if clusterMemberArn, ok := clusterMember["db_cluster_arn"]; ok && clusterMemberArn.(string) != "" { - modInput := &docdb.ModifyDBClusterInput{ - ApplyImmediately: aws.Bool(true), - DBClusterIdentifier: aws.String(clusterMemberArn.(string)), - EngineVersion: aws.String(engineVersion), - } - err := retry.RetryContext(ctx, timeout, func() *retry.RetryError { - _, err := conn.ModifyDBClusterWithContext(ctx, modInput) - if err != nil { - if tfawserr.ErrMessageContains(err, "InvalidParameterValue", "IAM role ARN value is invalid or does not include the required permissions") { - return retry.RetryableError(err) - } - return retry.NonRetryableError(err) - } - return nil - }) - if tfresource.TimedOut(err) { - _, err := conn.ModifyDBClusterWithContext(ctx, modInput) - if err != nil { - return err - } - } - if err != nil { - return fmt.Errorf("failed to update engine_version on global cluster member (%s): %w", clusterMemberArn, err) - } - } - } - return nil -} diff --git a/internal/service/docdb/global_cluster_test.go b/internal/service/docdb/global_cluster_test.go index 550564d550a..017634a6491 100644 --- a/internal/service/docdb/global_cluster_test.go +++ b/internal/service/docdb/global_cluster_test.go @@ -19,13 +19,13 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfdocdb "github.com/hashicorp/terraform-provider-aws/internal/service/docdb" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccDocDBGlobalCluster_basic(t *testing.T) { ctx := acctest.Context(t) - var globalCluster1 docdb.GlobalCluster - - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + var globalCluster docdb.GlobalCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_docdb_global_cluster.test" resource.ParallelTest(t, resource.TestCase{ @@ -37,7 +37,7 @@ func TestAccDocDBGlobalCluster_basic(t *testing.T) { { Config: testAccGlobalClusterConfig_basic(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster1), + testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster), //This is a rds arn acctest.CheckResourceAttrGlobalARN(resourceName, "arn", "rds", fmt.Sprintf("global-cluster:%s", rName)), resource.TestCheckResourceAttr(resourceName, "database_name", ""), @@ -60,8 +60,8 @@ func TestAccDocDBGlobalCluster_basic(t *testing.T) { func TestAccDocDBGlobalCluster_disappears(t *testing.T) { ctx := acctest.Context(t) - var globalCluster1 docdb.GlobalCluster - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + var globalCluster docdb.GlobalCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_docdb_global_cluster.test" resource.ParallelTest(t, resource.TestCase{ @@ -73,8 +73,8 @@ func TestAccDocDBGlobalCluster_disappears(t *testing.T) { { Config: testAccGlobalClusterConfig_basic(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster1), - testAccCheckGlobalClusterDisappears(ctx, &globalCluster1), + testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfdocdb.ResourceGlobalCluster(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -85,7 +85,7 @@ func TestAccDocDBGlobalCluster_disappears(t *testing.T) { func TestAccDocDBGlobalCluster_DatabaseName(t *testing.T) { ctx := acctest.Context(t) var globalCluster1, globalCluster2 docdb.GlobalCluster - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_docdb_global_cluster.test" resource.ParallelTest(t, resource.TestCase{ @@ -121,7 +121,7 @@ func TestAccDocDBGlobalCluster_DatabaseName(t *testing.T) { func TestAccDocDBGlobalCluster_DeletionProtection(t *testing.T) { ctx := acctest.Context(t) var globalCluster1, globalCluster2 docdb.GlobalCluster - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_docdb_global_cluster.test" resource.ParallelTest(t, resource.TestCase{ @@ -156,8 +156,8 @@ func TestAccDocDBGlobalCluster_DeletionProtection(t *testing.T) { func TestAccDocDBGlobalCluster_Engine(t *testing.T) { ctx := acctest.Context(t) - var globalCluster1 docdb.GlobalCluster - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + var globalCluster docdb.GlobalCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_docdb_global_cluster.test" resource.ParallelTest(t, resource.TestCase{ @@ -169,7 +169,7 @@ func TestAccDocDBGlobalCluster_Engine(t *testing.T) { { Config: testAccGlobalClusterConfig_engine(rName, "docdb"), Check: resource.ComposeTestCheckFunc( - testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster1), + testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster), resource.TestCheckResourceAttr(resourceName, "engine", "docdb"), ), }, @@ -184,8 +184,8 @@ func TestAccDocDBGlobalCluster_Engine(t *testing.T) { func TestAccDocDBGlobalCluster_EngineVersion(t *testing.T) { ctx := acctest.Context(t) - var globalCluster1 docdb.GlobalCluster - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + var globalCluster docdb.GlobalCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_docdb_global_cluster.test" resource.ParallelTest(t, resource.TestCase{ @@ -197,7 +197,7 @@ func TestAccDocDBGlobalCluster_EngineVersion(t *testing.T) { { Config: testAccGlobalClusterConfig_engineVersion(rName, "docdb", "4.0.0"), Check: resource.ComposeTestCheckFunc( - testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster1), + testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster), resource.TestCheckResourceAttr(resourceName, "engine_version", "4.0.0"), ), }, @@ -212,8 +212,8 @@ func TestAccDocDBGlobalCluster_EngineVersion(t *testing.T) { func TestAccDocDBGlobalCluster_SourceDBClusterIdentifier_basic(t *testing.T) { ctx := acctest.Context(t) - var globalCluster1 docdb.GlobalCluster - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + var globalCluster docdb.GlobalCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) clusterResourceName := "aws_docdb_cluster.test" resourceName := "aws_docdb_global_cluster.test" @@ -226,7 +226,7 @@ func TestAccDocDBGlobalCluster_SourceDBClusterIdentifier_basic(t *testing.T) { { Config: testAccGlobalClusterConfig_sourceDBIdentifier(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster1), + testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster), resource.TestCheckResourceAttrPair(resourceName, "source_db_cluster_identifier", clusterResourceName, "arn"), ), }, @@ -242,8 +242,8 @@ func TestAccDocDBGlobalCluster_SourceDBClusterIdentifier_basic(t *testing.T) { func TestAccDocDBGlobalCluster_SourceDBClusterIdentifier_storageEncrypted(t *testing.T) { ctx := acctest.Context(t) - var globalCluster1 docdb.GlobalCluster - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + var globalCluster docdb.GlobalCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) clusterResourceName := "aws_docdb_cluster.test" resourceName := "aws_docdb_global_cluster.test" @@ -256,7 +256,7 @@ func TestAccDocDBGlobalCluster_SourceDBClusterIdentifier_storageEncrypted(t *tes { Config: testAccGlobalClusterConfig_sourceDBIdentifierStorageEncrypted(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster1), + testAccCheckGlobalClusterExists(ctx, resourceName, &globalCluster), resource.TestCheckResourceAttrPair(resourceName, "source_db_cluster_identifier", clusterResourceName, "arn"), ), }, @@ -273,7 +273,7 @@ func TestAccDocDBGlobalCluster_SourceDBClusterIdentifier_storageEncrypted(t *tes func TestAccDocDBGlobalCluster_StorageEncrypted(t *testing.T) { ctx := acctest.Context(t) var globalCluster1, globalCluster2 docdb.GlobalCluster - rName := sdkacctest.RandomWithPrefix("tf-acc-test") + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_docdb_global_cluster.test" resource.ParallelTest(t, resource.TestCase{ @@ -306,33 +306,22 @@ func TestAccDocDBGlobalCluster_StorageEncrypted(t *testing.T) { }) } -func testAccCheckGlobalClusterExists(ctx context.Context, resourceName string, globalCluster *docdb.GlobalCluster) resource.TestCheckFunc { +func testAccCheckGlobalClusterExists(ctx context.Context, n string, v *docdb.GlobalCluster) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] + rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("Not found: %s", resourceName) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("no DocumentDB Global Cluster ID is set") + return fmt.Errorf("Not found: %s", n) } conn := acctest.Provider.Meta().(*conns.AWSClient).DocDBConn(ctx) - cluster, err := tfdocdb.FindGlobalClusterById(ctx, conn, rs.Primary.ID) + + output, err := tfdocdb.FindGlobalClusterByID(ctx, conn, rs.Primary.ID) if err != nil { return err } - if cluster == nil { - return fmt.Errorf("DocumentDB Global Cluster not found") - } - - if aws.StringValue(cluster.Status) != "available" { - return fmt.Errorf("DocumentDB Global Cluster (%s) exists in non-available (%s) state", rs.Primary.ID, aws.StringValue(cluster.Status)) - } - - *globalCluster = *cluster + *v = *output return nil } @@ -347,9 +336,9 @@ func testAccCheckGlobalClusterDestroy(ctx context.Context) resource.TestCheckFun continue } - globalCluster, err := tfdocdb.FindGlobalClusterById(ctx, conn, rs.Primary.ID) + _, err := tfdocdb.FindGlobalClusterByID(ctx, conn, rs.Primary.ID) - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) { + if tfresource.NotFound(err) { continue } @@ -357,35 +346,13 @@ func testAccCheckGlobalClusterDestroy(ctx context.Context) resource.TestCheckFun return err } - if globalCluster == nil { - continue - } - - return fmt.Errorf("DocumentDB Global Cluster (%s) still exists in non-deleted (%s) state", rs.Primary.ID, aws.StringValue(globalCluster.Status)) + return fmt.Errorf("DocumentDB Global Cluster %s still exists", rs.Primary.ID) } return nil } } -func testAccCheckGlobalClusterDisappears(ctx context.Context, globalCluster *docdb.GlobalCluster) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := acctest.Provider.Meta().(*conns.AWSClient).DocDBConn(ctx) - - input := &docdb.DeleteGlobalClusterInput{ - GlobalClusterIdentifier: globalCluster.GlobalClusterIdentifier, - } - - _, err := conn.DeleteGlobalClusterWithContext(ctx, input) - - if err != nil { - return err - } - - return tfdocdb.WaitForGlobalClusterDeletion(ctx, conn, aws.StringValue(globalCluster.GlobalClusterIdentifier), tfdocdb.GlobalClusterDeleteTimeout) - } -} - func testAccCheckGlobalClusterNotRecreated(i, j *docdb.GlobalCluster) resource.TestCheckFunc { return func(s *terraform.State) error { if aws.StringValue(i.GlobalClusterArn) != aws.StringValue(j.GlobalClusterArn) { @@ -427,7 +394,7 @@ func testAccGlobalClusterConfig_basic(rName string) string { return fmt.Sprintf(` resource "aws_docdb_global_cluster" "test" { engine = "docdb" - global_cluster_identifier = %q + global_cluster_identifier = %[1]q } `, rName) } @@ -436,8 +403,8 @@ func testAccGlobalClusterConfig_databaseName(rName, databaseName string) string return fmt.Sprintf(` resource "aws_docdb_global_cluster" "test" { engine = "docdb" - database_name = %q - global_cluster_identifier = %q + database_name = %[1]q + global_cluster_identifier = %[2]q } `, databaseName, rName) } @@ -446,17 +413,17 @@ func testAccGlobalClusterConfig_deletionProtection(rName string, deletionProtect return fmt.Sprintf(` resource "aws_docdb_global_cluster" "test" { engine = "docdb" - deletion_protection = %t - global_cluster_identifier = %q + deletion_protection = %[2]t + global_cluster_identifier = %[1]q } -`, deletionProtection, rName) +`, rName, deletionProtection) } func testAccGlobalClusterConfig_engine(rName, engine string) string { return fmt.Sprintf(` resource "aws_docdb_global_cluster" "test" { - engine = %q - global_cluster_identifier = %q + engine = %[1]q + global_cluster_identifier = %[2]q } `, engine, rName) } @@ -464,9 +431,9 @@ resource "aws_docdb_global_cluster" "test" { func testAccGlobalClusterConfig_engineVersion(rName, engine, engineVersion string) string { return fmt.Sprintf(` resource "aws_docdb_global_cluster" "test" { - engine = %q - engine_version = %q - global_cluster_identifier = %q + engine = %[1]q + engine_version = %[2]q + global_cluster_identifier = %[3]q } `, engine, engineVersion, rName) } @@ -523,9 +490,9 @@ resource "aws_docdb_global_cluster" "test" { func testAccGlobalClusterConfig_storageEncrypted(rName string, storageEncrypted bool) string { return fmt.Sprintf(` resource "aws_docdb_global_cluster" "test" { - global_cluster_identifier = %q + global_cluster_identifier = %[1]q engine = "docdb" - storage_encrypted = %t + storage_encrypted = %[2]t } `, rName, storageEncrypted) } diff --git a/internal/service/docdb/orderable_db_instance_data_source.go b/internal/service/docdb/orderable_db_instance_data_source.go index 91395002cd7..6c6d90675f9 100644 --- a/internal/service/docdb/orderable_db_instance_data_source.go +++ b/internal/service/docdb/orderable_db_instance_data_source.go @@ -12,6 +12,9 @@ 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/flex" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) // @SDKDataSource("aws_docdb_orderable_db_instance") @@ -24,39 +27,33 @@ func DataSourceOrderableDBInstance() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "engine": { Type: schema.TypeString, Optional: true, - Default: "docdb", + Default: engineDocDB, }, - "engine_version": { Type: schema.TypeString, Optional: true, Computed: true, }, - "instance_class": { Type: schema.TypeString, Optional: true, Computed: true, ConflictsWith: []string{"preferred_instance_classes"}, }, - "license_model": { Type: schema.TypeString, Optional: true, Default: "na", }, - "preferred_instance_classes": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, ConflictsWith: []string{"instance_class"}, }, - "vpc": { Type: schema.TypeBool, Optional: true, @@ -92,76 +89,78 @@ func dataSourceOrderableDBInstanceRead(ctx context.Context, d *schema.ResourceDa input.Vpc = aws.Bool(v.(bool)) } - var instanceClassResults []*docdb.OrderableDBInstanceOption - - err := conn.DescribeOrderableDBInstanceOptionsPagesWithContext(ctx, input, func(resp *docdb.DescribeOrderableDBInstanceOptionsOutput, lastPage bool) bool { - for _, instanceOption := range resp.OrderableDBInstanceOptions { - if instanceOption == nil { - continue + var orderableDBInstance *docdb.OrderableDBInstanceOption + var err error + if preferredInstanceClasses := flex.ExpandStringValueList(d.Get("preferred_instance_classes").([]interface{})); len(preferredInstanceClasses) > 0 { + var orderableDBInstances []*docdb.OrderableDBInstanceOption + + orderableDBInstances, err = findOrderableDBInstances(ctx, conn, input) + if err == nil { + PreferredInstanceClassLoop: + for _, preferredInstanceClass := range preferredInstanceClasses { + for _, v := range orderableDBInstances { + if preferredInstanceClass == aws.StringValue(v.DBInstanceClass) { + orderableDBInstance = v + break PreferredInstanceClassLoop + } + } } - instanceClassResults = append(instanceClassResults, instanceOption) + if orderableDBInstance == nil { + err = tfresource.NewEmptyResultError(input) + } } - return !lastPage - }) - - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading DocumentDB orderable DB instance options: %s", err) + } else { + orderableDBInstance, err = findOrderableDBInstance(ctx, conn, input) } - if len(instanceClassResults) == 0 { - return sdkdiag.AppendErrorf(diags, "no DocumentDB Orderable DB Instance options found matching criteria; try different search") + if err != nil { + return sdkdiag.AppendFromErr(diags, tfresource.SingularDataSourceFindError("DocumentDB Orderable DB Instance", err)) } - // preferred classes - var found *docdb.OrderableDBInstanceOption - if l := d.Get("preferred_instance_classes").([]interface{}); len(l) > 0 { - for _, elem := range l { - preferredInstanceClass, ok := elem.(string) + d.SetId(aws.StringValue(orderableDBInstance.DBInstanceClass)) + d.Set("availability_zones", tfslices.ApplyToAll(orderableDBInstance.AvailabilityZones, func(v *docdb.AvailabilityZone) string { + return aws.StringValue(v.Name) + })) + d.Set("engine", orderableDBInstance.Engine) + d.Set("engine_version", orderableDBInstance.EngineVersion) + d.Set("instance_class", orderableDBInstance.DBInstanceClass) + d.Set("license_model", orderableDBInstance.LicenseModel) + d.Set("vpc", orderableDBInstance.Vpc) - if !ok { - continue - } + return diags +} - for _, instanceClassResult := range instanceClassResults { - if preferredInstanceClass == aws.StringValue(instanceClassResult.DBInstanceClass) { - found = instanceClassResult - break - } - } +func findOrderableDBInstance(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeOrderableDBInstanceOptionsInput) (*docdb.OrderableDBInstanceOption, error) { + output, err := findOrderableDBInstances(ctx, conn, input) - if found != nil { - break - } - } + if err != nil { + return nil, err } - if found == nil && len(instanceClassResults) > 1 { - return sdkdiag.AppendErrorf(diags, "multiple DocumentDB DB Instance Classes (%v) match the criteria; try a different search", instanceClassResults) - } + return tfresource.AssertSinglePtrResult(output) +} - if found == nil && len(instanceClassResults) == 1 { - found = instanceClassResults[0] - } +func findOrderableDBInstances(ctx context.Context, conn *docdb.DocDB, input *docdb.DescribeOrderableDBInstanceOptionsInput) ([]*docdb.OrderableDBInstanceOption, error) { + var output []*docdb.OrderableDBInstanceOption - if found == nil { - return sdkdiag.AppendErrorf(diags, "no DocumentDB DB Instance Classes match the criteria; try a different search") - } + err := conn.DescribeOrderableDBInstanceOptionsPagesWithContext(ctx, input, func(page *docdb.DescribeOrderableDBInstanceOptionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - d.SetId(aws.StringValue(found.DBInstanceClass)) + for _, v := range page.OrderableDBInstanceOptions { + if v != nil { + output = append(output, v) + } + } - d.Set("instance_class", found.DBInstanceClass) + return !lastPage + }) - var availabilityZones []string - for _, az := range found.AvailabilityZones { - availabilityZones = append(availabilityZones, aws.StringValue(az.Name)) + if err != nil { + return nil, err } - d.Set("availability_zones", availabilityZones) - d.Set("engine", found.Engine) - d.Set("engine_version", found.EngineVersion) - d.Set("license_model", found.LicenseModel) - d.Set("vpc", found.Vpc) - - return diags + return output, nil } diff --git a/internal/service/docdb/orderable_db_instance_data_source_test.go b/internal/service/docdb/orderable_db_instance_data_source_test.go index f5e4088871a..e3f8cfd1385 100644 --- a/internal/service/docdb/orderable_db_instance_data_source_test.go +++ b/internal/service/docdb/orderable_db_instance_data_source_test.go @@ -27,11 +27,10 @@ func TestAccDocDBOrderableDBInstanceDataSource_basic(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckOrderableDBInstance(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, docdb.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccOrderableDBInstanceDataSourceConfig_basic(class, engine, engineVersion, license), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(dataSourceName, "instance_class", class), resource.TestCheckResourceAttr(dataSourceName, "engine", engine), resource.TestCheckResourceAttr(dataSourceName, "engine_version", engineVersion), @@ -54,7 +53,6 @@ func TestAccDocDBOrderableDBInstanceDataSource_preferred(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckOrderableDBInstance(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, docdb.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccOrderableDBInstanceDataSourceConfig_preferred(engine, engineVersion, license, preferredOption), @@ -90,10 +88,10 @@ func testAccPreCheckOrderableDBInstance(ctx context.Context, t *testing.T) { func testAccOrderableDBInstanceDataSourceConfig_basic(class, engine, version, license string) string { return fmt.Sprintf(` data "aws_docdb_orderable_db_instance" "test" { - instance_class = %q - engine = %q - engine_version = %q - license_model = %q + instance_class = %[1]q + engine = %[2]q + engine_version = %[3]q + license_model = %[4]q } `, class, engine, version, license) } @@ -101,13 +99,13 @@ data "aws_docdb_orderable_db_instance" "test" { func testAccOrderableDBInstanceDataSourceConfig_preferred(engine, version, license, preferredOption string) string { return fmt.Sprintf(` data "aws_docdb_orderable_db_instance" "test" { - engine = %q - engine_version = %q - license_model = %q + engine = %[1]q + engine_version = %[2]q + license_model = %[3]q preferred_instance_classes = [ "db.xyz.xlarge", - %q, + %[4]q, "db.t3.small", ] } diff --git a/internal/service/docdb/status.go b/internal/service/docdb/status.go deleted file mode 100644 index 1a268bd3b86..00000000000 --- a/internal/service/docdb/status.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package docdb - -import ( - "context" - "fmt" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/docdb" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-provider-aws/internal/tfresource" -) - -func statusGlobalClusterRefreshFunc(ctx context.Context, conn *docdb.DocDB, globalClusterID string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - globalCluster, err := FindGlobalClusterById(ctx, conn, globalClusterID) - - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeGlobalClusterNotFoundFault) || globalCluster == nil { - return nil, GlobalClusterStatusDeleted, nil - } - - if err != nil { - return nil, "", fmt.Errorf("reading DocumentDB Global Cluster (%s): %w", globalClusterID, err) - } - - return globalCluster, aws.StringValue(globalCluster.Status), nil - } -} - -func statusDBClusterSnapshotRefreshFunc(ctx context.Context, conn *docdb.DocDB, dBClusterSnapshotID string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - dBClusterSnapshot, err := FindDBClusterSnapshotById(ctx, conn, dBClusterSnapshotID) - - if tfawserr.ErrCodeEquals(err, docdb.ErrCodeDBClusterSnapshotNotFoundFault) || dBClusterSnapshot == nil { - return nil, DBClusterSnapshotStatusDeleted, nil - } - - if err != nil { - return nil, "", fmt.Errorf("reading DocumentDB Cluster Snapshot (%s): %w", dBClusterSnapshotID, err) - } - - return dBClusterSnapshot, aws.StringValue(dBClusterSnapshot.Status), nil - } -} - -func statusEventSubscription(ctx context.Context, conn *docdb.DocDB, id string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - output, err := FindEventSubscriptionByID(ctx, conn, id) - - if tfresource.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - return output, aws.StringValue(output.Status), nil - } -} diff --git a/internal/service/docdb/sweep.go b/internal/service/docdb/sweep.go index ab8569b58d9..3d7c5baf3bb 100644 --- a/internal/service/docdb/sweep.go +++ b/internal/service/docdb/sweep.go @@ -13,62 +13,62 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/sweep" "github.com/hashicorp/terraform-provider-aws/internal/sweep/awsv1" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func RegisterSweepers() { - resource.AddTestSweepers("aws_docdb_global_cluster", &resource.Sweeper{ - Name: "aws_docdb_global_cluster", - F: sweepGlobalClusters, - Dependencies: []string{ - "aws_docdb_cluster", - }, - }) - - resource.AddTestSweepers("aws_docdb_subnet_group", &resource.Sweeper{ - Name: "aws_docdb_subnet_group", - F: sweepDBSubnetGroups, + resource.AddTestSweepers("aws_docdb_cluster", &resource.Sweeper{ + Name: "aws_docdb_cluster", + F: sweepClusters, Dependencies: []string{ "aws_docdb_cluster_instance", }, }) - resource.AddTestSweepers("aws_docdb_event_subscription", &resource.Sweeper{ - Name: "aws_docdb_event_subscription", - F: sweepEventSubscriptions, + resource.AddTestSweepers("aws_docdb_cluster_instance", &resource.Sweeper{ + Name: "aws_docdb_cluster_instance", + F: sweepClusterInstances, }) - resource.AddTestSweepers("aws_docdb_cluster", &resource.Sweeper{ - Name: "aws_docdb_cluster", - F: sweepDBClusters, + resource.AddTestSweepers("aws_docdb_cluster_parameter_group", &resource.Sweeper{ + Name: "aws_docdb_cluster_parameter_group", + F: sweepClusterParameterGroups, Dependencies: []string{ - "aws_docdb_cluster_instance", - "aws_docdb_cluster_snapshot", + "aws_docdb_cluster", }, }) resource.AddTestSweepers("aws_docdb_cluster_snapshot", &resource.Sweeper{ Name: "aws_docdb_cluster_snapshot", - F: sweepDBClusterSnapshots, + F: sweepClusterSnapshots, Dependencies: []string{ - "aws_docdb_cluster_instance", + "aws_docdb_cluster", }, }) - resource.AddTestSweepers("aws_docdb_cluster_instance", &resource.Sweeper{ - Name: "aws_docdb_cluster_instance", - F: sweepDBInstances, + resource.AddTestSweepers("aws_docdb_event_subscription", &resource.Sweeper{ + Name: "aws_docdb_event_subscription", + F: sweepEventSubscriptions, }) - resource.AddTestSweepers("aws_docdb_cluster_parameter_group", &resource.Sweeper{ - Name: "aws_docdb_cluster_parameter_group", - F: sweepDBClusterParameterGroups, + resource.AddTestSweepers("aws_docdb_global_cluster", &resource.Sweeper{ + Name: "aws_docdb_global_cluster", + F: sweepGlobalClusters, Dependencies: []string{ - "aws_docdb_cluster_instance", + "aws_docdb_cluster", + }, + }) + + resource.AddTestSweepers("aws_docdb_subnet_group", &resource.Sweeper{ + Name: "aws_docdb_subnet_group", + F: sweepSubnetGroups, + Dependencies: []string{ + "aws_docdb_cluster", }, }) } -func sweepDBClusters(region string) error { +func sweepClusters(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) if err != nil { @@ -84,11 +84,22 @@ func sweepDBClusters(region string) error { } for _, v := range page.DBClusters { + arn := aws.StringValue(v.DBClusterArn) + id := aws.StringValue(v.DBClusterIdentifier) + r := ResourceCluster() d := r.Data(nil) - d.SetId(aws.StringValue(v.DBClusterIdentifier)) + d.SetId(id) d.Set("skip_final_snapshot", true) - if globalCluster, err := findGlobalClusterByARN(ctx, conn, aws.StringValue(v.DBClusterArn)); err == nil && globalCluster != nil { + + globalCluster, err := findGlobalClusterByClusterARN(ctx, conn, arn) + + if err != nil && !tfresource.NotFound(err) { + log.Printf("[WARN] Reading DocumentDB Cluster %s Global Cluster information: %s", id, err) + continue + } + + if globalCluster != nil && globalCluster.GlobalClusterIdentifier != nil { d.Set("global_cluster_identifier", globalCluster.GlobalClusterIdentifier) } @@ -116,37 +127,29 @@ func sweepDBClusters(region string) error { return nil } -func sweepDBClusterSnapshots(region string) error { +func sweepClusterSnapshots(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) - if err != nil { return fmt.Errorf("error getting client: %w", err) } - conn := client.DocDBConn(ctx) input := &docdb.DescribeDBClusterSnapshotsInput{} + sweepResources := make([]sweep.Sweepable, 0) - err = conn.DescribeDBClusterSnapshotsPagesWithContext(ctx, input, func(out *docdb.DescribeDBClusterSnapshotsOutput, lastPage bool) bool { - for _, dBClusterSnapshot := range out.DBClusterSnapshots { - name := aws.StringValue(dBClusterSnapshot.DBClusterSnapshotIdentifier) - input := &docdb.DeleteDBClusterSnapshotInput{ - DBClusterSnapshotIdentifier: dBClusterSnapshot.DBClusterSnapshotIdentifier, - } - - log.Printf("[INFO] Deleting DocumentDB Cluster Snapshot: %s", name) - - _, err := conn.DeleteDBClusterSnapshotWithContext(ctx, input) + err = conn.DescribeDBClusterSnapshotsPagesWithContext(ctx, input, func(page *docdb.DescribeDBClusterSnapshotsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - if err != nil { - log.Printf("[ERROR] Failed to delete DocumentDB Cluster Snapshot (%s): %s", name, err) - continue - } + for _, v := range page.DBClusterSnapshots { + r := ResourceClusterSnapshot() + d := r.Data(nil) + d.SetId(aws.StringValue(v.DBClusterSnapshotIdentifier)) - if err := WaitForDBClusterSnapshotDeletion(ctx, conn, name, DBClusterSnapshotDeleteTimeout); err != nil { - log.Printf("[ERROR] Failure while waiting for DocumentDB Cluster Snapshot (%s) to be deleted: %s", name, err) - } + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } + return !lastPage }) @@ -156,13 +159,19 @@ func sweepDBClusterSnapshots(region string) error { } if err != nil { - return fmt.Errorf("retrieving DocumentDB Cluster Snapshots: %w", err) + return fmt.Errorf("listing DocumentDB Cluster Snapshots (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("sweeping DocumentDB Cluster Snapshots (%s): %w", region, err) } return nil } -func sweepDBClusterParameterGroups(region string) error { +func sweepClusterParameterGroups(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) if err != nil { @@ -213,7 +222,7 @@ func sweepDBClusterParameterGroups(region string) error { return nil } -func sweepDBInstances(region string) error { +func sweepClusterInstances(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) if err != nil { @@ -260,34 +269,26 @@ func sweepDBInstances(region string) error { func sweepGlobalClusters(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) - if err != nil { return fmt.Errorf("error getting client: %w", err) } - conn := client.DocDBConn(ctx) input := &docdb.DescribeGlobalClustersInput{} + sweepResources := make([]sweep.Sweepable, 0) - err = conn.DescribeGlobalClustersPagesWithContext(ctx, input, func(out *docdb.DescribeGlobalClustersOutput, lastPage bool) bool { - for _, globalCluster := range out.GlobalClusters { - id := aws.StringValue(globalCluster.GlobalClusterIdentifier) - input := &docdb.DeleteGlobalClusterInput{ - GlobalClusterIdentifier: globalCluster.GlobalClusterIdentifier, - } - - log.Printf("[INFO] Deleting DocumentDB Global Cluster: %s", id) - - _, err := conn.DeleteGlobalClusterWithContext(ctx, input) + err = conn.DescribeGlobalClustersPagesWithContext(ctx, input, func(page *docdb.DescribeGlobalClustersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - if err != nil { - log.Printf("[ERROR] Failed to delete DocumentDB Global Cluster (%s): %s", id, err) - continue - } + for _, v := range page.GlobalClusters { + r := ResourceGlobalCluster() + d := r.Data(nil) + d.SetId(aws.StringValue(v.GlobalClusterIdentifier)) - if err := WaitForGlobalClusterDeletion(ctx, conn, id, GlobalClusterDeleteTimeout); err != nil { - log.Printf("[ERROR] Failure while waiting for DocumentDB Global Cluster (%s) to be deleted: %s", id, err) - } + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } + return !lastPage }) @@ -297,13 +298,19 @@ func sweepGlobalClusters(region string) error { } if err != nil { - return fmt.Errorf("retrieving DocumentDB Global Clusters: %w", err) + return fmt.Errorf("listing DocumentDB Global Clusters (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("sweeping DocumentDB Global Clusters (%s): %w", region, err) } return nil } -func sweepDBSubnetGroups(region string) error { +func sweepSubnetGroups(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) if err != nil { @@ -350,34 +357,26 @@ func sweepDBSubnetGroups(region string) error { func sweepEventSubscriptions(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) - if err != nil { return fmt.Errorf("error getting client: %w", err) } - conn := client.DocDBConn(ctx) input := &docdb.DescribeEventSubscriptionsInput{} + sweepResources := make([]sweep.Sweepable, 0) - err = conn.DescribeEventSubscriptionsPagesWithContext(ctx, input, func(out *docdb.DescribeEventSubscriptionsOutput, lastPage bool) bool { - for _, eventSubscription := range out.EventSubscriptionsList { - id := aws.StringValue(eventSubscription.CustSubscriptionId) - input := &docdb.DeleteEventSubscriptionInput{ - SubscriptionName: eventSubscription.CustSubscriptionId, - } - - log.Printf("[INFO] Deleting DocumentDB Event Subscription: %s", id) - - _, err := conn.DeleteEventSubscriptionWithContext(ctx, input) + err = conn.DescribeEventSubscriptionsPagesWithContext(ctx, input, func(page *docdb.DescribeEventSubscriptionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - if err != nil { - log.Printf("[ERROR] Failed to delete DocumentDB Event Subscription (%s): %s", id, err) - continue - } + for _, v := range page.EventSubscriptionsList { + r := ResourceEventSubscription() + d := r.Data(nil) + d.SetId(aws.StringValue(v.CustSubscriptionId)) - if _, err := waitEventSubscriptionDeleted(ctx, conn, id, EventSubscriptionDeleteTimeout); err != nil { - log.Printf("[ERROR] Failure while waiting for DocumentDB Event Subscription (%s) to be deleted: %s", id, err) - } + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } + return !lastPage }) @@ -387,7 +386,13 @@ func sweepEventSubscriptions(region string) error { } if err != nil { - return fmt.Errorf("retrieving DocumentDB Event Subscriptions: %w", err) + return fmt.Errorf("listing DocumentDB Event Subscriptions (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("sweeping DocumentDB Event Subscriptions (%s): %w", region, err) } return nil diff --git a/internal/service/docdb/wait.go b/internal/service/docdb/wait.go deleted file mode 100644 index 7066e119eb6..00000000000 --- a/internal/service/docdb/wait.go +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package docdb - -import ( - "context" - "fmt" - "log" - "time" - - "github.com/aws/aws-sdk-go/service/docdb" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-provider-aws/internal/tfresource" -) - -const ( - DBClusterSnapshotDeleteTimeout = 5 * time.Minute - EventSubscriptionDeleteTimeout = 5 * time.Minute - GlobalClusterCreateTimeout = 5 * time.Minute - GlobalClusterDeleteTimeout = 5 * time.Minute - GlobalClusterUpdateTimeout = 5 * time.Minute -) - -const ( - DBClusterSnapshotStatusAvailable = "available" - DBClusterSnapshotStatusDeleted = "deleted" - DBClusterSnapshotStatusDeleting = "deleting" - GlobalClusterStatusAvailable = "available" - GlobalClusterStatusCreating = "creating" - GlobalClusterStatusDeleted = "deleted" - GlobalClusterStatusDeleting = "deleting" - GlobalClusterStatusModifying = "modifying" - GlobalClusterStatusUpgrading = "upgrading" -) - -func waitForGlobalClusterCreation(ctx context.Context, conn *docdb.DocDB, globalClusterID string, timeout time.Duration) error { - stateConf := &retry.StateChangeConf{ - Pending: []string{GlobalClusterStatusCreating}, - Target: []string{GlobalClusterStatusAvailable}, - Refresh: statusGlobalClusterRefreshFunc(ctx, conn, globalClusterID), - Timeout: timeout, - } - - log.Printf("[DEBUG] Waiting for DocumentDB Global Cluster (%s) availability", globalClusterID) - _, err := stateConf.WaitForStateContext(ctx) - - return err -} - -func waitForGlobalClusterUpdate(ctx context.Context, conn *docdb.DocDB, globalClusterID string, timeout time.Duration) error { - stateConf := &retry.StateChangeConf{ - Pending: []string{GlobalClusterStatusModifying, GlobalClusterStatusUpgrading}, - Target: []string{GlobalClusterStatusAvailable}, - Refresh: statusGlobalClusterRefreshFunc(ctx, conn, globalClusterID), - Timeout: timeout, - Delay: 30 * time.Second, - } - - log.Printf("[DEBUG] Waiting for DocumentDB Global Cluster (%s) availability", globalClusterID) - _, err := stateConf.WaitForStateContext(ctx) - - return err -} - -func waitForGlobalClusterRemoval(ctx context.Context, conn *docdb.DocDB, dbClusterIdentifier string, timeout time.Duration) error { - var globalCluster *docdb.GlobalCluster - stillExistsErr := fmt.Errorf("DocumentDB Cluster still exists in DocumentDB Global Cluster") - - err := retry.RetryContext(ctx, timeout, func() *retry.RetryError { - var err error - - globalCluster, err = findGlobalClusterByARN(ctx, conn, dbClusterIdentifier) - - if err != nil { - return retry.NonRetryableError(err) - } - - if globalCluster != nil { - return retry.RetryableError(stillExistsErr) - } - - return nil - }) - - if tfresource.TimedOut(err) { - _, err = findGlobalClusterByARN(ctx, conn, dbClusterIdentifier) - } - - if err != nil { - return err - } - - if globalCluster != nil { - return stillExistsErr - } - - return nil -} - -func WaitForDBClusterSnapshotDeletion(ctx context.Context, conn *docdb.DocDB, dBClusterSnapshotID string, timeout time.Duration) error { - stateConf := &retry.StateChangeConf{ - Pending: []string{DBClusterSnapshotStatusAvailable, DBClusterSnapshotStatusDeleting}, - Target: []string{DBClusterSnapshotStatusDeleted}, - Refresh: statusDBClusterSnapshotRefreshFunc(ctx, conn, dBClusterSnapshotID), - Timeout: timeout, - NotFoundChecks: 1, - } - - log.Printf("[DEBUG] Waiting for DocumentDB Cluster Snapshot (%s) deletion", dBClusterSnapshotID) - _, err := stateConf.WaitForStateContext(ctx) - - if tfresource.NotFound(err) { - return nil - } - - return err -} - -func WaitForGlobalClusterDeletion(ctx context.Context, conn *docdb.DocDB, globalClusterID string, timeout time.Duration) error { - stateConf := &retry.StateChangeConf{ - Pending: []string{GlobalClusterStatusAvailable, GlobalClusterStatusDeleting}, - Target: []string{GlobalClusterStatusDeleted}, - Refresh: statusGlobalClusterRefreshFunc(ctx, conn, globalClusterID), - Timeout: timeout, - NotFoundChecks: 1, - } - - log.Printf("[DEBUG] Waiting for DocumentDB Global Cluster (%s) deletion", globalClusterID) - _, err := stateConf.WaitForStateContext(ctx) - - if tfresource.NotFound(err) { - return nil - } - - return err -} - -func waitEventSubscriptionActive(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.EventSubscription, error) { //nolint:unparam - stateConf := &retry.StateChangeConf{ - Pending: []string{"creating", "modifying"}, - Target: []string{"active"}, - Refresh: statusEventSubscription(ctx, conn, id), - Timeout: timeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*docdb.EventSubscription); ok { - return output, err - } - - return nil, err -} - -func waitEventSubscriptionDeleted(ctx context.Context, conn *docdb.DocDB, id string, timeout time.Duration) (*docdb.EventSubscription, error) { //nolint:unparam - stateConf := &retry.StateChangeConf{ - Pending: []string{"deleting"}, - Target: []string{}, - Refresh: statusEventSubscription(ctx, conn, id), - Timeout: timeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - - if output, ok := outputRaw.(*docdb.EventSubscription); ok { - return output, err - } - - return nil, err -} diff --git a/internal/service/neptune/cluster.go b/internal/service/neptune/cluster.go index 1c7ca3c41d6..b6fad51c39f 100644 --- a/internal/service/neptune/cluster.go +++ b/internal/service/neptune/cluster.go @@ -142,8 +142,8 @@ func ResourceCluster() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - Default: "neptune", - ValidateFunc: validEngine(), + Default: engineNeptune, + ValidateFunc: validation.StringInSlice(engine_Values(), false), }, "engine_version": { Type: schema.TypeString, @@ -725,7 +725,7 @@ func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta int conn := meta.(*conns.AWSClient).NeptuneConn(ctx) skipFinalSnapshot := d.Get("skip_final_snapshot").(bool) - input := neptune.DeleteDBClusterInput{ + input := &neptune.DeleteDBClusterInput{ DBClusterIdentifier: aws.String(d.Id()), SkipFinalSnapshot: aws.Bool(skipFinalSnapshot), } @@ -746,7 +746,7 @@ func resourceClusterDelete(ctx context.Context, d *schema.ResourceData, meta int log.Printf("[DEBUG] Deleting Neptune Cluster: %s", d.Id()) _, err := tfresource.RetryWhenAWSErrMessageContains(ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { - return conn.DeleteDBClusterWithContext(ctx, &input) + return conn.DeleteDBClusterWithContext(ctx, input) }, neptune.ErrCodeInvalidDBClusterStateFault, "is not currently in the available state") if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBClusterNotFoundFault) { diff --git a/internal/service/neptune/cluster_instance.go b/internal/service/neptune/cluster_instance.go index b9b2a67c710..3b7e607cbb4 100644 --- a/internal/service/neptune/cluster_instance.go +++ b/internal/service/neptune/cluster_instance.go @@ -16,6 +16,7 @@ import ( "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-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" @@ -87,8 +88,8 @@ func ResourceClusterInstance() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - Default: "neptune", - ValidateFunc: validEngine(), + Default: engineNeptune, + ValidateFunc: validation.StringInSlice(engine_Values(), false), }, "engine_version": { Type: schema.TypeString, diff --git a/internal/service/neptune/cluster_parameter_group.go b/internal/service/neptune/cluster_parameter_group.go index 7a4f6d707be..4085c20b4ff 100644 --- a/internal/service/neptune/cluster_parameter_group.go +++ b/internal/service/neptune/cluster_parameter_group.go @@ -23,10 +23,9 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" + "golang.org/x/exp/slices" ) -const clusterParameterGroupMaxParamsBulkEdit = 20 - // @SDKResource("aws_neptune_cluster_parameter_group", name="Cluster Parameter Group") // @Tags(identifierAttribute="arn") func ResourceClusterParameterGroup() *schema.Resource { @@ -123,9 +122,7 @@ func resourceClusterParameterGroupCreate(ctx context.Context, d *schema.Resource d.SetId(name) if v, ok := d.GetOk("parameter"); ok && v.(*schema.Set).Len() > 0 { - err := modifyClusterParameterGroupParameters(ctx, conn, d.Id(), expandParameters(v.(*schema.Set).List())) - - if err != nil { + if err := modifyClusterParameterGroupParameters(ctx, conn, d.Id(), expandParameters(v.(*schema.Set).List())); err != nil { return sdkdiag.AppendFromErr(diags, err) } } @@ -156,66 +153,42 @@ func resourceClusterParameterGroupRead(ctx context.Context, d *schema.ResourceDa d.Set("name", dbClusterParameterGroup.DBClusterParameterGroupName) d.Set("name_prefix", create.NamePrefixFromName(aws.StringValue(dbClusterParameterGroup.DBClusterParameterGroupName))) - // Only include user customized parameters as there's hundreds of system/default ones + // Only include user customized parameters as there's hundreds of system/default ones. input := &neptune.DescribeDBClusterParametersInput{ DBClusterParameterGroupName: aws.String(d.Id()), Source: aws.String("user"), } - var parameters []*neptune.Parameter - - err = conn.DescribeDBClusterParametersPagesWithContext(ctx, input, func(page *neptune.DescribeDBClusterParametersOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - for _, v := range page.Parameters { - if v != nil { - parameters = append(parameters, v) - } - } - - return !lastPage - }) + parameters, err := findDBClusterParameters(ctx, conn, input, tfslices.PredicateTrue[*neptune.Parameter]()) if err != nil { - return sdkdiag.AppendErrorf(diags, "reading Neptune Cluster Parameter Group (%s) parameters: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "reading Neptune Cluster Parameter Group (%s) user parameters: %s", d.Id(), err) } - // Add only system parameters that are set in the config + // Add only system parameters that are set in the config. p := d.Get("parameter") if p == nil { p = new(schema.Set) } - s := p.(*schema.Set) - configParameters := expandParameters(s.List()) + configParameters := expandParameters(p.(*schema.Set).List()) input = &neptune.DescribeDBClusterParametersInput{ DBClusterParameterGroupName: aws.String(d.Id()), Source: aws.String("engine-default"), } - err = conn.DescribeDBClusterParametersPagesWithContext(ctx, input, func(page *neptune.DescribeDBClusterParametersOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, v := range page.Parameters { - if v != nil { - for _, p := range configParameters { - if aws.StringValue(v.ParameterName) == aws.StringValue(p.ParameterName) { - parameters = append(parameters, v) - } - } - } - } - - return !lastPage + systemParameters, err := findDBClusterParameters(ctx, conn, input, func(v *neptune.Parameter) bool { + return slices.ContainsFunc(configParameters, func(p *neptune.Parameter) bool { + return aws.StringValue(v.ParameterName) == aws.StringValue(p.ParameterName) + }) }) if err != nil { - return sdkdiag.AppendErrorf(diags, "reading Neptune Cluster Parameter Group (%s) parameters: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "reading Neptune Cluster Parameter Group (%s) system parameters: %s", d.Id(), err) } + parameters = append(parameters, systemParameters...) + if err := d.Set("parameter", flattenParameters(parameters)); err != nil { return sdkdiag.AppendErrorf(diags, "setting parameter: %s", err) } @@ -229,15 +202,7 @@ func resourceClusterParameterGroupUpdate(ctx context.Context, d *schema.Resource if d.HasChange("parameter") { o, n := d.GetChange("parameter") - if o == nil { - o = new(schema.Set) - } - if n == nil { - n = new(schema.Set) - } - - os := o.(*schema.Set) - ns := n.(*schema.Set) + os, ns := o.(*schema.Set), n.(*schema.Set) if parameters := expandParameters(ns.Difference(os).List()); len(parameters) > 0 { err := modifyClusterParameterGroupParameters(ctx, conn, d.Id(), parameters) @@ -272,6 +237,9 @@ func resourceClusterParameterGroupDelete(ctx context.Context, d *schema.Resource } func modifyClusterParameterGroupParameters(ctx context.Context, conn *neptune.Neptune, name string, parameters []*neptune.Parameter) error { + const ( + clusterParameterGroupMaxParamsBulkEdit = 20 + ) // We can only modify 20 parameters at a time, so chunk them until we've got them all. for _, chunk := range tfslices.Chunks(parameters, clusterParameterGroupMaxParamsBulkEdit) { input := &neptune.ModifyDBClusterParameterGroupInput{ @@ -349,3 +317,34 @@ func findDBClusterParameterGroups(ctx context.Context, conn *neptune.Neptune, in return output, nil } + +func findDBClusterParameters(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBClusterParametersInput, filter tfslices.Predicate[*neptune.Parameter]) ([]*neptune.Parameter, error) { + var output []*neptune.Parameter + + err := conn.DescribeDBClusterParametersPagesWithContext(ctx, input, func(page *neptune.DescribeDBClusterParametersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.Parameters { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBParameterGroupNotFoundFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} diff --git a/internal/service/neptune/cluster_snapshot.go b/internal/service/neptune/cluster_snapshot.go index fc3539b98ad..ef149d8a14a 100644 --- a/internal/service/neptune/cluster_snapshot.go +++ b/internal/service/neptune/cluster_snapshot.go @@ -186,34 +186,61 @@ func FindClusterSnapshotByID(ctx context.Context, conn *neptune.Neptune, id stri input := &neptune.DescribeDBClusterSnapshotsInput{ DBClusterSnapshotIdentifier: aws.String(id), } + output, err := findClusterSnapshot(ctx, conn, input) - output, err := conn.DescribeDBClusterSnapshotsWithContext(ctx, input) + if err != nil { + return nil, err + } - if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBClusterSnapshotNotFoundFault) { + // Eventual consistency check. + if aws.StringValue(output.DBClusterSnapshotIdentifier) != id { return nil, &retry.NotFoundError{ - LastError: err, LastRequest: input, } } + return output, nil +} + +func findClusterSnapshot(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBClusterSnapshotsInput) (*neptune.DBClusterSnapshot, error) { + output, err := findClusterSnapshots(ctx, conn, input) + if err != nil { return nil, err } - if output == nil || len(output.DBClusterSnapshots) == 0 || output.DBClusterSnapshots[0] == nil { - return nil, tfresource.NewEmptyResultError(input) - } + return tfresource.AssertSinglePtrResult(output) +} - dbClusterSnapshot := output.DBClusterSnapshots[0] +func findClusterSnapshots(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBClusterSnapshotsInput) ([]*neptune.DBClusterSnapshot, error) { + var output []*neptune.DBClusterSnapshot - // Eventual consistency check. - if aws.StringValue(dbClusterSnapshot.DBClusterSnapshotIdentifier) != id { + err := conn.DescribeDBClusterSnapshotsPagesWithContext(ctx, input, func(page *neptune.DescribeDBClusterSnapshotsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DBClusterSnapshots { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBClusterSnapshotNotFoundFault) { return nil, &retry.NotFoundError{ + LastError: err, LastRequest: input, } } - return dbClusterSnapshot, nil + if err != nil { + return nil, err + } + + return output, nil } func statusClusterSnapshot(ctx context.Context, conn *neptune.Neptune, id string) retry.StateRefreshFunc { @@ -234,8 +261,8 @@ func statusClusterSnapshot(ctx context.Context, conn *neptune.Neptune, id string func waitClusterSnapshotCreated(ctx context.Context, conn *neptune.Neptune, id string, timeout time.Duration) (*neptune.DBClusterSnapshot, error) { stateConf := &retry.StateChangeConf{ - Pending: []string{"creating"}, - Target: []string{"available"}, + Pending: []string{clusterSnapshotStatusCreating}, + Target: []string{clusterSnapshotStatusAvailable}, Refresh: statusClusterSnapshot(ctx, conn, id), Timeout: timeout, MinTimeout: 10 * time.Second, diff --git a/internal/service/neptune/consts.go b/internal/service/neptune/consts.go index e038c7096ad..f2f5f6c4146 100644 --- a/internal/service/neptune/consts.go +++ b/internal/service/neptune/consts.go @@ -11,6 +11,16 @@ const ( propagationTimeout = 2 * time.Minute ) +const ( + engineNeptune = "neptune" // nosemgrep:ci.neptune-in-const-name,ci.neptune-in-var-name +) + +func engine_Values() []string { + return []string{ + engineNeptune, + } +} + const ( clusterEndpointStatusAvailable = "available" clusterEndpointStatusCreating = "creating" @@ -18,6 +28,11 @@ const ( clusterEndpointStatusModifying = "modifying" ) +const ( + clusterSnapshotStatusAvailable = "available" + clusterSnapshotStatusCreating = "creating" +) + const ( clusterStatusAvailable = "available" clusterStatusBackingUp = "backing-up" diff --git a/internal/service/neptune/engine_version_data_source.go b/internal/service/neptune/engine_version_data_source.go index b01b3d57052..1a9a571d9ca 100644 --- a/internal/service/neptune/engine_version_data_source.go +++ b/internal/service/neptune/engine_version_data_source.go @@ -5,7 +5,6 @@ package neptune import ( "context" - "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/neptune" @@ -13,6 +12,9 @@ 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/flex" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) // @SDKDataSource("aws_neptune_engine_version") @@ -23,64 +25,52 @@ func DataSourceEngineVersion() *schema.Resource { "engine": { Type: schema.TypeString, Optional: true, - Default: "neptune", + Default: engineNeptune, }, - "engine_description": { Type: schema.TypeString, Computed: true, }, - "exportable_log_types": { Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeString}, Computed: true, }, - "parameter_group_family": { Type: schema.TypeString, Computed: true, Optional: true, }, - "preferred_versions": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, ConflictsWith: []string{"version"}, }, - "supported_timezones": { Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, Computed: true, - Set: schema.HashString, + Elem: &schema.Schema{Type: schema.TypeString}, }, - "supports_log_exports_to_cloudwatch": { Type: schema.TypeBool, Computed: true, }, - "supports_read_replica": { Type: schema.TypeBool, Computed: true, }, - "valid_upgrade_targets": { Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, Computed: true, - Set: schema.HashString, + Elem: &schema.Schema{Type: schema.TypeString}, }, - "version": { Type: schema.TypeString, Computed: true, Optional: true, ConflictsWith: []string{"preferred_versions"}, }, - "version_description": { Type: schema.TypeString, Computed: true, @@ -105,95 +95,91 @@ func dataSourceEngineVersionRead(ctx context.Context, d *schema.ResourceData, me if v, ok := d.GetOk("version"); ok { input.EngineVersion = aws.String(v.(string)) + } else if _, ok := d.GetOk("preferred_versions"); !ok { + input.DefaultOnly = aws.Bool(true) } - if _, ok := d.GetOk("version"); !ok { - if _, ok := d.GetOk("preferred_versions"); !ok { - input.DefaultOnly = aws.Bool(true) - } - } - - log.Printf("[DEBUG] Reading Neptune engine versions: %v", input) - var engineVersions []*neptune.DBEngineVersion + var engineVersion *neptune.DBEngineVersion + var err error + if preferredVersions := flex.ExpandStringValueList(d.Get("preferred_versions").([]interface{})); len(preferredVersions) > 0 { + var engineVersions []*neptune.DBEngineVersion + + engineVersions, err = findEngineVersions(ctx, conn, input) + + if err == nil { + PreferredVersionLoop: + // Return the first matching version. + for _, preferredVersion := range preferredVersions { + for _, v := range engineVersions { + if preferredVersion == aws.StringValue(v.EngineVersion) { + engineVersion = v + break PreferredVersionLoop + } + } + } - err := conn.DescribeDBEngineVersionsPagesWithContext(ctx, input, func(resp *neptune.DescribeDBEngineVersionsOutput, lastPage bool) bool { - for _, engineVersion := range resp.DBEngineVersions { if engineVersion == nil { - continue + err = tfresource.NewEmptyResultError(input) } - - engineVersions = append(engineVersions, engineVersion) } - return !lastPage - }) - - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading Neptune engine versions: %s", err) + } else { + engineVersion, err = findEngineVersion(ctx, conn, input) } - if len(engineVersions) == 0 { - return sdkdiag.AppendErrorf(diags, "no Neptune engine versions found") + if err != nil { + return sdkdiag.AppendFromErr(diags, tfresource.SingularDataSourceFindError("Neptune Engine Version", err)) } - // preferred versions - var found *neptune.DBEngineVersion - if l := d.Get("preferred_versions").([]interface{}); len(l) > 0 { - for _, elem := range l { - preferredVersion, ok := elem.(string) - - if !ok { - continue - } - - for _, engineVersion := range engineVersions { - if preferredVersion == aws.StringValue(engineVersion.EngineVersion) { - found = engineVersion - break - } - } + d.SetId(aws.StringValue(engineVersion.EngineVersion)) + d.Set("engine", engineVersion.Engine) + d.Set("engine_description", engineVersion.DBEngineDescription) + d.Set("exportable_log_types", aws.StringValueSlice(engineVersion.ExportableLogTypes)) + d.Set("parameter_group_family", engineVersion.DBParameterGroupFamily) + d.Set("supported_timezones", tfslices.ApplyToAll(engineVersion.SupportedTimezones, func(v *neptune.Timezone) string { + return aws.StringValue(v.TimezoneName) + })) + d.Set("supports_log_exports_to_cloudwatch", engineVersion.SupportsLogExportsToCloudwatchLogs) + d.Set("supports_read_replica", engineVersion.SupportsReadReplica) + d.Set("valid_upgrade_targets", tfslices.ApplyToAll(engineVersion.ValidUpgradeTarget, func(v *neptune.UpgradeTarget) string { + return aws.StringValue(v.EngineVersion) + })) + + d.Set("version", engineVersion.EngineVersion) + d.Set("version_description", engineVersion.DBEngineVersionDescription) - if found != nil { - break - } - } - } + return diags +} - if found == nil && len(engineVersions) > 1 { - return sdkdiag.AppendErrorf(diags, "multiple Neptune engine versions (%v) match the criteria", engineVersions) - } +func findEngineVersion(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBEngineVersionsInput) (*neptune.DBEngineVersion, error) { + output, err := findEngineVersions(ctx, conn, input) - if found == nil && len(engineVersions) == 1 { - found = engineVersions[0] + if err != nil { + return nil, err } - if found == nil { - return sdkdiag.AppendErrorf(diags, "no Neptune engine versions match the criteria") - } + return tfresource.AssertSinglePtrResult(output) +} - d.SetId(aws.StringValue(found.EngineVersion)) +func findEngineVersions(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBEngineVersionsInput) ([]*neptune.DBEngineVersion, error) { + var output []*neptune.DBEngineVersion - d.Set("engine", found.Engine) - d.Set("engine_description", found.DBEngineDescription) - d.Set("exportable_log_types", found.ExportableLogTypes) - d.Set("parameter_group_family", found.DBParameterGroupFamily) + err := conn.DescribeDBEngineVersionsPagesWithContext(ctx, input, func(page *neptune.DescribeDBEngineVersionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - var timezones []string - for _, tz := range found.SupportedTimezones { - timezones = append(timezones, aws.StringValue(tz.TimezoneName)) - } - d.Set("supported_timezones", timezones) + for _, v := range page.DBEngineVersions { + if v != nil { + output = append(output, v) + } + } - d.Set("supports_log_exports_to_cloudwatch", found.SupportsLogExportsToCloudwatchLogs) - d.Set("supports_read_replica", found.SupportsReadReplica) + return !lastPage + }) - var upgradeTargets []string - for _, ut := range found.ValidUpgradeTarget { - upgradeTargets = append(upgradeTargets, aws.StringValue(ut.EngineVersion)) + if err != nil { + return nil, err } - d.Set("valid_upgrade_targets", upgradeTargets) - - d.Set("version", found.EngineVersion) - d.Set("version_description", found.DBEngineVersionDescription) - return diags + return output, nil } diff --git a/internal/service/neptune/engine_version_data_source_test.go b/internal/service/neptune/engine_version_data_source_test.go index dcd464096a5..92dddc4ed94 100644 --- a/internal/service/neptune/engine_version_data_source_test.go +++ b/internal/service/neptune/engine_version_data_source_test.go @@ -25,7 +25,6 @@ func TestAccNeptuneEngineVersionDataSource_basic(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccEngineVersionPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, neptune.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccEngineVersionDataSourceConfig_basic(version), @@ -54,7 +53,6 @@ func TestAccNeptuneEngineVersionDataSource_preferred(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccEngineVersionPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, neptune.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccEngineVersionDataSourceConfig_preferred(), @@ -75,7 +73,6 @@ func TestAccNeptuneEngineVersionDataSource_defaultOnly(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccEngineVersionPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, neptune.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccEngineVersionDataSourceConfig_defaultOnly(), @@ -111,7 +108,7 @@ func testAccEngineVersionDataSourceConfig_basic(version string) string { return fmt.Sprintf(` data "aws_neptune_engine_version" "test" { engine = "neptune" - version = %q + version = %[1]q } `, version) } diff --git a/internal/service/neptune/flex.go b/internal/service/neptune/flex.go index 34ea166a95e..c8beb2c1563 100644 --- a/internal/service/neptune/flex.go +++ b/internal/service/neptune/flex.go @@ -8,13 +8,9 @@ import ( "github.com/aws/aws-sdk-go/service/neptune" ) -// Takes the result of flatmap.Expand for an array of parameters and -// returns Parameter API compatible objects func expandParameters(configured []interface{}) []*neptune.Parameter { parameters := make([]*neptune.Parameter, 0, len(configured)) - // Loop over our configured parameters and create - // an array of aws-sdk-go compatible objects for _, pRaw := range configured { data := pRaw.(map[string]interface{}) @@ -30,9 +26,9 @@ func expandParameters(configured []interface{}) []*neptune.Parameter { return parameters } -// Flattens an array of Parameters into a []map[string]interface{} func flattenParameters(list []*neptune.Parameter) []map[string]interface{} { result := make([]map[string]interface{}, 0, len(list)) + for _, i := range list { if i.ParameterValue != nil { result = append(result, map[string]interface{}{ @@ -42,5 +38,6 @@ func flattenParameters(list []*neptune.Parameter) []map[string]interface{} { }) } } + return result } diff --git a/internal/service/neptune/global_cluster.go b/internal/service/neptune/global_cluster.go index 20b457e1aaa..cce125159b2 100644 --- a/internal/service/neptune/global_cluster.go +++ b/internal/service/neptune/global_cluster.go @@ -14,6 +14,7 @@ import ( "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-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" @@ -56,7 +57,7 @@ func ResourceGlobalCluster() *schema.Resource { Computed: true, ForceNew: true, ExactlyOneOf: []string{"engine", "source_db_cluster_identifier"}, - ValidateFunc: validEngine(), + ValidateFunc: validation.StringInSlice(engine_Values(), false), }, "engine_version": { Type: schema.TypeString, @@ -159,13 +160,13 @@ func resourceGlobalClusterRead(ctx context.Context, d *schema.ResourceData, meta globalCluster, err := FindGlobalClusterByID(ctx, conn, d.Id()) if !d.IsNewResource() && tfresource.NotFound(err) { - log.Printf("[WARN] Neptune Cluster (%s) not found, removing from state", d.Id()) + log.Printf("[WARN] Neptune Global Cluster (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return diag.Errorf("reading Neptune Cluster (%s): %s", d.Id(), err) + return diag.Errorf("reading Neptune Global Cluster (%s): %s", d.Id(), err) } d.Set("arn", globalCluster.GlobalClusterArn) @@ -198,7 +199,7 @@ func resourceGlobalClusterUpdate(ctx context.Context, d *schema.ResourceData, me } if err != nil { - return diag.Errorf("updating neptune Global Cluster (%s): %s", d.Id(), err) + return diag.Errorf("updating Neptune Global Cluster (%s): %s", d.Id(), err) } if _, err := waitGlobalClusterUpdated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { diff --git a/internal/service/neptune/global_cluster_test.go b/internal/service/neptune/global_cluster_test.go index ad29854adfe..0dbb3be1076 100644 --- a/internal/service/neptune/global_cluster_test.go +++ b/internal/service/neptune/global_cluster_test.go @@ -318,10 +318,6 @@ func testAccCheckGlobalClusterExists(ctx context.Context, n string, v *neptune.G return fmt.Errorf("not found: %s", n) } - if rs.Primary.ID == "" { - return fmt.Errorf("No Neptune Global Cluster ID is set") - } - conn := acctest.Provider.Meta().(*conns.AWSClient).NeptuneConn(ctx) output, err := tfneptune.FindGlobalClusterByID(ctx, conn, rs.Primary.ID) diff --git a/internal/service/neptune/orderable_db_instance_data_source.go b/internal/service/neptune/orderable_db_instance_data_source.go index 59f9d61c7ef..1ed5031a327 100644 --- a/internal/service/neptune/orderable_db_instance_data_source.go +++ b/internal/service/neptune/orderable_db_instance_data_source.go @@ -5,7 +5,6 @@ package neptune import ( "context" - "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/neptune" @@ -13,6 +12,9 @@ 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/flex" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) // @SDKDataSource("aws_neptune_orderable_db_instance") @@ -25,109 +27,89 @@ func DataSourceOrderableDBInstance() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, - "engine": { Type: schema.TypeString, Optional: true, - Default: "neptune", + Default: engineNeptune, }, - "engine_version": { Type: schema.TypeString, Optional: true, Computed: true, }, - "instance_class": { Type: schema.TypeString, Optional: true, Computed: true, ConflictsWith: []string{"preferred_instance_classes"}, }, - "license_model": { Type: schema.TypeString, Optional: true, Default: "amazon-license", }, - "max_iops_per_db_instance": { Type: schema.TypeInt, Computed: true, }, - "max_iops_per_gib": { Type: schema.TypeFloat, Computed: true, }, - "max_storage_size": { Type: schema.TypeInt, Computed: true, }, - "min_iops_per_db_instance": { Type: schema.TypeInt, Computed: true, }, - "min_iops_per_gib": { Type: schema.TypeFloat, Computed: true, }, - "min_storage_size": { Type: schema.TypeInt, Computed: true, }, - "multi_az_capable": { Type: schema.TypeBool, Computed: true, }, - "preferred_instance_classes": { Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, ConflictsWith: []string{"instance_class"}, }, - "read_replica_capable": { Type: schema.TypeBool, Computed: true, }, - "storage_type": { Type: schema.TypeString, Computed: true, }, - "supports_enhanced_monitoring": { Type: schema.TypeBool, Computed: true, }, - "supports_iam_database_authentication": { Type: schema.TypeBool, Computed: true, }, - "supports_iops": { Type: schema.TypeBool, Computed: true, }, - "supports_performance_insights": { Type: schema.TypeBool, Computed: true, }, - "supports_storage_encryption": { Type: schema.TypeBool, Computed: true, }, - "vpc": { Type: schema.TypeBool, Optional: true, @@ -163,91 +145,92 @@ func dataSourceOrderableDBInstanceRead(ctx context.Context, d *schema.ResourceDa input.Vpc = aws.Bool(v.(bool)) } - log.Printf("[DEBUG] Reading Neptune Orderable DB Instance Options: %v", input) - - var instanceClassResults []*neptune.OrderableDBInstanceOption - err := conn.DescribeOrderableDBInstanceOptionsPagesWithContext(ctx, input, func(resp *neptune.DescribeOrderableDBInstanceOptionsOutput, lastPage bool) bool { - for _, instanceOption := range resp.OrderableDBInstanceOptions { - if instanceOption == nil { - continue + var orderableDBInstance *neptune.OrderableDBInstanceOption + var err error + if preferredInstanceClasses := flex.ExpandStringValueList(d.Get("preferred_instance_classes").([]interface{})); len(preferredInstanceClasses) > 0 { + var orderableDBInstances []*neptune.OrderableDBInstanceOption + + orderableDBInstances, err = findOrderableDBInstances(ctx, conn, input) + if err == nil { + PreferredInstanceClassLoop: + for _, preferredInstanceClass := range preferredInstanceClasses { + for _, v := range orderableDBInstances { + if preferredInstanceClass == aws.StringValue(v.DBInstanceClass) { + orderableDBInstance = v + break PreferredInstanceClassLoop + } + } } - instanceClassResults = append(instanceClassResults, instanceOption) + if orderableDBInstance == nil { + err = tfresource.NewEmptyResultError(input) + } } - return !lastPage - }) - - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading Neptune orderable DB instance options: %s", err) + } else { + orderableDBInstance, err = findOrderableDBInstance(ctx, conn, input) } - if len(instanceClassResults) == 0 { - return sdkdiag.AppendErrorf(diags, "no Neptune Orderable DB Instance options found matching criteria; try different search") + if err != nil { + return sdkdiag.AppendFromErr(diags, tfresource.SingularDataSourceFindError("Neptune Orderable DB Instance", err)) } - // preferred classes - var found *neptune.OrderableDBInstanceOption - if l := d.Get("preferred_instance_classes").([]interface{}); len(l) > 0 { - for _, elem := range l { - preferredInstanceClass, ok := elem.(string) + d.SetId(aws.StringValue(orderableDBInstance.DBInstanceClass)) + d.Set("availability_zones", tfslices.ApplyToAll(orderableDBInstance.AvailabilityZones, func(v *neptune.AvailabilityZone) string { + return aws.StringValue(v.Name) + })) + d.Set("engine", orderableDBInstance.Engine) + d.Set("engine_version", orderableDBInstance.EngineVersion) + d.Set("license_model", orderableDBInstance.LicenseModel) + d.Set("max_iops_per_db_instance", orderableDBInstance.MaxIopsPerDbInstance) + d.Set("max_iops_per_gib", orderableDBInstance.MaxIopsPerGib) + d.Set("max_storage_size", orderableDBInstance.MaxStorageSize) + d.Set("min_iops_per_db_instance", orderableDBInstance.MinIopsPerDbInstance) + d.Set("min_iops_per_gib", orderableDBInstance.MinIopsPerGib) + d.Set("min_storage_size", orderableDBInstance.MinStorageSize) + d.Set("multi_az_capable", orderableDBInstance.MultiAZCapable) + d.Set("instance_class", orderableDBInstance.DBInstanceClass) + d.Set("read_replica_capable", orderableDBInstance.ReadReplicaCapable) + d.Set("storage_type", orderableDBInstance.StorageType) + d.Set("supports_enhanced_monitoring", orderableDBInstance.SupportsEnhancedMonitoring) + d.Set("supports_iam_database_authentication", orderableDBInstance.SupportsIAMDatabaseAuthentication) + d.Set("supports_iops", orderableDBInstance.SupportsIops) + d.Set("supports_performance_insights", orderableDBInstance.SupportsPerformanceInsights) + d.Set("supports_storage_encryption", orderableDBInstance.SupportsStorageEncryption) + d.Set("vpc", orderableDBInstance.Vpc) - if !ok { - continue - } + return diags +} - for _, instanceClassResult := range instanceClassResults { - if preferredInstanceClass == aws.StringValue(instanceClassResult.DBInstanceClass) { - found = instanceClassResult - break - } - } +func findOrderableDBInstance(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeOrderableDBInstanceOptionsInput) (*neptune.OrderableDBInstanceOption, error) { + output, err := findOrderableDBInstances(ctx, conn, input) - if found != nil { - break - } - } + if err != nil { + return nil, err } - if found == nil && len(instanceClassResults) > 1 { - return sdkdiag.AppendErrorf(diags, "multiple Neptune DB Instance Classes (%v) match the criteria; try a different search", instanceClassResults) - } + return tfresource.AssertSinglePtrResult(output) +} - if found == nil && len(instanceClassResults) == 1 { - found = instanceClassResults[0] - } +func findOrderableDBInstances(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeOrderableDBInstanceOptionsInput) ([]*neptune.OrderableDBInstanceOption, error) { + var output []*neptune.OrderableDBInstanceOption - if found == nil { - return sdkdiag.AppendErrorf(diags, "no Neptune DB Instance Classes match the criteria; try a different search") - } + err := conn.DescribeOrderableDBInstanceOptionsPagesWithContext(ctx, input, func(page *neptune.DescribeOrderableDBInstanceOptionsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - d.SetId(aws.StringValue(found.DBInstanceClass)) + for _, v := range page.OrderableDBInstanceOptions { + if v != nil { + output = append(output, v) + } + } - d.Set("instance_class", found.DBInstanceClass) + return !lastPage + }) - var availabilityZones []string - for _, az := range found.AvailabilityZones { - availabilityZones = append(availabilityZones, aws.StringValue(az.Name)) + if err != nil { + return nil, err } - d.Set("availability_zones", availabilityZones) - - d.Set("engine", found.Engine) - d.Set("engine_version", found.EngineVersion) - d.Set("license_model", found.LicenseModel) - d.Set("max_iops_per_db_instance", found.MaxIopsPerDbInstance) - d.Set("max_iops_per_gib", found.MaxIopsPerGib) - d.Set("max_storage_size", found.MaxStorageSize) - d.Set("min_iops_per_db_instance", found.MinIopsPerDbInstance) - d.Set("min_iops_per_gib", found.MinIopsPerGib) - d.Set("min_storage_size", found.MinStorageSize) - d.Set("multi_az_capable", found.MultiAZCapable) - d.Set("read_replica_capable", found.ReadReplicaCapable) - d.Set("storage_type", found.StorageType) - d.Set("supports_enhanced_monitoring", found.SupportsEnhancedMonitoring) - d.Set("supports_iam_database_authentication", found.SupportsIAMDatabaseAuthentication) - d.Set("supports_iops", found.SupportsIops) - d.Set("supports_performance_insights", found.SupportsPerformanceInsights) - d.Set("supports_storage_encryption", found.SupportsStorageEncryption) - d.Set("vpc", found.Vpc) - return diags + return output, nil } diff --git a/internal/service/neptune/orderable_db_instance_data_source_test.go b/internal/service/neptune/orderable_db_instance_data_source_test.go index 3cd500ee83e..aedc916e392 100644 --- a/internal/service/neptune/orderable_db_instance_data_source_test.go +++ b/internal/service/neptune/orderable_db_instance_data_source_test.go @@ -27,7 +27,6 @@ func TestAccNeptuneOrderableDBInstanceDataSource_basic(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckOrderableDBInstance(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, neptune.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccOrderableDBInstanceDataSourceConfig_basic(class, engine, engineVersion, licenseModel), @@ -54,7 +53,6 @@ func TestAccNeptuneOrderableDBInstanceDataSource_preferred(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckOrderableDBInstance(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, neptune.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: nil, Steps: []resource.TestStep{ { Config: testAccOrderableDBInstanceDataSourceConfig_preferred(engine, engineVersion, licenseModel, preferredOption), @@ -90,10 +88,10 @@ func testAccPreCheckOrderableDBInstance(ctx context.Context, t *testing.T) { func testAccOrderableDBInstanceDataSourceConfig_basic(class, engine, version, license string) string { return fmt.Sprintf(` data "aws_neptune_orderable_db_instance" "test" { - instance_class = %q - engine = %q - engine_version = %q - license_model = %q + instance_class = %[1]q + engine = %[2]q + engine_version = %[3]q + license_model = %[4]q } `, class, engine, version, license) } @@ -101,13 +99,13 @@ data "aws_neptune_orderable_db_instance" "test" { func testAccOrderableDBInstanceDataSourceConfig_preferred(engine, version, license, preferredOption string) string { return fmt.Sprintf(` data "aws_neptune_orderable_db_instance" "test" { - engine = %q - engine_version = %q - license_model = %q + engine = %[1]q + engine_version = %[2]q + license_model = %[3]q preferred_instance_classes = [ "db.xyz.xlarge", - %q, + %[4]q, "db.t3.small", ] } diff --git a/internal/service/neptune/parameter_group.go b/internal/service/neptune/parameter_group.go index 634e4f17cc0..6b3466f4d2c 100644 --- a/internal/service/neptune/parameter_group.go +++ b/internal/service/neptune/parameter_group.go @@ -5,8 +5,8 @@ package neptune import ( "context" + "fmt" "log" - "strings" "time" "github.com/aws/aws-sdk-go/aws" @@ -17,16 +17,18 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + 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" "github.com/hashicorp/terraform-provider-aws/names" ) -// We can only modify 20 parameters at a time, so walk them until -// we've got them all. -const maxParams = 20 +const ( + dbParameterGroupMaxParamsBulkEdit = 20 +) // @SDKResource("aws_neptune_parameter_group", name="Parameter Group") // @Tags(identifierAttribute="arn") @@ -36,38 +38,54 @@ func ResourceParameterGroup() *schema.Resource { ReadWithoutTimeout: resourceParameterGroupRead, UpdateWithoutTimeout: resourceParameterGroupUpdate, DeleteWithoutTimeout: resourceParameterGroupDelete, + Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, + Schema: map[string]*schema.Schema{ "arn": { Type: schema.TypeString, Computed: true, }, - "name": { + "description": { Type: schema.TypeString, + Optional: true, ForceNew: true, - Required: true, - StateFunc: func(val interface{}) string { - return strings.ToLower(val.(string)) - }, + Default: "Managed by Terraform", }, "family": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "description": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Default: "Managed by Terraform", + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name_prefix"}, + ValidateFunc: validParamGroupName, + }, + "name_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name"}, + ValidateFunc: validParamGroupNamePrefix, }, "parameter": { Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "apply_method": { + Type: schema.TypeString, + Optional: true, + Default: neptune.ApplyMethodPendingReboot, + ValidateFunc: validation.StringInSlice(neptune.ApplyMethod_Values(), false), + }, "name": { Type: schema.TypeString, Required: true, @@ -76,15 +94,6 @@ func ResourceParameterGroup() *schema.Resource { Type: schema.TypeString, Required: true, }, - "apply_method": { - Type: schema.TypeString, - Optional: true, - Default: neptune.ApplyMethodPendingReboot, - ValidateFunc: validation.StringInSlice([]string{ - neptune.ApplyMethodImmediate, - neptune.ApplyMethodPendingReboot, - }, false), - }, }, }, }, @@ -100,71 +109,61 @@ func resourceParameterGroupCreate(ctx context.Context, d *schema.ResourceData, m var diags diag.Diagnostics conn := meta.(*conns.AWSClient).NeptuneConn(ctx) - createOpts := neptune.CreateDBParameterGroupInput{ - DBParameterGroupName: aws.String(d.Get("name").(string)), + name := create.Name(d.Get("name").(string), d.Get("name_prefix").(string)) + input := &neptune.CreateDBParameterGroupInput{ DBParameterGroupFamily: aws.String(d.Get("family").(string)), + DBParameterGroupName: aws.String(name), Description: aws.String(d.Get("description").(string)), Tags: getTagsIn(ctx), } - log.Printf("[DEBUG] Create Neptune Parameter Group: %#v", createOpts) - resp, err := conn.CreateDBParameterGroupWithContext(ctx, &createOpts) + output, err := conn.CreateDBParameterGroupWithContext(ctx, input) + if err != nil { - return sdkdiag.AppendErrorf(diags, "creating Neptune Parameter Group: %s", err) + return sdkdiag.AppendErrorf(diags, "creating Neptune Parameter Group (%s): %s", name, err) } - d.SetId(aws.StringValue(resp.DBParameterGroup.DBParameterGroupName)) - d.Set("arn", resp.DBParameterGroup.DBParameterGroupArn) - log.Printf("[INFO] Neptune Parameter Group ID: %s", d.Id()) + d.SetId(aws.StringValue(output.DBParameterGroup.DBParameterGroupName)) - return append(diags, resourceParameterGroupUpdate(ctx, d, meta)...) + if v, ok := d.GetOk("parameter"); ok && v.(*schema.Set).Len() > 0 { + if err := addDBParameterGroupParameters(ctx, conn, d.Id(), expandParameters(v.(*schema.Set).List())); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + } + + return append(diags, resourceParameterGroupRead(ctx, d, meta)...) } func resourceParameterGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).NeptuneConn(ctx) - describeOpts := neptune.DescribeDBParameterGroupsInput{ - DBParameterGroupName: aws.String(d.Id()), + dbParameterGroup, err := FindDBParameterGroupByName(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Neptune Parameter Group (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags } - describeResp, err := conn.DescribeDBParameterGroupsWithContext(ctx, &describeOpts) if err != nil { - if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBParameterGroupNotFoundFault) { - log.Printf("[WARN] Neptune Parameter Group (%s) not found, removing from state", d.Id()) - d.SetId("") - return diags - } return sdkdiag.AppendErrorf(diags, "reading Neptune Parameter Group (%s): %s", d.Id(), err) } - if describeResp == nil { - return sdkdiag.AppendErrorf(diags, "reading Neptune Parameter Group (%s): empty result", d.Id()) - } - - if len(describeResp.DBParameterGroups) != 1 || - aws.StringValue(describeResp.DBParameterGroups[0].DBParameterGroupName) != d.Id() { - return sdkdiag.AppendErrorf(diags, "reading Neptune Parameter Group (%s): no match", d.Id()) - } - - arn := aws.StringValue(describeResp.DBParameterGroups[0].DBParameterGroupArn) - d.Set("arn", arn) - d.Set("name", describeResp.DBParameterGroups[0].DBParameterGroupName) - d.Set("family", describeResp.DBParameterGroups[0].DBParameterGroupFamily) - d.Set("description", describeResp.DBParameterGroups[0].Description) + d.Set("arn", dbParameterGroup.DBParameterGroupArn) + d.Set("description", dbParameterGroup.Description) + d.Set("family", dbParameterGroup.DBParameterGroupFamily) + d.Set("name", dbParameterGroup.DBParameterGroupName) + d.Set("name_prefix", create.NamePrefixFromName(aws.StringValue(dbParameterGroup.DBParameterGroupName))) - // Only include user customized parameters as there's hundreds of system/default ones - describeParametersOpts := neptune.DescribeDBParametersInput{ + // Only include user customized parameters as there's hundreds of system/default ones. + input := &neptune.DescribeDBParametersInput{ DBParameterGroupName: aws.String(d.Id()), Source: aws.String("user"), } - var parameters []*neptune.Parameter - err = conn.DescribeDBParametersPagesWithContext(ctx, &describeParametersOpts, - func(describeParametersResp *neptune.DescribeDBParametersOutput, lastPage bool) bool { - parameters = append(parameters, describeParametersResp.Parameters...) - return !lastPage - }) + parameters, err := findDBParameters(ctx, conn, input) + if err != nil { return sdkdiag.AppendErrorf(diags, "reading Neptune Parameter Group (%s) parameters: %s", d.Id(), err) } @@ -182,110 +181,171 @@ func resourceParameterGroupUpdate(ctx context.Context, d *schema.ResourceData, m if d.HasChange("parameter") { o, n := d.GetChange("parameter") - if o == nil { - o = new(schema.Set) + os, ns := o.(*schema.Set), n.(*schema.Set) + add, del := ns.Difference(os).List(), os.Difference(ns).List() + + if len(del) > 0 { + if err := delDBParameterGroupParameters(ctx, conn, d.Id(), expandParameters(del)); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } } - if n == nil { - n = new(schema.Set) + + if len(add) > 0 { + if err := addDBParameterGroupParameters(ctx, conn, d.Id(), expandParameters(add)); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } } + } - os := o.(*schema.Set) - ns := n.(*schema.Set) + return append(diags, resourceParameterGroupRead(ctx, d, meta)...) +} - toRemove := expandParameters(os.Difference(ns).List()) +func resourceParameterGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).NeptuneConn(ctx) - log.Printf("[DEBUG] Parameters to remove: %#v", toRemove) + log.Printf("[DEBUG] Deleting Neptune Parameter Group: %s", d.Id()) + _, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, 3*time.Minute, func() (interface{}, error) { + return conn.DeleteDBParameterGroupWithContext(ctx, &neptune.DeleteDBParameterGroupInput{ + DBParameterGroupName: aws.String(d.Id()), + }) + }, neptune.ErrCodeInvalidDBParameterGroupStateFault) - toAdd := expandParameters(ns.Difference(os).List()) + if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBParameterGroupNotFoundFault) { + return diags + } - log.Printf("[DEBUG] Parameters to add: %#v", toAdd) + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting Neptune Parameter Group (%s): %s", d.Id(), err) + } - for len(toRemove) > 0 { - var paramsToModify []*neptune.Parameter - if len(toRemove) <= maxParams { - paramsToModify, toRemove = toRemove[:], nil - } else { - paramsToModify, toRemove = toRemove[:maxParams], toRemove[maxParams:] - } - resetOpts := neptune.ResetDBParameterGroupInput{ - DBParameterGroupName: aws.String(d.Get("name").(string)), - Parameters: paramsToModify, - } + return diags +} - log.Printf("[DEBUG] Reset Neptune Parameter Group: %s", resetOpts) - err := retry.RetryContext(ctx, 30*time.Second, func() *retry.RetryError { - _, err := conn.ResetDBParameterGroupWithContext(ctx, &resetOpts) - if err != nil { - if tfawserr.ErrMessageContains(err, "InvalidDBParameterGroupState", " has pending changes") { - return retry.RetryableError(err) - } - return retry.NonRetryableError(err) - } - return nil - }) - if tfresource.TimedOut(err) { - _, err = conn.ResetDBParameterGroupWithContext(ctx, &resetOpts) - } - if err != nil { - return sdkdiag.AppendErrorf(diags, "resetting Neptune Parameter Group: %s", err) - } +func addDBParameterGroupParameters(ctx context.Context, conn *neptune.Neptune, name string, parameters []*neptune.Parameter) error { // We can only modify 20 parameters at a time, so chunk them until we've got them all. + for _, chunk := range tfslices.Chunks(parameters, dbParameterGroupMaxParamsBulkEdit) { + input := &neptune.ModifyDBParameterGroupInput{ + DBParameterGroupName: aws.String(name), + Parameters: chunk, } - for len(toAdd) > 0 { - var paramsToModify []*neptune.Parameter - if len(toAdd) <= maxParams { - paramsToModify, toAdd = toAdd[:], nil - } else { - paramsToModify, toAdd = toAdd[:maxParams], toAdd[maxParams:] - } - modifyOpts := neptune.ModifyDBParameterGroupInput{ - DBParameterGroupName: aws.String(d.Get("name").(string)), - Parameters: paramsToModify, - } + _, err := conn.ModifyDBParameterGroupWithContext(ctx, input) - log.Printf("[DEBUG] Modify Neptune Parameter Group: %s", modifyOpts) - _, err := conn.ModifyDBParameterGroupWithContext(ctx, &modifyOpts) - if err != nil { - return sdkdiag.AppendErrorf(diags, "modifying Neptune Parameter Group: %s", err) - } + if err != nil { + return fmt.Errorf("modifying Neptune Parameter Group (%s): %w", name, err) } } - return append(diags, resourceParameterGroupRead(ctx, d, meta)...) + return nil } -func resourceParameterGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).NeptuneConn(ctx) +func delDBParameterGroupParameters(ctx context.Context, conn *neptune.Neptune, name string, parameters []*neptune.Parameter) error { // We can only modify 20 parameters at a time, so chunk them until we've got them all. + for _, chunk := range tfslices.Chunks(parameters, dbParameterGroupMaxParamsBulkEdit) { + input := &neptune.ResetDBParameterGroupInput{ + DBParameterGroupName: aws.String(name), + Parameters: chunk, + } + + _, err := tfresource.RetryWhenAWSErrMessageContains(ctx, 30*time.Second, func() (interface{}, error) { + return conn.ResetDBParameterGroupWithContext(ctx, input) + }, neptune.ErrCodeInvalidDBParameterGroupStateFault, "has pending changes") - deleteOpts := neptune.DeleteDBParameterGroupInput{ - DBParameterGroupName: aws.String(d.Id()), - } - err := retry.RetryContext(ctx, 3*time.Minute, func() *retry.RetryError { - _, err := conn.DeleteDBParameterGroupWithContext(ctx, &deleteOpts) if err != nil { - if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBParameterGroupNotFoundFault) { - return nil - } - if tfawserr.ErrCodeEquals(err, neptune.ErrCodeInvalidDBParameterGroupStateFault) { - return retry.RetryableError(err) + return fmt.Errorf("resetting Neptune Parameter Group (%s): %w", name, err) + } + } + + return nil +} + +func FindDBParameterGroupByName(ctx context.Context, conn *neptune.Neptune, name string) (*neptune.DBParameterGroup, error) { + input := &neptune.DescribeDBParameterGroupsInput{ + DBParameterGroupName: aws.String(name), + } + output, err := findDBParameterGroup(ctx, conn, input) + + if err != nil { + return nil, err + } + + // Eventual consistency check. + if aws.StringValue(output.DBParameterGroupName) != name { + return nil, &retry.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + +func findDBParameterGroup(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBParameterGroupsInput) (*neptune.DBParameterGroup, error) { + output, err := findDBParameterGroups(ctx, conn, input) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findDBParameterGroups(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBParameterGroupsInput) ([]*neptune.DBParameterGroup, error) { + var output []*neptune.DBParameterGroup + + err := conn.DescribeDBParameterGroupsPagesWithContext(ctx, input, func(page *neptune.DescribeDBParameterGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DBParameterGroups { + if v != nil { + output = append(output, v) } - return retry.NonRetryableError(err) } - return nil + + return !lastPage }) - if tfresource.TimedOut(err) { - _, err = conn.DeleteDBParameterGroupWithContext(ctx, &deleteOpts) + if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBParameterGroupNotFoundFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err } + return output, nil +} + +func findDBParameters(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBParametersInput) ([]*neptune.Parameter, error) { + var output []*neptune.Parameter + + err := conn.DescribeDBParametersPagesWithContext(ctx, input, func(page *neptune.DescribeDBParametersOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.Parameters { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBParameterGroupNotFoundFault) { - return diags + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } } if err != nil { - return sdkdiag.AppendErrorf(diags, "deleting Neptune Parameter Group: %s", err) + return nil, err } - return diags + return output, nil } diff --git a/internal/service/neptune/parameter_group_test.go b/internal/service/neptune/parameter_group_test.go index e33f7ea4335..cd20a65eb5c 100644 --- a/internal/service/neptune/parameter_group_test.go +++ b/internal/service/neptune/parameter_group_test.go @@ -9,14 +9,15 @@ import ( "testing" "github.com/YakDriver/regexache" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/neptune" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" 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" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfneptune "github.com/hashicorp/terraform-provider-aws/internal/service/neptune" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccNeptuneParameterGroup_basic(t *testing.T) { @@ -33,13 +34,14 @@ func TestAccNeptuneParameterGroup_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccParameterGroupConfig_required(rName), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( testAccCheckParameterGroupExists(ctx, resourceName, &v), testAccCheckParameterGroupAttributes(&v, rName), acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "rds", fmt.Sprintf("pg:%s", rName)), resource.TestCheckResourceAttr(resourceName, "description", "Managed by Terraform"), resource.TestCheckResourceAttr(resourceName, "family", "neptune1"), resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "name_prefix", ""), resource.TestCheckResourceAttr(resourceName, "parameter.#", "0"), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), ), @@ -53,6 +55,86 @@ func TestAccNeptuneParameterGroup_basic(t *testing.T) { }) } +func TestAccNeptuneParameterGroup_disappears(t *testing.T) { + ctx := acctest.Context(t) + var v neptune.DBParameterGroup + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_neptune_parameter_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, neptune.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckParameterGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccParameterGroupConfig_required(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(ctx, resourceName, &v), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfneptune.ResourceParameterGroup(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccNeptuneParameterGroup_nameGenerated(t *testing.T) { + ctx := acctest.Context(t) + var v neptune.DBParameterGroup + resourceName := "aws_neptune_parameter_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, neptune.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckParameterGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccParameterGroupConfig_nameGenerated(), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(ctx, resourceName, &v), + acctest.CheckResourceAttrNameGenerated(resourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "name_prefix", id.UniqueIdPrefix), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccNeptuneParameterGroup_namePrefix(t *testing.T) { + ctx := acctest.Context(t) + var v neptune.DBParameterGroup + resourceName := "aws_neptune_parameter_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, neptune.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckParameterGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccParameterGroupConfig_namePrefix("tf-acc-test-prefix-"), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterGroupExists(ctx, resourceName, &v), + acctest.CheckResourceAttrNameFromPrefix(resourceName, "name", "tf-acc-test-prefix-"), + resource.TestCheckResourceAttr(resourceName, "name_prefix", "tf-acc-test-prefix-"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccNeptuneParameterGroup_description(t *testing.T) { ctx := acctest.Context(t) var v neptune.DBParameterGroup @@ -144,7 +226,7 @@ func TestAccNeptuneParameterGroup_tags(t *testing.T) { CheckDestroy: testAccCheckParameterGroupDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccParameterGroupConfig_tagsSingle(rName, "key1", "value1"), + Config: testAccParameterGroupConfig_tags1(rName, "key1", "value1"), Check: resource.ComposeTestCheckFunc( testAccCheckParameterGroupExists(ctx, resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), @@ -157,20 +239,20 @@ func TestAccNeptuneParameterGroup_tags(t *testing.T) { ImportStateVerify: true, }, { - Config: testAccParameterGroupConfig_tagsSingle(rName, "key1", "value2"), + Config: testAccParameterGroupConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), Check: resource.ComposeTestCheckFunc( testAccCheckParameterGroupExists(ctx, resourceName, &v), - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.key1", "value2"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), ), }, { - Config: testAccParameterGroupConfig_tagsMultiple(rName, "key2", "value2", "key3", "value3"), + Config: testAccParameterGroupConfig_tags1(rName, "key2", "value2"), Check: resource.ComposeTestCheckFunc( testAccCheckParameterGroupExists(ctx, resourceName, &v), - resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), - resource.TestCheckResourceAttr(resourceName, "tags.key3", "value3"), ), }, }, @@ -186,35 +268,17 @@ func testAccCheckParameterGroupDestroy(ctx context.Context) resource.TestCheckFu continue } - // Try to find the Group - resp, err := conn.DescribeDBParameterGroupsWithContext(ctx, &neptune.DescribeDBParameterGroupsInput{ - DBParameterGroupName: aws.String(rs.Primary.ID), - }) + _, err := tfneptune.FindDBParameterGroupByName(ctx, conn, rs.Primary.ID) - if err != nil { - if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBParameterGroupNotFoundFault) { - return nil - } - return err + if tfresource.NotFound(err) { + continue } - if len(resp.DBParameterGroups) != 0 && aws.StringValue(resp.DBParameterGroups[0].DBParameterGroupName) == rs.Primary.ID { - return fmt.Errorf("DB Parameter Group still exists") + if err != nil { + return err } - } - - return nil - } -} - -func testAccCheckParameterGroupAttributes(v *neptune.DBParameterGroup, rName string) resource.TestCheckFunc { - return func(s *terraform.State) error { - if *v.DBParameterGroupName != rName { - return fmt.Errorf("bad name: %#v", v.DBParameterGroupName) - } - if *v.DBParameterGroupFamily != "neptune1" { - return fmt.Errorf("bad family: %#v", v.DBParameterGroupFamily) + return fmt.Errorf("Neptune Parameter Group %s still exists", rs.Primary.ID) } return nil @@ -228,28 +292,29 @@ func testAccCheckParameterGroupExists(ctx context.Context, n string, v *neptune. return fmt.Errorf("Not found: %s", n) } - if rs.Primary.ID == "" { - return fmt.Errorf("No Neptune Parameter Group ID is set") - } - conn := acctest.Provider.Meta().(*conns.AWSClient).NeptuneConn(ctx) - opts := neptune.DescribeDBParameterGroupsInput{ - DBParameterGroupName: aws.String(rs.Primary.ID), - } - - resp, err := conn.DescribeDBParameterGroupsWithContext(ctx, &opts) + output, err := tfneptune.FindDBParameterGroupByName(ctx, conn, rs.Primary.ID) if err != nil { return err } - if len(resp.DBParameterGroups) != 1 || - *resp.DBParameterGroups[0].DBParameterGroupName != rs.Primary.ID { - return fmt.Errorf("Neptune Parameter Group not found") + *v = *output + + return nil + } +} + +func testAccCheckParameterGroupAttributes(v *neptune.DBParameterGroup, rName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *v.DBParameterGroupName != rName { + return fmt.Errorf("bad name: %#v", v.DBParameterGroupName) } - *v = *resp.DBParameterGroups[0] + if *v.DBParameterGroupFamily != "neptune1" { + return fmt.Errorf("bad family: %#v", v.DBParameterGroupFamily) + } return nil } @@ -259,59 +324,76 @@ func testAccParameterGroupConfig_basic(rName, pName, pValue, pApplyMethod string return fmt.Sprintf(` resource "aws_neptune_parameter_group" "test" { family = "neptune1" - name = %q + name = %[1]q parameter { - apply_method = %q - name = %q - value = %q + apply_method = %[2]q + name = %[3]q + value = %[4]q } } `, rName, pApplyMethod, pName, pValue) } +func testAccParameterGroupConfig_nameGenerated() string { + return ` +resource "aws_neptune_parameter_group" "test" { + family = "neptune1" +} +` +} + +func testAccParameterGroupConfig_namePrefix(namePrefix string) string { + return fmt.Sprintf(` +resource "aws_neptune_parameter_group" "test" { + family = "neptune1" + name_prefix = %[1]q +} +`, namePrefix) +} + func testAccParameterGroupConfig_description(rName, description string) string { return fmt.Sprintf(` resource "aws_neptune_parameter_group" "test" { - description = %q + description = %[2]q family = "neptune1" - name = %q + name = %[1]q } -`, description, rName) +`, rName, description) } func testAccParameterGroupConfig_required(rName string) string { return fmt.Sprintf(` resource "aws_neptune_parameter_group" "test" { family = "neptune1" - name = %q + name = %[1]q } `, rName) } -func testAccParameterGroupConfig_tagsSingle(name, tKey, tValue string) string { +func testAccParameterGroupConfig_tags1(rName, tagKey1, tagValue1 string) string { return fmt.Sprintf(` resource "aws_neptune_parameter_group" "test" { family = "neptune1" - name = %q + name = %[1]q tags = { - %s = %q + %[2]q = %[3]q } } -`, name, tKey, tValue) +`, rName, tagKey1, tagValue1) } -func testAccParameterGroupConfig_tagsMultiple(name, tKey1, tValue1, tKey2, tValue2 string) string { +func testAccParameterGroupConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { return fmt.Sprintf(` resource "aws_neptune_parameter_group" "test" { family = "neptune1" - name = %q + name = %[1]q tags = { - %s = %q - %s = %q + %[2]q = %[3]q + %[4]q = %[5]q } } -`, name, tKey1, tValue1, tKey2, tValue2) +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) } diff --git a/internal/service/neptune/subnet_group.go b/internal/service/neptune/subnet_group.go index be08ae2bf47..359047d7abd 100644 --- a/internal/service/neptune/subnet_group.go +++ b/internal/service/neptune/subnet_group.go @@ -174,32 +174,59 @@ func FindSubnetGroupByName(ctx context.Context, conn *neptune.Neptune, name stri input := &neptune.DescribeDBSubnetGroupsInput{ DBSubnetGroupName: aws.String(name), } + output, err := findDBSubnetGroup(ctx, conn, input) - output, err := conn.DescribeDBSubnetGroupsWithContext(ctx, input) + if err != nil { + return nil, err + } - if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBSubnetGroupNotFoundFault) { + // Eventual consistency check. + if aws.StringValue(output.DBSubnetGroupName) != name { return nil, &retry.NotFoundError{ - LastError: err, LastRequest: input, } } + return output, nil +} + +func findDBSubnetGroup(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBSubnetGroupsInput) (*neptune.DBSubnetGroup, error) { + output, err := findDBSubnetGroups(ctx, conn, input) + if err != nil { return nil, err } - if output == nil || len(output.DBSubnetGroups) == 0 || output.DBSubnetGroups[0] == nil { - return nil, tfresource.NewEmptyResultError(input) - } + return tfresource.AssertSinglePtrResult(output) +} - dbSubnetGroup := output.DBSubnetGroups[0] +func findDBSubnetGroups(ctx context.Context, conn *neptune.Neptune, input *neptune.DescribeDBSubnetGroupsInput) ([]*neptune.DBSubnetGroup, error) { + var output []*neptune.DBSubnetGroup - // Eventual consistency check. - if aws.StringValue(dbSubnetGroup.DBSubnetGroupName) != name { + err := conn.DescribeDBSubnetGroupsPagesWithContext(ctx, input, func(page *neptune.DescribeDBSubnetGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DBSubnetGroups { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, neptune.ErrCodeDBSubnetGroupNotFoundFault) { return nil, &retry.NotFoundError{ + LastError: err, LastRequest: input, } } - return dbSubnetGroup, nil + if err != nil { + return nil, err + } + + return output, nil } diff --git a/internal/service/neptune/sweep.go b/internal/service/neptune/sweep.go index f92afa3930d..f4e7ce6977f 100644 --- a/internal/service/neptune/sweep.go +++ b/internal/service/neptune/sweep.go @@ -6,6 +6,7 @@ package neptune import ( "fmt" "log" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/neptune" @@ -16,11 +17,6 @@ import ( ) func RegisterSweepers() { - resource.AddTestSweepers("aws_neptune_event_subscription", &resource.Sweeper{ - Name: "aws_neptune_event_subscription", - F: sweepEventSubscriptions, - }) - resource.AddTestSweepers("aws_neptune_cluster", &resource.Sweeper{ Name: "aws_neptune_cluster", F: sweepClusters, @@ -34,6 +30,27 @@ func RegisterSweepers() { F: sweepClusterInstances, }) + resource.AddTestSweepers("aws_neptune_cluster_parameter_group", &resource.Sweeper{ + Name: "aws_neptune_cluster_parameter_group", + F: sweepClusterParameterGroups, + Dependencies: []string{ + "aws_neptune_cluster", + }, + }) + + resource.AddTestSweepers("aws_neptune_cluster_snapshot", &resource.Sweeper{ + Name: "aws_neptune_cluster_snapshot", + F: sweepClusterSnapshots, + Dependencies: []string{ + "aws_neptune_cluster", + }, + }) + + resource.AddTestSweepers("aws_neptune_event_subscription", &resource.Sweeper{ + Name: "aws_neptune_event_subscription", + F: sweepEventSubscriptions, + }) + resource.AddTestSweepers("aws_neptune_global_cluster", &resource.Sweeper{ Name: "aws_neptune_global_cluster", F: sweepGlobalClusters, @@ -41,6 +58,22 @@ func RegisterSweepers() { "aws_neptune_cluster", }, }) + + resource.AddTestSweepers("aws_neptune_parameter_group", &resource.Sweeper{ + Name: "aws_neptune_parameter_group", + F: sweepParameterGroups, + Dependencies: []string{ + "aws_neptune_cluster_instance", + }, + }) + + resource.AddTestSweepers("aws_neptune_subnet_group", &resource.Sweeper{ + Name: "aws_neptune_subnet_group", + F: sweepSubnetGroups, + Dependencies: []string{ + "aws_neptune_cluster", + }, + }) } func sweepEventSubscriptions(region string) error { @@ -149,6 +182,101 @@ func sweepClusters(region string) error { return nil } +func sweepClusterSnapshots(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.NeptuneConn(ctx) + input := &neptune.DescribeDBClusterSnapshotsInput{} + sweepResources := make([]sweep.Sweepable, 0) + + err = conn.DescribeDBClusterSnapshotsPagesWithContext(ctx, input, func(page *neptune.DescribeDBClusterSnapshotsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DBClusterSnapshots { + r := ResourceClusterSnapshot() + d := r.Data(nil) + d.SetId(aws.StringValue(v.DBClusterSnapshotIdentifier)) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if awsv1.SkipSweepError(err) { + log.Printf("[WARN] Skipping Neptune Cluster Snapshot sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("listing Neptune Cluster Snapshots (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("sweeping Neptune Cluster Snapshots (%s): %w", region, err) + } + + return nil +} + +func sweepClusterParameterGroups(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.NeptuneConn(ctx) + input := &neptune.DescribeDBClusterParameterGroupsInput{} + sweepResources := make([]sweep.Sweepable, 0) + + err = conn.DescribeDBClusterParameterGroupsPagesWithContext(ctx, input, func(page *neptune.DescribeDBClusterParameterGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DBClusterParameterGroups { + name := aws.StringValue(v.DBClusterParameterGroupName) + + if strings.HasPrefix(name, "default.") { + log.Printf("[INFO] Skipping Neptune Cluster Parameter Group: %s", name) + continue + } + + r := ResourceClusterParameterGroup() + d := r.Data(nil) + d.SetId(name) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if awsv1.SkipSweepError(err) { + log.Printf("[WARN] Skipping Neptune Cluster Parameter Group sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing Neptune Cluster Parameter Groups (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping Neptune Cluster Parameter Groups (%s): %w", region, err) + } + + return nil +} + func sweepClusterInstances(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) @@ -244,3 +372,98 @@ func sweepGlobalClusters(region string) error { return nil } + +func sweepParameterGroups(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.NeptuneConn(ctx) + input := &neptune.DescribeDBParameterGroupsInput{} + sweepResources := make([]sweep.Sweepable, 0) + + err = conn.DescribeDBParameterGroupsPagesWithContext(ctx, input, func(page *neptune.DescribeDBParameterGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DBParameterGroups { + name := aws.StringValue(v.DBParameterGroupName) + + if strings.HasPrefix(name, "default.") { + log.Printf("[INFO] Skipping Neptune Parameter Group: %s", name) + continue + } + + r := ResourceParameterGroup() + d := r.Data(nil) + d.SetId(name) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if awsv1.SkipSweepError(err) { + log.Printf("[WARN] Skipping Neptune Parameter Group sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing Neptune Parameter Groups (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping Neptune Parameter Groups (%s): %w", region, err) + } + + return nil +} + +func sweepSubnetGroups(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.NeptuneConn(ctx) + input := &neptune.DescribeDBSubnetGroupsInput{} + sweepResources := make([]sweep.Sweepable, 0) + + err = conn.DescribeDBSubnetGroupsPagesWithContext(ctx, input, func(page *neptune.DescribeDBSubnetGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DBSubnetGroups { + r := ResourceSubnetGroup() + d := r.Data(nil) + d.SetId(aws.StringValue(v.DBSubnetGroupName)) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if awsv1.SkipSweepError(err) { + log.Printf("[WARN] Skipping Neptune Subnet Group sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing Neptune Subnet Groups (%s): %w", region, err) + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping Neptune Subnet Groups (%s): %w", region, err) + } + + return nil +} diff --git a/internal/service/neptune/validate.go b/internal/service/neptune/validate.go index 567757c405a..eea745bcd93 100644 --- a/internal/service/neptune/validate.go +++ b/internal/service/neptune/validate.go @@ -8,16 +8,8 @@ import ( "github.com/YakDriver/regexache" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -func validEngine() schema.SchemaValidateFunc { - return validation.StringInSlice([]string{ - "neptune", - }, false) -} - func validEventSubscriptionName(v interface{}, k string) (ws []string, errors []error) { value := v.(string) if !regexache.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) { diff --git a/website/docs/r/docdb_cluster.html.markdown b/website/docs/r/docdb_cluster.html.markdown index 16d80874e73..9e309e20c8a 100644 --- a/website/docs/r/docdb_cluster.html.markdown +++ b/website/docs/r/docdb_cluster.html.markdown @@ -57,7 +57,7 @@ This argument supports the following arguments: * `enabled_cloudwatch_logs_exports` - (Optional) List of log types to export to cloudwatch. If omitted, no logs will be exported. The following log types are supported: `audit`, `profiler`. * `engine_version` - (Optional) The database engine version. Updating this argument results in an outage. -* `engine` - (Optional) The name of the database engine to be used for this DB cluster. Defaults to `docdb`. Valid Values: `docdb` +* `engine` - (Optional) The name of the database engine to be used for this DB cluster. Defaults to `docdb`. Valid values: `docdb`. * `final_snapshot_identifier` - (Optional) The name of your final DB snapshot when this DB cluster is deleted. If omitted, no final snapshot will be made. @@ -73,6 +73,7 @@ Default: A 30-minute window selected at random from an 8-hour block of time per * `skip_final_snapshot` - (Optional) Determines whether a final DB snapshot is created before the DB cluster is deleted. If true is specified, no DB snapshot is created. If false is specified, a DB snapshot is created before the DB cluster is deleted, using the value from `final_snapshot_identifier`. Default is `false`. * `snapshot_identifier` - (Optional) Specifies whether or not to create this cluster from a snapshot. You can use either the name or ARN when specifying a DB cluster snapshot, or the ARN when specifying a DB snapshot. Automated snapshots **should not** be used for this attribute, unless from a different cluster. Automated snapshots are deleted as part of cluster destruction when the resource is replaced. * `storage_encrypted` - (Optional) Specifies whether the DB cluster is encrypted. The default is `false`. +* `storage_type` - (Optional) The storage type to associate with the DB cluster. Valid values: `standard`, `iopt1`. * `tags` - (Optional) A map of tags to assign to the DB cluster. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. * `vpc_security_group_ids` - (Optional) List of VPC security groups to associate with the Cluster diff --git a/website/docs/r/neptune_parameter_group.html.markdown b/website/docs/r/neptune_parameter_group.html.markdown index 23eb8a7a431..029f14836ca 100644 --- a/website/docs/r/neptune_parameter_group.html.markdown +++ b/website/docs/r/neptune_parameter_group.html.markdown @@ -28,7 +28,8 @@ resource "aws_neptune_parameter_group" "example" { This resource supports the following arguments: -* `name` - (Required, Forces new resource) The name of the Neptune parameter group. +* `name` - (Optional, Forces new resource) The name of the Neptune parameter group. +* `name_prefix` - (Optional, Forces new resource) Creates a unique name beginning with the specified prefix. Conflicts with `name`. * `family` - (Required) The family of the Neptune parameter group. * `description` - (Optional) The description of the Neptune parameter group. Defaults to "Managed by Terraform". * `parameter` - (Optional) A list of Neptune parameters to apply.