|
| 1 | +import { execSync } from 'child_process'; |
| 2 | +import fs from 'fs'; |
| 3 | +import os from 'os'; |
| 4 | +import path from 'path'; |
| 5 | + |
| 6 | +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); |
| 7 | + |
| 8 | +function run(cmd: string, opts?: { cwd?: string }) { |
| 9 | + execSync(cmd, { |
| 10 | + stdio: 'inherit', |
| 11 | + cwd: opts?.cwd, |
| 12 | + env: process.env, |
| 13 | + timeout: 1000 * 60 * 20, // 20 minutes per command |
| 14 | + }); |
| 15 | +} |
| 16 | + |
| 17 | +function dockerExec(container: string, cmd: string) { |
| 18 | + execSync(`docker exec ${container} bash -lc ${JSON.stringify(cmd)}`, { |
| 19 | + stdio: 'pipe', |
| 20 | + env: process.env, |
| 21 | + timeout: 1000 * 60 * 5, |
| 22 | + }); |
| 23 | +} |
| 24 | + |
| 25 | +function dockerExecCapture(container: string, cmd: string) { |
| 26 | + return execSync(`docker exec ${container} bash -lc ${JSON.stringify(cmd)}`, { |
| 27 | + stdio: 'pipe', |
| 28 | + env: process.env, |
| 29 | + timeout: 1000 * 60 * 5, |
| 30 | + }).toString('utf8'); |
| 31 | +} |
| 32 | + |
| 33 | +const shouldRun = process.env.RUN_DOCKER_DEB_INSTALL_START_TEST === '1'; |
| 34 | +const itOrSkip = shouldRun ? it : it.skip; |
| 35 | + |
| 36 | +describe('Docker Debian DEB smoke', () => { |
| 37 | + jest.setTimeout(1000 * 60 * 30); // 30 minutes |
| 38 | + |
| 39 | + itOrSkip('build deb, install and start sesame-daemon on Debian', async () => { |
| 40 | + const repoRoot = path.resolve(__dirname, '../..'); |
| 41 | + const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); |
| 42 | + |
| 43 | + run('docker version'); |
| 44 | + |
| 45 | + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sesame-daemon-deb-test-')); |
| 46 | + const containerName = `sesame-daemon-deb-smoke-${process.pid}-${Date.now()}`; |
| 47 | + |
| 48 | + try { |
| 49 | + const binPath = path.join(workDir, 'sesame-daemon-linux'); |
| 50 | + |
| 51 | + // Build the Linux binary (pkg) in a throwaway container. |
| 52 | + run( |
| 53 | + [ |
| 54 | + 'docker run --rm', |
| 55 | + `-v ${JSON.stringify(repoRoot)}:/repo`, |
| 56 | + `-v ${JSON.stringify(workDir)}:/output`, |
| 57 | + '-w /repo', |
| 58 | + 'node:18-bookworm-slim bash -lc', |
| 59 | + JSON.stringify([ |
| 60 | + 'set -eux', |
| 61 | + 'yarn install --prefer-offline --frozen-lockfile --non-interactive', |
| 62 | + 'yarn run build', |
| 63 | + 'npm i -g pkg', |
| 64 | + `rm -f /output/sesame-daemon-linux`, |
| 65 | + `pkg dist/main.js -o /output/sesame-daemon-linux --targets node18-linux-x64 --config package.json`, |
| 66 | + 'chmod +x /output/sesame-daemon-linux', |
| 67 | + ].join(' && ')), |
| 68 | + ].join(' '), |
| 69 | + ); |
| 70 | + |
| 71 | + // Prepare deb package root. |
| 72 | + const debRoot = path.join(workDir, '.debpkg'); |
| 73 | + fs.mkdirSync(path.join(debRoot, 'DEBIAN'), { recursive: true }); |
| 74 | + |
| 75 | + // Minimal control file (we avoid postinst/postrm because systemd is fragile in containers). |
| 76 | + const control = [ |
| 77 | + 'Package: sesame-daemon', |
| 78 | + `Version: ${pkg.version}`, |
| 79 | + 'Section: utils', |
| 80 | + 'Priority: optional', |
| 81 | + 'Architecture: amd64', |
| 82 | + 'Maintainer: Libertech-FR <noreply@example.com>', |
| 83 | + 'Description: Sesame Daemon', |
| 84 | + '', |
| 85 | + ].join('\n'); |
| 86 | + fs.writeFileSync(path.join(debRoot, 'DEBIAN', 'control'), control, 'utf8'); |
| 87 | + |
| 88 | + fs.mkdirSync(path.join(debRoot, 'usr/bin'), { recursive: true }); |
| 89 | + fs.copyFileSync(binPath, path.join(debRoot, 'usr/bin/sesame-daemon')); |
| 90 | + fs.chmodSync(path.join(debRoot, 'usr/bin/sesame-daemon'), 0o755); |
| 91 | + |
| 92 | + // Data directory + backends + package.json for runtime reads. |
| 93 | + fs.mkdirSync(path.join(debRoot, 'var/lib/sesame-daemon'), { recursive: true }); |
| 94 | + fs.copyFileSync(path.join(repoRoot, 'package.json'), path.join(debRoot, 'var/lib/sesame-daemon/package.json')); |
| 95 | + fs.mkdirSync(path.join(debRoot, 'var/lib/sesame-daemon/backends'), { recursive: true }); |
| 96 | + for (const entry of fs.readdirSync(path.join(repoRoot, 'backends.example'))) { |
| 97 | + const from = path.join(repoRoot, 'backends.example', entry); |
| 98 | + const to = path.join(debRoot, 'var/lib/sesame-daemon/backends', entry); |
| 99 | + fs.cpSync(from, to, { recursive: true }); |
| 100 | + } |
| 101 | + |
| 102 | + // Service file + defaults (even if we don't use systemd in this test). |
| 103 | + fs.mkdirSync(path.join(debRoot, 'usr/share/sesame-daemon'), { recursive: true }); |
| 104 | + fs.copyFileSync( |
| 105 | + path.join(repoRoot, '.debpkg/usr/share/sesame-daemon/sesame-daemon.service'), |
| 106 | + path.join(debRoot, 'usr/share/sesame-daemon/sesame-daemon.service'), |
| 107 | + ); |
| 108 | + fs.mkdirSync(path.join(debRoot, 'etc/default'), { recursive: true }); |
| 109 | + fs.copyFileSync( |
| 110 | + path.join(repoRoot, '.debpkg/etc/default/sesame-daemon'), |
| 111 | + path.join(debRoot, 'etc/default/sesame-daemon'), |
| 112 | + ); |
| 113 | + |
| 114 | + // Build the .deb inside a Debian container (ensures dpkg-deb availability). |
| 115 | + const debFileName = `sesame-daemon_${pkg.version}_amd64.deb`; |
| 116 | + run( |
| 117 | + [ |
| 118 | + 'docker run --rm --platform linux/amd64', |
| 119 | + `-v ${JSON.stringify(workDir)}:/work`, |
| 120 | + '-w /work', |
| 121 | + 'debian:bookworm-slim bash -lc', |
| 122 | + JSON.stringify(['set -eux', 'apt-get update', 'apt-get install -y dpkg-dev', `dpkg-deb --build .debpkg ${debFileName}`].join(' && ')), |
| 123 | + ].join(' '), |
| 124 | + ); |
| 125 | + |
| 126 | + const debPathOnHost = path.join(workDir, debFileName); |
| 127 | + if (!fs.existsSync(debPathOnHost)) { |
| 128 | + throw new Error(`.deb not found: ${debPathOnHost}`); |
| 129 | + } |
| 130 | + |
| 131 | + const debPathInContainer = path.join('/tmp/payload', debFileName); |
| 132 | + |
| 133 | + // Start Debian runtime container. |
| 134 | + run( |
| 135 | + [ |
| 136 | + 'docker run -d --rm --platform linux/amd64', |
| 137 | + '--name ' + containerName, |
| 138 | + '-v ' + JSON.stringify(workDir) + ':/tmp/payload:ro', |
| 139 | + 'debian:bookworm-slim bash -lc "while true; do sleep 3600; done"', |
| 140 | + ].join(' '), |
| 141 | + ); |
| 142 | + |
| 143 | + // Install runtime dependencies + tools. |
| 144 | + dockerExec( |
| 145 | + containerName, |
| 146 | + 'apt-get update && apt-get install -y bash ca-certificates procps redis-server', |
| 147 | + ); |
| 148 | + dockerExec(containerName, 'redis-server --daemonize yes'); |
| 149 | + |
| 150 | + // Install .deb. |
| 151 | + try { |
| 152 | + dockerExec(containerName, `dpkg -i ${debPathInContainer}`); |
| 153 | + } catch { |
| 154 | + dockerExec(containerName, 'apt-get -f install -y'); |
| 155 | + dockerExec(containerName, `dpkg -i ${debPathInContainer}`); |
| 156 | + } |
| 157 | + |
| 158 | + // Start the daemon directly (systemd is often unavailable in container tests). |
| 159 | + dockerExecCapture( |
| 160 | + containerName, |
| 161 | + 'cd /var/lib/sesame-daemon; set -a; . /etc/default/sesame-daemon; set +a; ' + |
| 162 | + 'nohup /usr/bin/sesame-daemon </dev/null > /tmp/sesame-daemon.log 2>&1 &', |
| 163 | + ); |
| 164 | + |
| 165 | + // Wait for process to show up and stay up briefly. |
| 166 | + let running = false; |
| 167 | + for (let i = 0; i < 30; i++) { |
| 168 | + const pids = dockerExecCapture(containerName, "pgrep -f sesame-daemon || true").trim(); |
| 169 | + if (pids) { |
| 170 | + running = true; |
| 171 | + break; |
| 172 | + } |
| 173 | + // eslint-disable-next-line no-await-in-loop |
| 174 | + await sleep(1000); |
| 175 | + } |
| 176 | + |
| 177 | + if (!running) { |
| 178 | + const logsHead = dockerExecCapture(containerName, "sed -n '1,400p' /tmp/sesame-daemon.log 2>/dev/null || true"); |
| 179 | + const logsTail = dockerExecCapture(containerName, "tail -n 200 /tmp/sesame-daemon.log 2>/dev/null || true"); |
| 180 | + const ps = dockerExecCapture(containerName, "ps -ef | grep -v grep | grep -E 'sesame-daemon|node' || true"); |
| 181 | + const redisPing = dockerExecCapture(containerName, 'redis-cli ping || true'); |
| 182 | + throw new Error( |
| 183 | + 'sesame-daemon process is not running.' + |
| 184 | + '\nredis-cli ping: ' + |
| 185 | + redisPing + |
| 186 | + '\nps:\n' + |
| 187 | + ps + |
| 188 | + '\nlogs(head):\n' + |
| 189 | + logsHead + |
| 190 | + '\nlogs(tail):\n' + |
| 191 | + logsTail, |
| 192 | + ); |
| 193 | + } |
| 194 | + } finally { |
| 195 | + try { |
| 196 | + run(`docker rm -f ${containerName}`); |
| 197 | + } catch { |
| 198 | + // ignore |
| 199 | + } |
| 200 | + } |
| 201 | + }); |
| 202 | +}); |
| 203 | + |
0 commit comments