// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package setters2 import ( "encoding/json" "fmt" "strings" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/validate" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio/kioutil" "sigs.k8s.io/kustomize/kyaml/openapi" "sigs.k8s.io/kustomize/kyaml/sets" "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 // Count is the number of fields that were updated by calling Filter Count int // SetAll if set to true will set all setters regardless of name SetAll bool } // Filter implements Set as a yaml.Filter func (s *Set) Filter(object *yaml.RNode) (*yaml.RNode, error) { return object, accept(s, object) } // isMatch returns true if the setter with name should have the field // value set func (s *Set) isMatch(name string) bool { return s.SetAll || s.Name == name } // visitSequence will perform setters for sequences func (s *Set) visitSequence(object *yaml.RNode, p string, schema *openapi.ResourceSchema) error { ext, err := getExtFromComment(schema) if err != nil { return err } if ext == nil || ext.Setter == nil || !s.isMatch(ext.Setter.Name) || len(ext.Setter.ListValues) == 0 { // setter was not invoked for this sequence return nil } s.Count++ // set the values on the sequences var elements []*yaml.Node if len(ext.Setter.ListValues) > 0 { if err := validateAgainstSchema(ext, schema.Schema); err != nil { return err } } for i := range ext.Setter.ListValues { v := ext.Setter.ListValues[i] n := yaml.NewScalarRNode(v).YNode() n.Style = yaml.DoubleQuotedStyle elements = append(elements, n) } object.YNode().Content = elements object.YNode().Style = yaml.FoldedStyle return nil } // visitScalar func (s *Set) visitScalar(object *yaml.RNode, p string, schema *openapi.ResourceSchema) error { // get the openAPI for this field describing how to apply the setter ext, err := getExtFromComment(schema) if err != nil { return err } if ext == nil { return nil } // perform a direct set of the field if it matches ok, err := s.set(object, ext, schema.Schema) if err != nil { return err } if ok { s.Count++ return nil } // perform a substitution of the field if it matches sub, err := s.substitute(object, ext, schema.Schema) 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, _ *spec.Schema) (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) } if err := validateAgainstSchema(subSetter, setter); err != nil { return false, err } 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 s.isMatch(subSetter.Setter.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 // substitutions are always strings field.YNode().Tag = yaml.StringTag 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, sch *spec.Schema) (bool, error) { // check full setter if ext.Setter == nil || !s.isMatch(ext.Setter.Name) { return false, nil } if err := validateAgainstSchema(ext, sch); err != nil { return false, err } 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, nil } // 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, nil } // validateAgainstSchema validates the input setter value against user provided // openAI schema func validateAgainstSchema(ext *cliExtension, sch *spec.Schema) error { sc := spec.Schema{} sc.Properties = map[string]spec.Schema{} sc.Properties[ext.Setter.Name] = *sch input := map[string]interface{}{} var inputJSON string if len(ext.Setter.ListValues) > 0 { format := `{"%s" : [%s]}` inputJSON = fmt.Sprintf(format, ext.Setter.Name, JoinCompositeStrings(ext.Setter.ListValues)) } else { format := `{"%s" : "%s"}` if yaml.IsValueNonString(ext.Setter.Value) { format = `{"%s" : %s}` } inputJSON = fmt.Sprintf(format, ext.Setter.Name, ext.Setter.Value) } // leverage json.Unmarshal to parse the value type // Ex: `{"setter" : "true"}` parses the value as string whereas // `{"setter" : true}` parses as boolean err := json.Unmarshal([]byte(inputJSON), &input) if err != nil { return err } err = validate.AgainstSchema(&sc, input, strfmt.Default) if err != nil { return errors.Errorf("The input value doesn't validate against provided OpenAPI schema: %v\n", err.Error()) } return nil } // JoinCompositeStrings takes in strings whose values can be of different types and returns // joined string with quotes for only string type values // ex: ["10", "true", "hi", "1.1"] returns 10,true,"hi",1.1 func JoinCompositeStrings(listValues []string) string { res := "" for _, val := range listValues { if yaml.IsValueNonString(val) { res += fmt.Sprintf(`%s,`, val) } else { res += fmt.Sprintf(`"%s",`, val) } } return strings.TrimSuffix(res, ",") } // 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"` // ListValue is the current value for a list of items ListValues []string `yaml:"listValue"` 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 oa, err := object.Pipe(yaml.Lookup("openAPI", "definitions", key)) if err != nil { return nil, err } if oa == nil { return nil, errors.Errorf("no setter %s found", s.Name) } def, err := oa.Pipe(yaml.Lookup("x-k8s-cli", "setter")) if err != nil { return nil, err } if def == nil { return nil, errors.Errorf("no setter %s found", s.Name) } // record the OpenAPI type for the setter var t string if n := oa.Field("type"); n != nil { t = n.Value.YNode().Value } // 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 = yaml.StringTag v.YNode().Style = yaml.DoubleQuotedStyle if t != "array" { // set a scalar value if err := def.PipeE(&yaml.FieldSetter{Name: "value", Value: v}); err != nil { return nil, err } } else { // set a list value if err := def.PipeE(&yaml.FieldClearer{Name: "value"}); err != nil { return nil, err } // create the list values var elements []*yaml.Node n := yaml.NewScalarRNode(s.Value).YNode() n.Tag = yaml.StringTag n.Style = yaml.DoubleQuotedStyle elements = append(elements, n) for i := range s.ListValues { v := s.ListValues[i] n := yaml.NewScalarRNode(v).YNode() n.Style = yaml.DoubleQuotedStyle elements = append(elements, n) } l := yaml.NewRNode(&yaml.Node{ Kind: yaml.SequenceNode, Content: elements, }) def.YNode().Style = yaml.FoldedStyle if err := def.PipeE(&yaml.FieldSetter{Name: "listValues", Value: l}); 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 } // SetAll applies the set filter for all yaml nodes and only returns the nodes whose // corresponding file has at least one node with input setter func SetAll(s *Set) kio.Filter { return kio.FilterFunc(func(nodes []*yaml.RNode) ([]*yaml.RNode, error) { filesToUpdate := sets.String{} // for each node record the set fields count before and after filter is applied and // store the corresponding file paths if there is an increment in setters count for i := range nodes { preCount := s.Count _, err := s.Filter(nodes[i]) if err != nil { return nil, errors.Wrap(err) } if s.Count > preCount { path, _, err := kioutil.GetFileAnnotations(nodes[i]) if err != nil { return nil, errors.Wrap(err) } filesToUpdate.Insert(path) } } var nodesInUpdatedFiles []*yaml.RNode // return only the nodes whose corresponding file has at least one node with input setter for i := range nodes { path, _, err := kioutil.GetFileAnnotations(nodes[i]) if err != nil { return nil, errors.Wrap(err) } if filesToUpdate.Has(path) { nodesInUpdatedFiles = append(nodesInUpdatedFiles, nodes[i]) } } return nodesInUpdatedFiles, nil }) }