diff --git a/app.go b/app.go index 9b8cfe1..f633aff 100644 --- a/app.go +++ b/app.go @@ -23,6 +23,8 @@ var examples = []string{ "kubectl-slice -f foo.yaml -o ./ --exclude-kind Pod", "kubectl-slice -f foo.yaml -o ./ --exclude-name *-svc", "kubectl-slice -f foo.yaml --exclude-name *-svc --stdout", + "kubectl-slice -f foo.yaml --include Pod/* --stdout", + "kubectl-slice -f foo.yaml --exclude deployment/kube* --stdout", } func generateExamples([]string) string { @@ -80,6 +82,8 @@ func root() *coral.Command { rootCommand.Flags().StringSliceVar(&opts.ExcludedKinds, "exclude-kind", nil, "resource kind to exclude in the output (singular, case insensitive, glob supported)") rootCommand.Flags().StringSliceVar(&opts.IncludedNames, "include-name", nil, "resource name to include in the output (singular, case insensitive, glob supported)") rootCommand.Flags().StringSliceVar(&opts.ExcludedNames, "exclude-name", nil, "resource name to exclude in the output (singular, case insensitive, glob supported)") + rootCommand.Flags().StringSliceVar(&opts.Included, "include", nil, "resource name to include in the output (format /, case insensitive, glob supported)") + rootCommand.Flags().StringSliceVar(&opts.Excluded, "exclude", nil, "resource name to exclude in the output (format /, case insensitive, glob supported)") rootCommand.Flags().BoolVarP(&opts.StrictKubernetes, "skip-non-k8s", "s", false, "if enabled, any YAMLs that don't contain at least an \"apiVersion\", \"kind\" and \"metadata.name\" will be excluded from the split") rootCommand.Flags().BoolVar(&opts.SortByKind, "sort-by-kind", false, "if enabled, resources are sorted by Kind, a la Helm, before saving them to disk") rootCommand.Flags().BoolVar(&opts.OutputToStdout, "stdout", false, "if enabled, no resource is written to disk and all resources are printed to stdout instead") diff --git a/go.sum b/go.sum index 068021d..f72bcf0 100644 --- a/go.sum +++ b/go.sum @@ -17,9 +17,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/slice/execute_test.go b/slice/execute_test.go index 03e6e28..bc8f7e7 100644 --- a/slice/execute_test.go +++ b/slice/execute_test.go @@ -9,11 +9,12 @@ import ( func TestSplit_processSingleFile(t *testing.T) { tests := []struct { - name string - fields Options - fileInput string - wantErr bool - fileOutput *yamlFile + name string + fields Options + fileInput string + wantErr bool + wantFilterErr bool + fileOutput *yamlFile }{ { name: "basic pod", @@ -42,6 +43,26 @@ metadata: fileInput: ` apiVersion: v1 kind: Pod +metadata: + name: nginx-ingress +`, + fileOutput: &yamlFile{ + filename: "pod-nginx-ingress.yaml", + meta: kubeObjectMeta{ + APIVersion: "v1", + Kind: "Pod", + Name: "nginx-ingress", + }, + }, + }, + { + name: "include Pod using include option", + fields: Options{ + Included: []string{"Pod/*"}, + }, + fileInput: ` +apiVersion: v1 +kind: Pod metadata: name: nginx-ingress `, @@ -129,6 +150,20 @@ kind: "Namespace `, wantErr: true, }, + { + name: "invalid excluded", + fields: Options{ + Excluded: []string{"Pod/Namespace/*"}, + }, + wantFilterErr: true, + }, + { + name: "invalid included", + fields: Options{ + Included: []string{"Pod"}, + }, + wantFilterErr: true, + }, } for _, tt := range tests { @@ -139,6 +174,10 @@ kind: "Namespace template: template.Must(template.New("split").Funcs(templateFuncs).Parse(DefaultTemplateName)), } + if err := s.validateFilters(); (err != nil) != tt.wantFilterErr { + t.Errorf("error = %v, wantErr %v", err, tt.wantFilterErr) + } + if err := s.processSingleFile([]byte(tt.fileInput)); (err != nil) != tt.wantErr { t.Errorf("error = %v, wantErr %v", err, tt.wantErr) } diff --git a/slice/process.go b/slice/process.go index d337089..9c0410c 100644 --- a/slice/process.go +++ b/slice/process.go @@ -49,49 +49,35 @@ func (s *Split) parseYAMLManifest(contents []byte) (yamlFile, error) { // Check before handling if we're about to filter resources var ( - hasKindIncluded = len(s.opts.IncludedKinds) > 0 - hasKindExcluded = len(s.opts.ExcludedKinds) > 0 - hasNameIncluded = len(s.opts.IncludedNames) > 0 - hasNameExcluded = len(s.opts.ExcludedNames) > 0 + hasIncluded = len(s.opts.Included) > 0 + hasExcluded = len(s.opts.Excluded) > 0 ) s.log.Printf( - "Applying filters -> kindIncluded: %v; kindExcluded: %v; nameIncluded: %v; nameExcluded: %v", - s.opts.IncludedKinds, s.opts.ExcludedKinds, s.opts.IncludedNames, s.opts.ExcludedNames, - ) + "Applying filters -> Included: %v; Excluded: %v", s.opts.Included, s.opts.Excluded) s.log.Printf("Found K8s meta -> %#v", k8smeta) // Check if we have a Kubernetes kind and we're requesting inclusion or exclusion - if k8smeta.Kind == "" && (hasKindIncluded || hasKindExcluded) { + if k8smeta.Kind == "" && (hasIncluded || hasExcluded) { return yamlFile{}, fmt.Errorf("unable to find Kubernetes \"kind\" field in file number %d", s.fileCount) } // Check if we have a Kubernetes name and we're requesting inclusion or exclusion - if k8smeta.Name == "" && (hasNameIncluded || hasNameExcluded) { + if k8smeta.Name == "" && (hasIncluded || hasExcluded) { return yamlFile{}, fmt.Errorf("unable to find Kubernetes \"metadata.name\" field in file number %d", s.fileCount) } - // We need to check if the file is skipped by kind - if hasKindIncluded || hasKindExcluded || hasNameIncluded || hasNameExcluded { - // If we're working with including only specific kinds, then filter by it - if hasKindIncluded && !inSliceIgnoreCaseGlob(s.opts.IncludedKinds, k8smeta.Kind) { - return yamlFile{}, &skipErr{kind: "kind", name: k8smeta.Kind} - } - - // Otherwise exclude kinds based on the parameter received - if hasKindExcluded && inSliceIgnoreCaseGlob(s.opts.ExcludedKinds, k8smeta.Kind) { - return yamlFile{}, &skipErr{kind: "kind", name: k8smeta.Kind} - } - - // If we're working with including only specific names, then filter by it - if hasNameIncluded && !inSliceIgnoreCaseGlob(s.opts.IncludedNames, k8smeta.Name) { - return yamlFile{}, &skipErr{kind: "name", name: k8smeta.Name} + // We need to check if the file should be skipped + if hasExcluded || hasIncluded { + // If we're working with including only specific resources, then filter by them + if hasIncluded && !inSliceIgnoreCaseGlob(s.opts.Included, fmt.Sprintf("%s/%s", k8smeta.Kind, k8smeta.Name)) { + return yamlFile{}, &skipErr{kind: "kind/name", name: fmt.Sprintf("%s/%s", k8smeta.Kind, k8smeta.Name)} } - // Otherwise exclude names based on the parameter received - if hasNameExcluded && inSliceIgnoreCaseGlob(s.opts.ExcludedNames, k8smeta.Name) { - return yamlFile{}, &skipErr{kind: "name", name: k8smeta.Name} + // Otherwise exclude resources based on the parameter received + if hasExcluded && inSliceIgnoreCaseGlob(s.opts.Excluded, fmt.Sprintf("%s/%s", k8smeta.Kind, k8smeta.Name)) { + return yamlFile{}, &skipErr{kind: "kind/name", name: fmt.Sprintf("%s/%s", k8smeta.Kind, k8smeta.Name)} } } diff --git a/slice/split.go b/slice/split.go index e478ccf..774b5bc 100644 --- a/slice/split.go +++ b/slice/split.go @@ -68,6 +68,8 @@ type Options struct { ExcludedKinds []string IncludedNames []string ExcludedNames []string + Included []string + Excluded []string StrictKubernetes bool // if true, any YAMLs that don't contain at least an "apiVersion", "kind" and "metadata.name" will be excluded SortByKind bool // if true, it will sort the resources by kind diff --git a/slice/validate.go b/slice/validate.go index 0223584..3eb89ec 100644 --- a/slice/validate.go +++ b/slice/validate.go @@ -2,17 +2,13 @@ package slice import ( "fmt" + "regexp" ) -func (s *Split) init() error { - if len(s.opts.IncludedKinds) > 0 && len(s.opts.ExcludedKinds) > 0 { - return fmt.Errorf("cannot specify both included and excluded kinds") - } +var regKN = regexp.MustCompile(`^[^/]+/[^/]+$`) - if len(s.opts.IncludedNames) > 0 && len(s.opts.ExcludedNames) > 0 { - return fmt.Errorf("cannot specify both included and excluded names") - } +func (s *Split) init() error { s.log.Printf("Loading file %s", s.opts.InputFile) buf, err := loadfile(s.opts.InputFile) if err != nil { @@ -35,5 +31,51 @@ func (s *Split) init() error { return err } + return s.validateFilters() +} + +func (s *Split) validateFilters() error { + if len(s.opts.IncludedKinds) > 0 && len(s.opts.ExcludedKinds) > 0 { + return fmt.Errorf("cannot specify both included and excluded kinds") + } + + if len(s.opts.IncludedNames) > 0 && len(s.opts.ExcludedNames) > 0 { + return fmt.Errorf("cannot specify both included and excluded names") + } + + if len(s.opts.Included) > 0 && len(s.opts.Excluded) > 0 { + return fmt.Errorf("cannot specify both included and excluded") + } + + // Merge all filters into excluded and included. + for _, v := range s.opts.IncludedKinds { + s.opts.Included = append(s.opts.Included, fmt.Sprintf("%s/*", v)) + } + + for _, v := range s.opts.ExcludedKinds { + s.opts.Excluded = append(s.opts.Excluded, fmt.Sprintf("%s/*", v)) + } + + for _, v := range s.opts.IncludedNames { + s.opts.Included = append(s.opts.Included, fmt.Sprintf("*/%s", v)) + } + + for _, v := range s.opts.ExcludedNames { + s.opts.Excluded = append(s.opts.Excluded, fmt.Sprintf("*/%s", v)) + } + + // Validate included and excluded filters. + for _, included := range s.opts.Included { + if !regKN.MatchString(included) { + return fmt.Errorf("invalid included pattern %q should be /", included) + } + } + + for _, excluded := range s.opts.Excluded { + if !regKN.MatchString(excluded) { + return fmt.Errorf("invalid excluded pattern %q should be /", excluded) + } + } + return nil }