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, 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> | 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();