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