@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE consumer-removal rule, driven by the declarations
|
|
3
|
+
* (openspec/changes/capability-owned-tables task 2.4).
|
|
4
|
+
*
|
|
5
|
+
* Before this, "the row dies with its consumer" had five implementations for one
|
|
6
|
+
* rule, and two of them were functions core imported BY NAME from
|
|
7
|
+
* `consumer-cleanup.ts` — the file whose whole purpose is deleting exactly that
|
|
8
|
+
* kind of per-capability special-casing.
|
|
9
|
+
*
|
|
10
|
+
* The property worth protecting is not "port forwards get deleted". It is that
|
|
11
|
+
* core clears EVERY declared table without naming any of them, so declaring a
|
|
12
|
+
* new one is sufficient to have it cleaned up.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
16
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { allDeclaredTables } from '@celilo/capabilities';
|
|
20
|
+
import type { DbClient } from '../db/client';
|
|
21
|
+
import { dnsInternalRecords, modules, portForwards, trustedSources, webRoutes } from '../db/schema';
|
|
22
|
+
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
23
|
+
import { deleteClaimedRows, planClaimedRowDeletion } from './capability-table-rows';
|
|
24
|
+
|
|
25
|
+
const FW = '192.168.0.254';
|
|
26
|
+
|
|
27
|
+
describe('planClaimedRowDeletion', () => {
|
|
28
|
+
/**
|
|
29
|
+
* The anti-regression that matters. If someone declares a table and this plan
|
|
30
|
+
* does not grow, their rows outlive the consumer silently — which is the whole
|
|
31
|
+
* failure the declaration exists to make impossible.
|
|
32
|
+
*/
|
|
33
|
+
it('covers every declared table, so declaring one is enough to have it cleared', () => {
|
|
34
|
+
const planned = new Set(planClaimedRowDeletion().map((t) => t.table));
|
|
35
|
+
const declared = allDeclaredTables().map((d) => d.declaration.table);
|
|
36
|
+
|
|
37
|
+
expect(declared.length).toBeGreaterThan(0);
|
|
38
|
+
for (const table of declared) expect(planned.has(table)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("carries each table's OWN claim column, which is spelled three ways", () => {
|
|
42
|
+
const byTable = new Map(planClaimedRowDeletion().map((t) => [t.table, t.claimColumn]));
|
|
43
|
+
|
|
44
|
+
expect(byTable.get('web_routes')).toBe('module_id');
|
|
45
|
+
expect(byTable.get('port_forwards')).toBe('registered_by');
|
|
46
|
+
expect(byTable.get('trusted_sources')).toBe('registered_by');
|
|
47
|
+
expect(byTable.get('dns_internal_records')).toBe('consumer_module_id');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('deleteClaimedRows', () => {
|
|
52
|
+
let dir: string;
|
|
53
|
+
let db: DbClient;
|
|
54
|
+
|
|
55
|
+
beforeEach(async () => {
|
|
56
|
+
dir = mkdtempSync(join(tmpdir(), 'claimed-'));
|
|
57
|
+
const dbPath = join(dir, 'celilo.db');
|
|
58
|
+
process.env.CELILO_DB_PATH = dbPath;
|
|
59
|
+
db = await setupTestDatabaseAt(dbPath);
|
|
60
|
+
|
|
61
|
+
for (const id of ['caddy', 'forgejo', 'technitium']) {
|
|
62
|
+
db.insert(modules)
|
|
63
|
+
.values({
|
|
64
|
+
id,
|
|
65
|
+
name: id,
|
|
66
|
+
version: '1.0.0',
|
|
67
|
+
state: 'VERIFIED',
|
|
68
|
+
sourcePath: `/tmp/${id}`,
|
|
69
|
+
manifestData: {},
|
|
70
|
+
})
|
|
71
|
+
.run();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
db.$client.close();
|
|
77
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
78
|
+
rmSync(dir, { recursive: true, force: true });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function seed(consumer: string, hostname: string, port: number, subnet: string) {
|
|
82
|
+
db.insert(webRoutes)
|
|
83
|
+
.values({
|
|
84
|
+
slug: `${consumer}-slug`,
|
|
85
|
+
moduleId: consumer,
|
|
86
|
+
type: 'reverse_proxy',
|
|
87
|
+
path: `/${consumer}`,
|
|
88
|
+
hostname,
|
|
89
|
+
})
|
|
90
|
+
.run();
|
|
91
|
+
db.insert(portForwards)
|
|
92
|
+
.values({
|
|
93
|
+
firewallIp: FW,
|
|
94
|
+
internalIp: '10.0.20.5',
|
|
95
|
+
port,
|
|
96
|
+
protocol: 'TCP',
|
|
97
|
+
registeredBy: consumer,
|
|
98
|
+
})
|
|
99
|
+
.run();
|
|
100
|
+
db.insert(trustedSources).values({ firewallIp: FW, subnet, registeredBy: consumer }).run();
|
|
101
|
+
db.insert(dnsInternalRecords)
|
|
102
|
+
.values({
|
|
103
|
+
providerModuleId: 'technitium',
|
|
104
|
+
consumerModuleId: consumer,
|
|
105
|
+
host: hostname,
|
|
106
|
+
ip: '10.0.20.5',
|
|
107
|
+
})
|
|
108
|
+
.run();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
it('clears the departing consumer from all four declared tables at once', () => {
|
|
112
|
+
seed('caddy', 'a.example.org', 443, '10.1.0.0/24');
|
|
113
|
+
|
|
114
|
+
const cleared = deleteClaimedRows(db, 'caddy');
|
|
115
|
+
|
|
116
|
+
expect(cleared.map((c) => c.table).sort()).toEqual([
|
|
117
|
+
'dns_internal_records',
|
|
118
|
+
'port_forwards',
|
|
119
|
+
'trusted_sources',
|
|
120
|
+
'web_routes',
|
|
121
|
+
]);
|
|
122
|
+
expect(db.select().from(webRoutes).all()).toHaveLength(0);
|
|
123
|
+
expect(db.select().from(portForwards).all()).toHaveLength(0);
|
|
124
|
+
expect(db.select().from(trustedSources).all()).toHaveLength(0);
|
|
125
|
+
expect(db.select().from(dnsInternalRecords).all()).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The refcount case, the bug most likely to ship silently. Two consumers each
|
|
130
|
+
* hold their own row, and one leaving must not withdraw what the other still
|
|
131
|
+
* needs.
|
|
132
|
+
*/
|
|
133
|
+
it("leaves another consumer's rows untouched", () => {
|
|
134
|
+
seed('caddy', 'a.example.org', 443, '10.1.0.0/24');
|
|
135
|
+
seed('forgejo', 'b.example.org', 2222, '10.2.0.0/24');
|
|
136
|
+
|
|
137
|
+
deleteClaimedRows(db, 'caddy');
|
|
138
|
+
|
|
139
|
+
expect(
|
|
140
|
+
db
|
|
141
|
+
.select()
|
|
142
|
+
.from(webRoutes)
|
|
143
|
+
.all()
|
|
144
|
+
.map((r) => r.moduleId),
|
|
145
|
+
).toEqual(['forgejo']);
|
|
146
|
+
expect(
|
|
147
|
+
db
|
|
148
|
+
.select()
|
|
149
|
+
.from(portForwards)
|
|
150
|
+
.all()
|
|
151
|
+
.map((r) => r.registeredBy),
|
|
152
|
+
).toEqual(['forgejo']);
|
|
153
|
+
expect(
|
|
154
|
+
db
|
|
155
|
+
.select()
|
|
156
|
+
.from(trustedSources)
|
|
157
|
+
.all()
|
|
158
|
+
.map((r) => r.registeredBy),
|
|
159
|
+
).toEqual(['forgejo']);
|
|
160
|
+
expect(
|
|
161
|
+
db
|
|
162
|
+
.select()
|
|
163
|
+
.from(dnsInternalRecords)
|
|
164
|
+
.all()
|
|
165
|
+
.map((r) => r.consumerModuleId),
|
|
166
|
+
).toEqual(['forgejo']);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* `dns_internal_records` is claimed by its CONSUMER, never its provider. The
|
|
171
|
+
* declaration cannot express anything else, which is what makes celilo#1010
|
|
172
|
+
* unrepresentable — but the runtime rule has to agree, or the declaration is
|
|
173
|
+
* decoration.
|
|
174
|
+
*/
|
|
175
|
+
it('does not treat the PROVIDER as the claimant of a dns_internal record', () => {
|
|
176
|
+
seed('caddy', 'a.example.org', 443, '10.1.0.0/24');
|
|
177
|
+
|
|
178
|
+
deleteClaimedRows(db, 'technitium');
|
|
179
|
+
|
|
180
|
+
expect(db.select().from(dnsInternalRecords).all()).toHaveLength(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('is a no-op for a module that claimed nothing', () => {
|
|
184
|
+
seed('caddy', 'a.example.org', 443, '10.1.0.0/24');
|
|
185
|
+
|
|
186
|
+
deleteClaimedRows(db, 'forgejo');
|
|
187
|
+
|
|
188
|
+
expect(db.select().from(webRoutes).all()).toHaveLength(1);
|
|
189
|
+
expect(db.select().from(portForwards).all()).toHaveLength(1);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acting on every capability-declared table without naming any of them
|
|
3
|
+
* (openspec/changes/capability-owned-tables, design D3).
|
|
4
|
+
*
|
|
5
|
+
* "This row dies with its consumer" had five implementations for one rule: an FK
|
|
6
|
+
* cascade for `web_routes`, two functions core imported BY NAME from
|
|
7
|
+
* `consumer-cleanup.ts` for `port_forwards` and `trusted_sources`, a second
|
|
8
|
+
* table pruned on read for `dns_registrations`, a cascade on both provider and
|
|
9
|
+
* consumer for `dns_internal_records`, and a hook rewriting a JSON blob for the
|
|
10
|
+
* rest. `consumer-cleanup.ts` exists specifically to delete that kind of
|
|
11
|
+
* per-capability special-casing. It succeeded for the hook dispatch and then
|
|
12
|
+
* grew two hardcoded imports for the rows, because the rows had no generic rule.
|
|
13
|
+
*
|
|
14
|
+
* This is the generic rule. The declaration names the claim column, so core no
|
|
15
|
+
* longer has to know that three tables spell it `module_id`, `registered_by` and
|
|
16
|
+
* `consumer_module_id`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { CAPABILITY_DECLARED_TABLES, allDeclaredTables } from '@celilo/capabilities';
|
|
20
|
+
import { type Column, eq, is } from 'drizzle-orm';
|
|
21
|
+
import { SQLiteTable, getTableConfig } from 'drizzle-orm/sqlite-core';
|
|
22
|
+
import type { DbClient } from '../db/client';
|
|
23
|
+
import * as dbSchema from '../db/schema';
|
|
24
|
+
|
|
25
|
+
export interface ClaimedRowTarget {
|
|
26
|
+
capability: string;
|
|
27
|
+
/** Physical table name. */
|
|
28
|
+
table: string;
|
|
29
|
+
/** The column holding the consumer's module id. */
|
|
30
|
+
claimColumn: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Physical table name → the drizzle table object, for the tables celilo can
|
|
35
|
+
* actually act on. Built by walking the schema rather than by a hand-written
|
|
36
|
+
* map, so a table cannot be declared and then silently unreachable.
|
|
37
|
+
*/
|
|
38
|
+
function schemaTables(): Map<string, SQLiteTable> {
|
|
39
|
+
const out = new Map<string, SQLiteTable>();
|
|
40
|
+
for (const value of Object.values(dbSchema)) {
|
|
41
|
+
if (!is(value, SQLiteTable)) continue;
|
|
42
|
+
out.set(getTableConfig(value).name, value);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* What a consumer's removal must clear. Pure — the list is a property of the
|
|
49
|
+
* declarations, so it is worth being able to assert without a database.
|
|
50
|
+
*
|
|
51
|
+
* A declaration naming a table that does not exist is skipped here rather than
|
|
52
|
+
* throwing. `policy/capability-shape-drift.test.ts` Scan A is what fails on
|
|
53
|
+
* that, loudly and at build time; a removal is the wrong moment to discover it,
|
|
54
|
+
* and refusing to clear the OTHER tables because one declaration is wrong would
|
|
55
|
+
* strand real rows.
|
|
56
|
+
*/
|
|
57
|
+
export function planClaimedRowDeletion(): ClaimedRowTarget[] {
|
|
58
|
+
const tables = schemaTables();
|
|
59
|
+
return allDeclaredTables()
|
|
60
|
+
.filter(({ declaration }) => tables.has(declaration.table))
|
|
61
|
+
.map(({ capability, declaration }) => ({
|
|
62
|
+
capability,
|
|
63
|
+
table: declaration.table,
|
|
64
|
+
claimColumn: declaration.claim.column,
|
|
65
|
+
}))
|
|
66
|
+
.sort((a, b) => a.table.localeCompare(b.table));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Delete every declared row this consumer claimed.
|
|
71
|
+
*
|
|
72
|
+
* Called AFTER every provider has been told the consumer is going (its D4): a
|
|
73
|
+
* provider's `on_consumer_removed` is a full converge, and converging from a set
|
|
74
|
+
* the rows had already been removed from would withdraw more than the departing
|
|
75
|
+
* consumer's share.
|
|
76
|
+
*
|
|
77
|
+
* `web_routes` and `dns_internal_records` also carry an FK cascade, so their
|
|
78
|
+
* rows would go anyway once the `modules` row is deleted. Doing it here as well
|
|
79
|
+
* is deliberate and not merely harmless: it makes the ORDER explicit rather than
|
|
80
|
+
* a consequence of when some other statement happens to run, and it is the same
|
|
81
|
+
* point in the sequence at which `port_forwards` and `trusted_sources` were
|
|
82
|
+
* already being cleared by hand.
|
|
83
|
+
*/
|
|
84
|
+
export function deleteClaimedRows(db: DbClient, consumer: string): ClaimedRowTarget[] {
|
|
85
|
+
const tables = schemaTables();
|
|
86
|
+
const cleared: ClaimedRowTarget[] = [];
|
|
87
|
+
for (const target of planClaimedRowDeletion()) {
|
|
88
|
+
const table = tables.get(target.table);
|
|
89
|
+
if (!table) continue;
|
|
90
|
+
const column = getTableConfig(table).columns.find((c) => c.name === target.claimColumn);
|
|
91
|
+
if (!column) continue;
|
|
92
|
+
db.delete(table)
|
|
93
|
+
.where(eq(column as Column, consumer))
|
|
94
|
+
.run();
|
|
95
|
+
cleared.push(target);
|
|
96
|
+
}
|
|
97
|
+
return cleared;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Capability names that declare at least one table — for logs and reports. */
|
|
101
|
+
export function capabilitiesWithDeclaredTables(): string[] {
|
|
102
|
+
return Object.keys(CAPABILITY_DECLARED_TABLES).sort();
|
|
103
|
+
}
|
|
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import type { DbClient } from '../db/client';
|
|
6
6
|
import type { ModuleManifest } from '../manifest/schema';
|
|
7
|
-
import {
|
|
7
|
+
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
8
8
|
import {
|
|
9
9
|
type CleanupTarget,
|
|
10
10
|
type ProviderRow,
|
|
@@ -167,6 +167,43 @@ describe('planConsumerCleanup', () => {
|
|
|
167
167
|
});
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
describe('planConsumerCleanup stays permissive after celilo#1072', () => {
|
|
171
|
+
it('notifies a provider the consumer has no recorded binding to', () => {
|
|
172
|
+
// celilo#1072 added `capability_bindings`, which records the provider a
|
|
173
|
+
// consumer actually CALLED. That set is precise and it is deliberately not
|
|
174
|
+
// this one. Cleanup must reach every provider that MIGHT hold minted state,
|
|
175
|
+
// including one a hook resolved, minted against, and never called again —
|
|
176
|
+
// and including one whose binding row was never written because the state
|
|
177
|
+
// was minted by an older celilo. Narrowing this to the recorded set would
|
|
178
|
+
// leave a site block in caddy or a DNAT rule in a ruleset with nothing left
|
|
179
|
+
// in the registry to reconcile it away, which is the exact failure
|
|
180
|
+
// `consumer-cleanup.ts` exists to prevent.
|
|
181
|
+
//
|
|
182
|
+
// The inputs here are manifest + provider rows and NOTHING ELSE. That is
|
|
183
|
+
// the assertion: no binding table is consulted, so no binding table can
|
|
184
|
+
// shrink the result.
|
|
185
|
+
const plan = planConsumerCleanup(
|
|
186
|
+
'tango-nexus',
|
|
187
|
+
manifest({ optional: ['external_web', 'public_web', 'source_forge', 'registry_publish'] }),
|
|
188
|
+
provides(
|
|
189
|
+
['cpanel-host', 'external_web'],
|
|
190
|
+
['caddy', 'public_web'],
|
|
191
|
+
['forgejo', 'source_forge'],
|
|
192
|
+
['celilo-registry', 'registry_publish'],
|
|
193
|
+
),
|
|
194
|
+
deployed('cpanel-host', 'caddy', 'forgejo', 'celilo-registry'),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(plan.map((t) => t.providerId)).toEqual([
|
|
198
|
+
'caddy',
|
|
199
|
+
'celilo-registry',
|
|
200
|
+
'cpanel-host',
|
|
201
|
+
'forgejo',
|
|
202
|
+
]);
|
|
203
|
+
expect(plan.every((t) => t.skip === undefined)).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
170
207
|
describe('runConsumerCleanup — the paused skip is reported, not silent', () => {
|
|
171
208
|
let dir: string;
|
|
172
209
|
let db: DbClient;
|
|
@@ -175,7 +212,7 @@ describe('runConsumerCleanup — the paused skip is reported, not silent', () =>
|
|
|
175
212
|
dir = mkdtempSync(join(tmpdir(), 'cc-'));
|
|
176
213
|
const dbPath = join(dir, 'celilo.db');
|
|
177
214
|
process.env.CELILO_DB_PATH = dbPath;
|
|
178
|
-
db = await
|
|
215
|
+
db = await setupTestDatabaseAt(dbPath);
|
|
179
216
|
});
|
|
180
217
|
afterEach(() => {
|
|
181
218
|
db.$client.close();
|
|
@@ -244,7 +281,7 @@ describe('runConsumerCleanup — dispatch', () => {
|
|
|
244
281
|
dir = mkdtempSync(join(tmpdir(), 'ccd-'));
|
|
245
282
|
const dbPath = join(dir, 'celilo.db');
|
|
246
283
|
process.env.CELILO_DB_PATH = dbPath;
|
|
247
|
-
db = await
|
|
284
|
+
db = await setupTestDatabaseAt(dbPath);
|
|
248
285
|
});
|
|
249
286
|
afterEach(() => {
|
|
250
287
|
db.$client.close();
|
|
@@ -23,8 +23,7 @@ import { capabilities, modules } from '../db/schema';
|
|
|
23
23
|
import { type RunNamedHookResult, runNamedHook } from '../hooks/run-named-hook';
|
|
24
24
|
import type { HookLogger } from '../hooks/types';
|
|
25
25
|
import type { ModuleManifest } from '../manifest/schema';
|
|
26
|
-
import {
|
|
27
|
-
import { deleteTrustedSourcesForModule } from './trusted-sources';
|
|
26
|
+
import { deleteClaimedRows } from './capability-table-rows';
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* States in which a module has never resolved a capability and therefore holds
|
|
@@ -213,11 +212,18 @@ export async function runConsumerCleanup(
|
|
|
213
212
|
);
|
|
214
213
|
}
|
|
215
214
|
|
|
216
|
-
// The rows, once every provider has converged without them (D4).
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
deleteTrustedSourcesForModule
|
|
215
|
+
// The rows, once every provider has converged without them (D4).
|
|
216
|
+
//
|
|
217
|
+
// Driven by the capability declarations rather than by name. This used to be
|
|
218
|
+
// two hardcoded imports, `deletePortForwardsForModule` and
|
|
219
|
+
// `deleteTrustedSourcesForModule`, which is the shape of the whole problem
|
|
220
|
+
// openspec/changes/capability-owned-tables addresses: this file exists to
|
|
221
|
+
// delete per-capability special-casing, it succeeded for the hook dispatch,
|
|
222
|
+
// and then grew two capability-named calls for the rows because the rows had
|
|
223
|
+
// no generic rule. Now they have one, and core no longer has to know that the
|
|
224
|
+
// claim column is spelled `module_id` on one table, `registered_by` on two and
|
|
225
|
+
// `consumer_module_id` on a fourth.
|
|
226
|
+
deleteClaimedRows(db, consumer);
|
|
221
227
|
|
|
222
228
|
return result;
|
|
223
229
|
}
|
|
@@ -4,9 +4,10 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import type { DnsRecordRequest } from '@celilo/capabilities';
|
|
6
6
|
import type { ViewOverride } from '@celilo/capabilities';
|
|
7
|
+
import { eq } from 'drizzle-orm';
|
|
7
8
|
import type { DbClient } from '../db/client';
|
|
8
|
-
import { modules } from '../db/schema';
|
|
9
|
-
import {
|
|
9
|
+
import { dnsInternalRecords, modules } from '../db/schema';
|
|
10
|
+
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
10
11
|
import {
|
|
11
12
|
listDnsInternalRecords,
|
|
12
13
|
listViewOverrides,
|
|
@@ -24,7 +25,7 @@ describe('dns-internal-records ledger', () => {
|
|
|
24
25
|
dir = mkdtempSync(join(tmpdir(), 'dns-int-'));
|
|
25
26
|
dbPath = join(dir, 'celilo.db');
|
|
26
27
|
process.env.CELILO_DB_PATH = dbPath;
|
|
27
|
-
db = await
|
|
28
|
+
db = await setupTestDatabaseAt(dbPath);
|
|
28
29
|
// FK targets: provider + consumer modules.
|
|
29
30
|
for (const id of ['technitium', 'forgejo']) {
|
|
30
31
|
db.insert(modules)
|
|
@@ -197,3 +198,73 @@ describe('dns-internal-records ledger', () => {
|
|
|
197
198
|
});
|
|
198
199
|
});
|
|
199
200
|
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* celilo#1010 — swapping the internal DNS provider must not delete the ledger.
|
|
204
|
+
*
|
|
205
|
+
* `dns_internal_records` used to cascade on `provider_module_id` as well as on
|
|
206
|
+
* the consumer, so removing `technitium` to install `knot-unbound-internal` took
|
|
207
|
+
* the fleet's entire internal DNS ledger with it, `zone_routable_ip` view
|
|
208
|
+
* overrides included — the durable desired state the resolver's split-horizon
|
|
209
|
+
* config is reconciled from. `web_routes` cascades on its consumer only, and the
|
|
210
|
+
* two tables' docblocks claimed to be siblings, so the divergence read as intent
|
|
211
|
+
* and was not.
|
|
212
|
+
*
|
|
213
|
+
* The claim on a capability-owned table is the CONSUMER
|
|
214
|
+
* (openspec/changes/capability-owned-tables D3/D8). Migration 0027 makes the
|
|
215
|
+
* table agree with the declaration, which cannot express anything else.
|
|
216
|
+
*/
|
|
217
|
+
describe('provider cascade (celilo#1010)', () => {
|
|
218
|
+
let dir: string;
|
|
219
|
+
let db: DbClient;
|
|
220
|
+
|
|
221
|
+
beforeEach(async () => {
|
|
222
|
+
dir = mkdtempSync(join(tmpdir(), 'dns-cascade-'));
|
|
223
|
+
const dbPath = join(dir, 'celilo.db');
|
|
224
|
+
process.env.CELILO_DB_PATH = dbPath;
|
|
225
|
+
db = await setupTestDatabaseAt(dbPath);
|
|
226
|
+
for (const id of ['technitium', 'knot-unbound-internal', 'caddy']) {
|
|
227
|
+
db.insert(modules)
|
|
228
|
+
.values({
|
|
229
|
+
id,
|
|
230
|
+
name: id,
|
|
231
|
+
version: '1.0.0',
|
|
232
|
+
state: 'VERIFIED',
|
|
233
|
+
sourcePath: `/tmp/${id}`,
|
|
234
|
+
manifestData: {},
|
|
235
|
+
})
|
|
236
|
+
.run();
|
|
237
|
+
}
|
|
238
|
+
db.insert(dnsInternalRecords)
|
|
239
|
+
.values({
|
|
240
|
+
providerModuleId: 'technitium',
|
|
241
|
+
consumerModuleId: 'caddy',
|
|
242
|
+
host: 'auth.example.org',
|
|
243
|
+
ip: '192.168.0.253',
|
|
244
|
+
zoneRoutableIp: '10.0.10.14',
|
|
245
|
+
})
|
|
246
|
+
.run();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
afterEach(() => {
|
|
250
|
+
db.$client.close();
|
|
251
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
252
|
+
rmSync(dir, { recursive: true, force: true });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('keeps the ledger when the PROVIDER is removed, view overrides included', () => {
|
|
256
|
+
db.delete(modules).where(eq(modules.id, 'technitium')).run();
|
|
257
|
+
|
|
258
|
+
const left = db.select().from(dnsInternalRecords).all();
|
|
259
|
+
expect(left).toHaveLength(1);
|
|
260
|
+
// The override is the part whose loss is silent: the resolver keeps
|
|
261
|
+
// answering, just with the wrong address for in-zone clients.
|
|
262
|
+
expect(left[0]?.zoneRoutableIp).toBe('10.0.10.14');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('still dies with its CONSUMER, which is the rule that did not change', () => {
|
|
266
|
+
db.delete(modules).where(eq(modules.id, 'caddy')).run();
|
|
267
|
+
|
|
268
|
+
expect(db.select().from(dnsInternalRecords).all()).toHaveLength(0);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
systemConfig,
|
|
14
14
|
} from '../db/schema';
|
|
15
15
|
import type { ModuleManifest } from '../manifest/schema';
|
|
16
|
-
import {
|
|
16
|
+
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
17
17
|
import { ensureInboundSubscriber, ensureSweepSubscriber } from './alerting/monitors';
|
|
18
18
|
import { ensureBackupSweepSubscriber } from './backup-sweep';
|
|
19
19
|
import { getDaemonUnitPath } from './events-daemon';
|
|
@@ -263,7 +263,7 @@ describe('checkSubscribers + checkCapabilityProviders', () => {
|
|
|
263
263
|
busPath = join(dir, 'events.db');
|
|
264
264
|
process.env.CELILO_DB_PATH = dbPath;
|
|
265
265
|
process.env.EVENT_BUS_DB = busPath;
|
|
266
|
-
db = await
|
|
266
|
+
db = await setupTestDatabaseAt(dbPath);
|
|
267
267
|
bus = openBus({ dbPath: busPath, events: defineEvents({}) });
|
|
268
268
|
});
|
|
269
269
|
afterEach(() => {
|
|
@@ -737,7 +737,7 @@ describe('checkControlPlaneNetwork', () => {
|
|
|
737
737
|
|
|
738
738
|
beforeEach(async () => {
|
|
739
739
|
cpDir = mkdtempSync(join(tmpdir(), 'fleet-cpn-'));
|
|
740
|
-
db = await
|
|
740
|
+
db = await setupTestDatabaseAt(join(cpDir, 'celilo.db'));
|
|
741
741
|
});
|
|
742
742
|
|
|
743
743
|
afterEach(() => {
|
|
@@ -971,7 +971,7 @@ describe('runFleetChecks includes host liveness (#728)', () => {
|
|
|
971
971
|
dir = mkdtempSync(join(tmpdir(), 'fleet-liveness-'));
|
|
972
972
|
process.env.CELILO_DB_PATH = join(dir, 'celilo.db');
|
|
973
973
|
process.env.EVENT_BUS_DB = join(dir, 'events.db');
|
|
974
|
-
db = await
|
|
974
|
+
db = await setupTestDatabaseAt(join(dir, 'celilo.db'));
|
|
975
975
|
bus = openBus({ dbPath: join(dir, 'events.db'), events: defineEvents({}) });
|
|
976
976
|
});
|
|
977
977
|
|