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