Files
kustomize/kyaml/setters2/set.go
2020-05-14 14:00:58 -07:00

390 lines
11 KiB
Go

// 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
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{}{}
format := `{"%s" : "%s"}`
if yaml.IsValueNonString(ext.Setter.Value) {
format = `{"%s" : %s}`
}
// leverage json.Unmarshal to parse the value type
// Ex: `{"setter" : "true"}` parses the value as string whereas
// `{"setter" : true}` parses as boolean
inputJSON := fmt.Sprintf(format, ext.Setter.Name, ext.Setter.Value)
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
}
// 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
})
}