agones.dev/agones@v1.53.0/pkg/gameservers/succeeded.go (about) 1 // Copyright 2025 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 gameservers 16 17 import ( 18 "context" 19 20 "agones.dev/agones/pkg/apis/agones" 21 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 22 "agones.dev/agones/pkg/client/clientset/versioned" 23 "agones.dev/agones/pkg/client/clientset/versioned/scheme" 24 getterv1 "agones.dev/agones/pkg/client/clientset/versioned/typed/agones/v1" 25 "agones.dev/agones/pkg/client/informers/externalversions" 26 listerv1 "agones.dev/agones/pkg/client/listers/agones/v1" 27 "agones.dev/agones/pkg/util/logfields" 28 "agones.dev/agones/pkg/util/runtime" 29 "agones.dev/agones/pkg/util/workerqueue" 30 "github.com/heptiolabs/healthcheck" 31 "github.com/pkg/errors" 32 "github.com/sirupsen/logrus" 33 corev1 "k8s.io/api/core/v1" 34 k8serrors "k8s.io/apimachinery/pkg/api/errors" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/client-go/informers" 37 "k8s.io/client-go/kubernetes" 38 typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" 39 corelisterv1 "k8s.io/client-go/listers/core/v1" 40 "k8s.io/client-go/tools/cache" 41 "k8s.io/client-go/tools/record" 42 ) 43 44 // SucceededController changes the state of a GameServer to Shutdown 45 // when its Pod has a backing state of Succeeded. 46 type SucceededController struct { 47 baseLogger *logrus.Entry 48 podSynced cache.InformerSynced 49 podLister corelisterv1.PodLister 50 gameServerSynced cache.InformerSynced 51 gameServerGetter getterv1.GameServersGetter 52 gameServerLister listerv1.GameServerLister 53 workerqueue *workerqueue.WorkerQueue 54 recorder record.EventRecorder 55 } 56 57 // NewSucceededController creates a new SucceededController and sets up event handlers. 58 func NewSucceededController(health healthcheck.Handler, 59 kubeClient kubernetes.Interface, 60 agonesClient versioned.Interface, 61 kubeInformerFactory informers.SharedInformerFactory, 62 agonesInformerFactory externalversions.SharedInformerFactory) *SucceededController { 63 podInformer := kubeInformerFactory.Core().V1().Pods().Informer() 64 gameServers := agonesInformerFactory.Agones().V1().GameServers() 65 66 c := &SucceededController{ 67 podSynced: podInformer.HasSynced, 68 podLister: kubeInformerFactory.Core().V1().Pods().Lister(), 69 gameServerSynced: gameServers.Informer().HasSynced, 70 gameServerGetter: agonesClient.AgonesV1(), 71 gameServerLister: gameServers.Lister(), 72 } 73 74 c.baseLogger = runtime.NewLoggerWithType(c) 75 c.workerqueue = workerqueue.NewWorkerQueue(c.syncGameServer, c.baseLogger, logfields.GameServerKey, agones.GroupName+".SucceededController") 76 health.AddLivenessCheck("gameserver-succeeded-workerqueue", healthcheck.Check(c.workerqueue.Healthy)) 77 78 eventBroadcaster := record.NewBroadcaster() 79 eventBroadcaster.StartLogging(c.baseLogger.Debugf) 80 eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")}) 81 c.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "succeeded-controller"}) 82 83 _, _ = podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ 84 AddFunc: func(obj interface{}) { 85 pod := obj.(*corev1.Pod) 86 if isGameServerPod(pod) && pod.Status.Phase == corev1.PodSucceeded { 87 c.workerqueue.Enqueue(pod) 88 } 89 }, 90 UpdateFunc: func(_, newObj interface{}) { 91 pod := newObj.(*corev1.Pod) 92 if isGameServerPod(pod) && pod.Status.Phase == corev1.PodSucceeded { 93 c.workerqueue.Enqueue(pod) 94 } 95 }, 96 }) 97 98 return c 99 } 100 101 // Run starts the SucceededController worker queue after ensuring caches are synced. 102 func (c *SucceededController) Run(ctx context.Context, workers int) error { 103 c.baseLogger.Debug("Wait for cache sync") 104 if !cache.WaitForCacheSync(ctx.Done(), c.gameServerSynced, c.podSynced) { 105 return errors.New("failed to wait for caches to sync") 106 } 107 108 c.workerqueue.Run(ctx, workers) 109 return nil 110 } 111 112 func (c *SucceededController) loggerForGameServerKey(key string) *logrus.Entry { 113 return logfields.AugmentLogEntry(c.baseLogger, logfields.GameServerKey, key) 114 } 115 116 // syncGameServer changes a GameServer to Shutdown state when its Pod is in Succeeded state 117 func (c *SucceededController) syncGameServer(ctx context.Context, key string) error { 118 namespace, name, err := cache.SplitMetaNamespaceKey(key) 119 if err != nil { 120 // don't return an error, as we don't want this retried 121 runtime.HandleError(c.loggerForGameServerKey(key), errors.Wrapf(err, "invalid resource key")) 122 return nil 123 } 124 125 // check if the pod exists and is in Succeeded state 126 pod, err := c.podLister.Pods(namespace).Get(name) 127 if err != nil { 128 if !k8serrors.IsNotFound(err) { 129 return errors.Wrapf(err, "error retrieving Pod %s from namespace %s", name, namespace) 130 } 131 // If the pod doesn't exist, we don't need to do anything 132 return nil 133 } 134 135 // If the pod exists but is not in Succeeded state or is being terminated, we don't need to do anything 136 if !isGameServerPod(pod) || pod.Status.Phase != corev1.PodSucceeded || !pod.ObjectMeta.DeletionTimestamp.IsZero() { 137 return nil 138 } 139 140 c.loggerForGameServerKey(key).Debug("Pod is in Succeeded state. Moving GameServer to Shutdown.") 141 142 gs, err := c.gameServerLister.GameServers(namespace).Get(name) 143 if err != nil { 144 if k8serrors.IsNotFound(err) { 145 c.loggerForGameServerKey(key).Debug("GameServer is no longer available for syncing") 146 return nil 147 } 148 return errors.Wrapf(err, "error retrieving GameServer %s from namespace %s", name, namespace) 149 } 150 151 // already on the way out, so no need to do anything. 152 if gs.IsBeingDeleted() || agonesv1.TerminalGameServerStates[gs.Status.State] { 153 c.loggerForGameServerKey(key).WithField("state", gs.Status.State).Debug("GameServer already being deleted/shutdown. Skipping.") 154 return nil 155 } 156 157 gsCopy := gs.DeepCopy() 158 gsCopy.Status.State = agonesv1.GameServerStateShutdown 159 gs, err = c.gameServerGetter.GameServers(gsCopy.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{}) 160 if err != nil { 161 return errors.Wrap(err, "error updating GameServer to Shutdown") 162 } 163 164 c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Pod is in Succeeded state") 165 return nil 166 }