Tell what a container inherits from what it overrides
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.
This commit is contained in:
@@ -474,6 +474,14 @@ func withFakeDocker(t *testing.T) {
|
||||
// dockerPreflight runs a minimal but real preflight against a fake 0.15.5
|
||||
// install with a fake docker on PATH.
|
||||
func dockerPreflight(t *testing.T, advisory bool) Report {
|
||||
t.Helper()
|
||||
return dockerPreflightOn(t, advisory, nil)
|
||||
}
|
||||
|
||||
// dockerPreflightOn runs preflight against a container whose inspect
|
||||
// document has been rewritten by edit, for the cases that need it to be
|
||||
// something other than an ordinary one.
|
||||
func dockerPreflightOn(t *testing.T, advisory bool, edit func(string) string) Report {
|
||||
t.Helper()
|
||||
// disk-space stats DataDir on this host, and container-data-volume
|
||||
// wants it covered by a mount, so it has to be both: a real directory,
|
||||
@@ -484,7 +492,11 @@ func dockerPreflight(t *testing.T, advisory bool) Report {
|
||||
t.Skipf("host has %s, which detection prefers over docker", p)
|
||||
}
|
||||
}
|
||||
fakeInspect(t, inspectDoc(t, nil, []Mount{dataVolume(dataDir)}))
|
||||
doc := inspectDoc(t, nil, []Mount{dataVolume(dataDir)})
|
||||
if edit != nil {
|
||||
doc = edit(doc)
|
||||
}
|
||||
fakeInspect(t, doc)
|
||||
|
||||
counterPath := filepath.Join(t.TempDir(), "invocations")
|
||||
binaryPath := writeFakeBinary(t, "0.15.5", counterPath)
|
||||
@@ -553,3 +565,45 @@ func TestRehearseStillRunsAgainstADockerDeployment(t *testing.T) {
|
||||
t.Fatalf("advisory mode should not block on docker, got:\n%s", report.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Cutover already refused a container it could not recreate, but cutover
|
||||
// is downstream of the stop, the settings conversion and the store
|
||||
// migration - so that refusal arrived with the mail down and the data
|
||||
// already moved, which is the shape of failure issue #1 was filed for.
|
||||
// The answer never changes between the two points, so it is asked here,
|
||||
// while the server is still running.
|
||||
func TestPreflightRefusesAContainerItCouldNotRecreate(t *testing.T) {
|
||||
report := dockerPreflightOn(t, false, func(doc string) string {
|
||||
return strings.Replace(doc, `"State":`, `"HostConfig":{"Privileged":true},"State":`, 1)
|
||||
})
|
||||
if !report.Blocking() {
|
||||
t.Fatalf("a container preflight cannot recreate should block before anything stops:\n%s", report.String())
|
||||
}
|
||||
var found bool
|
||||
for _, res := range report.Results {
|
||||
if res.Name == "container-recreatable" {
|
||||
found = true
|
||||
if res.Status != StatusFail {
|
||||
t.Errorf("container-recreatable status = %q, want %q", res.Status, StatusFail)
|
||||
}
|
||||
if !strings.Contains(res.Detail, "privileged") {
|
||||
t.Errorf("detail should name what would be dropped, got %q", res.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("no container-recreatable result in report:\n%s", report.String())
|
||||
}
|
||||
}
|
||||
|
||||
// rehearse never stops or recreates anything, so the same finding is a
|
||||
// warning there: an operator migrating by hand needs to know it more than
|
||||
// an automated run does.
|
||||
func TestRehearseWarnsRatherThanBlocksOnAnUnrecreatableContainer(t *testing.T) {
|
||||
report := dockerPreflightOn(t, true, func(doc string) string {
|
||||
return strings.Replace(doc, `"State":`, `"HostConfig":{"Privileged":true},"State":`, 1)
|
||||
})
|
||||
if report.Blocking() {
|
||||
t.Fatalf("advisory mode should not block, got:\n%s", report.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user