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  }