istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/util/yaml.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package util
    16  
    17  import (
    18  	"bufio"
    19  	"bytes"
    20  	"fmt"
    21  	"io"
    22  	"reflect"
    23  	"strings"
    24  
    25  	jsonpatch "github.com/evanphx/json-patch/v5" // nolint: staticcheck
    26  	"github.com/kylelemons/godebug/diff"
    27  	"google.golang.org/protobuf/proto"
    28  	yaml3 "k8s.io/apimachinery/pkg/util/yaml"
    29  	"sigs.k8s.io/yaml"
    30  
    31  	"istio.io/istio/pkg/util/protomarshal"
    32  )
    33  
    34  func ToYAMLGeneric(root any) ([]byte, error) {
    35  	var vs []byte
    36  	if proto, ok := root.(proto.Message); ok {
    37  		v, err := protomarshal.ToYAML(proto)
    38  		if err != nil {
    39  			return nil, err
    40  		}
    41  		vs = []byte(v)
    42  	} else {
    43  		v, err := yaml.Marshal(root)
    44  		if err != nil {
    45  			return nil, err
    46  		}
    47  		vs = v
    48  	}
    49  	return vs, nil
    50  }
    51  
    52  func MustToYAMLGeneric(root any) string {
    53  	var vs []byte
    54  	if proto, ok := root.(proto.Message); ok {
    55  		v, err := protomarshal.ToYAML(proto)
    56  		if err != nil {
    57  			return err.Error()
    58  		}
    59  		vs = []byte(v)
    60  	} else {
    61  		v, err := yaml.Marshal(root)
    62  		if err != nil {
    63  			return err.Error()
    64  		}
    65  		vs = v
    66  	}
    67  	return string(vs)
    68  }
    69  
    70  // ToYAML returns a YAML string representation of val, or the error string if an error occurs.
    71  func ToYAML(val any) string {
    72  	y, err := yaml.Marshal(val)
    73  	if err != nil {
    74  		return err.Error()
    75  	}
    76  	return string(y)
    77  }
    78  
    79  // ToYAMLWithJSONPB returns a YAML string representation of val (using jsonpb), or the error string if an error occurs.
    80  func ToYAMLWithJSONPB(val proto.Message) string {
    81  	v := reflect.ValueOf(val)
    82  	if val == nil || (v.Kind() == reflect.Ptr && v.IsNil()) {
    83  		return "null"
    84  	}
    85  	js, err := protomarshal.ToJSONWithOptions(val, "", true)
    86  	if err != nil {
    87  		return err.Error()
    88  	}
    89  	yb, err := yaml.JSONToYAML([]byte(js))
    90  	if err != nil {
    91  		return err.Error()
    92  	}
    93  	return string(yb)
    94  }
    95  
    96  // MarshalWithJSONPB returns a YAML string representation of val (using jsonpb).
    97  func MarshalWithJSONPB(val proto.Message) (string, error) {
    98  	return protomarshal.ToYAML(val)
    99  }
   100  
   101  // UnmarshalWithJSONPB unmarshals y into out using gogo jsonpb (required for many proto defined structs).
   102  func UnmarshalWithJSONPB(y string, out proto.Message, allowUnknownField bool) error {
   103  	// Treat nothing as nothing.  If we called jsonpb.Unmarshaler it would return the same.
   104  	if y == "" {
   105  		return nil
   106  	}
   107  	jb, err := yaml.YAMLToJSON([]byte(y))
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	if allowUnknownField {
   113  		err = protomarshal.UnmarshalAllowUnknown(jb, out)
   114  	} else {
   115  		err = protomarshal.Unmarshal(jb, out)
   116  	}
   117  	if err != nil {
   118  		return err
   119  	}
   120  	return nil
   121  }
   122  
   123  // OverlayTrees performs a sequential JSON strategic of overlays over base.
   124  func OverlayTrees(base map[string]any, overlays ...map[string]any) (map[string]any, error) {
   125  	needsOverlay := false
   126  	for _, o := range overlays {
   127  		if len(o) > 0 {
   128  			needsOverlay = true
   129  			break
   130  		}
   131  	}
   132  	if !needsOverlay {
   133  		// Avoid expensive overlay if possible
   134  		return base, nil
   135  	}
   136  	bby, err := yaml.Marshal(base)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	by := string(bby)
   141  
   142  	for _, o := range overlays {
   143  		oy, err := yaml.Marshal(o)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  
   148  		by, err = OverlayYAML(by, string(oy))
   149  		if err != nil {
   150  			return nil, err
   151  		}
   152  	}
   153  
   154  	out := make(map[string]any)
   155  	err = yaml.Unmarshal([]byte(by), &out)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	return out, nil
   160  }
   161  
   162  // OverlayYAML patches the overlay tree over the base tree and returns the result. All trees are expressed as YAML
   163  // strings.
   164  func OverlayYAML(base, overlay string) (string, error) {
   165  	if strings.TrimSpace(base) == "" {
   166  		return overlay, nil
   167  	}
   168  	if strings.TrimSpace(overlay) == "" {
   169  		return base, nil
   170  	}
   171  	bj, err := yaml.YAMLToJSON([]byte(base))
   172  	if err != nil {
   173  		return "", fmt.Errorf("yamlToJSON error in base: %s\n%s", err, bj)
   174  	}
   175  	oj, err := yaml.YAMLToJSON([]byte(overlay))
   176  	if err != nil {
   177  		return "", fmt.Errorf("yamlToJSON error in overlay: %s\n%s", err, oj)
   178  	}
   179  	if base == "" {
   180  		bj = []byte("{}")
   181  	}
   182  	if overlay == "" {
   183  		oj = []byte("{}")
   184  	}
   185  
   186  	merged, err := jsonpatch.MergePatch(bj, oj)
   187  	if err != nil {
   188  		return "", fmt.Errorf("json merge error (%s) for base object: \n%s\n override object: \n%s", err, bj, oj)
   189  	}
   190  	my, err := yaml.JSONToYAML(merged)
   191  	if err != nil {
   192  		return "", fmt.Errorf("jsonToYAML error (%s) for merged object: \n%s", err, merged)
   193  	}
   194  
   195  	return string(my), nil
   196  }
   197  
   198  // yamlDiff compares single YAML file
   199  func yamlDiff(a, b string) string {
   200  	ao, bo := make(map[string]any), make(map[string]any)
   201  	if err := yaml.Unmarshal([]byte(a), &ao); err != nil {
   202  		return err.Error()
   203  	}
   204  	if err := yaml.Unmarshal([]byte(b), &bo); err != nil {
   205  		return err.Error()
   206  	}
   207  
   208  	ay, err := yaml.Marshal(ao)
   209  	if err != nil {
   210  		return err.Error()
   211  	}
   212  	by, err := yaml.Marshal(bo)
   213  	if err != nil {
   214  		return err.Error()
   215  	}
   216  
   217  	return diff.Diff(string(ay), string(by))
   218  }
   219  
   220  // yamlStringsToList yaml string parse to string list
   221  func yamlStringsToList(str string) []string {
   222  	reader := bufio.NewReader(strings.NewReader(str))
   223  	decoder := yaml3.NewYAMLReader(reader)
   224  	res := make([]string, 0)
   225  	for {
   226  		doc, err := decoder.Read()
   227  		if err == io.EOF {
   228  			break
   229  		}
   230  		if err != nil {
   231  			break
   232  		}
   233  
   234  		chunk := bytes.TrimSpace(doc)
   235  		res = append(res, string(chunk))
   236  	}
   237  	return res
   238  }
   239  
   240  // multiYamlDiffOutput multi yaml diff output format
   241  func multiYamlDiffOutput(res, diff string) string {
   242  	if res == "" {
   243  		return diff
   244  	}
   245  	if diff == "" {
   246  		return res
   247  	}
   248  
   249  	return res + "\n" + diff
   250  }
   251  
   252  func diffStringList(l1, l2 []string) string {
   253  	var maxLen int
   254  	var minLen int
   255  	var l1Max bool
   256  	res := ""
   257  	if len(l1)-len(l2) > 0 {
   258  		maxLen = len(l1)
   259  		minLen = len(l2)
   260  		l1Max = true
   261  	} else {
   262  		maxLen = len(l2)
   263  		minLen = len(l1)
   264  		l1Max = false
   265  	}
   266  
   267  	for i := 0; i < maxLen; i++ {
   268  		d := ""
   269  		if i >= minLen {
   270  			if l1Max {
   271  				d = yamlDiff(l1[i], "")
   272  			} else {
   273  				d = yamlDiff("", l2[i])
   274  			}
   275  		} else {
   276  			d = yamlDiff(l1[i], l2[i])
   277  		}
   278  		res = multiYamlDiffOutput(res, d)
   279  	}
   280  	return res
   281  }
   282  
   283  // YAMLDiff compares multiple YAML files and single YAML file
   284  func YAMLDiff(a, b string) string {
   285  	al := yamlStringsToList(a)
   286  	bl := yamlStringsToList(b)
   287  	res := diffStringList(al, bl)
   288  
   289  	return res
   290  }
   291  
   292  // IsYAMLEqual reports whether the YAML in strings a and b are equal.
   293  func IsYAMLEqual(a, b string) bool {
   294  	if strings.TrimSpace(a) == "" && strings.TrimSpace(b) == "" {
   295  		return true
   296  	}
   297  	ajb, err := yaml.YAMLToJSON([]byte(a))
   298  	if err != nil {
   299  		scope.Debugf("bad YAML in isYAMLEqual:\n%s", a)
   300  		return false
   301  	}
   302  	bjb, err := yaml.YAMLToJSON([]byte(b))
   303  	if err != nil {
   304  		scope.Debugf("bad YAML in isYAMLEqual:\n%s", b)
   305  		return false
   306  	}
   307  
   308  	return bytes.Equal(ajb, bjb)
   309  }
   310  
   311  // IsYAMLEmpty reports whether the YAML string y is logically empty.
   312  func IsYAMLEmpty(y string) bool {
   313  	var yc []string
   314  	for _, l := range strings.Split(y, "\n") {
   315  		yt := strings.TrimSpace(l)
   316  		if !strings.HasPrefix(yt, "#") && !strings.HasPrefix(yt, "---") {
   317  			yc = append(yc, l)
   318  		}
   319  	}
   320  	res := strings.TrimSpace(strings.Join(yc, "\n"))
   321  	return res == "{}" || res == ""
   322  }