sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/cluster/cluster_controller_phases_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 cluster 18 19 import ( 20 "testing" 21 "time" 22 23 . "github.com/onsi/gomega" 24 corev1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/client-go/tools/record" 28 ctrl "sigs.k8s.io/controller-runtime" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 "sigs.k8s.io/controller-runtime/pkg/client/fake" 31 32 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 33 capierrors "sigs.k8s.io/cluster-api/errors" 34 "sigs.k8s.io/cluster-api/internal/test/builder" 35 ) 36 37 func TestClusterReconcilePhases(t *testing.T) { 38 t.Run("reconcile infrastructure", func(t *testing.T) { 39 cluster := &clusterv1.Cluster{ 40 ObjectMeta: metav1.ObjectMeta{ 41 Name: "test-cluster", 42 Namespace: "test-namespace", 43 }, 44 Status: clusterv1.ClusterStatus{ 45 InfrastructureReady: true, 46 }, 47 Spec: clusterv1.ClusterSpec{ 48 ControlPlaneEndpoint: clusterv1.APIEndpoint{ 49 Host: "1.2.3.4", 50 Port: 8443, 51 }, 52 InfrastructureRef: &corev1.ObjectReference{ 53 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 54 Kind: "GenericInfrastructureMachine", 55 Name: "test", 56 }, 57 }, 58 } 59 60 tests := []struct { 61 name string 62 cluster *clusterv1.Cluster 63 infraRef map[string]interface{} 64 expectErr bool 65 expectResult ctrl.Result 66 }{ 67 { 68 name: "returns no error if infrastructure ref is nil", 69 cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "test-cluster", Namespace: "test-namespace"}}, 70 expectErr: false, 71 }, 72 { 73 name: "returns error if unable to reconcile infrastructure ref", 74 cluster: cluster, 75 expectErr: false, 76 expectResult: ctrl.Result{RequeueAfter: 30 * time.Second}, 77 }, 78 { 79 name: "returns no error if infra config is marked for deletion", 80 cluster: cluster, 81 infraRef: map[string]interface{}{ 82 "kind": "GenericInfrastructureMachine", 83 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 84 "metadata": map[string]interface{}{ 85 "name": "test", 86 "namespace": "test-namespace", 87 "deletionTimestamp": "sometime", 88 }, 89 }, 90 expectErr: false, 91 }, 92 { 93 name: "returns no error if infrastructure is marked ready on cluster", 94 cluster: cluster, 95 infraRef: map[string]interface{}{ 96 "kind": "GenericInfrastructureMachine", 97 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 98 "metadata": map[string]interface{}{ 99 "name": "test", 100 "namespace": "test-namespace", 101 "deletionTimestamp": "sometime", 102 }, 103 }, 104 expectErr: false, 105 }, 106 { 107 name: "returns error if infrastructure has the paused annotation", 108 cluster: cluster, 109 infraRef: map[string]interface{}{ 110 "kind": "GenericInfrastructureMachine", 111 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 112 "metadata": map[string]interface{}{ 113 "name": "test", 114 "namespace": "test-namespace", 115 "annotations": map[string]interface{}{ 116 "cluster.x-k8s.io/paused": "true", 117 }, 118 }, 119 }, 120 expectErr: false, 121 }, 122 } 123 124 for _, tt := range tests { 125 t.Run(tt.name, func(t *testing.T) { 126 g := NewWithT(t) 127 128 var c client.Client 129 if tt.infraRef != nil { 130 infraConfig := &unstructured.Unstructured{Object: tt.infraRef} 131 c = fake.NewClientBuilder(). 132 WithObjects(builder.GenericInfrastructureMachineCRD.DeepCopy(), tt.cluster, infraConfig). 133 Build() 134 } else { 135 c = fake.NewClientBuilder(). 136 WithObjects(builder.GenericInfrastructureMachineCRD.DeepCopy(), tt.cluster). 137 Build() 138 } 139 r := &Reconciler{ 140 Client: c, 141 UnstructuredCachingClient: c, 142 recorder: record.NewFakeRecorder(32), 143 } 144 145 res, err := r.reconcileInfrastructure(ctx, tt.cluster) 146 g.Expect(res).To(BeComparableTo(tt.expectResult)) 147 if tt.expectErr { 148 g.Expect(err).To(HaveOccurred()) 149 } else { 150 g.Expect(err).ToNot(HaveOccurred()) 151 } 152 }) 153 } 154 }) 155 156 t.Run("reconcile kubeconfig", func(t *testing.T) { 157 cluster := &clusterv1.Cluster{ 158 ObjectMeta: metav1.ObjectMeta{ 159 Name: "test-cluster", 160 }, 161 Spec: clusterv1.ClusterSpec{ 162 ControlPlaneEndpoint: clusterv1.APIEndpoint{ 163 Host: "1.2.3.4", 164 Port: 8443, 165 }, 166 }, 167 } 168 169 tests := []struct { 170 name string 171 cluster *clusterv1.Cluster 172 secret *corev1.Secret 173 wantErr bool 174 wantRequeue bool 175 }{ 176 { 177 name: "cluster not provisioned, apiEndpoint is not set", 178 cluster: &clusterv1.Cluster{}, 179 wantErr: false, 180 }, 181 { 182 name: "kubeconfig secret found", 183 cluster: cluster, 184 secret: &corev1.Secret{ 185 ObjectMeta: metav1.ObjectMeta{ 186 Name: "test-cluster-kubeconfig", 187 }, 188 }, 189 wantErr: false, 190 }, 191 { 192 name: "kubeconfig secret not found, should requeue", 193 cluster: cluster, 194 wantErr: false, 195 wantRequeue: true, 196 }, 197 { 198 name: "invalid ca secret, should return error", 199 cluster: cluster, 200 secret: &corev1.Secret{ 201 ObjectMeta: metav1.ObjectMeta{ 202 Name: "test-cluster-ca", 203 }, 204 }, 205 wantErr: true, 206 }, 207 } 208 for _, tt := range tests { 209 t.Run(tt.name, func(t *testing.T) { 210 g := NewWithT(t) 211 212 c := fake.NewClientBuilder(). 213 WithObjects(tt.cluster). 214 Build() 215 if tt.secret != nil { 216 c = fake.NewClientBuilder(). 217 WithObjects(tt.cluster, tt.secret). 218 Build() 219 } 220 r := &Reconciler{ 221 Client: c, 222 UnstructuredCachingClient: c, 223 recorder: record.NewFakeRecorder(32), 224 } 225 res, err := r.reconcileKubeconfig(ctx, tt.cluster) 226 if tt.wantErr { 227 g.Expect(err).To(HaveOccurred()) 228 } else { 229 g.Expect(err).ToNot(HaveOccurred()) 230 } 231 232 if tt.wantRequeue { 233 g.Expect(res.RequeueAfter).To(BeNumerically(">=", 0)) 234 } 235 }) 236 } 237 }) 238 } 239 240 func TestClusterReconciler_reconcilePhase(t *testing.T) { 241 cluster := &clusterv1.Cluster{ 242 ObjectMeta: metav1.ObjectMeta{ 243 Name: "test-cluster", 244 }, 245 Status: clusterv1.ClusterStatus{}, 246 Spec: clusterv1.ClusterSpec{}, 247 } 248 createClusterError := capierrors.CreateClusterError 249 failureMsg := "Create failed" 250 251 tests := []struct { 252 name string 253 cluster *clusterv1.Cluster 254 wantPhase clusterv1.ClusterPhase 255 }{ 256 { 257 name: "cluster not provisioned", 258 cluster: cluster, 259 wantPhase: clusterv1.ClusterPhasePending, 260 }, 261 { 262 name: "cluster has infrastructureRef", 263 cluster: &clusterv1.Cluster{ 264 ObjectMeta: metav1.ObjectMeta{ 265 Name: "test-cluster", 266 }, 267 Status: clusterv1.ClusterStatus{}, 268 Spec: clusterv1.ClusterSpec{ 269 InfrastructureRef: &corev1.ObjectReference{}, 270 }, 271 }, 272 273 wantPhase: clusterv1.ClusterPhaseProvisioning, 274 }, 275 { 276 name: "cluster infrastructure is ready", 277 cluster: &clusterv1.Cluster{ 278 ObjectMeta: metav1.ObjectMeta{ 279 Name: "test-cluster", 280 }, 281 Status: clusterv1.ClusterStatus{ 282 InfrastructureReady: true, 283 }, 284 Spec: clusterv1.ClusterSpec{ 285 InfrastructureRef: &corev1.ObjectReference{}, 286 }, 287 }, 288 289 wantPhase: clusterv1.ClusterPhaseProvisioning, 290 }, 291 { 292 name: "cluster infrastructure is ready and ControlPlaneEndpoint is set", 293 cluster: &clusterv1.Cluster{ 294 ObjectMeta: metav1.ObjectMeta{ 295 Name: "test-cluster", 296 }, 297 Spec: clusterv1.ClusterSpec{ 298 InfrastructureRef: &corev1.ObjectReference{}, 299 ControlPlaneEndpoint: clusterv1.APIEndpoint{ 300 Host: "1.2.3.4", 301 Port: 8443, 302 }, 303 }, 304 Status: clusterv1.ClusterStatus{ 305 InfrastructureReady: true, 306 }, 307 }, 308 309 wantPhase: clusterv1.ClusterPhaseProvisioned, 310 }, 311 { 312 name: "cluster status has FailureReason", 313 cluster: &clusterv1.Cluster{ 314 ObjectMeta: metav1.ObjectMeta{ 315 Name: "test-cluster", 316 }, 317 Status: clusterv1.ClusterStatus{ 318 InfrastructureReady: true, 319 FailureReason: &createClusterError, 320 }, 321 Spec: clusterv1.ClusterSpec{ 322 InfrastructureRef: &corev1.ObjectReference{}, 323 }, 324 }, 325 326 wantPhase: clusterv1.ClusterPhaseFailed, 327 }, 328 { 329 name: "cluster status has FailureMessage", 330 cluster: &clusterv1.Cluster{ 331 ObjectMeta: metav1.ObjectMeta{ 332 Name: "test-cluster", 333 }, 334 Status: clusterv1.ClusterStatus{ 335 InfrastructureReady: true, 336 FailureMessage: &failureMsg, 337 }, 338 Spec: clusterv1.ClusterSpec{ 339 InfrastructureRef: &corev1.ObjectReference{}, 340 }, 341 }, 342 343 wantPhase: clusterv1.ClusterPhaseFailed, 344 }, 345 { 346 name: "cluster has deletion timestamp", 347 cluster: &clusterv1.Cluster{ 348 ObjectMeta: metav1.ObjectMeta{ 349 Name: "test-cluster", 350 DeletionTimestamp: &metav1.Time{Time: time.Now().UTC()}, 351 Finalizers: []string{clusterv1.ClusterFinalizer}, 352 }, 353 Status: clusterv1.ClusterStatus{ 354 InfrastructureReady: true, 355 }, 356 Spec: clusterv1.ClusterSpec{ 357 InfrastructureRef: &corev1.ObjectReference{}, 358 }, 359 }, 360 361 wantPhase: clusterv1.ClusterPhaseDeleting, 362 }, 363 } 364 for _, tt := range tests { 365 t.Run(tt.name, func(t *testing.T) { 366 g := NewWithT(t) 367 368 c := fake.NewClientBuilder(). 369 WithObjects(tt.cluster). 370 Build() 371 372 r := &Reconciler{ 373 Client: c, 374 UnstructuredCachingClient: c, 375 recorder: record.NewFakeRecorder(32), 376 } 377 r.reconcilePhase(ctx, tt.cluster) 378 g.Expect(tt.cluster.Status.GetTypedPhase()).To(Equal(tt.wantPhase)) 379 }) 380 } 381 } 382 383 func TestClusterReconcilePhases_reconcileFailureDomains(t *testing.T) { 384 cluster := &clusterv1.Cluster{ 385 ObjectMeta: metav1.ObjectMeta{ 386 Name: "test-cluster", 387 Namespace: "test-namespace", 388 }, 389 Status: clusterv1.ClusterStatus{ 390 InfrastructureReady: true, 391 }, 392 Spec: clusterv1.ClusterSpec{ 393 ControlPlaneEndpoint: clusterv1.APIEndpoint{ 394 Host: "1.2.3.4", 395 Port: 8443, 396 }, 397 InfrastructureRef: &corev1.ObjectReference{ 398 APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1", 399 Kind: "GenericInfrastructureCluster", 400 Name: "test", 401 }, 402 }, 403 } 404 405 newFailureDomain := clusterv1.FailureDomains{ 406 "newdomain": clusterv1.FailureDomainSpec{ 407 ControlPlane: false, 408 Attributes: map[string]string{ 409 "attribute1": "value1", 410 }, 411 }, 412 } 413 414 newFailureDomainUpdated := clusterv1.FailureDomains{ 415 "newdomain": clusterv1.FailureDomainSpec{ 416 ControlPlane: false, 417 Attributes: map[string]string{ 418 "attribute2": "value2", 419 }, 420 }, 421 } 422 423 clusterWithNewFailureDomainUpdated := cluster.DeepCopy() 424 clusterWithNewFailureDomainUpdated.Status.FailureDomains = newFailureDomainUpdated 425 426 oldFailureDomain := clusterv1.FailureDomains{ 427 "olddomain": clusterv1.FailureDomainSpec{ 428 ControlPlane: false, 429 Attributes: map[string]string{ 430 "attribute1": "value1", 431 }, 432 }, 433 } 434 435 clusterWithOldFailureDomain := cluster.DeepCopy() 436 clusterWithOldFailureDomain.Status.FailureDomains = oldFailureDomain 437 438 tests := []struct { 439 name string 440 cluster *clusterv1.Cluster 441 infraRef map[string]interface{} 442 expectFailureDomains clusterv1.FailureDomains 443 }{ 444 { 445 name: "expect no failure domain if infrastructure ref is nil", 446 cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "test-cluster", Namespace: "test-namespace"}}, 447 }, 448 { 449 name: "expect no failure domain if infra config does not have failure domain", 450 cluster: cluster.DeepCopy(), 451 infraRef: generateInfraRef(false), 452 expectFailureDomains: clusterv1.FailureDomains{}, 453 }, 454 { 455 name: "expect cluster failure domain to be reset to empty if infra config does not have failure domain", 456 cluster: clusterWithOldFailureDomain.DeepCopy(), 457 infraRef: generateInfraRef(false), 458 expectFailureDomains: clusterv1.FailureDomains{}, 459 }, 460 { 461 name: "expect failure domain to remain same if infra config have same failure domain", 462 cluster: cluster.DeepCopy(), 463 infraRef: generateInfraRef(true), 464 expectFailureDomains: newFailureDomain, 465 }, 466 { 467 name: "expect failure domain to be updated if infra config has updates to failure domain", 468 cluster: clusterWithNewFailureDomainUpdated.DeepCopy(), 469 infraRef: generateInfraRef(true), 470 expectFailureDomains: newFailureDomain, 471 }, 472 { 473 name: "expect failure domain to be reset if infra config have different failure domain", 474 cluster: clusterWithOldFailureDomain.DeepCopy(), 475 infraRef: generateInfraRef(true), 476 expectFailureDomains: newFailureDomain, 477 }, 478 } 479 480 for _, tt := range tests { 481 t.Run(tt.name, func(t *testing.T) { 482 g := NewWithT(t) 483 484 objs := []client.Object{builder.GenericInfrastructureClusterCRD.DeepCopy(), tt.cluster} 485 if tt.infraRef != nil { 486 objs = append(objs, &unstructured.Unstructured{Object: tt.infraRef}) 487 } 488 489 c := fake.NewClientBuilder().WithObjects(objs...).Build() 490 r := &Reconciler{ 491 Client: c, 492 UnstructuredCachingClient: c, 493 recorder: record.NewFakeRecorder(32), 494 } 495 496 _, err := r.reconcileInfrastructure(ctx, tt.cluster) 497 g.Expect(err).ToNot(HaveOccurred()) 498 g.Expect(tt.cluster.Status.FailureDomains).To(BeEquivalentTo(tt.expectFailureDomains)) 499 }) 500 } 501 } 502 503 func generateInfraRef(withFailureDomain bool) map[string]interface{} { 504 infraRef := map[string]interface{}{ 505 "kind": "GenericInfrastructureCluster", 506 "apiVersion": "infrastructure.cluster.x-k8s.io/v1beta1", 507 "metadata": map[string]interface{}{ 508 "name": "test", 509 "namespace": "test-namespace", 510 "deletionTimestamp": "sometime", 511 }, 512 "status": map[string]interface{}{ 513 "ready": true, 514 }, 515 } 516 517 if withFailureDomain { 518 infraRef["status"] = map[string]interface{}{ 519 "failureDomains": map[string]interface{}{ 520 "newdomain": map[string]interface{}{ 521 "controlPlane": false, 522 "attributes": map[string]interface{}{ 523 "attribute1": "value1", 524 }, 525 }, 526 }, 527 "ready": true, 528 } 529 } 530 531 return infraRef 532 }