Wire the container path up, behind a flag that says what it is

Everything the container migration needs has landed a piece at a time and
nothing called any of it. `run` now does: stage pulls and verifies an image
instead of downloading a binary, the recovery cycle launches a throwaway
container against the live container's own mounts, and cutover recreates it.
Preflight's blanket refusal of docker goes with it -- what still refuses is
specific to a container rather than to containers, which is compose and
data that is not on a volume.

It refuses without --container-path-unproven, and that flag is the honest
part of this change. Every test drives a fake docker. That proves the right
commands are assembled and proves nothing about whether a real image reads
the config it is handed -- which is the exact limit ARCHITECTURE.md section
4.8 records about the rollback code that was deleted for being tested only
against fakes. A doc note seemed too quiet for a tool that stops a mail
server, so it is a flag nobody reaches without being told.

The converted config reaches the container through the data volume. It is
written under the host side of whichever mount covers --data-dir and named
on the container side, because cutover recreates a container with the mounts
it had and cannot invent a new one for a config file. --data-dir therefore
names the path inside the container, which preflight already says when it
matches no mount.

PatchPaths stays unused, deliberately. Its documented purpose is pointing a
rehearsal at a sandbox; a real container's dumped settings already carry
container-side paths, because they come from the live server rather than
from a file on this host.

The preflight test that asserted docker was refused outright now asserts
the replacement rather than being deleted -- "docker is allowed through
here" is the thing that would be wrong to regress. Its fixture had to make
--data-dir both a real host directory and one the fake container mounts,
since disk-space stats it and container-data-volume wants it covered.

README gains the container section and, at the top, the note that this is
ihasmail's companion.
This commit is contained in:
2026-08-28 17:42:31 -07:00
parent a23a00dfba
commit 272439cf2a
6 changed files with 207 additions and 39 deletions
+10 -10
View File
@@ -200,19 +200,19 @@ func (c *Checker) Run(ctx context.Context, store *checkpoint.Store, rs *checkpoi
// automate it - but cutover runs after the service has been
// stopped. Refusing there means refusing with mail already down,
// which is how a migration attempt turned into an outage.
if kind == DeploymentDocker && !c.opts.DeploymentCheckAdvisory {
return CheckResult{
Status: StatusFail,
Detail: "detected deployment kind: docker - this tool cannot cut over a container. " +
"Migrating one means pulling the new image and recreating the container, which has to be done by hand; " +
"`rehearse` still works and will tell you what the migration involves",
}, string(kind)
}
status := StatusOK
if kind == DeploymentUnknown {
detail := fmt.Sprintf("detected deployment kind: %s", kind)
switch kind {
case DeploymentUnknown:
status = StatusWarn
case DeploymentDocker:
// No longer a refusal on its own: a container can be migrated
// now. What still refuses is specific and checked below -
// compose, and data that is not on a volume - because those are
// properties of this container rather than of containers.
detail += " - the container checks below decide whether this one can be migrated"
}
return CheckResult{Status: status, Detail: fmt.Sprintf("detected deployment kind: %s", kind)}, string(kind)
return CheckResult{Status: status, Detail: detail}, string(kind)
})
if err != nil {
return report, err