container updates respect the new image's command/entrypoint
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user