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>
This commit is contained in:
151
docs/reference-repos/icehunter/cmd/dune-admin/control.go
Normal file
151
docs/reference-repos/icehunter/cmd/dune-admin/control.go
Normal file
@@ -0,0 +1,151 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user