...

Source file src/sigs.k8s.io/kustomize/kyaml/kio/filters/fmtr.go

Documentation: sigs.k8s.io/kustomize/kyaml/kio/filters

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  // Package yamlfmt contains libraries for formatting yaml files containing
     5  // Kubernetes Resource configuration.
     6  //
     7  // Yaml files are formatted by:
     8  // - Sorting fields and map values
     9  // - Sorting unordered lists for whitelisted types
    10  // - Applying a canonical yaml Style
    11  //
    12  // Fields are ordered using a relative ordering applied to commonly
    13  // encountered Resource fields.  All Resources,  including non-builtin
    14  // Resources such as CRDs, share the same field precedence.
    15  //
    16  // Fields that do not appear in the explicit ordering are ordered
    17  // lexicographically.
    18  //
    19  // A subset of well known known unordered lists are sorted by element field
    20  // values.
    21  package filters
    22  
    23  import (
    24  	"bytes"
    25  	"fmt"
    26  	"io"
    27  	"sort"
    28  
    29  	"sigs.k8s.io/kustomize/kyaml/kio"
    30  	"sigs.k8s.io/kustomize/kyaml/openapi"
    31  	"sigs.k8s.io/kustomize/kyaml/yaml"
    32  )
    33  
    34  type FormattingStrategy = string
    35  
    36  const (
    37  	// NoFmtAnnotation determines if the resource should be formatted.
    38  	FmtAnnotation string = "config.kubernetes.io/formatting"
    39  
    40  	// FmtStrategyStandard means the resource will be formatted according
    41  	// to the default rules.
    42  	FmtStrategyStandard FormattingStrategy = "standard"
    43  
    44  	// FmtStrategyNone means the resource will not be formatted.
    45  	FmtStrategyNone FormattingStrategy = "none"
    46  )
    47  
    48  // FormatInput returns the formatted input.
    49  func FormatInput(input io.Reader) (*bytes.Buffer, error) {
    50  	buff := &bytes.Buffer{}
    51  	err := kio.Pipeline{
    52  		Inputs:  []kio.Reader{&kio.ByteReader{Reader: input}},
    53  		Filters: []kio.Filter{FormatFilter{}},
    54  		Outputs: []kio.Writer{kio.ByteWriter{Writer: buff}},
    55  	}.Execute()
    56  
    57  	return buff, err
    58  }
    59  
    60  // FormatFileOrDirectory reads the file or directory and formats each file's
    61  // contents by writing it back to the file.
    62  func FormatFileOrDirectory(path string) error {
    63  	return kio.Pipeline{
    64  		Inputs: []kio.Reader{kio.LocalPackageReader{
    65  			PackagePath: path,
    66  		}},
    67  		Filters: []kio.Filter{FormatFilter{}},
    68  		Outputs: []kio.Writer{kio.LocalPackageWriter{PackagePath: path}},
    69  	}.Execute()
    70  }
    71  
    72  type FormatFilter struct {
    73  	Process   func(n *yaml.Node) error
    74  	UseSchema bool
    75  }
    76  
    77  var _ kio.Filter = FormatFilter{}
    78  
    79  func (f FormatFilter) Filter(slice []*yaml.RNode) ([]*yaml.RNode, error) {
    80  	for i := range slice {
    81  		fmtStrategy, err := getFormattingStrategy(slice[i])
    82  		if err != nil {
    83  			return nil, err
    84  		}
    85  
    86  		if fmtStrategy == FmtStrategyNone {
    87  			continue
    88  		}
    89  
    90  		kindNode, err := slice[i].Pipe(yaml.Get("kind"))
    91  		if err != nil {
    92  			return nil, err
    93  		}
    94  		if kindNode == nil {
    95  			continue
    96  		}
    97  		apiVersionNode, err := slice[i].Pipe(yaml.Get("apiVersion"))
    98  		if err != nil {
    99  			return nil, err
   100  		}
   101  		if apiVersionNode == nil {
   102  			continue
   103  		}
   104  		kind, apiVersion := kindNode.YNode().Value, apiVersionNode.YNode().Value
   105  		var s *openapi.ResourceSchema
   106  		if f.UseSchema {
   107  			s = openapi.SchemaForResourceType(yaml.TypeMeta{APIVersion: apiVersion, Kind: kind})
   108  		} else {
   109  			s = nil
   110  		}
   111  		err = (&formatter{apiVersion: apiVersion, kind: kind, process: f.Process}).
   112  			fmtNode(slice[i].YNode(), "", s)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  	}
   117  	return slice, nil
   118  }
   119  
   120  // getFormattingStrategy looks for the formatting annotation to determine
   121  // which strategy should be used for formatting. The default is standard
   122  // if no annotation is found.
   123  func getFormattingStrategy(node *yaml.RNode) (FormattingStrategy, error) {
   124  	value, err := node.Pipe(yaml.GetAnnotation(FmtAnnotation))
   125  	if err != nil || value == nil {
   126  		return FmtStrategyStandard, err
   127  	}
   128  
   129  	fmtStrategy := value.YNode().Value
   130  
   131  	switch fmtStrategy {
   132  	case FmtStrategyStandard:
   133  		return FmtStrategyStandard, nil
   134  	case FmtStrategyNone:
   135  		return FmtStrategyNone, nil
   136  	default:
   137  		return "", fmt.Errorf(
   138  			"formatting annotation has illegal value %s", fmtStrategy)
   139  	}
   140  }
   141  
   142  type formatter struct {
   143  	apiVersion string
   144  	kind       string
   145  	process    func(n *yaml.Node) error
   146  }
   147  
   148  // fmtNode recursively formats the Document Contents.
   149  // See: https://godoc.org/gopkg.in/yaml.v3#Node
   150  func (f *formatter) fmtNode(n *yaml.Node, path string, schema *openapi.ResourceSchema) error {
   151  	if n.Kind == yaml.ScalarNode && schema != nil && schema.Schema != nil {
   152  		// ensure values that are interpreted as non-string values (e.g. "true")
   153  		// are properly quoted
   154  		yaml.FormatNonStringStyle(n, *schema.Schema)
   155  	}
   156  
   157  	// sort the order of mapping fields
   158  	if n.Kind == yaml.MappingNode {
   159  		sort.Sort(sortedMapContents(*n))
   160  	}
   161  
   162  	// sort the order of sequence elements if it is whitelisted
   163  	if n.Kind == yaml.SequenceNode {
   164  		if yaml.WhitelistedListSortKinds.Has(f.kind) &&
   165  			yaml.WhitelistedListSortApis.Has(f.apiVersion) {
   166  			if sortField, found := yaml.WhitelistedListSortFields[path]; found {
   167  				sort.Sort(sortedSeqContents{Node: *n, sortField: sortField})
   168  			}
   169  		}
   170  	}
   171  
   172  	// format the Content
   173  	for i := range n.Content {
   174  		// MappingNode are structured as having their fields as Content,
   175  		// with the field-key and field-value alternating.  e.g. Even elements
   176  		// are the keys and odd elements are the values
   177  		isFieldKey := n.Kind == yaml.MappingNode && i%2 == 0
   178  		isFieldValue := n.Kind == yaml.MappingNode && i%2 == 1
   179  		isElement := n.Kind == yaml.SequenceNode
   180  
   181  		// run the process callback on the node if it has been set
   182  		// don't process keys: their format should be fixed
   183  		if f.process != nil && !isFieldKey {
   184  			if err := f.process(n.Content[i]); err != nil {
   185  				return err
   186  			}
   187  		}
   188  
   189  		// get the schema for this Node
   190  		p := path
   191  		var s *openapi.ResourceSchema
   192  		switch {
   193  		case isFieldValue:
   194  			// if the node is a field, lookup the schema using the field name
   195  			p = fmt.Sprintf("%s.%s", path, n.Content[i-1].Value)
   196  			if schema != nil {
   197  				s = schema.Field(n.Content[i-1].Value)
   198  			}
   199  		case isElement:
   200  			// if the node is a list element, lookup the schema for the array items
   201  			if schema != nil {
   202  				s = schema.Elements()
   203  			}
   204  		}
   205  		// format the node using the schema
   206  		err := f.fmtNode(n.Content[i], p, s)
   207  		if err != nil {
   208  			return err
   209  		}
   210  	}
   211  	return nil
   212  }
   213  
   214  // sortedMapContents sorts the Contents field of a MappingNode by the field names using a statically
   215  // defined field precedence, and falling back on lexicographical sorting
   216  type sortedMapContents yaml.Node
   217  
   218  func (s sortedMapContents) Len() int {
   219  	return len(s.Content) / 2
   220  }
   221  func (s sortedMapContents) Swap(i, j int) {
   222  	// yaml MappingNode Contents are a list of field names followed by
   223  	// field values, rather than a list of field <name, value> pairs.
   224  	// increment.
   225  	//
   226  	// e.g. ["field1Name", "field1Value", "field2Name", "field2Value"]
   227  	iFieldNameIndex := i * 2
   228  	jFieldNameIndex := j * 2
   229  	iFieldValueIndex := iFieldNameIndex + 1
   230  	jFieldValueIndex := jFieldNameIndex + 1
   231  
   232  	// swap field names
   233  	s.Content[iFieldNameIndex], s.Content[jFieldNameIndex] =
   234  		s.Content[jFieldNameIndex], s.Content[iFieldNameIndex]
   235  
   236  	// swap field values
   237  	s.Content[iFieldValueIndex], s.Content[jFieldValueIndex] = s.
   238  		Content[jFieldValueIndex], s.Content[iFieldValueIndex]
   239  }
   240  
   241  func (s sortedMapContents) Less(i, j int) bool {
   242  	iFieldNameIndex := i * 2
   243  	jFieldNameIndex := j * 2
   244  	iFieldName := s.Content[iFieldNameIndex].Value
   245  	jFieldName := s.Content[jFieldNameIndex].Value
   246  
   247  	// order by their precedence values looked up from the index
   248  	iOrder, foundI := yaml.FieldOrder[iFieldName]
   249  	jOrder, foundJ := yaml.FieldOrder[jFieldName]
   250  	if foundI && foundJ {
   251  		return iOrder < jOrder
   252  	}
   253  
   254  	// known fields come before unknown fields
   255  	if foundI {
   256  		return true
   257  	}
   258  	if foundJ {
   259  		return false
   260  	}
   261  
   262  	// neither field is known, sort them lexicographically
   263  	return iFieldName < jFieldName
   264  }
   265  
   266  // sortedSeqContents sorts the Contents field of a SequenceNode by the value of
   267  // the elements sortField.
   268  // e.g. it will sort spec.template.spec.containers by the value of the container `name` field
   269  type sortedSeqContents struct {
   270  	yaml.Node
   271  	sortField string
   272  }
   273  
   274  func (s sortedSeqContents) Len() int {
   275  	return len(s.Content)
   276  }
   277  func (s sortedSeqContents) Swap(i, j int) {
   278  	s.Content[i], s.Content[j] = s.Content[j], s.Content[i]
   279  }
   280  func (s sortedSeqContents) Less(i, j int) bool {
   281  	// primitive lists -- sort by the element's primitive values
   282  	if s.sortField == "" {
   283  		iValue := s.Content[i].Value
   284  		jValue := s.Content[j].Value
   285  		return iValue < jValue
   286  	}
   287  
   288  	// map lists -- sort by the element's sortField values
   289  	var iValue, jValue string
   290  	for a := range s.Content[i].Content {
   291  		if a%2 != 0 {
   292  			continue // not a fieldNameIndex
   293  		}
   294  		// locate the index of the sortField field
   295  		if s.Content[i].Content[a].Value == s.sortField {
   296  			// a is the yaml node for the field key, a+1 is the node for the field value
   297  			iValue = s.Content[i].Content[a+1].Value
   298  		}
   299  	}
   300  	for a := range s.Content[j].Content {
   301  		if a%2 != 0 {
   302  			continue // not a fieldNameIndex
   303  		}
   304  
   305  		// locate the index of the sortField field
   306  		if s.Content[j].Content[a].Value == s.sortField {
   307  			// a is the yaml node for the field key, a+1 is the node for the field value
   308  			jValue = s.Content[j].Content[a+1].Value
   309  		}
   310  	}
   311  
   312  	// compare the field values
   313  	return iValue < jValue
   314  }
   315  

View as plain text