sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/scope/strategies/machinepool_deployments/machinepool_deployment_strategy_test.go (about)

     1  /*
     2  Copyright 2021 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 machinepool
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  	"time"
    23  
    24  	. "github.com/onsi/gomega"
    25  	"github.com/onsi/gomega/types"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/util/intstr"
    28  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    29  	infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1"
    30  	"sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomega"
    31  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    32  )
    33  
    34  func TestMachinePoolRollingUpdateStrategy_Type(t *testing.T) {
    35  	g := NewWithT(t)
    36  	strategy := NewMachinePoolDeploymentStrategy(infrav1exp.AzureMachinePoolDeploymentStrategy{
    37  		Type: infrav1exp.RollingUpdateAzureMachinePoolDeploymentStrategyType,
    38  	})
    39  	g.Expect(strategy.Type()).To(Equal(infrav1exp.RollingUpdateAzureMachinePoolDeploymentStrategyType))
    40  }
    41  
    42  func TestMachinePoolRollingUpdateStrategy_Surge(t *testing.T) {
    43  	var (
    44  		two           = intstr.FromInt(2)
    45  		twentyPercent = intstr.FromString("20%")
    46  	)
    47  
    48  	tests := []struct {
    49  		name            string
    50  		strategy        Surger
    51  		desiredReplicas int
    52  		want            int
    53  		errStr          string
    54  	}{
    55  		{
    56  			name:     "Strategy is empty",
    57  			strategy: &rollingUpdateStrategy{},
    58  			want:     1,
    59  		},
    60  		{
    61  			name: "MaxSurge is set to 2",
    62  			strategy: &rollingUpdateStrategy{
    63  				MachineRollingUpdateDeployment: infrav1exp.MachineRollingUpdateDeployment{
    64  					MaxSurge: &two,
    65  				},
    66  			},
    67  			want: 2,
    68  		},
    69  		{
    70  			name: "MaxSurge is set to 20% and desiredReplicas is 20",
    71  			strategy: &rollingUpdateStrategy{
    72  				MachineRollingUpdateDeployment: infrav1exp.MachineRollingUpdateDeployment{
    73  					MaxSurge: &twentyPercent,
    74  				},
    75  			},
    76  			desiredReplicas: 20,
    77  			want:            4,
    78  		},
    79  		{
    80  			name: "MaxSurge is set to 20% and desiredReplicas is 21; rounds up",
    81  			strategy: &rollingUpdateStrategy{
    82  				MachineRollingUpdateDeployment: infrav1exp.MachineRollingUpdateDeployment{
    83  					MaxSurge: &twentyPercent,
    84  				},
    85  			},
    86  			desiredReplicas: 21,
    87  			want:            5,
    88  		},
    89  	}
    90  
    91  	for _, tt := range tests {
    92  		t.Run(tt.name, func(t *testing.T) {
    93  			g := NewWithT(t)
    94  			got, err := tt.strategy.Surge(tt.desiredReplicas)
    95  			if tt.errStr == "" {
    96  				g.Expect(err).To(Succeed())
    97  				g.Expect(got).To(Equal(tt.want))
    98  			} else {
    99  				g.Expect(err).To(MatchError(tt.errStr))
   100  			}
   101  		})
   102  	}
   103  }
   104  
   105  func TestMachinePoolScope_maxUnavailable(t *testing.T) {
   106  	var (
   107  		two           = intstr.FromInt(2)
   108  		twentyPercent = intstr.FromString("20%")
   109  	)
   110  
   111  	tests := []struct {
   112  		name            string
   113  		strategy        *rollingUpdateStrategy
   114  		desiredReplicas int
   115  		want            int
   116  		errStr          string
   117  	}{
   118  		{
   119  			name:     "Strategy is empty",
   120  			strategy: &rollingUpdateStrategy{},
   121  		},
   122  		{
   123  			name: "MaxUnavailable is nil",
   124  			strategy: &rollingUpdateStrategy{
   125  				MachineRollingUpdateDeployment: infrav1exp.MachineRollingUpdateDeployment{},
   126  			},
   127  			want: 0,
   128  		},
   129  		{
   130  			name: "MaxUnavailable is set to 2",
   131  			strategy: &rollingUpdateStrategy{
   132  				MachineRollingUpdateDeployment: infrav1exp.MachineRollingUpdateDeployment{
   133  					MaxUnavailable: &two,
   134  				},
   135  			},
   136  			want: 2,
   137  		},
   138  		{
   139  			name: "MaxUnavailable is set to 20%",
   140  			strategy: &rollingUpdateStrategy{
   141  				MachineRollingUpdateDeployment: infrav1exp.MachineRollingUpdateDeployment{
   142  					MaxUnavailable: &twentyPercent,
   143  				},
   144  			},
   145  			desiredReplicas: 20,
   146  			want:            4,
   147  		},
   148  		{
   149  			name: "MaxUnavailable is set to 20% and it rounds down",
   150  			strategy: &rollingUpdateStrategy{
   151  				MachineRollingUpdateDeployment: infrav1exp.MachineRollingUpdateDeployment{
   152  					MaxUnavailable: &twentyPercent,
   153  				},
   154  			},
   155  			desiredReplicas: 21,
   156  			want:            4,
   157  		},
   158  	}
   159  
   160  	for _, tt := range tests {
   161  		t.Run(tt.name, func(t *testing.T) {
   162  			g := NewWithT(t)
   163  			got, err := tt.strategy.maxUnavailable(tt.desiredReplicas)
   164  			if tt.errStr == "" {
   165  				g.Expect(err).To(Succeed())
   166  				g.Expect(got).To(Equal(tt.want))
   167  			} else {
   168  				g.Expect(err).To(MatchError(tt.errStr))
   169  			}
   170  		})
   171  	}
   172  }
   173  
   174  func TestMachinePoolRollingUpdateStrategy_SelectMachinesToDelete(t *testing.T) {
   175  	var (
   176  		one              = intstr.FromInt(1)
   177  		two              = intstr.FromInt(2)
   178  		fortyFivePercent = intstr.FromString("45%")
   179  		thirtyPercent    = intstr.FromString("30%")
   180  		succeeded        = infrav1.Succeeded
   181  		baseTime         = time.Now().Add(-24 * time.Hour).Truncate(time.Microsecond)
   182  		deleteTime       = metav1.NewTime(time.Now())
   183  	)
   184  
   185  	tests := []struct {
   186  		name            string
   187  		strategy        DeleteSelector
   188  		input           map[string]infrav1exp.AzureMachinePoolMachine
   189  		desiredReplicas int32
   190  		want            types.GomegaMatcher
   191  		errStr          string
   192  	}{
   193  		{
   194  			name:            "should not select machines to delete if less than desired replica count",
   195  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{}),
   196  			desiredReplicas: 1,
   197  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   198  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   199  			},
   200  			want: Equal([]infrav1exp.AzureMachinePoolMachine{}),
   201  		},
   202  		{
   203  			name:            "if over-provisioned, select a machine with an out-of-date model",
   204  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{}),
   205  			desiredReplicas: 2,
   206  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   207  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   208  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   209  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   210  			},
   211  			want: Equal([]infrav1exp.AzureMachinePoolMachine{
   212  				makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   213  			}),
   214  		},
   215  		{
   216  			name:            "if over-provisioned, select a machine with an out-of-date model when using Random Delete Policy",
   217  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.RandomDeletePolicyType}),
   218  			desiredReplicas: 2,
   219  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   220  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   221  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   222  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   223  			},
   224  			want: Equal([]infrav1exp.AzureMachinePoolMachine{
   225  				makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   226  			}),
   227  		},
   228  		{
   229  			name:            "if over-provisioned, select the oldest machine",
   230  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   231  			desiredReplicas: 2,
   232  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   233  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   234  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   235  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   236  			},
   237  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   238  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   239  			}),
   240  		},
   241  		{
   242  			name:            "if machine has delete machine annotation, select those machines first",
   243  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   244  			desiredReplicas: 2,
   245  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   246  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   247  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour)), HasDeleteMachineAnnotation: true}),
   248  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   249  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   250  			},
   251  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   252  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour)), HasDeleteMachineAnnotation: true}),
   253  			}),
   254  		},
   255  		{
   256  			name:            "if over-provisioned, select machines ordered by creation date",
   257  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   258  			desiredReplicas: 2,
   259  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   260  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   261  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   262  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   263  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   264  			},
   265  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   266  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   267  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   268  			}),
   269  		},
   270  		{
   271  			name:            "if over-provisioned, select machines ordered by newest first",
   272  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.NewestDeletePolicyType}),
   273  			desiredReplicas: 2,
   274  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   275  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   276  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   277  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   278  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   279  			},
   280  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   281  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   282  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   283  			}),
   284  		},
   285  		{
   286  			name:            "if over-provisioned but with an equivalent number marked for deletion, nothing to do; this is the case where Azure has not yet caught up to capz",
   287  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   288  			desiredReplicas: 2,
   289  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   290  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   291  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   292  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   293  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   294  			},
   295  			want: BeEmpty(),
   296  		},
   297  		{
   298  			name:            "if Azure is deleting 2 machines, but we have already marked their AzureMachinePoolMachine equivalents for deletion, nothing to do; this is the case where capz has not yet caught up to Azure",
   299  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   300  			desiredReplicas: 2,
   301  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   302  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   303  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   304  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   305  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   306  			},
   307  			want: BeEmpty(),
   308  		},
   309  		{
   310  			name:            "if Azure is deleting 2 machines, we want to delete their AzureMachinePoolMachine equivalents",
   311  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   312  			desiredReplicas: 2,
   313  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   314  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   315  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   316  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: nil, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   317  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: nil, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   318  			},
   319  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   320  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: nil, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   321  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: nil, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   322  			}),
   323  		},
   324  		{
   325  			name:            "if Azure is deleting 1 machine, pick another candidate for deletion",
   326  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   327  			desiredReplicas: 2,
   328  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   329  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   330  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   331  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   332  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   333  			},
   334  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   335  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   336  			}),
   337  		},
   338  		{
   339  			name:            "if maxUnavailable is 1, and 1 is not the latest model, delete it.",
   340  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &one}),
   341  			desiredReplicas: 3,
   342  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   343  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   344  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   345  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   346  			},
   347  			want: Equal([]infrav1exp.AzureMachinePoolMachine{
   348  				makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   349  			}),
   350  		},
   351  		{
   352  			name:            "if maxUnavailable is 1, and all are the latest model, delete nothing.",
   353  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &one}),
   354  			desiredReplicas: 3,
   355  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   356  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   357  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   358  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   359  			},
   360  			want: BeEmpty(),
   361  		},
   362  		{
   363  			name:            "if maxUnavailable is 2, and there are 2 with the latest model == false, delete 2.",
   364  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &two}),
   365  			desiredReplicas: 3,
   366  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   367  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   368  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   369  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   370  			},
   371  			want: Equal([]infrav1exp.AzureMachinePoolMachine{
   372  				makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   373  				makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   374  			}),
   375  		},
   376  		{
   377  			name:            "if maxUnavailable is 45%, and there are 2 with the latest model == false, delete 1.",
   378  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &fortyFivePercent}),
   379  			desiredReplicas: 3,
   380  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   381  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   382  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   383  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   384  			},
   385  			want: HaveLen(1),
   386  		},
   387  		{
   388  			name:            "if maxUnavailable is 30%, and there are 2 with the latest model == false, delete 0.",
   389  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &thirtyPercent}),
   390  			desiredReplicas: 3,
   391  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   392  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   393  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   394  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   395  			},
   396  			want: BeEmpty(),
   397  		},
   398  	}
   399  
   400  	for _, tt := range tests {
   401  		t.Run(tt.name, func(t *testing.T) {
   402  			g := NewWithT(t)
   403  			got, err := tt.strategy.SelectMachinesToDelete(context.Background(), tt.desiredReplicas, tt.input)
   404  			if tt.errStr == "" {
   405  				g.Expect(err).To(Succeed())
   406  				g.Expect(got).To(tt.want)
   407  			} else {
   408  				g.Expect(err).To(MatchError(tt.errStr))
   409  			}
   410  		})
   411  	}
   412  }
   413  
   414  func makeRollingUpdateStrategy(rolling infrav1exp.MachineRollingUpdateDeployment) *rollingUpdateStrategy {
   415  	return &rollingUpdateStrategy{
   416  		MachineRollingUpdateDeployment: rolling,
   417  	}
   418  }
   419  
   420  type ampmOptions struct {
   421  	Ready                      bool
   422  	LatestModel                bool
   423  	ProvisioningState          infrav1.ProvisioningState
   424  	CreationTime               metav1.Time
   425  	DeletionTime               *metav1.Time
   426  	HasDeleteMachineAnnotation bool
   427  }
   428  
   429  func makeAMPM(opts ampmOptions) infrav1exp.AzureMachinePoolMachine {
   430  	ampm := infrav1exp.AzureMachinePoolMachine{
   431  		ObjectMeta: metav1.ObjectMeta{
   432  			CreationTimestamp: opts.CreationTime,
   433  			DeletionTimestamp: opts.DeletionTime,
   434  			Annotations:       map[string]string{},
   435  		},
   436  		Status: infrav1exp.AzureMachinePoolMachineStatus{
   437  			Ready:              opts.Ready,
   438  			LatestModelApplied: opts.LatestModel,
   439  			ProvisioningState:  &opts.ProvisioningState,
   440  		},
   441  	}
   442  
   443  	if opts.HasDeleteMachineAnnotation {
   444  		ampm.Annotations[clusterv1.DeleteMachineAnnotation] = "true"
   445  	}
   446  
   447  	return ampm
   448  }