sigs.k8s.io/cluster-api-provider-azure@v1.14.3/controllers/azurecluster_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 "context" 21 "testing" 22 "time" 23 24 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" 25 asonetworkv1 "github.com/Azure/azure-service-operator/v2/api/network/v1api20201101" 26 asoresourcesv1 "github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601" 27 . "github.com/onsi/ginkgo/v2" 28 . "github.com/onsi/gomega" 29 "github.com/pkg/errors" 30 corev1 "k8s.io/api/core/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/apimachinery/pkg/types" 34 "k8s.io/client-go/tools/record" 35 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 36 "sigs.k8s.io/cluster-api-provider-azure/azure" 37 "sigs.k8s.io/cluster-api-provider-azure/azure/scope" 38 "sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus" 39 "sigs.k8s.io/cluster-api-provider-azure/internal/test" 40 "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" 41 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 42 clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3" 43 capierrors "sigs.k8s.io/cluster-api/errors" 44 ctrl "sigs.k8s.io/controller-runtime" 45 "sigs.k8s.io/controller-runtime/pkg/client" 46 "sigs.k8s.io/controller-runtime/pkg/client/fake" 47 "sigs.k8s.io/controller-runtime/pkg/reconcile" 48 ) 49 50 type TestClusterReconcileInput struct { 51 createAzureClusterService func(*scope.ClusterScope) (*azureClusterService, error) 52 azureClusterOptions func(ac *infrav1.AzureCluster) 53 clusterScopeFailureReason capierrors.ClusterStatusError 54 cache *scope.ClusterCache 55 expectedResult reconcile.Result 56 expectedErr string 57 ready bool 58 } 59 60 const ( 61 location = "westus2" 62 namespace = "default" 63 ) 64 65 var _ = Describe("AzureClusterReconciler", func() { 66 BeforeEach(func() {}) 67 AfterEach(func() {}) 68 69 Context("Reconcile an AzureCluster", func() { 70 It("should not error with minimal set up", func() { 71 reconciler := NewAzureClusterReconciler(testEnv, testEnv.GetEventRecorderFor("azurecluster-reconciler"), reconciler.Timeouts{}, "") 72 By("Calling reconcile") 73 name := test.RandomName("foo", 10) 74 instance := &infrav1.AzureCluster{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}} 75 result, err := reconciler.Reconcile(context.Background(), ctrl.Request{ 76 NamespacedName: client.ObjectKey{ 77 Namespace: instance.Namespace, 78 Name: instance.Name, 79 }, 80 }) 81 82 Expect(err).NotTo(HaveOccurred()) 83 Expect(result.RequeueAfter).To(BeZero()) 84 }) 85 }) 86 }) 87 88 func TestAzureClusterReconcile(t *testing.T) { 89 g := NewWithT(t) 90 scheme, err := newScheme() 91 g.Expect(err).NotTo(HaveOccurred()) 92 93 defaultCluster := getFakeCluster() 94 defaultAzureCluster := getFakeAzureCluster() 95 96 cases := map[string]struct { 97 objects []runtime.Object 98 fail bool 99 err string 100 event string 101 }{ 102 "should reconcile normally": { 103 objects: []runtime.Object{ 104 defaultCluster, 105 defaultAzureCluster, 106 }, 107 }, 108 "should raise event if the azure cluster is not found": { 109 objects: []runtime.Object{ 110 defaultCluster, 111 }, 112 event: "AzureClusterObjectNotFound", 113 }, 114 "should raise event if cluster is not found": { 115 objects: []runtime.Object{ 116 getFakeAzureCluster(func(ac *infrav1.AzureCluster) { 117 ac.OwnerReferences = nil 118 }), 119 defaultCluster, 120 }, 121 event: "OwnerRefNotSet", 122 }, 123 } 124 125 for name, tc := range cases { 126 t.Run(name, func(t *testing.T) { 127 client := fake.NewClientBuilder(). 128 WithScheme(scheme). 129 WithRuntimeObjects(tc.objects...). 130 WithStatusSubresource( 131 &infrav1.AzureCluster{}, 132 ). 133 Build() 134 135 reconciler := &AzureClusterReconciler{ 136 Client: client, 137 Recorder: record.NewFakeRecorder(128), 138 } 139 140 _, err := reconciler.Reconcile(context.Background(), ctrl.Request{ 141 NamespacedName: types.NamespacedName{ 142 Namespace: namespace, 143 Name: "my-azure-cluster", 144 }, 145 }) 146 if tc.event != "" { 147 g.Expect(reconciler.Recorder.(*record.FakeRecorder).Events).To(Receive(ContainSubstring(tc.event))) 148 } 149 if tc.fail { 150 g.Expect(err).To(MatchError(tc.err)) 151 } else { 152 g.Expect(err).NotTo(HaveOccurred()) 153 } 154 }) 155 } 156 } 157 158 func TestAzureClusterReconcileNormal(t *testing.T) { 159 cases := map[string]TestClusterReconcileInput{ 160 "should reconcile normally": { 161 createAzureClusterService: func(cs *scope.ClusterScope) (*azureClusterService, error) { 162 return getDefaultAzureClusterService(func(acs *azureClusterService) { 163 acs.skuCache = resourceskus.NewStaticCache([]armcompute.ResourceSKU{}, cs.Location()) 164 acs.scope = cs 165 }), nil 166 }, 167 cache: &scope.ClusterCache{}, 168 ready: true, 169 }, 170 "should fail if azure cluster service creator fails": { 171 createAzureClusterService: func(*scope.ClusterScope) (*azureClusterService, error) { 172 return nil, errors.New("failed to create azure cluster service") 173 }, 174 cache: &scope.ClusterCache{}, 175 expectedErr: "failed to create azure cluster service", 176 }, 177 "should reconcile if terminal error is received": { 178 createAzureClusterService: func(cs *scope.ClusterScope) (*azureClusterService, error) { 179 return getDefaultAzureClusterService(func(acs *azureClusterService) { 180 acs.skuCache = resourceskus.NewStaticCache([]armcompute.ResourceSKU{}, cs.Location()) 181 acs.scope = cs 182 }), nil 183 }, 184 clusterScopeFailureReason: capierrors.CreateClusterError, 185 cache: &scope.ClusterCache{}, 186 }, 187 "should requeue if transient error is received": { 188 createAzureClusterService: func(cs *scope.ClusterScope) (*azureClusterService, error) { 189 return getDefaultAzureClusterService(func(acs *azureClusterService) { 190 acs.skuCache = resourceskus.NewStaticCache([]armcompute.ResourceSKU{}, cs.Location()) 191 acs.scope = cs 192 acs.Reconcile = func(ctx context.Context) error { 193 return azure.WithTransientError(errors.New("failed to reconcile AzureCluster"), 10*time.Second) 194 } 195 }), nil 196 }, 197 cache: &scope.ClusterCache{}, 198 expectedResult: reconcile.Result{RequeueAfter: 10 * time.Second}, 199 }, 200 "should return error for general failures": { 201 createAzureClusterService: func(cs *scope.ClusterScope) (*azureClusterService, error) { 202 return getDefaultAzureClusterService(func(acs *azureClusterService) { 203 acs.skuCache = resourceskus.NewStaticCache([]armcompute.ResourceSKU{}, cs.Location()) 204 acs.scope = cs 205 acs.Reconcile = func(context.Context) error { 206 return errors.New("foo error") 207 } 208 acs.Pause = func(context.Context) error { 209 return errors.New("foo error") 210 } 211 acs.Delete = func(context.Context) error { 212 return errors.New("foo error") 213 } 214 }), nil 215 }, 216 cache: &scope.ClusterCache{}, 217 expectedErr: "failed to reconcile cluster services", 218 }, 219 } 220 221 for name, c := range cases { 222 tc := c 223 t.Run(name, func(t *testing.T) { 224 g := NewWithT(t) 225 reconciler, clusterScope, err := getClusterReconcileInputs(tc) 226 g.Expect(err).NotTo(HaveOccurred()) 227 228 result, err := reconciler.reconcileNormal(context.Background(), clusterScope) 229 g.Expect(result).To(Equal(tc.expectedResult)) 230 231 if tc.ready { 232 g.Expect(clusterScope.AzureCluster.Status.Ready).To(BeTrue()) 233 } 234 if tc.expectedErr != "" { 235 g.Expect(err).To(HaveOccurred()) 236 g.Expect(err.Error()).To(ContainSubstring(tc.expectedErr)) 237 } else { 238 g.Expect(err).NotTo(HaveOccurred()) 239 } 240 }) 241 } 242 } 243 244 func TestAzureClusterReconcilePaused(t *testing.T) { 245 g := NewWithT(t) 246 247 ctx := context.Background() 248 249 sb := runtime.NewSchemeBuilder( 250 clusterv1.AddToScheme, 251 infrav1.AddToScheme, 252 asoresourcesv1.AddToScheme, 253 asonetworkv1.AddToScheme, 254 corev1.AddToScheme, 255 ) 256 s := runtime.NewScheme() 257 g.Expect(sb.AddToScheme(s)).To(Succeed()) 258 fakeIdentity := &infrav1.AzureClusterIdentity{ 259 ObjectMeta: metav1.ObjectMeta{ 260 Name: "fake-identity", 261 Namespace: namespace, 262 }, 263 Spec: infrav1.AzureClusterIdentitySpec{ 264 Type: infrav1.ServicePrincipal, 265 TenantID: "fake-tenantid", 266 }, 267 } 268 fakeSecret := &corev1.Secret{Data: map[string][]byte{"clientSecret": []byte("fooSecret")}} 269 270 initObjects := []runtime.Object{fakeIdentity, fakeSecret} 271 c := fake.NewClientBuilder(). 272 WithScheme(s). 273 WithRuntimeObjects(initObjects...). 274 Build() 275 276 recorder := record.NewFakeRecorder(1) 277 278 reconciler := NewAzureClusterReconciler(c, recorder, reconciler.Timeouts{}, "") 279 name := test.RandomName("paused", 10) 280 namespace := namespace 281 282 cluster := &clusterv1.Cluster{ 283 ObjectMeta: metav1.ObjectMeta{ 284 Name: name, 285 Namespace: namespace, 286 }, 287 Spec: clusterv1.ClusterSpec{ 288 Paused: true, 289 }, 290 } 291 g.Expect(c.Create(ctx, cluster)).To(Succeed()) 292 293 instance := &infrav1.AzureCluster{ 294 ObjectMeta: metav1.ObjectMeta{ 295 Name: name, 296 Namespace: namespace, 297 Annotations: map[string]string{ 298 clusterctlv1.BlockMoveAnnotation: "true", 299 }, 300 OwnerReferences: []metav1.OwnerReference{ 301 { 302 Kind: "Cluster", 303 APIVersion: clusterv1.GroupVersion.String(), 304 Name: cluster.Name, 305 UID: cluster.UID, 306 }, 307 }, 308 }, 309 Spec: infrav1.AzureClusterSpec{ 310 AzureClusterClassSpec: infrav1.AzureClusterClassSpec{ 311 SubscriptionID: "something", 312 IdentityRef: &corev1.ObjectReference{ 313 Name: "fake-identity", 314 Namespace: namespace, 315 Kind: "AzureClusterIdentity", 316 }, 317 }, 318 ResourceGroup: name, 319 NetworkSpec: infrav1.NetworkSpec{ 320 Vnet: infrav1.VnetSpec{ 321 Name: name, 322 ResourceGroup: name, 323 }, 324 }, 325 }, 326 } 327 g.Expect(c.Create(ctx, instance)).To(Succeed()) 328 329 rg := &asoresourcesv1.ResourceGroup{ 330 ObjectMeta: metav1.ObjectMeta{ 331 Name: name, 332 Namespace: namespace, 333 }, 334 } 335 g.Expect(c.Create(ctx, rg)).To(Succeed()) 336 337 vnet := &asonetworkv1.VirtualNetwork{ 338 ObjectMeta: metav1.ObjectMeta{ 339 Name: name, 340 Namespace: namespace, 341 }, 342 } 343 g.Expect(c.Create(ctx, vnet)).To(Succeed()) 344 345 result, err := reconciler.Reconcile(context.Background(), ctrl.Request{ 346 NamespacedName: client.ObjectKey{ 347 Namespace: instance.Namespace, 348 Name: instance.Name, 349 }, 350 }) 351 352 g.Expect(err).NotTo(HaveOccurred()) 353 g.Expect(result.RequeueAfter).To(BeZero()) 354 355 g.Eventually(recorder.Events).Should(Receive(Equal("Normal ClusterPaused AzureCluster or linked Cluster is marked as paused. Won't reconcile normally"))) 356 357 g.Expect(c.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) 358 g.Expect(instance.GetAnnotations()).NotTo(HaveKey(clusterctlv1.BlockMoveAnnotation)) 359 } 360 361 func TestAzureClusterReconcileDelete(t *testing.T) { 362 cases := map[string]TestClusterReconcileInput{ 363 "should delete successfully": { 364 createAzureClusterService: func(cs *scope.ClusterScope) (*azureClusterService, error) { 365 return getDefaultAzureClusterService(func(acs *azureClusterService) { 366 acs.skuCache = resourceskus.NewStaticCache([]armcompute.ResourceSKU{}, cs.Location()) 367 acs.scope = cs 368 }), nil 369 }, 370 cache: &scope.ClusterCache{}, 371 }, 372 "should fail if failed to create azure cluster service": { 373 createAzureClusterService: func(cs *scope.ClusterScope) (*azureClusterService, error) { 374 return nil, errors.New("failed to create AzureClusterService") 375 }, 376 cache: &scope.ClusterCache{}, 377 expectedErr: "failed to create AzureClusterService", 378 }, 379 "should requeue if transient error is received": { 380 createAzureClusterService: func(cs *scope.ClusterScope) (*azureClusterService, error) { 381 return getDefaultAzureClusterService(func(acs *azureClusterService) { 382 acs.skuCache = resourceskus.NewStaticCache([]armcompute.ResourceSKU{}, cs.Location()) 383 acs.scope = cs 384 acs.Reconcile = func(ctx context.Context) error { 385 return azure.WithTransientError(errors.New("failed to reconcile AzureCluster"), 10*time.Second) 386 } 387 }), nil 388 }, 389 cache: &scope.ClusterCache{}, 390 expectedResult: reconcile.Result{}, 391 }, 392 "should fail to delete for non-transient errors": { 393 createAzureClusterService: func(cs *scope.ClusterScope) (*azureClusterService, error) { 394 return getDefaultAzureClusterService(func(acs *azureClusterService) { 395 acs.skuCache = resourceskus.NewStaticCache([]armcompute.ResourceSKU{}, cs.Location()) 396 acs.scope = cs 397 acs.Reconcile = func(context.Context) error { 398 return errors.New("foo error") 399 } 400 acs.Pause = func(context.Context) error { 401 return errors.New("foo error") 402 } 403 acs.Delete = func(context.Context) error { 404 return errors.New("foo error") 405 } 406 }), nil 407 }, 408 cache: &scope.ClusterCache{}, 409 expectedErr: "error deleting AzureCluster", 410 }, 411 } 412 413 for name, c := range cases { 414 tc := c 415 t.Run(name, func(t *testing.T) { 416 g := NewWithT(t) 417 418 reconciler, clusterScope, err := getClusterReconcileInputs(tc) 419 g.Expect(err).NotTo(HaveOccurred()) 420 421 result, err := reconciler.reconcileDelete(context.Background(), clusterScope) 422 g.Expect(result).To(Equal(tc.expectedResult)) 423 424 if tc.expectedErr != "" { 425 g.Expect(err).To(HaveOccurred()) 426 g.Expect(err.Error()).To(ContainSubstring(tc.expectedErr)) 427 } else { 428 g.Expect(err).NotTo(HaveOccurred()) 429 } 430 }) 431 } 432 } 433 434 func getDefaultAzureClusterService(changes ...func(*azureClusterService)) *azureClusterService { 435 input := &azureClusterService{ 436 services: []azure.ServiceReconciler{}, 437 Reconcile: func(ctx context.Context) error { 438 return nil 439 }, 440 Delete: func(ctx context.Context) error { 441 return nil 442 }, 443 Pause: func(ctx context.Context) error { 444 return nil 445 }, 446 } 447 448 for _, change := range changes { 449 change(input) 450 } 451 452 return input 453 } 454 455 func getClusterReconcileInputs(tc TestClusterReconcileInput) (*AzureClusterReconciler, *scope.ClusterScope, error) { 456 scheme, err := newScheme() 457 if err != nil { 458 return nil, nil, err 459 } 460 461 cluster := getFakeCluster() 462 463 var azureCluster *infrav1.AzureCluster 464 if tc.azureClusterOptions != nil { 465 azureCluster = getFakeAzureCluster(tc.azureClusterOptions, func(ac *infrav1.AzureCluster) { 466 ac.Spec.Location = location 467 }) 468 } else { 469 azureCluster = getFakeAzureCluster(func(ac *infrav1.AzureCluster) { 470 ac.Spec.Location = location 471 }) 472 } 473 474 fakeIdentity := &infrav1.AzureClusterIdentity{ 475 ObjectMeta: metav1.ObjectMeta{ 476 Name: "fake-identity", 477 Namespace: namespace, 478 }, 479 Spec: infrav1.AzureClusterIdentitySpec{ 480 Type: infrav1.ServicePrincipal, 481 TenantID: "fake-tenantid", 482 }, 483 } 484 fakeSecret := &corev1.Secret{Data: map[string][]byte{"clientSecret": []byte("fooSecret")}} 485 486 objects := []runtime.Object{ 487 cluster, 488 azureCluster, 489 fakeIdentity, 490 fakeSecret, 491 } 492 493 client := fake.NewClientBuilder(). 494 WithScheme(scheme). 495 WithRuntimeObjects(objects...). 496 WithStatusSubresource( 497 &infrav1.AzureCluster{}, 498 ). 499 Build() 500 501 reconciler := &AzureClusterReconciler{ 502 Client: client, 503 Recorder: record.NewFakeRecorder(128), 504 createAzureClusterService: tc.createAzureClusterService, 505 } 506 507 clusterScope, err := scope.NewClusterScope(context.Background(), scope.ClusterScopeParams{ 508 Client: client, 509 Cluster: cluster, 510 AzureCluster: azureCluster, 511 Cache: tc.cache, 512 }) 513 if err != nil { 514 return nil, nil, err 515 } 516 517 return reconciler, clusterScope, nil 518 }