sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/topology/cluster/patches/inline/json_patch_generator_test.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package inline 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "testing" 24 25 . "github.com/onsi/gomega" 26 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/utils/pointer" 31 32 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 33 runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" 34 patchvariables "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables" 35 ) 36 37 func TestGenerate(t *testing.T) { 38 tests := []struct { 39 name string 40 patch *clusterv1.ClusterClassPatch 41 req *runtimehooksv1.GeneratePatchesRequest 42 want *runtimehooksv1.GeneratePatchesResponse 43 }{ 44 { 45 name: "Should generate JSON Results with correct variable values", 46 patch: &clusterv1.ClusterClassPatch{ 47 Name: "clusterName", 48 Definitions: []clusterv1.PatchDefinition{ 49 { 50 Selector: clusterv1.PatchSelector{ 51 APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", 52 Kind: "ControlPlaneTemplate", 53 MatchResources: clusterv1.PatchSelectorMatch{ 54 ControlPlane: true, 55 }, 56 }, 57 JSONPatches: []clusterv1.JSONPatch{ 58 // .value 59 { 60 Op: "replace", 61 Path: "/spec/value", 62 Value: &apiextensionsv1.JSON{Raw: []byte("1")}, 63 }, 64 // .valueFrom.variable 65 { 66 Op: "replace", 67 Path: "/spec/valueFrom/variable", 68 ValueFrom: &clusterv1.JSONPatchValue{ 69 Variable: pointer.String("variableA"), 70 }, 71 }, 72 // .valueFrom.template using sprig functions 73 { 74 Op: "replace", 75 Path: "/spec/valueFrom/template", 76 ValueFrom: &clusterv1.JSONPatchValue{ 77 Template: pointer.String(`template {{ .variableB | lower | repeat 5 }}`), 78 }, 79 }, 80 // template-specific variable takes precedent, if the same variable exists 81 // in the global and template-specific variables. 82 { 83 Op: "replace", 84 Path: "/spec/templatePrecedent", 85 ValueFrom: &clusterv1.JSONPatchValue{ 86 Variable: pointer.String("variableC"), 87 }, 88 }, 89 // global builtin variable should work. 90 // (verify that merging builtin variables works) 91 { 92 Op: "replace", 93 Path: "/spec/builtinClusterName", 94 ValueFrom: &clusterv1.JSONPatchValue{ 95 Variable: pointer.String("builtin.cluster.name"), 96 }, 97 }, 98 // template-specific builtin variable should work. 99 // (verify that merging builtin variables works) 100 { 101 Op: "replace", 102 Path: "/spec/builtinControlPlaneReplicas", 103 ValueFrom: &clusterv1.JSONPatchValue{ 104 Variable: pointer.String("builtin.controlPlane.replicas"), 105 }, 106 }, 107 // test .builtin.controlPlane.machineTemplate.InfrastructureRef.name var. 108 { 109 Op: "replace", 110 Path: "/spec/template/spec/files", 111 ValueFrom: &clusterv1.JSONPatchValue{ 112 Template: pointer.String(`[{"contentFrom":{"secret":{"key":"control-plane-azure.json","name":"{{ .builtin.controlPlane.machineTemplate.infrastructureRef.name }}-azure-json"}}}]`), 113 }, 114 }, 115 }, 116 }, 117 }, 118 }, 119 req: &runtimehooksv1.GeneratePatchesRequest{ 120 Variables: []runtimehooksv1.Variable{ 121 { 122 Name: "builtin", 123 Value: apiextensionsv1.JSON{Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 124 }, 125 { 126 Name: "variableA", 127 Value: apiextensionsv1.JSON{Raw: []byte(`"A"`)}, 128 }, 129 { 130 Name: "variableB", 131 Value: apiextensionsv1.JSON{Raw: []byte(`"B"`)}, 132 }, 133 { 134 Name: "variableC", 135 Value: apiextensionsv1.JSON{Raw: []byte(`"C"`)}, 136 }, 137 }, 138 Items: []runtimehooksv1.GeneratePatchesRequestItem{ 139 { 140 UID: "1", 141 HolderReference: runtimehooksv1.HolderReference{ 142 APIVersion: clusterv1.GroupVersion.String(), 143 Kind: "Cluster", 144 Name: "my-cluster", 145 Namespace: "default", 146 FieldPath: "spec.controlPlaneRef", 147 }, 148 Variables: []runtimehooksv1.Variable{ 149 { 150 Name: "builtin", 151 Value: apiextensionsv1.JSON{Raw: []byte(`{"controlPlane":{"replicas":3,"machineTemplate":{"infrastructureRef":{"name":"controlPlaneInfrastructureMachineTemplate1"}}}}`)}, 152 }, 153 { 154 Name: "variableC", 155 Value: apiextensionsv1.JSON{Raw: []byte(`"C-template"`)}, 156 }, 157 }, 158 Object: runtime.RawExtension{ 159 Object: &unstructured.Unstructured{ 160 Object: map[string]interface{}{ 161 "apiVersion": "controlplane.cluster.x-k8s.io/v1beta1", 162 "kind": "ControlPlaneTemplate", 163 }, 164 }, 165 }, 166 }, 167 }, 168 }, 169 want: &runtimehooksv1.GeneratePatchesResponse{ 170 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 171 { 172 UID: "1", 173 Patch: toJSONCompact(`[ 174 {"op":"replace","path":"/spec/value","value":1}, 175 {"op":"replace","path":"/spec/valueFrom/variable","value":"A"}, 176 {"op":"replace","path":"/spec/valueFrom/template","value":"template bbbbb"}, 177 {"op":"replace","path":"/spec/templatePrecedent","value":"C-template"}, 178 {"op":"replace","path":"/spec/builtinClusterName","value":"cluster-name"}, 179 {"op":"replace","path":"/spec/builtinControlPlaneReplicas","value":3}, 180 {"op":"replace","path":"/spec/template/spec/files","value":[{ 181 "contentFrom":{ 182 "secret":{ 183 "key":"control-plane-azure.json", 184 "name":"controlPlaneInfrastructureMachineTemplate1-azure-json" 185 } 186 } 187 }]}]`), 188 PatchType: runtimehooksv1.JSONPatchType, 189 }, 190 }, 191 }, 192 }, 193 { 194 name: "Should generate JSON Results (multiple PatchDefinitions)", 195 patch: &clusterv1.ClusterClassPatch{ 196 Name: "clusterName", 197 Definitions: []clusterv1.PatchDefinition{ 198 { 199 Selector: clusterv1.PatchSelector{ 200 APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", 201 Kind: "ControlPlaneTemplate", 202 MatchResources: clusterv1.PatchSelectorMatch{ 203 ControlPlane: true, 204 }, 205 }, 206 JSONPatches: []clusterv1.JSONPatch{ 207 { 208 Op: "replace", 209 Path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/controllerManager/extraArgs/cluster-name", 210 ValueFrom: &clusterv1.JSONPatchValue{ 211 Variable: pointer.String("builtin.cluster.name"), 212 }, 213 }, 214 { 215 Op: "replace", 216 Path: "/spec/template/spec/kubeadmConfigSpec/files", 217 ValueFrom: &clusterv1.JSONPatchValue{ 218 Template: pointer.String(` 219 - contentFrom: 220 secret: 221 key: control-plane-azure.json 222 name: "{{ .builtin.cluster.name }}-control-plane-azure-json" 223 owner: root:root 224 `), 225 }, 226 }, 227 { 228 Op: "remove", 229 Path: "/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs", 230 }, 231 }, 232 }, 233 { 234 Selector: clusterv1.PatchSelector{ 235 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 236 Kind: "BootstrapTemplate", 237 MatchResources: clusterv1.PatchSelectorMatch{ 238 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 239 Names: []string{"default-worker"}, 240 }, 241 }, 242 }, 243 JSONPatches: []clusterv1.JSONPatch{ 244 { 245 Op: "replace", 246 Path: "/spec/template/spec/joinConfiguration/nodeRegistration/kubeletExtraArgs/cluster-name", 247 ValueFrom: &clusterv1.JSONPatchValue{ 248 Variable: pointer.String("builtin.cluster.name"), 249 }, 250 }, 251 { 252 Op: "replace", 253 Path: "/spec/template/spec/files", 254 ValueFrom: &clusterv1.JSONPatchValue{ 255 Template: pointer.String(` 256 [{ 257 "contentFrom":{ 258 "secret":{ 259 "key":"worker-node-azure.json", 260 "name":"{{ .builtin.cluster.name }}-md-0-azure-json" 261 } 262 }, 263 "owner":"root:root" 264 }]`), 265 }, 266 }, 267 }, 268 }, 269 { 270 Selector: clusterv1.PatchSelector{ 271 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 272 Kind: "BootstrapTemplate", 273 MatchResources: clusterv1.PatchSelectorMatch{ 274 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 275 Names: []string{"default-mp-worker"}, 276 }, 277 }, 278 }, 279 JSONPatches: []clusterv1.JSONPatch{ 280 { 281 Op: "replace", 282 Path: "/spec/template/spec/joinConfiguration/nodeRegistration/kubeletExtraArgs/cluster-name", 283 ValueFrom: &clusterv1.JSONPatchValue{ 284 Variable: pointer.String("builtin.cluster.name"), 285 }, 286 }, 287 { 288 Op: "replace", 289 Path: "/spec/template/spec/files", 290 ValueFrom: &clusterv1.JSONPatchValue{ 291 Template: pointer.String(` 292 [{ 293 "contentFrom":{ 294 "secret":{ 295 "key":"worker-node-azure.json", 296 "name":"{{ .builtin.cluster.name }}-mp-0-azure-json" 297 } 298 }, 299 "owner":"root:root" 300 }]`), 301 }, 302 }, 303 }, 304 }, 305 }, 306 }, 307 req: &runtimehooksv1.GeneratePatchesRequest{ 308 Variables: []runtimehooksv1.Variable{ 309 { 310 Name: "builtin", 311 Value: apiextensionsv1.JSON{Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 312 }, 313 }, 314 Items: []runtimehooksv1.GeneratePatchesRequestItem{ 315 { 316 UID: "1", 317 HolderReference: runtimehooksv1.HolderReference{ 318 APIVersion: clusterv1.GroupVersion.String(), 319 Kind: "Cluster", 320 Name: "my-cluster", 321 Namespace: "default", 322 FieldPath: "spec.controlPlaneRef", 323 }, 324 Object: runtime.RawExtension{ 325 Object: &unstructured.Unstructured{ 326 Object: map[string]interface{}{ 327 "apiVersion": "controlplane.cluster.x-k8s.io/v1beta1", 328 "kind": "ControlPlaneTemplate", 329 }, 330 }, 331 }, 332 }, 333 { 334 UID: "2", 335 HolderReference: runtimehooksv1.HolderReference{ 336 APIVersion: clusterv1.GroupVersion.String(), 337 Kind: "MachineDeployment", 338 Name: "my-md-0", 339 Namespace: "default", 340 FieldPath: "spec.template.spec.bootstrap.configRef", 341 }, 342 Variables: []runtimehooksv1.Variable{ 343 { 344 Name: "builtin", 345 Value: apiextensionsv1.JSON{Raw: []byte(`{"machineDeployment":{"class":"default-worker"}}`)}, 346 }, 347 }, 348 Object: runtime.RawExtension{ 349 Object: &unstructured.Unstructured{ 350 Object: map[string]interface{}{ 351 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 352 "kind": "BootstrapTemplate", 353 }, 354 }, 355 }, 356 }, 357 { 358 UID: "3", 359 HolderReference: runtimehooksv1.HolderReference{ 360 APIVersion: clusterv1.GroupVersion.String(), 361 Kind: "MachinePool", 362 Name: "my-mp-0", 363 Namespace: "default", 364 FieldPath: "spec.template.spec.bootstrap.configRef", 365 }, 366 Variables: []runtimehooksv1.Variable{ 367 { 368 Name: "builtin", 369 Value: apiextensionsv1.JSON{Raw: []byte(`{"machinePool":{"class":"default-mp-worker"}}`)}, 370 }, 371 }, 372 Object: runtime.RawExtension{ 373 Object: &unstructured.Unstructured{ 374 Object: map[string]interface{}{ 375 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 376 "kind": "BootstrapTemplate", 377 }, 378 }, 379 }, 380 }, 381 }, 382 }, 383 want: &runtimehooksv1.GeneratePatchesResponse{ 384 Items: []runtimehooksv1.GeneratePatchesResponseItem{ 385 { 386 UID: "1", 387 Patch: toJSONCompact(`[ 388 {"op":"replace","path":"/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/controllerManager/extraArgs/cluster-name","value":"cluster-name"}, 389 {"op":"replace","path":"/spec/template/spec/kubeadmConfigSpec/files","value":[{"contentFrom":{"secret":{"key":"control-plane-azure.json","name":"cluster-name-control-plane-azure-json"}},"owner":"root:root"}]}, 390 {"op":"remove","path":"/spec/template/spec/kubeadmConfigSpec/clusterConfiguration/apiServer/extraArgs"} 391 ]`), 392 PatchType: runtimehooksv1.JSONPatchType, 393 }, 394 { 395 UID: "2", 396 Patch: toJSONCompact(`[ 397 {"op":"replace","path":"/spec/template/spec/joinConfiguration/nodeRegistration/kubeletExtraArgs/cluster-name","value":"cluster-name"}, 398 {"op":"replace","path":"/spec/template/spec/files","value":[{"contentFrom":{"secret":{"key":"worker-node-azure.json","name":"cluster-name-md-0-azure-json"}},"owner":"root:root"}]} 399 ]`), 400 PatchType: runtimehooksv1.JSONPatchType, 401 }, 402 { 403 UID: "3", 404 Patch: toJSONCompact(`[ 405 {"op":"replace","path":"/spec/template/spec/joinConfiguration/nodeRegistration/kubeletExtraArgs/cluster-name","value":"cluster-name"}, 406 {"op":"replace","path":"/spec/template/spec/files","value":[{"contentFrom":{"secret":{"key":"worker-node-azure.json","name":"cluster-name-mp-0-azure-json"}},"owner":"root:root"}]} 407 ]`), 408 PatchType: runtimehooksv1.JSONPatchType, 409 }, 410 }, 411 }, 412 }, 413 } 414 415 for _, tt := range tests { 416 t.Run(tt.name, func(t *testing.T) { 417 g := NewWithT(t) 418 419 got, err := NewGenerator(tt.patch).Generate(context.Background(), &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "default"}}, tt.req) 420 421 g.Expect(got).To(BeComparableTo(tt.want)) 422 g.Expect(err).ToNot(HaveOccurred()) 423 }) 424 } 425 } 426 427 func TestMatchesSelector(t *testing.T) { 428 tests := []struct { 429 name string 430 req *runtimehooksv1.GeneratePatchesRequestItem 431 templateVariables map[string]apiextensionsv1.JSON 432 selector clusterv1.PatchSelector 433 match bool 434 }{ 435 { 436 name: "Don't match: apiVersion mismatch", 437 req: &runtimehooksv1.GeneratePatchesRequestItem{ 438 Object: runtime.RawExtension{ 439 Object: &unstructured.Unstructured{ 440 Object: map[string]interface{}{ 441 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 442 "kind": "AzureMachineTemplate", 443 }, 444 }, 445 }, 446 }, 447 selector: clusterv1.PatchSelector{ 448 APIVersion: "infrastructure.cluster.x-k8s.io/v1alpha4", 449 Kind: "AzureMachineTemplate", 450 }, 451 match: false, 452 }, 453 { 454 name: "Don't match: kind mismatch", 455 req: &runtimehooksv1.GeneratePatchesRequestItem{ 456 Object: runtime.RawExtension{ 457 Object: &unstructured.Unstructured{ 458 Object: map[string]interface{}{ 459 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 460 "kind": "AzureMachineTemplate", 461 }, 462 }, 463 }, 464 }, 465 selector: clusterv1.PatchSelector{ 466 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 467 Kind: "AzureClusterTemplate", 468 }, 469 match: false, 470 }, 471 { 472 name: "Match InfrastructureClusterTemplate", 473 req: &runtimehooksv1.GeneratePatchesRequestItem{ 474 Object: runtime.RawExtension{ 475 Object: &unstructured.Unstructured{ 476 Object: map[string]interface{}{ 477 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 478 "kind": "AzureClusterTemplate", 479 }, 480 }, 481 }, 482 HolderReference: runtimehooksv1.HolderReference{ 483 APIVersion: clusterv1.GroupVersion.String(), 484 Kind: "Cluster", 485 Name: "my-cluster", 486 Namespace: "default", 487 FieldPath: "spec.infrastructureRef", 488 }, 489 }, 490 selector: clusterv1.PatchSelector{ 491 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 492 Kind: "AzureClusterTemplate", 493 MatchResources: clusterv1.PatchSelectorMatch{ 494 InfrastructureCluster: true, 495 }, 496 }, 497 match: true, 498 }, 499 { 500 name: "Don't match InfrastructureClusterTemplate, .matchResources.infrastructureCluster not set", 501 req: &runtimehooksv1.GeneratePatchesRequestItem{ 502 Object: runtime.RawExtension{ 503 Object: &unstructured.Unstructured{ 504 Object: map[string]interface{}{ 505 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 506 "kind": "AzureClusterTemplate", 507 }, 508 }, 509 }, 510 HolderReference: runtimehooksv1.HolderReference{ 511 APIVersion: clusterv1.GroupVersion.String(), 512 Kind: "Cluster", 513 Name: "my-cluster", 514 Namespace: "default", 515 FieldPath: "spec.infrastructureRef", 516 }, 517 }, 518 selector: clusterv1.PatchSelector{ 519 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 520 Kind: "AzureClusterTemplate", 521 MatchResources: clusterv1.PatchSelectorMatch{}, 522 }, 523 match: false, 524 }, 525 { 526 name: "Don't match InfrastructureClusterTemplate, .matchResources.infrastructureCluster false", 527 req: &runtimehooksv1.GeneratePatchesRequestItem{ 528 Object: runtime.RawExtension{ 529 Object: &unstructured.Unstructured{ 530 Object: map[string]interface{}{ 531 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 532 "kind": "AzureClusterTemplate", 533 }, 534 }, 535 }, 536 HolderReference: runtimehooksv1.HolderReference{ 537 APIVersion: clusterv1.GroupVersion.String(), 538 Kind: "Cluster", 539 Name: "my-cluster", 540 Namespace: "default", 541 FieldPath: "spec.infrastructureRef", 542 }, 543 }, 544 selector: clusterv1.PatchSelector{ 545 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 546 Kind: "AzureClusterTemplate", 547 MatchResources: clusterv1.PatchSelectorMatch{ 548 InfrastructureCluster: false, 549 }, 550 }, 551 match: false, 552 }, 553 { 554 name: "Match ControlPlaneTemplate", 555 req: &runtimehooksv1.GeneratePatchesRequestItem{ 556 Object: runtime.RawExtension{ 557 Object: &unstructured.Unstructured{ 558 Object: map[string]interface{}{ 559 "apiVersion": "controlplane.cluster.x-k8s.io/v1beta1", 560 "kind": "ControlPlaneTemplate", 561 }, 562 }, 563 }, 564 HolderReference: runtimehooksv1.HolderReference{ 565 APIVersion: clusterv1.GroupVersion.String(), 566 Kind: "Cluster", 567 Name: "my-cluster", 568 Namespace: "default", 569 FieldPath: "spec.controlPlaneRef", 570 }, 571 }, 572 selector: clusterv1.PatchSelector{ 573 APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", 574 Kind: "ControlPlaneTemplate", 575 MatchResources: clusterv1.PatchSelectorMatch{ 576 ControlPlane: true, 577 }, 578 }, 579 match: true, 580 }, 581 { 582 name: "Don't match ControlPlaneTemplate, .matchResources.controlPlane not set", 583 req: &runtimehooksv1.GeneratePatchesRequestItem{ 584 Object: runtime.RawExtension{ 585 Object: &unstructured.Unstructured{ 586 Object: map[string]interface{}{ 587 "apiVersion": "controlplane.cluster.x-k8s.io/v1beta1", 588 "kind": "ControlPlaneTemplate", 589 }, 590 }, 591 }, 592 HolderReference: runtimehooksv1.HolderReference{ 593 APIVersion: clusterv1.GroupVersion.String(), 594 Kind: "Cluster", 595 Name: "my-cluster", 596 Namespace: "default", 597 FieldPath: "spec.controlPlaneRef", 598 }, 599 }, 600 selector: clusterv1.PatchSelector{ 601 APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", 602 Kind: "ControlPlaneTemplate", 603 MatchResources: clusterv1.PatchSelectorMatch{}, 604 }, 605 match: false, 606 }, 607 { 608 name: "Don't match ControlPlaneTemplate, .matchResources.controlPlane false", 609 req: &runtimehooksv1.GeneratePatchesRequestItem{ 610 Object: runtime.RawExtension{ 611 Object: &unstructured.Unstructured{ 612 Object: map[string]interface{}{ 613 "apiVersion": "controlplane.cluster.x-k8s.io/v1beta1", 614 "kind": "ControlPlaneTemplate", 615 }, 616 }, 617 }, 618 HolderReference: runtimehooksv1.HolderReference{ 619 APIVersion: clusterv1.GroupVersion.String(), 620 Kind: "Cluster", 621 Name: "my-cluster", 622 Namespace: "default", 623 FieldPath: "spec.controlPlaneRef", 624 }, 625 }, 626 selector: clusterv1.PatchSelector{ 627 APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", 628 Kind: "ControlPlaneTemplate", 629 MatchResources: clusterv1.PatchSelectorMatch{ 630 ControlPlane: false, 631 }, 632 }, 633 match: false, 634 }, 635 { 636 name: "Match ControlPlane InfrastructureMachineTemplate", 637 req: &runtimehooksv1.GeneratePatchesRequestItem{ 638 Object: runtime.RawExtension{ 639 Object: &unstructured.Unstructured{ 640 Object: map[string]interface{}{ 641 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 642 "kind": "AzureMachineTemplate", 643 }, 644 }, 645 }, 646 HolderReference: runtimehooksv1.HolderReference{ 647 APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", 648 Kind: "KubeadmControlPlane", 649 Name: "my-controlplane", 650 Namespace: "default", 651 FieldPath: "spec.machineTemplate.infrastructureRef", 652 }, 653 }, 654 selector: clusterv1.PatchSelector{ 655 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 656 Kind: "AzureMachineTemplate", 657 MatchResources: clusterv1.PatchSelectorMatch{ 658 ControlPlane: true, 659 }, 660 }, 661 match: true, 662 }, 663 { 664 name: "Match MD BootstrapTemplate", 665 req: &runtimehooksv1.GeneratePatchesRequestItem{ 666 Object: runtime.RawExtension{ 667 Object: &unstructured.Unstructured{ 668 Object: map[string]interface{}{ 669 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 670 "kind": "BootstrapTemplate", 671 }, 672 }, 673 }, 674 HolderReference: runtimehooksv1.HolderReference{ 675 APIVersion: clusterv1.GroupVersion.String(), 676 Kind: "MachineDeployment", 677 Name: "my-md-0", 678 Namespace: "default", 679 FieldPath: "spec.template.spec.bootstrap.configRef", 680 }, 681 }, 682 templateVariables: map[string]apiextensionsv1.JSON{ 683 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"classA"}}`)}, 684 }, 685 selector: clusterv1.PatchSelector{ 686 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 687 Kind: "BootstrapTemplate", 688 MatchResources: clusterv1.PatchSelectorMatch{ 689 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 690 Names: []string{"classA"}, 691 }, 692 }, 693 }, 694 match: true, 695 }, 696 { 697 name: "Match MP BootstrapTemplate", 698 req: &runtimehooksv1.GeneratePatchesRequestItem{ 699 Object: runtime.RawExtension{ 700 Object: &unstructured.Unstructured{ 701 Object: map[string]interface{}{ 702 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 703 "kind": "BootstrapTemplate", 704 }, 705 }, 706 }, 707 HolderReference: runtimehooksv1.HolderReference{ 708 APIVersion: clusterv1.GroupVersion.String(), 709 Kind: "MachinePool", 710 Name: "my-mp-0", 711 Namespace: "default", 712 FieldPath: "spec.template.spec.bootstrap.configRef", 713 }, 714 }, 715 templateVariables: map[string]apiextensionsv1.JSON{ 716 "builtin": {Raw: []byte(`{"machinePool":{"class":"classA"}}`)}, 717 }, 718 selector: clusterv1.PatchSelector{ 719 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 720 Kind: "BootstrapTemplate", 721 MatchResources: clusterv1.PatchSelectorMatch{ 722 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 723 Names: []string{"classA"}, 724 }, 725 }, 726 }, 727 match: true, 728 }, 729 { 730 name: "Match all MD BootstrapTemplate", 731 req: &runtimehooksv1.GeneratePatchesRequestItem{ 732 Object: runtime.RawExtension{ 733 Object: &unstructured.Unstructured{ 734 Object: map[string]interface{}{ 735 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 736 "kind": "BootstrapTemplate", 737 }, 738 }, 739 }, 740 HolderReference: runtimehooksv1.HolderReference{ 741 APIVersion: clusterv1.GroupVersion.String(), 742 Kind: "MachineDeployment", 743 Name: "my-md-0", 744 Namespace: "default", 745 FieldPath: "spec.template.spec.bootstrap.configRef", 746 }, 747 }, 748 templateVariables: map[string]apiextensionsv1.JSON{ 749 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"classA"}}`)}, 750 }, 751 selector: clusterv1.PatchSelector{ 752 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 753 Kind: "BootstrapTemplate", 754 MatchResources: clusterv1.PatchSelectorMatch{ 755 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 756 Names: []string{"*"}, 757 }, 758 }, 759 }, 760 match: true, 761 }, 762 { 763 name: "Match all MP BootstrapTemplate", 764 req: &runtimehooksv1.GeneratePatchesRequestItem{ 765 Object: runtime.RawExtension{ 766 Object: &unstructured.Unstructured{ 767 Object: map[string]interface{}{ 768 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 769 "kind": "BootstrapTemplate", 770 }, 771 }, 772 }, 773 HolderReference: runtimehooksv1.HolderReference{ 774 APIVersion: clusterv1.GroupVersion.String(), 775 Kind: "MachinePool", 776 Name: "my-mp-0", 777 Namespace: "default", 778 FieldPath: "spec.template.spec.bootstrap.configRef", 779 }, 780 }, 781 templateVariables: map[string]apiextensionsv1.JSON{ 782 "builtin": {Raw: []byte(`{"machinePool":{"class":"classA"}}`)}, 783 }, 784 selector: clusterv1.PatchSelector{ 785 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 786 Kind: "BootstrapTemplate", 787 MatchResources: clusterv1.PatchSelectorMatch{ 788 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 789 Names: []string{"*"}, 790 }, 791 }, 792 }, 793 match: true, 794 }, 795 { 796 name: "Glob match MD BootstrapTemplate with <string>-*", 797 req: &runtimehooksv1.GeneratePatchesRequestItem{ 798 Object: runtime.RawExtension{ 799 Object: &unstructured.Unstructured{ 800 Object: map[string]interface{}{ 801 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 802 "kind": "BootstrapTemplate", 803 }, 804 }, 805 }, 806 HolderReference: runtimehooksv1.HolderReference{ 807 APIVersion: clusterv1.GroupVersion.String(), 808 Kind: "MachineDeployment", 809 Name: "my-md-0", 810 Namespace: "default", 811 FieldPath: "spec.template.spec.bootstrap.configRef", 812 }, 813 }, 814 templateVariables: map[string]apiextensionsv1.JSON{ 815 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"class-A"}}`)}, 816 }, 817 selector: clusterv1.PatchSelector{ 818 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 819 Kind: "BootstrapTemplate", 820 MatchResources: clusterv1.PatchSelectorMatch{ 821 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 822 Names: []string{"class-*"}, 823 }, 824 }, 825 }, 826 match: true, 827 }, 828 { 829 name: "Glob match MP BootstrapTemplate with <string>-*", 830 req: &runtimehooksv1.GeneratePatchesRequestItem{ 831 Object: runtime.RawExtension{ 832 Object: &unstructured.Unstructured{ 833 Object: map[string]interface{}{ 834 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 835 "kind": "BootstrapTemplate", 836 }, 837 }, 838 }, 839 HolderReference: runtimehooksv1.HolderReference{ 840 APIVersion: clusterv1.GroupVersion.String(), 841 Kind: "MachinePool", 842 Name: "my-mp-0", 843 Namespace: "default", 844 FieldPath: "spec.template.spec.bootstrap.configRef", 845 }, 846 }, 847 templateVariables: map[string]apiextensionsv1.JSON{ 848 "builtin": {Raw: []byte(`{"machinePool":{"class":"class-A"}}`)}, 849 }, 850 selector: clusterv1.PatchSelector{ 851 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 852 Kind: "BootstrapTemplate", 853 MatchResources: clusterv1.PatchSelectorMatch{ 854 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 855 Names: []string{"class-*"}, 856 }, 857 }, 858 }, 859 match: true, 860 }, 861 { 862 name: "Glob match MD BootstrapTemplate with *-<string>", 863 req: &runtimehooksv1.GeneratePatchesRequestItem{ 864 Object: runtime.RawExtension{ 865 Object: &unstructured.Unstructured{ 866 Object: map[string]interface{}{ 867 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 868 "kind": "BootstrapTemplate", 869 }, 870 }, 871 }, 872 HolderReference: runtimehooksv1.HolderReference{ 873 APIVersion: clusterv1.GroupVersion.String(), 874 Kind: "MachineDeployment", 875 Name: "my-md-0", 876 Namespace: "default", 877 FieldPath: "spec.template.spec.bootstrap.configRef", 878 }, 879 }, 880 templateVariables: map[string]apiextensionsv1.JSON{ 881 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"class-A"}}`)}, 882 }, 883 selector: clusterv1.PatchSelector{ 884 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 885 Kind: "BootstrapTemplate", 886 MatchResources: clusterv1.PatchSelectorMatch{ 887 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 888 Names: []string{"*-A"}, 889 }, 890 }, 891 }, 892 match: true, 893 }, 894 { 895 name: "Glob match MP BootstrapTemplate with *-<string>", 896 req: &runtimehooksv1.GeneratePatchesRequestItem{ 897 Object: runtime.RawExtension{ 898 Object: &unstructured.Unstructured{ 899 Object: map[string]interface{}{ 900 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 901 "kind": "BootstrapTemplate", 902 }, 903 }, 904 }, 905 HolderReference: runtimehooksv1.HolderReference{ 906 APIVersion: clusterv1.GroupVersion.String(), 907 Kind: "MachinePool", 908 Name: "my-mp-0", 909 Namespace: "default", 910 FieldPath: "spec.template.spec.bootstrap.configRef", 911 }, 912 }, 913 templateVariables: map[string]apiextensionsv1.JSON{ 914 "builtin": {Raw: []byte(`{"machinePool":{"class":"class-A"}}`)}, 915 }, 916 selector: clusterv1.PatchSelector{ 917 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 918 Kind: "BootstrapTemplate", 919 MatchResources: clusterv1.PatchSelectorMatch{ 920 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 921 Names: []string{"*-A"}, 922 }, 923 }, 924 }, 925 match: true, 926 }, 927 { 928 name: "Don't match BootstrapTemplate, .matchResources.machineDeploymentClass.names is empty", 929 req: &runtimehooksv1.GeneratePatchesRequestItem{ 930 Object: runtime.RawExtension{ 931 Object: &unstructured.Unstructured{ 932 Object: map[string]interface{}{ 933 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 934 "kind": "BootstrapTemplate", 935 }, 936 }, 937 }, 938 HolderReference: runtimehooksv1.HolderReference{ 939 APIVersion: clusterv1.GroupVersion.String(), 940 Kind: "MachineDeployment", 941 Name: "my-md-0", 942 Namespace: "default", 943 FieldPath: "spec.template.spec.bootstrap.configRef", 944 }, 945 }, 946 templateVariables: map[string]apiextensionsv1.JSON{ 947 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"classA"}}`)}, 948 }, 949 selector: clusterv1.PatchSelector{ 950 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 951 Kind: "BootstrapTemplate", 952 MatchResources: clusterv1.PatchSelectorMatch{ 953 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 954 Names: []string{}, 955 }, 956 }, 957 }, 958 match: false, 959 }, 960 { 961 name: "Don't match BootstrapTemplate, .matchResources.machinePoolClass.names is empty", 962 req: &runtimehooksv1.GeneratePatchesRequestItem{ 963 Object: runtime.RawExtension{ 964 Object: &unstructured.Unstructured{ 965 Object: map[string]interface{}{ 966 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 967 "kind": "BootstrapTemplate", 968 }, 969 }, 970 }, 971 HolderReference: runtimehooksv1.HolderReference{ 972 APIVersion: clusterv1.GroupVersion.String(), 973 Kind: "MachinePool", 974 Name: "my-mp-0", 975 Namespace: "default", 976 FieldPath: "spec.template.spec.bootstrap.configRef", 977 }, 978 }, 979 templateVariables: map[string]apiextensionsv1.JSON{ 980 "builtin": {Raw: []byte(`{"machinePool":{"class":"classA"}}`)}, 981 }, 982 selector: clusterv1.PatchSelector{ 983 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 984 Kind: "BootstrapTemplate", 985 MatchResources: clusterv1.PatchSelectorMatch{ 986 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 987 Names: []string{}, 988 }, 989 }, 990 }, 991 match: false, 992 }, 993 { 994 name: "Do not match BootstrapTemplate, .matchResources.machineDeploymentClass is set to nil", 995 req: &runtimehooksv1.GeneratePatchesRequestItem{ 996 Object: runtime.RawExtension{ 997 Object: &unstructured.Unstructured{ 998 Object: map[string]interface{}{ 999 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 1000 "kind": "BootstrapTemplate", 1001 }, 1002 }, 1003 }, 1004 HolderReference: runtimehooksv1.HolderReference{ 1005 APIVersion: clusterv1.GroupVersion.String(), 1006 Kind: "MachineDeployment", 1007 Name: "my-md-0", 1008 Namespace: "default", 1009 FieldPath: "spec.template.spec.bootstrap.configRef", 1010 }, 1011 }, 1012 templateVariables: map[string]apiextensionsv1.JSON{ 1013 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"classA"}}`)}, 1014 }, 1015 selector: clusterv1.PatchSelector{ 1016 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1017 Kind: "BootstrapTemplate", 1018 MatchResources: clusterv1.PatchSelectorMatch{ 1019 MachineDeploymentClass: nil, 1020 }, 1021 }, 1022 match: false, 1023 }, 1024 { 1025 name: "Do not match BootstrapTemplate, .matchResources.machinePoolClass is set to nil", 1026 req: &runtimehooksv1.GeneratePatchesRequestItem{ 1027 Object: runtime.RawExtension{ 1028 Object: &unstructured.Unstructured{ 1029 Object: map[string]interface{}{ 1030 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 1031 "kind": "BootstrapTemplate", 1032 }, 1033 }, 1034 }, 1035 HolderReference: runtimehooksv1.HolderReference{ 1036 APIVersion: clusterv1.GroupVersion.String(), 1037 Kind: "MachinePool", 1038 Name: "my-mp-0", 1039 Namespace: "default", 1040 FieldPath: "spec.template.spec.bootstrap.configRef", 1041 }, 1042 }, 1043 templateVariables: map[string]apiextensionsv1.JSON{ 1044 "builtin": {Raw: []byte(`{"machinePool":{"class":"classA"}}`)}, 1045 }, 1046 selector: clusterv1.PatchSelector{ 1047 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1048 Kind: "BootstrapTemplate", 1049 MatchResources: clusterv1.PatchSelectorMatch{ 1050 MachinePoolClass: nil, 1051 }, 1052 }, 1053 match: false, 1054 }, 1055 { 1056 name: "Don't match BootstrapTemplate, .matchResources.machineDeploymentClass not set", 1057 req: &runtimehooksv1.GeneratePatchesRequestItem{ 1058 Object: runtime.RawExtension{ 1059 Object: &unstructured.Unstructured{ 1060 Object: map[string]interface{}{ 1061 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 1062 "kind": "BootstrapTemplate", 1063 }, 1064 }, 1065 }, 1066 HolderReference: runtimehooksv1.HolderReference{ 1067 APIVersion: clusterv1.GroupVersion.String(), 1068 Kind: "MachineDeployment", 1069 Name: "my-md-0", 1070 Namespace: "default", 1071 FieldPath: "spec.template.spec.bootstrap.configRef", 1072 }, 1073 }, 1074 templateVariables: map[string]apiextensionsv1.JSON{ 1075 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"classA"}}`)}, 1076 }, 1077 selector: clusterv1.PatchSelector{ 1078 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1079 Kind: "BootstrapTemplate", 1080 MatchResources: clusterv1.PatchSelectorMatch{}, 1081 }, 1082 match: false, 1083 }, 1084 { 1085 name: "Don't match BootstrapTemplate, .matchResources.machinePoolClass not set", 1086 req: &runtimehooksv1.GeneratePatchesRequestItem{ 1087 Object: runtime.RawExtension{ 1088 Object: &unstructured.Unstructured{ 1089 Object: map[string]interface{}{ 1090 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 1091 "kind": "BootstrapTemplate", 1092 }, 1093 }, 1094 }, 1095 HolderReference: runtimehooksv1.HolderReference{ 1096 APIVersion: clusterv1.GroupVersion.String(), 1097 Kind: "MachinePool", 1098 Name: "my-mp-0", 1099 Namespace: "default", 1100 FieldPath: "spec.template.spec.bootstrap.configRef", 1101 }, 1102 }, 1103 templateVariables: map[string]apiextensionsv1.JSON{ 1104 "builtin": {Raw: []byte(`{"machinePool":{"class":"classA"}}`)}, 1105 }, 1106 selector: clusterv1.PatchSelector{ 1107 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1108 Kind: "BootstrapTemplate", 1109 MatchResources: clusterv1.PatchSelectorMatch{}, 1110 }, 1111 match: false, 1112 }, 1113 { 1114 name: "Don't match BootstrapTemplate, .matchResources.machineDeploymentClass does not match", 1115 req: &runtimehooksv1.GeneratePatchesRequestItem{ 1116 Object: runtime.RawExtension{ 1117 Object: &unstructured.Unstructured{ 1118 Object: map[string]interface{}{ 1119 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 1120 "kind": "BootstrapTemplate", 1121 }, 1122 }, 1123 }, 1124 HolderReference: runtimehooksv1.HolderReference{ 1125 APIVersion: clusterv1.GroupVersion.String(), 1126 Kind: "MachineDeployment", 1127 Name: "my-md-0", 1128 Namespace: "default", 1129 FieldPath: "spec.template.spec.bootstrap.configRef", 1130 }, 1131 }, 1132 templateVariables: map[string]apiextensionsv1.JSON{ 1133 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"classA"}}`)}, 1134 }, 1135 selector: clusterv1.PatchSelector{ 1136 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1137 Kind: "BootstrapTemplate", 1138 MatchResources: clusterv1.PatchSelectorMatch{ 1139 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 1140 Names: []string{"classB"}, 1141 }, 1142 }, 1143 }, 1144 match: false, 1145 }, 1146 { 1147 name: "Don't match BootstrapTemplate, .matchResources.machinePoolClass does not match", 1148 req: &runtimehooksv1.GeneratePatchesRequestItem{ 1149 Object: runtime.RawExtension{ 1150 Object: &unstructured.Unstructured{ 1151 Object: map[string]interface{}{ 1152 "apiVersion": "bootstrap.cluster.x-k8s.io/v1beta1", 1153 "kind": "BootstrapTemplate", 1154 }, 1155 }, 1156 }, 1157 HolderReference: runtimehooksv1.HolderReference{ 1158 APIVersion: clusterv1.GroupVersion.String(), 1159 Kind: "MachinePool", 1160 Name: "my-mp-0", 1161 Namespace: "default", 1162 FieldPath: "spec.template.spec.bootstrap.configRef", 1163 }, 1164 }, 1165 templateVariables: map[string]apiextensionsv1.JSON{ 1166 "builtin": {Raw: []byte(`{"machinePool":{"class":"classA"}}`)}, 1167 }, 1168 selector: clusterv1.PatchSelector{ 1169 APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1", 1170 Kind: "BootstrapTemplate", 1171 MatchResources: clusterv1.PatchSelectorMatch{ 1172 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 1173 Names: []string{"classB"}, 1174 }, 1175 }, 1176 }, 1177 match: false, 1178 }, 1179 { 1180 name: "Match MD InfrastructureMachineTemplate", 1181 req: &runtimehooksv1.GeneratePatchesRequestItem{ 1182 Object: runtime.RawExtension{ 1183 Object: &unstructured.Unstructured{ 1184 Object: map[string]interface{}{ 1185 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 1186 "kind": "AzureMachineTemplate", 1187 }, 1188 }, 1189 }, 1190 HolderReference: runtimehooksv1.HolderReference{ 1191 APIVersion: clusterv1.GroupVersion.String(), 1192 Kind: "MachineDeployment", 1193 Name: "my-md-0", 1194 Namespace: "default", 1195 FieldPath: "spec.template.spec.infrastructureRef", 1196 }, 1197 }, 1198 templateVariables: map[string]apiextensionsv1.JSON{ 1199 "builtin": {Raw: []byte(`{"machineDeployment":{"class":"classA"}}`)}, 1200 }, 1201 selector: clusterv1.PatchSelector{ 1202 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 1203 Kind: "AzureMachineTemplate", 1204 MatchResources: clusterv1.PatchSelectorMatch{ 1205 MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ 1206 Names: []string{"classA"}, 1207 }, 1208 }, 1209 }, 1210 match: true, 1211 }, 1212 { 1213 name: "Match MP InfrastructureMachinePoolTemplate", 1214 req: &runtimehooksv1.GeneratePatchesRequestItem{ 1215 Object: runtime.RawExtension{ 1216 Object: &unstructured.Unstructured{ 1217 Object: map[string]interface{}{ 1218 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 1219 "kind": "AzureMachinePoolTemplate", 1220 }, 1221 }, 1222 }, 1223 HolderReference: runtimehooksv1.HolderReference{ 1224 APIVersion: clusterv1.GroupVersion.String(), 1225 Kind: "MachinePool", 1226 Name: "my-mp-0", 1227 Namespace: "default", 1228 FieldPath: "spec.template.spec.infrastructureRef", 1229 }, 1230 }, 1231 templateVariables: map[string]apiextensionsv1.JSON{ 1232 "builtin": {Raw: []byte(`{"machinePool":{"class":"classA"}}`)}, 1233 }, 1234 selector: clusterv1.PatchSelector{ 1235 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 1236 Kind: "AzureMachinePoolTemplate", 1237 MatchResources: clusterv1.PatchSelectorMatch{ 1238 MachinePoolClass: &clusterv1.PatchSelectorMatchMachinePoolClass{ 1239 Names: []string{"classA"}, 1240 }, 1241 }, 1242 }, 1243 match: true, 1244 }, 1245 { 1246 name: "Don't match: unknown field path", 1247 req: &runtimehooksv1.GeneratePatchesRequestItem{ 1248 Object: runtime.RawExtension{ 1249 Object: &unstructured.Unstructured{ 1250 Object: map[string]interface{}{ 1251 "apiVersion": "controlplane.cluster.x-k8s.io/v1beta1", 1252 "kind": "ControlPlaneTemplate", 1253 }, 1254 }, 1255 }, 1256 HolderReference: runtimehooksv1.HolderReference{ 1257 APIVersion: clusterv1.GroupVersion.String(), 1258 Kind: "Custom", 1259 Name: "my-md-0", 1260 Namespace: "default", 1261 FieldPath: "spec.machineTemplate.unknown.infrastructureRef", 1262 }, 1263 }, 1264 selector: clusterv1.PatchSelector{ 1265 APIVersion: "controlplane.cluster.x-k8s.io/v1beta1", 1266 Kind: "ControlPlaneTemplate", 1267 MatchResources: clusterv1.PatchSelectorMatch{ 1268 ControlPlane: true, 1269 }, 1270 }, 1271 match: false, 1272 }, 1273 } 1274 for _, tt := range tests { 1275 t.Run(tt.name, func(t *testing.T) { 1276 g := NewWithT(t) 1277 1278 g.Expect(matchesSelector(tt.req, tt.templateVariables, tt.selector)).To(Equal(tt.match)) 1279 }) 1280 } 1281 } 1282 1283 func TestPatchIsEnabled(t *testing.T) { 1284 tests := []struct { 1285 name string 1286 enabledIf *string 1287 variables map[string]apiextensionsv1.JSON 1288 want bool 1289 wantErr bool 1290 }{ 1291 { 1292 name: "Enabled if enabledIf is not set", 1293 enabledIf: nil, 1294 want: true, 1295 }, 1296 { 1297 name: "Fail if template is invalid", 1298 enabledIf: pointer.String(`{{ variable }}`), // . is missing 1299 wantErr: true, 1300 }, 1301 // Hardcoded value. 1302 { 1303 name: "Enabled if template is true ", 1304 enabledIf: pointer.String(`true`), 1305 want: true, 1306 }, 1307 { 1308 name: "Enabled if template is true (even with leading and trailing new line)", 1309 enabledIf: pointer.String(` 1310 true 1311 `), 1312 want: true, 1313 }, 1314 { 1315 name: "Disabled if template is false", 1316 enabledIf: pointer.String(`false`), 1317 want: false, 1318 }, 1319 // Boolean variable. 1320 { 1321 name: "Enabled if simple template with boolean variable evaluates to true", 1322 enabledIf: pointer.String(`{{ .httpProxyEnabled }}`), 1323 variables: map[string]apiextensionsv1.JSON{ 1324 "httpProxyEnabled": {Raw: []byte(`true`)}, 1325 }, 1326 want: true, 1327 }, 1328 { 1329 name: "Enabled if simple template with boolean variable evaluates to true (even with leading and trailing new line", 1330 enabledIf: pointer.String(` 1331 {{ .httpProxyEnabled }} 1332 `), 1333 variables: map[string]apiextensionsv1.JSON{ 1334 "httpProxyEnabled": {Raw: []byte(`true`)}, 1335 }, 1336 want: true, 1337 }, 1338 { 1339 name: "Disabled if simple template with boolean variable evaluates to false", 1340 enabledIf: pointer.String(`{{ .httpProxyEnabled }}`), 1341 variables: map[string]apiextensionsv1.JSON{ 1342 "httpProxyEnabled": {Raw: []byte(`false`)}, 1343 }, 1344 want: false, 1345 }, 1346 // Render value with if/else. 1347 { 1348 name: "Enabled if template with if evaluates to true", 1349 // Else is not needed because we check if the result is equal to true. 1350 enabledIf: pointer.String(`{{ if eq "v1.21.1" .builtin.cluster.topology.version }}true{{end}}`), 1351 variables: map[string]apiextensionsv1.JSON{ 1352 "builtin": {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1353 }, 1354 want: true, 1355 }, 1356 { 1357 name: "Disabled if template with if evaluates to false", 1358 enabledIf: pointer.String(`{{ if eq "v1.21.2" .builtin.cluster.topology.version }}true{{end}}`), 1359 variables: map[string]apiextensionsv1.JSON{ 1360 "builtin": {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1361 }, 1362 want: false, 1363 }, 1364 { 1365 name: "Enabled if template with if/else evaluates to true", 1366 enabledIf: pointer.String(`{{ if eq "v1.21.1" .builtin.cluster.topology.version }}true{{else}}false{{end}}`), 1367 variables: map[string]apiextensionsv1.JSON{ 1368 "builtin": {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1369 }, 1370 want: true, 1371 }, 1372 { 1373 name: "Disabled if template with if/else evaluates to false", 1374 enabledIf: pointer.String(`{{ if eq "v1.21.2" .builtin.cluster.topology.version }}true{{else}}false{{end}}`), 1375 variables: map[string]apiextensionsv1.JSON{ 1376 "builtin": {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1377 }, 1378 want: false, 1379 }, 1380 // Render value with if to check if var is not empty. 1381 { 1382 name: "Enabled if template which checks if variable is set evaluates to true", 1383 enabledIf: pointer.String(`{{ if .variableA }}true{{end}}`), 1384 variables: map[string]apiextensionsv1.JSON{ 1385 "variableA": {Raw: []byte(`"abc"`)}, 1386 }, 1387 want: true, 1388 }, 1389 { 1390 name: "Disabled if template which checks if variable is set evaluates to false (variable empty)", 1391 enabledIf: pointer.String(`{{ if .variableA }}true{{end}}`), 1392 variables: map[string]apiextensionsv1.JSON{ 1393 "variableA": {Raw: []byte(``)}, 1394 }, 1395 want: false, 1396 }, 1397 { 1398 name: "Disabled if template which checks if variable is set evaluates to false (variable empty string)", 1399 enabledIf: pointer.String(`{{ if .variableA }}true{{end}}`), 1400 variables: map[string]apiextensionsv1.JSON{ 1401 "variableA": {Raw: []byte(`""`)}, 1402 }, 1403 want: false, 1404 }, 1405 { 1406 name: "Disabled if template which checks if variable is set evaluates to false (variable does not exist)", 1407 enabledIf: pointer.String(`{{ if .variableA }}true{{end}}`), 1408 variables: map[string]apiextensionsv1.JSON{ 1409 "variableB": {Raw: []byte(``)}, 1410 }, 1411 want: false, 1412 }, 1413 // Render value with object variable. 1414 // NOTE: the builtin variable tests above test something very similar, so this 1415 // test mostly exists to visualize how user-defined object variables can be used. 1416 { 1417 name: "Enabled if template with complex variable evaluates to true", 1418 enabledIf: pointer.String(`{{ if .httpProxy.enabled }}true{{end}}`), 1419 variables: map[string]apiextensionsv1.JSON{ 1420 "httpProxy": {Raw: []byte(`{"enabled": true, "url": "localhost:3128", "noProxy": "internal.example.com"}`)}, 1421 }, 1422 want: true, 1423 }, 1424 { 1425 name: "Disabled if template with complex variable evaluates to false", 1426 enabledIf: pointer.String(`{{ if .httpProxy.enabled }}true{{end}}`), 1427 variables: map[string]apiextensionsv1.JSON{ 1428 "httpProxy": {Raw: []byte(`{"enabled": false, "url": "localhost:3128", "noProxy": "internal.example.com"}`)}, 1429 }, 1430 want: false, 1431 }, 1432 } 1433 for _, tt := range tests { 1434 t.Run(tt.name, func(t *testing.T) { 1435 g := NewWithT(t) 1436 1437 got, err := patchIsEnabled(tt.enabledIf, tt.variables) 1438 if tt.wantErr { 1439 g.Expect(err).To(HaveOccurred()) 1440 return 1441 } 1442 g.Expect(err).ToNot(HaveOccurred()) 1443 1444 g.Expect(got).To(Equal(tt.want)) 1445 }) 1446 } 1447 } 1448 1449 func TestCalculateValue(t *testing.T) { 1450 tests := []struct { 1451 name string 1452 patch clusterv1.JSONPatch 1453 variables map[string]apiextensionsv1.JSON 1454 want *apiextensionsv1.JSON 1455 wantErr bool 1456 }{ 1457 { 1458 name: "Fails if neither .value nor .valueFrom are set", 1459 patch: clusterv1.JSONPatch{}, 1460 wantErr: true, 1461 }, 1462 { 1463 name: "Fails if both .value and .valueFrom are set", 1464 patch: clusterv1.JSONPatch{ 1465 Value: &apiextensionsv1.JSON{Raw: []byte(`"value"`)}, 1466 ValueFrom: &clusterv1.JSONPatchValue{ 1467 Variable: pointer.String("variableA"), 1468 }, 1469 }, 1470 wantErr: true, 1471 }, 1472 { 1473 name: "Fails if .valueFrom.variable and .valueFrom.template are set", 1474 patch: clusterv1.JSONPatch{ 1475 ValueFrom: &clusterv1.JSONPatchValue{ 1476 Variable: pointer.String("variableA"), 1477 Template: pointer.String("template"), 1478 }, 1479 }, 1480 wantErr: true, 1481 }, 1482 { 1483 name: "Fails if .valueFrom is set, but .valueFrom.variable and .valueFrom.template are both not set", 1484 patch: clusterv1.JSONPatch{ 1485 ValueFrom: &clusterv1.JSONPatchValue{}, 1486 }, 1487 wantErr: true, 1488 }, 1489 { 1490 name: "Should return .value if set", 1491 patch: clusterv1.JSONPatch{ 1492 Value: &apiextensionsv1.JSON{Raw: []byte(`"value"`)}, 1493 }, 1494 want: &apiextensionsv1.JSON{Raw: []byte(`"value"`)}, 1495 }, 1496 { 1497 name: "Should return .valueFrom.variable if set", 1498 patch: clusterv1.JSONPatch{ 1499 ValueFrom: &clusterv1.JSONPatchValue{ 1500 Variable: pointer.String("variableA"), 1501 }, 1502 }, 1503 variables: map[string]apiextensionsv1.JSON{ 1504 "variableA": {Raw: []byte(`"value"`)}, 1505 }, 1506 want: &apiextensionsv1.JSON{Raw: []byte(`"value"`)}, 1507 }, 1508 { 1509 name: "Fails if .valueFrom.variable is set but variable does not exist", 1510 patch: clusterv1.JSONPatch{ 1511 ValueFrom: &clusterv1.JSONPatchValue{ 1512 Variable: pointer.String("variableA"), 1513 }, 1514 }, 1515 variables: map[string]apiextensionsv1.JSON{ 1516 "variableB": {Raw: []byte(`"value"`)}, 1517 }, 1518 wantErr: true, 1519 }, 1520 { 1521 name: "Should return .valueFrom.variable if set: builtinVariable int", 1522 patch: clusterv1.JSONPatch{ 1523 ValueFrom: &clusterv1.JSONPatchValue{ 1524 Variable: pointer.String("builtin.controlPlane.replicas"), 1525 }, 1526 }, 1527 variables: map[string]apiextensionsv1.JSON{ 1528 patchvariables.BuiltinsName: {Raw: []byte(`{"controlPlane":{"replicas":3}}`)}, 1529 }, 1530 want: &apiextensionsv1.JSON{Raw: []byte(`3`)}, 1531 }, 1532 { 1533 name: "Should return .valueFrom.variable if set: builtinVariable string", 1534 patch: clusterv1.JSONPatch{ 1535 ValueFrom: &clusterv1.JSONPatchValue{ 1536 Variable: pointer.String("builtin.cluster.topology.version"), 1537 }, 1538 }, 1539 variables: map[string]apiextensionsv1.JSON{ 1540 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1541 }, 1542 want: &apiextensionsv1.JSON{Raw: []byte(`"v1.21.1"`)}, 1543 }, 1544 { 1545 name: "Should return .valueFrom.variable if set: variable 'builtin'", 1546 patch: clusterv1.JSONPatch{ 1547 ValueFrom: &clusterv1.JSONPatchValue{ 1548 Variable: pointer.String("builtin"), 1549 }, 1550 }, 1551 variables: map[string]apiextensionsv1.JSON{ 1552 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1553 }, 1554 want: &apiextensionsv1.JSON{Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1555 }, 1556 { 1557 name: "Should return .valueFrom.variable if set: variable 'builtin.cluster'", 1558 patch: clusterv1.JSONPatch{ 1559 ValueFrom: &clusterv1.JSONPatchValue{ 1560 Variable: pointer.String("builtin.cluster"), 1561 }, 1562 }, 1563 variables: map[string]apiextensionsv1.JSON{ 1564 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1565 }, 1566 want: &apiextensionsv1.JSON{Raw: []byte(`{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}`)}, 1567 }, 1568 { 1569 name: "Should return .valueFrom.variable if set: variable 'builtin.cluster.topology'", 1570 patch: clusterv1.JSONPatch{ 1571 ValueFrom: &clusterv1.JSONPatchValue{ 1572 Variable: pointer.String("builtin.cluster.topology"), 1573 }, 1574 }, 1575 variables: map[string]apiextensionsv1.JSON{ 1576 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, 1577 }, 1578 want: &apiextensionsv1.JSON{Raw: []byte(`{"class":"clusterClass1","version":"v1.21.1"}`)}, 1579 }, 1580 { 1581 // NOTE: Template rendering is tested more extensively in TestRenderValueTemplate 1582 name: "Should return rendered .valueFrom.template if set", 1583 patch: clusterv1.JSONPatch{ 1584 ValueFrom: &clusterv1.JSONPatchValue{ 1585 Template: pointer.String("{{ .variableA }}"), 1586 }, 1587 }, 1588 variables: map[string]apiextensionsv1.JSON{ 1589 "variableA": {Raw: []byte(`"value"`)}, 1590 }, 1591 want: &apiextensionsv1.JSON{Raw: []byte(`"value"`)}, 1592 }, 1593 // Objects 1594 { 1595 name: "Should return .valueFrom.variable if set: whole object", 1596 patch: clusterv1.JSONPatch{ 1597 ValueFrom: &clusterv1.JSONPatchValue{ 1598 Variable: pointer.String("variableObject"), 1599 }, 1600 }, 1601 variables: map[string]apiextensionsv1.JSON{ 1602 "variableObject": {Raw: []byte(`{"requiredProperty":false,"boolProperty":true,"integerProperty":1,"enumProperty":"enumValue2"}`)}, 1603 }, 1604 want: &apiextensionsv1.JSON{Raw: []byte(`{"requiredProperty":false,"boolProperty":true,"integerProperty":1,"enumProperty":"enumValue2"}`)}, 1605 }, 1606 { 1607 name: "Should return .valueFrom.variable if set: nested bool property", 1608 patch: clusterv1.JSONPatch{ 1609 ValueFrom: &clusterv1.JSONPatchValue{ 1610 Variable: pointer.String("variableObject.boolProperty"), 1611 }, 1612 }, 1613 variables: map[string]apiextensionsv1.JSON{ 1614 "variableObject": {Raw: []byte(`{"boolProperty":true,"integerProperty":1,"enumProperty":"enumValue2"}`)}, 1615 }, 1616 want: &apiextensionsv1.JSON{Raw: []byte(`true`)}, 1617 }, 1618 { 1619 name: "Should return .valueFrom.variable if set: nested integer property", 1620 patch: clusterv1.JSONPatch{ 1621 ValueFrom: &clusterv1.JSONPatchValue{ 1622 Variable: pointer.String("variableObject.integerProperty"), 1623 }, 1624 }, 1625 variables: map[string]apiextensionsv1.JSON{ 1626 "variableObject": {Raw: []byte(`{"boolProperty":true,"integerProperty":1,"enumProperty":"enumValue2"}`)}, 1627 }, 1628 want: &apiextensionsv1.JSON{Raw: []byte(`1`)}, 1629 }, 1630 { 1631 name: "Should return .valueFrom.variable if set: nested string property", 1632 patch: clusterv1.JSONPatch{ 1633 ValueFrom: &clusterv1.JSONPatchValue{ 1634 Variable: pointer.String("variableObject.enumProperty"), 1635 }, 1636 }, 1637 variables: map[string]apiextensionsv1.JSON{ 1638 "variableObject": {Raw: []byte(`{"boolProperty":true,"integerProperty":1,"enumProperty":"enumValue2"}`)}, 1639 }, 1640 want: &apiextensionsv1.JSON{Raw: []byte(`"enumValue2"`)}, 1641 }, 1642 { 1643 name: "Fails if .valueFrom.variable object variable does not exist", 1644 patch: clusterv1.JSONPatch{ 1645 ValueFrom: &clusterv1.JSONPatchValue{ 1646 Variable: pointer.String("variableObject.enumProperty"), 1647 }, 1648 }, 1649 variables: map[string]apiextensionsv1.JSON{ 1650 "anotherObject": {Raw: []byte(`{"boolProperty":true,"integerProperty":1,"enumProperty":"enumValue2"}`)}, 1651 }, 1652 wantErr: true, 1653 }, 1654 { 1655 name: "Fails if .valueFrom.variable nested object property does not exist", 1656 patch: clusterv1.JSONPatch{ 1657 ValueFrom: &clusterv1.JSONPatchValue{ 1658 Variable: pointer.String("variableObject.nonExistingProperty"), 1659 }, 1660 }, 1661 variables: map[string]apiextensionsv1.JSON{ 1662 "anotherObject": {Raw: []byte(`{"boolProperty":true,"integerProperty":1}`)}, 1663 }, 1664 wantErr: true, 1665 }, 1666 { 1667 name: "Fails if .valueFrom.variable nested object property is an array instead", 1668 patch: clusterv1.JSONPatch{ 1669 ValueFrom: &clusterv1.JSONPatchValue{ 1670 // NOTE: it's not possible to access a property of an array element without index. 1671 Variable: pointer.String("variableObject.nonExistingProperty"), 1672 }, 1673 }, 1674 variables: map[string]apiextensionsv1.JSON{ 1675 "anotherObject": {Raw: []byte(`[{"boolProperty":true,"integerProperty":1}]`)}, 1676 }, 1677 wantErr: true, 1678 }, 1679 // Deeper nested Objects 1680 { 1681 name: "Should return .valueFrom.variable if set: nested object property top-level", 1682 patch: clusterv1.JSONPatch{ 1683 ValueFrom: &clusterv1.JSONPatchValue{ 1684 Variable: pointer.String("variableObject"), 1685 }, 1686 }, 1687 variables: map[string]apiextensionsv1.JSON{ 1688 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 1689 }, 1690 want: &apiextensionsv1.JSON{Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 1691 }, 1692 { 1693 name: "Should return .valueFrom.variable if set: nested object property firstLevel", 1694 patch: clusterv1.JSONPatch{ 1695 ValueFrom: &clusterv1.JSONPatchValue{ 1696 Variable: pointer.String("variableObject.firstLevel"), 1697 }, 1698 }, 1699 variables: map[string]apiextensionsv1.JSON{ 1700 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 1701 }, 1702 want: &apiextensionsv1.JSON{Raw: []byte(`{"secondLevel":{"leaf":"value"}}`)}, 1703 }, 1704 { 1705 name: "Should return .valueFrom.variable if set: nested object property secondLevel", 1706 patch: clusterv1.JSONPatch{ 1707 ValueFrom: &clusterv1.JSONPatchValue{ 1708 Variable: pointer.String("variableObject.firstLevel.secondLevel"), 1709 }, 1710 }, 1711 variables: map[string]apiextensionsv1.JSON{ 1712 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 1713 }, 1714 want: &apiextensionsv1.JSON{Raw: []byte(`{"leaf":"value"}`)}, 1715 }, 1716 { 1717 name: "Should return .valueFrom.variable if set: nested object property leaf", 1718 patch: clusterv1.JSONPatch{ 1719 ValueFrom: &clusterv1.JSONPatchValue{ 1720 Variable: pointer.String("variableObject.firstLevel.secondLevel.leaf"), 1721 }, 1722 }, 1723 variables: map[string]apiextensionsv1.JSON{ 1724 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 1725 }, 1726 want: &apiextensionsv1.JSON{Raw: []byte(`"value"`)}, 1727 }, 1728 // Array 1729 { 1730 name: "Should return .valueFrom.variable if set: array", 1731 patch: clusterv1.JSONPatch{ 1732 ValueFrom: &clusterv1.JSONPatchValue{ 1733 Variable: pointer.String("variableArray"), 1734 }, 1735 }, 1736 variables: map[string]apiextensionsv1.JSON{ 1737 "variableArray": {Raw: []byte(`["abc","def"]`)}, 1738 }, 1739 want: &apiextensionsv1.JSON{Raw: []byte(`["abc","def"]`)}, 1740 }, 1741 { 1742 name: "Should return .valueFrom.variable if set: array element", 1743 patch: clusterv1.JSONPatch{ 1744 ValueFrom: &clusterv1.JSONPatchValue{ 1745 Variable: pointer.String("variableArray[0]"), 1746 }, 1747 }, 1748 variables: map[string]apiextensionsv1.JSON{ 1749 "variableArray": {Raw: []byte(`["abc","def"]`)}, 1750 }, 1751 want: &apiextensionsv1.JSON{Raw: []byte(`"abc"`)}, 1752 }, 1753 { 1754 name: "Should return .valueFrom.variable if set: nested array", 1755 patch: clusterv1.JSONPatch{ 1756 ValueFrom: &clusterv1.JSONPatchValue{ 1757 Variable: pointer.String("variableArray.firstLevel"), 1758 }, 1759 }, 1760 variables: map[string]apiextensionsv1.JSON{ 1761 "variableArray": {Raw: []byte(`{"firstLevel":["abc","def"]}`)}, 1762 }, 1763 want: &apiextensionsv1.JSON{Raw: []byte(`["abc","def"]`)}, 1764 }, 1765 { 1766 name: "Should return .valueFrom.variable if set: nested array element", 1767 patch: clusterv1.JSONPatch{ 1768 ValueFrom: &clusterv1.JSONPatchValue{ 1769 Variable: pointer.String("variableArray.firstLevel[1]"), 1770 }, 1771 }, 1772 variables: map[string]apiextensionsv1.JSON{ 1773 "variableArray": {Raw: []byte(`{"firstLevel":[{"secondLevel":"firstElement"},{"secondLevel":"secondElement"}]}`)}, 1774 }, 1775 want: &apiextensionsv1.JSON{Raw: []byte(`{"secondLevel":"secondElement"}`)}, 1776 }, 1777 { 1778 name: "Should return .valueFrom.variable if set: nested field of nested array element", 1779 patch: clusterv1.JSONPatch{ 1780 ValueFrom: &clusterv1.JSONPatchValue{ 1781 Variable: pointer.String("variableArray.firstLevel[1].secondLevel"), 1782 }, 1783 }, 1784 variables: map[string]apiextensionsv1.JSON{ 1785 "variableArray": {Raw: []byte(`{"firstLevel":[{"secondLevel":"firstElement"},{"secondLevel":"secondElement"}]}`)}, 1786 }, 1787 want: &apiextensionsv1.JSON{Raw: []byte(`"secondElement"`)}, 1788 }, 1789 { 1790 name: "Fails if .valueFrom.variable array path is invalid: only left delimiter", 1791 patch: clusterv1.JSONPatch{ 1792 ValueFrom: &clusterv1.JSONPatchValue{ 1793 Variable: pointer.String("variableArray.firstLevel["), 1794 }, 1795 }, 1796 variables: map[string]apiextensionsv1.JSON{ 1797 "variableArray": {Raw: []byte(`{"firstLevel":[{"secondLevel":"firstElement"}]}`)}, 1798 }, 1799 wantErr: true, 1800 }, 1801 { 1802 name: "Fails if .valueFrom.variable array path is invalid: only right delimiter", 1803 patch: clusterv1.JSONPatch{ 1804 ValueFrom: &clusterv1.JSONPatchValue{ 1805 Variable: pointer.String("variableArray.firstLevel]"), 1806 }, 1807 }, 1808 variables: map[string]apiextensionsv1.JSON{ 1809 "variableArray": {Raw: []byte(`{"firstLevel":[{"secondLevel":"firstElement"}]}`)}, 1810 }, 1811 wantErr: true, 1812 }, 1813 { 1814 name: "Fails if .valueFrom.variable array path is invalid: no index", 1815 patch: clusterv1.JSONPatch{ 1816 ValueFrom: &clusterv1.JSONPatchValue{ 1817 Variable: pointer.String("variableArray.firstLevel[]"), 1818 }, 1819 }, 1820 variables: map[string]apiextensionsv1.JSON{ 1821 "variableArray": {Raw: []byte(`{"firstLevel":[{"secondLevel":"firstElement"}]}`)}, 1822 }, 1823 wantErr: true, 1824 }, 1825 { 1826 name: "Fails if .valueFrom.variable array path is invalid: text index", 1827 patch: clusterv1.JSONPatch{ 1828 ValueFrom: &clusterv1.JSONPatchValue{ 1829 Variable: pointer.String("variableArray.firstLevel[someText]"), 1830 }, 1831 }, 1832 variables: map[string]apiextensionsv1.JSON{ 1833 "variableArray": {Raw: []byte(`{"firstLevel":[{"secondLevel":"firstElement"}]}`)}, 1834 }, 1835 wantErr: true, 1836 }, 1837 { 1838 name: "Fails if .valueFrom.variable array path is invalid: negative index", 1839 patch: clusterv1.JSONPatch{ 1840 ValueFrom: &clusterv1.JSONPatchValue{ 1841 Variable: pointer.String("variableArray.firstLevel[-1]"), 1842 }, 1843 }, 1844 variables: map[string]apiextensionsv1.JSON{ 1845 "variableArray": {Raw: []byte(`{"firstLevel":[{"secondLevel":"firstElement"}]}`)}, 1846 }, 1847 wantErr: true, 1848 }, 1849 { 1850 name: "Fails if .valueFrom.variable array path is invalid: index out of bounds", 1851 patch: clusterv1.JSONPatch{ 1852 ValueFrom: &clusterv1.JSONPatchValue{ 1853 Variable: pointer.String("variableArray.firstLevel[1]"), 1854 }, 1855 }, 1856 variables: map[string]apiextensionsv1.JSON{ 1857 "variableArray": {Raw: []byte(`{"firstLevel":[{"secondLevel":"firstElement"}]}`)}, 1858 }, 1859 wantErr: true, 1860 }, 1861 { 1862 name: "Fails if .valueFrom.variable array path is invalid: variable is an object instead", 1863 patch: clusterv1.JSONPatch{ 1864 ValueFrom: &clusterv1.JSONPatchValue{ 1865 Variable: pointer.String("variableArray.firstLevel[1]"), 1866 }, 1867 }, 1868 variables: map[string]apiextensionsv1.JSON{ 1869 "variableArray": {Raw: []byte(`{"firstLevel":{"secondLevel":"firstElement"}}`)}, 1870 }, 1871 wantErr: true, 1872 }, 1873 } 1874 for _, tt := range tests { 1875 t.Run(tt.name, func(t *testing.T) { 1876 g := NewWithT(t) 1877 1878 got, err := calculateValue(tt.patch, tt.variables) 1879 if tt.wantErr { 1880 g.Expect(err).To(HaveOccurred()) 1881 return 1882 } 1883 g.Expect(err).ToNot(HaveOccurred()) 1884 1885 g.Expect(got).To(BeComparableTo(tt.want)) 1886 }) 1887 } 1888 } 1889 1890 func TestRenderValueTemplate(t *testing.T) { 1891 tests := []struct { 1892 name string 1893 template string 1894 variables map[string]apiextensionsv1.JSON 1895 want *apiextensionsv1.JSON 1896 wantErr bool 1897 }{ 1898 // Basic types 1899 { 1900 name: "Should render a string variable", 1901 template: `{{ .stringVariable }}`, 1902 variables: map[string]apiextensionsv1.JSON{ 1903 "stringVariable": {Raw: []byte(`"bar"`)}, 1904 }, 1905 want: &apiextensionsv1.JSON{Raw: []byte(`"bar"`)}, 1906 }, 1907 { 1908 name: "Should render an integer variable", 1909 template: `{{ .integerVariable }}`, 1910 variables: map[string]apiextensionsv1.JSON{ 1911 "integerVariable": {Raw: []byte("3")}, 1912 }, 1913 want: &apiextensionsv1.JSON{Raw: []byte(`3`)}, 1914 }, 1915 { 1916 name: "Should render a number variable", 1917 template: `{{ .numberVariable }}`, 1918 variables: map[string]apiextensionsv1.JSON{ 1919 "numberVariable": {Raw: []byte("2.5")}, 1920 }, 1921 want: &apiextensionsv1.JSON{Raw: []byte(`2.5`)}, 1922 }, 1923 { 1924 name: "Should render a boolean variable", 1925 template: `{{ .booleanVariable }}`, 1926 variables: map[string]apiextensionsv1.JSON{ 1927 "booleanVariable": {Raw: []byte("true")}, 1928 }, 1929 want: &apiextensionsv1.JSON{Raw: []byte(`true`)}, 1930 }, 1931 { 1932 name: "Fails if the template is invalid", 1933 template: `{{ booleanVariable }}`, 1934 variables: map[string]apiextensionsv1.JSON{ 1935 "booleanVariable": {Raw: []byte("true")}, 1936 }, 1937 wantErr: true, 1938 }, 1939 // Default variables via template 1940 { 1941 name: "Should render depending on variable existence: variable is set", 1942 template: `{{ if .vnetName }}{{.vnetName}}{{else}}{{.builtin.cluster.name}}-vnet{{end}}`, 1943 variables: map[string]apiextensionsv1.JSON{ 1944 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster1"}}`)}, 1945 "vnetName": {Raw: []byte(`"custom-network"`)}, 1946 }, 1947 want: &apiextensionsv1.JSON{Raw: []byte(`"custom-network"`)}, 1948 }, 1949 { 1950 name: "Should render depending on variable existence: variable is not set", 1951 template: `{{ if .vnetName }}{{.vnetName}}{{else}}{{.builtin.cluster.name}}-vnet{{end}}`, 1952 variables: map[string]apiextensionsv1.JSON{ 1953 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster1"}}`)}, 1954 }, 1955 want: &apiextensionsv1.JSON{Raw: []byte(`"cluster1-vnet"`)}, 1956 }, 1957 // YAML 1958 { 1959 name: "Should render a YAML array", 1960 template: ` 1961 - contentFrom: 1962 secret: 1963 key: control-plane-azure.json 1964 name: "{{ .builtin.cluster.name }}-control-plane-azure-json" 1965 owner: root:root 1966 `, 1967 variables: map[string]apiextensionsv1.JSON{ 1968 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster1"}}`)}, 1969 }, 1970 want: &apiextensionsv1.JSON{Raw: []byte(` 1971 [{ 1972 "contentFrom":{ 1973 "secret":{ 1974 "key":"control-plane-azure.json", 1975 "name":"cluster1-control-plane-azure-json" 1976 } 1977 }, 1978 "owner":"root:root" 1979 }]`), 1980 }, 1981 }, 1982 { 1983 name: "Should render a YAML object", 1984 template: ` 1985 contentFrom: 1986 secret: 1987 key: control-plane-azure.json 1988 name: "{{ .builtin.cluster.name }}-control-plane-azure-json" 1989 owner: root:root 1990 `, 1991 variables: map[string]apiextensionsv1.JSON{ 1992 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster1"}}`)}, 1993 }, 1994 want: &apiextensionsv1.JSON{Raw: []byte(` 1995 { 1996 "contentFrom":{ 1997 "secret":{ 1998 "key":"control-plane-azure.json", 1999 "name":"cluster1-control-plane-azure-json" 2000 } 2001 }, 2002 "owner":"root:root" 2003 }`), 2004 }, 2005 }, 2006 // JSON 2007 { 2008 name: "Should render a JSON array", 2009 template: ` 2010 [{ 2011 "contentFrom":{ 2012 "secret":{ 2013 "key":"control-plane-azure.json", 2014 "name":"{{ .builtin.cluster.name }}-control-plane-azure-json" 2015 } 2016 }, 2017 "owner":"root:root" 2018 }]`, 2019 variables: map[string]apiextensionsv1.JSON{ 2020 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster1"}}`)}, 2021 }, 2022 want: &apiextensionsv1.JSON{Raw: []byte(` 2023 [{ 2024 "contentFrom":{ 2025 "secret":{ 2026 "key":"control-plane-azure.json", 2027 "name":"cluster1-control-plane-azure-json" 2028 } 2029 }, 2030 "owner":"root:root" 2031 }]`), 2032 }, 2033 }, 2034 { 2035 name: "Should render a JSON object", 2036 template: ` 2037 { 2038 "contentFrom":{ 2039 "secret":{ 2040 "key":"control-plane-azure.json", 2041 "name":"{{ .builtin.cluster.name }}-control-plane-azure-json" 2042 } 2043 }, 2044 "owner":"root:root" 2045 }`, 2046 variables: map[string]apiextensionsv1.JSON{ 2047 patchvariables.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster1"}}`)}, 2048 }, 2049 want: &apiextensionsv1.JSON{Raw: []byte(` 2050 { 2051 "contentFrom":{ 2052 "secret":{ 2053 "key":"control-plane-azure.json", 2054 "name":"cluster1-control-plane-azure-json" 2055 } 2056 }, 2057 "owner":"root:root" 2058 }`), 2059 }, 2060 }, 2061 // Object types 2062 { 2063 name: "Should render a object property top-level", 2064 template: `{{ .variableObject }}`, 2065 variables: map[string]apiextensionsv1.JSON{ 2066 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 2067 }, 2068 want: &apiextensionsv1.JSON{Raw: []byte(`"map[firstLevel:map[secondLevel:map[leaf:value]]]"`)}, // Not ideal but that's go templating. 2069 }, 2070 { 2071 name: "Should render a object property firstLevel", 2072 template: `{{ .variableObject.firstLevel }}`, 2073 variables: map[string]apiextensionsv1.JSON{ 2074 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 2075 }, 2076 want: &apiextensionsv1.JSON{Raw: []byte(`"map[secondLevel:map[leaf:value]]"`)}, // Not ideal but that's go templating. 2077 }, 2078 { 2079 name: "Should render a object property secondLevel", 2080 template: `{{ .variableObject.firstLevel.secondLevel }}`, 2081 variables: map[string]apiextensionsv1.JSON{ 2082 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 2083 }, 2084 want: &apiextensionsv1.JSON{Raw: []byte(`"map[leaf:value]"`)}, // Not ideal but that's go templating. 2085 }, 2086 { 2087 name: "Should render a object property leaf", 2088 template: `{{ .variableObject.firstLevel.secondLevel.leaf }}`, 2089 variables: map[string]apiextensionsv1.JSON{ 2090 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 2091 }, 2092 want: &apiextensionsv1.JSON{Raw: []byte(`"value"`)}, 2093 }, 2094 { 2095 name: "Should render even if object property leaf does not exist", 2096 template: `{{ .variableObject.firstLevel.secondLevel.anotherLeaf }}`, 2097 variables: map[string]apiextensionsv1.JSON{ 2098 "variableObject": {Raw: []byte(`{"firstLevel":{"secondLevel":{"leaf":"value"}}}`)}, 2099 }, 2100 want: &apiextensionsv1.JSON{Raw: []byte(`"\u003cno value\u003e"`)}, 2101 }, 2102 { 2103 name: "Should render a object with range", 2104 template: ` 2105 { 2106 {{ range $key, $value := .variableObject }} 2107 "{{$key}}-modified": "{{$value}}", 2108 {{end}} 2109 } 2110 `, 2111 variables: map[string]apiextensionsv1.JSON{ 2112 "variableObject": {Raw: []byte(`{"key1":"value1","key2":"value2"}`)}, 2113 }, 2114 want: &apiextensionsv1.JSON{Raw: []byte(`{"key1-modified":"value1","key2-modified":"value2"}`)}, 2115 }, 2116 // Arrays 2117 { 2118 name: "Should render an array property", 2119 template: `{{ .variableArray }}`, 2120 variables: map[string]apiextensionsv1.JSON{ 2121 "variableArray": {Raw: []byte(`["string1","string2","string3"]`)}, 2122 }, 2123 want: &apiextensionsv1.JSON{Raw: []byte(`["string1 string2 string3"]`)}, // // Not ideal but that's go templating. 2124 }, 2125 { 2126 name: "Should render an array property with range", 2127 template: ` 2128 { 2129 {{ range .variableArray }} 2130 "{{.}}-modified": "value", 2131 {{end}} 2132 } 2133 `, 2134 variables: map[string]apiextensionsv1.JSON{ 2135 "variableArray": {Raw: []byte(`["string1","string2","string3"]`)}, 2136 }, 2137 want: &apiextensionsv1.JSON{Raw: []byte(`{"string1-modified":"value","string2-modified":"value","string3-modified":"value"}`)}, 2138 }, 2139 { 2140 name: "Should render an array property: array element", 2141 template: `{{ index .variableArray 1 }}`, 2142 variables: map[string]apiextensionsv1.JSON{ 2143 "variableArray": {Raw: []byte(`["string1","string2","string3"]`)}, 2144 }, 2145 want: &apiextensionsv1.JSON{Raw: []byte(`"string2"`)}, 2146 }, 2147 { 2148 name: "Should render an array property: array object element field", 2149 template: `{{ (index .variableArray 1).propertyA }}`, 2150 variables: map[string]apiextensionsv1.JSON{ 2151 "variableArray": {Raw: []byte(`[{"propertyA":"A0","propertyB":"B0"},{"propertyA":"A1","propertyB":"B1"}]`)}, 2152 }, 2153 want: &apiextensionsv1.JSON{Raw: []byte(`"A1"`)}, 2154 }, 2155 // Pick up config for a specific MD Class 2156 { 2157 name: "Should render a object property with a lookup based on a builtin variable (class)", 2158 template: `{{ (index .mdConfig .builtin.machineDeployment.class).config }}`, 2159 variables: map[string]apiextensionsv1.JSON{ 2160 "mdConfig": {Raw: []byte(`{ 2161 "mdClass1":{ 2162 "config":"configValue1" 2163 }, 2164 "mdClass2":{ 2165 "config":"configValue2" 2166 } 2167 }`)}, 2168 // Schema must either support complex objects with predefined keys/mdClasses or maps with additionalProperties. 2169 patchvariables.BuiltinsName: {Raw: []byte(`{ 2170 "machineDeployment":{ 2171 "version":"v1.21.1", 2172 "class":"mdClass2", 2173 "name":"md1", 2174 "topologyName":"md-topology", 2175 "replicas":3 2176 }}`)}, 2177 }, 2178 want: &apiextensionsv1.JSON{Raw: []byte(`"configValue2"`)}, 2179 }, 2180 // Pick up config for a specific MP Class 2181 { 2182 name: "Should render a object property with a lookup based on a builtin variable (class)", 2183 template: `{{ (index .mpConfig .builtin.machinePool.class).config }}`, 2184 variables: map[string]apiextensionsv1.JSON{ 2185 "mpConfig": {Raw: []byte(`{ 2186 "mpClass1":{ 2187 "config":"configValue1" 2188 }, 2189 "mpClass2":{ 2190 "config":"configValue2" 2191 } 2192 }`)}, 2193 // Schema must either support complex objects with predefined keys/mdClasses or maps with additionalProperties. 2194 patchvariables.BuiltinsName: {Raw: []byte(`{ 2195 "machinePool":{ 2196 "version":"v1.21.1", 2197 "class":"mpClass2", 2198 "name":"mp1", 2199 "topologyName":"mp-topology", 2200 "replicas":3 2201 }}`)}, 2202 }, 2203 want: &apiextensionsv1.JSON{Raw: []byte(`"configValue2"`)}, 2204 }, 2205 // Pick up config for a specific version 2206 { 2207 name: "Should render a object property with a lookup based on a builtin variable (version)", 2208 template: `{{ (index .mdConfig .builtin.machineDeployment.version).config }}`, 2209 variables: map[string]apiextensionsv1.JSON{ 2210 "mdConfig": {Raw: []byte(`{ 2211 "v1.21.0":{ 2212 "config":"configValue1" 2213 }, 2214 "v1.21.1":{ 2215 "config":"configValue2" 2216 } 2217 }`)}, 2218 // Schema must either support complex objects with predefined keys/mdClasses or maps with additionalProperties. 2219 patchvariables.BuiltinsName: {Raw: []byte(`{"machineDeployment":{"version":"v1.21.1","class":"mdClass2","name":"md1","topologyName":"md-topology","replicas":3}}`)}, 2220 }, 2221 want: &apiextensionsv1.JSON{Raw: []byte(`"configValue2"`)}, 2222 }, 2223 { 2224 name: "Should render a object property with a lookup based on a builtin variable (version)", 2225 template: `{{ (index .mpConfig .builtin.machinePool.version).config }}`, 2226 variables: map[string]apiextensionsv1.JSON{ 2227 "mpConfig": {Raw: []byte(`{ 2228 "v1.21.0":{ 2229 "config":"configValue1" 2230 }, 2231 "v1.21.1":{ 2232 "config":"configValue2" 2233 } 2234 }`)}, 2235 // Schema must either support complex objects with predefined keys/mpClasses or maps with additionalProperties. 2236 patchvariables.BuiltinsName: {Raw: []byte(`{"machinePool":{"version":"v1.21.1","class":"mpClass2","name":"mp1","topologyName":"mp-topology","replicas":3}}`)}, 2237 }, 2238 want: &apiextensionsv1.JSON{Raw: []byte(`"configValue2"`)}, 2239 }, 2240 } 2241 2242 for _, tt := range tests { 2243 t.Run(tt.name, func(t *testing.T) { 2244 g := NewWithT(t) 2245 2246 got, err := renderValueTemplate(tt.template, tt.variables) 2247 if tt.wantErr { 2248 g.Expect(err).To(HaveOccurred()) 2249 return 2250 } 2251 g.Expect(err).ToNot(HaveOccurred()) 2252 2253 // Compact tt.want so we can use easily readable multi-line 2254 // strings in the test definition. 2255 var compactWant bytes.Buffer 2256 g.Expect(json.Compact(&compactWant, tt.want.Raw)).To(Succeed()) 2257 2258 g.Expect(string(got.Raw)).To(Equal(compactWant.String())) 2259 }) 2260 } 2261 } 2262 2263 func TestCalculateTemplateData(t *testing.T) { 2264 tests := []struct { 2265 name string 2266 variables map[string]apiextensionsv1.JSON 2267 want map[string]interface{} 2268 wantErr bool 2269 }{ 2270 { 2271 name: "Fails for invalid JSON value (missing closing quote)", 2272 variables: map[string]apiextensionsv1.JSON{ 2273 "stringVariable": {Raw: []byte(`"cluster-name`)}, 2274 }, 2275 wantErr: true, 2276 }, 2277 { 2278 name: "Fails for invalid JSON value (string without quotes)", 2279 variables: map[string]apiextensionsv1.JSON{ 2280 "stringVariable": {Raw: []byte(`cluster-name`)}, 2281 }, 2282 wantErr: true, 2283 }, 2284 { 2285 name: "Should convert basic types", 2286 variables: map[string]apiextensionsv1.JSON{ 2287 "stringVariable": {Raw: []byte(`"cluster-name"`)}, 2288 "integerVariable": {Raw: []byte("4")}, 2289 "numberVariable": {Raw: []byte("2.5")}, 2290 "booleanVariable": {Raw: []byte("true")}, 2291 }, 2292 want: map[string]interface{}{ 2293 "stringVariable": "cluster-name", 2294 "integerVariable": float64(4), 2295 "numberVariable": float64(2.5), 2296 "booleanVariable": true, 2297 }, 2298 }, 2299 { 2300 name: "Should handle nested variables correctly", 2301 variables: map[string]apiextensionsv1.JSON{ 2302 "builtin": {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.22.0"}},"controlPlane":{"replicas":3},"machineDeployment":{"version":"v1.21.2"},"machinePool":{"version":"v1.21.2"}}`)}, 2303 "userVariable": {Raw: []byte(`"value"`)}, 2304 }, 2305 want: map[string]interface{}{ 2306 "builtin": map[string]interface{}{ 2307 "cluster": map[string]interface{}{ 2308 "name": "cluster-name", 2309 "namespace": "default", 2310 "topology": map[string]interface{}{ 2311 "class": "clusterClass1", 2312 "version": "v1.22.0", 2313 }, 2314 }, 2315 "controlPlane": map[string]interface{}{ 2316 "replicas": float64(3), 2317 }, 2318 "machineDeployment": map[string]interface{}{ 2319 "version": "v1.21.2", 2320 }, 2321 "machinePool": map[string]interface{}{ 2322 "version": "v1.21.2", 2323 }, 2324 }, 2325 "userVariable": "value", 2326 }, 2327 }, 2328 } 2329 2330 for _, tt := range tests { 2331 t.Run(tt.name, func(t *testing.T) { 2332 g := NewWithT(t) 2333 2334 got, err := calculateTemplateData(tt.variables) 2335 if tt.wantErr { 2336 g.Expect(err).To(HaveOccurred()) 2337 return 2338 } 2339 g.Expect(err).ToNot(HaveOccurred()) 2340 2341 g.Expect(got).To(BeComparableTo(tt.want)) 2342 }) 2343 } 2344 } 2345 2346 // toJSONCompact is used to be able to write JSON values in a readable manner. 2347 func toJSONCompact(value string) []byte { 2348 var compactValue bytes.Buffer 2349 if err := json.Compact(&compactValue, []byte(value)); err != nil { 2350 panic(err) 2351 } 2352 return compactValue.Bytes() 2353 }