Skip to content

Commit

Permalink
Merge pull request #17615 from hashicorp/elasticache_global_replicati…
Browse files Browse the repository at this point in the history
…on_group

Adds ElastiCache Global Replication Group basic support
  • Loading branch information
gdavison authored Feb 19, 2021
2 parents bb0ac71 + b83eab6 commit 00649b8
Show file tree
Hide file tree
Showing 15 changed files with 962 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .changelog/15885.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_elasticache_global_replication_group
```
13 changes: 11 additions & 2 deletions aws/aws_sweeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down
27 changes: 27 additions & 0 deletions aws/internal/envvar/funcs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package envvar

import (
"fmt"
"os"
)

Expand All @@ -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
}
116 changes: 116 additions & 0 deletions aws/internal/envvar/funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,119 @@ func TestGetWithDefault(t *testing.T) {
}
})
}

func TestRequireOneOf(t *testing.T) {
envVar1 := "TESTENVVAR_REQUIREONEOF1"
envVar2 := "TESTENVVAR_REQUIREONEOF2"
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_REQUIRE"

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)
}
})
}
14 changes: 5 additions & 9 deletions aws/internal/envvar/testing_funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
66 changes: 60 additions & 6 deletions aws/internal/service/elasticache/finder/finder.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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,
Expand All @@ -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 nil, err
}

clusters, err := CacheClustersByID(conn, aws.StringValueSlice(rg.MemberClusters))
Expand Down Expand Up @@ -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 globalReplicationGroup == nil || 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),
}
}
30 changes: 27 additions & 3 deletions aws/internal/service/elasticache/waiter/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}
Loading

0 comments on commit 00649b8

Please sign in to comment.