agones.dev/agones@v1.53.0/pkg/fleetautoscalers/fleetautoscalers_test.go (about) 1 /* 2 * Copyright 2018 Google LLC All Rights Reserved. 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 fleetautoscalers 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "strconv" 27 "testing" 28 "time" 29 30 "github.com/sirupsen/logrus" 31 "github.com/stretchr/testify/assert" 32 admregv1 "k8s.io/api/admissionregistration/v1" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/util/intstr" 36 k8stesting "k8s.io/client-go/testing" 37 38 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 39 autoscalingv1 "agones.dev/agones/pkg/apis/autoscaling/v1" 40 "agones.dev/agones/pkg/gameservers" 41 agtesting "agones.dev/agones/pkg/testing" 42 utilruntime "agones.dev/agones/pkg/util/runtime" 43 ) 44 45 const ( 46 scaleFactor = 2 47 webhookURL = "scale" 48 ) 49 50 type testServer struct{} 51 52 func (t testServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 53 w.Header().Set("Content-Type", "application/json") 54 if r == nil { 55 http.Error(w, "Empty request", http.StatusInternalServerError) 56 return 57 } 58 59 var faRequest autoscalingv1.FleetAutoscaleReview 60 61 res, err := io.ReadAll(r.Body) 62 if err != nil { 63 http.Error(w, err.Error(), http.StatusInternalServerError) 64 return 65 } 66 err = json.Unmarshal(res, &faRequest) 67 if err != nil { 68 http.Error(w, err.Error(), http.StatusInternalServerError) 69 return 70 } 71 72 // return different errors for tests 73 if faRequest.Request.Status.AllocatedReplicas == -10 { 74 http.Error(w, "Wrong Status Replicas Parameter", http.StatusInternalServerError) 75 return 76 } 77 78 if faRequest.Request.Status.AllocatedReplicas == -20 { 79 _, err = io.WriteString(w, "invalid data") 80 if err != nil { 81 http.Error(w, "Error writing json from /address", http.StatusInternalServerError) 82 return 83 } 84 } 85 86 faReq := faRequest.Request 87 faResp := autoscalingv1.FleetAutoscaleResponse{ 88 Scale: false, 89 Replicas: faReq.Status.Replicas, 90 UID: faReq.UID, 91 } 92 allocatedPercent := float32(faReq.Status.AllocatedReplicas) / float32(faReq.Status.Replicas) 93 if allocatedPercent > 0.7 { 94 faResp.Scale = true 95 faResp.Replicas = faReq.Status.Replicas * scaleFactor 96 } 97 98 // override value for tests 99 if faReq.Annotations != nil { 100 if value, ok := faReq.Annotations["fixedReplicas"]; ok { 101 faResp.Scale = true 102 replicas, _ := strconv.Atoi(value) 103 faResp.Replicas = int32(replicas) 104 } 105 } 106 107 review := &autoscalingv1.FleetAutoscaleReview{ 108 Request: faReq, 109 Response: &faResp, 110 } 111 112 result, err := json.Marshal(&review) 113 if err != nil { 114 http.Error(w, "Error marshaling json", http.StatusInternalServerError) 115 return 116 } 117 118 _, err = w.Write(result) 119 if err != nil { 120 http.Error(w, "Error writing json from /address", http.StatusInternalServerError) 121 return 122 } 123 } 124 125 func TestComputeDesiredFleetSize(t *testing.T) { 126 t.Parallel() 127 128 nc := map[string]gameservers.NodeCount{} 129 130 fas, f := defaultFixtures() 131 132 type expected struct { 133 replicas int32 134 limited bool 135 err string 136 } 137 138 var testCases = []struct { 139 description string 140 specReplicas int32 141 statusReplicas int32 142 statusAllocatedReplicas int32 143 statusReadyReplicas int32 144 policy autoscalingv1.FleetAutoscalerPolicy 145 expected expected 146 }{ 147 { 148 description: "Increase replicas", 149 specReplicas: 50, 150 statusReplicas: 50, 151 statusAllocatedReplicas: 40, 152 statusReadyReplicas: 10, 153 policy: autoscalingv1.FleetAutoscalerPolicy{ 154 Type: autoscalingv1.BufferPolicyType, 155 Buffer: &autoscalingv1.BufferPolicy{ 156 BufferSize: intstr.FromInt(20), 157 MaxReplicas: 100, 158 }, 159 }, 160 expected: expected{ 161 replicas: 60, 162 limited: false, 163 err: "", 164 }, 165 }, 166 { 167 description: "Wrong policy", 168 specReplicas: 50, 169 statusReplicas: 60, 170 statusAllocatedReplicas: 40, 171 statusReadyReplicas: 10, 172 policy: autoscalingv1.FleetAutoscalerPolicy{ 173 Type: "", 174 Buffer: &autoscalingv1.BufferPolicy{ 175 BufferSize: intstr.FromInt(20), 176 MaxReplicas: 100, 177 }, 178 }, 179 expected: expected{ 180 replicas: 0, 181 limited: false, 182 err: "wrong policy type, should be one of: Buffer, Webhook, Counter, List, Schedule, Chain", 183 }, 184 }, 185 } 186 187 for _, tc := range testCases { 188 t.Run(tc.description, func(t *testing.T) { 189 ctx := context.Background() 190 fas.Spec.Policy = tc.policy 191 f.Spec.Replicas = tc.specReplicas 192 f.Status.Replicas = tc.statusReplicas 193 f.Status.AllocatedReplicas = tc.statusAllocatedReplicas 194 f.Status.ReadyReplicas = tc.statusReadyReplicas 195 196 m := agtesting.NewMocks() 197 m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 198 return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{}}, nil 199 }) 200 201 gameServers := m.AgonesInformerFactory.Agones().V1().GameServers() 202 _, cancel := agtesting.StartInformers(m, gameServers.Informer().HasSynced) 203 defer cancel() 204 205 fasLog := FasLogger{ 206 fas: fas, 207 baseLogger: newTestLogger(), 208 recorder: m.FakeRecorder, 209 currChainEntry: &fas.Status.LastAppliedPolicy, 210 } 211 212 replicas, limited, err := computeDesiredFleetSize(ctx, map[string]any{}, fas.Spec.Policy, f, gameServers.Lister().GameServers(f.ObjectMeta.Namespace), nc, &fasLog) 213 214 if tc.expected.err != "" && assert.NotNil(t, err) { 215 assert.Equal(t, tc.expected.err, err.Error()) 216 } else { 217 assert.Nil(t, err) 218 assert.Equal(t, tc.expected.replicas, replicas) 219 assert.Equal(t, tc.expected.limited, limited) 220 } 221 }) 222 } 223 } 224 225 func TestApplyBufferPolicy(t *testing.T) { 226 t.Parallel() 227 228 fas, f := defaultFixtures() 229 230 type expected struct { 231 replicas int32 232 limited bool 233 err string 234 } 235 236 var testCases = []struct { 237 description string 238 specReplicas int32 239 statusReplicas int32 240 statusAllocatedReplicas int32 241 statusReadyReplicas int32 242 buffer *autoscalingv1.BufferPolicy 243 expected expected 244 }{ 245 { 246 description: "Increase replicas", 247 specReplicas: 50, 248 statusReplicas: 50, 249 statusAllocatedReplicas: 40, 250 statusReadyReplicas: 10, 251 buffer: &autoscalingv1.BufferPolicy{ 252 BufferSize: intstr.FromInt(20), 253 MaxReplicas: 100, 254 }, 255 expected: expected{ 256 replicas: 60, 257 limited: false, 258 err: "", 259 }, 260 }, 261 { 262 description: "Min replicas set, limited == true", 263 specReplicas: 50, 264 statusReplicas: 50, 265 statusAllocatedReplicas: 40, 266 statusReadyReplicas: 10, 267 buffer: &autoscalingv1.BufferPolicy{ 268 BufferSize: intstr.FromInt(20), 269 MinReplicas: 65, 270 MaxReplicas: 100, 271 }, 272 expected: expected{ 273 replicas: 65, 274 limited: true, 275 err: "", 276 }, 277 }, 278 { 279 description: "Replicas == max", 280 specReplicas: 50, 281 statusReplicas: 50, 282 statusAllocatedReplicas: 40, 283 statusReadyReplicas: 10, 284 buffer: &autoscalingv1.BufferPolicy{ 285 BufferSize: intstr.FromInt(20), 286 MinReplicas: 0, 287 MaxReplicas: 55, 288 }, 289 expected: expected{ 290 replicas: 55, 291 limited: true, 292 err: "", 293 }, 294 }, 295 { 296 description: "FromString buffer size, scale up", 297 specReplicas: 50, 298 statusReplicas: 50, 299 statusAllocatedReplicas: 50, 300 statusReadyReplicas: 0, 301 buffer: &autoscalingv1.BufferPolicy{ 302 BufferSize: intstr.FromString("20%"), 303 MinReplicas: 0, 304 MaxReplicas: 100, 305 }, 306 expected: expected{ 307 replicas: 63, 308 limited: false, 309 err: "", 310 }, 311 }, 312 { 313 description: "FromString buffer size, scale up twice", 314 specReplicas: 1, 315 statusReplicas: 1, 316 statusAllocatedReplicas: 1, 317 statusReadyReplicas: 0, 318 buffer: &autoscalingv1.BufferPolicy{ 319 BufferSize: intstr.FromString("10%"), 320 MinReplicas: 0, 321 MaxReplicas: 10, 322 }, 323 expected: expected{ 324 replicas: 2, 325 limited: false, 326 err: "", 327 }, 328 }, 329 { 330 description: "FromString buffer size is invalid, err received", 331 specReplicas: 1, 332 statusReplicas: 1, 333 statusAllocatedReplicas: 1, 334 statusReadyReplicas: 0, 335 buffer: &autoscalingv1.BufferPolicy{ 336 BufferSize: intstr.FromString("asd"), 337 MinReplicas: 0, 338 MaxReplicas: 10, 339 }, 340 expected: expected{ 341 replicas: 0, 342 limited: false, 343 err: "invalid value for IntOrString: invalid value \"asd\": strconv.Atoi: parsing \"asd\": invalid syntax", 344 }, 345 }, 346 } 347 348 for _, tc := range testCases { 349 t.Run(tc.description, func(t *testing.T) { 350 f.Spec.Replicas = tc.specReplicas 351 f.Status.Replicas = tc.statusReplicas 352 f.Status.AllocatedReplicas = tc.statusAllocatedReplicas 353 f.Status.ReadyReplicas = tc.statusReadyReplicas 354 355 m := agtesting.NewMocks() 356 fasLog := FasLogger{ 357 fas: fas, 358 baseLogger: newTestLogger(), 359 recorder: m.FakeRecorder, 360 currChainEntry: &fas.Status.LastAppliedPolicy, 361 } 362 replicas, limited, err := applyBufferPolicy(tc.buffer, f, &fasLog) 363 364 if tc.expected.err != "" && assert.NotNil(t, err) { 365 assert.Equal(t, tc.expected.err, err.Error()) 366 } else { 367 assert.Nil(t, err) 368 assert.Equal(t, tc.expected.replicas, replicas) 369 assert.Equal(t, tc.expected.limited, limited) 370 } 371 }) 372 } 373 } 374 375 func TestApplyWebhookPolicy(t *testing.T) { 376 t.Parallel() 377 ts := testServer{} 378 server := httptest.NewServer(ts) 379 defer server.Close() 380 381 fas, f := defaultWebhookFixtures() 382 url := webhookURL 383 emptyString := "" 384 invalidURL := ")1golang.org/" 385 wrongServerURL := "http://127.0.0.1:1" 386 387 type expected struct { 388 replicas int32 389 limited bool 390 err string 391 } 392 393 var testCases = []struct { 394 description string 395 webhookPolicy *autoscalingv1.URLConfiguration 396 specReplicas int32 397 statusReplicas int32 398 statusAllocatedReplicas int32 399 statusReadyReplicas int32 400 expected expected 401 }{ 402 { 403 description: "Allocated replicas per cent < 70%, no scaling", 404 webhookPolicy: &autoscalingv1.URLConfiguration{ 405 Service: nil, 406 URL: &(server.URL), 407 }, 408 specReplicas: 50, 409 statusReplicas: 50, 410 statusAllocatedReplicas: 10, 411 statusReadyReplicas: 40, 412 expected: expected{ 413 replicas: 50, 414 limited: false, 415 err: "", 416 }, 417 }, 418 { 419 description: "Allocated replicas per cent == 70%, no scaling", 420 webhookPolicy: &autoscalingv1.URLConfiguration{ 421 Service: nil, 422 URL: &(server.URL), 423 }, 424 specReplicas: 50, 425 statusReplicas: 50, 426 statusAllocatedReplicas: 35, 427 statusReadyReplicas: 15, 428 expected: expected{ 429 replicas: 50, 430 limited: false, 431 err: "", 432 }, 433 }, 434 { 435 description: "Allocated replicas per cent 80% > 70%, scale up", 436 webhookPolicy: &autoscalingv1.URLConfiguration{ 437 Service: nil, 438 URL: &(server.URL), 439 }, 440 specReplicas: 50, 441 statusReplicas: 50, 442 statusAllocatedReplicas: 40, 443 statusReadyReplicas: 10, 444 expected: expected{ 445 replicas: 50 * scaleFactor, 446 limited: false, 447 err: "", 448 }, 449 }, 450 { 451 description: "nil WebhookPolicy, error returned", 452 webhookPolicy: nil, 453 expected: expected{ 454 replicas: 0, 455 limited: false, 456 err: "webhookPolicy parameter must not be nil", 457 }, 458 }, 459 { 460 description: "URL and Service are not nil", 461 webhookPolicy: &autoscalingv1.URLConfiguration{ 462 Service: &admregv1.ServiceReference{ 463 Name: "service1", 464 Namespace: "default", 465 Path: &url, 466 }, 467 URL: &(server.URL), 468 }, 469 expected: expected{ 470 replicas: 0, 471 limited: false, 472 err: "service and URL cannot be used simultaneously", 473 }, 474 }, 475 { 476 description: "URL not nil but empty", 477 webhookPolicy: &autoscalingv1.URLConfiguration{ 478 Service: nil, 479 URL: &emptyString, 480 }, 481 expected: expected{ 482 replicas: 0, 483 limited: false, 484 err: "URL was not provided", 485 }, 486 }, 487 { 488 description: "Invalid URL", 489 webhookPolicy: &autoscalingv1.URLConfiguration{ 490 Service: nil, 491 URL: &invalidURL, 492 }, 493 expected: expected{ 494 replicas: 0, 495 limited: false, 496 err: "parse \")1golang.org/\": invalid URI for request", 497 }, 498 }, 499 { 500 description: "Service name is empty", 501 webhookPolicy: &autoscalingv1.URLConfiguration{ 502 Service: &admregv1.ServiceReference{ 503 Name: "", 504 Namespace: "default", 505 Path: &url, 506 }, 507 }, 508 expected: expected{ 509 replicas: 0, 510 limited: false, 511 err: "service name was not provided", 512 }, 513 }, 514 { 515 description: "No certs", 516 webhookPolicy: &autoscalingv1.URLConfiguration{ 517 Service: &admregv1.ServiceReference{ 518 Name: "service1", 519 Namespace: "default", 520 Path: &url, 521 }, 522 CABundle: []byte("invalid-value"), 523 }, 524 expected: expected{ 525 replicas: 0, 526 limited: false, 527 err: "no certs were appended from caBundle", 528 }, 529 }, 530 { 531 description: "Wrong server URL", 532 webhookPolicy: &autoscalingv1.URLConfiguration{ 533 Service: nil, 534 URL: &wrongServerURL, 535 }, 536 expected: expected{ 537 replicas: 0, 538 limited: false, 539 err: "Post \"http://127.0.0.1:1\": dial tcp 127.0.0.1:1: connect: connection refused", 540 }, 541 }, 542 { 543 description: "Handle server error", 544 webhookPolicy: &autoscalingv1.URLConfiguration{ 545 Service: nil, 546 URL: &(server.URL), 547 }, 548 specReplicas: 50, 549 statusReplicas: 50, 550 // hardcoded value in a server implementation 551 statusAllocatedReplicas: -10, 552 statusReadyReplicas: 40, 553 expected: expected{ 554 replicas: 50, 555 limited: false, 556 err: fmt.Sprintf("bad status code %d from the server: %s", http.StatusInternalServerError, server.URL), 557 }, 558 }, 559 { 560 description: "Handle invalid response from the server", 561 webhookPolicy: &autoscalingv1.URLConfiguration{ 562 Service: nil, 563 URL: &(server.URL), 564 }, 565 specReplicas: 50, 566 statusReplicas: 50, 567 statusAllocatedReplicas: -20, 568 statusReadyReplicas: 40, 569 expected: expected{ 570 replicas: 50, 571 limited: false, 572 err: "invalid character 'i' looking for beginning of value", 573 }, 574 }, 575 { 576 description: "Service and URL are nil", 577 webhookPolicy: &autoscalingv1.URLConfiguration{ 578 Service: nil, 579 URL: nil, 580 }, 581 expected: expected{ 582 replicas: 0, 583 limited: false, 584 err: "service was not provided, either URL or Service must be provided", 585 }, 586 }, 587 } 588 589 for _, tc := range testCases { 590 t.Run(tc.description, func(t *testing.T) { 591 f.Spec.Replicas = tc.specReplicas 592 f.Status.Replicas = tc.statusReplicas 593 f.Status.AllocatedReplicas = tc.statusAllocatedReplicas 594 f.Status.ReadyReplicas = tc.statusReadyReplicas 595 596 m := agtesting.NewMocks() 597 fasLog := FasLogger{ 598 fas: fas, 599 baseLogger: newTestLogger(), 600 recorder: m.FakeRecorder, 601 currChainEntry: &fas.Status.LastAppliedPolicy, 602 } 603 replicas, limited, err := applyWebhookPolicy(tc.webhookPolicy, f, &fasLog) 604 605 if tc.expected.err != "" && assert.NotNil(t, err) { 606 assert.Equal(t, tc.expected.err, err.Error()) 607 } else { 608 assert.Equal(t, tc.expected.replicas, replicas) 609 assert.Equal(t, tc.expected.limited, limited) 610 assert.Nil(t, err) 611 } 612 }) 613 } 614 } 615 616 func TestApplyWebhookPolicyWithMetadata(t *testing.T) { 617 t.Parallel() 618 619 utilruntime.FeatureTestMutex.Lock() 620 defer utilruntime.FeatureTestMutex.Unlock() 621 622 err := utilruntime.ParseFeatures(string(utilruntime.FeatureFleetAutoscaleRequestMetaData) + "=true") 623 assert.NoError(t, err) 624 defer func() { 625 _ = utilruntime.ParseFeatures("") 626 }() 627 628 ts := testServer{} 629 server := httptest.NewServer(ts) 630 defer server.Close() 631 632 fas, fleet := defaultWebhookFixtures() 633 634 fixedReplicas := int32(11) 635 fleet.ObjectMeta.Annotations = map[string]string{ 636 "fixedReplicas": fmt.Sprintf("%d", fixedReplicas), 637 } 638 639 webhookPolicy := &autoscalingv1.URLConfiguration{ 640 Service: nil, 641 URL: &(server.URL), 642 } 643 644 m := agtesting.NewMocks() 645 fasLog := FasLogger{ 646 fas: fas, 647 baseLogger: newTestLogger(), 648 recorder: m.FakeRecorder, 649 currChainEntry: &fas.Status.LastAppliedPolicy, 650 } 651 replicas, limited, err := applyWebhookPolicy(webhookPolicy, fleet, &fasLog) 652 653 assert.Nil(t, err) 654 assert.False(t, limited) 655 assert.Equal(t, fixedReplicas, replicas) 656 } 657 658 func TestApplyWebhookPolicyNilFleet(t *testing.T) { 659 t.Parallel() 660 661 url := webhookURL 662 w := &autoscalingv1.URLConfiguration{ 663 Service: &admregv1.ServiceReference{ 664 Name: "service1", 665 Namespace: "default", 666 Path: &url, 667 }, 668 } 669 670 fas, _ := defaultFixtures() 671 m := agtesting.NewMocks() 672 fasLog := FasLogger{ 673 fas: fas, 674 baseLogger: newTestLogger(), 675 recorder: m.FakeRecorder, 676 currChainEntry: &fas.Status.LastAppliedPolicy, 677 } 678 replicas, limited, err := applyWebhookPolicy(w, nil, &fasLog) 679 680 if assert.NotNil(t, err) { 681 assert.Equal(t, "fleet parameter must not be nil", err.Error()) 682 } 683 684 assert.False(t, limited) 685 assert.Zero(t, replicas) 686 } 687 688 func TestCreateURL(t *testing.T) { 689 t.Parallel() 690 var nonStandardPort int32 = 8888 691 692 var testCases = []struct { 693 description string 694 scheme string 695 name string 696 namespace string 697 path string 698 port *int32 699 expected string 700 }{ 701 { 702 description: "OK, path not empty", 703 scheme: "http", 704 name: "service1", 705 namespace: "default", 706 path: "scale", 707 expected: "http://service1.default.svc:8000/scale", 708 }, 709 { 710 description: "OK, path not empty with slash", 711 scheme: "http", 712 name: "service1", 713 namespace: "default", 714 path: "/scale", 715 expected: "http://service1.default.svc:8000/scale", 716 }, 717 { 718 description: "OK, path is empty", 719 scheme: "http", 720 name: "service1", 721 namespace: "default", 722 path: "", 723 expected: "http://service1.default.svc:8000", 724 }, 725 { 726 description: "OK, port specified", 727 scheme: "http", 728 name: "service1", 729 namespace: "default", 730 path: "scale", 731 port: &nonStandardPort, 732 expected: "http://service1.default.svc:8888/scale", 733 }, 734 } 735 736 for _, tc := range testCases { 737 t.Run(tc.description, func(t *testing.T) { 738 res := createURL(tc.scheme, tc.name, tc.namespace, tc.path, tc.port) 739 740 if assert.NotNil(t, res) { 741 assert.Equal(t, tc.expected, res.String()) 742 } 743 }) 744 } 745 } 746 747 func TestBuildURLFromWebhookPolicyNoNamespace(t *testing.T) { 748 url := "testurl" 749 750 type expected struct { 751 url string 752 err string 753 } 754 755 var testCases = []struct { 756 description string 757 webhookPolicy *autoscalingv1.URLConfiguration 758 expected expected 759 }{ 760 { 761 description: "No namespace provided, default should be used", 762 webhookPolicy: &autoscalingv1.URLConfiguration{ 763 Service: &admregv1.ServiceReference{ 764 Name: "service1", 765 Namespace: "", 766 Path: &url, 767 }, 768 }, 769 expected: expected{ 770 url: "http://service1.default.svc:8000/testurl", 771 err: "", 772 }, 773 }, 774 { 775 description: "No url provided, empty string should be used", 776 webhookPolicy: &autoscalingv1.URLConfiguration{ 777 Service: &admregv1.ServiceReference{ 778 Name: "service1", 779 Namespace: "test", 780 Path: nil, 781 }, 782 }, 783 expected: expected{ 784 url: "http://service1.test.svc:8000", 785 err: "", 786 }, 787 }, 788 } 789 790 for _, tc := range testCases { 791 t.Run(tc.description, func(t *testing.T) { 792 url, err := buildURLFromWebhookPolicy(tc.webhookPolicy) 793 794 if tc.expected.err != "" && assert.NotNil(t, err) { 795 assert.Equal(t, tc.expected.err, err.Error()) 796 } else { 797 assert.Nil(t, err) 798 assert.Equal(t, tc.expected.url, url.String()) 799 } 800 }) 801 } 802 } 803 804 // nolint:dupl // Linter errors on lines are duplicate of TestApplyListPolicy 805 func TestApplyCounterPolicy(t *testing.T) { 806 t.Parallel() 807 808 nc := map[string]gameservers.NodeCount{ 809 "n1": {Ready: 1, Allocated: 1}, 810 } 811 812 modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { 813 _, fleet := defaultFixtures() // The ObjectMeta.Name of the defaultFixtures fleet is "fleet-1" 814 f(fleet) 815 return fleet 816 } 817 818 type expected struct { 819 replicas int32 820 limited bool 821 wantErr bool 822 } 823 824 testCases := map[string]struct { 825 fleet *agonesv1.Fleet 826 featureFlags string 827 cp *autoscalingv1.CounterPolicy 828 gsList []agonesv1.GameServer 829 want expected 830 }{ 831 "counts and lists not enabled": { 832 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 833 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 834 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 835 Count: 0, 836 Capacity: 7} 837 f.Status.Replicas = 10 838 f.Status.ReadyReplicas = 5 839 f.Status.AllocatedReplicas = 5 840 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 841 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 842 Count: 31, 843 Capacity: 70, 844 } 845 }), 846 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=false", 847 cp: &autoscalingv1.CounterPolicy{ 848 Key: "rooms", 849 MaxCapacity: 100, 850 MinCapacity: 10, 851 BufferSize: intstr.FromInt(10), 852 }, 853 want: expected{ 854 replicas: 0, 855 limited: false, 856 wantErr: true, 857 }, 858 }, 859 "Counter based fleet does not have any replicas": { 860 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 861 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 862 f.Spec.Template.Spec.Counters["gamers"] = agonesv1.CounterStatus{ 863 Count: 0, 864 Capacity: 7} 865 f.Status.Replicas = 0 866 f.Status.ReadyReplicas = 0 867 f.Status.AllocatedReplicas = 0 868 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 869 f.Status.Counters["gamers"] = agonesv1.AggregatedCounterStatus{} 870 }), 871 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 872 cp: &autoscalingv1.CounterPolicy{ 873 Key: "gamers", 874 MaxCapacity: 100, 875 MinCapacity: 10, 876 BufferSize: intstr.FromInt(10), 877 }, 878 want: expected{ 879 replicas: 2, 880 limited: true, 881 wantErr: false, 882 }, 883 }, 884 "fleet spec does not have counter": { 885 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 886 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 887 f.Spec.Template.Spec.Counters["brooms"] = agonesv1.CounterStatus{ 888 Count: 0, 889 Capacity: 7} 890 f.Status.Replicas = 10 891 f.Status.ReadyReplicas = 5 892 f.Status.AllocatedReplicas = 5 893 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 894 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 895 Count: 31, 896 Capacity: 70, 897 } 898 }), 899 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 900 cp: &autoscalingv1.CounterPolicy{ 901 Key: "rooms", 902 MaxCapacity: 100, 903 MinCapacity: 10, 904 BufferSize: intstr.FromInt(10), 905 }, 906 want: expected{ 907 replicas: 0, 908 limited: false, 909 wantErr: true, 910 }, 911 }, 912 "fleet status does not have counter": { 913 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 914 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 915 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 916 Count: 0, 917 Capacity: 7} 918 f.Status.Replicas = 10 919 f.Status.ReadyReplicas = 5 920 f.Status.AllocatedReplicas = 5 921 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 922 f.Status.Counters["brooms"] = agonesv1.AggregatedCounterStatus{ 923 Count: 31, 924 Capacity: 70, 925 } 926 }), 927 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 928 cp: &autoscalingv1.CounterPolicy{ 929 Key: "rooms", 930 MaxCapacity: 100, 931 MinCapacity: 10, 932 BufferSize: intstr.FromInt(10), 933 }, 934 want: expected{ 935 replicas: 0, 936 limited: false, 937 wantErr: true, 938 }, 939 }, 940 "scale down": { 941 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 942 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 943 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 944 Count: 0, 945 Capacity: 7} 946 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Ascending"}} 947 f.Status.Replicas = 8 948 f.Status.ReadyReplicas = 4 949 f.Status.AllocatedReplicas = 4 950 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 951 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 952 Count: 31, 953 Capacity: 70, 954 } 955 }), 956 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 957 cp: &autoscalingv1.CounterPolicy{ 958 Key: "rooms", 959 MaxCapacity: 100, 960 MinCapacity: 10, 961 BufferSize: intstr.FromInt(10), 962 }, 963 gsList: []agonesv1.GameServer{ 964 {ObjectMeta: metav1.ObjectMeta{ 965 Name: "gs1", 966 Namespace: "default", 967 // We need the Label here so that the Lister can pick up that the gameserver is a part of 968 // the fleet. If this was a real gameserver it would also have a label for 969 // "agones.dev/gameserverset": "gameServerSetName". 970 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 971 Status: agonesv1.GameServerStatus{ 972 // We need NodeName here for sorting, otherwise sortGameServersByLeastFullNodes 973 // will return the list of GameServers in the opposite order the were return by 974 // ListGameServersByGameServerSetOwner (which is a nondeterministic order). 975 NodeName: "n1", 976 Counters: map[string]agonesv1.CounterStatus{ 977 "rooms": { 978 Count: 10, 979 Capacity: 10, 980 }}}}, 981 {ObjectMeta: metav1.ObjectMeta{ 982 Name: "gs2", 983 Namespace: "default", 984 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 985 Status: agonesv1.GameServerStatus{ 986 NodeName: "n1", 987 Counters: map[string]agonesv1.CounterStatus{ 988 "rooms": { 989 Count: 3, 990 Capacity: 5, 991 }}}}, 992 {ObjectMeta: metav1.ObjectMeta{ 993 Name: "gs3", 994 Namespace: "default", 995 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 996 Status: agonesv1.GameServerStatus{ 997 NodeName: "n1", 998 Counters: map[string]agonesv1.CounterStatus{ 999 "rooms": { 1000 Count: 7, 1001 Capacity: 7, 1002 }}}}, 1003 {ObjectMeta: metav1.ObjectMeta{ 1004 Name: "gs4", 1005 Namespace: "default", 1006 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1007 Status: agonesv1.GameServerStatus{ 1008 NodeName: "n1", 1009 Counters: map[string]agonesv1.CounterStatus{ 1010 "rooms": { 1011 Count: 11, 1012 Capacity: 14, 1013 }}}}, 1014 {ObjectMeta: metav1.ObjectMeta{ 1015 Name: "gs5", 1016 Namespace: "default", 1017 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1018 Status: agonesv1.GameServerStatus{ 1019 NodeName: "n1", 1020 Counters: map[string]agonesv1.CounterStatus{ 1021 "rooms": { 1022 Count: 0, 1023 Capacity: 13, 1024 }}}}, 1025 {ObjectMeta: metav1.ObjectMeta{ 1026 Name: "gs6", 1027 Namespace: "default", 1028 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1029 Status: agonesv1.GameServerStatus{ 1030 NodeName: "n1", 1031 Counters: map[string]agonesv1.CounterStatus{ 1032 "rooms": { 1033 Count: 0, 1034 Capacity: 7, 1035 }}}}, 1036 {ObjectMeta: metav1.ObjectMeta{ 1037 Name: "gs7", 1038 Namespace: "default", 1039 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1040 Status: agonesv1.GameServerStatus{ 1041 NodeName: "n1", 1042 Counters: map[string]agonesv1.CounterStatus{ 1043 "rooms": { 1044 Count: 0, 1045 Capacity: 7, 1046 }}}}, 1047 {ObjectMeta: metav1.ObjectMeta{ 1048 Name: "gs8", 1049 Namespace: "default", 1050 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1051 Status: agonesv1.GameServerStatus{ 1052 NodeName: "n1", 1053 Counters: map[string]agonesv1.CounterStatus{ 1054 "rooms": { 1055 Count: 0, 1056 Capacity: 7, 1057 }}}}, 1058 }, 1059 want: expected{ 1060 replicas: 1, 1061 limited: false, 1062 wantErr: false, 1063 }, 1064 }, 1065 "scale up": { 1066 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1067 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1068 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1069 Count: 0, 1070 Capacity: 7} 1071 f.Status.Replicas = 10 1072 f.Status.ReadyReplicas = 0 1073 f.Status.AllocatedReplicas = 10 1074 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1075 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1076 Count: 68, 1077 Capacity: 70} 1078 }), 1079 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1080 cp: &autoscalingv1.CounterPolicy{ 1081 Key: "rooms", 1082 MaxCapacity: 100, 1083 MinCapacity: 10, 1084 BufferSize: intstr.FromInt(10), 1085 }, 1086 want: expected{ 1087 replicas: 12, 1088 limited: false, 1089 wantErr: false, 1090 }, 1091 }, 1092 "scale up integer": { 1093 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1094 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1095 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1096 Count: 7, 1097 Capacity: 10} 1098 f.Status.Replicas = 3 1099 f.Status.ReadyReplicas = 3 1100 f.Status.AllocatedReplicas = 0 1101 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1102 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1103 Count: 21, 1104 Capacity: 30} 1105 }), 1106 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1107 cp: &autoscalingv1.CounterPolicy{ 1108 Key: "rooms", 1109 MaxCapacity: 100, 1110 MinCapacity: 10, 1111 BufferSize: intstr.FromInt(25), 1112 }, 1113 want: expected{ 1114 replicas: 9, 1115 limited: false, 1116 wantErr: false, 1117 }, 1118 }, 1119 "scale same": { 1120 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1121 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1122 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1123 Count: 0, 1124 Capacity: 7} 1125 f.Status.Replicas = 10 1126 f.Status.ReadyReplicas = 0 1127 f.Status.AllocatedReplicas = 10 1128 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1129 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1130 Count: 60, 1131 Capacity: 70} 1132 }), 1133 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1134 cp: &autoscalingv1.CounterPolicy{ 1135 Key: "rooms", 1136 MaxCapacity: 100, 1137 MinCapacity: 10, 1138 BufferSize: intstr.FromInt(10), 1139 }, 1140 want: expected{ 1141 replicas: 10, 1142 limited: false, 1143 wantErr: false, 1144 }, 1145 }, 1146 "scale down at MinCapacity": { 1147 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1148 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1149 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1150 Count: 0, 1151 Capacity: 7} 1152 f.Status.Replicas = 10 1153 f.Status.ReadyReplicas = 9 1154 f.Status.AllocatedReplicas = 1 1155 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1156 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1157 Count: 1, 1158 Capacity: 70} 1159 }), 1160 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1161 cp: &autoscalingv1.CounterPolicy{ 1162 Key: "rooms", 1163 MaxCapacity: 700, 1164 MinCapacity: 70, 1165 BufferSize: intstr.FromInt(10), 1166 }, 1167 want: expected{ 1168 replicas: 10, 1169 limited: true, 1170 wantErr: false, 1171 }, 1172 }, 1173 "scale down limited": { 1174 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1175 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1176 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1177 Count: 0, 1178 Capacity: 7} 1179 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Descending"}} 1180 f.Status.Replicas = 4 1181 f.Status.ReadyReplicas = 3 1182 f.Status.AllocatedReplicas = 1 1183 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1184 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1185 Count: 1, 1186 Capacity: 36} 1187 }), 1188 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1189 cp: &autoscalingv1.CounterPolicy{ 1190 Key: "rooms", 1191 MaxCapacity: 700, 1192 MinCapacity: 7, 1193 BufferSize: intstr.FromInt(1), 1194 }, 1195 gsList: []agonesv1.GameServer{ 1196 {ObjectMeta: metav1.ObjectMeta{ 1197 Name: "gs1", 1198 Namespace: "default", 1199 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1200 Status: agonesv1.GameServerStatus{ 1201 NodeName: "n1", 1202 Counters: map[string]agonesv1.CounterStatus{ 1203 "rooms": { 1204 Count: 0, 1205 Capacity: 10, 1206 }}}}, 1207 {ObjectMeta: metav1.ObjectMeta{ 1208 Name: "gs2", 1209 Namespace: "default", 1210 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1211 Status: agonesv1.GameServerStatus{ 1212 NodeName: "n1", 1213 Counters: map[string]agonesv1.CounterStatus{ 1214 "rooms": { 1215 Count: 1, 1216 Capacity: 5, 1217 }}}}, 1218 {ObjectMeta: metav1.ObjectMeta{ 1219 Name: "gs3", 1220 Namespace: "default", 1221 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1222 Status: agonesv1.GameServerStatus{ 1223 NodeName: "n1", 1224 Counters: map[string]agonesv1.CounterStatus{ 1225 "rooms": { 1226 Count: 0, 1227 Capacity: 7, 1228 }}}}, 1229 {ObjectMeta: metav1.ObjectMeta{ 1230 Name: "gs4", 1231 Namespace: "default", 1232 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1233 Status: agonesv1.GameServerStatus{ 1234 NodeName: "n1", 1235 Counters: map[string]agonesv1.CounterStatus{ 1236 "rooms": { 1237 Count: 0, 1238 Capacity: 14, 1239 }}}}}, 1240 want: expected{ 1241 replicas: 2, 1242 limited: true, 1243 wantErr: false, 1244 }, 1245 }, 1246 "scale down limited must scale up": { 1247 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1248 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1249 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1250 Count: 0, 1251 Capacity: 7} 1252 f.Status.Replicas = 7 1253 f.Status.ReadyReplicas = 6 1254 f.Status.AllocatedReplicas = 1 1255 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1256 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1257 Count: 1, 1258 Capacity: 49} 1259 }), 1260 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1261 cp: &autoscalingv1.CounterPolicy{ 1262 Key: "rooms", 1263 MaxCapacity: 700, 1264 MinCapacity: 70, 1265 BufferSize: intstr.FromInt(10), 1266 }, 1267 want: expected{ 1268 replicas: 10, 1269 limited: true, 1270 wantErr: false, 1271 }, 1272 }, 1273 "scale up limited": { 1274 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1275 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1276 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1277 Count: 0, 1278 Capacity: 7} 1279 f.Status.Replicas = 14 1280 f.Status.ReadyReplicas = 0 1281 f.Status.AllocatedReplicas = 14 1282 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1283 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1284 Count: 98, 1285 Capacity: 98} 1286 }), 1287 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1288 cp: &autoscalingv1.CounterPolicy{ 1289 Key: "rooms", 1290 MaxCapacity: 100, 1291 MinCapacity: 10, 1292 BufferSize: intstr.FromInt(10), 1293 }, 1294 want: expected{ 1295 replicas: 14, 1296 limited: true, 1297 wantErr: false, 1298 }, 1299 }, 1300 "scale up limited must scale down": { 1301 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1302 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1303 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1304 Count: 0, 1305 Capacity: 7} 1306 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Descending"}} 1307 f.Status.Replicas = 1 1308 f.Status.ReadyReplicas = 0 1309 f.Status.AllocatedReplicas = 1 1310 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1311 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1312 Count: 7, 1313 Capacity: 7} 1314 }), 1315 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1316 cp: &autoscalingv1.CounterPolicy{ 1317 Key: "rooms", 1318 MaxCapacity: 2, 1319 MinCapacity: 0, 1320 BufferSize: intstr.FromInt(1), 1321 }, 1322 gsList: []agonesv1.GameServer{ 1323 {ObjectMeta: metav1.ObjectMeta{ 1324 Name: "gs1", 1325 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1326 Status: agonesv1.GameServerStatus{ 1327 NodeName: "n1", 1328 Counters: map[string]agonesv1.CounterStatus{ 1329 "rooms": { 1330 Count: 7, 1331 Capacity: 7, 1332 }}}}}, 1333 want: expected{ 1334 replicas: 1, 1335 limited: true, 1336 wantErr: false, 1337 }, 1338 }, 1339 "scale down to max capacity": { 1340 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1341 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1342 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1343 Count: 0, 1344 Capacity: 5} 1345 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Descending"}} 1346 f.Status.Replicas = 3 1347 f.Status.ReadyReplicas = 3 1348 f.Status.AllocatedReplicas = 0 1349 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1350 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1351 Count: 0, 1352 Capacity: 15} 1353 }), 1354 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1355 cp: &autoscalingv1.CounterPolicy{ 1356 Key: "rooms", 1357 MaxCapacity: 5, 1358 MinCapacity: 1, 1359 BufferSize: intstr.FromInt(5), 1360 }, 1361 gsList: []agonesv1.GameServer{ 1362 {ObjectMeta: metav1.ObjectMeta{ 1363 Name: "gs1", 1364 Namespace: "default", 1365 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1366 Status: agonesv1.GameServerStatus{ 1367 NodeName: "n1", 1368 Counters: map[string]agonesv1.CounterStatus{ 1369 "rooms": { 1370 Count: 0, 1371 Capacity: 5, 1372 }}}}, 1373 {ObjectMeta: metav1.ObjectMeta{ 1374 Name: "gs2", 1375 Namespace: "default", 1376 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1377 Status: agonesv1.GameServerStatus{ 1378 NodeName: "n1", 1379 Counters: map[string]agonesv1.CounterStatus{ 1380 "rooms": { 1381 Count: 0, 1382 Capacity: 5, 1383 }}}}, 1384 {ObjectMeta: metav1.ObjectMeta{ 1385 Name: "gs3", 1386 Namespace: "default", 1387 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1388 Status: agonesv1.GameServerStatus{ 1389 NodeName: "n1", 1390 Counters: map[string]agonesv1.CounterStatus{ 1391 "rooms": { 1392 Count: 0, 1393 Capacity: 5, 1394 }}}}, 1395 }, 1396 want: expected{ 1397 replicas: 1, 1398 limited: false, 1399 wantErr: false, 1400 }, 1401 }, 1402 "scale up to MinCapacity": { 1403 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1404 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1405 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1406 Count: 0, 1407 Capacity: 10} 1408 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Descending"}} 1409 f.Status.Replicas = 3 1410 f.Status.ReadyReplicas = 0 1411 f.Status.AllocatedReplicas = 3 1412 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1413 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1414 Count: 20, 1415 Capacity: 30, 1416 } 1417 }), 1418 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1419 cp: &autoscalingv1.CounterPolicy{ 1420 Key: "rooms", 1421 MaxCapacity: 100, 1422 MinCapacity: 50, 1423 BufferSize: intstr.FromString("10%"), 1424 }, 1425 want: expected{ 1426 replicas: 5, 1427 limited: true, 1428 wantErr: false, 1429 }, 1430 }, 1431 "scale up by percent": { 1432 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1433 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1434 f.Spec.Template.Spec.Counters["players"] = agonesv1.CounterStatus{ 1435 Count: 0, 1436 Capacity: 1} 1437 f.Status.Replicas = 10 1438 f.Status.ReadyReplicas = 2 1439 f.Status.AllocatedReplicas = 8 1440 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1441 f.Status.Counters["players"] = agonesv1.AggregatedCounterStatus{ 1442 AllocatedCount: 8, 1443 AllocatedCapacity: 10, 1444 Count: 8, 1445 Capacity: 10, 1446 } 1447 }), 1448 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1449 cp: &autoscalingv1.CounterPolicy{ 1450 Key: "players", 1451 MaxCapacity: 100, 1452 MinCapacity: 10, 1453 BufferSize: intstr.FromString("30%"), 1454 }, 1455 want: expected{ 1456 replicas: 12, 1457 limited: false, 1458 wantErr: false, 1459 }, 1460 }, 1461 "scale up by percent with Count": { 1462 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1463 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1464 f.Spec.Template.Spec.Counters["players"] = agonesv1.CounterStatus{ 1465 Count: 3, 1466 Capacity: 10} 1467 f.Status.Replicas = 3 1468 f.Status.ReadyReplicas = 0 1469 f.Status.AllocatedReplicas = 3 1470 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1471 f.Status.Counters["players"] = agonesv1.AggregatedCounterStatus{ 1472 AllocatedCount: 20, 1473 AllocatedCapacity: 30, 1474 Count: 20, 1475 Capacity: 30, 1476 } 1477 }), 1478 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1479 cp: &autoscalingv1.CounterPolicy{ 1480 Key: "players", 1481 MaxCapacity: 100, 1482 MinCapacity: 10, 1483 BufferSize: intstr.FromString("50%"), 1484 }, 1485 want: expected{ 1486 replicas: 5, 1487 limited: false, 1488 wantErr: false, 1489 }, 1490 }, 1491 "scale down by integer buffer": { 1492 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1493 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1494 f.Spec.Template.Spec.Counters["players"] = agonesv1.CounterStatus{ 1495 Count: 7, 1496 Capacity: 10} 1497 f.Status.Replicas = 3 1498 f.Status.ReadyReplicas = 0 1499 f.Status.AllocatedReplicas = 3 1500 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1501 f.Status.Counters["players"] = agonesv1.AggregatedCounterStatus{ 1502 Count: 21, 1503 Capacity: 30, 1504 } 1505 }), 1506 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1507 cp: &autoscalingv1.CounterPolicy{ 1508 Key: "players", 1509 MaxCapacity: 100, 1510 MinCapacity: 10, 1511 BufferSize: intstr.FromInt(5), 1512 }, 1513 gsList: []agonesv1.GameServer{ 1514 {ObjectMeta: metav1.ObjectMeta{ 1515 Name: "gs1", 1516 Namespace: "default", 1517 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1518 Status: agonesv1.GameServerStatus{ 1519 NodeName: "n1", 1520 Counters: map[string]agonesv1.CounterStatus{ 1521 "players": { 1522 Count: 7, 1523 Capacity: 10, 1524 }}}}, 1525 {ObjectMeta: metav1.ObjectMeta{ 1526 Name: "gs2", 1527 Namespace: "default", 1528 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1529 Status: agonesv1.GameServerStatus{ 1530 NodeName: "n1", 1531 Counters: map[string]agonesv1.CounterStatus{ 1532 "players": { 1533 Count: 7, 1534 Capacity: 10, 1535 }}}}, 1536 {ObjectMeta: metav1.ObjectMeta{ 1537 Name: "gs3", 1538 Namespace: "default", 1539 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1540 Status: agonesv1.GameServerStatus{ 1541 NodeName: "n1", 1542 Counters: map[string]agonesv1.CounterStatus{ 1543 "players": { 1544 Count: 7, 1545 Capacity: 10, 1546 }}}}, 1547 }, 1548 want: expected{ 1549 replicas: 2, 1550 limited: false, 1551 wantErr: false, 1552 }, 1553 }, 1554 } 1555 1556 utilruntime.FeatureTestMutex.Lock() 1557 defer utilruntime.FeatureTestMutex.Unlock() 1558 1559 for name, tc := range testCases { 1560 t.Run(name, func(t *testing.T) { 1561 err := utilruntime.ParseFeatures(tc.featureFlags) 1562 assert.NoError(t, err) 1563 1564 m := agtesting.NewMocks() 1565 m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1566 return true, &agonesv1.GameServerList{Items: tc.gsList}, nil 1567 }) 1568 1569 informer := m.AgonesInformerFactory.Agones().V1() 1570 _, cancel := agtesting.StartInformers(m, 1571 informer.GameServers().Informer().HasSynced) 1572 defer cancel() 1573 1574 replicas, limited, err := applyCounterOrListPolicy(tc.cp, nil, tc.fleet, informer.GameServers().Lister().GameServers(tc.fleet.ObjectMeta.Namespace), nc) 1575 1576 if tc.want.wantErr { 1577 assert.NotNil(t, err) 1578 } else { 1579 assert.Nil(t, err) 1580 assert.Equal(t, tc.want.replicas, replicas) 1581 assert.Equal(t, tc.want.limited, limited) 1582 } 1583 }) 1584 } 1585 } 1586 1587 // nolint:dupl // Linter errors on lines are duplicate of TestApplyCounterPolicy 1588 // NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateListPolicy) 1589 func TestApplyListPolicy(t *testing.T) { 1590 t.Parallel() 1591 1592 nc := map[string]gameservers.NodeCount{ 1593 "n1": {Ready: 0, Allocated: 2}, 1594 "n2": {Ready: 1}, 1595 } 1596 1597 modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { 1598 _, fleet := defaultFixtures() 1599 f(fleet) 1600 return fleet 1601 } 1602 1603 type expected struct { 1604 replicas int32 1605 limited bool 1606 wantErr bool 1607 } 1608 1609 testCases := map[string]struct { 1610 fleet *agonesv1.Fleet 1611 featureFlags string 1612 lp *autoscalingv1.ListPolicy 1613 gsList []agonesv1.GameServer 1614 want expected 1615 }{ 1616 "counts and lists not enabled": { 1617 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1618 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1619 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1620 Values: []string{}, 1621 Capacity: 7} 1622 f.Status.Replicas = 10 1623 f.Status.ReadyReplicas = 5 1624 f.Status.AllocatedReplicas = 5 1625 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1626 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1627 Count: 31, 1628 Capacity: 70, 1629 } 1630 }), 1631 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=false", 1632 lp: &autoscalingv1.ListPolicy{ 1633 Key: "gamers", 1634 MaxCapacity: 100, 1635 MinCapacity: 10, 1636 BufferSize: intstr.FromInt(10), 1637 }, 1638 want: expected{ 1639 replicas: 0, 1640 limited: false, 1641 wantErr: true, 1642 }, 1643 }, 1644 "fleet spec does not have list": { 1645 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1646 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1647 f.Spec.Template.Spec.Lists["tamers"] = agonesv1.ListStatus{ 1648 Values: []string{}, 1649 Capacity: 7} 1650 f.Status.Replicas = 10 1651 f.Status.ReadyReplicas = 5 1652 f.Status.AllocatedReplicas = 5 1653 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1654 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1655 Count: 31, 1656 Capacity: 70, 1657 } 1658 }), 1659 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1660 lp: &autoscalingv1.ListPolicy{ 1661 Key: "gamers", 1662 MaxCapacity: 100, 1663 MinCapacity: 10, 1664 BufferSize: intstr.FromInt(10), 1665 }, 1666 want: expected{ 1667 replicas: 0, 1668 limited: false, 1669 wantErr: true, 1670 }, 1671 }, 1672 "fleet status does not have list": { 1673 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1674 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1675 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1676 Values: []string{}, 1677 Capacity: 7} 1678 f.Status.Replicas = 10 1679 f.Status.ReadyReplicas = 5 1680 f.Status.AllocatedReplicas = 5 1681 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1682 f.Status.Lists["tamers"] = agonesv1.AggregatedListStatus{ 1683 Count: 31, 1684 Capacity: 70, 1685 } 1686 }), 1687 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1688 lp: &autoscalingv1.ListPolicy{ 1689 Key: "gamers", 1690 MaxCapacity: 100, 1691 MinCapacity: 10, 1692 BufferSize: intstr.FromInt(10), 1693 }, 1694 want: expected{ 1695 replicas: 0, 1696 limited: false, 1697 wantErr: true, 1698 }, 1699 }, 1700 "List based fleet does not have any replicas": { 1701 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1702 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1703 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1704 Values: []string{}, 1705 Capacity: 7} 1706 f.Status.Replicas = 0 1707 f.Status.ReadyReplicas = 0 1708 f.Status.AllocatedReplicas = 0 1709 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1710 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{} 1711 }), 1712 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1713 lp: &autoscalingv1.ListPolicy{ 1714 Key: "gamers", 1715 MaxCapacity: 100, 1716 MinCapacity: 10, 1717 BufferSize: intstr.FromInt(10), 1718 }, 1719 want: expected{ 1720 replicas: 2, 1721 limited: true, 1722 wantErr: false, 1723 }, 1724 }, 1725 "scale up": { 1726 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1727 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1728 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1729 Values: []string{"default", "default2"}, 1730 Capacity: 3} 1731 f.Status.Replicas = 10 1732 f.Status.ReadyReplicas = 0 1733 f.Status.AllocatedReplicas = 10 1734 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1735 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1736 Count: 29, 1737 Capacity: 30, 1738 } 1739 }), 1740 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1741 lp: &autoscalingv1.ListPolicy{ 1742 Key: "gamers", 1743 MaxCapacity: 100, 1744 MinCapacity: 10, 1745 BufferSize: intstr.FromInt(5), 1746 }, 1747 want: expected{ 1748 replicas: 14, 1749 limited: false, 1750 wantErr: false, 1751 }, 1752 }, 1753 "scale up to maxcapacity": { 1754 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1755 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1756 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1757 Values: []string{"default", "default2", "default3"}, 1758 Capacity: 5} 1759 f.Status.Replicas = 3 1760 f.Status.ReadyReplicas = 3 1761 f.Status.AllocatedReplicas = 0 1762 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1763 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1764 Count: 9, 1765 Capacity: 15, 1766 } 1767 }), 1768 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1769 lp: &autoscalingv1.ListPolicy{ 1770 Key: "gamers", 1771 MaxCapacity: 25, 1772 MinCapacity: 15, 1773 BufferSize: intstr.FromInt(15), 1774 }, 1775 want: expected{ 1776 replicas: 5, 1777 limited: true, 1778 wantErr: false, 1779 }, 1780 }, 1781 "scale down": { 1782 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1783 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1784 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1785 Values: []string{"default"}, 1786 Capacity: 10} 1787 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Descending"}} 1788 f.Status.Replicas = 8 1789 f.Status.ReadyReplicas = 6 1790 f.Status.AllocatedReplicas = 4 1791 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1792 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1793 Count: 15, 1794 Capacity: 70, 1795 } 1796 }), 1797 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1798 lp: &autoscalingv1.ListPolicy{ 1799 Key: "gamers", 1800 MaxCapacity: 70, 1801 MinCapacity: 10, 1802 BufferSize: intstr.FromInt(10), 1803 }, 1804 gsList: []agonesv1.GameServer{ 1805 {ObjectMeta: metav1.ObjectMeta{ 1806 Name: "gs1", 1807 Namespace: "default", 1808 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1809 Status: agonesv1.GameServerStatus{ 1810 NodeName: "n1", 1811 Lists: map[string]agonesv1.ListStatus{ 1812 "gamers": { 1813 Values: []string{}, 1814 Capacity: 10, 1815 }}}}, 1816 {ObjectMeta: metav1.ObjectMeta{ 1817 Name: "gs2", 1818 Namespace: "default", 1819 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1820 Status: agonesv1.GameServerStatus{ 1821 NodeName: "n1", 1822 Lists: map[string]agonesv1.ListStatus{ 1823 "gamers": { 1824 Values: []string{}, 1825 Capacity: 10, 1826 }}}}, 1827 {ObjectMeta: metav1.ObjectMeta{ 1828 Name: "gs3", 1829 Namespace: "default", 1830 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1831 Status: agonesv1.GameServerStatus{ 1832 NodeName: "n1", 1833 Lists: map[string]agonesv1.ListStatus{ 1834 "gamers": { 1835 Values: []string{}, 1836 Capacity: 10, 1837 }}}}, 1838 {ObjectMeta: metav1.ObjectMeta{ 1839 Name: "gs4", 1840 Namespace: "default", 1841 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1842 Status: agonesv1.GameServerStatus{ 1843 NodeName: "n1", 1844 Lists: map[string]agonesv1.ListStatus{ 1845 "gamers": { 1846 Values: []string{"default1", "default2", "default3", "default4", "default5", "default6", "default7", "default8"}, 1847 Capacity: 8, 1848 }}}}, 1849 {ObjectMeta: metav1.ObjectMeta{ 1850 Name: "gs5", 1851 Namespace: "default", 1852 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1853 Status: agonesv1.GameServerStatus{ 1854 NodeName: "n1", 1855 Lists: map[string]agonesv1.ListStatus{ 1856 "gamers": { 1857 Values: []string{"default9", "default10", "default11", "default12"}, 1858 Capacity: 10, 1859 }}}}, 1860 {ObjectMeta: metav1.ObjectMeta{ 1861 Name: "gs6", 1862 Namespace: "default", 1863 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1864 Status: agonesv1.GameServerStatus{ 1865 NodeName: "n1", 1866 Lists: map[string]agonesv1.ListStatus{ 1867 "gamers": { 1868 Values: []string{"default"}, 1869 Capacity: 4, 1870 }}}}, 1871 {ObjectMeta: metav1.ObjectMeta{ 1872 Name: "gs7", 1873 Namespace: "default", 1874 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1875 Status: agonesv1.GameServerStatus{ 1876 NodeName: "n1", 1877 Lists: map[string]agonesv1.ListStatus{ 1878 "gamers": { 1879 Values: []string{"default"}, 1880 Capacity: 8, 1881 }}}}, 1882 {ObjectMeta: metav1.ObjectMeta{ 1883 Name: "gs8", 1884 Namespace: "default", 1885 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1886 Status: agonesv1.GameServerStatus{ 1887 NodeName: "n1", 1888 Lists: map[string]agonesv1.ListStatus{ 1889 "gamers": { 1890 Values: []string{"default"}, 1891 Capacity: 10, 1892 }}}}, 1893 }, 1894 want: expected{ 1895 replicas: 4, 1896 limited: false, 1897 wantErr: false, 1898 }, 1899 }, 1900 "scale up limited": { 1901 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1902 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1903 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1904 Values: []string{"default", "default2"}, 1905 Capacity: 3} 1906 f.Status.Replicas = 10 1907 f.Status.ReadyReplicas = 0 1908 f.Status.AllocatedReplicas = 10 1909 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1910 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1911 Count: 29, 1912 Capacity: 30, 1913 } 1914 }), 1915 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1916 lp: &autoscalingv1.ListPolicy{ 1917 Key: "gamers", 1918 MaxCapacity: 30, 1919 MinCapacity: 10, 1920 BufferSize: intstr.FromInt(5), 1921 }, 1922 want: expected{ 1923 replicas: 10, 1924 limited: true, 1925 wantErr: false, 1926 }, 1927 }, 1928 "scale down limited": { 1929 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1930 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1931 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1932 Values: []string{}, 1933 Capacity: 5} 1934 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Ascending"}} 1935 f.Status.Replicas = 4 1936 f.Status.ReadyReplicas = 3 1937 f.Status.AllocatedReplicas = 1 1938 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1939 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1940 Count: 3, 1941 Capacity: 20, 1942 } 1943 }), 1944 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1945 lp: &autoscalingv1.ListPolicy{ 1946 Key: "gamers", 1947 MaxCapacity: 100, 1948 MinCapacity: 10, 1949 BufferSize: intstr.FromInt(1), 1950 }, 1951 gsList: []agonesv1.GameServer{ 1952 {ObjectMeta: metav1.ObjectMeta{ 1953 Name: "gs1", 1954 Namespace: "default", 1955 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1956 Status: agonesv1.GameServerStatus{ 1957 NodeName: "n1", 1958 Lists: map[string]agonesv1.ListStatus{ 1959 "gamers": { 1960 Values: []string{}, 1961 Capacity: 5, 1962 }}}}, 1963 {ObjectMeta: metav1.ObjectMeta{ 1964 Name: "gs2", 1965 Namespace: "default", 1966 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1967 Status: agonesv1.GameServerStatus{ 1968 NodeName: "n1", 1969 Lists: map[string]agonesv1.ListStatus{ 1970 "gamers": { 1971 Values: []string{}, 1972 Capacity: 5, 1973 }}}}, 1974 {ObjectMeta: metav1.ObjectMeta{ 1975 Name: "gs3", 1976 Namespace: "default", 1977 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1978 Status: agonesv1.GameServerStatus{ 1979 NodeName: "n1", 1980 Lists: map[string]agonesv1.ListStatus{ 1981 "gamers": { 1982 Values: []string{}, 1983 Capacity: 5, 1984 }}}}, 1985 {ObjectMeta: metav1.ObjectMeta{ 1986 Name: "gs4", 1987 Namespace: "default", 1988 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1989 Status: agonesv1.GameServerStatus{ 1990 NodeName: "n1", 1991 Lists: map[string]agonesv1.ListStatus{ 1992 "gamers": { 1993 Values: []string{"default1", "default2", "default3"}, 1994 Capacity: 5, 1995 }}}}}, 1996 want: expected{ 1997 replicas: 2, 1998 limited: true, 1999 wantErr: false, 2000 }, 2001 }, 2002 "scale up by percent limited": { 2003 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2004 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2005 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2006 Values: []string{"default", "default2", "default3"}, 2007 Capacity: 10} 2008 f.Status.Replicas = 3 2009 f.Status.ReadyReplicas = 0 2010 f.Status.AllocatedReplicas = 3 2011 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2012 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2013 AllocatedCount: 20, 2014 AllocatedCapacity: 30, 2015 Count: 20, 2016 Capacity: 30, 2017 } 2018 }), 2019 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2020 lp: &autoscalingv1.ListPolicy{ 2021 Key: "gamers", 2022 MaxCapacity: 45, 2023 MinCapacity: 10, 2024 BufferSize: intstr.FromString("50%"), 2025 }, 2026 want: expected{ 2027 replicas: 4, 2028 limited: true, 2029 wantErr: false, 2030 }, 2031 }, 2032 "scale up by percent": { 2033 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2034 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2035 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2036 Values: []string{"default"}, 2037 Capacity: 3} 2038 f.Status.Replicas = 11 2039 f.Status.ReadyReplicas = 1 2040 f.Status.AllocatedReplicas = 10 2041 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2042 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2043 AllocatedCount: 29, 2044 AllocatedCapacity: 30, 2045 Count: 30, 2046 Capacity: 30, 2047 } 2048 }), 2049 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2050 lp: &autoscalingv1.ListPolicy{ 2051 Key: "gamers", 2052 MaxCapacity: 50, 2053 MinCapacity: 10, 2054 BufferSize: intstr.FromString("10%"), 2055 }, 2056 want: expected{ 2057 replicas: 13, 2058 limited: false, 2059 wantErr: false, 2060 }, 2061 }, 2062 "scale down by percent to Zero": { 2063 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2064 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2065 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2066 Values: []string{"default", "default2"}, 2067 Capacity: 10} 2068 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Descending"}} 2069 f.Status.Replicas = 3 2070 f.Status.ReadyReplicas = 3 2071 f.Status.AllocatedReplicas = 0 2072 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2073 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2074 AllocatedCount: 0, 2075 AllocatedCapacity: 0, 2076 Count: 15, 2077 Capacity: 30, 2078 } 2079 }), 2080 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2081 lp: &autoscalingv1.ListPolicy{ 2082 Key: "gamers", 2083 MaxCapacity: 50, 2084 MinCapacity: 0, 2085 BufferSize: intstr.FromString("20%"), 2086 }, 2087 gsList: []agonesv1.GameServer{ 2088 {ObjectMeta: metav1.ObjectMeta{ 2089 Name: "gs1", 2090 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2091 Status: agonesv1.GameServerStatus{ 2092 NodeName: "n1", 2093 Lists: map[string]agonesv1.ListStatus{ 2094 "gamers": { 2095 Values: []string{"1", "2", "3", "4", "5"}, 2096 Capacity: 15, 2097 }}}}, 2098 {ObjectMeta: metav1.ObjectMeta{ 2099 Name: "gs2", 2100 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2101 Status: agonesv1.GameServerStatus{ 2102 NodeName: "n1", 2103 Lists: map[string]agonesv1.ListStatus{ 2104 "gamers": { 2105 Values: []string{"1", "2", "3", "4", "5", "6", "7"}, 2106 Capacity: 10, 2107 }}}}, 2108 {ObjectMeta: metav1.ObjectMeta{ 2109 Name: "gs3", 2110 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2111 Status: agonesv1.GameServerStatus{ 2112 NodeName: "n1", 2113 Lists: map[string]agonesv1.ListStatus{ 2114 "gamers": { 2115 Values: []string{"1", "2", "3"}, 2116 Capacity: 5, 2117 }}}}, 2118 }, 2119 want: expected{ 2120 replicas: 1, 2121 limited: true, 2122 wantErr: false, 2123 }, 2124 }, 2125 "scale down by percent": { 2126 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2127 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2128 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2129 Values: []string{"default", "default2"}, 2130 Capacity: 10} 2131 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Descending"}} 2132 f.Status.Replicas = 5 2133 f.Status.ReadyReplicas = 2 2134 f.Status.AllocatedReplicas = 3 2135 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2136 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2137 AllocatedCount: 15, 2138 AllocatedCapacity: 30, 2139 Count: 18, 2140 Capacity: 50, 2141 } 2142 }), 2143 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2144 lp: &autoscalingv1.ListPolicy{ 2145 Key: "gamers", 2146 MaxCapacity: 50, 2147 MinCapacity: 0, 2148 BufferSize: intstr.FromString("50%"), 2149 }, 2150 gsList: []agonesv1.GameServer{ 2151 {ObjectMeta: metav1.ObjectMeta{ 2152 Name: "gs1", 2153 Namespace: "default", 2154 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2155 Status: agonesv1.GameServerStatus{ 2156 NodeName: "n1", 2157 Lists: map[string]agonesv1.ListStatus{ 2158 "gamers": { 2159 Values: []string{"1", "2", "3", "4", "5"}, 2160 Capacity: 15, 2161 }}}}, 2162 {ObjectMeta: metav1.ObjectMeta{ 2163 Name: "gs2", 2164 Namespace: "default", 2165 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2166 Status: agonesv1.GameServerStatus{ 2167 NodeName: "n1", 2168 Lists: map[string]agonesv1.ListStatus{ 2169 "gamers": { 2170 Values: []string{"1", "2", "3", "4", "5", "6", "7"}, 2171 Capacity: 10, 2172 }}}}, 2173 {ObjectMeta: metav1.ObjectMeta{ 2174 Name: "gs3", 2175 Namespace: "default", 2176 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2177 Status: agonesv1.GameServerStatus{ 2178 NodeName: "n1", 2179 Lists: map[string]agonesv1.ListStatus{ 2180 "gamers": { 2181 Values: []string{"1", "2", "3"}, 2182 Capacity: 5, 2183 }}}}, 2184 {ObjectMeta: metav1.ObjectMeta{ 2185 Name: "gs4", 2186 Namespace: "default", 2187 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2188 Status: agonesv1.GameServerStatus{ 2189 NodeName: "n2", 2190 Lists: map[string]agonesv1.ListStatus{ 2191 "gamers": { 2192 Values: []string{"1", "2", "3"}, 2193 Capacity: 5, 2194 }}}}, 2195 {ObjectMeta: metav1.ObjectMeta{ 2196 Name: "gs5", 2197 Namespace: "default", 2198 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2199 Status: agonesv1.GameServerStatus{ 2200 NodeName: "n2", 2201 Lists: map[string]agonesv1.ListStatus{ 2202 "gamers": { 2203 Values: []string{}, 2204 Capacity: 15, 2205 }}}}, 2206 }, 2207 want: expected{ 2208 replicas: 3, 2209 limited: false, 2210 wantErr: false, 2211 }, 2212 }, 2213 "scale down by percent limited": { 2214 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2215 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2216 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2217 Values: []string{"default", "default2"}, 2218 Capacity: 10} 2219 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Descending"}} 2220 f.Status.Replicas = 3 2221 f.Status.ReadyReplicas = 3 2222 f.Status.AllocatedReplicas = 0 2223 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2224 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2225 AllocatedCount: 0, 2226 AllocatedCapacity: 0, 2227 Count: 15, 2228 Capacity: 30, 2229 } 2230 }), 2231 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2232 lp: &autoscalingv1.ListPolicy{ 2233 Key: "gamers", 2234 MaxCapacity: 50, 2235 MinCapacity: 1, 2236 BufferSize: intstr.FromString("20%"), 2237 }, 2238 gsList: []agonesv1.GameServer{ 2239 {ObjectMeta: metav1.ObjectMeta{ 2240 Name: "gs1", 2241 Namespace: "default", 2242 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2243 Status: agonesv1.GameServerStatus{ 2244 NodeName: "n1", 2245 Lists: map[string]agonesv1.ListStatus{ 2246 "gamers": { 2247 Values: []string{"1", "2", "3", "4", "5"}, 2248 Capacity: 15, 2249 }}}}, 2250 {ObjectMeta: metav1.ObjectMeta{ 2251 Name: "gs2", 2252 Namespace: "default", 2253 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2254 Status: agonesv1.GameServerStatus{ 2255 NodeName: "n1", 2256 Lists: map[string]agonesv1.ListStatus{ 2257 "gamers": { 2258 Values: []string{"1", "2", "3", "4", "5", "6", "7"}, 2259 Capacity: 10, 2260 }}}}, 2261 {ObjectMeta: metav1.ObjectMeta{ 2262 Name: "gs3", 2263 Namespace: "default", 2264 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2265 Status: agonesv1.GameServerStatus{ 2266 NodeName: "n1", 2267 Lists: map[string]agonesv1.ListStatus{ 2268 "gamers": { 2269 Values: []string{"1", "2", "3"}, 2270 Capacity: 5, 2271 }}}}, 2272 }, 2273 want: expected{ 2274 replicas: 1, 2275 limited: true, 2276 wantErr: false, 2277 }, 2278 }, 2279 } 2280 2281 utilruntime.FeatureTestMutex.Lock() 2282 defer utilruntime.FeatureTestMutex.Unlock() 2283 2284 for name, tc := range testCases { 2285 t.Run(name, func(t *testing.T) { 2286 err := utilruntime.ParseFeatures(tc.featureFlags) 2287 assert.NoError(t, err) 2288 2289 m := agtesting.NewMocks() 2290 m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 2291 return true, &agonesv1.GameServerList{Items: tc.gsList}, nil 2292 }) 2293 2294 informer := m.AgonesInformerFactory.Agones().V1() 2295 _, cancel := agtesting.StartInformers(m, 2296 informer.GameServers().Informer().HasSynced) 2297 defer cancel() 2298 2299 replicas, limited, err := applyCounterOrListPolicy(nil, tc.lp, tc.fleet, informer.GameServers().Lister().GameServers(tc.fleet.ObjectMeta.Namespace), nc) 2300 2301 if tc.want.wantErr { 2302 assert.NotNil(t, err) 2303 } else { 2304 assert.Nil(t, err) 2305 assert.Equal(t, tc.want.replicas, replicas) 2306 assert.Equal(t, tc.want.limited, limited) 2307 } 2308 }) 2309 } 2310 } 2311 2312 // nolint:dupl // Linter errors on lines are duplicate of TestApplySchedulePolicy 2313 // NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateSchedulePolicy) 2314 func TestApplySchedulePolicy(t *testing.T) { 2315 t.Parallel() 2316 2317 type expected struct { 2318 replicas int32 2319 limited bool 2320 wantErr bool 2321 } 2322 2323 bufferPolicy := autoscalingv1.FleetAutoscalerPolicy{ 2324 Type: autoscalingv1.BufferPolicyType, 2325 Buffer: &autoscalingv1.BufferPolicy{ 2326 BufferSize: intstr.FromInt(1), 2327 MinReplicas: 3, 2328 MaxReplicas: 10, 2329 }, 2330 } 2331 expectedWhenActive := expected{ 2332 replicas: 3, 2333 limited: false, 2334 wantErr: false, 2335 } 2336 expectedWhenInactive := expected{ 2337 replicas: 0, 2338 limited: false, 2339 wantErr: true, 2340 } 2341 2342 testCases := map[string]struct { 2343 featureFlags string 2344 specReplicas int32 2345 statusReplicas int32 2346 statusAllocatedReplicas int32 2347 statusReadyReplicas int32 2348 now time.Time 2349 sp *autoscalingv1.SchedulePolicy 2350 gsList []agonesv1.GameServer 2351 want expected 2352 }{ 2353 "scheduled autoscaler feature flag not enabled": { 2354 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", 2355 sp: &autoscalingv1.SchedulePolicy{}, 2356 want: expected{ 2357 replicas: 0, 2358 limited: false, 2359 wantErr: true, 2360 }, 2361 }, 2362 "no start time": { 2363 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2364 now: mustParseTime("2020-12-26T08:30:00Z"), 2365 sp: &autoscalingv1.SchedulePolicy{ 2366 Between: autoscalingv1.Between{ 2367 End: mustParseMetav1Time("2021-01-01T00:00:00Z"), 2368 }, 2369 ActivePeriod: autoscalingv1.ActivePeriod{ 2370 Timezone: "UTC", 2371 StartCron: "* * * * *", 2372 Duration: "48h", 2373 }, 2374 Policy: bufferPolicy, 2375 }, 2376 want: expectedWhenActive, 2377 }, 2378 "no end time": { 2379 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2380 now: mustParseTime("2021-01-02T00:00:00Z"), 2381 sp: &autoscalingv1.SchedulePolicy{ 2382 Between: autoscalingv1.Between{ 2383 Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), 2384 }, 2385 ActivePeriod: autoscalingv1.ActivePeriod{ 2386 Timezone: "UTC", 2387 StartCron: "* * * * *", 2388 Duration: "1h", 2389 }, 2390 Policy: bufferPolicy, 2391 }, 2392 want: expectedWhenActive, 2393 }, 2394 "no cron time": { 2395 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2396 now: mustParseTime("2021-01-01T0:30:00Z"), 2397 sp: &autoscalingv1.SchedulePolicy{ 2398 Between: autoscalingv1.Between{ 2399 Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), 2400 End: mustParseMetav1Time("2021-01-01T01:00:00Z"), 2401 }, 2402 ActivePeriod: autoscalingv1.ActivePeriod{ 2403 Timezone: "UTC", 2404 Duration: "1h", 2405 }, 2406 Policy: bufferPolicy, 2407 }, 2408 want: expectedWhenActive, 2409 }, 2410 "no duration": { 2411 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2412 now: mustParseTime("2021-01-01T0:30:00Z"), 2413 sp: &autoscalingv1.SchedulePolicy{ 2414 Between: autoscalingv1.Between{ 2415 Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), 2416 End: mustParseMetav1Time("2021-01-01T01:00:00Z"), 2417 }, 2418 ActivePeriod: autoscalingv1.ActivePeriod{ 2419 Timezone: "UTC", 2420 StartCron: "* * * * *", 2421 }, 2422 Policy: bufferPolicy, 2423 }, 2424 want: expectedWhenActive, 2425 }, 2426 "no start time, end time, cron time, duration": { 2427 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2428 now: mustParseTime("2021-01-01T00:00:00Z"), 2429 sp: &autoscalingv1.SchedulePolicy{ 2430 Policy: bufferPolicy, 2431 }, 2432 want: expectedWhenActive, 2433 }, 2434 "daylight saving time start": { 2435 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2436 now: mustParseTime("2021-03-14T02:00:00Z"), 2437 sp: &autoscalingv1.SchedulePolicy{ 2438 Between: autoscalingv1.Between{ 2439 Start: mustParseMetav1Time("2021-03-13T00:00:00Z"), 2440 End: mustParseMetav1Time("2021-03-15T00:00:00Z"), 2441 }, 2442 ActivePeriod: autoscalingv1.ActivePeriod{ 2443 Timezone: "UTC", 2444 StartCron: "* 2 * * *", 2445 Duration: "1h", 2446 }, 2447 Policy: bufferPolicy, 2448 }, 2449 want: expectedWhenActive, 2450 }, 2451 "daylight saving time end": { 2452 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2453 now: mustParseTime("2021-11-07T01:59:59Z"), 2454 sp: &autoscalingv1.SchedulePolicy{ 2455 Between: autoscalingv1.Between{ 2456 Start: mustParseMetav1Time("2021-11-07T00:00:00Z"), 2457 End: mustParseMetav1Time("2021-11-08T00:00:00Z"), 2458 }, 2459 ActivePeriod: autoscalingv1.ActivePeriod{ 2460 Timezone: "UTC", 2461 StartCron: "0 2 * * *", 2462 Duration: "1h", 2463 }, 2464 Policy: bufferPolicy, 2465 }, 2466 want: expectedWhenActive, 2467 }, 2468 "new year": { 2469 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2470 now: mustParseTime("2021-01-01T00:00:00Z"), 2471 sp: &autoscalingv1.SchedulePolicy{ 2472 Between: autoscalingv1.Between{ 2473 Start: mustParseMetav1Time("2020-12-31T24:59:59Z"), 2474 End: mustParseMetav1Time("2021-01-02T00:00:00Z"), 2475 }, 2476 ActivePeriod: autoscalingv1.ActivePeriod{ 2477 Timezone: "UTC", 2478 StartCron: "* 0 * * *", 2479 Duration: "1h", 2480 }, 2481 Policy: bufferPolicy, 2482 }, 2483 want: expectedWhenActive, 2484 }, 2485 "inactive schedule": { 2486 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2487 now: mustParseTime("2023-12-12T03:49:00Z"), 2488 sp: &autoscalingv1.SchedulePolicy{ 2489 Between: autoscalingv1.Between{ 2490 Start: mustParseMetav1Time("2022-12-31T24:59:59Z"), 2491 End: mustParseMetav1Time("2023-03-02T00:00:00Z"), 2492 }, 2493 ActivePeriod: autoscalingv1.ActivePeriod{ 2494 Timezone: "UTC", 2495 StartCron: "* 0 * 3 *", 2496 Duration: "", 2497 }, 2498 Policy: bufferPolicy, 2499 }, 2500 want: expectedWhenInactive, 2501 }, 2502 } 2503 2504 utilruntime.FeatureTestMutex.Lock() 2505 defer utilruntime.FeatureTestMutex.Unlock() 2506 2507 for name, tc := range testCases { 2508 t.Run(name, func(t *testing.T) { 2509 err := utilruntime.ParseFeatures(tc.featureFlags) 2510 assert.NoError(t, err) 2511 2512 ctx := context.Background() 2513 fas, f := defaultFixtures() 2514 m := agtesting.NewMocks() 2515 fasLog := FasLogger{ 2516 fas: fas, 2517 baseLogger: newTestLogger(), 2518 recorder: m.FakeRecorder, 2519 currChainEntry: &fas.Status.LastAppliedPolicy, 2520 } 2521 replicas, limited, err := applySchedulePolicy(ctx, map[string]any{}, tc.sp, f, nil, nil, tc.now, &fasLog) 2522 2523 if tc.want.wantErr { 2524 assert.NotNil(t, err) 2525 } else { 2526 assert.Nil(t, err) 2527 assert.Equal(t, tc.want.replicas, replicas) 2528 assert.Equal(t, tc.want.limited, limited) 2529 } 2530 }) 2531 } 2532 } 2533 2534 // nolint:dupl // Linter errors on lines are duplicate of TestApplyChainPolicy 2535 // NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateChainPolicy) 2536 func TestApplyChainPolicy(t *testing.T) { 2537 t.Parallel() 2538 2539 // For Webhook Policy 2540 ts := testServer{} 2541 server := httptest.NewServer(ts) 2542 defer server.Close() 2543 url := webhookURL 2544 2545 type expected struct { 2546 replicas int32 2547 limited bool 2548 wantErr bool 2549 } 2550 2551 scheduleOne := autoscalingv1.ChainEntry{ 2552 ID: "schedule-1", 2553 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2554 Type: autoscalingv1.SchedulePolicyType, 2555 Schedule: &autoscalingv1.SchedulePolicy{ 2556 Between: autoscalingv1.Between{ 2557 Start: mustParseMetav1Time("2024-08-01T10:07:36-06:00"), 2558 }, 2559 ActivePeriod: autoscalingv1.ActivePeriod{ 2560 Timezone: "America/Chicago", 2561 StartCron: "* * * * *", 2562 Duration: "", 2563 }, 2564 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2565 Type: autoscalingv1.BufferPolicyType, 2566 Buffer: &autoscalingv1.BufferPolicy{ 2567 BufferSize: intstr.FromInt(1), 2568 MinReplicas: 10, 2569 MaxReplicas: 10, 2570 }, 2571 }, 2572 }, 2573 }, 2574 } 2575 scheduleTwo := autoscalingv1.ChainEntry{ 2576 ID: "schedule-2", 2577 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2578 Type: autoscalingv1.SchedulePolicyType, 2579 Schedule: &autoscalingv1.SchedulePolicy{ 2580 Between: autoscalingv1.Between{ 2581 End: mustParseMetav1Time("2021-01-02T4:53:00-05:00"), 2582 }, 2583 ActivePeriod: autoscalingv1.ActivePeriod{ 2584 Timezone: "America/New_York", 2585 StartCron: "0 1 3 * *", 2586 Duration: "", 2587 }, 2588 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2589 Type: autoscalingv1.BufferPolicyType, 2590 Buffer: &autoscalingv1.BufferPolicy{ 2591 BufferSize: intstr.FromInt(1), 2592 MinReplicas: 3, 2593 MaxReplicas: 10, 2594 }, 2595 }, 2596 }, 2597 }, 2598 } 2599 webhookEntry := autoscalingv1.ChainEntry{ 2600 ID: "webhook policy", 2601 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2602 Type: autoscalingv1.WebhookPolicyType, 2603 Webhook: &autoscalingv1.URLConfiguration{ 2604 Service: &admregv1.ServiceReference{ 2605 Name: "service1", 2606 Namespace: "default", 2607 Path: &url, 2608 }, 2609 CABundle: []byte("invalid-value"), 2610 }, 2611 }, 2612 } 2613 defaultEntry := autoscalingv1.ChainEntry{ 2614 ID: "default", 2615 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2616 Type: autoscalingv1.BufferPolicyType, 2617 Buffer: &autoscalingv1.BufferPolicy{ 2618 BufferSize: intstr.FromInt(1), 2619 MinReplicas: 6, 2620 MaxReplicas: 10, 2621 }, 2622 }, 2623 } 2624 2625 testCases := map[string]struct { 2626 fleet *agonesv1.Fleet 2627 featureFlags string 2628 specReplicas int32 2629 statusReplicas int32 2630 statusAllocatedReplicas int32 2631 statusReadyReplicas int32 2632 now time.Time 2633 cp *autoscalingv1.ChainPolicy 2634 gsList []agonesv1.GameServer 2635 want expected 2636 }{ 2637 "scheduled autoscaler feature flag not enabled": { 2638 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", 2639 cp: &autoscalingv1.ChainPolicy{}, 2640 want: expected{ 2641 replicas: 0, 2642 limited: false, 2643 wantErr: true, 2644 }, 2645 }, 2646 "default policy": { 2647 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2648 cp: &autoscalingv1.ChainPolicy{defaultEntry}, 2649 want: expected{ 2650 replicas: 6, 2651 limited: true, 2652 wantErr: false, 2653 }, 2654 }, 2655 "one invalid webhook policy, one default (fallthrough)": { 2656 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2657 cp: &autoscalingv1.ChainPolicy{webhookEntry, defaultEntry}, 2658 want: expected{ 2659 replicas: 6, 2660 limited: true, 2661 wantErr: false, 2662 }, 2663 }, 2664 "two inactive schedule entries, no default (fall off chain)": { 2665 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2666 now: mustParseTime("2021-01-01T0:30:00Z"), 2667 cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleOne}, 2668 want: expected{ 2669 replicas: 5, 2670 limited: false, 2671 wantErr: true, 2672 }, 2673 }, 2674 "two inactive schedules entries, one default (fallthrough)": { 2675 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2676 now: mustParseTime("2021-11-05T5:30:00Z"), 2677 cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleTwo, defaultEntry}, 2678 want: expected{ 2679 replicas: 6, 2680 limited: true, 2681 wantErr: false, 2682 }, 2683 }, 2684 "two overlapping/active schedule entries, schedule-1 applied": { 2685 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2686 now: mustParseTime("2024-08-01T10:07:36-06:00"), 2687 cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleTwo}, 2688 want: expected{ 2689 replicas: 10, 2690 limited: true, 2691 wantErr: false, 2692 }, 2693 }, 2694 } 2695 2696 utilruntime.FeatureTestMutex.Lock() 2697 defer utilruntime.FeatureTestMutex.Unlock() 2698 2699 for name, tc := range testCases { 2700 t.Run(name, func(t *testing.T) { 2701 err := utilruntime.ParseFeatures(tc.featureFlags) 2702 assert.NoError(t, err) 2703 2704 ctx := context.Background() 2705 fas, f := defaultFixtures() 2706 m := agtesting.NewMocks() 2707 fasLog := FasLogger{ 2708 fas: fas, 2709 baseLogger: newTestLogger(), 2710 recorder: m.FakeRecorder, 2711 currChainEntry: &fas.Status.LastAppliedPolicy, 2712 } 2713 replicas, limited, err := applyChainPolicy(ctx, map[string]any{}, *tc.cp, f, nil, nil, tc.now, &fasLog) 2714 2715 if tc.want.wantErr { 2716 assert.NotNil(t, err) 2717 } else { 2718 assert.Nil(t, err) 2719 assert.Equal(t, tc.want.replicas, replicas) 2720 assert.Equal(t, tc.want.limited, limited) 2721 } 2722 }) 2723 } 2724 } 2725 2726 // Parse a time string and return a metav1.Time 2727 func mustParseMetav1Time(timeStr string) metav1.Time { 2728 t, _ := time.Parse(time.RFC3339, timeStr) 2729 return metav1.NewTime(t) 2730 } 2731 2732 // Parse a time string and return a time.Time 2733 func mustParseTime(timeStr string) time.Time { 2734 t, _ := time.Parse(time.RFC3339, timeStr) 2735 return t 2736 } 2737 2738 // Create a fake test logger using logr 2739 func newTestLogger() *logrus.Entry { 2740 return utilruntime.NewLoggerWithType(testServer{}) 2741 }