feat: add onebox basic lifecycle scenario

This commit is contained in:
2026-04-29 01:30:21 +00:00
parent 733b56398c
commit 3f01f3ebdc
5 changed files with 303 additions and 4 deletions
+3 -2
View File
@@ -6,10 +6,11 @@
"description": "Whole-system integration scenarios for serve.zone components.",
"scripts": {
"bootstrap:components": "pnpm --dir ../interfaces install && pnpm --dir ../api install && pnpm --dir ../cloudly install && pnpm --dir ../coreflow install && pnpm --dir ../coretraffic install && pnpm install",
"test": "pnpm scenario:registry-deploy-on-push",
"test": "pnpm scenario:registry-deploy-on-push && pnpm scenario:onebox-basic-lifecycle",
"scenario:registry-deploy-on-push": "tsx --tsconfig ../cloudly/tsconfig.json scenarios/registry-deploy-on-push/scenario.ts",
"scenario:onebox-basic-lifecycle": "deno run --allow-all --config ../onebox/deno.json scenarios/onebox-basic-lifecycle/scenario.ts",
"vagrant:up": "vagrant up",
"vagrant:test": "vagrant ssh -c 'cd /serve.zone/testing && pnpm bootstrap:components && pnpm scenario:registry-deploy-on-push'",
"vagrant:test": "vagrant ssh -c 'cd /serve.zone/testing && pnpm bootstrap:components && pnpm test'",
"vagrant:destroy": "vagrant destroy -f"
},
"devDependencies": {
+2 -1
View File
@@ -7,6 +7,7 @@ Fast package tests stay in each component repo. This repo is for stateful cross-
## Scenarios
- `registry-deploy-on-push`: starts Cloudly with isolated Mongo/S3 helpers, connects Coreflow as a cluster, pushes a Docker image to Cloudly's built-in registry, verifies Cloudly metadata updates, verifies Coreflow creates the workload service, verifies Coretraffic HTTPS routing, then pushes the same tag again and verifies service recreation and routing through the new digest.
- `onebox-basic-lifecycle`: starts Onebox in dev mode, verifies core services, deploys a workload, checks HTTP plus HTTPS routing through ingress, removes the workload, and verifies cleanup.
## Host Run
@@ -14,7 +15,7 @@ Requires Docker with Swarm already active.
```bash
pnpm bootstrap:components
pnpm scenario:registry-deploy-on-push
pnpm test
```
## Vagrant Run
@@ -0,0 +1,12 @@
# onebox-basic-lifecycle
This scenario verifies Onebox's basic single-server lifecycle.
Assertions:
- Onebox initializes its database, Docker integration, reverse proxy, registry, and platform services.
- The core reverse proxy platform service is exposed as `smartproxy`.
- A workload service can be deployed on the Onebox overlay network.
- HTTP and HTTPS ingress route to the workload through the reverse proxy.
- The workload can be removed and its Docker service is cleaned up.
- Onebox shutdown stops the reverse proxy and built-in registry cleanly.
@@ -0,0 +1,280 @@
import { dirname, fromFileUrl, join, resolve } from '@std/path';
import { Onebox } from '../../../onebox/ts/classes/onebox.ts';
const scenarioName = 'onebox-basic-lifecycle';
const smokeId = `onebox-basic-${Date.now().toString(36)}`;
const testingDir = resolve(dirname(fromFileUrl(import.meta.url)), '../..');
const buildDir = join(testingDir, '.nogit', scenarioName, smokeId);
const serviceName = `app-${Date.now().toString(36)}`;
const dockerServiceName = `onebox-${serviceName}`;
const routeDomain = `${serviceName}.test`;
const delayFor = async (millisecondsArg: number) => {
await new Promise((resolveArg) => setTimeout(resolveArg, millisecondsArg));
};
const run = async (commandArg: string, argsArg: string[]) => {
const command = new Deno.Command(commandArg, {
args: argsArg,
stdout: 'piped',
stderr: 'piped',
});
const output = await command.output();
const stdout = new TextDecoder().decode(output.stdout).trim();
const stderr = new TextDecoder().decode(output.stderr).trim();
if (stdout) {
console.log(stdout);
}
if (stderr) {
console.log(stderr);
}
if (!output.success) {
throw new Error(`${commandArg} ${argsArg.join(' ')} exited with ${output.code}`);
}
return { stdout, stderr };
};
const waitFor = async (checkFunctionArg: () => boolean | Promise<boolean>, messageArg: string) => {
const startTime = Date.now();
while (Date.now() - startTime < 120000) {
if (await checkFunctionArg()) {
return;
}
await delayFor(500);
}
throw new Error(`Timed out waiting for ${messageArg}`);
};
const dockerServiceExists = async (serviceNameArg: string) => {
const command = new Deno.Command('docker', {
args: ['service', 'inspect', serviceNameArg],
stdout: 'null',
stderr: 'null',
});
return (await command.output()).success;
};
const removeDockerService = async (serviceNameArg: string) => {
if (await dockerServiceExists(serviceNameArg)) {
await run('docker', ['service', 'rm', serviceNameArg]).catch(() => null);
await delayFor(2000);
}
};
const assertNoPreexistingOneboxIngress = async () => {
if (await dockerServiceExists('onebox-smartproxy')) {
throw new Error('onebox-smartproxy already exists; refusing to overwrite a running Onebox ingress service');
}
};
const waitForDockerServiceRunning = async (serviceNameArg: string) => {
await waitFor(async () => {
const command = new Deno.Command('docker', {
args: ['service', 'ps', serviceNameArg, '--format', '{{.CurrentState}}'],
stdout: 'piped',
stderr: 'null',
});
const output = await command.output();
if (!output.success) {
return false;
}
return new TextDecoder().decode(output.stdout).includes('Running');
}, `${serviceNameArg} running task`);
};
const waitForDockerServiceRemoved = async (serviceNameArg: string) => {
await waitFor(async () => !(await dockerServiceExists(serviceNameArg)), `${serviceNameArg} removal`);
};
const requestRoute = async (protocolArg: 'http' | 'https', portArg: number) => {
const curlArgs = [
'-sS',
'--noproxy',
'*',
'--max-time',
'10',
'--resolve',
`${routeDomain}:${portArg}:127.0.0.1`,
'-o',
'-',
'-w',
'\n%{http_code}',
`${protocolArg}://${routeDomain}:${portArg}/`,
];
if (protocolArg === 'https') {
curlArgs.unshift('-k');
}
const command = new Deno.Command('curl', {
args: curlArgs,
stdout: 'piped',
stderr: 'piped',
});
const output = await command.output();
const stdout = new TextDecoder().decode(output.stdout).trim();
const stderr = new TextDecoder().decode(output.stderr).trim();
if (!output.success) {
throw new Error(`curl failed: ${stderr || stdout}`);
}
const lines = stdout.split('\n');
const statusCode = lines[lines.length - 1];
const body = lines.slice(0, -1).join('\n');
return { statusCode, body, stderr };
};
const waitForRoute = async (protocolArg: 'http' | 'https', portArg: number) => {
let lastResponse: Awaited<ReturnType<typeof requestRoute>> | undefined;
let lastError: Error | undefined;
try {
await waitFor(async () => {
try {
lastResponse = await requestRoute(protocolArg, portArg);
lastError = undefined;
return lastResponse.statusCode === '200' && /Caddy|serve/i.test(lastResponse.body);
} catch (error) {
lastError = error as Error;
return false;
}
}, `${protocolArg.toUpperCase()} route through Onebox ingress`);
} catch (error) {
console.log(`[${scenarioName}] Last route response: ${JSON.stringify(lastResponse)}`);
if (lastError) {
console.log(`[${scenarioName}] Last route error: ${lastError.message}`);
}
await run('docker', ['service', 'ps', 'onebox-smartproxy']).catch(() => null);
await run('docker', ['service', 'logs', '--raw', '--tail', '120', 'onebox-smartproxy']).catch(() => null);
await run('docker', ['service', 'ps', dockerServiceName]).catch(() => null);
await run('docker', ['service', 'logs', '--raw', '--tail', '120', dockerServiceName]).catch(() => null);
throw error;
}
};
const createSelfSignedCertificate = async () => {
const keyPath = join(buildDir, 'route.key');
const certPath = join(buildDir, 'route.crt');
await run('openssl', [
'req',
'-x509',
'-newkey',
'rsa:2048',
'-nodes',
'-keyout',
keyPath,
'-out',
certPath,
'-subj',
`/CN=${routeDomain}`,
'-addext',
`subjectAltName=DNS:${routeDomain}`,
'-days',
'1',
]);
return {
privateKey: await Deno.readTextFile(keyPath),
publicKey: await Deno.readTextFile(certPath),
};
};
const ensureDockerReady = async () => {
await run('docker', ['version']);
const { stdout } = await run('docker', ['info', '--format', '{{.Swarm.LocalNodeState}}']);
if (stdout.trim() !== 'active') {
throw new Error('Docker Swarm must be active. In Vagrant this is handled by scripts/provision-vm.sh.');
}
};
const assertOneboxCoreReady = async (oneboxArg: Onebox) => {
const status = await oneboxArg.getSystemStatus();
const reverseProxy = status.reverseProxy as { backend: string; http: { running: boolean }; https: { running: boolean } };
if (reverseProxy.backend !== 'smartproxy-docker') {
throw new Error(`Unexpected reverse proxy backend: ${JSON.stringify(reverseProxy)}`);
}
if (!reverseProxy.http.running || !reverseProxy.https.running) {
throw new Error(`Reverse proxy not fully running: ${JSON.stringify(reverseProxy)}`);
}
const registryStatus = oneboxArg.registry.getStatus();
if (!registryStatus.running || registryStatus.port !== 4000) {
throw new Error(`Registry not running: ${JSON.stringify(registryStatus)}`);
}
const platformServices = status.platformServices as Array<{ type: string; status: string }>;
if (!platformServices.some((serviceArg) => serviceArg.type === 'smartproxy' && serviceArg.status === 'running')) {
throw new Error(`SmartProxy platform service not reported as running: ${JSON.stringify(platformServices)}`);
}
if (platformServices.some((serviceArg) => serviceArg.type === 'caddy')) {
throw new Error(`Unexpected legacy caddy platform service: ${JSON.stringify(platformServices)}`);
}
};
const main = async () => {
let onebox: Onebox | undefined;
let deployedService = false;
try {
await ensureDockerReady();
await assertNoPreexistingOneboxIngress();
await Deno.mkdir(buildDir, { recursive: true });
Deno.chdir(buildDir);
Deno.env.set('ONEBOX_DEV', 'true');
console.log(`[${scenarioName}] Starting Onebox from ${buildDir}`);
onebox = new Onebox();
await onebox.init();
await waitForDockerServiceRunning('onebox-smartproxy');
await assertOneboxCoreReady(onebox);
console.log(`[${scenarioName}] Deploying workload ${serviceName}`);
const service = await onebox.services.deployService({
name: serviceName,
image: 'caddy:2-alpine',
port: 80,
domain: routeDomain,
autoDNS: false,
envVars: {},
});
deployedService = true;
if (service.status !== 'running' || service.domain !== routeDomain) {
throw new Error(`Unexpected deployed service state: ${JSON.stringify(service)}`);
}
if (!onebox.services.listServices().some((serviceArg) => serviceArg.name === serviceName)) {
throw new Error('Deployed service not present in Onebox service list');
}
await waitForDockerServiceRunning(dockerServiceName);
await waitForRoute('http', 8080);
const certificate = await createSelfSignedCertificate();
await onebox.reverseProxy.addCertificate(routeDomain, certificate.publicKey, certificate.privateKey);
await waitForRoute('https', 8443);
console.log(`[${scenarioName}] Removing workload ${serviceName}`);
await onebox.services.removeService(serviceName);
deployedService = false;
await waitForDockerServiceRemoved(dockerServiceName);
if (onebox.services.getService(serviceName)) {
throw new Error('Removed service still present in Onebox database');
}
console.log(`[${scenarioName}] Scenario passed`);
} finally {
if (onebox && deployedService) {
await onebox.services.removeService(serviceName).catch((error) => {
console.log(`[${scenarioName}] Failed to remove Onebox service: ${(error as Error).message}`);
});
}
if (onebox) {
await onebox.shutdown().catch((error) => {
console.log(`[${scenarioName}] Failed to shut down Onebox: ${(error as Error).message}`);
});
}
await removeDockerService(dockerServiceName);
await removeDockerService('onebox-smartproxy');
}
};
await main();
+6 -1
View File
@@ -4,7 +4,7 @@ set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y ca-certificates curl git docker.io openssl
apt-get install -y ca-certificates curl git docker.io openssl unzip
if [ -d /serve.zone ]; then
chown -R vagrant:vagrant /serve.zone
@@ -18,6 +18,10 @@ if ! command -v node >/dev/null 2>&1; then
apt-get install -y nodejs
fi
if ! command -v deno >/dev/null 2>&1; then
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh
fi
corepack enable
corepack prepare pnpm@10.7.0 --activate
@@ -28,3 +32,4 @@ fi
docker pull caddy:2-alpine
docker pull node:22-trixie-slim
docker pull code.foss.global/host.today/ht-docker-smartproxy:latest