@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
package/README.md ADDED
@@ -0,0 +1,360 @@
1
+ # Celilo E2E Test Environment
2
+
3
+ A Docker-based simulated internet for end-to-end testing of Celilo's full deployment flow: importing modules, configuring firewalls, registering DNS, deploying services via Ansible, and obtaining TLS certificates via ACME.
4
+
5
+ ![Network Diagram](docs/network-diagram.svg)
6
+
7
+ ## Why This Exists
8
+
9
+ Unit and integration tests validate individual components, but can't test the real interactions between SSH, Ansible, iptables, DNS resolution, and ACME certificate issuance. The E2E environment simulates a complete internet with real routing, a real DNS hierarchy, and service simulators that behave like Namecheap, the ISP router, and Let's Encrypt.
10
+
11
+ Celilo runs inside this simulated network and deploys to Docker containers as if they were real machines. From Celilo's perspective, it's deploying to a real home lab with a real ISP and real internet services.
12
+
13
+ ## Network Architecture
14
+
15
+ The network consists of 7 Docker bridge networks simulating a realistic home lab topology:
16
+
17
+ | Network | Subnet | Purpose |
18
+ |---------|--------|---------|
19
+ | **internal** | 192.168.0.0/24 | Home LAN (management, firewalls) |
20
+ | **dmz** | 10.0.10.0/24 | Public-facing services |
21
+ | **app** | 10.0.20.0/24 | Internal applications |
22
+ | **secure** | 10.0.30.0/24 | Sensitive services |
23
+ | **isp-external** | 100.100.0.0/24 | ISP network (between home and internet) |
24
+ | **internet-external** | 100.64.0.0/24 | Simulated internet (DNS, ACME, etc.) |
25
+ | **real-internet** | 172.30.0.0/24 | Bridge to actual internet (for apt, pip, etc.) |
26
+
27
+ All networks except `real-internet` have Docker IP masquerade **disabled** — traffic flows through explicit iptables routing, not Docker's NAT. The `real-internet` network has masquerade enabled to provide actual internet access for package downloads.
28
+
29
+ ## Machines
30
+
31
+ ### Fixed Infrastructure (always present)
32
+
33
+ | Machine | Role | Networks | IPs |
34
+ |---------|------|----------|-----|
35
+ | **management** | Celilo CLI, Ansible, SSH | internal | 192.168.0.100 |
36
+ | **fw-main** | iptables firewall (managed by Celilo) | internal, dmz, app, secure | 192.168.0.254, 10.0.10.1, 10.0.20.1, 10.0.30.1 |
37
+ | **fw-isp** | Greenwave router simulator | internal, isp-external | 192.168.0.1, 100.100.0.100 |
38
+ | **fw-ext** | Edge router + transparent HTTPS proxy (Squid) | isp-external, internet-external, real-internet | 100.100.0.101, 100.64.0.1, 172.30.0.5 |
39
+ | **comcast-resolver** | Unbound recursive DNS resolver | isp-external, real-internet | 100.100.0.1, 172.30.0.4 |
40
+ | **root-dns** | Knot authoritative DNS (root zone) | internet-external | 100.64.0.53 |
41
+ | **tld-dns** | Knot authoritative DNS (.com, .org TLDs) | internet-external | 100.64.0.54 |
42
+ | **namecheap-dns** | Knot DNS + DDNS API simulator | internet-external | 100.64.0.55 |
43
+ | **letsencrypt** | Pebble ACME test server | internet-external | 100.64.0.100 |
44
+ | **apt-cache** | apt-cacher-ng package proxy | internet-external, real-internet | 100.64.0.2, 172.30.0.2 |
45
+
46
+ ### Dynamic Test Machines
47
+
48
+ Added per-test via the `network()` builder API. Each is an Ubuntu 22.04 container running systemd, with SSH server, placed on the appropriate zone network.
49
+
50
+ | Example | Network | IP | Purpose |
51
+ |---------|---------|-----|---------|
52
+ | caddy | dmz | 10.0.10.10 | Caddy reverse proxy deployment |
53
+ | idp | app | 10.0.20.100 | Identity provider deployment |
54
+ | db | secure | 10.0.30.50 | Database deployment |
55
+
56
+ ## Routing
57
+
58
+ Traffic flows through explicit routing chains, mimicking a real network:
59
+
60
+ | From | To | Path |
61
+ |------|----|------|
62
+ | management | dmz/app/secure | via fw-main (192.168.0.254) |
63
+ | management | internet | via fw-isp (192.168.0.1) → fw-ext |
64
+ | dmz machines | internet | via fw-main → fw-isp → fw-ext |
65
+ | Pebble | caddy (HTTP-01 challenge) | via fw-ext → fw-isp DNAT → fw-main DNAT → caddy |
66
+
67
+ All firewall/router containers run `ip_forward=1` and `MASQUERADE` on their outbound interfaces.
68
+
69
+ ## DNS
70
+
71
+ The DNS system is a hybrid of simulated and real resolution:
72
+
73
+ **Simulated domains** (handled by the E2E DNS hierarchy):
74
+ - `iamtheinternet.org` — stub zone to namecheap-dns (100.64.0.55)
75
+ - `park-your-domain.com` — stub zone to namecheap-dns (Namecheap DDNS API)
76
+ - `acme-v02.api.letsencrypt.org` — local-data in Unbound pointing to Pebble (100.64.0.100)
77
+
78
+ **Real domains** (forwarded to actual DNS):
79
+ - Everything else (e.g., `dl.cloudsmith.io`, `deb.debian.org`) is forwarded by Unbound to `8.8.8.8` and `1.1.1.1` via the `real-internet` bridge.
80
+
81
+ This means Celilo's modules install real packages from the internet while all domain-specific operations (DDNS, ACME) go through the simulation.
82
+
83
+ ## Transparent HTTPS Proxy
84
+
85
+ A Squid proxy with SSL bumping runs on fw-ext, transparently intercepting outbound HTTPS traffic from the simulated network. This lets target machines `curl https://dl.cloudsmith.io` (to install Caddy from its apt repo) without explicit proxy configuration.
86
+
87
+ **How it works:**
88
+ 1. iptables REDIRECT rules on fw-ext's ISP interface capture port 80/443 traffic
89
+ 2. Exception: traffic to `100.64.0.0/24` (simulated services) passes through directly
90
+ 3. Squid does SSL bump (MITM) with a pre-shared CA trusted by all machines
91
+ 4. Squid resolves DNS via real public DNS and fetches from the real internet
92
+
93
+ ## Simulators
94
+
95
+ ### Namecheap Dynamic DNS
96
+
97
+ Runs on `namecheap-dns` (100.64.0.55). A Bun HTTP server on port 8080 that implements the Namecheap DDNS API:
98
+
99
+ ```
100
+ GET /update?host=<host>&domain=<domain>&password=<password>&ip=<ip>
101
+ ```
102
+
103
+ When called, it updates the Knot DNS zone file and signals a zone reload. DNS propagates through the full chain (namecheap-dns → tld-dns → root-dns → comcast-resolver).
104
+
105
+ ### Greenwave Router
106
+
107
+ Runs on `fw-isp` (192.168.0.1). A Bun HTTPS server implementing the C4000XG REST API subset:
108
+ - `POST /cgi/cgi_action` — Login/logout
109
+ - `GET /cgi/cgi_get` — Read config (public IP, port mappings)
110
+ - `POST /cgi/cgi_set` — Add/remove port forwarding rules
111
+
112
+ Port forwarding rules are applied via actual iptables commands, making NAT functional in the simulated network. The management HTTPS interface binds to the internal interface only (192.168.0.1), not the external interface.
113
+
114
+ ### Pebble (Let's Encrypt)
115
+
116
+ Uses the official Pebble ACME test server, wrapped in a custom Dockerfile that adds routing. Pebble uses the comcast-resolver for DNS and validates HTTP-01 challenges through the full routing chain.
117
+
118
+ The Pebble TLS certificate includes `acme-v02.api.letsencrypt.org` as a SAN, so Caddy's default ACME configuration works with zero changes (DNS resolves the real Let's Encrypt hostname to Pebble).
119
+
120
+ fw-ext automatically fetches Pebble's runtime ACME root CA from its management API (`https://pebble:15000/roots/0`) at startup, so `curl` from fw-ext can verify Caddy's ACME-issued certificates.
121
+
122
+ ## Manual Usage
123
+
124
+ ### Starting the Environment
125
+
126
+ ```bash
127
+ cd e2e
128
+
129
+ # Infrastructure only (no target machines)
130
+ ./bin/e2e-up
131
+
132
+ # With a caddy machine in the DMZ
133
+ ./bin/e2e-up --caddy
134
+
135
+ # Full stack (caddy + idp + db)
136
+ ./bin/e2e-up --full-stack
137
+
138
+ # Custom machine spec
139
+ ./bin/e2e-up --custom '{"dmz":{"caddy":"10.0.10.10","web":"10.0.10.20"}}'
140
+ ```
141
+
142
+ After startup, you're dropped into the management machine shell with tab completion for `celilo` commands (aliased as `c`).
143
+
144
+ ### Accessing Machines
145
+
146
+ ```bash
147
+ # Reconnect to management (default)
148
+ ./bin/e2e-shell
149
+
150
+ # Shell into any container
151
+ ./bin/e2e-shell caddy
152
+ ./bin/e2e-shell fw-main
153
+ ./bin/e2e-shell fw-ext
154
+ ./bin/e2e-shell namecheap-dns
155
+ ./bin/e2e-shell letsencrypt
156
+ ```
157
+
158
+ Management uses `zsh` (with celilo aliases). All other containers use `bash`.
159
+
160
+ ### Running the Manual Test Script
161
+
162
+ From the management shell (`./bin/e2e-shell` or after `./bin/e2e-up`):
163
+
164
+ ```bash
165
+ cd /celilo/modules
166
+
167
+ # System init
168
+ c system init --accept-defaults \
169
+ network.dmz.subnet=10.0.10.0/24 \
170
+ network.app.subnet=10.0.20.0/24 \
171
+ network.secure.subnet=10.0.30.0/24 \
172
+ network.internal.subnet=192.168.0.0/24 \
173
+ primary_domain=iamtheinternet.org \
174
+ admin.email=admin@iamtheinternet.org \
175
+ dns.primary=100.100.0.1 \
176
+ dns.fallback=1.0.0.1,8.8.8.8
177
+
178
+ # Import all modules
179
+ c module import namecheap
180
+ c module import greenwave
181
+ c module import iptables
182
+ c module import caddy
183
+
184
+ # Add machines (--ssh-user root needed for non-interactive)
185
+ c machine add 192.168.0.1 --ssh-user root --earmark greenwave
186
+ c machine add 192.168.0.254 --ssh-user root --earmark iptables
187
+ c machine add 10.0.10.10 --ssh-user root
188
+
189
+ # Pre-configure secrets and non-derivable config
190
+ c module secret set namecheap ddns_password test123
191
+ c module config set greenwave router_ip 192.168.0.1
192
+ c module secret set greenwave router_username admin
193
+ c module secret set greenwave router_password admin
194
+ c module config set iptables nat_ip 192.168.0.253
195
+ c module config set caddy hostname www
196
+ c module config set caddy acme_ca https://acme-v02.api.letsencrypt.org/dir
197
+
198
+ # Deploy (iptables auto-derives config from earmarked machine)
199
+ c module deploy namecheap --no-interactive
200
+ c module deploy greenwave --no-interactive
201
+ c module deploy iptables
202
+ c module deploy caddy --no-interactive
203
+ ```
204
+
205
+ ### Verifying the Deployment
206
+
207
+ From fw-ext (the "internet" side):
208
+
209
+ ```bash
210
+ ./bin/e2e-shell fw-ext
211
+ curl -s https://www.iamtheinternet.org
212
+ # Expected: "Caddy reverse proxy is running"
213
+ ```
214
+
215
+ ### Checking Status
216
+
217
+ ```bash
218
+ ./bin/e2e-status # Shows containers, DNS, connectivity
219
+ ```
220
+
221
+ ### Tearing Down
222
+
223
+ ```bash
224
+ ./bin/e2e-down # Stop and remove everything
225
+ ./bin/e2e-down --keep # Stop but keep volumes (faster restart)
226
+ ```
227
+
228
+ ## Writing Automated Tests
229
+
230
+ Tests use Vitest and the `NetworkBuilder` API to start isolated networks, run Celilo commands, and verify results.
231
+
232
+ ### Test Structure
233
+
234
+ ```typescript
235
+ import { afterAll, describe, expect, it } from 'vitest';
236
+ import { CADDY_DEPLOYMENT } from '../src/fixtures';
237
+ import type { NetworkHandle } from '../src/types';
238
+
239
+ describe('my deployment test', () => {
240
+ let net: NetworkHandle;
241
+
242
+ afterAll(async () => {
243
+ await net?.stop(); // Always clean up
244
+ });
245
+
246
+ it('deploys and verifies', async () => {
247
+ // 1. Start the network with desired machines
248
+ net = await CADDY_DEPLOYMENT().start();
249
+
250
+ // 2. Add machines, import modules, configure
251
+ await net.celilo('machine add 10.0.10.10 --ssh-user root');
252
+ await net.celilo('module import /celilo/modules/caddy');
253
+ await net.celilo('module config set caddy hostname www');
254
+
255
+ // 3. Deploy
256
+ const result = await net.celilo('module deploy caddy --no-interactive');
257
+ expect(result.exitCode).toBe(0);
258
+
259
+ // 4. Verify from any container
260
+ const curl = await net.exec('fw-ext', 'curl -s https://www.iamtheinternet.org');
261
+ expect(curl.stdout).toContain('Caddy');
262
+ }, 300_000); // Per-test timeout
263
+ });
264
+ ```
265
+
266
+ ### NetworkHandle API
267
+
268
+ | Method | Description |
269
+ |--------|-------------|
270
+ | `celilo(cmd, timeout?)` | Run a celilo CLI command on the management machine |
271
+ | `exec(container, cmd, timeout?)` | Execute a command in any container |
272
+ | `dig(name)` | Resolve a DNS name from management |
273
+ | `waitFor(check, timeout, label)` | Poll until a condition is true |
274
+ | `stop()` | Tear down the entire network |
275
+
276
+ ### Fixtures
277
+
278
+ Pre-built network configurations in `src/fixtures.ts`:
279
+
280
+ ```typescript
281
+ CADDY_DEPLOYMENT() // caddy machine in DMZ
282
+ FULL_STACK() // caddy (dmz) + idp (app) + db (secure)
283
+ INFRASTRUCTURE_ONLY() // no dynamic machines
284
+ ```
285
+
286
+ ### Custom Network Configurations
287
+
288
+ ```typescript
289
+ import { network } from '../src/network-builder';
290
+
291
+ const net = await network()
292
+ .dmz({ caddy: '10.0.10.10', web: '10.0.10.20' })
293
+ .app({ api: '10.0.20.100' })
294
+ .secure({ db: '10.0.30.50' })
295
+ .start();
296
+ ```
297
+
298
+ ### Running Tests
299
+
300
+ ```bash
301
+ cd e2e
302
+
303
+ # Run all E2E tests
304
+ bun run vitest run
305
+
306
+ # Run a specific test
307
+ bun run vitest run tests/caddy-deploy.test.ts
308
+
309
+ # Run with verbose output
310
+ bun run vitest run --reporter=verbose
311
+ ```
312
+
313
+ Tests run sequentially (fixed CIDRs prevent parallelism). The container manager automatically cleans up leftover networks from previous failed runs.
314
+
315
+ ## Key Design Decisions
316
+
317
+ **Systemd on target machines:** Target machines run systemd as PID 1 (privileged mode) so Ansible's `systemd` module works naturally. Network setup runs as a oneshot systemd service at boot.
318
+
319
+ **No test parallelism:** Fixed network CIDRs mean only one test network can run at a time. Tests run sequentially via Vitest's `singleFork` config.
320
+
321
+ **Bind-mounted source:** The celilo source tree is bind-mounted at `/celilo` on the management container. Code changes take effect immediately without rebuilding images.
322
+
323
+ **Docker image caching:** Images are built once and cached. Only config/simulator changes require rebuilds. The management container uses the bind mount, so celilo code changes are free.
324
+
325
+ **ACME via DNS interception:** Instead of overriding Caddy's ACME URL, `acme-v02.api.letsencrypt.org` resolves to Pebble (100.64.0.100) in the simulated DNS. The caddy module's `acme_ca` variable allows pointing to Pebble's `/dir` endpoint (vs Let's Encrypt's `/directory`).
326
+
327
+ ## Directory Structure
328
+
329
+ ```
330
+ e2e/
331
+ bin/
332
+ e2e-up # Start interactive network
333
+ e2e-down # Tear down network
334
+ e2e-shell # Shell into containers
335
+ e2e-status # Show network status
336
+ config/
337
+ dns/ # Knot zone files and configs
338
+ pebble/ # Pebble ACME config and TLS certs
339
+ proxy/ # Squid transparent proxy config
340
+ resolver/ # Unbound recursive resolver config
341
+ routing/ # Per-container routing scripts
342
+ ssh/ # SSH key generation
343
+ docker/
344
+ Dockerfile.* # Container images
345
+ docs/
346
+ network-diagram.svg # Network topology diagram
347
+ simulators/
348
+ greenwave/ # C4000XG router REST API simulator
349
+ namecheap-ddns/ # Namecheap DDNS API simulator
350
+ src/
351
+ container-manager.ts # Docker compose orchestration
352
+ docker-compose-generator.ts # Generates compose YAML
353
+ fixtures.ts # Pre-built network configs
354
+ network-builder.ts # Fluent API for network setup
355
+ types.ts # Shared type definitions
356
+ tests/
357
+ caddy-deploy.test.ts # Full caddy deployment with HTTPS
358
+ smoke.test.ts # Basic connectivity verification
359
+ ...
360
+ ```
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Pre-pull and save Docker images needed by e2e test modules.
4
+ # Saves them as tarballs that get mounted into app-zone containers
5
+ # for fast loading at boot time.
6
+ #
7
+ # Run this once (or after updating image versions) to populate the cache.
8
+ # Images are stored in e2e/docker-image-cache/
9
+
10
+ set -e
11
+
12
+ CACHE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/docker-image-cache"
13
+ mkdir -p "$CACHE_DIR"
14
+
15
+ IMAGES=(
16
+ "docker.io/library/postgres:15-alpine"
17
+ "docker.io/library/redis:alpine"
18
+ "ghcr.io/goauthentik/server:2024.2.2"
19
+ )
20
+
21
+ echo "Caching Docker images for e2e tests..."
22
+ echo "Cache directory: $CACHE_DIR"
23
+ echo ""
24
+
25
+ for image in "${IMAGES[@]}"; do
26
+ # Create a safe filename from the image name
27
+ filename=$(echo "$image" | sed 's|[/:]|_|g').tar
28
+
29
+ if [[ -f "$CACHE_DIR/$filename" ]]; then
30
+ echo "✓ $image (already cached)"
31
+ continue
32
+ fi
33
+
34
+ echo "▸ Pulling $image..."
35
+ docker pull "$image"
36
+
37
+ echo " Saving to $filename..."
38
+ docker save "$image" > "$CACHE_DIR/$filename"
39
+
40
+ size=$(du -h "$CACHE_DIR/$filename" | cut -f1)
41
+ echo "✓ $image ($size)"
42
+ done
43
+
44
+ echo ""
45
+ echo "Cache complete. $(ls "$CACHE_DIR"/*.tar 2>/dev/null | wc -l | tr -d ' ') images cached."
46
+ echo "Total size: $(du -sh "$CACHE_DIR" | cut -f1)"
package/bin/e2e-build ADDED
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ #
3
+ # Pre-build all E2E Docker images.
4
+ #
5
+ # Run once before the test suite. Subsequent test runs use the cached
6
+ # images and skip the build phase entirely (~5s vs ~3min per test).
7
+ #
8
+ # Usage:
9
+ # ./e2e/bin/e2e-build # build all images
10
+ # ./e2e/bin/e2e-build --save # build + save to tarball for colima restarts
11
+ #
12
+
13
+ set -e
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ E2E_DIR="$(dirname "$SCRIPT_DIR")"
17
+ cd "$E2E_DIR"
18
+
19
+ GREEN='\033[0;32m'
20
+ DIM='\033[2m'
21
+ BOLD='\033[1m'
22
+ NC='\033[0m'
23
+
24
+ echo -e "${BOLD}Building E2E Docker images...${NC}"
25
+ echo ""
26
+
27
+ DOCKERFILES=(docker/Dockerfile.*)
28
+ TOTAL=${#DOCKERFILES[@]}
29
+ START=$(date +%s)
30
+
31
+ for ((i=0; i<TOTAL; i++)); do
32
+ FILE="${DOCKERFILES[$i]}"
33
+ NAME=$(basename "$FILE" | sed 's/Dockerfile\.//')
34
+ TAG="celilo-e2e/${NAME}"
35
+
36
+ printf " [%d/%d] %-25s " "$((i+1))" "$TOTAL" "$NAME"
37
+
38
+ BUILD_START=$(date +%s)
39
+ docker build -t "$TAG" -f "$FILE" . > /dev/null 2>&1
40
+ BUILD_END=$(date +%s)
41
+ DURATION=$((BUILD_END - BUILD_START))
42
+
43
+ echo -e "${GREEN}✔${NC} ${DIM}${DURATION}s${NC}"
44
+ done
45
+
46
+ END=$(date +%s)
47
+ TOTAL_TIME=$((END - START))
48
+ echo ""
49
+ echo -e "${GREEN}All $TOTAL images built in ${TOTAL_TIME}s${NC}"
50
+
51
+ # Optionally save to tarball
52
+ if [ "$1" = "--save" ]; then
53
+ echo ""
54
+ echo -e "${BOLD}Saving images to tarball...${NC}"
55
+ TARBALL="$E2E_DIR/.docker-cache/celilo-e2e-images.tar"
56
+ mkdir -p "$(dirname "$TARBALL")"
57
+
58
+ IMAGES=$(docker images --filter "reference=celilo-e2e/*" --format "{{.Repository}}:{{.Tag}}" | sort)
59
+ docker save $IMAGES -o "$TARBALL"
60
+
61
+ SIZE=$(du -h "$TARBALL" | cut -f1)
62
+ echo -e "${GREEN}Saved to $TARBALL ($SIZE)${NC}"
63
+ echo ""
64
+ echo "To restore after colima restart:"
65
+ echo " ./e2e/bin/e2e-load"
66
+ fi
package/bin/e2e-down ADDED
@@ -0,0 +1,67 @@
1
+ #!/bin/bash
2
+ # Tear down ALL E2E test artifacts — interactive sessions AND orphaned test runs.
3
+ #
4
+ # Usage:
5
+ # ./bin/e2e-down # Stop and remove everything
6
+ # ./bin/e2e-down --keep # Stop containers but keep volumes (faster restart)
7
+ # ./bin/e2e-down --all # Also stop module containers (authentik, etc.) that compete for resources
8
+
9
+ set -e
10
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
11
+ cd "$SCRIPT_DIR"
12
+
13
+ KEEP_VOLUMES=false
14
+ STOP_ALL=false
15
+ for arg in "$@"; do
16
+ case "$arg" in
17
+ --keep) KEEP_VOLUMES=true ;;
18
+ --all) STOP_ALL=true ;;
19
+ esac
20
+ done
21
+
22
+ # 1. Stop the interactive session (if running)
23
+ echo "Stopping interactive session..."
24
+ docker compose -p "conductor-e2e-interactive" down --remove-orphans 2>/dev/null || true
25
+
26
+ # 2. Kill ALL containers matching conductor-e2e-* (orphaned test runs)
27
+ CONTAINERS=$(docker ps -a --filter "name=conductor-e2e" --format "{{.ID}}" 2>/dev/null)
28
+ if [ -n "$CONTAINERS" ]; then
29
+ COUNT=$(echo "$CONTAINERS" | wc -l | tr -d ' ')
30
+ echo "Removing $COUNT orphaned e2e containers..."
31
+ echo "$CONTAINERS" | xargs docker rm -f 2>/dev/null || true
32
+ fi
33
+
34
+ # 3. Stop module containers that compete for resources (--all flag)
35
+ # These come from local docker-compose runs, not from e2e, but starve the VM.
36
+ if [ "$STOP_ALL" = true ]; then
37
+ MODULE_PATTERNS="authentik- caddy- build-your-own-internet-"
38
+ for pattern in $MODULE_PATTERNS; do
39
+ MODULE_CONTAINERS=$(docker ps -a --filter "name=$pattern" --format "{{.ID}}" 2>/dev/null)
40
+ if [ -n "$MODULE_CONTAINERS" ]; then
41
+ COUNT=$(echo "$MODULE_CONTAINERS" | wc -l | tr -d ' ')
42
+ echo "Stopping $COUNT containers matching '$pattern*'..."
43
+ echo "$MODULE_CONTAINERS" | xargs docker rm -f 2>/dev/null || true
44
+ fi
45
+ done
46
+ fi
47
+
48
+ # 3. Remove all conductor-e2e networks
49
+ NETWORKS=$(docker network ls --filter "name=conductor-e2e" --format "{{.ID}}" 2>/dev/null)
50
+ if [ -n "$NETWORKS" ]; then
51
+ echo "Removing e2e networks..."
52
+ echo "$NETWORKS" | xargs docker network rm 2>/dev/null || true
53
+ fi
54
+
55
+ # 4. Remove volumes (unless --keep)
56
+ if [ "$KEEP_VOLUMES" = false ]; then
57
+ VOLUMES=$(docker volume ls --filter "name=conductor-e2e" --format "{{.Name}}" 2>/dev/null)
58
+ if [ -n "$VOLUMES" ]; then
59
+ echo "Removing e2e volumes..."
60
+ echo "$VOLUMES" | xargs docker volume rm 2>/dev/null || true
61
+ fi
62
+ fi
63
+
64
+ # 5. Clean up generated compose file
65
+ rm -f docker-compose.test.yml
66
+
67
+ echo "E2E environment cleaned."
package/bin/e2e-infra ADDED
@@ -0,0 +1,92 @@
1
+ #!/bin/bash
2
+ #
3
+ # Manage the shared E2E infrastructure (DNS, Pebble, apt-cache).
4
+ #
5
+ # Usage:
6
+ # ./e2e/bin/e2e-infra start # Start shared infra
7
+ # ./e2e/bin/e2e-infra stop # Stop shared infra
8
+ # ./e2e/bin/e2e-infra status # Check if running
9
+ # ./e2e/bin/e2e-infra restart # Restart shared infra
10
+ #
11
+
12
+ set -e
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ E2E_DIR="$(dirname "$SCRIPT_DIR")"
16
+ cd "$E2E_DIR"
17
+
18
+ SHARED_PROJECT="celilo-e2e-shared"
19
+ SHARED_COMPOSE="docker-compose.shared.yml"
20
+
21
+ GREEN='\033[0;32m'
22
+ RED='\033[0;31m'
23
+ DIM='\033[2m'
24
+ BOLD='\033[1m'
25
+ NC='\033[0m'
26
+
27
+ case "${1:-status}" in
28
+ start)
29
+ echo -e "${BOLD}Starting shared E2E infrastructure...${NC}"
30
+
31
+ # Generate compose file
32
+ bun -e "
33
+ import { generateSharedInfraYaml } from './src/docker-compose-generator';
34
+ console.log(generateSharedInfraYaml());
35
+ " > "$SHARED_COMPOSE"
36
+
37
+ docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" build
38
+ docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" up -d
39
+
40
+ echo -e "${DIM}Waiting for DNS convergence...${NC}"
41
+ for i in $(seq 1 30); do
42
+ DNS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" exec -T comcast-resolver dig @127.0.0.1 iamtheinternet.org NS +short +timeout=2 2>/dev/null || true)
43
+ if [ -n "$DNS" ]; then
44
+ echo -e "${GREEN}Shared infrastructure ready${NC}"
45
+ echo ""
46
+ docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" ps --format " {{.Name}}\t{{.Status}}"
47
+ exit 0
48
+ fi
49
+ sleep 2
50
+ done
51
+ echo -e "${RED}DNS convergence timed out${NC}"
52
+ exit 1
53
+ ;;
54
+
55
+ stop)
56
+ echo -e "${BOLD}Stopping shared E2E infrastructure...${NC}"
57
+ docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" down --volumes --remove-orphans 2>/dev/null || true
58
+ docker network prune -f > /dev/null 2>&1 || true
59
+ echo -e "${GREEN}Shared infrastructure stopped${NC}"
60
+ ;;
61
+
62
+ restart)
63
+ "$0" stop
64
+ "$0" start
65
+ ;;
66
+
67
+ status)
68
+ CONTAINERS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" ps -q 2>/dev/null || true)
69
+ if [ -n "$CONTAINERS" ]; then
70
+ echo -e "${GREEN}Shared infrastructure is running${NC}"
71
+ echo ""
72
+ docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" ps --format " {{.Name}}\t{{.Status}}"
73
+
74
+ # Health check
75
+ DNS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" exec -T comcast-resolver dig @127.0.0.1 iamtheinternet.org NS +short +timeout=2 2>/dev/null || true)
76
+ echo ""
77
+ if [ -n "$DNS" ]; then
78
+ echo -e " DNS: ${GREEN}healthy${NC}"
79
+ else
80
+ echo -e " DNS: ${RED}unhealthy${NC}"
81
+ fi
82
+ else
83
+ echo -e "${DIM}Shared infrastructure is not running${NC}"
84
+ echo " Start with: ./e2e/bin/e2e-infra start"
85
+ fi
86
+ ;;
87
+
88
+ *)
89
+ echo "Usage: $0 {start|stop|restart|status}"
90
+ exit 1
91
+ ;;
92
+ esac
package/bin/e2e-load ADDED
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ #
3
+ # Load pre-built E2E Docker images from tarball.
4
+ #
5
+ # Run after a colima restart to restore the image cache without
6
+ # rebuilding from scratch. Takes ~10s vs ~3min for a full rebuild.
7
+ #
8
+ # Usage:
9
+ # ./e2e/bin/e2e-load
10
+ #
11
+
12
+ set -e
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ E2E_DIR="$(dirname "$SCRIPT_DIR")"
16
+
17
+ BOLD='\033[1m'
18
+ GREEN='\033[0;32m'
19
+ RED='\033[0;31m'
20
+ NC='\033[0m'
21
+
22
+ TARBALL="$E2E_DIR/.docker-cache/celilo-e2e-images.tar"
23
+
24
+ if [ ! -f "$TARBALL" ]; then
25
+ echo -e "${RED}No cached images found at $TARBALL${NC}"
26
+ echo "Run ./e2e/bin/e2e-build --save first."
27
+ exit 1
28
+ fi
29
+
30
+ SIZE=$(du -h "$TARBALL" | cut -f1)
31
+ echo -e "${BOLD}Loading E2E Docker images ($SIZE)...${NC}"
32
+
33
+ START=$(date +%s)
34
+ docker load -i "$TARBALL"
35
+ END=$(date +%s)
36
+
37
+ echo ""
38
+ echo -e "${GREEN}Images loaded in $((END - START))s${NC}"
39
+ echo ""
40
+ docker images --filter "reference=celilo-e2e/*" --format " {{.Repository}}:{{.Tag}} ({{.Size}})"