agones.dev/agones@v1.53.0/pkg/apis/allocation/v1/gameserverallocation.go (about)

     1  // Copyright 2019 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  	"errors"
    19  	"fmt"
    20  
    21  	"agones.dev/agones/pkg/apis"
    22  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    23  	"agones.dev/agones/pkg/util/runtime"
    24  	"github.com/mitchellh/hashstructure/v2"
    25  	corev1 "k8s.io/api/core/v1"
    26  	apivalidation "k8s.io/apimachinery/pkg/api/validation"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
    29  	"k8s.io/apimachinery/pkg/labels"
    30  	"k8s.io/apimachinery/pkg/util/validation/field"
    31  )
    32  
    33  const (
    34  	// GameServerAllocationAllocated is allocation successful
    35  	GameServerAllocationAllocated GameServerAllocationState = "Allocated"
    36  	// GameServerAllocationUnAllocated when the allocation is unsuccessful
    37  	GameServerAllocationUnAllocated GameServerAllocationState = "UnAllocated"
    38  	// GameServerAllocationContention when the allocation is unsuccessful
    39  	// because of contention
    40  	GameServerAllocationContention GameServerAllocationState = "Contention"
    41  )
    42  
    43  // GameServerAllocationState is the Allocation state
    44  type GameServerAllocationState string
    45  
    46  // +genclient
    47  // +genclient:onlyVerbs=create
    48  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    49  
    50  // GameServerAllocation is the data structure for allocating against a set of
    51  // GameServers, defined `selectors` selectors
    52  type GameServerAllocation struct {
    53  	metav1.TypeMeta   `json:",inline"`
    54  	metav1.ObjectMeta `json:"metadata,omitempty"`
    55  	Spec              GameServerAllocationSpec   `json:"spec"`
    56  	Status            GameServerAllocationStatus `json:"status,omitempty"`
    57  }
    58  
    59  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    60  
    61  // GameServerAllocationList is a list of GameServer Allocation resources
    62  type GameServerAllocationList struct {
    63  	metav1.TypeMeta `json:",inline"`
    64  	metav1.ListMeta `json:"metadata,omitempty"`
    65  
    66  	Items []GameServerAllocation `json:"items"`
    67  }
    68  
    69  // GameServerAllocationSpec is the spec for a GameServerAllocation
    70  type GameServerAllocationSpec struct {
    71  	// MultiClusterPolicySelector if specified, multi-cluster policies are applied.
    72  	// Otherwise, allocation will happen locally.
    73  	MultiClusterSetting MultiClusterSetting `json:"multiClusterSetting,omitempty" hash:"ignore"`
    74  
    75  	// Deprecated: use field Selectors instead. If Selectors is set, this field is ignored.
    76  	// Required is the GameServer selector from which to choose GameServers from.
    77  	// Defaults to all GameServers.
    78  	Required GameServerSelector `json:"required,omitempty" hash:"ignore"`
    79  
    80  	// Deprecated: use field Selectors instead. If Selectors is set, this field is ignored.
    81  	// Preferred is an ordered list of preferred GameServer selectors
    82  	// that are optional to be fulfilled, but will be searched before the `required` selector.
    83  	// If the first selector is not matched, the selection attempts the second selector, and so on.
    84  	// If any of the preferred selectors are matched, the required selector is not considered.
    85  	// This is useful for things like smoke testing of new game servers.
    86  	Preferred []GameServerSelector `json:"preferred,omitempty" hash:"ignore"`
    87  
    88  	// [Stage: Beta]
    89  	// [FeatureFlag:CountsAndLists]
    90  	// `Priorities` configuration alters the order in which `GameServers` are searched for matches to the configured `selectors`.
    91  	//
    92  	// Priority of sorting is in descending importance. I.e. The position 0 `priority` entry is checked first.
    93  	//
    94  	// For `Packed` strategy sorting, this priority list will be the tie-breaker within the least utilised infrastructure, to ensure optimal
    95  	// infrastructure usage while also allowing some custom prioritisation of `GameServers`.
    96  	//
    97  	// For `Distributed` strategy sorting, the entire selection of `GameServers` will be sorted by this priority list to provide the
    98  	// order that `GameServers` will be allocated by.
    99  	// +optional
   100  	Priorities []agonesv1.Priority `json:"priorities,omitempty"`
   101  
   102  	// Ordered list of GameServer label selectors.
   103  	// If the first selector is not matched, the selection attempts the second selector, and so on.
   104  	// This is useful for things like smoke testing of new game servers.
   105  	// Note: This field can only be set if neither Required or Preferred is set.
   106  	Selectors []GameServerSelector `json:"selectors,omitempty" hash:"ignore"`
   107  
   108  	// Scheduling strategy. Defaults to "Packed".
   109  	Scheduling apis.SchedulingStrategy `json:"scheduling"`
   110  
   111  	// MetaPatch is optional custom metadata that is added to the game server at allocation
   112  	// You can use this to tell the server necessary session data
   113  	MetaPatch MetaPatch `json:"metadata,omitempty" hash:"ignore"`
   114  
   115  	// [Stage: Beta]
   116  	// [FeatureFlag:CountsAndLists]
   117  	// Counter actions to perform during allocation.
   118  	// +optional
   119  	Counters map[string]CounterAction `json:"counters,omitempty" hash:"ignore"`
   120  	// [Stage: Beta]
   121  	// [FeatureFlag:CountsAndLists]
   122  	// List actions to perform during allocation.
   123  	// +optional
   124  	Lists map[string]ListAction `json:"lists,omitempty" hash:"ignore"`
   125  }
   126  
   127  // GameServerSelector contains all the filter options for selecting
   128  // a GameServer for allocation.
   129  type GameServerSelector struct {
   130  	// See: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
   131  	metav1.LabelSelector `json:",inline"`
   132  	// GameServerState specifies which State is the filter to be used when attempting to retrieve a GameServer
   133  	// via Allocation. Defaults to "Ready". The only other option is "Allocated", which can be used in conjunction with
   134  	// label/annotation/player selectors to retrieve an already Allocated GameServer.
   135  	GameServerState *agonesv1.GameServerState `json:"gameServerState,omitempty"`
   136  	// [Stage:Alpha]
   137  	// [FeatureFlag:PlayerAllocationFilter]
   138  	// +optional
   139  	// Players provides a filter on minimum and maximum values for player capacity when retrieving a GameServer
   140  	// through Allocation. Defaults to no limits.
   141  	Players *PlayerSelector `json:"players,omitempty"`
   142  	// [Stage: Beta]
   143  	// [FeatureFlag:CountsAndLists]
   144  	// Counters provides filters on minimum and maximum values
   145  	// for a Counter's count and available capacity when retrieving a GameServer through Allocation.
   146  	// Defaults to no limits.
   147  	// +optional
   148  	Counters map[string]CounterSelector `json:"counters,omitempty"`
   149  	// [Stage: Beta]
   150  	// [FeatureFlag:CountsAndLists]
   151  	// Lists provides filters on minimum and maximum values
   152  	// for List capacity, and for the existence of a value in a List, when retrieving a GameServer
   153  	// through Allocation. Defaults to no limits.
   154  	// +optional
   155  	Lists map[string]ListSelector `json:"lists,omitempty"`
   156  }
   157  
   158  // PlayerSelector is the filter options for a GameServer based on player counts
   159  type PlayerSelector struct {
   160  	MinAvailable int64 `json:"minAvailable,omitempty"`
   161  	MaxAvailable int64 `json:"maxAvailable,omitempty"`
   162  }
   163  
   164  // CounterSelector is the filter options for a GameServer based on the count and/or available capacity.
   165  type CounterSelector struct {
   166  	// MinCount is the minimum current value. Defaults to 0.
   167  	// +optional
   168  	MinCount int64 `json:"minCount"`
   169  	// MaxCount is the maximum current value. Defaults to 0, which translates as max(in64).
   170  	// +optional
   171  	MaxCount int64 `json:"maxCount"`
   172  	// MinAvailable specifies the minimum capacity (current capacity - current count) available on a GameServer. Defaults to 0.
   173  	// +optional
   174  	MinAvailable int64 `json:"minAvailable"`
   175  	// MaxAvailable specifies the maximum capacity (current capacity - current count) available on a GameServer. Defaults to 0, which translates to max(int64).
   176  	// +optional
   177  	MaxAvailable int64 `json:"maxAvailable"`
   178  }
   179  
   180  // ListSelector is the filter options for a GameServer based on List available capacity and/or the
   181  // existence of a value in a List.
   182  type ListSelector struct {
   183  	// ContainsValue says to only match GameServers who has this value in the list. Defaults to "", which is all.
   184  	// +optional
   185  	ContainsValue string `json:"containsValue"`
   186  	// MinAvailable specifies the minimum capacity (current capacity - current count) available on a GameServer. Defaults to 0.
   187  	// +optional
   188  	MinAvailable int64 `json:"minAvailable"`
   189  	// MaxAvailable specifies the maximum capacity (current capacity - current count) available on a GameServer. Defaults to 0, which is translated as max(int64).
   190  	// +optional
   191  	MaxAvailable int64 `json:"maxAvailable"`
   192  }
   193  
   194  // CounterAction is an optional action that can be performed on a Counter at allocation.
   195  type CounterAction struct {
   196  	// Action must to either "Increment" or "Decrement" the Counter's Count. Must also define the Amount.
   197  	// +optional
   198  	Action *string `json:"action,omitempty"`
   199  	// Amount is the amount to increment or decrement the Count. Must be a positive integer.
   200  	// +optional
   201  	Amount *int64 `json:"amount,omitempty"`
   202  	// Capacity is the amount to update the maximum capacity of the Counter to this number. Min 0, Max int64.
   203  	// +optional
   204  	Capacity *int64 `json:"capacity,omitempty"`
   205  }
   206  
   207  // ListAction is an optional action that can be performed on a List at allocation.
   208  type ListAction struct {
   209  	// AddValues appends values to a List's Values array. Any duplicate values will be ignored.
   210  	// +optional
   211  	AddValues []string `json:"addValues,omitempty"`
   212  	// DeleteValues removes values from a List's Values array. Any nonexistant values will be ignored.
   213  	// +optional
   214  	DeleteValues []string `json:"deleteValues,omitempty"`
   215  	// Capacity updates the maximum capacity of the Counter to this number. Min 0, Max 1000.
   216  	// +optional
   217  	Capacity *int64 `json:"capacity,omitempty"`
   218  }
   219  
   220  // ApplyDefaults applies default values
   221  func (s *GameServerSelector) ApplyDefaults() {
   222  	if s.GameServerState == nil {
   223  		state := agonesv1.GameServerStateReady
   224  		s.GameServerState = &state
   225  	}
   226  
   227  	if runtime.FeatureEnabled(runtime.FeaturePlayerAllocationFilter) {
   228  		if s.Players == nil {
   229  			s.Players = &PlayerSelector{}
   230  		}
   231  	}
   232  
   233  	if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   234  		if s.Counters == nil {
   235  			s.Counters = make(map[string]CounterSelector)
   236  		}
   237  		if s.Lists == nil {
   238  			s.Lists = make(map[string]ListSelector)
   239  		}
   240  	}
   241  }
   242  
   243  // Matches checks to see if a GameServer matches a given GameServerSelector's criteria.
   244  // Will panic if the `GameServerSelector` has not passed `Validate()`.
   245  func (s *GameServerSelector) Matches(gs *agonesv1.GameServer) bool {
   246  
   247  	// Assume at this point, this has already been run through Validate(), and it can be converted.
   248  	// We end up running LabelSelectorAsSelector twice for each allocation, but if we store the results of this
   249  	// function within the GameServerSelector, we can't fuzz the GameServerAllocation as reflect.DeepEqual
   250  	// will fail due to the unexported field.
   251  	selector, err := metav1.LabelSelectorAsSelector(&s.LabelSelector)
   252  	if err != nil {
   253  		panic("GameServerSelector.Validate() has not been called before calling GameServerSelector.Matches(...)")
   254  	}
   255  
   256  	// first check labels
   257  	if !selector.Matches(labels.Set(gs.ObjectMeta.Labels)) {
   258  		return false
   259  	}
   260  
   261  	// then if state is being checked, check state
   262  	if s.GameServerState != nil && gs.Status.State != *s.GameServerState {
   263  		return false
   264  	}
   265  
   266  	// then if player count is being checked, check that
   267  	if runtime.FeatureEnabled(runtime.FeaturePlayerAllocationFilter) {
   268  		// 0 is unlimited number of players
   269  		if s.Players != nil && gs.Status.Players != nil && s.Players.MaxAvailable != 0 {
   270  			available := gs.Status.Players.Capacity - gs.Status.Players.Count
   271  			if !(available >= s.Players.MinAvailable && available <= s.Players.MaxAvailable) {
   272  				return false
   273  			}
   274  		}
   275  	}
   276  
   277  	if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   278  		// Only check for matches if there are CounterSelectors or ListSelectors
   279  		if (s.Counters != nil) && (len(s.Counters) != 0) {
   280  			if !(s.matchCounters(gs)) {
   281  				return false
   282  			}
   283  		}
   284  		if (s.Lists != nil) && (len(s.Lists) != 0) {
   285  			if !(s.matchLists(gs)) {
   286  				return false
   287  			}
   288  		}
   289  	}
   290  
   291  	return true
   292  }
   293  
   294  // matchCounters returns true if there is a match for the CounterSelector in the GameServerStatus
   295  func (s *GameServerSelector) matchCounters(gs *agonesv1.GameServer) bool {
   296  	if gs.Status.Counters == nil {
   297  		return false
   298  	}
   299  	for counter, counterSelector := range s.Counters {
   300  		// If the Counter Selector does not exist in GameServerStatus, return false.
   301  		counterStatus, ok := gs.Status.Counters[counter]
   302  		if !ok {
   303  			return false
   304  		}
   305  		// 0 means undefined (unlimited) for MaxAvailable.
   306  		available := counterStatus.Capacity - counterStatus.Count
   307  		if available < counterSelector.MinAvailable ||
   308  			(counterSelector.MaxAvailable != 0 && available > counterSelector.MaxAvailable) {
   309  			return false
   310  		}
   311  		// 0 means undefined (unlimited) for MaxCount.
   312  		if counterStatus.Count < counterSelector.MinCount ||
   313  			(counterSelector.MaxCount != 0 && counterStatus.Count > counterSelector.MaxCount) {
   314  			return false
   315  		}
   316  	}
   317  	return true
   318  }
   319  
   320  // CounterActions attempts to peform any actions from the CounterAction on the GameServer Counter.
   321  // Returns the errors of any actions that could not be performed.
   322  func (ca *CounterAction) CounterActions(counter string, gs *agonesv1.GameServer) error {
   323  	var errs error
   324  	if ca.Capacity != nil {
   325  		capErr := gs.UpdateCounterCapacity(counter, *ca.Capacity)
   326  		if capErr != nil {
   327  			errs = errors.Join(errs, capErr)
   328  		}
   329  	}
   330  	if ca.Action != nil && ca.Amount != nil {
   331  		cntErr := gs.UpdateCount(counter, *ca.Action, *ca.Amount)
   332  		if cntErr != nil {
   333  			errs = errors.Join(errs, cntErr)
   334  		}
   335  	}
   336  	return errs
   337  }
   338  
   339  // ListActions attempts to peform any actions from the ListAction on the GameServer List.
   340  // Returns a string list of any actions that could not be performed.
   341  func (la *ListAction) ListActions(list string, gs *agonesv1.GameServer) error {
   342  	var errs error
   343  	if la.Capacity != nil {
   344  		capErr := gs.UpdateListCapacity(list, *la.Capacity)
   345  		if capErr != nil {
   346  			errs = errors.Join(errs, capErr)
   347  		}
   348  	}
   349  	if len(la.AddValues) > 0 {
   350  		cntErr := gs.AppendListValues(list, la.AddValues)
   351  		if cntErr != nil {
   352  			errs = errors.Join(errs, cntErr)
   353  		}
   354  	}
   355  	if len(la.DeleteValues) > 0 {
   356  		cntErr := gs.DeleteListValues(list, la.DeleteValues)
   357  		if cntErr != nil {
   358  			errs = errors.Join(errs, cntErr)
   359  		}
   360  	}
   361  	return errs
   362  }
   363  
   364  // matchLists returns true if there is a match for the ListSelector in the GameServerStatus
   365  func (s *GameServerSelector) matchLists(gs *agonesv1.GameServer) bool {
   366  	if gs.Status.Lists == nil {
   367  		return false
   368  	}
   369  	for list, listSelector := range s.Lists {
   370  		// If the List Selector does not exist in GameServerStatus, return false.
   371  		listStatus, ok := gs.Status.Lists[list]
   372  		if !ok {
   373  			return false
   374  		}
   375  		// Match List based on capacity
   376  		available := listStatus.Capacity - int64(len(listStatus.Values))
   377  		// 0 means undefined (unlimited) for MaxAvailable.
   378  		if available < listSelector.MinAvailable ||
   379  			(listSelector.MaxAvailable != 0 && available > listSelector.MaxAvailable) {
   380  			return false
   381  		}
   382  		// Check if List contains ContainsValue (if a value has been specified)
   383  		if listSelector.ContainsValue != "" {
   384  			valueExists := false
   385  			for _, value := range listStatus.Values {
   386  				if value == listSelector.ContainsValue {
   387  					valueExists = true
   388  					break
   389  				}
   390  			}
   391  			if !valueExists {
   392  				return false
   393  			}
   394  		}
   395  	}
   396  	return true
   397  }
   398  
   399  // Validate validates that the selection fields have valid values
   400  func (s *GameServerSelector) Validate(fldPath *field.Path) field.ErrorList {
   401  	var allErrs field.ErrorList
   402  
   403  	_, err := metav1.LabelSelectorAsSelector(&s.LabelSelector)
   404  	if err != nil {
   405  		allErrs = append(allErrs, field.Invalid(fldPath.Child("labelSelector"), s.LabelSelector, fmt.Sprintf("Error converting label selector: %s", err)))
   406  	}
   407  
   408  	if s.GameServerState != nil && !(*s.GameServerState == agonesv1.GameServerStateAllocated || *s.GameServerState == agonesv1.GameServerStateReady) {
   409  		allErrs = append(allErrs, field.Invalid(fldPath.Child("gameServerState"), *s.GameServerState, "GameServerState must be either Allocated or Ready"))
   410  	}
   411  
   412  	if runtime.FeatureEnabled(runtime.FeaturePlayerAllocationFilter) && s.Players != nil {
   413  		if s.Players.MinAvailable < 0 {
   414  			allErrs = append(allErrs, field.Invalid(fldPath.Child("players").Child("minAvailable"), s.Players.MinAvailable, apivalidation.IsNegativeErrorMsg))
   415  		}
   416  
   417  		if s.Players.MaxAvailable < 0 {
   418  			allErrs = append(allErrs, field.Invalid(fldPath.Child("players").Child("maxAvailable"), s.Players.MaxAvailable, apivalidation.IsNegativeErrorMsg))
   419  		}
   420  
   421  		if s.Players.MinAvailable > s.Players.MaxAvailable {
   422  			allErrs = append(allErrs, field.Invalid(fldPath.Child("players").Child("minAvailable"), s.Players.MinAvailable, "minAvailable cannot be greater than maxAvailable"))
   423  		}
   424  	}
   425  
   426  	if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   427  		if s.Counters != nil {
   428  			allErrs = append(allErrs, validateCounters(s.Counters, fldPath.Child("counters"))...)
   429  		}
   430  		if s.Lists != nil {
   431  			allErrs = append(allErrs, validateLists(s.Lists, fldPath.Child("lists"))...)
   432  		}
   433  	} else {
   434  		if s.Counters != nil {
   435  			allErrs = append(allErrs, field.Forbidden(fldPath.Child("counters"), "Feature CountsAndLists must be enabled"))
   436  		}
   437  		if s.Lists != nil {
   438  			allErrs = append(allErrs, field.Forbidden(fldPath.Child("lists"), "Feature CountsAndLists must be enabled"))
   439  		}
   440  	}
   441  
   442  	return allErrs
   443  }
   444  
   445  // validateCounters validates that the selection field has valid values for CounterSelectors
   446  func validateCounters(counters map[string]CounterSelector, fldPath *field.Path) field.ErrorList {
   447  	var allErrs field.ErrorList
   448  	for key, counterSelector := range counters {
   449  		keyPath := fldPath.Key(key)
   450  		if counterSelector.MinCount < 0 {
   451  			allErrs = append(allErrs, field.Invalid(keyPath.Child("minCount"), counterSelector.MinCount, apivalidation.IsNegativeErrorMsg))
   452  		}
   453  		if counterSelector.MaxCount < 0 {
   454  			allErrs = append(allErrs, field.Invalid(keyPath.Child("maxCount"), counterSelector.MaxCount, apivalidation.IsNegativeErrorMsg))
   455  		}
   456  		if (counterSelector.MaxCount < counterSelector.MinCount) && (counterSelector.MaxCount != 0) {
   457  			allErrs = append(allErrs, field.Invalid(keyPath, counterSelector.MaxCount, fmt.Sprintf("maxCount must zero or greater than minCount %d", counterSelector.MinCount)))
   458  		}
   459  		if counterSelector.MinAvailable < 0 {
   460  			allErrs = append(allErrs, field.Invalid(keyPath.Child("minAvailable"), counterSelector.MinAvailable, apivalidation.IsNegativeErrorMsg))
   461  		}
   462  		if counterSelector.MaxAvailable < 0 {
   463  			allErrs = append(allErrs, field.Invalid(keyPath.Child("maxAvailable"), counterSelector.MaxAvailable, apivalidation.IsNegativeErrorMsg))
   464  		}
   465  		if (counterSelector.MaxAvailable < counterSelector.MinAvailable) && (counterSelector.MaxAvailable != 0) {
   466  			allErrs = append(allErrs, field.Invalid(keyPath, counterSelector.MaxAvailable, fmt.Sprintf("maxAvailable must zero or greater than minAvailable %d", counterSelector.MinAvailable)))
   467  		}
   468  	}
   469  
   470  	return allErrs
   471  }
   472  
   473  // validateLists validates that the selection field has valid values for ListSelectors
   474  func validateLists(lists map[string]ListSelector, fldPath *field.Path) field.ErrorList {
   475  	var allErrs field.ErrorList
   476  	for key, listSelector := range lists {
   477  		keyPath := fldPath.Key(key)
   478  		if listSelector.MinAvailable < 0 {
   479  			allErrs = append(allErrs, field.Invalid(keyPath.Child("minAvailable"), listSelector.MinAvailable, apivalidation.IsNegativeErrorMsg))
   480  		}
   481  		if listSelector.MaxAvailable < 0 {
   482  			allErrs = append(allErrs, field.Invalid(keyPath.Child("maxAvailable"), listSelector.MaxAvailable, apivalidation.IsNegativeErrorMsg))
   483  		}
   484  		if (listSelector.MaxAvailable < listSelector.MinAvailable) && (listSelector.MaxAvailable != 0) {
   485  			allErrs = append(allErrs, field.Invalid(keyPath, listSelector.MaxAvailable, fmt.Sprintf("maxAvailable must zero or greater than minAvailable %d", listSelector.MinAvailable)))
   486  		}
   487  	}
   488  
   489  	return allErrs
   490  }
   491  
   492  // validatePriorities validates that the Priorities fields has valid values for Priorities
   493  func validatePriorities(priorities []agonesv1.Priority, fldPath *field.Path) field.ErrorList {
   494  	var allErrs field.ErrorList
   495  	for index, priority := range priorities {
   496  		keyPath := fldPath.Index(index)
   497  		if priority.Type != agonesv1.GameServerPriorityCounter && priority.Type != agonesv1.GameServerPriorityList {
   498  			allErrs = append(allErrs, field.Invalid(keyPath, priority.Type, "type must be \"Counter\" or \"List\""))
   499  		}
   500  		if priority.Key == "" {
   501  			allErrs = append(allErrs, field.Invalid(keyPath, priority.Type, "key must not be nil"))
   502  		}
   503  		if priority.Order != agonesv1.GameServerPriorityAscending && priority.Order != agonesv1.GameServerPriorityDescending {
   504  			allErrs = append(allErrs, field.Invalid(keyPath, priority.Order, "order must be \"Ascending\" or \"Descending\""))
   505  		}
   506  	}
   507  
   508  	return allErrs
   509  }
   510  
   511  // validateCounterActions validates that the Counters field has valid values for CounterActions
   512  func validateCounterActions(counters map[string]CounterAction, fldPath *field.Path) field.ErrorList {
   513  	var allErrs field.ErrorList
   514  	for key, counterAction := range counters {
   515  		keyPath := fldPath.Key(key)
   516  		if counterAction.Amount != nil && *counterAction.Amount < 0 {
   517  			allErrs = append(allErrs, field.Invalid(keyPath.Child("amount"), counterAction.Amount, apivalidation.IsNegativeErrorMsg))
   518  		}
   519  		if counterAction.Capacity != nil && *counterAction.Capacity < 0 {
   520  			allErrs = append(allErrs, field.Invalid(keyPath.Child("capacity"), counterAction.Capacity, apivalidation.IsNegativeErrorMsg))
   521  		}
   522  		if counterAction.Amount != nil && counterAction.Action == nil {
   523  			allErrs = append(allErrs, field.Invalid(keyPath, counterAction.Action, "action must be \"Increment\" or \"Decrement\" if the amount is not nil"))
   524  		}
   525  		if counterAction.Amount == nil && counterAction.Action != nil {
   526  			allErrs = append(allErrs, field.Invalid(keyPath, counterAction.Amount, "amount must not be nil if action is not nil"))
   527  		}
   528  	}
   529  
   530  	return allErrs
   531  }
   532  
   533  // validateListActions validates that the Lists field has valid values for ListActions
   534  func validateListActions(lists map[string]ListAction, fldPath *field.Path) field.ErrorList {
   535  	var allErrs field.ErrorList
   536  	for key, listAction := range lists {
   537  		keyPath := fldPath.Key(key)
   538  		if listAction.Capacity != nil && *listAction.Capacity < 0 {
   539  			allErrs = append(allErrs, field.Invalid(keyPath.Child("capacity"), listAction.Capacity, apivalidation.IsNegativeErrorMsg))
   540  		}
   541  	}
   542  
   543  	return allErrs
   544  }
   545  
   546  // MultiClusterSetting specifies settings for multi-cluster allocation.
   547  type MultiClusterSetting struct {
   548  	Enabled        bool                 `json:"enabled,omitempty"`
   549  	PolicySelector metav1.LabelSelector `json:"policySelector,omitempty"`
   550  }
   551  
   552  // MetaPatch is the metadata used to patch the GameServer metadata on allocation
   553  type MetaPatch struct {
   554  	Labels      map[string]string `json:"labels,omitempty"`
   555  	Annotations map[string]string `json:"annotations,omitempty"`
   556  }
   557  
   558  // Validate returns if the labels and/or annotations that are to be applied to a `GameServer` post
   559  // allocation are valid.
   560  func (mp *MetaPatch) Validate(fldPath *field.Path) field.ErrorList {
   561  	allErrs := metav1validation.ValidateLabels(mp.Labels, fldPath.Child("labels"))
   562  	allErrs = append(allErrs, apivalidation.ValidateAnnotations(mp.Annotations, fldPath.Child("annotations"))...)
   563  	return allErrs
   564  }
   565  
   566  // GameServerAllocationStatus is the status for an GameServerAllocation resource
   567  type GameServerAllocationStatus struct {
   568  	// GameServerState is the current state of an GameServerAllocation, e.g. Allocated, or UnAllocated
   569  	State          GameServerAllocationState       `json:"state"`
   570  	GameServerName string                          `json:"gameServerName"`
   571  	Ports          []agonesv1.GameServerStatusPort `json:"ports,omitempty"`
   572  	Address        string                          `json:"address,omitempty"`
   573  	Addresses      []corev1.NodeAddress            `json:"addresses,omitempty"`
   574  	NodeName       string                          `json:"nodeName,omitempty"`
   575  	// If the allocation is from a remote cluster, Source is the endpoint of the remote agones-allocator.
   576  	// Otherwise, Source is "local"
   577  	Source   string                            `json:"source"`
   578  	Metadata *GameServerMetadata               `json:"metadata,omitempty"`
   579  	Counters map[string]agonesv1.CounterStatus `json:"counters,omitempty"`
   580  	Lists    map[string]agonesv1.ListStatus    `json:"lists,omitempty"`
   581  }
   582  
   583  // GameServerMetadata is the metadata from the allocated game server at allocation time
   584  type GameServerMetadata struct {
   585  	Labels      map[string]string `json:"labels,omitempty"`
   586  	Annotations map[string]string `json:"annotations,omitempty"`
   587  }
   588  
   589  // ApplyDefaults applies the default values to this GameServerAllocation
   590  func (gsa *GameServerAllocation) ApplyDefaults() {
   591  	if gsa.Spec.Scheduling == "" {
   592  		gsa.Spec.Scheduling = apis.Packed
   593  	}
   594  
   595  	for i := range gsa.Spec.Priorities {
   596  		if len(gsa.Spec.Priorities[i].Order) == 0 {
   597  			gsa.Spec.Priorities[i].Order = agonesv1.GameServerPriorityAscending
   598  		}
   599  	}
   600  
   601  	if len(gsa.Spec.Selectors) == 0 {
   602  		gsa.Spec.Required.ApplyDefaults()
   603  
   604  		for i := range gsa.Spec.Preferred {
   605  			gsa.Spec.Preferred[i].ApplyDefaults()
   606  		}
   607  	} else {
   608  		for i := range gsa.Spec.Selectors {
   609  			gsa.Spec.Selectors[i].ApplyDefaults()
   610  		}
   611  	}
   612  }
   613  
   614  // Validate validation for the GameServerAllocation
   615  // Validate should be called before attempting to Match any of the GameServer selectors.
   616  func (gsa *GameServerAllocation) Validate() field.ErrorList {
   617  	var allErrs field.ErrorList
   618  	specPath := field.NewPath("spec")
   619  	if gsa.Spec.Scheduling != apis.Packed && gsa.Spec.Scheduling != apis.Distributed {
   620  		allErrs = append(allErrs, field.NotSupported(specPath.Child("scheduling"), string(gsa.Spec.Scheduling), []string{string(apis.Packed), string(apis.Distributed)}))
   621  	}
   622  
   623  	allErrs = append(allErrs, gsa.Spec.Required.Validate(specPath.Child("required"))...)
   624  	for i := range gsa.Spec.Preferred {
   625  		allErrs = append(allErrs, gsa.Spec.Preferred[i].Validate(specPath.Child("preferred").Index(i))...)
   626  	}
   627  	for i := range gsa.Spec.Selectors {
   628  		allErrs = append(allErrs, gsa.Spec.Selectors[i].Validate(specPath.Child("selectors").Index(i))...)
   629  	}
   630  
   631  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   632  		if gsa.Spec.Priorities != nil {
   633  			allErrs = append(allErrs, field.Forbidden(specPath.Child("priorities"), "Feature CountsAndLists must be enabled if Priorities is specified"))
   634  		}
   635  		if gsa.Spec.Counters != nil {
   636  			allErrs = append(allErrs, field.Forbidden(specPath.Child("counters"), "Feature CountsAndLists must be enabled if Counters is specified"))
   637  		}
   638  		if gsa.Spec.Lists != nil {
   639  			allErrs = append(allErrs, field.Forbidden(specPath.Child("lists"), "Feature CountsAndLists must be enabled if Lists is specified"))
   640  		}
   641  	}
   642  
   643  	if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   644  		if gsa.Spec.Priorities != nil {
   645  			allErrs = append(allErrs, validatePriorities(gsa.Spec.Priorities, specPath.Child("priorities"))...)
   646  		}
   647  		if gsa.Spec.Counters != nil {
   648  			allErrs = append(allErrs, validateCounterActions(gsa.Spec.Counters, specPath.Child("counters"))...)
   649  		}
   650  		if gsa.Spec.Lists != nil {
   651  			allErrs = append(allErrs, validateListActions(gsa.Spec.Lists, specPath.Child("lists"))...)
   652  		}
   653  	}
   654  
   655  	allErrs = append(allErrs, gsa.Spec.MetaPatch.Validate(specPath.Child("metadata"))...)
   656  	return allErrs
   657  }
   658  
   659  // Converter converts game server allocation required and preferred fields to selectors field.
   660  func (gsa *GameServerAllocation) Converter() {
   661  	if len(gsa.Spec.Selectors) == 0 {
   662  		var selectors []GameServerSelector
   663  		selectors = append(selectors, gsa.Spec.Preferred...)
   664  		selectors = append(selectors, gsa.Spec.Required)
   665  		gsa.Spec.Selectors = selectors
   666  	}
   667  }
   668  
   669  // SortKey generates and returns the hash of the GameServerAllocationSpec []Priority and Scheduling.
   670  // Note: The hash:"ignore" in GameServerAllocationSpec means that these fields will not be considered
   671  // in hashing. The hash is used for determining when GameServerAllocations have equal or different
   672  // []Priority and Scheduling.
   673  func (gsa *GameServerAllocation) SortKey() (uint64, error) {
   674  	hash, err := hashstructure.Hash(gsa.Spec, hashstructure.FormatV2, nil)
   675  	if err != nil {
   676  		return 0, err
   677  	}
   678  	return hash, nil
   679  }