From 42388a0e2daebf7ed6df4de33aaeb83364dedf36 Mon Sep 17 00:00:00 2001 From: MqllR Date: Tue, 6 Oct 2020 22:18:13 +0200 Subject: [PATCH 01/25] feat: implement the elasticache_global_replication_group resource --- aws/provider.go | 1 + ...ws_elasticache_global_replication_group.go | 469 ++++++++++++++++++ 2 files changed, 470 insertions(+) create mode 100644 aws/resource_aws_elasticache_global_replication_group.go diff --git a/aws/provider.go b/aws/provider.go index 4040c614d91..17b8d6b8cd8 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -607,6 +607,7 @@ func Provider() *schema.Provider { "aws_eks_fargate_profile": resourceAwsEksFargateProfile(), "aws_eks_node_group": resourceAwsEksNodeGroup(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), + "aws_elasticache_global_replication_group": resourceAwsElasticacheGlobalReplicationGroup(), "aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(), "aws_elasticache_replication_group": resourceAwsElasticacheReplicationGroup(), "aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(), diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go new file mode 100644 index 00000000000..a2210299fb1 --- /dev/null +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -0,0 +1,469 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + elasticacheGlobalReplicationGroupRemovalTimeout = 2 * time.Minute +) + +func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsElasticacheGlobalReplicationGroupCreate, + Read: resourceAwsElasticacheGlobalReplicationGroupRead, + Update: resourceAwsElasticacheGlobalReplicationGroupUpdate, + Delete: resourceAwsElasticacheGlobalReplicationGroupDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "apply_immediately": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "at_rest_encryption_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "auth_token_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "automatic_failover_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "cache_node_type": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "cluster_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "engine": { + Type: schema.TypeString, + Computed: true, + }, + "engine_version": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "global_replication_group_id_suffix": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "global_replication_group_description": { + Type: schema.TypeString, + Optional: true, + Default: false, + }, + "global_replication_group_members": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "replication_group_id": { + Type: schema.TypeString, + Computed: true, + }, + "replication_group_region": { + Type: schema.TypeString, + Computed: true, + }, + "role": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "primary_replication_group_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "retain_primary_replication_group": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "transit_encryption_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func resourceAwsElasticacheGlobalReplicationGroupCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + input := &elasticache.CreateGlobalReplicationGroupInput{ + GlobalReplicationGroupIdSuffix: aws.String(d.Get("global_replication_group_id_suffix").(string)), + PrimaryReplicationGroupId: aws.String(d.Get("primary_replication_group_id").(string)), + } + + if v, ok := d.GetOk("global_replication_group_description"); ok { + input.GlobalReplicationGroupDescription = aws.String(v.(string)) + } + + output, err := conn.CreateGlobalReplicationGroup(input) + if err != nil { + return fmt.Errorf("error creating ElastiCache Global Replication Group: %s", err) + } + + d.SetId(aws.StringValue(output.GlobalReplicationGroup.GlobalReplicationGroupId)) + + if err := waitForElasticacheGlobalReplicationGroupCreation(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) availability: %s", d.Id(), err) + } + + return resourceAwsElasticacheGlobalReplicationGroupRead(d, meta) +} + +func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, d.Id()) + + if isAWSErr(err, elasticache.ErrCodeReplicationGroupNotFoundFault, "") { + log.Printf("[WARN] ElastiCache Global Replication Group (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading ElastiCache Replication Group: %s", err) + } + + if globalReplicationGroup == nil { + log.Printf("[WARN] ElastiCache Global Replication Group (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if aws.StringValue(globalReplicationGroup.Status) == "deleting" || aws.StringValue(globalReplicationGroup.Status) == "deleted" { + log.Printf("[WARN] ElastiCache Global Replication Group (%s) in deleted state (%s), removing from state", d.Id(), aws.StringValue(globalReplicationGroup.Status)) + d.SetId("") + return nil + } + + d.Set("arn", globalReplicationGroup.ARN) + d.Set("at_rest_encryption_enabled", globalReplicationGroup.AtRestEncryptionEnabled) + d.Set("auth_token_enabled", globalReplicationGroup.AuthTokenEnabled) + d.Set("cache_node_type", globalReplicationGroup.CacheNodeType) + d.Set("cluster_enabled", globalReplicationGroup.ClusterEnabled) + d.Set("engine", globalReplicationGroup.Engine) + d.Set("engine_version", globalReplicationGroup.EngineVersion) + d.Set("transit_encryption_enabled", globalReplicationGroup.TransitEncryptionEnabled) + + if err := d.Set("global_replication_group_members", flattenElasticacheGlobalReplicationGroupMembers(globalReplicationGroup.Members)); err != nil { + return fmt.Errorf("error setting global_cluster_members: %w", err) + } + + return nil +} + +func resourceAwsElasticacheGlobalReplicationGroupUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + input := &elasticache.ModifyGlobalReplicationGroupInput{ + ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), + AutomaticFailoverEnabled: aws.Bool(d.Get("automatic_failover_enabled").(bool)), + GlobalReplicationGroupId: aws.String(d.Id()), + } + + requestUpdate := false + if d.HasChange("cache_node_type") { + input.CacheNodeType = aws.String(d.Get("cache_node_type").(string)) + requestUpdate = true + } + + if d.HasChange("engine_version") { + input.EngineVersion = aws.String(d.Get("engine_version").(string)) + requestUpdate = true + } + + if d.HasChange("global_replication_group_description") { + input.GlobalReplicationGroupDescription = aws.String(d.Get("global_replication_group_description").(string)) + requestUpdate = true + } + + if requestUpdate { + _, err := conn.ModifyGlobalReplicationGroup(input) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) + } + + if err := waitForElasticacheGlobalReplicationUpdate(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for ElastiCache Global Replcation Cluster (%s) update: %s", d.Id(), err) + } + } + + return nil +} + +func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + for _, globalReplicationGroupMemberRaw := range d.Get("global_replication_group_members").(*schema.Set).List() { + globalReplicationGroupMember, ok := globalReplicationGroupMemberRaw.(map[string]interface{}) + + if !ok { + continue + } + replicationGroupId, ok := globalReplicationGroupMember["replication_group_id"].(string) + if !ok { + continue + } + + role, ok := globalReplicationGroupMember["role"].(string) + if !ok { + continue + } + + if role == "secondary" { + replicationGroupRegion, ok := globalReplicationGroupMember["replication_group_region"].(string) + if !ok { + continue + } + + input := &elasticache.DisassociateGlobalReplicationGroupInput{ + GlobalReplicationGroupId: aws.String(d.Id()), + ReplicationGroupId: aws.String(replicationGroupId), + ReplicationGroupRegion: aws.String(replicationGroupRegion), + } + + _, err := conn.DisassociateGlobalReplicationGroup(input) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + return nil + } + + /* + if err := waitForElasticacheGlobalReplicationGroupRemoval(conn, replicationGroupId); err != nil { + return fmt.Errorf("error waiting for Elasticache Replication Group (%s) removal from Elasticache Global Replication Group (%s): %w", replicationGroupId, d.Id(), err) + } + */ + } + } + + input := &elasticache.DeleteGlobalReplicationGroupInput{ + GlobalReplicationGroupId: aws.String(d.Id()), + RetainPrimaryReplicationGroup: aws.Bool(d.Get("retain_primary_replication_group").(bool)), + } + + log.Printf("[DEBUG] Deleting ElastiCache Global Replication Group (%s): %s", d.Id(), input) + + err := resource.Retry(1*time.Minute, func() *resource.RetryError { + _, err := conn.DeleteGlobalReplicationGroup(input) + + if isAWSErr(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "is not empty") { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if isResourceTimeoutError(err) { + _, err = conn.DeleteGlobalReplicationGroup(input) + } + + if isAWSErr(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) + } + + if err := waitForElasticacheGlobalReplicationDeletion(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) deletion: %s", d.Id(), err) + } + + return nil +} + +func flattenElasticacheGlobalReplicationGroupMembers(apiObjects []*elasticache.GlobalReplicationGroupMember) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + tfMap := map[string]interface{}{ + "replication_group_id": aws.StringValue(apiObject.ReplicationGroupId), + "replication_group_region": aws.StringValue(apiObject.ReplicationGroupRegion), + "role": aws.StringValue(apiObject.Role), + } + + tfList = append(tfList, tfMap) + } + + return tfList +} + +func elasticacheDescribeGlobalReplicationGroup(conn *elasticache.ElastiCache, globalReplicationGroupID string) (*elasticache.GlobalReplicationGroup, error) { + var globalReplicationGroup *elasticache.GlobalReplicationGroup + + input := &elasticache.DescribeGlobalReplicationGroupsInput{ + GlobalReplicationGroupId: aws.String(globalReplicationGroupID), + } + + log.Printf("[DEBUG] Reading ElastiCache Global Replication Group (%s): %s", globalReplicationGroupID, input) + err := conn.DescribeGlobalReplicationGroupsPages(input, func(page *elasticache.DescribeGlobalReplicationGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, gc := range page.GlobalReplicationGroups { + if gc == nil { + continue + } + + if aws.StringValue(gc.GlobalReplicationGroupId) == globalReplicationGroupID { + globalReplicationGroup = gc + return false + } + } + + return !lastPage + }) + + return globalReplicationGroup, err +} + +func elasticacheGlobalReplicationGroupRefreshFunc(conn *elasticache.ElastiCache, globalReplicationGroupID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, globalReplicationGroupID) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + return nil, "deleted", nil + } + + if err != nil { + return nil, "", fmt.Errorf("error reading ElastiCache Global Replication Group (%s): %s", globalReplicationGroupID, err) + } + + if globalReplicationGroup == nil { + return nil, "deleted", nil + } + + return globalReplicationGroup, aws.StringValue(globalReplicationGroup.Status), nil + } +} + +func waitForElasticacheGlobalReplicationGroupCreation(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating"}, + Target: []string{"available", "primary-only"}, + Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), + Timeout: 10 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) availability", globalReplicationGroupID) + _, err := stateConf.WaitForState() + + return err +} + +func waitForElasticacheGlobalReplicationUpdate(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"modifying"}, + Target: []string{"available"}, + Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), + Timeout: 10 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) availability", globalReplicationGroupID) + _, err := stateConf.WaitForState() + + return err +} + +func waitForElasticacheGlobalReplicationDeletion(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "available", + "deleting", + }, + Target: []string{"deleted"}, + Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), + Timeout: 10 * time.Minute, + NotFoundChecks: 1, + } + + log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) deletion", globalReplicationGroupID) + _, err := stateConf.WaitForState() + + if isResourceNotFoundError(err) { + return nil + } + + return err +} + +/* +func waitForElasticacheReplicationGroupDisassociation(conn *elasticache.ElastiCache, replicationGroupId string) error { + stillExistsErr := fmt.Errorf("ElastiCache Replication Group still associated in ElastiCache Global Replication Group") + + err := resource.Retry(elasticacheGlobalReplicationGroupRemovalTimeout, func() *resource.RetryError { + var err error + + replicationGroup, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, replicationGroupId) + + if err != nil { + return resource.NonRetryableError(err) + } + + if replicationGroup != nil { + return resource.RetryableError(stillExistsErr) + } + + return nil + }) + + if isResourceTimeoutError(err) { + _, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, replicationGroupId) + } + + if err != nil { + return err + } + + if replicationGroup != nil { + return stillExistsErr + } + + return nil +} +*/ From 45d9c5a2b735723516037988751107bda6bd3e6b Mon Sep 17 00:00:00 2001 From: MqllR Date: Thu, 8 Oct 2020 11:23:00 +0200 Subject: [PATCH 02/25] feat: manage the global replication group deletion --- ...ws_elasticache_global_replication_group.go | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index a2210299fb1..b5584c30017 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -236,7 +236,7 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, if !ok { continue } - replicationGroupId, ok := globalReplicationGroupMember["replication_group_id"].(string) + replicationGroupID, ok := globalReplicationGroupMember["replication_group_id"].(string) if !ok { continue } @@ -246,7 +246,7 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, continue } - if role == "secondary" { + if role == "SECONDARY" { replicationGroupRegion, ok := globalReplicationGroupMember["replication_group_region"].(string) if !ok { continue @@ -254,7 +254,7 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, input := &elasticache.DisassociateGlobalReplicationGroupInput{ GlobalReplicationGroupId: aws.String(d.Id()), - ReplicationGroupId: aws.String(replicationGroupId), + ReplicationGroupId: aws.String(replicationGroupID), ReplicationGroupRegion: aws.String(replicationGroupRegion), } @@ -264,11 +264,9 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, return nil } - /* - if err := waitForElasticacheGlobalReplicationGroupRemoval(conn, replicationGroupId); err != nil { - return fmt.Errorf("error waiting for Elasticache Replication Group (%s) removal from Elasticache Global Replication Group (%s): %w", replicationGroupId, d.Id(), err) - } - */ + if err := waitForElasticacheGlobalReplicationGroupDisassociation(conn, d.Id(), replicationGroupID); err != nil { + return fmt.Errorf("error waiting for Elasticache Replication Group (%s) removal from Elasticache Global Replication Group (%s): %w", replicationGroupID, d.Id(), err) + } } } @@ -337,6 +335,7 @@ func elasticacheDescribeGlobalReplicationGroup(conn *elasticache.ElastiCache, gl input := &elasticache.DescribeGlobalReplicationGroupsInput{ GlobalReplicationGroupId: aws.String(globalReplicationGroupID), + ShowMemberInfo: aws.Bool(true), } log.Printf("[DEBUG] Reading ElastiCache Global Replication Group (%s): %s", globalReplicationGroupID, input) @@ -414,6 +413,8 @@ func waitForElasticacheGlobalReplicationDeletion(conn *elasticache.ElastiCache, stateConf := &resource.StateChangeConf{ Pending: []string{ "available", + "primary-only", + "modifying", "deleting", }, Target: []string{"deleted"}, @@ -432,14 +433,14 @@ func waitForElasticacheGlobalReplicationDeletion(conn *elasticache.ElastiCache, return err } -/* -func waitForElasticacheReplicationGroupDisassociation(conn *elasticache.ElastiCache, replicationGroupId string) error { +func waitForElasticacheGlobalReplicationGroupDisassociation(conn *elasticache.ElastiCache, globalReplicationGroupID string, replicationGroupID string) error { stillExistsErr := fmt.Errorf("ElastiCache Replication Group still associated in ElastiCache Global Replication Group") + var replicationGroup *elasticache.GlobalReplicationGroupMember err := resource.Retry(elasticacheGlobalReplicationGroupRemovalTimeout, func() *resource.RetryError { var err error - replicationGroup, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, replicationGroupId) + replicationGroup, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, globalReplicationGroupID, replicationGroupID) if err != nil { return resource.NonRetryableError(err) @@ -453,7 +454,7 @@ func waitForElasticacheReplicationGroupDisassociation(conn *elasticache.ElastiCa }) if isResourceTimeoutError(err) { - _, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, replicationGroupId) + _, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, globalReplicationGroupID, replicationGroupID) } if err != nil { @@ -466,4 +467,25 @@ func waitForElasticacheReplicationGroupDisassociation(conn *elasticache.ElastiCa return nil } -*/ + +func elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn *elasticache.ElastiCache, globalReplicationGroupID string, replicationGroupID string) (*elasticache.GlobalReplicationGroupMember, error) { + globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, globalReplicationGroupID) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + return nil, err + } + + members := globalReplicationGroup.Members + + if len(members) == 0 { + return nil, nil + } + + for _, member := range members { + if *member.ReplicationGroupId == replicationGroupID { + return member, nil + } + } + + return nil, nil +} From a2264f04fa30b2cda4c3662ec5ae9b5b1283fefd Mon Sep 17 00:00:00 2001 From: MqllR Date: Thu, 8 Oct 2020 21:36:05 +0200 Subject: [PATCH 03/25] feat: create the base documentation page for elasticache_global_replication_group --- ...che_global_replication_group.html.markdown | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 website/docs/r/elasticache_global_replication_group.html.markdown diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown new file mode 100644 index 00000000000..0ef7bb6b84a --- /dev/null +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "ElastiCache" +layout: "aws" +page_title: "AWS: aws_elasticache_global_replication_group" +description: |- + Provides an ElastiCache Global Replication Group resource. +--- + +# Resource: aws_elasticache_global_replication_group + +Provides an ElastiCache Global Replication Group resource. + +## Example Usage + +### Simple redis global replication group mode cluster disabled + +To create a single shard primary with single read replica: + +```hcl +resource "aws_elasticache_global_replication_group" "replication_group" { + global_replication_group_id_suffix = "example" + primary_replication_group_id = aws_elasticache_replication_group.primary.id +} + +resource "aws_elasticache_replication_group" "primary" { + replication_group_id = "example" + replication_group_description = "test example" + + engine = "redis" + engine_version = "5.0.6" + node_type = "cache.m5.large" + number_cache_clusters = 1 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `global_replication_group_id_suffix` – (Required) The suffix name of a Global Datastore. +* `replication_group_id` – (Required) The replication group identifier. The Global Datastore will be created from this replication group. +* `global_replication_group_description` – (Optional) A user-created description for the global replication group. +* `retain_primary_replication_group` - (Optional) Whether to retain the primary replication group when the global replication group is deleted. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the ElastiCache Global Replication Group. + +## Import + +ElastiCache Global Replication Groups can be imported using the `global_replication_group_id`, e.g. + +``` +$ terraform import aws_elasticache_global_replication_group.my_global_replication_group global-replication-group-1 +``` From 5f98c7f4e9a7a570c91293e3dc5fd5507e22df80 Mon Sep 17 00:00:00 2001 From: MqllR Date: Tue, 13 Oct 2020 17:54:14 +0200 Subject: [PATCH 04/25] feat: add basic acceptance test and initiate doc --- ...ws_elasticache_global_replication_group.go | 10 +- ...asticache_global_replication_group_test.go | 203 ++++++++++++++++++ ...che_global_replication_group.html.markdown | 4 +- 3 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 aws/resource_aws_elasticache_global_replication_group_test.go diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index b5584c30017..c1c5071e46a 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -145,7 +145,7 @@ func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, me globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, d.Id()) - if isAWSErr(err, elasticache.ErrCodeReplicationGroupNotFoundFault, "") { + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { log.Printf("[WARN] ElastiCache Global Replication Group (%s) not found, removing from state", d.Id()) d.SetId("") return nil @@ -219,7 +219,7 @@ func resourceAwsElasticacheGlobalReplicationGroupUpdate(d *schema.ResourceData, return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) } - if err := waitForElasticacheGlobalReplicationUpdate(conn, d.Id()); err != nil { + if err := waitForElasticacheGlobalReplicationGroupUpdate(conn, d.Id()); err != nil { return fmt.Errorf("error waiting for ElastiCache Global Replcation Cluster (%s) update: %s", d.Id(), err) } } @@ -303,7 +303,7 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) } - if err := waitForElasticacheGlobalReplicationDeletion(conn, d.Id()); err != nil { + if err := waitForElasticacheGlobalReplicationGroupDeletion(conn, d.Id()); err != nil { return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) deletion: %s", d.Id(), err) } @@ -395,7 +395,7 @@ func waitForElasticacheGlobalReplicationGroupCreation(conn *elasticache.ElastiCa return err } -func waitForElasticacheGlobalReplicationUpdate(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { +func waitForElasticacheGlobalReplicationGroupUpdate(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { stateConf := &resource.StateChangeConf{ Pending: []string{"modifying"}, Target: []string{"available"}, @@ -409,7 +409,7 @@ func waitForElasticacheGlobalReplicationUpdate(conn *elasticache.ElastiCache, gl return err } -func waitForElasticacheGlobalReplicationDeletion(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { +func waitForElasticacheGlobalReplicationGroupDeletion(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { stateConf := &resource.StateChangeConf{ Pending: []string{ "available", diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go new file mode 100644 index 00000000000..defb1d66f6f --- /dev/null +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -0,0 +1,203 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func init() { + resource.AddTestSweepers("aws_elasticache_global_replication_group", &resource.Sweeper{ + Name: "aws_elasticache_global_replication_group", + F: testSweepElasticacheGlobalReplicationGroups, + Dependencies: []string{ + "aws_elasticache_replication_group", + }, + }) +} + +func testSweepElasticacheGlobalReplicationGroups(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + conn := client.(*AWSClient).elasticacheconn + input := &elasticache.DescribeGlobalReplicationGroupsInput{} + + err = conn.DescribeGlobalReplicationGroupsPages(input, func(out *elasticache.DescribeGlobalReplicationGroupsOutput, lastPage bool) bool { + for _, globalReplicationGroup := range out.GlobalReplicationGroups { + id := aws.StringValue(globalReplicationGroup.GlobalReplicationGroupId) + input := &elasticache.DeleteGlobalReplicationGroupInput{ + GlobalReplicationGroupId: globalReplicationGroup.GlobalReplicationGroupId, + } + + log.Printf("[INFO] Deleting Elasticache Global Replication Group: %s", id) + + _, err := conn.DeleteGlobalReplicationGroup(input) + + if err != nil { + log.Printf("[ERROR] Failed to delete ElastiCache Global Replication Group (%s): %s", id, err) + continue + } + + if err := waitForElasticacheGlobalReplicationGroupDeletion(conn, id); err != nil { + log.Printf("[ERROR] Failure while waiting for ElastiCache Global Replication Group (%s) to be deleted: %s", id, err) + } + } + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping ElastiCache Global Replication Group sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error retrieving ElastiCache Global Replication Groups: %s", err) + } + + return nil +} + +func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { + var globalReplcationGroup1 elasticache.GlobalReplicationGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_elasticache_global_replication_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSElasticacheGlobalReplicationGroup(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheGlobalReplicationGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheGlobalReplicationGroupConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup1), + testAccCheckResourceAttrGlobalARN(resourceName, "arn", "elasticache", fmt.Sprintf("global-replication-group:%s", rName)), + resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", ""), + resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), + resource.TestCheckResourceAttrSet(resourceName, "automatic_failover_enabled"), + resource.TestCheckResourceAttrSet(resourceName, "cache_node_type"), + resource.TestCheckResourceAttr(resourceName, "cluster_enabled", rName), + resource.TestCheckResourceAttr(resourceName, "engine", "redis"), + resource.TestCheckResourceAttr(resourceName, "engine_version", "5.0.6"), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_id_suffix", rName), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", "false"), + resource.TestCheckResourceAttrSet(resourceName, "global_replication_group_members"), + resource.TestCheckResourceAttr(resourceName, "primary_replication_group_id", rName), + resource.TestCheckResourceAttr(resourceName, "retain_primary_replication_group", "true"), + resource.TestCheckResourceAttrSet(resourceName, "transit_encryption_enabled"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, globalReplicationGroup *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Elasticache Global Replication Group ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + cluster, err := elasticacheDescribeGlobalReplicationGroup(conn, rs.Primary.ID) + + if err != nil { + return err + } + + if cluster == nil { + return fmt.Errorf("Elasticache Global Replication Group not found") + } + + if aws.StringValue(cluster.Status) != "available" && aws.StringValue(cluster.Status) != "primary-only" { + return fmt.Errorf("Elasticache Global Replication Group (%s) exists in non-available (%s) state", rs.Primary.ID, aws.StringValue(cluster.Status)) + } + + *globalReplicationGroup = *cluster + + return nil + } +} + +func testAccCheckAWSElasticacheGlobalReplicationGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_elasticache_global_replication_group" { + continue + } + + globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, rs.Primary.ID) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + continue + } + + if err != nil { + return err + } + + if globalReplicationGroup == nil { + continue + } + + return fmt.Errorf("Elasticache Global Replication Group (%s) still exists in non-deleted (%s) state", rs.Primary.ID, aws.StringValue(globalReplicationGroup.Status)) + } + + return nil +} + +func testAccPreCheckAWSElasticacheGlobalReplicationGroup(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + input := &elasticache.DescribeGlobalReplicationGroupsInput{} + + _, err := conn.DescribeGlobalReplicationGroups(input) + + if testAccPreCheckSkipError(err) || isAWSErr(err, "InvalidParameterValue", "Access Denied to API Version: APIGlobalDatastore") { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccAWSElasticacheGlobalReplicationGroupConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_elasticache_global_replication_group" "test" { + global_replication_group_id_suffix = %q + primary_replication_group_id = aws_elasticache_replication_group.test.id +} + +resource "aws_elasticache_replication_group" "test" { + replication_group_id = %q + replication_group_description = "test" + + engine = "redis" + engine_version = "5.0.6" + node_type = "cache.m5.large" + number_cache_clusters = 1 +} +`, rName, rName) +} diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 0ef7bb6b84a..0bed96da135 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -18,8 +18,8 @@ To create a single shard primary with single read replica: ```hcl resource "aws_elasticache_global_replication_group" "replication_group" { - global_replication_group_id_suffix = "example" - primary_replication_group_id = aws_elasticache_replication_group.primary.id + global_replication_group_id_suffix = "example" + primary_replication_group_id = aws_elasticache_replication_group.primary.id } resource "aws_elasticache_replication_group" "primary" { From 57a8afad41502dd07ad656ec72fab7bd5ea780a2 Mon Sep 17 00:00:00 2001 From: MqllR Date: Fri, 16 Oct 2020 17:25:01 +0200 Subject: [PATCH 05/25] feat: improve doc resource page and fix fmt test --- ...ws_elasticache_global_replication_group.go | 10 ++++++--- ...asticache_global_replication_group_test.go | 4 ++-- ...che_global_replication_group.html.markdown | 22 +++++++++++++++---- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index c1c5071e46a..a6ab93b8179 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -28,8 +28,7 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { Schema: map[string]*schema.Schema{ "apply_immediately": { Type: schema.TypeBool, - Optional: true, - Computed: true, + Required: true, }, "arn": { Type: schema.TypeString, @@ -188,11 +187,16 @@ func resourceAwsElasticacheGlobalReplicationGroupUpdate(d *schema.ResourceData, input := &elasticache.ModifyGlobalReplicationGroupInput{ ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), - AutomaticFailoverEnabled: aws.Bool(d.Get("automatic_failover_enabled").(bool)), GlobalReplicationGroupId: aws.String(d.Id()), } requestUpdate := false + + if d.HasChange("automatic_failover_enabled") { + input.AutomaticFailoverEnabled = aws.Bool(d.Get("automatic_failover_enabled").(bool)) + requestUpdate = true + } + if d.HasChange("cache_node_type") { input.CacheNodeType = aws.String(d.Get("cache_node_type").(string)) requestUpdate = true diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index defb1d66f6f..ff0450a554c 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -186,8 +186,8 @@ func testAccPreCheckAWSElasticacheGlobalReplicationGroup(t *testing.T) { func testAccAWSElasticacheGlobalReplicationGroupConfig(rName string) string { return fmt.Sprintf(` resource "aws_elasticache_global_replication_group" "test" { - global_replication_group_id_suffix = %q - primary_replication_group_id = aws_elasticache_replication_group.test.id + global_replication_group_id_suffix = %q + primary_replication_group_id = aws_elasticache_replication_group.test.id } resource "aws_elasticache_replication_group" "test" { diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 0bed96da135..51388fa79f7 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -8,11 +8,11 @@ description: |- # Resource: aws_elasticache_global_replication_group -Provides an ElastiCache Global Replication Group resource. +Provides an ElastiCache Global Replication Group resource, which manage a replication between 2 or more redis replication group in different region. ## Example Usage -### Simple redis global replication group mode cluster disabled +### Global replication group with a single instance redis replication group To create a single shard primary with single read replica: @@ -38,20 +38,34 @@ resource "aws_elasticache_replication_group" "primary" { The following arguments are supported: * `global_replication_group_id_suffix` – (Required) The suffix name of a Global Datastore. -* `replication_group_id` – (Required) The replication group identifier. The Global Datastore will be created from this replication group. +* `primary_replication_group_id` – (Required) The name of the primary cluster that accepts writes and will replicate updates to the secondary cluster. * `global_replication_group_description` – (Optional) A user-created description for the global replication group. * `retain_primary_replication_group` - (Optional) Whether to retain the primary replication group when the global replication group is deleted. +* `apply_immediately` - (Required) This parameter causes the modifications in this request and any pending modifications to be applied, asynchronously and as soon as possible. Modifications to Global Replication Groups cannot be requested to be applied in PreferredMaintenceWindow. +* `automatic_failover_enabled` - (Optional) Determines whether a read replica is automatically promoted to read/write primary if the existing primary encounters a failure. +* `cache_node_type` - (Optional) A valid cache node type that you want to scale this Global Datastore to. +* `engine_version` - (Optional) The upgraded version of the cache engine to be run on the clusters in the Global Datastore. ## Attributes Reference In addition to all arguments above, the following attributes are exported: * `id` - The ID of the ElastiCache Global Replication Group. +* `arn` - The ARN of the ElastiCache Global Replication Group. +* `at_rest_encryption_enabled` - A flag that indicate wheter the encryption at rest is enabled. +* `auth_token_enabled` - A flag that indicate wheter AuthToken (password) is enabled. +* `cluster_enabled` - A flag that indicates whether the Global Datastore is cluster enabled. +* `engine` - The Elasticache engine. For redis only +* `global_replication_group_members` - The identifiers of all the replication group members that are part of this global replication group. + * `replication_group_id` - The replication group id of the Global Datastore member + * `replication_group_region` - The AWS region of the Global Datastore member + * `role` - Indicates the role of the replication group, primary or secondary +* `transit_encryption_enabled` - A flag that indicates whether the encryption in transit is enabled. ## Import ElastiCache Global Replication Groups can be imported using the `global_replication_group_id`, e.g. ``` -$ terraform import aws_elasticache_global_replication_group.my_global_replication_group global-replication-group-1 +$ terraform import aws_elasticache_global_replication_group.my_global_replication_group okuqm-global-replication-group-1 ``` From 60850dd367ef2cf968996ae8af0242b170881e18 Mon Sep 17 00:00:00 2001 From: MqllR Date: Fri, 16 Oct 2020 17:28:27 +0200 Subject: [PATCH 06/25] fix misspell words --- .../docs/r/elasticache_global_replication_group.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 51388fa79f7..d60320f46fe 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -52,8 +52,8 @@ In addition to all arguments above, the following attributes are exported: * `id` - The ID of the ElastiCache Global Replication Group. * `arn` - The ARN of the ElastiCache Global Replication Group. -* `at_rest_encryption_enabled` - A flag that indicate wheter the encryption at rest is enabled. -* `auth_token_enabled` - A flag that indicate wheter AuthToken (password) is enabled. +* `at_rest_encryption_enabled` - A flag that indicate whether the encryption at rest is enabled. +* `auth_token_enabled` - A flag that indicate whether AuthToken (password) is enabled. * `cluster_enabled` - A flag that indicates whether the Global Datastore is cluster enabled. * `engine` - The Elasticache engine. For redis only * `global_replication_group_members` - The identifiers of all the replication group members that are part of this global replication group. From 43280e9dd309a72c148c4f703b409f652bf9394e Mon Sep 17 00:00:00 2001 From: MqllR Date: Wed, 21 Oct 2020 18:56:38 +0200 Subject: [PATCH 07/25] add disappears tests --- ...ws_elasticache_global_replication_group.go | 3 +- ...asticache_global_replication_group_test.go | 57 ++++++++++++++++--- ...che_global_replication_group.html.markdown | 2 +- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index a6ab93b8179..48a2bf7d52a 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -28,7 +28,8 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { Schema: map[string]*schema.Schema{ "apply_immediately": { Type: schema.TypeBool, - Required: true, + Optional: true, + Default: true, }, "arn": { Type: schema.TypeString, diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index ff0450a554c..8cdf4c4bdb0 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "regexp" "testing" "github.com/aws/aws-sdk-go/aws" @@ -81,20 +82,19 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { Config: testAccAWSElasticacheGlobalReplicationGroupConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup1), - testAccCheckResourceAttrGlobalARN(resourceName, "arn", "elasticache", fmt.Sprintf("global-replication-group:%s", rName)), - resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", ""), + testAccMatchResourceAttrGlobalARN(resourceName, "arn", "elasticache", regexp.MustCompile(`globalreplicationgroup:\w{5}-`+rName)), // \w{5} is the AWS prefix + resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), - resource.TestCheckResourceAttrSet(resourceName, "automatic_failover_enabled"), - resource.TestCheckResourceAttrSet(resourceName, "cache_node_type"), - resource.TestCheckResourceAttr(resourceName, "cluster_enabled", rName), + resource.TestCheckResourceAttr(resourceName, "automatic_failover_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "cache_node_type", "cache.m5.large"), + resource.TestCheckResourceAttr(resourceName, "cluster_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "engine", "redis"), resource.TestCheckResourceAttr(resourceName, "engine_version", "5.0.6"), resource.TestCheckResourceAttr(resourceName, "global_replication_group_id_suffix", rName), - resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", "false"), - resource.TestCheckResourceAttrSet(resourceName, "global_replication_group_members"), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", "0"), resource.TestCheckResourceAttr(resourceName, "primary_replication_group_id", rName), resource.TestCheckResourceAttr(resourceName, "retain_primary_replication_group", "true"), - resource.TestCheckResourceAttrSet(resourceName, "transit_encryption_enabled"), + resource.TestCheckResourceAttr(resourceName, "transit_encryption_enabled", "false"), ), }, { @@ -106,6 +106,28 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { }) } +func TestAccAWSElasticacheGlobalReplicationGroup_disappears(t *testing.T) { + var globalReplcationGroup1 elasticache.GlobalReplicationGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_elasticache_global_replication_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSElasticacheGlobalReplicationGroup(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheGlobalReplicationGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheGlobalReplicationGroupConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup1), + testAccCheckAWSElasticacheGlobalReplicationGroupDisappears(&globalReplcationGroup1), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, globalReplicationGroup *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -139,6 +161,25 @@ func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, } } +func testAccCheckAWSElasticacheGlobalReplicationGroupDisappears(globalReplicationGroup *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + input := &elasticache.DeleteGlobalReplicationGroupInput{ + GlobalReplicationGroupId: globalReplicationGroup.GlobalReplicationGroupId, + RetainPrimaryReplicationGroup: aws.Bool(true), + } + + _, err := conn.DeleteGlobalReplicationGroup(input) + + if err != nil { + return err + } + + return waitForElasticacheGlobalReplicationGroupDeletion(conn, aws.StringValue(globalReplicationGroup.GlobalReplicationGroupId)) + } +} + func testAccCheckAWSElasticacheGlobalReplicationGroupDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).elasticacheconn diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index d60320f46fe..87c94f05fbb 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -41,7 +41,7 @@ The following arguments are supported: * `primary_replication_group_id` – (Required) The name of the primary cluster that accepts writes and will replicate updates to the secondary cluster. * `global_replication_group_description` – (Optional) A user-created description for the global replication group. * `retain_primary_replication_group` - (Optional) Whether to retain the primary replication group when the global replication group is deleted. -* `apply_immediately` - (Required) This parameter causes the modifications in this request and any pending modifications to be applied, asynchronously and as soon as possible. Modifications to Global Replication Groups cannot be requested to be applied in PreferredMaintenceWindow. +* `apply_immediately` - (Optional) This parameter causes the modifications in this request and any pending modifications to be applied, asynchronously and as soon as possible. Modifications to Global Replication Groups cannot be requested to be applied in PreferredMaintenceWindow. Default to true. * `automatic_failover_enabled` - (Optional) Determines whether a read replica is automatically promoted to read/write primary if the existing primary encounters a failure. * `cache_node_type` - (Optional) A valid cache node type that you want to scale this Global Datastore to. * `engine_version` - (Optional) The upgraded version of the cache engine to be run on the clusters in the Global Datastore. From 02b5f604fa1ee3cf1e396ec1f6f24d377d6d2f55 Mon Sep 17 00:00:00 2001 From: MqllR Date: Tue, 6 Oct 2020 22:18:13 +0200 Subject: [PATCH 08/25] feat: implement the elasticache_global_replication_group resource --- aws/provider.go | 1 + ...ws_elasticache_global_replication_group.go | 469 ++++++++++++++++++ 2 files changed, 470 insertions(+) create mode 100644 aws/resource_aws_elasticache_global_replication_group.go diff --git a/aws/provider.go b/aws/provider.go index c22517dd2b3..1f4ae10d82b 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -618,6 +618,7 @@ func Provider() *schema.Provider { "aws_eks_fargate_profile": resourceAwsEksFargateProfile(), "aws_eks_node_group": resourceAwsEksNodeGroup(), "aws_elasticache_cluster": resourceAwsElasticacheCluster(), + "aws_elasticache_global_replication_group": resourceAwsElasticacheGlobalReplicationGroup(), "aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(), "aws_elasticache_replication_group": resourceAwsElasticacheReplicationGroup(), "aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(), diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go new file mode 100644 index 00000000000..a2210299fb1 --- /dev/null +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -0,0 +1,469 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + elasticacheGlobalReplicationGroupRemovalTimeout = 2 * time.Minute +) + +func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsElasticacheGlobalReplicationGroupCreate, + Read: resourceAwsElasticacheGlobalReplicationGroupRead, + Update: resourceAwsElasticacheGlobalReplicationGroupUpdate, + Delete: resourceAwsElasticacheGlobalReplicationGroupDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "apply_immediately": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "at_rest_encryption_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "auth_token_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "automatic_failover_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "cache_node_type": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "cluster_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "engine": { + Type: schema.TypeString, + Computed: true, + }, + "engine_version": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "global_replication_group_id_suffix": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "global_replication_group_description": { + Type: schema.TypeString, + Optional: true, + Default: false, + }, + "global_replication_group_members": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "replication_group_id": { + Type: schema.TypeString, + Computed: true, + }, + "replication_group_region": { + Type: schema.TypeString, + Computed: true, + }, + "role": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "primary_replication_group_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "retain_primary_replication_group": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "transit_encryption_enabled": { + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func resourceAwsElasticacheGlobalReplicationGroupCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + input := &elasticache.CreateGlobalReplicationGroupInput{ + GlobalReplicationGroupIdSuffix: aws.String(d.Get("global_replication_group_id_suffix").(string)), + PrimaryReplicationGroupId: aws.String(d.Get("primary_replication_group_id").(string)), + } + + if v, ok := d.GetOk("global_replication_group_description"); ok { + input.GlobalReplicationGroupDescription = aws.String(v.(string)) + } + + output, err := conn.CreateGlobalReplicationGroup(input) + if err != nil { + return fmt.Errorf("error creating ElastiCache Global Replication Group: %s", err) + } + + d.SetId(aws.StringValue(output.GlobalReplicationGroup.GlobalReplicationGroupId)) + + if err := waitForElasticacheGlobalReplicationGroupCreation(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) availability: %s", d.Id(), err) + } + + return resourceAwsElasticacheGlobalReplicationGroupRead(d, meta) +} + +func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, d.Id()) + + if isAWSErr(err, elasticache.ErrCodeReplicationGroupNotFoundFault, "") { + log.Printf("[WARN] ElastiCache Global Replication Group (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading ElastiCache Replication Group: %s", err) + } + + if globalReplicationGroup == nil { + log.Printf("[WARN] ElastiCache Global Replication Group (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if aws.StringValue(globalReplicationGroup.Status) == "deleting" || aws.StringValue(globalReplicationGroup.Status) == "deleted" { + log.Printf("[WARN] ElastiCache Global Replication Group (%s) in deleted state (%s), removing from state", d.Id(), aws.StringValue(globalReplicationGroup.Status)) + d.SetId("") + return nil + } + + d.Set("arn", globalReplicationGroup.ARN) + d.Set("at_rest_encryption_enabled", globalReplicationGroup.AtRestEncryptionEnabled) + d.Set("auth_token_enabled", globalReplicationGroup.AuthTokenEnabled) + d.Set("cache_node_type", globalReplicationGroup.CacheNodeType) + d.Set("cluster_enabled", globalReplicationGroup.ClusterEnabled) + d.Set("engine", globalReplicationGroup.Engine) + d.Set("engine_version", globalReplicationGroup.EngineVersion) + d.Set("transit_encryption_enabled", globalReplicationGroup.TransitEncryptionEnabled) + + if err := d.Set("global_replication_group_members", flattenElasticacheGlobalReplicationGroupMembers(globalReplicationGroup.Members)); err != nil { + return fmt.Errorf("error setting global_cluster_members: %w", err) + } + + return nil +} + +func resourceAwsElasticacheGlobalReplicationGroupUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + input := &elasticache.ModifyGlobalReplicationGroupInput{ + ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), + AutomaticFailoverEnabled: aws.Bool(d.Get("automatic_failover_enabled").(bool)), + GlobalReplicationGroupId: aws.String(d.Id()), + } + + requestUpdate := false + if d.HasChange("cache_node_type") { + input.CacheNodeType = aws.String(d.Get("cache_node_type").(string)) + requestUpdate = true + } + + if d.HasChange("engine_version") { + input.EngineVersion = aws.String(d.Get("engine_version").(string)) + requestUpdate = true + } + + if d.HasChange("global_replication_group_description") { + input.GlobalReplicationGroupDescription = aws.String(d.Get("global_replication_group_description").(string)) + requestUpdate = true + } + + if requestUpdate { + _, err := conn.ModifyGlobalReplicationGroup(input) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) + } + + if err := waitForElasticacheGlobalReplicationUpdate(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for ElastiCache Global Replcation Cluster (%s) update: %s", d.Id(), err) + } + } + + return nil +} + +func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).elasticacheconn + + for _, globalReplicationGroupMemberRaw := range d.Get("global_replication_group_members").(*schema.Set).List() { + globalReplicationGroupMember, ok := globalReplicationGroupMemberRaw.(map[string]interface{}) + + if !ok { + continue + } + replicationGroupId, ok := globalReplicationGroupMember["replication_group_id"].(string) + if !ok { + continue + } + + role, ok := globalReplicationGroupMember["role"].(string) + if !ok { + continue + } + + if role == "secondary" { + replicationGroupRegion, ok := globalReplicationGroupMember["replication_group_region"].(string) + if !ok { + continue + } + + input := &elasticache.DisassociateGlobalReplicationGroupInput{ + GlobalReplicationGroupId: aws.String(d.Id()), + ReplicationGroupId: aws.String(replicationGroupId), + ReplicationGroupRegion: aws.String(replicationGroupRegion), + } + + _, err := conn.DisassociateGlobalReplicationGroup(input) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + return nil + } + + /* + if err := waitForElasticacheGlobalReplicationGroupRemoval(conn, replicationGroupId); err != nil { + return fmt.Errorf("error waiting for Elasticache Replication Group (%s) removal from Elasticache Global Replication Group (%s): %w", replicationGroupId, d.Id(), err) + } + */ + } + } + + input := &elasticache.DeleteGlobalReplicationGroupInput{ + GlobalReplicationGroupId: aws.String(d.Id()), + RetainPrimaryReplicationGroup: aws.Bool(d.Get("retain_primary_replication_group").(bool)), + } + + log.Printf("[DEBUG] Deleting ElastiCache Global Replication Group (%s): %s", d.Id(), input) + + err := resource.Retry(1*time.Minute, func() *resource.RetryError { + _, err := conn.DeleteGlobalReplicationGroup(input) + + if isAWSErr(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "is not empty") { + return resource.RetryableError(err) + } + + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + + if isResourceTimeoutError(err) { + _, err = conn.DeleteGlobalReplicationGroup(input) + } + + if isAWSErr(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "") { + return nil + } + + if err != nil { + return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) + } + + if err := waitForElasticacheGlobalReplicationDeletion(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) deletion: %s", d.Id(), err) + } + + return nil +} + +func flattenElasticacheGlobalReplicationGroupMembers(apiObjects []*elasticache.GlobalReplicationGroupMember) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + tfMap := map[string]interface{}{ + "replication_group_id": aws.StringValue(apiObject.ReplicationGroupId), + "replication_group_region": aws.StringValue(apiObject.ReplicationGroupRegion), + "role": aws.StringValue(apiObject.Role), + } + + tfList = append(tfList, tfMap) + } + + return tfList +} + +func elasticacheDescribeGlobalReplicationGroup(conn *elasticache.ElastiCache, globalReplicationGroupID string) (*elasticache.GlobalReplicationGroup, error) { + var globalReplicationGroup *elasticache.GlobalReplicationGroup + + input := &elasticache.DescribeGlobalReplicationGroupsInput{ + GlobalReplicationGroupId: aws.String(globalReplicationGroupID), + } + + log.Printf("[DEBUG] Reading ElastiCache Global Replication Group (%s): %s", globalReplicationGroupID, input) + err := conn.DescribeGlobalReplicationGroupsPages(input, func(page *elasticache.DescribeGlobalReplicationGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, gc := range page.GlobalReplicationGroups { + if gc == nil { + continue + } + + if aws.StringValue(gc.GlobalReplicationGroupId) == globalReplicationGroupID { + globalReplicationGroup = gc + return false + } + } + + return !lastPage + }) + + return globalReplicationGroup, err +} + +func elasticacheGlobalReplicationGroupRefreshFunc(conn *elasticache.ElastiCache, globalReplicationGroupID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, globalReplicationGroupID) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + return nil, "deleted", nil + } + + if err != nil { + return nil, "", fmt.Errorf("error reading ElastiCache Global Replication Group (%s): %s", globalReplicationGroupID, err) + } + + if globalReplicationGroup == nil { + return nil, "deleted", nil + } + + return globalReplicationGroup, aws.StringValue(globalReplicationGroup.Status), nil + } +} + +func waitForElasticacheGlobalReplicationGroupCreation(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"creating"}, + Target: []string{"available", "primary-only"}, + Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), + Timeout: 10 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) availability", globalReplicationGroupID) + _, err := stateConf.WaitForState() + + return err +} + +func waitForElasticacheGlobalReplicationUpdate(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{"modifying"}, + Target: []string{"available"}, + Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), + Timeout: 10 * time.Minute, + } + + log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) availability", globalReplicationGroupID) + _, err := stateConf.WaitForState() + + return err +} + +func waitForElasticacheGlobalReplicationDeletion(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "available", + "deleting", + }, + Target: []string{"deleted"}, + Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), + Timeout: 10 * time.Minute, + NotFoundChecks: 1, + } + + log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) deletion", globalReplicationGroupID) + _, err := stateConf.WaitForState() + + if isResourceNotFoundError(err) { + return nil + } + + return err +} + +/* +func waitForElasticacheReplicationGroupDisassociation(conn *elasticache.ElastiCache, replicationGroupId string) error { + stillExistsErr := fmt.Errorf("ElastiCache Replication Group still associated in ElastiCache Global Replication Group") + + err := resource.Retry(elasticacheGlobalReplicationGroupRemovalTimeout, func() *resource.RetryError { + var err error + + replicationGroup, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, replicationGroupId) + + if err != nil { + return resource.NonRetryableError(err) + } + + if replicationGroup != nil { + return resource.RetryableError(stillExistsErr) + } + + return nil + }) + + if isResourceTimeoutError(err) { + _, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, replicationGroupId) + } + + if err != nil { + return err + } + + if replicationGroup != nil { + return stillExistsErr + } + + return nil +} +*/ From 348260e55c61edaa23b80c210ce8d6b9754b603d Mon Sep 17 00:00:00 2001 From: MqllR Date: Thu, 8 Oct 2020 11:23:00 +0200 Subject: [PATCH 09/25] feat: manage the global replication group deletion --- ...ws_elasticache_global_replication_group.go | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index a2210299fb1..b5584c30017 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -236,7 +236,7 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, if !ok { continue } - replicationGroupId, ok := globalReplicationGroupMember["replication_group_id"].(string) + replicationGroupID, ok := globalReplicationGroupMember["replication_group_id"].(string) if !ok { continue } @@ -246,7 +246,7 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, continue } - if role == "secondary" { + if role == "SECONDARY" { replicationGroupRegion, ok := globalReplicationGroupMember["replication_group_region"].(string) if !ok { continue @@ -254,7 +254,7 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, input := &elasticache.DisassociateGlobalReplicationGroupInput{ GlobalReplicationGroupId: aws.String(d.Id()), - ReplicationGroupId: aws.String(replicationGroupId), + ReplicationGroupId: aws.String(replicationGroupID), ReplicationGroupRegion: aws.String(replicationGroupRegion), } @@ -264,11 +264,9 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, return nil } - /* - if err := waitForElasticacheGlobalReplicationGroupRemoval(conn, replicationGroupId); err != nil { - return fmt.Errorf("error waiting for Elasticache Replication Group (%s) removal from Elasticache Global Replication Group (%s): %w", replicationGroupId, d.Id(), err) - } - */ + if err := waitForElasticacheGlobalReplicationGroupDisassociation(conn, d.Id(), replicationGroupID); err != nil { + return fmt.Errorf("error waiting for Elasticache Replication Group (%s) removal from Elasticache Global Replication Group (%s): %w", replicationGroupID, d.Id(), err) + } } } @@ -337,6 +335,7 @@ func elasticacheDescribeGlobalReplicationGroup(conn *elasticache.ElastiCache, gl input := &elasticache.DescribeGlobalReplicationGroupsInput{ GlobalReplicationGroupId: aws.String(globalReplicationGroupID), + ShowMemberInfo: aws.Bool(true), } log.Printf("[DEBUG] Reading ElastiCache Global Replication Group (%s): %s", globalReplicationGroupID, input) @@ -414,6 +413,8 @@ func waitForElasticacheGlobalReplicationDeletion(conn *elasticache.ElastiCache, stateConf := &resource.StateChangeConf{ Pending: []string{ "available", + "primary-only", + "modifying", "deleting", }, Target: []string{"deleted"}, @@ -432,14 +433,14 @@ func waitForElasticacheGlobalReplicationDeletion(conn *elasticache.ElastiCache, return err } -/* -func waitForElasticacheReplicationGroupDisassociation(conn *elasticache.ElastiCache, replicationGroupId string) error { +func waitForElasticacheGlobalReplicationGroupDisassociation(conn *elasticache.ElastiCache, globalReplicationGroupID string, replicationGroupID string) error { stillExistsErr := fmt.Errorf("ElastiCache Replication Group still associated in ElastiCache Global Replication Group") + var replicationGroup *elasticache.GlobalReplicationGroupMember err := resource.Retry(elasticacheGlobalReplicationGroupRemovalTimeout, func() *resource.RetryError { var err error - replicationGroup, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, replicationGroupId) + replicationGroup, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, globalReplicationGroupID, replicationGroupID) if err != nil { return resource.NonRetryableError(err) @@ -453,7 +454,7 @@ func waitForElasticacheReplicationGroupDisassociation(conn *elasticache.ElastiCa }) if isResourceTimeoutError(err) { - _, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, replicationGroupId) + _, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, globalReplicationGroupID, replicationGroupID) } if err != nil { @@ -466,4 +467,25 @@ func waitForElasticacheReplicationGroupDisassociation(conn *elasticache.ElastiCa return nil } -*/ + +func elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn *elasticache.ElastiCache, globalReplicationGroupID string, replicationGroupID string) (*elasticache.GlobalReplicationGroupMember, error) { + globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, globalReplicationGroupID) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + return nil, err + } + + members := globalReplicationGroup.Members + + if len(members) == 0 { + return nil, nil + } + + for _, member := range members { + if *member.ReplicationGroupId == replicationGroupID { + return member, nil + } + } + + return nil, nil +} From 5e266b60c66454e5f114a57a9f4063cac74d3c4d Mon Sep 17 00:00:00 2001 From: MqllR Date: Thu, 8 Oct 2020 21:36:05 +0200 Subject: [PATCH 10/25] feat: create the base documentation page for elasticache_global_replication_group --- ...che_global_replication_group.html.markdown | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 website/docs/r/elasticache_global_replication_group.html.markdown diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown new file mode 100644 index 00000000000..0ef7bb6b84a --- /dev/null +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "ElastiCache" +layout: "aws" +page_title: "AWS: aws_elasticache_global_replication_group" +description: |- + Provides an ElastiCache Global Replication Group resource. +--- + +# Resource: aws_elasticache_global_replication_group + +Provides an ElastiCache Global Replication Group resource. + +## Example Usage + +### Simple redis global replication group mode cluster disabled + +To create a single shard primary with single read replica: + +```hcl +resource "aws_elasticache_global_replication_group" "replication_group" { + global_replication_group_id_suffix = "example" + primary_replication_group_id = aws_elasticache_replication_group.primary.id +} + +resource "aws_elasticache_replication_group" "primary" { + replication_group_id = "example" + replication_group_description = "test example" + + engine = "redis" + engine_version = "5.0.6" + node_type = "cache.m5.large" + number_cache_clusters = 1 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `global_replication_group_id_suffix` – (Required) The suffix name of a Global Datastore. +* `replication_group_id` – (Required) The replication group identifier. The Global Datastore will be created from this replication group. +* `global_replication_group_description` – (Optional) A user-created description for the global replication group. +* `retain_primary_replication_group` - (Optional) Whether to retain the primary replication group when the global replication group is deleted. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the ElastiCache Global Replication Group. + +## Import + +ElastiCache Global Replication Groups can be imported using the `global_replication_group_id`, e.g. + +``` +$ terraform import aws_elasticache_global_replication_group.my_global_replication_group global-replication-group-1 +``` From adc8f6baf3e993ed26a4b70dfd55424871b246ad Mon Sep 17 00:00:00 2001 From: MqllR Date: Tue, 13 Oct 2020 17:54:14 +0200 Subject: [PATCH 11/25] feat: add basic acceptance test and initiate doc --- ...ws_elasticache_global_replication_group.go | 10 +- ...asticache_global_replication_group_test.go | 203 ++++++++++++++++++ ...che_global_replication_group.html.markdown | 4 +- 3 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 aws/resource_aws_elasticache_global_replication_group_test.go diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index b5584c30017..c1c5071e46a 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -145,7 +145,7 @@ func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, me globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, d.Id()) - if isAWSErr(err, elasticache.ErrCodeReplicationGroupNotFoundFault, "") { + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { log.Printf("[WARN] ElastiCache Global Replication Group (%s) not found, removing from state", d.Id()) d.SetId("") return nil @@ -219,7 +219,7 @@ func resourceAwsElasticacheGlobalReplicationGroupUpdate(d *schema.ResourceData, return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) } - if err := waitForElasticacheGlobalReplicationUpdate(conn, d.Id()); err != nil { + if err := waitForElasticacheGlobalReplicationGroupUpdate(conn, d.Id()); err != nil { return fmt.Errorf("error waiting for ElastiCache Global Replcation Cluster (%s) update: %s", d.Id(), err) } } @@ -303,7 +303,7 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) } - if err := waitForElasticacheGlobalReplicationDeletion(conn, d.Id()); err != nil { + if err := waitForElasticacheGlobalReplicationGroupDeletion(conn, d.Id()); err != nil { return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) deletion: %s", d.Id(), err) } @@ -395,7 +395,7 @@ func waitForElasticacheGlobalReplicationGroupCreation(conn *elasticache.ElastiCa return err } -func waitForElasticacheGlobalReplicationUpdate(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { +func waitForElasticacheGlobalReplicationGroupUpdate(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { stateConf := &resource.StateChangeConf{ Pending: []string{"modifying"}, Target: []string{"available"}, @@ -409,7 +409,7 @@ func waitForElasticacheGlobalReplicationUpdate(conn *elasticache.ElastiCache, gl return err } -func waitForElasticacheGlobalReplicationDeletion(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { +func waitForElasticacheGlobalReplicationGroupDeletion(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { stateConf := &resource.StateChangeConf{ Pending: []string{ "available", diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go new file mode 100644 index 00000000000..defb1d66f6f --- /dev/null +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -0,0 +1,203 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func init() { + resource.AddTestSweepers("aws_elasticache_global_replication_group", &resource.Sweeper{ + Name: "aws_elasticache_global_replication_group", + F: testSweepElasticacheGlobalReplicationGroups, + Dependencies: []string{ + "aws_elasticache_replication_group", + }, + }) +} + +func testSweepElasticacheGlobalReplicationGroups(region string) error { + client, err := sharedClientForRegion(region) + + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + conn := client.(*AWSClient).elasticacheconn + input := &elasticache.DescribeGlobalReplicationGroupsInput{} + + err = conn.DescribeGlobalReplicationGroupsPages(input, func(out *elasticache.DescribeGlobalReplicationGroupsOutput, lastPage bool) bool { + for _, globalReplicationGroup := range out.GlobalReplicationGroups { + id := aws.StringValue(globalReplicationGroup.GlobalReplicationGroupId) + input := &elasticache.DeleteGlobalReplicationGroupInput{ + GlobalReplicationGroupId: globalReplicationGroup.GlobalReplicationGroupId, + } + + log.Printf("[INFO] Deleting Elasticache Global Replication Group: %s", id) + + _, err := conn.DeleteGlobalReplicationGroup(input) + + if err != nil { + log.Printf("[ERROR] Failed to delete ElastiCache Global Replication Group (%s): %s", id, err) + continue + } + + if err := waitForElasticacheGlobalReplicationGroupDeletion(conn, id); err != nil { + log.Printf("[ERROR] Failure while waiting for ElastiCache Global Replication Group (%s) to be deleted: %s", id, err) + } + } + return !lastPage + }) + + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping ElastiCache Global Replication Group sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error retrieving ElastiCache Global Replication Groups: %s", err) + } + + return nil +} + +func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { + var globalReplcationGroup1 elasticache.GlobalReplicationGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_elasticache_global_replication_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSElasticacheGlobalReplicationGroup(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheGlobalReplicationGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheGlobalReplicationGroupConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup1), + testAccCheckResourceAttrGlobalARN(resourceName, "arn", "elasticache", fmt.Sprintf("global-replication-group:%s", rName)), + resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", ""), + resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), + resource.TestCheckResourceAttrSet(resourceName, "automatic_failover_enabled"), + resource.TestCheckResourceAttrSet(resourceName, "cache_node_type"), + resource.TestCheckResourceAttr(resourceName, "cluster_enabled", rName), + resource.TestCheckResourceAttr(resourceName, "engine", "redis"), + resource.TestCheckResourceAttr(resourceName, "engine_version", "5.0.6"), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_id_suffix", rName), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", "false"), + resource.TestCheckResourceAttrSet(resourceName, "global_replication_group_members"), + resource.TestCheckResourceAttr(resourceName, "primary_replication_group_id", rName), + resource.TestCheckResourceAttr(resourceName, "retain_primary_replication_group", "true"), + resource.TestCheckResourceAttrSet(resourceName, "transit_encryption_enabled"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, globalReplicationGroup *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Elasticache Global Replication Group ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + cluster, err := elasticacheDescribeGlobalReplicationGroup(conn, rs.Primary.ID) + + if err != nil { + return err + } + + if cluster == nil { + return fmt.Errorf("Elasticache Global Replication Group not found") + } + + if aws.StringValue(cluster.Status) != "available" && aws.StringValue(cluster.Status) != "primary-only" { + return fmt.Errorf("Elasticache Global Replication Group (%s) exists in non-available (%s) state", rs.Primary.ID, aws.StringValue(cluster.Status)) + } + + *globalReplicationGroup = *cluster + + return nil + } +} + +func testAccCheckAWSElasticacheGlobalReplicationGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_elasticache_global_replication_group" { + continue + } + + globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, rs.Primary.ID) + + if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + continue + } + + if err != nil { + return err + } + + if globalReplicationGroup == nil { + continue + } + + return fmt.Errorf("Elasticache Global Replication Group (%s) still exists in non-deleted (%s) state", rs.Primary.ID, aws.StringValue(globalReplicationGroup.Status)) + } + + return nil +} + +func testAccPreCheckAWSElasticacheGlobalReplicationGroup(t *testing.T) { + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + input := &elasticache.DescribeGlobalReplicationGroupsInput{} + + _, err := conn.DescribeGlobalReplicationGroups(input) + + if testAccPreCheckSkipError(err) || isAWSErr(err, "InvalidParameterValue", "Access Denied to API Version: APIGlobalDatastore") { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccAWSElasticacheGlobalReplicationGroupConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_elasticache_global_replication_group" "test" { + global_replication_group_id_suffix = %q + primary_replication_group_id = aws_elasticache_replication_group.test.id +} + +resource "aws_elasticache_replication_group" "test" { + replication_group_id = %q + replication_group_description = "test" + + engine = "redis" + engine_version = "5.0.6" + node_type = "cache.m5.large" + number_cache_clusters = 1 +} +`, rName, rName) +} diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 0ef7bb6b84a..0bed96da135 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -18,8 +18,8 @@ To create a single shard primary with single read replica: ```hcl resource "aws_elasticache_global_replication_group" "replication_group" { - global_replication_group_id_suffix = "example" - primary_replication_group_id = aws_elasticache_replication_group.primary.id + global_replication_group_id_suffix = "example" + primary_replication_group_id = aws_elasticache_replication_group.primary.id } resource "aws_elasticache_replication_group" "primary" { From e362f58953ce1bc5d4d86b8b1035afd0a685216b Mon Sep 17 00:00:00 2001 From: MqllR Date: Fri, 16 Oct 2020 17:25:01 +0200 Subject: [PATCH 12/25] feat: improve doc resource page and fix fmt test --- ...ws_elasticache_global_replication_group.go | 10 ++++++--- ...asticache_global_replication_group_test.go | 4 ++-- ...che_global_replication_group.html.markdown | 22 +++++++++++++++---- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index c1c5071e46a..a6ab93b8179 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -28,8 +28,7 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { Schema: map[string]*schema.Schema{ "apply_immediately": { Type: schema.TypeBool, - Optional: true, - Computed: true, + Required: true, }, "arn": { Type: schema.TypeString, @@ -188,11 +187,16 @@ func resourceAwsElasticacheGlobalReplicationGroupUpdate(d *schema.ResourceData, input := &elasticache.ModifyGlobalReplicationGroupInput{ ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), - AutomaticFailoverEnabled: aws.Bool(d.Get("automatic_failover_enabled").(bool)), GlobalReplicationGroupId: aws.String(d.Id()), } requestUpdate := false + + if d.HasChange("automatic_failover_enabled") { + input.AutomaticFailoverEnabled = aws.Bool(d.Get("automatic_failover_enabled").(bool)) + requestUpdate = true + } + if d.HasChange("cache_node_type") { input.CacheNodeType = aws.String(d.Get("cache_node_type").(string)) requestUpdate = true diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index defb1d66f6f..ff0450a554c 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -186,8 +186,8 @@ func testAccPreCheckAWSElasticacheGlobalReplicationGroup(t *testing.T) { func testAccAWSElasticacheGlobalReplicationGroupConfig(rName string) string { return fmt.Sprintf(` resource "aws_elasticache_global_replication_group" "test" { - global_replication_group_id_suffix = %q - primary_replication_group_id = aws_elasticache_replication_group.test.id + global_replication_group_id_suffix = %q + primary_replication_group_id = aws_elasticache_replication_group.test.id } resource "aws_elasticache_replication_group" "test" { diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 0bed96da135..51388fa79f7 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -8,11 +8,11 @@ description: |- # Resource: aws_elasticache_global_replication_group -Provides an ElastiCache Global Replication Group resource. +Provides an ElastiCache Global Replication Group resource, which manage a replication between 2 or more redis replication group in different region. ## Example Usage -### Simple redis global replication group mode cluster disabled +### Global replication group with a single instance redis replication group To create a single shard primary with single read replica: @@ -38,20 +38,34 @@ resource "aws_elasticache_replication_group" "primary" { The following arguments are supported: * `global_replication_group_id_suffix` – (Required) The suffix name of a Global Datastore. -* `replication_group_id` – (Required) The replication group identifier. The Global Datastore will be created from this replication group. +* `primary_replication_group_id` – (Required) The name of the primary cluster that accepts writes and will replicate updates to the secondary cluster. * `global_replication_group_description` – (Optional) A user-created description for the global replication group. * `retain_primary_replication_group` - (Optional) Whether to retain the primary replication group when the global replication group is deleted. +* `apply_immediately` - (Required) This parameter causes the modifications in this request and any pending modifications to be applied, asynchronously and as soon as possible. Modifications to Global Replication Groups cannot be requested to be applied in PreferredMaintenceWindow. +* `automatic_failover_enabled` - (Optional) Determines whether a read replica is automatically promoted to read/write primary if the existing primary encounters a failure. +* `cache_node_type` - (Optional) A valid cache node type that you want to scale this Global Datastore to. +* `engine_version` - (Optional) The upgraded version of the cache engine to be run on the clusters in the Global Datastore. ## Attributes Reference In addition to all arguments above, the following attributes are exported: * `id` - The ID of the ElastiCache Global Replication Group. +* `arn` - The ARN of the ElastiCache Global Replication Group. +* `at_rest_encryption_enabled` - A flag that indicate wheter the encryption at rest is enabled. +* `auth_token_enabled` - A flag that indicate wheter AuthToken (password) is enabled. +* `cluster_enabled` - A flag that indicates whether the Global Datastore is cluster enabled. +* `engine` - The Elasticache engine. For redis only +* `global_replication_group_members` - The identifiers of all the replication group members that are part of this global replication group. + * `replication_group_id` - The replication group id of the Global Datastore member + * `replication_group_region` - The AWS region of the Global Datastore member + * `role` - Indicates the role of the replication group, primary or secondary +* `transit_encryption_enabled` - A flag that indicates whether the encryption in transit is enabled. ## Import ElastiCache Global Replication Groups can be imported using the `global_replication_group_id`, e.g. ``` -$ terraform import aws_elasticache_global_replication_group.my_global_replication_group global-replication-group-1 +$ terraform import aws_elasticache_global_replication_group.my_global_replication_group okuqm-global-replication-group-1 ``` From 19996bda99c9f7a56c7096e024ac3d58dbc67277 Mon Sep 17 00:00:00 2001 From: MqllR Date: Fri, 16 Oct 2020 17:28:27 +0200 Subject: [PATCH 13/25] fix misspell words --- .../docs/r/elasticache_global_replication_group.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 51388fa79f7..d60320f46fe 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -52,8 +52,8 @@ In addition to all arguments above, the following attributes are exported: * `id` - The ID of the ElastiCache Global Replication Group. * `arn` - The ARN of the ElastiCache Global Replication Group. -* `at_rest_encryption_enabled` - A flag that indicate wheter the encryption at rest is enabled. -* `auth_token_enabled` - A flag that indicate wheter AuthToken (password) is enabled. +* `at_rest_encryption_enabled` - A flag that indicate whether the encryption at rest is enabled. +* `auth_token_enabled` - A flag that indicate whether AuthToken (password) is enabled. * `cluster_enabled` - A flag that indicates whether the Global Datastore is cluster enabled. * `engine` - The Elasticache engine. For redis only * `global_replication_group_members` - The identifiers of all the replication group members that are part of this global replication group. From 6de6f93fe0e1b95242eb8181aa43189a3e355c02 Mon Sep 17 00:00:00 2001 From: MqllR Date: Wed, 21 Oct 2020 18:56:38 +0200 Subject: [PATCH 14/25] add disappears tests --- ...ws_elasticache_global_replication_group.go | 3 +- ...asticache_global_replication_group_test.go | 57 ++++++++++++++++--- ...che_global_replication_group.html.markdown | 2 +- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index a6ab93b8179..48a2bf7d52a 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -28,7 +28,8 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { Schema: map[string]*schema.Schema{ "apply_immediately": { Type: schema.TypeBool, - Required: true, + Optional: true, + Default: true, }, "arn": { Type: schema.TypeString, diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index ff0450a554c..8cdf4c4bdb0 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -3,6 +3,7 @@ package aws import ( "fmt" "log" + "regexp" "testing" "github.com/aws/aws-sdk-go/aws" @@ -81,20 +82,19 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { Config: testAccAWSElasticacheGlobalReplicationGroupConfig(rName), Check: resource.ComposeTestCheckFunc( testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup1), - testAccCheckResourceAttrGlobalARN(resourceName, "arn", "elasticache", fmt.Sprintf("global-replication-group:%s", rName)), - resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", ""), + testAccMatchResourceAttrGlobalARN(resourceName, "arn", "elasticache", regexp.MustCompile(`globalreplicationgroup:\w{5}-`+rName)), // \w{5} is the AWS prefix + resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), - resource.TestCheckResourceAttrSet(resourceName, "automatic_failover_enabled"), - resource.TestCheckResourceAttrSet(resourceName, "cache_node_type"), - resource.TestCheckResourceAttr(resourceName, "cluster_enabled", rName), + resource.TestCheckResourceAttr(resourceName, "automatic_failover_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "cache_node_type", "cache.m5.large"), + resource.TestCheckResourceAttr(resourceName, "cluster_enabled", "false"), resource.TestCheckResourceAttr(resourceName, "engine", "redis"), resource.TestCheckResourceAttr(resourceName, "engine_version", "5.0.6"), resource.TestCheckResourceAttr(resourceName, "global_replication_group_id_suffix", rName), - resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", "false"), - resource.TestCheckResourceAttrSet(resourceName, "global_replication_group_members"), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", "0"), resource.TestCheckResourceAttr(resourceName, "primary_replication_group_id", rName), resource.TestCheckResourceAttr(resourceName, "retain_primary_replication_group", "true"), - resource.TestCheckResourceAttrSet(resourceName, "transit_encryption_enabled"), + resource.TestCheckResourceAttr(resourceName, "transit_encryption_enabled", "false"), ), }, { @@ -106,6 +106,28 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { }) } +func TestAccAWSElasticacheGlobalReplicationGroup_disappears(t *testing.T) { + var globalReplcationGroup1 elasticache.GlobalReplicationGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_elasticache_global_replication_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSElasticacheGlobalReplicationGroup(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheGlobalReplicationGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheGlobalReplicationGroupConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup1), + testAccCheckAWSElasticacheGlobalReplicationGroupDisappears(&globalReplcationGroup1), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, globalReplicationGroup *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -139,6 +161,25 @@ func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, } } +func testAccCheckAWSElasticacheGlobalReplicationGroupDisappears(globalReplicationGroup *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + input := &elasticache.DeleteGlobalReplicationGroupInput{ + GlobalReplicationGroupId: globalReplicationGroup.GlobalReplicationGroupId, + RetainPrimaryReplicationGroup: aws.Bool(true), + } + + _, err := conn.DeleteGlobalReplicationGroup(input) + + if err != nil { + return err + } + + return waitForElasticacheGlobalReplicationGroupDeletion(conn, aws.StringValue(globalReplicationGroup.GlobalReplicationGroupId)) + } +} + func testAccCheckAWSElasticacheGlobalReplicationGroupDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).elasticacheconn diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index d60320f46fe..87c94f05fbb 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -41,7 +41,7 @@ The following arguments are supported: * `primary_replication_group_id` – (Required) The name of the primary cluster that accepts writes and will replicate updates to the secondary cluster. * `global_replication_group_description` – (Optional) A user-created description for the global replication group. * `retain_primary_replication_group` - (Optional) Whether to retain the primary replication group when the global replication group is deleted. -* `apply_immediately` - (Required) This parameter causes the modifications in this request and any pending modifications to be applied, asynchronously and as soon as possible. Modifications to Global Replication Groups cannot be requested to be applied in PreferredMaintenceWindow. +* `apply_immediately` - (Optional) This parameter causes the modifications in this request and any pending modifications to be applied, asynchronously and as soon as possible. Modifications to Global Replication Groups cannot be requested to be applied in PreferredMaintenceWindow. Default to true. * `automatic_failover_enabled` - (Optional) Determines whether a read replica is automatically promoted to read/write primary if the existing primary encounters a failure. * `cache_node_type` - (Optional) A valid cache node type that you want to scale this Global Datastore to. * `engine_version` - (Optional) The upgraded version of the cache engine to be run on the clusters in the Global Datastore. From cc7380fe35a312ffa1a088e15557306d80d312ef Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 11 Feb 2021 13:21:30 -0800 Subject: [PATCH 15/25] Fixes acceptance tests and moves waiter functions to service waiter package --- .../service/elasticache/finder/finder.go | 66 ++- .../service/elasticache/waiter/status.go | 30 +- .../service/elasticache/waiter/waiter.go | 66 ++- ...ws_elasticache_global_replication_group.go | 422 +++++------------- ...asticache_global_replication_group_test.go | 217 +++++---- .../docs/r/elasticache_cluster.html.markdown | 4 +- ...che_global_replication_group.html.markdown | 26 +- ...lasticache_replication_group.html.markdown | 2 +- 8 files changed, 414 insertions(+), 419 deletions(-) diff --git a/aws/internal/service/elasticache/finder/finder.go b/aws/internal/service/elasticache/finder/finder.go index 6491028c7d1..f794edb5611 100644 --- a/aws/internal/service/elasticache/finder/finder.go +++ b/aws/internal/service/elasticache/finder/finder.go @@ -1,6 +1,8 @@ package finder import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/aws-sdk-go-base/tfawserr" @@ -12,7 +14,7 @@ func ReplicationGroupByID(conn *elasticache.ElastiCache, id string) (*elasticach input := &elasticache.DescribeReplicationGroupsInput{ ReplicationGroupId: aws.String(id), } - result, err := conn.DescribeReplicationGroups(input) + output, err := conn.DescribeReplicationGroups(input) if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeReplicationGroupNotFoundFault) { return nil, &resource.NotFoundError{ LastError: err, @@ -23,23 +25,21 @@ func ReplicationGroupByID(conn *elasticache.ElastiCache, id string) (*elasticach return nil, err } - if result == nil || len(result.ReplicationGroups) == 0 || result.ReplicationGroups[0] == nil { + if output == nil || len(output.ReplicationGroups) == 0 || output.ReplicationGroups[0] == nil { return nil, &resource.NotFoundError{ Message: "Empty result", LastRequest: input, } } - return result.ReplicationGroups[0], nil + return output.ReplicationGroups[0], nil } // ReplicationGroupMemberClustersByID retrieves all of an ElastiCache Replication Group's MemberClusters by the id of the Replication Group. func ReplicationGroupMemberClustersByID(conn *elasticache.ElastiCache, id string) ([]*elasticache.CacheCluster, error) { - var results []*elasticache.CacheCluster - rg, err := ReplicationGroupByID(conn, id) if err != nil { - return results, err + return []*elasticache.CacheCluster{}, err } clusters, err := CacheClustersByID(conn, aws.StringValueSlice(rg.MemberClusters)) @@ -125,3 +125,57 @@ func CacheClustersByID(conn *elasticache.ElastiCache, idList []string) ([]*elast return results, err } + +// GlobalReplicationGroupByID() retrieves an ElastiCache Global Replication Group by id. +func GlobalReplicationGroupByID(conn *elasticache.ElastiCache, id string) (*elasticache.GlobalReplicationGroup, error) { + input := &elasticache.DescribeGlobalReplicationGroupsInput{ + GlobalReplicationGroupId: aws.String(id), + ShowMemberInfo: aws.Bool(true), + } + output, err := conn.DescribeGlobalReplicationGroups(input) + if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { + return nil, err + } + + if output == nil || len(output.GlobalReplicationGroups) == 0 || output.GlobalReplicationGroups[0] == nil { + return nil, &resource.NotFoundError{ + Message: "empty result", + LastRequest: input, + } + } + + return output.GlobalReplicationGroups[0], nil +} + +// GlobalReplicationGroupMemberByID retrieves a member Replication Group by id from a Global Replication Group. +func GlobalReplicationGroupMemberByID(conn *elasticache.ElastiCache, globalReplicationGroupID string, id string) (*elasticache.GlobalReplicationGroupMember, error) { + globalReplicationGroup, err := GlobalReplicationGroupByID(conn, globalReplicationGroupID) + if err != nil { + return nil, &resource.NotFoundError{ + Message: "unable to retrieve enclosing Global Replication Group", + LastError: err, + } + } + + if len(globalReplicationGroup.Members) == 0 { + return nil, &resource.NotFoundError{ + Message: "empty result", + } + } + + for _, member := range globalReplicationGroup.Members { + if aws.StringValue(member.ReplicationGroupId) == id { + return member, nil + } + } + + return nil, &resource.NotFoundError{ + Message: fmt.Sprintf("Replication Group %q not found in Global Replication Group %q", id, globalReplicationGroupID), + } +} diff --git a/aws/internal/service/elasticache/waiter/status.go b/aws/internal/service/elasticache/waiter/status.go index d9bd1e44258..73af9a4d94b 100644 --- a/aws/internal/service/elasticache/waiter/status.go +++ b/aws/internal/service/elasticache/waiter/status.go @@ -17,7 +17,7 @@ const ( ReplicationGroupStatusSnapshotting = "snapshotting" ) -// ReplicationGroupStatus fetches the ReplicationGroup and its Status +// ReplicationGroupStatus fetches the Replication Group and its Status func ReplicationGroupStatus(conn *elasticache.ElastiCache, replicationGroupID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { rg, err := finder.ReplicationGroupByID(conn, replicationGroupID) @@ -32,7 +32,7 @@ func ReplicationGroupStatus(conn *elasticache.ElastiCache, replicationGroupID st } } -// ReplicationGroupMemberClustersStatus fetches the ReplicationGroup's Member Clusters and either "available" or the first non-"available" status. +// ReplicationGroupMemberClustersStatus fetches the Replication Group's Member Clusters and either "available" or the first non-"available" status. // NOTE: This function assumes that the intended end-state is to have all member clusters in "available" status. func ReplicationGroupMemberClustersStatus(conn *elasticache.ElastiCache, replicationGroupID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { @@ -68,7 +68,7 @@ const ( CacheClusterStatusSnapshotting = "snapshotting" ) -// CacheClusterStatus fetches the CacheCluster and its Status +// CacheClusterStatus fetches the Cache Cluster and its Status func CacheClusterStatus(conn *elasticache.ElastiCache, cacheClusterID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { c, err := finder.CacheClusterByID(conn, cacheClusterID) @@ -82,3 +82,27 @@ func CacheClusterStatus(conn *elasticache.ElastiCache, cacheClusterID string) re return c, aws.StringValue(c.CacheClusterStatus), nil } } + +const ( + GlobalReplicationGroupStatusAvailable = "available" + GlobalReplicationGroupStatusCreating = "creating" + GlobalReplicationGroupStatusModifying = "modifying" + GlobalReplicationGroupStatusPrimaryOnly = "primary-only" + GlobalReplicationGroupStatusDeleting = "deleting" + GlobalReplicationGroupStatusDeleted = "deleted" +) + +// GlobalReplicationGroupStatus fetches the Global Replication Group and its Status +func GlobalReplicationGroupStatus(conn *elasticache.ElastiCache, globalReplicationGroupID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + grg, err := finder.GlobalReplicationGroupByID(conn, globalReplicationGroupID) + if tfresource.NotFound(err) { + return nil, "", nil + } + if err != nil { + return nil, "", err + } + + return grg, aws.StringValue(grg.Status), nil + } +} diff --git a/aws/internal/service/elasticache/waiter/waiter.go b/aws/internal/service/elasticache/waiter/waiter.go index 83c95788e2d..002a5ee2804 100644 --- a/aws/internal/service/elasticache/waiter/waiter.go +++ b/aws/internal/service/elasticache/waiter/waiter.go @@ -97,8 +97,8 @@ const ( cacheClusterDeletedDelay = 30 * time.Second ) -// CacheClusterAvailable waits for a ReplicationGroup to return Available -func CacheClusterAvailable(conn *elasticache.ElastiCache, cacheClusterID string, timeout time.Duration) (*elasticache.ReplicationGroup, error) { +// CacheClusterAvailable waits for a Cache Cluster to return Available +func CacheClusterAvailable(conn *elasticache.ElastiCache, cacheClusterID string, timeout time.Duration) (*elasticache.CacheCluster, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ CacheClusterStatusCreating, @@ -114,14 +114,14 @@ func CacheClusterAvailable(conn *elasticache.ElastiCache, cacheClusterID string, } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*elasticache.ReplicationGroup); ok { + if v, ok := outputRaw.(*elasticache.CacheCluster); ok { return v, err } return nil, err } -// CacheClusterDeleted waits for a ReplicationGroup to be deleted -func CacheClusterDeleted(conn *elasticache.ElastiCache, cacheClusterID string, timeout time.Duration) (*elasticache.ReplicationGroup, error) { +// CacheClusterDeleted waits for a Cache Cluster to be deleted +func CacheClusterDeleted(conn *elasticache.ElastiCache, cacheClusterID string, timeout time.Duration) (*elasticache.CacheCluster, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ CacheClusterStatusCreating, @@ -140,7 +140,61 @@ func CacheClusterDeleted(conn *elasticache.ElastiCache, cacheClusterID string, t } outputRaw, err := stateConf.WaitForState() - if v, ok := outputRaw.(*elasticache.ReplicationGroup); ok { + if v, ok := outputRaw.(*elasticache.CacheCluster); ok { + return v, err + } + return nil, err +} + +const ( + GlobalReplicationGroupDefaultCreatedTimeout = 20 * time.Minute + GlobalReplicationGroupDefaultUpdatedTimeout = ReplicationGroupDefaultUpdatedTimeout + GlobalReplicationGroupDefaultDeletedTimeout = 20 * time.Minute + + globalReplicationGroupAvailableMinTimeout = 10 * time.Second + globalReplicationGroupAvailableDelay = 30 * time.Second + + globalReplicationGroupDeletedMinTimeout = 10 * time.Second + globalReplicationGroupDeletedDelay = 30 * time.Second +) + +// GlobalReplicationGroupAvailable waits for a Global Replication Group to be available, +// with status either "available" or "primary-only" +func GlobalReplicationGroupAvailable(conn *elasticache.ElastiCache, globalReplicationGroupID string, timeout time.Duration) (*elasticache.GlobalReplicationGroup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{GlobalReplicationGroupStatusCreating, GlobalReplicationGroupStatusModifying}, + Target: []string{GlobalReplicationGroupStatusAvailable, GlobalReplicationGroupStatusPrimaryOnly}, + Refresh: GlobalReplicationGroupStatus(conn, globalReplicationGroupID), + Timeout: timeout, + MinTimeout: globalReplicationGroupAvailableMinTimeout, + Delay: globalReplicationGroupAvailableDelay, + } + + outputRaw, err := stateConf.WaitForState() + if v, ok := outputRaw.(*elasticache.GlobalReplicationGroup); ok { + return v, err + } + return nil, err +} + +// GlobalReplicationGroupDeleted waits for a Global Replication Group to be deleted +func GlobalReplicationGroupDeleted(conn *elasticache.ElastiCache, globalReplicationGroupID string) (*elasticache.GlobalReplicationGroup, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + GlobalReplicationGroupStatusAvailable, + GlobalReplicationGroupStatusPrimaryOnly, + GlobalReplicationGroupStatusModifying, + GlobalReplicationGroupStatusDeleting, + }, + Target: []string{}, + Refresh: GlobalReplicationGroupStatus(conn, globalReplicationGroupID), + Timeout: GlobalReplicationGroupDefaultDeletedTimeout, + MinTimeout: globalReplicationGroupDeletedMinTimeout, + Delay: globalReplicationGroupDeletedDelay, + } + + outputRaw, err := stateConf.WaitForState() + if v, ok := outputRaw.(*elasticache.GlobalReplicationGroup); ok { return v, err } return nil, err diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index 48a2bf7d52a..9289bb519cb 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -3,16 +3,27 @@ package aws import ( "fmt" "log" - "time" + "regexp" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( - elasticacheGlobalReplicationGroupRemovalTimeout = 2 * time.Minute + elasticacheEmptyDescription = " " +) + +const ( + elasticacheGlobalReplicationGroupRegionPrefixFormat = "[[:alpha:]]{5}-" +) + +const ( + GlobalReplicationGroupMemberRolePrimary = "PRIMARY" + GlobalReplicationGroupMemberRoleSecondary = "SECONDARY" ) func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { @@ -22,15 +33,15 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { Update: resourceAwsElasticacheGlobalReplicationGroupUpdate, Delete: resourceAwsElasticacheGlobalReplicationGroupDelete, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + re := regexp.MustCompile("^" + elasticacheGlobalReplicationGroupRegionPrefixFormat) + d.Set("global_replication_group_id_suffix", re.ReplaceAllLiteralString(d.Id(), "")) + + return []*schema.ResourceData{d}, nil + }, }, Schema: map[string]*schema.Schema{ - "apply_immediately": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, "arn": { Type: schema.TypeString, Computed: true, @@ -43,15 +54,9 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { Type: schema.TypeBool, Computed: true, }, - "automatic_failover_enabled": { - Type: schema.TypeBool, - Optional: true, - Default: false, - }, "cache_node_type": { Type: schema.TypeString, Computed: true, - Optional: true, }, "cluster_enabled": { Type: schema.TypeBool, @@ -61,10 +66,18 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "engine_version": { + // Leaving space for `engine_version` for creation and updating. + // `engine_version` cannot be used for returning the version because, starting with Redis 6, + // version configuration is major-version-only: `engine_version = "6.x"`, while `actual_engine_version` + // will be e.g. `6.0.5` + // See also https://github.com/hashicorp/terraform-provider-aws/issues/15625 + "actual_engine_version": { + Type: schema.TypeString, + Computed: true, + }, + "global_replication_group_id": { Type: schema.TypeString, Computed: true, - Optional: true, }, "global_replication_group_id_suffix": { Type: schema.TypeString, @@ -72,9 +85,10 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { ForceNew: true, }, "global_replication_group_description": { - Type: schema.TypeString, - Optional: true, - Default: false, + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: elasticacheDescriptionDiffSuppress, + StateFunc: elasticacheDescriptionStateFunc, }, "global_replication_group_members": { Type: schema.TypeSet, @@ -101,11 +115,6 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { Required: true, ForceNew: true, }, - "retain_primary_replication_group": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, "transit_encryption_enabled": { Type: schema.TypeBool, Computed: true, @@ -114,6 +123,21 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { } } +func elasticacheDescriptionDiffSuppress(_, old, new string, d *schema.ResourceData) bool { + if (old == elasticacheEmptyDescription && new == "") || (old == "" && new == elasticacheEmptyDescription) { + return true + } + return false +} + +func elasticacheDescriptionStateFunc(v interface{}) string { + s := v.(string) + if s == "" { + return elasticacheEmptyDescription + } + return s +} + func resourceAwsElasticacheGlobalReplicationGroupCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn @@ -128,13 +152,13 @@ func resourceAwsElasticacheGlobalReplicationGroupCreate(d *schema.ResourceData, output, err := conn.CreateGlobalReplicationGroup(input) if err != nil { - return fmt.Errorf("error creating ElastiCache Global Replication Group: %s", err) + return fmt.Errorf("error creating ElastiCache Global Replication Group: %w", err) } d.SetId(aws.StringValue(output.GlobalReplicationGroup.GlobalReplicationGroupId)) - if err := waitForElasticacheGlobalReplicationGroupCreation(conn, d.Id()); err != nil { - return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) availability: %s", d.Id(), err) + if _, err := waiter.GlobalReplicationGroupAvailable(conn, d.Id(), waiter.GlobalReplicationGroupDefaultCreatedTimeout); err != nil { + return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) availability: %w", d.Id(), err) } return resourceAwsElasticacheGlobalReplicationGroupRead(d, meta) @@ -143,22 +167,14 @@ func resourceAwsElasticacheGlobalReplicationGroupCreate(d *schema.ResourceData, func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn - globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, d.Id()) - - if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + globalReplicationGroup, err := finder.GlobalReplicationGroupByID(conn, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] ElastiCache Global Replication Group (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - if err != nil { - return fmt.Errorf("error reading ElastiCache Replication Group: %s", err) - } - - if globalReplicationGroup == nil { - log.Printf("[WARN] ElastiCache Global Replication Group (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil + return fmt.Errorf("error reading ElastiCache Replication Group: %w", err) } if aws.StringValue(globalReplicationGroup.Status) == "deleting" || aws.StringValue(globalReplicationGroup.Status) == "deleted" { @@ -173,9 +189,13 @@ func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, me d.Set("cache_node_type", globalReplicationGroup.CacheNodeType) d.Set("cluster_enabled", globalReplicationGroup.ClusterEnabled) d.Set("engine", globalReplicationGroup.Engine) - d.Set("engine_version", globalReplicationGroup.EngineVersion) + d.Set("actual_engine_version", globalReplicationGroup.EngineVersion) + d.Set("global_replication_group_description", globalReplicationGroup.GlobalReplicationGroupDescription) + d.Set("global_replication_group_id", globalReplicationGroup.GlobalReplicationGroupId) d.Set("transit_encryption_enabled", globalReplicationGroup.TransitEncryptionEnabled) + d.Set("primary_replication_group_id", flattenElasticacheGlobalReplicationGroupPrimaryGroupID(globalReplicationGroup.Members)) + if err := d.Set("global_replication_group_members", flattenElasticacheGlobalReplicationGroupMembers(globalReplicationGroup.Members)); err != nil { return fmt.Errorf("error setting global_cluster_members: %w", err) } @@ -186,47 +206,40 @@ func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, me func resourceAwsElasticacheGlobalReplicationGroupUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn - input := &elasticache.ModifyGlobalReplicationGroupInput{ - ApplyImmediately: aws.Bool(d.Get("apply_immediately").(bool)), - GlobalReplicationGroupId: aws.String(d.Id()), + // Only one field can be changed per request + updaters := map[string]elasticacheGlobalReplicationGroupUpdater{} + if !d.IsNewResource() { + updaters["global_replication_group_description"] = func(input *elasticache.ModifyGlobalReplicationGroupInput) { + input.GlobalReplicationGroupDescription = aws.String(d.Get("global_replication_group_description").(string)) + } } - requestUpdate := false - - if d.HasChange("automatic_failover_enabled") { - input.AutomaticFailoverEnabled = aws.Bool(d.Get("automatic_failover_enabled").(bool)) - requestUpdate = true + for k, f := range updaters { + if d.HasChange(k) { + if err := updateElasticacheGlobalReplicationGroup(conn, d.Id(), f); err != nil { + return fmt.Errorf("error updating ElastiCache Global Replication Group (%s): %w", d.Id(), err) + } + } } - if d.HasChange("cache_node_type") { - input.CacheNodeType = aws.String(d.Get("cache_node_type").(string)) - requestUpdate = true - } + return resourceAwsElasticacheGlobalReplicationGroupRead(d, meta) +} - if d.HasChange("engine_version") { - input.EngineVersion = aws.String(d.Get("engine_version").(string)) - requestUpdate = true - } +type elasticacheGlobalReplicationGroupUpdater func(input *elasticache.ModifyGlobalReplicationGroupInput) - if d.HasChange("global_replication_group_description") { - input.GlobalReplicationGroupDescription = aws.String(d.Get("global_replication_group_description").(string)) - requestUpdate = true +func updateElasticacheGlobalReplicationGroup(conn *elasticache.ElastiCache, id string, f elasticacheGlobalReplicationGroupUpdater) error { + input := &elasticache.ModifyGlobalReplicationGroupInput{ + ApplyImmediately: aws.Bool(true), + GlobalReplicationGroupId: aws.String(id), } + f(input) - if requestUpdate { - _, err := conn.ModifyGlobalReplicationGroup(input) - - if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { - return nil - } - - if err != nil { - return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) - } + if _, err := conn.ModifyGlobalReplicationGroup(input); err != nil { + return err + } - if err := waitForElasticacheGlobalReplicationGroupUpdate(conn, d.Id()); err != nil { - return fmt.Errorf("error waiting for ElastiCache Global Replcation Cluster (%s) update: %s", d.Id(), err) - } + if _, err := waiter.GlobalReplicationGroupAvailable(conn, id, waiter.GlobalReplicationGroupDefaultUpdatedTimeout); err != nil { + return fmt.Errorf("waiting for completion: %w", err) } return nil @@ -235,262 +248,73 @@ func resourceAwsElasticacheGlobalReplicationGroupUpdate(d *schema.ResourceData, func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn - for _, globalReplicationGroupMemberRaw := range d.Get("global_replication_group_members").(*schema.Set).List() { - globalReplicationGroupMember, ok := globalReplicationGroupMemberRaw.(map[string]interface{}) - - if !ok { - continue - } - replicationGroupID, ok := globalReplicationGroupMember["replication_group_id"].(string) - if !ok { - continue - } - - role, ok := globalReplicationGroupMember["role"].(string) - if !ok { - continue - } - - if role == "SECONDARY" { - replicationGroupRegion, ok := globalReplicationGroupMember["replication_group_region"].(string) - if !ok { - continue - } - - input := &elasticache.DisassociateGlobalReplicationGroupInput{ - GlobalReplicationGroupId: aws.String(d.Id()), - ReplicationGroupId: aws.String(replicationGroupID), - ReplicationGroupRegion: aws.String(replicationGroupRegion), - } - - _, err := conn.DisassociateGlobalReplicationGroup(input) - - if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { - return nil - } - - if err := waitForElasticacheGlobalReplicationGroupDisassociation(conn, d.Id(), replicationGroupID); err != nil { - return fmt.Errorf("error waiting for Elasticache Replication Group (%s) removal from Elasticache Global Replication Group (%s): %w", replicationGroupID, d.Id(), err) - } - } + err := deleteElasticacheGlobalReplicationGroup(conn, d.Id(), true) + if err != nil { + return fmt.Errorf("error deleting ElastiCache Global Replication Group: %w", err) } + return nil +} + +func deleteElasticacheGlobalReplicationGroup(conn *elasticache.ElastiCache, id string, retainPrimaryReplicationGroup bool) error { input := &elasticache.DeleteGlobalReplicationGroupInput{ - GlobalReplicationGroupId: aws.String(d.Id()), - RetainPrimaryReplicationGroup: aws.Bool(d.Get("retain_primary_replication_group").(bool)), + GlobalReplicationGroupId: aws.String(id), + RetainPrimaryReplicationGroup: aws.Bool(retainPrimaryReplicationGroup), } - log.Printf("[DEBUG] Deleting ElastiCache Global Replication Group (%s): %s", d.Id(), input) - - err := resource.Retry(1*time.Minute, func() *resource.RetryError { - _, err := conn.DeleteGlobalReplicationGroup(input) - - if isAWSErr(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "is not empty") { - return resource.RetryableError(err) - } + // // TODO: is this needed? + // err := resource.Retry(1*time.Minute, func() *resource.RetryError { + _, err := conn.DeleteGlobalReplicationGroup(input) - if err != nil { - return resource.NonRetryableError(err) - } + // if isAWSErr(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "is not empty") { + // return resource.RetryableError(err) + // } - return nil - }) - - if isResourceTimeoutError(err) { - _, err = conn.DeleteGlobalReplicationGroup(input) - } + // if err != nil { + // return resource.NonRetryableError(err) + // } - if isAWSErr(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "") { - return nil - } + // return nil + // }) + // if isResourceTimeoutError(err) { + // _, err = conn.DeleteGlobalReplicationGroup(input) + // } if err != nil { - return fmt.Errorf("error deleting ElastiCache Global Replication Group: %s", err) + return err } - if err := waitForElasticacheGlobalReplicationGroupDeletion(conn, d.Id()); err != nil { - return fmt.Errorf("error waiting for ElastiCache Global Replication Group (%s) deletion: %s", d.Id(), err) + if _, err := waiter.GlobalReplicationGroupDeleted(conn, id); err != nil { + return fmt.Errorf("waiting for completion: %w", err) } return nil } -func flattenElasticacheGlobalReplicationGroupMembers(apiObjects []*elasticache.GlobalReplicationGroupMember) []interface{} { - if len(apiObjects) == 0 { +func flattenElasticacheGlobalReplicationGroupPrimaryGroupID(members []*elasticache.GlobalReplicationGroupMember) string { + for _, member := range members { + if aws.StringValue(member.Role) == GlobalReplicationGroupMemberRolePrimary { + return aws.StringValue(member.ReplicationGroupId) + } + } + return "" +} + +func flattenElasticacheGlobalReplicationGroupMembers(members []*elasticache.GlobalReplicationGroupMember) []interface{} { + if len(members) == 0 { return nil } var tfList []interface{} - for _, apiObject := range apiObjects { + for _, apiObject := range members { tfMap := map[string]interface{}{ "replication_group_id": aws.StringValue(apiObject.ReplicationGroupId), "replication_group_region": aws.StringValue(apiObject.ReplicationGroupRegion), "role": aws.StringValue(apiObject.Role), } - tfList = append(tfList, tfMap) } return tfList } - -func elasticacheDescribeGlobalReplicationGroup(conn *elasticache.ElastiCache, globalReplicationGroupID string) (*elasticache.GlobalReplicationGroup, error) { - var globalReplicationGroup *elasticache.GlobalReplicationGroup - - input := &elasticache.DescribeGlobalReplicationGroupsInput{ - GlobalReplicationGroupId: aws.String(globalReplicationGroupID), - ShowMemberInfo: aws.Bool(true), - } - - log.Printf("[DEBUG] Reading ElastiCache Global Replication Group (%s): %s", globalReplicationGroupID, input) - err := conn.DescribeGlobalReplicationGroupsPages(input, func(page *elasticache.DescribeGlobalReplicationGroupsOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, gc := range page.GlobalReplicationGroups { - if gc == nil { - continue - } - - if aws.StringValue(gc.GlobalReplicationGroupId) == globalReplicationGroupID { - globalReplicationGroup = gc - return false - } - } - - return !lastPage - }) - - return globalReplicationGroup, err -} - -func elasticacheGlobalReplicationGroupRefreshFunc(conn *elasticache.ElastiCache, globalReplicationGroupID string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, globalReplicationGroupID) - - if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { - return nil, "deleted", nil - } - - if err != nil { - return nil, "", fmt.Errorf("error reading ElastiCache Global Replication Group (%s): %s", globalReplicationGroupID, err) - } - - if globalReplicationGroup == nil { - return nil, "deleted", nil - } - - return globalReplicationGroup, aws.StringValue(globalReplicationGroup.Status), nil - } -} - -func waitForElasticacheGlobalReplicationGroupCreation(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{"creating"}, - Target: []string{"available", "primary-only"}, - Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), - Timeout: 10 * time.Minute, - } - - log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) availability", globalReplicationGroupID) - _, err := stateConf.WaitForState() - - return err -} - -func waitForElasticacheGlobalReplicationGroupUpdate(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{"modifying"}, - Target: []string{"available"}, - Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), - Timeout: 10 * time.Minute, - } - - log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) availability", globalReplicationGroupID) - _, err := stateConf.WaitForState() - - return err -} - -func waitForElasticacheGlobalReplicationGroupDeletion(conn *elasticache.ElastiCache, globalReplicationGroupID string) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{ - "available", - "primary-only", - "modifying", - "deleting", - }, - Target: []string{"deleted"}, - Refresh: elasticacheGlobalReplicationGroupRefreshFunc(conn, globalReplicationGroupID), - Timeout: 10 * time.Minute, - NotFoundChecks: 1, - } - - log.Printf("[DEBUG] Waiting for ElastiCache Global Replication Group (%s) deletion", globalReplicationGroupID) - _, err := stateConf.WaitForState() - - if isResourceNotFoundError(err) { - return nil - } - - return err -} - -func waitForElasticacheGlobalReplicationGroupDisassociation(conn *elasticache.ElastiCache, globalReplicationGroupID string, replicationGroupID string) error { - stillExistsErr := fmt.Errorf("ElastiCache Replication Group still associated in ElastiCache Global Replication Group") - var replicationGroup *elasticache.GlobalReplicationGroupMember - - err := resource.Retry(elasticacheGlobalReplicationGroupRemovalTimeout, func() *resource.RetryError { - var err error - - replicationGroup, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, globalReplicationGroupID, replicationGroupID) - - if err != nil { - return resource.NonRetryableError(err) - } - - if replicationGroup != nil { - return resource.RetryableError(stillExistsErr) - } - - return nil - }) - - if isResourceTimeoutError(err) { - _, err = elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn, globalReplicationGroupID, replicationGroupID) - } - - if err != nil { - return err - } - - if replicationGroup != nil { - return stillExistsErr - } - - return nil -} - -func elasticacheDescribeGlobalReplicationGroupFromReplicationGroup(conn *elasticache.ElastiCache, globalReplicationGroupID string, replicationGroupID string) (*elasticache.GlobalReplicationGroupMember, error) { - globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, globalReplicationGroupID) - - if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { - return nil, err - } - - members := globalReplicationGroup.Members - - if len(members) == 0 { - return nil, nil - } - - for _, member := range members { - if *member.ReplicationGroupId == replicationGroupID { - return member, nil - } - } - - return nil, nil -} diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index 8cdf4c4bdb0..611eee74a53 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -8,9 +8,14 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/waiter" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) func init() { @@ -25,53 +30,56 @@ func init() { func testSweepElasticacheGlobalReplicationGroups(region string) error { client, err := sharedClientForRegion(region) - if err != nil { - return fmt.Errorf("error getting client: %s", err) + return fmt.Errorf("error getting client: %w", err) } - conn := client.(*AWSClient).elasticacheconn - input := &elasticache.DescribeGlobalReplicationGroupsInput{} - err = conn.DescribeGlobalReplicationGroupsPages(input, func(out *elasticache.DescribeGlobalReplicationGroupsOutput, lastPage bool) bool { - for _, globalReplicationGroup := range out.GlobalReplicationGroups { - id := aws.StringValue(globalReplicationGroup.GlobalReplicationGroupId) - input := &elasticache.DeleteGlobalReplicationGroupInput{ - GlobalReplicationGroupId: globalReplicationGroup.GlobalReplicationGroupId, - } + var sweeperErrs *multierror.Error - log.Printf("[INFO] Deleting Elasticache Global Replication Group: %s", id) + input := &elasticache.DescribeGlobalReplicationGroupsInput{} + err = conn.DescribeGlobalReplicationGroupsPages(input, func(page *elasticache.DescribeGlobalReplicationGroupsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } - _, err := conn.DeleteGlobalReplicationGroup(input) + for _, globalReplicationGroup := range page.GlobalReplicationGroups { + id := aws.StringValue(globalReplicationGroup.GlobalReplicationGroupId) + log.Printf("[INFO] Deleting ElastiCache Global Replication Group: %s", id) + err := deleteElasticacheGlobalReplicationGroup(conn, id, false) if err != nil { - log.Printf("[ERROR] Failed to delete ElastiCache Global Replication Group (%s): %s", id, err) + sweeperErr := fmt.Errorf("error deleting ElastiCache Global Replication Group (%s): %w", id, err) + log.Printf("[ERROR] %s", sweeperErr) + sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) continue } - - if err := waitForElasticacheGlobalReplicationGroupDeletion(conn, id); err != nil { - log.Printf("[ERROR] Failure while waiting for ElastiCache Global Replication Group (%s) to be deleted: %s", id, err) - } } + return !lastPage }) if testSweepSkipSweepError(err) { - log.Printf("[WARN] Skipping ElastiCache Global Replication Group sweep for %s: %s", region, err) - return nil + log.Printf("[WARN] Skipping ElastiCache Global Replication Group sweep for %q: %s", region, err) + return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors } if err != nil { - return fmt.Errorf("error retrieving ElastiCache Global Replication Groups: %s", err) + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing ElastiCache Global Replication Groups: %w", err)) } - return nil + return sweeperErrs.ErrorOrNil() } func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { - var globalReplcationGroup1 elasticache.GlobalReplicationGroup + var globalReplcationGroup elasticache.GlobalReplicationGroup + var primaryReplcationGroup elasticache.ReplicationGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + primaryReplicationGroupId := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_elasticache_global_replication_group.test" + primaryReplicationGroupResourceName := "aws_elasticache_replication_group.test" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSElasticacheGlobalReplicationGroup(t) }, @@ -79,22 +87,31 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { CheckDestroy: testAccCheckAWSElasticacheGlobalReplicationGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSElasticacheGlobalReplicationGroupConfig(rName), + Config: testAccAWSElasticacheGlobalReplicationGroupConfig_basic(rName, primaryReplicationGroupId), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup1), - testAccMatchResourceAttrGlobalARN(resourceName, "arn", "elasticache", regexp.MustCompile(`globalreplicationgroup:\w{5}-`+rName)), // \w{5} is the AWS prefix - resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "automatic_failover_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "cache_node_type", "cache.m5.large"), - resource.TestCheckResourceAttr(resourceName, "cluster_enabled", "false"), - resource.TestCheckResourceAttr(resourceName, "engine", "redis"), - resource.TestCheckResourceAttr(resourceName, "engine_version", "5.0.6"), + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), + testAccCheckAWSElasticacheReplicationGroupExists(primaryReplicationGroupResourceName, &primaryReplcationGroup), + testAccMatchResourceAttrGlobalARN(resourceName, "arn", "elasticache", regexp.MustCompile(`globalreplicationgroup:`+elasticacheGlobalReplicationGroupRegionPrefixFormat+rName)), + resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", "false"), // TODO: change to Pair + resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), // TODO: change to Pair + resource.TestCheckResourceAttr(resourceName, "cache_node_type", "cache.m5.large"), // TODO: change to Pair + resource.TestCheckResourceAttr(resourceName, "cluster_enabled", "false"), // TODO: change to Pair + resource.TestCheckResourceAttr(resourceName, "engine", "redis"), // TODO: change to Pair + resource.TestCheckResourceAttr(resourceName, "actual_engine_version", "5.0.6"), // TODO: change to Pair resource.TestCheckResourceAttr(resourceName, "global_replication_group_id_suffix", rName), - resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", "0"), - resource.TestCheckResourceAttr(resourceName, "primary_replication_group_id", rName), - resource.TestCheckResourceAttr(resourceName, "retain_primary_replication_group", "true"), + resource.TestMatchResourceAttr(resourceName, "global_replication_group_id", regexp.MustCompile(elasticacheGlobalReplicationGroupRegionPrefixFormat+rName)), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", elasticacheEmptyDescription), + resource.TestCheckResourceAttr(resourceName, "primary_replication_group_id", primaryReplicationGroupId), resource.TestCheckResourceAttr(resourceName, "transit_encryption_enabled", "false"), + + resource.TestCheckResourceAttr(resourceName, "global_replication_group_members.#", "1"), + func(s *terraform.State) error { + return resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_replication_group_members.*", map[string]string{ + "replication_group_id": aws.StringValue(primaryReplcationGroup.ReplicationGroupId), + "replication_group_region": testAccGetRegion(), + "role": GlobalReplicationGroupMemberRolePrimary, + })(s) + }, ), }, { @@ -106,9 +123,46 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { }) } +func TestAccAWSElasticacheGlobalReplicationGroup_Description(t *testing.T) { + var globalReplcationGroup elasticache.GlobalReplicationGroup + rName := acctest.RandomWithPrefix("tf-acc-test") + primaryReplicationGroupId := acctest.RandomWithPrefix("tf-acc-test") + description1 := acctest.RandString(10) + description2 := acctest.RandString(10) + resourceName := "aws_elasticache_global_replication_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccPreCheckAWSElasticacheGlobalReplicationGroup(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheGlobalReplicationGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheGlobalReplicationGroupConfig_description(rName, primaryReplicationGroupId, description1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", description1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSElasticacheGlobalReplicationGroupConfig_description(rName, primaryReplicationGroupId, description2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), + resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", description2), + ), + }, + }, + }) +} + func TestAccAWSElasticacheGlobalReplicationGroup_disappears(t *testing.T) { - var globalReplcationGroup1 elasticache.GlobalReplicationGroup + var globalReplcationGroup elasticache.GlobalReplicationGroup rName := acctest.RandomWithPrefix("tf-acc-test") + primaryReplicationGroupId := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_elasticache_global_replication_group.test" resource.ParallelTest(t, resource.TestCase{ @@ -117,10 +171,10 @@ func TestAccAWSElasticacheGlobalReplicationGroup_disappears(t *testing.T) { CheckDestroy: testAccCheckAWSElasticacheGlobalReplicationGroupDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSElasticacheGlobalReplicationGroupConfig(rName), + Config: testAccAWSElasticacheGlobalReplicationGroupConfig_basic(rName, primaryReplicationGroupId), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup1), - testAccCheckAWSElasticacheGlobalReplicationGroupDisappears(&globalReplcationGroup1), + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), + testAccCheckResourceDisappears(testAccProvider, resourceAwsElasticacheGlobalReplicationGroup(), resourceName), ), ExpectNonEmptyPlan: true, }, @@ -128,7 +182,7 @@ func TestAccAWSElasticacheGlobalReplicationGroup_disappears(t *testing.T) { }) } -func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, globalReplicationGroup *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { +func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, v *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] if !ok { @@ -136,50 +190,25 @@ func testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName string, } if rs.Primary.ID == "" { - return fmt.Errorf("No Elasticache Global Replication Group ID is set") + return fmt.Errorf("No ElastiCache Global Replication Group ID is set") } conn := testAccProvider.Meta().(*AWSClient).elasticacheconn - - cluster, err := elasticacheDescribeGlobalReplicationGroup(conn, rs.Primary.ID) - + grg, err := finder.GlobalReplicationGroupByID(conn, rs.Primary.ID) if err != nil { - return err + return fmt.Errorf("error retrieving ElastiCache Global Replication Group (%s): %w", rs.Primary.ID, err) } - if cluster == nil { - return fmt.Errorf("Elasticache Global Replication Group not found") + if aws.StringValue(grg.Status) != waiter.GlobalReplicationGroupStatusAvailable && aws.StringValue(grg.Status) != waiter.GlobalReplicationGroupStatusPrimaryOnly { + return fmt.Errorf("ElastiCache Global Replication Group (%s) exists, but is in a non-available state: %s", rs.Primary.ID, aws.StringValue(grg.Status)) } - if aws.StringValue(cluster.Status) != "available" && aws.StringValue(cluster.Status) != "primary-only" { - return fmt.Errorf("Elasticache Global Replication Group (%s) exists in non-available (%s) state", rs.Primary.ID, aws.StringValue(cluster.Status)) - } - - *globalReplicationGroup = *cluster + *v = *grg return nil } } -func testAccCheckAWSElasticacheGlobalReplicationGroupDisappears(globalReplicationGroup *elasticache.GlobalReplicationGroup) resource.TestCheckFunc { - return func(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).elasticacheconn - - input := &elasticache.DeleteGlobalReplicationGroupInput{ - GlobalReplicationGroupId: globalReplicationGroup.GlobalReplicationGroupId, - RetainPrimaryReplicationGroup: aws.Bool(true), - } - - _, err := conn.DeleteGlobalReplicationGroup(input) - - if err != nil { - return err - } - - return waitForElasticacheGlobalReplicationGroupDeletion(conn, aws.StringValue(globalReplicationGroup.GlobalReplicationGroupId)) - } -} - func testAccCheckAWSElasticacheGlobalReplicationGroupDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).elasticacheconn @@ -188,21 +217,14 @@ func testAccCheckAWSElasticacheGlobalReplicationGroupDestroy(s *terraform.State) continue } - globalReplicationGroup, err := elasticacheDescribeGlobalReplicationGroup(conn, rs.Primary.ID) - - if isAWSErr(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault, "") { + _, err := finder.GlobalReplicationGroupByID(conn, rs.Primary.ID) + if tfresource.NotFound(err) { continue } - if err != nil { return err } - - if globalReplicationGroup == nil { - continue - } - - return fmt.Errorf("Elasticache Global Replication Group (%s) still exists in non-deleted (%s) state", rs.Primary.ID, aws.StringValue(globalReplicationGroup.Status)) + return fmt.Errorf("ElastiCache Global Replication Group (%s) still exists", rs.Primary.ID) } return nil @@ -212,10 +234,10 @@ func testAccPreCheckAWSElasticacheGlobalReplicationGroup(t *testing.T) { conn := testAccProvider.Meta().(*AWSClient).elasticacheconn input := &elasticache.DescribeGlobalReplicationGroupsInput{} - _, err := conn.DescribeGlobalReplicationGroups(input) - if testAccPreCheckSkipError(err) || isAWSErr(err, "InvalidParameterValue", "Access Denied to API Version: APIGlobalDatastore") { + if testAccPreCheckSkipError(err) || + tfawserr.ErrMessageContains(err, elasticache.ErrCodeInvalidParameterValueException, "Access Denied to API Version: APIGlobalDatastore") { t.Skipf("skipping acceptance testing: %s", err) } @@ -224,15 +246,36 @@ func testAccPreCheckAWSElasticacheGlobalReplicationGroup(t *testing.T) { } } -func testAccAWSElasticacheGlobalReplicationGroupConfig(rName string) string { +func testAccAWSElasticacheGlobalReplicationGroupConfig_basic(rName, primaryReplicationGroupId string) string { + return fmt.Sprintf(` +resource "aws_elasticache_global_replication_group" "test" { + global_replication_group_id_suffix = %[1]q + primary_replication_group_id = aws_elasticache_replication_group.test.id +} + +resource "aws_elasticache_replication_group" "test" { + replication_group_id = %[2]q + replication_group_description = "test" + + engine = "redis" + engine_version = "5.0.6" + node_type = "cache.m5.large" + number_cache_clusters = 1 +} +`, rName, primaryReplicationGroupId) +} + +func testAccAWSElasticacheGlobalReplicationGroupConfig_description(rName, primaryReplicationGroupId, description string) string { return fmt.Sprintf(` resource "aws_elasticache_global_replication_group" "test" { - global_replication_group_id_suffix = %q + global_replication_group_id_suffix = %[1]q primary_replication_group_id = aws_elasticache_replication_group.test.id + + global_replication_group_description = %[3]q } resource "aws_elasticache_replication_group" "test" { - replication_group_id = %q + replication_group_id = %[2]q replication_group_description = "test" engine = "redis" @@ -240,5 +283,5 @@ resource "aws_elasticache_replication_group" "test" { node_type = "cache.m5.large" number_cache_clusters = 1 } -`, rName, rName) +`, rName, primaryReplicationGroupId, description) } diff --git a/website/docs/r/elasticache_cluster.html.markdown b/website/docs/r/elasticache_cluster.html.markdown index 2c721d49c90..8b0dcaaee2d 100644 --- a/website/docs/r/elasticache_cluster.html.markdown +++ b/website/docs/r/elasticache_cluster.html.markdown @@ -87,9 +87,7 @@ in the AWS Documentation center for supported versions on the cache cluster is performed. The format is `ddd:hh24:mi-ddd:hh24:mi` (24H Clock UTC). The minimum maintenance window is a 60 minute period. Example: `sun:05:00-sun:09:00` -* `node_type` – (Required unless `replication_group_id` is provided) The compute and memory capacity of the nodes. See -[Available Cache Node Types](https://aws.amazon.com/elasticache/pricing/#Available_node_types) for -supported node types. For Memcached, changing this value will re-create the resource. +* `node_type` – (Required unless `replication_group_id` is provided) The instance class used. See AWS documentation for information on [supported node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). See AWS documentation for information on [supported node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/nodes-select-size.html). For Memcached, changing this value will re-create the resource. * `num_cache_nodes` – (Required unless `replication_group_id` is provided) The initial number of cache nodes that the cache cluster will have. For Redis, this value must be 1. For Memcached, this diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 87c94f05fbb..0ebd733dc44 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -8,7 +8,7 @@ description: |- # Resource: aws_elasticache_global_replication_group -Provides an ElastiCache Global Replication Group resource, which manage a replication between 2 or more redis replication group in different region. +Provides an ElastiCache Global Replication Group resource, which manage a replication between 2 or more redis replication group in different regions. ## Example Usage @@ -37,14 +37,9 @@ resource "aws_elasticache_replication_group" "primary" { The following arguments are supported: -* `global_replication_group_id_suffix` – (Required) The suffix name of a Global Datastore. -* `primary_replication_group_id` – (Required) The name of the primary cluster that accepts writes and will replicate updates to the secondary cluster. +* `global_replication_group_id_suffix` – (Required) The suffix name of a Global Datastore. If `global_replication_group_id_suffix` is changed, creates a new resource. +* `primary_replication_group_id` – (Required) The ID of the primary cluster that accepts writes and will replicate updates to the secondary cluster. If `primary_replication_group_id` is changed, creates a new resource. * `global_replication_group_description` – (Optional) A user-created description for the global replication group. -* `retain_primary_replication_group` - (Optional) Whether to retain the primary replication group when the global replication group is deleted. -* `apply_immediately` - (Optional) This parameter causes the modifications in this request and any pending modifications to be applied, asynchronously and as soon as possible. Modifications to Global Replication Groups cannot be requested to be applied in PreferredMaintenceWindow. Default to true. -* `automatic_failover_enabled` - (Optional) Determines whether a read replica is automatically promoted to read/write primary if the existing primary encounters a failure. -* `cache_node_type` - (Optional) A valid cache node type that you want to scale this Global Datastore to. -* `engine_version` - (Optional) The upgraded version of the cache engine to be run on the clusters in the Global Datastore. ## Attributes Reference @@ -52,14 +47,17 @@ In addition to all arguments above, the following attributes are exported: * `id` - The ID of the ElastiCache Global Replication Group. * `arn` - The ARN of the ElastiCache Global Replication Group. +* `actual_engine_version` - The full version number of the cache engine running on the members of this global replication group. * `at_rest_encryption_enabled` - A flag that indicate whether the encryption at rest is enabled. * `auth_token_enabled` - A flag that indicate whether AuthToken (password) is enabled. -* `cluster_enabled` - A flag that indicates whether the Global Datastore is cluster enabled. -* `engine` - The Elasticache engine. For redis only -* `global_replication_group_members` - The identifiers of all the replication group members that are part of this global replication group. - * `replication_group_id` - The replication group id of the Global Datastore member - * `replication_group_region` - The AWS region of the Global Datastore member - * `role` - Indicates the role of the replication group, primary or secondary +* `cache_node_type` - The instance class used. See AWS documentation for information on [supported node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). +* `cluster_enabled` - Indicates whether the Global Datastore is cluster enabled. +* `engine` - The name of the cache engine to be used for the clusters in this global replication group. +* `global_replication_group_id` - The full ID of the global replication group. +* `global_replication_group_members` - Set of the replication groups that are part of this global replication group. + * `replication_group_id` - The replication group id of the Global Datastore member. + * `replication_group_region` - The AWS region of the Global Datastore member. + * `role` - Indicates the role of the replication group, either `PRIMARY` or `SECONDARY`. * `transit_encryption_enabled` - A flag that indicates whether the encryption in transit is enabled. ## Import diff --git a/website/docs/r/elasticache_replication_group.html.markdown b/website/docs/r/elasticache_replication_group.html.markdown index f95268c0b32..996fd6541aa 100644 --- a/website/docs/r/elasticache_replication_group.html.markdown +++ b/website/docs/r/elasticache_replication_group.html.markdown @@ -110,7 +110,7 @@ The following arguments are supported: * `replication_group_id` – (Required) The replication group identifier. This parameter is stored as a lowercase string. * `replication_group_description` – (Required) A user-created description for the replication group. * `number_cache_clusters` - (Optional) The number of cache clusters (primary and replicas) this replication group will have. If Multi-AZ is enabled, the value of this parameter must be at least 2. Updates will occur before other modifications. One of `number_cache_clusters` or `cluster_mode` is required. -* `node_type` - (Required) The compute and memory capacity of the nodes in the node group. +* `node_type` - (Required) The instance class to be used. See AWS documentation for information on [supported node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). * `automatic_failover_enabled` - (Optional) Specifies whether a read-only replica will be automatically promoted to read/write primary if the existing primary fails. If true, Multi-AZ is enabled for this replication group. If false, Multi-AZ is disabled for this replication group. Must be enabled for Redis (cluster mode enabled) replication groups. Defaults to `false`. * `multi_az_enabled` - (Optional) Specifies whether to enable Multi-AZ Support for the replication group. If `true`, `automatic_failover_enabled` must also be enabled. Defaults to `false`. * `auto_minor_version_upgrade` - (Optional) Specifies whether a minor engine upgrades will be applied automatically to the underlying Cache Cluster instances during the maintenance window. This parameter is currently not supported by the AWS API. Defaults to `true`. From c86c0ccdcb7c2c1793054e5ede855cc00c6fcbb4 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Sun, 14 Feb 2021 23:14:01 -0800 Subject: [PATCH 16/25] Removes commented code --- ..._aws_elasticache_global_replication_group.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index 9289bb519cb..487be1d2ce4 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -262,24 +262,7 @@ func deleteElasticacheGlobalReplicationGroup(conn *elasticache.ElastiCache, id s RetainPrimaryReplicationGroup: aws.Bool(retainPrimaryReplicationGroup), } - // // TODO: is this needed? - // err := resource.Retry(1*time.Minute, func() *resource.RetryError { _, err := conn.DeleteGlobalReplicationGroup(input) - - // if isAWSErr(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "is not empty") { - // return resource.RetryableError(err) - // } - - // if err != nil { - // return resource.NonRetryableError(err) - // } - - // return nil - // }) - // if isResourceTimeoutError(err) { - // _, err = conn.DeleteGlobalReplicationGroup(input) - // } - if err != nil { return err } From 29f01670b91c41cdb1e6d3aee4cd60cd803e2fcd Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Mon, 15 Feb 2021 18:00:16 -0800 Subject: [PATCH 17/25] Adds CHANGELOG --- .changelog/15885.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/15885.txt diff --git a/.changelog/15885.txt b/.changelog/15885.txt new file mode 100644 index 00000000000..021aa6bc29e --- /dev/null +++ b/.changelog/15885.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_elasticache_global_replication_group +``` From 9ef51454523d43b2086736b57b1414e8f51c8ba5 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Tue, 16 Feb 2021 11:58:53 -0800 Subject: [PATCH 18/25] Adds support for authenticating sweepers with AWS_CONTAINER_CREDENTIALS_FULL_URI --- aws/aws_sweeper_test.go | 13 ++- aws/internal/envvar/funcs.go | 27 +++++++ aws/internal/envvar/funcs_test.go | 116 +++++++++++++++++++++++++++ aws/internal/envvar/testing_funcs.go | 14 ++-- 4 files changed, 159 insertions(+), 11 deletions(-) diff --git a/aws/aws_sweeper_test.go b/aws/aws_sweeper_test.go index fdece2fff54..36113743231 100644 --- a/aws/aws_sweeper_test.go +++ b/aws/aws_sweeper_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/envvar" ) // sweeperAwsClients is a shared cache of regional AWSClient @@ -24,8 +25,16 @@ func sharedClientForRegion(region string) (interface{}, error) { return client, nil } - if os.Getenv("AWS_PROFILE") == "" && (os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "") { - return nil, fmt.Errorf("must provide environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY or environment variable AWS_PROFILE") + _, _, err := envvar.RequireOneOf([]string{envvar.AwsProfile, envvar.AwsAccessKeyId, envvar.AwsContainerCredentialsFullUri}, "credentials for running sweepers") + if err != nil { + return nil, err + } + + if os.Getenv(envvar.AwsAccessKeyId) != "" { + _, err := envvar.Require(envvar.AwsSecretAccessKey, "static credentials value when using "+envvar.AwsAccessKeyId) + if err != nil { + return nil, err + } } conf := &Config{ diff --git a/aws/internal/envvar/funcs.go b/aws/internal/envvar/funcs.go index de23c2e08a7..73f5ac01972 100644 --- a/aws/internal/envvar/funcs.go +++ b/aws/internal/envvar/funcs.go @@ -1,6 +1,7 @@ package envvar import ( + "fmt" "os" ) @@ -14,3 +15,29 @@ func GetWithDefault(variable string, defaultValue string) string { return value } + +// RequireOneOf verifies that at least one environment variable is non-empty or returns an error. +// +// If at lease one environment variable is non-empty, returns the first name and value. +func RequireOneOf(names []string, usageMessage string) (string, string, error) { + for _, variable := range names { + value := os.Getenv(variable) + + if value != "" { + return variable, value, nil + } + } + + return "", "", fmt.Errorf("at least one environment variable of %v must be set. Usage: %s", names, usageMessage) +} + +// Require verifies that an environment variable is non-empty or returns an error. +func Require(name string, usageMessage string) (string, error) { + value := os.Getenv(name) + + if value == "" { + return "", fmt.Errorf("environment variable %s must be set. Usage: %s", name, usageMessage) + } + + return value, nil +} diff --git a/aws/internal/envvar/funcs_test.go b/aws/internal/envvar/funcs_test.go index 1a232627283..e7771a9ac06 100644 --- a/aws/internal/envvar/funcs_test.go +++ b/aws/internal/envvar/funcs_test.go @@ -48,3 +48,119 @@ func TestGetWithDefault(t *testing.T) { } }) } + +func TestRequireOneOf(t *testing.T) { + envVar1 := "TESTENVVAR_FAILIFALLEMPTY1" + envVar2 := "TESTENVVAR_FAILIFALLEMPTY2" + envVars := []string{envVar1, envVar2} + + t.Run("missing", func(t *testing.T) { + for _, envVar := range envVars { + os.Unsetenv(envVar) + } + + _, _, err := envvar.RequireOneOf(envVars, "usage") + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("all empty", func(t *testing.T) { + os.Setenv(envVar1, "") + os.Setenv(envVar2, "") + defer unsetEnvVars(envVars) + + _, _, err := envvar.RequireOneOf(envVars, "usage") + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("some empty", func(t *testing.T) { + wantValue := "pickme" + + os.Setenv(envVar1, "") + os.Setenv(envVar2, wantValue) + defer unsetEnvVars(envVars) + + gotName, gotValue, err := envvar.RequireOneOf(envVars, "usage") + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if gotName != envVar2 { + t.Fatalf("expected name: %s, got: %s", envVar2, gotName) + } + + if gotValue != wantValue { + t.Fatalf("expected value: %s, got: %s", wantValue, gotValue) + } + }) + + t.Run("all not empty", func(t *testing.T) { + wantValue := "pickme" + + os.Setenv(envVar1, wantValue) + os.Setenv(envVar2, "other") + defer unsetEnvVars(envVars) + + gotName, gotValue, err := envvar.RequireOneOf(envVars, "usage") + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if gotName != envVar1 { + t.Fatalf("expected name: %s, got: %s", envVar1, gotName) + } + + if gotValue != wantValue { + t.Fatalf("expected value: %s, got: %s", wantValue, gotValue) + } + }) +} + +func TestRequire(t *testing.T) { + envVar := "TESTENVVAR_FAILIFEMPTY" + + t.Run("missing", func(t *testing.T) { + os.Unsetenv(envVar) + + _, err := envvar.Require(envVar, "usage") + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("empty", func(t *testing.T) { + os.Setenv(envVar, "") + defer os.Unsetenv(envVar) + + _, err := envvar.Require(envVar, "usage") + + if err == nil { + t.Fatal("expected error") + } + }) + + t.Run("not empty", func(t *testing.T) { + want := "notempty" + + os.Setenv(envVar, want) + defer os.Unsetenv(envVar) + + got, err := envvar.Require(envVar, "usage") + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if got != want { + t.Fatalf("expected value: %s, got: %s", want, got) + } + }) +} diff --git a/aws/internal/envvar/testing_funcs.go b/aws/internal/envvar/testing_funcs.go index ff01b950a02..841ec3d0115 100644 --- a/aws/internal/envvar/testing_funcs.go +++ b/aws/internal/envvar/testing_funcs.go @@ -12,17 +12,13 @@ import ( func TestFailIfAllEmpty(t testing.T, names []string, usageMessage string) (string, string) { t.Helper() - for _, variable := range names { - value := os.Getenv(variable) - - if value != "" { - return variable, value - } + name, value, err := RequireOneOf(names, usageMessage) + if err != nil { + t.Fatal(err) + return "", "" } - t.Fatalf("at least one environment variable of %v must be set. Usage: %s", names, usageMessage) - - return "", "" + return name, value } // TestFailIfEmpty verifies that an environment variable is non-empty or fails the test. From 6bdb36fe8b161d3e774e2e4a6633facbeee7bfb4 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Tue, 16 Feb 2021 12:00:31 -0800 Subject: [PATCH 19/25] Fixes sweeper dependency order for Global Replication Groups --- aws/resource_aws_elasticache_global_replication_group.go | 6 +++--- ...esource_aws_elasticache_global_replication_group_test.go | 5 +---- aws/resource_aws_elasticache_replication_group_test.go | 3 +++ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index 487be1d2ce4..054b8e5ffe8 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -248,7 +248,7 @@ func updateElasticacheGlobalReplicationGroup(conn *elasticache.ElastiCache, id s func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn - err := deleteElasticacheGlobalReplicationGroup(conn, d.Id(), true) + err := deleteElasticacheGlobalReplicationGroup(conn, d.Id()) if err != nil { return fmt.Errorf("error deleting ElastiCache Global Replication Group: %w", err) } @@ -256,10 +256,10 @@ func resourceAwsElasticacheGlobalReplicationGroupDelete(d *schema.ResourceData, return nil } -func deleteElasticacheGlobalReplicationGroup(conn *elasticache.ElastiCache, id string, retainPrimaryReplicationGroup bool) error { +func deleteElasticacheGlobalReplicationGroup(conn *elasticache.ElastiCache, id string) error { input := &elasticache.DeleteGlobalReplicationGroupInput{ GlobalReplicationGroupId: aws.String(id), - RetainPrimaryReplicationGroup: aws.Bool(retainPrimaryReplicationGroup), + RetainPrimaryReplicationGroup: aws.Bool(true), } _, err := conn.DeleteGlobalReplicationGroup(input) diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index 611eee74a53..70c3762ff0a 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -22,9 +22,6 @@ func init() { resource.AddTestSweepers("aws_elasticache_global_replication_group", &resource.Sweeper{ Name: "aws_elasticache_global_replication_group", F: testSweepElasticacheGlobalReplicationGroups, - Dependencies: []string{ - "aws_elasticache_replication_group", - }, }) } @@ -47,7 +44,7 @@ func testSweepElasticacheGlobalReplicationGroups(region string) error { id := aws.StringValue(globalReplicationGroup.GlobalReplicationGroupId) log.Printf("[INFO] Deleting ElastiCache Global Replication Group: %s", id) - err := deleteElasticacheGlobalReplicationGroup(conn, id, false) + err := deleteElasticacheGlobalReplicationGroup(conn, id) if err != nil { sweeperErr := fmt.Errorf("error deleting ElastiCache Global Replication Group (%s): %w", id, err) log.Printf("[ERROR] %s", sweeperErr) diff --git a/aws/resource_aws_elasticache_replication_group_test.go b/aws/resource_aws_elasticache_replication_group_test.go index 5eabccd4490..3817ce359f1 100644 --- a/aws/resource_aws_elasticache_replication_group_test.go +++ b/aws/resource_aws_elasticache_replication_group_test.go @@ -23,6 +23,9 @@ func init() { resource.AddTestSweepers("aws_elasticache_replication_group", &resource.Sweeper{ Name: "aws_elasticache_replication_group", F: testSweepElasticacheReplicationGroups, + Dependencies: []string{ + "aws_elasticache_global_replication_group", + }, }) } From 6685a8cd610b23a909aa51fb8b63b3a56f8d0c13 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 17 Feb 2021 16:10:29 -0800 Subject: [PATCH 20/25] Updates test to use resource.TestCheckResourceAttrPair --- ..._aws_elasticache_global_replication_group_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index 70c3762ff0a..2551c28c1c2 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -89,12 +89,12 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), testAccCheckAWSElasticacheReplicationGroupExists(primaryReplicationGroupResourceName, &primaryReplcationGroup), testAccMatchResourceAttrGlobalARN(resourceName, "arn", "elasticache", regexp.MustCompile(`globalreplicationgroup:`+elasticacheGlobalReplicationGroupRegionPrefixFormat+rName)), - resource.TestCheckResourceAttr(resourceName, "at_rest_encryption_enabled", "false"), // TODO: change to Pair - resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), // TODO: change to Pair - resource.TestCheckResourceAttr(resourceName, "cache_node_type", "cache.m5.large"), // TODO: change to Pair - resource.TestCheckResourceAttr(resourceName, "cluster_enabled", "false"), // TODO: change to Pair - resource.TestCheckResourceAttr(resourceName, "engine", "redis"), // TODO: change to Pair - resource.TestCheckResourceAttr(resourceName, "actual_engine_version", "5.0.6"), // TODO: change to Pair + resource.TestCheckResourceAttrPair(resourceName, "at_rest_encryption_enabled", primaryReplicationGroupResourceName, "at_rest_encryption_enabled"), + resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), + resource.TestCheckResourceAttrPair(resourceName, "cache_node_type", primaryReplicationGroupResourceName, "node_type"), + resource.TestCheckResourceAttrPair(resourceName, "cluster_enabled", primaryReplicationGroupResourceName, "cluster_enabled"), + resource.TestCheckResourceAttrPair(resourceName, "engine", primaryReplicationGroupResourceName, "engine"), + resource.TestCheckResourceAttrPair(resourceName, "actual_engine_version", primaryReplicationGroupResourceName, "engine_version"), resource.TestCheckResourceAttr(resourceName, "global_replication_group_id_suffix", rName), resource.TestMatchResourceAttr(resourceName, "global_replication_group_id", regexp.MustCompile(elasticacheGlobalReplicationGroupRegionPrefixFormat+rName)), resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", elasticacheEmptyDescription), From 22eeca68c908a3968f47920a6f5e7f43336e3d04 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 18 Feb 2021 14:01:14 -0800 Subject: [PATCH 21/25] Cleans up envvar testing Co-authored-by: Brian Flad --- aws/internal/envvar/funcs_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws/internal/envvar/funcs_test.go b/aws/internal/envvar/funcs_test.go index e7771a9ac06..d2bc27b6b24 100644 --- a/aws/internal/envvar/funcs_test.go +++ b/aws/internal/envvar/funcs_test.go @@ -50,8 +50,8 @@ func TestGetWithDefault(t *testing.T) { } func TestRequireOneOf(t *testing.T) { - envVar1 := "TESTENVVAR_FAILIFALLEMPTY1" - envVar2 := "TESTENVVAR_FAILIFALLEMPTY2" + envVar1 := "TESTENVVAR_REQUIREONEOF1" + envVar2 := "TESTENVVAR_REQUIREONEOF2" envVars := []string{envVar1, envVar2} t.Run("missing", func(t *testing.T) { @@ -124,7 +124,7 @@ func TestRequireOneOf(t *testing.T) { } func TestRequire(t *testing.T) { - envVar := "TESTENVVAR_FAILIFEMPTY" + envVar := "TESTENVVAR_REQUIRE" t.Run("missing", func(t *testing.T) { os.Unsetenv(envVar) From f51f065721436cb08d7aad564df7688906b178b4 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 18 Feb 2021 14:08:18 -0800 Subject: [PATCH 22/25] Apply suggestions from code review Co-authored-by: Brian Flad --- .../service/elasticache/finder/finder.go | 4 ++-- ...asticache_global_replication_group_test.go | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/aws/internal/service/elasticache/finder/finder.go b/aws/internal/service/elasticache/finder/finder.go index f794edb5611..c4d37907090 100644 --- a/aws/internal/service/elasticache/finder/finder.go +++ b/aws/internal/service/elasticache/finder/finder.go @@ -39,7 +39,7 @@ func ReplicationGroupByID(conn *elasticache.ElastiCache, id string) (*elasticach func ReplicationGroupMemberClustersByID(conn *elasticache.ElastiCache, id string) ([]*elasticache.CacheCluster, error) { rg, err := ReplicationGroupByID(conn, id) if err != nil { - return []*elasticache.CacheCluster{}, err + return nil, err } clusters, err := CacheClustersByID(conn, aws.StringValueSlice(rg.MemberClusters)) @@ -163,7 +163,7 @@ func GlobalReplicationGroupMemberByID(conn *elasticache.ElastiCache, globalRepli } } - if len(globalReplicationGroup.Members) == 0 { + if globalReplicationGroup == nil || len(globalReplicationGroup.Members) == 0 { return nil, &resource.NotFoundError{ Message: "empty result", } diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index 2551c28c1c2..259f19dc478 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -69,8 +69,8 @@ func testSweepElasticacheGlobalReplicationGroups(region string) error { } func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { - var globalReplcationGroup elasticache.GlobalReplicationGroup - var primaryReplcationGroup elasticache.ReplicationGroup + var globalReplicationGroup elasticache.GlobalReplicationGroup + var primaryReplicationGroup elasticache.ReplicationGroup rName := acctest.RandomWithPrefix("tf-acc-test") primaryReplicationGroupId := acctest.RandomWithPrefix("tf-acc-test") @@ -86,8 +86,8 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { { Config: testAccAWSElasticacheGlobalReplicationGroupConfig_basic(rName, primaryReplicationGroupId), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), - testAccCheckAWSElasticacheReplicationGroupExists(primaryReplicationGroupResourceName, &primaryReplcationGroup), + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplicationGroup), + testAccCheckAWSElasticacheReplicationGroupExists(primaryReplicationGroupResourceName, &primaryReplicationGroup), testAccMatchResourceAttrGlobalARN(resourceName, "arn", "elasticache", regexp.MustCompile(`globalreplicationgroup:`+elasticacheGlobalReplicationGroupRegionPrefixFormat+rName)), resource.TestCheckResourceAttrPair(resourceName, "at_rest_encryption_enabled", primaryReplicationGroupResourceName, "at_rest_encryption_enabled"), resource.TestCheckResourceAttr(resourceName, "auth_token_enabled", "false"), @@ -104,7 +104,7 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "global_replication_group_members.#", "1"), func(s *terraform.State) error { return resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_replication_group_members.*", map[string]string{ - "replication_group_id": aws.StringValue(primaryReplcationGroup.ReplicationGroupId), + "replication_group_id": aws.StringValue(primaryReplicationGroup.ReplicationGroupId), "replication_group_region": testAccGetRegion(), "role": GlobalReplicationGroupMemberRolePrimary, })(s) @@ -121,7 +121,7 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { } func TestAccAWSElasticacheGlobalReplicationGroup_Description(t *testing.T) { - var globalReplcationGroup elasticache.GlobalReplicationGroup + var globalReplicationGroup elasticache.GlobalReplicationGroup rName := acctest.RandomWithPrefix("tf-acc-test") primaryReplicationGroupId := acctest.RandomWithPrefix("tf-acc-test") description1 := acctest.RandString(10) @@ -136,7 +136,7 @@ func TestAccAWSElasticacheGlobalReplicationGroup_Description(t *testing.T) { { Config: testAccAWSElasticacheGlobalReplicationGroupConfig_description(rName, primaryReplicationGroupId, description1), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplicationGroup), resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", description1), ), }, @@ -148,7 +148,7 @@ func TestAccAWSElasticacheGlobalReplicationGroup_Description(t *testing.T) { { Config: testAccAWSElasticacheGlobalReplicationGroupConfig_description(rName, primaryReplicationGroupId, description2), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplicationGroup), resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", description2), ), }, @@ -157,7 +157,7 @@ func TestAccAWSElasticacheGlobalReplicationGroup_Description(t *testing.T) { } func TestAccAWSElasticacheGlobalReplicationGroup_disappears(t *testing.T) { - var globalReplcationGroup elasticache.GlobalReplicationGroup + var globalReplicationGroup elasticache.GlobalReplicationGroup rName := acctest.RandomWithPrefix("tf-acc-test") primaryReplicationGroupId := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_elasticache_global_replication_group.test" @@ -170,7 +170,7 @@ func TestAccAWSElasticacheGlobalReplicationGroup_disappears(t *testing.T) { { Config: testAccAWSElasticacheGlobalReplicationGroupConfig_basic(rName, primaryReplicationGroupId), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplcationGroup), + testAccCheckAWSElasticacheGlobalReplicationGroupExists(resourceName, &globalReplicationGroup), testAccCheckResourceDisappears(testAccProvider, resourceAwsElasticacheGlobalReplicationGroup(), resourceName), ), ExpectNonEmptyPlan: true, From 203c219f2a4df44d8e7321c8ba103ea1496944f7 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 18 Feb 2021 13:01:29 -0800 Subject: [PATCH 23/25] Restores retry on delete operation --- .../service/elasticache/waiter/waiter.go | 2 +- ...ws_elasticache_global_replication_group.go | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/aws/internal/service/elasticache/waiter/waiter.go b/aws/internal/service/elasticache/waiter/waiter.go index 002a5ee2804..b1ab13ced6e 100644 --- a/aws/internal/service/elasticache/waiter/waiter.go +++ b/aws/internal/service/elasticache/waiter/waiter.go @@ -148,7 +148,7 @@ func CacheClusterDeleted(conn *elasticache.ElastiCache, cacheClusterID string, t const ( GlobalReplicationGroupDefaultCreatedTimeout = 20 * time.Minute - GlobalReplicationGroupDefaultUpdatedTimeout = ReplicationGroupDefaultUpdatedTimeout + GlobalReplicationGroupDefaultUpdatedTimeout = 40 * time.Minute GlobalReplicationGroupDefaultDeletedTimeout = 20 * time.Minute globalReplicationGroupAvailableMinTimeout = 10 * time.Second diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index 054b8e5ffe8..91bd85f38b9 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -7,6 +7,8 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/waiter" @@ -262,7 +264,30 @@ func deleteElasticacheGlobalReplicationGroup(conn *elasticache.ElastiCache, id s RetainPrimaryReplicationGroup: aws.Bool(true), } - _, err := conn.DeleteGlobalReplicationGroup(input) + // Using Update timeout because the Global Replication Group could be in the middle of an update operation + err := resource.Retry(waiter.GlobalReplicationGroupDefaultUpdatedTimeout, func() *resource.RetryError { + _, err := conn.DeleteGlobalReplicationGroup(input) + if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeGlobalReplicationGroupNotFoundFault) { + return resource.NonRetryableError(&resource.NotFoundError{ + LastError: err, + LastRequest: input, + }) + } + if tfawserr.ErrMessageContains(err, elasticache.ErrCodeInvalidGlobalReplicationGroupStateFault, "is not empty") { + return resource.RetryableError(err) + } + if err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + if tfresource.TimedOut(err) { + _, err = conn.DeleteGlobalReplicationGroup(input) + } + if tfresource.NotFound(err) { + return nil + } if err != nil { return err } From d203915b064dc8d0a2f9219866167a9f0e148c1f Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 18 Feb 2021 14:08:34 -0800 Subject: [PATCH 24/25] Removes global_replication_group_members since it can't be properly populated --- ...ws_elasticache_global_replication_group.go | 65 +++++++------------ ...asticache_global_replication_group_test.go | 9 --- 2 files changed, 22 insertions(+), 52 deletions(-) diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index 91bd85f38b9..1d8e731f857 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -92,26 +92,28 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { DiffSuppressFunc: elasticacheDescriptionDiffSuppress, StateFunc: elasticacheDescriptionStateFunc, }, - "global_replication_group_members": { - Type: schema.TypeSet, - Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "replication_group_id": { - Type: schema.TypeString, - Computed: true, - }, - "replication_group_region": { - Type: schema.TypeString, - Computed: true, - }, - "role": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, + // global_replication_group_members cannot be correctly implemented because any secondary + // replication groups will be added after this resource completes. + // "global_replication_group_members": { + // Type: schema.TypeSet, + // Computed: true, + // Elem: &schema.Resource{ + // Schema: map[string]*schema.Schema{ + // "replication_group_id": { + // Type: schema.TypeString, + // Computed: true, + // }, + // "replication_group_region": { + // Type: schema.TypeString, + // Computed: true, + // }, + // "role": { + // Type: schema.TypeString, + // Computed: true, + // }, + // }, + // }, + // }, "primary_replication_group_id": { Type: schema.TypeString, Required: true, @@ -198,10 +200,6 @@ func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, me d.Set("primary_replication_group_id", flattenElasticacheGlobalReplicationGroupPrimaryGroupID(globalReplicationGroup.Members)) - if err := d.Set("global_replication_group_members", flattenElasticacheGlobalReplicationGroupMembers(globalReplicationGroup.Members)); err != nil { - return fmt.Errorf("error setting global_cluster_members: %w", err) - } - return nil } @@ -307,22 +305,3 @@ func flattenElasticacheGlobalReplicationGroupPrimaryGroupID(members []*elasticac } return "" } - -func flattenElasticacheGlobalReplicationGroupMembers(members []*elasticache.GlobalReplicationGroupMember) []interface{} { - if len(members) == 0 { - return nil - } - - var tfList []interface{} - - for _, apiObject := range members { - tfMap := map[string]interface{}{ - "replication_group_id": aws.StringValue(apiObject.ReplicationGroupId), - "replication_group_region": aws.StringValue(apiObject.ReplicationGroupRegion), - "role": aws.StringValue(apiObject.Role), - } - tfList = append(tfList, tfMap) - } - - return tfList -} diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index 259f19dc478..8baf3136fb8 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -100,15 +100,6 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "global_replication_group_description", elasticacheEmptyDescription), resource.TestCheckResourceAttr(resourceName, "primary_replication_group_id", primaryReplicationGroupId), resource.TestCheckResourceAttr(resourceName, "transit_encryption_enabled", "false"), - - resource.TestCheckResourceAttr(resourceName, "global_replication_group_members.#", "1"), - func(s *terraform.State) error { - return resource.TestCheckTypeSetElemNestedAttrs(resourceName, "global_replication_group_members.*", map[string]string{ - "replication_group_id": aws.StringValue(primaryReplicationGroup.ReplicationGroupId), - "replication_group_region": testAccGetRegion(), - "role": GlobalReplicationGroupMemberRolePrimary, - })(s) - }, ), }, { From b83eab6d3d3322088558dc2815add4c28cb4c70a Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Thu, 18 Feb 2021 16:24:01 -0800 Subject: [PATCH 25/25] Updates documentation --- .../docs/r/elasticache_global_replication_group.html.markdown | 4 ---- 1 file changed, 4 deletions(-) diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 0ebd733dc44..f578d36f94f 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -54,10 +54,6 @@ In addition to all arguments above, the following attributes are exported: * `cluster_enabled` - Indicates whether the Global Datastore is cluster enabled. * `engine` - The name of the cache engine to be used for the clusters in this global replication group. * `global_replication_group_id` - The full ID of the global replication group. -* `global_replication_group_members` - Set of the replication groups that are part of this global replication group. - * `replication_group_id` - The replication group id of the Global Datastore member. - * `replication_group_region` - The AWS region of the Global Datastore member. - * `role` - Indicates the role of the replication group, either `PRIMARY` or `SECONDARY`. * `transit_encryption_enabled` - A flag that indicates whether the encryption in transit is enabled. ## Import