@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/bin/e2e-run ADDED
@@ -0,0 +1,354 @@
1
+ #!/bin/bash
2
+ #
3
+ # E2E Test Runner with Live Progress Reporting
4
+ #
5
+ # Runs each test file sequentially with real-time progress display.
6
+ # Shows sub-steps as they happen (building, deploying, configuring).
7
+ # Captures full output per test in e2e/results/<timestamp>/.
8
+ #
9
+ # Shared infrastructure (DNS, Pebble, apt-cache) is started once
10
+ # before the first test and torn down after the last test. Per-test
11
+ # containers connect to the shared networks.
12
+ #
13
+ # Usage:
14
+ # ./e2e/bin/e2e-run # run all tests
15
+ # ./e2e/bin/e2e-run smoke caddy # run tests matching these patterns
16
+ #
17
+
18
+ set -e
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ # Package root (where docker/, config/ live)
22
+ PKG_DIR="$(dirname "$SCRIPT_DIR")"
23
+ # Test directory — can be overridden via E2E_TEST_DIR env var.
24
+ # Default: look for tests/ relative to CWD, or fall back to package dir.
25
+ E2E_TEST_DIR="${E2E_TEST_DIR:-$(pwd)}"
26
+ cd "$PKG_DIR"
27
+
28
+ SHARED_PROJECT="celilo-e2e-shared"
29
+ SHARED_COMPOSE="docker-compose.shared.yml"
30
+
31
+ # Handle Ctrl+C: kill running test, clean up, and exit immediately.
32
+ INTERRUPTED=false
33
+ cleanup_and_exit() {
34
+ INTERRUPTED=true
35
+ echo ""
36
+ echo -e "${RED}Interrupted. Cleaning up...${NC}"
37
+ # Kill any running test process
38
+ [ -n "$TEST_PID" ] && kill $TEST_PID 2>/dev/null
39
+ # Remove named pipe
40
+ [ -n "$PIPE" ] && rm -f "$PIPE"
41
+ # Tear down shared infra
42
+ echo -e "${DIM} Stopping shared infrastructure...${NC}"
43
+ docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" down --volumes --remove-orphans > /dev/null 2>&1 || true
44
+ # Clean up Docker
45
+ docker network prune -f > /dev/null 2>&1 || true
46
+ echo -e "${RED}Aborted.${NC}"
47
+ exit 130
48
+ }
49
+ trap cleanup_and_exit INT TERM
50
+
51
+ # Colors
52
+ GREEN='\033[0;32m'
53
+ RED='\033[0;31m'
54
+ DIM='\033[2m'
55
+ BOLD='\033[1m'
56
+ CYAN='\033[0;36m'
57
+ NC='\033[0m'
58
+
59
+ # Create results directory (in the test project, not the package)
60
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
61
+ RESULTS_DIR="$E2E_TEST_DIR/results/$TIMESTAMP"
62
+ mkdir -p "$RESULTS_DIR"
63
+
64
+ # Discover test files from E2E_TEST_DIR
65
+ TESTS_PATH="$E2E_TEST_DIR/tests"
66
+ if [ ! -d "$TESTS_PATH" ]; then
67
+ echo -e "${RED}No tests/ directory found at $TESTS_PATH${NC}"
68
+ echo "Set E2E_TEST_DIR to the directory containing your tests/ folder."
69
+ exit 1
70
+ fi
71
+
72
+ if [ $# -gt 0 ]; then
73
+ TEST_FILES=()
74
+ for pattern in "$@"; do
75
+ for f in "$TESTS_PATH"/*${pattern}*.test.ts; do
76
+ [ -f "$f" ] && TEST_FILES+=("$f")
77
+ done
78
+ done
79
+ else
80
+ TEST_FILES=("$TESTS_PATH"/*.test.ts)
81
+ fi
82
+
83
+ TOTAL=${#TEST_FILES[@]}
84
+ PASSED=0
85
+ FAILED=0
86
+ RESULTS=()
87
+
88
+ echo ""
89
+ echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
90
+ echo -e "${BOLD}║ Celilo E2E Tests ║${NC}"
91
+ printf "${BOLD}║ %d test(s) queued — %s ║${NC}\n" "$TOTAL" "$(date +%H:%M:%S)"
92
+ echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
93
+ echo ""
94
+
95
+ SUITE_START=$(date +%s)
96
+
97
+ # ─── Shared Infrastructure Startup ────────────────────────────────────
98
+ # The first test will call ensureSharedInfra() which starts the shared
99
+ # project. But we also pre-check here so the runner can show progress.
100
+ echo -e "${BOLD}Shared Infrastructure${NC}"
101
+
102
+ # Check if shared infra is already running
103
+ SHARED_RUNNING=false
104
+ SHARED_CONTAINERS=$(docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" ps -q 2>/dev/null || true)
105
+ if [ -n "$SHARED_CONTAINERS" ]; then
106
+ # Verify DNS is healthy
107
+ DNS_CHECK=$(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)
108
+ if [ -n "$DNS_CHECK" ]; then
109
+ SHARED_RUNNING=true
110
+ echo -e " ${GREEN}✔ already running (DNS healthy)${NC}"
111
+ else
112
+ echo -e " ${DIM}DNS unhealthy, restarting...${NC}"
113
+ docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" down --volumes --remove-orphans > /dev/null 2>&1 || true
114
+ fi
115
+ fi
116
+
117
+ if [ "$SHARED_RUNNING" = false ]; then
118
+ echo -e " ${DIM}Starting DNS hierarchy, Pebble ACME, apt-cache...${NC}"
119
+ # The actual startup happens inside the first test's ensureSharedInfra()
120
+ # call. We just report it here for visibility.
121
+ fi
122
+
123
+ SHARED_END=$(date +%s)
124
+ echo ""
125
+
126
+ # Track step count per test for percentage
127
+ STEP_COUNT=0
128
+ ESTIMATED_STEPS=15
129
+
130
+ # Parse a line of test output and display as a progress step.
131
+ show_progress() {
132
+ local line="$1"
133
+ local test_start="$2"
134
+ local step=""
135
+
136
+ # Shared infra progress (from ensureSharedInfra)
137
+ if [[ "$line" == *"[e2e] Shared infrastructure ready"* ]]; then
138
+ step="✔ shared infrastructure ready"
139
+ elif [[ "$line" == *"[e2e] Building shared"* ]]; then
140
+ step="building shared infrastructure images"
141
+ elif [[ "$line" == *"[e2e] Starting shared"* ]]; then
142
+ step="starting shared infrastructure"
143
+ elif [[ "$line" == *"[e2e] Cleaning up stale shared"* ]]; then
144
+ step="cleaning up stale shared infra"
145
+
146
+ # Per-test container manager progress
147
+ elif [[ "$line" == *"[e2e] Building images"* ]]; then
148
+ step="building test images"
149
+ elif [[ "$line" == *"[e2e] Starting containers"* ]]; then
150
+ step="starting test containers"
151
+ elif [[ "$line" == *"[e2e] Cleaning up"* ]]; then
152
+ step="cleaning up stale test resources"
153
+ elif [[ "$line" == *"[e2e] Waiting for DNS"* ]]; then
154
+ step="waiting for DNS convergence"
155
+ elif [[ "$line" == *"[e2e] Verifying routing"* ]]; then
156
+ step="verifying routing"
157
+ elif [[ "$line" == *"[e2e] Waiting for target"* ]]; then
158
+ step="waiting for target machines"
159
+ elif [[ "$line" == *"[e2e] Waiting for DHCP"* ]]; then
160
+ step="waiting for DHCP"
161
+ elif [[ "$line" == *"[e2e] Initializing celilo"* ]]; then
162
+ step="initializing celilo"
163
+ elif [[ "$line" == *"[e2e] Network ready"* ]]; then
164
+ step="network ready"
165
+ elif [[ "$line" == *"[e2e] Stopping network"* ]]; then
166
+ step="stopping network"
167
+
168
+ # Test-level progress (console.log from tests)
169
+ elif [[ "$line" == *"Adding machines"* ]]; then
170
+ step="adding machines"
171
+ elif [[ "$line" == *"Importing modules"* ]]; then
172
+ step="importing modules"
173
+ elif [[ "$line" == *"Configuring modules"* ]]; then
174
+ step="configuring modules"
175
+ elif [[ "$line" == *"Prerequisites ready"* ]]; then
176
+ step="prerequisites ready"
177
+
178
+ # Celilo CLI: "Deploying module: namecheap"
179
+ elif [[ "$line" == *"Deploying module:"* ]]; then
180
+ local module=$(echo "$line" | sed -n 's/.*Deploying module: \([^ ]*\).*/\1/p')
181
+ if [ -n "$module" ]; then
182
+ step="deploying $module"
183
+ else
184
+ step="deploying module"
185
+ fi
186
+ # Celilo CLI: "Deploying software..."
187
+ elif [[ "$line" == *"Deploying software"* ]]; then
188
+ step="running Ansible"
189
+
190
+ # Test console.log: "[45s] Deploying namecheap..."
191
+ elif [[ "$line" =~ Deploying\ ([a-z_-]+) ]]; then
192
+ step="deploying ${BASH_REMATCH[1]}"
193
+
194
+ # Celilo CLI: "✓ Module 'namecheap' deployed successfully"
195
+ elif [[ "$line" == *"deployed successfully"* ]]; then
196
+ local module=$(echo "$line" | sed -n "s/.*Module '\([^']*\)' deployed.*/\1/p")
197
+ step="✔ ${module:-module} deployed"
198
+
199
+ # Celilo CLI: "Successfully imported module: namecheap"
200
+ elif [[ "$line" == *"Successfully imported module:"* ]]; then
201
+ local module=$(echo "$line" | sed -n 's/.*imported module: \([^ ]*\).*/\1/p')
202
+ step="✔ ${module:-module} imported"
203
+
204
+ # Celilo CLI: "Infrastructure selected: machine"
205
+ elif [[ "$line" == *"Infrastructure selected"* ]]; then
206
+ step="infrastructure selected"
207
+ # Celilo CLI: "Terraform"
208
+ elif [[ "$line" == *"Terraform skipped"* ]]; then
209
+ step="terraform skipped (existing machine)"
210
+ elif [[ "$line" == *"terraform init"* ]] || [[ "$line" == *"Terraform init"* ]]; then
211
+ step="terraform init"
212
+ elif [[ "$line" == *"terraform apply"* ]] || [[ "$line" == *"Terraform apply"* ]]; then
213
+ step="terraform apply"
214
+
215
+ # Ansible health retries
216
+ elif [[ "$line" == *"RETRYING"* ]] && [[ "$line" == *"healthy"* ]]; then
217
+ local retries=$(echo "$line" | sed -n 's/.*(\([0-9]*\) retries left).*/\1/p')
218
+ step="waiting for health check ($retries retries left)"
219
+
220
+ # Skip noise
221
+ elif [[ "$line" == *"Set config for"* ]]; then return
222
+ elif [[ "$line" == *"Set secret for"* ]]; then return
223
+ elif [[ "$line" == *"Auto-generated"* ]]; then return
224
+ elif [[ "$line" == *"Warning: Permanently added"* ]]; then return
225
+
226
+ # Error detection
227
+ elif [[ "$line" == *"Error:"* ]] && [[ "$line" != *"vitest"* ]] && [[ "$line" != *"bun test"* ]] && [[ "$line" != *"ipv4: Address already"* ]]; then
228
+ local err=$(echo "$line" | sed 's/.*Error: //' | cut -c1-70)
229
+ step="✗ $err"
230
+ fi
231
+
232
+ if [ -n "$step" ]; then
233
+ STEP_COUNT=$((STEP_COUNT + 1))
234
+ # Update estimate upward if we're exceeding it
235
+ if [ $STEP_COUNT -gt $ESTIMATED_STEPS ]; then
236
+ ESTIMATED_STEPS=$((STEP_COUNT + 5))
237
+ fi
238
+ local pct=$(( (STEP_COUNT * 100) / ESTIMATED_STEPS ))
239
+ [ $pct -gt 99 ] && pct=99 # Don't show 100% until actually done
240
+ local now=$(date +%s)
241
+ local elapsed=$((now - test_start))
242
+ printf " ${DIM}%3ds${NC} ${CYAN}%2d%%${NC} %s\n" "$elapsed" "$pct" "$step"
243
+ fi
244
+ }
245
+
246
+ for ((i=0; i<TOTAL; i++)); do
247
+ FILE="${TEST_FILES[$i]}"
248
+ NAME=$(basename "$FILE" .test.ts)
249
+ TEST_DIR="$RESULTS_DIR/$NAME"
250
+ mkdir -p "$TEST_DIR"
251
+
252
+ # Percentage complete (based on test count, not time)
253
+ PCT=$(( (i * 100) / TOTAL ))
254
+ ELAPSED_TOTAL=$(($(date +%s) - SUITE_START))
255
+ if [ $i -gt 0 ]; then
256
+ AVG_PER_TEST=$((ELAPSED_TOTAL / i))
257
+ REMAINING=$(( (TOTAL - i) * AVG_PER_TEST ))
258
+ ETA_MIN=$((REMAINING / 60))
259
+ ETA_SEC=$((REMAINING % 60))
260
+ ETA_STR=" — ~${ETA_MIN}m ${ETA_SEC}s remaining"
261
+ else
262
+ ETA_STR=""
263
+ fi
264
+
265
+ echo -e "${BOLD}[${i}/${TOTAL}] ${CYAN}${PCT}%${NC}${BOLD} ${NAME}${NC}${DIM}${ETA_STR}${NC}"
266
+
267
+ STEP_COUNT=0
268
+ TEST_START=$(date +%s)
269
+ PIPE=$(mktemp -u)
270
+ mkfifo "$PIPE"
271
+
272
+ # Background: run bun test, merge stdout+stderr, tee to log + pipe
273
+ (bun test --timeout 300000 "$FILE" 2>&1 | tee "$TEST_DIR/output.log" > "$PIPE") &
274
+ TEST_PID=$!
275
+
276
+ # Foreground: read pipe line-by-line and show progress
277
+ while IFS= read -r line; do
278
+ show_progress "$line" "$TEST_START"
279
+ done < "$PIPE"
280
+
281
+ # Wait for test to finish and get exit code
282
+ wait $TEST_PID 2>/dev/null
283
+ EXIT_CODE=$?
284
+ rm -f "$PIPE"
285
+
286
+ TEST_END=$(date +%s)
287
+ DURATION=$((TEST_END - TEST_START))
288
+
289
+ if [ $EXIT_CODE -eq 0 ]; then
290
+ echo -e " ${GREEN}✔ passed (${DURATION}s)${NC}"
291
+ PASSED=$((PASSED + 1))
292
+ RESULTS+=("✓|$NAME|${DURATION}s")
293
+ echo '{"status":"pass","duration":'$DURATION'}' > "$TEST_DIR/result.json"
294
+ else
295
+ echo -e " ${RED}✗ failed (${DURATION}s)${NC}"
296
+ FAILED=$((FAILED + 1))
297
+ ERROR=$(grep -E "Error:|expected.*to be|Timeout" "$TEST_DIR/output.log" 2>/dev/null | grep -v "vitest\|bun test" | head -1 | sed 's/.*Error: //' | cut -c1-80)
298
+ RESULTS+=("✗|$NAME|${DURATION}s|$ERROR")
299
+ echo '{"status":"fail","duration":'$DURATION',"error":"'"${ERROR//\"/\\\"}"'"}' > "$TEST_DIR/result.json"
300
+ echo -e " ${RED} └─ ${ERROR}${NC}"
301
+ fi
302
+ echo ""
303
+
304
+ # Clean up per-test Docker resources between tests (not shared infra)
305
+ docker network prune -f > /dev/null 2>&1 || true
306
+ done
307
+
308
+ # ─── Shared Infrastructure Teardown ───────────────────────────────────
309
+ echo -e "${DIM}Stopping shared infrastructure...${NC}"
310
+ docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" down --volumes --remove-orphans > /dev/null 2>&1 || true
311
+ docker network prune -f > /dev/null 2>&1 || true
312
+
313
+ SUITE_END=$(date +%s)
314
+ SUITE_DURATION=$((SUITE_END - SUITE_START))
315
+ SUITE_MINUTES=$((SUITE_DURATION / 60))
316
+ SUITE_SECONDS=$((SUITE_DURATION % 60))
317
+
318
+ echo ""
319
+ echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}"
320
+ printf "${BOLD}║ Results: ${GREEN}%d passed${NC}${BOLD}, ${RED}%d failed${NC}${BOLD} — %dm %ds total ║${NC}\n" "$PASSED" "$FAILED" "$SUITE_MINUTES" "$SUITE_SECONDS"
321
+ echo -e "${BOLD}╠══════════════════════════════════════════════════════════════╣${NC}"
322
+
323
+ for result in "${RESULTS[@]}"; do
324
+ IFS='|' read -r icon name duration error <<< "$result"
325
+ if [ "$icon" = "✓" ]; then
326
+ printf "${BOLD}║${NC} ${GREEN}%s${NC} %-28s ${DIM}%6s${NC}\n" "$icon" "$name" "$duration"
327
+ else
328
+ printf "${BOLD}║${NC} ${RED}%s${NC} %-28s ${DIM}%6s${NC}\n" "$icon" "$name" "$duration"
329
+ if [ -n "$error" ]; then
330
+ printf "${BOLD}║${NC} ${RED}└─ %s${NC}\n" "$error"
331
+ fi
332
+ fi
333
+ done
334
+
335
+ echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}"
336
+ echo ""
337
+ echo -e "Full logs: ${DIM}$RESULTS_DIR${NC}"
338
+
339
+ # Write summary
340
+ cat > "$RESULTS_DIR/summary.json" <<EOF
341
+ {
342
+ "timestamp": "$TIMESTAMP",
343
+ "total": $TOTAL,
344
+ "passed": $PASSED,
345
+ "failed": $FAILED,
346
+ "duration": $SUITE_DURATION
347
+ }
348
+ EOF
349
+
350
+ # Add results dir to gitignore if not already there
351
+ grep -q "^results/" "$E2E_TEST_DIR/.gitignore" 2>/dev/null || echo "results/" >> "$E2E_TEST_DIR/.gitignore"
352
+
353
+ # Exit with failure if any tests failed
354
+ [ $FAILED -eq 0 ]
package/bin/e2e-shell ADDED
@@ -0,0 +1,27 @@
1
+ #!/bin/bash
2
+ # Connect to a running E2E container.
3
+ #
4
+ # Usage:
5
+ # ./bin/e2e-shell # Shell into management machine
6
+ # ./bin/e2e-shell caddy # Shell into caddy container
7
+ # ./bin/e2e-shell fw-main # Shell into fw-main
8
+ # ./bin/e2e-shell namecheap-dns # Shell into namecheap-dns
9
+
10
+ set -e
11
+ PROJECT_NAME="conductor-e2e-interactive"
12
+ CONTAINER="${1:-management}"
13
+
14
+ # Check if network is running
15
+ if ! docker compose -p "$PROJECT_NAME" ps --status running 2>/dev/null | grep -q "$CONTAINER"; then
16
+ echo "Container '$CONTAINER' is not running."
17
+ echo "Available containers:"
18
+ docker compose -p "$PROJECT_NAME" ps --format ' {{.Name}} ({{.Status}})' 2>/dev/null || echo " No E2E network running. Start with: ./bin/e2e-up"
19
+ exit 1
20
+ fi
21
+
22
+ # Use zsh for management (has conductor aliases + completion), bash for others
23
+ if [ "$CONTAINER" = "management" ]; then
24
+ docker compose -p "$PROJECT_NAME" exec "$CONTAINER" zsh -li
25
+ else
26
+ docker compose -p "$PROJECT_NAME" exec "$CONTAINER" bash
27
+ fi
package/bin/e2e-status ADDED
@@ -0,0 +1,29 @@
1
+ #!/bin/bash
2
+ # Show status of the E2E test network.
3
+
4
+ PROJECT_NAME="conductor-e2e-interactive"
5
+
6
+ echo "=== Containers ==="
7
+ docker compose -p "$PROJECT_NAME" ps 2>/dev/null || { echo "No E2E network running."; exit 0; }
8
+
9
+ echo ""
10
+ echo "=== DNS Check ==="
11
+ echo -n " iamtheinternet.org NS: "
12
+ docker compose -p "$PROJECT_NAME" exec -T comcast-resolver dig @127.0.0.1 iamtheinternet.org NS +short +timeout=2 2>/dev/null || echo "(failed)"
13
+
14
+ echo -n " www.iamtheinternet.org A: "
15
+ docker compose -p "$PROJECT_NAME" exec -T comcast-resolver dig @127.0.0.1 www.iamtheinternet.org A +short +timeout=2 2>/dev/null || echo "(not set)"
16
+
17
+ echo ""
18
+ echo "=== Connectivity (from management) ==="
19
+ for target in "fw-main:192.168.0.254" "fw-isp:192.168.0.1" "comcast-resolver:100.100.0.1" "root-dns:100.64.0.53" "namecheap-dns:100.64.0.55"; do
20
+ name="${target%%:*}"
21
+ ip="${target##*:}"
22
+ echo -n " $name ($ip): "
23
+ docker compose -p "$PROJECT_NAME" exec -T management ping -c1 -W2 "$ip" >/dev/null 2>&1 && echo "OK" || echo "FAIL"
24
+ done
25
+
26
+ echo ""
27
+ echo "=== DDNS Simulator ==="
28
+ echo -n " Status: "
29
+ docker compose -p "$PROJECT_NAME" exec -T namecheap-dns curl -s http://localhost:8080/status 2>/dev/null || echo "(not running)"
package/bin/e2e-up ADDED
@@ -0,0 +1,160 @@
1
+ #!/bin/bash
2
+ # Start the E2E test network interactively.
3
+ #
4
+ # Usage:
5
+ # ./bin/e2e-up # Infrastructure only
6
+ # ./bin/e2e-up --caddy # + caddy machine in dmz
7
+ # ./bin/e2e-up --full-stack # + caddy, idp, db
8
+ # ./bin/e2e-up --custom '{"dmz":{"caddy":"10.0.10.10","web":"10.0.10.20"}}'
9
+ #
10
+ # After startup, drops you into a shell on the management machine.
11
+ # Use `exit` to leave the shell (network stays running).
12
+ # Use `./bin/e2e-down` to tear everything down.
13
+
14
+ set -e
15
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
16
+ cd "$SCRIPT_DIR"
17
+
18
+ PROJECT_NAME="conductor-e2e-interactive"
19
+
20
+ # Parse args
21
+ PRESET="infrastructure"
22
+ CUSTOM_JSON=""
23
+ while [[ $# -gt 0 ]]; do
24
+ case "$1" in
25
+ --caddy) PRESET="caddy"; shift ;;
26
+ --full-stack) PRESET="full-stack"; shift ;;
27
+ --custom) shift; CUSTOM_JSON="$1"; shift ;;
28
+ --help|-h)
29
+ echo "Usage: e2e-up [--caddy|--full-stack|--custom '<json>']"
30
+ echo ""
31
+ echo "Presets:"
32
+ echo " (default) Infrastructure only (management, firewalls, DNS, Pebble)"
33
+ echo " --caddy + caddy machine at 10.0.10.10 in dmz"
34
+ echo " --full-stack + caddy (dmz), idp (app), db (secure)"
35
+ echo " --custom JSON Custom machine spec, e.g. '{\"dmz\":{\"caddy\":\"10.0.10.10\"}}'"
36
+ echo ""
37
+ echo "After startup, you'll be dropped into the management machine shell."
38
+ echo "Use ./bin/e2e-down to tear down, or ./bin/e2e-shell to reconnect."
39
+ exit 0
40
+ ;;
41
+ *) shift ;;
42
+ esac
43
+ done
44
+
45
+ # Generate docker-compose.yml
46
+ export PRESET CUSTOM_JSON
47
+ echo "Generating docker-compose.yml (preset: $PRESET)..."
48
+ bun -e "
49
+ import { generateComposeYaml } from './src/docker-compose-generator';
50
+
51
+ const presets = {
52
+ 'infrastructure': {
53
+ topology: 'default',
54
+ dmzMachines: [],
55
+ appMachines: [],
56
+ secureMachines: [],
57
+ domain: 'iamtheinternet.org',
58
+ ddnsPassword: 'test123',
59
+ verifyRouting: false,
60
+ },
61
+ 'caddy': {
62
+ topology: 'default',
63
+ dmzMachines: [{ name: 'caddy', ip: '10.0.10.10', zone: 'dmz' }],
64
+ appMachines: [],
65
+ secureMachines: [],
66
+ domain: 'iamtheinternet.org',
67
+ ddnsPassword: 'test123',
68
+ verifyRouting: false,
69
+ },
70
+ 'full-stack': {
71
+ topology: 'default',
72
+ dmzMachines: [{ name: 'caddy', ip: '10.0.10.10', zone: 'dmz' }],
73
+ appMachines: [{ name: 'idp', ip: '10.0.20.100', zone: 'app' }],
74
+ secureMachines: [{ name: 'db', ip: '10.0.30.50', zone: 'secure' }],
75
+ domain: 'iamtheinternet.org',
76
+ ddnsPassword: 'test123',
77
+ verifyRouting: false,
78
+ },
79
+ };
80
+
81
+ const customJson = process.env.CUSTOM_JSON;
82
+ let config;
83
+ if (customJson) {
84
+ const spec = JSON.parse(customJson);
85
+ config = { ...presets['infrastructure'] };
86
+ for (const [zone, machines] of Object.entries(spec)) {
87
+ const key = zone + 'Machines';
88
+ if (key in config) {
89
+ config[key] = Object.entries(machines).map(([name, ip]) => ({ name, ip, zone }));
90
+ }
91
+ }
92
+ } else {
93
+ config = presets[process.env.PRESET || 'infrastructure'];
94
+ }
95
+
96
+ console.log(generateComposeYaml(config));
97
+ " > docker-compose.yml
98
+
99
+ # Build and start
100
+ echo "Building images (this may take a while on first run)..."
101
+ docker compose -p "$PROJECT_NAME" build
102
+
103
+ echo "Starting containers..."
104
+ docker compose -p "$PROJECT_NAME" up -d
105
+
106
+ echo ""
107
+ echo "Waiting for management machine to initialize..."
108
+ for i in $(seq 1 15); do
109
+ if docker compose -p "$PROJECT_NAME" exec -T management test -f /root/.zshrc 2>/dev/null; then
110
+ break
111
+ fi
112
+ sleep 1
113
+ done
114
+
115
+ echo "Waiting for DNS convergence..."
116
+ for i in $(seq 1 30); do
117
+ if docker compose -p "$PROJECT_NAME" exec -T comcast-resolver dig @127.0.0.1 iamtheinternet.org NS +short +timeout=2 2>/dev/null | grep -q '.'; then
118
+ echo "DNS is ready!"
119
+ break
120
+ fi
121
+ if [ "$i" = "30" ]; then
122
+ echo "WARNING: DNS did not converge after 60s. Continuing anyway."
123
+ fi
124
+ sleep 2
125
+ done
126
+
127
+ echo ""
128
+ echo "================================================"
129
+ echo " E2E Network is running!"
130
+ echo " Project: $PROJECT_NAME"
131
+ echo "================================================"
132
+ echo ""
133
+ echo "Networks:"
134
+ echo " internal: 192.168.0.0/24"
135
+ echo " dmz: 10.0.10.0/24"
136
+ echo " app: 10.0.20.0/24"
137
+ echo " secure: 10.0.30.0/24"
138
+ echo " isp-external: 100.100.0.0/24"
139
+ echo " internet-external: 100.64.0.0/24"
140
+ echo ""
141
+ echo "Key machines:"
142
+ echo " management: 192.168.0.100 (conductor CLI)"
143
+ echo " fw-main: 192.168.0.254 (iptables firewall)"
144
+ echo " fw-isp: 192.168.0.1 (greenwave router sim)"
145
+ echo " comcast-resolver: 100.100.0.1 (DNS resolver)"
146
+ echo " namecheap-dns: 100.64.0.55 (DDNS simulator)"
147
+ echo " letsencrypt: 100.64.0.100 (Pebble ACME)"
148
+ echo " apt-cache: 100.64.0.2 (apt proxy → real internet)"
149
+ echo ""
150
+ echo "Commands:"
151
+ echo " ./bin/e2e-shell Reconnect to management"
152
+ echo " ./bin/e2e-shell <container> Shell into any container"
153
+ echo " ./bin/e2e-down Tear everything down"
154
+ echo " docker compose -p $PROJECT_NAME ps List containers"
155
+ echo ""
156
+ echo "Dropping you into the management machine..."
157
+ echo "(use 'conductor <cmd>' or 'c <cmd>' — tab completion works)"
158
+ echo ""
159
+
160
+ docker compose -p "$PROJECT_NAME" exec management zsh -li
@@ -0,0 +1,27 @@
1
+ # dnsmasq DHCP server for e2e internal network
2
+ # Runs on the greenwave simulator (fw-isp) container
3
+
4
+ # Only DHCP, no DNS (technitium handles DNS)
5
+ port=0
6
+
7
+ # Listen on internal interface only
8
+ interface=eth0
9
+ bind-interfaces
10
+
11
+ # DHCP range: .200-.220 (small range for test clients)
12
+ dhcp-range=192.168.0.200,192.168.0.220,255.255.255.0,1h
13
+
14
+ # Default gateway (the greenwave router itself)
15
+ dhcp-option=option:router,192.168.0.1
16
+
17
+ # DNS server (initially the router, updated by greenwave API)
18
+ dhcp-option=option:dns-server,192.168.0.1
19
+
20
+ # Domain name / search domain
21
+ # dhcp-option=option:domain-name,iamtheinternet.org
22
+
23
+ # Short lease time for testing
24
+ dhcp-lease-max=20
25
+
26
+ # Log DHCP transactions
27
+ log-dhcp
@@ -0,0 +1,11 @@
1
+ $ORIGIN com.
2
+ $TTL 86400
3
+
4
+ @ IN SOA tld-dns.lab. admin.lab. (
5
+ 2024010101 1800 900 604800 86400
6
+ )
7
+
8
+ com. IN NS tld-dns.lab.
9
+
10
+ park-your-domain IN NS ns1.park-your-domain.com.
11
+ ns1.park-your-domain.com. IN A 100.64.0.55
@@ -0,0 +1,15 @@
1
+ $ORIGIN iamtheinternet.org.
2
+ $TTL 300
3
+
4
+ @ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
5
+ 2024010686 300 60 604800 300
6
+ )
7
+
8
+ @ IN NS ns1.iamtheinternet.org.
9
+ ns1 IN A 100.64.0.55
10
+ @ IN A 0.0.0.0
11
+ caddy IN A 100.100.0.100
12
+ www IN A 100.100.0.100
13
+ deathstar IN A 100.100.0.100
14
+ test IN A 0.0.0.0
15
+ auth IN A 100.100.0.100
@@ -0,0 +1,11 @@
1
+ server:
2
+ listen: 0.0.0.0@53
3
+
4
+ zone:
5
+ - domain: "iamtheinternet.org"
6
+ file: "/config/iamtheinternet.org.zone"
7
+ storage: "/var/lib/knot"
8
+
9
+ - domain: "park-your-domain.com"
10
+ file: "/config/park-your-domain.com.zone"
11
+ storage: "/var/lib/knot"
@@ -0,0 +1,7 @@
1
+ server:
2
+ listen: 0.0.0.0@53
3
+
4
+ zone:
5
+ - domain: "."
6
+ file: "/config/root.zone"
7
+ storage: "/var/lib/knot"
@@ -0,0 +1,11 @@
1
+ server:
2
+ listen: 0.0.0.0@53
3
+
4
+ zone:
5
+ - domain: "com"
6
+ file: "/config/com.zone"
7
+ storage: "/var/lib/knot"
8
+
9
+ - domain: "org"
10
+ file: "/config/org.zone"
11
+ storage: "/var/lib/knot"
@@ -0,0 +1,14 @@
1
+ $ORIGIN org.
2
+ $TTL 86400
3
+
4
+ @ IN SOA tld-dns.lab. admin.lab. (
5
+ 2024010101 1800 900 604800 86400
6
+ )
7
+
8
+ org. IN NS tld-dns.lab.
9
+
10
+ iamtheinternet IN NS ns1.iamtheinternet.org.
11
+ ns1.iamtheinternet.org. IN A 100.64.0.55
12
+
13
+ ; Note: Let's Encrypt ACME endpoints are handled by unbound local-data
14
+ ; (resolves acme-v02.api.letsencrypt.org → 100.64.0.100 directly)