container update no longer fails on Podman for containers using userns_mode: keep-id

This commit is contained in:
jarek committed 2026-08-16 08:15:30 +02:00
1 parent 0a08c94287
commit 3b43946a38
2 files changed
+49 -1

No files matched your search

+23 -1
View File
@@ -18,7 +18,7 @@ import { computeRequestTimeoutMs } from './backups/request-timeout';
import type { Environment } from './db';
import { getSetting } from './db';
import { getAdditionalVolumeBinds, dedupeVolumesForRecreate } from './mount-dedupe';
import { resolveNanoCpusConflict } from './hostconfig-recreate';
import { resolveNanoCpusConflict, resolvePodmanUsernsMode } from './hostconfig-recreate';
import { rebaseEnvOntoImage, rebaseLabelsOntoImage, rebaseCommand, describeEnvRebase, describeLabelRebase, type ImageEnvLabels } from './container-env-merge';
import { encodeRegistryAuth } from './registry-auth';
import { isSystemContainer, classifyEmptyDigestImage, localDigestIsIndexChild } from './scheduler/tasks/update-utils';
@@ -2093,6 +2093,20 @@ export async function recreateContainerFromInspect(
log?.('Dropped CpuPeriod/CpuQuota — they conflict with NanoCpus at create time (kept NanoCpus)');
}
// Podman lowers `--userns keep-id` to UsernsMode:"private" in inspect, which create then
// rejects without inline mappings (#1409). Restore the original intent from the annotation
// so keep-id survives the recreate; Docker (UsernsMode != "private") is untouched.
const usernsFix = resolvePodmanUsernsMode(createConfig.HostConfig?.UsernsMode, config.Annotations);
if (usernsFix && createConfig.HostConfig) {
if ('mode' in usernsFix) {
createConfig.HostConfig.UsernsMode = usernsFix.mode;
log?.(`Restored UsernsMode "${usernsFix.mode}" from Podman annotation (was "private")`);
} else {
delete createConfig.HostConfig.UsernsMode;
log?.('Dropped UsernsMode "private" — Podman rejects it without inline UID/GID mappings');
}
}
// container:<name> mode shares the network namespace — Docker rejects
// networking-related fields on the dependent container since they're
// owned by the network provider container
@@ -2305,6 +2319,14 @@ export async function createContainerFromMetadata(
delete createConfig.HostConfig.MemorySwappiness;
}
// Podman keep-id -> UsernsMode:"private" in inspect, rejected on create (#1409).
// Restore the intent from the annotation; Docker (UsernsMode != "private") untouched.
const usernsFix = resolvePodmanUsernsMode(createConfig.HostConfig?.UsernsMode, config.Annotations);
if (usernsFix && createConfig.HostConfig) {
if ('mode' in usernsFix) createConfig.HostConfig.UsernsMode = usernsFix.mode;
else delete createConfig.HostConfig.UsernsMode;
}
// container:<name> mode: clean up conflicting network fields
if (networkMode.startsWith('container:')) {
delete createConfig.Hostname;
+26
View File
@@ -32,3 +32,29 @@ export function resolveNanoCpusConflict(hostConfig: CpuHostConfig | null | undef
delete hostConfig.CpuQuota;
return true;
}
const PODMAN_USERNS_ANNOTATION = 'io.podman.annotations.userns';
/**
* Podman lowers `--userns keep-id`/`auto`/`nomap` to `UsernsMode: "private"` plus generated
* `IDMappings` in inspect, losing the original intent. Replaying `UsernsMode: "private"` to
* create WITHOUT inline mappings is rejected ("must provide at least one UID or GID mapping
* to configure a user namespace", #1409). Podman keeps the real intent in the
* `io.podman.annotations.userns` annotation, and `create --userns keep-id` regenerates the
* mappings - so restore the annotation value to keep keep-id working across a recreate.
*
* Returns the corrected UsernsMode: the annotation value ("keep-id", "auto",
* "keep-id:uid=1000,gid=1000", ...) when present, `undefined` to STRIP a bare "private" with
* no recorded intent, or `null` for "leave UsernsMode untouched". Only the lowered "private"
* form is ever changed, so Docker (UsernsMode "", "host", "container:x", ...) is never
* touched - no Docker regression.
*/
export function resolvePodmanUsernsMode(
usernsMode: string | undefined | null,
annotations: Record<string, string> | undefined | null
): { mode: string } | { strip: true } | null {
if (usernsMode !== 'private') return null;
const intent = annotations?.[PODMAN_USERNS_ANNOTATION]?.trim();
if (intent) return { mode: intent };
return { strip: true };
}