autoscaler/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder_test.go

428 lines
13 KiB
Go

/*
Copyright 2019 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 input
import (
"fmt"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
autoscalingv1 "k8s.io/api/autoscaling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
controllerfetcher "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/controller_fetcher"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/history"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/spec"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model"
target_mock "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target/mock"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test"
)
type fakeControllerFetcher struct {
key *controllerfetcher.ControllerKeyWithAPIVersion
err error
}
func (f *fakeControllerFetcher) FindTopMostWellKnownOrScalable(controller *controllerfetcher.ControllerKeyWithAPIVersion) (*controllerfetcher.ControllerKeyWithAPIVersion, error) {
return f.key, f.err
}
func parseLabelSelector(selector string) labels.Selector {
labelSelector, _ := metav1.ParseToLabelSelector(selector)
parsedSelector, _ := metav1.LabelSelectorAsSelector(labelSelector)
return parsedSelector
}
var (
unsupportedConditionNoLongerSupported = "Label selector is no longer supported, please migrate to targetRef"
unsupportedConditionTextFromFetcher = "Cannot read targetRef. Reason: targetRef not defined"
unsupportedConditionNoExtraText = "Cannot read targetRef"
unsupportedConditionBothDefined = "Both targetRef and label selector defined. Please remove label selector"
unsupportedConditionNoTargetRef = "Cannot read targetRef"
unsupportedConditionMudaMudaMuda = "Error checking if target is a topmost well-known or scalable controller: muda muda muda"
unsupportedTargetRefHasParent = "The targetRef controller has a parent but it should point to a topmost well-known or scalable controller"
)
const (
kind = "dodokind"
name1 = "dotaro"
name2 = "doseph"
namespace = "testNamespace"
apiVersion = "stardust"
testGcPeriod = time.Minute
)
func TestLoadPods(t *testing.T) {
type testCase struct {
name string
selector labels.Selector
fetchSelectorError error
targetRef *autoscalingv1.CrossVersionObjectReference
topMostWellKnownOrScalableKey *controllerfetcher.ControllerKeyWithAPIVersion
findTopMostWellKnownOrScalableError error
expectedSelector labels.Selector
expectedConfigUnsupported *string
expectedConfigDeprecated *string
}
testCases := []testCase{
{
name: "no selector",
selector: nil,
fetchSelectorError: fmt.Errorf("targetRef not defined"),
expectedSelector: labels.Nothing(),
expectedConfigUnsupported: &unsupportedConditionTextFromFetcher,
expectedConfigDeprecated: nil,
},
{
name: "also no selector but no error",
selector: nil,
fetchSelectorError: nil,
expectedSelector: labels.Nothing(),
expectedConfigUnsupported: &unsupportedConditionNoExtraText,
expectedConfigDeprecated: nil,
},
{
name: "targetRef selector",
selector: parseLabelSelector("app = test"),
fetchSelectorError: nil,
targetRef: &autoscalingv1.CrossVersionObjectReference{
Kind: kind,
Name: name1,
APIVersion: apiVersion,
},
topMostWellKnownOrScalableKey: &controllerfetcher.ControllerKeyWithAPIVersion{
ControllerKey: controllerfetcher.ControllerKey{
Kind: kind,
Name: name1,
Namespace: namespace,
},
ApiVersion: apiVersion,
},
expectedSelector: parseLabelSelector("app = test"),
expectedConfigUnsupported: nil,
expectedConfigDeprecated: nil,
},
{
name: "no targetRef",
selector: parseLabelSelector("app = test"),
fetchSelectorError: nil,
expectedSelector: labels.Nothing(),
expectedConfigUnsupported: nil,
expectedConfigDeprecated: nil,
},
{
name: "can't decide if top-level-ref",
selector: nil,
fetchSelectorError: nil,
expectedSelector: labels.Nothing(),
targetRef: &autoscalingv1.CrossVersionObjectReference{
Kind: kind,
Name: name1,
APIVersion: apiVersion,
},
expectedConfigUnsupported: &unsupportedConditionNoTargetRef,
},
{
name: "non-top-level targetRef",
selector: parseLabelSelector("app = test"),
fetchSelectorError: nil,
expectedSelector: labels.Nothing(),
targetRef: &autoscalingv1.CrossVersionObjectReference{
Kind: kind,
Name: name1,
APIVersion: apiVersion,
},
topMostWellKnownOrScalableKey: &controllerfetcher.ControllerKeyWithAPIVersion{
ControllerKey: controllerfetcher.ControllerKey{
Kind: kind,
Name: name2,
Namespace: namespace,
},
ApiVersion: apiVersion,
},
expectedConfigUnsupported: &unsupportedTargetRefHasParent,
},
{
name: "error checking if top-level-ref",
selector: parseLabelSelector("app = test"),
fetchSelectorError: nil,
expectedSelector: labels.Nothing(),
targetRef: &autoscalingv1.CrossVersionObjectReference{
Kind: "doestar",
Name: "doseph-doestar",
APIVersion: "taxonomy",
},
expectedConfigUnsupported: &unsupportedConditionMudaMudaMuda,
findTopMostWellKnownOrScalableError: fmt.Errorf("muda muda muda"),
},
{
name: "top-level target ref",
selector: parseLabelSelector("app = test"),
fetchSelectorError: nil,
expectedSelector: parseLabelSelector("app = test"),
targetRef: &autoscalingv1.CrossVersionObjectReference{
Kind: kind,
Name: name1,
APIVersion: apiVersion,
},
topMostWellKnownOrScalableKey: &controllerfetcher.ControllerKeyWithAPIVersion{
ControllerKey: controllerfetcher.ControllerKey{
Kind: kind,
Name: name1,
Namespace: namespace,
},
ApiVersion: apiVersion,
},
expectedConfigUnsupported: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
vpa := test.VerticalPodAutoscaler().WithName("testVpa").WithContainer("container").WithNamespace("testNamespace").WithTargetRef(tc.targetRef).Get()
vpaLister := &test.VerticalPodAutoscalerListerMock{}
vpaLister.On("List").Return([]*vpa_types.VerticalPodAutoscaler{vpa}, nil)
targetSelectorFetcher := target_mock.NewMockVpaTargetSelectorFetcher(ctrl)
clusterState := model.NewClusterState(testGcPeriod)
clusterStateFeeder := clusterStateFeeder{
vpaLister: vpaLister,
clusterState: clusterState,
selectorFetcher: targetSelectorFetcher,
controllerFetcher: &fakeControllerFetcher{
key: tc.topMostWellKnownOrScalableKey,
err: tc.findTopMostWellKnownOrScalableError,
},
}
targetSelectorFetcher.EXPECT().Fetch(vpa).Return(tc.selector, tc.fetchSelectorError)
clusterStateFeeder.LoadVPAs()
vpaID := model.VpaID{
Namespace: vpa.Namespace,
VpaName: vpa.Name,
}
assert.Contains(t, clusterState.Vpas, vpaID)
storedVpa := clusterState.Vpas[vpaID]
if tc.expectedSelector != nil {
assert.NotNil(t, storedVpa.PodSelector)
assert.Equal(t, tc.expectedSelector.String(), storedVpa.PodSelector.String())
} else {
assert.Nil(t, storedVpa.PodSelector)
}
if tc.expectedConfigDeprecated != nil {
assert.Contains(t, storedVpa.Conditions, vpa_types.ConfigDeprecated)
assert.Equal(t, *tc.expectedConfigDeprecated, storedVpa.Conditions[vpa_types.ConfigDeprecated].Message)
} else {
assert.NotContains(t, storedVpa.Conditions, vpa_types.ConfigDeprecated)
}
if tc.expectedConfigUnsupported != nil {
assert.Contains(t, storedVpa.Conditions, vpa_types.ConfigUnsupported)
assert.Equal(t, *tc.expectedConfigUnsupported, storedVpa.Conditions[vpa_types.ConfigUnsupported].Message)
} else {
assert.NotContains(t, storedVpa.Conditions, vpa_types.ConfigUnsupported)
}
})
}
}
type testSpecClient struct {
pods []*spec.BasicPodSpec
}
func (c *testSpecClient) GetPodSpecs() ([]*spec.BasicPodSpec, error) {
return c.pods, nil
}
func makeTestSpecClient(podLabels []map[string]string) spec.SpecClient {
pods := make([]*spec.BasicPodSpec, len(podLabels))
for i, l := range podLabels {
pods[i] = &spec.BasicPodSpec{
ID: model.PodID{Namespace: "default", PodName: fmt.Sprintf("pod-%d", i)},
PodLabels: l,
}
}
return &testSpecClient{
pods: pods,
}
}
func TestClusterStateFeeder_LoadPods(t *testing.T) {
for _, tc := range []struct {
Name string
VPALabelSelectors []string
PodLabels []map[string]string
TrackedPods int
}{
{
Name: "simple",
VPALabelSelectors: []string{"name=vpa-pod"},
PodLabels: []map[string]string{
{"name": "vpa-pod"},
{"type": "stateful"},
},
TrackedPods: 1,
},
{
Name: "multiple",
VPALabelSelectors: []string{"name=vpa-pod,type=stateful"},
PodLabels: []map[string]string{
{"name": "vpa-pod", "type": "stateful"},
{"type": "stateful"},
{"name": "vpa-pod"},
},
TrackedPods: 1,
},
{
Name: "no matches",
VPALabelSelectors: []string{"name=vpa-pod"},
PodLabels: []map[string]string{
{"name": "non-vpa-pod", "type": "stateful"},
},
TrackedPods: 0,
},
{
Name: "set based",
VPALabelSelectors: []string{"environment in (staging, qa),name=vpa-pod"},
PodLabels: []map[string]string{
{"name": "vpa-pod", "environment": "staging"},
{"name": "vpa-pod", "environment": "production"},
{"name": "non-vpa-pod", "environment": "staging"},
{"name": "non-vpa-pod", "environment": "production"},
},
TrackedPods: 1,
},
} {
t.Run(tc.Name, func(t *testing.T) {
clusterState := model.NewClusterState(testGcPeriod)
for i, selector := range tc.VPALabelSelectors {
vpaLabel, err := labels.Parse(selector)
assert.NoError(t, err)
clusterState.Vpas = map[model.VpaID]*model.Vpa{
{VpaName: fmt.Sprintf("test-vpa-%d", i), Namespace: "default"}: {PodSelector: vpaLabel},
}
}
feeder := clusterStateFeeder{
specClient: makeTestSpecClient(tc.PodLabels),
memorySaveMode: true,
clusterState: clusterState,
}
feeder.LoadPods()
assert.Len(t, feeder.clusterState.Pods, tc.TrackedPods, "number of pods is not %d", tc.TrackedPods)
feeder = clusterStateFeeder{
specClient: makeTestSpecClient(tc.PodLabels),
memorySaveMode: false,
clusterState: clusterState,
}
feeder.LoadPods()
assert.Len(t, feeder.clusterState.Pods, len(tc.PodLabels), "number of pods is not %d", len(tc.PodLabels))
})
}
}
type fakeHistoryProvider struct {
history map[model.PodID]*history.PodHistory
err error
}
func (fhp *fakeHistoryProvider) GetClusterHistory() (map[model.PodID]*history.PodHistory, error) {
return fhp.history, fhp.err
}
func TestClusterStateFeeder_InitFromHistoryProvider(t *testing.T) {
pod1 := model.PodID{
Namespace: "ns",
PodName: "a-pod",
}
memAmount := model.ResourceAmount(128 * 1024 * 1024)
t0 := time.Date(2021, time.August, 30, 10, 21, 0, 0, time.UTC)
containerCpu := "containerCpu"
containerMem := "containerMem"
pod1History := history.PodHistory{
LastLabels: map[string]string{},
LastSeen: t0,
Samples: map[string][]model.ContainerUsageSample{
containerCpu: {
{
MeasureStart: t0,
Usage: 10,
Request: 101,
Resource: model.ResourceCPU,
},
},
containerMem: {
{
MeasureStart: t0,
Usage: memAmount,
Request: 1024 * 1024 * 1024,
Resource: model.ResourceMemory,
},
},
},
}
provider := fakeHistoryProvider{
history: map[model.PodID]*history.PodHistory{
pod1: &pod1History,
},
}
clusterState := model.NewClusterState()
feeder := clusterStateFeeder{
clusterState: clusterState,
}
feeder.InitFromHistoryProvider(&provider)
if !assert.Contains(t, feeder.clusterState.Pods, pod1) {
return
}
pod1State := feeder.clusterState.Pods[pod1]
if !assert.Contains(t, pod1State.Containers, containerCpu) {
return
}
containerState := pod1State.Containers[containerCpu]
if !assert.NotNil(t, containerState) {
return
}
assert.Equal(t, t0, containerState.LastCPUSampleStart)
if !assert.Contains(t, pod1State.Containers, containerMem) {
return
}
containerState = pod1State.Containers[containerMem]
if !assert.NotNil(t, containerState) {
return
}
assert.Equal(t, memAmount, containerState.GetMaxMemoryPeak())
}