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  }