diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 99941c685..000000000 --- a/.travis.yml +++ /dev/null @@ -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 diff --git a/Makefile b/Makefile index f11f0a7d3..763f8037e 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/api/resmap/resmap.go b/api/resmap/resmap.go index c1bd8910c..380dbe79d 100644 --- a/api/resmap/resmap.go +++ b/api/resmap/resmap.go @@ -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 diff --git a/cmd/config/go.sum b/cmd/config/go.sum index 1f60f6909..4b4b73455 100644 --- a/cmd/config/go.sum +++ b/cmd/config/go.sum @@ -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= diff --git a/cmd/config/internal/commands/cmdcreatesetter.go b/cmd/config/internal/commands/cmdcreatesetter.go index 9bcbe03d0..ec4297eb3 100644 --- a/cmd/config/internal/commands/cmdcreatesetter.go +++ b/cmd/config/internal/commands/cmdcreatesetter.go @@ -38,8 +38,7 @@ func NewCreateSetterRunner(parent string) *CreateSetterRunner { "kind of the Resource on which to create the setter.") set.Flags().MarkHidden("kind") set.Flags().StringVar(&r.Set.SetPartialField.Type, "type", "", - "valid OpenAPI field type -- e.g. integer,boolean,string.") - set.Flags().MarkHidden("type") + "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") @@ -89,6 +88,7 @@ func (r *CreateSetterRunner) preRunE(c *cobra.Command, args []string) 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 } diff --git a/cmd/config/internal/commands/cmdcreatesubstitution_test.go b/cmd/config/internal/commands/cmdcreatesubstitution_test.go index 86e80cbe9..bc014d9c2 100644 --- a/cmd/config/internal/commands/cmdcreatesubstitution_test.go +++ b/cmd/config/internal/commands/cmdcreatesubstitution_test.go @@ -102,6 +102,72 @@ spec: 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 `, }, } diff --git a/cmd/config/internal/commands/cmdset_test.go b/cmd/config/internal/commands/cmdset_test.go index f617660b7..289bcfa95 100644 --- a/cmd/config/internal/commands/cmdset_test.go +++ b/cmd/config/internal/commands/cmdset_test.go @@ -109,7 +109,6 @@ openAPI: setter: name: replicas value: "4" - setBy: me `, expectedResources: ` apiVersion: apps/v1 diff --git a/kyaml/go.mod b/kyaml/go.mod index de2170cd8..33eefaad1 100644 --- a/kyaml/go.mod +++ b/kyaml/go.mod @@ -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 ) diff --git a/kyaml/go.sum b/kyaml/go.sum index 98d3f4c1c..05b1b96aa 100644 --- a/kyaml/go.sum +++ b/kyaml/go.sum @@ -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= diff --git a/kyaml/setters2/add.go b/kyaml/setters2/add.go index 8bf7cc49d..83575c070 100644 --- a/kyaml/setters2/add.go +++ b/kyaml/setters2/add.go @@ -104,6 +104,17 @@ type SetterDefinition struct { // 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 { @@ -127,6 +138,15 @@ func (sd SetterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) { 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 diff --git a/kyaml/setters2/set.go b/kyaml/setters2/set.go index ec242d805..b75f61d56 100644 --- a/kyaml/setters2/set.go +++ b/kyaml/setters2/set.go @@ -30,7 +30,7 @@ func (s *Set) Filter(object *yaml.RNode) (*yaml.RNode, error) { // visitScalar 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 } @@ -39,13 +39,13 @@ func (s *Set) visitScalar(object *yaml.RNode, p 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 - sub, err := s.substitute(object, ext) + sub, err := s.substitute(object, ext, sch) if err != nil { return err } @@ -57,7 +57,7 @@ func (s *Set) visitScalar(object *yaml.RNode, p string) error { // 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 @@ -86,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 @@ -102,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 @@ -114,8 +125,18 @@ 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 } @@ -146,14 +167,50 @@ func (s SetOpenAPI) Filter(object *yaml.RNode) (*yaml.RNode, error) { if def == nil { return nil, errors.Errorf("no setter %s found", s.Name) } - if err := def.PipeE(&yaml.FieldSetter{Name: "value", StringValue: s.Value}); err != nil { + + // 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 s.SetBy != "" { - if err := def.PipeE(&yaml.FieldSetter{Name: "setBy", StringValue: s.SetBy}); err != nil { - return nil, err - } + if err := def.PipeE(&yaml.FieldSetter{Name: "setBy", StringValue: s.SetBy}); err != nil { + return nil, err } if s.Description != "" { diff --git a/kyaml/setters2/set_test.go b/kyaml/setters2/set_test.go index 1550a6244..3414cf10d 100644 --- a/kyaml/setters2/set_test.go +++ b/kyaml/setters2/set_test.go @@ -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"} `, }, { @@ -491,6 +725,29 @@ openAPI: 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" `, }, { @@ -583,6 +840,137 @@ openAPI: 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", diff --git a/kyaml/setters2/settersutil/settercreator.go b/kyaml/setters2/settersutil/settercreator.go index 121be30ce..8b6221335 100644 --- a/kyaml/setters2/settersutil/settercreator.go +++ b/kyaml/setters2/settersutil/settercreator.go @@ -19,6 +19,8 @@ type SetterCreator struct { 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 -- @@ -35,7 +37,9 @@ type SetterCreator struct { 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} + Name: c.Name, Value: c.FieldValue, Description: c.Description, SetBy: c.SetBy, + Type: c.Type, + } if err := sd.AddToFile(openAPIPath); err != nil { return err } diff --git a/kyaml/setters2/settersutil/substitutioncreator.go b/kyaml/setters2/settersutil/substitutioncreator.go index 094f34d1c..fc187c63e 100644 --- a/kyaml/setters2/settersutil/substitutioncreator.go +++ b/kyaml/setters2/settersutil/substitutioncreator.go @@ -4,9 +4,13 @@ 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 @@ -40,6 +44,12 @@ func (c SubstitutionCreator) Create(openAPIPath, resourcesPath string) error { Values: c.Values, Pattern: c.Pattern, } + + err := c.CreateSettersForSubstitution(openAPIPath) + if err != nil { + return err + } + if err := d.AddToFile(openAPIPath); err != nil { return err } @@ -62,3 +72,187 @@ func (c SubstitutionCreator) Create(openAPIPath, resourcesPath string) error { 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 +} diff --git a/kyaml/setters2/settersutil/substitutioncreator_test.go b/kyaml/setters2/settersutil/substitutioncreator_test.go new file mode 100644 index 000000000..9c6b2951b --- /dev/null +++ b/kyaml/setters2/settersutil/substitutioncreator_test.go @@ -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()) + } + }) + } +} diff --git a/kyaml/setters2/types.go b/kyaml/setters2/types.go index bd8f62a53..b896a2cd7 100644 --- a/kyaml/setters2/types.go +++ b/kyaml/setters2/types.go @@ -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 } diff --git a/releasing/VERSIONS b/releasing/VERSIONS index ffac7d0de..fa3eeacc3 100644 --- a/releasing/VERSIONS +++ b/releasing/VERSIONS @@ -6,7 +6,7 @@ # kyaml version export kyaml_major=0 export kyaml_minor=0 -export kyaml_patch=12 +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=12 +export cmd_config_patch=13 # cmd/kubectl version export cmd_kubectl_major=0