diff --git a/cmd/root.go b/cmd/root.go index 0f9189e..2cc47ac 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,12 +13,19 @@ import ( "github.com/spf13/cobra" ) -var GitCommit, Version string +var ( + // GitCommit tracks the current git commit + GitCommit string + // Version tracks the current version + Version string +) type root struct { - kind string - name string - filename string + includeKinds []string + includeNames []string + excludeKinds []string + excludeNames []string + filename string } func newRootCommand(args []string) *cobra.Command { @@ -38,8 +45,10 @@ func newRootCommand(args []string) *cobra.Command { }(), } - rootCmd.Flags().StringVarP(&root.kind, "kind", "k", "", "Only include resources of kind") - rootCmd.Flags().StringVarP(&root.name, "name", "n", "", "Only include resources of name") + rootCmd.Flags().StringSliceVarP(&root.includeKinds, "kind", "k", []string{}, "Only include resources of kind") + rootCmd.Flags().StringSliceVarP(&root.includeNames, "name", "n", []string{}, "Only include resources with name") + rootCmd.Flags().StringSliceVarP(&root.excludeKinds, "exclude-kind", "K", []string{}, "Exclude resources of kind") + rootCmd.Flags().StringSliceVarP(&root.excludeNames, "exclude-name", "N", []string{}, "Exclude resources with name") rootCmd.Flags().StringVarP(&root.filename, "filename", "f", "", "Read manifests from file") rootCmd.SetVersionTemplate(`{{.Version}}`) @@ -73,10 +82,15 @@ func (r *root) run() error { } // filter - filtered := filter.New( - filter.KindMatcher([]string{r.kind}), - filter.NameMatcher([]string{r.name}), - ).Filter(results) + filters := []filter.Filter{} + filters = append( + filters, + filter.ExcludeNameFilter(r.excludeNames...), + filter.ExcludeKindFilter(r.excludeKinds...), + filter.NameFilter(r.includeNames...), + filter.KindFilter(r.includeKinds...), + ) + filtered := filter.New(filters...).Filter(results) // print if err := printer.New().Print(filtered); err != nil { @@ -86,6 +100,7 @@ func (r *root) run() error { return nil } +// Execute runs the root command func Execute(args []string) { if err := newRootCommand(args).Execute(); err != nil { log.WithError(err).Error() diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go index 798c76f..17dee38 100644 --- a/pkg/filter/filter.go +++ b/pkg/filter/filter.go @@ -8,23 +8,24 @@ type Filter interface { Filter([]unstructured.Unstructured) []unstructured.Unstructured } -type defaultFilter struct { - matchers []Matcher +type chainedFilter struct { + filters []Filter } type Matcher interface { Match(unstructured.Unstructured) bool + Valid() bool } -func New(matchers ...Matcher) Filter { - return &defaultFilter{matchers} +func New(filters ...Filter) Filter { + return &chainedFilter{filters} } -func (f *defaultFilter) Filter(unstructureds []unstructured.Unstructured) []unstructured.Unstructured { +func (f *chainedFilter) Filter(unstructureds []unstructured.Unstructured) []unstructured.Unstructured { filtered := unstructureds - for _, matcher := range f.matchers { - filtered = filter(filtered, matcher) + for _, filter := range f.filters { + filtered = filter.Filter(filtered) } return filtered @@ -39,3 +40,13 @@ func filter(unstructureds []unstructured.Unstructured, matcher Matcher) []unstru } return filtered } + +func excludeFilter(unstructureds []unstructured.Unstructured, matcher Matcher) []unstructured.Unstructured { + filtered := []unstructured.Unstructured{} + for _, u := range unstructureds { + if !matcher.Match(u) { + filtered = append(filtered, u) + } + } + return filtered +} diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go index 2c7a91d..0d1b39e 100644 --- a/pkg/filter/filter_test.go +++ b/pkg/filter/filter_test.go @@ -6,23 +6,112 @@ import ( "github.com/ryane/kfilt/pkg/filter" ) +type includeNames []string +type includeKinds []string +type excludeNames []string +type excludeKinds []string +type expectNames []string + func TestFilter(t *testing.T) { tests := []struct { - names []string - kinds []string - expectNames []string + includeKinds includeKinds + includeNames includeNames + excludeKinds excludeKinds + excludeNames excludeNames + expectNames []string }{ - {[]string{"test-sa"}, []string{"Deployment"}, []string{}}, - {[]string{"test-sa"}, []string{}, []string{"test-sa"}}, - {[]string{"test-sa"}, []string{""}, []string{"test-sa"}}, - {[]string{}, []string{"ServiceAccount"}, []string{"test-sa", "test-sa-2"}}, - {[]string{"test-pod", "test-deployment"}, []string{"ServiceAccount"}, []string{}}, + { + includeKinds{"Deployment", "Pod"}, + includeNames{}, + excludeKinds{}, + excludeNames{}, + expectNames{"test-pod", "test-deployment"}, + }, + { + includeKinds{"Deployment"}, + includeNames{"test-sa"}, + excludeKinds{}, + excludeNames{}, + expectNames{}, + }, + { + includeKinds{}, + includeNames{"test-sa"}, + excludeKinds{}, + excludeNames{}, + expectNames{"test-sa"}, + }, + { + includeKinds{}, + includeNames{"test-sa", "test-sa-2"}, + excludeKinds{}, + excludeNames{}, + expectNames{"test-sa", "test-sa-2"}, + }, + { + includeKinds{""}, + includeNames{"test-sa"}, + excludeKinds{}, + excludeNames{}, + expectNames{"test-sa"}, + }, + { + includeKinds{"ServiceAccount"}, + includeNames{}, + excludeKinds{}, + excludeNames{}, + expectNames{"test-sa", "test-sa-2"}, + }, + { + includeKinds{"ServiceAccount"}, + includeNames{"test-pod", "test-deployment"}, + excludeKinds{}, + excludeNames{}, + expectNames{}, + }, + { + includeKinds{"ServiceAccount"}, + includeNames{}, + excludeKinds{}, + excludeNames{"test-sa"}, + expectNames{"test-sa-2"}, + }, + { + includeKinds{}, + includeNames{}, + excludeKinds{"ServiceAccount"}, + excludeNames{}, + expectNames{"test-pod", "test-deployment"}, + }, + { + includeKinds{}, + includeNames{}, + excludeKinds{"ServiceAccount", "Deployment"}, + excludeNames{}, + expectNames{"test-pod"}, + }, + { + includeKinds{"ServiceAccount", "Deployment"}, + includeNames{}, + excludeKinds{"ServiceAccount"}, + excludeNames{}, + expectNames{"test-deployment"}, + }, + { + includeKinds{}, + includeNames{"test-sa", "test-sa-2"}, + excludeKinds{"ServiceAccount"}, + excludeNames{}, + expectNames{}, + }, } for _, test := range tests { f := filter.New( - filter.KindMatcher(test.kinds), - filter.NameMatcher(test.names), + filter.ExcludeNameFilter(test.excludeNames...), + filter.ExcludeKindFilter(test.excludeKinds...), + filter.NameFilter(test.includeNames...), + filter.KindFilter(test.includeKinds...), ) results := f.Filter(input) diff --git a/pkg/filter/kind_filter.go b/pkg/filter/kind_filter.go index 734f75b..1939442 100644 --- a/pkg/filter/kind_filter.go +++ b/pkg/filter/kind_filter.go @@ -6,6 +6,36 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +type kindFilter struct { + matcher Matcher +} + +func (f *kindFilter) Filter(unstructureds []unstructured.Unstructured) []unstructured.Unstructured { + if f.matcher.Valid() { + return filter(unstructureds, f.matcher) + } + return unstructureds +} + +func KindFilter(kinds ...string) Filter { + return &kindFilter{KindMatcher(kinds)} +} + +type excludeKindFilter struct { + matcher Matcher +} + +func (f *excludeKindFilter) Filter(unstructureds []unstructured.Unstructured) []unstructured.Unstructured { + if f.matcher.Valid() { + return excludeFilter(unstructureds, f.matcher) + } + return unstructureds +} + +func ExcludeKindFilter(kinds ...string) Filter { + return &excludeKindFilter{KindMatcher(kinds)} +} + type kindMatcher struct { kinds []string } @@ -14,12 +44,11 @@ func KindMatcher(kinds []string) Matcher { return &kindMatcher{validKinds(kinds)} } -func (f *kindMatcher) Match(u unstructured.Unstructured) bool { - // no kinds specified so we just return a match - if len(f.kinds) == 0 { - return true - } +func (f *kindMatcher) Valid() bool { + return len(f.kinds) > 0 +} +func (f *kindMatcher) Match(u unstructured.Unstructured) bool { for _, kind := range f.kinds { if strings.EqualFold(kind, u.GetKind()) { return true diff --git a/pkg/filter/kind_filter_test.go b/pkg/filter/kind_filter_test.go index c6cc4a3..c0eff26 100644 --- a/pkg/filter/kind_filter_test.go +++ b/pkg/filter/kind_filter_test.go @@ -4,25 +4,36 @@ import ( "testing" "github.com/ryane/kfilt/pkg/filter" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -func TestKindFilterNil(t *testing.T) { - matcher := filter.KindMatcher(nil) - - for _, u := range input { - if !matcher.Match(u) { - t.Errorf("expected match for %s", u.GetKind()) - t.FailNow() - } +func TestKindMatcher(t *testing.T) { + var u = unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": "ServiceAccount", + "metadata": map[string]interface{}{ + "name": "test-sa", + }, + }, } -} -func TestKindFilterEmptyVals(t *testing.T) { - matcher := filter.KindMatcher([]string{" ", ""}) + tests := []struct { + kinds []string + expected bool + }{ + {[]string{"ServiceAccount"}, true}, + {[]string{"serviceaccount"}, true}, + {[]string{""}, false}, + {[]string{" "}, false}, + {[]string{}, false}, + {[]string{"pod"}, false}, + {[]string{"Deployment", "ServiceAccount"}, true}, + } - for _, u := range input { - if !matcher.Match(u) { - t.Errorf("expected match for %s", u.GetKind()) + for _, test := range tests { + matcher := filter.KindMatcher(test.kinds) + if result := matcher.Match(u); result != test.expected { + t.Errorf("expected %v for %v, got %v", test.expected, test.kinds, result) t.FailNow() } } @@ -34,6 +45,7 @@ func TestKindFilter(t *testing.T) { expectNames []string }{ {[]string{"ServiceAccount"}, []string{"test-sa", "test-sa-2"}}, + {[]string{"ServiceAccount", "Deployment"}, []string{"test-sa", "test-sa-2", "test-deployment"}}, {[]string{"Deployment"}, []string{"test-deployment"}}, {[]string{"deployment"}, []string{"test-deployment"}}, {[]string{"Pod"}, []string{"test-pod"}}, @@ -41,7 +53,40 @@ func TestKindFilter(t *testing.T) { } for _, test := range tests { - f := filter.New(filter.KindMatcher(test.kinds)) + f := filter.KindFilter(test.kinds...) + + results := f.Filter(input) + if len(results) != len(test.expectNames) { + t.Errorf("expected %d results, got %d", len(test.expectNames), len(results)) + t.FailNow() + } + + for i, u := range results { + name := u.GetName() + if name != test.expectNames[i] { + t.Errorf("expected %s, got %s", test.expectNames[i], name) + t.FailNow() + } + } + } +} + +func TestExcludeKindFilter(t *testing.T) { + tests := []struct { + kinds []string + expectNames []string + }{ + {[]string{}, []string{"test-sa", "test-sa-2", "test-pod", "test-deployment"}}, + {[]string{"", " "}, []string{"test-sa", "test-sa-2", "test-pod", "test-deployment"}}, + {[]string{"ServiceAccount"}, []string{"test-pod", "test-deployment"}}, + {[]string{"Deployment"}, []string{"test-sa", "test-sa-2", "test-pod"}}, + {[]string{"deployment"}, []string{"test-sa", "test-sa-2", "test-pod"}}, + {[]string{"Pod"}, []string{"test-sa", "test-sa-2", "test-deployment"}}, + {[]string{"ServiceAccount", "Deployment"}, []string{"test-pod"}}, + } + + for _, test := range tests { + f := filter.ExcludeKindFilter(test.kinds...) results := f.Filter(input) if len(results) != len(test.expectNames) { diff --git a/pkg/filter/name_filter.go b/pkg/filter/name_filter.go index 013a40d..95f5ad1 100644 --- a/pkg/filter/name_filter.go +++ b/pkg/filter/name_filter.go @@ -6,6 +6,36 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +type nameFilter struct { + matcher Matcher +} + +func (f *nameFilter) Filter(unstructureds []unstructured.Unstructured) []unstructured.Unstructured { + if f.matcher.Valid() { + return filter(unstructureds, f.matcher) + } + return unstructureds +} + +func NameFilter(names ...string) Filter { + return &nameFilter{NameMatcher(names)} +} + +type excludeNameFilter struct { + matcher Matcher +} + +func (f *excludeNameFilter) Filter(unstructureds []unstructured.Unstructured) []unstructured.Unstructured { + if f.matcher.Valid() { + return excludeFilter(unstructureds, f.matcher) + } + return unstructureds +} + +func ExcludeNameFilter(names ...string) Filter { + return &excludeNameFilter{NameMatcher(names)} +} + type nameMatcher struct { names []string } @@ -14,12 +44,11 @@ func NameMatcher(names []string) Matcher { return &nameMatcher{validNames(names)} } -func (f *nameMatcher) Match(u unstructured.Unstructured) bool { - // no names specified so we just return a match - if len(f.names) == 0 { - return true - } +func (f *nameMatcher) Valid() bool { + return len(f.names) > 0 +} +func (f *nameMatcher) Match(u unstructured.Unstructured) bool { for _, name := range f.names { if strings.EqualFold(name, u.GetName()) { return true diff --git a/pkg/filter/name_filter_test.go b/pkg/filter/name_filter_test.go index 23f7d03..de72202 100644 --- a/pkg/filter/name_filter_test.go +++ b/pkg/filter/name_filter_test.go @@ -4,25 +4,36 @@ import ( "testing" "github.com/ryane/kfilt/pkg/filter" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -func TestNameFilterNil(t *testing.T) { - matcher := filter.NameMatcher(nil) - - for _, u := range input { - if !matcher.Match(u) { - t.Errorf("expected match for %s", u.GetName()) - t.FailNow() - } +func TestNameMatcher(t *testing.T) { + var u = unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": "ServiceAccount", + "metadata": map[string]interface{}{ + "name": "test-sa", + }, + }, } -} -func TestNameFilterEmptyVals(t *testing.T) { - matcher := filter.NameMatcher([]string{" ", ""}) + tests := []struct { + names []string + expected bool + }{ + {[]string{"test-sa"}, true}, + {[]string{"TEST-SA"}, true}, + {[]string{""}, false}, + {[]string{" "}, false}, + {[]string{}, false}, + {[]string{"test-pod"}, false}, + {[]string{"test-deployment", "test-sa"}, true}, + } - for _, u := range input { - if !matcher.Match(u) { - t.Errorf("expected match for %s", u.GetName()) + for _, test := range tests { + matcher := filter.NameMatcher(test.names) + if result := matcher.Match(u); result != test.expected { + t.Errorf("expected %v for %v, got %v", test.expected, test.names, result) t.FailNow() } } @@ -41,7 +52,40 @@ func TestNameFilter(t *testing.T) { } for _, test := range tests { - f := filter.New(filter.NameMatcher(test.names)) + f := filter.NameFilter(test.names...) + + results := f.Filter(input) + if len(results) != len(test.expectNames) { + t.Errorf("expected %d results, got %d", len(test.expectNames), len(results)) + t.FailNow() + } + + for i, u := range results { + name := u.GetName() + if name != test.expectNames[i] { + t.Errorf("expected %s, got %s", test.expectNames[i], name) + t.FailNow() + } + } + } +} + +func TestExcludeNameFilter(t *testing.T) { + tests := []struct { + names []string + expectNames []string + }{ + {[]string{}, []string{"test-sa", "test-sa-2", "test-pod", "test-deployment"}}, + {[]string{"", " "}, []string{"test-sa", "test-sa-2", "test-pod", "test-deployment"}}, + {[]string{"test-sa"}, []string{"test-sa-2", "test-pod", "test-deployment"}}, + {[]string{"TEST-sa"}, []string{"test-sa-2", "test-pod", "test-deployment"}}, + {[]string{"test-deployment"}, []string{"test-sa", "test-sa-2", "test-pod"}}, + {[]string{"test-pod"}, []string{"test-sa", "test-sa-2", "test-deployment"}}, + {[]string{"test-sa", "test-deployment", "test-sa-2"}, []string{"test-pod"}}, + } + + for _, test := range tests { + f := filter.ExcludeNameFilter(test.names...) results := f.Filter(input) if len(results) != len(test.expectNames) {