sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/machinedeployment/machinedeployment_sync_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 machinedeployment
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	. "github.com/onsi/gomega"
    27  	"github.com/pkg/errors"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	apirand "k8s.io/apimachinery/pkg/util/rand"
    32  	"k8s.io/client-go/tools/record"
    33  	"k8s.io/utils/ptr"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    36  
    37  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    38  	capierrors "sigs.k8s.io/cluster-api/errors"
    39  	"sigs.k8s.io/cluster-api/internal/controllers/machinedeployment/mdutil"
    40  	"sigs.k8s.io/cluster-api/util/conditions"
    41  )
    42  
    43  func TestCalculateStatus(t *testing.T) {
    44  	msStatusError := capierrors.MachineSetStatusError("some failure")
    45  
    46  	var tests = map[string]struct {
    47  		machineSets    []*clusterv1.MachineSet
    48  		newMachineSet  *clusterv1.MachineSet
    49  		deployment     *clusterv1.MachineDeployment
    50  		expectedStatus clusterv1.MachineDeploymentStatus
    51  	}{
    52  		"all machines are running": {
    53  			machineSets: []*clusterv1.MachineSet{{
    54  				Spec: clusterv1.MachineSetSpec{
    55  					Replicas: ptr.To[int32](2),
    56  				},
    57  				Status: clusterv1.MachineSetStatus{
    58  					Selector:           "",
    59  					AvailableReplicas:  2,
    60  					ReadyReplicas:      2,
    61  					Replicas:           2,
    62  					ObservedGeneration: 1,
    63  				},
    64  			}},
    65  			newMachineSet: &clusterv1.MachineSet{
    66  				Spec: clusterv1.MachineSetSpec{
    67  					Replicas: ptr.To[int32](2),
    68  				},
    69  				Status: clusterv1.MachineSetStatus{
    70  					Selector:           "",
    71  					AvailableReplicas:  2,
    72  					ReadyReplicas:      2,
    73  					Replicas:           2,
    74  					ObservedGeneration: 1,
    75  				},
    76  			},
    77  			deployment: &clusterv1.MachineDeployment{
    78  				ObjectMeta: metav1.ObjectMeta{
    79  					Generation: 2,
    80  				},
    81  				Spec: clusterv1.MachineDeploymentSpec{
    82  					Replicas: ptr.To[int32](2),
    83  				},
    84  			},
    85  			expectedStatus: clusterv1.MachineDeploymentStatus{
    86  				ObservedGeneration:  2,
    87  				Replicas:            2,
    88  				UpdatedReplicas:     2,
    89  				ReadyReplicas:       2,
    90  				AvailableReplicas:   2,
    91  				UnavailableReplicas: 0,
    92  				Phase:               "Running",
    93  			},
    94  		},
    95  		"scaling up": {
    96  			machineSets: []*clusterv1.MachineSet{{
    97  				Spec: clusterv1.MachineSetSpec{
    98  					Replicas: ptr.To[int32](2),
    99  				},
   100  				Status: clusterv1.MachineSetStatus{
   101  					Selector:           "",
   102  					AvailableReplicas:  1,
   103  					ReadyReplicas:      1,
   104  					Replicas:           2,
   105  					ObservedGeneration: 1,
   106  				},
   107  			}},
   108  			newMachineSet: &clusterv1.MachineSet{
   109  				Spec: clusterv1.MachineSetSpec{
   110  					Replicas: ptr.To[int32](2),
   111  				},
   112  				Status: clusterv1.MachineSetStatus{
   113  					Selector:           "",
   114  					AvailableReplicas:  1,
   115  					ReadyReplicas:      1,
   116  					Replicas:           2,
   117  					ObservedGeneration: 1,
   118  				},
   119  			},
   120  			deployment: &clusterv1.MachineDeployment{
   121  				ObjectMeta: metav1.ObjectMeta{
   122  					Generation: 2,
   123  				},
   124  				Spec: clusterv1.MachineDeploymentSpec{
   125  					Replicas: ptr.To[int32](2),
   126  				},
   127  			},
   128  			expectedStatus: clusterv1.MachineDeploymentStatus{
   129  				ObservedGeneration:  2,
   130  				Replicas:            2,
   131  				UpdatedReplicas:     2,
   132  				ReadyReplicas:       1,
   133  				AvailableReplicas:   1,
   134  				UnavailableReplicas: 1,
   135  				Phase:               "ScalingUp",
   136  			},
   137  		},
   138  		"scaling down": {
   139  			machineSets: []*clusterv1.MachineSet{{
   140  				Spec: clusterv1.MachineSetSpec{
   141  					Replicas: ptr.To[int32](2),
   142  				},
   143  				Status: clusterv1.MachineSetStatus{
   144  					Selector:           "",
   145  					AvailableReplicas:  3,
   146  					ReadyReplicas:      2,
   147  					Replicas:           2,
   148  					ObservedGeneration: 1,
   149  				},
   150  			}},
   151  			newMachineSet: &clusterv1.MachineSet{
   152  				Spec: clusterv1.MachineSetSpec{
   153  					Replicas: ptr.To[int32](2),
   154  				},
   155  				Status: clusterv1.MachineSetStatus{
   156  					Selector:           "",
   157  					AvailableReplicas:  3,
   158  					ReadyReplicas:      2,
   159  					Replicas:           2,
   160  					ObservedGeneration: 1,
   161  				},
   162  			},
   163  			deployment: &clusterv1.MachineDeployment{
   164  				ObjectMeta: metav1.ObjectMeta{
   165  					Generation: 2,
   166  				},
   167  				Spec: clusterv1.MachineDeploymentSpec{
   168  					Replicas: ptr.To[int32](2),
   169  				},
   170  			},
   171  			expectedStatus: clusterv1.MachineDeploymentStatus{
   172  				ObservedGeneration:  2,
   173  				Replicas:            2,
   174  				UpdatedReplicas:     2,
   175  				ReadyReplicas:       2,
   176  				AvailableReplicas:   3,
   177  				UnavailableReplicas: 0,
   178  				Phase:               "ScalingDown",
   179  			},
   180  		},
   181  		"MachineSet failed": {
   182  			machineSets: []*clusterv1.MachineSet{{
   183  				Spec: clusterv1.MachineSetSpec{
   184  					Replicas: ptr.To[int32](2),
   185  				},
   186  				Status: clusterv1.MachineSetStatus{
   187  					Selector:           "",
   188  					AvailableReplicas:  0,
   189  					ReadyReplicas:      0,
   190  					Replicas:           2,
   191  					ObservedGeneration: 1,
   192  					FailureReason:      &msStatusError,
   193  				},
   194  			}},
   195  			newMachineSet: &clusterv1.MachineSet{
   196  				Spec: clusterv1.MachineSetSpec{
   197  					Replicas: ptr.To[int32](2),
   198  				},
   199  				Status: clusterv1.MachineSetStatus{
   200  					Selector:           "",
   201  					AvailableReplicas:  0,
   202  					ReadyReplicas:      0,
   203  					Replicas:           2,
   204  					ObservedGeneration: 1,
   205  				},
   206  			},
   207  			deployment: &clusterv1.MachineDeployment{
   208  				ObjectMeta: metav1.ObjectMeta{
   209  					Generation: 2,
   210  				},
   211  				Spec: clusterv1.MachineDeploymentSpec{
   212  					Replicas: ptr.To[int32](2),
   213  				},
   214  			},
   215  			expectedStatus: clusterv1.MachineDeploymentStatus{
   216  				ObservedGeneration:  2,
   217  				Replicas:            2,
   218  				UpdatedReplicas:     2,
   219  				ReadyReplicas:       0,
   220  				AvailableReplicas:   0,
   221  				UnavailableReplicas: 2,
   222  				Phase:               "Failed",
   223  			},
   224  		},
   225  	}
   226  
   227  	for name, test := range tests {
   228  		t.Run(name, func(t *testing.T) {
   229  			g := NewWithT(t)
   230  
   231  			actualStatus := calculateStatus(test.machineSets, test.newMachineSet, test.deployment)
   232  			g.Expect(actualStatus).To(BeComparableTo(test.expectedStatus))
   233  		})
   234  	}
   235  }
   236  
   237  func TestScaleMachineSet(t *testing.T) {
   238  	testCases := []struct {
   239  		name              string
   240  		machineDeployment *clusterv1.MachineDeployment
   241  		machineSet        *clusterv1.MachineSet
   242  		newScale          int32
   243  		error             error
   244  	}{
   245  		{
   246  			name: "It fails when new MachineSet has no replicas",
   247  			machineDeployment: &clusterv1.MachineDeployment{
   248  				Spec: clusterv1.MachineDeploymentSpec{
   249  					Replicas: ptr.To[int32](2),
   250  				},
   251  			},
   252  			machineSet: &clusterv1.MachineSet{
   253  				ObjectMeta: metav1.ObjectMeta{
   254  					Namespace: "foo",
   255  					Name:      "bar",
   256  				},
   257  			},
   258  			error: errors.Errorf("spec.replicas for MachineSet foo/bar is nil, this is unexpected"),
   259  		},
   260  		{
   261  			name: "It fails when new MachineDeployment has no replicas",
   262  			machineDeployment: &clusterv1.MachineDeployment{
   263  				ObjectMeta: metav1.ObjectMeta{
   264  					Namespace: "foo",
   265  					Name:      "bar",
   266  				},
   267  				Spec: clusterv1.MachineDeploymentSpec{},
   268  			},
   269  			machineSet: &clusterv1.MachineSet{
   270  				ObjectMeta: metav1.ObjectMeta{
   271  					Namespace: "foo",
   272  					Name:      "bar",
   273  				},
   274  				Spec: clusterv1.MachineSetSpec{
   275  					Replicas: ptr.To[int32](2),
   276  				},
   277  			},
   278  			error: errors.Errorf("spec.replicas for MachineDeployment foo/bar is nil, this is unexpected"),
   279  		},
   280  		{
   281  			name: "Scale up",
   282  			machineDeployment: &clusterv1.MachineDeployment{
   283  				ObjectMeta: metav1.ObjectMeta{
   284  					Namespace: "foo",
   285  					Name:      "bar",
   286  				},
   287  				Spec: clusterv1.MachineDeploymentSpec{
   288  					Strategy: &clusterv1.MachineDeploymentStrategy{
   289  						Type: clusterv1.RollingUpdateMachineDeploymentStrategyType,
   290  						RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{
   291  							MaxUnavailable: intOrStrPtr(0),
   292  							MaxSurge:       intOrStrPtr(2),
   293  						},
   294  					},
   295  					Replicas: ptr.To[int32](2),
   296  				},
   297  			},
   298  			machineSet: &clusterv1.MachineSet{
   299  				ObjectMeta: metav1.ObjectMeta{
   300  					Namespace: "foo",
   301  					Name:      "bar",
   302  				},
   303  				Spec: clusterv1.MachineSetSpec{
   304  					Replicas: ptr.To[int32](0),
   305  				},
   306  			},
   307  			newScale: 2,
   308  		},
   309  		{
   310  			name: "Scale down",
   311  			machineDeployment: &clusterv1.MachineDeployment{
   312  				ObjectMeta: metav1.ObjectMeta{
   313  					Namespace: "foo",
   314  					Name:      "bar",
   315  				},
   316  				Spec: clusterv1.MachineDeploymentSpec{
   317  					Strategy: &clusterv1.MachineDeploymentStrategy{
   318  						Type: clusterv1.RollingUpdateMachineDeploymentStrategyType,
   319  						RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{
   320  							MaxUnavailable: intOrStrPtr(0),
   321  							MaxSurge:       intOrStrPtr(2),
   322  						},
   323  					},
   324  					Replicas: ptr.To[int32](2),
   325  				},
   326  			},
   327  			machineSet: &clusterv1.MachineSet{
   328  				ObjectMeta: metav1.ObjectMeta{
   329  					Namespace: "foo",
   330  					Name:      "bar",
   331  				},
   332  				Spec: clusterv1.MachineSetSpec{
   333  					Replicas: ptr.To[int32](4),
   334  				},
   335  			},
   336  			newScale: 2,
   337  		},
   338  		{
   339  			name: "Same replicas does not scale",
   340  			machineDeployment: &clusterv1.MachineDeployment{
   341  				ObjectMeta: metav1.ObjectMeta{
   342  					Namespace: "foo",
   343  					Name:      "bar",
   344  				},
   345  				Spec: clusterv1.MachineDeploymentSpec{
   346  					Strategy: &clusterv1.MachineDeploymentStrategy{
   347  						Type: clusterv1.RollingUpdateMachineDeploymentStrategyType,
   348  						RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{
   349  							MaxUnavailable: intOrStrPtr(0),
   350  							MaxSurge:       intOrStrPtr(2),
   351  						},
   352  					},
   353  					Replicas: ptr.To[int32](2),
   354  				},
   355  			},
   356  			machineSet: &clusterv1.MachineSet{
   357  				ObjectMeta: metav1.ObjectMeta{
   358  					Namespace: "foo",
   359  					Name:      "bar",
   360  				},
   361  				Spec: clusterv1.MachineSetSpec{
   362  					Replicas: ptr.To[int32](2),
   363  				},
   364  			},
   365  			newScale: 2,
   366  		},
   367  	}
   368  	for _, tc := range testCases {
   369  		t.Run(tc.name, func(t *testing.T) {
   370  			g := NewWithT(t)
   371  
   372  			resources := []client.Object{
   373  				tc.machineDeployment,
   374  				tc.machineSet,
   375  			}
   376  
   377  			r := &Reconciler{
   378  				Client:   fake.NewClientBuilder().WithObjects(resources...).Build(),
   379  				recorder: record.NewFakeRecorder(32),
   380  			}
   381  
   382  			err := r.scaleMachineSet(context.Background(), tc.machineSet, tc.newScale, tc.machineDeployment)
   383  			if tc.error != nil {
   384  				g.Expect(err.Error()).To(BeEquivalentTo(tc.error.Error()))
   385  				return
   386  			}
   387  
   388  			g.Expect(err).ToNot(HaveOccurred())
   389  
   390  			freshMachineSet := &clusterv1.MachineSet{}
   391  			err = r.Client.Get(ctx, client.ObjectKeyFromObject(tc.machineSet), freshMachineSet)
   392  			g.Expect(err).ToNot(HaveOccurred())
   393  
   394  			g.Expect(*freshMachineSet.Spec.Replicas).To(BeEquivalentTo(tc.newScale))
   395  
   396  			expectedMachineSetAnnotations := map[string]string{
   397  				clusterv1.DesiredReplicasAnnotation: fmt.Sprintf("%d", *tc.machineDeployment.Spec.Replicas),
   398  				clusterv1.MaxReplicasAnnotation:     fmt.Sprintf("%d", (*tc.machineDeployment.Spec.Replicas)+mdutil.MaxSurge(*tc.machineDeployment)),
   399  			}
   400  			g.Expect(freshMachineSet.GetAnnotations()).To(BeEquivalentTo(expectedMachineSetAnnotations))
   401  		})
   402  	}
   403  }
   404  
   405  func newTestMachineDeployment(pds *int32, replicas, statusReplicas, updatedReplicas, availableReplicas int32, conditions clusterv1.Conditions) *clusterv1.MachineDeployment {
   406  	d := &clusterv1.MachineDeployment{
   407  		ObjectMeta: metav1.ObjectMeta{
   408  			Name: "progress-test",
   409  		},
   410  		Spec: clusterv1.MachineDeploymentSpec{
   411  			ProgressDeadlineSeconds: pds,
   412  			Replicas:                &replicas,
   413  			Strategy: &clusterv1.MachineDeploymentStrategy{
   414  				Type: clusterv1.RollingUpdateMachineDeploymentStrategyType,
   415  				RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{
   416  					MaxUnavailable: intOrStrPtr(0),
   417  					MaxSurge:       intOrStrPtr(1),
   418  					DeletePolicy:   ptr.To("Oldest"),
   419  				},
   420  			},
   421  		},
   422  		Status: clusterv1.MachineDeploymentStatus{
   423  			Replicas:          statusReplicas,
   424  			UpdatedReplicas:   updatedReplicas,
   425  			AvailableReplicas: availableReplicas,
   426  			Conditions:        conditions,
   427  		},
   428  	}
   429  	return d
   430  }
   431  
   432  // helper to create MS with given availableReplicas.
   433  func newTestMachinesetWithReplicas(name string, specReplicas, statusReplicas, availableReplicas int32, conditions clusterv1.Conditions) *clusterv1.MachineSet {
   434  	return &clusterv1.MachineSet{
   435  		ObjectMeta: metav1.ObjectMeta{
   436  			Name:              name,
   437  			CreationTimestamp: metav1.Time{},
   438  			Namespace:         metav1.NamespaceDefault,
   439  		},
   440  		Spec: clusterv1.MachineSetSpec{
   441  			Replicas: ptr.To[int32](specReplicas),
   442  		},
   443  		Status: clusterv1.MachineSetStatus{
   444  			AvailableReplicas: availableReplicas,
   445  			Replicas:          statusReplicas,
   446  			Conditions:        conditions,
   447  		},
   448  	}
   449  }
   450  
   451  func TestSyncDeploymentStatus(t *testing.T) {
   452  	pds := int32(60)
   453  	tests := []struct {
   454  		name               string
   455  		d                  *clusterv1.MachineDeployment
   456  		oldMachineSets     []*clusterv1.MachineSet
   457  		newMachineSet      *clusterv1.MachineSet
   458  		expectedConditions []*clusterv1.Condition
   459  	}{
   460  		{
   461  			name:           "Deployment not available: MachineDeploymentAvailableCondition should exist and be false",
   462  			d:              newTestMachineDeployment(&pds, 3, 2, 2, 2, clusterv1.Conditions{}),
   463  			oldMachineSets: []*clusterv1.MachineSet{},
   464  			newMachineSet:  newTestMachinesetWithReplicas("foo", 3, 2, 2, clusterv1.Conditions{}),
   465  			expectedConditions: []*clusterv1.Condition{
   466  				{
   467  					Type:     clusterv1.MachineDeploymentAvailableCondition,
   468  					Status:   corev1.ConditionFalse,
   469  					Severity: clusterv1.ConditionSeverityWarning,
   470  					Reason:   clusterv1.WaitingForAvailableMachinesReason,
   471  				},
   472  			},
   473  		},
   474  		{
   475  			name:           "Deployment Available: MachineDeploymentAvailableCondition should exist and be true",
   476  			d:              newTestMachineDeployment(&pds, 3, 3, 3, 3, clusterv1.Conditions{}),
   477  			oldMachineSets: []*clusterv1.MachineSet{},
   478  			newMachineSet:  newTestMachinesetWithReplicas("foo", 3, 3, 3, clusterv1.Conditions{}),
   479  			expectedConditions: []*clusterv1.Condition{
   480  				{
   481  					Type:   clusterv1.MachineDeploymentAvailableCondition,
   482  					Status: corev1.ConditionTrue,
   483  				},
   484  			},
   485  		},
   486  		{
   487  			name:           "MachineSet exist: MachineSetReadyCondition should exist and mirror MachineSet Ready condition",
   488  			d:              newTestMachineDeployment(&pds, 3, 3, 3, 3, clusterv1.Conditions{}),
   489  			oldMachineSets: []*clusterv1.MachineSet{},
   490  			newMachineSet: newTestMachinesetWithReplicas("foo", 3, 3, 3, clusterv1.Conditions{
   491  				{
   492  					Type:    clusterv1.ReadyCondition,
   493  					Status:  corev1.ConditionFalse,
   494  					Reason:  "TestErrorResaon",
   495  					Message: "test error messsage",
   496  				},
   497  			}),
   498  			expectedConditions: []*clusterv1.Condition{
   499  				{
   500  					Type:    clusterv1.MachineSetReadyCondition,
   501  					Status:  corev1.ConditionFalse,
   502  					Reason:  "TestErrorResaon",
   503  					Message: "test error messsage",
   504  				},
   505  			},
   506  		},
   507  		{
   508  			name:           "MachineSet doesn't exist: MachineSetReadyCondition should exist and be false",
   509  			d:              newTestMachineDeployment(&pds, 3, 3, 3, 3, clusterv1.Conditions{}),
   510  			oldMachineSets: []*clusterv1.MachineSet{},
   511  			newMachineSet:  nil,
   512  			expectedConditions: []*clusterv1.Condition{
   513  				{
   514  					Type:     clusterv1.MachineSetReadyCondition,
   515  					Status:   corev1.ConditionFalse,
   516  					Severity: clusterv1.ConditionSeverityInfo,
   517  					Reason:   clusterv1.WaitingForMachineSetFallbackReason,
   518  				},
   519  			},
   520  		},
   521  	}
   522  
   523  	for _, test := range tests {
   524  		t.Run(test.name, func(t *testing.T) {
   525  			g := NewWithT(t)
   526  			r := &Reconciler{
   527  				Client:   fake.NewClientBuilder().Build(),
   528  				recorder: record.NewFakeRecorder(32),
   529  			}
   530  			allMachineSets := append(test.oldMachineSets, test.newMachineSet)
   531  			err := r.syncDeploymentStatus(allMachineSets, test.newMachineSet, test.d)
   532  			g.Expect(err).ToNot(HaveOccurred())
   533  			assertConditions(t, test.d, test.expectedConditions...)
   534  		})
   535  	}
   536  }
   537  
   538  func TestComputeDesiredMachineSet(t *testing.T) {
   539  	duration5s := &metav1.Duration{Duration: 5 * time.Second}
   540  	duration10s := &metav1.Duration{Duration: 10 * time.Second}
   541  
   542  	infraRef := corev1.ObjectReference{
   543  		Kind:       "GenericInfrastructureMachineTemplate",
   544  		Name:       "infra-template-1",
   545  		APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
   546  	}
   547  	bootstrapRef := corev1.ObjectReference{
   548  		Kind:       "GenericBootstrapConfigTemplate",
   549  		Name:       "bootstrap-template-1",
   550  		APIVersion: "bootstrap.cluster.x-k8s.io/v1beta1",
   551  	}
   552  
   553  	deployment := &clusterv1.MachineDeployment{
   554  		ObjectMeta: metav1.ObjectMeta{
   555  			Namespace:   "default",
   556  			Name:        "md1",
   557  			Annotations: map[string]string{"top-level-annotation": "top-level-annotation-value"},
   558  		},
   559  		Spec: clusterv1.MachineDeploymentSpec{
   560  			ClusterName:     "test-cluster",
   561  			Replicas:        ptr.To[int32](3),
   562  			MinReadySeconds: ptr.To[int32](10),
   563  			Strategy: &clusterv1.MachineDeploymentStrategy{
   564  				Type: clusterv1.RollingUpdateMachineDeploymentStrategyType,
   565  				RollingUpdate: &clusterv1.MachineRollingUpdateDeployment{
   566  					MaxSurge:       intOrStrPtr(1),
   567  					DeletePolicy:   ptr.To("Random"),
   568  					MaxUnavailable: intOrStrPtr(0),
   569  				},
   570  			},
   571  			Selector: metav1.LabelSelector{
   572  				MatchLabels: map[string]string{"k1": "v1"},
   573  			},
   574  			Template: clusterv1.MachineTemplateSpec{
   575  				ObjectMeta: clusterv1.ObjectMeta{
   576  					Labels:      map[string]string{"machine-label1": "machine-value1"},
   577  					Annotations: map[string]string{"machine-annotation1": "machine-value1"},
   578  				},
   579  				Spec: clusterv1.MachineSpec{
   580  					Version:           ptr.To("v1.25.3"),
   581  					InfrastructureRef: infraRef,
   582  					Bootstrap: clusterv1.Bootstrap{
   583  						ConfigRef: &bootstrapRef,
   584  					},
   585  					NodeDrainTimeout:        duration10s,
   586  					NodeVolumeDetachTimeout: duration10s,
   587  					NodeDeletionTimeout:     duration10s,
   588  				},
   589  			},
   590  		},
   591  	}
   592  
   593  	skeletonMSBasedOnMD := &clusterv1.MachineSet{
   594  		ObjectMeta: metav1.ObjectMeta{
   595  			Namespace:   "default",
   596  			Labels:      map[string]string{"machine-label1": "machine-value1"},
   597  			Annotations: map[string]string{"top-level-annotation": "top-level-annotation-value"},
   598  		},
   599  		Spec: clusterv1.MachineSetSpec{
   600  			ClusterName:     "test-cluster",
   601  			Replicas:        ptr.To[int32](3),
   602  			MinReadySeconds: 10,
   603  			DeletePolicy:    string(clusterv1.RandomMachineSetDeletePolicy),
   604  			Selector:        metav1.LabelSelector{MatchLabels: map[string]string{"k1": "v1"}},
   605  			Template:        *deployment.Spec.Template.DeepCopy(),
   606  		},
   607  	}
   608  
   609  	t.Run("should compute a new MachineSet when no old MachineSets exist", func(t *testing.T) {
   610  		expectedMS := skeletonMSBasedOnMD.DeepCopy()
   611  
   612  		g := NewWithT(t)
   613  		actualMS, err := (&Reconciler{}).computeDesiredMachineSet(ctx, deployment, nil, nil)
   614  		g.Expect(err).ToNot(HaveOccurred())
   615  		assertMachineSet(g, actualMS, expectedMS)
   616  	})
   617  
   618  	t.Run("should compute a new MachineSet when old MachineSets exist", func(t *testing.T) {
   619  		oldMS := skeletonMSBasedOnMD.DeepCopy()
   620  		oldMS.Spec.Replicas = ptr.To[int32](2)
   621  
   622  		expectedMS := skeletonMSBasedOnMD.DeepCopy()
   623  		expectedMS.Spec.Replicas = ptr.To[int32](2) // 4 (maxsurge+replicas) - 2 (replicas of old ms) = 2
   624  
   625  		g := NewWithT(t)
   626  		actualMS, err := (&Reconciler{}).computeDesiredMachineSet(ctx, deployment, nil, []*clusterv1.MachineSet{oldMS})
   627  		g.Expect(err).ToNot(HaveOccurred())
   628  		assertMachineSet(g, actualMS, expectedMS)
   629  	})
   630  
   631  	t.Run("should compute the updated MachineSet when no old MachineSets exists", func(t *testing.T) {
   632  		uniqueID := apirand.String(5)
   633  		existingMS := skeletonMSBasedOnMD.DeepCopy()
   634  		// computeDesiredMachineSet should retain the UID, name and the "machine-template-hash" label value
   635  		// of the existing machine.
   636  		// Other fields like labels, annotations, node timeout, etc are expected to change.
   637  		existingMSUID := types.UID("abc-123-uid")
   638  		existingMS.UID = existingMSUID
   639  		existingMS.Name = deployment.Name + "-" + uniqueID
   640  		existingMS.Labels = map[string]string{
   641  			clusterv1.MachineDeploymentUniqueLabel: uniqueID,
   642  			"ms-label-1":                           "ms-value-1",
   643  		}
   644  		existingMS.Annotations = nil
   645  		existingMS.Spec.Template.Labels = map[string]string{
   646  			clusterv1.MachineDeploymentUniqueLabel: uniqueID,
   647  			"ms-label-2":                           "ms-value-2",
   648  		}
   649  		existingMS.Spec.Template.Annotations = nil
   650  		existingMS.Spec.Template.Spec.NodeDrainTimeout = duration5s
   651  		existingMS.Spec.Template.Spec.NodeDeletionTimeout = duration5s
   652  		existingMS.Spec.Template.Spec.NodeVolumeDetachTimeout = duration5s
   653  		existingMS.Spec.DeletePolicy = string(clusterv1.NewestMachineSetDeletePolicy)
   654  		existingMS.Spec.MinReadySeconds = 0
   655  
   656  		expectedMS := skeletonMSBasedOnMD.DeepCopy()
   657  		expectedMS.UID = existingMSUID
   658  		expectedMS.Name = deployment.Name + "-" + uniqueID
   659  		expectedMS.Labels[clusterv1.MachineDeploymentUniqueLabel] = uniqueID
   660  		expectedMS.Spec.Template.Labels[clusterv1.MachineDeploymentUniqueLabel] = uniqueID
   661  
   662  		g := NewWithT(t)
   663  		actualMS, err := (&Reconciler{}).computeDesiredMachineSet(ctx, deployment, existingMS, nil)
   664  		g.Expect(err).ToNot(HaveOccurred())
   665  		assertMachineSet(g, actualMS, expectedMS)
   666  	})
   667  
   668  	t.Run("should compute the updated MachineSet when old MachineSets exist", func(t *testing.T) {
   669  		uniqueID := apirand.String(5)
   670  		existingMS := skeletonMSBasedOnMD.DeepCopy()
   671  		existingMSUID := types.UID("abc-123-uid")
   672  		existingMS.UID = existingMSUID
   673  		existingMS.Name = deployment.Name + "-" + uniqueID
   674  		existingMS.Labels = map[string]string{
   675  			clusterv1.MachineDeploymentUniqueLabel: uniqueID,
   676  			"ms-label-1":                           "ms-value-1",
   677  		}
   678  		existingMS.Annotations = nil
   679  		existingMS.Spec.Template.Labels = map[string]string{
   680  			clusterv1.MachineDeploymentUniqueLabel: uniqueID,
   681  			"ms-label-2":                           "ms-value-2",
   682  		}
   683  		existingMS.Spec.Template.Annotations = nil
   684  		existingMS.Spec.Template.Spec.NodeDrainTimeout = duration5s
   685  		existingMS.Spec.Template.Spec.NodeDeletionTimeout = duration5s
   686  		existingMS.Spec.Template.Spec.NodeVolumeDetachTimeout = duration5s
   687  		existingMS.Spec.DeletePolicy = string(clusterv1.NewestMachineSetDeletePolicy)
   688  		existingMS.Spec.MinReadySeconds = 0
   689  
   690  		oldMS := skeletonMSBasedOnMD.DeepCopy()
   691  		oldMS.Spec.Replicas = ptr.To[int32](2)
   692  
   693  		// Note: computeDesiredMachineSet does not modify the replicas on the updated MachineSet.
   694  		// Therefore, even though we have the old machineset with replicas 2 the updatedMS does not
   695  		// get modified replicas (2 = 4(maxsuge+spec.replica) - 2(oldMS replicas)).
   696  		// Nb. The final replicas of the MachineSet are calculated elsewhere.
   697  		expectedMS := skeletonMSBasedOnMD.DeepCopy()
   698  		expectedMS.UID = existingMSUID
   699  		expectedMS.Name = deployment.Name + "-" + uniqueID
   700  		expectedMS.Labels[clusterv1.MachineDeploymentUniqueLabel] = uniqueID
   701  		expectedMS.Spec.Template.Labels[clusterv1.MachineDeploymentUniqueLabel] = uniqueID
   702  
   703  		g := NewWithT(t)
   704  		actualMS, err := (&Reconciler{}).computeDesiredMachineSet(ctx, deployment, existingMS, []*clusterv1.MachineSet{oldMS})
   705  		g.Expect(err).ToNot(HaveOccurred())
   706  		assertMachineSet(g, actualMS, expectedMS)
   707  	})
   708  
   709  	t.Run("should compute the updated MachineSet when no old MachineSets exists (", func(t *testing.T) {
   710  		// Set rollout strategy to "OnDelete".
   711  		deployment := deployment.DeepCopy()
   712  		deployment.Spec.Strategy = &clusterv1.MachineDeploymentStrategy{
   713  			Type:          clusterv1.OnDeleteMachineDeploymentStrategyType,
   714  			RollingUpdate: nil,
   715  		}
   716  
   717  		uniqueID := apirand.String(5)
   718  		existingMS := skeletonMSBasedOnMD.DeepCopy()
   719  		// computeDesiredMachineSet should retain the UID, name and the "machine-template-hash" label value
   720  		// of the existing machine.
   721  		// Other fields like labels, annotations, node timeout, etc are expected to change.
   722  		existingMSUID := types.UID("abc-123-uid")
   723  		existingMS.UID = existingMSUID
   724  		existingMS.Name = deployment.Name + "-" + uniqueID
   725  		existingMS.Labels = map[string]string{
   726  			clusterv1.MachineDeploymentUniqueLabel: uniqueID,
   727  			"ms-label-1":                           "ms-value-1",
   728  		}
   729  		existingMS.Annotations = nil
   730  		existingMS.Spec.Template.Labels = map[string]string{
   731  			clusterv1.MachineDeploymentUniqueLabel: uniqueID,
   732  			"ms-label-2":                           "ms-value-2",
   733  		}
   734  		existingMS.Spec.Template.Annotations = nil
   735  		existingMS.Spec.Template.Spec.NodeDrainTimeout = duration5s
   736  		existingMS.Spec.Template.Spec.NodeDeletionTimeout = duration5s
   737  		existingMS.Spec.Template.Spec.NodeVolumeDetachTimeout = duration5s
   738  		existingMS.Spec.DeletePolicy = string(clusterv1.NewestMachineSetDeletePolicy)
   739  		existingMS.Spec.MinReadySeconds = 0
   740  
   741  		expectedMS := skeletonMSBasedOnMD.DeepCopy()
   742  		expectedMS.UID = existingMSUID
   743  		expectedMS.Name = deployment.Name + "-" + uniqueID
   744  		expectedMS.Labels[clusterv1.MachineDeploymentUniqueLabel] = uniqueID
   745  		expectedMS.Spec.Template.Labels[clusterv1.MachineDeploymentUniqueLabel] = uniqueID
   746  		// DeletePolicy should be empty with rollout strategy "OnDelete".
   747  		expectedMS.Spec.DeletePolicy = ""
   748  
   749  		g := NewWithT(t)
   750  		actualMS, err := (&Reconciler{}).computeDesiredMachineSet(ctx, deployment, existingMS, nil)
   751  		g.Expect(err).ToNot(HaveOccurred())
   752  		assertMachineSet(g, actualMS, expectedMS)
   753  	})
   754  }
   755  
   756  func assertMachineSet(g *WithT, actualMS *clusterv1.MachineSet, expectedMS *clusterv1.MachineSet) {
   757  	// check UID
   758  	if expectedMS.UID != "" {
   759  		g.Expect(actualMS.UID).Should(Equal(expectedMS.UID))
   760  	}
   761  	// Check Name
   762  	if expectedMS.Name != "" {
   763  		g.Expect(actualMS.Name).Should(Equal(expectedMS.Name))
   764  	}
   765  	// Check Namespace
   766  	g.Expect(actualMS.Namespace).Should(Equal(expectedMS.Namespace))
   767  
   768  	// Check Replicas
   769  	g.Expect(actualMS.Spec.Replicas).ShouldNot(BeNil())
   770  	g.Expect(actualMS.Spec.Replicas).Should(HaveValue(Equal(*expectedMS.Spec.Replicas)))
   771  
   772  	// Check ClusterName
   773  	g.Expect(actualMS.Spec.ClusterName).Should(Equal(expectedMS.Spec.ClusterName))
   774  
   775  	// Check Labels
   776  	for k, v := range expectedMS.Labels {
   777  		g.Expect(actualMS.Labels).Should(HaveKeyWithValue(k, v))
   778  	}
   779  	for k, v := range expectedMS.Spec.Template.Labels {
   780  		g.Expect(actualMS.Spec.Template.Labels).Should(HaveKeyWithValue(k, v))
   781  	}
   782  	// Verify that the labels also has the unique identifier key.
   783  	g.Expect(actualMS.Labels).Should(HaveKey(clusterv1.MachineDeploymentUniqueLabel))
   784  	g.Expect(actualMS.Spec.Template.Labels).Should(HaveKey(clusterv1.MachineDeploymentUniqueLabel))
   785  
   786  	// Check Annotations
   787  	// Note: More nuanced validation of the Revision annotation calculations are done when testing `ComputeMachineSetAnnotations`.
   788  	for k, v := range expectedMS.Annotations {
   789  		g.Expect(actualMS.Annotations).Should(HaveKeyWithValue(k, v))
   790  	}
   791  	for k, v := range expectedMS.Spec.Template.Annotations {
   792  		g.Expect(actualMS.Spec.Template.Annotations).Should(HaveKeyWithValue(k, v))
   793  	}
   794  
   795  	// Check MinReadySeconds
   796  	g.Expect(actualMS.Spec.MinReadySeconds).Should(Equal(expectedMS.Spec.MinReadySeconds))
   797  
   798  	// Check DeletePolicy
   799  	g.Expect(actualMS.Spec.DeletePolicy).Should(Equal(expectedMS.Spec.DeletePolicy))
   800  
   801  	// Check MachineTemplateSpec
   802  	g.Expect(actualMS.Spec.Template.Spec).Should(BeComparableTo(expectedMS.Spec.Template.Spec))
   803  }
   804  
   805  // asserts the conditions set on the Getter object.
   806  // TODO: replace this with util.condition.MatchConditions (or a new matcher in controller runtime komega).
   807  func assertConditions(t *testing.T, from conditions.Getter, conditions ...*clusterv1.Condition) {
   808  	t.Helper()
   809  
   810  	for _, condition := range conditions {
   811  		assertCondition(t, from, condition)
   812  	}
   813  }
   814  
   815  // asserts whether a condition of type is set on the Getter object
   816  // when the condition is true, asserting the reason/severity/message
   817  // for the condition are avoided.
   818  func assertCondition(t *testing.T, from conditions.Getter, condition *clusterv1.Condition) {
   819  	t.Helper()
   820  
   821  	g := NewWithT(t)
   822  	g.Expect(conditions.Has(from, condition.Type)).To(BeTrue())
   823  
   824  	if condition.Status == corev1.ConditionTrue {
   825  		conditions.IsTrue(from, condition.Type)
   826  	} else {
   827  		conditionToBeAsserted := conditions.Get(from, condition.Type)
   828  		g.Expect(conditionToBeAsserted.Status).To(Equal(condition.Status))
   829  		g.Expect(conditionToBeAsserted.Severity).To(Equal(condition.Severity))
   830  		g.Expect(conditionToBeAsserted.Reason).To(Equal(condition.Reason))
   831  		if condition.Message != "" {
   832  			g.Expect(conditionToBeAsserted.Message).To(Equal(condition.Message))
   833  		}
   834  	}
   835  }
   836  
   837  func Test_computeNewMachineSetName(t *testing.T) {
   838  	tests := []struct {
   839  		base       string
   840  		wantPrefix string
   841  	}{
   842  		{
   843  			"a",
   844  			"a",
   845  		},
   846  		{
   847  			fmt.Sprintf("%058d", 0),
   848  			fmt.Sprintf("%058d", 0),
   849  		},
   850  		{
   851  			fmt.Sprintf("%059d", 0),
   852  			fmt.Sprintf("%058d", 0),
   853  		},
   854  		{
   855  			fmt.Sprintf("%0100d", 0),
   856  			fmt.Sprintf("%058d", 0),
   857  		},
   858  	}
   859  	for _, tt := range tests {
   860  		t.Run(fmt.Sprintf("base=%q, wantPrefix=%q", tt.base, tt.wantPrefix), func(t *testing.T) {
   861  			got, gotSuffix := computeNewMachineSetName(tt.base)
   862  			gotPrefix := strings.TrimSuffix(got, gotSuffix)
   863  			if gotPrefix != tt.wantPrefix {
   864  				t.Errorf("computeNewMachineSetName() = (%v, %v) wantPrefix %v", got, gotSuffix, tt.wantPrefix)
   865  			}
   866  			if len(got) > maxNameLength {
   867  				t.Errorf("expected %s to be of max length %d", got, maxNameLength)
   868  			}
   869  		})
   870  	}
   871  }