// Copyright (c) 2022 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package util import ( "bufio" "bytes" "fmt" "io" "strings" jsonpatch "github.com/evanphx/json-patch/v5" // nolint: staticcheck "github.com/kylelemons/godebug/diff" yaml3 "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/yaml" ) // //func ToYAMLGeneric(root any) ([]byte, error) { // var vs []byte // if proto, ok := root.(proto.Message); ok { // v, err := protomarshal.ToYAML(proto) // if err != nil { // return nil, err // } // vs = []byte(v) // } else { // v, err := yaml.Marshal(root) // if err != nil { // return nil, err // } // vs = v // } // return vs, nil //} // //func MustToYAMLGeneric(root any) string { // var vs []byte // if proto, ok := root.(proto.Message); ok { // v, err := protomarshal.ToYAML(proto) // if err != nil { // return err.Error() // } // vs = []byte(v) // } else { // v, err := yaml.Marshal(root) // if err != nil { // return err.Error() // } // vs = v // } // return string(vs) //} // ToYAML returns a YAML string representation of val, or the error string if an error occurs. func ToYAML(val any) string { y, err := yaml.Marshal(val) if err != nil { return err.Error() } return string(y) } // //// ToYAMLWithJSONPB returns a YAML string representation of val (using jsonpb), or the error string if an error occurs. //func ToYAMLWithJSONPB(val proto.Message) string { // v := reflect.ValueOf(val) // if val == nil || (v.Kind() == reflect.Ptr && v.IsNil()) { // return "null" // } // js, err := protomarshal.ToJSONWithOptions(val, "", true) // if err != nil { // return err.Error() // } // yb, err := yaml.JSONToYAML([]byte(js)) // if err != nil { // return err.Error() // } // return string(yb) //} // //// MarshalWithJSONPB returns a YAML string representation of val (using jsonpb). //func MarshalWithJSONPB(val proto.Message) (string, error) { // return protomarshal.ToYAML(val) //} // //// UnmarshalWithJSONPB unmarshals y into out using gogo jsonpb (required for many proto defined structs). //func UnmarshalWithJSONPB(y string, out proto.Message, allowUnknownField bool) error { // // Treat nothing as nothing. If we called jsonpb.Unmarshaler it would return the same. // if y == "" { // return nil // } // jb, err := yaml.YAMLToJSON([]byte(y)) // if err != nil { // return err // } // // if allowUnknownField { // err = protomarshal.UnmarshalAllowUnknown(jb, out) // } else { // err = protomarshal.Unmarshal(jb, out) // } // if err != nil { // return err // } // return nil //} // OverlayTrees performs a sequential JSON strategic of overlays over base. func OverlayTrees(base map[string]any, overlays ...map[string]any) (map[string]any, error) { needsOverlay := false for _, o := range overlays { if len(o) > 0 { needsOverlay = true break } } if !needsOverlay { // Avoid expensive overlay if possible return base, nil } bby, err := yaml.Marshal(base) if err != nil { return nil, err } by := string(bby) for _, o := range overlays { oy, err := yaml.Marshal(o) if err != nil { return nil, err } by, err = OverlayYAML(by, string(oy)) if err != nil { return nil, err } } out := make(map[string]any) err = yaml.Unmarshal([]byte(by), &out) if err != nil { return nil, err } return out, nil } // OverlayYAML patches the overlay tree over the base tree and returns the result. All trees are expressed as YAML // strings. func OverlayYAML(base, overlay string) (string, error) { if strings.TrimSpace(base) == "" { return overlay, nil } if strings.TrimSpace(overlay) == "" { return base, nil } bj, err := yaml.YAMLToJSON([]byte(base)) if err != nil { return "", fmt.Errorf("yamlToJSON error in base: %s\n%s", err, bj) } oj, err := yaml.YAMLToJSON([]byte(overlay)) if err != nil { return "", fmt.Errorf("yamlToJSON error in overlay: %s\n%s", err, oj) } if base == "" { bj = []byte("{}") } if overlay == "" { oj = []byte("{}") } merged, err := jsonpatch.MergePatch(bj, oj) if err != nil { return "", fmt.Errorf("json merge error (%s) for base object: \n%s\n override object: \n%s", err, bj, oj) } my, err := yaml.JSONToYAML(merged) if err != nil { return "", fmt.Errorf("jsonToYAML error (%s) for merged object: \n%s", err, merged) } return string(my), nil } // yamlDiff compares single YAML file func yamlDiff(a, b string) string { ao, bo := make(map[string]any), make(map[string]any) if err := yaml.Unmarshal([]byte(a), &ao); err != nil { return err.Error() } if err := yaml.Unmarshal([]byte(b), &bo); err != nil { return err.Error() } ay, err := yaml.Marshal(ao) if err != nil { return err.Error() } by, err := yaml.Marshal(bo) if err != nil { return err.Error() } return diff.Diff(string(ay), string(by)) } // yamlStringsToList yaml string parse to string list func yamlStringsToList(str string) []string { reader := bufio.NewReader(strings.NewReader(str)) decoder := yaml3.NewYAMLReader(reader) res := make([]string, 0) for { doc, err := decoder.Read() if err == io.EOF { break } if err != nil { break } chunk := bytes.TrimSpace(doc) res = append(res, string(chunk)) } return res } // multiYamlDiffOutput multi yaml diff output format func multiYamlDiffOutput(res, diff string) string { if res == "" { return diff } if diff == "" { return res } return res + "\n" + diff } func diffStringList(l1, l2 []string) string { var maxLen int var minLen int var l1Max bool res := "" if len(l1)-len(l2) > 0 { maxLen = len(l1) minLen = len(l2) l1Max = true } else { maxLen = len(l2) minLen = len(l1) l1Max = false } for i := 0; i < maxLen; i++ { d := "" if i >= minLen { if l1Max { d = yamlDiff(l1[i], "") } else { d = yamlDiff("", l2[i]) } } else { d = yamlDiff(l1[i], l2[i]) } res = multiYamlDiffOutput(res, d) } return res } // YAMLDiff compares multiple YAML files and single YAML file func YAMLDiff(a, b string) string { al := yamlStringsToList(a) bl := yamlStringsToList(b) res := diffStringList(al, bl) return res } // IsYAMLEqual reports whether the YAML in strings a and b are equal. func IsYAMLEqual(a, b string) bool { if strings.TrimSpace(a) == "" && strings.TrimSpace(b) == "" { return true } ajb, err := yaml.YAMLToJSON([]byte(a)) if err != nil { return false } bjb, err := yaml.YAMLToJSON([]byte(b)) if err != nil { return false } return bytes.Equal(ajb, bjb) } // IsYAMLEmpty reports whether the YAML string y is logically empty. func IsYAMLEmpty(y string) bool { var yc []string for _, l := range strings.Split(y, "\n") { yt := strings.TrimSpace(l) if !strings.HasPrefix(yt, "#") && !strings.HasPrefix(yt, "---") { yc = append(yc, l) } } res := strings.TrimSpace(strings.Join(yc, "\n")) return res == "{}" || res == "" }