Repository: gruntwork-io/cloud-nuke
Pull request #784 (2024-11-11 21:07:25)

add support for elastic-cache serverless & grafana resources.
## Description
added support for elastic-cache server-less & grafana resources.

## TODOs

Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).

- [x] Update the docs.
- [x] Run the relevant tests successfully, including pre-commit checks.
- [x] Ensure any 3rd party code adheres with our [license policy](https://www.notion.so/gruntwork/Gruntwork-licenses-and-open-source-usage-policy-f7dece1f780341c7b69c1763f22b1378) or delete this line if its not applicable.
- [x] Include release notes. If this PR is backward incompatible, include a migration guide.
- [ ] Attention Grunts - if this PR adds support for a new resource, ensure the `nuke_sandbox` and `nuke_phxdevops` jobs in `.circleci/config.yml` have been updated with appropriate exclusions (either directly in the job or via the `.circleci/nuke_config.yml` file) to prevent nuking IAM roles, groups, resources, etc that are important for the test accounts.


## Release Notes (draft)

<!-- One-line description of the PR that can be included in the final release notes. -->
- [x] Added elastic-cache server-less & grafana resources.

### Migration Guide
n/a


wakeful: rebase done

Patch:
diff --git a/README.md b/README.md
index 8576771..331ce9f 100644
--- a/README.md
+++ b/README.md
@@ -52,11 +52,13 @@ Cloud-nuke suppports 🔎 inspecting and 🔥💀 deleting the following AWS res
 | Elasticache             | Clusters                                                 |
 | Elasticache             | Parameter Groups                                         |
 | Elasticache             | Subnet Groups                                            |
+| Elasticache Serverless  | Clusters                                                 |
 | Elastic Beanstalk       | Applications                                             |
 | ECS                     | Services                                                 |
 | ECS                     | Clusters                                                 |
 | EKS                     | Clusters                                                 |
 | DynamoDB                | Tables                                                   |
+| Grafana                 | Workspace                                                |
 | Lambda                  | Functions                                                |
 | SQS                     | Queues                                                   |
 | S3                      | Buckets                                                  |
@@ -584,7 +586,7 @@ of the file that are supported are listed here.
 | ec2-ipam-pool                    | EC2IPAMPool                   | ✅ (IPAM Pool name)                    | ✅ (Creation Time)                   | ✅    | ✅       |
 | ec2-ipam-resource-discovery      | EC2IPAMResourceDiscovery      | ✅ (IPAM Discovery Name)               | ✅ (Creation Time)                   | ✅    | ✅       |
 | ec2-ipam-scope                   | EC2IPAMScope                  | ✅ (IPAM Scope Name)                   | ✅ (Creation Time)                   | ✅    | ✅       |
-| ec2-placement-groups             | EC2PlacementGroups | ✅ (Placement Group Name)                         | ✅ (First Seen Tag Time)             | ✅    | ✅       |
+| ec2-placement-groups             | EC2PlacementGroups            | ✅ (Placement Group Name)              | ✅ (First Seen Tag Time)             | ✅    | ✅       |
 | ec2-subnet                       | EC2Subnet                     | ✅ (Subnet Name)                       | ✅ (Creation Time)                   | ✅    | ❌       |
 | ec2-endpoint                     | EC2Endpoint                   | ✅ (Endpoint Name)                     | ✅ (Creation Time)                   | ✅    | ✅       |
 | ecr                              | ECRRepository                 | ✅ (Repository Name)                   | ✅ (Creation Time)                   | ❌    | ✅       |
@@ -596,6 +598,7 @@ of the file that are supported are listed here.
 | efs                              | ElasticFileSystem             | ✅ (File System Name)                  | ✅ (Creation Time)                   | ❌    | ✅       |
 | eip                              | ElasticIP                     | ✅ (Elastic IP Allocation Name)        | ✅ (First Seen Tag Time)             | ✅    | ✅       |
 | elasticache                      | Elasticache                   | ✅ (Cluster ID & Replication Group ID) | ✅ (Creation Time)                   | ❌    | ✅       |
+| elasticcache-serverless          | ElasticCacheServerless        | ✅ (Cluster Name )                     | ✅ (Creation Time)                   | ❌    | ✅       |
 | elasticacheparametergroups       | ElasticacheParameterGroups    | ✅ (Parameter Group Name)              | ❌                                   | ❌    | ✅       |
 | elasticachesubnetgroups          | ElasticacheSubnetGroups       | ✅ (Subnet Group Name)                 | ❌                                   | ❌    | ✅       |
 | event-bridge                     | EventBridge                   | ✅ (Bus  Name)                         | ✅ (Creation Time)                   | ❌    | ✅       |
@@ -603,6 +606,7 @@ of the file that are supported are listed here.
 | event-bridge-rule                | EventBridgeRule               | ✅ (Bus Rule Name)                     | ❌                                   | ❌    | ✅       |
 | event-bridge-schedule            | EventBridgeSchedule           | ✅ (Schedule Name)                     | ✅ (Creation Time)                   | ❌    | ✅       |
 | event-bridge-schedule-group      | EventBridgeScheduleGroup      | ✅ (Schedule Group Name)               | ✅ (Creation Time)                   | ❌    | ✅       |
+| grafana                          | Grafana                       | ✅ (Workspace Name)                    | ✅ (Creation Time)                   | ✅    | ✅       |
 | guardduty                        | GuardDuty                     | ❌                                     | ✅ (Created Time)                    | ❌    | ✅       |
 | iam-group                        | IAMGroups                     | ✅ (Group Name)                        | ✅ (Creation Time)                   | ❌    | ✅       |
 | iam-policy                       | IAMPolicies                   | ✅ (Policy Name)                       | ✅ (Creation Time)                   | ❌    | ✅       |
diff --git a/aws/resource_registry.go b/aws/resource_registry.go
index 625ba64..56a1b79 100644
--- a/aws/resource_registry.go
+++ b/aws/resource_registry.go
@@ -64,6 +64,7 @@ func getRegisteredRegionalResources() []AwsResource {
 		&resources.AppRunnerService{},
 		&resources.BackupVault{},
 		&resources.ManagedPrometheus{},
+		&resources.Grafana{},
 		&resources.EventBridgeSchedule{},
 		&resources.EventBridgeScheduleGroup{},
 		&resources.EventBridgeArchive{},
@@ -100,6 +101,7 @@ func getRegisteredRegionalResources() []AwsResource {
 		&resources.ElasticFileSystem{},
 		&resources.EIPAddresses{},
 		&resources.EKSClusters{},
+		&resources.ElasticCacheServerless{},
 		&resources.Elasticaches{},
 		&resources.ElasticacheParameterGroups{},
 		&resources.ElasticacheSubnetGroups{},
diff --git a/aws/resources/elasticache_serverless.go b/aws/resources/elasticache_serverless.go
new file mode 100644
index 0000000..4d80a9d
--- /dev/null
+++ b/aws/resources/elasticache_serverless.go
@@ -0,0 +1,90 @@
+package resources
+
+import (
+	"context"
+	"strings"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/elasticache"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/logging"
+	"github.com/gruntwork-io/cloud-nuke/report"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+func (cache *ElasticCacheServerless) nukeAll(identifiers []*string) error {
+	if len(identifiers) == 0 {
+		logging.Debugf("[ElasticCache Serverless] No cluster found in region %s", cache.Region)
+		return nil
+	}
+
+	logging.Debugf("[ElasticCache Serverless] Deleting all clusters in %s", cache.Region)
+	var deleted []*string
+
+	for _, identifier := range identifiers {
+		_, err := cache.Client.DeleteServerlessCache(cache.Context, &elasticache.DeleteServerlessCacheInput{
+			ServerlessCacheName: identifier,
+		})
+		if err != nil {
+			logging.Debugf("[ElasticCache Serverless] Error deleting cluster %s in region %s", *identifier, cache.Region)
+		} else {
+			deleted = append(deleted, identifier)
+			logging.Debugf("[ElasticCache Serverless] Deleted cluster %s in region %s", *identifier, cache.Region)
+		}
+
+		e := report.Entry{
+			Identifier:   aws.ToString(identifier),
+			ResourceType: cache.ResourceName(),
+			Error:        err,
+		}
+		report.Record(e)
+	}
+
+	logging.Debugf("[OK] %d ElasticCache Serverless deleted in %s", len(deleted), cache.Region)
+	return nil
+}
+
+func (cache *ElasticCacheServerless) getAll(ctx context.Context, cnfObj config.Config) ([]*string, error) {
+	var output []*string
+
+	paginator := elasticache.NewDescribeServerlessCachesPaginator(cache.Client, &elasticache.DescribeServerlessCachesInput{})
+	for paginator.HasMorePages() {
+		page, err := paginator.NextPage(ctx)
+		if err != nil {
+			return nil, errors.WithStackTrace(err)
+		}
+
+		for _, cluster := range page.ServerlessCaches {
+			if strings.ToLower(*cluster.Status) != "available" {
+				logging.Debugf(
+					"[ElasticCache Serverless] skiping cluster: %s, status: %s",
+					*cluster.ARN,
+					*cluster.Status,
+				)
+
+				continue
+			}
+
+			split := strings.Split(*cluster.ARN, ":")
+			if len(split) == 0 {
+				logging.Debugf(
+					"[ElasticCache Serverless] skiping cluster: %s",
+					*cluster.ARN,
+				)
+
+				continue
+			}
+
+			name := split[len(split)-1]
+
+			if cnfObj.ElasticCacheServerless.ShouldInclude(config.ResourceValue{
+				Name: aws.String(name),
+				Time: cluster.CreateTime,
+			}) {
+				output = append(output, aws.String(name))
+			}
+		}
+	}
+
+	return output, nil
+}
diff --git a/aws/resources/elasticache_serverless_types.go b/aws/resources/elasticache_serverless_types.go
new file mode 100644
index 0000000..d083790
--- /dev/null
+++ b/aws/resources/elasticache_serverless_types.go
@@ -0,0 +1,63 @@
+package resources
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/elasticache"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+type ElasticCacheServerlessAPI interface {
+	DeleteServerlessCache(ctx context.Context, params *elasticache.DeleteServerlessCacheInput, optFns ...func(*elasticache.Options)) (*elasticache.DeleteServerlessCacheOutput, error)
+	DescribeServerlessCaches(ctx context.Context, params *elasticache.DescribeServerlessCachesInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeServerlessCachesOutput, error)
+}
+
+type ElasticCacheServerless struct {
+	BaseAwsResource
+	Client     ElasticCacheServerlessAPI
+	Region     string
+	ClusterIds []string
+}
+
+func (cache *ElasticCacheServerless) InitV2(cfg aws.Config) {
+	cache.Client = elasticache.NewFromConfig(cfg)
+}
+
+func (cache *ElasticCacheServerless) IsUsingV2() bool { return true }
+
+func (cache *ElasticCacheServerless) ResourceName() string {
+	return "elasticcache-serverless"
+}
+
+func (cache *ElasticCacheServerless) ResourceIdentifiers() []string {
+	return cache.ClusterIds
+}
+
+func (cache *ElasticCacheServerless) MaxBatchSize() int {
+	// Tentative batch size to ensure AWS doesn't throttle
+	return 49
+}
+
+func (cache *ElasticCacheServerless) GetAndSetResourceConfig(configObj config.Config) config.ResourceType {
+	return configObj.Elasticache
+}
+
+func (cache *ElasticCacheServerless) GetAndSetIdentifiers(c context.Context, configObj config.Config) ([]string, error) {
+	identifiers, err := cache.getAll(c, configObj)
+	if err != nil {
+		return nil, err
+	}
+
+	cache.ClusterIds = aws.ToStringSlice(identifiers)
+	return cache.ClusterIds, nil
+}
+
+func (cache *ElasticCacheServerless) Nuke(identifiers []string) error {
+	if err := cache.nukeAll(aws.StringSlice(identifiers)); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
diff --git a/aws/resources/grafana.go b/aws/resources/grafana.go
new file mode 100644
index 0000000..463385e
--- /dev/null
+++ b/aws/resources/grafana.go
@@ -0,0 +1,79 @@
+package resources
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/grafana"
+	"github.com/aws/aws-sdk-go-v2/service/grafana/types"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/cloud-nuke/logging"
+	"github.com/gruntwork-io/cloud-nuke/report"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+func (g *Grafana) getAll(ctx context.Context, cnfObj config.Config) ([]*string, error) {
+	var workspaceIDs []*string
+
+	paginator := grafana.NewListWorkspacesPaginator(g.Client, &grafana.ListWorkspacesInput{})
+	for paginator.HasMorePages() {
+		page, err := paginator.NextPage(ctx)
+		if err != nil {
+			return nil, errors.WithStackTrace(err)
+		}
+
+		for _, workspace := range page.Workspaces {
+			if workspace.Status != types.WorkspaceStatusActive {
+				logging.Debugf(
+					"[Grafana] skiping grafana workspaces: %s, status: %s",
+					*workspace.Name,
+					workspace.Status,
+				)
+
+				continue
+			}
+
+			if cnfObj.Grafana.ShouldInclude(config.ResourceValue{
+				Name: workspace.Name,
+				Time: workspace.Created,
+				Tags: workspace.Tags,
+			}) {
+				workspaceIDs = append(workspaceIDs, workspace.Id)
+			}
+		}
+	}
+
+	return workspaceIDs, nil
+}
+
+func (g *Grafana) nukeAll(identifiers []*string) error {
+	if len(identifiers) == 0 {
+		logging.Debugf("[Grafana] No Grafana Workspaces found in region %s", g.Region)
+		return nil
+	}
+
+	logging.Debugf("[Grafana] Deleting all Grafana Workspaces in %s", g.Region)
+	var deleted []*string
+
+	for _, identifier := range identifiers {
+		_, err := g.Client.DeleteWorkspace(g.Context, &grafana.DeleteWorkspaceInput{
+			WorkspaceId: identifier,
+		})
+		if err != nil {
+			logging.Debugf("[Grafana] Error deleting Workspace %s in region %s", *identifier, g.Region)
+		} else {
+			deleted = append(deleted, identifier)
+			logging.Debugf("[Grafana] Deleted Workspace %s in region %s", *identifier, g.Region)
+		}
+
+		e := report.Entry{
+			Identifier:   aws.ToString(identifier),
+			ResourceType: g.ResourceName(),
+			Error:        err,
+		}
+		report.Record(e)
+	}
+
+	logging.Debugf("[OK] %d Grafana Workspace(s) deleted in %s", len(deleted), g.Region)
+	return nil
+}
diff --git a/aws/resources/grafana_types.go b/aws/resources/grafana_types.go
new file mode 100644
index 0000000..2c914dc
--- /dev/null
+++ b/aws/resources/grafana_types.go
@@ -0,0 +1,58 @@
+package resources
+
+import (
+	"context"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/grafana"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/gruntwork-io/go-commons/errors"
+)
+
+type GrafanaAPI interface {
+	DeleteWorkspace(ctx context.Context, params *grafana.DeleteWorkspaceInput, optFns ...func(*grafana.Options)) (*grafana.DeleteWorkspaceOutput, error)
+	ListWorkspaces(ctx context.Context, params *grafana.ListWorkspacesInput, optFns ...func(*grafana.Options)) (*grafana.ListWorkspacesOutput, error)
+}
+
+type Grafana struct {
+	BaseAwsResource
+	Client     GrafanaAPI
+	Region     string
+	WorkSpaces []string
+}
+
+func (g *Grafana) GetAndSetResourceConfig(configObj config.Config) config.ResourceType {
+	return configObj.ManagedPrometheus
+}
+
+func (g *Grafana) InitV2(cfg aws.Config) {
+	g.Client = grafana.NewFromConfig(cfg)
+}
+
+func (g *Grafana) IsUsingV2() bool { return true }
+
+func (g *Grafana) ResourceName() string { return "grafana" }
+
+func (g *Grafana) ResourceIdentifiers() []string { return g.WorkSpaces }
+
+func (g *Grafana) MaxBatchSize() int {
+	return 100
+}
+
+func (g *Grafana) Nuke(identifiers []string) error {
+	if err := g.nukeAll(aws.StringSlice(identifiers)); err != nil {
+		return errors.WithStackTrace(err)
+	}
+
+	return nil
+}
+
+func (g *Grafana) GetAndSetIdentifiers(ctx context.Context, cnfObj config.Config) ([]string, error) {
+	identifiers, err := g.getAll(ctx, cnfObj)
+	if err != nil {
+		return nil, err
+	}
+
+	g.WorkSpaces = aws.ToStringSlice(identifiers)
+	return g.WorkSpaces, nil
+}
diff --git a/config/config.go b/config/config.go
index a1aac29..d9f0b0a 100644
--- a/config/config.go
+++ b/config/config.go
@@ -70,12 +70,14 @@ type Config struct {
 	ElasticIP                       ResourceType               `yaml:"ElasticIP"`
 	Elasticache                     ResourceType               `yaml:"Elasticache"`
 	ElasticacheParameterGroups      ResourceType               `yaml:"ElasticacheParameterGroups"`
+	ElasticCacheServerless          ResourceType               `yaml:"ElasticCacheServerless"`
 	ElasticacheSubnetGroups         ResourceType               `yaml:"ElasticacheSubnetGroups"`
 	EventBridge                     ResourceType               `yaml:"EventBridge"`
 	EventBridgeArchive              ResourceType               `yaml:"EventBridgeArchive"`
 	EventBridgeRule                 ResourceType               `yaml:"EventBridgeRule"`
 	EventBridgeSchedule             ResourceType               `yaml:"EventBridgeSchedule"`
 	EventBridgeScheduleGroup        ResourceType               `yaml:"EventBridgeScheduleGroup"`
+	Grafana                         ResourceType               `yaml:"Grafana"`
 	GuardDuty                       ResourceType               `yaml:"GuardDuty"`
 	IAMGroups                       ResourceType               `yaml:"IAMGroups"`
 	IAMPolicies                     ResourceType               `yaml:"IAMPolicies"`
diff --git a/go.mod b/go.mod
index 154c1ac..8db8ec0 100644
--- a/go.mod
+++ b/go.mod
@@ -34,11 +34,12 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.41.1
 	github.com/aws/aws-sdk-go-v2/service/eventbridge v1.35.5
 	github.com/aws/aws-sdk-go-v2/service/firehose v1.35.0
+	github.com/aws/aws-sdk-go-v2/service/grafana v1.26.5
 	github.com/aws/aws-sdk-go-v2/service/guardduty v1.51.1
 	github.com/aws/aws-sdk-go-v2/service/iam v1.37.4
 	github.com/aws/aws-sdk-go-v2/service/kinesis v1.32.5
 	github.com/aws/aws-sdk-go-v2/service/kms v1.37.5
-	github.com/aws/aws-sdk-go-v2/service/lambda v1.65.0
+	github.com/aws/aws-sdk-go-v2/service/lambda v1.66.0
 	github.com/aws/aws-sdk-go-v2/service/macie2 v1.43.5
 	github.com/aws/aws-sdk-go-v2/service/scheduler v1.12.5
 	github.com/aws/aws-sdk-go-v2/service/securityhub v1.54.6
diff --git a/go.sum b/go.sum
index 33f7b5e..834b706 100644
--- a/go.sum
+++ b/go.sum
@@ -87,6 +87,8 @@ github.com/aws/aws-sdk-go-v2/service/eventbridge v1.35.5 h1:O7UMjjX8eAM4eLs303Vr
 github.com/aws/aws-sdk-go-v2/service/eventbridge v1.35.5/go.mod h1:U1Wwh1TVfPHB8sbmBt3yqH2etdYERX1quammRvGWtXs=
 github.com/aws/aws-sdk-go-v2/service/firehose v1.35.0 h1:gMObW4IgUF4z8PxOb8efA/5P/chbjPc4XcTgDwZB/3g=
 github.com/aws/aws-sdk-go-v2/service/firehose v1.35.0/go.mod h1:eC8yjekJg1MXGYwFDX0tYIFnBMMvJVmwuAnrQ6A88IE=
+github.com/aws/aws-sdk-go-v2/service/grafana v1.26.5 h1:xgJiP0R7hXAO03rh9h8iupmMnb96rWM1dXG0ZKP8JCA=
+github.com/aws/aws-sdk-go-v2/service/grafana v1.26.5/go.mod h1:QOCIdJ0zLzzcUDbBNtFT+IrzFFHD0909mxlPdXh53lg=
 github.com/aws/aws-sdk-go-v2/service/guardduty v1.51.1 h1:nAK988z7ZsAYHZtVD8IYV1HVI3ZcE2atQCoBEO5xEQE=
 github.com/aws/aws-sdk-go-v2/service/guardduty v1.51.1/go.mod h1:ISU8hcBgCVmpZTUmD2ONSQO+0dGpIjyCU1r+yIy7ha0=
 github.com/aws/aws-sdk-go-v2/service/iam v1.37.4 h1:MrH2MJRzxPGXtavvL1JtDLFJzXN+4ObO090jzauqcPk=
@@ -101,8 +103,8 @@ github.com/aws/aws-sdk-go-v2/service/kinesis v1.32.5 h1:qlSo29NECbPML9fKL3RZ28XM
 github.com/aws/aws-sdk-go-v2/service/kinesis v1.32.5/go.mod h1:2/lI9/4ZEq+wLGLUpbo2LLGYfFv+HGOex3Iu67k7lvU=
 github.com/aws/aws-sdk-go-v2/service/kms v1.37.5 h1:5dQJ6Q5QrQOqZxXjSbRXukBqU8Pgu6Ro6Qqtyd8yiz4=
 github.com/aws/aws-sdk-go-v2/service/kms v1.37.5/go.mod h1:A9vfQcNHVBCE7ZZN6H+UUJpXtbH26Vv6L7Zhk5nIJAY=
-github.com/aws/aws-sdk-go-v2/service/lambda v1.65.0 h1:c4eYRkhqXsyoIQ4Z8e3E1fBmxOB3XnAfbYw0x+kyHdw=
-github.com/aws/aws-sdk-go-v2/service/lambda v1.65.0/go.mod h1:4L6vIpiChdahncljlDFzKWGiZsLgszGwDoYqMDhb6T4=
+github.com/aws/aws-sdk-go-v2/service/lambda v1.66.0 h1:jMqMB8t/xbJnDdr11kH5rKdAhoW2a3Nq3XxrkTp0gso=
+github.com/aws/aws-sdk-go-v2/service/lambda v1.66.0/go.mod h1:4L6vIpiChdahncljlDFzKWGiZsLgszGwDoYqMDhb6T4=
 github.com/aws/aws-sdk-go-v2/service/macie2 v1.43.5 h1:WJZG9ajCzS93t5qgG3NqVJqJqvpab9frc+2OhEszF/s=
 github.com/aws/aws-sdk-go-v2/service/macie2 v1.43.5/go.mod h1:uAeVP7yiKq1iECAGBsHgMzlUH77OIrmsA1CBmIIeCe4=
 github.com/aws/aws-sdk-go-v2/service/scheduler v1.12.5 h1:iitjgZ+/+ICxhA4EBvT5sqKchDcTiKBSOy+Ow0N8M+U=
diff --git a/v2_migration_report/output.md b/v2_migration_report/output.md
index 74f3d1f..60ac052 100644
--- a/v2_migration_report/output.md
+++ b/v2_migration_report/output.md
@@ -44,6 +44,7 @@ run `go generate ./...` to refresh this report.
 | elasticache                      | :white_check_mark: |
 | elasticacheParameterGroups       | :white_check_mark: |
 | elasticacheSubnetGroups          | :white_check_mark: |
+| elasticcache-serverless          | :white_check_mark: |
 | elb                              | :white_check_mark: |
 | elbv2                            | :white_check_mark: |
 | event-bridge                     | :white_check_mark: |
@@ -51,6 +52,7 @@ run `go generate ./...` to refresh this report.
 | event-bridge-rule                | :white_check_mark: |
 | event-bridge-schedule            | :white_check_mark: |
 | event-bridge-schedule-group      | :white_check_mark: |
+| grafana                          | :white_check_mark: |
 | guardduty                        | :white_check_mark: |
 | iam                              | :white_check_mark: |
 | iam-group                        | :white_check_mark: |


Test patch:
diff --git a/aws/resources/elasticache_serverless_test.go b/aws/resources/elasticache_serverless_test.go
new file mode 100644
index 0000000..6e7d2dc
--- /dev/null
+++ b/aws/resources/elasticache_serverless_test.go
@@ -0,0 +1,107 @@
+package resources
+
+import (
+	"context"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/elasticache"
+	"github.com/aws/aws-sdk-go-v2/service/elasticache/types"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+type mockedElasticCacheServerlessService struct {
+	ElasticCacheServerlessAPI
+	DeleteServerlessCacheOutput    elasticache.DeleteServerlessCacheOutput
+	DescribeServerlessCachesOutput elasticache.DescribeServerlessCachesOutput
+}
+
+func (m mockedElasticCacheServerlessService) DeleteServerlessCache(ctx context.Context, params *elasticache.DeleteServerlessCacheInput, optFns ...func(*elasticache.Options)) (*elasticache.DeleteServerlessCacheOutput, error) {
+	return &m.DeleteServerlessCacheOutput, nil
+}
+
+func (m mockedElasticCacheServerlessService) DescribeServerlessCaches(ctx context.Context, params *elasticache.DescribeServerlessCachesInput, optFns ...func(*elasticache.Options)) (*elasticache.DescribeServerlessCachesOutput, error) {
+	return &m.DescribeServerlessCachesOutput, nil
+}
+
+func Test_ElasticCacheServerless_NukeAll(t *testing.T) {
+	t.Parallel()
+
+	clusterName := "test-workspace-1"
+	service := ElasticCacheServerless{
+		Client: mockedElasticCacheServerlessService{
+			DeleteServerlessCacheOutput: elasticache.DeleteServerlessCacheOutput{},
+		},
+	}
+
+	err := service.nukeAll([]*string{&clusterName})
+	assert.NoError(t, err)
+}
+
+func Test_ElasticCacheServerless_GetAll(t *testing.T) {
+	t.Parallel()
+
+	now := time.Now()
+	cluster1 := "test-workspace-1"
+	cluster2 := "test-workspace-2"
+
+	service := ElasticCacheServerless{
+		Client: mockedElasticCacheServerlessService{
+			DescribeServerlessCachesOutput: elasticache.DescribeServerlessCachesOutput{
+				ServerlessCaches: []types.ServerlessCache{
+					{
+						ARN:        aws.String("arn::region::" + cluster1),
+						CreateTime: &now,
+						Status:     aws.String("available"),
+					},
+					{
+						ARN:        aws.String("arn::region::" + cluster2),
+						CreateTime: aws.Time(now.Add(1 * time.Hour)),
+						Status:     aws.String("available"),
+					},
+				},
+			},
+		},
+	}
+
+	tests := map[string]struct {
+		configObj config.ResourceType
+		expected  []string
+	}{
+		"emptyFilter": {
+			configObj: config.ResourceType{},
+			expected:  []string{cluster1, cluster2},
+		},
+		"nameExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					NamesRegExp: []config.Expression{{
+						RE: *regexp.MustCompile(cluster1),
+					}},
+				}},
+			expected: []string{cluster2},
+		},
+		"timeAfterExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					TimeAfter: aws.Time(now.Add(-1 * time.Hour)),
+				}},
+			expected: []string{},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			workspaces, err := service.getAll(
+				context.Background(),
+				config.Config{ElasticCacheServerless: tc.configObj},
+			)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, aws.ToStringSlice(workspaces))
+		})
+	}
+}
diff --git a/aws/resources/grafana_test.go b/aws/resources/grafana_test.go
new file mode 100644
index 0000000..679ce52
--- /dev/null
+++ b/aws/resources/grafana_test.go
@@ -0,0 +1,109 @@
+package resources
+
+import (
+	"context"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/service/grafana"
+	"github.com/aws/aws-sdk-go-v2/service/grafana/types"
+	"github.com/gruntwork-io/cloud-nuke/config"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+type mockedGrafanaService struct {
+	GrafanaAPI
+	DeleteWorkspaceOutput grafana.DeleteWorkspaceOutput
+	ListWorkspacesOutput  grafana.ListWorkspacesOutput
+}
+
+func (m mockedGrafanaService) DeleteWorkspace(ctx context.Context, params *grafana.DeleteWorkspaceInput, optFns ...func(*grafana.Options)) (*grafana.DeleteWorkspaceOutput, error) {
+	return &m.DeleteWorkspaceOutput, nil
+}
+
+func (m mockedGrafanaService) ListWorkspaces(ctx context.Context, params *grafana.ListWorkspacesInput, optFns ...func(*grafana.Options)) (*grafana.ListWorkspacesOutput, error) {
+	return &m.ListWorkspacesOutput, nil
+}
+
+func Test_Grafana_NukeAll(t *testing.T) {
+	t.Parallel()
+
+	workspaceName := "test-workspace-1"
+	service := Grafana{
+		Client: mockedGrafanaService{
+			DeleteWorkspaceOutput: grafana.DeleteWorkspaceOutput{},
+		},
+	}
+
+	err := service.nukeAll([]*string{&workspaceName})
+	assert.NoError(t, err)
+}
+
+func Test_Grafana_GetAll(t *testing.T) {
+	t.Parallel()
+
+	now := time.Now()
+	workSpace1 := "test-workspace-1"
+	workSpace2 := "test-workspace-2"
+
+	service := Grafana{
+		Client: mockedGrafanaService{
+			ListWorkspacesOutput: grafana.ListWorkspacesOutput{
+				Workspaces: []types.WorkspaceSummary{
+					{
+						Id:      aws.String(workSpace1),
+						Name:    aws.String(workSpace1),
+						Created: &now,
+						Status:  types.WorkspaceStatusActive,
+					},
+					{
+						Id:      aws.String(workSpace2),
+						Name:    aws.String(workSpace2),
+						Created: aws.Time(now.Add(time.Hour)),
+						Status:  types.WorkspaceStatusActive,
+					},
+				},
+			},
+		},
+	}
+
+	tests := map[string]struct {
+		configObj config.ResourceType
+		expected  []string
+	}{
+		"emptyFilter": {
+			configObj: config.ResourceType{},
+			expected:  []string{workSpace1, workSpace2},
+		},
+		"nameExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					NamesRegExp: []config.Expression{{
+						RE: *regexp.MustCompile(workSpace1),
+					}},
+				}},
+			expected: []string{workSpace2},
+		},
+		"timeAfterExclusionFilter": {
+			configObj: config.ResourceType{
+				ExcludeRule: config.FilterRule{
+					TimeAfter: aws.Time(now.Add(-1 * time.Hour)),
+				}},
+			expected: []string{},
+		},
+	}
+
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			workspaces, err := service.getAll(
+				context.Background(),
+				config.Config{Grafana: tc.configObj},
+			)
+			require.NoError(t, err)
+			require.Equal(t, tc.expected, aws.ToStringSlice(workspaces))
+		})
+	}
+}
diff --git a/config/config_test.go b/config/config_test.go
index 4681f3c..86592ff 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -62,12 +62,14 @@ func emptyConfig() *Config {
 		ElasticIP:                       ResourceType{FilterRule{}, FilterRule{}, "", false},
 		Elasticache:                     ResourceType{FilterRule{}, FilterRule{}, "", false},
 		ElasticacheParameterGroups:      ResourceType{FilterRule{}, FilterRule{}, "", false},
+		ElasticCacheServerless:          ResourceType{FilterRule{}, FilterRule{}, "", false},
 		ElasticacheSubnetGroups:         ResourceType{FilterRule{}, FilterRule{}, "", false},
 		EventBridge:                     ResourceType{FilterRule{}, FilterRule{}, "", false},
 		EventBridgeArchive:              ResourceType{FilterRule{}, FilterRule{}, "", false},
 		EventBridgeRule:                 ResourceType{FilterRule{}, FilterRule{}, "", false},
 		EventBridgeSchedule:             ResourceType{FilterRule{}, FilterRule{}, "", false},
 		EventBridgeScheduleGroup:        ResourceType{FilterRule{}, FilterRule{}, "", false},
+		Grafana:                         ResourceType{FilterRule{}, FilterRule{}, "", false},
 		GuardDuty:                       ResourceType{FilterRule{}, FilterRule{}, "", false},
 		IAMGroups:                       ResourceType{FilterRule{}, FilterRule{}, "", false},
 		IAMPolicies:                     ResourceType{FilterRule{}, FilterRule{}, "", false},