123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766 |
- /*
- Copyright 2015 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 podautoscaler
- import (
- "encoding/json"
- "fmt"
- "math"
- "sync"
- "testing"
- "time"
- autoscalingv1 "k8s.io/api/autoscaling/v1"
- autoscalingv2 "k8s.io/api/autoscaling/v2beta2"
- v1 "k8s.io/api/core/v1"
- "k8s.io/apimachinery/pkg/api/meta/testrestmapper"
- "k8s.io/apimachinery/pkg/api/resource"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/labels"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/apimachinery/pkg/watch"
- "k8s.io/client-go/informers"
- "k8s.io/client-go/kubernetes/fake"
- scalefake "k8s.io/client-go/scale/fake"
- core "k8s.io/client-go/testing"
- "k8s.io/kubernetes/pkg/api/legacyscheme"
- "k8s.io/kubernetes/pkg/apis/autoscaling"
- "k8s.io/kubernetes/pkg/controller"
- "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
- cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
- emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
- metricsapi "k8s.io/metrics/pkg/apis/metrics/v1beta1"
- metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake"
- cmfake "k8s.io/metrics/pkg/client/custom_metrics/fake"
- emfake "k8s.io/metrics/pkg/client/external_metrics/fake"
- "github.com/stretchr/testify/assert"
- _ "k8s.io/kubernetes/pkg/apis/apps/install"
- _ "k8s.io/kubernetes/pkg/apis/autoscaling/install"
- )
- var statusOk = []autoscalingv2.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"},
- {Type: autoscalingv2.ScalingActive, Status: v1.ConditionTrue, Reason: "ValidMetricFound"},
- {Type: autoscalingv2.ScalingLimited, Status: v1.ConditionFalse, Reason: "DesiredWithinRange"},
- }
- // statusOkWithOverrides returns the "ok" status with the given conditions as overridden
- func statusOkWithOverrides(overrides ...autoscalingv2.HorizontalPodAutoscalerCondition) []autoscalingv1.HorizontalPodAutoscalerCondition {
- resv2 := make([]autoscalingv2.HorizontalPodAutoscalerCondition, len(statusOk))
- copy(resv2, statusOk)
- for _, override := range overrides {
- resv2 = setConditionInList(resv2, override.Type, override.Status, override.Reason, override.Message)
- }
- // copy to a v1 slice
- resv1 := make([]autoscalingv1.HorizontalPodAutoscalerCondition, len(resv2))
- for i, cond := range resv2 {
- resv1[i] = autoscalingv1.HorizontalPodAutoscalerCondition{
- Type: autoscalingv1.HorizontalPodAutoscalerConditionType(cond.Type),
- Status: cond.Status,
- Reason: cond.Reason,
- }
- }
- return resv1
- }
- func alwaysReady() bool { return true }
- type fakeResource struct {
- name string
- apiVersion string
- kind string
- }
- type testCase struct {
- sync.Mutex
- minReplicas int32
- maxReplicas int32
- initialReplicas int32
- // CPU target utilization as a percentage of the requested resources.
- CPUTarget int32
- CPUCurrent int32
- verifyCPUCurrent bool
- reportedLevels []uint64
- reportedCPURequests []resource.Quantity
- reportedPodReadiness []v1.ConditionStatus
- reportedPodStartTime []metav1.Time
- reportedPodPhase []v1.PodPhase
- reportedPodDeletionTimestamp []bool
- scaleUpdated bool
- statusUpdated bool
- eventCreated bool
- verifyEvents bool
- useMetricsAPI bool
- metricsTarget []autoscalingv2.MetricSpec
- expectedDesiredReplicas int32
- expectedConditions []autoscalingv1.HorizontalPodAutoscalerCondition
- // Channel with names of HPA objects which we have reconciled.
- processed chan string
- // Target resource information.
- resource *fakeResource
- // Last scale time
- lastScaleTime *metav1.Time
- // override the test clients
- testClient *fake.Clientset
- testMetricsClient *metricsfake.Clientset
- testCMClient *cmfake.FakeCustomMetricsClient
- testEMClient *emfake.FakeExternalMetricsClient
- testScaleClient *scalefake.FakeScaleClient
- recommendations []timestampedRecommendation
- }
- // Needs to be called under a lock.
- func (tc *testCase) computeCPUCurrent() {
- if len(tc.reportedLevels) != len(tc.reportedCPURequests) || len(tc.reportedLevels) == 0 {
- return
- }
- reported := 0
- for _, r := range tc.reportedLevels {
- reported += int(r)
- }
- requested := 0
- for _, req := range tc.reportedCPURequests {
- requested += int(req.MilliValue())
- }
- tc.CPUCurrent = int32(100 * reported / requested)
- }
- func init() {
- // set this high so we don't accidentally run into it when testing
- scaleUpLimitFactor = 8
- }
- func (tc *testCase) prepareTestClient(t *testing.T) (*fake.Clientset, *metricsfake.Clientset, *cmfake.FakeCustomMetricsClient, *emfake.FakeExternalMetricsClient, *scalefake.FakeScaleClient) {
- namespace := "test-namespace"
- hpaName := "test-hpa"
- podNamePrefix := "test-pod"
- labelSet := map[string]string{"name": podNamePrefix}
- selector := labels.SelectorFromSet(labelSet).String()
- tc.Lock()
- tc.scaleUpdated = false
- tc.statusUpdated = false
- tc.eventCreated = false
- tc.processed = make(chan string, 100)
- if tc.CPUCurrent == 0 {
- tc.computeCPUCurrent()
- }
- if tc.resource == nil {
- tc.resource = &fakeResource{
- name: "test-rc",
- apiVersion: "v1",
- kind: "ReplicationController",
- }
- }
- tc.Unlock()
- fakeClient := &fake.Clientset{}
- fakeClient.AddReactor("list", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := &autoscalingv2.HorizontalPodAutoscalerList{
- Items: []autoscalingv2.HorizontalPodAutoscaler{
- {
- ObjectMeta: metav1.ObjectMeta{
- Name: hpaName,
- Namespace: namespace,
- SelfLink: "experimental/v1/namespaces/" + namespace + "/horizontalpodautoscalers/" + hpaName,
- },
- Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
- ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
- Kind: tc.resource.kind,
- Name: tc.resource.name,
- APIVersion: tc.resource.apiVersion,
- },
- MinReplicas: &tc.minReplicas,
- MaxReplicas: tc.maxReplicas,
- },
- Status: autoscalingv2.HorizontalPodAutoscalerStatus{
- CurrentReplicas: tc.initialReplicas,
- DesiredReplicas: tc.initialReplicas,
- LastScaleTime: tc.lastScaleTime,
- },
- },
- },
- }
- if tc.CPUTarget > 0 {
- obj.Items[0].Spec.Metrics = []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ResourceMetricSourceType,
- Resource: &autoscalingv2.ResourceMetricSource{
- Name: v1.ResourceCPU,
- Target: autoscalingv2.MetricTarget{
- AverageUtilization: &tc.CPUTarget,
- },
- },
- },
- }
- }
- if len(tc.metricsTarget) > 0 {
- obj.Items[0].Spec.Metrics = append(obj.Items[0].Spec.Metrics, tc.metricsTarget...)
- }
- if len(obj.Items[0].Spec.Metrics) == 0 {
- // manually add in the defaulting logic
- obj.Items[0].Spec.Metrics = []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ResourceMetricSourceType,
- Resource: &autoscalingv2.ResourceMetricSource{
- Name: v1.ResourceCPU,
- },
- },
- }
- }
- // and... convert to autoscaling v1 to return the right type
- objv1, err := unsafeConvertToVersionVia(obj, autoscalingv1.SchemeGroupVersion)
- if err != nil {
- return true, nil, err
- }
- return true, objv1, nil
- })
- fakeClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := &v1.PodList{}
- specifiedCPURequests := tc.reportedCPURequests != nil
- numPodsToCreate := int(tc.initialReplicas)
- if specifiedCPURequests {
- numPodsToCreate = len(tc.reportedCPURequests)
- }
- for i := 0; i < numPodsToCreate; i++ {
- podReadiness := v1.ConditionTrue
- if tc.reportedPodReadiness != nil {
- podReadiness = tc.reportedPodReadiness[i]
- }
- var podStartTime metav1.Time
- if tc.reportedPodStartTime != nil {
- podStartTime = tc.reportedPodStartTime[i]
- }
- podPhase := v1.PodRunning
- if tc.reportedPodPhase != nil {
- podPhase = tc.reportedPodPhase[i]
- }
- podDeletionTimestamp := false
- if tc.reportedPodDeletionTimestamp != nil {
- podDeletionTimestamp = tc.reportedPodDeletionTimestamp[i]
- }
- podName := fmt.Sprintf("%s-%d", podNamePrefix, i)
- reportedCPURequest := resource.MustParse("1.0")
- if specifiedCPURequests {
- reportedCPURequest = tc.reportedCPURequests[i]
- }
- pod := v1.Pod{
- Status: v1.PodStatus{
- Phase: podPhase,
- Conditions: []v1.PodCondition{
- {
- Type: v1.PodReady,
- Status: podReadiness,
- LastTransitionTime: podStartTime,
- },
- },
- StartTime: &podStartTime,
- },
- ObjectMeta: metav1.ObjectMeta{
- Name: podName,
- Namespace: namespace,
- Labels: map[string]string{
- "name": podNamePrefix,
- },
- },
- Spec: v1.PodSpec{
- Containers: []v1.Container{
- {
- Resources: v1.ResourceRequirements{
- Requests: v1.ResourceList{
- v1.ResourceCPU: reportedCPURequest,
- },
- },
- },
- },
- },
- }
- if podDeletionTimestamp {
- pod.DeletionTimestamp = &metav1.Time{Time: time.Now()}
- }
- obj.Items = append(obj.Items, pod)
- }
- return true, obj, nil
- })
- fakeClient.AddReactor("update", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- handled, obj, err := func() (handled bool, ret *autoscalingv1.HorizontalPodAutoscaler, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := action.(core.UpdateAction).GetObject().(*autoscalingv1.HorizontalPodAutoscaler)
- assert.Equal(t, namespace, obj.Namespace, "the HPA namespace should be as expected")
- assert.Equal(t, hpaName, obj.Name, "the HPA name should be as expected")
- assert.Equal(t, tc.expectedDesiredReplicas, obj.Status.DesiredReplicas, "the desired replica count reported in the object status should be as expected")
- if tc.verifyCPUCurrent {
- if assert.NotNil(t, obj.Status.CurrentCPUUtilizationPercentage, "the reported CPU utilization percentage should be non-nil") {
- assert.Equal(t, tc.CPUCurrent, *obj.Status.CurrentCPUUtilizationPercentage, "the report CPU utilization percentage should be as expected")
- }
- }
- var actualConditions []autoscalingv1.HorizontalPodAutoscalerCondition
- if err := json.Unmarshal([]byte(obj.ObjectMeta.Annotations[autoscaling.HorizontalPodAutoscalerConditionsAnnotation]), &actualConditions); err != nil {
- return true, nil, err
- }
- // TODO: it's ok not to sort these becaues statusOk
- // contains all the conditions, so we'll never be appending.
- // Default to statusOk when missing any specific conditions
- if tc.expectedConditions == nil {
- tc.expectedConditions = statusOkWithOverrides()
- }
- // clear the message so that we can easily compare
- for i := range actualConditions {
- actualConditions[i].Message = ""
- actualConditions[i].LastTransitionTime = metav1.Time{}
- }
- assert.Equal(t, tc.expectedConditions, actualConditions, "the status conditions should have been as expected")
- tc.statusUpdated = true
- // Every time we reconcile HPA object we are updating status.
- return true, obj, nil
- }()
- if obj != nil {
- tc.processed <- obj.Name
- }
- return handled, obj, err
- })
- fakeScaleClient := &scalefake.FakeScaleClient{}
- fakeScaleClient.AddReactor("get", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := &autoscalingv1.Scale{
- ObjectMeta: metav1.ObjectMeta{
- Name: tc.resource.name,
- Namespace: namespace,
- },
- Spec: autoscalingv1.ScaleSpec{
- Replicas: tc.initialReplicas,
- },
- Status: autoscalingv1.ScaleStatus{
- Replicas: tc.initialReplicas,
- Selector: selector,
- },
- }
- return true, obj, nil
- })
- fakeScaleClient.AddReactor("get", "deployments", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := &autoscalingv1.Scale{
- ObjectMeta: metav1.ObjectMeta{
- Name: tc.resource.name,
- Namespace: namespace,
- },
- Spec: autoscalingv1.ScaleSpec{
- Replicas: tc.initialReplicas,
- },
- Status: autoscalingv1.ScaleStatus{
- Replicas: tc.initialReplicas,
- Selector: selector,
- },
- }
- return true, obj, nil
- })
- fakeScaleClient.AddReactor("get", "replicasets", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := &autoscalingv1.Scale{
- ObjectMeta: metav1.ObjectMeta{
- Name: tc.resource.name,
- Namespace: namespace,
- },
- Spec: autoscalingv1.ScaleSpec{
- Replicas: tc.initialReplicas,
- },
- Status: autoscalingv1.ScaleStatus{
- Replicas: tc.initialReplicas,
- Selector: selector,
- },
- }
- return true, obj, nil
- })
- fakeScaleClient.AddReactor("update", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale)
- replicas := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale).Spec.Replicas
- assert.Equal(t, tc.expectedDesiredReplicas, replicas, "the replica count of the RC should be as expected")
- tc.scaleUpdated = true
- return true, obj, nil
- })
- fakeScaleClient.AddReactor("update", "deployments", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale)
- replicas := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale).Spec.Replicas
- assert.Equal(t, tc.expectedDesiredReplicas, replicas, "the replica count of the deployment should be as expected")
- tc.scaleUpdated = true
- return true, obj, nil
- })
- fakeScaleClient.AddReactor("update", "replicasets", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale)
- replicas := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale).Spec.Replicas
- assert.Equal(t, tc.expectedDesiredReplicas, replicas, "the replica count of the replicaset should be as expected")
- tc.scaleUpdated = true
- return true, obj, nil
- })
- fakeWatch := watch.NewFake()
- fakeClient.AddWatchReactor("*", core.DefaultWatchReactor(fakeWatch, nil))
- fakeMetricsClient := &metricsfake.Clientset{}
- fakeMetricsClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- metrics := &metricsapi.PodMetricsList{}
- for i, cpu := range tc.reportedLevels {
- // NB: the list reactor actually does label selector filtering for us,
- // so we have to make sure our results match the label selector
- podMetric := metricsapi.PodMetrics{
- ObjectMeta: metav1.ObjectMeta{
- Name: fmt.Sprintf("%s-%d", podNamePrefix, i),
- Namespace: namespace,
- Labels: labelSet,
- },
- Timestamp: metav1.Time{Time: time.Now()},
- Window: metav1.Duration{Duration: time.Minute},
- Containers: []metricsapi.ContainerMetrics{
- {
- Name: "container",
- Usage: v1.ResourceList{
- v1.ResourceCPU: *resource.NewMilliQuantity(
- int64(cpu),
- resource.DecimalSI),
- v1.ResourceMemory: *resource.NewQuantity(
- int64(1024*1024),
- resource.BinarySI),
- },
- },
- },
- }
- metrics.Items = append(metrics.Items, podMetric)
- }
- return true, metrics, nil
- })
- fakeCMClient := &cmfake.FakeCustomMetricsClient{}
- fakeCMClient.AddReactor("get", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- getForAction, wasGetFor := action.(cmfake.GetForAction)
- if !wasGetFor {
- return true, nil, fmt.Errorf("expected a get-for action, got %v instead", action)
- }
- if getForAction.GetName() == "*" {
- metrics := &cmapi.MetricValueList{}
- // multiple objects
- assert.Equal(t, "pods", getForAction.GetResource().Resource, "the type of object that we requested multiple metrics for should have been pods")
- assert.Equal(t, "qps", getForAction.GetMetricName(), "the metric name requested should have been qps, as specified in the metric spec")
- for i, level := range tc.reportedLevels {
- podMetric := cmapi.MetricValue{
- DescribedObject: v1.ObjectReference{
- Kind: "Pod",
- Name: fmt.Sprintf("%s-%d", podNamePrefix, i),
- Namespace: namespace,
- },
- Timestamp: metav1.Time{Time: time.Now()},
- Metric: cmapi.MetricIdentifier{
- Name: "qps",
- },
- Value: *resource.NewMilliQuantity(int64(level), resource.DecimalSI),
- }
- metrics.Items = append(metrics.Items, podMetric)
- }
- return true, metrics, nil
- }
- name := getForAction.GetName()
- mapper := testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)
- metrics := &cmapi.MetricValueList{}
- var matchedTarget *autoscalingv2.MetricSpec
- for i, target := range tc.metricsTarget {
- if target.Type == autoscalingv2.ObjectMetricSourceType && name == target.Object.DescribedObject.Name {
- gk := schema.FromAPIVersionAndKind(target.Object.DescribedObject.APIVersion, target.Object.DescribedObject.Kind).GroupKind()
- mapping, err := mapper.RESTMapping(gk)
- if err != nil {
- t.Logf("unable to get mapping for %s: %v", gk.String(), err)
- continue
- }
- groupResource := mapping.Resource.GroupResource()
- if getForAction.GetResource().Resource == groupResource.String() {
- matchedTarget = &tc.metricsTarget[i]
- }
- }
- }
- assert.NotNil(t, matchedTarget, "this request should have matched one of the metric specs")
- assert.Equal(t, "qps", getForAction.GetMetricName(), "the metric name requested should have been qps, as specified in the metric spec")
- metrics.Items = []cmapi.MetricValue{
- {
- DescribedObject: v1.ObjectReference{
- Kind: matchedTarget.Object.DescribedObject.Kind,
- APIVersion: matchedTarget.Object.DescribedObject.APIVersion,
- Name: name,
- },
- Timestamp: metav1.Time{Time: time.Now()},
- Metric: cmapi.MetricIdentifier{
- Name: "qps",
- },
- Value: *resource.NewMilliQuantity(int64(tc.reportedLevels[0]), resource.DecimalSI),
- },
- }
- return true, metrics, nil
- })
- fakeEMClient := &emfake.FakeExternalMetricsClient{}
- fakeEMClient.AddReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- listAction, wasList := action.(core.ListAction)
- if !wasList {
- return true, nil, fmt.Errorf("expected a list action, got %v instead", action)
- }
- metrics := &emapi.ExternalMetricValueList{}
- assert.Equal(t, "qps", listAction.GetResource().Resource, "the metric name requested should have been qps, as specified in the metric spec")
- for _, level := range tc.reportedLevels {
- metric := emapi.ExternalMetricValue{
- Timestamp: metav1.Time{Time: time.Now()},
- MetricName: "qps",
- Value: *resource.NewMilliQuantity(int64(level), resource.DecimalSI),
- }
- metrics.Items = append(metrics.Items, metric)
- }
- return true, metrics, nil
- })
- return fakeClient, fakeMetricsClient, fakeCMClient, fakeEMClient, fakeScaleClient
- }
- func (tc *testCase) verifyResults(t *testing.T) {
- tc.Lock()
- defer tc.Unlock()
- assert.Equal(t, tc.initialReplicas != tc.expectedDesiredReplicas, tc.scaleUpdated, "the scale should only be updated if we expected a change in replicas")
- assert.True(t, tc.statusUpdated, "the status should have been updated")
- if tc.verifyEvents {
- assert.Equal(t, tc.initialReplicas != tc.expectedDesiredReplicas, tc.eventCreated, "an event should have been created only if we expected a change in replicas")
- }
- }
- func (tc *testCase) setupController(t *testing.T) (*HorizontalController, informers.SharedInformerFactory) {
- testClient, testMetricsClient, testCMClient, testEMClient, testScaleClient := tc.prepareTestClient(t)
- if tc.testClient != nil {
- testClient = tc.testClient
- }
- if tc.testMetricsClient != nil {
- testMetricsClient = tc.testMetricsClient
- }
- if tc.testCMClient != nil {
- testCMClient = tc.testCMClient
- }
- if tc.testEMClient != nil {
- testEMClient = tc.testEMClient
- }
- if tc.testScaleClient != nil {
- testScaleClient = tc.testScaleClient
- }
- metricsClient := metrics.NewRESTMetricsClient(
- testMetricsClient.MetricsV1beta1(),
- testCMClient,
- testEMClient,
- )
- eventClient := &fake.Clientset{}
- eventClient.AddReactor("create", "events", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- obj := action.(core.CreateAction).GetObject().(*v1.Event)
- if tc.verifyEvents {
- switch obj.Reason {
- case "SuccessfulRescale":
- assert.Equal(t, fmt.Sprintf("New size: %d; reason: cpu resource utilization (percentage of request) above target", tc.expectedDesiredReplicas), obj.Message)
- case "DesiredReplicasComputed":
- assert.Equal(t, fmt.Sprintf(
- "Computed the desired num of replicas: %d (avgCPUutil: %d, current replicas: %d)",
- tc.expectedDesiredReplicas,
- (int64(tc.reportedLevels[0])*100)/tc.reportedCPURequests[0].MilliValue(), tc.initialReplicas), obj.Message)
- default:
- assert.False(t, true, fmt.Sprintf("Unexpected event: %s / %s", obj.Reason, obj.Message))
- }
- }
- tc.eventCreated = true
- return true, obj, nil
- })
- informerFactory := informers.NewSharedInformerFactory(testClient, controller.NoResyncPeriodFunc())
- defaultDownscalestabilizationWindow := 5 * time.Minute
- hpaController := NewHorizontalController(
- eventClient.CoreV1(),
- testScaleClient,
- testClient.AutoscalingV1(),
- testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme),
- metricsClient,
- informerFactory.Autoscaling().V1().HorizontalPodAutoscalers(),
- informerFactory.Core().V1().Pods(),
- controller.NoResyncPeriodFunc(),
- defaultDownscalestabilizationWindow,
- defaultTestingTolerance,
- defaultTestingCpuInitializationPeriod,
- defaultTestingDelayOfInitialReadinessStatus,
- )
- hpaController.hpaListerSynced = alwaysReady
- if tc.recommendations != nil {
- hpaController.recommendations["test-namespace/test-hpa"] = tc.recommendations
- }
- return hpaController, informerFactory
- }
- func hotCpuCreationTime() metav1.Time {
- return metav1.Time{Time: time.Now()}
- }
- func coolCpuCreationTime() metav1.Time {
- return metav1.Time{Time: time.Now().Add(-3 * time.Minute)}
- }
- func (tc *testCase) runTestWithController(t *testing.T, hpaController *HorizontalController, informerFactory informers.SharedInformerFactory) {
- stop := make(chan struct{})
- defer close(stop)
- informerFactory.Start(stop)
- go hpaController.Run(stop)
- tc.Lock()
- shouldWait := tc.verifyEvents
- tc.Unlock()
- if shouldWait {
- // We need to wait for events to be broadcasted (sleep for longer than record.sleepDuration).
- timeoutTime := time.Now().Add(2 * time.Second)
- for now := time.Now(); timeoutTime.After(now); now = time.Now() {
- sleepUntil := timeoutTime.Sub(now)
- select {
- case <-tc.processed:
- // drain the chan of any sent events to keep it from filling before the timeout
- case <-time.After(sleepUntil):
- // timeout reached, ready to verifyResults
- }
- }
- } else {
- // Wait for HPA to be processed.
- <-tc.processed
- }
- tc.verifyResults(t)
- }
- func (tc *testCase) runTest(t *testing.T) {
- hpaController, informerFactory := tc.setupController(t)
- tc.runTestWithController(t, hpaController, informerFactory)
- }
- func TestScaleUp(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 5,
- CPUTarget: 30,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{300, 500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- }
- tc.runTest(t)
- }
- func TestScaleUpUnreadyLessScale(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- CPUTarget: 30,
- CPUCurrent: 60,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{300, 500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
- useMetricsAPI: true,
- }
- tc.runTest(t)
- }
- func TestScaleUpHotCpuLessScale(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- CPUTarget: 30,
- CPUCurrent: 60,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{300, 500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- reportedPodStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime()},
- useMetricsAPI: true,
- }
- tc.runTest(t)
- }
- func TestScaleUpUnreadyNoScale(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 30,
- CPUCurrent: 40,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{400, 500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestScaleUpHotCpuNoScale(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 30,
- CPUCurrent: 40,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{400, 500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
- reportedPodStartTime: []metav1.Time{coolCpuCreationTime(), hotCpuCreationTime(), hotCpuCreationTime()},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestScaleUpIgnoresFailedPods(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 2,
- expectedDesiredReplicas: 4,
- CPUTarget: 30,
- CPUCurrent: 60,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
- reportedPodPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
- useMetricsAPI: true,
- }
- tc.runTest(t)
- }
- func TestScaleUpIgnoresDeletionPods(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 2,
- expectedDesiredReplicas: 4,
- CPUTarget: 30,
- CPUCurrent: 60,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
- reportedPodPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
- reportedPodDeletionTimestamp: []bool{false, false, true, true},
- useMetricsAPI: true,
- }
- tc.runTest(t)
- }
- func TestScaleUpDeployment(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 5,
- CPUTarget: 30,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{300, 500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- resource: &fakeResource{
- name: "test-dep",
- apiVersion: "apps/v1",
- kind: "Deployment",
- },
- }
- tc.runTest(t)
- }
- func TestScaleUpReplicaSet(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 5,
- CPUTarget: 30,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{300, 500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- resource: &fakeResource{
- name: "test-replicaset",
- apiVersion: "apps/v1",
- kind: "ReplicaSet",
- },
- }
- tc.runTest(t)
- }
- func TestScaleUpCM(t *testing.T) {
- averageValue := resource.MustParse("15.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{20000, 10000, 30000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- }
- tc.runTest(t)
- }
- func TestScaleUpCMUnreadyAndHotCpuNoLessScale(t *testing.T) {
- averageValue := resource.MustParse("15.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 6,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{50000, 10000, 30000},
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse},
- reportedPodStartTime: []metav1.Time{coolCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime()},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- }
- tc.runTest(t)
- }
- func TestScaleUpCMUnreadyandCpuHot(t *testing.T) {
- averageValue := resource.MustParse("15.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 6,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{50000, 15000, 30000},
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
- reportedPodStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime()},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "SucceededRescale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "TooManyReplicas",
- }),
- }
- tc.runTest(t)
- }
- func TestScaleUpHotCpuNoScaleWouldScaleDown(t *testing.T) {
- averageValue := resource.MustParse("15.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 6,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{50000, 15000, 30000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- reportedPodStartTime: []metav1.Time{hotCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime()},
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "SucceededRescale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "TooManyReplicas",
- }),
- }
- tc.runTest(t)
- }
- func TestScaleUpCMObject(t *testing.T) {
- targetValue := resource.MustParse("15.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ObjectMetricSourceType,
- Object: &autoscalingv2.ObjectMetricSource{
- DescribedObject: autoscalingv2.CrossVersionObjectReference{
- APIVersion: "apps/v1",
- Kind: "Deployment",
- Name: "some-deployment",
- },
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- Value: &targetValue,
- },
- },
- },
- },
- reportedLevels: []uint64{20000},
- }
- tc.runTest(t)
- }
- func TestScaleUpPerPodCMObject(t *testing.T) {
- targetAverageValue := resource.MustParse("10.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ObjectMetricSourceType,
- Object: &autoscalingv2.ObjectMetricSource{
- DescribedObject: autoscalingv2.CrossVersionObjectReference{
- APIVersion: "apps/v1",
- Kind: "Deployment",
- Name: "some-deployment",
- },
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &targetAverageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{40000},
- }
- tc.runTest(t)
- }
- func TestScaleUpCMExternal(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- Value: resource.NewMilliQuantity(6666, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{8600},
- }
- tc.runTest(t)
- }
- func TestScaleUpPerPodCMExternal(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: resource.NewMilliQuantity(2222, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{8600},
- }
- tc.runTest(t)
- }
- func TestScaleDown(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 3,
- CPUTarget: 50,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{100, 300, 500, 250, 250},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownStabilizeInitialSize(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 5,
- CPUTarget: 50,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{100, 300, 500, 250, 250},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- recommendations: nil,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ScaleDownStabilized",
- }),
- }
- tc.runTest(t)
- }
- func TestScaleDownCM(t *testing.T) {
- averageValue := resource.MustParse("20.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 3,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{12000, 12000, 12000, 12000, 12000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownCMObject(t *testing.T) {
- targetValue := resource.MustParse("20.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 3,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ObjectMetricSourceType,
- Object: &autoscalingv2.ObjectMetricSource{
- DescribedObject: autoscalingv2.CrossVersionObjectReference{
- APIVersion: "apps/v1",
- Kind: "Deployment",
- Name: "some-deployment",
- },
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- Value: &targetValue,
- },
- },
- },
- },
- reportedLevels: []uint64{12000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownPerPodCMObject(t *testing.T) {
- targetAverageValue := resource.MustParse("20.0")
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 3,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ObjectMetricSourceType,
- Object: &autoscalingv2.ObjectMetricSource{
- DescribedObject: autoscalingv2.CrossVersionObjectReference{
- APIVersion: "apps/v1",
- Kind: "Deployment",
- Name: "some-deployment",
- },
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &targetAverageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{60000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownCMExternal(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 3,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- Value: resource.NewMilliQuantity(14400, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{8600},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownPerPodCMExternal(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 3,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: resource.NewMilliQuantity(3000, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{8600},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownIncludeUnreadyPods(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 2,
- CPUTarget: 50,
- CPUCurrent: 30,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{100, 300, 500, 250, 250},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownIgnoreHotCpuPods(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 2,
- CPUTarget: 50,
- CPUCurrent: 30,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{100, 300, 500, 250, 250},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- reportedPodStartTime: []metav1.Time{coolCpuCreationTime(), coolCpuCreationTime(), coolCpuCreationTime(), hotCpuCreationTime(), hotCpuCreationTime()},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownIgnoresFailedPods(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 3,
- CPUTarget: 50,
- CPUCurrent: 28,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{100, 300, 500, 250, 250},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
- reportedPodPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestScaleDownIgnoresDeletionPods(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 3,
- CPUTarget: 50,
- CPUCurrent: 28,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{100, 300, 500, 250, 250},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
- reportedPodPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
- reportedPodDeletionTimestamp: []bool{false, false, false, false, false, true, true},
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestTolerance(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 100,
- reportedLevels: []uint64{1010, 1030, 1020},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestToleranceCM(t *testing.T) {
- averageValue := resource.MustParse("20.0")
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{20000, 20001, 21000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestToleranceCMObject(t *testing.T) {
- targetValue := resource.MustParse("20.0")
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ObjectMetricSourceType,
- Object: &autoscalingv2.ObjectMetricSource{
- DescribedObject: autoscalingv2.CrossVersionObjectReference{
- APIVersion: "apps/v1",
- Kind: "Deployment",
- Name: "some-deployment",
- },
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- Value: &targetValue,
- },
- },
- },
- },
- reportedLevels: []uint64{20050},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestToleranceCMExternal(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 4,
- expectedDesiredReplicas: 4,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- Value: resource.NewMilliQuantity(8666, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{8600},
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestTolerancePerPodCMObject(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 4,
- expectedDesiredReplicas: 4,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ObjectMetricSourceType,
- Object: &autoscalingv2.ObjectMetricSource{
- DescribedObject: autoscalingv2.CrossVersionObjectReference{
- APIVersion: "apps/v1",
- Kind: "Deployment",
- Name: "some-deployment",
- },
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: resource.NewMilliQuantity(2200, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{8600},
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestTolerancePerPodCMExternal(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 4,
- expectedDesiredReplicas: 4,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: resource.NewMilliQuantity(2200, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{8600},
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestMinReplicas(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 2,
- CPUTarget: 90,
- reportedLevels: []uint64{10, 95, 10},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "TooFewReplicas",
- }),
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestMinReplicasDesiredZero(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 2,
- CPUTarget: 90,
- reportedLevels: []uint64{0, 0, 0},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "TooFewReplicas",
- }),
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestZeroReplicas(t *testing.T) {
- tc := testCase{
- minReplicas: 3,
- maxReplicas: 5,
- initialReplicas: 0,
- expectedDesiredReplicas: 0,
- CPUTarget: 90,
- reportedLevels: []uint64{},
- reportedCPURequests: []resource.Quantity{},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"},
- {Type: autoscalingv1.ScalingActive, Status: v1.ConditionFalse, Reason: "ScalingDisabled"},
- },
- }
- tc.runTest(t)
- }
- func TestTooFewReplicas(t *testing.T) {
- tc := testCase{
- minReplicas: 3,
- maxReplicas: 5,
- initialReplicas: 2,
- expectedDesiredReplicas: 3,
- CPUTarget: 90,
- reportedLevels: []uint64{},
- reportedCPURequests: []resource.Quantity{},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"},
- },
- }
- tc.runTest(t)
- }
- func TestTooManyReplicas(t *testing.T) {
- tc := testCase{
- minReplicas: 3,
- maxReplicas: 5,
- initialReplicas: 10,
- expectedDesiredReplicas: 5,
- CPUTarget: 90,
- reportedLevels: []uint64{},
- reportedCPURequests: []resource.Quantity{},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"},
- },
- }
- tc.runTest(t)
- }
- func TestMaxReplicas(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 5,
- CPUTarget: 90,
- reportedLevels: []uint64{8000, 9500, 1000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "TooManyReplicas",
- }),
- }
- tc.runTest(t)
- }
- func TestSuperfluousMetrics(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 4,
- expectedDesiredReplicas: 6,
- CPUTarget: 100,
- reportedLevels: []uint64{4000, 9500, 3000, 7000, 3200, 2000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "TooManyReplicas",
- }),
- }
- tc.runTest(t)
- }
- func TestMissingMetrics(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 4,
- expectedDesiredReplicas: 3,
- CPUTarget: 100,
- reportedLevels: []uint64{400, 95},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestEmptyMetrics(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 4,
- expectedDesiredReplicas: 4,
- CPUTarget: 100,
- reportedLevels: []uint64{},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"},
- {Type: autoscalingv1.ScalingActive, Status: v1.ConditionFalse, Reason: "FailedGetResourceMetric"},
- },
- }
- tc.runTest(t)
- }
- func TestEmptyCPURequest(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 1,
- expectedDesiredReplicas: 1,
- CPUTarget: 100,
- reportedLevels: []uint64{200},
- reportedCPURequests: []resource.Quantity{},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"},
- {Type: autoscalingv1.ScalingActive, Status: v1.ConditionFalse, Reason: "FailedGetResourceMetric"},
- },
- }
- tc.runTest(t)
- }
- func TestEventCreated(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 1,
- expectedDesiredReplicas: 2,
- CPUTarget: 50,
- reportedLevels: []uint64{200},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.2")},
- verifyEvents: true,
- useMetricsAPI: true,
- }
- tc.runTest(t)
- }
- func TestEventNotCreated(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 2,
- expectedDesiredReplicas: 2,
- CPUTarget: 50,
- reportedLevels: []uint64{200, 200},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.4"), resource.MustParse("0.4")},
- verifyEvents: true,
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc.runTest(t)
- }
- func TestMissingReports(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 4,
- expectedDesiredReplicas: 2,
- CPUTarget: 50,
- reportedLevels: []uint64{200},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.2")},
- useMetricsAPI: true,
- recommendations: []timestampedRecommendation{},
- }
- tc.runTest(t)
- }
- func TestUpscaleCap(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 100,
- initialReplicas: 3,
- expectedDesiredReplicas: 24,
- CPUTarget: 10,
- reportedLevels: []uint64{100, 200, 300},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "ScaleUpLimit",
- }),
- }
- tc.runTest(t)
- }
- func TestUpscaleCapGreaterThanMaxReplicas(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 20,
- initialReplicas: 3,
- // expectedDesiredReplicas would be 24 without maxReplicas
- expectedDesiredReplicas: 20,
- CPUTarget: 10,
- reportedLevels: []uint64{100, 200, 300},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "TooManyReplicas",
- }),
- }
- tc.runTest(t)
- }
- func TestConditionInvalidSelectorMissing(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 100,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 10,
- reportedLevels: []uint64{100, 200, 300},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {
- Type: autoscalingv1.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "SucceededGetScale",
- },
- {
- Type: autoscalingv1.ScalingActive,
- Status: v1.ConditionFalse,
- Reason: "InvalidSelector",
- },
- },
- }
- _, _, _, _, testScaleClient := tc.prepareTestClient(t)
- tc.testScaleClient = testScaleClient
- testScaleClient.PrependReactor("get", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- obj := &autoscalingv1.Scale{
- ObjectMeta: metav1.ObjectMeta{
- Name: tc.resource.name,
- },
- Spec: autoscalingv1.ScaleSpec{
- Replicas: tc.initialReplicas,
- },
- Status: autoscalingv1.ScaleStatus{
- Replicas: tc.initialReplicas,
- },
- }
- return true, obj, nil
- })
- tc.runTest(t)
- }
- func TestConditionInvalidSelectorUnparsable(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 100,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 10,
- reportedLevels: []uint64{100, 200, 300},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {
- Type: autoscalingv1.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "SucceededGetScale",
- },
- {
- Type: autoscalingv1.ScalingActive,
- Status: v1.ConditionFalse,
- Reason: "InvalidSelector",
- },
- },
- }
- _, _, _, _, testScaleClient := tc.prepareTestClient(t)
- tc.testScaleClient = testScaleClient
- testScaleClient.PrependReactor("get", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- obj := &autoscalingv1.Scale{
- ObjectMeta: metav1.ObjectMeta{
- Name: tc.resource.name,
- },
- Spec: autoscalingv1.ScaleSpec{
- Replicas: tc.initialReplicas,
- },
- Status: autoscalingv1.ScaleStatus{
- Replicas: tc.initialReplicas,
- Selector: "cheddar cheese",
- },
- }
- return true, obj, nil
- })
- tc.runTest(t)
- }
- func TestConditionFailedGetMetrics(t *testing.T) {
- targetValue := resource.MustParse("15.0")
- averageValue := resource.MustParse("15.0")
- metricsTargets := map[string][]autoscalingv2.MetricSpec{
- "FailedGetResourceMetric": nil,
- "FailedGetPodsMetric": {
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- "FailedGetObjectMetric": {
- {
- Type: autoscalingv2.ObjectMetricSourceType,
- Object: &autoscalingv2.ObjectMetricSource{
- DescribedObject: autoscalingv2.CrossVersionObjectReference{
- APIVersion: "apps/v1",
- Kind: "Deployment",
- Name: "some-deployment",
- },
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- Value: &targetValue,
- },
- },
- },
- },
- "FailedGetExternalMetric": {
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- Value: resource.NewMilliQuantity(300, resource.DecimalSI),
- },
- },
- },
- },
- }
- for reason, specs := range metricsTargets {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 100,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 10,
- reportedLevels: []uint64{100, 200, 300},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- }
- _, testMetricsClient, testCMClient, testEMClient, _ := tc.prepareTestClient(t)
- tc.testMetricsClient = testMetricsClient
- tc.testCMClient = testCMClient
- tc.testEMClient = testEMClient
- testMetricsClient.PrependReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- return true, &metricsapi.PodMetricsList{}, fmt.Errorf("something went wrong")
- })
- testCMClient.PrependReactor("get", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- return true, &cmapi.MetricValueList{}, fmt.Errorf("something went wrong")
- })
- testEMClient.PrependReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- return true, &emapi.ExternalMetricValueList{}, fmt.Errorf("something went wrong")
- })
- tc.expectedConditions = []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"},
- {Type: autoscalingv1.ScalingActive, Status: v1.ConditionFalse, Reason: reason},
- }
- if specs != nil {
- tc.CPUTarget = 0
- } else {
- tc.CPUTarget = 10
- }
- tc.metricsTarget = specs
- tc.runTest(t)
- }
- }
- func TestConditionInvalidSourceType(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: "CheddarCheese",
- },
- },
- reportedLevels: []uint64{20000},
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {
- Type: autoscalingv1.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "SucceededGetScale",
- },
- {
- Type: autoscalingv1.ScalingActive,
- Status: v1.ConditionFalse,
- Reason: "InvalidMetricSourceType",
- },
- },
- }
- tc.runTest(t)
- }
- func TestConditionFailedGetScale(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 100,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 10,
- reportedLevels: []uint64{100, 200, 300},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {
- Type: autoscalingv1.AbleToScale,
- Status: v1.ConditionFalse,
- Reason: "FailedGetScale",
- },
- },
- }
- _, _, _, _, testScaleClient := tc.prepareTestClient(t)
- tc.testScaleClient = testScaleClient
- testScaleClient.PrependReactor("get", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- return true, &autoscalingv1.Scale{}, fmt.Errorf("something went wrong")
- })
- tc.runTest(t)
- }
- func TestConditionFailedUpdateScale(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 100,
- reportedLevels: []uint64{150, 150, 150},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionFalse,
- Reason: "FailedUpdateScale",
- }),
- }
- _, _, _, _, testScaleClient := tc.prepareTestClient(t)
- tc.testScaleClient = testScaleClient
- testScaleClient.PrependReactor("update", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- return true, &autoscalingv1.Scale{}, fmt.Errorf("something went wrong")
- })
- tc.runTest(t)
- }
- func NoTestBackoffUpscale(t *testing.T) {
- time := metav1.Time{Time: time.Now()}
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 3,
- CPUTarget: 100,
- reportedLevels: []uint64{150, 150, 150},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- lastScaleTime: &time,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "SucceededRescale",
- }),
- }
- tc.runTest(t)
- }
- func TestNoBackoffUpscaleCM(t *testing.T) {
- averageValue := resource.MustParse("15.0")
- time := metav1.Time{Time: time.Now()}
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- CPUTarget: 0,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{20000, 10000, 30000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- //useMetricsAPI: true,
- lastScaleTime: &time,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "SucceededRescale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionFalse,
- Reason: "DesiredWithinRange",
- }),
- }
- tc.runTest(t)
- }
- func TestNoBackoffUpscaleCMNoBackoffCpu(t *testing.T) {
- averageValue := resource.MustParse("15.0")
- time := metav1.Time{Time: time.Now()}
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 3,
- expectedDesiredReplicas: 5,
- CPUTarget: 10,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.PodsMetricSourceType,
- Pods: &autoscalingv2.PodsMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- },
- Target: autoscalingv2.MetricTarget{
- AverageValue: &averageValue,
- },
- },
- },
- },
- reportedLevels: []uint64{20000, 10000, 30000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- lastScaleTime: &time,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "SucceededRescale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- Reason: "TooManyReplicas",
- }),
- }
- tc.runTest(t)
- }
- func TestStabilizeDownscale(t *testing.T) {
- tc := testCase{
- minReplicas: 1,
- maxReplicas: 5,
- initialReplicas: 4,
- expectedDesiredReplicas: 4,
- CPUTarget: 100,
- reportedLevels: []uint64{50, 50, 50},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")},
- useMetricsAPI: true,
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }, autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ScaleDownStabilized",
- }),
- recommendations: []timestampedRecommendation{
- {10, time.Now().Add(-10 * time.Minute)},
- {4, time.Now().Add(-1 * time.Minute)},
- },
- }
- tc.runTest(t)
- }
- // TestComputedToleranceAlgImplementation is a regression test which
- // back-calculates a minimal percentage for downscaling based on a small percentage
- // increase in pod utilization which is calibrated against the tolerance value.
- func TestComputedToleranceAlgImplementation(t *testing.T) {
- startPods := int32(10)
- // 150 mCPU per pod.
- totalUsedCPUOfAllPods := uint64(startPods * 150)
- // Each pod starts out asking for 2X what is really needed.
- // This means we will have a 50% ratio of used/requested
- totalRequestedCPUOfAllPods := int32(2 * totalUsedCPUOfAllPods)
- requestedToUsed := float64(totalRequestedCPUOfAllPods / int32(totalUsedCPUOfAllPods))
- // Spread the amount we ask over 10 pods. We can add some jitter later in reportedLevels.
- perPodRequested := totalRequestedCPUOfAllPods / startPods
- // Force a minimal scaling event by satisfying (tolerance < 1 - resourcesUsedRatio).
- target := math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .01
- finalCPUPercentTarget := int32(target * 100)
- resourcesUsedRatio := float64(totalUsedCPUOfAllPods) / float64(float64(totalRequestedCPUOfAllPods)*target)
- // i.e. .60 * 20 -> scaled down expectation.
- finalPods := int32(math.Ceil(resourcesUsedRatio * float64(startPods)))
- // To breach tolerance we will create a utilization ratio difference of tolerance to usageRatioToleranceValue)
- tc1 := testCase{
- minReplicas: 0,
- maxReplicas: 1000,
- initialReplicas: startPods,
- expectedDesiredReplicas: finalPods,
- CPUTarget: finalCPUPercentTarget,
- reportedLevels: []uint64{
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- },
- reportedCPURequests: []resource.Quantity{
- resource.MustParse(fmt.Sprint(perPodRequested+100) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested-100) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested+10) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested-10) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested+2) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested-2) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested+1) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested-1) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
- },
- useMetricsAPI: true,
- recommendations: []timestampedRecommendation{},
- }
- tc1.runTest(t)
- target = math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .004
- finalCPUPercentTarget = int32(target * 100)
- tc2 := testCase{
- minReplicas: 0,
- maxReplicas: 1000,
- initialReplicas: startPods,
- expectedDesiredReplicas: startPods,
- CPUTarget: finalCPUPercentTarget,
- reportedLevels: []uint64{
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- totalUsedCPUOfAllPods / 10,
- },
- reportedCPURequests: []resource.Quantity{
- resource.MustParse(fmt.Sprint(perPodRequested+100) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested-100) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested+10) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested-10) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested+2) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested-2) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested+1) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested-1) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
- resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
- },
- useMetricsAPI: true,
- recommendations: []timestampedRecommendation{},
- expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- Reason: "ReadyForNewScale",
- }),
- }
- tc2.runTest(t)
- }
- func TestScaleUpRCImmediately(t *testing.T) {
- time := metav1.Time{Time: time.Now()}
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 1,
- expectedDesiredReplicas: 2,
- verifyCPUCurrent: false,
- reportedLevels: []uint64{0, 0, 0, 0},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- lastScaleTime: &time,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"},
- },
- }
- tc.runTest(t)
- }
- func TestScaleDownRCImmediately(t *testing.T) {
- time := metav1.Time{Time: time.Now()}
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 5,
- initialReplicas: 6,
- expectedDesiredReplicas: 5,
- CPUTarget: 50,
- reportedLevels: []uint64{8000, 9500, 1000},
- reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
- useMetricsAPI: true,
- lastScaleTime: &time,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"},
- },
- }
- tc.runTest(t)
- }
- func TestAvoidUncessaryUpdates(t *testing.T) {
- now := metav1.Time{Time: time.Now().Add(-time.Hour)}
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 2,
- expectedDesiredReplicas: 2,
- CPUTarget: 30,
- CPUCurrent: 40,
- verifyCPUCurrent: true,
- reportedLevels: []uint64{400, 500, 700},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- reportedPodStartTime: []metav1.Time{coolCpuCreationTime(), hotCpuCreationTime(), hotCpuCreationTime()},
- useMetricsAPI: true,
- lastScaleTime: &now,
- recommendations: []timestampedRecommendation{},
- }
- testClient, _, _, _, _ := tc.prepareTestClient(t)
- tc.testClient = testClient
- testClient.PrependReactor("list", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- tc.Lock()
- defer tc.Unlock()
- // fake out the verification logic and mark that we're done processing
- go func() {
- // wait a tick and then mark that we're finished (otherwise, we have no
- // way to indicate that we're finished, because the function decides not to do anything)
- time.Sleep(1 * time.Second)
- tc.Lock()
- tc.statusUpdated = true
- tc.Unlock()
- tc.processed <- "test-hpa"
- }()
- quantity := resource.MustParse("400m")
- obj := &autoscalingv2.HorizontalPodAutoscalerList{
- Items: []autoscalingv2.HorizontalPodAutoscaler{
- {
- ObjectMeta: metav1.ObjectMeta{
- Name: "test-hpa",
- Namespace: "test-namespace",
- SelfLink: "experimental/v1/namespaces/test-namespace/horizontalpodautoscalers/test-hpa",
- },
- Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
- ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
- Kind: "ReplicationController",
- Name: "test-rc",
- APIVersion: "v1",
- },
- MinReplicas: &tc.minReplicas,
- MaxReplicas: tc.maxReplicas,
- },
- Status: autoscalingv2.HorizontalPodAutoscalerStatus{
- CurrentReplicas: tc.initialReplicas,
- DesiredReplicas: tc.initialReplicas,
- LastScaleTime: tc.lastScaleTime,
- CurrentMetrics: []autoscalingv2.MetricStatus{
- {
- Type: autoscalingv2.ResourceMetricSourceType,
- Resource: &autoscalingv2.ResourceMetricStatus{
- Name: v1.ResourceCPU,
- Current: autoscalingv2.MetricValueStatus{
- AverageValue: &quantity,
- AverageUtilization: &tc.CPUCurrent,
- },
- },
- },
- },
- Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{
- {
- Type: autoscalingv2.AbleToScale,
- Status: v1.ConditionTrue,
- LastTransitionTime: *tc.lastScaleTime,
- Reason: "ReadyForNewScale",
- Message: "recommended size matches current size",
- },
- {
- Type: autoscalingv2.ScalingActive,
- Status: v1.ConditionTrue,
- LastTransitionTime: *tc.lastScaleTime,
- Reason: "ValidMetricFound",
- Message: "the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)",
- },
- {
- Type: autoscalingv2.ScalingLimited,
- Status: v1.ConditionTrue,
- LastTransitionTime: *tc.lastScaleTime,
- Reason: "TooFewReplicas",
- Message: "the desired replica count is more than the maximum replica count",
- },
- },
- },
- },
- },
- }
- // and... convert to autoscaling v1 to return the right type
- objv1, err := unsafeConvertToVersionVia(obj, autoscalingv1.SchemeGroupVersion)
- if err != nil {
- return true, nil, err
- }
- return true, objv1, nil
- })
- testClient.PrependReactor("update", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- assert.Fail(t, "should not have attempted to update the HPA when nothing changed")
- // mark that we've processed this HPA
- tc.processed <- ""
- return true, nil, fmt.Errorf("unexpected call")
- })
- controller, informerFactory := tc.setupController(t)
- tc.runTestWithController(t, controller, informerFactory)
- }
- func TestConvertDesiredReplicasWithRules(t *testing.T) {
- conversionTestCases := []struct {
- currentReplicas int32
- expectedDesiredReplicas int32
- hpaMinReplicas int32
- hpaMaxReplicas int32
- expectedConvertedDesiredReplicas int32
- expectedCondition string
- annotation string
- }{
- {
- currentReplicas: 5,
- expectedDesiredReplicas: 7,
- hpaMinReplicas: 3,
- hpaMaxReplicas: 8,
- expectedConvertedDesiredReplicas: 7,
- expectedCondition: "DesiredWithinRange",
- annotation: "prenormalized desired replicas within range",
- },
- {
- currentReplicas: 3,
- expectedDesiredReplicas: 1,
- hpaMinReplicas: 2,
- hpaMaxReplicas: 8,
- expectedConvertedDesiredReplicas: 2,
- expectedCondition: "TooFewReplicas",
- annotation: "prenormalized desired replicas < minReplicas",
- },
- {
- currentReplicas: 1,
- expectedDesiredReplicas: 0,
- hpaMinReplicas: 0,
- hpaMaxReplicas: 10,
- expectedConvertedDesiredReplicas: 1,
- expectedCondition: "TooFewReplicas",
- annotation: "1 is minLimit because hpaMinReplicas < 1",
- },
- {
- currentReplicas: 20,
- expectedDesiredReplicas: 1000,
- hpaMinReplicas: 1,
- hpaMaxReplicas: 10,
- expectedConvertedDesiredReplicas: 10,
- expectedCondition: "TooManyReplicas",
- annotation: "maxReplicas is the limit because maxReplicas < scaleUpLimit",
- },
- {
- currentReplicas: 3,
- expectedDesiredReplicas: 1000,
- hpaMinReplicas: 1,
- hpaMaxReplicas: 2000,
- expectedConvertedDesiredReplicas: calculateScaleUpLimit(3),
- expectedCondition: "ScaleUpLimit",
- annotation: "scaleUpLimit is the limit because scaleUpLimit < maxReplicas",
- },
- }
- for _, ctc := range conversionTestCases {
- actualConvertedDesiredReplicas, actualCondition, _ := convertDesiredReplicasWithRules(
- ctc.currentReplicas, ctc.expectedDesiredReplicas, ctc.hpaMinReplicas, ctc.hpaMaxReplicas,
- )
- assert.Equal(t, ctc.expectedConvertedDesiredReplicas, actualConvertedDesiredReplicas, ctc.annotation)
- assert.Equal(t, ctc.expectedCondition, actualCondition, ctc.annotation)
- }
- }
- func TestNormalizeDesiredReplicas(t *testing.T) {
- tests := []struct {
- name string
- key string
- recommendations []timestampedRecommendation
- prenormalizedDesiredReplicas int32
- expectedStabilizedReplicas int32
- expectedLogLength int
- }{
- {
- "empty log",
- "",
- []timestampedRecommendation{},
- 5,
- 5,
- 1,
- },
- {
- "stabilize",
- "",
- []timestampedRecommendation{
- {4, time.Now().Add(-2 * time.Minute)},
- {5, time.Now().Add(-1 * time.Minute)},
- },
- 3,
- 5,
- 3,
- },
- {
- "no stabilize",
- "",
- []timestampedRecommendation{
- {1, time.Now().Add(-2 * time.Minute)},
- {2, time.Now().Add(-1 * time.Minute)},
- },
- 3,
- 3,
- 3,
- },
- {
- "no stabilize - old recommendations",
- "",
- []timestampedRecommendation{
- {10, time.Now().Add(-10 * time.Minute)},
- {9, time.Now().Add(-9 * time.Minute)},
- },
- 3,
- 3,
- 2,
- },
- {
- "stabilize - old recommendations",
- "",
- []timestampedRecommendation{
- {10, time.Now().Add(-10 * time.Minute)},
- {4, time.Now().Add(-1 * time.Minute)},
- {5, time.Now().Add(-2 * time.Minute)},
- {9, time.Now().Add(-9 * time.Minute)},
- },
- 3,
- 5,
- 4,
- },
- }
- for _, tc := range tests {
- hc := HorizontalController{
- downscaleStabilisationWindow: 5 * time.Minute,
- recommendations: map[string][]timestampedRecommendation{
- tc.key: tc.recommendations,
- },
- }
- r := hc.stabilizeRecommendation(tc.key, tc.prenormalizedDesiredReplicas)
- if r != tc.expectedStabilizedReplicas {
- t.Errorf("[%s] got %d stabilized replicas, expected %d", tc.name, r, tc.expectedStabilizedReplicas)
- }
- if len(hc.recommendations[tc.key]) != tc.expectedLogLength {
- t.Errorf("[%s] after stabilization recommendations log has %d entries, expected %d", tc.name, len(hc.recommendations[tc.key]), tc.expectedLogLength)
- }
- }
- }
- func TestScaleUpOneMetricEmpty(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 3,
- expectedDesiredReplicas: 4,
- CPUTarget: 30,
- verifyCPUCurrent: true,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- Value: resource.NewMilliQuantity(100, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{300, 400, 500},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- }
- _, _, _, testEMClient, _ := tc.prepareTestClient(t)
- testEMClient.PrependReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- return true, &emapi.ExternalMetricValueList{}, fmt.Errorf("something went wrong")
- })
- tc.testEMClient = testEMClient
- tc.runTest(t)
- }
- func TestNoScaleDownOneMetricInvalid(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 5,
- CPUTarget: 50,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: "CheddarCheese",
- },
- },
- reportedLevels: []uint64{100, 300, 500, 250, 250},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"},
- {Type: autoscalingv1.ScalingActive, Status: v1.ConditionFalse, Reason: "InvalidMetricSourceType"},
- },
- }
- tc.runTest(t)
- }
- func TestNoScaleDownOneMetricEmpty(t *testing.T) {
- tc := testCase{
- minReplicas: 2,
- maxReplicas: 6,
- initialReplicas: 5,
- expectedDesiredReplicas: 5,
- CPUTarget: 50,
- metricsTarget: []autoscalingv2.MetricSpec{
- {
- Type: autoscalingv2.ExternalMetricSourceType,
- External: &autoscalingv2.ExternalMetricSource{
- Metric: autoscalingv2.MetricIdentifier{
- Name: "qps",
- Selector: &metav1.LabelSelector{},
- },
- Target: autoscalingv2.MetricTarget{
- Value: resource.NewMilliQuantity(1000, resource.DecimalSI),
- },
- },
- },
- },
- reportedLevels: []uint64{100, 300, 500, 250, 250},
- reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
- useMetricsAPI: true,
- expectedConditions: []autoscalingv1.HorizontalPodAutoscalerCondition{
- {Type: autoscalingv1.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"},
- {Type: autoscalingv1.ScalingActive, Status: v1.ConditionFalse, Reason: "FailedGetExternalMetric"},
- },
- }
- _, _, _, testEMClient, _ := tc.prepareTestClient(t)
- testEMClient.PrependReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
- return true, &emapi.ExternalMetricValueList{}, fmt.Errorf("something went wrong")
- })
- tc.testEMClient = testEMClient
- tc.runTest(t)
- }
- // TODO: add more tests
|