agones.dev/agones@v1.53.0/pkg/gameservers/controller.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 gameservers
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"strconv"
    22  	"sync"
    23  	"time"
    24  
    25  	"agones.dev/agones/pkg/apis/agones"
    26  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    27  	"agones.dev/agones/pkg/client/clientset/versioned"
    28  	getterv1 "agones.dev/agones/pkg/client/clientset/versioned/typed/agones/v1"
    29  	"agones.dev/agones/pkg/client/informers/externalversions"
    30  	listerv1 "agones.dev/agones/pkg/client/listers/agones/v1"
    31  	"agones.dev/agones/pkg/cloudproduct"
    32  	"agones.dev/agones/pkg/portallocator"
    33  	"agones.dev/agones/pkg/util/crd"
    34  	"agones.dev/agones/pkg/util/logfields"
    35  	"agones.dev/agones/pkg/util/runtime"
    36  	"agones.dev/agones/pkg/util/webhooks"
    37  	"agones.dev/agones/pkg/util/workerqueue"
    38  	"github.com/heptiolabs/healthcheck"
    39  	"github.com/pkg/errors"
    40  	"github.com/sirupsen/logrus"
    41  	"gomodules.xyz/jsonpatch/v2"
    42  	admissionv1 "k8s.io/api/admission/v1"
    43  	corev1 "k8s.io/api/core/v1"
    44  	extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    45  	apiextclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
    46  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    47  	"k8s.io/apimachinery/pkg/api/resource"
    48  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    49  	runtimeschema "k8s.io/apimachinery/pkg/runtime/schema"
    50  	"k8s.io/apimachinery/pkg/util/intstr"
    51  	"k8s.io/client-go/informers"
    52  	"k8s.io/client-go/kubernetes"
    53  	"k8s.io/client-go/kubernetes/scheme"
    54  	typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    55  	corelisterv1 "k8s.io/client-go/listers/core/v1"
    56  	"k8s.io/client-go/tools/cache"
    57  	"k8s.io/client-go/tools/record"
    58  	"k8s.io/client-go/util/workqueue"
    59  	"k8s.io/utils/ptr"
    60  )
    61  
    62  const (
    63  	sdkserverSidecarName  = "agones-gameserver-sidecar"
    64  	grpcPortEnvVar        = "AGONES_SDK_GRPC_PORT"
    65  	httpPortEnvVar        = "AGONES_SDK_HTTP_PORT"
    66  	passthroughPortEnvVar = "PASSTHROUGH"
    67  )
    68  
    69  // Extensions struct contains what is needed to bind webhook handlers
    70  type Extensions struct {
    71  	baseLogger *logrus.Entry
    72  	apiHooks   agonesv1.APIHooks
    73  }
    74  
    75  // Controller is a the main GameServer crd controller
    76  //
    77  //nolint:govet // ignore fieldalignment, singleton
    78  type Controller struct {
    79  	baseLogger               *logrus.Entry
    80  	controllerHooks          cloudproduct.ControllerHooksInterface
    81  	sidecarImage             string
    82  	alwaysPullSidecarImage   bool
    83  	sidecarCPURequest        resource.Quantity
    84  	sidecarCPULimit          resource.Quantity
    85  	sidecarMemoryRequest     resource.Quantity
    86  	sidecarMemoryLimit       resource.Quantity
    87  	sidecarRunAsUser         int
    88  	sidecarRequestsRateLimit time.Duration
    89  	sdkServiceAccount        string
    90  	crdGetter                apiextclientv1.CustomResourceDefinitionInterface
    91  	podGetter                typedcorev1.PodsGetter
    92  	podLister                corelisterv1.PodLister
    93  	podSynced                cache.InformerSynced
    94  	gameServerGetter         getterv1.GameServersGetter
    95  	gameServerLister         listerv1.GameServerLister
    96  	gameServerSynced         cache.InformerSynced
    97  	nodeLister               corelisterv1.NodeLister
    98  	nodeSynced               cache.InformerSynced
    99  	portAllocator            portallocator.Interface
   100  	healthController         *HealthController
   101  	migrationController      *MigrationController
   102  	missingPodController     *MissingPodController
   103  	succeededController      *SucceededController
   104  	workerqueue              *workerqueue.WorkerQueue
   105  	creationWorkerQueue      *workerqueue.WorkerQueue // handles creation only
   106  	deletionWorkerQueue      *workerqueue.WorkerQueue // handles deletion only
   107  	recorder                 record.EventRecorder
   108  }
   109  
   110  // NewController returns a new gameserver crd controller
   111  func NewController(
   112  	controllerHooks cloudproduct.ControllerHooksInterface,
   113  	health healthcheck.Handler,
   114  	portRanges map[string]portallocator.PortRange,
   115  	sidecarImage string,
   116  	alwaysPullSidecarImage bool,
   117  	sidecarCPURequest resource.Quantity,
   118  	sidecarCPULimit resource.Quantity,
   119  	sidecarMemoryRequest resource.Quantity,
   120  	sidecarMemoryLimit resource.Quantity,
   121  	sidecarRunAsUser int,
   122  	sidecarRequestsRateLimit time.Duration,
   123  	sdkServiceAccount string,
   124  	kubeClient kubernetes.Interface,
   125  	kubeInformerFactory informers.SharedInformerFactory,
   126  	extClient extclientset.Interface,
   127  	agonesClient versioned.Interface,
   128  	agonesInformerFactory externalversions.SharedInformerFactory,
   129  ) *Controller {
   130  
   131  	pods := kubeInformerFactory.Core().V1().Pods()
   132  	gameServers := agonesInformerFactory.Agones().V1().GameServers()
   133  	gsInformer := gameServers.Informer()
   134  
   135  	c := &Controller{
   136  		controllerHooks:          controllerHooks,
   137  		sidecarImage:             sidecarImage,
   138  		sidecarCPULimit:          sidecarCPULimit,
   139  		sidecarCPURequest:        sidecarCPURequest,
   140  		sidecarMemoryLimit:       sidecarMemoryLimit,
   141  		sidecarMemoryRequest:     sidecarMemoryRequest,
   142  		sidecarRunAsUser:         sidecarRunAsUser,
   143  		sidecarRequestsRateLimit: sidecarRequestsRateLimit,
   144  		alwaysPullSidecarImage:   alwaysPullSidecarImage,
   145  		sdkServiceAccount:        sdkServiceAccount,
   146  		crdGetter:                extClient.ApiextensionsV1().CustomResourceDefinitions(),
   147  		podGetter:                kubeClient.CoreV1(),
   148  		podLister:                pods.Lister(),
   149  		podSynced:                pods.Informer().HasSynced,
   150  		gameServerGetter:         agonesClient.AgonesV1(),
   151  		gameServerLister:         gameServers.Lister(),
   152  		gameServerSynced:         gsInformer.HasSynced,
   153  		nodeLister:               kubeInformerFactory.Core().V1().Nodes().Lister(),
   154  		nodeSynced:               kubeInformerFactory.Core().V1().Nodes().Informer().HasSynced,
   155  		portAllocator:            controllerHooks.NewPortAllocator(portRanges, kubeInformerFactory, agonesInformerFactory),
   156  		healthController:         NewHealthController(health, kubeClient, agonesClient, kubeInformerFactory, agonesInformerFactory, controllerHooks.WaitOnFreePorts()),
   157  		migrationController:      NewMigrationController(health, kubeClient, agonesClient, kubeInformerFactory, agonesInformerFactory, controllerHooks.SyncPodPortsToGameServer),
   158  		missingPodController:     NewMissingPodController(health, kubeClient, agonesClient, kubeInformerFactory, agonesInformerFactory),
   159  		succeededController:      NewSucceededController(health, kubeClient, agonesClient, kubeInformerFactory, agonesInformerFactory),
   160  	}
   161  
   162  	c.baseLogger = runtime.NewLoggerWithType(c)
   163  
   164  	eventBroadcaster := record.NewBroadcaster()
   165  	eventBroadcaster.StartLogging(c.baseLogger.Debugf)
   166  	eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")})
   167  	c.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "gameserver-controller"})
   168  
   169  	c.workerqueue = workerqueue.NewWorkerQueueWithRateLimiter(c.syncGameServer, c.baseLogger, logfields.GameServerKey, agones.GroupName+".GameServerController", fastRateLimiter())
   170  	c.creationWorkerQueue = workerqueue.NewWorkerQueueWithRateLimiter(c.syncGameServer, c.baseLogger.WithField("subqueue", "creation"), logfields.GameServerKey, agones.GroupName+".GameServerControllerCreation", fastRateLimiter())
   171  	c.deletionWorkerQueue = workerqueue.NewWorkerQueueWithRateLimiter(c.syncGameServer, c.baseLogger.WithField("subqueue", "deletion"), logfields.GameServerKey, agones.GroupName+".GameServerControllerDeletion", fastRateLimiter())
   172  	health.AddLivenessCheck("gameserver-workerqueue", healthcheck.Check(c.workerqueue.Healthy))
   173  	health.AddLivenessCheck("gameserver-creation-workerqueue", healthcheck.Check(c.creationWorkerQueue.Healthy))
   174  	health.AddLivenessCheck("gameserver-deletion-workerqueue", healthcheck.Check(c.deletionWorkerQueue.Healthy))
   175  
   176  	_, _ = gsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
   177  		AddFunc: c.enqueueGameServerBasedOnState,
   178  		UpdateFunc: func(oldObj, newObj interface{}) {
   179  			// no point in processing unless there is a State change
   180  			oldGs := oldObj.(*agonesv1.GameServer)
   181  			newGs := newObj.(*agonesv1.GameServer)
   182  			if oldGs.Status.State != newGs.Status.State || !newGs.ObjectMeta.DeletionTimestamp.IsZero() {
   183  				c.enqueueGameServerBasedOnState(newGs)
   184  			}
   185  		},
   186  	})
   187  
   188  	// track pod deletions, for when GameServers are deleted
   189  	_, _ = pods.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
   190  		UpdateFunc: func(oldObj, newObj interface{}) {
   191  			oldPod := oldObj.(*corev1.Pod)
   192  			if isGameServerPod(oldPod) {
   193  				newPod := newObj.(*corev1.Pod)
   194  				//  node name has changed -- i.e. it has been scheduled
   195  				if oldPod.Spec.NodeName != newPod.Spec.NodeName {
   196  					owner := metav1.GetControllerOf(newPod)
   197  					c.workerqueue.Enqueue(cache.ExplicitKey(newPod.ObjectMeta.Namespace + "/" + owner.Name))
   198  				}
   199  			}
   200  		},
   201  		DeleteFunc: func(obj interface{}) {
   202  			// Could be a DeletedFinalStateUnknown, in which case, just ignore it
   203  			pod, ok := obj.(*corev1.Pod)
   204  			if ok && isGameServerPod(pod) {
   205  				owner := metav1.GetControllerOf(pod)
   206  				c.workerqueue.Enqueue(cache.ExplicitKey(pod.ObjectMeta.Namespace + "/" + owner.Name))
   207  			}
   208  		},
   209  	})
   210  
   211  	return c
   212  }
   213  
   214  // NewExtensions binds the handlers to the webhook outside the initialization of the controller
   215  // initializes a new logger for extensions.
   216  func NewExtensions(apiHooks agonesv1.APIHooks, wh *webhooks.WebHook) *Extensions {
   217  	ext := &Extensions{apiHooks: apiHooks}
   218  
   219  	ext.baseLogger = runtime.NewLoggerWithType(ext)
   220  
   221  	wh.AddHandler("/mutate", agonesv1.Kind("GameServer"), admissionv1.Create, ext.creationMutationHandler)
   222  	wh.AddHandler("/validate", agonesv1.Kind("GameServer"), admissionv1.Create, ext.creationValidationHandler)
   223  
   224  	if runtime.FeatureEnabled(runtime.FeatureAutopilotPassthroughPort) {
   225  		wh.AddHandler("/mutate", corev1.SchemeGroupVersion.WithKind("Pod").GroupKind(), admissionv1.Create, ext.creationMutationHandlerPod)
   226  	}
   227  	return ext
   228  }
   229  
   230  func (c *Controller) enqueueGameServerBasedOnState(item interface{}) {
   231  	gs := item.(*agonesv1.GameServer)
   232  
   233  	switch gs.Status.State {
   234  	case agonesv1.GameServerStatePortAllocation,
   235  		agonesv1.GameServerStateCreating:
   236  		c.creationWorkerQueue.Enqueue(gs)
   237  
   238  	case agonesv1.GameServerStateShutdown:
   239  		c.deletionWorkerQueue.Enqueue(gs)
   240  
   241  	default:
   242  		c.workerqueue.Enqueue(gs)
   243  	}
   244  }
   245  
   246  // fastRateLimiter returns a fast rate limiter, without exponential back-off.
   247  func fastRateLimiter() workqueue.TypedRateLimiter[any] {
   248  	const numFastRetries = 5
   249  	const fastDelay = 20 * time.Millisecond  // first few retries up to 'numFastRetries' are fast
   250  	const slowDelay = 500 * time.Millisecond // subsequent retries are slow
   251  
   252  	return workqueue.NewTypedItemFastSlowRateLimiter[any](fastDelay, slowDelay, numFastRetries)
   253  }
   254  
   255  // creationMutationHandler is the handler for the mutating webhook that sets the
   256  // the default values on the GameServer
   257  // Should only be called on gameserver create operations.
   258  // nolint:dupl
   259  func (ext *Extensions) creationMutationHandler(review admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) {
   260  	obj := review.Request.Object
   261  	gs := &agonesv1.GameServer{}
   262  	err := json.Unmarshal(obj.Raw, gs)
   263  	if err != nil {
   264  		// If the JSON is invalid during mutation, fall through to validation. This allows OpenAPI schema validation
   265  		// to proceed, resulting in a more user friendly error message.
   266  		return review, nil
   267  	}
   268  
   269  	// This is the main logic of this function
   270  	// the rest is really just json plumbing
   271  	gs.ApplyDefaults()
   272  
   273  	newGS, err := json.Marshal(gs)
   274  	if err != nil {
   275  		return review, errors.Wrapf(err, "error marshalling default applied GameServer %s to json", gs.ObjectMeta.Name)
   276  	}
   277  
   278  	patch, err := jsonpatch.CreatePatch(obj.Raw, newGS)
   279  	if err != nil {
   280  		return review, errors.Wrapf(err, "error creating patch for GameServer %s", gs.ObjectMeta.Name)
   281  	}
   282  
   283  	jsonPatch, err := json.Marshal(patch)
   284  	if err != nil {
   285  		return review, errors.Wrapf(err, "error creating json for patch for GameServer %s", gs.ObjectMeta.Name)
   286  	}
   287  
   288  	pt := admissionv1.PatchTypeJSONPatch
   289  	review.Response.PatchType = &pt
   290  	review.Response.Patch = jsonPatch
   291  
   292  	return review, nil
   293  }
   294  
   295  func loggerForGameServerKey(key string, logger *logrus.Entry) *logrus.Entry {
   296  	return logfields.AugmentLogEntry(logger, logfields.GameServerKey, key)
   297  }
   298  
   299  func loggerForGameServer(gs *agonesv1.GameServer, logger *logrus.Entry) *logrus.Entry {
   300  	gsName := logfields.NilGameServer
   301  	if gs != nil {
   302  		gsName = gs.Namespace + "/" + gs.Name
   303  	}
   304  	return loggerForGameServerKey(gsName, logger).WithField("gs", gs)
   305  }
   306  
   307  // creationValidationHandler that validates a GameServer when it is created
   308  // Should only be called on gameserver create operations.
   309  func (ext *Extensions) creationValidationHandler(review admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) {
   310  	obj := review.Request.Object
   311  	gs := &agonesv1.GameServer{}
   312  	err := json.Unmarshal(obj.Raw, gs)
   313  	if err != nil {
   314  		return review, errors.Wrapf(err, "error unmarshalling GameServer json after schema validation: %s", obj.Raw)
   315  	}
   316  
   317  	loggerForGameServer(gs, ext.baseLogger).WithField("review", review).Debug("creationValidationHandler")
   318  
   319  	if errs := gs.Validate(ext.apiHooks); len(errs) > 0 {
   320  		kind := runtimeschema.GroupKind{
   321  			Group: review.Request.Kind.Group,
   322  			Kind:  review.Request.Kind.Kind,
   323  		}
   324  		statusErr := k8serrors.NewInvalid(kind, review.Request.Name, errs)
   325  		review.Response.Allowed = false
   326  		review.Response.Result = &statusErr.ErrStatus
   327  		loggerForGameServer(gs, ext.baseLogger).WithField("review", review).Debug("Invalid GameServer")
   328  	}
   329  	return review, nil
   330  }
   331  
   332  // creationMutationHandlerPod that mutates a GameServer pod when it is created
   333  // Should only be called on gameserver pod create operations.
   334  func (ext *Extensions) creationMutationHandlerPod(review admissionv1.AdmissionReview) (admissionv1.AdmissionReview, error) {
   335  	obj := review.Request.Object
   336  	pod := &corev1.Pod{}
   337  	err := json.Unmarshal(obj.Raw, pod)
   338  	if err != nil {
   339  		// If the JSON is invalid during mutation, fall through to validation. This allows OpenAPI schema validation
   340  		// to proceed, resulting in a more user friendly error message.
   341  		return review, nil
   342  	}
   343  
   344  	ext.baseLogger.WithField("pod.Name", pod.Name).Debug("creationMutationHandlerPod")
   345  
   346  	annotation, ok := pod.ObjectMeta.Annotations[agonesv1.PassthroughPortAssignmentAnnotation]
   347  	if !ok {
   348  		ext.baseLogger.WithField("pod.Name", pod.Name).Info("the agones.dev/container-passthrough-port-assignment annotation is empty and it's unexpected")
   349  		return review, nil
   350  	}
   351  
   352  	passthroughPortAssignmentMap := make(map[string][]int)
   353  	if err := json.Unmarshal([]byte(annotation), &passthroughPortAssignmentMap); err != nil {
   354  		return review, errors.Wrapf(err, "could not unmarshal annotation %q (value %q)", passthroughPortAssignmentMap, annotation)
   355  	}
   356  
   357  	for _, container := range pod.Spec.Containers {
   358  		for _, portIdx := range passthroughPortAssignmentMap[container.Name] {
   359  			container.Ports[portIdx].ContainerPort = container.Ports[portIdx].HostPort
   360  		}
   361  	}
   362  
   363  	newPod, err := json.Marshal(pod)
   364  	if err != nil {
   365  		return review, errors.Wrapf(err, "error marshalling changes applied Pod %s to json", pod.ObjectMeta.Name)
   366  	}
   367  
   368  	patch, err := jsonpatch.CreatePatch(obj.Raw, newPod)
   369  	if err != nil {
   370  		return review, errors.Wrapf(err, "error creating patch for Pod %s", pod.ObjectMeta.Name)
   371  	}
   372  
   373  	jsonPatch, err := json.Marshal(patch)
   374  	if err != nil {
   375  		return review, errors.Wrapf(err, "error creating json for patch for Pod %s", pod.ObjectMeta.Name)
   376  	}
   377  
   378  	pt := admissionv1.PatchTypeJSONPatch
   379  	review.Response.PatchType = &pt
   380  	review.Response.Patch = jsonPatch
   381  
   382  	return review, nil
   383  }
   384  
   385  // Run the GameServer controller. Will block until stop is closed.
   386  // Runs threadiness number workers to process the rate limited queue
   387  func (c *Controller) Run(ctx context.Context, workers int) error {
   388  	err := crd.WaitForEstablishedCRD(ctx, c.crdGetter, "gameservers.agones.dev", c.baseLogger)
   389  	if err != nil {
   390  		return err
   391  	}
   392  
   393  	c.baseLogger.Debug("Wait for cache sync")
   394  	if !cache.WaitForCacheSync(ctx.Done(), c.gameServerSynced, c.podSynced, c.nodeSynced) {
   395  		return errors.New("failed to wait for caches to sync")
   396  	}
   397  
   398  	// Run the Port Allocator
   399  	if err = c.portAllocator.Run(ctx); err != nil {
   400  		return errors.Wrap(err, "error running the port allocator")
   401  	}
   402  
   403  	// Run the Health Controller
   404  	go func() {
   405  		if err := c.healthController.Run(ctx, workers); err != nil {
   406  			c.baseLogger.WithError(err).Error("error running health controller")
   407  		}
   408  	}()
   409  
   410  	// Run the Migration Controller
   411  	go func() {
   412  		if err := c.migrationController.Run(ctx, workers); err != nil {
   413  			c.baseLogger.WithError(err).Error("error running migration controller")
   414  		}
   415  	}()
   416  
   417  	// Run the Missing Pod Controller
   418  	go func() {
   419  		if err := c.missingPodController.Run(ctx, workers); err != nil {
   420  			c.baseLogger.WithError(err).Error("error running missing pod controller")
   421  		}
   422  	}()
   423  
   424  	// Run the Succeeded Controller
   425  	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
   426  		go func() {
   427  			if err := c.succeededController.Run(ctx, workers); err != nil {
   428  				c.baseLogger.WithError(err).Error("error running succeeded controller")
   429  			}
   430  		}()
   431  	}
   432  
   433  	// start work queues
   434  	var wg sync.WaitGroup
   435  
   436  	startWorkQueue := func(wq *workerqueue.WorkerQueue) {
   437  		wg.Add(1)
   438  		go func() {
   439  			defer wg.Done()
   440  			wq.Run(ctx, workers)
   441  		}()
   442  	}
   443  
   444  	startWorkQueue(c.workerqueue)
   445  	startWorkQueue(c.creationWorkerQueue)
   446  	startWorkQueue(c.deletionWorkerQueue)
   447  	wg.Wait()
   448  	return nil
   449  }
   450  
   451  // syncGameServer synchronises the Pods for the GameServers.
   452  // and reacts to status changes that can occur through the client SDK
   453  func (c *Controller) syncGameServer(ctx context.Context, key string) error {
   454  	loggerForGameServerKey(key, c.baseLogger).Debug("Synchronising")
   455  
   456  	// Convert the namespace/name string into a distinct namespace and name
   457  	namespace, name, err := cache.SplitMetaNamespaceKey(key)
   458  	if err != nil {
   459  		// don't return an error, as we don't want this retried
   460  		runtime.HandleError(loggerForGameServerKey(key, c.baseLogger), errors.Wrapf(err, "invalid resource key"))
   461  		return nil
   462  	}
   463  
   464  	gs, err := c.gameServerLister.GameServers(namespace).Get(name)
   465  	if err != nil {
   466  		if k8serrors.IsNotFound(err) {
   467  			loggerForGameServerKey(key, c.baseLogger).Debug("GameServer is no longer available for syncing")
   468  			return nil
   469  		}
   470  		return errors.Wrapf(err, "error retrieving GameServer %s from namespace %s", name, namespace)
   471  	}
   472  
   473  	if gs, err = c.syncGameServerDeletionTimestamp(ctx, gs); err != nil {
   474  		return err
   475  	}
   476  	if gs, err = c.syncGameServerPortAllocationState(ctx, gs); err != nil {
   477  		return err
   478  	}
   479  	if gs, err = c.syncGameServerCreatingState(ctx, gs); err != nil {
   480  		return err
   481  	}
   482  	if gs, err = c.syncGameServerStartingState(ctx, gs); err != nil {
   483  		return err
   484  	}
   485  	if gs, err = c.syncGameServerRequestReadyState(ctx, gs); err != nil {
   486  		return err
   487  	}
   488  	if gs, err = c.syncDevelopmentGameServer(ctx, gs); err != nil {
   489  		return err
   490  	}
   491  	if err := c.syncGameServerShutdownState(ctx, gs); err != nil {
   492  		return err
   493  	}
   494  
   495  	return nil
   496  }
   497  
   498  // syncGameServerDeletionTimestamp if the deletion timestamp is non-zero
   499  // then do one of two things:
   500  // - if the GameServer has Pods running, delete them
   501  // - if there no pods, remove the finalizer
   502  func (c *Controller) syncGameServerDeletionTimestamp(ctx context.Context, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) {
   503  	if gs.ObjectMeta.DeletionTimestamp.IsZero() {
   504  		return gs, nil
   505  	}
   506  
   507  	loggerForGameServer(gs, c.baseLogger).Debug("Syncing with Deletion Timestamp")
   508  
   509  	pod, err := c.gameServerPod(gs)
   510  	if err != nil && !k8serrors.IsNotFound(err) {
   511  		return gs, err
   512  	}
   513  
   514  	_, isDev := gs.GetDevAddress()
   515  	if pod != nil && !isDev {
   516  		// only need to do this once
   517  		if pod.ObjectMeta.DeletionTimestamp.IsZero() {
   518  			err = c.podGetter.Pods(pod.ObjectMeta.Namespace).Delete(ctx, pod.ObjectMeta.Name, metav1.DeleteOptions{})
   519  			if err != nil {
   520  				return gs, errors.Wrapf(err, "error deleting pod for GameServer. Name: %s, Namespace: %s", gs.ObjectMeta.Name, pod.ObjectMeta.Namespace)
   521  			}
   522  			c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), fmt.Sprintf("Deleting Pod %s", pod.ObjectMeta.Name))
   523  		}
   524  
   525  		// but no removing finalizers until it's truly gone
   526  		return gs, nil
   527  	}
   528  
   529  	gsCopy := gs.DeepCopy()
   530  	// remove the finalizer for this controller
   531  	var fin []string
   532  	for _, f := range gsCopy.ObjectMeta.Finalizers {
   533  		if f != agones.GroupName && f != agonesv1.FinalizerName {
   534  			fin = append(fin, f)
   535  		}
   536  	}
   537  	gsCopy.ObjectMeta.Finalizers = fin
   538  	loggerForGameServer(gsCopy, c.baseLogger).Debugf("No pods found, removing finalizer %s", agonesv1.FinalizerName)
   539  	gs, err = c.gameServerGetter.GameServers(gsCopy.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
   540  	return gs, errors.Wrapf(err, "error removing finalizer for GameServer %s", gsCopy.ObjectMeta.Name)
   541  }
   542  
   543  // syncGameServerPortAllocationState gives a port to a dynamically allocating GameServer
   544  func (c *Controller) syncGameServerPortAllocationState(ctx context.Context, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) {
   545  	if !(gs.Status.State == agonesv1.GameServerStatePortAllocation && gs.ObjectMeta.DeletionTimestamp.IsZero()) {
   546  		return gs, nil
   547  	}
   548  
   549  	gsCopy := c.portAllocator.Allocate(gs.DeepCopy())
   550  
   551  	gsCopy.Status.State = agonesv1.GameServerStateCreating
   552  	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Port allocated")
   553  
   554  	loggerForGameServer(gsCopy, c.baseLogger).Debug("Syncing Port Allocation GameServerState")
   555  	gs, err := c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
   556  	if err != nil {
   557  		// if the GameServer doesn't get updated with the port data, then put the port
   558  		// back in the pool, as it will get retried on the next pass
   559  		c.portAllocator.DeAllocate(gsCopy)
   560  		return gs, errors.Wrapf(err, "error updating GameServer %s to default values", gs.Name)
   561  	}
   562  
   563  	return gs, nil
   564  }
   565  
   566  // syncGameServerCreatingState checks if the GameServer is in the Creating state, and if so
   567  // creates a Pod for the GameServer and moves the state to Starting
   568  func (c *Controller) syncGameServerCreatingState(ctx context.Context, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) {
   569  	if !(gs.Status.State == agonesv1.GameServerStateCreating && gs.ObjectMeta.DeletionTimestamp.IsZero()) {
   570  		return gs, nil
   571  	}
   572  	if _, isDev := gs.GetDevAddress(); isDev {
   573  		return gs, nil
   574  	}
   575  
   576  	loggerForGameServer(gs, c.baseLogger).Debug("Syncing Create State")
   577  
   578  	// Maybe something went wrong, and the pod was created, but the state was never moved to Starting, so let's check
   579  	_, err := c.gameServerPod(gs)
   580  	if k8serrors.IsNotFound(err) {
   581  
   582  		for i := range gs.Spec.Ports {
   583  			if gs.Spec.Ports[i].PortPolicy == agonesv1.Static && gs.Spec.Ports[i].Protocol == agonesv1.ProtocolTCPUDP {
   584  				name := gs.Spec.Ports[i].Name
   585  				gs.Spec.Ports[i].Name = name + "-tcp"
   586  				gs.Spec.Ports[i].Protocol = corev1.ProtocolTCP
   587  
   588  				// Add separate UDP port configuration
   589  				gs.Spec.Ports = append(gs.Spec.Ports, agonesv1.GameServerPort{
   590  					PortPolicy:    agonesv1.Static,
   591  					Name:          name + "-udp",
   592  					ContainerPort: gs.Spec.Ports[i].ContainerPort,
   593  					HostPort:      gs.Spec.Ports[i].HostPort,
   594  					Protocol:      corev1.ProtocolUDP,
   595  					Container:     gs.Spec.Ports[i].Container,
   596  				})
   597  			}
   598  		}
   599  		gs, err = c.createGameServerPod(ctx, gs)
   600  		if err != nil || gs.Status.State == agonesv1.GameServerStateError {
   601  			return gs, err
   602  		}
   603  	}
   604  
   605  	if err != nil {
   606  		return nil, errors.WithStack(err)
   607  	}
   608  
   609  	gsCopy := gs.DeepCopy()
   610  	gsCopy.Status.State = agonesv1.GameServerStateStarting
   611  	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
   612  	if err != nil {
   613  		return gs, errors.Wrapf(err, "error updating GameServer %s to Starting state", gs.Name)
   614  	}
   615  	return gs, nil
   616  }
   617  
   618  // syncDevelopmentGameServer manages advances a development gameserver to Ready status and registers its address and ports.
   619  func (c *Controller) syncDevelopmentGameServer(ctx context.Context, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) {
   620  	// do not sync if the server is deleting.
   621  	if !(gs.ObjectMeta.DeletionTimestamp.IsZero()) {
   622  		return gs, nil
   623  	}
   624  	// Get the development IP address
   625  	devIPAddress, isDevGs := gs.GetDevAddress()
   626  	if !isDevGs {
   627  		return gs, nil
   628  	}
   629  
   630  	// Only move from Creating -> Ready or RequestReady -> Ready.
   631  	// Shutdown -> Delete will still be handled normally by syncGameServerShutdownState.
   632  	// Other manual state changes are up to the end user.
   633  	if gs.Status.State != agonesv1.GameServerStateCreating && gs.Status.State != agonesv1.GameServerStateRequestReady {
   634  		return gs, nil
   635  	}
   636  
   637  	loggerForGameServer(gs, c.baseLogger).Debug("GS is a development game server and will not be managed by Agones.")
   638  	gsCopy := gs.DeepCopy()
   639  
   640  	gsCopy.Status.State = agonesv1.GameServerStateReady
   641  
   642  	if gs.Status.State == agonesv1.GameServerStateCreating {
   643  		var ports []agonesv1.GameServerStatusPort
   644  		for _, p := range gs.Spec.Ports {
   645  			ports = append(ports, p.Status())
   646  		}
   647  
   648  		gsCopy.Status.Ports = ports
   649  		gsCopy.Status.Address = devIPAddress
   650  		gsCopy.Status.Addresses = []corev1.NodeAddress{{Address: devIPAddress, Type: "InternalIP"}}
   651  		gsCopy.Status.NodeName = devIPAddress
   652  	}
   653  
   654  	gs, err := c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
   655  	if err != nil {
   656  		return gs, errors.Wrapf(err, "error updating GameServer %s to %v status", gs.Name, gs.Status)
   657  	}
   658  	return gs, nil
   659  }
   660  
   661  // createGameServerPod creates the backing Pod for a given GameServer
   662  func (c *Controller) createGameServerPod(ctx context.Context, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) {
   663  	sidecar := c.sidecar(gs)
   664  	pod, err := gs.Pod(c.controllerHooks, sidecar)
   665  	if err != nil {
   666  		// this shouldn't happen, but if it does.
   667  		loggerForGameServer(gs, c.baseLogger).WithError(err).Error("error creating pod from Game Server")
   668  		gs, err = c.moveToErrorState(ctx, gs, err.Error())
   669  		return gs, err
   670  	}
   671  
   672  	// if the service account is not set, then you are in the "opinionated"
   673  	// mode. If the user sets the service account, we assume they know what they are
   674  	// doing, and don't disable the gameserver container.
   675  	if pod.Spec.ServiceAccountName == "" {
   676  		pod.Spec.ServiceAccountName = c.sdkServiceAccount
   677  		err = gs.DisableServiceAccount(pod)
   678  		if err != nil {
   679  			return gs, err
   680  		}
   681  	}
   682  
   683  	err = c.addGameServerHealthCheck(gs, pod)
   684  	if err != nil {
   685  		return gs, err
   686  	}
   687  	c.addSDKServerEnvVars(gs, pod)
   688  
   689  	loggerForGameServer(gs, c.baseLogger).WithField("pod", pod).Debug("Creating Pod for GameServer")
   690  	pod, err = c.podGetter.Pods(gs.ObjectMeta.Namespace).Create(ctx, pod, metav1.CreateOptions{})
   691  	if err != nil {
   692  		switch {
   693  		case k8serrors.IsAlreadyExists(err):
   694  			c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Pod already exists, reused")
   695  			return gs, nil
   696  		case k8serrors.IsInvalid(err):
   697  			loggerForGameServer(gs, c.baseLogger).WithField("pod", pod).Errorf("Pod created is invalid")
   698  			gs, err = c.moveToErrorState(ctx, gs, err.Error())
   699  			return gs, err
   700  		case k8serrors.IsForbidden(err):
   701  			loggerForGameServer(gs, c.baseLogger).WithField("pod", pod).Errorf("Pod created is forbidden")
   702  			gs, err = c.moveToErrorState(ctx, gs, err.Error())
   703  			return gs, err
   704  		default:
   705  			c.recorder.Eventf(gs, corev1.EventTypeWarning, string(gs.Status.State), "error creating Pod for GameServer %s", gs.Name)
   706  			return gs, errors.Wrapf(err, "error creating Pod for GameServer %s", gs.Name)
   707  		}
   708  	}
   709  	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State),
   710  		fmt.Sprintf("Pod %s created", pod.ObjectMeta.Name))
   711  
   712  	return gs, nil
   713  }
   714  
   715  // sidecar creates the sidecar container for a given GameServer
   716  func (c *Controller) sidecar(gs *agonesv1.GameServer) corev1.Container {
   717  	sidecar := corev1.Container{
   718  		Name:  sdkserverSidecarName,
   719  		Image: c.sidecarImage,
   720  		Env: []corev1.EnvVar{
   721  			{
   722  				Name:  "GAMESERVER_NAME",
   723  				Value: gs.ObjectMeta.Name,
   724  			},
   725  			{
   726  				Name: "POD_NAMESPACE",
   727  				ValueFrom: &corev1.EnvVarSource{
   728  					FieldRef: &corev1.ObjectFieldSelector{
   729  						FieldPath: "metadata.namespace",
   730  					},
   731  				},
   732  			},
   733  			{
   734  				Name:  "FEATURE_GATES",
   735  				Value: runtime.EncodeFeatures(),
   736  			},
   737  			{
   738  				Name:  "LOG_LEVEL",
   739  				Value: string(gs.Spec.SdkServer.LogLevel),
   740  			},
   741  			{
   742  				Name:  "REQUESTS_RATE_LIMIT",
   743  				Value: c.sidecarRequestsRateLimit.String(),
   744  			},
   745  		},
   746  		Resources: corev1.ResourceRequirements{},
   747  		LivenessProbe: &corev1.Probe{
   748  			ProbeHandler: corev1.ProbeHandler{
   749  				HTTPGet: &corev1.HTTPGetAction{
   750  					Path: "/healthz",
   751  					Port: intstr.FromInt(8080),
   752  				},
   753  			},
   754  			InitialDelaySeconds: 3,
   755  			PeriodSeconds:       3,
   756  		},
   757  	}
   758  
   759  	if gs.Spec.SdkServer.GRPCPort != 0 {
   760  		sidecar.Args = append(sidecar.Args, fmt.Sprintf("--grpc-port=%d", gs.Spec.SdkServer.GRPCPort))
   761  	}
   762  
   763  	if gs.Spec.SdkServer.HTTPPort != 0 {
   764  		sidecar.Args = append(sidecar.Args, fmt.Sprintf("--http-port=%d", gs.Spec.SdkServer.HTTPPort))
   765  	}
   766  
   767  	requests := corev1.ResourceList{}
   768  	if !c.sidecarCPURequest.IsZero() {
   769  		requests[corev1.ResourceCPU] = c.sidecarCPURequest
   770  	}
   771  	if !c.sidecarMemoryRequest.IsZero() {
   772  		requests[corev1.ResourceMemory] = c.sidecarMemoryRequest
   773  	}
   774  	sidecar.Resources.Requests = requests
   775  
   776  	limits := corev1.ResourceList{}
   777  	if !c.sidecarCPULimit.IsZero() {
   778  		limits[corev1.ResourceCPU] = c.sidecarCPULimit
   779  	}
   780  	if !c.sidecarMemoryLimit.IsZero() {
   781  		limits[corev1.ResourceMemory] = c.sidecarMemoryLimit
   782  	}
   783  	sidecar.Resources.Limits = limits
   784  
   785  	if c.alwaysPullSidecarImage {
   786  		sidecar.ImagePullPolicy = corev1.PullAlways
   787  	}
   788  
   789  	sidecar.SecurityContext = &corev1.SecurityContext{
   790  		AllowPrivilegeEscalation: ptr.To(false),
   791  		RunAsNonRoot:             ptr.To(true),
   792  		RunAsUser:                ptr.To(int64(c.sidecarRunAsUser)),
   793  	}
   794  
   795  	return sidecar
   796  }
   797  
   798  // addGameServerHealthCheck adds the http health check to the GameServer container
   799  func (c *Controller) addGameServerHealthCheck(gs *agonesv1.GameServer, pod *corev1.Pod) error {
   800  	if gs.Spec.Health.Disabled {
   801  		return nil
   802  	}
   803  
   804  	return gs.ApplyToPodContainer(pod, gs.Spec.Container, func(c corev1.Container) corev1.Container {
   805  		if c.LivenessProbe == nil {
   806  			c.LivenessProbe = &corev1.Probe{
   807  				ProbeHandler: corev1.ProbeHandler{
   808  					HTTPGet: &corev1.HTTPGetAction{
   809  						Path: "/gshealthz",
   810  						Port: intstr.FromInt(8080),
   811  					},
   812  				},
   813  				// The sidecar relies on kubelet to delay by InitialDelaySeconds after the
   814  				// container is started (after image pull, etc).
   815  				InitialDelaySeconds: gs.Spec.Health.InitialDelaySeconds,
   816  				PeriodSeconds:       gs.Spec.Health.PeriodSeconds,
   817  
   818  				// By the time /gshealthz returns unhealthy, the sidecar has already evaluated
   819  				// {FailureThreshold in a row} failed health checks, so in theory on the kubelet
   820  				// side, one failure is sufficient to know the game server is unhealthy. However,
   821  				// with only one failure, if the sidecar doesn't come up at all, we unnecessarily
   822  				// restart the game server. So use FailureThreshold as startup wiggle-room as well.
   823  				//
   824  				// Note that in general, FailureThreshold could also be infinite - the controller
   825  				// and sidecar are responsible for health management.
   826  				FailureThreshold: gs.Spec.Health.FailureThreshold,
   827  			}
   828  		}
   829  
   830  		return c
   831  	})
   832  }
   833  
   834  func (c *Controller) addSDKServerEnvVars(gs *agonesv1.GameServer, pod *corev1.Pod) {
   835  	for i := range pod.Spec.Containers {
   836  		c := &pod.Spec.Containers[i]
   837  		if c.Name != sdkserverSidecarName {
   838  			sdkEnvVars := sdkEnvironmentVariables(gs)
   839  			if sdkEnvVars == nil {
   840  				// If a gameserver was created before 1.1 when we started defaulting the grpc and http ports,
   841  				// don't change the container spec.
   842  				continue
   843  			}
   844  
   845  			// Filter out environment variables that have reserved names.
   846  			// From https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
   847  			env := c.Env[:0]
   848  			for _, e := range c.Env {
   849  				if !reservedEnvironmentVariableName(e.Name) {
   850  					env = append(env, e)
   851  				}
   852  			}
   853  			env = append(env, sdkEnvVars...)
   854  			c.Env = env
   855  			pod.Spec.Containers[i] = *c
   856  		}
   857  	}
   858  }
   859  
   860  func reservedEnvironmentVariableName(name string) bool {
   861  	return name == grpcPortEnvVar || name == httpPortEnvVar
   862  }
   863  
   864  func sdkEnvironmentVariables(gs *agonesv1.GameServer) []corev1.EnvVar {
   865  	var env []corev1.EnvVar
   866  	if gs.Spec.SdkServer.GRPCPort != 0 {
   867  		env = append(env, corev1.EnvVar{
   868  			Name:  grpcPortEnvVar,
   869  			Value: strconv.Itoa(int(gs.Spec.SdkServer.GRPCPort)),
   870  		})
   871  	}
   872  	if gs.Spec.SdkServer.HTTPPort != 0 {
   873  		env = append(env, corev1.EnvVar{
   874  			Name:  httpPortEnvVar,
   875  			Value: strconv.Itoa(int(gs.Spec.SdkServer.HTTPPort)),
   876  		})
   877  	}
   878  	return env
   879  }
   880  
   881  // syncGameServerStartingState looks for a pod that has been scheduled for this GameServer
   882  // and then sets the Status > Address and Ports values.
   883  func (c *Controller) syncGameServerStartingState(ctx context.Context, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) {
   884  	if !(gs.Status.State == agonesv1.GameServerStateStarting && gs.ObjectMeta.DeletionTimestamp.IsZero()) {
   885  		return gs, nil
   886  	}
   887  	if _, isDev := gs.GetDevAddress(); isDev {
   888  		return gs, nil
   889  	}
   890  
   891  	loggerForGameServer(gs, c.baseLogger).Debug("Syncing Starting GameServerState")
   892  
   893  	// there should be a pod (although it may not have a scheduled container),
   894  	// so if there is an error of any kind, then move this to queue backoff
   895  	pod, err := c.gameServerPod(gs)
   896  	if err != nil {
   897  		// expected to happen, so don't log it.
   898  		if k8serrors.IsNotFound(err) {
   899  			return nil, workerqueue.NewTraceError(err)
   900  		}
   901  
   902  		// do log if it's something other than NotFound, since that's weird.
   903  		return nil, err
   904  	}
   905  	if pod.Spec.NodeName == "" {
   906  		return gs, workerqueue.NewTraceError(errors.Errorf("node not yet populated for Pod %s", pod.ObjectMeta.Name))
   907  	}
   908  
   909  	// Ensure the pod IPs are populated
   910  	if len(pod.Status.PodIPs) == 0 {
   911  		return gs, workerqueue.NewTraceError(errors.Errorf("pod IPs not yet populated for Pod %s", pod.ObjectMeta.Name))
   912  	}
   913  
   914  	node, err := c.nodeLister.Get(pod.Spec.NodeName)
   915  	if err != nil {
   916  		return gs, errors.Wrapf(err, "error retrieving node %s for Pod %s", pod.Spec.NodeName, pod.ObjectMeta.Name)
   917  	}
   918  	gsCopy := gs.DeepCopy()
   919  	gsCopy, err = applyGameServerAddressAndPort(gsCopy, node, pod, c.controllerHooks.SyncPodPortsToGameServer)
   920  	if err != nil {
   921  		return gs, err
   922  	}
   923  
   924  	gsCopy.Status.State = agonesv1.GameServerStateScheduled
   925  	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
   926  	if err != nil {
   927  		return gs, errors.Wrapf(err, "error updating GameServer %s to Scheduled state", gs.Name)
   928  	}
   929  	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
   930  
   931  	return gs, nil
   932  }
   933  
   934  // syncGameServerRequestReadyState checks if the Game Server is Requesting to be ready,
   935  // and then adds the IP and Port information to the Status and marks the GameServer
   936  // as Ready
   937  func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) {
   938  	if !(gs.Status.State == agonesv1.GameServerStateRequestReady && gs.ObjectMeta.DeletionTimestamp.IsZero()) ||
   939  		gs.Status.State == agonesv1.GameServerStateUnhealthy {
   940  		return gs, nil
   941  	}
   942  	if _, isDev := gs.GetDevAddress(); isDev {
   943  		return gs, nil
   944  	}
   945  
   946  	loggerForGameServer(gs, c.baseLogger).Debug("Syncing RequestReady State")
   947  
   948  	gsCopy := gs.DeepCopy()
   949  
   950  	pod, err := c.gameServerPod(gs)
   951  	// NotFound should never happen, and if it does -- something bad happened,
   952  	// so go into workerqueue backoff.
   953  	if err != nil {
   954  		return nil, err
   955  	}
   956  
   957  	// if the address hasn't been populated, and the Ready request comes
   958  	// before the controller has had a chance to do it, then
   959  	// do it here instead
   960  	addressPopulated := false
   961  	if gs.Status.NodeName == "" {
   962  		addressPopulated = true
   963  		if pod.Spec.NodeName == "" {
   964  			return gs, workerqueue.NewTraceError(errors.Errorf("node not yet populated for Pod %s", pod.ObjectMeta.Name))
   965  		}
   966  		node, err := c.nodeLister.Get(pod.Spec.NodeName)
   967  		if err != nil {
   968  			return gs, errors.Wrapf(err, "error retrieving node %s for Pod %s", pod.Spec.NodeName, pod.ObjectMeta.Name)
   969  		}
   970  		gsCopy, err = applyGameServerAddressAndPort(gsCopy, node, pod, c.controllerHooks.SyncPodPortsToGameServer)
   971  		if err != nil {
   972  			return gs, err
   973  		}
   974  	}
   975  
   976  	gsCopy, err = c.applyGameServerReadyContainerIDAnnotation(ctx, gsCopy, pod)
   977  	if err != nil {
   978  		return gs, err
   979  	}
   980  
   981  	gsCopy.Status.State = agonesv1.GameServerStateReady
   982  	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
   983  	if err != nil {
   984  		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
   985  	}
   986  
   987  	if addressPopulated {
   988  		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
   989  	}
   990  	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
   991  	return gs, nil
   992  }
   993  
   994  // applyGameServerReadyContainerIDAnnotation updates the GameServer and its corresponding Pod with an annotation
   995  // indicating the ID of the container that is running the game server, once it's in a Running state.
   996  func (c *Controller) applyGameServerReadyContainerIDAnnotation(ctx context.Context, gsCopy *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, error) {
   997  	// if there is a sidecar container, we escape right away. On move to stable, this method can be deleted.
   998  	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
   999  		return gsCopy, nil
  1000  	}
  1001  
  1002  	// track the ready gameserver container, so we can determine that after this point, we should move to Unhealthy
  1003  	// if there is a container crash/restart after we move to Ready
  1004  	for _, cs := range pod.Status.ContainerStatuses {
  1005  		if cs.Name == gsCopy.Spec.Container {
  1006  			if _, ok := gsCopy.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]; !ok {
  1007  				// check to make sure this container is actually running. If there was a recent crash, the cache may
  1008  				// not yet have the newer, running container.
  1009  				if cs.State.Running == nil {
  1010  					return nil, workerqueue.NewTraceError(fmt.Errorf("game server container for GameServer %s in namespace %s is not currently running, try again", gsCopy.ObjectMeta.Name, gsCopy.ObjectMeta.Namespace))
  1011  				}
  1012  				gsCopy.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = cs.ContainerID
  1013  			}
  1014  			break
  1015  		}
  1016  	}
  1017  	// Verify that we found the game server container - we may have a stale cache where pod is missing ContainerStatuses.
  1018  	if _, ok := gsCopy.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]; !ok {
  1019  		return nil, workerqueue.NewTraceError(fmt.Errorf("game server container for GameServer %s in namespace %s not present in pod status, try again", gsCopy.ObjectMeta.Name, gsCopy.ObjectMeta.Namespace))
  1020  	}
  1021  
  1022  	// Also update the pod with the same annotation, so we can check if the Pod data is up-to-date, now and also in the HealthController.
  1023  	// But if it is already set, then ignore it, since we only need to do this one time.
  1024  	if _, ok := pod.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]; !ok {
  1025  		podCopy := pod.DeepCopy()
  1026  		if podCopy.ObjectMeta.Annotations == nil {
  1027  			podCopy.ObjectMeta.Annotations = map[string]string{}
  1028  		}
  1029  
  1030  		podCopy.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = gsCopy.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]
  1031  		if _, err := c.podGetter.Pods(pod.ObjectMeta.Namespace).Update(ctx, podCopy, metav1.UpdateOptions{}); err != nil {
  1032  			return nil, errors.Wrapf(err, "error updating ready annotation on Pod: %s", pod.ObjectMeta.Name)
  1033  		}
  1034  	}
  1035  	return gsCopy, nil
  1036  }
  1037  
  1038  // syncGameServerShutdownState deletes the GameServer (and therefore the backing Pod) if it is in shutdown state
  1039  func (c *Controller) syncGameServerShutdownState(ctx context.Context, gs *agonesv1.GameServer) error {
  1040  	if !(gs.Status.State == agonesv1.GameServerStateShutdown && gs.ObjectMeta.DeletionTimestamp.IsZero()) {
  1041  		return nil
  1042  	}
  1043  
  1044  	loggerForGameServer(gs, c.baseLogger).Debug("Syncing Shutdown State")
  1045  	// be explicit about where to delete.
  1046  	p := metav1.DeletePropagationBackground
  1047  	err := c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Delete(ctx, gs.ObjectMeta.Name, metav1.DeleteOptions{PropagationPolicy: &p})
  1048  	if err != nil {
  1049  		return errors.Wrapf(err, "error deleting Game Server %s", gs.ObjectMeta.Name)
  1050  	}
  1051  	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Deletion started")
  1052  	return nil
  1053  }
  1054  
  1055  // moveToErrorState moves the GameServer to the error state
  1056  func (c *Controller) moveToErrorState(ctx context.Context, gs *agonesv1.GameServer, msg string) (*agonesv1.GameServer, error) {
  1057  	gsCopy := gs.DeepCopy()
  1058  	if gsCopy.Annotations == nil {
  1059  		gsCopy.Annotations = make(map[string]string, 1)
  1060  	}
  1061  	gsCopy.Annotations[agonesv1.GameServerErroredAtAnnotation] = time.Now().Format(time.RFC3339)
  1062  	gsCopy.Status.State = agonesv1.GameServerStateError
  1063  
  1064  	gs, err := c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
  1065  	if err != nil {
  1066  		return gs, errors.Wrapf(err, "error moving GameServer %s to Error State", gs.ObjectMeta.Name)
  1067  	}
  1068  
  1069  	c.recorder.Event(gs, corev1.EventTypeWarning, string(gs.Status.State), msg)
  1070  	return gs, nil
  1071  }
  1072  
  1073  // gameServerPod returns the Pod for this Game Server, or an error if there are none,
  1074  // or it cannot be determined (there are more than one, which should not happen)
  1075  func (c *Controller) gameServerPod(gs *agonesv1.GameServer) (*corev1.Pod, error) {
  1076  	// If the game server is a dev server we do not create a pod for it, return an empty pod.
  1077  	if _, isDev := gs.GetDevAddress(); isDev {
  1078  		return &corev1.Pod{}, nil
  1079  	}
  1080  
  1081  	pod, err := c.podLister.Pods(gs.ObjectMeta.Namespace).Get(gs.ObjectMeta.Name)
  1082  
  1083  	// if not found, propagate this error up, so we can use it in checks
  1084  	if k8serrors.IsNotFound(err) {
  1085  		return nil, err
  1086  	}
  1087  
  1088  	if !metav1.IsControlledBy(pod, gs) {
  1089  		return nil, k8serrors.NewNotFound(corev1.Resource("pod"), gs.ObjectMeta.Name)
  1090  	}
  1091  
  1092  	return pod, errors.Wrapf(err, "error retrieving pod for GameServer %s", gs.ObjectMeta.Name)
  1093  }