sigs.k8s.io/cluster-api@v1.7.1/internal/runtime/client/client_test.go (about) 1 /* 2 Copyright 2022 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 client 18 19 import ( 20 "context" 21 "crypto/tls" 22 "encoding/json" 23 "fmt" 24 "net/http" 25 "net/http/httptest" 26 "reflect" 27 "regexp" 28 "testing" 29 30 . "github.com/onsi/gomega" 31 corev1 "k8s.io/api/core/v1" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/labels" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts" 36 "k8s.io/utils/ptr" 37 ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" 38 "sigs.k8s.io/controller-runtime/pkg/client/fake" 39 40 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 41 runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" 42 runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog" 43 runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" 44 runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry" 45 fakev1alpha1 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha1" 46 fakev1alpha2 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha2" 47 ) 48 49 func TestClient_httpCall(t *testing.T) { 50 g := NewWithT(t) 51 52 tableTests := []struct { 53 name string 54 request runtime.Object 55 response runtime.Object 56 opts *httpCallOptions 57 wantErr bool 58 }{ 59 { 60 name: "error if request, response and options are nil", 61 request: nil, 62 response: nil, 63 opts: nil, 64 wantErr: true, 65 }, 66 { 67 name: "error if catalog is not set", 68 request: &fakev1alpha1.FakeRequest{}, 69 response: &fakev1alpha1.FakeResponse{}, 70 opts: &httpCallOptions{ 71 catalog: nil, 72 }, 73 wantErr: true, 74 }, 75 { 76 name: "error if hooks is not registered with catalog", 77 request: &fakev1alpha1.FakeRequest{}, 78 response: &fakev1alpha1.FakeResponse{}, 79 opts: &httpCallOptions{ 80 catalog: runtimecatalog.New(), 81 }, 82 wantErr: true, 83 }, 84 { 85 name: "succeed for valid request and response objects", 86 request: &fakev1alpha1.FakeRequest{ 87 TypeMeta: metav1.TypeMeta{ 88 Kind: "FakeRequest", 89 APIVersion: fakev1alpha1.GroupVersion.Identifier(), 90 }, 91 }, 92 response: &fakev1alpha1.FakeResponse{}, 93 opts: func() *httpCallOptions { 94 c := runtimecatalog.New() 95 g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed()) 96 97 // get same gvh for hook by using the FakeHook and catalog 98 gvh, err := c.GroupVersionHook(fakev1alpha1.FakeHook) 99 g.Expect(err).To(Succeed()) 100 101 return &httpCallOptions{ 102 catalog: c, 103 registrationGVH: gvh, 104 hookGVH: gvh, 105 } 106 }(), 107 wantErr: false, 108 }, 109 { 110 name: "success if request and response are valid objects - with conversion", 111 request: &fakev1alpha2.FakeRequest{ 112 TypeMeta: metav1.TypeMeta{ 113 Kind: "FakeRequest", 114 APIVersion: fakev1alpha2.GroupVersion.Identifier(), 115 }, 116 }, 117 response: &fakev1alpha2.FakeResponse{}, 118 opts: func() *httpCallOptions { 119 c := runtimecatalog.New() 120 // register fakev1alpha1 and fakev1alpha2 to enable conversion 121 g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed()) 122 g.Expect(fakev1alpha2.AddToCatalog(c)).To(Succeed()) 123 124 // get same gvh for hook by using the FakeHook and catalog 125 registrationGVH, err := c.GroupVersionHook(fakev1alpha1.FakeHook) 126 g.Expect(err).To(Succeed()) 127 hookGVH, err := c.GroupVersionHook(fakev1alpha2.FakeHook) 128 g.Expect(err).To(Succeed()) 129 130 return &httpCallOptions{ 131 catalog: c, 132 registrationGVH: registrationGVH, 133 hookGVH: hookGVH, 134 } 135 }(), 136 wantErr: false, 137 }, 138 { 139 name: "succeed if request doesn't define TypeMeta", 140 request: &fakev1alpha2.FakeRequest{}, 141 response: &fakev1alpha2.FakeResponse{}, 142 opts: func() *httpCallOptions { 143 c := runtimecatalog.New() 144 // register fakev1alpha1 and fakev1alpha2 to enable conversion 145 g.Expect(fakev1alpha2.AddToCatalog(c)).To(Succeed()) 146 147 // get same gvh for hook by using the FakeHook and catalog 148 gvh, err := c.GroupVersionHook(fakev1alpha2.FakeHook) 149 g.Expect(err).To(Succeed()) 150 151 return &httpCallOptions{ 152 catalog: c, 153 registrationGVH: gvh, 154 hookGVH: gvh, 155 } 156 }(), 157 wantErr: false, 158 }, 159 { 160 name: "success if request doesn't define TypeMeta - with conversion", 161 request: &fakev1alpha2.FakeRequest{}, 162 response: &fakev1alpha2.FakeResponse{}, 163 opts: func() *httpCallOptions { 164 c := runtimecatalog.New() 165 // register fakev1alpha1 and fakev1alpha2 to enable conversion 166 g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed()) 167 g.Expect(fakev1alpha2.AddToCatalog(c)).To(Succeed()) 168 169 // get same gvh for hook by using the FakeHook and catalog 170 registrationGVH, err := c.GroupVersionHook(fakev1alpha1.FakeHook) 171 g.Expect(err).To(Succeed()) 172 hookGVH, err := c.GroupVersionHook(fakev1alpha2.FakeHook) 173 g.Expect(err).To(Succeed()) 174 175 return &httpCallOptions{ 176 catalog: c, 177 hookGVH: hookGVH, 178 registrationGVH: registrationGVH, 179 } 180 }(), 181 wantErr: false, 182 }, 183 } 184 for _, tt := range tableTests { 185 t.Run(tt.name, func(*testing.T) { 186 // a http server is only required if we have a valid catalog, otherwise httpCall will not reach out to the server 187 if tt.opts != nil && tt.opts.catalog != nil { 188 // create http server with fakeHookHandler 189 mux := http.NewServeMux() 190 mux.HandleFunc("/", fakeHookHandler) 191 192 srv := newUnstartedTLSServer(mux) 193 srv.StartTLS() 194 defer srv.Close() 195 196 // set url to srv for in tt.opts 197 tt.opts.config.URL = ptr.To(srv.URL) 198 tt.opts.config.CABundle = testcerts.CACert 199 } 200 201 err := httpCall(context.TODO(), tt.request, tt.response, tt.opts) 202 if tt.wantErr { 203 g.Expect(err).To(HaveOccurred()) 204 } else { 205 g.Expect(err).ToNot(HaveOccurred()) 206 } 207 }) 208 } 209 } 210 211 func fakeHookHandler(w http.ResponseWriter, _ *http.Request) { 212 response := &fakev1alpha1.FakeResponse{ 213 TypeMeta: metav1.TypeMeta{ 214 Kind: "FakeHookResponse", 215 APIVersion: fakev1alpha1.GroupVersion.Identifier(), 216 }, 217 Second: "", 218 First: 1, 219 } 220 respBody, err := json.Marshal(response) 221 if err != nil { 222 panic(err) 223 } 224 w.WriteHeader(http.StatusOK) 225 _, _ = w.Write(respBody) 226 } 227 228 func TestURLForExtension(t *testing.T) { 229 type args struct { 230 config runtimev1.ClientConfig 231 gvh runtimecatalog.GroupVersionHook 232 extensionHandlerName string 233 } 234 235 type want struct { 236 scheme string 237 host string 238 path string 239 } 240 241 gvh := runtimecatalog.GroupVersionHook{ 242 Group: "test.runtime.cluster.x-k8s.io", 243 Version: "v1alpha1", 244 Hook: "testhook.test-extension", 245 } 246 247 tests := []struct { 248 name string 249 args args 250 want want 251 wantErr bool 252 }{ 253 { 254 name: "ClientConfig using service should have correct URL values", 255 args: args{ 256 config: runtimev1.ClientConfig{ 257 Service: &runtimev1.ServiceReference{ 258 Namespace: "test1", 259 Name: "extension-service", 260 Port: ptr.To[int32](8443), 261 }, 262 }, 263 gvh: gvh, 264 extensionHandlerName: "test-handler", 265 }, 266 want: want{ 267 scheme: "https", 268 host: "extension-service.test1.svc:8443", 269 path: runtimecatalog.GVHToPath(gvh, "test-handler"), 270 }, 271 wantErr: false, 272 }, 273 { 274 name: "ClientConfig using service and CAbundle should have correct URL values", 275 args: args{ 276 config: runtimev1.ClientConfig{ 277 Service: &runtimev1.ServiceReference{ 278 Namespace: "test1", 279 Name: "extension-service", 280 Port: ptr.To[int32](8443), 281 }, 282 CABundle: []byte("some-ca-data"), 283 }, 284 gvh: gvh, 285 extensionHandlerName: "test-handler", 286 }, 287 want: want{ 288 scheme: "https", 289 host: "extension-service.test1.svc:8443", 290 path: runtimecatalog.GVHToPath(gvh, "test-handler"), 291 }, 292 wantErr: false, 293 }, 294 { 295 name: "ClientConfig using URL should have correct URL values", 296 args: args{ 297 config: runtimev1.ClientConfig{ 298 URL: ptr.To("https://extension-host.com"), 299 }, 300 gvh: gvh, 301 extensionHandlerName: "test-handler", 302 }, 303 want: want{ 304 scheme: "https", 305 host: "extension-host.com", 306 path: runtimecatalog.GVHToPath(gvh, "test-handler"), 307 }, 308 wantErr: false, 309 }, 310 { 311 name: "should error if both Service and URL are missing", 312 args: args{ 313 config: runtimev1.ClientConfig{}, 314 gvh: gvh, 315 extensionHandlerName: "test-handler", 316 }, 317 wantErr: true, 318 }, 319 } 320 321 for _, tt := range tests { 322 t.Run(tt.name, func(t *testing.T) { 323 g := NewWithT(t) 324 u, err := urlForExtension(tt.args.config, tt.args.gvh, tt.args.extensionHandlerName) 325 if tt.wantErr { 326 g.Expect(err).To(HaveOccurred()) 327 } else { 328 g.Expect(err).ToNot(HaveOccurred()) 329 g.Expect(u.Scheme).To(Equal(tt.want.scheme)) 330 g.Expect(u.Host).To(Equal(tt.want.host)) 331 g.Expect(u.Path).To(Equal(tt.want.path)) 332 } 333 }) 334 } 335 } 336 337 func Test_defaultAndValidateDiscoveryResponse(t *testing.T) { 338 var invalidFailurePolicy runtimehooksv1.FailurePolicy = "DONT_FAIL" 339 cat := runtimecatalog.New() 340 _ = fakev1alpha1.AddToCatalog(cat) 341 342 tests := []struct { 343 name string 344 discovery *runtimehooksv1.DiscoveryResponse 345 wantErr bool 346 }{ 347 { 348 name: "succeed with valid skeleton DiscoveryResponse", 349 discovery: &runtimehooksv1.DiscoveryResponse{ 350 TypeMeta: metav1.TypeMeta{ 351 Kind: "DiscoveryResponse", 352 APIVersion: runtimehooksv1.GroupVersion.String(), 353 }, 354 Handlers: []runtimehooksv1.ExtensionHandler{{ 355 Name: "extension", 356 RequestHook: runtimehooksv1.GroupVersionHook{ 357 Hook: "FakeHook", 358 APIVersion: fakev1alpha1.GroupVersion.String(), 359 }, 360 }}, 361 }, 362 wantErr: false, 363 }, 364 { 365 name: "error if handler name has capital letters", 366 discovery: &runtimehooksv1.DiscoveryResponse{ 367 TypeMeta: metav1.TypeMeta{ 368 Kind: "DiscoveryResponse", 369 APIVersion: runtimehooksv1.GroupVersion.String(), 370 }, 371 Handlers: []runtimehooksv1.ExtensionHandler{{ 372 Name: "HAS-CAPITAL-LETTERS", 373 RequestHook: runtimehooksv1.GroupVersionHook{ 374 Hook: "FakeHook", 375 APIVersion: fakev1alpha1.GroupVersion.String(), 376 }, 377 }}, 378 }, 379 wantErr: true, 380 }, 381 { 382 name: "error if handler name has full stops", 383 discovery: &runtimehooksv1.DiscoveryResponse{ 384 TypeMeta: metav1.TypeMeta{ 385 Kind: "DiscoveryResponse", 386 APIVersion: runtimehooksv1.GroupVersion.String(), 387 }, 388 Handlers: []runtimehooksv1.ExtensionHandler{{ 389 Name: "has.full.stops", 390 RequestHook: runtimehooksv1.GroupVersionHook{ 391 Hook: "FakeHook", 392 APIVersion: fakev1alpha1.GroupVersion.String(), 393 }, 394 }}, 395 }, 396 wantErr: true, 397 }, 398 { 399 name: "error with Timeout of over 30 seconds", 400 discovery: &runtimehooksv1.DiscoveryResponse{ 401 TypeMeta: metav1.TypeMeta{ 402 Kind: "DiscoveryResponse", 403 APIVersion: runtimehooksv1.GroupVersion.String(), 404 }, 405 Handlers: []runtimehooksv1.ExtensionHandler{{ 406 Name: "ext1", 407 RequestHook: runtimehooksv1.GroupVersionHook{ 408 Hook: "FakeHook", 409 APIVersion: fakev1alpha1.GroupVersion.String(), 410 }, 411 TimeoutSeconds: ptr.To[int32](100), 412 }}, 413 }, 414 wantErr: true, 415 }, 416 { 417 name: "error with Timeout of less than 0", 418 discovery: &runtimehooksv1.DiscoveryResponse{ 419 TypeMeta: metav1.TypeMeta{ 420 Kind: "DiscoveryResponse", 421 APIVersion: runtimehooksv1.GroupVersion.String(), 422 }, 423 Handlers: []runtimehooksv1.ExtensionHandler{{ 424 Name: "ext1", 425 RequestHook: runtimehooksv1.GroupVersionHook{ 426 Hook: "FakeHook", 427 APIVersion: fakev1alpha1.GroupVersion.String(), 428 }, 429 TimeoutSeconds: ptr.To[int32](-1), 430 }}, 431 }, 432 wantErr: true, 433 }, 434 { 435 name: "error with FailurePolicy not Fail or Ignore", 436 discovery: &runtimehooksv1.DiscoveryResponse{ 437 TypeMeta: metav1.TypeMeta{ 438 Kind: "DiscoveryResponse", 439 APIVersion: runtimehooksv1.GroupVersion.String(), 440 }, 441 Handlers: []runtimehooksv1.ExtensionHandler{{ 442 Name: "ext1", 443 RequestHook: runtimehooksv1.GroupVersionHook{ 444 Hook: "FakeHook", 445 APIVersion: fakev1alpha1.GroupVersion.String(), 446 }, 447 TimeoutSeconds: ptr.To[int32](20), 448 FailurePolicy: &invalidFailurePolicy, 449 }}, 450 }, 451 wantErr: true, 452 }, 453 { 454 name: "error when handler name is duplicated", 455 discovery: &runtimehooksv1.DiscoveryResponse{ 456 TypeMeta: metav1.TypeMeta{ 457 Kind: "DiscoveryResponse", 458 APIVersion: runtimehooksv1.GroupVersion.String(), 459 }, 460 Handlers: []runtimehooksv1.ExtensionHandler{ 461 { 462 Name: "ext1", 463 RequestHook: runtimehooksv1.GroupVersionHook{ 464 Hook: "FakeHook", 465 APIVersion: fakev1alpha1.GroupVersion.String(), 466 }, 467 }, 468 { 469 Name: "ext1", 470 RequestHook: runtimehooksv1.GroupVersionHook{ 471 Hook: "FakeHook", 472 APIVersion: fakev1alpha1.GroupVersion.String(), 473 }, 474 }, 475 { 476 Name: "ext2", 477 RequestHook: runtimehooksv1.GroupVersionHook{ 478 Hook: "FakeHook", 479 APIVersion: fakev1alpha1.GroupVersion.String(), 480 }, 481 }, 482 }, 483 }, 484 wantErr: true, 485 }, 486 { 487 name: "error if handler GroupVersionHook is not registered", 488 discovery: &runtimehooksv1.DiscoveryResponse{ 489 TypeMeta: metav1.TypeMeta{ 490 Kind: "DiscoveryResponse", 491 APIVersion: runtimehooksv1.GroupVersion.String(), 492 }, 493 Handlers: []runtimehooksv1.ExtensionHandler{{ 494 Name: "ext1", 495 RequestHook: runtimehooksv1.GroupVersionHook{ 496 Hook: "FakeHook", 497 // Version v1alpha2 is not registered with the catalog 498 APIVersion: fakev1alpha2.GroupVersion.String(), 499 }, 500 }}, 501 }, 502 wantErr: true, 503 }, 504 { 505 name: "error if handler GroupVersion can not be parsed", 506 discovery: &runtimehooksv1.DiscoveryResponse{ 507 TypeMeta: metav1.TypeMeta{ 508 Kind: "DiscoveryResponse", 509 APIVersion: runtimehooksv1.GroupVersion.String(), 510 }, 511 Handlers: []runtimehooksv1.ExtensionHandler{{ 512 Name: "ext1", 513 RequestHook: runtimehooksv1.GroupVersionHook{ 514 Hook: "FakeHook", 515 // Version v1alpha2 is not registered with the catalog 516 APIVersion: "too/many/slashes", 517 }, 518 }}, 519 }, 520 wantErr: true, 521 }, 522 } 523 for _, tt := range tests { 524 t.Run(tt.name, func(t *testing.T) { 525 if err := defaultAndValidateDiscoveryResponse(cat, tt.discovery); (err != nil) != tt.wantErr { 526 t.Errorf("defaultAndValidateDiscoveryResponse() error = %v, wantErr %v", err, tt.wantErr) 527 } 528 }) 529 } 530 } 531 532 func TestClient_CallExtension(t *testing.T) { 533 ns := &corev1.Namespace{ 534 TypeMeta: metav1.TypeMeta{ 535 Kind: "Namespace", 536 APIVersion: corev1.SchemeGroupVersion.String(), 537 }, 538 ObjectMeta: metav1.ObjectMeta{ 539 Name: "foo", 540 }, 541 } 542 fpFail := runtimev1.FailurePolicyFail 543 fpIgnore := runtimev1.FailurePolicyIgnore 544 545 validExtensionHandlerWithFailPolicy := runtimev1.ExtensionConfig{ 546 Spec: runtimev1.ExtensionConfigSpec{ 547 ClientConfig: runtimev1.ClientConfig{ 548 // Set a fake URL, in test cases where we start the test server the URL will be overridden. 549 URL: ptr.To("https://127.0.0.1/"), 550 CABundle: testcerts.CACert, 551 }, 552 NamespaceSelector: &metav1.LabelSelector{}, 553 }, 554 Status: runtimev1.ExtensionConfigStatus{ 555 Handlers: []runtimev1.ExtensionHandler{ 556 { 557 Name: "valid-extension", 558 RequestHook: runtimev1.GroupVersionHook{ 559 APIVersion: fakev1alpha1.GroupVersion.String(), 560 Hook: "FakeHook", 561 }, 562 TimeoutSeconds: ptr.To[int32](1), 563 FailurePolicy: &fpFail, 564 }, 565 }, 566 }, 567 } 568 validExtensionHandlerWithIgnorePolicy := runtimev1.ExtensionConfig{ 569 Spec: runtimev1.ExtensionConfigSpec{ 570 ClientConfig: runtimev1.ClientConfig{ 571 // Set a fake URL, in test cases where we start the test server the URL will be overridden. 572 URL: ptr.To("https://127.0.0.1/"), 573 CABundle: testcerts.CACert, 574 }, 575 NamespaceSelector: &metav1.LabelSelector{}}, 576 Status: runtimev1.ExtensionConfigStatus{ 577 Handlers: []runtimev1.ExtensionHandler{ 578 { 579 Name: "valid-extension", 580 RequestHook: runtimev1.GroupVersionHook{ 581 APIVersion: fakev1alpha1.GroupVersion.String(), 582 Hook: "FakeHook", 583 }, 584 TimeoutSeconds: ptr.To[int32](1), 585 FailurePolicy: &fpIgnore, 586 }, 587 }, 588 }, 589 } 590 type args struct { 591 hook runtimecatalog.Hook 592 name string 593 request runtimehooksv1.RequestObject 594 response runtimehooksv1.ResponseObject 595 } 596 tests := []struct { 597 name string 598 registeredExtensionConfigs []runtimev1.ExtensionConfig 599 args args 600 testServer testServerConfig 601 wantErr bool 602 }{ 603 { 604 name: "should fail when hook and request/response are not compatible", 605 registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy}, 606 testServer: testServerConfig{ 607 start: false, 608 }, 609 args: args{ 610 hook: fakev1alpha1.FakeHook, 611 name: "valid-extension", 612 request: &fakev1alpha1.SecondFakeRequest{}, 613 response: &fakev1alpha1.SecondFakeResponse{}, 614 }, 615 wantErr: true, 616 }, 617 { 618 name: "should fail when hook GVH does not match the registered ExtensionHandler", 619 registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy}, 620 testServer: testServerConfig{ 621 start: false, 622 }, 623 args: args{ 624 hook: fakev1alpha1.SecondFakeHook, 625 name: "valid-extension", 626 request: &fakev1alpha1.SecondFakeRequest{}, 627 response: &fakev1alpha1.SecondFakeResponse{}, 628 }, 629 wantErr: true, 630 }, 631 { 632 name: "should fail if ExtensionHandler is not registered", 633 registeredExtensionConfigs: nil, 634 testServer: testServerConfig{ 635 start: true, 636 responses: map[string]testServerResponse{ 637 "/*": response(runtimehooksv1.ResponseStatusSuccess), 638 }, 639 }, 640 args: args{ 641 hook: fakev1alpha1.FakeHook, 642 name: "unregistered-extension", 643 request: &fakev1alpha1.FakeRequest{}, 644 response: &fakev1alpha1.FakeResponse{}, 645 }, 646 wantErr: true, 647 }, 648 { 649 name: "should succeed when calling ExtensionHandler with success response and FailurePolicyFail", 650 registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy}, 651 testServer: testServerConfig{ 652 start: true, 653 responses: map[string]testServerResponse{ 654 "/*": response(runtimehooksv1.ResponseStatusSuccess), 655 }, 656 }, 657 args: args{ 658 hook: fakev1alpha1.FakeHook, 659 name: "valid-extension", 660 request: &fakev1alpha1.FakeRequest{}, 661 response: &fakev1alpha1.FakeResponse{}, 662 }, 663 wantErr: false, 664 }, 665 { 666 name: "should succeed when calling ExtensionHandler with success response and FailurePolicyIgnore", 667 registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithIgnorePolicy}, 668 testServer: testServerConfig{ 669 start: true, 670 responses: map[string]testServerResponse{ 671 "/*": response(runtimehooksv1.ResponseStatusSuccess), 672 }, 673 }, 674 args: args{ 675 hook: fakev1alpha1.FakeHook, 676 name: "valid-extension", 677 request: &fakev1alpha1.FakeRequest{}, 678 response: &fakev1alpha1.FakeResponse{}, 679 }, 680 wantErr: false, 681 }, 682 { 683 name: "should fail when calling ExtensionHandler with failure response and FailurePolicyFail", 684 registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy}, 685 testServer: testServerConfig{ 686 start: true, 687 responses: map[string]testServerResponse{ 688 "/*": response(runtimehooksv1.ResponseStatusFailure), 689 }, 690 }, 691 args: args{ 692 hook: fakev1alpha1.FakeHook, 693 name: "valid-extension", 694 request: &fakev1alpha1.FakeRequest{}, 695 response: &fakev1alpha1.FakeResponse{}, 696 }, 697 wantErr: true, 698 }, 699 { 700 name: "should fail when calling ExtensionHandler with failure response and FailurePolicyIgnore", 701 registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithIgnorePolicy}, 702 testServer: testServerConfig{ 703 start: true, 704 responses: map[string]testServerResponse{ 705 "/*": response(runtimehooksv1.ResponseStatusFailure), 706 }, 707 }, 708 args: args{ 709 hook: fakev1alpha1.FakeHook, 710 name: "valid-extension", 711 request: &fakev1alpha1.FakeRequest{}, 712 response: &fakev1alpha1.FakeResponse{}, 713 }, 714 wantErr: true, 715 }, 716 717 { 718 name: "should succeed with unreachable extension and FailurePolicyIgnore", 719 registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithIgnorePolicy}, 720 testServer: testServerConfig{ 721 start: false, 722 }, 723 args: args{ 724 hook: fakev1alpha1.FakeHook, 725 name: "valid-extension", 726 request: &fakev1alpha1.FakeRequest{}, 727 response: &fakev1alpha1.FakeResponse{}, 728 }, 729 wantErr: false, 730 }, 731 { 732 name: "should fail with unreachable extension and FailurePolicyFail", 733 registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy}, 734 testServer: testServerConfig{ 735 start: false, 736 }, 737 args: args{ 738 hook: fakev1alpha1.FakeHook, 739 name: "valid-extension", 740 request: &fakev1alpha1.FakeRequest{}, 741 response: &fakev1alpha1.FakeResponse{}, 742 }, 743 wantErr: true, 744 }, 745 } 746 747 for _, tt := range tests { 748 t.Run(tt.name, func(t *testing.T) { 749 g := NewWithT(t) 750 751 if tt.testServer.start { 752 srv := createSecureTestServer(tt.testServer) 753 srv.StartTLS() 754 defer srv.Close() 755 756 // Set the URL to the real address of the test server. 757 for i := range tt.registeredExtensionConfigs { 758 tt.registeredExtensionConfigs[i].Spec.ClientConfig.URL = ptr.To(fmt.Sprintf("https://%s/", srv.Listener.Addr().String())) 759 } 760 } 761 762 cat := runtimecatalog.New() 763 _ = fakev1alpha1.AddToCatalog(cat) 764 _ = fakev1alpha2.AddToCatalog(cat) 765 fakeClient := fake.NewClientBuilder(). 766 WithObjects(ns). 767 Build() 768 769 c := New(Options{ 770 Catalog: cat, 771 Registry: registry(tt.registeredExtensionConfigs), 772 Client: fakeClient, 773 }) 774 775 obj := &clusterv1.Cluster{ 776 ObjectMeta: metav1.ObjectMeta{ 777 Name: "cluster", 778 Namespace: "foo", 779 }, 780 } 781 err := c.CallExtension(context.Background(), tt.args.hook, obj, tt.args.name, tt.args.request, tt.args.response) 782 783 if tt.wantErr { 784 g.Expect(err).To(HaveOccurred()) 785 } else { 786 g.Expect(err).ToNot(HaveOccurred()) 787 } 788 }) 789 } 790 } 791 792 func TestPrepareRequest(t *testing.T) { 793 t.Run("request should have the correct settings", func(t *testing.T) { 794 tests := []struct { 795 name string 796 request runtimehooksv1.RequestObject 797 registration *runtimeregistry.ExtensionRegistration 798 want map[string]string 799 }{ 800 { 801 name: "settings in request should be preserved as is if there are not setting in the registration", 802 request: &runtimehooksv1.BeforeClusterCreateRequest{ 803 CommonRequest: runtimehooksv1.CommonRequest{ 804 Settings: map[string]string{ 805 "key1": "value1", 806 }, 807 }, 808 }, 809 registration: &runtimeregistry.ExtensionRegistration{}, 810 want: map[string]string{ 811 "key1": "value1", 812 }, 813 }, 814 { 815 name: "settings in registration should be used as is if there are no settings in the request", 816 request: &runtimehooksv1.BeforeClusterCreateRequest{ 817 CommonRequest: runtimehooksv1.CommonRequest{}, 818 }, 819 registration: &runtimeregistry.ExtensionRegistration{ 820 Settings: map[string]string{ 821 "key1": "value1", 822 }, 823 }, 824 want: map[string]string{ 825 "key1": "value1", 826 }, 827 }, 828 { 829 name: "settings in request and registry should be merged with request taking precedence", 830 request: &runtimehooksv1.BeforeClusterCreateRequest{ 831 CommonRequest: runtimehooksv1.CommonRequest{ 832 Settings: map[string]string{ 833 "key1": "value1", 834 "key2": "value2", 835 }, 836 }, 837 }, 838 registration: &runtimeregistry.ExtensionRegistration{ 839 Settings: map[string]string{ 840 "key1": "value11", 841 "key3": "value3", 842 }, 843 }, 844 want: map[string]string{ 845 "key1": "value1", 846 "key2": "value2", 847 "key3": "value3", 848 }, 849 }, 850 } 851 852 for _, tt := range tests { 853 t.Run(tt.name, func(t *testing.T) { 854 g := NewWithT(t) 855 var originalRegistrationSettings map[string]string 856 if tt.registration.Settings != nil { 857 originalRegistrationSettings = map[string]string{} 858 for k, v := range tt.registration.Settings { 859 originalRegistrationSettings[k] = v 860 } 861 } 862 863 g.Expect(cloneAndAddSettings(tt.request, tt.registration.Settings).GetSettings()).To(Equal(tt.want)) 864 // Make sure that the original settings in the registration are not modified. 865 g.Expect(tt.registration.Settings).To(Equal(originalRegistrationSettings)) 866 }) 867 } 868 }) 869 } 870 871 func TestClient_CallAllExtensions(t *testing.T) { 872 ns := &corev1.Namespace{ 873 TypeMeta: metav1.TypeMeta{ 874 Kind: "Namespace", 875 APIVersion: corev1.SchemeGroupVersion.String(), 876 }, 877 ObjectMeta: metav1.ObjectMeta{ 878 Name: "foo", 879 }, 880 } 881 fpFail := runtimev1.FailurePolicyFail 882 883 extensionConfig := runtimev1.ExtensionConfig{ 884 Spec: runtimev1.ExtensionConfigSpec{ 885 ClientConfig: runtimev1.ClientConfig{ 886 // Set a fake URL, in test cases where we start the test server the URL will be overridden. 887 URL: ptr.To("https://127.0.0.1/"), 888 CABundle: testcerts.CACert, 889 }, 890 NamespaceSelector: &metav1.LabelSelector{}, 891 }, 892 Status: runtimev1.ExtensionConfigStatus{ 893 Handlers: []runtimev1.ExtensionHandler{ 894 { 895 Name: "first-extension", 896 RequestHook: runtimev1.GroupVersionHook{ 897 APIVersion: fakev1alpha1.GroupVersion.String(), 898 Hook: "FakeHook", 899 }, 900 TimeoutSeconds: ptr.To[int32](1), 901 FailurePolicy: &fpFail, 902 }, 903 { 904 Name: "second-extension", 905 RequestHook: runtimev1.GroupVersionHook{ 906 APIVersion: fakev1alpha1.GroupVersion.String(), 907 Hook: "FakeHook", 908 }, 909 TimeoutSeconds: ptr.To[int32](1), 910 FailurePolicy: &fpFail, 911 }, 912 { 913 Name: "third-extension", 914 RequestHook: runtimev1.GroupVersionHook{ 915 APIVersion: fakev1alpha1.GroupVersion.String(), 916 Hook: "FakeHook", 917 }, 918 TimeoutSeconds: ptr.To[int32](1), 919 FailurePolicy: &fpFail, 920 }, 921 }, 922 }, 923 } 924 925 type args struct { 926 hook runtimecatalog.Hook 927 request runtimehooksv1.RequestObject 928 response runtimehooksv1.ResponseObject 929 } 930 tests := []struct { 931 name string 932 registeredExtensionConfigs []runtimev1.ExtensionConfig 933 args args 934 testServer testServerConfig 935 wantErr bool 936 }{ 937 { 938 name: "should fail when hook and request/response are not compatible", 939 registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig}, 940 testServer: testServerConfig{ 941 start: false, 942 }, 943 args: args{ 944 hook: fakev1alpha1.SecondFakeHook, 945 request: &fakev1alpha1.FakeRequest{}, 946 response: &fakev1alpha1.FakeResponse{}, 947 }, 948 wantErr: true, 949 }, 950 { 951 name: "should succeed when no ExtensionHandlers are registered for the hook", 952 registeredExtensionConfigs: []runtimev1.ExtensionConfig{}, 953 testServer: testServerConfig{ 954 start: false, 955 }, 956 args: args{ 957 hook: fakev1alpha1.FakeHook, 958 request: &fakev1alpha1.FakeRequest{}, 959 response: &fakev1alpha1.FakeResponse{}, 960 }, 961 wantErr: false, 962 }, 963 { 964 name: "should succeed when calling ExtensionHandlers with success responses", 965 registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig}, 966 testServer: testServerConfig{ 967 start: true, 968 responses: map[string]testServerResponse{ 969 "/*": response(runtimehooksv1.ResponseStatusSuccess), 970 }, 971 }, 972 args: args{ 973 hook: fakev1alpha1.FakeHook, 974 request: &fakev1alpha1.FakeRequest{}, 975 response: &fakev1alpha1.FakeResponse{}, 976 }, 977 wantErr: false, 978 }, 979 { 980 name: "should fail when calling ExtensionHandlers with failure responses", 981 registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig}, 982 testServer: testServerConfig{ 983 start: true, 984 responses: map[string]testServerResponse{ 985 "/*": response(runtimehooksv1.ResponseStatusFailure), 986 }, 987 }, 988 args: args{ 989 hook: fakev1alpha1.FakeHook, 990 request: &fakev1alpha1.FakeRequest{}, 991 response: &fakev1alpha1.FakeResponse{}, 992 }, 993 wantErr: true, 994 }, 995 { 996 name: "should fail when one of the ExtensionHandlers returns a failure responses", 997 registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig}, 998 testServer: testServerConfig{ 999 start: true, 1000 responses: map[string]testServerResponse{ 1001 "/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/first-extension.*": response(runtimehooksv1.ResponseStatusSuccess), 1002 "/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/second-extension.*": response(runtimehooksv1.ResponseStatusFailure), 1003 "/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/third-extension.*": response(runtimehooksv1.ResponseStatusSuccess), 1004 }, 1005 }, 1006 args: args{ 1007 hook: fakev1alpha1.FakeHook, 1008 request: &fakev1alpha1.FakeRequest{}, 1009 response: &fakev1alpha1.FakeResponse{}, 1010 }, 1011 wantErr: true, 1012 }, 1013 { 1014 name: "should fail when one of the ExtensionHandlers returns 404", 1015 registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig}, 1016 testServer: testServerConfig{ 1017 start: true, 1018 responses: map[string]testServerResponse{ 1019 "/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/first-extension.*": response(runtimehooksv1.ResponseStatusSuccess), 1020 "/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/second-extension.*": response(runtimehooksv1.ResponseStatusFailure), 1021 // third-extension has no handler. 1022 }, 1023 }, 1024 args: args{ 1025 hook: fakev1alpha1.FakeHook, 1026 request: &fakev1alpha1.FakeRequest{}, 1027 response: &fakev1alpha1.FakeResponse{}, 1028 }, 1029 wantErr: true, 1030 }, 1031 } 1032 for _, tt := range tests { 1033 t.Run(tt.name, func(t *testing.T) { 1034 g := NewWithT(t) 1035 1036 if tt.testServer.start { 1037 srv := createSecureTestServer(tt.testServer) 1038 srv.StartTLS() 1039 defer srv.Close() 1040 1041 // Set the URL to the real address of the test server. 1042 for i := range tt.registeredExtensionConfigs { 1043 tt.registeredExtensionConfigs[i].Spec.ClientConfig.URL = ptr.To(fmt.Sprintf("https://%s/", srv.Listener.Addr().String())) 1044 } 1045 } 1046 1047 cat := runtimecatalog.New() 1048 _ = fakev1alpha1.AddToCatalog(cat) 1049 _ = fakev1alpha2.AddToCatalog(cat) 1050 fakeClient := fake.NewClientBuilder(). 1051 WithObjects(ns). 1052 Build() 1053 c := New(Options{ 1054 Catalog: cat, 1055 Registry: registry(tt.registeredExtensionConfigs), 1056 Client: fakeClient, 1057 }) 1058 1059 obj := &clusterv1.Cluster{ 1060 ObjectMeta: metav1.ObjectMeta{ 1061 Name: "cluster", 1062 Namespace: "foo", 1063 }, 1064 } 1065 err := c.CallAllExtensions(context.Background(), tt.args.hook, obj, tt.args.request, tt.args.response) 1066 1067 if tt.wantErr { 1068 g.Expect(err).To(HaveOccurred()) 1069 } else { 1070 g.Expect(err).ToNot(HaveOccurred()) 1071 } 1072 }) 1073 } 1074 } 1075 1076 func Test_client_matchNamespace(t *testing.T) { 1077 g := NewWithT(t) 1078 foo := &corev1.Namespace{ 1079 TypeMeta: metav1.TypeMeta{ 1080 Kind: "Namespace", 1081 APIVersion: corev1.SchemeGroupVersion.String(), 1082 }, 1083 ObjectMeta: metav1.ObjectMeta{ 1084 Name: "foo", 1085 Labels: map[string]string{ 1086 corev1.LabelMetadataName: "foo", 1087 }, 1088 }, 1089 } 1090 bar := &corev1.Namespace{ 1091 TypeMeta: metav1.TypeMeta{ 1092 Kind: "Namespace", 1093 APIVersion: corev1.SchemeGroupVersion.String(), 1094 }, 1095 ObjectMeta: metav1.ObjectMeta{ 1096 Name: "bar", 1097 Labels: map[string]string{ 1098 corev1.LabelMetadataName: "bar", 1099 }, 1100 }, 1101 } 1102 matchingMatchExpressions, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{ 1103 MatchExpressions: []metav1.LabelSelectorRequirement{ 1104 { 1105 Key: corev1.LabelMetadataName, 1106 Operator: metav1.LabelSelectorOpIn, 1107 Values: []string{"foo", "bar"}, 1108 }, 1109 }, 1110 }) 1111 g.Expect(err).ToNot(HaveOccurred()) 1112 notMatchingMatchExpressions, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{ 1113 MatchExpressions: []metav1.LabelSelectorRequirement{ 1114 { 1115 Key: corev1.LabelMetadataName, 1116 Operator: metav1.LabelSelectorOpIn, 1117 Values: []string{"non-existing", "other"}, 1118 }, 1119 }, 1120 }) 1121 g.Expect(err).ToNot(HaveOccurred()) 1122 tests := []struct { 1123 name string 1124 selector labels.Selector 1125 namespace string 1126 existingNamespaces []ctrlclient.Object 1127 want bool 1128 wantErr bool 1129 }{ 1130 { 1131 name: "match with single label selector", 1132 selector: labels.SelectorFromSet(labels.Set{corev1.LabelMetadataName: foo.Name}), 1133 namespace: "foo", 1134 existingNamespaces: []ctrlclient.Object{foo, bar}, 1135 want: true, 1136 wantErr: false, 1137 }, 1138 { 1139 name: "error with non-existent namespace", 1140 selector: labels.SelectorFromSet(labels.Set{corev1.LabelMetadataName: foo.Name}), 1141 namespace: "non-existent", 1142 existingNamespaces: []ctrlclient.Object{foo, bar}, 1143 want: false, 1144 wantErr: true, 1145 }, 1146 { 1147 name: "doesn't match if namespaceSelector doesn't match namespace", 1148 selector: labels.SelectorFromSet(labels.Set{corev1.LabelMetadataName: bar.Name}), 1149 namespace: "foo", 1150 existingNamespaces: []ctrlclient.Object{foo, bar}, 1151 want: false, 1152 wantErr: false, 1153 }, 1154 { 1155 name: "match if match expressions match namespace", 1156 selector: matchingMatchExpressions, 1157 namespace: "bar", 1158 existingNamespaces: []ctrlclient.Object{foo, bar}, 1159 want: true, 1160 wantErr: false, 1161 }, 1162 { 1163 name: "doesn't match if match expressions doesn't match namespace", 1164 selector: notMatchingMatchExpressions, 1165 namespace: "foo", 1166 existingNamespaces: []ctrlclient.Object{foo, bar}, 1167 want: false, 1168 wantErr: false, 1169 }, 1170 } 1171 for _, tt := range tests { 1172 t.Run(tt.name, func(t *testing.T) { 1173 c := client{ 1174 client: fake.NewClientBuilder(). 1175 WithObjects(tt.existingNamespaces...). 1176 Build(), 1177 } 1178 got, err := c.matchNamespace(context.Background(), tt.selector, tt.namespace) 1179 if (err != nil) != tt.wantErr { 1180 t.Errorf("matchNamespace() error = %v, wantErr %v", err, tt.wantErr) 1181 return 1182 } 1183 if got != tt.want { 1184 t.Errorf("matchNamespace() got = %v, want %v", got, tt.want) 1185 } 1186 }) 1187 } 1188 } 1189 1190 func Test_aggregateResponses(t *testing.T) { 1191 tests := []struct { 1192 name string 1193 aggregateResponse runtimehooksv1.ResponseObject 1194 responses []runtimehooksv1.ResponseObject 1195 want runtimehooksv1.ResponseObject 1196 }{ 1197 { 1198 name: "Aggregate response if there is only one response", 1199 aggregateResponse: fakeSuccessResponse(""), 1200 responses: []runtimehooksv1.ResponseObject{ 1201 fakeSuccessResponse("test"), 1202 }, 1203 want: fakeSuccessResponse("test"), 1204 }, 1205 { 1206 name: "Aggregate retry response if there is only one response", 1207 aggregateResponse: fakeRetryableSuccessResponse(0, ""), 1208 responses: []runtimehooksv1.ResponseObject{ 1209 fakeRetryableSuccessResponse(5, "test"), 1210 }, 1211 want: fakeRetryableSuccessResponse(5, "test"), 1212 }, 1213 { 1214 name: "Aggregate retry responses to lowest non-zero retryAfterSeconds value", 1215 aggregateResponse: fakeRetryableSuccessResponse(0, ""), 1216 responses: []runtimehooksv1.ResponseObject{ 1217 fakeRetryableSuccessResponse(0, "test1"), 1218 fakeRetryableSuccessResponse(1, "test2"), 1219 fakeRetryableSuccessResponse(5, ""), 1220 fakeRetryableSuccessResponse(4, ""), 1221 fakeRetryableSuccessResponse(3, ""), 1222 }, 1223 want: fakeRetryableSuccessResponse(1, "test1, test2"), 1224 }, 1225 } 1226 for _, tt := range tests { 1227 t.Run(tt.name, func(t *testing.T) { 1228 aggregateSuccessfulResponses(tt.aggregateResponse, tt.responses) 1229 1230 if !reflect.DeepEqual(tt.aggregateResponse, tt.want) { 1231 t.Errorf("aggregateSuccessfulResponses() got = %v, want %v", tt.aggregateResponse, tt.want) 1232 } 1233 }) 1234 } 1235 } 1236 1237 type testServerConfig struct { 1238 start bool 1239 responses map[string]testServerResponse 1240 } 1241 1242 type testServerResponse struct { 1243 response runtime.Object 1244 responseStatusCode int 1245 } 1246 1247 func response(status runtimehooksv1.ResponseStatus) testServerResponse { 1248 return testServerResponse{ 1249 response: &fakev1alpha1.FakeResponse{ 1250 CommonResponse: runtimehooksv1.CommonResponse{ 1251 Status: status, 1252 }, 1253 }, 1254 responseStatusCode: http.StatusOK, 1255 } 1256 } 1257 1258 func createSecureTestServer(server testServerConfig) *httptest.Server { 1259 mux := http.NewServeMux() 1260 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 1261 // Write the response for the first match in tt.testServer.responses. 1262 for pathRegex, resp := range server.responses { 1263 if !regexp.MustCompile(pathRegex).MatchString(r.URL.Path) { 1264 continue 1265 } 1266 1267 respBody, err := json.Marshal(resp.response) 1268 if err != nil { 1269 panic(err) 1270 } 1271 w.WriteHeader(resp.responseStatusCode) 1272 _, _ = w.Write(respBody) 1273 return 1274 } 1275 1276 // Otherwise write a 404. 1277 w.WriteHeader(http.StatusNotFound) 1278 }) 1279 1280 srv := newUnstartedTLSServer(mux) 1281 1282 return srv 1283 } 1284 1285 func registry(configs []runtimev1.ExtensionConfig) runtimeregistry.ExtensionRegistry { 1286 registry := runtimeregistry.New() 1287 err := registry.WarmUp(&runtimev1.ExtensionConfigList{ 1288 Items: configs, 1289 }) 1290 if err != nil { 1291 panic(err) 1292 } 1293 return registry 1294 } 1295 1296 func fakeSuccessResponse(message string) *fakev1alpha1.FakeResponse { 1297 return &fakev1alpha1.FakeResponse{ 1298 TypeMeta: metav1.TypeMeta{ 1299 Kind: "FakeResponse", 1300 APIVersion: "v1alpha1", 1301 }, 1302 CommonResponse: runtimehooksv1.CommonResponse{ 1303 Message: message, 1304 Status: runtimehooksv1.ResponseStatusSuccess, 1305 }, 1306 } 1307 } 1308 1309 func fakeRetryableSuccessResponse(retryAfterSeconds int32, message string) *fakev1alpha1.RetryableFakeResponse { 1310 return &fakev1alpha1.RetryableFakeResponse{ 1311 TypeMeta: metav1.TypeMeta{ 1312 Kind: "FakeResponse", 1313 APIVersion: "v1alpha1", 1314 }, 1315 CommonResponse: runtimehooksv1.CommonResponse{ 1316 Message: message, 1317 Status: runtimehooksv1.ResponseStatusSuccess, 1318 }, 1319 CommonRetryResponse: runtimehooksv1.CommonRetryResponse{ 1320 RetryAfterSeconds: retryAfterSeconds, 1321 }, 1322 } 1323 } 1324 1325 func newUnstartedTLSServer(handler http.Handler) *httptest.Server { 1326 cert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey) 1327 if err != nil { 1328 panic(err) 1329 } 1330 srv := httptest.NewUnstartedServer(handler) 1331 srv.TLS = &tls.Config{ 1332 MinVersion: tls.VersionTLS13, 1333 Certificates: []tls.Certificate{cert}, 1334 } 1335 return srv 1336 } 1337 1338 func TestNameForHandler(t *testing.T) { 1339 tests := []struct { 1340 name string 1341 handler runtimehooksv1.ExtensionHandler 1342 extensionConfig *runtimev1.ExtensionConfig 1343 want string 1344 wantErr bool 1345 }{ 1346 { 1347 name: "return well formatted name", 1348 handler: runtimehooksv1.ExtensionHandler{Name: "discover-variables"}, 1349 extensionConfig: &runtimev1.ExtensionConfig{ObjectMeta: metav1.ObjectMeta{Name: "runtime1"}}, 1350 want: "discover-variables.runtime1", 1351 }, 1352 { 1353 name: "return well formatted name", 1354 handler: runtimehooksv1.ExtensionHandler{Name: "discover-variables"}, 1355 extensionConfig: nil, 1356 wantErr: true, 1357 }, 1358 } 1359 for _, tt := range tests { 1360 t.Run(tt.name, func(t *testing.T) { 1361 got, err := NameForHandler(tt.handler, tt.extensionConfig) 1362 if (err != nil) != tt.wantErr { 1363 t.Errorf("NameForHandler() error = %v, wantErr %v", err, tt.wantErr) 1364 return 1365 } 1366 if got != tt.want { 1367 t.Errorf("NameForHandler() got = %v, want %v", got, tt.want) 1368 } 1369 }) 1370 } 1371 } 1372 1373 func TestExtensionNameFromHandlerName(t *testing.T) { 1374 tests := []struct { 1375 name string 1376 registeredHandlerName string 1377 want string 1378 wantErr bool 1379 }{ 1380 { 1381 name: "Get name from correctly formatted handler name", 1382 registeredHandlerName: "discover-variables.runtime1", 1383 want: "runtime1", 1384 }, 1385 { 1386 name: "error from incorrectly formatted handler name", 1387 // Two periods make this name badly formed. 1388 registeredHandlerName: "discover-variables.runtime.1", 1389 wantErr: true, 1390 }, 1391 } 1392 for _, tt := range tests { 1393 t.Run(tt.name, func(t *testing.T) { 1394 got, err := ExtensionNameFromHandlerName(tt.registeredHandlerName) 1395 if (err != nil) != tt.wantErr { 1396 t.Errorf("ExtensionNameFromHandlerName() error = %v, wantErr %v", err, tt.wantErr) 1397 return 1398 } 1399 if got != tt.want { 1400 t.Errorf("ExtensionNameFromHandlerName() got = %v, want %v", got, tt.want) 1401 } 1402 }) 1403 } 1404 }