@celilo/e2e 0.19.0 → 0.19.2

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.
@@ -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';
@@ -613,20 +614,26 @@ function buildNetworkHandle(
613
614
  );
614
615
  // Give the box its fleet key BEFORE deploying, using the harness keypair.
615
616
  //
616
- // `ensureFleetKey` (modules/celilo-mgmt/scripts/discovery.ts) mints a new
617
- // `celilo-fleet` key under the state dir when none exists, and the deploy
618
- // then sets `ssh.public_key` to it. In a real fleet that is right. In the
619
- // rig it is not survivable: every target container trusts the HARNESS key,
620
- // baked into authorized_keys at boot, and a freshly minted key is trusted
621
- // by nothing. celilo would hold a key no machine accepts, and the next
622
- // `machine add` fails with "no matching private key was found in ~/.ssh/"
623
- // — which reads as a missing fixture and is actually a rotated key.
617
+ // `ensureFleetKey` (apps/celilo/src/services/fleet-key.ts, reached by the
618
+ // deploy through `celilo system ensure-fleet-key`) mints a new
619
+ // `celilo-fleet` key when none exists and records `ssh.public_key`. In a
620
+ // real fleet that is right. In the rig it is not survivable: every target
621
+ // container trusts the HARNESS key, baked into authorized_keys at boot,
622
+ // and a freshly minted key is trusted by nothing. celilo would hold a key
623
+ // no machine accepts, and the next `machine add` fails with "no matching
624
+ // private key was found in ~/.ssh/" — which reads as a missing fixture and
625
+ // is actually a rotated key.
624
626
  //
625
627
  // `ensureFleetKey` is idempotent and REUSES an existing key, so seeding it
626
628
  // with the harness pair means the deploy runs its real code path and
627
629
  // arrives at the key the fleet already trusts. This models an operator
628
630
  // whose management box already has its fleet key, which is the ordinary
629
631
  // case after the first deploy.
632
+ //
633
+ // The directory below must match `getFleetSshDir()`, which is
634
+ // `dirname(getDbPath())/.ssh`. The `db_path` set just above is what makes
635
+ // the two agree — the framework reads it from the env `on_install`
636
+ // exports before shelling out.
630
637
  const stateSshDir = '/root/.local/share/celilo/.ssh';
631
638
  dockerExec(
632
639
  projectName,
@@ -1005,12 +1012,19 @@ function buildNetworkHandle(
1005
1012
  }
1006
1013
 
1007
1014
  try {
1008
- // Copy directly into the registry container's /uploads dir.
1009
- // The registry rebuilds its index on every request, so no HTTP handshake needed.
1010
- run(
1011
- `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} cp ${JSON.stringify(netappPath)} registry:/uploads/${moduleId}.netapp`,
1012
- { cwd: composeDir, timeout: 120_000 },
1013
- );
1015
+ // Write HOST-side into the dir bound at the registry's /uploads. The
1016
+ // registry rescans that dir on every request, so it needs no HTTP
1017
+ // handshake and no restart.
1018
+ //
1019
+ // Deliberately not a `docker compose cp` into the container: /uploads
1020
+ // is a READ-ONLY bind in a consumer install, so the container write
1021
+ // failed for every npm-installed consumer while passing forever in the
1022
+ // monorepo, where the same path is not a mount at all (celilo#1142).
1023
+ // registryUploadsHostDir() is the same function the compose generator
1024
+ // builds the mount from, so the two cannot drift apart again.
1025
+ const uploadsDir = registryUploadsHostDir();
1026
+ mkdirSync(uploadsDir, { recursive: true });
1027
+ copyFileSync(netappPath, join(uploadsDir, `${moduleId}.netapp`));
1014
1028
  } finally {
1015
1029
  if (cleanup)
1016
1030
  try {
@@ -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
- * Volumes for the registry container. Picks between source-bootstrap and
18
- * uploads-bootstrap based on where the @celilo/e2e package lives:
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
- * - **Monorepo dev** — `../../modules` from the compose file (which lives
21
- * in `packages/e2e/`) resolves to `<repo>/modules`. The registry runs
22
- * in BOOTSTRAP_MODULES_DIR mode and packs source modules on the fly,
23
- * so edits to a module's source flow through without manual repack.
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.
24
48
  *
25
- * - **npm-installed consumer** (lunacycle and friends) — `../../modules`
26
- * would resolve to `<consumer>/node_modules/modules`, which doesn't
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.
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.
33
62
  *
34
- * Detection: check whether the would-be monorepo modules dir is real.
35
- * `import.meta.dir` is `<pkgDir>/src/`, so `../../../modules` from there
36
- * is `<repo>/modules` in the monorepo and `<consumer>/node_modules/modules`
37
- * (nonexistent) in a consumer install.
63
+ * Creates it: Docker silently creates a MISSING bind source as an empty
64
+ * root-owned directory, which the host then cannot write into.
65
+ *
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 monorepoModules = join(import.meta.dir, '..', '..', '..', 'modules');
41
- return existsSync(monorepoModules) ? ['../../modules:/modules:ro'] : ['./netapps:/uploads:ro'];
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 = {
@@ -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