mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-11 17:12:51 +00:00
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:
@@ -27,6 +27,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +55,9 @@ func FormatFileOrDirectory(path string) error {
|
|||||||
}.Execute()
|
}.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormatFilter struct{}
|
type FormatFilter struct {
|
||||||
|
Process func(n *yaml.Node) error
|
||||||
|
}
|
||||||
|
|
||||||
var _ kio.Filter = FormatFilter{}
|
var _ kio.Filter = FormatFilter{}
|
||||||
|
|
||||||
@@ -75,7 +78,9 @@ func (f FormatFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
kind, apiVersion := kindNode.YNode().Value, apiVersionNode.YNode().Value
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -86,10 +91,17 @@ func (f FormatFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
|
|||||||
type formatter struct {
|
type formatter struct {
|
||||||
apiVersion string
|
apiVersion string
|
||||||
kind string
|
kind string
|
||||||
|
process func(n *yaml.Node) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// fmtNode recursively formats the Document Contents.
|
// 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
|
// sort the order of mapping fields
|
||||||
if n.Kind == yaml.MappingNode {
|
if n.Kind == yaml.MappingNode {
|
||||||
sort.Sort(sortedMapContents(*n))
|
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 {
|
for i := range n.Content {
|
||||||
p := path
|
// MappingNode are structured as having their fields as Content,
|
||||||
if n.Kind == yaml.MappingNode && i%2 == 1 {
|
// with the field-key and field-value alternating. e.g. Even elements
|
||||||
p = fmt.Sprintf("%s.%s", path, n.Content[i-1].Value)
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -143,6 +186,7 @@ func (s sortedMapContents) Swap(i, j int) {
|
|||||||
s.Content[iFieldValueIndex], s.Content[jFieldValueIndex] = s.
|
s.Content[iFieldValueIndex], s.Content[jFieldValueIndex] = s.
|
||||||
Content[jFieldValueIndex], s.Content[iFieldValueIndex]
|
Content[jFieldValueIndex], s.Content[iFieldValueIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s sortedMapContents) Less(i, j int) bool {
|
func (s sortedMapContents) Less(i, j int) bool {
|
||||||
iFieldNameIndex := i * 2
|
iFieldNameIndex := i * 2
|
||||||
jFieldNameIndex := j * 2
|
jFieldNameIndex := j * 2
|
||||||
|
|||||||
@@ -13,10 +13,195 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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"
|
||||||
"sigs.k8s.io/kustomize/kyaml/kio/filters/testyaml"
|
"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) {
|
func TestFormatInput_Style(t *testing.T) {
|
||||||
y := `
|
y := `
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
Reference in New Issue
Block a user