mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
514 lines
15 KiB
Go
514 lines
15 KiB
Go
// Copyright 2019 The Kubernetes Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package setters2
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/go-openapi/spec"
|
|
"github.com/go-openapi/strfmt"
|
|
"github.com/go-openapi/validate"
|
|
goyaml "gopkg.in/yaml.v3"
|
|
"sigs.k8s.io/kustomize/kyaml/errors"
|
|
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
|
|
"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
|
|
}
|
|
|
|
func (s *Set) visitMapping(_ *yaml.RNode, p string, _ *openapi.ResourceSchema) error {
|
|
return nil
|
|
}
|
|
|
|
// 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, oa, setterSchema *openapi.ResourceSchema) error {
|
|
// get the openAPI for this field describing how to apply the setter
|
|
ext, err := getExtFromComment(setterSchema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ext == nil {
|
|
return nil
|
|
}
|
|
|
|
var k8sSchema *spec.Schema
|
|
if oa != nil {
|
|
k8sSchema = oa.Schema
|
|
}
|
|
|
|
// perform a direct set of the field if it matches
|
|
ok, err := s.set(object, ext, k8sSchema, setterSchema.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)
|
|
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) (bool, error) {
|
|
// check partial setters to see if they contain the setter as part of a
|
|
// substitution
|
|
if ext.Substitution == nil {
|
|
return false, nil
|
|
}
|
|
|
|
// track the visited nodes to detect cycles in nested substitutions
|
|
visited := sets.String{}
|
|
|
|
// nameMatch indicates if the input substitution depends on the specified setter,
|
|
// the substitution in ext is parsed recursively and if the setter in Set is hit while
|
|
// parsing, it indicates the match
|
|
nameMatch := false
|
|
|
|
res, err := s.substituteUtil(ext, visited, &nameMatch)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !nameMatch {
|
|
// doesn't depend on the setter, don't modify its value
|
|
return false, nil
|
|
}
|
|
|
|
field.YNode().Value = res
|
|
|
|
// substitutions are always strings
|
|
field.YNode().Tag = yaml.NodeTagString
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// substituteUtil recursively parses nested substitutions in ext and sets the setter value
|
|
// returns error if cyclic substitution is detected or any other unexpected errors
|
|
func (s *Set) substituteUtil(ext *CliExtension, visited sets.String, nameMatch *bool) (string, error) {
|
|
// check if the substitution has already been visited and throw error as cycles
|
|
// are not allowed in nested substitutions
|
|
if visited.Has(ext.Substitution.Name) {
|
|
return "", errors.Errorf(
|
|
"cyclic substitution detected with name " + ext.Substitution.Name)
|
|
}
|
|
|
|
visited.Insert(ext.Substitution.Name)
|
|
pattern := ext.Substitution.Pattern
|
|
|
|
// substitute each setter into the pattern to get the new value
|
|
// if substitution references to another substitution, recursively
|
|
// process the nested substitutions to replace the pattern with setter values
|
|
for _, v := range ext.Substitution.Values {
|
|
if v.Ref == "" {
|
|
return "", errors.Errorf(
|
|
"missing reference on substitution " + ext.Substitution.Name)
|
|
}
|
|
ref, err := spec.NewRef(v.Ref)
|
|
if err != nil {
|
|
return "", errors.Wrap(err)
|
|
}
|
|
def, err := openapi.Resolve(&ref) // resolve the def to its openAPI def
|
|
if err != nil {
|
|
return "", errors.Wrap(err)
|
|
}
|
|
defExt, err := GetExtFromSchema(def) // parse the extension out of the openAPI
|
|
if err != nil {
|
|
return "", errors.Wrap(err)
|
|
}
|
|
|
|
if defExt.Substitution != nil {
|
|
// parse recursively if it reference is substitution
|
|
substVal, err := s.substituteUtil(defExt, visited, nameMatch)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
pattern = strings.ReplaceAll(pattern, v.Marker, substVal)
|
|
continue
|
|
}
|
|
|
|
// if code reaches this point, this is a setter, so validate the setter schema
|
|
if err := validateAgainstSchema(defExt, def); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if s.isMatch(defExt.Setter.Name) {
|
|
// the substitution depends on the specified setter
|
|
*nameMatch = true
|
|
}
|
|
|
|
if val, found := defExt.Setter.EnumValues[defExt.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
|
|
pattern = strings.ReplaceAll(pattern, v.Marker, val)
|
|
} else {
|
|
pattern = strings.ReplaceAll(pattern, v.Marker, defExt.Setter.Value)
|
|
}
|
|
}
|
|
|
|
return pattern, nil
|
|
}
|
|
|
|
// set applies the value from ext to field if its name matches s.Name
|
|
func (s *Set) set(field *yaml.RNode, ext *CliExtension, k8sSch, 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. If there is
|
|
// type information on the setter schema, we use it. Otherwise we
|
|
// fall back to the field schema if it exists.
|
|
if len(sch.Type) > 0 {
|
|
yaml.FormatNonStringStyle(field.YNode(), *sch)
|
|
} else if k8sSch != nil {
|
|
yaml.FormatNonStringStyle(field.YNode(), *k8sSch)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// validateAgainstSchema validates the input setter value against user provided
|
|
// openAI schema
|
|
func validateAgainstSchema(ext *CliExtension, sch *spec.Schema) error {
|
|
fixSchemaTypes(sch)
|
|
sc := spec.Schema{}
|
|
sc.Properties = map[string]spec.Schema{}
|
|
sc.Properties[ext.Setter.Name] = *sch
|
|
|
|
var inputYAML string
|
|
if len(ext.Setter.ListValues) > 0 {
|
|
// tmplText contains the template we will use to produce a yaml
|
|
// document that we can use for validation.
|
|
var tmplText string
|
|
if sch.Items != nil && sch.Items.Schema != nil &&
|
|
sch.Items.Schema.Type.Contains("string") {
|
|
// If string is one of the legal types for the value, we
|
|
// output it with quotes in the yaml document to make sure it
|
|
// is later parsed as a string.
|
|
tmplText = `{{.key}}:{{block "list" .values}}{{"\n"}}{{range .}}{{printf "- %q\n" .}}{{end}}{{end}}`
|
|
} else {
|
|
// If string is not specifically set as the type, we just
|
|
// let the yaml unmarshaller detect the correct type. Thus, we
|
|
// do not add quotes around the value.
|
|
tmplText = `{{.key}}:{{block "list" .values}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
|
|
}
|
|
|
|
tmpl, err := template.New("validator").Parse(tmplText)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var builder strings.Builder
|
|
err = tmpl.Execute(&builder, map[string]interface{}{
|
|
"key": ext.Setter.Name,
|
|
"values": ext.Setter.ListValues,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
inputYAML = builder.String()
|
|
} else {
|
|
var format string
|
|
// Only add quotes around the value is string is one of the
|
|
// types in the schema.
|
|
if sch.Type.Contains("string") {
|
|
format = "%s: \"%s\""
|
|
} else {
|
|
format = "%s: %s"
|
|
}
|
|
inputYAML = fmt.Sprintf(format, ext.Setter.Name, ext.Setter.Value)
|
|
}
|
|
|
|
input := map[string]interface{}{}
|
|
err := goyaml.Unmarshal([]byte(inputYAML), &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
|
|
}
|
|
|
|
// fixSchemaTypes traverses the schema and checks for some common
|
|
// errors for the type field. This currently involves users using
|
|
// 'int' instead of 'integer' and 'bool' instead of 'boolean'. Early versions
|
|
// of setters didn't validate this, so there are users that have invalid
|
|
// types in their packages.
|
|
func fixSchemaTypes(sc *spec.Schema) {
|
|
for i := range sc.Type {
|
|
currentType := sc.Type[i]
|
|
if currentType == "int" {
|
|
sc.Type[i] = "integer"
|
|
}
|
|
if currentType == "bool" {
|
|
sc.Type[i] = "boolean"
|
|
}
|
|
}
|
|
|
|
if items := sc.Items; items != nil {
|
|
if items.Schema != nil {
|
|
fixSchemaTypes(items.Schema)
|
|
}
|
|
for i := range items.Schemas {
|
|
schema := items.Schemas[i]
|
|
fixSchemaTypes(&schema)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 := fieldmeta.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.NodeTagString
|
|
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.NodeTagString
|
|
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 err := def.PipeE(&yaml.FieldSetter{Name: "isSet", StringValue: "true"}); 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
|
|
})
|
|
}
|