PSA/ee/appliance/ubuntu-iso/scripts/preflight-appliance-smoke.mjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

322 lines
14 KiB
JavaScript
Executable File

#!/usr/bin/env node
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { parse, parseAllDocuments } from 'yaml';
const repoRoot = path.resolve(path.join(import.meta.dirname, '..', '..', '..', '..'));
const expectedHelmReleases = ['alga-core', 'pgbouncer', 'temporal', 'workflow-worker', 'email-service', 'temporal-worker'];
function parseArgs(argv) {
const args = {
channel: 'stable',
iso: '',
workRoot: '',
keepExtracted: false
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--channel') args.channel = argv[++i] || '';
else if (arg === '--iso') args.iso = argv[++i] || '';
else if (arg === '--work-root') args.workRoot = argv[++i] || '';
else if (arg === '--keep-extracted') args.keepExtracted = true;
else if (arg === '--help' || arg === '-h') args.help = true;
else throw new Error(`Unknown argument: ${arg}`);
}
args.channel = String(args.channel || '').trim();
args.iso = String(args.iso || '').trim();
args.workRoot = String(args.workRoot || '').trim();
return args;
}
function usage() {
return `Usage: node ee/appliance/ubuntu-iso/scripts/preflight-appliance-smoke.mjs [options]\n\nOptions:\n --channel <stable|nightly> Release channel submitted to setup (default: stable)\n --iso <path> Extract and validate a built appliance ISO\n --work-root <path> Validate an ISO build work root containing iso-root\n --keep-extracted Keep temporary ISO extraction directory\n`;
}
function run(command, args, options = {}) {
return spawnSync(command, args, {
cwd: options.cwd || repoRoot,
encoding: 'utf8',
input: options.input,
timeout: options.timeout || 120_000,
env: { ...process.env, ...(options.env || {}) }
});
}
function collect() {
const checks = [];
return {
pass(name, detail = '') { checks.push({ ok: true, name, detail }); },
fail(name, detail = '') { checks.push({ ok: false, name, detail }); },
get checks() { return checks; },
get failed() { return checks.filter((check) => !check.ok); }
};
}
function safeRm(target) {
if (!target || !fs.existsSync(target)) return;
let result = run('rm', ['-rf', target], { cwd: '/' });
if (result.status !== 0 && fs.existsSync(target)) {
run('chmod', ['-R', 'u+rwX', target], { cwd: '/' });
result = run('rm', ['-rf', target], { cwd: '/' });
}
if (result.status !== 0 && fs.existsSync(target)) {
process.stderr.write(`Warning: unable to remove temporary directory ${target}: ${(result.stderr || result.stdout || '').trim()}\n`);
}
}
function readText(file) {
return fs.readFileSync(file, 'utf8');
}
function parseYamlFile(file) {
return parse(readText(file));
}
function parseYamlDocsFile(file) {
return parseAllDocuments(readText(file)).map((doc) => doc.toJSON()).filter(Boolean);
}
function assertHelmReleaseRetries(checks, root, label) {
const releaseDir = path.join(root, 'ee', 'appliance', 'flux', 'base', 'releases');
for (const name of expectedHelmReleases) {
const file = path.join(releaseDir, `${name}.yaml`);
if (!fs.existsSync(file)) {
checks.fail(`${label}: ${name} HelmRelease exists`, file);
continue;
}
const doc = parseYamlFile(file);
const installRetries = Number(doc?.spec?.install?.remediation?.retries ?? 0);
const upgradeRetries = Number(doc?.spec?.upgrade?.remediation?.retries ?? 0);
if (installRetries >= 1 && upgradeRetries >= 1) {
checks.pass(`${label}: ${name} HelmRelease has retries`, `install=${installRetries}, upgrade=${upgradeRetries}`);
} else {
checks.fail(`${label}: ${name} HelmRelease has retries`, `install=${installRetries}, upgrade=${upgradeRetries}`);
}
}
}
function assertApplianceStatusNoHostNetwork(checks, root, label) {
const file = path.join(root, 'ee', 'appliance', 'flux', 'base', 'platform', 'appliance-status.yaml');
if (!fs.existsSync(file)) {
checks.fail(`${label}: appliance-status manifest exists`, file);
return;
}
const docs = parseYamlDocsFile(file);
const deployment = docs.find((doc) => doc.kind === 'Deployment' && doc.metadata?.name === 'appliance-status');
if (!deployment) {
checks.fail(`${label}: appliance-status Deployment exists`, file);
return;
}
if (deployment.spec?.template?.spec?.hostNetwork === true) {
checks.fail(`${label}: appliance-status does not use hostNetwork`, 'hostNetwork=true would collide with host service port 8080');
} else {
checks.pass(`${label}: appliance-status does not use hostNetwork`);
}
if (deployment.spec?.template?.spec?.dnsPolicy === 'ClusterFirst') {
checks.pass(`${label}: appliance-status dnsPolicy is ClusterFirst`);
} else {
checks.fail(`${label}: appliance-status dnsPolicy is ClusterFirst`, `dnsPolicy=${deployment.spec?.template?.spec?.dnsPolicy}`);
}
}
function assertAlgaCoreProgressDeadline(checks, root, label) {
const valuesFile = path.join(root, 'ee', 'appliance', 'flux', 'profiles', 'single-node', 'values', 'alga-core.single-node.yaml');
const deploymentTemplate = path.join(root, 'helm', 'templates', 'deployment.yaml');
const values = fs.existsSync(valuesFile) ? parseYamlFile(valuesFile) : null;
const template = fs.existsSync(deploymentTemplate) ? readText(deploymentTemplate) : '';
const progressDeadline = Number(values?.server?.progressDeadlineSeconds ?? 0);
if (template.includes('progressDeadlineSeconds') && template.includes('Values.server.progressDeadlineSeconds')) {
checks.pass(`${label}: alga-core chart renders configurable progressDeadlineSeconds`);
} else {
checks.fail(`${label}: alga-core chart renders configurable progressDeadlineSeconds`, deploymentTemplate);
}
if (progressDeadline >= 1800) {
checks.pass(`${label}: appliance alga-core progressDeadlineSeconds is first-install safe`, String(progressDeadline));
} else {
checks.fail(`${label}: appliance alga-core progressDeadlineSeconds is first-install safe`, `progressDeadlineSeconds=${progressDeadline}`);
}
}
function assertSetupUiAndApi(checks, root, label) {
const setupPage = path.join(root, 'ee', 'appliance', 'status-ui', 'app', 'setup', 'page.tsx');
const server = path.join(root, 'ee', 'appliance', 'host-service', 'server.mjs');
const setupEngine = path.join(root, 'ee', 'appliance', 'host-service', 'setup-engine.mjs');
const setupText = fs.existsSync(setupPage) ? readText(setupPage) : '';
if (setupText.includes('name="installCode"') && setupText.includes('name="channel"') && setupText.includes('name="releaseRef"')) {
checks.pass(`${label}: setup UI submits install code, channel, and optional release pin`);
} else {
checks.fail(`${label}: setup UI submits install code, channel, and optional release pin`, setupPage);
}
const serverText = fs.existsSync(server) ? readText(server) : '';
if (serverText.includes('acceptedInputs') && serverText.includes('releaseRef: payload.releaseRef') && serverText.includes('installCode: payload.installCode')) {
checks.pass(`${label}: setup API accepts registry release inputs and install code`);
} else {
checks.fail(`${label}: setup API accepts registry release inputs and install code`, server);
}
const setupEngineText = fs.existsSync(setupEngine) ? readText(setupEngine) : '';
if (setupEngineText.includes('resolveReleaseManifest') && setupEngineText.includes('DEFAULT_RELEASE_REPOSITORY') && setupEngineText.includes('releaseRef')) {
checks.pass(`${label}: setup engine resolves appliance release metadata from OCI`);
} else {
checks.fail(`${label}: setup engine resolves appliance release metadata from OCI`, setupEngine);
}
}
function assertStatusUiDist(checks, distRoot, label) {
if (!fs.existsSync(distRoot)) {
checks.fail(`${label}: status UI dist exists`, distRoot);
return;
}
const files = [];
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else files.push(full);
}
};
walk(distRoot);
const setupHtml = path.join(distRoot, 'setup', 'index.html');
if (fs.existsSync(setupHtml)) checks.pass(`${label}: setup static HTML exists`);
else checks.fail(`${label}: setup static HTML exists`, setupHtml);
const combined = files
.filter((file) => /\.(html|js)$/.test(file))
.map((file) => readText(file))
.join('\n');
if (combined.includes('setup-install-code') && combined.includes('releaseRef')) {
checks.pass(`${label}: status UI dist contains install code and release-pin payload logic`);
} else {
checks.fail(`${label}: status UI dist contains install code and release-pin payload logic`, distRoot);
}
}
function assertLocalRepo(checks) {
assertHelmReleaseRetries(checks, repoRoot, 'local source');
assertApplianceStatusNoHostNetwork(checks, repoRoot, 'local source');
assertAlgaCoreProgressDeadline(checks, repoRoot, 'local source');
assertSetupUiAndApi(checks, repoRoot, 'local source');
}
function overlayRootFromWorkRoot(workRoot) {
const direct = path.join(workRoot, 'iso-root', 'alga-overlay', 'opt', 'alga-appliance');
if (fs.existsSync(direct)) return direct;
const alternate = path.join(workRoot, 'alga-overlay', 'opt', 'alga-appliance');
if (fs.existsSync(alternate)) return alternate;
return direct;
}
function assertOverlay(checks, overlayRoot, label) {
const fluxRoot = path.join(overlayRoot, 'flux');
const pseudoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-overlay-root-'));
fs.mkdirSync(path.join(pseudoRoot, 'ee', 'appliance'), { recursive: true });
fs.symlinkSync(fluxRoot, path.join(pseudoRoot, 'ee', 'appliance', 'flux'));
assertHelmReleaseRetries(checks, pseudoRoot, label);
assertApplianceStatusNoHostNetwork(checks, pseudoRoot, label);
fs.rmSync(pseudoRoot, { recursive: true, force: true });
assertStatusUiDist(checks, path.join(overlayRoot, 'status-ui', 'dist'), label);
const server = path.join(overlayRoot, 'host-service', 'server.mjs');
const setupEngine = path.join(overlayRoot, 'host-service', 'setup-engine.mjs');
const serverText = fs.existsSync(server) ? readText(server) : '';
const setupEngineText = fs.existsSync(setupEngine) ? readText(setupEngine) : '';
if (serverText.includes('acceptedInputs') && serverText.includes('releaseRef: payload.releaseRef') && serverText.includes('installCode: payload.installCode')) {
checks.pass(`${label}: packaged setup API accepts registry release inputs and install code`);
} else {
checks.fail(`${label}: packaged setup API accepts registry release inputs and install code`, server);
}
if (setupEngineText.includes('resolveReleaseManifest') && setupEngineText.includes('DEFAULT_RELEASE_REPOSITORY')) {
checks.pass(`${label}: packaged setup engine resolves OCI release metadata`);
} else {
checks.fail(`${label}: packaged setup engine resolves OCI release metadata`, setupEngine);
}
}
function assertIso(checks, isoPath, keepExtracted) {
if (!fs.existsSync(isoPath)) {
checks.fail('ISO: file exists', isoPath);
return;
}
const xorriso = run('sh', ['-c', 'command -v xorriso']);
if (xorriso.status !== 0) {
checks.fail('ISO: xorriso available for extraction', 'Install xorriso or omit --iso.');
return;
}
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-iso-preflight-'));
fs.mkdirSync(path.join(tmp, '.disk'), { recursive: true });
const extractOverlay = run('xorriso', ['-osirrox', 'on', '-indev', isoPath, '-extract', '/alga-overlay', path.join(tmp, 'alga-overlay')], { timeout: 300_000 });
const extractDiskInfo = run('xorriso', ['-osirrox', 'on', '-indev', isoPath, '-extract', '/.disk/info', path.join(tmp, '.disk', 'info')], { timeout: 60_000 });
if (extractOverlay.status !== 0 || extractDiskInfo.status !== 0) {
checks.fail('ISO: critical artifact extraction succeeds', `${(extractOverlay.stderr || extractOverlay.stdout || '').trim()} ${(extractDiskInfo.stderr || extractDiskInfo.stdout || '').trim()}`.trim());
safeRm(tmp);
return;
}
checks.pass('ISO: critical artifact extraction succeeds', tmp);
assertOverlay(checks, path.join(tmp, 'alga-overlay', 'opt', 'alga-appliance'), 'ISO overlay');
const diskInfo = path.join(tmp, '.disk', 'info');
if (fs.existsSync(diskInfo) && readText(diskInfo).trim() === 'AlgaPSA Install') {
checks.pass('ISO: boot label is AlgaPSA Install');
} else {
checks.fail('ISO: boot label is AlgaPSA Install', diskInfo);
}
if (keepExtracted) {
checks.pass('ISO: extracted tree kept', tmp);
} else {
safeRm(tmp);
}
}
function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
process.stdout.write(usage());
return;
}
const checks = collect();
if (!['stable', 'nightly'].includes(args.channel)) {
checks.fail('inputs: channel is stable or nightly', args.channel);
} else {
checks.pass('inputs: channel is stable or nightly', args.channel);
}
assertLocalRepo(checks);
if (args.workRoot) {
assertOverlay(checks, overlayRootFromWorkRoot(args.workRoot), 'work-root overlay');
}
if (args.iso) {
assertIso(checks, args.iso, args.keepExtracted);
}
for (const check of checks.checks) {
const mark = check.ok ? 'PASS' : 'FAIL';
process.stdout.write(`${mark} ${check.name}${check.detail ? `${check.detail}` : ''}\n`);
}
if (checks.failed.length > 0) {
process.stderr.write(`\n${checks.failed.length} preflight gate(s) failed. Do not launch a fresh VM until these are fixed.\n`);
process.exitCode = 1;
return;
}
process.stdout.write('\nAll appliance smoke preflight gates passed.\n');
}
try {
main();
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
process.exitCode = 1;
}