2051 lines
62 KiB
Go
2051 lines
62 KiB
Go
/*
|
|
Copyright 2020 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package clusterapi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider"
|
|
"k8s.io/autoscaler/cluster-autoscaler/config"
|
|
gpuapis "k8s.io/autoscaler/cluster-autoscaler/utils/gpu"
|
|
"k8s.io/client-go/tools/cache"
|
|
)
|
|
|
|
const (
|
|
testNamespace = "test-namespace"
|
|
)
|
|
|
|
func TestNodeGroupNewNodeGroupConstructor(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
annotations map[string]string
|
|
errors bool
|
|
replicas int32
|
|
minSize int
|
|
maxSize int
|
|
nodeCount int
|
|
expectNil bool
|
|
}
|
|
|
|
var testCases = []testCase{{
|
|
description: "errors because minSize is invalid",
|
|
annotations: map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "-1",
|
|
nodeGroupMaxSizeAnnotationKey: "0",
|
|
},
|
|
errors: true,
|
|
}, {
|
|
description: "errors because maxSize is invalid",
|
|
annotations: map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "0",
|
|
nodeGroupMaxSizeAnnotationKey: "-1",
|
|
},
|
|
errors: true,
|
|
}, {
|
|
description: "errors because minSize > maxSize",
|
|
annotations: map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "0",
|
|
},
|
|
errors: true,
|
|
}, {
|
|
description: "errors because maxSize < minSize",
|
|
annotations: map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "0",
|
|
},
|
|
errors: true,
|
|
}, {
|
|
description: "no error: min=0, max=0",
|
|
minSize: 0,
|
|
maxSize: 0,
|
|
replicas: 0,
|
|
errors: false,
|
|
expectNil: true,
|
|
}, {
|
|
description: "no error: min=0, max=1",
|
|
annotations: map[string]string{
|
|
nodeGroupMaxSizeAnnotationKey: "1",
|
|
},
|
|
minSize: 0,
|
|
maxSize: 1,
|
|
replicas: 0,
|
|
errors: false,
|
|
expectNil: true,
|
|
}, {
|
|
description: "no error: min=1, max=10, replicas=5",
|
|
annotations: map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
minSize: 1,
|
|
maxSize: 10,
|
|
replicas: 5,
|
|
nodeCount: 5,
|
|
errors: false,
|
|
expectNil: true,
|
|
}, {
|
|
description: "no error and expect notNil: min=max=2",
|
|
annotations: map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "2",
|
|
nodeGroupMaxSizeAnnotationKey: "2",
|
|
},
|
|
nodeCount: 1,
|
|
minSize: 2,
|
|
maxSize: 2,
|
|
replicas: 1,
|
|
errors: false,
|
|
expectNil: false,
|
|
}}
|
|
|
|
newNodeGroup := func(controller *machineController, testConfig *testConfig) (*nodegroup, error) {
|
|
if testConfig.machineDeployment != nil {
|
|
return newNodeGroupFromScalableResource(controller, testConfig.machineDeployment)
|
|
}
|
|
return newNodeGroupFromScalableResource(controller, testConfig.machineSet)
|
|
}
|
|
|
|
test := func(t *testing.T, tc testCase, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
ng, err := newNodeGroup(controller, testConfig)
|
|
if tc.errors && err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
|
|
if tc.errors {
|
|
// if the test case is expected to error then
|
|
// don't assert the remainder
|
|
return
|
|
}
|
|
|
|
if tc.expectNil && ng == nil {
|
|
// if the test case is expected to return nil then
|
|
// don't assert the remainder
|
|
return
|
|
}
|
|
|
|
if ng == nil {
|
|
t.Fatal("expected nodegroup to be non-nil")
|
|
}
|
|
|
|
var expectedName, expectedKind string
|
|
|
|
if testConfig.machineDeployment != nil {
|
|
expectedKind = machineDeploymentKind
|
|
expectedName = testConfig.spec.machineDeploymentName
|
|
} else {
|
|
expectedKind = machineSetKind
|
|
expectedName = testConfig.spec.machineSetName
|
|
}
|
|
|
|
expectedID := path.Join(expectedKind, testConfig.spec.namespace, expectedName)
|
|
expectedDebug := fmt.Sprintf(debugFormat, expectedID, tc.minSize, tc.maxSize, tc.replicas)
|
|
|
|
if ng.scalableResource.Name() != expectedName {
|
|
t.Errorf("expected %q, got %q", expectedName, ng.scalableResource.Name())
|
|
}
|
|
|
|
if ng.scalableResource.Namespace() != testConfig.spec.namespace {
|
|
t.Errorf("expected %q, got %q", testConfig.spec.namespace, ng.scalableResource.Namespace())
|
|
}
|
|
|
|
if ng.MinSize() != tc.minSize {
|
|
t.Errorf("expected %v, got %v", tc.minSize, ng.MinSize())
|
|
}
|
|
|
|
if ng.MaxSize() != tc.maxSize {
|
|
t.Errorf("expected %v, got %v", tc.maxSize, ng.MaxSize())
|
|
}
|
|
|
|
if ng.Id() != expectedID {
|
|
t.Errorf("expected %q, got %q", expectedID, ng.Id())
|
|
}
|
|
|
|
if ng.Debug() != expectedDebug {
|
|
t.Errorf("expected %q, got %q", expectedDebug, ng.Debug())
|
|
}
|
|
|
|
if exists := ng.Exist(); !exists {
|
|
t.Errorf("expected %t, got %t", true, exists)
|
|
}
|
|
|
|
if _, err := ng.Create(); err != cloudprovider.ErrAlreadyExist {
|
|
t.Error("expected error")
|
|
}
|
|
|
|
if err := ng.Delete(); err != cloudprovider.ErrNotImplemented {
|
|
t.Error("expected error")
|
|
}
|
|
|
|
if result := ng.Autoprovisioned(); result {
|
|
t.Errorf("expected %t, got %t", false, result)
|
|
}
|
|
|
|
// We test ng.Nodes() in TestControllerNodeGroupsNodeCount
|
|
}
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
test(t, tc, createMachineSetTestConfig(RandomString(6), RandomString(6), RandomString(6), tc.nodeCount, tc.annotations, nil, nil))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
test(t, tc, createMachineDeploymentTestConfig(RandomString(6), RandomString(6), RandomString(6), tc.nodeCount, tc.annotations, nil, nil))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupIncreaseSizeErrors(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
delta int
|
|
initial int32
|
|
errorMsg string
|
|
}
|
|
|
|
testCases := []testCase{{
|
|
description: "errors because delta is negative",
|
|
delta: -1,
|
|
initial: 3,
|
|
errorMsg: "size increase must be positive",
|
|
}, {
|
|
description: "errors because initial+delta > maxSize",
|
|
delta: 8,
|
|
initial: 3,
|
|
errorMsg: "size increase too large - desired:11 max:10",
|
|
}}
|
|
|
|
test := func(t *testing.T, tc *testCase, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0].(*nodegroup)
|
|
currReplicas, err := ng.TargetSize()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if currReplicas != int(tc.initial) {
|
|
t.Errorf("expected %v, got %v", tc.initial, currReplicas)
|
|
}
|
|
|
|
errors := len(tc.errorMsg) > 0
|
|
|
|
err = ng.IncreaseSize(tc.delta)
|
|
if errors && err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
|
|
if !errors && err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), tc.errorMsg) {
|
|
t.Errorf("expected error message to contain %q, got %q", tc.errorMsg, err.Error())
|
|
}
|
|
|
|
gvr, err := ng.scalableResource.GroupVersionResource()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
scalableResource, err := ng.machineController.managementScaleClient.Scales(testConfig.spec.namespace).
|
|
Get(context.TODO(), gvr.GroupResource(), ng.scalableResource.Name(), metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if scalableResource.Spec.Replicas != tc.initial {
|
|
t.Errorf("expected %v, got %v", tc.initial, scalableResource.Spec.Replicas)
|
|
}
|
|
}
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
annotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
test(t, &tc, createMachineSetTestConfig(RandomString(6), RandomString(6), RandomString(6), int(tc.initial), annotations, nil, nil))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
annotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
test(t, &tc, createMachineDeploymentTestConfig(RandomString(6), RandomString(6), RandomString(6), int(tc.initial), annotations, nil, nil))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupIncreaseSize(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
delta int
|
|
initial int32
|
|
expected int32
|
|
}
|
|
|
|
test := func(t *testing.T, tc *testCase, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0].(*nodegroup)
|
|
currReplicas, err := ng.TargetSize()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if currReplicas != int(tc.initial) {
|
|
t.Errorf("initially expected %v, got %v", tc.initial, currReplicas)
|
|
}
|
|
|
|
if err := ng.IncreaseSize(tc.delta); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
gvr, err := ng.scalableResource.GroupVersionResource()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
scalableResource, err := ng.machineController.managementScaleClient.Scales(ng.scalableResource.Namespace()).
|
|
Get(context.TODO(), gvr.GroupResource(), ng.scalableResource.Name(), metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if scalableResource.Spec.Replicas != tc.expected {
|
|
t.Errorf("expected %v, got %v", tc.expected, scalableResource.Spec.Replicas)
|
|
}
|
|
}
|
|
|
|
annotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
tc := testCase{
|
|
description: "increase by 1",
|
|
initial: 3,
|
|
expected: 4,
|
|
delta: 1,
|
|
}
|
|
test(t, &tc, createMachineSetTestConfig(RandomString(6), RandomString(6), RandomString(6), int(tc.initial), annotations, nil, nil))
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
tc := testCase{
|
|
description: "increase by 1",
|
|
initial: 3,
|
|
expected: 4,
|
|
delta: 1,
|
|
}
|
|
test(t, &tc, createMachineDeploymentTestConfig(RandomString(6), RandomString(6), RandomString(6), int(tc.initial), annotations, nil, nil))
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupDecreaseTargetSize(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
delta int
|
|
initial int32
|
|
targetSizeIncrement int32
|
|
expected int32
|
|
expectedError bool
|
|
includeDeletingMachine bool
|
|
includeFailedMachine bool
|
|
includeFailedMachineWithProviderID bool
|
|
includePendingMachine bool
|
|
includePendingMachineWithProviderID bool
|
|
machinesDoNotHaveProviderIDs bool
|
|
}
|
|
|
|
test := func(t *testing.T, tc *testCase, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
// machines in deletion should not be counted towards the active nodes when calculating a decrease in size.
|
|
if tc.includeDeletingMachine {
|
|
if tc.initial < 1 {
|
|
t.Fatal("test cannot pass, deleted machine requires at least 1 machine in machineset")
|
|
}
|
|
|
|
// Simulate a machine in deleting
|
|
machine := testConfig.machines[0].DeepCopy()
|
|
timestamp := metav1.Now()
|
|
machine.SetDeletionTimestamp(×tamp)
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
|
|
// machines that have failed should not be counted towards the active nodes when calculating a decrease in size.
|
|
if tc.includeFailedMachine {
|
|
// because we want to allow for tests that have deleted machines and failed machines, we use the second machine in the test data.
|
|
if tc.initial < 2 {
|
|
t.Fatal("test cannot pass, failed machine requires at least 2 machine in machineset")
|
|
}
|
|
|
|
// Simulate a failed machine
|
|
machine := testConfig.machines[1].DeepCopy()
|
|
|
|
if !tc.includeFailedMachineWithProviderID {
|
|
unstructured.RemoveNestedField(machine.Object, "spec", "providerID")
|
|
}
|
|
unstructured.SetNestedField(machine.Object, "FailureMessage", "status", "failureMessage")
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
|
|
// machines that are in pending state should not be counted towards the active nodes when calculating a decrease in size.
|
|
if tc.includePendingMachine {
|
|
// because we want to allow for tests that have deleted, failed machines, and pending machine, we use the third machine in the test data.
|
|
if tc.initial < 3 {
|
|
t.Fatal("test cannot pass, pending machine requires at least 3 machine in machineset")
|
|
}
|
|
|
|
// Simulate a pending machine
|
|
machine := testConfig.machines[2].DeepCopy()
|
|
|
|
if !tc.includePendingMachineWithProviderID {
|
|
unstructured.RemoveNestedField(machine.Object, "spec", "providerID")
|
|
}
|
|
unstructured.RemoveNestedField(machine.Object, "status", "nodeRef")
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
|
|
// machines with no provider id can be created on some providers, notably bare metal
|
|
if tc.machinesDoNotHaveProviderIDs {
|
|
for _, machine := range testConfig.machines {
|
|
updated := machine.DeepCopy()
|
|
unstructured.RemoveNestedField(updated.Object, "spec", "providerID")
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, updated); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0].(*nodegroup)
|
|
|
|
gvr, err := ng.scalableResource.GroupVersionResource()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// DecreaseTargetSize should only decrease the size when the current target size of the nodeGroup
|
|
// is bigger than the number existing instances for that group. We force such a scenario with targetSizeIncrement.
|
|
scalableResource, err := controller.managementScaleClient.Scales(testConfig.spec.namespace).
|
|
Get(context.TODO(), gvr.GroupResource(), ng.scalableResource.Name(), metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
ch := make(chan error)
|
|
checkDone := func(obj interface{}) (bool, error) {
|
|
u, ok := obj.(*unstructured.Unstructured)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
if u.GetResourceVersion() != scalableResource.GetResourceVersion() {
|
|
return false, nil
|
|
}
|
|
ng, err := newNodeGroupFromScalableResource(controller, u)
|
|
if err != nil {
|
|
return true, fmt.Errorf("unexpected error: %v", err)
|
|
}
|
|
if ng == nil {
|
|
return false, nil
|
|
}
|
|
currReplicas, err := ng.TargetSize()
|
|
if err != nil {
|
|
return true, fmt.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if currReplicas != int(tc.initial)+int(tc.targetSizeIncrement) {
|
|
return true, fmt.Errorf("expected %v, got %v", tc.initial+tc.targetSizeIncrement, currReplicas)
|
|
}
|
|
|
|
if err := ng.DecreaseTargetSize(tc.delta); (err != nil) != tc.expectedError {
|
|
return true, fmt.Errorf("expected error: %v, got: %v", tc.expectedError, err)
|
|
}
|
|
|
|
scalableResource, err := controller.managementScaleClient.Scales(testConfig.spec.namespace).
|
|
Get(context.TODO(), gvr.GroupResource(), ng.scalableResource.Name(), metav1.GetOptions{})
|
|
if err != nil {
|
|
return true, fmt.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
if scalableResource.Spec.Replicas != tc.expected {
|
|
return true, fmt.Errorf("expected %v, got %v", tc.expected, scalableResource.Spec.Replicas)
|
|
}
|
|
return true, nil
|
|
}
|
|
handler := cache.ResourceEventHandlerFuncs{
|
|
AddFunc: func(obj interface{}) {
|
|
match, err := checkDone(obj)
|
|
if match {
|
|
ch <- err
|
|
}
|
|
},
|
|
UpdateFunc: func(oldObj, newObj interface{}) {
|
|
match, err := checkDone(newObj)
|
|
if match {
|
|
ch <- err
|
|
}
|
|
},
|
|
}
|
|
if _, err := controller.machineSetInformer.Informer().AddEventHandler(handler); err != nil {
|
|
t.Fatalf("unexpected error adding event handler for machineSetInformer: %v", err)
|
|
}
|
|
if _, err := controller.machineDeploymentInformer.Informer().AddEventHandler(handler); err != nil {
|
|
t.Fatalf("unexpected error adding event handler for machineDeploymentInformer: %v", err)
|
|
}
|
|
|
|
scalableResource.Spec.Replicas += tc.targetSizeIncrement
|
|
|
|
_, err = ng.machineController.managementScaleClient.Scales(ng.scalableResource.Namespace()).
|
|
Update(context.TODO(), gvr.GroupResource(), scalableResource, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
lastErr := fmt.Errorf("no updates received yet")
|
|
for lastErr != nil {
|
|
select {
|
|
case err = <-ch:
|
|
lastErr = err
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal(fmt.Errorf("timeout while waiting for update. Last error was: %v", lastErr))
|
|
}
|
|
}
|
|
}
|
|
|
|
testCases := []testCase{
|
|
{
|
|
description: "Same number of existing instances and node group target size should error",
|
|
initial: 3,
|
|
targetSizeIncrement: 0,
|
|
expected: 3,
|
|
delta: -1,
|
|
expectedError: true,
|
|
},
|
|
{
|
|
description: "A node group with target size 4 but only 3 existing instances should decrease by 1",
|
|
initial: 3,
|
|
targetSizeIncrement: 1,
|
|
expected: 3,
|
|
delta: -1,
|
|
},
|
|
{
|
|
description: "A node group with 4 replicas with one machine in deleting state should decrease by 1",
|
|
initial: 4,
|
|
targetSizeIncrement: 0,
|
|
expected: 3,
|
|
delta: -1,
|
|
includeDeletingMachine: true,
|
|
},
|
|
{
|
|
description: "A node group with 4 replicas with one failed machine should decrease by 1",
|
|
initial: 4,
|
|
targetSizeIncrement: 0,
|
|
expected: 3,
|
|
delta: -1,
|
|
includeFailedMachine: true,
|
|
},
|
|
{
|
|
description: "A node group with 4 replicas with one pending machine should decrease by 1",
|
|
initial: 4,
|
|
targetSizeIncrement: 0,
|
|
expected: 3,
|
|
delta: -1,
|
|
includePendingMachine: true,
|
|
},
|
|
{
|
|
description: "A node group with 5 replicas with one pending and one failed machine should decrease by 2",
|
|
initial: 5,
|
|
targetSizeIncrement: 0,
|
|
expected: 3,
|
|
delta: -2,
|
|
includeFailedMachine: true,
|
|
includePendingMachine: true,
|
|
},
|
|
{
|
|
description: "A node group with 5 replicas with one pending, one failed, and one deleting machine should decrease by 3",
|
|
initial: 5,
|
|
targetSizeIncrement: 0,
|
|
expected: 2,
|
|
delta: -3,
|
|
includeFailedMachine: true,
|
|
includePendingMachine: true,
|
|
includeDeletingMachine: true,
|
|
},
|
|
{
|
|
description: "A node group with 4 replicas with one failed machine that has a provider ID should decrease by 1",
|
|
initial: 4,
|
|
targetSizeIncrement: 0,
|
|
expected: 3,
|
|
delta: -1,
|
|
includeFailedMachine: true,
|
|
includeFailedMachineWithProviderID: true,
|
|
},
|
|
{
|
|
description: "A node group with 4 replicas with one pending machine that has a provider ID should decrease by 1",
|
|
initial: 4,
|
|
targetSizeIncrement: 0,
|
|
expected: 3,
|
|
delta: -1,
|
|
includePendingMachine: true,
|
|
includePendingMachineWithProviderID: true,
|
|
},
|
|
{
|
|
description: "A node group with target size 4 but only 3 existing instances without provider IDs should not scale out",
|
|
initial: 3,
|
|
targetSizeIncrement: 1,
|
|
expected: 3,
|
|
delta: -1,
|
|
machinesDoNotHaveProviderIDs: true,
|
|
},
|
|
}
|
|
|
|
annotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
test(t, &tc, createMachineSetTestConfig(RandomString(6), RandomString(6), RandomString(6), int(tc.initial), annotations, nil, nil))
|
|
})
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
test(t, &tc, createMachineDeploymentTestConfig(RandomString(6), RandomString(6), RandomString(6), int(tc.initial), annotations, nil, nil))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNodeGroupDecreaseSizeErrors(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
delta int
|
|
initial int32
|
|
errorMsg string
|
|
}
|
|
|
|
testCases := []testCase{{
|
|
description: "errors because delta is positive",
|
|
delta: 1,
|
|
initial: 3,
|
|
errorMsg: "size decrease must be negative",
|
|
}, {
|
|
description: "errors because initial+delta < len(nodes)",
|
|
delta: -1,
|
|
initial: 3,
|
|
errorMsg: "attempt to delete existing nodes currentReplicas:3 delta:-1 existingNodes: 3",
|
|
}}
|
|
|
|
test := func(t *testing.T, tc *testCase, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0].(*nodegroup)
|
|
currReplicas, err := ng.TargetSize()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if currReplicas != int(tc.initial) {
|
|
t.Errorf("expected %v, got %v", tc.initial, currReplicas)
|
|
}
|
|
|
|
errors := len(tc.errorMsg) > 0
|
|
|
|
err = ng.DecreaseTargetSize(tc.delta)
|
|
if errors && err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
|
|
if !errors && err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), tc.errorMsg) {
|
|
t.Errorf("expected error message to contain %q, got %q", tc.errorMsg, err.Error())
|
|
}
|
|
|
|
gvr, err := ng.scalableResource.GroupVersionResource()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
scalableResource, err := ng.machineController.managementScaleClient.Scales(testConfig.spec.namespace).
|
|
Get(context.TODO(), gvr.GroupResource(), ng.scalableResource.Name(), metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if scalableResource.Spec.Replicas != tc.initial {
|
|
t.Errorf("expected %v, got %v", tc.initial, scalableResource.Spec.Replicas)
|
|
}
|
|
}
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
annotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
test(t, &tc, createMachineSetTestConfig(RandomString(6), RandomString(6), RandomString(6), int(tc.initial), annotations, nil, nil))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
annotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
test(t, &tc, createMachineDeploymentTestConfig(RandomString(6), RandomString(6), RandomString(6), int(tc.initial), annotations, nil, nil))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupDeleteNodes(t *testing.T) {
|
|
test := func(t *testing.T, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0].(*nodegroup)
|
|
nodeNames, err := ng.Nodes()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(nodeNames) != len(testConfig.nodes) {
|
|
t.Fatalf("expected len=%v, got len=%v", len(testConfig.nodes), len(nodeNames))
|
|
}
|
|
|
|
sort.SliceStable(nodeNames, func(i, j int) bool {
|
|
return nodeNames[i].Id < nodeNames[j].Id
|
|
})
|
|
|
|
for i := 0; i < len(nodeNames); i++ {
|
|
if nodeNames[i].Id != testConfig.nodes[i].Spec.ProviderID {
|
|
t.Fatalf("expected %q, got %q", testConfig.nodes[i].Spec.ProviderID, nodeNames[i].Id)
|
|
}
|
|
}
|
|
|
|
if err := ng.DeleteNodes(testConfig.nodes[5:]); err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
|
|
for i := 5; i < len(testConfig.machines); i++ {
|
|
machine, err := controller.managementClient.Resource(controller.machineResource).
|
|
Namespace(testConfig.spec.namespace).
|
|
Get(context.TODO(), testConfig.machines[i].GetName(), metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if _, found := machine.GetAnnotations()[machineDeleteAnnotationKey]; !found {
|
|
t.Errorf("expected annotation %q on machine %s", machineDeleteAnnotationKey, machine.GetName())
|
|
}
|
|
}
|
|
|
|
gvr, err := ng.scalableResource.GroupVersionResource()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
scalableResource, err := ng.machineController.managementScaleClient.Scales(testConfig.spec.namespace).
|
|
Get(context.TODO(), gvr.GroupResource(), ng.scalableResource.Name(), metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if scalableResource.Spec.Replicas != 5 {
|
|
t.Errorf("expected 5, got %v", scalableResource.Spec.Replicas)
|
|
}
|
|
}
|
|
|
|
// Note: 10 is an upper bound for the number of nodes/replicas
|
|
// Going beyond 10 will break the sorting that happens in the
|
|
// test() function because sort.Strings() will not do natural
|
|
// sorting and the expected semantics in test() will fail.
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineSetTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineDeploymentTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupMachineSetDeleteNodesWithMismatchedNodes(t *testing.T) {
|
|
test := func(t *testing.T, expected int, testConfigs []*testConfig) {
|
|
testConfig0, testConfig1 := testConfigs[0], testConfigs[1]
|
|
controller, stop := mustCreateTestController(t, testConfigs...)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if l := len(nodegroups); l != expected {
|
|
t.Fatalf("expected %d, got %d", expected, l)
|
|
}
|
|
|
|
ng0, err := controller.nodeGroupForNode(testConfig0.nodes[0])
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
ng1, err := controller.nodeGroupForNode(testConfig1.nodes[0])
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Deleting nodes that are not in ng0 should fail.
|
|
err0 := ng0.DeleteNodes(testConfig1.nodes)
|
|
if err0 == nil {
|
|
t.Error("expected an error")
|
|
}
|
|
|
|
expectedErrSubstring := "doesn't belong to node group"
|
|
|
|
if !strings.Contains(err0.Error(), expectedErrSubstring) {
|
|
t.Errorf("expected error: %q to contain: %q", err0.Error(), expectedErrSubstring)
|
|
}
|
|
|
|
// Deleting nodes that are not in ng1 should fail.
|
|
err1 := ng1.DeleteNodes(testConfig0.nodes)
|
|
if err1 == nil {
|
|
t.Error("expected an error")
|
|
}
|
|
|
|
if !strings.Contains(err1.Error(), expectedErrSubstring) {
|
|
t.Errorf("expected error: %q to contain: %q", err0.Error(), expectedErrSubstring)
|
|
}
|
|
|
|
// Deleting from correct node group should fail because
|
|
// replicas would become <= 0.
|
|
if err := ng0.DeleteNodes(testConfig0.nodes); err == nil {
|
|
t.Error("expected error")
|
|
}
|
|
|
|
// Deleting from correct node group should fail because
|
|
// replicas would become <= 0.
|
|
if err := ng1.DeleteNodes(testConfig1.nodes); err == nil {
|
|
t.Error("expected error")
|
|
}
|
|
}
|
|
|
|
annotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "3",
|
|
}
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
namespace := RandomString(6)
|
|
clusterName := RandomString(6)
|
|
testConfig0 := createMachineSetTestConfigs(namespace, clusterName, RandomString(6), 1, 2, annotations, nil, nil)
|
|
testConfig1 := createMachineSetTestConfigs(namespace, clusterName, RandomString(6), 1, 2, annotations, nil, nil)
|
|
test(t, 2, append(testConfig0, testConfig1...))
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
namespace := RandomString(6)
|
|
clusterName := RandomString(6)
|
|
testConfig0 := createMachineDeploymentTestConfigs(namespace, clusterName, RandomString(6), 1, 2, annotations, nil, nil)
|
|
testConfig1 := createMachineDeploymentTestConfigs(namespace, clusterName, RandomString(6), 1, 2, annotations, nil, nil)
|
|
test(t, 2, append(testConfig0, testConfig1...))
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupDeleteNodesTwice(t *testing.T) {
|
|
addDeletionTimestampToMachine := func(controller *machineController, node *corev1.Node) error {
|
|
m, err := controller.findMachineByProviderID(normalizedProviderString(node.Spec.ProviderID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Simulate delete that would have happened if the
|
|
// Machine API controllers were running Don't actually
|
|
// delete since the fake client does not support
|
|
// finalizers.
|
|
now := metav1.Now()
|
|
|
|
m.SetDeletionTimestamp(&now)
|
|
|
|
if _, err := controller.managementClient.Resource(controller.machineResource).
|
|
Namespace(m.GetNamespace()).Update(context.TODO(), m, metav1.UpdateOptions{}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// This is the size we expect the NodeGroup to be after we have called DeleteNodes.
|
|
// We need at least 8 nodes for this test to be valid.
|
|
expectedSize := 7
|
|
|
|
test := func(t *testing.T, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0].(*nodegroup)
|
|
nodeNames, err := ng.Nodes()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Check that the test case is valid before executing DeleteNodes
|
|
// 1. We must have at least 1 more node than the expected size otherwise DeleteNodes is a no-op
|
|
// 2. MinSize must be less than the expected size otherwise a second call to DeleteNodes may
|
|
// not make the nodegroup size less than the expected size.
|
|
if len(nodeNames) <= expectedSize {
|
|
t.Fatalf("expected more nodes than the expected size: %d <= %d", len(nodeNames), expectedSize)
|
|
}
|
|
if ng.MinSize() >= expectedSize {
|
|
t.Fatalf("expected min size to be less than expected size: %d >= %d", ng.MinSize(), expectedSize)
|
|
}
|
|
|
|
if len(nodeNames) != len(testConfig.nodes) {
|
|
t.Fatalf("expected len=%v, got len=%v", len(testConfig.nodes), len(nodeNames))
|
|
}
|
|
|
|
sort.SliceStable(nodeNames, func(i, j int) bool {
|
|
return nodeNames[i].Id < nodeNames[j].Id
|
|
})
|
|
|
|
for i := 0; i < len(nodeNames); i++ {
|
|
if nodeNames[i].Id != testConfig.nodes[i].Spec.ProviderID {
|
|
t.Fatalf("expected %q, got %q", testConfig.nodes[i].Spec.ProviderID, nodeNames[i].Id)
|
|
}
|
|
}
|
|
|
|
// These are the nodes which are over the final expectedSize
|
|
nodesToBeDeleted := testConfig.nodes[expectedSize:]
|
|
|
|
// Assert that we have no DeletionTimestamp
|
|
for i := expectedSize; i < len(testConfig.machines); i++ {
|
|
if !testConfig.machines[i].GetDeletionTimestamp().IsZero() {
|
|
t.Fatalf("unexpected DeletionTimestamp")
|
|
}
|
|
}
|
|
|
|
// Delete all nodes over the expectedSize
|
|
if err := ng.DeleteNodes(nodesToBeDeleted); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
for _, node := range nodesToBeDeleted {
|
|
if err := addDeletionTimestampToMachine(controller, node); err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
}
|
|
|
|
// Wait for the machineset to have been updated
|
|
if err := wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) {
|
|
nodegroups, err = controller.nodeGroups()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
targetSize, err := nodegroups[0].TargetSize()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return targetSize == expectedSize, nil
|
|
}); err != nil {
|
|
t.Fatalf("unexpected error waiting for nodegroup to be expected size: %v", err)
|
|
}
|
|
|
|
nodegroups, err = controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
ng = nodegroups[0].(*nodegroup)
|
|
|
|
// Check the nodegroup is at the expected size
|
|
actualSize, err := ng.TargetSize()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if actualSize != expectedSize {
|
|
t.Fatalf("expected %d nodes, got %d", expectedSize, actualSize)
|
|
}
|
|
|
|
// Check that the machines deleted in the last run have DeletionTimestamp's
|
|
// when fetched from the API
|
|
for _, node := range nodesToBeDeleted {
|
|
// Ensure the update has propogated
|
|
if err := wait.PollImmediate(100*time.Millisecond, 5*time.Minute, func() (bool, error) {
|
|
m, err := controller.findMachineByProviderID(normalizedProviderString(node.Spec.ProviderID))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return !m.GetDeletionTimestamp().IsZero(), nil
|
|
}); err != nil {
|
|
t.Fatalf("unexpected error waiting for machine to have deletion timestamp: %v", err)
|
|
}
|
|
}
|
|
|
|
// Attempt to delete the nodes again which verifies
|
|
// that nodegroup.DeleteNodes() skips over nodes that
|
|
// have a non-nil DeletionTimestamp value.
|
|
if err := ng.DeleteNodes(nodesToBeDeleted); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
gvr, err := ng.scalableResource.GroupVersionResource()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
scalableResource, err := ng.machineController.managementScaleClient.Scales(testConfig.spec.namespace).
|
|
Get(context.TODO(), gvr.GroupResource(), ng.scalableResource.Name(), metav1.GetOptions{})
|
|
|
|
if scalableResource.Spec.Replicas != int32(expectedSize) {
|
|
t.Errorf("expected %v, got %v", expectedSize, scalableResource.Spec.Replicas)
|
|
}
|
|
}
|
|
|
|
// Note: 10 is an upper bound for the number of nodes/replicas
|
|
// Going beyond 10 will break the sorting that happens in the
|
|
// test() function because sort.Strings() will not do natural
|
|
// sorting and the expected semantics in test() will fail.
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineSetTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineDeploymentTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupDeleteNodesSequential(t *testing.T) {
|
|
// This is the size we expect the NodeGroup to be after we have called DeleteNodes.
|
|
// We need at least 8 nodes for this test to be valid.
|
|
expectedSize := 7
|
|
|
|
test := func(t *testing.T, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0].(*nodegroup)
|
|
nodeNames, err := ng.Nodes()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Check that the test case is valid before executing DeleteNodes
|
|
// 1. We must have at least 1 more node than the expected size otherwise DeleteNodes is a no-op
|
|
// 2. MinSize must be less than the expected size otherwise a second call to DeleteNodes may
|
|
// not make the nodegroup size less than the expected size.
|
|
if len(nodeNames) <= expectedSize {
|
|
t.Fatalf("expected more nodes than the expected size: %d <= %d", len(nodeNames), expectedSize)
|
|
}
|
|
if ng.MinSize() >= expectedSize {
|
|
t.Fatalf("expected min size to be less than expected size: %d >= %d", ng.MinSize(), expectedSize)
|
|
}
|
|
|
|
if len(nodeNames) != len(testConfig.nodes) {
|
|
t.Fatalf("expected len=%v, got len=%v", len(testConfig.nodes), len(nodeNames))
|
|
}
|
|
|
|
sort.SliceStable(nodeNames, func(i, j int) bool {
|
|
return nodeNames[i].Id < nodeNames[j].Id
|
|
})
|
|
|
|
for i := 0; i < len(nodeNames); i++ {
|
|
if nodeNames[i].Id != testConfig.nodes[i].Spec.ProviderID {
|
|
t.Fatalf("expected %q, got %q", testConfig.nodes[i].Spec.ProviderID, nodeNames[i].Id)
|
|
}
|
|
}
|
|
|
|
// These are the nodes which are over the final expectedSize
|
|
nodesToBeDeleted := testConfig.nodes[expectedSize:]
|
|
|
|
// Assert that we have no DeletionTimestamp
|
|
for i := expectedSize; i < len(testConfig.machines); i++ {
|
|
if !testConfig.machines[i].GetDeletionTimestamp().IsZero() {
|
|
t.Fatalf("unexpected DeletionTimestamp")
|
|
}
|
|
}
|
|
|
|
// When the core autoscaler scales down nodes, it fetches the node group for each node in separate
|
|
// go routines and then scales them down individually, we use a lock to ensure the scale down is
|
|
// performed sequentially but this means that the cached scalable resource may not be up to date.
|
|
// We need to replicate this out of date nature by constructing the node groups then deleting.
|
|
nodeToNodeGroup := make(map[*corev1.Node]*nodegroup)
|
|
|
|
for _, node := range nodesToBeDeleted {
|
|
nodeGroup, err := controller.nodeGroupForNode(node)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
nodeToNodeGroup[node] = nodeGroup
|
|
}
|
|
|
|
for node, nodeGroup := range nodeToNodeGroup {
|
|
if err := nodeGroup.DeleteNodes([]*corev1.Node{node}); err != nil {
|
|
t.Fatalf("unexpected error deleting node: %v", err)
|
|
}
|
|
}
|
|
|
|
nodegroups, err = controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
ng = nodegroups[0].(*nodegroup)
|
|
|
|
// Check the nodegroup is at the expected size
|
|
actualSize, err := ng.scalableResource.Replicas()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if actualSize != expectedSize {
|
|
t.Fatalf("expected %d nodes, got %d", expectedSize, actualSize)
|
|
}
|
|
|
|
gvr, err := ng.scalableResource.GroupVersionResource()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
scalableResource, err := ng.machineController.managementScaleClient.Scales(testConfig.spec.namespace).
|
|
Get(context.TODO(), gvr.GroupResource(), ng.scalableResource.Name(), metav1.GetOptions{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if scalableResource.Spec.Replicas != int32(expectedSize) {
|
|
t.Errorf("expected %v, got %v", expectedSize, scalableResource.Spec.Replicas)
|
|
}
|
|
}
|
|
|
|
// Note: 10 is an upper bound for the number of nodes/replicas
|
|
// Going beyond 10 will break the sorting that happens in the
|
|
// test() function because sort.Strings() will not do natural
|
|
// sorting and the expected semantics in test() will fail.
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineSetTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineDeploymentTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupWithFailedMachine(t *testing.T) {
|
|
test := func(t *testing.T, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
// Simulate a failed machine
|
|
machine := testConfig.machines[3].DeepCopy()
|
|
|
|
unstructured.RemoveNestedField(machine.Object, "spec", "providerID")
|
|
if err := unstructured.SetNestedField(machine.Object, "FailureMessage", "status", "failureMessage"); err != nil {
|
|
t.Fatalf("unexpected error setting nested field: %v", err)
|
|
}
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0]
|
|
nodeNames, err := ng.Nodes()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(nodeNames) != len(testConfig.nodes) {
|
|
t.Fatalf("expected len=%v, got len=%v", len(testConfig.nodes), len(nodeNames))
|
|
}
|
|
|
|
sort.SliceStable(nodeNames, func(i, j int) bool {
|
|
return nodeNames[i].Id < nodeNames[j].Id
|
|
})
|
|
|
|
// The failed machine key is sorted to the first index
|
|
failedMachineID := fmt.Sprintf("%s%s_%s", failedMachinePrefix, machine.GetNamespace(), machine.GetName())
|
|
if nodeNames[0].Id != failedMachineID {
|
|
t.Fatalf("expected %q, got %q", failedMachineID, nodeNames[0].Id)
|
|
}
|
|
|
|
for i := 1; i < len(nodeNames); i++ {
|
|
// Fix the indexing due the failed machine being removed from the list
|
|
var nodeIndex int
|
|
if i < 4 {
|
|
// for nodes 0, 1, 2
|
|
nodeIndex = i - 1
|
|
} else {
|
|
// for nodes 4 onwards
|
|
nodeIndex = i
|
|
}
|
|
|
|
if nodeNames[i].Id != testConfig.nodes[nodeIndex].Spec.ProviderID {
|
|
t.Fatalf("expected %q, got %q", testConfig.nodes[nodeIndex].Spec.ProviderID, nodeNames[i].Id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: 10 is an upper bound for the number of nodes/replicas
|
|
// Going beyond 10 will break the sorting that happens in the
|
|
// test() function because sort.Strings() will not do natural
|
|
// sorting and the expected semantics in test() will fail.
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineSetTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineDeploymentTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
},
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestNodeGroupTemplateNodeInfo(t *testing.T) {
|
|
enableScaleAnnotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
|
|
type testResourceSlice struct {
|
|
driverName string
|
|
gpuCount int
|
|
deviceType string
|
|
}
|
|
|
|
type testCaseConfig struct {
|
|
nodeLabels map[string]string
|
|
includeNodes bool
|
|
expectedErr error
|
|
expectedCapacity map[corev1.ResourceName]int64
|
|
expectedNodeLabels map[string]string
|
|
expectedResourceSlice testResourceSlice
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
nodeGroupAnnotations map[string]string
|
|
config testCaseConfig
|
|
}{
|
|
{
|
|
name: "When the NodeGroup cannot scale from zero",
|
|
config: testCaseConfig{
|
|
expectedErr: cloudprovider.ErrNotImplemented,
|
|
},
|
|
},
|
|
{
|
|
name: "When the NodeGroup can scale from zero",
|
|
nodeGroupAnnotations: map[string]string{
|
|
memoryKey: "2048Mi",
|
|
cpuKey: "2",
|
|
gpuTypeKey: gpuapis.ResourceNvidiaGPU,
|
|
gpuCountKey: "1",
|
|
},
|
|
config: testCaseConfig{
|
|
expectedErr: nil,
|
|
nodeLabels: map[string]string{
|
|
"kubernetes.io/os": "linux",
|
|
"kubernetes.io/arch": "amd64",
|
|
},
|
|
expectedCapacity: map[corev1.ResourceName]int64{
|
|
corev1.ResourceCPU: 2,
|
|
corev1.ResourceMemory: 2048 * 1024 * 1024,
|
|
corev1.ResourcePods: 110,
|
|
gpuapis.ResourceNvidiaGPU: 1,
|
|
},
|
|
expectedNodeLabels: map[string]string{
|
|
"kubernetes.io/os": "linux",
|
|
"kubernetes.io/arch": "amd64",
|
|
"kubernetes.io/hostname": "random value",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "When the NodeGroup can scale from zero, the label capacity annotations merge with the pre-built node labels and take precedence if the same key is defined in both",
|
|
nodeGroupAnnotations: map[string]string{
|
|
memoryKey: "2048Mi",
|
|
cpuKey: "2",
|
|
gpuTypeKey: gpuapis.ResourceNvidiaGPU,
|
|
gpuCountKey: "1",
|
|
labelsKey: "kubernetes.io/arch=arm64,my-custom-label=custom-value",
|
|
},
|
|
config: testCaseConfig{
|
|
expectedErr: nil,
|
|
expectedCapacity: map[corev1.ResourceName]int64{
|
|
corev1.ResourceCPU: 2,
|
|
corev1.ResourceMemory: 2048 * 1024 * 1024,
|
|
corev1.ResourcePods: 110,
|
|
gpuapis.ResourceNvidiaGPU: 1,
|
|
},
|
|
expectedNodeLabels: map[string]string{
|
|
"kubernetes.io/os": "linux",
|
|
"kubernetes.io/arch": "arm64",
|
|
"kubernetes.io/hostname": "random value",
|
|
"my-custom-label": "custom-value",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "When the NodeGroup can scale from zero and the Node still exists, it includes the known node labels",
|
|
nodeGroupAnnotations: map[string]string{
|
|
memoryKey: "2048Mi",
|
|
cpuKey: "2",
|
|
},
|
|
config: testCaseConfig{
|
|
includeNodes: true,
|
|
expectedErr: nil,
|
|
nodeLabels: map[string]string{
|
|
"kubernetes.io/os": "windows",
|
|
"kubernetes.io/arch": "arm64",
|
|
"node.kubernetes.io/instance-type": "instance1",
|
|
},
|
|
expectedCapacity: map[corev1.ResourceName]int64{
|
|
corev1.ResourceCPU: 2,
|
|
corev1.ResourceMemory: 2048 * 1024 * 1024,
|
|
corev1.ResourcePods: 110,
|
|
},
|
|
expectedNodeLabels: map[string]string{
|
|
"kubernetes.io/hostname": "random value",
|
|
"kubernetes.io/os": "windows",
|
|
"kubernetes.io/arch": "arm64",
|
|
"node.kubernetes.io/instance-type": "instance1",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "When the NodeGroup can scale from zero and DRA is enabled, it creates ResourceSlice derived from the annotation of DRA driver name and GPU count",
|
|
nodeGroupAnnotations: map[string]string{
|
|
memoryKey: "2048Mi",
|
|
cpuKey: "2",
|
|
draDriverKey: "gpu.nvidia.com",
|
|
gpuCountKey: "2",
|
|
},
|
|
config: testCaseConfig{
|
|
expectedErr: nil,
|
|
expectedCapacity: map[corev1.ResourceName]int64{
|
|
corev1.ResourceCPU: 2,
|
|
corev1.ResourceMemory: 2048 * 1024 * 1024,
|
|
corev1.ResourcePods: 110,
|
|
},
|
|
expectedResourceSlice: testResourceSlice{
|
|
driverName: "gpu.nvidia.com",
|
|
gpuCount: 2,
|
|
deviceType: GpuDeviceType,
|
|
},
|
|
expectedNodeLabels: map[string]string{
|
|
"kubernetes.io/os": "linux",
|
|
"kubernetes.io/arch": "amd64",
|
|
"kubernetes.io/hostname": "random value",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
test := func(t *testing.T, testConfig *testConfig, config testCaseConfig) {
|
|
if config.includeNodes {
|
|
for i := range testConfig.nodes {
|
|
testConfig.nodes[i].SetLabels(config.nodeLabels)
|
|
}
|
|
} else {
|
|
testConfig.nodes = []*corev1.Node{}
|
|
}
|
|
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0]
|
|
nodeInfo, err := ng.TemplateNodeInfo()
|
|
if config.expectedErr != nil {
|
|
if err != config.expectedErr {
|
|
t.Fatalf("expected error: %v, but got: %v", config.expectedErr, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
nodeAllocatable := nodeInfo.Node().Status.Allocatable
|
|
nodeCapacity := nodeInfo.Node().Status.Capacity
|
|
for resource, expectedCapacity := range config.expectedCapacity {
|
|
if gotAllocatable, ok := nodeAllocatable[resource]; !ok {
|
|
t.Errorf("Expected allocatable to have resource %q, resource not found", resource)
|
|
} else if gotAllocatable.Value() != expectedCapacity {
|
|
t.Errorf("Expected allocatable %q: %+v, Got: %+v", resource, expectedCapacity, gotAllocatable.Value())
|
|
}
|
|
|
|
if gotCapactiy, ok := nodeCapacity[resource]; !ok {
|
|
t.Errorf("Expected capacity to have resource %q, resource not found", resource)
|
|
} else if gotCapactiy.Value() != expectedCapacity {
|
|
t.Errorf("Expected capacity %q: %+v, Got: %+v", resource, expectedCapacity, gotCapactiy.Value())
|
|
}
|
|
}
|
|
|
|
if len(nodeInfo.Node().GetLabels()) != len(config.expectedNodeLabels) {
|
|
t.Errorf("Expected node labels to have len: %d, but got: %d, labels are: %v", len(config.expectedNodeLabels), len(nodeInfo.Node().GetLabels()), nodeInfo.Node().GetLabels())
|
|
}
|
|
for key, value := range nodeInfo.Node().GetLabels() {
|
|
// Exclude the hostname label as it is randomized
|
|
if key != corev1.LabelHostname {
|
|
if expected, ok := config.expectedNodeLabels[key]; ok {
|
|
if value != expected {
|
|
t.Errorf("Expected node label %q: %q, Got: %q", key, config.expectedNodeLabels[key], value)
|
|
}
|
|
} else {
|
|
t.Errorf("Expected node label %q to exist in node", key)
|
|
}
|
|
}
|
|
}
|
|
for _, resourceslice := range nodeInfo.LocalResourceSlices {
|
|
if resourceslice.Spec.Driver != config.expectedResourceSlice.driverName {
|
|
t.Errorf("Expected DRA driver in ResourceSlice to have: %s, but got: %s", config.expectedResourceSlice.driverName, resourceslice.Spec.Driver)
|
|
} else if len(resourceslice.Spec.Devices) != config.expectedResourceSlice.gpuCount {
|
|
t.Errorf("Expected the number of DRA devices in ResourceSlice to have: %d, but got: %d", config.expectedResourceSlice.gpuCount, len(resourceslice.Spec.Devices))
|
|
}
|
|
for _, device := range resourceslice.Spec.Devices {
|
|
if *device.Attributes["type"].StringValue != config.expectedResourceSlice.deviceType {
|
|
t.Errorf("Expected device type to have: %s, but got: %s", config.expectedResourceSlice.deviceType, *device.Attributes["type"].StringValue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineSetTestConfig(
|
|
testNamespace,
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
cloudprovider.JoinStringMaps(enableScaleAnnotations, tc.nodeGroupAnnotations),
|
|
nil,
|
|
nil,
|
|
),
|
|
tc.config,
|
|
)
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineDeploymentTestConfig(
|
|
testNamespace,
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
cloudprovider.JoinStringMaps(enableScaleAnnotations, tc.nodeGroupAnnotations),
|
|
nil,
|
|
nil,
|
|
),
|
|
tc.config,
|
|
)
|
|
})
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestNodeGroupGetOptions(t *testing.T) {
|
|
enableScaleAnnotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
|
|
defaultOptions := config.NodeGroupAutoscalingOptions{
|
|
ScaleDownUtilizationThreshold: 0.1,
|
|
ScaleDownGpuUtilizationThreshold: 0.2,
|
|
ScaleDownUnneededTime: time.Second,
|
|
ScaleDownUnreadyTime: time.Minute,
|
|
MaxNodeProvisionTime: 15 * time.Minute,
|
|
}
|
|
|
|
cases := []struct {
|
|
desc string
|
|
opts map[string]string
|
|
expected *config.NodeGroupAutoscalingOptions
|
|
}{
|
|
{
|
|
desc: "return provided defaults on empty metadata",
|
|
opts: map[string]string{},
|
|
expected: &defaultOptions,
|
|
},
|
|
{
|
|
desc: "return specified options",
|
|
opts: map[string]string{
|
|
config.DefaultScaleDownGpuUtilizationThresholdKey: "0.6",
|
|
config.DefaultScaleDownUtilizationThresholdKey: "0.7",
|
|
config.DefaultScaleDownUnneededTimeKey: "1h",
|
|
config.DefaultScaleDownUnreadyTimeKey: "30m",
|
|
config.DefaultMaxNodeProvisionTimeKey: "60m",
|
|
},
|
|
expected: &config.NodeGroupAutoscalingOptions{
|
|
ScaleDownGpuUtilizationThreshold: 0.6,
|
|
ScaleDownUtilizationThreshold: 0.7,
|
|
ScaleDownUnneededTime: time.Hour,
|
|
ScaleDownUnreadyTime: 30 * time.Minute,
|
|
MaxNodeProvisionTime: 60 * time.Minute,
|
|
},
|
|
},
|
|
{
|
|
desc: "complete partial options specs with defaults",
|
|
opts: map[string]string{
|
|
config.DefaultScaleDownGpuUtilizationThresholdKey: "0.1",
|
|
config.DefaultScaleDownUnneededTimeKey: "1m",
|
|
},
|
|
expected: &config.NodeGroupAutoscalingOptions{
|
|
ScaleDownGpuUtilizationThreshold: 0.1,
|
|
ScaleDownUtilizationThreshold: defaultOptions.ScaleDownUtilizationThreshold,
|
|
ScaleDownUnneededTime: time.Minute,
|
|
ScaleDownUnreadyTime: defaultOptions.ScaleDownUnreadyTime,
|
|
MaxNodeProvisionTime: 15 * time.Minute,
|
|
},
|
|
},
|
|
{
|
|
desc: "keep defaults on unparsable options values",
|
|
opts: map[string]string{
|
|
config.DefaultScaleDownGpuUtilizationThresholdKey: "foo",
|
|
config.DefaultScaleDownUnneededTimeKey: "bar",
|
|
},
|
|
expected: &defaultOptions,
|
|
},
|
|
}
|
|
|
|
test := func(t *testing.T, testConfig *testConfig, expectedOptions *config.NodeGroupAutoscalingOptions) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0]
|
|
opts, err := ng.GetOptions(defaultOptions)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expectedOptions, opts)
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.desc, func(t *testing.T) {
|
|
annotations := map[string]string{}
|
|
for k, v := range c.opts {
|
|
annotations[nodeGroupAutoscalingOptionsKeyPrefix+k] = v
|
|
}
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineSetTestConfig(
|
|
testNamespace,
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
cloudprovider.JoinStringMaps(enableScaleAnnotations, annotations),
|
|
nil,
|
|
nil,
|
|
),
|
|
c.expected,
|
|
)
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
test(
|
|
t,
|
|
createMachineDeploymentTestConfig(
|
|
testNamespace,
|
|
RandomString(6),
|
|
RandomString(6),
|
|
10,
|
|
cloudprovider.JoinStringMaps(enableScaleAnnotations, annotations),
|
|
nil,
|
|
nil,
|
|
),
|
|
c.expected,
|
|
)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNodeGroupNodesInstancesStatus(t *testing.T) {
|
|
type testCase struct {
|
|
description string
|
|
nodeCount int
|
|
includePendingMachine bool
|
|
includeDeletingMachine bool
|
|
includeFailedMachineWithNodeRef bool
|
|
includeFailedMachineWithoutNodeRef bool
|
|
includeFailedMachineDeleting bool
|
|
}
|
|
|
|
testCases := []testCase{
|
|
{
|
|
description: "standard number of nodes",
|
|
nodeCount: 5,
|
|
},
|
|
{
|
|
description: "includes a machine in pending state",
|
|
nodeCount: 5,
|
|
includePendingMachine: true,
|
|
},
|
|
{
|
|
description: "includes a machine in deleting state",
|
|
nodeCount: 5,
|
|
includeDeletingMachine: true,
|
|
},
|
|
{
|
|
description: "includes a machine in failed state with nodeRef",
|
|
nodeCount: 5,
|
|
includeFailedMachineWithNodeRef: true,
|
|
},
|
|
{
|
|
description: "includes a machine in failed state without nodeRef",
|
|
nodeCount: 5,
|
|
includeFailedMachineWithoutNodeRef: true,
|
|
},
|
|
}
|
|
|
|
test := func(t *testing.T, tc *testCase, testConfig *testConfig) {
|
|
controller, stop := mustCreateTestController(t, testConfig)
|
|
defer stop()
|
|
|
|
if tc.includePendingMachine {
|
|
if tc.nodeCount < 1 {
|
|
t.Fatal("test cannot pass, deleted machine requires at least 1 machine in machineset")
|
|
}
|
|
|
|
machine := testConfig.machines[0].DeepCopy()
|
|
unstructured.RemoveNestedField(machine.Object, "spec", "providerID")
|
|
unstructured.RemoveNestedField(machine.Object, "status", "nodeRef")
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
|
|
if tc.includeDeletingMachine {
|
|
if tc.nodeCount < 2 {
|
|
t.Fatal("test cannot pass, deleted machine requires at least 2 machine in machineset")
|
|
}
|
|
|
|
machine := testConfig.machines[1].DeepCopy()
|
|
timestamp := metav1.Now()
|
|
machine.SetDeletionTimestamp(×tamp)
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
|
|
if tc.includeFailedMachineWithNodeRef {
|
|
if tc.nodeCount < 3 {
|
|
t.Fatal("test cannot pass, deleted machine requires at least 3 machine in machineset")
|
|
}
|
|
|
|
machine := testConfig.machines[2].DeepCopy()
|
|
unstructured.SetNestedField(machine.Object, "node-1", "status", "nodeRef", "name")
|
|
unstructured.SetNestedField(machine.Object, "ErrorMessage", "status", "errorMessage")
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
|
|
if tc.includeFailedMachineWithoutNodeRef {
|
|
if tc.nodeCount < 4 {
|
|
t.Fatal("test cannot pass, deleted machine requires at least 4 machine in machineset")
|
|
}
|
|
|
|
machine := testConfig.machines[3].DeepCopy()
|
|
unstructured.RemoveNestedField(machine.Object, "status", "nodeRef")
|
|
unstructured.SetNestedField(machine.Object, "ErrorMessage", "status", "errorMessage")
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
|
|
if tc.includeFailedMachineDeleting {
|
|
if tc.nodeCount < 5 {
|
|
t.Fatal("test cannot pass, deleted machine requires at least 5 machine in machineset")
|
|
}
|
|
|
|
machine := testConfig.machines[4].DeepCopy()
|
|
timestamp := metav1.Now()
|
|
machine.SetDeletionTimestamp(×tamp)
|
|
unstructured.SetNestedField(machine.Object, "ErrorMessage", "status", "errorMessage")
|
|
|
|
if err := updateResource(controller.managementClient, controller.machineInformer, controller.machineResource, machine); err != nil {
|
|
t.Fatalf("unexpected error updating machine, got %v", err)
|
|
}
|
|
}
|
|
|
|
nodegroups, err := controller.nodeGroups()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if l := len(nodegroups); l != 1 {
|
|
t.Fatalf("expected 1 nodegroup, got %d", l)
|
|
}
|
|
|
|
ng := nodegroups[0]
|
|
instances, err := ng.Nodes()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
expectedCount := tc.nodeCount
|
|
if len(instances) != expectedCount {
|
|
t.Errorf("expected %d nodes, got %d", expectedCount, len(instances))
|
|
}
|
|
|
|
// Sort instances by Id for stable comparison
|
|
sort.Slice(instances, func(i, j int) bool {
|
|
return instances[i].Id < instances[j].Id
|
|
})
|
|
|
|
for _, instance := range instances {
|
|
t.Logf("instance: %v", instance)
|
|
if tc.includePendingMachine && strings.HasPrefix(instance.Id, pendingMachinePrefix) {
|
|
if instance.Status == nil || instance.Status.State != cloudprovider.InstanceCreating {
|
|
t.Errorf("expected pending machine to have status %v, got %v", cloudprovider.InstanceCreating, instance.Status)
|
|
}
|
|
} else if tc.includeDeletingMachine && strings.HasPrefix(instance.Id, deletingMachinePrefix) {
|
|
if instance.Status == nil || instance.Status.State != cloudprovider.InstanceDeleting {
|
|
t.Errorf("expected deleting machine to have status %v, got %v", cloudprovider.InstanceDeleting, instance.Status)
|
|
}
|
|
} else if tc.includeFailedMachineWithNodeRef && strings.HasPrefix(instance.Id, failedMachinePrefix) {
|
|
if instance.Status != nil {
|
|
t.Errorf("expected failed machine with nodeRef to not have status, got %v", instance.Status)
|
|
}
|
|
} else if tc.includeFailedMachineWithoutNodeRef && strings.HasPrefix(instance.Id, failedMachinePrefix) {
|
|
if instance.Status == nil || instance.Status.State != cloudprovider.InstanceCreating {
|
|
t.Errorf("expected failed machine without nodeRef to have status %v, got %v", cloudprovider.InstanceCreating, instance.Status)
|
|
}
|
|
if instance.Status == nil || instance.Status.ErrorInfo.ErrorClass != cloudprovider.OtherErrorClass {
|
|
t.Errorf("expected failed machine without nodeRef to have error class %v, got %v", cloudprovider.OtherErrorClass, instance.Status.ErrorInfo.ErrorClass)
|
|
}
|
|
if instance.Status == nil || instance.Status.ErrorInfo.ErrorCode != "ProvisioningFailed" {
|
|
t.Errorf("expected failed machine without nodeRef to have error code %v, got %v", "ProvisioningFailed", instance.Status.ErrorInfo.ErrorCode)
|
|
}
|
|
} else if tc.includeFailedMachineDeleting && strings.HasPrefix(instance.Id, failedMachinePrefix) {
|
|
if instance.Status == nil || instance.Status.State != cloudprovider.InstanceDeleting {
|
|
t.Errorf("expected failed machine deleting to have status %v, got %v", cloudprovider.InstanceDeleting, instance.Status)
|
|
}
|
|
if instance.Status == nil || instance.Status.ErrorInfo.ErrorClass != cloudprovider.OtherErrorClass {
|
|
t.Errorf("expected failed machine deleting to have error class %v, got %v", cloudprovider.OtherErrorClass, instance.Status.ErrorInfo.ErrorClass)
|
|
}
|
|
if instance.Status == nil || instance.Status.ErrorInfo.ErrorCode != "DeletingFailed" {
|
|
t.Errorf("expected failed machine deleting to have error code %v, got %v", "DeletingFailed", instance.Status.ErrorInfo.ErrorCode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
annotations := map[string]string{
|
|
nodeGroupMinSizeAnnotationKey: "1",
|
|
nodeGroupMaxSizeAnnotationKey: "10",
|
|
}
|
|
|
|
t.Run("MachineSet", func(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
test(
|
|
t,
|
|
&tc,
|
|
createMachineSetTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
tc.nodeCount,
|
|
annotations,
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("MachineDeployment", func(t *testing.T) {
|
|
for _, tc := range testCases {
|
|
t.Run(tc.description, func(t *testing.T) {
|
|
test(
|
|
t,
|
|
&tc,
|
|
createMachineDeploymentTestConfig(
|
|
RandomString(6),
|
|
RandomString(6),
|
|
RandomString(6),
|
|
tc.nodeCount,
|
|
annotations,
|
|
nil,
|
|
nil,
|
|
),
|
|
)
|
|
})
|
|
}
|
|
})
|
|
}
|