istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/tpath/tree_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 tpath 16 17 import ( 18 "testing" 19 20 "sigs.k8s.io/yaml" 21 22 "istio.io/istio/operator/pkg/util" 23 ) 24 25 func TestWritePathContext(t *testing.T) { 26 rootYAML := ` 27 a: 28 b: 29 - name: n1 30 value: v1 31 - name: n2 32 list: 33 - v1 34 - v2 35 - v3_regex 36 ` 37 tests := []struct { 38 desc string 39 path string 40 value any 41 want string 42 wantFound bool 43 wantErr string 44 }{ 45 { 46 desc: "AddListEntry", 47 path: `a.b.[name:n2].list`, 48 value: `foo`, 49 wantFound: true, 50 want: ` 51 a: 52 b: 53 - name: n1 54 value: v1 55 - name: n2 56 list: 57 - v1 58 - v2 59 - v3_regex 60 - foo 61 `, 62 }, 63 { 64 desc: "ModifyListEntryValue", 65 path: `a.b.[name:n1].value`, 66 value: `v2`, 67 wantFound: true, 68 want: ` 69 a: 70 b: 71 - name: n1 72 value: v2 73 - list: 74 - v1 75 - v2 76 - v3_regex 77 name: n2 78 `, 79 }, 80 { 81 desc: "ModifyListEntryValueQuoted", 82 path: `a.b.[name:n1].value`, 83 value: `v2`, 84 wantFound: true, 85 want: ` 86 a: 87 b: 88 - name: "n1" 89 value: v2 90 - list: 91 - v1 92 - v2 93 - v3_regex 94 name: n2 95 `, 96 }, 97 { 98 desc: "ModifyListEntry", 99 path: `a.b.[name:n2].list.[:v2]`, 100 value: `v3`, 101 wantFound: true, 102 want: ` 103 a: 104 b: 105 - name: n1 106 value: v1 107 - list: 108 - v1 109 - v3 110 - v3_regex 111 name: n2 112 `, 113 }, 114 { 115 desc: "ModifyListEntryMapValue", 116 path: `a.b.[name:n2]`, 117 value: `name: n2 118 list: 119 - nk1: nv1 120 - nk2: nv2`, 121 wantFound: true, 122 want: ` 123 a: 124 b: 125 - name: n1 126 value: v1 127 - name: n2 128 list: 129 - nk1: nv1 130 - nk2: nv2 131 `, 132 }, 133 { 134 desc: "ModifyNthListEntry", 135 path: `a.b.[1].list.[:v2]`, 136 value: `v-the-second`, 137 wantFound: true, 138 want: ` 139 a: 140 b: 141 - name: n1 142 value: v1 143 - list: 144 - v1 145 - v-the-second 146 - v3_regex 147 name: n2 148 `, 149 }, 150 { 151 desc: "ModifyNthLeafListEntry", 152 path: `a.b.[1].list.[2]`, 153 value: `v-the-third`, 154 wantFound: true, 155 want: ` 156 a: 157 b: 158 - name: n1 159 value: v1 160 - list: 161 - v1 162 - v2 163 - v-the-third 164 name: n2 165 `, 166 }, 167 { 168 desc: "ModifyListEntryValueDotless", 169 path: `a.b[name:n1].value`, 170 value: `v2`, 171 wantFound: true, 172 want: ` 173 a: 174 b: 175 - name: n1 176 value: v2 177 - list: 178 - v1 179 - v2 180 - v3_regex 181 name: n2 182 `, 183 }, 184 { 185 desc: "DeleteListEntry", 186 path: `a.b.[name:n1]`, 187 wantFound: true, 188 want: ` 189 a: 190 b: 191 - list: 192 - v1 193 - v2 194 - v3_regex 195 name: n2 196 `, 197 }, 198 { 199 desc: "DeleteListEntryValue", 200 path: `a.b.[name:n2].list.[:v2]`, 201 wantFound: true, 202 want: ` 203 a: 204 b: 205 - name: n1 206 value: v1 207 - list: 208 - v1 209 - v3_regex 210 name: n2 211 `, 212 }, 213 { 214 desc: "DeleteListEntryIndex", 215 path: `a.b.[name:n2].list.[1]`, 216 wantFound: true, 217 want: ` 218 a: 219 b: 220 - name: n1 221 value: v1 222 - list: 223 - v1 224 - v3_regex 225 name: n2 226 `, 227 }, 228 { 229 desc: "DeleteListEntryValueRegex", 230 path: `a.b.[name:n2].list.[:v3]`, 231 wantFound: true, 232 want: ` 233 a: 234 b: 235 - name: n1 236 value: v1 237 - list: 238 - v1 239 - v2 240 name: n2 241 `, 242 }, 243 { 244 desc: "DeleteListLeafEntryBogusIndex", 245 path: `a.b.[name:n2].list.[-200]`, 246 wantFound: false, 247 wantErr: `path a.b.[name:n2].list.[-200]: element [-200] not found`, 248 }, 249 { 250 desc: "DeleteListEntryBogusIndex", 251 path: `a.b.[1000000].list.[:v2]`, 252 wantFound: false, 253 wantErr: `index 1000000 exceeds list length 2 at path [1000000].list.[:v2]`, 254 }, 255 { 256 desc: "AddMapEntry", 257 path: `a.new_key`, 258 value: `new_val`, 259 wantFound: true, 260 want: ` 261 a: 262 b: 263 - name: n1 264 value: v1 265 - name: n2 266 list: 267 - v1 268 - v2 269 - v3_regex 270 new_key: new_val 271 `, 272 }, 273 { 274 desc: "AddMapEntryMapValue", 275 path: `a.new_key`, 276 value: `new_key: 277 nk1: 278 nk2: nv2`, 279 wantFound: true, 280 want: ` 281 a: 282 b: 283 - name: n1 284 value: v1 285 - name: n2 286 list: 287 - v1 288 - v2 289 - v3_regex 290 new_key: 291 nk1: 292 nk2: nv2 293 `, 294 }, 295 { 296 desc: "ModifyMapEntryMapValue", 297 path: `a.b`, 298 value: `nk1: 299 nk2: nv2`, 300 wantFound: true, 301 want: ` 302 a: 303 nk1: 304 nk2: nv2 305 `, 306 }, 307 { 308 desc: "DeleteMapEntry", 309 path: `a.b`, 310 wantFound: true, 311 want: ` 312 a: {} 313 `, 314 }, 315 { 316 desc: "path not found", 317 path: `a.c.[name:n2].list.[:v3]`, 318 wantFound: false, 319 wantErr: `path not found at element c in path a.c.[name:n2].list.[:v3]`, 320 }, 321 { 322 desc: "error key", 323 path: `a.b.[].list`, 324 wantFound: false, 325 wantErr: `path a.b.[].list: [] is not a valid key:value path element`, 326 }, 327 { 328 desc: "invalid index", 329 path: `a.c.[n2].list.[:v3]`, 330 wantFound: false, 331 wantErr: `path not found at element c in path a.c.[n2].list.[:v3]`, 332 }, 333 } 334 for _, tt := range tests { 335 t.Run(tt.desc, func(t *testing.T) { 336 root := make(map[string]any) 337 if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil { 338 t.Fatal(err) 339 } 340 pc, gotFound, gotErr := GetPathContext(root, util.PathFromString(tt.path), false) 341 if gotErr, wantErr := errToString(gotErr), tt.wantErr; gotErr != wantErr { 342 t.Fatalf("GetPathContext(%s): gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr) 343 } 344 if gotFound != tt.wantFound { 345 t.Fatalf("GetPathContext(%s): gotFound:%v, wantFound:%v", tt.desc, gotFound, tt.wantFound) 346 } 347 if tt.wantErr != "" || !tt.wantFound { 348 if tt.want != "" { 349 t.Error("tt.want is set but never checked") 350 } 351 return 352 } 353 354 err := WritePathContext(pc, tt.value, false) 355 if err != nil { 356 t.Fatal(err) 357 } 358 359 gotYAML := util.ToYAML(root) 360 diff := util.YAMLDiff(gotYAML, tt.want) 361 if diff != "" { 362 t.Errorf("%s: (got:-, want:+):\n%s\n", tt.desc, diff) 363 } 364 }) 365 } 366 } 367 368 func TestWriteNode(t *testing.T) { 369 testTreeYAML := ` 370 a: 371 b: 372 c: val1 373 list1: 374 - i1: val1 375 - i2: val2 376 - i3a: key1 377 i3b: 378 list2: 379 - i1: val1 380 - i2: val2 381 - i3a: key1 382 i3b: 383 i1: va11 384 ` 385 tests := []struct { 386 desc string 387 baseYAML string 388 path string 389 value string 390 want string 391 wantErr string 392 }{ 393 { 394 desc: "insert empty", 395 path: "a.b.c", 396 value: "val1", 397 want: ` 398 a: 399 b: 400 c: val1 401 `, 402 }, 403 { 404 desc: "overwrite", 405 baseYAML: testTreeYAML, 406 path: "a.b.c", 407 value: "val2", 408 want: ` 409 a: 410 b: 411 c: val2 412 list1: 413 - i1: val1 414 - i2: val2 415 - i3a: key1 416 i3b: 417 list2: 418 - i1: val1 419 - i2: val2 420 - i3a: key1 421 i3b: 422 i1: va11 423 `, 424 }, 425 { 426 desc: "partial create", 427 baseYAML: testTreeYAML, 428 path: "a.b.d", 429 value: "val3", 430 want: ` 431 a: 432 b: 433 c: val1 434 d: val3 435 list1: 436 - i1: val1 437 - i2: val2 438 - i3a: key1 439 i3b: 440 list2: 441 - i1: val1 442 - i2: val2 443 - i3a: key1 444 i3b: 445 i1: va11 446 `, 447 }, 448 { 449 desc: "list keys", 450 baseYAML: testTreeYAML, 451 path: "a.b.list1.[i3a:key1].i3b.list2.[i3a:key1].i3b.i1", 452 value: "val2", 453 want: ` 454 a: 455 b: 456 c: val1 457 list1: 458 - i1: val1 459 - i2: val2 460 - i3a: key1 461 i3b: 462 list2: 463 - i1: val1 464 - i2: val2 465 - i3a: key1 466 i3b: 467 i1: val2 468 `, 469 }, 470 // For https://github.com/istio/istio/issues/20950 471 { 472 desc: "with initial list", 473 baseYAML: ` 474 components: 475 ingressGateways: 476 - enabled: true 477 `, 478 path: "components.ingressGateways[0].enabled", 479 value: "false", 480 want: ` 481 components: 482 ingressGateways: 483 - enabled: "false" 484 `, 485 }, 486 { 487 desc: "no initial list", 488 baseYAML: "", 489 path: "components.ingressGateways[0].enabled", 490 value: "false", 491 want: ` 492 components: 493 ingressGateways: 494 - enabled: "false" 495 `, 496 }, 497 { 498 desc: "no initial list for entry", 499 baseYAML: ` 500 a: {} 501 `, 502 path: "a.list.[0]", 503 value: "v1", 504 want: ` 505 a: 506 list: 507 - v1 508 `, 509 }, 510 { 511 desc: "ExtendNthLeafListEntry", 512 baseYAML: ` 513 a: 514 list: 515 - v1 516 `, 517 path: `a.list.[1]`, 518 value: `v2`, 519 want: ` 520 a: 521 list: 522 - v1 523 - v2 524 `, 525 }, 526 { 527 desc: "ExtendLeafListEntryLargeIndex", 528 baseYAML: ` 529 a: 530 list: 531 - v1 532 `, 533 path: `a.list.[999]`, 534 value: `v2`, 535 want: ` 536 a: 537 list: 538 - v1 539 - v2 540 `, 541 }, 542 { 543 desc: "ExtendLeafListEntryNegativeIndex", 544 baseYAML: ` 545 a: 546 list: 547 - v1 548 `, 549 path: `a.list.[-1]`, 550 value: `v2`, 551 want: ` 552 a: 553 list: 554 - v1 555 - v2 556 `, 557 }, 558 { 559 desc: "ExtendNthListEntry", 560 baseYAML: ` 561 a: 562 list: 563 - name: foo 564 `, 565 path: `a.list.[1].name`, 566 value: `bar`, 567 want: ` 568 a: 569 list: 570 - name: foo 571 - name: bar 572 `, 573 }, 574 } 575 for _, tt := range tests { 576 t.Run(tt.desc, func(t *testing.T) { 577 root := make(map[string]any) 578 if tt.baseYAML != "" { 579 if err := yaml.Unmarshal([]byte(tt.baseYAML), &root); err != nil { 580 t.Fatal(err) 581 } 582 } 583 p := util.PathFromString(tt.path) 584 err := WriteNode(root, p, tt.value) 585 if gotErr, wantErr := errToString(err), tt.wantErr; gotErr != wantErr { 586 t.Errorf("%s: gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr) 587 return 588 } 589 if got, want := util.ToYAML(root), tt.want; err == nil && util.YAMLDiff(got, want) != "" { 590 t.Errorf("%s: got:\n%s\nwant:\n%s\ndiff:\n%s\n", tt.desc, got, want, util.YAMLDiff(got, want)) 591 } 592 }) 593 } 594 } 595 596 func TestMergeNode(t *testing.T) { 597 testTreeYAML := ` 598 a: 599 b: 600 c: val1 601 list1: 602 - i1: val1 603 - i2: val2 604 ` 605 tests := []struct { 606 desc string 607 baseYAML string 608 path string 609 value string 610 want string 611 wantErr string 612 }{ 613 { 614 desc: "merge list entry", 615 baseYAML: testTreeYAML, 616 path: "a.b.list1.[i1:val1]", 617 value: ` 618 i2b: val2`, 619 want: ` 620 a: 621 b: 622 c: val1 623 list1: 624 - i1: val1 625 i2b: val2 626 - i2: val2 627 `, 628 }, 629 { 630 desc: "merge list 2", 631 baseYAML: testTreeYAML, 632 path: "a.b.list1", 633 value: ` 634 i3: 635 a: val3 636 `, 637 want: ` 638 a: 639 b: 640 c: val1 641 list1: 642 - i1: val1 643 - i2: val2 644 - i3: 645 a: val3 646 `, 647 }, 648 } 649 for _, tt := range tests { 650 t.Run(tt.desc, func(t *testing.T) { 651 root := make(map[string]any) 652 if tt.baseYAML != "" { 653 if err := yaml.Unmarshal([]byte(tt.baseYAML), &root); err != nil { 654 t.Fatal(err) 655 } 656 } 657 p := util.PathFromString(tt.path) 658 iv := make(map[string]any) 659 err := yaml.Unmarshal([]byte(tt.value), &iv) 660 if err != nil { 661 t.Fatal(err) 662 } 663 err = MergeNode(root, p, iv) 664 if gotErr, wantErr := errToString(err), tt.wantErr; gotErr != wantErr { 665 t.Errorf("%s: gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr) 666 return 667 } 668 if got, want := util.ToYAML(root), tt.want; err == nil && util.YAMLDiff(got, want) != "" { 669 t.Errorf("%s: got:\n%s\nwant:\n%s\ndiff:\n%s\n", tt.desc, got, want, util.YAMLDiff(got, want)) 670 } 671 }) 672 } 673 } 674 675 // errToString returns the string representation of err and the empty string if 676 // err is nil. 677 func errToString(err error) string { 678 if err == nil { 679 return "" 680 } 681 return err.Error() 682 } 683 684 // TestSecretVolumes simulates https://github.com/istio/istio/issues/20381 685 func TestSecretVolumes(t *testing.T) { 686 rootYAML := ` 687 values: 688 gateways: 689 istio-egressgateway: 690 secretVolumes: [] 691 ` 692 root := make(map[string]any) 693 if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil { 694 t.Fatal(err) 695 } 696 overrides := []struct { 697 path string 698 value any 699 }{ 700 { 701 path: "values.gateways.istio-egressgateway.secretVolumes[0].name", 702 value: "egressgateway-certs", 703 }, 704 { 705 path: "values.gateways.istio-egressgateway.secretVolumes[0].secretName", 706 value: "istio-egressgateway-certs", 707 }, 708 { 709 path: "values.gateways.istio-egressgateway.secretVolumes[0].mountPath", 710 value: "/etc/istio/egressgateway-certs", 711 }, 712 { 713 path: "values.gateways.istio-egressgateway.secretVolumes[1].name", 714 value: "egressgateway-ca-certs", 715 }, 716 { 717 path: "values.gateways.istio-egressgateway.secretVolumes[1].secretName", 718 value: "istio-egressgateway-ca-certs", 719 }, 720 { 721 path: "values.gateways.istio-egressgateway.secretVolumes[1].mountPath", 722 value: "/etc/istio/egressgateway-ca-certs", 723 }, 724 { 725 path: "values.gateways.istio-egressgateway.secretVolumes[2].name", 726 value: "nginx-client-certs", 727 }, 728 { 729 path: "values.gateways.istio-egressgateway.secretVolumes[2].secretName", 730 value: "nginx-client-certs", 731 }, 732 { 733 path: "values.gateways.istio-egressgateway.secretVolumes[2].mountPath", 734 value: "/etc/istio/nginx-client-certs", 735 }, 736 { 737 path: "values.gateways.istio-egressgateway.secretVolumes[3].name", 738 value: "nginx-ca-certs", 739 }, 740 { 741 path: "values.gateways.istio-egressgateway.secretVolumes[3].secretName", 742 value: "nginx-ca-certs", 743 }, 744 { 745 path: "values.gateways.istio-egressgateway.secretVolumes[3].mountPath", 746 value: "/etc/istio/nginx-ca-certs", 747 }, 748 } 749 750 for _, override := range overrides { 751 752 pc, _, err := GetPathContext(root, util.PathFromString(override.path), true) 753 if err != nil { 754 t.Fatalf("GetPathContext(%q): %v", override.path, err) 755 } 756 err = WritePathContext(pc, override.value, false) 757 if err != nil { 758 t.Fatalf("WritePathContext(%q): %v", override.path, err) 759 } 760 } 761 762 want := ` 763 values: 764 gateways: 765 istio-egressgateway: 766 secretVolumes: 767 - mountPath: /etc/istio/egressgateway-certs 768 name: egressgateway-certs 769 secretName: istio-egressgateway-certs 770 - mountPath: /etc/istio/egressgateway-ca-certs 771 name: egressgateway-ca-certs 772 secretName: istio-egressgateway-ca-certs 773 - mountPath: /etc/istio/nginx-client-certs 774 name: nginx-client-certs 775 secretName: nginx-client-certs 776 - mountPath: /etc/istio/nginx-ca-certs 777 name: nginx-ca-certs 778 secretName: nginx-ca-certs 779 ` 780 gotYAML := util.ToYAML(root) 781 diff := util.YAMLDiff(gotYAML, want) 782 if diff != "" { 783 t.Errorf("TestSecretVolumes: diff:\n%s\n", diff) 784 } 785 } 786 787 // Simulates https://github.com/istio/istio/issues/19196 788 func TestWriteEscapedPathContext(t *testing.T) { 789 rootYAML := ` 790 values: 791 sidecarInjectorWebhook: 792 injectedAnnotations: {} 793 ` 794 tests := []struct { 795 desc string 796 path string 797 value any 798 want string 799 wantFound bool 800 wantErr string 801 }{ 802 { 803 desc: "ModifyEscapedPathValue", 804 path: `values.sidecarInjectorWebhook.injectedAnnotations.container\.apparmor\.security\.beta\.kubernetes\.io/istio-proxy`, 805 value: `runtime/default`, 806 wantFound: true, 807 want: ` 808 values: 809 sidecarInjectorWebhook: 810 injectedAnnotations: 811 container.apparmor.security.beta.kubernetes.io/istio-proxy: runtime/default 812 `, 813 }, 814 } 815 for _, tt := range tests { 816 t.Run(tt.desc, func(t *testing.T) { 817 root := make(map[string]any) 818 if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil { 819 t.Fatal(err) 820 } 821 pc, gotFound, gotErr := GetPathContext(root, util.PathFromString(tt.path), false) 822 if gotErr, wantErr := errToString(gotErr), tt.wantErr; gotErr != wantErr { 823 t.Fatalf("GetPathContext(%s): gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr) 824 } 825 if gotFound != tt.wantFound { 826 t.Fatalf("GetPathContext(%s): gotFound:%v, wantFound:%v", tt.desc, gotFound, tt.wantFound) 827 } 828 if tt.wantErr != "" || !tt.wantFound { 829 return 830 } 831 832 err := WritePathContext(pc, tt.value, false) 833 if err != nil { 834 t.Fatal(err) 835 } 836 837 gotYAML := util.ToYAML(root) 838 diff := util.YAMLDiff(gotYAML, tt.want) 839 if diff != "" { 840 t.Errorf("%s: diff:\n%s\n", tt.desc, diff) 841 } 842 }) 843 } 844 }