diff --git a/Makefile b/Makefile index 00c81e6af..47e7d10e5 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ test: ## Run unit tests .PHONY: test-all test-all: ## Run tests (including acceptance and integration tests) go clean -testcache ${PKG_LIST} - ./bin/go-acc ${PKG_LIST} -- -v -$(TESTARGS) -p 1 -race -timeout 30m + ./bin/go-acc ${PKG_LIST} -- -v $(TESTARGS) -p 1 -race -timeout 30m .PHONY: build build: ## Build binary diff --git a/README.md b/README.md index dbec9ba51..7010c52fa 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,21 @@ -# AWSweeper - -

- - Release - - - pipeline status - - - Go Report - - - Go Doc - - - Software License - +

+ AWSweeper Logo +

AWSweeper

+

A tool for cleaning your AWS account

-AWSweeper wipes out all (or parts) of the resources in your AWS account. Resources to be deleted can be filtered by their ID, tags or -creation date using [regular expressions](https://golang.org/pkg/regexp/syntax/) declared in a yaml file (see [config.yml](example/config.yml)). +--- +[![Release](https://img.shields.io/github/release/cloudetc/awsweeper.svg?style=for-the-badge)](https://github.com/cloudetc/awsweeper/releases/latest) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge)](/LICENSE.md) +[![Travis](https://img.shields.io/travis/cloudetc/awsweeper/master.svg?style=for-the-badge)](https://travis-ci.org/cloudetc/awsweeper) + +AWSweeper cleans out all (or parts) of the resources in your AWS account. Resources to be deleted can be filtered by +their ID, tags or creation date using [regular expressions](https://golang.org/pkg/regexp/syntax/) declared via a filter +in a YAML file (see [filter.yml](example/config.yml) as an example). AWSweeper [can delete many](#supported-resources), but not all resources yet. Your help -supporting more resources is very much appreciated ([please read this issue](https://github.com/cloudetc/awsweeper/issues/21) - to see how easy it is). Note that AWSweeper is based on the cloud-agnostic Terraform API for deletion - so it's planned to support - deleting Azure and Google Cloud Platform resources soon, too. +to support more resources is very much appreciated ([please read this issue](https://github.com/cloudetc/awsweeper/issues/21) + to see how easy it is). Happy erasing! @@ -32,99 +23,101 @@ Happy erasing! ## Installation -It's recommended to install a specific version of awsweeper available on the +It's recommended to install a specific version of AWSweeper available on the [releases page](https://github.com/cloudetc/awsweeper/releases). -Here is the recommended way to install awsweeper v0.6.0: +Here is the recommended way to install AWSweeper v0.8.0: ```bash # install it into ./bin/ -curl -sSfL https://raw.githubusercontent.com/cloudetc/awsweeper/master/install.sh | sh -s v0.6.0 +curl -sSfL https://raw.githubusercontent.com/cloudetc/awsweeper/master/install.sh | sh -s v0.8.0 ``` ## Usage - awsweeper [options] + awsweeper [options] To see options available run `awsweeper --help`. -## Filtering +## Filter -Resources to be deleted are filtered by a yaml configuration. To learn how, have a look at the following example: +Resources are deleted via a filter declared in a YAML file. aws_instance: + # instance filter part 1 - id: ^foo.* - tags: - foo: bar - bla: blub created: - before: 2018-06-14 - after: 2018-10-28 12:28:39.0000 + before: 2018-10-14 + after: 2018-06-28 12:28:39 + + # instance filter part 2 - tags: foo: bar - created: - before: 2018-06-14 - - tags: - foo: NOT(bar) - created: - after: 2018-06-14 - aws_iam_role: - -This config would delete all instances which ID matches `^foo.*` *AND* which have tags `foo: bar` *AND* `bla: blub` -*AND* which have been created between `2018-10-28 12:28:39 +0000 UTC` and `2018-06-14`. Additionally, it would delete instances -with tag `foo: bar` and which are older than `2018-06-14`. + NOT(owner): .* + + aws_security_groups: -Furthermore, this config would delete all IAM roles, as there is no list of filters provided for this resource type. +The filter snippet above deletes all EC2 instances that ID matches `^foo.*` and that have been created between + `2018-06-28 12:28:39` and `2018-10-14` UTC (instance filter part 1); additionally, EC2 instances having a tag + `foo: bar` *AND* not a tag key `owner` with any value are deleted (instance filter part 2); last but not least, + ALL security groups are deleted by this filter. -The general syntax of the filter config is as follows: +The general filter syntax is as follows: : - # filter 1 - id: | NOT() + tagged: bool (optional) tags: - : | NOT() + | NOT(key): | NOT() ... created: before: (optional) after: (optional) - # filter 2 + # OR - ... : ... -A more detailed description of the ways to filter resources: +Here is a more detailed description of the various ways to filter resources: -##### 1) All resources of a particular type +##### 1) Delete all resources of a particular type - [Terraform types](https://www.terraform.io/docs/providers/aws/index.html) are used to identify resources of a particular type - (e.g., `aws_security_group` selects all resources that are security groups, `aws_iam_role` all roles, - or `aws_instance` all EC2 instances). - - In the example above, by simply adding `security_group:` (no further filters for IDs or tags), - all security groups in your account would be deleted. Use the [all.yml](./all.yml), to delete all (currently supported) + [Terraform resource type indentifiers](https://www.terraform.io/docs/providers/aws/index.html) are used to delete + resources by type. The following filter snippet deletes *ALL* security groups, IAM roles, and EC2 instances: + + aws_security_group: + aws_iam_role: + aws_instance: + + Don't forget the `:` at the end of each line. Use the [all.yml](./all.yml), to delete all (currently supported) resources. -##### 2) By tags - - You can narrow down on particular types of resources by the tags they have. +##### 2) Delete by tags - If most of your resources have tags, this is probably the best to filter them - for deletion. But be aware: not all resources support tags and can be filtered this way. + If most of your resources have tags, this is probably the best way to filter them + for deletion. **Be aware**: Not all resources [support tags](#supported-resources) yet and can be filtered this way. + + The key and the value part of the tag filter can be negated by a surrounding `NOT(...)`. This allows for removing of + all resources not matching some tag key or value. In the example below, all EC2 instances without the `owner: me` + tag are deleted: - In the example above, all EC2 instances are terminated that have a tag with key `foo` and value `bar` as well as - `bla` and value `blub`. - - The tag filter can be negated by surrounding the regex with `NOT(...)` + aws_instance: + - tags: + NOT(Owner): me + + The flag `tagged: false` deletes all resources that have no tags. Contrary, resources with any tags can be deleted + with `tagged: true`: -##### 3) By ID + aws_instance: + - tagged: true - You can narrow down on particular types of resources by filtering on their IDs. +##### 3) Delete By ID - To see what the IDs of your resources are (could be their name, ARN, a random number), - run awsweeper in dry-run mode: `awsweeper --dry-run all.yml`. This way, nothing is deleted but - all the IDs and tags of your resources are printed. Then, use this information to create the yaml file. + You can narrow down on particular types of resources by filtering on based their IDs. - In the example above, all roles which name starts with `foo` are deleted (the ID of roles is their name). + To see what the ID of a resource is (could be its name, ARN, a random number), + run AWSweeper in dry-run mode: `awsweeper --dry-run all.yml`. This way, nothing is deleted but + all the IDs and tags of your resources are printed. Then, use this information to create the YAML config file. The id filter can be negated by surrounding the regex with `NOT(...)` @@ -152,8 +145,8 @@ A more detailed description of the ways to filter resources: ## Dry-run mode - Use `awsweeper --dry-run ` to only show what -would be deleted. This way, you can fine-tune your yaml configuration until it works the way you want it to. + Use `awsweeper --dry-run ` to only show what +would be deleted. This way, you can fine-tune your YAML filter configuration until it works the way you want it to. ## Supported resources diff --git a/command/wipe.go b/command/wipe.go index 32901dc0d..a300a38e9 100644 --- a/command/wipe.go +++ b/command/wipe.go @@ -62,11 +62,16 @@ func list(c *Wipe) []terradozerRes.DestroyableResource { // Run executes the wipe command. func (c *Wipe) Run(args []string) int { if len(args) == 1 { - c.filter = resource.NewFilter(args[0]) + filter, err := resource.NewFilter(args[0]) + if err != nil { + log.WithError(err).Fatal("failed to create resource filter") + } + + c.filter = filter - err := c.filter.Validate() + err = c.filter.Validate() if err != nil { - log.WithError(err).Fatal("failed to validate filter config") + log.WithError(err).Fatal("invalid filter config") } } else { fmt.Println(help()) diff --git a/command/wrapped_main.go b/command/wrapped_main.go index f2fdf749c..3dc8d53ef 100644 --- a/command/wrapped_main.go +++ b/command/wrapped_main.go @@ -21,7 +21,7 @@ import ( // WrappedMain is the actual main function that does not exit for acceptance testing purposes func WrappedMain() int { app := "awsweeper" - version := "v0.7.0" + version := "v0.8.0" set := flag.NewFlagSet(app, 0) versionFlag := set.Bool("version", false, "Show version") diff --git a/img/logo.png b/img/logo.png new file mode 100644 index 000000000..902215129 Binary files /dev/null and b/img/logo.png differ diff --git a/resource/filter.go b/resource/filter.go index 369bfd6bc..69d1601b4 100644 --- a/resource/filter.go +++ b/resource/filter.go @@ -14,15 +14,15 @@ import ( "gopkg.in/yaml.v2" ) -// Config represents the content of a yaml file that is used as a contract to filter resources for deletion. -type Config map[TerraformResourceType][]TypeFilter +// Filter represents the content of a yaml file that is used to filter resources for deletion. +type Filter map[TerraformResourceType][]TypeFilter -// TypeFilter represents an entry in Config and selects the resources of a particular resource type. +// TypeFilter represents an entry in the yaml file to filter the resources of a particular resource type. type TypeFilter struct { - ID *StringFilter `yaml:",omitempty"` - Tags map[string]*StringFilter `yaml:",omitempty"` - // select resources by creation time - Created *Created `yaml:",omitempty"` + ID *StringFilter `yaml:",omitempty"` + Tagged *bool `yaml:",omitempty"` + Tags map[string]StringFilter `yaml:",omitempty"` + Created *Created `yaml:",omitempty"` } type StringMatcher interface { @@ -43,40 +43,28 @@ type Created struct { After *CreatedTime `yaml:",omitempty"` } -// Filter selects resources based on a given yaml config. -type Filter struct { - Cfg Config -} - -// NewFilter creates a new filter based on a config given via a yaml file. -func NewFilter(yamlFile string) *Filter { - return &Filter{ - Cfg: read(yamlFile), - } -} +// NewFilter creates a resource filter defined via a given path to a yaml file. +func NewFilter(path string) (*Filter, error) { + var cfg Filter -// read reads a filter from a yaml file. -func read(filename string) Config { - var cfg Config - - data, err := ioutil.ReadFile(filename) + data, err := ioutil.ReadFile(path) if err != nil { - log.WithError(err).Fatalf("failed to read config file: %s", filename) + return nil, err } err = yaml.UnmarshalStrict(data, &cfg) if err != nil { - log.WithError(err).Fatalf("failed to unmarshal config: %s", filename) + return nil, err } - return cfg + return &cfg, nil } // Validate checks if all resource types appearing in the config are currently supported. func (f Filter) Validate() error { for _, resType := range f.Types() { if !SupportedResourceType(resType) { - return fmt.Errorf("unsupported resource type found in yaml config: %s", resType) + return fmt.Errorf("unsupported resource type: %s", resType) } } return nil @@ -84,9 +72,9 @@ func (f Filter) Validate() error { // Types returns all the resource types in the config in their dependency order. func (f Filter) Types() []TerraformResourceType { - resTypes := make([]TerraformResourceType, 0, len(f.Cfg)) + resTypes := make([]TerraformResourceType, 0, len(f)) - for k := range f.Cfg { + for k := range f { resTypes = append(resTypes, k) } @@ -98,12 +86,12 @@ func (f Filter) Types() []TerraformResourceType { } // MatchID checks whether a resource ID matches the filter. -func (rtf TypeFilter) matchID(id string) bool { - if rtf.ID == nil { +func (f TypeFilter) matchID(id string) bool { + if f.ID == nil { return true } - if ok, err := rtf.ID.matches(id); ok { + if ok, err := f.ID.matches(id); ok { if err != nil { log.WithError(err).Fatal("failed to match ID") } @@ -113,22 +101,47 @@ func (rtf TypeFilter) matchID(id string) bool { return false } -// MatchesTags checks whether a resource's tags -// match the filter. The keys must match exactly, whereas the tag value is checked against a regex. -func (rtf TypeFilter) matchTags(tags map[string]string) bool { - if rtf.Tags == nil { +// MatchTagged filters resources with a non-empty or empty tag set. +func (f TypeFilter) MatchTagged(tags map[string]string) bool { + if f.Tagged == nil { + return true + } + + if *f.Tagged && len(tags) != 0 { + return true + } + + if !*f.Tagged && len(tags) == 0 { + return true + } + + return false +} + +// MatchesTags checks whether a resource's tag set matches the filter. +func (f TypeFilter) MatchTags(tags map[string]string) bool { + return f.matchIncludedTags(tags) && f.matchExcludedTags(tags) +} + +// matchIncludedTags checks for tags that must be included in a resource's tag set. +func (f TypeFilter) matchIncludedTags(tags map[string]string) bool { + tagFilters := notNegatedTagFilterExpr(f.Tags) + + if len(tagFilters) == 0 { return true } - for cfgTagKey, regex := range rtf.Tags { - if tagVal, ok := tags[cfgTagKey]; ok { - if matched, err := regex.matches(tagVal); !matched { - if err != nil { - log.WithError(err).Fatal("failed to match tags") - } - return false + for key, valueFilter := range tagFilters { + value, ok := tags[key] + if !ok { + return false + } + + if match, err := valueFilter.matches(value); !match { + if err != nil { + log.WithError(err).Fatal("failed to match tags") } - } else { + return false } } @@ -136,8 +149,67 @@ func (rtf TypeFilter) matchTags(tags map[string]string) bool { return true } -func (rtf TypeFilter) matchCreated(creationTime *time.Time) bool { - if rtf.Created == nil { +// matchExcludedTags checks for tags that must not exist in a resource's tag set. +func (f TypeFilter) matchExcludedTags(tags map[string]string) bool { + tagFilters := negatedTagFilterExpr(f.Tags) + + if len(tagFilters) == 0 { + return true + } + + for key, valueFilter := range tagFilters { + value, ok := tags[key] + if !ok { + return true + } + + if match, err := valueFilter.matches(value); !match { + if err != nil { + log.WithError(err).Fatal("failed to match tags") + } + + return true + } + } + + return false +} + +// notNegatedTagFilterExpr returns tag filter expressions where keys are not surrounded by NOT(...). +func notNegatedTagFilterExpr(tags map[string]StringFilter) map[string]StringFilter { + result := map[string]StringFilter{} + + for key, value := range tags { + if !isNegatedTagKey(key) { + result[key] = value + } + } + + return result +} + +// notNegatedTagFilterExpr returns tag filter expressions where keys are surrounded by NOT(...). +func negatedTagFilterExpr(tags map[string]StringFilter) map[string]StringFilter { + result := map[string]StringFilter{} + + for key, value := range tags { + if isNegatedTagKey(key) { + key = strings.TrimPrefix(key, "NOT(") + key = strings.TrimSuffix(key, ")") + + result[key] = value + } + } + + return result +} + +func isNegatedTagKey(key string) bool { + return strings.HasPrefix(key, "NOT(") && strings.HasSuffix(key, ")") +} + +func (f TypeFilter) matchCreated(creationTime *time.Time) bool { + if f.Created == nil { return true } @@ -146,21 +218,21 @@ func (rtf TypeFilter) matchCreated(creationTime *time.Time) bool { } createdAfter := true - if rtf.Created.After != nil { - createdAfter = creationTime.Unix() > rtf.Created.After.Unix() + if f.Created.After != nil { + createdAfter = creationTime.Unix() > f.Created.After.Unix() } createdBefore := true - if rtf.Created.Before != nil { - createdBefore = creationTime.Unix() < rtf.Created.Before.Unix() + if f.Created.Before != nil { + createdBefore = creationTime.Unix() < f.Created.Before.Unix() } return createdAfter && createdBefore } -// matches checks whether a resource matches the filter criteria. -func (f Filter) matches(r *Resource) bool { - resTypeFilters, found := f.Cfg[r.Type] +// Match checks whether a resource matches the filter criteria. +func (f Filter) Match(r *Resource) bool { + resTypeFilters, found := f[r.Type] if !found { return false } @@ -170,10 +242,14 @@ func (f Filter) matches(r *Resource) bool { } for _, rtf := range resTypeFilters { - if rtf.matchTags(r.Tags) && rtf.matchID(r.ID) && rtf.matchCreated(r.Created) { + if rtf.MatchTagged(r.Tags) && + rtf.MatchTags(r.Tags) && + rtf.matchID(r.ID) && + rtf.matchCreated(r.Created) { return true } } + return false } diff --git a/resource/filter_test.go b/resource/filter_test.go index 659895e41..ccac01415 100644 --- a/resource/filter_test.go +++ b/resource/filter_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/cloudetc/awsweeper/resource" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,12 +16,10 @@ import ( func TestYamlFilter_Validate(t *testing.T) { // given f := &resource.Filter{ - Cfg: resource.Config{ - resource.IamRole: {}, - resource.SecurityGroup: {}, - resource.Instance: {}, - resource.Vpc: {}, - }, + resource.IamRole: {}, + resource.SecurityGroup: {}, + resource.Instance: {}, + resource.Vpc: {}, } // when @@ -31,9 +31,7 @@ func TestYamlFilter_Validate(t *testing.T) { func TestYamlFilter_Validate_EmptyConfig(t *testing.T) { // given - f := &resource.Filter{ - Cfg: resource.Config{}, - } + f := &resource.Filter{} // when err := f.Validate() @@ -45,26 +43,22 @@ func TestYamlFilter_Validate_EmptyConfig(t *testing.T) { func TestYamlFilter_Validate_UnsupportedType(t *testing.T) { // given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: {}, - "not_supported_type": {}, - }, + resource.Instance: {}, + "not_supported_type": {}, } // when err := f.Validate() // then - assert.EqualError(t, err, "unsupported resource type found in yaml config: not_supported_type") + assert.EqualError(t, err, "unsupported resource type: not_supported_type") } func TestYamlFilter_Types(t *testing.T) { // given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: {}, - resource.Vpc: {}, - }, + resource.Instance: {}, + resource.Vpc: {}, } // when @@ -79,10 +73,8 @@ func TestYamlFilter_Types(t *testing.T) { func TestYamlFilter_Types_DependencyOrder(t *testing.T) { // given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Subnet: {}, - resource.Vpc: {}, - }, + resource.Subnet: {}, + resource.Vpc: {}, } // when @@ -104,7 +96,7 @@ func Test_ParseFile(t *testing.T) { created: before: 23h`) - var cfg resource.Config + var cfg resource.Filter err := yaml.UnmarshalStrict(input, &cfg) require.NoError(t, err) require.NotNil(t, cfg[resource.Instance]) @@ -125,3 +117,281 @@ func Test_ParseFile(t *testing.T) { assert.True(t, cfg[resource.Instance][1].Created.Before.After(time.Now().UTC().Add(-24*time.Hour))) require.Nil(t, cfg[resource.Instance][1].Created.After) } + +func TestTypeFilter_MatchTagged(t *testing.T) { + tests := []struct { + name string + filter resource.TypeFilter + tags map[string]string + want bool + }{ + { + name: "no tagged filter, resource has tags", + filter: resource.TypeFilter{}, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "no tagged filter, resource has no tags", + filter: resource.TypeFilter{}, + want: true, + }, + { + name: "filter tagged resources, resource has tags", + filter: resource.TypeFilter{ + Tagged: aws.Bool(true), + }, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "filter tagged resources, resource has no tags", + filter: resource.TypeFilter{ + Tagged: aws.Bool(true), + }, + want: false, + }, + { + name: "filter untagged resources, resource has tags", + filter: resource.TypeFilter{ + Tagged: aws.Bool(false), + }, + tags: map[string]string{"foo": "bar"}, + want: false, + }, + { + name: "filter untagged resources, resource has no tags", + filter: resource.TypeFilter{ + Tagged: aws.Bool(false), + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.filter.MatchTagged(tt.tags); got != tt.want { + t.Errorf("MatchTagged() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTypeFilter_MatchTags(t *testing.T) { + tests := []struct { + name string + filter resource.TypeFilter + tags map[string]string + want bool + }{ + { + name: "no tags filter, resources has no tags", + filter: resource.TypeFilter{}, + want: true, + }, + { + name: "no tags filter, resources has tags", + filter: resource.TypeFilter{}, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "filter one tag, resource has no tags", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + }, + }, + want: false, + }, + { + name: "filter one tag, resource tags have no matching key", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foz": "bar"}, + want: false, + }, + { + name: "filter one tag, one resource tag's key matches, but not value", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, + }, + }, + tags: map[string]string{"foo": "baz"}, + want: false, + }, + { + name: "filter one tag, resource tag's key and value match", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "filter one tag, one out of multiple resource tag's key and value match", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: true, + }, + { + name: "filter multiple tags, all match", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + "boo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: true, + }, + { + name: "filter multiple tags, one doesn't match (key)", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + "boo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boz": "baz"}, + want: false, + }, + { + name: "filter multiple tags, one doesn't match (value)", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^ba"}, + "boo": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "boz"}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.filter.MatchTags(tt.tags); got != tt.want { + t.Errorf("MatchTags() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTypeFilter_MatchNoTags(t *testing.T) { + tests := []struct { + name string + filter resource.TypeFilter + tags map[string]string + want bool + }{ + { + name: "no notags filter, resource has no tags", + filter: resource.TypeFilter{}, + want: true, + }, + { + name: "no notags filter, resource has tags", + filter: resource.TypeFilter{}, + tags: map[string]string{"foo": "bar"}, + want: true, + }, + { + name: "resource has no tags", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + }, + }, + want: true, + }, + { + name: "no matching key", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foz": "bar"}, + want: true, + }, + { + name: "matching key, but not value", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^bar"}, + }, + }, + tags: map[string]string{"foo": "baz"}, + want: true, + }, + { + name: "matching key and value", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: false, + }, + { + name: "matching key and value, multiple tags", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: false, + }, + { + name: "multiple filter match", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + "NOT(boo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "baz"}, + want: false, + }, + { + name: "one of multiple filter rules doesn't match key", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + "NOT(boo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boz": "baz"}, + want: true, + }, + { + name: "one of multiple filter rules doesn't match value", + filter: resource.TypeFilter{ + Tags: map[string]resource.StringFilter{ + "NOT(foo)": {Pattern: "^ba"}, + "NOT(boo)": {Pattern: "^ba"}, + }, + }, + tags: map[string]string{"foo": "bar", "boo": "boz"}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.filter.MatchTags(tt.tags); got != tt.want { + t.Errorf("MatchTags() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/resource/select.go b/resource/select.go index d534ed422..c430eaf34 100644 --- a/resource/select.go +++ b/resource/select.go @@ -37,7 +37,7 @@ func (f Filter) defaultFilter(res Resources) []Resources { result := Resources{} for _, r := range res { - if f.matches(r) { + if f.Match(r) { result = append(result, r) } } @@ -49,7 +49,7 @@ func (f Filter) efsFileSystemFilter(res Resources, raw interface{}, c *AWS) []Re resultMt := Resources{} for _, r := range res { - if f.matches(&Resource{Type: r.Type, ID: *raw.([]*efs.FileSystemDescription)[0].Name}) { + if f.Match(&Resource{Type: r.Type, ID: *raw.([]*efs.FileSystemDescription)[0].Name}) { res, err := c.DescribeMountTargets(&efs.DescribeMountTargetsInput{ FileSystemId: &r.ID, }) @@ -74,7 +74,7 @@ func (f Filter) iamUserFilter(res Resources, c *AWS) []Resources { resultUserPol := Resources{} for _, r := range res { - if f.matches(r) { + if f.Match(r) { // list inline policies, delete with "aws_iam_user_policy" delete routine ups, err := c.ListUserPolicies(&iam.ListUserPoliciesInput{ UserName: &r.ID, @@ -116,7 +116,7 @@ func (f Filter) iamPolicyFilter(res Resources, raw interface{}, c *AWS) []Resour resultAtt := Resources{} for i, r := range res { - if f.matches(r) { + if f.Match(r) { es, err := c.ListEntitiesForPolicy(&iam.ListEntitiesForPolicyInput{ PolicyArn: &r.ID, }) @@ -161,7 +161,7 @@ func (f Filter) kmsKeysFilter(res Resources, c *AWS) []Resources { result := Resources{} for _, r := range res { - if f.matches(r) { + if f.Match(r) { req, res := c.DescribeKeyRequest(&kms.DescribeKeyInput{ KeyId: aws.String(r.ID), }) @@ -184,7 +184,7 @@ func (f Filter) kmsKeyAliasFilter(res Resources) []Resources { result := Resources{} for _, r := range res { - if f.matches(r) && !strings.HasPrefix(r.ID, "alias/aws/") { + if f.Match(r) && !strings.HasPrefix(r.ID, "alias/aws/") { result = append(result, r) } } diff --git a/resource/select_test.go b/resource/select_test.go index e4332c95e..0317e20d5 100644 --- a/resource/select_test.go +++ b/resource/select_test.go @@ -14,9 +14,8 @@ import ( func TestYamlFilter_Apply_EmptyConfig(t *testing.T) { //given - f := &resource.Filter{ - Cfg: resource.Config{}, - } + f := &resource.Filter{} + res := []*resource.Resource{ { Type: resource.Instance, @@ -34,9 +33,7 @@ func TestYamlFilter_Apply_EmptyConfig(t *testing.T) { func TestYamlFilter_Apply_FilterAll(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: {}, - }, + resource.Instance: {}, } res := []*resource.Resource{ { @@ -56,11 +53,9 @@ func TestYamlFilter_Apply_FilterAll(t *testing.T) { func TestYamlFilter_Apply_FilterByID(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - ID: &resource.StringFilter{Pattern: "^select"}, - }, + resource.Instance: { + { + ID: &resource.StringFilter{Pattern: "^select"}, }, }, } @@ -87,12 +82,10 @@ func TestYamlFilter_Apply_FilterByID(t *testing.T) { func TestYamlFilter_Apply_FilterByTag(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar"}, - }, + resource.Instance: { + { + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, }, }, }, @@ -130,13 +123,11 @@ func TestYamlFilter_Apply_FilterByTag(t *testing.T) { func TestYamlFilter_Apply_FilterByMultipleTags(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar"}, - "bla": {Pattern: "^blub"}, - }, + resource.Instance: { + { + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, + "bla": {Pattern: "^blub"}, }, }, }, @@ -171,13 +162,11 @@ func TestYamlFilter_Apply_FilterByMultipleTags(t *testing.T) { func TestYamlFilter_Apply_FilterByIDandTag(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - ID: &resource.StringFilter{Pattern: "^foo"}, - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar"}, - }, + resource.Instance: { + { + ID: &resource.StringFilter{Pattern: "^foo"}, + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, }, }, }, @@ -215,13 +204,11 @@ func TestYamlFilter_Apply_FilterByIDandTag(t *testing.T) { func TestYamlFilter_Apply_Created(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Created: &resource.Created{ - After: &resource.CreatedTime{Time: time.Date(2018, 11, 17, 0, 0, 0, 0, time.UTC)}, - Before: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, - }, + resource.Instance: { + { + Created: &resource.Created{ + After: &resource.CreatedTime{Time: time.Date(2018, 11, 17, 0, 0, 0, 0, time.UTC)}, + Before: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, }, }, }, @@ -265,12 +252,10 @@ func TestYamlFilter_Apply_Created(t *testing.T) { func TestYamlFilter_Apply_CreatedBefore(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Created: &resource.Created{ - Before: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, - }, + resource.Instance: { + { + Created: &resource.Created{ + Before: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, }, }, }, @@ -304,12 +289,10 @@ func TestYamlFilter_Apply_CreatedBefore(t *testing.T) { func TestYamlFilter_Apply_CreatedAfter(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - Created: &resource.Created{ - After: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, - }, + resource.Instance: { + { + Created: &resource.Created{ + After: &resource.CreatedTime{Time: time.Date(2018, 11, 20, 0, 0, 0, 0, time.UTC)}, }, }, }, @@ -343,15 +326,13 @@ func TestYamlFilter_Apply_CreatedAfter(t *testing.T) { func TestYamlFilter_Apply_MultipleFiltersPerResourceType(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - ID: &resource.StringFilter{Pattern: "^select"}, - }, - { - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar"}, - }, + resource.Instance: { + { + ID: &resource.StringFilter{Pattern: "^select"}, + }, + { + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar"}, }, }, }, @@ -393,15 +374,13 @@ func TestYamlFilter_Apply_MultipleFiltersPerResourceType(t *testing.T) { func TestYamlFilter_Apply_NegatedStringFilter(t *testing.T) { //given f := &resource.Filter{ - Cfg: resource.Config{ - resource.Instance: { - { - ID: &resource.StringFilter{Pattern: "^select", Negate: true}, - }, - { - Tags: map[string]*resource.StringFilter{ - "foo": {Pattern: "^bar", Negate: true}, - }, + resource.Instance: { + { + ID: &resource.StringFilter{Pattern: "^select", Negate: true}, + }, + { + Tags: map[string]resource.StringFilter{ + "foo": {Pattern: "^bar", Negate: true}, }, }, },