Checked against a real stalwartlabs/stalwart image, `docker inspect` on an ordinary container reports User "stalwart", Entrypoint ["/usr/local/bin/stalwart"] and Cmd ["--config", "/etc/stalwart/config.json"] — all three inherited, none of them given. Two things followed from reading those as the operator's. A container user was listed as configuration a recreate would drop, so every container off the official image was refused as unrecreatable. That refusal lived in cutover, downstream of the stop, the settings conversion and the store migration: it arrived with mail down and data already moved, which is the failure issue #1 was filed for. Each of the three is now compared against `docker image inspect` of the image the container is on. Inherited values are left to the new image, whose own defaults are the ones that go with it. Overrides are carried: --user, --entrypoint, and the rest of an entrypoint as leading argv. Cmd and Entrypoint were not being read at all, so an overridden one was silently dropped — the exact loss the unsupported list exists to prevent. The recreatability question also moved into preflight, while the server is still running. Cutover asks it again, since the two are separated by the whole migration, but only one of them can refuse without cost. The other half: the recreated container is now started with `--config` pointing at the migrated config in the data volume. Left to the image's default command it came up on /etc/stalwart/config.json — a different volume, holding whatever the old version left there — so cutover would have produced a running server with nothing to do with the migration that preceded it. An overridden command and that --config are the same argv and cannot be merged honestly, so a container with one is refused and told why. The config is also chowned to whatever owns the data directory, before the recovery cycle opens it. The image runs as uid 2000 and this tool writes as root; §4.8 is the standing reminder that byte-perfect and unreadable is a way to report success. Found while checking @kaya-eu's field report in #1 against a real image. Their three manual migrations are where the config step comes from.
50 lines
1.8 KiB
Go
50 lines
1.8 KiB
Go
// SPDX-FileCopyrightText: 2026 LINUXexpert-org
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
)
|
|
|
|
// matchOwnership gives paths the uid and gid of reference.
|
|
//
|
|
// It exists for the container path, where this tool writes the migrated
|
|
// config onto the host side of a volume and something inside the container
|
|
// has to read it. That something is not root: the official Stalwart image
|
|
// runs as uid 2000, so a config written root-owned and 0640 is a config
|
|
// the server cannot open, and the failure arrives as a recovery boot that
|
|
// will not come up rather than as a permissions error anyone would
|
|
// recognise.
|
|
//
|
|
// The reference is the data directory itself, whose ownership is already
|
|
// whatever the container writes as. Copying it is more reliable than
|
|
// resolving a user out of the image, and it is the same move
|
|
// cutover.installConfig makes for the systemd path with
|
|
// ConfigOwnerReference.
|
|
//
|
|
// ARCHITECTURE.md §4.8 is the reason this is not left to chance: the
|
|
// rollback that was removed from this tool restored every byte correctly
|
|
// and lost ownership, and reported success.
|
|
func matchOwnership(reference string, paths ...string) (string, error) {
|
|
info, err := os.Stat(reference)
|
|
if err != nil {
|
|
return "", fmt.Errorf("stat %s to copy its ownership: %w", reference, err)
|
|
}
|
|
sys, ok := info.Sys().(*syscall.Stat_t)
|
|
if !ok {
|
|
return "ownership unchanged (this platform does not report it)", nil
|
|
}
|
|
uid, gid := int(sys.Uid), int(sys.Gid)
|
|
for _, p := range paths {
|
|
if err := os.Chown(p, uid, gid); err != nil {
|
|
return "", fmt.Errorf(
|
|
"set ownership on %s to %d:%d, which is what owns %s - the server in the container runs as that user and "+
|
|
"cannot read a config it does not own: %w", p, uid, gid, reference, err)
|
|
}
|
|
}
|
|
return fmt.Sprintf("uid %d, gid %d, copied from %s", uid, gid, reference), nil
|
|
}
|