agones.dev/agones@v1.53.0/pkg/apis/agones/v1/gameserver.go (about) 1 // Copyright 2017 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 v1 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "net" 21 "slices" 22 "strings" 23 24 "agones.dev/agones/pkg" 25 "agones.dev/agones/pkg/apis" 26 "agones.dev/agones/pkg/apis/agones" 27 "agones.dev/agones/pkg/util/apiserver" 28 "agones.dev/agones/pkg/util/runtime" 29 "github.com/pkg/errors" 30 "gomodules.xyz/jsonpatch/v2" 31 corev1 "k8s.io/api/core/v1" 32 "k8s.io/apimachinery/pkg/api/resource" 33 apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" 34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 35 "k8s.io/apimachinery/pkg/labels" 36 "k8s.io/apimachinery/pkg/util/validation/field" 37 ) 38 39 // GameServerState is the state for the GameServer 40 type GameServerState string 41 42 const ( 43 // GameServerStatePortAllocation is for when a dynamically allocating GameServer 44 // is being created, an open port needs to be allocated 45 GameServerStatePortAllocation GameServerState = "PortAllocation" 46 // GameServerStateCreating is before the Pod for the GameServer is being created 47 GameServerStateCreating GameServerState = "Creating" 48 // GameServerStateStarting is for when the Pods for the GameServer are being 49 // created but are not yet Scheduled 50 GameServerStateStarting GameServerState = "Starting" 51 // GameServerStateScheduled is for when we have determined that the Pod has been 52 // scheduled in the cluster -- basically, we have a NodeName 53 GameServerStateScheduled GameServerState = "Scheduled" 54 // GameServerStateRequestReady is when the GameServer has declared that it is ready 55 GameServerStateRequestReady GameServerState = "RequestReady" 56 // GameServerStateReady is when a GameServer is ready to take connections 57 // from Game clients 58 GameServerStateReady GameServerState = "Ready" 59 // GameServerStateShutdown is when the GameServer has shutdown and everything needs to be 60 // deleted from the cluster 61 GameServerStateShutdown GameServerState = "Shutdown" 62 // GameServerStateError is when something has gone wrong with the Gameserver and 63 // it cannot be resolved 64 GameServerStateError GameServerState = "Error" 65 // GameServerStateUnhealthy is when the GameServer has failed its health checks 66 GameServerStateUnhealthy GameServerState = "Unhealthy" 67 // GameServerStateReserved is for when a GameServer is reserved and therefore can be allocated but not removed 68 GameServerStateReserved GameServerState = "Reserved" 69 // GameServerStateAllocated is when the GameServer has been allocated to a session 70 GameServerStateAllocated GameServerState = "Allocated" 71 ) 72 73 // PortPolicy is the port policy for the GameServer 74 type PortPolicy string 75 76 const ( 77 // Static PortPolicy means that the user defines the hostPort to be used 78 // in the configuration. 79 Static PortPolicy = "Static" 80 // Dynamic PortPolicy means that the system will choose an open 81 // port for the GameServer in question 82 Dynamic PortPolicy = "Dynamic" 83 // Passthrough dynamically sets the `containerPort` to the same value as the dynamically selected hostPort. 84 // This will mean that users will need to lookup what port has been opened through the server side SDK. 85 Passthrough PortPolicy = "Passthrough" 86 // None means the `hostPort` is ignored and if defined, the `containerPort` (optional) is used to set the port on the GameServer instance. 87 None PortPolicy = "None" 88 ) 89 90 // EvictionSafe specified whether the game server supports termination via SIGTERM 91 type EvictionSafe string 92 93 const ( 94 // EvictionSafeAlways means the game server supports termination via SIGTERM, and wants eviction signals 95 // from Cluster Autoscaler scaledown and node upgrades. 96 EvictionSafeAlways EvictionSafe = "Always" 97 // EvictionSafeOnUpgrade means the game server supports termination via SIGTERM, and wants eviction signals 98 // from node upgrades, but not Cluster Autoscaler scaledown. 99 EvictionSafeOnUpgrade EvictionSafe = "OnUpgrade" 100 // EvictionSafeNever means the game server should run to completion and may not understand SIGTERM. Eviction 101 // from ClusterAutoscaler and upgrades should both be blocked. 102 EvictionSafeNever EvictionSafe = "Never" 103 ) 104 105 // SdkServerLogLevel is the log level for SDK server (sidecar) logs 106 type SdkServerLogLevel string 107 108 const ( 109 // SdkServerLogLevelInfo will cause the SDK server to output all messages except for debug messages. 110 SdkServerLogLevelInfo SdkServerLogLevel = "Info" 111 // SdkServerLogLevelDebug will cause the SDK server to output all messages including debug messages. 112 SdkServerLogLevelDebug SdkServerLogLevel = "Debug" 113 // SdkServerLogLevelError will cause the SDK server to only output error messages. 114 SdkServerLogLevelError SdkServerLogLevel = "Error" 115 // SdkServerLogLevelTrace will cause the SDK server to output all messages, including detailed tracing information. 116 SdkServerLogLevelTrace SdkServerLogLevel = "Trace" 117 ) 118 119 const ( 120 // ProtocolTCPUDP Protocol exposes the hostPort allocated for this container for both TCP and UDP. 121 ProtocolTCPUDP corev1.Protocol = "TCPUDP" 122 123 // DefaultPortRange is the name of the default port range. 124 DefaultPortRange = "default" 125 126 // RoleLabel is the label in which the Agones role is specified. 127 // Pods from a GameServer will have the value "gameserver" 128 RoleLabel = agones.GroupName + "/role" 129 // GameServerLabelRole is the GameServer label value for RoleLabel 130 GameServerLabelRole = "gameserver" 131 // GameServerPodLabel is the label that the name of the GameServer 132 // is set on the Pod the GameServer controls 133 GameServerPodLabel = agones.GroupName + "/gameserver" 134 // GameServerPortPolicyPodLabel is the label to identify the port policy 135 // of the pod 136 GameServerPortPolicyPodLabel = agones.GroupName + "/port" 137 // GameServerContainerAnnotation is the annotation that stores 138 // which container is the container that runs the dedicated game server 139 GameServerContainerAnnotation = agones.GroupName + "/container" 140 // DevAddressAnnotation is an annotation to indicate that a GameServer hosted outside of Agones. 141 // A locally hosted GameServer is not managed by Agones it is just simply registered. 142 DevAddressAnnotation = "agones.dev/dev-address" 143 // GameServerReadyContainerIDAnnotation is an annotation that is set on the GameServer 144 // becomes ready, so we can track when restarts should occur and when a GameServer 145 // should be moved to Unhealthy. 146 GameServerReadyContainerIDAnnotation = agones.GroupName + "/ready-container-id" 147 // PodSafeToEvictAnnotation is an annotation that the Kubernetes cluster autoscaler uses to 148 // determine if a pod can safely be evicted to compact a cluster by moving pods between nodes 149 // and scaling down nodes. 150 PodSafeToEvictAnnotation = "cluster-autoscaler.kubernetes.io/safe-to-evict" 151 // SafeToEvictLabel is a label that, when "false", matches the restrictive PDB agones-gameserver-safe-to-evict-false. 152 SafeToEvictLabel = agones.GroupName + "/safe-to-evict" 153 // GameServerErroredAtAnnotation is an annotation that records the timestamp the GameServer entered the 154 // error state. The timestamp is encoded in RFC3339 format. 155 GameServerErroredAtAnnotation = agones.GroupName + "/errored-at" 156 // FinalizerName is the domain name and finalizer path used to manage garbage collection of the GameServer. 157 FinalizerName = agones.GroupName + "/controller" 158 159 // NodePodIP identifies an IP address from a pod. 160 NodePodIP corev1.NodeAddressType = "PodIP" 161 162 // PassthroughPortAssignmentAnnotation is an annotation to keep track of game server container and its Passthrough ports indices 163 PassthroughPortAssignmentAnnotation = "agones.dev/container-passthrough-port-assignment" 164 165 // True is the string "true" to appease the goconst lint. 166 True = "true" 167 // False is the string "false" to appease the goconst lint. 168 False = "false" 169 ) 170 171 var ( 172 // GameServerRolePodSelector is the selector to get all GameServer Pods 173 GameServerRolePodSelector = labels.SelectorFromSet(labels.Set{RoleLabel: GameServerLabelRole}) 174 175 // TerminalGameServerStates is a set (map[GameServerState]bool) of states from which a GameServer will not recover. 176 // From state diagram at https://agones.dev/site/docs/reference/gameserver/ 177 TerminalGameServerStates = map[GameServerState]bool{ 178 GameServerStateShutdown: true, 179 GameServerStateError: true, 180 GameServerStateUnhealthy: true, 181 } 182 ) 183 184 // +genclient 185 // +genclient:noStatus 186 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 187 188 // GameServer is the data structure for a GameServer resource. 189 // It is worth noting that while there is a `GameServerStatus` Status entry for the `GameServer`, it is not 190 // defined as a subresource - unlike `Fleet` and other Agones resources. 191 // This is so that we can retain the ability to change multiple aspects of a `GameServer` in a single atomic operation, 192 // which is particularly useful for operations such as allocation. 193 type GameServer struct { 194 metav1.TypeMeta `json:",inline"` 195 metav1.ObjectMeta `json:"metadata,omitempty"` 196 197 Spec GameServerSpec `json:"spec"` 198 Status GameServerStatus `json:"status"` 199 } 200 201 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 202 203 // GameServerList is a list of GameServer resources 204 type GameServerList struct { 205 metav1.TypeMeta `json:",inline"` 206 metav1.ListMeta `json:"metadata,omitempty"` 207 208 Items []GameServer `json:"items"` 209 } 210 211 // GameServerTemplateSpec is a template for GameServers 212 type GameServerTemplateSpec struct { 213 metav1.ObjectMeta `json:"metadata,omitempty"` 214 Spec GameServerSpec `json:"spec"` 215 } 216 217 // GameServerSpec is the spec for a GameServer resource 218 type GameServerSpec struct { 219 // Container specifies which Pod container is the game server. Only required if there is more than one 220 // container defined 221 Container string `json:"container,omitempty"` 222 // Ports are the array of ports that can be exposed via the game server 223 Ports []GameServerPort `json:"ports,omitempty"` 224 // Health configures health checking 225 Health Health `json:"health,omitempty"` 226 // Scheduling strategy. Defaults to "Packed" 227 Scheduling apis.SchedulingStrategy `json:"scheduling,omitempty"` 228 // SdkServer specifies parameters for the Agones SDK Server sidecar container 229 SdkServer SdkServer `json:"sdkServer,omitempty"` 230 // Template describes the Pod that will be created for the GameServer 231 Template corev1.PodTemplateSpec `json:"template"` 232 // (Alpha, PlayerTracking feature flag) Players provides the configuration for player tracking features. 233 // +optional 234 Players *PlayersSpec `json:"players,omitempty"` 235 // (Beta, CountsAndLists feature flag) Counters provides the configuration for tracking of int64 values against a GameServer. 236 // Keys must be declared at GameServer creation time. 237 // +optional 238 Counters map[string]CounterStatus `json:"counters,omitempty"` 239 // (Beta, CountsAndLists feature flag) Lists provides the configuration for tracking of lists of up to 1000 values against a GameServer. 240 // Keys must be declared at GameServer creation time. 241 // +optional 242 Lists map[string]ListStatus `json:"lists,omitempty"` 243 // Eviction specifies the eviction tolerance of the GameServer. Defaults to "Never". 244 // +optional 245 Eviction *Eviction `json:"eviction,omitempty"` 246 // immutableReplicas is present in gameservers.agones.dev but omitted here (it's always 1). 247 } 248 249 // PlayersSpec tracks the initial player capacity 250 type PlayersSpec struct { 251 InitialCapacity int64 `json:"initialCapacity,omitempty"` 252 } 253 254 // Eviction specifies the eviction tolerance of the GameServer 255 type Eviction struct { 256 // Game server supports termination via SIGTERM: 257 // - Always: Allow eviction for both Cluster Autoscaler and node drain for upgrades 258 // - OnUpgrade: Allow eviction for upgrades alone 259 // - Never (default): Pod should run to completion 260 Safe EvictionSafe `json:"safe,omitempty"` 261 } 262 263 // Health configures health checking on the GameServer 264 type Health struct { 265 // Disabled is whether health checking is disabled or not 266 Disabled bool `json:"disabled,omitempty"` 267 // PeriodSeconds is the number of seconds each health ping has to occur in 268 PeriodSeconds int32 `json:"periodSeconds,omitempty"` 269 // FailureThreshold how many failures in a row constitutes unhealthy 270 FailureThreshold int32 `json:"failureThreshold,omitempty"` 271 // InitialDelaySeconds initial delay before checking health 272 InitialDelaySeconds int32 `json:"initialDelaySeconds,omitempty"` 273 } 274 275 // GameServerPort defines a set of Ports that 276 // are to be exposed via the GameServer 277 type GameServerPort struct { 278 // Name is the descriptive name of the port 279 Name string `json:"name,omitempty"` 280 // (Alpha, PortRanges feature flag) Range is the port range name from which to select a port when using a 281 // 'Dynamic' or 'Passthrough' port policy. 282 // +optional 283 Range string `json:"range,omitempty"` 284 // PortPolicy defines the policy for how the HostPort is populated. 285 // Dynamic port will allocate a HostPort within the selected MIN_PORT and MAX_PORT range passed to the controller 286 // at installation time. 287 // When `Static` portPolicy is specified, `HostPort` is required, to specify the port that game clients will 288 // connect to 289 // `Passthrough` dynamically sets the `containerPort` to the same value as the dynamically selected hostPort. 290 // `None` portPolicy ignores `HostPort` and the `containerPort` (optional) is used to set the port on the GameServer instance. 291 PortPolicy PortPolicy `json:"portPolicy,omitempty"` 292 // Container is the name of the container on which to open the port. Defaults to the game server container. 293 // +optional 294 Container *string `json:"container,omitempty"` 295 // ContainerPort is the port that is being opened on the specified container's process 296 ContainerPort int32 `json:"containerPort,omitempty"` 297 // HostPort the port exposed on the host for clients to connect to 298 HostPort int32 `json:"hostPort,omitempty"` 299 // Protocol is the network protocol being used. Defaults to UDP. TCP and TCPUDP are other options. 300 Protocol corev1.Protocol `json:"protocol,omitempty"` 301 } 302 303 // SdkServer specifies parameters for the Agones SDK Server sidecar container 304 type SdkServer struct { 305 // LogLevel for SDK server (sidecar) logs. Defaults to "Info" 306 LogLevel SdkServerLogLevel `json:"logLevel,omitempty"` 307 // GRPCPort is the port on which the SDK Server binds the gRPC server to accept incoming connections 308 GRPCPort int32 `json:"grpcPort,omitempty"` 309 // HTTPPort is the port on which the SDK Server binds the HTTP gRPC gateway server to accept incoming connections 310 HTTPPort int32 `json:"httpPort,omitempty"` 311 } 312 313 // GameServerStatus is the status for a GameServer resource 314 type GameServerStatus struct { 315 // GameServerState is the current state of a GameServer, e.g. Creating, Starting, Ready, etc 316 State GameServerState `json:"state"` 317 Ports []GameServerStatusPort `json:"ports"` 318 Address string `json:"address"` 319 // Addresses is the array of addresses at which the GameServer can be reached; copy of Node.Status.addresses. 320 // +optional 321 Addresses []corev1.NodeAddress `json:"addresses"` 322 NodeName string `json:"nodeName"` 323 ReservedUntil *metav1.Time `json:"reservedUntil"` 324 // [Stage:Alpha] 325 // [FeatureFlag:PlayerTracking] 326 // +optional 327 Players *PlayerStatus `json:"players"` 328 // (Beta, CountsAndLists feature flag) Counters and Lists provides the configuration for generic tracking features. 329 // +optional 330 Counters map[string]CounterStatus `json:"counters,omitempty"` 331 // +optional 332 Lists map[string]ListStatus `json:"lists,omitempty"` 333 // Eviction specifies the eviction tolerance of the GameServer. 334 // +optional 335 Eviction *Eviction `json:"eviction,omitempty"` 336 // immutableReplicas is present in gameservers.agones.dev but omitted here (it's always 1). 337 } 338 339 // GameServerStatusPort shows the port that was allocated to a 340 // GameServer. 341 type GameServerStatusPort struct { 342 Name string `json:"name,omitempty"` 343 Port int32 `json:"port"` 344 } 345 346 // PlayerStatus stores the current player capacity values 347 type PlayerStatus struct { 348 Count int64 `json:"count"` 349 Capacity int64 `json:"capacity"` 350 IDs []string `json:"ids"` 351 } 352 353 // CounterStatus stores the current counter values and maximum capacity 354 type CounterStatus struct { 355 Count int64 `json:"count"` 356 Capacity int64 `json:"capacity"` 357 } 358 359 // ListStatus stores the current list values and maximum capacity 360 type ListStatus struct { 361 Capacity int64 `json:"capacity"` 362 Values []string `json:"values"` 363 } 364 365 // ApplyDefaults applies default values to the GameServer if they are not already populated 366 func (gs *GameServer) ApplyDefaults() { 367 // VersionAnnotation is the annotation that stores 368 // the version of sdk which runs in a sidecar 369 if gs.ObjectMeta.Annotations == nil { 370 gs.ObjectMeta.Annotations = map[string]string{} 371 } 372 gs.ObjectMeta.Annotations[VersionAnnotation] = pkg.Version 373 gs.ObjectMeta.Finalizers = append(gs.ObjectMeta.Finalizers, FinalizerName) 374 375 gs.Spec.ApplyDefaults() 376 gs.applyStatusDefaults() 377 } 378 379 // ApplyDefaults applies default values to the GameServerSpec if they are not already populated 380 func (gss *GameServerSpec) ApplyDefaults() { 381 gss.applyContainerDefaults() 382 gss.applyPortDefaults() 383 gss.applyHealthDefaults() 384 gss.applyEvictionDefaults() 385 gss.applySchedulingDefaults() 386 gss.applySdkServerDefaults() 387 } 388 389 // applySdkServerDefaults applies the default log level ("Info") for the sidecar 390 func (gss *GameServerSpec) applySdkServerDefaults() { 391 if gss.SdkServer.LogLevel == "" { 392 gss.SdkServer.LogLevel = SdkServerLogLevelInfo 393 } 394 if gss.SdkServer.GRPCPort == 0 { 395 gss.SdkServer.GRPCPort = 9357 396 } 397 if gss.SdkServer.HTTPPort == 0 { 398 gss.SdkServer.HTTPPort = 9358 399 } 400 } 401 402 // applyContainerDefaults applies the container defaults 403 func (gss *GameServerSpec) applyContainerDefaults() { 404 if len(gss.Template.Spec.Containers) == 1 { 405 gss.Container = gss.Template.Spec.Containers[0].Name 406 } 407 } 408 409 // applyHealthDefaults applies health checking defaults 410 func (gss *GameServerSpec) applyHealthDefaults() { 411 if !gss.Health.Disabled { 412 if gss.Health.PeriodSeconds <= 0 { 413 gss.Health.PeriodSeconds = 5 414 } 415 if gss.Health.FailureThreshold <= 0 { 416 gss.Health.FailureThreshold = 3 417 } 418 if gss.Health.InitialDelaySeconds <= 0 { 419 gss.Health.InitialDelaySeconds = 5 420 } 421 } 422 } 423 424 // applyStatusDefaults applies Status defaults 425 func (gs *GameServer) applyStatusDefaults() { 426 if gs.Status.State == "" { 427 gs.Status.State = GameServerStateCreating 428 // applyStatusDefaults() should be called after applyPortDefaults() 429 if gs.HasPortPolicy(Dynamic) || gs.HasPortPolicy(Passthrough) { 430 gs.Status.State = GameServerStatePortAllocation 431 } 432 } 433 434 if runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { 435 // set value if enabled, otherwise very easy to accidentally panic 436 // when gs.Status.Players is nil 437 if gs.Status.Players == nil { 438 gs.Status.Players = &PlayerStatus{} 439 } 440 if gs.Spec.Players != nil { 441 gs.Status.Players.Capacity = gs.Spec.Players.InitialCapacity 442 } 443 } 444 445 gs.applyEvictionStatus() 446 gs.applyCountsListsStatus() 447 } 448 449 // applyPortDefaults applies default values for all ports 450 func (gss *GameServerSpec) applyPortDefaults() { 451 for i, p := range gss.Ports { 452 // basic spec 453 if p.PortPolicy == "" { 454 gss.Ports[i].PortPolicy = Dynamic 455 } 456 457 if p.Range == "" { 458 gss.Ports[i].Range = DefaultPortRange 459 } 460 461 if p.Protocol == "" { 462 gss.Ports[i].Protocol = "UDP" 463 } 464 465 if p.Container == nil || *p.Container == "" { 466 gss.Ports[i].Container = &gss.Container 467 } 468 } 469 } 470 471 func (gss *GameServerSpec) applySchedulingDefaults() { 472 if gss.Scheduling == "" { 473 gss.Scheduling = apis.Packed 474 } 475 } 476 477 func (gss *GameServerSpec) applyEvictionDefaults() { 478 if gss.Eviction == nil { 479 gss.Eviction = &Eviction{} 480 } 481 if gss.Eviction.Safe == "" { 482 gss.Eviction.Safe = EvictionSafeNever 483 } 484 } 485 486 func (gs *GameServer) applyEvictionStatus() { 487 gs.Status.Eviction = gs.Spec.Eviction.DeepCopy() 488 if gs.Spec.Template.ObjectMeta.Annotations[PodSafeToEvictAnnotation] == "true" { 489 if gs.Status.Eviction == nil { 490 gs.Status.Eviction = &Eviction{} 491 } 492 gs.Status.Eviction.Safe = EvictionSafeAlways 493 } 494 } 495 496 func (gs *GameServer) applyCountsListsStatus() { 497 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 498 return 499 } 500 if gs.Spec.Counters != nil { 501 countersCopy := make(map[string]CounterStatus, len(gs.Spec.Counters)) 502 for key, val := range gs.Spec.Counters { 503 countersCopy[key] = *val.DeepCopy() 504 } 505 gs.Status.Counters = countersCopy 506 } 507 if gs.Spec.Lists != nil { 508 listsCopy := make(map[string]ListStatus, len(gs.Spec.Lists)) 509 for key, val := range gs.Spec.Lists { 510 listsCopy[key] = *val.DeepCopy() 511 } 512 gs.Status.Lists = listsCopy 513 } 514 } 515 516 // validateFeatureGates checks if fields are set when the associated feature gate is not set. 517 func (gss *GameServerSpec) validateFeatureGates(fldPath *field.Path) field.ErrorList { 518 var allErrs field.ErrorList 519 if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { 520 if gss.Players != nil { 521 allErrs = append(allErrs, field.Forbidden(fldPath.Child("players"), fmt.Sprintf("Value cannot be set unless feature flag %s is enabled", runtime.FeaturePlayerTracking))) 522 } 523 } 524 525 if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 526 if gss.Counters != nil { 527 allErrs = append(allErrs, field.Forbidden(fldPath.Child("counters"), fmt.Sprintf("Value cannot be set unless feature flag %s is enabled", runtime.FeatureCountsAndLists))) 528 } 529 if gss.Lists != nil { 530 allErrs = append(allErrs, field.Forbidden(fldPath.Child("lists"), fmt.Sprintf("Value cannot be set unless feature flag %s is enabled", runtime.FeatureCountsAndLists))) 531 } 532 } 533 534 if !runtime.FeatureEnabled(runtime.FeaturePortPolicyNone) { 535 for i, p := range gss.Ports { 536 if p.PortPolicy == None { 537 allErrs = append(allErrs, field.Forbidden(fldPath.Child("ports").Index(i).Child("portPolicy"), fmt.Sprintf("Value cannot be set to %s unless feature flag %s is enabled", None, runtime.FeaturePortPolicyNone))) 538 } 539 } 540 } 541 542 return allErrs 543 } 544 545 // Validate validates the GameServerSpec configuration. 546 // devAddress is a specific IP address used for local Gameservers, for fleets "" is used 547 // If a GameServer Spec is invalid there will be > 0 values in the returned array 548 func (gss *GameServerSpec) Validate(apiHooks APIHooks, devAddress string, fldPath *field.Path) field.ErrorList { 549 allErrs := gss.validateFeatureGates(fldPath) 550 if len(devAddress) > 0 { 551 // verify that the value is a valid IP address. 552 if net.ParseIP(devAddress) == nil { 553 // Authentication is only required if the gameserver is created directly. 554 allErrs = append(allErrs, field.Invalid(field.NewPath("metadata", "annotations", DevAddressAnnotation), devAddress, "must be a valid IP address")) 555 } 556 557 for i, p := range gss.Ports { 558 if p.HostPort == 0 { 559 allErrs = append(allErrs, field.Required(fldPath.Child("ports").Index(i).Child("hostPort"), DevAddressAnnotation)) 560 } 561 if p.PortPolicy != Static { 562 allErrs = append(allErrs, field.Required(fldPath.Child("ports").Index(i).Child("portPolicy"), ErrPortPolicyStatic)) 563 } 564 } 565 566 allErrs = append(allErrs, validateObjectMeta(&gss.Template.ObjectMeta, fldPath.Child("template", "metadata"))...) 567 return allErrs 568 } 569 570 // make sure a name is specified when there is multiple containers in the pod. 571 if gss.Container == "" && len(gss.Template.Spec.Containers) > 1 { 572 allErrs = append(allErrs, field.Required(fldPath.Child("container"), ErrContainerRequired)) 573 } 574 575 // make sure the container value points to a valid container 576 _, _, err := gss.FindContainer(gss.Container) 577 if err != nil { 578 allErrs = append(allErrs, field.Invalid(fldPath.Child("container"), gss.Container, err.Error())) 579 } 580 581 // no host port when using dynamic PortPolicy 582 for i, p := range gss.Ports { 583 path := fldPath.Child("ports").Index(i) 584 if p.PortPolicy == Dynamic || p.PortPolicy == Static { 585 if p.ContainerPort <= 0 { 586 allErrs = append(allErrs, field.Required(path.Child("containerPort"), ErrContainerPortRequired)) 587 } 588 } 589 590 if p.PortPolicy == Passthrough && p.ContainerPort > 0 { 591 allErrs = append(allErrs, field.Required(path.Child("containerPort"), ErrContainerPortPassthrough)) 592 } 593 594 if p.HostPort > 0 && (p.PortPolicy == Dynamic || p.PortPolicy == Passthrough) { 595 allErrs = append(allErrs, field.Forbidden(path.Child("hostPort"), ErrHostPort)) 596 } 597 598 if p.Container != nil && gss.Container != "" { 599 _, _, err := gss.FindContainer(*p.Container) 600 if err != nil { 601 allErrs = append(allErrs, field.Invalid(path.Child("container"), *p.Container, ErrContainerNameInvalid)) 602 } 603 } 604 } 605 for i, c := range gss.Template.Spec.Containers { 606 path := fldPath.Child("template", "spec", "containers").Index(i) 607 allErrs = append(allErrs, ValidateResourceRequirements(&c.Resources, path.Child("resources"))...) 608 } 609 610 allErrs = append(allErrs, apiHooks.ValidateGameServerSpec(gss, fldPath)...) 611 allErrs = append(allErrs, validateObjectMeta(&gss.Template.ObjectMeta, fldPath.Child("template", "metadata"))...) 612 return allErrs 613 } 614 615 // ValidateResourceRequirements Validates resource requirement spec. 616 func ValidateResourceRequirements(requirements *corev1.ResourceRequirements, fldPath *field.Path) field.ErrorList { 617 allErrs := field.ErrorList{} 618 limPath := fldPath.Child("limits") 619 reqPath := fldPath.Child("requests") 620 621 for resourceName, quantity := range requirements.Limits { 622 fldPath := limPath.Key(string(resourceName)) 623 // Validate resource quantity. 624 allErrs = append(allErrs, ValidateNonnegativeQuantity(quantity, fldPath)...) 625 626 } 627 628 for resourceName, quantity := range requirements.Requests { 629 fldPath := reqPath.Key(string(resourceName)) 630 // Validate resource quantity. 631 allErrs = append(allErrs, ValidateNonnegativeQuantity(quantity, fldPath)...) 632 633 // Check that request <= limit. 634 limitQuantity, exists := requirements.Limits[resourceName] 635 if exists && quantity.Cmp(limitQuantity) > 0 { 636 allErrs = append(allErrs, field.Invalid(reqPath, quantity.String(), fmt.Sprintf("must be less than or equal to %s limit of %s", resourceName, limitQuantity.String()))) 637 } 638 } 639 return allErrs 640 } 641 642 // ValidateNonnegativeQuantity Validates that a Quantity is not negative 643 func ValidateNonnegativeQuantity(value resource.Quantity, fldPath *field.Path) field.ErrorList { 644 allErrs := field.ErrorList{} 645 if value.Cmp(resource.Quantity{}) < 0 { 646 allErrs = append(allErrs, field.Invalid(fldPath, value.String(), apimachineryvalidation.IsNegativeErrorMsg)) 647 } 648 return allErrs 649 } 650 651 // Validate validates the GameServer configuration. 652 // If a GameServer is invalid there will be > 0 values in 653 // the returned array 654 func (gs *GameServer) Validate(apiHooks APIHooks) field.ErrorList { 655 allErrs := validateName(gs, field.NewPath("metadata")) 656 657 // make sure the host port is specified if this is a development server 658 devAddress, _ := gs.GetDevAddress() 659 allErrs = append(allErrs, gs.Spec.Validate(apiHooks, devAddress, field.NewPath("spec"))...) 660 return allErrs 661 } 662 663 // GetDevAddress returns the address for game server. 664 func (gs *GameServer) GetDevAddress() (string, bool) { 665 devAddress, hasDevAddress := gs.ObjectMeta.Annotations[DevAddressAnnotation] 666 return devAddress, hasDevAddress 667 } 668 669 // IsDeletable returns false if the server is currently allocated/reserved and is not already in the 670 // process of being deleted 671 func (gs *GameServer) IsDeletable() bool { 672 if gs.Status.State == GameServerStateAllocated || gs.Status.State == GameServerStateReserved { 673 return !gs.ObjectMeta.DeletionTimestamp.IsZero() 674 } 675 676 return true 677 } 678 679 // IsBeingDeleted returns true if the server is in the process of being deleted. 680 func (gs *GameServer) IsBeingDeleted() bool { 681 return !gs.ObjectMeta.DeletionTimestamp.IsZero() || gs.Status.State == GameServerStateShutdown 682 } 683 684 // IsBeforeReady returns true if the GameServer Status has yet to move to or past the Ready 685 // state in its lifecycle, such as Allocated or Reserved, or any of the Error/Unhealthy states 686 func (gs *GameServer) IsBeforeReady() bool { 687 switch gs.Status.State { 688 case GameServerStatePortAllocation: 689 return true 690 case GameServerStateCreating: 691 return true 692 case GameServerStateStarting: 693 return true 694 case GameServerStateScheduled: 695 return true 696 case GameServerStateRequestReady: 697 return true 698 } 699 700 return false 701 } 702 703 // IsActive returns true if the GameServer status is Ready, Reserved, or Allocated state. 704 func (gs *GameServer) IsActive() bool { 705 switch gs.Status.State { 706 case GameServerStateAllocated: 707 return true 708 case GameServerStateReady: 709 return true 710 case GameServerStateReserved: 711 return true 712 } 713 714 return false 715 } 716 717 // FindContainer returns the container specified by the name parameter. Returns the index and the value. 718 // Returns an error if not found. 719 func (gss *GameServerSpec) FindContainer(name string) (int, corev1.Container, error) { 720 for i, c := range gss.Template.Spec.Containers { 721 if c.Name == name { 722 return i, c, nil 723 } 724 } 725 726 return -1, corev1.Container{}, errors.Errorf("Could not find a container named %s", name) 727 } 728 729 // ApplyToPodContainer applies func(v1.Container) to the specified container in the pod. 730 // Returns an error if the container is not found. 731 func (gs *GameServer) ApplyToPodContainer(pod *corev1.Pod, containerName string, f func(corev1.Container) corev1.Container) error { 732 for i, c := range pod.Spec.Containers { 733 if c.Name == containerName { 734 pod.Spec.Containers[i] = f(c) 735 return nil 736 } 737 } 738 return errors.Errorf("failed to find container named %s in pod spec", containerName) 739 } 740 741 // Pod creates a new Pod from the PodTemplateSpec 742 // attached to the GameServer resource 743 func (gs *GameServer) Pod(apiHooks APIHooks, sidecars ...corev1.Container) (*corev1.Pod, error) { 744 pod := &corev1.Pod{ 745 ObjectMeta: *gs.Spec.Template.ObjectMeta.DeepCopy(), 746 Spec: *gs.Spec.Template.Spec.DeepCopy(), 747 } 748 749 if len(pod.Spec.Hostname) == 0 { 750 // replace . with - since it must match RFC 1123 751 pod.Spec.Hostname = strings.ReplaceAll(gs.ObjectMeta.Name, ".", "-") 752 } 753 754 gs.podObjectMeta(pod) 755 756 passthroughContainerPortMap := make(map[string][]int) 757 for _, p := range gs.Spec.Ports { 758 var hostPort int32 759 portIdx := 0 760 761 if !runtime.FeatureEnabled(runtime.FeaturePortPolicyNone) || p.PortPolicy != None { 762 hostPort = p.HostPort 763 } 764 765 cp := corev1.ContainerPort{ 766 ContainerPort: p.ContainerPort, 767 HostPort: hostPort, 768 Protocol: p.Protocol, 769 } 770 err := gs.ApplyToPodContainer(pod, *p.Container, func(c corev1.Container) corev1.Container { 771 portIdx = len(c.Ports) 772 c.Ports = append(c.Ports, cp) 773 774 return c 775 }) 776 if err != nil { 777 return nil, err 778 } 779 if runtime.FeatureEnabled(runtime.FeatureAutopilotPassthroughPort) && p.PortPolicy == Passthrough { 780 passthroughContainerPortMap[*p.Container] = append(passthroughContainerPortMap[*p.Container], portIdx) 781 } 782 } 783 784 if len(passthroughContainerPortMap) != 0 { 785 containerToPassthroughMapJSON, err := json.Marshal(passthroughContainerPortMap) 786 if err != nil { 787 return nil, err 788 } 789 pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation] = string(containerToPassthroughMapJSON) 790 } 791 792 if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) { 793 // make sure all sidecars have a restart policy of Always, so they are valid sidecar containers. 794 // https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/#sidecar-containers-and-pod-lifecycle 795 always := corev1.ContainerRestartPolicyAlways 796 for i := range sidecars { 797 sidecars[i].RestartPolicy = &always 798 } 799 800 // addSidecarsAsInitContainers puts the sidecars in the initContainers list so that they can have their own independent 801 // container restart policies. 802 pod.Spec.InitContainers = slices.Concat(sidecars, pod.Spec.InitContainers) 803 804 // default GameServer container should also be Restart: Never 805 if len(pod.Spec.RestartPolicy) == 0 { 806 pod.Spec.RestartPolicy = corev1.RestartPolicyNever 807 } 808 } else { 809 gs.addSidecarsAsContainers(sidecars, pod) 810 } 811 812 gs.podScheduling(pod) 813 814 if err := apiHooks.MutateGameServerPod(&gs.Spec, pod); err != nil { 815 return nil, err 816 } 817 if err := apiHooks.SetEviction(gs.Status.Eviction, pod); err != nil { 818 return nil, err 819 } 820 821 return pod, nil 822 } 823 824 // addSidecarsAsContainers puts the sidecars at the start of the general list of containers so that the kubelet starts 825 // them first 826 func (gs *GameServer) addSidecarsAsContainers(sidecars []corev1.Container, pod *corev1.Pod) { 827 containers := make([]corev1.Container, 0, len(sidecars)+len(pod.Spec.Containers)) 828 containers = append(containers, sidecars...) 829 containers = append(containers, pod.Spec.Containers...) 830 pod.Spec.Containers = containers 831 } 832 833 // podObjectMeta configures the pod ObjectMeta details 834 func (gs *GameServer) podObjectMeta(pod *corev1.Pod) { 835 pod.ObjectMeta.GenerateName = "" 836 // Pods inherit the name of their gameserver. It's safe since there's 837 // a guarantee that pod won't outlive its parent. 838 pod.ObjectMeta.Name = gs.ObjectMeta.Name 839 // Pods for GameServers need to stay in the same namespace 840 pod.ObjectMeta.Namespace = gs.ObjectMeta.Namespace 841 // Make sure these are blank, just in case 842 pod.ObjectMeta.ResourceVersion = "" 843 pod.ObjectMeta.UID = "" 844 if pod.ObjectMeta.Labels == nil { 845 pod.ObjectMeta.Labels = make(map[string]string, 2) 846 } 847 if pod.ObjectMeta.Annotations == nil { 848 pod.ObjectMeta.Annotations = make(map[string]string, 2) 849 } 850 pod.ObjectMeta.Labels[RoleLabel] = GameServerLabelRole 851 // store the GameServer name as a label, for easy lookup later on 852 pod.ObjectMeta.Labels[GameServerPodLabel] = gs.ObjectMeta.Name 853 // store the GameServer container as an annotation, to make lookup at a Pod level easier 854 pod.ObjectMeta.Annotations[GameServerContainerAnnotation] = gs.Spec.Container 855 ref := metav1.NewControllerRef(gs, SchemeGroupVersion.WithKind("GameServer")) 856 pod.ObjectMeta.OwnerReferences = append(pod.ObjectMeta.OwnerReferences, *ref) 857 858 // Add Agones version into Pod Annotations 859 pod.ObjectMeta.Annotations[VersionAnnotation] = pkg.Version 860 } 861 862 // podScheduling applies the Fleet scheduling strategy to the passed in Pod 863 // this sets the a PreferredDuringSchedulingIgnoredDuringExecution for GameServer 864 // pods to a host topology. Basically doing a half decent job of packing GameServer 865 // pods together. 866 func (gs *GameServer) podScheduling(pod *corev1.Pod) { 867 if gs.Spec.Scheduling == apis.Packed { 868 if pod.Spec.Affinity == nil { 869 pod.Spec.Affinity = &corev1.Affinity{} 870 } 871 if pod.Spec.Affinity.PodAffinity == nil { 872 pod.Spec.Affinity.PodAffinity = &corev1.PodAffinity{} 873 } 874 875 wpat := corev1.WeightedPodAffinityTerm{ 876 Weight: 100, 877 PodAffinityTerm: corev1.PodAffinityTerm{ 878 TopologyKey: "kubernetes.io/hostname", 879 LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{RoleLabel: GameServerLabelRole}}, 880 }, 881 } 882 883 pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution = append(pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution, wpat) 884 } 885 } 886 887 // DisableServiceAccount disables the service account for the gameserver container 888 func (gs *GameServer) DisableServiceAccount(pod *corev1.Pod) error { 889 // gameservers don't get access to the k8s api. 890 emptyVol := corev1.Volume{Name: "empty", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}} 891 pod.Spec.Volumes = append(pod.Spec.Volumes, emptyVol) 892 mount := corev1.VolumeMount{MountPath: "/var/run/secrets/kubernetes.io/serviceaccount", Name: emptyVol.Name, ReadOnly: true} 893 894 return gs.ApplyToPodContainer(pod, gs.Spec.Container, func(c corev1.Container) corev1.Container { 895 c.VolumeMounts = append(c.VolumeMounts, mount) 896 897 return c 898 }) 899 } 900 901 // HasPortPolicy checks if there is a port with a given 902 // PortPolicy 903 func (gs *GameServer) HasPortPolicy(policy PortPolicy) bool { 904 for _, p := range gs.Spec.Ports { 905 if p.PortPolicy == policy { 906 return true 907 } 908 } 909 return false 910 } 911 912 // Status returns a GameServerStatusPort for this GameServerPort 913 func (p GameServerPort) Status() GameServerStatusPort { 914 if runtime.FeatureEnabled(runtime.FeaturePortPolicyNone) && p.PortPolicy == None { 915 return GameServerStatusPort{Name: p.Name, Port: p.ContainerPort} 916 } 917 918 return GameServerStatusPort{Name: p.Name, Port: p.HostPort} 919 } 920 921 // CountPorts returns the number of 922 // ports that match condition function 923 func (gs *GameServer) CountPorts(f func(policy PortPolicy) bool) int { 924 count := 0 925 for _, p := range gs.Spec.Ports { 926 if f(p.PortPolicy) { 927 count++ 928 } 929 } 930 return count 931 } 932 933 // CountPortsForRange returns the number of ports that match condition function and range name. 934 func (gs *GameServer) CountPortsForRange(name string, f func(policy PortPolicy) bool) int { 935 count := 0 936 for _, p := range gs.Spec.Ports { 937 if p.Range == name && f(p.PortPolicy) { 938 count++ 939 } 940 } 941 return count 942 } 943 944 // Patch creates a JSONPatch to move the current GameServer to the passed in delta GameServer. 945 // Returned Patch includes a "test" operation that will cause the GameServers.Patch() operation to 946 // fail if the Game Server has been updated (ResourceVersion has changed) in between when the Patch 947 // was created and applied. 948 func (gs *GameServer) Patch(delta *GameServer) ([]byte, error) { 949 var result []byte 950 951 oldJSON, err := json.Marshal(gs) 952 if err != nil { 953 return result, errors.Wrapf(err, "error marshalling to json current GameServer %s", gs.ObjectMeta.Name) 954 } 955 956 newJSON, err := json.Marshal(delta) 957 if err != nil { 958 return result, errors.Wrapf(err, "error marshalling to json delta GameServer %s", delta.ObjectMeta.Name) 959 } 960 961 patch, err := jsonpatch.CreatePatch(oldJSON, newJSON) 962 if err != nil { 963 return result, errors.Wrapf(err, "error creating patch for GameServer %s", gs.ObjectMeta.Name) 964 } 965 966 // Per https://jsonpatch.com/ "Tests that the specified value is set in the document. If the test 967 // fails, then the patch as a whole should not apply." 968 // Used here to check the object has not been updated (has not changed ResourceVersion). 969 patches := []jsonpatch.JsonPatchOperation{{Operation: "test", Path: "/metadata/resourceVersion", Value: gs.ObjectMeta.ResourceVersion}} 970 patches = append(patches, patch...) 971 972 result, err = json.Marshal(patches) 973 return result, errors.Wrapf(err, "error creating json for patch for GameServer %s", gs.ObjectMeta.Name) 974 } 975 976 // UpdateCount increments or decrements a CounterStatus on a Game Server by the given amount. 977 func (gs *GameServer) UpdateCount(name string, action string, amount int64) error { 978 if !(action == GameServerPriorityIncrement || action == GameServerPriorityDecrement) { 979 return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Allocation action must be one of %s or %s", name, action, amount, GameServerPriorityIncrement, GameServerPriorityDecrement) 980 } 981 if amount < 0 { 982 return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Amount must be greater than 0", name, action, amount) 983 } 984 if counter, ok := gs.Status.Counters[name]; ok { 985 cnt := counter.Count 986 if action == GameServerPriorityIncrement { 987 cnt += amount 988 } else { 989 cnt -= amount 990 } 991 // Truncate to Capacity if Count > Capacity 992 if cnt > counter.Capacity { 993 cnt = counter.Capacity 994 } 995 // Truncate to Zero if Count is negative 996 if cnt < 0 { 997 cnt = 0 998 } 999 counter.Count = cnt 1000 gs.Status.Counters[name] = counter 1001 return nil 1002 } 1003 return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Counter not found in GameServer %s", name, action, amount, gs.ObjectMeta.GetName()) 1004 } 1005 1006 // UpdateCounterCapacity updates the CounterStatus Capacity to the given capacity. 1007 func (gs *GameServer) UpdateCounterCapacity(name string, capacity int64) error { 1008 if capacity < 0 { 1009 return errors.Errorf("unable to UpdateCounterCapacity: Name %s, Capacity %d. Capacity must be greater than or equal to 0", name, capacity) 1010 } 1011 if counter, ok := gs.Status.Counters[name]; ok { 1012 counter.Capacity = capacity 1013 // If Capacity is now less than Count, reset Count here to equal Capacity 1014 if counter.Count > counter.Capacity { 1015 counter.Count = counter.Capacity 1016 } 1017 gs.Status.Counters[name] = counter 1018 return nil 1019 } 1020 return errors.Errorf("unable to UpdateCounterCapacity: Name %s, Capacity %d. Counter not found in GameServer %s", name, capacity, gs.ObjectMeta.GetName()) 1021 } 1022 1023 // UpdateListCapacity updates the ListStatus Capacity to the given capacity. 1024 func (gs *GameServer) UpdateListCapacity(name string, capacity int64) error { 1025 if capacity < 0 || capacity > apiserver.ListMaxCapacity { 1026 return errors.Errorf("unable to UpdateListCapacity: Name %s, Capacity %d. Capacity must be between 0 and 1000, inclusive", name, capacity) 1027 } 1028 if list, ok := gs.Status.Lists[name]; ok { 1029 list.Capacity = capacity 1030 list.Values = truncateList(list.Capacity, list.Values) 1031 gs.Status.Lists[name] = list 1032 return nil 1033 } 1034 return errors.Errorf("unable to UpdateListCapacity: Name %s, Capacity %d. List not found in GameServer %s", name, capacity, gs.ObjectMeta.GetName()) 1035 } 1036 1037 // AppendListValues adds unique values to the ListStatus Values list. 1038 func (gs *GameServer) AppendListValues(name string, values []string) error { 1039 if values == nil { 1040 return errors.Errorf("unable to AppendListValues: Name %s, Values %s. Values must not be nil", name, values) 1041 } 1042 if list, ok := gs.Status.Lists[name]; ok { 1043 mergedList := MergeRemoveDuplicates(list.Values, values) 1044 // Any duplicate values are silently dropped. 1045 list.Values = mergedList 1046 list.Values = truncateList(list.Capacity, list.Values) 1047 gs.Status.Lists[name] = list 1048 return nil 1049 } 1050 return errors.Errorf("unable to AppendListValues: Name %s, Values %s. List not found in GameServer %s", name, values, gs.ObjectMeta.GetName()) 1051 } 1052 1053 // DeleteListValues removes values from the ListStatus Values list. Values in the DeleteListValues 1054 // list that are not in the ListStatus Values list are ignored. 1055 func (gs *GameServer) DeleteListValues(name string, values []string) error { 1056 if values == nil { 1057 return errors.Errorf("unable to DeleteListValues: Name %s, Values %s. Values must not be nil", name, values) 1058 } 1059 if list, ok := gs.Status.Lists[name]; ok { 1060 deleteValuesMap := make(map[string]bool) 1061 for _, value := range values { 1062 deleteValuesMap[value] = true 1063 } 1064 newList := deleteValues(list.Values, deleteValuesMap) 1065 list.Values = newList 1066 gs.Status.Lists[name] = list 1067 return nil 1068 } 1069 return errors.Errorf("unable to DeleteListValues: Name %s, Values %s. List not found in GameServer %s", name, values, gs.ObjectMeta.GetName()) 1070 } 1071 1072 // deleteValues returns a new list with all the values in valuesList that are not keys in deleteValuesMap. 1073 func deleteValues(valuesList []string, deleteValuesMap map[string]bool) []string { 1074 newValuesList := []string{} 1075 for _, value := range valuesList { 1076 if _, ok := deleteValuesMap[value]; ok { 1077 continue 1078 } 1079 newValuesList = append(newValuesList, value) 1080 } 1081 return newValuesList 1082 } 1083 1084 // truncateList truncates the list to the given capacity 1085 func truncateList(capacity int64, list []string) []string { 1086 if list == nil || len(list) <= int(capacity) { 1087 return list 1088 } 1089 list = append([]string{}, list[:capacity]...) 1090 return list 1091 } 1092 1093 // MergeRemoveDuplicates merges two lists and removes any duplicate values. 1094 // Maintains ordering, so new values from list2 are appended to the end of list1. 1095 // Returns a new list with unique values only. 1096 func MergeRemoveDuplicates(list1 []string, list2 []string) []string { 1097 uniqueList := []string{} 1098 listMap := make(map[string]bool) 1099 for _, v1 := range list1 { 1100 if _, ok := listMap[v1]; !ok { 1101 uniqueList = append(uniqueList, v1) 1102 listMap[v1] = true 1103 } 1104 } 1105 for _, v2 := range list2 { 1106 if _, ok := listMap[v2]; !ok { 1107 uniqueList = append(uniqueList, v2) 1108 listMap[v2] = true 1109 } 1110 } 1111 return uniqueList 1112 } 1113 1114 // CompareCountAndListPriorities compares two game servers based on a list of CountsAndLists Priorities using available 1115 // capacity as the comparison. 1116 func (gs *GameServer) CompareCountAndListPriorities(priorities []Priority, other *GameServer) *bool { 1117 for _, priority := range priorities { 1118 res := gs.compareCountAndListPriority(&priority, other) 1119 if res != nil { 1120 // reverse if descending 1121 if priority.Order == GameServerPriorityDescending { 1122 flip := !*res 1123 return &flip 1124 } 1125 1126 return res 1127 } 1128 } 1129 1130 return nil 1131 } 1132 1133 // compareCountAndListPriority compares two game servers based on a CountsAndLists Priority using available 1134 // capacity (Capacity - Count for Counters, and Capacity - len(Values) for Lists) as the comparison. 1135 // Returns true if gs1 < gs2; false if gs1 > gs2; nil if gs1 == gs2; nil if neither gamer server has the Priority. 1136 // If only one game server has the Priority, prefer that server. I.e. nil < gsX when Priority 1137 // Order is Descending (3, 2, 1, 0, nil), and nil > gsX when Order is Ascending (0, 1, 2, 3, nil). 1138 func (gs *GameServer) compareCountAndListPriority(p *Priority, other *GameServer) *bool { 1139 var gs1ok, gs2ok bool 1140 t := true 1141 f := false 1142 switch p.Type { 1143 case GameServerPriorityCounter: 1144 // Check if both game servers contain the Counter. 1145 counter1, ok1 := gs.Status.Counters[p.Key] 1146 counter2, ok2 := other.Status.Counters[p.Key] 1147 // If both game servers have the Counter 1148 if ok1 && ok2 { 1149 availCapacity1 := counter1.Capacity - counter1.Count 1150 availCapacity2 := counter2.Capacity - counter2.Count 1151 if availCapacity1 < availCapacity2 { 1152 return &t 1153 } 1154 if availCapacity1 > availCapacity2 { 1155 return &f 1156 } 1157 if availCapacity1 == availCapacity2 { 1158 return nil 1159 } 1160 } 1161 gs1ok = ok1 1162 gs2ok = ok2 1163 case GameServerPriorityList: 1164 // Check if both game servers contain the List. 1165 list1, ok1 := gs.Status.Lists[p.Key] 1166 list2, ok2 := other.Status.Lists[p.Key] 1167 // If both game servers have the List 1168 if ok1 && ok2 { 1169 availCapacity1 := list1.Capacity - int64(len(list1.Values)) 1170 availCapacity2 := list2.Capacity - int64(len(list2.Values)) 1171 if availCapacity1 < availCapacity2 { 1172 return &t 1173 } 1174 if availCapacity1 > availCapacity2 { 1175 return &f 1176 } 1177 if availCapacity1 == availCapacity2 { 1178 return nil 1179 } 1180 } 1181 gs1ok = ok1 1182 gs2ok = ok2 1183 } 1184 // If only one game server has the Priority, prefer that server. I.e. nil < gsX when Order is 1185 // Descending (3, 2, 1, 0, nil), and nil > gsX when Order is Ascending (0, 1, 2, 3, nil). 1186 if (gs1ok && p.Order == GameServerPriorityDescending) || 1187 (gs2ok && p.Order == GameServerPriorityAscending) { 1188 return &f 1189 } 1190 if (gs1ok && p.Order == GameServerPriorityAscending) || 1191 (gs2ok && p.Order == GameServerPriorityDescending) { 1192 return &t 1193 } 1194 // If neither game server has the Priority 1195 return nil 1196 }