container updates respect the new image's command/entrypoint

This commit is contained in:
jarek committed 2026-08-09 10:31:28 +02:00
1 parent 1c1d7979d7
commit d33185d1fa
4 files changed
+292 -90

No files matched your search

@@ -386,12 +386,12 @@ export async function runContainerUpdate(
// Get the actual image ID from inspect data
const currentImageId = inspectData.Image;
// Capture the OLD image's Env/Labels BEFORE the pull — the old digest may
// be untagged/GC'd afterward. Forwarded to the env/label rebase (#1226, #1256).
// Capture the OLD image's Env/Labels/Cmd/Entrypoint BEFORE the pull — the old digest may
// be untagged/GC'd afterward. Forwarded to the env/label/command rebase (#1226, #1256, #1371).
let oldImageConfig: ImageEnvLabels | null = null;
try {
const oldImg = await inspectImage(currentImageId, envId) as any;
oldImageConfig = { Env: oldImg?.Config?.Env, Labels: oldImg?.Config?.Labels };
oldImageConfig = { Env: oldImg?.Config?.Env, Labels: oldImg?.Config?.Labels, Cmd: oldImg?.Config?.Cmd ?? null, Entrypoint: oldImg?.Config?.Entrypoint ?? null };
} catch {
// Best-effort; rebase falls back if unavailable.
}
@@ -669,10 +669,30 @@ export async function recreateContainer(
const inspectData = await inspectContainer(container.id, envId) as any;
const imageName = imageNameOverride || inspectData.Config?.Image;
// Capture the parent's id BEFORE recreate. A recreate gives the parent a NEW id,
// and any child using `network_mode: service:parent` / `container:parent` stores
// that old id in its own HostConfig.NetworkMode (`container:<oldId>`). After the
// parent update those children still point at the dead id and fail to (re)start
// with "joining network namespace of container: No such container" (#570).
const oldParentId: string = inspectData.Id;
log?.(`Recreating container: ${containerName} (image: ${imageName})`);
await recreateContainerFromInspect(inspectData, imageName, envId, log, oldImageConfig);
// Parent recreate SUCCEEDED (a failure would have thrown and rolled the parent
// back to its original id, leaving children valid). Repoint any dependent
// children at the new parent. Best-effort and fully isolated: a child failure
// NEVER changes the parent's already-successful update. No-op when there are no
// such children — the common case pays nothing beyond one already-in-hand id.
try {
const reconnected = await reconnectDependentChildren(oldParentId, containerName, envId, log);
if (reconnected > 0) {
log?.(`Reconnected ${reconnected} dependent container${reconnected === 1 ? '' : 's'} to the new parent`);
}
} catch (e: any) {
log?.(`WARNING: dependent-child reconnect step errored (parent update stands): ${e?.message}`);
}
return { success: true };
} catch (error: any) {
log?.(`Failed to recreate container: ${error.message}`);
@@ -680,3 +700,52 @@ export async function recreateContainer(
}
}
/**
* After a parent container is recreated (new id), find containers whose network is
* shared into the parent by id (`network_mode: service:X`/`container:X`, which Docker
* lowers to `HostConfig.NetworkMode = "container:<parentId>"`) and repoint them at the
* new parent. A plain restart is NOT enough — the stale id is baked into the child's
* stored NetworkMode, so the child must be RECREATED with the reference rewritten. We
* rewrite to `container:<parentName>` (not the new id) so the child also survives future
* parent recreations. Best-effort per child; the caller isolates this from the parent
* update result. Returns the number of children repointed. (#570)
*/
async function reconnectDependentChildren(
oldParentId: string,
newParentName: string,
envId: number | undefined,
log?: (msg: string) => void,
visited: Set<string> = new Set()
): Promise<number> {
if (visited.has(oldParentId)) return 0; // cycle guard for A<-B<-C chains
visited.add(oldParentId);
const all = await listContainers(true, envId); // include stopped
let count = 0;
for (const c of all) {
if (c.id === oldParentId) continue;
let ci: any;
try { ci = await inspectContainer(c.id, envId); } catch { continue; }
const mode: string = ci.HostConfig?.NetworkMode || '';
if (!mode.startsWith('container:')) continue;
const ref = mode.slice('container:'.length);
// Match the parent by full id, or defensively by short (12-char) id.
if (ref !== oldParentId && ref !== oldParentId.slice(0, 12) && !oldParentId.startsWith(ref)) continue;
log?.(`Reconnecting dependent container "${c.name}" -> container:${newParentName}`);
const oldChildId: string = ci.Id;
ci.HostConfig.NetworkMode = `container:${newParentName}`;
try {
const { Id: newChildId } = await recreateContainerFromInspect(ci, ci.Config.Image, envId, log);
count++;
// Chain: if this child was itself a parent to others, repoint them too.
if (newChildId && newChildId !== oldChildId) {
count += await reconnectDependentChildren(oldChildId, c.name, envId, log, visited);
}
} catch (e: any) {
log?.(`WARNING: failed to reconnect dependent "${c.name}": ${e?.message}`);
}
}
return count;
}