diff --git a/cmd/config/configcobra/cmds.go b/cmd/config/configcobra/cmds.go index 04de41914..82754e74d 100644 --- a/cmd/config/configcobra/cmds.go +++ b/cmd/config/configcobra/cmds.go @@ -45,22 +45,23 @@ Advanced Documentation Topics: // Export commands publicly for composition var ( - Annotate = commands.AnnotateCommand - Cat = commands.CatCommand - Count = commands.CountCommand - CreateSetter = commands.CreateSetterCommand - Fmt = commands.FmtCommand - Grep = commands.GrepCommand - ListSetters = commands.ListSettersCommand - Merge = commands.MergeCommand - Merge3 = commands.Merge3Command - RunFn = commands.RunFnCommand - Set = commands.SetCommand - Sink = commands.SinkCommand - Source = commands.SourceCommand - Tree = commands.TreeCommand - Wrap = commands.WrapCommand - XArgs = commands.XArgsCommand + Annotate = commands.AnnotateCommand + Cat = commands.CatCommand + Count = commands.CountCommand + CreateSetter = commands.CreateSetterCommand + CreateSubstitution = commands.CreateSubstitutionCommand + Fmt = commands.FmtCommand + Grep = commands.GrepCommand + ListSetters = commands.ListSettersCommand + Merge = commands.MergeCommand + Merge3 = commands.Merge3Command + RunFn = commands.RunFnCommand + Set = commands.SetCommand + Sink = commands.SinkCommand + Source = commands.SourceCommand + Tree = commands.TreeCommand + Wrap = commands.WrapCommand + XArgs = commands.XArgsCommand StackOnError = &commands.StackOnError ExitOnError = &commands.ExitOnError @@ -107,6 +108,7 @@ func NewConfigCommand(name string) *cobra.Command { root.AddCommand(commands.SetCommand(name)) root.AddCommand(commands.ListSettersCommand(name)) root.AddCommand(commands.CreateSetterCommand(name)) + root.AddCommand(commands.CreateSubstitutionCommand(name)) root.AddCommand(commands.SinkCommand(name)) root.AddCommand(commands.SourceCommand(name)) diff --git a/cmd/config/internal/commands/cmdcreatesetter.go b/cmd/config/internal/commands/cmdcreatesetter.go index 010606c8e..54725c54c 100644 --- a/cmd/config/internal/commands/cmdcreatesetter.go +++ b/cmd/config/internal/commands/cmdcreatesetter.go @@ -8,6 +8,7 @@ import ( "sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/setters" + "sigs.k8s.io/kustomize/kyaml/setters2/settersutil" ) // NewCreateSetterRunner returns a command runner. @@ -23,21 +24,28 @@ func NewCreateSetterRunner(parent string) *CreateSetterRunner { RunE: r.runE, } set.Flags().StringVar(&r.Set.SetPartialField.SetBy, "set-by", "", - "set the setBy annotation.") + "record who the field was default by.") set.Flags().StringVar(&r.Set.SetPartialField.Description, "description", "", - "set the description of the field value.") + "record a description for the current setter value.") set.Flags().StringVar(&r.Set.SetPartialField.Field, "field", "", - "name of the field to set -- e.g. --field port") + "name of the field to set -- e.g. --field port. defaults to all fields match"+ + "VALUE. maybe be the field name, field path, or partial field path (suffix)") set.Flags().StringVar(&r.Set.ResourceMeta.Name, "name", "", "name of the Resource on which to create the setter.") + set.Flags().MarkHidden("name") set.Flags().StringVar(&r.Set.ResourceMeta.Kind, "kind", "", "kind of the Resource on which to create the setter.") + set.Flags().MarkHidden("kind") set.Flags().StringVar(&r.Set.SetPartialField.Type, "type", "", "valid OpenAPI field type -- e.g. integer,boolean,string.") + set.Flags().MarkHidden("type") set.Flags().BoolVar(&r.Set.SetPartialField.Partial, "partial", false, "create a partial setter for only part of the field value.") + set.Flags().MarkHidden("partial") + set.Flags().StringVar(&setterVersion, "version", "", + "use this version of the setter format") + set.Flags().MarkHidden("version") fixDocs(parent, set) - set.MarkFlagRequired("type") r.Command = set return r } @@ -47,8 +55,10 @@ func CreateSetterCommand(parent string) *cobra.Command { } type CreateSetterRunner struct { - Command *cobra.Command - Set setters.CreateSetter + Command *cobra.Command + Set setters.CreateSetter + CreateSetter settersutil.SetterCreator + OpenAPIFile string } func (r *CreateSetterRunner) runE(c *cobra.Command, args []string) error { @@ -56,12 +66,40 @@ func (r *CreateSetterRunner) runE(c *cobra.Command, args []string) error { } func (r *CreateSetterRunner) preRunE(c *cobra.Command, args []string) error { + var err error r.Set.SetPartialField.Setter.Name = args[1] r.Set.SetPartialField.Setter.Value = args[2] + r.CreateSetter.Name = args[1] + r.CreateSetter.FieldValue = args[2] + r.CreateSetter.FieldName, err = c.Flags().GetString("field") + if err != nil { + return err + } + + if setterVersion == "" { + if len(args) < 3 { + setterVersion = "v1" + } else if err := initSetterVersion(c, args); err != nil { + return err + } + } + if setterVersion == "v2" { + var err error + r.OpenAPIFile, err = GetOpenAPIFile(args) + r.CreateSetter.Description = r.Set.SetPartialField.Description + r.CreateSetter.SetBy = r.Set.SetPartialField.SetBy + if err != nil { + return err + } + } return nil } func (r *CreateSetterRunner) set(c *cobra.Command, args []string) error { + if setterVersion == "v2" { + return r.CreateSetter.Create(r.OpenAPIFile, args[0]) + } + rw := &kio.LocalPackageReadWriter{PackagePath: args[0]} err := kio.Pipeline{ Inputs: []kio.Reader{rw}, diff --git a/cmd/config/internal/commands/cmdcreatesetter_test.go b/cmd/config/internal/commands/cmdcreatesetter_test.go new file mode 100644 index 000000000..00cd1bb02 --- /dev/null +++ b/cmd/config/internal/commands/cmdcreatesetter_test.go @@ -0,0 +1,128 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package commands_test + +import ( + "bytes" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/cmd/config/internal/commands" + "sigs.k8s.io/kustomize/kyaml/openapi" +) + +func TestCreateSetterCommand(t *testing.T) { + var tests = []struct { + name string + input string + args []string + out string + expectedOpenAPI string + expectedResources string + }{ + { + name: "add replicas", + args: []string{"replicas", "3", "--description", "hello world", "--set-by", "me"}, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + `, + expectedOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: hello world + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me + `, + expectedResources: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 # {"$ref":"#/definitions/io.k8s.cli.setters.replicas"} + `, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + // reset the openAPI afterward + openapi.ResetOpenAPI() + defer openapi.ResetOpenAPI() + + f, err := ioutil.TempFile("", "k8s-cli-") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(f.Name()) + err = ioutil.WriteFile(f.Name(), []byte(` +apiVersion: v1alpha1 +kind: Example +`), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + commands.GetOpenAPIFile = func(args []string) (s string, err error) { + return f.Name(), nil + } + + r, err := ioutil.TempFile("", "k8s-cli-*.yaml") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(r.Name()) + err = ioutil.WriteFile(r.Name(), []byte(test.input), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + + runner := commands.NewCreateSetterRunner("") + out := &bytes.Buffer{} + runner.Command.SetOut(out) + runner.Command.SetArgs(append([]string{r.Name()}, test.args...)) + err = runner.Command.Execute() + if !assert.NoError(t, err) { + t.FailNow() + } + + if !assert.Equal(t, test.out, out.String()) { + t.FailNow() + } + + actualResources, err := ioutil.ReadFile(r.Name()) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, + strings.TrimSpace(test.expectedResources), + strings.TrimSpace(string(actualResources))) { + t.FailNow() + } + + actualOpenAPI, err := ioutil.ReadFile(f.Name()) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, + strings.TrimSpace(test.expectedOpenAPI), + strings.TrimSpace(string(actualOpenAPI))) { + t.FailNow() + } + }) + } +} diff --git a/cmd/config/internal/commands/cmdcreatesubstitution.go b/cmd/config/internal/commands/cmdcreatesubstitution.go new file mode 100644 index 000000000..c7d5e63c6 --- /dev/null +++ b/cmd/config/internal/commands/cmdcreatesubstitution.go @@ -0,0 +1,80 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "strings" + + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/setters2" + "sigs.k8s.io/kustomize/kyaml/setters2/settersutil" +) + +// NewCreateSubstitutionRunner returns a command runner. +func NewCreateSubstitutionRunner(parent string) *CreateSubstitutionRunner { + r := &CreateSubstitutionRunner{} + cs := &cobra.Command{ + Use: "create-subst DIR NAME VALUE", + Args: cobra.ExactArgs(3), + PreRunE: r.preRunE, + RunE: r.runE, + } + cs.Flags().StringVar(&r.CreateSubstitution.FieldName, "field", "", + "name of the field to set -- e.g. --field port") + cs.Flags().StringVar(&r.CreateSubstitution.Pattern, "pattern", "", + "substitution pattern") + cs.Flags().StringSliceVar(&r.Values, "value", []string{""}, + "substitution values for the pattern. format is PATTERN_MARKER=SETTER_NAME"+ + "where PATTERN_MARKER is the pattern substring to replace, and SETTER_NAME is the"+ + "setter from which to take the replacement value.") + _ = cs.MarkFlagRequired("pattern") + fixDocs(parent, cs) + r.Command = cs + return r +} + +func CreateSubstitutionCommand(parent string) *cobra.Command { + return NewCreateSubstitutionRunner(parent).Command +} + +type CreateSubstitutionRunner struct { + Command *cobra.Command + CreateSubstitution settersutil.SubstitutionCreator + OpenAPIFile string + Values []string +} + +func (r *CreateSubstitutionRunner) runE(c *cobra.Command, args []string) error { + return handleError(c, r.CreateSubstitution.Create(r.OpenAPIFile, args[0])) +} + +func (r *CreateSubstitutionRunner) preRunE(c *cobra.Command, args []string) error { + var err error + r.CreateSubstitution.Name = args[1] + r.CreateSubstitution.FieldValue = args[2] + if err != nil { + return err + } + + r.OpenAPIFile, err = GetOpenAPIFile(args) + if err != nil { + return err + } + + // parse the marker values + for i := range r.Values { + parts := strings.SplitN(r.Values[i], "=", 2) + if len(parts) < 2 { + return errors.Errorf("values must be specified as PATTERN_MARKER=SETTER_NAME") + } + ref := setters2.DefinitionsPrefix + setters2.SetterDefinitionPrefix + parts[1] + r.CreateSubstitution.Values = append( + r.CreateSubstitution.Values, + setters2.Value{Marker: parts[0], Ref: ref}, + ) + } + + return nil +} diff --git a/cmd/config/internal/commands/cmdcreatesubstitution_test.go b/cmd/config/internal/commands/cmdcreatesubstitution_test.go new file mode 100644 index 000000000..b4e4940ff --- /dev/null +++ b/cmd/config/internal/commands/cmdcreatesubstitution_test.go @@ -0,0 +1,237 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package commands_test + +import ( + "bytes" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/cmd/config/internal/commands" + "sigs.k8s.io/kustomize/kyaml/openapi" +) + +func TestCreateSubstitutionCommand(t *testing.T) { + var tests = []struct { + name string + inputOpenAPI string + input string + args []string + out string + expectedOpenAPI string + expectedResources string + }{ + { + name: "substitution replicas", + args: []string{ + "image", "nginx:1.7.9", "--pattern", "IMAGE:TAG", + "--value", "IMAGE=image", "--value", "TAG=tag"}, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 + - name: sidecar + image: sidecar:1.7.9 + `, + inputOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.image: + x-k8s-cli: + setter: + name: image + value: "nginx" + io.k8s.cli.setters.tag: + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + `, + expectedOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.image: + x-k8s-cli: + setter: + name: image + value: "nginx" + io.k8s.cli.setters.tag: + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + expectedResources: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref":"#/definitions/io.k8s.cli.substitutions.image"} + - name: sidecar + image: sidecar:1.7.9 + `, + }, + { + name: "substitution and create setters 1", + args: []string{ + "image", "something/nginx:1.7.9", "--pattern", "something/IMAGE:TAG", + "--value", "IMAGE=image", "--value", "TAG=tag"}, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + template: + spec: + containers: + - name: nginx + image: something/nginx:1.7.9 + - name: sidecar + image: sidecar:1.7.9 + `, + inputOpenAPI: ` +apiVersion: v1alpha1 +kind: Example + `, + expectedOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.image: + x-k8s-cli: + setter: + name: image + value: nginx + io.k8s.cli.setters.tag: + x-k8s-cli: + setter: + name: tag + value: 1.7.9 + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: something/IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + expectedResources: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + template: + spec: + containers: + - name: nginx + image: something/nginx:1.7.9 # {"$ref":"#/definitions/io.k8s.cli.substitutions.image"} + - name: sidecar + image: sidecar:1.7.9 + `, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + // reset the openAPI afterward + openapi.ResetOpenAPI() + defer openapi.ResetOpenAPI() + + f, err := ioutil.TempFile("", "k8s-cli-") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(f.Name()) + err = ioutil.WriteFile(f.Name(), []byte(test.inputOpenAPI), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + commands.GetOpenAPIFile = func(args []string) (s string, err error) { + return f.Name(), nil + } + + r, err := ioutil.TempFile("", "k8s-cli-*.yaml") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(r.Name()) + err = ioutil.WriteFile(r.Name(), []byte(test.input), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + + runner := commands.NewCreateSubstitutionRunner("") + out := &bytes.Buffer{} + runner.Command.SetOut(out) + runner.Command.SetArgs(append([]string{r.Name()}, test.args...)) + err = runner.Command.Execute() + if !assert.NoError(t, err) { + t.FailNow() + } + + if !assert.Equal(t, test.out, out.String()) { + t.FailNow() + } + + actualResources, err := ioutil.ReadFile(r.Name()) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, + strings.TrimSpace(test.expectedResources), + strings.TrimSpace(string(actualResources))) { + t.FailNow() + } + + actualOpenAPI, err := ioutil.ReadFile(f.Name()) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, + strings.TrimSpace(test.expectedOpenAPI), + strings.TrimSpace(string(actualOpenAPI))) { + t.FailNow() + } + }) + } +} diff --git a/cmd/config/internal/commands/cmdlistsetters.go b/cmd/config/internal/commands/cmdlistsetters.go index 5cf5afea6..c4a8e29b5 100644 --- a/cmd/config/internal/commands/cmdlistsetters.go +++ b/cmd/config/internal/commands/cmdlistsetters.go @@ -4,9 +4,15 @@ package commands import ( + "fmt" + "io" + "os" + + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" "sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands" "sigs.k8s.io/kustomize/kyaml/setters" + "sigs.k8s.io/kustomize/kyaml/setters2" ) // NewListSettersRunner returns a command runner. @@ -33,15 +39,57 @@ func ListSettersCommand(parent string) *cobra.Command { type ListSettersRunner struct { Command *cobra.Command Lookup setters.LookupSetters + List setters2.List } func (r *ListSettersRunner) preRunE(c *cobra.Command, args []string) error { if len(args) > 1 { r.Lookup.Name = args[1] + r.List.Name = args[1] } + + initSetterVersion(c, args) return nil } func (r *ListSettersRunner) runE(c *cobra.Command, args []string) error { + if setterVersion == "v2" { + // use setters v2 + path, err := GetOpenAPIFile(args) + if err != nil { + return err + } + if err := r.List.List(path, args[0]); err != nil { + return err + } + table := newTable(c.OutOrStdout()) + table.SetHeader([]string{"NAME", "VALUE", "SET BY", "DESCRIPTION", "COUNT"}) + for i := range r.List.Setters { + s := r.List.Setters[i] + table.Append([]string{ + s.Name, s.Value, s.SetBy, s.Description, fmt.Sprintf("%d", s.Count)}) + } + table.Render() + + if len(r.List.Setters) == 0 { + // exit non-0 if no matching setters are found + if ExitOnError { + os.Exit(1) + } + } + return nil + } + return handleError(c, lookup(r.Lookup, c, args)) } + +func newTable(o io.Writer) *tablewriter.Table { + table := tablewriter.NewWriter(o) + table.SetRowLine(false) + table.SetBorder(false) + table.SetHeaderLine(false) + table.SetColumnSeparator(" ") + table.SetCenterSeparator(" ") + table.SetAlignment(tablewriter.ALIGN_LEFT) + return table +} diff --git a/cmd/config/internal/commands/cmdlistsetters_test.go b/cmd/config/internal/commands/cmdlistsetters_test.go new file mode 100644 index 000000000..017ba6ce7 --- /dev/null +++ b/cmd/config/internal/commands/cmdlistsetters_test.go @@ -0,0 +1,296 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package commands_test + +import ( + "bytes" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/cmd/config/internal/commands" + "sigs.k8s.io/kustomize/kyaml/openapi" +) + +func TestListSettersCommand(t *testing.T) { + var tests = []struct { + name string + openapi string + input string + args []string + expected string + }{ + { + name: "list-replicas", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me + description: "hello world" + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + `, + expected: ` NAME VALUE SET BY DESCRIPTION COUNT + replicas 3 me hello world 1 +`, + }, + { + name: "list-multiple", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: "hello world 1" + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me1 + io.k8s.cli.setters.image: + description: "hello world 2" + x-k8s-cli: + setter: + name: image + value: "nginx" + setBy: me2 + io.k8s.cli.setters.tag: + description: "hello world 3" + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + setBy: me3 + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"} + `, + expected: ` NAME VALUE SET BY DESCRIPTION COUNT + image nginx me2 hello world 2 2 + replicas 3 me1 hello world 1 1 + tag 1.7.9 me3 hello world 3 1 +`, + }, + { + name: "list-multiple-resources", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: "hello world 1" + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me1 + io.k8s.cli.setters.image: + description: "hello world 2" + x-k8s-cli: + setter: + name: image + value: "nginx" + setBy: me2 + io.k8s.cli.setters.tag: + description: "hello world 3" + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + setBy: me3 + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-1 +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-2 +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx +`, + expected: ` NAME VALUE SET BY DESCRIPTION COUNT + image nginx me2 hello world 2 3 + replicas 3 me1 hello world 1 2 + tag 1.7.9 me3 hello world 3 2 +`, + }, + { + name: "list-name", + args: []string{"image"}, + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: "hello world 1" + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me1 + io.k8s.cli.setters.image: + description: "hello world 2" + x-k8s-cli: + setter: + name: image + value: "nginx" + setBy: me2 + io.k8s.cli.setters.tag: + description: "hello world 3" + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + setBy: me3 + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-1 +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-2 +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx +`, + expected: ` NAME VALUE SET BY DESCRIPTION COUNT + image nginx me2 hello world 2 3 +`, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + // reset the openAPI afterward + openapi.ResetOpenAPI() + defer openapi.ResetOpenAPI() + + f, err := ioutil.TempFile("", "k8s-cli-") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(f.Name()) + commands.GetOpenAPIFile = func(args []string) (s string, err error) { + err = ioutil.WriteFile(f.Name(), []byte(test.openapi), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + return f.Name(), nil + } + + r, err := ioutil.TempFile("", "k8s-cli-*.yaml") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(r.Name()) + err = ioutil.WriteFile(r.Name(), []byte(test.input), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + + runner := commands.NewListSettersRunner("") + actual := &bytes.Buffer{} + runner.Command.SetOut(actual) + runner.Command.SetArgs(append([]string{r.Name()}, test.args...)) + err = runner.Command.Execute() + if !assert.NoError(t, err) { + t.FailNow() + } + + if !assert.Equal(t, test.expected, actual.String()) { + t.FailNow() + } + }) + } +} diff --git a/cmd/config/internal/commands/cmdset.go b/cmd/config/internal/commands/cmdset.go index 0b40a3c4d..a646fd1de 100644 --- a/cmd/config/internal/commands/cmdset.go +++ b/cmd/config/internal/commands/cmdset.go @@ -6,19 +6,21 @@ package commands import ( "fmt" "os" + "path/filepath" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" "sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/setters" + "sigs.k8s.io/kustomize/kyaml/setters2/settersutil" ) // NewSetRunner returns a command runner. func NewSetRunner(parent string) *SetRunner { r := &SetRunner{} c := &cobra.Command{ - Use: "set DIR [NAME] [VALUE]", + Use: "set DIR NAME [VALUE]", Args: cobra.RangeArgs(1, 3), Short: commands.SetShort, Long: commands.SetLong, @@ -32,18 +34,48 @@ func NewSetRunner(parent string) *SetRunner { "annotate the field with who set it") c.Flags().StringVar(&r.Perform.Description, "description", "", "annotate the field with a description of its value") + c.Flags().StringVar(&setterVersion, "version", "", + "use this version of the setter format") + c.Flags().MarkHidden("version") return r } +var setterVersion string + +var GetOpenAPIFile = func(args []string) (string, error) { + return filepath.Join(args[0], "kustomization"), nil +} + func SetCommand(parent string) *cobra.Command { return NewSetRunner(parent).Command } type SetRunner struct { - Command *cobra.Command - Lookup setters.LookupSetters - Perform setters.PerformSetters + Command *cobra.Command + Lookup setters.LookupSetters + Perform setters.PerformSetters + Set settersutil.FieldSetter + OpenAPIFile string +} + +func initSetterVersion(c *cobra.Command, args []string) error { + setterVersion = "v2" + l := setters.LookupSetters{} + + // backwards compatibility for resources with setter v1 + err := kio.Pipeline{ + Inputs: []kio.Reader{&kio.LocalPackageReader{PackagePath: args[0]}}, + Filters: []kio.Filter{&l}, + }.Execute() + if err != nil { + return err + } + if len(l.SetterCounts) > 0 { + setterVersion = "v1" + } + + return nil } func (r *SetRunner) preRunE(c *cobra.Command, args []string) error { @@ -55,15 +87,37 @@ func (r *SetRunner) preRunE(c *cobra.Command, args []string) error { r.Perform.Value = args[2] } + if setterVersion == "" { + if len(args) < 3 { + setterVersion = "v1" + } else if err := initSetterVersion(c, args); err != nil { + return err + } + } + if setterVersion == "v2" { + var err error + r.Set.Name = args[1] + r.Set.Value = args[2] + r.Set.Description = r.Perform.Description + r.Set.SetBy = r.Perform.SetBy + r.OpenAPIFile, err = GetOpenAPIFile(args) + if err != nil { + return err + } + } + return nil } func (r *SetRunner) runE(c *cobra.Command, args []string) error { - + if setterVersion == "v2" { + count, err := r.Set.Set(r.OpenAPIFile, args[0]) + fmt.Fprintf(c.OutOrStdout(), "set %d fields\n", count) + return handleError(c, err) + } if len(args) == 3 { return handleError(c, r.perform(c, args)) } - return handleError(c, lookup(r.Lookup, c, args)) } diff --git a/cmd/config/internal/commands/cmdset_test.go b/cmd/config/internal/commands/cmdset_test.go new file mode 100644 index 000000000..97b45fba5 --- /dev/null +++ b/cmd/config/internal/commands/cmdset_test.go @@ -0,0 +1,275 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package commands_test + +import ( + "bytes" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/cmd/config/internal/commands" + "sigs.k8s.io/kustomize/kyaml/openapi" +) + +func TestSetCommand(t *testing.T) { + var tests = []struct { + name string + inputOpenAPI string + input string + args []string + out string + expectedOpenAPI string + expectedResources string + }{ + { + name: "set replicas", + args: []string{"replicas", "4", "--description", "hi there", "--set-by", "pw"}, + out: "set 1 fields\n", + inputOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: hello world + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 # {"$ref":"#/definitions/io.k8s.cli.setters.replicas"} + `, + expectedOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: hi there + x-k8s-cli: + setter: + name: replicas + value: "4" + setBy: pw + `, + expectedResources: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 4 # {"$ref":"#/definitions/io.k8s.cli.setters.replicas"} + `, + }, + { + name: "set replicas no description", + args: []string{"replicas", "4"}, + out: "set 1 fields\n", + inputOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: hello world + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 # {"$ref":"#/definitions/io.k8s.cli.setters.replicas"} + `, + expectedOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: hello world + x-k8s-cli: + setter: + name: replicas + value: "4" + setBy: me + `, + expectedResources: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 4 # {"$ref":"#/definitions/io.k8s.cli.setters.replicas"} + `, + }, + { + name: "set image", + args: []string{"tag", "1.8.1"}, + out: "set 1 fields\n", + inputOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.image: + x-k8s-cli: + setter: + name: image + value: "nginx" + io.k8s.cli.setters.tag: + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref":"#/definitions/io.k8s.cli.substitutions.image"} + - name: sidecar + image: sidecar:1.7.9 + `, + expectedOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.image: + x-k8s-cli: + setter: + name: image + value: "nginx" + io.k8s.cli.setters.tag: + x-k8s-cli: + setter: + name: tag + value: "1.8.1" + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + + `, + expectedResources: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + template: + spec: + containers: + - name: nginx + image: nginx:1.8.1 # {"$ref":"#/definitions/io.k8s.cli.substitutions.image"} + - name: sidecar + image: sidecar:1.7.9 +`, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + // reset the openAPI afterward + openapi.ResetOpenAPI() + defer openapi.ResetOpenAPI() + + f, err := ioutil.TempFile("", "k8s-cli-") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(f.Name()) + err = ioutil.WriteFile(f.Name(), []byte(test.inputOpenAPI), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + commands.GetOpenAPIFile = func(args []string) (s string, err error) { + return f.Name(), nil + } + + r, err := ioutil.TempFile("", "k8s-cli-*.yaml") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(r.Name()) + err = ioutil.WriteFile(r.Name(), []byte(test.input), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + + runner := commands.NewSetRunner("") + out := &bytes.Buffer{} + runner.Command.SetOut(out) + runner.Command.SetArgs(append([]string{r.Name()}, test.args...)) + err = runner.Command.Execute() + if !assert.NoError(t, err) { + t.FailNow() + } + + if !assert.Equal(t, test.out, out.String()) { + t.FailNow() + } + + actualResources, err := ioutil.ReadFile(r.Name()) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, + strings.TrimSpace(test.expectedResources), + strings.TrimSpace(string(actualResources))) { + t.FailNow() + } + + actualOpenAPI, err := ioutil.ReadFile(f.Name()) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, + strings.TrimSpace(test.expectedOpenAPI), + strings.TrimSpace(string(actualOpenAPI))) { + t.FailNow() + } + }) + } +} diff --git a/cmd/config/internal/commands/fmt_test.go b/cmd/config/internal/commands/fmt_test.go index bc7e5df56..c373a5ca7 100644 --- a/cmd/config/internal/commands/fmt_test.go +++ b/cmd/config/internal/commands/fmt_test.go @@ -143,6 +143,8 @@ func TestCmd_failFiles(t *testing.T) { // fmt the files r := commands.GetFmtRunner("") r.Command.SetArgs([]string{"notrealfile"}) + r.Command.SilenceUsage = true + r.Command.SilenceErrors = true err := r.Command.Execute() assert.EqualError(t, err, "lstat notrealfile: no such file or directory") } diff --git a/kyaml/fieldmeta/fieldmeta.go b/kyaml/fieldmeta/fieldmeta.go index 0e83a8767..b4d6383d4 100644 --- a/kyaml/fieldmeta/fieldmeta.go +++ b/kyaml/fieldmeta/fieldmeta.go @@ -17,7 +17,7 @@ import ( type FieldMeta struct { Schema spec.Schema - Extensions *XKustomize + Extensions XKustomize } type XKustomize struct { @@ -62,9 +62,22 @@ func (fm *FieldMeta) Read(n *yaml.RNode) error { return nil } +func isExtensionEmpty(x XKustomize) bool { + if x.FieldSetter != nil { + return false + } + if x.SetBy != "" { + return false + } + if len(x.PartialFieldSetters) > 0 { + return false + } + return true +} + // Write writes the FieldMeta to a node func (fm *FieldMeta) Write(n *yaml.RNode) error { - if fm.Extensions != nil { + if !isExtensionEmpty(fm.Extensions) { fm.Schema.VendorExtensible.AddExtension("x-kustomize", fm.Extensions) } else { delete(fm.Schema.VendorExtensible.Extensions, "x-kustomize") diff --git a/kyaml/openapi/openapi.go b/kyaml/openapi/openapi.go index 5f1ace8e5..a349388fa 100644 --- a/kyaml/openapi/openapi.go +++ b/kyaml/openapi/openapi.go @@ -6,6 +6,7 @@ package openapi import ( "encoding/json" "fmt" + "io/ioutil" "sync" "github.com/go-openapi/spec" @@ -44,6 +45,66 @@ func SchemaForResourceType(t yaml.TypeMeta) *ResourceSchema { return &ResourceSchema{Schema: rs} } +// SupplementaryOpenAPIFieldName is the conventional field name (JSON/YAML) containing +// supplementary OpenAPI definitions. +const SupplementaryOpenAPIFieldName = "openAPI" + +// AddSchemaFromFile reads the file at path and parses the OpenAPI definitions +// from the field "openAPI" +func AddSchemaFromFile(path string) error { + return AddSchemaFromFileUsingField(path, SupplementaryOpenAPIFieldName) +} + +// AddSchemaFromFileUsingField reads the file at path and parses the OpenAPI definitions +// from the specified field. If field is the empty string, use the whole document as +// OpenAPI. +func AddSchemaFromFileUsingField(path, field string) error { + b, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + // parse the yaml file (json is a subset of yaml, so will also parse) + y, err := yaml.Parse(string(b)) + if err != nil { + return err + } + + if field != "" { + // get the field containing the openAPI + m := y.Field(field) + if yaml.IsFieldEmpty(m) { + // doesn't contain openAPI definitions + return nil + } + y = m.Value + } + + oAPI, err := y.String() + if err != nil { + return err + } + + // convert the yaml openAPI to a JSON string by unmarshalling it to an + // interface{} and the marshalling it to a string + var o interface{} + err = yaml.Unmarshal([]byte(oAPI), &o) + if err != nil { + return err + } + j, err := json.Marshal(o) + if err != nil { + return err + } + + // add the json schema to the global schema + _, err = AddSchema(j) + if err != nil { + return err + } + return nil +} + // AddSchema parses s, and adds definitions from s to the global schema. func AddSchema(s []byte) (*spec.Schema, error) { return parse(s) @@ -296,41 +357,6 @@ func resolve(root interface{}, ref *spec.Ref) (*spec.Schema, error) { } } -func PopulateDefsInOpenAPI(s string) error { - y, err := yaml.Parse(s) - if err != nil { - return err - } - // get the field containing the openAPI - f := y.Field("openAPI") - - defs, err := f.Value.String() - if err != nil { - return err - } - - // convert the yaml openAPI to an interface{} - // which can be marshalled into json - var o interface{} - err = yaml.Unmarshal([]byte(defs), &o) - if err != nil { - return err - } - - // convert the interface{} into a json string - j, err := json.Marshal(o) - if err != nil { - return err - } - - // add the json schema to the global schema - _, err = AddSchema(j) - if err != nil { - return err - } - return nil -} - func rootSchema() *spec.Schema { initSchema() return &globalSchema.schema diff --git a/kyaml/openapi/openapi_test.go b/kyaml/openapi/openapi_test.go index 988818f5f..bf0228986 100644 --- a/kyaml/openapi/openapi_test.go +++ b/kyaml/openapi/openapi_test.go @@ -5,6 +5,7 @@ package openapi import ( "fmt" + "io/ioutil" "testing" "github.com/stretchr/testify/assert" @@ -123,8 +124,8 @@ func TestSchemaForResourceType(t *testing.T) { } } -func TestPopulateDefsInOpenAPI_Setter(t *testing.T) { - globalSchema = openapiData{} +func TestAddSchemaFromFile(t *testing.T) { + ResetOpenAPI() inputyaml := ` openAPI: definitions: @@ -134,8 +135,15 @@ openAPI: name: image-name value: "nginx" ` - err := PopulateDefsInOpenAPI(inputyaml) + f, err := ioutil.TempFile("", "openapi-") + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.NoError(t, ioutil.WriteFile(f.Name(), []byte(inputyaml), 0600)) { + t.FailNow() + } + err = AddSchemaFromFile(f.Name()) if !assert.NoError(t, err) { t.FailNow() } @@ -153,7 +161,7 @@ openAPI: } func TestPopulateDefsInOpenAPI_Substitution(t *testing.T) { - globalSchema = openapiData{} + ResetOpenAPI() inputyaml := ` openAPI: definitions: @@ -178,11 +186,18 @@ openAPI: - marker: "IMAGE_TAG" ref: "#/definitions/io.k8s.cli.setters.image-tag" ` - err := PopulateDefsInOpenAPI(inputyaml) + f, err := ioutil.TempFile("", "openapi-") if !assert.NoError(t, err) { t.FailNow() } + if !assert.NoError(t, ioutil.WriteFile(f.Name(), []byte(inputyaml), 0600)) { + t.FailNow() + } + + if !assert.NoError(t, AddSchemaFromFile(f.Name())) { + t.FailNow() + } s, err := GetSchema(`{"$ref": "#/definitions/io.k8s.cli.substitutions.image"}`) @@ -199,3 +214,26 @@ openAPI: ` map[marker:IMAGE_TAG ref:#/definitions/io.k8s.cli.setters.image-tag]]]]]`, fmt.Sprintf("%v", s.Schema.Extensions)) } + +func TestAddSchemaFromFile_empty(t *testing.T) { + ResetOpenAPI() + inputyaml := ` +kind: Example + ` + + f, err := ioutil.TempFile("", "openapi-") + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.NoError(t, ioutil.WriteFile(f.Name(), []byte(inputyaml), 0600)) { + t.FailNow() + } + + if !assert.NoError(t, AddSchemaFromFile(f.Name())) { + t.FailNow() + } + + if !assert.Equal(t, len(globalSchema.schema.Definitions), 0) { + t.FailNow() + } +} diff --git a/kyaml/setters2/add.go b/kyaml/setters2/add.go index 9dac90cfb..8bf7cc49d 100644 --- a/kyaml/setters2/add.go +++ b/kyaml/setters2/add.go @@ -9,6 +9,7 @@ import ( "github.com/go-openapi/spec" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/fieldmeta" + "sigs.k8s.io/kustomize/kyaml/openapi" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -72,3 +73,128 @@ func (a *Add) visitScalar(object *yaml.RNode, p string) error { } return nil } + +const ( + // CLIDefinitionsPrefix is the prefix for cli definition keys. + CLIDefinitionsPrefix = "io.k8s.cli." + + // SetterDefinitionPrefix is the prefix for setter definition keys. + SetterDefinitionPrefix = CLIDefinitionsPrefix + "setters." + + // SubstitutionDefinitionPrefix is the prefix for substitution definition keys. + SubstitutionDefinitionPrefix = CLIDefinitionsPrefix + "substitutions." + + // DefinitionsPrefix is the prefix used to reference definitions in the OpenAPI + DefinitionsPrefix = "#/definitions/" +) + +// SetterDefinition may be used to update a files OpenAPI definitions with a new setter. +type SetterDefinition struct { + // Name is the name of the setter to create or update. + Name string `yaml:"name"` + + // Value is the value of the setter. + Value string `yaml:"value"` + + // SetBy is the person or role that last set the value. + SetBy string `yaml:"setBy,omitempty"` + + // Description is a description of the value. + Description string `yaml:"description,omitempty"` + + // Count is the number of fields set by this setter. + Count int `yaml:"count,omitempty"` +} + +func (sd SetterDefinition) AddToFile(path string) error { + return yaml.UpdateFile(sd, path) +} + +func (sd SetterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) { + key := SetterDefinitionPrefix + sd.Name + + def, err := object.Pipe(yaml.LookupCreate( + yaml.MappingNode, openapi.SupplementaryOpenAPIFieldName, "definitions", key)) + if err != nil { + return nil, err + } + if sd.Description != "" { + err = def.PipeE(yaml.FieldSetter{Name: "description", StringValue: sd.Description}) + if err != nil { + return nil, err + } + // don't write the description to the extension + sd.Description = "" + } + + ext, err := def.Pipe(yaml.LookupCreate(yaml.MappingNode, K8sCliExtensionKey)) + if err != nil { + return nil, err + } + + b, err := yaml.Marshal(sd) + if err != nil { + return nil, err + } + y, err := yaml.Parse(string(b)) + if err != nil { + return nil, err + } + + if err := ext.PipeE(yaml.SetField("setter", y)); err != nil { + return nil, err + } + + return object, nil +} + +// SetterDefinition may be used to update a files OpenAPI definitions with a new substitution. +type SubstitutionDefinition struct { + // Name is the name of the substitution to create or update + Name string `yaml:"name"` + + // Pattern is the substitution pattern into which setter values are substituted + Pattern string `yaml:"pattern"` + + // Values are setters which are substituted into pattern to produce a field value + Values []Value `yaml:"values"` +} + +type Value struct { + // Marker is the string marker in pattern that is replace by the referenced setter. + Marker string `yaml:"marker"` + + // Ref is a reference to a setter to pull the replacement value from. + Ref string `yaml:"ref"` +} + +func (sd SubstitutionDefinition) AddToFile(path string) error { + return yaml.UpdateFile(sd, path) +} + +func (sd SubstitutionDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) { + // create the substitution extension value by marshalling the SubstitutionDefinition itself + b, err := yaml.Marshal(sd) + if err != nil { + return nil, err + } + sub, err := yaml.Parse(string(b)) + if err != nil { + return nil, err + } + + // lookup or create the definition for the substitution + defKey := SubstitutionDefinitionPrefix + sd.Name + def, err := object.Pipe(yaml.LookupCreate( + yaml.MappingNode, openapi.SupplementaryOpenAPIFieldName, "definitions", defKey, "x-k8s-cli")) + if err != nil { + return nil, err + } + + // set the substitution on the definition + if err := def.PipeE(yaml.SetField("substitution", sub)); err != nil { + return nil, err + } + + return object, nil +} diff --git a/kyaml/setters2/add_test.go b/kyaml/setters2/add_test.go index 8a867c63e..ab5524287 100644 --- a/kyaml/setters2/add_test.go +++ b/kyaml/setters2/add_test.go @@ -4,6 +4,9 @@ package setters2 import ( + "io/ioutil" + "os" + "path/filepath" "strings" "testing" @@ -204,3 +207,152 @@ spec: }) } } + +var resourcefile = `apiVersion: resource.dev/v1alpha1 +kind: resourcefile +metadata: + name: hello-world-set +upstream: + type: git + git: + commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe + directory: /package-examples/helloworld-set + ref: v0.1.0 +packageMetadata: + shortDescription: example package using setters` + +func TestAdd_Filter2(t *testing.T) { + path := filepath.Join(os.TempDir(), "resourcefile") + + //write initial resourcefile to temp path + err := ioutil.WriteFile(path, []byte(resourcefile), 0666) + if !assert.NoError(t, err) { + t.FailNow() + } + + //add a setter definition + sd := SetterDefinition{ + Name: "image", + Value: "1", + } + + err = sd.AddToFile(path) + + if !assert.NoError(t, err) { + t.FailNow() + } + + // update setter definition + sd2 := SetterDefinition{ + Name: "image", + Value: "2", + } + + err = sd2.AddToFile(path) + + if !assert.NoError(t, err) { + t.FailNow() + } + + b, err := ioutil.ReadFile(path) + if err != nil { + t.FailNow() + } + + expected := `apiVersion: resource.dev/v1alpha1 +kind: resourcefile +metadata: + name: hello-world-set +upstream: + type: git + git: + commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe + directory: /package-examples/helloworld-set + ref: v0.1.0 +packageMetadata: + shortDescription: example package using setters +openAPI: + definitions: + io.k8s.cli.setters.image: + x-k8s-cli: + setter: + name: image + value: "2" +` + assert.Equal(t, expected, string(b)) +} + +func TestAddUpdateSubstitution(t *testing.T) { + path := filepath.Join(os.TempDir(), "resourcefile") + + //write initial resourcefile to temp path + err := ioutil.WriteFile(path, []byte(resourcefile), 0666) + if !assert.NoError(t, err) { + t.FailNow() + } + + value1 := Value{ + Marker: "IMAGE_NAME", + Ref: "#/definitions/io.k8s.cli.setters.image-name", + } + + value2 := Value{ + Marker: "IMAGE_TAG", + Ref: "#/definitions/io.k8s.cli.setters.image-tag", + } + + values := []Value{value1, value2} + + //add a setter definition + subd := SubstitutionDefinition{ + Name: "image", + Pattern: "IMAGE_NAME:IMAGE_TAG", + Values: values, + } + + err = subd.AddToFile(path) + + if !assert.NoError(t, err) { + t.FailNow() + } + + // update setter definition + subd2 := SubstitutionDefinition{ + Name: "image", + Pattern: "IMAGE_NAME:IMAGE_TAG2", + } + + err = subd2.AddToFile(path) + + if !assert.NoError(t, err) { + t.FailNow() + } + + b, err := ioutil.ReadFile(path) + if err != nil { + t.FailNow() + } + + expected := `apiVersion: resource.dev/v1alpha1 +kind: resourcefile +metadata: + name: hello-world-set +upstream: + type: git + git: + commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe + directory: /package-examples/helloworld-set + ref: v0.1.0 +packageMetadata: + shortDescription: example package using setters +openAPI: + definitions: + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE_NAME:IMAGE_TAG2 + values: [] +` + assert.Equal(t, expected, string(b)) +} diff --git a/kyaml/setters2/list.go b/kyaml/setters2/list.go new file mode 100644 index 000000000..a0436732f --- /dev/null +++ b/kyaml/setters2/list.go @@ -0,0 +1,117 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package setters2 + +import ( + "sort" + "strings" + + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// List lists the setters specified in the OpenAPI +type List struct { + Name string + + Setters []SetterDefinition +} + +// List initializes l.Setters with the setters from the OpenAPI definitions in the file +func (l *List) List(openAPIPath, resourcePath string) error { + if err := openapi.AddSchemaFromFile(openAPIPath); err != nil { + return err + } + y, err := yaml.ReadFile(openAPIPath) + if err != nil { + return err + } + return l.list(y, resourcePath) +} + +func (l *List) list(object *yaml.RNode, resourcePath string) error { + // read the OpenAPI definitions + def, err := object.Pipe(yaml.LookupCreate(yaml.MappingNode, "openAPI", "definitions")) + if err != nil { + return err + } + if yaml.IsEmpty(def) { + return nil + } + + // iterate over definitions -- find those that are setters + err = def.VisitFields(func(node *yaml.MapNode) error { + setter := SetterDefinition{} + + // the definition key -- contains the setter name + key := node.Key.YNode().Value + + if !strings.HasPrefix(key, SetterDefinitionPrefix) { + // not a setter -- doesn't have the right prefix + return nil + } + + setterNode, err := node.Value.Pipe(yaml.Lookup(K8sCliExtensionKey, "setter")) + if err != nil { + return err + } + if yaml.IsEmpty(setterNode) { + // has the setter prefix, but missing the setter extension + return errors.Errorf("missing x-k8s-cli.setter for %s", key) + } + + // unmarshal the yaml for the setter extension into the definition struct + b, err := setterNode.String() + if err != nil { + return err + } + if err := yaml.Unmarshal([]byte(b), &setter); err != nil { + return err + } + + if l.Name != "" && l.Name != setter.Name { + // not the setter that was requested by list + return nil + } + + // the description is not part of the extension, and should be pulled out + // separately from the extension values. + description := node.Value.Field("description") + if description != nil { + setter.Description = description.Value.YNode().Value + } + + // count the number of fields set by this setter + setter.Count, err = l.count(resourcePath, setter.Name) + if err != nil { + return err + } + + l.Setters = append(l.Setters, setter) + return nil + }) + if err != nil { + return err + } + + // sort the setters by their name + sort.Slice(l.Setters, func(i, j int) bool { + return l.Setters[i].Name < l.Setters[j].Name + }) + + return nil +} + +// count returns the number of fields set by the setter with name +func (l *List) count(path, name string) (int, error) { + s := &Set{Name: name} + err := kio.Pipeline{ + Inputs: []kio.Reader{&kio.LocalPackageReader{PackagePath: path}}, + Filters: []kio.Filter{kio.FilterAll(s)}, + }.Execute() + + return s.Count, err +} diff --git a/kyaml/setters2/list_test.go b/kyaml/setters2/list_test.go new file mode 100644 index 000000000..4b1a24874 --- /dev/null +++ b/kyaml/setters2/list_test.go @@ -0,0 +1,289 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package setters2 + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/openapi" +) + +func TestList(t *testing.T) { + var tests = []struct { + name string + setter string + openapi string + input string + expected []SetterDefinition + }{ + { + name: "list-replicas", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me + description: "hello world" + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + `, + expected: []SetterDefinition{ + {Name: "replicas", Value: "3", SetBy: "me", Description: "hello world", Count: 1}, + }, + }, + { + name: "list-multiple", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: "hello world 1" + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me1 + io.k8s.cli.setters.image: + description: "hello world 2" + x-k8s-cli: + setter: + name: image + value: "nginx" + setBy: me2 + io.k8s.cli.setters.tag: + description: "hello world 3" + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + setBy: me3 + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"} + `, + expected: []SetterDefinition{ + {Name: "image", Value: "nginx", SetBy: "me2", Description: "hello world 2", Count: 2}, + {Name: "replicas", Value: "3", SetBy: "me1", Description: "hello world 1", Count: 1}, + {Name: "tag", Value: "1.7.9", SetBy: "me3", Description: "hello world 3", Count: 1}, + }, + }, + { + name: "list-multiple-resources", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: "hello world 1" + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me1 + io.k8s.cli.setters.image: + description: "hello world 2" + x-k8s-cli: + setter: + name: image + value: "nginx" + setBy: me2 + io.k8s.cli.setters.tag: + description: "hello world 3" + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + setBy: me3 + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-1 +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-2 +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx +`, + expected: []SetterDefinition{ + {Name: "image", Value: "nginx", SetBy: "me2", Description: "hello world 2", Count: 3}, + {Name: "replicas", Value: "3", SetBy: "me1", Description: "hello world 1", Count: 2}, + {Name: "tag", Value: "1.7.9", SetBy: "me3", Description: "hello world 3", Count: 2}, + }, + }, + { + name: "list-name", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + description: "hello world 1" + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: me1 + io.k8s.cli.setters.image: + description: "hello world 2" + x-k8s-cli: + setter: + name: image + value: "nginx" + setBy: me2 + io.k8s.cli.setters.tag: + description: "hello world 3" + x-k8s-cli: + setter: + name: tag + value: "1.7.9" + setBy: me3 + io.k8s.cli.substitutions.image: + x-k8s-cli: + substitution: + name: image + pattern: IMAGE:TAG + values: + - marker: IMAGE + ref: '#/definitions/io.k8s.cli.setters.image' + - marker: TAG + ref: '#/definitions/io.k8s.cli.setters.tag' + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-1 +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx # {"$ref": "#/definitions/io.k8s.cli.setters.image"} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment-2 +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + template: + spec: + containers: + - name: nginx + image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + - name: nginx2 + image: nginx +`, + setter: "image", + expected: []SetterDefinition{ + {Name: "image", Value: "nginx", SetBy: "me2", Description: "hello world 2", Count: 3}, + }, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + // reset the openAPI afterward + defer openapi.ResetOpenAPI() + initSchema(t, test.openapi) + + f, err := ioutil.TempFile("", "k8s-cli-") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(f.Name()) + err = ioutil.WriteFile(f.Name(), []byte(test.openapi), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + + r, err := ioutil.TempFile("", "k8s-cli-*.yaml") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(r.Name()) + err = ioutil.WriteFile(r.Name(), []byte(test.input), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + + // invoke the setter + instance := &List{Name: test.setter} + err = instance.List(f.Name(), r.Name()) + if !assert.NoError(t, err) { + t.FailNow() + } + + if !assert.Equal(t, test.expected, instance.Setters) { + t.FailNow() + } + }) + } +} diff --git a/kyaml/setters2/set.go b/kyaml/setters2/set.go index dc81e014b..ec242d805 100644 --- a/kyaml/setters2/set.go +++ b/kyaml/setters2/set.go @@ -17,6 +17,9 @@ type Set struct { // Name is the name of the setter to set on the object. i.e. matches the x-k8s-cli.setter.name // of the setter that should have its value applied to fields which reference it. Name string + + // Count is the number of fields that were updated by calling Filter + Count int } // Filter implements Set as a yaml.Filter @@ -25,7 +28,7 @@ func (s *Set) Filter(object *yaml.RNode) (*yaml.RNode, error) { } // visitScalar -func (s *Set) visitScalar(object *yaml.RNode, _ string) error { +func (s *Set) visitScalar(object *yaml.RNode, p string) error { // get the openAPI for this field describing how to apply the setter ext, err := getExtFromComment(object) if err != nil { @@ -37,14 +40,18 @@ func (s *Set) visitScalar(object *yaml.RNode, _ string) error { // perform a direct set of the field if it matches if s.set(object, ext) { + s.Count++ return nil } // perform a substitution of the field if it matches - if sub, err := s.substitute(object, ext); sub || err != nil { + sub, err := s.substitute(object, ext) + if err != nil { return err } - + if sub { + s.Count++ + } return nil } @@ -111,3 +118,54 @@ func (s *Set) set(field *yaml.RNode, ext *cliExtension) bool { field.YNode().Value = ext.Setter.Value return true } + +// SetOpenAPI updates a setter value +type SetOpenAPI struct { + // Name is the name of the setter to add + Name string `yaml:"name"` + // Value is the current value of the setter + Value string `yaml:"value"` + + Description string `yaml:"description"` + + SetBy string `yaml:"setBy"` +} + +// UpdateFile updates the OpenAPI definitions in a file with the given setter value. +func (s SetOpenAPI) UpdateFile(path string) error { + return yaml.UpdateFile(s, path) +} + +func (s SetOpenAPI) Filter(object *yaml.RNode) (*yaml.RNode, error) { + key := SetterDefinitionPrefix + s.Name + def, err := object.Pipe(yaml.Lookup( + "openAPI", "definitions", key, "x-k8s-cli", "setter")) + if err != nil { + return nil, err + } + if def == nil { + return nil, errors.Errorf("no setter %s found", s.Name) + } + if err := def.PipeE(&yaml.FieldSetter{Name: "value", StringValue: s.Value}); err != nil { + return nil, err + } + + if s.SetBy != "" { + if err := def.PipeE(&yaml.FieldSetter{Name: "setBy", StringValue: s.SetBy}); err != nil { + return nil, err + } + } + + if s.Description != "" { + d, err := object.Pipe(yaml.LookupCreate( + yaml.MappingNode, "openAPI", "definitions", key)) + if err != nil { + return nil, err + } + if err := d.PipeE(&yaml.FieldSetter{Name: "description", StringValue: s.Description}); err != nil { + return nil, err + } + } + + return object, nil +} diff --git a/kyaml/setters2/set_test.go b/kyaml/setters2/set_test.go index 07e984a2b..1550a6244 100644 --- a/kyaml/setters2/set_test.go +++ b/kyaml/setters2/set_test.go @@ -438,3 +438,218 @@ func initSchema(t *testing.T, s string) { t.FailNow() } } + +func TestSetOpenAPI_Filter(t *testing.T) { + var tests = []struct { + name string + setter string + value string + input string + expected string + description string + setBy string + err string + }{ + { + name: "set-replicas", + setter: "replicas", + value: "3", + input: ` +openAPI: + definitions: + io.k8s.cli.setters.no-match-1': + x-k8s-cli: + setter: + name: no-match-1 + value: "1" + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "4" + io.k8s.cli.setters.no-match-2': + x-k8s-cli: + setter: + name: no-match-2 + value: "2" + `, + expected: ` +openAPI: + definitions: + io.k8s.cli.setters.no-match-1': + x-k8s-cli: + setter: + name: no-match-1 + value: "1" + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "3" + io.k8s.cli.setters.no-match-2': + x-k8s-cli: + setter: + name: no-match-2 + value: "2" +`, + }, + { + name: "set-replicas-description", + setter: "replicas", + value: "3", + description: "hello world", + input: ` +openAPI: + definitions: + io.k8s.cli.setters.no-match-1': + x-k8s-cli: + setter: + name: no-match-1 + value: "1" + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "4" + io.k8s.cli.setters.no-match-2': + x-k8s-cli: + setter: + name: no-match-2 + value: "2" + `, + expected: ` +openAPI: + definitions: + io.k8s.cli.setters.no-match-1': + x-k8s-cli: + setter: + name: no-match-1 + value: "1" + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "3" + description: hello world + io.k8s.cli.setters.no-match-2': + x-k8s-cli: + setter: + name: no-match-2 + value: "2" +`, + }, + { + name: "set-replicas-set-by", + setter: "replicas", + value: "3", + setBy: "carl", + input: ` +openAPI: + definitions: + io.k8s.cli.setters.no-match-1': + x-k8s-cli: + setter: + name: no-match-1 + value: "1" + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "4" + io.k8s.cli.setters.no-match-2': + x-k8s-cli: + setter: + name: no-match-2 + value: "2" + `, + expected: ` +openAPI: + definitions: + io.k8s.cli.setters.no-match-1': + x-k8s-cli: + setter: + name: no-match-1 + value: "1" + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "3" + setBy: carl + io.k8s.cli.setters.no-match-2': + x-k8s-cli: + setter: + name: no-match-2 + value: "2" +`, + }, + { + name: "error", + setter: "replicas", + err: "no setter replicas found", + input: ` +openAPI: + definitions: + io.k8s.cli.setters.no-match-1': + x-k8s-cli: + setter: + name: no-match-1 + value: "1" + io.k8s.cli.setters.no-match-2': + x-k8s-cli: + setter: + name: no-match-2 + value: "2" + `, + expected: ` +openAPI: + definitions: + io.k8s.cli.setters.no-match-1': + x-k8s-cli: + setter: + name: no-match-1 + value: "1" + io.k8s.cli.setters.no-match-2': + x-k8s-cli: + setter: + name: no-match-2 + value: "2" + `, + }, + } + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + in, err := yaml.Parse(test.input) + if !assert.NoError(t, err) { + t.FailNow() + } + + // invoke the setter + instance := &SetOpenAPI{ + Name: test.setter, Value: test.value, + SetBy: test.setBy, Description: test.description} + result, err := instance.Filter(in) + if test.err != "" { + if !assert.EqualError(t, err, test.err) { + t.FailNow() + } + return + } + if !assert.NoError(t, err) { + t.FailNow() + } + + // compare the actual and expected output + actual, err := result.String() + if !assert.NoError(t, err) { + t.FailNow() + } + actual = strings.TrimSpace(actual) + expected := strings.TrimSpace(test.expected) + if !assert.Equal(t, expected, actual) { + t.FailNow() + } + }) + } +} diff --git a/kyaml/setters2/setterdefinition.go b/kyaml/setters2/setterdefinition.go deleted file mode 100644 index be8604dcf..000000000 --- a/kyaml/setters2/setterdefinition.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package setters2 - -import ( - "io/ioutil" - - "sigs.k8s.io/kustomize/kyaml/yaml" -) - -const DefinitionPrefix = "io.k8s.cli.setters." - -type SetterDefinition struct { - Name string - Value string -} - -func (sd SetterDefinition) AddSetterToFile(path string) error { - b, err := ioutil.ReadFile(path) - if err != nil { - return err - } - y, err := yaml.Parse(string(b)) - if err != nil { - return err - } - if err := y.PipeE(sd); err != nil { - return err - } - out, err := y.String() - if err != nil { - return err - } - if err := ioutil.WriteFile(path, []byte(out), 0600); err != nil { - return err - } - return nil -} - -func (sd SetterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) { - key := DefinitionPrefix + sd.Name - - def, err := object.Pipe(yaml.LookupCreate( - yaml.MappingNode, "openAPI", "definitions", key, "x-k8s-cli", "setter")) - if err != nil { - return nil, err - } - if err := def.PipeE(yaml.FieldSetter{Name: "name", StringValue: sd.Name}); err != nil { - return nil, err - } - if err := def.PipeE(yaml.FieldSetter{Name: "value", StringValue: sd.Value}); err != nil { - return nil, err - } - return object, nil -} diff --git a/kyaml/setters2/setterdefinition_test.go b/kyaml/setters2/setterdefinition_test.go deleted file mode 100644 index f233271e2..000000000 --- a/kyaml/setters2/setterdefinition_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package setters2 - -import ( - "io/ioutil" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -var resourcefile = `apiVersion: resource.dev/v1alpha1 -kind: resourcefile -metadata: - name: hello-world-set -upstream: - type: git - git: - commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe - directory: /package-examples/helloworld-set - ref: v0.1.0 -packageMetadata: - shortDescription: example package using setters` - -func TestAddUpdateSetter(t *testing.T) { - path := os.TempDir() + "/resourcefile" - - //write initial resourcefile to temp path - err := ioutil.WriteFile(path, []byte(resourcefile), 0666) - if !assert.NoError(t, err) { - t.FailNow() - } - - //add a setter definition - sd := SetterDefinition{ - Name: "image", - Value: "1", - } - - err = sd.AddSetterToFile(path) - - if !assert.NoError(t, err) { - t.FailNow() - } - - // update setter definition - sd2 := SetterDefinition{ - Name: "image", - Value: "2", - } - - err = sd2.AddSetterToFile(path) - - if !assert.NoError(t, err) { - t.FailNow() - } - - b, err := ioutil.ReadFile(path) - if err != nil { - t.FailNow() - } - - expected := `apiVersion: resource.dev/v1alpha1 -kind: resourcefile -metadata: - name: hello-world-set -upstream: - type: git - git: - commit: 5c1c019b59299a4f6c7edd1ff5ff54d720621bbe - directory: /package-examples/helloworld-set - ref: v0.1.0 -packageMetadata: - shortDescription: example package using setters -openAPI: - definitions: - io.k8s.cli.setters.image: - x-k8s-cli: - setter: - name: image - value: 2 -` - assert.Equal(t, expected, string(b)) -} diff --git a/kyaml/setters2/settersutil/fieldsetter.go b/kyaml/setters2/settersutil/fieldsetter.go new file mode 100644 index 000000000..bf2b44fa6 --- /dev/null +++ b/kyaml/setters2/settersutil/fieldsetter.go @@ -0,0 +1,48 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package settersutil + +import ( + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/setters2" +) + +// FieldSetter sets the value for a field setter. +type FieldSetter struct { + // Name is the name of the setter to set + Name string + + // Value is the value to set + Value string + + Description string + + SetBy string +} + +// Set updates the OpenAPI definitions and resources with the new setter value +func (fs FieldSetter) Set(openAPIPath, resourcesPath string) (int, error) { + // Update the OpenAPI definitions + soa := setters2.SetOpenAPI{ + Name: fs.Name, Value: fs.Value, Description: fs.Description, SetBy: fs.SetBy} + if err := soa.UpdateFile(openAPIPath); err != nil { + return 0, err + } + + // Load the updated definitions + if err := openapi.AddSchemaFromFile(openAPIPath); err != nil { + return 0, err + } + + // Update the resources with the new value + inout := &kio.LocalPackageReadWriter{PackagePath: resourcesPath} + s := &setters2.Set{Name: fs.Name} + err := kio.Pipeline{ + Inputs: []kio.Reader{inout}, + Filters: []kio.Filter{kio.FilterAll(s)}, + Outputs: []kio.Writer{inout}, + }.Execute() + return s.Count, err +} diff --git a/kyaml/setters2/settersutil/settercreator.go b/kyaml/setters2/settersutil/settercreator.go new file mode 100644 index 000000000..121be30ce --- /dev/null +++ b/kyaml/setters2/settersutil/settercreator.go @@ -0,0 +1,60 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package settersutil + +import ( + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/setters2" +) + +// SetterCreator creates or updates a setter in the OpenAPI definitions, and inserts references +// to the setter from matching resource fields. +type SetterCreator struct { + // Name is the name of the setter to create or update. + Name string + + Description string + + SetBy string + + // FieldName if set will add the OpenAPI reference to fields with this name or path + // FieldName may be the full name of the field, full path to the field, or the path suffix. + // e.g. all of the following would match spec.template.spec.containers.image -- + // [image, containers.image, spec.containers.image, template.spec.containers.image, + // spec.template.spec.containers.image] + // Optional. If unspecified match all field names. + FieldName string + + // FieldValue if set will add the OpenAPI reference to fields if they have this value. + // Optional. If unspecified match all field values. + FieldValue string +} + +func (c SetterCreator) Create(openAPIPath, resourcesPath string) error { + // Update the OpenAPI definitions to hace the setter + sd := setters2.SetterDefinition{ + Name: c.Name, Value: c.FieldValue, Description: c.Description, SetBy: c.SetBy} + if err := sd.AddToFile(openAPIPath); err != nil { + return err + } + + // Load the updated definitions + if err := openapi.AddSchemaFromFile(openAPIPath); err != nil { + return err + } + + // Update the resources with the setter reference + inout := &kio.LocalPackageReadWriter{PackagePath: resourcesPath} + return kio.Pipeline{ + Inputs: []kio.Reader{inout}, + Filters: []kio.Filter{kio.FilterAll( + &setters2.Add{ + FieldName: c.FieldName, + FieldValue: c.FieldValue, + Ref: setters2.DefinitionsPrefix + setters2.SetterDefinitionPrefix + c.Name, + })}, + Outputs: []kio.Writer{inout}, + }.Execute() +} diff --git a/kyaml/setters2/settersutil/substitutioncreator.go b/kyaml/setters2/settersutil/substitutioncreator.go new file mode 100644 index 000000000..15059abd0 --- /dev/null +++ b/kyaml/setters2/settersutil/substitutioncreator.go @@ -0,0 +1,208 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package settersutil + +import ( + "io/ioutil" + "math" + "strings" + + "sigs.k8s.io/kind/pkg/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/setters2" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// SubstitutionCreator creates or updates a substitution in the OpenAPI definitions, and +// inserts references to the substitution from matching resource fields. +type SubstitutionCreator struct { + // Name is the name of the substitution to create + Name string + + // Pattern is the substitution pattern + Pattern string + + // Values are the substitution values for the pattern + Values []setters2.Value + + // FieldName if set will add the OpenAPI reference to fields with this name or path + // FieldName may be the full name of the field, full path to the field, or the path suffix. + // e.g. all of the following would match spec.template.spec.containers.image -- + // [image, containers.image, spec.containers.image, template.spec.containers.image, + // spec.template.spec.containers.image] + // Optional. If unspecified match all field names. + FieldName string + + // FieldValue if set will add the OpenAPI reference to fields if they have this value. + // Optional. If unspecified match all field values. + FieldValue string +} + +func (c SubstitutionCreator) Create(openAPIPath, resourcesPath string) error { + d := setters2.SubstitutionDefinition{ + Name: c.Name, + Values: c.Values, + Pattern: c.Pattern, + } + + err := c.CreateSettersForSubstitution(openAPIPath) + if err != nil { + return err + } + + if err := d.AddToFile(openAPIPath); err != nil { + return err + } + + // Load the updated definitions + if err := openapi.AddSchemaFromFile(openAPIPath); err != nil { + return err + } + + // Update the resources with the setter reference + inout := &kio.LocalPackageReadWriter{PackagePath: resourcesPath} + return kio.Pipeline{ + Inputs: []kio.Reader{inout}, + Filters: []kio.Filter{kio.FilterAll( + &setters2.Add{ + FieldName: c.FieldName, + FieldValue: c.FieldValue, + Ref: setters2.DefinitionsPrefix + setters2.SubstitutionDefinitionPrefix + c.Name, + })}, + Outputs: []kio.Writer{inout}, + }.Execute() +} + +// CreateSettersForSubstitution creates the setters for all the references in the substitution +// values if they don't already exist in openAPIPath file. +func (c SubstitutionCreator) CreateSettersForSubstitution(openAPIPath string) error { + b, err := ioutil.ReadFile(openAPIPath) + if err != nil { + return err + } + + // parse the yaml file + y, err := yaml.Parse(string(b)) + if err != nil { + return err + } + + m, err := c.GetValuesForMarkers() + if err != nil { + return err + } + + // for each ref in values, check if the setter already exists, if not create them + for _, value := range c.Values { + obj, err := y.Pipe(yaml.Lookup( + // get the setter key from ref. Ex: from #/definitions/io.k8s.cli.setters.image_setter + // extract io.k8s.cli.setters.image_setter + "openAPI", "definitions", strings.TrimPrefix(value.Ref, "#/definitions/"))) + + if err != nil { + return err + } + + if obj == nil { + sd := setters2.SetterDefinition{ + // get the setter name from ref. Ex: from #/definitions/io.k8s.cli.setters.image_setter + // extract image_setter + Name: strings.Split(value.Ref, ".")[4], + Value: m[value.Marker], + } + sd.AddToFile(openAPIPath) + } + } + return nil +} + +// GetValuesForMarkers parses the pattern and field value to derive values for the +// markers in the pattern string. Returns error if the marker values can't be derived +func (c SubstitutionCreator) GetValuesForMarkers() (map[string]string, error) { + m := make(map[string]string) + indices, err := c.GetStartIndices() + if err != nil { + return nil, err + } + s:=c.FieldValue + p:=c.Pattern + i:=0 + j:=0 + // iterate s, p with indices i, j respectively and when j hits the index of a marker, freeze j and iterate + // i and capture string till we find the substring just after current marker and before next marker + + // Ex: s = "something/ubuntu:0.1.0", p = "something/IMAGE::VERSION", till j reaches 10 + // just proceed i and j and check if s[i]==p[j] + // when j is 10, freeze j and move i till it sees substring '::' which derives IMAGE = ubuntu and so on. + for i< len(s) && j< len(p) { + if marker, ok := indices[j]; ok { + value := "" + e := j+len(marker) + + for i j { + res = min(k-j, res) + } + } + return res +} + +func min(a int, b int) int { + if a