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  }