yaml formatter improvements

- identify and fix yaml 1.1 compatibility issues in configuration
- support providing function for performing custom formatting
This commit is contained in:
Phillip Wittrock
2020-01-06 10:37:26 -08:00
parent 04f5e6c953
commit 54e92f1ab0
2 changed files with 236 additions and 7 deletions

View File

@@ -27,6 +27,7 @@ import (
"sort"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
@@ -54,7 +55,9 @@ func FormatFileOrDirectory(path string) error {
}.Execute()
}
type FormatFilter struct{}
type FormatFilter struct {
Process func(n *yaml.Node) error
}
var _ kio.Filter = FormatFilter{}
@@ -75,7 +78,9 @@ func (f FormatFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
continue
}
kind, apiVersion := kindNode.YNode().Value, apiVersionNode.YNode().Value
err = (&formatter{apiVersion: apiVersion, kind: kind}).fmtNode(slice[i].YNode(), "")
s := openapi.SchemaForResourceType(yaml.TypeMeta{APIVersion: apiVersion, Kind: kind})
err = (&formatter{apiVersion: apiVersion, kind: kind, process: f.Process}).
fmtNode(slice[i].YNode(), "", s)
if err != nil {
return nil, err
}
@@ -86,10 +91,17 @@ func (f FormatFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
type formatter struct {
apiVersion string
kind string
process func(n *yaml.Node) error
}
// fmtNode recursively formats the Document Contents.
func (f *formatter) fmtNode(n *yaml.Node, path string) error {
func (f *formatter) fmtNode(n *yaml.Node, path string, schema *openapi.ResourceSchema) error {
if n.Kind == yaml.ScalarNode && schema != nil && schema.Schema != nil {
// ensure values that are interpreted as non-string values (e.g. "true")
// are properly quoted
yaml.FormatNonStringStyle(n, *schema.Schema)
}
// sort the order of mapping fields
if n.Kind == yaml.MappingNode {
sort.Sort(sortedMapContents(*n))
@@ -104,12 +116,43 @@ func (f *formatter) fmtNode(n *yaml.Node, path string) error {
}
}
}
// format the Content
for i := range n.Content {
p := path
if n.Kind == yaml.MappingNode && i%2 == 1 {
p = fmt.Sprintf("%s.%s", path, n.Content[i-1].Value)
// MappingNode are structured as having their fields as Content,
// with the field-key and field-value alternating. e.g. Even elements
// are the keys and odd elements are the values
isFieldKey := n.Kind == yaml.MappingNode && i%2 == 0
isFieldValue := n.Kind == yaml.MappingNode && i%2 == 1
isElement := n.Kind == yaml.SequenceNode
// run the process callback on the node if it has been set
// don't process keys: their format should be fixed
if f.process != nil && !isFieldKey {
if err := f.process(n.Content[i]); err != nil {
return err
}
}
err := f.fmtNode(n.Content[i], p)
// get the schema for this Node
p := path
var s *openapi.ResourceSchema
switch {
case isFieldValue:
// if the node is a field, lookup the schema using the field name
p = fmt.Sprintf("%s.%s", path, n.Content[i-1].Value)
if schema != nil {
s = schema.SchemaForField(n.Content[i-1].Value)
}
case isElement:
// if the node is a list element, lookup the schema for the array items
if schema != nil {
s = schema.SchemaForElements()
}
}
// format the node using the schema
err := f.fmtNode(n.Content[i], p, s)
if err != nil {
return err
}
@@ -143,6 +186,7 @@ func (s sortedMapContents) Swap(i, j int) {
s.Content[iFieldValueIndex], s.Content[jFieldValueIndex] = s.
Content[jFieldValueIndex], s.Content[iFieldValueIndex]
}
func (s sortedMapContents) Less(i, j int) bool {
iFieldNameIndex := i * 2
jFieldNameIndex := j * 2

View File

@@ -13,10 +13,195 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/kio"
. "sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/kio/filters/testyaml"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
func TestFormatInput_FixYaml1_1Compatibility(t *testing.T) {
y := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
labels:
foo: on
foo2: hello1
annotations:
bar: 1
bar2: hello2
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.0.0
args:
- on
- 1
- hello
ports:
- name: http
targetPort: 80
containerPort: 80
`
// keep the style on values that parse as non-string types
expected := `apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
labels:
foo: "on"
foo2: hello1
annotations:
bar: "1"
bar2: hello2
spec:
template:
spec:
containers:
- name: nginx
image: nginx:1.0.0
args:
- "on"
- "1"
- hello
ports:
- name: http
targetPort: 80
containerPort: 80
`
buff := &bytes.Buffer{}
err := kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: strings.NewReader(y)}},
Filters: []kio.Filter{FormatFilter{}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: buff}},
}.Execute()
assert.NoError(t, err)
assert.Equal(t, expected, buff.String())
}
func TestFormatInput_PostprocessStyle(t *testing.T) {
y := `
apiVersion: v1
kind: Foo
metadata:
name: foo
spec:
notBoolean: "true"
notBoolean2: "on"
isBoolean: on
isBoolean2: true
notInt: "12345"
isInt: 12345
isString1: hello world
isString2: "hello world"
`
// keep the style on values that parse as non-string types
expected := `apiVersion: v1
kind: Foo
metadata:
name: foo
spec:
isBoolean: on
isBoolean2: true
isInt: 12345
isString1: hello world
isString2: hello world
notBoolean: "true"
notBoolean2: "on"
notInt: "12345"
`
buff := &bytes.Buffer{}
err := kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: strings.NewReader(y)}},
Filters: []kio.Filter{FormatFilter{Process: func(n *yaml.Node) error {
if yaml.IsYaml1_1NonString(n) {
// don't change these styles, they are important for backwards compatibility
// e.g. "on" must remain quoted, on must remain unquoted
return nil
}
// style does not have semantic meaning
n.Style = 0
return nil
}}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: buff}},
}.Execute()
assert.NoError(t, err)
assert.Equal(t, expected, buff.String())
y = `
apiVersion: v1
kind: Foo
metadata:
name: 'foo'
spec:
notBoolean: "true"
notBoolean2: "on"
notBoolean3: y is yes
isBoolean: on
isBoolean2: true
isBoolean3: y
notInt2: 1234 five
notInt3: one 2345
notInt: "12345"
isInt1: 12345
isInt2: -12345
isFloat1: 1.1234
isFloat2: 1.1234
isString1: hello world
isString2: "hello world"
isString3: 'hello world'
`
// keep the style on values that parse as non-string types
expected = `apiVersion: 'v1'
kind: 'Foo'
metadata:
name: 'foo'
spec:
isBoolean: on
isBoolean2: true
isBoolean3: y
isFloat1: 1.1234
isFloat2: 1.1234
isInt1: 12345
isInt2: -12345
isString1: 'hello world'
isString2: 'hello world'
isString3: 'hello world'
notBoolean: "true"
notBoolean2: "on"
notBoolean3: 'y is yes'
notInt: "12345"
notInt2: '1234 five'
notInt3: 'one 2345'
`
buff = &bytes.Buffer{}
err = kio.Pipeline{
Inputs: []kio.Reader{&kio.ByteReader{Reader: strings.NewReader(y)}},
Filters: []kio.Filter{FormatFilter{Process: func(n *yaml.Node) error {
if yaml.IsYaml1_1NonString(n) {
// don't change these styles, they are important for backwards compatibility
// e.g. "on" must remain quoted, on must remain unquoted
return nil
}
// style does not have semantic meaning
n.Style = yaml.SingleQuotedStyle
return nil
}}},
Outputs: []kio.Writer{kio.ByteWriter{Writer: buff}},
}.Execute()
assert.NoError(t, err)
assert.Equal(t, expected, buff.String())
}
func TestFormatInput_Style(t *testing.T) {
y := `
apiVersion: v1