agones.dev/agones@v1.54.0/pkg/apis/autoscaling/v1/fleetautoscaler.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  	"crypto/x509"
    19  	"errors"
    20  	"fmt"
    21  	"net/url"
    22  	"strings"
    23  	"time"
    24  
    25  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    26  	"agones.dev/agones/pkg/util/runtime"
    27  	"github.com/robfig/cron/v3"
    28  	admregv1 "k8s.io/api/admissionregistration/v1"
    29  	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/types"
    32  	"k8s.io/apimachinery/pkg/util/intstr"
    33  	"k8s.io/apimachinery/pkg/util/validation/field"
    34  )
    35  
    36  // +genclient
    37  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    38  
    39  // FleetAutoscaler is the data structure for a FleetAutoscaler resource
    40  type FleetAutoscaler struct {
    41  	metav1.TypeMeta   `json:",inline"`
    42  	metav1.ObjectMeta `json:"metadata,omitempty"`
    43  
    44  	Spec   FleetAutoscalerSpec   `json:"spec"`
    45  	Status FleetAutoscalerStatus `json:"status"`
    46  }
    47  
    48  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    49  
    50  // FleetAutoscalerList is a list of Fleet Scaler resources
    51  type FleetAutoscalerList struct {
    52  	metav1.TypeMeta `json:",inline"`
    53  	metav1.ListMeta `json:"metadata,omitempty"`
    54  
    55  	Items []FleetAutoscaler `json:"items"`
    56  }
    57  
    58  // FleetAutoscalerSpec is the spec for a Fleet Scaler
    59  type FleetAutoscalerSpec struct {
    60  	FleetName string `json:"fleetName"`
    61  
    62  	// Autoscaling policy
    63  	Policy FleetAutoscalerPolicy `json:"policy"`
    64  	// Sync defines when FleetAutoscalers runs autoscaling
    65  	// +optional
    66  	Sync *FleetAutoscalerSync `json:"sync,omitempty"`
    67  }
    68  
    69  // FleetAutoscalerPolicy describes how to scale a fleet
    70  type FleetAutoscalerPolicy struct {
    71  	// Type of autoscaling policy.
    72  	Type FleetAutoscalerPolicyType `json:"type"`
    73  
    74  	// Buffer policy config params. Present only if FleetAutoscalerPolicyType = Buffer.
    75  	// +optional
    76  	Buffer *BufferPolicy `json:"buffer,omitempty"`
    77  	// Webhook policy config params. Present only if FleetAutoscalerPolicyType = Webhook.
    78  	// +optional
    79  	Webhook *URLConfiguration `json:"webhook,omitempty"`
    80  	// [Stage:Beta]
    81  	// [FeatureFlag:CountsAndLists]
    82  	// Counter policy config params. Present only if FleetAutoscalerPolicyType = Counter.
    83  	// +optional
    84  	Counter *CounterPolicy `json:"counter,omitempty"`
    85  	// [Stage:Beta]
    86  	// [FeatureFlag:CountsAndLists]
    87  	// List policy config params. Present only if FleetAutoscalerPolicyType = List.
    88  	// +optional
    89  	List *ListPolicy `json:"list,omitempty"`
    90  	// [Stage:Beta]
    91  	// [FeatureFlag:ScheduledAutoscaler]
    92  	// Schedule policy config params. Present only if FleetAutoscalerPolicyType = Schedule.
    93  	// +optional
    94  	Schedule *SchedulePolicy `json:"schedule,omitempty"`
    95  	// [Stage:Beta]
    96  	// [FeatureFlag:ScheduledAutoscaler]
    97  	// Chain policy config params. Present only if FleetAutoscalerPolicyType = Chain.
    98  	// +optional
    99  	Chain ChainPolicy `json:"chain,omitempty"`
   100  	// Wasm policy config params. Present only if FleetAutoscalerPolicyType = Wasm.
   101  	// +optional
   102  	Wasm *WasmPolicy `json:"wasm,omitempty"`
   103  }
   104  
   105  // FleetAutoscalerPolicyType is the policy for autoscaling
   106  // for a given Fleet
   107  type FleetAutoscalerPolicyType string
   108  
   109  // FleetAutoscalerSync describes when to sync a fleet
   110  type FleetAutoscalerSync struct {
   111  	// Type of autoscaling sync.
   112  	Type FleetAutoscalerSyncType `json:"type"`
   113  
   114  	// FixedInterval config params. Present only if FleetAutoscalerSyncType = FixedInterval.
   115  	// +optional
   116  	FixedInterval FixedIntervalSync `json:"fixedInterval"`
   117  }
   118  
   119  // FleetAutoscalerSyncType is the sync strategy for a given Fleet
   120  type FleetAutoscalerSyncType string
   121  
   122  const (
   123  	// BufferPolicyType FleetAutoscalerPolicyType is a simple buffering strategy for Ready
   124  	// GameServers
   125  	BufferPolicyType FleetAutoscalerPolicyType = "Buffer"
   126  	// WebhookPolicyType is a simple webhook strategy used for horizontal fleet scaling
   127  	// GameServers
   128  	WebhookPolicyType FleetAutoscalerPolicyType = "Webhook"
   129  	// [Stage:Beta]
   130  	// [FeatureFlag:CountsAndLists]
   131  	// CounterPolicyType is for Counter based fleet autoscaling
   132  	// nolint:revive // Linter contains comment doesn't start with CounterPolicyType
   133  	CounterPolicyType FleetAutoscalerPolicyType = "Counter"
   134  	// [Stage:Beta]
   135  	// [FeatureFlag:CountsAndLists]
   136  	// ListPolicyType is for List based fleet autoscaling
   137  	// nolint:revive // Linter contains comment doesn't start with ListPolicyType
   138  	ListPolicyType FleetAutoscalerPolicyType = "List"
   139  	// [Stage:Beta]
   140  	// [FeatureFlag:ScheduledAutoscaler]
   141  	// SchedulePolicyType is for Schedule based fleet autoscaling
   142  	// nolint:revive // Linter contains comment doesn't start with SchedulePolicyType
   143  	SchedulePolicyType FleetAutoscalerPolicyType = "Schedule"
   144  	// [Stage:Beta]
   145  	// [FeatureFlag:ScheduledAutoscaler]
   146  	// ChainPolicyType is for Chain based fleet autoscaling
   147  	// nolint:revive // Linter contains comment doesn't start with ChainPolicyType
   148  	ChainPolicyType FleetAutoscalerPolicyType = "Chain"
   149  	// WasmPolicyType is for WebAssembly based fleet autoscaling
   150  	// [Stage:Alpha]
   151  	// [FeatureFlag:WasmAutoscaler]
   152  	WasmPolicyType FleetAutoscalerPolicyType = "Wasm"
   153  	// FixedIntervalSyncType is a simple fixed interval based strategy for trigger autoscaling
   154  	FixedIntervalSyncType FleetAutoscalerSyncType = "FixedInterval"
   155  
   156  	defaultIntervalSyncSeconds int32 = 30
   157  )
   158  
   159  // BufferPolicy controls the desired behavior of the buffer policy.
   160  type BufferPolicy struct {
   161  	// MaxReplicas is the maximum amount of replicas that the fleet may have.
   162  	// It must be bigger than both MinReplicas and BufferSize
   163  	MaxReplicas int32 `json:"maxReplicas"`
   164  
   165  	// MinReplicas is the minimum amount of replicas that the fleet must have
   166  	// If zero, it is ignored.
   167  	// If non zero, it must be smaller than MaxReplicas and bigger than BufferSize
   168  	MinReplicas int32 `json:"minReplicas"`
   169  
   170  	// BufferSize defines how many replicas the autoscaler tries to have ready all the time
   171  	// Value can be an absolute number (ex: 5) or a percentage of desired gs instances (ex: 15%)
   172  	// Absolute number is calculated from percentage by rounding up.
   173  	// Example: when this is set to 20%, the autoscaler will make sure that 20%
   174  	//   of the fleet's game server replicas are ready. When this is set to 20,
   175  	//   the autoscaler will make sure that there are 20 available game servers
   176  	// Must be bigger than 0
   177  	// Note: by "ready" we understand in this case "non-allocated"; this is done to ensure robustness
   178  	//       and computation stability in different edge case (fleet just created, not enough
   179  	//       capacity in the cluster etc)
   180  	BufferSize intstr.IntOrString `json:"bufferSize"`
   181  }
   182  
   183  // URLConfiguration is a generic configuration for any integration with an external URL or
   184  // cluster provided service. For example, it can be used to configure a webhook or Wasm module
   185  type URLConfiguration admregv1.WebhookClientConfig
   186  
   187  // CounterPolicy controls the desired behavior of the Counter autoscaler policy.
   188  type CounterPolicy struct {
   189  	// Key is the name of the Counter. Required field.
   190  	Key string `json:"key"`
   191  
   192  	// MaxCapacity is the maximum aggregate Counter total capacity across the fleet.
   193  	// MaxCapacity must be bigger than both MinCapacity and BufferSize. Required field.
   194  	MaxCapacity int64 `json:"maxCapacity"`
   195  
   196  	// MinCapacity is the minimum aggregate Counter total capacity across the fleet.
   197  	// If zero, MinCapacity is ignored.
   198  	// If non zero, MinCapacity must be smaller than MaxCapacity and bigger than BufferSize.
   199  	MinCapacity int64 `json:"minCapacity"`
   200  
   201  	// BufferSize is the size of a buffer of counted items that are available in the Fleet (available
   202  	// capacity). Value can be an absolute number (ex: 5) or a percentage of desired gs instances
   203  	// (ex: 5%). An absolute number is calculated from percentage by rounding up.
   204  	// Must be bigger than 0. Required field.
   205  	BufferSize intstr.IntOrString `json:"bufferSize"`
   206  }
   207  
   208  // ListPolicy controls the desired behavior of the List autoscaler policy.
   209  type ListPolicy struct {
   210  	// Key is the name of the List. Required field.
   211  	Key string `json:"key"`
   212  
   213  	// MaxCapacity is the maximum aggregate List total capacity across the fleet.
   214  	// MaxCapacity must be bigger than both MinCapacity and BufferSize. Required field.
   215  	MaxCapacity int64 `json:"maxCapacity"`
   216  
   217  	// MinCapacity is the minimum aggregate List total capacity across the fleet.
   218  	// If zero, it is ignored.
   219  	// If non zero, it must be smaller than MaxCapacity and bigger than BufferSize.
   220  	MinCapacity int64 `json:"minCapacity"`
   221  
   222  	// BufferSize is the size of a buffer based on the List capacity that is available over the
   223  	// current aggregate List length in the Fleet (available capacity). It can be specified either
   224  	// as an absolute value (i.e. 5) or percentage format (i.e. 5%).
   225  	// Must be bigger than 0. Required field.
   226  	BufferSize intstr.IntOrString `json:"bufferSize"`
   227  }
   228  
   229  // Between defines the time period that the policy is eligible to be applied.
   230  type Between struct {
   231  	// Start is the datetime that the policy is eligible to be applied.
   232  	// This field must conform to RFC3339 format. If this field not set or is in the past, the policy is eligible to be applied
   233  	// as soon as the fleet autoscaler is running.
   234  	Start metav1.Time `json:"start"`
   235  
   236  	// End is the datetime that the policy is no longer eligible to be applied.
   237  	// This field must conform to RFC3339 format. If not set, the policy is always eligible to be applied, after the start time above.
   238  	End metav1.Time `json:"end"`
   239  }
   240  
   241  // ActivePeriod defines the time period that the policy is applied.
   242  type ActivePeriod struct {
   243  	// Timezone to be used for the startCron field. If not set, startCron is defaulted to the UTC timezone.
   244  	Timezone string `json:"timezone"`
   245  
   246  	// StartCron defines when the policy should be applied.
   247  	// If not set, the policy is always to be applied within the start and end time.
   248  	// This field must conform to UNIX cron syntax.
   249  	StartCron string `json:"startCron"`
   250  
   251  	// Duration is the length of time that the policy is applied.
   252  	// If not set, the duration is indefinite.
   253  	// A duration string is a possibly signed sequence of decimal numbers,
   254  	// (e.g. "300ms", "-1.5h" or "2h45m").
   255  	// The representation limits the largest representable duration to approximately 290 years.
   256  	// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
   257  	Duration string `json:"duration"`
   258  }
   259  
   260  // SchedulePolicy controls the desired behavior of the Schedule autoscaler policy.
   261  type SchedulePolicy struct {
   262  	// Between defines the time period that the policy is eligible to be applied.
   263  	Between Between `json:"between"`
   264  
   265  	// ActivePeriod defines the time period that the policy is applied.
   266  	ActivePeriod ActivePeriod `json:"activePeriod"`
   267  
   268  	// Policy is the name of the policy to be applied. Required field.
   269  	Policy FleetAutoscalerPolicy `json:"policy"`
   270  }
   271  
   272  // ChainEntry defines a single entry in the ChainPolicy.
   273  type ChainEntry struct {
   274  	// ID is the unique identifier for a ChainEntry. If not set the identifier will be set to the index of chain entry.
   275  	ID string `json:"id"`
   276  
   277  	// Policy is the name of the policy to be applied. Required field.
   278  	FleetAutoscalerPolicy `json:",inline"`
   279  }
   280  
   281  // ChainPolicy controls the desired behavior of the Chain autoscaler policy.
   282  type ChainPolicy []ChainEntry
   283  
   284  // WasmFrom defines the source of the Wasm module
   285  type WasmFrom struct {
   286  	// URL is the URL of the Wasm module to use for autoscaling.
   287  	URL *URLConfiguration `json:"url"`
   288  }
   289  
   290  // WasmPolicy controls the desired behavior of the Wasm policy.
   291  // It contains the configuration for the WebAssembly module used for autoscaling.
   292  type WasmPolicy struct {
   293  	// Function is the exported function to call in the wasm module, defaults to 'scale'
   294  	// +optional
   295  	Function string `json:"function,omitempty"`
   296  	// Config values to pass to the wasm program on startup
   297  	// +optional
   298  	Config map[string]string `json:"config,omitempty"`
   299  	// WasmFrom defines the source of the Wasm module
   300  	From WasmFrom `json:"from"`
   301  	// Hash of the Wasm module, used to verify the integrity of the module
   302  	// +optional
   303  	Hash string `json:"hash,omitempty"`
   304  }
   305  
   306  // FixedIntervalSync controls the desired behavior of the fixed interval based sync.
   307  type FixedIntervalSync struct {
   308  	// Seconds defines how often we run fleet autoscaling in seconds
   309  	Seconds int32 `json:"seconds"`
   310  }
   311  
   312  // FleetAutoscalerStatus defines the current status of a FleetAutoscaler
   313  type FleetAutoscalerStatus struct {
   314  	// CurrentReplicas is the current number of gameserver replicas
   315  	// of the fleet managed by this autoscaler, as last seen by the autoscaler
   316  	CurrentReplicas int32 `json:"currentReplicas"`
   317  
   318  	// DesiredReplicas is the desired number of gameserver replicas
   319  	// of the fleet managed by this autoscaler, as last calculated by the autoscaler
   320  	DesiredReplicas int32 `json:"desiredReplicas"`
   321  
   322  	// lastScaleTime is the last time the FleetAutoscaler scaled the attached fleet,
   323  	// +optional
   324  	LastScaleTime *metav1.Time `json:"lastScaleTime"`
   325  
   326  	// AbleToScale indicates that we can access the target fleet
   327  	AbleToScale bool `json:"ableToScale"`
   328  
   329  	// ScalingLimited indicates that the calculated scale would be above or below the range
   330  	// defined by MinReplicas and MaxReplicas, and has thus been capped.
   331  	ScalingLimited bool `json:"scalingLimited"`
   332  
   333  	// LastAppliedPolicy is the ID of the last applied policy in the ChainPolicy.
   334  	// Used to track policy transitions for logging purposes.
   335  	LastAppliedPolicy FleetAutoscalerPolicyType `json:"lastAppliedPolicy"`
   336  }
   337  
   338  // FleetAutoscaleRequest defines the request to webhook autoscaler endpoint
   339  type FleetAutoscaleRequest struct {
   340  	// UID is an identifier for the individual request/response. It allows us to distinguish instances of requests which are
   341  	// otherwise identical (parallel requests, requests when earlier requests did not modify etc)
   342  	// The UID is meant to track the round trip (request/response) between the Autoscaler and the WebHook, not the user request.
   343  	// It is suitable for correlating log entries between the webhook and apiserver, for either auditing or debugging.
   344  	UID types.UID `json:"uid"`
   345  	// Name is the name of the Fleet being scaled
   346  	Name string `json:"name"`
   347  	// Namespace is the namespace associated with the request (if any).
   348  	Namespace string `json:"namespace"`
   349  	// The Fleet's status values
   350  	Status agonesv1.FleetStatus `json:"status"`
   351  	// Standard map labels; More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels.
   352  	Labels map[string]string `json:"labels,omitempty"`
   353  	// Standard map annotations; More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations.
   354  	Annotations map[string]string `json:"annotations,omitempty"`
   355  }
   356  
   357  // FleetAutoscaleResponse defines the response of webhook autoscaler endpoint
   358  type FleetAutoscaleResponse struct {
   359  	// UID is an identifier for the individual request/response.
   360  	// This should be copied over from the corresponding FleetAutoscaleRequest.
   361  	UID types.UID `json:"uid"`
   362  	// Set to false if no scaling should occur to the Fleet
   363  	Scale bool `json:"scale"`
   364  	// The targeted replica count
   365  	Replicas int32 `json:"replicas"`
   366  }
   367  
   368  // FleetAutoscaleReview is passed to the webhook with a populated Request value,
   369  // and then returned with a populated Response.
   370  type FleetAutoscaleReview struct {
   371  	Request  *FleetAutoscaleRequest  `json:"request"`
   372  	Response *FleetAutoscaleResponse `json:"response"`
   373  }
   374  
   375  // Validate validates the FleetAutoscaler scaling settings
   376  func (fas *FleetAutoscaler) Validate() field.ErrorList {
   377  	allErrs := fas.Spec.Policy.ValidatePolicy(field.NewPath("spec", "policy"))
   378  
   379  	if fas.Spec.Sync != nil {
   380  		allErrs = append(allErrs, fas.Spec.Sync.FixedInterval.ValidateFixedIntervalSync(field.NewPath("spec", "sync", "fixedInterval"))...)
   381  	}
   382  	return allErrs
   383  }
   384  
   385  // ValidatePolicy validates a FleetAutoscalerPolicy's settings.
   386  func (f *FleetAutoscalerPolicy) ValidatePolicy(fldPath *field.Path) field.ErrorList {
   387  	var allErrs field.ErrorList
   388  	switch f.Type {
   389  	case BufferPolicyType:
   390  		allErrs = f.Buffer.ValidateBufferPolicy(fldPath.Child("buffer"))
   391  
   392  	case WebhookPolicyType:
   393  		allErrs = f.Webhook.ValidateURLConfiguration(fldPath.Child("webhook"))
   394  
   395  	case CounterPolicyType:
   396  		allErrs = f.Counter.ValidateCounterPolicy(fldPath.Child("counter"))
   397  
   398  	case ListPolicyType:
   399  		allErrs = f.List.ValidateListPolicy(fldPath.Child("list"))
   400  
   401  	case SchedulePolicyType:
   402  		allErrs = f.Schedule.ValidateSchedulePolicy(fldPath.Child("schedule"))
   403  
   404  	case ChainPolicyType:
   405  		allErrs = f.Chain.ValidateChainPolicy(fldPath.Child("chain"))
   406  
   407  	case WasmPolicyType:
   408  		allErrs = f.Wasm.ValidateWasmPolicy(fldPath.Child("wasm"))
   409  	}
   410  	return allErrs
   411  }
   412  
   413  // ValidateURLConfiguration validates the URLConfiguration settings to make sure either a URL is set or
   414  // a Service is set, but not both. Also checks that the CABundle is valid if HTTPS is used.
   415  func (w *URLConfiguration) ValidateURLConfiguration(fldPath *field.Path) field.ErrorList {
   416  	var allErrs field.ErrorList
   417  	if w == nil {
   418  		return append(allErrs, field.Required(fldPath, "webhook policy config params are missing"))
   419  	}
   420  	if w.Service == nil && w.URL == nil {
   421  		allErrs = append(allErrs, field.Required(fldPath, "url should be provided"))
   422  	}
   423  	if w.Service != nil && w.URL != nil {
   424  		allErrs = append(allErrs, field.Duplicate(fldPath.Child("url"), "service and url cannot be used simultaneously"))
   425  	}
   426  	if w.CABundle != nil {
   427  		rootCAs := x509.NewCertPool()
   428  		// Check that CABundle provided is correctly encoded certificate
   429  		if ok := rootCAs.AppendCertsFromPEM(w.CABundle); !ok {
   430  			allErrs = append(allErrs, field.Invalid(fldPath.Child("caBundle"), w.CABundle, "CA Bundle is not valid"))
   431  		}
   432  	}
   433  	if w.URL != nil {
   434  		_, err := url.Parse(*w.URL)
   435  		if err != nil {
   436  			allErrs = append(allErrs, field.Invalid(fldPath.Child("url"), *w.URL, "url is not valid"))
   437  		}
   438  	}
   439  	return allErrs
   440  }
   441  
   442  // ValidateBufferPolicy validates the FleetAutoscaler Buffer policy settings
   443  func (b *BufferPolicy) ValidateBufferPolicy(fldPath *field.Path) field.ErrorList {
   444  	var allErrs field.ErrorList
   445  	if b == nil {
   446  		return append(allErrs, field.Required(fldPath, "buffer policy config params are missing"))
   447  	}
   448  	if b.MinReplicas > b.MaxReplicas {
   449  		allErrs = append(allErrs, field.Invalid(fldPath.Child("minReplicas"), b.MinReplicas, "minReplicas should be smaller than maxReplicas"))
   450  	}
   451  	if b.BufferSize.Type == intstr.Int {
   452  		if b.BufferSize.IntValue() <= 0 {
   453  			allErrs = append(allErrs, field.Invalid(fldPath.Child("bufferSize"), b.BufferSize.IntValue(), apimachineryvalidation.IsNegativeErrorMsg))
   454  		}
   455  		if b.MaxReplicas < int32(b.BufferSize.IntValue()) {
   456  			allErrs = append(allErrs, field.Invalid(fldPath.Child("maxReplicas"), b.MaxReplicas, "maxReplicas should be bigger than or equal to bufferSize"))
   457  		}
   458  		if b.MinReplicas != 0 && b.MinReplicas < int32(b.BufferSize.IntValue()) {
   459  			allErrs = append(allErrs, field.Invalid(fldPath.Child("minReplicas"), b.MinReplicas, "minReplicas should be bigger than or equal to bufferSize"))
   460  		}
   461  	} else {
   462  		r, err := intstr.GetScaledValueFromIntOrPercent(&b.BufferSize, 100, true)
   463  		if err != nil || r < 1 || r > 99 {
   464  			allErrs = append(allErrs, field.Invalid(fldPath.Child("bufferSize"), b.BufferSize.String(), "bufferSize should be between 1% and 99%"))
   465  		}
   466  		// When there is no allocated gameservers in a fleet,
   467  		// Fleetautoscaler would reduce size of a fleet to MinReplicas.
   468  		// If we have 0 MinReplicas and 0 Allocated then Fleetautoscaler would set Ready Replicas to 0
   469  		// and we will not be able to raise the number of GS in a Fleet above zero
   470  		if b.MinReplicas < 1 {
   471  			allErrs = append(allErrs, field.Invalid(fldPath.Child("minReplicas"), b.MinReplicas, apimachineryvalidation.IsNegativeErrorMsg))
   472  		}
   473  	}
   474  	return allErrs
   475  }
   476  
   477  // ValidateCounterPolicy validates the FleetAutoscaler Counter policy settings.
   478  // Does not validate if a Counter with name CounterPolicy.Key is present in the fleet.
   479  // nolint:dupl  // Linter errors on lines are duplicate of ValidateListPolicy
   480  func (c *CounterPolicy) ValidateCounterPolicy(fldPath *field.Path) field.ErrorList {
   481  	var allErrs field.ErrorList
   482  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   483  		return append(allErrs, field.Forbidden(fldPath, "feature CountsAndLists must be enabled"))
   484  	}
   485  
   486  	if c == nil {
   487  		return append(allErrs, field.Required(fldPath, "counter policy config params are missing"))
   488  	}
   489  
   490  	if c.MinCapacity > c.MaxCapacity {
   491  		allErrs = append(allErrs, field.Invalid(fldPath.Child("minCapacity"), c.MinCapacity, "minCapacity should be smaller than maxCapacity"))
   492  	}
   493  
   494  	if c.BufferSize.Type == intstr.Int {
   495  		if c.BufferSize.IntValue() <= 0 {
   496  			allErrs = append(allErrs, field.Invalid(fldPath.Child("bufferSize"), c.BufferSize.IntValue(), apimachineryvalidation.IsNegativeErrorMsg))
   497  		}
   498  		if c.MaxCapacity < int64(c.BufferSize.IntValue()) {
   499  			allErrs = append(allErrs, field.Invalid(fldPath.Child("maxCapacity"), c.MaxCapacity, "maxCapacity should be bigger than or equal to bufferSize"))
   500  		}
   501  		if c.MinCapacity != 0 && c.MinCapacity < int64(c.BufferSize.IntValue()) {
   502  			allErrs = append(allErrs, field.Invalid(fldPath.Child("minCapacity"), c.MinCapacity, "minCapacity should be bigger than or equal to bufferSize"))
   503  		}
   504  	} else {
   505  		r, err := intstr.GetScaledValueFromIntOrPercent(&c.BufferSize, 100, true)
   506  		if err != nil || r < 1 || r > 99 {
   507  			allErrs = append(allErrs, field.Invalid(fldPath.Child("bufferSize"), c.BufferSize.String(), "bufferSize should be between 1% and 99%"))
   508  		}
   509  		// When bufferSize in percentage format is used, minCapacity should be more than 0.
   510  		if c.MinCapacity < 1 {
   511  			allErrs = append(allErrs, field.Invalid(fldPath.Child("minCapacity"), c.BufferSize.String(), " when bufferSize in percentage format is used, minCapacity should be more than 0"))
   512  		}
   513  	}
   514  
   515  	return allErrs
   516  }
   517  
   518  // ValidateListPolicy validates the FleetAutoscaler List policy settings.
   519  // Does not validate if a List with name ListPolicy.Key is present in the fleet.
   520  // nolint:dupl  // Linter errors on lines are duplicate of ValidateCounterPolicy
   521  func (l *ListPolicy) ValidateListPolicy(fldPath *field.Path) field.ErrorList {
   522  	var allErrs field.ErrorList
   523  	if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) {
   524  		return append(allErrs, field.Forbidden(fldPath, "feature CountsAndLists must be enabled"))
   525  	}
   526  	if l == nil {
   527  		return append(allErrs, field.Required(fldPath, "list policy config params are missing"))
   528  	}
   529  	if l.MinCapacity > l.MaxCapacity {
   530  		allErrs = append(allErrs, field.Invalid(fldPath.Child("minCapacity"), l.MinCapacity, "minCapacity should be smaller than maxCapacity"))
   531  	}
   532  	if l.BufferSize.Type == intstr.Int {
   533  		if l.BufferSize.IntValue() <= 0 {
   534  			allErrs = append(allErrs, field.Invalid(fldPath.Child("bufferSize"), l.BufferSize.IntValue(), apimachineryvalidation.IsNegativeErrorMsg))
   535  		}
   536  		if l.MaxCapacity < int64(l.BufferSize.IntValue()) {
   537  			allErrs = append(allErrs, field.Invalid(fldPath.Child("maxCapacity"), l.MaxCapacity, "maxCapacity should be bigger than or equal to bufferSize"))
   538  		}
   539  		if l.MinCapacity != 0 && l.MinCapacity < int64(l.BufferSize.IntValue()) {
   540  			allErrs = append(allErrs, field.Invalid(fldPath.Child("minCapacity"), l.MinCapacity, "minCapacity should be bigger than or equal to bufferSize"))
   541  		}
   542  	} else {
   543  		r, err := intstr.GetScaledValueFromIntOrPercent(&l.BufferSize, 100, true)
   544  		if err != nil || r < 1 || r > 99 {
   545  			allErrs = append(allErrs, field.Invalid(fldPath.Child("bufferSize"), l.BufferSize.String(), "bufferSize should be between 1% and 99%"))
   546  		}
   547  		// When bufferSize in percentage format is used, minCapacity should be more than 0.
   548  		if l.MinCapacity < 1 {
   549  			allErrs = append(allErrs, field.Invalid(fldPath.Child("minCapacity"), l.BufferSize.String(), " when bufferSize in percentage format is used, minCapacity should be more than 0"))
   550  		}
   551  	}
   552  	return allErrs
   553  }
   554  
   555  // ValidateSchedulePolicy validates the FleetAutoscaler Schedule policy settings.
   556  func (s *SchedulePolicy) ValidateSchedulePolicy(fldPath *field.Path) field.ErrorList {
   557  	var allErrs field.ErrorList
   558  	if s == nil {
   559  		return append(allErrs, field.Required(fldPath, "schedule policy config params are missing"))
   560  	}
   561  	if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) {
   562  		return append(allErrs, field.Forbidden(fldPath, "feature ScheduledAutoscaler must be enabled"))
   563  	}
   564  	if s.ActivePeriod.Timezone != "" {
   565  		if _, err := time.LoadLocation(s.ActivePeriod.Timezone); err != nil {
   566  			allErrs = append(allErrs, field.Invalid(fldPath.Child("activePeriod").Child("timezone"), s.ActivePeriod.Timezone, fmt.Sprintf("Error parsing timezone: %s\n", err)))
   567  		}
   568  	}
   569  	if !s.Between.End.IsZero() {
   570  		startTime := s.Between.Start.Time
   571  		endTime := s.Between.End.Time
   572  		var endErr error
   573  		if endTime.Before(time.Now()) {
   574  			allErrs = append(allErrs, field.Invalid(fldPath.Child("between").Child("end"), s.Between.End, "end time must be after the current time"))
   575  			endErr = errors.New("end time before current time")
   576  		}
   577  
   578  		if !startTime.IsZero() && endErr == nil {
   579  			if endTime.Before(startTime) {
   580  				allErrs = append(allErrs, field.Invalid(fldPath.Child("between"), s.Between, "start time must be before end time"))
   581  			}
   582  		}
   583  	}
   584  	if s.ActivePeriod.StartCron != "" {
   585  		// If startCron is not a valid cron expression, append an error
   586  		if _, err := cron.ParseStandard(s.ActivePeriod.StartCron); err != nil {
   587  			allErrs = append(allErrs, field.Invalid(fldPath.Child("activePeriod").Child("startCron"), s.ActivePeriod.StartCron, fmt.Sprintf("invalid startCron: %s", err)))
   588  		}
   589  		// If the cron expression contains a CRON_TZ or TZ specification, append an error
   590  		if strings.Contains(s.ActivePeriod.StartCron, "TZ") {
   591  			allErrs = append(allErrs, field.Invalid(fldPath.Child("activePeriod").Child("startCron"), s.ActivePeriod.StartCron, "CRON_TZ or TZ used in activePeriod is not supported, please use the .schedule.timezone field to specify a timezone"))
   592  		}
   593  	}
   594  	if s.ActivePeriod.Duration != "" {
   595  		if _, err := time.ParseDuration(s.ActivePeriod.Duration); err != nil {
   596  			allErrs = append(allErrs, field.Invalid(fldPath.Child("activePeriod").Child("duration"), s.ActivePeriod.StartCron, fmt.Sprintf("invalid duration: %s", err)))
   597  		}
   598  	}
   599  	return allErrs
   600  }
   601  
   602  // ValidateChainPolicy validates the FleetAutoscaler Chain policy settings.
   603  func (c *ChainPolicy) ValidateChainPolicy(fldPath *field.Path) field.ErrorList {
   604  	var allErrs field.ErrorList
   605  	if c == nil {
   606  		return append(allErrs, field.Required(fldPath, "chain policy config params are missing"))
   607  	}
   608  	if !runtime.FeatureEnabled(runtime.FeatureScheduledAutoscaler) {
   609  		return append(allErrs, field.Forbidden(fldPath, "feature ScheduledAutoscaler must be enabled"))
   610  	}
   611  	seenIDs := make(map[string]bool)
   612  	for i, entry := range *c {
   613  
   614  		// Ensure that all IDs are unique
   615  		if seenIDs[entry.ID] {
   616  			allErrs = append(allErrs, field.Invalid(fldPath, entry.ID, "id of chain entry must be unique"))
   617  		} else {
   618  			seenIDs[entry.ID] = true
   619  		}
   620  		// Ensure that chain entry has a policy
   621  		hasValidPolicy := entry.Buffer != nil || entry.Webhook != nil || entry.Counter != nil || entry.List != nil || entry.Schedule != nil || entry.Wasm != nil
   622  		if entry.Type == "" || !hasValidPolicy {
   623  			allErrs = append(allErrs, field.Required(fldPath.Index(i), "valid policy is missing"))
   624  		}
   625  		// Validate the chain entry's policy
   626  		allErrs = append(allErrs, entry.FleetAutoscalerPolicy.ValidatePolicy(fldPath.Index(i).Child("policy"))...)
   627  	}
   628  	return allErrs
   629  }
   630  
   631  // ValidateWasmPolicy validates the FleetAutoscaler Wasm policy settings
   632  func (w *WasmPolicy) ValidateWasmPolicy(fldPath *field.Path) field.ErrorList {
   633  	var allErrs field.ErrorList
   634  	if w == nil {
   635  		return append(allErrs, field.Required(fldPath, "wasm policy config params are missing"))
   636  	}
   637  
   638  	if !runtime.FeatureEnabled(runtime.FeatureWasmAutoscaler) {
   639  		return append(allErrs, field.Forbidden(fldPath, "feature WasmAutoscaler must be enabled"))
   640  	}
   641  
   642  	fldPath = fldPath.Child("from")
   643  	if w.From.URL == nil {
   644  		return append(allErrs, field.Required(fldPath, "wasm from configuration is missing"))
   645  	}
   646  	allErrs = w.From.URL.ValidateURLConfiguration(fldPath.Child("url"))
   647  
   648  	return allErrs
   649  }
   650  
   651  // ValidateFixedIntervalSync validates the FixedIntervalSync settings
   652  func (i *FixedIntervalSync) ValidateFixedIntervalSync(fldPath *field.Path) field.ErrorList {
   653  	var allErrs field.ErrorList
   654  	if i == nil {
   655  		return append(allErrs, field.Required(fldPath, "fixedInterval sync config params are missing"))
   656  	}
   657  	if i.Seconds <= 0 {
   658  		allErrs = append(allErrs, field.Invalid(fldPath.Child("seconds"), i.Seconds, apimachineryvalidation.IsNegativeErrorMsg))
   659  	}
   660  	return allErrs
   661  }
   662  
   663  // ApplyDefaults applies default values to the FleetAutoscaler
   664  func (fas *FleetAutoscaler) ApplyDefaults() {
   665  	if fas.Spec.Sync == nil {
   666  		fas.Spec.Sync = &FleetAutoscalerSync{}
   667  	}
   668  	if fas.Spec.Sync.Type == "" {
   669  		fas.Spec.Sync.Type = FixedIntervalSyncType
   670  	}
   671  	if fas.Spec.Sync.FixedInterval.Seconds == 0 {
   672  		fas.Spec.Sync.FixedInterval.Seconds = defaultIntervalSyncSeconds
   673  	}
   674  }