sigs.k8s.io/cluster-api-provider-azure@v1.14.3/controllers/asosecret_controller_test.go (about)

     1  /*
     2  Copyright 2023 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  	"os"
    22  	"testing"
    23  
    24  	. "github.com/onsi/gomega"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/types"
    29  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    30  	"k8s.io/client-go/tools/record"
    31  	"k8s.io/utils/ptr"
    32  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    33  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    34  	ctrl "sigs.k8s.io/controller-runtime"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    37  )
    38  
    39  func TestASOSecretReconcile(t *testing.T) {
    40  	os.Setenv("AZURE_CLIENT_ID", "fooClient")
    41  	os.Setenv("AZURE_CLIENT_SECRET", "fooSecret")
    42  	os.Setenv("AZURE_TENANT_ID", "fooTenant")
    43  	os.Setenv("AZURE_SUBSCRIPTION_ID", "fooSubscription")
    44  
    45  	scheme := runtime.NewScheme()
    46  	_ = clusterv1.AddToScheme(scheme)
    47  	_ = infrav1.AddToScheme(scheme)
    48  	_ = clientgoscheme.AddToScheme(scheme)
    49  
    50  	defaultCluster := getASOCluster()
    51  	defaultAzureCluster := getASOAzureCluster()
    52  	defaultAzureManagedControlPlane := getASOAzureManagedControlPlane()
    53  	defaultASOSecret := getASOSecret(defaultAzureCluster)
    54  	defaultClusterIdentityType := infrav1.ServicePrincipal
    55  
    56  	cases := map[string]struct {
    57  		clusterName string
    58  		objects     []runtime.Object
    59  		err         string
    60  		event       string
    61  		asoSecret   *corev1.Secret
    62  	}{
    63  		"should not fail if the azure cluster is not found": {
    64  			clusterName: defaultAzureCluster.Name,
    65  			objects: []runtime.Object{
    66  				getASOCluster(func(c *clusterv1.Cluster) {
    67  					c.Spec.InfrastructureRef.Name = defaultAzureCluster.Name
    68  					c.Spec.InfrastructureRef.Kind = defaultAzureCluster.Kind
    69  				}),
    70  			},
    71  		},
    72  		"should not fail for AzureCluster without ownerRef set yet": {
    73  			clusterName: defaultAzureCluster.Name,
    74  			objects: []runtime.Object{
    75  				getASOAzureCluster(func(c *infrav1.AzureCluster) {
    76  					c.ObjectMeta.OwnerReferences = nil
    77  				}),
    78  				defaultCluster,
    79  			},
    80  		},
    81  		"should reconcile normally for AzureCluster with IdentityRef configured": {
    82  			clusterName: defaultAzureCluster.Name,
    83  			objects: []runtime.Object{
    84  				getASOAzureCluster(func(c *infrav1.AzureCluster) {
    85  					c.Spec.IdentityRef = &corev1.ObjectReference{
    86  						Name:      "my-azure-cluster-identity",
    87  						Namespace: "default",
    88  					}
    89  				}),
    90  				getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
    91  					identity.Spec.Type = defaultClusterIdentityType
    92  					identity.Spec.ClientSecret = corev1.SecretReference{
    93  						Name:      "fooSecret",
    94  						Namespace: "default",
    95  					}
    96  				}),
    97  				getASOAzureClusterIdentitySecret(),
    98  				defaultCluster,
    99  			},
   100  			asoSecret: getASOSecret(defaultAzureCluster, func(s *corev1.Secret) {
   101  				s.Data = map[string][]byte{
   102  					"AZURE_SUBSCRIPTION_ID": []byte("123"),
   103  					"AZURE_TENANT_ID":       []byte("fooTenant"),
   104  					"AZURE_CLIENT_ID":       []byte("fooClient"),
   105  					"AZURE_CLIENT_SECRET":   []byte("fooSecret"),
   106  				}
   107  			}),
   108  		},
   109  		"should reconcile normally for AzureManagedControlPlane with IdentityRef configured": {
   110  			clusterName: defaultAzureManagedControlPlane.Name,
   111  			objects: []runtime.Object{
   112  				getASOAzureManagedControlPlane(func(c *infrav1.AzureManagedControlPlane) {
   113  					c.Spec.IdentityRef = &corev1.ObjectReference{
   114  						Name:      "my-azure-cluster-identity",
   115  						Namespace: "default",
   116  					}
   117  				}),
   118  				getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
   119  					identity.Spec.Type = defaultClusterIdentityType
   120  					identity.Spec.ClientSecret = corev1.SecretReference{
   121  						Name:      "fooSecret",
   122  						Namespace: "default",
   123  					}
   124  				}),
   125  				getASOAzureClusterIdentitySecret(),
   126  				defaultCluster,
   127  			},
   128  			asoSecret: getASOSecret(defaultAzureManagedControlPlane, func(s *corev1.Secret) {
   129  				s.Data = map[string][]byte{
   130  					"AZURE_SUBSCRIPTION_ID": []byte("fooSubscription"),
   131  					"AZURE_TENANT_ID":       []byte("fooTenant"),
   132  					"AZURE_CLIENT_ID":       []byte("fooClient"),
   133  					"AZURE_CLIENT_SECRET":   []byte("fooSecret"),
   134  				}
   135  			}),
   136  		},
   137  		"should reconcile normally for AzureCluster with an IdentityRef of type WorkloadIdentity": {
   138  			clusterName: defaultAzureCluster.Name,
   139  			objects: []runtime.Object{
   140  				getASOAzureCluster(func(c *infrav1.AzureCluster) {
   141  					c.Spec.IdentityRef = &corev1.ObjectReference{
   142  						Name:      "my-azure-cluster-identity",
   143  						Namespace: "default",
   144  					}
   145  				}),
   146  				getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
   147  					identity.Spec.Type = "WorkloadIdentity"
   148  				}),
   149  				defaultCluster,
   150  			},
   151  			asoSecret: getASOSecret(defaultAzureCluster, func(s *corev1.Secret) {
   152  				s.Data = map[string][]byte{
   153  					"AZURE_SUBSCRIPTION_ID": []byte("123"),
   154  					"AZURE_TENANT_ID":       []byte("fooTenant"),
   155  					"AZURE_CLIENT_ID":       []byte("fooClient"),
   156  					"AUTH_MODE":             []byte("workloadidentity"),
   157  				}
   158  			}),
   159  		},
   160  		"should reconcile normally for AzureManagedControlPlane with an IdentityRef of type WorkloadIdentity": {
   161  			clusterName: defaultAzureManagedControlPlane.Name,
   162  			objects: []runtime.Object{
   163  				getASOAzureManagedControlPlane(func(c *infrav1.AzureManagedControlPlane) {
   164  					c.Spec.IdentityRef = &corev1.ObjectReference{
   165  						Name:      "my-azure-cluster-identity",
   166  						Namespace: "default",
   167  					}
   168  				}),
   169  				getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
   170  					identity.Spec.Type = infrav1.WorkloadIdentity
   171  				}),
   172  				defaultCluster,
   173  			},
   174  			asoSecret: getASOSecret(defaultAzureManagedControlPlane, func(s *corev1.Secret) {
   175  				s.Data = map[string][]byte{
   176  					"AZURE_SUBSCRIPTION_ID": []byte("fooSubscription"),
   177  					"AZURE_TENANT_ID":       []byte("fooTenant"),
   178  					"AZURE_CLIENT_ID":       []byte("fooClient"),
   179  					"AUTH_MODE":             []byte("workloadidentity"),
   180  				}
   181  			}),
   182  		},
   183  		"should reconcile normally for AzureCluster with an IdentityRef of type UserAssignedMSI": {
   184  			clusterName: defaultAzureCluster.Name,
   185  			objects: []runtime.Object{
   186  				getASOAzureCluster(func(c *infrav1.AzureCluster) {
   187  					c.Spec.IdentityRef = &corev1.ObjectReference{
   188  						Name:      "my-azure-cluster-identity",
   189  						Namespace: "default",
   190  					}
   191  				}),
   192  				getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
   193  					identity.Spec.Type = infrav1.UserAssignedMSI
   194  				}),
   195  				defaultCluster,
   196  			},
   197  			asoSecret: getASOSecret(defaultAzureCluster, func(s *corev1.Secret) {
   198  				s.Data = map[string][]byte{
   199  					"AZURE_SUBSCRIPTION_ID": []byte("123"),
   200  					"AZURE_TENANT_ID":       []byte("fooTenant"),
   201  					"AZURE_CLIENT_ID":       []byte("fooClient"),
   202  					"AUTH_MODE":             []byte("podidentity"),
   203  				}
   204  			}),
   205  		},
   206  		"should reconcile normally for AzureManagedControlPlane with an IdentityRef of type UserAssignedMSI": {
   207  			clusterName: defaultAzureManagedControlPlane.Name,
   208  			objects: []runtime.Object{
   209  				getASOAzureManagedControlPlane(func(c *infrav1.AzureManagedControlPlane) {
   210  					c.Spec.IdentityRef = &corev1.ObjectReference{
   211  						Name:      "my-azure-cluster-identity",
   212  						Namespace: "default",
   213  					}
   214  				}),
   215  				getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
   216  					identity.Spec.Type = infrav1.UserAssignedMSI
   217  				}),
   218  				defaultCluster,
   219  			},
   220  			asoSecret: getASOSecret(defaultAzureManagedControlPlane, func(s *corev1.Secret) {
   221  				s.Data = map[string][]byte{
   222  					"AZURE_SUBSCRIPTION_ID": []byte("fooSubscription"),
   223  					"AZURE_TENANT_ID":       []byte("fooTenant"),
   224  					"AZURE_CLIENT_ID":       []byte("fooClient"),
   225  					"AUTH_MODE":             []byte("podidentity"),
   226  				}
   227  			}),
   228  		},
   229  		"should fail if IdentityRef secret doesn't exist": {
   230  			clusterName: defaultAzureManagedControlPlane.Name,
   231  			objects: []runtime.Object{
   232  				getASOAzureManagedControlPlane(func(c *infrav1.AzureManagedControlPlane) {
   233  					c.Spec.IdentityRef = &corev1.ObjectReference{
   234  						Name:      "my-azure-cluster-identity",
   235  						Namespace: "default",
   236  					}
   237  				}),
   238  				getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
   239  					identity.Spec.Type = defaultClusterIdentityType
   240  					identity.Spec.ClientSecret = corev1.SecretReference{
   241  						Name:      "fooSecret",
   242  						Namespace: "default",
   243  					}
   244  				}),
   245  				defaultCluster,
   246  			},
   247  			err: "secrets \"fooSecret\" not found",
   248  		},
   249  		"should return if cluster does not exist": {
   250  			clusterName: defaultAzureCluster.Name,
   251  			objects: []runtime.Object{
   252  				defaultAzureCluster,
   253  			},
   254  			err: "failed to get Cluster/my-cluster: clusters.cluster.x-k8s.io \"my-cluster\" not found",
   255  		},
   256  		"should return if cluster is paused": {
   257  			clusterName: defaultAzureCluster.Name,
   258  			objects: []runtime.Object{
   259  				getASOCluster(func(c *clusterv1.Cluster) {
   260  					c.Spec.Paused = true
   261  				}),
   262  				getASOAzureCluster(func(c *infrav1.AzureCluster) {
   263  					c.Spec.IdentityRef = &corev1.ObjectReference{
   264  						Name:      "my-azure-cluster-identity",
   265  						Namespace: "default",
   266  					}
   267  				}),
   268  				getASOAzureClusterIdentity(func(identity *infrav1.AzureClusterIdentity) {
   269  					identity.Spec.Type = defaultClusterIdentityType
   270  					identity.Spec.ClientSecret = corev1.SecretReference{
   271  						Name:      "fooSecret",
   272  						Namespace: "default",
   273  					}
   274  				}),
   275  				getASOAzureClusterIdentitySecret(),
   276  			},
   277  			event: "AzureCluster or linked Cluster is marked as paused. Won't reconcile",
   278  		},
   279  		"should return if azureCluster is not yet available": {
   280  			clusterName: defaultAzureCluster.Name,
   281  			objects: []runtime.Object{
   282  				defaultCluster,
   283  			},
   284  			event: "AzureClusterObjectNotFound AzureCluster object default/my-azure-cluster not found",
   285  		},
   286  	}
   287  
   288  	for name, tc := range cases {
   289  		t.Run(name, func(t *testing.T) {
   290  			g := NewWithT(t)
   291  			clientBuilder := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(tc.objects...).Build()
   292  
   293  			reconciler := &ASOSecretReconciler{
   294  				Client:   clientBuilder,
   295  				Recorder: record.NewFakeRecorder(128),
   296  			}
   297  
   298  			_, err := reconciler.Reconcile(context.Background(), ctrl.Request{
   299  				NamespacedName: types.NamespacedName{
   300  					Namespace: "default",
   301  					Name:      tc.clusterName,
   302  				},
   303  			})
   304  
   305  			existingASOSecret := &corev1.Secret{}
   306  			asoSecretErr := clientBuilder.Get(context.Background(), types.NamespacedName{
   307  				Namespace: defaultASOSecret.Namespace,
   308  				Name:      defaultASOSecret.Name,
   309  			}, existingASOSecret)
   310  
   311  			if tc.asoSecret != nil {
   312  				g.Expect(asoSecretErr).NotTo(HaveOccurred())
   313  				g.Expect(tc.asoSecret.Data).To(BeEquivalentTo(existingASOSecret.Data))
   314  			} else {
   315  				g.Expect(asoSecretErr).To(HaveOccurred())
   316  			}
   317  
   318  			if tc.err != "" {
   319  				g.Expect(err).To(MatchError(ContainSubstring(tc.err)))
   320  			} else {
   321  				g.Expect(err).NotTo(HaveOccurred())
   322  			}
   323  			if tc.event != "" {
   324  				g.Expect(reconciler.Recorder.(*record.FakeRecorder).Events).To(Receive(ContainSubstring(tc.event)))
   325  			}
   326  		})
   327  	}
   328  }
   329  
   330  func getASOCluster(changes ...func(*clusterv1.Cluster)) *clusterv1.Cluster {
   331  	input := &clusterv1.Cluster{
   332  		ObjectMeta: metav1.ObjectMeta{
   333  			Name:      "my-cluster",
   334  			Namespace: "default",
   335  		},
   336  		Spec: clusterv1.ClusterSpec{
   337  			InfrastructureRef: &corev1.ObjectReference{
   338  				APIVersion: infrav1.GroupVersion.String(),
   339  			},
   340  		},
   341  		Status: clusterv1.ClusterStatus{
   342  			InfrastructureReady: true,
   343  		},
   344  	}
   345  
   346  	for _, change := range changes {
   347  		change(input)
   348  	}
   349  
   350  	return input
   351  }
   352  
   353  func getASOAzureCluster(changes ...func(*infrav1.AzureCluster)) *infrav1.AzureCluster {
   354  	input := &infrav1.AzureCluster{
   355  		ObjectMeta: metav1.ObjectMeta{
   356  			Name:      "my-azure-cluster",
   357  			Namespace: "default",
   358  			OwnerReferences: []metav1.OwnerReference{
   359  				{
   360  					APIVersion: clusterv1.GroupVersion.String(),
   361  					Kind:       "Cluster",
   362  					Name:       "my-cluster",
   363  				},
   364  			},
   365  		},
   366  		Spec: infrav1.AzureClusterSpec{
   367  			AzureClusterClassSpec: infrav1.AzureClusterClassSpec{
   368  				SubscriptionID: "123",
   369  			},
   370  		},
   371  	}
   372  	for _, change := range changes {
   373  		change(input)
   374  	}
   375  
   376  	return input
   377  }
   378  
   379  func getASOAzureManagedControlPlane(changes ...func(*infrav1.AzureManagedControlPlane)) *infrav1.AzureManagedControlPlane {
   380  	input := &infrav1.AzureManagedControlPlane{
   381  		ObjectMeta: metav1.ObjectMeta{
   382  			Name:      "my-azure-managed-control-plane",
   383  			Namespace: "default",
   384  			OwnerReferences: []metav1.OwnerReference{
   385  				{
   386  					Name:       "my-cluster",
   387  					Kind:       "Cluster",
   388  					APIVersion: clusterv1.GroupVersion.String(),
   389  				},
   390  			},
   391  		},
   392  		Spec: infrav1.AzureManagedControlPlaneSpec{},
   393  		Status: infrav1.AzureManagedControlPlaneStatus{
   394  			Ready:       true,
   395  			Initialized: true,
   396  		},
   397  	}
   398  	for _, change := range changes {
   399  		change(input)
   400  	}
   401  
   402  	return input
   403  }
   404  
   405  func getASOAzureClusterIdentity(changes ...func(identity *infrav1.AzureClusterIdentity)) *infrav1.AzureClusterIdentity {
   406  	input := &infrav1.AzureClusterIdentity{
   407  		ObjectMeta: metav1.ObjectMeta{
   408  			Name:      "my-azure-cluster-identity",
   409  			Namespace: "default",
   410  		},
   411  		Spec: infrav1.AzureClusterIdentitySpec{
   412  			ClientID: "fooClient",
   413  			TenantID: "fooTenant",
   414  		},
   415  	}
   416  
   417  	for _, change := range changes {
   418  		change(input)
   419  	}
   420  
   421  	return input
   422  }
   423  
   424  func getASOAzureClusterIdentitySecret(changes ...func(secret *corev1.Secret)) *corev1.Secret {
   425  	input := &corev1.Secret{
   426  		ObjectMeta: metav1.ObjectMeta{
   427  			Name:      "fooSecret",
   428  			Namespace: "default",
   429  		},
   430  		Data: map[string][]byte{
   431  			"clientSecret": []byte("fooSecret"),
   432  		},
   433  	}
   434  
   435  	for _, change := range changes {
   436  		change(input)
   437  	}
   438  
   439  	return input
   440  }
   441  
   442  func getASOSecret(cluster client.Object, changes ...func(secret *corev1.Secret)) *corev1.Secret {
   443  	input := &corev1.Secret{
   444  		ObjectMeta: metav1.ObjectMeta{
   445  			Name:      "my-cluster-aso-secret",
   446  			Namespace: "default",
   447  			Labels: map[string]string{
   448  				"my-cluster": "owned",
   449  			},
   450  			OwnerReferences: []metav1.OwnerReference{
   451  				{
   452  					APIVersion: cluster.GetObjectKind().GroupVersionKind().GroupVersion().String(),
   453  					Kind:       cluster.GetObjectKind().GroupVersionKind().Kind,
   454  					Name:       cluster.GetName(),
   455  					UID:        cluster.GetUID(),
   456  					Controller: ptr.To(true),
   457  				},
   458  			},
   459  		},
   460  		Data: map[string][]byte{
   461  			"AZURE_SUBSCRIPTION_ID": []byte("fooSubscription"),
   462  		},
   463  	}
   464  
   465  	for _, change := range changes {
   466  		change(input)
   467  	}
   468  
   469  	return input
   470  }