@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
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { stringify } from 'yaml';
|
|
2
|
+
import type { MachineSpec, NetworkConfig, TopologyPreset } from './types';
|
|
3
|
+
import { ZONE_GATEWAYS } from './types';
|
|
4
|
+
|
|
5
|
+
const NETWORK_DRIVER_OPTS = {
|
|
6
|
+
'com.docker.network.bridge.enable_ip_masquerade': 'false',
|
|
7
|
+
'com.docker.network.endpoint.sysctls': 'net.ipv4.conf.IFNAME.rp_filter=0',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const ROUTER_SYSCTLS = {
|
|
11
|
+
'net.ipv4.ip_forward': '1',
|
|
12
|
+
'net.ipv4.conf.all.rp_filter': '0',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function networkDef(subnet: string, gateway: string) {
|
|
16
|
+
return {
|
|
17
|
+
driver: 'bridge',
|
|
18
|
+
driver_opts: NETWORK_DRIVER_OPTS,
|
|
19
|
+
ipam: { config: [{ subnet, gateway }] },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Image tag prefix for pre-built e2e images. When images with these
|
|
25
|
+
* tags exist locally, `docker compose build` skips rebuilding them.
|
|
26
|
+
* The e2e-build script creates these tags; e2e-save/e2e-load persist
|
|
27
|
+
* them across colima restarts.
|
|
28
|
+
*/
|
|
29
|
+
const IMAGE_PREFIX = 'celilo-e2e';
|
|
30
|
+
|
|
31
|
+
function imageTag(dockerfile: string): string {
|
|
32
|
+
// docker/Dockerfile.management -> celilo-e2e/management
|
|
33
|
+
const name = dockerfile.replace('docker/Dockerfile.', '');
|
|
34
|
+
return `${IMAGE_PREFIX}/${name}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function baseService(opts: {
|
|
38
|
+
build?: { context: string; dockerfile: string };
|
|
39
|
+
image?: string;
|
|
40
|
+
networks: Record<string, { ipv4_address: string }>;
|
|
41
|
+
cap_add?: string[];
|
|
42
|
+
sysctls?: Record<string, string>;
|
|
43
|
+
volumes?: string[];
|
|
44
|
+
command?: string;
|
|
45
|
+
depends_on?: string[];
|
|
46
|
+
environment?: Record<string, string>;
|
|
47
|
+
tmpfs?: string[];
|
|
48
|
+
privileged?: boolean;
|
|
49
|
+
}) {
|
|
50
|
+
// Auto-assign an image tag from the Dockerfile name so Docker can
|
|
51
|
+
// skip builds when pre-built images exist.
|
|
52
|
+
const service: Record<string, unknown> = { ...opts, restart: 'unless-stopped' };
|
|
53
|
+
if (opts.build && !opts.image) {
|
|
54
|
+
service.image = imageTag(opts.build.dockerfile);
|
|
55
|
+
}
|
|
56
|
+
return service;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Shared infrastructure project name. Used by both the shared infra
|
|
61
|
+
// manager and per-test compose files (to reference external networks).
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export const SHARED_PROJECT_NAME = 'celilo-e2e-shared';
|
|
65
|
+
|
|
66
|
+
/** Docker network names as created by the shared infrastructure project */
|
|
67
|
+
export const SHARED_NETWORKS = {
|
|
68
|
+
'isp-external': `${SHARED_PROJECT_NAME}_isp-external`,
|
|
69
|
+
'internet-external': `${SHARED_PROJECT_NAME}_internet-external`,
|
|
70
|
+
'real-internet': `${SHARED_PROJECT_NAME}_real-internet`,
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Topology: services south of fw-ext (firewalls, ISP routers)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Default topology: fw-isp (greenwave) + fw-main (iptables)
|
|
79
|
+
* Two-layer NAT: internet → fw-isp → fw-main → target
|
|
80
|
+
*/
|
|
81
|
+
function defaultTopologyServices(): Record<string, unknown> {
|
|
82
|
+
return {
|
|
83
|
+
'fw-main': baseService({
|
|
84
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.firewall' },
|
|
85
|
+
networks: {
|
|
86
|
+
internal: { ipv4_address: '192.168.0.254' },
|
|
87
|
+
dmz: { ipv4_address: '10.0.10.1' },
|
|
88
|
+
app: { ipv4_address: '10.0.20.1' },
|
|
89
|
+
secure: { ipv4_address: '10.0.30.1' },
|
|
90
|
+
},
|
|
91
|
+
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
92
|
+
sysctls: ROUTER_SYSCTLS,
|
|
93
|
+
volumes: ['ssh-keys:/ssh-keys:ro'],
|
|
94
|
+
}),
|
|
95
|
+
'fw-isp': baseService({
|
|
96
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.greenwave-sim' },
|
|
97
|
+
networks: {
|
|
98
|
+
internal: { ipv4_address: '192.168.0.1' },
|
|
99
|
+
'isp-external': { ipv4_address: '100.100.0.100' },
|
|
100
|
+
},
|
|
101
|
+
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
102
|
+
sysctls: ROUTER_SYSCTLS,
|
|
103
|
+
volumes: ['ssh-keys:/ssh-keys:ro'],
|
|
104
|
+
}),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Direct internet topology: fw-main has external interface, no fw-isp
|
|
110
|
+
* Single-layer NAT: internet → fw-main → target
|
|
111
|
+
* Management routes default through fw-main instead of fw-isp
|
|
112
|
+
*/
|
|
113
|
+
function directInternetTopologyServices(): Record<string, unknown> {
|
|
114
|
+
return {
|
|
115
|
+
'fw-main': baseService({
|
|
116
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.firewall' },
|
|
117
|
+
networks: {
|
|
118
|
+
internal: { ipv4_address: '192.168.0.254' },
|
|
119
|
+
dmz: { ipv4_address: '10.0.10.1' },
|
|
120
|
+
app: { ipv4_address: '10.0.20.1' },
|
|
121
|
+
secure: { ipv4_address: '10.0.30.1' },
|
|
122
|
+
'isp-external': { ipv4_address: '100.100.0.100' },
|
|
123
|
+
},
|
|
124
|
+
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
125
|
+
sysctls: ROUTER_SYSCTLS,
|
|
126
|
+
volumes: ['ssh-keys:/ssh-keys:ro'],
|
|
127
|
+
}),
|
|
128
|
+
// No fw-isp — fw-main handles external routing directly
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const TOPOLOGY_SERVICES: Record<TopologyPreset, () => Record<string, unknown>> = {
|
|
133
|
+
default: defaultTopologyServices,
|
|
134
|
+
'direct-internet': directInternetTopologyServices,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Shared infrastructure compose (DNS, Pebble, apt-cache, fw-ext)
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generate the compose YAML for shared infrastructure that runs once
|
|
143
|
+
* per test suite. Contains: fw-ext, DNS hierarchy, Pebble ACME, apt-cache.
|
|
144
|
+
* Owns the isp-external, internet-external, and real-internet networks.
|
|
145
|
+
*/
|
|
146
|
+
export function generateSharedInfraYaml(): string {
|
|
147
|
+
const networks: Record<string, unknown> = {
|
|
148
|
+
'isp-external': networkDef('100.100.0.0/24', '100.100.0.250'),
|
|
149
|
+
'internet-external': networkDef('100.64.0.0/24', '100.64.0.250'),
|
|
150
|
+
'real-internet': {
|
|
151
|
+
driver: 'bridge',
|
|
152
|
+
ipam: { config: [{ subnet: '172.30.0.0/24' }] },
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const volumes: Record<string, unknown> = {
|
|
157
|
+
'apt-cache': { driver: 'local' },
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const services: Record<string, unknown> = {};
|
|
161
|
+
|
|
162
|
+
services['fw-ext'] = baseService({
|
|
163
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
|
|
164
|
+
networks: {
|
|
165
|
+
'isp-external': { ipv4_address: '100.100.0.101' },
|
|
166
|
+
'internet-external': { ipv4_address: '100.64.0.1' },
|
|
167
|
+
'real-internet': { ipv4_address: '172.30.0.5' },
|
|
168
|
+
},
|
|
169
|
+
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
170
|
+
sysctls: ROUTER_SYSCTLS,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
services['comcast-resolver'] = baseService({
|
|
174
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.resolver' },
|
|
175
|
+
networks: {
|
|
176
|
+
'isp-external': { ipv4_address: '100.100.0.1' },
|
|
177
|
+
'real-internet': { ipv4_address: '172.30.0.4' },
|
|
178
|
+
},
|
|
179
|
+
cap_add: ['NET_ADMIN'],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
services['root-dns'] = baseService({
|
|
183
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
184
|
+
networks: { 'internet-external': { ipv4_address: '100.64.0.53' } },
|
|
185
|
+
cap_add: ['NET_ADMIN'],
|
|
186
|
+
volumes: [
|
|
187
|
+
'./config/dns/knot-root.conf:/config/knot.conf:ro',
|
|
188
|
+
'./config/dns/root.zone:/config/root.zone:ro',
|
|
189
|
+
],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
services['tld-dns'] = baseService({
|
|
193
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
194
|
+
networks: { 'internet-external': { ipv4_address: '100.64.0.54' } },
|
|
195
|
+
cap_add: ['NET_ADMIN'],
|
|
196
|
+
volumes: [
|
|
197
|
+
'./config/dns/knot-tld.conf:/config/knot.conf:ro',
|
|
198
|
+
'./config/dns/com.zone:/config/com.zone:ro',
|
|
199
|
+
'./config/dns/org.zone:/config/org.zone:ro',
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
services['namecheap-dns'] = baseService({
|
|
204
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.namecheap-dns' },
|
|
205
|
+
networks: { 'internet-external': { ipv4_address: '100.64.0.55' } },
|
|
206
|
+
cap_add: ['NET_ADMIN'],
|
|
207
|
+
volumes: [
|
|
208
|
+
'./config/dns/knot-namecheap.conf:/config/knot.conf:ro',
|
|
209
|
+
'./config/dns/iamtheinternet.org.zone:/config/iamtheinternet.org.zone',
|
|
210
|
+
'./config/dns/park-your-domain.com.zone:/config/park-your-domain.com.zone',
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
services['apt-cache'] = baseService({
|
|
215
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.apt-cache' },
|
|
216
|
+
networks: {
|
|
217
|
+
'internet-external': { ipv4_address: '100.64.0.2' },
|
|
218
|
+
'real-internet': { ipv4_address: '172.30.0.2' },
|
|
219
|
+
},
|
|
220
|
+
cap_add: ['NET_ADMIN'],
|
|
221
|
+
volumes: ['apt-cache:/var/cache/apt-cacher-ng'],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
services.letsencrypt = baseService({
|
|
225
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.pebble' },
|
|
226
|
+
networks: { 'internet-external': { ipv4_address: '100.64.0.100' } },
|
|
227
|
+
cap_add: ['NET_ADMIN'],
|
|
228
|
+
command: '-config /config/pebble-config.json -dnsserver 100.100.0.1:53',
|
|
229
|
+
volumes: [
|
|
230
|
+
'./config/pebble/pebble-config.json:/config/pebble-config.json:ro',
|
|
231
|
+
'./config/pebble/pebble-tls.crt:/config/pebble-tls.crt:ro',
|
|
232
|
+
'./config/pebble/pebble-tls.key:/config/pebble-tls.key:ro',
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const compose = { networks, volumes, services };
|
|
237
|
+
return stringify(compose, { lineWidth: 120 });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Per-test compose generation
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Generate the compose YAML for a single test's containers.
|
|
246
|
+
* References the shared infrastructure's isp-external network as external.
|
|
247
|
+
* Creates its own internal, dmz, app, secure networks.
|
|
248
|
+
*/
|
|
249
|
+
export function generateTestComposeYaml(config: NetworkConfig, celiloRoot = '..'): string {
|
|
250
|
+
const networks: Record<string, unknown> = {
|
|
251
|
+
internal: networkDef('192.168.0.0/24', '192.168.0.250'),
|
|
252
|
+
dmz: networkDef('10.0.10.0/24', '10.0.10.250'),
|
|
253
|
+
app: networkDef('10.0.20.0/24', '10.0.20.250'),
|
|
254
|
+
secure: networkDef('10.0.30.0/24', '10.0.30.250'),
|
|
255
|
+
// isp-external is owned by the shared infra project — reference it
|
|
256
|
+
'isp-external': {
|
|
257
|
+
external: true,
|
|
258
|
+
name: SHARED_NETWORKS['isp-external'],
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const volumes: Record<string, unknown> = {
|
|
263
|
+
'ssh-keys': { driver: 'local' },
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const services: Record<string, unknown> = {};
|
|
267
|
+
|
|
268
|
+
// --- Management (always present) ---
|
|
269
|
+
const managementDefaultGw = config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
|
|
270
|
+
|
|
271
|
+
services.management = baseService({
|
|
272
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.management' },
|
|
273
|
+
networks: { internal: { ipv4_address: '192.168.0.100' } },
|
|
274
|
+
cap_add: ['NET_ADMIN'],
|
|
275
|
+
volumes: [
|
|
276
|
+
`${celiloRoot}:/celilo`,
|
|
277
|
+
'ssh-keys:/ssh-keys',
|
|
278
|
+
...(config.managementVolumes ?? []),
|
|
279
|
+
],
|
|
280
|
+
environment: {
|
|
281
|
+
DDNS_ENDPOINT: 'http://100.64.0.55:8080/update',
|
|
282
|
+
DEFAULT_GATEWAY: managementDefaultGw,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// --- Topology-specific infrastructure (south of fw-ext) ---
|
|
287
|
+
const topologyFn = TOPOLOGY_SERVICES[config.topology] || TOPOLOGY_SERVICES.default;
|
|
288
|
+
Object.assign(services, topologyFn());
|
|
289
|
+
|
|
290
|
+
// --- Dynamic test machines ---
|
|
291
|
+
const allMachines = [
|
|
292
|
+
...config.dmzMachines,
|
|
293
|
+
...config.appMachines,
|
|
294
|
+
...config.secureMachines,
|
|
295
|
+
...config.internalMachines,
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
for (const machine of allMachines) {
|
|
299
|
+
const zoneNetwork = machine.zone;
|
|
300
|
+
const gateway = ZONE_GATEWAYS[machine.zone];
|
|
301
|
+
|
|
302
|
+
const isAppZone = machine.zone === 'app';
|
|
303
|
+
const dockerfile = isAppZone
|
|
304
|
+
? 'docker/Dockerfile.target-machine-docker'
|
|
305
|
+
: 'docker/Dockerfile.target-machine';
|
|
306
|
+
|
|
307
|
+
const machineVolumes = ['ssh-keys:/ssh-keys:ro'];
|
|
308
|
+
if (isAppZone) {
|
|
309
|
+
machineVolumes.push('./docker-image-cache:/docker-image-cache:ro');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
services[machine.name] = baseService({
|
|
313
|
+
build: { context: '.', dockerfile },
|
|
314
|
+
networks: { [zoneNetwork]: { ipv4_address: machine.ip } },
|
|
315
|
+
privileged: true,
|
|
316
|
+
volumes: machineVolumes,
|
|
317
|
+
environment: { GATEWAY: gateway },
|
|
318
|
+
tmpfs: ['/run', '/run/lock', '/tmp'],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- Optional DHCP client ---
|
|
323
|
+
if (config.dhcpClient) {
|
|
324
|
+
services['dhcp-client'] = baseService({
|
|
325
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.dhcp-client' },
|
|
326
|
+
networks: { internal: { ipv4_address: '192.168.0.210' } },
|
|
327
|
+
cap_add: ['NET_ADMIN'],
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const compose = { networks, volumes, services };
|
|
332
|
+
return stringify(compose, { lineWidth: 120 });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Legacy: monolithic compose (kept for standalone/fallback use)
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): string {
|
|
340
|
+
const networks: Record<string, unknown> = {
|
|
341
|
+
internal: networkDef('192.168.0.0/24', '192.168.0.250'),
|
|
342
|
+
dmz: networkDef('10.0.10.0/24', '10.0.10.250'),
|
|
343
|
+
app: networkDef('10.0.20.0/24', '10.0.20.250'),
|
|
344
|
+
secure: networkDef('10.0.30.0/24', '10.0.30.250'),
|
|
345
|
+
'isp-external': networkDef('100.100.0.0/24', '100.100.0.250'),
|
|
346
|
+
'internet-external': networkDef('100.64.0.0/24', '100.64.0.250'),
|
|
347
|
+
'real-internet': {
|
|
348
|
+
driver: 'bridge',
|
|
349
|
+
ipam: { config: [{ subnet: '172.30.0.0/24' }] },
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const volumes: Record<string, unknown> = {
|
|
354
|
+
'ssh-keys': { driver: 'local' },
|
|
355
|
+
'apt-cache': { driver: 'local' },
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const services: Record<string, unknown> = {};
|
|
359
|
+
|
|
360
|
+
const managementDefaultGw = config.topology === 'direct-internet' ? '192.168.0.254' : '192.168.0.1';
|
|
361
|
+
|
|
362
|
+
services.management = baseService({
|
|
363
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.management' },
|
|
364
|
+
networks: { internal: { ipv4_address: '192.168.0.100' } },
|
|
365
|
+
cap_add: ['NET_ADMIN'],
|
|
366
|
+
volumes: [
|
|
367
|
+
`${celiloRoot}:/celilo`,
|
|
368
|
+
'ssh-keys:/ssh-keys',
|
|
369
|
+
...(config.managementVolumes ?? []),
|
|
370
|
+
],
|
|
371
|
+
environment: {
|
|
372
|
+
DDNS_ENDPOINT: 'http://100.64.0.55:8080/update',
|
|
373
|
+
DEFAULT_GATEWAY: managementDefaultGw,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const topologyFn = TOPOLOGY_SERVICES[config.topology] || TOPOLOGY_SERVICES.default;
|
|
378
|
+
Object.assign(services, topologyFn());
|
|
379
|
+
|
|
380
|
+
services['fw-ext'] = baseService({
|
|
381
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.router' },
|
|
382
|
+
networks: {
|
|
383
|
+
'isp-external': { ipv4_address: '100.100.0.101' },
|
|
384
|
+
'internet-external': { ipv4_address: '100.64.0.1' },
|
|
385
|
+
'real-internet': { ipv4_address: '172.30.0.5' },
|
|
386
|
+
},
|
|
387
|
+
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
388
|
+
sysctls: ROUTER_SYSCTLS,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
services['comcast-resolver'] = baseService({
|
|
392
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.resolver' },
|
|
393
|
+
networks: {
|
|
394
|
+
'isp-external': { ipv4_address: '100.100.0.1' },
|
|
395
|
+
'real-internet': { ipv4_address: '172.30.0.4' },
|
|
396
|
+
},
|
|
397
|
+
cap_add: ['NET_ADMIN'],
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
services['root-dns'] = baseService({
|
|
401
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
402
|
+
networks: { 'internet-external': { ipv4_address: '100.64.0.53' } },
|
|
403
|
+
cap_add: ['NET_ADMIN'],
|
|
404
|
+
volumes: [
|
|
405
|
+
'./config/dns/knot-root.conf:/config/knot.conf:ro',
|
|
406
|
+
'./config/dns/root.zone:/config/root.zone:ro',
|
|
407
|
+
],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
services['tld-dns'] = baseService({
|
|
411
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.authoritative-dns' },
|
|
412
|
+
networks: { 'internet-external': { ipv4_address: '100.64.0.54' } },
|
|
413
|
+
cap_add: ['NET_ADMIN'],
|
|
414
|
+
volumes: [
|
|
415
|
+
'./config/dns/knot-tld.conf:/config/knot.conf:ro',
|
|
416
|
+
'./config/dns/com.zone:/config/com.zone:ro',
|
|
417
|
+
'./config/dns/org.zone:/config/org.zone:ro',
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
services['namecheap-dns'] = baseService({
|
|
422
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.namecheap-dns' },
|
|
423
|
+
networks: { 'internet-external': { ipv4_address: '100.64.0.55' } },
|
|
424
|
+
cap_add: ['NET_ADMIN'],
|
|
425
|
+
volumes: [
|
|
426
|
+
'./config/dns/knot-namecheap.conf:/config/knot.conf:ro',
|
|
427
|
+
'./config/dns/iamtheinternet.org.zone:/config/iamtheinternet.org.zone',
|
|
428
|
+
'./config/dns/park-your-domain.com.zone:/config/park-your-domain.com.zone',
|
|
429
|
+
],
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
services['apt-cache'] = baseService({
|
|
433
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.apt-cache' },
|
|
434
|
+
networks: {
|
|
435
|
+
'internet-external': { ipv4_address: '100.64.0.2' },
|
|
436
|
+
'real-internet': { ipv4_address: '172.30.0.2' },
|
|
437
|
+
},
|
|
438
|
+
cap_add: ['NET_ADMIN'],
|
|
439
|
+
volumes: ['apt-cache:/var/cache/apt-cacher-ng'],
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
services.letsencrypt = baseService({
|
|
443
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.pebble' },
|
|
444
|
+
networks: { 'internet-external': { ipv4_address: '100.64.0.100' } },
|
|
445
|
+
cap_add: ['NET_ADMIN'],
|
|
446
|
+
command: '-config /config/pebble-config.json -dnsserver 100.100.0.1:53',
|
|
447
|
+
volumes: [
|
|
448
|
+
'./config/pebble/pebble-config.json:/config/pebble-config.json:ro',
|
|
449
|
+
'./config/pebble/pebble-tls.crt:/config/pebble-tls.crt:ro',
|
|
450
|
+
'./config/pebble/pebble-tls.key:/config/pebble-tls.key:ro',
|
|
451
|
+
],
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const allMachines = [
|
|
455
|
+
...config.dmzMachines,
|
|
456
|
+
...config.appMachines,
|
|
457
|
+
...config.secureMachines,
|
|
458
|
+
...config.internalMachines,
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
for (const machine of allMachines) {
|
|
462
|
+
const zoneNetwork = machine.zone;
|
|
463
|
+
const gateway = ZONE_GATEWAYS[machine.zone];
|
|
464
|
+
|
|
465
|
+
const isAppZone = machine.zone === 'app';
|
|
466
|
+
const dockerfile = isAppZone
|
|
467
|
+
? 'docker/Dockerfile.target-machine-docker'
|
|
468
|
+
: 'docker/Dockerfile.target-machine';
|
|
469
|
+
|
|
470
|
+
const machineVolumes = ['ssh-keys:/ssh-keys:ro'];
|
|
471
|
+
if (isAppZone) {
|
|
472
|
+
machineVolumes.push('./docker-image-cache:/docker-image-cache:ro');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
services[machine.name] = baseService({
|
|
476
|
+
build: { context: '.', dockerfile },
|
|
477
|
+
networks: { [zoneNetwork]: { ipv4_address: machine.ip } },
|
|
478
|
+
privileged: true,
|
|
479
|
+
volumes: machineVolumes,
|
|
480
|
+
environment: { GATEWAY: gateway },
|
|
481
|
+
tmpfs: ['/run', '/run/lock', '/tmp'],
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (config.dhcpClient) {
|
|
486
|
+
services['dhcp-client'] = baseService({
|
|
487
|
+
build: { context: '.', dockerfile: 'docker/Dockerfile.dhcp-client' },
|
|
488
|
+
networks: { internal: { ipv4_address: '192.168.0.210' } },
|
|
489
|
+
cap_add: ['NET_ADMIN'],
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const compose = { networks, volumes, services };
|
|
494
|
+
return stringify(compose, { lineWidth: 120 });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function getAllMachines(config: NetworkConfig): MachineSpec[] {
|
|
498
|
+
return [...config.dmzMachines, ...config.appMachines, ...config.secureMachines, ...config.internalMachines];
|
|
499
|
+
}
|
package/src/fixtures.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NetworkBuilder, network } from './network-builder';
|
|
2
|
+
|
|
3
|
+
/** Single caddy machine in the DMZ — basic web server deployment */
|
|
4
|
+
export const CADDY_DEPLOYMENT = (): NetworkBuilder => network().dmz({ caddy: '10.0.10.10' });
|
|
5
|
+
|
|
6
|
+
/** Full stack: web server, identity provider, database */
|
|
7
|
+
export const FULL_STACK = (): NetworkBuilder =>
|
|
8
|
+
network()
|
|
9
|
+
.dmz({ caddy: '10.0.10.10' })
|
|
10
|
+
.app({ idp: '10.0.20.100' })
|
|
11
|
+
.secure({ db: '10.0.30.50' });
|
|
12
|
+
|
|
13
|
+
/** Minimal: no dynamic machines, just fixed infrastructure */
|
|
14
|
+
export const INFRASTRUCTURE_ONLY = (): NetworkBuilder => network();
|
|
15
|
+
|
|
16
|
+
/** Caddy + Authentik: web server in DMZ, identity provider in app zone */
|
|
17
|
+
export const AUTHENTIK_DEPLOYMENT = (): NetworkBuilder =>
|
|
18
|
+
network()
|
|
19
|
+
.dmz({ caddy: '10.0.10.10' })
|
|
20
|
+
.app({ authentik: '10.0.20.30' });
|
|
21
|
+
|
|
22
|
+
/** Multiple DMZ machines for testing multi-service deployments */
|
|
23
|
+
export const MULTI_DMZ = (): NetworkBuilder =>
|
|
24
|
+
network().dmz({
|
|
25
|
+
caddy: '10.0.10.10',
|
|
26
|
+
'web-app': '10.0.10.20',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/** Technitium DNS + Caddy: internal DNS resolver + full web stack */
|
|
30
|
+
export const TECHNITIUM_STACK = (): NetworkBuilder =>
|
|
31
|
+
network()
|
|
32
|
+
.internal({ 'dns-int': '192.168.0.53' })
|
|
33
|
+
.dmz({ caddy: '10.0.10.10' });
|
|
34
|
+
|
|
35
|
+
/** Technitium DNS with DHCP client: tests DHCP DNS propagation */
|
|
36
|
+
export const TECHNITIUM_DHCP_STACK = (): NetworkBuilder =>
|
|
37
|
+
network()
|
|
38
|
+
.internal({ 'dns-int': '192.168.0.53' })
|
|
39
|
+
.dmz({ caddy: '10.0.10.10' })
|
|
40
|
+
.withDhcpClient();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @celilo/e2e — E2E test infrastructure for Celilo-deployed applications.
|
|
3
|
+
*
|
|
4
|
+
* Provides a simulated internet environment in Docker with:
|
|
5
|
+
* - DNS hierarchy (root → TLD → registrar → recursive resolver)
|
|
6
|
+
* - ACME server (Pebble, simulating Let's Encrypt)
|
|
7
|
+
* - APT cache (speeds up package installs in containers)
|
|
8
|
+
* - Firewall topology (ISP router, main firewall, zone routing)
|
|
9
|
+
* - Target machines (systemd-based containers for deployment)
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { network } from '@celilo/e2e';
|
|
13
|
+
*
|
|
14
|
+
* const net = await network()
|
|
15
|
+
* .dmz({ caddy: '10.0.10.10' })
|
|
16
|
+
* .app({ myapp: '10.0.20.10' })
|
|
17
|
+
* .start();
|
|
18
|
+
*
|
|
19
|
+
* await net.celilo('module deploy myapp --no-interactive');
|
|
20
|
+
* // ... assertions ...
|
|
21
|
+
* await net.stop();
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Builder API
|
|
25
|
+
export { network, NetworkBuilder } from './network-builder';
|
|
26
|
+
|
|
27
|
+
// Types
|
|
28
|
+
export type {
|
|
29
|
+
NetworkConfig,
|
|
30
|
+
NetworkHandle,
|
|
31
|
+
ExecResult,
|
|
32
|
+
MachineSpec,
|
|
33
|
+
Zone,
|
|
34
|
+
TopologyPreset,
|
|
35
|
+
} from './types';
|
|
36
|
+
export { ZONE_GATEWAYS, ZONE_SUBNETS } from './types';
|
|
37
|
+
|
|
38
|
+
// Common fixtures
|
|
39
|
+
export {
|
|
40
|
+
CADDY_DEPLOYMENT,
|
|
41
|
+
FULL_STACK,
|
|
42
|
+
INFRASTRUCTURE_ONLY,
|
|
43
|
+
AUTHENTIK_DEPLOYMENT,
|
|
44
|
+
MULTI_DMZ,
|
|
45
|
+
TECHNITIUM_STACK,
|
|
46
|
+
TECHNITIUM_DHCP_STACK,
|
|
47
|
+
} from './fixtures';
|
|
48
|
+
|
|
49
|
+
// Shared infrastructure management (for test runners)
|
|
50
|
+
export { ensureSharedInfra, stopSharedInfra, isSharedInfraRunning } from './shared-infra';
|
|
51
|
+
|
|
52
|
+
// Low-level (for custom compose generation)
|
|
53
|
+
export {
|
|
54
|
+
generateSharedInfraYaml,
|
|
55
|
+
generateTestComposeYaml,
|
|
56
|
+
generateComposeYaml,
|
|
57
|
+
SHARED_PROJECT_NAME,
|
|
58
|
+
SHARED_NETWORKS,
|
|
59
|
+
} from './docker-compose-generator';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { startNetwork } from './container-manager';
|
|
2
|
+
import type { NetworkConfig, NetworkHandle, TopologyPreset } from './types';
|
|
3
|
+
|
|
4
|
+
export class NetworkBuilder {
|
|
5
|
+
private config: NetworkConfig = {
|
|
6
|
+
topology: 'default',
|
|
7
|
+
dmzMachines: [],
|
|
8
|
+
appMachines: [],
|
|
9
|
+
secureMachines: [],
|
|
10
|
+
internalMachines: [],
|
|
11
|
+
dhcpClient: false,
|
|
12
|
+
domain: 'iamtheinternet.org',
|
|
13
|
+
ddnsPassword: 'test123',
|
|
14
|
+
verifyRouting: process.env.VERIFY_ROUTING === '1',
|
|
15
|
+
managementVolumes: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
topology(preset: TopologyPreset): this {
|
|
19
|
+
this.config.topology = preset;
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
dmz(machines: Record<string, string>): this {
|
|
24
|
+
for (const [name, ip] of Object.entries(machines)) {
|
|
25
|
+
this.config.dmzMachines.push({ name, ip, zone: 'dmz' });
|
|
26
|
+
}
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
app(machines: Record<string, string>): this {
|
|
31
|
+
for (const [name, ip] of Object.entries(machines)) {
|
|
32
|
+
this.config.appMachines.push({ name, ip, zone: 'app' });
|
|
33
|
+
}
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
secure(machines: Record<string, string>): this {
|
|
38
|
+
for (const [name, ip] of Object.entries(machines)) {
|
|
39
|
+
this.config.secureMachines.push({ name, ip, zone: 'secure' });
|
|
40
|
+
}
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
internal(machines: Record<string, string>): this {
|
|
45
|
+
for (const [name, ip] of Object.entries(machines)) {
|
|
46
|
+
this.config.internalMachines.push({ name, ip, zone: 'internal' });
|
|
47
|
+
}
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
withDhcpClient(): this {
|
|
52
|
+
this.config.dhcpClient = true;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
domain(domain: string): this {
|
|
57
|
+
this.config.domain = domain;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
withRoutingVerification(): this {
|
|
62
|
+
this.config.verifyRouting = true;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Mount a host directory into the management container */
|
|
67
|
+
mount(hostPath: string, containerPath: string): this {
|
|
68
|
+
this.config.managementVolumes.push(`${hostPath}:${containerPath}`);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set the path to the Celilo project root. This directory is mounted
|
|
74
|
+
* into the management container at /celilo, providing access to the
|
|
75
|
+
* Celilo CLI and module source code.
|
|
76
|
+
*
|
|
77
|
+
* If not set, auto-detected by walking up from the @celilo/e2e package.
|
|
78
|
+
*/
|
|
79
|
+
celilo(rootPath: string): this {
|
|
80
|
+
this.config.celiloRoot = rootPath;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async start(): Promise<NetworkHandle> {
|
|
85
|
+
return startNetwork(this.config);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function network(): NetworkBuilder {
|
|
90
|
+
return new NetworkBuilder();
|
|
91
|
+
}
|