panel public release
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDetectHostNetUnsupported_EnvOverrides verifies the explicit env
|
||||
// overrides win over autodetection (the daemon-info path needs a live
|
||||
// daemon and is covered by the integration smoke test, not here).
|
||||
func TestDetectHostNetUnsupported_EnvOverrides(t *testing.T) {
|
||||
t.Setenv("PANEL_FORCE_BRIDGE", "1")
|
||||
if !detectHostNetUnsupported(nil) {
|
||||
t.Fatal("PANEL_FORCE_BRIDGE=1 must force bridge fallback (true)")
|
||||
}
|
||||
os.Unsetenv("PANEL_FORCE_BRIDGE")
|
||||
|
||||
t.Setenv("PANEL_FORCE_HOST_NET", "1")
|
||||
if detectHostNetUnsupported(nil) {
|
||||
t.Fatal("PANEL_FORCE_HOST_NET=1 must force host networking (false)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBridgeFallbackPreservesBindings is the load-bearing check: on a
|
||||
// Docker-Desktop-style daemon, a host-mode module must be converted to
|
||||
// bridge networking WITH its published port bindings intact (so the
|
||||
// panel-allocated ports actually reach players), whereas on native Linux
|
||||
// the same module keeps host mode and drops the meaningless bindings.
|
||||
//
|
||||
// It exercises the exact decision block in Create() without a live daemon
|
||||
// by replicating its inputs.
|
||||
func TestBridgeFallbackPreservesBindings(t *testing.T) {
|
||||
ports := []PortSpec{
|
||||
{Proto: "udp", HostPort: 14507, ContainerPort: 14507},
|
||||
{Proto: "udp", HostPort: 14508, ContainerPort: 14508},
|
||||
}
|
||||
exposed, bindings, err := buildPortBindings(ports)
|
||||
if err != nil {
|
||||
t.Fatalf("buildPortBindings: %v", err)
|
||||
}
|
||||
if len(bindings) != 2 {
|
||||
t.Fatalf("expected 2 published bindings, got %d", len(bindings))
|
||||
}
|
||||
|
||||
// Native Linux (hostNetUnsupported=false): host mode kept, bindings dropped.
|
||||
r := &DockerRuntime{hostNetUnsupported: false}
|
||||
netMode, gotExposed, gotBindings := applyHostNetPolicy(r, "host", exposed, bindings)
|
||||
if netMode != "host" {
|
||||
t.Errorf("linux: netMode = %q, want host", netMode)
|
||||
}
|
||||
if gotBindings != nil || gotExposed != nil {
|
||||
t.Errorf("linux: expected bindings/exposed dropped for host mode")
|
||||
}
|
||||
|
||||
// Docker Desktop (hostNetUnsupported=true): host mode → bridge, bindings kept.
|
||||
r = &DockerRuntime{hostNetUnsupported: true}
|
||||
netMode, gotExposed, gotBindings = applyHostNetPolicy(r, "host", exposed, bindings)
|
||||
if netMode != "" {
|
||||
t.Errorf("docker desktop: netMode = %q, want bridge (empty)", netMode)
|
||||
}
|
||||
if len(gotBindings) != 2 {
|
||||
t.Errorf("docker desktop: expected 2 bindings preserved, got %d", len(gotBindings))
|
||||
}
|
||||
if len(gotExposed) != 2 {
|
||||
t.Errorf("docker desktop: expected 2 exposed ports preserved, got %d", len(gotExposed))
|
||||
}
|
||||
}
|
||||
@@ -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 ~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
|
||||
}
|
||||
Reference in New Issue
Block a user