mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
0bad9e07db
Add opt-in control-plane registration to server/supervisor: once Agones
Ready succeeds, POST /v1/servers/{id}/register (assignment_ready=false)
using a workload token read fresh from disk each call -- matching how a
Kubernetes projected service account token is rotated in place by
kubelet, unlike a cached/env-var secret. ControlPlaneURL empty (the
default) is a total no-op, so direct/Compose mode and allocated-without-
control-plane mode are both byte-for-byte unaffected; New() rejects a
half-configured registration (URL set without token path/server/match/
digest) rather than silently skipping it.
ServerID/MatchID/ImageDigest are read from env vars named by CLI flags
(--server-id-env, --match-id-env, --image-digest-env), matching the
existing --drain-token-env convention in this same binary, rather than
parsed out of the Agones SDK's own GameServer JSON -- that shape isn't
independently verifiable from here, whereas the Kubernetes Downward API
(fieldRef: metadata.name) populating an env var is a standard, safe
pattern already used elsewhere in this codebase for exactly this class
of secret.
A registration failure now kills the child (matching the existing
waitReady failure path) rather than leaving Agones-Ready-but-
control-plane-unregistered process running -- a real gap the second new
test (TestControlPlaneRegistrationFailureKillsChildRatherThanRunningUnregistered)
had to be corrected to actually exercise: its first draft omitted
ReadyURL and was failing at waitReady, before ever reaching the code
path it claimed to test.
85 lines
3.2 KiB
Go
85 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/cosmic-clash/cosmic-clash/server/supervisor"
|
|
)
|
|
|
|
const usageText = `Usage: game-server-supervisor [options] -- <game-server-command> [args...]
|
|
|
|
The child command is started only after an allocated Agones endpoint has been
|
|
validated and, when configured, an explicit process-ready probe succeeds.
|
|
SIGTERM/SIGINT requests authenticated drain before the bounded grace deadline.
|
|
`
|
|
|
|
func main() {
|
|
args := os.Args[1:]
|
|
separator := -1
|
|
for i, arg := range args {
|
|
if arg == "--" {
|
|
separator = i
|
|
break
|
|
}
|
|
}
|
|
if separator < 0 || separator == len(args)-1 {
|
|
fmt.Fprint(os.Stderr, usageText)
|
|
os.Exit(2)
|
|
}
|
|
|
|
options := flag.NewFlagSet("game-server-supervisor", flag.ContinueOnError)
|
|
options.SetOutput(os.Stderr)
|
|
sdkBaseURL := options.String("sdk-base-url", "", "Agones SDK REST base URL; empty enables direct mode")
|
|
readyURL := options.String("ready-url", "", "explicit process-ready probe URL")
|
|
drainURL := options.String("drain-url", "", "loopback drain URL")
|
|
drainTokenEnv := options.String("drain-token-env", "COSMIC_CLASH_DRAIN_TOKEN", "environment variable containing the drain bearer token")
|
|
transport := options.String("transport", "enet", "enet or steam_sdr")
|
|
grace := options.Duration("drain-grace", supervisor.DefaultDrainGrace, "maximum graceful drain duration")
|
|
controlPlaneURL := options.String("control-plane-url", "", "matchmaking control-plane base URL; empty skips process-ready registration entirely")
|
|
workloadTokenPath := options.String("workload-token-path", "", "path to the projected workload service-account token, read fresh on every registration call")
|
|
serverIDEnv := options.String("server-id-env", "COSMIC_CLASH_SERVER_ID", "environment variable containing this GameServer's control-plane server ID (populate via the Kubernetes Downward API, fieldRef: metadata.name)")
|
|
matchIDEnv := options.String("match-id-env", "COSMIC_CLASH_MATCH_ID", "environment variable containing the allocated match ID")
|
|
protocolVersion := options.Int("protocol-version", 0, "protocol version reported at registration")
|
|
imageDigestEnv := options.String("image-digest-env", "COSMIC_CLASH_IMAGE_DIGEST", "environment variable containing this build's sha256 image digest")
|
|
if err := options.Parse(args[:separator]); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
|
|
token := ""
|
|
if *drainTokenEnv != "" {
|
|
token = os.Getenv(*drainTokenEnv)
|
|
}
|
|
s, err := supervisor.New(supervisor.Config{
|
|
Command: args[separator+1:],
|
|
SDKBaseURL: *sdkBaseURL,
|
|
ReadyURL: *readyURL,
|
|
DrainURL: *drainURL,
|
|
DrainToken: token,
|
|
Transport: *transport,
|
|
ReadyTimeout: 30 * time.Second,
|
|
|
|
ControlPlaneURL: *controlPlaneURL,
|
|
WorkloadTokenPath: *workloadTokenPath,
|
|
ServerID: os.Getenv(*serverIDEnv),
|
|
MatchID: os.Getenv(*matchIDEnv),
|
|
ProtocolVersion: *protocolVersion,
|
|
ImageDigest: os.Getenv(*imageDigestEnv),
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "game-server-supervisor: %v\n", err)
|
|
os.Exit(2)
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
if err := s.Run(ctx, *grace); err != nil {
|
|
fmt.Fprintf(os.Stderr, "game-server-supervisor: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|