istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/translate/translate_value_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 translate 16 17 import ( 18 "testing" 19 20 "sigs.k8s.io/yaml" 21 22 "istio.io/istio/operator/pkg/apis/istio" 23 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 24 "istio.io/istio/operator/pkg/util" 25 "istio.io/istio/pkg/util/protomarshal" 26 ) 27 28 func TestValueToProto(t *testing.T) { 29 tests := []struct { 30 desc string 31 valueYAML string 32 want string 33 wantErr string 34 }{ 35 { 36 desc: "K8s resources translation", 37 valueYAML: ` 38 pilot: 39 enabled: true 40 rollingMaxSurge: 100% 41 rollingMaxUnavailable: 25% 42 resources: 43 requests: 44 cpu: 1000m 45 memory: 1G 46 replicaCount: 1 47 nodeSelector: 48 kubernetes.io/os: linux 49 tolerations: 50 - key: dedicated 51 operator: Exists 52 effect: NoSchedule 53 - key: CriticalAddonsOnly 54 operator: Exists 55 autoscaleEnabled: true 56 autoscaleMax: 3 57 autoscaleMin: 1 58 cpu: 59 targetAverageUtilization: 80 60 traceSampling: 1.0 61 image: pilot 62 env: 63 GODEBUG: gctrace=1 64 global: 65 hub: docker.io/istio 66 istioNamespace: istio-system 67 tag: 1.2.3 68 proxy: 69 readinessInitialDelaySeconds: 2 70 `, 71 want: ` 72 hub: docker.io/istio 73 tag: 1.2.3 74 components: 75 pilot: 76 enabled: true 77 k8s: 78 replicaCount: 1 79 env: 80 - name: GODEBUG 81 value: gctrace=1 82 nodeSelector: 83 kubernetes.io/os: linux 84 tolerations: 85 - key: dedicated 86 operator: Exists 87 effect: NoSchedule 88 - key: CriticalAddonsOnly 89 operator: Exists 90 resources: 91 requests: 92 cpu: 1000m 93 memory: 1G 94 strategy: 95 rollingUpdate: 96 maxSurge: 100% 97 maxUnavailable: 25% 98 values: 99 global: 100 istioNamespace: istio-system 101 proxy: 102 readinessInitialDelaySeconds: 2 103 pilot: 104 image: pilot 105 autoscaleEnabled: true 106 autoscaleMax: 3 107 autoscaleMin: 1 108 cpu: 109 targetAverageUtilization: 80 110 traceSampling: 1 111 `, 112 }, 113 { 114 desc: "All Enabled", 115 valueYAML: ` 116 global: 117 hub: docker.io/istio 118 istioNamespace: istio-system 119 tag: 1.2.3 120 pilot: 121 enabled: true 122 gateways: 123 enabled: true 124 istio-ingressgateway: 125 rollingMaxSurge: 4 126 rollingMaxUnavailable: 1 127 resources: 128 requests: 129 cpu: 1000m 130 memory: 1G 131 enabled: true 132 `, 133 want: ` 134 hub: docker.io/istio 135 tag: 1.2.3 136 components: 137 pilot: 138 enabled: true 139 ingressGateways: 140 - name: istio-ingressgateway 141 enabled: true 142 k8s: 143 resources: 144 requests: 145 cpu: 1000m 146 memory: 1G 147 strategy: 148 rollingUpdate: 149 maxSurge: 4 150 maxUnavailable: 1 151 values: 152 global: 153 istioNamespace: istio-system 154 `, 155 }, 156 { 157 desc: "Some components Disabled", 158 valueYAML: ` 159 pilot: 160 enabled: true 161 global: 162 hub: docker.io/istio 163 istioNamespace: istio-system 164 tag: 1.2.3 165 `, 166 want: ` 167 hub: docker.io/istio 168 tag: 1.2.3 169 components: 170 pilot: 171 enabled: true 172 values: 173 global: 174 istioNamespace: istio-system 175 `, 176 }, 177 } 178 tr := NewReverseTranslator() 179 180 for _, tt := range tests { 181 t.Run(tt.desc, func(t *testing.T) { 182 valueStruct := v1alpha1.Values{} 183 err := util.UnmarshalWithJSONPB(tt.valueYAML, &valueStruct, false) 184 if err != nil { 185 t.Fatalf("unmarshal(%s): got error %s", tt.desc, err) 186 } 187 gotSpec, err := tr.TranslateFromValueToSpec([]byte(tt.valueYAML), false) 188 if gotErr, wantErr := errToString(err), tt.wantErr; gotErr != wantErr { 189 t.Errorf("ValuesToProto(%s)(%v): gotErr:%s, wantErr:%s", tt.desc, tt.valueYAML, gotErr, wantErr) 190 } 191 if tt.wantErr == "" { 192 byteArray, err := protomarshal.Marshal(gotSpec) 193 if err != nil { 194 t.Errorf("failed to marshal translated IstioOperatorSpec: %s", err) 195 } 196 cpYaml, _ := yaml.JSONToYAML(byteArray) 197 if want := tt.want; !util.IsYAMLEqual(string(byteArray), want) { 198 t.Errorf("ValuesToProto(%s): got:\n%s\n\nwant:\n%s\nDiff:\n%s\n", tt.desc, string(cpYaml), want, util.YAMLDiff(string(byteArray), want)) 199 } 200 201 } 202 }) 203 } 204 } 205 206 func TestValueToK8s(t *testing.T) { 207 tests := []struct { 208 desc string 209 inIOPSpec string 210 want string 211 wantErr string 212 }{ 213 { 214 desc: "pilot env k8s setting with values", 215 inIOPSpec: ` 216 spec: 217 components: 218 pilot: 219 k8s: 220 nodeSelector: 221 master: "true" 222 env: 223 - name: EXTERNAL_CA 224 value: ISTIOD_RA_KUBERNETES_API 225 - name: K8S_SIGNER 226 value: kubernetes.io/legacy-unknown 227 values: 228 pilot: 229 enabled: true 230 rollingMaxSurge: 100% 231 rollingMaxUnavailable: 25% 232 resources: 233 requests: 234 cpu: 1000m 235 memory: 1G 236 replicaCount: 1 237 nodeSelector: 238 kubernetes.io/os: linux 239 tolerations: 240 - key: dedicated 241 operator: Exists 242 effect: NoSchedule 243 - key: CriticalAddonsOnly 244 operator: Exists 245 autoscaleEnabled: true 246 autoscaleMax: 3 247 autoscaleMin: 1 248 cpu: 249 targetAverageUtilization: 80 250 memory: 251 targetAverageUtilization: 80 252 traceSampling: 1.0 253 image: pilot 254 env: 255 GODEBUG: gctrace=1 256 global: 257 hub: docker.io/istio 258 tag: 1.2.3 259 istioNamespace: istio-system 260 proxy: 261 readinessInitialDelaySeconds: 2 262 `, 263 want: ` 264 spec: 265 components: 266 pilot: 267 k8s: 268 env: 269 - name: EXTERNAL_CA 270 value: ISTIOD_RA_KUBERNETES_API 271 - name: K8S_SIGNER 272 value: kubernetes.io/legacy-unknown 273 - name: GODEBUG 274 value: gctrace=1 275 nodeSelector: 276 master: "true" 277 kubernetes.io/os: linux 278 replicaCount: 1 279 resources: 280 requests: 281 cpu: 1000m 282 memory: 1G 283 strategy: 284 rollingUpdate: 285 maxSurge: 100% 286 maxUnavailable: 25% 287 tolerations: 288 - effect: NoSchedule 289 key: dedicated 290 operator: Exists 291 - key: CriticalAddonsOnly 292 operator: Exists 293 values: 294 global: 295 hub: docker.io/istio 296 tag: 1.2.3 297 istioNamespace: istio-system 298 proxy: 299 readinessInitialDelaySeconds: 2 300 pilot: 301 enabled: true 302 rollingMaxSurge: 100% 303 rollingMaxUnavailable: 25% 304 resources: 305 requests: 306 cpu: 1000m 307 memory: 1G 308 replicaCount: 1 309 nodeSelector: 310 kubernetes.io/os: linux 311 tolerations: 312 - key: dedicated 313 operator: Exists 314 effect: NoSchedule 315 - key: CriticalAddonsOnly 316 operator: Exists 317 autoscaleEnabled: true 318 autoscaleMax: 3 319 autoscaleMin: 1 320 cpu: 321 targetAverageUtilization: 80 322 memory: 323 targetAverageUtilization: 80 324 traceSampling: 1.0 325 image: pilot 326 env: 327 GODEBUG: gctrace=1 328 `, 329 }, 330 { 331 desc: "pilot no env k8s setting with values", 332 inIOPSpec: ` 333 spec: 334 components: 335 pilot: 336 k8s: 337 nodeSelector: 338 master: "true" 339 values: 340 pilot: 341 enabled: true 342 rollingMaxSurge: 100% 343 rollingMaxUnavailable: 25% 344 resources: 345 requests: 346 cpu: 1000m 347 memory: 1G 348 replicaCount: 1 349 nodeSelector: 350 kubernetes.io/os: linux 351 tolerations: 352 - key: dedicated 353 operator: Exists 354 effect: NoSchedule 355 - key: CriticalAddonsOnly 356 operator: Exists 357 autoscaleEnabled: true 358 autoscaleMax: 3 359 autoscaleMin: 1 360 cpu: 361 targetAverageUtilization: 80 362 memory: 363 targetAverageUtilization: 80 364 traceSampling: 1.0 365 image: pilot 366 env: 367 GODEBUG: gctrace=1 368 global: 369 hub: docker.io/istio 370 tag: 1.2.3 371 istioNamespace: istio-system 372 proxy: 373 readinessInitialDelaySeconds: 2 374 `, 375 want: ` 376 spec: 377 components: 378 pilot: 379 k8s: 380 env: 381 - name: GODEBUG 382 value: gctrace=1 383 nodeSelector: 384 master: "true" 385 kubernetes.io/os: linux 386 replicaCount: 1 387 resources: 388 requests: 389 cpu: 1000m 390 memory: 1G 391 strategy: 392 rollingUpdate: 393 maxSurge: 100% 394 maxUnavailable: 25% 395 tolerations: 396 - effect: NoSchedule 397 key: dedicated 398 operator: Exists 399 - key: CriticalAddonsOnly 400 operator: Exists 401 values: 402 global: 403 hub: docker.io/istio 404 tag: 1.2.3 405 istioNamespace: istio-system 406 proxy: 407 readinessInitialDelaySeconds: 2 408 pilot: 409 enabled: true 410 rollingMaxSurge: 100% 411 rollingMaxUnavailable: 25% 412 resources: 413 requests: 414 cpu: 1000m 415 memory: 1G 416 replicaCount: 1 417 nodeSelector: 418 kubernetes.io/os: linux 419 tolerations: 420 - key: dedicated 421 operator: Exists 422 effect: NoSchedule 423 - key: CriticalAddonsOnly 424 operator: Exists 425 autoscaleEnabled: true 426 autoscaleMax: 3 427 autoscaleMin: 1 428 cpu: 429 targetAverageUtilization: 80 430 memory: 431 targetAverageUtilization: 80 432 traceSampling: 1.0 433 image: pilot 434 env: 435 GODEBUG: gctrace=1 436 `, 437 }, 438 { 439 desc: "pilot k8s setting with empty env in values", 440 inIOPSpec: ` 441 spec: 442 components: 443 pilot: 444 k8s: 445 nodeSelector: 446 master: "true" 447 env: 448 - name: SPIFFE_BUNDLE_ENDPOINTS 449 value: "SPIFFE_BUNDLE_ENDPOINT" 450 values: 451 pilot: 452 enabled: true 453 rollingMaxSurge: 100% 454 rollingMaxUnavailable: 25% 455 resources: 456 requests: 457 cpu: 1000m 458 memory: 1G 459 replicaCount: 1 460 nodeSelector: 461 kubernetes.io/os: linux 462 tolerations: 463 - key: dedicated 464 operator: Exists 465 effect: NoSchedule 466 - key: CriticalAddonsOnly 467 operator: Exists 468 autoscaleEnabled: true 469 autoscaleMax: 3 470 autoscaleMin: 1 471 cpu: 472 targetAverageUtilization: 80 473 memory: 474 targetAverageUtilization: 80 475 traceSampling: 1.0 476 image: pilot 477 env: {} 478 global: 479 hub: docker.io/istio 480 tag: 1.2.3 481 istioNamespace: istio-system 482 proxy: 483 readinessInitialDelaySeconds: 2 484 `, 485 want: ` 486 spec: 487 components: 488 pilot: 489 k8s: 490 env: 491 - name: SPIFFE_BUNDLE_ENDPOINTS 492 value: "SPIFFE_BUNDLE_ENDPOINT" 493 nodeSelector: 494 master: "true" 495 kubernetes.io/os: linux 496 replicaCount: 1 497 resources: 498 requests: 499 cpu: 1000m 500 memory: 1G 501 strategy: 502 rollingUpdate: 503 maxSurge: 100% 504 maxUnavailable: 25% 505 tolerations: 506 - effect: NoSchedule 507 key: dedicated 508 operator: Exists 509 - key: CriticalAddonsOnly 510 operator: Exists 511 values: 512 global: 513 hub: docker.io/istio 514 tag: 1.2.3 515 istioNamespace: istio-system 516 proxy: 517 readinessInitialDelaySeconds: 2 518 pilot: 519 enabled: true 520 rollingMaxSurge: 100% 521 rollingMaxUnavailable: 25% 522 resources: 523 requests: 524 cpu: 1000m 525 memory: 1G 526 replicaCount: 1 527 nodeSelector: 528 kubernetes.io/os: linux 529 tolerations: 530 - key: dedicated 531 operator: Exists 532 effect: NoSchedule 533 - key: CriticalAddonsOnly 534 operator: Exists 535 autoscaleEnabled: true 536 autoscaleMax: 3 537 autoscaleMin: 1 538 cpu: 539 targetAverageUtilization: 80 540 memory: 541 targetAverageUtilization: 80 542 traceSampling: 1.0 543 image: pilot 544 env: {} 545 `, 546 }, 547 { 548 desc: "pilot no env k8s setting with multiple env variables in values", 549 inIOPSpec: ` 550 spec: 551 components: 552 pilot: 553 k8s: 554 nodeSelector: 555 master: "true" 556 values: 557 pilot: 558 enabled: true 559 rollingMaxSurge: 100% 560 rollingMaxUnavailable: 25% 561 resources: 562 requests: 563 cpu: 1000m 564 memory: 1G 565 replicaCount: 1 566 nodeSelector: 567 kubernetes.io/os: linux 568 tolerations: 569 - key: dedicated 570 operator: Exists 571 effect: NoSchedule 572 - key: CriticalAddonsOnly 573 operator: Exists 574 autoscaleEnabled: true 575 autoscaleMax: 3 576 autoscaleMin: 1 577 cpu: 578 targetAverageUtilization: 80 579 memory: 580 targetAverageUtilization: 80 581 traceSampling: 1.0 582 image: pilot 583 env: 584 PILOT_ENABLE_ISTIO_TAGS: false 585 PILOT_ENABLE_LEGACY_ISTIO_MUTUAL_CREDENTIAL_NAME: false 586 PROXY_XDS_DEBUG_VIA_AGENT: false 587 global: 588 hub: docker.io/istio 589 tag: 1.2.3 590 istioNamespace: istio-system 591 proxy: 592 readinessInitialDelaySeconds: 2 593 `, 594 want: ` 595 spec: 596 components: 597 pilot: 598 k8s: 599 env: 600 - name: PILOT_ENABLE_ISTIO_TAGS 601 value: "false" 602 - name: PILOT_ENABLE_LEGACY_ISTIO_MUTUAL_CREDENTIAL_NAME 603 value: "false" 604 - name: PROXY_XDS_DEBUG_VIA_AGENT 605 value: "false" 606 nodeSelector: 607 master: "true" 608 kubernetes.io/os: linux 609 replicaCount: 1 610 resources: 611 requests: 612 cpu: 1000m 613 memory: 1G 614 strategy: 615 rollingUpdate: 616 maxSurge: 100% 617 maxUnavailable: 25% 618 tolerations: 619 - effect: NoSchedule 620 key: dedicated 621 operator: Exists 622 - key: CriticalAddonsOnly 623 operator: Exists 624 values: 625 global: 626 hub: docker.io/istio 627 tag: 1.2.3 628 istioNamespace: istio-system 629 proxy: 630 readinessInitialDelaySeconds: 2 631 pilot: 632 enabled: true 633 rollingMaxSurge: 100% 634 rollingMaxUnavailable: 25% 635 resources: 636 requests: 637 cpu: 1000m 638 memory: 1G 639 replicaCount: 1 640 nodeSelector: 641 kubernetes.io/os: linux 642 tolerations: 643 - key: dedicated 644 operator: Exists 645 effect: NoSchedule 646 - key: CriticalAddonsOnly 647 operator: Exists 648 autoscaleEnabled: true 649 autoscaleMax: 3 650 autoscaleMin: 1 651 cpu: 652 targetAverageUtilization: 80 653 memory: 654 targetAverageUtilization: 80 655 traceSampling: 1.0 656 image: pilot 657 env: 658 PILOT_ENABLE_ISTIO_TAGS: false 659 PILOT_ENABLE_LEGACY_ISTIO_MUTUAL_CREDENTIAL_NAME: false 660 PROXY_XDS_DEBUG_VIA_AGENT: false 661 `, 662 }, 663 { 664 desc: "pilot k8s setting with multiple env variables in values", 665 inIOPSpec: ` 666 spec: 667 components: 668 pilot: 669 k8s: 670 nodeSelector: 671 master: "true" 672 env: 673 - name: SPIFFE_BUNDLE_ENDPOINTS 674 value: "SPIFFE_BUNDLE_ENDPOINT" 675 values: 676 pilot: 677 enabled: true 678 rollingMaxSurge: 100% 679 rollingMaxUnavailable: 25% 680 resources: 681 requests: 682 cpu: 1000m 683 memory: 1G 684 replicaCount: 1 685 nodeSelector: 686 kubernetes.io/os: linux 687 tolerations: 688 - key: dedicated 689 operator: Exists 690 effect: NoSchedule 691 - key: CriticalAddonsOnly 692 operator: Exists 693 autoscaleEnabled: true 694 autoscaleMax: 3 695 autoscaleMin: 1 696 cpu: 697 targetAverageUtilization: 80 698 memory: 699 targetAverageUtilization: 80 700 traceSampling: 1.0 701 image: pilot 702 env: 703 PILOT_ENABLE_ISTIO_TAGS: false 704 PILOT_ENABLE_LEGACY_ISTIO_MUTUAL_CREDENTIAL_NAME: false 705 PROXY_XDS_DEBUG_VIA_AGENT: false 706 global: 707 hub: docker.io/istio 708 tag: 1.2.3 709 istioNamespace: istio-system 710 proxy: 711 readinessInitialDelaySeconds: 2 712 `, 713 want: ` 714 spec: 715 components: 716 pilot: 717 k8s: 718 env: 719 - name: SPIFFE_BUNDLE_ENDPOINTS 720 value: "SPIFFE_BUNDLE_ENDPOINT" 721 - name: PILOT_ENABLE_ISTIO_TAGS 722 value: "false" 723 - name: PILOT_ENABLE_LEGACY_ISTIO_MUTUAL_CREDENTIAL_NAME 724 value: "false" 725 - name: PROXY_XDS_DEBUG_VIA_AGENT 726 value: "false" 727 nodeSelector: 728 master: "true" 729 kubernetes.io/os: linux 730 replicaCount: 1 731 resources: 732 requests: 733 cpu: 1000m 734 memory: 1G 735 strategy: 736 rollingUpdate: 737 maxSurge: 100% 738 maxUnavailable: 25% 739 tolerations: 740 - effect: NoSchedule 741 key: dedicated 742 operator: Exists 743 - key: CriticalAddonsOnly 744 operator: Exists 745 values: 746 global: 747 hub: docker.io/istio 748 tag: 1.2.3 749 istioNamespace: istio-system 750 proxy: 751 readinessInitialDelaySeconds: 2 752 pilot: 753 enabled: true 754 rollingMaxSurge: 100% 755 rollingMaxUnavailable: 25% 756 resources: 757 requests: 758 cpu: 1000m 759 memory: 1G 760 replicaCount: 1 761 nodeSelector: 762 kubernetes.io/os: linux 763 tolerations: 764 - key: dedicated 765 operator: Exists 766 effect: NoSchedule 767 - key: CriticalAddonsOnly 768 operator: Exists 769 autoscaleEnabled: true 770 autoscaleMax: 3 771 autoscaleMin: 1 772 cpu: 773 targetAverageUtilization: 80 774 memory: 775 targetAverageUtilization: 80 776 traceSampling: 1.0 777 image: pilot 778 env: 779 PILOT_ENABLE_ISTIO_TAGS: false 780 PILOT_ENABLE_LEGACY_ISTIO_MUTUAL_CREDENTIAL_NAME: false 781 PROXY_XDS_DEBUG_VIA_AGENT: false 782 `, 783 }, 784 { 785 desc: "ingressgateway k8s setting with values", 786 inIOPSpec: ` 787 spec: 788 components: 789 pilot: 790 enabled: false 791 ingressGateways: 792 - namespace: istio-system 793 name: istio-ingressgateway 794 enabled: true 795 k8s: 796 service: 797 externalTrafficPolicy: Local 798 serviceAnnotations: 799 manifest-generate: "testserviceAnnotation" 800 securityContext: 801 sysctls: 802 - name: "net.ipv4.ip_local_port_range" 803 value: "80 65535" 804 values: 805 gateways: 806 istio-ingressgateway: 807 serviceAnnotations: {} 808 nodeSelector: {} 809 tolerations: [] 810 global: 811 hub: docker.io/istio 812 tag: 1.2.3 813 istioNamespace: istio-system 814 `, 815 want: ` 816 spec: 817 components: 818 ingressGateways: 819 - name: istio-ingressgateway 820 namespace: istio-system 821 enabled: true 822 k8s: 823 securityContext: 824 sysctls: 825 - name: net.ipv4.ip_local_port_range 826 value: "80 65535" 827 service: 828 externalTrafficPolicy: Local 829 serviceAnnotations: 830 manifest-generate: testserviceAnnotation 831 pilot: 832 enabled: false 833 values: 834 gateways: 835 istio-ingressgateway: 836 serviceAnnotations: {} 837 nodeSelector: {} 838 tolerations: [] 839 global: 840 hub: docker.io/istio 841 tag: 1.2.3 842 istioNamespace: istio-system 843 `, 844 }, 845 { 846 desc: "pilot env k8s setting with non-empty hpa values", 847 inIOPSpec: ` 848 spec: 849 revision: canary 850 components: 851 pilot: 852 enabled: true 853 values: 854 pilot: 855 autoscaleMin: 1 856 autoscaleMax: 3 857 cpu: 858 targetAverageUtilization: 80 859 memory: 860 targetAverageUtilization: 80 861 `, 862 want: ` 863 spec: 864 revision: canary 865 components: 866 pilot: 867 enabled: true 868 values: 869 pilot: 870 autoscaleMin: 1 871 autoscaleMax: 3 872 cpu: 873 targetAverageUtilization: 80 874 memory: 875 targetAverageUtilization: 80 876 `, 877 }, 878 } 879 tr := NewReverseTranslator() 880 881 for _, tt := range tests { 882 t.Run(tt.desc, func(t *testing.T) { 883 _, err := istio.UnmarshalIstioOperator(tt.inIOPSpec, false) 884 if err != nil { 885 t.Fatalf("unmarshal(%s): got error %s", tt.desc, err) 886 } 887 gotSpec, err := tr.TranslateK8SfromValueToIOP(tt.inIOPSpec) 888 if gotErr, wantErr := errToString(err), tt.wantErr; gotErr != wantErr { 889 t.Errorf("ValuesToK8s(%s)(%v): gotErr:%s, wantErr:%s", tt.desc, tt.inIOPSpec, gotErr, wantErr) 890 } 891 if tt.wantErr == "" { 892 if want := tt.want; !util.IsYAMLEqual(gotSpec, want) { 893 t.Errorf("ValuesToK8s(%s): got:\n%s\n\nwant:\n%s\nDiff:\n%s\n", tt.desc, gotSpec, want, util.YAMLDiff(gotSpec, want)) 894 } 895 } 896 }) 897 } 898 } 899 900 // errToString returns the string representation of err and the empty string if 901 // err is nil. 902 func errToString(err error) string { 903 if err == nil { 904 return "" 905 } 906 return err.Error() 907 }