sigs.k8s.io/cluster-api-provider-azure@v1.14.3/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 over-provisioned and has delete machine annotation, select machines those first and then by oldest",
   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  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   254  			}),
   255  		},
   256  		{
   257  			name:            "if over-provisioned, select machines ordered by creation date",
   258  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   259  			desiredReplicas: 2,
   260  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   261  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   262  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   263  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   264  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   265  			},
   266  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   267  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   268  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   269  			}),
   270  		},
   271  		{
   272  			name:            "if over-provisioned and has delete machine annotation, prioritize those machines first over creation date",
   273  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   274  			desiredReplicas: 2,
   275  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   276  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   277  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour)), HasDeleteMachineAnnotation: true}),
   278  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   279  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   280  			},
   281  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   282  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour)), HasDeleteMachineAnnotation: true}),
   283  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   284  			}),
   285  		},
   286  		{
   287  			name:            "if over-provisioned, select machines ordered by newest first",
   288  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.NewestDeletePolicyType}),
   289  			desiredReplicas: 2,
   290  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   291  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   292  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   293  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   294  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   295  			},
   296  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   297  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   298  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   299  			}),
   300  		},
   301  		{
   302  			name:            "if over-provisioned and has delete machine annotation, select those machines first followed by newest",
   303  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.NewestDeletePolicyType}),
   304  			desiredReplicas: 2,
   305  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   306  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   307  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   308  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   309  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour)), HasDeleteMachineAnnotation: true}),
   310  			},
   311  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   312  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour)), HasDeleteMachineAnnotation: true}),
   313  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   314  			}),
   315  		},
   316  		{
   317  			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",
   318  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   319  			desiredReplicas: 2,
   320  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   321  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   322  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   323  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   324  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   325  			},
   326  			want: BeEmpty(),
   327  		},
   328  		{
   329  			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",
   330  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   331  			desiredReplicas: 2,
   332  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   333  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   334  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   335  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   336  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   337  			},
   338  			want: BeEmpty(),
   339  		},
   340  		{
   341  			name:            "if Azure is deleting 2 machines, we want to delete their AzureMachinePoolMachine equivalents",
   342  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   343  			desiredReplicas: 2,
   344  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   345  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   346  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   347  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: nil, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   348  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: nil, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   349  			},
   350  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   351  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: nil, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   352  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: infrav1.Deleting, DeletionTime: nil, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   353  			}),
   354  		},
   355  		{
   356  			name:            "if Azure is deleting 1 machine, pick another candidate for deletion",
   357  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{DeletePolicy: infrav1exp.OldestDeletePolicyType}),
   358  			desiredReplicas: 2,
   359  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   360  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(4 * time.Hour))}),
   361  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(3 * time.Hour))}),
   362  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   363  				"bar": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, DeletionTime: &deleteTime, CreationTime: metav1.NewTime(baseTime.Add(1 * time.Hour))}),
   364  			},
   365  			want: gomega.DiffEq([]infrav1exp.AzureMachinePoolMachine{
   366  				makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded, CreationTime: metav1.NewTime(baseTime.Add(2 * time.Hour))}),
   367  			}),
   368  		},
   369  		{
   370  			name:            "if maxUnavailable is 1, and 1 is not the latest model, delete it.",
   371  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &one}),
   372  			desiredReplicas: 3,
   373  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   374  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   375  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   376  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   377  			},
   378  			want: Equal([]infrav1exp.AzureMachinePoolMachine{
   379  				makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   380  			}),
   381  		},
   382  		{
   383  			name:            "if maxUnavailable is 1, and all are the latest model, delete nothing.",
   384  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &one}),
   385  			desiredReplicas: 3,
   386  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   387  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   388  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   389  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   390  			},
   391  			want: BeEmpty(),
   392  		},
   393  		{
   394  			name:            "if maxUnavailable is 2, and there are 2 with the latest model == false, delete 2.",
   395  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &two}),
   396  			desiredReplicas: 3,
   397  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   398  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   399  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   400  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   401  			},
   402  			want: Equal([]infrav1exp.AzureMachinePoolMachine{
   403  				makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   404  				makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   405  			}),
   406  		},
   407  		{
   408  			name:            "if maxUnavailable is 45%, and there are 2 with the latest model == false, delete 1.",
   409  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &fortyFivePercent}),
   410  			desiredReplicas: 3,
   411  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   412  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   413  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   414  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   415  			},
   416  			want: HaveLen(1),
   417  		},
   418  		{
   419  			name:            "if maxUnavailable is 30%, and there are 2 with the latest model == false, delete 0.",
   420  			strategy:        makeRollingUpdateStrategy(infrav1exp.MachineRollingUpdateDeployment{MaxUnavailable: &thirtyPercent}),
   421  			desiredReplicas: 3,
   422  			input: map[string]infrav1exp.AzureMachinePoolMachine{
   423  				"foo": makeAMPM(ampmOptions{Ready: true, LatestModel: true, ProvisioningState: succeeded}),
   424  				"bin": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   425  				"baz": makeAMPM(ampmOptions{Ready: true, LatestModel: false, ProvisioningState: succeeded}),
   426  			},
   427  			want: BeEmpty(),
   428  		},
   429  	}
   430  
   431  	for _, tt := range tests {
   432  		t.Run(tt.name, func(t *testing.T) {
   433  			g := NewWithT(t)
   434  			got, err := tt.strategy.SelectMachinesToDelete(context.Background(), tt.desiredReplicas, tt.input)
   435  			if tt.errStr == "" {
   436  				g.Expect(err).To(Succeed())
   437  				g.Expect(got).To(tt.want)
   438  			} else {
   439  				g.Expect(err).To(MatchError(tt.errStr))
   440  			}
   441  		})
   442  	}
   443  }
   444  
   445  func makeRollingUpdateStrategy(rolling infrav1exp.MachineRollingUpdateDeployment) *rollingUpdateStrategy {
   446  	return &rollingUpdateStrategy{
   447  		MachineRollingUpdateDeployment: rolling,
   448  	}
   449  }
   450  
   451  type ampmOptions struct {
   452  	Ready                      bool
   453  	LatestModel                bool
   454  	ProvisioningState          infrav1.ProvisioningState
   455  	CreationTime               metav1.Time
   456  	DeletionTime               *metav1.Time
   457  	HasDeleteMachineAnnotation bool
   458  }
   459  
   460  func makeAMPM(opts ampmOptions) infrav1exp.AzureMachinePoolMachine {
   461  	ampm := infrav1exp.AzureMachinePoolMachine{
   462  		ObjectMeta: metav1.ObjectMeta{
   463  			CreationTimestamp: opts.CreationTime,
   464  			DeletionTimestamp: opts.DeletionTime,
   465  			Annotations:       map[string]string{},
   466  		},
   467  		Status: infrav1exp.AzureMachinePoolMachineStatus{
   468  			Ready:              opts.Ready,
   469  			LatestModelApplied: opts.LatestModel,
   470  			ProvisioningState:  &opts.ProvisioningState,
   471  		},
   472  	}
   473  
   474  	if opts.HasDeleteMachineAnnotation {
   475  		ampm.Annotations[clusterv1.DeleteMachineAnnotation] = "true"
   476  	}
   477  
   478  	return ampm
   479  }