@celilo/e2e 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cele2e +14 -0
- package/bin/e2e-cache-images +57 -0
- package/bin/e2e-shell +36 -13
- package/bin/e2e-status +8 -2
- package/bin/e2e-up +5 -5
- package/completions/install-zsh-completion.sh +78 -0
- package/config/dns/iamtheinternet.org.zone +1 -1
- package/config/routing/docker-image-preload.service +14 -0
- package/config/routing/docker-image-preload.sh +24 -0
- package/config/routing/management-routes.sh +5 -2
- package/docker/Dockerfile.management +2 -0
- package/docker/Dockerfile.target-machine +20 -0
- package/docker/Dockerfile.target-machine-docker +11 -19
- package/docker/Dockerfile.target-technitium +97 -0
- package/netapps/authentik.netapp +0 -0
- package/netapps/caddy.netapp +0 -0
- package/netapps/greenwave.netapp +0 -0
- package/netapps/homebridge.netapp +0 -0
- package/netapps/iptables.netapp +0 -0
- package/netapps/knot-unbound-internal.netapp +0 -0
- package/netapps/namecheap.netapp +0 -0
- package/netapps/technitium.netapp +0 -0
- package/netapps/test-ansible-output.netapp +0 -0
- package/package.json +7 -1
- package/simulators/greenwave/state.ts +7 -12
- package/src/cli/build.ts +204 -0
- package/src/cli/command-registry.ts +99 -0
- package/src/cli/command-tree-parser.ts +52 -0
- package/src/cli/completion.ts +171 -0
- package/src/cli/index.ts +203 -0
- package/src/cli/scaffold.ts +109 -0
- package/src/container-manager.ts +16 -6
- package/src/docker-compose-generator.ts +12 -10
- package/src/network-builder.ts +21 -0
- package/src/parse-line.ts +136 -0
- package/src/runner.ts +81 -88
- package/src/types.ts +4 -0
- package/bin/cache-docker-images +0 -46
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* cele2e — E2E test CLI for Celilo-deployed applications.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* cele2e run [pattern] [--keep] [--reuse] [--live] [--published]
|
|
7
|
+
* cele2e up [--caddy|--full-stack|--infrastructure|--custom '<json>']
|
|
8
|
+
* cele2e down [--keep] [--all]
|
|
9
|
+
* cele2e build-infra [module-dir...] [--save]
|
|
10
|
+
* cele2e clear-timing
|
|
11
|
+
* cele2e version
|
|
12
|
+
* cele2e shell [container]
|
|
13
|
+
* cele2e status
|
|
14
|
+
* cele2e load
|
|
15
|
+
* cele2e scaffold <test-name>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
20
|
+
import { join, resolve } from 'node:path';
|
|
21
|
+
import { runBuild } from './build';
|
|
22
|
+
import { runScaffold } from './scaffold';
|
|
23
|
+
import { generateBashCompletion, generateZshCompletion, getCompletions } from './completion';
|
|
24
|
+
|
|
25
|
+
const PKG_DIR = resolve(import.meta.dir, '../..');
|
|
26
|
+
const BIN_DIR = join(PKG_DIR, 'bin');
|
|
27
|
+
|
|
28
|
+
function detectTestDir(): string {
|
|
29
|
+
if (process.env.E2E_TEST_DIR) return process.env.E2E_TEST_DIR;
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
// Check e2e/tests/ first — more specific than a generic tests/ directory
|
|
32
|
+
if (existsSync(join(cwd, 'e2e', 'tests'))) return join(cwd, 'e2e');
|
|
33
|
+
if (existsSync(join(cwd, 'tests'))) return cwd;
|
|
34
|
+
return cwd;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runScript(script: string, args: string[], env?: Record<string, string>): never {
|
|
38
|
+
const result = spawnSync(join(BIN_DIR, script), args, {
|
|
39
|
+
stdio: 'inherit',
|
|
40
|
+
env: { ...process.env, ...env },
|
|
41
|
+
});
|
|
42
|
+
process.exit(result.status ?? 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printHelp(): void {
|
|
46
|
+
console.log(`
|
|
47
|
+
cele2e — E2E test CLI for Celilo-deployed applications
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
cele2e <command> [options]
|
|
51
|
+
|
|
52
|
+
Commands:
|
|
53
|
+
run [pattern] Run e2e tests (pattern filters by test file name)
|
|
54
|
+
up [preset] Start interactive test network
|
|
55
|
+
down Tear down running network
|
|
56
|
+
build-infra [dirs...] Rebuild Docker images and package all modules as .netapps
|
|
57
|
+
clear-timing Clear saved timing history used for test ETAs
|
|
58
|
+
version Show cele2e version
|
|
59
|
+
shell [container] Shell into a running container (default: management)
|
|
60
|
+
status Show network status and DNS health
|
|
61
|
+
load Load cached Docker images from tarball
|
|
62
|
+
scaffold <name> Generate a new test file from template
|
|
63
|
+
completion <shell> Generate shell completion script (zsh|bash)
|
|
64
|
+
|
|
65
|
+
Options for \`run\`:
|
|
66
|
+
--keep Keep network running after tests
|
|
67
|
+
--reuse Reuse existing network if running
|
|
68
|
+
--live Use live (non-simulated) internet
|
|
69
|
+
--published Use published .netapp packages
|
|
70
|
+
|
|
71
|
+
Options for \`up\`:
|
|
72
|
+
--caddy Start with caddy machine in DMZ
|
|
73
|
+
--full-stack Start with caddy, IDP, and DB machines
|
|
74
|
+
--custom <json> Custom machine spec JSON
|
|
75
|
+
|
|
76
|
+
Options for \`down\`:
|
|
77
|
+
--keep Stop containers but preserve volumes
|
|
78
|
+
--all Also stop competing module containers
|
|
79
|
+
|
|
80
|
+
Options for \`build-infra\`:
|
|
81
|
+
--save Save Docker images to tarball after build
|
|
82
|
+
--skip-modules Skip .netapp packaging (only rebuild Docker images)
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
cele2e run # run all tests
|
|
86
|
+
cele2e run deploy # run tests matching "deploy"
|
|
87
|
+
cele2e run --reuse deploy # reuse existing network
|
|
88
|
+
cele2e up --full-stack # interactive full-stack environment
|
|
89
|
+
cele2e shell caddy # debug running caddy container
|
|
90
|
+
cele2e build-infra # rebuild Docker images + repackage all modules
|
|
91
|
+
cele2e build-infra --skip-modules # rebuild Docker images only (faster)
|
|
92
|
+
cele2e scaffold lunacycle-auth # create tests/lunacycle-auth.test.ts
|
|
93
|
+
cele2e completion zsh # print zsh completion script
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [, , command, ...args] = process.argv;
|
|
98
|
+
const testDir = detectTestDir();
|
|
99
|
+
|
|
100
|
+
// Handle --get-completions before routing (used by shell completion scripts)
|
|
101
|
+
if (command === '--get-completions') {
|
|
102
|
+
const words = args.slice(0, -1);
|
|
103
|
+
const current = Number.parseInt(args[args.length - 1] || '0', 10);
|
|
104
|
+
const suggestions = getCompletions(words, current);
|
|
105
|
+
process.stdout.write(`${suggestions.join('\n')}\n`);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
switch (command) {
|
|
110
|
+
case 'run':
|
|
111
|
+
runScript('e2e-run', args, { E2E_TEST_DIR: testDir });
|
|
112
|
+
|
|
113
|
+
case 'up':
|
|
114
|
+
runScript('e2e-up', args);
|
|
115
|
+
|
|
116
|
+
case 'down':
|
|
117
|
+
runScript('e2e-down', args, { E2E_TEST_DIR: testDir });
|
|
118
|
+
|
|
119
|
+
case 'shell':
|
|
120
|
+
runScript('e2e-shell', args);
|
|
121
|
+
|
|
122
|
+
case 'status': {
|
|
123
|
+
const persistentFile = join(testDir, '.e2e-persistent.json');
|
|
124
|
+
let projectName: string | undefined;
|
|
125
|
+
if (existsSync(persistentFile)) {
|
|
126
|
+
try {
|
|
127
|
+
const data = JSON.parse(readFileSync(persistentFile, 'utf-8'));
|
|
128
|
+
projectName = data.projectName;
|
|
129
|
+
console.log('=== Persistent Test Network ===');
|
|
130
|
+
console.log(` Project: ${data.projectName}`);
|
|
131
|
+
console.log(` Test: ${data.testFile}`);
|
|
132
|
+
console.log(` Created: ${data.createdAt}`);
|
|
133
|
+
console.log(` Reuse: cele2e run --reuse`);
|
|
134
|
+
console.log(` Tear down: cele2e down`);
|
|
135
|
+
console.log('');
|
|
136
|
+
} catch { /* ignore parse errors */ }
|
|
137
|
+
}
|
|
138
|
+
runScript('e2e-status', args, projectName ? { CELILO_E2E_PROJECT: projectName } : undefined);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case 'load':
|
|
142
|
+
runScript('e2e-load', args);
|
|
143
|
+
|
|
144
|
+
case 'build-infra': {
|
|
145
|
+
const save = args.includes('--save');
|
|
146
|
+
const skipModules = args.includes('--skip-modules');
|
|
147
|
+
const moduleDirs = args.filter((a) => !a.startsWith('--'));
|
|
148
|
+
runBuild({ pkgDir: PKG_DIR, moduleDirs, save, skipModules });
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'clear-timing': {
|
|
153
|
+
const timingFile = join(testDir, '.e2e-timing.json');
|
|
154
|
+
if (existsSync(timingFile)) {
|
|
155
|
+
rmSync(timingFile);
|
|
156
|
+
console.log(`Cleared timing data: ${timingFile}`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(`No timing data found at ${timingFile}`);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'scaffold': {
|
|
164
|
+
const name = args[0];
|
|
165
|
+
if (!name) {
|
|
166
|
+
console.error('Error: scaffold requires a test name\n cele2e scaffold <name>');
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
runScaffold({ testDir, name });
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'version': {
|
|
174
|
+
const pkg = await import('../../package.json', { with: { type: 'json' } });
|
|
175
|
+
console.log(`cele2e v${pkg.default.version}`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
case 'completion': {
|
|
180
|
+
const shell = args[0];
|
|
181
|
+
if (shell === 'zsh') {
|
|
182
|
+
process.stdout.write(generateZshCompletion());
|
|
183
|
+
} else if (shell === 'bash') {
|
|
184
|
+
process.stdout.write(generateBashCompletion());
|
|
185
|
+
} else {
|
|
186
|
+
console.error('Usage: cele2e completion <zsh|bash>');
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case undefined:
|
|
193
|
+
case '--help':
|
|
194
|
+
case '-h':
|
|
195
|
+
case 'help':
|
|
196
|
+
printHelp();
|
|
197
|
+
break;
|
|
198
|
+
|
|
199
|
+
default:
|
|
200
|
+
console.error(`Unknown command: ${command}\n`);
|
|
201
|
+
printHelp();
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cele2e scaffold — generate a new e2e test file from template.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const green = '\x1b[32m';
|
|
9
|
+
const dim = '\x1b[2m';
|
|
10
|
+
const reset = '\x1b[0m';
|
|
11
|
+
|
|
12
|
+
interface ScaffoldOptions {
|
|
13
|
+
testDir: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toTitleCase(kebab: string): string {
|
|
18
|
+
return kebab
|
|
19
|
+
.split('-')
|
|
20
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
21
|
+
.join(' ');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function generateTestTemplate(name: string): string {
|
|
25
|
+
const title = toTitleCase(name);
|
|
26
|
+
return `/**
|
|
27
|
+
* ${title} E2E Test
|
|
28
|
+
*
|
|
29
|
+
* Tests that ${title} deploys and operates correctly through Celilo's infrastructure.
|
|
30
|
+
*
|
|
31
|
+
* Run: cele2e run ${name}
|
|
32
|
+
* Run (reuse): cele2e run --reuse ${name}
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { afterAll, describe, expect, test } from 'bun:test';
|
|
36
|
+
import { network, progress, reconnectNetwork } from '@celilo/e2e';
|
|
37
|
+
import type { NetworkHandle } from '@celilo/e2e/types';
|
|
38
|
+
|
|
39
|
+
const REUSE = process.argv.includes('--reuse');
|
|
40
|
+
const PROJECT_NAME = 'celilo-e2e-${name}';
|
|
41
|
+
|
|
42
|
+
describe('${name}', () => {
|
|
43
|
+
let net: NetworkHandle;
|
|
44
|
+
let stageError: string | null = null;
|
|
45
|
+
|
|
46
|
+
function requireStage(stage: string) {
|
|
47
|
+
if (stageError) {
|
|
48
|
+
throw new Error(\`Skipped: \${stage} — \${stageError}\`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await net?.stop();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('start network', async () => {
|
|
57
|
+
if (REUSE) {
|
|
58
|
+
net = await reconnectNetwork(PROJECT_NAME);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
net = await network({ projectName: PROJECT_NAME })
|
|
63
|
+
.dmz({ 'machine-1': '10.0.10.10' })
|
|
64
|
+
// .app({ 'app-1': '10.0.20.10' })
|
|
65
|
+
.start();
|
|
66
|
+
}, 120_000);
|
|
67
|
+
|
|
68
|
+
test('deploy', async () => {
|
|
69
|
+
requireStage('deploy');
|
|
70
|
+
try {
|
|
71
|
+
progress('importing module', 'module imported');
|
|
72
|
+
await net.celilo('module import /path/to/module.netapp');
|
|
73
|
+
progress.done();
|
|
74
|
+
|
|
75
|
+
progress('deploying', 'deployed');
|
|
76
|
+
await net.celilo('module deploy my-module');
|
|
77
|
+
progress.done();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
stageError = String(error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}, 300_000);
|
|
83
|
+
|
|
84
|
+
test('verify', async () => {
|
|
85
|
+
requireStage('verify');
|
|
86
|
+
// TODO: add verification steps
|
|
87
|
+
// const result = await net.exec('machine-1', 'curl -s http://localhost');
|
|
88
|
+
// expect(result.exitCode).toBe(0);
|
|
89
|
+
}, 60_000);
|
|
90
|
+
});
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function runScaffold(options: ScaffoldOptions): void {
|
|
95
|
+
const { testDir, name } = options;
|
|
96
|
+
|
|
97
|
+
const testsDir = join(testDir, 'tests');
|
|
98
|
+
mkdirSync(testsDir, { recursive: true });
|
|
99
|
+
|
|
100
|
+
const outPath = join(testsDir, `${name}.test.ts`);
|
|
101
|
+
if (existsSync(outPath)) {
|
|
102
|
+
console.error(`Error: ${outPath} already exists`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
writeFileSync(outPath, generateTestTemplate(name));
|
|
107
|
+
console.log(`${green}✔${reset} Created ${outPath}`);
|
|
108
|
+
console.log(`\n${dim}Run it with:\n cele2e run ${name}${reset}`);
|
|
109
|
+
}
|
package/src/container-manager.ts
CHANGED
|
@@ -14,11 +14,21 @@ const PACKAGE_ROOT = join(__dirname, '..');
|
|
|
14
14
|
* Looks for apps/celilo/ as the marker.
|
|
15
15
|
*/
|
|
16
16
|
/**
|
|
17
|
-
* Try to find the Celilo project root
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Try to find the Celilo project root.
|
|
18
|
+
*
|
|
19
|
+
* Resolution order:
|
|
20
|
+
* 1. CELILO_E2E_INFRA_ROOT env var — explicit path (dev) or empty string
|
|
21
|
+
* (force published mode). Empty string means "do not mount source".
|
|
22
|
+
* 2. Walk up from the package directory looking for apps/celilo/. Works
|
|
23
|
+
* automatically when @celilo/e2e is used directly inside the infra repo.
|
|
24
|
+
* 3. undefined — falls back to the baked-in npm package inside Docker.
|
|
20
25
|
*/
|
|
21
26
|
function findCeliloRoot(): string | undefined {
|
|
27
|
+
const envRoot = process.env.CELILO_E2E_INFRA_ROOT;
|
|
28
|
+
if (envRoot !== undefined) {
|
|
29
|
+
return envRoot === '' ? undefined : envRoot;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
let dir = PACKAGE_ROOT;
|
|
23
33
|
for (let i = 0; i < 10; i++) {
|
|
24
34
|
if (existsSync(join(dir, 'apps', 'celilo'))) {
|
|
@@ -108,11 +118,11 @@ function dockerExecAsync(
|
|
|
108
118
|
timeoutMs = 60_000,
|
|
109
119
|
): Promise<ExecResult> {
|
|
110
120
|
return new Promise((resolve) => {
|
|
111
|
-
// stdbuf -oL forces line-buffered stdout
|
|
112
|
-
// rather than
|
|
121
|
+
// stdbuf -oL -eL forces line-buffered stdout and stderr so Ansible play/task
|
|
122
|
+
// markers stream in real time rather than arriving in one burst at the end.
|
|
113
123
|
const args = [
|
|
114
124
|
'compose', '-f', COMPOSE_FILE, '-p', projectName,
|
|
115
|
-
'exec', '-T', container, 'stdbuf', '-oL', 'bash', '-c', cmd,
|
|
125
|
+
'exec', '-T', container, 'stdbuf', '-oL', '-eL', 'bash', '-c', cmd,
|
|
116
126
|
];
|
|
117
127
|
const child = spawn('docker', args, { cwd: composeDir });
|
|
118
128
|
|
|
@@ -306,13 +306,15 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
306
306
|
const zoneNetwork = machine.zone;
|
|
307
307
|
const gateway = ZONE_GATEWAYS[machine.zone];
|
|
308
308
|
|
|
309
|
-
const
|
|
310
|
-
const dockerfile =
|
|
311
|
-
? 'docker/Dockerfile.target-machine-docker'
|
|
312
|
-
: 'docker/Dockerfile.target-machine';
|
|
309
|
+
const needsDocker = machine.zone === 'app' || machine.docker === true;
|
|
310
|
+
const dockerfile = machine.dockerfile
|
|
311
|
+
|| (needsDocker ? 'docker/Dockerfile.target-machine-docker' : 'docker/Dockerfile.target-machine');
|
|
313
312
|
|
|
314
313
|
const machineVolumes = ['ssh-keys:/ssh-keys:ro'];
|
|
315
|
-
|
|
314
|
+
|
|
315
|
+
// Mount the host image cache into Docker-capable machines so the
|
|
316
|
+
// docker-image-preload service can load them at startup without pulling.
|
|
317
|
+
if (needsDocker) {
|
|
316
318
|
machineVolumes.push('./docker-image-cache:/docker-image-cache:ro');
|
|
317
319
|
}
|
|
318
320
|
|
|
@@ -469,13 +471,13 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
469
471
|
const zoneNetwork = machine.zone;
|
|
470
472
|
const gateway = ZONE_GATEWAYS[machine.zone];
|
|
471
473
|
|
|
472
|
-
const
|
|
473
|
-
const dockerfile =
|
|
474
|
-
? 'docker/Dockerfile.target-machine-docker'
|
|
475
|
-
: 'docker/Dockerfile.target-machine';
|
|
474
|
+
const needsDocker = machine.zone === 'app' || machine.docker === true;
|
|
475
|
+
const dockerfile = machine.dockerfile
|
|
476
|
+
|| (needsDocker ? 'docker/Dockerfile.target-machine-docker' : 'docker/Dockerfile.target-machine');
|
|
476
477
|
|
|
477
478
|
const machineVolumes = ['ssh-keys:/ssh-keys:ro'];
|
|
478
|
-
|
|
479
|
+
|
|
480
|
+
if (needsDocker) {
|
|
479
481
|
machineVolumes.push('./docker-image-cache:/docker-image-cache:ro');
|
|
480
482
|
}
|
|
481
483
|
|
package/src/network-builder.ts
CHANGED
|
@@ -78,6 +78,27 @@ export class NetworkBuilder {
|
|
|
78
78
|
return this;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Add internal-zone machines that need the Docker-capable image (e.g., Technitium) */
|
|
82
|
+
internalDocker(machines: Record<string, string>): this {
|
|
83
|
+
for (const [name, ip] of Object.entries(machines)) {
|
|
84
|
+
this.config.internalMachines.push({ name, ip, zone: 'internal', docker: true });
|
|
85
|
+
}
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Add a Technitium DNS machine using the dedicated pre-installed image */
|
|
90
|
+
technitium(machines: Record<string, string>): this {
|
|
91
|
+
for (const [name, ip] of Object.entries(machines)) {
|
|
92
|
+
this.config.internalMachines.push({
|
|
93
|
+
name,
|
|
94
|
+
ip,
|
|
95
|
+
zone: 'internal',
|
|
96
|
+
dockerfile: 'docker/Dockerfile.target-technitium',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
81
102
|
withDhcpClient(): this {
|
|
82
103
|
this.config.dhcpClient = true;
|
|
83
104
|
return this;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line parser for the e2e test runner.
|
|
3
|
+
*
|
|
4
|
+
* Translates celilo CLI output lines into progress display calls.
|
|
5
|
+
* Extracted as a separate module so it can be unit-tested without
|
|
6
|
+
* spinning up Docker or a full ProgressDisplay.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const dim = '\x1b[2m';
|
|
10
|
+
const green = '\x1b[32m';
|
|
11
|
+
const red = '\x1b[31m';
|
|
12
|
+
const reset = '\x1b[0m';
|
|
13
|
+
|
|
14
|
+
export interface DisplaySink {
|
|
15
|
+
startStep(doing: string, done: string): void;
|
|
16
|
+
doneStep(override?: string): void;
|
|
17
|
+
failStep(message: string): void;
|
|
18
|
+
instantEvent(message: string): void;
|
|
19
|
+
subEvent(message: string): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseLine(line: string, display: DisplaySink): void {
|
|
23
|
+
// Protocol: [progress:start] doing | done
|
|
24
|
+
const startMatch = line.match(/\[progress:start\] (.*?) \| (.*)/);
|
|
25
|
+
if (startMatch) {
|
|
26
|
+
display.startStep(startMatch[1], startMatch[2]);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Protocol: [progress:done] optional message
|
|
31
|
+
const doneMatch = line.match(/\[progress:done\](.*)/);
|
|
32
|
+
if (doneMatch) {
|
|
33
|
+
const msg = doneMatch[1].trim();
|
|
34
|
+
display.doneStep(msg || undefined);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Protocol: [progress:fail] message
|
|
39
|
+
const failMatch = line.match(/\[progress:fail\] (.*)/);
|
|
40
|
+
if (failMatch) {
|
|
41
|
+
display.failStep(failMatch[1]);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Legacy: [progress] message (treat as instant event)
|
|
46
|
+
const legacyMatch = line.match(/\[progress\] (.*)/);
|
|
47
|
+
if (legacyMatch) {
|
|
48
|
+
display.instantEvent(legacyMatch[1]);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const deployMatch = line.match(/Module '([^']+)' deployed successfully/);
|
|
53
|
+
if (deployMatch) {
|
|
54
|
+
display.subEvent(`${green}✔${reset} ${deployMatch[1]} deployed`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const importMatch = line.match(/Successfully imported module: (\S+)/);
|
|
59
|
+
if (importMatch) {
|
|
60
|
+
display.subEvent(`${green}✔${reset} ${importMatch[1]} imported`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (line.includes('Deploying software')) {
|
|
65
|
+
display.subEvent('running Ansible');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (line.includes('Running on_install hook') || line.includes('on_install hook')) {
|
|
69
|
+
display.subEvent('running on_install hook');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (line.includes('Software deployed') || line.includes('Software deployed successfully')) {
|
|
73
|
+
display.subEvent(`${green}✔${reset} Ansible done`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (line.includes('Infrastructure selected')) {
|
|
77
|
+
display.subEvent('infrastructure selected');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (line.includes('Terraform skipped')) {
|
|
81
|
+
display.subEvent('terraform skipped (existing machine)');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (line.includes('terraform init') || line.includes('Terraform init')) {
|
|
85
|
+
display.subEvent('running terraform init');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (line.includes('terraform apply') || line.includes('Terraform apply')) {
|
|
89
|
+
display.subEvent('running terraform apply');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const retryMatch = line.match(/RETRYING.*?(\d+) retries left/);
|
|
94
|
+
if (retryMatch) {
|
|
95
|
+
display.subEvent(`health check (${retryMatch[1]} retries left)`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Ansible progress markers emitted via process.stderr in deploy-ansible.ts
|
|
100
|
+
if (line.startsWith('[ansible:play] ')) {
|
|
101
|
+
display.subEvent(`${dim}▶${reset} ${line.slice(15)}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (line.startsWith('[ansible:task] ')) {
|
|
105
|
+
display.subEvent(` • ${line.slice(15)}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (line.startsWith('[ansible:handler] ')) {
|
|
109
|
+
display.subEvent(` ↺ ${line.slice(18)}`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (line.startsWith('[ansible:recap] ')) {
|
|
113
|
+
const recap = line.slice(16);
|
|
114
|
+
const failed = recap.match(/failed=(\d+)/)?.[1];
|
|
115
|
+
const icon = failed && failed !== '0' ? `${red}✗${reset}` : `${green}✔${reset}`;
|
|
116
|
+
display.subEvent(`${icon} ${recap}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Hook log output: "[moduleId:hookName] message"
|
|
121
|
+
const hookLogMatch = line.match(/^\[[\w-]+:[\w_-]+\] (.*)/);
|
|
122
|
+
if (hookLogMatch) {
|
|
123
|
+
display.subEvent(`${dim}hook:${reset} ${hookLogMatch[1]}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Error lines
|
|
128
|
+
if (
|
|
129
|
+
line.includes('Error:') &&
|
|
130
|
+
!line.includes('bun test') &&
|
|
131
|
+
!line.includes('ipv4: Address already')
|
|
132
|
+
) {
|
|
133
|
+
const err = line.replace(/.*Error: /, '').slice(0, 70);
|
|
134
|
+
display.subEvent(`${red}✗${reset} ${err}`);
|
|
135
|
+
}
|
|
136
|
+
}
|