diff --git a/cmd/config/internal/commands/cmdcreatesetter.go b/cmd/config/internal/commands/cmdcreatesetter.go index 4a9db6a73..dd3174c74 100644 --- a/cmd/config/internal/commands/cmdcreatesetter.go +++ b/cmd/config/internal/commands/cmdcreatesetter.go @@ -48,6 +48,9 @@ func NewCreateSetterRunner(parent string) *CreateSetterRunner { set.Flags().MarkHidden("partial") set.Flags().StringVar(&setterVersion, "version", "", "use this version of the setter format") + set.Flags().StringVar(&r.CreateSetter.SchemaPath, "schema-path", "", + `openAPI schema file path for setter constraints -- file content `+ + `e.g. {"type": "string", "maxLength": 15, "enum": ["allowedValue1", "allowedValue2"]}`) set.Flags().MarkHidden("version") fixDocs(parent, set) r.Command = set diff --git a/cmd/config/internal/commands/cmdcreatesetter_test.go b/cmd/config/internal/commands/cmdcreatesetter_test.go index 9ec0e9451..ff6da4f31 100644 --- a/cmd/config/internal/commands/cmdcreatesetter_test.go +++ b/cmd/config/internal/commands/cmdcreatesetter_test.go @@ -21,6 +21,7 @@ func TestCreateSetterCommand(t *testing.T) { name string input string args []string + schema string out string inputOpenAPI string expectedOpenAPI string @@ -85,6 +86,88 @@ openAPI: `, err: "substitution with name my-image already exists, substitution and setter can't have same name", }, + + { + name: "add replicas with schema", + args: []string{"replicas", "3", "--description", "hello world", "--set-by", "me"}, + schema: `{"maximum": 10, "type": "integer"}`, + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + replicas: 3 + `, + inputOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +`, + expectedOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.replicas: + maximum: 10 + type: integer + 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"} + `, + }, + + { + name: "add replicas with schema list values", + args: []string{"list", "a", "--description", "hello world", "--set-by", "me", "--type", "array"}, + schema: `{"maxItems": 2, "type": "array", "items": {"type": "string"}}`, + input: ` +apiVersion: example.com/v1beta1 +kind: Example +spec: + list: + - "a" + `, + inputOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +`, + expectedOpenAPI: ` +apiVersion: v1alpha1 +kind: Example +openAPI: + definitions: + io.k8s.cli.setters.list: + items: + type: string + maxItems: 2 + type: array + description: hello world + x-k8s-cli: + setter: + name: list + value: a + setBy: me + `, + expectedResources: ` +apiVersion: example.com/v1beta1 +kind: Example +spec: + list: + - "a" # {"$ref":"#/definitions/io.k8s.cli.setters.list"} + `, + }, } for i := range tests { test := tests[i] @@ -98,10 +181,27 @@ openAPI: t.FailNow() } defer os.Remove(f.Name()) + err = ioutil.WriteFile(f.Name(), []byte(test.inputOpenAPI), 0600) if !assert.NoError(t, err) { t.FailNow() } + + if test.schema != "" { + sch, err := ioutil.TempFile("", "schema.json") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.Remove(sch.Name()) + + err = ioutil.WriteFile(sch.Name(), []byte(test.schema), 0600) + if !assert.NoError(t, err) { + t.FailNow() + } + + test.args = append(test.args, "--schema-path", sch.Name()) + } + old := ext.GetOpenAPIFile defer func() { ext.GetOpenAPIFile = old }() ext.GetOpenAPIFile = func(args []string) (s string, err error) { diff --git a/kyaml/setters2/add.go b/kyaml/setters2/add.go index 93bc47f5f..4488e69ca 100644 --- a/kyaml/setters2/add.go +++ b/kyaml/setters2/add.go @@ -4,6 +4,7 @@ package setters2 import ( + "encoding/json" "strings" "github.com/go-openapi/spec" @@ -116,6 +117,9 @@ type SetterDefinition struct { // Type is the type of the setter value. Type string `yaml:"type,omitempty"` + // Schema is the openAPI schema for setter constraints. + Schema string `yaml:"schema,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 @@ -137,6 +141,26 @@ func (sd SetterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) { if err != nil { return nil, err } + + definitions, err := object.Pipe(yaml.Lookup(openapi.SupplementaryOpenAPIFieldName, "definitions")) + if err != nil { + return nil, err + } + + if sd.Schema != "" { + schNode, err := ConvertJSONToYamlNode(sd.Schema) + if err != nil { + return nil, err + } + + err = definitions.PipeE(yaml.SetField(key, schNode)) + if err != nil { + return nil, err + } + // don't write the schema to the extension + sd.Schema = "" + } + if sd.Description != "" { err = def.PipeE(yaml.FieldSetter{Name: "description", StringValue: sd.Description}) if err != nil { @@ -176,6 +200,24 @@ func (sd SetterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) { return object, nil } +// ConvertJSONToYamlNode parses input json string and returns equivalent yaml node +func ConvertJSONToYamlNode(jsonStr string) (*yaml.RNode, error) { + var body map[string]interface{} + err := json.Unmarshal([]byte(jsonStr), &body) + if err != nil { + return nil, err + } + yml, err := yaml.Marshal(body) + if err != nil { + return nil, err + } + ymlStr, err := yaml.Parse(string(yml)) + if err != nil { + return nil, err + } + return ymlStr, 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 diff --git a/kyaml/setters2/settersutil/settercreator.go b/kyaml/setters2/settersutil/settercreator.go index 8b6221335..03094fb53 100644 --- a/kyaml/setters2/settersutil/settercreator.go +++ b/kyaml/setters2/settersutil/settercreator.go @@ -4,6 +4,8 @@ package settersutil import ( + "io/ioutil" + "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/openapi" "sigs.k8s.io/kustomize/kyaml/setters2" @@ -21,6 +23,8 @@ type SetterCreator struct { Type string + SchemaPath 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,10 +39,14 @@ type SetterCreator struct { } func (c SetterCreator) Create(openAPIPath, resourcesPath string) error { + schema, err := schemaFromFile(c.SchemaPath) + if err != nil { + return err + } // 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, + Type: c.Type, Schema: schema, } if err := sd.AddToFile(openAPIPath); err != nil { return err @@ -62,3 +70,15 @@ func (c SetterCreator) Create(openAPIPath, resourcesPath string) error { Outputs: []kio.Writer{inout}, }.Execute() } + +// schemaFromFile reads the contents from schemaPath and returns schema +func schemaFromFile(schemaPath string) (string, error) { + if schemaPath == "" { + return "", nil + } + sch, err := ioutil.ReadFile(schemaPath) + if err != nil { + return "", err + } + return string(sch), nil +} diff --git a/kyaml/testutil/testutil.go b/kyaml/testutil/testutil.go index cce2f2e8f..aa4a3e866 100644 --- a/kyaml/testutil/testutil.go +++ b/kyaml/testutil/testutil.go @@ -1,3 +1,6 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + package testutil_test import (