k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/controller/podautoscaler/horizontal_test.go (about) 1 /* 2 Copyright 2015 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package podautoscaler 18 19 import ( 20 "context" 21 "fmt" 22 "math" 23 goruntime "runtime" 24 "strings" 25 "sync" 26 "testing" 27 "time" 28 29 autoscalingv1 "k8s.io/api/autoscaling/v1" 30 autoscalingv2 "k8s.io/api/autoscaling/v2" 31 v1 "k8s.io/api/core/v1" 32 "k8s.io/apimachinery/pkg/api/meta/testrestmapper" 33 "k8s.io/apimachinery/pkg/api/resource" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/labels" 36 "k8s.io/apimachinery/pkg/runtime" 37 "k8s.io/apimachinery/pkg/runtime/schema" 38 "k8s.io/apimachinery/pkg/util/wait" 39 "k8s.io/apimachinery/pkg/watch" 40 "k8s.io/client-go/informers" 41 "k8s.io/client-go/kubernetes/fake" 42 scalefake "k8s.io/client-go/scale/fake" 43 core "k8s.io/client-go/testing" 44 "k8s.io/kubernetes/pkg/api/legacyscheme" 45 autoscalingapiv2 "k8s.io/kubernetes/pkg/apis/autoscaling/v2" 46 "k8s.io/kubernetes/pkg/controller" 47 "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics" 48 "k8s.io/kubernetes/pkg/controller/podautoscaler/monitor" 49 "k8s.io/kubernetes/pkg/controller/util/selectors" 50 "k8s.io/kubernetes/test/utils/ktesting" 51 cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2" 52 emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1" 53 metricsapi "k8s.io/metrics/pkg/apis/metrics/v1beta1" 54 metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake" 55 cmfake "k8s.io/metrics/pkg/client/custom_metrics/fake" 56 emfake "k8s.io/metrics/pkg/client/external_metrics/fake" 57 "k8s.io/utils/pointer" 58 59 "github.com/stretchr/testify/assert" 60 61 _ "k8s.io/kubernetes/pkg/apis/apps/install" 62 _ "k8s.io/kubernetes/pkg/apis/autoscaling/install" 63 ) 64 65 // From now on, the HPA controller does have history in it (scaleUpEvents, scaleDownEvents) 66 // Hence the second HPA controller reconcile cycle might return different result (comparing with the first run). 67 // Current test infrastructure has a race condition, when several reconcile cycles will be performed 68 // while it should be stopped right after the first one. And the second will raise an exception 69 // because of different result. 70 71 // This comment has more info: https://github.com/kubernetes/kubernetes/pull/74525#issuecomment-502653106 72 // We need to rework this infrastructure: https://github.com/kubernetes/kubernetes/issues/79222 73 74 var statusOk = []autoscalingv2.HorizontalPodAutoscalerCondition{ 75 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"}, 76 {Type: autoscalingv2.ScalingActive, Status: v1.ConditionTrue, Reason: "ValidMetricFound"}, 77 {Type: autoscalingv2.ScalingLimited, Status: v1.ConditionFalse, Reason: "DesiredWithinRange"}, 78 } 79 80 // statusOkWithOverrides returns the "ok" status with the given conditions as overridden 81 func statusOkWithOverrides(overrides ...autoscalingv2.HorizontalPodAutoscalerCondition) []autoscalingv2.HorizontalPodAutoscalerCondition { 82 resv2 := make([]autoscalingv2.HorizontalPodAutoscalerCondition, len(statusOk)) 83 copy(resv2, statusOk) 84 for _, override := range overrides { 85 resv2 = setConditionInList(resv2, override.Type, override.Status, override.Reason, override.Message) 86 } 87 88 // copy to a v1 slice 89 resv1 := make([]autoscalingv2.HorizontalPodAutoscalerCondition, len(resv2)) 90 for i, cond := range resv2 { 91 resv1[i] = autoscalingv2.HorizontalPodAutoscalerCondition{ 92 Type: autoscalingv2.HorizontalPodAutoscalerConditionType(cond.Type), 93 Status: cond.Status, 94 Reason: cond.Reason, 95 } 96 } 97 98 return resv1 99 } 100 101 func alwaysReady() bool { return true } 102 103 type fakeResource struct { 104 name string 105 apiVersion string 106 kind string 107 } 108 109 type testCase struct { 110 sync.Mutex 111 minReplicas int32 112 maxReplicas int32 113 specReplicas int32 114 statusReplicas int32 115 initialReplicas int32 116 scaleUpRules *autoscalingv2.HPAScalingRules 117 scaleDownRules *autoscalingv2.HPAScalingRules 118 119 // CPU target utilization as a percentage of the requested resources. 120 CPUTarget int32 121 CPUCurrent int32 122 verifyCPUCurrent bool 123 reportedLevels []uint64 124 reportedCPURequests []resource.Quantity 125 reportedPodReadiness []v1.ConditionStatus 126 reportedPodStartTime []metav1.Time 127 reportedPodPhase []v1.PodPhase 128 reportedPodDeletionTimestamp []bool 129 scaleUpdated bool 130 statusUpdated bool 131 eventCreated bool 132 verifyEvents bool 133 useMetricsAPI bool 134 metricsTarget []autoscalingv2.MetricSpec 135 expectedDesiredReplicas int32 136 expectedConditions []autoscalingv2.HorizontalPodAutoscalerCondition 137 // Channel with names of HPA objects which we have reconciled. 138 processed chan string 139 140 // expected results reported to the mock monitor at first. 141 expectedReportedReconciliationActionLabel monitor.ActionLabel 142 expectedReportedReconciliationErrorLabel monitor.ErrorLabel 143 expectedReportedMetricComputationActionLabels map[autoscalingv2.MetricSourceType]monitor.ActionLabel 144 expectedReportedMetricComputationErrorLabels map[autoscalingv2.MetricSourceType]monitor.ErrorLabel 145 146 // Target resource information. 147 resource *fakeResource 148 149 // Last scale time 150 lastScaleTime *metav1.Time 151 152 // override the test clients 153 testClient *fake.Clientset 154 testMetricsClient *metricsfake.Clientset 155 testCMClient *cmfake.FakeCustomMetricsClient 156 testEMClient *emfake.FakeExternalMetricsClient 157 testScaleClient *scalefake.FakeScaleClient 158 159 recommendations []timestampedRecommendation 160 hpaSelectors *selectors.BiMultimap 161 } 162 163 // Needs to be called under a lock. 164 func (tc *testCase) computeCPUCurrent() { 165 if len(tc.reportedLevels) != len(tc.reportedCPURequests) || len(tc.reportedLevels) == 0 { 166 return 167 } 168 reported := 0 169 for _, r := range tc.reportedLevels { 170 reported += int(r) 171 } 172 requested := 0 173 for _, req := range tc.reportedCPURequests { 174 requested += int(req.MilliValue()) 175 } 176 tc.CPUCurrent = int32(100 * reported / requested) 177 } 178 179 func init() { 180 // set this high so we don't accidentally run into it when testing 181 scaleUpLimitFactor = 8 182 } 183 184 func (tc *testCase) prepareTestClient(t *testing.T) (*fake.Clientset, *metricsfake.Clientset, *cmfake.FakeCustomMetricsClient, *emfake.FakeExternalMetricsClient, *scalefake.FakeScaleClient) { 185 namespace := "test-namespace" 186 hpaName := "test-hpa" 187 podNamePrefix := "test-pod" 188 labelSet := map[string]string{"name": podNamePrefix} 189 selector := labels.SelectorFromSet(labelSet).String() 190 191 tc.Lock() 192 193 tc.scaleUpdated = false 194 tc.statusUpdated = false 195 tc.eventCreated = false 196 tc.processed = make(chan string, 100) 197 if tc.CPUCurrent == 0 { 198 tc.computeCPUCurrent() 199 } 200 201 if tc.resource == nil { 202 tc.resource = &fakeResource{ 203 name: "test-rc", 204 apiVersion: "v1", 205 kind: "ReplicationController", 206 } 207 } 208 tc.Unlock() 209 210 fakeClient := &fake.Clientset{} 211 fakeClient.AddReactor("list", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 212 tc.Lock() 213 defer tc.Unlock() 214 var behavior *autoscalingv2.HorizontalPodAutoscalerBehavior 215 if tc.scaleUpRules != nil || tc.scaleDownRules != nil { 216 behavior = &autoscalingv2.HorizontalPodAutoscalerBehavior{ 217 ScaleUp: tc.scaleUpRules, 218 ScaleDown: tc.scaleDownRules, 219 } 220 } 221 hpa := autoscalingv2.HorizontalPodAutoscaler{ 222 ObjectMeta: metav1.ObjectMeta{ 223 Name: hpaName, 224 Namespace: namespace, 225 }, 226 Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ 227 ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ 228 Kind: tc.resource.kind, 229 Name: tc.resource.name, 230 APIVersion: tc.resource.apiVersion, 231 }, 232 MinReplicas: &tc.minReplicas, 233 MaxReplicas: tc.maxReplicas, 234 Behavior: behavior, 235 }, 236 Status: autoscalingv2.HorizontalPodAutoscalerStatus{ 237 CurrentReplicas: tc.specReplicas, 238 DesiredReplicas: tc.specReplicas, 239 LastScaleTime: tc.lastScaleTime, 240 }, 241 } 242 // Initialize default values 243 autoscalingapiv2.SetDefaults_HorizontalPodAutoscalerBehavior(&hpa) 244 245 obj := &autoscalingv2.HorizontalPodAutoscalerList{ 246 Items: []autoscalingv2.HorizontalPodAutoscaler{hpa}, 247 } 248 249 if tc.CPUTarget > 0 { 250 obj.Items[0].Spec.Metrics = []autoscalingv2.MetricSpec{ 251 { 252 Type: autoscalingv2.ResourceMetricSourceType, 253 Resource: &autoscalingv2.ResourceMetricSource{ 254 Name: v1.ResourceCPU, 255 Target: autoscalingv2.MetricTarget{ 256 Type: autoscalingv2.UtilizationMetricType, 257 AverageUtilization: &tc.CPUTarget, 258 }, 259 }, 260 }, 261 } 262 } 263 if len(tc.metricsTarget) > 0 { 264 obj.Items[0].Spec.Metrics = append(obj.Items[0].Spec.Metrics, tc.metricsTarget...) 265 } 266 267 if len(obj.Items[0].Spec.Metrics) == 0 { 268 // manually add in the defaulting logic 269 obj.Items[0].Spec.Metrics = []autoscalingv2.MetricSpec{ 270 { 271 Type: autoscalingv2.ResourceMetricSourceType, 272 Resource: &autoscalingv2.ResourceMetricSource{ 273 Name: v1.ResourceCPU, 274 }, 275 }, 276 } 277 } 278 279 return true, obj, nil 280 }) 281 282 fakeClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { 283 tc.Lock() 284 defer tc.Unlock() 285 286 obj := &v1.PodList{} 287 288 specifiedCPURequests := tc.reportedCPURequests != nil 289 290 numPodsToCreate := int(tc.statusReplicas) 291 if specifiedCPURequests { 292 numPodsToCreate = len(tc.reportedCPURequests) 293 } 294 295 for i := 0; i < numPodsToCreate; i++ { 296 podReadiness := v1.ConditionTrue 297 if tc.reportedPodReadiness != nil { 298 podReadiness = tc.reportedPodReadiness[i] 299 } 300 var podStartTime metav1.Time 301 if tc.reportedPodStartTime != nil { 302 podStartTime = tc.reportedPodStartTime[i] 303 } 304 305 podPhase := v1.PodRunning 306 if tc.reportedPodPhase != nil { 307 podPhase = tc.reportedPodPhase[i] 308 } 309 310 podDeletionTimestamp := false 311 if tc.reportedPodDeletionTimestamp != nil { 312 podDeletionTimestamp = tc.reportedPodDeletionTimestamp[i] 313 } 314 315 podName := fmt.Sprintf("%s-%d", podNamePrefix, i) 316 317 reportedCPURequest := resource.MustParse("1.0") 318 if specifiedCPURequests { 319 reportedCPURequest = tc.reportedCPURequests[i] 320 } 321 322 pod := v1.Pod{ 323 Status: v1.PodStatus{ 324 Phase: podPhase, 325 Conditions: []v1.PodCondition{ 326 { 327 Type: v1.PodReady, 328 Status: podReadiness, 329 LastTransitionTime: podStartTime, 330 }, 331 }, 332 StartTime: &podStartTime, 333 }, 334 ObjectMeta: metav1.ObjectMeta{ 335 Name: podName, 336 Namespace: namespace, 337 Labels: map[string]string{ 338 "name": podNamePrefix, 339 }, 340 }, 341 342 Spec: v1.PodSpec{ 343 Containers: []v1.Container{ 344 { 345 Name: "container1", 346 Resources: v1.ResourceRequirements{ 347 Requests: v1.ResourceList{ 348 v1.ResourceCPU: *resource.NewMilliQuantity(reportedCPURequest.MilliValue()/2, resource.DecimalSI), 349 }, 350 }, 351 }, 352 { 353 Name: "container2", 354 Resources: v1.ResourceRequirements{ 355 Requests: v1.ResourceList{ 356 v1.ResourceCPU: *resource.NewMilliQuantity(reportedCPURequest.MilliValue()/2, resource.DecimalSI), 357 }, 358 }, 359 }, 360 }, 361 }, 362 } 363 if podDeletionTimestamp { 364 pod.DeletionTimestamp = &metav1.Time{Time: time.Now()} 365 } 366 obj.Items = append(obj.Items, pod) 367 } 368 return true, obj, nil 369 }) 370 371 fakeClient.AddReactor("update", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 372 handled, obj, err := func() (handled bool, ret *autoscalingv2.HorizontalPodAutoscaler, err error) { 373 tc.Lock() 374 defer tc.Unlock() 375 376 obj := action.(core.UpdateAction).GetObject().(*autoscalingv2.HorizontalPodAutoscaler) 377 assert.Equal(t, namespace, obj.Namespace, "the HPA namespace should be as expected") 378 assert.Equal(t, hpaName, obj.Name, "the HPA name should be as expected") 379 assert.Equal(t, tc.expectedDesiredReplicas, obj.Status.DesiredReplicas, "the desired replica count reported in the object status should be as expected") 380 if tc.verifyCPUCurrent { 381 if utilization := findCpuUtilization(obj.Status.CurrentMetrics); assert.NotNil(t, utilization, "the reported CPU utilization percentage should be non-nil") { 382 assert.Equal(t, tc.CPUCurrent, *utilization, "the report CPU utilization percentage should be as expected") 383 } 384 } 385 actualConditions := obj.Status.Conditions 386 // TODO: it's ok not to sort these because statusOk 387 // contains all the conditions, so we'll never be appending. 388 // Default to statusOk when missing any specific conditions 389 if tc.expectedConditions == nil { 390 tc.expectedConditions = statusOkWithOverrides() 391 } 392 // clear the message so that we can easily compare 393 for i := range actualConditions { 394 actualConditions[i].Message = "" 395 actualConditions[i].LastTransitionTime = metav1.Time{} 396 } 397 assert.Equal(t, tc.expectedConditions, actualConditions, "the status conditions should have been as expected") 398 tc.statusUpdated = true 399 // Every time we reconcile HPA object we are updating status. 400 return true, obj, nil 401 }() 402 if obj != nil { 403 tc.processed <- obj.Name 404 } 405 return handled, obj, err 406 }) 407 408 fakeScaleClient := &scalefake.FakeScaleClient{} 409 fakeScaleClient.AddReactor("get", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 410 tc.Lock() 411 defer tc.Unlock() 412 413 obj := &autoscalingv1.Scale{ 414 ObjectMeta: metav1.ObjectMeta{ 415 Name: tc.resource.name, 416 Namespace: namespace, 417 }, 418 Spec: autoscalingv1.ScaleSpec{ 419 Replicas: tc.specReplicas, 420 }, 421 Status: autoscalingv1.ScaleStatus{ 422 Replicas: tc.statusReplicas, 423 Selector: selector, 424 }, 425 } 426 return true, obj, nil 427 }) 428 429 fakeScaleClient.AddReactor("get", "deployments", func(action core.Action) (handled bool, ret runtime.Object, err error) { 430 tc.Lock() 431 defer tc.Unlock() 432 433 obj := &autoscalingv1.Scale{ 434 ObjectMeta: metav1.ObjectMeta{ 435 Name: tc.resource.name, 436 Namespace: namespace, 437 }, 438 Spec: autoscalingv1.ScaleSpec{ 439 Replicas: tc.specReplicas, 440 }, 441 Status: autoscalingv1.ScaleStatus{ 442 Replicas: tc.statusReplicas, 443 Selector: selector, 444 }, 445 } 446 return true, obj, nil 447 }) 448 449 fakeScaleClient.AddReactor("get", "replicasets", func(action core.Action) (handled bool, ret runtime.Object, err error) { 450 tc.Lock() 451 defer tc.Unlock() 452 453 obj := &autoscalingv1.Scale{ 454 ObjectMeta: metav1.ObjectMeta{ 455 Name: tc.resource.name, 456 Namespace: namespace, 457 }, 458 Spec: autoscalingv1.ScaleSpec{ 459 Replicas: tc.specReplicas, 460 }, 461 Status: autoscalingv1.ScaleStatus{ 462 Replicas: tc.statusReplicas, 463 Selector: selector, 464 }, 465 } 466 return true, obj, nil 467 }) 468 469 fakeScaleClient.AddReactor("update", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 470 tc.Lock() 471 defer tc.Unlock() 472 473 obj := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale) 474 replicas := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale).Spec.Replicas 475 assert.Equal(t, tc.expectedDesiredReplicas, replicas, "the replica count of the RC should be as expected") 476 tc.scaleUpdated = true 477 return true, obj, nil 478 }) 479 480 fakeScaleClient.AddReactor("update", "deployments", func(action core.Action) (handled bool, ret runtime.Object, err error) { 481 tc.Lock() 482 defer tc.Unlock() 483 484 obj := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale) 485 replicas := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale).Spec.Replicas 486 assert.Equal(t, tc.expectedDesiredReplicas, replicas, "the replica count of the deployment should be as expected") 487 tc.scaleUpdated = true 488 return true, obj, nil 489 }) 490 491 fakeScaleClient.AddReactor("update", "replicasets", func(action core.Action) (handled bool, ret runtime.Object, err error) { 492 tc.Lock() 493 defer tc.Unlock() 494 495 obj := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale) 496 replicas := action.(core.UpdateAction).GetObject().(*autoscalingv1.Scale).Spec.Replicas 497 assert.Equal(t, tc.expectedDesiredReplicas, replicas, "the replica count of the replicaset should be as expected") 498 tc.scaleUpdated = true 499 return true, obj, nil 500 }) 501 502 fakeWatch := watch.NewFake() 503 fakeClient.AddWatchReactor("*", core.DefaultWatchReactor(fakeWatch, nil)) 504 505 fakeMetricsClient := &metricsfake.Clientset{} 506 fakeMetricsClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { 507 tc.Lock() 508 defer tc.Unlock() 509 510 metrics := &metricsapi.PodMetricsList{} 511 for i, cpu := range tc.reportedLevels { 512 // NB: the list reactor actually does label selector filtering for us, 513 // so we have to make sure our results match the label selector 514 podMetric := metricsapi.PodMetrics{ 515 ObjectMeta: metav1.ObjectMeta{ 516 Name: fmt.Sprintf("%s-%d", podNamePrefix, i), 517 Namespace: namespace, 518 Labels: labelSet, 519 }, 520 Timestamp: metav1.Time{Time: time.Now()}, 521 Window: metav1.Duration{Duration: time.Minute}, 522 Containers: []metricsapi.ContainerMetrics{ 523 { 524 Name: "container1", 525 Usage: v1.ResourceList{ 526 v1.ResourceCPU: *resource.NewMilliQuantity( 527 int64(cpu/2), 528 resource.DecimalSI), 529 v1.ResourceMemory: *resource.NewQuantity( 530 int64(1024*1024/2), 531 resource.BinarySI), 532 }, 533 }, 534 { 535 Name: "container2", 536 Usage: v1.ResourceList{ 537 v1.ResourceCPU: *resource.NewMilliQuantity( 538 int64(cpu/2), 539 resource.DecimalSI), 540 v1.ResourceMemory: *resource.NewQuantity( 541 int64(1024*1024/2), 542 resource.BinarySI), 543 }, 544 }, 545 }, 546 } 547 metrics.Items = append(metrics.Items, podMetric) 548 } 549 550 return true, metrics, nil 551 }) 552 553 fakeCMClient := &cmfake.FakeCustomMetricsClient{} 554 fakeCMClient.AddReactor("get", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) { 555 tc.Lock() 556 defer tc.Unlock() 557 558 getForAction, wasGetFor := action.(cmfake.GetForAction) 559 if !wasGetFor { 560 return true, nil, fmt.Errorf("expected a get-for action, got %v instead", action) 561 } 562 563 if getForAction.GetName() == "*" { 564 metrics := &cmapi.MetricValueList{} 565 566 // multiple objects 567 assert.Equal(t, "pods", getForAction.GetResource().Resource, "the type of object that we requested multiple metrics for should have been pods") 568 assert.Equal(t, "qps", getForAction.GetMetricName(), "the metric name requested should have been qps, as specified in the metric spec") 569 570 for i, level := range tc.reportedLevels { 571 podMetric := cmapi.MetricValue{ 572 DescribedObject: v1.ObjectReference{ 573 Kind: "Pod", 574 Name: fmt.Sprintf("%s-%d", podNamePrefix, i), 575 Namespace: namespace, 576 }, 577 Timestamp: metav1.Time{Time: time.Now()}, 578 Metric: cmapi.MetricIdentifier{ 579 Name: "qps", 580 }, 581 Value: *resource.NewMilliQuantity(int64(level), resource.DecimalSI), 582 } 583 metrics.Items = append(metrics.Items, podMetric) 584 } 585 586 return true, metrics, nil 587 } 588 589 name := getForAction.GetName() 590 mapper := testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme) 591 metrics := &cmapi.MetricValueList{} 592 var matchedTarget *autoscalingv2.MetricSpec 593 for i, target := range tc.metricsTarget { 594 if target.Type == autoscalingv2.ObjectMetricSourceType && name == target.Object.DescribedObject.Name { 595 gk := schema.FromAPIVersionAndKind(target.Object.DescribedObject.APIVersion, target.Object.DescribedObject.Kind).GroupKind() 596 mapping, err := mapper.RESTMapping(gk) 597 if err != nil { 598 t.Logf("unable to get mapping for %s: %v", gk.String(), err) 599 continue 600 } 601 groupResource := mapping.Resource.GroupResource() 602 603 if getForAction.GetResource().Resource == groupResource.String() { 604 matchedTarget = &tc.metricsTarget[i] 605 } 606 } 607 } 608 assert.NotNil(t, matchedTarget, "this request should have matched one of the metric specs") 609 assert.Equal(t, "qps", getForAction.GetMetricName(), "the metric name requested should have been qps, as specified in the metric spec") 610 611 metrics.Items = []cmapi.MetricValue{ 612 { 613 DescribedObject: v1.ObjectReference{ 614 Kind: matchedTarget.Object.DescribedObject.Kind, 615 APIVersion: matchedTarget.Object.DescribedObject.APIVersion, 616 Name: name, 617 }, 618 Timestamp: metav1.Time{Time: time.Now()}, 619 Metric: cmapi.MetricIdentifier{ 620 Name: "qps", 621 }, 622 Value: *resource.NewMilliQuantity(int64(tc.reportedLevels[0]), resource.DecimalSI), 623 }, 624 } 625 626 return true, metrics, nil 627 }) 628 629 fakeEMClient := &emfake.FakeExternalMetricsClient{} 630 631 fakeEMClient.AddReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) { 632 tc.Lock() 633 defer tc.Unlock() 634 635 listAction, wasList := action.(core.ListAction) 636 if !wasList { 637 return true, nil, fmt.Errorf("expected a list action, got %v instead", action) 638 } 639 640 metrics := &emapi.ExternalMetricValueList{} 641 642 assert.Equal(t, "qps", listAction.GetResource().Resource, "the metric name requested should have been qps, as specified in the metric spec") 643 644 for _, level := range tc.reportedLevels { 645 metric := emapi.ExternalMetricValue{ 646 Timestamp: metav1.Time{Time: time.Now()}, 647 MetricName: "qps", 648 Value: *resource.NewMilliQuantity(int64(level), resource.DecimalSI), 649 } 650 metrics.Items = append(metrics.Items, metric) 651 } 652 653 return true, metrics, nil 654 }) 655 656 return fakeClient, fakeMetricsClient, fakeCMClient, fakeEMClient, fakeScaleClient 657 } 658 659 func findCpuUtilization(metricStatus []autoscalingv2.MetricStatus) (utilization *int32) { 660 for _, s := range metricStatus { 661 if s.Type != autoscalingv2.ResourceMetricSourceType { 662 continue 663 } 664 if s.Resource == nil { 665 continue 666 } 667 if s.Resource.Name != v1.ResourceCPU { 668 continue 669 } 670 if s.Resource.Current.AverageUtilization == nil { 671 continue 672 } 673 return s.Resource.Current.AverageUtilization 674 } 675 return nil 676 } 677 678 func (tc *testCase) verifyResults(t *testing.T, m *mockMonitor) { 679 tc.Lock() 680 defer tc.Unlock() 681 682 assert.Equal(t, tc.specReplicas != tc.expectedDesiredReplicas, tc.scaleUpdated, "the scale should only be updated if we expected a change in replicas") 683 assert.True(t, tc.statusUpdated, "the status should have been updated") 684 if tc.verifyEvents { 685 assert.Equal(t, tc.specReplicas != tc.expectedDesiredReplicas, tc.eventCreated, "an event should have been created only if we expected a change in replicas") 686 } 687 688 tc.verifyRecordedMetric(t, m) 689 } 690 691 func (tc *testCase) verifyRecordedMetric(t *testing.T, m *mockMonitor) { 692 // First, wait for the reconciliation completed at least once. 693 m.waitUntilRecorded(t) 694 695 assert.Equal(t, tc.expectedReportedReconciliationActionLabel, m.reconciliationActionLabels[0], "the reconciliation action should be recorded in monitor expectedly") 696 assert.Equal(t, tc.expectedReportedReconciliationErrorLabel, m.reconciliationErrorLabels[0], "the reconciliation error should be recorded in monitor expectedly") 697 698 if len(tc.expectedReportedMetricComputationActionLabels) != len(m.metricComputationActionLabels) { 699 t.Fatalf("the metric computation actions for %d types should be recorded, but actually only %d was recorded", len(tc.expectedReportedMetricComputationActionLabels), len(m.metricComputationActionLabels)) 700 } 701 if len(tc.expectedReportedMetricComputationErrorLabels) != len(m.metricComputationErrorLabels) { 702 t.Fatalf("the metric computation errors for %d types should be recorded, but actually only %d was recorded", len(tc.expectedReportedMetricComputationErrorLabels), len(m.metricComputationErrorLabels)) 703 } 704 705 for metricType, l := range tc.expectedReportedMetricComputationActionLabels { 706 _, ok := m.metricComputationActionLabels[metricType] 707 if !ok { 708 t.Fatalf("the metric computation action should be recorded with metricType %s, but actually nothing was recorded", metricType) 709 } 710 assert.Equal(t, l, m.metricComputationActionLabels[metricType][0], "the metric computation action should be recorded in monitor expectedly") 711 } 712 for metricType, l := range tc.expectedReportedMetricComputationErrorLabels { 713 _, ok := m.metricComputationErrorLabels[metricType] 714 if !ok { 715 t.Fatalf("the metric computation error should be recorded with metricType %s, but actually nothing was recorded", metricType) 716 } 717 assert.Equal(t, l, m.metricComputationErrorLabels[metricType][0], "the metric computation error should be recorded in monitor expectedly") 718 } 719 } 720 721 func (tc *testCase) setupController(t *testing.T) (*HorizontalController, informers.SharedInformerFactory) { 722 testClient, testMetricsClient, testCMClient, testEMClient, testScaleClient := tc.prepareTestClient(t) 723 if tc.testClient != nil { 724 testClient = tc.testClient 725 } 726 if tc.testMetricsClient != nil { 727 testMetricsClient = tc.testMetricsClient 728 } 729 if tc.testCMClient != nil { 730 testCMClient = tc.testCMClient 731 } 732 if tc.testEMClient != nil { 733 testEMClient = tc.testEMClient 734 } 735 if tc.testScaleClient != nil { 736 testScaleClient = tc.testScaleClient 737 } 738 metricsClient := metrics.NewRESTMetricsClient( 739 testMetricsClient.MetricsV1beta1(), 740 testCMClient, 741 testEMClient, 742 ) 743 744 eventClient := &fake.Clientset{} 745 eventClient.AddReactor("create", "events", func(action core.Action) (handled bool, ret runtime.Object, err error) { 746 tc.Lock() 747 defer tc.Unlock() 748 749 obj := action.(core.CreateAction).GetObject().(*v1.Event) 750 if tc.verifyEvents { 751 switch obj.Reason { 752 case "SuccessfulRescale": 753 assert.Equal(t, fmt.Sprintf("New size: %d; reason: cpu resource utilization (percentage of request) above target", tc.expectedDesiredReplicas), obj.Message) 754 case "DesiredReplicasComputed": 755 assert.Equal(t, fmt.Sprintf( 756 "Computed the desired num of replicas: %d (avgCPUutil: %d, current replicas: %d)", 757 tc.expectedDesiredReplicas, 758 (int64(tc.reportedLevels[0])*100)/tc.reportedCPURequests[0].MilliValue(), tc.specReplicas), obj.Message) 759 default: 760 assert.False(t, true, fmt.Sprintf("Unexpected event: %s / %s", obj.Reason, obj.Message)) 761 } 762 } 763 tc.eventCreated = true 764 return true, obj, nil 765 }) 766 767 informerFactory := informers.NewSharedInformerFactory(testClient, controller.NoResyncPeriodFunc()) 768 defaultDownscalestabilizationWindow := 5 * time.Minute 769 770 tCtx := ktesting.Init(t) 771 hpaController := NewHorizontalController( 772 tCtx, 773 eventClient.CoreV1(), 774 testScaleClient, 775 testClient.AutoscalingV2(), 776 testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme), 777 metricsClient, 778 informerFactory.Autoscaling().V2().HorizontalPodAutoscalers(), 779 informerFactory.Core().V1().Pods(), 780 100*time.Millisecond, // we need non-zero resync period to avoid race conditions 781 defaultDownscalestabilizationWindow, 782 defaultTestingTolerance, 783 defaultTestingCPUInitializationPeriod, 784 defaultTestingDelayOfInitialReadinessStatus, 785 ) 786 hpaController.hpaListerSynced = alwaysReady 787 if tc.recommendations != nil { 788 hpaController.recommendations["test-namespace/test-hpa"] = tc.recommendations 789 } 790 if tc.hpaSelectors != nil { 791 hpaController.hpaSelectors = tc.hpaSelectors 792 } 793 794 hpaController.monitor = newMockMonitor() 795 return hpaController, informerFactory 796 } 797 798 func hotCPUCreationTime() metav1.Time { 799 return metav1.Time{Time: time.Now()} 800 } 801 802 func coolCPUCreationTime() metav1.Time { 803 return metav1.Time{Time: time.Now().Add(-3 * time.Minute)} 804 } 805 806 func (tc *testCase) runTestWithController(t *testing.T, hpaController *HorizontalController, informerFactory informers.SharedInformerFactory) { 807 ctx, cancel := context.WithCancel(context.Background()) 808 defer cancel() 809 informerFactory.Start(ctx.Done()) 810 go hpaController.Run(ctx, 5) 811 812 tc.Lock() 813 shouldWait := tc.verifyEvents 814 tc.Unlock() 815 816 if shouldWait { 817 // We need to wait for events to be broadcasted (sleep for longer than record.sleepDuration). 818 timeoutTime := time.Now().Add(2 * time.Second) 819 for now := time.Now(); timeoutTime.After(now); now = time.Now() { 820 sleepUntil := timeoutTime.Sub(now) 821 select { 822 case <-tc.processed: 823 // drain the chan of any sent events to keep it from filling before the timeout 824 case <-time.After(sleepUntil): 825 // timeout reached, ready to verifyResults 826 } 827 } 828 } else { 829 // Wait for HPA to be processed. 830 <-tc.processed 831 } 832 m, ok := hpaController.monitor.(*mockMonitor) 833 if !ok { 834 t.Fatalf("test HPA controller should have mockMonitor, but actually not") 835 } 836 tc.verifyResults(t, m) 837 } 838 839 func (tc *testCase) runTest(t *testing.T) { 840 hpaController, informerFactory := tc.setupController(t) 841 tc.runTestWithController(t, hpaController, informerFactory) 842 } 843 844 // mockMonitor implements monitor.Monitor interface. 845 // It records which results are observed in slices. 846 type mockMonitor struct { 847 sync.RWMutex 848 reconciliationActionLabels []monitor.ActionLabel 849 reconciliationErrorLabels []monitor.ErrorLabel 850 851 metricComputationActionLabels map[autoscalingv2.MetricSourceType][]monitor.ActionLabel 852 metricComputationErrorLabels map[autoscalingv2.MetricSourceType][]monitor.ErrorLabel 853 } 854 855 func newMockMonitor() *mockMonitor { 856 return &mockMonitor{ 857 metricComputationActionLabels: make(map[autoscalingv2.MetricSourceType][]monitor.ActionLabel), 858 metricComputationErrorLabels: make(map[autoscalingv2.MetricSourceType][]monitor.ErrorLabel), 859 } 860 } 861 862 func (m *mockMonitor) ObserveReconciliationResult(action monitor.ActionLabel, err monitor.ErrorLabel, _ time.Duration) { 863 m.Lock() 864 defer m.Unlock() 865 m.reconciliationActionLabels = append(m.reconciliationActionLabels, action) 866 m.reconciliationErrorLabels = append(m.reconciliationErrorLabels, err) 867 } 868 869 func (m *mockMonitor) ObserveMetricComputationResult(action monitor.ActionLabel, err monitor.ErrorLabel, duration time.Duration, metricType autoscalingv2.MetricSourceType) { 870 m.Lock() 871 defer m.Unlock() 872 873 m.metricComputationActionLabels[metricType] = append(m.metricComputationActionLabels[metricType], action) 874 m.metricComputationErrorLabels[metricType] = append(m.metricComputationErrorLabels[metricType], err) 875 } 876 877 // waitUntilRecorded waits for the HPA controller to reconcile at least once. 878 func (m *mockMonitor) waitUntilRecorded(t *testing.T) { 879 if err := wait.Poll(20*time.Millisecond, 100*time.Millisecond, func() (done bool, err error) { 880 m.RWMutex.RLock() 881 defer m.RWMutex.RUnlock() 882 if len(m.reconciliationActionLabels) == 0 || len(m.reconciliationErrorLabels) == 0 { 883 return false, nil 884 } 885 return true, nil 886 }); err != nil { 887 t.Fatalf("no reconciliation is recorded in the monitor, len(monitor.reconciliationActionLabels)=%v len(monitor.reconciliationErrorLabels)=%v ", len(m.reconciliationActionLabels), len(m.reconciliationErrorLabels)) 888 } 889 } 890 891 func TestScaleUp(t *testing.T) { 892 tc := testCase{ 893 minReplicas: 2, 894 maxReplicas: 6, 895 specReplicas: 3, 896 statusReplicas: 3, 897 expectedDesiredReplicas: 5, 898 CPUTarget: 30, 899 verifyCPUCurrent: true, 900 reportedLevels: []uint64{300, 500, 700}, 901 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 902 useMetricsAPI: true, 903 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 904 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 905 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 906 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 907 }, 908 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 909 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 910 }, 911 } 912 tc.runTest(t) 913 } 914 915 func TestScaleUpContainer(t *testing.T) { 916 tc := testCase{ 917 minReplicas: 2, 918 maxReplicas: 6, 919 specReplicas: 3, 920 statusReplicas: 3, 921 expectedDesiredReplicas: 5, 922 metricsTarget: []autoscalingv2.MetricSpec{{ 923 Type: autoscalingv2.ContainerResourceMetricSourceType, 924 ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ 925 Name: v1.ResourceCPU, 926 Target: autoscalingv2.MetricTarget{ 927 Type: autoscalingv2.UtilizationMetricType, 928 AverageUtilization: pointer.Int32(30), 929 }, 930 Container: "container1", 931 }, 932 }}, 933 reportedLevels: []uint64{300, 500, 700}, 934 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 935 useMetricsAPI: true, 936 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 937 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 938 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 939 autoscalingv2.ContainerResourceMetricSourceType: monitor.ActionLabelScaleUp, 940 }, 941 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 942 autoscalingv2.ContainerResourceMetricSourceType: monitor.ErrorLabelNone, 943 }, 944 } 945 tc.runTest(t) 946 } 947 948 func TestScaleUpUnreadyLessScale(t *testing.T) { 949 tc := testCase{ 950 minReplicas: 2, 951 maxReplicas: 6, 952 specReplicas: 3, 953 statusReplicas: 3, 954 expectedDesiredReplicas: 4, 955 CPUTarget: 30, 956 CPUCurrent: 60, 957 verifyCPUCurrent: true, 958 reportedLevels: []uint64{300, 500, 700}, 959 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 960 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionTrue}, 961 useMetricsAPI: true, 962 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 963 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 964 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 965 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 966 }, 967 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 968 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 969 }, 970 } 971 tc.runTest(t) 972 } 973 974 func TestScaleUpHotCpuLessScale(t *testing.T) { 975 tc := testCase{ 976 minReplicas: 2, 977 maxReplicas: 6, 978 specReplicas: 3, 979 statusReplicas: 3, 980 expectedDesiredReplicas: 4, 981 CPUTarget: 30, 982 CPUCurrent: 60, 983 verifyCPUCurrent: true, 984 reportedLevels: []uint64{300, 500, 700}, 985 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 986 reportedPodStartTime: []metav1.Time{hotCPUCreationTime(), coolCPUCreationTime(), coolCPUCreationTime()}, 987 useMetricsAPI: true, 988 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 989 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 990 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 991 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 992 }, 993 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 994 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 995 }, 996 } 997 tc.runTest(t) 998 } 999 1000 func TestScaleUpUnreadyNoScale(t *testing.T) { 1001 tc := testCase{ 1002 minReplicas: 2, 1003 maxReplicas: 6, 1004 specReplicas: 3, 1005 statusReplicas: 3, 1006 expectedDesiredReplicas: 3, 1007 CPUTarget: 30, 1008 CPUCurrent: 40, 1009 verifyCPUCurrent: true, 1010 reportedLevels: []uint64{400, 500, 700}, 1011 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1012 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse}, 1013 useMetricsAPI: true, 1014 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 1015 Type: autoscalingv2.AbleToScale, 1016 Status: v1.ConditionTrue, 1017 Reason: "ReadyForNewScale", 1018 }), 1019 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 1020 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1021 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1022 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelNone, 1023 }, 1024 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1025 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1026 }, 1027 } 1028 tc.runTest(t) 1029 } 1030 1031 func TestScaleUpHotCpuNoScale(t *testing.T) { 1032 tc := testCase{ 1033 minReplicas: 2, 1034 maxReplicas: 6, 1035 specReplicas: 3, 1036 statusReplicas: 3, 1037 expectedDesiredReplicas: 3, 1038 CPUTarget: 30, 1039 CPUCurrent: 40, 1040 verifyCPUCurrent: true, 1041 reportedLevels: []uint64{400, 500, 700}, 1042 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1043 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse}, 1044 reportedPodStartTime: []metav1.Time{coolCPUCreationTime(), hotCPUCreationTime(), hotCPUCreationTime()}, 1045 useMetricsAPI: true, 1046 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 1047 Type: autoscalingv2.AbleToScale, 1048 Status: v1.ConditionTrue, 1049 Reason: "ReadyForNewScale", 1050 }), 1051 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 1052 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1053 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1054 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelNone, 1055 }, 1056 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1057 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1058 }, 1059 } 1060 tc.runTest(t) 1061 } 1062 1063 func TestScaleUpIgnoresFailedPods(t *testing.T) { 1064 tc := testCase{ 1065 minReplicas: 2, 1066 maxReplicas: 6, 1067 specReplicas: 2, 1068 statusReplicas: 2, 1069 expectedDesiredReplicas: 4, 1070 CPUTarget: 30, 1071 CPUCurrent: 60, 1072 verifyCPUCurrent: true, 1073 reportedLevels: []uint64{500, 700}, 1074 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1075 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse}, 1076 reportedPodPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed}, 1077 useMetricsAPI: true, 1078 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1079 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1080 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1081 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 1082 }, 1083 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1084 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1085 }, 1086 } 1087 tc.runTest(t) 1088 } 1089 1090 func TestScaleUpIgnoresDeletionPods(t *testing.T) { 1091 tc := testCase{ 1092 minReplicas: 2, 1093 maxReplicas: 6, 1094 specReplicas: 2, 1095 statusReplicas: 2, 1096 expectedDesiredReplicas: 4, 1097 CPUTarget: 30, 1098 CPUCurrent: 60, 1099 verifyCPUCurrent: true, 1100 reportedLevels: []uint64{500, 700}, 1101 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1102 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse}, 1103 reportedPodPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning}, 1104 reportedPodDeletionTimestamp: []bool{false, false, true, true}, 1105 useMetricsAPI: true, 1106 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1107 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1108 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1109 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 1110 }, 1111 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1112 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1113 }, 1114 } 1115 tc.runTest(t) 1116 } 1117 1118 func TestScaleUpDeployment(t *testing.T) { 1119 tc := testCase{ 1120 minReplicas: 2, 1121 maxReplicas: 6, 1122 specReplicas: 3, 1123 statusReplicas: 3, 1124 expectedDesiredReplicas: 5, 1125 CPUTarget: 30, 1126 verifyCPUCurrent: true, 1127 reportedLevels: []uint64{300, 500, 700}, 1128 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1129 useMetricsAPI: true, 1130 resource: &fakeResource{ 1131 name: "test-dep", 1132 apiVersion: "apps/v1", 1133 kind: "Deployment", 1134 }, 1135 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1136 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1137 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1138 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 1139 }, 1140 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1141 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1142 }, 1143 } 1144 tc.runTest(t) 1145 } 1146 1147 func TestScaleUpReplicaSet(t *testing.T) { 1148 tc := testCase{ 1149 minReplicas: 2, 1150 maxReplicas: 6, 1151 specReplicas: 3, 1152 statusReplicas: 3, 1153 expectedDesiredReplicas: 5, 1154 CPUTarget: 30, 1155 verifyCPUCurrent: true, 1156 reportedLevels: []uint64{300, 500, 700}, 1157 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1158 useMetricsAPI: true, 1159 resource: &fakeResource{ 1160 name: "test-replicaset", 1161 apiVersion: "apps/v1", 1162 kind: "ReplicaSet", 1163 }, 1164 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1165 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1166 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1167 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 1168 }, 1169 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1170 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1171 }, 1172 } 1173 tc.runTest(t) 1174 } 1175 1176 func TestScaleUpCM(t *testing.T) { 1177 averageValue := resource.MustParse("15.0") 1178 tc := testCase{ 1179 minReplicas: 2, 1180 maxReplicas: 6, 1181 specReplicas: 3, 1182 statusReplicas: 3, 1183 expectedDesiredReplicas: 4, 1184 CPUTarget: 0, 1185 metricsTarget: []autoscalingv2.MetricSpec{ 1186 { 1187 Type: autoscalingv2.PodsMetricSourceType, 1188 Pods: &autoscalingv2.PodsMetricSource{ 1189 Metric: autoscalingv2.MetricIdentifier{ 1190 Name: "qps", 1191 }, 1192 Target: autoscalingv2.MetricTarget{ 1193 Type: autoscalingv2.AverageValueMetricType, 1194 AverageValue: &averageValue, 1195 }, 1196 }, 1197 }, 1198 }, 1199 reportedLevels: []uint64{20000, 10000, 30000}, 1200 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1201 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1202 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1203 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1204 autoscalingv2.PodsMetricSourceType: monitor.ActionLabelScaleUp, 1205 }, 1206 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1207 autoscalingv2.PodsMetricSourceType: monitor.ErrorLabelNone, 1208 }, 1209 } 1210 tc.runTest(t) 1211 } 1212 1213 func TestScaleUpCMUnreadyAndHotCpuNoLessScale(t *testing.T) { 1214 averageValue := resource.MustParse("15.0") 1215 tc := testCase{ 1216 minReplicas: 2, 1217 maxReplicas: 6, 1218 specReplicas: 3, 1219 statusReplicas: 3, 1220 expectedDesiredReplicas: 6, 1221 CPUTarget: 0, 1222 metricsTarget: []autoscalingv2.MetricSpec{ 1223 { 1224 Type: autoscalingv2.PodsMetricSourceType, 1225 Pods: &autoscalingv2.PodsMetricSource{ 1226 Metric: autoscalingv2.MetricIdentifier{ 1227 Name: "qps", 1228 }, 1229 Target: autoscalingv2.MetricTarget{ 1230 Type: autoscalingv2.AverageValueMetricType, 1231 AverageValue: &averageValue, 1232 }, 1233 }, 1234 }, 1235 }, 1236 reportedLevels: []uint64{50000, 10000, 30000}, 1237 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse}, 1238 reportedPodStartTime: []metav1.Time{coolCPUCreationTime(), coolCPUCreationTime(), hotCPUCreationTime()}, 1239 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1240 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1241 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1242 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1243 autoscalingv2.PodsMetricSourceType: monitor.ActionLabelScaleUp, 1244 }, 1245 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1246 autoscalingv2.PodsMetricSourceType: monitor.ErrorLabelNone, 1247 }, 1248 } 1249 tc.runTest(t) 1250 } 1251 1252 func TestScaleUpCMUnreadyandCpuHot(t *testing.T) { 1253 averageValue := resource.MustParse("15.0") 1254 tc := testCase{ 1255 minReplicas: 2, 1256 maxReplicas: 6, 1257 specReplicas: 3, 1258 statusReplicas: 3, 1259 expectedDesiredReplicas: 6, 1260 CPUTarget: 0, 1261 metricsTarget: []autoscalingv2.MetricSpec{ 1262 { 1263 Type: autoscalingv2.PodsMetricSourceType, 1264 Pods: &autoscalingv2.PodsMetricSource{ 1265 Metric: autoscalingv2.MetricIdentifier{ 1266 Name: "qps", 1267 }, 1268 Target: autoscalingv2.MetricTarget{ 1269 Type: autoscalingv2.AverageValueMetricType, 1270 AverageValue: &averageValue, 1271 }, 1272 }, 1273 }, 1274 }, 1275 reportedLevels: []uint64{50000, 15000, 30000}, 1276 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionFalse, v1.ConditionTrue, v1.ConditionFalse}, 1277 reportedPodStartTime: []metav1.Time{hotCPUCreationTime(), coolCPUCreationTime(), hotCPUCreationTime()}, 1278 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1279 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 1280 Type: autoscalingv2.AbleToScale, 1281 Status: v1.ConditionTrue, 1282 Reason: "SucceededRescale", 1283 }, autoscalingv2.HorizontalPodAutoscalerCondition{ 1284 Type: autoscalingv2.ScalingLimited, 1285 Status: v1.ConditionTrue, 1286 Reason: "TooManyReplicas", 1287 }), 1288 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1289 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1290 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1291 autoscalingv2.PodsMetricSourceType: monitor.ActionLabelScaleUp, 1292 }, 1293 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1294 autoscalingv2.PodsMetricSourceType: monitor.ErrorLabelNone, 1295 }, 1296 } 1297 tc.runTest(t) 1298 } 1299 1300 func TestScaleUpHotCpuNoScaleWouldScaleDown(t *testing.T) { 1301 averageValue := resource.MustParse("15.0") 1302 tc := testCase{ 1303 minReplicas: 2, 1304 maxReplicas: 6, 1305 specReplicas: 3, 1306 statusReplicas: 3, 1307 expectedDesiredReplicas: 6, 1308 CPUTarget: 0, 1309 metricsTarget: []autoscalingv2.MetricSpec{ 1310 { 1311 Type: autoscalingv2.PodsMetricSourceType, 1312 Pods: &autoscalingv2.PodsMetricSource{ 1313 Metric: autoscalingv2.MetricIdentifier{ 1314 Name: "qps", 1315 }, 1316 Target: autoscalingv2.MetricTarget{ 1317 Type: autoscalingv2.AverageValueMetricType, 1318 AverageValue: &averageValue, 1319 }, 1320 }, 1321 }, 1322 }, 1323 reportedLevels: []uint64{50000, 15000, 30000}, 1324 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1325 reportedPodStartTime: []metav1.Time{hotCPUCreationTime(), coolCPUCreationTime(), hotCPUCreationTime()}, 1326 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 1327 Type: autoscalingv2.AbleToScale, 1328 Status: v1.ConditionTrue, 1329 Reason: "SucceededRescale", 1330 }, autoscalingv2.HorizontalPodAutoscalerCondition{ 1331 Type: autoscalingv2.ScalingLimited, 1332 Status: v1.ConditionTrue, 1333 Reason: "TooManyReplicas", 1334 }), 1335 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1336 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1337 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1338 autoscalingv2.PodsMetricSourceType: monitor.ActionLabelScaleUp, 1339 }, 1340 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1341 autoscalingv2.PodsMetricSourceType: monitor.ErrorLabelNone, 1342 }, 1343 } 1344 tc.runTest(t) 1345 } 1346 1347 func TestScaleUpCMObject(t *testing.T) { 1348 targetValue := resource.MustParse("15.0") 1349 tc := testCase{ 1350 minReplicas: 2, 1351 maxReplicas: 6, 1352 specReplicas: 3, 1353 statusReplicas: 3, 1354 expectedDesiredReplicas: 4, 1355 CPUTarget: 0, 1356 metricsTarget: []autoscalingv2.MetricSpec{ 1357 { 1358 Type: autoscalingv2.ObjectMetricSourceType, 1359 Object: &autoscalingv2.ObjectMetricSource{ 1360 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 1361 APIVersion: "apps/v1", 1362 Kind: "Deployment", 1363 Name: "some-deployment", 1364 }, 1365 Metric: autoscalingv2.MetricIdentifier{ 1366 Name: "qps", 1367 }, 1368 Target: autoscalingv2.MetricTarget{ 1369 Type: autoscalingv2.ValueMetricType, 1370 Value: &targetValue, 1371 }, 1372 }, 1373 }, 1374 }, 1375 reportedLevels: []uint64{20000}, 1376 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1377 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1378 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1379 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelScaleUp, 1380 }, 1381 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1382 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 1383 }, 1384 } 1385 tc.runTest(t) 1386 } 1387 1388 func TestScaleUpFromZeroCMObject(t *testing.T) { 1389 targetValue := resource.MustParse("15.0") 1390 tc := testCase{ 1391 minReplicas: 0, 1392 maxReplicas: 6, 1393 specReplicas: 0, 1394 statusReplicas: 0, 1395 expectedDesiredReplicas: 2, 1396 CPUTarget: 0, 1397 metricsTarget: []autoscalingv2.MetricSpec{ 1398 { 1399 Type: autoscalingv2.ObjectMetricSourceType, 1400 Object: &autoscalingv2.ObjectMetricSource{ 1401 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 1402 APIVersion: "apps/v1", 1403 Kind: "Deployment", 1404 Name: "some-deployment", 1405 }, 1406 Metric: autoscalingv2.MetricIdentifier{ 1407 Name: "qps", 1408 }, 1409 Target: autoscalingv2.MetricTarget{ 1410 Type: autoscalingv2.ValueMetricType, 1411 Value: &targetValue, 1412 }, 1413 }, 1414 }, 1415 }, 1416 reportedLevels: []uint64{20000}, 1417 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1418 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1419 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1420 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelScaleUp, 1421 }, 1422 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1423 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 1424 }, 1425 } 1426 tc.runTest(t) 1427 } 1428 1429 func TestScaleUpFromZeroIgnoresToleranceCMObject(t *testing.T) { 1430 targetValue := resource.MustParse("1.0") 1431 tc := testCase{ 1432 minReplicas: 0, 1433 maxReplicas: 6, 1434 specReplicas: 0, 1435 statusReplicas: 0, 1436 expectedDesiredReplicas: 1, 1437 CPUTarget: 0, 1438 metricsTarget: []autoscalingv2.MetricSpec{ 1439 { 1440 Type: autoscalingv2.ObjectMetricSourceType, 1441 Object: &autoscalingv2.ObjectMetricSource{ 1442 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 1443 APIVersion: "apps/v1", 1444 Kind: "Deployment", 1445 Name: "some-deployment", 1446 }, 1447 Metric: autoscalingv2.MetricIdentifier{ 1448 Name: "qps", 1449 }, 1450 Target: autoscalingv2.MetricTarget{ 1451 Type: autoscalingv2.ValueMetricType, 1452 Value: &targetValue, 1453 }, 1454 }, 1455 }, 1456 }, 1457 reportedLevels: []uint64{1000}, 1458 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1459 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1460 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1461 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelScaleUp, 1462 }, 1463 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1464 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 1465 }, 1466 } 1467 tc.runTest(t) 1468 } 1469 1470 func TestScaleUpPerPodCMObject(t *testing.T) { 1471 targetAverageValue := resource.MustParse("10.0") 1472 tc := testCase{ 1473 minReplicas: 2, 1474 maxReplicas: 6, 1475 specReplicas: 3, 1476 statusReplicas: 3, 1477 expectedDesiredReplicas: 4, 1478 CPUTarget: 0, 1479 metricsTarget: []autoscalingv2.MetricSpec{ 1480 { 1481 Type: autoscalingv2.ObjectMetricSourceType, 1482 Object: &autoscalingv2.ObjectMetricSource{ 1483 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 1484 APIVersion: "apps/v1", 1485 Kind: "Deployment", 1486 Name: "some-deployment", 1487 }, 1488 Metric: autoscalingv2.MetricIdentifier{ 1489 Name: "qps", 1490 }, 1491 Target: autoscalingv2.MetricTarget{ 1492 Type: autoscalingv2.AverageValueMetricType, 1493 AverageValue: &targetAverageValue, 1494 }, 1495 }, 1496 }, 1497 }, 1498 reportedLevels: []uint64{40000}, 1499 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1500 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1501 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1502 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelScaleUp, 1503 }, 1504 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1505 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 1506 }, 1507 } 1508 tc.runTest(t) 1509 } 1510 1511 func TestScaleUpCMExternal(t *testing.T) { 1512 tc := testCase{ 1513 minReplicas: 2, 1514 maxReplicas: 6, 1515 specReplicas: 3, 1516 statusReplicas: 3, 1517 expectedDesiredReplicas: 4, 1518 metricsTarget: []autoscalingv2.MetricSpec{ 1519 { 1520 Type: autoscalingv2.ExternalMetricSourceType, 1521 External: &autoscalingv2.ExternalMetricSource{ 1522 Metric: autoscalingv2.MetricIdentifier{ 1523 Name: "qps", 1524 Selector: &metav1.LabelSelector{}, 1525 }, 1526 Target: autoscalingv2.MetricTarget{ 1527 Type: autoscalingv2.ValueMetricType, 1528 Value: resource.NewMilliQuantity(6666, resource.DecimalSI), 1529 }, 1530 }, 1531 }, 1532 }, 1533 reportedLevels: []uint64{8600}, 1534 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1535 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1536 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1537 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelScaleUp, 1538 }, 1539 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1540 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelNone, 1541 }, 1542 } 1543 tc.runTest(t) 1544 } 1545 1546 func TestScaleUpPerPodCMExternal(t *testing.T) { 1547 tc := testCase{ 1548 minReplicas: 2, 1549 maxReplicas: 6, 1550 specReplicas: 3, 1551 statusReplicas: 3, 1552 expectedDesiredReplicas: 4, 1553 metricsTarget: []autoscalingv2.MetricSpec{ 1554 { 1555 Type: autoscalingv2.ExternalMetricSourceType, 1556 External: &autoscalingv2.ExternalMetricSource{ 1557 Metric: autoscalingv2.MetricIdentifier{ 1558 Name: "qps", 1559 Selector: &metav1.LabelSelector{}, 1560 }, 1561 Target: autoscalingv2.MetricTarget{ 1562 Type: autoscalingv2.AverageValueMetricType, 1563 AverageValue: resource.NewMilliQuantity(2222, resource.DecimalSI), 1564 }, 1565 }, 1566 }, 1567 }, 1568 reportedLevels: []uint64{8600}, 1569 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1570 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1571 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1572 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelScaleUp, 1573 }, 1574 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1575 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelNone, 1576 }, 1577 } 1578 tc.runTest(t) 1579 } 1580 1581 func TestScaleDown(t *testing.T) { 1582 tc := testCase{ 1583 minReplicas: 2, 1584 maxReplicas: 6, 1585 specReplicas: 5, 1586 statusReplicas: 5, 1587 expectedDesiredReplicas: 3, 1588 CPUTarget: 50, 1589 verifyCPUCurrent: true, 1590 reportedLevels: []uint64{100, 300, 500, 250, 250}, 1591 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1592 useMetricsAPI: true, 1593 recommendations: []timestampedRecommendation{}, 1594 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 1595 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1596 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1597 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 1598 }, 1599 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1600 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1601 }, 1602 } 1603 tc.runTest(t) 1604 } 1605 1606 func TestScaleDownContainerResource(t *testing.T) { 1607 tc := testCase{ 1608 minReplicas: 2, 1609 maxReplicas: 6, 1610 specReplicas: 5, 1611 statusReplicas: 5, 1612 expectedDesiredReplicas: 3, 1613 reportedLevels: []uint64{100, 300, 500, 250, 250}, 1614 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1615 metricsTarget: []autoscalingv2.MetricSpec{{ 1616 Type: autoscalingv2.ContainerResourceMetricSourceType, 1617 ContainerResource: &autoscalingv2.ContainerResourceMetricSource{ 1618 Container: "container2", 1619 Name: v1.ResourceCPU, 1620 Target: autoscalingv2.MetricTarget{ 1621 Type: autoscalingv2.UtilizationMetricType, 1622 AverageUtilization: pointer.Int32(50), 1623 }, 1624 }, 1625 }}, 1626 useMetricsAPI: true, 1627 recommendations: []timestampedRecommendation{}, 1628 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 1629 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1630 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1631 autoscalingv2.ContainerResourceMetricSourceType: monitor.ActionLabelScaleDown, 1632 }, 1633 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1634 autoscalingv2.ContainerResourceMetricSourceType: monitor.ErrorLabelNone, 1635 }, 1636 } 1637 tc.runTest(t) 1638 } 1639 1640 func TestScaleDownWithScalingRules(t *testing.T) { 1641 tc := testCase{ 1642 minReplicas: 2, 1643 maxReplicas: 6, 1644 scaleUpRules: generateScalingRules(0, 0, 100, 15, 30), 1645 specReplicas: 5, 1646 statusReplicas: 5, 1647 expectedDesiredReplicas: 3, 1648 CPUTarget: 50, 1649 verifyCPUCurrent: true, 1650 reportedLevels: []uint64{100, 300, 500, 250, 250}, 1651 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1652 useMetricsAPI: true, 1653 recommendations: []timestampedRecommendation{}, 1654 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 1655 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1656 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1657 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 1658 }, 1659 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1660 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1661 }, 1662 } 1663 tc.runTest(t) 1664 } 1665 1666 func TestScaleUpOneMetricInvalid(t *testing.T) { 1667 tc := testCase{ 1668 minReplicas: 2, 1669 maxReplicas: 6, 1670 specReplicas: 3, 1671 statusReplicas: 3, 1672 expectedDesiredReplicas: 4, 1673 CPUTarget: 30, 1674 verifyCPUCurrent: true, 1675 metricsTarget: []autoscalingv2.MetricSpec{ 1676 { 1677 Type: "CheddarCheese", 1678 }, 1679 }, 1680 reportedLevels: []uint64{300, 400, 500}, 1681 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1682 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1683 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 1684 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1685 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 1686 // Actually, such an invalid type should be validated in the kube-apiserver and invalid metric type shouldn't be recorded. 1687 "CheddarCheese": monitor.ActionLabelNone, 1688 }, 1689 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1690 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1691 // Actually, such an invalid type should be validated in the kube-apiserver and invalid metric type shouldn't be recorded. 1692 "CheddarCheese": monitor.ErrorLabelSpec, 1693 }, 1694 } 1695 tc.runTest(t) 1696 } 1697 1698 func TestScaleUpFromZeroOneMetricInvalid(t *testing.T) { 1699 tc := testCase{ 1700 minReplicas: 0, 1701 maxReplicas: 6, 1702 specReplicas: 0, 1703 statusReplicas: 0, 1704 expectedDesiredReplicas: 4, 1705 CPUTarget: 30, 1706 verifyCPUCurrent: true, 1707 metricsTarget: []autoscalingv2.MetricSpec{ 1708 { 1709 Type: "CheddarCheese", 1710 }, 1711 }, 1712 reportedLevels: []uint64{300, 400, 500}, 1713 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1714 recommendations: []timestampedRecommendation{}, 1715 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 1716 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 1717 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1718 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 1719 // Actually, such an invalid type should be validated in the kube-apiserver and invalid metric type shouldn't be recorded. 1720 "CheddarCheese": monitor.ActionLabelNone, 1721 }, 1722 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1723 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1724 // Actually, such an invalid type should be validated in the kube-apiserver and invalid metric type shouldn't be recorded. 1725 "CheddarCheese": monitor.ErrorLabelSpec, 1726 }, 1727 } 1728 tc.runTest(t) 1729 } 1730 1731 func TestScaleUpBothMetricsEmpty(t *testing.T) { // Switch to missing 1732 tc := testCase{ 1733 minReplicas: 2, 1734 maxReplicas: 6, 1735 specReplicas: 3, 1736 statusReplicas: 3, 1737 expectedDesiredReplicas: 3, 1738 CPUTarget: 0, 1739 metricsTarget: []autoscalingv2.MetricSpec{ 1740 { 1741 Type: "CheddarCheese", 1742 }, 1743 }, 1744 reportedLevels: []uint64{}, 1745 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1746 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 1747 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"}, 1748 {Type: autoscalingv2.ScalingActive, Status: v1.ConditionFalse, Reason: "InvalidMetricSourceType"}, 1749 }, 1750 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 1751 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 1752 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1753 // Actually, such an invalid type should be validated in the kube-apiserver and invalid metric type shouldn't be recorded. 1754 "CheddarCheese": monitor.ActionLabelNone, 1755 }, 1756 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1757 // Actually, such an invalid type should be validated in the kube-apiserver and invalid metric type shouldn't be recorded. 1758 "CheddarCheese": monitor.ErrorLabelSpec, 1759 }, 1760 } 1761 tc.runTest(t) 1762 } 1763 1764 func TestScaleDownStabilizeInitialSize(t *testing.T) { 1765 tc := testCase{ 1766 minReplicas: 2, 1767 maxReplicas: 6, 1768 specReplicas: 5, 1769 statusReplicas: 5, 1770 expectedDesiredReplicas: 5, 1771 CPUTarget: 50, 1772 verifyCPUCurrent: true, 1773 reportedLevels: []uint64{100, 300, 500, 250, 250}, 1774 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1775 useMetricsAPI: true, 1776 recommendations: nil, 1777 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 1778 Type: autoscalingv2.AbleToScale, 1779 Status: v1.ConditionTrue, 1780 Reason: "ReadyForNewScale", 1781 }, autoscalingv2.HorizontalPodAutoscalerCondition{ 1782 Type: autoscalingv2.AbleToScale, 1783 Status: v1.ConditionTrue, 1784 Reason: "ScaleDownStabilized", 1785 }), 1786 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 1787 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1788 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1789 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 1790 }, 1791 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1792 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 1793 }, 1794 } 1795 tc.runTest(t) 1796 } 1797 1798 func TestScaleDownCM(t *testing.T) { 1799 averageValue := resource.MustParse("20.0") 1800 tc := testCase{ 1801 minReplicas: 2, 1802 maxReplicas: 6, 1803 specReplicas: 5, 1804 statusReplicas: 5, 1805 expectedDesiredReplicas: 3, 1806 CPUTarget: 0, 1807 metricsTarget: []autoscalingv2.MetricSpec{ 1808 { 1809 Type: autoscalingv2.PodsMetricSourceType, 1810 Pods: &autoscalingv2.PodsMetricSource{ 1811 Metric: autoscalingv2.MetricIdentifier{ 1812 Name: "qps", 1813 }, 1814 Target: autoscalingv2.MetricTarget{ 1815 Type: autoscalingv2.AverageValueMetricType, 1816 AverageValue: &averageValue, 1817 }, 1818 }, 1819 }, 1820 }, 1821 reportedLevels: []uint64{12000, 12000, 12000, 12000, 12000}, 1822 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1823 recommendations: []timestampedRecommendation{}, 1824 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 1825 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1826 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1827 autoscalingv2.PodsMetricSourceType: monitor.ActionLabelScaleDown, 1828 }, 1829 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1830 autoscalingv2.PodsMetricSourceType: monitor.ErrorLabelNone, 1831 }, 1832 } 1833 tc.runTest(t) 1834 } 1835 1836 func TestScaleDownCMObject(t *testing.T) { 1837 targetValue := resource.MustParse("20.0") 1838 tc := testCase{ 1839 minReplicas: 2, 1840 maxReplicas: 6, 1841 specReplicas: 5, 1842 statusReplicas: 5, 1843 expectedDesiredReplicas: 3, 1844 CPUTarget: 0, 1845 metricsTarget: []autoscalingv2.MetricSpec{ 1846 { 1847 Type: autoscalingv2.ObjectMetricSourceType, 1848 Object: &autoscalingv2.ObjectMetricSource{ 1849 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 1850 APIVersion: "apps/v1", 1851 Kind: "Deployment", 1852 Name: "some-deployment", 1853 }, 1854 Metric: autoscalingv2.MetricIdentifier{ 1855 Name: "qps", 1856 }, 1857 Target: autoscalingv2.MetricTarget{ 1858 Type: autoscalingv2.ValueMetricType, 1859 Value: &targetValue, 1860 }, 1861 }, 1862 }, 1863 }, 1864 reportedLevels: []uint64{12000}, 1865 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1866 recommendations: []timestampedRecommendation{}, 1867 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 1868 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1869 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1870 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelScaleDown, 1871 }, 1872 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1873 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 1874 }, 1875 } 1876 tc.runTest(t) 1877 } 1878 1879 func TestScaleDownToZeroCMObject(t *testing.T) { 1880 targetValue := resource.MustParse("20.0") 1881 tc := testCase{ 1882 minReplicas: 0, 1883 maxReplicas: 6, 1884 specReplicas: 5, 1885 statusReplicas: 5, 1886 expectedDesiredReplicas: 0, 1887 CPUTarget: 0, 1888 metricsTarget: []autoscalingv2.MetricSpec{ 1889 { 1890 Type: autoscalingv2.ObjectMetricSourceType, 1891 Object: &autoscalingv2.ObjectMetricSource{ 1892 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 1893 APIVersion: "apps/v1", 1894 Kind: "Deployment", 1895 Name: "some-deployment", 1896 }, 1897 Metric: autoscalingv2.MetricIdentifier{ 1898 Name: "qps", 1899 }, 1900 Target: autoscalingv2.MetricTarget{ 1901 Type: autoscalingv2.ValueMetricType, 1902 Value: &targetValue, 1903 }, 1904 }, 1905 }, 1906 }, 1907 reportedLevels: []uint64{0}, 1908 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1909 recommendations: []timestampedRecommendation{}, 1910 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 1911 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1912 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1913 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelScaleDown, 1914 }, 1915 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1916 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 1917 }, 1918 } 1919 tc.runTest(t) 1920 } 1921 1922 func TestScaleDownPerPodCMObject(t *testing.T) { 1923 targetAverageValue := resource.MustParse("20.0") 1924 tc := testCase{ 1925 minReplicas: 2, 1926 maxReplicas: 6, 1927 specReplicas: 5, 1928 statusReplicas: 5, 1929 expectedDesiredReplicas: 3, 1930 CPUTarget: 0, 1931 metricsTarget: []autoscalingv2.MetricSpec{ 1932 { 1933 Type: autoscalingv2.ObjectMetricSourceType, 1934 Object: &autoscalingv2.ObjectMetricSource{ 1935 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 1936 APIVersion: "apps/v1", 1937 Kind: "Deployment", 1938 Name: "some-deployment", 1939 }, 1940 Metric: autoscalingv2.MetricIdentifier{ 1941 Name: "qps", 1942 }, 1943 Target: autoscalingv2.MetricTarget{ 1944 Type: autoscalingv2.AverageValueMetricType, 1945 AverageValue: &targetAverageValue, 1946 }, 1947 }, 1948 }, 1949 }, 1950 reportedLevels: []uint64{60000}, 1951 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 1952 recommendations: []timestampedRecommendation{}, 1953 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 1954 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1955 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1956 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelScaleDown, 1957 }, 1958 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1959 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 1960 }, 1961 } 1962 tc.runTest(t) 1963 } 1964 1965 func TestScaleDownCMExternal(t *testing.T) { 1966 tc := testCase{ 1967 minReplicas: 2, 1968 maxReplicas: 6, 1969 specReplicas: 5, 1970 statusReplicas: 5, 1971 expectedDesiredReplicas: 3, 1972 metricsTarget: []autoscalingv2.MetricSpec{ 1973 { 1974 Type: autoscalingv2.ExternalMetricSourceType, 1975 External: &autoscalingv2.ExternalMetricSource{ 1976 Metric: autoscalingv2.MetricIdentifier{ 1977 Name: "qps", 1978 Selector: &metav1.LabelSelector{}, 1979 }, 1980 Target: autoscalingv2.MetricTarget{ 1981 Type: autoscalingv2.ValueMetricType, 1982 Value: resource.NewMilliQuantity(14400, resource.DecimalSI), 1983 }, 1984 }, 1985 }, 1986 }, 1987 reportedLevels: []uint64{8600}, 1988 recommendations: []timestampedRecommendation{}, 1989 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 1990 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 1991 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 1992 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelScaleDown, 1993 }, 1994 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 1995 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelNone, 1996 }, 1997 } 1998 tc.runTest(t) 1999 } 2000 2001 func TestScaleDownToZeroCMExternal(t *testing.T) { 2002 tc := testCase{ 2003 minReplicas: 0, 2004 maxReplicas: 6, 2005 specReplicas: 5, 2006 statusReplicas: 5, 2007 expectedDesiredReplicas: 0, 2008 metricsTarget: []autoscalingv2.MetricSpec{ 2009 { 2010 Type: autoscalingv2.ExternalMetricSourceType, 2011 External: &autoscalingv2.ExternalMetricSource{ 2012 Metric: autoscalingv2.MetricIdentifier{ 2013 Name: "qps", 2014 Selector: &metav1.LabelSelector{}, 2015 }, 2016 Target: autoscalingv2.MetricTarget{ 2017 Type: autoscalingv2.ValueMetricType, 2018 Value: resource.NewMilliQuantity(14400, resource.DecimalSI), 2019 }, 2020 }, 2021 }, 2022 }, 2023 reportedLevels: []uint64{0}, 2024 recommendations: []timestampedRecommendation{}, 2025 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2026 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2027 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2028 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelScaleDown, 2029 }, 2030 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2031 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelNone, 2032 }, 2033 } 2034 tc.runTest(t) 2035 } 2036 2037 func TestScaleDownPerPodCMExternal(t *testing.T) { 2038 tc := testCase{ 2039 minReplicas: 2, 2040 maxReplicas: 6, 2041 specReplicas: 5, 2042 statusReplicas: 5, 2043 expectedDesiredReplicas: 3, 2044 metricsTarget: []autoscalingv2.MetricSpec{ 2045 { 2046 Type: autoscalingv2.ExternalMetricSourceType, 2047 External: &autoscalingv2.ExternalMetricSource{ 2048 Metric: autoscalingv2.MetricIdentifier{ 2049 Name: "qps", 2050 Selector: &metav1.LabelSelector{}, 2051 }, 2052 Target: autoscalingv2.MetricTarget{ 2053 Type: autoscalingv2.AverageValueMetricType, 2054 AverageValue: resource.NewMilliQuantity(3000, resource.DecimalSI), 2055 }, 2056 }, 2057 }, 2058 }, 2059 reportedLevels: []uint64{8600}, 2060 recommendations: []timestampedRecommendation{}, 2061 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2062 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2063 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2064 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelScaleDown, 2065 }, 2066 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2067 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelNone, 2068 }, 2069 } 2070 tc.runTest(t) 2071 } 2072 2073 func TestScaleDownIncludeUnreadyPods(t *testing.T) { 2074 tc := testCase{ 2075 minReplicas: 2, 2076 maxReplicas: 6, 2077 specReplicas: 5, 2078 statusReplicas: 5, 2079 expectedDesiredReplicas: 2, 2080 CPUTarget: 50, 2081 CPUCurrent: 30, 2082 verifyCPUCurrent: true, 2083 reportedLevels: []uint64{100, 300, 500, 250, 250}, 2084 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 2085 useMetricsAPI: true, 2086 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse}, 2087 recommendations: []timestampedRecommendation{}, 2088 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2089 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2090 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2091 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2092 }, 2093 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2094 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2095 }, 2096 } 2097 tc.runTest(t) 2098 } 2099 2100 func TestScaleDownIgnoreHotCpuPods(t *testing.T) { 2101 tc := testCase{ 2102 minReplicas: 2, 2103 maxReplicas: 6, 2104 specReplicas: 5, 2105 statusReplicas: 5, 2106 expectedDesiredReplicas: 2, 2107 CPUTarget: 50, 2108 CPUCurrent: 30, 2109 verifyCPUCurrent: true, 2110 reportedLevels: []uint64{100, 300, 500, 250, 250}, 2111 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 2112 useMetricsAPI: true, 2113 reportedPodStartTime: []metav1.Time{coolCPUCreationTime(), coolCPUCreationTime(), coolCPUCreationTime(), hotCPUCreationTime(), hotCPUCreationTime()}, 2114 recommendations: []timestampedRecommendation{}, 2115 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2116 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2117 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2118 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2119 }, 2120 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2121 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2122 }, 2123 } 2124 tc.runTest(t) 2125 } 2126 2127 func TestScaleDownIgnoresFailedPods(t *testing.T) { 2128 tc := testCase{ 2129 minReplicas: 2, 2130 maxReplicas: 6, 2131 specReplicas: 5, 2132 statusReplicas: 5, 2133 expectedDesiredReplicas: 3, 2134 CPUTarget: 50, 2135 CPUCurrent: 28, 2136 verifyCPUCurrent: true, 2137 reportedLevels: []uint64{100, 300, 500, 250, 250}, 2138 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")}, 2139 useMetricsAPI: true, 2140 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse}, 2141 reportedPodPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodFailed, v1.PodFailed}, 2142 recommendations: []timestampedRecommendation{}, 2143 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2144 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2145 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2146 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2147 }, 2148 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2149 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2150 }, 2151 } 2152 tc.runTest(t) 2153 } 2154 2155 func TestScaleDownIgnoresDeletionPods(t *testing.T) { 2156 tc := testCase{ 2157 minReplicas: 2, 2158 maxReplicas: 6, 2159 specReplicas: 5, 2160 statusReplicas: 5, 2161 expectedDesiredReplicas: 3, 2162 CPUTarget: 50, 2163 CPUCurrent: 28, 2164 verifyCPUCurrent: true, 2165 reportedLevels: []uint64{100, 300, 500, 250, 250}, 2166 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")}, 2167 useMetricsAPI: true, 2168 reportedPodReadiness: []v1.ConditionStatus{v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionTrue, v1.ConditionFalse, v1.ConditionFalse}, 2169 reportedPodPhase: []v1.PodPhase{v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning, v1.PodRunning}, 2170 reportedPodDeletionTimestamp: []bool{false, false, false, false, false, true, true}, 2171 recommendations: []timestampedRecommendation{}, 2172 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2173 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2174 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2175 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2176 }, 2177 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2178 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2179 }, 2180 } 2181 tc.runTest(t) 2182 } 2183 2184 func TestTolerance(t *testing.T) { 2185 tc := testCase{ 2186 minReplicas: 1, 2187 maxReplicas: 5, 2188 specReplicas: 3, 2189 statusReplicas: 3, 2190 expectedDesiredReplicas: 3, 2191 CPUTarget: 100, 2192 reportedLevels: []uint64{1010, 1030, 1020}, 2193 reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")}, 2194 useMetricsAPI: true, 2195 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2196 Type: autoscalingv2.AbleToScale, 2197 Status: v1.ConditionTrue, 2198 Reason: "ReadyForNewScale", 2199 }), 2200 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2201 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2202 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2203 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelNone, 2204 }, 2205 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2206 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2207 }, 2208 } 2209 tc.runTest(t) 2210 } 2211 2212 func TestToleranceCM(t *testing.T) { 2213 averageValue := resource.MustParse("20.0") 2214 tc := testCase{ 2215 minReplicas: 1, 2216 maxReplicas: 5, 2217 specReplicas: 3, 2218 statusReplicas: 3, 2219 expectedDesiredReplicas: 3, 2220 metricsTarget: []autoscalingv2.MetricSpec{ 2221 { 2222 Type: autoscalingv2.PodsMetricSourceType, 2223 Pods: &autoscalingv2.PodsMetricSource{ 2224 Metric: autoscalingv2.MetricIdentifier{ 2225 Name: "qps", 2226 }, 2227 Target: autoscalingv2.MetricTarget{ 2228 Type: autoscalingv2.AverageValueMetricType, 2229 AverageValue: &averageValue, 2230 }, 2231 }, 2232 }, 2233 }, 2234 reportedLevels: []uint64{20000, 20001, 21000}, 2235 reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")}, 2236 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2237 Type: autoscalingv2.AbleToScale, 2238 Status: v1.ConditionTrue, 2239 Reason: "ReadyForNewScale", 2240 }), 2241 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2242 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2243 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2244 autoscalingv2.PodsMetricSourceType: monitor.ActionLabelNone, 2245 }, 2246 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2247 autoscalingv2.PodsMetricSourceType: monitor.ErrorLabelNone, 2248 }, 2249 } 2250 tc.runTest(t) 2251 } 2252 2253 func TestToleranceCMObject(t *testing.T) { 2254 targetValue := resource.MustParse("20.0") 2255 tc := testCase{ 2256 minReplicas: 1, 2257 maxReplicas: 5, 2258 specReplicas: 3, 2259 statusReplicas: 3, 2260 expectedDesiredReplicas: 3, 2261 metricsTarget: []autoscalingv2.MetricSpec{ 2262 { 2263 Type: autoscalingv2.ObjectMetricSourceType, 2264 Object: &autoscalingv2.ObjectMetricSource{ 2265 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 2266 APIVersion: "apps/v1", 2267 Kind: "Deployment", 2268 Name: "some-deployment", 2269 }, 2270 Metric: autoscalingv2.MetricIdentifier{ 2271 Name: "qps", 2272 }, 2273 Target: autoscalingv2.MetricTarget{ 2274 Type: autoscalingv2.ValueMetricType, 2275 Value: &targetValue, 2276 }, 2277 }, 2278 }, 2279 }, 2280 reportedLevels: []uint64{20050}, 2281 reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")}, 2282 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2283 Type: autoscalingv2.AbleToScale, 2284 Status: v1.ConditionTrue, 2285 Reason: "ReadyForNewScale", 2286 }), 2287 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2288 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2289 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2290 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelNone, 2291 }, 2292 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2293 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 2294 }, 2295 } 2296 tc.runTest(t) 2297 } 2298 2299 func TestToleranceCMExternal(t *testing.T) { 2300 tc := testCase{ 2301 minReplicas: 2, 2302 maxReplicas: 6, 2303 specReplicas: 4, 2304 statusReplicas: 4, 2305 expectedDesiredReplicas: 4, 2306 metricsTarget: []autoscalingv2.MetricSpec{ 2307 { 2308 Type: autoscalingv2.ExternalMetricSourceType, 2309 External: &autoscalingv2.ExternalMetricSource{ 2310 Metric: autoscalingv2.MetricIdentifier{ 2311 Name: "qps", 2312 Selector: &metav1.LabelSelector{}, 2313 }, 2314 Target: autoscalingv2.MetricTarget{ 2315 Type: autoscalingv2.ValueMetricType, 2316 Value: resource.NewMilliQuantity(8666, resource.DecimalSI), 2317 }, 2318 }, 2319 }, 2320 }, 2321 reportedLevels: []uint64{8600}, 2322 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2323 Type: autoscalingv2.AbleToScale, 2324 Status: v1.ConditionTrue, 2325 Reason: "ReadyForNewScale", 2326 }), 2327 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2328 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2329 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2330 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelNone, 2331 }, 2332 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2333 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelNone, 2334 }, 2335 } 2336 tc.runTest(t) 2337 } 2338 2339 func TestTolerancePerPodCMObject(t *testing.T) { 2340 tc := testCase{ 2341 minReplicas: 2, 2342 maxReplicas: 6, 2343 specReplicas: 4, 2344 statusReplicas: 4, 2345 expectedDesiredReplicas: 4, 2346 metricsTarget: []autoscalingv2.MetricSpec{ 2347 { 2348 Type: autoscalingv2.ObjectMetricSourceType, 2349 Object: &autoscalingv2.ObjectMetricSource{ 2350 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 2351 APIVersion: "apps/v1", 2352 Kind: "Deployment", 2353 Name: "some-deployment", 2354 }, 2355 Metric: autoscalingv2.MetricIdentifier{ 2356 Name: "qps", 2357 Selector: &metav1.LabelSelector{}, 2358 }, 2359 Target: autoscalingv2.MetricTarget{ 2360 Type: autoscalingv2.AverageValueMetricType, 2361 AverageValue: resource.NewMilliQuantity(2200, resource.DecimalSI), 2362 }, 2363 }, 2364 }, 2365 }, 2366 reportedLevels: []uint64{8600}, 2367 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2368 Type: autoscalingv2.AbleToScale, 2369 Status: v1.ConditionTrue, 2370 Reason: "ReadyForNewScale", 2371 }), 2372 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2373 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2374 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2375 autoscalingv2.ObjectMetricSourceType: monitor.ActionLabelNone, 2376 }, 2377 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2378 autoscalingv2.ObjectMetricSourceType: monitor.ErrorLabelNone, 2379 }, 2380 } 2381 tc.runTest(t) 2382 } 2383 2384 func TestTolerancePerPodCMExternal(t *testing.T) { 2385 tc := testCase{ 2386 minReplicas: 2, 2387 maxReplicas: 6, 2388 specReplicas: 4, 2389 statusReplicas: 4, 2390 expectedDesiredReplicas: 4, 2391 metricsTarget: []autoscalingv2.MetricSpec{ 2392 { 2393 Type: autoscalingv2.ExternalMetricSourceType, 2394 External: &autoscalingv2.ExternalMetricSource{ 2395 Metric: autoscalingv2.MetricIdentifier{ 2396 Name: "qps", 2397 Selector: &metav1.LabelSelector{}, 2398 }, 2399 Target: autoscalingv2.MetricTarget{ 2400 Type: autoscalingv2.AverageValueMetricType, 2401 AverageValue: resource.NewMilliQuantity(2200, resource.DecimalSI), 2402 }, 2403 }, 2404 }, 2405 }, 2406 reportedLevels: []uint64{8600}, 2407 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2408 Type: autoscalingv2.AbleToScale, 2409 Status: v1.ConditionTrue, 2410 Reason: "ReadyForNewScale", 2411 }), 2412 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2413 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2414 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2415 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelNone, 2416 }, 2417 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2418 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelNone, 2419 }, 2420 } 2421 tc.runTest(t) 2422 } 2423 2424 func TestMinReplicas(t *testing.T) { 2425 tc := testCase{ 2426 minReplicas: 2, 2427 maxReplicas: 5, 2428 specReplicas: 3, 2429 statusReplicas: 3, 2430 expectedDesiredReplicas: 2, 2431 CPUTarget: 90, 2432 reportedLevels: []uint64{10, 95, 10}, 2433 reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")}, 2434 useMetricsAPI: true, 2435 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2436 Type: autoscalingv2.ScalingLimited, 2437 Status: v1.ConditionTrue, 2438 Reason: "TooFewReplicas", 2439 }), 2440 recommendations: []timestampedRecommendation{}, 2441 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2442 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2443 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2444 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2445 }, 2446 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2447 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2448 }, 2449 } 2450 tc.runTest(t) 2451 } 2452 2453 func TestZeroMinReplicasDesiredZero(t *testing.T) { 2454 tc := testCase{ 2455 minReplicas: 0, 2456 maxReplicas: 5, 2457 specReplicas: 3, 2458 statusReplicas: 3, 2459 expectedDesiredReplicas: 0, 2460 CPUTarget: 90, 2461 reportedLevels: []uint64{0, 0, 0}, 2462 reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")}, 2463 useMetricsAPI: true, 2464 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2465 Type: autoscalingv2.ScalingLimited, 2466 Status: v1.ConditionFalse, 2467 Reason: "DesiredWithinRange", 2468 }), 2469 recommendations: []timestampedRecommendation{}, 2470 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2471 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2472 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2473 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2474 }, 2475 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2476 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2477 }, 2478 } 2479 tc.runTest(t) 2480 } 2481 2482 func TestMinReplicasDesiredZero(t *testing.T) { 2483 tc := testCase{ 2484 minReplicas: 2, 2485 maxReplicas: 5, 2486 specReplicas: 3, 2487 statusReplicas: 3, 2488 expectedDesiredReplicas: 2, 2489 CPUTarget: 90, 2490 reportedLevels: []uint64{0, 0, 0}, 2491 reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")}, 2492 useMetricsAPI: true, 2493 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2494 Type: autoscalingv2.ScalingLimited, 2495 Status: v1.ConditionTrue, 2496 Reason: "TooFewReplicas", 2497 }), 2498 recommendations: []timestampedRecommendation{}, 2499 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2500 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2501 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2502 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2503 }, 2504 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2505 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2506 }, 2507 } 2508 tc.runTest(t) 2509 } 2510 2511 func TestZeroReplicas(t *testing.T) { 2512 tc := testCase{ 2513 minReplicas: 3, 2514 maxReplicas: 5, 2515 specReplicas: 0, 2516 statusReplicas: 0, 2517 expectedDesiredReplicas: 0, 2518 CPUTarget: 90, 2519 reportedLevels: []uint64{}, 2520 reportedCPURequests: []resource.Quantity{}, 2521 useMetricsAPI: true, 2522 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 2523 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"}, 2524 {Type: autoscalingv2.ScalingActive, Status: v1.ConditionFalse, Reason: "ScalingDisabled"}, 2525 }, 2526 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2527 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2528 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 2529 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 2530 } 2531 tc.runTest(t) 2532 } 2533 2534 func TestTooFewReplicas(t *testing.T) { 2535 tc := testCase{ 2536 minReplicas: 3, 2537 maxReplicas: 5, 2538 specReplicas: 2, 2539 statusReplicas: 2, 2540 expectedDesiredReplicas: 3, 2541 CPUTarget: 90, 2542 reportedLevels: []uint64{}, 2543 reportedCPURequests: []resource.Quantity{}, 2544 useMetricsAPI: true, 2545 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 2546 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"}, 2547 }, 2548 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 2549 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2550 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 2551 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 2552 } 2553 tc.runTest(t) 2554 } 2555 2556 func TestTooManyReplicas(t *testing.T) { 2557 tc := testCase{ 2558 minReplicas: 3, 2559 maxReplicas: 5, 2560 specReplicas: 10, 2561 statusReplicas: 10, 2562 expectedDesiredReplicas: 5, 2563 CPUTarget: 90, 2564 reportedLevels: []uint64{}, 2565 reportedCPURequests: []resource.Quantity{}, 2566 useMetricsAPI: true, 2567 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 2568 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"}, 2569 }, 2570 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2571 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2572 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 2573 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 2574 } 2575 tc.runTest(t) 2576 } 2577 2578 func TestMaxReplicas(t *testing.T) { 2579 tc := testCase{ 2580 minReplicas: 2, 2581 maxReplicas: 5, 2582 specReplicas: 3, 2583 statusReplicas: 3, 2584 expectedDesiredReplicas: 5, 2585 CPUTarget: 90, 2586 reportedLevels: []uint64{8000, 9500, 1000}, 2587 reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")}, 2588 useMetricsAPI: true, 2589 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2590 Type: autoscalingv2.ScalingLimited, 2591 Status: v1.ConditionTrue, 2592 Reason: "TooManyReplicas", 2593 }), 2594 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 2595 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2596 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2597 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 2598 }, 2599 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2600 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2601 }, 2602 } 2603 tc.runTest(t) 2604 } 2605 2606 func TestSuperfluousMetrics(t *testing.T) { 2607 tc := testCase{ 2608 minReplicas: 2, 2609 maxReplicas: 6, 2610 specReplicas: 4, 2611 statusReplicas: 4, 2612 expectedDesiredReplicas: 6, 2613 CPUTarget: 100, 2614 reportedLevels: []uint64{4000, 9500, 3000, 7000, 3200, 2000}, 2615 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 2616 useMetricsAPI: true, 2617 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2618 Type: autoscalingv2.ScalingLimited, 2619 Status: v1.ConditionTrue, 2620 Reason: "TooManyReplicas", 2621 }), 2622 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 2623 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2624 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2625 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 2626 }, 2627 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2628 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2629 }, 2630 } 2631 tc.runTest(t) 2632 } 2633 2634 func TestMissingMetrics(t *testing.T) { 2635 tc := testCase{ 2636 minReplicas: 2, 2637 maxReplicas: 6, 2638 specReplicas: 4, 2639 statusReplicas: 4, 2640 expectedDesiredReplicas: 3, 2641 CPUTarget: 100, 2642 reportedLevels: []uint64{400, 95}, 2643 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 2644 useMetricsAPI: true, 2645 recommendations: []timestampedRecommendation{}, 2646 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2647 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2648 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2649 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2650 }, 2651 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2652 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2653 }, 2654 } 2655 tc.runTest(t) 2656 } 2657 2658 func TestEmptyMetrics(t *testing.T) { 2659 tc := testCase{ 2660 minReplicas: 2, 2661 maxReplicas: 6, 2662 specReplicas: 4, 2663 statusReplicas: 4, 2664 expectedDesiredReplicas: 4, 2665 CPUTarget: 100, 2666 reportedLevels: []uint64{}, 2667 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 2668 useMetricsAPI: true, 2669 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 2670 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"}, 2671 {Type: autoscalingv2.ScalingActive, Status: v1.ConditionFalse, Reason: "FailedGetResourceMetric"}, 2672 }, 2673 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2674 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 2675 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2676 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelNone, 2677 }, 2678 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2679 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelInternal, 2680 }, 2681 } 2682 tc.runTest(t) 2683 } 2684 2685 func TestEmptyCPURequest(t *testing.T) { 2686 tc := testCase{ 2687 minReplicas: 1, 2688 maxReplicas: 5, 2689 specReplicas: 1, 2690 statusReplicas: 1, 2691 expectedDesiredReplicas: 1, 2692 CPUTarget: 100, 2693 reportedLevels: []uint64{200}, 2694 reportedCPURequests: []resource.Quantity{}, 2695 useMetricsAPI: true, 2696 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 2697 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"}, 2698 {Type: autoscalingv2.ScalingActive, Status: v1.ConditionFalse, Reason: "FailedGetResourceMetric"}, 2699 }, 2700 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2701 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 2702 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2703 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelNone, 2704 }, 2705 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2706 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelInternal, 2707 }, 2708 } 2709 tc.runTest(t) 2710 } 2711 2712 func TestEventCreated(t *testing.T) { 2713 tc := testCase{ 2714 minReplicas: 1, 2715 maxReplicas: 5, 2716 specReplicas: 1, 2717 statusReplicas: 1, 2718 expectedDesiredReplicas: 2, 2719 CPUTarget: 50, 2720 reportedLevels: []uint64{200}, 2721 reportedCPURequests: []resource.Quantity{resource.MustParse("0.2")}, 2722 verifyEvents: true, 2723 useMetricsAPI: true, 2724 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 2725 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2726 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2727 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 2728 }, 2729 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2730 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2731 }, 2732 } 2733 tc.runTest(t) 2734 } 2735 2736 func TestEventNotCreated(t *testing.T) { 2737 tc := testCase{ 2738 minReplicas: 1, 2739 maxReplicas: 5, 2740 specReplicas: 2, 2741 statusReplicas: 2, 2742 expectedDesiredReplicas: 2, 2743 CPUTarget: 50, 2744 reportedLevels: []uint64{200, 200}, 2745 reportedCPURequests: []resource.Quantity{resource.MustParse("0.4"), resource.MustParse("0.4")}, 2746 verifyEvents: true, 2747 useMetricsAPI: true, 2748 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2749 Type: autoscalingv2.AbleToScale, 2750 Status: v1.ConditionTrue, 2751 Reason: "ReadyForNewScale", 2752 }), 2753 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2754 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2755 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2756 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelNone, 2757 }, 2758 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2759 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2760 }, 2761 } 2762 tc.runTest(t) 2763 } 2764 2765 func TestMissingReports(t *testing.T) { 2766 tc := testCase{ 2767 minReplicas: 1, 2768 maxReplicas: 5, 2769 specReplicas: 4, 2770 statusReplicas: 4, 2771 expectedDesiredReplicas: 2, 2772 CPUTarget: 50, 2773 reportedLevels: []uint64{200}, 2774 reportedCPURequests: []resource.Quantity{resource.MustParse("0.2")}, 2775 useMetricsAPI: true, 2776 recommendations: []timestampedRecommendation{}, 2777 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 2778 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2779 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2780 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 2781 }, 2782 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2783 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2784 }, 2785 } 2786 tc.runTest(t) 2787 } 2788 2789 func TestUpscaleCap(t *testing.T) { 2790 tc := testCase{ 2791 minReplicas: 1, 2792 maxReplicas: 100, 2793 specReplicas: 3, 2794 statusReplicas: 3, 2795 scaleUpRules: generateScalingRules(0, 0, 700, 60, 0), 2796 initialReplicas: 3, 2797 expectedDesiredReplicas: 24, 2798 CPUTarget: 10, 2799 reportedLevels: []uint64{100, 200, 300}, 2800 reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")}, 2801 useMetricsAPI: true, 2802 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2803 Type: autoscalingv2.ScalingLimited, 2804 Status: v1.ConditionTrue, 2805 Reason: "ScaleUpLimit", 2806 }), 2807 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 2808 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2809 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2810 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 2811 }, 2812 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2813 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2814 }, 2815 } 2816 tc.runTest(t) 2817 } 2818 2819 func TestUpscaleCapGreaterThanMaxReplicas(t *testing.T) { 2820 // TODO: Remove skip once this issue is resolved: https://github.com/kubernetes/kubernetes/issues/124083 2821 if goruntime.GOOS == "windows" { 2822 t.Skip("Skip flaking test on Windows.") 2823 } 2824 tc := testCase{ 2825 minReplicas: 1, 2826 maxReplicas: 20, 2827 specReplicas: 3, 2828 statusReplicas: 3, 2829 scaleUpRules: generateScalingRules(0, 0, 700, 60, 0), 2830 initialReplicas: 3, 2831 // expectedDesiredReplicas would be 24 without maxReplicas 2832 expectedDesiredReplicas: 20, 2833 CPUTarget: 10, 2834 reportedLevels: []uint64{100, 200, 300}, 2835 reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")}, 2836 useMetricsAPI: true, 2837 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2838 Type: autoscalingv2.ScalingLimited, 2839 Status: v1.ConditionTrue, 2840 Reason: "TooManyReplicas", 2841 }), 2842 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 2843 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2844 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2845 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 2846 }, 2847 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2848 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2849 }, 2850 } 2851 tc.runTest(t) 2852 } 2853 2854 func TestMoreReplicasThanSpecNoScale(t *testing.T) { 2855 // TODO: Remove skip once this issue is resolved: https://github.com/kubernetes/kubernetes/issues/124083 2856 if goruntime.GOOS == "windows" { 2857 t.Skip("Skip flaking test on Windows.") 2858 } 2859 tc := testCase{ 2860 minReplicas: 1, 2861 maxReplicas: 8, 2862 specReplicas: 4, 2863 statusReplicas: 5, // Deployment update with 25% surge. 2864 expectedDesiredReplicas: 4, 2865 CPUTarget: 50, 2866 reportedLevels: []uint64{500, 500, 500, 500, 500}, 2867 reportedCPURequests: []resource.Quantity{ 2868 resource.MustParse("1"), 2869 resource.MustParse("1"), 2870 resource.MustParse("1"), 2871 resource.MustParse("1"), 2872 resource.MustParse("1"), 2873 }, 2874 useMetricsAPI: true, 2875 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 2876 Type: autoscalingv2.AbleToScale, 2877 Status: v1.ConditionTrue, 2878 Reason: "ReadyForNewScale", 2879 }), 2880 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2881 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 2882 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 2883 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelNone, 2884 }, 2885 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 2886 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 2887 }, 2888 } 2889 tc.runTest(t) 2890 } 2891 2892 func TestConditionInvalidSelectorMissing(t *testing.T) { 2893 tc := testCase{ 2894 minReplicas: 1, 2895 maxReplicas: 100, 2896 specReplicas: 3, 2897 statusReplicas: 3, 2898 expectedDesiredReplicas: 3, 2899 CPUTarget: 10, 2900 reportedLevels: []uint64{100, 200, 300}, 2901 reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")}, 2902 useMetricsAPI: true, 2903 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 2904 { 2905 Type: autoscalingv2.AbleToScale, 2906 Status: v1.ConditionTrue, 2907 Reason: "SucceededGetScale", 2908 }, 2909 { 2910 Type: autoscalingv2.ScalingActive, 2911 Status: v1.ConditionFalse, 2912 Reason: "InvalidSelector", 2913 }, 2914 }, 2915 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2916 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 2917 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 2918 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 2919 } 2920 2921 _, _, _, _, testScaleClient := tc.prepareTestClient(t) 2922 tc.testScaleClient = testScaleClient 2923 2924 testScaleClient.PrependReactor("get", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 2925 obj := &autoscalingv1.Scale{ 2926 ObjectMeta: metav1.ObjectMeta{ 2927 Name: tc.resource.name, 2928 }, 2929 Spec: autoscalingv1.ScaleSpec{ 2930 Replicas: tc.specReplicas, 2931 }, 2932 Status: autoscalingv1.ScaleStatus{ 2933 Replicas: tc.specReplicas, 2934 }, 2935 } 2936 return true, obj, nil 2937 }) 2938 2939 tc.runTest(t) 2940 } 2941 2942 func TestConditionInvalidSelectorUnparsable(t *testing.T) { 2943 tc := testCase{ 2944 minReplicas: 1, 2945 maxReplicas: 100, 2946 specReplicas: 3, 2947 statusReplicas: 3, 2948 expectedDesiredReplicas: 3, 2949 CPUTarget: 10, 2950 reportedLevels: []uint64{100, 200, 300}, 2951 reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")}, 2952 useMetricsAPI: true, 2953 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 2954 { 2955 Type: autoscalingv2.AbleToScale, 2956 Status: v1.ConditionTrue, 2957 Reason: "SucceededGetScale", 2958 }, 2959 { 2960 Type: autoscalingv2.ScalingActive, 2961 Status: v1.ConditionFalse, 2962 Reason: "InvalidSelector", 2963 }, 2964 }, 2965 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 2966 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 2967 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 2968 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 2969 } 2970 2971 _, _, _, _, testScaleClient := tc.prepareTestClient(t) 2972 tc.testScaleClient = testScaleClient 2973 2974 testScaleClient.PrependReactor("get", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 2975 obj := &autoscalingv1.Scale{ 2976 ObjectMeta: metav1.ObjectMeta{ 2977 Name: tc.resource.name, 2978 }, 2979 Spec: autoscalingv1.ScaleSpec{ 2980 Replicas: tc.specReplicas, 2981 }, 2982 Status: autoscalingv1.ScaleStatus{ 2983 Replicas: tc.specReplicas, 2984 Selector: "cheddar cheese", 2985 }, 2986 } 2987 return true, obj, nil 2988 }) 2989 2990 tc.runTest(t) 2991 } 2992 2993 func TestConditionNoAmbiguousSelectorWhenNoSelectorOverlapBetweenHPAs(t *testing.T) { 2994 hpaSelectors := selectors.NewBiMultimap() 2995 hpaSelectors.PutSelector(selectors.Key{Name: "test-hpa-2", Namespace: testNamespace}, labels.SelectorFromSet(labels.Set{"cheddar": "cheese"})) 2996 2997 tc := testCase{ 2998 minReplicas: 2, 2999 maxReplicas: 6, 3000 specReplicas: 3, 3001 statusReplicas: 3, 3002 expectedDesiredReplicas: 5, 3003 CPUTarget: 30, 3004 reportedLevels: []uint64{300, 500, 700}, 3005 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 3006 useMetricsAPI: true, 3007 hpaSelectors: hpaSelectors, 3008 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 3009 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3010 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3011 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 3012 }, 3013 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3014 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 3015 }, 3016 } 3017 tc.runTest(t) 3018 } 3019 3020 func TestConditionAmbiguousSelectorWhenFullSelectorOverlapBetweenHPAs(t *testing.T) { 3021 hpaSelectors := selectors.NewBiMultimap() 3022 hpaSelectors.PutSelector(selectors.Key{Name: "test-hpa-2", Namespace: testNamespace}, labels.SelectorFromSet(labels.Set{"name": podNamePrefix})) 3023 3024 tc := testCase{ 3025 minReplicas: 2, 3026 maxReplicas: 6, 3027 specReplicas: 3, 3028 statusReplicas: 3, 3029 expectedDesiredReplicas: 3, 3030 CPUTarget: 30, 3031 reportedLevels: []uint64{300, 500, 700}, 3032 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 3033 useMetricsAPI: true, 3034 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 3035 { 3036 Type: autoscalingv2.AbleToScale, 3037 Status: v1.ConditionTrue, 3038 Reason: "SucceededGetScale", 3039 }, 3040 { 3041 Type: autoscalingv2.ScalingActive, 3042 Status: v1.ConditionFalse, 3043 Reason: "AmbiguousSelector", 3044 }, 3045 }, 3046 hpaSelectors: hpaSelectors, 3047 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3048 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 3049 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 3050 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 3051 } 3052 tc.runTest(t) 3053 } 3054 3055 func TestConditionAmbiguousSelectorWhenPartialSelectorOverlapBetweenHPAs(t *testing.T) { 3056 hpaSelectors := selectors.NewBiMultimap() 3057 hpaSelectors.PutSelector(selectors.Key{Name: "test-hpa-2", Namespace: testNamespace}, labels.SelectorFromSet(labels.Set{"cheddar": "cheese"})) 3058 3059 tc := testCase{ 3060 minReplicas: 2, 3061 maxReplicas: 6, 3062 specReplicas: 3, 3063 statusReplicas: 3, 3064 expectedDesiredReplicas: 3, 3065 CPUTarget: 30, 3066 reportedLevels: []uint64{300, 500, 700}, 3067 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 3068 useMetricsAPI: true, 3069 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 3070 { 3071 Type: autoscalingv2.AbleToScale, 3072 Status: v1.ConditionTrue, 3073 Reason: "SucceededGetScale", 3074 }, 3075 { 3076 Type: autoscalingv2.ScalingActive, 3077 Status: v1.ConditionFalse, 3078 Reason: "AmbiguousSelector", 3079 }, 3080 }, 3081 hpaSelectors: hpaSelectors, 3082 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3083 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 3084 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 3085 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 3086 } 3087 3088 testClient, _, _, _, _ := tc.prepareTestClient(t) 3089 tc.testClient = testClient 3090 3091 testClient.PrependReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { 3092 tc.Lock() 3093 defer tc.Unlock() 3094 3095 obj := &v1.PodList{} 3096 for i := range tc.reportedCPURequests { 3097 pod := v1.Pod{ 3098 ObjectMeta: metav1.ObjectMeta{ 3099 Name: fmt.Sprintf("%s-%d", podNamePrefix, i), 3100 Namespace: testNamespace, 3101 Labels: map[string]string{ 3102 "name": podNamePrefix, // selected by the original HPA 3103 "cheddar": "cheese", // selected by test-hpa-2 3104 }, 3105 }, 3106 } 3107 obj.Items = append(obj.Items, pod) 3108 } 3109 return true, obj, nil 3110 }) 3111 3112 tc.runTest(t) 3113 } 3114 3115 func TestConditionFailedGetMetrics(t *testing.T) { 3116 targetValue := resource.MustParse("15.0") 3117 averageValue := resource.MustParse("15.0") 3118 metricsTargets := map[string][]autoscalingv2.MetricSpec{ 3119 "FailedGetResourceMetric": nil, 3120 "FailedGetPodsMetric": { 3121 { 3122 Type: autoscalingv2.PodsMetricSourceType, 3123 Pods: &autoscalingv2.PodsMetricSource{ 3124 Metric: autoscalingv2.MetricIdentifier{ 3125 Name: "qps", 3126 }, 3127 Target: autoscalingv2.MetricTarget{ 3128 Type: autoscalingv2.AverageValueMetricType, 3129 AverageValue: &averageValue, 3130 }, 3131 }, 3132 }, 3133 }, 3134 "FailedGetObjectMetric": { 3135 { 3136 Type: autoscalingv2.ObjectMetricSourceType, 3137 Object: &autoscalingv2.ObjectMetricSource{ 3138 DescribedObject: autoscalingv2.CrossVersionObjectReference{ 3139 APIVersion: "apps/v1", 3140 Kind: "Deployment", 3141 Name: "some-deployment", 3142 }, 3143 Metric: autoscalingv2.MetricIdentifier{ 3144 Name: "qps", 3145 }, 3146 Target: autoscalingv2.MetricTarget{ 3147 Type: autoscalingv2.ValueMetricType, 3148 Value: &targetValue, 3149 }, 3150 }, 3151 }, 3152 }, 3153 "FailedGetExternalMetric": { 3154 { 3155 Type: autoscalingv2.ExternalMetricSourceType, 3156 External: &autoscalingv2.ExternalMetricSource{ 3157 Metric: autoscalingv2.MetricIdentifier{ 3158 Name: "qps", 3159 Selector: &metav1.LabelSelector{}, 3160 }, 3161 Target: autoscalingv2.MetricTarget{ 3162 Type: autoscalingv2.ValueMetricType, 3163 Value: resource.NewMilliQuantity(300, resource.DecimalSI), 3164 }, 3165 }, 3166 }, 3167 }, 3168 } 3169 3170 for reason, specs := range metricsTargets { 3171 metricType := autoscalingv2.ResourceMetricSourceType 3172 if specs != nil { 3173 metricType = specs[0].Type 3174 } 3175 tc := testCase{ 3176 minReplicas: 1, 3177 maxReplicas: 100, 3178 specReplicas: 3, 3179 statusReplicas: 3, 3180 expectedDesiredReplicas: 3, 3181 CPUTarget: 10, 3182 reportedLevels: []uint64{100, 200, 300}, 3183 reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")}, 3184 useMetricsAPI: true, 3185 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3186 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 3187 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3188 metricType: monitor.ActionLabelNone, 3189 }, 3190 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3191 metricType: monitor.ErrorLabelInternal, 3192 }, 3193 } 3194 _, testMetricsClient, testCMClient, testEMClient, _ := tc.prepareTestClient(t) 3195 tc.testMetricsClient = testMetricsClient 3196 tc.testCMClient = testCMClient 3197 tc.testEMClient = testEMClient 3198 3199 testMetricsClient.PrependReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { 3200 return true, &metricsapi.PodMetricsList{}, fmt.Errorf("something went wrong") 3201 }) 3202 testCMClient.PrependReactor("get", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) { 3203 return true, &cmapi.MetricValueList{}, fmt.Errorf("something went wrong") 3204 }) 3205 testEMClient.PrependReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) { 3206 return true, &emapi.ExternalMetricValueList{}, fmt.Errorf("something went wrong") 3207 }) 3208 3209 tc.expectedConditions = []autoscalingv2.HorizontalPodAutoscalerCondition{ 3210 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"}, 3211 {Type: autoscalingv2.ScalingActive, Status: v1.ConditionFalse, Reason: reason}, 3212 } 3213 if specs != nil { 3214 tc.CPUTarget = 0 3215 } else { 3216 tc.CPUTarget = 10 3217 } 3218 tc.metricsTarget = specs 3219 tc.runTest(t) 3220 } 3221 } 3222 3223 func TestConditionInvalidSourceType(t *testing.T) { 3224 tc := testCase{ 3225 minReplicas: 2, 3226 maxReplicas: 6, 3227 specReplicas: 3, 3228 statusReplicas: 3, 3229 expectedDesiredReplicas: 3, 3230 CPUTarget: 0, 3231 metricsTarget: []autoscalingv2.MetricSpec{ 3232 { 3233 Type: "CheddarCheese", 3234 }, 3235 }, 3236 reportedLevels: []uint64{20000}, 3237 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 3238 { 3239 Type: autoscalingv2.AbleToScale, 3240 Status: v1.ConditionTrue, 3241 Reason: "SucceededGetScale", 3242 }, 3243 { 3244 Type: autoscalingv2.ScalingActive, 3245 Status: v1.ConditionFalse, 3246 Reason: "InvalidMetricSourceType", 3247 }, 3248 }, 3249 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3250 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 3251 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3252 // Actually, such an invalid type should be validated in the kube-apiserver and invalid metric type shouldn't be recorded. 3253 "CheddarCheese": monitor.ActionLabelNone, 3254 }, 3255 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3256 // Actually, such an invalid type should be validated in the kube-apiserver and invalid metric type shouldn't be recorded. 3257 "CheddarCheese": monitor.ErrorLabelSpec, 3258 }, 3259 } 3260 tc.runTest(t) 3261 } 3262 3263 func TestConditionFailedGetScale(t *testing.T) { 3264 tc := testCase{ 3265 minReplicas: 1, 3266 maxReplicas: 100, 3267 specReplicas: 3, 3268 statusReplicas: 3, 3269 expectedDesiredReplicas: 3, 3270 CPUTarget: 10, 3271 reportedLevels: []uint64{100, 200, 300}, 3272 reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")}, 3273 useMetricsAPI: true, 3274 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 3275 { 3276 Type: autoscalingv2.AbleToScale, 3277 Status: v1.ConditionFalse, 3278 Reason: "FailedGetScale", 3279 }, 3280 }, 3281 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3282 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 3283 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 3284 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 3285 } 3286 3287 _, _, _, _, testScaleClient := tc.prepareTestClient(t) 3288 tc.testScaleClient = testScaleClient 3289 3290 testScaleClient.PrependReactor("get", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 3291 return true, &autoscalingv1.Scale{}, fmt.Errorf("something went wrong") 3292 }) 3293 3294 tc.runTest(t) 3295 } 3296 3297 func TestConditionFailedUpdateScale(t *testing.T) { 3298 tc := testCase{ 3299 minReplicas: 1, 3300 maxReplicas: 5, 3301 specReplicas: 3, 3302 statusReplicas: 3, 3303 expectedDesiredReplicas: 3, 3304 CPUTarget: 100, 3305 reportedLevels: []uint64{150, 150, 150}, 3306 reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")}, 3307 useMetricsAPI: true, 3308 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 3309 Type: autoscalingv2.AbleToScale, 3310 Status: v1.ConditionFalse, 3311 Reason: "FailedUpdateScale", 3312 }), 3313 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3314 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 3315 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3316 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 3317 }, 3318 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3319 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 3320 }, 3321 } 3322 3323 _, _, _, _, testScaleClient := tc.prepareTestClient(t) 3324 tc.testScaleClient = testScaleClient 3325 3326 testScaleClient.PrependReactor("update", "replicationcontrollers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 3327 return true, &autoscalingv1.Scale{}, fmt.Errorf("something went wrong") 3328 }) 3329 3330 tc.runTest(t) 3331 } 3332 3333 func TestNoBackoffUpscaleCM(t *testing.T) { 3334 averageValue := resource.MustParse("15.0") 3335 time := metav1.Time{Time: time.Now()} 3336 tc := testCase{ 3337 minReplicas: 1, 3338 maxReplicas: 5, 3339 specReplicas: 3, 3340 statusReplicas: 3, 3341 expectedDesiredReplicas: 4, 3342 CPUTarget: 0, 3343 metricsTarget: []autoscalingv2.MetricSpec{ 3344 { 3345 Type: autoscalingv2.PodsMetricSourceType, 3346 Pods: &autoscalingv2.PodsMetricSource{ 3347 Metric: autoscalingv2.MetricIdentifier{ 3348 Name: "qps", 3349 }, 3350 Target: autoscalingv2.MetricTarget{ 3351 Type: autoscalingv2.AverageValueMetricType, 3352 AverageValue: &averageValue, 3353 }, 3354 }, 3355 }, 3356 }, 3357 reportedLevels: []uint64{20000, 10000, 30000}, 3358 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 3359 //useMetricsAPI: true, 3360 lastScaleTime: &time, 3361 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 3362 Type: autoscalingv2.AbleToScale, 3363 Status: v1.ConditionTrue, 3364 Reason: "ReadyForNewScale", 3365 }, autoscalingv2.HorizontalPodAutoscalerCondition{ 3366 Type: autoscalingv2.AbleToScale, 3367 Status: v1.ConditionTrue, 3368 Reason: "SucceededRescale", 3369 }, autoscalingv2.HorizontalPodAutoscalerCondition{ 3370 Type: autoscalingv2.ScalingLimited, 3371 Status: v1.ConditionFalse, 3372 Reason: "DesiredWithinRange", 3373 }), 3374 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 3375 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3376 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3377 autoscalingv2.PodsMetricSourceType: monitor.ActionLabelScaleUp, 3378 }, 3379 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3380 autoscalingv2.PodsMetricSourceType: monitor.ErrorLabelNone, 3381 }, 3382 } 3383 tc.runTest(t) 3384 } 3385 3386 func TestNoBackoffUpscaleCMNoBackoffCpu(t *testing.T) { 3387 averageValue := resource.MustParse("15.0") 3388 time := metav1.Time{Time: time.Now()} 3389 tc := testCase{ 3390 minReplicas: 1, 3391 maxReplicas: 5, 3392 specReplicas: 3, 3393 statusReplicas: 3, 3394 expectedDesiredReplicas: 5, 3395 CPUTarget: 10, 3396 metricsTarget: []autoscalingv2.MetricSpec{ 3397 { 3398 Type: autoscalingv2.PodsMetricSourceType, 3399 Pods: &autoscalingv2.PodsMetricSource{ 3400 Metric: autoscalingv2.MetricIdentifier{ 3401 Name: "qps", 3402 }, 3403 Target: autoscalingv2.MetricTarget{ 3404 Type: autoscalingv2.AverageValueMetricType, 3405 AverageValue: &averageValue, 3406 }, 3407 }, 3408 }, 3409 }, 3410 reportedLevels: []uint64{20000, 10000, 30000}, 3411 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 3412 useMetricsAPI: true, 3413 lastScaleTime: &time, 3414 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 3415 Type: autoscalingv2.AbleToScale, 3416 Status: v1.ConditionTrue, 3417 Reason: "ReadyForNewScale", 3418 }, autoscalingv2.HorizontalPodAutoscalerCondition{ 3419 Type: autoscalingv2.AbleToScale, 3420 Status: v1.ConditionTrue, 3421 Reason: "SucceededRescale", 3422 }, autoscalingv2.HorizontalPodAutoscalerCondition{ 3423 Type: autoscalingv2.ScalingLimited, 3424 Status: v1.ConditionTrue, 3425 Reason: "TooManyReplicas", 3426 }), 3427 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 3428 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3429 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3430 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 3431 autoscalingv2.PodsMetricSourceType: monitor.ActionLabelScaleUp, 3432 }, 3433 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3434 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 3435 autoscalingv2.PodsMetricSourceType: monitor.ErrorLabelNone, 3436 }, 3437 } 3438 tc.runTest(t) 3439 } 3440 3441 func TestStabilizeDownscale(t *testing.T) { 3442 tc := testCase{ 3443 minReplicas: 1, 3444 maxReplicas: 5, 3445 specReplicas: 4, 3446 statusReplicas: 4, 3447 expectedDesiredReplicas: 4, 3448 CPUTarget: 100, 3449 reportedLevels: []uint64{50, 50, 50}, 3450 reportedCPURequests: []resource.Quantity{resource.MustParse("0.1"), resource.MustParse("0.1"), resource.MustParse("0.1")}, 3451 useMetricsAPI: true, 3452 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 3453 Type: autoscalingv2.AbleToScale, 3454 Status: v1.ConditionTrue, 3455 Reason: "ReadyForNewScale", 3456 }, autoscalingv2.HorizontalPodAutoscalerCondition{ 3457 Type: autoscalingv2.AbleToScale, 3458 Status: v1.ConditionTrue, 3459 Reason: "ScaleDownStabilized", 3460 }), 3461 recommendations: []timestampedRecommendation{ 3462 {10, time.Now().Add(-10 * time.Minute)}, 3463 {4, time.Now().Add(-1 * time.Minute)}, 3464 }, 3465 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3466 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3467 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3468 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 3469 }, 3470 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3471 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 3472 }, 3473 } 3474 tc.runTest(t) 3475 } 3476 3477 // TestComputedToleranceAlgImplementation is a regression test which 3478 // back-calculates a minimal percentage for downscaling based on a small percentage 3479 // increase in pod utilization which is calibrated against the tolerance value. 3480 func TestComputedToleranceAlgImplementation(t *testing.T) { 3481 3482 startPods := int32(10) 3483 // 150 mCPU per pod. 3484 totalUsedCPUOfAllPods := uint64(startPods * 150) 3485 // Each pod starts out asking for 2X what is really needed. 3486 // This means we will have a 50% ratio of used/requested 3487 totalRequestedCPUOfAllPods := int32(2 * totalUsedCPUOfAllPods) 3488 requestedToUsed := float64(totalRequestedCPUOfAllPods / int32(totalUsedCPUOfAllPods)) 3489 // Spread the amount we ask over 10 pods. We can add some jitter later in reportedLevels. 3490 perPodRequested := totalRequestedCPUOfAllPods / startPods 3491 3492 // Force a minimal scaling event by satisfying (tolerance < 1 - resourcesUsedRatio). 3493 target := math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .01 3494 finalCPUPercentTarget := int32(target * 100) 3495 resourcesUsedRatio := float64(totalUsedCPUOfAllPods) / float64(float64(totalRequestedCPUOfAllPods)*target) 3496 3497 // i.e. .60 * 20 -> scaled down expectation. 3498 finalPods := int32(math.Ceil(resourcesUsedRatio * float64(startPods))) 3499 3500 // To breach tolerance we will create a utilization ratio difference of tolerance to usageRatioToleranceValue) 3501 tc1 := testCase{ 3502 minReplicas: 0, 3503 maxReplicas: 1000, 3504 specReplicas: startPods, 3505 statusReplicas: startPods, 3506 expectedDesiredReplicas: finalPods, 3507 CPUTarget: finalCPUPercentTarget, 3508 reportedLevels: []uint64{ 3509 totalUsedCPUOfAllPods / 10, 3510 totalUsedCPUOfAllPods / 10, 3511 totalUsedCPUOfAllPods / 10, 3512 totalUsedCPUOfAllPods / 10, 3513 totalUsedCPUOfAllPods / 10, 3514 totalUsedCPUOfAllPods / 10, 3515 totalUsedCPUOfAllPods / 10, 3516 totalUsedCPUOfAllPods / 10, 3517 totalUsedCPUOfAllPods / 10, 3518 totalUsedCPUOfAllPods / 10, 3519 }, 3520 reportedCPURequests: []resource.Quantity{ 3521 resource.MustParse(fmt.Sprint(perPodRequested+100) + "m"), 3522 resource.MustParse(fmt.Sprint(perPodRequested-100) + "m"), 3523 resource.MustParse(fmt.Sprint(perPodRequested+10) + "m"), 3524 resource.MustParse(fmt.Sprint(perPodRequested-10) + "m"), 3525 resource.MustParse(fmt.Sprint(perPodRequested+2) + "m"), 3526 resource.MustParse(fmt.Sprint(perPodRequested-2) + "m"), 3527 resource.MustParse(fmt.Sprint(perPodRequested+1) + "m"), 3528 resource.MustParse(fmt.Sprint(perPodRequested-1) + "m"), 3529 resource.MustParse(fmt.Sprint(perPodRequested) + "m"), 3530 resource.MustParse(fmt.Sprint(perPodRequested) + "m"), 3531 }, 3532 useMetricsAPI: true, 3533 recommendations: []timestampedRecommendation{}, 3534 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 3535 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3536 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3537 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 3538 }, 3539 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3540 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 3541 }, 3542 } 3543 tc1.runTest(t) 3544 3545 target = math.Abs(1/(requestedToUsed*(1-defaultTestingTolerance))) + .004 3546 finalCPUPercentTarget = int32(target * 100) 3547 tc2 := testCase{ 3548 minReplicas: 0, 3549 maxReplicas: 1000, 3550 specReplicas: startPods, 3551 statusReplicas: startPods, 3552 expectedDesiredReplicas: startPods, 3553 CPUTarget: finalCPUPercentTarget, 3554 reportedLevels: []uint64{ 3555 totalUsedCPUOfAllPods / 10, 3556 totalUsedCPUOfAllPods / 10, 3557 totalUsedCPUOfAllPods / 10, 3558 totalUsedCPUOfAllPods / 10, 3559 totalUsedCPUOfAllPods / 10, 3560 totalUsedCPUOfAllPods / 10, 3561 totalUsedCPUOfAllPods / 10, 3562 totalUsedCPUOfAllPods / 10, 3563 totalUsedCPUOfAllPods / 10, 3564 totalUsedCPUOfAllPods / 10, 3565 }, 3566 reportedCPURequests: []resource.Quantity{ 3567 resource.MustParse(fmt.Sprint(perPodRequested+100) + "m"), 3568 resource.MustParse(fmt.Sprint(perPodRequested-100) + "m"), 3569 resource.MustParse(fmt.Sprint(perPodRequested+10) + "m"), 3570 resource.MustParse(fmt.Sprint(perPodRequested-10) + "m"), 3571 resource.MustParse(fmt.Sprint(perPodRequested+2) + "m"), 3572 resource.MustParse(fmt.Sprint(perPodRequested-2) + "m"), 3573 resource.MustParse(fmt.Sprint(perPodRequested+1) + "m"), 3574 resource.MustParse(fmt.Sprint(perPodRequested-1) + "m"), 3575 resource.MustParse(fmt.Sprint(perPodRequested) + "m"), 3576 resource.MustParse(fmt.Sprint(perPodRequested) + "m"), 3577 }, 3578 useMetricsAPI: true, 3579 recommendations: []timestampedRecommendation{}, 3580 expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{ 3581 Type: autoscalingv2.AbleToScale, 3582 Status: v1.ConditionTrue, 3583 Reason: "ReadyForNewScale", 3584 }), 3585 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3586 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3587 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3588 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelNone, 3589 }, 3590 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3591 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 3592 }, 3593 } 3594 tc2.runTest(t) 3595 } 3596 3597 func TestScaleUpRCImmediately(t *testing.T) { 3598 time := metav1.Time{Time: time.Now()} 3599 tc := testCase{ 3600 minReplicas: 2, 3601 maxReplicas: 6, 3602 specReplicas: 1, 3603 statusReplicas: 1, 3604 expectedDesiredReplicas: 2, 3605 verifyCPUCurrent: false, 3606 reportedLevels: []uint64{0, 0, 0, 0}, 3607 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 3608 useMetricsAPI: true, 3609 lastScaleTime: &time, 3610 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 3611 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"}, 3612 }, 3613 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 3614 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3615 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 3616 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 3617 } 3618 tc.runTest(t) 3619 } 3620 3621 func TestScaleDownRCImmediately(t *testing.T) { 3622 time := metav1.Time{Time: time.Now()} 3623 tc := testCase{ 3624 minReplicas: 2, 3625 maxReplicas: 5, 3626 specReplicas: 6, 3627 statusReplicas: 6, 3628 expectedDesiredReplicas: 5, 3629 CPUTarget: 50, 3630 reportedLevels: []uint64{8000, 9500, 1000}, 3631 reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")}, 3632 useMetricsAPI: true, 3633 lastScaleTime: &time, 3634 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 3635 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededRescale"}, 3636 }, 3637 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleDown, 3638 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3639 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{}, 3640 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{}, 3641 } 3642 tc.runTest(t) 3643 } 3644 3645 func TestAvoidUnnecessaryUpdates(t *testing.T) { 3646 now := metav1.Time{Time: time.Now().Add(-time.Hour)} 3647 tc := testCase{ 3648 minReplicas: 2, 3649 maxReplicas: 6, 3650 specReplicas: 2, 3651 statusReplicas: 2, 3652 expectedDesiredReplicas: 2, 3653 CPUTarget: 30, 3654 CPUCurrent: 40, 3655 verifyCPUCurrent: true, 3656 reportedLevels: []uint64{400, 500, 700}, 3657 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 3658 reportedPodStartTime: []metav1.Time{coolCPUCreationTime(), hotCPUCreationTime(), hotCPUCreationTime()}, 3659 useMetricsAPI: true, 3660 lastScaleTime: &now, 3661 recommendations: []timestampedRecommendation{}, 3662 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 3663 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone, 3664 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 3665 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 3666 }, 3667 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 3668 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 3669 }, 3670 } 3671 testClient, _, _, _, _ := tc.prepareTestClient(t) 3672 tc.testClient = testClient 3673 testClient.PrependReactor("list", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 3674 tc.Lock() 3675 defer tc.Unlock() 3676 // fake out the verification logic and mark that we're done processing 3677 go func() { 3678 // wait a tick and then mark that we're finished (otherwise, we have no 3679 // way to indicate that we're finished, because the function decides not to do anything) 3680 time.Sleep(1 * time.Second) 3681 tc.Lock() 3682 tc.statusUpdated = true 3683 tc.Unlock() 3684 tc.processed <- "test-hpa" 3685 }() 3686 3687 var eighty int32 = 80 3688 3689 quantity := resource.MustParse("400m") 3690 obj := &autoscalingv2.HorizontalPodAutoscalerList{ 3691 Items: []autoscalingv2.HorizontalPodAutoscaler{ 3692 { 3693 ObjectMeta: metav1.ObjectMeta{ 3694 Name: "test-hpa", 3695 Namespace: "test-namespace", 3696 }, 3697 Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ 3698 ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ 3699 Kind: "ReplicationController", 3700 Name: "test-rc", 3701 APIVersion: "v1", 3702 }, 3703 Metrics: []autoscalingv2.MetricSpec{{ 3704 Type: autoscalingv2.ResourceMetricSourceType, 3705 Resource: &autoscalingv2.ResourceMetricSource{ 3706 Name: v1.ResourceCPU, 3707 Target: autoscalingv2.MetricTarget{ 3708 Type: autoscalingv2.UtilizationMetricType, 3709 // TODO: Change this to &tc.CPUTarget and the expected ScaleLimited 3710 // condition to False. This test incorrectly leaves the v1 3711 // HPA field TargetCPUUtilizization field blank and the 3712 // controller defaults to a target of 80. So the test relies 3713 // on downscale stabilization to prevent a scale change. 3714 AverageUtilization: &eighty, 3715 }, 3716 }, 3717 }}, 3718 MinReplicas: &tc.minReplicas, 3719 MaxReplicas: tc.maxReplicas, 3720 }, 3721 Status: autoscalingv2.HorizontalPodAutoscalerStatus{ 3722 CurrentReplicas: tc.specReplicas, 3723 DesiredReplicas: tc.specReplicas, 3724 LastScaleTime: tc.lastScaleTime, 3725 CurrentMetrics: []autoscalingv2.MetricStatus{ 3726 { 3727 Type: autoscalingv2.ResourceMetricSourceType, 3728 Resource: &autoscalingv2.ResourceMetricStatus{ 3729 Name: v1.ResourceCPU, 3730 Current: autoscalingv2.MetricValueStatus{ 3731 AverageValue: &quantity, 3732 AverageUtilization: &tc.CPUCurrent, 3733 }, 3734 }, 3735 }, 3736 }, 3737 Conditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 3738 { 3739 Type: autoscalingv2.AbleToScale, 3740 Status: v1.ConditionTrue, 3741 LastTransitionTime: *tc.lastScaleTime, 3742 Reason: "ReadyForNewScale", 3743 Message: "recommended size matches current size", 3744 }, 3745 { 3746 Type: autoscalingv2.ScalingActive, 3747 Status: v1.ConditionTrue, 3748 LastTransitionTime: *tc.lastScaleTime, 3749 Reason: "ValidMetricFound", 3750 Message: "the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)", 3751 }, 3752 { 3753 Type: autoscalingv2.ScalingLimited, 3754 Status: v1.ConditionTrue, 3755 LastTransitionTime: *tc.lastScaleTime, 3756 Reason: "TooFewReplicas", 3757 Message: "the desired replica count is less than the minimum replica count", 3758 }, 3759 }, 3760 }, 3761 }, 3762 }, 3763 } 3764 3765 return true, obj, nil 3766 }) 3767 testClient.PrependReactor("update", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 3768 assert.Fail(t, "should not have attempted to update the HPA when nothing changed") 3769 // mark that we've processed this HPA 3770 tc.processed <- "" 3771 return true, nil, fmt.Errorf("unexpected call") 3772 }) 3773 3774 controller, informerFactory := tc.setupController(t) 3775 tc.runTestWithController(t, controller, informerFactory) 3776 } 3777 3778 func TestConvertDesiredReplicasWithRules(t *testing.T) { 3779 conversionTestCases := []struct { 3780 currentReplicas int32 3781 expectedDesiredReplicas int32 3782 hpaMinReplicas int32 3783 hpaMaxReplicas int32 3784 expectedConvertedDesiredReplicas int32 3785 expectedCondition string 3786 annotation string 3787 }{ 3788 { 3789 currentReplicas: 5, 3790 expectedDesiredReplicas: 7, 3791 hpaMinReplicas: 3, 3792 hpaMaxReplicas: 8, 3793 expectedConvertedDesiredReplicas: 7, 3794 expectedCondition: "DesiredWithinRange", 3795 annotation: "prenormalized desired replicas within range", 3796 }, 3797 { 3798 currentReplicas: 3, 3799 expectedDesiredReplicas: 1, 3800 hpaMinReplicas: 2, 3801 hpaMaxReplicas: 8, 3802 expectedConvertedDesiredReplicas: 2, 3803 expectedCondition: "TooFewReplicas", 3804 annotation: "prenormalized desired replicas < minReplicas", 3805 }, 3806 { 3807 currentReplicas: 1, 3808 expectedDesiredReplicas: 0, 3809 hpaMinReplicas: 0, 3810 hpaMaxReplicas: 10, 3811 expectedConvertedDesiredReplicas: 0, 3812 expectedCondition: "DesiredWithinRange", 3813 annotation: "prenormalized desired zeroed replicas within range", 3814 }, 3815 { 3816 currentReplicas: 20, 3817 expectedDesiredReplicas: 1000, 3818 hpaMinReplicas: 1, 3819 hpaMaxReplicas: 10, 3820 expectedConvertedDesiredReplicas: 10, 3821 expectedCondition: "TooManyReplicas", 3822 annotation: "maxReplicas is the limit because maxReplicas < scaleUpLimit", 3823 }, 3824 { 3825 currentReplicas: 3, 3826 expectedDesiredReplicas: 1000, 3827 hpaMinReplicas: 1, 3828 hpaMaxReplicas: 2000, 3829 expectedConvertedDesiredReplicas: calculateScaleUpLimit(3), 3830 expectedCondition: "ScaleUpLimit", 3831 annotation: "scaleUpLimit is the limit because scaleUpLimit < maxReplicas", 3832 }, 3833 } 3834 3835 for _, ctc := range conversionTestCases { 3836 t.Run(ctc.annotation, func(t *testing.T) { 3837 actualConvertedDesiredReplicas, actualCondition, _ := convertDesiredReplicasWithRules( 3838 ctc.currentReplicas, ctc.expectedDesiredReplicas, ctc.hpaMinReplicas, ctc.hpaMaxReplicas, 3839 ) 3840 3841 assert.Equal(t, ctc.expectedConvertedDesiredReplicas, actualConvertedDesiredReplicas, ctc.annotation) 3842 assert.Equal(t, ctc.expectedCondition, actualCondition, ctc.annotation) 3843 }) 3844 } 3845 } 3846 3847 func TestCalculateScaleUpLimitWithScalingRules(t *testing.T) { 3848 policy := autoscalingv2.MinChangePolicySelect 3849 3850 calculated := calculateScaleUpLimitWithScalingRules(1, []timestampedScaleEvent{}, []timestampedScaleEvent{}, &autoscalingv2.HPAScalingRules{ 3851 StabilizationWindowSeconds: pointer.Int32(300), 3852 SelectPolicy: &policy, 3853 Policies: []autoscalingv2.HPAScalingPolicy{ 3854 { 3855 Type: autoscalingv2.PodsScalingPolicy, 3856 Value: 2, 3857 PeriodSeconds: 60, 3858 }, 3859 { 3860 Type: autoscalingv2.PercentScalingPolicy, 3861 Value: 50, 3862 PeriodSeconds: 60, 3863 }, 3864 }, 3865 }) 3866 assert.Equal(t, calculated, int32(2)) 3867 } 3868 3869 func TestCalculateScaleDownLimitWithBehaviors(t *testing.T) { 3870 policy := autoscalingv2.MinChangePolicySelect 3871 3872 calculated := calculateScaleDownLimitWithBehaviors(5, []timestampedScaleEvent{}, []timestampedScaleEvent{}, &autoscalingv2.HPAScalingRules{ 3873 StabilizationWindowSeconds: pointer.Int32(300), 3874 SelectPolicy: &policy, 3875 Policies: []autoscalingv2.HPAScalingPolicy{ 3876 { 3877 Type: autoscalingv2.PodsScalingPolicy, 3878 Value: 2, 3879 PeriodSeconds: 60, 3880 }, 3881 { 3882 Type: autoscalingv2.PercentScalingPolicy, 3883 Value: 50, 3884 PeriodSeconds: 60, 3885 }, 3886 }, 3887 }) 3888 assert.Equal(t, calculated, int32(3)) 3889 } 3890 3891 func generateScalingRules(pods, podsPeriod, percent, percentPeriod, stabilizationWindow int32) *autoscalingv2.HPAScalingRules { 3892 policy := autoscalingv2.MaxChangePolicySelect 3893 directionBehavior := autoscalingv2.HPAScalingRules{ 3894 StabilizationWindowSeconds: pointer.Int32(stabilizationWindow), 3895 SelectPolicy: &policy, 3896 } 3897 if pods != 0 { 3898 directionBehavior.Policies = append(directionBehavior.Policies, 3899 autoscalingv2.HPAScalingPolicy{Type: autoscalingv2.PodsScalingPolicy, Value: pods, PeriodSeconds: podsPeriod}) 3900 } 3901 if percent != 0 { 3902 directionBehavior.Policies = append(directionBehavior.Policies, 3903 autoscalingv2.HPAScalingPolicy{Type: autoscalingv2.PercentScalingPolicy, Value: percent, PeriodSeconds: percentPeriod}) 3904 } 3905 return &directionBehavior 3906 } 3907 3908 // generateEventsUniformDistribution generates events that uniformly spread in the time window 3909 // 3910 // time.Now()-periodSeconds ; time.Now() 3911 // 3912 // It split the time window into several segments (by the number of events) and put the event in the center of the segment 3913 // it is needed if you want to create events for several policies (to check how "outdated" flag is set). 3914 // E.g. generateEventsUniformDistribution([]int{1,2,3,4}, 120) will spread events uniformly for the last 120 seconds: 3915 // 3916 // 1 2 3 4 3917 // 3918 // ----------------------------------------------- 3919 // 3920 // ^ ^ ^ ^ ^ 3921 // 3922 // -120s -90s -60s -30s now() 3923 // And we can safely have two different stabilizationWindows: 3924 // - 60s (guaranteed to have last half of events) 3925 // - 120s (guaranteed to have all events) 3926 func generateEventsUniformDistribution(rawEvents []int, periodSeconds int) []timestampedScaleEvent { 3927 events := make([]timestampedScaleEvent, len(rawEvents)) 3928 segmentDuration := float64(periodSeconds) / float64(len(rawEvents)) 3929 for idx, event := range rawEvents { 3930 segmentBoundary := time.Duration(float64(periodSeconds) - segmentDuration*float64(idx+1) + segmentDuration/float64(2)) 3931 events[idx] = timestampedScaleEvent{ 3932 replicaChange: int32(event), 3933 timestamp: time.Now().Add(-time.Second * segmentBoundary), 3934 } 3935 } 3936 return events 3937 } 3938 3939 func TestNormalizeDesiredReplicas(t *testing.T) { 3940 tests := []struct { 3941 name string 3942 key string 3943 recommendations []timestampedRecommendation 3944 prenormalizedDesiredReplicas int32 3945 expectedStabilizedReplicas int32 3946 expectedLogLength int 3947 }{ 3948 { 3949 "empty log", 3950 "", 3951 []timestampedRecommendation{}, 3952 5, 3953 5, 3954 1, 3955 }, 3956 { 3957 "stabilize", 3958 "", 3959 []timestampedRecommendation{ 3960 {4, time.Now().Add(-2 * time.Minute)}, 3961 {5, time.Now().Add(-1 * time.Minute)}, 3962 }, 3963 3, 3964 5, 3965 3, 3966 }, 3967 { 3968 "no stabilize", 3969 "", 3970 []timestampedRecommendation{ 3971 {1, time.Now().Add(-2 * time.Minute)}, 3972 {2, time.Now().Add(-1 * time.Minute)}, 3973 }, 3974 3, 3975 3, 3976 3, 3977 }, 3978 { 3979 "no stabilize - old recommendations", 3980 "", 3981 []timestampedRecommendation{ 3982 {10, time.Now().Add(-10 * time.Minute)}, 3983 {9, time.Now().Add(-9 * time.Minute)}, 3984 }, 3985 3, 3986 3, 3987 2, 3988 }, 3989 { 3990 "stabilize - old recommendations", 3991 "", 3992 []timestampedRecommendation{ 3993 {10, time.Now().Add(-10 * time.Minute)}, 3994 {4, time.Now().Add(-1 * time.Minute)}, 3995 {5, time.Now().Add(-2 * time.Minute)}, 3996 {9, time.Now().Add(-9 * time.Minute)}, 3997 }, 3998 3, 3999 5, 4000 4, 4001 }, 4002 } 4003 for _, tc := range tests { 4004 hc := HorizontalController{ 4005 downscaleStabilisationWindow: 5 * time.Minute, 4006 recommendations: map[string][]timestampedRecommendation{ 4007 tc.key: tc.recommendations, 4008 }, 4009 } 4010 r := hc.stabilizeRecommendation(tc.key, tc.prenormalizedDesiredReplicas) 4011 if r != tc.expectedStabilizedReplicas { 4012 t.Errorf("[%s] got %d stabilized replicas, expected %d", tc.name, r, tc.expectedStabilizedReplicas) 4013 } 4014 if len(hc.recommendations[tc.key]) != tc.expectedLogLength { 4015 t.Errorf("[%s] after stabilization recommendations log has %d entries, expected %d", tc.name, len(hc.recommendations[tc.key]), tc.expectedLogLength) 4016 } 4017 } 4018 } 4019 4020 func TestScalingWithRules(t *testing.T) { 4021 type TestCase struct { 4022 name string 4023 key string 4024 // controller arguments 4025 scaleUpEvents []timestampedScaleEvent 4026 scaleDownEvents []timestampedScaleEvent 4027 // HPA Spec arguments 4028 specMinReplicas int32 4029 specMaxReplicas int32 4030 scaleUpRules *autoscalingv2.HPAScalingRules 4031 scaleDownRules *autoscalingv2.HPAScalingRules 4032 // external world state 4033 currentReplicas int32 4034 prenormalizedDesiredReplicas int32 4035 // test expected result 4036 expectedReplicas int32 4037 expectedCondition string 4038 4039 testThis bool 4040 } 4041 4042 tests := []TestCase{ 4043 { 4044 currentReplicas: 5, 4045 prenormalizedDesiredReplicas: 7, 4046 specMinReplicas: 3, 4047 specMaxReplicas: 8, 4048 expectedReplicas: 7, 4049 expectedCondition: "DesiredWithinRange", 4050 name: "prenormalized desired replicas within range", 4051 }, 4052 { 4053 currentReplicas: 3, 4054 prenormalizedDesiredReplicas: 1, 4055 specMinReplicas: 2, 4056 specMaxReplicas: 8, 4057 expectedReplicas: 2, 4058 expectedCondition: "TooFewReplicas", 4059 name: "prenormalized desired replicas < minReplicas", 4060 }, 4061 { 4062 currentReplicas: 1, 4063 prenormalizedDesiredReplicas: 0, 4064 specMinReplicas: 0, 4065 specMaxReplicas: 10, 4066 expectedReplicas: 0, 4067 expectedCondition: "DesiredWithinRange", 4068 name: "prenormalized desired replicas within range when minReplicas is 0", 4069 }, 4070 { 4071 currentReplicas: 20, 4072 prenormalizedDesiredReplicas: 1000, 4073 specMinReplicas: 1, 4074 specMaxReplicas: 10, 4075 expectedReplicas: 10, 4076 expectedCondition: "TooManyReplicas", 4077 name: "maxReplicas is the limit because maxReplicas < scaleUpLimit", 4078 }, 4079 { 4080 currentReplicas: 100, 4081 prenormalizedDesiredReplicas: 1000, 4082 specMinReplicas: 100, 4083 specMaxReplicas: 150, 4084 expectedReplicas: 150, 4085 expectedCondition: "TooManyReplicas", 4086 name: "desired replica count is more than the maximum replica count", 4087 }, 4088 { 4089 currentReplicas: 3, 4090 prenormalizedDesiredReplicas: 1000, 4091 specMinReplicas: 1, 4092 specMaxReplicas: 2000, 4093 expectedReplicas: 4, 4094 expectedCondition: "ScaleUpLimit", 4095 scaleUpRules: generateScalingRules(0, 0, 1, 60, 0), 4096 name: "scaleUpLimit is the limit because scaleUpLimit < maxReplicas with user policies", 4097 }, 4098 { 4099 currentReplicas: 1000, 4100 prenormalizedDesiredReplicas: 3, 4101 specMinReplicas: 3, 4102 specMaxReplicas: 2000, 4103 scaleDownRules: generateScalingRules(20, 60, 0, 0, 0), 4104 expectedReplicas: 980, 4105 expectedCondition: "ScaleDownLimit", 4106 name: "scaleDownLimit is the limit because scaleDownLimit > minReplicas with user defined policies", 4107 testThis: true, 4108 }, 4109 // ScaleUp without PeriodSeconds usage 4110 { 4111 name: "scaleUp with default behavior", 4112 specMinReplicas: 1, 4113 specMaxReplicas: 1000, 4114 currentReplicas: 10, 4115 prenormalizedDesiredReplicas: 50, 4116 expectedReplicas: 20, 4117 expectedCondition: "ScaleUpLimit", 4118 }, 4119 { 4120 name: "scaleUp with pods policy larger than percent policy", 4121 specMinReplicas: 1, 4122 specMaxReplicas: 1000, 4123 scaleUpRules: generateScalingRules(100, 60, 100, 60, 0), 4124 currentReplicas: 10, 4125 prenormalizedDesiredReplicas: 500, 4126 expectedReplicas: 110, 4127 expectedCondition: "ScaleUpLimit", 4128 }, 4129 { 4130 name: "scaleUp with percent policy larger than pods policy", 4131 specMinReplicas: 1, 4132 specMaxReplicas: 1000, 4133 scaleUpRules: generateScalingRules(2, 60, 100, 60, 0), 4134 currentReplicas: 10, 4135 prenormalizedDesiredReplicas: 500, 4136 expectedReplicas: 20, 4137 expectedCondition: "ScaleUpLimit", 4138 }, 4139 { 4140 name: "scaleUp with spec MaxReplicas limitation with large pod policy", 4141 specMinReplicas: 1, 4142 specMaxReplicas: 1000, 4143 scaleUpRules: generateScalingRules(100, 60, 0, 0, 0), 4144 currentReplicas: 10, 4145 prenormalizedDesiredReplicas: 50, 4146 expectedReplicas: 50, 4147 expectedCondition: "DesiredWithinRange", 4148 }, 4149 { 4150 name: "scaleUp with spec MaxReplicas limitation with large percent policy", 4151 specMinReplicas: 1, 4152 specMaxReplicas: 1000, 4153 scaleUpRules: generateScalingRules(10000, 60, 0, 0, 0), 4154 currentReplicas: 10, 4155 prenormalizedDesiredReplicas: 50, 4156 expectedReplicas: 50, 4157 expectedCondition: "DesiredWithinRange", 4158 }, 4159 { 4160 name: "scaleUp with pod policy limitation", 4161 specMinReplicas: 1, 4162 specMaxReplicas: 1000, 4163 scaleUpRules: generateScalingRules(30, 60, 0, 0, 0), 4164 currentReplicas: 10, 4165 prenormalizedDesiredReplicas: 50, 4166 expectedReplicas: 40, 4167 expectedCondition: "ScaleUpLimit", 4168 }, 4169 { 4170 name: "scaleUp with percent policy limitation", 4171 specMinReplicas: 1, 4172 specMaxReplicas: 1000, 4173 scaleUpRules: generateScalingRules(0, 0, 200, 60, 0), 4174 currentReplicas: 10, 4175 prenormalizedDesiredReplicas: 50, 4176 expectedReplicas: 30, 4177 expectedCondition: "ScaleUpLimit", 4178 }, 4179 { 4180 name: "scaleDown with percent policy larger than pod policy", 4181 specMinReplicas: 1, 4182 specMaxReplicas: 1000, 4183 scaleDownRules: generateScalingRules(20, 60, 1, 60, 300), 4184 currentReplicas: 100, 4185 prenormalizedDesiredReplicas: 2, 4186 expectedReplicas: 80, 4187 expectedCondition: "ScaleDownLimit", 4188 }, 4189 { 4190 name: "scaleDown with pod policy larger than percent policy", 4191 specMinReplicas: 1, 4192 specMaxReplicas: 1000, 4193 scaleDownRules: generateScalingRules(2, 60, 1, 60, 300), 4194 currentReplicas: 100, 4195 prenormalizedDesiredReplicas: 2, 4196 expectedReplicas: 98, 4197 expectedCondition: "ScaleDownLimit", 4198 }, 4199 { 4200 name: "scaleDown with spec MinReplicas=nil limitation with large pod policy", 4201 specMinReplicas: 1, 4202 specMaxReplicas: 1000, 4203 scaleDownRules: generateScalingRules(100, 60, 0, 0, 300), 4204 currentReplicas: 10, 4205 prenormalizedDesiredReplicas: 0, 4206 expectedReplicas: 1, 4207 expectedCondition: "TooFewReplicas", 4208 }, 4209 { 4210 name: "scaleDown with spec MinReplicas limitation with large pod policy", 4211 specMinReplicas: 1, 4212 specMaxReplicas: 1000, 4213 scaleDownRules: generateScalingRules(100, 60, 0, 0, 300), 4214 currentReplicas: 10, 4215 prenormalizedDesiredReplicas: 0, 4216 expectedReplicas: 1, 4217 expectedCondition: "TooFewReplicas", 4218 }, 4219 { 4220 name: "scaleDown with spec MinReplicas limitation with large percent policy", 4221 specMinReplicas: 5, 4222 specMaxReplicas: 1000, 4223 scaleDownRules: generateScalingRules(0, 0, 100, 60, 300), 4224 currentReplicas: 10, 4225 prenormalizedDesiredReplicas: 2, 4226 expectedReplicas: 5, 4227 expectedCondition: "TooFewReplicas", 4228 }, 4229 { 4230 name: "scaleDown with pod policy limitation", 4231 specMinReplicas: 1, 4232 specMaxReplicas: 1000, 4233 scaleDownRules: generateScalingRules(5, 60, 0, 0, 300), 4234 currentReplicas: 10, 4235 prenormalizedDesiredReplicas: 2, 4236 expectedReplicas: 5, 4237 expectedCondition: "ScaleDownLimit", 4238 }, 4239 { 4240 name: "scaleDown with percent policy limitation", 4241 specMinReplicas: 1, 4242 specMaxReplicas: 1000, 4243 scaleDownRules: generateScalingRules(0, 0, 50, 60, 300), 4244 currentReplicas: 10, 4245 prenormalizedDesiredReplicas: 5, 4246 expectedReplicas: 5, 4247 expectedCondition: "DesiredWithinRange", 4248 }, 4249 { 4250 name: "scaleUp with spec MaxReplicas limitation with large pod policy and events", 4251 scaleUpEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4252 specMinReplicas: 1, 4253 specMaxReplicas: 200, 4254 scaleUpRules: generateScalingRules(300, 60, 0, 0, 0), 4255 currentReplicas: 100, 4256 prenormalizedDesiredReplicas: 500, 4257 expectedReplicas: 200, // 200 < 100 - 15 + 300 4258 expectedCondition: "TooManyReplicas", 4259 }, 4260 { 4261 name: "scaleUp with spec MaxReplicas limitation with large percent policy and events", 4262 scaleUpEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4263 specMinReplicas: 1, 4264 specMaxReplicas: 200, 4265 scaleUpRules: generateScalingRules(0, 0, 10000, 60, 0), 4266 currentReplicas: 100, 4267 prenormalizedDesiredReplicas: 500, 4268 expectedReplicas: 200, 4269 expectedCondition: "TooManyReplicas", 4270 }, 4271 { 4272 // corner case for calculating the scaleUpLimit, when we changed pod policy after a lot of scaleUp events 4273 // in this case we shouldn't allow scale up, though, the naive formula will suggest that scaleUplimit is less then CurrentReplicas (100-15+5 < 100) 4274 name: "scaleUp with currentReplicas limitation with rate.PeriodSeconds with a lot of recent scale up events", 4275 scaleUpEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4276 specMinReplicas: 1, 4277 specMaxReplicas: 1000, 4278 scaleUpRules: generateScalingRules(5, 120, 0, 0, 0), 4279 currentReplicas: 100, 4280 prenormalizedDesiredReplicas: 500, 4281 expectedReplicas: 100, // 120 seconds ago we had (100 - 15) replicas, now the rate.Pods = 5, 4282 expectedCondition: "ScaleUpLimit", 4283 }, 4284 { 4285 name: "scaleUp with pod policy and previous scale up events", 4286 scaleUpEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4287 specMinReplicas: 1, 4288 specMaxReplicas: 1000, 4289 scaleUpRules: generateScalingRules(150, 120, 0, 0, 0), 4290 currentReplicas: 100, 4291 prenormalizedDesiredReplicas: 500, 4292 expectedReplicas: 235, // 100 - 15 + 150 4293 expectedCondition: "ScaleUpLimit", 4294 }, 4295 { 4296 name: "scaleUp with percent policy and previous scale up events", 4297 scaleUpEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4298 specMinReplicas: 1, 4299 specMaxReplicas: 1000, 4300 scaleUpRules: generateScalingRules(0, 0, 200, 120, 0), 4301 currentReplicas: 100, 4302 prenormalizedDesiredReplicas: 500, 4303 expectedReplicas: 255, // (100 - 15) + 200% 4304 expectedCondition: "ScaleUpLimit", 4305 }, 4306 { 4307 name: "scaleUp with percent policy and previous scale up and down events", 4308 scaleUpEvents: generateEventsUniformDistribution([]int{4}, 120), 4309 scaleDownEvents: generateEventsUniformDistribution([]int{2}, 120), 4310 specMinReplicas: 1, 4311 specMaxReplicas: 1000, 4312 scaleUpRules: generateScalingRules(0, 0, 300, 300, 0), 4313 currentReplicas: 6, 4314 prenormalizedDesiredReplicas: 24, 4315 expectedReplicas: 16, 4316 expectedCondition: "ScaleUpLimit", 4317 }, 4318 // ScaleDown with PeriodSeconds usage 4319 { 4320 name: "scaleDown with default policy and previous events", 4321 scaleDownEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4322 specMinReplicas: 1, 4323 specMaxReplicas: 1000, 4324 currentReplicas: 10, 4325 prenormalizedDesiredReplicas: 5, 4326 expectedReplicas: 5, // without scaleDown rate limitations the PeriodSeconds does not influence anything 4327 expectedCondition: "DesiredWithinRange", 4328 }, 4329 { 4330 name: "scaleDown with spec MinReplicas=nil limitation with large pod policy and previous events", 4331 scaleDownEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4332 specMinReplicas: 1, 4333 specMaxReplicas: 1000, 4334 scaleDownRules: generateScalingRules(115, 120, 0, 0, 300), 4335 currentReplicas: 100, 4336 prenormalizedDesiredReplicas: 0, 4337 expectedReplicas: 1, 4338 expectedCondition: "TooFewReplicas", 4339 }, 4340 { 4341 name: "scaleDown with spec MinReplicas limitation with large pod policy and previous events", 4342 scaleDownEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4343 specMinReplicas: 5, 4344 specMaxReplicas: 1000, 4345 scaleDownRules: generateScalingRules(130, 120, 0, 0, 300), 4346 currentReplicas: 100, 4347 prenormalizedDesiredReplicas: 0, 4348 expectedReplicas: 5, 4349 expectedCondition: "TooFewReplicas", 4350 }, 4351 { 4352 name: "scaleDown with spec MinReplicas limitation with large percent policy and previous events", 4353 scaleDownEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4354 specMinReplicas: 5, 4355 specMaxReplicas: 1000, 4356 scaleDownRules: generateScalingRules(0, 0, 100, 120, 300), // 100% removal - is always to 0 => limited by MinReplicas 4357 currentReplicas: 100, 4358 prenormalizedDesiredReplicas: 2, 4359 expectedReplicas: 5, 4360 expectedCondition: "TooFewReplicas", 4361 }, 4362 { 4363 name: "scaleDown with pod policy limitation and previous events", 4364 scaleDownEvents: generateEventsUniformDistribution([]int{1, 5, 9}, 120), 4365 specMinReplicas: 1, 4366 specMaxReplicas: 1000, 4367 scaleDownRules: generateScalingRules(5, 120, 0, 0, 300), 4368 currentReplicas: 100, 4369 prenormalizedDesiredReplicas: 2, 4370 expectedReplicas: 100, // 100 + 15 - 5 4371 expectedCondition: "ScaleDownLimit", 4372 }, 4373 { 4374 name: "scaleDown with percent policy limitation and previous events", 4375 scaleDownEvents: generateEventsUniformDistribution([]int{2, 4, 6}, 120), 4376 specMinReplicas: 1, 4377 specMaxReplicas: 1000, 4378 scaleDownRules: generateScalingRules(0, 0, 50, 120, 300), 4379 currentReplicas: 100, 4380 prenormalizedDesiredReplicas: 0, 4381 expectedReplicas: 56, // (100 + 12) - 50% 4382 expectedCondition: "ScaleDownLimit", 4383 }, 4384 { 4385 name: "scaleDown with percent policy and previous scale up and down events", 4386 scaleUpEvents: generateEventsUniformDistribution([]int{2}, 120), 4387 scaleDownEvents: generateEventsUniformDistribution([]int{4}, 120), 4388 specMinReplicas: 1, 4389 specMaxReplicas: 1000, 4390 scaleDownRules: generateScalingRules(0, 0, 50, 180, 0), 4391 currentReplicas: 10, 4392 prenormalizedDesiredReplicas: 1, 4393 expectedReplicas: 6, 4394 expectedCondition: "ScaleDownLimit", 4395 }, 4396 { 4397 // corner case for calculating the scaleDownLimit, when we changed pod or percent policy after a lot of scaleDown events 4398 // in this case we shouldn't allow scale down, though, the naive formula will suggest that scaleDownlimit is more then CurrentReplicas (100+30-10% > 100) 4399 name: "scaleDown with previous events preventing further scale down", 4400 scaleDownEvents: generateEventsUniformDistribution([]int{10, 10, 10}, 120), 4401 specMinReplicas: 1, 4402 specMaxReplicas: 1000, 4403 scaleDownRules: generateScalingRules(0, 0, 10, 120, 300), 4404 currentReplicas: 100, 4405 prenormalizedDesiredReplicas: 0, 4406 expectedReplicas: 100, // (100 + 30) - 10% = 117 is more then 100 (currentReplicas), keep 100 4407 expectedCondition: "ScaleDownLimit", 4408 }, 4409 { 4410 // corner case, the same as above, but calculation shows that we should go below zero 4411 name: "scaleDown with with previous events still allowing more scale down", 4412 scaleDownEvents: generateEventsUniformDistribution([]int{10, 10, 10}, 120), 4413 specMinReplicas: 1, 4414 specMaxReplicas: 1000, 4415 scaleDownRules: generateScalingRules(0, 0, 1000, 120, 300), 4416 currentReplicas: 10, 4417 prenormalizedDesiredReplicas: 5, 4418 expectedReplicas: 5, // (10 + 30) - 1000% = -360 is less than 0 and less then 5 (desired by metrics), set 5 4419 expectedCondition: "DesiredWithinRange", 4420 }, 4421 { 4422 name: "check 'outdated' flag for events for one behavior for up", 4423 scaleUpEvents: generateEventsUniformDistribution([]int{8, 12, 9, 11}, 120), 4424 specMinReplicas: 1, 4425 specMaxReplicas: 1000, 4426 scaleUpRules: generateScalingRules(1000, 60, 0, 0, 0), 4427 currentReplicas: 100, 4428 prenormalizedDesiredReplicas: 200, 4429 expectedReplicas: 200, 4430 expectedCondition: "DesiredWithinRange", 4431 }, 4432 { 4433 name: "check that events were not marked 'outdated' for two different policies in the behavior for up", 4434 scaleUpEvents: generateEventsUniformDistribution([]int{8, 12, 9, 11}, 120), 4435 specMinReplicas: 1, 4436 specMaxReplicas: 1000, 4437 scaleUpRules: generateScalingRules(1000, 120, 100, 60, 0), 4438 currentReplicas: 100, 4439 prenormalizedDesiredReplicas: 200, 4440 expectedReplicas: 200, 4441 expectedCondition: "DesiredWithinRange", 4442 }, 4443 { 4444 name: "check that events were marked 'outdated' for two different policies in the behavior for up", 4445 scaleUpEvents: generateEventsUniformDistribution([]int{8, 12, 9, 11}, 120), 4446 specMinReplicas: 1, 4447 specMaxReplicas: 1000, 4448 scaleUpRules: generateScalingRules(1000, 30, 100, 60, 0), 4449 currentReplicas: 100, 4450 prenormalizedDesiredReplicas: 200, 4451 expectedReplicas: 200, 4452 expectedCondition: "DesiredWithinRange", 4453 }, 4454 { 4455 name: "check 'outdated' flag for events for one behavior for down", 4456 scaleDownEvents: generateEventsUniformDistribution([]int{8, 12, 9, 11}, 120), 4457 specMinReplicas: 1, 4458 specMaxReplicas: 1000, 4459 scaleDownRules: generateScalingRules(1000, 60, 0, 0, 300), 4460 currentReplicas: 100, 4461 prenormalizedDesiredReplicas: 5, 4462 expectedReplicas: 5, 4463 expectedCondition: "DesiredWithinRange", 4464 }, 4465 { 4466 name: "check that events were not marked 'outdated' for two different policies in the behavior for down", 4467 scaleDownEvents: generateEventsUniformDistribution([]int{8, 12, 9, 11}, 120), 4468 specMinReplicas: 1, 4469 specMaxReplicas: 1000, 4470 scaleDownRules: generateScalingRules(1000, 120, 100, 60, 300), 4471 currentReplicas: 100, 4472 prenormalizedDesiredReplicas: 5, 4473 expectedReplicas: 5, 4474 expectedCondition: "DesiredWithinRange", 4475 }, 4476 { 4477 name: "check that events were marked 'outdated' for two different policies in the behavior for down", 4478 scaleDownEvents: generateEventsUniformDistribution([]int{8, 12, 9, 11}, 120), 4479 specMinReplicas: 1, 4480 specMaxReplicas: 1000, 4481 scaleDownRules: generateScalingRules(1000, 30, 100, 60, 300), 4482 currentReplicas: 100, 4483 prenormalizedDesiredReplicas: 5, 4484 expectedReplicas: 5, 4485 expectedCondition: "DesiredWithinRange", 4486 }, 4487 } 4488 4489 for _, tc := range tests { 4490 t.Run(tc.name, func(t *testing.T) { 4491 4492 if tc.testThis { 4493 return 4494 } 4495 hc := HorizontalController{ 4496 scaleUpEvents: map[string][]timestampedScaleEvent{ 4497 tc.key: tc.scaleUpEvents, 4498 }, 4499 scaleDownEvents: map[string][]timestampedScaleEvent{ 4500 tc.key: tc.scaleDownEvents, 4501 }, 4502 } 4503 arg := NormalizationArg{ 4504 Key: tc.key, 4505 ScaleUpBehavior: autoscalingapiv2.GenerateHPAScaleUpRules(tc.scaleUpRules), 4506 ScaleDownBehavior: autoscalingapiv2.GenerateHPAScaleDownRules(tc.scaleDownRules), 4507 MinReplicas: tc.specMinReplicas, 4508 MaxReplicas: tc.specMaxReplicas, 4509 DesiredReplicas: tc.prenormalizedDesiredReplicas, 4510 CurrentReplicas: tc.currentReplicas, 4511 } 4512 4513 replicas, condition, _ := hc.convertDesiredReplicasWithBehaviorRate(arg) 4514 assert.Equal(t, tc.expectedReplicas, replicas, "expected replicas do not match with converted replicas") 4515 assert.Equal(t, tc.expectedCondition, condition, "HPA condition does not match with expected condition") 4516 }) 4517 } 4518 4519 } 4520 4521 // TestStoreScaleEvents tests events storage and usage 4522 func TestStoreScaleEvents(t *testing.T) { 4523 type TestCase struct { 4524 name string 4525 key string 4526 replicaChange int32 4527 prevScaleEvents []timestampedScaleEvent 4528 newScaleEvents []timestampedScaleEvent 4529 scalingRules *autoscalingv2.HPAScalingRules 4530 expectedReplicasChange int32 4531 } 4532 tests := []TestCase{ 4533 { 4534 name: "empty entries with default behavior", 4535 replicaChange: 5, 4536 prevScaleEvents: []timestampedScaleEvent{}, // no history -> 0 replica change 4537 newScaleEvents: []timestampedScaleEvent{}, // no behavior -> no events are stored 4538 expectedReplicasChange: 0, 4539 }, 4540 { 4541 name: "empty entries with two-policy-behavior", 4542 replicaChange: 5, 4543 prevScaleEvents: []timestampedScaleEvent{}, // no history -> 0 replica change 4544 newScaleEvents: []timestampedScaleEvent{{5, time.Now(), false}}, 4545 scalingRules: generateScalingRules(10, 60, 100, 60, 0), 4546 expectedReplicasChange: 0, 4547 }, 4548 { 4549 name: "one outdated entry to be kept untouched without behavior", 4550 replicaChange: 5, 4551 prevScaleEvents: []timestampedScaleEvent{ 4552 {7, time.Now().Add(-time.Second * time.Duration(61)), false}, // outdated event, should be replaced 4553 }, 4554 newScaleEvents: []timestampedScaleEvent{ 4555 {7, time.Now(), false}, // no behavior -> we don't touch stored events 4556 }, 4557 expectedReplicasChange: 0, 4558 }, 4559 { 4560 name: "one outdated entry to be replaced with behavior", 4561 replicaChange: 5, 4562 prevScaleEvents: []timestampedScaleEvent{ 4563 {7, time.Now().Add(-time.Second * time.Duration(61)), false}, // outdated event, should be replaced 4564 }, 4565 newScaleEvents: []timestampedScaleEvent{ 4566 {5, time.Now(), false}, 4567 }, 4568 scalingRules: generateScalingRules(10, 60, 100, 60, 0), 4569 expectedReplicasChange: 0, 4570 }, 4571 { 4572 name: "one actual entry to be not touched with behavior", 4573 replicaChange: 5, 4574 prevScaleEvents: []timestampedScaleEvent{ 4575 {7, time.Now().Add(-time.Second * time.Duration(58)), false}, 4576 }, 4577 newScaleEvents: []timestampedScaleEvent{ 4578 {7, time.Now(), false}, 4579 {5, time.Now(), false}, 4580 }, 4581 scalingRules: generateScalingRules(10, 60, 100, 60, 0), 4582 expectedReplicasChange: 7, 4583 }, 4584 { 4585 name: "two entries, one of them to be replaced", 4586 replicaChange: 5, 4587 prevScaleEvents: []timestampedScaleEvent{ 4588 {7, time.Now().Add(-time.Second * time.Duration(61)), false}, // outdated event, should be replaced 4589 {6, time.Now().Add(-time.Second * time.Duration(59)), false}, 4590 }, 4591 newScaleEvents: []timestampedScaleEvent{ 4592 {5, time.Now(), false}, 4593 {6, time.Now(), false}, 4594 }, 4595 scalingRules: generateScalingRules(10, 60, 0, 0, 0), 4596 expectedReplicasChange: 6, 4597 }, 4598 { 4599 name: "replace one entry, use policies with different periods", 4600 replicaChange: 5, 4601 prevScaleEvents: []timestampedScaleEvent{ 4602 {8, time.Now().Add(-time.Second * time.Duration(29)), false}, 4603 {6, time.Now().Add(-time.Second * time.Duration(59)), false}, 4604 {7, time.Now().Add(-time.Second * time.Duration(61)), false}, // outdated event, should be marked as outdated 4605 {9, time.Now().Add(-time.Second * time.Duration(61)), false}, // outdated event, should be replaced 4606 }, 4607 newScaleEvents: []timestampedScaleEvent{ 4608 {8, time.Now(), false}, 4609 {6, time.Now(), false}, 4610 {7, time.Now(), true}, 4611 {5, time.Now(), false}, 4612 }, 4613 scalingRules: generateScalingRules(10, 60, 100, 30, 0), 4614 expectedReplicasChange: 14, 4615 }, 4616 { 4617 name: "two entries, both actual", 4618 replicaChange: 5, 4619 prevScaleEvents: []timestampedScaleEvent{ 4620 {7, time.Now().Add(-time.Second * time.Duration(58)), false}, 4621 {6, time.Now().Add(-time.Second * time.Duration(59)), false}, 4622 }, 4623 newScaleEvents: []timestampedScaleEvent{ 4624 {7, time.Now(), false}, 4625 {6, time.Now(), false}, 4626 {5, time.Now(), false}, 4627 }, 4628 scalingRules: generateScalingRules(10, 120, 100, 30, 0), 4629 expectedReplicasChange: 13, 4630 }, 4631 } 4632 4633 for _, tc := range tests { 4634 t.Run(tc.name, func(t *testing.T) { 4635 // testing scale up 4636 var behaviorUp *autoscalingv2.HorizontalPodAutoscalerBehavior 4637 if tc.scalingRules != nil { 4638 behaviorUp = &autoscalingv2.HorizontalPodAutoscalerBehavior{ 4639 ScaleUp: tc.scalingRules, 4640 } 4641 } 4642 hcUp := HorizontalController{ 4643 scaleUpEvents: map[string][]timestampedScaleEvent{ 4644 tc.key: append([]timestampedScaleEvent{}, tc.prevScaleEvents...), 4645 }, 4646 } 4647 gotReplicasChangeUp := getReplicasChangePerPeriod(60, hcUp.scaleUpEvents[tc.key]) 4648 assert.Equal(t, tc.expectedReplicasChange, gotReplicasChangeUp) 4649 hcUp.storeScaleEvent(behaviorUp, tc.key, 10, 10+tc.replicaChange) 4650 if !assert.Len(t, hcUp.scaleUpEvents[tc.key], len(tc.newScaleEvents), "up: scale events differ in length") { 4651 return 4652 } 4653 for i, gotEvent := range hcUp.scaleUpEvents[tc.key] { 4654 expEvent := tc.newScaleEvents[i] 4655 assert.Equal(t, expEvent.replicaChange, gotEvent.replicaChange, "up: idx:%v replicaChange", i) 4656 assert.Equal(t, expEvent.outdated, gotEvent.outdated, "up: idx:%v outdated", i) 4657 } 4658 // testing scale down 4659 var behaviorDown *autoscalingv2.HorizontalPodAutoscalerBehavior 4660 if tc.scalingRules != nil { 4661 behaviorDown = &autoscalingv2.HorizontalPodAutoscalerBehavior{ 4662 ScaleDown: tc.scalingRules, 4663 } 4664 } 4665 hcDown := HorizontalController{ 4666 scaleDownEvents: map[string][]timestampedScaleEvent{ 4667 tc.key: append([]timestampedScaleEvent{}, tc.prevScaleEvents...), 4668 }, 4669 } 4670 gotReplicasChangeDown := getReplicasChangePerPeriod(60, hcDown.scaleDownEvents[tc.key]) 4671 assert.Equal(t, tc.expectedReplicasChange, gotReplicasChangeDown) 4672 hcDown.storeScaleEvent(behaviorDown, tc.key, 10, 10-tc.replicaChange) 4673 if !assert.Len(t, hcDown.scaleDownEvents[tc.key], len(tc.newScaleEvents), "down: scale events differ in length") { 4674 return 4675 } 4676 for i, gotEvent := range hcDown.scaleDownEvents[tc.key] { 4677 expEvent := tc.newScaleEvents[i] 4678 assert.Equal(t, expEvent.replicaChange, gotEvent.replicaChange, "down: idx:%v replicaChange", i) 4679 assert.Equal(t, expEvent.outdated, gotEvent.outdated, "down: idx:%v outdated", i) 4680 } 4681 }) 4682 } 4683 } 4684 4685 func TestNormalizeDesiredReplicasWithBehavior(t *testing.T) { 4686 now := time.Now() 4687 type TestCase struct { 4688 name string 4689 key string 4690 recommendations []timestampedRecommendation 4691 currentReplicas int32 4692 prenormalizedDesiredReplicas int32 4693 expectedStabilizedReplicas int32 4694 expectedRecommendations []timestampedRecommendation 4695 scaleUpStabilizationWindowSeconds int32 4696 scaleDownStabilizationWindowSeconds int32 4697 } 4698 tests := []TestCase{ 4699 { 4700 name: "empty recommendations for scaling down", 4701 key: "", 4702 recommendations: []timestampedRecommendation{}, 4703 currentReplicas: 100, 4704 prenormalizedDesiredReplicas: 5, 4705 expectedStabilizedReplicas: 5, 4706 expectedRecommendations: []timestampedRecommendation{ 4707 {5, now}, 4708 }, 4709 }, 4710 { 4711 name: "simple scale down stabilization", 4712 key: "", 4713 recommendations: []timestampedRecommendation{ 4714 {4, now.Add(-2 * time.Minute)}, 4715 {5, now.Add(-1 * time.Minute)}}, 4716 currentReplicas: 100, 4717 prenormalizedDesiredReplicas: 3, 4718 expectedStabilizedReplicas: 5, 4719 expectedRecommendations: []timestampedRecommendation{ 4720 {4, now}, 4721 {5, now}, 4722 {3, now}, 4723 }, 4724 scaleDownStabilizationWindowSeconds: 60 * 3, 4725 }, 4726 { 4727 name: "simple scale up stabilization", 4728 key: "", 4729 recommendations: []timestampedRecommendation{ 4730 {4, now.Add(-2 * time.Minute)}, 4731 {5, now.Add(-1 * time.Minute)}}, 4732 currentReplicas: 1, 4733 prenormalizedDesiredReplicas: 7, 4734 expectedStabilizedReplicas: 4, 4735 expectedRecommendations: []timestampedRecommendation{ 4736 {4, now}, 4737 {5, now}, 4738 {7, now}, 4739 }, 4740 scaleUpStabilizationWindowSeconds: 60 * 5, 4741 }, 4742 { 4743 name: "no scale down stabilization", 4744 key: "", 4745 recommendations: []timestampedRecommendation{ 4746 {1, now.Add(-2 * time.Minute)}, 4747 {2, now.Add(-1 * time.Minute)}}, 4748 currentReplicas: 100, // to apply scaleDown delay we should have current > desired 4749 prenormalizedDesiredReplicas: 3, 4750 expectedStabilizedReplicas: 3, 4751 expectedRecommendations: []timestampedRecommendation{ 4752 {1, now}, 4753 {2, now}, 4754 {3, now}, 4755 }, 4756 scaleUpStabilizationWindowSeconds: 60 * 5, 4757 }, 4758 { 4759 name: "no scale up stabilization", 4760 key: "", 4761 recommendations: []timestampedRecommendation{ 4762 {4, now.Add(-2 * time.Minute)}, 4763 {5, now.Add(-1 * time.Minute)}}, 4764 currentReplicas: 1, // to apply scaleDown delay we should have current > desired 4765 prenormalizedDesiredReplicas: 3, 4766 expectedStabilizedReplicas: 3, 4767 expectedRecommendations: []timestampedRecommendation{ 4768 {4, now}, 4769 {5, now}, 4770 {3, now}, 4771 }, 4772 scaleDownStabilizationWindowSeconds: 60 * 5, 4773 }, 4774 { 4775 name: "no scale down stabilization, reuse recommendation element", 4776 key: "", 4777 recommendations: []timestampedRecommendation{ 4778 {10, now.Add(-10 * time.Minute)}, 4779 {9, now.Add(-9 * time.Minute)}}, 4780 currentReplicas: 100, // to apply scaleDown delay we should have current > desired 4781 prenormalizedDesiredReplicas: 3, 4782 expectedStabilizedReplicas: 3, 4783 expectedRecommendations: []timestampedRecommendation{ 4784 {10, now}, 4785 {3, now}, 4786 }, 4787 }, 4788 { 4789 name: "no scale up stabilization, reuse recommendation element", 4790 key: "", 4791 recommendations: []timestampedRecommendation{ 4792 {10, now.Add(-10 * time.Minute)}, 4793 {9, now.Add(-9 * time.Minute)}}, 4794 currentReplicas: 1, 4795 prenormalizedDesiredReplicas: 100, 4796 expectedStabilizedReplicas: 100, 4797 expectedRecommendations: []timestampedRecommendation{ 4798 {10, now}, 4799 {100, now}, 4800 }, 4801 }, 4802 { 4803 name: "scale down stabilization, reuse one of obsolete recommendation element", 4804 key: "", 4805 recommendations: []timestampedRecommendation{ 4806 {10, now.Add(-10 * time.Minute)}, 4807 {4, now.Add(-1 * time.Minute)}, 4808 {5, now.Add(-2 * time.Minute)}, 4809 {9, now.Add(-9 * time.Minute)}}, 4810 currentReplicas: 100, 4811 prenormalizedDesiredReplicas: 3, 4812 expectedStabilizedReplicas: 5, 4813 expectedRecommendations: []timestampedRecommendation{ 4814 {10, now}, 4815 {4, now}, 4816 {5, now}, 4817 {3, now}, 4818 }, 4819 scaleDownStabilizationWindowSeconds: 3 * 60, 4820 }, 4821 { 4822 // we can reuse only the first recommendation element 4823 // as the scale up delay = 150 (set in test), scale down delay = 300 (by default) 4824 // hence, only the first recommendation is obsolete for both scale up and scale down 4825 name: "scale up stabilization, reuse one of obsolete recommendation element", 4826 key: "", 4827 recommendations: []timestampedRecommendation{ 4828 {10, now.Add(-100 * time.Minute)}, 4829 {6, now.Add(-1 * time.Minute)}, 4830 {5, now.Add(-2 * time.Minute)}, 4831 {9, now.Add(-3 * time.Minute)}}, 4832 currentReplicas: 1, 4833 prenormalizedDesiredReplicas: 100, 4834 expectedStabilizedReplicas: 5, 4835 expectedRecommendations: []timestampedRecommendation{ 4836 {100, now}, 4837 {6, now}, 4838 {5, now}, 4839 {9, now}, 4840 }, 4841 scaleUpStabilizationWindowSeconds: 300, 4842 }, { 4843 name: "scale up and down stabilization, do not scale up when prenormalized rec goes down", 4844 key: "", 4845 recommendations: []timestampedRecommendation{ 4846 {2, now.Add(-100 * time.Minute)}, 4847 {3, now.Add(-3 * time.Minute)}, 4848 }, 4849 currentReplicas: 2, 4850 prenormalizedDesiredReplicas: 1, 4851 expectedStabilizedReplicas: 2, 4852 scaleUpStabilizationWindowSeconds: 300, 4853 scaleDownStabilizationWindowSeconds: 300, 4854 }, { 4855 name: "scale up and down stabilization, do not scale down when prenormalized rec goes up", 4856 key: "", 4857 recommendations: []timestampedRecommendation{ 4858 {2, now.Add(-100 * time.Minute)}, 4859 {1, now.Add(-3 * time.Minute)}, 4860 }, 4861 currentReplicas: 2, 4862 prenormalizedDesiredReplicas: 3, 4863 expectedStabilizedReplicas: 2, 4864 scaleUpStabilizationWindowSeconds: 300, 4865 scaleDownStabilizationWindowSeconds: 300, 4866 }, 4867 } 4868 for _, tc := range tests { 4869 t.Run(tc.name, func(t *testing.T) { 4870 hc := HorizontalController{ 4871 recommendations: map[string][]timestampedRecommendation{ 4872 tc.key: tc.recommendations, 4873 }, 4874 } 4875 arg := NormalizationArg{ 4876 Key: tc.key, 4877 DesiredReplicas: tc.prenormalizedDesiredReplicas, 4878 CurrentReplicas: tc.currentReplicas, 4879 ScaleUpBehavior: &autoscalingv2.HPAScalingRules{ 4880 StabilizationWindowSeconds: &tc.scaleUpStabilizationWindowSeconds, 4881 }, 4882 ScaleDownBehavior: &autoscalingv2.HPAScalingRules{ 4883 StabilizationWindowSeconds: &tc.scaleDownStabilizationWindowSeconds, 4884 }, 4885 } 4886 r, _, _ := hc.stabilizeRecommendationWithBehaviors(arg) 4887 assert.Equal(t, tc.expectedStabilizedReplicas, r, "expected replicas do not match") 4888 if tc.expectedRecommendations != nil { 4889 if !assert.Len(t, hc.recommendations[tc.key], len(tc.expectedRecommendations), "stored recommendations differ in length") { 4890 return 4891 } 4892 for i, r := range hc.recommendations[tc.key] { 4893 expectedRecommendation := tc.expectedRecommendations[i] 4894 assert.Equal(t, expectedRecommendation.recommendation, r.recommendation, "stored recommendation differs at position %d", i) 4895 } 4896 } 4897 }) 4898 } 4899 } 4900 4901 func TestScaleUpOneMetricEmpty(t *testing.T) { 4902 tc := testCase{ 4903 minReplicas: 2, 4904 maxReplicas: 6, 4905 specReplicas: 3, 4906 statusReplicas: 3, 4907 expectedDesiredReplicas: 4, 4908 CPUTarget: 30, 4909 verifyCPUCurrent: true, 4910 metricsTarget: []autoscalingv2.MetricSpec{ 4911 { 4912 Type: autoscalingv2.ExternalMetricSourceType, 4913 External: &autoscalingv2.ExternalMetricSource{ 4914 Metric: autoscalingv2.MetricIdentifier{ 4915 Name: "qps", 4916 Selector: &metav1.LabelSelector{}, 4917 }, 4918 Target: autoscalingv2.MetricTarget{ 4919 Type: autoscalingv2.ValueMetricType, 4920 Value: resource.NewMilliQuantity(100, resource.DecimalSI), 4921 }, 4922 }, 4923 }, 4924 }, 4925 reportedLevels: []uint64{300, 400, 500}, 4926 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 4927 expectedReportedReconciliationActionLabel: monitor.ActionLabelScaleUp, 4928 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 4929 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 4930 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleUp, 4931 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelNone, 4932 }, 4933 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 4934 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 4935 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelInternal, 4936 }, 4937 } 4938 _, _, _, testEMClient, _ := tc.prepareTestClient(t) 4939 testEMClient.PrependReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) { 4940 return true, &emapi.ExternalMetricValueList{}, fmt.Errorf("something went wrong") 4941 }) 4942 tc.testEMClient = testEMClient 4943 tc.runTest(t) 4944 } 4945 4946 func TestNoScaleDownOneMetricInvalid(t *testing.T) { 4947 tc := testCase{ 4948 minReplicas: 2, 4949 maxReplicas: 6, 4950 specReplicas: 5, 4951 statusReplicas: 5, 4952 expectedDesiredReplicas: 5, 4953 CPUTarget: 50, 4954 metricsTarget: []autoscalingv2.MetricSpec{ 4955 { 4956 Type: "CheddarCheese", 4957 }, 4958 }, 4959 reportedLevels: []uint64{100, 300, 500, 250, 250}, 4960 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 4961 useMetricsAPI: true, 4962 recommendations: []timestampedRecommendation{}, 4963 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 4964 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"}, 4965 {Type: autoscalingv2.ScalingActive, Status: v1.ConditionFalse, Reason: "InvalidMetricSourceType"}, 4966 }, 4967 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 4968 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 4969 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 4970 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 4971 "CheddarCheese": monitor.ActionLabelNone, 4972 }, 4973 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 4974 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 4975 "CheddarCheese": monitor.ErrorLabelSpec, 4976 }, 4977 } 4978 4979 tc.runTest(t) 4980 } 4981 4982 func TestNoScaleDownOneMetricEmpty(t *testing.T) { 4983 tc := testCase{ 4984 minReplicas: 2, 4985 maxReplicas: 6, 4986 specReplicas: 5, 4987 statusReplicas: 5, 4988 expectedDesiredReplicas: 5, 4989 CPUTarget: 50, 4990 metricsTarget: []autoscalingv2.MetricSpec{ 4991 { 4992 Type: autoscalingv2.ExternalMetricSourceType, 4993 External: &autoscalingv2.ExternalMetricSource{ 4994 Metric: autoscalingv2.MetricIdentifier{ 4995 Name: "qps", 4996 Selector: &metav1.LabelSelector{}, 4997 }, 4998 Target: autoscalingv2.MetricTarget{ 4999 Type: autoscalingv2.ValueMetricType, 5000 Value: resource.NewMilliQuantity(1000, resource.DecimalSI), 5001 }, 5002 }, 5003 }, 5004 }, 5005 reportedLevels: []uint64{100, 300, 500, 250, 250}, 5006 reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")}, 5007 useMetricsAPI: true, 5008 recommendations: []timestampedRecommendation{}, 5009 expectedConditions: []autoscalingv2.HorizontalPodAutoscalerCondition{ 5010 {Type: autoscalingv2.AbleToScale, Status: v1.ConditionTrue, Reason: "SucceededGetScale"}, 5011 {Type: autoscalingv2.ScalingActive, Status: v1.ConditionFalse, Reason: "FailedGetExternalMetric"}, 5012 }, 5013 expectedReportedReconciliationActionLabel: monitor.ActionLabelNone, 5014 expectedReportedReconciliationErrorLabel: monitor.ErrorLabelInternal, 5015 expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{ 5016 autoscalingv2.ResourceMetricSourceType: monitor.ActionLabelScaleDown, 5017 autoscalingv2.ExternalMetricSourceType: monitor.ActionLabelNone, 5018 }, 5019 expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{ 5020 autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone, 5021 autoscalingv2.ExternalMetricSourceType: monitor.ErrorLabelInternal, 5022 }, 5023 } 5024 _, _, _, testEMClient, _ := tc.prepareTestClient(t) 5025 testEMClient.PrependReactor("list", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) { 5026 return true, &emapi.ExternalMetricValueList{}, fmt.Errorf("something went wrong") 5027 }) 5028 tc.testEMClient = testEMClient 5029 tc.runTest(t) 5030 } 5031 5032 func TestMultipleHPAs(t *testing.T) { 5033 const hpaCount = 1000 5034 const testNamespace = "dummy-namespace" 5035 5036 processed := make(chan string, hpaCount) 5037 5038 testClient := &fake.Clientset{} 5039 testScaleClient := &scalefake.FakeScaleClient{} 5040 testMetricsClient := &metricsfake.Clientset{} 5041 5042 hpaList := [hpaCount]autoscalingv2.HorizontalPodAutoscaler{} 5043 scaleUpEventsMap := map[string][]timestampedScaleEvent{} 5044 scaleDownEventsMap := map[string][]timestampedScaleEvent{} 5045 scaleList := map[string]*autoscalingv1.Scale{} 5046 podList := map[string]*v1.Pod{} 5047 5048 var minReplicas int32 = 1 5049 var cpuTarget int32 = 10 5050 5051 // generate resources (HPAs, Scales, Pods...) 5052 for i := 0; i < hpaCount; i++ { 5053 hpaName := fmt.Sprintf("dummy-hpa-%v", i) 5054 deploymentName := fmt.Sprintf("dummy-target-%v", i) 5055 labelSet := map[string]string{"name": deploymentName} 5056 selector := labels.SelectorFromSet(labelSet).String() 5057 5058 // generate HPAs 5059 h := autoscalingv2.HorizontalPodAutoscaler{ 5060 ObjectMeta: metav1.ObjectMeta{ 5061 Name: hpaName, 5062 Namespace: testNamespace, 5063 }, 5064 Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ 5065 ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ 5066 APIVersion: "apps/v1", 5067 Kind: "Deployment", 5068 Name: deploymentName, 5069 }, 5070 MinReplicas: &minReplicas, 5071 MaxReplicas: 10, 5072 Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{ 5073 ScaleUp: generateScalingRules(100, 60, 0, 0, 0), 5074 ScaleDown: generateScalingRules(2, 60, 1, 60, 300), 5075 }, 5076 Metrics: []autoscalingv2.MetricSpec{ 5077 { 5078 Type: autoscalingv2.ResourceMetricSourceType, 5079 Resource: &autoscalingv2.ResourceMetricSource{ 5080 Name: v1.ResourceCPU, 5081 Target: autoscalingv2.MetricTarget{ 5082 Type: autoscalingv2.UtilizationMetricType, 5083 AverageUtilization: &cpuTarget, 5084 }, 5085 }, 5086 }, 5087 }, 5088 }, 5089 Status: autoscalingv2.HorizontalPodAutoscalerStatus{ 5090 CurrentReplicas: 1, 5091 DesiredReplicas: 5, 5092 LastScaleTime: &metav1.Time{Time: time.Now()}, 5093 }, 5094 } 5095 hpaList[i] = h 5096 5097 // generate Scale 5098 scaleList[deploymentName] = &autoscalingv1.Scale{ 5099 ObjectMeta: metav1.ObjectMeta{ 5100 Name: deploymentName, 5101 Namespace: testNamespace, 5102 }, 5103 Spec: autoscalingv1.ScaleSpec{ 5104 Replicas: 1, 5105 }, 5106 Status: autoscalingv1.ScaleStatus{ 5107 Replicas: 1, 5108 Selector: selector, 5109 }, 5110 } 5111 5112 // generate Pods 5113 cpuRequest := resource.MustParse("1.0") 5114 pod := v1.Pod{ 5115 Status: v1.PodStatus{ 5116 Phase: v1.PodRunning, 5117 Conditions: []v1.PodCondition{ 5118 { 5119 Type: v1.PodReady, 5120 Status: v1.ConditionTrue, 5121 }, 5122 }, 5123 StartTime: &metav1.Time{Time: time.Now().Add(-10 * time.Minute)}, 5124 }, 5125 ObjectMeta: metav1.ObjectMeta{ 5126 Name: fmt.Sprintf("%s-0", deploymentName), 5127 Namespace: testNamespace, 5128 Labels: labelSet, 5129 }, 5130 5131 Spec: v1.PodSpec{ 5132 Containers: []v1.Container{ 5133 { 5134 Name: "container1", 5135 Resources: v1.ResourceRequirements{ 5136 Requests: v1.ResourceList{ 5137 v1.ResourceCPU: *resource.NewMilliQuantity(cpuRequest.MilliValue()/2, resource.DecimalSI), 5138 }, 5139 }, 5140 }, 5141 { 5142 Name: "container2", 5143 Resources: v1.ResourceRequirements{ 5144 Requests: v1.ResourceList{ 5145 v1.ResourceCPU: *resource.NewMilliQuantity(cpuRequest.MilliValue()/2, resource.DecimalSI), 5146 }, 5147 }, 5148 }, 5149 }, 5150 }, 5151 } 5152 podList[deploymentName] = &pod 5153 5154 scaleUpEventsMap[fmt.Sprintf("%s/%s", testNamespace, hpaName)] = generateEventsUniformDistribution([]int{8, 12, 9, 11}, 120) 5155 scaleDownEventsMap[fmt.Sprintf("%s/%s", testNamespace, hpaName)] = generateEventsUniformDistribution([]int{10, 10, 10}, 120) 5156 } 5157 5158 testMetricsClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { 5159 podNamePrefix := "" 5160 labelSet := map[string]string{} 5161 5162 // selector should be in form: "name=dummy-target-X" where X is the number of resource 5163 selector := action.(core.ListAction).GetListRestrictions().Labels 5164 parsedSelector := strings.Split(selector.String(), "=") 5165 if len(parsedSelector) > 1 { 5166 labelSet[parsedSelector[0]] = parsedSelector[1] 5167 podNamePrefix = parsedSelector[1] 5168 } 5169 5170 podMetric := metricsapi.PodMetrics{ 5171 ObjectMeta: metav1.ObjectMeta{ 5172 Name: fmt.Sprintf("%s-0", podNamePrefix), 5173 Namespace: testNamespace, 5174 Labels: labelSet, 5175 }, 5176 Timestamp: metav1.Time{Time: time.Now()}, 5177 Window: metav1.Duration{Duration: time.Minute}, 5178 Containers: []metricsapi.ContainerMetrics{ 5179 { 5180 Name: "container1", 5181 Usage: v1.ResourceList{ 5182 v1.ResourceCPU: *resource.NewMilliQuantity( 5183 int64(200), 5184 resource.DecimalSI), 5185 v1.ResourceMemory: *resource.NewQuantity( 5186 int64(1024*1024/2), 5187 resource.BinarySI), 5188 }, 5189 }, 5190 { 5191 Name: "container2", 5192 Usage: v1.ResourceList{ 5193 v1.ResourceCPU: *resource.NewMilliQuantity( 5194 int64(300), 5195 resource.DecimalSI), 5196 v1.ResourceMemory: *resource.NewQuantity( 5197 int64(1024*1024/2), 5198 resource.BinarySI), 5199 }, 5200 }, 5201 }, 5202 } 5203 metrics := &metricsapi.PodMetricsList{} 5204 metrics.Items = append(metrics.Items, podMetric) 5205 5206 return true, metrics, nil 5207 }) 5208 5209 metricsClient := metrics.NewRESTMetricsClient( 5210 testMetricsClient.MetricsV1beta1(), 5211 &cmfake.FakeCustomMetricsClient{}, 5212 &emfake.FakeExternalMetricsClient{}, 5213 ) 5214 5215 testScaleClient.AddReactor("get", "deployments", func(action core.Action) (handled bool, ret runtime.Object, err error) { 5216 deploymentName := action.(core.GetAction).GetName() 5217 obj := scaleList[deploymentName] 5218 return true, obj, nil 5219 }) 5220 5221 testClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { 5222 obj := &v1.PodList{} 5223 5224 // selector should be in form: "name=dummy-target-X" where X is the number of resource 5225 selector := action.(core.ListAction).GetListRestrictions().Labels 5226 parsedSelector := strings.Split(selector.String(), "=") 5227 5228 // list with filter 5229 if len(parsedSelector) > 1 { 5230 obj.Items = append(obj.Items, *podList[parsedSelector[1]]) 5231 } else { 5232 // no filter - return all pods 5233 for _, p := range podList { 5234 obj.Items = append(obj.Items, *p) 5235 } 5236 } 5237 5238 return true, obj, nil 5239 }) 5240 5241 testClient.AddReactor("list", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 5242 obj := &autoscalingv2.HorizontalPodAutoscalerList{ 5243 Items: hpaList[:], 5244 } 5245 return true, obj, nil 5246 }) 5247 5248 testClient.AddReactor("update", "horizontalpodautoscalers", func(action core.Action) (handled bool, ret runtime.Object, err error) { 5249 handled, obj, err := func() (handled bool, ret *autoscalingv2.HorizontalPodAutoscaler, err error) { 5250 obj := action.(core.UpdateAction).GetObject().(*autoscalingv2.HorizontalPodAutoscaler) 5251 assert.Equal(t, testNamespace, obj.Namespace, "the HPA namespace should be as expected") 5252 5253 return true, obj, nil 5254 }() 5255 processed <- obj.Name 5256 5257 return handled, obj, err 5258 }) 5259 5260 informerFactory := informers.NewSharedInformerFactory(testClient, controller.NoResyncPeriodFunc()) 5261 5262 tCtx := ktesting.Init(t) 5263 hpaController := NewHorizontalController( 5264 tCtx, 5265 testClient.CoreV1(), 5266 testScaleClient, 5267 testClient.AutoscalingV2(), 5268 testrestmapper.TestOnlyStaticRESTMapper(legacyscheme.Scheme), 5269 metricsClient, 5270 informerFactory.Autoscaling().V2().HorizontalPodAutoscalers(), 5271 informerFactory.Core().V1().Pods(), 5272 100*time.Millisecond, 5273 5*time.Minute, 5274 defaultTestingTolerance, 5275 defaultTestingCPUInitializationPeriod, 5276 defaultTestingDelayOfInitialReadinessStatus, 5277 ) 5278 hpaController.scaleUpEvents = scaleUpEventsMap 5279 hpaController.scaleDownEvents = scaleDownEventsMap 5280 5281 informerFactory.Start(tCtx.Done()) 5282 go hpaController.Run(tCtx, 5) 5283 5284 timeoutTime := time.After(15 * time.Second) 5285 timeout := false 5286 processedHPA := make(map[string]bool) 5287 for timeout == false && len(processedHPA) < hpaCount { 5288 select { 5289 case hpaName := <-processed: 5290 processedHPA[hpaName] = true 5291 case <-timeoutTime: 5292 timeout = true 5293 } 5294 } 5295 5296 assert.Equal(t, hpaCount, len(processedHPA), "Expected to process all HPAs") 5297 }