From 7097013426e300afdfa8f2029f6916d70ad1c75f Mon Sep 17 00:00:00 2001 From: Phillip Wittrock Date: Mon, 10 Feb 2020 12:32:07 -0800 Subject: [PATCH] setters 2.0 --- kyaml/fieldmeta/fieldmeta.go | 4 +- kyaml/openapi/openapi.go | 7 +- kyaml/openapi/openapi_test.go | 2 +- kyaml/setters2/doc.go | 157 ++++++++++++ kyaml/setters2/example_test.go | 130 ++++++++++ kyaml/setters2/set.go | 113 +++++++++ kyaml/setters2/set_test.go | 440 +++++++++++++++++++++++++++++++++ kyaml/setters2/types.go | 87 +++++++ kyaml/setters2/walk.go | 45 ++++ 9 files changed, 981 insertions(+), 4 deletions(-) create mode 100644 kyaml/setters2/doc.go create mode 100644 kyaml/setters2/example_test.go create mode 100644 kyaml/setters2/set.go create mode 100644 kyaml/setters2/set_test.go create mode 100644 kyaml/setters2/types.go create mode 100644 kyaml/setters2/walk.go diff --git a/kyaml/fieldmeta/fieldmeta.go b/kyaml/fieldmeta/fieldmeta.go index 3ed5fc73c..07a9ca7a6 100644 --- a/kyaml/fieldmeta/fieldmeta.go +++ b/kyaml/fieldmeta/fieldmeta.go @@ -55,7 +55,7 @@ func (fm *FieldMeta) Read(n *yaml.RNode) error { } b, err := json.Marshal(fe) if err != nil { - return err + return errors.Wrap(err) } return json.Unmarshal(b, &fm.Extensions) } @@ -67,7 +67,7 @@ func (fm *FieldMeta) Write(n *yaml.RNode) error { fm.Schema.VendorExtensible.AddExtension("x-kustomize", fm.Extensions) b, err := json.Marshal(fm.Schema) if err != nil { - return err + return errors.Wrap(err) } n.YNode().LineComment = string(b) return nil diff --git a/kyaml/openapi/openapi.go b/kyaml/openapi/openapi.go index 7587968f1..b4fce67f9 100644 --- a/kyaml/openapi/openapi.go +++ b/kyaml/openapi/openapi.go @@ -49,6 +49,11 @@ func AddSchema(s []byte) (*spec.Schema, error) { return parse(s) } +// ResetOpenAPI resets the openapi data to empty +func ResetOpenAPI() { + globalSchema = openapiData{} +} + // AddDefinitions adds the definitions to the global schema. func AddDefinitions(definitions spec.Definitions) { // initialize values if they have not yet been set @@ -123,7 +128,7 @@ func GetSchema(s string) (*ResourceSchema, error) { // schema as part of the global schema. // Must be called before the schema is used. func SuppressBuiltInSchemaUse() { - globalSchema.noUseBuiltInSchema = false + globalSchema.noUseBuiltInSchema = true } // Elements returns the Schema for the elements of an array. diff --git a/kyaml/openapi/openapi_test.go b/kyaml/openapi/openapi_test.go index 04bf26f0b..a65bee70b 100644 --- a/kyaml/openapi/openapi_test.go +++ b/kyaml/openapi/openapi_test.go @@ -40,7 +40,7 @@ func TestNoUseBuiltInSchema_AddSchema(t *testing.T) { t.FailNow() } s, err := GetSchema(`{"$ref": "#/definitions/io.k8s.config.setters.replicas"}`) - if !assert.Greater(t, len(globalSchema.schema.Definitions), 1) { + if !assert.Equal(t, len(globalSchema.schema.Definitions), 1) { t.FailNow() } if !assert.NoError(t, err) { diff --git a/kyaml/setters2/doc.go b/kyaml/setters2/doc.go new file mode 100644 index 000000000..5eaa60a98 --- /dev/null +++ b/kyaml/setters2/doc.go @@ -0,0 +1,157 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 +// +// Package setters2 contains libraries for setting resource field values from OpenAPI setter +// extensions. +// +// Setters +// +// Setters are used to programmatically set configuration field values -- e.g. through a cli or ui. +// +// Setters are defined through OpenAPI definitions using the x-k8s-cli extension. +// Note: additional OpenAPI definitions may be registered through openapi.AddSchema([]byte) +// +// Example OpenAPI schema containing a setter: +// +// { +// "definitions": { +// "io.k8s.cli.setters.replicas": { +// "x-k8s-cli": { +// "setter": { +// "name": "replicas", +// "value": "4" +// } +// } +// } +// } +// } +// +// Setter fields: +// +// x-k8s-cli.setter.name: name of the setter +// x-k8s-cli.setter.value: value of the setter that should be applied to fields +// +// The setter definition key must be of the form "io.k8s.cli.setters.NAME", where NAME matches the +// value of "x-k8s-cli.setter.name". +// +// When Set.Filter is called, the named setter will have its value applied to all resource +// fields referencing it. +// +// Fields may reference setters through a yaml comment containing the serialized JSON OpenAPI. +// +// Example Deployment resource with a "spec.replicas" field set by the "replicas" setter: +// +// apiVersion: apps/v1 +// kind: Deployment +// metadata: +// name: nginx-deployment +// spec: +// replicas: 4 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} +// +// If the OpenAPI io.k8s.cli.setters.replicas x-k8s-cli.setter.value was changed from "4" to "5", +// then calling Set{Name: "replicas"}.Filter(deployment) would update the Deployment spec.replicas +// value from 4 to 5. +// +// Updated OpenAPI: +// +// { +// "definitions": { +// "io.k8s.cli.setters.replicas": { +// "x-k8s-cli": { +// "setter": { +// "name": "replicas", +// "value": "5" +// } +// } +// } +// } +// } +// +// Updated Deployment Configuration: +// +// apiVersion: apps/v1 +// kind: Deployment +// metadata: +// name: nginx-deployment +// spec: +// replicas: 5 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} +// +// Substitutions +// +// Substitutions are used to programmatically set configuration field values using multiple +// setters which are substituted into a pattern string. +// +// Substitutions may be used when a field value does not cleanly map to a single setter, but +// instead matches some string pattern where setters may be substituted in. +// +// Fields may reference substitutions the same way they do setters, however substitutions +// reference setters from which they are derived. +// +// Example OpenAPI schema containing a substitution derived from 2 setters: +// +// { +// "definitions": { +// "io.k8s.cli.setters.image-name": { +// "x-k8s-cli": { +// "setter": { +// "name": "image-name", +// "value": "nginx" +// } +// } +// }, +// "io.k8s.cli.setters.image-tag": { +// "x-k8s-cli": { +// "setter": { +// "name": "image-tag", +// "value": "1.8.1" +// } +// } +// }, +// "io.k8s.cli.substitutions.image-name-tag": { +// "x-k8s-cli": { +// "substitution": { +// "name": "image-name-tag", +// "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"} +// ] +// } +// } +// } +// } +// } +// +// Substitution Fields. +// +// x-k8s-cli.substitution.name: name of the substitution +// x-k8s-cli.substitution.pattern: string pattern to substitute markers into +// x-k8s-cli.substitution.values.marker: the marker substring within pattern to replace +// x-k8s-cli.substitution.values.ref: the setter ref containing the value to replace the marker with +// +// The substitution is composed of a "pattern" containing markers, and a list of setter "values" +// which are substituted into the markers. +// +// Example Deployment with substitution: +// +// apiVersion: apps/v1 +// kind: Deployment +// metadata: +// name: nginx-deployment +// spec: +// template: +// spec: +// containers: +// - name: nginx +// image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image-name-tag"} +// +// spec.template.spec.containers[name=nginx].image is set by the "image" substitution any time +// either "image-name" or "image-tag" is set. Whenever any setter referenced by a substitution +// is set, the substitution will be recalculated by substituting its values into its pattern. +// +// +// If the OpenAPI io.k8s.cli.setters.image-name x-k8s-cli.setter.value was changed from "1.8.1" +// to "1.8.2", then calling either Set{Name: "image-name"}.Filter(deployment) or +// Set{Name: "image-tag"}.Filter(deployment) would update the Deployment field +// spec.template.spec.container[name=nginx].image from "nginx:1.8.1" to "nginx:1.8.2". +package setters2 diff --git a/kyaml/setters2/example_test.go b/kyaml/setters2/example_test.go new file mode 100644 index 000000000..1f6e06ebb --- /dev/null +++ b/kyaml/setters2/example_test.go @@ -0,0 +1,130 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package setters2 + +import ( + "fmt" + + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// ExampleSet demonstrates using Set to replace the current field value in an object +func ExampleSet() { + openapi.ResetOpenAPI() + + // OpenAPI definitions with setter extensions on definitions + schema := ` +{ + "definitions": { + "io.k8s.cli.setters.replicas": { + "x-k8s-cli": { + "setter": { + "name": "replicas", + "value": "4" + } + } + } + } +} +` + // Resource with field referencing OpenAPI definition + deployment := ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} +` + _, err := openapi.AddSchema([]byte(schema)) // add the schema definitions + if err != nil { + panic(err) + } + object := yaml.MustParse(deployment) // parse the configuration + err = object.PipeE(&Set{Name: "replicas"}) // set replicas from the setter + if err != nil { + panic(err) + } + + fmt.Println(object.MustString()) + + // Output: + // apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: nginx-deployment + // spec: + // replicas: 4 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} +} + +// ExampleSet_Substitution demonstrates using Set to substitute a value into the field of +// an object. Only part of the field value is modified. +func ExampleSet_substitution() { + openapi.ResetOpenAPI() + + // set the version setter + schema := ` +{ + "definitions": { + "io.k8s.cli.setters.version": { + "x-k8s-cli": { + "setter": { + "name": "version", + "value": "1.8.1" + } + } + }, + "io.k8s.cli.substitutions.image": { + "x-k8s-cli": { + "substitution": { + "name": "image", + "pattern": "nginx:VERSION", + "values": [ + {"marker": "VERSION", "ref": "#/definitions/io.k8s.cli.setters.version"} + ] + } + } + } + } +}` + + deployment := ` +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"} +` + + _, err := openapi.AddSchema([]byte(schema)) // add the schema definitions + if err != nil { + panic(err) + } + object := yaml.MustParse(deployment) // parse the configuration + err = object.PipeE(&Set{Name: "version"}) // set replicas from the setter + if err != nil { + panic(err) + } + + // Print the object with the update value + fmt.Println(object.MustString()) + + // Output: + // apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: nginx-deployment + // spec: + // template: + // spec: + // containers: + // - name: nginx + // image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} +} diff --git a/kyaml/setters2/set.go b/kyaml/setters2/set.go new file mode 100644 index 000000000..dc81e014b --- /dev/null +++ b/kyaml/setters2/set.go @@ -0,0 +1,113 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package setters2 + +import ( + "strings" + + "github.com/go-openapi/spec" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// Set sets resource field values from an OpenAPI setter +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 +} + +// Filter implements Set as a yaml.Filter +func (s *Set) Filter(object *yaml.RNode) (*yaml.RNode, error) { + return object, accept(s, object) +} + +// visitScalar +func (s *Set) visitScalar(object *yaml.RNode, _ string) error { + // get the openAPI for this field describing how to apply the setter + ext, err := getExtFromComment(object) + if err != nil { + return err + } + if ext == nil { + return nil + } + + // perform a direct set of the field if it matches + if s.set(object, ext) { + return nil + } + + // perform a substitution of the field if it matches + if sub, err := s.substitute(object, ext); sub || err != nil { + return err + } + + 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) { + nameMatch := false + + // check partial setters to see if they contain the setter as part of a + // substitution + if ext.Substitution == nil { + return false, nil + } + + p := ext.Substitution.Pattern + + // substitute each setter into the pattern to get the new value + for _, v := range ext.Substitution.Values { + if v.Ref == "" { + return false, errors.Errorf( + "missing reference on substitution " + ext.Substitution.Name) + } + ref, err := spec.NewRef(v.Ref) + if err != nil { + return false, errors.Wrap(err) + } + setter, err := openapi.Resolve(&ref) // resolve the setter to its openAPI def + if err != nil { + return false, errors.Wrap(err) + } + subSetter, err := getExtFromSchema(setter) // parse the extension out of the openAPI + 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 subSetter.Setter.Name == s.Name { + // the substitution depends on the specified setter + nameMatch = true + } + } + if !nameMatch { + // doesn't depend on the setter, don't modify its value + return false, nil + } + + // TODO(pwittrock): validate the field value + + field.YNode().Value = p + 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 { + // check full setter + if ext.Setter == nil || ext.Setter.Name != s.Name { + return false + } + + // TODO(pwittrock): validate the field value + + // this has a full setter, set its value + field.YNode().Value = ext.Setter.Value + return true +} diff --git a/kyaml/setters2/set_test.go b/kyaml/setters2/set_test.go new file mode 100644 index 000000000..07e984a2b --- /dev/null +++ b/kyaml/setters2/set_test.go @@ -0,0 +1,440 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package setters2 + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/openapi" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func TestSet_Filter(t *testing.T) { + var tests = []struct { + name string + setter string + openapi string + input string + expected string + }{ + { + name: "set-replicas", + 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: "4" + 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: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + `, + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 4 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"} + `, + }, + { + name: "set-arg", + setter: "arg1", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.replicas: + x-k8s-cli: + setter: + name: replicas + value: "4" + io.k8s.cli.setters.arg1: + x-k8s-cli: + setter: + name: arg1 + value: "some value" + `, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + template: + spec: + containers: + - name: nginx + args: + - a + - b # {"$ref": "#/definitions/io.k8s.cli.setters.arg1"} + `, + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + template: + spec: + containers: + - name: nginx + args: + - a + - some value # {"$ref": "#/definitions/io.k8s.cli.setters.arg1"}`, + }, + { + name: "substitute-image-tag", + setter: "image-tag", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.image-name: + x-k8s-cli: + setter: + name: image-name + value: "nginx" + 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: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + `, + }, + { + name: "substitute-annotation", + setter: "project", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.project: + x-k8s-cli: + setter: + name: project + value: "a" + io.k8s.cli.setters.location: + x-k8s-cli: + setter: + name: location + value: "b" + io.k8s.cli.setters.cluster: + x-k8s-cli: + setter: + name: cluster + value: "c" + io.k8s.cli.substitutions.key: + x-k8s-cli: + substitution: + name: key + pattern: https://container.googleapis.com/v1/projects/PROJECT/locations/LOCATION/clusters/CLUSTER + values: + - marker: "PROJECT" + ref: "#/definitions/io.k8s.cli.setters.project" + - marker: "LOCATION" + ref: "#/definitions/io.k8s.cli.setters.location" + - marker: "CLUSTER" + ref: "#/definitions/io.k8s.cli.setters.cluster" +`, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + annotations: + key: 'https://container.googleapis.com/v1/projects/a/locations/a/clusters/a' # {"$ref": "#/definitions/io.k8s.cli.substitutions.key"} +`, + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + annotations: + key: 'https://container.googleapis.com/v1/projects/a/locations/b/clusters/c' # {"$ref": "#/definitions/io.k8s.cli.substitutions.key"} +`, + }, + { + name: "substitute-not-match-setter", + setter: "not-real", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.project: + x-k8s-cli: + setter: + name: project + value: "a" + io.k8s.cli.setters.location: + x-k8s-cli: + setter: + name: location + value: "b" + io.k8s.cli.setters.cluster: + x-k8s-cli: + setter: + name: cluster + value: "c" + io.k8s.cli.substitutions.key: + x-k8s-cli: + substitution: + name: key + pattern: https://container.googleapis.com/v1/projects/PROJECT/locations/LOCATION/clusters/CLUSTER + values: + - marker: "PROJECT" + ref: "#/definitions/io.k8s.cli.setters.project" + - marker: "LOCATION" + ref: "#/definitions/io.k8s.cli.setters.location" + - marker: "CLUSTER" + ref: "#/definitions/io.k8s.cli.setters.cluster" +`, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + annotations: + key: 'https://container.googleapis.com/v1/projects/a/locations/a/clusters/a' # {"$ref": "#/definitions/io.k8s.cli.substitutions.key"} +`, + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + annotations: + key: 'https://container.googleapis.com/v1/projects/a/locations/a/clusters/a' # {"$ref": "#/definitions/io.k8s.cli.substitutions.key"} +`, + }, + { + name: "substitute-image-name", + setter: "image-name", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.image-name: + x-k8s-cli: + setter: + name: image-name + value: "foo" + io.k8s.cli.setters.image-tag: + x-k8s-cli: + setter: + name: image-tag + value: "1.7.9" + 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: foo:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + `, + }, + { + name: "substitute-substring", + setter: "image-tag", + openapi: ` +openAPI: + definitions: + io.k8s.cli.setters.image-name: + x-k8s-cli: + setter: + name: image-name + value: "nginx" + 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: a:a # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + `, + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + template: + spec: + containers: + - name: nginx + image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} + `, + }, + } + 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) + + // parse the input to be modified + r, err := yaml.Parse(test.input) + if !assert.NoError(t, err) { + t.FailNow() + } + + // invoke the setter + instance := &Set{Name: test.setter} + result, err := instance.Filter(r) + 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() + } + }) + } +} + +// initSchema initializes the openAPI with the definitions from s +func initSchema(t *testing.T, s string) { + // parse out the schema from the input openAPI + y, err := yaml.Parse(s) + if !assert.NoError(t, err) { + t.FailNow() + } + // get the field containing the openAPI + f := y.Field("openAPI") + if !assert.NotNil(t, f) { + t.FailNow() + } + defs, err := f.Value.String() + if !assert.NoError(t, err) { + t.FailNow() + } + + // convert the yaml openAPI to an interface{} + // which can be marshalled into json + var o interface{} + err = yaml.Unmarshal([]byte(defs), &o) + if !assert.NoError(t, err) { + t.FailNow() + } + + // convert the interface{} into a json string + j, err := json.Marshal(o) + if !assert.NoError(t, err) { + t.FailNow() + } + + // reset the openAPI to clear existing definitions + openapi.ResetOpenAPI() + + // add the json schema to the global schema + _, err = openapi.AddSchema(j) + if !assert.NoError(t, err) { + t.FailNow() + } +} diff --git a/kyaml/setters2/types.go b/kyaml/setters2/types.go new file mode 100644 index 000000000..bd8f62a53 --- /dev/null +++ b/kyaml/setters2/types.go @@ -0,0 +1,87 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package setters2 + +import ( + "encoding/json" + + "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" +) + +type cliExtension struct { + Setter *setter `yaml:"setter,omitempty" json:"setter,omitempty"` + Substitution *substitution `yaml:"substitution,omitempty" json:"substitution,omitempty"` +} + +type setter struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Value string `yaml:"value,omitempty" json:"value,omitempty"` +} + +type substitution struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Pattern string `yaml:"pattern,omitempty" json:"pattern,omitempty"` + Values []substitutionSetterReference `yaml:"values,omitempty" json:"values,omitempty"` +} + +type substitutionSetterReference struct { + Ref string `yaml:"ref,omitempty" json:"ref,omitempty"` + Marker string `yaml:"marker,omitempty" json:"marker,omitempty"` +} + +//K8sCliExtensionKey is the name of the OpenAPI field containing the setter extensions +const K8sCliExtensionKey = "x-k8s-cli" + +// getExtFromSchema returns the cliExtension openAPI extension if it is present in schema +func getExtFromSchema(schema *spec.Schema) (*cliExtension, error) { + cep := schema.VendorExtensible.Extensions[K8sCliExtensionKey] + if cep == nil { + return nil, nil + } + b, err := json.Marshal(cep) + if err != nil { + return nil, err + } + val := &cliExtension{} + if err := json.Unmarshal(b, val); err != nil { + return nil, err + } + return val, nil +} + +// getExtFromComment returns the cliExtension openAPI extension if it is present as +// a comment on the field. +func getExtFromComment(object *yaml.RNode) (*cliExtension, 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) + } + if fm.Schema.Ref.String() == "" { + return 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) + } + if r == nil { + // no schema found + // TODO(pwittrock): should this be an error if it doesn't resolve? + return 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 ext, nil +} diff --git a/kyaml/setters2/walk.go b/kyaml/setters2/walk.go new file mode 100644 index 000000000..d084842ce --- /dev/null +++ b/kyaml/setters2/walk.go @@ -0,0 +1,45 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package setters2 + +import ( + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// visitor is implemented by structs which need to walk the configuration. +// visitor is provided to accept to walk configuration +type visitor interface { + // visitScalar is called for each scalar field value on a resource + // node is the scalar field value + // path is the path to the field; path elements are separated by '.' + visitScalar(node *yaml.RNode, path string) error +} + +// accept invokes the appropriate function on v for each field in object +func accept(v visitor, object *yaml.RNode) error { + return acceptImpl(v, object, "") +} + +// acceptImpl implements accept using recursion +func acceptImpl(v visitor, object *yaml.RNode, p string) error { + switch object.YNode().Kind { + case yaml.DocumentNode: + // Traverse the child of the document + return accept(v, yaml.NewRNode(object.YNode())) + case yaml.MappingNode: + return object.VisitFields(func(node *yaml.MapNode) error { + // Traverse each field value + return acceptImpl(v, node.Value, p+"."+node.Key.YNode().Value) + }) + case yaml.SequenceNode: + return object.VisitElements(func(node *yaml.RNode) error { + // Traverse each list element + return acceptImpl(v, node, p) + }) + case yaml.ScalarNode: + // Visit the scalar field + return v.visitScalar(object, p) + } + return nil +}