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 }