// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package kustfile import ( "bytes" "errors" "fmt" "io" "log" "reflect" "regexp" "strings" "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", "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", "Inventory", "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) { // nolint 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 } data, err = types.FixKustomizationPreUnmarshalling(data) if err != nil { return nil, err } var k types.Kustomization err = k.Unmarshal(data) if err != nil { return nil, err } k.FixKustomizationPostUnmarshalling() err = mf.parseCommentedFields(data) if err != nil { return nil, err } return &k, err } 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) } // StringInSlice returns true if the string is in the slice. func StringInSlice(str string, list []string) bool { for _, v := range list { if v == str { return true } } return false } 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) v := r.FieldByName(strings.Title(field)) if !v.IsValid() || isEmpty(v) { return []byte{}, nil } k := &types.Kustomization{} kr := reflect.ValueOf(k) kv := kr.Elem().FieldByName(strings.Title(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 }