istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/object/objects.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 /* 16 Package manifest provides functions for going between in-memory k8s objects (unstructured.Unstructured) and their JSON 17 or YAML representations. 18 */ 19 package object 20 21 import ( 22 "bytes" 23 "fmt" 24 "io" 25 "sort" 26 "strings" 27 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 "k8s.io/apimachinery/pkg/runtime/serializer/json" 31 "k8s.io/apimachinery/pkg/util/intstr" 32 k8syaml "k8s.io/apimachinery/pkg/util/yaml" 33 "sigs.k8s.io/yaml" 34 35 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 36 "istio.io/istio/operator/pkg/helm" 37 names "istio.io/istio/operator/pkg/name" 38 "istio.io/istio/operator/pkg/tpath" 39 "istio.io/istio/operator/pkg/util" 40 "istio.io/istio/pkg/log" 41 ) 42 43 const ( 44 // YAMLSeparator is a separator for multi-document YAML files. 45 YAMLSeparator = "\n---\n" 46 ) 47 48 // K8sObject is an in-memory representation of a k8s object, used for moving between different representations 49 // (Unstructured, JSON, YAML) with cached rendering. 50 type K8sObject struct { 51 object *unstructured.Unstructured 52 53 Group string 54 Kind string 55 Name string 56 Namespace string 57 58 json []byte 59 yaml []byte 60 } 61 62 // NewK8sObject creates a new K8sObject and returns a ptr to it. 63 func NewK8sObject(u *unstructured.Unstructured, json, yaml []byte) *K8sObject { 64 o := &K8sObject{ 65 object: u, 66 json: json, 67 yaml: yaml, 68 } 69 70 gvk := u.GetObjectKind().GroupVersionKind() 71 o.Group = gvk.Group 72 o.Kind = gvk.Kind 73 o.Name = u.GetName() 74 o.Namespace = u.GetNamespace() 75 76 return o 77 } 78 79 // Hash returns a unique, insecure hash based on kind, namespace and name. 80 func Hash(kind, namespace, name string) string { 81 switch kind { 82 case names.ClusterRoleStr, names.ClusterRoleBindingStr: 83 namespace = "" 84 } 85 return strings.Join([]string{kind, namespace, name}, ":") 86 } 87 88 // FromHash parses kind, namespace and name from a hash. 89 func FromHash(hash string) (kind, namespace, name string) { 90 hv := strings.Split(hash, ":") 91 if len(hv) != 3 { 92 return "Bad hash string: " + hash, "", "" 93 } 94 kind, namespace, name = hv[0], hv[1], hv[2] 95 return 96 } 97 98 // HashNameKind returns a unique, insecure hash based on kind and name. 99 func HashNameKind(kind, name string) string { 100 return strings.Join([]string{kind, name}, ":") 101 } 102 103 // ParseJSONToK8sObject parses JSON to an K8sObject. 104 func ParseJSONToK8sObject(json []byte) (*K8sObject, error) { 105 o, _, err := unstructured.UnstructuredJSONScheme.Decode(json, nil, nil) 106 if err != nil { 107 return nil, fmt.Errorf("error parsing json into unstructured object: %v", err) 108 } 109 110 u, ok := o.(*unstructured.Unstructured) 111 if !ok { 112 return nil, fmt.Errorf("parsed unexpected type %T", o) 113 } 114 115 return NewK8sObject(u, json, nil), nil 116 } 117 118 // ParseYAMLToK8sObject parses YAML to an Object. 119 func ParseYAMLToK8sObject(yaml []byte) (*K8sObject, error) { 120 objects, err := ParseK8sObjectsFromYAMLManifest(string(yaml)) 121 if err != nil { 122 return nil, err 123 } 124 if len(objects) > 1 { 125 return nil, fmt.Errorf("expect one object, actually: %d", len(objects)) 126 } 127 if len(objects) == 0 || objects[0] == nil { 128 return nil, fmt.Errorf("decoding object %v: %v", string(yaml), "no object found") 129 } 130 return objects[0], nil 131 } 132 133 // UnstructuredObject exposes the raw object, primarily for testing 134 func (o *K8sObject) UnstructuredObject() *unstructured.Unstructured { 135 return o.object 136 } 137 138 // ResolveK8sConflict - This method resolves k8s object possible 139 // conflicting settings. Which K8sObjects may need such method 140 // depends on the type of the K8sObject. 141 func (o *K8sObject) ResolveK8sConflict() *K8sObject { 142 if o.Kind == names.PDBStr { 143 return resolvePDBConflict(o) 144 } 145 return o 146 } 147 148 // Unstructured exposes the raw object content, primarily for testing 149 func (o *K8sObject) Unstructured() map[string]any { 150 return o.UnstructuredObject().UnstructuredContent() 151 } 152 153 // Container returns a container subtree for Deployment objects if one is found, or nil otherwise. 154 func (o *K8sObject) Container(name string) map[string]any { 155 u := o.Unstructured() 156 path := fmt.Sprintf("spec.template.spec.containers.[name:%s]", name) 157 node, f, err := tpath.GetPathContext(u, util.PathFromString(path), false) 158 if err == nil && f { 159 // Must be the type from the schema. 160 return node.Node.(map[string]any) 161 } 162 return nil 163 } 164 165 // GroupVersionKind returns the GroupVersionKind for the K8sObject 166 func (o *K8sObject) GroupVersionKind() schema.GroupVersionKind { 167 return o.object.GroupVersionKind() 168 } 169 170 // Version returns the APIVersion of the K8sObject 171 func (o *K8sObject) Version() string { 172 return o.object.GetAPIVersion() 173 } 174 175 // Hash returns a unique hash for the K8sObject 176 func (o *K8sObject) Hash() string { 177 return Hash(o.Kind, o.Namespace, o.Name) 178 } 179 180 // HashNameKind returns a hash for the K8sObject based on the name and kind only. 181 func (o *K8sObject) HashNameKind() string { 182 return HashNameKind(o.Kind, o.Name) 183 } 184 185 // JSON returns a JSON representation of the K8sObject, using an internal cache. 186 func (o *K8sObject) JSON() ([]byte, error) { 187 if o.json != nil { 188 return o.json, nil 189 } 190 191 b, err := o.object.MarshalJSON() 192 if err != nil { 193 return nil, err 194 } 195 return b, nil 196 } 197 198 // YAML returns a YAML representation of the K8sObject, using an internal cache. 199 func (o *K8sObject) YAML() ([]byte, error) { 200 if o == nil { 201 return nil, nil 202 } 203 if o.yaml != nil { 204 return o.yaml, nil 205 } 206 oj, err := o.JSON() 207 if err != nil { 208 return nil, err 209 } 210 o.json = oj 211 y, err := yaml.JSONToYAML(oj) 212 if err != nil { 213 return nil, err 214 } 215 o.yaml = y 216 return y, nil 217 } 218 219 // YAMLDebugString returns a YAML representation of the K8sObject, or an error string if the K8sObject cannot be rendered to YAML. 220 func (o *K8sObject) YAMLDebugString() string { 221 y, err := o.YAML() 222 if err != nil { 223 return err.Error() 224 } 225 return string(y) 226 } 227 228 // K8sObjects holds a collection of k8s objects, so that we can filter / sequence them 229 type K8sObjects []*K8sObject 230 231 // String implements the Stringer interface. 232 func (os K8sObjects) String() string { 233 var out []string 234 for _, oo := range os { 235 out = append(out, oo.YAMLDebugString()) 236 } 237 return strings.Join(out, helm.YAMLSeparator) 238 } 239 240 // Keys returns a slice with the keys of os. 241 func (os K8sObjects) Keys() []string { 242 out := make([]string, 0, len(os)) 243 for _, oo := range os { 244 out = append(out, oo.Hash()) 245 } 246 return out 247 } 248 249 // UnstructuredItems returns the list of items of unstructured.Unstructured. 250 func (os K8sObjects) UnstructuredItems() []unstructured.Unstructured { 251 usList := make([]unstructured.Unstructured, 0, len(os)) 252 for _, obj := range os { 253 usList = append(usList, *obj.UnstructuredObject()) 254 } 255 return usList 256 } 257 258 // ParseK8sObjectsFromYAMLManifest returns a K8sObjects representation of manifest. 259 func ParseK8sObjectsFromYAMLManifest(manifest string) (K8sObjects, error) { 260 return ParseK8sObjectsFromYAMLManifestFailOption(manifest, true) 261 } 262 263 // ParseK8sObjectsFromYAMLManifestFailOption returns a K8sObjects representation of manifest. Continues parsing when a bad object 264 // is found if failOnError is set to false. 265 func ParseK8sObjectsFromYAMLManifestFailOption(manifest string, failOnError bool) (K8sObjects, error) { 266 jsonDecoder := k8syaml.NewYAMLToJSONDecoder(bytes.NewReader([]byte(manifest))) 267 var objects K8sObjects 268 269 wrapErr := func(err error) error { 270 return fmt.Errorf("failed to parse YAML to a k8s object: %v", err) 271 } 272 273 s := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, json.SerializerOptions{ 274 Yaml: true, 275 Pretty: true, 276 Strict: true, 277 }) 278 for { 279 var obj unstructured.Unstructured 280 if err := jsonDecoder.Decode(&obj); err != nil { 281 if err == io.EOF { 282 break 283 } 284 err = wrapErr(err) 285 if failOnError { 286 return nil, err 287 } 288 log.Error(err.Error()) 289 continue 290 } 291 if obj.Object == nil { 292 continue 293 } 294 295 if !isValidKubernetesObject(obj) { 296 if failOnError { 297 err := wrapErr(fmt.Errorf("failed to parse YAML to a k8s object: object is an invalid k8s object: %v", obj)) 298 return nil, err 299 } 300 } 301 302 // Convert the unstructured object back into YAML, without comments 303 var buf bytes.Buffer 304 if err := s.Encode(&obj, &buf); err != nil { 305 err = wrapErr(err) 306 if failOnError { 307 return nil, err 308 } 309 log.Error(err.Error()) 310 continue 311 } 312 cleanedYaml := buf.String() 313 314 k8sObj := NewK8sObject(&obj, nil, []byte(cleanedYaml)) 315 if k8sObj.Valid() { 316 objects = append(objects, k8sObj) 317 } 318 } 319 return objects, nil 320 } 321 322 // YAMLManifest returns a YAML representation of K8sObjects os. 323 func (os K8sObjects) YAMLManifest() (string, error) { 324 var b bytes.Buffer 325 326 for i, item := range os { 327 if i != 0 { 328 if _, err := b.WriteString("\n\n"); err != nil { 329 return "", err 330 } 331 } 332 ym, err := item.YAML() 333 if err != nil { 334 return "", fmt.Errorf("error building yaml: %v", err) 335 } 336 if _, err := b.Write(ym); err != nil { 337 return "", err 338 } 339 if _, err := b.WriteString(YAMLSeparator); err != nil { 340 return "", err 341 } 342 343 } 344 345 return b.String(), nil 346 } 347 348 // Sort will order the items in K8sObjects in order of score, group, kind, name. The intent is to 349 // have a deterministic ordering in which K8sObjects are applied. 350 func (os K8sObjects) Sort(score func(o *K8sObject) int) { 351 sort.Slice(os, func(i, j int) bool { 352 iScore := score(os[i]) 353 jScore := score(os[j]) 354 return iScore < jScore || 355 (iScore == jScore && 356 os[i].Group < os[j].Group) || 357 (iScore == jScore && 358 os[i].Group == os[j].Group && 359 os[i].Kind < os[j].Kind) || 360 (iScore == jScore && 361 os[i].Group == os[j].Group && 362 os[i].Kind == os[j].Kind && 363 os[i].Name < os[j].Name) 364 }) 365 } 366 367 // ToMap returns a map of K8sObject hash to K8sObject. 368 func (os K8sObjects) ToMap() map[string]*K8sObject { 369 ret := make(map[string]*K8sObject) 370 for _, oo := range os { 371 if oo.Valid() { 372 ret[oo.Hash()] = oo 373 } 374 } 375 return ret 376 } 377 378 // ToNameKindMap returns a map of K8sObject name/kind hash to K8sObject. 379 func (os K8sObjects) ToNameKindMap() map[string]*K8sObject { 380 ret := make(map[string]*K8sObject) 381 for _, oo := range os { 382 if oo.Valid() { 383 ret[oo.HashNameKind()] = oo 384 } 385 } 386 return ret 387 } 388 389 // Valid checks returns true if Kind of K8sObject is not empty. 390 func (o *K8sObject) Valid() bool { 391 return o.Kind != "" 392 } 393 394 // FullName returns namespace/name of K8s object 395 func (o *K8sObject) FullName() string { 396 return fmt.Sprintf("%s/%s", o.Namespace, o.Name) 397 } 398 399 // Equal returns true if o and other are both valid and equal to each other. 400 func (o *K8sObject) Equal(other *K8sObject) bool { 401 if o == nil { 402 return other == nil 403 } 404 if other == nil { 405 return o == nil 406 } 407 408 ay, err := o.YAML() 409 if err != nil { 410 return false 411 } 412 by, err := other.YAML() 413 if err != nil { 414 return false 415 } 416 417 return util.IsYAMLEqual(string(ay), string(by)) 418 } 419 420 // DefaultObjectOrder is default sorting function used to sort k8s objects. 421 func DefaultObjectOrder() func(o *K8sObject) int { 422 return func(o *K8sObject) int { 423 gk := o.Group + "/" + o.Kind 424 switch { 425 // Create CRDs asap - both because they are slow and because we will likely create instances of them soon 426 case gk == "apiextensions.k8s.io/CustomResourceDefinition": 427 return -1000 428 429 // We need to create ServiceAccounts, Roles before we bind them with a RoleBinding 430 case gk == "/ServiceAccount" || gk == "rbac.authorization.k8s.io/ClusterRole": 431 return 1 432 case gk == "rbac.authorization.k8s.io/ClusterRoleBinding": 433 return 2 434 435 // validatingwebhookconfiguration is configured to FAIL-OPEN in the default install. For the 436 // re-install case we want to apply the validatingwebhookconfiguration first to reset any 437 // orphaned validatingwebhookconfiguration that is FAIL-CLOSE. 438 case gk == "admissionregistration.k8s.io/ValidatingWebhookConfiguration": 439 return 3 440 441 // Pods might need configmap or secrets - avoid backoff by creating them first 442 case gk == "/ConfigMap" || gk == "/Secrets": 443 return 100 444 445 // Create the pods after we've created other things they might be waiting for 446 case gk == "extensions/Deployment" || gk == "apps/Deployment": 447 return 1000 448 449 // Autoscalers typically act on a deployment 450 case gk == "autoscaling/HorizontalPodAutoscaler": 451 return 1001 452 453 // Create services late - after pods have been started 454 case gk == "/Service": 455 return 10000 456 457 default: 458 return 1000 459 } 460 } 461 } 462 463 func ObjectsNotInLists(objects K8sObjects, lists ...K8sObjects) K8sObjects { 464 var ret K8sObjects 465 466 filterMap := make(map[*K8sObject]bool) 467 for _, list := range lists { 468 for _, object := range list { 469 filterMap[object] = true 470 } 471 } 472 473 for _, o := range objects { 474 if !filterMap[o] { 475 ret = append(ret, o) 476 } 477 } 478 return ret 479 } 480 481 // KindObjects returns the subset of objs with the given kind. 482 func KindObjects(objs K8sObjects, kind string) K8sObjects { 483 var ret K8sObjects 484 for _, o := range objs { 485 if o.Kind == kind { 486 ret = append(ret, o) 487 } 488 } 489 return ret 490 } 491 492 // ParseK8SYAMLToIstioOperator parses a IstioOperator CustomResource YAML string and unmarshals in into 493 // an IstioOperatorSpec object. It returns the object and an API group/version with it. 494 func ParseK8SYAMLToIstioOperator(yml string) (*v1alpha1.IstioOperator, *schema.GroupVersionKind, error) { 495 o, err := ParseYAMLToK8sObject([]byte(yml)) 496 if err != nil { 497 return nil, nil, err 498 } 499 iop := &v1alpha1.IstioOperator{} 500 if err := yaml.UnmarshalStrict([]byte(yml), iop); err != nil { 501 return nil, nil, err 502 } 503 gvk := o.GroupVersionKind() 504 v1alpha1.SetNamespace(iop.Spec, o.Namespace) 505 return iop, &gvk, nil 506 } 507 508 // AllObjectHashes returns a map with object hashes of all the objects contained in cmm as the keys. 509 func AllObjectHashes(m string) map[string]bool { 510 ret := make(map[string]bool) 511 objs, err := ParseK8sObjectsFromYAMLManifest(m) 512 if err != nil { 513 log.Error(err.Error()) 514 } 515 for _, o := range objs { 516 ret[o.Hash()] = true 517 } 518 519 return ret 520 } 521 522 // resolvePDBConflict When user uses both minAvailable and 523 // maxUnavailable to configure istio instances, these two 524 // parameters are mutually exclusive, care must be taken 525 // to resolve the issue 526 func resolvePDBConflict(o *K8sObject) *K8sObject { 527 if o.json == nil { 528 return o 529 } 530 if o.object.Object["spec"] == nil { 531 return o 532 } 533 spec := o.object.Object["spec"].(map[string]any) 534 isDefault := func(item any) bool { 535 var ii intstr.IntOrString 536 switch item := item.(type) { 537 case int: 538 ii = intstr.FromInt32(int32(item)) 539 case int64: 540 ii = intstr.FromInt32(int32(item)) 541 case string: 542 ii = intstr.FromString(item) 543 default: 544 ii = intstr.FromInt32(0) 545 } 546 intVal, err := intstr.GetScaledValueFromIntOrPercent(&ii, 100, false) 547 if err != nil || intVal == 0 { 548 return true 549 } 550 return false 551 } 552 if spec["maxUnavailable"] != nil && spec["minAvailable"] != nil { 553 // When both maxUnavailable and minAvailable present and 554 // neither has value 0, this is considered a conflict, 555 // then maxUnavailable will take precedence. 556 if !isDefault(spec["maxUnavailable"]) && !isDefault(spec["minAvailable"]) { 557 delete(spec, "minAvailable") 558 // Make sure that the json and yaml representation of the object 559 // is consistent with the changed object 560 o.json = nil 561 o.json, _ = o.JSON() 562 if o.yaml != nil { 563 o.yaml = nil 564 o.yaml, _ = o.YAML() 565 } 566 } 567 } 568 return o 569 } 570 571 func isValidKubernetesObject(obj unstructured.Unstructured) bool { 572 if _, ok := obj.Object["apiVersion"]; !ok { 573 return false 574 } 575 if _, ok := obj.Object["kind"]; !ok { 576 return false 577 } 578 return true 579 }