mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-10 16:42:51 +00:00
Functions for identifying and fixing yaml 1.1 compatbility issues
- Identify if a field value would parse as a non-string yaml 1.1 value - Using OpenAPI Schema to get Resource's field type - Format yaml so value will be parsed as the correct type using a yaml 1.1 parser
This commit is contained in:
85
kyaml/yaml/compatibility.go
Normal file
85
kyaml/yaml/compatibility.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
y1_1 "gopkg.in/yaml.v2"
|
||||
y1_2 "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// typeToTag maps OpenAPI schema types to yaml 1.2 tags
|
||||
var typeToTag = map[string]string{
|
||||
"string": "!!str",
|
||||
"integer": "!!int",
|
||||
"boolean": "!!bool",
|
||||
"number": "!!float",
|
||||
}
|
||||
|
||||
// FormatNonStringStyle makes sure that values which parse as non-string values in yaml 1.1
|
||||
// are correctly formatted given the Schema type.
|
||||
func FormatNonStringStyle(node *Node, schema spec.Schema) {
|
||||
if len(schema.Type) != 1 {
|
||||
return
|
||||
}
|
||||
t := schema.Type[0]
|
||||
|
||||
if !IsYaml1_1NonString(node) {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case t == "string" && schema.Format != "int-or-string":
|
||||
if (node.Style&DoubleQuotedStyle == 0) && (node.Style&SingleQuotedStyle == 0) {
|
||||
// must quote values so they are parsed as strings
|
||||
node.Style = DoubleQuotedStyle
|
||||
}
|
||||
case t == "boolean" || t == "integer" || t == "number":
|
||||
if (node.Style&DoubleQuotedStyle != 0) || (node.Style&SingleQuotedStyle != 0) {
|
||||
// must NOT quote the values so they aren't parsed as strings
|
||||
node.Style = 0
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
if tag, found := typeToTag[t]; found {
|
||||
// make sure the right tag is set
|
||||
node.Tag = tag
|
||||
}
|
||||
}
|
||||
|
||||
// IsYaml1_1NonString returns true if the value parses as a non-string value in yaml 1.1
|
||||
// when unquoted.
|
||||
//
|
||||
// Note: yaml 1.2 uses different keywords than yaml 1.1. Example: yaml 1.2 interprets
|
||||
// `field: on` and `field: "on"` as equivalent (both strings). However Yaml 1.1 interprets
|
||||
// `field: on` as on being a bool and `field: "on"` as on being a string.
|
||||
// If an input is read with `field: "on"`, and the style is changed from DoubleQuote to 0,
|
||||
// it will change the type of the field from a string to a bool. For this reason, fields
|
||||
// which are keywords in yaml 1.1 should never have their style changed, as it would break
|
||||
// backwards compatibility with yaml 1.1 -- which is what is used by the Kubernetes apiserver.
|
||||
func IsYaml1_1NonString(node *Node) bool {
|
||||
if node.Kind != y1_2.ScalarNode {
|
||||
// not a keyword
|
||||
return false
|
||||
}
|
||||
if strings.Contains(node.Value, "\n") {
|
||||
// multi-line strings will fail to unmarshal
|
||||
return false
|
||||
}
|
||||
// check if the value will unmarshal into a non-string value using a yaml 1.1 parser
|
||||
var i1 interface{}
|
||||
if err := y1_1.Unmarshal([]byte(node.Value), &i1); err != nil {
|
||||
return false
|
||||
}
|
||||
if reflect.TypeOf(i1) != stringType {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var stringType = reflect.TypeOf("string")
|
||||
147
kyaml/yaml/compatibility_test.go
Normal file
147
kyaml/yaml/compatibility_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package yaml_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
func TestIsYaml1_1NonString(t *testing.T) {
|
||||
type testCase struct {
|
||||
val string
|
||||
expected bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{val: "hello world", expected: false},
|
||||
{val: "2.0", expected: true},
|
||||
{val: "1.0\nhello", expected: false}, // multiline strings should always be false
|
||||
}
|
||||
|
||||
for k := range valueToTagMap {
|
||||
testCases = append(testCases, testCase{val: k, expected: true})
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
assert.Equal(t, test.expected,
|
||||
yaml.IsYaml1_1NonString(&yaml.Node{Kind: yaml.ScalarNode, Value: test.val}), test.val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatNonStringStyle(t *testing.T) {
|
||||
n := yaml.MustParse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: foo
|
||||
args:
|
||||
- bar
|
||||
- on
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: "80"
|
||||
`)
|
||||
s := openapi.SchemaForResourceType(
|
||||
yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"})
|
||||
|
||||
args, err := n.Pipe(yaml.Lookup(
|
||||
"spec", "template", "spec", "containers", "[name=foo]", "args"))
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.NotNil(t, args) {
|
||||
t.FailNow()
|
||||
}
|
||||
on := args.YNode().Content[1]
|
||||
onS := s.
|
||||
SchemaForField("spec").
|
||||
SchemaForField("template").
|
||||
SchemaForField("spec").
|
||||
SchemaForField("containers").
|
||||
SchemaForElements().
|
||||
SchemaForField("args").
|
||||
SchemaForElements()
|
||||
yaml.FormatNonStringStyle(on, *onS.Schema)
|
||||
|
||||
containerPort, err := n.Pipe(yaml.Lookup(
|
||||
"spec", "template", "spec", "containers", "[name=foo]", "ports",
|
||||
"[name=http]", "containerPort"))
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.NotNil(t, containerPort) {
|
||||
t.FailNow()
|
||||
}
|
||||
cpS := s.
|
||||
SchemaForField("spec").
|
||||
SchemaForField("template").
|
||||
SchemaForField("spec").
|
||||
SchemaForField("containers").
|
||||
SchemaForElements().
|
||||
SchemaForField("ports").
|
||||
SchemaForElements().
|
||||
SchemaForField("containerPort")
|
||||
if !assert.NotNil(t, cpS) {
|
||||
t.FailNow()
|
||||
}
|
||||
yaml.FormatNonStringStyle(containerPort.YNode(), *cpS.Schema)
|
||||
|
||||
actual := n.MustString()
|
||||
expected := `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: foo
|
||||
args:
|
||||
- bar
|
||||
- "on"
|
||||
image: nginx:1.7.9
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
`
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
// valueToTagMap is a map of values interpreted as non-strings in yaml 1.1 when left
|
||||
// unquoted.
|
||||
// To keep compatibility with the yaml parser used by Kubernetes (yaml 1.1) make sure the values
|
||||
// which are treated as non-string values are kept as non-string values.
|
||||
// https://github.com/go-yaml/yaml/blob/v2/resolve.go
|
||||
var valueToTagMap = func() map[string]string {
|
||||
val := map[string]string{}
|
||||
|
||||
// https://yaml.org/type/null.html
|
||||
values := []string{"", "~", "null", "Null", "NULL"}
|
||||
for i := range values {
|
||||
val[values[i]] = "!!null"
|
||||
}
|
||||
|
||||
// https://yaml.org/type/bool.html
|
||||
values = []string{
|
||||
"y", "Y", "yes", "Yes", "YES", "true", "True", "TRUE", "on", "On", "ON", "n", "N", "no",
|
||||
"No", "NO", "false", "False", "FALSE", "off", "Off", "OFF"}
|
||||
for i := range values {
|
||||
val[values[i]] = "!!bool"
|
||||
}
|
||||
|
||||
// https://yaml.org/type/float.html
|
||||
values = []string{
|
||||
".nan", ".NaN", ".NAN", ".inf", ".Inf", ".INF",
|
||||
"+.inf", "+.Inf", "+.INF", "-.inf", "-.Inf", "-.INF"}
|
||||
for i := range values {
|
||||
val[values[i]] = "!!float"
|
||||
}
|
||||
|
||||
return val
|
||||
}()
|
||||
Reference in New Issue
Block a user