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