Skip to content

Commit

Permalink
Skip if tag value is empty or set to true (#823)
Browse files Browse the repository at this point in the history
* 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 f67b0de.

* 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
  • Loading branch information
wakeful authored Dec 31, 2024
1 parent 8de0565 commit 9b42f5e
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 7 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 16 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
}
Expand Down
193 changes: 193 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}))
})
}
}

0 comments on commit 9b42f5e

Please sign in to comment.