1
16
17 package podautoscaler
18
19 import (
20 "context"
21 "fmt"
22 "math"
23 "testing"
24 "time"
25
26 autoscalingv2 "k8s.io/api/autoscaling/v2"
27 v1 "k8s.io/api/core/v1"
28 "k8s.io/apimachinery/pkg/api/meta/testrestmapper"
29 "k8s.io/apimachinery/pkg/api/resource"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/runtime"
32 "k8s.io/apimachinery/pkg/runtime/schema"
33 "k8s.io/apimachinery/pkg/util/sets"
34 "k8s.io/client-go/informers"
35 "k8s.io/client-go/kubernetes/fake"
36 core "k8s.io/client-go/testing"
37 "k8s.io/client-go/tools/cache"
38 "k8s.io/kubernetes/pkg/api/legacyscheme"
39 "k8s.io/kubernetes/pkg/controller"
40 metricsclient "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
41 cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
42 emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
43 metricsapi "k8s.io/metrics/pkg/apis/metrics/v1beta1"
44 metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake"
45 cmfake "k8s.io/metrics/pkg/client/custom_metrics/fake"
46 emfake "k8s.io/metrics/pkg/client/external_metrics/fake"
47
48 "github.com/stretchr/testify/assert"
49 "github.com/stretchr/testify/require"
50 )
51
52 type resourceInfo struct {
53 name v1.ResourceName
54 requests []resource.Quantity
55 levels [][]int64
56
57 podNames []string
58
59 targetUtilization int32
60 expectedUtilization int32
61 expectedValue int64
62 }
63
64 type metricType int
65
66 const (
67 objectMetric metricType = iota
68 objectPerPodMetric
69 externalMetric
70 externalPerPodMetric
71 podMetric
72 )
73
74 type metricInfo struct {
75 name string
76 levels []int64
77 singleObject *autoscalingv2.CrossVersionObjectReference
78 selector *metav1.LabelSelector
79 metricType metricType
80
81 targetUsage int64
82 perPodTargetUsage int64
83 expectedUsage int64
84 }
85
86 type replicaCalcTestCase struct {
87 currentReplicas int32
88 expectedReplicas int32
89 expectedError error
90
91 timestamp time.Time
92
93 resource *resourceInfo
94 metric *metricInfo
95 container string
96
97 podReadiness []v1.ConditionStatus
98 podStartTime []metav1.Time
99 podPhase []v1.PodPhase
100 podDeletionTimestamp []bool
101 }
102
103 const (
104 testNamespace = "test-namespace"
105 podNamePrefix = "test-pod"
106 numContainersPerPod = 2
107 )
108
109 func (tc *replicaCalcTestCase) prepareTestClientSet() *fake.Clientset {
110 fakeClient := &fake.Clientset{}
111 fakeClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
112 obj := &v1.PodList{}
113 podsCount := int(tc.currentReplicas)
114
115 if tc.podPhase != nil && len(tc.podPhase) > podsCount {
116 podsCount = len(tc.podPhase)
117 }
118 for i := 0; i < podsCount; i++ {
119 podReadiness := v1.ConditionTrue
120 if tc.podReadiness != nil && i < len(tc.podReadiness) {
121 podReadiness = tc.podReadiness[i]
122 }
123 var podStartTime metav1.Time
124 if tc.podStartTime != nil {
125 podStartTime = tc.podStartTime[i]
126 }
127 podPhase := v1.PodRunning
128 if tc.podPhase != nil {
129 podPhase = tc.podPhase[i]
130 }
131 podDeletionTimestamp := false
132 if tc.podDeletionTimestamp != nil {
133 podDeletionTimestamp = tc.podDeletionTimestamp[i]
134 }
135 podName := fmt.Sprintf("%s-%d", podNamePrefix, i)
136 pod := v1.Pod{
137 Status: v1.PodStatus{
138 Phase: podPhase,
139 StartTime: &podStartTime,
140 Conditions: []v1.PodCondition{
141 {
142 Type: v1.PodReady,
143 Status: podReadiness,
144 },
145 },
146 },
147 ObjectMeta: metav1.ObjectMeta{
148 Name: podName,
149 Namespace: testNamespace,
150 Labels: map[string]string{
151 "name": podNamePrefix,
152 },
153 },
154 Spec: v1.PodSpec{
155 Containers: []v1.Container{{Name: "container1"}, {Name: "container2"}},
156 },
157 }
158 if podDeletionTimestamp {
159 pod.DeletionTimestamp = &metav1.Time{Time: time.Now()}
160 }
161
162 if tc.resource != nil && i < len(tc.resource.requests) {
163 pod.Spec.Containers[0].Resources = v1.ResourceRequirements{
164 Requests: v1.ResourceList{
165 tc.resource.name: tc.resource.requests[i],
166 },
167 }
168 pod.Spec.Containers[1].Resources = v1.ResourceRequirements{
169 Requests: v1.ResourceList{
170 tc.resource.name: tc.resource.requests[i],
171 },
172 }
173 }
174 obj.Items = append(obj.Items, pod)
175 }
176 return true, obj, nil
177 })
178 return fakeClient
179 }
180
181 func (tc *replicaCalcTestCase) prepareTestMetricsClient() *metricsfake.Clientset {
182 fakeMetricsClient := &metricsfake.Clientset{}
183
184 fakeMetricsClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) {
185 if tc.resource != nil {
186 metrics := &metricsapi.PodMetricsList{}
187 for i, resValue := range tc.resource.levels {
188 podName := fmt.Sprintf("%s-%d", podNamePrefix, i)
189 if len(tc.resource.podNames) > i {
190 podName = tc.resource.podNames[i]
191 }
192
193
194 podMetric := metricsapi.PodMetrics{
195 ObjectMeta: metav1.ObjectMeta{
196 Name: podName,
197 Namespace: testNamespace,
198 Labels: map[string]string{"name": podNamePrefix},
199 },
200 Timestamp: metav1.Time{Time: tc.timestamp},
201 Window: metav1.Duration{Duration: time.Minute},
202 Containers: make([]metricsapi.ContainerMetrics, numContainersPerPod),
203 }
204
205 for i, m := range resValue {
206 podMetric.Containers[i] = metricsapi.ContainerMetrics{
207 Name: fmt.Sprintf("container%v", i+1),
208 Usage: v1.ResourceList{
209 tc.resource.name: *resource.NewMilliQuantity(m, resource.DecimalSI),
210 },
211 }
212 }
213 metrics.Items = append(metrics.Items, podMetric)
214 }
215 return true, metrics, nil
216 }
217
218 return true, nil, fmt.Errorf("no pod resource metrics specified in test client")
219 })
220 return fakeMetricsClient
221 }
222
223 func (tc *replicaCalcTestCase) prepareTestCMClient(t *testing.T) *cmfake.FakeCustomMetricsClient {
224 fakeCMClient := &cmfake.FakeCustomMetricsClient{}
225 fakeCMClient.AddReactor("get", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
226 getForAction, wasGetFor := action.(cmfake.GetForAction)
227 if !wasGetFor {
228 return true, nil, fmt.Errorf("expected a get-for action, got %v instead", action)
229 }
230
231 if tc.metric == nil {
232 return true, nil, fmt.Errorf("no custom metrics specified in test client")
233 }
234
235 assert.Equal(t, tc.metric.name, getForAction.GetMetricName(), "the metric requested should have matched the one specified")
236
237 if getForAction.GetName() == "*" {
238 metrics := cmapi.MetricValueList{}
239
240
241 assert.Equal(t, "pods", getForAction.GetResource().Resource, "the type of object that we requested multiple metrics for should have been pods")
242
243 for i, level := range tc.metric.levels {
244 podMetric := cmapi.MetricValue{
245 DescribedObject: v1.ObjectReference{
246 Kind: "Pod",
247 Name: fmt.Sprintf("%s-%d", podNamePrefix, i),
248 Namespace: testNamespace,
249 },
250 Timestamp: metav1.Time{Time: tc.timestamp},
251 Metric: cmapi.MetricIdentifier{
252 Name: tc.metric.name,
253 },
254 Value: *resource.NewMilliQuantity(level, resource.DecimalSI),
255 }
256 metrics.Items = append(metrics.Items, podMetric)
257 }
258
259 return true, &metrics, nil
260 }
261 name := getForAction.GetName()
262 mapper := testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme)
263 metrics := &cmapi.MetricValueList{}
264 assert.NotNil(t, tc.metric.singleObject, "should have only requested a single-object metric when calling GetObjectMetricReplicas")
265 gk := schema.FromAPIVersionAndKind(tc.metric.singleObject.APIVersion, tc.metric.singleObject.Kind).GroupKind()
266 mapping, err := mapper.RESTMapping(gk)
267 if err != nil {
268 return true, nil, fmt.Errorf("unable to get mapping for %s: %v", gk.String(), err)
269 }
270 groupResource := mapping.Resource.GroupResource()
271
272 assert.Equal(t, groupResource.String(), getForAction.GetResource().Resource, "should have requested metrics for the resource matching the GroupKind passed in")
273 assert.Equal(t, tc.metric.singleObject.Name, name, "should have requested metrics for the object matching the name passed in")
274
275 metrics.Items = []cmapi.MetricValue{
276 {
277 DescribedObject: v1.ObjectReference{
278 Kind: tc.metric.singleObject.Kind,
279 APIVersion: tc.metric.singleObject.APIVersion,
280 Name: name,
281 },
282 Timestamp: metav1.Time{Time: tc.timestamp},
283 Metric: cmapi.MetricIdentifier{
284 Name: tc.metric.name,
285 },
286 Value: *resource.NewMilliQuantity(int64(tc.metric.levels[0]), resource.DecimalSI),
287 },
288 }
289
290 return true, metrics, nil
291 })
292 return fakeCMClient
293 }
294
295 func (tc *replicaCalcTestCase) prepareTestEMClient(t *testing.T) *emfake.FakeExternalMetricsClient {
296 fakeEMClient := &emfake.FakeExternalMetricsClient{}
297 fakeEMClient.AddReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
298 listAction, wasList := action.(core.ListAction)
299 if !wasList {
300 return true, nil, fmt.Errorf("expected a list-for action, got %v instead", action)
301 }
302
303 if tc.metric == nil {
304 return true, nil, fmt.Errorf("no external metrics specified in test client")
305 }
306
307 assert.Equal(t, tc.metric.name, listAction.GetResource().Resource, "the metric requested should have matched the one specified")
308
309 selector, err := metav1.LabelSelectorAsSelector(tc.metric.selector)
310 if err != nil {
311 return true, nil, fmt.Errorf("failed to convert label selector specified in test client")
312 }
313 assert.Equal(t, selector, listAction.GetListRestrictions().Labels, "the metric selector should have matched the one specified")
314
315 metrics := emapi.ExternalMetricValueList{}
316
317 for _, level := range tc.metric.levels {
318 metric := emapi.ExternalMetricValue{
319 Timestamp: metav1.Time{Time: tc.timestamp},
320 MetricName: tc.metric.name,
321 Value: *resource.NewMilliQuantity(level, resource.DecimalSI),
322 }
323 metrics.Items = append(metrics.Items, metric)
324 }
325
326 return true, &metrics, nil
327 })
328 return fakeEMClient
329 }
330
331 func (tc *replicaCalcTestCase) prepareTestClient(t *testing.T) (*fake.Clientset, *metricsfake.Clientset, *cmfake.FakeCustomMetricsClient, *emfake.FakeExternalMetricsClient) {
332 fakeClient := tc.prepareTestClientSet()
333 fakeMetricsClient := tc.prepareTestMetricsClient()
334 fakeCMClient := tc.prepareTestCMClient(t)
335 fakeEMClient := tc.prepareTestEMClient(t)
336 return fakeClient, fakeMetricsClient, fakeCMClient, fakeEMClient
337 }
338
339 func (tc *replicaCalcTestCase) runTest(t *testing.T) {
340 testClient, testMetricsClient, testCMClient, testEMClient := tc.prepareTestClient(t)
341 metricsClient := metricsclient.NewRESTMetricsClient(testMetricsClient.MetricsV1beta1(), testCMClient, testEMClient)
342
343 informerFactory := informers.NewSharedInformerFactory(testClient, controller.NoResyncPeriodFunc())
344 informer := informerFactory.Core().V1().Pods()
345
346 replicaCalc := NewReplicaCalculator(metricsClient, informer.Lister(), defaultTestingTolerance, defaultTestingCPUInitializationPeriod, defaultTestingDelayOfInitialReadinessStatus)
347
348 stop := make(chan struct{})
349 defer close(stop)
350 informerFactory.Start(stop)
351 if !cache.WaitForNamedCacheSync("HPA", stop, informer.Informer().HasSynced) {
352 return
353 }
354
355 selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
356 MatchLabels: map[string]string{"name": podNamePrefix},
357 })
358 if err != nil {
359 require.Nil(t, err, "something went horribly wrong...")
360 }
361
362 if tc.resource != nil {
363 outReplicas, outUtilization, outRawValue, outTimestamp, err := replicaCalc.GetResourceReplicas(context.TODO(), tc.currentReplicas, tc.resource.targetUtilization, tc.resource.name, testNamespace, selector, tc.container)
364
365 if tc.expectedError != nil {
366 require.Error(t, err, "there should be an error calculating the replica count")
367 assert.Contains(t, err.Error(), tc.expectedError.Error(), "the error message should have contained the expected error message")
368 return
369 }
370 require.NoError(t, err, "there should not have been an error calculating the replica count")
371 assert.Equal(t, tc.expectedReplicas, outReplicas, "replicas should be as expected")
372 assert.Equal(t, tc.resource.expectedUtilization, outUtilization, "utilization should be as expected")
373 assert.Equal(t, tc.resource.expectedValue, outRawValue, "raw value should be as expected")
374 assert.True(t, tc.timestamp.Equal(outTimestamp), "timestamp should be as expected")
375 return
376 }
377
378 var outReplicas int32
379 var outUsage int64
380 var outTimestamp time.Time
381 switch tc.metric.metricType {
382 case objectMetric:
383 if tc.metric.singleObject == nil {
384 t.Fatal("Metric specified as objectMetric but metric.singleObject is nil.")
385 }
386 outReplicas, outUsage, outTimestamp, err = replicaCalc.GetObjectMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, testNamespace, tc.metric.singleObject, selector, nil)
387 case objectPerPodMetric:
388 if tc.metric.singleObject == nil {
389 t.Fatal("Metric specified as objectMetric but metric.singleObject is nil.")
390 }
391 outReplicas, outUsage, outTimestamp, err = replicaCalc.GetObjectPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUsage, tc.metric.name, testNamespace, tc.metric.singleObject, nil)
392 case externalMetric:
393 if tc.metric.selector == nil {
394 t.Fatal("Metric specified as externalMetric but metric.selector is nil.")
395 }
396 if tc.metric.targetUsage <= 0 {
397 t.Fatalf("Metric specified as externalMetric but metric.targetUsage is %d which is <=0.", tc.metric.targetUsage)
398 }
399 outReplicas, outUsage, outTimestamp, err = replicaCalc.GetExternalMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, testNamespace, tc.metric.selector, selector)
400 case externalPerPodMetric:
401 if tc.metric.selector == nil {
402 t.Fatal("Metric specified as externalPerPodMetric but metric.selector is nil.")
403 }
404 if tc.metric.perPodTargetUsage <= 0 {
405 t.Fatalf("Metric specified as externalPerPodMetric but metric.perPodTargetUsage is %d which is <=0.", tc.metric.perPodTargetUsage)
406 }
407
408 outReplicas, outUsage, outTimestamp, err = replicaCalc.GetExternalPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUsage, tc.metric.name, testNamespace, tc.metric.selector)
409 case podMetric:
410 outReplicas, outUsage, outTimestamp, err = replicaCalc.GetMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, testNamespace, selector, nil)
411 default:
412 t.Fatalf("Unknown metric type: %d", tc.metric.metricType)
413 }
414
415 if tc.expectedError != nil {
416 require.Error(t, err, "there should be an error calculating the replica count")
417 assert.Contains(t, err.Error(), tc.expectedError.Error(), "the error message should have contained the expected error message")
418 return
419 }
420 require.NoError(t, err, "there should not have been an error calculating the replica count")
421 assert.Equal(t, tc.expectedReplicas, outReplicas, "replicas should be as expected")
422 assert.Equal(t, tc.metric.expectedUsage, outUsage, "usage should be as expected")
423 assert.True(t, tc.timestamp.Equal(outTimestamp), "timestamp should be as expected")
424 }
425 func makePodMetricLevels(containerMetric ...int64) [][]int64 {
426 metrics := make([][]int64, len(containerMetric))
427 for i := 0; i < len(containerMetric); i++ {
428 metrics[i] = make([]int64, numContainersPerPod)
429 for j := 0; j < numContainersPerPod; j++ {
430 metrics[i][j] = containerMetric[i]
431 }
432 }
433 return metrics
434 }
435 func TestReplicaCalcDisjointResourcesMetrics(t *testing.T) {
436 tc := replicaCalcTestCase{
437 currentReplicas: 1,
438 expectedError: fmt.Errorf("no metrics returned matched known pods"),
439 resource: &resourceInfo{
440 name: v1.ResourceCPU,
441 requests: []resource.Quantity{resource.MustParse("1.0")},
442 levels: makePodMetricLevels(100),
443 podNames: []string{"an-older-pod-name"},
444
445 targetUtilization: 100,
446 },
447 }
448 tc.runTest(t)
449 }
450
451 func TestReplicaCalcMissingContainerMetricError(t *testing.T) {
452 tc := replicaCalcTestCase{
453 currentReplicas: 1,
454 expectedError: fmt.Errorf("container container2 not present in metrics for pod test-namespace/test-pod-0"),
455 resource: &resourceInfo{
456 name: v1.ResourceCPU,
457 requests: []resource.Quantity{resource.MustParse("1.0")},
458 levels: [][]int64{{0}},
459 },
460 container: "container2",
461 }
462 tc.runTest(t)
463 }
464
465 func TestReplicaCalcScaleUp(t *testing.T) {
466 tc := replicaCalcTestCase{
467 currentReplicas: 3,
468 expectedReplicas: 5,
469 resource: &resourceInfo{
470 name: v1.ResourceCPU,
471 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
472 levels: makePodMetricLevels(300, 500, 700),
473
474 targetUtilization: 30,
475 expectedUtilization: 50,
476 expectedValue: numContainersPerPod * 500,
477 },
478 }
479 tc.runTest(t)
480 }
481
482 func TestReplicaCalcContainerScaleUp(t *testing.T) {
483 tc := replicaCalcTestCase{
484 currentReplicas: 3,
485 expectedReplicas: 5,
486 resource: &resourceInfo{
487 name: v1.ResourceCPU,
488 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
489 levels: [][]int64{{1000, 300}, {1000, 500}, {1000, 700}},
490
491 targetUtilization: 30,
492 expectedUtilization: 50,
493 expectedValue: 500,
494 },
495 container: "container2",
496 }
497 tc.runTest(t)
498 }
499
500 func TestReplicaCalcScaleUpUnreadyLessScale(t *testing.T) {
501 tc := replicaCalcTestCase{
502 currentReplicas: 3,
503 expectedReplicas: 4,
504 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
505 resource: &resourceInfo{
506 name: v1.ResourceCPU,
507 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
508 levels: makePodMetricLevels(300, 500, 700),
509
510 targetUtilization: 30,
511 expectedUtilization: 60,
512 expectedValue: numContainersPerPod * 600,
513 },
514 }
515 tc.runTest(t)
516 }
517
518 func TestReplicaCalcScaleUpContainerHotCpuLessScale(t *testing.T) {
519 tc := replicaCalcTestCase{
520 currentReplicas: 3,
521 expectedReplicas: 4,
522 podStartTime: []metav1.Time{hotCPUCreationTime(), coolCPUCreationTime(), coolCPUCreationTime()},
523 resource: &resourceInfo{
524 name: v1.ResourceCPU,
525 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
526 levels: [][]int64{{0, 300}, {0, 500}, {0, 700}},
527
528 targetUtilization: 30,
529 expectedUtilization: 60,
530 expectedValue: 600,
531 },
532 container: "container2",
533 }
534 tc.runTest(t)
535 }
536
537 func TestReplicaCalcScaleUpHotCpuLessScale(t *testing.T) {
538 tc := replicaCalcTestCase{
539 currentReplicas: 3,
540 expectedReplicas: 4,
541 podStartTime: []metav1.Time{hotCPUCreationTime(), coolCPUCreationTime(), coolCPUCreationTime()},
542 resource: &resourceInfo{
543 name: v1.ResourceCPU,
544 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
545 levels: makePodMetricLevels(300, 500, 700),
546
547 targetUtilization: 30,
548 expectedUtilization: 60,
549 expectedValue: numContainersPerPod * 600,
550 },
551 }
552 tc.runTest(t)
553 }
554
555 func TestReplicaCalcScaleUpUnreadyNoScale(t *testing.T) {
556 tc := replicaCalcTestCase{
557 currentReplicas: 3,
558 expectedReplicas: 3,
559 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
560 resource: &resourceInfo{
561 name: v1.ResourceCPU,
562 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
563 levels: makePodMetricLevels(400, 500, 700),
564
565 targetUtilization: 30,
566 expectedUtilization: 40,
567 expectedValue: numContainersPerPod * 400,
568 },
569 }
570 tc.runTest(t)
571 }
572
573 func TestReplicaCalcScaleHotCpuNoScale(t *testing.T) {
574 tc := replicaCalcTestCase{
575 currentReplicas: 3,
576 expectedReplicas: 3,
577 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
578 podStartTime: []metav1.Time{coolCPUCreationTime(), hotCPUCreationTime(), hotCPUCreationTime()},
579 resource: &resourceInfo{
580 name: v1.ResourceCPU,
581 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
582 levels: makePodMetricLevels(400, 500, 700),
583
584 targetUtilization: 30,
585 expectedUtilization: 40,
586 expectedValue: numContainersPerPod * 400,
587 },
588 }
589 tc.runTest(t)
590 }
591
592 func TestReplicaCalcScaleUpIgnoresFailedPods(t *testing.T) {
593 tc := replicaCalcTestCase{
594 currentReplicas: 2,
595 expectedReplicas: 4,
596 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
597 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
598 resource: &resourceInfo{
599 name: v1.ResourceCPU,
600 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
601 levels: makePodMetricLevels(500, 700),
602
603 targetUtilization: 30,
604 expectedUtilization: 60,
605 expectedValue: numContainersPerPod * 600,
606 },
607 }
608 tc.runTest(t)
609 }
610
611 func TestReplicaCalcScaleUpContainerIgnoresFailedPods(t *testing.T) {
612 tc := replicaCalcTestCase{
613 currentReplicas: 2,
614 expectedReplicas: 4,
615 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
616 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
617 resource: &resourceInfo{
618 name: v1.ResourceCPU,
619 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
620 levels: [][]int64{{1000, 500}, {9000, 700}},
621
622 targetUtilization: 30,
623 expectedUtilization: 60,
624 expectedValue: 600,
625 },
626 container: "container2",
627 }
628 tc.runTest(t)
629 }
630
631 func TestReplicaCalcScaleUpIgnoresDeletionPods(t *testing.T) {
632 tc := replicaCalcTestCase{
633 currentReplicas: 2,
634 expectedReplicas: 4,
635 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
636 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
637 podDeletionTimestamp: []bool{false, false, true, true},
638 resource: &resourceInfo{
639 name: v1.ResourceCPU,
640 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
641 levels: makePodMetricLevels(500, 700),
642
643 targetUtilization: 30,
644 expectedUtilization: 60,
645 expectedValue: numContainersPerPod * 600,
646 },
647 }
648 tc.runTest(t)
649 }
650
651 func TestReplicaCalcScaleUpContainerIgnoresDeletionPods(t *testing.T) {
652 tc := replicaCalcTestCase{
653 currentReplicas: 2,
654 expectedReplicas: 4,
655 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
656 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
657 podDeletionTimestamp: []bool{false, false, true, true},
658 resource: &resourceInfo{
659 name: v1.ResourceCPU,
660 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
661 levels: makePodMetricLevels(500, 700),
662
663 targetUtilization: 30,
664 expectedUtilization: 60,
665 expectedValue: 600,
666 },
667 container: "container1",
668 }
669 tc.runTest(t)
670 }
671
672 func TestReplicaCalcScaleUpCM(t *testing.T) {
673 tc := replicaCalcTestCase{
674 currentReplicas: 3,
675 expectedReplicas: 4,
676 metric: &metricInfo{
677 name: "qps",
678 levels: []int64{20000, 10000, 30000},
679 targetUsage: 15000,
680 expectedUsage: 20000,
681 metricType: podMetric,
682 },
683 }
684 tc.runTest(t)
685 }
686
687 func TestReplicaCalcScaleUpCMUnreadyHotCpuNoLessScale(t *testing.T) {
688 tc := replicaCalcTestCase{
689 currentReplicas: 3,
690 expectedReplicas: 6,
691 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse},
692 podStartTime: []metav1.Time{coolCPUCreationTime(), coolCPUCreationTime(), hotCPUCreationTime()},
693 metric: &metricInfo{
694 name: "qps",
695 levels: []int64{50000, 10000, 30000},
696 targetUsage: 15000,
697 expectedUsage: 30000,
698 metricType: podMetric,
699 },
700 }
701 tc.runTest(t)
702 }
703
704 func TestReplicaCalcScaleUpCMUnreadyHotCpuScaleWouldScaleDown(t *testing.T) {
705 tc := replicaCalcTestCase{
706 currentReplicas: 3,
707 expectedReplicas: 7,
708 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
709 podStartTime: []metav1.Time{hotCPUCreationTime(), coolCPUCreationTime(), hotCPUCreationTime()},
710 metric: &metricInfo{
711 name: "qps",
712 levels: []int64{50000, 15000, 30000},
713 targetUsage: 15000,
714 expectedUsage: 31666,
715 metricType: podMetric,
716 },
717 }
718 tc.runTest(t)
719 }
720
721 func TestReplicaCalcScaleUpCMObject(t *testing.T) {
722 tc := replicaCalcTestCase{
723 currentReplicas: 3,
724 expectedReplicas: 4,
725 metric: &metricInfo{
726 name: "qps",
727 levels: []int64{20000},
728 targetUsage: 15000,
729 expectedUsage: 20000,
730 singleObject: &autoscalingv2.CrossVersionObjectReference{
731 Kind: "Deployment",
732 APIVersion: "apps/v1",
733 Name: "some-deployment",
734 },
735 },
736 }
737 tc.runTest(t)
738 }
739
740 func TestReplicaCalcScaleUpCMPerPodObject(t *testing.T) {
741 tc := replicaCalcTestCase{
742 currentReplicas: 3,
743 expectedReplicas: 4,
744 metric: &metricInfo{
745 metricType: objectPerPodMetric,
746 name: "qps",
747 levels: []int64{20000},
748 perPodTargetUsage: 5000,
749 expectedUsage: 6667,
750 singleObject: &autoscalingv2.CrossVersionObjectReference{
751 Kind: "Deployment",
752 APIVersion: "apps/v1",
753 Name: "some-deployment",
754 },
755 },
756 }
757 tc.runTest(t)
758 }
759
760 func TestReplicaCalcScaleUpCMObjectIgnoresUnreadyPods(t *testing.T) {
761 tc := replicaCalcTestCase{
762 currentReplicas: 3,
763 expectedReplicas: 5,
764 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
765 metric: &metricInfo{
766 name: "qps",
767 levels: []int64{50000},
768 targetUsage: 10000,
769 expectedUsage: 50000,
770 singleObject: &autoscalingv2.CrossVersionObjectReference{
771 Kind: "Deployment",
772 APIVersion: "apps/v1",
773 Name: "some-deployment",
774 },
775 },
776 }
777 tc.runTest(t)
778 }
779
780 func TestReplicaCalcScaleUpCMExternal(t *testing.T) {
781 tc := replicaCalcTestCase{
782 currentReplicas: 1,
783 expectedReplicas: 2,
784 metric: &metricInfo{
785 name: "qps",
786 levels: []int64{8600},
787 targetUsage: 4400,
788 expectedUsage: 8600,
789 selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
790 metricType: podMetric,
791 },
792 }
793 tc.runTest(t)
794 }
795
796 func TestReplicaCalcScaleUpCMExternalIgnoresUnreadyPods(t *testing.T) {
797 tc := replicaCalcTestCase{
798 currentReplicas: 3,
799 expectedReplicas: 2,
800 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse},
801 metric: &metricInfo{
802 name: "qps",
803 levels: []int64{8600},
804 targetUsage: 4400,
805 expectedUsage: 8600,
806 selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
807 metricType: externalMetric,
808 },
809 }
810 tc.runTest(t)
811 }
812
813 func TestReplicaCalcScaleUpCMExternalNoLabels(t *testing.T) {
814 tc := replicaCalcTestCase{
815 currentReplicas: 1,
816 expectedReplicas: 2,
817 metric: &metricInfo{
818 name: "qps",
819 levels: []int64{8600},
820 targetUsage: 4400,
821 expectedUsage: 8600,
822 metricType: podMetric,
823 },
824 }
825 tc.runTest(t)
826 }
827
828 func TestReplicaCalcScaleUpPerPodCMExternal(t *testing.T) {
829 tc := replicaCalcTestCase{
830 currentReplicas: 3,
831 expectedReplicas: 4,
832 metric: &metricInfo{
833 name: "qps",
834 levels: []int64{8600},
835 perPodTargetUsage: 2150,
836 expectedUsage: 2867,
837 selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
838 metricType: externalPerPodMetric,
839 },
840 }
841 tc.runTest(t)
842 }
843
844 func TestReplicaCalcScaleDown(t *testing.T) {
845 tc := replicaCalcTestCase{
846 currentReplicas: 5,
847 expectedReplicas: 3,
848 resource: &resourceInfo{
849 name: v1.ResourceCPU,
850 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
851 levels: makePodMetricLevels(100, 300, 500, 250, 250),
852
853 targetUtilization: 50,
854 expectedUtilization: 28,
855 expectedValue: numContainersPerPod * 280,
856 },
857 }
858 tc.runTest(t)
859 }
860
861 func TestReplicaCalcContainerScaleDown(t *testing.T) {
862 tc := replicaCalcTestCase{
863 currentReplicas: 5,
864 expectedReplicas: 3,
865 resource: &resourceInfo{
866 name: v1.ResourceCPU,
867 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
868 levels: [][]int64{{1000, 100}, {1000, 300}, {1000, 500}, {1000, 250}, {1000, 250}},
869
870 targetUtilization: 50,
871 expectedUtilization: 28,
872 expectedValue: 280,
873 },
874 container: "container2",
875 }
876 tc.runTest(t)
877 }
878
879 func TestReplicaCalcScaleDownCM(t *testing.T) {
880 tc := replicaCalcTestCase{
881 currentReplicas: 5,
882 expectedReplicas: 3,
883 metric: &metricInfo{
884 name: "qps",
885 levels: []int64{12000, 12000, 12000, 12000, 12000},
886 targetUsage: 20000,
887 expectedUsage: 12000,
888 metricType: podMetric,
889 },
890 }
891 tc.runTest(t)
892 }
893
894 func TestReplicaCalcScaleDownPerPodCMObject(t *testing.T) {
895 tc := replicaCalcTestCase{
896 currentReplicas: 5,
897 expectedReplicas: 3,
898 metric: &metricInfo{
899 name: "qps",
900 levels: []int64{6000},
901 perPodTargetUsage: 2000,
902 expectedUsage: 1200,
903 singleObject: &autoscalingv2.CrossVersionObjectReference{
904 Kind: "Deployment",
905 APIVersion: "apps/v1",
906 Name: "some-deployment",
907 },
908 metricType: objectPerPodMetric,
909 },
910 }
911 tc.runTest(t)
912 }
913
914 func TestReplicaCalcScaleDownCMObject(t *testing.T) {
915 tc := replicaCalcTestCase{
916 currentReplicas: 5,
917 expectedReplicas: 3,
918 metric: &metricInfo{
919 name: "qps",
920 levels: []int64{12000},
921 targetUsage: 20000,
922 expectedUsage: 12000,
923 singleObject: &autoscalingv2.CrossVersionObjectReference{
924 Kind: "Deployment",
925 APIVersion: "apps/v1",
926 Name: "some-deployment",
927 },
928 },
929 }
930 tc.runTest(t)
931 }
932
933 func TestReplicaCalcScaleDownCMExternal(t *testing.T) {
934 tc := replicaCalcTestCase{
935 currentReplicas: 5,
936 expectedReplicas: 3,
937 metric: &metricInfo{
938 name: "qps",
939 levels: []int64{8600},
940 targetUsage: 14334,
941 expectedUsage: 8600,
942 selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
943 metricType: externalMetric,
944 },
945 }
946 tc.runTest(t)
947 }
948
949 func TestReplicaCalcScaleDownPerPodCMExternal(t *testing.T) {
950 tc := replicaCalcTestCase{
951 currentReplicas: 5,
952 expectedReplicas: 3,
953 metric: &metricInfo{
954 name: "qps",
955 levels: []int64{8600},
956 perPodTargetUsage: 2867,
957 expectedUsage: 1720,
958 selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
959 metricType: externalPerPodMetric,
960 },
961 }
962 tc.runTest(t)
963 }
964
965 func TestReplicaCalcScaleDownExcludeUnreadyPods(t *testing.T) {
966 tc := replicaCalcTestCase{
967 currentReplicas: 5,
968 expectedReplicas: 2,
969 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
970 resource: &resourceInfo{
971 name: v1.ResourceCPU,
972 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
973 levels: makePodMetricLevels(100, 300, 500, 250, 250),
974
975 targetUtilization: 50,
976 expectedUtilization: 30,
977 expectedValue: numContainersPerPod * 300,
978 },
979 }
980 tc.runTest(t)
981 }
982
983 func TestReplicaCalcScaleDownContainerExcludeUnreadyPods(t *testing.T) {
984 tc := replicaCalcTestCase{
985 currentReplicas: 5,
986 expectedReplicas: 2,
987 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
988 resource: &resourceInfo{
989 name: v1.ResourceCPU,
990 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
991 levels: [][]int64{{1000, 100}, {1000, 300}, {1000, 500}, {1000, 250}, {1000, 250}},
992
993 targetUtilization: 50,
994 expectedUtilization: 30,
995 expectedValue: 300,
996 },
997 container: "container2",
998 }
999 tc.runTest(t)
1000 }
1001
1002 func TestReplicaCalcScaleDownExcludeUnscheduledPods(t *testing.T) {
1003 tc := replicaCalcTestCase{
1004 currentReplicas: 5,
1005 expectedReplicas: 1,
1006 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse, v1.ConditionFalse, v1.ConditionFalse},
1007 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodPending, v1.PodPending, v1.PodPending, v1.PodPending},
1008 resource: &resourceInfo{
1009 name: v1.ResourceCPU,
1010 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1011 levels: makePodMetricLevels(100),
1012
1013 targetUtilization: 50,
1014 expectedUtilization: 10,
1015 expectedValue: numContainersPerPod * 100,
1016 },
1017 }
1018 tc.runTest(t)
1019 }
1020
1021 func TestReplicaCalcScaleDownContainerExcludeUnscheduledPods(t *testing.T) {
1022 tc := replicaCalcTestCase{
1023 currentReplicas: 5,
1024 expectedReplicas: 1,
1025 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse, v1.ConditionFalse, v1.ConditionFalse},
1026 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodPending, v1.PodPending, v1.PodPending, v1.PodPending},
1027 resource: &resourceInfo{
1028 name: v1.ResourceCPU,
1029 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1030 levels: [][]int64{{1000, 100}, {1000, 300}, {1000, 500}, {1000, 250}, {1000, 250}},
1031
1032 targetUtilization: 50,
1033 expectedUtilization: 10,
1034 expectedValue: 100,
1035 },
1036 container: "container2",
1037 }
1038 tc.runTest(t)
1039 }
1040
1041 func TestReplicaCalcScaleDownIgnoreHotCpuPods(t *testing.T) {
1042 tc := replicaCalcTestCase{
1043 currentReplicas: 5,
1044 expectedReplicas: 2,
1045 podStartTime: []metav1.Time{coolCPUCreationTime(), coolCPUCreationTime(), coolCPUCreationTime(), hotCPUCreationTime(), hotCPUCreationTime()},
1046 resource: &resourceInfo{
1047 name: v1.ResourceCPU,
1048 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1049 levels: makePodMetricLevels(100, 300, 500, 250, 250),
1050
1051 targetUtilization: 50,
1052 expectedUtilization: 30,
1053 expectedValue: numContainersPerPod * 300,
1054 },
1055 }
1056 tc.runTest(t)
1057 }
1058
1059 func TestReplicaCalcScaleDownContainerIgnoreHotCpuPods(t *testing.T) {
1060 tc := replicaCalcTestCase{
1061 currentReplicas: 5,
1062 expectedReplicas: 2,
1063 podStartTime: []metav1.Time{coolCPUCreationTime(), coolCPUCreationTime(), coolCPUCreationTime(), hotCPUCreationTime(), hotCPUCreationTime()},
1064 resource: &resourceInfo{
1065 name: v1.ResourceCPU,
1066 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1067 levels: [][]int64{{1000, 100}, {1000, 300}, {1000, 500}, {1000, 1000}, {1000, 1000}},
1068
1069 targetUtilization: 50,
1070 expectedUtilization: 30,
1071 expectedValue: 300,
1072 },
1073 container: "container2",
1074 }
1075 tc.runTest(t)
1076 }
1077
1078 func TestReplicaCalcScaleDownIgnoresFailedPods(t *testing.T) {
1079 tc := replicaCalcTestCase{
1080 currentReplicas: 5,
1081 expectedReplicas: 3,
1082 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
1083 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
1084 resource: &resourceInfo{
1085 name: v1.ResourceCPU,
1086 requests: []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")},
1087 levels: makePodMetricLevels(100, 300, 500, 250, 250),
1088
1089 targetUtilization: 50,
1090 expectedUtilization: 28,
1091 expectedValue: numContainersPerPod * 280,
1092 },
1093 }
1094 tc.runTest(t)
1095 }
1096
1097 func TestReplicaCalcScaleDownContainerIgnoresFailedPods(t *testing.T) {
1098 tc := replicaCalcTestCase{
1099 currentReplicas: 5,
1100 expectedReplicas: 3,
1101 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
1102 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed},
1103 resource: &resourceInfo{
1104 name: v1.ResourceCPU,
1105 requests: []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")},
1106 levels: [][]int64{{1000, 100}, {1000, 300}, {1000, 500}, {1000, 250}, {1000, 250}},
1107
1108 targetUtilization: 50,
1109 expectedUtilization: 28,
1110 expectedValue: 280,
1111 },
1112 container: "container2",
1113 }
1114 tc.runTest(t)
1115 }
1116
1117 func TestReplicaCalcScaleDownIgnoresDeletionPods(t *testing.T) {
1118 tc := replicaCalcTestCase{
1119 currentReplicas: 5,
1120 expectedReplicas: 3,
1121 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
1122 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
1123 podDeletionTimestamp: []bool{false, false, false, false, false, true, true},
1124 resource: &resourceInfo{
1125 name: v1.ResourceCPU,
1126 requests: []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")},
1127 levels: makePodMetricLevels(100, 300, 500, 250, 250),
1128
1129 targetUtilization: 50,
1130 expectedUtilization: 28,
1131 expectedValue: numContainersPerPod * 280,
1132 },
1133 }
1134 tc.runTest(t)
1135 }
1136
1137
1138 func TestReplicaCalcScaleDownIgnoresDeletionPods_StillRunning(t *testing.T) {
1139 tc := replicaCalcTestCase{
1140 currentReplicas: 5,
1141 expectedReplicas: 3,
1142 podReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse},
1143 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning},
1144 podDeletionTimestamp: []bool{false, false, false, false, false, true, true},
1145 resource: &resourceInfo{
1146 name: v1.ResourceCPU,
1147 requests: []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")},
1148 levels: [][]int64{{1000, 100}, {1000, 300}, {1000, 500}, {1000, 250}, {1000, 250}},
1149
1150 targetUtilization: 50,
1151 expectedUtilization: 28,
1152 expectedValue: 280,
1153 },
1154 container: "container2",
1155 }
1156 tc.runTest(t)
1157 }
1158
1159 func TestReplicaCalcTolerance(t *testing.T) {
1160 tc := replicaCalcTestCase{
1161 currentReplicas: 3,
1162 expectedReplicas: 3,
1163 resource: &resourceInfo{
1164 name: v1.ResourceCPU,
1165 requests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
1166 levels: makePodMetricLevels(1010, 1030, 1020),
1167
1168 targetUtilization: 100,
1169 expectedUtilization: 102,
1170 expectedValue: numContainersPerPod * 1020,
1171 },
1172 }
1173 tc.runTest(t)
1174 }
1175
1176 func TestReplicaCalcToleranceCM(t *testing.T) {
1177 tc := replicaCalcTestCase{
1178 currentReplicas: 3,
1179 expectedReplicas: 3,
1180 metric: &metricInfo{
1181 name: "qps",
1182 levels: []int64{20000, 21000, 21000},
1183 targetUsage: 20000,
1184 expectedUsage: 20666,
1185 metricType: podMetric,
1186 },
1187 }
1188 tc.runTest(t)
1189 }
1190
1191 func TestReplicaCalcToleranceCMObject(t *testing.T) {
1192 tc := replicaCalcTestCase{
1193 currentReplicas: 3,
1194 expectedReplicas: 3,
1195 metric: &metricInfo{
1196 name: "qps",
1197 levels: []int64{20666},
1198 targetUsage: 20000,
1199 expectedUsage: 20666,
1200 singleObject: &autoscalingv2.CrossVersionObjectReference{
1201 Kind: "Deployment",
1202 APIVersion: "apps/v1",
1203 Name: "some-deployment",
1204 },
1205 },
1206 }
1207 tc.runTest(t)
1208 }
1209
1210 func TestReplicaCalcTolerancePerPodCMObject(t *testing.T) {
1211 tc := replicaCalcTestCase{
1212 currentReplicas: 4,
1213 expectedReplicas: 4,
1214 metric: &metricInfo{
1215 metricType: objectPerPodMetric,
1216 name: "qps",
1217 levels: []int64{20166},
1218 perPodTargetUsage: 5000,
1219 expectedUsage: 5042,
1220 singleObject: &autoscalingv2.CrossVersionObjectReference{
1221 Kind: "Deployment",
1222 APIVersion: "apps/v1",
1223 Name: "some-deployment",
1224 },
1225 },
1226 }
1227 tc.runTest(t)
1228 }
1229
1230 func TestReplicaCalcToleranceCMExternal(t *testing.T) {
1231 tc := replicaCalcTestCase{
1232 currentReplicas: 3,
1233 expectedReplicas: 3,
1234 metric: &metricInfo{
1235 name: "qps",
1236 levels: []int64{8600},
1237 targetUsage: 8888,
1238 expectedUsage: 8600,
1239 selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
1240 metricType: externalMetric,
1241 },
1242 }
1243 tc.runTest(t)
1244 }
1245
1246 func TestReplicaCalcTolerancePerPodCMExternal(t *testing.T) {
1247 tc := replicaCalcTestCase{
1248 currentReplicas: 3,
1249 expectedReplicas: 3,
1250 metric: &metricInfo{
1251 name: "qps",
1252 levels: []int64{8600},
1253 perPodTargetUsage: 2900,
1254 expectedUsage: 2867,
1255 selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
1256 metricType: externalPerPodMetric,
1257 },
1258 }
1259 tc.runTest(t)
1260 }
1261
1262 func TestReplicaCalcSuperfluousMetrics(t *testing.T) {
1263 tc := replicaCalcTestCase{
1264 currentReplicas: 4,
1265 expectedReplicas: 24,
1266 resource: &resourceInfo{
1267 name: v1.ResourceCPU,
1268 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1269 levels: makePodMetricLevels(4000, 9500, 3000, 7000, 3200, 2000),
1270 targetUtilization: 100,
1271 expectedUtilization: 587,
1272 expectedValue: numContainersPerPod * 5875,
1273 },
1274 }
1275 tc.runTest(t)
1276 }
1277
1278 func TestReplicaCalcMissingMetrics(t *testing.T) {
1279 tc := replicaCalcTestCase{
1280 currentReplicas: 4,
1281 expectedReplicas: 3,
1282 resource: &resourceInfo{
1283 name: v1.ResourceCPU,
1284 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1285 levels: makePodMetricLevels(400, 95),
1286
1287 targetUtilization: 100,
1288 expectedUtilization: 24,
1289 expectedValue: 495,
1290 },
1291 }
1292 tc.runTest(t)
1293 }
1294
1295 func TestReplicaCalcEmptyMetrics(t *testing.T) {
1296 tc := replicaCalcTestCase{
1297 currentReplicas: 4,
1298 expectedError: fmt.Errorf("unable to get metrics for resource cpu: no metrics returned from resource metrics API"),
1299 resource: &resourceInfo{
1300 name: v1.ResourceCPU,
1301 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1302 levels: makePodMetricLevels(),
1303
1304 targetUtilization: 100,
1305 },
1306 }
1307 tc.runTest(t)
1308 }
1309
1310 func TestReplicaCalcEmptyCPURequest(t *testing.T) {
1311 tc := replicaCalcTestCase{
1312 currentReplicas: 1,
1313 expectedError: fmt.Errorf("missing request for"),
1314 resource: &resourceInfo{
1315 name: v1.ResourceCPU,
1316 requests: []resource.Quantity{},
1317 levels: makePodMetricLevels(200),
1318
1319 targetUtilization: 100,
1320 },
1321 }
1322 tc.runTest(t)
1323 }
1324
1325 func TestPlainMetricReplicaCalcMissingMetricsNoChangeEq(t *testing.T) {
1326 tc := replicaCalcTestCase{
1327 currentReplicas: 5,
1328 expectedReplicas: 5,
1329 metric: &metricInfo{
1330 name: "qps",
1331 levels: []int64{20000, 19000, 21000},
1332 targetUsage: 20000,
1333 expectedUsage: 20000,
1334 metricType: podMetric,
1335 },
1336 }
1337 tc.runTest(t)
1338 }
1339
1340 func TestReplicaCalcMissingMetricsNoChangeEq(t *testing.T) {
1341 tc := replicaCalcTestCase{
1342 currentReplicas: 2,
1343 expectedReplicas: 2,
1344 resource: &resourceInfo{
1345 name: v1.ResourceCPU,
1346 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
1347 levels: makePodMetricLevels(1000),
1348
1349 targetUtilization: 100,
1350 expectedUtilization: 100,
1351 expectedValue: numContainersPerPod * 1000,
1352 },
1353 }
1354 tc.runTest(t)
1355 }
1356
1357 func TestReplicaCalcMissingMetricsNoChangeGt(t *testing.T) {
1358 tc := replicaCalcTestCase{
1359 currentReplicas: 2,
1360 expectedReplicas: 2,
1361 resource: &resourceInfo{
1362 name: v1.ResourceCPU,
1363 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
1364 levels: makePodMetricLevels(1900),
1365
1366 targetUtilization: 100,
1367 expectedUtilization: 190,
1368 expectedValue: numContainersPerPod * 1900,
1369 },
1370 }
1371 tc.runTest(t)
1372 }
1373
1374 func TestReplicaCalcMissingMetricsNoChangeLt(t *testing.T) {
1375 tc := replicaCalcTestCase{
1376 currentReplicas: 2,
1377 expectedReplicas: 2,
1378 resource: &resourceInfo{
1379 name: v1.ResourceCPU,
1380 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0")},
1381 levels: makePodMetricLevels(600),
1382
1383 targetUtilization: 100,
1384 expectedUtilization: 60,
1385 expectedValue: numContainersPerPod * 600,
1386 },
1387 }
1388 tc.runTest(t)
1389 }
1390
1391 func TestReplicaCalcMissingMetricsUnreadyChange(t *testing.T) {
1392 tc := replicaCalcTestCase{
1393 currentReplicas: 3,
1394 expectedReplicas: 3,
1395 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
1396 resource: &resourceInfo{
1397 name: v1.ResourceCPU,
1398 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1399 levels: makePodMetricLevels(100, 450),
1400
1401 targetUtilization: 50,
1402 expectedUtilization: 45,
1403 expectedValue: numContainersPerPod * 450,
1404 },
1405 }
1406 tc.runTest(t)
1407 }
1408
1409 func TestReplicaCalcMissingMetricsHotCpuNoChange(t *testing.T) {
1410 tc := replicaCalcTestCase{
1411 currentReplicas: 3,
1412 expectedReplicas: 3,
1413 podStartTime: []metav1.Time{hotCPUCreationTime(), coolCPUCreationTime(), coolCPUCreationTime()},
1414 resource: &resourceInfo{
1415 name: v1.ResourceCPU,
1416 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1417 levels: makePodMetricLevels(100, 450),
1418
1419 targetUtilization: 50,
1420 expectedUtilization: 45,
1421 expectedValue: numContainersPerPod * 450,
1422 },
1423 }
1424 tc.runTest(t)
1425 }
1426
1427 func TestReplicaCalcMissingMetricsUnreadyScaleUp(t *testing.T) {
1428 tc := replicaCalcTestCase{
1429 currentReplicas: 3,
1430 expectedReplicas: 4,
1431 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
1432 resource: &resourceInfo{
1433 name: v1.ResourceCPU,
1434 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1435 levels: makePodMetricLevels(100, 2000),
1436
1437 targetUtilization: 50,
1438 expectedUtilization: 200,
1439 expectedValue: numContainersPerPod * 2000,
1440 },
1441 }
1442 tc.runTest(t)
1443 }
1444
1445 func TestReplicaCalcMissingMetricsHotCpuScaleUp(t *testing.T) {
1446 tc := replicaCalcTestCase{
1447 currentReplicas: 3,
1448 expectedReplicas: 4,
1449 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue},
1450 podStartTime: []metav1.Time{hotCPUCreationTime(), coolCPUCreationTime(), coolCPUCreationTime()},
1451 resource: &resourceInfo{
1452 name: v1.ResourceCPU,
1453 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1454 levels: makePodMetricLevels(100, 2000),
1455
1456 targetUtilization: 50,
1457 expectedUtilization: 200,
1458 expectedValue: numContainersPerPod * 2000,
1459 },
1460 }
1461 tc.runTest(t)
1462 }
1463
1464 func TestReplicaCalcMissingMetricsUnreadyScaleDown(t *testing.T) {
1465 tc := replicaCalcTestCase{
1466 currentReplicas: 4,
1467 expectedReplicas: 3,
1468 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue},
1469 resource: &resourceInfo{
1470 name: v1.ResourceCPU,
1471 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1472 levels: makePodMetricLevels(100, 100, 100),
1473
1474 targetUtilization: 50,
1475 expectedUtilization: 10,
1476 expectedValue: numContainersPerPod * 100,
1477 },
1478 }
1479 tc.runTest(t)
1480 }
1481
1482 func TestReplicaCalcMissingMetricsScaleDownTargetOver100(t *testing.T) {
1483 tc := replicaCalcTestCase{
1484 currentReplicas: 4,
1485 expectedReplicas: 2,
1486 podReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue},
1487 resource: &resourceInfo{
1488 name: v1.ResourceCPU,
1489 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("2.0"), resource.MustParse("2.0")},
1490 levels: makePodMetricLevels(200, 100, 100),
1491
1492 targetUtilization: 300,
1493 expectedUtilization: 6,
1494 expectedValue: numContainersPerPod * 100,
1495 },
1496 }
1497 tc.runTest(t)
1498 }
1499
1500 func TestReplicaCalcDuringRollingUpdateWithMaxSurge(t *testing.T) {
1501 tc := replicaCalcTestCase{
1502 currentReplicas: 2,
1503 expectedReplicas: 2,
1504 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning},
1505 resource: &resourceInfo{
1506 name: v1.ResourceCPU,
1507 requests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
1508 levels: makePodMetricLevels(100, 100),
1509
1510 targetUtilization: 50,
1511 expectedUtilization: 10,
1512 expectedValue: numContainersPerPod * 100,
1513 },
1514 }
1515 tc.runTest(t)
1516 }
1517
1518 func TestReplicaCalcDuringRollingUpdateWithMaxSurgeCM(t *testing.T) {
1519 tc := replicaCalcTestCase{
1520 currentReplicas: 2,
1521 expectedReplicas: 2,
1522 podPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning},
1523 metric: &metricInfo{
1524 name: "qps",
1525 levels: []int64{10000, 10000},
1526 targetUsage: 17000,
1527 expectedUsage: 10000,
1528 metricType: podMetric,
1529 },
1530 }
1531 tc.runTest(t)
1532 }
1533
1534
1535
1536
1537 func TestReplicaCalcComputedToleranceAlgImplementation(t *testing.T) {
1538
1539 startPods := int32(10)
1540
1541 totalUsedCPUOfAllPods := int64(startPods * 150)
1542
1543
1544 totalRequestedCPUOfAllPods := int32(2 * totalUsedCPUOfAllPods)
1545 requestedToUsed := float64(totalRequestedCPUOfAllPods / int32(totalUsedCPUOfAllPods))
1546
1547 perPodRequested := totalRequestedCPUOfAllPods / startPods
1548
1549
1550 target := math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .01
1551 finalCPUPercentTarget := int32(target * 100)
1552 resourcesUsedRatio := float64(totalUsedCPUOfAllPods) / float64(float64(totalRequestedCPUOfAllPods)*target)
1553
1554
1555 finalPods := int32(math.Ceil(resourcesUsedRatio * float64(startPods)))
1556
1557
1558 tc := replicaCalcTestCase{
1559 currentReplicas: startPods,
1560 expectedReplicas: finalPods,
1561 resource: &resourceInfo{
1562 name: v1.ResourceCPU,
1563 levels: makePodMetricLevels(
1564 totalUsedCPUOfAllPods/10,
1565 totalUsedCPUOfAllPods/10,
1566 totalUsedCPUOfAllPods/10,
1567 totalUsedCPUOfAllPods/10,
1568 totalUsedCPUOfAllPods/10,
1569 totalUsedCPUOfAllPods/10,
1570 totalUsedCPUOfAllPods/10,
1571 totalUsedCPUOfAllPods/10,
1572 totalUsedCPUOfAllPods/10,
1573 totalUsedCPUOfAllPods/10,
1574 ),
1575 requests: []resource.Quantity{
1576 resource.MustParse(fmt.Sprint(perPodRequested+100) + "m"),
1577 resource.MustParse(fmt.Sprint(perPodRequested-100) + "m"),
1578 resource.MustParse(fmt.Sprint(perPodRequested+10) + "m"),
1579 resource.MustParse(fmt.Sprint(perPodRequested-10) + "m"),
1580 resource.MustParse(fmt.Sprint(perPodRequested+2) + "m"),
1581 resource.MustParse(fmt.Sprint(perPodRequested-2) + "m"),
1582 resource.MustParse(fmt.Sprint(perPodRequested+1) + "m"),
1583 resource.MustParse(fmt.Sprint(perPodRequested-1) + "m"),
1584 resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
1585 resource.MustParse(fmt.Sprint(perPodRequested) + "m"),
1586 },
1587
1588 targetUtilization: finalCPUPercentTarget,
1589 expectedUtilization: int32(totalUsedCPUOfAllPods*100) / totalRequestedCPUOfAllPods,
1590 expectedValue: numContainersPerPod * totalUsedCPUOfAllPods / 10,
1591 },
1592 }
1593
1594 tc.runTest(t)
1595
1596
1597
1598 target = math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .004
1599 finalCPUPercentTarget = int32(target * 100)
1600 tc.resource.targetUtilization = finalCPUPercentTarget
1601 tc.currentReplicas = startPods
1602 tc.expectedReplicas = startPods
1603 tc.runTest(t)
1604 }
1605
1606 func TestGroupPods(t *testing.T) {
1607 tests := []struct {
1608 name string
1609 pods []*v1.Pod
1610 metrics metricsclient.PodMetricsInfo
1611 resource v1.ResourceName
1612 expectReadyPodCount int
1613 expectUnreadyPods sets.String
1614 expectMissingPods sets.String
1615 expectIgnoredPods sets.String
1616 }{
1617 {
1618 name: "void",
1619 pods: []*v1.Pod{},
1620 metrics: metricsclient.PodMetricsInfo{},
1621 resource: v1.ResourceCPU,
1622 expectReadyPodCount: 0,
1623 expectUnreadyPods: sets.NewString(),
1624 expectMissingPods: sets.NewString(),
1625 expectIgnoredPods: sets.NewString(),
1626 }, {
1627 name: "count in a ready pod - memory",
1628 pods: []*v1.Pod{
1629 {
1630 ObjectMeta: metav1.ObjectMeta{
1631 Name: "bentham",
1632 },
1633 Status: v1.PodStatus{
1634 Phase: v1.PodSucceeded,
1635 },
1636 },
1637 },
1638 metrics: metricsclient.PodMetricsInfo{
1639 "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: time.Minute},
1640 },
1641 resource: v1.ResourceMemory,
1642 expectReadyPodCount: 1,
1643 expectUnreadyPods: sets.NewString(),
1644 expectMissingPods: sets.NewString(),
1645 expectIgnoredPods: sets.NewString(),
1646 }, {
1647 name: "unready a pod without ready condition - CPU",
1648 pods: []*v1.Pod{
1649 {
1650 ObjectMeta: metav1.ObjectMeta{
1651 Name: "lucretius",
1652 },
1653 Status: v1.PodStatus{
1654 Phase: v1.PodSucceeded,
1655 StartTime: &metav1.Time{
1656 Time: time.Now(),
1657 },
1658 },
1659 },
1660 },
1661 metrics: metricsclient.PodMetricsInfo{
1662 "lucretius": metricsclient.PodMetric{Value: 1},
1663 },
1664 resource: v1.ResourceCPU,
1665 expectReadyPodCount: 0,
1666 expectUnreadyPods: sets.NewString("lucretius"),
1667 expectMissingPods: sets.NewString(),
1668 expectIgnoredPods: sets.NewString(),
1669 }, {
1670 name: "count in a ready pod with fresh metrics during initialization period - CPU",
1671 pods: []*v1.Pod{
1672 {
1673 ObjectMeta: metav1.ObjectMeta{
1674 Name: "bentham",
1675 },
1676 Status: v1.PodStatus{
1677 Phase: v1.PodSucceeded,
1678 StartTime: &metav1.Time{
1679 Time: time.Now().Add(-1 * time.Minute),
1680 },
1681 Conditions: []v1.PodCondition{
1682 {
1683 Type: v1.PodReady,
1684 LastTransitionTime: metav1.Time{Time: time.Now().Add(-30 * time.Second)},
1685 Status: v1.ConditionTrue,
1686 },
1687 },
1688 },
1689 },
1690 },
1691 metrics: metricsclient.PodMetricsInfo{
1692 "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: 30 * time.Second},
1693 },
1694 resource: v1.ResourceCPU,
1695 expectReadyPodCount: 1,
1696 expectUnreadyPods: sets.NewString(),
1697 expectMissingPods: sets.NewString(),
1698 expectIgnoredPods: sets.NewString(),
1699 }, {
1700 name: "unready a ready pod without fresh metrics during initialization period - CPU",
1701 pods: []*v1.Pod{
1702 {
1703 ObjectMeta: metav1.ObjectMeta{
1704 Name: "bentham",
1705 },
1706 Status: v1.PodStatus{
1707 Phase: v1.PodSucceeded,
1708 StartTime: &metav1.Time{
1709 Time: time.Now().Add(-1 * time.Minute),
1710 },
1711 Conditions: []v1.PodCondition{
1712 {
1713 Type: v1.PodReady,
1714 LastTransitionTime: metav1.Time{Time: time.Now().Add(-30 * time.Second)},
1715 Status: v1.ConditionTrue,
1716 },
1717 },
1718 },
1719 },
1720 },
1721 metrics: metricsclient.PodMetricsInfo{
1722 "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now(), Window: 60 * time.Second},
1723 },
1724 resource: v1.ResourceCPU,
1725 expectReadyPodCount: 0,
1726 expectUnreadyPods: sets.NewString("bentham"),
1727 expectMissingPods: sets.NewString(),
1728 expectIgnoredPods: sets.NewString(),
1729 }, {
1730 name: "unready an unready pod during initialization period - CPU",
1731 pods: []*v1.Pod{
1732 {
1733 ObjectMeta: metav1.ObjectMeta{
1734 Name: "lucretius",
1735 },
1736 Status: v1.PodStatus{
1737 Phase: v1.PodSucceeded,
1738 StartTime: &metav1.Time{
1739 Time: time.Now().Add(-10 * time.Minute),
1740 },
1741 Conditions: []v1.PodCondition{
1742 {
1743 Type: v1.PodReady,
1744 LastTransitionTime: metav1.Time{Time: time.Now().Add(-9*time.Minute - 54*time.Second)},
1745 Status: v1.ConditionFalse,
1746 },
1747 },
1748 },
1749 },
1750 },
1751 metrics: metricsclient.PodMetricsInfo{
1752 "lucretius": metricsclient.PodMetric{Value: 1},
1753 },
1754 resource: v1.ResourceCPU,
1755 expectReadyPodCount: 0,
1756 expectUnreadyPods: sets.NewString("lucretius"),
1757 expectMissingPods: sets.NewString(),
1758 expectIgnoredPods: sets.NewString(),
1759 }, {
1760 name: "count in a ready pod without fresh metrics after initialization period - CPU",
1761 pods: []*v1.Pod{
1762 {
1763 ObjectMeta: metav1.ObjectMeta{
1764 Name: "bentham",
1765 },
1766 Status: v1.PodStatus{
1767 Phase: v1.PodSucceeded,
1768 StartTime: &metav1.Time{
1769 Time: time.Now().Add(-3 * time.Minute),
1770 },
1771 Conditions: []v1.PodCondition{
1772 {
1773 Type: v1.PodReady,
1774 LastTransitionTime: metav1.Time{Time: time.Now().Add(-3 * time.Minute)},
1775 Status: v1.ConditionTrue,
1776 },
1777 },
1778 },
1779 },
1780 },
1781 metrics: metricsclient.PodMetricsInfo{
1782 "bentham": metricsclient.PodMetric{Value: 1, Timestamp: time.Now().Add(-2 * time.Minute), Window: time.Minute},
1783 },
1784 resource: v1.ResourceCPU,
1785 expectReadyPodCount: 1,
1786 expectUnreadyPods: sets.NewString(),
1787 expectMissingPods: sets.NewString(),
1788 expectIgnoredPods: sets.NewString(),
1789 }, {
1790 name: "count in an unready pod that was ready after initialization period - CPU",
1791 pods: []*v1.Pod{
1792 {
1793 ObjectMeta: metav1.ObjectMeta{
1794 Name: "lucretius",
1795 },
1796 Status: v1.PodStatus{
1797 Phase: v1.PodSucceeded,
1798 StartTime: &metav1.Time{
1799 Time: time.Now().Add(-10 * time.Minute),
1800 },
1801 Conditions: []v1.PodCondition{
1802 {
1803 Type: v1.PodReady,
1804 LastTransitionTime: metav1.Time{Time: time.Now().Add(-9 * time.Minute)},
1805 Status: v1.ConditionFalse,
1806 },
1807 },
1808 },
1809 },
1810 },
1811 metrics: metricsclient.PodMetricsInfo{
1812 "lucretius": metricsclient.PodMetric{Value: 1},
1813 },
1814 resource: v1.ResourceCPU,
1815 expectReadyPodCount: 1,
1816 expectUnreadyPods: sets.NewString(),
1817 expectMissingPods: sets.NewString(),
1818 expectIgnoredPods: sets.NewString(),
1819 }, {
1820 name: "unready pod that has never been ready after initialization period - CPU",
1821 pods: []*v1.Pod{
1822 {
1823 ObjectMeta: metav1.ObjectMeta{
1824 Name: "lucretius",
1825 },
1826 Status: v1.PodStatus{
1827 Phase: v1.PodSucceeded,
1828 StartTime: &metav1.Time{
1829 Time: time.Now().Add(-10 * time.Minute),
1830 },
1831 Conditions: []v1.PodCondition{
1832 {
1833 Type: v1.PodReady,
1834 LastTransitionTime: metav1.Time{Time: time.Now().Add(-9*time.Minute - 50*time.Second)},
1835 Status: v1.ConditionFalse,
1836 },
1837 },
1838 },
1839 },
1840 },
1841 metrics: metricsclient.PodMetricsInfo{
1842 "lucretius": metricsclient.PodMetric{Value: 1},
1843 },
1844 resource: v1.ResourceCPU,
1845 expectReadyPodCount: 1,
1846 expectUnreadyPods: sets.NewString(),
1847 expectMissingPods: sets.NewString(),
1848 expectIgnoredPods: sets.NewString(),
1849 }, {
1850 name: "a missing pod",
1851 pods: []*v1.Pod{
1852 {
1853 ObjectMeta: metav1.ObjectMeta{
1854 Name: "epicurus",
1855 },
1856 Status: v1.PodStatus{
1857 Phase: v1.PodSucceeded,
1858 StartTime: &metav1.Time{
1859 Time: time.Now().Add(-3 * time.Minute),
1860 },
1861 },
1862 },
1863 },
1864 metrics: metricsclient.PodMetricsInfo{},
1865 resource: v1.ResourceCPU,
1866 expectReadyPodCount: 0,
1867 expectUnreadyPods: sets.NewString(),
1868 expectMissingPods: sets.NewString("epicurus"),
1869 expectIgnoredPods: sets.NewString(),
1870 }, {
1871 name: "several pods",
1872 pods: []*v1.Pod{
1873 {
1874 ObjectMeta: metav1.ObjectMeta{
1875 Name: "lucretius",
1876 },
1877 Status: v1.PodStatus{
1878 Phase: v1.PodSucceeded,
1879 StartTime: &metav1.Time{
1880 Time: time.Now(),
1881 },
1882 },
1883 },
1884 {
1885 ObjectMeta: metav1.ObjectMeta{
1886 Name: "niccolo",
1887 },
1888 Status: v1.PodStatus{
1889 Phase: v1.PodSucceeded,
1890 StartTime: &metav1.Time{
1891 Time: time.Now().Add(-3 * time.Minute),
1892 },
1893 Conditions: []v1.PodCondition{
1894 {
1895 Type: v1.PodReady,
1896 LastTransitionTime: metav1.Time{Time: time.Now().Add(-3 * time.Minute)},
1897 Status: v1.ConditionTrue,
1898 },
1899 },
1900 },
1901 },
1902 {
1903 ObjectMeta: metav1.ObjectMeta{
1904 Name: "epicurus",
1905 },
1906 Status: v1.PodStatus{
1907 Phase: v1.PodSucceeded,
1908 StartTime: &metav1.Time{
1909 Time: time.Now().Add(-3 * time.Minute),
1910 },
1911 },
1912 },
1913 },
1914 metrics: metricsclient.PodMetricsInfo{
1915 "lucretius": metricsclient.PodMetric{Value: 1},
1916 "niccolo": metricsclient.PodMetric{Value: 1},
1917 },
1918 resource: v1.ResourceCPU,
1919 expectReadyPodCount: 1,
1920 expectUnreadyPods: sets.NewString("lucretius"),
1921 expectMissingPods: sets.NewString("epicurus"),
1922 expectIgnoredPods: sets.NewString(),
1923 }, {
1924 name: "pending pods are unreadied",
1925 pods: []*v1.Pod{
1926 {
1927 ObjectMeta: metav1.ObjectMeta{
1928 Name: "unscheduled",
1929 },
1930 Status: v1.PodStatus{
1931 Phase: v1.PodPending,
1932 },
1933 },
1934 },
1935 metrics: metricsclient.PodMetricsInfo{},
1936 resource: v1.ResourceCPU,
1937 expectReadyPodCount: 0,
1938 expectUnreadyPods: sets.NewString("unscheduled"),
1939 expectMissingPods: sets.NewString(),
1940 expectIgnoredPods: sets.NewString(),
1941 }, {
1942 name: "ignore pods with deletion timestamps",
1943 pods: []*v1.Pod{
1944 {
1945 ObjectMeta: metav1.ObjectMeta{
1946 Name: "deleted",
1947 DeletionTimestamp: &metav1.Time{Time: time.Unix(1, 0)},
1948 },
1949 Status: v1.PodStatus{
1950 Phase: v1.PodPending,
1951 },
1952 },
1953 },
1954 metrics: metricsclient.PodMetricsInfo{
1955 "deleted": metricsclient.PodMetric{Value: 1},
1956 },
1957 resource: v1.ResourceCPU,
1958 expectReadyPodCount: 0,
1959 expectUnreadyPods: sets.NewString(),
1960 expectMissingPods: sets.NewString(),
1961 expectIgnoredPods: sets.NewString("deleted"),
1962 }, {
1963 name: "ignore pods in a failed state",
1964 pods: []*v1.Pod{
1965 {
1966 ObjectMeta: metav1.ObjectMeta{
1967 Name: "failed",
1968 },
1969 Status: v1.PodStatus{
1970 Phase: v1.PodFailed,
1971 },
1972 },
1973 },
1974 metrics: metricsclient.PodMetricsInfo{
1975 "failed": metricsclient.PodMetric{Value: 1},
1976 },
1977 resource: v1.ResourceCPU,
1978 expectReadyPodCount: 0,
1979 expectUnreadyPods: sets.NewString(),
1980 expectMissingPods: sets.NewString(),
1981 expectIgnoredPods: sets.NewString("failed"),
1982 },
1983 }
1984 for _, tc := range tests {
1985 t.Run(tc.name, func(t *testing.T) {
1986 readyPodCount, unreadyPods, missingPods, ignoredPods := groupPods(tc.pods, tc.metrics, tc.resource, defaultTestingCPUInitializationPeriod, defaultTestingDelayOfInitialReadinessStatus)
1987 if readyPodCount != tc.expectReadyPodCount {
1988 t.Errorf("%s got readyPodCount %d, expected %d", tc.name, readyPodCount, tc.expectReadyPodCount)
1989 }
1990 if !unreadyPods.Equal(tc.expectUnreadyPods) {
1991 t.Errorf("%s got unreadyPods %v, expected %v", tc.name, unreadyPods, tc.expectUnreadyPods)
1992 }
1993 if !missingPods.Equal(tc.expectMissingPods) {
1994 t.Errorf("%s got missingPods %v, expected %v", tc.name, missingPods, tc.expectMissingPods)
1995 }
1996 if !ignoredPods.Equal(tc.expectIgnoredPods) {
1997 t.Errorf("%s got ignoredPods %v, expected %v", tc.name, ignoredPods, tc.expectIgnoredPods)
1998 }
1999 })
2000 }
2001 }
2002
2003 func TestCalculatePodRequests(t *testing.T) {
2004 containerRestartPolicyAlways := v1.ContainerRestartPolicyAlways
2005 testPod := "test-pod"
2006
2007 tests := []struct {
2008 name string
2009 pods []*v1.Pod
2010 container string
2011 resource v1.ResourceName
2012 expectedRequests map[string]int64
2013 expectedError error
2014 }{
2015 {
2016 name: "void",
2017 pods: []*v1.Pod{},
2018 container: "",
2019 resource: v1.ResourceCPU,
2020 expectedRequests: map[string]int64{},
2021 expectedError: nil,
2022 },
2023 {
2024 name: "pod with regular containers",
2025 pods: []*v1.Pod{{
2026 ObjectMeta: metav1.ObjectMeta{
2027 Name: testPod,
2028 Namespace: testNamespace,
2029 },
2030 Spec: v1.PodSpec{
2031 Containers: []v1.Container{
2032 {Name: "container1", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI)}}},
2033 {Name: "container2", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI)}}},
2034 },
2035 },
2036 }},
2037 container: "",
2038 resource: v1.ResourceCPU,
2039 expectedRequests: map[string]int64{testPod: 150},
2040 expectedError: nil,
2041 },
2042 {
2043 name: "calculate requests with special container",
2044 pods: []*v1.Pod{{
2045 ObjectMeta: metav1.ObjectMeta{
2046 Name: testPod,
2047 Namespace: testNamespace,
2048 },
2049 Spec: v1.PodSpec{
2050 Containers: []v1.Container{
2051 {Name: "container1", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI)}}},
2052 {Name: "container2", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI)}}},
2053 },
2054 },
2055 }},
2056 container: "container1",
2057 resource: v1.ResourceCPU,
2058 expectedRequests: map[string]int64{testPod: 100},
2059 expectedError: nil,
2060 },
2061 {
2062 name: "container missing requests",
2063 pods: []*v1.Pod{{
2064 ObjectMeta: metav1.ObjectMeta{
2065 Name: testPod,
2066 Namespace: testNamespace,
2067 },
2068 Spec: v1.PodSpec{
2069 Containers: []v1.Container{
2070 {Name: "container1"},
2071 },
2072 },
2073 }},
2074 container: "",
2075 resource: v1.ResourceCPU,
2076 expectedRequests: nil,
2077 expectedError: fmt.Errorf("missing request for %s in container %s of Pod %s", v1.ResourceCPU, "container1", testPod),
2078 },
2079 {
2080 name: "pod with restartable init containers",
2081 pods: []*v1.Pod{{
2082 ObjectMeta: metav1.ObjectMeta{
2083 Name: testPod,
2084 Namespace: testNamespace,
2085 },
2086 Spec: v1.PodSpec{
2087 Containers: []v1.Container{
2088 {Name: "container1", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI)}}},
2089 },
2090 InitContainers: []v1.Container{
2091 {Name: "init-container1", Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(20, resource.DecimalSI)}}},
2092 {Name: "restartable-container1", RestartPolicy: &containerRestartPolicyAlways, Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI)}}},
2093 },
2094 },
2095 }},
2096 container: "",
2097 resource: v1.ResourceCPU,
2098 expectedRequests: map[string]int64{testPod: 150},
2099 expectedError: nil,
2100 },
2101 }
2102
2103 for _, tc := range tests {
2104 t.Run(tc.name, func(t *testing.T) {
2105 requests, err := calculatePodRequests(tc.pods, tc.container, tc.resource)
2106 assert.Equal(t, tc.expectedRequests, requests, "requests should be as expected")
2107 assert.Equal(t, tc.expectedError, err, "error should be as expected")
2108 })
2109 }
2110 }
2111
View as plain text