Skip to content

Commit

Permalink
Merge pull request #256 from Petersoj/elasticache-serverless
Browse files Browse the repository at this point in the history
feat(elasticache): add ElastiCache Serverless nuking
  • Loading branch information
ekristen authored Aug 31, 2024
2 parents c9d1fdc + 5c65554 commit f3d5dda
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 129 deletions.
139 changes: 139 additions & 0 deletions resources/elasticache-cachecluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package resources

import (
"context"

"github.com/gotidy/ptr"
"github.com/sirupsen/logrus"

"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/service/elasticache/elasticacheiface"

"github.com/ekristen/libnuke/pkg/registry"
"github.com/ekristen/libnuke/pkg/resource"
"github.com/ekristen/libnuke/pkg/types"

"github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

const ElasticacheCacheClusterResource = "ElasticacheCacheCluster"

func init() {
registry.Register(&registry.Registration{
Name: ElasticacheCacheClusterResource,
Scope: nuke.Account,
Lister: &ElasticacheCacheClusterLister{},
DependsOn: []string{
ElasticacheCacheParameterGroupResource,
ElasticacheReplicationGroupResource,
ElasticacheSubnetGroupResource,
},
})
}

type ElasticacheCacheClusterLister struct {
mockSvc elasticacheiface.ElastiCacheAPI
}

func (l *ElasticacheCacheClusterLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) {
opts := o.(*nuke.ListerOpts)

var svc elasticacheiface.ElastiCacheAPI
if l.mockSvc != nil {
svc = l.mockSvc
} else {
svc = elasticache.New(opts.Session)
}

params := &elasticache.DescribeCacheClustersInput{MaxRecords: ptr.Int64(100)}
resp, err := svc.DescribeCacheClusters(params)
if err != nil {
return nil, err
}

var resources []resource.Resource
for _, cacheCluster := range resp.CacheClusters {
tags, err := l.getResourceTags(svc, cacheCluster.ARN)
if err != nil {
logrus.WithError(err).Error("unable to retrieve tags")
}

resources = append(resources, &ElasticacheCacheCluster{
svc: svc,
ClusterID: cacheCluster.CacheClusterId,
Status: cacheCluster.CacheClusterStatus,
Tags: tags,
})
}

serverlessParams := &elasticache.DescribeServerlessCachesInput{MaxResults: ptr.Int64(100)}
serverlessResp, serverlessErr := svc.DescribeServerlessCaches(serverlessParams)
if serverlessErr != nil {
return nil, serverlessErr
}

for _, serverlessCache := range serverlessResp.ServerlessCaches {
var tags []*elasticache.Tag

if ptr.ToString(serverlessCache.Status) == "available" ||
ptr.ToString(serverlessCache.Status) == "modifying" {
tags, err = l.getResourceTags(svc, serverlessCache.ARN)
if err != nil {
logrus.WithError(err).Error("unable to retrieve tags")
}
}

resources = append(resources, &ElasticacheCacheCluster{
svc: svc,
Serverless: true,
ClusterID: serverlessCache.ServerlessCacheName,
Status: serverlessCache.Status,
Tags: tags,
})
}

return resources, nil
}

func (l *ElasticacheCacheClusterLister) getResourceTags(svc elasticacheiface.ElastiCacheAPI, arn *string) ([]*elasticache.Tag, error) {
tags, err := svc.ListTagsForResource(&elasticache.ListTagsForResourceInput{
ResourceName: arn,
})
if err != nil {
return []*elasticache.Tag{}, err
}

return tags.TagList, nil
}

type ElasticacheCacheCluster struct {
svc elasticacheiface.ElastiCacheAPI
ClusterID *string
Status *string
Serverless bool
Tags []*elasticache.Tag
}

func (r *ElasticacheCacheCluster) Remove(_ context.Context) error {
if r.Serverless {
_, err := r.svc.DeleteServerlessCache(&elasticache.DeleteServerlessCacheInput{
ServerlessCacheName: r.ClusterID,
})

return err
}

_, err := r.svc.DeleteCacheCluster(&elasticache.DeleteCacheClusterInput{
CacheClusterId: r.ClusterID,
})

return err
}

func (r *ElasticacheCacheCluster) Properties() types.Properties {
return types.NewPropertiesFromStruct(r)
}

func (r *ElasticacheCacheCluster) String() string {
return *r.ClusterID
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
"testing"

"github.com/golang/mock/gomock"
"github.com/gotidy/ptr"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elasticache"

Expand All @@ -27,17 +27,38 @@ func Test_Mock_ElastiCache_CacheCluster_Remove(t *testing.T) {

cacheCluster := ElasticacheCacheCluster{
svc: mockElastiCache,
clusterID: aws.String("foobar"),
ClusterID: ptr.String("foobar"),
}

mockElastiCache.EXPECT().DeleteCacheCluster(&elasticache.DeleteCacheClusterInput{
CacheClusterId: aws.String("foobar"),
CacheClusterId: ptr.String("foobar"),
}).Return(&elasticache.DeleteCacheClusterOutput{}, nil)

err := cacheCluster.Remove(context.TODO())
a.Nil(err)
}

func Test_Mock_ElastiCache_CacheCluster_Remove_Serverless(t *testing.T) {
a := assert.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockElastiCache := mock_elasticacheiface.NewMockElastiCacheAPI(ctrl)

cacheCluster := ElasticacheCacheCluster{
svc: mockElastiCache,
ClusterID: ptr.String("foobar"),
Serverless: true,
}

mockElastiCache.EXPECT().DeleteServerlessCache(&elasticache.DeleteServerlessCacheInput{
ServerlessCacheName: ptr.String("foobar"),
}).Return(&elasticache.DeleteServerlessCacheOutput{}, nil)

err := cacheCluster.Remove(context.TODO())
a.Nil(err)
}

func Test_Mock_ElastiCache_CacheCluster_List_NoTags(t *testing.T) {
a := assert.New(t)
ctrl := gomock.NewController(t)
Expand All @@ -52,23 +73,38 @@ func Test_Mock_ElastiCache_CacheCluster_List_NoTags(t *testing.T) {
mockElastiCache.EXPECT().DescribeCacheClusters(gomock.Any()).Return(&elasticache.DescribeCacheClustersOutput{
CacheClusters: []*elasticache.CacheCluster{
{
ARN: aws.String("arn:aws:elasticache:us-west-2:123456789012:cluster:foobar"),
CacheClusterId: aws.String("foobar"),
CacheClusterStatus: aws.String("available"),
ARN: ptr.String("arn:aws:elasticache:us-west-2:123456789012:cluster:foobar"),
CacheClusterId: ptr.String("foobar"),
CacheClusterStatus: ptr.String("available"),
},
},
}, nil)
mockElastiCache.EXPECT().DescribeServerlessCaches(gomock.Any()).Return(&elasticache.DescribeServerlessCachesOutput{
ServerlessCaches: []*elasticache.ServerlessCache{
{
ARN: ptr.String("arn:aws:elasticache:us-west-2:123456789012:serverless:foobar"),
ServerlessCacheName: ptr.String("serverless"),
Status: ptr.String("available"),
},
},
}, nil)

mockElastiCache.EXPECT().ListTagsForResource(&elasticache.ListTagsForResourceInput{
ResourceName: ptr.String("arn:aws:elasticache:us-west-2:123456789012:cluster:foobar"),
}).Return(&elasticache.TagListMessage{}, nil)

mockElastiCache.EXPECT().ListTagsForResource(&elasticache.ListTagsForResourceInput{
ResourceName: aws.String("arn:aws:elasticache:us-west-2:123456789012:cluster:foobar"),
ResourceName: ptr.String("arn:aws:elasticache:us-west-2:123456789012:serverless:foobar"),
}).Return(&elasticache.TagListMessage{}, nil)

resources, err := cacheClusterLister.List(context.TODO(), &nuke.ListerOpts{})
a.Nil(err)
a.Len(resources, 1)
a.Len(resources, 2)

resource := resources[0].(*ElasticacheCacheCluster)
a.Equal("foobar", resource.String())
serverlessResource := resources[1].(*ElasticacheCacheCluster)
a.Equal("serverless", serverlessResource.String())
}

func Test_Mock_ElastiCache_CacheCluster_List_WithTags(t *testing.T) {
Expand All @@ -85,36 +121,48 @@ func Test_Mock_ElastiCache_CacheCluster_List_WithTags(t *testing.T) {
mockElastiCache.EXPECT().DescribeCacheClusters(gomock.Any()).Return(&elasticache.DescribeCacheClustersOutput{
CacheClusters: []*elasticache.CacheCluster{
{
ARN: aws.String("arn:aws:elasticache:us-west-2:123456789012:cluster:foobar"),
CacheClusterId: aws.String("foobar"),
ARN: ptr.String("arn:aws:elasticache:us-west-2:123456789012:cluster:foobar"),
CacheClusterId: ptr.String("foobar"),
},
},
}, nil)
mockElastiCache.EXPECT().DescribeServerlessCaches(gomock.Any()).Return(&elasticache.DescribeServerlessCachesOutput{
ServerlessCaches: []*elasticache.ServerlessCache{
{
ARN: ptr.String("arn:aws:elasticache:us-west-2:123456789012:serverless:foobar"),
ServerlessCacheName: ptr.String("serverless"),
},
},
}, nil)

mockElastiCache.EXPECT().ListTagsForResource(&elasticache.ListTagsForResourceInput{
ResourceName: aws.String("arn:aws:elasticache:us-west-2:123456789012:cluster:foobar"),
ResourceName: ptr.String("arn:aws:elasticache:us-west-2:123456789012:cluster:foobar"),
}).Return(&elasticache.TagListMessage{
TagList: []*elasticache.Tag{
{
Key: aws.String("Name"),
Value: aws.String("foobar"),
Key: ptr.String("Name"),
Value: ptr.String("foobar"),
},
{
Key: aws.String("aws-nuke"),
Value: aws.String("test"),
Key: ptr.String("aws-nuke"),
Value: ptr.String("test"),
},
},
}, nil)

resources, err := cacheClusterLister.List(context.TODO(), &nuke.ListerOpts{})
a.Nil(err)
a.Len(resources, 1)
a.Len(resources, 2)

resource := resources[0].(*ElasticacheCacheCluster)
a.Len(resource.Tags, 2)
a.Equal("foobar", resource.String())
a.Equal("foobar", resource.Properties().Get("tag:Name"))
a.Equal("test", resource.Properties().Get("tag:aws-nuke"))

serverlessResource := resources[1].(*ElasticacheCacheCluster)
a.Nil(serverlessResource.Tags)
a.Equal("serverless", serverlessResource.String())
}

func Test_Mock_ElastiCache_CacheCluster_List_TagsInvalidARN(t *testing.T) {
Expand Down Expand Up @@ -144,19 +192,32 @@ func Test_Mock_ElastiCache_CacheCluster_List_TagsInvalidARN(t *testing.T) {
mockElastiCache.EXPECT().DescribeCacheClusters(gomock.Any()).Return(&elasticache.DescribeCacheClustersOutput{
CacheClusters: []*elasticache.CacheCluster{
{
ARN: aws.String("foobar:invalid:arn"),
CacheClusterId: aws.String("foobar"),
ARN: ptr.String("foobar:invalid:arn"),
CacheClusterId: ptr.String("foobar"),
},
},
}, nil)
mockElastiCache.EXPECT().DescribeServerlessCaches(gomock.Any()).Return(&elasticache.DescribeServerlessCachesOutput{
ServerlessCaches: []*elasticache.ServerlessCache{
{
ARN: ptr.String("foobar:invalid:arn"),
ServerlessCacheName: ptr.String("serverless"),
Status: ptr.String("available"),
},
},
}, nil)

mockElastiCache.EXPECT().ListTagsForResource(&elasticache.ListTagsForResourceInput{
ResourceName: aws.String("foobar:invalid:arn"),
ResourceName: ptr.String("foobar:invalid:arn"),
}).Return(nil, awserr.New(elasticache.ErrCodeInvalidARNFault, elasticache.ErrCodeInvalidARNFault, nil))

mockElastiCache.EXPECT().ListTagsForResource(&elasticache.ListTagsForResourceInput{
ResourceName: ptr.String("foobar:invalid:arn"),
}).Return(nil, awserr.New(elasticache.ErrCodeInvalidARNFault, elasticache.ErrCodeInvalidARNFault, nil))

resources, err := cacheClusterLister.List(context.TODO(), &nuke.ListerOpts{})
a.Nil(err)
a.Len(resources, 0)
a.Len(resources, 2)

a.True(called, "expected global hook called and log message to be found")
}
Loading

0 comments on commit f3d5dda

Please sign in to comment.