Panel — public source drop (v0.9.0)
Self-hostable game-server control panel: controller + agent + 26 game modules. One-line install (prebuilt release, no Go required): curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
// Package module (agent-side) resolves a manifest + InstanceCreate RPC
|
||||
// into a runtime.InstanceSpec ready for the Docker / host runtime to launch.
|
||||
package module
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dbledeez/panel/agent/internal/runtime"
|
||||
modulepkg "github.com/dbledeez/panel/pkg/module"
|
||||
panelv1 "github.com/dbledeez/panel/proto/panel/v1"
|
||||
)
|
||||
|
||||
// ResolveDocker produces an InstanceSpec for Docker-mode launch of the given
|
||||
// module manifest and RPC create request. It substitutes $DATA_PATH in volume
|
||||
// targets and maps declared ports against the request's port overrides.
|
||||
func ResolveDocker(manifest *modulepkg.Manifest, req *panelv1.InstanceCreate) (runtime.InstanceSpec, error) {
|
||||
if manifest == nil {
|
||||
return runtime.InstanceSpec{}, errors.New("manifest is nil")
|
||||
}
|
||||
if manifest.Runtime.Docker == nil {
|
||||
return runtime.InstanceSpec{}, fmt.Errorf("module %q has no docker runtime", manifest.ID)
|
||||
}
|
||||
if req.DataPath == "" {
|
||||
return runtime.InstanceSpec{}, errors.New("req.data_path is required")
|
||||
}
|
||||
|
||||
docker := manifest.Runtime.Docker
|
||||
|
||||
env := map[string]string{}
|
||||
for k, v := range docker.Env {
|
||||
env[k] = expand(v, req.DataPath)
|
||||
}
|
||||
// Per-instance config_values layer: if the operator (or controller
|
||||
// feature code) set a value for an env key the module already
|
||||
// declares, use that value instead of the manifest default. Only
|
||||
// keys that ALREADY exist in docker.Env get overridden — we don't
|
||||
// let config_values smuggle in unrelated env vars. Used today by
|
||||
// the ARK cluster feature to override CLUSTER_ID per-instance.
|
||||
for k, v := range req.ConfigValues {
|
||||
if _, ok := env[k]; ok {
|
||||
env[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
volumes := make([]runtime.VolumeSpec, 0, len(docker.Volumes))
|
||||
for _, v := range docker.Volumes {
|
||||
vs := runtime.VolumeSpec{
|
||||
ContainerPath: v.Container,
|
||||
ReadOnly: v.ReadOnly,
|
||||
}
|
||||
kind := v.Type
|
||||
if kind == "" {
|
||||
// Infer from which field was populated.
|
||||
if v.Name != "" {
|
||||
kind = "volume"
|
||||
} else {
|
||||
kind = "bind"
|
||||
}
|
||||
}
|
||||
switch kind {
|
||||
case "volume":
|
||||
vs.Type = "volume"
|
||||
vs.VolumeName = expandInstance(v.Name, req.InstanceId)
|
||||
default:
|
||||
vs.Type = "bind"
|
||||
vs.HostPath = expand(v.Target, req.DataPath)
|
||||
}
|
||||
// Apply per-instance mount override if the controller asked for
|
||||
// one by container path. An absolute path (Unix `/...` or
|
||||
// Windows `C:\...`) is a bind mount host path; anything else is
|
||||
// a Docker named volume. Used by the ARK cluster feature to
|
||||
// swap the default per-instance cluster volume for a shared
|
||||
// host-visible `arkcluster/<id>/` bind mount. `$AGENT_DATA_ROOT`
|
||||
// has already been expanded by handleCreate.
|
||||
if ov, ok := req.MountOverrides[v.Container]; ok && ov != "" {
|
||||
if filepath.IsAbs(ov) {
|
||||
vs.Type = "bind"
|
||||
vs.HostPath = ov
|
||||
vs.VolumeName = ""
|
||||
} else {
|
||||
vs.Type = "volume"
|
||||
vs.VolumeName = ov
|
||||
vs.HostPath = ""
|
||||
}
|
||||
}
|
||||
volumes = append(volumes, vs)
|
||||
}
|
||||
|
||||
portOverrides := indexPortOverrides(req.Ports)
|
||||
ports := make([]runtime.PortSpec, 0, len(manifest.Ports))
|
||||
for _, p := range manifest.Ports {
|
||||
containerPort := uint16(p.Default)
|
||||
hostPort := containerPort
|
||||
if ov, ok := portOverrides[p.Name]; ok {
|
||||
if ov.ContainerPort != 0 {
|
||||
containerPort = uint16(ov.ContainerPort)
|
||||
}
|
||||
if ov.HostPort != 0 {
|
||||
hostPort = uint16(ov.HostPort)
|
||||
}
|
||||
}
|
||||
// Internal ports (e.g. 7DTD's telnet RCON on 8081) must still be
|
||||
// reachable from the Target agent — it runs on the same host — so we
|
||||
// bind them to 127.0.0.1 instead of 0.0.0.0. Public ports bind to
|
||||
// all interfaces (HostIP left empty).
|
||||
hostIP := ""
|
||||
if p.Internal {
|
||||
hostIP = "127.0.0.1"
|
||||
}
|
||||
ports = append(ports, runtime.PortSpec{
|
||||
ContainerPort: containerPort,
|
||||
HostPort: hostPort,
|
||||
Proto: strings.ToLower(p.Proto),
|
||||
HostIP: hostIP,
|
||||
})
|
||||
}
|
||||
|
||||
// Preserve nil when the manifest doesn't override — passing []string{} to
|
||||
// Docker ContainerCreate replaces the image's default entrypoint/cmd with
|
||||
// empty, which errors out as "no command specified".
|
||||
var entrypoint, command []string
|
||||
if len(docker.Entrypoint) > 0 {
|
||||
entrypoint = append([]string{}, docker.Entrypoint...)
|
||||
}
|
||||
if len(docker.Command) > 0 {
|
||||
command = append([]string{}, docker.Command...)
|
||||
}
|
||||
|
||||
spec := runtime.InstanceSpec{
|
||||
InstanceID: req.InstanceId,
|
||||
Image: docker.Image,
|
||||
Entrypoint: entrypoint,
|
||||
Command: command,
|
||||
Env: env,
|
||||
User: docker.User,
|
||||
Volumes: volumes,
|
||||
Ports: ports,
|
||||
NetworkMode: docker.NetworkMode,
|
||||
BuildContext: manifest.Dir, // module's directory holds the Dockerfile
|
||||
}
|
||||
// Auto-enable OpenStdin whenever the module's RCON adapter is stdio —
|
||||
// it's the one feature the adapter needs from the runtime, and making
|
||||
// it implicit saves a second knob every stdio-based module would have
|
||||
// to remember to set.
|
||||
if manifest.RCON != nil && manifest.RCON.Adapter == "stdio" {
|
||||
spec.OpenStdin = true
|
||||
}
|
||||
if lim := req.Limits; lim != nil {
|
||||
spec.Resources = runtime.ResourceLimits{
|
||||
CPUShares: lim.CpuShares,
|
||||
MemoryBytes: lim.MemBytes,
|
||||
}
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
|
||||
// expand substitutes $DATA_PATH in the given string.
|
||||
func expand(s, dataPath string) string {
|
||||
return strings.ReplaceAll(s, "$DATA_PATH", dataPath)
|
||||
}
|
||||
|
||||
// expandInstance substitutes $INSTANCE_ID in the given string. Used for
|
||||
// named-volume templating so each instance gets its own volume.
|
||||
func expandInstance(s, instanceID string) string {
|
||||
return strings.ReplaceAll(s, "$INSTANCE_ID", instanceID)
|
||||
}
|
||||
|
||||
// ResolveVolumes translates manifest volumes into runtime.VolumeSpec form
|
||||
// for a specific instance. Exposed so sidecar launchers (SteamCMD updater,
|
||||
// future backup-runner, etc.) can mount the same volumes the main
|
||||
// container uses, putting files where the main container expects them.
|
||||
func ResolveVolumes(manifest *modulepkg.Manifest, instanceID, dataPath string) []runtime.VolumeSpec {
|
||||
if manifest == nil || manifest.Runtime.Docker == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]runtime.VolumeSpec, 0, len(manifest.Runtime.Docker.Volumes))
|
||||
for _, v := range manifest.Runtime.Docker.Volumes {
|
||||
vs := runtime.VolumeSpec{
|
||||
ContainerPath: v.Container,
|
||||
ReadOnly: v.ReadOnly,
|
||||
}
|
||||
kind := v.Type
|
||||
if kind == "" {
|
||||
if v.Name != "" {
|
||||
kind = "volume"
|
||||
} else {
|
||||
kind = "bind"
|
||||
}
|
||||
}
|
||||
switch kind {
|
||||
case "volume":
|
||||
vs.Type = "volume"
|
||||
vs.VolumeName = expandInstance(v.Name, instanceID)
|
||||
default:
|
||||
vs.Type = "bind"
|
||||
vs.HostPath = expand(v.Target, dataPath)
|
||||
}
|
||||
out = append(out, vs)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// indexPortOverrides builds a by-name lookup of PortMap entries from the RPC.
|
||||
func indexPortOverrides(ports []*panelv1.PortMap) map[string]*panelv1.PortMap {
|
||||
out := map[string]*panelv1.PortMap{}
|
||||
for _, p := range ports {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
out[p.Name] = p
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user