The container deployment's answer to downloading a release: pull the image the operator named, then ask it what it is. That last part is the point of the phase, exactly as it is for a binary - the tag, the registry and the repository name are all assumptions about someone else's publishing process, and the image's own answer is the only thing that settles what arrived. The image is never derived from the running container by swapping its tag. That derivation is wrong for a digest-pinned image, wrong for a mirror and wrong for a fork, and being wrong here means pulling the wrong software into a mail server. It is named in full or the phase refuses. What comes back is the image's ID rather than the tag it arrived under. A tag can move between staging and cutover -- that is the whole reason latest is a hazard -- and running the tag later would run something other than what was verified here. VersionFromOutput is exported from preflight so both paths parse a version identically. Two copies of that regex could disagree about what they staged, which is a difference nobody would look for. One caveat recorded rather than hidden: asking an image its version means running it with --version, which assumes its entrypoint is the server and passes flags through. That has not been confirmed against a published Stalwart image, there being none to hand. If the assumption is wrong this fails loudly with the image's own output rather than staging something unverified, and the fallback tries the binary by name before giving up. SkipPull is for a host that loaded the image from a tarball, where a pull cannot work and its failure would say nothing useful.
96 lines
2.7 KiB
Go
96 lines
2.7 KiB
Go
// SPDX-FileCopyrightText: 2026 LINUXexpert-org
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package preflight
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// semver is a minimal major.minor.patch version - enough for this tool's
|
|
// comparisons since Stalwart's pre-1.0 release tags don't use pre-release
|
|
// or build metadata.
|
|
type semver struct {
|
|
Major, Minor, Patch int
|
|
}
|
|
|
|
var versionPattern = regexp.MustCompile(`v?(\d+)\.(\d+)\.(\d+)`)
|
|
|
|
func parseSemver(s string) (semver, error) {
|
|
m := versionPattern.FindStringSubmatch(s)
|
|
if m == nil {
|
|
return semver{}, fmt.Errorf("preflight: no version number found in %q", s)
|
|
}
|
|
major, _ := strconv.Atoi(m[1])
|
|
minor, _ := strconv.Atoi(m[2])
|
|
patch, _ := strconv.Atoi(m[3])
|
|
return semver{major, minor, patch}, nil
|
|
}
|
|
|
|
func (v semver) String() string { return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) }
|
|
|
|
// Compare returns -1, 0, or 1 as v is less than, equal to, or greater than o.
|
|
func (v semver) Compare(o semver) int {
|
|
if v.Major != o.Major {
|
|
return cmp(v.Major, o.Major)
|
|
}
|
|
if v.Minor != o.Minor {
|
|
return cmp(v.Minor, o.Minor)
|
|
}
|
|
return cmp(v.Patch, o.Patch)
|
|
}
|
|
|
|
func cmp(a, b int) int {
|
|
switch {
|
|
case a < b:
|
|
return -1
|
|
case a > b:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// minSupportedSource is the oldest version this tool will start a migration
|
|
// from. Stalwart's own upgrade guidance says older installs need to reach
|
|
// 0.15.x first before crossing the 0.15/0.16 schema boundary this tool
|
|
// automates - see ARCHITECTURE.md §1/§4.1.
|
|
var minSupportedSource = semver{0, 15, 0}
|
|
|
|
// VersionFromOutput extracts a semver from whatever a Stalwart build
|
|
// printed when asked for its version. Exported because the same question
|
|
// gets asked of a container image (internal/stage), where the command is
|
|
// `docker run <image> --version` rather than the binary directly - and the
|
|
// answer has to be parsed identically or the two paths could disagree
|
|
// about what they staged.
|
|
func VersionFromOutput(out string) (string, error) {
|
|
v, err := parseSemver(out)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return v.String(), nil
|
|
}
|
|
|
|
// DetectVersion runs the installed binary's --version flag and extracts a
|
|
// semver from its output.
|
|
func DetectVersion(ctx context.Context, binaryPath string) (string, error) {
|
|
cmd := exec.CommandContext(ctx, binaryPath, "--version")
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("preflight: run %s --version: %w (output: %s)", binaryPath, err, strings.TrimSpace(out.String()))
|
|
}
|
|
v, err := parseSemver(out.String())
|
|
if err != nil {
|
|
return "", fmt.Errorf("preflight: parse version from %s --version output %q: %w", binaryPath, strings.TrimSpace(out.String()), err)
|
|
}
|
|
return v.String(), nil
|
|
}
|