@celilo/e2e 0.18.0 → 0.19.1
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/routing/fw-isp-routes.sh +13 -3
- package/docker/Dockerfile.management +6 -0
- package/package.json +3 -3
- package/registry-server/src/bootstrap.ts +5 -0
- package/registry-server/src/server.test.ts +183 -1
- package/registry-server/src/server.ts +75 -0
- package/registry-server/src/storage.ts +76 -2
- package/registry-server/src/sweep.test.ts +326 -0
- package/registry-server/src/sweep.ts +289 -0
- package/scripts/consumer-paths-changed.ts +3 -0
- package/src/address-plan.test.ts +21 -1
- package/src/container-manager.ts +23 -9
- package/src/docker-compose-generator.ts +107 -24
- package/src/network-builder.ts +17 -0
- package/src/shared-infra.ts +6 -0
- package/src/types.ts +13 -0
package/src/container-manager.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ExecSyncOptions, execSync, spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { basename, join, resolve } from 'node:path';
|
|
5
5
|
import { parse as parseYaml } from 'yaml';
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
firewallZoneLegs,
|
|
11
11
|
generateTestComposeYaml,
|
|
12
12
|
getAllMachines,
|
|
13
|
+
registryUploadsHostDir,
|
|
13
14
|
} from './docker-compose-generator';
|
|
14
15
|
import { explainBuildFailure } from './doctor';
|
|
15
16
|
import { type ModuleHost, parseModuleHost } from './module-host';
|
|
@@ -1005,12 +1006,19 @@ function buildNetworkHandle(
|
|
|
1005
1006
|
}
|
|
1006
1007
|
|
|
1007
1008
|
try {
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1009
|
+
// Write HOST-side into the dir bound at the registry's /uploads. The
|
|
1010
|
+
// registry rescans that dir on every request, so it needs no HTTP
|
|
1011
|
+
// handshake and no restart.
|
|
1012
|
+
//
|
|
1013
|
+
// Deliberately not a `docker compose cp` into the container: /uploads
|
|
1014
|
+
// is a READ-ONLY bind in a consumer install, so the container write
|
|
1015
|
+
// failed for every npm-installed consumer while passing forever in the
|
|
1016
|
+
// monorepo, where the same path is not a mount at all (celilo#1142).
|
|
1017
|
+
// registryUploadsHostDir() is the same function the compose generator
|
|
1018
|
+
// builds the mount from, so the two cannot drift apart again.
|
|
1019
|
+
const uploadsDir = registryUploadsHostDir();
|
|
1020
|
+
mkdirSync(uploadsDir, { recursive: true });
|
|
1021
|
+
copyFileSync(netappPath, join(uploadsDir, `${moduleId}.netapp`));
|
|
1014
1022
|
} finally {
|
|
1015
1023
|
if (cleanup)
|
|
1016
1024
|
try {
|
|
@@ -1264,8 +1272,14 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
|
|
|
1264
1272
|
);
|
|
1265
1273
|
}
|
|
1266
1274
|
|
|
1267
|
-
// Wait for DHCP client lease
|
|
1268
|
-
|
|
1275
|
+
// Wait for DHCP client lease.
|
|
1276
|
+
//
|
|
1277
|
+
// Skipped when the router's DHCP is off, because then NOTHING is serving yet:
|
|
1278
|
+
// celilo's own DHCP server is a module, and modules deploy long after the
|
|
1279
|
+
// network is up. Waiting here would time out at 60s on every such suite and
|
|
1280
|
+
// report it as a network-start failure. Those suites drive the client
|
|
1281
|
+
// themselves once their server is deployed.
|
|
1282
|
+
if (config.dhcpClient && config.routerDhcp !== false) {
|
|
1269
1283
|
console.log('[progress:start] waiting for DHCP client lease | DHCP lease acquired');
|
|
1270
1284
|
// Self-diagnosing via waitFor's onTimeout (e2e-confidence #255): a DHCP-lease
|
|
1271
1285
|
// timeout is an intermittent in-suite flake (passes solo). On timeout, dump
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
3
|
import { stringify } from 'yaml';
|
|
4
4
|
import { normalizeObservers, observerEnv, observerPlacement } from './observer';
|
|
5
5
|
import { PROXMOX_SIM_IP, SIMULATOR_IPS } from './simulator-ips';
|
|
@@ -13,32 +13,76 @@ import {
|
|
|
13
13
|
zoneIp,
|
|
14
14
|
} from './types';
|
|
15
15
|
|
|
16
|
+
const PACKAGE_DIR = join(import.meta.dir, '..');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Is this @celilo/e2e running from the monorepo, or from an npm install?
|
|
20
|
+
*
|
|
21
|
+
* `<pkgDir>/../../modules` is `<repo>/modules` in the monorepo and
|
|
22
|
+
* `<consumer>/node_modules/modules` (nonexistent) in a consumer install.
|
|
23
|
+
*/
|
|
24
|
+
function isMonorepoInstall(): boolean {
|
|
25
|
+
return existsSync(join(PACKAGE_DIR, '..', '..', 'modules'));
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
29
|
+
* The host dir bound read-only at the registry's `/uploads` — and therefore
|
|
30
|
+
* the dir `publishModule` writes a `.netapp` into. The registry rescans it on
|
|
31
|
+
* every request, so a host-side write is picked up with no container write and
|
|
32
|
+
* no HTTP handshake.
|
|
33
|
+
*
|
|
34
|
+
* Both install shapes bind one. They differ only in WHICH, because they differ
|
|
35
|
+
* in where standard modules come from:
|
|
36
|
+
*
|
|
37
|
+
* - **Monorepo dev** — modules are served live from `/modules`
|
|
38
|
+
* (`../../modules`, BOOTSTRAP_MODULES_DIR mode), so edits to a module's
|
|
39
|
+
* source flow through without a manual repack. `/uploads` is therefore a
|
|
40
|
+
* dedicated drop-zone, emptied on every shared-infra start (see
|
|
41
|
+
* `prepareRegistryDropZone`) — a leftover `.netapp` would shadow live source.
|
|
19
42
|
*
|
|
20
|
-
* - **
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
43
|
+
* - **npm-installed consumer** (lunacycle and friends) — `../../modules` does
|
|
44
|
+
* not exist, so there is no source to serve. `/uploads` IS the netapp cache
|
|
45
|
+
* `cele2e build-infra` fills from the public registry (see
|
|
46
|
+
* stageNetappsFromRegistry in cli/build.ts), and a published module lands
|
|
47
|
+
* beside the standard ones. Consumers vendor nothing.
|
|
48
|
+
*
|
|
49
|
+
* Read this through `registryUploadsHostDir()` rather than re-deriving it.
|
|
50
|
+
* publishModule used to `docker cp` into the container's `/uploads` instead,
|
|
51
|
+
* which works only where that path is NOT a mount — so it passed forever in
|
|
52
|
+
* the monorepo and failed for every consumer against the read-only bind
|
|
53
|
+
* (celilo#1142). One function now decides the mount and the write target
|
|
54
|
+
* together, so they cannot disagree again.
|
|
55
|
+
*/
|
|
56
|
+
export function registryUploadsHostDir(): string {
|
|
57
|
+
return join(PACKAGE_DIR, isMonorepoInstall() ? 'uploads' : 'netapps');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Make the drop-zone ready for a shared-infra bring-up.
|
|
24
62
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* exist. Docker fails the mount with "permission denied" trying to
|
|
28
|
-
* create that path. Instead we bind the local `./netapps/` dir to
|
|
29
|
-
* `/uploads`, and the registry's scanUploadsDir picks up the .netapp
|
|
30
|
-
* files. That dir is populated at `cele2e build-infra` time by fetching
|
|
31
|
-
* the standard-module netapps from the public registry (see
|
|
32
|
-
* stageNetappsFromRegistry in cli/build.ts) — consumers vendor nothing.
|
|
63
|
+
* Creates it: Docker silently creates a MISSING bind source as an empty
|
|
64
|
+
* root-owned directory, which the host then cannot write into.
|
|
33
65
|
*
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
66
|
+
* Empties it, but only in the monorepo, where `/uploads` is dedicated — a
|
|
67
|
+
* `.netapp` left by a previous run would shadow the live module source it was
|
|
68
|
+
* packed from. In a consumer install the same dir IS the build-infra netapp
|
|
69
|
+
* cache, so clearing it would delete every standard module. (Before
|
|
70
|
+
* celilo#1142 published netapps lived in the registry's container layer and
|
|
71
|
+
* died with the container, so starting empty is the behaviour being kept, not
|
|
72
|
+
* a new one.)
|
|
38
73
|
*/
|
|
74
|
+
export function prepareRegistryDropZone(): void {
|
|
75
|
+
const dir = registryUploadsHostDir();
|
|
76
|
+
mkdirSync(dir, { recursive: true });
|
|
77
|
+
if (!isMonorepoInstall()) return;
|
|
78
|
+
for (const file of readdirSync(dir)) {
|
|
79
|
+
if (file.endsWith('.netapp')) rmSync(join(dir, file), { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
function getRegistryVolumes(): string[] {
|
|
40
|
-
const
|
|
41
|
-
return
|
|
84
|
+
const uploads = `./${basename(registryUploadsHostDir())}:/uploads:ro`;
|
|
85
|
+
return isMonorepoInstall() ? ['../../modules:/modules:ro', uploads] : [uploads];
|
|
42
86
|
}
|
|
43
87
|
|
|
44
88
|
const NETWORK_DRIVER_OPTS = {
|
|
@@ -94,6 +138,12 @@ function baseService(opts: {
|
|
|
94
138
|
environment?: Record<string, string>;
|
|
95
139
|
tmpfs?: string[];
|
|
96
140
|
privileged?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Docker `security_opt` entries, e.g. `seccomp=unconfined`. Needed by the
|
|
143
|
+
* management box so the hook jail's bubblewrap can build a namespace inside
|
|
144
|
+
* a container — see the measured ladder at its call site.
|
|
145
|
+
*/
|
|
146
|
+
security_opt?: string[];
|
|
97
147
|
dns?: string[];
|
|
98
148
|
/** Host devices to expose, e.g. `/dev/net/tun` for a userspace WireGuard tunnel. */
|
|
99
149
|
devices?: string[];
|
|
@@ -215,7 +265,13 @@ function defaultTopologyServices(config: NetworkConfig): Record<string, unknown>
|
|
|
215
265
|
volumes: ['ssh-keys:/ssh-keys:ro'],
|
|
216
266
|
// Which real ISP router this sim stands in for. The protocol is identical
|
|
217
267
|
// across the two; only the TR-181 vendor extension prefix differs.
|
|
218
|
-
environment: {
|
|
268
|
+
environment: {
|
|
269
|
+
ROUTER_VENDOR_PREFIX: config.routerVendorPrefix ?? 'X_GWS_',
|
|
270
|
+
// The simulated router serves DHCP on `internal` by default, which is
|
|
271
|
+
// what an operator's router does. A suite testing celilo's OWN DHCP
|
|
272
|
+
// turns it off, because two servers on one broadcast domain race.
|
|
273
|
+
ROUTER_DHCP: config.routerDhcp === false ? 'off' : 'on',
|
|
274
|
+
},
|
|
219
275
|
}),
|
|
220
276
|
};
|
|
221
277
|
}
|
|
@@ -568,7 +624,34 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
|
|
|
568
624
|
image:
|
|
569
625
|
variant === 'vanilla' ? 'celilo-e2e/management:vanilla' : 'celilo-e2e/management:latest',
|
|
570
626
|
networks: { [zone]: { ipv4_address: ip } },
|
|
571
|
-
|
|
627
|
+
// SYS_ADMIN is for the hook jail, NET_ADMIN for the routing the box does
|
|
628
|
+
// anyway. See the security_opt block below for why both are here.
|
|
629
|
+
cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
|
|
630
|
+
// The hook jail runs each hook under bubblewrap, which has to build a
|
|
631
|
+
// user namespace and then mount inside it. Docker denies that four
|
|
632
|
+
// separate ways, and every one of these is load-bearing — measured on a
|
|
633
|
+
// colima Ubuntu 24.04.1 VM (kernel 6.8.0-50,
|
|
634
|
+
// kernel.apparmor_restrict_unprivileged_userns=1), by removing one at a
|
|
635
|
+
// time from the working set:
|
|
636
|
+
//
|
|
637
|
+
// drop seccomp=unconfined -> bwrap: pivot_root: Operation not permitted
|
|
638
|
+
// drop apparmor=unconfined -> bwrap: Failed to make / slave: Permission denied
|
|
639
|
+
// drop systempaths=unconfined -> bwrap: Can't mount proc on /newroot/proc
|
|
640
|
+
// drop SYS_ADMIN -> bwrap: setting up uid map: Permission denied
|
|
641
|
+
//
|
|
642
|
+
// Deliberately NOT `privileged: true`, which design D8 measured as
|
|
643
|
+
// working and task 4.1 asked to improve on. It would also grant every
|
|
644
|
+
// capability and every host device; this set grants four named things.
|
|
645
|
+
//
|
|
646
|
+
// `systempaths=unconfined` is the one D8's ladder was missing: it unmasks
|
|
647
|
+
// the /proc paths Docker hides, which is why "all three" of the earlier
|
|
648
|
+
// options still failed on the proc mount.
|
|
649
|
+
//
|
|
650
|
+
// NOTE this makes the container a WEAK place to test the AppArmor
|
|
651
|
+
// profile: apparmor=unconfined means the restriction the profile exists
|
|
652
|
+
// to satisfy is not in force here. That gate is the celilo-builder probe
|
|
653
|
+
// on a real kernel, never this container.
|
|
654
|
+
security_opt: ['seccomp=unconfined', 'apparmor=unconfined', 'systempaths=unconfined'],
|
|
572
655
|
// ISS-0157: `module import`'s `bun install` hard-links packages from the
|
|
573
656
|
// bun cache into node_modules, which on overlayfs forces a copy-up + fsync
|
|
574
657
|
// per file. On a slow/contended builder disk that storm blows past the
|
package/src/network-builder.ts
CHANGED
|
@@ -140,6 +140,23 @@ export class NetworkBuilder {
|
|
|
140
140
|
return this;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Stop the simulated ISP router serving DHCP on `internal`.
|
|
145
|
+
*
|
|
146
|
+
* For suites where CELILO serves DHCP (`modules/dnsmasq-dhcp`). Two servers
|
|
147
|
+
* on one broadcast domain race, so without this a lease assertion is a coin
|
|
148
|
+
* toss. It is also what an operator does when moving DHCP to celilo, so the
|
|
149
|
+
* suite models the migration rather than dodging a conflict.
|
|
150
|
+
*
|
|
151
|
+
* NOTE: `withDhcpClient()`'s start-up lease wait expects SOMETHING to answer.
|
|
152
|
+
* Pair this with a suite that deploys celilo's DHCP server, or the client
|
|
153
|
+
* gets no lease and the network never comes up.
|
|
154
|
+
*/
|
|
155
|
+
withoutRouterDhcp(): this {
|
|
156
|
+
this.config.routerDhcp = false;
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
/**
|
|
144
161
|
* Add the Proxmox API simulator on `secure-mgmt`.
|
|
145
162
|
*
|
package/src/shared-infra.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SHARED_NETWORKS,
|
|
18
18
|
SHARED_PROJECT_NAME,
|
|
19
19
|
generateSharedInfraYaml,
|
|
20
|
+
prepareRegistryDropZone,
|
|
20
21
|
} from './docker-compose-generator';
|
|
21
22
|
import { ensureRegistryServerBundle, ensureTerraformFakeBundle } from './registry-bundle';
|
|
22
23
|
|
|
@@ -161,6 +162,11 @@ export async function ensureSharedInfra(): Promise<void> {
|
|
|
161
162
|
const yaml = generateSharedInfraYaml();
|
|
162
163
|
writeFileSync(join(e2eDir, SHARED_COMPOSE_FILE), yaml);
|
|
163
164
|
|
|
165
|
+
// The registry's /uploads bind source must exist before `up` (a missing
|
|
166
|
+
// bind source becomes an empty root-owned dir), and in the monorepo it
|
|
167
|
+
// starts empty so no stale .netapp shadows live module source.
|
|
168
|
+
prepareRegistryDropZone();
|
|
169
|
+
|
|
164
170
|
// Seed the live DNS zone files from their templates BEFORE the compose
|
|
165
171
|
// mounts them. config/dns/{iamtheinternet.org,example.net}.zone are
|
|
166
172
|
// gitignored runtime state (scrubDnsZones rewrites them per-test); on a
|
package/src/types.ts
CHANGED
|
@@ -66,6 +66,19 @@ export interface NetworkConfig {
|
|
|
66
66
|
* serves both rather than a near-duplicate second one.
|
|
67
67
|
*/
|
|
68
68
|
routerVendorPrefix?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Whether the simulated ISP router serves DHCP on `internal`. Defaults to
|
|
71
|
+
* true, which is what an operator's router does and what every existing suite
|
|
72
|
+
* relies on.
|
|
73
|
+
*
|
|
74
|
+
* Set false when celilo serves DHCP instead (`modules/dnsmasq-dhcp`). Two
|
|
75
|
+
* DHCP servers on one broadcast domain race — whichever answers a DISCOVER
|
|
76
|
+
* first wins — so a suite asserting WHICH server issued a lease would be
|
|
77
|
+
* asserting a coin toss. Turning the router's off is also what a real
|
|
78
|
+
* operator does when moving DHCP to celilo, so this models the migration
|
|
79
|
+
* rather than papering over a conflict.
|
|
80
|
+
*/
|
|
81
|
+
routerDhcp?: boolean;
|
|
69
82
|
/** Extra volume mounts for the management container (host:container format) */
|
|
70
83
|
managementVolumes: string[];
|
|
71
84
|
/** Include a DHCP client container on the internal network */
|