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  }