agones.dev/agones@v1.54.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, &fasState{}, 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(&fasState{}, 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(&fasState{}, 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(&fasState{}, 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(&fasState{}, 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 TestBuildURLFromConfiguration(t *testing.T) { 748 t.Parallel() 749 750 testURL := "testurl" 751 emptyURL := "" 752 validURL := "http://example.com/webhook" 753 invalidURL := "ht!tp://invalid url" 754 customPort := int32(9090) 755 invalidCABundle := []byte("invalid-ca-bundle") 756 757 type expected struct { 758 url string 759 err string 760 clientSet bool 761 } 762 763 testCases := []struct { 764 description string 765 state *fasState 766 config *autoscalingv1.URLConfiguration 767 expected expected 768 }{ 769 // Error cases 770 { 771 description: "Error: Both URL and Service provided", 772 state: &fasState{}, 773 config: &autoscalingv1.URLConfiguration{ 774 URL: &validURL, 775 Service: &admregv1.ServiceReference{ 776 Name: "service1", 777 Namespace: "default", 778 }, 779 }, 780 expected: expected{ 781 err: "service and URL cannot be used simultaneously", 782 }, 783 }, 784 { 785 description: "Error: Empty URL string", 786 state: &fasState{}, 787 config: &autoscalingv1.URLConfiguration{ 788 URL: &emptyURL, 789 }, 790 expected: expected{ 791 err: "URL was not provided", 792 }, 793 }, 794 { 795 description: "Error: Neither URL nor Service provided", 796 state: &fasState{}, 797 config: &autoscalingv1.URLConfiguration{ 798 URL: nil, 799 Service: nil, 800 }, 801 expected: expected{ 802 err: "service was not provided, either URL or Service must be provided", 803 }, 804 }, 805 { 806 description: "Error: Service name empty", 807 state: &fasState{}, 808 config: &autoscalingv1.URLConfiguration{ 809 Service: &admregv1.ServiceReference{ 810 Name: "", 811 Namespace: "default", 812 }, 813 }, 814 expected: expected{ 815 err: "service name was not provided", 816 }, 817 }, 818 { 819 description: "Error: Invalid URL format", 820 state: &fasState{}, 821 config: &autoscalingv1.URLConfiguration{ 822 URL: &invalidURL, 823 }, 824 expected: expected{ 825 err: "parse", 826 }, 827 }, 828 // Valid URL cases 829 { 830 description: "Valid URL without CABundle", 831 state: &fasState{}, 832 config: &autoscalingv1.URLConfiguration{ 833 URL: &validURL, 834 }, 835 expected: expected{ 836 url: validURL, 837 clientSet: true, 838 }, 839 }, 840 { 841 description: "Error: Invalid CABundle", 842 state: &fasState{}, 843 config: &autoscalingv1.URLConfiguration{ 844 URL: &validURL, 845 CABundle: invalidCABundle, 846 }, 847 expected: expected{ 848 err: "no certs were appended from caBundle", 849 }, 850 }, 851 // Service configuration cases 852 { 853 description: "Service: No namespace provided, default should be used", 854 state: &fasState{}, 855 config: &autoscalingv1.URLConfiguration{ 856 Service: &admregv1.ServiceReference{ 857 Name: "service1", 858 Namespace: "", 859 Path: &testURL, 860 }, 861 }, 862 expected: expected{ 863 url: "http://service1.default.svc:8000/testurl", 864 clientSet: true, 865 }, 866 }, 867 { 868 description: "Service: No path provided, empty string should be used", 869 state: &fasState{}, 870 config: &autoscalingv1.URLConfiguration{ 871 Service: &admregv1.ServiceReference{ 872 Name: "service1", 873 Namespace: "test", 874 Path: nil, 875 }, 876 }, 877 expected: expected{ 878 url: "http://service1.test.svc:8000", 879 clientSet: true, 880 }, 881 }, 882 { 883 description: "Service: Custom port specified", 884 state: &fasState{}, 885 config: &autoscalingv1.URLConfiguration{ 886 Service: &admregv1.ServiceReference{ 887 Name: "service1", 888 Namespace: "custom", 889 Path: &testURL, 890 Port: &customPort, 891 }, 892 }, 893 expected: expected{ 894 url: "http://service1.custom.svc:9090/testurl", 895 clientSet: true, 896 }, 897 }, 898 { 899 description: "HTTP client reused when already initialized", 900 state: &fasState{ 901 httpClient: &http.Client{}, 902 }, 903 config: &autoscalingv1.URLConfiguration{ 904 Service: &admregv1.ServiceReference{ 905 Name: "service1", 906 Namespace: "default", 907 }, 908 }, 909 expected: expected{ 910 url: "http://service1.default.svc:8000", 911 clientSet: true, 912 }, 913 }, 914 } 915 916 for _, tc := range testCases { 917 t.Run(tc.description, func(t *testing.T) { 918 url, err := buildURLFromConfiguration(tc.state, tc.config) 919 920 if tc.expected.err != "" { 921 assert.NotNil(t, err) 922 assert.Contains(t, err.Error(), tc.expected.err) 923 } else { 924 assert.Nil(t, err) 925 assert.NotNil(t, url) 926 assert.Equal(t, tc.expected.url, url.String()) 927 928 if tc.expected.clientSet { 929 assert.NotNil(t, tc.state.httpClient, "HTTP client should be initialized") 930 } 931 } 932 }) 933 } 934 } 935 936 // nolint:dupl // Linter errors on lines are duplicate of TestApplyListPolicy 937 func TestApplyCounterPolicy(t *testing.T) { 938 t.Parallel() 939 940 nc := map[string]gameservers.NodeCount{ 941 "n1": {Ready: 1, Allocated: 1}, 942 } 943 944 modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { 945 _, fleet := defaultFixtures() // The ObjectMeta.Name of the defaultFixtures fleet is "fleet-1" 946 f(fleet) 947 return fleet 948 } 949 950 type expected struct { 951 replicas int32 952 limited bool 953 wantErr bool 954 } 955 956 testCases := map[string]struct { 957 fleet *agonesv1.Fleet 958 featureFlags string 959 cp *autoscalingv1.CounterPolicy 960 gsList []agonesv1.GameServer 961 want expected 962 }{ 963 "counts and lists not enabled": { 964 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 965 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 966 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 967 Count: 0, 968 Capacity: 7} 969 f.Status.Replicas = 10 970 f.Status.ReadyReplicas = 5 971 f.Status.AllocatedReplicas = 5 972 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 973 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 974 Count: 31, 975 Capacity: 70, 976 } 977 }), 978 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=false", 979 cp: &autoscalingv1.CounterPolicy{ 980 Key: "rooms", 981 MaxCapacity: 100, 982 MinCapacity: 10, 983 BufferSize: intstr.FromInt(10), 984 }, 985 want: expected{ 986 replicas: 0, 987 limited: false, 988 wantErr: true, 989 }, 990 }, 991 "Counter based fleet does not have any replicas": { 992 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 993 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 994 f.Spec.Template.Spec.Counters["gamers"] = agonesv1.CounterStatus{ 995 Count: 0, 996 Capacity: 7} 997 f.Status.Replicas = 0 998 f.Status.ReadyReplicas = 0 999 f.Status.AllocatedReplicas = 0 1000 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1001 f.Status.Counters["gamers"] = agonesv1.AggregatedCounterStatus{} 1002 }), 1003 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1004 cp: &autoscalingv1.CounterPolicy{ 1005 Key: "gamers", 1006 MaxCapacity: 100, 1007 MinCapacity: 10, 1008 BufferSize: intstr.FromInt(10), 1009 }, 1010 want: expected{ 1011 replicas: 2, 1012 limited: true, 1013 wantErr: false, 1014 }, 1015 }, 1016 "fleet spec does not have counter": { 1017 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1018 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1019 f.Spec.Template.Spec.Counters["brooms"] = agonesv1.CounterStatus{ 1020 Count: 0, 1021 Capacity: 7} 1022 f.Status.Replicas = 10 1023 f.Status.ReadyReplicas = 5 1024 f.Status.AllocatedReplicas = 5 1025 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1026 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1027 Count: 31, 1028 Capacity: 70, 1029 } 1030 }), 1031 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1032 cp: &autoscalingv1.CounterPolicy{ 1033 Key: "rooms", 1034 MaxCapacity: 100, 1035 MinCapacity: 10, 1036 BufferSize: intstr.FromInt(10), 1037 }, 1038 want: expected{ 1039 replicas: 0, 1040 limited: false, 1041 wantErr: true, 1042 }, 1043 }, 1044 "fleet status does not have counter": { 1045 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1046 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1047 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1048 Count: 0, 1049 Capacity: 7} 1050 f.Status.Replicas = 10 1051 f.Status.ReadyReplicas = 5 1052 f.Status.AllocatedReplicas = 5 1053 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1054 f.Status.Counters["brooms"] = agonesv1.AggregatedCounterStatus{ 1055 Count: 31, 1056 Capacity: 70, 1057 } 1058 }), 1059 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1060 cp: &autoscalingv1.CounterPolicy{ 1061 Key: "rooms", 1062 MaxCapacity: 100, 1063 MinCapacity: 10, 1064 BufferSize: intstr.FromInt(10), 1065 }, 1066 want: expected{ 1067 replicas: 0, 1068 limited: false, 1069 wantErr: true, 1070 }, 1071 }, 1072 "scale down": { 1073 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1074 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1075 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1076 Count: 0, 1077 Capacity: 7} 1078 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Ascending"}} 1079 f.Status.Replicas = 8 1080 f.Status.ReadyReplicas = 4 1081 f.Status.AllocatedReplicas = 4 1082 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1083 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1084 Count: 31, 1085 Capacity: 70, 1086 } 1087 }), 1088 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1089 cp: &autoscalingv1.CounterPolicy{ 1090 Key: "rooms", 1091 MaxCapacity: 100, 1092 MinCapacity: 10, 1093 BufferSize: intstr.FromInt(10), 1094 }, 1095 gsList: []agonesv1.GameServer{ 1096 {ObjectMeta: metav1.ObjectMeta{ 1097 Name: "gs1", 1098 Namespace: "default", 1099 // We need the Label here so that the Lister can pick up that the gameserver is a part of 1100 // the fleet. If this was a real gameserver it would also have a label for 1101 // "agones.dev/gameserverset": "gameServerSetName". 1102 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1103 Status: agonesv1.GameServerStatus{ 1104 // We need NodeName here for sorting, otherwise sortGameServersByLeastFullNodes 1105 // will return the list of GameServers in the opposite order the were return by 1106 // ListGameServersByGameServerSetOwner (which is a nondeterministic order). 1107 NodeName: "n1", 1108 Counters: map[string]agonesv1.CounterStatus{ 1109 "rooms": { 1110 Count: 10, 1111 Capacity: 10, 1112 }}}}, 1113 {ObjectMeta: metav1.ObjectMeta{ 1114 Name: "gs2", 1115 Namespace: "default", 1116 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1117 Status: agonesv1.GameServerStatus{ 1118 NodeName: "n1", 1119 Counters: map[string]agonesv1.CounterStatus{ 1120 "rooms": { 1121 Count: 3, 1122 Capacity: 5, 1123 }}}}, 1124 {ObjectMeta: metav1.ObjectMeta{ 1125 Name: "gs3", 1126 Namespace: "default", 1127 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1128 Status: agonesv1.GameServerStatus{ 1129 NodeName: "n1", 1130 Counters: map[string]agonesv1.CounterStatus{ 1131 "rooms": { 1132 Count: 7, 1133 Capacity: 7, 1134 }}}}, 1135 {ObjectMeta: metav1.ObjectMeta{ 1136 Name: "gs4", 1137 Namespace: "default", 1138 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1139 Status: agonesv1.GameServerStatus{ 1140 NodeName: "n1", 1141 Counters: map[string]agonesv1.CounterStatus{ 1142 "rooms": { 1143 Count: 11, 1144 Capacity: 14, 1145 }}}}, 1146 {ObjectMeta: metav1.ObjectMeta{ 1147 Name: "gs5", 1148 Namespace: "default", 1149 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1150 Status: agonesv1.GameServerStatus{ 1151 NodeName: "n1", 1152 Counters: map[string]agonesv1.CounterStatus{ 1153 "rooms": { 1154 Count: 0, 1155 Capacity: 13, 1156 }}}}, 1157 {ObjectMeta: metav1.ObjectMeta{ 1158 Name: "gs6", 1159 Namespace: "default", 1160 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1161 Status: agonesv1.GameServerStatus{ 1162 NodeName: "n1", 1163 Counters: map[string]agonesv1.CounterStatus{ 1164 "rooms": { 1165 Count: 0, 1166 Capacity: 7, 1167 }}}}, 1168 {ObjectMeta: metav1.ObjectMeta{ 1169 Name: "gs7", 1170 Namespace: "default", 1171 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1172 Status: agonesv1.GameServerStatus{ 1173 NodeName: "n1", 1174 Counters: map[string]agonesv1.CounterStatus{ 1175 "rooms": { 1176 Count: 0, 1177 Capacity: 7, 1178 }}}}, 1179 {ObjectMeta: metav1.ObjectMeta{ 1180 Name: "gs8", 1181 Namespace: "default", 1182 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1183 Status: agonesv1.GameServerStatus{ 1184 NodeName: "n1", 1185 Counters: map[string]agonesv1.CounterStatus{ 1186 "rooms": { 1187 Count: 0, 1188 Capacity: 7, 1189 }}}}, 1190 }, 1191 want: expected{ 1192 replicas: 1, 1193 limited: false, 1194 wantErr: false, 1195 }, 1196 }, 1197 "scale up": { 1198 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1199 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1200 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1201 Count: 0, 1202 Capacity: 7} 1203 f.Status.Replicas = 10 1204 f.Status.ReadyReplicas = 0 1205 f.Status.AllocatedReplicas = 10 1206 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1207 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1208 Count: 68, 1209 Capacity: 70} 1210 }), 1211 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1212 cp: &autoscalingv1.CounterPolicy{ 1213 Key: "rooms", 1214 MaxCapacity: 100, 1215 MinCapacity: 10, 1216 BufferSize: intstr.FromInt(10), 1217 }, 1218 want: expected{ 1219 replicas: 12, 1220 limited: false, 1221 wantErr: false, 1222 }, 1223 }, 1224 "scale up integer": { 1225 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1226 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1227 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1228 Count: 7, 1229 Capacity: 10} 1230 f.Status.Replicas = 3 1231 f.Status.ReadyReplicas = 3 1232 f.Status.AllocatedReplicas = 0 1233 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1234 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1235 Count: 21, 1236 Capacity: 30} 1237 }), 1238 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1239 cp: &autoscalingv1.CounterPolicy{ 1240 Key: "rooms", 1241 MaxCapacity: 100, 1242 MinCapacity: 10, 1243 BufferSize: intstr.FromInt(25), 1244 }, 1245 want: expected{ 1246 replicas: 9, 1247 limited: false, 1248 wantErr: false, 1249 }, 1250 }, 1251 "scale same": { 1252 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1253 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1254 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1255 Count: 0, 1256 Capacity: 7} 1257 f.Status.Replicas = 10 1258 f.Status.ReadyReplicas = 0 1259 f.Status.AllocatedReplicas = 10 1260 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1261 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1262 Count: 60, 1263 Capacity: 70} 1264 }), 1265 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1266 cp: &autoscalingv1.CounterPolicy{ 1267 Key: "rooms", 1268 MaxCapacity: 100, 1269 MinCapacity: 10, 1270 BufferSize: intstr.FromInt(10), 1271 }, 1272 want: expected{ 1273 replicas: 10, 1274 limited: false, 1275 wantErr: false, 1276 }, 1277 }, 1278 "scale down at MinCapacity": { 1279 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1280 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1281 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1282 Count: 0, 1283 Capacity: 7} 1284 f.Status.Replicas = 10 1285 f.Status.ReadyReplicas = 9 1286 f.Status.AllocatedReplicas = 1 1287 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1288 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1289 Count: 1, 1290 Capacity: 70} 1291 }), 1292 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1293 cp: &autoscalingv1.CounterPolicy{ 1294 Key: "rooms", 1295 MaxCapacity: 700, 1296 MinCapacity: 70, 1297 BufferSize: intstr.FromInt(10), 1298 }, 1299 want: expected{ 1300 replicas: 10, 1301 limited: true, 1302 wantErr: false, 1303 }, 1304 }, 1305 "scale down limited": { 1306 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1307 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1308 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1309 Count: 0, 1310 Capacity: 7} 1311 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Descending"}} 1312 f.Status.Replicas = 4 1313 f.Status.ReadyReplicas = 3 1314 f.Status.AllocatedReplicas = 1 1315 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1316 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1317 Count: 1, 1318 Capacity: 36} 1319 }), 1320 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1321 cp: &autoscalingv1.CounterPolicy{ 1322 Key: "rooms", 1323 MaxCapacity: 700, 1324 MinCapacity: 7, 1325 BufferSize: intstr.FromInt(1), 1326 }, 1327 gsList: []agonesv1.GameServer{ 1328 {ObjectMeta: metav1.ObjectMeta{ 1329 Name: "gs1", 1330 Namespace: "default", 1331 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1332 Status: agonesv1.GameServerStatus{ 1333 NodeName: "n1", 1334 Counters: map[string]agonesv1.CounterStatus{ 1335 "rooms": { 1336 Count: 0, 1337 Capacity: 10, 1338 }}}}, 1339 {ObjectMeta: metav1.ObjectMeta{ 1340 Name: "gs2", 1341 Namespace: "default", 1342 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1343 Status: agonesv1.GameServerStatus{ 1344 NodeName: "n1", 1345 Counters: map[string]agonesv1.CounterStatus{ 1346 "rooms": { 1347 Count: 1, 1348 Capacity: 5, 1349 }}}}, 1350 {ObjectMeta: metav1.ObjectMeta{ 1351 Name: "gs3", 1352 Namespace: "default", 1353 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1354 Status: agonesv1.GameServerStatus{ 1355 NodeName: "n1", 1356 Counters: map[string]agonesv1.CounterStatus{ 1357 "rooms": { 1358 Count: 0, 1359 Capacity: 7, 1360 }}}}, 1361 {ObjectMeta: metav1.ObjectMeta{ 1362 Name: "gs4", 1363 Namespace: "default", 1364 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1365 Status: agonesv1.GameServerStatus{ 1366 NodeName: "n1", 1367 Counters: map[string]agonesv1.CounterStatus{ 1368 "rooms": { 1369 Count: 0, 1370 Capacity: 14, 1371 }}}}}, 1372 want: expected{ 1373 replicas: 2, 1374 limited: true, 1375 wantErr: false, 1376 }, 1377 }, 1378 "scale down limited must scale up": { 1379 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1380 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1381 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1382 Count: 0, 1383 Capacity: 7} 1384 f.Status.Replicas = 7 1385 f.Status.ReadyReplicas = 6 1386 f.Status.AllocatedReplicas = 1 1387 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1388 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1389 Count: 1, 1390 Capacity: 49} 1391 }), 1392 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1393 cp: &autoscalingv1.CounterPolicy{ 1394 Key: "rooms", 1395 MaxCapacity: 700, 1396 MinCapacity: 70, 1397 BufferSize: intstr.FromInt(10), 1398 }, 1399 want: expected{ 1400 replicas: 10, 1401 limited: true, 1402 wantErr: false, 1403 }, 1404 }, 1405 "scale up limited": { 1406 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1407 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1408 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1409 Count: 0, 1410 Capacity: 7} 1411 f.Status.Replicas = 14 1412 f.Status.ReadyReplicas = 0 1413 f.Status.AllocatedReplicas = 14 1414 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1415 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1416 Count: 98, 1417 Capacity: 98} 1418 }), 1419 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1420 cp: &autoscalingv1.CounterPolicy{ 1421 Key: "rooms", 1422 MaxCapacity: 100, 1423 MinCapacity: 10, 1424 BufferSize: intstr.FromInt(10), 1425 }, 1426 want: expected{ 1427 replicas: 14, 1428 limited: true, 1429 wantErr: false, 1430 }, 1431 }, 1432 "scale up limited must scale down": { 1433 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1434 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1435 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1436 Count: 0, 1437 Capacity: 7} 1438 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Descending"}} 1439 f.Status.Replicas = 1 1440 f.Status.ReadyReplicas = 0 1441 f.Status.AllocatedReplicas = 1 1442 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1443 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1444 Count: 7, 1445 Capacity: 7} 1446 }), 1447 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1448 cp: &autoscalingv1.CounterPolicy{ 1449 Key: "rooms", 1450 MaxCapacity: 2, 1451 MinCapacity: 0, 1452 BufferSize: intstr.FromInt(1), 1453 }, 1454 gsList: []agonesv1.GameServer{ 1455 {ObjectMeta: metav1.ObjectMeta{ 1456 Name: "gs1", 1457 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1458 Status: agonesv1.GameServerStatus{ 1459 NodeName: "n1", 1460 Counters: map[string]agonesv1.CounterStatus{ 1461 "rooms": { 1462 Count: 7, 1463 Capacity: 7, 1464 }}}}}, 1465 want: expected{ 1466 replicas: 1, 1467 limited: true, 1468 wantErr: false, 1469 }, 1470 }, 1471 "scale down to max capacity": { 1472 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1473 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1474 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1475 Count: 0, 1476 Capacity: 5} 1477 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Descending"}} 1478 f.Status.Replicas = 3 1479 f.Status.ReadyReplicas = 3 1480 f.Status.AllocatedReplicas = 0 1481 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1482 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1483 Count: 0, 1484 Capacity: 15} 1485 }), 1486 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1487 cp: &autoscalingv1.CounterPolicy{ 1488 Key: "rooms", 1489 MaxCapacity: 5, 1490 MinCapacity: 1, 1491 BufferSize: intstr.FromInt(5), 1492 }, 1493 gsList: []agonesv1.GameServer{ 1494 {ObjectMeta: metav1.ObjectMeta{ 1495 Name: "gs1", 1496 Namespace: "default", 1497 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1498 Status: agonesv1.GameServerStatus{ 1499 NodeName: "n1", 1500 Counters: map[string]agonesv1.CounterStatus{ 1501 "rooms": { 1502 Count: 0, 1503 Capacity: 5, 1504 }}}}, 1505 {ObjectMeta: metav1.ObjectMeta{ 1506 Name: "gs2", 1507 Namespace: "default", 1508 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1509 Status: agonesv1.GameServerStatus{ 1510 NodeName: "n1", 1511 Counters: map[string]agonesv1.CounterStatus{ 1512 "rooms": { 1513 Count: 0, 1514 Capacity: 5, 1515 }}}}, 1516 {ObjectMeta: metav1.ObjectMeta{ 1517 Name: "gs3", 1518 Namespace: "default", 1519 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1520 Status: agonesv1.GameServerStatus{ 1521 NodeName: "n1", 1522 Counters: map[string]agonesv1.CounterStatus{ 1523 "rooms": { 1524 Count: 0, 1525 Capacity: 5, 1526 }}}}, 1527 }, 1528 want: expected{ 1529 replicas: 1, 1530 limited: false, 1531 wantErr: false, 1532 }, 1533 }, 1534 "scale up to MinCapacity": { 1535 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1536 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1537 f.Spec.Template.Spec.Counters["rooms"] = agonesv1.CounterStatus{ 1538 Count: 0, 1539 Capacity: 10} 1540 f.Spec.Priorities = []agonesv1.Priority{{Type: "Counter", Key: "rooms", Order: "Descending"}} 1541 f.Status.Replicas = 3 1542 f.Status.ReadyReplicas = 0 1543 f.Status.AllocatedReplicas = 3 1544 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1545 f.Status.Counters["rooms"] = agonesv1.AggregatedCounterStatus{ 1546 Count: 20, 1547 Capacity: 30, 1548 } 1549 }), 1550 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1551 cp: &autoscalingv1.CounterPolicy{ 1552 Key: "rooms", 1553 MaxCapacity: 100, 1554 MinCapacity: 50, 1555 BufferSize: intstr.FromString("10%"), 1556 }, 1557 want: expected{ 1558 replicas: 5, 1559 limited: true, 1560 wantErr: false, 1561 }, 1562 }, 1563 "scale up by percent": { 1564 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1565 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1566 f.Spec.Template.Spec.Counters["players"] = agonesv1.CounterStatus{ 1567 Count: 0, 1568 Capacity: 1} 1569 f.Status.Replicas = 10 1570 f.Status.ReadyReplicas = 2 1571 f.Status.AllocatedReplicas = 8 1572 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1573 f.Status.Counters["players"] = agonesv1.AggregatedCounterStatus{ 1574 AllocatedCount: 8, 1575 AllocatedCapacity: 10, 1576 Count: 8, 1577 Capacity: 10, 1578 } 1579 }), 1580 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1581 cp: &autoscalingv1.CounterPolicy{ 1582 Key: "players", 1583 MaxCapacity: 100, 1584 MinCapacity: 10, 1585 BufferSize: intstr.FromString("30%"), 1586 }, 1587 want: expected{ 1588 replicas: 12, 1589 limited: false, 1590 wantErr: false, 1591 }, 1592 }, 1593 "scale up by percent with Count": { 1594 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1595 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1596 f.Spec.Template.Spec.Counters["players"] = agonesv1.CounterStatus{ 1597 Count: 3, 1598 Capacity: 10} 1599 f.Status.Replicas = 3 1600 f.Status.ReadyReplicas = 0 1601 f.Status.AllocatedReplicas = 3 1602 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1603 f.Status.Counters["players"] = agonesv1.AggregatedCounterStatus{ 1604 AllocatedCount: 20, 1605 AllocatedCapacity: 30, 1606 Count: 20, 1607 Capacity: 30, 1608 } 1609 }), 1610 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1611 cp: &autoscalingv1.CounterPolicy{ 1612 Key: "players", 1613 MaxCapacity: 100, 1614 MinCapacity: 10, 1615 BufferSize: intstr.FromString("50%"), 1616 }, 1617 want: expected{ 1618 replicas: 5, 1619 limited: false, 1620 wantErr: false, 1621 }, 1622 }, 1623 "scale down by integer buffer": { 1624 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1625 f.Spec.Template.Spec.Counters = make(map[string]agonesv1.CounterStatus) 1626 f.Spec.Template.Spec.Counters["players"] = agonesv1.CounterStatus{ 1627 Count: 7, 1628 Capacity: 10} 1629 f.Status.Replicas = 3 1630 f.Status.ReadyReplicas = 0 1631 f.Status.AllocatedReplicas = 3 1632 f.Status.Counters = make(map[string]agonesv1.AggregatedCounterStatus) 1633 f.Status.Counters["players"] = agonesv1.AggregatedCounterStatus{ 1634 Count: 21, 1635 Capacity: 30, 1636 } 1637 }), 1638 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1639 cp: &autoscalingv1.CounterPolicy{ 1640 Key: "players", 1641 MaxCapacity: 100, 1642 MinCapacity: 10, 1643 BufferSize: intstr.FromInt(5), 1644 }, 1645 gsList: []agonesv1.GameServer{ 1646 {ObjectMeta: metav1.ObjectMeta{ 1647 Name: "gs1", 1648 Namespace: "default", 1649 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1650 Status: agonesv1.GameServerStatus{ 1651 NodeName: "n1", 1652 Counters: map[string]agonesv1.CounterStatus{ 1653 "players": { 1654 Count: 7, 1655 Capacity: 10, 1656 }}}}, 1657 {ObjectMeta: metav1.ObjectMeta{ 1658 Name: "gs2", 1659 Namespace: "default", 1660 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1661 Status: agonesv1.GameServerStatus{ 1662 NodeName: "n1", 1663 Counters: map[string]agonesv1.CounterStatus{ 1664 "players": { 1665 Count: 7, 1666 Capacity: 10, 1667 }}}}, 1668 {ObjectMeta: metav1.ObjectMeta{ 1669 Name: "gs3", 1670 Namespace: "default", 1671 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1672 Status: agonesv1.GameServerStatus{ 1673 NodeName: "n1", 1674 Counters: map[string]agonesv1.CounterStatus{ 1675 "players": { 1676 Count: 7, 1677 Capacity: 10, 1678 }}}}, 1679 }, 1680 want: expected{ 1681 replicas: 2, 1682 limited: false, 1683 wantErr: false, 1684 }, 1685 }, 1686 } 1687 1688 utilruntime.FeatureTestMutex.Lock() 1689 defer utilruntime.FeatureTestMutex.Unlock() 1690 1691 for name, tc := range testCases { 1692 t.Run(name, func(t *testing.T) { 1693 err := utilruntime.ParseFeatures(tc.featureFlags) 1694 assert.NoError(t, err) 1695 1696 m := agtesting.NewMocks() 1697 m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1698 return true, &agonesv1.GameServerList{Items: tc.gsList}, nil 1699 }) 1700 1701 informer := m.AgonesInformerFactory.Agones().V1() 1702 _, cancel := agtesting.StartInformers(m, 1703 informer.GameServers().Informer().HasSynced) 1704 defer cancel() 1705 1706 replicas, limited, err := applyCounterOrListPolicy(tc.cp, nil, tc.fleet, informer.GameServers().Lister().GameServers(tc.fleet.ObjectMeta.Namespace), nc) 1707 1708 if tc.want.wantErr { 1709 assert.NotNil(t, err) 1710 } else { 1711 assert.Nil(t, err) 1712 assert.Equal(t, tc.want.replicas, replicas) 1713 assert.Equal(t, tc.want.limited, limited) 1714 } 1715 }) 1716 } 1717 } 1718 1719 // nolint:dupl // Linter errors on lines are duplicate of TestApplyCounterPolicy 1720 // NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateListPolicy) 1721 func TestApplyListPolicy(t *testing.T) { 1722 t.Parallel() 1723 1724 nc := map[string]gameservers.NodeCount{ 1725 "n1": {Ready: 0, Allocated: 2}, 1726 "n2": {Ready: 1}, 1727 } 1728 1729 modifiedFleet := func(f func(*agonesv1.Fleet)) *agonesv1.Fleet { 1730 _, fleet := defaultFixtures() 1731 f(fleet) 1732 return fleet 1733 } 1734 1735 type expected struct { 1736 replicas int32 1737 limited bool 1738 wantErr bool 1739 } 1740 1741 testCases := map[string]struct { 1742 fleet *agonesv1.Fleet 1743 featureFlags string 1744 lp *autoscalingv1.ListPolicy 1745 gsList []agonesv1.GameServer 1746 want expected 1747 }{ 1748 "counts and lists not enabled": { 1749 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1750 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1751 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1752 Values: []string{}, 1753 Capacity: 7} 1754 f.Status.Replicas = 10 1755 f.Status.ReadyReplicas = 5 1756 f.Status.AllocatedReplicas = 5 1757 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1758 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1759 Count: 31, 1760 Capacity: 70, 1761 } 1762 }), 1763 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=false", 1764 lp: &autoscalingv1.ListPolicy{ 1765 Key: "gamers", 1766 MaxCapacity: 100, 1767 MinCapacity: 10, 1768 BufferSize: intstr.FromInt(10), 1769 }, 1770 want: expected{ 1771 replicas: 0, 1772 limited: false, 1773 wantErr: true, 1774 }, 1775 }, 1776 "fleet spec does not have list": { 1777 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1778 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1779 f.Spec.Template.Spec.Lists["tamers"] = agonesv1.ListStatus{ 1780 Values: []string{}, 1781 Capacity: 7} 1782 f.Status.Replicas = 10 1783 f.Status.ReadyReplicas = 5 1784 f.Status.AllocatedReplicas = 5 1785 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1786 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1787 Count: 31, 1788 Capacity: 70, 1789 } 1790 }), 1791 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1792 lp: &autoscalingv1.ListPolicy{ 1793 Key: "gamers", 1794 MaxCapacity: 100, 1795 MinCapacity: 10, 1796 BufferSize: intstr.FromInt(10), 1797 }, 1798 want: expected{ 1799 replicas: 0, 1800 limited: false, 1801 wantErr: true, 1802 }, 1803 }, 1804 "fleet status does not have list": { 1805 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1806 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1807 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1808 Values: []string{}, 1809 Capacity: 7} 1810 f.Status.Replicas = 10 1811 f.Status.ReadyReplicas = 5 1812 f.Status.AllocatedReplicas = 5 1813 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1814 f.Status.Lists["tamers"] = agonesv1.AggregatedListStatus{ 1815 Count: 31, 1816 Capacity: 70, 1817 } 1818 }), 1819 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1820 lp: &autoscalingv1.ListPolicy{ 1821 Key: "gamers", 1822 MaxCapacity: 100, 1823 MinCapacity: 10, 1824 BufferSize: intstr.FromInt(10), 1825 }, 1826 want: expected{ 1827 replicas: 0, 1828 limited: false, 1829 wantErr: true, 1830 }, 1831 }, 1832 "List based fleet does not have any replicas": { 1833 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1834 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1835 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1836 Values: []string{}, 1837 Capacity: 7} 1838 f.Status.Replicas = 0 1839 f.Status.ReadyReplicas = 0 1840 f.Status.AllocatedReplicas = 0 1841 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1842 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{} 1843 }), 1844 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1845 lp: &autoscalingv1.ListPolicy{ 1846 Key: "gamers", 1847 MaxCapacity: 100, 1848 MinCapacity: 10, 1849 BufferSize: intstr.FromInt(10), 1850 }, 1851 want: expected{ 1852 replicas: 2, 1853 limited: true, 1854 wantErr: false, 1855 }, 1856 }, 1857 "scale up": { 1858 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1859 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1860 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1861 Values: []string{"default", "default2"}, 1862 Capacity: 3} 1863 f.Status.Replicas = 10 1864 f.Status.ReadyReplicas = 0 1865 f.Status.AllocatedReplicas = 10 1866 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1867 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1868 Count: 29, 1869 Capacity: 30, 1870 } 1871 }), 1872 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1873 lp: &autoscalingv1.ListPolicy{ 1874 Key: "gamers", 1875 MaxCapacity: 100, 1876 MinCapacity: 10, 1877 BufferSize: intstr.FromInt(5), 1878 }, 1879 want: expected{ 1880 replicas: 14, 1881 limited: false, 1882 wantErr: false, 1883 }, 1884 }, 1885 "scale up to maxcapacity": { 1886 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1887 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1888 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1889 Values: []string{"default", "default2", "default3"}, 1890 Capacity: 5} 1891 f.Status.Replicas = 3 1892 f.Status.ReadyReplicas = 3 1893 f.Status.AllocatedReplicas = 0 1894 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1895 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1896 Count: 9, 1897 Capacity: 15, 1898 } 1899 }), 1900 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1901 lp: &autoscalingv1.ListPolicy{ 1902 Key: "gamers", 1903 MaxCapacity: 25, 1904 MinCapacity: 15, 1905 BufferSize: intstr.FromInt(15), 1906 }, 1907 want: expected{ 1908 replicas: 5, 1909 limited: true, 1910 wantErr: false, 1911 }, 1912 }, 1913 "scale down": { 1914 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 1915 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 1916 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 1917 Values: []string{"default"}, 1918 Capacity: 10} 1919 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Descending"}} 1920 f.Status.Replicas = 8 1921 f.Status.ReadyReplicas = 6 1922 f.Status.AllocatedReplicas = 4 1923 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 1924 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 1925 Count: 15, 1926 Capacity: 70, 1927 } 1928 }), 1929 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 1930 lp: &autoscalingv1.ListPolicy{ 1931 Key: "gamers", 1932 MaxCapacity: 70, 1933 MinCapacity: 10, 1934 BufferSize: intstr.FromInt(10), 1935 }, 1936 gsList: []agonesv1.GameServer{ 1937 {ObjectMeta: metav1.ObjectMeta{ 1938 Name: "gs1", 1939 Namespace: "default", 1940 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1941 Status: agonesv1.GameServerStatus{ 1942 NodeName: "n1", 1943 Lists: map[string]agonesv1.ListStatus{ 1944 "gamers": { 1945 Values: []string{}, 1946 Capacity: 10, 1947 }}}}, 1948 {ObjectMeta: metav1.ObjectMeta{ 1949 Name: "gs2", 1950 Namespace: "default", 1951 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1952 Status: agonesv1.GameServerStatus{ 1953 NodeName: "n1", 1954 Lists: map[string]agonesv1.ListStatus{ 1955 "gamers": { 1956 Values: []string{}, 1957 Capacity: 10, 1958 }}}}, 1959 {ObjectMeta: metav1.ObjectMeta{ 1960 Name: "gs3", 1961 Namespace: "default", 1962 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1963 Status: agonesv1.GameServerStatus{ 1964 NodeName: "n1", 1965 Lists: map[string]agonesv1.ListStatus{ 1966 "gamers": { 1967 Values: []string{}, 1968 Capacity: 10, 1969 }}}}, 1970 {ObjectMeta: metav1.ObjectMeta{ 1971 Name: "gs4", 1972 Namespace: "default", 1973 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1974 Status: agonesv1.GameServerStatus{ 1975 NodeName: "n1", 1976 Lists: map[string]agonesv1.ListStatus{ 1977 "gamers": { 1978 Values: []string{"default1", "default2", "default3", "default4", "default5", "default6", "default7", "default8"}, 1979 Capacity: 8, 1980 }}}}, 1981 {ObjectMeta: metav1.ObjectMeta{ 1982 Name: "gs5", 1983 Namespace: "default", 1984 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1985 Status: agonesv1.GameServerStatus{ 1986 NodeName: "n1", 1987 Lists: map[string]agonesv1.ListStatus{ 1988 "gamers": { 1989 Values: []string{"default9", "default10", "default11", "default12"}, 1990 Capacity: 10, 1991 }}}}, 1992 {ObjectMeta: metav1.ObjectMeta{ 1993 Name: "gs6", 1994 Namespace: "default", 1995 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 1996 Status: agonesv1.GameServerStatus{ 1997 NodeName: "n1", 1998 Lists: map[string]agonesv1.ListStatus{ 1999 "gamers": { 2000 Values: []string{"default"}, 2001 Capacity: 4, 2002 }}}}, 2003 {ObjectMeta: metav1.ObjectMeta{ 2004 Name: "gs7", 2005 Namespace: "default", 2006 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2007 Status: agonesv1.GameServerStatus{ 2008 NodeName: "n1", 2009 Lists: map[string]agonesv1.ListStatus{ 2010 "gamers": { 2011 Values: []string{"default"}, 2012 Capacity: 8, 2013 }}}}, 2014 {ObjectMeta: metav1.ObjectMeta{ 2015 Name: "gs8", 2016 Namespace: "default", 2017 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2018 Status: agonesv1.GameServerStatus{ 2019 NodeName: "n1", 2020 Lists: map[string]agonesv1.ListStatus{ 2021 "gamers": { 2022 Values: []string{"default"}, 2023 Capacity: 10, 2024 }}}}, 2025 }, 2026 want: expected{ 2027 replicas: 4, 2028 limited: false, 2029 wantErr: false, 2030 }, 2031 }, 2032 "scale up limited": { 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", "default2"}, 2037 Capacity: 3} 2038 f.Status.Replicas = 10 2039 f.Status.ReadyReplicas = 0 2040 f.Status.AllocatedReplicas = 10 2041 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2042 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2043 Count: 29, 2044 Capacity: 30, 2045 } 2046 }), 2047 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2048 lp: &autoscalingv1.ListPolicy{ 2049 Key: "gamers", 2050 MaxCapacity: 30, 2051 MinCapacity: 10, 2052 BufferSize: intstr.FromInt(5), 2053 }, 2054 want: expected{ 2055 replicas: 10, 2056 limited: true, 2057 wantErr: false, 2058 }, 2059 }, 2060 "scale down limited": { 2061 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2062 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2063 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2064 Values: []string{}, 2065 Capacity: 5} 2066 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Ascending"}} 2067 f.Status.Replicas = 4 2068 f.Status.ReadyReplicas = 3 2069 f.Status.AllocatedReplicas = 1 2070 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2071 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2072 Count: 3, 2073 Capacity: 20, 2074 } 2075 }), 2076 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2077 lp: &autoscalingv1.ListPolicy{ 2078 Key: "gamers", 2079 MaxCapacity: 100, 2080 MinCapacity: 10, 2081 BufferSize: intstr.FromInt(1), 2082 }, 2083 gsList: []agonesv1.GameServer{ 2084 {ObjectMeta: metav1.ObjectMeta{ 2085 Name: "gs1", 2086 Namespace: "default", 2087 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2088 Status: agonesv1.GameServerStatus{ 2089 NodeName: "n1", 2090 Lists: map[string]agonesv1.ListStatus{ 2091 "gamers": { 2092 Values: []string{}, 2093 Capacity: 5, 2094 }}}}, 2095 {ObjectMeta: metav1.ObjectMeta{ 2096 Name: "gs2", 2097 Namespace: "default", 2098 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2099 Status: agonesv1.GameServerStatus{ 2100 NodeName: "n1", 2101 Lists: map[string]agonesv1.ListStatus{ 2102 "gamers": { 2103 Values: []string{}, 2104 Capacity: 5, 2105 }}}}, 2106 {ObjectMeta: metav1.ObjectMeta{ 2107 Name: "gs3", 2108 Namespace: "default", 2109 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2110 Status: agonesv1.GameServerStatus{ 2111 NodeName: "n1", 2112 Lists: map[string]agonesv1.ListStatus{ 2113 "gamers": { 2114 Values: []string{}, 2115 Capacity: 5, 2116 }}}}, 2117 {ObjectMeta: metav1.ObjectMeta{ 2118 Name: "gs4", 2119 Namespace: "default", 2120 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2121 Status: agonesv1.GameServerStatus{ 2122 NodeName: "n1", 2123 Lists: map[string]agonesv1.ListStatus{ 2124 "gamers": { 2125 Values: []string{"default1", "default2", "default3"}, 2126 Capacity: 5, 2127 }}}}}, 2128 want: expected{ 2129 replicas: 2, 2130 limited: true, 2131 wantErr: false, 2132 }, 2133 }, 2134 "scale up by percent limited": { 2135 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2136 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2137 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2138 Values: []string{"default", "default2", "default3"}, 2139 Capacity: 10} 2140 f.Status.Replicas = 3 2141 f.Status.ReadyReplicas = 0 2142 f.Status.AllocatedReplicas = 3 2143 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2144 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2145 AllocatedCount: 20, 2146 AllocatedCapacity: 30, 2147 Count: 20, 2148 Capacity: 30, 2149 } 2150 }), 2151 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2152 lp: &autoscalingv1.ListPolicy{ 2153 Key: "gamers", 2154 MaxCapacity: 45, 2155 MinCapacity: 10, 2156 BufferSize: intstr.FromString("50%"), 2157 }, 2158 want: expected{ 2159 replicas: 4, 2160 limited: true, 2161 wantErr: false, 2162 }, 2163 }, 2164 "scale up by percent": { 2165 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2166 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2167 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2168 Values: []string{"default"}, 2169 Capacity: 3} 2170 f.Status.Replicas = 11 2171 f.Status.ReadyReplicas = 1 2172 f.Status.AllocatedReplicas = 10 2173 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2174 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2175 AllocatedCount: 29, 2176 AllocatedCapacity: 30, 2177 Count: 30, 2178 Capacity: 30, 2179 } 2180 }), 2181 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2182 lp: &autoscalingv1.ListPolicy{ 2183 Key: "gamers", 2184 MaxCapacity: 50, 2185 MinCapacity: 10, 2186 BufferSize: intstr.FromString("10%"), 2187 }, 2188 want: expected{ 2189 replicas: 13, 2190 limited: false, 2191 wantErr: false, 2192 }, 2193 }, 2194 "scale down by percent to Zero": { 2195 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2196 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2197 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2198 Values: []string{"default", "default2"}, 2199 Capacity: 10} 2200 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Descending"}} 2201 f.Status.Replicas = 3 2202 f.Status.ReadyReplicas = 3 2203 f.Status.AllocatedReplicas = 0 2204 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2205 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2206 AllocatedCount: 0, 2207 AllocatedCapacity: 0, 2208 Count: 15, 2209 Capacity: 30, 2210 } 2211 }), 2212 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2213 lp: &autoscalingv1.ListPolicy{ 2214 Key: "gamers", 2215 MaxCapacity: 50, 2216 MinCapacity: 0, 2217 BufferSize: intstr.FromString("20%"), 2218 }, 2219 gsList: []agonesv1.GameServer{ 2220 {ObjectMeta: metav1.ObjectMeta{ 2221 Name: "gs1", 2222 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2223 Status: agonesv1.GameServerStatus{ 2224 NodeName: "n1", 2225 Lists: map[string]agonesv1.ListStatus{ 2226 "gamers": { 2227 Values: []string{"1", "2", "3", "4", "5"}, 2228 Capacity: 15, 2229 }}}}, 2230 {ObjectMeta: metav1.ObjectMeta{ 2231 Name: "gs2", 2232 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2233 Status: agonesv1.GameServerStatus{ 2234 NodeName: "n1", 2235 Lists: map[string]agonesv1.ListStatus{ 2236 "gamers": { 2237 Values: []string{"1", "2", "3", "4", "5", "6", "7"}, 2238 Capacity: 10, 2239 }}}}, 2240 {ObjectMeta: metav1.ObjectMeta{ 2241 Name: "gs3", 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"}, 2248 Capacity: 5, 2249 }}}}, 2250 }, 2251 want: expected{ 2252 replicas: 1, 2253 limited: true, 2254 wantErr: false, 2255 }, 2256 }, 2257 "scale down by percent": { 2258 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2259 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2260 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2261 Values: []string{"default", "default2"}, 2262 Capacity: 10} 2263 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Descending"}} 2264 f.Status.Replicas = 5 2265 f.Status.ReadyReplicas = 2 2266 f.Status.AllocatedReplicas = 3 2267 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2268 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2269 AllocatedCount: 15, 2270 AllocatedCapacity: 30, 2271 Count: 18, 2272 Capacity: 50, 2273 } 2274 }), 2275 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2276 lp: &autoscalingv1.ListPolicy{ 2277 Key: "gamers", 2278 MaxCapacity: 50, 2279 MinCapacity: 0, 2280 BufferSize: intstr.FromString("50%"), 2281 }, 2282 gsList: []agonesv1.GameServer{ 2283 {ObjectMeta: metav1.ObjectMeta{ 2284 Name: "gs1", 2285 Namespace: "default", 2286 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2287 Status: agonesv1.GameServerStatus{ 2288 NodeName: "n1", 2289 Lists: map[string]agonesv1.ListStatus{ 2290 "gamers": { 2291 Values: []string{"1", "2", "3", "4", "5"}, 2292 Capacity: 15, 2293 }}}}, 2294 {ObjectMeta: metav1.ObjectMeta{ 2295 Name: "gs2", 2296 Namespace: "default", 2297 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2298 Status: agonesv1.GameServerStatus{ 2299 NodeName: "n1", 2300 Lists: map[string]agonesv1.ListStatus{ 2301 "gamers": { 2302 Values: []string{"1", "2", "3", "4", "5", "6", "7"}, 2303 Capacity: 10, 2304 }}}}, 2305 {ObjectMeta: metav1.ObjectMeta{ 2306 Name: "gs3", 2307 Namespace: "default", 2308 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2309 Status: agonesv1.GameServerStatus{ 2310 NodeName: "n1", 2311 Lists: map[string]agonesv1.ListStatus{ 2312 "gamers": { 2313 Values: []string{"1", "2", "3"}, 2314 Capacity: 5, 2315 }}}}, 2316 {ObjectMeta: metav1.ObjectMeta{ 2317 Name: "gs4", 2318 Namespace: "default", 2319 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2320 Status: agonesv1.GameServerStatus{ 2321 NodeName: "n2", 2322 Lists: map[string]agonesv1.ListStatus{ 2323 "gamers": { 2324 Values: []string{"1", "2", "3"}, 2325 Capacity: 5, 2326 }}}}, 2327 {ObjectMeta: metav1.ObjectMeta{ 2328 Name: "gs5", 2329 Namespace: "default", 2330 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2331 Status: agonesv1.GameServerStatus{ 2332 NodeName: "n2", 2333 Lists: map[string]agonesv1.ListStatus{ 2334 "gamers": { 2335 Values: []string{}, 2336 Capacity: 15, 2337 }}}}, 2338 }, 2339 want: expected{ 2340 replicas: 3, 2341 limited: false, 2342 wantErr: false, 2343 }, 2344 }, 2345 "scale down by percent limited": { 2346 fleet: modifiedFleet(func(f *agonesv1.Fleet) { 2347 f.Spec.Template.Spec.Lists = make(map[string]agonesv1.ListStatus) 2348 f.Spec.Template.Spec.Lists["gamers"] = agonesv1.ListStatus{ 2349 Values: []string{"default", "default2"}, 2350 Capacity: 10} 2351 f.Spec.Priorities = []agonesv1.Priority{{Type: "List", Key: "gamers", Order: "Descending"}} 2352 f.Status.Replicas = 3 2353 f.Status.ReadyReplicas = 3 2354 f.Status.AllocatedReplicas = 0 2355 f.Status.Lists = make(map[string]agonesv1.AggregatedListStatus) 2356 f.Status.Lists["gamers"] = agonesv1.AggregatedListStatus{ 2357 AllocatedCount: 0, 2358 AllocatedCapacity: 0, 2359 Count: 15, 2360 Capacity: 30, 2361 } 2362 }), 2363 featureFlags: string(utilruntime.FeatureCountsAndLists) + "=true", 2364 lp: &autoscalingv1.ListPolicy{ 2365 Key: "gamers", 2366 MaxCapacity: 50, 2367 MinCapacity: 1, 2368 BufferSize: intstr.FromString("20%"), 2369 }, 2370 gsList: []agonesv1.GameServer{ 2371 {ObjectMeta: metav1.ObjectMeta{ 2372 Name: "gs1", 2373 Namespace: "default", 2374 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2375 Status: agonesv1.GameServerStatus{ 2376 NodeName: "n1", 2377 Lists: map[string]agonesv1.ListStatus{ 2378 "gamers": { 2379 Values: []string{"1", "2", "3", "4", "5"}, 2380 Capacity: 15, 2381 }}}}, 2382 {ObjectMeta: metav1.ObjectMeta{ 2383 Name: "gs2", 2384 Namespace: "default", 2385 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2386 Status: agonesv1.GameServerStatus{ 2387 NodeName: "n1", 2388 Lists: map[string]agonesv1.ListStatus{ 2389 "gamers": { 2390 Values: []string{"1", "2", "3", "4", "5", "6", "7"}, 2391 Capacity: 10, 2392 }}}}, 2393 {ObjectMeta: metav1.ObjectMeta{ 2394 Name: "gs3", 2395 Namespace: "default", 2396 Labels: map[string]string{"agones.dev/fleet": "fleet-1"}}, 2397 Status: agonesv1.GameServerStatus{ 2398 NodeName: "n1", 2399 Lists: map[string]agonesv1.ListStatus{ 2400 "gamers": { 2401 Values: []string{"1", "2", "3"}, 2402 Capacity: 5, 2403 }}}}, 2404 }, 2405 want: expected{ 2406 replicas: 1, 2407 limited: true, 2408 wantErr: false, 2409 }, 2410 }, 2411 } 2412 2413 utilruntime.FeatureTestMutex.Lock() 2414 defer utilruntime.FeatureTestMutex.Unlock() 2415 2416 for name, tc := range testCases { 2417 t.Run(name, func(t *testing.T) { 2418 err := utilruntime.ParseFeatures(tc.featureFlags) 2419 assert.NoError(t, err) 2420 2421 m := agtesting.NewMocks() 2422 m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 2423 return true, &agonesv1.GameServerList{Items: tc.gsList}, nil 2424 }) 2425 2426 informer := m.AgonesInformerFactory.Agones().V1() 2427 _, cancel := agtesting.StartInformers(m, 2428 informer.GameServers().Informer().HasSynced) 2429 defer cancel() 2430 2431 replicas, limited, err := applyCounterOrListPolicy(nil, tc.lp, tc.fleet, informer.GameServers().Lister().GameServers(tc.fleet.ObjectMeta.Namespace), nc) 2432 2433 if tc.want.wantErr { 2434 assert.NotNil(t, err) 2435 } else { 2436 assert.Nil(t, err) 2437 assert.Equal(t, tc.want.replicas, replicas) 2438 assert.Equal(t, tc.want.limited, limited) 2439 } 2440 }) 2441 } 2442 } 2443 2444 // nolint:dupl // Linter errors on lines are duplicate of TestApplySchedulePolicy 2445 // NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateSchedulePolicy) 2446 func TestApplySchedulePolicy(t *testing.T) { 2447 t.Parallel() 2448 2449 type expected struct { 2450 replicas int32 2451 limited bool 2452 wantErr bool 2453 } 2454 2455 bufferPolicy := autoscalingv1.FleetAutoscalerPolicy{ 2456 Type: autoscalingv1.BufferPolicyType, 2457 Buffer: &autoscalingv1.BufferPolicy{ 2458 BufferSize: intstr.FromInt(1), 2459 MinReplicas: 3, 2460 MaxReplicas: 10, 2461 }, 2462 } 2463 expectedWhenActive := expected{ 2464 replicas: 3, 2465 limited: false, 2466 wantErr: false, 2467 } 2468 expectedWhenInactive := expected{ 2469 replicas: 0, 2470 limited: false, 2471 wantErr: true, 2472 } 2473 2474 testCases := map[string]struct { 2475 featureFlags string 2476 specReplicas int32 2477 statusReplicas int32 2478 statusAllocatedReplicas int32 2479 statusReadyReplicas int32 2480 now time.Time 2481 sp *autoscalingv1.SchedulePolicy 2482 gsList []agonesv1.GameServer 2483 want expected 2484 }{ 2485 "scheduled autoscaler feature flag not enabled": { 2486 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", 2487 sp: &autoscalingv1.SchedulePolicy{}, 2488 want: expected{ 2489 replicas: 0, 2490 limited: false, 2491 wantErr: true, 2492 }, 2493 }, 2494 "no start time": { 2495 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2496 now: mustParseTime("2020-12-26T08:30:00Z"), 2497 sp: &autoscalingv1.SchedulePolicy{ 2498 Between: autoscalingv1.Between{ 2499 End: mustParseMetav1Time("2021-01-01T00:00:00Z"), 2500 }, 2501 ActivePeriod: autoscalingv1.ActivePeriod{ 2502 Timezone: "UTC", 2503 StartCron: "* * * * *", 2504 Duration: "48h", 2505 }, 2506 Policy: bufferPolicy, 2507 }, 2508 want: expectedWhenActive, 2509 }, 2510 "no end time": { 2511 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2512 now: mustParseTime("2021-01-02T00:00:00Z"), 2513 sp: &autoscalingv1.SchedulePolicy{ 2514 Between: autoscalingv1.Between{ 2515 Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), 2516 }, 2517 ActivePeriod: autoscalingv1.ActivePeriod{ 2518 Timezone: "UTC", 2519 StartCron: "* * * * *", 2520 Duration: "1h", 2521 }, 2522 Policy: bufferPolicy, 2523 }, 2524 want: expectedWhenActive, 2525 }, 2526 "no cron time": { 2527 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2528 now: mustParseTime("2021-01-01T0:30:00Z"), 2529 sp: &autoscalingv1.SchedulePolicy{ 2530 Between: autoscalingv1.Between{ 2531 Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), 2532 End: mustParseMetav1Time("2021-01-01T01:00:00Z"), 2533 }, 2534 ActivePeriod: autoscalingv1.ActivePeriod{ 2535 Timezone: "UTC", 2536 Duration: "1h", 2537 }, 2538 Policy: bufferPolicy, 2539 }, 2540 want: expectedWhenActive, 2541 }, 2542 "no duration": { 2543 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2544 now: mustParseTime("2021-01-01T0:30:00Z"), 2545 sp: &autoscalingv1.SchedulePolicy{ 2546 Between: autoscalingv1.Between{ 2547 Start: mustParseMetav1Time("2021-01-01T00:00:00Z"), 2548 End: mustParseMetav1Time("2021-01-01T01:00:00Z"), 2549 }, 2550 ActivePeriod: autoscalingv1.ActivePeriod{ 2551 Timezone: "UTC", 2552 StartCron: "* * * * *", 2553 }, 2554 Policy: bufferPolicy, 2555 }, 2556 want: expectedWhenActive, 2557 }, 2558 "no start time, end time, cron time, duration": { 2559 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2560 now: mustParseTime("2021-01-01T00:00:00Z"), 2561 sp: &autoscalingv1.SchedulePolicy{ 2562 Policy: bufferPolicy, 2563 }, 2564 want: expectedWhenActive, 2565 }, 2566 "daylight saving time start": { 2567 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2568 now: mustParseTime("2021-03-14T02:00:00Z"), 2569 sp: &autoscalingv1.SchedulePolicy{ 2570 Between: autoscalingv1.Between{ 2571 Start: mustParseMetav1Time("2021-03-13T00:00:00Z"), 2572 End: mustParseMetav1Time("2021-03-15T00:00:00Z"), 2573 }, 2574 ActivePeriod: autoscalingv1.ActivePeriod{ 2575 Timezone: "UTC", 2576 StartCron: "* 2 * * *", 2577 Duration: "1h", 2578 }, 2579 Policy: bufferPolicy, 2580 }, 2581 want: expectedWhenActive, 2582 }, 2583 "daylight saving time end": { 2584 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2585 now: mustParseTime("2021-11-07T01:59:59Z"), 2586 sp: &autoscalingv1.SchedulePolicy{ 2587 Between: autoscalingv1.Between{ 2588 Start: mustParseMetav1Time("2021-11-07T00:00:00Z"), 2589 End: mustParseMetav1Time("2021-11-08T00:00:00Z"), 2590 }, 2591 ActivePeriod: autoscalingv1.ActivePeriod{ 2592 Timezone: "UTC", 2593 StartCron: "0 2 * * *", 2594 Duration: "1h", 2595 }, 2596 Policy: bufferPolicy, 2597 }, 2598 want: expectedWhenActive, 2599 }, 2600 "new year": { 2601 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2602 now: mustParseTime("2021-01-01T00:00:00Z"), 2603 sp: &autoscalingv1.SchedulePolicy{ 2604 Between: autoscalingv1.Between{ 2605 Start: mustParseMetav1Time("2020-12-31T24:59:59Z"), 2606 End: mustParseMetav1Time("2021-01-02T00:00:00Z"), 2607 }, 2608 ActivePeriod: autoscalingv1.ActivePeriod{ 2609 Timezone: "UTC", 2610 StartCron: "* 0 * * *", 2611 Duration: "1h", 2612 }, 2613 Policy: bufferPolicy, 2614 }, 2615 want: expectedWhenActive, 2616 }, 2617 "inactive schedule": { 2618 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2619 now: mustParseTime("2023-12-12T03:49:00Z"), 2620 sp: &autoscalingv1.SchedulePolicy{ 2621 Between: autoscalingv1.Between{ 2622 Start: mustParseMetav1Time("2022-12-31T24:59:59Z"), 2623 End: mustParseMetav1Time("2023-03-02T00:00:00Z"), 2624 }, 2625 ActivePeriod: autoscalingv1.ActivePeriod{ 2626 Timezone: "UTC", 2627 StartCron: "* 0 * 3 *", 2628 Duration: "", 2629 }, 2630 Policy: bufferPolicy, 2631 }, 2632 want: expectedWhenInactive, 2633 }, 2634 } 2635 2636 utilruntime.FeatureTestMutex.Lock() 2637 defer utilruntime.FeatureTestMutex.Unlock() 2638 2639 for name, tc := range testCases { 2640 t.Run(name, func(t *testing.T) { 2641 err := utilruntime.ParseFeatures(tc.featureFlags) 2642 assert.NoError(t, err) 2643 2644 ctx := context.Background() 2645 fas, f := defaultFixtures() 2646 m := agtesting.NewMocks() 2647 fasLog := FasLogger{ 2648 fas: fas, 2649 baseLogger: newTestLogger(), 2650 recorder: m.FakeRecorder, 2651 currChainEntry: &fas.Status.LastAppliedPolicy, 2652 } 2653 replicas, limited, err := applySchedulePolicy(ctx, &fasState{}, tc.sp, f, nil, nil, tc.now, &fasLog) 2654 2655 if tc.want.wantErr { 2656 assert.NotNil(t, err) 2657 } else { 2658 assert.Nil(t, err) 2659 assert.Equal(t, tc.want.replicas, replicas) 2660 assert.Equal(t, tc.want.limited, limited) 2661 } 2662 }) 2663 } 2664 } 2665 2666 // nolint:dupl // Linter errors on lines are duplicate of TestApplyChainPolicy 2667 // NOTE: Does not test for the validity of a fleet autoscaler policy (ValidateChainPolicy) 2668 func TestApplyChainPolicy(t *testing.T) { 2669 t.Parallel() 2670 2671 // For Webhook Policy 2672 ts := testServer{} 2673 server := httptest.NewServer(ts) 2674 defer server.Close() 2675 url := webhookURL 2676 2677 type expected struct { 2678 replicas int32 2679 limited bool 2680 wantErr bool 2681 } 2682 2683 scheduleOne := autoscalingv1.ChainEntry{ 2684 ID: "schedule-1", 2685 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2686 Type: autoscalingv1.SchedulePolicyType, 2687 Schedule: &autoscalingv1.SchedulePolicy{ 2688 Between: autoscalingv1.Between{ 2689 Start: mustParseMetav1Time("2024-08-01T10:07:36-06:00"), 2690 }, 2691 ActivePeriod: autoscalingv1.ActivePeriod{ 2692 Timezone: "America/Chicago", 2693 StartCron: "* * * * *", 2694 Duration: "", 2695 }, 2696 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2697 Type: autoscalingv1.BufferPolicyType, 2698 Buffer: &autoscalingv1.BufferPolicy{ 2699 BufferSize: intstr.FromInt(1), 2700 MinReplicas: 10, 2701 MaxReplicas: 10, 2702 }, 2703 }, 2704 }, 2705 }, 2706 } 2707 scheduleTwo := autoscalingv1.ChainEntry{ 2708 ID: "schedule-2", 2709 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2710 Type: autoscalingv1.SchedulePolicyType, 2711 Schedule: &autoscalingv1.SchedulePolicy{ 2712 Between: autoscalingv1.Between{ 2713 End: mustParseMetav1Time("2021-01-02T4:53:00-05:00"), 2714 }, 2715 ActivePeriod: autoscalingv1.ActivePeriod{ 2716 Timezone: "America/New_York", 2717 StartCron: "0 1 3 * *", 2718 Duration: "", 2719 }, 2720 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2721 Type: autoscalingv1.BufferPolicyType, 2722 Buffer: &autoscalingv1.BufferPolicy{ 2723 BufferSize: intstr.FromInt(1), 2724 MinReplicas: 3, 2725 MaxReplicas: 10, 2726 }, 2727 }, 2728 }, 2729 }, 2730 } 2731 webhookEntry := autoscalingv1.ChainEntry{ 2732 ID: "webhook policy", 2733 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2734 Type: autoscalingv1.WebhookPolicyType, 2735 Webhook: &autoscalingv1.URLConfiguration{ 2736 Service: &admregv1.ServiceReference{ 2737 Name: "service1", 2738 Namespace: "default", 2739 Path: &url, 2740 }, 2741 CABundle: []byte("invalid-value"), 2742 }, 2743 }, 2744 } 2745 defaultEntry := autoscalingv1.ChainEntry{ 2746 ID: "default", 2747 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2748 Type: autoscalingv1.BufferPolicyType, 2749 Buffer: &autoscalingv1.BufferPolicy{ 2750 BufferSize: intstr.FromInt(1), 2751 MinReplicas: 6, 2752 MaxReplicas: 10, 2753 }, 2754 }, 2755 } 2756 2757 testCases := map[string]struct { 2758 fleet *agonesv1.Fleet 2759 featureFlags string 2760 specReplicas int32 2761 statusReplicas int32 2762 statusAllocatedReplicas int32 2763 statusReadyReplicas int32 2764 now time.Time 2765 cp *autoscalingv1.ChainPolicy 2766 gsList []agonesv1.GameServer 2767 want expected 2768 }{ 2769 "scheduled autoscaler feature flag not enabled": { 2770 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=false", 2771 cp: &autoscalingv1.ChainPolicy{}, 2772 want: expected{ 2773 replicas: 0, 2774 limited: false, 2775 wantErr: true, 2776 }, 2777 }, 2778 "default policy": { 2779 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2780 cp: &autoscalingv1.ChainPolicy{defaultEntry}, 2781 want: expected{ 2782 replicas: 6, 2783 limited: true, 2784 wantErr: false, 2785 }, 2786 }, 2787 "one invalid webhook policy, one default (fallthrough)": { 2788 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2789 cp: &autoscalingv1.ChainPolicy{webhookEntry, defaultEntry}, 2790 want: expected{ 2791 replicas: 6, 2792 limited: true, 2793 wantErr: false, 2794 }, 2795 }, 2796 "two inactive schedule entries, no default (fall off chain)": { 2797 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2798 now: mustParseTime("2021-01-01T0:30:00Z"), 2799 cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleOne}, 2800 want: expected{ 2801 replicas: 5, 2802 limited: false, 2803 wantErr: true, 2804 }, 2805 }, 2806 "two inactive schedules entries, one default (fallthrough)": { 2807 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2808 now: mustParseTime("2021-11-05T5:30:00Z"), 2809 cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleTwo, defaultEntry}, 2810 want: expected{ 2811 replicas: 6, 2812 limited: true, 2813 wantErr: false, 2814 }, 2815 }, 2816 "two overlapping/active schedule entries, schedule-1 applied": { 2817 featureFlags: string(utilruntime.FeatureScheduledAutoscaler) + "=true", 2818 now: mustParseTime("2024-08-01T10:07:36-06:00"), 2819 cp: &autoscalingv1.ChainPolicy{scheduleOne, scheduleTwo}, 2820 want: expected{ 2821 replicas: 10, 2822 limited: true, 2823 wantErr: false, 2824 }, 2825 }, 2826 } 2827 2828 utilruntime.FeatureTestMutex.Lock() 2829 defer utilruntime.FeatureTestMutex.Unlock() 2830 2831 for name, tc := range testCases { 2832 t.Run(name, func(t *testing.T) { 2833 err := utilruntime.ParseFeatures(tc.featureFlags) 2834 assert.NoError(t, err) 2835 2836 ctx := context.Background() 2837 fas, f := defaultFixtures() 2838 m := agtesting.NewMocks() 2839 fasLog := FasLogger{ 2840 fas: fas, 2841 baseLogger: newTestLogger(), 2842 recorder: m.FakeRecorder, 2843 currChainEntry: &fas.Status.LastAppliedPolicy, 2844 } 2845 replicas, limited, err := applyChainPolicy(ctx, &fasState{}, *tc.cp, f, nil, nil, tc.now, &fasLog) 2846 2847 if tc.want.wantErr { 2848 assert.NotNil(t, err) 2849 } else { 2850 assert.Nil(t, err) 2851 assert.Equal(t, tc.want.replicas, replicas) 2852 assert.Equal(t, tc.want.limited, limited) 2853 } 2854 }) 2855 } 2856 } 2857 2858 // Parse a time string and return a metav1.Time 2859 func mustParseMetav1Time(timeStr string) metav1.Time { 2860 t, _ := time.Parse(time.RFC3339, timeStr) 2861 return metav1.NewTime(t) 2862 } 2863 2864 // Parse a time string and return a time.Time 2865 func mustParseTime(timeStr string) time.Time { 2866 t, _ := time.Parse(time.RFC3339, timeStr) 2867 return t 2868 } 2869 2870 // Create a fake test logger using logr 2871 func newTestLogger() *logrus.Entry { 2872 return utilruntime.NewLoggerWithType(testServer{}) 2873 }