Files
kustomize/kustomize/commands/internal/kustfile/kustomizationfile.go
Oliver F 16a7ce2b8b Fix sortOptions removal when running edit command (#5689)
* Fix sortOptions and Validators removal when running edit command

* fixed linting
2024-06-04 20:26:41 -07:00

306 lines
7.1 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package kustfile
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"reflect"
"regexp"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/filesys"
"sigs.k8s.io/yaml"
)
var fieldMarshallingOrder = determineFieldOrder()
// determineFieldOrder returns a slice of Kustomization field
// names in the preferred order for serialization to a file.
// The field list is checked against the actual struct type
// to confirm that all fields are present, and no unknown
// fields are specified. Deprecated fields are removed from
// the list, meaning they will drop to the bottom on output
// (if present). The ordering and/or deprecation of fields
// in nested structs is not determined or considered.
func determineFieldOrder() []string {
m := make(map[string]bool)
s := reflect.ValueOf(&types.Kustomization{}).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
m[typeOfT.Field(i).Name] = false
}
ordered := []string{
"MetaData",
"SortOptions",
"Resources",
"Bases",
"NamePrefix",
"NameSuffix",
"Namespace",
"Crds",
"CommonLabels",
"Labels",
"CommonAnnotations",
"PatchesStrategicMerge",
"PatchesJson6902",
"Patches",
"ConfigMapGenerator",
"SecretGenerator",
"HelmCharts",
"HelmChartInflationGenerator",
"HelmGlobals",
"GeneratorOptions",
"Vars",
"Images",
"Replacements",
"Replicas",
"Configurations",
"Generators",
"Transformers",
"Validators",
"Components",
"OpenAPI",
"BuildMetadata",
}
// Add deprecated fields here.
deprecated := map[string]bool{}
// Account for the inlined TypeMeta fields.
var result []string
result = append(result, "APIVersion", "Kind")
m["TypeMeta"] = true
// Make sure all these fields are recognized.
for _, n := range ordered {
if _, ok := m[n]; ok {
m[n] = true
} else {
log.Fatalf("%s is not a recognized field.", n)
}
// Keep if not deprecated.
if _, f := deprecated[n]; !f {
result = append(result, n)
}
}
return result
}
// commentedField records the comment associated with a kustomization field
// field has to be a recognized kustomization field
// comment can be empty
type commentedField struct {
field string
comment []byte
}
func (cf *commentedField) appendComment(comment []byte) {
cf.comment = append(cf.comment, comment...)
}
func squash(x [][]byte) []byte {
return bytes.Join(x, []byte(``))
}
type kustomizationFile struct {
path string
fSys filesys.FileSystem
originalFields []*commentedField
}
// NewKustomizationFile returns a new instance.
func NewKustomizationFile(fSys filesys.FileSystem) (*kustomizationFile, error) {
mf := &kustomizationFile{fSys: fSys}
err := mf.validate()
if err != nil {
return nil, err
}
return mf, nil
}
func (mf *kustomizationFile) GetPath() string {
if mf == nil {
return ""
}
return mf.path
}
func (mf *kustomizationFile) validate() error {
match := 0
var path []string
for _, kfilename := range konfig.RecognizedKustomizationFileNames() {
if mf.fSys.Exists(kfilename) {
match += 1
path = append(path, kfilename)
}
}
switch match {
case 0:
return fmt.Errorf(
"Missing kustomization file '%s'.\n",
konfig.DefaultKustomizationFileName())
case 1:
mf.path = path[0]
default:
return fmt.Errorf("Found multiple kustomization file: %v\n", path)
}
if mf.fSys.IsDir(mf.path) {
return fmt.Errorf("%s should be a file", mf.path)
}
return nil
}
func (mf *kustomizationFile) Read() (*types.Kustomization, error) {
data, err := mf.fSys.ReadFile(mf.path)
if err != nil {
return nil, err
}
var k types.Kustomization
if err := k.Unmarshal(data); err != nil {
return nil, err
}
k.FixKustomization()
if err := mf.parseCommentedFields(data); err != nil {
return nil, err
}
return &k, nil
}
func (mf *kustomizationFile) Write(kustomization *types.Kustomization) error {
if kustomization == nil {
return errors.New("util: kustomization file arg is nil")
}
data, err := mf.marshal(kustomization)
if err != nil {
return err
}
return mf.fSys.WriteFile(mf.path, data)
}
func (mf *kustomizationFile) parseCommentedFields(content []byte) error {
buffer := bytes.NewBuffer(content)
var comments [][]byte
line, err := buffer.ReadBytes('\n')
for err == nil {
if isCommentOrBlankLine(line) {
comments = append(comments, line)
} else {
matched, field := findMatchedField(line)
if matched {
mf.originalFields = append(mf.originalFields, &commentedField{field: field, comment: squash(comments)})
comments = [][]byte{}
} else if len(comments) > 0 && len(mf.originalFields) > 0 {
mf.originalFields[len(mf.originalFields)-1].appendComment(squash(comments))
comments = [][]byte{}
}
}
line, err = buffer.ReadBytes('\n')
}
if err != io.EOF {
return err
}
return nil
}
// marshal converts a kustomization to a byte stream.
func (mf *kustomizationFile) marshal(kustomization *types.Kustomization) ([]byte, error) {
var output []byte
for _, comment := range mf.originalFields {
output = append(output, comment.comment...)
content, err := marshalField(comment.field, kustomization)
if err != nil {
return content, err
}
output = append(output, content...)
}
for _, field := range fieldMarshallingOrder {
if mf.hasField(field) {
continue
}
content, err := marshalField(field, kustomization)
if err != nil {
return content, nil
}
output = append(output, content...)
}
return output, nil
}
func (mf *kustomizationFile) hasField(name string) bool {
for _, n := range mf.originalFields {
if n.field == name {
return true
}
}
return false
}
/*
isCommentOrBlankLine determines if a line is a comment or blank line
Return true for following lines
# This line is a comment
# This line is also a comment with several leading white spaces
(The line above is a blank line)
*/
func isCommentOrBlankLine(line []byte) bool {
s := bytes.TrimRight(bytes.TrimLeft(line, " "), "\n")
return len(s) == 0 || bytes.HasPrefix(s, []byte(`#`))
}
func findMatchedField(line []byte) (bool, string) {
for _, field := range fieldMarshallingOrder {
// (?i) is for case insensitive regexp matching
r := regexp.MustCompile("^(" + "(?i)" + field + "):")
if r.Match(line) {
return true, field
}
}
return false, ""
}
// marshalField marshal a given field of a kustomization object into yaml format.
// If the field wasn't in the original kustomization.yaml file or wasn't added,
// an empty []byte is returned.
func marshalField(field string, kustomization *types.Kustomization) ([]byte, error) {
r := reflect.ValueOf(*kustomization)
titleCaser := cases.Title(language.English, cases.NoLower)
v := r.FieldByName(titleCaser.String(field))
if !v.IsValid() || isEmpty(v) {
return []byte{}, nil
}
k := &types.Kustomization{}
kr := reflect.ValueOf(k)
kv := kr.Elem().FieldByName(titleCaser.String(field))
kv.Set(v)
return yaml.Marshal(k)
}
func isEmpty(v reflect.Value) bool {
// If v is a pointer type
if v.Type().Kind() == reflect.Ptr {
return v.IsNil()
}
return v.Len() == 0
}