// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini /** * Real Docker Engine helpers: uniquely named, labelled test containers with guaranteed cleanup. * * Every container is named `dpsapi-e2e---` and labelled * `dpsapi.e2e=` + `dpsapi.e2e.test=`. Assertions must only ever look at these * names/labels — the host runs unrelated containers. */ import { ALPINE_IMAGE, LABEL_RUN, LABEL_TEST, RESOURCE_PREFIX, runId } from './env'; import { HarnessError } from './errors'; import { mapLimit, runProcess, toolEnv, type ProcResult } from './proc'; export interface DockerCliOptions { /** Default 60 s. */ timeoutMs?: number; /** Return non-zero results instead of throwing HarnessError('infrastructure'). */ allowFailure?: boolean; input?: string | Buffer; } /** Runs the docker CLI. Non-zero exit is a HarnessError unless allowFailure. */ export async function dockerCli(args: string[], opts: DockerCliOptions = {}): Promise { const r = await runProcess('docker', args, { env: toolEnv(), timeoutMs: opts.timeoutMs ?? 60_000, input: opts.input, label: `docker ${args[0]}` }); if (r.exitCode !== 0 && !opts.allowFailure) { throw new HarnessError('infrastructure', `docker ${args.join(' ')} failed (${r.exitCode}): ${r.stderr.trim()}`, r); } return r; } export interface TestContainer { id: string; name: string; image: string; labels: Record; command: string[]; } export interface RunContainerOptions { /** Name suffix (default c); final name dpsapi-e2e---. */ suffix?: string; /** Default alpine:3.22. */ image?: string; /** Default ['sleep', '3600']. */ command?: string[]; /** Extra labels (the run/test labels are always added). */ labels?: Record; env?: Record; /** --init so SIGTERM stops sleep-style commands promptly (default true). */ init?: boolean; /** Default 'none'. */ network?: string; extraArgs?: string[]; } export interface OwnContainer { id: string; name: string; state: string; labels: Record; } let seq = 0; export class RealDocker { readonly runId = runId(); /** Prefix of every container name created through this instance. */ readonly namePrefix: string; private readonly created = new Set(); constructor(readonly testKey: string) { this.namePrefix = `${RESOURCE_PREFIX}${this.runId}-${testKey}-`; } /** Full container name for a suffix. */ name(suffix: string): string { return this.namePrefix + suffix; } /** Run + test labels (+ extra). */ labels(extra: Record = {}): Record { return { [LABEL_RUN]: this.runId, [LABEL_TEST]: this.testKey, ...extra }; } /** `docker run -d` a labelled container and return its identity. */ async run(opts: RunContainerOptions = {}): Promise { const suffix = opts.suffix ?? `c${++seq}`; if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(suffix)) throw new HarnessError('infrastructure', `invalid container suffix ${suffix}`); const name = this.name(suffix); const image = opts.image ?? ALPINE_IMAGE; const command = opts.command ?? ['sleep', '3600']; const labels = this.labels(opts.labels); const args = ['run', '-d', '--name', name, '--network', opts.network ?? 'none']; if (opts.init ?? true) args.push('--init'); for (const [k, v] of Object.entries(labels)) args.push('--label', `${k}=${v}`); for (const [k, v] of Object.entries(opts.env ?? {})) args.push('-e', `${k}=${v}`); args.push(...(opts.extraArgs ?? []), image, ...command); this.created.add(name); const r = await dockerCli(args); return { id: r.stdout.trim(), name, image, labels, command }; } /** Starts n containers with bounded concurrency; optsFor(i) customizes each. */ runMany(n: number, optsFor: (i: number) => RunContainerOptions = () => ({}), concurrency = 6): Promise { return mapLimit( Array.from({ length: n }, (_, i) => i), concurrency, (i) => this.run(optsFor(i)), ); } /** docker stop -t (default 2). */ async stop(c: TestContainer | string, timeoutSec = 2): Promise { await dockerCli(['stop', '-t', String(timeoutSec), typeof c === 'string' ? c : c.name]); } async start(c: TestContainer | string): Promise { await dockerCli(['start', typeof c === 'string' ? c : c.name]); } /** docker rm (-f by default). */ async remove(c: TestContainer | string, force = true): Promise { await dockerCli(['rm', ...(force ? ['-f'] : []), typeof c === 'string' ? c : c.name]); } async inspect(c: TestContainer | string): Promise> { const r = await dockerCli(['inspect', typeof c === 'string' ? c : c.name]); return (JSON.parse(r.stdout) as Array>)[0]; } /** Containers (any state by default) carrying this test's label. */ async listOwn({ all = true }: { all?: boolean } = {}): Promise { return listByLabel(`${LABEL_TEST}=${this.testKey}`, all); } /** Full IDs of this test's running containers (what `docker ps` would show). */ async runningIds(): Promise { return (await this.listOwn({ all: false })).map((c) => c.id); } /** Force-removes every container carrying this test's label (and any name created here). Returns removed names. */ async cleanup(): Promise { const own = await this.listOwn(); const names = new Set([...own.map((c) => c.name)]); if (names.size === 0) return []; await dockerCli(['rm', '-f', '-v', ...names], { allowFailure: true, timeoutMs: 120_000 }); const left = await this.listOwn(); if (left.length) throw new HarnessError('leak', `containers survived cleanup: ${left.map((c) => c.name).join(', ')}`); return [...names]; } } /** docker ps [-a] --filter label=