diff --git a/cmd/initcontainer/main.go b/cmd/initcontainer/main.go index 780389c4..0347555a 100644 --- a/cmd/initcontainer/main.go +++ b/cmd/initcontainer/main.go @@ -115,9 +115,9 @@ func main() { // parseBuildMetadata parses the build metadata from the corresponding environment variable func parseBuildMetadata() map[string]string { buildMetadata := make(map[string]string) - if os.Getenv("PF_GAMESERVER_BUILD_METADATA") != "" { - metadata := os.Getenv("PF_GAMESERVER_BUILD_METADATA") - s := strings.Split(metadata, "?") + envMetadata := os.Getenv("PF_GAMESERVER_BUILD_METADATA") + if envMetadata != "" { + s := strings.Split(envMetadata, "?") for _, s2 := range s { if s2 == "" { continue diff --git a/go.mod b/go.mod index 74a05223..5d0b6d92 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/playfab/thundernetes go 1.18 require ( + github.com/caarlos0/env/v6 v6.9.3 github.com/gin-gonic/gin v1.7.4 github.com/go-logr/logr v1.2.0 github.com/google/uuid v1.2.0 diff --git a/go.sum b/go.sum index d0ef994c..fae77b0c 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/caarlos0/env/v6 v6.9.3 h1:Tyg69hoVXDnpO5Qvpsu8EoquarbPyQb+YwExWHP8wWU= +github.com/caarlos0/env/v6 v6.9.3/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= diff --git a/pkg/operator/config.go b/pkg/operator/config.go new file mode 100644 index 00000000..06ab7d0f --- /dev/null +++ b/pkg/operator/config.go @@ -0,0 +1 @@ +package main diff --git a/pkg/operator/controllers/controller_utils.go b/pkg/operator/controllers/controller_utils.go index 9f9b4196..203b4b67 100644 --- a/pkg/operator/controllers/controller_utils.go +++ b/pkg/operator/controllers/controller_utils.go @@ -3,15 +3,12 @@ package controllers import ( "bytes" "context" - "errors" "fmt" "math/rand" - "os" "strconv" "strings" "time" - "github.com/go-logr/logr" mpsv1alpha1 "github.com/playfab/thundernetes/pkg/operator/api/v1alpha1" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -459,19 +456,3 @@ func getValueByState(gs *mpsv1alpha1.GameServer) int { return 3 } } - -// GetInitContainerImages returns the init container images from the environment variables -func GetInitContainerImages(l logr.Logger) (string, string) { - initContainerImageLinux := os.Getenv("THUNDERNETES_INIT_CONTAINER_IMAGE") - if initContainerImageLinux == "" { - l.Error(errors.New("THUNDERNETES_INIT_CONTAINER_IMAGE is not set, setting to a mock value"), "") - initContainerImageLinux = "testInitContainerImage" - } - initContainerImageWin := os.Getenv("THUNDERNETES_INIT_CONTAINER_IMAGE_WIN") - if initContainerImageWin == "" { - l.Error(errors.New("THUNDERNETES_INIT_CONTAINER_IMAGE_WIN is not set, setting to a mock value"), "") - initContainerImageWin = "testInitContainerImageWin" - } - l.Info("init container images", "linux", initContainerImageLinux, "win", initContainerImageWin) - return initContainerImageLinux, initContainerImageWin -} diff --git a/pkg/operator/controllers/suite_test.go b/pkg/operator/controllers/suite_test.go index e1f2d71d..c79fd6fb 100644 --- a/pkg/operator/controllers/suite_test.go +++ b/pkg/operator/controllers/suite_test.go @@ -91,7 +91,7 @@ var _ = BeforeSuite(func() { err = portRegistry.SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) - initContainerImageLinux, initContainerImageWin := GetInitContainerImages(z) + initContainerImageLinux, initContainerImageWin := "testImageLinux", "testImageWin" Expect(initContainerImageLinux).ToNot(BeEmpty()) Expect(initContainerImageWin).ToNot(BeEmpty()) diff --git a/pkg/operator/main.go b/pkg/operator/main.go index dc8984e1..e793e828 100644 --- a/pkg/operator/main.go +++ b/pkg/operator/main.go @@ -20,12 +20,14 @@ import ( "context" "errors" "flag" + "log" "os" - "strconv" "time" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + "github.com/caarlos0/env/v6" + "github.com/go-logr/logr" _ "go.uber.org/automaxprocs" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" @@ -38,8 +40,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" - "github.com/go-logr/logr" - mpsv1alpha1 "github.com/playfab/thundernetes/pkg/operator/api/v1alpha1" "github.com/playfab/thundernetes/pkg/operator/controllers" @@ -48,17 +48,27 @@ import ( corev1 "k8s.io/api/core/v1" ) +// Config is a struct containing configuration from environment variables +// source: https://github.com/caarlos0/env +type Config struct { + ApiServiceSecurity string `env:"API_SERVICE_SECURITY"` + TlsSecretName string `env:"TLS_SECRET_NAME" envDefault:"tls-secret"` + TlsSecretNamespace string `env:"TLS_SECRET_NAMESPACE" envDefault:"thundernetes-system"` + TlsCertificateName string `env:"TLS_CERTIFICATE_FILENAME" envDefault:"tls.crt"` + TlsPrivateKeyFilename string `env:"TLS_PRIVATE_KEY_FILENAME" envDefault:"tls.key"` + PortRegistryExclusivelyGameServerNodes bool `env:"PORT_REGISTRY_EXCLUSIVELY_GAME_SERVER_NODES" envDefault:"false"` + LogLevel string `env:"LOG_LEVEL" envDefault:"info"` + MinPort int32 `env:"MIN_PORT" envDefault:"10000"` + MaxPort int32 `env:"MAX_PORT" envDefault:"12000"` + InitContainerImageLinux string `env:"THUNDERNETES_INIT_CONTAINER_IMAGE,notEmpty"` + InitContainerImageWin string `env:"THUNDERNETES_INIT_CONTAINER_IMAGE_WIN,notEmpty"` +} + var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) -const ( - secretName = "tls-secret" - certificateFileName = "tls.crt" - privateKeyFileName = "tls.key" -) - func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) @@ -67,6 +77,13 @@ func init() { } func main() { + // load configuration from env variables + cfg := &Config{} + if err := env.Parse(cfg); err != nil { + log.Fatal(err, "Cannot load configuration from environment variables") + } + + // load the rest of the configuration from command-line flags var metricsAddr string var enableLeaderElection bool var probeAddr string @@ -77,17 +94,17 @@ func main() { "Enabling this will ensure there is only one active controller manager.") opts := zap.Options{ Development: true, - Level: getLogLevel(), + Level: getLogLevel(cfg.LogLevel), // https://github.com/uber-go/zap/issues/661#issuecomment-520686037 and https://github.com/uber-go/zap/issues/485#issuecomment-834021392 TimeEncoder: zapcore.TimeEncoderOfLayout(time.RFC3339), } opts.BindFlags(flag.CommandLine) flag.Parse() + // setupLog is valid after this call ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - // get init container images from environment variables - initContainerImageLinux, initContainerImageWin := controllers.GetInitContainerImages(setupLog) + setupLog.Info("Loaded configuration from environment variables", "config", cfg) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, @@ -101,11 +118,10 @@ func main() { setupLog.Error(err, "unable to start manager") os.Exit(1) } - // initialize a live API client, used for the PortRegistry and fetching the mTLS secret k8sClient := mgr.GetAPIReader() - // get public and privage key, if enabled - crt, key := getCrtKeyIfTlsEnabled(k8sClient) + // get public and private key, if enabled + crt, key := getCrtKeyIfTlsEnabled(k8sClient, cfg) // initialize the allocation API service, which is also a controller. So we add it to the manager aas := controllers.NewAllocationApiServer(crt, key, mgr.GetClient()) @@ -115,7 +131,7 @@ func main() { } // initialize the portRegistry - portRegistry, err := initializePortRegistry(k8sClient, mgr.GetClient(), setupLog) + portRegistry, err := initializePortRegistry(k8sClient, mgr.GetClient(), setupLog, cfg) if err != nil { setupLog.Error(err, "unable to initialize portRegistry") os.Exit(1) @@ -133,8 +149,8 @@ func main() { PortRegistry: portRegistry, Recorder: mgr.GetEventRecorderFor("GameServer"), GetNodeDetailsProvider: controllers.GetNodeDetails, - InitContainerImageLinux: initContainerImageLinux, - InitContainerImageWin: initContainerImageWin, + InitContainerImageLinux: cfg.InitContainerImageLinux, + InitContainerImageWin: cfg.InitContainerImageWin, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "GameServer") os.Exit(1) @@ -182,13 +198,13 @@ func main() { // initializePortRegistry performs some initialization and creates a new PortRegistry struct // the k8sClient is a live API client and is used to get the existing gameservers and the "Ready" Nodes // the crClient is the cached controller-runtime client, used to watch for changes to the nodes from inside the PortRegistry -func initializePortRegistry(k8sClient client.Reader, crClient client.Client, setupLog logr.Logger) (*controllers.PortRegistry, error) { +func initializePortRegistry(k8sClient client.Reader, crClient client.Client, setupLog logr.Logger, cfg *Config) (*controllers.PortRegistry, error) { var gameServers mpsv1alpha1.GameServerList if err := k8sClient.List(context.Background(), &gameServers); err != nil { return nil, err } - useExclusivelyGameServerNodesForPortRegistry := useExclusivelyGameServerNodesForPortRegistry() + useExclusivelyGameServerNodesForPortRegistry := cfg.PortRegistryExclusivelyGameServerNodes var nodes corev1.NodeList if err := k8sClient.List(context.Background(), &nodes); err != nil { @@ -206,7 +222,7 @@ func initializePortRegistry(k8sClient client.Reader, crClient client.Client, set // get the min/max port from enviroment variables // the code does not offer any protection in case the port range changes while game servers are running - minPort, maxPort, err := getMinMaxPortFromEnv() + minPort, maxPort, err := validateMinMaxPort(cfg) if err != nil { return nil, err } @@ -223,61 +239,29 @@ func initializePortRegistry(k8sClient client.Reader, crClient client.Client, set // getTlsSecret returns the TLS secret from the given namespace // used in the allocation API service -func getTlsSecret(k8sClient client.Reader, namespace string) ([]byte, []byte, error) { +func getTlsSecret(k8sClient client.Reader, cfg *Config) ([]byte, []byte, error) { var secret corev1.Secret err := k8sClient.Get(context.Background(), types.NamespacedName{ - Name: secretName, - Namespace: namespace, + Name: cfg.TlsSecretName, + Namespace: cfg.TlsSecretNamespace, }, &secret) if err != nil { return nil, nil, err } - return []byte(secret.Data[certificateFileName]), []byte(secret.Data[privateKeyFileName]), nil + return []byte(secret.Data[cfg.TlsCertificateName]), []byte(secret.Data[cfg.TlsPrivateKeyFilename]), nil } -// getMinMaxPortFromEnv returns minimum and maximum port from environment variables -func getMinMaxPortFromEnv() (int32, int32, error) { - minPortStr := os.Getenv("MIN_PORT") - maxPortStr := os.Getenv("MAX_PORT") - - // if both of them are not set, return default values - if minPortStr == "" && maxPortStr == "" { - setupLog.Info("MIN_PORT and MAX_PORT environment variables are not set. Using default values 10000 and 12000.") - return 10000, 12000, nil - } - - if minPortStr == "" { - // this means that MAX_PORT is set, but not MIN_PORT - return 0, 0, errors.New("MIN_PORT env variable is not set") - } - // we use ParseInt insteaf of Atoi because CodeQL triggered this https://codeql.github.com/codeql-query-help/go/go-incorrect-integer-conversion/ - minPortParsed, err := strconv.ParseInt(minPortStr, 10, 32) - if err != nil { - return 0, 0, err - } - - if maxPortStr == "" { - // this means that MIN_PORT is set, but not MAX_PORT - return 0, 0, errors.New("MAX_PORT env variable is not set") - } - maxPortParsed, err := strconv.ParseInt(maxPortStr, 10, 32) - if err != nil { - return 0, 0, err - } - - minPort := int32(minPortParsed) - maxPort := int32(maxPortParsed) - - if minPort >= maxPort { +// validateMinMaxPort validates minimum and maximum ports +func validateMinMaxPort(cfg *Config) (int32, int32, error) { + if cfg.MinPort >= cfg.MaxPort { return 0, 0, errors.New("MIN_PORT cannot be greater or equal than MAX_PORT") } - return minPort, maxPort, nil + return cfg.MinPort, cfg.MaxPort, nil } // getLogLevel returns the log level based on the LOG_LEVEL environment variable -func getLogLevel() zapcore.LevelEnabler { - logLevel := os.Getenv("LOG_LEVEL") +func getLogLevel(logLevel string) zapcore.LevelEnabler { switch logLevel { case "debug": return zapcore.DebugLevel @@ -300,16 +284,9 @@ func getLogLevel() zapcore.LevelEnabler { // for this to happen, user has to set "API_SERVICE_SECURITY" env as "usetls" and set the env "TLS_SECRET_NAMESPACE" with the namespace // that contains the Kubernetes Secret with the cert // if any of the mentioned conditions are not set, method returns nil -func getCrtKeyIfTlsEnabled(c client.Reader) ([]byte, []byte) { - apiServiceSecurity := os.Getenv("API_SERVICE_SECURITY") - - if apiServiceSecurity == "usetls" { - namespace := os.Getenv("TLS_SECRET_NAMESPACE") - if namespace == "" { - setupLog.Error(errors.New("unable to get TLS_SECRET_NAMESPACE env variable"), "mTLS is enabled, but TLS_SECRET_NAMESPACE is not set") - os.Exit(1) - } - crt, key, err := getTlsSecret(c, namespace) +func getCrtKeyIfTlsEnabled(c client.Reader, cfg *Config) ([]byte, []byte) { + if cfg.ApiServiceSecurity == "usetls" { + crt, key, err := getTlsSecret(c, cfg) if err != nil { setupLog.Error(err, "unable to get TLS secret") os.Exit(1) @@ -318,7 +295,3 @@ func getCrtKeyIfTlsEnabled(c client.Reader) ([]byte, []byte) { } return nil, nil } - -func useExclusivelyGameServerNodesForPortRegistry() bool { - return os.Getenv("PORT_REGISTRY_EXCLUSIVELY_GAMESERVER_NODES") == "true" -}