diff --git a/README.md b/README.md index a432015..a6c57d8 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ exist in your statefiles, and prints the missing ones in a json format for ease | lambda | lambda_function | | dynamodb | dynamodb_table | | cloudwatchlogs | cloudwatchlogs_log_group | +| elasticache | elasticache_cluster | ## How to use diff --git a/aws/elasticache.go b/aws/elasticache.go new file mode 100644 index 0000000..2e0e8af --- /dev/null +++ b/aws/elasticache.go @@ -0,0 +1,66 @@ +package aws + +import ( + "context" + "log" + + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticache" + "github.com/noclickops/common" +) + +type ElasticacheClient interface { + DescribeCacheClusters(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) +} + +type NoclickopsElasticacheClient struct { + Client ElasticacheClient + ClientMeta +} + +type NoclickopsElasticacheService struct { + Clients []NoclickopsElasticacheClient + common.ServiceMeta +} + +func NewElasticacheServiceFromConfigs(cfg []awssdk.Config, meta common.ServiceMeta) NoclickopsElasticacheService { + service := NoclickopsElasticacheService{ServiceMeta: meta} + for _, c := range cfg { + service.Clients = append(service.Clients, NoclickopsElasticacheClient{ + Client: elasticache.NewFromConfig(c), + ClientMeta: ClientMeta{Region: c.Region}, + }) + } + return service +} + +func (s *NoclickopsElasticacheService) GetAllResources() []common.Resource { + return s.GetClusters() +} + +func (s *NoclickopsElasticacheService) GetClusters() []common.Resource { + var resources []common.Resource + for _, rc := range s.Clients { + var marker *string = nil + for { + res, err := rc.Client.DescribeCacheClusters(context.TODO(), &elasticache.DescribeCacheClustersInput{ + Marker: marker, + ShowCacheClustersNotInReplicationGroups: awssdk.Bool(true), + }) + if err != nil { + log.Printf("warning: %v", err) + break + } + + for _, cluster := range res.CacheClusters { + resources = append(resources, common.Resource{Arn: *cluster.ARN, TerraformID: *cluster.CacheClusterId, ResourceType: common.Elasticache_cluster, Region: rc.Region}) + } + + if res.Marker == nil { + break + } + marker = res.Marker + } + } + return resources +} diff --git a/aws/elasticache_test.go b/aws/elasticache_test.go new file mode 100644 index 0000000..1723891 --- /dev/null +++ b/aws/elasticache_test.go @@ -0,0 +1,71 @@ +package aws_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/elasticache" + "github.com/aws/aws-sdk-go-v2/service/elasticache/types" + "github.com/google/go-cmp/cmp" + "github.com/noclickops/aws" + "github.com/noclickops/common" +) + +func getMockedElasticacheService(mock *mockElasticacheClient) aws.NoclickopsElasticacheService { + return aws.NoclickopsElasticacheService{ + Clients: []aws.NoclickopsElasticacheClient{ + { + Client: mock, + ClientMeta: aws.ClientMeta{Region: "eu-west-1"}, + }, + }, + ServiceMeta: common.ServiceMeta{Global: false, ServiceName: "elasticache"}, + } +} + +func TestGetCacheClusters_PaginationFollowed(t *testing.T) { + callCount := 0 + mock := &mockElasticacheClient{ + describeCacheClustersFn: func(_ context.Context, params *elasticache.DescribeCacheClustersInput, _ ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) { + callCount++ + if callCount == 1 { + return &elasticache.DescribeCacheClustersOutput{ + Marker: ptr("next"), + CacheClusters: []types.CacheCluster{{CacheClusterId: ptr("cluster-1"), ARN: ptr("arn:aws:elasticache:eu-west-1:123456789012:cluster:cluster-1")}}, + }, nil + } + if params.Marker == nil || *params.Marker != "next" { + return nil, fmt.Errorf("wrong Marker, expected 'next' got '%v'", params.Marker) + } + return &elasticache.DescribeCacheClustersOutput{ + CacheClusters: []types.CacheCluster{{CacheClusterId: ptr("cluster-2"), ARN: ptr("arn:aws:elasticache:eu-west-1:123456789012:cluster:cluster-2")}}, + }, nil + }, + } + service := getMockedElasticacheService(mock) + got := service.GetClusters() + expected := []common.Resource{ + {Arn: "arn:aws:elasticache:eu-west-1:123456789012:cluster:cluster-1", TerraformID: "cluster-1", ResourceType: common.Elasticache_cluster, Region: "eu-west-1"}, + {Arn: "arn:aws:elasticache:eu-west-1:123456789012:cluster:cluster-2", TerraformID: "cluster-2", ResourceType: common.Elasticache_cluster, Region: "eu-west-1"}, + } + if diff := cmp.Diff(got, expected); diff != "" { + t.Errorf("expected %v, got %v", expected, got) + } + if callCount != 2 { + t.Errorf("expected 2 calls to DescribeCacheClusters, got %d", callCount) + } +} + +func TestGetCacheClusters_ErrorIsSkipped(t *testing.T) { + mock := &mockElasticacheClient{ + describeCacheClustersFn: func(_ context.Context, _ *elasticache.DescribeCacheClustersInput, _ ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) { + return nil, fmt.Errorf("some error") + }, + } + service := getMockedElasticacheService(mock) + got := service.GetClusters() + if len(got) != 0 { + t.Errorf("expected no resources on error, got %v", got) + } +} diff --git a/aws/helpers_test.go b/aws/helpers_test.go index 000af64..21a5a47 100644 --- a/aws/helpers_test.go +++ b/aws/helpers_test.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/elasticache" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/eks" @@ -214,3 +215,11 @@ type mockDynamodbClient struct { func (m *mockDynamodbClient) ListTables(ctx context.Context, params *dynamodb.ListTablesInput, optFns ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) { return m.listTablesFn(ctx, params, optFns...) } + +type mockElasticacheClient struct { + describeCacheClustersFn func(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) +} + +func (m *mockElasticacheClient) DescribeCacheClusters(ctx context.Context, params *elasticache.DescribeCacheClustersInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) { + return m.describeCacheClustersFn(ctx, params, optFns...) +} diff --git a/aws/types.go b/aws/types.go index 914109a..d0e1920 100644 --- a/aws/types.go +++ b/aws/types.go @@ -28,6 +28,7 @@ var SERVICES = map[common.AWSServiceName]common.ServiceMeta{ common.Lambda: {Global: false, ServiceName: "lambda"}, common.CloudwatchLogs: {Global: false, ServiceName: "cloudwatchlogs"}, common.Dynamodb: {Global: false, ServiceName: "dynamodb"}, + common.Elasticache: {Global: false, ServiceName: "elasticache"}, // not included in the switch/case in `NewclickopsServiceFromConfigs` // because it doesn't follow the same invocation pattern. @@ -99,6 +100,9 @@ func NewNoclickopsServiceFromConfigs(service common.AWSServiceName, configs []aw case common.Lambda: c := NewLambdaServiceFromConfigs(configs, meta) return &c + case common.Elasticache: + c := NewElasticacheServiceFromConfigs(configs, meta) + return &c case common.IdentityStore: ssoMeta := SERVICES[common.SSOAdmin] ssoClient := NewSSOAdminServiceFromConfigs(configs[:1], ssoMeta) diff --git a/common/awsservicename_string.go b/common/awsservicename_string.go index db460b0..82aca46 100644 --- a/common/awsservicename_string.go +++ b/common/awsservicename_string.go @@ -26,11 +26,12 @@ func _() { _ = x[ResourceGroupsTaggingAPI-15] _ = x[CloudwatchLogs-16] _ = x[Dynamodb-17] + _ = x[Elasticache-18] } -const _AWSServiceName_name = "Route53IAMEKSSSMEC2RDSSSOAdminIdentityStoreSNSS3CloudFrontELBELBV2ASGLambdaResourceGroupsTaggingAPICloudwatchLogsDynamodb" +const _AWSServiceName_name = "Route53IAMEKSSSMEC2RDSSSOAdminIdentityStoreSNSS3CloudFrontELBELBV2ASGLambdaResourceGroupsTaggingAPICloudwatchLogsDynamodbElasticache" -var _AWSServiceName_index = [...]uint8{0, 7, 10, 13, 16, 19, 22, 30, 43, 46, 48, 58, 61, 66, 69, 75, 99, 113, 121} +var _AWSServiceName_index = [...]uint8{0, 7, 10, 13, 16, 19, 22, 30, 43, 46, 48, 58, 61, 66, 69, 75, 99, 113, 121, 132} func (i AWSServiceName) String() string { idx := int(i) - 0 diff --git a/common/resourcetype_string.go b/common/resourcetype_string.go index 09614ff..213c9f0 100644 --- a/common/resourcetype_string.go +++ b/common/resourcetype_string.go @@ -40,11 +40,12 @@ func _() { _ = x[Autoscaling_group-29] _ = x[Cloudwatchlogs_log_group-30] _ = x[Dynamodb_table-31] + _ = x[Elasticache_cluster-32] } -const _ResourceType_name = "Route53_zoneRoute53_recordIAM_policyIAM_userIAM_groupSSM_parameterSecurity_groupSecurity_group_ruleEKS_clusterEKS_node_groupSSOAdmin_permission_setIdentitystore_userIdentitystore_groupInstanceEipDB_instanceRDS_clusterSNS_topicSNS_subscriptionS3_bucketCloudFront_distributionELB_load_balancerELBV2_load_balancerVPCInternet_gatewayNAT_gatewaySubnetVPC_endpointLambda_functionAutoscaling_groupCloudwatchlogs_log_groupDynamodb_table" +const _ResourceType_name = "Route53_zoneRoute53_recordIAM_policyIAM_userIAM_groupSSM_parameterSecurity_groupSecurity_group_ruleEKS_clusterEKS_node_groupSSOAdmin_permission_setIdentitystore_userIdentitystore_groupInstanceEipDB_instanceRDS_clusterSNS_topicSNS_subscriptionS3_bucketCloudFront_distributionELB_load_balancerELBV2_load_balancerVPCInternet_gatewayNAT_gatewaySubnetVPC_endpointLambda_functionAutoscaling_groupCloudwatchlogs_log_groupDynamodb_tableElasticache_cluster" -var _ResourceType_index = [...]uint16{0, 12, 26, 36, 44, 53, 66, 80, 99, 110, 124, 147, 165, 184, 192, 195, 206, 217, 226, 242, 251, 274, 291, 310, 313, 329, 340, 346, 358, 373, 390, 414, 428} +var _ResourceType_index = [...]uint16{0, 12, 26, 36, 44, 53, 66, 80, 99, 110, 124, 147, 165, 184, 192, 195, 206, 217, 226, 242, 251, 274, 291, 310, 313, 329, 340, 346, 358, 373, 390, 414, 428, 447} func (i ResourceType) String() string { idx := int(i) - 0 diff --git a/common/types.go b/common/types.go index 13182ac..c4ed0fa 100644 --- a/common/types.go +++ b/common/types.go @@ -38,6 +38,7 @@ const ( Autoscaling_group Cloudwatchlogs_log_group Dynamodb_table + Elasticache_cluster ) func (r ResourceType) MarshalJSON() ([]byte, error) { @@ -66,6 +67,7 @@ const ( ResourceGroupsTaggingAPI CloudwatchLogs Dynamodb + Elasticache ) type Resource struct { diff --git a/go.mod b/go.mod index b294bac..15b7436 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/dynamodb v1.57.4 github.com/aws/aws-sdk-go-v2/service/ec2 v1.299.0 github.com/aws/aws-sdk-go-v2/service/eks v1.82.1 + github.com/aws/aws-sdk-go-v2/service/elasticache v1.52.2 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.24 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.11 github.com/aws/aws-sdk-go-v2/service/iam v1.53.8 diff --git a/go.sum b/go.sum index 4e70a25..9966a80 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/aws/aws-sdk-go-v2/service/ec2 v1.299.0 h1:qTozRFl2YFFU2HJGl7ZAywlRQvB github.com/aws/aws-sdk-go-v2/service/ec2 v1.299.0/go.mod h1:E1pnYwWFZ8N3REmeN9Fe/Zipbpps4HJj8DQGNnLUMYc= github.com/aws/aws-sdk-go-v2/service/eks v1.82.1 h1:xTzXiQ8Q6U4ACdMNSCm72zd4Ds7QxhgVLqt5x8GXLBM= github.com/aws/aws-sdk-go-v2/service/eks v1.82.1/go.mod h1:jjcGpziR11RTrr3JIgXg/Nn8GSwK3WOz2z1v/RqEBUI= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.52.2 h1:5wbCUfyxXcjIqesyVfJBBJs0bDMyejthtHyy48mfZCI= +github.com/aws/aws-sdk-go-v2/service/elasticache v1.52.2/go.mod h1:o4vQxDt6oteknUjkXIEskp0ccy+93NRTPKXw3HlVMFE= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.24 h1:VoaXlgix9ZgAjRjq86f827UVzm9pgiYX+zkyU1brhZ4= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.24/go.mod h1:OtTEw8mOh0CCWVx072DtJ0WOlR3Ulngdqwa36oV6jm4= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.11 h1:0iNKMyO0SXuRfl5FF6TQASAHTXnTYZlQS3/oJSfpEbQ=