diff --git a/cmd/config/configcobra/cmds.go b/cmd/config/configcobra/cmds.go index 5b5fa1729..04de41914 100644 --- a/cmd/config/configcobra/cmds.go +++ b/cmd/config/configcobra/cmds.go @@ -45,6 +45,7 @@ Advanced Documentation Topics: // Export commands publicly for composition var ( + Annotate = commands.AnnotateCommand Cat = commands.CatCommand Count = commands.CountCommand CreateSetter = commands.CreateSetterCommand @@ -91,6 +92,7 @@ func NewConfigCommand(name string) *cobra.Command { name = strings.TrimSpace(name + " config") commands.ExitOnError = true + root.AddCommand(commands.AnnotateCommand(name)) root.AddCommand(commands.GrepCommand(name)) root.AddCommand(commands.TreeCommand(name)) root.AddCommand(commands.CatCommand(name)) diff --git a/cmd/config/docs/commands/annotate.md b/cmd/config/docs/commands/annotate.md new file mode 100644 index 000000000..37299a933 --- /dev/null +++ b/cmd/config/docs/commands/annotate.md @@ -0,0 +1,18 @@ +## annotate + +[Alpha] Set an annotation on Resources. + +### Synopsis + +[Alpha] Set an annotation on Resources. + + DIR: + Path to local directory. + +### Examples + + kustomize config annotate my-dir/ --kv foo=bar + + kustomize config annotate my-dir/ --kv foo=bar --kv a=b + + kustomize config annotate my-dir/ --kv foo=bar --kind Deployment --name foo diff --git a/cmd/config/internal/commands/annotate.go b/cmd/config/internal/commands/annotate.go new file mode 100644 index 000000000..f3a80ff98 --- /dev/null +++ b/cmd/config/internal/commands/annotate.go @@ -0,0 +1,102 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "strings" + + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// NewAnnotateRunner returns a command runner. +func NewAnnotateRunner(parent string) *AnnotateRunner { + r := &AnnotateRunner{} + c := &cobra.Command{ + Use: "annotate [DIR]", + Args: cobra.MaximumNArgs(1), + Short: commands.AnnotateShort, + Long: commands.AnnotateLong, + Example: commands.AnnotateExamples, + RunE: r.runE, + } + fixDocs(parent, c) + r.Command = c + c.Flags().StringVar(&r.Kind, "kind", "", "Resource kind to annotate") + c.Flags().StringVar(&r.ApiVersion, "apiVersion", "", "Resource apiVersion to annotate") + c.Flags().StringVar(&r.Name, "name", "", "Resource name to annotate") + c.Flags().StringVar(&r.Namespace, "namespace", "", "Resource namespace to annotate") + c.Flags().StringSliceVar(&r.Values, "kv", []string{}, "annotation as KEY=VALUE") + return r +} + +func AnnotateCommand(parent string) *cobra.Command { + return NewAnnotateRunner(parent).Command +} + +type AnnotateRunner struct { + Command *cobra.Command + Values []string + Kind string + Name string + ApiVersion string + Namespace string + Path string +} + +func (r *AnnotateRunner) runE(c *cobra.Command, args []string) error { + var input []kio.Reader + var output []kio.Writer + if len(args) == 0 { + rw := &kio.ByteReadWriter{Reader: c.InOrStdin(), Writer: c.OutOrStdout()} + input = []kio.Reader{rw} + output = []kio.Writer{rw} + } else { + rw := &kio.LocalPackageReadWriter{PackagePath: args[0], NoDeleteFiles: true} + input = []kio.Reader{rw} + output = []kio.Writer{rw} + } + return handleError(c, kio.Pipeline{ + Inputs: input, + Filters: []kio.Filter{r}, + Outputs: output}.Execute()) +} + +func (r *AnnotateRunner) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + for i := range nodes { + n := nodes[i] + m, err := n.GetMeta() + if err != nil { + return nil, err + } + if r.Kind != "" && r.Kind != m.Kind { + continue + } + if r.ApiVersion != "" && r.ApiVersion != m.APIVersion { + continue + } + if r.Namespace != "" && r.Namespace != m.Namespace { + continue + } + if r.Name != "" && r.Name != m.Name { + continue + } + + for i := range r.Values { + // split key, value pairs + kv := strings.SplitN(r.Values[i], "=", 2) + if len(kv) != 2 { + return nil, errors.Errorf("must specify --kv as KEY=VALUE: %s", r.Values[i]) + } + if err := n.PipeE(yaml.SetAnnotation(kv[0], kv[1])); err != nil { + return nil, err + } + } + + } + return nodes, nil +} diff --git a/cmd/config/internal/commands/annotate_test.go b/cmd/config/internal/commands/annotate_test.go new file mode 100644 index 000000000..8354cff9b --- /dev/null +++ b/cmd/config/internal/commands/annotate_test.go @@ -0,0 +1,469 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +func TestAnnotateCommand(t *testing.T) { + var tests = []struct { + name string + args []string + expected string + }{ + { + name: "single value", + args: []string{"--kv", "a=b"}, + expected: `kind: Deployment +metadata: + labels: + app: nginx2 + name: foo + annotations: + app: nginx2 + a: 'b' + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f1.yaml' +spec: + replicas: 1 +--- +kind: Service +metadata: + name: foo + annotations: + app: nginx + a: 'b' + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f1.yaml' +spec: + selector: + app: nginx +--- +apiVersion: v1 +kind: Abstraction +metadata: + name: foo + configFn: + container: + image: gcr.io/example/reconciler:v1 + annotations: + config.kubernetes.io/local-config: "true" + a: 'b' + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f2.yaml' + namespace: bar +spec: + replicas: 3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx + name: bar + annotations: + app: nginx + a: 'b' + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f2.yaml' + namespace: foo +spec: + replicas: 3 +`, + }, + { + name: "multi value", + args: []string{"--kv", "a=b", "--kv", "c=d"}, + expected: `kind: Deployment +metadata: + labels: + app: nginx2 + name: foo + annotations: + app: nginx2 + a: 'b' + c: 'd' + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f1.yaml' +spec: + replicas: 1 +--- +kind: Service +metadata: + name: foo + annotations: + app: nginx + a: 'b' + c: 'd' + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f1.yaml' +spec: + selector: + app: nginx +--- +apiVersion: v1 +kind: Abstraction +metadata: + name: foo + configFn: + container: + image: gcr.io/example/reconciler:v1 + annotations: + config.kubernetes.io/local-config: "true" + a: 'b' + c: 'd' + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f2.yaml' + namespace: bar +spec: + replicas: 3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx + name: bar + annotations: + app: nginx + a: 'b' + c: 'd' + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f2.yaml' + namespace: foo +spec: + replicas: 3 +`, + }, + { + name: "filter kind", + args: []string{"--kv", "a=b", "--kind", "Service"}, + expected: `kind: Deployment +metadata: + labels: + app: nginx2 + name: foo + annotations: + app: nginx2 + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f1.yaml' +spec: + replicas: 1 +--- +kind: Service +metadata: + name: foo + annotations: + app: nginx + a: 'b' + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f1.yaml' +spec: + selector: + app: nginx +--- +apiVersion: v1 +kind: Abstraction +metadata: + name: foo + configFn: + container: + image: gcr.io/example/reconciler:v1 + annotations: + config.kubernetes.io/local-config: "true" + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f2.yaml' + namespace: bar +spec: + replicas: 3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx + name: bar + annotations: + app: nginx + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f2.yaml' + namespace: foo +spec: + replicas: 3 +`, + }, + { + name: "filter apiVersion", + args: []string{"--kv", "a=b", "--apiVersion", "v1"}, + expected: `kind: Deployment +metadata: + labels: + app: nginx2 + name: foo + annotations: + app: nginx2 + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f1.yaml' +spec: + replicas: 1 +--- +kind: Service +metadata: + name: foo + annotations: + app: nginx + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f1.yaml' +spec: + selector: + app: nginx +--- +apiVersion: v1 +kind: Abstraction +metadata: + name: foo + configFn: + container: + image: gcr.io/example/reconciler:v1 + annotations: + config.kubernetes.io/local-config: "true" + a: 'b' + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f2.yaml' + namespace: bar +spec: + replicas: 3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx + name: bar + annotations: + app: nginx + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f2.yaml' + namespace: foo +spec: + replicas: 3 +`, + }, + { + name: "filter name", + args: []string{"--kv", "a=b", "--name", "bar"}, + expected: `kind: Deployment +metadata: + labels: + app: nginx2 + name: foo + annotations: + app: nginx2 + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f1.yaml' +spec: + replicas: 1 +--- +kind: Service +metadata: + name: foo + annotations: + app: nginx + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f1.yaml' +spec: + selector: + app: nginx +--- +apiVersion: v1 +kind: Abstraction +metadata: + name: foo + configFn: + container: + image: gcr.io/example/reconciler:v1 + annotations: + config.kubernetes.io/local-config: "true" + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f2.yaml' + namespace: bar +spec: + replicas: 3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx + name: bar + annotations: + app: nginx + a: 'b' + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f2.yaml' + namespace: foo +spec: + replicas: 3 +`, + }, + { + name: "filter namespace", + args: []string{"--kv", "a=b", "--namespace", "bar"}, + expected: `kind: Deployment +metadata: + labels: + app: nginx2 + name: foo + annotations: + app: nginx2 + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f1.yaml' +spec: + replicas: 1 +--- +kind: Service +metadata: + name: foo + annotations: + app: nginx + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f1.yaml' +spec: + selector: + app: nginx +--- +apiVersion: v1 +kind: Abstraction +metadata: + name: foo + configFn: + container: + image: gcr.io/example/reconciler:v1 + annotations: + config.kubernetes.io/local-config: "true" + a: 'b' + config.kubernetes.io/index: '0' + config.kubernetes.io/path: 'f2.yaml' + namespace: bar +spec: + replicas: 3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx + name: bar + annotations: + app: nginx + config.kubernetes.io/index: '1' + config.kubernetes.io/path: 'f2.yaml' + namespace: foo +spec: + replicas: 3 +`, + }, + } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + d := setTestFiles(t) + defer os.RemoveAll(d) + + a := NewAnnotateRunner("") + a.Command.SetArgs(append([]string{d}, tt.args...)) + a.Command.SilenceUsage = true + a.Command.SilenceErrors = true + + err := a.Command.Execute() + if !assert.NoError(t, err) { + t.FailNow() + } + + actual := &bytes.Buffer{} + err = kio.Pipeline{ + Inputs: []kio.Reader{kio.LocalPackageReader{PackagePath: d}}, + Outputs: []kio.Writer{kio.ByteWriter{Writer: actual, KeepReaderAnnotations: true}}, + }.Execute() + if !assert.NoError(t, err) { + t.FailNow() + } + + if !assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(actual.String())) { + t.FailNow() + } + }) + } +} + +func setTestFiles(t *testing.T) string { + d, err := ioutil.TempDir("", "kustomize-cat-test") + if !assert.NoError(t, err) { + t.FailNow() + } + err = ioutil.WriteFile(filepath.Join(d, "f1.yaml"), []byte(` +kind: Deployment +metadata: + labels: + app: nginx2 + name: foo + annotations: + app: nginx2 +spec: + replicas: 1 +--- +kind: Service +metadata: + name: foo + annotations: + app: nginx +spec: + selector: + app: nginx +`), 0600) + if !assert.NoError(t, err) { + defer os.RemoveAll(d) + t.FailNow() + } + err = ioutil.WriteFile(filepath.Join(d, "f2.yaml"), []byte(` +apiVersion: v1 +kind: Abstraction +metadata: + name: foo + configFn: + container: + image: gcr.io/example/reconciler:v1 + annotations: + config.kubernetes.io/local-config: "true" + namespace: bar +spec: + replicas: 3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: nginx + name: bar + annotations: + app: nginx + namespace: foo +spec: + replicas: 3 +`), 0600) + if !assert.NoError(t, err) { + defer os.RemoveAll(d) + t.FailNow() + } + return d +} diff --git a/cmd/config/internal/generateddocs/commands/docs.go b/cmd/config/internal/generateddocs/commands/docs.go index aca3a2d7d..b4cfce577 100644 --- a/cmd/config/internal/generateddocs/commands/docs.go +++ b/cmd/config/internal/generateddocs/commands/docs.go @@ -4,6 +4,20 @@ // Code generated by "mdtogo"; DO NOT EDIT. package commands +var AnnotateShort = `[Alpha] Set an annotation on Resources.` +var AnnotateLong = ` +[Alpha] Set an annotation on Resources. + + DIR: + Path to local directory. +` +var AnnotateExamples = ` + kustomize config annotate my-dir/ --kv foo=bar + + kustomize config annotate my-dir/ --kv foo=bar --kv a=b + + kustomize config annotate my-dir/ --kv foo=bar --kind Deployment --name foo` + var CatShort = `[Alpha] Print Resource Config from a local directory.` var CatLong = ` [Alpha] Print Resource Config from a local directory.