@celilo/e2e 0.7.13 → 0.7.15
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/config/dns/knot-namecheap.conf +2 -2
- package/config/dns/net.zone +2 -2
- package/config/dns/{lunacycle.net.zone → templates/example.net.zone} +3 -3
- package/config/resolver/unbound.conf +2 -2
- package/config/routing/observer-setup.sh +60 -0
- package/config/routing/resolver-internal-routes.sh +2 -2
- package/docker/Dockerfile.observer +26 -0
- package/package.json +2 -2
- package/src/cli/command-registry.ts +9 -3
- package/src/cli/completion.ts +12 -2
- package/src/cli/index.ts +105 -1
- package/src/container-manager.ts +4 -4
- package/src/docker-compose-generator.ts +19 -4
- package/src/index.ts +28 -1
- package/src/network-builder.ts +18 -1
- package/src/observer.test.ts +155 -0
- package/src/observer.ts +270 -0
- package/src/runner.ts +18 -0
- package/src/types.ts +58 -0
- package/src/vantage-helpers.ts +78 -0
- package/src/vantage.test.ts +200 -0
- package/src/vantage.ts +266 -0
- package/src/zone-classifier.test.ts +82 -0
- package/src/zone-classifier.ts +92 -0
- package/config/dns/iamtheinternet.org.zone +0 -16
- package/config/dns/templates/lunacycle.net.zone +0 -16
|
@@ -10,8 +10,8 @@ zone:
|
|
|
10
10
|
file: "/config/celilo.computer.zone"
|
|
11
11
|
storage: "/var/lib/knot"
|
|
12
12
|
|
|
13
|
-
- domain: "
|
|
14
|
-
file: "/config/
|
|
13
|
+
- domain: "example.net"
|
|
14
|
+
file: "/config/example.net.zone"
|
|
15
15
|
storage: "/var/lib/knot"
|
|
16
16
|
|
|
17
17
|
- domain: "park-your-domain.com"
|
package/config/dns/net.zone
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
$ORIGIN
|
|
1
|
+
$ORIGIN example.net.
|
|
2
2
|
$TTL 300
|
|
3
3
|
|
|
4
|
-
@ IN SOA ns1.
|
|
4
|
+
@ IN SOA ns1.example.net. admin.example.net. (
|
|
5
5
|
2024020108 300 60 604800 300
|
|
6
6
|
)
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ $TTL 300
|
|
|
10
10
|
; private IPs are unreachable from outside. All public-facing names
|
|
11
11
|
; resolve to the firewall's external IP (100.100.0.100), which DNATs
|
|
12
12
|
; inbound to the right internal host.
|
|
13
|
-
@ IN NS ns1.
|
|
13
|
+
@ IN NS ns1.example.net.
|
|
14
14
|
ns1 IN A 100.64.0.55
|
|
15
15
|
@ IN A 100.100.0.100
|
|
16
16
|
www IN A 100.100.0.100
|
|
@@ -34,11 +34,11 @@ stub-zone:
|
|
|
34
34
|
stub-addr: 100.64.0.55
|
|
35
35
|
|
|
36
36
|
stub-zone:
|
|
37
|
-
name: "
|
|
37
|
+
name: "example.net."
|
|
38
38
|
stub-addr: 100.64.0.55
|
|
39
39
|
|
|
40
40
|
# celilo.computer hosts the website + install.sh (served by the website-sim).
|
|
41
|
-
# Without this stub it falls through to the real internet (
|
|
41
|
+
# Without this stub it falls through to the real internet (203.0.113.11), so the
|
|
42
42
|
# website-sim's ACME challenge can't be validated in-topology and the bake's
|
|
43
43
|
# `curl https://celilo.computer/install.sh` never sees a cert. Covers
|
|
44
44
|
# www.celilo.computer too. (ISS-0063)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Observer vantage setup (ISS-0117).
|
|
3
|
+
#
|
|
4
|
+
# Installs the routing profile that makes this container a FAITHFUL vantage — the
|
|
5
|
+
# load-bearing part of the framework. A vantage's correctness is its routing table:
|
|
6
|
+
# an internalDevice must reach a fronted service only via the firewall natIp, never a
|
|
7
|
+
# segmented-zone container IP directly. Driven by env from the compose generator:
|
|
8
|
+
# OBSERVER_GATEWAY - default route (the home router for internalDevice, the zone
|
|
9
|
+
# firewall for dmz/app/secure, fw-ext for publicInternet)
|
|
10
|
+
# OBSERVER_INTERZONE - "1" to add the management-style explicit inter-zone routes
|
|
11
|
+
# (the all-VLAN trap). "0" for every faithful consumer vantage.
|
|
12
|
+
# OBSERVER_RESOLVERS - space-separated nameservers for /etc/resolv.conf, in order
|
|
13
|
+
set -u
|
|
14
|
+
|
|
15
|
+
GATEWAY="${OBSERVER_GATEWAY:?OBSERVER_GATEWAY is required}"
|
|
16
|
+
INTERZONE="${OBSERVER_INTERZONE:-0}"
|
|
17
|
+
RESOLVERS="${OBSERVER_RESOLVERS:-100.100.0.1}"
|
|
18
|
+
|
|
19
|
+
echo "Observer starting: gateway=${GATEWAY} interzone=${INTERZONE} resolvers='${RESOLVERS}'"
|
|
20
|
+
|
|
21
|
+
# --- Default route ---
|
|
22
|
+
# Docker seeds a default via the network's own gateway; replace it with the gateway a
|
|
23
|
+
# real device at this location would use.
|
|
24
|
+
ip route del default 2>/dev/null || true
|
|
25
|
+
ip route add default via "${GATEWAY}"
|
|
26
|
+
|
|
27
|
+
# --- Inter-zone routes (the management trap; only when explicitly requested) ---
|
|
28
|
+
# A real consumer vantage NEVER has these — that is the whole point. Present only so a
|
|
29
|
+
# `management`-style all-VLAN box can be modelled deliberately.
|
|
30
|
+
if [ "${INTERZONE}" = "1" ]; then
|
|
31
|
+
ip route add 10.0.10.0/24 via 192.168.0.254 2>/dev/null || true # dmz via fw-main
|
|
32
|
+
ip route add 10.0.20.0/24 via 192.168.0.254 2>/dev/null || true # app via fw-main
|
|
33
|
+
ip route add 10.0.30.0/24 via 192.168.0.254 2>/dev/null || true # secure via fw-main
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# --- Resolver ---
|
|
37
|
+
: > /etc/resolv.conf
|
|
38
|
+
for ns in ${RESOLVERS}; do
|
|
39
|
+
echo "nameserver ${ns}" >> /etc/resolv.conf
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
# --- Pebble runtime ACME root CA ---
|
|
43
|
+
# Pebble mints a fresh root each startup, so the build-time CA can't verify certs Caddy
|
|
44
|
+
# obtained at runtime. Fetch the live root so curl/openssl can validate real chains —
|
|
45
|
+
# the no-self-signed / CA-provenance assertions depend on a correct trust store.
|
|
46
|
+
echo "Fetching Pebble ACME root CA..."
|
|
47
|
+
for _ in $(seq 1 30); do
|
|
48
|
+
if curl -sk https://100.64.0.100:15000/roots/0 -o /usr/local/share/ca-certificates/pebble-acme-root.crt 2>/dev/null; then
|
|
49
|
+
if [ -s /usr/local/share/ca-certificates/pebble-acme-root.crt ]; then
|
|
50
|
+
update-ca-certificates 2>/dev/null
|
|
51
|
+
echo "Pebble ACME root CA installed"
|
|
52
|
+
break
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
sleep 2
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
echo "Observer ready: $(hostname)"
|
|
59
|
+
ip route
|
|
60
|
+
sleep infinity
|
|
@@ -9,9 +9,9 @@ CADDY_IP="${CADDY_IP:-10.0.10.10}"
|
|
|
9
9
|
# DOMAINS: comma-separated list of e2e test domains that should be
|
|
10
10
|
# split-horizon redirected to caddy. Default covers the canonical
|
|
11
11
|
# (iamtheinternet.org) plus the cross-domain test's second domain
|
|
12
|
-
# (
|
|
12
|
+
# (example.net). Tests can override via DEFAULT_GATEWAY/DOMAINS env
|
|
13
13
|
# vars in docker-compose.
|
|
14
|
-
DOMAINS="${DOMAINS:-iamtheinternet.org,
|
|
14
|
+
DOMAINS="${DOMAINS:-iamtheinternet.org,example.net}"
|
|
15
15
|
|
|
16
16
|
# Routing — reach comcast-resolver (100.100.0.1) via fw-main or fw-isp.
|
|
17
17
|
# DEFAULT_GATEWAY is set by docker-compose from the topology config.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Observer — a passive vantage-point spy (ISS-0117).
|
|
2
|
+
#
|
|
3
|
+
# Carries the full probe toolbox (dig, curl, openssl, nc, ping, ip) so a single image
|
|
4
|
+
# can be injected at any network location and assert resolve/reach/TLS from that seat.
|
|
5
|
+
# It runs NO services and deploys NO modules — it never perturbs the system under test.
|
|
6
|
+
# Its faithfulness as a vantage comes from the routing profile installed at startup
|
|
7
|
+
# (see config/routing/observer-setup.sh), driven by the OBSERVER_* env the compose
|
|
8
|
+
# generator sets per vantage.
|
|
9
|
+
FROM ubuntu:22.04
|
|
10
|
+
|
|
11
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
12
|
+
|
|
13
|
+
RUN apt-get update && apt-get install -y \
|
|
14
|
+
dnsutils \
|
|
15
|
+
curl \
|
|
16
|
+
openssl \
|
|
17
|
+
netcat-openbsd \
|
|
18
|
+
iputils-ping \
|
|
19
|
+
iproute2 \
|
|
20
|
+
ca-certificates \
|
|
21
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
22
|
+
|
|
23
|
+
COPY config/routing/observer-setup.sh /startup.sh
|
|
24
|
+
RUN chmod +x /startup.sh
|
|
25
|
+
|
|
26
|
+
CMD ["/startup.sh"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@celilo/cli-display": "^0.1.9",
|
|
40
|
-
"@celilo/event-bus": "^0.1.
|
|
40
|
+
"@celilo/event-bus": "^0.1.7",
|
|
41
41
|
"yaml": "^2.8.0",
|
|
42
42
|
"zod": "^3.24.1"
|
|
43
43
|
},
|
|
@@ -23,15 +23,21 @@ export interface FlagDef {
|
|
|
23
23
|
export const COMMANDS: CommandDef[] = [
|
|
24
24
|
{
|
|
25
25
|
name: 'run',
|
|
26
|
-
description: 'Run e2e tests — pass a module path or pattern, or --all-modules',
|
|
26
|
+
description: 'Run e2e tests — pass a module path or pattern, or --all / --all-modules',
|
|
27
27
|
flags: [
|
|
28
|
+
{ name: '--all', description: 'Run the full regression: every module suite + top-level e2e/tests/' },
|
|
29
|
+
{ name: '--all-modules', description: 'Run e2e tests for all modules with e2e/ directories' },
|
|
28
30
|
{ name: '--keep', description: 'Keep network running after tests' },
|
|
29
31
|
{ name: '--reuse', description: 'Reuse existing network if running' },
|
|
30
32
|
{ name: '--live', description: 'Use live (non-simulated) internet' },
|
|
31
33
|
{ name: '--published', description: 'Use published .netapp packages' },
|
|
32
|
-
{ name: '--all-modules', description: 'Run e2e tests for all modules with e2e/ directories' },
|
|
33
34
|
],
|
|
34
35
|
},
|
|
36
|
+
{
|
|
37
|
+
name: 'list',
|
|
38
|
+
description: 'List every discoverable Docker e2e test (module + top-level)',
|
|
39
|
+
flags: [{ name: '--paths', description: 'Also show each test file path' }],
|
|
40
|
+
},
|
|
35
41
|
{
|
|
36
42
|
name: 'up',
|
|
37
43
|
description: 'Start interactive test network',
|
|
@@ -93,7 +99,7 @@ export const COMMANDS: CommandDef[] = [
|
|
|
93
99
|
|
|
94
100
|
export const UP_PRESETS = ['--caddy', '--full-stack', '--infrastructure'];
|
|
95
101
|
|
|
96
|
-
export const RUN_FLAGS = ['--
|
|
102
|
+
export const RUN_FLAGS = ['--all', '--all-modules', '--keep', '--reuse', '--live', '--published'];
|
|
97
103
|
|
|
98
104
|
export const DOWN_FLAGS = ['--keep', '--all'];
|
|
99
105
|
|
package/src/cli/completion.ts
CHANGED
|
@@ -56,6 +56,11 @@ export function getCompletions(words: string[], current: number): string[] {
|
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
if (command === 'list' && currentIndex >= 1) {
|
|
60
|
+
const used = new Set(args.slice(1));
|
|
61
|
+
return filterSuggestions(['--paths'].filter((f) => !used.has(f)), args[currentIndex] || '');
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
if (command === 'completion' && currentIndex === 1) {
|
|
60
65
|
return filterSuggestions(['zsh', 'bash'], args[1] || '');
|
|
61
66
|
}
|
|
@@ -108,13 +113,17 @@ _cele2e() {
|
|
|
108
113
|
case $line[1] in
|
|
109
114
|
run)
|
|
110
115
|
_arguments \\
|
|
116
|
+
'--all[Run the full regression: every module suite + top-level e2e/tests/]' \\
|
|
117
|
+
'--all-modules[Run e2e tests for all modules with e2e/ directories]' \\
|
|
111
118
|
'--keep[Keep network running after tests]' \\
|
|
112
119
|
'--reuse[Reuse existing network if running]' \\
|
|
113
120
|
'--live[Use live (non-simulated) internet]' \\
|
|
114
121
|
'--published[Use published .netapp packages]' \\
|
|
115
|
-
'--all-modules[Run e2e tests for all modules with e2e/ directories]' \\
|
|
116
122
|
'*::test name or module path:_cele2e_run_args'
|
|
117
123
|
;;
|
|
124
|
+
list)
|
|
125
|
+
_arguments '--paths[Also show each test file path]'
|
|
126
|
+
;;
|
|
118
127
|
up)
|
|
119
128
|
_arguments '1: :_cele2e_up_presets'
|
|
120
129
|
;;
|
|
@@ -137,7 +146,7 @@ _cele2e() {
|
|
|
137
146
|
completion)
|
|
138
147
|
_arguments '1: :_cele2e_completion_shells'
|
|
139
148
|
;;
|
|
140
|
-
status|load|clear-timing)
|
|
149
|
+
status|load|clear-timing|list)
|
|
141
150
|
;;
|
|
142
151
|
esac
|
|
143
152
|
;;
|
|
@@ -147,6 +156,7 @@ _cele2e() {
|
|
|
147
156
|
_cele2e_commands() {
|
|
148
157
|
local commands=(
|
|
149
158
|
'run:Run e2e tests (pattern filters by test file name)'
|
|
159
|
+
'list:List every discoverable Docker e2e test (module + top-level)'
|
|
150
160
|
'up:Start interactive test network'
|
|
151
161
|
'down:Tear down running network'
|
|
152
162
|
'build-infra:Rebuild @celilo/e2e Docker images and standard module .netapps'
|
package/src/cli/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
|
-
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
20
20
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
21
21
|
import { parse as parseYaml } from 'yaml';
|
|
22
22
|
import { runBuild } from './build';
|
|
@@ -55,6 +55,53 @@ function detectTestDir(): { stateDir: string; testsPath: string } {
|
|
|
55
55
|
return { stateDir: cwd, testsPath: join(cwd, 'tests') };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the repo root to scan for the full e2e suite (`run --all` / `list`).
|
|
60
|
+
*
|
|
61
|
+
* The workspace `./cele2e` wrapper cd's into packages/e2e before exec'ing, so
|
|
62
|
+
* process.cwd() points at the e2e package — useless for module discovery. The
|
|
63
|
+
* wrapper exports E2E_INVOKE_CWD with the operator's real PWD; we walk up from
|
|
64
|
+
* there (or process.cwd() as a fallback) to the nearest ancestor that has a
|
|
65
|
+
* `modules/` directory, which is the infra repo root. This is what makes the
|
|
66
|
+
* full-regression command immune to the wrapper's internal cd (ISS-0013).
|
|
67
|
+
*/
|
|
68
|
+
function resolveSuiteRoot(): string {
|
|
69
|
+
const start = resolve(process.env.E2E_INVOKE_CWD || process.cwd());
|
|
70
|
+
let dir = start;
|
|
71
|
+
for (;;) {
|
|
72
|
+
if (existsSync(join(dir, 'modules'))) return dir;
|
|
73
|
+
const parent = dirname(dir);
|
|
74
|
+
if (parent === dir) break;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
return start;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Enumerate every discoverable Docker e2e test (all module suites + the repo's
|
|
82
|
+
* top-level e2e/tests/) as `{ name, file }`, sorted, with module tests first.
|
|
83
|
+
*/
|
|
84
|
+
function discoverSuiteTests(suiteRoot: string): { name: string; file: string; group: string }[] {
|
|
85
|
+
const out: { name: string; file: string; group: string }[] = [];
|
|
86
|
+
for (const moduleDir of findAllModules(suiteRoot)) {
|
|
87
|
+
const tp = join(moduleDir, 'e2e');
|
|
88
|
+
const testsDir = existsSync(join(moduleDir, 'manifest.yml'))
|
|
89
|
+
? resolve(moduleDir, readManifestTestsDir(join(moduleDir, 'manifest.yml')) ?? 'e2e')
|
|
90
|
+
: tp;
|
|
91
|
+
if (!existsSync(testsDir)) continue;
|
|
92
|
+
for (const f of readdirSync(testsDir).filter((f) => f.endsWith('.test.ts')).sort()) {
|
|
93
|
+
out.push({ name: f.replace(/\.test\.ts$/, ''), file: join(testsDir, f), group: basename(moduleDir) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const topLevel = join(suiteRoot, 'e2e', 'tests');
|
|
97
|
+
if (existsSync(topLevel)) {
|
|
98
|
+
for (const f of readdirSync(topLevel).filter((f) => f.endsWith('.test.ts')).sort()) {
|
|
99
|
+
out.push({ name: f.replace(/\.test\.ts$/, ''), file: join(topLevel, f), group: '(top-level)' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
58
105
|
function runScript(script: string, args: string[], env?: Record<string, string>): never {
|
|
59
106
|
const result = spawnSync(join(BIN_DIR, script), args, {
|
|
60
107
|
stdio: 'inherit',
|
|
@@ -72,6 +119,8 @@ Usage:
|
|
|
72
119
|
|
|
73
120
|
Commands:
|
|
74
121
|
run [pattern] Run e2e tests (pattern filters by test file name)
|
|
122
|
+
run --all Run the FULL regression: every module suite + top-level e2e/tests/
|
|
123
|
+
list List every discoverable Docker e2e test (module + top-level)
|
|
75
124
|
up [preset] Start interactive test network
|
|
76
125
|
down Tear down running network
|
|
77
126
|
build-infra [dirs...] Rebuild Docker images and package all modules as .netapps
|
|
@@ -84,6 +133,8 @@ Commands:
|
|
|
84
133
|
completion <shell> Generate shell completion script (zsh|bash)
|
|
85
134
|
|
|
86
135
|
Options for \`run\`:
|
|
136
|
+
--all Run every module suite AND the top-level e2e/tests/ (full regression)
|
|
137
|
+
--all-modules Run every module suite (no top-level tests)
|
|
87
138
|
--keep Keep network running after tests
|
|
88
139
|
--reuse Reuse existing network if running
|
|
89
140
|
--live Use live (non-simulated) internet
|
|
@@ -103,6 +154,8 @@ Options for \`build-infra\`:
|
|
|
103
154
|
--skip-modules Skip .netapp packaging (only rebuild Docker images)
|
|
104
155
|
|
|
105
156
|
Examples:
|
|
157
|
+
cele2e run --all # FULL regression: every module suite + top-level e2e/tests/
|
|
158
|
+
cele2e list # enumerate every discoverable Docker e2e test
|
|
106
159
|
cele2e run # in a module: run its tests; outside: run all modules' tests
|
|
107
160
|
cele2e run deploy # run tests matching "deploy"
|
|
108
161
|
cele2e run caddy-direct-internet # from modules/, finds the matching module automatically
|
|
@@ -130,6 +183,34 @@ if (command === '--get-completions') {
|
|
|
130
183
|
|
|
131
184
|
switch (command) {
|
|
132
185
|
case 'run': {
|
|
186
|
+
// Full-suite modes (ISS-0013). `--all` runs every module suite AND the
|
|
187
|
+
// repo's top-level e2e/tests/ in one invocation; `--all-modules` runs just
|
|
188
|
+
// the module suites. Both resolve the repo root via resolveSuiteRoot() so
|
|
189
|
+
// they work through the `./cele2e` wrapper's cd into packages/e2e.
|
|
190
|
+
if (args.includes('--all') || args.includes('--all-modules')) {
|
|
191
|
+
const wantTopLevel = args.includes('--all');
|
|
192
|
+
const suiteRoot = resolveSuiteRoot();
|
|
193
|
+
const allModules = findAllModules(suiteRoot);
|
|
194
|
+
const env: Record<string, string> = {
|
|
195
|
+
E2E_TEST_DIR: join(suiteRoot, 'e2e'),
|
|
196
|
+
E2E_MODULE_DIRS: allModules.join('\n'),
|
|
197
|
+
};
|
|
198
|
+
const topLevel = join(suiteRoot, 'e2e', 'tests');
|
|
199
|
+
if (wantTopLevel && existsSync(topLevel)) {
|
|
200
|
+
env.E2E_TOP_LEVEL_TESTS = topLevel;
|
|
201
|
+
}
|
|
202
|
+
if (allModules.length === 0 && !env.E2E_TOP_LEVEL_TESTS) {
|
|
203
|
+
console.error(`No e2e tests found under ${suiteRoot}`);
|
|
204
|
+
console.error('Run from the infra repo root (or set E2E_INVOKE_CWD to it).');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
runScript(
|
|
208
|
+
'e2e-run',
|
|
209
|
+
args.filter((a) => a !== '--all' && a !== '--all-modules'),
|
|
210
|
+
env,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
133
214
|
// Check if any non-flag arg is a module path (has manifest.yml)
|
|
134
215
|
const modulePath = args
|
|
135
216
|
.filter((a) => !a.startsWith('--'))
|
|
@@ -180,6 +261,29 @@ switch (command) {
|
|
|
180
261
|
break;
|
|
181
262
|
}
|
|
182
263
|
|
|
264
|
+
case 'list': {
|
|
265
|
+
// Enumerate every discoverable Docker e2e test (module + top-level) so the
|
|
266
|
+
// suite is inspectable without reading runner.ts (ISS-0013).
|
|
267
|
+
const suiteRoot = resolveSuiteRoot();
|
|
268
|
+
const tests = discoverSuiteTests(suiteRoot);
|
|
269
|
+
if (tests.length === 0) {
|
|
270
|
+
console.error(`No e2e tests found under ${suiteRoot}`);
|
|
271
|
+
console.error('Run from the infra repo root (or set E2E_INVOKE_CWD to it).');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
const wantPaths = args.includes('--paths');
|
|
275
|
+
let lastGroup = '';
|
|
276
|
+
for (const t of tests) {
|
|
277
|
+
if (t.group !== lastGroup) {
|
|
278
|
+
console.log(`\n${t.group}:`);
|
|
279
|
+
lastGroup = t.group;
|
|
280
|
+
}
|
|
281
|
+
console.log(wantPaths ? ` ${t.name} ${t.file}` : ` ${t.name}`);
|
|
282
|
+
}
|
|
283
|
+
console.log(`\n${tests.length} tests. Run all: cele2e run --all`);
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
|
|
183
287
|
case 'up':
|
|
184
288
|
runScript('e2e-up', args);
|
|
185
289
|
|
package/src/container-manager.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
ProxyOptions,
|
|
17
17
|
SocksProxyHandle,
|
|
18
18
|
} from './types';
|
|
19
|
-
import { CeliloCommandError, ZONE_GATEWAYS, ZONE_SUBNETS } from './types';
|
|
19
|
+
import { CeliloCommandError, internalNatIp, ZONE_GATEWAYS, ZONE_SUBNETS } from './types';
|
|
20
20
|
|
|
21
21
|
/** Package root — where docker/, config/, simulators/ live */
|
|
22
22
|
const PACKAGE_ROOT = join(__dirname, '..');
|
|
@@ -137,7 +137,7 @@ const SHARED_CONTAINERS = new Set([
|
|
|
137
137
|
* set (useful for --keep debugging).
|
|
138
138
|
*/
|
|
139
139
|
async function scrubDnsZones(): Promise<void> {
|
|
140
|
-
const zones = ['iamtheinternet.org', '
|
|
140
|
+
const zones = ['iamtheinternet.org', 'example.net'];
|
|
141
141
|
const composeDir = PACKAGE_ROOT;
|
|
142
142
|
|
|
143
143
|
for (const zone of zones) {
|
|
@@ -675,7 +675,7 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
675
675
|
async deployFirewall(opts = {}): Promise<void> {
|
|
676
676
|
const zones = opts.zones ?? ['dmz', 'app', 'secure'];
|
|
677
677
|
const firewallIp = ZONE_GATEWAYS.internal; // fw-main on internal
|
|
678
|
-
const natIp = opts.natIp ??
|
|
678
|
+
const natIp = opts.natIp ?? internalNatIp();
|
|
679
679
|
const providedNetworks = zones.map((zone) => ({
|
|
680
680
|
zone,
|
|
681
681
|
subnet: ZONE_SUBNETS[zone],
|
|
@@ -950,7 +950,7 @@ export function reconnectNetwork(projectName: string): NetworkHandle {
|
|
|
950
950
|
async deployFirewall(opts = {}): Promise<void> {
|
|
951
951
|
const zones = opts.zones ?? ['dmz', 'app', 'secure'];
|
|
952
952
|
const firewallIp = ZONE_GATEWAYS.internal; // fw-main on internal
|
|
953
|
-
const natIp = opts.natIp ??
|
|
953
|
+
const natIp = opts.natIp ?? internalNatIp();
|
|
954
954
|
const providedNetworks = zones.map((zone) => ({
|
|
955
955
|
zone,
|
|
956
956
|
subnet: ZONE_SUBNETS[zone],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { stringify } from 'yaml';
|
|
4
|
+
import { normalizeObservers, observerEnv, observerPlacement } from './observer';
|
|
4
5
|
import { SIMULATOR_IPS } from './simulator-ips';
|
|
5
6
|
import type { MachineSpec, NetworkConfig, TopologyPreset } from './types';
|
|
6
7
|
import { ZONE_GATEWAYS } from './types';
|
|
@@ -216,7 +217,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
216
217
|
// `test123` still applies to any domain not listed.
|
|
217
218
|
environment: {
|
|
218
219
|
DDNS_PASSWORDS:
|
|
219
|
-
'{"iamtheinternet.org":"test123","celilo.computer":"test456","
|
|
220
|
+
'{"iamtheinternet.org":"test123","celilo.computer":"test456","example.net":"test789"}',
|
|
220
221
|
},
|
|
221
222
|
volumes: [
|
|
222
223
|
'./config/dns/knot-namecheap.conf:/config/knot.conf:ro',
|
|
@@ -227,7 +228,7 @@ export function generateSharedInfraYaml(): string {
|
|
|
227
228
|
// namecheap-startup.sh for the full rationale).
|
|
228
229
|
'./config/dns/iamtheinternet.org.zone:/seed/iamtheinternet.org.zone:ro',
|
|
229
230
|
'./config/dns/celilo.computer.zone:/seed/celilo.computer.zone:ro',
|
|
230
|
-
'./config/dns/
|
|
231
|
+
'./config/dns/example.net.zone:/seed/example.net.zone:ro',
|
|
231
232
|
'./config/dns/park-your-domain.com.zone:/seed/park-your-domain.com.zone:ro',
|
|
232
233
|
],
|
|
233
234
|
});
|
|
@@ -504,6 +505,20 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
504
505
|
});
|
|
505
506
|
}
|
|
506
507
|
|
|
508
|
+
// --- Observer vantages (passive spies; ISS-0117) ---
|
|
509
|
+
// Each is the SAME image at a different network location with a routing profile that
|
|
510
|
+
// mirrors a real device there — that routing profile is what makes it a faithful seat
|
|
511
|
+
// to assert from, rather than the all-VLAN management trap.
|
|
512
|
+
for (const spec of normalizeObservers(config.observers ?? [])) {
|
|
513
|
+
const p = observerPlacement(spec.vantage);
|
|
514
|
+
services[p.service] = baseService({
|
|
515
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.observer' },
|
|
516
|
+
networks: { [p.network]: { ipv4_address: p.ip } },
|
|
517
|
+
cap_add: ['NET_ADMIN'],
|
|
518
|
+
environment: observerEnv(p),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
507
522
|
const compose = { networks, volumes, services };
|
|
508
523
|
return stringify(compose, { lineWidth: 120 });
|
|
509
524
|
}
|
|
@@ -619,7 +634,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
619
634
|
// `test123` still applies to any domain not listed.
|
|
620
635
|
environment: {
|
|
621
636
|
DDNS_PASSWORDS:
|
|
622
|
-
'{"iamtheinternet.org":"test123","celilo.computer":"test456","
|
|
637
|
+
'{"iamtheinternet.org":"test123","celilo.computer":"test456","example.net":"test789"}',
|
|
623
638
|
},
|
|
624
639
|
volumes: [
|
|
625
640
|
'./config/dns/knot-namecheap.conf:/config/knot.conf:ro',
|
|
@@ -630,7 +645,7 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
|
|
|
630
645
|
// namecheap-startup.sh for the full rationale).
|
|
631
646
|
'./config/dns/iamtheinternet.org.zone:/seed/iamtheinternet.org.zone:ro',
|
|
632
647
|
'./config/dns/celilo.computer.zone:/seed/celilo.computer.zone:ro',
|
|
633
|
-
'./config/dns/
|
|
648
|
+
'./config/dns/example.net.zone:/seed/example.net.zone:ro',
|
|
634
649
|
'./config/dns/park-your-domain.com.zone:/seed/park-your-domain.com.zone:ro',
|
|
635
650
|
],
|
|
636
651
|
});
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type {
|
|
|
37
37
|
ExecResult,
|
|
38
38
|
MachineSpec,
|
|
39
39
|
Zone,
|
|
40
|
+
Vantage,
|
|
41
|
+
ObserverSpec,
|
|
40
42
|
TopologyPreset,
|
|
41
43
|
ProxyOptions,
|
|
42
44
|
SocksProxyHandle,
|
|
@@ -44,7 +46,32 @@ export type {
|
|
|
44
46
|
BrowserHandle,
|
|
45
47
|
BrowserLauncher,
|
|
46
48
|
} from './types';
|
|
47
|
-
export {
|
|
49
|
+
export {
|
|
50
|
+
CeliloCommandError,
|
|
51
|
+
greenwaveRouterIp,
|
|
52
|
+
internalNatIp,
|
|
53
|
+
internalResolverIp,
|
|
54
|
+
publicResolverIp,
|
|
55
|
+
ZONE_GATEWAYS,
|
|
56
|
+
ZONE_SUBNETS,
|
|
57
|
+
} from './types';
|
|
58
|
+
|
|
59
|
+
// Vantage-point network assertions (ISS-0117)
|
|
60
|
+
export { VantageProbe, VantageAssertionError } from './vantage';
|
|
61
|
+
export type {
|
|
62
|
+
ProbeTransport,
|
|
63
|
+
ResolveMethod,
|
|
64
|
+
ResolveResult,
|
|
65
|
+
HttpsResult,
|
|
66
|
+
CertInfo,
|
|
67
|
+
ResolutionInspection,
|
|
68
|
+
ExpectedZone,
|
|
69
|
+
} from './vantage';
|
|
70
|
+
export { createObserverTransport, vantageContainer, OBSERVER_PLACEMENTS } from './observer';
|
|
71
|
+
export { classifyIp, isSegmentedZoneIp, isPublicLeakSafe } from './zone-classifier';
|
|
72
|
+
export type { ZoneClass } from './zone-classifier';
|
|
73
|
+
export { assertFrontedServicePositives, assertFrontedHostnames } from './vantage-helpers';
|
|
74
|
+
export type { FrontedServiceExpectation } from './vantage-helpers';
|
|
48
75
|
|
|
49
76
|
// Common fixtures
|
|
50
77
|
export {
|
package/src/network-builder.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { execSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { startNetwork } from './container-manager';
|
|
5
|
-
import type { NetworkConfig, NetworkHandle, TopologyPreset } from './types';
|
|
5
|
+
import type { NetworkConfig, NetworkHandle, TopologyPreset, Vantage } from './types';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* If a mounted path looks like a celilo module (has scripts/package.json),
|
|
@@ -39,6 +39,7 @@ export class NetworkBuilder {
|
|
|
39
39
|
secureMachines: [],
|
|
40
40
|
internalMachines: [],
|
|
41
41
|
dhcpClient: false,
|
|
42
|
+
observers: [],
|
|
42
43
|
domain: 'iamtheinternet.org',
|
|
43
44
|
ddnsPassword: 'test123',
|
|
44
45
|
verifyRouting: process.env.VERIFY_ROUTING === '1',
|
|
@@ -104,6 +105,22 @@ export class NetworkBuilder {
|
|
|
104
105
|
return this;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Inject one or more observer vantages (ISS-0117) — passive spies carrying the probe
|
|
110
|
+
* toolbox, each placed at its vantage's network location with a routing profile that
|
|
111
|
+
* mirrors a real device there. Drive them via `createObserverTransport(handle)` +
|
|
112
|
+
* `VantageProbe`. `management` is not injectable (that vantage reuses the real
|
|
113
|
+
* management container).
|
|
114
|
+
*
|
|
115
|
+
* network().dmz({ caddy: '10.0.10.10' }).observe('internalDevice', 'publicInternet')
|
|
116
|
+
*/
|
|
117
|
+
observe(...vantages: Vantage[]): this {
|
|
118
|
+
for (const vantage of vantages) {
|
|
119
|
+
(this.config.observers ??= []).push({ vantage });
|
|
120
|
+
}
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
domain(domain: string): this {
|
|
108
125
|
this.config.domain = domain;
|
|
109
126
|
return this;
|