agones.dev/agones@v1.53.0/cmd/sdk-server/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  // sidecar for the game server that the sdk connects to
    16  package main
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"net"
    22  	"net/http"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  	"time"
    27  
    28  	gwruntime "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    29  	"github.com/pkg/errors"
    30  	"github.com/sirupsen/logrus"
    31  	"github.com/spf13/pflag"
    32  	"github.com/spf13/viper"
    33  	"github.com/tmc/grpc-websocket-proxy/wsproxy"
    34  	"google.golang.org/grpc"
    35  	"google.golang.org/grpc/credentials/insecure"
    36  	"k8s.io/client-go/kubernetes"
    37  	"k8s.io/client-go/rest"
    38  
    39  	"agones.dev/agones/pkg"
    40  	"agones.dev/agones/pkg/client/clientset/versioned"
    41  	"agones.dev/agones/pkg/sdk"
    42  	sdkalpha "agones.dev/agones/pkg/sdk/alpha"
    43  	sdkbeta "agones.dev/agones/pkg/sdk/beta"
    44  	"agones.dev/agones/pkg/sdkserver"
    45  	"agones.dev/agones/pkg/util/runtime"
    46  	"agones.dev/agones/pkg/util/signals"
    47  )
    48  
    49  const (
    50  	defaultGRPCPort   = 9357
    51  	defaultHTTPPort   = 9358
    52  	defaultHealthPort = 8080
    53  
    54  	// Flags (that can also be env vars)
    55  	gameServerNameFlag      = "gameserver-name"
    56  	podNamespaceFlag        = "pod-namespace"
    57  	localFlag               = "local"
    58  	fileFlag                = "file"
    59  	testFlag                = "test"
    60  	testSdkNameFlag         = "sdk-name"
    61  	kubeconfigFlag          = "kubeconfig"
    62  	gracefulTerminationFlag = "graceful-termination"
    63  	addressFlag             = "address"
    64  	delayFlag               = "delay"
    65  	timeoutFlag             = "timeout"
    66  	grpcPortFlag            = "grpc-port"
    67  	httpPortFlag            = "http-port"
    68  	healthPortFlag          = "health-port"
    69  	logLevelFlag            = "log-level"
    70  	requestRateLimitFlag    = "request-rate-limit"
    71  )
    72  
    73  var (
    74  	logger = runtime.NewLoggerWithSource("main")
    75  )
    76  
    77  func main() {
    78  	ctlConf := parseEnvFlags()
    79  	logLevel, err := logrus.ParseLevel(ctlConf.LogLevel)
    80  	if err != nil {
    81  		logrus.WithError(err).Warn("Invalid LOG_LEVEL value. Defaulting to 'info'.")
    82  		logLevel = logrus.InfoLevel
    83  	}
    84  	logger.Logger.SetLevel(logLevel)
    85  	logger.WithField("version", pkg.Version).WithField("featureGates", runtime.EncodeFeatures()).
    86  		WithField("ctlConf", ctlConf).Info("Starting sdk sidecar")
    87  
    88  	if ctlConf.Delay > 0 {
    89  		logger.Infof("Waiting %d seconds before starting", ctlConf.Delay)
    90  		time.Sleep(time.Duration(ctlConf.Delay) * time.Second)
    91  	}
    92  
    93  	ctx, _ := signals.NewSigKillContext()
    94  
    95  	grpcServer := grpc.NewServer()
    96  	// don't graceful stop, because if we get a SIGKILL signal
    97  	// then the gameserver is being shut down, and we no longer
    98  	// care about running RPC calls.
    99  	defer grpcServer.Stop()
   100  
   101  	mux := runtime.NewServerMux()
   102  	httpServer := &http.Server{
   103  		Addr:    fmt.Sprintf("%s:%d", ctlConf.Address, ctlConf.HTTPPort),
   104  		Handler: wsproxy.WebsocketProxy(healthCheckWrapper(mux)),
   105  	}
   106  	defer httpServer.Close() // nolint: errcheck
   107  
   108  	switch {
   109  	case ctlConf.IsLocal:
   110  		cancel, err := registerLocal(grpcServer, ctlConf)
   111  		if err != nil {
   112  			logger.WithError(err).Fatal("Could not start local SDK server")
   113  		}
   114  		defer cancel()
   115  
   116  		if ctlConf.Timeout != 0 {
   117  			ctx, cancel = context.WithTimeout(ctx, time.Duration(ctlConf.Timeout)*time.Second)
   118  			defer cancel()
   119  		}
   120  	case ctlConf.Test != "":
   121  		cancel, err := registerTestSdkServer(grpcServer, ctlConf)
   122  		if err != nil {
   123  			logger.WithError(err).Fatal("Could not start test SDK server")
   124  		}
   125  		defer cancel()
   126  
   127  		if ctlConf.Timeout != 0 {
   128  			ctx, cancel = context.WithTimeout(ctx, time.Duration(ctlConf.Timeout)*time.Second)
   129  			defer cancel()
   130  		}
   131  	default:
   132  		var config *rest.Config
   133  		// if the kubeconfig fails InClusterBuildConfig will try in cluster config
   134  		config, err := runtime.InClusterBuildConfig(logger, ctlConf.KubeConfig)
   135  		if err != nil {
   136  			logger.WithError(err).Fatal("Could not create in cluster config")
   137  		}
   138  
   139  		var kubeClient *kubernetes.Clientset
   140  		kubeClient, err = kubernetes.NewForConfig(config)
   141  		if err != nil {
   142  			logger.WithError(err).Fatal("Could not create the kubernetes clientset")
   143  		}
   144  
   145  		var agonesClient *versioned.Clientset
   146  		agonesClient, err = versioned.NewForConfig(config)
   147  		if err != nil {
   148  			logger.WithError(err).Fatalf("Could not create the agones api clientset")
   149  		}
   150  
   151  		var s *sdkserver.SDKServer
   152  		s, err = sdkserver.NewSDKServer(ctlConf.GameServerName, ctlConf.PodNamespace,
   153  			kubeClient, agonesClient, logLevel, ctlConf.HealthPort, ctlConf.RequestsRateLimit)
   154  		if err != nil {
   155  			logger.WithError(err).Fatalf("Could not start sidecar")
   156  		}
   157  		// wait for networking prior to replacing context, otherwise we'll
   158  		// end up waiting the full grace period if it fails.
   159  		if err := s.WaitForConnection(ctx); err != nil {
   160  			logger.WithError(err).Fatalf("Sidecar networking failure")
   161  		}
   162  
   163  		// if sidecar init container, we no longer need to wait for Shutdown as the sdkserver has an independent lifecycle
   164  		// from the main pod containers.
   165  		if ctlConf.GracefulTermination && !runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
   166  			ctx = s.NewSDKServerContext(ctx)
   167  		}
   168  		go func() {
   169  			err := s.Run(ctx)
   170  			if err != nil {
   171  				logger.WithError(err).Fatalf("Could not run sidecar")
   172  			}
   173  		}()
   174  		sdk.RegisterSDKServer(grpcServer, s)
   175  		sdkalpha.RegisterSDKServer(grpcServer, s)
   176  		sdkbeta.RegisterSDKServer(grpcServer, s)
   177  	}
   178  
   179  	grpcEndpoint := fmt.Sprintf("%s:%d", ctlConf.Address, ctlConf.GRPCPort)
   180  	go runGrpc(grpcServer, grpcEndpoint)
   181  	go runGateway(ctx, grpcEndpoint, mux, httpServer)
   182  
   183  	<-ctx.Done()
   184  	logger.Info("Shutting down SDK server")
   185  }
   186  
   187  // registerLocal registers the local SDK servers, and returns a cancel func that
   188  // closes all the SDK implementations
   189  func registerLocal(grpcServer *grpc.Server, ctlConf config) (func(), error) {
   190  	filePath := ""
   191  	if ctlConf.LocalFile != "" {
   192  		var err error
   193  		filePath, err = filepath.Abs(ctlConf.LocalFile)
   194  		if err != nil {
   195  			return nil, err
   196  		}
   197  
   198  		if _, err = os.Stat(filePath); os.IsNotExist(err) {
   199  			return nil, errors.Errorf("Could not find file: %s", filePath)
   200  		}
   201  	}
   202  
   203  	s, err := sdkserver.NewLocalSDKServer(filePath, ctlConf.TestSdkName)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	sdk.RegisterSDKServer(grpcServer, s)
   209  	sdkalpha.RegisterSDKServer(grpcServer, s)
   210  	sdkbeta.RegisterSDKServer(grpcServer, s)
   211  
   212  	return func() {
   213  		s.Close()
   214  	}, err
   215  }
   216  
   217  // registerLocal registers the local test SDK servers, and returns a cancel func that
   218  // closes all the SDK implementations
   219  func registerTestSdkServer(grpcServer *grpc.Server, ctlConf config) (func(), error) {
   220  	s, err := sdkserver.NewLocalSDKServer("", "")
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	s.SetTestMode(true)
   226  	s.GenerateUID()
   227  	expectedFuncs := strings.Split(ctlConf.Test, ",")
   228  	s.SetExpectedSequence(expectedFuncs)
   229  	s.SetSdkName(ctlConf.TestSdkName)
   230  
   231  	sdk.RegisterSDKServer(grpcServer, s)
   232  	sdkalpha.RegisterSDKServer(grpcServer, s)
   233  	sdkbeta.RegisterSDKServer(grpcServer, s)
   234  	return func() {
   235  		s.Close()
   236  	}, err
   237  }
   238  
   239  // runGrpc runs the grpc service
   240  func runGrpc(grpcServer *grpc.Server, grpcEndpoint string) {
   241  	lis, err := net.Listen("tcp", grpcEndpoint)
   242  	if err != nil {
   243  		logger.WithField("grpcEndpoint", grpcEndpoint).Fatal("Could not listen on grpc endpoint")
   244  	}
   245  
   246  	logger.WithField("grpcEndpoint", grpcEndpoint).Info("Starting SDKServer grpc service...")
   247  	if err := grpcServer.Serve(lis); err != nil {
   248  		logger.WithError(err).Fatal("Could not serve grpc server")
   249  	}
   250  }
   251  
   252  // runGateway runs the grpc-gateway
   253  func runGateway(ctx context.Context, grpcEndpoint string, mux *gwruntime.ServeMux, httpServer *http.Server) {
   254  	// nolint: staticcheck
   255  	conn, err := grpc.NewClient(grpcEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
   256  	if err != nil {
   257  		logger.WithError(err).Fatal("Could not dial grpc server...")
   258  	}
   259  
   260  	if err := sdk.RegisterSDKHandler(ctx, mux, conn); err != nil {
   261  		logger.WithError(err).Fatal("Could not register sdk grpc-gateway")
   262  	}
   263  
   264  	if err := sdkalpha.RegisterSDKHandler(ctx, mux, conn); err != nil {
   265  		logger.WithError(err).Fatal("Could not register alpha sdk grpc-gateway")
   266  	}
   267  
   268  	if err := sdkbeta.RegisterSDKHandler(ctx, mux, conn); err != nil {
   269  		logger.WithError(err).Fatal("Could not register beta sdk grpc-gateway")
   270  	}
   271  
   272  	logger.WithField("httpEndpoint", httpServer.Addr).Info("Starting SDKServer grpc-gateway...")
   273  	if err := httpServer.ListenAndServe(); err != nil {
   274  		if err == http.ErrServerClosed {
   275  			logger.WithError(err).Info("http server closed")
   276  		} else {
   277  			logger.WithError(err).Fatal("Could not serve http server")
   278  		}
   279  	}
   280  }
   281  
   282  // parseEnvFlags parses all the flags and environment variables and returns
   283  // a configuration structure
   284  func parseEnvFlags() config {
   285  	viper.AllowEmptyEnv(true)
   286  	viper.SetDefault(localFlag, false)
   287  	viper.SetDefault(fileFlag, "")
   288  	viper.SetDefault(testFlag, "")
   289  	viper.SetDefault(testSdkNameFlag, "")
   290  	viper.SetDefault(addressFlag, "localhost")
   291  	viper.SetDefault(delayFlag, 0)
   292  	viper.SetDefault(timeoutFlag, 0)
   293  	viper.SetDefault(gracefulTerminationFlag, true)
   294  	viper.SetDefault(grpcPortFlag, defaultGRPCPort)
   295  	viper.SetDefault(httpPortFlag, defaultHTTPPort)
   296  	viper.SetDefault(healthPortFlag, defaultHealthPort)
   297  	viper.SetDefault(logLevelFlag, "Info")
   298  	viper.SetDefault(requestRateLimitFlag, "500ms")
   299  	pflag.String(gameServerNameFlag, viper.GetString(gameServerNameFlag),
   300  		"Optional flag to set GameServer name. Overrides value given from `GAMESERVER_NAME` environment variable.")
   301  	pflag.String(podNamespaceFlag, viper.GetString(gameServerNameFlag),
   302  		"Optional flag to set Kubernetes namespace which the GameServer/pod is in. Overrides value given from `POD_NAMESPACE` environment variable.")
   303  	pflag.Bool(localFlag, viper.GetBool(localFlag),
   304  		"Set this, or LOCAL env, to 'true' to run this binary in local development mode. Defaults to 'false'")
   305  	pflag.StringP(fileFlag, "f", viper.GetString(fileFlag), "Set this, or FILE env var to the path of a local yaml or json file that contains your GameServer resoure configuration")
   306  	pflag.String(addressFlag, viper.GetString(addressFlag), "The Address to bind the server grpcPort to. Defaults to 'localhost'")
   307  	pflag.Int(grpcPortFlag, viper.GetInt(grpcPortFlag), fmt.Sprintf("Port on which to bind the gRPC server. Defaults to %d", defaultGRPCPort))
   308  	pflag.Int(httpPortFlag, viper.GetInt(httpPortFlag), fmt.Sprintf("Port on which to bind the HTTP server. Defaults to %d", defaultHTTPPort))
   309  	pflag.Int(healthPortFlag, viper.GetInt(healthPortFlag), fmt.Sprintf("Port on which to bind the healthcheck port on the HTTP server. Defaults to %d", defaultHealthPort))
   310  	pflag.Int(delayFlag, viper.GetInt(delayFlag), "Time to delay (in seconds) before starting to execute main. Useful for tests")
   311  	pflag.Int(timeoutFlag, viper.GetInt(timeoutFlag), "Time of execution (in seconds) before close. Useful for tests")
   312  	pflag.String(testFlag, viper.GetString(testFlag), "List functions which should be called during the SDK Conformance test run.")
   313  	pflag.String(testSdkNameFlag, viper.GetString(testSdkNameFlag), "SDK name which is tested by this SDK Conformance test.")
   314  	pflag.String(kubeconfigFlag, viper.GetString(kubeconfigFlag),
   315  		"Optional. kubeconfig to run the SDK server out of the cluster.")
   316  	pflag.Bool(gracefulTerminationFlag, viper.GetBool(gracefulTerminationFlag),
   317  		"When false, immediately quits when receiving interrupt instead of waiting for GameServer state to progress to \"Shutdown\".")
   318  	pflag.String(requestRateLimitFlag, viper.GetString(requestRateLimitFlag), "Time to delay between requests to the API server. Defaults to 500ms.")
   319  	runtime.FeaturesBindFlags()
   320  	pflag.Parse()
   321  
   322  	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
   323  	runtime.Must(viper.BindEnv(gameServerNameFlag))
   324  	runtime.Must(viper.BindEnv(podNamespaceFlag))
   325  	runtime.Must(viper.BindEnv(localFlag))
   326  	runtime.Must(viper.BindEnv(fileFlag))
   327  	runtime.Must(viper.BindEnv(addressFlag))
   328  	runtime.Must(viper.BindEnv(testFlag))
   329  	runtime.Must(viper.BindEnv(testSdkNameFlag))
   330  	runtime.Must(viper.BindEnv(kubeconfigFlag))
   331  	runtime.Must(viper.BindEnv(delayFlag))
   332  	runtime.Must(viper.BindEnv(timeoutFlag))
   333  	runtime.Must(viper.BindEnv(grpcPortFlag))
   334  	runtime.Must(viper.BindEnv(httpPortFlag))
   335  	runtime.Must(viper.BindEnv(healthPortFlag))
   336  	runtime.Must(viper.BindPFlags(pflag.CommandLine))
   337  	runtime.Must(viper.BindEnv(logLevelFlag))
   338  	runtime.Must(viper.BindEnv(requestRateLimitFlag))
   339  	runtime.Must(runtime.FeaturesBindEnv())
   340  	runtime.Must(runtime.ParseFeaturesFromEnv())
   341  
   342  	return config{
   343  		GameServerName:      viper.GetString(gameServerNameFlag),
   344  		PodNamespace:        viper.GetString(podNamespaceFlag),
   345  		IsLocal:             viper.GetBool(localFlag),
   346  		Address:             viper.GetString(addressFlag),
   347  		LocalFile:           viper.GetString(fileFlag),
   348  		Delay:               viper.GetInt(delayFlag),
   349  		Timeout:             viper.GetInt(timeoutFlag),
   350  		Test:                viper.GetString(testFlag),
   351  		TestSdkName:         viper.GetString(testSdkNameFlag),
   352  		KubeConfig:          viper.GetString(kubeconfigFlag),
   353  		GracefulTermination: viper.GetBool(gracefulTerminationFlag),
   354  		GRPCPort:            viper.GetInt(grpcPortFlag),
   355  		HTTPPort:            viper.GetInt(httpPortFlag),
   356  		HealthPort:          viper.GetInt(healthPortFlag),
   357  		LogLevel:            viper.GetString(logLevelFlag),
   358  		RequestsRateLimit:   viper.GetDuration(requestRateLimitFlag),
   359  	}
   360  }
   361  
   362  // config is all the configuration for this program
   363  type config struct {
   364  	GameServerName      string
   365  	PodNamespace        string
   366  	Address             string
   367  	IsLocal             bool
   368  	LocalFile           string
   369  	Delay               int
   370  	Timeout             int
   371  	Test                string
   372  	TestSdkName         string
   373  	KubeConfig          string
   374  	GracefulTermination bool
   375  	GRPCPort            int
   376  	HTTPPort            int
   377  	HealthPort          int
   378  	LogLevel            string
   379  	RequestsRateLimit   time.Duration
   380  }
   381  
   382  // healthCheckWrapper ensures that an http 400 response is returned
   383  // if the healthcheck receives a request with an empty post body
   384  func healthCheckWrapper(h http.Handler) http.Handler {
   385  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   386  		if r.URL.Path == "/health" && r.Body == http.NoBody {
   387  			w.WriteHeader(http.StatusBadRequest)
   388  			return
   389  		}
   390  
   391  		h.ServeHTTP(w, r)
   392  	})
   393  }