4ccccc6fe2
Self-hostable game server control panel: Go controller + agent, 26 game modules, embedded web UI. One-line install via install.sh / install.ps1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
226 lines
10 KiB
Go
226 lines
10 KiB
Go
// Package runtime defines the Target-side instance execution abstraction.
|
||
// A Runtime is the thing that actually launches and tears down game server
|
||
// processes — either inside a container (DockerRuntime) or directly on the
|
||
// host (HostRuntime — future).
|
||
package runtime
|
||
|
||
import (
|
||
"context"
|
||
"io"
|
||
"time"
|
||
)
|
||
|
||
// InstanceSpec is the resolved, runtime-ready launch recipe for one instance.
|
||
// It is independent of the manifest format: a resolver in agent/internal/module
|
||
// flattens (Manifest, InstanceCreate RPC) into this struct.
|
||
type InstanceSpec struct {
|
||
InstanceID string // panel-side identifier, used as container name
|
||
Image string // e.g. vinanrra/7dtd-server:latest
|
||
Entrypoint []string // override, empty = image default
|
||
Command []string // override, empty = image default
|
||
Env map[string]string // environment variables
|
||
User string // uid:gid or name, empty = image default
|
||
WorkingDir string
|
||
Volumes []VolumeSpec
|
||
Ports []PortSpec
|
||
Resources ResourceLimits
|
||
// RestartPolicy: "no", "on-failure", "unless-stopped", "always"
|
||
RestartPolicy string
|
||
// OpenStdin keeps the container's stdin open so we can later attach and
|
||
// write admin-console commands (stdio RCON adapter). Auto-enabled by
|
||
// the module resolver when rcon.adapter == "stdio".
|
||
OpenStdin bool
|
||
// NetworkMode is the Docker network mode. Empty → bridge (default).
|
||
// "host" shares the host's network namespace; required for modules
|
||
// whose TURN/STUN clients can't survive Docker's UDP NAT rebinds
|
||
// (e.g. Windrose). In host mode, Ports become informational only.
|
||
NetworkMode string
|
||
// BuildContext is an absolute filesystem path containing a Dockerfile
|
||
// that produces this Image. When set, the runtime auto-builds the
|
||
// image on first use if it's missing locally — the AMP-class "drop in
|
||
// an agent and any module just works" behavior. Required for `panel-*`
|
||
// images, since they are never pushed to a registry. Empty for stock
|
||
// public images (alpine, the acekorneya ASA image, etc.) which the
|
||
// runtime pulls normally.
|
||
BuildContext string
|
||
// LogSink optionally receives runtime-side progress lines (image
|
||
// pull / image build / etc.) so the agent can forward them to the
|
||
// controller's log stream. Without this, first-time builds happen
|
||
// silently and the operator sees "installing" with no visible
|
||
// progress for ~30s–3min while the Dockerfile runs.
|
||
LogSink func(line string)
|
||
// SecurityOpts is passed through to Docker's HostConfig.SecurityOpt
|
||
// (one entry per --security-opt CLI flag). The SteamCMD sidecar sets
|
||
// "seccomp=unconfined" so that the 32-bit Steam runtime's socket
|
||
// syscalls aren't ENOSYS'd by Docker's stricter default seccomp
|
||
// profile in newer Docker releases (observed on Docker 29.4 + Ubuntu
|
||
// 24.04 kernel 6.8: "CreateBoundSocket: failed to create socket,
|
||
// error [no name available] (38)"). Same image works fine on
|
||
// Docker 29.1 with the default profile.
|
||
SecurityOpts []string
|
||
// IsSidecar marks this as a transient helper container (fs browse,
|
||
// steamcmd, backup, restore, wipe, warmseed, basemods) rather than a
|
||
// main game-server instance. The runtime stamps a "panel.role" label
|
||
// accordingly so orphan-detection (Rehydrate) can tell main instances
|
||
// apart from helpers without brittle name-suffix matching.
|
||
IsSidecar bool
|
||
}
|
||
|
||
// VolumeSpec is a mount declaration, either a bind mount (host path →
|
||
// container path) or a Docker named volume. The resolver is responsible
|
||
// for substituting $DATA_PATH / $INSTANCE_ID before we see it here.
|
||
type VolumeSpec struct {
|
||
Type string // "bind" (default) or "volume"
|
||
HostPath string // populated when Type == "bind"
|
||
VolumeName string // populated when Type == "volume"
|
||
ContainerPath string
|
||
ReadOnly bool
|
||
}
|
||
|
||
// PortSpec is a port mapping.
|
||
type PortSpec struct {
|
||
HostPort uint16
|
||
ContainerPort uint16
|
||
Proto string // "tcp" or "udp"
|
||
// HostIP — bind address on the Target. Empty = 0.0.0.0.
|
||
HostIP string
|
||
}
|
||
|
||
// ResourceLimits caps CPU / memory for the instance.
|
||
type ResourceLimits struct {
|
||
CPUShares uint32
|
||
MemoryBytes uint64
|
||
PidsLimit int64 // 0 = unlimited
|
||
NanoCPUs int64 // Docker "--cpus" as nano-CPUs (1.5 cpus = 1.5e9)
|
||
StopGrace time.Duration
|
||
}
|
||
|
||
// RuntimeInstanceState is the runtime's view of an instance at a point in
|
||
// time, used by the Agent to rehydrate after restart or detect drift.
|
||
type RuntimeInstanceState struct {
|
||
ContainerID string
|
||
Status string // "running" | "exited" | "created" | "paused" | "restarting" | "removing" | "dead"
|
||
ExitCode int32
|
||
}
|
||
|
||
// LogStream identifies which stream a log line came from.
|
||
type LogStream string
|
||
|
||
const (
|
||
LogStreamStdout LogStream = "stdout"
|
||
LogStreamStderr LogStream = "stderr"
|
||
)
|
||
|
||
// LogHandler receives one log line at a time. Lines are already stripped of
|
||
// trailing newline. Handler must not block; if it does, log pressure will
|
||
// build up in the Runtime.
|
||
type LogHandler func(stream LogStream, line string, at time.Time)
|
||
|
||
// Runtime is the interface that both Docker and Host runtimes implement.
|
||
type Runtime interface {
|
||
// Create pulls the image (if needed) and creates the container.
|
||
// Returns a runtime-specific identifier used by Start/Stop/Remove.
|
||
Create(ctx context.Context, spec InstanceSpec) (id string, err error)
|
||
|
||
// Start starts a previously-created instance.
|
||
Start(ctx context.Context, id string) error
|
||
|
||
// Stop stops a running instance, waiting up to grace before SIGKILL.
|
||
Stop(ctx context.Context, id string, grace time.Duration) error
|
||
|
||
// Restart restarts a running instance, waiting up to grace before
|
||
// SIGKILL. Container ends up running again — equivalent to
|
||
// `docker container restart`. Used by guardrails (e.g. arkHangGuard)
|
||
// that want a bounce without surfacing as an operator stop.
|
||
Restart(ctx context.Context, id string, grace time.Duration) error
|
||
|
||
// Remove deletes the instance (after Stop). Safe to call on stopped.
|
||
Remove(ctx context.Context, id string) error
|
||
|
||
// StreamLogs follows stdout/stderr and calls handler for each line.
|
||
// Returns when ctx is cancelled or the instance exits.
|
||
StreamLogs(ctx context.Context, id string, handler LogHandler) error
|
||
|
||
// Wait blocks until the instance exits and returns its exit code.
|
||
Wait(ctx context.Context, id string) (exitCode int, err error)
|
||
|
||
// StatsStream returns a reader of Docker's stats stream (JSON frames,
|
||
// one per second). Closer cancels the stream. Used by the per-instance
|
||
// stats poller to compute CPU%/mem/net for UI + alerting.
|
||
StatsStream(ctx context.Context, id string) (io.ReadCloser, error)
|
||
|
||
// InspectByName looks up an instance by its runtime-level name
|
||
// (for Docker: "panel-<instance_id>") and returns its current state.
|
||
// Returns an error whose predicate is not-found-able if the instance
|
||
// has been removed from the runtime — used during agent rehydrate
|
||
// to detect stale sidecar metadata.
|
||
InspectByName(ctx context.Context, name string) (RuntimeInstanceState, error)
|
||
|
||
// ContainerExists reports whether a container with the given
|
||
// runtime-level name currently exists. Unlike InspectByName, it
|
||
// distinguishes "definitely gone" from "couldn't tell":
|
||
// exists=false, err=nil → the runtime confirmed no such container
|
||
// exists=false, err!=nil → the query itself failed (daemon down,
|
||
// connection dropped, ctx cancelled) — the
|
||
// container's fate is UNKNOWN, not gone.
|
||
// Callers that gate destructive bookkeeping (e.g. deleting sidecar
|
||
// metadata after a container remove) must treat a non-nil err as
|
||
// "do not assume removal succeeded."
|
||
ContainerExists(ctx context.Context, name string) (exists bool, err error)
|
||
|
||
// ---- Container-path file operations ----
|
||
// These operate inside the running container (or container filesystem
|
||
// for stopped containers where the runtime allows it). Used by the
|
||
// file manager for volume-backed instances where the host-side bind
|
||
// mount is absent or empty.
|
||
|
||
// ExecCapture runs a command inside the container and returns its
|
||
// captured stdout + stderr + exit code. Requires container to be running.
|
||
ExecCapture(ctx context.Context, id string, cmd []string) (stdout, stderr []byte, exitCode int, err error)
|
||
|
||
// CopyFileFromContainer returns the raw contents of a single file at
|
||
// containerPath. Works even on stopped containers.
|
||
CopyFileFromContainer(ctx context.Context, id, containerPath string) ([]byte, error)
|
||
|
||
// CopyFileToContainer writes content to containerPath, creating
|
||
// parent dirs inside the container if the container is running.
|
||
CopyFileToContainer(ctx context.Context, id, containerPath string, content []byte) error
|
||
|
||
// CopyTarToContainer writes a raw tar stream onto the container at
|
||
// dstDir. The tar entries are extracted in place by the Docker API.
|
||
// Used by the file manager's extract path so we can build one big
|
||
// tar from the archive's entries and ship it in a single API call.
|
||
CopyTarToContainer(ctx context.Context, id, dstDir string, tarStream io.Reader) error
|
||
|
||
// CopyDirFromContainer returns a tar stream of the contents at
|
||
// containerPath. For files: a single-entry tar. For directories:
|
||
// one entry per file/dir under it. Used by the compress path so the
|
||
// agent can iterate file contents without per-file round-trips.
|
||
CopyDirFromContainer(ctx context.Context, id, containerPath string) (io.ReadCloser, error)
|
||
|
||
// AttachStdio attaches to the container's stdin/stdout/stderr stream
|
||
// and returns a read/write/close handle. Used by the stdio RCON adapter
|
||
// for games whose admin surface is stdin-only. Requires the container
|
||
// to have been created with OpenStdin=true.
|
||
AttachStdio(ctx context.Context, id string) (io.ReadWriteCloser, error)
|
||
|
||
// ContainerMounts returns the mount points of a container by name.
|
||
// Works on stopped containers (the daemon keeps the spec). Used by the
|
||
// cluster player-save snapshotter to find the host path backing a 7DTD
|
||
// instance's /cluster bind without depending on a path convention.
|
||
ContainerMounts(ctx context.Context, name string) ([]MountInfo, error)
|
||
|
||
// Name returns the runtime kind ("docker", "host") for logging.
|
||
Name() string
|
||
}
|
||
|
||
// MountInfo is one container mount point, host-side. Type is "bind" or
|
||
// "volume"; Source is the host path backing it (for a named volume that's
|
||
// the docker volume's _data dir); Destination is the in-container path.
|
||
type MountInfo struct {
|
||
Type string
|
||
Source string
|
||
Destination string
|
||
Name string
|
||
}
|