@celilo/e2e 0.19.2 → 0.20.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 +30 -13
- package/bin/e2e-bake-management +171 -12
- package/bin/e2e-infra +0 -1
- package/bin/e2e-up +14 -3
- package/docker/Dockerfile.observer +12 -1
- package/docker/Dockerfile.target-machine +22 -1
- package/npm-registry-server/package.json +1 -1
- package/package.json +3 -3
- package/registry-server/package.json +1 -1
- package/scripts/pack-celilo-packages.ts +15 -0
- package/src/block-timing.test.ts +559 -0
- package/src/block-timing.ts +366 -0
- package/src/cli/build.test.ts +54 -4
- package/src/cli/build.ts +204 -88
- package/src/cli/command-registry.ts +21 -0
- package/src/cli/command-tree-parser.ts +11 -2
- package/src/cli/completion.ts +9 -0
- package/src/cli/host.ts +252 -0
- package/src/cli/index.ts +78 -51
- package/src/cli/module-discovery.ts +108 -13
- package/src/cli/scaffold.ts +18 -26
- package/src/container-manager.cleanup.test.ts +284 -0
- package/src/container-manager.runner.test.ts +351 -0
- package/src/container-manager.test.ts +84 -0
- package/src/container-manager.ts +721 -185
- package/src/docker-compose-generator.ts +135 -61
- package/src/doctor.test.ts +259 -4
- package/src/doctor.ts +276 -3
- package/src/exit-cleanup.test.ts +83 -1
- package/src/fleet-nameserver-gate.test.ts +45 -0
- package/src/host-vm.test.ts +156 -0
- package/src/host-vm.ts +230 -0
- package/src/index.ts +11 -0
- package/src/live-stack.test.ts +184 -0
- package/src/live-stack.ts +145 -0
- package/src/no-unjustified-sleep.test.ts +90 -0
- package/src/proxmox-provisioner.test.ts +18 -2
- package/src/proxmox-provisioner.ts +22 -0
- package/src/public-sim-routes.test.ts +9 -2
- package/src/repo-root.ts +33 -0
- package/src/run-args.test.ts +76 -0
- package/src/run-args.ts +89 -0
- package/src/runner.ts +213 -8
- package/src/shared-infra.ts +83 -32
- package/src/socks-proxy.ts +2 -0
- package/src/source-fingerprint.test.ts +213 -0
- package/src/source-fingerprint.ts +201 -0
- package/src/stage-simulator-inputs.ts +93 -0
- package/src/stages.ts +133 -0
- package/src/wait-for-run.ts +1 -0
package/README.md
CHANGED
|
@@ -281,10 +281,27 @@ describe('my deployment test', () => {
|
|
|
281
281
|
| `exec(container, cmd, timeout?)` | Execute a command in any container |
|
|
282
282
|
| `dig(name)` | Resolve a DNS name from management |
|
|
283
283
|
| `waitFor(check, timeout, label)` | Poll until a condition is true |
|
|
284
|
+
| `debug(container?)` | Pause the test and shell into a container (default `management`); exit resumes ([details](#mid-test-pause)) |
|
|
284
285
|
| `socksProxy(opts?)` | Spawn a SOCKS5 proxy attached to the test network ([details](#browser-driven-tests)) |
|
|
285
286
|
| `browser(opts)` | Launch a host-side browser routed into the network ([details](#browser-driven-tests)) |
|
|
286
287
|
| `stop()` | Tear down the entire network |
|
|
287
288
|
|
|
289
|
+
### Mid-test pause
|
|
290
|
+
|
|
291
|
+
`net.debug()` freezes a running suite at the line that calls it and hands you a
|
|
292
|
+
bash shell inside the named container (default `management`). Type `exit` and
|
|
293
|
+
the test resumes. In a non-interactive run it instead prints
|
|
294
|
+
`Resume: touch <signalFile>` and waits, so you can probe the stack from another
|
|
295
|
+
terminal and resume by touching the file.
|
|
296
|
+
|
|
297
|
+
This shows live state at the exact failing moment, which `--keep` cannot: a
|
|
298
|
+
kept stack is only available after the suite finished, once later stages have
|
|
299
|
+
already mutated it.
|
|
300
|
+
|
|
301
|
+
The pause holds the machine-global run lock for up to 24 hours, so a forgotten
|
|
302
|
+
debug session blocks every other session's run. Resume promptly. See
|
|
303
|
+
`docs/RUNNING_CELE2E_TESTS.md` for the full write-up.
|
|
304
|
+
|
|
288
305
|
### Browser-driven tests
|
|
289
306
|
|
|
290
307
|
`net.browser()` returns a Playwright `Browser` whose traffic enters the simulated network through a containerized SOCKS5 proxy, so a host-side browser sees the deployed app exactly as a real user would (DNS, TLS, ingress firewall, all in the loop).
|
|
@@ -347,20 +364,18 @@ const net = await network()
|
|
|
347
364
|
|
|
348
365
|
### Running Tests
|
|
349
366
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
# Run all E2E tests
|
|
354
|
-
bun run vitest run
|
|
355
|
-
|
|
356
|
-
# Run a specific test
|
|
357
|
-
bun run vitest run tests/caddy-deploy.test.ts
|
|
367
|
+
The repo's own tests live in `e2e/tests/` at the repo root. They resolve
|
|
368
|
+
`@celilo/e2e` through the root workspace install, so there is nothing to
|
|
369
|
+
install in `e2e/` itself.
|
|
358
370
|
|
|
359
|
-
|
|
360
|
-
|
|
371
|
+
```bash
|
|
372
|
+
# From the repo root, after `bun run setup`:
|
|
373
|
+
cele2e run <test-name> # Docker-backed suite, e.g. `cele2e run machine-add`
|
|
374
|
+
cele2e run --all # every module suite + the top-level e2e/tests/
|
|
361
375
|
```
|
|
362
376
|
|
|
363
|
-
|
|
377
|
+
See `docs/RUNNING_CELE2E_TESTS.md` for the full operator guide (run lock,
|
|
378
|
+
build-infra triggers, `--keep` debugging).
|
|
364
379
|
|
|
365
380
|
## Key Design Decisions
|
|
366
381
|
|
|
@@ -368,9 +383,11 @@ Tests run sequentially (fixed CIDRs prevent parallelism). The container manager
|
|
|
368
383
|
|
|
369
384
|
**No test parallelism:** Fixed network CIDRs mean only one test network can run at a time. Tests run sequentially via Vitest's `singleFork` config.
|
|
370
385
|
|
|
371
|
-
**Bind-mounted source:** The celilo source tree is bind-mounted at `/celilo` on the management container
|
|
386
|
+
**Bind-mounted source:** The celilo source tree is bind-mounted at `/celilo` on the management container, which is where module sources are read from (`celilo module import /celilo/modules/<id>`).
|
|
387
|
+
|
|
388
|
+
The CLI itself is NOT read from there. Tests run the `celilo` baked into the management image by `cele2e build-infra` — the artifact install.sh produces — so **a change under `apps/celilo/` or `packages/` needs a `build-infra` before it reaches a test.** `cele2e doctor` compares a fingerprint stamped on the image against the working tree and warns when they differ, and `cele2e run --source-cli` runs the mounted source instead for iteration. Running the source roughly doubles each celilo command's start-up (measured `celilo --version`: 0.14s installed against 0.31s from the mount on virtiofs, 0.16s against 0.56s on sshfs).
|
|
372
389
|
|
|
373
|
-
**Docker image caching:** Images are built once and cached. Only config/simulator changes require rebuilds.
|
|
390
|
+
**Docker image caching:** Images are built once and cached. Only config/simulator changes require rebuilds. A run skips `docker compose build` entirely when every image it needs already exists — that check costs one `docker images` listing, against about 0.7s per service for a build that rebuilds nothing.
|
|
374
391
|
|
|
375
392
|
**ACME via DNS interception:** Instead of overriding Caddy's ACME URL, `acme-v02.api.letsencrypt.org` resolves to Pebble (100.64.0.100) in the simulated DNS. The caddy module's `acme_ca` variable allows pointing to Pebble's `/dir` endpoint (vs Let's Encrypt's `/directory`).
|
|
376
393
|
|
package/bin/e2e-bake-management
CHANGED
|
@@ -26,6 +26,17 @@ import { execSync } from 'node:child_process';
|
|
|
26
26
|
import { join } from 'node:path';
|
|
27
27
|
import { ensureSharedInfra } from '../src/shared-infra';
|
|
28
28
|
import { network } from '../src/network-builder';
|
|
29
|
+
import { imageTag } from '../src/docker-compose-generator';
|
|
30
|
+
import { findMonorepoRoot } from '../src/repo-root';
|
|
31
|
+
import {
|
|
32
|
+
packNpmRegistryTarballs,
|
|
33
|
+
stageWebsiteDist,
|
|
34
|
+
} from '../src/stage-simulator-inputs';
|
|
35
|
+
import {
|
|
36
|
+
PUBLISHED_FINGERPRINT_PREFIX,
|
|
37
|
+
SOURCE_LABEL,
|
|
38
|
+
} from '../src/source-fingerprint';
|
|
39
|
+
import { readFileSync } from 'node:fs';
|
|
29
40
|
|
|
30
41
|
const PACKAGE_ROOT = join(import.meta.dir, '..');
|
|
31
42
|
const COMPOSE_FILE = join(PACKAGE_ROOT, 'docker-compose.test.yml');
|
|
@@ -42,13 +53,40 @@ interface ExecResult {
|
|
|
42
53
|
type Exec = (cmd: string, timeoutMs?: number) => Promise<ExecResult>;
|
|
43
54
|
|
|
44
55
|
/**
|
|
45
|
-
* Write the
|
|
46
|
-
*
|
|
56
|
+
* Write the CLI shim to /usr/local/bin/celilo and verify the CLI works through
|
|
57
|
+
* it. Shared by both bake modes.
|
|
47
58
|
*
|
|
48
59
|
* The shim (at /usr/local/bin/celilo so plain `docker exec bash -c celilo`
|
|
49
|
-
* finds it)
|
|
50
|
-
*
|
|
51
|
-
*
|
|
60
|
+
* finds it) runs the INSTALLED CLI at /root/.bun/bin/celilo — the artifact
|
|
61
|
+
* install.sh just produced, which is the thing the rig exists to test. It runs
|
|
62
|
+
* the mounted /celilo workspace instead only when `CELILO_E2E_SOURCE_CLI` is
|
|
63
|
+
* set, which `cele2e run --source-cli` does.
|
|
64
|
+
*
|
|
65
|
+
* That default used to be the other way round, and both halves of the swap are
|
|
66
|
+
* worth stating.
|
|
67
|
+
*
|
|
68
|
+
* It cost real time per invocation. `bun run <source>` resolves 1522 modules
|
|
69
|
+
* across the bind mount rather than starting an installed binary. Measured
|
|
70
|
+
* inside this image on 2026-09-05, same command and same container, on a VM
|
|
71
|
+
* sized per the host policy: `celilo --version` took 0.14s installed and 0.31s
|
|
72
|
+
* from the mounted source over virtiofs, or 0.16s against 0.56s over sshfs. A
|
|
73
|
+
* test issues 25 to 40 celilo commands.
|
|
74
|
+
*
|
|
75
|
+
* (An earlier measurement put that gap at 2.3s. Most of that was a separate
|
|
76
|
+
* problem — the VM had been given 24 of the host's 32 GiB and the host was
|
|
77
|
+
* swapping 21.9 GiB. Fixing the memory removed about 1.9s of it. Worth stating,
|
|
78
|
+
* because a number measured with two variables moving blamed the wrong one.)
|
|
79
|
+
*
|
|
80
|
+
* And it made the verify below dishonest. This function exists because of
|
|
81
|
+
* ISS-0124 — a broken shim fallback shipping green — and its comment says the
|
|
82
|
+
* verify runs "with NO /celilo mount, i.e. exactly the shipped path". That was
|
|
83
|
+
* true of the published bake, which runs in a standalone container, and false
|
|
84
|
+
* of this one: the sim bake's container HAS the workspace mounted, so the old
|
|
85
|
+
* shim sent the verify down the source branch and the installed CLI was never
|
|
86
|
+
* executed at all. The check could not reach its subject. Now both modes verify
|
|
87
|
+
* the installed binary.
|
|
88
|
+
*
|
|
89
|
+
* Two things must hold, both bitten in ISS-0124:
|
|
52
90
|
*
|
|
53
91
|
* 1. `bun add -g` links the celilo binary at /usr/local/bin/celilo as a
|
|
54
92
|
* SYMLINK into the package (bun's global bin dir in this image is
|
|
@@ -65,12 +103,14 @@ type Exec = (cmd: string, timeoutMs?: number) => Promise<ExecResult>;
|
|
|
65
103
|
* shipped path — so a dangling/looping fallback fails the bake instead of
|
|
66
104
|
* shipping green.
|
|
67
105
|
*/
|
|
68
|
-
async function installShimAndVerify(exec: Exec, label: string): Promise<
|
|
106
|
+
async function installShimAndVerify(exec: Exec, label: string): Promise<string> {
|
|
69
107
|
process.stdout.write(' source-mount shim ...... ');
|
|
70
108
|
const t0 = Date.now();
|
|
71
109
|
const shim = [
|
|
72
110
|
'#!/bin/bash',
|
|
73
|
-
'
|
|
111
|
+
'# Opt-in source loop: `cele2e run --source-cli` sets this in the container',
|
|
112
|
+
'# environment. Unset (the default) runs the CLI install.sh actually installed.',
|
|
113
|
+
'if [ -n "$CELILO_E2E_SOURCE_CLI" ] && [ -d /celilo/apps/celilo ]; then',
|
|
74
114
|
' exec bun run /celilo/apps/celilo/src/cli/index.ts "$@"',
|
|
75
115
|
'fi',
|
|
76
116
|
'exec /root/.bun/bin/celilo "$@"',
|
|
@@ -101,6 +141,113 @@ async function installShimAndVerify(exec: Exec, label: string): Promise<void> {
|
|
|
101
141
|
);
|
|
102
142
|
}
|
|
103
143
|
console.log(`✔ ${Math.round((Date.now() - t1) / 1000)}s (${verify.stdout.trim()})`);
|
|
144
|
+
return verify.stdout.trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The image id currently on a tag, or '' when the tag is unused.
|
|
149
|
+
*
|
|
150
|
+
* Captured before a commit so the superseded image can be removed after it.
|
|
151
|
+
* Every bake used to orphan a 1.5 GB untagged image and nothing ever collected
|
|
152
|
+
* them: measured 2026-09-05, 3178 dangling images holding 25.8 GB, 1478 of them
|
|
153
|
+
* from a single 35-hour window. That pile is what drives an operator to reach
|
|
154
|
+
* for `docker image prune -a`, which deletes the TAGGED base images the next
|
|
155
|
+
* build needs and costs a full 27-image rebuild.
|
|
156
|
+
*/
|
|
157
|
+
function imageIdOnTag(tag: string): string {
|
|
158
|
+
try {
|
|
159
|
+
return run(`docker image inspect --format '{{.Id}}' ${tag}`).trim();
|
|
160
|
+
} catch {
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Remove an image the bake just superseded. Best-effort: a tag or container still using it wins. */
|
|
166
|
+
function removeSupersededImage(previousId: string, tag: string): void {
|
|
167
|
+
if (!previousId || previousId === imageIdOnTag(tag)) return;
|
|
168
|
+
try {
|
|
169
|
+
run(`docker image rm ${previousId}`);
|
|
170
|
+
console.log(` superseded image ....... ✔ removed ${previousId.slice(7, 19)}`);
|
|
171
|
+
} catch {
|
|
172
|
+
// Still referenced (a running container, another tag). Leaving it is correct.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The sim tarballs the default bake installs, staged fresh by
|
|
178
|
+
* restageSimulatorInputs, carry a manifest recording the tree fingerprint they
|
|
179
|
+
* were packed from. The stamp IS that value: the label then describes the
|
|
180
|
+
* tarballs install.sh actually installs, not the tree the bake happened to run
|
|
181
|
+
* in. Stamping `computeSourceFingerprint(tree)` here is what produced a green
|
|
182
|
+
* doctor over a CLI without the tree's changes (celilo#1299): the tree moved
|
|
183
|
+
* between the last `cele2e build` and the bake, and the fingerprint followed
|
|
184
|
+
* the tree, not the stale cache the bake reinstalled.
|
|
185
|
+
*/
|
|
186
|
+
function simStampFromManifest(): string {
|
|
187
|
+
const manifestPath = join(PACKAGE_ROOT, '.npm-registry-cache', 'pack-manifest.json');
|
|
188
|
+
let manifest: { sourceFingerprint?: string | null };
|
|
189
|
+
try {
|
|
190
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as {
|
|
191
|
+
sourceFingerprint?: string | null;
|
|
192
|
+
};
|
|
193
|
+
} catch {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`No readable pack-manifest.json at ${manifestPath} — the sim tarballs were never staged. ` +
|
|
196
|
+
'Run the bake from a monorepo checkout; restageSimulatorInputs writes the manifest.',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (!manifest.sourceFingerprint) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'pack-manifest.json carries no sourceFingerprint — the tarballs this bake installs cannot be attributed to a tree.',
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return manifest.sourceFingerprint;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Stamp the source this image was baked from, so `cele2e doctor` can say
|
|
209
|
+
* whether the CLI under test is the code in the working tree. See
|
|
210
|
+
* `src/source-fingerprint.ts` for why that has to be measured, not remembered.
|
|
211
|
+
*/
|
|
212
|
+
function sourceStamp(published: boolean, version: string): string {
|
|
213
|
+
if (published) return `${PUBLISHED_FINGERPRINT_PREFIX}${version || 'unknown'}`;
|
|
214
|
+
return simStampFromManifest();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Re-stage the simulator inputs the default bake's install path depends on,
|
|
219
|
+
* and rebuild the two sim images that COPY them at docker-build time.
|
|
220
|
+
*
|
|
221
|
+
* Without this, the bake reinstalls whatever tarballs the last `cele2e build`
|
|
222
|
+
* left in `.npm-registry-cache/` — measured 2026-09-07: a bake at 00:17 UTC
|
|
223
|
+
* shipped @celilo/cli 2.1.0 from a cache packed at 15:18 the day before,
|
|
224
|
+
* silently dropping a CLI fix that had landed at 19:48 (celilo#1299). The
|
|
225
|
+
* rebuilds are not optional either: the per-test stack only builds MISSING
|
|
226
|
+
* images, so fresh tarballs on disk never reach the running sim otherwise.
|
|
227
|
+
* Both docker builds are cache-warm no-ops when nothing staged has changed.
|
|
228
|
+
*/
|
|
229
|
+
function restageSimulatorInputs(): void {
|
|
230
|
+
const repoRoot = findMonorepoRoot(PACKAGE_ROOT);
|
|
231
|
+
if (!repoRoot) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
'The default bake installs the monorepo DEV cli via the sim registry — it needs a celilo checkout. For a published-CLI bake use --published.',
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
stageWebsiteDist(repoRoot, PACKAGE_ROOT);
|
|
237
|
+
packNpmRegistryTarballs(repoRoot, PACKAGE_ROOT);
|
|
238
|
+
for (const dockerfile of [
|
|
239
|
+
'docker/Dockerfile.celilo-website-sim',
|
|
240
|
+
'docker/Dockerfile.npm-registry-sim',
|
|
241
|
+
]) {
|
|
242
|
+
const tag = imageTag(dockerfile);
|
|
243
|
+
process.stdout.write(` rebuild ${tag.padEnd(22)} `);
|
|
244
|
+
const t0 = Date.now();
|
|
245
|
+
execSync(
|
|
246
|
+
`docker build -q -f ${join(PACKAGE_ROOT, dockerfile)} -t ${tag} ${PACKAGE_ROOT}`,
|
|
247
|
+
{ stdio: 'pipe' },
|
|
248
|
+
);
|
|
249
|
+
console.log(`✔ ${Math.round((Date.now() - t0) / 1000)}s`);
|
|
250
|
+
}
|
|
104
251
|
}
|
|
105
252
|
|
|
106
253
|
/**
|
|
@@ -116,6 +263,11 @@ async function bakeViaSim(): Promise<void> {
|
|
|
116
263
|
await ensureSharedInfra();
|
|
117
264
|
console.log(`✔ ${Math.round((Date.now() - t0) / 1000)}s`);
|
|
118
265
|
|
|
266
|
+
// Fresh tarballs and site dist, and sim images that actually carry them,
|
|
267
|
+
// BEFORE install.sh runs — otherwise this bake reinstalls the last build's
|
|
268
|
+
// CLI (celilo#1299).
|
|
269
|
+
restageSimulatorInputs();
|
|
270
|
+
|
|
119
271
|
process.stdout.write(' per-test stack ......... ');
|
|
120
272
|
const t1 = Date.now();
|
|
121
273
|
const net = await network().topology('default').managementVariant('vanilla').start();
|
|
@@ -166,8 +318,9 @@ async function bakeViaSim(): Promise<void> {
|
|
|
166
318
|
}
|
|
167
319
|
console.log(`✔ ${Math.round((Date.now() - t2) / 1000)}s`);
|
|
168
320
|
|
|
169
|
-
// Shim + verify
|
|
170
|
-
|
|
321
|
+
// Shim + verify. Post-shim and, since the shim now prefers the installed
|
|
322
|
+
// CLI, genuinely against the installed binary rather than the mount.
|
|
323
|
+
const version = await installShimAndVerify(exec, 'sim');
|
|
171
324
|
|
|
172
325
|
process.stdout.write(' docker commit .......... ');
|
|
173
326
|
const t5 = Date.now();
|
|
@@ -177,8 +330,12 @@ async function bakeViaSim(): Promise<void> {
|
|
|
177
330
|
if (!containerId) {
|
|
178
331
|
throw new Error('Could not resolve management container id');
|
|
179
332
|
}
|
|
180
|
-
|
|
333
|
+
const superseded = imageIdOnTag('celilo-e2e/management:latest');
|
|
334
|
+
run(
|
|
335
|
+
`docker commit --change ${JSON.stringify(`LABEL ${SOURCE_LABEL}=${sourceStamp(false, version)}`)} ${containerId} celilo-e2e/management:latest`,
|
|
336
|
+
);
|
|
181
337
|
console.log(`✔ ${Math.round((Date.now() - t5) / 1000)}s`);
|
|
338
|
+
removeSupersededImage(superseded, 'celilo-e2e/management:latest');
|
|
182
339
|
} finally {
|
|
183
340
|
process.stdout.write(' tearing down ........... ');
|
|
184
341
|
const tDown = Date.now();
|
|
@@ -265,7 +422,7 @@ async function bakeViaPublished(): Promise<void> {
|
|
|
265
422
|
console.log(`✔ ${Math.round((Date.now() - t2) / 1000)}s`);
|
|
266
423
|
|
|
267
424
|
// Shim + verify (verify is post-shim, no mount — the shipped path).
|
|
268
|
-
await installShimAndVerify(dexec, 'published');
|
|
425
|
+
const version = await installShimAndVerify(dexec, 'published');
|
|
269
426
|
|
|
270
427
|
// Restore the sim @celilo scope override so the committed image is
|
|
271
428
|
// drop-in compatible with the simulated topology it runs tests in.
|
|
@@ -280,10 +437,12 @@ async function bakeViaPublished(): Promise<void> {
|
|
|
280
437
|
// real entrypoint (Dockerfile.management: CMD ["/startup.sh"], no
|
|
281
438
|
// ENTRYPOINT). Without this the baked image never runs /startup.sh ->
|
|
282
439
|
// ssh-keys volume stays empty -> fw-main crashloops (ce-um6).
|
|
440
|
+
const superseded = imageIdOnTag('celilo-e2e/management:latest');
|
|
283
441
|
run(
|
|
284
|
-
`docker commit --change 'CMD ["/startup.sh"]' ${name} celilo-e2e/management:latest`,
|
|
442
|
+
`docker commit --change 'CMD ["/startup.sh"]' --change ${JSON.stringify(`LABEL ${SOURCE_LABEL}=${sourceStamp(true, version)}`)} ${name} celilo-e2e/management:latest`,
|
|
285
443
|
);
|
|
286
444
|
console.log(`✔ ${Math.round((Date.now() - t3) / 1000)}s`);
|
|
445
|
+
removeSupersededImage(superseded, 'celilo-e2e/management:latest');
|
|
287
446
|
} finally {
|
|
288
447
|
process.stdout.write(' removing container ..... ');
|
|
289
448
|
const t4 = Date.now();
|
package/bin/e2e-infra
CHANGED
|
@@ -34,7 +34,6 @@ case "${1:-status}" in
|
|
|
34
34
|
console.log(generateSharedInfraYaml());
|
|
35
35
|
" > "$SHARED_COMPOSE"
|
|
36
36
|
|
|
37
|
-
docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" build
|
|
38
37
|
docker compose -f "$SHARED_COMPOSE" -p "$SHARED_PROJECT" up -d
|
|
39
38
|
|
|
40
39
|
echo -e "${DIM}Waiting for DNS convergence...${NC}"
|
package/bin/e2e-up
CHANGED
|
@@ -96,9 +96,20 @@ if (customJson) {
|
|
|
96
96
|
console.log(generateComposeYaml(config));
|
|
97
97
|
" > docker-compose.yml
|
|
98
98
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
# Verify baked images. The compose carries `image:` only, so nothing here
|
|
100
|
+
# builds — a missing tag would surface as a confusing "pull access denied"
|
|
101
|
+
# from `up`. Name the remedy instead.
|
|
102
|
+
echo "Verifying baked images (suite time never builds)..."
|
|
103
|
+
MISSING=()
|
|
104
|
+
while read -r tag; do
|
|
105
|
+
[ -n "$tag" ] || continue
|
|
106
|
+
docker image inspect "$tag" >/dev/null 2>&1 || MISSING+=("$tag")
|
|
107
|
+
done < <(awk '/image: celilo-e2e\//{print $3}' docker-compose.yml | sort -u)
|
|
108
|
+
if [ ${#MISSING[@]} -gt 0 ]; then
|
|
109
|
+
echo "ERROR: ${#MISSING[@]} baked image(s) missing: ${MISSING[*]}"
|
|
110
|
+
echo "Remedy: cele2e build-infra"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
102
113
|
|
|
103
114
|
echo "Starting containers..."
|
|
104
115
|
docker compose -p "$PROJECT_NAME" up -d
|
|
@@ -10,13 +10,24 @@ FROM ubuntu:22.04
|
|
|
10
10
|
|
|
11
11
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# `--no-install-recommends` is load-bearing: `wireguard-tools` Recommends
|
|
14
|
+
# `wireguard-modules | wireguard-dkms`, which apt satisfies with a realtime
|
|
15
|
+
# KERNEL plus `linux-firmware` (1.1 GB). Measured 2026-09-05: 1.83 GB of this
|
|
16
|
+
# image's 1.9 GB was that, for a probe toolbox that runs no kernel at all.
|
|
17
|
+
# See the same note in Dockerfile.target-machine.
|
|
18
|
+
#
|
|
19
|
+
# `iptables` is named because it used to arrive via `wireguard-tools`' other
|
|
20
|
+
# Recommends. This image dials a real tunnel (`wg-quick up wg0`), and wg-quick
|
|
21
|
+
# reaches for `iptables-save` behind a `type -p` guard — so losing it would not
|
|
22
|
+
# fail, it would silently skip part of teardown.
|
|
23
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
14
24
|
dnsutils \
|
|
15
25
|
curl \
|
|
16
26
|
openssl \
|
|
17
27
|
netcat-openbsd \
|
|
18
28
|
iputils-ping \
|
|
19
29
|
iproute2 \
|
|
30
|
+
iptables \
|
|
20
31
|
ca-certificates \
|
|
21
32
|
wireguard-tools \
|
|
22
33
|
wireguard-go \
|
|
@@ -69,10 +69,31 @@ RUN systemctl enable ssh target-setup
|
|
|
69
69
|
# --- Caddy pre-installation ---
|
|
70
70
|
# Install Caddy (same steps as the Ansible role, but at image build time so
|
|
71
71
|
# the role finds it already installed and skips the slow cloudsmith.io downloads)
|
|
72
|
-
|
|
72
|
+
#
|
|
73
|
+
# `--no-install-recommends` is load-bearing here, not tidiness. `wireguard-tools`
|
|
74
|
+
# Recommends `wireguard-modules | wireguard-dkms`, and apt satisfies that with
|
|
75
|
+
# a KERNEL: `linux-modules-extra-*-realtime`, which drags in the realtime kernel
|
|
76
|
+
# image, its modules and `linux-firmware`. Measured 2026-09-05: that one line
|
|
77
|
+
# added 1.76 GB to this image (2.08 GB total), of which linux-firmware alone was
|
|
78
|
+
# 1.1 GB. A container cannot load a kernel module, and wg-quick already falls
|
|
79
|
+
# back to the userspace `wireguard-go` below, so none of it was ever used.
|
|
80
|
+
# The price of `--no-install-recommends` is that anything this image USED to get
|
|
81
|
+
# for free has to be named. Two things did, and both are load-bearing:
|
|
82
|
+
#
|
|
83
|
+
# gnupg — the very next step pipes the caddy signing key through `gpg
|
|
84
|
+
# --dearmor`. Without it the build dies with `gpg: not found`,
|
|
85
|
+
# which is how this was found: by building it.
|
|
86
|
+
# iptables — arrived via `wireguard-tools`' other Recommends
|
|
87
|
+
# (`nftables | iptables`). The wireguard module's health check
|
|
88
|
+
# runs `iptables-save` on the host it lands on, and wg-quick
|
|
89
|
+
# consults `iptables-save` on teardown behind a `type -p` guard,
|
|
90
|
+
# so its absence would not error — it would quietly do less.
|
|
91
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
73
92
|
debian-keyring \
|
|
74
93
|
debian-archive-keyring \
|
|
75
94
|
apt-transport-https \
|
|
95
|
+
gnupg \
|
|
96
|
+
iptables \
|
|
76
97
|
wireguard-tools \
|
|
77
98
|
wireguard-go \
|
|
78
99
|
&& rm -rf /var/lib/apt/lists/*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
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
|
"README.md"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@celilo/capabilities": "^4.
|
|
40
|
+
"@celilo/capabilities": "^4.2.0",
|
|
41
41
|
"@celilo/cli-display": "^0.2.0",
|
|
42
42
|
"@celilo/event-bus": "^0.6.0",
|
|
43
43
|
"@celilo/terraform-fake": "^0.3.1",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"zod": "^3.24.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@types/bun": "
|
|
48
|
+
"@types/bun": "1.4.0"
|
|
49
49
|
},
|
|
50
50
|
"keywords": [
|
|
51
51
|
"celilo",
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
workspaceVersionMap,
|
|
39
39
|
} from '../../../scripts/workspace-graph';
|
|
40
40
|
import { ensureRegistryServerBundle } from '../src/registry-bundle';
|
|
41
|
+
import { computeSourceFingerprint } from '../src/source-fingerprint';
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Resolve the celilo monorepo root by walking up from `startDir` (the caller's
|
|
@@ -242,6 +243,20 @@ function main(): void {
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
console.log(`✓ Packed ${results.length} package(s) to ${destination}`);
|
|
246
|
+
|
|
247
|
+
// Record the tree fingerprint these tarballs were packed FROM, next to the
|
|
248
|
+
// tarballs. The management-image bake stamps THIS value, not a fingerprint
|
|
249
|
+
// recomputed from the tree at bake time: the tarballs are what install.sh
|
|
250
|
+
// actually installs, so the stamp must describe them (celilo#1299 — a bake
|
|
251
|
+
// that reinstalls a stale cache used to stamp a fingerprint of the newer
|
|
252
|
+
// tree, claiming CLI content the image did not carry). The npm-registry
|
|
253
|
+
// server only serves *.tgz, so a JSON file in the same directory is inert.
|
|
254
|
+
const manifest = {
|
|
255
|
+
packedAt: new Date().toISOString(),
|
|
256
|
+
sourceFingerprint: computeSourceFingerprint(repoRoot()),
|
|
257
|
+
packages: results,
|
|
258
|
+
};
|
|
259
|
+
writeFileSync(join(destination, 'pack-manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
245
260
|
}
|
|
246
261
|
|
|
247
262
|
if (import.meta.main) {
|