From 9b42f5e4bf79823ce08c28cc56c0038c1e18c1ea Mon Sep 17 00:00:00 2001 From: "AJ (.jASM)" Date: Tue, 31 Dec 2024 01:17:30 +0000 Subject: [PATCH] Skip if tag value is empty or set to true (#823) * add tests for ShouldIncludeBasedOnTag & ShouldInclude in the config pkg, part of #822. * add logic for tag_exist, part of #822. * Revert "add logic for tag_exist, part of #822." This reverts commit f67b0deea62fddfd93dd801e1a65409d98e7a340. * change the behaviour to allow matching only on the tag name when excluding resources from being nuked, fix #822 * change the behaviour of excluding resources to allow specifying the tag value using a regular expression, fix #822 --- README.md | 12 ++- config/config.go | 21 +++-- config/config_test.go | 193 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b1391925..1b9868b1 100644 --- a/README.md +++ b/README.md @@ -527,13 +527,21 @@ s3: #### Tag Filter -You can also exclude resources by tags. The following config will exclude all s3 buckets that have a tag with key `foo` -and value `true` (case-insensitive). +You can also exclude resources by tags. The following configuration will exclude all S3 buckets that have a tag with the key `foo`. +By default, we will check if the tag's value is set to `true` (case-insensitive). +```yaml +s3: + exclude: + tag: 'foo' # exclude if tag foo exists with value of 'true' +``` + +You can also overwrite the expected value by specifying `tag_value` (you can use regular expressions). ```yaml s3: exclude: tag: 'foo' + tag_value: 'dev-.*' ``` #### Timeout You have the flexibility to set individual timeout options for specific resources. The execution will pause until the designated timeout is reached for each resource. diff --git a/config/config.go b/config/config.go index d9f0b0a8..f4ae2aa9 100644 --- a/config/config.go +++ b/config/config.go @@ -13,10 +13,11 @@ import ( ) const ( - DefaultAwsResourceExclusionTagKey = "cloud-nuke-excluded" - CloudNukeAfterExclusionTagKey = "cloud-nuke-after" - CloudNukeAfterTimeFormat = time.RFC3339 - CloudNukeAfterTimeFormatLegacy = time.DateTime + DefaultAwsResourceExclusionTagKey = "cloud-nuke-excluded" + DefaultAwsResourceExclusionTagValue = "true" + CloudNukeAfterExclusionTagKey = "cloud-nuke-after" + CloudNukeAfterTimeFormat = time.RFC3339 + CloudNukeAfterTimeFormatLegacy = time.DateTime ) // Config - the config object we pass around @@ -272,6 +273,7 @@ type FilterRule struct { TimeAfter *time.Time `yaml:"time_after"` TimeBefore *time.Time `yaml:"time_before"` Tag *string `yaml:"tag"` // A tag to filter resources by. (e.g., If set under ExcludedRule, resources with this tag will be excluded). + TagValue *Expression `yaml:"tag_value"` } type Expression struct { @@ -377,6 +379,14 @@ func (r ResourceType) getExclusionTag() string { return DefaultAwsResourceExclusionTagKey } +func (r ResourceType) getExclusionTagValue() *Expression { + if r.ExcludeRule.TagValue != nil { + return r.ExcludeRule.TagValue + } + + return &Expression{RE: *regexp.MustCompile(DefaultAwsResourceExclusionTagValue)} +} + func ParseTimestamp(timestamp string) (*time.Time, error) { parsed, err := time.Parse(CloudNukeAfterTimeFormat, timestamp) if err != nil { @@ -394,8 +404,9 @@ func ParseTimestamp(timestamp string) (*time.Time, error) { func (r ResourceType) ShouldIncludeBasedOnTag(tags map[string]string) bool { // Handle exclude rule first exclusionTag := r.getExclusionTag() + exclusionTagValue := r.getExclusionTagValue() if value, ok := tags[exclusionTag]; ok { - if strings.ToLower(value) == "true" { + if matches(strings.ToLower(value), []Expression{*exclusionTagValue}) { return false } } diff --git a/config/config_test.go b/config/config_test.go index b84a8bfb..4b2a1c42 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -311,3 +311,196 @@ func TestAddIncludeAndExcludeAfterTime(t *testing.T) { assert.Equal(t, testConfig.ACM.ExcludeRule.TimeAfter, now) assert.Equal(t, testConfig.ACM.IncludeRule.TimeAfter, now) } + +func TestGetExclusionTag(t *testing.T) { + tests := []struct { + name string + want string + ExcludeRule FilterRule + }{ + { + name: "empty config returns default tag", + want: DefaultAwsResourceExclusionTagKey, + }, + { + name: "custom exclusion tag is returned", + ExcludeRule: FilterRule{ + Tag: aws.String("my-custom-tag"), + }, + want: "my-custom-tag", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testConfig := &Config{} + testConfig.ACM = ResourceType{ + ExcludeRule: tt.ExcludeRule, + } + + require.Equal(t, tt.want, testConfig.ACM.getExclusionTag()) + }) + } +} + +func TestShouldIncludeBasedOnTag(t *testing.T) { + timeIn2h := time.Now().Add(2 * time.Hour) + + type arg struct { + ExcludeRule FilterRule + ProtectUntilExpire bool + } + tests := []struct { + name string + given arg + when map[string]string + expect bool + }{ + { + name: "should include resource with default exclude tag", + given: arg{}, + when: map[string]string{DefaultAwsResourceExclusionTagKey: "true"}, + expect: false, + }, + { + name: "should include resource with custom exclude tag", + given: arg{ + ExcludeRule: FilterRule{ + Tag: aws.String("my-custom-skip-tag"), + }, + ProtectUntilExpire: false, + }, + when: map[string]string{"my-custom-skip-tag": "true"}, + expect: false, + }, + { + name: "should include resource with custom exclude tag and empty value", + given: arg{ + ExcludeRule: FilterRule{ + Tag: aws.String("my-custom-skip-tag"), + TagValue: &Expression{ + RE: *regexp.MustCompile(""), + }, + }, + ProtectUntilExpire: false, + }, + when: map[string]string{"my-custom-skip-tag": ""}, + expect: false, + }, + { + name: "should include resource with custom exclude tag and empty value (using regular expression)", + given: arg{ + ExcludeRule: FilterRule{ + Tag: aws.String("my-custom-skip-tag"), + TagValue: &Expression{ + RE: *regexp.MustCompile(".*"), + }, + }, + ProtectUntilExpire: false, + }, + when: map[string]string{"my-custom-skip-tag": ""}, + expect: false, + }, + { + name: "should include resource with custom exclude tag and prefix value (using regular expression)", + given: arg{ + ExcludeRule: FilterRule{ + Tag: aws.String("my-custom-skip-tag"), + TagValue: &Expression{ + RE: *regexp.MustCompile("protected-.*"), + }, + }, + ProtectUntilExpire: false, + }, + when: map[string]string{"my-custom-skip-tag": "protected-database"}, + expect: false, + }, + { + name: "should include resource when exclude tag is not set to true", + given: arg{ + ExcludeRule: FilterRule{ + Tag: aws.String(DefaultAwsResourceExclusionTagKey), + }, + ProtectUntilExpire: false, + }, + when: map[string]string{DefaultAwsResourceExclusionTagKey: "false"}, + expect: true, + }, + { + name: "should include resource when no tags are set", + given: arg{ + ExcludeRule: FilterRule{ + Tag: aws.String(DefaultAwsResourceExclusionTagKey), + }, + ProtectUntilExpire: false, + }, + when: map[string]string{}, + expect: true, + }, + { + name: "should include resource when protection expires in the future", + given: arg{ + ExcludeRule: FilterRule{}, + ProtectUntilExpire: true, + }, + when: map[string]string{CloudNukeAfterExclusionTagKey: timeIn2h.Format(time.RFC3339)}, + expect: false, + }, + { + name: "should include resource when protection expired in the past", + given: arg{ + ExcludeRule: FilterRule{}, + ProtectUntilExpire: true, + }, + when: map[string]string{CloudNukeAfterExclusionTagKey: time.Now().Format(time.RFC3339)}, + expect: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := ResourceType{ + ExcludeRule: tt.given.ExcludeRule, + ProtectUntilExpire: tt.given.ProtectUntilExpire, + } + + require.Equal(t, tt.expect, r.ShouldIncludeBasedOnTag(tt.when)) + }) + } +} + +func TestShouldIncludeWithTags(t *testing.T) { + tests := []struct { + name string + tags map[string]string + want bool + }{ + { + name: "should include when resource has no tags", + tags: map[string]string{}, + want: true, + }, + { + name: "should include when resource has tags", + tags: map[string]string{"env": "production"}, + want: true, + }, + { + name: "should include when resource has default skip tag set", + tags: map[string]string{DefaultAwsResourceExclusionTagKey: "true"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testConfig := &Config{ + ACM: ResourceType{}, + } + + assert.Equal(t, tt.want, testConfig.ACM.ShouldInclude(ResourceValue{ + Tags: tt.tags, + })) + }) + } +}