Skip to content

Commit

Permalink
Add support for globs in resource names (#21)
Browse files Browse the repository at this point in the history
* Code cleanup. Add more tests.

* Document and improve the code. Fixes #17
  • Loading branch information
patrickdappollonio authored Nov 10, 2021
1 parent 80836b8 commit 8be5fbc
Show file tree
Hide file tree
Showing 16 changed files with 495 additions and 178 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,18 @@ Usage:
kubectl-slice [flags]
Examples:
kubectl-slice -f foo.yaml -o ./ -i Pod,Namespace
kubectl-slice -f foo.yaml -o ./ --include-kind Pod,Namespace
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
Flags:
--dry-run if true, no files are created, but the potentially generated files will be printed as the command output
-e, --exclude-kind strings kinds to exclude in the output (singular, case insensitive); if empty, all Kubernetes object kinds are excluded
--exclude-kind strings resource kind to exclude in the output (singular, case insensitive, glob supported)
--exclude-name strings resource name to exclude in the output (singular, case insensitive, glob supported)
-h, --help help for kubectl-slice
-i, --include-kind strings kinds to include in the output (singular, case insensitive); if empty, all Kubernetes object kinds are included
--include-kind strings resource kind to include in the output (singular, case insensitive, glob supported)
--include-name strings resource name to include in the output (singular, case insensitive, glob supported)
-f, --input-file string the input file used to read the initial macro YAML file; if empty or "-", stdin is used
-o, --output-dir string the output directory used to output the splitted files
-s, --skip-non-k8s if enabled, any YAMLs that don't contain at least an "apiVersion", "kind" and "metadata.name" will be excluded from the split
Expand All @@ -113,10 +118,10 @@ Flags:
* If the rendered file name includes a path separator, subfolders under `--output-dir` will be created.
* If a file already exists in `--output-directory` under this generated file name, their contents will be replaced.
* `--exclude-kind`:
* A case-insensitive, comma-separated list of Kubernetes object kinds to exclude from the output.
* A case-insensitive, comma-separated list of Kubernetes object kinds to exclude from the output. Globs are supported.
* You can also repeat the parameter multiple times to achieve the same effect (`--exclude-kind pod --exclude-kind deployment`)
* `--include-kind`:
* A case-insensitive, comma-separated list of Kubernetes object kinds to include in the output. Any other Kubernetes object kinds will be excluded.
* A case-insensitive, comma-separated list of Kubernetes object kinds to include in the output. Globs are supported. Any other Kubernetes object kinds will be excluded.
* You can also repeat the parameter multiple times to achieve the same effect (`--include-kind pod --include-kind deployment`)
* `--skip-non-k8s`:
* If enabled, any YAMLs that don't contain at least an `apiVersion`, `kind` and `metadata.name` will be excluded from the split
Expand All @@ -127,6 +132,12 @@ Flags:
* If this flag is not present, resources are outputted following the order in which they were found in the YAML file.
* `--stdout`:
* If enabled, no resource is written to disk and all resources are printed to `stdout` instead, useful if you want to pipe the output of `kubectl-slice` to another command or to itself. File names are still generated, but used as reference and prepended at the top of each file in the multi-YAML output. Other than that, the file name template has no effect -- it won't create any subfolders, for example.
* `--include-name`:
* A case-insensitive, comma-separated list of Kubernetes object names to include in the output. Globs are supported. Any other Kubernetes object names will be excluded.
* You can also repeat the parameter multiple times to achieve the same effect (`--include-name foo --include-name bar`)
* `--exclude-name`:
* A case-insensitive, comma-separated list of Kubernetes object names to exclude from the output. Globs are supported.
* You can also repeat the parameter multiple times to achieve the same effect (`--exclude-name foo --exclude-name bar`)

## Why `kubectl-slice`?

Expand All @@ -143,5 +154,4 @@ Pull requests are welcomed! So far, looking for help with the following items, w
* Adding unit tests
* Improving the YAML file-by-file parser, right now it works by buffering line by line
* Adding support to install through `brew`
* Functions to allow accessing labels and annotations on a way different than the dot-notation from Go templates
* [Adding new features marked as `enhancement`](//github.com/patrickdappollonio/kubectl-slice/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
29 changes: 26 additions & 3 deletions app.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bytes"
"fmt"
"os"

Expand All @@ -17,6 +18,26 @@ const (
For documentation, available functions, and more, visit: https://github.com/patrickdappollonio/kubectl-slice.`
)

var examples = []string{
"kubectl-slice -f foo.yaml -o ./ --include-kind Pod,Namespace",
"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",
}

func generateExamples([]string) string {
var s bytes.Buffer
for pos, v := range examples {
s.WriteString(fmt.Sprintf(" %s", v))

if pos != len(examples)-1 {
s.WriteString("\n")
}
}

return s.String()
}

func root() *cobra.Command {
opts := slice.Options{}

Expand All @@ -27,7 +48,7 @@ func root() *cobra.Command {
Version: version,
SilenceUsage: true,
SilenceErrors: true,
Example: `kubectl-slice -f foo.yaml -o ./ -i Pod,Namespace`,
Example: generateExamples(examples),
RunE: func(_ *cobra.Command, args []string) error {
// If no input file has been provided or it's "-", then
// point the app to stdin
Expand All @@ -50,8 +71,10 @@ func root() *cobra.Command {
rootCommand.Flags().StringVarP(&opts.GoTemplate, "template", "t", slice.DefaultTemplateName, "go template used to generate the file name when creating the resource files in the output directory")
rootCommand.Flags().BoolVar(&opts.DryRun, "dry-run", false, "if true, no files are created, but the potentially generated files will be printed as the command output")
rootCommand.Flags().BoolVar(&opts.DebugMode, "debug", false, "enable debug mode")
rootCommand.Flags().StringSliceVarP(&opts.IncludedKinds, "include-kind", "i", nil, "kinds to include in the output (singular, case insensitive); if empty, all Kubernetes object kinds are included")
rootCommand.Flags().StringSliceVarP(&opts.ExcludedKinds, "exclude-kind", "e", nil, "kinds to exclude in the output (singular, case insensitive); if empty, all Kubernetes object kinds are excluded")
rootCommand.Flags().StringSliceVar(&opts.IncludedKinds, "include-kind", nil, "resource kind to include in the output (singular, case insensitive, glob supported)")
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().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")
Expand Down
14 changes: 13 additions & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,23 @@ kubectl-slice --exclude-kind=Deployment,ReplicaSet,DaemonSet
kubectl-slice --exclude-kind=Deployment --exclude-kind=ReplicaSet --exclude-kind=DaemonSet
```

Additionally, you can use `--include-name` and `--exclude-name` to include or exclude resources by Kubernetes name.

```bash
kubectl-slice --exclude-name=my-deployment
```

Globs are also supported, so you can use `--include-name=foo*` to include all resources with names starting with `foo`.

If a YAML resource can't be parsed to detect its name or kind, it will be included by default on any `exclude` flag.

All flags are also case insensitive -- they will be converted to lowercase before applying the glob check.

## Some of the code in my YAML is an entire YAML file commented out, how do I skip it?

By default, `kubectl-slice` will also slice out commented YAML file sections. If you would rather want to ensure only Kubernetes resources are sliced from the original YAML file, then there's two options:

* Use `--include-kind` to only include Kubernetes resources by kind; or
* Use `--include-kind` or `--include-name` to only include Kubernetes resources by kind or name; or
* Use `--skip-non-k8s` to skip any non-Kubernetes resources

`--include-kind` can be used so you control your entire output by specifying only the resources you want. For example, if you want to only slice out `Deployment` resources, you can use `--include-kind=Deployment`.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ require (

require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 h1:NK3O7S5FRD/wj7ORQ5C3Mx1STpyEMuFe+/F0Lakd1Nk=
github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4/go.mod h1:FqD3ES5hx6zpzDainDaHgkTIqrPaI9uX4CVWqYZoQjY=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
Expand Down
13 changes: 7 additions & 6 deletions slice/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ package slice

import "fmt"

type strictModeErr struct {
type strictModeSkipErr struct {
fieldName string
}

func (s *strictModeErr) Error() string {
func (s *strictModeSkipErr) Error() string {
return fmt.Sprintf(
"resource does not have a Kubernetes %q field or the field is invalid or empty", s.fieldName,
)
}

type kindSkipErr struct {
Kind string
type skipErr struct {
name string
kind string
}

func (e *kindSkipErr) Error() string {
return fmt.Sprintf("resource kind %q is configured to be skipped", e.Kind)
func (e *skipErr) Error() string {
return fmt.Sprintf("resource %s %q is configured to be skipped", e.kind, e.name)
}
4 changes: 2 additions & 2 deletions slice/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ package slice
import "testing"

func TestErrorsInterface(t *testing.T) {
var _ error = &strictModeErr{}
var _ error = &kindSkipErr{}
var _ error = &strictModeSkipErr{}
var _ error = &skipErr{}
}
40 changes: 19 additions & 21 deletions slice/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,22 @@ func (s *Split) processSingleFile(file []byte) error {
return nil
}

s.fileCount++
return nil
}

// Add an empty line at the end
file = append(file, []byte("\n\n")...)

// Send it for processing
name, kind, err := s.parseYAMLManifest(file, s.fileCount, s.template)
meta, err := s.parseYAMLManifest(file)
if err != nil {
switch err.(type) {
case *kindSkipErr:
case *skipErr:
s.log.Printf("Skipping file %d: %s", s.fileCount, err.Error())
s.fileCount++
return nil

case *strictModeErr:
case *strictModeSkipErr:
s.log.Printf("Skipping file %d: %s", s.fileCount, err.Error())
s.fileCount++
return nil

default:
Expand All @@ -58,41 +55,39 @@ func (s *Split) processSingleFile(file []byte) error {

existentData, position := []byte(nil), -1
for pos := 0; pos < len(s.filesFound); pos++ {
if s.filesFound[pos].name == name {
if s.filesFound[pos].filename == meta.filename {
existentData = s.filesFound[pos].data
position = pos
break
}
}

if position == -1 {
s.log.Printf("Got nonexistent file. Adding it to the list: %s", name)
s.log.Printf("Got nonexistent file. Adding it to the list: %s", meta.filename)
s.filesFound = append(s.filesFound, yamlFile{
name: name,
kind: kind,
data: file,
filename: meta.filename,
meta: meta.meta,
data: file,
})
} else {
s.log.Printf("Got existent file. Appending to original buffer: %s", name)
s.log.Printf("Got existent file. Appending to original buffer: %s", meta.filename)
existentData = append(existentData, []byte("---\n\n")...)
existentData = append(existentData, file...)
s.filesFound[position] = yamlFile{
name: name,
kind: kind,
data: existentData,
filename: meta.filename,
meta: meta.meta,
data: existentData,
}
}

s.fileCount++
return nil
}

func (s *Split) scan() error {
s.fileCount = 0

// Since we'll be iterating over files that potentially might end up being
// duplicated files, we need to store them somewhere to, later, save them
// to files
s.fileCount = 0
s.filesFound = make([]yamlFile, 0)

// We can totally create a single decoder then decode using that, however,
Expand Down Expand Up @@ -125,6 +120,7 @@ func (s *Split) scan() error {
if err := parseFile(); err != nil {
return err
}
s.fileCount++
break
}

Expand All @@ -138,6 +134,7 @@ func (s *Split) scan() error {
if err := parseFile(); err != nil {
return err
}
s.fileCount++
continue
}

Expand All @@ -164,7 +161,7 @@ func (s *Split) store() error {
for _, v := range s.filesFound {
s.fileCount++

fullpath := filepath.Join(s.opts.OutputDirectory, v.name)
fullpath := filepath.Join(s.opts.OutputDirectory, v.filename)
fileLength := len(v.data)

s.log.Printf("Handling file %q: %d bytes long.", fullpath, fileLength)
Expand All @@ -185,7 +182,7 @@ func (s *Split) store() error {

default:
// do nothing, handling below
if err := writeToFile(fullpath, v.data); err != nil {
if err := s.writeToFile(fullpath, v.data); err != nil {
return err
}

Expand Down Expand Up @@ -226,7 +223,7 @@ func (s *Split) Execute() error {
return s.store()
}

func writeToFile(path string, data []byte) error {
func (s *Split) writeToFile(path string, data []byte) error {
// Since a single Go Template File Name might render different folder prefixes,
// we need to ensure they're all created.
if err := os.MkdirAll(filepath.Dir(path), folderChmod); err != nil {
Expand All @@ -235,6 +232,7 @@ func writeToFile(path string, data []byte) error {

// Open the file as read/write, create the file if it doesn't exist, and if
// it does, truncate it.
s.log.Printf("Opening file path %q", path)
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultChmod)
if err != nil {
return fmt.Errorf("unable to create/open file %q: %w", path, err)
Expand Down
Loading

0 comments on commit 8be5fbc

Please sign in to comment.