Take the target version from the binary when there is no route out

preflight asked the GitHub release API which version it was upgrading to,
so a host with no internet failed there even with the target binary already
on disk - and `run` never passed its own --target-binary down to preflight,
so supplying one did not help. Read the version from the binary instead
when one is given, and fail if it is not the version the run targets.

Found by the first preflight against the production clone, which is
deliberately cut off from the network.
This commit is contained in:
2026-08-24 15:27:20 -07:00
parent 0739866c14
commit b786e89b42
4 changed files with 125 additions and 21 deletions
+31 -9
View File
@@ -18,15 +18,19 @@ import (
// Options configures a Checker. Every field has a conservative default
// applied by New except the ones that must name a real path on this host.
type Options struct {
BinaryPath string // installed stalwart binary, e.g. /usr/local/bin/stalwart
ConfigPath string // its config file (TOML pre-0.16, JSON 0.16+)
DataDir string // data directory to size/space-check
ContainerName string // docker container name, if applicable
AdminURL string // base URL for the JMAP reachability check; empty skips it
AdminUser string
AdminPassword string
TargetVersion string // e.g. "0.16.14" or "latest"
MinFreeMultiple float64
BinaryPath string // installed stalwart binary, e.g. /usr/local/bin/stalwart
ConfigPath string // its config file (TOML pre-0.16, JSON 0.16+)
DataDir string // data directory to size/space-check
ContainerName string // docker container name, if applicable
AdminURL string // base URL for the JMAP reachability check; empty skips it
AdminUser string
AdminPassword string
TargetVersion string // e.g. "0.16.14" or "latest"
// TargetBinaryPath, when set, is read for the target version instead of
// asking the release API - the only way a host with no route out can
// pass this check.
TargetBinaryPath string
MinFreeMultiple float64
// CLIPath and PythonPath are the external programs the migration
// shells out to. Checked before anything is touched - see
// CheckExternalTools.
@@ -103,6 +107,24 @@ func (c *Checker) Run(ctx context.Context, store *checkpoint.Store, rs *checkpoi
}
targetOutcome, err := runCheck("target-release", func() (CheckResult, string) {
// A binary already on disk answers the question the release API was
// being asked - which version are we upgrading to - without needing
// a route to the internet. A host that has none cannot reach the
// API at all, and failing here would stop it migrating even though
// everything it needs is present.
if c.opts.TargetBinaryPath != "" {
got, err := DetectVersion(ctx, c.opts.TargetBinaryPath)
if err != nil {
return CheckResult{Status: StatusFail, Detail: fmt.Sprintf("couldn't read the version of %s: %v", c.opts.TargetBinaryPath, err)}, ""
}
want := strings.TrimPrefix(c.opts.TargetVersion, "v")
if want != "" && want != "latest" && want != got {
return CheckResult{Status: StatusFail, Detail: fmt.Sprintf(
"%s reports version %s, but this run targets %s - migrating to a version nobody planned for",
c.opts.TargetBinaryPath, got, want)}, ""
}
return CheckResult{Status: StatusOK, Detail: fmt.Sprintf("target is %s, taken from %s (no release lookup needed)", got, c.opts.TargetBinaryPath)}, got
}
rel, err := ResolveRelease(ctx, c.opts.HTTPClient, c.opts.TargetVersion)
if err != nil {
return CheckResult{Status: StatusFail, Detail: err.Error()}, ""