diff --git a/src/lib/onboard/preflight-podman-compat.test.ts b/src/lib/onboard/preflight-podman-compat.test.ts index ac8e48235bc..1666589b56b 100644 --- a/src/lib/onboard/preflight-podman-compat.test.ts +++ b/src/lib/onboard/preflight-podman-compat.test.ts @@ -71,13 +71,31 @@ describe("assessHost Podman docker-compat detection (#7320)", () => { expect(result.isUnsupportedRuntime).toBe(true); }); - it("keeps real Docker classified as supported (no Podman false positive)", () => { + it.each([ + ["an empty JSON object", "{}"], + ["unexpected plain text", "unexpected version output"], + ])("rejects a Podman-compatible runtime when version output is %s (#7320)", (_case, versionOutput) => { + const result = assessHost({ + platform: "darwin", + env: {}, + dockerInfoOutput: PODMAN_COMPAT_DOCKER_INFO, + dockerVersionOutput: versionOutput, + commandExistsImpl: (name: string) => name === "docker", + }); + + expect(result.dockerReachable).toBe(true); + expect(result.runtime).toBe("podman"); + expect(result.isUnsupportedRuntime).toBe(true); + }); + + it("keeps Docker Engine supported when ProductLicense reports Apache-2.0", () => { const realDockerInfo = JSON.stringify({ ServerVersion: "29.6.2", OperatingSystem: "Ubuntu 24.04.3 LTS", OSType: "linux", Architecture: "x86_64", DefaultRuntime: "runc", + ProductLicense: "Apache-2.0", DockerRootDir: "/var/lib/docker", DefaultAddressPools: [{ Base: "192.168.240.0/20", Size: 24 }], ClientInfo: { Platform: { Name: "Docker Engine - Community" } }, diff --git a/src/lib/onboard/preflight.ts b/src/lib/onboard/preflight.ts index 2f0bf89380c..f9a7003d305 100644 --- a/src/lib/onboard/preflight.ts +++ b/src/lib/onboard/preflight.ts @@ -216,9 +216,11 @@ function inferContainerRuntime(info = ""): ContainerRuntime { return "unknown"; } +type DockerVersionIdentity = "docker" | "podman" | "unknown"; + /** - * Detect Podman fronting the Docker CLI compatibility socket from the explicit - * `docker version --format '{{json .}}'` engine banner. + * Classify the engine identity from the explicit + * `docker version --format '{{json .}}'` banner. * * Podman's docker-compat `/info` endpoint mimics Docker so closely that * `docker info` carries no "podman" marker (observed on Apple Silicon macOS: @@ -228,43 +230,51 @@ function inferContainerRuntime(info = ""): ContainerRuntime { * `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). + * `Engine`/`containerd`/`runc`, which provide positive Docker identity on + * Docker Engine, Docker Desktop, and Colima (#7320). */ -function dockerVersionReportsPodman(versionOutput = ""): boolean { +function classifyDockerVersionIdentity(versionOutput = ""): DockerVersionIdentity { const text = String(versionOutput || "").trim(); - if (!text) return false; + if (!text) return "unknown"; 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); + if (/podman/i.test(text)) return "podman"; + return /docker engine/i.test(text) ? "docker" : "unknown"; } const server = (parsed as Record | null)?.Server; - if (!server || typeof server !== "object") return false; + if (!server || typeof server !== "object") return "unknown"; const s = server as Record; const platformName = (s.Platform as Record | undefined)?.Name; - if (typeof platformName === "string" && /podman/i.test(platformName)) return true; + if (typeof platformName === "string" && /podman/i.test(platformName)) return "podman"; const components = s.Components; - return ( - Array.isArray(components) && - components.some((component) => { - const name = - component && typeof component === "object" - ? (component as Record).Name - : undefined; - return typeof name === "string" && /podman/i.test(name); - }) - ); + const componentNames = Array.isArray(components) + ? components.flatMap((component) => { + const name = + component && typeof component === "object" + ? (component as Record).Name + : undefined; + return typeof name === "string" ? [name] : []; + }) + : []; + if (componentNames.some((name) => /podman/i.test(name))) return "podman"; + if ( + (typeof platformName === "string" && /^docker (?:engine|desktop)\b/i.test(platformName)) || + componentNames.some((name) => name.trim().toLowerCase() === "engine") + ) { + return "docker"; + } + return "unknown"; } /** - * 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). + * Use `ProductLicense: "Apache-2.0"` as a Podman backstop only when the + * authoritative `docker version` probe is unavailable. Podman's docker-compat + * `/info` reports this value, but Docker Engine can also report it. Without + * version evidence, preflight conservatively rejects that ambiguous runtime + * before the Docker-driver path (#7320). */ function dockerInfoReportsPodmanCompat(infoOutput = ""): boolean { const text = String(infoOutput || "").trim(); @@ -287,10 +297,10 @@ function dockerInfoReportsPodmanCompat(infoOutput = ""): boolean { * `EADDRNOTAVAIL` (#7320). */ function isDockerCompatPodman(dockerInfoOutput = "", dockerVersionOutput = ""): boolean { - return ( - dockerVersionReportsPodman(dockerVersionOutput) || - dockerInfoReportsPodmanCompat(dockerInfoOutput) - ); + const versionIdentity = classifyDockerVersionIdentity(dockerVersionOutput); + if (versionIdentity === "podman") return true; + if (versionIdentity === "docker") return false; + return dockerInfoReportsPodmanCompat(dockerInfoOutput); } function parseDockerCgroupVersion(info = ""): "v1" | "v2" | "unknown" {