k3s/vendor/sigs.k8s.io/kustomize/kyaml/fieldmeta/fieldmeta.go

276 lines
7.1 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package fieldmeta
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/go-openapi/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// FieldMeta contains metadata that may be attached to fields as comments
type FieldMeta struct {
Schema spec.Schema
Extensions XKustomize
SettersSchema *spec.Schema
}
type XKustomize struct {
SetBy string `yaml:"setBy,omitempty" json:"setBy,omitempty"`
PartialFieldSetters []PartialFieldSetter `yaml:"partialSetters,omitempty" json:"partialSetters,omitempty"`
FieldSetter *PartialFieldSetter `yaml:"setter,omitempty" json:"setter,omitempty"`
}
// PartialFieldSetter defines how to set part of a field rather than the full field
// value. e.g. the tag part of an image field
type PartialFieldSetter struct {
// Name is the name of this setter.
Name string `yaml:"name" json:"name"`
// Value is the current value that has been set.
Value string `yaml:"value" json:"value"`
}
// IsEmpty returns true if the FieldMeta has any empty Schema
func (fm *FieldMeta) IsEmpty() bool {
if fm == nil {
return true
}
return reflect.DeepEqual(fm.Schema, spec.Schema{})
}
// Read reads the FieldMeta from a node
func (fm *FieldMeta) Read(n *yaml.RNode) error {
// check for metadata on head and line comments
comments := []string{n.YNode().LineComment, n.YNode().HeadComment}
for _, c := range comments {
if c == "" {
continue
}
c := strings.TrimLeft(c, "#")
// check for new short hand notation or fall back to openAPI ref format
if !fm.processShortHand(c) {
// if it doesn't Unmarshal that is fine, it means there is no metadata
// other comments are valid, they just don't parse
// TODO: consider more sophisticated parsing techniques similar to what is used
// for go struct tags.
if err := fm.Schema.UnmarshalJSON([]byte(c)); err != nil {
// note: don't return an error if the comment isn't a fieldmeta struct
return nil
}
}
fe := fm.Schema.VendorExtensible.Extensions["x-kustomize"]
if fe == nil {
return nil
}
b, err := json.Marshal(fe)
if err != nil {
return errors.Wrap(err)
}
return json.Unmarshal(b, &fm.Extensions)
}
return nil
}
// processShortHand parses the comment for short hand ref, loads schema to fm
// and returns true if successful, returns false for any other cases and not throw
// error, as the comment might not be a setter ref
func (fm *FieldMeta) processShortHand(comment string) bool {
input := map[string]string{}
err := json.Unmarshal([]byte(comment), &input)
if err != nil {
return false
}
name := input[shortHandRef]
if name == "" {
return false
}
// check if setter with the name exists, else check for a substitution
// setter and substitution can't have same name in shorthand
setterRef, err := spec.NewRef(DefinitionsPrefix + SetterDefinitionPrefix + name)
if err != nil {
return false
}
setterRefBytes, err := setterRef.MarshalJSON()
if err != nil {
return false
}
if _, err := openapi.Resolve(&setterRef, fm.SettersSchema); err == nil {
setterErr := fm.Schema.UnmarshalJSON(setterRefBytes)
return setterErr == nil
}
substRef, err := spec.NewRef(DefinitionsPrefix + SubstitutionDefinitionPrefix + name)
if err != nil {
return false
}
substRefBytes, err := substRef.MarshalJSON()
if err != nil {
return false
}
if _, err := openapi.Resolve(&substRef, fm.SettersSchema); err == nil {
substErr := fm.Schema.UnmarshalJSON(substRefBytes)
return substErr == nil
}
return false
}
func isExtensionEmpty(x XKustomize) bool {
if x.FieldSetter != nil {
return false
}
if x.SetBy != "" {
return false
}
if len(x.PartialFieldSetters) > 0 {
return false
}
return true
}
// Write writes the FieldMeta to a node
func (fm *FieldMeta) Write(n *yaml.RNode) error {
if !isExtensionEmpty(fm.Extensions) {
return fm.WriteV1Setters(n)
}
// Ref is removed when a setter is deleted, so the Ref string could be empty.
if fm.Schema.Ref.String() != "" {
// Ex: {"$ref":"#/definitions/io.k8s.cli.setters.replicas"} should be converted to
// {"$openAPI":"replicas"} and added to the line comment
ref := fm.Schema.Ref.String()
var shortHandRefValue string
switch {
case strings.HasPrefix(ref, DefinitionsPrefix+SetterDefinitionPrefix):
shortHandRefValue = strings.TrimPrefix(ref, DefinitionsPrefix+SetterDefinitionPrefix)
case strings.HasPrefix(ref, DefinitionsPrefix+SubstitutionDefinitionPrefix):
shortHandRefValue = strings.TrimPrefix(ref, DefinitionsPrefix+SubstitutionDefinitionPrefix)
default:
return fmt.Errorf("unexpected ref format: %s", ref)
}
n.YNode().LineComment = fmt.Sprintf(`{"%s":"%s"}`, shortHandRef,
shortHandRefValue)
} else {
n.YNode().LineComment = ""
}
return nil
}
// WriteV1Setters is the v1 setters way of writing setter definitions
// TODO: pmarupaka - remove this method after migration
func (fm *FieldMeta) WriteV1Setters(n *yaml.RNode) error {
fm.Schema.VendorExtensible.AddExtension("x-kustomize", fm.Extensions)
b, err := json.Marshal(fm.Schema)
if err != nil {
return errors.Wrap(err)
}
n.YNode().LineComment = string(b)
return nil
}
// FieldValueType defines the type of input to register
type FieldValueType string
const (
// String defines a string flag
String FieldValueType = "string"
// Bool defines a bool flag
Bool = "boolean"
// Int defines an int flag
Int = "integer"
)
func (it FieldValueType) String() string {
if it == "" {
return "string"
}
return string(it)
}
func (it FieldValueType) Validate(value string) error {
switch it {
case Int:
if _, err := strconv.Atoi(value); err != nil {
return errors.WrapPrefixf(err, "value must be an int")
}
case Bool:
if _, err := strconv.ParseBool(value); err != nil {
return errors.WrapPrefixf(err, "value must be a bool")
}
}
return nil
}
func (it FieldValueType) Tag() string {
switch it {
case String:
return yaml.NodeTagString
case Bool:
return yaml.NodeTagBool
case Int:
return yaml.NodeTagInt
}
return ""
}
func (it FieldValueType) TagForValue(value string) string {
switch it {
case String:
return yaml.NodeTagString
case Bool:
if _, err := strconv.ParseBool(string(it)); err != nil {
return ""
}
return yaml.NodeTagBool
case Int:
if _, err := strconv.ParseInt(string(it), 0, 32); err != nil {
return ""
}
return yaml.NodeTagInt
}
return ""
}
const (
// CLIDefinitionsPrefix is the prefix for cli definition keys.
CLIDefinitionsPrefix = "io.k8s.cli."
// SetterDefinitionPrefix is the prefix for setter definition keys.
SetterDefinitionPrefix = CLIDefinitionsPrefix + "setters."
// SubstitutionDefinitionPrefix is the prefix for substitution definition keys.
SubstitutionDefinitionPrefix = CLIDefinitionsPrefix + "substitutions."
// DefinitionsPrefix is the prefix used to reference definitions in the OpenAPI
DefinitionsPrefix = "#/definitions/"
)
// shortHandRef is the shorthand reference to setters and substitutions
var shortHandRef = "$openapi"
func SetShortHandRef(ref string) {
shortHandRef = ref
}
func ShortHandRef() string {
return shortHandRef
}