Files
panel/agent/internal/runtime/spec.go
T
dbledeez 4ccccc6fe2 panel v0.9.2 — public release
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>
2026-07-15 00:43:35 -07:00

226 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 ~30s3min 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
}