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 }