From 2d6fba5f16928ce1fad14ee95271609ccfcd2aa2 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 15 Jun 2026 13:50:06 +0000 Subject: [PATCH] fix(pods): self-heal pre-existing pods to pick up the public_html bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The box auto-deploys (self-update timer pulls origin/main, rebuilds, restarts agentbbs), but `systemctl restart agentbbs` only restarts the daemon — it never touches the long-lived per-user pod containers. ensure() also short-circuits on any container that already exists, so pods created before the homepage bind landed would never gain the /home/dev/public_html mount without a manual `podman rm`. That defeats the "everything happens automatically on push" goal. Make ensure() self-healing: when a pod exists but lacks the public_html mount, recreate it so the bind is applied. The named home volume survives `rm`, so the member's files are kept. Only heal when the pod is idle (attached count 0) to avoid pulling a running pod out from under an active session — an unbound pod heals on its next idle attach. New hasMount() inspects the container's mounts. Net effect: push to main -> self-update redeploys within the timer interval -> the next `ssh pod@` recreates the pod with the bind. No manual step. Co-Authored-By: Claude Opus 4.8 --- internal/pods/pods.go | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/internal/pods/pods.go b/internal/pods/pods.go index ef6e87e..0b10acb 100644 --- a/internal/pods/pods.go +++ b/internal/pods/pods.go @@ -70,6 +70,23 @@ func (m *Manager) publicHTMLMount(user string) (host, spec string) { return host, host + ":/home/dev/public_html" } +// hasMount reports whether the named container already has a mount at the given +// destination path. Used to detect pods created before a mount was introduced so +// ensure can recreate them. A failed inspect reports false (treat as missing). +func (m *Manager) hasMount(name, dest string) bool { + out, err := exec.Command(m.engine, "container", "inspect", + "-f", "{{range .Mounts}}{{println .Destination}}{{end}}", name).Output() + if err != nil { + return false + } + for _, line := range strings.Split(string(out), "\n") { + if strings.TrimSpace(line) == dest { + return true + } + } + return false +} + // Engine reports the active container engine. func (m *Manager) Engine() string { return m.engine } @@ -106,9 +123,21 @@ func (m *Manager) ensure(user string) (string, error) { } // Already exists? if err := exec.Command(m.engine, "container", "inspect", name).Run(); err == nil { - _ = exec.Command(m.engine, "start", name).Run() // no-op if running - m.tuneApt(name) - return name, nil + // Self-heal pods created before the homepage bind existed: recreate so + // ~/public_html maps to the served dir. The named home volume persists + // across rm, so the member's files are kept. Only heal when the pod is + // idle (no live session) — never pull a running pod out from under an + // active session; a still-unbound pod heals on its next idle attach. + m.mu.Lock() + idle := m.attached[name] == 0 + m.mu.Unlock() + if pubSpec != "" && idle && !m.hasMount(name, "/home/dev/public_html") { + _ = exec.Command(m.engine, "rm", "-f", name).Run() // fall through to recreate with the bind + } else { + _ = exec.Command(m.engine, "start", name).Run() // no-op if running + m.tuneApt(name) + return name, nil + } } args := []string{ "run", "-d",