@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.
- package/README.md +360 -0
- package/bin/cache-docker-images +46 -0
- package/bin/e2e-build +66 -0
- package/bin/e2e-down +67 -0
- package/bin/e2e-infra +92 -0
- package/bin/e2e-load +40 -0
- package/bin/e2e-run +354 -0
- package/bin/e2e-shell +27 -0
- package/bin/e2e-status +29 -0
- package/bin/e2e-up +160 -0
- package/config/dhcp/dnsmasq.conf +27 -0
- package/config/dns/com.zone +11 -0
- package/config/dns/iamtheinternet.org.zone +15 -0
- package/config/dns/knot-namecheap.conf +11 -0
- package/config/dns/knot-root.conf +7 -0
- package/config/dns/knot-tld.conf +11 -0
- package/config/dns/org.zone +14 -0
- package/config/dns/park-your-domain.com.zone +10 -0
- package/config/dns/root.zone +14 -0
- package/config/pebble/pebble-ca.crt +20 -0
- package/config/pebble/pebble-config.json +11 -0
- package/config/pebble/pebble-tls.crt +21 -0
- package/config/pebble/pebble-tls.key +28 -0
- package/config/proxy/generate-ca.sh +20 -0
- package/config/proxy/squid-ca.crt +22 -0
- package/config/proxy/squid-ca.pem +50 -0
- package/config/proxy/squid.conf +38 -0
- package/config/proxy/startup.sh +11 -0
- package/config/resolver/root.hints +2 -0
- package/config/resolver/unbound.conf +28 -0
- package/config/routing/apt-cache-routes.sh +21 -0
- package/config/routing/dhcp-client-setup.sh +17 -0
- package/config/routing/dns-routes.sh +12 -0
- package/config/routing/fw-ext-routes.sh +50 -0
- package/config/routing/fw-isp-routes.sh +37 -0
- package/config/routing/fw-main-routes.sh +31 -0
- package/config/routing/management-routes.sh +96 -0
- package/config/routing/namecheap-startup.sh +17 -0
- package/config/routing/pebble-startup.sh +9 -0
- package/config/routing/resolver-routes.sh +15 -0
- package/config/routing/target-routes.sh +23 -0
- package/config/routing/target-setup.service +13 -0
- package/config/routing/target-setup.sh +36 -0
- package/config/ssh/generate-keys.sh +17 -0
- package/docker/Dockerfile.apt-cache +16 -0
- package/docker/Dockerfile.authoritative-dns +15 -0
- package/docker/Dockerfile.dhcp-client +16 -0
- package/docker/Dockerfile.firewall +28 -0
- package/docker/Dockerfile.forward-proxy +28 -0
- package/docker/Dockerfile.greenwave-sim +29 -0
- package/docker/Dockerfile.management +30 -0
- package/docker/Dockerfile.namecheap-dns +25 -0
- package/docker/Dockerfile.pebble +18 -0
- package/docker/Dockerfile.resolver +20 -0
- package/docker/Dockerfile.router +33 -0
- package/docker/Dockerfile.target-machine +58 -0
- package/docker/Dockerfile.target-machine-docker +105 -0
- package/package.json +45 -0
- package/simulators/greenwave/server.ts +164 -0
- package/simulators/greenwave/state.ts +206 -0
- package/simulators/greenwave/types.ts +31 -0
- package/simulators/namecheap-ddns/server.ts +97 -0
- package/simulators/namecheap-ddns/types.ts +12 -0
- package/simulators/namecheap-ddns/zone-updater.ts +49 -0
- package/src/container-manager.ts +324 -0
- package/src/docker-compose-generator.ts +499 -0
- package/src/fixtures.ts +40 -0
- package/src/index.ts +59 -0
- package/src/network-builder.ts +91 -0
- package/src/shared-infra.ts +163 -0
- 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
|
+

|
|
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}})"
|