panel v0.9.2 — open-source game server manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/dbledeez/panel/agent/internal/runtime"
|
||||
)
|
||||
|
||||
func medicEnvOr(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// Medic host paths on the agent box (figaro, for 7DTD). Overridable via env so
|
||||
// the same agent binary works if 7DTD ever moves hosts.
|
||||
func regionRollingRoot() string {
|
||||
return medicEnvOr("PANEL_REGION_ROLLING_DIR", "/home/refuge/region-rolling")
|
||||
}
|
||||
func regionMedicBin() string {
|
||||
return medicEnvOr("PANEL_REGION_MEDIC_BIN", "/home/refuge/panel/bin/region-medic")
|
||||
}
|
||||
func discordTokenFile() string {
|
||||
return medicEnvOr("PANEL_DISCORD_TOKEN_FILE", "/home/refuge/panel/discord-bot-token")
|
||||
}
|
||||
|
||||
func medicFileExists(p string) bool {
|
||||
st, err := os.Stat(p)
|
||||
return err == nil && !st.IsDir()
|
||||
}
|
||||
|
||||
// runRegionMedicOnStart runs the autoheal CLI in a throwaway debian sidecar while
|
||||
// the 7DTD container is STILL STOPPED, just before the agent starts it:
|
||||
// - validate every .7rg in the active world's Region dir,
|
||||
// - heal corrupt ones from the newest clean rolling snapshot, then the panel
|
||||
// tar backups as a fallback,
|
||||
// - snapshot the now-clean Region dir to a fresh rolling slot,
|
||||
// - post a Discord summary (naming the server) if anything healed.
|
||||
//
|
||||
// The per-server medic config (enable / rolling-keep / Discord channel) lives in
|
||||
// the SAVES volume at region-medic.json — written by the panel's medic-config
|
||||
// endpoint via FsWrite (NO container recreate) and read by the CLI here in the
|
||||
// sidecar. The agent therefore always launches the sidecar for 7DTD and lets the
|
||||
// CLI decide; an absent/empty config just means defaults + no Discord.
|
||||
//
|
||||
// Best-effort by contract: any failure is logged + swallowed, never blocking the
|
||||
// start. 10-min hard cap so a wedged sidecar/daemon can't stall a boot.
|
||||
func (d *Dispatcher) runRegionMedicOnStart(ctx context.Context, rec *instanceRecord) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
log := d.log.With("instance_id", rec.InstanceID, "medic", true)
|
||||
medicBin := regionMedicBin()
|
||||
if !medicFileExists(medicBin) {
|
||||
log.Warn("region-medic: binary missing, skipping pre-start heal", "bin", medicBin)
|
||||
return
|
||||
}
|
||||
savesVol := fmt.Sprintf("panel-%s-saves", rec.InstanceID)
|
||||
rollingDir := path.Join(regionRollingRoot(), rec.InstanceID)
|
||||
_ = os.MkdirAll(rollingDir, 0o755)
|
||||
backupsDir := path.Join(d.backupDir, rec.InstanceID)
|
||||
|
||||
cmd := []string{"/rm", "autoheal",
|
||||
"-saves-root", "/saves",
|
||||
"-rolling-dir", "/rolling",
|
||||
"-keep", "2", // default; overridden by region-medic.json in the saves volume
|
||||
"-label", rec.InstanceID,
|
||||
"-apply",
|
||||
}
|
||||
vols := []runtime.VolumeSpec{
|
||||
{Type: "volume", VolumeName: savesVol, ContainerPath: "/saves"},
|
||||
{Type: "bind", HostPath: rollingDir, ContainerPath: "/rolling"},
|
||||
{Type: "bind", HostPath: medicBin, ContainerPath: "/rm", ReadOnly: true},
|
||||
}
|
||||
// debian:12-slim ships no CA bundle, so the CLI's HTTPS POST to Discord can't
|
||||
// verify the TLS cert ("x509: certificate signed by unknown authority"). Mount
|
||||
// the host's CA certs read-only — Go's x509 finds ca-certificates.crt there.
|
||||
if _, err := os.Stat("/etc/ssl/certs"); err == nil {
|
||||
vols = append(vols, runtime.VolumeSpec{Type: "bind", HostPath: "/etc/ssl/certs", ContainerPath: "/etc/ssl/certs", ReadOnly: true})
|
||||
}
|
||||
// Panel tar backups are the heal fallback when no rolling slot has a clean
|
||||
// copy (e.g. very first boot, before any snapshot). Only mount if present.
|
||||
if st, err := os.Stat(backupsDir); err == nil && st.IsDir() {
|
||||
cmd = append(cmd, "-backups-dir", "/backups")
|
||||
vols = append(vols, runtime.VolumeSpec{Type: "bind", HostPath: backupsDir, ContainerPath: "/backups", ReadOnly: true})
|
||||
}
|
||||
// The token is mounted whenever it exists; the CLI only posts if the saves
|
||||
// config names a channel. No -discord-channel flag — the CLI reads it from
|
||||
// /saves/region-medic.json.
|
||||
if tf := discordTokenFile(); medicFileExists(tf) {
|
||||
cmd = append(cmd, "-discord-token-file", "/discord-token")
|
||||
vols = append(vols, runtime.VolumeSpec{Type: "bind", HostPath: tf, ContainerPath: "/discord-token", ReadOnly: true})
|
||||
}
|
||||
|
||||
spec := runtime.InstanceSpec{
|
||||
InstanceID: rec.InstanceID + "-medic",
|
||||
IsSidecar: true,
|
||||
Image: "debian:12-slim",
|
||||
Command: cmd,
|
||||
RestartPolicy: "no",
|
||||
Volumes: vols,
|
||||
}
|
||||
|
||||
log.Info("region-medic: pre-start scan+heal")
|
||||
contID, err := d.runtime.Create(ctx, spec)
|
||||
if err != nil {
|
||||
log.Warn("region-medic: create sidecar failed", "err", err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
rmCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
_ = d.runtime.Remove(rmCtx, contID)
|
||||
}()
|
||||
if err := d.runtime.Start(ctx, contID); err != nil {
|
||||
log.Warn("region-medic: start sidecar failed", "err", err)
|
||||
return
|
||||
}
|
||||
logCtx, cancelLogs := context.WithCancel(ctx)
|
||||
defer cancelLogs()
|
||||
logDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(logDone)
|
||||
_ = d.runtime.StreamLogs(logCtx, contID, func(_ runtime.LogStream, line string, _ time.Time) {
|
||||
log.Info("region-medic", "line", line)
|
||||
})
|
||||
}()
|
||||
exitCode, err := d.runtime.Wait(ctx, contID)
|
||||
cancelLogs()
|
||||
<-logDone
|
||||
if err != nil {
|
||||
log.Warn("region-medic: wait sidecar failed", "err", err)
|
||||
return
|
||||
}
|
||||
if exitCode != 0 {
|
||||
log.Warn("region-medic: sidecar exited non-zero", "code", exitCode)
|
||||
return
|
||||
}
|
||||
log.Info("region-medic: pre-start heal complete")
|
||||
}
|
||||
Reference in New Issue
Block a user