istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/object/objects_test.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 object 16 17 import ( 18 "fmt" 19 "io" 20 "os" 21 "reflect" 22 "strings" 23 "testing" 24 25 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 26 k8syaml "k8s.io/apimachinery/pkg/util/yaml" 27 28 "istio.io/istio/operator/pkg/util" 29 "istio.io/istio/pkg/test/util/assert" 30 ) 31 32 func TestHash(t *testing.T) { 33 hashTests := []struct { 34 desc string 35 kind string 36 namespace string 37 name string 38 want string 39 }{ 40 {"CalculateHashForObjectWithNormalCharacter", "Service", "default", "ingressgateway", "Service:default:ingressgateway"}, 41 {"CalculateHashForObjectWithDash", "Deployment", "istio-system", "istio-pilot", "Deployment:istio-system:istio-pilot"}, 42 {"CalculateHashForObjectWithDot", "ConfigMap", "istio-system", "my.config", "ConfigMap:istio-system:my.config"}, 43 } 44 45 for _, tt := range hashTests { 46 t.Run(tt.desc, func(t *testing.T) { 47 got := Hash(tt.kind, tt.namespace, tt.name) 48 if got != tt.want { 49 t.Errorf("Hash(%s): got %s for kind %s, namespace %s, name %s, want %s", tt.desc, got, tt.kind, tt.namespace, tt.name, tt.want) 50 } 51 }) 52 } 53 } 54 55 func TestFromHash(t *testing.T) { 56 hashTests := []struct { 57 desc string 58 hash string 59 kind string 60 namespace string 61 name string 62 }{ 63 {"ParseHashWithNormalCharacter", "Service:default:ingressgateway", "Service", "default", "ingressgateway"}, 64 {"ParseHashForObjectWithDash", "Deployment:istio-system:istio-pilot", "Deployment", "istio-system", "istio-pilot"}, 65 {"ParseHashForObjectWithDot", "ConfigMap:istio-system:my.config", "ConfigMap", "istio-system", "my.config"}, 66 {"InvalidHash", "test", "Bad hash string: test", "", ""}, 67 } 68 69 for _, tt := range hashTests { 70 t.Run(tt.desc, func(t *testing.T) { 71 k, ns, name := FromHash(tt.hash) 72 if k != tt.kind || ns != tt.namespace || name != tt.name { 73 t.Errorf("FromHash(%s): got kind %s, namespace %s, name %s, want kind %s, namespace %s, name %s", tt.desc, k, ns, name, tt.kind, tt.namespace, tt.name) 74 } 75 }) 76 } 77 } 78 79 func TestHashNameKind(t *testing.T) { 80 hashNameKindTests := []struct { 81 desc string 82 kind string 83 name string 84 want string 85 }{ 86 {"CalculateHashNameKindForObjectWithNormalCharacter", "Service", "ingressgateway", "Service:ingressgateway"}, 87 {"CalculateHashNameKindForObjectWithDash", "Deployment", "istio-pilot", "Deployment:istio-pilot"}, 88 {"CalculateHashNameKindForObjectWithDot", "ConfigMap", "my.config", "ConfigMap:my.config"}, 89 } 90 91 for _, tt := range hashNameKindTests { 92 t.Run(tt.desc, func(t *testing.T) { 93 got := HashNameKind(tt.kind, tt.name) 94 if got != tt.want { 95 t.Errorf("HashNameKind(%s): got %s for kind %s, name %s, want %s", tt.desc, got, tt.kind, tt.name, tt.want) 96 } 97 }) 98 } 99 } 100 101 func TestParseJSONToK8sObject(t *testing.T) { 102 testDeploymentJSON := `{ 103 "apiVersion": "apps/v1", 104 "kind": "Deployment", 105 "metadata": { 106 "name": "istio-citadel", 107 "namespace": "istio-system", 108 "labels": { 109 "istio": "citadel" 110 } 111 }, 112 "spec": { 113 "replicas": 1, 114 "selector": { 115 "matchLabels": { 116 "istio": "citadel" 117 } 118 }, 119 "template": { 120 "metadata": { 121 "labels": { 122 "istio": "citadel" 123 } 124 }, 125 "spec": { 126 "containers": [ 127 { 128 "name": "citadel", 129 "image": "docker.io/istio/citadel:1.1.8", 130 "args": [ 131 "--append-dns-names=true", 132 "--grpc-port=8060", 133 "--grpc-hostname=citadel", 134 "--citadel-storage-namespace=istio-system", 135 "--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system", 136 "--monitoring-port=15014", 137 "--self-signed-ca=true" 138 ] 139 } 140 ] 141 } 142 } 143 } 144 }` 145 testPodJSON := `{ 146 "apiVersion": "v1", 147 "kind": "Pod", 148 "metadata": { 149 "name": "istio-galley-75bcd59768-hpt5t", 150 "namespace": "istio-system", 151 "labels": { 152 "istio": "galley" 153 } 154 }, 155 "spec": { 156 "containers": [ 157 { 158 "name": "galley", 159 "image": "docker.io/istio/galley:1.1.8", 160 "command": [ 161 "/usr/local/bin/galley", 162 "server", 163 "--meshConfigFile=/etc/mesh-config/mesh", 164 "--livenessProbeInterval=1s", 165 "--livenessProbePath=/healthliveness", 166 "--readinessProbePath=/healthready", 167 "--readinessProbeInterval=1s", 168 "--deployment-namespace=istio-system", 169 "--insecure=true", 170 "--validation-webhook-config-file", 171 "/etc/config/validatingwebhookconfiguration.yaml", 172 "--monitoringPort=15014", 173 "--log_output_level=default:info" 174 ], 175 "ports": [ 176 { 177 "containerPort": 443, 178 "protocol": "TCP" 179 }, 180 { 181 "containerPort": 15014, 182 "protocol": "TCP" 183 }, 184 { 185 "containerPort": 9901, 186 "protocol": "TCP" 187 } 188 ] 189 } 190 ] 191 } 192 }` 193 testServiceJSON := `{ 194 "apiVersion": "v1", 195 "kind": "Service", 196 "metadata": { 197 "labels": { 198 "app": "pilot" 199 }, 200 "name": "istio-pilot", 201 "namespace": "istio-system" 202 }, 203 "spec": { 204 "clusterIP": "10.102.230.31", 205 "ports": [ 206 { 207 "name": "grpc-xds", 208 "port": 15010, 209 "protocol": "TCP", 210 "targetPort": 15010 211 }, 212 { 213 "name": "https-xds", 214 "port": 15011, 215 "protocol": "TCP", 216 "targetPort": 15011 217 }, 218 { 219 "name": "http-legacy-discovery", 220 "port": 8080, 221 "protocol": "TCP", 222 "targetPort": 8080 223 }, 224 { 225 "name": "http-monitoring", 226 "port": 15014, 227 "protocol": "TCP", 228 "targetPort": 15014 229 } 230 ], 231 "selector": { 232 "istio": "pilot" 233 }, 234 "sessionAffinity": "None", 235 "type": "ClusterIP" 236 } 237 }` 238 239 testInvalidJSON := `invalid json` 240 241 parseJSONToK8sObjectTests := []struct { 242 desc string 243 objString string 244 wantGroup string 245 wantKind string 246 wantName string 247 wantNamespace string 248 wantErr bool 249 }{ 250 {"ParseJsonToK8sDeployment", testDeploymentJSON, "apps", "Deployment", "istio-citadel", "istio-system", false}, 251 {"ParseJsonToK8sPod", testPodJSON, "", "Pod", "istio-galley-75bcd59768-hpt5t", "istio-system", false}, 252 {"ParseJsonToK8sService", testServiceJSON, "", "Service", "istio-pilot", "istio-system", false}, 253 {"ParseJsonError", testInvalidJSON, "", "", "", "", true}, 254 } 255 256 for _, tt := range parseJSONToK8sObjectTests { 257 t.Run(tt.desc, func(t *testing.T) { 258 k8sObj, err := ParseJSONToK8sObject([]byte(tt.objString)) 259 if err == nil { 260 if tt.wantErr { 261 t.Errorf("ParseJsonToK8sObject(%s): should be error", tt.desc) 262 } 263 k8sObjStr := k8sObj.YAMLDebugString() 264 if k8sObj.Group != tt.wantGroup { 265 t.Errorf("ParseJsonToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Group, k8sObjStr, tt.wantGroup) 266 } 267 if k8sObj.Kind != tt.wantKind { 268 t.Errorf("ParseJsonToK8sObject(%s): got kind %s for k8s object %s, want %s", tt.desc, k8sObj.Kind, k8sObjStr, tt.wantKind) 269 } 270 if k8sObj.Name != tt.wantName { 271 t.Errorf("ParseJsonToK8sObject(%s): got name %s for k8s object %s, want %s", tt.desc, k8sObj.Name, k8sObjStr, tt.wantName) 272 } 273 if k8sObj.Namespace != tt.wantNamespace { 274 t.Errorf("ParseJsonToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Namespace, k8sObjStr, tt.wantNamespace) 275 } 276 } else if !tt.wantErr { 277 t.Errorf("ParseJsonToK8sObject(%s): got unexpected error: %v", tt.desc, err) 278 } 279 }) 280 } 281 } 282 283 func TestParseK8sObjectsFromYAMLManifest(t *testing.T) { 284 testDeploymentYaml := `apiVersion: apps/v1 285 kind: Deployment 286 metadata: 287 name: istio-citadel 288 namespace: istio-system 289 labels: 290 istio: citadel 291 spec: 292 replicas: 1 293 selector: 294 matchLabels: 295 istio: citadel 296 template: 297 metadata: 298 labels: 299 istio: citadel 300 spec: 301 containers: 302 - name: citadel 303 image: docker.io/istio/citadel:1.1.8 304 args: 305 - "--append-dns-names=true" 306 - "--grpc-port=8060" 307 - "--grpc-hostname=citadel" 308 - "--citadel-storage-namespace=istio-system" 309 - "--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system" 310 - "--monitoring-port=15014" 311 - "--self-signed-ca=true"` 312 313 testPodYaml := `apiVersion: v1 314 kind: Pod 315 metadata: 316 name: istio-galley-75bcd59768-hpt5t 317 namespace: istio-system 318 labels: 319 istio: galley 320 spec: 321 containers: 322 - name: galley 323 image: docker.io/istio/galley:1.1.8 324 command: 325 - "/usr/local/bin/galley" 326 - server 327 - "--meshConfigFile=/etc/mesh-config/mesh" 328 - "--livenessProbeInterval=1s" 329 - "--livenessProbePath=/healthliveness" 330 - "--readinessProbePath=/healthready" 331 - "--readinessProbeInterval=1s" 332 - "--deployment-namespace=istio-system" 333 - "--insecure=true" 334 - "--validation-webhook-config-file" 335 - "/etc/config/validatingwebhookconfiguration.yaml" 336 - "--monitoringPort=15014" 337 - "--log_output_level=default:info" 338 ports: 339 - containerPort: 443 340 protocol: TCP 341 - containerPort: 15014 342 protocol: TCP 343 - containerPort: 9901 344 protocol: TCP` 345 346 testServiceYaml := `apiVersion: v1 347 kind: Service 348 metadata: 349 labels: 350 app: pilot 351 name: istio-pilot 352 namespace: istio-system 353 spec: 354 clusterIP: 10.102.230.31 355 ports: 356 - name: grpc-xds 357 port: 15010 358 protocol: TCP 359 targetPort: 15010 360 - name: https-xds 361 port: 15011 362 protocol: TCP 363 targetPort: 15011 364 - name: http-legacy-discovery 365 port: 8080 366 protocol: TCP 367 targetPort: 8080 368 - name: http-monitoring 369 port: 15014 370 protocol: TCP 371 targetPort: 15014 372 selector: 373 istio: pilot 374 sessionAffinity: None 375 type: ClusterIP` 376 377 parseK8sObjectsFromYAMLManifestTests := []struct { 378 desc string 379 objsMap map[string]string 380 }{ 381 { 382 "FromHybridYAMLManifest", 383 map[string]string{ 384 "Deployment:istio-system:istio-citadel": testDeploymentYaml, 385 "Pod:istio-system:istio-galley-75bcd59768-hpt5t": testPodYaml, 386 "Service:istio-system:istio-pilot": testServiceYaml, 387 }, 388 }, 389 } 390 391 for _, tt := range parseK8sObjectsFromYAMLManifestTests { 392 t.Run(tt.desc, func(t *testing.T) { 393 testManifestYaml := strings.Join([]string{testDeploymentYaml, testPodYaml, testServiceYaml}, YAMLSeparator) 394 gotK8sObjs, err := ParseK8sObjectsFromYAMLManifest(testManifestYaml) 395 if err != nil { 396 gotK8sObjsMap := gotK8sObjs.ToMap() 397 for objHash, want := range tt.objsMap { 398 if gotObj, ok := gotK8sObjsMap[objHash]; ok { 399 gotObjYaml := gotObj.YAMLDebugString() 400 if !util.IsYAMLEqual(gotObjYaml, want) { 401 t.Errorf("ParseK8sObjectsFromYAMLManifest(%s): got:\n%s\n\nwant:\n%s\nDiff:\n%s\n", tt.desc, gotObjYaml, want, util.YAMLDiff(gotObjYaml, want)) 402 } 403 } 404 } 405 } 406 }) 407 } 408 } 409 410 func TestK8sObject_Equal(t *testing.T) { 411 obj1 := K8sObject{ 412 object: &unstructured.Unstructured{Object: map[string]any{ 413 "key": "value1", 414 }}, 415 } 416 obj2 := K8sObject{ 417 object: &unstructured.Unstructured{Object: map[string]any{ 418 "key": "value2", 419 }}, 420 } 421 cases := []struct { 422 desc string 423 o1 *K8sObject 424 o2 *K8sObject 425 want bool 426 }{ 427 { 428 desc: "Equals", 429 o1: &obj1, 430 o2: &obj1, 431 want: true, 432 }, 433 { 434 desc: "NotEquals", 435 o1: &obj1, 436 o2: &obj2, 437 want: false, 438 }, 439 { 440 desc: "NilSource", 441 o1: nil, 442 o2: &obj2, 443 want: false, 444 }, 445 { 446 desc: "NilDest", 447 o1: &obj1, 448 o2: nil, 449 want: false, 450 }, 451 { 452 desc: "TwoNils", 453 o1: nil, 454 o2: nil, 455 want: true, 456 }, 457 } 458 for _, tt := range cases { 459 t.Run(tt.desc, func(t *testing.T) { 460 res := tt.o1.Equal(tt.o2) 461 if res != tt.want { 462 t.Errorf("got %v, want: %v", res, tt.want) 463 } 464 }) 465 } 466 } 467 468 func TestK8sObject_ResolveK8sConflict(t *testing.T) { 469 getK8sObject := func(ystr string) *K8sObject { 470 o, err := ParseYAMLToK8sObject([]byte(ystr)) 471 if err != nil { 472 panic(err) 473 } 474 // Ensure that json data is in sync. 475 // Since the object was created using yaml, json is empty. 476 // make sure the object json is set correctly. 477 o.json, _ = o.JSON() 478 return o 479 } 480 481 cases := []struct { 482 desc string 483 o1 *K8sObject 484 o2 *K8sObject 485 }{ 486 { 487 desc: "not applicable kind", 488 o1: getK8sObject(` 489 apiVersion: v1 490 kind: Service 491 metadata: 492 labels: 493 app: pilot 494 name: istio-pilot 495 namespace: istio-system 496 spec: 497 clusterIP: 10.102.230.31`), 498 o2: getK8sObject(` 499 apiVersion: v1 500 kind: Service 501 metadata: 502 labels: 503 app: pilot 504 name: istio-pilot 505 namespace: istio-system 506 spec: 507 clusterIP: 10.102.230.31`), 508 }, 509 { 510 desc: "only minAvailable is set", 511 o1: getK8sObject(` 512 apiVersion: policy/v1 513 kind: PodDisruptionBudget 514 metadata: 515 name: zk-pdb 516 spec: 517 minAvailable: 2`), 518 o2: getK8sObject(` 519 apiVersion: policy/v1 520 kind: PodDisruptionBudget 521 metadata: 522 name: zk-pdb 523 spec: 524 minAvailable: 2`), 525 }, 526 { 527 desc: "only maxUnavailable is set", 528 o1: getK8sObject(` 529 apiVersion: policy/v1 530 kind: PodDisruptionBudget 531 metadata: 532 name: istio 533 spec: 534 maxUnavailable: 3`), 535 o2: getK8sObject(` 536 apiVersion: policy/v1 537 kind: PodDisruptionBudget 538 metadata: 539 name: istio 540 spec: 541 maxUnavailable: 3`), 542 }, 543 { 544 desc: "minAvailable and maxUnavailable are set to none zero values", 545 o1: getK8sObject(` 546 apiVersion: policy/v1 547 kind: PodDisruptionBudget 548 metadata: 549 name: istio 550 spec: 551 maxUnavailable: 50% 552 minAvailable: 3`), 553 o2: getK8sObject(` 554 apiVersion: policy/v1 555 kind: PodDisruptionBudget 556 metadata: 557 name: istio 558 spec: 559 maxUnavailable: 50%`), 560 }, 561 { 562 desc: "both minAvailable and maxUnavailable are set default", 563 o1: getK8sObject(` 564 apiVersion: policy/v1 565 kind: PodDisruptionBudget 566 metadata: 567 name: istio 568 spec: 569 minAvailable: 0 570 maxUnavailable: 0`), 571 o2: getK8sObject(` 572 apiVersion: policy/v1 573 kind: PodDisruptionBudget 574 metadata: 575 name: istio 576 spec: 577 maxUnavailable: 0 578 minAvailable: 0`), 579 }, 580 } 581 for _, tt := range cases { 582 t.Run(tt.desc, func(t *testing.T) { 583 newObj := tt.o1.ResolveK8sConflict() 584 if !newObj.Equal(tt.o2) { 585 newObjjson, _ := newObj.JSON() 586 wantedObjjson, _ := tt.o2.JSON() 587 t.Errorf("Got: %s, want: %s", string(newObjjson), string(wantedObjjson)) 588 } 589 }) 590 } 591 } 592 593 func TestParseK8sObjectsFromYAMLManifestFailOption(t *testing.T) { 594 cases := []struct { 595 name string 596 input string 597 failOnError bool 598 expectErr bool 599 expectCount int 600 expectOut bool 601 }{ 602 { 603 name: "well formed yaml, no errors", 604 input: "well-formed", 605 failOnError: false, 606 expectErr: false, 607 expectCount: 2, 608 }, 609 { 610 name: "malformed yaml, fail on error", 611 input: "malformed", 612 failOnError: true, 613 expectErr: true, 614 expectCount: 0, 615 }, 616 { 617 name: "malformed yaml, continue on error", 618 input: "malformed", 619 failOnError: false, 620 expectErr: false, 621 expectCount: 1, 622 }, 623 { 624 name: "space in the end of the manifest", 625 input: "well-formed-with-space", 626 expectCount: 1, 627 expectOut: true, 628 }, 629 { 630 name: "some random comments", 631 input: "well-formed-with-comments", 632 expectCount: 1, 633 expectOut: true, 634 }, 635 { 636 name: "invalid k8s object - missing kind", 637 input: "invalid", 638 failOnError: true, 639 expectErr: true, 640 expectCount: 0, 641 }, 642 { 643 name: "invalid k8s object - missing kind - skip error", 644 input: "invalid", 645 failOnError: false, 646 expectErr: false, 647 expectCount: 0, 648 }, 649 { 650 name: "empty object - do not have errors", 651 input: "empty", 652 failOnError: true, 653 expectErr: false, 654 expectCount: 0, 655 }, 656 } 657 658 for _, tc := range cases { 659 t.Run(tc.name, func(t *testing.T) { 660 inputFileName := fmt.Sprintf("testdata/%s.yaml", tc.input) 661 inputFile, err := os.Open(inputFileName) 662 if err != nil { 663 t.Errorf("error opening test data file: %v", err) 664 } 665 defer inputFile.Close() 666 manifest, err := io.ReadAll(inputFile) 667 if err != nil { 668 t.Errorf("error reading test data file: %v", err) 669 } 670 objects, err := ParseK8sObjectsFromYAMLManifestFailOption(string(manifest), tc.failOnError) 671 if tc.expectErr { 672 assert.Error(t, err) 673 } else { 674 assert.NoError(t, err) 675 } 676 assert.Equal(t, tc.expectCount, len(objects)) 677 if tc.expectOut { 678 outputFileName := fmt.Sprintf("testdata/%s.out.yaml", tc.input) 679 outputFile, err := os.Open(outputFileName) 680 if err != nil { 681 t.Errorf("error opening test data file: %v", err) 682 } 683 defer outputFile.Close() 684 expectedYAML, err := io.ReadAll(outputFile) 685 if err != nil { 686 t.Errorf("error reading test data file: %v", err) 687 } 688 expectedYAMLs := strings.Split(string(expectedYAML), "---") 689 if len(expectedYAMLs) != len(objects) { 690 t.Errorf("expected %d objects, got %d", len(expectedYAMLs), len(objects)) 691 } 692 for i, obj := range objects { 693 assert.Equal(t, true, compareYAMLContent(string(obj.yaml), expectedYAMLs[i])) 694 } 695 } 696 }) 697 } 698 } 699 700 // compareYAMLContent compares two yaml resources and returns true if they are equal. If they have same content but different 701 // order of fields, it will return true as well. 702 func compareYAMLContent(yaml1, yaml2 string) bool { 703 var obj1, obj2 interface{} 704 err := k8syaml.Unmarshal([]byte(yaml1), &obj1) 705 if err != nil { 706 return false 707 } 708 err = k8syaml.Unmarshal([]byte(yaml2), &obj2) 709 if err != nil { 710 return false 711 } 712 return reflect.DeepEqual(obj1, obj2) 713 }