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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
280 lines
14 KiB
JavaScript
280 lines
14 KiB
JavaScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { parse, parseAllDocuments } from 'yaml';
|
|
|
|
const repoRoot = path.resolve(path.join(import.meta.dirname, '..', '..', '..', '..'));
|
|
const buildScript = path.join(repoRoot, 'ee', 'appliance', 'ubuntu-iso', 'scripts', 'build-ubuntu-appliance-iso.sh');
|
|
const userDataPath = path.join(repoRoot, 'ee', 'appliance', 'ubuntu-iso', 'config', 'nocloud', 'user-data');
|
|
const overlayRoot = path.join(repoRoot, 'ee', 'appliance', 'ubuntu-iso', 'overlay', 'opt', 'alga-appliance');
|
|
const storageManifestPath = path.join(repoRoot, 'ee', 'appliance', 'manifests', 'local-path-storage.yaml');
|
|
const algaCoreChartPath = path.join(repoRoot, 'helm');
|
|
const temporalChartPath = path.join(repoRoot, 'ee', 'helm', 'temporal');
|
|
const temporalWorkerChartPath = path.join(repoRoot, 'ee', 'helm', 'temporal-worker');
|
|
const temporalProfileValuesPath = path.join(repoRoot, 'ee', 'appliance', 'flux', 'profiles', 'single-node', 'values', 'temporal.single-node.yaml');
|
|
const temporalWorkerProfileValuesPath = path.join(repoRoot, 'ee', 'appliance', 'flux', 'profiles', 'single-node', 'values', 'temporal-worker.single-node.yaml');
|
|
const fluxReleaseDir = path.join(repoRoot, 'ee', 'appliance', 'flux', 'base', 'releases');
|
|
const applianceStatusManifestPath = path.join(repoRoot, 'ee', 'appliance', 'flux', 'base', 'platform', 'appliance-status.yaml');
|
|
|
|
function run(command, args, env = process.env) {
|
|
return spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8', env });
|
|
}
|
|
|
|
function writeFakeXorriso(binDir, logFile, labelFile) {
|
|
const fakeXorriso = path.join(binDir, 'xorriso');
|
|
fs.writeFileSync(fakeXorriso, `#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
printf '%s\\n' "$*" >> "$FAKE_XORRISO_LOG"
|
|
|
|
if [[ "\${1:-}" == "-osirrox" ]]; then
|
|
args=("$@")
|
|
dest="\${args[$((\${#args[@]} - 1))]}"
|
|
mkdir -p "$dest/boot/grub" "$dest/casper" "$dest/EFI/boot"
|
|
cat > "$dest/boot/grub/grub.cfg" <<'CFG'
|
|
set timeout=30
|
|
menuentry "Try or Install Ubuntu Server" {
|
|
set gfxpayload=keep
|
|
linux /casper/vmlinuz ---
|
|
initrd /casper/initrd
|
|
}
|
|
CFG
|
|
cp "$dest/boot/grub/grub.cfg" "$dest/boot/grub/loopback.cfg"
|
|
touch "$dest/casper/vmlinuz" "$dest/casper/initrd" "$dest/EFI/boot/bootx64.efi"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "\${1:-}" == "-as" && "\${2:-}" == "mkisofs" ]]; then
|
|
args=("$@")
|
|
output=""
|
|
label=""
|
|
for ((i = 0; i < \${#args[@]}; i++)); do
|
|
case "\${args[$i]}" in
|
|
-o)
|
|
output="\${args[$((i + 1))]}"
|
|
;;
|
|
-V)
|
|
label="\${args[$((i + 1))]}"
|
|
;;
|
|
esac
|
|
done
|
|
printf '%s\\n' "$label" > "$FAKE_XORRISO_LABEL"
|
|
printf 'fake iso\\n' > "$output"
|
|
exit 0
|
|
fi
|
|
|
|
exit 2
|
|
`);
|
|
fs.chmodSync(fakeXorriso, 0o755);
|
|
}
|
|
|
|
function helmTemplate(chartPath, valuesPath) {
|
|
const args = ['template', 'test-release', chartPath, '--namespace', 'msp'];
|
|
if (valuesPath) {
|
|
args.push('-f', valuesPath);
|
|
}
|
|
const result = run('helm', args);
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
return parseAllDocuments(result.stdout).map((doc) => doc.toJSON()).filter(Boolean);
|
|
}
|
|
|
|
function runBuildWithFakeXorriso() {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-ubuntu-iso-test-'));
|
|
const tmpIso = path.join(tmp, 'base.iso');
|
|
const binDir = path.join(tmp, 'bin');
|
|
const workRoot = path.join(tmp, 'work');
|
|
const outputRoot = path.join(tmp, 'output');
|
|
const logFile = path.join(tmp, 'xorriso.log');
|
|
const labelFile = path.join(tmp, 'xorriso-label.txt');
|
|
const releaseVersion = `test-${Date.now()}`;
|
|
|
|
fs.mkdirSync(binDir);
|
|
fs.mkdirSync(workRoot);
|
|
fs.mkdirSync(outputRoot);
|
|
fs.writeFileSync(tmpIso, 'fake-iso-content');
|
|
writeFakeXorriso(binDir, logFile, labelFile);
|
|
|
|
const env = {
|
|
...process.env,
|
|
PATH: `${binDir}${path.delimiter}${process.env.PATH}`,
|
|
FAKE_XORRISO_LOG: logFile,
|
|
FAKE_XORRISO_LABEL: labelFile,
|
|
ALGA_APPLIANCE_ISO_WORK_DIR: workRoot,
|
|
ALGA_APPLIANCE_ISO_OUTPUT_DIR: outputRoot
|
|
};
|
|
|
|
const build = run('bash', [buildScript, '--base-iso', tmpIso, '--release-version', releaseVersion], env);
|
|
assert.equal(build.status, 0, build.stderr || build.stdout);
|
|
|
|
return { workRoot, outputRoot, releaseVersion, labelFile };
|
|
}
|
|
|
|
test('T001 build smoke: remastered ISO is branded and includes the offline appliance overlay', () => {
|
|
const tmpIso = path.join(os.tmpdir(), `alga-ubuntu-base-${Date.now()}.iso`);
|
|
const releaseVersion = `test-${Date.now()}`;
|
|
fs.writeFileSync(tmpIso, 'fake-iso-content');
|
|
|
|
const dryRun = run('bash', [buildScript, '--base-iso', tmpIso, '--release-version', releaseVersion, '--dry-run']);
|
|
assert.equal(dryRun.status, 0, dryRun.stderr || dryRun.stdout);
|
|
assert.match(dryRun.stdout, /layout validated/i);
|
|
|
|
const build = runBuildWithFakeXorriso();
|
|
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'appliance')), true);
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'host-service')), true);
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'operator')), true);
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'scripts')), true);
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'manifests', 'local-path-storage.yaml')), true);
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'flux')), true);
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'releases')), false);
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'status-ui', 'dist', 'index.html')), true);
|
|
assert.equal(fs.existsSync(path.join(overlayRoot, 'status-ui', 'dist', 'setup', 'index.html')), true);
|
|
|
|
const isoRoot = path.join(build.workRoot, 'iso-root');
|
|
assert.equal(fs.readFileSync(path.join(isoRoot, '.disk', 'info'), 'utf8'), 'AlgaPSA Install\n');
|
|
assert.equal(fs.readFileSync(build.labelFile, 'utf8'), 'ALGAPSA_INSTALL\n');
|
|
assert.equal(fs.existsSync(path.join(isoRoot, 'alga-overlay', 'opt', 'alga-appliance', 'host-service')), true);
|
|
assert.equal(fs.existsSync(path.join(isoRoot, 'alga-overlay', 'opt', 'alga-appliance', 'releases')), false);
|
|
|
|
const grubConfig = fs.readFileSync(path.join(isoRoot, 'boot', 'grub', 'grub.cfg'), 'utf8');
|
|
assert.match(grubConfig, /menuentry "AlgaPSA Install"/);
|
|
assert.match(grubConfig, /autoinstall ds=nocloud\\;s=\/cdrom\/nocloud\//);
|
|
|
|
const isoOut = path.join(build.outputRoot, `alga-appliance-ubuntu-${build.releaseVersion}.iso`);
|
|
const shaOut = `${isoOut}.sha256`;
|
|
assert.equal(fs.existsSync(isoOut), true);
|
|
assert.equal(fs.existsSync(shaOut), true);
|
|
});
|
|
|
|
test('T002 installer config sanity: network and storage remain user-confirmed before install actions', () => {
|
|
const userData = parse(fs.readFileSync(userDataPath, 'utf8'));
|
|
const lateCommands = userData.autoinstall['late-commands'];
|
|
|
|
assert.deepEqual(userData.autoinstall['interactive-sections'], ['network', 'storage']);
|
|
assert.equal(userData.autoinstall.storage.layout.name, 'direct');
|
|
assert.equal(userData.autoinstall.storage.layout['sizing-policy'], 'all');
|
|
assert.equal(userData.autoinstall.ssh['install-server'], true);
|
|
assert.equal(userData.autoinstall.ssh['allow-pw'], true);
|
|
assert.equal(lateCommands.some((command) => command.includes('passwd -l alga-admin')), false);
|
|
});
|
|
|
|
test('T003 storage manifest avoids k3s default local-path RBAC collisions and grants configmap access', () => {
|
|
const docs = parseAllDocuments(fs.readFileSync(storageManifestPath, 'utf8')).map((doc) => doc.toJSON());
|
|
const clusterRole = docs.find((doc) => doc?.kind === 'ClusterRole' && doc.metadata?.name === 'alga-local-path-provisioner-cluster-role');
|
|
const clusterRoleBinding = docs.find((doc) => doc?.kind === 'ClusterRoleBinding' && doc.metadata?.name === 'alga-local-path-provisioner-cluster-bind');
|
|
const namespacedRole = docs.find((doc) => doc?.kind === 'Role' && doc.metadata?.name === 'alga-local-path-provisioner-namespaced-role');
|
|
const namespacedRoleBinding = docs.find((doc) => doc?.kind === 'RoleBinding' && doc.metadata?.name === 'alga-local-path-provisioner-namespaced-bind');
|
|
|
|
assert.ok(clusterRole);
|
|
assert.ok(clusterRoleBinding);
|
|
assert.ok(namespacedRole);
|
|
assert.ok(namespacedRoleBinding);
|
|
assert.equal(docs.some((doc) => doc?.kind === 'ClusterRoleBinding' && doc.metadata?.name === 'local-path-provisioner-bind'), false);
|
|
assert.equal(clusterRoleBinding.roleRef.name, 'alga-local-path-provisioner-cluster-role');
|
|
assert.equal(namespacedRoleBinding.roleRef.name, 'alga-local-path-provisioner-namespaced-role');
|
|
assert.ok(namespacedRole.rules.some((rule) => rule.resources.includes('configmaps') && rule.verbs.includes('get')));
|
|
assert.ok(clusterRole.rules.some((rule) => rule.resources.includes('configmaps') && rule.verbs.includes('get')));
|
|
});
|
|
|
|
test('T004 Temporal chart waits for schema-safe startup and creates the default namespace with a Helm hook', () => {
|
|
const docs = helmTemplate(temporalChartPath, temporalProfileValuesPath);
|
|
const deployment = docs.find((doc) => doc.kind === 'Deployment' && doc.metadata?.name === 'test-release-temporal');
|
|
const job = docs.find((doc) => doc.kind === 'Job' && doc.metadata?.name === 'test-release-temporal-namespace-init');
|
|
|
|
assert.ok(deployment);
|
|
assert.ok(job);
|
|
const temporalContainer = deployment.spec.template.spec.containers.find((container) => container.name === 'temporal');
|
|
const env = Object.fromEntries(temporalContainer.env.map((entry) => [entry.name, entry.value]));
|
|
assert.equal(env.DEFAULT_NAMESPACE, 'default');
|
|
assert.equal(env.SKIP_DEFAULT_NAMESPACE_CREATION, 'true');
|
|
assert.equal(env.SKIP_ADD_CUSTOM_SEARCH_ATTRIBUTES, 'true');
|
|
assert.equal(temporalContainer.livenessProbe.initialDelaySeconds, 300);
|
|
assert.ok(temporalContainer.startupProbe.failureThreshold >= 60);
|
|
|
|
assert.equal(job.metadata.annotations['helm.sh/hook'], 'post-install,post-upgrade');
|
|
assert.match(job.spec.template.spec.containers[0].command.join('\n'), /temporal operator namespace create/);
|
|
});
|
|
|
|
test('T005 temporal-worker profile injects NEXTAUTH_SECRET from alga-core secrets', () => {
|
|
const docs = helmTemplate(temporalWorkerChartPath, temporalWorkerProfileValuesPath);
|
|
const deployment = docs.find((doc) => doc.kind === 'Deployment' && doc.metadata?.name === 'test-release-temporal-worker');
|
|
assert.ok(deployment);
|
|
|
|
const env = deployment.spec.template.spec.containers[0].env;
|
|
const nextAuth = env.find((entry) => entry.name === 'NEXTAUTH_SECRET');
|
|
assert.deepEqual(nextAuth.valueFrom.secretKeyRef, {
|
|
name: 'alga-core-sebastian-secrets',
|
|
key: 'NEXTAUTH_SECRET'
|
|
});
|
|
});
|
|
|
|
test('T006 alga-core appliance profile gives the app Deployment a first-install-safe progress deadline', () => {
|
|
const docs = helmTemplate(algaCoreChartPath, path.join(repoRoot, 'ee', 'appliance', 'flux', 'profiles', 'single-node', 'values', 'alga-core.single-node.yaml'));
|
|
const deployment = docs.find((doc) => doc.kind === 'Deployment' && doc.metadata?.name === 'test-release-sebastian');
|
|
assert.ok(deployment);
|
|
assert.ok(deployment.spec.progressDeadlineSeconds >= 1800);
|
|
});
|
|
|
|
test('T006b alga-core appliance bootstrap is serialized and avoids duplicate first-boot app image pulls', () => {
|
|
const docs = helmTemplate(algaCoreChartPath, path.join(repoRoot, 'ee', 'appliance', 'flux', 'profiles', 'single-node', 'values', 'alga-core.single-node.yaml'));
|
|
const job = docs.find((doc) => doc.kind === 'Job' && doc.metadata?.name === 'test-release-sebastian-bootstrap');
|
|
const deployment = docs.find((doc) => doc.kind === 'Deployment' && doc.metadata?.name === 'test-release-sebastian');
|
|
const configMap = docs.find((doc) => doc.kind === 'ConfigMap' && doc.metadata?.name === 'test-release-sebastian-appliance-bootstrap');
|
|
|
|
assert.ok(job);
|
|
assert.equal(job.metadata.annotations['helm.sh/hook'], 'post-install,post-upgrade');
|
|
assert.match(job.metadata.annotations['helm.sh/hook-delete-policy'], /before-hook-creation/);
|
|
assert.equal(job.spec.backoffLimit, 0);
|
|
assert.equal(job.spec.template.spec.containers[0].imagePullPolicy, 'IfNotPresent');
|
|
|
|
assert.ok(deployment);
|
|
const waitInit = deployment.spec.template.spec.initContainers.find((container) => container.name === 'wait-for-bootstrap');
|
|
assert.ok(waitInit);
|
|
assert.equal(waitInit.image, 'ankane/pgvector:latest');
|
|
assert.equal(waitInit.imagePullPolicy, 'IfNotPresent');
|
|
assert.equal(deployment.spec.template.spec.containers[0].imagePullPolicy, 'IfNotPresent');
|
|
|
|
assert.ok(configMap);
|
|
const script = configMap.data['appliance-bootstrap.sh'];
|
|
assert.match(script, /createdb_admin\(\)/);
|
|
assert.match(script, /acquire_bootstrap_lock\(\)/);
|
|
assert.match(script, /alga_appliance_bootstrap_lock/);
|
|
assert.match(script, /clear_stale_knex_migration_lock/);
|
|
assert.match(script, /Database .* was created by another bootstrap attempt/);
|
|
});
|
|
|
|
test('T007 Flux HelmReleases retry transient single-node install stalls', () => {
|
|
for (const file of fs.readdirSync(fluxReleaseDir).filter((name) => name.endsWith('.yaml'))) {
|
|
const doc = parse(fs.readFileSync(path.join(fluxReleaseDir, file), 'utf8'));
|
|
assert.ok(doc.spec.install.remediation.retries >= 1, `${file} install retries`);
|
|
assert.ok(doc.spec.upgrade.remediation.retries >= 1, `${file} upgrade retries`);
|
|
}
|
|
});
|
|
|
|
test('T008 platform appliance-status does not bind host port 8080 used by the Ubuntu host service', () => {
|
|
const docs = parseAllDocuments(fs.readFileSync(applianceStatusManifestPath, 'utf8')).map((doc) => doc.toJSON()).filter(Boolean);
|
|
const deployment = docs.find((doc) => doc.kind === 'Deployment' && doc.metadata?.name === 'appliance-status');
|
|
assert.ok(deployment);
|
|
assert.notEqual(deployment.spec.template.spec.hostNetwork, true);
|
|
assert.equal(deployment.spec.template.spec.dnsPolicy, 'ClusterFirst');
|
|
});
|
|
|
|
test('T009/T010 install flow sanity: payload copy and disk-first marker are present in generated installer inputs', () => {
|
|
const userData = parse(fs.readFileSync(userDataPath, 'utf8'));
|
|
const lateCommands = userData.autoinstall['late-commands'];
|
|
const build = runBuildWithFakeXorriso();
|
|
const grubConfig = fs.readFileSync(path.join(build.workRoot, 'iso-root', 'boot', 'grub', 'grub.cfg'), 'utf8');
|
|
|
|
assert.ok(lateCommands.some((command) => command.includes('/cdrom/alga-overlay') && command.includes('/target/')));
|
|
assert.ok(lateCommands.some((command) => command.includes('systemctl enable alga-appliance.service')));
|
|
assert.ok(lateCommands.some((command) => command.includes('systemctl enable alga-appliance-console.service')));
|
|
assert.ok(lateCommands.some((command) => command.includes('/target/etc/alga-appliance/booted-from-disk')));
|
|
assert.ok(lateCommands.some((command) => command.includes('lvextend -r -l +100%FREE /dev/ubuntu-vg/ubuntu-lv')));
|
|
|
|
assert.match(grubConfig, /search --no-floppy --file --set=alga_root \/etc\/alga-appliance\/booted-from-disk/);
|
|
assert.match(grubConfig, /configfile \/boot\/grub\/grub.cfg/);
|
|
});
|