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>
152 lines
5.7 KiB
Go
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 &Control{
|
|
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)
|
|
}
|