@celilo/e2e 0.6.0 → 0.6.2

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.
Files changed (43) hide show
  1. package/README.md +2 -1
  2. package/config/dns/celilo.computer.zone +16 -0
  3. package/config/dns/iamtheinternet.org.zone +6 -4
  4. package/config/dns/knot-namecheap.conf +8 -0
  5. package/config/dns/knot-tld.conf +4 -0
  6. package/config/dns/lunacycle.net.zone +16 -0
  7. package/config/dns/net.zone +11 -0
  8. package/config/dns/org.zone +5 -0
  9. package/config/dns/root.zone +2 -0
  10. package/config/dns/templates/iamtheinternet.org.zone +16 -0
  11. package/config/dns/templates/lunacycle.net.zone +16 -0
  12. package/config/resolver/unbound.conf +10 -0
  13. package/config/routing/management-routes.sh +19 -0
  14. package/config/routing/resolver-internal-routes.sh +59 -0
  15. package/docker/Dockerfile.internal-resolver +16 -0
  16. package/docker/Dockerfile.isitup +37 -0
  17. package/docker/Dockerfile.registry +34 -0
  18. package/netapps/authentik.netapp +0 -0
  19. package/netapps/caddy.netapp +0 -0
  20. package/netapps/greenwave.netapp +0 -0
  21. package/netapps/homebridge.netapp +0 -0
  22. package/netapps/iptables.netapp +0 -0
  23. package/netapps/knot-unbound-internal.netapp +0 -0
  24. package/netapps/namecheap.netapp +0 -0
  25. package/netapps/technitium.netapp +0 -0
  26. package/netapps/test-ansible-output.netapp +0 -0
  27. package/package.json +2 -1
  28. package/simulators/isitup/Caddyfile +11 -0
  29. package/simulators/isitup/entrypoint.sh +47 -0
  30. package/simulators/isitup/server.ts +112 -0
  31. package/simulators/namecheap-ddns/server.ts +31 -2
  32. package/src/cli/build.ts +17 -3
  33. package/src/cli/command-registry.ts +3 -2
  34. package/src/cli/completion.ts +29 -5
  35. package/src/cli/index.ts +85 -15
  36. package/src/cli/module-discovery.ts +120 -0
  37. package/src/cli/scaffold.ts +4 -5
  38. package/src/container-manager.ts +358 -71
  39. package/src/docker-compose-generator.ts +119 -17
  40. package/src/parse-line.ts +14 -3
  41. package/src/runner.ts +122 -165
  42. package/src/simulator-ips.ts +42 -0
  43. package/src/types.ts +24 -0
@@ -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: '100.64.0.53' } },
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: '100.64.0.54' } },
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: '100.64.0.55' } },
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: '100.64.0.100' } },
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 = config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
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: 'http://100.64.0.55:8080/update',
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
  });
@@ -307,8 +362,11 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
307
362
  const gateway = ZONE_GATEWAYS[machine.zone];
308
363
 
309
364
  const needsDocker = machine.zone === 'app' || machine.docker === true;
310
- const dockerfile = machine.dockerfile
311
- || (needsDocker ? 'docker/Dockerfile.target-machine-docker' : 'docker/Dockerfile.target-machine');
365
+ const dockerfile =
366
+ machine.dockerfile ||
367
+ (needsDocker
368
+ ? 'docker/Dockerfile.target-machine-docker'
369
+ : 'docker/Dockerfile.target-machine');
312
370
 
313
371
  const machineVolumes = ['ssh-keys:/ssh-keys:ro'];
314
372
 
@@ -366,7 +424,8 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
366
424
 
367
425
  const services: Record<string, unknown> = {};
368
426
 
369
- const managementDefaultGw = config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
427
+ const managementDefaultGw =
428
+ config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
370
429
 
371
430
  services.management = baseService({
372
431
  build: { context: '.', dockerfile: 'docker/Dockerfile.management' },
@@ -378,14 +437,30 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
378
437
  ...(config.managementVolumes ?? []),
379
438
  ],
380
439
  environment: {
381
- DDNS_ENDPOINT: 'http://100.64.0.55:8080/update',
440
+ DDNS_ENDPOINT: `http://${SIMULATOR_IPS.NAMECHEAP_DNS}:8080/update`,
382
441
  DEFAULT_GATEWAY: managementDefaultGw,
442
+ CELILO_REGISTRY_URL: 'http://e2e-registry.lab',
383
443
  },
384
444
  });
385
445
 
386
446
  const topologyFn = TOPOLOGY_SERVICES[config.topology] || TOPOLOGY_SERVICES.default;
387
447
  Object.assign(services, topologyFn());
388
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
+
389
464
  services['fw-ext'] = baseService({
390
465
  build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
391
466
  networks: {
@@ -408,7 +483,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
408
483
 
409
484
  services['root-dns'] = baseService({
410
485
  build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
411
- networks: { 'internet-external': { ipv4_address: '100.64.0.53' } },
486
+ networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.ROOT_DNS } },
412
487
  cap_add: ['NET_ADMIN'],
413
488
  volumes: [
414
489
  './config/dns/knot-root.conf:/config/knot.conf:ro',
@@ -418,22 +493,31 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
418
493
 
419
494
  services['tld-dns'] = baseService({
420
495
  build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
421
- networks: { 'internet-external': { ipv4_address: '100.64.0.54' } },
496
+ networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.TLD_DNS } },
422
497
  cap_add: ['NET_ADMIN'],
423
498
  volumes: [
424
499
  './config/dns/knot-tld.conf:/config/knot.conf:ro',
425
500
  './config/dns/com.zone:/config/com.zone:ro',
426
501
  './config/dns/org.zone:/config/org.zone:ro',
502
+ './config/dns/net.zone:/config/net.zone:ro',
427
503
  ],
428
504
  });
429
505
 
430
506
  services['namecheap-dns'] = baseService({
431
507
  build: { context: '.', dockerfile: 'docker/Dockerfile.namecheap-dns' },
432
- networks: { 'internet-external': { ipv4_address: '100.64.0.55' } },
508
+ networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.NAMECHEAP_DNS } },
433
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
+ },
434
516
  volumes: [
435
517
  './config/dns/knot-namecheap.conf:/config/knot.conf:ro',
436
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',
437
521
  './config/dns/park-your-domain.com.zone:/config/park-your-domain.com.zone',
438
522
  ],
439
523
  });
@@ -450,7 +534,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
450
534
 
451
535
  services.letsencrypt = baseService({
452
536
  build: { context: '.', dockerfile: 'docker/Dockerfile.pebble' },
453
- networks: { 'internet-external': { ipv4_address: '100.64.0.100' } },
537
+ networks: { 'internet-external': { ipv4_address: SIMULATOR_IPS.PEBBLE } },
454
538
  cap_add: ['NET_ADMIN'],
455
539
  command: '-config /config/pebble-config.json -dnsserver 100.100.0.1:53',
456
540
  volumes: [
@@ -460,6 +544,16 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
460
544
  ],
461
545
  });
462
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
+
463
557
  const allMachines = [
464
558
  ...config.dmzMachines,
465
559
  ...config.appMachines,
@@ -472,8 +566,11 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
472
566
  const gateway = ZONE_GATEWAYS[machine.zone];
473
567
 
474
568
  const needsDocker = machine.zone === 'app' || machine.docker === true;
475
- const dockerfile = machine.dockerfile
476
- || (needsDocker ? 'docker/Dockerfile.target-machine-docker' : 'docker/Dockerfile.target-machine');
569
+ const dockerfile =
570
+ machine.dockerfile ||
571
+ (needsDocker
572
+ ? 'docker/Dockerfile.target-machine-docker'
573
+ : 'docker/Dockerfile.target-machine');
477
574
 
478
575
  const machineVolumes = ['ssh-keys:/ssh-keys:ro'];
479
576
 
@@ -504,5 +601,10 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
504
601
  }
505
602
 
506
603
  export function getAllMachines(config: NetworkConfig): MachineSpec[] {
507
- return [...config.dmzMachines, ...config.appMachines, ...config.secureMachines, ...config.internalMachines];
604
+ return [
605
+ ...config.dmzMachines,
606
+ ...config.appMachines,
607
+ ...config.secureMachines,
608
+ ...config.internalMachines,
609
+ ];
508
610
  }
package/src/parse-line.ts CHANGED
@@ -42,6 +42,13 @@ export function parseLine(line: string, display: DisplaySink): void {
42
42
  return;
43
43
  }
44
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
+
45
52
  // Legacy: [progress] message (treat as instant event)
46
53
  const legacyMatch = line.match(/\[progress\] (.*)/);
47
54
  if (legacyMatch) {
@@ -124,13 +131,17 @@ export function parseLine(line: string, display: DisplaySink): void {
124
131
  return;
125
132
  }
126
133
 
127
- // Error lines
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: (.*)/);
128
139
  if (
129
- line.includes('Error:') &&
140
+ errorMatch &&
130
141
  !line.includes('bun test') &&
131
142
  !line.includes('ipv4: Address already')
132
143
  ) {
133
- const err = line.replace(/.*Error: /, '').slice(0, 70);
144
+ const err = errorMatch[1].slice(0, 70);
134
145
  display.subEvent(`${red}✗${reset} ${err}`);
135
146
  }
136
147
  }