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 }