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  }