// 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-") 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 }