...

Source file src/sigs.k8s.io/kustomize/kyaml/fieldmeta/fieldmeta.go

Documentation: sigs.k8s.io/kustomize/kyaml/fieldmeta

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package fieldmeta
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"reflect"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"k8s.io/kube-openapi/pkg/validation/spec"
    14  	"sigs.k8s.io/kustomize/kyaml/errors"
    15  	"sigs.k8s.io/kustomize/kyaml/openapi"
    16  	"sigs.k8s.io/kustomize/kyaml/yaml"
    17  )
    18  
    19  // FieldMeta contains metadata that may be attached to fields as comments
    20  type FieldMeta struct {
    21  	Schema spec.Schema
    22  
    23  	Extensions XKustomize
    24  
    25  	SettersSchema *spec.Schema
    26  }
    27  
    28  type XKustomize struct {
    29  	SetBy               string               `yaml:"setBy,omitempty" json:"setBy,omitempty"`
    30  	PartialFieldSetters []PartialFieldSetter `yaml:"partialSetters,omitempty" json:"partialSetters,omitempty"`
    31  	FieldSetter         *PartialFieldSetter  `yaml:"setter,omitempty" json:"setter,omitempty"`
    32  }
    33  
    34  // PartialFieldSetter defines how to set part of a field rather than the full field
    35  // value.  e.g. the tag part of an image field
    36  type PartialFieldSetter struct {
    37  	// Name is the name of this setter.
    38  	Name string `yaml:"name" json:"name"`
    39  
    40  	// Value is the current value that has been set.
    41  	Value string `yaml:"value" json:"value"`
    42  }
    43  
    44  // IsEmpty returns true if the FieldMeta has any empty Schema
    45  func (fm *FieldMeta) IsEmpty() bool {
    46  	if fm == nil {
    47  		return true
    48  	}
    49  	return reflect.DeepEqual(fm.Schema, spec.Schema{})
    50  }
    51  
    52  // Read reads the FieldMeta from a node
    53  func (fm *FieldMeta) Read(n *yaml.RNode) error {
    54  	// check for metadata on head and line comments
    55  	comments := []string{n.YNode().LineComment, n.YNode().HeadComment}
    56  	for _, c := range comments {
    57  		if c == "" {
    58  			continue
    59  		}
    60  		c := strings.TrimLeft(c, "#")
    61  
    62  		// check for new short hand notation or fall back to openAPI ref format
    63  		if !fm.processShortHand(c) {
    64  			// if it doesn't Unmarshal that is fine, it means there is no metadata
    65  			// other comments are valid, they just don't parse
    66  			// TODO: consider more sophisticated parsing techniques similar to what is used
    67  			// for go struct tags.
    68  			if err := fm.Schema.UnmarshalJSON([]byte(c)); err != nil {
    69  				// note: don't return an error if the comment isn't a fieldmeta struct
    70  				return nil
    71  			}
    72  		}
    73  		fe := fm.Schema.VendorExtensible.Extensions["x-kustomize"]
    74  		if fe == nil {
    75  			return nil
    76  		}
    77  		b, err := json.Marshal(fe)
    78  		if err != nil {
    79  			return errors.Wrap(err)
    80  		}
    81  		return json.Unmarshal(b, &fm.Extensions)
    82  	}
    83  	return nil
    84  }
    85  
    86  // processShortHand parses the comment for short hand ref, loads schema to fm
    87  // and returns true if successful, returns false for any other cases and not throw
    88  // error, as the comment might not be a setter ref
    89  func (fm *FieldMeta) processShortHand(comment string) bool {
    90  	input := map[string]string{}
    91  	err := json.Unmarshal([]byte(comment), &input)
    92  	if err != nil {
    93  		return false
    94  	}
    95  	name := input[shortHandRef]
    96  	if name == "" {
    97  		return false
    98  	}
    99  
   100  	// check if setter with the name exists, else check for a substitution
   101  	// setter and substitution can't have same name in shorthand
   102  
   103  	setterRef, err := spec.NewRef(DefinitionsPrefix + SetterDefinitionPrefix + name)
   104  	if err != nil {
   105  		return false
   106  	}
   107  
   108  	setterRefBytes, err := setterRef.MarshalJSON()
   109  	if err != nil {
   110  		return false
   111  	}
   112  
   113  	if _, err := openapi.Resolve(&setterRef, fm.SettersSchema); err == nil {
   114  		setterErr := fm.Schema.UnmarshalJSON(setterRefBytes)
   115  		return setterErr == nil
   116  	}
   117  
   118  	substRef, err := spec.NewRef(DefinitionsPrefix + SubstitutionDefinitionPrefix + name)
   119  	if err != nil {
   120  		return false
   121  	}
   122  
   123  	substRefBytes, err := substRef.MarshalJSON()
   124  	if err != nil {
   125  		return false
   126  	}
   127  
   128  	if _, err := openapi.Resolve(&substRef, fm.SettersSchema); err == nil {
   129  		substErr := fm.Schema.UnmarshalJSON(substRefBytes)
   130  		return substErr == nil
   131  	}
   132  	return false
   133  }
   134  
   135  func isExtensionEmpty(x XKustomize) bool {
   136  	if x.FieldSetter != nil {
   137  		return false
   138  	}
   139  	if x.SetBy != "" {
   140  		return false
   141  	}
   142  	if len(x.PartialFieldSetters) > 0 {
   143  		return false
   144  	}
   145  	return true
   146  }
   147  
   148  // Write writes the FieldMeta to a node
   149  func (fm *FieldMeta) Write(n *yaml.RNode) error {
   150  	if !isExtensionEmpty(fm.Extensions) {
   151  		return fm.WriteV1Setters(n)
   152  	}
   153  
   154  	// Ref is removed when a setter is deleted, so the Ref string could be empty.
   155  	if fm.Schema.Ref.String() != "" {
   156  		// Ex: {"$ref":"#/definitions/io.k8s.cli.setters.replicas"} should be converted to
   157  		// {"$openAPI":"replicas"} and added to the line comment
   158  		ref := fm.Schema.Ref.String()
   159  		var shortHandRefValue string
   160  		switch {
   161  		case strings.HasPrefix(ref, DefinitionsPrefix+SetterDefinitionPrefix):
   162  			shortHandRefValue = strings.TrimPrefix(ref, DefinitionsPrefix+SetterDefinitionPrefix)
   163  		case strings.HasPrefix(ref, DefinitionsPrefix+SubstitutionDefinitionPrefix):
   164  			shortHandRefValue = strings.TrimPrefix(ref, DefinitionsPrefix+SubstitutionDefinitionPrefix)
   165  		default:
   166  			return fmt.Errorf("unexpected ref format: %s", ref)
   167  		}
   168  		n.YNode().LineComment = fmt.Sprintf(`{"%s":"%s"}`, shortHandRef,
   169  			shortHandRefValue)
   170  	} else {
   171  		n.YNode().LineComment = ""
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  // WriteV1Setters is the v1 setters way of writing setter definitions
   178  // TODO: pmarupaka - remove this method after migration
   179  func (fm *FieldMeta) WriteV1Setters(n *yaml.RNode) error {
   180  	fm.Schema.VendorExtensible.AddExtension("x-kustomize", fm.Extensions)
   181  	b, err := json.Marshal(fm.Schema)
   182  	if err != nil {
   183  		return errors.Wrap(err)
   184  	}
   185  	n.YNode().LineComment = string(b)
   186  	return nil
   187  }
   188  
   189  // FieldValueType defines the type of input to register
   190  type FieldValueType string
   191  
   192  const (
   193  	// String defines a string flag
   194  	String FieldValueType = "string"
   195  	// Bool defines a bool flag
   196  	Bool = "boolean"
   197  	// Int defines an int flag
   198  	Int = "integer"
   199  )
   200  
   201  func (it FieldValueType) String() string {
   202  	if it == "" {
   203  		return "string"
   204  	}
   205  	return string(it)
   206  }
   207  
   208  func (it FieldValueType) Validate(value string) error {
   209  	switch it {
   210  	case Int:
   211  		if _, err := strconv.Atoi(value); err != nil {
   212  			return errors.WrapPrefixf(err, "value must be an int")
   213  		}
   214  	case Bool:
   215  		if _, err := strconv.ParseBool(value); err != nil {
   216  			return errors.WrapPrefixf(err, "value must be a bool")
   217  		}
   218  	}
   219  	return nil
   220  }
   221  
   222  func (it FieldValueType) Tag() string {
   223  	switch it {
   224  	case String:
   225  		return yaml.NodeTagString
   226  	case Bool:
   227  		return yaml.NodeTagBool
   228  	case Int:
   229  		return yaml.NodeTagInt
   230  	}
   231  	return ""
   232  }
   233  
   234  func (it FieldValueType) TagForValue(value string) string {
   235  	switch it {
   236  	case String:
   237  		return yaml.NodeTagString
   238  	case Bool:
   239  		if _, err := strconv.ParseBool(string(it)); err != nil {
   240  			return ""
   241  		}
   242  		return yaml.NodeTagBool
   243  	case Int:
   244  		if _, err := strconv.ParseInt(string(it), 0, 32); err != nil {
   245  			return ""
   246  		}
   247  		return yaml.NodeTagInt
   248  	}
   249  	return ""
   250  }
   251  
   252  const (
   253  	// CLIDefinitionsPrefix is the prefix for cli definition keys.
   254  	CLIDefinitionsPrefix = "io.k8s.cli."
   255  
   256  	// SetterDefinitionPrefix is the prefix for setter definition keys.
   257  	SetterDefinitionPrefix = CLIDefinitionsPrefix + "setters."
   258  
   259  	// SubstitutionDefinitionPrefix is the prefix for substitution definition keys.
   260  	SubstitutionDefinitionPrefix = CLIDefinitionsPrefix + "substitutions."
   261  
   262  	// DefinitionsPrefix is the prefix used to reference definitions in the OpenAPI
   263  	DefinitionsPrefix = "#/definitions/"
   264  )
   265  
   266  // shortHandRef is the shorthand reference to setters and substitutions
   267  var shortHandRef = "$openapi"
   268  
   269  func SetShortHandRef(ref string) {
   270  	shortHandRef = ref
   271  }
   272  
   273  func ShortHandRef() string {
   274  	return shortHandRef
   275  }
   276  

View as plain text