@celilo/e2e 0.5.1 → 0.6.1
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/README.md +2 -1
- 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/celilo.computer.zone +16 -0
- package/config/dns/iamtheinternet.org.zone +7 -6
- package/config/dns/knot-namecheap.conf +8 -0
- package/config/dns/knot-tld.conf +4 -0
- package/config/dns/lunacycle.net.zone +16 -0
- package/config/dns/net.zone +11 -0
- package/config/dns/org.zone +5 -0
- package/config/dns/root.zone +2 -0
- package/config/dns/templates/iamtheinternet.org.zone +16 -0
- package/config/dns/templates/lunacycle.net.zone +16 -0
- package/config/resolver/unbound.conf +10 -0
- package/config/routing/docker-image-preload.service +14 -0
- package/config/routing/docker-image-preload.sh +24 -0
- package/config/routing/management-routes.sh +24 -2
- package/config/routing/resolver-internal-routes.sh +59 -0
- package/docker/Dockerfile.internal-resolver +16 -0
- package/docker/Dockerfile.isitup +37 -0
- package/docker/Dockerfile.management +2 -0
- package/docker/Dockerfile.registry +34 -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 +8 -1
- package/simulators/greenwave/state.ts +7 -12
- package/simulators/isitup/Caddyfile +11 -0
- package/simulators/isitup/entrypoint.sh +47 -0
- package/simulators/isitup/server.ts +112 -0
- package/simulators/namecheap-ddns/server.ts +31 -2
- package/src/cli/build.ts +214 -0
- package/src/cli/command-registry.ts +100 -0
- package/src/cli/command-tree-parser.ts +52 -0
- package/src/cli/completion.ts +195 -0
- package/src/cli/index.ts +273 -0
- package/src/cli/module-discovery.ts +120 -0
- package/src/cli/scaffold.ts +108 -0
- package/src/container-manager.ts +373 -76
- package/src/docker-compose-generator.ts +127 -23
- package/src/network-builder.ts +21 -0
- package/src/parse-line.ts +147 -0
- package/src/runner.ts +197 -247
- package/src/simulator-ips.ts +42 -0
- package/src/types.ts +28 -0
- package/bin/cache-docker-images +0 -46
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stringify } from 'yaml';
|
|
2
|
+
import { SIMULATOR_IPS } from './simulator-ips';
|
|
2
3
|
import type { MachineSpec, NetworkConfig, TopologyPreset } from './types';
|
|
3
4
|
import { ZONE_GATEWAYS } from './types';
|
|
4
5
|
|
|
@@ -46,6 +47,7 @@ function baseService(opts: {
|
|
|
46
47
|
environment?: Record<string, string>;
|
|
47
48
|
tmpfs?: string[];
|
|
48
49
|
privileged?: boolean;
|
|
50
|
+
dns?: string[];
|
|
49
51
|
}) {
|
|
50
52
|
// Auto-assign an image tag from the Dockerfile name so Docker can
|
|
51
53
|
// skip builds when pre-built images exist.
|
|
@@ -162,7 +164,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
162
164
|
|
|
163
165
|
services['root-dns'] = baseService({
|
|
164
166
|
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
165
|
-
networks: { 'internet-external': { ipv4_address:
|
|
167
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.ROOT_DNS } },
|
|
166
168
|
cap_add: ['NET_ADMIN'],
|
|
167
169
|
volumes: [
|
|
168
170
|
'./config/dns/knot-root.conf:/config/knot.conf:ro',
|
|
@@ -172,22 +174,31 @@ export function generateSharedInfraYaml(): string {
|
|
|
172
174
|
|
|
173
175
|
services['tld-dns'] = baseService({
|
|
174
176
|
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
175
|
-
networks: { 'internet-external': { ipv4_address:
|
|
177
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.TLD_DNS } },
|
|
176
178
|
cap_add: ['NET_ADMIN'],
|
|
177
179
|
volumes: [
|
|
178
180
|
'./config/dns/knot-tld.conf:/config/knot.conf:ro',
|
|
179
181
|
'./config/dns/com.zone:/config/com.zone:ro',
|
|
180
182
|
'./config/dns/org.zone:/config/org.zone:ro',
|
|
183
|
+
'./config/dns/net.zone:/config/net.zone:ro',
|
|
181
184
|
],
|
|
182
185
|
});
|
|
183
186
|
|
|
184
187
|
services['namecheap-dns'] = baseService({
|
|
185
188
|
build: { context: '.', dockerfile: 'docker/Dockerfile.namecheap-dns' },
|
|
186
|
-
networks: { 'internet-external': { ipv4_address:
|
|
189
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.NAMECHEAP_DNS } },
|
|
187
190
|
cap_add: ['NET_ADMIN'],
|
|
191
|
+
// Per-domain DDNS passwords for the cross-domain test. Fallback
|
|
192
|
+
// `test123` still applies to any domain not listed.
|
|
193
|
+
environment: {
|
|
194
|
+
DDNS_PASSWORDS:
|
|
195
|
+
'{"iamtheinternet.org":"test123","celilo.computer":"test456","lunacycle.net":"test789"}',
|
|
196
|
+
},
|
|
188
197
|
volumes: [
|
|
189
198
|
'./config/dns/knot-namecheap.conf:/config/knot.conf:ro',
|
|
190
199
|
'./config/dns/iamtheinternet.org.zone:/config/iamtheinternet.org.zone',
|
|
200
|
+
'./config/dns/celilo.computer.zone:/config/celilo.computer.zone',
|
|
201
|
+
'./config/dns/lunacycle.net.zone:/config/lunacycle.net.zone',
|
|
191
202
|
'./config/dns/park-your-domain.com.zone:/config/park-your-domain.com.zone',
|
|
192
203
|
],
|
|
193
204
|
});
|
|
@@ -204,7 +215,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
204
215
|
|
|
205
216
|
services.letsencrypt = baseService({
|
|
206
217
|
build: { context: '.', dockerfile: 'docker/Dockerfile.pebble' },
|
|
207
|
-
networks: { 'internet-external': { ipv4_address:
|
|
218
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.PEBBLE } },
|
|
208
219
|
cap_add: ['NET_ADMIN'],
|
|
209
220
|
command: '-config /config/pebble-config.json -dnsserver 100.100.0.1:53',
|
|
210
221
|
volumes: [
|
|
@@ -214,6 +225,28 @@ export function generateSharedInfraYaml(): string {
|
|
|
214
225
|
],
|
|
215
226
|
});
|
|
216
227
|
|
|
228
|
+
// Registry stand-in. Context widened to `..` (the `packages/` dir) so the
|
|
229
|
+
// Dockerfile can COPY from packages/registry-server (single source of truth
|
|
230
|
+
// shared with the celilo-registry module).
|
|
231
|
+
services.registry = baseService({
|
|
232
|
+
build: { context: '..', dockerfile: 'e2e/docker/Dockerfile.registry' },
|
|
233
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.REGISTRY } },
|
|
234
|
+
cap_add: ['NET_ADMIN'],
|
|
235
|
+
volumes: ['../../modules:/modules:ro'],
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// External website prober simulator — mimics isitup.org's /api.json endpoint.
|
|
239
|
+
// Sits on internet-external so probe requests exercise the real public-facing
|
|
240
|
+
// path (target domain → DNS → ISP → firewall → Caddy). Uses comcast-resolver
|
|
241
|
+
// at 100.100.0.1 as DNS so it can recursively resolve the simulated public
|
|
242
|
+
// zones (e.g. iamtheinternet.org).
|
|
243
|
+
services.isitup = baseService({
|
|
244
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.isitup' },
|
|
245
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.ISITUP } },
|
|
246
|
+
cap_add: ['NET_ADMIN'],
|
|
247
|
+
dns: ['100.100.0.1'],
|
|
248
|
+
});
|
|
249
|
+
|
|
217
250
|
const compose = { networks, volumes, services };
|
|
218
251
|
return stringify(compose, { lineWidth: 120 });
|
|
219
252
|
}
|
|
@@ -252,7 +285,8 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
252
285
|
const services: Record<string, unknown> = {};
|
|
253
286
|
|
|
254
287
|
// --- Management (always present) ---
|
|
255
|
-
const managementDefaultGw =
|
|
288
|
+
const managementDefaultGw =
|
|
289
|
+
config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
|
|
256
290
|
|
|
257
291
|
services.management = baseService({
|
|
258
292
|
build: { context: '.', dockerfile: 'docker/Dockerfile.management' },
|
|
@@ -264,7 +298,28 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
264
298
|
...(config.managementVolumes ?? []),
|
|
265
299
|
],
|
|
266
300
|
environment: {
|
|
267
|
-
DDNS_ENDPOINT:
|
|
301
|
+
DDNS_ENDPOINT: `http://${SIMULATOR_IPS.NAMECHEAP_DNS}:8080/update`,
|
|
302
|
+
DEFAULT_GATEWAY: managementDefaultGw,
|
|
303
|
+
CELILO_REGISTRY_URL: 'http://e2e-registry.lab',
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// --- Internal split-horizon DNS resolver ---
|
|
308
|
+
// Resolves the test domain to caddy's internal DMZ IP so management can
|
|
309
|
+
// reach it via a direct route rather than through the external interface
|
|
310
|
+
// (hairpin NAT would be needed otherwise). Management already has
|
|
311
|
+
// nameserver 192.168.0.10 as its primary resolver in /etc/resolv.conf.
|
|
312
|
+
const caddyIp =
|
|
313
|
+
config.dmzMachines.find((m) => m.name === 'caddy')?.ip ??
|
|
314
|
+
config.dmzMachines[0]?.ip ??
|
|
315
|
+
'10.0.10.10';
|
|
316
|
+
services['dns-int'] = baseService({
|
|
317
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.internal-resolver' },
|
|
318
|
+
networks: { internal: { ipv4_address: '192.168.0.10' } },
|
|
319
|
+
cap_add: ['NET_ADMIN'],
|
|
320
|
+
environment: {
|
|
321
|
+
CADDY_IP: caddyIp,
|
|
322
|
+
DOMAIN: config.domain ?? 'iamtheinternet.org',
|
|
268
323
|
DEFAULT_GATEWAY: managementDefaultGw,
|
|
269
324
|
},
|
|
270
325
|
});
|
|
@@ -306,13 +361,18 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
306
361
|
const zoneNetwork = machine.zone;
|
|
307
362
|
const gateway = ZONE_GATEWAYS[machine.zone];
|
|
308
363
|
|
|
309
|
-
const
|
|
310
|
-
const dockerfile =
|
|
311
|
-
|
|
312
|
-
|
|
364
|
+
const needsDocker = machine.zone === 'app' || machine.docker === true;
|
|
365
|
+
const dockerfile =
|
|
366
|
+
machine.dockerfile ||
|
|
367
|
+
(needsDocker
|
|
368
|
+
? 'docker/Dockerfile.target-machine-docker'
|
|
369
|
+
: 'docker/Dockerfile.target-machine');
|
|
313
370
|
|
|
314
371
|
const machineVolumes = ['ssh-keys:/ssh-keys:ro'];
|
|
315
|
-
|
|
372
|
+
|
|
373
|
+
// Mount the host image cache into Docker-capable machines so the
|
|
374
|
+
// docker-image-preload service can load them at startup without pulling.
|
|
375
|
+
if (needsDocker) {
|
|
316
376
|
machineVolumes.push('./docker-image-cache:/docker-image-cache:ro');
|
|
317
377
|
}
|
|
318
378
|
|
|
@@ -364,7 +424,8 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
364
424
|
|
|
365
425
|
const services: Record<string, unknown> = {};
|
|
366
426
|
|
|
367
|
-
const managementDefaultGw =
|
|
427
|
+
const managementDefaultGw =
|
|
428
|
+
config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
|
|
368
429
|
|
|
369
430
|
services.management = baseService({
|
|
370
431
|
build: { context: '.', dockerfile: 'docker/Dockerfile.management' },
|
|
@@ -376,14 +437,30 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
376
437
|
...(config.managementVolumes ?? []),
|
|
377
438
|
],
|
|
378
439
|
environment: {
|
|
379
|
-
DDNS_ENDPOINT:
|
|
440
|
+
DDNS_ENDPOINT: `http://${SIMULATOR_IPS.NAMECHEAP_DNS}:8080/update`,
|
|
380
441
|
DEFAULT_GATEWAY: managementDefaultGw,
|
|
442
|
+
CELILO_REGISTRY_URL: 'http://e2e-registry.lab',
|
|
381
443
|
},
|
|
382
444
|
});
|
|
383
445
|
|
|
384
446
|
const topologyFn = TOPOLOGY_SERVICES[config.topology] || TOPOLOGY_SERVICES.default;
|
|
385
447
|
Object.assign(services, topologyFn());
|
|
386
448
|
|
|
449
|
+
const legacyCaddyIp =
|
|
450
|
+
config.dmzMachines.find((m) => m.name === 'caddy')?.ip ??
|
|
451
|
+
config.dmzMachines[0]?.ip ??
|
|
452
|
+
'10.0.10.10';
|
|
453
|
+
services['dns-int'] = baseService({
|
|
454
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.internal-resolver' },
|
|
455
|
+
networks: { internal: { ipv4_address: '192.168.0.10' } },
|
|
456
|
+
cap_add: ['NET_ADMIN'],
|
|
457
|
+
environment: {
|
|
458
|
+
CADDY_IP: legacyCaddyIp,
|
|
459
|
+
DOMAIN: config.domain ?? 'iamtheinternet.org',
|
|
460
|
+
DEFAULT_GATEWAY: managementDefaultGw,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
387
464
|
services['fw-ext'] = baseService({
|
|
388
465
|
build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
|
|
389
466
|
networks: {
|
|
@@ -406,7 +483,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
406
483
|
|
|
407
484
|
services['root-dns'] = baseService({
|
|
408
485
|
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
409
|
-
networks: { 'internet-external': { ipv4_address:
|
|
486
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.ROOT_DNS } },
|
|
410
487
|
cap_add: ['NET_ADMIN'],
|
|
411
488
|
volumes: [
|
|
412
489
|
'./config/dns/knot-root.conf:/config/knot.conf:ro',
|
|
@@ -416,22 +493,31 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
416
493
|
|
|
417
494
|
services['tld-dns'] = baseService({
|
|
418
495
|
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
419
|
-
networks: { 'internet-external': { ipv4_address:
|
|
496
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.TLD_DNS } },
|
|
420
497
|
cap_add: ['NET_ADMIN'],
|
|
421
498
|
volumes: [
|
|
422
499
|
'./config/dns/knot-tld.conf:/config/knot.conf:ro',
|
|
423
500
|
'./config/dns/com.zone:/config/com.zone:ro',
|
|
424
501
|
'./config/dns/org.zone:/config/org.zone:ro',
|
|
502
|
+
'./config/dns/net.zone:/config/net.zone:ro',
|
|
425
503
|
],
|
|
426
504
|
});
|
|
427
505
|
|
|
428
506
|
services['namecheap-dns'] = baseService({
|
|
429
507
|
build: { context: '.', dockerfile: 'docker/Dockerfile.namecheap-dns' },
|
|
430
|
-
networks: { 'internet-external': { ipv4_address:
|
|
508
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.NAMECHEAP_DNS } },
|
|
431
509
|
cap_add: ['NET_ADMIN'],
|
|
510
|
+
// Per-domain DDNS passwords for the cross-domain test. Fallback
|
|
511
|
+
// `test123` still applies to any domain not listed.
|
|
512
|
+
environment: {
|
|
513
|
+
DDNS_PASSWORDS:
|
|
514
|
+
'{"iamtheinternet.org":"test123","celilo.computer":"test456","lunacycle.net":"test789"}',
|
|
515
|
+
},
|
|
432
516
|
volumes: [
|
|
433
517
|
'./config/dns/knot-namecheap.conf:/config/knot.conf:ro',
|
|
434
518
|
'./config/dns/iamtheinternet.org.zone:/config/iamtheinternet.org.zone',
|
|
519
|
+
'./config/dns/celilo.computer.zone:/config/celilo.computer.zone',
|
|
520
|
+
'./config/dns/lunacycle.net.zone:/config/lunacycle.net.zone',
|
|
435
521
|
'./config/dns/park-your-domain.com.zone:/config/park-your-domain.com.zone',
|
|
436
522
|
],
|
|
437
523
|
});
|
|
@@ -448,7 +534,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
448
534
|
|
|
449
535
|
services.letsencrypt = baseService({
|
|
450
536
|
build: { context: '.', dockerfile: 'docker/Dockerfile.pebble' },
|
|
451
|
-
networks: { 'internet-external': { ipv4_address:
|
|
537
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.PEBBLE } },
|
|
452
538
|
cap_add: ['NET_ADMIN'],
|
|
453
539
|
command: '-config /config/pebble-config.json -dnsserver 100.100.0.1:53',
|
|
454
540
|
volumes: [
|
|
@@ -458,6 +544,16 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
458
544
|
],
|
|
459
545
|
});
|
|
460
546
|
|
|
547
|
+
// Registry stand-in. Context widened to `..` (the `packages/` dir) so the
|
|
548
|
+
// Dockerfile can COPY from packages/registry-server (single source of truth
|
|
549
|
+
// shared with the celilo-registry module).
|
|
550
|
+
services.registry = baseService({
|
|
551
|
+
build: { context: '..', dockerfile: 'e2e/docker/Dockerfile.registry' },
|
|
552
|
+
networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.REGISTRY } },
|
|
553
|
+
cap_add: ['NET_ADMIN'],
|
|
554
|
+
volumes: ['../../modules:/modules:ro'],
|
|
555
|
+
});
|
|
556
|
+
|
|
461
557
|
const allMachines = [
|
|
462
558
|
...config.dmzMachines,
|
|
463
559
|
...config.appMachines,
|
|
@@ -469,13 +565,16 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
469
565
|
const zoneNetwork = machine.zone;
|
|
470
566
|
const gateway = ZONE_GATEWAYS[machine.zone];
|
|
471
567
|
|
|
472
|
-
const
|
|
473
|
-
const dockerfile =
|
|
474
|
-
|
|
475
|
-
|
|
568
|
+
const needsDocker = machine.zone === 'app' || machine.docker === true;
|
|
569
|
+
const dockerfile =
|
|
570
|
+
machine.dockerfile ||
|
|
571
|
+
(needsDocker
|
|
572
|
+
? 'docker/Dockerfile.target-machine-docker'
|
|
573
|
+
: 'docker/Dockerfile.target-machine');
|
|
476
574
|
|
|
477
575
|
const machineVolumes = ['ssh-keys:/ssh-keys:ro'];
|
|
478
|
-
|
|
576
|
+
|
|
577
|
+
if (needsDocker) {
|
|
479
578
|
machineVolumes.push('./docker-image-cache:/docker-image-cache:ro');
|
|
480
579
|
}
|
|
481
580
|
|
|
@@ -502,5 +601,10 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
502
601
|
}
|
|
503
602
|
|
|
504
603
|
export function getAllMachines(config: NetworkConfig): MachineSpec[] {
|
|
505
|
-
return [
|
|
604
|
+
return [
|
|
605
|
+
...config.dmzMachines,
|
|
606
|
+
...config.appMachines,
|
|
607
|
+
...config.secureMachines,
|
|
608
|
+
...config.internalMachines,
|
|
609
|
+
];
|
|
506
610
|
}
|
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,147 @@
|
|
|
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
|
+
// Protocol: [progress:sub] message (sub-event under the current step)
|
|
46
|
+
const subMatch = line.match(/\[progress:sub\] (.*)/);
|
|
47
|
+
if (subMatch) {
|
|
48
|
+
display.subEvent(subMatch[1]);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Legacy: [progress] message (treat as instant event)
|
|
53
|
+
const legacyMatch = line.match(/\[progress\] (.*)/);
|
|
54
|
+
if (legacyMatch) {
|
|
55
|
+
display.instantEvent(legacyMatch[1]);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const deployMatch = line.match(/Module '([^']+)' deployed successfully/);
|
|
60
|
+
if (deployMatch) {
|
|
61
|
+
display.subEvent(`${green}✔${reset} ${deployMatch[1]} deployed`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const importMatch = line.match(/Successfully imported module: (\S+)/);
|
|
66
|
+
if (importMatch) {
|
|
67
|
+
display.subEvent(`${green}✔${reset} ${importMatch[1]} imported`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (line.includes('Deploying software')) {
|
|
72
|
+
display.subEvent('running Ansible');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (line.includes('Running on_install hook') || line.includes('on_install hook')) {
|
|
76
|
+
display.subEvent('running on_install hook');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (line.includes('Software deployed') || line.includes('Software deployed successfully')) {
|
|
80
|
+
display.subEvent(`${green}✔${reset} Ansible done`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (line.includes('Infrastructure selected')) {
|
|
84
|
+
display.subEvent('infrastructure selected');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (line.includes('Terraform skipped')) {
|
|
88
|
+
display.subEvent('terraform skipped (existing machine)');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (line.includes('terraform init') || line.includes('Terraform init')) {
|
|
92
|
+
display.subEvent('running terraform init');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (line.includes('terraform apply') || line.includes('Terraform apply')) {
|
|
96
|
+
display.subEvent('running terraform apply');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const retryMatch = line.match(/RETRYING.*?(\d+) retries left/);
|
|
101
|
+
if (retryMatch) {
|
|
102
|
+
display.subEvent(`health check (${retryMatch[1]} retries left)`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Ansible progress markers emitted via process.stderr in deploy-ansible.ts
|
|
107
|
+
if (line.startsWith('[ansible:play] ')) {
|
|
108
|
+
display.subEvent(`${dim}▶${reset} ${line.slice(15)}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (line.startsWith('[ansible:task] ')) {
|
|
112
|
+
display.subEvent(` • ${line.slice(15)}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (line.startsWith('[ansible:handler] ')) {
|
|
116
|
+
display.subEvent(` ↺ ${line.slice(18)}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (line.startsWith('[ansible:recap] ')) {
|
|
120
|
+
const recap = line.slice(16);
|
|
121
|
+
const failed = recap.match(/failed=(\d+)/)?.[1];
|
|
122
|
+
const icon = failed && failed !== '0' ? `${red}✗${reset}` : `${green}✔${reset}`;
|
|
123
|
+
display.subEvent(`${icon} ${recap}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Hook log output: "[moduleId:hookName] message"
|
|
128
|
+
const hookLogMatch = line.match(/^\[[\w-]+:[\w_-]+\] (.*)/);
|
|
129
|
+
if (hookLogMatch) {
|
|
130
|
+
display.subEvent(`${dim}hook:${reset} ${hookLogMatch[1]}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Error lines. Require the identifier ending in "Error:" to be either
|
|
135
|
+
// standalone ("Error:") or PascalCase ("CeliloCommandError:") — NOT a
|
|
136
|
+
// camelCase variable like "stageError:" that bun prints as source context
|
|
137
|
+
// for failing tests.
|
|
138
|
+
const errorMatch = line.match(/\b(?:[A-Z]\w*)?Error: (.*)/);
|
|
139
|
+
if (
|
|
140
|
+
errorMatch &&
|
|
141
|
+
!line.includes('bun test') &&
|
|
142
|
+
!line.includes('ipv4: Address already')
|
|
143
|
+
) {
|
|
144
|
+
const err = errorMatch[1].slice(0, 70);
|
|
145
|
+
display.subEvent(`${red}✗${reset} ${err}`);
|
|
146
|
+
}
|
|
147
|
+
}
|