agones.dev/agones@v1.54.0/pkg/apis/autoscaling/v1/fleetautoscaler_test.go (about)

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package v1
    16  
    17  import (
    18  	"testing"
    19  	"time"
    20  
    21  	"agones.dev/agones/pkg/util/runtime"
    22  	"github.com/stretchr/testify/assert"
    23  	admregv1 "k8s.io/api/admissionregistration/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/util/intstr"
    26  )
    27  
    28  func TestFleetAutoscalerValidateUpdate(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	t.Run("bad buffer size", func(t *testing.T) {
    32  		fas := defaultFixture()
    33  		fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(0)
    34  		causes := fas.Validate()
    35  
    36  		assert.Len(t, causes, 1)
    37  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
    38  	})
    39  
    40  	t.Run("bad min replicas", func(t *testing.T) {
    41  		fas := defaultFixture()
    42  		fas.Spec.Policy.Buffer.MinReplicas = 2
    43  
    44  		causes := fas.Validate()
    45  
    46  		assert.Len(t, causes, 1)
    47  		assert.Equal(t, "spec.policy.buffer.minReplicas", causes[0].Field)
    48  	})
    49  
    50  	t.Run("bad max replicas", func(t *testing.T) {
    51  		fas := defaultFixture()
    52  		fas.Spec.Policy.Buffer.MaxReplicas = 2
    53  		causes := fas.Validate()
    54  
    55  		assert.Len(t, causes, 1)
    56  		assert.Equal(t, "spec.policy.buffer.maxReplicas", causes[0].Field)
    57  	})
    58  
    59  	t.Run("minReplicas > maxReplicas", func(t *testing.T) {
    60  		fas := defaultFixture()
    61  		fas.Spec.Policy.Buffer.MinReplicas = 20
    62  		causes := fas.Validate()
    63  
    64  		assert.Len(t, causes, 1)
    65  		assert.Equal(t, "spec.policy.buffer.minReplicas", causes[0].Field)
    66  	})
    67  
    68  	t.Run("bufferSize good percent", func(t *testing.T) {
    69  		fas := defaultFixture()
    70  		fas.Spec.Policy.Buffer.MinReplicas = 1
    71  		fas.Spec.Policy.Buffer.BufferSize = intstr.FromString("20%")
    72  		causes := fas.Validate()
    73  
    74  		assert.Len(t, causes, 0)
    75  	})
    76  
    77  	t.Run("bufferSize bad percent", func(t *testing.T) {
    78  		fas := defaultFixture()
    79  		fas.Spec.Policy.Buffer.MinReplicas = 1
    80  
    81  		fasCopy := fas.DeepCopy()
    82  		fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("120%")
    83  		causes := fasCopy.Validate()
    84  		assert.Len(t, causes, 1)
    85  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
    86  
    87  		fasCopy = fas.DeepCopy()
    88  		fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("0%")
    89  		causes = fasCopy.Validate()
    90  		assert.Len(t, causes, 1)
    91  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
    92  
    93  		fasCopy = fas.DeepCopy()
    94  		fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("-10%")
    95  		causes = fasCopy.Validate()
    96  		assert.Len(t, causes, 1)
    97  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
    98  		fasCopy = fas.DeepCopy()
    99  
   100  		fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("notgood")
   101  		causes = fasCopy.Validate()
   102  		assert.Len(t, causes, 1)
   103  		assert.Equal(t, "spec.policy.buffer.bufferSize", causes[0].Field)
   104  	})
   105  
   106  	t.Run("bad min replicas with percentage value of bufferSize", func(t *testing.T) {
   107  		fas := defaultFixture()
   108  		fas.Spec.Policy.Buffer.BufferSize = intstr.FromString("10%")
   109  		fas.Spec.Policy.Buffer.MinReplicas = 0
   110  
   111  		causes := fas.Validate()
   112  
   113  		assert.Len(t, causes, 1)
   114  		assert.Equal(t, "spec.policy.buffer.minReplicas", causes[0].Field)
   115  	})
   116  
   117  	t.Run("bad sync interval seconds", func(t *testing.T) {
   118  
   119  		fas := defaultFixture()
   120  		fas.Spec.Sync.FixedInterval.Seconds = 0
   121  
   122  		causes := fas.Validate()
   123  
   124  		assert.Len(t, causes, 1)
   125  		assert.Equal(t, "spec.sync.fixedInterval.seconds", causes[0].Field)
   126  	})
   127  }
   128  func TestFleetAutoscalerWebhookValidateUpdate(t *testing.T) {
   129  	t.Parallel()
   130  
   131  	t.Run("good service value", func(t *testing.T) {
   132  		fas := webhookFixture()
   133  		causes := fas.Validate()
   134  
   135  		assert.Len(t, causes, 0)
   136  	})
   137  
   138  	t.Run("good url value", func(t *testing.T) {
   139  		fas := webhookFixture()
   140  		url := "http://good.example.com"
   141  		fas.Spec.Policy.Webhook.URL = &url
   142  		fas.Spec.Policy.Webhook.Service = nil
   143  		causes := fas.Validate()
   144  
   145  		assert.Len(t, causes, 0)
   146  	})
   147  
   148  	t.Run("bad URL and service value", func(t *testing.T) {
   149  		fas := webhookFixture()
   150  		fas.Spec.Policy.Webhook.URL = nil
   151  		fas.Spec.Policy.Webhook.Service = nil
   152  		causes := fas.Validate()
   153  
   154  		assert.Len(t, causes, 1)
   155  		assert.Equal(t, "spec.policy.webhook", causes[0].Field)
   156  	})
   157  
   158  	t.Run("both URL and service value are used - fail", func(t *testing.T) {
   159  
   160  		fas := webhookFixture()
   161  		url := "123"
   162  		fas.Spec.Policy.Webhook.URL = &url
   163  
   164  		causes := fas.Validate()
   165  
   166  		assert.Len(t, causes, 1)
   167  		assert.Equal(t, "spec.policy.webhook.url", causes[0].Field)
   168  	})
   169  
   170  	goodCaBundle := "\n-----BEGIN CERTIFICATE-----\nMIIDXjCCAkYCCQDvT9MAXwnuqDANBgkqhkiG9w0BAQsFADBxMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEP\nMA0GA1UECgwGQWdvbmVzMQ8wDQYDVQQLDAZBZ29uZXMxEzARBgNVBAMMCmFnb25l\ncy5kZXYwHhcNMTkwMTAzMTEwNTA0WhcNMjExMDIzMTEwNTA0WjBxMQswCQYDVQQG\nEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmll\ndzEPMA0GA1UECgwGQWdvbmVzMQ8wDQYDVQQLDAZBZ29uZXMxEzARBgNVBAMMCmFn\nb25lcy5kZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFwohJp3xK\n4iORkJXNO2KEkdrVYK7xpXTrPvZqLzoyMBOXi9b+lOKILUPaKtZ33GIwola31bHp\ni7V97vh3irIQVpap6uncesRTX0qk5Y70f7T6lByMKDsxi5ddea3ztAftH+PMYSLn\nE7H9276R1lvX8HZ0E2T4ea63PcVcTldw74ueEQr7HFMVucO+hHjgNJXDsWFUNppv\nxqWOvlIEDRdQzB1UYd13orqX0t514Ikp5Y3oNigXftDH+lZPlrWGsknMIDWr4DKP\n7NB1BZMfLFu/HXTGI9dK5Zc4T4GG4DBZqlgDPdzAXSBUT9cRQvbLrZ5+tUjOZK5E\nzEEIqyUo1+QdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABgtnWaWIDFCbKvhD8cF\nd5fvARFJcRl4dcChoqANeUXK4iNiCEPiDJb4xDGrLSVOeQ2IMbghqCwJfH93aqTr\n9kFQPvYbCt10TPQpmmh2//QjWGc7lxniFWR8pAVYdCGHqIAMvW2V2177quHsqc2I\nNTXyEUus0SDHLK8swLQxoCVw4fSq+kjMFW/3zOvMfh13rZH7Lo0gQyAUcuHM5U7g\nbhCZ3yVkDYpPxVv2vL0eyWUdLrQjYXyY7MWHPXvDozi3CtuBZlp6ulgeubi6jhHE\nIzuOM3qiLMJ/KG8MlIgGCwSX/x0vfO0/LtkZM7P1+yptSr/Se5QiZMtmpxC+DDWJ\n2xw=\n-----END CERTIFICATE-----"
   171  	t.Run("good url and CABundle value", func(t *testing.T) {
   172  		fas := webhookFixture()
   173  		url := "https://good.example.com"
   174  		fas.Spec.Policy.Webhook.URL = &url
   175  		fas.Spec.Policy.Webhook.Service = nil
   176  		fas.Spec.Policy.Webhook.CABundle = []byte(goodCaBundle)
   177  
   178  		causes := fas.Validate()
   179  		assert.Len(t, causes, 0)
   180  	})
   181  
   182  	t.Run("https url and invalid CABundle value", func(t *testing.T) {
   183  		fas := webhookFixture()
   184  		url := "https://bad.example.com"
   185  		fas.Spec.Policy.Webhook.URL = &url
   186  		fas.Spec.Policy.Webhook.Service = nil
   187  		fas.Spec.Policy.Webhook.CABundle = []byte("SomeInvalidCABundle")
   188  
   189  		causes := fas.Validate()
   190  		assert.Len(t, causes, 1)
   191  		assert.Equal(t, "spec.policy.webhook.caBundle", causes[0].Field)
   192  	})
   193  
   194  	t.Run("https url and missing CABundle value", func(t *testing.T) {
   195  		fas := webhookFixture()
   196  		url := "https://bad.example.com"
   197  		fas.Spec.Policy.Webhook.URL = &url
   198  		fas.Spec.Policy.Webhook.Service = nil
   199  		fas.Spec.Policy.Webhook.CABundle = nil
   200  
   201  		causes := fas.Validate()
   202  		assert.Len(t, causes, 0)
   203  	})
   204  
   205  	t.Run("bad url value", func(t *testing.T) {
   206  		fas := webhookFixture()
   207  		url := "http:/bad.example.com%"
   208  		fas.Spec.Policy.Webhook.URL = &url
   209  		fas.Spec.Policy.Webhook.Service = nil
   210  		fas.Spec.Policy.Webhook.CABundle = []byte(goodCaBundle)
   211  
   212  		causes := fas.Validate()
   213  		assert.Len(t, causes, 1)
   214  		assert.Equal(t, "spec.policy.webhook.url", causes[0].Field)
   215  	})
   216  
   217  }
   218  
   219  // nolint:dupl  // Linter errors on lines are duplicate of TestFleetAutoscalerListValidateUpdate
   220  func TestFleetAutoscalerCounterValidateUpdate(t *testing.T) {
   221  	t.Parallel()
   222  
   223  	modifiedFAS := func(f func(*FleetAutoscalerPolicy)) *FleetAutoscaler {
   224  		fas := counterFixture()
   225  		f(&fas.Spec.Policy)
   226  		return fas
   227  	}
   228  
   229  	testCases := map[string]struct {
   230  		fas          *FleetAutoscaler
   231  		featureFlags string
   232  		wantLength   int
   233  		wantField    string
   234  	}{
   235  		"feature gate not turned on": {
   236  			fas:          counterFixture(),
   237  			featureFlags: string(runtime.FeatureCountsAndLists) + "=false",
   238  			wantLength:   1,
   239  			wantField:    "spec.policy.counter",
   240  		},
   241  		"nil parameters": {
   242  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   243  				fap.Counter = nil
   244  			}),
   245  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   246  			wantLength:   1,
   247  			wantField:    "spec.policy.counter",
   248  		},
   249  		"minCapacity size too large": {
   250  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   251  				fap.Counter.MinCapacity = int64(11)
   252  			}),
   253  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   254  			wantLength:   1,
   255  			wantField:    "spec.policy.counter.minCapacity",
   256  		},
   257  		"bufferSize size too small": {
   258  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   259  				fap.Counter.BufferSize = intstr.FromInt(0)
   260  			}),
   261  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   262  			wantLength:   1,
   263  			wantField:    "spec.policy.counter.bufferSize",
   264  		},
   265  		"maxCapacity size too small": {
   266  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   267  				fap.Counter.MaxCapacity = int64(4)
   268  			}),
   269  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   270  			wantLength:   1,
   271  			wantField:    "spec.policy.counter.maxCapacity",
   272  		},
   273  		"minCapacity size too small": {
   274  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   275  				fap.Counter.MinCapacity = int64(4)
   276  			}),
   277  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   278  			wantLength:   1,
   279  			wantField:    "spec.policy.counter.minCapacity",
   280  		},
   281  		"bufferSize percentage OK": {
   282  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   283  				fap.Counter.BufferSize.Type = intstr.String
   284  				fap.Counter.BufferSize = intstr.FromString("99%")
   285  				fap.Counter.MinCapacity = 10
   286  			}),
   287  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   288  			wantLength:   0,
   289  		},
   290  		"bufferSize percentage can't parse": {
   291  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   292  				fap.Counter.BufferSize.Type = intstr.String
   293  				fap.Counter.BufferSize = intstr.FromString("99.0%")
   294  				fap.Counter.MinCapacity = 1
   295  			}),
   296  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   297  			wantLength:   1,
   298  			wantField:    "spec.policy.counter.bufferSize",
   299  		},
   300  		"bufferSize percentage and MinCapacity too small": {
   301  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   302  				fap.Counter.BufferSize.Type = intstr.String
   303  				fap.Counter.BufferSize = intstr.FromString("0%")
   304  			}),
   305  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   306  			wantLength:   2,
   307  			wantField:    "spec.policy.counter.bufferSize",
   308  		},
   309  		"bufferSize percentage too large": {
   310  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   311  				fap.Counter.BufferSize.Type = intstr.String
   312  				fap.Counter.BufferSize = intstr.FromString("100%")
   313  				fap.Counter.MinCapacity = 10
   314  			}),
   315  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   316  			wantLength:   1,
   317  			wantField:    "spec.policy.counter.bufferSize",
   318  		},
   319  	}
   320  
   321  	runtime.FeatureTestMutex.Lock()
   322  	defer runtime.FeatureTestMutex.Unlock()
   323  
   324  	for name, tc := range testCases {
   325  		t.Run(name, func(t *testing.T) {
   326  			err := runtime.ParseFeatures(tc.featureFlags)
   327  			assert.NoError(t, err)
   328  
   329  			causes := tc.fas.Validate()
   330  
   331  			assert.Len(t, causes, tc.wantLength)
   332  			if tc.wantLength > 0 {
   333  				assert.Equal(t, tc.wantField, causes[0].Field)
   334  			}
   335  		})
   336  	}
   337  }
   338  
   339  // nolint:dupl  // Linter errors on lines are duplicate of TestFleetAutoscalerCounterValidateUpdate
   340  func TestFleetAutoscalerListValidateUpdate(t *testing.T) {
   341  	t.Parallel()
   342  
   343  	modifiedFAS := func(f func(*FleetAutoscalerPolicy)) *FleetAutoscaler {
   344  		fas := listFixture()
   345  		f(&fas.Spec.Policy)
   346  		return fas
   347  	}
   348  
   349  	testCases := map[string]struct {
   350  		fas          *FleetAutoscaler
   351  		featureFlags string
   352  		wantLength   int
   353  		wantField    string
   354  	}{
   355  		"feature gate not turned on": {
   356  			fas:          listFixture(),
   357  			featureFlags: string(runtime.FeatureCountsAndLists) + "=false",
   358  			wantLength:   1,
   359  			wantField:    "spec.policy.list",
   360  		},
   361  		"nil parameters": {
   362  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   363  				fap.List = nil
   364  			}),
   365  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   366  			wantLength:   1,
   367  			wantField:    "spec.policy.list",
   368  		},
   369  		"minCapacity size too large": {
   370  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   371  				fap.List.MinCapacity = int64(11)
   372  			}),
   373  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   374  			wantLength:   1,
   375  			wantField:    "spec.policy.list.minCapacity",
   376  		},
   377  		"bufferSize size too small": {
   378  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   379  				fap.List.BufferSize = intstr.FromInt(0)
   380  			}),
   381  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   382  			wantLength:   1,
   383  			wantField:    "spec.policy.list.bufferSize",
   384  		},
   385  		"maxCapacity size too small": {
   386  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   387  				fap.List.MaxCapacity = int64(4)
   388  			}),
   389  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   390  			wantLength:   1,
   391  			wantField:    "spec.policy.list.maxCapacity",
   392  		},
   393  		"minCapacity size too small": {
   394  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   395  				fap.List.MinCapacity = int64(4)
   396  			}),
   397  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   398  			wantLength:   1,
   399  			wantField:    "spec.policy.list.minCapacity",
   400  		},
   401  		"bufferSize percentage OK": {
   402  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   403  				fap.List.BufferSize.Type = intstr.String
   404  				fap.List.BufferSize = intstr.FromString("99%")
   405  				fap.List.MinCapacity = 1
   406  			}),
   407  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   408  			wantLength:   0,
   409  		},
   410  		"bufferSize percentage can't parse": {
   411  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   412  				fap.List.BufferSize.Type = intstr.String
   413  				fap.List.BufferSize = intstr.FromString("99.0%")
   414  				fap.List.MinCapacity = 1
   415  			}),
   416  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   417  			wantLength:   1,
   418  			wantField:    "spec.policy.list.bufferSize",
   419  		},
   420  		"bufferSize percentage and MinCapacity too small": {
   421  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   422  				fap.List.BufferSize.Type = intstr.String
   423  				fap.List.BufferSize = intstr.FromString("0%")
   424  			}),
   425  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   426  			wantLength:   2,
   427  			wantField:    "spec.policy.list.bufferSize",
   428  		},
   429  		"bufferSize percentage too large": {
   430  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   431  				fap.List.BufferSize.Type = intstr.String
   432  				fap.List.BufferSize = intstr.FromString("100%")
   433  				fap.List.MinCapacity = 1
   434  			}),
   435  			featureFlags: string(runtime.FeatureCountsAndLists) + "=true",
   436  			wantLength:   1,
   437  			wantField:    "spec.policy.list.bufferSize",
   438  		},
   439  	}
   440  
   441  	runtime.FeatureTestMutex.Lock()
   442  	defer runtime.FeatureTestMutex.Unlock()
   443  
   444  	for name, tc := range testCases {
   445  		t.Run(name, func(t *testing.T) {
   446  			err := runtime.ParseFeatures(tc.featureFlags)
   447  			assert.NoError(t, err)
   448  
   449  			causes := tc.fas.Validate()
   450  
   451  			assert.Len(t, causes, tc.wantLength)
   452  			if tc.wantLength > 0 {
   453  				assert.Equal(t, tc.wantField, causes[0].Field)
   454  			}
   455  		})
   456  	}
   457  }
   458  
   459  func TestFleetAutoscalerScheduleValidateUpdate(t *testing.T) {
   460  	t.Parallel()
   461  
   462  	modifiedFAS := func(f func(*FleetAutoscalerPolicy)) *FleetAutoscaler {
   463  		fas := scheduleFixture()
   464  		f(&fas.Spec.Policy)
   465  		return fas
   466  	}
   467  
   468  	testCases := map[string]struct {
   469  		fas          *FleetAutoscaler
   470  		featureFlags string
   471  		wantLength   int
   472  		wantField    string
   473  	}{
   474  		"feature gate not turned on": {
   475  			fas:          scheduleFixture(),
   476  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=false",
   477  			wantLength:   1,
   478  			wantField:    "spec.policy.schedule",
   479  		},
   480  		"end time before current time": {
   481  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   482  				fap.Schedule.Between.End = mustParseDate("2024-07-03T15:59:59Z")
   483  			}),
   484  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   485  			wantLength:   1,
   486  			wantField:    "spec.policy.schedule.between.end",
   487  		},
   488  		"end time before start time": {
   489  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   490  				fap.Schedule.Between.Start = mustParseDate("3999-06-15T15:59:59Z")
   491  				fap.Schedule.Between.End = mustParseDate("3999-05-15T15:59:59Z")
   492  			}),
   493  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   494  			wantLength:   1,
   495  			wantField:    "spec.policy.schedule.between",
   496  		},
   497  		"invalid cron format": {
   498  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   499  				fap.Schedule.ActivePeriod.StartCron = "* * * * * * * *"
   500  			}),
   501  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   502  			wantLength:   1,
   503  			wantField:    "spec.policy.schedule.activePeriod.startCron",
   504  		},
   505  		"invalid cron format with tz specification": {
   506  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   507  				fap.Schedule.ActivePeriod.StartCron = "CRON_TZ=America/New_York * * * * *"
   508  			}),
   509  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   510  			wantLength:   1,
   511  			wantField:    "spec.policy.schedule.activePeriod.startCron",
   512  		},
   513  	}
   514  
   515  	runtime.FeatureTestMutex.Lock()
   516  	defer runtime.FeatureTestMutex.Unlock()
   517  
   518  	for name, tc := range testCases {
   519  		t.Run(name, func(t *testing.T) {
   520  			err := runtime.ParseFeatures(tc.featureFlags)
   521  			assert.NoError(t, err)
   522  
   523  			causes := tc.fas.Validate()
   524  
   525  			assert.Len(t, causes, tc.wantLength)
   526  			if tc.wantLength > 0 {
   527  				assert.Equal(t, tc.wantField, causes[0].Field)
   528  			}
   529  		})
   530  	}
   531  }
   532  
   533  func TestFleetAutoscalerChainValidateUpdate(t *testing.T) {
   534  	t.Parallel()
   535  
   536  	modifiedFAS := func(f func(*FleetAutoscalerPolicy)) *FleetAutoscaler {
   537  		fas := chainFixture()
   538  		f(&fas.Spec.Policy)
   539  		return fas
   540  	}
   541  
   542  	testCases := map[string]struct {
   543  		fas          *FleetAutoscaler
   544  		featureFlags string
   545  		wantLength   int
   546  		wantField    string
   547  	}{
   548  		"feature gate not turned on": {
   549  			fas:          chainFixture(),
   550  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=false",
   551  			wantLength:   1,
   552  			wantField:    "spec.policy.chain",
   553  		},
   554  		"duplicate id": {
   555  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   556  				fap.Chain[1].ID = "weekends"
   557  			}),
   558  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   559  			wantLength:   1,
   560  			wantField:    "spec.policy.chain",
   561  		},
   562  		"missing policy": {
   563  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   564  				fap.Chain[0].FleetAutoscalerPolicy = FleetAutoscalerPolicy{}
   565  			}),
   566  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   567  			wantLength:   1,
   568  			wantField:    "spec.policy.chain[0]",
   569  		},
   570  		"nested chain policy not allowed": {
   571  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   572  				fap.Chain[1].FleetAutoscalerPolicy = FleetAutoscalerPolicy{
   573  					Type: ChainPolicyType,
   574  					Chain: ChainPolicy{
   575  						{
   576  							FleetAutoscalerPolicy: FleetAutoscalerPolicy{
   577  								Type: BufferPolicyType,
   578  								Buffer: &BufferPolicy{
   579  									BufferSize:  intstr.FromInt(5),
   580  									MaxReplicas: 10,
   581  								},
   582  							},
   583  						},
   584  					},
   585  				}
   586  			}),
   587  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   588  			wantLength:   1,
   589  			wantField:    "spec.policy.chain[1]",
   590  		},
   591  		"invalid nested policy format": {
   592  			fas: modifiedFAS(func(fap *FleetAutoscalerPolicy) {
   593  				fap.Chain[1].FleetAutoscalerPolicy.Buffer.MinReplicas = 20
   594  			}),
   595  			featureFlags: string(runtime.FeatureScheduledAutoscaler) + "=true",
   596  			wantLength:   1,
   597  			wantField:    "spec.policy.chain[1].policy.buffer.minReplicas",
   598  		},
   599  	}
   600  
   601  	runtime.FeatureTestMutex.Lock()
   602  	defer runtime.FeatureTestMutex.Unlock()
   603  
   604  	for name, tc := range testCases {
   605  		t.Run(name, func(t *testing.T) {
   606  			err := runtime.ParseFeatures(tc.featureFlags)
   607  			assert.NoError(t, err)
   608  
   609  			causes := tc.fas.Validate()
   610  
   611  			assert.Len(t, causes, tc.wantLength)
   612  			if tc.wantLength > 0 {
   613  				assert.Equal(t, tc.wantField, causes[0].Field)
   614  			}
   615  		})
   616  	}
   617  }
   618  
   619  func TestFleetAutoscalerWasmValidateUpdate(t *testing.T) {
   620  	t.Parallel()
   621  
   622  	testCases := map[string]struct {
   623  		fas          *FleetAutoscaler
   624  		featureFlags string
   625  		wantLength   int
   626  		wantField    string
   627  	}{
   628  		"valid": {
   629  			fas:          wasmFixture(),
   630  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=true",
   631  			wantLength:   0,
   632  		},
   633  		"feature gate not turned on": {
   634  			fas:          wasmFixture(),
   635  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=false",
   636  			wantLength:   1,
   637  			wantField:    "spec.policy.wasm",
   638  		},
   639  		"nil wasm policy": {
   640  			fas: func() *FleetAutoscaler {
   641  				fas := wasmFixture()
   642  				fas.Spec.Policy.Wasm = nil
   643  				return fas
   644  			}(),
   645  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=true",
   646  			wantLength:   1,
   647  			wantField:    "spec.policy.wasm",
   648  		},
   649  		"nil from.url": {
   650  			fas: func() *FleetAutoscaler {
   651  				fas := wasmFixture()
   652  				fas.Spec.Policy.Wasm.From.URL = nil
   653  				return fas
   654  			}(),
   655  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=true",
   656  			wantLength:   1,
   657  			wantField:    "spec.policy.wasm.from",
   658  		},
   659  		"invalid url": {
   660  			fas: func() *FleetAutoscaler {
   661  				fas := wasmFixture()
   662  				badURL := "://invalid-url"
   663  				fas.Spec.Policy.Wasm.From.URL.URL = &badURL
   664  				return fas
   665  			}(),
   666  			featureFlags: string(runtime.FeatureWasmAutoscaler) + "=true",
   667  			wantLength:   1,
   668  			wantField:    "spec.policy.wasm.from.url.url",
   669  		},
   670  	}
   671  
   672  	runtime.FeatureTestMutex.Lock()
   673  	defer runtime.FeatureTestMutex.Unlock()
   674  
   675  	for name, tc := range testCases {
   676  		t.Run(name, func(t *testing.T) {
   677  			err := runtime.ParseFeatures(tc.featureFlags)
   678  			assert.NoError(t, err)
   679  
   680  			causes := tc.fas.Validate()
   681  
   682  			assert.Len(t, causes, tc.wantLength)
   683  			if tc.wantLength > 0 && len(causes) > 0 {
   684  				assert.Equal(t, tc.wantField, causes[0].Field)
   685  			}
   686  		})
   687  	}
   688  }
   689  
   690  func TestFleetAutoscalerApplyDefaults(t *testing.T) {
   691  	fas := &FleetAutoscaler{}
   692  
   693  	// gate
   694  	assert.Nil(t, fas.Spec.Sync)
   695  
   696  	fas.ApplyDefaults()
   697  	assert.NotNil(t, fas.Spec.Sync)
   698  	assert.Equal(t, FixedIntervalSyncType, fas.Spec.Sync.Type)
   699  	assert.Equal(t, defaultIntervalSyncSeconds, fas.Spec.Sync.FixedInterval.Seconds)
   700  
   701  	// Test apply defaults is idempotent -- calling ApplyDefaults more than one time does not change the original result.
   702  	fas.ApplyDefaults()
   703  	assert.NotNil(t, fas.Spec.Sync)
   704  	assert.Equal(t, FixedIntervalSyncType, fas.Spec.Sync.Type)
   705  	assert.Equal(t, defaultIntervalSyncSeconds, fas.Spec.Sync.FixedInterval.Seconds)
   706  }
   707  
   708  func mustParseDate(timeStr string) metav1.Time {
   709  	t, _ := time.Parse(time.RFC3339, timeStr)
   710  	return metav1.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
   711  }
   712  
   713  func defaultFixture() *FleetAutoscaler {
   714  	return customFixture(BufferPolicyType)
   715  }
   716  
   717  func webhookFixture() *FleetAutoscaler {
   718  	return customFixture(WebhookPolicyType)
   719  }
   720  
   721  func counterFixture() *FleetAutoscaler {
   722  	return customFixture(CounterPolicyType)
   723  }
   724  
   725  func listFixture() *FleetAutoscaler {
   726  	return customFixture(ListPolicyType)
   727  }
   728  
   729  func scheduleFixture() *FleetAutoscaler {
   730  	return customFixture(SchedulePolicyType)
   731  }
   732  
   733  func chainFixture() *FleetAutoscaler {
   734  	return customFixture(ChainPolicyType)
   735  }
   736  
   737  func wasmFixture() *FleetAutoscaler {
   738  	return customFixture(WasmPolicyType)
   739  }
   740  
   741  func customFixture(t FleetAutoscalerPolicyType) *FleetAutoscaler {
   742  
   743  	res := &FleetAutoscaler{
   744  		ObjectMeta: metav1.ObjectMeta{Name: "test"},
   745  		Spec: FleetAutoscalerSpec{
   746  			FleetName: "testing",
   747  			Policy: FleetAutoscalerPolicy{
   748  				Type: BufferPolicyType,
   749  				Buffer: &BufferPolicy{
   750  					BufferSize:  intstr.FromInt(5),
   751  					MaxReplicas: 10,
   752  				},
   753  			},
   754  			Sync: &FleetAutoscalerSync{
   755  				Type: FixedIntervalSyncType,
   756  				FixedInterval: FixedIntervalSync{
   757  					Seconds: 30,
   758  				},
   759  			},
   760  		},
   761  	}
   762  	switch t {
   763  	case BufferPolicyType:
   764  	case WebhookPolicyType:
   765  		res.Spec.Policy.Type = WebhookPolicyType
   766  		res.Spec.Policy.Buffer = nil
   767  		url := "/scale"
   768  		res.Spec.Policy.Webhook = &URLConfiguration{
   769  			Service: &admregv1.ServiceReference{
   770  				Name:      "service1",
   771  				Namespace: "default",
   772  				Path:      &url,
   773  			},
   774  		}
   775  	case CounterPolicyType:
   776  		res.Spec.Policy.Type = CounterPolicyType
   777  		res.Spec.Policy.Buffer = nil
   778  		res.Spec.Policy.Counter = &CounterPolicy{
   779  			BufferSize:  intstr.FromInt(5),
   780  			MaxCapacity: 10,
   781  		}
   782  	case ListPolicyType:
   783  		res.Spec.Policy.Type = ListPolicyType
   784  		res.Spec.Policy.Buffer = nil
   785  		res.Spec.Policy.List = &ListPolicy{
   786  			BufferSize:  intstr.FromInt(5),
   787  			MaxCapacity: 10,
   788  		}
   789  	case SchedulePolicyType:
   790  		res.Spec.Policy.Type = SchedulePolicyType
   791  		res.Spec.Policy.Buffer = nil
   792  		res.Spec.Policy.Schedule = &SchedulePolicy{
   793  			Between: Between{
   794  				Start: mustParseDate("2024-07-01T15:59:59Z"),
   795  				End:   mustParseDate("9999-07-03T15:59:59Z"),
   796  			},
   797  			ActivePeriod: ActivePeriod{
   798  				Timezone:  "",
   799  				StartCron: "* * * * 1-5",
   800  				Duration:  "",
   801  			},
   802  			Policy: FleetAutoscalerPolicy{
   803  				Type: BufferPolicyType,
   804  				Buffer: &BufferPolicy{
   805  					BufferSize:  intstr.FromInt(5),
   806  					MaxReplicas: 10,
   807  				},
   808  			},
   809  		}
   810  	case ChainPolicyType:
   811  		res.Spec.Policy.Type = ChainPolicyType
   812  		res.Spec.Policy.Buffer = nil
   813  		res.Spec.Policy.Chain = ChainPolicy{
   814  			{
   815  				ID: "weekends",
   816  				FleetAutoscalerPolicy: FleetAutoscalerPolicy{
   817  					Type: SchedulePolicyType,
   818  					Schedule: &SchedulePolicy{
   819  						Between: Between{
   820  							Start: mustParseDate("2024-07-04T15:59:59Z"),
   821  							End:   mustParseDate("9999-07-05T15:59:59Z"),
   822  						},
   823  						ActivePeriod: ActivePeriod{
   824  							Timezone:  "",
   825  							StartCron: "* * * * 5-6",
   826  							Duration:  "",
   827  						},
   828  						Policy: FleetAutoscalerPolicy{
   829  							Type: CounterPolicyType,
   830  							Counter: &CounterPolicy{
   831  								Key:         "playerCount",
   832  								BufferSize:  intstr.FromInt32(5),
   833  								MaxCapacity: 10,
   834  							},
   835  						},
   836  					},
   837  				},
   838  			},
   839  			{
   840  				ID: "",
   841  				FleetAutoscalerPolicy: FleetAutoscalerPolicy{
   842  					Type: BufferPolicyType,
   843  					Buffer: &BufferPolicy{
   844  						BufferSize:  intstr.FromInt32(5),
   845  						MaxReplicas: 10,
   846  					},
   847  				},
   848  			},
   849  		}
   850  	case WasmPolicyType:
   851  		res.Spec.Policy.Type = WasmPolicyType
   852  		res.Spec.Policy.Buffer = nil
   853  		url := "http://example.com/wasm-module"
   854  		res.Spec.Policy.Wasm = &WasmPolicy{
   855  			Function: "scale",
   856  			Config: map[string]string{
   857  				"scale_buffer": "10",
   858  			},
   859  			From: WasmFrom{
   860  				URL: &URLConfiguration{
   861  					URL: &url,
   862  				},
   863  			},
   864  		}
   865  	}
   866  	return res
   867  }