agones.dev/agones@v1.54.0/test/e2e/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 e2e 16 17 import ( 18 "bytes" 19 "context" 20 cryptorand "crypto/rand" 21 "crypto/rsa" 22 "crypto/sha256" 23 "crypto/x509" 24 "crypto/x509/pkix" 25 "encoding/hex" 26 "encoding/pem" 27 "fmt" 28 "math/big" 29 "math/rand" 30 "os" 31 "os/exec" 32 "path/filepath" 33 "strconv" 34 "strings" 35 "testing" 36 "time" 37 38 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 39 allocationv1 "agones.dev/agones/pkg/apis/allocation/v1" 40 autoscalingv1 "agones.dev/agones/pkg/apis/autoscaling/v1" 41 "agones.dev/agones/pkg/util/runtime" 42 helper "agones.dev/agones/test/e2e/allochelper" 43 e2e "agones.dev/agones/test/e2e/framework" 44 "github.com/pkg/errors" 45 "github.com/sirupsen/logrus" 46 "github.com/stretchr/testify/assert" 47 "github.com/stretchr/testify/require" 48 admregv1 "k8s.io/api/admissionregistration/v1" 49 appsv1 "k8s.io/api/apps/v1" 50 corev1 "k8s.io/api/core/v1" 51 k8serrors "k8s.io/apimachinery/pkg/api/errors" 52 "k8s.io/apimachinery/pkg/api/resource" 53 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 54 "k8s.io/apimachinery/pkg/fields" 55 "k8s.io/apimachinery/pkg/labels" 56 "k8s.io/apimachinery/pkg/types" 57 "k8s.io/apimachinery/pkg/util/intstr" 58 "k8s.io/apimachinery/pkg/util/uuid" 59 "k8s.io/apimachinery/pkg/util/wait" 60 ) 61 62 var deletePropagationForeground = metav1.DeletePropagationForeground 63 64 var waitForDeletion = metav1.DeleteOptions{ 65 PropagationPolicy: &deletePropagationForeground, 66 } 67 68 func TestAutoscalerBasicFunctions(t *testing.T) { 69 t.Parallel() 70 ctx := context.Background() 71 72 stable := framework.AgonesClient.AgonesV1() 73 fleets := stable.Fleets(framework.Namespace) 74 flt, err := fleets.Create(ctx, defaultFleet(framework.Namespace), metav1.CreateOptions{}) 75 if assert.Nil(t, err) { 76 defer fleets.Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 77 } 78 79 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 80 81 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 82 defaultFas := defaultFleetAutoscaler(flt, framework.Namespace) 83 fas, err := fleetautoscalers.Create(ctx, defaultFas, metav1.CreateOptions{}) 84 require.NoError(t, err) 85 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 86 87 // the fleet autoscaler should scale the fleet up now up to BufferSize 88 bufferSize := int32(fas.Spec.Policy.Buffer.BufferSize.IntValue()) 89 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(bufferSize)) 90 91 // patch the autoscaler to increase MinReplicas and watch the fleet scale up 92 fas, err = patchFleetAutoscaler(ctx, fas, intstr.FromInt(int(bufferSize)), bufferSize+2, fas.Spec.Policy.Buffer.MaxReplicas) 93 assert.Nil(t, err, "could not patch fleetautoscaler") 94 95 // min replicas is now higher than buffer size, will scale to that level 96 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(fas.Spec.Policy.Buffer.MinReplicas)) 97 98 // patch the autoscaler to remove MinReplicas and watch the fleet scale down to bufferSize 99 fas, err = patchFleetAutoscaler(ctx, fas, intstr.FromInt(int(bufferSize)), 0, fas.Spec.Policy.Buffer.MaxReplicas) 100 assert.Nil(t, err, "could not patch fleetautoscaler") 101 102 bufferSize = int32(fas.Spec.Policy.Buffer.BufferSize.IntValue()) 103 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(bufferSize)) 104 105 // do an allocation and watch the fleet scale up 106 gsa := framework.CreateAndApplyAllocation(t, flt) 107 framework.AssertFleetCondition(t, flt, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 108 return fleet.Status.AllocatedReplicas == 1 109 }) 110 111 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(bufferSize)) 112 113 // patch autoscaler to switch to relative buffer size and check if the fleet adjusts 114 _, err = patchFleetAutoscaler(ctx, fas, intstr.FromString("10%"), 1, fas.Spec.Policy.Buffer.MaxReplicas) 115 require.NoError(t, err, "could not patch fleetautoscaler") 116 117 // 10% with only one allocated GS means only one ready server 118 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(1)) 119 120 // get the Status of the fleetautoscaler 121 fas, err = framework.AgonesClient.AutoscalingV1().FleetAutoscalers(fas.ObjectMeta.Namespace).Get(ctx, fas.Name, metav1.GetOptions{}) 122 require.NoError(t, err, "could not get fleetautoscaler") 123 require.True(t, fas.Status.AbleToScale, "Could not get AbleToScale status") 124 125 // check that we are able to scale 126 framework.WaitForFleetAutoScalerCondition(t, fas, func(_ *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 127 return !fas.Status.ScalingLimited 128 }) 129 130 // patch autoscaler to a maxReplicas count equal to current replicas count 131 _, err = patchFleetAutoscaler(ctx, fas, intstr.FromInt(1), 1, 1) 132 require.NoError(t, err, "could not patch fleetautoscaler") 133 134 // check that we are not able to scale 135 framework.WaitForFleetAutoScalerCondition(t, fas, func(_ *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 136 return fas.Status.ScalingLimited 137 }) 138 139 // delete the allocated GameServer and watch the fleet scale down 140 gp := int64(1) 141 err = stable.GameServers(framework.Namespace).Delete(ctx, gsa.Status.GameServerName, metav1.DeleteOptions{GracePeriodSeconds: &gp}) 142 require.NoError(t, err) 143 framework.AssertFleetCondition(t, flt, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 144 return fleet.Status.AllocatedReplicas == 0 && 145 fleet.Status.ReadyReplicas == 1 && 146 fleet.Status.Replicas == 1 147 }) 148 } 149 150 func TestFleetAutoscalerDefaultSyncInterval(t *testing.T) { 151 t.Parallel() 152 ctx := context.Background() 153 154 stable := framework.AgonesClient.AgonesV1() 155 fleets := stable.Fleets(framework.Namespace) 156 flt, err := fleets.Create(ctx, defaultFleet(framework.Namespace), metav1.CreateOptions{}) 157 if assert.Nil(t, err) { 158 defer fleets.Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 159 } 160 161 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 162 163 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 164 dummyFleetName := "dummy-fleet" 165 defaultFas := &autoscalingv1.FleetAutoscaler{ 166 ObjectMeta: metav1.ObjectMeta{ 167 Name: dummyFleetName + "-autoscaler", 168 Namespace: framework.Namespace, 169 }, 170 Spec: autoscalingv1.FleetAutoscalerSpec{ 171 FleetName: dummyFleetName, 172 Policy: autoscalingv1.FleetAutoscalerPolicy{ 173 Type: autoscalingv1.BufferPolicyType, 174 Buffer: &autoscalingv1.BufferPolicy{ 175 BufferSize: intstr.FromInt(3), 176 MaxReplicas: 10, 177 }, 178 }, 179 }, 180 } 181 fas, err := fleetautoscalers.Create(ctx, defaultFas, metav1.CreateOptions{}) 182 if assert.Nil(t, err) { 183 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 184 } else { 185 // if we could not create the autoscaler, their is no point going further 186 logrus.Error("Failed creating autoscaler, aborting TestFleetAutoscalerDefaultSyncInterval") 187 return 188 } 189 190 defaultSyncIntervalFas := &autoscalingv1.FleetAutoscaler{} 191 defaultSyncIntervalFas.ApplyDefaults() 192 assert.Equal(t, defaultSyncIntervalFas.Spec.Sync.FixedInterval.Seconds, fas.Spec.Sync.FixedInterval.Seconds) 193 } 194 195 // TestFleetAutoScalerRollingUpdate - test fleet with RollingUpdate strategy work with 196 // FleetAutoscaler, verify that number of GameServers does not goes down below RollingUpdate strategy 197 // defined level on Fleet updates. 198 func TestFleetAutoScalerRollingUpdate(t *testing.T) { 199 t.Parallel() 200 ctx := context.Background() 201 202 stable := framework.AgonesClient.AgonesV1() 203 fleets := stable.Fleets(framework.Namespace) 204 flt := defaultFleet(framework.Namespace) 205 flt.Spec.Replicas = 2 206 maxSurge := 1 207 rollingUpdateCount := intstr.FromInt(maxSurge) 208 209 flt.Spec.Strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{} 210 // Set both MaxSurge and MaxUnavaible to 1 211 flt.Spec.Strategy.RollingUpdate.MaxSurge = &rollingUpdateCount 212 flt.Spec.Strategy.RollingUpdate.MaxUnavailable = &rollingUpdateCount 213 214 flt, err := fleets.Create(ctx, flt, metav1.CreateOptions{}) 215 if assert.Nil(t, err) { 216 defer fleets.Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 217 } 218 219 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 220 221 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 222 223 // Create FleetAutoScaler with 7 Buffer and MinReplicas 224 targetScale := 7 225 fas := defaultFleetAutoscaler(flt, framework.Namespace) 226 fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(targetScale) 227 fas.Spec.Policy.Buffer.MinReplicas = int32(targetScale) 228 fas, err = fleetautoscalers.Create(ctx, fas, metav1.CreateOptions{}) 229 if assert.Nil(t, err) { 230 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 231 } else { 232 // if we could not create the autoscaler, their is no point going further 233 logrus.Error("Failed creating autoscaler, aborting TestAutoscalerBasicFunctions") 234 return 235 } 236 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(int32(targetScale))) 237 238 // get the Status of the fleetautoscaler 239 fas, err = framework.AgonesClient.AutoscalingV1().FleetAutoscalers(fas.ObjectMeta.Namespace).Get(ctx, fas.Name, metav1.GetOptions{}) 240 require.NoError(t, err, "could not get fleetautoscaler") 241 assert.True(t, fas.Status.AbleToScale, "Could not get AbleToScale status") 242 243 // check that we are able to scale 244 framework.WaitForFleetAutoScalerCondition(t, fas, func(_ *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 245 return !fas.Status.ScalingLimited 246 }) 247 248 // Change ContainerPort to trigger creating a new GSSet 249 flt, err = framework.AgonesClient.AgonesV1().Fleets(framework.Namespace).Get(ctx, flt.ObjectMeta.Name, metav1.GetOptions{}) 250 251 assert.Nil(t, err, "Able to get the Fleet") 252 fltCopy := flt.DeepCopy() 253 fltCopy.Spec.Template.Spec.Ports[0].ContainerPort++ 254 logrus.Info("Current fleet replicas count: ", fltCopy.Spec.Replicas) 255 256 // In ticket #1156 we apply new Replicas size 2, which is smaller than 7 257 // And RollingUpdate is broken, scaling immediately from 7 to 2 and then back to 7 258 // Uncomment line below to break this test 259 // fltCopy.Spec.Replicas = 2 260 261 flt, err = framework.AgonesClient.AgonesV1().Fleets(framework.Namespace).Update(ctx, fltCopy, metav1.UpdateOptions{}) 262 assert.NoError(t, err) 263 264 selector := labels.SelectorFromSet(labels.Set{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}) 265 // Wait till new GSS is created 266 err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, true, func(ctx context.Context) (bool, error) { 267 gssList, err := framework.AgonesClient.AgonesV1().GameServerSets(framework.Namespace).List(ctx, 268 metav1.ListOptions{LabelSelector: selector.String()}) 269 if err != nil { 270 return false, err 271 } 272 return len(gssList.Items) == 2, nil 273 }) 274 assert.NoError(t, err) 275 276 // Check that total number of gameservers in the system does not goes lower than RollingUpdate 277 // parameters (deleting no more than maxUnavailable servers at a time) 278 // Wait for old GSSet to be deleted 279 err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { 280 list, err := framework.AgonesClient.AgonesV1().GameServers(framework.Namespace).List(ctx, 281 metav1.ListOptions{LabelSelector: selector.String()}) 282 if err != nil { 283 return false, err 284 } 285 286 maxUnavailable, err := intstr.GetValueFromIntOrPercent(flt.Spec.Strategy.RollingUpdate.MaxUnavailable, 100, true) 287 assert.Nil(t, err) 288 if len(list.Items) < targetScale-maxUnavailable { 289 err = errors.New("New replicas should be not less than (target - maxUnavailable)") 290 } 291 if err != nil { 292 return false, err 293 } 294 gssList, err := framework.AgonesClient.AgonesV1().GameServerSets(framework.Namespace).List(ctx, 295 metav1.ListOptions{LabelSelector: selector.String()}) 296 if err != nil { 297 return false, err 298 } 299 return len(gssList.Items) == 1, nil 300 }) 301 302 assert.NoError(t, err) 303 } 304 305 // TestAutoscalerStressCreate creates many fleetautoscalers with random values 306 // to check if the creation validation works as expected and if the fleet scales 307 // to the expected number of replicas (when the creation is valid) 308 func TestAutoscalerStressCreate(t *testing.T) { 309 t.Parallel() 310 ctx := context.Background() 311 log := e2e.TestLogger(t) 312 313 alpha1 := framework.AgonesClient.AgonesV1() 314 fleets := alpha1.Fleets(framework.Namespace) 315 flt, err := fleets.Create(ctx, defaultFleet(framework.Namespace), metav1.CreateOptions{}) 316 if assert.Nil(t, err) { 317 defer fleets.Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 318 } 319 320 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 321 322 r := rand.New(rand.NewSource(1783)) 323 324 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 325 326 for i := 0; i < 5; i++ { 327 fas := defaultFleetAutoscaler(flt, framework.Namespace) 328 bufferSize := r.Int31n(5) 329 minReplicas := r.Int31n(5) 330 maxReplicas := r.Int31n(8) 331 fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(int(bufferSize)) 332 fas.Spec.Policy.Buffer.MinReplicas = minReplicas 333 fas.Spec.Policy.Buffer.MaxReplicas = maxReplicas 334 335 valid := bufferSize > 0 && 336 fas.Spec.Policy.Buffer.MaxReplicas > 0 && 337 fas.Spec.Policy.Buffer.MaxReplicas >= bufferSize && 338 fas.Spec.Policy.Buffer.MinReplicas <= fas.Spec.Policy.Buffer.MaxReplicas && 339 (fas.Spec.Policy.Buffer.MinReplicas == 0 || fas.Spec.Policy.Buffer.MinReplicas >= bufferSize) 340 341 log.WithField("buffer", fmt.Sprintf("%#v", fas.Spec.Policy.Buffer)).Info("This is the FAS policy!") 342 343 // create a closure to have defered delete func called on each loop iteration. 344 func() { 345 fas, err := fleetautoscalers.Create(ctx, fas, metav1.CreateOptions{}) 346 if err == nil { 347 log.WithField("fas", fas.ObjectMeta.Name).Info("Created!") 348 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 349 require.True(t, valid, 350 fmt.Sprintf("FleetAutoscaler created even if the parameters are NOT valid: %d %d %d", 351 bufferSize, 352 fas.Spec.Policy.Buffer.MinReplicas, 353 fas.Spec.Policy.Buffer.MaxReplicas)) 354 355 expectedReplicas := bufferSize 356 if expectedReplicas < fas.Spec.Policy.Buffer.MinReplicas { 357 expectedReplicas = fas.Spec.Policy.Buffer.MinReplicas 358 } 359 if expectedReplicas > fas.Spec.Policy.Buffer.MaxReplicas { 360 expectedReplicas = fas.Spec.Policy.Buffer.MaxReplicas 361 } 362 // the fleet autoscaler should scale the fleet now to expectedReplicas 363 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(expectedReplicas)) 364 } else { 365 require.False(t, valid, 366 fmt.Sprintf("FleetAutoscaler NOT created even if the parameters are valid: %d %d %d (%s)", 367 bufferSize, 368 minReplicas, 369 maxReplicas, err)) 370 } 371 }() 372 } 373 } 374 375 // scaleFleet creates a patch to apply to a Fleet. 376 // easier for testing, as it removes object generational issues. 377 func patchFleetAutoscaler(ctx context.Context, fas *autoscalingv1.FleetAutoscaler, bufferSize intstr.IntOrString, minReplicas int32, maxReplicas int32) (*autoscalingv1.FleetAutoscaler, error) { 378 var bufferSizeFmt string 379 if bufferSize.Type == intstr.Int { 380 bufferSizeFmt = fmt.Sprintf("%d", bufferSize.IntValue()) 381 } else { 382 bufferSizeFmt = fmt.Sprintf("%q", bufferSize.String()) 383 } 384 385 patch := fmt.Sprintf( 386 `[{ "op": "replace", "path": "/spec/policy/buffer/bufferSize", "value": %s },`+ 387 `{ "op": "replace", "path": "/spec/policy/buffer/minReplicas", "value": %d },`+ 388 `{ "op": "replace", "path": "/spec/policy/buffer/maxReplicas", "value": %d }]`, 389 bufferSizeFmt, minReplicas, maxReplicas) 390 logrus. 391 WithField("fleetautoscaler", fas.ObjectMeta.Name). 392 WithField("bufferSize", bufferSize.String()). 393 WithField("minReplicas", minReplicas). 394 WithField("maxReplicas", maxReplicas). 395 WithField("patch", patch). 396 Info("Patching fleetautoscaler") 397 398 fas, err := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace). 399 Patch(ctx, fas.ObjectMeta.Name, types.JSONPatchType, []byte(patch), metav1.PatchOptions{}) 400 logrus.WithField("fleetautoscaler", fas).Info("Patched fleet autoscaler") 401 return fas, err 402 } 403 404 // defaultFleetAutoscaler returns a default fleet autoscaler configuration for a given fleet 405 func defaultFleetAutoscaler(f *agonesv1.Fleet, namespace string) *autoscalingv1.FleetAutoscaler { 406 return &autoscalingv1.FleetAutoscaler{ 407 ObjectMeta: metav1.ObjectMeta{Name: f.ObjectMeta.Name + "-autoscaler", Namespace: namespace}, 408 Spec: autoscalingv1.FleetAutoscalerSpec{ 409 FleetName: f.ObjectMeta.Name, 410 Policy: autoscalingv1.FleetAutoscalerPolicy{ 411 Type: autoscalingv1.BufferPolicyType, 412 Buffer: &autoscalingv1.BufferPolicy{ 413 BufferSize: intstr.FromInt(3), 414 MaxReplicas: 10, 415 }, 416 }, 417 Sync: &autoscalingv1.FleetAutoscalerSync{ 418 Type: autoscalingv1.FixedIntervalSyncType, 419 FixedInterval: autoscalingv1.FixedIntervalSync{ 420 Seconds: 30, 421 }, 422 }, 423 }, 424 } 425 } 426 427 // Test fleetautoscaler with webhook policy type 428 // scaling from Replicas equals to 1 to 2 429 func TestAutoscalerWebhook(t *testing.T) { 430 t.Parallel() 431 ctx := context.Background() 432 pod, svc := defaultAutoscalerWebhook(framework.Namespace, "false") 433 pod, err := framework.KubeClient.CoreV1().Pods(framework.Namespace).Create(ctx, pod, metav1.CreateOptions{}) 434 require.NoError(t, err) 435 defer framework.KubeClient.CoreV1().Pods(framework.Namespace).Delete(ctx, pod.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 436 svc.ObjectMeta.Name = "" 437 svc.ObjectMeta.GenerateName = "test-service-" 438 439 svc, err = framework.KubeClient.CoreV1().Services(framework.Namespace).Create(ctx, svc, metav1.CreateOptions{}) 440 require.NoError(t, err) 441 defer framework.KubeClient.CoreV1().Services(framework.Namespace).Delete(ctx, svc.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 442 443 alpha1 := framework.AgonesClient.AgonesV1() 444 fleets := alpha1.Fleets(framework.Namespace) 445 flt := defaultFleet(framework.Namespace) 446 initialReplicasCount := int32(1) 447 flt.Spec.Replicas = initialReplicasCount 448 flt, err = fleets.Create(ctx, flt, metav1.CreateOptions{}) 449 require.NoError(t, err) 450 defer fleets.Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 451 452 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 453 454 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 455 fas := defaultFleetAutoscaler(flt, framework.Namespace) 456 fas.Spec.Policy.Type = autoscalingv1.WebhookPolicyType 457 fas.Spec.Policy.Buffer = nil 458 path := "scale" //nolint:goconst 459 fas.Spec.Policy.Webhook = &autoscalingv1.URLConfiguration{ 460 Service: &admregv1.ServiceReference{ 461 Name: svc.ObjectMeta.Name, 462 Namespace: framework.Namespace, 463 Path: &path, 464 }, 465 } 466 fas, err = fleetautoscalers.Create(ctx, fas, metav1.CreateOptions{}) 467 require.NoError(t, err) 468 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 469 470 framework.CreateAndApplyAllocation(t, flt) 471 framework.AssertFleetCondition(t, flt, func(log *logrus.Entry, fleet *agonesv1.Fleet) bool { 472 log.WithField("fleetStatus", fmt.Sprintf("%+v", fleet.Status)).WithField("fleet", fleet.ObjectMeta.Name).Info("Awaiting fleet.Status.AllocatedReplicas == 1") 473 return fleet.Status.AllocatedReplicas == 1 474 }) 475 476 framework.AssertFleetCondition(t, flt, func(log *logrus.Entry, fleet *agonesv1.Fleet) bool { 477 log.WithField("fleetStatus", fmt.Sprintf("%+v", fleet.Status)). 478 WithField("fleet", fleet.ObjectMeta.Name). 479 WithField("initialReplicasCount", initialReplicasCount). 480 Info("Awaiting fleet.Status.Replicas > initialReplicasCount") 481 return fleet.Status.Replicas > initialReplicasCount 482 }) 483 484 // Wait for LastAppliedPolicy to be set to WebhookPolicyType 485 framework.WaitForFleetAutoScalerCondition(t, fas, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 486 log.WithField("LastAppliedPolicy", fas.Status.LastAppliedPolicy). 487 Info("Waiting for LastAppliedPolicy to be set to WebhookPolicyType") 488 return fas.Status.LastAppliedPolicy == autoscalingv1.WebhookPolicyType 489 }) 490 491 // Cause an error in Webhook config 492 // Use wrong service Path 493 err = wait.PollUntilContextTimeout(context.Background(), time.Second, time.Minute, true, func(ctx context.Context) (bool, error) { 494 fas, err = fleetautoscalers.Get(ctx, fas.ObjectMeta.Name, metav1.GetOptions{}) 495 if err != nil { 496 return true, err 497 } 498 newPath := path + "2" 499 fas.Spec.Policy.Webhook.Service.Path = &newPath 500 labels := map[string]string{"fleetautoscaler": "wrong"} 501 fas.ObjectMeta.Labels = labels 502 _, err = fleetautoscalers.Update(ctx, fas, metav1.UpdateOptions{}) 503 if err != nil { 504 logrus.WithError(err).Warn("could not update fleet autoscaler") 505 return false, nil 506 } 507 508 return true, nil 509 }) 510 require.NoError(t, err) 511 512 var l *corev1.EventList 513 errString := "Error calculating desired fleet size on FleetAutoscaler" 514 found := false 515 516 // Error - net/http: request canceled while waiting for connection (Client.Timeout exceeded 517 // while awaiting headers) 518 err = wait.PollUntilContextTimeout(context.Background(), time.Second, time.Minute, true, func(ctx context.Context) (bool, error) { 519 events := framework.KubeClient.CoreV1().Events(framework.Namespace) 520 l, err = events.List(ctx, metav1.ListOptions{FieldSelector: fields.AndSelectors(fields.OneTermEqualSelector("involvedObject.name", fas.ObjectMeta.Name), fields.OneTermEqualSelector("type", "Warning")).String()}) 521 if err != nil { 522 return false, err 523 } 524 for _, v := range l.Items { 525 if strings.Contains(v.Message, errString) { 526 found = true 527 } 528 } 529 return found, nil 530 }) 531 assert.NoError(t, err, "Received unexpected error") 532 assert.True(t, found, "Expected error was not received") 533 } 534 535 func TestFleetAutoscalerTLSWebhook(t *testing.T) { 536 t.Parallel() 537 ctx := context.Background() 538 // we hardcode 'default' namespace here because certificates above are generated to use this one 539 defaultNS := "default" 540 541 // certs 542 caPem, _, caCert, caPrivKey, err := generateRootCA() 543 require.NoError(t, err) 544 clientCertPEM, clientCertPrivKeyPEM, err := generateLocalCert(caCert, caPrivKey) 545 require.NoError(t, err) 546 547 secr := &corev1.Secret{ 548 ObjectMeta: metav1.ObjectMeta{ 549 GenerateName: "autoscalersecret-", 550 }, 551 Type: corev1.SecretTypeTLS, 552 Data: make(map[string][]byte), 553 } 554 555 secr.Data[corev1.TLSCertKey] = clientCertPEM 556 secr.Data[corev1.TLSPrivateKeyKey] = clientCertPrivKeyPEM 557 558 secrets := framework.KubeClient.CoreV1().Secrets(defaultNS) 559 secr, err = secrets.Create(ctx, secr.DeepCopy(), metav1.CreateOptions{}) 560 if assert.Nil(t, err) { 561 defer secrets.Delete(ctx, secr.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 562 } 563 564 pod, svc := defaultAutoscalerWebhook(defaultNS, "false") 565 pod.Spec.Volumes = make([]corev1.Volume, 1) 566 pod.Spec.Volumes[0] = corev1.Volume{ 567 Name: "secret-volume", 568 VolumeSource: corev1.VolumeSource{ 569 Secret: &corev1.SecretVolumeSource{ 570 SecretName: secr.ObjectMeta.Name, 571 }, 572 }, 573 } 574 pod.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{{ 575 Name: "secret-volume", 576 MountPath: "/home/service/certs", 577 }} 578 pod, err = framework.KubeClient.CoreV1().Pods(defaultNS).Create(ctx, pod.DeepCopy(), metav1.CreateOptions{}) 579 if assert.Nil(t, err) { 580 defer framework.KubeClient.CoreV1().Pods(defaultNS).Delete(ctx, pod.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 581 } else { 582 // if we could not create the webhook, there is no point going further 583 assert.FailNow(t, "Failed creating webhook pod, aborting TestTlsWebhook") 584 } 585 586 // since we're using statically-named service, perform a best-effort delete of a previous service 587 err = framework.KubeClient.CoreV1().Services(defaultNS).Delete(ctx, svc.ObjectMeta.Name, waitForDeletion) 588 if err != nil { 589 assert.True(t, k8serrors.IsNotFound(err)) 590 } 591 592 // making sure the service is really gone. 593 err = wait.PollUntilContextTimeout(context.Background(), 2*time.Second, time.Minute, true, func(ctx context.Context) (bool, error) { 594 _, err := framework.KubeClient.CoreV1().Services(defaultNS).Get(ctx, svc.ObjectMeta.Name, metav1.GetOptions{}) 595 return k8serrors.IsNotFound(err), nil 596 }) 597 assert.Nil(t, err) 598 599 svc, err = framework.KubeClient.CoreV1().Services(defaultNS).Create(ctx, svc.DeepCopy(), metav1.CreateOptions{}) 600 if assert.Nil(t, err) { 601 defer framework.KubeClient.CoreV1().Services(defaultNS).Delete(ctx, svc.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 602 } else { 603 // if we could not create the service, there is no point going further 604 assert.FailNow(t, "Failed creating service, aborting TestTlsWebhook") 605 } 606 607 alpha1 := framework.AgonesClient.AgonesV1() 608 fleets := alpha1.Fleets(defaultNS) 609 flt := defaultFleet(defaultNS) 610 initialReplicasCount := int32(1) 611 flt.Spec.Replicas = initialReplicasCount 612 flt, err = fleets.Create(ctx, flt.DeepCopy(), metav1.CreateOptions{}) 613 if assert.Nil(t, err) { 614 defer fleets.Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 615 } 616 617 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 618 619 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(defaultNS) 620 fas := defaultFleetAutoscaler(flt, defaultNS) 621 fas.Spec.Policy.Type = autoscalingv1.WebhookPolicyType 622 fas.Spec.Policy.Buffer = nil 623 path := "scale" 624 625 fas.Spec.Policy.Webhook = &autoscalingv1.URLConfiguration{ 626 Service: &admregv1.ServiceReference{ 627 Name: svc.ObjectMeta.Name, 628 Namespace: defaultNS, 629 Path: &path, 630 }, 631 CABundle: caPem, 632 } 633 fas, err = fleetautoscalers.Create(ctx, fas.DeepCopy(), metav1.CreateOptions{}) 634 if assert.Nil(t, err) { 635 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 636 } else { 637 // if we could not create the autoscaler, their is no point going further 638 assert.FailNow(t, "Failed creating autoscaler, aborting TestTlsWebhook") 639 } 640 framework.CreateAndApplyAllocation(t, flt) 641 framework.AssertFleetCondition(t, flt, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 642 return fleet.Status.AllocatedReplicas == 1 643 }) 644 645 framework.AssertFleetCondition(t, flt, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 646 return fleet.Status.Replicas > initialReplicasCount 647 }) 648 649 // Wait for LastAppliedPolicy to be set to WebhookPolicyType 650 framework.WaitForFleetAutoScalerCondition(t, fas, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 651 log.WithField("LastAppliedPolicy", fas.Status.LastAppliedPolicy). 652 Info("Waiting for LastAppliedPolicy to be set to WebhookPolicyType") 653 return fas.Status.LastAppliedPolicy == autoscalingv1.WebhookPolicyType 654 }) 655 } 656 657 func TestAutoscalerWebhookWithMetadata(t *testing.T) { 658 if !runtime.FeatureEnabled(runtime.FeatureFleetAutoscaleRequestMetaData) { 659 t.SkipNow() 660 } 661 t.Parallel() 662 663 ctx := context.Background() 664 // Create webhook Pod and Service 665 pod, svc := defaultAutoscalerWebhook(framework.Namespace, "true") 666 pod, err := framework.KubeClient.CoreV1().Pods(framework.Namespace).Create(ctx, pod, metav1.CreateOptions{}) 667 require.NoError(t, err) 668 defer framework.KubeClient.CoreV1().Pods(framework.Namespace).Delete(ctx, pod.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 669 svc.ObjectMeta.Name = "" 670 svc.ObjectMeta.GenerateName = "test-service-" 671 672 svc, err = framework.KubeClient.CoreV1().Services(framework.Namespace).Create(ctx, svc, metav1.CreateOptions{}) 673 require.NoError(t, err) 674 defer framework.KubeClient.CoreV1().Services(framework.Namespace).Delete(ctx, svc.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 675 676 // Create Fleet with metadata annotation 677 alpha1 := framework.AgonesClient.AgonesV1() 678 flt := defaultFleet(framework.Namespace) 679 initialReplicasCount := int32(1) 680 fixedReplicas := int32(11) 681 flt.Spec.Replicas = initialReplicasCount 682 flt.ObjectMeta.Annotations = map[string]string{ 683 "fixedReplicas": fmt.Sprintf("%d", fixedReplicas), 684 } 685 flt, err = alpha1.Fleets(framework.Namespace).Create(ctx, flt, metav1.CreateOptions{}) 686 require.NoError(t, err) 687 defer alpha1.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 688 689 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(initialReplicasCount)) 690 691 // Create FleetAutoscaler with webhook policy 692 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 693 fas := defaultFleetAutoscaler(flt, framework.Namespace) 694 fas.Spec.Policy.Type = autoscalingv1.WebhookPolicyType 695 fas.Spec.Policy.Buffer = nil 696 path := "scale" 697 fas.Spec.Policy.Webhook = &autoscalingv1.URLConfiguration{ 698 Service: &admregv1.ServiceReference{ 699 Name: svc.ObjectMeta.Name, 700 Namespace: framework.Namespace, 701 Path: &path, 702 }, 703 } 704 fas, err = fleetautoscalers.Create(ctx, fas, metav1.CreateOptions{}) 705 require.NoError(t, err) 706 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 707 708 // Trigger allocation to cause autoscaler logic to kick in 709 framework.CreateAndApplyAllocation(t, flt) 710 711 // Wait until replicas match the fixedReplicas value 712 framework.AssertFleetCondition(t, flt, func(log *logrus.Entry, fleet *agonesv1.Fleet) bool { 713 log.WithFields(logrus.Fields{ 714 "fleetStatus": fmt.Sprintf("%+v", fleet.Status), 715 "expectedReplicas": fixedReplicas, 716 "fleet": fleet.ObjectMeta.Name, 717 "fleetAllocatedCount": fleet.Status.AllocatedReplicas, 718 }).Info("Waiting for fleet.Status.Replicas == fixedReplicas") 719 return fleet.Status.Replicas == fixedReplicas 720 }) 721 722 // Wait for LastAppliedPolicy to be set correctly 723 framework.WaitForFleetAutoScalerCondition(t, fas, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 724 log.WithField("LastAppliedPolicy", fas.Status.LastAppliedPolicy). 725 Info("Waiting for LastAppliedPolicy to be set to WebhookPolicyType") 726 return fas.Status.LastAppliedPolicy == autoscalingv1.WebhookPolicyType 727 }) 728 } 729 730 func defaultAutoscalerWebhook(namespace string, fixedReplicasEnabled string) (*corev1.Pod, *corev1.Service) { 731 l := make(map[string]string) 732 appName := fmt.Sprintf("autoscaler-webhook-%v", time.Now().UnixNano()) 733 l["app"] = appName 734 l[e2e.AutoCleanupLabelKey] = e2e.AutoCleanupLabelValue 735 736 pod := &corev1.Pod{ 737 ObjectMeta: metav1.ObjectMeta{ 738 GenerateName: "auto-webhook-", 739 Namespace: namespace, 740 Labels: l, 741 }, 742 Spec: corev1.PodSpec{ 743 Containers: []corev1.Container{ 744 { 745 Name: "webhook", 746 Image: "us-docker.pkg.dev/agones-images/examples/autoscaler-webhook:0.20", 747 ImagePullPolicy: corev1.PullIfNotPresent, 748 Ports: []corev1.ContainerPort{{ 749 ContainerPort: 8000, 750 Name: "autoscaler", 751 }}, 752 Env: []corev1.EnvVar{ 753 { 754 Name: "FIXED_REPLICAS", 755 Value: fixedReplicasEnabled, 756 }, 757 }, 758 }, 759 }, 760 }, 761 } 762 m := make(map[string]string) 763 m["app"] = appName 764 service := &corev1.Service{ 765 ObjectMeta: metav1.ObjectMeta{ 766 Name: "autoscaler-tls-service", 767 Namespace: namespace, 768 }, 769 Spec: corev1.ServiceSpec{ 770 Selector: m, 771 Ports: []corev1.ServicePort{{ 772 Name: "newport", 773 Port: 8000, 774 TargetPort: intstr.FromString("autoscaler"), 775 }}, 776 }, 777 } 778 779 return pod, service 780 } 781 782 // Instructions: https://agones.dev/site/docs/getting-started/create-webhook-fleetautoscaler/#chapter-2-configuring-https-fleetautoscaler-webhook-with-ca-bundle 783 // but also, credits/inspiration to https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/cloudprovider/aws/aws-sdk-go/awstesting/certificate_utils.go 784 785 func generateRootCA() ( 786 caPEM, caPrivKeyPEM []byte, caCert *x509.Certificate, caPrivKey *rsa.PrivateKey, err error, 787 ) { 788 caCert = &x509.Certificate{ 789 SerialNumber: big.NewInt(42), 790 Subject: pkix.Name{ 791 Country: []string{"US"}, 792 Organization: []string{"Agones"}, 793 CommonName: "Test Root CA", 794 }, 795 NotBefore: time.Now().Add(-time.Minute), 796 NotAfter: time.Now().AddDate(1, 0, 0), 797 KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, 798 ExtKeyUsage: []x509.ExtKeyUsage{ 799 x509.ExtKeyUsageClientAuth, 800 x509.ExtKeyUsageServerAuth, 801 }, 802 BasicConstraintsValid: true, 803 IsCA: true, 804 } 805 806 // Create CA private and public key 807 caPrivKey, err = rsa.GenerateKey(cryptorand.Reader, 4096) 808 if err != nil { 809 return nil, nil, nil, nil, fmt.Errorf("failed generate CA RSA key, %w", err) 810 } 811 812 // Create CA certificate 813 caBytes, err := x509.CreateCertificate(cryptorand.Reader, caCert, caCert, &caPrivKey.PublicKey, caPrivKey) 814 if err != nil { 815 return nil, nil, nil, nil, fmt.Errorf("failed generate CA certificate, %w", err) 816 } 817 818 // PEM encode CA certificate and private key 819 var caPEMBuf bytes.Buffer 820 err = pem.Encode(&caPEMBuf, &pem.Block{ 821 Type: "CERTIFICATE", 822 Bytes: caBytes, 823 }) 824 if err != nil { 825 return nil, nil, nil, nil, fmt.Errorf("failed to endcode root PEM, %w", err) 826 } 827 828 var caPrivKeyPEMBuf bytes.Buffer 829 err = pem.Encode(&caPrivKeyPEMBuf, &pem.Block{ 830 Type: "RSA PRIVATE KEY", 831 Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), 832 }) 833 if err != nil { 834 return nil, nil, nil, nil, fmt.Errorf("failed to endcode private root PEM, %w", err) 835 } 836 837 return caPEMBuf.Bytes(), caPrivKeyPEMBuf.Bytes(), caCert, caPrivKey, nil 838 } 839 840 func generateLocalCert(parentCert *x509.Certificate, parentPrivKey *rsa.PrivateKey) ( 841 certPEM, certPrivKeyPEM []byte, err error, 842 ) { 843 cert := &x509.Certificate{ 844 SerialNumber: big.NewInt(42), 845 Subject: pkix.Name{ 846 Country: []string{"US"}, 847 Organization: []string{"Agones"}, 848 CommonName: "autoscaler-tls-service.default.svc", 849 }, 850 NotBefore: time.Now().Add(-time.Minute), 851 NotAfter: time.Now().AddDate(1, 0, 0), 852 ExtKeyUsage: []x509.ExtKeyUsage{ 853 x509.ExtKeyUsageClientAuth, 854 x509.ExtKeyUsageServerAuth, 855 }, 856 KeyUsage: x509.KeyUsageDigitalSignature, 857 DNSNames: []string{"autoscaler-tls-service.default.svc"}, 858 } 859 860 // Create server private and public key 861 certPrivKey, err := rsa.GenerateKey(cryptorand.Reader, 4096) 862 if err != nil { 863 return nil, nil, fmt.Errorf("failed to generate server RSA private key, %w", err) 864 } 865 866 // Create server certificate 867 certBytes, err := x509.CreateCertificate(cryptorand.Reader, cert, parentCert, &certPrivKey.PublicKey, parentPrivKey) 868 if err != nil { 869 return nil, nil, fmt.Errorf("failed to generate server certificate, %w", err) 870 } 871 872 // PEM encode certificate and private key 873 var certPEMBuf bytes.Buffer 874 err = pem.Encode(&certPEMBuf, &pem.Block{ 875 Type: "CERTIFICATE", 876 Bytes: certBytes, 877 }) 878 if err != nil { 879 return nil, nil, fmt.Errorf("failed to endcode certificate pem, %w", err) 880 } 881 882 var certPrivKeyPEMBuf bytes.Buffer 883 err = pem.Encode(&certPrivKeyPEMBuf, &pem.Block{ 884 Type: "RSA PRIVATE KEY", 885 Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), 886 }) 887 if err != nil { 888 return nil, nil, fmt.Errorf("failed to endcode private pem, %w", err) 889 } 890 891 return certPEMBuf.Bytes(), certPrivKeyPEMBuf.Bytes(), nil 892 } 893 894 func TestCounterAutoscaler(t *testing.T) { 895 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 896 t.SkipNow() 897 } 898 t.Parallel() 899 900 ctx := context.Background() 901 client := framework.AgonesClient.AgonesV1() 902 log := e2e.TestLogger(t) 903 904 flt := defaultFleet(framework.Namespace) 905 flt.Spec.Template.Spec.Counters = map[string]agonesv1.CounterStatus{ 906 "players": { 907 Count: 7, // AggregateCount 21 908 Capacity: 10, // AggregateCapacity 30 909 }, 910 "sessions": { 911 Count: 0, // AggregateCount 0 912 Capacity: 5, // AggregateCapacity 15 913 }, 914 } 915 916 flt, err := client.Fleets(framework.Namespace).Create(ctx, flt.DeepCopy(), metav1.CreateOptions{}) 917 require.NoError(t, err) 918 defer client.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 919 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 920 921 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 922 923 counterFas := func(f func(fap *autoscalingv1.FleetAutoscalerPolicy)) *autoscalingv1.FleetAutoscaler { 924 fas := autoscalingv1.FleetAutoscaler{ 925 ObjectMeta: metav1.ObjectMeta{Name: flt.ObjectMeta.Name + "-counter-autoscaler", Namespace: framework.Namespace}, 926 Spec: autoscalingv1.FleetAutoscalerSpec{ 927 FleetName: flt.ObjectMeta.Name, 928 Policy: autoscalingv1.FleetAutoscalerPolicy{ 929 Type: autoscalingv1.CounterPolicyType, 930 }, 931 Sync: &autoscalingv1.FleetAutoscalerSync{ 932 Type: autoscalingv1.FixedIntervalSyncType, 933 FixedInterval: autoscalingv1.FixedIntervalSync{ 934 Seconds: 1, 935 }, 936 }, 937 }, 938 } 939 f(&fas.Spec.Policy) 940 return &fas 941 } 942 943 testCases := map[string]struct { 944 fas *autoscalingv1.FleetAutoscaler 945 wantFasErr bool 946 wantReplicas int32 947 }{ 948 "Scale Down Buffer Int": { 949 fas: counterFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 950 fap.Counter = &autoscalingv1.CounterPolicy{ 951 Key: "players", 952 BufferSize: intstr.FromInt(5), // Buffer refers to the available capacity (AggregateCapacity - AggregateCount) 953 MinCapacity: 10, // Min and MaxCapacity refer to the total capacity aggregated across the fleet, NOT the available capacity 954 MaxCapacity: 100, 955 } 956 }), 957 wantFasErr: false, 958 wantReplicas: 2, 959 }, 960 "Scale Up Buffer Int": { 961 fas: counterFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 962 fap.Counter = &autoscalingv1.CounterPolicy{ 963 Key: "players", 964 BufferSize: intstr.FromInt(25), 965 MinCapacity: 25, 966 MaxCapacity: 100, 967 } 968 }), 969 wantFasErr: false, 970 wantReplicas: 9, 971 }, 972 "Scale Down to MaxCapacity": { 973 fas: counterFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 974 fap.Counter = &autoscalingv1.CounterPolicy{ 975 Key: "sessions", 976 BufferSize: intstr.FromInt(5), 977 MinCapacity: 0, 978 MaxCapacity: 5, 979 } 980 }), 981 wantFasErr: false, 982 wantReplicas: 1, 983 }, 984 "Scale Up to MinCapacity": { 985 fas: counterFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 986 fap.Counter = &autoscalingv1.CounterPolicy{ 987 Key: "sessions", 988 BufferSize: intstr.FromInt(1), 989 MinCapacity: 30, 990 MaxCapacity: 100, 991 } 992 }), 993 wantFasErr: false, 994 wantReplicas: 6, 995 }, 996 "Cannot scale up (MaxCapacity)": { 997 fas: counterFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 998 fap.Counter = &autoscalingv1.CounterPolicy{ 999 Key: "players", 1000 BufferSize: intstr.FromInt(10), 1001 MinCapacity: 10, 1002 MaxCapacity: 30, 1003 } 1004 }), 1005 wantFasErr: false, 1006 wantReplicas: 3, 1007 }, 1008 "Cannot scale down (MinCapacity)": { 1009 fas: counterFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1010 fap.Counter = &autoscalingv1.CounterPolicy{ 1011 Key: "sessions", 1012 BufferSize: intstr.FromInt(5), 1013 MinCapacity: 15, 1014 MaxCapacity: 100, 1015 } 1016 }), 1017 wantFasErr: false, 1018 wantReplicas: 3, 1019 }, 1020 "Buffer Greater than MinCapacity invalid FAS": { 1021 fas: counterFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1022 fap.Counter = &autoscalingv1.CounterPolicy{ 1023 Key: "players", 1024 BufferSize: intstr.FromInt(25), 1025 MinCapacity: 10, 1026 MaxCapacity: 100, 1027 } 1028 }), 1029 wantFasErr: true, 1030 }, 1031 } 1032 for name, testCase := range testCases { 1033 t.Run(name, func(t *testing.T) { 1034 1035 fas, err := fleetautoscalers.Create(ctx, testCase.fas, metav1.CreateOptions{}) 1036 if testCase.wantFasErr { 1037 assert.Error(t, err) 1038 return 1039 } 1040 assert.NoError(t, err) 1041 1042 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(testCase.wantReplicas)) 1043 fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1044 1045 // Return to starting 3 replicas 1046 framework.ScaleFleet(t, log, flt, 3) 1047 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(3)) 1048 }) 1049 } 1050 } 1051 1052 // nolint:dupl // Linter errors on lines are duplicate of TestListAutoscalerWithNoReplicas 1053 func TestCounterAutoscalerWithNoReplicas(t *testing.T) { 1054 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 1055 t.SkipNow() 1056 } 1057 t.Parallel() 1058 1059 ctx := context.Background() 1060 client := framework.AgonesClient.AgonesV1() 1061 log := e2e.TestLogger(t) 1062 1063 flt := defaultEmptyFleet(framework.Namespace) 1064 flt.Spec.Template.Spec.Counters = map[string]agonesv1.CounterStatus{ 1065 "games": { 1066 Capacity: 5, 1067 }, 1068 } 1069 1070 flt, err := client.Fleets(framework.Namespace).Create(ctx, flt.DeepCopy(), metav1.CreateOptions{}) 1071 require.NoError(t, err) 1072 defer client.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1073 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 1074 1075 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 1076 1077 counterFas := func(f func(fap *autoscalingv1.FleetAutoscalerPolicy)) *autoscalingv1.FleetAutoscaler { 1078 fas := autoscalingv1.FleetAutoscaler{ 1079 ObjectMeta: metav1.ObjectMeta{Name: flt.ObjectMeta.Name + "-counter-autoscaler", Namespace: framework.Namespace}, 1080 Spec: autoscalingv1.FleetAutoscalerSpec{ 1081 FleetName: flt.ObjectMeta.Name, 1082 Policy: autoscalingv1.FleetAutoscalerPolicy{ 1083 Type: autoscalingv1.CounterPolicyType, 1084 }, 1085 Sync: &autoscalingv1.FleetAutoscalerSync{ 1086 Type: autoscalingv1.FixedIntervalSyncType, 1087 FixedInterval: autoscalingv1.FixedIntervalSync{ 1088 Seconds: 1, 1089 }, 1090 }, 1091 }, 1092 } 1093 f(&fas.Spec.Policy) 1094 return &fas 1095 } 1096 testCases := map[string]struct { 1097 fas *autoscalingv1.FleetAutoscaler 1098 wantFasErr bool 1099 wantReplicas int32 1100 }{ 1101 "Scale Up to MinCapacity": { 1102 fas: counterFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1103 fap.Counter = &autoscalingv1.CounterPolicy{ 1104 Key: "games", 1105 BufferSize: intstr.FromInt(3), 1106 MinCapacity: 16, 1107 MaxCapacity: 100, 1108 } 1109 }), 1110 wantFasErr: false, 1111 wantReplicas: 4, // Capacity:20 1112 }, 1113 } 1114 for name, testCase := range testCases { 1115 t.Run(name, func(t *testing.T) { 1116 1117 fas, err := fleetautoscalers.Create(ctx, testCase.fas, metav1.CreateOptions{}) 1118 if testCase.wantFasErr { 1119 assert.Error(t, err) 1120 return 1121 } 1122 assert.NoError(t, err) 1123 1124 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(testCase.wantReplicas)) 1125 fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1126 1127 // Return to starting 0 replicas 1128 framework.ScaleFleet(t, log, flt, 0) 1129 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(0)) 1130 }) 1131 } 1132 } 1133 1134 func TestCounterAutoscalerAllocated(t *testing.T) { 1135 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 1136 t.SkipNow() 1137 } 1138 t.Parallel() 1139 1140 ctx := context.Background() 1141 client := framework.AgonesClient.AgonesV1() 1142 log := e2e.TestLogger(t) 1143 1144 defaultFlt := defaultFleet(framework.Namespace) 1145 defaultFlt.Spec.Template.Spec.Counters = map[string]agonesv1.CounterStatus{ 1146 "players": { 1147 Count: 7, // AggregateCount 21 1148 Capacity: 10, // AggregateCapacity 30 1149 }, 1150 } 1151 1152 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 1153 1154 testCases := map[string]struct { 1155 fas autoscalingv1.CounterPolicy 1156 wantAllocatedGs int32 // Must be >= 0 && <= 3 1157 wantReadyGs int32 1158 }{ 1159 "Scale Down Buffer Percent": { 1160 fas: autoscalingv1.CounterPolicy{ 1161 Key: "players", 1162 BufferSize: intstr.FromString("5%"), 1163 MinCapacity: 10, 1164 MaxCapacity: 100, 1165 }, 1166 wantAllocatedGs: 0, 1167 wantReadyGs: 1, 1168 }, 1169 "Scale Up Buffer Percent": { 1170 fas: autoscalingv1.CounterPolicy{ 1171 Key: "players", 1172 BufferSize: intstr.FromString("40%"), 1173 MinCapacity: 10, 1174 MaxCapacity: 100, 1175 }, 1176 wantAllocatedGs: 3, 1177 wantReadyGs: 2, 1178 }, 1179 } 1180 // nolint:dupl // Linter errors on lines are duplicate of TestListAutoscalerAllocated 1181 for name, testCase := range testCases { 1182 t.Run(name, func(t *testing.T) { 1183 flt, err := client.Fleets(framework.Namespace).Create(ctx, defaultFlt.DeepCopy(), metav1.CreateOptions{}) 1184 require.NoError(t, err) 1185 defer client.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1186 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 1187 1188 gsa := allocationv1.GameServerAllocation{ 1189 Spec: allocationv1.GameServerAllocationSpec{ 1190 Selectors: []allocationv1.GameServerSelector{ 1191 {LabelSelector: metav1.LabelSelector{ 1192 MatchLabels: map[string]string{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}}}, 1193 }}} 1194 1195 // Allocate game servers, as Buffer Percent scales up (or down) based on allocated aggregate capacity 1196 for i := int32(0); i < testCase.wantAllocatedGs; i++ { 1197 _, err := framework.AgonesClient.AllocationV1().GameServerAllocations(flt.ObjectMeta.Namespace).Create(ctx, gsa.DeepCopy(), metav1.CreateOptions{}) 1198 require.NoError(t, err) 1199 } 1200 framework.AssertFleetCondition(t, flt, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 1201 log.WithField("fleet", fmt.Sprintf("%+v", fleet.Status)).Info("Checking for game server allocations") 1202 return fleet.Status.AllocatedReplicas == testCase.wantAllocatedGs 1203 }) 1204 1205 counterFas := &autoscalingv1.FleetAutoscaler{ 1206 ObjectMeta: metav1.ObjectMeta{Name: flt.ObjectMeta.Name + "-counter-autoscaler", Namespace: framework.Namespace}, 1207 Spec: autoscalingv1.FleetAutoscalerSpec{ 1208 FleetName: flt.ObjectMeta.Name, 1209 Policy: autoscalingv1.FleetAutoscalerPolicy{ 1210 Type: autoscalingv1.CounterPolicyType, 1211 Counter: &testCase.fas, 1212 }, 1213 Sync: &autoscalingv1.FleetAutoscalerSync{ 1214 Type: autoscalingv1.FixedIntervalSyncType, 1215 FixedInterval: autoscalingv1.FixedIntervalSync{ 1216 Seconds: 1, 1217 }, 1218 }, 1219 }, 1220 } 1221 1222 fas, err := fleetautoscalers.Create(ctx, counterFas, metav1.CreateOptions{}) 1223 assert.NoError(t, err) 1224 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1225 1226 framework.AssertFleetCondition(t, flt, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 1227 return fleet.Status.AllocatedReplicas == testCase.wantAllocatedGs && fleet.Status.ReadyReplicas == testCase.wantReadyGs 1228 }) 1229 }) 1230 } 1231 } 1232 1233 // Related to the issue about the fleet autoscaler policy not namespaced: https://github.com/googleforgames/agones/issues/3954 1234 func TestCounterAutoscalerAllocatedMultipleNamespaces(t *testing.T) { 1235 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 1236 t.SkipNow() 1237 } 1238 1239 ctx := context.Background() 1240 client := framework.AgonesClient.AgonesV1() 1241 log := e2e.TestLogger(t) 1242 1243 // Create namespaces A and B 1244 namespaceA := framework.Namespace // let's reuse an existing one 1245 helper.CopyDefaultAllocatorClientSecret(ctx, t, namespaceA, framework) 1246 1247 namespaceB := fmt.Sprintf("autoscaler-b-%s", uuid.NewUUID()) 1248 err := framework.CreateNamespace(namespaceB) 1249 require.NoError(t, err) 1250 defer func() { 1251 if derr := framework.DeleteNamespace(namespaceB); derr != nil { 1252 t.Error(derr) 1253 } 1254 }() 1255 1256 // Init default fleet A and B with same name in namespace A and B 1257 defaultFltA := defaultFleet(namespaceA) 1258 defaultFltA.ObjectMeta.Name = "simple-fleet" 1259 defaultFltA.Spec.Template.Spec.Counters = map[string]agonesv1.CounterStatus{ 1260 "players": { 1261 Count: 7, // AggregateCount 21 1262 Capacity: 10, // AggregateCapacity 30 1263 }, 1264 } 1265 defaultFltB := defaultFleet(namespaceB) 1266 defaultFltB.ObjectMeta.Name = "simple-fleet" 1267 defaultFltB.Spec.Template.Spec.Counters = map[string]agonesv1.CounterStatus{ 1268 "players": { 1269 Count: 7, // AggregateCount 21 1270 Capacity: 20, // AggregateCapacity 60 1271 }, 1272 } 1273 1274 fleetautoscalers := framework.AgonesClient.AutoscalingV1() 1275 1276 testCases := map[string]struct { 1277 fasA autoscalingv1.CounterPolicy 1278 fasB autoscalingv1.CounterPolicy 1279 wantAllocatedGsA int32 // Must be >= 0 && <= 3 1280 wantReadyGsA int32 1281 wantAllocatedGsB int32 // Must be >= 0 && <= 3 1282 wantReadyGsB int32 1283 }{ 1284 "Scale Down Buffer Percent from different namespaces with same fleet name": { 1285 fasA: autoscalingv1.CounterPolicy{ 1286 Key: "players", 1287 BufferSize: intstr.FromString("5%"), 1288 MinCapacity: 10, 1289 MaxCapacity: 100, 1290 }, 1291 fasB: autoscalingv1.CounterPolicy{ 1292 Key: "players", 1293 BufferSize: intstr.FromString("5%"), 1294 MinCapacity: 10, 1295 MaxCapacity: 100, 1296 }, 1297 wantAllocatedGsA: 3, 1298 wantReadyGsA: 0, 1299 wantAllocatedGsB: 2, 1300 wantReadyGsB: 0, 1301 }, 1302 } 1303 1304 //nolint:dupl // Linter errors on lines are duplicate of TestListAutoscalerAllocated 1305 for name, testCase := range testCases { 1306 t.Run(name, func(t *testing.T) { 1307 // Create both fleet A and B 1308 fltA, err := client.Fleets(namespaceA).Create(ctx, defaultFltA.DeepCopy(), metav1.CreateOptions{}) 1309 require.NoError(t, err) 1310 defer client.Fleets(namespaceA).Delete(ctx, fltA.ObjectMeta.Name, metav1.DeleteOptions{}) //nolint:errcheck 1311 framework.AssertFleetCondition(t, fltA, e2e.FleetReadyCount(fltA.Spec.Replicas)) 1312 1313 fltB, err := client.Fleets(namespaceB).Create(ctx, defaultFltB.DeepCopy(), metav1.CreateOptions{}) 1314 require.NoError(t, err) 1315 defer client.Fleets(namespaceB).Delete(ctx, fltB.ObjectMeta.Name, metav1.DeleteOptions{}) //nolint:errcheck 1316 framework.AssertFleetCondition(t, fltB, e2e.FleetReadyCount(fltB.Spec.Replicas)) 1317 1318 // Allocate gameservers in A and B 1319 gsaA := allocationv1.GameServerAllocation{ 1320 Spec: allocationv1.GameServerAllocationSpec{ 1321 Selectors: []allocationv1.GameServerSelector{ 1322 {LabelSelector: metav1.LabelSelector{ 1323 MatchLabels: map[string]string{agonesv1.FleetNameLabel: fltA.ObjectMeta.Name}}}, 1324 }}} 1325 1326 // Allocate game servers, as Buffer Percent scales up (or down) based on allocated aggregate capacity 1327 for i := int32(0); i < testCase.wantAllocatedGsA; i++ { 1328 _, err := framework.AgonesClient.AllocationV1().GameServerAllocations(fltA.ObjectMeta.Namespace).Create(ctx, gsaA.DeepCopy(), metav1.CreateOptions{}) 1329 require.NoError(t, err) 1330 } 1331 framework.AssertFleetCondition(t, fltA, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 1332 log.WithField("fleet", fmt.Sprintf("%+v", fleet.Status)).Info("Checking for game server allocations") 1333 return fleet.Status.AllocatedReplicas == testCase.wantAllocatedGsA 1334 }) 1335 1336 gsaB := allocationv1.GameServerAllocation{ 1337 Spec: allocationv1.GameServerAllocationSpec{ 1338 Selectors: []allocationv1.GameServerSelector{ 1339 {LabelSelector: metav1.LabelSelector{ 1340 MatchLabels: map[string]string{agonesv1.FleetNameLabel: fltB.ObjectMeta.Name}}}, 1341 }}} 1342 1343 // Allocate game servers, as Buffer Percent scales up (or down) based on allocated aggregate capacity 1344 for i := int32(0); i < testCase.wantAllocatedGsB; i++ { 1345 _, err := framework.AgonesClient.AllocationV1().GameServerAllocations(fltB.ObjectMeta.Namespace).Create(ctx, gsaB.DeepCopy(), metav1.CreateOptions{}) 1346 require.NoError(t, err) 1347 } 1348 framework.AssertFleetCondition(t, fltB, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 1349 log.WithField("fleet", fmt.Sprintf("%+v", fleet.Status)).Info("Checking for game server allocations") 1350 return fleet.Status.AllocatedReplicas == testCase.wantAllocatedGsB 1351 }) 1352 1353 // Create fleetautoscaler for A and B 1354 counterFasA := &autoscalingv1.FleetAutoscaler{ 1355 ObjectMeta: metav1.ObjectMeta{Name: fltA.ObjectMeta.Name + "-counter-autoscaler", Namespace: namespaceA}, 1356 Spec: autoscalingv1.FleetAutoscalerSpec{ 1357 FleetName: fltA.ObjectMeta.Name, 1358 Policy: autoscalingv1.FleetAutoscalerPolicy{ 1359 Type: autoscalingv1.CounterPolicyType, 1360 Counter: &testCase.fasA, 1361 }, 1362 Sync: &autoscalingv1.FleetAutoscalerSync{ 1363 Type: autoscalingv1.FixedIntervalSyncType, 1364 FixedInterval: autoscalingv1.FixedIntervalSync{ 1365 Seconds: 1, 1366 }, 1367 }, 1368 }, 1369 } 1370 1371 fasA, err := fleetautoscalers.FleetAutoscalers(namespaceA).Create(ctx, counterFasA, metav1.CreateOptions{}) 1372 assert.NoError(t, err) 1373 defer fleetautoscalers.FleetAutoscalers(namespaceA).Delete(ctx, fasA.ObjectMeta.Name, metav1.DeleteOptions{}) //nolint:errcheck 1374 1375 counterFasB := &autoscalingv1.FleetAutoscaler{ 1376 ObjectMeta: metav1.ObjectMeta{Name: fltB.ObjectMeta.Name + "-counter-autoscaler", Namespace: namespaceB}, 1377 Spec: autoscalingv1.FleetAutoscalerSpec{ 1378 FleetName: fltB.ObjectMeta.Name, 1379 Policy: autoscalingv1.FleetAutoscalerPolicy{ 1380 Type: autoscalingv1.CounterPolicyType, 1381 Counter: &testCase.fasB, 1382 }, 1383 Sync: &autoscalingv1.FleetAutoscalerSync{ 1384 Type: autoscalingv1.FixedIntervalSyncType, 1385 FixedInterval: autoscalingv1.FixedIntervalSync{ 1386 Seconds: 1, 1387 }, 1388 }, 1389 }, 1390 } 1391 1392 fasB, err := fleetautoscalers.FleetAutoscalers(namespaceB).Create(ctx, counterFasB, metav1.CreateOptions{}) 1393 assert.NoError(t, err) 1394 defer fleetautoscalers.FleetAutoscalers(namespaceB).Delete(ctx, fasB.ObjectMeta.Name, metav1.DeleteOptions{}) //nolint:errcheck 1395 1396 // Ensure there is no warning / error on creation, ensure the currentReplicas > 0 for A and B 1397 framework.WaitForFleetAutoScalerCondition(t, fasA, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 1398 log.WithField("fleet autoscaler", fmt.Sprintf("%+v", fas.Status)).Info("Checking for fleet autoscaler") 1399 return fas.Status.CurrentReplicas > 0 1400 }) 1401 1402 framework.WaitForFleetAutoScalerCondition(t, fasB, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 1403 log.WithField("fleet autoscaler", fmt.Sprintf("%+v", fas.Status)).Info("Checking for fleet autoscaler") 1404 return fas.Status.CurrentReplicas > 0 1405 }) 1406 1407 // Wait for LastAppliedPolicy to be set to CounterPolicyType for fasA 1408 framework.WaitForFleetAutoScalerCondition(t, fasA, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 1409 log.WithField("LastAppliedPolicy", fas.Status.LastAppliedPolicy). 1410 Info("Waiting for LastAppliedPolicy to be set to CounterPolicyType") 1411 return fas.Status.LastAppliedPolicy == autoscalingv1.CounterPolicyType 1412 }) 1413 1414 // Wait for LastAppliedPolicy to be set to CounterPolicyType for fasB 1415 framework.WaitForFleetAutoScalerCondition(t, fasB, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 1416 log.WithField("LastAppliedPolicy", fas.Status.LastAppliedPolicy). 1417 Info("Waiting for LastAppliedPolicy to be set to CounterPolicyType") 1418 return fas.Status.LastAppliedPolicy == autoscalingv1.CounterPolicyType 1419 }) 1420 1421 // Ensure the allocated and ready replicas are correct for A and B 1422 framework.AssertFleetCondition(t, fltA, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 1423 return fleet.Status.AllocatedReplicas == testCase.wantAllocatedGsA && fleet.Status.ReadyReplicas == testCase.wantReadyGsA 1424 }) 1425 framework.AssertFleetCondition(t, fltB, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 1426 return fleet.Status.AllocatedReplicas == testCase.wantAllocatedGsB && fleet.Status.ReadyReplicas == testCase.wantReadyGsB 1427 }) 1428 }) 1429 } 1430 } 1431 1432 func TestListAutoscaler(t *testing.T) { 1433 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 1434 t.SkipNow() 1435 } 1436 t.Parallel() 1437 1438 ctx := context.Background() 1439 client := framework.AgonesClient.AgonesV1() 1440 log := e2e.TestLogger(t) 1441 1442 flt := defaultFleet(framework.Namespace) 1443 flt.Spec.Template.Spec.Lists = map[string]agonesv1.ListStatus{ 1444 "games": { 1445 Values: []string{"game1", "game2", "game3"}, // AggregateCount 9 1446 Capacity: 5, // AggregateCapacity 15 1447 }, 1448 } 1449 1450 flt, err := client.Fleets(framework.Namespace).Create(ctx, flt.DeepCopy(), metav1.CreateOptions{}) 1451 require.NoError(t, err) 1452 defer client.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1453 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 1454 1455 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 1456 1457 listFas := func(f func(fap *autoscalingv1.FleetAutoscalerPolicy)) *autoscalingv1.FleetAutoscaler { 1458 fas := autoscalingv1.FleetAutoscaler{ 1459 ObjectMeta: metav1.ObjectMeta{Name: flt.ObjectMeta.Name + "-list-autoscaler", Namespace: framework.Namespace}, 1460 Spec: autoscalingv1.FleetAutoscalerSpec{ 1461 FleetName: flt.ObjectMeta.Name, 1462 Policy: autoscalingv1.FleetAutoscalerPolicy{ 1463 Type: autoscalingv1.ListPolicyType, 1464 }, 1465 Sync: &autoscalingv1.FleetAutoscalerSync{ 1466 Type: autoscalingv1.FixedIntervalSyncType, 1467 FixedInterval: autoscalingv1.FixedIntervalSync{ 1468 Seconds: 1, 1469 }, 1470 }, 1471 }, 1472 } 1473 f(&fas.Spec.Policy) 1474 return &fas 1475 } 1476 testCases := map[string]struct { 1477 fas *autoscalingv1.FleetAutoscaler 1478 wantFasErr bool 1479 wantReplicas int32 1480 }{ 1481 "Scale Down to Minimum 1 Replica": { 1482 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1483 fap.List = &autoscalingv1.ListPolicy{ 1484 Key: "games", 1485 BufferSize: intstr.FromInt(2), 1486 MinCapacity: 0, 1487 MaxCapacity: 3, 1488 } 1489 }), 1490 wantFasErr: false, 1491 wantReplicas: 1, // Count:3 Capacity:5 1492 }, 1493 "Scale Down to Buffer": { 1494 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1495 fap.List = &autoscalingv1.ListPolicy{ 1496 Key: "games", 1497 BufferSize: intstr.FromInt(3), 1498 MinCapacity: 0, 1499 MaxCapacity: 5, 1500 } 1501 }), 1502 wantFasErr: false, 1503 wantReplicas: 2, // Count:6 Capacity:10 1504 }, 1505 "MinCapacity Must Be Greater Than Zero Percentage Buffer": { 1506 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1507 fap.List = &autoscalingv1.ListPolicy{ 1508 Key: "games", 1509 BufferSize: intstr.FromString("50%"), 1510 MinCapacity: 0, 1511 MaxCapacity: 100, 1512 } 1513 }), 1514 wantFasErr: true, 1515 wantReplicas: 3, 1516 }, 1517 "Scale Up to MinCapacity": { 1518 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1519 fap.List = &autoscalingv1.ListPolicy{ 1520 Key: "games", 1521 BufferSize: intstr.FromInt(3), 1522 MinCapacity: 16, 1523 MaxCapacity: 100, 1524 } 1525 }), 1526 wantFasErr: false, 1527 wantReplicas: 4, // Count:12 Capacity:20 1528 }, 1529 "Scale Down to MinCapacity": { 1530 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1531 fap.List = &autoscalingv1.ListPolicy{ 1532 Key: "games", 1533 BufferSize: intstr.FromInt(1), 1534 MinCapacity: 10, 1535 MaxCapacity: 100, 1536 } 1537 }), 1538 wantFasErr: false, 1539 wantReplicas: 2, // Count:6 Capacity:10 1540 }, 1541 "MinCapacity Less Than Buffer Invalid": { 1542 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1543 fap.List = &autoscalingv1.ListPolicy{ 1544 Key: "games", 1545 BufferSize: intstr.FromInt(15), 1546 MinCapacity: 5, 1547 MaxCapacity: 25, 1548 } 1549 }), 1550 wantFasErr: true, 1551 wantReplicas: 3, 1552 }, 1553 "Scale Up to Buffer": { 1554 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1555 fap.List = &autoscalingv1.ListPolicy{ 1556 Key: "games", 1557 BufferSize: intstr.FromInt(15), 1558 MinCapacity: 15, 1559 MaxCapacity: 100, 1560 } 1561 }), 1562 wantFasErr: false, 1563 wantReplicas: 8, // Count:24 Capacity:40 1564 }, 1565 "Scale Up to MaxCapacity": { 1566 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1567 fap.List = &autoscalingv1.ListPolicy{ 1568 Key: "games", 1569 BufferSize: intstr.FromInt(15), 1570 MinCapacity: 15, 1571 MaxCapacity: 25, 1572 } 1573 }), 1574 wantFasErr: false, 1575 wantReplicas: 5, // Count:15 Capacity:25 1576 }, 1577 } 1578 for name, testCase := range testCases { 1579 t.Run(name, func(t *testing.T) { 1580 1581 fas, err := fleetautoscalers.Create(ctx, testCase.fas, metav1.CreateOptions{}) 1582 if testCase.wantFasErr { 1583 assert.Error(t, err) 1584 return 1585 } 1586 assert.NoError(t, err) 1587 1588 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(testCase.wantReplicas)) 1589 fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1590 1591 // Return to starting 3 replicas 1592 framework.ScaleFleet(t, log, flt, 3) 1593 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(3)) 1594 }) 1595 } 1596 } 1597 1598 // nolint:dupl // Linter errors on lines are duplicate of TestCounterAutoscalerWithNoReplicas 1599 func TestListAutoscalerWithNoReplicas(t *testing.T) { 1600 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 1601 t.SkipNow() 1602 } 1603 t.Parallel() 1604 1605 ctx := context.Background() 1606 client := framework.AgonesClient.AgonesV1() 1607 log := e2e.TestLogger(t) 1608 1609 flt := defaultEmptyFleet(framework.Namespace) 1610 flt.Spec.Template.Spec.Lists = map[string]agonesv1.ListStatus{ 1611 "games": { 1612 Capacity: 5, 1613 }, 1614 } 1615 1616 flt, err := client.Fleets(framework.Namespace).Create(ctx, flt.DeepCopy(), metav1.CreateOptions{}) 1617 require.NoError(t, err) 1618 defer client.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1619 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 1620 1621 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 1622 1623 listFas := func(f func(fap *autoscalingv1.FleetAutoscalerPolicy)) *autoscalingv1.FleetAutoscaler { 1624 fas := autoscalingv1.FleetAutoscaler{ 1625 ObjectMeta: metav1.ObjectMeta{Name: flt.ObjectMeta.Name + "-list-autoscaler", Namespace: framework.Namespace}, 1626 Spec: autoscalingv1.FleetAutoscalerSpec{ 1627 FleetName: flt.ObjectMeta.Name, 1628 Policy: autoscalingv1.FleetAutoscalerPolicy{ 1629 Type: autoscalingv1.ListPolicyType, 1630 }, 1631 Sync: &autoscalingv1.FleetAutoscalerSync{ 1632 Type: autoscalingv1.FixedIntervalSyncType, 1633 FixedInterval: autoscalingv1.FixedIntervalSync{ 1634 Seconds: 1, 1635 }, 1636 }, 1637 }, 1638 } 1639 f(&fas.Spec.Policy) 1640 return &fas 1641 } 1642 testCases := map[string]struct { 1643 fas *autoscalingv1.FleetAutoscaler 1644 wantFasErr bool 1645 wantReplicas int32 1646 }{ 1647 "Scale Up to MinCapacity": { 1648 fas: listFas(func(fap *autoscalingv1.FleetAutoscalerPolicy) { 1649 fap.List = &autoscalingv1.ListPolicy{ 1650 Key: "games", 1651 BufferSize: intstr.FromInt(3), 1652 MinCapacity: 16, 1653 MaxCapacity: 100, 1654 } 1655 }), 1656 wantFasErr: false, 1657 wantReplicas: 4, // Capacity:20 1658 }, 1659 } 1660 for name, testCase := range testCases { 1661 t.Run(name, func(t *testing.T) { 1662 1663 fas, err := fleetautoscalers.Create(ctx, testCase.fas, metav1.CreateOptions{}) 1664 if testCase.wantFasErr { 1665 assert.Error(t, err) 1666 return 1667 } 1668 assert.NoError(t, err) 1669 1670 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(testCase.wantReplicas)) 1671 fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1672 1673 // Return to starting 0 replicas 1674 framework.ScaleFleet(t, log, flt, 0) 1675 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(0)) 1676 }) 1677 } 1678 } 1679 1680 func TestListAutoscalerAllocated(t *testing.T) { 1681 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 1682 t.SkipNow() 1683 } 1684 t.Parallel() 1685 1686 ctx := context.Background() 1687 client := framework.AgonesClient.AgonesV1() 1688 log := e2e.TestLogger(t) 1689 1690 defaultFlt := defaultFleet(framework.Namespace) 1691 defaultFlt.Spec.Template.Spec.Lists = map[string]agonesv1.ListStatus{ 1692 "gamers": { 1693 Values: []string{"gamer5", "gamer6"}, 1694 Capacity: 6, // AggregateCapacity 18 1695 }, 1696 } 1697 1698 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 1699 1700 testCases := map[string]struct { 1701 fas autoscalingv1.ListPolicy 1702 wantAllocatedGs int32 // Must be >= 0 && <= 3 1703 wantReadyGs int32 1704 wantSecondAllocation int32 // Must be <= wantReadyGs 1705 wantSecondReady int32 1706 }{ 1707 "Scale Down Buffer Percent": { 1708 fas: autoscalingv1.ListPolicy{ 1709 Key: "gamers", 1710 BufferSize: intstr.FromString("50%"), 1711 MinCapacity: 6, 1712 MaxCapacity: 60, 1713 }, 1714 wantAllocatedGs: 0, 1715 wantReadyGs: 1, 1716 }, 1717 "Scale Up Buffer Percent": { 1718 fas: autoscalingv1.ListPolicy{ 1719 Key: "gamers", 1720 BufferSize: intstr.FromString("50%"), 1721 MinCapacity: 6, 1722 MaxCapacity: 60, 1723 }, 1724 wantAllocatedGs: 3, 1725 wantReadyGs: 2, 1726 }, 1727 "Scales Down to Number of Game Servers Allocated": { 1728 fas: autoscalingv1.ListPolicy{ 1729 Key: "gamers", 1730 BufferSize: intstr.FromInt(2), 1731 MinCapacity: 6, 1732 MaxCapacity: 60, 1733 }, 1734 wantAllocatedGs: 2, 1735 wantReadyGs: 0, 1736 }, 1737 } 1738 // nolint:dupl // Linter errors on lines are duplicate of TestCounterAutoscalerAllocated 1739 for name, testCase := range testCases { 1740 t.Run(name, func(t *testing.T) { 1741 flt, err := client.Fleets(framework.Namespace).Create(ctx, defaultFlt.DeepCopy(), metav1.CreateOptions{}) 1742 require.NoError(t, err) 1743 defer client.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1744 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 1745 1746 // Adds 4 gamers to each allocated gameserver, and removes 2 existing gamers. 1747 gsa := allocationv1.GameServerAllocation{ 1748 Spec: allocationv1.GameServerAllocationSpec{ 1749 Selectors: []allocationv1.GameServerSelector{ 1750 {LabelSelector: metav1.LabelSelector{ 1751 MatchLabels: map[string]string{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}}}, 1752 }, 1753 Lists: map[string]allocationv1.ListAction{ 1754 "gamers": { 1755 AddValues: []string{"gamer1", "gamer2", "gamer3", "gamer4"}, 1756 DeleteValues: []string{"gamer5", "gamer6"}, 1757 }}}} 1758 1759 // Allocate game servers, as Buffer Percent scales up (or down) based on allocated aggregate capacity 1760 for i := int32(0); i < testCase.wantAllocatedGs; i++ { 1761 _, err := framework.AgonesClient.AllocationV1().GameServerAllocations(flt.ObjectMeta.Namespace).Create(ctx, gsa.DeepCopy(), metav1.CreateOptions{}) 1762 require.NoError(t, err) 1763 } 1764 framework.AssertFleetCondition(t, flt, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 1765 log.WithField("fleet", fmt.Sprintf("%+v", fleet.Status)).Info("Checking for game server allocations") 1766 return fleet.Status.AllocatedReplicas == testCase.wantAllocatedGs 1767 }) 1768 1769 listFas := &autoscalingv1.FleetAutoscaler{ 1770 ObjectMeta: metav1.ObjectMeta{Name: flt.ObjectMeta.Name + "-list-autoscaler", Namespace: framework.Namespace}, 1771 Spec: autoscalingv1.FleetAutoscalerSpec{ 1772 FleetName: flt.ObjectMeta.Name, 1773 Policy: autoscalingv1.FleetAutoscalerPolicy{ 1774 Type: autoscalingv1.ListPolicyType, 1775 List: &testCase.fas, 1776 }, 1777 Sync: &autoscalingv1.FleetAutoscalerSync{ 1778 Type: autoscalingv1.FixedIntervalSyncType, 1779 FixedInterval: autoscalingv1.FixedIntervalSync{ 1780 Seconds: 1, 1781 }, 1782 }, 1783 }, 1784 } 1785 1786 fas, err := fleetautoscalers.Create(ctx, listFas, metav1.CreateOptions{}) 1787 assert.NoError(t, err) 1788 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1789 1790 framework.AssertFleetCondition(t, flt, func(_ *logrus.Entry, fleet *agonesv1.Fleet) bool { 1791 return fleet.Status.AllocatedReplicas == testCase.wantAllocatedGs && fleet.Status.ReadyReplicas == testCase.wantReadyGs 1792 }) 1793 }) 1794 } 1795 } 1796 1797 func TestListAutoscalerWithSDKMethods(t *testing.T) { 1798 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 1799 t.SkipNow() 1800 } 1801 t.Parallel() 1802 1803 ctx := context.Background() 1804 client := framework.AgonesClient.AgonesV1() 1805 1806 defaultFlt := defaultFleet(framework.Namespace) 1807 defaultFlt.Spec.Template.Spec.Lists = map[string]agonesv1.ListStatus{ 1808 "sessions": { 1809 Values: []string{"session1", "session2"}, // AggregateCount 6 1810 Capacity: 4, // AggregateCapacity 12 1811 }, 1812 } 1813 1814 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 1815 1816 testCases := map[string]struct { 1817 fas autoscalingv1.ListPolicy 1818 order string // Priority order Ascending or Descending for fleet ready replica deletion 1819 msg string // See agones/examples/simple-game-server/README for list of commands 1820 startReplicas int32 // After applying autoscaler policy but before sending update message 1821 wantReplicas int32 // After applying autoscaler policy and sending update message 1822 }{ 1823 "Scale Up to Buffer": { 1824 fas: autoscalingv1.ListPolicy{ 1825 Key: "sessions", 1826 BufferSize: intstr.FromInt(10), 1827 MinCapacity: 12, 1828 MaxCapacity: 400, 1829 }, 1830 order: agonesv1.GameServerPriorityDescending, 1831 msg: "APPEND_LIST_VALUE sessions session0", 1832 startReplicas: 5, 1833 wantReplicas: 6, 1834 }, 1835 "Scale Down to Buffer": { 1836 fas: autoscalingv1.ListPolicy{ 1837 Key: "sessions", 1838 BufferSize: intstr.FromInt(3), 1839 MinCapacity: 3, 1840 MaxCapacity: 400, 1841 }, 1842 msg: "DELETE_LIST_VALUE sessions session1", 1843 order: agonesv1.GameServerPriorityAscending, 1844 startReplicas: 2, 1845 wantReplicas: 1, 1846 }, 1847 } 1848 for name, testCase := range testCases { 1849 t.Run(name, func(t *testing.T) { 1850 defaultFlt.Spec.Priorities = []agonesv1.Priority{ 1851 { 1852 Type: agonesv1.GameServerPriorityList, 1853 Key: "sessions", 1854 Order: testCase.order, 1855 }, 1856 } 1857 flt, err := client.Fleets(framework.Namespace).Create(ctx, defaultFlt.DeepCopy(), metav1.CreateOptions{}) 1858 require.NoError(t, err) 1859 defer client.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1860 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 1861 1862 listFas := &autoscalingv1.FleetAutoscaler{ 1863 ObjectMeta: metav1.ObjectMeta{Name: flt.ObjectMeta.Name + "-list-autoscaler", Namespace: framework.Namespace}, 1864 Spec: autoscalingv1.FleetAutoscalerSpec{ 1865 FleetName: flt.ObjectMeta.Name, 1866 Policy: autoscalingv1.FleetAutoscalerPolicy{ 1867 Type: autoscalingv1.ListPolicyType, 1868 List: &testCase.fas, 1869 }, 1870 Sync: &autoscalingv1.FleetAutoscalerSync{ 1871 Type: autoscalingv1.FixedIntervalSyncType, 1872 FixedInterval: autoscalingv1.FixedIntervalSync{ 1873 Seconds: 1, 1874 }, 1875 }, 1876 }, 1877 } 1878 1879 fas, err := fleetautoscalers.Create(ctx, listFas, metav1.CreateOptions{}) 1880 assert.NoError(t, err) 1881 defer fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1882 1883 // Wait until autoscaler has first re-sized before getting the list of gameservers 1884 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(testCase.startReplicas)) 1885 1886 gameservers, err := framework.ListGameServersFromFleet(flt) 1887 assert.NoError(t, err) 1888 1889 gs := &gameservers[1] 1890 logrus.WithField("command", testCase.msg).WithField("gs", gs.ObjectMeta.Name).Info(name) 1891 _, err = framework.SendGameServerUDP(t, gs, testCase.msg) 1892 require.NoError(t, err) 1893 1894 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(testCase.wantReplicas)) 1895 }) 1896 } 1897 } 1898 1899 func TestScheduleAutoscaler(t *testing.T) { 1900 if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) { 1901 t.SkipNow() 1902 } 1903 t.Parallel() 1904 ctx := context.Background() 1905 log := e2e.TestLogger(t) 1906 1907 stable := framework.AgonesClient.AgonesV1() 1908 fleets := stable.Fleets(framework.Namespace) 1909 flt, err := fleets.Create(ctx, defaultFleet(framework.Namespace), metav1.CreateOptions{}) 1910 require.NoError(t, err) 1911 defer fleets.Delete(context.Background(), flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1912 1913 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 1914 1915 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 1916 1917 // Active Cron Schedule (e.g. run after 1 * * * *, which is the after the first minute of the hour) 1918 scheduleAutoscaler := defaultAutoscalerSchedule(t, flt) 1919 scheduleAutoscaler.Spec.Policy.Schedule.ActivePeriod.StartCron = nextCronMinute(time.Now()) 1920 fas, err := fleetautoscalers.Create(ctx, scheduleAutoscaler, metav1.CreateOptions{}) 1921 require.NoError(t, err) 1922 1923 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(5)) 1924 fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1925 1926 // Return to starting 3 replicas 1927 framework.ScaleFleet(t, log, flt, 3) 1928 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(3)) 1929 1930 // Between Active Period Cron Schedule (e.g. run between 1-2 * * * *, which is between the first minute and second minute of the hour) 1931 scheduleAutoscaler = defaultAutoscalerSchedule(t, flt) 1932 scheduleAutoscaler.Spec.Policy.Schedule.ActivePeriod.StartCron = nextCronMinuteBetween(time.Now()) 1933 fas, err = fleetautoscalers.Create(ctx, scheduleAutoscaler, metav1.CreateOptions{}) 1934 require.NoError(t, err) 1935 1936 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(5)) 1937 fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1938 } 1939 1940 func TestChainAutoscaler(t *testing.T) { 1941 if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) { 1942 t.SkipNow() 1943 } 1944 t.Parallel() 1945 ctx := context.Background() 1946 log := e2e.TestLogger(t) 1947 1948 stable := framework.AgonesClient.AgonesV1() 1949 fleets := stable.Fleets(framework.Namespace) 1950 flt, err := fleets.Create(ctx, defaultFleet(framework.Namespace), metav1.CreateOptions{}) 1951 if assert.NoError(t, err) { 1952 defer fleets.Delete(context.Background(), flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1953 } 1954 1955 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 1956 1957 fleetautoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 1958 1959 // 1st Schedule Inactive, 2nd Schedule Active - 30 seconds (Fallthrough) 1960 chainAutoscaler := defaultAutoscalerChain(t, flt) 1961 fas, err := fleetautoscalers.Create(ctx, chainAutoscaler, metav1.CreateOptions{}) 1962 assert.NoError(t, err) 1963 1964 // Verify only the second schedule ran 1965 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(4)) 1966 expectedChainPolicy := autoscalingv1.FleetAutoscalerPolicyType(fmt.Sprintf("%s:%s:%s", autoscalingv1.ChainPolicyType, "schedule-2", autoscalingv1.SchedulePolicyType)) 1967 framework.WaitForFleetAutoScalerCondition(t, chainAutoscaler, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 1968 log.WithField("LastAppliedPolicy", fas.Status.LastAppliedPolicy).Info("Awaiting application of expected SchedulePolicyType in chain autoscaler") 1969 return fas.Status.LastAppliedPolicy == expectedChainPolicy 1970 }) 1971 1972 fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 1973 1974 // Return to starting 3 replicas 1975 framework.ScaleFleet(t, log, flt, 3) 1976 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(3)) 1977 1978 // 2 Active Schedules back to back - 1 minute (Fallthrough) 1979 chainAutoscaler = defaultAutoscalerChain(t, flt) 1980 currentTime := time.Now() 1981 1982 // First schedule runs for 1 minute 1983 chainAutoscaler.Spec.Policy.Chain[0].Schedule.ActivePeriod.StartCron = nextCronMinute(currentTime) 1984 chainAutoscaler.Spec.Policy.Chain[0].Schedule.ActivePeriod.Duration = "1m" 1985 1986 // Second schedule runs 1 minute after the first schedule 1987 oneMinute := mustParseDuration(t, "1m") 1988 chainAutoscaler.Spec.Policy.Chain[0].Schedule.ActivePeriod.StartCron = nextCronMinute(currentTime.Add(oneMinute)) 1989 chainAutoscaler.Spec.Policy.Chain[1].Schedule.ActivePeriod.Duration = "5m" 1990 1991 fas, err = fleetautoscalers.Create(ctx, chainAutoscaler, metav1.CreateOptions{}) 1992 assert.NoError(t, err) 1993 1994 // Verify the first schedule has been applied 1995 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(10)) 1996 expectedChainPolicy = autoscalingv1.FleetAutoscalerPolicyType(fmt.Sprintf("%s:%s:%s", autoscalingv1.ChainPolicyType, "schedule-1", autoscalingv1.SchedulePolicyType)) 1997 framework.WaitForFleetAutoScalerCondition(t, chainAutoscaler, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 1998 log.WithField("LastAppliedPolicy", fas.Status.LastAppliedPolicy).Info("Awaiting application of expected SchedulePolicyType in chain autoscaler") 1999 return fas.Status.LastAppliedPolicy == expectedChainPolicy 2000 }) 2001 // Verify the second schedule has been applied 2002 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(4)) 2003 expectedChainPolicy = autoscalingv1.FleetAutoscalerPolicyType(fmt.Sprintf("%s:%s:%s", autoscalingv1.ChainPolicyType, "schedule-2", autoscalingv1.SchedulePolicyType)) 2004 framework.WaitForFleetAutoScalerCondition(t, chainAutoscaler, func(log *logrus.Entry, fas *autoscalingv1.FleetAutoscaler) bool { 2005 log.WithField("LastAppliedPolicy", fas.Status.LastAppliedPolicy).Info("Awaiting application of expected SchedulePolicyType in chain autoscaler") 2006 return fas.Status.LastAppliedPolicy == expectedChainPolicy 2007 }) 2008 2009 fleetautoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 2010 } 2011 2012 func TestWasmAutoScaler(t *testing.T) { 2013 if !runtime.FeatureEnabled(runtime.FeatureWasmAutoscaler) { 2014 t.SkipNow() 2015 } 2016 // Parent test is not marked t.Parallel() because it performs shared setup (nginx pod + service) 2017 // that is reused by parallel subtests. 2018 2019 ctx := context.Background() 2020 2021 // Shared setup: Get the path to the WASM plugin file and ensure it exists 2022 wasmFilePath := filepath.Join("..", "..", "examples", "autoscaler-wasm", "plugin.wasm") 2023 _, err := os.Stat(wasmFilePath) 2024 require.NoError(t, err, "WASM plugin file does not exist at %s", wasmFilePath) 2025 2026 // Shared setup: Create a single nginx pod to serve the WASM plugin 2027 emptyDirSize := resource.MustParse("50Mi") // 50Mi ~= 50MB 2028 var port int32 = 80 2029 path := "/plugin.wasm" 2030 2031 pod := &corev1.Pod{ 2032 ObjectMeta: metav1.ObjectMeta{ 2033 GenerateName: "wasm-server-", 2034 Namespace: framework.Namespace, 2035 Labels: map[string]string{ 2036 "app": "wasm-server", 2037 }, 2038 }, 2039 Spec: corev1.PodSpec{ 2040 Volumes: []corev1.Volume{ 2041 { 2042 Name: "ephemeral", 2043 VolumeSource: corev1.VolumeSource{ 2044 EmptyDir: &corev1.EmptyDirVolumeSource{SizeLimit: &emptyDirSize}, 2045 }, 2046 }, 2047 }, 2048 Containers: []corev1.Container{ 2049 { 2050 Name: "nginx", 2051 Image: "nginx:alpine", 2052 Ports: []corev1.ContainerPort{{ 2053 ContainerPort: port, 2054 Name: "http", 2055 }}, 2056 VolumeMounts: []corev1.VolumeMount{ 2057 { 2058 Name: "ephemeral", 2059 MountPath: "/usr/share/nginx/html", 2060 }, 2061 }, 2062 }, 2063 }, 2064 }, 2065 } 2066 podClient := framework.KubeClient.CoreV1().Pods(framework.Namespace) 2067 pod, err = podClient.Create(ctx, pod, metav1.CreateOptions{}) 2068 require.NoError(t, err) 2069 t.Cleanup(func() { _ = podClient.Delete(context.Background(), pod.ObjectMeta.Name, metav1.DeleteOptions{}) }) 2070 2071 // Wait until the Pod is Running 2072 require.NoError(t, wait.PollUntilContextTimeout(ctx, time.Second, 2*time.Minute, true, func(ctx context.Context) (bool, error) { 2073 p, err := podClient.Get(ctx, pod.ObjectMeta.Name, metav1.GetOptions{}) 2074 if err != nil { 2075 return false, nil 2076 } 2077 return p.Status.Phase == corev1.PodRunning, nil 2078 })) 2079 2080 // Copy the WASM plugin file to the nginx container once 2081 err = copyFileToContainer(t, framework.Namespace, pod.ObjectMeta.Name, "nginx", wasmFilePath, "/usr/share/nginx/html/plugin.wasm") 2082 require.NoError(t, err, "Failed to copy WASM plugin file to container") 2083 2084 // Compute correct and incorrect SHA256 hashes for the served WASM 2085 pluginBytes, err := os.ReadFile(wasmFilePath) 2086 require.NoError(t, err) 2087 sum := sha256.Sum256(pluginBytes) 2088 hashStr := hex.EncodeToString(sum[:]) 2089 badSum := make([]byte, len(sum)) 2090 copy(badSum, sum[:]) 2091 badSum[0] ^= 0xFF 2092 badHash := hex.EncodeToString(badSum) 2093 2094 // Shared setup: Create a service to expose the pod 2095 service := &corev1.Service{ 2096 ObjectMeta: metav1.ObjectMeta{ 2097 GenerateName: "wasm-server-", 2098 Namespace: framework.Namespace, 2099 }, 2100 Spec: corev1.ServiceSpec{ 2101 Selector: map[string]string{ 2102 "app": "wasm-server", 2103 }, 2104 Ports: []corev1.ServicePort{{ 2105 Port: port, 2106 TargetPort: intstr.FromInt32(port), 2107 Protocol: corev1.ProtocolTCP, 2108 }}, 2109 }, 2110 } 2111 serviceClient := framework.KubeClient.CoreV1().Services(framework.Namespace) 2112 service, err = serviceClient.Create(ctx, service, metav1.CreateOptions{}) 2113 require.NoError(t, err) 2114 t.Cleanup(func() { 2115 _ = serviceClient.Delete(context.Background(), service.ObjectMeta.Name, metav1.DeleteOptions{}) 2116 }) 2117 2118 testCases := map[string]struct { 2119 bufferSizeConfig *int 2120 functionNameOverride *string 2121 hash *string 2122 expectedFleetSize int32 2123 }{ 2124 "No buffer_size config": { 2125 bufferSizeConfig: nil, 2126 expectedFleetSize: 5, 2127 }, 2128 "buffer_size config of 4": { 2129 bufferSizeConfig: func() *int { i := 4; return &i }(), 2130 expectedFleetSize: 4, 2131 }, 2132 "overwrite function to scaleNone": { 2133 bufferSizeConfig: nil, 2134 functionNameOverride: func() *string { s := "scaleNone"; return &s }(), 2135 expectedFleetSize: 3, 2136 }, 2137 "Correct hash set; scales as expected": { 2138 bufferSizeConfig: nil, 2139 hash: func() *string { s := hashStr; return &s }(), 2140 expectedFleetSize: 5, 2141 }, 2142 "Incorrect hash set; no scaling occurs": { 2143 bufferSizeConfig: nil, 2144 hash: func() *string { s := badHash; return &s }(), 2145 expectedFleetSize: 3, 2146 }, 2147 } 2148 2149 for name, tc := range testCases { 2150 t.Run(name, func(t *testing.T) { 2151 t.Parallel() 2152 2153 ctx := context.Background() 2154 2155 // Create fleet 2156 fleets := framework.AgonesClient.AgonesV1().Fleets(framework.Namespace) 2157 flt, err := fleets.Create(ctx, defaultFleet(framework.Namespace), metav1.CreateOptions{}) 2158 require.NoError(t, err) 2159 defer fleets.Delete(context.Background(), flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck 2160 2161 // Create WASM FleetAutoscaler 2162 fleetAutoscalers := framework.AgonesClient.AutoscalingV1().FleetAutoscalers(framework.Namespace) 2163 2164 wasmAutoscaler := &autoscalingv1.FleetAutoscaler{ 2165 ObjectMeta: metav1.ObjectMeta{ 2166 Name: flt.ObjectMeta.Name + "-wasm-autoscaler", 2167 Namespace: framework.Namespace, 2168 }, 2169 Spec: autoscalingv1.FleetAutoscalerSpec{ 2170 FleetName: flt.ObjectMeta.Name, 2171 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2172 Type: autoscalingv1.WasmPolicyType, 2173 Wasm: &autoscalingv1.WasmPolicy{ 2174 Function: "scale", 2175 Config: make(map[string]string), 2176 From: autoscalingv1.WasmFrom{ 2177 URL: &autoscalingv1.URLConfiguration{ 2178 Service: &admregv1.ServiceReference{ 2179 Namespace: framework.Namespace, 2180 Name: service.ObjectMeta.Name, 2181 Path: &path, 2182 Port: &port, 2183 }, 2184 }, 2185 }, 2186 }, 2187 }, 2188 Sync: &autoscalingv1.FleetAutoscalerSync{ 2189 Type: autoscalingv1.FixedIntervalSyncType, 2190 FixedInterval: autoscalingv1.FixedIntervalSync{ 2191 Seconds: 1, 2192 }, 2193 }, 2194 }, 2195 } 2196 2197 // Overwrite the function name if provided in the test case 2198 if tc.functionNameOverride != nil { 2199 wasmAutoscaler.Spec.Policy.Wasm.Function = *tc.functionNameOverride 2200 } 2201 2202 // Set buffer_size config if provided 2203 if tc.bufferSizeConfig != nil { 2204 wasmAutoscaler.Spec.Policy.Wasm.Config["buffer_size"] = strconv.Itoa(*tc.bufferSizeConfig) 2205 } 2206 2207 // Set hash if provided 2208 if tc.hash != nil { 2209 wasmAutoscaler.Spec.Policy.Wasm.Hash = *tc.hash 2210 } 2211 2212 // make sure the fleet has the correct number of replicas 2213 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 2214 2215 // Create the FleetAutoscaler 2216 fas, err := fleetAutoscalers.Create(ctx, wasmAutoscaler, metav1.CreateOptions{}) 2217 require.NoError(t, err) 2218 defer func() { _ = fleetAutoscalers.Delete(ctx, fas.ObjectMeta.Name, metav1.DeleteOptions{}) }() 2219 2220 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(tc.expectedFleetSize)) 2221 }) 2222 } 2223 } 2224 2225 // defaultAutoscalerSchedule returns a default scheduled autoscaler for testing. 2226 func defaultAutoscalerSchedule(t *testing.T, f *agonesv1.Fleet) *autoscalingv1.FleetAutoscaler { 2227 return &autoscalingv1.FleetAutoscaler{ 2228 ObjectMeta: metav1.ObjectMeta{ 2229 Name: f.ObjectMeta.Name + "-scheduled-autoscaler", 2230 Namespace: framework.Namespace, 2231 }, 2232 Spec: autoscalingv1.FleetAutoscalerSpec{ 2233 FleetName: f.ObjectMeta.Name, 2234 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2235 Type: autoscalingv1.SchedulePolicyType, 2236 Schedule: &autoscalingv1.SchedulePolicy{ 2237 Between: autoscalingv1.Between{ 2238 Start: currentTimePlusDuration(t, "1s"), 2239 End: currentTimePlusDuration(t, "1m"), 2240 }, 2241 ActivePeriod: autoscalingv1.ActivePeriod{ 2242 Timezone: "UTC", 2243 StartCron: "* * * * *", 2244 Duration: "", 2245 }, 2246 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2247 Type: autoscalingv1.BufferPolicyType, 2248 Buffer: &autoscalingv1.BufferPolicy{ 2249 BufferSize: intstr.FromInt(5), 2250 MinReplicas: 5, 2251 MaxReplicas: 12, 2252 }, 2253 }, 2254 }, 2255 }, 2256 Sync: &autoscalingv1.FleetAutoscalerSync{ 2257 Type: autoscalingv1.FixedIntervalSyncType, 2258 FixedInterval: autoscalingv1.FixedIntervalSync{ 2259 Seconds: 5, 2260 }, 2261 }, 2262 }, 2263 } 2264 } 2265 2266 // defaultAutoscalerChain returns a default chain autoscaler for testing. 2267 func defaultAutoscalerChain(t *testing.T, f *agonesv1.Fleet) *autoscalingv1.FleetAutoscaler { 2268 return &autoscalingv1.FleetAutoscaler{ 2269 ObjectMeta: metav1.ObjectMeta{ 2270 Name: f.ObjectMeta.Name + "-chain-autoscaler", 2271 Namespace: framework.Namespace, 2272 }, 2273 Spec: autoscalingv1.FleetAutoscalerSpec{ 2274 FleetName: f.ObjectMeta.Name, 2275 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2276 Type: autoscalingv1.ChainPolicyType, 2277 Chain: autoscalingv1.ChainPolicy{ 2278 { 2279 ID: "schedule-1", 2280 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2281 Type: autoscalingv1.SchedulePolicyType, 2282 Schedule: &autoscalingv1.SchedulePolicy{ 2283 Between: autoscalingv1.Between{ 2284 Start: currentTimePlusDuration(t, "1s"), 2285 End: currentTimePlusDuration(t, "2m"), 2286 }, 2287 ActivePeriod: autoscalingv1.ActivePeriod{ 2288 Timezone: "", 2289 StartCron: inactiveCronSchedule(time.Now()), 2290 Duration: "1m", 2291 }, 2292 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2293 Type: autoscalingv1.BufferPolicyType, 2294 Buffer: &autoscalingv1.BufferPolicy{ 2295 BufferSize: intstr.FromInt(10), 2296 MinReplicas: 10, 2297 MaxReplicas: 20, 2298 }, 2299 }, 2300 }, 2301 }, 2302 }, 2303 { 2304 ID: "schedule-2", 2305 FleetAutoscalerPolicy: autoscalingv1.FleetAutoscalerPolicy{ 2306 Type: autoscalingv1.SchedulePolicyType, 2307 Schedule: &autoscalingv1.SchedulePolicy{ 2308 Between: autoscalingv1.Between{ 2309 Start: currentTimePlusDuration(t, "1s"), 2310 End: currentTimePlusDuration(t, "5m"), 2311 }, 2312 ActivePeriod: autoscalingv1.ActivePeriod{ 2313 Timezone: "", 2314 StartCron: nextCronMinute(time.Now()), 2315 Duration: "", 2316 }, 2317 Policy: autoscalingv1.FleetAutoscalerPolicy{ 2318 Type: autoscalingv1.BufferPolicyType, 2319 Buffer: &autoscalingv1.BufferPolicy{ 2320 BufferSize: intstr.FromInt(4), 2321 MinReplicas: 3, 2322 MaxReplicas: 7, 2323 }, 2324 }, 2325 }, 2326 }, 2327 }, 2328 }, 2329 }, 2330 Sync: &autoscalingv1.FleetAutoscalerSync{ 2331 Type: autoscalingv1.FixedIntervalSyncType, 2332 FixedInterval: autoscalingv1.FixedIntervalSync{ 2333 Seconds: 5, 2334 }, 2335 }, 2336 }, 2337 } 2338 } 2339 2340 // inactiveCronSchedule returns the time 3 minutes ago 2341 // e.g. if the current time is 12:00, this method will return "57 * * * *" 2342 // meaning 3 minutes before 12:00 2343 func inactiveCronSchedule(currentTime time.Time) string { 2344 prevMinute := currentTime.Add(time.Minute * -3).Minute() 2345 return fmt.Sprintf("%d * * * *", prevMinute) 2346 } 2347 2348 // nextCronMinute returns the very next minute in 2349 // e.g. if the current time is 12:00, this method will return "1 * * * *" 2350 // meaning after 12:01 2351 func nextCronMinute(currentTime time.Time) string { 2352 nextMinute := currentTime.Add(time.Minute).Minute() 2353 return fmt.Sprintf("%d * * * *", nextMinute) 2354 } 2355 2356 // nextCronMinuteBetween returns the minute between the very next minute 2357 // e.g. if the current time is 12:00, this method will return "1-2 * * * *" 2358 // meaning between 12:01 - 12:02 2359 // if the current minute if "59" since 59-0 is invalid, we'll return "0-1 * * * *" and wait for a bit longer on e2e tests. 2360 func nextCronMinuteBetween(currentTime time.Time) string { 2361 nextMinute := currentTime.Add(time.Minute).Minute() 2362 if nextMinute == 59 { 2363 return "0-1 * * * *" 2364 } 2365 2366 secondMinute := currentTime.Add(2 * time.Minute).Minute() 2367 return fmt.Sprintf("%d-%d * * * *", nextMinute, secondMinute) 2368 } 2369 2370 // Parse a duration string and return a duration struct 2371 func mustParseDuration(t *testing.T, duration string) time.Duration { 2372 d, err := time.ParseDuration(duration) 2373 assert.Nil(t, err) 2374 return d 2375 } 2376 2377 // Parse a time string and return a metav1.Time 2378 func currentTimePlusDuration(t *testing.T, duration string) metav1.Time { 2379 d := mustParseDuration(t, duration) 2380 currentTimePlusDuration := time.Now().Add(d) 2381 return metav1.NewTime(currentTimePlusDuration) 2382 } 2383 2384 // copyFileToContainer copies a file from the local filesystem to a container in a pod 2385 // Needs kubectl to be on the file path. 2386 // May want to replace this with a more robust solution using the Kubernetes client-go library at some point, but since all e2e tests use kubectl, this is a quick solution. 2387 func copyFileToContainer(t *testing.T, namespace, podName, containerName, srcPath, destPath string) error { 2388 cmd := exec.Command("kubectl", "cp", srcPath, fmt.Sprintf("%s/%s:%s", namespace, podName, destPath), "-c", containerName) 2389 output, err := cmd.CombinedOutput() 2390 if err != nil { 2391 t.Logf("kubectl cp failed: %s", string(output)) 2392 return fmt.Errorf("failed to copy file to container: %w", err) 2393 } 2394 t.Logf("Successfully copied %s to %s/%s:%s", srcPath, namespace, podName, destPath) 2395 return nil 2396 }