121 lines
5.5 KiB
JavaScript
121 lines
5.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { EventEmitter } from 'node:events';
|
|
import { PassThrough } from 'node:stream';
|
|
import { buildRunArgs, devRunArgs, createDocker, spawnDocker } from '../src/docker.js';
|
|
|
|
const base = { image: 'node:20', workDir: '/srv/w/build/1', cacheDir: '/srv/c/repo', cmd: ['npm', 'ci'] };
|
|
const pair = (args, flag, val) => args.some((a, i) => a === flag && args[i + 1] === val);
|
|
|
|
test('buildRunArgs: komplettes Sicherheitsprofil', () => {
|
|
const a = buildRunArgs(base);
|
|
assert.deepEqual(a.slice(0, 2), ['run', '--rm']);
|
|
assert.ok(a.includes('--init'));
|
|
for (const [f, v] of [['--memory', '1g'], ['--cpus', '1'], ['--pids-limit', '512'], ['--cap-drop', 'ALL'], ['--security-opt', 'no-new-privileges'], ['--user', '1000:1000'], ['--network', 'preview-sandbox'], ['-w', '/work'], ['-v', '/srv/w/build/1:/work'], ['-v', '/srv/c/repo:/cache'], ['-e', 'HOME=/cache/home'], ['-e', 'npm_config_cache=/cache/npm'], ['-e', 'COREPACK_HOME=/cache/corepack']]) {
|
|
assert.ok(pair(a, f, v), `${f} ${v}`);
|
|
}
|
|
assert.equal(a.filter((x) => x === '-v').length, 2);
|
|
assert.deepEqual(a.slice(-3), ['node:20', 'npm', 'ci']);
|
|
});
|
|
|
|
test('buildRunArgs: Variablen nur als Name, Werte nie in args', () => {
|
|
const a = buildRunArgs({ ...base, env: { API_KEY: 'geheim-wert', X: 'abc' }, memory: '1g' });
|
|
assert.ok(pair(a, '-e', 'API_KEY') && pair(a, '-e', 'X'));
|
|
assert.ok(!a.join(' ').includes('geheim-wert'));
|
|
});
|
|
|
|
test('buildRunArgs: ungültiger Name/NUL/Name-Validierung', () => {
|
|
assert.throws(() => buildRunArgs({ ...base, env: { 'a b': '1' } }));
|
|
assert.throws(() => buildRunArgs({ ...base, env: { A: 'x\0y' } }));
|
|
assert.throws(() => buildRunArgs({ ...base, env: { HOME: 'x' } }));
|
|
assert.throws(() => buildRunArgs({ ...base, name: '-x' }));
|
|
});
|
|
|
|
test('buildRunArgs: memory/name optional', () => {
|
|
const a = buildRunArgs({ ...base, name: 'job-1', memory: '2g' });
|
|
assert.ok(pair(a, '--memory', '2g') && pair(a, '--name', 'job-1'));
|
|
});
|
|
|
|
test('devRunArgs: detached, Name, Label, ohne --rm, 512m', () => {
|
|
const a = devRunArgs({ ...base, sub: 'demo-1', cmd: ['npm', 'run', 'dev'] });
|
|
assert.ok(!a.includes('--rm'));
|
|
assert.ok(a.includes('-d'));
|
|
assert.ok(pair(a, '--name', 'preview-dev-demo-1'));
|
|
assert.ok(pair(a, '--label', 'webklar.preview=demo-1'));
|
|
assert.ok(pair(a, '--restart', 'no'));
|
|
assert.ok(pair(a, '--memory', '512m'));
|
|
assert.ok(pair(a, '--cap-drop', 'ALL') && pair(a, '--user', '1000:1000'));
|
|
assert.deepEqual(a.slice(-4), ['node:20', 'npm', 'run', 'dev']);
|
|
});
|
|
|
|
test('devRunArgs: Subdomain wird validiert', () => {
|
|
for (const sub of ['', 'A', 'a/b', '-x;', 'x'.repeat(64), undefined]) assert.throws(() => devRunArgs({ ...base, sub }), sub);
|
|
});
|
|
|
|
// Fake-Kindprozess
|
|
function fakeChild() {
|
|
const c = new EventEmitter();
|
|
c.stdout = new PassThrough(); c.stderr = new PassThrough();
|
|
c.killed = []; c.kill = (s) => { c.killed.push(s); };
|
|
return c;
|
|
}
|
|
|
|
test('run: streamt Zeilen, liefert Exit-Code, Umgebung minimal + Job-Env', async () => {
|
|
const calls = [];
|
|
const child = fakeChild();
|
|
const docker = createDocker({ exec: (args, opts) => { calls.push({ args, opts }); return child; }, baseEnv: { PATH: '/bin', DOCKER_HOST: 'unix:///x', PREVIEW_RUNNER_TOKEN: 'leak' } });
|
|
const lines = [];
|
|
const p = docker.run(['run', '-e', 'K'], { env: { K: 'v' }, onLine: (l) => lines.push(l) });
|
|
child.stdout.write('eins\nzw');
|
|
child.stderr.write('fehler\n');
|
|
child.stdout.write('ei\n');
|
|
child.stdout.end(); child.stderr.end();
|
|
await new Promise((r) => setImmediate(r));
|
|
child.emit('close', 3);
|
|
assert.deepEqual(await p, { code: 3 });
|
|
assert.deepEqual(lines.sort(), ['eins', 'fehler', 'zwei']);
|
|
assert.deepEqual(calls[0].opts.env, { PATH: '/bin', DOCKER_HOST: 'unix:///x', K: 'v' });
|
|
});
|
|
|
|
test('run: NUL im Wert ⇒ Fehler vor dem Start', async () => {
|
|
let n = 0;
|
|
const docker = createDocker({ exec: () => { n++; return fakeChild(); } });
|
|
await assert.rejects(docker.run(['run'], { env: { A: 'x\0' } }));
|
|
assert.equal(n, 0);
|
|
});
|
|
|
|
test('run: Zeitlimit ⇒ Kill, docker rm -f, code 124', async () => {
|
|
const calls = [];
|
|
const child = fakeChild();
|
|
const docker = createDocker({ exec: (args) => { calls.push(args); return calls.length === 1 ? child : Object.assign(fakeChild(), { __rm: true }); } });
|
|
const p = docker.run(['run', '--name', 'job-9', 'img'], { timeoutMs: 20 });
|
|
const r = await p;
|
|
assert.deepEqual(r, { code: 124 });
|
|
assert.deepEqual(child.killed, ['SIGKILL']);
|
|
assert.deepEqual(calls[1], ['rm', '-f', 'job-9']);
|
|
});
|
|
|
|
test('ps/stop/rm', async () => {
|
|
const calls = [];
|
|
const exec = (args) => {
|
|
calls.push(args);
|
|
const c = fakeChild();
|
|
setImmediate(() => { c.stdout.write('preview-dev-a\npreview-dev-b\n\n'); c.stdout.end(); c.stderr.end(); c.emit('close', 0); });
|
|
return c;
|
|
};
|
|
const docker = createDocker({ exec });
|
|
assert.deepEqual(await docker.ps('webklar.preview'), ['preview-dev-a', 'preview-dev-b']);
|
|
assert.deepEqual(calls[0], ['ps', '--filter', 'label=webklar.preview', '--format', '{{.Names}}']);
|
|
await docker.stop('preview-dev-a');
|
|
await docker.rm('preview-dev-a');
|
|
assert.deepEqual(calls[1], ['rm', '-f', 'preview-dev-a']);
|
|
assert.deepEqual(calls[2], ['rm', '-f', 'preview-dev-a']);
|
|
assert.equal(typeof spawnDocker, 'function');
|
|
});
|
|
|
|
test('rm/stop: ungültiger Name wird abgelehnt', async () => {
|
|
const docker = createDocker({ exec: () => { throw new Error('nicht aufrufen'); } });
|
|
await assert.rejects(docker.rm('--all'));
|
|
await assert.rejects(docker.stop('a b'));
|
|
});
|