@celilo/e2e 0.10.4 → 0.11.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/README.md +37 -28
- package/bin/e2e-status +1 -1
- package/bin/e2e-up +14 -14
- package/config/dhcp/dnsmasq.conf +3 -3
- package/config/resolver/unbound.conf +2 -2
- package/config/routing/fw-ext-routes.sh +4 -4
- package/config/routing/fw-isp-routes.sh +3 -3
- package/config/routing/fw-main-routes.sh +15 -4
- package/config/routing/management-routes.sh +11 -11
- package/config/routing/observer-setup.sh +3 -3
- package/config/routing/resolver-internal-routes.sh +5 -5
- package/config/routing/target-routes.sh +1 -1
- package/config/routing/target-setup.sh +18 -16
- package/config/socks/startup.sh +3 -3
- package/docker/Dockerfile.target-machine +15 -1
- package/docker/Dockerfile.target-machine-docker +6 -0
- package/docker/Dockerfile.target-technitium +6 -0
- package/npm-registry-server/src/packument.test.ts +2 -5
- package/npm-registry-server/src/packument.ts +3 -3
- package/npm-registry-server/src/server.ts +1 -1
- package/package.json +3 -3
- package/registry-server/src/bootstrap.ts +1 -1
- package/registry-server/src/introspection.test.ts +8 -12
- package/registry-server/src/server.test.ts +2 -2
- package/registry-server/src/server.ts +1 -1
- package/registry-server/src/storage.test.ts +8 -4
- package/registry-server/src/storage.ts +1 -1
- package/registry-server/src/validation.ts +2 -4
- package/scripts/pack-celilo-packages.ts +1 -4
- package/scripts/stage-apt-repo.ts +1 -1
- package/scripts/stage-libsignal.ts +1 -3
- package/simulators/greenwave/server.ts +4 -2
- package/simulators/greenwave/state.ts +24 -17
- package/simulators/isitup/server.ts +1 -5
- package/simulators/namecheap-ddns/server.ts +2 -4
- package/simulators/namecheap-ddns/zone-updater.test.ts +3 -1
- package/simulators/namecheap-ddns/zone-updater.ts +6 -10
- package/simulators/signal-cli/server.ts +3 -1
- package/src/address-plan.test.ts +95 -0
- package/src/browser.ts +3 -3
- package/src/bus-events.ts +2 -2
- package/src/cli/command-tree-parser.ts +8 -2
- package/src/cli/completion.ts +5 -8
- package/src/cli/index.ts +9 -2
- package/src/cli/module-discovery.ts +3 -1
- package/src/cli/scaffold.ts +2 -2
- package/src/cli-version-contract.ts +5 -6
- package/src/container-manager.ts +25 -8
- package/src/docker-compose-generator.ts +67 -39
- package/src/extract-failure.ts +92 -0
- package/src/fixtures.ts +10 -17
- package/src/index.ts +2 -2
- package/src/network-builder.ts +14 -9
- package/src/observer.test.ts +50 -19
- package/src/observer.ts +15 -8
- package/src/registry-bundle.ts +1 -3
- package/src/run-lock.test.ts +2 -2
- package/src/run-lock.ts +19 -5
- package/src/runner.ts +9 -67
- package/src/shared-infra.ts +50 -22
- package/src/socks-proxy.ts +16 -19
- package/src/types.ts +67 -17
- package/src/vantage-helpers.ts +6 -2
- package/src/vantage.test.ts +94 -40
- package/src/vantage.ts +24 -17
- package/src/wait-for-run.ts +1 -1
- package/src/zone-classifier.test.ts +32 -21
- package/src/zone-classifier.ts +7 -6
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
2
|
import { ADMIN_SCOPE } from './auth';
|
|
3
|
-
import {
|
|
4
|
-
IntrospectionVerifier,
|
|
5
|
-
claimToScope,
|
|
6
|
-
introspectionConfigFromEnv,
|
|
7
|
-
} from './introspection';
|
|
3
|
+
import { IntrospectionVerifier, claimToScope, introspectionConfigFromEnv } from './introspection';
|
|
8
4
|
|
|
9
5
|
const CONFIG = {
|
|
10
6
|
endpoint: 'https://idp.test/application/o/introspect/',
|
|
@@ -88,9 +84,7 @@ describe('IntrospectionVerifier.scopeOf', () => {
|
|
|
88
84
|
const verifier = new IntrospectionVerifier(CONFIG);
|
|
89
85
|
|
|
90
86
|
test('active admin-group token → ADMIN_SCOPE', async () => {
|
|
91
|
-
stubFetch(() =>
|
|
92
|
-
introspectResponse({ active: true, sub: 'alice', groups: ['celilo-admins'] }),
|
|
93
|
-
);
|
|
87
|
+
stubFetch(() => introspectResponse({ active: true, sub: 'alice', groups: ['celilo-admins'] }));
|
|
94
88
|
expect(await verifier.scopeOf('Bearer idp-token')).toBe(ADMIN_SCOPE);
|
|
95
89
|
});
|
|
96
90
|
|
|
@@ -106,9 +100,7 @@ describe('IntrospectionVerifier.scopeOf', () => {
|
|
|
106
100
|
|
|
107
101
|
test('expired exp → null even if active', async () => {
|
|
108
102
|
const pastSec = Math.floor(Date.now() / 1000) - 60;
|
|
109
|
-
stubFetch(() =>
|
|
110
|
-
introspectResponse({ active: true, groups: ['celilo-admins'], exp: pastSec }),
|
|
111
|
-
);
|
|
103
|
+
stubFetch(() => introspectResponse({ active: true, groups: ['celilo-admins'], exp: pastSec }));
|
|
112
104
|
expect(await verifier.scopeOf('idp-token')).toBeNull();
|
|
113
105
|
});
|
|
114
106
|
|
|
@@ -238,7 +230,11 @@ describe('IntrospectionVerifier.identify (ce-1ch)', () => {
|
|
|
238
230
|
});
|
|
239
231
|
|
|
240
232
|
test('admin group is configurable, not a literal', async () => {
|
|
241
|
-
const custom = new IntrospectionVerifier({
|
|
233
|
+
const custom = new IntrospectionVerifier({
|
|
234
|
+
...CONFIG,
|
|
235
|
+
adminGroup: 'ops',
|
|
236
|
+
publisherGroup: 'devs',
|
|
237
|
+
});
|
|
242
238
|
stubFetch(() => introspectResponse({ active: true, sub: 'dan', groups: ['ops'] }));
|
|
243
239
|
const id = await custom.identify('Bearer t');
|
|
244
240
|
expect(id?.isAdmin).toBe(true);
|
|
@@ -714,7 +714,7 @@ describe('module-owner authorization (ce-1ch)', () => {
|
|
|
714
714
|
const res = await fetch(`${url}/api/v1/modules/owners/caddy`, {
|
|
715
715
|
headers: { Authorization: 'Bearer opaque-admin' },
|
|
716
716
|
});
|
|
717
|
-
expect((await res.json() as { owner: { ownerSub: string } }).owner.ownerSub).toBe('ops');
|
|
717
|
+
expect(((await res.json()) as { owner: { ownerSub: string } }).owner.ownerSub).toBe('ops');
|
|
718
718
|
});
|
|
719
719
|
|
|
720
720
|
test('admin reassign lets a new owner publish the name', async () => {
|
|
@@ -741,7 +741,7 @@ describe('module-owner authorization (ce-1ch)', () => {
|
|
|
741
741
|
headers: { Authorization: 'Bearer opaque-admin' },
|
|
742
742
|
});
|
|
743
743
|
expect(ok.status).toBe(200);
|
|
744
|
-
expect((await ok.json() as { owners: unknown[] }).owners).toEqual([]);
|
|
744
|
+
expect(((await ok.json()) as { owners: unknown[] }).owners).toEqual([]);
|
|
745
745
|
});
|
|
746
746
|
|
|
747
747
|
test('owner reassign requires admin (a plain publisher cannot)', async () => {
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { ADMIN_SCOPE, TokenAuth } from './auth';
|
|
24
|
-
import { IntrospectionVerifier } from './introspection';
|
|
25
24
|
import {
|
|
26
25
|
type BootstrapEntry,
|
|
27
26
|
bootstrapIndexEntry,
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
scanBootstrapDir,
|
|
30
29
|
scanUploadsDir,
|
|
31
30
|
} from './bootstrap';
|
|
31
|
+
import { IntrospectionVerifier } from './introspection';
|
|
32
32
|
import { landingResponse } from './landing';
|
|
33
33
|
import { ModuleOwnerStore, fileModuleOwnerPersistence } from './module-owner-store';
|
|
34
34
|
import { type RateLimiter, clientIp, createRateLimiter } from './rate-limit';
|
|
@@ -37,9 +37,7 @@ describe('RegistryStorage.indexPath', () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
test('long name uses first 4 chars for directories', () => {
|
|
40
|
-
expect(storage.indexPath('homebridge')).toBe(
|
|
41
|
-
join(dataDir, 'index', 'ho', 'me', 'homebridge'),
|
|
42
|
-
);
|
|
40
|
+
expect(storage.indexPath('homebridge')).toBe(join(dataDir, 'index', 'ho', 'me', 'homebridge'));
|
|
43
41
|
});
|
|
44
42
|
|
|
45
43
|
test('real module names produce expected paths', () => {
|
|
@@ -128,7 +126,13 @@ describe('RegistryStorage listModules / getModule', () => {
|
|
|
128
126
|
|
|
129
127
|
test('listModules returns stored modules with their index entries', () => {
|
|
130
128
|
storage.storePackage('homebridge', '1.0.0+1', Buffer.from('a'));
|
|
131
|
-
storage.appendIndex({
|
|
129
|
+
storage.appendIndex({
|
|
130
|
+
name: 'homebridge',
|
|
131
|
+
vers: '1.0.0+1',
|
|
132
|
+
deps: [],
|
|
133
|
+
cksum: 'x',
|
|
134
|
+
yanked: false,
|
|
135
|
+
});
|
|
132
136
|
const list = storage.listModules();
|
|
133
137
|
expect(list).toHaveLength(1);
|
|
134
138
|
expect(list[0].name).toBe('homebridge');
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* downloads/{name} — per-module download counter (single integer)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
10
|
import { createHash } from 'node:crypto';
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
13
|
|
|
14
14
|
export interface IndexEntry {
|
|
@@ -44,16 +44,14 @@ export function validateNameAndVersion(
|
|
|
44
44
|
return {
|
|
45
45
|
ok: false,
|
|
46
46
|
field: 'name',
|
|
47
|
-
message:
|
|
48
|
-
'Invalid module name — must be kebab-case lowercase, 1-128 chars (e.g. "my-module")',
|
|
47
|
+
message: 'Invalid module name — must be kebab-case lowercase, 1-128 chars (e.g. "my-module")',
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
if (!isValidVersion(vers)) {
|
|
52
51
|
return {
|
|
53
52
|
ok: false,
|
|
54
53
|
field: 'vers',
|
|
55
|
-
message:
|
|
56
|
-
'Invalid version — must match "<major>.<minor>.<patch>+<pkgrev>" (e.g. "1.4.2+1")',
|
|
54
|
+
message: 'Invalid version — must match "<major>.<minor>.<patch>+<pkgrev>" (e.g. "1.4.2+1")',
|
|
57
55
|
};
|
|
58
56
|
}
|
|
59
57
|
return { ok: true };
|
|
@@ -127,10 +127,7 @@ export function rewriteWorkspacePins(
|
|
|
127
127
|
const version = versions.get(name);
|
|
128
128
|
if (!version) {
|
|
129
129
|
throw new Error(
|
|
130
|
-
`Cannot rewrite ${bucket} "${name}": "${spec}" — ${name} is not a known
|
|
131
|
-
`@celilo/* workspace package. Add it to the workspace, or fix the dep name. ` +
|
|
132
|
-
`(This is the loud failure the old hardcoded pack list lacked — ` +
|
|
133
|
-
`see apps/celilo/designs/WORKSPACE_PACKAGE_GRAPH.md.)`,
|
|
130
|
+
`Cannot rewrite ${bucket} "${name}": "${spec}" — ${name} is not a known @celilo/* workspace package. Add it to the workspace, or fix the dep name. (This is the loud failure the old hardcoded pack list lacked — see apps/celilo/designs/WORKSPACE_PACKAGE_GRAPH.md.)`,
|
|
134
131
|
);
|
|
135
132
|
}
|
|
136
133
|
const range = spec.slice('workspace:'.length);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
|
-
import { cpSync, existsSync, mkdirSync,
|
|
27
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
28
28
|
import { join } from 'node:path';
|
|
29
29
|
|
|
30
30
|
const green = '\x1b[32m';
|
|
@@ -102,9 +102,7 @@ export function stageLibsignal(repoRoot: string, pkgDir: string): boolean {
|
|
|
102
102
|
console.log(`${yellow}skip${reset} ${dim}(build failed)${reset}`);
|
|
103
103
|
return false;
|
|
104
104
|
}
|
|
105
|
-
console.log(
|
|
106
|
-
`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s${reset}`,
|
|
107
|
-
);
|
|
105
|
+
console.log(`${green}✔${reset} ${dim}${Math.round((Date.now() - t0) / 1000)}s${reset}`);
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
process.stdout.write(` ${'libsignal-jni (stage)'.padEnd(28)} `);
|
|
@@ -68,7 +68,7 @@ const tlsOptions = (() => {
|
|
|
68
68
|
|
|
69
69
|
// Bind to internal interface only (like a real router's management UI)
|
|
70
70
|
// The external interface should only handle port-forwarded traffic via iptables
|
|
71
|
-
const BIND_HOST = process.env.ROUTER_BIND_HOST || '
|
|
71
|
+
const BIND_HOST = process.env.ROUTER_BIND_HOST || '10.226.1.1';
|
|
72
72
|
|
|
73
73
|
const server = Bun.serve({
|
|
74
74
|
port: PORT,
|
|
@@ -161,4 +161,6 @@ const server = Bun.serve({
|
|
|
161
161
|
},
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
console.log(
|
|
164
|
+
console.log(
|
|
165
|
+
`[greenwave] C4000XG simulator listening on ${BIND_HOST}:${server.port} (${tlsOptions ? 'HTTPS' : 'HTTP'})`,
|
|
166
|
+
);
|
|
@@ -17,11 +17,11 @@ let nextIndex = 1;
|
|
|
17
17
|
const dhcpPool: Record<string, string> = {
|
|
18
18
|
Alias: 'cpe-Pool-1',
|
|
19
19
|
Enable: 'true',
|
|
20
|
-
MinAddress: '
|
|
21
|
-
MaxAddress: '
|
|
20
|
+
MinAddress: '10.226.1.2',
|
|
21
|
+
MaxAddress: '10.226.1.150',
|
|
22
22
|
SubnetMask: '255.255.255.0',
|
|
23
|
-
IPRouters: '
|
|
24
|
-
DNSServers: '
|
|
23
|
+
IPRouters: '10.226.1.1',
|
|
24
|
+
DNSServers: '10.226.1.1',
|
|
25
25
|
DomainName: '',
|
|
26
26
|
LeaseTime: '3600',
|
|
27
27
|
Status: 'Enabled',
|
|
@@ -117,11 +117,16 @@ export function setDhcpPool(params: Record<string, string>): CgiObject[] {
|
|
|
117
117
|
rewriteDnsmasqConfig();
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
return [
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
ObjName: 'Device.DHCPv4.Server.Pool.1.',
|
|
123
|
+
Param: [{ ParamName: 'Success', ParamValue: 'Success' }],
|
|
124
|
+
},
|
|
125
|
+
];
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
function rewriteDnsmasqConfig(): void {
|
|
124
|
-
const dnsServers = dhcpPool.DNSServers || '
|
|
129
|
+
const dnsServers = dhcpPool.DNSServers || '10.226.1.1';
|
|
125
130
|
const domainName = dhcpPool.DomainName || '';
|
|
126
131
|
|
|
127
132
|
const lines = [
|
|
@@ -129,8 +134,8 @@ function rewriteDnsmasqConfig(): void {
|
|
|
129
134
|
'port=0',
|
|
130
135
|
'interface=eth0',
|
|
131
136
|
'bind-interfaces',
|
|
132
|
-
'dhcp-range=
|
|
133
|
-
'dhcp-option=option:router,
|
|
137
|
+
'dhcp-range=10.226.1.200,10.226.1.220,255.255.255.0,60s',
|
|
138
|
+
'dhcp-option=option:router,10.226.1.1',
|
|
134
139
|
`dhcp-option=option:dns-server,${dnsServers}`,
|
|
135
140
|
'dhcp-lease-max=20',
|
|
136
141
|
'log-dhcp',
|
|
@@ -144,10 +149,13 @@ function rewriteDnsmasqConfig(): void {
|
|
|
144
149
|
writeFileSync('/etc/dnsmasq.d/e2e.conf', `${lines.join('\n')}\n`);
|
|
145
150
|
// dnsmasq requires full restart to re-read config files (SIGHUP only reloads /etc/hosts)
|
|
146
151
|
try {
|
|
147
|
-
execSync(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
execSync(
|
|
153
|
+
'pkill -9 dnsmasq; sleep 0.5; dnsmasq --conf-dir=/etc/dnsmasq.d --log-facility=/dev/stderr',
|
|
154
|
+
{
|
|
155
|
+
timeout: 5000,
|
|
156
|
+
shell: '/bin/bash',
|
|
157
|
+
},
|
|
158
|
+
);
|
|
151
159
|
} catch {}
|
|
152
160
|
console.log(`[greenwave] dnsmasq restarted: DNS=${dnsServers}, domain=${domainName}`);
|
|
153
161
|
} catch (err) {
|
|
@@ -168,10 +176,9 @@ function applyIptablesRule(mapping: PortMapping): void {
|
|
|
168
176
|
{ timeout: 5000 },
|
|
169
177
|
);
|
|
170
178
|
// Allow forwarded traffic
|
|
171
|
-
execSync(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
);
|
|
179
|
+
execSync(`iptables -A FORWARD -p ${p} -d ${intIp} --dport ${intPort} -j ACCEPT`, {
|
|
180
|
+
timeout: 5000,
|
|
181
|
+
});
|
|
175
182
|
}
|
|
176
183
|
|
|
177
184
|
try {
|
|
@@ -180,7 +187,7 @@ function applyIptablesRule(mapping: PortMapping): void {
|
|
|
180
187
|
addRules('udp');
|
|
181
188
|
}
|
|
182
189
|
} catch (err) {
|
|
183
|
-
console.error(
|
|
190
|
+
console.error('[greenwave] Failed to apply iptables rule:', err);
|
|
184
191
|
}
|
|
185
192
|
}
|
|
186
193
|
|
|
@@ -41,11 +41,7 @@ async function probe(targetUrl: string): Promise<ProbeResponse> {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const port = parsed.port
|
|
45
|
-
? Number(parsed.port)
|
|
46
|
-
: parsed.protocol === 'https:'
|
|
47
|
-
? 443
|
|
48
|
-
: 80;
|
|
44
|
+
const port = parsed.port ? Number(parsed.port) : parsed.protocol === 'https:' ? 443 : 80;
|
|
49
45
|
|
|
50
46
|
const start = Date.now();
|
|
51
47
|
try {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* Returns XML matching the real Namecheap response format.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { updateZone } from './zone-updater';
|
|
12
11
|
import type { DdnsRecord } from './types';
|
|
12
|
+
import { updateZone } from './zone-updater';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Minimal structural type for the bits of Bun's server we use — avoids
|
|
@@ -56,9 +56,7 @@ function expectedPasswordFor(domain: string): string {
|
|
|
56
56
|
const records: Map<string, DdnsRecord> = new Map();
|
|
57
57
|
|
|
58
58
|
function xmlResponse(errCount: number, ip: string, errors: string[]): Response {
|
|
59
|
-
const errorElements = errors
|
|
60
|
-
.map((e, i) => `<Err${i + 1}>${e}</Err${i + 1}>`)
|
|
61
|
-
.join('\n ');
|
|
59
|
+
const errorElements = errors.map((e, i) => `<Err${i + 1}>${e}</Err${i + 1}>`).join('\n ');
|
|
62
60
|
|
|
63
61
|
const xml = `<?xml version="1.0" encoding="utf-16"?>
|
|
64
62
|
<interface-response>
|
|
@@ -23,7 +23,9 @@ describe('namecheap-ddns protected records (e2e-confidence #257)', () => {
|
|
|
23
23
|
// The guard runs before any readFileSync, so this throws the protection
|
|
24
24
|
// error (not an ENOENT) even with no /config present — proving a DDNS apex
|
|
25
25
|
// clobber of the website-sim can never reach the zone file.
|
|
26
|
-
expect(() => updateZone('celilo.computer', '@', '100.64.0.1')).toThrow(
|
|
26
|
+
expect(() => updateZone('celilo.computer', '@', '100.64.0.1')).toThrow(
|
|
27
|
+
/reserved e2e infrastructure/,
|
|
28
|
+
);
|
|
27
29
|
expect(() => updateZone('celilo.computer', 'www', '100.64.0.1')).toThrow(
|
|
28
30
|
/reserved e2e infrastructure/,
|
|
29
31
|
);
|
|
@@ -33,21 +33,17 @@ export function isProtectedRecord(domain: string, host: string): boolean {
|
|
|
33
33
|
export function updateZone(domain: string, host: string, ip: string): void {
|
|
34
34
|
if (isProtectedRecord(domain, host)) {
|
|
35
35
|
throw new Error(
|
|
36
|
-
`Refusing DDNS update to "${host}.${domain}": it is reserved e2e infrastructure
|
|
37
|
-
`(the website-sim), not a DDNS-managed host (e2e-confidence #257).`,
|
|
36
|
+
`Refusing DDNS update to "${host}.${domain}": it is reserved e2e infrastructure (the website-sim), not a DDNS-managed host (e2e-confidence #257).`,
|
|
38
37
|
);
|
|
39
38
|
}
|
|
40
39
|
const zoneFile = `${ZONE_DIR}/${domain}.zone`;
|
|
41
40
|
let content = readFileSync(zoneFile, 'utf-8');
|
|
42
41
|
|
|
43
42
|
// Increment SOA serial (YYYYMMDDnn format — just increment the number)
|
|
44
|
-
content = content.replace(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return `${newSerial}${rest}`;
|
|
49
|
-
},
|
|
50
|
-
);
|
|
43
|
+
content = content.replace(/(\d{10})(\s+\d+\s+\d+\s+\d+\s+\d+\s*\))/, (_match, serial, rest) => {
|
|
44
|
+
const newSerial = String(Number(serial) + 1);
|
|
45
|
+
return `${newSerial}${rest}`;
|
|
46
|
+
});
|
|
51
47
|
|
|
52
48
|
// Check if host already has an A record. Match ANY existing value (\S+),
|
|
53
49
|
// not just dotted-decimal: a prior update may have written a malformed
|
|
@@ -61,7 +57,7 @@ export function updateZone(domain: string, host: string, ip: string): void {
|
|
|
61
57
|
content = content.replace(recordRegex, `${host} IN A ${ip}`);
|
|
62
58
|
} else {
|
|
63
59
|
// Append new record
|
|
64
|
-
content = content.trimEnd()
|
|
60
|
+
content = `${content.trimEnd()}\n${host} IN A ${ip}\n`;
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
writeFileSync(zoneFile, content);
|
|
@@ -280,7 +280,9 @@ Bun.serve({
|
|
|
280
280
|
|
|
281
281
|
// Verified endpoints on the real daemon.
|
|
282
282
|
if (url.pathname === '/api/v1/check') {
|
|
283
|
-
return req.method === 'GET'
|
|
283
|
+
return req.method === 'GET'
|
|
284
|
+
? new Response(null, { status: 200 })
|
|
285
|
+
: new Response(null, { status: 405 });
|
|
284
286
|
}
|
|
285
287
|
if (url.pathname === '/api/v1/events') {
|
|
286
288
|
return new Response('', { status: 200, headers: { 'content-type': 'text/event-stream' } });
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recurrence gate for #539.
|
|
3
|
+
*
|
|
4
|
+
* The sim used to number its zones exactly like a real celilo fleet, so a
|
|
5
|
+
* leaked stack on celilo's own forgejo-builder (which lives at 10.0.10.12 in
|
|
6
|
+
* the REAL dmz) put a second interface on the builder's own /24 and blackholed
|
|
7
|
+
* every containerized CI job's route to the forge. These tests make the two
|
|
8
|
+
* properties that prevent it non-negotiable:
|
|
9
|
+
*
|
|
10
|
+
* 1. every simulated private zone lives inside SIM_PRIVATE_SUPERNET, and
|
|
11
|
+
* 2. the retired fleet-colliding prefixes appear NOWHERE in the suite.
|
|
12
|
+
*
|
|
13
|
+
* (2) is the one that matters in practice: the plan itself is one table, but
|
|
14
|
+
* shell scripts, Knot zones and dnsmasq configs can't import TypeScript and so
|
|
15
|
+
* hard-code the values. This is what catches "I changed it in one place".
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, test } from 'bun:test';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import { SIM_PRIVATE_SUPERNET, ZONE_GATEWAYS, ZONE_SUBNETS, type Zone } from './types';
|
|
21
|
+
import { inCidr } from './zone-classifier';
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = execSync('git rev-parse --show-toplevel', {
|
|
24
|
+
cwd: import.meta.dir,
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
}).trim();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prefixes the sim must never use again: the fleet's zone /24s and the
|
|
30
|
+
* operator's LAN. Prefix match — `10.0.10.` catches an address, a CIDR and a
|
|
31
|
+
* prose mention alike, which is what we want.
|
|
32
|
+
*/
|
|
33
|
+
const RETIRED_PREFIXES = ['10.0.10.', '10.0.20.', '10.0.30.', '10.0.120.', '192.168.'];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Match the plain form AND the regex-escaped form (`192\.168\.0\.10`). Two
|
|
37
|
+
* escaped literals inside `toThrow(/…/)` survived the #539 sweep precisely
|
|
38
|
+
* because a naive search for the plain text does not see them.
|
|
39
|
+
*/
|
|
40
|
+
function toPattern(prefix: string): string {
|
|
41
|
+
return prefix.replace(/\./g, '\\\\?\\.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Everything that can put an address on a simulated network or assert one. */
|
|
45
|
+
const SCANNED_PATHS = ['packages/e2e', 'e2e/tests', 'modules/*/e2e'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* CHANGELOG is history — it describes what the addresses WERE. This test file
|
|
49
|
+
* necessarily names the retired prefixes to ban them.
|
|
50
|
+
*/
|
|
51
|
+
const EXCLUDED = [/^packages\/e2e\/CHANGELOG\.md$/, /address-plan\.test\.ts$/];
|
|
52
|
+
|
|
53
|
+
function grepRetired(): string[] {
|
|
54
|
+
const pattern = RETIRED_PREFIXES.map(toPattern).join('|');
|
|
55
|
+
let out = '';
|
|
56
|
+
try {
|
|
57
|
+
out = execSync(`git grep -nE '${pattern}' -- ${SCANNED_PATHS.join(' ')}`, {
|
|
58
|
+
cwd: REPO_ROOT,
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
// git grep exits 1 when there are no matches — that is the passing case.
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
.split('\n')
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.filter((line) => !EXCLUDED.some((re) => re.test(line.split(':')[0])));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('simulated address plan (#539)', () => {
|
|
73
|
+
test('every zone lives inside the sim supernet', () => {
|
|
74
|
+
for (const zone of Object.keys(ZONE_SUBNETS) as Zone[]) {
|
|
75
|
+
const base = ZONE_SUBNETS[zone].split('/')[0];
|
|
76
|
+
expect(inCidr(base, SIM_PRIVATE_SUPERNET)).toBe(true);
|
|
77
|
+
expect(inCidr(ZONE_GATEWAYS[zone], SIM_PRIVATE_SUPERNET)).toBe(true);
|
|
78
|
+
expect(inCidr(ZONE_GATEWAYS[zone], ZONE_SUBNETS[zone])).toBe(true);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('zones do not overlap each other', () => {
|
|
83
|
+
const subnets = Object.values(ZONE_SUBNETS);
|
|
84
|
+
expect(new Set(subnets).size).toBe(subnets.length);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('the fleet-colliding prefixes appear nowhere in the suite', () => {
|
|
88
|
+
const hits = grepRetired();
|
|
89
|
+
expect(
|
|
90
|
+
hits,
|
|
91
|
+
`The e2e suite must not use a real fleet's addresses (#539). Offending lines:\n${hits.join('\n')}\n\n` +
|
|
92
|
+
`Use zoneIp()/ZONE_SUBNETS from packages/e2e/src/types.ts, or an address inside ${SIM_PRIVATE_SUPERNET}.`,
|
|
93
|
+
).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
});
|
package/src/browser.ts
CHANGED
|
@@ -14,10 +14,10 @@ export async function startBrowser(
|
|
|
14
14
|
): Promise<BrowserHandle> {
|
|
15
15
|
if (!options.chromium) {
|
|
16
16
|
throw new Error(
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
'net.browser() requires a `chromium` launcher in options.\n\n' +
|
|
18
|
+
'Pass it from your own playwright install:\n' +
|
|
19
19
|
" import { chromium } from 'playwright-core';\n" +
|
|
20
|
-
|
|
20
|
+
' const browser = await net.browser({ vantage, chromium });',
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
const proxy = await startSocksProxy(projectName, options);
|
package/src/bus-events.ts
CHANGED
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* without the bus, just without observability hooks.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
22
22
|
import { homedir } from 'node:os';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
25
24
|
import { dirname } from 'node:path';
|
|
25
|
+
import { type Bus, defineEvents, openBus } from '@celilo/event-bus';
|
|
26
26
|
import { z } from 'zod';
|
|
27
27
|
|
|
28
28
|
const NO_SCHEMAS = defineEvents({});
|
|
@@ -21,8 +21,14 @@ function parseCommandsFromHelp(text: string): string[] {
|
|
|
21
21
|
let inCommandsSection = false;
|
|
22
22
|
|
|
23
23
|
for (const line of text.split('\n')) {
|
|
24
|
-
if (/^\s*Commands?:\s*$/i.test(line)) {
|
|
25
|
-
|
|
24
|
+
if (/^\s*Commands?:\s*$/i.test(line)) {
|
|
25
|
+
inCommandsSection = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (inCommandsSection && /^\s*(Options|Examples|Usage):/i.test(line)) {
|
|
29
|
+
inCommandsSection = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
26
32
|
if (!inCommandsSection) continue;
|
|
27
33
|
|
|
28
34
|
// Match " command-name" at 2-3 spaces indent, followed by space/bracket/end
|
package/src/cli/completion.ts
CHANGED
|
@@ -3,13 +3,7 @@
|
|
|
3
3
|
* All completions are static (no DB lookups needed).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
BUILD_INFRA_FLAGS,
|
|
8
|
-
COMMANDS,
|
|
9
|
-
DOWN_FLAGS,
|
|
10
|
-
RUN_FLAGS,
|
|
11
|
-
UP_PRESETS,
|
|
12
|
-
} from './command-registry';
|
|
6
|
+
import { BUILD_INFRA_FLAGS, COMMANDS, DOWN_FLAGS, RUN_FLAGS, UP_PRESETS } from './command-registry';
|
|
13
7
|
import { findAllTestNames } from './module-discovery';
|
|
14
8
|
|
|
15
9
|
export function getCompletions(words: string[], current: number): string[] {
|
|
@@ -58,7 +52,10 @@ export function getCompletions(words: string[], current: number): string[] {
|
|
|
58
52
|
|
|
59
53
|
if (command === 'list' && currentIndex >= 1) {
|
|
60
54
|
const used = new Set(args.slice(1));
|
|
61
|
-
return filterSuggestions(
|
|
55
|
+
return filterSuggestions(
|
|
56
|
+
['--paths'].filter((f) => !used.has(f)),
|
|
57
|
+
args[currentIndex] || '',
|
|
58
|
+
);
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
if (command === 'completion' && currentIndex === 1) {
|
package/src/cli/index.ts
CHANGED
|
@@ -333,6 +333,7 @@ switch (command) {
|
|
|
333
333
|
}
|
|
334
334
|
console.log(`\n${tests.length} tests. Run all: cele2e run --all`);
|
|
335
335
|
process.exit(0);
|
|
336
|
+
break;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
case 'up': {
|
|
@@ -350,15 +351,18 @@ switch (command) {
|
|
|
350
351
|
throw err;
|
|
351
352
|
}
|
|
352
353
|
runScript('e2e-up', args);
|
|
354
|
+
break;
|
|
353
355
|
}
|
|
354
356
|
|
|
355
357
|
case 'down':
|
|
356
358
|
// Tearing the network down frees the (possibly kept) lock.
|
|
357
359
|
clearLock();
|
|
358
360
|
runScript('e2e-down', args, { E2E_TEST_DIR: stateDir });
|
|
361
|
+
break;
|
|
359
362
|
|
|
360
363
|
case 'shell':
|
|
361
364
|
runScript('e2e-shell', args);
|
|
365
|
+
break;
|
|
362
366
|
|
|
363
367
|
case 'release': {
|
|
364
368
|
// Free a lock left by `--keep`/`up` (a `kept` stack) so the next run can
|
|
@@ -366,6 +370,7 @@ switch (command) {
|
|
|
366
370
|
const had = clearLock();
|
|
367
371
|
console.log(had ? 'Released the e2e run-lock.' : 'No e2e run-lock held.');
|
|
368
372
|
process.exit(0);
|
|
373
|
+
break;
|
|
369
374
|
}
|
|
370
375
|
|
|
371
376
|
case 'status': {
|
|
@@ -394,18 +399,20 @@ switch (command) {
|
|
|
394
399
|
console.log(` Project: ${data.projectName}`);
|
|
395
400
|
console.log(` Test: ${data.testFile}`);
|
|
396
401
|
console.log(` Created: ${data.createdAt}`);
|
|
397
|
-
console.log(
|
|
398
|
-
console.log(
|
|
402
|
+
console.log(' Reuse: cele2e run --reuse');
|
|
403
|
+
console.log(' Tear down: cele2e down');
|
|
399
404
|
console.log('');
|
|
400
405
|
} catch {
|
|
401
406
|
/* ignore parse errors */
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
runScript('e2e-status', args, projectName ? { CELILO_E2E_PROJECT: projectName } : undefined);
|
|
410
|
+
break;
|
|
405
411
|
}
|
|
406
412
|
|
|
407
413
|
case 'load':
|
|
408
414
|
runScript('e2e-load', args);
|
|
415
|
+
break;
|
|
409
416
|
|
|
410
417
|
case 'build-infra': {
|
|
411
418
|
const save = args.includes('--save');
|
|
@@ -18,7 +18,9 @@ const MAX_DEPTH = 2;
|
|
|
18
18
|
|
|
19
19
|
function readManifestTestsDir(manifestPath: string): string | undefined {
|
|
20
20
|
try {
|
|
21
|
-
const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as {
|
|
21
|
+
const parsed = parseYaml(readFileSync(manifestPath, 'utf-8')) as {
|
|
22
|
+
e2e?: { tests_dir?: string };
|
|
23
|
+
};
|
|
22
24
|
return parsed?.e2e?.tests_dir;
|
|
23
25
|
} catch {
|
|
24
26
|
return undefined;
|
package/src/cli/scaffold.ts
CHANGED
|
@@ -60,8 +60,8 @@ describe('${name}', () => {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
net = await network({ projectName: PROJECT_NAME })
|
|
63
|
-
.dmz({ 'machine-1': '10.
|
|
64
|
-
// .app({ 'app-1': '10.
|
|
63
|
+
.dmz({ 'machine-1': '10.226.10.10' })
|
|
64
|
+
// .app({ 'app-1': '10.226.20.10' })
|
|
65
65
|
.start();
|
|
66
66
|
}, 120_000);
|
|
67
67
|
|
|
@@ -17,7 +17,10 @@ export const MIN_CLI_VERSION = '0.9.0';
|
|
|
17
17
|
* it doesn't, or `null` when OK. No I/O, so it's unit-testable without a
|
|
18
18
|
* running management container.
|
|
19
19
|
*/
|
|
20
|
-
export function checkCliVersion(
|
|
20
|
+
export function checkCliVersion(
|
|
21
|
+
versionStdout: string,
|
|
22
|
+
minVersion = MIN_CLI_VERSION,
|
|
23
|
+
): string | null {
|
|
21
24
|
// Output is `celilo <version>` (apps/celilo displayVersion). Grab the token.
|
|
22
25
|
const version = versionStdout.match(/celilo\s+(\S+)/)?.[1];
|
|
23
26
|
if (!version) {
|
|
@@ -27,11 +30,7 @@ export function checkCliVersion(versionStdout: string, minVersion = MIN_CLI_VERS
|
|
|
27
30
|
);
|
|
28
31
|
}
|
|
29
32
|
if (Bun.semver.order(version, minVersion) < 0) {
|
|
30
|
-
return (
|
|
31
|
-
`Management image has celilo ${version}, harness requires >=${minVersion}. ` +
|
|
32
|
-
`Rebuild the management image against a newer @celilo/cli ` +
|
|
33
|
-
`(e.g. \`cele2e build-infra\`, or \`--published\` after publishing).`
|
|
34
|
-
);
|
|
33
|
+
return `Management image has celilo ${version}, harness requires >=${minVersion}. Rebuild the management image against a newer @celilo/cli (e.g. \`cele2e build-infra\`, or \`--published\` after publishing).`;
|
|
35
34
|
}
|
|
36
35
|
return null;
|
|
37
36
|
}
|