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  }