Initial commit: stalwart-migrator design and scaffolding
In-place upgrade tool for Stalwart Mail Server (0.15.5 -> latest) with checkpointed rollback and post-migration validation. Design stage; see ARCHITECTURE.md.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProcessOptions configures how the target binary is launched.
|
||||
type ProcessOptions struct {
|
||||
BinaryPath string
|
||||
ConfigPath string
|
||||
|
||||
// RecoveryMode, when true, sets STALWART_RECOVERY_MODE=1 and
|
||||
// STALWART_RECOVERY_ADMIN=<AdminUser>:<AdminPassword> - exactly the
|
||||
// environment variables Stalwart's own upgrade guide uses to bring the
|
||||
// new binary up in recovery mode against a not-yet-migrated store (see
|
||||
// UPGRADING/v0_16.md). When false, the binary is started as a normal
|
||||
// boot against ConfigPath - used after recovery mode to confirm the
|
||||
// migrated store comes up cleanly under an ordinary start, not just a
|
||||
// recovery one (see ARCHITECTURE.md's dry-run design).
|
||||
RecoveryMode bool
|
||||
AdminUser string
|
||||
AdminPassword string
|
||||
|
||||
// ExtraEnv is appended after any recovery-mode env vars. This is what
|
||||
// lets a dry-run point the process at a sandbox without RecoveryMode's
|
||||
// two env vars becoming the only way to parameterize the child process.
|
||||
ExtraEnv []string
|
||||
}
|
||||
|
||||
// Process supervises one run of the target Stalwart binary as a background
|
||||
// child process, so a caller can start it, wait for it to become healthy,
|
||||
// interact with it, and stop it again - without ever touching a real
|
||||
// systemd unit or Docker container. See ARCHITECTURE.md §4.4.
|
||||
type Process struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// Start launches the binary. It returns as soon as the OS has started the
|
||||
// process - it does not wait for Stalwart itself to become ready; use
|
||||
// WaitForHealthy for that.
|
||||
func (p *Process) Start(ctx context.Context, o ProcessOptions) error {
|
||||
var env []string
|
||||
if o.RecoveryMode {
|
||||
env = append(env,
|
||||
"STALWART_RECOVERY_MODE=1",
|
||||
fmt.Sprintf("STALWART_RECOVERY_ADMIN=%s:%s", o.AdminUser, o.AdminPassword),
|
||||
)
|
||||
}
|
||||
env = append(env, o.ExtraEnv...)
|
||||
|
||||
cmd := exec.CommandContext(ctx, o.BinaryPath, "--config", o.ConfigPath)
|
||||
cmd.Env = append(os.Environ(), env...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("recovery: start %s: %w", o.BinaryPath, err)
|
||||
}
|
||||
p.cmd = cmd
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop sends SIGTERM and waits up to gracePeriod for the process to exit -
|
||||
// mirroring the upgrade guide's own "Ctrl+C in the first terminal" step,
|
||||
// just automated - escalating to SIGKILL if it doesn't exit in time so a
|
||||
// stuck child process can never hang a migration run indefinitely. Safe to
|
||||
// call on a Process that was never successfully started.
|
||||
func (p *Process) Stop(gracePeriod time.Duration) error {
|
||||
if p.cmd == nil || p.cmd.Process == nil {
|
||||
return nil
|
||||
}
|
||||
if err := p.cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
||||
return fmt.Errorf("recovery: signal process (pid %d): %w", p.cmd.Process.Pid, err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- p.cmd.Wait() }()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
// A non-zero exit from a SIGTERM-based shutdown is expected and not
|
||||
// itself a failure worth reporting - only an unexpected Wait error is.
|
||||
if err != nil {
|
||||
if _, ok := err.(*exec.ExitError); !ok {
|
||||
return fmt.Errorf("recovery: wait for process (pid %d): %w", p.cmd.Process.Pid, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case <-time.After(gracePeriod):
|
||||
_ = p.cmd.Process.Kill()
|
||||
<-done
|
||||
return fmt.Errorf("recovery: process (pid %d) did not exit within %s of SIGTERM - sent SIGKILL", p.cmd.Process.Pid, gracePeriod)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user