Repository: gruntwork-io/cloud-nuke
Pull request #581 (2023-09-07 17:48:55)

Implement newer_than filter for aws nuke command
<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->

## Description

Fixes https://github.com/gruntwork-io/cloud-nuke/issues/485. 

Testing with the newer-than filter (retrieve elements "newer" than 1 hour): 
<img width="855" alt="image" src="https://github.com/gruntwork-io/cloud-nuke/assets/96548424/01e6510c-ecac-4516-8554-c6ce017ba696">

Testing with the newer-than filter (retrieve elements "newer" than 1s):
<img width="886" alt="image" src="https://github.com/gruntwork-io/cloud-nuke/assets/96548424/c4349d54-d7c6-4dd4-bb68-9251bac47cc9">


TODO: we may want to reword these filters to `excludeAfter` and `includeAfter` to be consistent with the naming within the code. Also, it could be quite confusing to understand "older-than" and "newer-than". 



<!-- Description of the changes introduced by this PR. -->

## TODOs

Read the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).

- [ ] Update the docs.
- [ ] Run the relevant tests successfully, including pre-commit checks.
- [ ] 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.
- [ ] 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. -->
Added / Removed / Updated [X].

### Migration Guide

<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->




Patch:
diff --git a/aws/aws.go b/aws/aws.go
index c67ddb8..b28bf0e 100644
--- a/aws/aws.go
+++ b/aws/aws.go
@@ -21,21 +21,17 @@ import (
 )
 
 // GetAllResources - Lists all aws resources
-func GetAllResources(
-	targetRegions []string,
-	excludeAfter time.Time,
-	resourceTypes []string,
-	configObj config.Config,
-	allowDeleteUnaliasedKeys bool) (*AwsAccountResources, error) {
-
-	configObj.AddExcludeAfterTime(&excludeAfter)
-	configObj.KMSCustomerKeys.IncludeUnaliasedKeys = allowDeleteUnaliasedKeys
+func GetAllResources(query *Query, configObj config.Config) (*AwsAccountResources, error) {
+
+	configObj.AddExcludeAfterTime(query.ExcludeAfter)
+	configObj.AddIncludeAfterTime(query.IncludeAfter)
+	configObj.KMSCustomerKeys.IncludeUnaliasedKeys = query.ListUnaliasedKMSKeys
 	account := AwsAccountResources{
 		Resources: make(map[string]AwsRegionResource),
 	}
 
 	spinner, _ := pterm.DefaultSpinner.WithRemoveWhenDone(true).Start()
-	for _, region := range targetRegions {
+	for _, region := range query.Regions {
 		cloudNukeSession := NewSession(region)
 		stsService := sts.New(cloudNukeSession)
 		resp, err := stsService.GetCallerIdentity(&sts.GetCallerIdentityInput{})
@@ -46,7 +42,7 @@ func GetAllResources(
 		awsResource := AwsRegionResource{}
 		registeredResources := GetAndInitRegisteredResources(cloudNukeSession, region)
 		for _, resource := range registeredResources {
-			if IsNukeable((*resource).ResourceName(), resourceTypes) {
+			if IsNukeable((*resource).ResourceName(), query.ResourceTypes) {
 				spinner.UpdateText(
 					fmt.Sprintf("Searching %s resources in %s", (*resource).ResourceName(), region))
 				start := time.Now()
diff --git a/aws/resources/elasticache.go b/aws/resources/elasticache.go
index d08220b..f8f3949 100644
--- a/aws/resources/elasticache.go
+++ b/aws/resources/elasticache.go
@@ -282,9 +282,10 @@ func (sg *ElasticacheSubnetGroups) getAll(configObj config.Config) ([]*string, e
 		&elasticache.DescribeCacheSubnetGroupsInput{},
 		func(page *elasticache.DescribeCacheSubnetGroupsOutput, lastPage bool) bool {
 			for _, subnetGroup := range page.CacheSubnetGroups {
-				if configObj.ElasticacheSubnetGroups.ShouldInclude(config.ResourceValue{
-					Name: subnetGroup.CacheSubnetGroupName,
-				}) {
+				if !strings.Contains(*subnetGroup.CacheSubnetGroupName, "default") &&
+					configObj.ElasticacheSubnetGroups.ShouldInclude(config.ResourceValue{
+						Name: subnetGroup.CacheSubnetGroupName,
+					}) {
 					subnetGroupNames = append(subnetGroupNames, subnetGroup.CacheSubnetGroupName)
 				}
 			}
diff --git a/aws/types.go b/aws/types.go
index 7e576f2..015e337 100644
--- a/aws/types.go
+++ b/aws/types.go
@@ -91,18 +91,20 @@ type Query struct {
 	ExcludeRegions       []string
 	ResourceTypes        []string
 	ExcludeResourceTypes []string
-	ExcludeAfter         time.Time
+	ExcludeAfter         *time.Time
+	IncludeAfter         *time.Time
 	ListUnaliasedKMSKeys bool
 }
 
 // NewQuery configures and returns a Query struct that can be passed into the InspectResources method
-func NewQuery(regions, excludeRegions, resourceTypes, excludeResourceTypes []string, excludeAfter time.Time, listUnaliasedKMSKeys bool) (*Query, error) {
+func NewQuery(regions, excludeRegions, resourceTypes, excludeResourceTypes []string, excludeAfter, includeAfter *time.Time, listUnaliasedKMSKeys bool) (*Query, error) {
 	q := &Query{
 		Regions:              regions,
 		ExcludeRegions:       excludeRegions,
 		ResourceTypes:        resourceTypes,
 		ExcludeResourceTypes: excludeResourceTypes,
 		ExcludeAfter:         excludeAfter,
+		IncludeAfter:         includeAfter,
 		ListUnaliasedKMSKeys: listUnaliasedKMSKeys,
 	}
 
diff --git a/commands/cli.go b/commands/cli.go
index db5f449..206cd26 100644
--- a/commands/cli.go
+++ b/commands/cli.go
@@ -71,6 +71,11 @@ func CreateCli(version string) *cli.App {
 					Usage: "Only delete resources older than this specified value. Can be any valid Go duration, such as 10m or 8h.",
 					Value: "0s",
 				},
+				&cli.StringFlag{
+					Name:  "newer-than",
+					Usage: "Only delete resources newer than this specified value. Can be any valid Go duration, such as 10m or 8h.",
+					Value: "0s",
+				},
 				&cli.BoolFlag{
 					Name:  "dry-run",
 					Usage: "Dry run without taking any action.",
@@ -152,6 +157,11 @@ func CreateCli(version string) *cli.App {
 					Usage: "Only inspect resources older than this specified value. Can be any valid Go duration, such as 10m or 8h.",
 					Value: "0s",
 				},
+				&cli.StringFlag{
+					Name:  "newer-than",
+					Usage: "Only delete resources newer than this specified value. Can be any valid Go duration, such as 10m or 8h.",
+					Value: "0s",
+				},
 				&cli.BoolFlag{
 					Name:  "list-unaliased-kms-keys",
 					Usage: "List KMS keys that do not have aliases associated with them.",
@@ -170,6 +180,10 @@ func CreateCli(version string) *cli.App {
 }
 
 func parseDurationParam(paramValue string) (*time.Time, error) {
+	if paramValue == "0s" {
+		return nil, nil
+	}
+
 	duration, err := time.ParseDuration(paramValue)
 	if err != nil {
 		return nil, errors.WithStackTrace(err)
@@ -527,9 +541,11 @@ func handleGetResources(c *cli.Context, configObj config.Config, includeUnaliase
 	*aws.Query, *aws.AwsAccountResources, error) {
 	excludeAfter, err := parseDurationParam(c.String("older-than"))
 	if err != nil {
-		telemetry.TrackEvent(commonTelemetry.EventContext{
-			EventName: "Error parsing duration",
-		}, map[string]interface{}{})
+		return nil, nil, errors.WithStackTrace(err)
+	}
+
+	includeAfter, err := parseDurationParam(c.String("newer-than"))
+	if err != nil {
 		return nil, nil, errors.WithStackTrace(err)
 	}
 
@@ -538,7 +554,8 @@ func handleGetResources(c *cli.Context, configObj config.Config, includeUnaliase
 		c.StringSlice("exclude-region"),
 		c.StringSlice("resource-type"),
 		c.StringSlice("exclude-resource-type"),
-		*excludeAfter,
+		excludeAfter,
+		includeAfter,
 		includeUnaliasedKmsKeys,
 	)
 	if err != nil {
@@ -552,8 +569,7 @@ func handleGetResources(c *cli.Context, configObj config.Config, includeUnaliase
 	}
 	pterm.Println()
 
-	accountResources, err := aws.GetAllResources(
-		query.Regions, query.ExcludeAfter, query.ResourceTypes, configObj, query.ListUnaliasedKMSKeys)
+	accountResources, err := aws.GetAllResources(query, configObj)
 	if err != nil {
 		telemetry.TrackEvent(commonTelemetry.EventContext{
 			EventName: "Error inspecting resources",
@@ -624,7 +640,12 @@ func RenderQueryAsBulletList(query *aws.Query) error {
 		tableData = append(tableData, []string{"Target Resource Types", strings.Join(query.ResourceTypes, ", ")})
 	}
 
-	tableData = append(tableData, []string{"Exclude After Filter", query.ExcludeAfter.Format("2006-01-02 15:04:05")})
+	if query.ExcludeAfter != nil {
+		tableData = append(tableData, []string{"Exclude After Filter", query.ExcludeAfter.Format("2006-01-02 15:04:05")})
+	}
+	if query.IncludeAfter != nil {
+		tableData = append(tableData, []string{"Include After Filter", query.IncludeAfter.Format("2006-01-02 15:04:05")})
+	}
 	tableData = append(tableData, []string{"List Unaliased KMS Keys", fmt.Sprintf("%t", query.ListUnaliasedKMSKeys)})
 
 	return pterm.DefaultTable.WithBoxed(true).
diff --git a/config/config.go b/config/config.go
index a1b4538..75d4fd2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -79,28 +79,40 @@ type Config struct {
 	VPC                          ResourceType               `yaml:"VPC"`
 }
 
-func (c *Config) AddExcludeAfterTime(excludeAfter *time.Time) {
-	// exclude after filter has been applied to all resources via `older-than` flag, we are
-	// setting this rule across all resource types.
-	//
-	// TODO: after refactoring all the code, we can remove having excludeAfter in config and
-	//  passing in as additional argument to GetAllResources.
+func (c *Config) addTimeAfterFilter(timeFilter *time.Time, fieldName string) {
+	// Do nothing if the time filter is nil
+	if timeFilter == nil {
+		return
+	}
+
 	v := reflect.ValueOf(c).Elem()
-	excludeFilterRule := FilterRule{TimeAfter: excludeAfter}
+	filterRule := FilterRule{TimeAfter: timeFilter}
 	for i := 0; i < v.NumField(); i++ {
 		field := v.Field(i)
 		if field.Kind() == reflect.Struct {
-			excludeRuleField := field.FieldByName("ExcludeRule")
-			if excludeRuleField.CanSet() {
-				excludeRuleField.Set(reflect.ValueOf(excludeFilterRule))
+			ruleField := field.FieldByName(fieldName)
+			if ruleField.CanSet() {
+				ruleField.Set(reflect.ValueOf(filterRule))
 			}
 		}
 	}
 }
 
+func (c *Config) AddIncludeAfterTime(includeAfter *time.Time) {
+	// include after filter has been applied to all resources via `newer-than` flag, we are
+	// setting this rule across all resource types.
+	c.addTimeAfterFilter(includeAfter, "IncludeRule")
+}
+
+func (c *Config) AddExcludeAfterTime(excludeAfter *time.Time) {
+	// exclude after filter has been applied to all resources via `older-than` flag, we are
+	// setting this rule across all resource types.
+	c.addTimeAfterFilter(excludeAfter, "ExcludeRule")
+}
+
 type KMSCustomerKeyResourceType struct {
 	IncludeUnaliasedKeys bool `yaml:"include_unaliased_keys"`
-	ResourceType        `yaml:",inline"`
+	ResourceType         `yaml:",inline"`
 }
 
 type ResourceType struct {


Test patch:
diff --git a/aws/types_test.go b/aws/types_test.go
index 4aebd6d..fb988dc 100644
--- a/aws/types_test.go
+++ b/aws/types_test.go
@@ -1,6 +1,7 @@
 package aws
 
 import (
+	"github.com/aws/aws-sdk-go/aws"
 	"testing"
 	"time"
 
@@ -17,7 +18,8 @@ func TestNewQueryAcceptsValidExcludeAfterEntries(t *testing.T) {
 		ExcludeRegions       []string
 		ResourceTypes        []string
 		ExcludeResourceTypes []string
-		ExcludeAfter         time.Time
+		ExcludeAfter         *time.Time
+		IncludeAfter         *time.Time
 	}
 
 	testCases := []TestCase{
@@ -26,20 +28,22 @@ func TestNewQueryAcceptsValidExcludeAfterEntries(t *testing.T) {
 			Regions:        []string{"us-east-1"},
 			ExcludeRegions: []string{},
 			ResourceTypes:  []string{"ec2"},
-			ExcludeAfter:   time.Now().Add(time.Hour * 24),
+			ExcludeAfter:   aws.Time(time.Now().Add(time.Hour * 24)),
+			IncludeAfter:   aws.Time(time.Now().Add(time.Hour * 24)),
 		},
 		{
 			Name:           "Can pass time.Now",
 			Regions:        []string{"us-east-1"},
 			ExcludeRegions: []string{},
 			ResourceTypes:  []string{"ec2"},
-			ExcludeAfter:   time.Now(),
+			ExcludeAfter:   aws.Time(time.Now()),
+			IncludeAfter:   aws.Time(time.Now()),
 		},
 	}
 
 	for _, tc := range testCases {
 		t.Run(tc.Name, func(t *testing.T) {
-			_, err := NewQuery(tc.Regions, tc.ExcludeRegions, tc.ResourceTypes, tc.ExcludeResourceTypes, tc.ExcludeAfter, false)
+			_, err := NewQuery(tc.Regions, tc.ExcludeRegions, tc.ResourceTypes, tc.ExcludeResourceTypes, tc.ExcludeAfter, tc.IncludeAfter, false)
 			require.NoError(t, err)
 		})
 	}