diff --git a/cli/options.go b/cli/options.go index c59bc7e..190984e 100644 --- a/cli/options.go +++ b/cli/options.go @@ -17,6 +17,7 @@ type GlobalOptions struct { File string Namespace string Selector string + Exclude string TemplateDirs []string ParamDirs []string PublicKeyDir string @@ -102,6 +103,9 @@ func (o *GlobalOptions) UpdateWithFile(fileFlags map[string]string) { if val, ok := fileFlags["selector"]; ok { o.Selector = val } + if val, ok := fileFlags["exclude"]; ok { + o.Exclude = val + } if val, ok := fileFlags["template-dir"]; ok { o.TemplateDirs = strings.Split(val, ",") } @@ -122,7 +126,7 @@ func (o *GlobalOptions) UpdateWithFile(fileFlags map[string]string) { } } -func (o *GlobalOptions) UpdateWithFlags(verboseFlag bool, debugFlag bool, nonInteractiveFlag bool, ocBinaryFlag string, namespaceFlag string, selectorFlag string, templateDirFlag []string, paramDirFlag []string, publicKeyDirFlag string, privateKeyFlag string, passphraseFlag string, forceFlag bool) { +func (o *GlobalOptions) UpdateWithFlags(verboseFlag bool, debugFlag bool, nonInteractiveFlag bool, ocBinaryFlag string, namespaceFlag string, selectorFlag string, excludeFlag string, templateDirFlag []string, paramDirFlag []string, publicKeyDirFlag string, privateKeyFlag string, passphraseFlag string, forceFlag bool) { if verboseFlag { o.Verbose = true } @@ -147,6 +151,10 @@ func (o *GlobalOptions) UpdateWithFlags(verboseFlag bool, debugFlag bool, nonInt o.Selector = selectorFlag } + if len(excludeFlag) > 0 { + o.Exclude = excludeFlag + } + if len(o.TemplateDirs) == 0 { o.TemplateDirs = templateDirFlag } else if len(templateDirFlag) > 1 || templateDirFlag[0] != "." { diff --git a/commands/export.go b/commands/export.go index b30722b..cba6fe5 100644 --- a/commands/export.go +++ b/commands/export.go @@ -9,7 +9,7 @@ import ( // Export prints an export of targeted resources to STDOUT. func Export(exportOptions *cli.ExportOptions) error { - filter, err := openshift.NewResourceFilter(exportOptions.Resource, exportOptions.Selector) + filter, err := openshift.NewResourceFilter(exportOptions.Resource, exportOptions.Selector, exportOptions.Exclude) if err != nil { return err } diff --git a/commands/status.go b/commands/status.go index 0cdc49e..26f78a4 100644 --- a/commands/status.go +++ b/commands/status.go @@ -49,9 +49,8 @@ func calculateChangeset(compareOptions *cli.CompareOptions) (bool, *openshift.Ch } resource := compareOptions.Resource - selectorFlag := compareOptions.Selector - filter, err := openshift.NewResourceFilter(resource, selectorFlag) + filter, err := openshift.NewResourceFilter(resource, compareOptions.Selector, compareOptions.Exclude) if err != nil { return updateRequired, &openshift.Changeset{}, err } diff --git a/main.go b/main.go index d4f3937..2d7688c 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,10 @@ var ( "selector", "Selector (label query) to filter on", ).Short('l').String() + excludeFlag = app.Flag( + "exclude", + "Exclude kinds, names and labels (comma separated)", + ).Short('e').String() templateDirFlag = app.Flag( "template-dir", "Path to local templates", @@ -224,6 +228,7 @@ func main() { *ocBinaryFlag, *namespaceFlag, *selectorFlag, + *excludeFlag, *templateDirFlag, *paramDirFlag, *publicKeyDirFlag, diff --git a/openshift/change_test.go b/openshift/change_test.go index f4f03a9..7e8ad21 100644 --- a/openshift/change_test.go +++ b/openshift/change_test.go @@ -260,7 +260,7 @@ func TestDiff(t *testing.T) { actualPatches := change.Patches if !reflect.DeepEqual(actualPatches, tt.expectedPatches) { t.Errorf( - "Diff()\n===== expected =====\n%s\n===== actual =====\n%s", + "Diff()\n===== expected =====\n%v\n===== actual =====\n%v", tt.expectedPatches, actualPatches, ) diff --git a/openshift/filter.go b/openshift/filter.go index 341d1b7..2c6bf99 100644 --- a/openshift/filter.go +++ b/openshift/filter.go @@ -24,68 +24,101 @@ var availableKinds = []string{ } type ResourceFilter struct { - Kinds []string - Name string - Label string + Kinds []string + Name string + Label string + ExcludedKinds []string + ExcludedNames []string + ExcludedLabels []string } // NewResourceFilter returns a filter based on kinds and flags. // kindArg might be blank, or a list of kinds (e.g. 'pvc,dc') or // a kind/name combination (e.g. 'dc/foo'). // selectorFlag might be blank or a key and a label, e.g. 'name=foo'. -func NewResourceFilter(kindArg string, selectorFlag string) (*ResourceFilter, error) { +func NewResourceFilter(kindArg string, selectorFlag string, excludeFlag string) (*ResourceFilter, error) { filter := &ResourceFilter{ Kinds: []string{}, Name: "", Label: selectorFlag, } - if len(kindArg) == 0 { - return filter, nil - } + if len(kindArg) > 0 { + kindArg = strings.ToLower(kindArg) + + if strings.Contains(kindArg, "/") { + if strings.Contains(kindArg, ",") { + return nil, errors.New( + "You cannot target more than one resource name", + ) + } + nameParts := strings.Split(kindArg, "/") + filter.Name = KindMapping[nameParts[0]] + "/" + nameParts[1] + return filter, nil + } - kindArg = strings.ToLower(kindArg) + targetedKinds := make(map[string]bool) + unknownKinds := []string{} + kinds := strings.Split(kindArg, ",") + for _, kind := range kinds { + if _, ok := KindMapping[kind]; !ok { + unknownKinds = append(unknownKinds, kind) + } else { + targetedKinds[KindMapping[kind]] = true + } + } - if strings.Contains(kindArg, "/") { - if strings.Contains(kindArg, ",") { - return nil, errors.New( - "You cannot target more than one resource name", + if len(unknownKinds) > 0 { + return nil, fmt.Errorf( + "Unknown resource kinds: %s", + strings.Join(unknownKinds, ","), ) } - nameParts := strings.Split(kindArg, "/") - filter.Name = KindMapping[nameParts[0]] + "/" + nameParts[1] - return filter, nil - } - targetedKinds := make(map[string]bool) - unknownKinds := []string{} - kinds := strings.Split(kindArg, ",") - for _, kind := range kinds { - if _, ok := KindMapping[kind]; !ok { - unknownKinds = append(unknownKinds, kind) - } else { - targetedKinds[KindMapping[kind]] = true + for kind := range targetedKinds { + filter.Kinds = append(filter.Kinds, kind) } - } - if len(unknownKinds) > 0 { - return nil, fmt.Errorf( - "Unknown resource kinds: %s", - strings.Join(unknownKinds, ","), - ) + sort.Strings(filter.Kinds) } - for kind := range targetedKinds { - filter.Kinds = append(filter.Kinds, kind) - } + if len(excludeFlag) > 0 { + unknownKinds := []string{} + excludes := strings.Split(excludeFlag, ",") + for _, v := range excludes { + v = strings.ToLower(v) + if strings.Contains(v, "/") { // Name + nameParts := strings.Split(v, "/") + k := nameParts[0] + if _, ok := KindMapping[k]; !ok { + unknownKinds = append(unknownKinds, k) + } else { + filter.ExcludedNames = append(filter.ExcludedNames, KindMapping[k]+"/"+nameParts[1]) + } + } else if strings.Contains(v, "=") { // Label + filter.ExcludedLabels = append(filter.ExcludedLabels, v) + } else { // Kind + if _, ok := KindMapping[v]; !ok { + unknownKinds = append(unknownKinds, v) + } else { + filter.ExcludedKinds = append(filter.ExcludedKinds, KindMapping[v]) + } + } + } - sort.Strings(filter.Kinds) + if len(unknownKinds) > 0 { + return nil, fmt.Errorf( + "Unknown excluded resource kinds: %s", + strings.Join(unknownKinds, ","), + ) + } + } return filter, nil } func (f *ResourceFilter) String() string { - return fmt.Sprintf("Kind: %s, Name: %s, Label: %s", f.Kinds, f.Name, f.Label) + return fmt.Sprintf("Kinds: %s, Name: %s, Label: %s, ExcludedKinds: %s, ExcludedNames: %s, ExcludedLabels: %s", f.Kinds, f.Name, f.Label, f.ExcludedKinds, f.ExcludedNames, f.ExcludedLabels) } func (f *ResourceFilter) SatisfiedBy(item *ResourceItem) bool { @@ -100,10 +133,27 @@ func (f *ResourceFilter) SatisfiedBy(item *ResourceItem) bool { if len(f.Label) > 0 { labels := strings.Split(f.Label, ",") for _, label := range labels { - labelParts := strings.Split(label, "=") - if _, ok := item.Labels[labelParts[0]]; !ok { + if !item.HasLabel(label) { return false - } else if item.Labels[labelParts[0]].(string) != labelParts[1] { + } + } + } + + if len(f.ExcludedNames) > 0 { + if utils.Includes(f.ExcludedNames, item.FullName()) { + return false + } + } + + if len(f.ExcludedKinds) > 0 { + if utils.Includes(f.ExcludedKinds, item.Kind) { + return false + } + } + + if len(f.ExcludedLabels) > 0 { + for _, el := range f.ExcludedLabels { + if item.HasLabel(el) { return false } } diff --git a/openshift/filter_test.go b/openshift/filter_test.go index 71188e7..743b616 100644 --- a/openshift/filter_test.go +++ b/openshift/filter_test.go @@ -3,10 +3,12 @@ package openshift import ( "reflect" "testing" + + "github.com/ghodss/yaml" ) func TestNewResourceFilter(t *testing.T) { - actual, err := NewResourceFilter("pvc", "") + actual, err := NewResourceFilter("pvc", "", "") expected := &ResourceFilter{ Kinds: []string{"PersistentVolumeClaim"}, Name: "", @@ -16,7 +18,7 @@ func TestNewResourceFilter(t *testing.T) { t.Errorf("Kinds incorrect, got: %v, want: %v.", actual, expected) } - actual, err = NewResourceFilter("pvc,dc", "") + actual, err = NewResourceFilter("pvc,dc", "", "") expected = &ResourceFilter{ Kinds: []string{"DeploymentConfig", "PersistentVolumeClaim"}, Name: "", @@ -26,7 +28,7 @@ func TestNewResourceFilter(t *testing.T) { t.Errorf("Kinds incorrect, got: %v, want: %v.", actual, expected) } - actual, err = NewResourceFilter("pvc,persistentvolumeclaim,PersistentVolumeClaim", "") + actual, err = NewResourceFilter("pvc,persistentvolumeclaim,PersistentVolumeClaim", "", "") expected = &ResourceFilter{ Kinds: []string{"PersistentVolumeClaim"}, Name: "", @@ -36,13 +38,13 @@ func TestNewResourceFilter(t *testing.T) { t.Errorf("Kinds incorrect, got: %v, want: %v.", actual, expected) } - actual, err = NewResourceFilter("pvb", "") + actual, err = NewResourceFilter("pvb", "", "") expected = nil if err == nil { t.Errorf("Expected to detect unknown kind pvb.") } - actual, err = NewResourceFilter("dc/foo", "") + actual, err = NewResourceFilter("dc/foo", "", "") expected = &ResourceFilter{ Kinds: []string{}, Name: "DeploymentConfig/foo", @@ -52,7 +54,7 @@ func TestNewResourceFilter(t *testing.T) { t.Errorf("Kinds incorrect, got: %v, want: %v.", actual, expected) } - actual, err = NewResourceFilter("pvc", "name=foo") + actual, err = NewResourceFilter("pvc", "name=foo", "") expected = &ResourceFilter{ Kinds: []string{"PersistentVolumeClaim"}, Name: "", @@ -62,7 +64,7 @@ func TestNewResourceFilter(t *testing.T) { t.Errorf("Kinds incorrect, got: %v, want: %v.", actual, expected) } - actual, err = NewResourceFilter("pvc,dc", "name=foo") + actual, err = NewResourceFilter("pvc,dc", "name=foo", "") expected = &ResourceFilter{ Kinds: []string{"DeploymentConfig", "PersistentVolumeClaim"}, Name: "", @@ -72,3 +74,121 @@ func TestNewResourceFilter(t *testing.T) { t.Errorf("Kinds incorrect, got: %v, want: %v.", actual, expected) } } + +func TestSatisfiedBy(t *testing.T) { + bc := []byte( + `kind: BuildConfig +metadata: + labels: + app: foo + name: foo`) + tests := map[string]struct { + kindArg string + selectorFlag string + excludeFlag string + config []byte + expected bool + }{ + "item is included when no constraints are specified": { + kindArg: "", + selectorFlag: "", + excludeFlag: "", + config: bc, + expected: true, + }, + "item is included when kind is specified": { + kindArg: "bc", + selectorFlag: "", + excludeFlag: "", + config: bc, + expected: true, + }, + "item is included when name is specified": { + kindArg: "bc/foo", + selectorFlag: "", + excludeFlag: "", + config: bc, + expected: true, + }, + "item is included when label is specified": { + kindArg: "", + selectorFlag: "app=foo", + excludeFlag: "", + config: bc, + expected: true, + }, + "item is excluded when only some other kind is specified": { + kindArg: "is", + selectorFlag: "", + excludeFlag: "", + config: bc, + expected: false, + }, + "item is excluded when kind is excluded": { + kindArg: "", + selectorFlag: "", + excludeFlag: "bc", + config: bc, + expected: false, + }, + "item is excluded when name is excluded": { + kindArg: "", + selectorFlag: "", + excludeFlag: "bc/foo", + config: bc, + expected: false, + }, + "item is excluded when label is excluded": { + kindArg: "", + selectorFlag: "", + excludeFlag: "app=foo", + config: bc, + expected: false, + }, + "item is excluded when multiple excludes are given that match": { + kindArg: "", + selectorFlag: "", + excludeFlag: "app=foo,bc/foo", + config: bc, + expected: false, + }, + "item is excluded when multiple excludes are given that partially match": { + kindArg: "", + selectorFlag: "", + excludeFlag: "app=foobar,bc/foo", + config: bc, + expected: false, + }, + "item is not excluded when multiple excludes are given that do not match": { + kindArg: "", + selectorFlag: "", + excludeFlag: "app=foobar,dc/foo", + config: bc, + expected: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + item, err := makeItem(tc.config) + if err != nil { + t.Fatal(err) + } + filter, err := NewResourceFilter(tc.kindArg, tc.selectorFlag, tc.excludeFlag) + if err != nil { + t.Fatal(err) + } + actual := filter.SatisfiedBy(item) + if actual != tc.expected { + t.Errorf("Got: %+v, want: %+v. Filter is: %+v", actual, tc.expected, filter) + } + }) + } +} + +func makeItem(config []byte) (*ResourceItem, error) { + var f interface{} + yaml.Unmarshal(config, &f) + m := f.(map[string]interface{}) + return NewResourceItem(m, "template") +} diff --git a/openshift/item.go b/openshift/item.go index c84d8ea..71aa8b0 100644 --- a/openshift/item.go +++ b/openshift/item.go @@ -90,6 +90,16 @@ func (i *ResourceItem) FullName() string { return i.Kind + "/" + i.Name } +func (i *ResourceItem) HasLabel(label string) bool { + labelParts := strings.Split(label, "=") + if _, ok := i.Labels[labelParts[0]]; !ok { + return false + } else if i.Labels[labelParts[0]].(string) != labelParts[1] { + return false + } + return true +} + func (templateItem *ResourceItem) ChangesFrom(platformItem *ResourceItem, externallyModifiedPaths []string) ([]*Change, error) { err := templateItem.prepareForComparisonWithPlatformItem(platformItem, externallyModifiedPaths) if err != nil {