sigs.k8s.io/cluster-api-provider-azure@v1.17.0/controllers/azureasomanagedcontrolplane_controller_test.go (about) 1 /* 2 Copyright 2024 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 "context" 21 "encoding/json" 22 "testing" 23 "time" 24 25 asocontainerservicev1 "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001" 26 "github.com/Azure/azure-service-operator/v2/pkg/genruntime" 27 . "github.com/onsi/gomega" 28 corev1 "k8s.io/api/core/v1" 29 apierrors "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/apimachinery/pkg/types" 34 "k8s.io/utils/ptr" 35 infrav1alpha "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha1" 36 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 37 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" 38 "sigs.k8s.io/cluster-api/util/secret" 39 ctrl "sigs.k8s.io/controller-runtime" 40 "sigs.k8s.io/controller-runtime/pkg/client" 41 fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" 42 ) 43 44 func TestAzureASOManagedControlPlaneReconcile(t *testing.T) { 45 ctx := context.Background() 46 47 s := runtime.NewScheme() 48 sb := runtime.NewSchemeBuilder( 49 infrav1alpha.AddToScheme, 50 clusterv1.AddToScheme, 51 asocontainerservicev1.AddToScheme, 52 corev1.AddToScheme, 53 ) 54 NewGomegaWithT(t).Expect(sb.AddToScheme(s)).To(Succeed()) 55 fakeClientBuilder := func() *fakeclient.ClientBuilder { 56 return fakeclient.NewClientBuilder(). 57 WithScheme(s). 58 WithStatusSubresource(&infrav1alpha.AzureASOManagedControlPlane{}) 59 } 60 61 t.Run("AzureASOManagedControlPlane does not exist", func(t *testing.T) { 62 g := NewGomegaWithT(t) 63 64 c := fakeClientBuilder(). 65 Build() 66 r := &AzureASOManagedControlPlaneReconciler{ 67 Client: c, 68 } 69 result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "doesn't", Name: "exist"}}) 70 g.Expect(err).NotTo(HaveOccurred()) 71 g.Expect(result).To(Equal(ctrl.Result{})) 72 }) 73 74 t.Run("Cluster does not exist", func(t *testing.T) { 75 g := NewGomegaWithT(t) 76 77 asoManagedControlPlane := &infrav1alpha.AzureASOManagedControlPlane{ 78 ObjectMeta: metav1.ObjectMeta{ 79 Name: "amcp", 80 Namespace: "ns", 81 OwnerReferences: []metav1.OwnerReference{ 82 { 83 APIVersion: clusterv1.GroupVersion.Identifier(), 84 Kind: "Cluster", 85 Name: "cluster", 86 }, 87 }, 88 }, 89 } 90 c := fakeClientBuilder(). 91 WithObjects(asoManagedControlPlane). 92 Build() 93 r := &AzureASOManagedControlPlaneReconciler{ 94 Client: c, 95 } 96 _, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(asoManagedControlPlane)}) 97 g.Expect(err).To(HaveOccurred()) 98 }) 99 100 t.Run("adds a finalizer and block-move annotation", func(t *testing.T) { 101 g := NewGomegaWithT(t) 102 103 cluster := &clusterv1.Cluster{ 104 ObjectMeta: metav1.ObjectMeta{ 105 Name: "cluster", 106 Namespace: "ns", 107 }, 108 Spec: clusterv1.ClusterSpec{ 109 InfrastructureRef: &corev1.ObjectReference{ 110 APIVersion: infrav1alpha.GroupVersion.Identifier(), 111 Kind: infrav1alpha.AzureASOManagedClusterKind, 112 }, 113 }, 114 } 115 asoManagedControlPlane := &infrav1alpha.AzureASOManagedControlPlane{ 116 ObjectMeta: metav1.ObjectMeta{ 117 Name: "amcp", 118 Namespace: cluster.Namespace, 119 OwnerReferences: []metav1.OwnerReference{ 120 { 121 APIVersion: clusterv1.GroupVersion.Identifier(), 122 Kind: "Cluster", 123 Name: cluster.Name, 124 }, 125 }, 126 }, 127 } 128 c := fakeClientBuilder(). 129 WithObjects(cluster, asoManagedControlPlane). 130 Build() 131 r := &AzureASOManagedControlPlaneReconciler{ 132 Client: c, 133 } 134 result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(asoManagedControlPlane)}) 135 g.Expect(err).NotTo(HaveOccurred()) 136 g.Expect(result).To(Equal(ctrl.Result{Requeue: true})) 137 138 g.Expect(c.Get(ctx, client.ObjectKeyFromObject(asoManagedControlPlane), asoManagedControlPlane)).To(Succeed()) 139 g.Expect(asoManagedControlPlane.GetFinalizers()).To(ContainElement(infrav1alpha.AzureASOManagedControlPlaneFinalizer)) 140 g.Expect(asoManagedControlPlane.GetAnnotations()).To(HaveKey(clusterctlv1.BlockMoveAnnotation)) 141 }) 142 143 t.Run("reconciles resources that are not ready", func(t *testing.T) { 144 g := NewGomegaWithT(t) 145 146 cluster := &clusterv1.Cluster{ 147 ObjectMeta: metav1.ObjectMeta{ 148 Name: "cluster", 149 Namespace: "ns", 150 }, 151 Spec: clusterv1.ClusterSpec{ 152 InfrastructureRef: &corev1.ObjectReference{ 153 APIVersion: infrav1alpha.GroupVersion.Identifier(), 154 Kind: infrav1alpha.AzureASOManagedClusterKind, 155 }, 156 }, 157 } 158 asoManagedControlPlane := &infrav1alpha.AzureASOManagedControlPlane{ 159 ObjectMeta: metav1.ObjectMeta{ 160 Name: "amcp", 161 Namespace: cluster.Namespace, 162 OwnerReferences: []metav1.OwnerReference{ 163 { 164 APIVersion: clusterv1.GroupVersion.Identifier(), 165 Kind: "Cluster", 166 Name: cluster.Name, 167 }, 168 }, 169 Finalizers: []string{ 170 infrav1alpha.AzureASOManagedControlPlaneFinalizer, 171 }, 172 Annotations: map[string]string{ 173 clusterctlv1.BlockMoveAnnotation: "true", 174 }, 175 }, 176 Spec: infrav1alpha.AzureASOManagedControlPlaneSpec{ 177 AzureASOManagedControlPlaneTemplateResourceSpec: infrav1alpha.AzureASOManagedControlPlaneTemplateResourceSpec{ 178 Resources: []runtime.RawExtension{ 179 { 180 Raw: mcJSON(g, &asocontainerservicev1.ManagedCluster{ 181 ObjectMeta: metav1.ObjectMeta{ 182 Name: "mc", 183 }, 184 }), 185 }, 186 }, 187 }, 188 }, 189 Status: infrav1alpha.AzureASOManagedControlPlaneStatus{ 190 Ready: true, 191 }, 192 } 193 c := fakeClientBuilder(). 194 WithObjects(cluster, asoManagedControlPlane). 195 Build() 196 r := &AzureASOManagedControlPlaneReconciler{ 197 Client: c, 198 newResourceReconciler: func(asoManagedControlPlane *infrav1alpha.AzureASOManagedControlPlane, _ []*unstructured.Unstructured) resourceReconciler { 199 return &fakeResourceReconciler{ 200 owner: asoManagedControlPlane, 201 reconcileFunc: func(ctx context.Context, o client.Object) error { 202 asoManagedControlPlane.SetResourceStatuses([]infrav1alpha.ResourceStatus{ 203 {Ready: true}, 204 {Ready: false}, 205 {Ready: true}, 206 }) 207 return nil 208 }, 209 } 210 }, 211 } 212 result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(asoManagedControlPlane)}) 213 g.Expect(err).NotTo(HaveOccurred()) 214 g.Expect(result).To(Equal(ctrl.Result{})) 215 216 g.Expect(c.Get(ctx, client.ObjectKeyFromObject(asoManagedControlPlane), asoManagedControlPlane)).To(Succeed()) 217 g.Expect(asoManagedControlPlane.Status.Ready).To(BeFalse()) 218 }) 219 220 t.Run("successfully reconciles normally", func(t *testing.T) { 221 g := NewGomegaWithT(t) 222 223 cluster := &clusterv1.Cluster{ 224 ObjectMeta: metav1.ObjectMeta{ 225 Name: "cluster", 226 Namespace: "ns", 227 }, 228 Spec: clusterv1.ClusterSpec{ 229 InfrastructureRef: &corev1.ObjectReference{ 230 APIVersion: infrav1alpha.GroupVersion.Identifier(), 231 Kind: infrav1alpha.AzureASOManagedClusterKind, 232 }, 233 }, 234 } 235 kubeconfig := &corev1.Secret{ 236 ObjectMeta: metav1.ObjectMeta{ 237 Name: secret.Name(cluster.Name, secret.Kubeconfig), 238 Namespace: cluster.Namespace, 239 }, 240 Data: map[string][]byte{ 241 "some other key": []byte("some data"), 242 }, 243 } 244 managedCluster := &asocontainerservicev1.ManagedCluster{ 245 ObjectMeta: metav1.ObjectMeta{ 246 Name: "mc", 247 Namespace: cluster.Namespace, 248 }, 249 Spec: asocontainerservicev1.ManagedCluster_Spec{ 250 OperatorSpec: &asocontainerservicev1.ManagedClusterOperatorSpec{ 251 Secrets: &asocontainerservicev1.ManagedClusterOperatorSecrets{ 252 UserCredentials: &genruntime.SecretDestination{ 253 Name: secret.Name(cluster.Name, secret.Kubeconfig), 254 Key: "some other key", 255 }, 256 }, 257 }, 258 }, 259 Status: asocontainerservicev1.ManagedCluster_STATUS{ 260 Fqdn: ptr.To("endpoint"), 261 CurrentKubernetesVersion: ptr.To("Current"), 262 }, 263 } 264 asoManagedControlPlane := &infrav1alpha.AzureASOManagedControlPlane{ 265 ObjectMeta: metav1.ObjectMeta{ 266 Name: "amcp", 267 Namespace: cluster.Namespace, 268 OwnerReferences: []metav1.OwnerReference{ 269 { 270 APIVersion: clusterv1.GroupVersion.Identifier(), 271 Kind: "Cluster", 272 Name: cluster.Name, 273 }, 274 }, 275 Finalizers: []string{ 276 infrav1alpha.AzureASOManagedControlPlaneFinalizer, 277 }, 278 Annotations: map[string]string{ 279 clusterctlv1.BlockMoveAnnotation: "true", 280 }, 281 }, 282 Spec: infrav1alpha.AzureASOManagedControlPlaneSpec{ 283 AzureASOManagedControlPlaneTemplateResourceSpec: infrav1alpha.AzureASOManagedControlPlaneTemplateResourceSpec{ 284 Resources: []runtime.RawExtension{ 285 { 286 Raw: mcJSON(g, &asocontainerservicev1.ManagedCluster{ 287 ObjectMeta: metav1.ObjectMeta{ 288 Name: managedCluster.Name, 289 }, 290 }), 291 }, 292 }, 293 }, 294 }, 295 Status: infrav1alpha.AzureASOManagedControlPlaneStatus{ 296 Ready: false, 297 }, 298 } 299 c := fakeClientBuilder(). 300 WithObjects(cluster, asoManagedControlPlane, managedCluster, kubeconfig). 301 Build() 302 kubeConfigPatched := false 303 r := &AzureASOManagedControlPlaneReconciler{ 304 Client: &FakeClient{ 305 Client: c, 306 patchFunc: func(_ context.Context, obj client.Object, _ client.Patch, _ ...client.PatchOption) error { 307 kubeconfig := obj.(*corev1.Secret) 308 g.Expect(kubeconfig.Data[secret.KubeconfigDataName]).NotTo(BeEmpty()) 309 kubeConfigPatched = true 310 return nil 311 }, 312 }, 313 newResourceReconciler: func(_ *infrav1alpha.AzureASOManagedControlPlane, _ []*unstructured.Unstructured) resourceReconciler { 314 return &fakeResourceReconciler{ 315 reconcileFunc: func(ctx context.Context, o client.Object) error { 316 return nil 317 }, 318 } 319 }, 320 } 321 result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(asoManagedControlPlane)}) 322 g.Expect(err).NotTo(HaveOccurred()) 323 g.Expect(result).To(Equal(ctrl.Result{})) 324 325 g.Expect(c.Get(ctx, client.ObjectKeyFromObject(asoManagedControlPlane), asoManagedControlPlane)).To(Succeed()) 326 g.Expect(asoManagedControlPlane.Status.ControlPlaneEndpoint.Host).To(Equal("endpoint")) 327 g.Expect(asoManagedControlPlane.Status.Version).To(Equal("vCurrent")) 328 g.Expect(kubeConfigPatched).To(BeTrue()) 329 g.Expect(asoManagedControlPlane.Status.Ready).To(BeTrue()) 330 }) 331 332 t.Run("successfully reconciles pause", func(t *testing.T) { 333 g := NewGomegaWithT(t) 334 335 cluster := &clusterv1.Cluster{ 336 ObjectMeta: metav1.ObjectMeta{ 337 Name: "cluster", 338 Namespace: "ns", 339 }, 340 Spec: clusterv1.ClusterSpec{ 341 Paused: true, 342 }, 343 } 344 asoManagedControlPlane := &infrav1alpha.AzureASOManagedControlPlane{ 345 ObjectMeta: metav1.ObjectMeta{ 346 Name: "amcp", 347 Namespace: cluster.Namespace, 348 OwnerReferences: []metav1.OwnerReference{ 349 { 350 APIVersion: clusterv1.GroupVersion.Identifier(), 351 Kind: "Cluster", 352 Name: cluster.Name, 353 }, 354 }, 355 Annotations: map[string]string{ 356 clusterctlv1.BlockMoveAnnotation: "true", 357 }, 358 }, 359 } 360 c := fakeClientBuilder(). 361 WithObjects(cluster, asoManagedControlPlane). 362 Build() 363 r := &AzureASOManagedControlPlaneReconciler{ 364 Client: c, 365 newResourceReconciler: func(_ *infrav1alpha.AzureASOManagedControlPlane, _ []*unstructured.Unstructured) resourceReconciler { 366 return &fakeResourceReconciler{ 367 pauseFunc: func(_ context.Context, _ client.Object) error { 368 return nil 369 }, 370 } 371 }, 372 } 373 result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(asoManagedControlPlane)}) 374 g.Expect(err).NotTo(HaveOccurred()) 375 g.Expect(result).To(Equal(ctrl.Result{})) 376 377 g.Expect(c.Get(ctx, client.ObjectKeyFromObject(asoManagedControlPlane), asoManagedControlPlane)).To(Succeed()) 378 g.Expect(asoManagedControlPlane.GetAnnotations()).NotTo(HaveKey(clusterctlv1.BlockMoveAnnotation)) 379 }) 380 381 t.Run("successfully reconciles delete", func(t *testing.T) { 382 g := NewGomegaWithT(t) 383 384 asoManagedControlPlane := &infrav1alpha.AzureASOManagedControlPlane{ 385 ObjectMeta: metav1.ObjectMeta{ 386 Name: "amcp", 387 Namespace: "ns", 388 Finalizers: []string{ 389 infrav1alpha.AzureASOManagedControlPlaneFinalizer, 390 }, 391 DeletionTimestamp: &metav1.Time{Time: time.Date(1, 0, 0, 0, 0, 0, 0, time.UTC)}, 392 }, 393 } 394 c := fakeClientBuilder(). 395 WithObjects(asoManagedControlPlane). 396 Build() 397 r := &AzureASOManagedControlPlaneReconciler{ 398 Client: c, 399 newResourceReconciler: func(_ *infrav1alpha.AzureASOManagedControlPlane, _ []*unstructured.Unstructured) resourceReconciler { 400 return &fakeResourceReconciler{ 401 deleteFunc: func(ctx context.Context, o client.Object) error { 402 return nil 403 }, 404 } 405 }, 406 } 407 result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(asoManagedControlPlane)}) 408 g.Expect(err).NotTo(HaveOccurred()) 409 g.Expect(result).To(Equal(ctrl.Result{})) 410 411 err = c.Get(ctx, client.ObjectKeyFromObject(asoManagedControlPlane), asoManagedControlPlane) 412 g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) 413 }) 414 } 415 416 func TestGetControlPlaneEndpoint(t *testing.T) { 417 tests := []struct { 418 name string 419 managedCluster *asocontainerservicev1.ManagedCluster 420 expected clusterv1.APIEndpoint 421 }{ 422 { 423 name: "empty", 424 managedCluster: &asocontainerservicev1.ManagedCluster{}, 425 expected: clusterv1.APIEndpoint{}, 426 }, 427 { 428 name: "public fqdn", 429 managedCluster: &asocontainerservicev1.ManagedCluster{ 430 Status: asocontainerservicev1.ManagedCluster_STATUS{ 431 Fqdn: ptr.To("fqdn"), 432 }, 433 }, 434 expected: clusterv1.APIEndpoint{ 435 Host: "fqdn", 436 Port: 443, 437 }, 438 }, 439 { 440 name: "private fqdn", 441 managedCluster: &asocontainerservicev1.ManagedCluster{ 442 Status: asocontainerservicev1.ManagedCluster_STATUS{ 443 PrivateFQDN: ptr.To("fqdn"), 444 }, 445 }, 446 expected: clusterv1.APIEndpoint{ 447 Host: "fqdn", 448 Port: 443, 449 }, 450 }, 451 { 452 name: "public and private fqdn", 453 managedCluster: &asocontainerservicev1.ManagedCluster{ 454 Status: asocontainerservicev1.ManagedCluster_STATUS{ 455 PrivateFQDN: ptr.To("private"), 456 Fqdn: ptr.To("public"), 457 }, 458 }, 459 expected: clusterv1.APIEndpoint{ 460 Host: "private", 461 Port: 443, 462 }, 463 }, 464 } 465 466 for _, test := range tests { 467 t.Run(test.name, func(t *testing.T) { 468 g := NewGomegaWithT(t) 469 g.Expect(getControlPlaneEndpoint(test.managedCluster)).To(Equal(test.expected)) 470 }) 471 } 472 } 473 474 func mcJSON(g Gomega, mc *asocontainerservicev1.ManagedCluster) []byte { 475 mc.SetGroupVersionKind(asocontainerservicev1.GroupVersion.WithKind("ManagedCluster")) 476 j, err := json.Marshal(mc) 477 g.Expect(err).NotTo(HaveOccurred()) 478 return j 479 }