Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for "single-outpost" outpost_mode and preferred_outpost_arn to elasticache #27934

Merged
merged 11 commits into from
Nov 23, 2022
7 changes: 7 additions & 0 deletions .changelog/27934.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
data-source/aws_elasticache_cluster: Add `cache_nodes.outpost_arn` and `preferred_outpost_arn` attributes
```

```release-note:enhancement
resource/aws_elasticache_cluster: Add `outpost_mode` and `preferred_outpost_arn` arguments and `cache_nodes.outpost_arn` attribute. NOTE: Because we cannot easily test this functionality, it is best effort and we ask for community help in testing
```
28 changes: 28 additions & 0 deletions internal/service/elasticache/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ func ResourceCluster() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"outpost_arn": {
Type: schema.TypeString,
Computed: true,
},
"port": {
Type: schema.TypeInt,
Computed: true,
Expand Down Expand Up @@ -209,6 +213,13 @@ func ResourceCluster() *schema.Resource {
Optional: true,
Computed: true,
},
"outpost_mode": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
RequiredWith: []string{"preferred_outpost_arn"},
ValidateFunc: validation.StringInSlice(elasticache.OutpostMode_Values(), false),
},
"parameter_group_name": {
Type: schema.TypeString,
Optional: true,
Expand All @@ -232,6 +243,13 @@ func ResourceCluster() *schema.Resource {
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"preferred_outpost_arn": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: verify.ValidARN,
},
"replication_group_id": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -355,6 +373,14 @@ func resourceClusterCreate(d *schema.ResourceData, meta interface{}) error {
req.NumCacheNodes = aws.Int64(int64(v.(int)))
}

if v, ok := d.GetOk("outpost_mode"); ok {
req.OutpostMode = aws.String(v.(string))
}

if v, ok := d.GetOk("preferred_outpost_arn"); ok {
req.PreferredOutpostArn = aws.String(v.(string))
}

if v, ok := d.GetOk("engine"); ok {
req.Engine = aws.String(v.(string))
}
Expand Down Expand Up @@ -526,6 +552,7 @@ func resourceClusterRead(d *schema.ResourceData, meta interface{}) error {

d.Set("ip_discovery", c.IpDiscovery)
d.Set("network_type", c.NetworkType)
d.Set("preferred_outpost_arn", c.PreferredOutpostArn)

tags, err := ListTags(conn, aws.StringValue(c.ARN))

Expand Down Expand Up @@ -771,6 +798,7 @@ func setCacheNodeData(d *schema.ResourceData, c *elasticache.CacheCluster) error
"address": aws.StringValue(node.Endpoint.Address),
"port": aws.Int64Value(node.Endpoint.Port),
"availability_zone": aws.StringValue(node.CustomerAvailabilityZone),
"outpost_arn": aws.StringValue(node.CustomerOutpostArn),
})
}

Expand Down
9 changes: 9 additions & 0 deletions internal/service/elasticache/cluster_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func DataSourceCluster() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"outpost_arn": {
Type: schema.TypeString,
Computed: true,
},
"port": {
Type: schema.TypeInt,
Computed: true,
Expand Down Expand Up @@ -130,6 +134,10 @@ func DataSourceCluster() *schema.Resource {
Type: schema.TypeInt,
Computed: true,
},
"preferred_outpost_arn": {
Type: schema.TypeString,
Computed: true,
},
"replication_group_id": {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -184,6 +192,7 @@ func dataSourceClusterRead(d *schema.ResourceData, meta interface{}) error {
d.Set("engine_version", cluster.EngineVersion)
d.Set("ip_discovery", cluster.IpDiscovery)
d.Set("network_type", cluster.NetworkType)
d.Set("preferred_outpost_arn", cluster.PreferredOutpostArn)
d.Set("security_group_names", flattenSecurityGroupNames(cluster.CacheSecurityGroups))
d.Set("security_group_ids", flattenSecurityGroupIDs(cluster.SecurityGroups))

Expand Down
1 change: 1 addition & 0 deletions internal/service/elasticache/cluster_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestAccElastiCacheClusterDataSource_Data_basic(t *testing.T) {
resource.TestCheckResourceAttrPair(dataSourceName, "node_type", resourceName, "node_type"),
resource.TestCheckResourceAttrPair(dataSourceName, "num_cache_nodes", resourceName, "num_cache_nodes"),
resource.TestCheckResourceAttrPair(dataSourceName, "port", resourceName, "port"),
resource.TestCheckResourceAttrPair(dataSourceName, "preferred_outpost_arn", resourceName, "preferred_outpost_arn"),
),
},
},
Expand Down
195 changes: 195 additions & 0 deletions internal/service/elasticache/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,13 @@ func TestAccElastiCacheCluster_Engine_redis(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "auto_minor_version_upgrade", "true"),
resource.TestCheckResourceAttr(resourceName, "cache_nodes.#", "1"),
resource.TestCheckResourceAttr(resourceName, "cache_nodes.0.id", "0001"),
resource.TestCheckResourceAttr(resourceName, "cache_nodes.0.outpost_arn", ""),
resource.TestCheckResourceAttr(resourceName, "engine", "redis"),
resource.TestMatchResourceAttr(resourceName, "engine_version_actual", regexp.MustCompile(`^7\.[[:digit:]]+\.[[:digit:]]+$`)),
resource.TestCheckResourceAttr(resourceName, "ip_discovery", "ipv4"),
resource.TestCheckResourceAttr(resourceName, "network_type", "ipv4"),
resource.TestCheckNoResourceAttr(resourceName, "outpost_mode"),
resource.TestCheckResourceAttr(resourceName, "preferred_outpost_arn", ""),
),
},
{
Expand Down Expand Up @@ -1191,6 +1194,148 @@ func TestAccElastiCacheCluster_tagWithOtherModification(t *testing.T) {
})
}

func TestAccElastiCacheCluster_outpost_memcached(t *testing.T) {
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}

var ec elasticache.CacheCluster
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_elasticache_cluster.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckOutpostsOutposts(t) },
ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccClusterConfig_outpost_memcached(rName, 0),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckClusterExists(resourceName, &ec),
resource.TestCheckResourceAttr(resourceName, "cache_nodes.0.id", "0001"),
resource.TestCheckResourceAttrSet(resourceName, "cache_nodes.0.outpost_arn"),
resource.TestCheckResourceAttrSet(resourceName, "configuration_endpoint"),
resource.TestCheckResourceAttrSet(resourceName, "cluster_address"),
resource.TestCheckResourceAttr(resourceName, "engine", "memcached"),
resource.TestCheckResourceAttr(resourceName, "port", "11211"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"apply_immediately",
},
},
},
})
}

func TestAccElastiCacheCluster_outpost_redis(t *testing.T) {
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}

var ec elasticache.CacheCluster
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_elasticache_cluster.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckOutpostsOutposts(t) },
ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccClusterConfig_outpost_redis(rName, 0),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckClusterExists(resourceName, &ec),
resource.TestCheckResourceAttr(resourceName, "cache_nodes.0.id", "0001"),
resource.TestCheckResourceAttrSet(resourceName, "cache_nodes.0.outpost_arn"),
resource.TestCheckResourceAttr(resourceName, "engine", "redis"),
resource.TestMatchResourceAttr(resourceName, "engine_version_actual", regexp.MustCompile(`^7\.[[:digit:]]+\.[[:digit:]]+$`)),
resource.TestCheckResourceAttr(resourceName, "port", "6379"),
resource.TestCheckResourceAttr(resourceName, "auto_minor_version_upgrade", "true"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{
"apply_immediately",
},
},
},
})
}

func TestAccElastiCacheCluster_outpostID_memcached(t *testing.T) {
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}

var pre, post elasticache.CacheCluster
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_elasticache_cluster.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckOutpostsOutposts(t) },
ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccClusterConfig_outpost_memcached(rName, 0),
Check: resource.ComposeTestCheckFunc(
testAccCheckClusterExists(resourceName, &pre),
),
},
{
Config: testAccClusterConfig_outpost_memcached(rName, 1),
Check: resource.ComposeTestCheckFunc(
testAccCheckClusterExists(resourceName, &post),
testAccCheckClusterRecreated(&pre, &post),
),
},
},
})
}

func TestAccElastiCacheCluster_outpostID_redis(t *testing.T) {
if testing.Short() {
t.Skip("skipping long-running test in short mode")
}

var pre, post elasticache.CacheCluster
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_elasticache_cluster.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckOutpostsOutposts(t) },
ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckClusterDestroy,
Steps: []resource.TestStep{
{
Config: testAccClusterConfig_outpost_redis(rName, 0),
Check: resource.ComposeTestCheckFunc(
testAccCheckClusterExists(resourceName, &pre),
),
},
{
Config: testAccClusterConfig_outpost_redis(rName, 1),
Check: resource.ComposeTestCheckFunc(
testAccCheckClusterExists(resourceName, &post),
testAccCheckClusterNotRecreated(&pre, &post),
),
},
},
})
}

func testAccCheckClusterAttributes(v *elasticache.CacheCluster) resource.TestCheckFunc {
return func(s *terraform.State) error {
if v.NotificationConfiguration == nil {
Expand Down Expand Up @@ -1331,6 +1476,56 @@ resource "aws_elasticache_cluster" "test" {
`, rName)
}

func testAccClusterConfig_outpost_memcached(rName string, outpostID int) string {
return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 1), fmt.Sprintf(`
data "aws_outposts_outposts" "test" {}

data "aws_outposts_outpost" "test" {
id = tolist(data.aws_outposts_outposts.test.ids)[%[2]d]
}

resource "aws_elasticache_subnet_group" "test" {
name = %[1]q
subnet_ids = aws_subnet.test[*].id
}

resource "aws_elasticache_cluster" "test" {
cluster_id = %[1]q
outpost_mode = "single-outpost"
preferred_outpost_arn = data.aws_outposts_outpost.test.arn
engine = "memcached"
node_type = "cache.r5.large"
num_cache_nodes = 1
subnet_group_name = aws_elasticache_subnet_group.test.name
}
`, rName, outpostID))
}

func testAccClusterConfig_outpost_redis(rName string, outpostID int) string {
return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 1), fmt.Sprintf(`
data "aws_outposts_outposts" "test" {}

data "aws_outposts_outpost" "test" {
id = tolist(data.aws_outposts_outposts.test.ids)[%[2]d]
}

resource "aws_elasticache_subnet_group" "test" {
name = %[1]q
subnet_ids = aws_subnet.test[*].id
}

resource "aws_elasticache_cluster" "test" {
cluster_id = %[1]q
outpost_mode = "single-outpost"
preferred_outpost_arn = data.aws_outposts_outpost.test.arn
engine = "redis"
node_type = "cache.r5.large"
num_cache_nodes = 1
subnet_group_name = aws_elasticache_subnet_group.test.name
}
`, rName, outpostID))
}

func testAccClusterConfig_parameterGroupName(rName, engine, engineVersion, parameterGroupName string) string {
return fmt.Sprintf(`
resource "aws_elasticache_cluster" "test" {
Expand Down
3 changes: 2 additions & 1 deletion website/docs/d/elasticache_cluster.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ SNS topic that ElastiCache notifications get sent to.
accept connections.
* `configuration_endpoint` - (Memcached only) Configuration endpoint to allow host discovery.
* `cluster_address` - (Memcached only) DNS name of the cache cluster without the port appended.
* `cache_nodes` - List of node objects including `id`, `address`, `port` and `availability_zone`.
* `preferred_outpost_arn` - The outpost ARN in which the cache cluster was created if created in outpost.
* `cache_nodes` - List of node objects including `id`, `address`, `port`, `availability_zone` and `outpost_arn`.
Referenceable e.g., as `${data.aws_elasticache_cluster.bar.cache_nodes.0.address}`
* `tags` - Tags assigned to the resource
Loading