| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as NodeChildProcess from "node:child_process"; |
| 3 | import * as NodeNet from "node:net"; |
| 4 | |
| 5 | const 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 | */ |
| 24 | export 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 | |
| 43 | export 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 | }); |