Compare commits

..

38 Commits

Author SHA1 Message Date
Kubernetes Prow Robot
49ebb3f155 Merge pull request #2236 from pwittrock/parse
Setters type support
2020-02-27 12:06:38 -08:00
Phillip Wittrock
fa507f782f Setters: support for explicit setter typing
- ensure OpenAPI definitions always uses strings for setter values
- allow the field type to be defined -- integer,boolean,string
- format values using yaml 1.1 compatibility
2020-02-27 11:51:18 -08:00
Phillip Wittrock
da548f65ea fixup go.sum 2020-02-27 11:48:24 -08:00
Kubernetes Prow Robot
8991b193c6 Merge pull request #2232 from pwittrock/setby
Setters: support for enums
2020-02-26 20:33:19 -08:00
Phillip Wittrock
3c776b3435 fix setter clear set-by test 2020-02-26 20:25:05 -08:00
Phillip Wittrock
cf61a360e0 Support for enum mappings in setters 2020-02-26 20:13:06 -08:00
Kubernetes Prow Robot
a588f498ea Merge pull request #2228 from pwittrock/setby
Setters: clear set-by if unspecified when setting a value
2020-02-26 14:23:19 -08:00
Phillip Wittrock
573d7b7234 Setters: clear set-by if unspecified when setting a value 2020-02-26 13:53:47 -08:00
Kubernetes Prow Robot
1d94ee86df Merge pull request #2219 from phanimarupaka/substitutions
Create setters for substitutions
2020-02-26 13:41:19 -08:00
Phani Teja Marupaka
275bf05ae2 Refactoring and Table tests 2020-02-26 11:37:14 -08:00
Phani Teja Marupaka
d70f3a7958 Update go files 2020-02-25 14:09:37 -08:00
Kubernetes Prow Robot
5707962df5 Merge pull request #2224 from monopole/disableTravis
Disable travis testing now that prow works.
2020-02-25 11:52:50 -08:00
Jeffrey Regan
63c06eeaf1 Disable travis testing now that prow works. 2020-02-25 11:38:36 -08:00
Kubernetes Prow Robot
b3ce3a7882 Merge pull request #2213 from vanou/correct_explanation_of_ApendAll_method_in_ResMap_interface
Correct explanation of ApendAll method in ResMap interface
2020-02-24 16:25:35 -08:00
Phani Teja Marupaka
a8b5ec2c61 Suggested Changes and Unit Tests 2020-02-24 12:35:14 -08:00
Kubernetes Prow Robot
d190e1c18a Merge pull request #2220 from kubernetes-sigs/monopole-patch-1
Add lint-kustomize to prow-presubmit-check
2020-02-24 10:42:50 -08:00
Jeff Regan
96ac25fff5 Add lint-kustomize to prow-presubmit-check
To do tool installs.
2020-02-24 10:17:52 -08:00
Phani Teja Marupaka
1d988a0fd8 Merge branch 'master' into substitutions 2020-02-24 09:39:22 -08:00
Phillip Wittrock
6f176b1507 Merge pull request #2203 from pwittrock/setter-wiring
Manually merging since the prow automation does not appear to be configured correctly
2020-02-24 09:23:00 -08:00
Phillip Wittrock
d2f0b1b345 Merge pull request #2218 from pwittrock/master
Update kyaml to v0.0.13
2020-02-21 14:35:25 -08:00
Phillip Wittrock
c95a40933b Update kyaml to v0.0.13 2020-02-21 10:18:52 -08:00
Phillip Wittrock
bc7b880ab1 Merge pull request #2211 from pwittrock/master
Make GetOpenAPIFile publicly settable
2020-02-21 10:16:21 -08:00
Phillip Wittrock
e004c31700 Make GetOpenAPIFile publicly settable 2020-02-21 08:09:48 -08:00
Phillip Wittrock
ca9aa62c26 Merge pull request #2216 from pwittrock/version
release cmd/config and kyaml 0.0.12
2020-02-21 08:09:08 -08:00
Phillip Wittrock
cee7cb6589 release cmd/config and kyaml 0.0.12 2020-02-19 15:01:14 -08:00
Phillip Wittrock
4f905c9cff Merge pull request #2203 from pwittrock/setter-wiring
Manually merging since the prow automation does not appear to be configured correctly
2020-02-19 14:55:09 -08:00
Phillip Wittrock
232c1c8ee9 write create-substitution into command 2020-02-19 14:25:38 -08:00
Phillip Wittrock
61cf3e6ec5 wire set 2.0 command 2020-02-19 14:25:38 -08:00
Phillip Wittrock
1ce469f1fd stop printing expected error message in fmt command test 2020-02-19 14:25:38 -08:00
Phillip Wittrock
a49c9de4a4 wire create-setter 2.0 into command 2020-02-19 14:25:38 -08:00
Phillip Wittrock
bada055cd3 wire list-setters 2.0 into command 2020-02-19 14:25:38 -08:00
Phillip Wittrock
d7e0b1ac31 setter utilities for simplifying commands 2020-02-19 14:25:38 -08:00
Phillip Wittrock
5549035b69 support for listing setters 2020-02-19 14:25:38 -08:00
Phillip Wittrock
025200cc12 support for adding setter substitution
- refactor add setter to include file updates
- support add substitution file updates
2020-02-19 14:25:38 -08:00
Phillip Wittrock
154939803f better support for reading / writing single resource yaml files 2020-02-19 08:16:07 -08:00
Phillip Wittrock
64c30a0678 fix nil dereference issue in fieldmeta 2020-02-19 08:13:26 -08:00
Phillip Wittrock
b7bef5dc44 openapi support for loading definitions from a file 2020-02-19 08:13:26 -08:00
vanou
87b680e1c0 Correct explanation of ApendAll method in ResMap interface
This commit corrects expalation of ApendAll method in ResMap
interface. AppendAll method should fail on CurId collision,
not OrgId collision.
2020-02-19 23:53:10 +09:00
38 changed files with 3311 additions and 459 deletions

View File

@@ -1,23 +0,0 @@
name: Go
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build
run: ./travis/kyaml-pre-commit.sh
- name: Check
run: ./travis/check-go-mod.sh

View File

@@ -1,43 +0,0 @@
os:
- linux
- osx
# TODO: Speed up the slowness of the osx travis runs
# Maybe cache brew installs?
#
# TODO: Uncomment when some gets the tests running on Windows.
# - windows
addons:
apt:
packages:
- tree
homebrew:
packages:
- tree
update: true
# Only clone the most recent commit.
git:
depth: 1
language: go
go:
- "1.13"
go_import_path: sigs.k8s.io/kustomize
before_install:
- source ./travis/consider-early-travis-exit.sh
# Skip the install process; let pre-commit.sh do it.
install: true
script:
- make verify-kustomize
- ./travis/kyaml-pre-commit.sh
- ./travis/check-go-mod.sh
# TBD. Suppressing for now.
notifications:
email: false

View File

@@ -21,6 +21,7 @@ verify-kustomize: \
# https://github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/kustomize
.PHONY: prow-presubmit-check
prow-presubmit-check: \
lint-kustomize \
test-unit-kustomize-all
.PHONY: verify-kustomize-e2e

View File

@@ -114,7 +114,7 @@ type ResMap interface {
Append(*resource.Resource) error
// AppendAll appends another ResMap to self,
// failing on any OrgId collision.
// failing on any CurId collision.
AppendAll(ResMap) error
// AbsorbAll appends, replaces or merges the contents

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

12
cmd/config/ext/ext.go Normal file
View File

@@ -0,0 +1,12 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package ext
import "path/filepath"
// GetOpenAPIFile returns the path to the file containing supplementary OpenAPI definitions.
// Maybe be overridden to configure which file to read OpenAPI definitions from.
var GetOpenAPIFile = func(args []string) (string, error) {
return filepath.Join(args[0], "kustomization"), nil
}

View File

@@ -156,8 +156,10 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d h1:LCPbGQ34PMrwad11aMZ+dbz5SAsq/0ySjRwQ8I9Qwd8=
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 h1:XZx7nhd5GMaZpmDaEHFVafUZC7ya0fuo7cSJ3UCKYmM=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.17.0 h1:xRBnuie9rXcPxUkDizUsGvPf1cnlZCFu210op7J7LJo=
k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=

View File

@@ -5,9 +5,11 @@ package commands
import (
"github.com/spf13/cobra"
"sigs.k8s.io/kustomize/cmd/config/ext"
"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 +25,27 @@ 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.")
"OpenAPI field type for the setter -- e.g. integer,boolean,string.")
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,41 @@ 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 = ext.GetOpenAPIFile(args)
r.CreateSetter.Description = r.Set.SetPartialField.Description
r.CreateSetter.SetBy = r.Set.SetPartialField.SetBy
r.CreateSetter.Type = r.Set.SetPartialField.Type
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,131 @@
// 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/ext"
"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()
}
old := ext.GetOpenAPIFile
defer func() { ext.GetOpenAPIFile = old }()
ext.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,81 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package commands
import (
"strings"
"github.com/spf13/cobra"
"sigs.k8s.io/kustomize/cmd/config/ext"
"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 = ext.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,240 @@
// 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/ext"
"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/nginxotherthing", "--pattern", "something/IMAGE::TAG/nginxotherthing",
"--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/nginxotherthing
- 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/nginxotherthing
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/nginxotherthing # {"$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()
}
old := ext.GetOpenAPIFile
defer func() { ext.GetOpenAPIFile = old }()
ext.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,16 @@
package commands
import (
"fmt"
"io"
"os"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"sigs.k8s.io/kustomize/cmd/config/ext"
"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 +40,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 := ext.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,299 @@
// 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/ext"
"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())
old := ext.GetOpenAPIFile
defer func() { ext.GetOpenAPIFile = old }()
ext.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

@@ -9,16 +9,18 @@ import (
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"sigs.k8s.io/kustomize/cmd/config/ext"
"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,44 @@ 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
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 +83,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 = ext.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,277 @@
// 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/ext"
"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"
`,
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()
}
old := ext.GetOpenAPIFile
defer func() { ext.GetOpenAPIFile = old }()
ext.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

@@ -9,6 +9,7 @@ require (
github.com/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.4.0
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca
gopkg.in/yaml.v2 v2.2.4
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 // indirect
gopkg.in/yaml.v2 v2.2.7
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2
)

View File

@@ -39,18 +39,20 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d h1:LCPbGQ34PMrwad11aMZ+dbz5SAsq/0ySjRwQ8I9Qwd8=
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 h1:XZx7nhd5GMaZpmDaEHFVafUZC7ya0fuo7cSJ3UCKYmM=
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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,148 @@ 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"`
// Type is the type of the setter value.
Type string `yaml:"type,omitempty"`
// EnumValues is a map of possible setter values to actual field values.
// If EnumValues is specified, then the value set the by user 1) MUST
// be present in the enumValues map as a key, and 2) the map entry value
// MUST be used as the value to set in the configuration (rather than the key)
// Example -- may be used for t-shirt sizing values by allowing cpu to be
// set to small, medium or large, and then mapping these values to cpu values -- 0.5, 2, 8
EnumValues map[string]string `yaml:"enumValues,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 = ""
}
if sd.Type != "" {
err = def.PipeE(yaml.FieldSetter{Name: "type", StringValue: sd.Type})
if err != nil {
return nil, err
}
// don't write the type to the extension
sd.Type = ""
}
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,9 +28,9 @@ 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)
ext, sch, err := getExtFromComment(object)
if err != nil {
return err
}
@@ -36,21 +39,25 @@ func (s *Set) visitScalar(object *yaml.RNode, _ string) error {
}
// perform a direct set of the field if it matches
if s.set(object, ext) {
if s.set(object, ext, sch) {
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, sch)
if err != nil {
return err
}
if sub {
s.Count++
}
return nil
}
// substitute updates the value of field from ext if ext contains a substitution that
// depends on a setter whose name matches s.Name.
func (s *Set) substitute(field *yaml.RNode, ext *cliExtension) (bool, error) {
func (s *Set) substitute(field *yaml.RNode, ext *cliExtension, _ *spec.Schema) (bool, error) {
nameMatch := false
// check partial setters to see if they contain the setter as part of a
@@ -79,8 +86,15 @@ func (s *Set) substitute(field *yaml.RNode, ext *cliExtension) (bool, error) {
if err != nil {
return false, errors.Wrap(err)
}
// substitute the setters current value into the substitution pattern
p = strings.ReplaceAll(p, v.Marker, subSetter.Setter.Value)
if val, found := subSetter.Setter.EnumValues[subSetter.Setter.Value]; found {
// the setter has an enum-map. we should replace the marker with the
// enum value looked up from the map rather than the enum key
p = strings.ReplaceAll(p, v.Marker, val)
} else {
// substitute the setters current value into the substitution pattern
p = strings.ReplaceAll(p, v.Marker, subSetter.Setter.Value)
}
if subSetter.Setter.Name == s.Name {
// the substitution depends on the specified setter
@@ -95,11 +109,15 @@ func (s *Set) substitute(field *yaml.RNode, ext *cliExtension) (bool, error) {
// TODO(pwittrock): validate the field value
field.YNode().Value = p
// substitutions are always strings
field.YNode().Tag = "!!str"
return true, nil
}
// set applies the value from ext to field if its name matches s.Name
func (s *Set) set(field *yaml.RNode, ext *cliExtension) bool {
func (s *Set) set(field *yaml.RNode, ext *cliExtension, sch *spec.Schema) bool {
// check full setter
if ext.Setter == nil || ext.Setter.Name != s.Name {
return false
@@ -107,7 +125,104 @@ func (s *Set) set(field *yaml.RNode, ext *cliExtension) bool {
// TODO(pwittrock): validate the field value
if val, found := ext.Setter.EnumValues[ext.Setter.Value]; found {
// the setter has an enum-map. we should replace the marker with the
// enum value looked up from the map rather than the enum key
field.YNode().Value = val
return true
}
// this has a full setter, set its value
field.YNode().Value = ext.Setter.Value
// format the node so it is quoted if it is a string
yaml.FormatNonStringStyle(field.YNode(), *sch)
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 the setter contains an enumValues map, then ensure the set value appears
// as a key in the map
if values, err := def.Pipe(yaml.Lookup("enumValues")); err != nil {
// error looking up the enumValues
return nil, err
} else if values != nil {
// contains enumValues map -- validate the set value against the map entries
// get the enumValues keys
fields, err := values.Fields()
if err != nil {
return nil, err
}
// search for the user provided value in the set of allowed values
var match bool
for i := range fields {
if fields[i] == s.Value {
// found a match, we are good
match = true
break
}
}
if !match {
// no match found -- provide an informative error to the user
return nil, errors.Errorf("%s does not match the possible values for %s: [%s]",
s.Value, s.Name, strings.Join(fields, ","))
}
}
v := yaml.NewScalarRNode(s.Value)
// values are always represented as strings the OpenAPI
// since the are unmarshalled into strings. Use double quote style to
// ensure this consistently.
v.YNode().Tag = "!!str"
v.YNode().Style = yaml.DoubleQuotedStyle
if err := def.PipeE(&yaml.FieldSetter{Name: "value", Value: v}); err != nil {
return nil, err
}
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

@@ -15,11 +15,12 @@ import (
func TestSet_Filter(t *testing.T) {
var tests = []struct {
name string
setter string
openapi string
input string
expected string
name string
description string
setter string
openapi string
input string
expected string
}{
{
name: "set-replicas",
@@ -58,6 +59,184 @@ metadata:
name: nginx-deployment
spec:
replicas: 4 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
`,
},
{
name: "set-foo-type",
description: "if a type is specified for a setter, ensure the field is properly quoted",
setter: "foo",
openapi: `
openAPI:
definitions:
io.k8s.cli.setters.foo:
x-k8s-cli:
setter:
name: foo
value: "4"
type: string
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
foo: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.foo"}
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
foo: "4" # {"$ref": "#/definitions/io.k8s.cli.setters.foo"}
`,
},
{
name: "set-foo-type-wrong",
description: "if a type is specified for a setter, for the field to the type",
setter: "foo",
openapi: `
openAPI:
definitions:
io.k8s.cli.setters.foo:
x-k8s-cli:
setter:
name: foo
value: "4"
type: boolean
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
foo: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.foo"}
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
foo: !!bool 4 # {"$ref": "#/definitions/io.k8s.cli.setters.foo"}
`,
},
{
name: "set-foo-no-type",
description: "if a type is not specified for a setter, keep the existing quoting",
setter: "foo",
openapi: `
openAPI:
definitions:
io.k8s.cli.setters.foo:
x-k8s-cli:
setter:
name: foo
value: "4"
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
foo: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.foo"}
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
annotations:
foo: 4 # {"$ref": "#/definitions/io.k8s.cli.setters.foo"}
`,
},
{
name: "set-replicas-enum",
setter: "replicas",
openapi: `
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: "medium"
enumValues:
small: "1"
medium: "5"
large: "50"
io.k8s.cli.setters.no-match-2':
x-k8s-cli:
setter:
name: no-match-2
value: "2"
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 5 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
`,
},
{
name: "set-replicas-enum-large",
setter: "replicas",
openapi: `
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: "large"
enumValues:
small: "1"
medium: "5"
large: "50"
io.k8s.cli.setters.no-match-2':
x-k8s-cli:
setter:
name: no-match-2
value: "2"
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 1 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 50 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
`,
},
{
@@ -155,6 +334,61 @@ spec:
containers:
- name: nginx
image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
`,
},
{
name: "substitute-image-name-enum",
setter: "image-tag",
openapi: `
openAPI:
definitions:
io.k8s.cli.setters.image-name:
x-k8s-cli:
setter:
name: image-name
value: "helloworld"
enumValues:
nginx: gcr.io/nginx
helloworld: us.gcr.io/helloworld
io.k8s.cli.setters.image-tag:
x-k8s-cli:
setter:
name: image-tag
value: "1.8.1"
io.k8s.cli.substitutions.image:
x-k8s-cli:
substitution:
name: image
pattern: IMAGE_NAME:IMAGE_TAG
values:
- marker: "IMAGE_NAME"
ref: "#/definitions/io.k8s.cli.setters.image-name"
- marker: "IMAGE_TAG"
ref: "#/definitions/io.k8s.cli.setters.image-tag"
`,
input: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
`,
expected: `
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
template:
spec:
containers:
- name: nginx
image: us.gcr.io/helloworld:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
`,
},
{
@@ -438,3 +672,372 @@ 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-annotation-quoted",
setter: "replicas",
value: "3",
input: `
openAPI:
definitions:
io.k8s.cli.setters.replicas:
x-k8s-cli:
setter:
name: replicas
value: 4
`,
expected: `
openAPI:
definitions:
io.k8s.cli.setters.replicas:
x-k8s-cli:
setter:
name: replicas
value: "3"
`,
},
{
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: "set-replicas-set-by-empty",
setter: "replicas",
value: "3",
input: `
openAPI:
definitions:
io.k8s.cli.setters.no-match-1':
x-k8s-cli:
setter:
name: no-match-1
value: "1"
setBy: "package-default"
io.k8s.cli.setters.replicas:
x-k8s-cli:
setter:
name: replicas
value: "4"
setBy: "package-default"
io.k8s.cli.setters.no-match-2':
x-k8s-cli:
setter:
name: no-match-2
value: "2"
setBy: "package-default"
`,
expected: `
openAPI:
definitions:
io.k8s.cli.setters.no-match-1':
x-k8s-cli:
setter:
name: no-match-1
value: "1"
setBy: "package-default"
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"
setBy: "package-default"
`,
},
{
name: "set-replicas-with-enum",
setter: "replicas",
value: "baz",
input: `
openAPI:
definitions:
io.k8s.cli.setters.no-match-1':
x-k8s-cli:
setter:
name: no-match-1
value: "1"
setBy: "package-default"
io.k8s.cli.setters.replicas:
x-k8s-cli:
setter:
name: replicas
value: "foo"
enumValues:
foo: bar
baz: biz
io.k8s.cli.setters.no-match-2':
x-k8s-cli:
setter:
name: no-match-2
value: "2"
setBy: "package-default"
`,
expected: `
openAPI:
definitions:
io.k8s.cli.setters.no-match-1':
x-k8s-cli:
setter:
name: no-match-1
value: "1"
setBy: "package-default"
io.k8s.cli.setters.replicas:
x-k8s-cli:
setter:
name: replicas
value: "baz"
enumValues:
foo: bar
baz: biz
io.k8s.cli.setters.no-match-2':
x-k8s-cli:
setter:
name: no-match-2
value: "2"
setBy: "package-default"
`,
},
{
name: "set-replicas-fail",
setter: "replicas",
value: "hello",
input: `
openAPI:
definitions:
io.k8s.cli.setters.no-match-1':
x-k8s-cli:
setter:
name: no-match-1
value: "1"
setBy: "package-default"
io.k8s.cli.setters.replicas:
x-k8s-cli:
setter:
name: replicas
value: "foo"
enumValues:
foo: bar
baz: biz
io.k8s.cli.setters.no-match-2':
x-k8s-cli:
setter:
name: no-match-2
value: "2"
setBy: "package-default"
`,
err: "hello does not match the possible values for replicas: [foo,baz]",
},
{
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,64 @@
// 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
Type 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,
Type: c.Type,
}
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,258 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"strings"
"sigs.k8s.io/kustomize/kyaml/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 {
y, err := yaml.ReadFile(openAPIPath)
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.TrimPrefix(value.Ref, "#/definitions/io.k8s.cli.setters."),
Value: m[value.Marker],
}
err := sd.AddToFile(openAPIPath)
if err != nil {
return err
}
}
}
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
}
fv := c.FieldValue
pattern := c.Pattern
fvInd := 0
patternInd := 0
// iterate fv, pattern with indices fvInd, patternInd respectively and when patternInd hits the index of a marker,
// freeze patternInd and iterate fvInd and capture string till we find the substring just after current marker
// and before next marker
// Ex: fv = "something/ubuntu:0.1.0", pattern = "something/IMAGE:VERSION", till patternInd reaches 10
// just proceed fvInd and patternInd and check if fv[fvInd]==pattern[patternInd] when patternInd is 10,
// freeze patternInd and move fvInd till it sees substring ':' which derives IMAGE = ubuntu and so on.
for fvInd < len(fv) && patternInd < len(pattern) {
// if we hit marker index, extract its corresponding value
if marker, ok := indices[patternInd]; ok {
// increment the patternInd to end of marker. This helps us to extract the substring before next marker.
patternInd += len(marker)
var value string
if value, fvInd, err = c.extractValueForMarker(fvInd, fv, patternInd, indices); err != nil {
return nil, err
}
// 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(
"marker %s is found to have different values %s and %s", marker, prevValue, value)
}
m[marker] = value
} else {
// Ex: fv = "samething/ubuntu:0.1.0" pattern = "something/IMAGE:VERSION". Error out at 'a' in fv.
if fv[fvInd] != pattern[patternInd] {
return nil, errors.Errorf(
"unable to derive values for markers, " +
"create setters for all markers and then try again")
}
fvInd++
patternInd++
}
}
// check if both strings are completely visited or throw error
if fvInd < len(fv) || patternInd < len(pattern) {
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) {
indices := make(map[int]string)
for _, value := range c.Values {
found := false
for i := range c.Pattern {
if strings.HasPrefix(c.Pattern[i:], value.Marker) {
indices[i] = value.Marker
found = true
}
}
if !found {
return nil, errors.Errorf("unable to find marker " + value.Marker + " in the pattern")
}
}
if err := validateMarkers(indices); err != nil {
return nil, err
}
return indices, nil
}
// validateMarkers takes the indices map, checks if any of 2 markers not have delimiters,
// checks if any marker is substring of other and returns error
func validateMarkers(indices map[int]string) error {
for k1, v1 := range indices {
for k2, v2 := range indices {
if k1 != k2 && k1+len(v1) == k2 {
return errors.Errorf(
"markers %s and %s are found to have no delimiters between them,"+
" pre-create setters and try again", v1, v2)
}
if v1 != v2 && strings.Contains(v1, v2) {
return errors.Errorf(
"markers %s is substring of %s,"+
" no marker should be substring of other", v2, v1)
}
}
}
return nil
}
// extractValueForMarker returns the value string for a marker and the incremented index
func (c SubstitutionCreator) extractValueForMarker(fvInd int, fv string, patternInd int, indices map[int]string) (string, int, error) {
nonMarkerStr := strTillNextMarker(indices, patternInd, c.Pattern)
// return the remaining string of fv till end if patternInd is at end of pattern
if patternInd == len(c.Pattern) {
return fv[fvInd:], len(fv), nil
}
// split remaining fv starting from fvInd with the non marker substring delimiter and get the first value
// In example fv = "something/ubuntu::0.1.0", pattern = "something/IMAGE::VERSION",
// split with "::" delimiter in fv which gives markerValue = ubuntu for marker IMAGE
// increment fvInd by length of extracted marker value and return fvInd
if markerValues := strings.Split(fv[fvInd:], nonMarkerStr); len(markerValues) > 0 {
return markerValues[0], fvInd + len(markerValues[0]), nil
}
return "", -1, errors.Errorf(
"unable to derive values for markers," +
" create setters for all markers and then try again")
}
// substrOfLen takes a string, start index and length and returns substring of given length
// or till end of string
func substrOfLen(str string, startInd int, length int) string {
return str[startInd:min(len(str), startInd+length)]
}
// strTillNextMarker takes in the indices map, a start index and returns the substring till
// start of next marker
func strTillNextMarker(indices map[int]string, startInd int, pattern string) string {
// initialize with max value which is length of pattern
nextMarkerStartInd := len(pattern)
for ind := range indices {
if ind > startInd {
nextMarkerStartInd = min(ind-startInd, nextMarkerStartInd)
}
}
return substrOfLen(pattern, startInd, nextMarkerStartInd)
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -0,0 +1,111 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package settersutil
import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/setters2"
)
func TestGetValuesForMarkers(t *testing.T) {
var tests = []struct {
name string
pattern string
fieldValue string
markers []string
expectedError error
expectedOutput map[string]string
}{
{
name: "positive example",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE::VERSION/otherthing/IMAGE::VERSION/",
fieldValue: "something/nginx::0.1.0/otherthing/nginx::0.1.0/",
expectedOutput: map[string]string{"IMAGE": "nginx", "VERSION": "0.1.0"},
},
{
name: "marker with different values",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE:VERSION/IMAGE",
fieldValue: "something/nginx:0.1.0/ubuntu",
expectedError: errors.Errorf("marker IMAGE is found to have different values nginx and ubuntu"),
},
{
name: "unmatched pattern",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE:VERSION",
fieldValue: "otherthing/nginx:0.1.0",
expectedError: errors.Errorf("unable to derive values for markers"),
},
{
name: "unmatched pattern at the end",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE:VERSION/abc",
fieldValue: "something/nginx:0.1.0/abcd",
expectedError: errors.Errorf("unable to derive values for markers"),
},
{
name: "substring markers",
markers: []string{"IMAGE", "VERSION", "MAGE"},
pattern: "something/IMAGE:VERSION/abc/MAGE",
fieldValue: "something/nginx:0.1.0/abc/ubuntu",
expectedError: errors.Errorf("no marker should be substring of other"),
},
{
name: "markers with no delimiters",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGEVERSION/",
fieldValue: "something/nginx0.1.0/",
expectedError: errors.Errorf("no delimiters between them"),
},
{
name: "unmatched delimiter",
markers: []string{"IMAGE", "VERSION"},
pattern: "something/IMAGE:^VERSION/otherthing/IMAGE::VERSION/",
fieldValue: "something/nginx::0.1.0/otherthing/nginx::0.1.0/",
expectedError: errors.Errorf("unable to derive values for markers"),
},
}
for i := range tests {
test := tests[i]
t.Run(test.name, func(t *testing.T) {
values := []setters2.Value{}
for _, marker := range test.markers {
value := setters2.Value{
Marker: marker,
}
values = append(values, value)
}
sc := SubstitutionCreator{
Pattern: test.pattern,
Values: values,
FieldValue: test.fieldValue,
}
m, err := sc.GetValuesForMarkers()
if test.expectedError == nil {
// fail if expectedError is nil but actual error is not
if !assert.NoError(t, err) {
t.FailNow()
}
// check if all the expected markers and values are present in actual map
for k, v := range test.expectedOutput {
if val, ok := m[k]; ok {
assert.Equal(t, v, val)
} else {
t.FailNow()
}
}
} else {
//if expectedError is not nil, check for correctness of error message
assert.Contains(t, err.Error(), test.expectedError.Error())
}
})
}
}

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

@@ -19,8 +19,9 @@ type cliExtension struct {
}
type setter struct {
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
Name string `yaml:"name,omitempty" json:"name,omitempty"`
Value string `yaml:"value,omitempty" json:"value,omitempty"`
EnumValues map[string]string `yaml:"enumValues,omitempty" json:"enumValues,omitempty"`
}
type substitution struct {
@@ -56,32 +57,32 @@ func getExtFromSchema(schema *spec.Schema) (*cliExtension, error) {
// getExtFromComment returns the cliExtension openAPI extension if it is present as
// a comment on the field.
func getExtFromComment(object *yaml.RNode) (*cliExtension, error) {
func getExtFromComment(object *yaml.RNode) (*cliExtension, *spec.Schema, error) {
// TODO(pwittrock): also use path to the field to get openapi, not just comments
// parse comment containing the extended openapi for this field
fm := fieldmeta.FieldMeta{}
if err := fm.Read(object); err != nil {
return nil, errors.Wrap(err)
return nil, nil, errors.Wrap(err)
}
if fm.Schema.Ref.String() == "" {
return nil, nil
return nil, nil, nil
}
// resolve the comment reference to the extended openapi definitions
r, err := openapi.Resolve(&fm.Schema.Ref)
if err != nil {
return nil, errors.Wrap(err)
return nil, nil, errors.Wrap(err)
}
if r == nil {
// no schema found
// TODO(pwittrock): should this be an error if it doesn't resolve?
return nil, nil
return nil, nil, nil
}
// get the cli extension from the openapi (contains setter information)
ext, err := getExtFromSchema(r)
if err != nil {
return nil, errors.Wrap(err)
return nil, nil, errors.Wrap(err)
}
return ext, nil
return ext, r, nil
}

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

View File

@@ -6,7 +6,7 @@
# kyaml version
export kyaml_major=0
export kyaml_minor=0
export kyaml_patch=11
export kyaml_patch=13
# kstatus version
export kstatus_major=0
@@ -21,7 +21,7 @@ export api_patch=2
# cmd/config version
export cmd_config_major=0
export cmd_config_minor=0
export cmd_config_patch=11
export cmd_config_patch=13
# cmd/kubectl version
export cmd_kubectl_major=0