agones.dev/agones@v1.54.0/cmd/controller/main.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 // Controller for gameservers 16 package main 17 18 import ( 19 "context" 20 "encoding/json" 21 "fmt" 22 "io" 23 "os" 24 "path/filepath" 25 "strings" 26 "time" 27 28 "agones.dev/agones/pkg" 29 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 30 "agones.dev/agones/pkg/client/clientset/versioned" 31 "agones.dev/agones/pkg/client/informers/externalversions" 32 "agones.dev/agones/pkg/cloudproduct" 33 "agones.dev/agones/pkg/fleetautoscalers" 34 "agones.dev/agones/pkg/fleets" 35 "agones.dev/agones/pkg/gameservers" 36 "agones.dev/agones/pkg/gameserversets" 37 "agones.dev/agones/pkg/metrics" 38 "agones.dev/agones/pkg/portallocator" 39 "agones.dev/agones/pkg/util/httpserver" 40 "agones.dev/agones/pkg/util/runtime" 41 "agones.dev/agones/pkg/util/signals" 42 "github.com/google/uuid" 43 "github.com/heptiolabs/healthcheck" 44 "github.com/pkg/errors" 45 "github.com/sirupsen/logrus" 46 "github.com/spf13/pflag" 47 "github.com/spf13/viper" 48 "gopkg.in/natefinch/lumberjack.v2" 49 corev1 "k8s.io/api/core/v1" 50 extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 51 "k8s.io/apimachinery/pkg/api/resource" 52 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 53 "k8s.io/client-go/informers" 54 "k8s.io/client-go/kubernetes" 55 "k8s.io/client-go/tools/leaderelection" 56 "k8s.io/client-go/tools/leaderelection/resourcelock" 57 ) 58 59 const ( 60 enableStackdriverMetricsFlag = "stackdriver-exporter" 61 stackdriverLabels = "stackdriver-labels" 62 enablePrometheusMetricsFlag = "prometheus-exporter" 63 projectIDFlag = "gcp-project-id" 64 sidecarImageFlag = "sidecar-image" 65 sidecarCPURequestFlag = "sidecar-cpu-request" 66 sidecarCPULimitFlag = "sidecar-cpu-limit" 67 sidecarMemoryRequestFlag = "sidecar-memory-request" 68 sidecarMemoryLimitFlag = "sidecar-memory-limit" 69 sidecarRunAsUserFlag = "sidecar-run-as-user" 70 sidecarRequestsRateLimitFlag = "sidecar-requests-rate-limit" 71 sdkServerAccountFlag = "sdk-service-account" 72 pullSidecarFlag = "always-pull-sidecar" 73 minPortFlag = "min-port" 74 maxPortFlag = "max-port" 75 additionalPortRangesFlag = "additional-port-ranges" 76 certFileFlag = "cert-file" 77 keyFileFlag = "key-file" 78 numWorkersFlag = "num-workers" 79 apiServerSustainedQPSFlag = "api-server-qps" 80 apiServerBurstQPSFlag = "api-server-qps-burst" 81 logDirFlag = "log-dir" 82 logLevelFlag = "log-level" 83 logSizeLimitMBFlag = "log-size-limit-mb" 84 kubeconfigFlag = "kubeconfig" 85 allocationBatchWaitTime = "allocation-batch-wait-time" 86 defaultResync = 30 * time.Second 87 podNamespace = "pod-namespace" 88 leaderElectionFlag = "leader-election" 89 maxCreationParallelismFlag = "max-creation-parallelism" 90 maxGameServerCreationsPerBatchFlag = "max-game-server-creations-per-batch" 91 maxDeletionParallelismFlag = "max-deletion-parallelism" 92 maxGameServerDeletionsPerBatchFlag = "max-game-server-deletions-per-batch" 93 maxPodPendingCountFlag = "max-pod-pending-count" 94 ) 95 96 var ( 97 logger = runtime.NewLoggerWithSource("main") 98 ) 99 100 func setupLogging(logDir string, logSizeLimitMB int) { 101 logFileName := filepath.Join(logDir, "agones-controller-"+time.Now().Format("20060102_150405")+".log") 102 103 const maxLogSizeMB = 100 104 maxBackups := (logSizeLimitMB - maxLogSizeMB) / maxLogSizeMB 105 logger.WithField("filename", logFileName).WithField("numbackups", maxBackups).Info("logging to file") 106 logrus.SetOutput( 107 io.MultiWriter( 108 logrus.StandardLogger().Out, 109 &lumberjack.Logger{ 110 Filename: logFileName, 111 MaxSize: maxLogSizeMB, 112 MaxBackups: maxBackups, 113 }, 114 ), 115 ) 116 } 117 118 // main starts the operator for the gameserver CRD 119 func main() { 120 ctx, cancel := signals.NewSigKillContext() 121 ctlConf := parseEnvFlags() 122 123 if ctlConf.LogDir != "" { 124 setupLogging(ctlConf.LogDir, ctlConf.LogSizeLimitMB) 125 } 126 127 logger.WithField("logLevel", ctlConf.LogLevel).Info("Setting LogLevel configuration") 128 level, err := logrus.ParseLevel(strings.ToLower(ctlConf.LogLevel)) 129 if err == nil { 130 runtime.SetLevel(level) 131 } else { 132 logger.WithError(err).Info("Specified wrong Logging.SdkServer. Setting default loglevel - Info") 133 runtime.SetLevel(logrus.InfoLevel) 134 } 135 136 logger.WithField("version", pkg.Version).WithField("featureGates", runtime.EncodeFeatures()). 137 WithField("ctlConf", ctlConf).Info("starting gameServer operator...") 138 139 if errs := ctlConf.validate(); len(errs) != 0 { 140 for _, err := range errs { 141 logger.Error(err) 142 } 143 logger.Fatal("Could not create controller from environment or flags") 144 } 145 146 // if the kubeconfig fails InClusterBuildConfig will try in cluster config 147 clientConf, err := runtime.InClusterBuildConfig(logger, ctlConf.KubeConfig) 148 if err != nil { 149 logger.WithError(err).Fatal("Could not create in cluster config") 150 } 151 152 clientConf.QPS = float32(ctlConf.APIServerSustainedQPS) 153 clientConf.Burst = ctlConf.APIServerBurstQPS 154 155 kubeClient, err := kubernetes.NewForConfig(clientConf) 156 if err != nil { 157 logger.WithError(err).Fatal("Could not create the kubernetes clientset") 158 } 159 160 extClient, err := extclientset.NewForConfig(clientConf) 161 if err != nil { 162 logger.WithError(err).Fatal("Could not create the api extension clientset") 163 } 164 165 agonesClient, err := versioned.NewForConfig(clientConf) 166 if err != nil { 167 logger.WithError(err).Fatal("Could not create the agones api clientset") 168 } 169 170 controllerHooks, err := cloudproduct.NewFromFlag(ctx, kubeClient) 171 if err != nil { 172 logger.WithError(err).Fatal("Could not initialize cloud product") 173 } 174 175 agonesInformerFactory := externalversions.NewSharedInformerFactory(agonesClient, defaultResync) 176 kubeInformerFactory := informers.NewSharedInformerFactory(kubeClient, defaultResync) 177 178 server := &httpserver.Server{Logger: logger} 179 var rs []runner 180 var health healthcheck.Handler 181 182 metricsConf := metrics.Config{ 183 Stackdriver: ctlConf.Stackdriver, 184 PrometheusMetrics: ctlConf.PrometheusMetrics, 185 GCPProjectID: ctlConf.GCPProjectID, 186 StackdriverLabels: ctlConf.StackdriverLabels, 187 } 188 189 health, closer := metrics.SetupMetrics(metricsConf, server) 190 defer closer() 191 192 // If we are using Prometheus only exporter we can make reporting more often, 193 // every 1 seconds, if we are using Stackdriver we would use 60 seconds reporting period, 194 // which is a requirements of Stackdriver, otherwise most of time series would be invalid for Stackdriver 195 metrics.SetReportingPeriod(ctlConf.PrometheusMetrics, ctlConf.Stackdriver) 196 197 // Add metrics controller only if we configure one of metrics exporters 198 if ctlConf.PrometheusMetrics || ctlConf.Stackdriver { 199 rs = append(rs, metrics.NewController(kubeClient, agonesClient, kubeInformerFactory, agonesInformerFactory)) 200 } 201 202 server.Handle("/", health) 203 204 gsCounter := gameservers.NewPerNodeCounter(kubeInformerFactory, agonesInformerFactory) 205 206 gsController := gameservers.NewController(controllerHooks, health, 207 ctlConf.PortRanges, ctlConf.SidecarImage, ctlConf.AlwaysPullSidecar, 208 ctlConf.SidecarCPURequest, ctlConf.SidecarCPULimit, 209 ctlConf.SidecarMemoryRequest, ctlConf.SidecarMemoryLimit, ctlConf.SidecarRunAsUser, ctlConf.SidecarRequestsRateLimit, ctlConf.SdkServiceAccount, 210 kubeClient, kubeInformerFactory, extClient, agonesClient, agonesInformerFactory) 211 gsSetController := gameserversets.NewController(health, gsCounter, 212 kubeClient, extClient, agonesClient, agonesInformerFactory, ctlConf.MaxCreationParallelism, ctlConf.MaxDeletionParallelism, ctlConf.MaxGameServerCreationsPerBatch, ctlConf.MaxGameServerDeletionsPerBatch, ctlConf.MaxPodPendingCount) 213 fleetController := fleets.NewController(health, kubeClient, extClient, agonesClient, agonesInformerFactory) 214 fasController := fleetautoscalers.NewController(health, 215 kubeClient, extClient, agonesClient, agonesInformerFactory, gsCounter) 216 217 rs = append(rs, 218 gsCounter, gsController, gsSetController, fleetController, fasController) 219 220 runRunner := func(r runner) { 221 if err := r.Run(ctx, ctlConf.NumWorkers); err != nil { 222 logger.WithError(err).Fatalf("could not start runner! %T", r) 223 } 224 } 225 226 // Server has to be started earlier because it contains the health check. 227 // This allows the controller to not fail health check during install when there is replication 228 go runRunner(server) 229 230 whenLeader(ctx, cancel, logger, ctlConf.LeaderElection, kubeClient, ctlConf.PodNamespace, func(ctx context.Context) { 231 kubeInformerFactory.Start(ctx.Done()) 232 agonesInformerFactory.Start(ctx.Done()) 233 234 for _, r := range rs { 235 go runRunner(r) 236 } 237 238 <-ctx.Done() 239 logger.Info("Shut down agones controllers") 240 }) 241 } 242 243 func parseEnvFlags() config { 244 exec, err := os.Executable() 245 if err != nil { 246 logger.WithError(err).Fatal("Could not get executable path") 247 } 248 249 base := filepath.Dir(exec) 250 viper.SetDefault(sidecarImageFlag, "us-docker.pkg.dev/agones-images/release/agones-sdk:"+pkg.Version) 251 viper.SetDefault(sidecarCPURequestFlag, "0") 252 viper.SetDefault(sidecarCPULimitFlag, "0") 253 viper.SetDefault(sidecarMemoryRequestFlag, "0") 254 viper.SetDefault(sidecarMemoryLimitFlag, "0") 255 viper.SetDefault(sidecarRunAsUserFlag, "1000") 256 viper.SetDefault(sidecarRequestsRateLimitFlag, "500ms") 257 viper.SetDefault(pullSidecarFlag, false) 258 viper.SetDefault(sdkServerAccountFlag, "agones-sdk") 259 viper.SetDefault(certFileFlag, filepath.Join(base, "certs", "server.crt")) 260 viper.SetDefault(keyFileFlag, filepath.Join(base, "certs", "server.key")) 261 viper.SetDefault(enablePrometheusMetricsFlag, true) 262 viper.SetDefault(enableStackdriverMetricsFlag, false) 263 viper.SetDefault(stackdriverLabels, "") 264 viper.SetDefault(allocationBatchWaitTime, 500*time.Millisecond) 265 viper.SetDefault(podNamespace, "agones-system") 266 viper.SetDefault(leaderElectionFlag, false) 267 268 viper.SetDefault(projectIDFlag, "") 269 viper.SetDefault(numWorkersFlag, 64) 270 viper.SetDefault(apiServerSustainedQPSFlag, 100) 271 viper.SetDefault(apiServerBurstQPSFlag, 200) 272 viper.SetDefault(logDirFlag, "") 273 viper.SetDefault(logLevelFlag, "Info") 274 viper.SetDefault(logSizeLimitMBFlag, 10000) // 10 GB, will be split into 100 MB chunks 275 276 viper.SetDefault(maxCreationParallelismFlag, 16) 277 viper.SetDefault(maxGameServerCreationsPerBatchFlag, 64) 278 viper.SetDefault(maxDeletionParallelismFlag, 64) 279 viper.SetDefault(maxGameServerDeletionsPerBatchFlag, 64) 280 viper.SetDefault(maxPodPendingCountFlag, 5000) 281 282 pflag.String(sidecarImageFlag, viper.GetString(sidecarImageFlag), "Flag to overwrite the GameServer sidecar image that is used. Can also use SIDECAR env variable") 283 pflag.String(sidecarCPULimitFlag, viper.GetString(sidecarCPULimitFlag), "Flag to overwrite the GameServer sidecar container's cpu limit. Can also use SIDECAR_CPU_LIMIT env variable") 284 pflag.String(sidecarCPURequestFlag, viper.GetString(sidecarCPURequestFlag), "Flag to overwrite the GameServer sidecar container's cpu request. Can also use SIDECAR_CPU_REQUEST env variable") 285 pflag.String(sidecarMemoryLimitFlag, viper.GetString(sidecarMemoryLimitFlag), "Flag to overwrite the GameServer sidecar container's memory limit. Can also use SIDECAR_MEMORY_LIMIT env variable") 286 pflag.String(sidecarMemoryRequestFlag, viper.GetString(sidecarMemoryRequestFlag), "Flag to overwrite the GameServer sidecar container's memory request. Can also use SIDECAR_MEMORY_REQUEST env variable") 287 pflag.Int32(sidecarRunAsUserFlag, viper.GetInt32(sidecarRunAsUserFlag), "Flag to indicate the GameServer sidecar container's UID. Can also use SIDECAR_RUN_AS_USER env variable") 288 pflag.String(sidecarRequestsRateLimitFlag, viper.GetString(sidecarRequestsRateLimitFlag), "Flag to indicate the GameServer sidecar requests rate limit. Can also use SIDECAR_REQUESTS_RATE_LIMIT env variable") 289 pflag.Bool(pullSidecarFlag, viper.GetBool(pullSidecarFlag), "For development purposes, set the sidecar image to have a ImagePullPolicy of Always. Can also use ALWAYS_PULL_SIDECAR env variable") 290 pflag.String(sdkServerAccountFlag, viper.GetString(sdkServerAccountFlag), "Overwrite what service account default for GameServer Pods. Defaults to Can also use SDK_SERVICE_ACCOUNT") 291 pflag.Int32(minPortFlag, 0, "Required. The minimum port that that a GameServer can be allocated to. Can also use MIN_PORT env variable.") 292 pflag.Int32(maxPortFlag, 0, "Required. The maximum port that that a GameServer can be allocated to. Can also use MAX_PORT env variable.") 293 pflag.String(additionalPortRangesFlag, viper.GetString(additionalPortRangesFlag), `Optional. Named set of port ranges in JSON object format: '{"game": [5000, 6000]}'. Can Also use ADDITIONAL_PORT_RANGES env variable.`) 294 pflag.String(keyFileFlag, viper.GetString(keyFileFlag), "Optional. Path to the key file") 295 pflag.String(certFileFlag, viper.GetString(certFileFlag), "Optional. Path to the crt file") 296 pflag.String(kubeconfigFlag, viper.GetString(kubeconfigFlag), "Optional. kubeconfig to run the controller out of the cluster. Only use it for debugging as webhook won't works.") 297 pflag.Bool(enablePrometheusMetricsFlag, viper.GetBool(enablePrometheusMetricsFlag), "Flag to activate metrics of Agones. Can also use PROMETHEUS_EXPORTER env variable.") 298 pflag.Bool(enableStackdriverMetricsFlag, viper.GetBool(enableStackdriverMetricsFlag), "Flag to activate stackdriver monitoring metrics for Agones. Can also use STACKDRIVER_EXPORTER env variable.") 299 pflag.String(stackdriverLabels, viper.GetString(stackdriverLabels), "A set of default labels to add to all stackdriver metrics generated. By default metadata are automatically added using Kubernetes API and GCP metadata enpoint.") 300 pflag.String(projectIDFlag, viper.GetString(projectIDFlag), "GCP ProjectID used for Stackdriver, if not specified ProjectID from Application Default Credentials would be used. Can also use GCP_PROJECT_ID env variable.") 301 pflag.Int32(numWorkersFlag, 64, "Number of controller workers per resource type") 302 pflag.Int32(apiServerSustainedQPSFlag, 100, "Maximum sustained queries per second to send to the API server") 303 pflag.Int32(apiServerBurstQPSFlag, 200, "Maximum burst queries per second to send to the API server") 304 pflag.String(logDirFlag, viper.GetString(logDirFlag), "If set, store logs in a given directory.") 305 pflag.Int32(logSizeLimitMBFlag, 1000, "Log file size limit in MB") 306 pflag.Int32(maxCreationParallelismFlag, viper.GetInt32(maxCreationParallelismFlag), "Maximum number of parallelizing creation calls in GSS controller") 307 pflag.Int32(maxGameServerCreationsPerBatchFlag, viper.GetInt32(maxGameServerCreationsPerBatchFlag), "Maximum number of GameServer creation calls per batch") 308 pflag.Int32(maxDeletionParallelismFlag, viper.GetInt32(maxDeletionParallelismFlag), "Maximum number of parallelizing deletion calls in GSS controller") 309 pflag.Int32(maxGameServerDeletionsPerBatchFlag, viper.GetInt32(maxGameServerDeletionsPerBatchFlag), "Maximum number of GameServers deletion calls per batch") 310 pflag.Int32(maxPodPendingCountFlag, viper.GetInt32(maxPodPendingCountFlag), "Maximum number of pending pods per game server set") 311 pflag.String(logLevelFlag, viper.GetString(logLevelFlag), "Agones Log level") 312 pflag.Duration(allocationBatchWaitTime, viper.GetDuration(allocationBatchWaitTime), "Flag to configure the waiting period between allocations batches") 313 pflag.String(podNamespace, viper.GetString(podNamespace), "namespace of current pod") 314 pflag.Bool(leaderElectionFlag, viper.GetBool(leaderElectionFlag), "Flag to enable/disable leader election for controller pod") 315 cloudproduct.BindFlags() 316 runtime.FeaturesBindFlags() 317 pflag.Parse() 318 319 viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 320 runtime.Must(viper.BindEnv(sidecarImageFlag)) 321 runtime.Must(viper.BindEnv(sidecarCPULimitFlag)) 322 runtime.Must(viper.BindEnv(sidecarCPURequestFlag)) 323 runtime.Must(viper.BindEnv(sidecarMemoryLimitFlag)) 324 runtime.Must(viper.BindEnv(sidecarMemoryRequestFlag)) 325 runtime.Must(viper.BindEnv(sidecarRunAsUserFlag)) 326 runtime.Must(viper.BindEnv(sidecarRequestsRateLimitFlag)) 327 runtime.Must(viper.BindEnv(pullSidecarFlag)) 328 runtime.Must(viper.BindEnv(sdkServerAccountFlag)) 329 runtime.Must(viper.BindEnv(minPortFlag)) 330 runtime.Must(viper.BindEnv(maxPortFlag)) 331 runtime.Must(viper.BindEnv(additionalPortRangesFlag)) 332 runtime.Must(viper.BindEnv(keyFileFlag)) 333 runtime.Must(viper.BindEnv(certFileFlag)) 334 runtime.Must(viper.BindEnv(kubeconfigFlag)) 335 runtime.Must(viper.BindEnv(enablePrometheusMetricsFlag)) 336 runtime.Must(viper.BindEnv(enableStackdriverMetricsFlag)) 337 runtime.Must(viper.BindEnv(stackdriverLabels)) 338 runtime.Must(viper.BindEnv(projectIDFlag)) 339 runtime.Must(viper.BindEnv(numWorkersFlag)) 340 runtime.Must(viper.BindEnv(apiServerSustainedQPSFlag)) 341 runtime.Must(viper.BindEnv(apiServerBurstQPSFlag)) 342 runtime.Must(viper.BindEnv(logLevelFlag)) 343 runtime.Must(viper.BindEnv(logDirFlag)) 344 runtime.Must(viper.BindEnv(logSizeLimitMBFlag)) 345 runtime.Must(viper.BindEnv(maxCreationParallelismFlag)) 346 runtime.Must(viper.BindEnv(maxGameServerCreationsPerBatchFlag)) 347 runtime.Must(viper.BindEnv(maxDeletionParallelismFlag)) 348 runtime.Must(viper.BindEnv(maxGameServerDeletionsPerBatchFlag)) 349 runtime.Must(viper.BindEnv(maxPodPendingCountFlag)) 350 runtime.Must(viper.BindEnv(allocationBatchWaitTime)) 351 runtime.Must(viper.BindEnv(podNamespace)) 352 runtime.Must(viper.BindEnv(leaderElectionFlag)) 353 runtime.Must(viper.BindPFlags(pflag.CommandLine)) 354 runtime.Must(cloudproduct.BindEnv()) 355 runtime.Must(runtime.FeaturesBindEnv()) 356 357 runtime.Must(runtime.ParseFeaturesFromEnv()) 358 359 requestCPU, err := resource.ParseQuantity(viper.GetString(sidecarCPURequestFlag)) 360 if err != nil { 361 logger.WithError(err).Fatalf("could not parse %s", sidecarCPURequestFlag) 362 } 363 364 limitCPU, err := resource.ParseQuantity(viper.GetString(sidecarCPULimitFlag)) 365 if err != nil { 366 logger.WithError(err).Fatalf("could not parse %s", sidecarCPULimitFlag) 367 } 368 369 requestMemory, err := resource.ParseQuantity(viper.GetString(sidecarMemoryRequestFlag)) 370 if err != nil { 371 logger.WithError(err).Fatalf("could not parse %s", sidecarMemoryRequestFlag) 372 } 373 374 limitMemory, err := resource.ParseQuantity(viper.GetString(sidecarMemoryLimitFlag)) 375 if err != nil { 376 logger.WithError(err).Fatalf("could not parse %s", sidecarMemoryLimitFlag) 377 } 378 379 requestsRateLimit, err := time.ParseDuration(viper.GetString(sidecarRequestsRateLimitFlag)) 380 if err != nil { 381 logger.WithError(err).Fatalf("could not parse %s", sidecarRequestsRateLimitFlag) 382 } 383 384 portRanges, err := parsePortRanges(viper.GetString(additionalPortRangesFlag)) 385 if err != nil { 386 logger.WithError(err).Fatalf("could not parse %s", additionalPortRangesFlag) 387 } 388 portRanges[agonesv1.DefaultPortRange] = portallocator.PortRange{ 389 MinPort: int32(viper.GetInt64(minPortFlag)), 390 MaxPort: int32(viper.GetInt64(maxPortFlag)), 391 } 392 393 return config{ 394 PortRanges: portRanges, 395 SidecarImage: viper.GetString(sidecarImageFlag), 396 SidecarCPURequest: requestCPU, 397 SidecarCPULimit: limitCPU, 398 SidecarMemoryRequest: requestMemory, 399 SidecarMemoryLimit: limitMemory, 400 SidecarRunAsUser: int(viper.GetInt32(sidecarRunAsUserFlag)), 401 SidecarRequestsRateLimit: requestsRateLimit, 402 SdkServiceAccount: viper.GetString(sdkServerAccountFlag), 403 AlwaysPullSidecar: viper.GetBool(pullSidecarFlag), 404 KeyFile: viper.GetString(keyFileFlag), 405 CertFile: viper.GetString(certFileFlag), 406 KubeConfig: viper.GetString(kubeconfigFlag), 407 PrometheusMetrics: viper.GetBool(enablePrometheusMetricsFlag), 408 Stackdriver: viper.GetBool(enableStackdriverMetricsFlag), 409 GCPProjectID: viper.GetString(projectIDFlag), 410 NumWorkers: int(viper.GetInt32(numWorkersFlag)), 411 APIServerSustainedQPS: int(viper.GetInt32(apiServerSustainedQPSFlag)), 412 APIServerBurstQPS: int(viper.GetInt32(apiServerBurstQPSFlag)), 413 LogDir: viper.GetString(logDirFlag), 414 LogLevel: viper.GetString(logLevelFlag), 415 LogSizeLimitMB: int(viper.GetInt32(logSizeLimitMBFlag)), 416 MaxGameServerCreationsPerBatch: int(viper.GetInt32(maxGameServerCreationsPerBatchFlag)), 417 MaxCreationParallelism: int(viper.GetInt32(maxCreationParallelismFlag)), 418 MaxGameServerDeletionsPerBatch: int(viper.GetInt32(maxGameServerDeletionsPerBatchFlag)), 419 MaxDeletionParallelism: int(viper.GetInt32(maxDeletionParallelismFlag)), 420 MaxPodPendingCount: int(viper.GetInt32(maxPodPendingCountFlag)), 421 StackdriverLabels: viper.GetString(stackdriverLabels), 422 AllocationBatchWaitTime: viper.GetDuration(allocationBatchWaitTime), 423 PodNamespace: viper.GetString(podNamespace), 424 LeaderElection: viper.GetBool(leaderElectionFlag), 425 } 426 } 427 428 func parsePortRanges(s string) (map[string]portallocator.PortRange, error) { 429 if s == "" || !runtime.FeatureEnabled(runtime.FeaturePortRanges) { 430 return map[string]portallocator.PortRange{}, nil 431 } 432 433 prs := map[string][]int32{} 434 if err := json.Unmarshal([]byte(s), &prs); err != nil { 435 return nil, fmt.Errorf("invlaid additional port range format: %w", err) 436 } 437 438 portRanges := map[string]portallocator.PortRange{} 439 for k, v := range prs { 440 if len(v) != 2 { 441 return nil, fmt.Errorf("invalid port range ports for %s: requires both min and max port", k) 442 } 443 portRanges[k] = portallocator.PortRange{ 444 MinPort: v[0], 445 MaxPort: v[1], 446 } 447 } 448 return portRanges, nil 449 } 450 451 // config stores all required configuration to create a game server controller. 452 type config struct { 453 PortRanges map[string]portallocator.PortRange 454 SidecarImage string 455 SidecarCPURequest resource.Quantity 456 SidecarCPULimit resource.Quantity 457 SidecarMemoryRequest resource.Quantity 458 SidecarMemoryLimit resource.Quantity 459 SidecarRunAsUser int 460 SidecarRequestsRateLimit time.Duration 461 SdkServiceAccount string 462 AlwaysPullSidecar bool 463 PrometheusMetrics bool 464 Stackdriver bool 465 StackdriverLabels string 466 KeyFile string 467 CertFile string 468 KubeConfig string 469 GCPProjectID string 470 NumWorkers int 471 APIServerSustainedQPS int 472 APIServerBurstQPS int 473 LogDir string 474 LogLevel string 475 LogSizeLimitMB int 476 MaxGameServerCreationsPerBatch int 477 MaxCreationParallelism int 478 MaxGameServerDeletionsPerBatch int 479 MaxDeletionParallelism int 480 MaxPodPendingCount int 481 AllocationBatchWaitTime time.Duration 482 PodNamespace string 483 LeaderElection bool 484 } 485 486 // validate ensures the ctlConfig data is valid. 487 func (c *config) validate() []error { 488 validationErrors := make([]error, 0) 489 portErrors := validatePorts(c.PortRanges) 490 validationErrors = append(validationErrors, portErrors...) 491 resourceErrors := validateResource(c.SidecarCPURequest, c.SidecarCPULimit, corev1.ResourceCPU) 492 validationErrors = append(validationErrors, resourceErrors...) 493 resourceErrors = validateResource(c.SidecarMemoryRequest, c.SidecarMemoryLimit, corev1.ResourceMemory) 494 validationErrors = append(validationErrors, resourceErrors...) 495 return validationErrors 496 } 497 498 // validateResource validates limit or Memory CPU resources used for containers in pods 499 // If a GameServer is invalid there will be > 0 values in 500 // the returned array 501 // 502 // Moved from agones.dev/agones/pkg/apis/agones/v1 (#3255) 503 func validateResource(request resource.Quantity, limit resource.Quantity, resourceName corev1.ResourceName) []error { 504 validationErrors := make([]error, 0) 505 if !limit.IsZero() && request.Cmp(limit) > 0 { 506 validationErrors = append(validationErrors, errors.Errorf("Request must be less than or equal to %s limit", resourceName)) 507 } 508 if request.Cmp(resource.Quantity{}) < 0 { 509 validationErrors = append(validationErrors, errors.Errorf("Resource %s request value must be non negative", resourceName)) 510 } 511 if limit.Cmp(resource.Quantity{}) < 0 { 512 validationErrors = append(validationErrors, errors.Errorf("Resource %s limit value must be non negative", resourceName)) 513 } 514 515 return validationErrors 516 } 517 518 func validatePorts(portRanges map[string]portallocator.PortRange) []error { 519 validationErrors := make([]error, 0) 520 for k, r := range portRanges { 521 portErrors := validatePortRange(r.MinPort, r.MaxPort, k) 522 validationErrors = append(validationErrors, portErrors...) 523 524 } 525 526 if len(validationErrors) > 0 { 527 return validationErrors 528 } 529 530 keys := make([]string, 0, len(portRanges)) 531 values := make([]portallocator.PortRange, 0, len(portRanges)) 532 for k, v := range portRanges { 533 keys = append(keys, k) 534 values = append(values, v) 535 } 536 537 for i, pr := range values { 538 for j := i + 1; j < len(values); j++ { 539 if overlaps(values[j].MinPort, values[j].MaxPort, pr.MinPort, pr.MaxPort) { 540 switch { 541 case keys[j] == agonesv1.DefaultPortRange: 542 validationErrors = append(validationErrors, errors.Errorf("port range %s overlaps with min/max port", keys[i])) 543 case keys[i] == agonesv1.DefaultPortRange: 544 validationErrors = append(validationErrors, errors.Errorf("port range %s overlaps with min/max port", keys[j])) 545 default: 546 validationErrors = append(validationErrors, errors.Errorf("port range %s overlaps with min/max port of range %s", keys[i], keys[j])) 547 } 548 } 549 } 550 } 551 return validationErrors 552 } 553 554 func validatePortRange(minPort, maxPort int32, rangeName string) []error { 555 validationErrors := make([]error, 0) 556 var rangeCtx string 557 if rangeName != agonesv1.DefaultPortRange { 558 rangeCtx = " for port range " + rangeName 559 } 560 if minPort <= 0 || maxPort <= 0 { 561 validationErrors = append(validationErrors, errors.New("min Port and Max Port values are required"+rangeCtx)) 562 } 563 if maxPort < minPort { 564 validationErrors = append(validationErrors, errors.New("max Port cannot be set less that the Min Port"+rangeCtx)) 565 } 566 return validationErrors 567 } 568 569 func overlaps(minA, maxA, minB, maxB int32) bool { 570 return max(minA, minB) < min(maxA, maxB) 571 } 572 573 type runner interface { 574 Run(ctx context.Context, workers int) error 575 } 576 577 func whenLeader(ctx context.Context, cancel context.CancelFunc, logger *logrus.Entry, doLeaderElection bool, kubeClient *kubernetes.Clientset, namespace string, start func(_ context.Context)) { 578 if !doLeaderElection { 579 start(ctx) 580 return 581 } 582 583 id := uuid.New().String() 584 585 lock := &resourcelock.LeaseLock{ 586 LeaseMeta: metav1.ObjectMeta{ 587 Name: "agones-controller-lock", 588 Namespace: namespace, 589 }, 590 Client: kubeClient.CoordinationV1(), 591 LockConfig: resourcelock.ResourceLockConfig{ 592 Identity: id, 593 }, 594 } 595 596 logger.WithField("id", id).Info("Leader Election ID") 597 598 leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ 599 Lock: lock, 600 // IMPORTANT: you MUST ensure that any code you have that 601 // is protected by the lease must terminate **before** 602 // you call cancel. Otherwise, you could have a background 603 // loop still running and another process could 604 // get elected before your background loop finished, violating 605 // the stated goal of the lease. 606 ReleaseOnCancel: true, 607 LeaseDuration: 15 * time.Second, 608 RenewDeadline: 10 * time.Second, 609 RetryPeriod: 2 * time.Second, 610 Callbacks: leaderelection.LeaderCallbacks{ 611 OnStartedLeading: start, 612 OnStoppedLeading: func() { 613 logger.WithField("id", id).Info("Leader Lost") 614 cancel() 615 os.Exit(0) 616 }, 617 OnNewLeader: func(identity string) { 618 if identity == id { 619 return 620 } 621 logger.WithField("id", id).Info("New Leader Elected") 622 }, 623 }, 624 }) 625 }