Merge pull request #2203 from pwittrock/setter-wiring

Manually merging since the prow automation does not appear to be configured correctly
This commit is contained in:
Phillip Wittrock
2020-02-19 14:55:09 -08:00
committed by Phani Teja Marupaka
27 changed files with 2620 additions and 361 deletions

View File

@@ -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))

View File

@@ -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},

View File

@@ -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()
}
})
}
}

View File

@@ -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
}

View File

@@ -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()
}
})
}
}

View File

@@ -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
}

View File

@@ -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()
}
})
}
}

View File

@@ -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))
}

View File

@@ -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()
}
})
}
}

View File

@@ -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")
}

View File

@@ -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")

View File

@@ -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

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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))
}

117
kyaml/setters2/list.go Normal file
View File

@@ -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
}

289
kyaml/setters2/list_test.go Normal file
View File

@@ -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()
}
})
}
}

View File

@@ -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
}

View File

@@ -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()
}
})
}
}

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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<len(s) && (e == len(p) ||
s[i:min(len(s), i+lenToNextMarker(indices, e))] != p[e:min(e+lenToNextMarker(indices, e), len(p))]) {
value += string(s[i])
i++
}
// if marker is repeated in the pattern, make sure that the corresponding values
// are same and throw error if not.
if prevValue, ok := m[marker]; ok && prevValue != value {
return nil, errors.Errorf("Same marker is found to have different values in field value.")
}
m[marker] = value
j+=len(marker)
} else {
if s[i] != p[j] {
return nil, errors.Errorf("Unable to derive values for markers. Create setters for all markers.")
}
i++
j++
}
}
// check if both strings are completely visited or throw error
if i<len(s) || j<len(p) {
return nil, errors.Errorf("Unable to derive values for markers. Create setters for all markers and then try again.")
}
return m, nil
}
// GetStartIndices returns the start indices of all the markers in the pattern
func (c SubstitutionCreator) GetStartIndices() (map[int]string, error) {
inds := make(map[int]string)
p := c.Pattern
for _, value := range c.Values {
m := value.Marker
found := false
for i, _ := range p {
if strings.HasPrefix(p[i:], m) {
inds[i] = m
found = true
}
}
if !found {
return nil, errors.Errorf("Unable to find marker " + m + " in the pattern")
}
}
return inds, nil
}
// lenToNextMarker takes in the indices map, an index and returns the distance to
// next greater index
func lenToNextMarker(m map[int]string, j int) int {
res := math.MaxInt32
for k, _ := range m {
if k > j {
res = min(k-j, res)
}
}
return res
}
func min(a int, b int) int {
if a<b {
return a
}
return b
}

View File

@@ -1,60 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package setters2
import (
"io/ioutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
type SubstitutionDefinition struct {
Name string `yaml:"name"`
Pattern string `yaml:"pattern"`
Values []Value `yaml:"value"`
}
type Value struct {
Marker string `yaml:"marker"`
Ref string `yaml:"ref"`
}
func (subd SubstitutionDefinition) AddSubstitutionToFile(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(subd); err != nil {
return err
}
out, err := y.String()
if err != nil {
return err
}
if err := ioutil.WriteFile(path, []byte(out), 0666); err != nil {
return err
}
return nil
}
func (subd SubstitutionDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) {
key := DefinitionPrefix + subd.Name
def, err := object.Pipe(yaml.LookupCreate(
yaml.MappingNode, "openAPI", "definitions", key, "x-k8s-cli", "substitution"))
if err != nil {
return nil, err
}
if err := def.PipeE(yaml.FieldSetter{Name: "name", StringValue: subd.Name}); err != nil {
return nil, err
}
if err := def.PipeE(yaml.FieldSetter{Name: "pattern", StringValue: subd.Pattern}); err != nil {
return nil, err
}
return object, nil
}

View File

@@ -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"
)
func TestAddUpdateSubstitution(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()
}
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.AddSubstitutionToFile(path)
if !assert.NoError(t, err) {
t.FailNow()
}
// update setter definition
subd2 := SubstitutionDefinition{
Name: "image",
Pattern: "IMAGE_NAME:IMAGE_TAG2",
}
err = subd2.AddSubstitutionToFile(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:
substitution:
name: image
pattern: IMAGE_NAME:IMAGE_TAG2
`
assert.Equal(t, expected, string(b))
}

View File

@@ -6,6 +6,7 @@ package yaml
import (
"bytes"
"fmt"
"io/ioutil"
"strings"
"gopkg.in/yaml.v3"
@@ -99,6 +100,42 @@ func Parse(value string) (*RNode, error) {
return Parser{Value: value}.Filter(nil)
}
// ReadFile parses a single Resource from a yaml file
func ReadFile(path string) (*RNode, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return Parse(string(b))
}
// WriteFile writes a single Resource to a yaml file
func WriteFile(node *RNode, path string) error {
out, err := node.String()
if err != nil {
return err
}
return ioutil.WriteFile(path, []byte(out), 0600)
}
// UpdateFile reads the file at path, applies the filter to it, and write the result back.
// path must contain a exactly 1 resource (YAML).
func UpdateFile(filter Filter, path string) error {
// Read the yaml
y, err := ReadFile(path)
if err != nil {
return err
}
// Update the yaml
if err := y.PipeE(filter); err != nil {
return err
}
// Write the yaml
return WriteFile(y, path)
}
// TODO(pwittrock): test this
func GetStyle(styles ...string) Style {
var style Style