autoscaler/cluster-autoscaler/expander/grpcplugin/grpc_client_test.go

277 lines
8.8 KiB
Go

/*
Copyright 2021 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 grpcplugin
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/autoscaler/cluster-autoscaler/expander/grpcplugin/protos"
"k8s.io/autoscaler/cluster-autoscaler/expander/mocks"
. "k8s.io/autoscaler/cluster-autoscaler/utils/test"
schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/test"
"k8s.io/autoscaler/cluster-autoscaler/expander"
_ "github.com/golang/mock/mockgen/model"
)
var (
nodes = []*v1.Node{
BuildTestNode("n1", 1000, 1000),
BuildTestNode("n2", 1000, 1000),
BuildTestNode("n3", 1000, 1000),
BuildTestNode("n4", 1000, 1000),
}
eoT2Micro = expander.Option{
Debug: "t2.micro",
NodeGroup: test.NewTestNodeGroup("my-asg.t2.micro", 10, 1, 1, true, false, "t2.micro", nil, nil),
}
eoT2Large = expander.Option{
Debug: "t2.large",
NodeGroup: test.NewTestNodeGroup("my-asg.t2.large", 10, 1, 1, true, false, "t2.large", nil, nil),
}
eoT3Large = expander.Option{
Debug: "t3.large",
NodeGroup: test.NewTestNodeGroup("my-asg.t3.large", 10, 1, 1, true, false, "t3.large", nil, nil),
}
eoM44XLarge = expander.Option{
Debug: "m4.4xlarge",
NodeGroup: test.NewTestNodeGroup("my-asg.m4.4xlarge", 10, 1, 1, true, false, "m4.4xlarge", nil, nil),
}
options = []expander.Option{eoT2Micro, eoT2Large, eoT3Large, eoM44XLarge}
grpcEoT2Micro = protos.Option{
NodeGroupId: eoT2Micro.NodeGroup.Id(),
NodeCount: int32(eoT2Micro.NodeCount),
Debug: eoT2Micro.Debug,
Pod: eoT2Micro.Pods,
}
grpcEoT2Large = protos.Option{
NodeGroupId: eoT2Large.NodeGroup.Id(),
NodeCount: int32(eoT2Large.NodeCount),
Debug: eoT2Large.Debug,
Pod: eoT2Large.Pods,
}
grpcEoT3Large = protos.Option{
NodeGroupId: eoT3Large.NodeGroup.Id(),
NodeCount: int32(eoT3Large.NodeCount),
Debug: eoT3Large.Debug,
Pod: eoT3Large.Pods,
}
grpcEoM44XLarge = protos.Option{
NodeGroupId: eoM44XLarge.NodeGroup.Id(),
NodeCount: int32(eoM44XLarge.NodeCount),
Debug: eoM44XLarge.Debug,
Pod: eoM44XLarge.Pods,
}
)
func TestPopulateOptionsForGrpc(t *testing.T) {
testCases := []struct {
desc string
opts []expander.Option
expectedOpts []*protos.Option
expectedMap map[string]expander.Option
}{
{
desc: "empty options",
opts: []expander.Option{},
expectedOpts: []*protos.Option{},
expectedMap: map[string]expander.Option{},
},
{
desc: "one option",
opts: []expander.Option{eoT2Micro},
expectedOpts: []*protos.Option{&grpcEoT2Micro},
expectedMap: map[string]expander.Option{eoT2Micro.NodeGroup.Id(): eoT2Micro},
},
{
desc: "many options",
opts: options,
expectedOpts: []*protos.Option{&grpcEoT2Micro, &grpcEoT2Large, &grpcEoT3Large, &grpcEoM44XLarge},
expectedMap: map[string]expander.Option{
eoT2Micro.NodeGroup.Id(): eoT2Micro,
eoT2Large.NodeGroup.Id(): eoT2Large,
eoT3Large.NodeGroup.Id(): eoT3Large,
eoM44XLarge.NodeGroup.Id(): eoM44XLarge,
},
},
}
for _, tc := range testCases {
grpcOptionsSlice, nodeGroupIDOptionMap := populateOptionsForGRPC(tc.opts)
assert.Equal(t, tc.expectedOpts, grpcOptionsSlice)
assert.Equal(t, tc.expectedMap, nodeGroupIDOptionMap)
}
}
func makeFakeNodeInfos() map[string]*schedulerframework.NodeInfo {
nodeInfos := make(map[string]*schedulerframework.NodeInfo)
for i, opt := range options {
nodeInfo := schedulerframework.NewNodeInfo()
nodeInfo.SetNode(nodes[i])
nodeInfos[opt.NodeGroup.Id()] = nodeInfo
}
return nodeInfos
}
func TestPopulateNodeInfoForGRPC(t *testing.T) {
nodeInfos := makeFakeNodeInfos()
grpcNodeInfoMap := populateNodeInfoForGRPC(nodeInfos)
expectedGrpcNodeInfoMap := make(map[string]*v1.Node)
for i, opt := range options {
expectedGrpcNodeInfoMap[opt.NodeGroup.Id()] = nodes[i]
}
assert.Equal(t, expectedGrpcNodeInfoMap, grpcNodeInfoMap)
}
func TestValidTransformAndSanitizeOptionsFromGRPC(t *testing.T) {
responseOptionsSlice := []*protos.Option{&grpcEoT2Micro, &grpcEoT3Large, &grpcEoM44XLarge}
nodeGroupIDOptionMap := map[string]expander.Option{
eoT2Micro.NodeGroup.Id(): eoT2Micro,
eoT2Large.NodeGroup.Id(): eoT2Large,
eoT3Large.NodeGroup.Id(): eoT3Large,
eoM44XLarge.NodeGroup.Id(): eoM44XLarge,
}
expectedOptions := []expander.Option{eoT2Micro, eoT3Large, eoM44XLarge}
ret := transformAndSanitizeOptionsFromGRPC(responseOptionsSlice, nodeGroupIDOptionMap)
assert.Equal(t, expectedOptions, ret)
}
func TestAnInvalidTransformAndSanitizeOptionsFromGRPC(t *testing.T) {
responseOptionsSlice := []*protos.Option{&grpcEoT2Micro, &grpcEoT3Large, &grpcEoM44XLarge}
nodeGroupIDOptionMap := map[string]expander.Option{
eoT2Micro.NodeGroup.Id(): eoT2Micro,
eoT2Large.NodeGroup.Id(): eoT2Large,
eoT3Large.NodeGroup.Id(): eoT3Large,
}
ret := transformAndSanitizeOptionsFromGRPC(responseOptionsSlice, nodeGroupIDOptionMap)
assert.Equal(t, []expander.Option{eoT2Micro, eoT3Large}, ret)
}
func TestBestOptionsValid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := mocks.NewMockExpanderClient(ctrl)
g := &grpcclientstrategy{mockClient}
nodeInfos := makeFakeNodeInfos()
grpcNodeInfoMap := make(map[string]*v1.Node)
for i, opt := range options {
grpcNodeInfoMap[opt.NodeGroup.Id()] = nodes[i]
}
expectedBestOptionsReq := &protos.BestOptionsRequest{
Options: []*protos.Option{&grpcEoT2Micro, &grpcEoT2Large, &grpcEoT3Large, &grpcEoM44XLarge},
NodeMap: grpcNodeInfoMap,
}
mockClient.EXPECT().BestOptions(
gomock.Any(), gomock.Eq(expectedBestOptionsReq),
).Return(&protos.BestOptionsResponse{Options: []*protos.Option{&grpcEoT3Large}}, nil)
resp := g.BestOptions(options, nodeInfos)
assert.Equal(t, resp, []expander.Option{eoT3Large})
}
// All test cases should error, and no options should be filtered
func TestBestOptionsErrors(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := mocks.NewMockExpanderClient(ctrl)
g := grpcclientstrategy{mockClient}
badProtosOption := protos.Option{
NodeGroupId: "badID",
NodeCount: int32(eoM44XLarge.NodeCount),
Debug: eoM44XLarge.Debug,
Pod: eoM44XLarge.Pods,
}
testCases := []struct {
desc string
client grpcclientstrategy
nodeInfo map[string]*schedulerframework.NodeInfo
mockResponse protos.BestOptionsResponse
errResponse error
}{
{
desc: "Bad gRPC client config",
client: grpcclientstrategy{nil},
nodeInfo: makeFakeNodeInfos(),
mockResponse: protos.BestOptionsResponse{},
errResponse: nil,
},
{
desc: "gRPC error response",
client: g,
nodeInfo: makeFakeNodeInfos(),
mockResponse: protos.BestOptionsResponse{},
errResponse: errors.New("timeout error"),
},
{
desc: "bad bestOptions response",
client: g,
nodeInfo: makeFakeNodeInfos(),
mockResponse: protos.BestOptionsResponse{},
errResponse: nil,
},
{
desc: "bad bestOptions response, options nil",
client: g,
nodeInfo: makeFakeNodeInfos(),
mockResponse: protos.BestOptionsResponse{Options: nil},
errResponse: nil,
},
{
desc: "bad bestOptions response, options invalid - nil",
client: g,
nodeInfo: makeFakeNodeInfos(),
mockResponse: protos.BestOptionsResponse{Options: []*protos.Option{&grpcEoT2Micro, nil, &grpcEoT2Large, &grpcEoT3Large, &grpcEoM44XLarge}},
errResponse: nil,
},
{
desc: "bad bestOptions response, options invalid - nonExistent nodeID",
client: g,
nodeInfo: makeFakeNodeInfos(),
mockResponse: protos.BestOptionsResponse{Options: []*protos.Option{&grpcEoT2Micro, &badProtosOption, &grpcEoT2Large, &grpcEoT3Large, &grpcEoM44XLarge}},
errResponse: nil,
},
}
for _, tc := range testCases {
grpcNodeInfoMap := populateNodeInfoForGRPC(tc.nodeInfo)
mockClient.EXPECT().BestOptions(
gomock.Any(), gomock.Eq(
&protos.BestOptionsRequest{
Options: []*protos.Option{&grpcEoT2Micro, &grpcEoT2Large, &grpcEoT3Large, &grpcEoM44XLarge},
NodeMap: grpcNodeInfoMap,
})).Return(&tc.mockResponse, tc.errResponse)
resp := g.BestOptions(options, tc.nodeInfo)
assert.Equal(t, resp, options)
}
}