Routes Java Edition connections by the hostname in the handshake, with wildcard domains, backend failover, PROXY protocol v2, cached server-list status from health probes, hot-reloaded config, and an admin page and metrics endpoint. Includes CI, tagged releases and a container image. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e640c5bf02
commit
b53af0e196
29 files changed
+2640
No files matched your search
@@ -0,0 +1,114 @@
|
||||
// Command spinner routes Minecraft Java connections to backend servers by hostname.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.veillax.com/Veillax/reef/dotenv"
|
||||
"git.veillax.com/Veillax/spinner/internal/admin"
|
||||
"git.veillax.com/Veillax/spinner/internal/config"
|
||||
"git.veillax.com/Veillax/spinner/internal/health"
|
||||
"git.veillax.com/Veillax/spinner/internal/proxy"
|
||||
)
|
||||
|
||||
// version is set at build time (deploy/build.sh).
|
||||
var version = "dev"
|
||||
|
||||
const starterConfig = `{
|
||||
"bind": ":25565",
|
||||
"admin_bind": ":8080",
|
||||
"servers": []
|
||||
}
|
||||
`
|
||||
|
||||
func main() {
|
||||
envFile := dotenv.ArgPath(os.Args[1:], ".env")
|
||||
if _, err := dotenv.Load(envFile); err != nil {
|
||||
slog.Error("reading env file", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfgPath := flag.String("config", envOr("SPINNER_CONFIG", "spinner.json"), "config file")
|
||||
flag.String("env", ".env", "env file with SPINNER_ADMIN_TOKEN")
|
||||
debug := flag.Bool("debug", false, "verbose logging")
|
||||
showVersion := flag.Bool("version", false, "print the version and exit")
|
||||
flag.Parse()
|
||||
if *showVersion {
|
||||
os.Stdout.WriteString("spinner " + version + "\n")
|
||||
return
|
||||
}
|
||||
|
||||
level := slog.LevelInfo
|
||||
if *debug {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level}))
|
||||
|
||||
// First run, for example in a fresh container volume: write a starter config so the admin page can fill it in.
|
||||
if _, err := os.Stat(*cfgPath); errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.WriteFile(*cfgPath, []byte(starterConfig), 0o644); err != nil {
|
||||
log.Error("writing starter config", "path", *cfgPath, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info("wrote a starter config", "path", *cfgPath)
|
||||
}
|
||||
|
||||
mgr, err := config.NewManager(*cfgPath, log)
|
||||
if err != nil {
|
||||
log.Error("loading config", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
cfg := mgr.Current()
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
tracker := health.New(log)
|
||||
p := proxy.New(mgr, tracker, log)
|
||||
go mgr.Watch(ctx)
|
||||
go tracker.Run(ctx, mgr)
|
||||
|
||||
if cfg.AdminBind != "" {
|
||||
token := os.Getenv("SPINNER_ADMIN_TOKEN")
|
||||
if token == "" {
|
||||
log.Warn("admin_bind is set but SPINNER_ADMIN_TOKEN is empty; admin UI disabled")
|
||||
} else {
|
||||
a := &admin.Admin{Cfg: mgr, Health: tracker, Proxy: p, Token: token}
|
||||
srv := &http.Server{Addr: cfg.AdminBind, Handler: a.Handler(), ReadHeaderTimeout: 5 * time.Second}
|
||||
go func() {
|
||||
log.Info("admin listening", "addr", cfg.AdminBind)
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Error("admin server", "err", err)
|
||||
}
|
||||
}()
|
||||
go func() { <-ctx.Done(); srv.Close() }()
|
||||
}
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", cfg.Bind)
|
||||
if err != nil {
|
||||
log.Error("listen", "addr", cfg.Bind, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info("spinner listening", "addr", cfg.Bind, "servers", len(cfg.Servers))
|
||||
if err := p.Serve(ctx, ln); err != nil {
|
||||
log.Error("serve", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Info("shutting down")
|
||||
}
|
||||
|
||||
func envOr(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
Reference in new issue
Block a user