@celilo/cli 1.12.0 → 1.14.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/CELILO_CORE_MODULES.md +2 -1
- package/CELILO_SUBSYSTEMS.md +21 -2
- package/package.json +3 -3
- package/src/capabilities/public-web-helpers.test.ts +12 -6
- package/src/capabilities/public-web-publish.test.ts +24 -13
- package/src/capabilities/validation.test.ts +31 -0
- package/src/cli/commands/alerts-list.ts +16 -1
- package/src/cli/commands/backup-list.test.ts +82 -1
- package/src/cli/commands/backup-list.ts +113 -4
- package/src/cli/commands/console-get-chain.test.ts +96 -0
- package/src/cli/commands/console.ts +130 -0
- package/src/cli/commands/module-list.ts +3 -41
- package/src/cli/commands/notify-config.test.ts +79 -0
- package/src/cli/commands/notify-config.ts +13 -2
- package/src/cli/commands/system-ensure-fleet-key.ts +52 -0
- package/src/cli/completion.ts +6 -0
- package/src/cli/index.ts +31 -1
- package/src/console/closure.test.ts +322 -0
- package/src/console/closure.ts +294 -0
- package/src/console/control-plane-boundary.test.ts +75 -0
- package/src/console/projection.test.ts +293 -0
- package/src/console/projection.ts +364 -0
- package/src/db/schema.ts +19 -14
- package/src/hooks/broker.test.ts +4 -6
- package/src/hooks/capability-loader-control-plane-api.test.ts +124 -0
- package/src/hooks/capability-loader.ts +67 -10
- package/src/hooks/executor.test.ts +85 -4
- package/src/hooks/executor.ts +164 -9
- package/src/hooks/hook-jail-unreachability.test.ts +173 -0
- package/src/hooks/hook-state-dir.test.ts +14 -2
- package/src/hooks/hook-timeout.test.ts +2 -4
- package/src/hooks/hook-trespass.test.ts +50 -5
- package/src/hooks/jail.test.ts +370 -0
- package/src/hooks/jail.ts +491 -0
- package/src/hooks/mount-set.ts +24 -0
- package/src/hooks/test-fixtures/jail-probe-hook.ts +59 -0
- package/src/manifest/contracts/v1.ts +22 -1
- package/src/manifest/schema.ts +35 -0
- package/src/manifest/validate.test.ts +142 -0
- package/src/manifest/validate.ts +126 -4
- package/src/module/import.test.ts +116 -0
- package/src/module/import.ts +73 -1
- package/src/module/packaging/audit.ts +103 -1
- package/src/module/packaging/classify-module-path.test.ts +36 -0
- package/src/module/packaging/package-rules.ts +18 -0
- package/src/module/web-root.ts +35 -0
- package/src/policy/capability-shape-baseline.ts +8 -0
- package/src/policy/module-business-baseline.ts +26 -2
- package/src/policy/module-script-scan.test.ts +22 -0
- package/src/policy/module-script-scan.ts +32 -0
- package/src/services/alerting/observed-health.ts +71 -0
- package/src/services/api-principal-enrolment.test.ts +252 -0
- package/src/services/api-principal-enrolment.ts +158 -0
- package/src/services/audit/backups.ts +10 -1
- package/src/services/backup-create.ts +33 -7
- package/src/services/backup-metadata.ts +19 -11
- package/src/services/celilo-mgmt-hooks.test.ts +38 -79
- package/src/services/consumer-cleanup.ts +31 -5
- package/src/services/fleet-key.test.ts +47 -0
- package/src/services/fleet-key.ts +75 -0
- package/src/services/instance-ops.test.ts +302 -0
- package/src/services/instance-ops.ts +292 -0
- package/src/services/module-instances.test.ts +428 -42
- package/src/services/module-instances.ts +219 -26
- package/src/services/restore-from-file.ts +6 -5
- package/src/services/system-state-stage.test.ts +165 -0
- package/src/services/system-state-stage.ts +196 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console must not be reachable from the data plane.
|
|
3
|
+
*
|
|
4
|
+
* This is asserted here, cheaply, on every pull request. The e2e suite asserts
|
|
5
|
+
* the same thing for real by failing to reach the console from a data-plane
|
|
6
|
+
* zone, but that suite is slow, needs Docker, and does not run on every change.
|
|
7
|
+
* The way this boundary actually erodes is a one-line manifest edit that adds
|
|
8
|
+
* `private_web` because someone wanted to reach the console from a laptop
|
|
9
|
+
* without bringing up the VPN, and a gate that only fires in e2e would not catch
|
|
10
|
+
* that until much later.
|
|
11
|
+
*
|
|
12
|
+
* The history is why this is a test and not a comment. The change's second draft
|
|
13
|
+
* DID publish the console through caddy-internal, the operator refused it, and
|
|
14
|
+
* the final verification step still said "the route resolves through
|
|
15
|
+
* caddy-internal" for a further round after that. Prose did not hold this.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, test } from 'bun:test';
|
|
18
|
+
import { parse } from 'yaml';
|
|
19
|
+
|
|
20
|
+
const MANIFEST_PATH = new URL(
|
|
21
|
+
'../../../../modules/celilo-web-console/manifest.yml',
|
|
22
|
+
import.meta.url,
|
|
23
|
+
).pathname;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Capabilities that give something a way in from the data plane.
|
|
27
|
+
*
|
|
28
|
+
* `private_web` and `public_web` publish a route through a reverse proxy.
|
|
29
|
+
* `firewall` is how a module asks for a port forward or an ingress address.
|
|
30
|
+
* Any of the three would put a data-plane client on a path to the control
|
|
31
|
+
* plane.
|
|
32
|
+
*/
|
|
33
|
+
const INGRESS_CAPABILITIES = ['private_web', 'public_web', 'firewall'];
|
|
34
|
+
|
|
35
|
+
interface CapabilityRef {
|
|
36
|
+
name: string;
|
|
37
|
+
}
|
|
38
|
+
interface Manifest {
|
|
39
|
+
id: string;
|
|
40
|
+
requires?: { capabilities?: CapabilityRef[]; system?: { zone?: string } };
|
|
41
|
+
optional?: { capabilities?: CapabilityRef[] };
|
|
42
|
+
provides?: { capabilities?: CapabilityRef[] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function loadManifest(): Promise<Manifest> {
|
|
46
|
+
return parse(await Bun.file(MANIFEST_PATH).text()) as Manifest;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('celilo-web-console stays in the control plane', () => {
|
|
50
|
+
test('lands in secure-mgmt and nowhere else', async () => {
|
|
51
|
+
const manifest = await loadManifest();
|
|
52
|
+
expect(manifest.requires?.system?.zone).toBe('secure-mgmt');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('requires no capability that could give it data-plane ingress', async () => {
|
|
56
|
+
const manifest = await loadManifest();
|
|
57
|
+
const required = (manifest.requires?.capabilities ?? []).map((c) => c.name);
|
|
58
|
+
const found = required.filter((name) => INGRESS_CAPABILITIES.includes(name));
|
|
59
|
+
expect(found).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('does not pick up ingress through an OPTIONAL capability either', async () => {
|
|
63
|
+
// Optional is the likelier hole. It reads as harmless, and the closure walk
|
|
64
|
+
// in this same directory exists because optional edges are real edges.
|
|
65
|
+
const manifest = await loadManifest();
|
|
66
|
+
const optional = (manifest.optional?.capabilities ?? []).map((c) => c.name);
|
|
67
|
+
const found = optional.filter((name) => INGRESS_CAPABILITIES.includes(name));
|
|
68
|
+
expect(found).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('provides nothing, so nothing can bind to it as a dependency', async () => {
|
|
72
|
+
const manifest = await loadManifest();
|
|
73
|
+
expect(manifest.provides?.capabilities ?? []).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console projection, against a real schema.
|
|
3
|
+
*
|
|
4
|
+
* The payload SHAPE is the point of these reads, so the assertions are about
|
|
5
|
+
* what is present and what is deliberately absent, not only about values.
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
8
|
+
import type { DbClient } from '../db/client';
|
|
9
|
+
import { NETWORK_ZONES } from '../db/schema';
|
|
10
|
+
import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database';
|
|
11
|
+
import { consoleStatus, loadClosureInputs } from './projection';
|
|
12
|
+
|
|
13
|
+
/** A manifest big enough to notice if it ever leaked into the payload. */
|
|
14
|
+
const FAT_MANIFEST = JSON.stringify({
|
|
15
|
+
id: 'caddy-internal',
|
|
16
|
+
padding: 'x'.repeat(4096),
|
|
17
|
+
requires: { capabilities: [{ name: 'idp' }] },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('consoleStatus', () => {
|
|
21
|
+
let db: DbClient;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
db = await setupTestDatabase();
|
|
25
|
+
db.$client.run(
|
|
26
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data, state)
|
|
27
|
+
VALUES ('caddy-internal', 'Caddy', '1.2.0', '/path', '${FAT_MANIFEST}', 'VERIFIED')`,
|
|
28
|
+
);
|
|
29
|
+
db.$client.run(
|
|
30
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data, state)
|
|
31
|
+
VALUES ('api-only', 'API only', '0.1.0', '/path', '{}', 'VERIFIED')`,
|
|
32
|
+
);
|
|
33
|
+
db.$client.run(
|
|
34
|
+
`INSERT INTO module_systems (module_id, name, hostname, ipv4_address, zone, infra_type)
|
|
35
|
+
VALUES ('caddy-internal', 'web', 'caddy-int', '10.0.10.14', 'dmz', 'container_service')`,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await cleanupTestDatabase(db);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('serves the canonical zone order rather than a copy', () => {
|
|
44
|
+
// A zone added to NETWORK_ZONES must reach the console without a console
|
|
45
|
+
// release, so the payload carries the list itself.
|
|
46
|
+
expect(consoleStatus(db).zones).toEqual([...NETWORK_ZONES]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('does NOT carry manifestData', () => {
|
|
50
|
+
// The whole reason these reads exist. `module list --json` is 156 KB for 23
|
|
51
|
+
// modules because it embeds this blob on every row.
|
|
52
|
+
const payload = JSON.stringify(consoleStatus(db));
|
|
53
|
+
expect(payload).not.toContain('padding');
|
|
54
|
+
expect(payload.length).toBeLessThan(2000);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('carries a module with no system rather than omitting it', () => {
|
|
58
|
+
// A closure that names a module the topology then cannot draw is
|
|
59
|
+
// indistinguishable from a defect, so the payload keeps it with an empty
|
|
60
|
+
// systems list and the renderer decides where to put it.
|
|
61
|
+
const found = consoleStatus(db).modules.find((m) => m.id === 'api-only');
|
|
62
|
+
expect(found).toBeDefined();
|
|
63
|
+
expect(found?.systems).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('carries each system with the address recorded in deployment state', () => {
|
|
67
|
+
const caddy = consoleStatus(db).modules.find((m) => m.id === 'caddy-internal');
|
|
68
|
+
expect(caddy?.systems).toEqual([{ hostname: 'caddy-int', address: '10.0.10.14', zone: 'dmz' }]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a deployed module with no monitor reads as not observed, not as healthy', () => {
|
|
72
|
+
const caddy = consoleStatus(db).modules.find((m) => m.id === 'caddy-internal');
|
|
73
|
+
expect(caddy?.health.cell).toBe('not observed');
|
|
74
|
+
expect(caddy?.health.monitored).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('an undeployed module is distinguished from an unobserved one', () => {
|
|
78
|
+
db.$client.run(
|
|
79
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data, state)
|
|
80
|
+
VALUES ('imported', 'Imported', '0.1.0', '/path', '{}', 'IMPORTED')`,
|
|
81
|
+
);
|
|
82
|
+
const imported = consoleStatus(db).modules.find((m) => m.id === 'imported');
|
|
83
|
+
// "not observed" is a finding about a deployed module. Saying it about a
|
|
84
|
+
// module that was never deployed would be noise.
|
|
85
|
+
expect(imported?.health.cell).toBe('not deployed');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('reports no backup as null rather than as zero', () => {
|
|
89
|
+
const caddy = consoleStatus(db).modules.find((m) => m.id === 'caddy-internal');
|
|
90
|
+
expect(caddy?.lastBackupAt).toBeNull();
|
|
91
|
+
expect(caddy?.lastBackupFailed).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('modules come back in a stable order', () => {
|
|
95
|
+
const first = consoleStatus(db).modules.map((m) => m.id);
|
|
96
|
+
const second = consoleStatus(db).modules.map((m) => m.id);
|
|
97
|
+
expect(first).toEqual(second);
|
|
98
|
+
expect(first).toEqual([...first].sort());
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The three facts the roster's lamps read, and the unit the ages are in.
|
|
104
|
+
*
|
|
105
|
+
* Every one of these is a wrong-but-plausible number rather than a crash. A
|
|
106
|
+
* timestamp in the wrong unit renders as an age that looks like a fresh backup.
|
|
107
|
+
* A module reported as watched when nothing runs its monitor reads as healthy.
|
|
108
|
+
* Neither throws, and neither is visible in review.
|
|
109
|
+
*/
|
|
110
|
+
describe('consoleStatus derived facts', () => {
|
|
111
|
+
let db: DbClient;
|
|
112
|
+
|
|
113
|
+
const DAY = 86_400_000;
|
|
114
|
+
const HOOKED = JSON.stringify({
|
|
115
|
+
id: 'forgejo',
|
|
116
|
+
hooks: { on_backup: { script: './backup.ts' } },
|
|
117
|
+
backup: { schedule: 'daily' },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function moduleRow(id: string, manifest: string): void {
|
|
121
|
+
db.$client.run(
|
|
122
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data, state)
|
|
123
|
+
VALUES ('${id}', '${id}', '1.0.0', '/path', '${manifest}', 'VERIFIED')`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function backupRow(id: string, moduleId: string, status: string, completedAtMs: number): void {
|
|
128
|
+
db.$client.run(
|
|
129
|
+
`INSERT INTO backup_storages (id, storage_id, name, provider_name, credentials_encrypted, provider_config)
|
|
130
|
+
VALUES ('st-1', 'aws', 'AWS', 's3', 'x', '{}') ON CONFLICT DO NOTHING`,
|
|
131
|
+
);
|
|
132
|
+
db.$client.run(
|
|
133
|
+
`INSERT INTO backups (id, module_id, storage_id, storage_path, backup_type, status, started_at, completed_at, metadata)
|
|
134
|
+
VALUES ('${id}', '${moduleId}', 'st-1', 'p/${id}', 'module_data', '${status}',
|
|
135
|
+
${Math.floor(completedAtMs / 1000)}, ${Math.floor(completedAtMs / 1000)}, '{}')`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function monitorRow(target: string, opts: { policy?: boolean; ran?: boolean }): void {
|
|
140
|
+
db.$client.run(
|
|
141
|
+
`INSERT INTO monitors (id, kind, target, interval_minutes, enabled, escalation_policy_id, last_run_at)
|
|
142
|
+
VALUES ('mon-${target}', 'module_hook', '${target}', 60, 1, ${opts.policy ? "'pol-1'" : 'NULL'},
|
|
143
|
+
${opts.ran ? Math.floor(Date.now() / 1000) : 'NULL'})`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function moduleOf(id: string) {
|
|
148
|
+
const found = consoleStatus(db).modules.find((m) => m.id === id);
|
|
149
|
+
if (!found) throw new Error(`${id} missing from the projection`);
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
beforeEach(async () => {
|
|
154
|
+
db = await setupTestDatabase();
|
|
155
|
+
db.$client.run(
|
|
156
|
+
`INSERT INTO escalation_policies (id, name) VALUES ('pol-1', 'oncall') ON CONFLICT DO NOTHING`,
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterEach(async () => {
|
|
161
|
+
await cleanupTestDatabase(db);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('lastBackupAt is epoch MILLISECONDS', () => {
|
|
165
|
+
// The whole reason this test exists. It was seconds, the protocol on the
|
|
166
|
+
// other side says milliseconds, and nothing joined the two yet — so the
|
|
167
|
+
// factor of a thousand sat there looking like working code.
|
|
168
|
+
const at = Date.now() - 2 * DAY;
|
|
169
|
+
moduleRow('forgejo', HOOKED);
|
|
170
|
+
backupRow('b1', 'forgejo', 'completed', at);
|
|
171
|
+
|
|
172
|
+
const lastBackupAt = moduleOf('forgejo').lastBackupAt;
|
|
173
|
+
expect(lastBackupAt).not.toBeNull();
|
|
174
|
+
// Within a day of now in ms. In seconds this would be ~1.8 billion, which
|
|
175
|
+
// is fifty-odd years, and would render as an absurd age rather than throw.
|
|
176
|
+
expect(Date.now() - (lastBackupAt as number)).toBeLessThan(3 * DAY);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('a module with no monitor at all is unwatched', () => {
|
|
180
|
+
moduleRow('forgejo', HOOKED);
|
|
181
|
+
expect(moduleOf('forgejo').unwatched).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('a monitor that has NEVER RUN is not coverage', () => {
|
|
185
|
+
// It produces exactly as much evidence as no monitor does.
|
|
186
|
+
moduleRow('forgejo', HOOKED);
|
|
187
|
+
monitorRow('forgejo', { policy: true, ran: false });
|
|
188
|
+
expect(moduleOf('forgejo').unwatched).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('a monitor that runs makes the module watched', () => {
|
|
192
|
+
moduleRow('forgejo', HOOKED);
|
|
193
|
+
monitorRow('forgejo', { policy: true, ran: true });
|
|
194
|
+
expect(moduleOf('forgejo').unwatched).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('a running monitor with no escalation policy pages nobody', () => {
|
|
198
|
+
// Worse than unwatched in one respect: it looks monitored. The alert is
|
|
199
|
+
// raised on time and reaches no one.
|
|
200
|
+
moduleRow('forgejo', HOOKED);
|
|
201
|
+
monitorRow('forgejo', { policy: false, ran: true });
|
|
202
|
+
const module = moduleOf('forgejo');
|
|
203
|
+
expect(module.unwatched).toBe(false);
|
|
204
|
+
expect(module.pagesNobody).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('an overdue daily backup is stale', () => {
|
|
208
|
+
moduleRow('forgejo', HOOKED);
|
|
209
|
+
backupRow('b1', 'forgejo', 'completed', Date.now() - 5 * DAY);
|
|
210
|
+
expect(moduleOf('forgejo').backupStale).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('a fresh daily backup is not', () => {
|
|
214
|
+
moduleRow('forgejo', HOOKED);
|
|
215
|
+
backupRow('b1', 'forgejo', 'completed', Date.now() - 60_000);
|
|
216
|
+
expect(moduleOf('forgejo').backupStale).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('a module with NO on_backup hook is never stale', () => {
|
|
220
|
+
// There is nothing to run. Reporting eighteen such modules as overdue is
|
|
221
|
+
// how this page becomes an alarm nobody can act on (celilo#1131).
|
|
222
|
+
moduleRow('caddy', JSON.stringify({ id: 'caddy' }));
|
|
223
|
+
expect(moduleOf('caddy').backupStale).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('a hooked module that has NEVER been backed up is stale', () => {
|
|
227
|
+
// Distinct from the case above: this one was supposed to run and did not.
|
|
228
|
+
moduleRow('forgejo', HOOKED);
|
|
229
|
+
expect(moduleOf('forgejo').backupStale).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The reach probe for the chain (CLAUDE.md, "a check that cannot reach the
|
|
235
|
+
* thing it is checking").
|
|
236
|
+
*
|
|
237
|
+
* `orderFirewallChain` is unit-tested against hand-built rows. That says
|
|
238
|
+
* nothing about whether the closure's loader queries the right table with the
|
|
239
|
+
* right filter, and a wrong query here returns a well-formed empty list that
|
|
240
|
+
* reads exactly like a fleet with one firewall. So this drives the REAL loader
|
|
241
|
+
* against real `capabilities` rows.
|
|
242
|
+
*/
|
|
243
|
+
describe('loadClosureInputs, and the chain it carries', () => {
|
|
244
|
+
let db: DbClient;
|
|
245
|
+
|
|
246
|
+
beforeEach(async () => {
|
|
247
|
+
db = await setupTestDatabase();
|
|
248
|
+
for (const id of ['iptables', 'axon', 'caddy']) {
|
|
249
|
+
db.$client.run(
|
|
250
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data, state)
|
|
251
|
+
VALUES ('${id}', '${id}', '1.0.0', '/path', '{}', 'VERIFIED')`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
afterEach(async () => {
|
|
257
|
+
await cleanupTestDatabase(db);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
function firewallProvider(moduleId: string, data: string) {
|
|
261
|
+
db.$client.run(
|
|
262
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data)
|
|
263
|
+
VALUES ('${moduleId}', 'firewall', '1.0.0', '${data}')`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
test('reads the live registration and orders it, iptables first', () => {
|
|
268
|
+
firewallProvider('iptables', '{"firewall_ip":"192.168.0.254","nat_ip":"192.168.0.253"}');
|
|
269
|
+
firewallProvider('axon', '{"has_external":true,"router_ip":"192.168.0.1"}');
|
|
270
|
+
|
|
271
|
+
expect(loadClosureInputs(db).chains).toEqual([
|
|
272
|
+
{ capability: 'firewall', moduleIds: ['iptables', 'axon'] },
|
|
273
|
+
]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('a non-firewall capability is not swept into the chain', () => {
|
|
277
|
+
// `axon` provides dhcp_server too. A loader that read every row would put a
|
|
278
|
+
// module in the packet's path on the strength of an unrelated capability.
|
|
279
|
+
firewallProvider('iptables', '{"firewall_ip":"192.168.0.254","nat_ip":"192.168.0.253"}');
|
|
280
|
+
firewallProvider('axon', '{"has_external":true}');
|
|
281
|
+
db.$client.run(
|
|
282
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data)
|
|
283
|
+
VALUES ('caddy', 'public_web', '1.0.0', '{}')`,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(loadClosureInputs(db).chains[0]?.moduleIds).toEqual(['iptables', 'axon']);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('a fleet with one firewall has no chain', () => {
|
|
290
|
+
firewallProvider('axon', '{"has_external":true}');
|
|
291
|
+
expect(loadClosureInputs(db).chains).toEqual([]);
|
|
292
|
+
});
|
|
293
|
+
});
|