agones.dev/agones@v1.53.0/pkg/fleetautoscalers/fleetautoscalers_test.go (about)

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