1import * as Effect from "effect/Effect";
2import * as NodeChildProcess from "node:child_process";
3import * as NodeNet from "node:net";
4
5const swarmLocalNodeState = (): string | undefined => {
6 const result = NodeChildProcess.spawnSync(
7 "docker",
8 ["info", "--format", "{{.Swarm.LocalNodeState}}"],
9 { stdio: ["ignore", "pipe", "ignore"] },
10 );
11 return result.status === 0 ? String(result.stdout).trim() : undefined;
12};
13
14/**
15 * Idempotently provision the local single-node swarm the `Docker.Service`
16 * tests deploy to. A single-node manager on the local engine is harmless —
17 * regular (non-swarm) docker usage is unaffected — and stays active across
18 * runs so repeated invocations are a no-op. Deactivate manually with
19 * `docker swarm leave --force`.
20 *
21 * Concurrent test files race the init; the loser's "already part of a swarm"
22 * error is folded into success by re-checking the node state.
23 */
24export const ensureDockerSwarm: Effect.Effect<void, Error> = Effect.suspend(
25 () => {
26 if (swarmLocalNodeState() === "active") return Effect.void;
27 const init = NodeChildProcess.spawnSync(
28 "docker",
29 // 127.0.0.1 keeps init deterministic on hosts with several network
30 // interfaces (init otherwise refuses to pick an advertise address).
31 ["swarm", "init", "--advertise-addr", "127.0.0.1"],
32 { stdio: ["ignore", "ignore", "pipe"] },
33 );
34 if (init.status === 0 || swarmLocalNodeState() === "active") {
35 return Effect.void;
36 }
37 return Effect.fail(
38 new Error(`docker swarm init failed: ${String(init.stderr).trim()}`),
39 );
40 },
41);
42
43export const findAvailablePort = () =>
44 Effect.callback<number, Error>((resume) => {
45 const server = NodeNet.createServer();
46 server.unref();
47 server.on("error", (error) => resume(Effect.fail(error)));
48 server.listen(0, "127.0.0.1", () => {
49 const address = server.address();
50 const port =
51 typeof address === "object" && address ? address.port : undefined;
52 server.close((error) => {
53 if (error) {
54 resume(Effect.fail(error));
55 } else if (port) {
56 resume(Effect.succeed(port));
57 } else {
58 resume(Effect.fail(new Error("Failed to allocate a free host port")));
59 }
60 });
61 });
62 });