@celilo/cli 1.7.0 → 1.9.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 +3 -0
- package/CELILO_SUBSYSTEMS.md +7 -1
- package/drizzle/0027_dns_internal_records_consumer_cascade.sql +43 -0
- package/drizzle/0028_capability_bindings.sql +26 -0
- package/drizzle/0029_module_instances.sql +58 -0
- package/drizzle/meta/_journal.json +22 -1
- package/package.json +2 -2
- package/src/capabilities/validation.test.ts +51 -0
- package/src/capabilities/validation.ts +22 -8
- package/src/cli/commands/module-show.ts +1 -0
- package/src/db/dns-internal-cascade-migration.test.ts +184 -0
- package/src/db/foreign-keys.test.ts +101 -0
- package/src/db/schema.ts +182 -9
- package/src/hooks/broker.test.ts +152 -0
- package/src/hooks/broker.ts +307 -0
- package/src/hooks/capability-loader-bindings.test.ts +163 -0
- package/src/hooks/capability-loader-firewall.test.ts +108 -0
- package/src/hooks/capability-loader.test.ts +10 -2
- package/src/hooks/capability-loader.ts +59 -2
- package/src/hooks/executor.ts +234 -111
- package/src/hooks/hook-protocol.test.ts +192 -0
- package/src/hooks/hook-protocol.ts +275 -0
- package/src/hooks/hook-runner.ts +231 -0
- package/src/hooks/hook-timeout.test.ts +103 -0
- package/src/hooks/hook-trespass.test.ts +201 -0
- package/src/hooks/injected-capabilities.test.ts +75 -0
- package/src/hooks/test-fixtures/capability-calling-hook.ts +79 -0
- package/src/hooks/test-fixtures/runaway-hook.ts +26 -0
- package/src/hooks/test-fixtures/sigterm-ignoring-hook.ts +22 -0
- package/src/manifest/template-validator.test.ts +47 -0
- package/src/manifest/template-validator.ts +18 -1
- package/src/manifest/validate-provider-views.test.ts +61 -0
- package/src/manifest/validate.ts +21 -14
- package/src/module/import.ts +19 -1
- package/src/module/packaging/module-state-directory.test.ts +99 -0
- package/src/module/packaging/package-rules.ts +10 -2
- package/src/policy/capability-shape-baseline.ts +96 -0
- package/src/policy/capability-shape-drift.test.ts +162 -0
- package/src/policy/capability-shape.ts +129 -0
- package/src/policy/dns-aspect-coverage.test.ts +100 -0
- package/src/policy/module-business-baseline.ts +68 -7
- package/src/services/alerting/ack.test.ts +2 -2
- package/src/services/alerting/deferral.test.ts +2 -2
- package/src/services/alerting/delivery-loop.test.ts +2 -2
- package/src/services/alerting/deploy-hooks.test.ts +2 -2
- package/src/services/alerting/inbound-poller.test.ts +2 -2
- package/src/services/alerting/inbound.test.ts +2 -2
- package/src/services/alerting/notification-responder.test.ts +2 -2
- package/src/services/alerting/run-monitor.test.ts +2 -2
- package/src/services/alerting/store.test.ts +2 -2
- package/src/services/alerting/sweep-runner.test.ts +2 -2
- package/src/services/alerting/tokens.test.ts +2 -2
- package/src/services/capability-bindings.test.ts +104 -0
- package/src/services/capability-bindings.ts +107 -0
- package/src/services/capability-table-rows.test.ts +191 -0
- package/src/services/capability-table-rows.ts +103 -0
- package/src/services/consumer-cleanup.test.ts +40 -3
- package/src/services/consumer-cleanup.ts +13 -7
- package/src/services/dns-internal-records.test.ts +74 -3
- package/src/services/fleet-checks.test.ts +4 -4
- package/src/services/module-instances.test.ts +198 -0
- package/src/services/module-instances.ts +96 -0
- package/src/services/module-journal.test.ts +2 -2
- package/src/services/module-subscriptions.test.ts +1 -1
- package/src/services/module-validator/capability-versions.test.ts +6 -1
- package/src/services/port-forwards.test.ts +8 -4
- package/src/services/port-forwards.ts +0 -11
- package/src/services/trusted-sources.test.ts +3 -3
- package/src/services/trusted-sources.ts +0 -5
- package/src/templates/ingress-ip.test.ts +31 -0
- package/src/test-utils/database.ts +31 -1
- package/src/variables/context.ts +75 -10
- package/src/variables/lxc-nameserver.test.ts +144 -0
- package/src/test-utils/setup-test-db.ts +0 -80
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { rm } from 'node:fs/promises';
|
|
4
|
+
import { eq } from 'drizzle-orm';
|
|
4
5
|
import { type DbClient, createDbClient } from '../db/client';
|
|
5
6
|
import { capabilities, moduleConfigs, modules, systemConfig } from '../db/schema';
|
|
6
7
|
import { buildResolutionContext } from './context';
|
|
@@ -67,6 +68,71 @@ describe('lxc_nameserver composition', () => {
|
|
|
67
68
|
.run();
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Put the consumer in `zone` instead of the default `app`. */
|
|
72
|
+
function placeConsumerIn(zone: string): void {
|
|
73
|
+
db.update(modules)
|
|
74
|
+
.set({ manifestData: { requires: { system: { zone } } } })
|
|
75
|
+
.where(eq(modules.id, 'consumer'))
|
|
76
|
+
.run();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A dns_internal provider that advertises a zone-routable endpoint AND
|
|
81
|
+
* declares a base-module aspect, the way `knot-unbound-internal` does. The
|
|
82
|
+
* aspect's `applicable_zones` is what decides whether a system's ongoing DNS
|
|
83
|
+
* has an owner at all (design D5d).
|
|
84
|
+
*/
|
|
85
|
+
function registerManagedPrimary(opts: {
|
|
86
|
+
ip: string;
|
|
87
|
+
internalIp?: string;
|
|
88
|
+
aspectZones: string[];
|
|
89
|
+
}): void {
|
|
90
|
+
db.insert(modules)
|
|
91
|
+
.values({
|
|
92
|
+
id: 'dns-provider',
|
|
93
|
+
name: 'dns-provider',
|
|
94
|
+
version: '1.0.0',
|
|
95
|
+
manifestData: {},
|
|
96
|
+
sourcePath: '/tmp/dns',
|
|
97
|
+
})
|
|
98
|
+
.run();
|
|
99
|
+
db.insert(capabilities)
|
|
100
|
+
.values({
|
|
101
|
+
moduleId: 'dns-provider',
|
|
102
|
+
capabilityName: 'dns_internal',
|
|
103
|
+
version: '1.0.0',
|
|
104
|
+
data: {
|
|
105
|
+
server: { ip: opts.ip, internal_ip: opts.internalIp },
|
|
106
|
+
// The provider DECLARES what its aspect covers, rather than core
|
|
107
|
+
// finding the module row and reading its manifest. A manifest test
|
|
108
|
+
// holds this list and `base_module_aspect.applicable_zones` together.
|
|
109
|
+
aspect: { covered_zones: opts.aspectZones },
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
.run();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The secondary publishes its OWN capability, never a field on the primary's. */
|
|
116
|
+
function registerSecondary(opts: { ip: string; internalIp?: string }): void {
|
|
117
|
+
db.insert(modules)
|
|
118
|
+
.values({
|
|
119
|
+
id: 'dns-secondary',
|
|
120
|
+
name: 'dns-secondary',
|
|
121
|
+
version: '1.0.0',
|
|
122
|
+
manifestData: {},
|
|
123
|
+
sourcePath: '/tmp/dns2',
|
|
124
|
+
})
|
|
125
|
+
.run();
|
|
126
|
+
db.insert(capabilities)
|
|
127
|
+
.values({
|
|
128
|
+
moduleId: 'dns-secondary',
|
|
129
|
+
capabilityName: 'dns_internal_secondary',
|
|
130
|
+
version: '1.0.0',
|
|
131
|
+
data: { server: { ip: opts.ip, internal_ip: opts.internalIp } },
|
|
132
|
+
})
|
|
133
|
+
.run();
|
|
134
|
+
}
|
|
135
|
+
|
|
70
136
|
test('internal resolver first (CIDR stripped) + public fallback', async () => {
|
|
71
137
|
registerInternalDns('192.168.0.151/24');
|
|
72
138
|
const ctx = await buildResolutionContext('consumer', db);
|
|
@@ -83,4 +149,82 @@ describe('lxc_nameserver composition', () => {
|
|
|
83
149
|
const ctx = await buildResolutionContext('consumer', db);
|
|
84
150
|
expect(ctx.selfConfig.lxc_nameserver).toBe('10.0.20.30 1.1.1.1 1.0.0.1 8.8.8.8');
|
|
85
151
|
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Terraform injects `lifecycle { ignore_changes = [nameserver] }` into every
|
|
155
|
+
* `proxmox_lxc`, so it cannot correct the birth value afterwards even in
|
|
156
|
+
* principle. `lxc-dns-at-birth` splits ownership accordingly: terraform owns
|
|
157
|
+
* birth DNS, the base-module aspect owns ongoing DNS. A zone the aspect does
|
|
158
|
+
* not cover therefore has NO owner for ongoing DNS — not a late one, none —
|
|
159
|
+
* and the birth list is that system's permanent configuration.
|
|
160
|
+
*
|
|
161
|
+
* That is why dropping the public resolvers is a TIGHTENING in an
|
|
162
|
+
* aspect-covered zone and an OUTRIGHT BREAK outside one, from the same edit
|
|
163
|
+
* (design D5d). These assert on the composed CONFIGURATION rather than on a
|
|
164
|
+
* lookup result, deliberately: a resolution assertion passes whenever the
|
|
165
|
+
* internal resolver happens to be up, which is nearly always, so it would
|
|
166
|
+
* flake in the direction that reads as harness noise. The production symptom
|
|
167
|
+
* is a correct-looking answer from the wrong view, and this string is the
|
|
168
|
+
* only place that is ever visible.
|
|
169
|
+
*/
|
|
170
|
+
describe('a managed primary/secondary pair (design D5, D5d, D5e)', () => {
|
|
171
|
+
/**
|
|
172
|
+
* Both resolvers are reached the SAME way, and that is a consequence of
|
|
173
|
+
* placement rather than a convenience. Design D5e puts the secondary in
|
|
174
|
+
* `dmz` alongside the primary, so from dmz/app/secure both answer at their
|
|
175
|
+
* own zone addresses, and from `internal` — which cannot route into the dmz
|
|
176
|
+
* subnet — both answer at an internal-subnet address the firewall DNATs.
|
|
177
|
+
*
|
|
178
|
+
* This is worth stating because the symmetry is exactly what a reader
|
|
179
|
+
* should check rather than assume. Jeremy's version placed the secondary in
|
|
180
|
+
* `internal` and therefore selected its endpoints the other way round
|
|
181
|
+
* (native address for internal clients, a dmz ingress for everyone else).
|
|
182
|
+
* Under that placement this symmetric selection would be WRONG, and these
|
|
183
|
+
* tests would still pass, because the same premise would be in the test and
|
|
184
|
+
* in the code. If the secondary ever moves out of `dmz`, this block is what
|
|
185
|
+
* has to change first.
|
|
186
|
+
*/
|
|
187
|
+
test('an aspect-covered zone drops the public resolvers', async () => {
|
|
188
|
+
registerManagedPrimary({ ip: '10.0.10.5', aspectZones: ['dmz', 'app', 'secure'] });
|
|
189
|
+
registerSecondary({ ip: '10.0.10.6' });
|
|
190
|
+
const ctx = await buildResolutionContext('consumer', db);
|
|
191
|
+
expect(ctx.selfConfig.lxc_nameserver).toBe('10.0.10.5 10.0.10.6');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('a zone NO aspect covers keeps them, because nothing will ever rewrite it', async () => {
|
|
195
|
+
placeConsumerIn('external');
|
|
196
|
+
registerManagedPrimary({ ip: '10.0.10.5', aspectZones: ['dmz', 'app', 'secure'] });
|
|
197
|
+
registerSecondary({ ip: '10.0.10.6' });
|
|
198
|
+
const ctx = await buildResolutionContext('consumer', db);
|
|
199
|
+
// An `external` VPS sits outside the perimeter with no route to a
|
|
200
|
+
// dmz-resident resolver. Handing it two unreachable addresses and nothing
|
|
201
|
+
// else does not tighten anything; it takes DNS away. So the list stays
|
|
202
|
+
// what it has always been here — the primary, then the public resolvers
|
|
203
|
+
// that are the only ones it can actually reach. The secondary is not
|
|
204
|
+
// added either: a second unreachable address costs another timeout.
|
|
205
|
+
expect(ctx.selfConfig.lxc_nameserver).toBe('10.0.10.5 1.1.1.1 1.0.0.1 8.8.8.8');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('an internal-zone system takes the zone-routable endpoints', async () => {
|
|
209
|
+
placeConsumerIn('internal');
|
|
210
|
+
registerManagedPrimary({
|
|
211
|
+
ip: '10.0.10.5',
|
|
212
|
+
internalIp: '192.168.0.5',
|
|
213
|
+
aspectZones: ['dmz', 'app', 'secure', 'internal'],
|
|
214
|
+
});
|
|
215
|
+
registerSecondary({ ip: '10.0.10.6', internalIp: '192.168.0.6' });
|
|
216
|
+
const ctx = await buildResolutionContext('consumer', db);
|
|
217
|
+
// `internal` cannot route into the dmz subnet, so it uses the ingress
|
|
218
|
+
// addresses the firewall DNATs — not the resolvers' own zone addresses.
|
|
219
|
+
expect(ctx.selfConfig.lxc_nameserver).toBe('192.168.0.5 192.168.0.6');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('a primary with no secondary keeps the public fallback', async () => {
|
|
223
|
+
registerManagedPrimary({ ip: '10.0.10.5', aspectZones: ['dmz', 'app', 'secure'] });
|
|
224
|
+
const ctx = await buildResolutionContext('consumer', db);
|
|
225
|
+
// Removing the fallback and shipping a secondary are ONE decision (D5a).
|
|
226
|
+
// Without a secondary, a resolver redeploy would blank fleet DNS.
|
|
227
|
+
expect(ctx.selfConfig.lxc_nameserver).toBe('10.0.10.5 1.1.1.1 1.0.0.1 8.8.8.8');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
86
230
|
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { unlink } from 'node:fs/promises';
|
|
3
|
-
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
|
|
4
|
-
import { type DbClient, createDbClient, findMigrationsFolder } from '../db/client';
|
|
5
|
-
|
|
6
|
-
// `findMigrationsFolder` is imported from db/client rather than redefined here.
|
|
7
|
-
// This file used to carry its own copy, and the copy had drifted: it listed
|
|
8
|
-
// only CWD-RELATIVE candidates (`./drizzle`, `process.cwd()/drizzle`, …) and was
|
|
9
|
-
// missing the `join(currentDir, '../../drizzle')` fallback that resolves
|
|
10
|
-
// relative to the source file.
|
|
11
|
-
//
|
|
12
|
-
// The effect was invisible from `apps/celilo` and total from the repo root —
|
|
13
|
-
// which is where CLAUDE.md says to run the gate. Every suite using
|
|
14
|
-
// setupTestDatabase threw here, left `db` unassigned, and then failed again in
|
|
15
|
-
// its own afterEach on `db.$client`, so one missing path candidate presented as
|
|
16
|
-
// hundreds of unrelated-looking assertion failures (celilo: 668 across the
|
|
17
|
-
// repo, 341 in apps/celilo/src alone).
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Setup test database with real migrations
|
|
21
|
-
*
|
|
22
|
-
* This helper ensures tests use the same schema as production by running
|
|
23
|
-
* actual migrations instead of manual CREATE TABLE statements.
|
|
24
|
-
*
|
|
25
|
-
* Benefits:
|
|
26
|
-
* - Single source of truth for schema
|
|
27
|
-
* - Tests automatically use latest schema
|
|
28
|
-
* - Validates migration correctness
|
|
29
|
-
*
|
|
30
|
-
* @param testDbPath - Path to test database file
|
|
31
|
-
* @returns Database client instance
|
|
32
|
-
*/
|
|
33
|
-
export async function setupTestDatabase(testDbPath: string) {
|
|
34
|
-
// Create database client
|
|
35
|
-
const db = createDbClient({ path: testDbPath });
|
|
36
|
-
|
|
37
|
-
// Find and run migrations
|
|
38
|
-
const migrationsFolder = findMigrationsFolder();
|
|
39
|
-
await migrate(db, { migrationsFolder });
|
|
40
|
-
|
|
41
|
-
return db;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Cleanup test database
|
|
46
|
-
*
|
|
47
|
-
* @param db - Database client to close
|
|
48
|
-
* @param testDbPath - Path to test database file to delete
|
|
49
|
-
*/
|
|
50
|
-
export async function cleanupTestDatabase(db: DbClient, testDbPath: string) {
|
|
51
|
-
// Close database connection
|
|
52
|
-
db.$client.close();
|
|
53
|
-
|
|
54
|
-
// Delete test database file
|
|
55
|
-
if (existsSync(testDbPath)) {
|
|
56
|
-
await unlink(testDbPath);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Also delete WAL and SHM files if they exist
|
|
60
|
-
const walPath = `${testDbPath}-wal`;
|
|
61
|
-
const shmPath = `${testDbPath}-shm`;
|
|
62
|
-
|
|
63
|
-
if (existsSync(walPath)) {
|
|
64
|
-
await unlink(walPath);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (existsSync(shmPath)) {
|
|
68
|
-
await unlink(shmPath);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Generate unique test database path
|
|
74
|
-
*
|
|
75
|
-
* @param prefix - Optional prefix for the database file (defaults to 'test-celilo')
|
|
76
|
-
* @returns Unique database file path
|
|
77
|
-
*/
|
|
78
|
-
export function generateTestDbPath(prefix = 'test-celilo'): string {
|
|
79
|
-
return `./${prefix}-${Date.now()}-${Math.random()}.db`;
|
|
80
|
-
}
|