Refuse a Docker deployment before stopping anything

Cutover already refused a container -- recreating one from a new image is
not swapping a binary and rewriting a unit, and this tool does not automate
it -- but it refused from cutover.Run, which run.go calls at line 324. The
service is stopped at line 243. So the sequence on a container was: stop
Stalwart, convert the settings, then discover the deployment cannot be cut
over, return the error, and exit with mail still down. Reported as #1.

The deployment kind is known in preflight, before anything has been touched,
and that is now where it is acted on: docker is a blocking check. rehearse
keeps working -- it never stops the service or cuts over, and telling an
operator what the migration involves is most useful precisely when the tool
cannot do it for them -- so it sets DeploymentCheckAdvisory, alongside the
ToolCheckAdvisory it already set for the same reason.

The second half is not docker's alone. Every return between the stop and the
end of cutover returned with the service down; a failed settings conversion
would have done the same to a systemd host. run now registers a restart on
the way out, after the stop rather than before, so it only ever starts
something this tool stopped. It does not claim to have recovered the
migration -- a part-migrated store still needs --resume or the operator's
recovery point -- it removes the narrower failure of exiting on a
foreseeable error while the server it stopped stays stopped.
This commit is contained in:
2026-08-28 16:33:58 -07:00
parent 80a76fbb4a
commit edd8279743
4 changed files with 151 additions and 2 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ func runRehearse(args []string) (err error) {
BinaryPath: *binaryPath, ConfigPath: *configPath, DataDir: *dataDir, ContainerName: *containerName,
AdminURL: *adminURL, AdminUser: *adminUser, AdminPassword: *adminPassword,
TargetVersion: *targetVersion, MinFreeMultiple: *minFree, HTTPClient: httpClient,
CLIPath: *stalwartCLI, PythonPath: *pythonPath, ToolCheckAdvisory: true,
CLIPath: *stalwartCLI, PythonPath: *pythonPath, ToolCheckAdvisory: true, DeploymentCheckAdvisory: true,
})
pfReport, err := checker.Run(ctx, store, rs)
fmt.Print(pfReport.String())
+35
View File
@@ -253,6 +253,41 @@ func runRun(args []string) (err error) {
}
fmt.Println(controller.Target(), "stopped")
// Mail is down from here, and every return below is a return with it
// still down. Registered after the stop rather than before, so it only
// ever restarts something this tool actually stopped.
//
// It does not pretend to have recovered the migration: a run that
// aborted midway is still part-migrated and still needs --resume or the
// operator's recovery point. What it prevents is the narrower and worse
// outcome of the tool exiting on an error it could see coming while the
// server it stopped stays stopped.
defer func() {
if err == nil {
return
}
// The run's context may already be cancelled - that can be why we
// are here - and a cancelled context cannot start anything.
restartCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Minute)
defer cancel()
active, activeErr := controller.Active(restartCtx)
if activeErr == nil && active {
return
}
fmt.Println("\n--- this run failed with", controller.Target(), "stopped: starting it again ---")
if startErr := controller.Start(restartCtx); startErr != nil {
fmt.Printf("could not start %s: %v\n", controller.Target(), startErr)
fmt.Println("MAIL IS STILL DOWN - start it by hand before anything else.")
return
}
if waitErr := service.WaitFor(restartCtx, controller, true, 2*time.Minute); waitErr != nil {
fmt.Printf("%s was asked to start but did not come up: %v\n", controller.Target(), waitErr)
fmt.Println("MAIL IS STILL DOWN - check it by hand before anything else.")
return
}
fmt.Println(controller.Target(), "is running again. The migration itself did not complete - see the error below.")
}()
if p.CrossesMajorBoundary {
fmt.Println("\n--- convert ---")
if _, err := store.RunStep(rs, checkpoint.PhaseStage, "convert-settings", func() (checkpoint.StepOutcome, error) {