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