agones.dev/agones@v1.54.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 }