panel v0.9.1 — open-source game server manager

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 23:37:25 -07:00
commit 582b5a6b08
2161 changed files with 300950 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
// 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
}