sigs.k8s.io/cluster-api/bootstrap/kubeadm@v0.0.0-20191016155141-23a891785b60/controllers/kubeadmconfig_controller_test.go (about) 1 /* 2 Copyright 2019 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 controllers 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "reflect" 24 "testing" 25 "time" 26 27 "github.com/pkg/errors" 28 corev1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/types" 32 fakeclient "k8s.io/client-go/kubernetes/fake" 33 typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" 34 bootstrapapi "k8s.io/cluster-bootstrap/token/api" 35 "k8s.io/klog/klogr" 36 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha2" 37 internalcluster "sigs.k8s.io/cluster-api/bootstrap/kubeadm/internal/cluster" 38 kubeadmv1beta1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/kubeadm/v1beta1" 39 clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha2" 40 "sigs.k8s.io/cluster-api/util/secret" 41 ctrl "sigs.k8s.io/controller-runtime" 42 "sigs.k8s.io/controller-runtime/pkg/client" 43 "sigs.k8s.io/controller-runtime/pkg/client/fake" 44 "sigs.k8s.io/controller-runtime/pkg/handler" 45 "sigs.k8s.io/controller-runtime/pkg/runtime/log" 46 ) 47 48 func setupScheme() *runtime.Scheme { 49 scheme := runtime.NewScheme() 50 if err := clusterv1.AddToScheme(scheme); err != nil { 51 panic(err) 52 } 53 if err := bootstrapv1.AddToScheme(scheme); err != nil { 54 panic(err) 55 } 56 if err := corev1.AddToScheme(scheme); err != nil { 57 panic(err) 58 } 59 return scheme 60 } 61 62 // MachineToBootstrapMapFunc return kubeadm bootstrap configref name when configref exists 63 func TestKubeadmConfigReconciler_MachineToBootstrapMapFuncReturn(t *testing.T) { 64 cluster := newCluster("my-cluster") 65 objs := []runtime.Object{cluster} 66 machineObjs := []runtime.Object{} 67 var expectedConfigName string 68 for i := 0; i < 3; i++ { 69 m := newMachine(cluster, fmt.Sprintf("my-machine-%d", i)) 70 configName := fmt.Sprintf("my-config-%d", i) 71 if i == 1 { 72 c := newKubeadmConfig(m, configName) 73 objs = append(objs, m, c) 74 expectedConfigName = configName 75 } else { 76 objs = append(objs, m) 77 } 78 machineObjs = append(machineObjs, m) 79 } 80 fakeClient := fake.NewFakeClientWithScheme(setupScheme(), objs...) 81 reconciler := &KubeadmConfigReconciler{ 82 Log: log.Log, 83 Client: fakeClient, 84 } 85 for i := 0; i < 3; i++ { 86 o := handler.MapObject{ 87 Object: machineObjs[i], 88 } 89 configs := reconciler.MachineToBootstrapMapFunc(o) 90 if i == 1 { 91 if configs[0].Name != expectedConfigName { 92 t.Fatalf("unexpected config name: %s", configs[0].Name) 93 } 94 } else { 95 if configs[0].Name != "" { 96 t.Fatalf("unexpected config name: %s", configs[0].Name) 97 } 98 } 99 } 100 } 101 102 // Reconcile returns early if the kubeadm config is ready because it should never re-generate bootstrap data. 103 func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfKubeadmConfigIsReady(t *testing.T) { 104 config := newKubeadmConfig(nil, "cfg") 105 config.Status.Ready = true 106 107 objects := []runtime.Object{ 108 config, 109 } 110 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 111 112 k := &KubeadmConfigReconciler{ 113 Log: log.Log, 114 Client: myclient, 115 } 116 117 request := ctrl.Request{ 118 NamespacedName: types.NamespacedName{ 119 Namespace: "default", 120 Name: "cfg", 121 }, 122 } 123 result, err := k.Reconcile(request) 124 if err != nil { 125 t.Fatalf("Failed to reconcile:\n %+v", err) 126 } 127 if result.Requeue == true { 128 t.Fatal("did not expect to requeue") 129 } 130 if result.RequeueAfter != time.Duration(0) { 131 t.Fatal("did not expect to requeue after") 132 } 133 } 134 135 // Reconcile returns an error in this case because the owning machine should not go away before the things it owns. 136 func TestKubeadmConfigReconciler_Reconcile_ReturnErrorIfReferencedMachineIsNotFound(t *testing.T) { 137 machine := newMachine(nil, "machine") 138 config := newKubeadmConfig(machine, "cfg") 139 140 objects := []runtime.Object{ 141 // intentionally omitting machine 142 config, 143 } 144 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 145 146 k := &KubeadmConfigReconciler{ 147 Log: log.Log, 148 Client: myclient, 149 } 150 151 request := ctrl.Request{ 152 NamespacedName: types.NamespacedName{ 153 Namespace: "default", 154 Name: "cfg", 155 }, 156 } 157 _, err := k.Reconcile(request) 158 if err == nil { 159 t.Fatal("Expected error, got nil") 160 } 161 } 162 163 // If the machine has bootstrap data already then there is no need to generate more bootstrap data. The work is done. 164 func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfMachineHasBootstrapData(t *testing.T) { 165 machine := newMachine(nil, "machine") 166 machine.Spec.Bootstrap.Data = stringPtr("something") 167 168 config := newKubeadmConfig(machine, "cfg") 169 objects := []runtime.Object{ 170 machine, 171 config, 172 } 173 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 174 175 k := &KubeadmConfigReconciler{ 176 Log: log.Log, 177 Client: myclient, 178 } 179 180 request := ctrl.Request{ 181 NamespacedName: types.NamespacedName{ 182 Namespace: "default", 183 Name: "cfg", 184 }, 185 } 186 result, err := k.Reconcile(request) 187 if err != nil { 188 t.Fatalf("Failed to reconcile:\n %+v", err) 189 } 190 if result.Requeue == true { 191 t.Fatal("did not expect to requeue") 192 } 193 if result.RequeueAfter != time.Duration(0) { 194 t.Fatal("did not expect to requeue after") 195 } 196 } 197 198 // Return early If the owning machine does not have an associated cluster 199 func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfMachineHasNoCluster(t *testing.T) { 200 machine := newMachine(nil, "machine") // Machine without a cluster 201 config := newKubeadmConfig(machine, "cfg") 202 203 objects := []runtime.Object{ 204 machine, 205 config, 206 } 207 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 208 209 k := &KubeadmConfigReconciler{ 210 Log: log.Log, 211 Client: myclient, 212 } 213 214 request := ctrl.Request{ 215 NamespacedName: types.NamespacedName{ 216 Namespace: "default", 217 Name: "cfg", 218 }, 219 } 220 _, err := k.Reconcile(request) 221 if err != nil { 222 t.Fatalf("Not Expecting error, got an error: %+v", err) 223 } 224 } 225 226 // This does not expect an error, hoping the machine gets updated with a cluster 227 func TestKubeadmConfigReconciler_Reconcile_ReturnNilIfMachineDoesNotHaveAssociatedCluster(t *testing.T) { 228 machine := newMachine(nil, "machine") // intentionally omitting cluster 229 config := newKubeadmConfig(machine, "cfg") 230 231 objects := []runtime.Object{ 232 machine, 233 config, 234 } 235 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 236 237 k := &KubeadmConfigReconciler{ 238 Log: log.Log, 239 Client: myclient, 240 } 241 242 request := ctrl.Request{ 243 NamespacedName: types.NamespacedName{ 244 Namespace: "default", 245 Name: "cfg", 246 }, 247 } 248 _, err := k.Reconcile(request) 249 if err != nil { 250 t.Fatal("Not Expecting error, got an error") 251 } 252 } 253 254 // This does not expect an error, hoping that the associated cluster will be created 255 func TestKubeadmConfigReconciler_Reconcile_ReturnNilIfAssociatedClusterIsNotFound(t *testing.T) { 256 cluster := newCluster("cluster") 257 machine := newMachine(cluster, "machine") 258 config := newKubeadmConfig(machine, "cfg") 259 260 objects := []runtime.Object{ 261 // intentionally omitting cluster 262 machine, 263 config, 264 } 265 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 266 267 k := &KubeadmConfigReconciler{ 268 Log: log.Log, 269 Client: myclient, 270 } 271 272 request := ctrl.Request{ 273 NamespacedName: types.NamespacedName{ 274 Namespace: "default", 275 Name: "cfg", 276 }, 277 } 278 _, err := k.Reconcile(request) 279 if err != nil { 280 t.Fatal("Not Expecting error, got an error") 281 } 282 } 283 284 // If the control plane isn't initialized then there is no cluster for either a worker or control plane node to join. 285 func TestKubeadmConfigReconciler_Reconcile_RequeueJoiningNodesIfControlPlaneNotInitialized(t *testing.T) { 286 cluster := newCluster("cluster") 287 cluster.Status.InfrastructureReady = true 288 289 workerMachine := newWorkerMachine(cluster) 290 workerJoinConfig := newWorkerJoinKubeadmConfig(workerMachine) 291 292 controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine") 293 controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine, "control-plane-join-cfg") 294 295 testcases := []struct { 296 name string 297 request ctrl.Request 298 objects []runtime.Object 299 }{ 300 { 301 name: "requeue worker when control plane is not yet initialiezd", 302 request: ctrl.Request{ 303 NamespacedName: types.NamespacedName{ 304 Namespace: workerJoinConfig.Namespace, 305 Name: workerJoinConfig.Name, 306 }, 307 }, 308 objects: []runtime.Object{ 309 cluster, 310 workerMachine, 311 workerJoinConfig, 312 }, 313 }, 314 { 315 name: "requeue a secondary control plane when the control plane is not yet initialized", 316 request: ctrl.Request{ 317 NamespacedName: types.NamespacedName{ 318 Namespace: controlPlaneJoinConfig.Namespace, 319 Name: controlPlaneJoinConfig.Name, 320 }, 321 }, 322 objects: []runtime.Object{ 323 cluster, 324 controlPlaneJoinMachine, 325 controlPlaneJoinConfig, 326 }, 327 }, 328 } 329 for _, tc := range testcases { 330 t.Run(tc.name, func(t *testing.T) { 331 myclient := fake.NewFakeClientWithScheme(setupScheme(), tc.objects...) 332 333 k := &KubeadmConfigReconciler{ 334 Log: log.Log, 335 Client: myclient, 336 KubeadmInitLock: &myInitLocker{}, 337 } 338 339 result, err := k.Reconcile(tc.request) 340 if err != nil { 341 t.Fatalf("Failed to reconcile:\n %+v", err) 342 } 343 if result.Requeue == true { 344 t.Fatal("did not expect to requeue") 345 } 346 if result.RequeueAfter != 30*time.Second { 347 t.Fatal("expected to requeue after 30s") 348 } 349 }) 350 } 351 } 352 353 // This generates cloud-config data but does not test the validity of it. 354 func TestKubeadmConfigReconciler_Reconcile_GenerateCloudConfigData(t *testing.T) { 355 cluster := newCluster("cluster") 356 cluster.Status.InfrastructureReady = true 357 358 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 359 controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-cfg") 360 361 objects := []runtime.Object{ 362 cluster, 363 controlPlaneInitMachine, 364 controlPlaneInitConfig, 365 } 366 objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...) 367 368 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 369 370 k := &KubeadmConfigReconciler{ 371 Log: log.Log, 372 Client: myclient, 373 KubeadmInitLock: &myInitLocker{}, 374 } 375 376 request := ctrl.Request{ 377 NamespacedName: types.NamespacedName{ 378 Namespace: "default", 379 Name: "control-plane-init-cfg", 380 }, 381 } 382 result, err := k.Reconcile(request) 383 if err != nil { 384 t.Fatalf("Failed to reconcile:\n %+v", err) 385 } 386 if result.Requeue != false { 387 t.Fatal("did not expect to requeue") 388 } 389 if result.RequeueAfter != time.Duration(0) { 390 t.Fatal("did not expect to requeue after") 391 } 392 393 cfg, err := getKubeadmConfig(myclient, "control-plane-init-cfg") 394 if err != nil { 395 t.Fatalf("Failed to reconcile:\n %+v", err) 396 } 397 if cfg.Status.Ready != true { 398 t.Fatal("Expected status ready") 399 } 400 if cfg.Status.BootstrapData == nil { 401 t.Fatal("Expected generated bootstrap data") 402 } 403 404 // Ensure that we don't fail trying to refresh any bootstrap tokens 405 _, err = k.Reconcile(request) 406 if err != nil { 407 t.Fatalf("Failed to reconcile:\n %+v", err) 408 } 409 } 410 411 // If a control plane has no JoinConfiguration, then we will create a default and no error will occur 412 func TestKubeadmConfigReconciler_Reconcile_ErrorIfJoiningControlPlaneHasInvalidConfiguration(t *testing.T) { 413 // TODO: extract this kind of code into a setup function that puts the state of objects into an initialized controlplane (implies secrets exist) 414 cluster := newCluster("cluster") 415 cluster.Status.InfrastructureReady = true 416 cluster.Status.ControlPlaneInitialized = true 417 cluster.Status.APIEndpoints = []clusterv1.APIEndpoint{{Host: "100.105.150.1", Port: 6443}} 418 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 419 controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-cfg") 420 421 controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine") 422 controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine, "control-plane-join-cfg") 423 controlPlaneJoinConfig.Spec.JoinConfiguration.ControlPlane = nil // Makes controlPlaneJoinConfig invalid for a control plane machine 424 425 objects := []runtime.Object{ 426 cluster, 427 controlPlaneJoinMachine, 428 controlPlaneJoinConfig, 429 } 430 objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...) 431 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 432 433 k := &KubeadmConfigReconciler{ 434 Log: log.Log, 435 Client: myclient, 436 SecretsClientFactory: newFakeSecretFactory(), 437 KubeadmInitLock: &myInitLocker{}, 438 } 439 440 request := ctrl.Request{ 441 NamespacedName: types.NamespacedName{ 442 Namespace: "default", 443 Name: "control-plane-join-cfg", 444 }, 445 } 446 _, err := k.Reconcile(request) 447 if err != nil { 448 t.Fatalf("Expected no error but got %v", err) 449 } 450 } 451 452 // If there is no APIEndpoint but everything is ready then requeue in hopes of a new APIEndpoint showing up eventually. 453 func TestKubeadmConfigReconciler_Reconcile_RequeueIfControlPlaneIsMissingAPIEndpoints(t *testing.T) { 454 cluster := newCluster("cluster") 455 cluster.Status.InfrastructureReady = true 456 cluster.Status.ControlPlaneInitialized = true 457 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 458 controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-cfg") 459 460 workerMachine := newWorkerMachine(cluster) 461 workerJoinConfig := newWorkerJoinKubeadmConfig(workerMachine) 462 463 objects := []runtime.Object{ 464 cluster, 465 workerMachine, 466 workerJoinConfig, 467 } 468 objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...) 469 470 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 471 472 k := &KubeadmConfigReconciler{ 473 Log: log.Log, 474 Client: myclient, 475 KubeadmInitLock: &myInitLocker{}, 476 } 477 478 request := ctrl.Request{ 479 NamespacedName: types.NamespacedName{ 480 Namespace: "default", 481 Name: "worker-join-cfg", 482 }, 483 } 484 result, err := k.Reconcile(request) 485 if err != nil { 486 t.Fatalf("Failed to reconcile:\n %+v", err) 487 } 488 if result.Requeue == true { 489 t.Fatal("did not expect to requeue") 490 } 491 if result.RequeueAfter != 10*time.Second { 492 t.Fatal("expected to requeue after 10s") 493 } 494 } 495 496 func TestReconcileIfJoinNodesAndControlPlaneIsReady(t *testing.T) { 497 cluster := newCluster("cluster") 498 cluster.Status.InfrastructureReady = true 499 cluster.Status.ControlPlaneInitialized = true 500 cluster.Status.APIEndpoints = []clusterv1.APIEndpoint{{Host: "100.105.150.1", Port: 6443}} 501 502 var useCases = []struct { 503 name string 504 machine *clusterv1.Machine 505 configName string 506 configBuilder func(*clusterv1.Machine, string) *bootstrapv1.KubeadmConfig 507 }{ 508 { 509 name: "Join a worker node with a fully compiled kubeadm config object", 510 machine: newWorkerMachine(cluster), 511 configName: "worker-join-cfg", 512 configBuilder: func(machine *clusterv1.Machine, name string) *bootstrapv1.KubeadmConfig { 513 return newWorkerJoinKubeadmConfig(machine) 514 }, 515 }, 516 { 517 name: "Join a worker node with an empty kubeadm config object (defaults apply)", 518 machine: newWorkerMachine(cluster), 519 configName: "worker-join-cfg", 520 configBuilder: newKubeadmConfig, 521 }, 522 { 523 name: "Join a control plane node with a fully compiled kubeadm config object", 524 machine: newControlPlaneMachine(cluster, "control-plane-join-machine"), 525 configName: "control-plane-join-cfg", 526 configBuilder: newControlPlaneJoinKubeadmConfig, 527 }, 528 { 529 name: "Join a control plane node with an empty kubeadm config object (defaults apply)", 530 machine: newControlPlaneMachine(cluster, "control-plane-join-machine"), 531 configName: "control-plane-join-cfg", 532 configBuilder: newKubeadmConfig, 533 }, 534 } 535 536 for _, rt := range useCases { 537 rt := rt // pin! 538 t.Run(rt.name, func(t *testing.T) { 539 config := rt.configBuilder(rt.machine, rt.configName) 540 541 objects := []runtime.Object{ 542 cluster, 543 rt.machine, 544 config, 545 } 546 objects = append(objects, createSecrets(t, cluster, config)...) 547 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 548 k := &KubeadmConfigReconciler{ 549 Log: log.Log, 550 Client: myclient, 551 SecretsClientFactory: newFakeSecretFactory(), 552 KubeadmInitLock: &myInitLocker{}, 553 } 554 555 request := ctrl.Request{ 556 NamespacedName: types.NamespacedName{ 557 Namespace: config.GetNamespace(), 558 Name: rt.configName, 559 }, 560 } 561 result, err := k.Reconcile(request) 562 if err != nil { 563 t.Fatal(fmt.Sprintf("Failed to reconcile:\n %+v", err)) 564 } 565 if result.Requeue == true { 566 t.Fatal("did not expected to requeue") 567 } 568 if result.RequeueAfter != time.Duration(0) { 569 t.Fatal("did not expected to requeue after") 570 } 571 572 cfg, err := getKubeadmConfig(myclient, rt.configName) 573 if err != nil { 574 t.Fatal(fmt.Sprintf("Failed to reconcile:\n %+v", err)) 575 } 576 577 if cfg.Status.Ready != true { 578 t.Fatal("Expected status ready") 579 } 580 581 if cfg.Status.BootstrapData == nil { 582 t.Fatal("Expected status ready") 583 } 584 585 myremoteclient, _ := k.SecretsClientFactory.NewSecretsClient(nil, nil) 586 l, err := myremoteclient.List(metav1.ListOptions{}) 587 if err != nil { 588 t.Fatal(fmt.Sprintf("Failed to get secrets after reconcile:\n %+v", err)) 589 } 590 591 if len(l.Items) != 1 { 592 t.Fatal("Failed to get bootstrap token secret") 593 } 594 }) 595 596 } 597 } 598 599 func TestBootstrapTokenTTLExtension(t *testing.T) { 600 cluster := newCluster("cluster") 601 cluster.Status.InfrastructureReady = true 602 cluster.Status.ControlPlaneInitialized = true 603 cluster.Status.APIEndpoints = []clusterv1.APIEndpoint{{Host: "100.105.150.1", Port: 6443}} 604 605 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 606 initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-config") 607 workerMachine := newWorkerMachine(cluster) 608 workerJoinConfig := newWorkerJoinKubeadmConfig(workerMachine) 609 controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine") 610 controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine, "control-plane-join-cfg") 611 objects := []runtime.Object{ 612 cluster, 613 workerMachine, 614 workerJoinConfig, 615 controlPlaneJoinMachine, 616 controlPlaneJoinConfig, 617 } 618 619 objects = append(objects, createSecrets(t, cluster, initConfig)...) 620 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 621 k := &KubeadmConfigReconciler{ 622 Log: log.Log, 623 Client: myclient, 624 SecretsClientFactory: newFakeSecretFactory(), 625 KubeadmInitLock: &myInitLocker{}, 626 } 627 request := ctrl.Request{ 628 NamespacedName: types.NamespacedName{ 629 Namespace: "default", 630 Name: "worker-join-cfg", 631 }, 632 } 633 result, err := k.Reconcile(request) 634 if err != nil { 635 t.Fatalf("Failed to reconcile:\n %+v", err) 636 } 637 if result.Requeue == true { 638 t.Fatal("did not expect to requeue") 639 } 640 if result.RequeueAfter != time.Duration(0) { 641 t.Fatal("did not expect to requeue after") 642 } 643 cfg, err := getKubeadmConfig(myclient, "worker-join-cfg") 644 if err != nil { 645 t.Fatalf("Failed to reconcile:\n %+v", err) 646 } 647 if cfg.Status.Ready != true { 648 t.Fatal("Expected status ready") 649 } 650 if cfg.Status.BootstrapData == nil { 651 t.Fatal("Expected status ready") 652 } 653 request = ctrl.Request{ 654 NamespacedName: types.NamespacedName{ 655 Namespace: "default", 656 Name: "control-plane-join-cfg", 657 }, 658 } 659 result, err = k.Reconcile(request) 660 if err != nil { 661 t.Fatalf("Failed to reconcile:\n %+v", err) 662 } 663 if result.Requeue == true { 664 t.Fatal("did not expect to requeue") 665 } 666 if result.RequeueAfter != time.Duration(0) { 667 t.Fatal("did not expect to requeue after") 668 } 669 cfg, err = getKubeadmConfig(myclient, "control-plane-join-cfg") 670 if err != nil { 671 t.Fatalf("Failed to reconcile:\n %+v", err) 672 } 673 if cfg.Status.Ready != true { 674 t.Fatal("Expected status ready") 675 } 676 if cfg.Status.BootstrapData == nil { 677 t.Fatal("Expected status ready") 678 } 679 680 myremoteclient, _ := k.SecretsClientFactory.NewSecretsClient(nil, nil) 681 l, err := myremoteclient.List(metav1.ListOptions{}) 682 if err != nil { 683 t.Fatalf("Failed to read secrets:\n %+v", err) 684 } 685 686 if len(l.Items) != 2 { 687 t.Fatalf("Expected two bootstrap tokens, saw:\n %+d", len(l.Items)) 688 } 689 690 // ensure that the token is refreshed... 691 tokenExpires := make([][]byte, len(l.Items)) 692 693 for i, item := range l.Items { 694 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 695 } 696 697 <-time.After(1 * time.Second) 698 699 for _, req := range []ctrl.Request{ 700 { 701 NamespacedName: types.NamespacedName{ 702 Namespace: "default", 703 Name: "worker-join-cfg", 704 }, 705 }, 706 { 707 NamespacedName: types.NamespacedName{ 708 Namespace: "default", 709 Name: "control-plane-join-cfg", 710 }, 711 }, 712 } { 713 714 result, err := k.Reconcile(req) 715 if err != nil { 716 t.Fatalf("Failed to reconcile:\n %+v", err) 717 } 718 if result.RequeueAfter >= DefaultTokenTTL { 719 t.Fatal("expected a requeue duration less than the token TTL") 720 } 721 } 722 723 l, err = myremoteclient.List(metav1.ListOptions{}) 724 if err != nil { 725 t.Fatalf("Failed to read secrets:\n %+v", err) 726 } 727 728 if len(l.Items) != 2 { 729 t.Fatalf("Expected two bootstrap tokens, saw:\n %+d", len(l.Items)) 730 } 731 732 for i, item := range l.Items { 733 if bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey]) { 734 t.Fatal("Reconcile should have refreshed bootstrap token's expiration until the infrastructure was ready") 735 } 736 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 737 } 738 739 // ...until the infrastructure is marked "ready" 740 workerMachine.Status.InfrastructureReady = true 741 err = myclient.Update(context.Background(), workerMachine) 742 if err != nil { 743 t.Fatalf("unable to set machine infrastructure ready: %v", err) 744 } 745 746 controlPlaneJoinMachine.Status.InfrastructureReady = true 747 err = myclient.Update(context.Background(), controlPlaneJoinMachine) 748 if err != nil { 749 t.Fatalf("unable to set machine infrastructure ready: %v", err) 750 } 751 752 <-time.After(1 * time.Second) 753 754 for _, req := range []ctrl.Request{ 755 { 756 NamespacedName: types.NamespacedName{ 757 Namespace: "default", 758 Name: "worker-join-cfg", 759 }, 760 }, 761 { 762 NamespacedName: types.NamespacedName{ 763 Namespace: "default", 764 Name: "control-plane-join-cfg", 765 }, 766 }, 767 } { 768 769 result, err := k.Reconcile(req) 770 if err != nil { 771 t.Fatalf("Failed to reconcile:\n %+v", err) 772 } 773 if result.Requeue == true { 774 t.Fatal("did not expect to requeue") 775 } 776 if result.RequeueAfter != time.Duration(0) { 777 t.Fatal("did not expect to requeue after") 778 } 779 } 780 781 l, err = myremoteclient.List(metav1.ListOptions{}) 782 if err != nil { 783 t.Fatalf("Failed to read secrets:\n %+v", err) 784 } 785 786 if len(l.Items) != 2 { 787 t.Fatalf("Expected two bootstrap tokens, saw:\n %+d", len(l.Items)) 788 } 789 790 for i, item := range l.Items { 791 if !bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey]) { 792 t.Fatal("Reconcile should have let the bootstrap token expire after the infrastructure was ready") 793 } 794 } 795 } 796 797 // Ensure the discovery portion of the JoinConfiguration gets generated correctly. 798 func TestKubeadmConfigReconciler_Reconcile_DisocveryReconcileBehaviors(t *testing.T) { 799 k := &KubeadmConfigReconciler{ 800 Log: log.Log, 801 Client: nil, 802 SecretsClientFactory: newFakeSecretFactory(), 803 KubeadmInitLock: &myInitLocker{}, 804 } 805 806 dummyCAHash := []string{"...."} 807 bootstrapToken := kubeadmv1beta1.Discovery{ 808 BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{ 809 CACertHashes: dummyCAHash, 810 }, 811 } 812 goodcluster := &clusterv1.Cluster{ 813 Status: clusterv1.ClusterStatus{ 814 APIEndpoints: []clusterv1.APIEndpoint{ 815 { 816 Host: "example.com", 817 Port: 6443, 818 }, 819 }, 820 }, 821 } 822 testcases := []struct { 823 name string 824 cluster *clusterv1.Cluster 825 config *bootstrapv1.KubeadmConfig 826 validateDiscovery func(*bootstrapv1.KubeadmConfig) error 827 }{ 828 { 829 name: "Automatically generate token if discovery not specified", 830 cluster: goodcluster, 831 config: &bootstrapv1.KubeadmConfig{ 832 Spec: bootstrapv1.KubeadmConfigSpec{ 833 JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{ 834 Discovery: bootstrapToken, 835 }, 836 }, 837 }, 838 validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error { 839 d := c.Spec.JoinConfiguration.Discovery 840 if d.BootstrapToken == nil { 841 return errors.Errorf("BootstrapToken expected, got nil") 842 } 843 if d.BootstrapToken.Token == "" { 844 return errors.Errorf(("BootstrapToken.Token expected, got empty string")) 845 } 846 if d.BootstrapToken.APIServerEndpoint != "example.com:6443" { 847 return errors.Errorf("BootstrapToken.APIServerEndpoint=example.com:6443 expected, got %q", d.BootstrapToken.APIServerEndpoint) 848 } 849 if d.BootstrapToken.UnsafeSkipCAVerification == true { 850 return errors.Errorf("BootstrapToken.UnsafeSkipCAVerification=false expected, got true") 851 } 852 return nil 853 }, 854 }, 855 { 856 name: "Respect discoveryConfiguration.File", 857 cluster: goodcluster, 858 config: &bootstrapv1.KubeadmConfig{ 859 Spec: bootstrapv1.KubeadmConfigSpec{ 860 JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{ 861 Discovery: kubeadmv1beta1.Discovery{ 862 File: &kubeadmv1beta1.FileDiscovery{}, 863 }, 864 }, 865 }, 866 }, 867 validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error { 868 d := c.Spec.JoinConfiguration.Discovery 869 if d.BootstrapToken != nil { 870 return errors.Errorf("BootstrapToken should not be created when DiscoveryFile is defined") 871 } 872 return nil 873 }, 874 }, 875 { 876 name: "Respect discoveryConfiguration.BootstrapToken.APIServerEndpoint", 877 cluster: goodcluster, 878 config: &bootstrapv1.KubeadmConfig{ 879 Spec: bootstrapv1.KubeadmConfigSpec{ 880 JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{ 881 Discovery: kubeadmv1beta1.Discovery{ 882 BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{ 883 CACertHashes: dummyCAHash, 884 APIServerEndpoint: "bar.com:6443", 885 }, 886 }, 887 }, 888 }, 889 }, 890 validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error { 891 d := c.Spec.JoinConfiguration.Discovery 892 if d.BootstrapToken.APIServerEndpoint != "bar.com:6443" { 893 return errors.Errorf("BootstrapToken.APIServerEndpoint=https://bar.com:6443 expected, got %s", d.BootstrapToken.APIServerEndpoint) 894 } 895 return nil 896 }, 897 }, 898 { 899 name: "Respect discoveryConfiguration.BootstrapToken.Token", 900 cluster: goodcluster, 901 config: &bootstrapv1.KubeadmConfig{ 902 Spec: bootstrapv1.KubeadmConfigSpec{ 903 JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{ 904 Discovery: kubeadmv1beta1.Discovery{ 905 BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{ 906 CACertHashes: dummyCAHash, 907 Token: "abcdef.0123456789abcdef", 908 }, 909 }, 910 }, 911 }, 912 }, 913 validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error { 914 d := c.Spec.JoinConfiguration.Discovery 915 if d.BootstrapToken.Token != "abcdef.0123456789abcdef" { 916 return errors.Errorf("BootstrapToken.Token=abcdef.0123456789abcdef expected, got %s", d.BootstrapToken.Token) 917 } 918 return nil 919 }, 920 }, 921 { 922 name: "Respect discoveryConfiguration.BootstrapToken.CACertHashes", 923 cluster: goodcluster, 924 config: &bootstrapv1.KubeadmConfig{ 925 Spec: bootstrapv1.KubeadmConfigSpec{ 926 JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{ 927 Discovery: kubeadmv1beta1.Discovery{ 928 BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{ 929 CACertHashes: dummyCAHash, 930 }, 931 }, 932 }, 933 }, 934 }, 935 validateDiscovery: func(c *bootstrapv1.KubeadmConfig) error { 936 d := c.Spec.JoinConfiguration.Discovery 937 if !reflect.DeepEqual(d.BootstrapToken.CACertHashes, dummyCAHash) { 938 return errors.Errorf("BootstrapToken.CACertHashes=%s expected, got %s", dummyCAHash, d.BootstrapToken.Token) 939 } 940 return nil 941 }, 942 }, 943 } 944 945 for _, tc := range testcases { 946 t.Run(tc.name, func(t *testing.T) { 947 err := k.reconcileDiscovery(tc.cluster, tc.config, internalcluster.Certificates{}) 948 if err != nil { 949 t.Errorf("expected nil, got error %v", err) 950 } 951 952 if err := tc.validateDiscovery(tc.config); err != nil { 953 t.Fatal(err) 954 } 955 }) 956 } 957 } 958 959 // Test failure cases for the discovery reconcile function. 960 func TestKubeadmConfigReconciler_Reconcile_DisocveryReconcileFailureBehaviors(t *testing.T) { 961 k := &KubeadmConfigReconciler{ 962 Log: log.Log, 963 Client: nil, 964 } 965 966 testcases := []struct { 967 name string 968 cluster *clusterv1.Cluster 969 config *bootstrapv1.KubeadmConfig 970 }{ 971 { 972 name: "Fail if cluster has not APIEndpoints", 973 cluster: &clusterv1.Cluster{}, // cluster without endpoints 974 config: &bootstrapv1.KubeadmConfig{ 975 Spec: bootstrapv1.KubeadmConfigSpec{ 976 JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{ 977 Discovery: kubeadmv1beta1.Discovery{ 978 BootstrapToken: &kubeadmv1beta1.BootstrapTokenDiscovery{ 979 CACertHashes: []string{"item"}, 980 }, 981 }, 982 }, 983 }, 984 }, 985 }, 986 } 987 988 for _, tc := range testcases { 989 t.Run(tc.name, func(t *testing.T) { 990 err := k.reconcileDiscovery(tc.cluster, tc.config, internalcluster.Certificates{}) 991 if err == nil { 992 t.Error("expected error, got nil") 993 } 994 }) 995 } 996 } 997 998 // Set cluster configuration defaults based on dynamic values from the cluster object. 999 func TestKubeadmConfigReconciler_Reconcile_DynamicDefaultsForClusterConfiguration(t *testing.T) { 1000 k := &KubeadmConfigReconciler{ 1001 Log: log.Log, 1002 Client: nil, 1003 } 1004 1005 testcases := []struct { 1006 name string 1007 cluster *clusterv1.Cluster 1008 machine *clusterv1.Machine 1009 config *bootstrapv1.KubeadmConfig 1010 }{ 1011 { 1012 name: "Config settings have precedence", 1013 config: &bootstrapv1.KubeadmConfig{ 1014 Spec: bootstrapv1.KubeadmConfigSpec{ 1015 ClusterConfiguration: &kubeadmv1beta1.ClusterConfiguration{ 1016 ClusterName: "mycluster", 1017 KubernetesVersion: "myversion", 1018 Networking: kubeadmv1beta1.Networking{ 1019 PodSubnet: "myPodSubnet", 1020 ServiceSubnet: "myServiceSubnet", 1021 DNSDomain: "myDNSDomain", 1022 }, 1023 ControlPlaneEndpoint: "myControlPlaneEndpoint:6443", 1024 }, 1025 }, 1026 }, 1027 cluster: &clusterv1.Cluster{ 1028 ObjectMeta: metav1.ObjectMeta{ 1029 Name: "OtherName", 1030 }, 1031 Spec: clusterv1.ClusterSpec{ 1032 ClusterNetwork: &clusterv1.ClusterNetwork{ 1033 Services: &clusterv1.NetworkRanges{CIDRBlocks: []string{"otherServicesCidr"}}, 1034 Pods: &clusterv1.NetworkRanges{CIDRBlocks: []string{"otherPodsCidr"}}, 1035 ServiceDomain: "otherServiceDomain", 1036 }, 1037 }, 1038 Status: clusterv1.ClusterStatus{ 1039 APIEndpoints: []clusterv1.APIEndpoint{{Host: "otherVersion", Port: 0}}, 1040 }, 1041 }, 1042 machine: &clusterv1.Machine{ 1043 Spec: clusterv1.MachineSpec{ 1044 Version: stringPtr("otherVersion"), 1045 }, 1046 }, 1047 }, 1048 { 1049 name: "Top level object settings are used in case config settings are missing", 1050 config: &bootstrapv1.KubeadmConfig{ 1051 Spec: bootstrapv1.KubeadmConfigSpec{ 1052 ClusterConfiguration: &kubeadmv1beta1.ClusterConfiguration{}, 1053 }, 1054 }, 1055 cluster: &clusterv1.Cluster{ 1056 ObjectMeta: metav1.ObjectMeta{ 1057 Name: "mycluster", 1058 }, 1059 Spec: clusterv1.ClusterSpec{ 1060 ClusterNetwork: &clusterv1.ClusterNetwork{ 1061 Services: &clusterv1.NetworkRanges{CIDRBlocks: []string{"myServiceSubnet"}}, 1062 Pods: &clusterv1.NetworkRanges{CIDRBlocks: []string{"myPodSubnet"}}, 1063 ServiceDomain: "myDNSDomain", 1064 }, 1065 }, 1066 Status: clusterv1.ClusterStatus{ 1067 APIEndpoints: []clusterv1.APIEndpoint{{Host: "myControlPlaneEndpoint", Port: 6443}}, 1068 }, 1069 }, 1070 machine: &clusterv1.Machine{ 1071 Spec: clusterv1.MachineSpec{ 1072 Version: stringPtr("myversion"), 1073 }, 1074 }, 1075 }, 1076 } 1077 1078 for _, tc := range testcases { 1079 t.Run(tc.name, func(t *testing.T) { 1080 k.reconcileTopLevelObjectSettings(tc.cluster, tc.machine, tc.config) 1081 1082 if tc.config.Spec.ClusterConfiguration.ControlPlaneEndpoint != "myControlPlaneEndpoint:6443" { 1083 t.Errorf("expected ClusterConfiguration.ControlPlaneEndpoint %q, got %q", "myControlPlaneEndpoint:6443", tc.config.Spec.ClusterConfiguration.ControlPlaneEndpoint) 1084 } 1085 if tc.config.Spec.ClusterConfiguration.ClusterName != "mycluster" { 1086 t.Errorf("expected ClusterConfiguration.ClusterName %q, got %q", "mycluster", tc.config.Spec.ClusterConfiguration.ClusterName) 1087 } 1088 if tc.config.Spec.ClusterConfiguration.Networking.PodSubnet != "myPodSubnet" { 1089 t.Errorf("expected ClusterConfiguration.Networking.PodSubnet %q, got %q", "myPodSubnet", tc.config.Spec.ClusterConfiguration.Networking.PodSubnet) 1090 } 1091 if tc.config.Spec.ClusterConfiguration.Networking.ServiceSubnet != "myServiceSubnet" { 1092 t.Errorf("expected ClusterConfiguration.Networking.ServiceSubnet %q, got %q", "myServiceSubnet", tc.config.Spec.ClusterConfiguration.Networking.ServiceSubnet) 1093 } 1094 if tc.config.Spec.ClusterConfiguration.Networking.DNSDomain != "myDNSDomain" { 1095 t.Errorf("expected ClusterConfiguration.Networking.DNSDomain %q, got %q", "myDNSDomain", tc.config.Spec.ClusterConfiguration.Networking.DNSDomain) 1096 } 1097 if tc.config.Spec.ClusterConfiguration.KubernetesVersion != "myversion" { 1098 t.Errorf("expected ClusterConfiguration.KubernetesVersion %q, got %q", "myversion", tc.config.Spec.ClusterConfiguration.KubernetesVersion) 1099 } 1100 }) 1101 } 1102 } 1103 1104 // Allow users to skip CA Verification if they *really* want to. 1105 func TestKubeadmConfigReconciler_Reconcile_AlwaysCheckCAVerificationUnlessRequestedToSkip(t *testing.T) { 1106 // Setup work for an initialized cluster 1107 clusterName := "my-cluster" 1108 cluster := newCluster(clusterName) 1109 cluster.Status.ControlPlaneInitialized = true 1110 cluster.Status.InfrastructureReady = true 1111 cluster.Status.APIEndpoints = []clusterv1.APIEndpoint{ 1112 { 1113 Host: "example.com", 1114 Port: 6443, 1115 }, 1116 } 1117 controlPlaneInitMachine := newControlPlaneMachine(cluster, "my-control-plane-init-machine") 1118 initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "my-control-plane-init-config") 1119 1120 controlPlaneMachineName := "my-machine" 1121 machine := newMachine(cluster, controlPlaneMachineName) 1122 1123 workerMachineName := "my-worker" 1124 workerMachine := newMachine(cluster, workerMachineName) 1125 1126 controlPlaneConfigName := "my-config" 1127 config := newKubeadmConfig(machine, controlPlaneConfigName) 1128 1129 objects := []runtime.Object{ 1130 cluster, machine, workerMachine, config, 1131 } 1132 objects = append(objects, createSecrets(t, cluster, initConfig)...) 1133 1134 testcases := []struct { 1135 name string 1136 discovery *kubeadmv1beta1.BootstrapTokenDiscovery 1137 skipCAVerification bool 1138 }{ 1139 { 1140 name: "Do not skip CA verification by default", 1141 discovery: &kubeadmv1beta1.BootstrapTokenDiscovery{}, 1142 skipCAVerification: false, 1143 }, 1144 { 1145 name: "Skip CA verification if requested by the user", 1146 discovery: &kubeadmv1beta1.BootstrapTokenDiscovery{ 1147 UnsafeSkipCAVerification: true, 1148 }, 1149 skipCAVerification: true, 1150 }, 1151 { 1152 // skipCAVerification should be true since no Cert Hashes are provided, but reconcile will *always* get or create certs. 1153 // TODO: Certificate get/create behavior needs to be mocked to enable this test. 1154 name: "cannot test for defaulting behavior through the reconcile function", 1155 discovery: &kubeadmv1beta1.BootstrapTokenDiscovery{ 1156 CACertHashes: []string{""}, 1157 }, 1158 skipCAVerification: false, 1159 }, 1160 } 1161 for _, tc := range testcases { 1162 t.Run(tc.name, func(t *testing.T) { 1163 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 1164 reconciler := KubeadmConfigReconciler{ 1165 Client: myclient, 1166 SecretsClientFactory: newFakeSecretFactory(), 1167 KubeadmInitLock: &myInitLocker{}, 1168 Log: klogr.New(), 1169 } 1170 1171 wc := newWorkerJoinKubeadmConfig(workerMachine) 1172 wc.Spec.JoinConfiguration.Discovery.BootstrapToken = tc.discovery 1173 key := types.NamespacedName{Namespace: wc.Namespace, Name: wc.Name} 1174 if err := myclient.Create(context.Background(), wc); err != nil { 1175 t.Fatal(err) 1176 } 1177 req := ctrl.Request{NamespacedName: key} 1178 if _, err := reconciler.Reconcile(req); err != nil { 1179 t.Fatalf("reconciled an error: %v", err) 1180 } 1181 cfg := &bootstrapv1.KubeadmConfig{} 1182 if err := myclient.Get(context.Background(), key, cfg); err != nil { 1183 t.Fatal(err) 1184 } 1185 if cfg.Spec.JoinConfiguration.Discovery.BootstrapToken.UnsafeSkipCAVerification != tc.skipCAVerification { 1186 t.Fatalf("Expected skip CA verification: %v but was %v", tc.skipCAVerification, !tc.skipCAVerification) 1187 } 1188 }) 1189 } 1190 } 1191 1192 // If a cluster object changes then all associated KubeadmConfigs should be re-reconciled. 1193 // This allows us to not requeue a kubeadm config while we wait for InfrastructureReady. 1194 func TestKubeadmConfigReconciler_ClusterToKubeadmConfigs(t *testing.T) { 1195 cluster := newCluster("my-cluster") 1196 objs := []runtime.Object{cluster} 1197 expectedNames := []string{} 1198 for i := 0; i < 3; i++ { 1199 m := newMachine(cluster, fmt.Sprintf("my-machine-%d", i)) 1200 configName := fmt.Sprintf("my-config-%d", i) 1201 c := newKubeadmConfig(m, configName) 1202 expectedNames = append(expectedNames, configName) 1203 objs = append(objs, m, c) 1204 } 1205 fakeClient := fake.NewFakeClientWithScheme(setupScheme(), objs...) 1206 reconciler := &KubeadmConfigReconciler{ 1207 Log: log.Log, 1208 Client: fakeClient, 1209 } 1210 o := handler.MapObject{ 1211 Object: cluster, 1212 } 1213 configs := reconciler.ClusterToKubeadmConfigs(o) 1214 names := make([]string, 3) 1215 for i := range configs { 1216 names[i] = configs[i].Name 1217 } 1218 for _, name := range expectedNames { 1219 found := false 1220 for _, foundName := range names { 1221 if foundName == name { 1222 found = true 1223 } 1224 } 1225 if !found { 1226 t.Fatalf("did not find %s in %v", name, names) 1227 } 1228 } 1229 } 1230 1231 // Reconcile should not fail if the Etcd CA Secret already exists 1232 func TestKubeadmConfigReconciler_Reconcile_DoesNotFailIfCASecretsAlreadyExist(t *testing.T) { 1233 cluster := newCluster("my-cluster") 1234 cluster.Status.InfrastructureReady = true 1235 cluster.Status.ControlPlaneInitialized = false 1236 m := newControlPlaneMachine(cluster, "control-plane-machine") 1237 configName := "my-config" 1238 c := newControlPlaneInitKubeadmConfig(m, configName) 1239 scrt := &corev1.Secret{ 1240 ObjectMeta: metav1.ObjectMeta{ 1241 Name: fmt.Sprintf("%s-%s", cluster.Name, internalcluster.EtcdCA), 1242 Namespace: "default", 1243 }, 1244 Data: map[string][]byte{ 1245 "tls.crt": []byte("hello world"), 1246 "tls.key": []byte("hello world"), 1247 }, 1248 } 1249 fakec := fake.NewFakeClientWithScheme(setupScheme(), []runtime.Object{cluster, m, c, scrt}...) 1250 reconciler := &KubeadmConfigReconciler{ 1251 Log: log.Log, 1252 Client: fakec, 1253 KubeadmInitLock: &myInitLocker{}, 1254 } 1255 req := ctrl.Request{ 1256 NamespacedName: types.NamespacedName{Namespace: "default", Name: configName}, 1257 } 1258 if _, err := reconciler.Reconcile(req); err != nil { 1259 t.Fatal(err) 1260 } 1261 } 1262 1263 // Exactly one control plane machine initializes if there are multiple control plane machines defined 1264 func TestKubeadmConfigReconciler_Reconcile_ExactlyOneControlPlaneMachineInitializes(t *testing.T) { 1265 cluster := newCluster("cluster") 1266 cluster.Status.InfrastructureReady = true 1267 1268 controlPlaneInitMachineFirst := newControlPlaneMachine(cluster, "control-plane-init-machine-first") 1269 controlPlaneInitConfigFirst := newControlPlaneInitKubeadmConfig(controlPlaneInitMachineFirst, "control-plane-init-cfg-first") 1270 1271 controlPlaneInitMachineSecond := newControlPlaneMachine(cluster, "control-plane-init-machine-second") 1272 controlPlaneInitConfigSecond := newControlPlaneInitKubeadmConfig(controlPlaneInitMachineSecond, "control-plane-init-cfg-second") 1273 1274 objects := []runtime.Object{ 1275 cluster, 1276 controlPlaneInitMachineFirst, 1277 controlPlaneInitConfigFirst, 1278 controlPlaneInitMachineSecond, 1279 controlPlaneInitConfigSecond, 1280 } 1281 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 1282 k := &KubeadmConfigReconciler{ 1283 Log: log.Log, 1284 Client: myclient, 1285 SecretsClientFactory: newFakeSecretFactory(), 1286 KubeadmInitLock: &myInitLocker{}, 1287 } 1288 1289 request := ctrl.Request{ 1290 NamespacedName: types.NamespacedName{ 1291 Namespace: "default", 1292 Name: "control-plane-init-cfg-first", 1293 }, 1294 } 1295 result, err := k.Reconcile(request) 1296 if err != nil { 1297 t.Fatalf("Failed to reconcile:\n %+v", err) 1298 } 1299 if result.Requeue == true { 1300 t.Fatal("did not expect to requeue") 1301 } 1302 if result.RequeueAfter != time.Duration(0) { 1303 t.Fatal("did not expect to requeue after") 1304 } 1305 1306 request = ctrl.Request{ 1307 NamespacedName: types.NamespacedName{ 1308 Namespace: "default", 1309 Name: "control-plane-init-cfg-second", 1310 }, 1311 } 1312 result, err = k.Reconcile(request) 1313 if err != nil { 1314 t.Fatalf("Failed to reconcile:\n %+v", err) 1315 } 1316 if result.Requeue == true { 1317 t.Fatal("did not expect to requeue") 1318 } 1319 if result.RequeueAfter != 30*time.Second { 1320 t.Fatal("expected to requeue after 30s") 1321 } 1322 } 1323 1324 // No patch should be applied if there is an error in reconcile 1325 func TestKubeadmConfigReconciler_Reconcile_DoNotPatchWhenErrorOccurred(t *testing.T) { 1326 cluster := newCluster("cluster") 1327 cluster.Status.InfrastructureReady = true 1328 1329 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 1330 controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine, "control-plane-init-cfg") 1331 1332 // set InitConfiguration as nil, we will check this to determine if the kubeadm config has been patched 1333 controlPlaneInitConfig.Spec.InitConfiguration = nil 1334 1335 objects := []runtime.Object{ 1336 cluster, 1337 controlPlaneInitMachine, 1338 controlPlaneInitConfig, 1339 } 1340 1341 secrets := createSecrets(t, cluster, controlPlaneInitConfig) 1342 for _, obj := range secrets { 1343 s := obj.(*corev1.Secret) 1344 delete(s.Data, secret.TLSCrtDataName) // destroy the secrets, which will cause Reconcile to fail 1345 objects = append(objects, s) 1346 } 1347 1348 myclient := fake.NewFakeClientWithScheme(setupScheme(), objects...) 1349 k := &KubeadmConfigReconciler{ 1350 Log: log.Log, 1351 Client: myclient, 1352 SecretsClientFactory: newFakeSecretFactory(), 1353 KubeadmInitLock: &myInitLocker{}, 1354 } 1355 1356 request := ctrl.Request{ 1357 NamespacedName: types.NamespacedName{ 1358 Namespace: "default", 1359 Name: "control-plane-init-cfg", 1360 }, 1361 } 1362 1363 result, err := k.Reconcile(request) 1364 if err == nil { 1365 t.Fatal("Expected error, got nil") 1366 } 1367 if result.Requeue != false { 1368 t.Fatal("did not expect to requeue") 1369 } 1370 if result.RequeueAfter != time.Duration(0) { 1371 t.Fatal("did not expect to requeue after") 1372 } 1373 1374 cfg, err := getKubeadmConfig(myclient, "control-plane-init-cfg") 1375 if err != nil { 1376 t.Fatalf("Failed to reconcile:\n %+v", err) 1377 } 1378 1379 // check if the kubeadm config has been patched 1380 if cfg.Spec.InitConfiguration != nil { 1381 t.Fatal("did not expect to patch the kubeadm config if there was an error in Reconcile") 1382 } 1383 } 1384 1385 // test utils 1386 1387 // newCluster return a CAPI cluster object 1388 func newCluster(name string) *clusterv1.Cluster { 1389 return &clusterv1.Cluster{ 1390 TypeMeta: metav1.TypeMeta{ 1391 Kind: "Cluster", 1392 APIVersion: clusterv1.GroupVersion.String(), 1393 }, 1394 ObjectMeta: metav1.ObjectMeta{ 1395 Namespace: "default", 1396 Name: name, 1397 }, 1398 } 1399 } 1400 1401 // newMachine return a CAPI machine object; if cluster is not nil, the machine is linked to the cluster as well 1402 func newMachine(cluster *clusterv1.Cluster, name string) *clusterv1.Machine { 1403 machine := &clusterv1.Machine{ 1404 TypeMeta: metav1.TypeMeta{ 1405 Kind: "Machine", 1406 APIVersion: clusterv1.GroupVersion.String(), 1407 }, 1408 ObjectMeta: metav1.ObjectMeta{ 1409 Namespace: "default", 1410 Name: name, 1411 }, 1412 Spec: clusterv1.MachineSpec{ 1413 Bootstrap: clusterv1.Bootstrap{ 1414 ConfigRef: &corev1.ObjectReference{ 1415 Kind: "KubeadmConfig", 1416 APIVersion: bootstrapv1.GroupVersion.String(), 1417 }, 1418 }, 1419 }, 1420 } 1421 if cluster != nil { 1422 machine.ObjectMeta.Labels = map[string]string{ 1423 clusterv1.MachineClusterLabelName: cluster.Name, 1424 } 1425 } 1426 return machine 1427 } 1428 1429 func newWorkerMachine(cluster *clusterv1.Cluster) *clusterv1.Machine { 1430 return newMachine(cluster, "worker-machine") // machine by default is a worker node (not the bootstrapNode) 1431 } 1432 1433 func newControlPlaneMachine(cluster *clusterv1.Cluster, name string) *clusterv1.Machine { 1434 m := newMachine(cluster, name) 1435 m.Labels[clusterv1.MachineControlPlaneLabelName] = "true" 1436 return m 1437 } 1438 1439 // newKubeadmConfig return a CABPK KubeadmConfig object; if machine is not nil, the KubeadmConfig is linked to the machine as well 1440 func newKubeadmConfig(machine *clusterv1.Machine, name string) *bootstrapv1.KubeadmConfig { 1441 config := &bootstrapv1.KubeadmConfig{ 1442 TypeMeta: metav1.TypeMeta{ 1443 Kind: "KubeadmConfig", 1444 APIVersion: bootstrapv1.GroupVersion.String(), 1445 }, 1446 ObjectMeta: metav1.ObjectMeta{ 1447 Namespace: "default", 1448 Name: name, 1449 }, 1450 } 1451 if machine != nil { 1452 config.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ 1453 { 1454 Kind: "Machine", 1455 APIVersion: clusterv1.GroupVersion.String(), 1456 Name: machine.Name, 1457 UID: types.UID(fmt.Sprintf("%s uid", machine.Name)), 1458 }, 1459 } 1460 machine.Spec.Bootstrap.ConfigRef.Name = config.Name 1461 machine.Spec.Bootstrap.ConfigRef.Namespace = config.Namespace 1462 } 1463 return config 1464 } 1465 1466 func newWorkerJoinKubeadmConfig(machine *clusterv1.Machine) *bootstrapv1.KubeadmConfig { 1467 c := newKubeadmConfig(machine, "worker-join-cfg") 1468 c.Spec.JoinConfiguration = &kubeadmv1beta1.JoinConfiguration{ 1469 ControlPlane: nil, 1470 } 1471 return c 1472 } 1473 1474 func newControlPlaneJoinKubeadmConfig(machine *clusterv1.Machine, name string) *bootstrapv1.KubeadmConfig { 1475 c := newKubeadmConfig(machine, name) 1476 c.Spec.JoinConfiguration = &kubeadmv1beta1.JoinConfiguration{ 1477 ControlPlane: &kubeadmv1beta1.JoinControlPlane{}, 1478 } 1479 return c 1480 } 1481 1482 func newControlPlaneInitKubeadmConfig(machine *clusterv1.Machine, name string) *bootstrapv1.KubeadmConfig { 1483 c := newKubeadmConfig(machine, name) 1484 c.Spec.ClusterConfiguration = &kubeadmv1beta1.ClusterConfiguration{} 1485 c.Spec.InitConfiguration = &kubeadmv1beta1.InitConfiguration{} 1486 return c 1487 } 1488 1489 func createSecrets(t *testing.T, cluster *clusterv1.Cluster, owner *bootstrapv1.KubeadmConfig) []runtime.Object { 1490 out := []runtime.Object{} 1491 if owner.Spec.ClusterConfiguration == nil { 1492 owner.Spec.ClusterConfiguration = &kubeadmv1beta1.ClusterConfiguration{} 1493 } 1494 certificates := internalcluster.NewCertificatesForInitialControlPlane(owner.Spec.ClusterConfiguration) 1495 if err := certificates.Generate(); err != nil { 1496 t.Fatal(err) 1497 } 1498 for _, certificate := range certificates { 1499 out = append(out, certificate.AsSecret(cluster, owner)) 1500 } 1501 return out 1502 } 1503 1504 func stringPtr(s string) *string { 1505 return &s 1506 } 1507 1508 // TODO this is not a fake but an actual client whose behavior we cannot control. 1509 // TODO remove this, probably when https://github.com/kubernetes-sigs/cluster-api-bootstrap-provider-kubeadm/issues/127 is closed. 1510 func newFakeSecretFactory() FakeSecretFactory { 1511 return FakeSecretFactory{ 1512 client: fakeclient.NewSimpleClientset().CoreV1().Secrets(metav1.NamespaceSystem), 1513 } 1514 } 1515 1516 type FakeSecretFactory struct { 1517 client typedcorev1.SecretInterface 1518 } 1519 1520 func (f FakeSecretFactory) NewSecretsClient(client client.Client, cluster *clusterv1.Cluster) (typedcorev1.SecretInterface, error) { 1521 return f.client, nil 1522 } 1523 1524 type myInitLocker struct { 1525 locked bool 1526 } 1527 1528 func (m *myInitLocker) Lock(_ context.Context, _ *clusterv1.Cluster, _ *clusterv1.Machine) bool { 1529 if !m.locked { 1530 m.locked = true 1531 return true 1532 } 1533 return false 1534 } 1535 1536 func (m *myInitLocker) Unlock(_ context.Context, _ *clusterv1.Cluster) bool { 1537 if m.locked { 1538 m.locked = false 1539 } 1540 return true 1541 }