diff --git a/go.mod b/go.mod
index 4fbfc9e8..729097b9 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.25.0
require (
git.arvancloud.ir/arvancloud/cdn-go-sdk v0.12.1
github.com/aliyun/alibaba-cloud-sdk-go v1.62.560
- github.com/aws/aws-sdk-go v1.45.19
+ github.com/aws/aws-sdk-go v1.55.5
github.com/cloudflare/cloudflare-go v0.77.0
github.com/digitalocean/godo v1.102.1
github.com/fastly/go-fastly/v3 v3.12.0
diff --git a/go.sum b/go.sum
index 417e5307..685d1e8e 100644
--- a/go.sum
+++ b/go.sum
@@ -128,8 +128,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
-github.com/aws/aws-sdk-go v1.45.19 h1:+4yXWhldhCVXWFOQRF99ZTJ92t4DtoHROZIbN7Ujk/U=
-github.com/aws/aws-sdk-go v1.45.19/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
+github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
@@ -708,7 +708,6 @@ golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
@@ -782,7 +781,6 @@ golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXct
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
@@ -798,7 +796,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
diff --git a/pkg/providers/aws/aws.go b/pkg/providers/aws/aws.go
index 762f5a43..3545e123 100644
--- a/pkg/providers/aws/aws.go
+++ b/pkg/providers/aws/aws.go
@@ -15,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/service/eks"
+ "github.com/aws/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/service/elb"
"github.com/aws/aws-sdk-go/service/elbv2"
"github.com/aws/aws-sdk-go/service/lambda"
@@ -30,7 +31,7 @@ import (
sliceutil "github.com/projectdiscovery/utils/slice"
)
-var Services = []string{"ec2", "instance", "route53", "s3", "ecs", "eks", "lambda", "apigateway", "apigatewayv2", "alb", "elb", "lightsail", "cloudfront", "rds"}
+var Services = []string{"ec2", "instance", "route53", "s3", "ecs", "eks", "lambda", "apigateway", "apigatewayv2", "alb", "elb", "lightsail", "cloudfront", "rds", "elasticache"}
type ProviderOptions struct {
Id string
@@ -101,22 +102,23 @@ func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error {
// Provider is a data provider for aws API
type Provider struct {
- options *ProviderOptions
- ec2Client *ec2.EC2
- route53Client *route53.Route53
- s3Client *s3.S3
- ecsClient *ecs.ECS
- eksClient *eks.EKS
- lambdaClient *lambda.Lambda
- apiGateway *apigateway.APIGateway
- apiGatewayV2 *apigatewayv2.ApiGatewayV2
- albClient *elbv2.ELBV2
- elbClient *elb.ELB
- lightsailClient *lightsail.Lightsail
- cloudFrontClient *cloudfront.CloudFront
- rdsClient *rds.RDS
- regions *ec2.DescribeRegionsOutput
- session *session.Session
+ options *ProviderOptions
+ ec2Client *ec2.EC2
+ route53Client *route53.Route53
+ s3Client *s3.S3
+ ecsClient *ecs.ECS
+ eksClient *eks.EKS
+ lambdaClient *lambda.Lambda
+ apiGateway *apigateway.APIGateway
+ apiGatewayV2 *apigatewayv2.ApiGatewayV2
+ albClient *elbv2.ELBV2
+ elbClient *elb.ELB
+ lightsailClient *lightsail.Lightsail
+ cloudFrontClient *cloudfront.CloudFront
+ rdsClient *rds.RDS
+ elastiCacheClient *elasticache.ElastiCache
+ regions *ec2.DescribeRegionsOutput
+ session *session.Session
}
// New creates a new provider client for aws API
@@ -396,6 +398,9 @@ func (p *Provider) initServices(sess *session.Session) {
if services.Has("rds") {
p.rdsClient = rds.New(sess)
}
+ if services.Has("elasticache") {
+ p.elastiCacheClient = elasticache.New(sess)
+ }
}
const providerName = "aws"
@@ -503,6 +508,10 @@ func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) {
rdsProvider := &rdsProvider{rdsClient: p.rdsClient, options: *p.options, session: p.session, regions: p.regions}
assignWorker(rdsProvider.GetResource)
}
+ if p.elastiCacheClient != nil {
+ elastiCacheProvider := &elastiCacheProvider{elastiCacheClient: p.elastiCacheClient, options: *p.options, session: p.session, regions: p.regions}
+ assignWorker(elastiCacheProvider.GetResource)
+ }
go func() {
workersWaitGroup.Wait()
diff --git a/pkg/providers/aws/elasticache.go b/pkg/providers/aws/elasticache.go
new file mode 100644
index 00000000..f7850ad5
--- /dev/null
+++ b/pkg/providers/aws/elasticache.go
@@ -0,0 +1,226 @@
+package aws
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/ec2"
+ "github.com/aws/aws-sdk-go/service/elasticache"
+ "github.com/pkg/errors"
+ "github.com/projectdiscovery/cloudlist/pkg/schema"
+ "github.com/projectdiscovery/gologger"
+)
+
+// elastiCacheProvider is a provider for AWS ElastiCache API.
+type elastiCacheProvider struct {
+ options ProviderOptions
+ elastiCacheClient *elasticache.ElastiCache
+ session *session.Session
+ regions *ec2.DescribeRegionsOutput
+}
+
+func (ep *elastiCacheProvider) name() string {
+ return "elasticache"
+}
+
+// GetResource returns all the resources in the store for a provider.
+func (ep *elastiCacheProvider) GetResource(ctx context.Context) (*schema.Resources, error) {
+ list := schema.NewResources()
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var errs []error
+
+ for _, region := range ep.regions.Regions {
+ for _, client := range ep.getElastiCacheClients(region.RegionName) {
+ wg.Add(1)
+
+ go func(client *elasticache.ElastiCache) {
+ defer wg.Done()
+ defer func() {
+ if r := recover(); r != nil {
+ mu.Lock()
+ errs = append(errs, fmt.Errorf("panic in elasticache provider: %v", r))
+ mu.Unlock()
+ }
+ }()
+
+ resources, err := ep.listElastiCacheResources(client)
+ mu.Lock()
+ defer mu.Unlock()
+ if resources != nil {
+ list.Merge(resources)
+ }
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }(client)
+ }
+ }
+ wg.Wait()
+ if len(errs) > 0 && len(list.Items) == 0 {
+ return nil, fmt.Errorf("elasticache: all workers failed: %v", errs)
+ }
+ if len(errs) > 0 {
+ gologger.Warning().Msgf("elasticache: some listings failed: %v", errs)
+ }
+ return list, nil
+}
+
+func (ep *elastiCacheProvider) listElastiCacheResources(client *elasticache.ElastiCache) (*schema.Resources, error) {
+ list := schema.NewResources()
+ appendEndpoint := func(endpoint *elasticache.Endpoint, metadata map[string]string) {
+ if endpoint == nil || aws.StringValue(endpoint.Address) == "" {
+ return
+ }
+ list.Append(&schema.Resource{
+ ID: ep.options.Id,
+ Provider: providerName,
+ DNSName: aws.StringValue(endpoint.Address),
+ Public: true,
+ Service: ep.name(),
+ Metadata: metadata,
+ })
+ }
+
+ err := client.DescribeReplicationGroupsPages(&elasticache.DescribeReplicationGroupsInput{}, func(page *elasticache.DescribeReplicationGroupsOutput, _ bool) bool {
+ for _, group := range page.ReplicationGroups {
+ var metadata map[string]string
+ if ep.options.ExtendedMetadata {
+ metadata = getReplicationGroupMetadata(group)
+ }
+ appendEndpoint(group.ConfigurationEndpoint, metadata)
+ for _, nodeGroup := range group.NodeGroups {
+ appendEndpoint(nodeGroup.PrimaryEndpoint, metadata)
+ appendEndpoint(nodeGroup.ReaderEndpoint, metadata)
+ for _, member := range nodeGroup.NodeGroupMembers {
+ appendEndpoint(member.ReadEndpoint, metadata)
+ }
+ }
+ }
+ return true
+ })
+ if err != nil {
+ return list, errors.Wrap(err, "could not describe elasticache replication groups")
+ }
+
+ // Node endpoints are only returned when ShowCacheNodeInfo is set; Memcached
+ // clients connect to them directly, so they are reachable endpoints too.
+ err = client.DescribeCacheClustersPages(&elasticache.DescribeCacheClustersInput{ShowCacheNodeInfo: aws.Bool(true)}, func(page *elasticache.DescribeCacheClustersOutput, _ bool) bool {
+ for _, cluster := range page.CacheClusters {
+ var metadata map[string]string
+ if ep.options.ExtendedMetadata {
+ metadata = getCacheClusterMetadata(cluster)
+ }
+ appendEndpoint(cluster.ConfigurationEndpoint, metadata)
+ for _, node := range cluster.CacheNodes {
+ appendEndpoint(node.Endpoint, metadata)
+ }
+ }
+ return true
+ })
+ if err != nil {
+ return list, errors.Wrap(err, "could not describe elasticache cache clusters")
+ }
+
+ // Serverless caches are not available in every region, so a failure here
+ // must not discard the clusters already found.
+ _ = client.DescribeServerlessCachesPages(&elasticache.DescribeServerlessCachesInput{}, func(page *elasticache.DescribeServerlessCachesOutput, _ bool) bool {
+ for _, cache := range page.ServerlessCaches {
+ var metadata map[string]string
+ if ep.options.ExtendedMetadata {
+ metadata = getServerlessCacheMetadata(cache)
+ }
+ appendEndpoint(cache.Endpoint, metadata)
+ appendEndpoint(cache.ReaderEndpoint, metadata)
+ }
+ return true
+ })
+
+ return list, nil
+}
+
+func getReplicationGroupMetadata(group *elasticache.ReplicationGroup) map[string]string {
+ metadata := make(map[string]string)
+ schema.AddMetadata(metadata, "replication_group_id", group.ReplicationGroupId)
+ schema.AddMetadata(metadata, "arn", group.ARN)
+ schema.AddMetadata(metadata, "status", group.Status)
+ schema.AddMetadata(metadata, "cache_node_type", group.CacheNodeType)
+ schema.AddMetadata(metadata, "cluster_mode", group.ClusterMode)
+ if group.TransitEncryptionEnabled != nil {
+ metadata["transit_encryption_enabled"] = fmt.Sprintf("%t", *group.TransitEncryptionEnabled)
+ }
+ if group.AuthTokenEnabled != nil {
+ metadata["auth_token_enabled"] = fmt.Sprintf("%t", *group.AuthTokenEnabled)
+ }
+ return metadata
+}
+
+func getCacheClusterMetadata(cluster *elasticache.CacheCluster) map[string]string {
+ metadata := make(map[string]string)
+ schema.AddMetadata(metadata, "cache_cluster_id", cluster.CacheClusterId)
+ schema.AddMetadata(metadata, "arn", cluster.ARN)
+ schema.AddMetadata(metadata, "replication_group_id", cluster.ReplicationGroupId)
+ schema.AddMetadata(metadata, "engine", cluster.Engine)
+ schema.AddMetadata(metadata, "engine_version", cluster.EngineVersion)
+ schema.AddMetadata(metadata, "status", cluster.CacheClusterStatus)
+ schema.AddMetadata(metadata, "cache_node_type", cluster.CacheNodeType)
+ schema.AddMetadata(metadata, "availability_zone", cluster.PreferredAvailabilityZone)
+ if cluster.TransitEncryptionEnabled != nil {
+ metadata["transit_encryption_enabled"] = fmt.Sprintf("%t", *cluster.TransitEncryptionEnabled)
+ }
+ if cluster.CacheClusterCreateTime != nil {
+ metadata["created_at"] = cluster.CacheClusterCreateTime.Format(time.RFC3339)
+ }
+ return metadata
+}
+
+func getServerlessCacheMetadata(cache *elasticache.ServerlessCache) map[string]string {
+ metadata := make(map[string]string)
+ schema.AddMetadata(metadata, "serverless_cache_name", cache.ServerlessCacheName)
+ schema.AddMetadata(metadata, "arn", cache.ARN)
+ schema.AddMetadata(metadata, "engine", cache.Engine)
+ schema.AddMetadata(metadata, "engine_version", cache.FullEngineVersion)
+ schema.AddMetadata(metadata, "status", cache.Status)
+ if len(cache.SecurityGroupIds) > 0 {
+ metadata["security_group_ids"] = strings.Join(aws.StringValueSlice(cache.SecurityGroupIds), ",")
+ }
+ if cache.CreateTime != nil {
+ metadata["created_at"] = cache.CreateTime.Format(time.RFC3339)
+ }
+ return metadata
+}
+
+func (ep *elastiCacheProvider) getElastiCacheClients(region *string) []*elasticache.ElastiCache {
+ clients := make([]*elasticache.ElastiCache, 0)
+
+ clients = append(clients, elasticache.New(
+ ep.session,
+ aws.NewConfig().WithRegion(aws.StringValue(region)),
+ ))
+
+ if ep.options.AssumeRoleName == "" || len(ep.options.AccountIds) < 1 {
+ return clients
+ }
+
+ for _, accountId := range ep.options.AccountIds {
+ roleARN := fmt.Sprintf("arn:aws:iam::%s:role/%s", accountId, ep.options.AssumeRoleName)
+ creds := stscreds.NewCredentials(ep.session, roleARN)
+
+ assumeSession, err := session.NewSession(&aws.Config{
+ Region: region,
+ Credentials: creds,
+ })
+ if err != nil {
+ continue
+ }
+
+ clients = append(clients, elasticache.New(assumeSession))
+ }
+ return clients
+}
diff --git a/pkg/providers/aws/elasticache_test.go b/pkg/providers/aws/elasticache_test.go
new file mode 100644
index 00000000..550ba055
--- /dev/null
+++ b/pkg/providers/aws/elasticache_test.go
@@ -0,0 +1,253 @@
+package aws
+
+import (
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "sort"
+ "testing"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/ec2"
+ "github.com/aws/aws-sdk-go/service/elasticache"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var elastiCacheResponses = map[string]string{
+ "DescribeReplicationGroups": `
+
+
+
+ redis-rg
+ available
+
+
+ redis-rg.abc123.ng.0001.use1.cache.amazonaws.com6379
+ redis-rg-ro.abc123.ng.0001.use1.cache.amazonaws.com6379
+
+
+ redis-rg-001
+ redis-rg-001.abc123.0001.use1.cache.amazonaws.com6379
+
+
+
+
+
+
+ redis-cluster-mode
+ clustercfg.redis-cluster-mode.abc123.use1.cache.amazonaws.com6379
+
+
+
+`,
+ "DescribeCacheClusters": `
+
+
+
+ redis-rg-001
+ redis-rg
+ redis
+
+ 0001redis-rg-001.abc123.0001.use1.cache.amazonaws.com6379
+
+
+
+ memcached
+ memcached
+ memcached.abc123.cfg.use1.cache.amazonaws.com11211
+
+ 0001memcached.abc123.0001.use1.cache.amazonaws.com11211
+ 0002
+
+
+
+
+`,
+ "DescribeServerlessCaches": `
+
+
+
+ valkey-serverless
+ valkey
+ valkey-serverless-abc123.serverless.use1.cache.amazonaws.com6379
+ valkey-serverless-abc123.serverless.use1.cache.amazonaws.com6380
+
+
+
+`,
+}
+
+func newTestElastiCacheClient(t *testing.T, serverlessStatus, cacheStatus int) *elasticache.ElastiCache {
+ t.Helper()
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.NoError(t, r.ParseForm())
+ action := r.Form.Get("Action")
+ if action == "DescribeCacheClusters" {
+ assert.Equal(t, "true", r.Form.Get("ShowCacheNodeInfo"))
+ }
+ w.Header().Set("Content-Type", "text/xml")
+ if action == "DescribeCacheClusters" && cacheStatus != http.StatusOK {
+ w.WriteHeader(cacheStatus)
+ _, _ = w.Write([]byte(`AccessDenieddenied`))
+ return
+ }
+ if action == "DescribeServerlessCaches" && serverlessStatus != http.StatusOK {
+ w.WriteHeader(serverlessStatus)
+ _, _ = w.Write([]byte(`InvalidParameterValuenot supported`))
+ return
+ }
+ body, ok := elastiCacheResponses[action]
+ require.True(t, ok, "unexpected action %s", action)
+ _, _ = w.Write([]byte(body))
+ }))
+ t.Cleanup(server.Close)
+
+ sess, err := session.NewSession(&aws.Config{
+ Region: aws.String("us-east-1"),
+ Endpoint: aws.String(server.URL),
+ Credentials: credentials.NewStaticCredentials("test", "test", ""),
+ MaxRetries: aws.Int(0),
+ })
+ require.NoError(t, err)
+ return elasticache.New(sess)
+}
+
+func dnsNames(t *testing.T, provider *elastiCacheProvider, client *elasticache.ElastiCache) []string {
+ t.Helper()
+ resources, err := provider.listElastiCacheResources(client)
+ require.NoError(t, err)
+
+ var names []string
+ for _, item := range resources.Items {
+ assert.Equal(t, "elasticache", item.Service)
+ names = append(names, item.DNSName)
+ }
+ sort.Strings(names)
+ return names
+}
+
+func TestListElastiCacheResources(t *testing.T) {
+ provider := &elastiCacheProvider{options: ProviderOptions{Id: "test"}}
+
+ assert.Equal(t, []string{
+ "clustercfg.redis-cluster-mode.abc123.use1.cache.amazonaws.com",
+ "memcached.abc123.0001.use1.cache.amazonaws.com",
+ "memcached.abc123.cfg.use1.cache.amazonaws.com",
+ "redis-rg-001.abc123.0001.use1.cache.amazonaws.com",
+ "redis-rg-ro.abc123.ng.0001.use1.cache.amazonaws.com",
+ "redis-rg.abc123.ng.0001.use1.cache.amazonaws.com",
+ "valkey-serverless-abc123.serverless.use1.cache.amazonaws.com",
+ }, dnsNames(t, provider, newTestElastiCacheClient(t, http.StatusOK, http.StatusOK)))
+}
+
+func TestListElastiCacheResources_ServerlessUnsupported(t *testing.T) {
+ provider := &elastiCacheProvider{options: ProviderOptions{Id: "test"}}
+
+ names := dnsNames(t, provider, newTestElastiCacheClient(t, http.StatusBadRequest, http.StatusOK))
+ assert.Len(t, names, 6, "a serverless API failure must not drop cluster endpoints")
+ assert.NotContains(t, names, "valkey-serverless-abc123.serverless.use1.cache.amazonaws.com")
+}
+
+func TestListElastiCacheResourcesKeepsEarlierReplicationGroupPage(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.NoError(t, r.ParseForm())
+ w.Header().Set("Content-Type", "text/xml")
+ if r.Form.Get("Action") == "DescribeReplicationGroups" && r.Form.Get("Marker") == "next" {
+ w.WriteHeader(http.StatusInternalServerError)
+ _, _ = w.Write([]byte(`InternalErrorpage failed`))
+ return
+ }
+ if r.Form.Get("Action") != "DescribeReplicationGroups" {
+ w.WriteHeader(http.StatusForbidden)
+ _, _ = w.Write([]byte(`AccessDenieddenied`))
+ return
+ }
+ _, _ = w.Write([]byte(`
+
+ next
+
+
+ redis-rg
+
+ redis-rg.abc123.ng.0001.use1.cache.amazonaws.com6379
+
+
+
+
+`))
+ }))
+ t.Cleanup(server.Close)
+
+ sess, err := session.NewSession(&aws.Config{
+ Region: aws.String("us-east-1"),
+ Endpoint: aws.String(server.URL),
+ Credentials: credentials.NewStaticCredentials("test", "test", ""),
+ MaxRetries: aws.Int(0),
+ })
+ require.NoError(t, err)
+
+ provider := &elastiCacheProvider{options: ProviderOptions{Id: "test"}}
+ resources, err := provider.listElastiCacheResources(elasticache.New(sess))
+ require.Error(t, err)
+ require.NotNil(t, resources)
+ var names []string
+ for _, item := range resources.Items {
+ names = append(names, item.DNSName)
+ }
+ assert.Equal(t, []string{"redis-rg.abc123.ng.0001.use1.cache.amazonaws.com"}, names)
+}
+
+func TestListElastiCacheResourcesKeepsReplicationGroups(t *testing.T) {
+ provider := &elastiCacheProvider{options: ProviderOptions{Id: "test"}}
+ resources, err := provider.listElastiCacheResources(newTestElastiCacheClient(t, http.StatusOK, http.StatusForbidden))
+ require.Error(t, err)
+
+ var names []string
+ for _, item := range resources.Items {
+ names = append(names, item.DNSName)
+ }
+ assert.Contains(t, names, "redis-rg.abc123.ng.0001.use1.cache.amazonaws.com")
+ assert.NotContains(t, names, "memcached.abc123.cfg.use1.cache.amazonaws.com")
+}
+
+func TestListElastiCacheResources_ExtendedMetadata(t *testing.T) {
+ provider := &elastiCacheProvider{options: ProviderOptions{Id: "test", ExtendedMetadata: true}}
+
+ resources, err := provider.listElastiCacheResources(newTestElastiCacheClient(t, http.StatusOK, http.StatusOK))
+ require.NoError(t, err)
+
+ byName := map[string]map[string]string{}
+ for _, item := range resources.Items {
+ byName[item.DNSName] = item.Metadata
+ }
+ assert.Equal(t, "redis-rg", byName["redis-rg.abc123.ng.0001.use1.cache.amazonaws.com"]["replication_group_id"])
+ assert.Equal(t, "memcached", byName["memcached.abc123.cfg.use1.cache.amazonaws.com"]["engine"])
+ assert.Equal(t, "valkey-serverless", byName["valkey-serverless-abc123.serverless.use1.cache.amazonaws.com"]["serverless_cache_name"])
+}
+
+func TestElastiCacheGetResourceReportsListingErrors(t *testing.T) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusForbidden)
+ _, _ = w.Write([]byte(`AccessDenieddenied`))
+ }))
+ t.Cleanup(server.Close)
+
+ sess, err := session.NewSession(&aws.Config{
+ Region: aws.String("us-east-1"),
+ Endpoint: aws.String(server.URL),
+ Credentials: credentials.NewStaticCredentials("test", "test", ""),
+ MaxRetries: aws.Int(0),
+ })
+ require.NoError(t, err)
+
+ provider := &elastiCacheProvider{
+ options: ProviderOptions{Id: "test"},
+ session: sess,
+ regions: &ec2.DescribeRegionsOutput{Regions: []*ec2.Region{{RegionName: aws.String("us-east-1")}, {RegionName: aws.String("eu-west-1")}}},
+ }
+ _, err = provider.GetResource(context.Background())
+ require.Error(t, err, "a denied listing in every region must not look like an empty account")
+}