diff --git a/.github/docker-compose.ci.yml b/.github/docker-compose.ci.yml index 2989d7b..53f7a30 100644 --- a/.github/docker-compose.ci.yml +++ b/.github/docker-compose.ci.yml @@ -9,6 +9,8 @@ services: - "9138:9138" - "50019:50019" - "6473:6473" + - "4001:4001" + - "4001:4001/udp" volumes: - bitsocial-data:/data - bitsocial-logs:/logs diff --git a/Dockerfile b/Dockerfile index b2c8568..b5ea384 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,7 @@ ENV XDG_STATE_HOME=/logs ENV KUBO_RPC_URL="http://0.0.0.0:50019/api/v0" ENV IPFS_GATEWAY_URL="http://0.0.0.0:6473" -EXPOSE 9138 50019 6473 +EXPOSE 9138 50019 6473 4001 VOLUME ["/data", "/logs"] diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 400f44d..24e3924 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -18,9 +18,11 @@ services: container_name: bitsocial restart: unless-stopped ports: - - "9138:9138" # Plebbit RPC + Web UI - - "50019:50019" # Kubo IPFS API - - "6473:6473" # IPFS Gateway + - "9138:9138" # Plebbit RPC + Web UI + - "50019:50019" # Kubo IPFS API + - "6473:6473" # IPFS Gateway + - "4001:4001" # Kubo Swarm (TCP) + - "4001:4001/udp" # Kubo Swarm (QUIC + WebTransport + WebRTC-direct) volumes: - bitsocial-data:/data - bitsocial-logs:/logs diff --git a/docker-compose.yml b/docker-compose.yml index dbb3ff6..5a7de2b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,9 +19,11 @@ services: restart: unless-stopped stop_grace_period: 30s ports: - - "9138:9138" # Plebbit RPC + Web UI - - "50019:50019" # Kubo IPFS API - - "6473:6473" # IPFS Gateway + - "9138:9138" # Plebbit RPC + Web UI + - "50019:50019" # Kubo IPFS API + - "6473:6473" # IPFS Gateway + - "4001:4001" # Kubo Swarm (TCP) + - "4001:4001/udp" # Kubo Swarm (QUIC + WebTransport + WebRTC-direct) volumes: - bitsocial-data:/data - bitsocial-logs:/logs diff --git a/src/ipfs/startIpfs.ts b/src/ipfs/startIpfs.ts index 2d28e43..da9cb89 100644 --- a/src/ipfs/startIpfs.ts +++ b/src/ipfs/startIpfs.ts @@ -56,15 +56,7 @@ export async function mergeCliDefaultsIntoIpfsConfig(log: any, ipfsConfigPath: s Addresses: { ...(currentIpfsConfigFile["Addresses"] ?? {}), Gateway: `/ip4/${gatewayUrl.hostname}/tcp/${gatewayUrl.port}`, - API: `/ip4/${apiUrl.hostname}/tcp/${apiUrl.port}`, - Swarm: [ - "/ip4/0.0.0.0/tcp/0", - "/ip6/::/tcp/0", - "/ip4/0.0.0.0/udp/0/quic-v1", - "/ip4/0.0.0.0/udp/0/quic-v1/webtransport", - "/ip6/::/udp/0/quic-v1", - "/ip6/::/udp/0/quic-v1/webtransport" - ] + API: `/ip4/${apiUrl.hostname}/tcp/${apiUrl.port}` }, AutoTLS: { ...(currentIpfsConfigFile["AutoTLS"] ?? {}), diff --git a/test/helpers/daemon-helpers.ts b/test/helpers/daemon-helpers.ts index d64f648..0ec4e63 100644 --- a/test/helpers/daemon-helpers.ts +++ b/test/helpers/daemon-helpers.ts @@ -1,8 +1,10 @@ import { ChildProcess, spawn } from "child_process"; import net from "net"; +import path from "path"; import { directory as randomDirectory } from "tempy"; import WebSocket from "ws"; import defaults from "../../dist/common-utils/defaults.js"; +import { preInitKuboWithEphemeralSwarm } from "./kubo-helpers.js"; export type ManagedChildProcess = ChildProcess & { kuboRpcUrl?: URL; capturedStdout?: string; capturedStderr?: string }; @@ -72,7 +74,20 @@ export const startPkcDaemon = (args: string[], env?: Record): Pr const hasCustomDataPath = args.some((arg) => arg.startsWith("--pkcOptions.dataPath")); const hasCustomLogPath = args.some((arg) => arg === "--logPath"); const logPathArgs = hasCustomLogPath ? [] : ["--logPath", randomDirectory()]; - const daemonArgs = hasCustomDataPath ? args : ["--pkcOptions.dataPath", randomDirectory(), ...args]; + const dataPath = hasCustomDataPath + ? (args[args.findIndex((a) => a.startsWith("--pkcOptions.dataPath")) + 1] as string) + : randomDirectory(); + const daemonArgs = hasCustomDataPath ? args : ["--pkcOptions.dataPath", dataPath, ...args]; + + // Pre-init kubo so parallel test daemons don't collide on swarm port 4001. + const apiUrl = new URL(env?.KUBO_RPC_URL ?? defaults.KUBO_RPC_URL.toString()); + const gatewayUrl = new URL(env?.IPFS_GATEWAY_URL ?? defaults.IPFS_GATEWAY_URL.toString()); + try { + await preInitKuboWithEphemeralSwarm(path.join(dataPath, ".bitsocial-cli.ipfs"), apiUrl, gatewayUrl); + } catch (error) { + return reject(error); + } + const daemonProcess = spawn("node", ["./bin/run", "daemon", ...logPathArgs, ...daemonArgs], { stdio: ["pipe", "pipe", "pipe"], env: env ? { ...process.env, ...env } : undefined diff --git a/test/helpers/kubo-helpers.ts b/test/helpers/kubo-helpers.ts new file mode 100644 index 0000000..93cffab --- /dev/null +++ b/test/helpers/kubo-helpers.ts @@ -0,0 +1,39 @@ +import * as fs from "fs/promises"; +import path from "path"; +import { execFile } from "child_process"; +import { promisify } from "util"; +import { path as resolveKuboBinary } from "kubo"; +import { mergeCliDefaultsIntoIpfsConfig } from "../../src/ipfs/startIpfs.js"; + +const execFileAsync = promisify(execFile); + +const EPHEMERAL_SWARM_ADDRESSES = [ + "/ip4/0.0.0.0/tcp/0", + "/ip6/::/tcp/0", + "/ip4/0.0.0.0/udp/0/quic-v1", + "/ip4/0.0.0.0/udp/0/quic-v1/webtransport", + "/ip6/::/udp/0/quic-v1", + "/ip6/::/udp/0/quic-v1/webtransport" +]; + +// Pre-init a kubo repo so each parallel test daemon gets its own kernel-assigned +// swarm port instead of fighting over the default 4001. Mirrors what +// startKuboNode does on a fresh config (init + server profile + merge defaults), +// then overrides Swarm to ephemeral addresses. When the bitsocial daemon later +// runs `ipfs init` against this dir it'll bail with "configuration file already +// exists", skip mergeCliDefaultsIntoIpfsConfig, and spawn kubo with our Swarm. +export const preInitKuboWithEphemeralSwarm = async (ipfsDataPath: string, apiUrl: URL, gatewayUrl: URL) => { + await fs.mkdir(ipfsDataPath, { recursive: true }); + const kuboBinaryPath = await resolveKuboBinary(); + const env = { ...process.env, IPFS_PATH: ipfsDataPath }; + + await execFileAsync(kuboBinaryPath, ["init"], { env }); + await execFileAsync(kuboBinaryPath, ["config", "profile", "apply", "server"], { env }); + + const configPath = path.join(ipfsDataPath, "config"); + await mergeCliDefaultsIntoIpfsConfig(() => {}, configPath, apiUrl, gatewayUrl); + + const config = JSON.parse(await fs.readFile(configPath, "utf-8")); + config.Addresses = { ...(config.Addresses ?? {}), Swarm: EPHEMERAL_SWARM_ADDRESSES }; + await fs.writeFile(configPath, JSON.stringify(config, null, 4)); +}; diff --git a/test/kubo/kuboRpcGateway.integration.test.ts b/test/kubo/kuboRpcGateway.integration.test.ts index dd6bc08..04533e5 100644 --- a/test/kubo/kuboRpcGateway.integration.test.ts +++ b/test/kubo/kuboRpcGateway.integration.test.ts @@ -9,6 +9,7 @@ import { directory as tempDirectory } from "tempy"; import { setTimeout as delay } from "timers/promises"; import { promisify } from "util"; import { startKuboNode } from "../../src/ipfs/startIpfs.js"; +import { preInitKuboWithEphemeralSwarm } from "../helpers/kubo-helpers.js"; const execFileAsync = promisify(execFile); @@ -184,6 +185,8 @@ describe("kubo RPC + gateway integration", { timeout: 120_000 }, () => { apiUrl = new URL(`http://127.0.0.1:${apiPort}`); gatewayUrl = new URL(`http://127.0.0.1:${gatewayPort}`); + await preInitKuboWithEphemeralSwarm(ipfsRepoPath, apiUrl, gatewayUrl); + kuboProcess = await startKuboNode(apiUrl, gatewayUrl, dataPath); await waitForOkResponse(() => fetch(new URL("/api/v0/version", apiUrl), { method: "POST" })); diff --git a/test/kubo/mergeCliDefaultsIntoIpfsConfig.test.ts b/test/kubo/mergeCliDefaultsIntoIpfsConfig.test.ts index dc6e02f..7bb71a4 100644 --- a/test/kubo/mergeCliDefaultsIntoIpfsConfig.test.ts +++ b/test/kubo/mergeCliDefaultsIntoIpfsConfig.test.ts @@ -17,9 +17,19 @@ const writeConfigToTempFile = async (config: Record) => { describe("mergeCliDefaultsIntoIpfsConfig", () => { it("overrides core defaults on freshly initialized config", async () => { + const kuboDefaultSwarm = [ + "/ip4/0.0.0.0/tcp/4001", + "/ip6/::/tcp/4001", + "/ip4/0.0.0.0/udp/4001/webrtc-direct", + "/ip4/0.0.0.0/udp/4001/quic-v1", + "/ip4/0.0.0.0/udp/4001/quic-v1/webtransport", + "/ip6/::/udp/4001/webrtc-direct", + "/ip6/::/udp/4001/quic-v1", + "/ip6/::/udp/4001/quic-v1/webtransport" + ]; const initialConfig = { Addresses: { - Swarm: ["/ip4/0.0.0.0/tcp/4001"], + Swarm: kuboDefaultSwarm, Gateway: "/ip4/0.0.0.0/tcp/8080" } }; @@ -30,14 +40,7 @@ describe("mergeCliDefaultsIntoIpfsConfig", () => { const mergedConfig = JSON.parse(await fs.readFile(configPath, "utf-8")); expect(mergedConfig.Addresses.API).toBe("/ip4/127.0.0.1/tcp/5001"); expect(mergedConfig.Addresses.Gateway).toBe("/ip4/127.0.0.1/tcp/8080"); - expect(mergedConfig.Addresses.Swarm).toEqual([ - "/ip4/0.0.0.0/tcp/0", - "/ip6/::/tcp/0", - "/ip4/0.0.0.0/udp/0/quic-v1", - "/ip4/0.0.0.0/udp/0/quic-v1/webtransport", - "/ip6/::/udp/0/quic-v1", - "/ip6/::/udp/0/quic-v1/webtransport" - ]); + expect(mergedConfig.Addresses.Swarm).toEqual(kuboDefaultSwarm); expect(mergedConfig.AutoTLS.Enabled).toBe(true); });