sigs.k8s.io/cluster-api-provider-azure@v1.17.0/pkg/mutators/azureasomanagedmachinepool_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 mutators
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"testing"
    23  
    24  	asocontainerservicev1 "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001"
    25  	"github.com/google/go-cmp/cmp"
    26  	. "github.com/onsi/gomega"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/utils/ptr"
    31  	infrav1alpha "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha1"
    32  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    33  	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
    36  )
    37  
    38  func TestSetAgentPoolDefaults(t *testing.T) {
    39  	ctx := context.Background()
    40  	g := NewGomegaWithT(t)
    41  
    42  	tests := []struct {
    43  		name                  string
    44  		asoManagedMachinePool *infrav1alpha.AzureASOManagedMachinePool
    45  		machinePool           *expv1.MachinePool
    46  		expected              []*unstructured.Unstructured
    47  		expectedErr           error
    48  	}{
    49  		{
    50  			name: "no ManagedClustersAgentPool",
    51  			asoManagedMachinePool: &infrav1alpha.AzureASOManagedMachinePool{
    52  				Spec: infrav1alpha.AzureASOManagedMachinePoolSpec{
    53  					AzureASOManagedMachinePoolTemplateResourceSpec: infrav1alpha.AzureASOManagedMachinePoolTemplateResourceSpec{
    54  						Resources: []runtime.RawExtension{},
    55  					},
    56  				},
    57  			},
    58  			expectedErr: ErrNoManagedClustersAgentPoolDefined,
    59  		},
    60  		{
    61  			name: "success",
    62  			asoManagedMachinePool: &infrav1alpha.AzureASOManagedMachinePool{
    63  				Spec: infrav1alpha.AzureASOManagedMachinePoolSpec{
    64  					AzureASOManagedMachinePoolTemplateResourceSpec: infrav1alpha.AzureASOManagedMachinePoolTemplateResourceSpec{
    65  						Resources: []runtime.RawExtension{
    66  							{
    67  								Raw: apJSON(g, &asocontainerservicev1.ManagedClustersAgentPool{}),
    68  							},
    69  						},
    70  					},
    71  				},
    72  			},
    73  			machinePool: &expv1.MachinePool{
    74  				Spec: expv1.MachinePoolSpec{
    75  					Replicas: ptr.To[int32](1),
    76  					Template: clusterv1.MachineTemplateSpec{
    77  						Spec: clusterv1.MachineSpec{
    78  							Version: ptr.To("vcapi k8s version"),
    79  						},
    80  					},
    81  				},
    82  			},
    83  			expected: []*unstructured.Unstructured{
    84  				apUnstructured(g, &asocontainerservicev1.ManagedClustersAgentPool{
    85  					Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
    86  						OrchestratorVersion: ptr.To("capi k8s version"),
    87  						Count:               ptr.To(1),
    88  					},
    89  				}),
    90  			},
    91  		},
    92  	}
    93  
    94  	for _, test := range tests {
    95  		t.Run(test.name, func(t *testing.T) {
    96  			g := NewGomegaWithT(t)
    97  
    98  			mutator := SetAgentPoolDefaults(nil, test.machinePool)
    99  			actual, err := ApplyMutators(ctx, test.asoManagedMachinePool.Spec.Resources, mutator)
   100  			if test.expectedErr != nil {
   101  				g.Expect(err).To(MatchError(test.expectedErr))
   102  			} else {
   103  				g.Expect(err).NotTo(HaveOccurred())
   104  			}
   105  			g.Expect(cmp.Diff(test.expected, actual)).To(BeEmpty())
   106  		})
   107  	}
   108  }
   109  
   110  func TestSetAgentPoolOrchestratorVersion(t *testing.T) {
   111  	ctx := context.Background()
   112  
   113  	tests := []struct {
   114  		name        string
   115  		machinePool *expv1.MachinePool
   116  		agentPool   *asocontainerservicev1.ManagedClustersAgentPool
   117  		expected    *asocontainerservicev1.ManagedClustersAgentPool
   118  		expectedErr error
   119  	}{
   120  		{
   121  			name: "no CAPI opinion",
   122  			machinePool: &expv1.MachinePool{
   123  				Spec: expv1.MachinePoolSpec{
   124  					Template: clusterv1.MachineTemplateSpec{
   125  						Spec: clusterv1.MachineSpec{
   126  							Version: nil,
   127  						},
   128  					},
   129  				},
   130  			},
   131  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   132  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   133  					OrchestratorVersion: ptr.To("user k8s version"),
   134  				},
   135  			},
   136  			expected: &asocontainerservicev1.ManagedClustersAgentPool{
   137  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   138  					OrchestratorVersion: ptr.To("user k8s version"),
   139  				},
   140  			},
   141  		},
   142  		{
   143  			name: "set from CAPI opinion",
   144  			machinePool: &expv1.MachinePool{
   145  				Spec: expv1.MachinePoolSpec{
   146  					Template: clusterv1.MachineTemplateSpec{
   147  						Spec: clusterv1.MachineSpec{
   148  							Version: ptr.To("vcapi k8s version"),
   149  						},
   150  					},
   151  				},
   152  			},
   153  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   154  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   155  					OrchestratorVersion: nil,
   156  				},
   157  			},
   158  			expected: &asocontainerservicev1.ManagedClustersAgentPool{
   159  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   160  					OrchestratorVersion: ptr.To("capi k8s version"),
   161  				},
   162  			},
   163  		},
   164  		{
   165  			name: "user value matching CAPI ok",
   166  			machinePool: &expv1.MachinePool{
   167  				Spec: expv1.MachinePoolSpec{
   168  					Template: clusterv1.MachineTemplateSpec{
   169  						Spec: clusterv1.MachineSpec{
   170  							Version: ptr.To("vcapi k8s version"),
   171  						},
   172  					},
   173  				},
   174  			},
   175  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   176  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   177  					OrchestratorVersion: ptr.To("capi k8s version"),
   178  				},
   179  			},
   180  			expected: &asocontainerservicev1.ManagedClustersAgentPool{
   181  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   182  					OrchestratorVersion: ptr.To("capi k8s version"),
   183  				},
   184  			},
   185  		},
   186  		{
   187  			name: "incompatible",
   188  			machinePool: &expv1.MachinePool{
   189  				ObjectMeta: metav1.ObjectMeta{
   190  					Name: "mp",
   191  				},
   192  				Spec: expv1.MachinePoolSpec{
   193  					Template: clusterv1.MachineTemplateSpec{
   194  						Spec: clusterv1.MachineSpec{
   195  							Version: ptr.To("vcapi k8s version"),
   196  						},
   197  					},
   198  				},
   199  			},
   200  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   201  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   202  					OrchestratorVersion: ptr.To("user k8s version"),
   203  				},
   204  			},
   205  			expectedErr: Incompatible{
   206  				mutation: mutation{
   207  					location: ".spec.orchestratorVersion",
   208  					val:      "capi k8s version",
   209  					reason:   "because MachinePool mp's spec.template.spec.version is vcapi k8s version",
   210  				},
   211  				userVal: "user k8s version",
   212  			},
   213  		},
   214  	}
   215  
   216  	s := runtime.NewScheme()
   217  	NewGomegaWithT(t).Expect(asocontainerservicev1.AddToScheme(s)).To(Succeed())
   218  
   219  	for _, test := range tests {
   220  		t.Run(test.name, func(t *testing.T) {
   221  			g := NewGomegaWithT(t)
   222  
   223  			before := test.agentPool.DeepCopy()
   224  			uap := apUnstructured(g, test.agentPool)
   225  
   226  			err := setAgentPoolOrchestratorVersion(ctx, test.machinePool, "", uap)
   227  			g.Expect(s.Convert(uap, test.agentPool, nil)).To(Succeed())
   228  			if test.expectedErr != nil {
   229  				g.Expect(err).To(MatchError(test.expectedErr))
   230  				g.Expect(cmp.Diff(before, test.agentPool)).To(BeEmpty()) // errors should never modify the resource.
   231  			} else {
   232  				g.Expect(err).NotTo(HaveOccurred())
   233  				g.Expect(cmp.Diff(test.expected, test.agentPool)).To(BeEmpty())
   234  			}
   235  		})
   236  	}
   237  }
   238  
   239  func TestReconcileAutoscaling(t *testing.T) {
   240  	tests := []struct {
   241  		name        string
   242  		autoscaling bool
   243  		machinePool *expv1.MachinePool
   244  		expected    *expv1.MachinePool
   245  		expectedErr error
   246  	}{
   247  		{
   248  			name:        "autoscaling disabled removes aks annotation",
   249  			autoscaling: false,
   250  			machinePool: &expv1.MachinePool{
   251  				ObjectMeta: metav1.ObjectMeta{
   252  					Annotations: map[string]string{
   253  						clusterv1.ReplicasManagedByAnnotation: infrav1alpha.ReplicasManagedByAKS,
   254  					},
   255  				},
   256  			},
   257  			expected: &expv1.MachinePool{
   258  				ObjectMeta: metav1.ObjectMeta{
   259  					Annotations: map[string]string{},
   260  				},
   261  			},
   262  		},
   263  		{
   264  			name:        "autoscaling disabled leaves other annotation",
   265  			autoscaling: false,
   266  			machinePool: &expv1.MachinePool{
   267  				ObjectMeta: metav1.ObjectMeta{
   268  					Annotations: map[string]string{
   269  						clusterv1.ReplicasManagedByAnnotation: "not-" + infrav1alpha.ReplicasManagedByAKS,
   270  					},
   271  				},
   272  			},
   273  			expected: &expv1.MachinePool{
   274  				ObjectMeta: metav1.ObjectMeta{
   275  					Annotations: map[string]string{
   276  						clusterv1.ReplicasManagedByAnnotation: "not-" + infrav1alpha.ReplicasManagedByAKS,
   277  					},
   278  				},
   279  			},
   280  		},
   281  		{
   282  			name:        "autoscaling enabled, manager undefined adds annotation",
   283  			autoscaling: true,
   284  			machinePool: &expv1.MachinePool{
   285  				ObjectMeta: metav1.ObjectMeta{
   286  					Annotations: map[string]string{},
   287  				},
   288  			},
   289  			expected: &expv1.MachinePool{
   290  				ObjectMeta: metav1.ObjectMeta{
   291  					Annotations: map[string]string{
   292  						clusterv1.ReplicasManagedByAnnotation: infrav1alpha.ReplicasManagedByAKS,
   293  					},
   294  				},
   295  			},
   296  		},
   297  		{
   298  			name:        "autoscaling enabled, manager already set",
   299  			autoscaling: true,
   300  			machinePool: &expv1.MachinePool{
   301  				ObjectMeta: metav1.ObjectMeta{
   302  					Annotations: map[string]string{
   303  						clusterv1.ReplicasManagedByAnnotation: infrav1alpha.ReplicasManagedByAKS,
   304  					},
   305  				},
   306  			},
   307  			expected: &expv1.MachinePool{
   308  				ObjectMeta: metav1.ObjectMeta{
   309  					Annotations: map[string]string{
   310  						clusterv1.ReplicasManagedByAnnotation: infrav1alpha.ReplicasManagedByAKS,
   311  					},
   312  				},
   313  			},
   314  		},
   315  		{
   316  			name:        "autoscaling enabled, manager set to something else",
   317  			autoscaling: true,
   318  			machinePool: &expv1.MachinePool{
   319  				ObjectMeta: metav1.ObjectMeta{
   320  					Name: "mp",
   321  					Annotations: map[string]string{
   322  						clusterv1.ReplicasManagedByAnnotation: "not-" + infrav1alpha.ReplicasManagedByAKS,
   323  					},
   324  				},
   325  			},
   326  			expectedErr: errors.New("failed to enable autoscaling, replicas are already being managed by not-aks according to MachinePool mp's cluster.x-k8s.io/replicas-managed-by annotation"),
   327  		},
   328  	}
   329  
   330  	for _, test := range tests {
   331  		t.Run(test.name, func(t *testing.T) {
   332  			g := NewGomegaWithT(t)
   333  
   334  			agentPool := &asocontainerservicev1.ManagedClustersAgentPool{
   335  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   336  					EnableAutoScaling: ptr.To(test.autoscaling),
   337  				},
   338  			}
   339  
   340  			err := reconcileAutoscaling(apUnstructured(g, agentPool), test.machinePool)
   341  
   342  			if test.expectedErr != nil {
   343  				g.Expect(err).To(MatchError(test.expectedErr))
   344  			} else {
   345  				g.Expect(err).NotTo(HaveOccurred())
   346  				g.Expect(cmp.Diff(test.expected, test.machinePool)).To(BeEmpty())
   347  			}
   348  		})
   349  	}
   350  }
   351  
   352  func TestSetAgentPoolCount(t *testing.T) {
   353  	ctx := context.Background()
   354  
   355  	tests := []struct {
   356  		name              string
   357  		machinePool       *expv1.MachinePool
   358  		agentPool         *asocontainerservicev1.ManagedClustersAgentPool
   359  		existingAgentPool *asocontainerservicev1.ManagedClustersAgentPool
   360  		expected          *asocontainerservicev1.ManagedClustersAgentPool
   361  		expectedErr       error
   362  	}{
   363  		{
   364  			name: "no CAPI opinion",
   365  			machinePool: &expv1.MachinePool{
   366  				Spec: expv1.MachinePoolSpec{
   367  					Replicas: nil,
   368  				},
   369  			},
   370  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   371  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   372  					Count: ptr.To(2),
   373  				},
   374  			},
   375  			expected: &asocontainerservicev1.ManagedClustersAgentPool{
   376  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   377  					Count: ptr.To(2),
   378  				},
   379  			},
   380  		},
   381  		{
   382  			name: "autoscaling enabled",
   383  			machinePool: &expv1.MachinePool{
   384  				ObjectMeta: metav1.ObjectMeta{
   385  					Annotations: map[string]string{
   386  						clusterv1.ReplicasManagedByAnnotation: infrav1alpha.ReplicasManagedByAKS,
   387  					},
   388  				},
   389  				Spec: expv1.MachinePoolSpec{
   390  					Replicas: ptr.To[int32](3),
   391  				},
   392  			},
   393  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   394  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   395  					Count: nil,
   396  				},
   397  			},
   398  			existingAgentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   399  				Status: asocontainerservicev1.ManagedClusters_AgentPool_STATUS{
   400  					Count: ptr.To(2),
   401  				},
   402  			},
   403  			expected: &asocontainerservicev1.ManagedClustersAgentPool{
   404  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   405  					Count: nil,
   406  				},
   407  			},
   408  		},
   409  		{
   410  			name: "set from CAPI opinion",
   411  			machinePool: &expv1.MachinePool{
   412  				Spec: expv1.MachinePoolSpec{
   413  					Replicas: ptr.To[int32](1),
   414  				},
   415  			},
   416  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   417  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   418  					Count: nil,
   419  				},
   420  			},
   421  			expected: &asocontainerservicev1.ManagedClustersAgentPool{
   422  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   423  					Count: ptr.To(1),
   424  				},
   425  			},
   426  		},
   427  		{
   428  			name: "user value matching CAPI ok",
   429  			machinePool: &expv1.MachinePool{
   430  				Spec: expv1.MachinePoolSpec{
   431  					Replicas: ptr.To[int32](1),
   432  				},
   433  			},
   434  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   435  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   436  					Count: ptr.To(1),
   437  				},
   438  			},
   439  			expected: &asocontainerservicev1.ManagedClustersAgentPool{
   440  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   441  					Count: ptr.To(1),
   442  				},
   443  			},
   444  		},
   445  		{
   446  			name: "incompatible",
   447  			machinePool: &expv1.MachinePool{
   448  				ObjectMeta: metav1.ObjectMeta{
   449  					Name: "mp",
   450  				},
   451  				Spec: expv1.MachinePoolSpec{
   452  					Replicas: ptr.To[int32](1),
   453  				},
   454  			},
   455  			agentPool: &asocontainerservicev1.ManagedClustersAgentPool{
   456  				Spec: asocontainerservicev1.ManagedClusters_AgentPool_Spec{
   457  					Count: ptr.To(2),
   458  				},
   459  			},
   460  			expectedErr: Incompatible{
   461  				mutation: mutation{
   462  					location: ".spec.count",
   463  					val:      int64(1),
   464  					reason:   "because MachinePool mp's spec.replicas is 1",
   465  				},
   466  				userVal: int64(2),
   467  			},
   468  		},
   469  	}
   470  
   471  	s := runtime.NewScheme()
   472  	NewGomegaWithT(t).Expect(asocontainerservicev1.AddToScheme(s)).To(Succeed())
   473  
   474  	for _, test := range tests {
   475  		t.Run(test.name, func(t *testing.T) {
   476  			g := NewGomegaWithT(t)
   477  
   478  			var c client.Client
   479  			if test.existingAgentPool != nil {
   480  				c = fakeclient.NewClientBuilder().
   481  					WithScheme(s).
   482  					WithObjects(test.existingAgentPool).
   483  					Build()
   484  			}
   485  
   486  			before := test.agentPool.DeepCopy()
   487  			uap := apUnstructured(g, test.agentPool)
   488  
   489  			err := setAgentPoolCount(ctx, c, test.machinePool, "", uap)
   490  			g.Expect(s.Convert(uap, test.agentPool, nil)).To(Succeed())
   491  			if test.expectedErr != nil {
   492  				g.Expect(err).To(MatchError(test.expectedErr))
   493  				g.Expect(cmp.Diff(before, test.agentPool)).To(BeEmpty()) // errors should never modify the resource.
   494  			} else {
   495  				g.Expect(err).NotTo(HaveOccurred())
   496  				g.Expect(cmp.Diff(test.expected, test.agentPool)).To(BeEmpty())
   497  			}
   498  		})
   499  	}
   500  }