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..8b2946eed 100644 --- a/kyaml/setters2/add.go +++ b/kyaml/setters2/add.go @@ -116,6 +116,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 @@ -132,13 +135,33 @@ func (sd SetterDefinition) AddToFile(path string) error { 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)) + definitions, err := object.Pipe(yaml.LookupCreate( + yaml.MappingNode, openapi.SupplementaryOpenAPIFieldName, "definitions")) if err != nil { return nil, err } + + setterDef, err := definitions.Pipe(yaml.LookupCreate(yaml.MappingNode, key)) + if err != nil { + return nil, err + } + + if sd.Schema != "" { + schNode, err := yaml.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}) + err = setterDef.PipeE(yaml.FieldSetter{Name: "description", StringValue: sd.Description}) if err != nil { return nil, err } @@ -147,7 +170,7 @@ func (sd SetterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) { } if sd.Type != "" { - err = def.PipeE(yaml.FieldSetter{Name: "type", StringValue: sd.Type}) + err = setterDef.PipeE(yaml.FieldSetter{Name: "type", StringValue: sd.Type}) if err != nil { return nil, err } @@ -155,7 +178,7 @@ func (sd SetterDefinition) Filter(object *yaml.RNode) (*yaml.RNode, error) { sd.Type = "" } - ext, err := def.Pipe(yaml.LookupCreate(yaml.MappingNode, K8sCliExtensionKey)) + ext, err := setterDef.Pipe(yaml.LookupCreate(yaml.MappingNode, K8sCliExtensionKey)) if err != nil { return nil, err } 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/yaml/types.go b/kyaml/yaml/types.go index 3b3615265..d74cb8471 100644 --- a/kyaml/yaml/types.go +++ b/kyaml/yaml/types.go @@ -722,6 +722,24 @@ func (rn *RNode) UnmarshalJSON(b []byte) error { return nil } +// ConvertJSONToYamlNode parses input json string and returns equivalent yaml node +func ConvertJSONToYamlNode(jsonStr string) (*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 + } + node, err := Parse(string(yml)) + if err != nil { + return nil, err + } + return node, nil +} + // checkKey returns true if all elems have the key func checkKey(key string, elems []*Node) bool { count := 0 diff --git a/kyaml/yaml/types_test.go b/kyaml/yaml/types_test.go index a68af4ae5..b963d5b28 100644 --- a/kyaml/yaml/types_test.go +++ b/kyaml/yaml/types_test.go @@ -146,3 +146,23 @@ hello: world }) } } + +func TestConvertJSONToYamlNode(t *testing.T) { + inputJSON := `{"type": "string", "maxLength": 15, "enum": ["allowedValue1", "allowedValue2"]}` + expected := `enum: +- allowedValue1 +- allowedValue2 +maxLength: 15 +type: string +` + + node, err := ConvertJSONToYamlNode(inputJSON) + if !assert.NoError(t, err) { + t.FailNow() + } + actual, err := node.String() + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, expected, actual) +}