agones.dev/agones@v1.54.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 }