mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
CI on PR #56 first run came back "sshd entered unhealthy state: unhealthy". Two issues compounded: 1. The compose healthcheck shells `ss -lnt | grep ':22 '`, but `ss` isn't shipped in the minimal `ubuntu:22.04` base — the command exited non-zero every interval, so health stayed unhealthy past the retry budget. Adding `iproute2` to the apt-get install line gives us `ss` (~600 KB add). 2. When the start script gave up, it printed the timeout message and exited. We had no visibility into *why* sshd was unhealthy because docker logs weren't captured. Added `dumpContainerLogs` which calls `docker logs --tail 200` and `docker inspect --format '{{json .State.Health}}'` on every failure path so the next CI run (or local run) shows the actual sshd output and the healthcheck output history without manual digging.
95 lines
3.7 KiB
JavaScript
95 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Bring up the test sshd container.
|
|
*
|
|
* - Generates `docker/keys/id_test{,.pub}` if missing (ed25519,
|
|
* no passphrase). Both files are gitignored.
|
|
* - Runs `docker compose up -d --build` from the repo root.
|
|
* - Waits for sshd to accept connections on 127.0.0.1:2222 (using
|
|
* the compose healthcheck) before returning.
|
|
* - Prints the public key, container name, and host:port so a
|
|
* human knows what just happened.
|
|
*
|
|
* Idempotent: re-running it just re-checks the keys, brings the
|
|
* container back up if it was stopped, and waits for healthy.
|
|
*
|
|
* Used by the integration test suite (`npm run test:integration`).
|
|
*/
|
|
|
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(here, '..', '..');
|
|
const keyDir = path.join(repoRoot, 'docker', 'keys');
|
|
const keyPath = path.join(keyDir, 'id_test');
|
|
const pubPath = `${keyPath}.pub`;
|
|
|
|
fs.mkdirSync(keyDir, { recursive: true });
|
|
fs.mkdirSync(path.join(repoRoot, 'docker', 'test-vault'), { recursive: true });
|
|
|
|
if (!fs.existsSync(keyPath)) {
|
|
console.log(`Generating ed25519 keypair at ${keyPath}`);
|
|
// -N '' = no passphrase, -q = no banner. -f overwrites (already
|
|
// checked nonexistence above).
|
|
spawnRequire('ssh-keygen', ['-t', 'ed25519', '-N', '', '-f', keyPath, '-q', '-C', 'obsidian-remote-ssh-test']);
|
|
} else {
|
|
console.log(`Reusing existing keypair at ${keyPath}`);
|
|
}
|
|
|
|
console.log('Bringing up docker compose service `sshd`…');
|
|
spawnRequire('docker', ['compose', 'up', '-d', '--build', 'sshd'], { cwd: repoRoot });
|
|
|
|
console.log('Waiting for sshd to be healthy…');
|
|
const deadline = Date.now() + 60_000;
|
|
while (Date.now() < deadline) {
|
|
const out = spawnSync('docker', ['inspect', '--format', '{{.State.Health.Status}}',
|
|
'obsidian-remote-ssh-test-sshd'], { encoding: 'utf8' });
|
|
const status = (out.stdout || '').trim();
|
|
if (status === 'healthy') {
|
|
console.log('sshd is healthy.');
|
|
console.log('');
|
|
console.log(` host: 127.0.0.1`);
|
|
console.log(` port: 2222`);
|
|
console.log(` user: tester`);
|
|
console.log(` private key: ${keyPath}`);
|
|
console.log(` public key: ${pubPath}`);
|
|
console.log('');
|
|
process.exit(0);
|
|
}
|
|
if (status === 'unhealthy' || status === 'exited') {
|
|
console.error(`sshd entered unhealthy state: ${status}`);
|
|
dumpContainerLogs();
|
|
process.exit(1);
|
|
}
|
|
// 'starting' or empty (container not yet up) — wait a tick.
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
console.error('Timed out waiting for sshd to become healthy.');
|
|
dumpContainerLogs();
|
|
process.exit(1);
|
|
|
|
function dumpContainerLogs() {
|
|
console.error('--- docker logs (obsidian-remote-ssh-test-sshd) ---');
|
|
spawnSync('docker', ['logs', '--tail', '200', 'obsidian-remote-ssh-test-sshd'],
|
|
{ stdio: 'inherit' });
|
|
console.error('--- docker inspect (Health) ---');
|
|
const r = spawnSync('docker',
|
|
['inspect', '--format', '{{json .State.Health}}', 'obsidian-remote-ssh-test-sshd'],
|
|
{ encoding: 'utf8' });
|
|
console.error(r.stdout || r.stderr || '');
|
|
}
|
|
|
|
// ─── helpers ───────────────────────────────────────────────────────────
|
|
|
|
function spawnRequire(cmd, args, opts = {}) {
|
|
const r = spawnSync(cmd, args, { stdio: 'inherit', ...opts });
|
|
if (r.status !== 0) {
|
|
if (r.error && r.error.code === 'ENOENT') {
|
|
console.error(`Missing required tool: ${cmd}. Is it on PATH?`);
|
|
}
|
|
process.exit(r.status ?? 1);
|
|
}
|
|
}
|