Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/lib/onboard/fatal-runtime-preflight.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";
import { isLinuxDockerDriverGatewayEnabled } from "./docker-driver-platform";
import { rejectUnsupportedContainerRuntime } from "./fatal-runtime-preflight";
import type { HostAssessment } from "./preflight";

function hostWithRuntime(runtime: HostAssessment["runtime"]): HostAssessment {
return {
platform: process.platform,
isWsl: false,
runtime,
dockerInstalled: true,
dockerRunning: true,
dockerReachable: true,
nodeInstalled: true,
openshellInstalled: true,
isContainerRuntimeUnderProvisioned: false,
hasNestedOverlayConflict: false,
requiresHostCgroupnsFix: false,
isUnsupportedRuntime: runtime === "podman",
isHeadlessLikely: false,
hasNvidiaGpu: false,
dockerCdiSpecDirs: [],
cdiNvidiaGpuSpecMissing: false,
nvidiaContainerToolkitInstalled: false,
notes: [],
} as HostAssessment;
}

describe("rejectUnsupportedContainerRuntime (#7320)", () => {
// The Docker-driver gateway path is forced on Linux and Apple Silicon macOS;
// the reject gate only fires there. Gate the test on the same predicate via
// it.skipIf (not an in-body `if`) so it runs on the Linux CI runner.
it.skipIf(!isLinuxDockerDriverGatewayEnabled())(
"exits when Podman is detected on a Docker-driver gateway platform",
() => {
const exit = vi.fn(() => {
throw new Error("exit");
});
expect(() =>
rejectUnsupportedContainerRuntime(hostWithRuntime("podman"), exit as never),
).toThrow("exit");
expect(exit).toHaveBeenCalledWith(1);
},
);

it("does not exit for a supported Docker runtime", () => {
const exit = vi.fn();
rejectUnsupportedContainerRuntime(hostWithRuntime("docker"), exit as never);
expect(exit).not.toHaveBeenCalled();
});
});
3 changes: 3 additions & 0 deletions src/lib/onboard/preflight-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ describe("onboard preflight severity messages (#6004)", () => {
expect(lines(err)[0]).toContain("✗");
expect(lines(err)[0]).toContain("Docker driver");
expect(lines(err).join("\n")).toContain("Switch to Docker Engine");
// macOS reporters use Docker Desktop or Colima, not native Docker Engine (#7320).
expect(lines(err).join("\n")).toContain("Docker Desktop");
expect(lines(err).join("\n")).toContain("Colima");
});

it("prints the under-provisioned warning to stderr with a ⚠ marker and colima resize", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/onboard/preflight-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function printDockerNotReachableError(): void {
export function printUnsupportedRuntimeError(): void {
console.error(failLine(`${cliDisplayName()} onboarding now uses OpenShell's Docker driver.`));
console.error(` Podman is not supported for this ${cliDisplayName()} integration path.`);
console.error(" Switch to Docker Engine and rerun onboarding.");
console.error(" Switch to Docker Engine, Docker Desktop, or Colima, then rerun onboarding.");
}

export interface UnderProvisionedRuntimeWarning {
Expand Down
110 changes: 110 additions & 0 deletions src/lib/onboard/preflight-podman-compat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
// Import source directly so tests cannot pass against a stale build.
import { assessHost } from "./preflight";

// Regression: NemoClaw #7320. On Apple Silicon macOS a Docker CLI routed to
// Podman's docker-compat socket reaches the forced Docker-driver gateway and
// exits EADDRNOTAVAIL binding Podman's VM-only bridge IP (10.89.1.1, from
// Podman's DefaultAddressPools base 10.89.0.0/16). Podman's docker-compat
// `/info` mimics Docker (no "podman" marker: ServerVersion "5.6.2",
// OperatingSystem "fedora", plus "docker" strings like DockerRootDir), so
// `docker info` alone misclassifies it as `docker`. `docker version` still
// names the "Podman Engine" component, which reclassifies it as unsupported so
// the preflight gate rejects it before any gateway launch.
//
// Fixtures below are trimmed from live captures on an Apple Silicon macOS host
// running `podman machine` behind `/var/run/docker.sock`.
const PODMAN_COMPAT_DOCKER_INFO = JSON.stringify({
ServerVersion: "5.6.2",
OperatingSystem: "fedora",
OSType: "linux",
Architecture: "arm64",
Name: "localhost.localdomain",
DefaultRuntime: "crun",
ProductLicense: "Apache-2.0",
DockerRootDir: "/var/lib/containers/storage",
DefaultAddressPools: [{ Base: "10.89.0.0/16", Size: 24 }],
ClientInfo: { Platform: { Name: "Docker Engine - Community" }, Context: "default" },
});
const PODMAN_COMPAT_DOCKER_VERSION = JSON.stringify({
Client: { Platform: { Name: "Docker Engine - Community" }, Version: "29.3.1" },
Server: {
Platform: { Name: "linux/arm64/fedora-42" },
Version: "5.6.2",
Components: [
{ Name: "Podman Engine", Version: "5.6.2" },
{ Name: "Conmon", Version: "conmon version 2.1.13" },
{ Name: "OCI Runtime (crun)", Version: "crun version 1.23.1" },
],
},
});

describe("assessHost Podman docker-compat detection (#7320)", () => {
it("reclassifies Podman behind the Docker compatibility socket as unsupported", () => {
const result = assessHost({
platform: "darwin",
env: {},
dockerInfoOutput: PODMAN_COMPAT_DOCKER_INFO,
dockerVersionOutput: PODMAN_COMPAT_DOCKER_VERSION,
commandExistsImpl: (name: string) => name === "docker",
});

expect(result.dockerReachable).toBe(true);
expect(result.runtime).toBe("podman");
expect(result.isUnsupportedRuntime).toBe(true);
});

it("reclassifies Podman from docker info ProductLicense when the version probe is empty", () => {
const result = assessHost({
platform: "darwin",
env: {},
dockerInfoOutput: PODMAN_COMPAT_DOCKER_INFO,
dockerVersionOutput: "",
commandExistsImpl: (name: string) => name === "docker",
});

expect(result.dockerReachable).toBe(true);
expect(result.runtime).toBe("podman");
expect(result.isUnsupportedRuntime).toBe(true);
});

it("keeps real Docker classified as supported (no Podman false positive)", () => {
const realDockerInfo = JSON.stringify({
ServerVersion: "29.6.2",
OperatingSystem: "Ubuntu 24.04.3 LTS",
OSType: "linux",
Architecture: "x86_64",
DefaultRuntime: "runc",
DockerRootDir: "/var/lib/docker",
DefaultAddressPools: [{ Base: "192.168.240.0/20", Size: 24 }],
ClientInfo: { Platform: { Name: "Docker Engine - Community" } },
});
const realDockerVersion = JSON.stringify({
Client: { Platform: { Name: "Docker Engine - Community" } },
Server: {
Platform: { Name: "Docker Engine - Community" },
Version: "29.6.2",
Components: [
{ Name: "Engine", Version: "29.6.2" },
{ Name: "containerd", Version: "v2.2.6" },
{ Name: "runc", Version: "1.3.6" },
{ Name: "docker-init", Version: "0.19.0" },
],
},
});
const result = assessHost({
platform: "darwin",
env: {},
dockerInfoOutput: realDockerInfo,
dockerVersionOutput: realDockerVersion,
commandExistsImpl: (name: string) => name === "docker",
});

expect(result.dockerReachable).toBe(true);
expect(result.runtime).toBe("docker");
expect(result.isUnsupportedRuntime).toBe(false);
});
});
100 changes: 100 additions & 0 deletions src/lib/onboard/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export interface AssessHostOpts {
procVersion?: string;
dockerInfoOutput?: string;
dockerInfoError?: string;
dockerVersionOutput?: string;
readFileImpl?: (filePath: string, encoding: BufferEncoding) => string;
readdirImpl?: (dir: string) => string[];
runCaptureImpl?: RunCaptureFn;
Expand Down Expand Up @@ -215,6 +216,83 @@ function inferContainerRuntime(info = ""): ContainerRuntime {
return "unknown";
}

/**
* Detect Podman fronting the Docker CLI compatibility socket from the explicit
* `docker version --format '{{json .}}'` engine banner.
*
* Podman's docker-compat `/info` endpoint mimics Docker so closely that
* `docker info` carries no "podman" marker (observed on Apple Silicon macOS:
* `ServerVersion: "5.6.2"`, `OperatingSystem: "fedora"`, no "podman"
* substring), so `inferContainerRuntime` misclassifies it as plain Docker.
* The docker-compat `/version` payload still names the engine: a
* `Server.Components[].Name` of "Podman Engine" and a `Server.Platform.Name`
* like "linux/arm64/fedora-42". Real Docker reports
* `Server.Platform.Name: "Docker Engine - Community"` and components
* `Engine`/`containerd`/`runc`, so this stays false on Docker Engine,
* Docker Desktop, and Colima (#7320).
*/
function dockerVersionReportsPodman(versionOutput = ""): boolean {
const text = String(versionOutput || "").trim();
if (!text) return false;
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
// Plain-text `docker version` still prints the "Podman Engine" server banner.
return /podman/i.test(text);
}
const server = (parsed as Record<string, unknown> | null)?.Server;
if (!server || typeof server !== "object") return false;
const s = server as Record<string, unknown>;
const platformName = (s.Platform as Record<string, unknown> | undefined)?.Name;
if (typeof platformName === "string" && /podman/i.test(platformName)) return true;
const components = s.Components;
return (
Array.isArray(components) &&
components.some((component) => {
const name =
component && typeof component === "object"
? (component as Record<string, unknown>).Name
: undefined;
return typeof name === "string" && /podman/i.test(name);
})
);
}

/**
* Backstop Podman signal from `docker info --format '{{json .}}'` when the
* `docker version` probe is unavailable: Podman's docker-compat `/info` reports
* `ProductLicense: "Apache-2.0"` (Podman is Apache-2.0 licensed), whereas Docker
* Engine omits `ProductLicense` (Docker CE) or reports a Docker license string.
* Observed Apache-2.0 on the reporter-equivalent Podman machine (#7320).
*/
function dockerInfoReportsPodmanCompat(infoOutput = ""): boolean {
const text = String(infoOutput || "").trim();
if (!text) return false;
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
return false;
}
if (!parsed || typeof parsed !== "object") return false;
const license = (parsed as Record<string, unknown>).ProductLicense;
return typeof license === "string" && license.trim().toLowerCase() === "apache-2.0";
}

/**
* Reclassify a Docker CLI routed to Podman's compatibility socket as `podman`
* so the unsupported-runtime gate fires before the forced macOS/Linux
* Docker-driver gateway binds Podman's VM-only bridge address and exits
* `EADDRNOTAVAIL` (#7320).
*/
function isDockerCompatPodman(dockerInfoOutput = "", dockerVersionOutput = ""): boolean {
return (
dockerVersionReportsPodman(dockerVersionOutput) ||
dockerInfoReportsPodmanCompat(dockerInfoOutput)
);
}

function parseDockerCgroupVersion(info = ""): "v1" | "v2" | "unknown" {
if (/"CgroupVersion"\s*:\s*"2"/.test(info) || /CgroupVersion["=: ]+2/i.test(info)) {
return "v2";
Expand Down Expand Up @@ -536,6 +614,16 @@ export function assessHost(opts: AssessHostOpts = {}): HostAssessment {
dockerRunning = true;
}

// Capture the docker-compat engine banner so Podman fronting the Docker CLI
// socket is reclassified below. Only probed when the daemon is reachable so a
// down/absent Docker never pays for the extra call (#7320).
let dockerVersionOutput = opts.dockerVersionOutput;
if (dockerReachable && dockerVersionOutput === undefined) {
dockerVersionOutput = runCaptureImpl(["docker", "version", "--format", "{{json .}}"], {
ignoreError: true,
});
}

const release = opts.release ?? os.release();
const procVersion =
opts.procVersion ??
Expand All @@ -547,6 +635,18 @@ export function assessHost(opts: AssessHostOpts = {}): HostAssessment {
}
})();
let runtime = inferContainerRuntime(dockerInfoOutput);
// Podman fronting the Docker CLI compatibility socket mimics Docker in
// `docker info`, so the grep above misclassifies it as `docker`. Reclassify
// from the explicit docker-compat signals so the unsupported-runtime gate
// fires before the forced Docker-driver gateway binds Podman's VM-only
// bridge IP and exits EADDRNOTAVAIL (#7320).
if (
dockerReachable &&
runtime !== "podman" &&
isDockerCompatPodman(dockerInfoOutput, dockerVersionOutput)
) {
runtime = "podman";
}
if (dockerReachable && runtime === "unknown" && platform === "linux") {
runtime = "docker";
}
Expand Down
Loading