diff --git a/kyaml/kio/filters/fmtr.go b/kyaml/kio/filters/fmtr.go index 04796ed9b..c46dbec5a 100644 --- a/kyaml/kio/filters/fmtr.go +++ b/kyaml/kio/filters/fmtr.go @@ -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 diff --git a/kyaml/kio/filters/fmtr_test.go b/kyaml/kio/filters/fmtr_test.go index e40c403ea..dff6148f6 100644 --- a/kyaml/kio/filters/fmtr_test.go +++ b/kyaml/kio/filters/fmtr_test.go @@ -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