@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
268 lines (240 loc) • 10.9 kB
JavaScript
/**
* @module deploy-monitor.test
* @description End-to-end test of the two-phase deployment readiness contract
* between the in-pod runtime (`start.js` / `runtime-status.js`) and the CD-runner
* monitor (`Underpost.monitor.monitorReadyRunner`), exercised as real OS
* processes coordinating through the shared underpost env file and a real HTTP
* internal status endpoint.
*
* Contract under test:
*
* - The in-pod runtime publishes only `container-status` (Phase 2) and serves
* it over `GET /_internal/status`. It never propagates an exit code.
*
* - monitorReadyRunner (CD runner) confirms BOTH phases before exiting 0:
* Phase 1 — Kubernetes pod `Ready` condition (kubectl get pod -o json).
* Phase 2 — runtime `running-deployment` read over HTTP via
* `kubectl port-forward` to the internal endpoint.
* It throws (→ exit 1) on explicit runtime `error`, and returns (→ exit 0)
* only once both phases are satisfied.
*
* The cluster surface is supplied by fake `sudo`/`kubectl` binaries on PATH:
* - `get pods` / `get pod -o json` report one pod whose Ready condition is
* driven by FAKE_POD_READY.
* - `port-forward` is a no-op sleep; the monitor's HTTP GET reaches a REAL
* internal status server bound in this test process (localPort == port),
* which reads the same env file the runtime writes.
*
* The env file is redirected under a temp `npm_config_prefix`, so the test
* needs no cluster, no root, and never touches the machine's global install.
*
* Uses 'chai' for assertions.
*/
import { expect } from 'chai';
import fs from 'fs-extra';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import Underpost from '../src/index.js';
import { shellExec } from '../src/server/process.js';
import { startInternalStatusServer, stopInternalStatusServer } from '../src/server/runtime-status.js';
const node = process.execPath;
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const DEPLOY_ID = 'dd-test';
const ENV = 'production';
const TRAFFIC = 'green';
const POD_NAME = `${DEPLOY_ID}-${ENV}-${TRAFFIC}-pod`;
const RUNNING_STATUS = `${DEPLOY_ID}-${ENV}-running-deployment`;
const INIT_STATUS = `${DEPLOY_ID}-${ENV}-initializing-deployment`;
const BUILD_STATUS = `${DEPLOY_ID}-${ENV}-build-deployment`;
const INTERNAL_PORT = 39517; // internal status endpoint (real server bound here)
const CLOSED_PORT = 39518; // no server — used to force a transport failure
describe('Deploy monitor — two-phase state machine (e2e, real HTTP transport)', function () {
this.timeout(60000);
let prevPrefix;
let tmpPrefix;
let fakeBinDir;
let envFile;
let monitorScriptPath;
before(() => {
prevPrefix = process.env.npm_config_prefix;
tmpPrefix = fs.mkdtempSync(path.join(os.tmpdir(), 'underpost-e2e-'));
process.env.npm_config_prefix = tmpPrefix;
Underpost.env.set('container-status', 'init');
Underpost.env.set('start-container-status', '');
const npmRoot = shellExec('npm root -g', { stdout: true, silent: true, disableLog: true }).trim();
envFile = path.join(npmRoot, 'underpost', '.env');
process.env.UNDERPOST_INTERNAL_PORT = String(INTERNAL_PORT);
startInternalStatusServer(INTERNAL_PORT);
fakeBinDir = path.join(tmpPrefix, 'fakebin');
fs.ensureDirSync(fakeBinDir);
const sudoPath = path.join(fakeBinDir, 'sudo');
fs.writeFileSync(
sudoPath,
`#!/usr/bin/env bash
while [[ "$1" == -* || "$1" == "--" ]]; do shift; done
exec "$@"
`,
);
const kubectlPath = path.join(fakeBinDir, 'kubectl');
fs.writeFileSync(
kubectlPath,
`#!/usr/bin/env bash
verb="$1"; kind="$2"
if [[ "$verb" == "get" && "$kind" == "pods" ]]; then
printf 'NAME READY STATUS RESTARTS AGE\\n'
printf '%s 1/1 Running 0 1m\\n' "$POD_NAME"
exit 0
fi
if [[ "$verb" == "get" && "$kind" == "pod" ]]; then
printf '{"status":{"conditions":[{"type":"Ready","status":"%s"}]}}\\n' "\${FAKE_POD_READY:-True}"
exit 0
fi
if [[ "$verb" == "port-forward" ]]; then
sleep 30
exit 0
fi
if [[ "$verb" == "exec" ]]; then
grep -E '^container-status=' "$UNDERPOST_ENV_FILE" 2>/dev/null | tail -n1 | sed -E 's/^container-status=//'
exit 0
fi
exit 0
`,
);
fs.chmodSync(sudoPath, 0o755);
fs.chmodSync(kubectlPath, 0o755);
monitorScriptPath = path.join(tmpPrefix, 'monitor-ready.mjs');
fs.writeFileSync(
monitorScriptPath,
`import Underpost from ${JSON.stringify(pathToFileURL(path.join(repoRoot, 'src/index.js')).href)};
const options = {};
if (process.env.MON_READY_GATE) options.readyGate = process.env.MON_READY_GATE;
if (process.env.MON_TRANSPORT) options.statusTransport = process.env.MON_TRANSPORT;
try {
await Underpost.monitor.monitorReadyRunner(${JSON.stringify(DEPLOY_ID)}, ${JSON.stringify(ENV)}, ${JSON.stringify(
TRAFFIC,
)}, [], 'default', options);
process.exit(0);
} catch (_) {
process.exit(1);
}
`,
);
});
after(async () => {
await stopInternalStatusServer();
delete process.env.UNDERPOST_INTERNAL_PORT;
if (prevPrefix === undefined) delete process.env.npm_config_prefix;
else process.env.npm_config_prefix = prevPrefix;
fs.removeSync(tmpPrefix);
});
beforeEach(() => {
// Deploy in flight: app not yet reporting running.
Underpost.env.set('container-status', INIT_STATUS);
// start-container-status is unset (empty) — the readinessProbe
// sees 503 until the start pipeline completes and stamps this key.
Underpost.env.set('start-container-status', '');
});
const spawnMonitor = (overrides = {}) =>
new Promise((resolve) => {
const envVars = {
PATH: `${fakeBinDir}:${process.env.PATH}`,
UNDERPOST_ENV_FILE: envFile,
POD_NAME,
npm_config_prefix: tmpPrefix,
UNDERPOST_INTERNAL_PORT: String(INTERNAL_PORT),
UNDERPOST_PF_LOCAL_PORT: String(INTERNAL_PORT),
UNDERPOST_MONITOR_DELAY_MS: '100',
UNDERPOST_MONITOR_MAX_ITERATIONS: '60',
UNDERPOST_PF_ATTEMPTS: '3',
FAKE_POD_READY: 'True',
...overrides,
};
const prefix = Object.entries(envVars)
.map(([k, v]) => `${k}="${v}"`)
.join(' ');
shellExec(`${prefix} ${node} ${monitorScriptPath}`, {
async: true,
silent: true,
disableLog: true,
callback: (code) => resolve(code),
});
});
it('success (default exec transport): both phases satisfied → monitor exits 0', async () => {
Underpost.env.set('container-status', RUNNING_STATUS);
// start-container-status not needed for monitor (reads container-status);
// this test exercises the default exec transport path.
const code = await spawnMonitor({ FAKE_POD_READY: 'True' });
expect(code).to.equal(0);
});
it('success (opt-in http transport): both phases satisfied → monitor exits 0', async () => {
Underpost.env.set('container-status', RUNNING_STATUS);
Underpost.env.set('start-container-status', RUNNING_STATUS);
const code = await spawnMonitor({ FAKE_POD_READY: 'True', MON_TRANSPORT: 'http' });
expect(code).to.equal(0);
});
it('runtime failure: container-status=error → monitor exits 1', async () => {
const monitorExit = spawnMonitor({ FAKE_POD_READY: 'True' });
Underpost.env.set('container-status', 'error');
expect(await monitorExit).to.equal(1);
});
it('readiness mismatch: runtime running but pod not Ready → never succeeds (exits 1)', async () => {
// Phase 2 satisfied, Phase 1 not: success requires BOTH, so it must time out.
Underpost.env.set('container-status', RUNNING_STATUS);
// start-container-status is NOT set — the readinessProbe would fail,
// but the monitor only checks container-status via exec/http transport.
const code = await spawnMonitor({ FAKE_POD_READY: 'False', UNDERPOST_MONITOR_MAX_ITERATIONS: '4' });
expect(code).to.equal(1);
});
it('transport failure (http): endpoint unreachable is never success (exits 1)', async () => {
Underpost.env.set('container-status', RUNNING_STATUS);
const code = await spawnMonitor({
MON_TRANSPORT: 'http',
UNDERPOST_INTERNAL_PORT: String(CLOSED_PORT),
UNDERPOST_PF_LOCAL_PORT: String(CLOSED_PORT),
UNDERPOST_MONITOR_MAX_ITERATIONS: '3',
});
expect(code).to.equal(1);
});
it('timeout: runtime stuck initializing → monitor exits 1', async () => {
Underpost.env.set('container-status', INIT_STATUS);
// start-container-status remains unset — readinessProbe returns 503
// (correct, since the runtime is still initializing).
const code = await spawnMonitor({ FAKE_POD_READY: 'True', UNDERPOST_MONITOR_MAX_ITERATIONS: '4' });
expect(code).to.equal(1);
});
it('regression: advanced pod whose runtime status falls back → monitor exits 1', async () => {
// Pod advances past build, then its reported status regresses (pod restart);
// the monitor must treat this as a failure rather than wait it out.
Underpost.env.set('container-status', INIT_STATUS);
const monitorExit = spawnMonitor({ FAKE_POD_READY: 'False', UNDERPOST_MONITOR_MAX_ITERATIONS: '120' });
await new Promise((r) => setTimeout(r, 1500));
Underpost.env.set('container-status', BUILD_STATUS);
expect(await monitorExit).to.equal(1);
});
it('readinessProbe: start-container-status set → returns 200 (probe passes)', async () => {
Underpost.env.set('start-container-status', RUNNING_STATUS);
const res = await fetch(`http://127.0.0.1:${INTERNAL_PORT}/_internal/ready`);
expect(res.status).to.equal(200);
const body = await res.json();
expect(body.status).to.equal('running-deployment');
});
it('readinessProbe: start-container-status unset → returns 503 (probe fails)', async () => {
// Unset from beforeEach re-stamp; clear it.
Underpost.env.set('start-container-status', '');
const res = await fetch(`http://127.0.0.1:${INTERNAL_PORT}/_internal/ready`);
expect(res.status).to.equal(503);
});
// Custom instances (cyberia-*) gate on K8s Ready and read status via exec;
// their runtime never stamps `running-deployment` (stays `initializing`).
it('instance (kubernetes gate + exec): K8s Ready with initializing status → exits 0', async () => {
Underpost.env.set('container-status', INIT_STATUS);
const code = await spawnMonitor({ FAKE_POD_READY: 'True', MON_READY_GATE: 'kubernetes', MON_TRANSPORT: 'exec' });
expect(code).to.equal(0);
});
it('instance (kubernetes gate + exec): container-status=error → exits 1', async () => {
const monitorExit = spawnMonitor({ FAKE_POD_READY: 'True', MON_READY_GATE: 'kubernetes', MON_TRANSPORT: 'exec' });
Underpost.env.set('container-status', 'error');
expect(await monitorExit).to.equal(1);
});
});