@celilo/e2e 0.1.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.
Files changed (71) hide show
  1. package/README.md +360 -0
  2. package/bin/cache-docker-images +46 -0
  3. package/bin/e2e-build +66 -0
  4. package/bin/e2e-down +67 -0
  5. package/bin/e2e-infra +92 -0
  6. package/bin/e2e-load +40 -0
  7. package/bin/e2e-run +354 -0
  8. package/bin/e2e-shell +27 -0
  9. package/bin/e2e-status +29 -0
  10. package/bin/e2e-up +160 -0
  11. package/config/dhcp/dnsmasq.conf +27 -0
  12. package/config/dns/com.zone +11 -0
  13. package/config/dns/iamtheinternet.org.zone +15 -0
  14. package/config/dns/knot-namecheap.conf +11 -0
  15. package/config/dns/knot-root.conf +7 -0
  16. package/config/dns/knot-tld.conf +11 -0
  17. package/config/dns/org.zone +14 -0
  18. package/config/dns/park-your-domain.com.zone +10 -0
  19. package/config/dns/root.zone +14 -0
  20. package/config/pebble/pebble-ca.crt +20 -0
  21. package/config/pebble/pebble-config.json +11 -0
  22. package/config/pebble/pebble-tls.crt +21 -0
  23. package/config/pebble/pebble-tls.key +28 -0
  24. package/config/proxy/generate-ca.sh +20 -0
  25. package/config/proxy/squid-ca.crt +22 -0
  26. package/config/proxy/squid-ca.pem +50 -0
  27. package/config/proxy/squid.conf +38 -0
  28. package/config/proxy/startup.sh +11 -0
  29. package/config/resolver/root.hints +2 -0
  30. package/config/resolver/unbound.conf +28 -0
  31. package/config/routing/apt-cache-routes.sh +21 -0
  32. package/config/routing/dhcp-client-setup.sh +17 -0
  33. package/config/routing/dns-routes.sh +12 -0
  34. package/config/routing/fw-ext-routes.sh +50 -0
  35. package/config/routing/fw-isp-routes.sh +37 -0
  36. package/config/routing/fw-main-routes.sh +31 -0
  37. package/config/routing/management-routes.sh +96 -0
  38. package/config/routing/namecheap-startup.sh +17 -0
  39. package/config/routing/pebble-startup.sh +9 -0
  40. package/config/routing/resolver-routes.sh +15 -0
  41. package/config/routing/target-routes.sh +23 -0
  42. package/config/routing/target-setup.service +13 -0
  43. package/config/routing/target-setup.sh +36 -0
  44. package/config/ssh/generate-keys.sh +17 -0
  45. package/docker/Dockerfile.apt-cache +16 -0
  46. package/docker/Dockerfile.authoritative-dns +15 -0
  47. package/docker/Dockerfile.dhcp-client +16 -0
  48. package/docker/Dockerfile.firewall +28 -0
  49. package/docker/Dockerfile.forward-proxy +28 -0
  50. package/docker/Dockerfile.greenwave-sim +29 -0
  51. package/docker/Dockerfile.management +30 -0
  52. package/docker/Dockerfile.namecheap-dns +25 -0
  53. package/docker/Dockerfile.pebble +18 -0
  54. package/docker/Dockerfile.resolver +20 -0
  55. package/docker/Dockerfile.router +33 -0
  56. package/docker/Dockerfile.target-machine +58 -0
  57. package/docker/Dockerfile.target-machine-docker +105 -0
  58. package/package.json +45 -0
  59. package/simulators/greenwave/server.ts +164 -0
  60. package/simulators/greenwave/state.ts +206 -0
  61. package/simulators/greenwave/types.ts +31 -0
  62. package/simulators/namecheap-ddns/server.ts +97 -0
  63. package/simulators/namecheap-ddns/types.ts +12 -0
  64. package/simulators/namecheap-ddns/zone-updater.ts +49 -0
  65. package/src/container-manager.ts +324 -0
  66. package/src/docker-compose-generator.ts +499 -0
  67. package/src/fixtures.ts +40 -0
  68. package/src/index.ts +59 -0
  69. package/src/network-builder.ts +91 -0
  70. package/src/shared-infra.ts +163 -0
  71. package/src/types.ts +74 -0
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Shared Infrastructure Manager
3
+ *
4
+ * Starts the shared infrastructure (DNS hierarchy, Pebble ACME, apt-cache,
5
+ * fw-ext) once per test suite run. Per-test containers connect to the
6
+ * shared networks via Docker's external network references.
7
+ *
8
+ * The shared infra project name is deterministic (celilo-e2e-shared) so
9
+ * it can be referenced by per-test compose files and cleaned up reliably.
10
+ */
11
+
12
+ import { execSync } from 'node:child_process';
13
+ import { join } from 'node:path';
14
+ import { writeFileSync } from 'node:fs';
15
+ import { generateSharedInfraYaml, SHARED_PROJECT_NAME, SHARED_NETWORKS } from './docker-compose-generator';
16
+
17
+ const SHARED_COMPOSE_FILE = 'docker-compose.shared.yml';
18
+ const COMPOSE_TIMEOUT = 120_000;
19
+
20
+ function run(cmd: string, opts?: { cwd?: string; timeout?: number }): string {
21
+ return execSync(cmd, {
22
+ timeout: opts?.timeout ?? COMPOSE_TIMEOUT,
23
+ encoding: 'utf-8',
24
+ stdio: ['pipe', 'pipe', 'pipe'],
25
+ cwd: opts?.cwd,
26
+ }).trim();
27
+ }
28
+
29
+ let sharedInfraRunning = false;
30
+
31
+ /**
32
+ * Check if the shared infrastructure is already running and healthy.
33
+ */
34
+ export function isSharedInfraRunning(): boolean {
35
+ if (!sharedInfraRunning) return false;
36
+ try {
37
+ const containers = run(
38
+ `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} ps -q`,
39
+ { cwd: getE2eDir() },
40
+ );
41
+ return containers.trim().length > 0;
42
+ } catch {
43
+ sharedInfraRunning = false;
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function getE2eDir(): string {
49
+ return join(__dirname, '..');
50
+ }
51
+
52
+ /**
53
+ * Start the shared infrastructure. Idempotent — if already running,
54
+ * verifies health and returns immediately.
55
+ */
56
+ export async function ensureSharedInfra(): Promise<void> {
57
+ if (isSharedInfraRunning()) {
58
+ // Verify DNS is still healthy
59
+ try {
60
+ const result = run(
61
+ `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} exec -T comcast-resolver dig @127.0.0.1 iamtheinternet.org NS +short +timeout=2`,
62
+ { cwd: getE2eDir(), timeout: 10_000 },
63
+ );
64
+ if (result.trim().length > 0) {
65
+ return; // Shared infra healthy, nothing to do
66
+ }
67
+ } catch {
68
+ // DNS check failed — restart shared infra
69
+ console.log('[e2e] Shared infra DNS check failed, restarting...');
70
+ await stopSharedInfra();
71
+ }
72
+ }
73
+
74
+ const e2eDir = getE2eDir();
75
+
76
+ // Clean up any stale shared infra resources
77
+ console.log('[e2e] Cleaning up stale shared infrastructure...');
78
+ try {
79
+ run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
80
+ { cwd: e2eDir, timeout: 30_000 });
81
+ } catch {}
82
+
83
+ // Also clean up any per-test projects that reference our networks
84
+ try {
85
+ const networks = run('docker network ls --format "{{.Name}}"')
86
+ .split('\n')
87
+ .filter(n => n.startsWith('celilo-e2e-') && !n.startsWith(`${SHARED_PROJECT_NAME}_`));
88
+ if (networks.length > 0) {
89
+ const projects = [...new Set(networks.map(n => n.replace(/_[^_]+$/, '')))];
90
+ for (const project of projects) {
91
+ try { run(`docker compose -p ${project} down --volumes --remove-orphans`, { timeout: 30_000 }); } catch {}
92
+ }
93
+ }
94
+ } catch {}
95
+
96
+ try { run('docker network prune -f', { timeout: 10_000 }); } catch {}
97
+
98
+ // Generate and write compose file
99
+ const yaml = generateSharedInfraYaml();
100
+ writeFileSync(join(e2eDir, SHARED_COMPOSE_FILE), yaml);
101
+
102
+ // Build and start
103
+ console.log('[e2e] Building shared infrastructure images...');
104
+ run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} build`,
105
+ { cwd: e2eDir, timeout: 300_000 });
106
+
107
+ console.log('[e2e] Starting shared infrastructure...');
108
+ run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} up -d`,
109
+ { cwd: e2eDir, timeout: 120_000 });
110
+
111
+ // Wait for DNS convergence
112
+ console.log('[e2e] Waiting for DNS convergence...');
113
+ const start = Date.now();
114
+ const timeout = 60_000;
115
+ while (Date.now() - start < timeout) {
116
+ try {
117
+ const result = run(
118
+ `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} exec -T comcast-resolver dig @127.0.0.1 iamtheinternet.org NS +short +timeout=2`,
119
+ { cwd: e2eDir, timeout: 10_000 },
120
+ );
121
+ if (result.trim().length > 0) {
122
+ sharedInfraRunning = true;
123
+ console.log('[e2e] Shared infrastructure ready');
124
+ return;
125
+ }
126
+ } catch {
127
+ // retry
128
+ }
129
+ await new Promise(r => setTimeout(r, 2000));
130
+ }
131
+ throw new Error('Timeout waiting for shared infrastructure DNS convergence');
132
+ }
133
+
134
+ /**
135
+ * Stop the shared infrastructure. Called once at the end of the test suite.
136
+ */
137
+ export async function stopSharedInfra(): Promise<void> {
138
+ console.log('[e2e] Stopping shared infrastructure...');
139
+ const e2eDir = getE2eDir();
140
+ try {
141
+ run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
142
+ { cwd: e2eDir, timeout: 60_000 });
143
+ } catch {
144
+ // Best effort
145
+ }
146
+ sharedInfraRunning = false;
147
+ }
148
+
149
+ /**
150
+ * Execute a command in a shared infrastructure container.
151
+ */
152
+ export function sharedExec(container: string, cmd: string, timeoutMs = 60_000): { stdout: string; exitCode: number } {
153
+ try {
154
+ const stdout = run(
155
+ `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} exec -T ${container} bash -c ${JSON.stringify(cmd)}`,
156
+ { cwd: getE2eDir(), timeout: timeoutMs },
157
+ );
158
+ return { stdout, exitCode: 0 };
159
+ } catch (err: unknown) {
160
+ const e = err as { stdout?: string; status?: number };
161
+ return { stdout: e.stdout?.toString() ?? '', exitCode: e.status ?? 1 };
162
+ }
163
+ }
package/src/types.ts ADDED
@@ -0,0 +1,74 @@
1
+ export type Zone = 'dmz' | 'app' | 'secure' | 'internal';
2
+
3
+ export interface MachineSpec {
4
+ name: string;
5
+ ip: string;
6
+ zone: Zone;
7
+ }
8
+
9
+ export type TopologyPreset = 'default' | 'direct-internet';
10
+
11
+ export interface NetworkConfig {
12
+ topology: TopologyPreset;
13
+ dmzMachines: MachineSpec[];
14
+ appMachines: MachineSpec[];
15
+ secureMachines: MachineSpec[];
16
+ internalMachines: MachineSpec[];
17
+ domain: string;
18
+ ddnsPassword: string;
19
+ verifyRouting: boolean;
20
+ /** Extra volume mounts for the management container (host:container format) */
21
+ managementVolumes: string[];
22
+ /** Include a DHCP client container on the internal network */
23
+ dhcpClient: boolean;
24
+ /**
25
+ * Absolute path to the Celilo project root (the directory containing apps/celilo/).
26
+ * Mounted into the management container at /celilo so the CLI is available.
27
+ * If not set, auto-detected by walking up from the package directory.
28
+ */
29
+ celiloRoot?: string;
30
+ }
31
+
32
+ export interface ExecResult {
33
+ stdout: string;
34
+ stderr: string;
35
+ exitCode: number;
36
+ }
37
+
38
+ export interface NetworkHandle {
39
+ /** Run a celilo CLI command on the management machine */
40
+ celilo(cmd: string, timeoutMs?: number): Promise<ExecResult>;
41
+
42
+ /** Execute an arbitrary command in any container */
43
+ exec(container: string, cmd: string, timeoutMs?: number): Promise<ExecResult>;
44
+
45
+ /** Resolve a DNS name from the management machine */
46
+ dig(name: string): Promise<string>;
47
+
48
+ /** Wait for a condition with timeout */
49
+ waitFor(
50
+ check: () => Promise<boolean>,
51
+ timeoutMs: number,
52
+ label: string,
53
+ ): Promise<void>;
54
+
55
+ /** Tear down the entire network */
56
+ stop(): Promise<void>;
57
+
58
+ /** The docker-compose project name (for debugging) */
59
+ projectName: string;
60
+ }
61
+
62
+ export const ZONE_GATEWAYS: Record<Zone, string> = {
63
+ dmz: '10.0.10.1',
64
+ app: '10.0.20.1',
65
+ secure: '10.0.30.1',
66
+ internal: '192.168.0.254',
67
+ };
68
+
69
+ export const ZONE_SUBNETS: Record<Zone, string> = {
70
+ dmz: '10.0.10.0/24',
71
+ app: '10.0.20.0/24',
72
+ secure: '10.0.30.0/24',
73
+ internal: '192.168.0.0/24',
74
+ };