Skip to content

Commit 906780d

Browse files
committed
try to get docker socket from docker context ls
1 parent 8e9906c commit 906780d

File tree

8 files changed

+218
-52
lines changed

8 files changed

+218
-52
lines changed

packages/containers-shared/src/utils.ts

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { execFile, spawn } from "child_process";
1+
import { execFileSync, spawn } from "child_process";
22
import { randomUUID } from "crypto";
33
import { existsSync, statSync } from "fs";
44
import path from "path";
@@ -61,22 +61,15 @@ export const runDockerCmd = (
6161
};
6262
};
6363

64-
export const runDockerCmdWithOutput = async (
65-
dockerPath: string,
66-
args: string[]
67-
): Promise<string> => {
68-
return new Promise((resolve, reject) => {
69-
execFile(dockerPath, args, (error, stdout) => {
70-
if (error) {
71-
return reject(
72-
new Error(
73-
`Failed running docker command: ${error.message}. Command: ${dockerPath} ${args.join(" ")}`
74-
)
75-
);
76-
}
77-
return resolve(stdout.trim());
78-
});
79-
});
64+
export const runDockerCmdWithOutput = (dockerPath: string, args: string[]) => {
65+
try {
66+
const stdout = execFileSync(dockerPath, args, { encoding: "utf8" });
67+
return stdout.trim();
68+
} catch (error) {
69+
throw new Error(
70+
`Failed running docker command: ${(error as Error).message}. Command: ${dockerPath} ${args.join(" ")}`
71+
);
72+
}
8073
};
8174

8275
/** throws when docker is not installed */
@@ -209,7 +202,7 @@ export const getContainerIdsFromImage = async (
209202
dockerPath: string,
210203
ancestorImage: string
211204
) => {
212-
const output = await runDockerCmdWithOutput(dockerPath, [
205+
const output = runDockerCmdWithOutput(dockerPath, [
213206
"ps",
214207
"-a",
215208
"--filter",
@@ -250,3 +243,88 @@ export async function checkExposedPorts(
250243
export function generateContainerBuildId() {
251244
return randomUUID().slice(0, 8);
252245
}
246+
247+
type DockerContext = {
248+
Current: boolean;
249+
Description: string;
250+
DockerEndpoint: string;
251+
Error: string;
252+
Name: string;
253+
};
254+
255+
/**
256+
* Run `docker context ls` to get the Docker socket from the currently active Docker context
257+
*/
258+
export function getDockerSocketFromContext(dockerPath: string) {
259+
try {
260+
const output = runDockerCmdWithOutput(dockerPath, [
261+
"context",
262+
"ls",
263+
"--format",
264+
"json",
265+
]);
266+
267+
// Parse each line as a separate JSON object
268+
const lines = output.trim().split("\n");
269+
const contexts: DockerContext[] = lines.map((line) => JSON.parse(line));
270+
271+
// Find the current context
272+
const currentContext = contexts.find((context) => context.Current === true);
273+
274+
if (
275+
currentContext &&
276+
currentContext.DockerEndpoint &&
277+
typeof currentContext.DockerEndpoint === "string"
278+
) {
279+
return currentContext.DockerEndpoint;
280+
}
281+
} catch {
282+
// Fall back to null if docker context inspection fails
283+
}
284+
return null;
285+
}
286+
/**
287+
* Resolve Docker host as follows:
288+
* 1. Check WRANGLER_DOCKER_HOST environment variable
289+
* 2. Check DOCKER_HOST environment variable
290+
* 3. Try to get socket from active Docker context
291+
* 4. Fall back to platform-specific defaults
292+
*/
293+
export function resolveDockerHost(dockerPath: string) {
294+
if (process.env.WRANGLER_DOCKER_HOST) {
295+
return process.env.WRANGLER_DOCKER_HOST;
296+
}
297+
298+
if (process.env.DOCKER_HOST) {
299+
return process.env.DOCKER_HOST;
300+
}
301+
302+
// 3. Try to get socket from by running `docker context ls`
303+
try {
304+
const contextSocket = getDockerSocketFromContext(dockerPath);
305+
if (contextSocket) {
306+
return contextSocket;
307+
}
308+
} catch {}
309+
310+
// 4. Fall back to platform-specific defaults
311+
// (note windows doesn't work yet due to a runtime limitation)
312+
return process.platform === "win32"
313+
? "//./pipe/docker_engine"
314+
: "unix:///var/run/docker.sock";
315+
}
316+
317+
/**
318+
*
319+
* Get docker host from environment variables or platform defaults.
320+
* Does not use the docker context ls command, so we
321+
*/
322+
export const getDockerHostFromEnv = () => {
323+
const fromEnv = process.env.WRANGLER_DOCKER_HOST ?? process.env.DOCKER_HOST;
324+
if (fromEnv) {
325+
return fromEnv;
326+
}
327+
return process.platform === "win32"
328+
? "//./pipe/docker_engine"
329+
: "unix:///var/run/docker.sock";
330+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { resolveDockerHost } from "../src/utils";
3+
4+
const mockedDockerContextLsOutput = `{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///current/run/docker.sock","Error":"","Name":"default"}
5+
{"Current":false,"Description":"Docker Desktop","DockerEndpoint":"unix:///other/run/docker.sock","Error":"","Name":"desktop-linux"}`;
6+
7+
vi.mock("node:child_process");
8+
9+
describe("resolveDockerHost", () => {
10+
let mockExecFileSync: ReturnType<typeof vi.fn>;
11+
12+
beforeEach(async () => {
13+
vi.clearAllMocks();
14+
const childProcess = await import("node:child_process");
15+
mockExecFileSync = vi.mocked(childProcess.execFileSync);
16+
17+
mockExecFileSync.mockReturnValue(mockedDockerContextLsOutput);
18+
});
19+
20+
afterEach(() => {
21+
vi.unstubAllEnvs();
22+
});
23+
24+
it("should return WRANGLER_DOCKER_HOST when set", async () => {
25+
vi.stubEnv("WRANGLER_DOCKER_HOST", "unix:///foo/wrangler/socket");
26+
vi.stubEnv("DOCKER_HOST", "unix:///bar/docker/socket");
27+
28+
const result = await resolveDockerHost("/no/op/docker");
29+
expect(result).toBe("unix:///foo/wrangler/socket");
30+
31+
expect(mockExecFileSync).not.toHaveBeenCalled();
32+
});
33+
34+
it("should return DOCKER_HOST when WRANGLER_DOCKER_HOST is not set", async () => {
35+
expect(process.env.WRANGLER_DOCKER_HOST).toBeUndefined();
36+
vi.stubEnv("DOCKER_HOST", "unix:///bar/docker/socket");
37+
38+
const result = await resolveDockerHost("/no/op/docker");
39+
expect(result).toBe("unix:///bar/docker/socket");
40+
41+
expect(mockExecFileSync).not.toHaveBeenCalled();
42+
});
43+
44+
it("should use Docker context when no env vars are set", async () => {
45+
const result = await resolveDockerHost("/no/op/docker");
46+
expect(result).toBe("unix:///current/run/docker.sock");
47+
expect(mockExecFileSync).toHaveBeenCalledWith(
48+
"/no/op/docker",
49+
["context", "ls", "--format", "json"],
50+
{ encoding: 'utf8' }
51+
);
52+
});
53+
54+
it("should fall back to platform default on Unix when context fails", async () => {
55+
mockExecFileSync.mockImplementation(() => {
56+
throw new Error("Docker command failed");
57+
});
58+
59+
const result = await resolveDockerHost("/no/op/docker");
60+
expect(result).toBe("unix:///var/run/docker.sock");
61+
});
62+
});

packages/wrangler/src/__tests__/dev.test.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import type { Mock, MockInstance } from "vitest";
3434
vi.mock("../api/startDevWorker/ConfigController", (importOriginal) =>
3535
importOriginal()
3636
);
37-
37+
vi.mock("node:child_process");
3838
vi.mock("../dev/hotkeys");
3939

4040
vi.mock("@cloudflare/containers-shared", async (importOriginal) => {
@@ -1245,16 +1245,44 @@ describe.sequential("wrangler dev", () => {
12451245
});
12461246

12471247
describe("container engine", () => {
1248-
it("should default to docker socket", async () => {
1248+
const minimalContainerConfig = {
1249+
durable_objects: {
1250+
bindings: [
1251+
{
1252+
name: "EXAMPLE_DO_BINDING",
1253+
class_name: "ExampleDurableObject",
1254+
},
1255+
],
1256+
},
1257+
migrations: [{ tag: "v1", new_sqlite_classes: ["ExampleDurableObject"] }],
1258+
containers: [
1259+
{
1260+
name: "my-container",
1261+
max_instances: 10,
1262+
class_name: "ExampleDurableObject",
1263+
image: "docker.io/hello:world",
1264+
},
1265+
],
1266+
};
1267+
let mockExecFileSync: ReturnType<typeof vi.fn>;
1268+
const mockedDockerContextLsOutput = `{"Current":true,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///current/run/docker.sock","Error":"","Name":"default"}
1269+
{"Current":false,"Description":"Docker Desktop","DockerEndpoint":"unix:///other/run/docker.sock","Error":"","Name":"desktop-linux"}`;
1270+
1271+
beforeEach(async () => {
1272+
const childProcess = await import("node:child_process");
1273+
mockExecFileSync = vi.mocked(childProcess.execFileSync);
1274+
1275+
mockExecFileSync.mockReturnValue(mockedDockerContextLsOutput);
1276+
});
1277+
it("should default to socket of current docker context", async () => {
12491278
writeWranglerConfig({
12501279
main: "index.js",
1280+
...minimalContainerConfig,
12511281
});
12521282
fs.writeFileSync("index.js", `export default {};`);
12531283
const config = await runWranglerUntilConfig("dev");
12541284
expect(config.dev.containerEngine).toEqual(
1255-
process.platform === "win32"
1256-
? "//./pipe/docker_engine"
1257-
: "unix:///var/run/docker.sock"
1285+
"unix:///current/run/docker.sock"
12581286
);
12591287
});
12601288

@@ -1265,6 +1293,7 @@ describe.sequential("wrangler dev", () => {
12651293
port: 8888,
12661294
container_engine: "test.sock",
12671295
},
1296+
...minimalContainerConfig,
12681297
});
12691298
fs.writeFileSync("index.js", `export default {};`);
12701299

@@ -1277,6 +1306,7 @@ describe.sequential("wrangler dev", () => {
12771306
dev: {
12781307
port: 8888,
12791308
},
1309+
...minimalContainerConfig,
12801310
});
12811311
fs.writeFileSync("index.js", `export default {};`);
12821312
vi.stubEnv("WRANGLER_DOCKER_HOST", "blah.sock");

packages/wrangler/src/api/dev.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import events from "node:events";
22
import { fetch, Request } from "undici";
33
import { startDev } from "../dev";
4-
import {
5-
getDockerHost,
6-
getDockerPath,
7-
} from "../environment-variables/misc-variables";
4+
import { getDockerPath } from "../environment-variables/misc-variables";
85
import { run } from "../experimental-flags";
96
import { logger } from "../logger";
107
import type { Environment } from "../config";
@@ -165,6 +162,8 @@ export async function unstable_dev(
165162
const defaultLogLevel = testMode ? "warn" : "log";
166163
const local = options?.local ?? true;
167164

165+
const dockerPath = options?.experimental?.dockerPath ?? getDockerPath();
166+
168167
const devOptions: StartDevOptions = {
169168
script: script,
170169
inspect: false,
@@ -227,8 +226,8 @@ export async function unstable_dev(
227226
enableIpc: options?.experimental?.enableIpc,
228227
nodeCompat: undefined,
229228
enableContainers: options?.experimental?.enableContainers ?? false,
230-
dockerPath: options?.experimental?.dockerPath ?? getDockerPath(),
231-
containerEngine: options?.experimental?.containerEngine ?? getDockerHost(),
229+
dockerPath,
230+
containerEngine: options?.experimental?.containerEngine,
232231
};
233232

234233
//outside of test mode, rebuilds work fine, but only one instance of wrangler will work at a time

packages/wrangler/src/api/integrations/platform/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { resolveDockerHost } from "@cloudflare/containers-shared";
12
import { kCurrentWorker, Miniflare } from "miniflare";
23
import { getAssetsOptions, NonExistentAssetsDirError } from "../../../assets";
34
import { readConfig } from "../../../config";
@@ -12,7 +13,7 @@ import {
1213
buildSitesOptions,
1314
getImageNameFromDOClassName,
1415
} from "../../../dev/miniflare";
15-
import { getDockerHost } from "../../../environment-variables/misc-variables";
16+
import { getDockerPath } from "../../../environment-variables/misc-variables";
1617
import { logger } from "../../../logger";
1718
import { getSiteAssetPaths } from "../../../sites";
1819
import { dedent } from "../../../utils/dedent";
@@ -460,11 +461,15 @@ export function unstable_getMiniflareWorkerOptions(
460461
? buildAssetOptions({ assets: processedAssetOptions })
461462
: {};
462463

464+
const useContainers =
465+
config.dev?.enable_containers && config.containers?.length;
463466
const workerOptions: SourcelessWorkerOptions = {
464467
compatibilityDate: config.compatibility_date,
465468
compatibilityFlags: config.compatibility_flags,
466469
modulesRules,
467-
containerEngine: config.dev.container_engine ?? getDockerHost(),
470+
containerEngine: useContainers
471+
? config.dev.container_engine ?? resolveDockerHost(getDockerPath())
472+
: undefined,
468473

469474
...bindingOptions,
470475
...sitesOptions,

packages/wrangler/src/api/startDevWorker/ConfigController.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from "node:assert";
22
import path from "node:path";
3+
import { resolveDockerHost } from "@cloudflare/containers-shared";
34
import { watch } from "chokidar";
45
import { getAssetsOptions, validateAssetsArgsAndConfig } from "../../assets";
56
import { fillOpenAPIConfiguration } from "../../cloudchamber/common";
@@ -15,10 +16,7 @@ import {
1516
} from "../../dev";
1617
import { getClassNamesWhichUseSQLite } from "../../dev/class-names-sqlite";
1718
import { getLocalPersistencePath } from "../../dev/get-local-persistence-path";
18-
import {
19-
getDockerHost,
20-
getDockerPath,
21-
} from "../../environment-variables/misc-variables";
19+
import { getDockerPath } from "../../environment-variables/misc-variables";
2220
import { UserError } from "../../errors";
2321
import { getFlag } from "../../experimental-flags";
2422
import { logger, runWithLogLevel } from "../../logger";
@@ -119,6 +117,9 @@ async function resolveDevConfig(
119117

120118
const initialIpListenCheck = initialIp === "*" ? "0.0.0.0" : initialIp;
121119

120+
const useContainers =
121+
config.dev.enable_containers && config.containers?.length;
122+
122123
return {
123124
auth,
124125
remote: input.dev?.remote,
@@ -160,10 +161,11 @@ async function resolveDevConfig(
160161
enableContainers:
161162
input.dev?.enableContainers ?? config.dev.enable_containers,
162163
dockerPath: input.dev?.dockerPath ?? getDockerPath(),
163-
containerEngine:
164-
input.dev?.containerEngine ??
165-
config.dev.container_engine ??
166-
getDockerHost(),
164+
containerEngine: useContainers
165+
? input.dev?.containerEngine ??
166+
config.dev.container_engine ??
167+
resolveDockerHost(input.dev?.dockerPath ?? getDockerPath())
168+
: undefined,
167169
containerBuildId: input.dev?.containerBuildId,
168170
} satisfies StartDevWorkerOptions["dev"];
169171
}

packages/wrangler/src/environment-variables/misc-variables.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,3 @@ export const getDockerPath = getEnvironmentVariableFactory({
273273
return "docker";
274274
},
275275
});
276-
277-
/**
278-
* `WRANGLER_DOCKER_HOST` specifies the Docker socket to connect to.
279-
*/
280-
export const getDockerHost = getEnvironmentVariableFactory({
281-
variableName: "WRANGLER_DOCKER_HOST",
282-
defaultValue() {
283-
return process.platform === "win32"
284-
? "//./pipe/docker_engine"
285-
: "unix:///var/run/docker.sock";
286-
},
287-
});

0 commit comments

Comments
 (0)