sigs.k8s.io/cluster-api@v1.7.1/bootstrap/kubeadm/internal/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 ignition "github.com/flatcar/ignition/config/v2_3" 28 "github.com/go-logr/logr" 29 . "github.com/onsi/gomega" 30 corev1 "k8s.io/api/core/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/types" 33 bootstrapapi "k8s.io/cluster-bootstrap/token/api" 34 "k8s.io/utils/ptr" 35 ctrl "sigs.k8s.io/controller-runtime" 36 "sigs.k8s.io/controller-runtime/pkg/client" 37 "sigs.k8s.io/controller-runtime/pkg/client/fake" 38 "sigs.k8s.io/controller-runtime/pkg/log" 39 "sigs.k8s.io/controller-runtime/pkg/reconcile" 40 "sigs.k8s.io/yaml" 41 42 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 43 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" 44 bootstrapbuilder "sigs.k8s.io/cluster-api/bootstrap/kubeadm/internal/builder" 45 "sigs.k8s.io/cluster-api/controllers/remote" 46 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 47 "sigs.k8s.io/cluster-api/feature" 48 "sigs.k8s.io/cluster-api/internal/test/builder" 49 "sigs.k8s.io/cluster-api/util" 50 "sigs.k8s.io/cluster-api/util/conditions" 51 "sigs.k8s.io/cluster-api/util/patch" 52 "sigs.k8s.io/cluster-api/util/secret" 53 ) 54 55 // MachineToBootstrapMapFunc return kubeadm bootstrap configref name when configref exists. 56 func TestKubeadmConfigReconciler_MachineToBootstrapMapFuncReturn(t *testing.T) { 57 g := NewWithT(t) 58 cluster := builder.Cluster("my-cluster", metav1.NamespaceDefault).Build() 59 objs := []client.Object{cluster} 60 machineObjs := []client.Object{} 61 var expectedConfigName string 62 for i := 0; i < 3; i++ { 63 configName := fmt.Sprintf("my-config-%d", i) 64 m := builder.Machine(metav1.NamespaceDefault, fmt.Sprintf("my-machine-%d", i)). 65 WithVersion("v1.19.1"). 66 WithClusterName(cluster.Name). 67 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, "").Unstructured()). 68 Build() 69 if i == 1 { 70 c := newKubeadmConfig(metav1.NamespaceDefault, configName) 71 addKubeadmConfigToMachine(c, m) 72 objs = append(objs, m, c) 73 expectedConfigName = configName 74 } else { 75 objs = append(objs, m) 76 } 77 machineObjs = append(machineObjs, m) 78 } 79 fakeClient := fake.NewClientBuilder().WithObjects(objs...).Build() 80 reconciler := &KubeadmConfigReconciler{ 81 Client: fakeClient, 82 SecretCachingClient: fakeClient, 83 } 84 for i := 0; i < 3; i++ { 85 o := machineObjs[i] 86 configs := reconciler.MachineToBootstrapMapFunc(ctx, o) 87 if i == 1 { 88 g.Expect(configs[0].Name).To(Equal(expectedConfigName)) 89 } else { 90 g.Expect(configs[0].Name).To(Equal("")) 91 } 92 } 93 } 94 95 // Reconcile returns early if the kubeadm config is ready because it should never re-generate bootstrap data. 96 func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfKubeadmConfigIsReady(t *testing.T) { 97 g := NewWithT(t) 98 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1").Build() 99 cluster.Status.InfrastructureReady = true 100 machine := builder.Machine(metav1.NamespaceDefault, "m1").WithClusterName("cluster1").Build() 101 config := newKubeadmConfig(metav1.NamespaceDefault, "cfg") 102 addKubeadmConfigToMachine(config, machine) 103 104 config.Status.Ready = true 105 106 objects := []client.Object{ 107 cluster, 108 machine, 109 config, 110 } 111 myclient := fake.NewClientBuilder().WithObjects(objects...).Build() 112 113 k := &KubeadmConfigReconciler{ 114 Client: myclient, 115 SecretCachingClient: myclient, 116 } 117 118 request := ctrl.Request{ 119 NamespacedName: client.ObjectKey{ 120 Namespace: metav1.NamespaceDefault, 121 Name: "cfg", 122 }, 123 } 124 result, err := k.Reconcile(ctx, request) 125 g.Expect(err).ToNot(HaveOccurred()) 126 g.Expect(result.Requeue).To(BeFalse()) 127 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 128 } 129 130 // Reconcile should update owner references on bootstrap secrets on creation and update. 131 func TestKubeadmConfigReconciler_TestSecretOwnerReferenceReconciliation(t *testing.T) { 132 g := NewWithT(t) 133 134 clusterName := "my-cluster" 135 cluster := builder.Cluster(metav1.NamespaceDefault, clusterName).Build() 136 machine := builder.Machine(metav1.NamespaceDefault, "machine"). 137 WithVersion("v1.19.1"). 138 WithClusterName(clusterName). 139 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, "cfg").Unstructured()). 140 Build() 141 machine.Spec.Bootstrap.DataSecretName = ptr.To("something") 142 143 config := newKubeadmConfig(metav1.NamespaceDefault, "cfg") 144 config.SetOwnerReferences(util.EnsureOwnerRef(config.GetOwnerReferences(), metav1.OwnerReference{ 145 APIVersion: clusterv1.GroupVersion.String(), 146 Kind: "Machine", 147 Name: machine.Name, 148 UID: machine.UID, 149 })) 150 secret := &corev1.Secret{ 151 ObjectMeta: metav1.ObjectMeta{ 152 Name: config.Name, 153 Namespace: config.Namespace, 154 }, 155 Type: corev1.SecretTypeBootstrapToken, 156 } 157 config.Status.Ready = true 158 159 objects := []client.Object{ 160 config, 161 machine, 162 secret, 163 cluster, 164 } 165 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 166 167 k := &KubeadmConfigReconciler{ 168 Client: myclient, 169 SecretCachingClient: myclient, 170 } 171 172 request := ctrl.Request{ 173 NamespacedName: client.ObjectKey{ 174 Namespace: metav1.NamespaceDefault, 175 Name: "cfg", 176 }, 177 } 178 var err error 179 key := client.ObjectKeyFromObject(config) 180 actual := &corev1.Secret{} 181 182 t.Run("KubeadmConfig ownerReference is added on first reconcile", func(*testing.T) { 183 _, err = k.Reconcile(ctx, request) 184 g.Expect(err).ToNot(HaveOccurred()) 185 186 g.Expect(myclient.Get(ctx, key, actual)).To(Succeed()) 187 188 controllerOwner := metav1.GetControllerOf(actual) 189 g.Expect(controllerOwner).To(Not(BeNil())) 190 g.Expect(controllerOwner.Kind).To(Equal(config.Kind)) 191 g.Expect(controllerOwner.Name).To(Equal(config.Name)) 192 }) 193 194 t.Run("KubeadmConfig ownerReference re-reconciled without error", func(*testing.T) { 195 _, err = k.Reconcile(ctx, request) 196 g.Expect(err).ToNot(HaveOccurred()) 197 198 g.Expect(myclient.Get(ctx, key, actual)).To(Succeed()) 199 200 controllerOwner := metav1.GetControllerOf(actual) 201 g.Expect(controllerOwner).To(Not(BeNil())) 202 g.Expect(controllerOwner.Kind).To(Equal(config.Kind)) 203 g.Expect(controllerOwner.Name).To(Equal(config.Name)) 204 }) 205 t.Run("non-KubeadmConfig controller OwnerReference is replaced", func(*testing.T) { 206 g.Expect(myclient.Get(ctx, key, actual)).To(Succeed()) 207 208 actual.SetOwnerReferences([]metav1.OwnerReference{ 209 { 210 APIVersion: clusterv1.GroupVersion.String(), 211 Kind: "Machine", 212 Name: machine.Name, 213 UID: machine.UID, 214 Controller: ptr.To(true), 215 }}) 216 g.Expect(myclient.Update(ctx, actual)).To(Succeed()) 217 218 _, err = k.Reconcile(ctx, request) 219 g.Expect(err).ToNot(HaveOccurred()) 220 221 g.Expect(myclient.Get(ctx, key, actual)).To(Succeed()) 222 223 controllerOwner := metav1.GetControllerOf(actual) 224 g.Expect(controllerOwner).To(Not(BeNil())) 225 g.Expect(controllerOwner.Kind).To(Equal(config.Kind)) 226 g.Expect(controllerOwner.Name).To(Equal(config.Name)) 227 }) 228 } 229 230 // Reconcile returns nil if the referenced Machine cannot be found. 231 func TestKubeadmConfigReconciler_Reconcile_ReturnNilIfReferencedMachineIsNotFound(t *testing.T) { 232 g := NewWithT(t) 233 234 machine := builder.Machine(metav1.NamespaceDefault, "machine"). 235 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, "cfg").Unstructured()). 236 WithVersion("v1.19.1"). 237 Build() 238 config := newKubeadmConfig(metav1.NamespaceDefault, "cfg") 239 addKubeadmConfigToMachine(config, machine) 240 objects := []client.Object{ 241 // intentionally omitting machine 242 config, 243 } 244 myclient := fake.NewClientBuilder().WithObjects(objects...).Build() 245 246 k := &KubeadmConfigReconciler{ 247 Client: myclient, 248 SecretCachingClient: myclient, 249 } 250 251 request := ctrl.Request{ 252 NamespacedName: client.ObjectKey{ 253 Namespace: metav1.NamespaceDefault, 254 Name: "cfg", 255 }, 256 } 257 _, err := k.Reconcile(ctx, request) 258 g.Expect(err).ToNot(HaveOccurred()) 259 } 260 261 // If the machine has bootstrap data secret reference, there is no need to generate more bootstrap data. 262 func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfMachineHasDataSecretName(t *testing.T) { 263 g := NewWithT(t) 264 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1").Build() 265 cluster.Status.InfrastructureReady = true 266 267 machine := builder.Machine(metav1.NamespaceDefault, "machine"). 268 WithVersion("v1.19.1"). 269 WithClusterName("cluster1"). 270 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, "cfg").Unstructured()). 271 Build() 272 machine.Spec.Bootstrap.DataSecretName = ptr.To("something") 273 274 config := newKubeadmConfig(metav1.NamespaceDefault, "cfg") 275 addKubeadmConfigToMachine(config, machine) 276 objects := []client.Object{ 277 cluster, 278 machine, 279 config, 280 } 281 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 282 283 k := &KubeadmConfigReconciler{ 284 Client: myclient, 285 SecretCachingClient: myclient, 286 } 287 288 request := ctrl.Request{ 289 NamespacedName: client.ObjectKey{ 290 Namespace: metav1.NamespaceDefault, 291 Name: "cfg", 292 }, 293 } 294 result, err := k.Reconcile(ctx, request) 295 actual := &bootstrapv1.KubeadmConfig{} 296 g.Expect(myclient.Get(ctx, client.ObjectKey{Namespace: config.Namespace, Name: config.Name}, actual)).To(Succeed()) 297 g.Expect(err).ToNot(HaveOccurred()) 298 g.Expect(result.Requeue).To(BeFalse()) 299 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 300 assertHasTrueCondition(g, myclient, request, bootstrapv1.DataSecretAvailableCondition) 301 } 302 303 func TestKubeadmConfigReconciler_ReturnEarlyIfClusterInfraNotReady(t *testing.T) { 304 g := NewWithT(t) 305 306 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 307 machine := builder.Machine(metav1.NamespaceDefault, "machine"). 308 WithVersion("v1.19.1"). 309 WithClusterName(cluster.Name). 310 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, "cfg").Unstructured()). 311 Build() 312 config := newKubeadmConfig(metav1.NamespaceDefault, "cfg") 313 addKubeadmConfigToMachine(config, machine) 314 315 // cluster infra not ready 316 cluster.Status = clusterv1.ClusterStatus{ 317 InfrastructureReady: false, 318 } 319 320 objects := []client.Object{ 321 cluster, 322 machine, 323 config, 324 } 325 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 326 327 k := &KubeadmConfigReconciler{ 328 Client: myclient, 329 SecretCachingClient: myclient, 330 } 331 332 request := ctrl.Request{ 333 NamespacedName: client.ObjectKey{ 334 Namespace: metav1.NamespaceDefault, 335 Name: "cfg", 336 }, 337 } 338 339 expectedResult := reconcile.Result{} 340 actualResult, actualError := k.Reconcile(ctx, request) 341 g.Expect(actualResult).To(BeComparableTo(expectedResult)) 342 g.Expect(actualError).ToNot(HaveOccurred()) 343 assertHasFalseCondition(g, myclient, request, bootstrapv1.DataSecretAvailableCondition, clusterv1.ConditionSeverityInfo, bootstrapv1.WaitingForClusterInfrastructureReason) 344 } 345 346 // Return early If the owning machine does not have an associated cluster. 347 func TestKubeadmConfigReconciler_Reconcile_ReturnEarlyIfMachineHasNoCluster(t *testing.T) { 348 g := NewWithT(t) 349 machine := builder.Machine(metav1.NamespaceDefault, "machine"). 350 WithVersion("v1.19.1"). 351 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, "cfg").Unstructured()). 352 Build() 353 config := newKubeadmConfig(metav1.NamespaceDefault, "cfg") 354 addKubeadmConfigToMachine(config, machine) 355 356 objects := []client.Object{ 357 machine, 358 config, 359 } 360 myclient := fake.NewClientBuilder().WithObjects(objects...).Build() 361 362 k := &KubeadmConfigReconciler{ 363 Client: myclient, 364 SecretCachingClient: myclient, 365 } 366 367 request := ctrl.Request{ 368 NamespacedName: client.ObjectKey{ 369 Namespace: metav1.NamespaceDefault, 370 Name: "cfg", 371 }, 372 } 373 _, err := k.Reconcile(ctx, request) 374 g.Expect(err).ToNot(HaveOccurred()) 375 } 376 377 // This does not expect an error, hoping that the associated cluster will be created. 378 func TestKubeadmConfigReconciler_Reconcile_ReturnNilIfAssociatedClusterIsNotFound(t *testing.T) { 379 g := NewWithT(t) 380 381 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 382 machine := builder.Machine(metav1.NamespaceDefault, "machine"). 383 WithVersion("v1.19.1"). 384 WithClusterName(cluster.Name). 385 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, "cfg").Unstructured()). 386 Build() 387 config := newKubeadmConfig(metav1.NamespaceDefault, "cfg") 388 389 addKubeadmConfigToMachine(config, machine) 390 objects := []client.Object{ 391 // intentionally omitting cluster 392 machine, 393 config, 394 } 395 myclient := fake.NewClientBuilder().WithObjects(objects...).Build() 396 397 k := &KubeadmConfigReconciler{ 398 Client: myclient, 399 SecretCachingClient: myclient, 400 } 401 402 request := ctrl.Request{ 403 NamespacedName: client.ObjectKey{ 404 Namespace: metav1.NamespaceDefault, 405 Name: "cfg", 406 }, 407 } 408 _, err := k.Reconcile(ctx, request) 409 g.Expect(err).ToNot(HaveOccurred()) 410 } 411 412 // If the control plane isn't initialized then there is no cluster for either a worker or control plane node to join. 413 func TestKubeadmConfigReconciler_Reconcile_RequeueJoiningNodesIfControlPlaneNotInitialized(t *testing.T) { 414 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 415 cluster.Status.InfrastructureReady = true 416 417 workerMachine := newWorkerMachineForCluster(cluster) 418 workerJoinConfig := newWorkerJoinKubeadmConfig(metav1.NamespaceDefault, "worker-join-cfg") 419 addKubeadmConfigToMachine(workerJoinConfig, workerMachine) 420 421 controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine") 422 controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine.Namespace, "control-plane-join-cfg") 423 addKubeadmConfigToMachine(controlPlaneJoinConfig, controlPlaneJoinMachine) 424 425 testcases := []struct { 426 name string 427 request ctrl.Request 428 objects []client.Object 429 }{ 430 { 431 name: "requeue worker when control plane is not yet initialized", 432 request: ctrl.Request{ 433 NamespacedName: client.ObjectKey{ 434 Namespace: workerJoinConfig.Namespace, 435 Name: workerJoinConfig.Name, 436 }, 437 }, 438 objects: []client.Object{ 439 cluster, 440 workerMachine, 441 workerJoinConfig, 442 }, 443 }, 444 { 445 name: "requeue a secondary control plane when the control plane is not yet initialized", 446 request: ctrl.Request{ 447 NamespacedName: client.ObjectKey{ 448 Namespace: controlPlaneJoinConfig.Namespace, 449 Name: controlPlaneJoinConfig.Name, 450 }, 451 }, 452 objects: []client.Object{ 453 cluster, 454 controlPlaneJoinMachine, 455 controlPlaneJoinConfig, 456 }, 457 }, 458 } 459 for _, tc := range testcases { 460 t.Run(tc.name, func(t *testing.T) { 461 g := NewWithT(t) 462 463 myclient := fake.NewClientBuilder().WithObjects(tc.objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 464 465 k := &KubeadmConfigReconciler{ 466 Client: myclient, 467 SecretCachingClient: myclient, 468 KubeadmInitLock: &myInitLocker{}, 469 } 470 471 result, err := k.Reconcile(ctx, tc.request) 472 g.Expect(err).ToNot(HaveOccurred()) 473 g.Expect(result.Requeue).To(BeFalse()) 474 g.Expect(result.RequeueAfter).To(Equal(30 * time.Second)) 475 assertHasFalseCondition(g, myclient, tc.request, bootstrapv1.DataSecretAvailableCondition, clusterv1.ConditionSeverityInfo, clusterv1.WaitingForControlPlaneAvailableReason) 476 }) 477 } 478 } 479 480 // This generates cloud-config data but does not test the validity of it. 481 func TestKubeadmConfigReconciler_Reconcile_GenerateCloudConfigData(t *testing.T) { 482 g := NewWithT(t) 483 484 configName := "control-plane-init-cfg" 485 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 486 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "validhost", Port: 6443} 487 cluster.Status.InfrastructureReady = true 488 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 489 490 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 491 controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine.Namespace, configName) 492 controlPlaneInitConfig.Spec.JoinConfiguration = &bootstrapv1.JoinConfiguration{} 493 controlPlaneInitConfig.Spec.JoinConfiguration.Discovery.BootstrapToken = &bootstrapv1.BootstrapTokenDiscovery{ 494 CACertHashes: []string{"...."}, 495 } 496 497 addKubeadmConfigToMachine(controlPlaneInitConfig, controlPlaneInitMachine) 498 499 objects := []client.Object{ 500 cluster, 501 controlPlaneInitMachine, 502 controlPlaneInitConfig, 503 } 504 objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...) 505 506 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 507 508 k := &KubeadmConfigReconciler{ 509 Client: myclient, 510 SecretCachingClient: myclient, 511 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, myclient, myclient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 512 KubeadmInitLock: &myInitLocker{}, 513 } 514 515 request := ctrl.Request{ 516 NamespacedName: client.ObjectKey{ 517 Namespace: metav1.NamespaceDefault, 518 Name: "control-plane-init-cfg", 519 }, 520 } 521 s := &corev1.Secret{} 522 g.Expect(myclient.Get(ctx, client.ObjectKey{Namespace: metav1.NamespaceDefault, Name: configName}, s)).ToNot(Succeed()) 523 524 result, err := k.Reconcile(ctx, request) 525 g.Expect(err).ToNot(HaveOccurred()) 526 g.Expect(result.Requeue).To(BeFalse()) 527 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 528 529 cfg, err := getKubeadmConfig(myclient, "control-plane-init-cfg", metav1.NamespaceDefault) 530 g.Expect(err).ToNot(HaveOccurred()) 531 g.Expect(cfg.Status.Ready).To(BeTrue()) 532 g.Expect(cfg.Status.DataSecretName).NotTo(BeNil()) 533 g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil()) 534 assertHasTrueCondition(g, myclient, request, bootstrapv1.CertificatesAvailableCondition) 535 assertHasTrueCondition(g, myclient, request, bootstrapv1.DataSecretAvailableCondition) 536 537 // Expect the Secret to exist, and for it to contain some data under the "value" key. 538 g.Expect(myclient.Get(ctx, client.ObjectKey{Namespace: metav1.NamespaceDefault, Name: configName}, s)).To(Succeed()) 539 g.Expect(s.Data["value"]).ToNot(BeEmpty()) 540 // Ensure that we don't fail trying to refresh any bootstrap tokens 541 _, err = k.Reconcile(ctx, request) 542 g.Expect(err).ToNot(HaveOccurred()) 543 } 544 545 // If a control plane has no JoinConfiguration, then we will create a default and no error will occur. 546 func TestKubeadmConfigReconciler_Reconcile_ErrorIfJoiningControlPlaneHasInvalidConfiguration(t *testing.T) { 547 g := NewWithT(t) 548 // TODO: extract this kind of code into a setup function that puts the state of objects into an initialized controlplane (implies secrets exist) 549 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 550 cluster.Status.InfrastructureReady = true 551 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 552 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "100.105.150.1", Port: 6443} 553 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 554 controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine.Namespace, "control-plane-init-cfg") 555 addKubeadmConfigToMachine(controlPlaneInitConfig, controlPlaneInitMachine) 556 557 controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine") 558 controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine.Namespace, "control-plane-join-cfg") 559 controlPlaneJoinConfig.Spec.JoinConfiguration.ControlPlane = nil // Makes controlPlaneJoinConfig invalid for a control plane machine 560 addKubeadmConfigToMachine(controlPlaneJoinConfig, controlPlaneJoinMachine) 561 562 objects := []client.Object{ 563 cluster, 564 controlPlaneJoinMachine, 565 controlPlaneJoinConfig, 566 } 567 objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...) 568 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 569 570 k := &KubeadmConfigReconciler{ 571 Client: myclient, 572 SecretCachingClient: myclient, 573 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, myclient, myclient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 574 KubeadmInitLock: &myInitLocker{}, 575 } 576 577 request := ctrl.Request{ 578 NamespacedName: client.ObjectKey{ 579 Namespace: metav1.NamespaceDefault, 580 Name: controlPlaneJoinConfig.Name, 581 }, 582 } 583 _, err := k.Reconcile(ctx, request) 584 g.Expect(err).ToNot(HaveOccurred()) 585 actualConfig := &bootstrapv1.KubeadmConfig{} 586 g.Expect(myclient.Get(ctx, client.ObjectKey{Namespace: controlPlaneJoinConfig.Namespace, Name: controlPlaneJoinConfig.Name}, actualConfig)).To(Succeed()) 587 assertHasTrueCondition(g, myclient, request, bootstrapv1.DataSecretAvailableCondition) 588 assertHasTrueCondition(g, myclient, request, bootstrapv1.CertificatesAvailableCondition) 589 } 590 591 // If there is no APIEndpoint but everything is ready then requeue in hopes of a new APIEndpoint showing up eventually. 592 func TestKubeadmConfigReconciler_Reconcile_RequeueIfControlPlaneIsMissingAPIEndpoints(t *testing.T) { 593 g := NewWithT(t) 594 595 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 596 cluster.Status.InfrastructureReady = true 597 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 598 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 599 controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine.Namespace, "control-plane-init-cfg") 600 addKubeadmConfigToMachine(controlPlaneInitConfig, controlPlaneInitMachine) 601 602 workerMachine := newWorkerMachineForCluster(cluster) 603 workerJoinConfig := newWorkerJoinKubeadmConfig(metav1.NamespaceDefault, "worker-join-cfg") 604 addKubeadmConfigToMachine(workerJoinConfig, workerMachine) 605 606 objects := []client.Object{ 607 cluster, 608 workerMachine, 609 workerJoinConfig, 610 } 611 objects = append(objects, createSecrets(t, cluster, controlPlaneInitConfig)...) 612 613 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 614 615 k := &KubeadmConfigReconciler{ 616 Client: myclient, 617 SecretCachingClient: myclient, 618 KubeadmInitLock: &myInitLocker{}, 619 } 620 621 request := ctrl.Request{ 622 NamespacedName: client.ObjectKey{ 623 Namespace: metav1.NamespaceDefault, 624 Name: "worker-join-cfg", 625 }, 626 } 627 result, err := k.Reconcile(ctx, request) 628 g.Expect(err).ToNot(HaveOccurred()) 629 g.Expect(result.Requeue).To(BeFalse()) 630 g.Expect(result.RequeueAfter).To(Equal(10 * time.Second)) 631 632 actualConfig := &bootstrapv1.KubeadmConfig{} 633 g.Expect(myclient.Get(ctx, client.ObjectKey{Namespace: workerJoinConfig.Namespace, Name: workerJoinConfig.Name}, actualConfig)).To(Succeed()) 634 635 // At this point the DataSecretAvailableCondition should not be set. CertificatesAvailableCondition should be true. 636 g.Expect(conditions.Get(actualConfig, bootstrapv1.DataSecretAvailableCondition)).To(BeNil()) 637 assertHasTrueCondition(g, myclient, request, bootstrapv1.CertificatesAvailableCondition) 638 } 639 640 func TestReconcileIfJoinCertificatesAvailableConditioninNodesAndControlPlaneIsReady(t *testing.T) { 641 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 642 cluster.Status.InfrastructureReady = true 643 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 644 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "100.105.150.1", Port: 6443} 645 646 useCases := []struct { 647 name string 648 machine *clusterv1.Machine 649 configName string 650 configBuilder func(string, string) *bootstrapv1.KubeadmConfig 651 }{ 652 { 653 name: "Join a worker node with a fully compiled kubeadm config object", 654 machine: newWorkerMachineForCluster(cluster), 655 configName: "worker-join-cfg", 656 configBuilder: newWorkerJoinKubeadmConfig, 657 }, 658 { 659 name: "Join a worker node with an empty kubeadm config object (defaults apply)", 660 machine: newWorkerMachineForCluster(cluster), 661 configName: "worker-join-cfg", 662 configBuilder: newKubeadmConfig, 663 }, 664 { 665 name: "Join a control plane node with a fully compiled kubeadm config object", 666 machine: newControlPlaneMachine(cluster, "control-plane-join-machine"), 667 configName: "control-plane-join-cfg", 668 configBuilder: newControlPlaneJoinKubeadmConfig, 669 }, 670 { 671 name: "Join a control plane node with an empty kubeadm config object (defaults apply)", 672 machine: newControlPlaneMachine(cluster, "control-plane-join-machine"), 673 configName: "control-plane-join-cfg", 674 configBuilder: newKubeadmConfig, 675 }, 676 } 677 678 for _, rt := range useCases { 679 rt := rt // pin! 680 t.Run(rt.name, func(t *testing.T) { 681 g := NewWithT(t) 682 683 config := rt.configBuilder(rt.machine.Namespace, rt.configName) 684 addKubeadmConfigToMachine(config, rt.machine) 685 686 objects := []client.Object{ 687 cluster, 688 rt.machine, 689 config, 690 } 691 objects = append(objects, createSecrets(t, cluster, config)...) 692 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 693 k := &KubeadmConfigReconciler{ 694 Client: myclient, 695 SecretCachingClient: myclient, 696 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, myclient, myclient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 697 KubeadmInitLock: &myInitLocker{}, 698 } 699 700 request := ctrl.Request{ 701 NamespacedName: client.ObjectKey{ 702 Namespace: config.GetNamespace(), 703 Name: rt.configName, 704 }, 705 } 706 result, err := k.Reconcile(ctx, request) 707 g.Expect(err).ToNot(HaveOccurred()) 708 g.Expect(result.Requeue).To(BeFalse()) 709 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 710 711 cfg, err := getKubeadmConfig(myclient, rt.configName, metav1.NamespaceDefault) 712 g.Expect(err).ToNot(HaveOccurred()) 713 g.Expect(cfg.Status.Ready).To(BeTrue()) 714 g.Expect(cfg.Status.DataSecretName).NotTo(BeNil()) 715 g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil()) 716 assertHasTrueCondition(g, myclient, request, bootstrapv1.DataSecretAvailableCondition) 717 718 l := &corev1.SecretList{} 719 err = myclient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem))) 720 g.Expect(err).ToNot(HaveOccurred()) 721 g.Expect(l.Items).To(HaveLen(1)) 722 }) 723 } 724 } 725 726 func TestReconcileIfJoinNodePoolsAndControlPlaneIsReady(t *testing.T) { 727 _ = feature.MutableGates.Set("MachinePool=true") 728 729 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 730 cluster.Status.InfrastructureReady = true 731 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 732 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "100.105.150.1", Port: 6443} 733 734 useCases := []struct { 735 name string 736 machinePool *expv1.MachinePool 737 configName string 738 configBuilder func(string, string) *bootstrapv1.KubeadmConfig 739 }{ 740 { 741 name: "Join a worker node with a fully compiled kubeadm config object", 742 machinePool: newWorkerMachinePoolForCluster(cluster), 743 configName: "workerpool-join-cfg", 744 configBuilder: func(namespace, _ string) *bootstrapv1.KubeadmConfig { 745 return newWorkerJoinKubeadmConfig(namespace, "workerpool-join-cfg") 746 }, 747 }, 748 { 749 name: "Join a worker node with an empty kubeadm config object (defaults apply)", 750 machinePool: newWorkerMachinePoolForCluster(cluster), 751 configName: "workerpool-join-cfg", 752 configBuilder: newKubeadmConfig, 753 }, 754 } 755 756 for _, rt := range useCases { 757 rt := rt // pin! 758 t.Run(rt.name, func(t *testing.T) { 759 g := NewWithT(t) 760 761 config := rt.configBuilder(rt.machinePool.Namespace, rt.configName) 762 addKubeadmConfigToMachinePool(config, rt.machinePool) 763 764 objects := []client.Object{ 765 cluster, 766 rt.machinePool, 767 config, 768 } 769 objects = append(objects, createSecrets(t, cluster, config)...) 770 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 771 k := &KubeadmConfigReconciler{ 772 Client: myclient, 773 SecretCachingClient: myclient, 774 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, myclient, myclient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 775 KubeadmInitLock: &myInitLocker{}, 776 } 777 778 request := ctrl.Request{ 779 NamespacedName: client.ObjectKey{ 780 Namespace: config.GetNamespace(), 781 Name: rt.configName, 782 }, 783 } 784 result, err := k.Reconcile(ctx, request) 785 g.Expect(err).ToNot(HaveOccurred()) 786 g.Expect(result.Requeue).To(BeFalse()) 787 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 788 789 cfg, err := getKubeadmConfig(myclient, rt.configName, metav1.NamespaceDefault) 790 g.Expect(err).ToNot(HaveOccurred()) 791 g.Expect(cfg.Status.Ready).To(BeTrue()) 792 g.Expect(cfg.Status.DataSecretName).NotTo(BeNil()) 793 g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil()) 794 795 l := &corev1.SecretList{} 796 err = myclient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem))) 797 g.Expect(err).ToNot(HaveOccurred()) 798 g.Expect(l.Items).To(HaveLen(1)) 799 }) 800 } 801 } 802 803 // Ensure bootstrap data is generated in the correct format based on the format specified in the 804 // KubeadmConfig resource. 805 func TestBootstrapDataFormat(t *testing.T) { 806 testcases := []struct { 807 name string 808 isWorker bool 809 format bootstrapv1.Format 810 clusterInitialized bool 811 }{ 812 { 813 name: "cloud-config init config", 814 format: bootstrapv1.CloudConfig, 815 }, 816 { 817 name: "Ignition init config", 818 format: bootstrapv1.Ignition, 819 }, 820 { 821 name: "Ignition control plane join config", 822 format: bootstrapv1.Ignition, 823 clusterInitialized: true, 824 }, 825 { 826 name: "Ignition worker join config", 827 isWorker: true, 828 format: bootstrapv1.Ignition, 829 clusterInitialized: true, 830 }, 831 { 832 name: "Empty format field", 833 }, 834 } 835 836 for _, tc := range testcases { 837 t.Run(tc.name, func(t *testing.T) { 838 g := NewWithT(t) 839 840 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 841 cluster.Status.InfrastructureReady = true 842 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "100.105.150.1", Port: 6443} 843 if tc.clusterInitialized { 844 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 845 } 846 847 var machine *clusterv1.Machine 848 var config *bootstrapv1.KubeadmConfig 849 var configName string 850 if tc.isWorker { 851 machine = newWorkerMachineForCluster(cluster) 852 configName = "worker-join-cfg" 853 config = newWorkerJoinKubeadmConfig(metav1.NamespaceDefault, configName) 854 addKubeadmConfigToMachine(config, machine) 855 } else { 856 machine = newControlPlaneMachine(cluster, "machine") 857 configName = "cfg" 858 config = newControlPlaneInitKubeadmConfig(metav1.NamespaceDefault, configName) 859 addKubeadmConfigToMachine(config, machine) 860 } 861 config.Spec.Format = tc.format 862 863 objects := []client.Object{ 864 cluster, 865 machine, 866 config, 867 } 868 objects = append(objects, createSecrets(t, cluster, config)...) 869 870 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 871 872 k := &KubeadmConfigReconciler{ 873 Client: myclient, 874 SecretCachingClient: myclient, 875 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, myclient, myclient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 876 KubeadmInitLock: &myInitLocker{}, 877 } 878 request := ctrl.Request{ 879 NamespacedName: client.ObjectKey{ 880 Namespace: metav1.NamespaceDefault, 881 Name: configName, 882 }, 883 } 884 885 // Reconcile the KubeadmConfig resource. 886 _, err := k.Reconcile(ctx, request) 887 g.Expect(err).ToNot(HaveOccurred()) 888 889 // Verify the KubeadmConfig resource state is correct. 890 cfg, err := getKubeadmConfig(myclient, configName, metav1.NamespaceDefault) 891 g.Expect(err).ToNot(HaveOccurred()) 892 g.Expect(cfg.Status.Ready).To(BeTrue()) 893 g.Expect(cfg.Status.DataSecretName).NotTo(BeNil()) 894 895 // Read the secret containing the bootstrap data which was generated by the 896 // KubeadmConfig controller. 897 key := client.ObjectKey{ 898 Namespace: metav1.NamespaceDefault, 899 Name: *cfg.Status.DataSecretName, 900 } 901 secret := &corev1.Secret{} 902 err = myclient.Get(ctx, key, secret) 903 g.Expect(err).ToNot(HaveOccurred()) 904 905 // Verify the format field of the bootstrap data secret is correct. 906 g.Expect(string(secret.Data["format"])).To(Equal(string(tc.format))) 907 908 // Verify the bootstrap data value is in the correct format. 909 data := secret.Data["value"] 910 switch tc.format { 911 case bootstrapv1.CloudConfig, "": 912 // Verify the bootstrap data is valid YAML. 913 // TODO: Verify the YAML document is valid cloud-config? 914 var out interface{} 915 err = yaml.Unmarshal(data, &out) 916 g.Expect(err).ToNot(HaveOccurred()) 917 case bootstrapv1.Ignition: 918 // Verify the bootstrap data is valid Ignition. 919 _, reports, err := ignition.Parse(data) 920 g.Expect(err).ToNot(HaveOccurred()) 921 g.Expect(reports.IsFatal()).NotTo(BeTrue()) 922 } 923 }) 924 } 925 } 926 927 // during kubeadmconfig reconcile it is possible that bootstrap secret gets created 928 // but kubeadmconfig is not patched, do not error if secret already exists. 929 // ignore the alreadyexists error and update the status to ready. 930 func TestKubeadmConfigSecretCreatedStatusNotPatched(t *testing.T) { 931 g := NewWithT(t) 932 933 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 934 cluster.Status.InfrastructureReady = true 935 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 936 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "100.105.150.1", Port: 6443} 937 938 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 939 initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine.Namespace, "control-plane-init-config") 940 addKubeadmConfigToMachine(initConfig, controlPlaneInitMachine) 941 942 workerMachine := newWorkerMachineForCluster(cluster) 943 workerJoinConfig := newWorkerJoinKubeadmConfig(metav1.NamespaceDefault, "worker-join-cfg") 944 addKubeadmConfigToMachine(workerJoinConfig, workerMachine) 945 objects := []client.Object{ 946 cluster, 947 workerMachine, 948 workerJoinConfig, 949 } 950 951 objects = append(objects, createSecrets(t, cluster, initConfig)...) 952 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 953 k := &KubeadmConfigReconciler{ 954 Client: myclient, 955 SecretCachingClient: myclient, 956 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, myclient, myclient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 957 KubeadmInitLock: &myInitLocker{}, 958 } 959 request := ctrl.Request{ 960 NamespacedName: client.ObjectKey{ 961 Namespace: metav1.NamespaceDefault, 962 Name: "worker-join-cfg", 963 }, 964 } 965 secret := &corev1.Secret{ 966 ObjectMeta: metav1.ObjectMeta{ 967 Name: workerJoinConfig.Name, 968 Namespace: workerJoinConfig.Namespace, 969 Labels: map[string]string{ 970 clusterv1.ClusterNameLabel: cluster.Name, 971 }, 972 OwnerReferences: []metav1.OwnerReference{ 973 { 974 APIVersion: bootstrapv1.GroupVersion.String(), 975 Kind: "KubeadmConfig", 976 Name: workerJoinConfig.Name, 977 UID: workerJoinConfig.UID, 978 Controller: ptr.To(true), 979 }, 980 }, 981 }, 982 Data: map[string][]byte{ 983 "value": nil, 984 }, 985 Type: clusterv1.ClusterSecretType, 986 } 987 988 err := myclient.Create(ctx, secret) 989 g.Expect(err).ToNot(HaveOccurred()) 990 result, err := k.Reconcile(ctx, request) 991 g.Expect(err).ToNot(HaveOccurred()) 992 g.Expect(result.Requeue).To(BeFalse()) 993 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 994 995 cfg, err := getKubeadmConfig(myclient, "worker-join-cfg", metav1.NamespaceDefault) 996 g.Expect(err).ToNot(HaveOccurred()) 997 g.Expect(cfg.Status.Ready).To(BeTrue()) 998 g.Expect(cfg.Status.DataSecretName).NotTo(BeNil()) 999 g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil()) 1000 } 1001 1002 func TestBootstrapTokenTTLExtension(t *testing.T) { 1003 g := NewWithT(t) 1004 1005 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 1006 cluster.Status.InfrastructureReady = true 1007 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 1008 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "100.105.150.1", Port: 6443} 1009 1010 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 1011 initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine.Namespace, "control-plane-init-config") 1012 addKubeadmConfigToMachine(initConfig, controlPlaneInitMachine) 1013 1014 workerMachine := newWorkerMachineForCluster(cluster) 1015 workerJoinConfig := newWorkerJoinKubeadmConfig(metav1.NamespaceDefault, "worker-join-cfg") 1016 addKubeadmConfigToMachine(workerJoinConfig, workerMachine) 1017 1018 controlPlaneJoinMachine := newControlPlaneMachine(cluster, "control-plane-join-machine") 1019 controlPlaneJoinConfig := newControlPlaneJoinKubeadmConfig(controlPlaneJoinMachine.Namespace, "control-plane-join-cfg") 1020 addKubeadmConfigToMachine(controlPlaneJoinConfig, controlPlaneJoinMachine) 1021 objects := []client.Object{ 1022 cluster, 1023 workerMachine, 1024 workerJoinConfig, 1025 controlPlaneJoinMachine, 1026 controlPlaneJoinConfig, 1027 } 1028 1029 objects = append(objects, createSecrets(t, cluster, initConfig)...) 1030 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}, &clusterv1.Machine{}).Build() 1031 remoteClient := fake.NewClientBuilder().Build() 1032 k := &KubeadmConfigReconciler{ 1033 Client: myclient, 1034 SecretCachingClient: myclient, 1035 KubeadmInitLock: &myInitLocker{}, 1036 TokenTTL: DefaultTokenTTL, 1037 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, remoteClient, remoteClient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 1038 } 1039 request := ctrl.Request{ 1040 NamespacedName: client.ObjectKey{ 1041 Namespace: metav1.NamespaceDefault, 1042 Name: "worker-join-cfg", 1043 }, 1044 } 1045 result, err := k.Reconcile(ctx, request) 1046 g.Expect(err).ToNot(HaveOccurred()) 1047 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1048 1049 cfg, err := getKubeadmConfig(myclient, "worker-join-cfg", metav1.NamespaceDefault) 1050 g.Expect(err).ToNot(HaveOccurred()) 1051 g.Expect(cfg.Status.Ready).To(BeTrue()) 1052 g.Expect(cfg.Status.DataSecretName).NotTo(BeNil()) 1053 g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil()) 1054 1055 request = ctrl.Request{ 1056 NamespacedName: client.ObjectKey{ 1057 Namespace: metav1.NamespaceDefault, 1058 Name: "control-plane-join-cfg", 1059 }, 1060 } 1061 result, err = k.Reconcile(ctx, request) 1062 g.Expect(err).ToNot(HaveOccurred()) 1063 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1064 1065 cfg, err = getKubeadmConfig(myclient, "control-plane-join-cfg", metav1.NamespaceDefault) 1066 g.Expect(err).ToNot(HaveOccurred()) 1067 g.Expect(cfg.Status.Ready).To(BeTrue()) 1068 g.Expect(cfg.Status.DataSecretName).NotTo(BeNil()) 1069 g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil()) 1070 1071 l := &corev1.SecretList{} 1072 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1073 g.Expect(l.Items).To(HaveLen(2)) // control plane vs. worker 1074 1075 t.Log("Ensure that the token secret is not updated while it's still fresh") 1076 tokenExpires := make([][]byte, len(l.Items)) 1077 1078 for i, item := range l.Items { 1079 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1080 } 1081 1082 for _, req := range []ctrl.Request{ 1083 { 1084 NamespacedName: client.ObjectKey{ 1085 Namespace: metav1.NamespaceDefault, 1086 Name: "worker-join-cfg", 1087 }, 1088 }, 1089 { 1090 NamespacedName: client.ObjectKey{ 1091 Namespace: metav1.NamespaceDefault, 1092 Name: "control-plane-join-cfg", 1093 }, 1094 }, 1095 } { 1096 result, err := k.Reconcile(ctx, req) 1097 g.Expect(err).ToNot(HaveOccurred()) 1098 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1099 } 1100 1101 l = &corev1.SecretList{} 1102 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1103 g.Expect(l.Items).To(HaveLen(2)) 1104 1105 for i, item := range l.Items { 1106 // No refresh should have happened since no time passed and the token is therefore still fresh 1107 g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeTrue()) 1108 } 1109 1110 t.Log("Ensure that the token secret is updated if expiration time is soon") 1111 1112 for i, item := range l.Items { 1113 // Simulate that expiry time is only TTL/2 from now. This should trigger a refresh. 1114 item.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(time.Now().UTC().Add(k.TokenTTL / 2).Format(time.RFC3339)) 1115 g.Expect(remoteClient.Update(ctx, &l.Items[i])).To(Succeed()) 1116 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1117 } 1118 1119 for _, req := range []ctrl.Request{ 1120 { 1121 NamespacedName: client.ObjectKey{ 1122 Namespace: metav1.NamespaceDefault, 1123 Name: "worker-join-cfg", 1124 }, 1125 }, 1126 { 1127 NamespacedName: client.ObjectKey{ 1128 Namespace: metav1.NamespaceDefault, 1129 Name: "control-plane-join-cfg", 1130 }, 1131 }, 1132 } { 1133 result, err := k.Reconcile(ctx, req) 1134 g.Expect(err).ToNot(HaveOccurred()) 1135 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1136 } 1137 1138 l = &corev1.SecretList{} 1139 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1140 g.Expect(l.Items).To(HaveLen(2)) 1141 1142 for i, item := range l.Items { 1143 // Refresh should have happened since expiration is soon 1144 g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeFalse()) 1145 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1146 } 1147 1148 t.Log("If infrastructure is marked ready, the token should still be refreshed") 1149 1150 for i, item := range l.Items { 1151 // Simulate that expiry time is only TTL/2 from now. This should trigger a refresh. 1152 item.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(time.Now().UTC().Add(k.TokenTTL / 2).Format(time.RFC3339)) 1153 g.Expect(remoteClient.Update(ctx, &l.Items[i])).To(Succeed()) 1154 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1155 } 1156 1157 patchHelper, err := patch.NewHelper(workerMachine, myclient) 1158 g.Expect(err).ShouldNot(HaveOccurred()) 1159 workerMachine.Status.InfrastructureReady = true 1160 g.Expect(patchHelper.Patch(ctx, workerMachine)).To(Succeed()) 1161 1162 patchHelper, err = patch.NewHelper(controlPlaneJoinMachine, myclient) 1163 g.Expect(err).ShouldNot(HaveOccurred()) 1164 controlPlaneJoinMachine.Status.InfrastructureReady = true 1165 g.Expect(patchHelper.Patch(ctx, controlPlaneJoinMachine)).To(Succeed()) 1166 1167 for _, req := range []ctrl.Request{ 1168 { 1169 NamespacedName: client.ObjectKey{ 1170 Namespace: metav1.NamespaceDefault, 1171 Name: "worker-join-cfg", 1172 }, 1173 }, 1174 { 1175 NamespacedName: client.ObjectKey{ 1176 Namespace: metav1.NamespaceDefault, 1177 Name: "control-plane-join-cfg", 1178 }, 1179 }, 1180 } { 1181 result, err := k.Reconcile(ctx, req) 1182 g.Expect(err).ToNot(HaveOccurred()) 1183 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1184 } 1185 1186 l = &corev1.SecretList{} 1187 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1188 g.Expect(l.Items).To(HaveLen(2)) 1189 1190 for i, item := range l.Items { 1191 // Refresh should have happened since expiration is soon, even if infrastructure is ready 1192 g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeFalse()) 1193 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1194 } 1195 1196 t.Log("When the Nodes have actually joined the cluster and we get a nodeRef, no more refresh should happen") 1197 1198 for i, item := range l.Items { 1199 // Simulate that expiry time is only TTL/2 from now. This would normally trigger a refresh. 1200 item.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(time.Now().UTC().Add(k.TokenTTL / 2).Format(time.RFC3339)) 1201 g.Expect(remoteClient.Update(ctx, &l.Items[i])).To(Succeed()) 1202 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1203 } 1204 1205 patchHelper, err = patch.NewHelper(workerMachine, myclient) 1206 g.Expect(err).ShouldNot(HaveOccurred()) 1207 workerMachine.Status.NodeRef = &corev1.ObjectReference{ 1208 APIVersion: "v1", 1209 Kind: "Node", 1210 Name: "worker-node", 1211 } 1212 g.Expect(patchHelper.Patch(ctx, workerMachine)).To(Succeed()) 1213 1214 patchHelper, err = patch.NewHelper(controlPlaneJoinMachine, myclient) 1215 g.Expect(err).ShouldNot(HaveOccurred()) 1216 controlPlaneJoinMachine.Status.NodeRef = &corev1.ObjectReference{ 1217 APIVersion: "v1", 1218 Kind: "Node", 1219 Name: "control-plane-node", 1220 } 1221 g.Expect(patchHelper.Patch(ctx, controlPlaneJoinMachine)).To(Succeed()) 1222 1223 for _, req := range []ctrl.Request{ 1224 { 1225 NamespacedName: client.ObjectKey{ 1226 Namespace: metav1.NamespaceDefault, 1227 Name: "worker-join-cfg", 1228 }, 1229 }, 1230 { 1231 NamespacedName: client.ObjectKey{ 1232 Namespace: metav1.NamespaceDefault, 1233 Name: "control-plane-join-cfg", 1234 }, 1235 }, 1236 } { 1237 result, err := k.Reconcile(ctx, req) 1238 g.Expect(err).ToNot(HaveOccurred()) 1239 g.Expect(result.Requeue).To(BeFalse()) 1240 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 1241 } 1242 1243 l = &corev1.SecretList{} 1244 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1245 g.Expect(l.Items).To(HaveLen(2)) 1246 1247 for i, item := range l.Items { 1248 g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeTrue()) 1249 } 1250 } 1251 1252 func TestBootstrapTokenRotationMachinePool(t *testing.T) { 1253 _ = feature.MutableGates.Set("MachinePool=true") 1254 g := NewWithT(t) 1255 1256 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 1257 cluster.Status.InfrastructureReady = true 1258 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 1259 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{Host: "100.105.150.1", Port: 6443} 1260 1261 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 1262 initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine.Namespace, "control-plane-init-config") 1263 1264 addKubeadmConfigToMachine(initConfig, controlPlaneInitMachine) 1265 1266 workerMachinePool := newWorkerMachinePoolForCluster(cluster) 1267 workerJoinConfig := newWorkerJoinKubeadmConfig(workerMachinePool.Namespace, "workerpool-join-cfg") 1268 addKubeadmConfigToMachinePool(workerJoinConfig, workerMachinePool) 1269 objects := []client.Object{ 1270 cluster, 1271 workerMachinePool, 1272 workerJoinConfig, 1273 } 1274 1275 objects = append(objects, createSecrets(t, cluster, initConfig)...) 1276 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}, &expv1.MachinePool{}).Build() 1277 remoteClient := fake.NewClientBuilder().Build() 1278 k := &KubeadmConfigReconciler{ 1279 Client: myclient, 1280 SecretCachingClient: myclient, 1281 KubeadmInitLock: &myInitLocker{}, 1282 TokenTTL: DefaultTokenTTL, 1283 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, remoteClient, remoteClient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 1284 } 1285 request := ctrl.Request{ 1286 NamespacedName: client.ObjectKey{ 1287 Namespace: metav1.NamespaceDefault, 1288 Name: "workerpool-join-cfg", 1289 }, 1290 } 1291 result, err := k.Reconcile(ctx, request) 1292 g.Expect(err).ToNot(HaveOccurred()) 1293 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1294 1295 cfg, err := getKubeadmConfig(myclient, "workerpool-join-cfg", metav1.NamespaceDefault) 1296 g.Expect(err).ToNot(HaveOccurred()) 1297 g.Expect(cfg.Status.Ready).To(BeTrue()) 1298 g.Expect(cfg.Status.DataSecretName).NotTo(BeNil()) 1299 g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil()) 1300 1301 l := &corev1.SecretList{} 1302 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1303 g.Expect(l.Items).To(HaveLen(1)) 1304 1305 t.Log("Ensure that the token secret is not updated while it's still fresh") 1306 tokenExpires := make([][]byte, len(l.Items)) 1307 1308 for i, item := range l.Items { 1309 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1310 } 1311 1312 result, err = k.Reconcile(ctx, request) 1313 g.Expect(err).ToNot(HaveOccurred()) 1314 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1315 1316 l = &corev1.SecretList{} 1317 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1318 g.Expect(l.Items).To(HaveLen(1)) 1319 1320 for i, item := range l.Items { 1321 // No refresh should have happened since no time passed and the token is therefore still fresh 1322 g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeTrue()) 1323 } 1324 1325 t.Log("Ensure that the token secret is updated if expiration time is soon") 1326 1327 for i, item := range l.Items { 1328 // Simulate that expiry time is only TTL*3/4 from now. This should trigger a refresh. 1329 item.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(time.Now().UTC().Add(k.TokenTTL * 3 / 4).Format(time.RFC3339)) 1330 g.Expect(remoteClient.Update(ctx, &l.Items[i])).To(Succeed()) 1331 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1332 } 1333 1334 result, err = k.Reconcile(ctx, request) 1335 g.Expect(err).ToNot(HaveOccurred()) 1336 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1337 1338 l = &corev1.SecretList{} 1339 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1340 g.Expect(l.Items).To(HaveLen(1)) 1341 1342 for i, item := range l.Items { 1343 // Refresh should have happened since expiration is soon 1344 g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeFalse()) 1345 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1346 } 1347 1348 t.Log("If infrastructure is marked ready, the token should still be refreshed") 1349 1350 for i, item := range l.Items { 1351 // Simulate that expiry time is only TTL*3/4 from now. This should trigger a refresh. 1352 item.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(time.Now().UTC().Add(k.TokenTTL * 3 / 4).Format(time.RFC3339)) 1353 g.Expect(remoteClient.Update(ctx, &l.Items[i])).To(Succeed()) 1354 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1355 } 1356 1357 patchHelper, err := patch.NewHelper(workerMachinePool, myclient) 1358 g.Expect(err).ShouldNot(HaveOccurred()) 1359 workerMachinePool.Status.InfrastructureReady = true 1360 g.Expect(patchHelper.Patch(ctx, workerMachinePool, patch.WithStatusObservedGeneration{})).To(Succeed()) 1361 1362 result, err = k.Reconcile(ctx, request) 1363 g.Expect(err).ToNot(HaveOccurred()) 1364 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1365 1366 l = &corev1.SecretList{} 1367 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1368 g.Expect(l.Items).To(HaveLen(1)) 1369 1370 for i, item := range l.Items { 1371 // Refresh should have happened since expiration is soon, even if infrastructure is ready 1372 g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeFalse()) 1373 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1374 } 1375 1376 t.Log("When the Nodes have actually joined the cluster and we get a nodeRef, no more refresh should happen") 1377 1378 for i, item := range l.Items { 1379 // Simulate that expiry time is only TTL*3/4 from now. This would normally trigger a refresh. 1380 item.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(time.Now().UTC().Add(k.TokenTTL * 3 / 4).Format(time.RFC3339)) 1381 g.Expect(remoteClient.Update(ctx, &l.Items[i])).To(Succeed()) 1382 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1383 } 1384 1385 workerMachinePool.Status.NodeRefs = []corev1.ObjectReference{ 1386 { 1387 Kind: "Node", 1388 Namespace: metav1.NamespaceDefault, 1389 Name: "node-0", 1390 }, 1391 } 1392 g.Expect(patchHelper.Patch(ctx, workerMachinePool, patch.WithStatusObservedGeneration{})).To(Succeed()) 1393 1394 result, err = k.Reconcile(ctx, request) 1395 g.Expect(err).ToNot(HaveOccurred()) 1396 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1397 1398 l = &corev1.SecretList{} 1399 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1400 g.Expect(l.Items).To(HaveLen(1)) 1401 1402 for i, item := range l.Items { 1403 g.Expect(bytes.Equal(tokenExpires[i], item.Data[bootstrapapi.BootstrapTokenExpirationKey])).To(BeTrue()) 1404 } 1405 1406 t.Log("Token must be rotated before it expires") 1407 1408 for i, item := range l.Items { 1409 // Simulate that expiry time is only TTL*4/10 from now. This should trigger rotation. 1410 item.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(time.Now().UTC().Add(k.TokenTTL * 4 / 10).Format(time.RFC3339)) 1411 g.Expect(remoteClient.Update(ctx, &l.Items[i])).To(Succeed()) 1412 tokenExpires[i] = item.Data[bootstrapapi.BootstrapTokenExpirationKey] 1413 } 1414 1415 request = ctrl.Request{ 1416 NamespacedName: client.ObjectKey{ 1417 Namespace: metav1.NamespaceDefault, 1418 Name: "workerpool-join-cfg", 1419 }, 1420 } 1421 result, err = k.Reconcile(ctx, request) 1422 g.Expect(err).ToNot(HaveOccurred()) 1423 g.Expect(result.RequeueAfter).To(Equal(k.TokenTTL / 3)) 1424 1425 l = &corev1.SecretList{} 1426 g.Expect(remoteClient.List(ctx, l, client.ListOption(client.InNamespace(metav1.NamespaceSystem)))).To(Succeed()) 1427 g.Expect(l.Items).To(HaveLen(2)) // old and new token 1428 foundOld := false 1429 foundNew := true 1430 for _, item := range l.Items { 1431 if bytes.Equal(item.Data[bootstrapapi.BootstrapTokenExpirationKey], tokenExpires[0]) { 1432 foundOld = true 1433 } else { 1434 expirationTime, err := time.Parse(time.RFC3339, string(item.Data[bootstrapapi.BootstrapTokenExpirationKey])) 1435 g.Expect(err).ToNot(HaveOccurred()) 1436 g.Expect(expirationTime).Should(BeTemporally("~", time.Now().UTC().Add(k.TokenTTL), 10*time.Second)) 1437 foundNew = true 1438 } 1439 } 1440 g.Expect(foundOld).To(BeTrue()) 1441 g.Expect(foundNew).To(BeTrue()) 1442 } 1443 1444 // Ensure the discovery portion of the JoinConfiguration gets generated correctly. 1445 func TestKubeadmConfigReconciler_Reconcile_DiscoveryReconcileBehaviors(t *testing.T) { 1446 caHash := []string{"...."} 1447 bootstrapToken := bootstrapv1.Discovery{ 1448 BootstrapToken: &bootstrapv1.BootstrapTokenDiscovery{ 1449 CACertHashes: caHash, 1450 }, 1451 } 1452 goodcluster := &clusterv1.Cluster{ 1453 Spec: clusterv1.ClusterSpec{ 1454 ControlPlaneEndpoint: clusterv1.APIEndpoint{ 1455 Host: "example.com", 1456 Port: 6443, 1457 }, 1458 }, 1459 } 1460 testcases := []struct { 1461 name string 1462 cluster *clusterv1.Cluster 1463 config *bootstrapv1.KubeadmConfig 1464 validateDiscovery func(*WithT, *bootstrapv1.KubeadmConfig) error 1465 }{ 1466 { 1467 name: "Automatically generate token if discovery not specified", 1468 cluster: goodcluster, 1469 config: &bootstrapv1.KubeadmConfig{ 1470 Spec: bootstrapv1.KubeadmConfigSpec{ 1471 JoinConfiguration: &bootstrapv1.JoinConfiguration{ 1472 Discovery: bootstrapToken, 1473 }, 1474 }, 1475 }, 1476 validateDiscovery: func(g *WithT, c *bootstrapv1.KubeadmConfig) error { 1477 d := c.Spec.JoinConfiguration.Discovery 1478 g.Expect(d.BootstrapToken).NotTo(BeNil()) 1479 g.Expect(d.BootstrapToken.Token).NotTo(Equal("")) 1480 g.Expect(d.BootstrapToken.APIServerEndpoint).To(Equal("example.com:6443")) 1481 g.Expect(d.BootstrapToken.UnsafeSkipCAVerification).To(BeFalse()) 1482 return nil 1483 }, 1484 }, 1485 { 1486 name: "Respect discoveryConfiguration.File", 1487 cluster: goodcluster, 1488 config: &bootstrapv1.KubeadmConfig{ 1489 Spec: bootstrapv1.KubeadmConfigSpec{ 1490 JoinConfiguration: &bootstrapv1.JoinConfiguration{ 1491 Discovery: bootstrapv1.Discovery{ 1492 File: &bootstrapv1.FileDiscovery{}, 1493 }, 1494 }, 1495 }, 1496 }, 1497 validateDiscovery: func(g *WithT, c *bootstrapv1.KubeadmConfig) error { 1498 d := c.Spec.JoinConfiguration.Discovery 1499 g.Expect(d.BootstrapToken).To(BeNil()) 1500 return nil 1501 }, 1502 }, 1503 { 1504 name: "Respect discoveryConfiguration.BootstrapToken.APIServerEndpoint", 1505 cluster: goodcluster, 1506 config: &bootstrapv1.KubeadmConfig{ 1507 Spec: bootstrapv1.KubeadmConfigSpec{ 1508 JoinConfiguration: &bootstrapv1.JoinConfiguration{ 1509 Discovery: bootstrapv1.Discovery{ 1510 BootstrapToken: &bootstrapv1.BootstrapTokenDiscovery{ 1511 CACertHashes: caHash, 1512 APIServerEndpoint: "bar.com:6443", 1513 }, 1514 }, 1515 }, 1516 }, 1517 }, 1518 validateDiscovery: func(g *WithT, c *bootstrapv1.KubeadmConfig) error { 1519 d := c.Spec.JoinConfiguration.Discovery 1520 g.Expect(d.BootstrapToken.APIServerEndpoint).To(Equal("bar.com:6443")) 1521 return nil 1522 }, 1523 }, 1524 { 1525 name: "Respect discoveryConfiguration.BootstrapToken.Token", 1526 cluster: goodcluster, 1527 config: &bootstrapv1.KubeadmConfig{ 1528 Spec: bootstrapv1.KubeadmConfigSpec{ 1529 JoinConfiguration: &bootstrapv1.JoinConfiguration{ 1530 Discovery: bootstrapv1.Discovery{ 1531 BootstrapToken: &bootstrapv1.BootstrapTokenDiscovery{ 1532 CACertHashes: caHash, 1533 Token: "abcdef.0123456789abcdef", 1534 }, 1535 }, 1536 }, 1537 }, 1538 }, 1539 validateDiscovery: func(g *WithT, c *bootstrapv1.KubeadmConfig) error { 1540 d := c.Spec.JoinConfiguration.Discovery 1541 g.Expect(d.BootstrapToken.Token).To(Equal("abcdef.0123456789abcdef")) 1542 return nil 1543 }, 1544 }, 1545 { 1546 name: "Respect discoveryConfiguration.BootstrapToken.CACertHashes", 1547 cluster: goodcluster, 1548 config: &bootstrapv1.KubeadmConfig{ 1549 Spec: bootstrapv1.KubeadmConfigSpec{ 1550 JoinConfiguration: &bootstrapv1.JoinConfiguration{ 1551 Discovery: bootstrapv1.Discovery{ 1552 BootstrapToken: &bootstrapv1.BootstrapTokenDiscovery{ 1553 CACertHashes: caHash, 1554 }, 1555 }, 1556 }, 1557 }, 1558 }, 1559 validateDiscovery: func(g *WithT, c *bootstrapv1.KubeadmConfig) error { 1560 d := c.Spec.JoinConfiguration.Discovery 1561 g.Expect(reflect.DeepEqual(d.BootstrapToken.CACertHashes, caHash)).To(BeTrue()) 1562 return nil 1563 }, 1564 }, 1565 } 1566 1567 for _, tc := range testcases { 1568 t.Run(tc.name, func(t *testing.T) { 1569 g := NewWithT(t) 1570 1571 fakeClient := fake.NewClientBuilder().Build() 1572 k := &KubeadmConfigReconciler{ 1573 Client: fakeClient, 1574 SecretCachingClient: fakeClient, 1575 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), fakeClient, fakeClient, fakeClient.Scheme(), client.ObjectKey{Name: tc.cluster.Name, Namespace: tc.cluster.Namespace}), 1576 KubeadmInitLock: &myInitLocker{}, 1577 } 1578 1579 res, err := k.reconcileDiscovery(ctx, tc.cluster, tc.config, secret.Certificates{}) 1580 g.Expect(res.IsZero()).To(BeTrue()) 1581 g.Expect(err).ToNot(HaveOccurred()) 1582 1583 err = tc.validateDiscovery(g, tc.config) 1584 g.Expect(err).ToNot(HaveOccurred()) 1585 }) 1586 } 1587 } 1588 1589 // Test failure cases for the discovery reconcile function. 1590 func TestKubeadmConfigReconciler_Reconcile_DiscoveryReconcileFailureBehaviors(t *testing.T) { 1591 k := &KubeadmConfigReconciler{} 1592 1593 testcases := []struct { 1594 name string 1595 cluster *clusterv1.Cluster 1596 config *bootstrapv1.KubeadmConfig 1597 1598 result ctrl.Result 1599 err error 1600 }{ 1601 { 1602 name: "Should requeue if cluster has not ControlPlaneEndpoint", 1603 cluster: &clusterv1.Cluster{}, // cluster without endpoints 1604 config: &bootstrapv1.KubeadmConfig{ 1605 Spec: bootstrapv1.KubeadmConfigSpec{ 1606 JoinConfiguration: &bootstrapv1.JoinConfiguration{ 1607 Discovery: bootstrapv1.Discovery{ 1608 BootstrapToken: &bootstrapv1.BootstrapTokenDiscovery{ 1609 CACertHashes: []string{"item"}, 1610 }, 1611 }, 1612 }, 1613 }, 1614 }, 1615 result: ctrl.Result{RequeueAfter: 10 * time.Second}, 1616 }, 1617 } 1618 1619 for _, tc := range testcases { 1620 t.Run(tc.name, func(t *testing.T) { 1621 g := NewWithT(t) 1622 1623 res, err := k.reconcileDiscovery(ctx, tc.cluster, tc.config, secret.Certificates{}) 1624 g.Expect(res).To(BeComparableTo(tc.result)) 1625 if tc.err == nil { 1626 g.Expect(err).ToNot(HaveOccurred()) 1627 } else { 1628 g.Expect(err).To(Equal(tc.err)) 1629 } 1630 }) 1631 } 1632 } 1633 1634 // Set cluster configuration defaults based on dynamic values from the cluster object. 1635 func TestKubeadmConfigReconciler_Reconcile_DynamicDefaultsForClusterConfiguration(t *testing.T) { 1636 k := &KubeadmConfigReconciler{} 1637 1638 testcases := []struct { 1639 name string 1640 cluster *clusterv1.Cluster 1641 machine *clusterv1.Machine 1642 config *bootstrapv1.KubeadmConfig 1643 }{ 1644 { 1645 name: "Config settings have precedence", 1646 config: &bootstrapv1.KubeadmConfig{ 1647 Spec: bootstrapv1.KubeadmConfigSpec{ 1648 ClusterConfiguration: &bootstrapv1.ClusterConfiguration{ 1649 ClusterName: "mycluster", 1650 KubernetesVersion: "myversion", 1651 Networking: bootstrapv1.Networking{ 1652 PodSubnet: "myPodSubnet", 1653 ServiceSubnet: "myServiceSubnet", 1654 DNSDomain: "myDNSDomain", 1655 }, 1656 ControlPlaneEndpoint: "myControlPlaneEndpoint:6443", 1657 }, 1658 }, 1659 }, 1660 cluster: &clusterv1.Cluster{ 1661 ObjectMeta: metav1.ObjectMeta{ 1662 Name: "OtherName", 1663 }, 1664 Spec: clusterv1.ClusterSpec{ 1665 ClusterNetwork: &clusterv1.ClusterNetwork{ 1666 Services: &clusterv1.NetworkRanges{CIDRBlocks: []string{"otherServicesCidr"}}, 1667 Pods: &clusterv1.NetworkRanges{CIDRBlocks: []string{"otherPodsCidr"}}, 1668 ServiceDomain: "otherServiceDomain", 1669 }, 1670 ControlPlaneEndpoint: clusterv1.APIEndpoint{Host: "otherVersion", Port: 0}, 1671 }, 1672 }, 1673 machine: &clusterv1.Machine{ 1674 Spec: clusterv1.MachineSpec{ 1675 Version: ptr.To("otherVersion"), 1676 }, 1677 }, 1678 }, 1679 { 1680 name: "Top level object settings are used in case config settings are missing", 1681 config: &bootstrapv1.KubeadmConfig{ 1682 Spec: bootstrapv1.KubeadmConfigSpec{ 1683 ClusterConfiguration: &bootstrapv1.ClusterConfiguration{}, 1684 }, 1685 }, 1686 cluster: &clusterv1.Cluster{ 1687 ObjectMeta: metav1.ObjectMeta{ 1688 Name: "mycluster", 1689 }, 1690 Spec: clusterv1.ClusterSpec{ 1691 ClusterNetwork: &clusterv1.ClusterNetwork{ 1692 Services: &clusterv1.NetworkRanges{CIDRBlocks: []string{"myServiceSubnet"}}, 1693 Pods: &clusterv1.NetworkRanges{CIDRBlocks: []string{"myPodSubnet"}}, 1694 ServiceDomain: "myDNSDomain", 1695 }, 1696 ControlPlaneEndpoint: clusterv1.APIEndpoint{Host: "myControlPlaneEndpoint", Port: 6443}, 1697 }, 1698 }, 1699 machine: &clusterv1.Machine{ 1700 Spec: clusterv1.MachineSpec{ 1701 Version: ptr.To("myversion"), 1702 }, 1703 }, 1704 }, 1705 } 1706 1707 for _, tc := range testcases { 1708 t.Run(tc.name, func(t *testing.T) { 1709 g := NewWithT(t) 1710 1711 k.reconcileTopLevelObjectSettings(ctx, tc.cluster, tc.machine, tc.config) 1712 1713 g.Expect(tc.config.Spec.ClusterConfiguration.ControlPlaneEndpoint).To(Equal("myControlPlaneEndpoint:6443")) 1714 g.Expect(tc.config.Spec.ClusterConfiguration.ClusterName).To(Equal("mycluster")) 1715 g.Expect(tc.config.Spec.ClusterConfiguration.Networking.PodSubnet).To(Equal("myPodSubnet")) 1716 g.Expect(tc.config.Spec.ClusterConfiguration.Networking.ServiceSubnet).To(Equal("myServiceSubnet")) 1717 g.Expect(tc.config.Spec.ClusterConfiguration.Networking.DNSDomain).To(Equal("myDNSDomain")) 1718 g.Expect(tc.config.Spec.ClusterConfiguration.KubernetesVersion).To(Equal("myversion")) 1719 }) 1720 } 1721 } 1722 1723 // Allow users to skip CA Verification if they *really* want to. 1724 func TestKubeadmConfigReconciler_Reconcile_AlwaysCheckCAVerificationUnlessRequestedToSkip(t *testing.T) { 1725 // Setup work for an initialized cluster 1726 clusterName := "my-cluster" 1727 cluster := builder.Cluster(metav1.NamespaceDefault, clusterName).Build() 1728 conditions.MarkTrue(cluster, clusterv1.ControlPlaneInitializedCondition) 1729 cluster.Status.InfrastructureReady = true 1730 cluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{ 1731 Host: "example.com", 1732 Port: 6443, 1733 } 1734 controlPlaneInitMachine := newControlPlaneMachine(cluster, "my-control-plane-init-machine") 1735 initConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine.Namespace, "my-control-plane-init-config") 1736 1737 controlPlaneMachineName := "my-machine" 1738 machine := builder.Machine(metav1.NamespaceDefault, controlPlaneMachineName). 1739 WithVersion("v1.19.1"). 1740 WithClusterName(cluster.Name). 1741 Build() 1742 1743 workerMachineName := "my-worker" 1744 workerMachine := builder.Machine(metav1.NamespaceDefault, workerMachineName). 1745 WithVersion("v1.19.1"). 1746 WithClusterName(cluster.Name). 1747 Build() 1748 1749 controlPlaneConfigName := "my-config" 1750 config := newKubeadmConfig(metav1.NamespaceDefault, controlPlaneConfigName) 1751 1752 objects := []client.Object{ 1753 cluster, machine, workerMachine, config, 1754 } 1755 objects = append(objects, createSecrets(t, cluster, initConfig)...) 1756 1757 testcases := []struct { 1758 name string 1759 discovery *bootstrapv1.BootstrapTokenDiscovery 1760 skipCAVerification bool 1761 }{ 1762 { 1763 name: "Do not skip CA verification by default", 1764 discovery: &bootstrapv1.BootstrapTokenDiscovery{}, 1765 skipCAVerification: false, 1766 }, 1767 { 1768 name: "Skip CA verification if requested by the user", 1769 discovery: &bootstrapv1.BootstrapTokenDiscovery{ 1770 UnsafeSkipCAVerification: true, 1771 }, 1772 skipCAVerification: true, 1773 }, 1774 { 1775 // skipCAVerification should be true since no Cert Hashes are provided, but reconcile will *always* get or create certs. 1776 // TODO: Certificate get/create behavior needs to be mocked to enable this test. 1777 name: "cannot test for defaulting behavior through the reconcile function", 1778 discovery: &bootstrapv1.BootstrapTokenDiscovery{ 1779 CACertHashes: []string{""}, 1780 }, 1781 skipCAVerification: false, 1782 }, 1783 } 1784 for _, tc := range testcases { 1785 t.Run(tc.name, func(t *testing.T) { 1786 g := NewWithT(t) 1787 1788 myclient := fake.NewClientBuilder().WithObjects(objects...).Build() 1789 reconciler := KubeadmConfigReconciler{ 1790 Client: myclient, 1791 SecretCachingClient: myclient, 1792 Tracker: remote.NewTestClusterCacheTracker(logr.New(log.NullLogSink{}), myclient, myclient, myclient.Scheme(), client.ObjectKey{Name: cluster.Name, Namespace: cluster.Namespace}), 1793 KubeadmInitLock: &myInitLocker{}, 1794 } 1795 1796 wc := newWorkerJoinKubeadmConfig(metav1.NamespaceDefault, "worker-join-cfg") 1797 wc.Spec.JoinConfiguration.Discovery.BootstrapToken = tc.discovery 1798 key := client.ObjectKey{Namespace: wc.Namespace, Name: wc.Name} 1799 err := myclient.Create(ctx, wc) 1800 g.Expect(err).ToNot(HaveOccurred()) 1801 1802 req := ctrl.Request{NamespacedName: key} 1803 _, err = reconciler.Reconcile(ctx, req) 1804 g.Expect(err).ToNot(HaveOccurred()) 1805 1806 cfg := &bootstrapv1.KubeadmConfig{} 1807 err = myclient.Get(ctx, key, cfg) 1808 g.Expect(err).ToNot(HaveOccurred()) 1809 g.Expect(cfg.Spec.JoinConfiguration.Discovery.BootstrapToken.UnsafeSkipCAVerification).To(Equal(tc.skipCAVerification)) 1810 }) 1811 } 1812 } 1813 1814 // If a cluster object changes then all associated KubeadmConfigs should be re-reconciled. 1815 // This allows us to not requeue a kubeadm config while we wait for InfrastructureReady. 1816 func TestKubeadmConfigReconciler_ClusterToKubeadmConfigs(t *testing.T) { 1817 _ = feature.MutableGates.Set("MachinePool=true") 1818 g := NewWithT(t) 1819 1820 cluster := builder.Cluster(metav1.NamespaceDefault, "my-cluster").Build() 1821 objs := []client.Object{cluster} 1822 expectedNames := []string{} 1823 for i := 0; i < 3; i++ { 1824 configName := fmt.Sprintf("my-config-%d", i) 1825 m := builder.Machine(metav1.NamespaceDefault, fmt.Sprintf("my-machine-%d", i)). 1826 WithVersion("v1.19.1"). 1827 WithClusterName(cluster.Name). 1828 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, configName).Unstructured()). 1829 Build() 1830 c := newKubeadmConfig(metav1.NamespaceDefault, configName) 1831 addKubeadmConfigToMachine(c, m) 1832 expectedNames = append(expectedNames, configName) 1833 objs = append(objs, m, c) 1834 } 1835 for i := 3; i < 6; i++ { 1836 mp := newMachinePool(cluster, fmt.Sprintf("my-machinepool-%d", i)) 1837 configName := fmt.Sprintf("my-config-%d", i) 1838 c := newKubeadmConfig(mp.Namespace, configName) 1839 addKubeadmConfigToMachinePool(c, mp) 1840 expectedNames = append(expectedNames, configName) 1841 objs = append(objs, mp, c) 1842 } 1843 fakeClient := fake.NewClientBuilder().WithObjects(objs...).Build() 1844 reconciler := &KubeadmConfigReconciler{ 1845 Client: fakeClient, 1846 SecretCachingClient: fakeClient, 1847 } 1848 configs := reconciler.ClusterToKubeadmConfigs(ctx, cluster) 1849 names := make([]string, 6) 1850 for i := range configs { 1851 names[i] = configs[i].Name 1852 } 1853 for _, name := range expectedNames { 1854 found := false 1855 for _, foundName := range names { 1856 if foundName == name { 1857 found = true 1858 } 1859 } 1860 g.Expect(found).To(BeTrue()) 1861 } 1862 } 1863 1864 // Reconcile should not fail if the Etcd CA Secret already exists. 1865 func TestKubeadmConfigReconciler_Reconcile_DoesNotFailIfCASecretsAlreadyExist(t *testing.T) { 1866 g := NewWithT(t) 1867 1868 cluster := builder.Cluster(metav1.NamespaceDefault, "my-cluster").Build() 1869 cluster.Status.InfrastructureReady = true 1870 m := newControlPlaneMachine(cluster, "control-plane-machine") 1871 configName := "my-config" 1872 c := newControlPlaneInitKubeadmConfig(m.Namespace, configName) 1873 scrt := &corev1.Secret{ 1874 ObjectMeta: metav1.ObjectMeta{ 1875 Name: fmt.Sprintf("%s-%s", cluster.Name, secret.EtcdCA), 1876 Namespace: metav1.NamespaceDefault, 1877 }, 1878 Data: map[string][]byte{ 1879 "tls.crt": []byte("hello world"), 1880 "tls.key": []byte("hello world"), 1881 }, 1882 } 1883 fakec := fake.NewClientBuilder().WithObjects(cluster, m, c, scrt).Build() 1884 reconciler := &KubeadmConfigReconciler{ 1885 Client: fakec, 1886 SecretCachingClient: fakec, 1887 KubeadmInitLock: &myInitLocker{}, 1888 } 1889 req := ctrl.Request{ 1890 NamespacedName: client.ObjectKey{Namespace: metav1.NamespaceDefault, Name: configName}, 1891 } 1892 _, err := reconciler.Reconcile(ctx, req) 1893 g.Expect(err).ToNot(HaveOccurred()) 1894 } 1895 1896 // Exactly one control plane machine initializes if there are multiple control plane machines defined. 1897 func TestKubeadmConfigReconciler_Reconcile_ExactlyOneControlPlaneMachineInitializes(t *testing.T) { 1898 g := NewWithT(t) 1899 1900 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 1901 cluster.Status.InfrastructureReady = true 1902 1903 controlPlaneInitMachineFirst := newControlPlaneMachine(cluster, "control-plane-init-machine-first") 1904 controlPlaneInitConfigFirst := newControlPlaneInitKubeadmConfig(controlPlaneInitMachineFirst.Namespace, "control-plane-init-cfg-first") 1905 addKubeadmConfigToMachine(controlPlaneInitConfigFirst, controlPlaneInitMachineFirst) 1906 1907 controlPlaneInitMachineSecond := newControlPlaneMachine(cluster, "control-plane-init-machine-second") 1908 controlPlaneInitConfigSecond := newControlPlaneInitKubeadmConfig(controlPlaneInitMachineSecond.Namespace, "control-plane-init-cfg-second") 1909 addKubeadmConfigToMachine(controlPlaneInitConfigSecond, controlPlaneInitMachineSecond) 1910 1911 objects := []client.Object{ 1912 cluster, 1913 controlPlaneInitMachineFirst, 1914 controlPlaneInitConfigFirst, 1915 controlPlaneInitMachineSecond, 1916 controlPlaneInitConfigSecond, 1917 } 1918 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 1919 k := &KubeadmConfigReconciler{ 1920 Client: myclient, 1921 SecretCachingClient: myclient, 1922 KubeadmInitLock: &myInitLocker{}, 1923 } 1924 1925 request := ctrl.Request{ 1926 NamespacedName: client.ObjectKey{ 1927 Namespace: metav1.NamespaceDefault, 1928 Name: "control-plane-init-cfg-first", 1929 }, 1930 } 1931 result, err := k.Reconcile(ctx, request) 1932 g.Expect(err).ToNot(HaveOccurred()) 1933 g.Expect(result.Requeue).To(BeFalse()) 1934 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 1935 1936 request = ctrl.Request{ 1937 NamespacedName: client.ObjectKey{ 1938 Namespace: metav1.NamespaceDefault, 1939 Name: "control-plane-init-cfg-second", 1940 }, 1941 } 1942 result, err = k.Reconcile(ctx, request) 1943 g.Expect(err).ToNot(HaveOccurred()) 1944 g.Expect(result.Requeue).To(BeFalse()) 1945 g.Expect(result.RequeueAfter).To(Equal(30 * time.Second)) 1946 confList := &bootstrapv1.KubeadmConfigList{} 1947 g.Expect(myclient.List(ctx, confList)).To(Succeed()) 1948 for _, c := range confList.Items { 1949 // Ensure the DataSecretName is only set for controlPlaneInitConfigFirst. 1950 if c.Name == controlPlaneInitConfigFirst.Name { 1951 g.Expect(*c.Status.DataSecretName).To(Not(BeEmpty())) 1952 } 1953 if c.Name == controlPlaneInitConfigSecond.Name { 1954 g.Expect(c.Status.DataSecretName).To(BeNil()) 1955 } 1956 } 1957 } 1958 1959 // Patch should be applied if there is an error in reconcile. 1960 func TestKubeadmConfigReconciler_Reconcile_PatchWhenErrorOccurred(t *testing.T) { 1961 g := NewWithT(t) 1962 1963 cluster := builder.Cluster(metav1.NamespaceDefault, "cluster").Build() 1964 cluster.Status.InfrastructureReady = true 1965 1966 controlPlaneInitMachine := newControlPlaneMachine(cluster, "control-plane-init-machine") 1967 controlPlaneInitConfig := newControlPlaneInitKubeadmConfig(controlPlaneInitMachine.Namespace, "control-plane-init-cfg") 1968 addKubeadmConfigToMachine(controlPlaneInitConfig, controlPlaneInitMachine) 1969 // set InitConfiguration as nil, we will check this to determine if the kubeadm config has been patched 1970 controlPlaneInitConfig.Spec.InitConfiguration = nil 1971 1972 objects := []client.Object{ 1973 cluster, 1974 controlPlaneInitMachine, 1975 controlPlaneInitConfig, 1976 } 1977 1978 secrets := createSecrets(t, cluster, controlPlaneInitConfig) 1979 for _, obj := range secrets { 1980 s := obj.(*corev1.Secret) 1981 delete(s.Data, secret.TLSCrtDataName) // destroy the secrets, which will cause Reconcile to fail 1982 objects = append(objects, s) 1983 } 1984 1985 myclient := fake.NewClientBuilder().WithObjects(objects...).WithStatusSubresource(&bootstrapv1.KubeadmConfig{}).Build() 1986 k := &KubeadmConfigReconciler{ 1987 Client: myclient, 1988 SecretCachingClient: myclient, 1989 KubeadmInitLock: &myInitLocker{}, 1990 } 1991 1992 request := ctrl.Request{ 1993 NamespacedName: client.ObjectKey{ 1994 Namespace: metav1.NamespaceDefault, 1995 Name: "control-plane-init-cfg", 1996 }, 1997 } 1998 1999 result, err := k.Reconcile(ctx, request) 2000 g.Expect(err).To(HaveOccurred()) 2001 g.Expect(result.Requeue).To(BeFalse()) 2002 g.Expect(result.RequeueAfter).To(Equal(time.Duration(0))) 2003 2004 cfg, err := getKubeadmConfig(myclient, "control-plane-init-cfg", metav1.NamespaceDefault) 2005 g.Expect(err).ToNot(HaveOccurred()) 2006 // check if the kubeadm config has been patched 2007 g.Expect(cfg.Spec.InitConfiguration).ToNot(BeNil()) 2008 g.Expect(cfg.Status.ObservedGeneration).NotTo(BeNil()) 2009 } 2010 2011 func TestKubeadmConfigReconciler_ResolveFiles(t *testing.T) { 2012 testSecret := &corev1.Secret{ 2013 ObjectMeta: metav1.ObjectMeta{ 2014 Name: "source", 2015 }, 2016 Data: map[string][]byte{ 2017 "key": []byte("foo"), 2018 }, 2019 } 2020 2021 cases := map[string]struct { 2022 cfg *bootstrapv1.KubeadmConfig 2023 objects []client.Object 2024 expect []bootstrapv1.File 2025 }{ 2026 "content should pass through": { 2027 cfg: &bootstrapv1.KubeadmConfig{ 2028 Spec: bootstrapv1.KubeadmConfigSpec{ 2029 Files: []bootstrapv1.File{ 2030 { 2031 Content: "foo", 2032 Path: "/path", 2033 Owner: "root:root", 2034 Permissions: "0600", 2035 }, 2036 }, 2037 }, 2038 }, 2039 expect: []bootstrapv1.File{ 2040 { 2041 Content: "foo", 2042 Path: "/path", 2043 Owner: "root:root", 2044 Permissions: "0600", 2045 }, 2046 }, 2047 }, 2048 "contentFrom should convert correctly": { 2049 cfg: &bootstrapv1.KubeadmConfig{ 2050 Spec: bootstrapv1.KubeadmConfigSpec{ 2051 Files: []bootstrapv1.File{ 2052 { 2053 ContentFrom: &bootstrapv1.FileSource{ 2054 Secret: bootstrapv1.SecretFileSource{ 2055 Name: "source", 2056 Key: "key", 2057 }, 2058 }, 2059 Path: "/path", 2060 Owner: "root:root", 2061 Permissions: "0600", 2062 }, 2063 }, 2064 }, 2065 }, 2066 expect: []bootstrapv1.File{ 2067 { 2068 Content: "foo", 2069 Path: "/path", 2070 Owner: "root:root", 2071 Permissions: "0600", 2072 }, 2073 }, 2074 objects: []client.Object{testSecret}, 2075 }, 2076 "multiple files should work correctly": { 2077 cfg: &bootstrapv1.KubeadmConfig{ 2078 Spec: bootstrapv1.KubeadmConfigSpec{ 2079 Files: []bootstrapv1.File{ 2080 { 2081 Content: "bar", 2082 Path: "/bar", 2083 Owner: "root:root", 2084 Permissions: "0600", 2085 }, 2086 { 2087 ContentFrom: &bootstrapv1.FileSource{ 2088 Secret: bootstrapv1.SecretFileSource{ 2089 Name: "source", 2090 Key: "key", 2091 }, 2092 }, 2093 Path: "/path", 2094 Owner: "root:root", 2095 Permissions: "0600", 2096 }, 2097 }, 2098 }, 2099 }, 2100 expect: []bootstrapv1.File{ 2101 { 2102 Content: "bar", 2103 Path: "/bar", 2104 Owner: "root:root", 2105 Permissions: "0600", 2106 }, 2107 { 2108 Content: "foo", 2109 Path: "/path", 2110 Owner: "root:root", 2111 Permissions: "0600", 2112 }, 2113 }, 2114 objects: []client.Object{testSecret}, 2115 }, 2116 } 2117 2118 for name, tc := range cases { 2119 t.Run(name, func(t *testing.T) { 2120 g := NewWithT(t) 2121 2122 myclient := fake.NewClientBuilder().WithObjects(tc.objects...).Build() 2123 k := &KubeadmConfigReconciler{ 2124 Client: myclient, 2125 SecretCachingClient: myclient, 2126 KubeadmInitLock: &myInitLocker{}, 2127 } 2128 2129 // make a list of files we expect to be sourced from secrets 2130 // after we resolve files, assert that the original spec has 2131 // not been mutated and all paths we expected to be sourced 2132 // from secrets still are. 2133 contentFrom := map[string]bool{} 2134 for _, file := range tc.cfg.Spec.Files { 2135 if file.ContentFrom != nil { 2136 contentFrom[file.Path] = true 2137 } 2138 } 2139 2140 files, err := k.resolveFiles(ctx, tc.cfg) 2141 g.Expect(err).ToNot(HaveOccurred()) 2142 g.Expect(files).To(BeComparableTo(tc.expect)) 2143 for _, file := range tc.cfg.Spec.Files { 2144 if contentFrom[file.Path] { 2145 g.Expect(file.ContentFrom).NotTo(BeNil()) 2146 g.Expect(file.Content).To(Equal("")) 2147 } 2148 } 2149 }) 2150 } 2151 } 2152 2153 func TestKubeadmConfigReconciler_ResolveUsers(t *testing.T) { 2154 fakePasswd := "bar" 2155 testSecret := &corev1.Secret{ 2156 ObjectMeta: metav1.ObjectMeta{ 2157 Name: "source", 2158 }, 2159 Data: map[string][]byte{ 2160 "key": []byte(fakePasswd), 2161 }, 2162 } 2163 2164 cases := map[string]struct { 2165 cfg *bootstrapv1.KubeadmConfig 2166 objects []client.Object 2167 expect []bootstrapv1.User 2168 }{ 2169 "password should pass through": { 2170 cfg: &bootstrapv1.KubeadmConfig{ 2171 Spec: bootstrapv1.KubeadmConfigSpec{ 2172 Users: []bootstrapv1.User{ 2173 { 2174 Name: "foo", 2175 Passwd: &fakePasswd, 2176 }, 2177 }, 2178 }, 2179 }, 2180 expect: []bootstrapv1.User{ 2181 { 2182 Name: "foo", 2183 Passwd: &fakePasswd, 2184 }, 2185 }, 2186 }, 2187 "passwdFrom should convert correctly": { 2188 cfg: &bootstrapv1.KubeadmConfig{ 2189 Spec: bootstrapv1.KubeadmConfigSpec{ 2190 Users: []bootstrapv1.User{ 2191 { 2192 Name: "foo", 2193 PasswdFrom: &bootstrapv1.PasswdSource{ 2194 Secret: bootstrapv1.SecretPasswdSource{ 2195 Name: "source", 2196 Key: "key", 2197 }, 2198 }, 2199 }, 2200 }, 2201 }, 2202 }, 2203 expect: []bootstrapv1.User{ 2204 { 2205 Name: "foo", 2206 Passwd: &fakePasswd, 2207 }, 2208 }, 2209 objects: []client.Object{testSecret}, 2210 }, 2211 "multiple users should work correctly": { 2212 cfg: &bootstrapv1.KubeadmConfig{ 2213 Spec: bootstrapv1.KubeadmConfigSpec{ 2214 Users: []bootstrapv1.User{ 2215 { 2216 Name: "foo", 2217 Passwd: &fakePasswd, 2218 }, 2219 { 2220 Name: "bar", 2221 PasswdFrom: &bootstrapv1.PasswdSource{ 2222 Secret: bootstrapv1.SecretPasswdSource{ 2223 Name: "source", 2224 Key: "key", 2225 }, 2226 }, 2227 }, 2228 }, 2229 }, 2230 }, 2231 expect: []bootstrapv1.User{ 2232 { 2233 Name: "foo", 2234 Passwd: &fakePasswd, 2235 }, 2236 { 2237 Name: "bar", 2238 Passwd: &fakePasswd, 2239 }, 2240 }, 2241 objects: []client.Object{testSecret}, 2242 }, 2243 } 2244 2245 for name, tc := range cases { 2246 t.Run(name, func(t *testing.T) { 2247 g := NewWithT(t) 2248 2249 myclient := fake.NewClientBuilder().WithObjects(tc.objects...).Build() 2250 k := &KubeadmConfigReconciler{ 2251 Client: myclient, 2252 SecretCachingClient: myclient, 2253 KubeadmInitLock: &myInitLocker{}, 2254 } 2255 2256 // make a list of password we expect to be sourced from secrets 2257 // after we resolve users, assert that the original spec has 2258 // not been mutated and all password we expected to be sourced 2259 // from secret still are. 2260 passwdFrom := map[string]bool{} 2261 for _, user := range tc.cfg.Spec.Users { 2262 if user.PasswdFrom != nil { 2263 passwdFrom[user.Name] = true 2264 } 2265 } 2266 2267 users, err := k.resolveUsers(ctx, tc.cfg) 2268 g.Expect(err).ToNot(HaveOccurred()) 2269 g.Expect(users).To(BeComparableTo(tc.expect)) 2270 for _, user := range tc.cfg.Spec.Users { 2271 if passwdFrom[user.Name] { 2272 g.Expect(user.PasswdFrom).NotTo(BeNil()) 2273 g.Expect(user.Passwd).To(BeNil()) 2274 } 2275 } 2276 }) 2277 } 2278 } 2279 2280 // test utils. 2281 2282 // newWorkerMachineForCluster returns a Machine with the passed Cluster's information and a pre-configured name. 2283 func newWorkerMachineForCluster(cluster *clusterv1.Cluster) *clusterv1.Machine { 2284 return builder.Machine(cluster.Namespace, "worker-machine"). 2285 WithVersion("v1.19.1"). 2286 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(cluster.Namespace, "conf1").Unstructured()). 2287 WithClusterName(cluster.Name). 2288 Build() 2289 } 2290 2291 // newControlPlaneMachine returns a Machine with the passed Cluster information and a MachineControlPlaneLabel. 2292 func newControlPlaneMachine(cluster *clusterv1.Cluster, name string) *clusterv1.Machine { 2293 m := builder.Machine(cluster.Namespace, name). 2294 WithVersion("v1.19.1"). 2295 WithBootstrapTemplate(bootstrapbuilder.KubeadmConfig(metav1.NamespaceDefault, "cfg").Unstructured()). 2296 WithClusterName(cluster.Name). 2297 WithLabels(map[string]string{clusterv1.MachineControlPlaneLabel: ""}). 2298 Build() 2299 return m 2300 } 2301 2302 // newMachinePool return a MachinePool object with the passed Cluster information and a basic bootstrap template. 2303 func newMachinePool(cluster *clusterv1.Cluster, name string) *expv1.MachinePool { 2304 m := builder.MachinePool(cluster.Namespace, name). 2305 WithClusterName(cluster.Name). 2306 WithLabels(map[string]string{clusterv1.ClusterNameLabel: cluster.Name}). 2307 WithBootstrap(bootstrapbuilder.KubeadmConfig(cluster.Namespace, "conf1").Unstructured()). 2308 WithVersion("1.19.1"). 2309 Build() 2310 return m 2311 } 2312 2313 // newWorkerMachinePoolForCluster returns a MachinePool with the passed Cluster's information and a pre-configured name. 2314 func newWorkerMachinePoolForCluster(cluster *clusterv1.Cluster) *expv1.MachinePool { 2315 return newMachinePool(cluster, "worker-machinepool") 2316 } 2317 2318 // newKubeadmConfig return a CABPK KubeadmConfig object. 2319 func newKubeadmConfig(namespace, name string) *bootstrapv1.KubeadmConfig { 2320 return bootstrapbuilder.KubeadmConfig(namespace, name). 2321 Build() 2322 } 2323 2324 // newKubeadmConfig return a CABPK KubeadmConfig object with a worker JoinConfiguration. 2325 func newWorkerJoinKubeadmConfig(namespace, name string) *bootstrapv1.KubeadmConfig { 2326 return bootstrapbuilder.KubeadmConfig(namespace, name). 2327 WithJoinConfig(&bootstrapv1.JoinConfiguration{ 2328 ControlPlane: nil, 2329 }). 2330 Build() 2331 } 2332 2333 // newKubeadmConfig returns a CABPK KubeadmConfig object with a ControlPlane JoinConfiguration. 2334 func newControlPlaneJoinKubeadmConfig(namespace, name string) *bootstrapv1.KubeadmConfig { 2335 return bootstrapbuilder.KubeadmConfig(namespace, name). 2336 WithJoinConfig(&bootstrapv1.JoinConfiguration{ 2337 ControlPlane: &bootstrapv1.JoinControlPlane{}, 2338 }). 2339 Build() 2340 } 2341 2342 // newControlPlaneJoinConfig returns a CABPK KubeadmConfig object with a ControlPlane InitConfiguration and ClusterConfiguration. 2343 func newControlPlaneInitKubeadmConfig(namespace, name string) *bootstrapv1.KubeadmConfig { 2344 return bootstrapbuilder.KubeadmConfig(namespace, name). 2345 WithInitConfig(&bootstrapv1.InitConfiguration{}). 2346 WithClusterConfig(&bootstrapv1.ClusterConfiguration{}). 2347 Build() 2348 } 2349 2350 // addKubeadmConfigToMachine adds the config details to the passed Machine, and adds the Machine to the KubeadmConfig as an ownerReference. 2351 func addKubeadmConfigToMachine(config *bootstrapv1.KubeadmConfig, machine *clusterv1.Machine) { 2352 if machine == nil { 2353 panic("no machine passed to function") 2354 } 2355 config.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ 2356 { 2357 Kind: "Machine", 2358 APIVersion: clusterv1.GroupVersion.String(), 2359 Name: machine.Name, 2360 UID: types.UID(fmt.Sprintf("%s uid", machine.Name)), 2361 }, 2362 } 2363 2364 if machine.Spec.Bootstrap.ConfigRef == nil { 2365 machine.Spec.Bootstrap.ConfigRef = &corev1.ObjectReference{} 2366 } 2367 2368 machine.Spec.Bootstrap.ConfigRef.Name = config.Name 2369 machine.Spec.Bootstrap.ConfigRef.Namespace = config.Namespace 2370 } 2371 2372 // addKubeadmConfigToMachine adds the config details to the passed MachinePool and adds the Machine to the KubeadmConfig as an ownerReference. 2373 func addKubeadmConfigToMachinePool(config *bootstrapv1.KubeadmConfig, machinePool *expv1.MachinePool) { 2374 if machinePool == nil { 2375 panic("no machinePool passed to function") 2376 } 2377 config.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ 2378 { 2379 Kind: "MachinePool", 2380 APIVersion: expv1.GroupVersion.String(), 2381 Name: machinePool.Name, 2382 UID: types.UID(fmt.Sprintf("%s uid", machinePool.Name)), 2383 }, 2384 } 2385 machinePool.Spec.Template.Spec.Bootstrap.ConfigRef.Name = config.Name 2386 machinePool.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace = config.Namespace 2387 } 2388 2389 func createSecrets(t *testing.T, cluster *clusterv1.Cluster, config *bootstrapv1.KubeadmConfig) []client.Object { 2390 t.Helper() 2391 2392 out := []client.Object{} 2393 if config.Spec.ClusterConfiguration == nil { 2394 config.Spec.ClusterConfiguration = &bootstrapv1.ClusterConfiguration{} 2395 } 2396 certificates := secret.NewCertificatesForInitialControlPlane(config.Spec.ClusterConfiguration) 2397 if err := certificates.Generate(); err != nil { 2398 t.Fatal(err) 2399 } 2400 for _, certificate := range certificates { 2401 out = append(out, certificate.AsSecret(util.ObjectKey(cluster), *metav1.NewControllerRef(config, bootstrapv1.GroupVersion.WithKind("KubeadmConfig")))) 2402 } 2403 return out 2404 } 2405 2406 type myInitLocker struct { 2407 locked bool 2408 } 2409 2410 func (m *myInitLocker) Lock(_ context.Context, _ *clusterv1.Cluster, _ *clusterv1.Machine) bool { 2411 if !m.locked { 2412 m.locked = true 2413 return true 2414 } 2415 return false 2416 } 2417 2418 func (m *myInitLocker) Unlock(_ context.Context, _ *clusterv1.Cluster) bool { 2419 if m.locked { 2420 m.locked = false 2421 } 2422 return true 2423 } 2424 2425 func assertHasFalseCondition(g *WithT, myclient client.Client, req ctrl.Request, t clusterv1.ConditionType, s clusterv1.ConditionSeverity, r string) { 2426 config := &bootstrapv1.KubeadmConfig{ 2427 ObjectMeta: metav1.ObjectMeta{ 2428 Name: req.Name, 2429 Namespace: req.Namespace, 2430 }, 2431 } 2432 2433 configKey := client.ObjectKeyFromObject(config) 2434 g.Expect(myclient.Get(ctx, configKey, config)).To(Succeed()) 2435 c := conditions.Get(config, t) 2436 g.Expect(c).ToNot(BeNil()) 2437 g.Expect(c.Status).To(Equal(corev1.ConditionFalse)) 2438 g.Expect(c.Severity).To(Equal(s)) 2439 g.Expect(c.Reason).To(Equal(r)) 2440 } 2441 2442 func assertHasTrueCondition(g *WithT, myclient client.Client, req ctrl.Request, t clusterv1.ConditionType) { 2443 config := &bootstrapv1.KubeadmConfig{ 2444 ObjectMeta: metav1.ObjectMeta{ 2445 Name: req.Name, 2446 Namespace: req.Namespace, 2447 }, 2448 } 2449 configKey := client.ObjectKeyFromObject(config) 2450 g.Expect(myclient.Get(ctx, configKey, config)).To(Succeed()) 2451 c := conditions.Get(config, t) 2452 g.Expect(c).ToNot(BeNil()) 2453 g.Expect(c.Status).To(Equal(corev1.ConditionTrue)) 2454 }