agones.dev/agones@v1.53.0/pkg/apis/agones/v1/fleet.go (about) 1 // Copyright 2018 Google LLC All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package v1 16 17 import ( 18 appsv1 "k8s.io/api/apps/v1" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 "k8s.io/apimachinery/pkg/util/intstr" 21 "k8s.io/apimachinery/pkg/util/validation/field" 22 23 "agones.dev/agones/pkg" 24 "agones.dev/agones/pkg/apis" 25 "agones.dev/agones/pkg/apis/agones" 26 "agones.dev/agones/pkg/util/runtime" 27 ) 28 29 const ( 30 // FleetNameLabel is the label that the name of the Fleet 31 // is set to on GameServerSet and GameServer the Fleet controls 32 FleetNameLabel = agones.GroupName + "/fleet" 33 ) 34 35 // +genclient 36 // +genclient:method=GetScale,verb=get,subresource=scale,result=k8s.io/api/autoscaling/v1.Scale 37 // +genclient:method=UpdateScale,verb=update,subresource=scale,input=k8s.io/api/autoscaling/v1.Scale,result=k8s.io/api/autoscaling/v1.Scale 38 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 39 40 // Fleet is the data structure for a Fleet resource 41 type Fleet struct { 42 metav1.TypeMeta `json:",inline"` 43 metav1.ObjectMeta `json:"metadata,omitempty"` 44 45 Spec FleetSpec `json:"spec"` 46 Status FleetStatus `json:"status"` 47 } 48 49 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 50 51 // FleetList is a list of Fleet resources 52 type FleetList struct { 53 metav1.TypeMeta `json:",inline"` 54 metav1.ListMeta `json:"metadata,omitempty"` 55 56 Items []Fleet `json:"items"` 57 } 58 59 // FleetSpec is the spec for a Fleet 60 type FleetSpec struct { 61 // Replicas are the number of GameServers that should be in this set. Defaults to 0. 62 Replicas int32 `json:"replicas"` 63 // Labels and/or Annotations to apply to overflowing GameServers when the number of Allocated GameServers is more 64 // than the desired replicas on the underlying `GameServerSet` 65 // +optional 66 AllocationOverflow *AllocationOverflow `json:"allocationOverflow,omitempty"` 67 // Deployment strategy 68 Strategy appsv1.DeploymentStrategy `json:"strategy"` 69 // Scheduling strategy. Defaults to "Packed". 70 Scheduling apis.SchedulingStrategy `json:"scheduling"` 71 // [Stage: Beta] 72 // [FeatureFlag:CountsAndLists] 73 // `Priorities` configuration alters scale down logic in Fleets based on the configured available capacity order under that key. 74 // 75 // Priority of sorting is in descending importance. I.e. The position 0 `priority` entry is checked first. 76 // 77 // For `Packed` strategy scale down, this priority list will be the tie-breaker within the node, to ensure optimal 78 // infrastructure usage while also allowing some custom prioritisation of `GameServers`. 79 // 80 // For `Distributed` strategy scale down, the entire `Fleet` will be sorted by this priority list to provide the 81 // order of `GameServers` to delete on scale down. 82 // +optional 83 Priorities []Priority `json:"priorities,omitempty"` 84 // Template the GameServer template to apply for this Fleet 85 Template GameServerTemplateSpec `json:"template"` 86 } 87 88 // FleetStatus is the status of a Fleet 89 type FleetStatus struct { 90 // Replicas the total number of current GameServer replicas 91 Replicas int32 `json:"replicas"` 92 // ReadyReplicas are the number of Ready GameServer replicas 93 ReadyReplicas int32 `json:"readyReplicas"` 94 // ReservedReplicas are the total number of Reserved GameServer replicas in this fleet. 95 // Reserved instances won't be deleted on scale down, but won't cause an autoscaler to scale up. 96 ReservedReplicas int32 `json:"reservedReplicas"` 97 // AllocatedReplicas are the number of Allocated GameServer replicas 98 AllocatedReplicas int32 `json:"allocatedReplicas"` 99 // [Stage:Alpha] 100 // [FeatureFlag:PlayerTracking] 101 // Players are the current total player capacity and count for this Fleet 102 // +optional 103 Players *AggregatedPlayerStatus `json:"players,omitempty"` 104 // (Beta, CountsAndLists feature flag) Counters provides aggregated Counter capacity and Counter 105 // count for this Fleet. 106 // +optional 107 Counters map[string]AggregatedCounterStatus `json:"counters,omitempty"` 108 // (Beta, CountsAndLists feature flag) Lists provides aggregated List capacityv and List values 109 // for this Fleet. 110 // +optional 111 Lists map[string]AggregatedListStatus `json:"lists,omitempty"` 112 } 113 114 // GameServerSet returns a single GameServerSet for this Fleet definition 115 func (f *Fleet) GameServerSet() *GameServerSet { 116 gsSet := &GameServerSet{ 117 ObjectMeta: *f.Spec.Template.ObjectMeta.DeepCopy(), 118 Spec: GameServerSetSpec{ 119 Template: f.Spec.Template, 120 Scheduling: f.Spec.Scheduling, 121 }, 122 } 123 124 // Switch to GenerateName, so that we always get a Unique name for the GameServerSet, and there 125 // can be no collisions 126 gsSet.ObjectMeta.GenerateName = f.ObjectMeta.Name + "-" 127 gsSet.ObjectMeta.Name = "" 128 gsSet.ObjectMeta.Namespace = f.ObjectMeta.Namespace 129 gsSet.ObjectMeta.ResourceVersion = "" 130 gsSet.ObjectMeta.UID = "" 131 132 ref := metav1.NewControllerRef(f, SchemeGroupVersion.WithKind("Fleet")) 133 gsSet.ObjectMeta.OwnerReferences = append(gsSet.ObjectMeta.OwnerReferences, *ref) 134 135 if gsSet.ObjectMeta.Labels == nil { 136 gsSet.ObjectMeta.Labels = make(map[string]string, 1) 137 } 138 139 gsSet.ObjectMeta.Labels[FleetNameLabel] = f.ObjectMeta.Name 140 141 if f.Spec.AllocationOverflow != nil { 142 gsSet.Spec.AllocationOverflow = f.Spec.AllocationOverflow.DeepCopy() 143 } 144 145 if runtime.FeatureEnabled(runtime.FeatureCountsAndLists) && f.Spec.Priorities != nil { 146 // DeepCopy done manually here as f.Spec.Priorities does not have a DeepCopy() method. 147 gsSet.Spec.Priorities = make([]Priority, len(f.Spec.Priorities)) 148 copy(gsSet.Spec.Priorities, f.Spec.Priorities) 149 } 150 151 return gsSet 152 } 153 154 // ApplyDefaults applies default values to the Fleet 155 func (f *Fleet) ApplyDefaults() { 156 if f.Spec.Strategy.Type == "" { 157 f.Spec.Strategy.Type = appsv1.RollingUpdateDeploymentStrategyType 158 } 159 160 if f.Spec.Scheduling == "" { 161 f.Spec.Scheduling = apis.Packed 162 } 163 164 if f.Spec.Strategy.Type == appsv1.RollingUpdateDeploymentStrategyType { 165 if f.Spec.Strategy.RollingUpdate == nil { 166 f.Spec.Strategy.RollingUpdate = &appsv1.RollingUpdateDeployment{} 167 } 168 169 def := intstr.FromString("25%") 170 if f.Spec.Strategy.RollingUpdate.MaxSurge == nil { 171 f.Spec.Strategy.RollingUpdate.MaxSurge = &def 172 } 173 if f.Spec.Strategy.RollingUpdate.MaxUnavailable == nil { 174 f.Spec.Strategy.RollingUpdate.MaxUnavailable = &def 175 } 176 } 177 // Add Agones version into Fleet Annotations 178 if f.ObjectMeta.Annotations == nil { 179 f.ObjectMeta.Annotations = make(map[string]string, 1) 180 } 181 f.ObjectMeta.Annotations[VersionAnnotation] = pkg.Version 182 183 } 184 185 // GetGameServerSpec get underlying Gameserver specification 186 func (f *Fleet) GetGameServerSpec() *GameServerSpec { 187 return &f.Spec.Template.Spec 188 } 189 190 func (f *Fleet) validateRollingUpdate(value *intstr.IntOrString, fldPath *field.Path) field.ErrorList { 191 allErrs := field.ErrorList{} 192 r, err := intstr.GetValueFromIntOrPercent(value, 100, true) 193 if value.Type == intstr.String { 194 if err != nil || r < 1 || r > 99 { 195 allErrs = append(allErrs, field.Invalid(fldPath, value, "must be between 1% and 99%")) 196 } 197 } else if r < 1 { 198 allErrs = append(allErrs, field.Invalid(fldPath, value, "must be at least 1")) 199 } 200 return allErrs 201 } 202 203 // Validate validates the Fleet configuration. 204 // If a Fleet is invalid there will be > 0 values in 205 // the returned array 206 func (f *Fleet) Validate(apiHooks APIHooks) field.ErrorList { 207 allErrs := validateName(f, field.NewPath("metadata")) 208 209 strategyPath := field.NewPath("spec", "strategy") 210 if f.Spec.Strategy.Type == appsv1.RollingUpdateDeploymentStrategyType { 211 allErrs = append(allErrs, f.validateRollingUpdate(f.Spec.Strategy.RollingUpdate.MaxUnavailable, strategyPath.Child("rollingUpdate", "maxUnavailable"))...) 212 allErrs = append(allErrs, f.validateRollingUpdate(f.Spec.Strategy.RollingUpdate.MaxSurge, strategyPath.Child("rollingUpdate", "maxSurge"))...) 213 } else if f.Spec.Strategy.Type != appsv1.RecreateDeploymentStrategyType { 214 allErrs = append(allErrs, field.NotSupported(strategyPath.Child("type"), f.Spec.Strategy.Type, []string{"RollingUpdate", "Recreate"})) 215 } 216 217 // check Gameserver specification in a Fleet 218 allErrs = append(allErrs, validateGSSpec(apiHooks, f, field.NewPath("spec", "template", "spec"))...) 219 allErrs = append(allErrs, apiHooks.ValidateScheduling(f.Spec.Scheduling, field.NewPath("spec", "scheduling"))...) 220 allErrs = append(allErrs, validateObjectMeta(&f.Spec.Template.ObjectMeta, field.NewPath("spec", "template", "metadata"))...) 221 222 if f.Spec.AllocationOverflow != nil { 223 allErrs = append(allErrs, f.Spec.AllocationOverflow.Validate(field.NewPath("spec", "allocationOverflow"))...) 224 } 225 226 if f.Spec.Priorities != nil && !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { 227 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "priorities"), "FeatureCountsAndLists is not enabled")) 228 } 229 230 return allErrs 231 } 232 233 // UpperBoundReplicas returns whichever is smaller, 234 // the value i, or the f.Spec.Replicas. 235 func (f *Fleet) UpperBoundReplicas(i int32) int32 { 236 if i > f.Spec.Replicas { 237 return f.Spec.Replicas 238 } 239 return i 240 } 241 242 // LowerBoundReplicas returns 0 (the minimum value for 243 // replicas) if i is < 0 244 func (f *Fleet) LowerBoundReplicas(i int32) int32 { 245 if i < 0 { 246 return 0 247 } 248 return i 249 } 250 251 // SumGameServerSets calculates a total from the value returned from the passed in function. 252 // Useful for calculating totals based on status value(s), such as gsSet.Status.Replicas 253 // This should eventually replace the variety of `Sum*` and `GetReadyReplicaCountForGameServerSets` functions as this is 254 // a higher and more flexible abstraction. 255 func SumGameServerSets(list []*GameServerSet, f func(gsSet *GameServerSet) int32) int32 { 256 var total int32 257 for _, gsSet := range list { 258 if gsSet != nil { 259 total += f(gsSet) 260 } 261 } 262 263 return total 264 } 265 266 // SumStatusAllocatedReplicas returns the total number of 267 // Status.AllocatedReplicas in the list of GameServerSets 268 func SumStatusAllocatedReplicas(list []*GameServerSet) int32 { 269 total := int32(0) 270 for _, gsSet := range list { 271 total += gsSet.Status.AllocatedReplicas 272 } 273 274 return total 275 } 276 277 // SumStatusReplicas returns the total number of 278 // Status.Replicas in the list of GameServerSets 279 func SumStatusReplicas(list []*GameServerSet) int32 { 280 total := int32(0) 281 for _, gsSet := range list { 282 total += gsSet.Status.Replicas 283 } 284 285 return total 286 } 287 288 // SumSpecReplicas returns the total number of 289 // Spec.Replicas in the list of GameServerSets 290 func SumSpecReplicas(list []*GameServerSet) int32 { 291 total := int32(0) 292 for _, gsSet := range list { 293 if gsSet != nil { 294 total += gsSet.Spec.Replicas 295 } 296 } 297 298 return total 299 } 300 301 // GetReadyReplicaCountForGameServerSets returns the total number of 302 // Status.ReadyReplicas in the list of GameServerSets 303 func GetReadyReplicaCountForGameServerSets(gss []*GameServerSet) int32 { 304 totalReadyReplicas := int32(0) 305 for _, gss := range gss { 306 if gss != nil { 307 totalReadyReplicas += gss.Status.ReadyReplicas 308 } 309 } 310 return totalReadyReplicas 311 }