From cc58c147ac32cf74a40125e9e8f3281aa27f5d8e Mon Sep 17 00:00:00 2001 From: Mark Mandel Date: Tue, 27 Dec 2022 14:08:02 -0800 Subject: [PATCH] Allocated GameServers updated on Fleet update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Functional implementation and testing of applying labels and/or annotations to any `Allocated` `GameServers` that are overflowed past the configured replica count. Next ➡️ write some guides to close out the ticket. Work on #2682 --- examples/fleet.yaml | 11 + pkg/apis/agones/v1/fleet.go | 2 +- pkg/gameserversets/allocation_overflow.go | 162 ++++++++++++++ .../allocation_overflow_test.go | 203 ++++++++++++++++++ pkg/gameserversets/controller.go | 76 ++++--- pkg/gameserversets/gameserversets.go | 23 ++ pkg/util/runtime/features.go | 12 +- site/content/en/docs/Guides/feature-stages.md | 1 + .../Reference/agones_crd_api_reference.html | 188 ++++++++++++++-- site/content/en/docs/Reference/fleet.md | 15 ++ test/e2e/fleet_test.go | 97 +++++++++ test/e2e/framework/framework.go | 24 +++ 12 files changed, 745 insertions(+), 69 deletions(-) create mode 100644 pkg/gameserversets/allocation_overflow.go create mode 100644 pkg/gameserversets/allocation_overflow_test.go diff --git a/examples/fleet.yaml b/examples/fleet.yaml index 8d3e5caa7a..f08a49edbf 100644 --- a/examples/fleet.yaml +++ b/examples/fleet.yaml @@ -50,6 +50,17 @@ spec: maxSurge: 25% # the amount to decrements GameServers by. Defaults to 25% maxUnavailable: 25% + # [Stage:Alpha] + # [FeatureFlag:FleetAllocationOverflow] + # Labels and/or Annotations to apply to GameServers when the number of Allocated GameServers drops below + # the desired replicas on the underlying `GameServerSet` + # Commented out since Alpha, and disabled by default + # allocationOverflow: # applied to the GameServerSet's number Allocated GameServers that are over the desired replicas + # labels: + # mykey: myvalue + # version: "" # empty an existing label value + # annotations: + # otherkey: setthisvalue template: # GameServer metadata metadata: diff --git a/pkg/apis/agones/v1/fleet.go b/pkg/apis/agones/v1/fleet.go index 0131fcf967..e3413a4e57 100644 --- a/pkg/apis/agones/v1/fleet.go +++ b/pkg/apis/agones/v1/fleet.go @@ -62,7 +62,7 @@ type FleetSpec struct { Replicas int32 `json:"replicas"` // [Stage: Alpha] // [FeatureFlag:FleetAllocationOverflow] - // Labels and Annotations to apply to GameServers when the number of Allocated GameServers drops below + // Labels and/or Annotations to apply to GameServers when the number of Allocated GameServers drops below // the desired replicas on the underlying `GameServerSet` // +optional AllocationOverflow *AllocationOverflow `json:"allocationOverflow,omitempty"` diff --git a/pkg/gameserversets/allocation_overflow.go b/pkg/gameserversets/allocation_overflow.go new file mode 100644 index 0000000000..f4d9249507 --- /dev/null +++ b/pkg/gameserversets/allocation_overflow.go @@ -0,0 +1,162 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gameserversets + +import ( + "context" + "time" + + "agones.dev/agones/pkg/apis/agones" + agonesv1 "agones.dev/agones/pkg/apis/agones/v1" + "agones.dev/agones/pkg/client/clientset/versioned" + getterv1 "agones.dev/agones/pkg/client/clientset/versioned/typed/agones/v1" + "agones.dev/agones/pkg/client/informers/externalversions" + listerv1 "agones.dev/agones/pkg/client/listers/agones/v1" + "agones.dev/agones/pkg/gameservers" + "agones.dev/agones/pkg/util/logfields" + "agones.dev/agones/pkg/util/runtime" + "agones.dev/agones/pkg/util/workerqueue" + "github.com/heptiolabs/healthcheck" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" +) + +// AllocationOverflowController watches `GameServerSets`, and those with configured +// AllocationOverflow settings, will the relevant labels and annotations to `GameServers` attached to the given +// `GameServerSet` +type AllocationOverflowController struct { + baseLogger *logrus.Entry + counter *gameservers.PerNodeCounter + gameServerSynced cache.InformerSynced + gameServerGetter getterv1.GameServersGetter + gameServerLister listerv1.GameServerLister + gameServerSetSynced cache.InformerSynced + gameServerSetLister listerv1.GameServerSetLister + workerqueue *workerqueue.WorkerQueue +} + +// NewAllocatorOverflowController returns a new AllocationOverflowController +func NewAllocatorOverflowController( + health healthcheck.Handler, + counter *gameservers.PerNodeCounter, + agonesClient versioned.Interface, + agonesInformerFactory externalversions.SharedInformerFactory) *AllocationOverflowController { + gameServers := agonesInformerFactory.Agones().V1().GameServers() + gameServerSet := agonesInformerFactory.Agones().V1().GameServerSets() + gsSetInformer := gameServerSet.Informer() + + c := &AllocationOverflowController{ + counter: counter, + gameServerSynced: gameServers.Informer().HasSynced, + gameServerGetter: agonesClient.AgonesV1(), + gameServerLister: gameServers.Lister(), + gameServerSetSynced: gsSetInformer.HasSynced, + gameServerSetLister: gameServerSet.Lister(), + } + + c.baseLogger = runtime.NewLoggerWithType(c) + c.baseLogger.Debug("Created!") + c.workerqueue = workerqueue.NewWorkerQueueWithRateLimiter(c.syncGameServerSet, c.baseLogger, logfields.GameServerSetKey, agones.GroupName+".GameServerSetController", workerqueue.FastRateLimiter(3*time.Second)) + health.AddLivenessCheck("gameserverset-allocationoverflow-workerqueue", c.workerqueue.Healthy) + + gsSetInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(oldObj, newObj interface{}) { + newGss := newObj.(*agonesv1.GameServerSet) + + // Only process if there is an AllocationOverflow, and it has labels or annotations. + if newGss.Spec.AllocationOverflow == nil { + return + } else if len(newGss.Spec.AllocationOverflow.Labels) == 0 && len(newGss.Spec.AllocationOverflow.Annotations) == 0 { + return + } + if newGss.Status.AllocatedReplicas > newGss.Spec.Replicas { + c.workerqueue.Enqueue(newGss) + } + }, + }) + + return c +} + +// Run this controller. Will block until stop is closed. +func (c *AllocationOverflowController) Run(ctx context.Context) error { + c.baseLogger.Debug("Wait for cache sync") + if !cache.WaitForCacheSync(ctx.Done(), c.gameServerSynced, c.gameServerSetSynced) { + return errors.New("failed to wait for caches to sync") + } + + c.workerqueue.Run(ctx, 1) + return nil +} + +// syncGameServerSet checks to see if there are overflow Allocated GameServers and applied the labels and/or +// annotations to the requisite number of GameServers needed to alert the underlying system. +func (c *AllocationOverflowController) syncGameServerSet(ctx context.Context, key string) error { + // Convert the namespace/name string into a distinct namespace and name + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + // don't return an error, as we don't want this retried + runtime.HandleError(loggerForGameServerSetKey(c.baseLogger, key), errors.Wrapf(err, "invalid resource key")) + return nil + } + + gsSet, err := c.gameServerSetLister.GameServerSets(namespace).Get(name) + if err != nil { + if k8serrors.IsNotFound(err) { + loggerForGameServerSetKey(c.baseLogger, key).Debug("GameServerSet is no longer available for syncing") + return nil + } + return errors.Wrapf(err, "error retrieving GameServerSet %s from namespace %s", name, namespace) + } + + // just in case something changed, double check to avoid panics and/or sending work to the K8s API that we don't + // need to + if gsSet.Spec.AllocationOverflow == nil { + return nil + } + if gsSet.Status.AllocatedReplicas <= gsSet.Spec.Replicas { + return nil + } + + overflow := gsSet.Status.AllocatedReplicas - gsSet.Spec.Replicas + + list, err := ListGameServersByGameServerSetOwner(c.gameServerLister, gsSet) + if err != nil { + return err + } + + matches, rest := gsSet.Spec.AllocationOverflow.CountMatches(list) + if matches >= overflow { + return nil + } + + rest = sortGameServersByStrategy(gsSet.Spec.Scheduling, rest, c.counter.Counts()) + rest = rest[:(overflow - matches)] + + opts := v1.UpdateOptions{} + for _, gs := range rest { + gsCopy := gs.DeepCopy() + gsSet.Spec.AllocationOverflow.Apply(gsCopy) + + if _, err := c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, opts); err != nil { + return errors.Wrapf(err, "error updating GameServer %s with overflow labels and/or annotations", gs.ObjectMeta.Name) + } + } + + return nil +} diff --git a/pkg/gameserversets/allocation_overflow_test.go b/pkg/gameserversets/allocation_overflow_test.go new file mode 100644 index 0000000000..ffde7cf8b8 --- /dev/null +++ b/pkg/gameserversets/allocation_overflow_test.go @@ -0,0 +1,203 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gameserversets + +import ( + "context" + "fmt" + "testing" + "time" + + agonesv1 "agones.dev/agones/pkg/apis/agones/v1" + "agones.dev/agones/pkg/gameservers" + agtesting "agones.dev/agones/pkg/testing" + "github.com/heptiolabs/healthcheck" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + k8stesting "k8s.io/client-go/testing" +) + +func TestAllocationOverflowControllerWatchGameServers(t *testing.T) { + t.Parallel() + + gsSet := defaultFixture() + gsSet.Status.Replicas = gsSet.Spec.Replicas + gsSet.Status.ReadyReplicas = gsSet.Spec.Replicas + c, m := newFakeAllocationOverflowController() + + received := make(chan string, 10) + defer close(received) + + gsSetWatch := watch.NewFake() + m.AgonesClient.AddWatchReactor("gameserversets", k8stesting.DefaultWatchReactor(gsSetWatch, nil)) + + c.workerqueue.SyncHandler = func(_ context.Context, name string) error { + received <- name + return nil + } + + ctx, cancel := agtesting.StartInformers(m, c.gameServerSetSynced) + defer cancel() + + go func() { + err := c.Run(ctx) + require.NoError(t, err) + }() + + change := func() string { + select { + case result := <-received: + return result + case <-time.After(3 * time.Second): + require.FailNow(t, "timeout occurred") + } + return "" + } + + nochange := func() { + select { + case <-received: + assert.Fail(t, "Should be no value") + case <-time.After(time.Second): + } + } + + gsSetWatch.Add(gsSet.DeepCopy()) + nochange() + + // update with no allocation overflow + require.Nil(t, gsSet.Spec.AllocationOverflow) + gsSet.Spec.Replicas++ + gsSetWatch.Modify(gsSet.DeepCopy()) + nochange() + + // update with no labels or annotations + gsSet.Spec.AllocationOverflow = &agonesv1.AllocationOverflow{} + gsSet.Spec.Replicas++ + gsSetWatch.Modify(gsSet.DeepCopy()) + nochange() + + // update with allocation <= replicas (and a label) + gsSet.Spec.AllocationOverflow.Labels = map[string]string{"colour": "green"} + gsSet.Status.AllocatedReplicas = 2 + gsSetWatch.Modify(gsSet.DeepCopy()) + nochange() + + // update with allocation > replicas + gsSet.Status.AllocatedReplicas = 20 + gsSetWatch.Modify(gsSet.DeepCopy()) + require.Equal(t, fmt.Sprintf("%s/%s", gsSet.ObjectMeta.Namespace, gsSet.ObjectMeta.Name), change()) + + // delete + gsSetWatch.Delete(gsSet.DeepCopy()) + nochange() +} + +func TestAllocationOverflowSyncGameServerSet(t *testing.T) { + t.Parallel() + + // setup fictures. + setup := func(gs func(server *agonesv1.GameServer)) (*agonesv1.GameServerSet, *AllocationOverflowController, agtesting.Mocks) { + gsSet := defaultFixture() + gsSet.Status.AllocatedReplicas = 5 + gsSet.Status.Replicas = 3 + gsSet.Spec.Replicas = 3 + gsSet.Spec.AllocationOverflow = &agonesv1.AllocationOverflow{Labels: map[string]string{"colour": "green"}} + list := createGameServers(gsSet, 5) + for i := range list { + list[i].Status.State = agonesv1.GameServerStateAllocated + gs(&list[i]) + } + + c, m := newFakeAllocationOverflowController() + m.AgonesClient.AddReactor("list", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &agonesv1.GameServerSetList{Items: []agonesv1.GameServerSet{*gsSet}}, nil + }) + m.AgonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &agonesv1.GameServerList{Items: list}, nil + }) + return gsSet, c, m + } + + // run the sync process + run := func(c *AllocationOverflowController, m agtesting.Mocks, gsSet *agonesv1.GameServerSet, update func(action k8stesting.Action) (bool, runtime.Object, error)) func() { + m.AgonesClient.AddReactor("update", "gameservers", update) + ctx, cancel := agtesting.StartInformers(m, c.gameServerSetSynced, c.gameServerSynced) + err := c.syncGameServerSet(ctx, gsSet.ObjectMeta.Namespace+"/"+gsSet.ObjectMeta.Name) + require.NoError(t, err) + return cancel + } + + t.Run("labels are applied", func(t *testing.T) { + gsSet, c, m := setup(func(_ *agonesv1.GameServer) {}) + count := 0 + cancel := run(c, m, gsSet, func(action k8stesting.Action) (bool, runtime.Object, error) { + ua := action.(k8stesting.UpdateAction) + gs := ua.GetObject().(*agonesv1.GameServer) + require.Equal(t, gs.Status.State, agonesv1.GameServerStateAllocated) + require.Equal(t, "green", gs.ObjectMeta.Labels["colour"]) + + count++ + return true, nil, nil + }) + defer cancel() + require.Equal(t, 2, count) + }) + + t.Run("Labels are already set", func(t *testing.T) { + gsSet, c, m := setup(func(gs *agonesv1.GameServer) { + gs.ObjectMeta.Labels["colour"] = "green" + }) + cancel := run(c, m, gsSet, func(action k8stesting.Action) (bool, runtime.Object, error) { + require.Fail(t, "should not update") + return true, nil, nil + }) + defer cancel() + }) + + t.Run("one label is set", func(t *testing.T) { + set := false + gsSet, c, m := setup(func(gs *agonesv1.GameServer) { + // just make one as already set + if !set { + gs.ObjectMeta.Labels["colour"] = "green" + set = true + } + }) + + count := 0 + cancel := run(c, m, gsSet, func(action k8stesting.Action) (bool, runtime.Object, error) { + ua := action.(k8stesting.UpdateAction) + gs := ua.GetObject().(*agonesv1.GameServer) + require.Equal(t, gs.Status.State, agonesv1.GameServerStateAllocated) + require.Equal(t, "green", gs.ObjectMeta.Labels["colour"]) + + count++ + return true, nil, nil + }) + defer cancel() + require.Equal(t, 1, count) + }) +} + +// newFakeAllocationOverflowController returns a controller, backed by the fake Clientset +func newFakeAllocationOverflowController() (*AllocationOverflowController, agtesting.Mocks) { + m := agtesting.NewMocks() + counter := gameservers.NewPerNodeCounter(m.KubeInformerFactory, m.AgonesInformerFactory) + c := NewAllocatorOverflowController(healthcheck.NewHandler(), counter, m.AgonesClient, m.AgonesInformerFactory) + return c, m +} diff --git a/pkg/gameserversets/controller.go b/pkg/gameserversets/controller.go index 37314cde99..52cf100899 100644 --- a/pkg/gameserversets/controller.go +++ b/pkg/gameserversets/controller.go @@ -72,20 +72,21 @@ type Extensions struct { apiHooks agonesv1.APIHooks } -// Controller is a the GameServerSet controller +// Controller is a GameServerSet controller type Controller struct { - baseLogger *logrus.Entry - counter *gameservers.PerNodeCounter - crdGetter apiextclientv1.CustomResourceDefinitionInterface - gameServerGetter getterv1.GameServersGetter - gameServerLister listerv1.GameServerLister - gameServerSynced cache.InformerSynced - gameServerSetGetter getterv1.GameServerSetsGetter - gameServerSetLister listerv1.GameServerSetLister - gameServerSetSynced cache.InformerSynced - workerqueue *workerqueue.WorkerQueue - recorder record.EventRecorder - stateCache *gameServerStateCache + baseLogger *logrus.Entry + counter *gameservers.PerNodeCounter + crdGetter apiextclientv1.CustomResourceDefinitionInterface + gameServerGetter getterv1.GameServersGetter + gameServerLister listerv1.GameServerLister + gameServerSynced cache.InformerSynced + gameServerSetGetter getterv1.GameServerSetsGetter + gameServerSetLister listerv1.GameServerSetLister + gameServerSetSynced cache.InformerSynced + workerqueue *workerqueue.WorkerQueue + recorder record.EventRecorder + stateCache *gameServerStateCache + allocationController *AllocationOverflowController } // NewController returns a new gameserverset crd controller @@ -123,6 +124,10 @@ func NewController( eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")}) c.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "gameserverset-controller"}) + if runtime.FeatureEnabled(runtime.FeatureFleetAllocateOverflow) { + c.allocationController = NewAllocatorOverflowController(health, counter, agonesClient, agonesInformerFactory) + } + gsSetInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: c.workerqueue.Enqueue, UpdateFunc: func(oldObj, newObj interface{}) { @@ -178,6 +183,14 @@ func (c *Controller) Run(ctx context.Context, workers int) error { return errors.New("failed to wait for caches to sync") } + if runtime.FeatureEnabled(runtime.FeatureFleetAllocateOverflow) { + go func() { + if err := c.allocationController.Run(ctx); err != nil { + c.baseLogger.WithError(err).Error("error running allocation overflow controller") + } + }() + } + c.workerqueue.Run(ctx, workers) return nil } @@ -216,7 +229,7 @@ func (ext *Extensions) updateValidationHandler(review admissionv1.AdmissionRevie Details: &details, } - loggerForGameServerSet(newGss, ext.baseLogger).WithField("review", review).Debug("Invalid GameServerSet update") + loggerForGameServerSet(ext.baseLogger, newGss).WithField("review", review).Debug("Invalid GameServerSet update") return review, nil } @@ -251,7 +264,7 @@ func (ext *Extensions) creationValidationHandler(review admissionv1.AdmissionRev Details: &details, } - loggerForGameServerSet(newGss, ext.baseLogger).WithField("review", review).Debug("Invalid GameServerSet update") + loggerForGameServerSet(ext.baseLogger, newGss).WithField("review", review).Debug("Invalid GameServerSet update") return review, nil } return review, nil @@ -280,18 +293,6 @@ func (c *Controller) gameServerEventHandler(obj interface{}) { c.workerqueue.EnqueueImmediately(gsSet) } -func loggerForGameServerSetKey(key string, logger *logrus.Entry) *logrus.Entry { - return logfields.AugmentLogEntry(logger, logfields.GameServerSetKey, key) -} - -func loggerForGameServerSet(gsSet *agonesv1.GameServerSet, logger *logrus.Entry) *logrus.Entry { - gsSetName := "NilGameServerSet" - if gsSet != nil { - gsSetName = gsSet.Namespace + "/" + gsSet.Name - } - return loggerForGameServerSetKey(gsSetName, logger).WithField("gss", gsSet) -} - // syncGameServer synchronises the GameServers for the Set, // making sure there are aways as many GameServers as requested func (c *Controller) syncGameServerSet(ctx context.Context, key string) error { @@ -299,14 +300,14 @@ func (c *Controller) syncGameServerSet(ctx context.Context, key string) error { namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { // don't return an error, as we don't want this retried - runtime.HandleError(loggerForGameServerSetKey(key, c.baseLogger), errors.Wrapf(err, "invalid resource key")) + runtime.HandleError(loggerForGameServerSetKey(c.baseLogger, key), errors.Wrapf(err, "invalid resource key")) return nil } gsSet, err := c.gameServerSetLister.GameServerSets(namespace).Get(name) if err != nil { if k8serrors.IsNotFound(err) { - loggerForGameServerSetKey(key, c.baseLogger).Debug("GameServerSet is no longer available for syncing") + loggerForGameServerSetKey(c.baseLogger, key).Debug("GameServerSet is no longer available for syncing") return nil } return errors.Wrapf(err, "error retrieving GameServerSet %s from namespace %s", name, namespace) @@ -342,7 +343,7 @@ func (c *Controller) syncGameServerSet(ctx context.Context, key string) error { fields[key] = v.(int) + 1 } - loggerForGameServerSet(gsSet, c.baseLogger). + loggerForGameServerSet(c.baseLogger, gsSet). WithField("targetReplicaCount", gsSet.Spec.Replicas). WithField("numServersToAdd", numServersToAdd). WithField("numServersToDelete", len(toDelete)). @@ -359,13 +360,13 @@ func (c *Controller) syncGameServerSet(ctx context.Context, key string) error { if numServersToAdd > 0 { if err := c.addMoreGameServers(ctx, gsSet, numServersToAdd); err != nil { - loggerForGameServerSet(gsSet, c.baseLogger).WithError(err).Warning("error adding game servers") + loggerForGameServerSet(c.baseLogger, gsSet).WithError(err).Warning("error adding game servers") } } if len(toDelete) > 0 { if err := c.deleteGameServers(ctx, gsSet, toDelete); err != nil { - loggerForGameServerSet(gsSet, c.baseLogger).WithError(err).Warning("error deleting game servers") + loggerForGameServerSet(c.baseLogger, gsSet).WithError(err).Warning("error deleting game servers") } } @@ -476,12 +477,7 @@ func computeReconciliationAction(strategy apis.SchedulingStrategy, list []*agone } if deleteCount > 0 { - if strategy == apis.Packed { - potentialDeletions = sortGameServersByLeastFullNodes(potentialDeletions, counts) - } else { - potentialDeletions = sortGameServersByNewFirst(potentialDeletions) - } - + potentialDeletions = sortGameServersByStrategy(strategy, potentialDeletions, counts) toDelete = append(toDelete, potentialDeletions[0:deleteCount]...) } @@ -495,7 +491,7 @@ func computeReconciliationAction(strategy apis.SchedulingStrategy, list []*agone // addMoreGameServers adds diff more GameServers to the set func (c *Controller) addMoreGameServers(ctx context.Context, gsSet *agonesv1.GameServerSet, count int) error { - loggerForGameServerSet(gsSet, c.baseLogger).WithField("count", count).Debug("Adding more gameservers") + loggerForGameServerSet(c.baseLogger, gsSet).WithField("count", count).Debug("Adding more gameservers") return parallelize(newGameServersChannel(count, gsSet), maxCreationParalellism, func(gs *agonesv1.GameServer) error { gs, err := c.gameServerGetter.GameServers(gs.Namespace).Create(ctx, gs, metav1.CreateOptions{}) @@ -510,7 +506,7 @@ func (c *Controller) addMoreGameServers(ctx context.Context, gsSet *agonesv1.Gam } func (c *Controller) deleteGameServers(ctx context.Context, gsSet *agonesv1.GameServerSet, toDelete []*agonesv1.GameServer) error { - loggerForGameServerSet(gsSet, c.baseLogger).WithField("diff", len(toDelete)).Debug("Deleting gameservers") + loggerForGameServerSet(c.baseLogger, gsSet).WithField("diff", len(toDelete)).Debug("Deleting gameservers") return parallelize(gameServerListToChannel(toDelete), maxDeletionParallelism, func(gs *agonesv1.GameServer) error { // We should not delete the gameservers directly buy set their state to shutdown and let the gameserver controller to delete diff --git a/pkg/gameserversets/gameserversets.go b/pkg/gameserversets/gameserversets.go index 3d4eef24a2..03fd8c1e24 100644 --- a/pkg/gameserversets/gameserversets.go +++ b/pkg/gameserversets/gameserversets.go @@ -17,14 +17,37 @@ package gameserversets import ( "sort" + "agones.dev/agones/pkg/apis" agonesv1 "agones.dev/agones/pkg/apis/agones/v1" listerv1 "agones.dev/agones/pkg/client/listers/agones/v1" "agones.dev/agones/pkg/gameservers" + "agones.dev/agones/pkg/util/logfields" "github.com/pkg/errors" + "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" ) +func loggerForGameServerSetKey(log *logrus.Entry, key string) *logrus.Entry { + return logfields.AugmentLogEntry(log, logfields.GameServerSetKey, key) +} + +func loggerForGameServerSet(log *logrus.Entry, gsSet *agonesv1.GameServerSet) *logrus.Entry { + gsSetName := "NilGameServerSet" + if gsSet != nil { + gsSetName = gsSet.Namespace + "/" + gsSet.Name + } + return loggerForGameServerSetKey(log, gsSetName).WithField("gss", gsSet) +} + +// sortGameServersByStrategy will sort by least full nodes when Packed, and newest first when Distributed +func sortGameServersByStrategy(strategy apis.SchedulingStrategy, list []*agonesv1.GameServer, counts map[string]gameservers.NodeCount) []*agonesv1.GameServer { + if strategy == apis.Packed { + return sortGameServersByLeastFullNodes(list, counts) + } + return sortGameServersByNewFirst(list) +} + // sortGameServersByLeastFullNodes sorts the list of gameservers by which gameservers reside on the least full nodes func sortGameServersByLeastFullNodes(list []*agonesv1.GameServer, count map[string]gameservers.NodeCount) []*agonesv1.GameServer { sort.Slice(list, func(i, j int) bool { diff --git a/pkg/util/runtime/features.go b/pkg/util/runtime/features.go index 7a5d09baef..cdee73cbe3 100644 --- a/pkg/util/runtime/features.go +++ b/pkg/util/runtime/features.go @@ -60,16 +60,16 @@ const ( // FeaturePodHostname enables the Pod Hostname being assigned the name of the GameServer FeaturePodHostname = "PodHostname" + // FeatureFleetAllocateOverflow enables setting labels and/or annotations on Allocated GameServers + // if the desired number of the underlying GameServerSet drops below the number of Allocated GameServers. + FeatureFleetAllocateOverflow = "FleetAllocationOverflow" + // FeatureSplitControllerAndExtensions is a feature flag that will split agones-controller into two deployments FeatureSplitControllerAndExtensions = "SplitControllerAndExtensions" //////////////// // "Pre"-Alpha features - // FeatureFleetAllocateOverflow enables setting labels and/or annotations on Allocated GameServers - // if the desired number of the underlying GameServerSet drops below the number of Allocated GameServers - FeatureFleetAllocateOverflow = "FleetAllocationOverflow" - // FeatureCountsAndLists is a feature flag that enables/disables counts and lists feature // (a generic implenetation of the player tracking feature). FeatureCountsAndLists Feature = "CountsAndLists" @@ -119,11 +119,11 @@ var ( FeaturePlayerTracking: false, FeatureResetMetricsOnDelete: false, FeaturePodHostname: false, + FeatureFleetAllocateOverflow: false, FeatureSplitControllerAndExtensions: false, // Pre-Alpha features - FeatureCountsAndLists: false, - FeatureFleetAllocateOverflow: false, + FeatureCountsAndLists: false, // Example feature FeatureExample: false, diff --git a/site/content/en/docs/Guides/feature-stages.md b/site/content/en/docs/Guides/feature-stages.md index 3b10feeb78..898fab158f 100644 --- a/site/content/en/docs/Guides/feature-stages.md +++ b/site/content/en/docs/Guides/feature-stages.md @@ -35,6 +35,7 @@ The current set of `alpha` and `beta` feature gates: | [Reset Metric Export on Fleet / Autoscaler deletion]({{% relref "./metrics.md#dropping-metric-labels" %}}) | `ResetMetricsOnDelete` | Disabled | `Alpha` | 1.26.0 | | [GameServer Stable Network ID]({{% ref "/docs/Reference/gameserver.md#stable-network-id" %}}) | `PodHostname` | Disabled | `Alpha` | 1.29.0 | | [Split `agones-controller` ](https://github.com/googleforgames/agones/issues/2797) | `SplitControllerAndExtensions` | Disabled | `Alpha` | 1.30.0 | +| [Allocated GameServers are notified on relevant Fleet Updates](https://github.com/googleforgames/agones/issues/2682) | `FleetAllocationOverflow` | Disabled | `Alpha` | 1.30.0 | | Example Gate (not in use) | `Example` | Disabled | None | 0.13.0 | {{< alert title="Note" color="info" >}} diff --git a/site/content/en/docs/Reference/agones_crd_api_reference.html b/site/content/en/docs/Reference/agones_crd_api_reference.html index a917c84ef5..8e143a6701 100644 --- a/site/content/en/docs/Reference/agones_crd_api_reference.html +++ b/site/content/en/docs/Reference/agones_crd_api_reference.html @@ -3,7 +3,7 @@ description="Detailed list of Agones Custom Resource Definitions available" +++ -{{% feature expiryVersion="1.31.0" %}} +{{% feature expiryVersion="1.32.0" %}}

Packages: