commit 54c70d4c74c2707d9b94315bdef72c0925374407
parent 4fd94b10dcba620030e498bfcd1f09488e321bb1
Author: finwo <finwo@pm.me>
Date: Sun, 27 Sep 2026 20:07:19 +0200
Restore the end-to-end container suite against the task API
Diffstat:
2 files changed, 423 insertions(+), 428 deletions(-)
diff --git a/test/worker-docker.test.js b/test/worker-docker.test.js
@@ -0,0 +1,423 @@
+// test/worker-docker.test.js - the whole loop, for real
+
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs/promises';
+import os from 'node:os';
+import { execFile } from 'node:child_process';
+import { promisify } from 'node:util';
+import { startHarness, data, freePort } from './helpers/harness.js';
+import { createClient } from '../src/worker/client.js';
+import { createRuntime, containerName } from '../src/worker/docker.js';
+import { runTask } from '../src/worker/task.js';
+
+const execFileAsync = promisify(execFile);
+
+const IMAGE = process.env.CONDUCTOR_TEST_IMAGE || 'alpine:3';
+
+async function dockerUsable() {
+ if (process.env.CONDUCTOR_TEST_NO_DOCKER) return false;
+ try {
+ await execFileAsync('docker', ['version', '--format', '{{.Server.Version}}'], { timeout: 15000 });
+ await execFileAsync('docker', ['image', 'inspect', IMAGE], { timeout: 15000 })
+ .catch(() => execFileAsync('docker', ['pull', IMAGE], { timeout: 300000 }));
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+const available = await dockerUsable();
+const opts = { skip: available ? false : 'docker is not available' };
+
+const quiet = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} };
+
+// The port must settle before the server is built: task payloads carry
+// absolute callback URLs derived from public_url.
+async function listening(options = {}) {
+ const port = await freePort();
+ const url = `http://127.0.0.1:${port}`;
+ const h = await startHarness({ ...options, port, publicUrl: url });
+ await h.app.listen({ port, host: '127.0.0.1' });
+
+ const workerCfg = {
+ conductor_url: url,
+ name: 'docker-test-worker',
+ token: h.worker.token,
+ arches: [],
+ features: {},
+ concurrency: 1,
+ poll_interval: 1,
+ docker: 'docker',
+ shell: 'sh',
+ default_timeout: 300,
+ ...(options?.worker ?? {}),
+ };
+
+ return {
+ ...h,
+ url,
+ workerCfg,
+ client: createClient(workerCfg),
+ runtime: createRuntime(workerCfg, { logger: quiet }),
+ };
+}
+
+async function queue(h) {
+ const triggered = await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
+ const jobId = data(triggered).job_id;
+ await h.waitForJob(jobId);
+ return jobId;
+}
+
+async function logOf(h, taskId) {
+ const res = await h.app.inject({
+ method: 'GET', url: `/api/v1/tasks/${encodeURIComponent(taskId)}/log`,
+ });
+ return data(res).data;
+}
+
+async function artifactsOf(h, taskId) {
+ const res = await h.app.inject({
+ method: 'GET', url: `/api/v1/tasks/${encodeURIComponent(taskId)}/artifacts`,
+ });
+ return data(res);
+}
+
+function download(h, jobId, taskId, artifactId) {
+ return h.app.inject({
+ method: 'GET',
+ url: `/api/v1/projects/demo/jobs/${jobId}/tasks/${encodeURIComponent(taskId)}`
+ + `/artifacts/${artifactId}`,
+ });
+}
+
+async function stateOf(h, jobId) {
+ return data(await h.app.inject({ method: 'GET', url: `/api/v1/jobs/${jobId}` })).state;
+}
+
+async function exists(kind, name) {
+ const args = kind === 'network'
+ ? ['network', 'ls', '--filter', `name=^${name}$`, '--format', '{{.Name}}']
+ : ['ps', '-a', '--filter', `name=^${name}$`, '--format', '{{.Names}}'];
+ const { stdout } = await execFileAsync('docker', args);
+ return stdout.trim().length > 0;
+}
+
+test('a task runs in a container and its output reaches the conductor', opts, async () => {
+ const pipeline = `
+version: 1
+tasks:
+ hello:
+ image: ${IMAGE}
+ script:
+ - echo "hello from the task"
+ - echo "commit is $CONDUCTOR_SHA"
+ - echo "task is $CONDUCTOR_TASK"
+ - cat README.md
+`;
+ const h = await listening({ pipeline });
+ try {
+ const jobId = await queue(h);
+ const task = await h.client.claim({ arches: [], features: [], name: 'test' });
+ assert.ok(task, 'expected a task');
+
+ const result = await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+ assert.equal(result.success, true, `task failed: ${result.error}`);
+ assert.equal(result.exitCode, 0);
+
+ const log = await logOf(h, task.id);
+ assert.match(log, /\$ echo "hello from the task"/, 'each command should be echoed');
+ assert.match(log, /hello from the task/);
+ assert.match(log, new RegExp(`commit is ${h.sha}`));
+ assert.match(log, /task is hello/);
+ assert.match(log, /test repository/, 'the source tarball should have been unpacked');
+
+ assert.equal(await stateOf(h, jobId), 'success');
+ } finally {
+ await h.stop();
+ }
+});
+
+test('a failing command fails the task and preserves the log', opts, async () => {
+ const pipeline = `
+version: 1
+tasks:
+ boom:
+ image: ${IMAGE}
+ script:
+ - echo before
+ - exit 3
+ - echo after
+`;
+ const h = await listening({ pipeline });
+ try {
+ const jobId = await queue(h);
+ const task = await h.client.claim({ arches: [], features: [], name: 'test' });
+
+ const result = await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+ assert.equal(result.success, false);
+ assert.equal(result.exitCode, 3);
+
+ const log = await logOf(h, task.id);
+ assert.match(log, /before/);
+ assert.ok(!log.includes('after'), 'set -e must stop the script at the failure');
+
+ assert.equal(await stateOf(h, jobId), 'failed');
+ } finally {
+ await h.stop();
+ }
+});
+
+test('artifacts produced in the container are uploaded and downloadable', opts, async () => {
+ const pipeline = `
+version: 1
+tasks:
+ produce:
+ image: ${IMAGE}
+ script:
+ - mkdir -p dist/nested
+ - printf 'binary-content' > dist/app.bin
+ - printf 'deep' > dist/nested/deep.txt
+ artifacts:
+ paths: [dist/**]
+`;
+ const h = await listening({ pipeline });
+ try {
+ const jobId = await queue(h);
+ const task = await h.client.claim({ arches: [], features: [], name: 'test' });
+
+ const result = await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+ assert.equal(result.success, true, `task failed: ${result.error}`);
+
+ const artifacts = await artifactsOf(h, task.id);
+ assert.deepEqual(artifacts.map((a) => a.path).sort(), ['dist/app.bin', 'dist/nested/deep.txt']);
+
+ const binary = artifacts.find((a) => a.path === 'dist/app.bin');
+ assert.equal((await download(h, jobId, task.id, binary.id)).body, 'binary-content');
+ } finally {
+ await h.stop();
+ }
+});
+
+test('a worker feature injects a mount and environment the conductor never sees', opts, async () => {
+ const pipeline = `
+version: 1
+tasks:
+ signed:
+ image: ${IMAGE}
+ requires: [sign-key]
+ script:
+ - echo "key path is $SIGN_KEY"
+ - cat "$SIGN_KEY"
+`;
+ const h = await listening({
+ pipeline,
+ worker: {
+ features: {
+ 'sign-key': { mounts: [], env: { SIGN_KEY: '/keys/build.key' }, devices: [], privileged: false },
+ },
+ },
+ });
+
+ try {
+ await fs.mkdir(`${h.root}/keys`, { recursive: true });
+ await fs.writeFile(`${h.root}/keys/build.key`, 'PRIVATE-KEY-MATERIAL');
+ h.workerCfg.features['sign-key'].mounts = [`${h.root}/keys/build.key:/keys/build.key:ro`];
+
+ await queue(h);
+
+ const denied = await createClient({ ...h.workerCfg, token: h.worker.token })
+ .claim({ arches: [], features: [], name: 'test' });
+ assert.equal(denied, null, 'a worker lacking the feature must not be offered the task');
+
+ const task = await h.client.claim({ arches: [], features: ['sign-key'], name: 'test' });
+ assert.ok(task, 'expected the task once the feature is advertised');
+
+ const result = await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+ assert.equal(result.success, true, `task failed: ${result.error}`);
+
+ const log = await logOf(h, task.id);
+ assert.match(log, /key path is \/keys\/build\.key/);
+ assert.match(log, /PRIVATE-KEY-MATERIAL/);
+
+ const stored = await h.services.db.get(
+ 'SELECT requires, spec FROM tasks WHERE id = {id}', { id: task.id }
+ );
+ assert.equal(stored.requires, '["sign-key"]');
+ assert.ok(!stored.spec.includes('PRIVATE-KEY-MATERIAL'));
+ } finally {
+ await h.stop();
+ }
+});
+
+test('a service container is reachable from the task by its alias', opts, async () => {
+ const pipeline = `
+version: 1
+tasks:
+ talks:
+ image: ${IMAGE}
+ services:
+ - image: ${IMAGE}
+ alias: sidecar
+ command: [sleep, '60']
+ script:
+ - ping -c1 -W2 sidecar
+`;
+ const h = await listening({ pipeline });
+ try {
+ await queue(h);
+ const task = await h.client.claim({ arches: [], features: [], name: 'test' });
+
+ const result = await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+ const log = await logOf(h, task.id);
+ assert.equal(result.success, true, `task failed: ${result.error}\n${log}`);
+ assert.match(log, /1 packets received|1 received/);
+ } finally {
+ await h.stop();
+ }
+});
+
+test('a task that exceeds its timeout is stopped', opts, async () => {
+ const pipeline = `
+version: 1
+tasks:
+ slow:
+ image: ${IMAGE}
+ timeout: 2
+ script:
+ - echo starting
+ - sleep 60
+`;
+ const h = await listening({ pipeline });
+ try {
+ await queue(h);
+ const task = await h.client.claim({ arches: [], features: [], name: 'test' });
+
+ const started = Date.now();
+ const result = await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+ const elapsed = (Date.now() - started) / 1000;
+
+ assert.equal(result.success, false);
+ assert.equal(result.timedOut, true);
+ assert.ok(elapsed < 45, `should have been stopped promptly, took ${elapsed}s`);
+ assert.match(await logOf(h, task.id), /timed out/);
+ } finally {
+ await h.stop();
+ }
+});
+
+test('containers, networks and scratch directories are cleaned up', opts, async () => {
+ const pipeline = `
+version: 1
+tasks:
+ tidy:
+ image: ${IMAGE}
+ services:
+ - image: ${IMAGE}
+ alias: sidecar
+ command: [sleep, '60']
+ script:
+ - echo work
+`;
+ const h = await listening({ pipeline });
+ try {
+ const before = new Set(
+ (await fs.readdir(os.tmpdir())).filter((n) => n.startsWith('conductor-task-'))
+ );
+
+ await queue(h);
+ const task = await h.client.claim({ arches: [], features: [], name: 'test' });
+ await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+
+ assert.equal(await exists('container', containerName('conductor', task.id)), false,
+ 'the task container should be gone');
+ assert.equal(await exists('container', containerName('conductor-svc', task.id, 'sidecar')), false,
+ 'the service container should be gone');
+ assert.equal(await exists('network', containerName('conductor-net', task.id)), false,
+ 'the task network should be gone');
+
+ const leaked = (await fs.readdir(os.tmpdir()))
+ .filter((n) => n.startsWith('conductor-task-'))
+ .filter((n) => !before.has(n));
+ assert.deepEqual(leaked, [], 'the scratch directory should be removed');
+ } finally {
+ await h.stop();
+ }
+});
+
+test('a task runs with no mounts, and nothing of the worker is shared with it', opts, async () => {
+ const pipeline = `
+version: 1
+tasks:
+ isolated:
+ image: ${IMAGE}
+ script:
+ - pwd
+ - cat README.md
+ - cat /proc/mounts
+`;
+ const h = await listening({ pipeline });
+ try {
+ await queue(h);
+ const task = await h.client.claim({ arches: [], features: [], name: 'test' });
+
+ const created = h.runtime.buildCreateArgs({
+ name: 'inspect', image: IMAGE, workdir: task.workdir, entrypoint: 'sh', command: ['/tmp/entrypoint.sh'],
+ });
+ assert.ok(!created.includes('--volume'), `no volume should be passed, got: ${created.join(' ')}`);
+
+ const result = await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+ assert.equal(result.success, true, `task failed: ${result.error}`);
+
+ const log = await logOf(h, task.id);
+ assert.match(log, /^\/work$/m, 'the task should run in the working directory');
+ assert.match(log, /test repository/, 'the tree should have arrived');
+
+ const mounts = log.split('\n').filter((line) => / \/\S+ \S+ (rw|ro)[,\s]/.test(line));
+ assert.ok(mounts.length > 0, 'expected /proc/mounts in the log');
+
+ const shared = mounts
+ .map((line) => line.split(' ')[1])
+ .filter((target) => !/^\/(proc|sys|dev|etc\/(hostname|hosts|resolv\.conf))/.test(target))
+ .filter((target) => target !== '/');
+ assert.deepEqual(shared, [], `nothing should be mounted into the task, found: ${shared.join(', ')}`);
+ } finally {
+ await h.stop();
+ }
+});
+
+test('a pipeline chooses where its tree lands', opts, async () => {
+ const pipeline = `
+version: 1
+workdir: /usr/src/app
+tasks:
+ build:
+ image: ${IMAGE}
+ script:
+ - pwd
+ - cat README.md
+ - mkdir -p out && printf 'made here' > out/result.txt
+ artifacts:
+ paths: [out/**]
+`;
+ const h = await listening({ pipeline });
+ try {
+ const jobId = await queue(h);
+ const task = await h.client.claim({ arches: [], features: [], name: 'test' });
+ assert.equal(task.workdir, '/usr/src/app');
+
+ const result = await runTask({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, task, logger: quiet });
+ assert.equal(result.success, true, `task failed: ${result.error}`);
+
+ const log = await logOf(h, task.id);
+ assert.match(log, /^\/usr\/src\/app$/m, 'the script should run there');
+ assert.match(log, /test repository/, 'and the tree should be there, not at /work');
+
+ const artifacts = await artifactsOf(h, task.id);
+ assert.deepEqual(artifacts.map((a) => a.path), ['out/result.txt']);
+ assert.equal((await download(h, jobId, task.id, artifacts[0].id)).body, 'made here');
+ } finally {
+ await h.stop();
+ }
+});
diff --git a/test/worker-docker.test.js.disabled b/test/worker-docker.test.js.disabled
@@ -1,428 +0,0 @@
-// test/worker-docker.test.js - the whole loop, for real
-//
-// Runs actual containers against an actual conductor: trigger, claim, fetch
-// source, run, stream logs, upload artifacts, report, and clean up.
-// Skipped when no container runtime is available.
-//
-// The conductor is bound to a real port here rather than driven through
-// inject, because the worker is a separate process talking over HTTP.
-
-import test from 'node:test';
-import assert from 'node:assert/strict';
-import { execFile } from 'node:child_process';
-import { promisify } from 'node:util';
-import { startHarness, freePort } from './helpers/harness.js';
-import { createClient } from '../src/worker/client.js';
-import { createRuntime, containerName } from '../src/worker/docker.js';
-import { runJob } from '../src/worker/job.js';
-
-const execFileAsync = promisify(execFile);
-
-const IMAGE = process.env.CONDUCTOR_TEST_IMAGE || 'alpine:3';
-
-async function dockerUsable() {
- if (process.env.CONDUCTOR_TEST_NO_DOCKER) return false;
- try {
- await execFileAsync('docker', ['version', '--format', '{{.Server.Version}}'], { timeout: 15000 });
- await execFileAsync('docker', ['image', 'inspect', IMAGE], { timeout: 15000 })
- .catch(() => execFileAsync('docker', ['pull', IMAGE], { timeout: 300000 }));
- return true;
- } catch {
- return false;
- }
-}
-
-const available = await dockerUsable();
-const opts = { skip: available ? false : 'docker is not available' };
-
-// Starts the harness on a real port so the worker can reach it over HTTP.
-// The port has to be settled before the server is built, because the job
-// payload embeds absolute callback URLs derived from public_url.
-async function listening(options = {}) {
- const port = await freePort();
- const url = `http://127.0.0.1:${port}`;
- const h = await startHarness({ ...options, port, publicUrl: url });
- await h.app.listen({ port, host: '127.0.0.1' });
-
- const workerCfg = {
- conductor_url: url,
- name: 'docker-test-worker',
- token: h.worker.token,
- arches: [],
- features: {},
- concurrency: 1,
- poll_interval: 1,
- docker: 'docker',
- shell: 'sh',
- default_timeout: 300,
- ...(options?.worker ?? {}),
- };
-
- return {
- ...h,
- url,
- workerCfg,
- client: createClient(workerCfg),
- runtime: createRuntime(workerCfg, { logger: quiet }),
- };
-}
-
-const quiet = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} };
-
-async function logOf(h, jobId) {
- const res = await h.app.inject({ method: 'GET', url: `/api/jobs/${encodeURIComponent(jobId)}/log` });
- return res.body;
-}
-
-// Anchored, because a substring filter would also match unrelated
-// containers that happen to share the prefix.
-async function exists(kind, name) {
- const args = kind === 'network'
- ? ['network', 'ls', '--filter', `name=^${name}$`, '--format', '{{.Name}}']
- : ['ps', '-a', '--filter', `name=^${name}$`, '--format', '{{.Names}}'];
- const { stdout } = await execFileAsync('docker', args);
- return stdout.trim().length > 0;
-}
-
-test('a job runs in a container and its output reaches the conductor', opts, async () => {
- const pipeline = `
-version: 1
-jobs:
- hello:
- image: ${IMAGE}
- script:
- - echo "hello from the job"
- - echo "commit is $CONDUCTOR_SHA"
- - echo "job is $CONDUCTOR_JOB"
- - cat README.md
-`;
- const h = await listening({ pipeline });
- try {
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = await h.client.poll({ arches: [], features: [], name: 'test' });
- assert.ok(job, 'expected a job');
-
- const result = await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
- assert.equal(result.success, true, `job failed: ${result.error}`);
- assert.equal(result.exitCode, 0);
-
- const log = await logOf(h, job.id);
- assert.match(log, /\$ echo "hello from the job"/, 'each command should be echoed');
- assert.match(log, /hello from the job/);
- assert.match(log, new RegExp(`commit is ${h.sha}`), 'CONDUCTOR_SHA should be set');
- assert.match(log, /job is hello/);
- // Proves the source tarball was fetched and unpacked.
- assert.match(log, /test repository/);
-
- const detail = await h.app.inject({ method: 'GET', url: `/api/runs/${job.run_id}` });
- assert.equal(detail.json().run.state, 'success');
- } finally {
- await h.stop();
- }
-});
-
-test('a failing command fails the job and preserves the log', opts, async () => {
- const pipeline = `
-version: 1
-jobs:
- boom:
- image: ${IMAGE}
- script:
- - echo before
- - exit 3
- - echo after
-`;
- const h = await listening({ pipeline });
- try {
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = await h.client.poll({ arches: [], features: [], name: 'test' });
-
- const result = await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
- assert.equal(result.success, false);
- assert.equal(result.exitCode, 3);
-
- const log = await logOf(h, job.id);
- assert.match(log, /before/);
- // set -e must stop the script at the failure.
- assert.ok(!log.includes('after'), 'commands after a failure must not run');
-
- const detail = await h.app.inject({ method: 'GET', url: `/api/runs/${job.run_id}` });
- assert.equal(detail.json().run.state, 'failed');
- } finally {
- await h.stop();
- }
-});
-
-test('artifacts produced in the container are uploaded and downloadable', opts, async () => {
- const pipeline = `
-version: 1
-jobs:
- produce:
- image: ${IMAGE}
- script:
- - mkdir -p dist/nested
- - printf 'binary-content' > dist/app.bin
- - printf 'deep' > dist/nested/deep.txt
- artifacts:
- paths: [dist/**]
-`;
- const h = await listening({ pipeline });
- try {
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = await h.client.poll({ arches: [], features: [], name: 'test' });
-
- const result = await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
- assert.equal(result.success, true, `job failed: ${result.error}`);
-
- const detail = await h.app.inject({ method: 'GET', url: `/api/jobs/${encodeURIComponent(job.id)}` });
- const artifacts = detail.json().artifacts;
- const paths = artifacts.map((a) => a.path).sort();
- assert.deepEqual(paths, ['dist/app.bin', 'dist/nested/deep.txt']);
-
- const binary = artifacts.find((a) => a.path === 'dist/app.bin');
- const download = await h.app.inject({ method: 'GET', url: `/api/artifacts/${binary.id}` });
- assert.equal(download.body, 'binary-content');
- } finally {
- await h.stop();
- }
-});
-
-test('a worker feature injects a mount and environment the conductor never sees', opts, async () => {
- const pipeline = `
-version: 1
-jobs:
- signed:
- image: ${IMAGE}
- requires: [sign-key]
- script:
- - echo "key path is $SIGN_KEY"
- - cat "$SIGN_KEY"
-`;
- const h = await listening({
- pipeline,
- worker: {
- features: {
- 'sign-key': {
- mounts: [],
- env: { SIGN_KEY: '/keys/build.key' },
- devices: [],
- privileged: false,
- },
- },
- },
- });
-
- try {
- // The key lives on the worker host and is mounted in by the feature.
- const fs = await import('node:fs/promises');
- await fs.mkdir(`${h.root}/keys`, { recursive: true });
- await fs.writeFile(`${h.root}/keys/build.key`, 'PRIVATE-KEY-MATERIAL');
- h.workerCfg.features['sign-key'].mounts = [`${h.root}/keys/build.key:/keys/build.key:ro`];
-
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
-
- // Without the feature the job is not offered at all.
- const denied = await createClient({ ...h.workerCfg, token: h.worker.token })
- .poll({ arches: [], features: [], name: 'test' });
- assert.equal(denied, null, 'a worker lacking the feature must not be offered the job');
-
- const job = await h.client.poll({ arches: [], features: ['sign-key'], name: 'test' });
- assert.ok(job, 'expected the job once the feature is advertised');
-
- const result = await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
- assert.equal(result.success, true, `job failed: ${result.error}`);
-
- const log = await logOf(h, job.id);
- assert.match(log, /key path is \/keys\/build\.key/);
- assert.match(log, /PRIVATE-KEY-MATERIAL/);
-
- // The conductor stored the requirement, never the key itself.
- const stored = await h.services.db.get(
- 'SELECT requires, spec FROM jobs WHERE id = {id}', { id: job.id }
- );
- assert.equal(stored.requires, '["sign-key"]');
- assert.ok(!stored.spec.includes('PRIVATE-KEY-MATERIAL'));
- } finally {
- await h.stop();
- }
-});
-
-test('a service container is reachable from the job by its alias', opts, async () => {
- const pipeline = `
-version: 1
-jobs:
- talks:
- image: ${IMAGE}
- services:
- - image: ${IMAGE}
- alias: sidecar
- command: [sleep, '60']
- script:
- - ping -c1 -W2 sidecar
-`;
- const h = await listening({ pipeline });
- try {
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = await h.client.poll({ arches: [], features: [], name: 'test' });
-
- const result = await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
- const log = await logOf(h, job.id);
- assert.equal(result.success, true, `job failed: ${result.error}\n${log}`);
- assert.match(log, /1 packets received|1 received/);
- } finally {
- await h.stop();
- }
-});
-
-test('a job that exceeds its timeout is stopped', opts, async () => {
- const pipeline = `
-version: 1
-jobs:
- slow:
- image: ${IMAGE}
- timeout: 2
- script:
- - echo starting
- - sleep 60
-`;
- const h = await listening({ pipeline });
- try {
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = await h.client.poll({ arches: [], features: [], name: 'test' });
-
- const started = Date.now();
- const result = await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
- const elapsed = (Date.now() - started) / 1000;
-
- assert.equal(result.success, false);
- assert.equal(result.timedOut, true);
- assert.ok(elapsed < 45, `should have been stopped promptly, took ${elapsed}s`);
- assert.match(await logOf(h, job.id), /timed out/);
- } finally {
- await h.stop();
- }
-});
-
-test('containers, networks and workspaces are cleaned up', opts, async () => {
- const pipeline = `
-version: 1
-jobs:
- tidy:
- image: ${IMAGE}
- services:
- - image: ${IMAGE}
- alias: sidecar
- command: [sleep, '60']
- script:
- - echo work
-`;
- const h = await listening({ pipeline });
- try {
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = await h.client.poll({ arches: [], features: [], name: 'test' });
- await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
-
- assert.equal(await exists('container', containerName('conductor', job.id)), false,
- 'the job container should be gone');
- assert.equal(await exists('container', containerName('conductor-svc', job.id, 'sidecar')), false,
- 'the service container should be gone');
- assert.equal(await exists('network', containerName('conductor-net', job.id)), false,
- 'the job network should be gone');
-
- // The worker keeps nothing between jobs, so there is no workspace
- // left to remove: what it wrote lived in a temporary directory that
- // goes with the job.
- const fs = await import('node:fs/promises');
- const os = await import('node:os');
- const scratch = (await fs.readdir(os.tmpdir())).filter((n) => n.startsWith('conductor-job-'));
- assert.deepEqual(scratch, [], 'the job scratch directory should be removed');
- } finally {
- await h.stop();
- }
-});
-
-test('a job runs with no mounts, and nothing of the worker is shared with it', opts, async () => {
- // The point of copying the tree in rather than bind mounting it: the
- // container gets no path belonging to the worker, so nothing has to
- // line up between the two and the worker needs no storage at all.
- const pipeline = `
-version: 1
-jobs:
- isolated:
- image: ${IMAGE}
- script:
- - pwd
- - cat README.md
- - cat /proc/mounts
-`;
- const h = await listening({ pipeline });
- try {
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = await h.client.poll({ arches: [], features: [], name: 'test' });
-
- const created = h.runtime.buildCreateArgs({
- name: 'inspect', image: IMAGE, workdir: job.workdir, entrypoint: 'sh', command: ['/tmp/entrypoint.sh'],
- });
- assert.ok(!created.includes('--volume'), `no volume should be passed, got: ${created.join(' ')}`);
-
- const result = await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
- assert.equal(result.success, true, `job failed: ${result.error}`);
-
- const log = await logOf(h, job.id);
- assert.match(log, /^\/work$/m, 'the job should run in the working directory');
- assert.match(log, /test repository/, 'the tree should have arrived');
-
- // The proof, from inside the container: the only mounts are the ones
- // docker always adds. Nothing of the worker's filesystem is there, so
- // no path has to mean the same thing on both sides.
- const mounts = log.split('\n').filter((line) => / \/\S+ \S+ (rw|ro)[,\s]/.test(line));
- assert.ok(mounts.length > 0, 'expected /proc/mounts in the log');
-
- const shared = mounts
- .map((line) => line.split(' ')[1])
- .filter((target) => !/^\/(proc|sys|dev|etc\/(hostname|hosts|resolv\.conf))/.test(target))
- .filter((target) => target !== '/');
- assert.deepEqual(shared, [], `nothing should be mounted into the job, found: ${shared.join(', ')}`);
- } finally {
- await h.stop();
- }
-});
-
-test('a pipeline chooses where its tree lands', opts, async () => {
- const pipeline = `
-version: 1
-workdir: /usr/src/app
-jobs:
- build:
- image: ${IMAGE}
- script:
- - pwd
- - cat README.md
- - mkdir -p out && printf 'made here' > out/result.txt
- artifacts:
- paths: [out/**]
-`;
- const h = await listening({ pipeline });
- try {
- await h.trigger({ sha: h.sha, ref: 'refs/heads/main' });
- const job = await h.client.poll({ arches: [], features: [], name: 'test' });
- assert.equal(job.workdir, '/usr/src/app');
-
- const result = await runJob({ cfg: h.workerCfg, client: h.client, runtime: h.runtime, job, logger: quiet });
- assert.equal(result.success, true, `job failed: ${result.error}`);
-
- const log = await logOf(h, job.id);
- assert.match(log, /^\/usr\/src\/app$/m, 'the script should run there');
- assert.match(log, /test repository/, 'and the tree should be there, not at /work');
-
- // Artifacts are read back out relative to the same directory.
- const detail = await h.app.inject({ method: 'GET', url: `/api/jobs/${encodeURIComponent(job.id)}` });
- const artifacts = detail.json().artifacts;
- assert.deepEqual(artifacts.map((a) => a.path), ['out/result.txt']);
-
- const download = await h.app.inject({ method: 'GET', url: `/api/artifacts/${artifacts[0].id}` });
- assert.equal(download.body, 'made here');
- } finally {
- await h.stop();
- }
-});