Files
Vantz Stockwell 651a35d4be
All checks were successful
CI / backend-types (push) Successful in 10s
CI / frontend-build (push) Successful in 15s
CI / agent-tests (push) Successful in 39s
CI / integration (push) Successful in 22s
docs(reference): import Dune: Awakening server-manager references
Phase 2 references for the host-agent Dune adapter, moved out of volatile /tmp
into docs/reference-repos/ (per Commander). Three upstream projects, .git +
node_modules + compiled binaries stripped (16MB source). Nested AI-instruction
files (.claude/, CLAUDE.md) removed so they don't pollute Corrosion sessions.

- icehunter/    dune-admin (Go+React) — 4 control planes; SETUP_DOCKER.md is the
                closest analog to our agent's Dune docker control plane (compose
                lifecycle, docker logs, RabbitMQ-via-exec, dune Postgres schema)
- adainrivers/  Rust/Tauri desktop — SSH+k8s BattleGroup control, maintenance
                daemon, in-game admin console (Rust idiom reference)
- the4rchangel/ Node web UI replacing battlegroup.bat — matches the Commander's
                Hyper-V self-host path + game-config schema

See docs/reference-repos/README.md for the full index + how we use each.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:08:05 -04:00

152 lines
5.7 KiB
Go

package main
import (
"context"
"fmt"
)
// ControlPlane abstracts the server management layer. It determines WHAT
// commands to run (kubectl, docker, local shell) while the Executor determines
// WHERE they run (locally or over SSH).
type ControlPlane interface {
// Name returns the control plane identifier for status reporting.
Name() string
// GetStatus returns the battlegroup status and per-server runtime stats.
GetStatus(ctx context.Context, exec Executor) (*BattlegroupStatus, error)
// ExecCommand runs a lifecycle command: start, stop, restart, update, backup.
ExecCommand(ctx context.Context, exec Executor, cmd string) (string, error)
// ListProcesses returns running processes/pods/containers and a context label.
ListProcesses(ctx context.Context, exec Executor) ([]ProcessInfo, string, error)
// ListLogSources returns available log sources (pods, containers, services).
ListLogSources(ctx context.Context, exec Executor) ([]LogSource, error)
// StreamLog opens a log stream for the named source. The caller must invoke
// cancel when done to release the underlying session/process.
StreamLog(ctx context.Context, exec Executor, ns, name string) (<-chan string, func(), error)
// CaptureJWT extracts the ServiceAuthToken from the game daemon and returns
// a HostId and freshly-signed JWT for broker authentication.
CaptureJWT(ctx context.Context, exec Executor) (hostID, token string, err error)
// EvalOnGameBroker runs an Erlang expression via rabbitmqctl eval inside the
// mq-game broker. Used for publishing server commands with user_id="fls",
// which AMQP connections cannot set (broker validates UserId against auth'd user).
EvalOnGameBroker(ctx context.Context, exec Executor, expr string) (string, error)
// DiscoverIniDir returns the directory containing UserGame.ini and
// UserOverrides.ini. kubectl auto-discovers this from k3s storage;
// docker and local require server_ini_dir to be set in config.
DiscoverIniDir(ctx context.Context, exec Executor) (string, error)
// ReadDefaultINI reads DefaultGame.ini or DefaultEngine.ini from inside the
// game container/pod, where the file lives as part of the image. Returns the
// file contents or "" if unavailable. The local control plane returns "" and
// lets the host-path fallback handle it.
ReadDefaultINI(ctx context.Context, exec Executor, filename string) string
}
// ── Types shared across control plane implementations ─────────────────────────
type BattlegroupStatus struct {
Name string `json:"name"`
Title string `json:"title"`
Phase string `json:"phase"`
Database string `json:"database"`
Servers []ServerRow `json:"servers"`
}
type ServerRow struct {
Map string `json:"map"`
Sietch string `json:"sietch"`
Dimension int `json:"dimension"`
Partition int `json:"partition"`
Phase string `json:"phase"`
Ready bool `json:"ready"`
Players int `json:"players"`
PlayerHardCap int `json:"playerHardCap"`
Queue int `json:"queue"`
// Port is the game-server UDP port parsed from the process args (0 if
// unknown). AgeSeconds is how long the process has been running, sourced
// best-effort from `ps -o etimes=` (0 when unavailable, e.g. non-AMP planes).
Port int `json:"port,omitempty"`
AgeSeconds int `json:"ageSeconds,omitempty"`
}
type ProcessInfo struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Status string `json:"status,omitempty"`
}
type LogSource struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
}
// ── Factory ───────────────────────────────────────────────────────────────────
// newControlPlane returns the appropriate ControlPlane based on the control
// name ("kubectl", "docker", "local", "amp"). Unrecognised names fall back to local.
func newControlPlane(name string, cfg appConfig) ControlPlane {
switch name {
case "kubectl":
return &kubectlControl{namespace: cfg.ControlNamespace}
case "docker":
return &dockerControl{
gameserver: cfg.DockerGameserver,
brokerGame: cfg.DockerBrokerGame,
brokerAdmin: cfg.DockerBrokerAdmin,
}
case "amp":
user := cfg.AmpUser
if user == "" {
user = "amp"
}
container := cfg.AmpContainer
if container == "" && cfg.AmpInstance != "" {
container = "AMP_" + cfg.AmpInstance
}
// Default to container mode (CubeCoders' standard template) unless the
// admin explicitly opts out.
useContainer := true
if cfg.AmpUseContainer != nil {
useContainer = *cfg.AmpUseContainer
}
return &ampControl{
instance: cfg.AmpInstance,
container: container,
ampUser: user,
logPath: cfg.AmpLogPath,
directorURL: cfg.DirectorURL,
iniDir: cfg.ServerIniDir,
useContainer: useContainer,
containerRuntime: cfg.AmpContainerRuntime,
dataRoot: cfg.AmpDataRoot,
apiUser: cfg.AmpAPIUser,
apiPass: cfg.AmpAPIPass,
apiPort: cfg.AmpAPIPort,
pgBin: cfg.AmpPgBin,
pgLib: cfg.AmpPgLib,
}
default:
return &localControl{
cmdStart: cfg.CmdStart,
cmdStop: cfg.CmdStop,
cmdRestart: cfg.CmdRestart,
cmdStatus: cfg.CmdStatus,
controlNamespace: cfg.ControlNamespace,
brokerExecPrefix: cfg.BrokerExecPrefix,
}
}
}
// errNotSupported returns a consistent "not supported" error for control plane
// methods that are not available in a given implementation.
func errNotSupported(control, method string) error {
return fmt.Errorf("%s control plane does not support %s", control, method)
}