Skip to content

Commit

Permalink
feat: global filter support (#37)
Browse files Browse the repository at this point in the history
* feat: global filter support

* tests: for filters

* fix: remove reference to group

* fix: remove reference to group

* fix: remove op references

* fix: remove OpType

* test: add more tests

* fix: golangci-lint run
  • Loading branch information
ekristen authored Feb 16, 2024
1 parent adb6cff commit 3efe6a0
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 28 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ This provides common errors that can be used throughout the library for handling

### filter

This provides a way to filter resources based on a set of criteria.
This provides a way to filter resources based on a set of criteria. See [full documentation](pkg/filter/README.md)
for more information.

### log

Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func (c *Config) Filters(accountID string) (filter.Filters, error) {
return nil, notFound
}

filters.Merge(preset.Filters)
filters.Append(preset.Filters)
}

return filters, nil
Expand Down
32 changes: 32 additions & 0 deletions pkg/filter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Filter

The filter package is designed to allow you to define filters, primarily by yaml to be able to filter resources. This is
used in the `nuke` package to filter resources based on a set of criteria.

Filter's can be optionally added to a `group` filters within a `group` are combined with an `AND` operation. Filters in
different `group` are combined with an `OR` operation.

There is also the concept of a `global` filter that is applied to all resources.

## Types

There are multiple filter types that can be used to filter the resources. These types are used to match against the
property.

- empty
- exact
- glob
- regex
- contains
- dateOlderThan
- suffix
- prefix

## Global

You can define a global filter that will be applied to all resources. This is useful for defining a set of filters that
should be applied to all resources.

It has a special key called `__global__`.

This only works when you are defining it as a resource type as part of the `Filters` `map[string][]Filter` type.
48 changes: 44 additions & 4 deletions pkg/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,32 @@ const (
DateOlderThan Type = "dateOlderThan"
Suffix Type = "suffix"
Prefix Type = "prefix"

Global = "__global__"
)

type Filters map[string][]Filter

// Get returns the filters for a specific resource type or the global filters if they exist. If there are no filters it
// returns nil
func (f Filters) Get(resourceType string) []Filter {
var filters []Filter

if f[Global] != nil {
filters = append(filters, f[Global]...)
}

if f[resourceType] != nil {
filters = append(filters, f[resourceType]...)
}

if len(filters) == 0 {
return nil
}

return filters
}

func (f Filters) Validate() error {
for resourceType, filters := range f {
for _, filter := range filters {
Expand All @@ -38,19 +60,35 @@ func (f Filters) Validate() error {
return nil
}

func (f Filters) Merge(f2 Filters) {
// Append appends the filters from f2 to f
func (f Filters) Append(f2 Filters) {
for resourceType, filter := range f2 {
f[resourceType] = append(f[resourceType], filter...)
}
}

// Merge is an alias of Append for backwards compatibility
// Deprecated: use Append instead
func (f Filters) Merge(f2 Filters) {
f.Append(f2)
}

// Filter is a filter to apply to a resource
type Filter struct {
// Type is the type of filter to apply
Type Type

// Property is the name of the property to filter on
Property string
Type Type
Value string
Invert string

// Value is the value to filter on
Value string

// Invert is a flag to invert the filter
Invert string
}

// Validate checks if the filter is valid
func (f *Filter) Validate() error {
if f.Property == "" && f.Value == "" {
return fmt.Errorf("property and value cannot be empty")
Expand All @@ -59,6 +97,7 @@ func (f *Filter) Validate() error {
return nil
}

// Match checks if the filter matches the given value
func (f *Filter) Match(o string) (bool, error) {
switch f.Type {
case Empty, Exact:
Expand Down Expand Up @@ -134,6 +173,7 @@ func NewExactFilter(value string) Filter {
}
}

// parseDate parses a date from a string, it supports unix timestamps and RFC3339 formatted dates
func parseDate(input string) (time.Time, error) {
if i, err := strconv.ParseInt(input, 10, 64); err == nil {
t := time.Unix(i, 0)
Expand Down
75 changes: 68 additions & 7 deletions pkg/filter/filter_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package filter_test

import (
"os"
"reflect"
"strconv"
"testing"
Expand All @@ -13,7 +14,67 @@ import (
"github.com/ekristen/libnuke/pkg/filter"
)

func TestNewExactFilter(t *testing.T) {
func TestFilter_Nil(t *testing.T) {
f := filter.Filters{}

assert.Nil(t, f.Get("resource1"))
}

func TestFilter_Global(t *testing.T) {
f := filter.Filters{
filter.Global: []filter.Filter{
{Property: "prop1", Type: filter.Exact, Value: "value1"},
},
"resource1": []filter.Filter{
{Property: "prop2", Type: filter.Glob, Value: "value2"},
},
"resource2": []filter.Filter{
{Property: "prop3", Type: filter.Regex, Value: "value3"},
},
}

expected := filter.Filters{
"resource1": []filter.Filter{
{Property: "prop1", Type: filter.Exact, Value: "value1"},
{Property: "prop2", Type: filter.Glob, Value: "value2"},
},
"resource2": []filter.Filter{
{Property: "prop1", Type: filter.Exact, Value: "value1"},
{Property: "prop3", Type: filter.Regex, Value: "value3"},
},
}

assert.Equal(t, expected["resource1"], f.Get("resource1"))
assert.Equal(t, expected["resource2"], f.Get("resource2"))
}

func TestFilter_GlobalYAML(t *testing.T) {
data, err := os.ReadFile("testdata/global.yaml")
assert.NoError(t, err)

config := struct {
Filters filter.Filters `yaml:"filters"`
}{}

err = yaml.Unmarshal(data, &config)
assert.NoError(t, err)

expected := filter.Filters{
"Resource1": []filter.Filter{
{Property: "prop3", Type: filter.Exact, Value: "value3"},
{Property: "prop1", Type: filter.Exact, Value: "value1"},
},
"Resource2": []filter.Filter{
{Property: "prop3", Type: filter.Exact, Value: "value3"},
{Property: "prop2", Type: filter.Exact, Value: "value2"},
},
}

assert.Equal(t, expected["Resource1"], config.Filters.Get("Resource1"))
assert.Equal(t, expected["Resource2"], config.Filters.Get("Resource2"))
}

func TestFilter_NewExactFilter(t *testing.T) {
f := filter.NewExactFilter("testing")

assert.Equal(t, f.Type, filter.Exact)
Expand All @@ -27,7 +88,7 @@ func TestNewExactFilter(t *testing.T) {
assert.False(t, b2)
}

func TestValidation(t *testing.T) {
func TestFilter_Validation(t *testing.T) {
cases := []struct {
name string
yaml string
Expand Down Expand Up @@ -61,7 +122,7 @@ func TestValidation(t *testing.T) {
}
}

func TestUnmarshalFilter(t *testing.T) {
func TestFilter_UnmarshalFilter(t *testing.T) {
past := time.Now().UTC().Add(-24 * time.Hour)
future := time.Now().UTC().Add(24 * time.Hour)
cases := []struct {
Expand Down Expand Up @@ -197,7 +258,7 @@ func TestUnmarshalFilter(t *testing.T) {
}
}

func TestMerge(t *testing.T) {
func TestFilter_Merge(t *testing.T) {
// Create two Filters objects
f1 := filter.Filters{
"resource1": []filter.Filter{
Expand All @@ -213,8 +274,8 @@ func TestMerge(t *testing.T) {
},
}

// Merge the two Filters objects
f1.Merge(f2)
// Append the two Filters objects
f1.Append(f2)

// Create the expected result
expected := filter.Filters{
Expand All @@ -236,7 +297,7 @@ func TestMerge(t *testing.T) {
}
}

func Test_FiltersValidateError(t *testing.T) {
func TestFilter_ValidateError(t *testing.T) {
filters := filter.Filters{
"resource1": []filter.Filter{
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
filters:
__global__:
- name: prop3
- property: prop3
type: exact
value: value3
Resource1:
- name: prop1
- property: prop1
type: exact
value: value1
Resource2:
- name: prop2
- property: prop2
type: exact
value: value2
4 changes: 2 additions & 2 deletions pkg/nuke/nuke.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,8 @@ func (n *Nuke) Filter(item *queue.Item) error {
}
}

itemFilters, ok := n.Filters[item.Type]
if !ok {
itemFilters := n.Filters.Get(item.Type)
if itemFilters == nil {
log.Tracef("no filters found for type: %s", item.Type)
return nil
}
Expand Down
Loading

0 comments on commit 3efe6a0

Please sign in to comment.