@celilo/cli 2.1.0 → 2.2.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/drizzle/0031_module_config_source.sql +20 -0
- package/drizzle/meta/_journal.json +8 -1
- package/package.json +3 -3
- package/schemas/system_config.json +7 -1
- package/src/ansible/inventory.test.ts +2 -1
- package/src/api/sessions.test.ts +2 -1
- package/src/capabilities/public-web-publish.test.ts +61 -0
- package/src/cli/backup-rename.test.ts +2 -1
- package/src/cli/cli.test.ts +2 -1
- package/src/cli/commands/console-get-chain.test.ts +2 -1
- package/src/cli/commands/firewall-interface-list.test.ts +158 -8
- package/src/cli/commands/firewall-interface-list.ts +73 -7
- package/src/cli/commands/machine-add.ts +12 -55
- package/src/cli/commands/module-config.test.ts +22 -2
- package/src/cli/commands/module-deploy.ts +8 -2
- package/src/cli/commands/module-generate.test.ts +53 -0
- package/src/cli/commands/module-generate.ts +31 -26
- package/src/cli/commands/module-import-aspect.test.ts +2 -1
- package/src/cli/commands/module-import-registry.test.ts +2 -1
- package/src/cli/commands/module-import.ts +1 -1
- package/src/cli/commands/module-operations.test.ts +2 -1
- package/src/cli/commands/module-publish.test.ts +5 -12
- package/src/cli/commands/module-update.test.ts +87 -4
- package/src/cli/commands/module-update.ts +14 -4
- package/src/cli/commands/module-upgrade.test.ts +15 -0
- package/src/cli/commands/module-upgrade.ts +54 -2
- package/src/cli/commands/module-verify.test.ts +2 -3
- package/src/cli/commands/module-verify.ts +0 -1
- package/src/cli/commands/monitor.ts +2 -10
- package/src/cli/commands/notify-config.test.ts +5 -3
- package/src/cli/commands/registry-owner.test.ts +2 -1
- package/src/cli/commands/registry-token.test.ts +2 -1
- package/src/cli/commands/restore.ts +16 -6
- package/src/cli/commands/system-apply-config-equivalence.test.ts +5 -7
- package/src/cli/commands/system-config.test.ts +148 -0
- package/src/cli/commands/system-config.ts +26 -1
- package/src/cli/commands/system-doctor.test.ts +71 -0
- package/src/cli/commands/system-doctor.ts +110 -24
- package/src/cli/commands/system-init-deprecation.test.ts +6 -3
- package/src/cli/commands/system-migrate.test.ts +2 -1
- package/src/cli/generate-zsh-completion.ts +1 -1
- package/src/cli/index.ts +6 -4
- package/src/cli/restore-command.test.ts +2 -1
- package/src/cli/restore-migration-failure.test.ts +160 -0
- package/src/config/paths.test.ts +3 -3
- package/src/db/client.ts +5 -0
- package/src/db/migrate.test.ts +62 -135
- package/src/db/migrate.ts +17 -12
- package/src/db/schema.ts +10 -0
- package/src/hooks/broker.test.ts +106 -2
- package/src/hooks/broker.ts +91 -1
- package/src/hooks/capability-loader-firewall.test.ts +37 -0
- package/src/hooks/capability-loader.test.ts +78 -0
- package/src/hooks/capability-loader.ts +52 -3
- package/src/hooks/define-hook.test.ts +4 -3
- package/src/hooks/executor.test.ts +106 -19
- package/src/hooks/executor.ts +120 -13
- package/src/hooks/hook-jail-toolchain-reach.test.ts +3 -2
- package/src/hooks/hook-jail-unreachability.test.ts +4 -3
- package/src/hooks/hook-protocol.ts +46 -1
- package/src/hooks/hook-runner.ts +36 -0
- package/src/hooks/hook-store-proxy.test.ts +109 -0
- package/src/hooks/hook-store-proxy.ts +85 -0
- package/src/hooks/hook-store.test.ts +168 -0
- package/src/hooks/hook-store.ts +290 -0
- package/src/hooks/hook-timeout.test.ts +3 -2
- package/src/hooks/hook-trespass.test.ts +39 -5
- package/src/hooks/jail.test.ts +62 -3
- package/src/hooks/jail.ts +67 -8
- package/src/hooks/mount-set.test.ts +208 -0
- package/src/hooks/mount-set.ts +62 -14
- package/src/hooks/run-named-hook.ts +2 -0
- package/src/hooks/test-fixtures/jail-probe-hook.ts +1 -1
- package/src/hooks/test-fixtures/on-restore-staging-hook.ts +26 -0
- package/src/hooks/test-fixtures/store-backed.ts +47 -0
- package/src/hooks/test-fixtures/store-writing-hook.ts +63 -0
- package/src/hooks/unjailed-lint.test.ts +22 -6
- package/src/manifest/schema.ts +1 -0
- package/src/module/packaging/audit.ts +9 -26
- package/src/module/packaging/build-paths.test.ts +127 -0
- package/src/module/packaging/build-paths.ts +175 -0
- package/src/module/packaging/build.test.ts +71 -1
- package/src/module/packaging/build.ts +130 -2
- package/src/module/packaging/extract.ts +1 -5
- package/src/module/web-root.ts +17 -1
- package/src/policy/fixture-capability-coverage.test.ts +322 -0
- package/src/policy/module-script-scan.test.ts +42 -1
- package/src/policy/module-script-scan.ts +235 -5
- package/src/policy/no-hand-built-ssh.test.ts +34 -1
- package/src/policy/no-swallowed-refusal.test.ts +264 -0
- package/src/policy/no-tar-shell-out-in-services.test.ts +43 -0
- package/src/registry/client.test.ts +149 -0
- package/src/registry/client.ts +203 -11
- package/src/services/alerting/ack.test.ts +2 -1
- package/src/services/alerting/cadence-migration.test.ts +3 -2
- package/src/services/alerting/coverage-source.test.ts +2 -1
- package/src/services/alerting/deferral.test.ts +2 -1
- package/src/services/alerting/delivery-loop.test.ts +2 -1
- package/src/services/alerting/deploy-hooks.test.ts +2 -1
- package/src/services/alerting/format.test.ts +57 -0
- package/src/services/alerting/format.ts +24 -0
- package/src/services/alerting/inbound-poller.test.ts +2 -1
- package/src/services/alerting/inbound.test.ts +2 -1
- package/src/services/alerting/notification-responder.test.ts +2 -1
- package/src/services/alerting/run-monitor.test.ts +2 -1
- package/src/services/alerting/run-monitor.ts +2 -2
- package/src/services/alerting/store.test.ts +2 -1
- package/src/services/alerting/sweep-runner.test.ts +2 -1
- package/src/services/alerting/tokens.test.ts +2 -1
- package/src/services/aspect-approvals.test.ts +2 -1
- package/src/services/aspect-reconcile.test.ts +4 -3
- package/src/services/aspect-runner.test.ts +2 -1
- package/src/services/audit/module-integrity.test.ts +0 -21
- package/src/services/audit/module-integrity.ts +0 -14
- package/src/services/backup-age-agreement.test.ts +2 -1
- package/src/services/backup-create.ts +7 -7
- package/src/services/backup-envelope-roundtrip.test.ts +47 -3
- package/src/services/backup-in-flight-refusal.test.ts +2 -1
- package/src/services/backup-restore.ts +8 -4
- package/src/services/bus-ensure-flow.test.ts +2 -1
- package/src/services/bus-interview-park.test.ts +2 -1
- package/src/services/bus-interview.ts +37 -14
- package/src/services/bus-secret-flow.test.ts +2 -1
- package/src/services/capability-table-rows.test.ts +2 -1
- package/src/services/config-provenance.ts +4 -0
- package/src/services/consumer-cleanup.test.ts +3 -2
- package/src/services/container-service.test.ts +2 -1
- package/src/services/control-plane-bootstrap.test.ts +123 -2
- package/src/services/control-plane-bootstrap.ts +51 -4
- package/src/services/cross-module-read.test.ts +2 -1
- package/src/services/deploy-preflight.ts +8 -2
- package/src/services/deploy-validation.test.ts +25 -2
- package/src/services/deploy-validation.ts +8 -0
- package/src/services/dns-discovery.test.ts +54 -0
- package/src/services/dns-discovery.ts +47 -5
- package/src/services/dns-internal-records.test.ts +3 -2
- package/src/services/dns-provider-backfill.test.ts +2 -1
- package/src/services/dns-registrations.test.ts +2 -1
- package/src/services/ensure-interview.test.ts +3 -2
- package/src/services/fleet-checks.test.ts +3 -2
- package/src/services/fleet-key.test.ts +68 -3
- package/src/services/fleet-key.ts +54 -0
- package/src/services/health-runner.ts +2 -0
- package/src/services/infrastructure-selector.test.ts +2 -1
- package/src/services/infrastructure-variable-resolver.test.ts +2 -1
- package/src/services/machine-pool.test.ts +2 -1
- package/src/services/module-config.test.ts +2 -1
- package/src/services/module-config.ts +20 -2
- package/src/services/module-deploy.dns-repoint.test.ts +188 -0
- package/src/services/module-deploy.ts +199 -21
- package/src/services/module-operations.test.ts +2 -1
- package/src/services/module-subscriptions.test.ts +2 -1
- package/src/services/module-validator/git-hygiene.test.ts +122 -3
- package/src/services/module-validator/git-hygiene.ts +83 -14
- package/src/services/port-forwards.test.ts +2 -1
- package/src/services/programmatic-responder.aspect.test.ts +2 -1
- package/src/services/proxmox-reconcile.test.ts +2 -1
- package/src/services/restore-from-file.test.ts +23 -2
- package/src/services/restore-from-file.ts +21 -6
- package/src/services/restore-preflight.test.ts +2 -1
- package/src/services/secret-schema-loader.test.ts +2 -1
- package/src/services/ssh-key-manager.test.ts +2 -1
- package/src/services/static-content-converge.test.ts +144 -5
- package/src/services/static-content-converge.ts +87 -17
- package/src/services/system-config-schema-types.ts +1 -1
- package/src/services/system-config-validator.test.ts +36 -0
- package/src/services/system-config-validator.ts +11 -0
- package/src/services/system-state-stage.test.ts +2 -1
- package/src/services/trusted-sources.test.ts +33 -2
- package/src/services/trusted-sources.ts +47 -10
- package/src/services/zone-detector.test.ts +2 -1
- package/src/templates/generator.ts +9 -2
- package/src/test-utils/bus-responder.ts +5 -3
- package/src/test-utils/db-path.ts +25 -0
- package/src/test-utils/integration.ts +7 -0
- package/src/test-utils/module-fixtures.ts +5 -6
- package/src/variables/context.ts +16 -5
- package/src/module/packaging/generated-plane.test.ts +0 -79
- package/src/module/packaging/generated-plane.ts +0 -134
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import type { DbClient } from '../db/client';
|
|
6
6
|
import type { ModuleManifest } from '../manifest/schema';
|
|
7
7
|
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
8
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
8
9
|
import {
|
|
9
10
|
type CleanupTarget,
|
|
10
11
|
type ProviderRow,
|
|
@@ -216,7 +217,7 @@ describe('runConsumerCleanup — the paused skip is reported, not silent', () =>
|
|
|
216
217
|
});
|
|
217
218
|
afterEach(() => {
|
|
218
219
|
db.$client.close();
|
|
219
|
-
|
|
220
|
+
resetTestDbPath();
|
|
220
221
|
rmSync(dir, { recursive: true, force: true });
|
|
221
222
|
});
|
|
222
223
|
|
|
@@ -285,7 +286,7 @@ describe('runConsumerCleanup — dispatch', () => {
|
|
|
285
286
|
});
|
|
286
287
|
afterEach(() => {
|
|
287
288
|
db.$client.close();
|
|
288
|
-
|
|
289
|
+
resetTestDbPath();
|
|
289
290
|
rmSync(dir, { recursive: true, force: true });
|
|
290
291
|
});
|
|
291
292
|
|
|
@@ -10,6 +10,7 @@ import { eq } from 'drizzle-orm';
|
|
|
10
10
|
import { closeDb, createDbClient } from '../db/client';
|
|
11
11
|
import { runMigrations } from '../db/migrate';
|
|
12
12
|
import { containerServices } from '../db/schema';
|
|
13
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
13
14
|
import {
|
|
14
15
|
addContainerService,
|
|
15
16
|
getContainerService,
|
|
@@ -53,7 +54,7 @@ describe('container-service', () => {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// Clear environment variables
|
|
56
|
-
|
|
57
|
+
resetTestDbPath();
|
|
57
58
|
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
58
59
|
});
|
|
59
60
|
|
|
@@ -15,8 +15,9 @@ import { tmpdir } from 'node:os';
|
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
import { eq } from 'drizzle-orm';
|
|
17
17
|
import type { DbClient } from '../db/client';
|
|
18
|
-
import { systemConfig } from '../db/schema';
|
|
18
|
+
import { capabilities, modules, systemConfig } from '../db/schema';
|
|
19
19
|
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
20
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
20
21
|
import {
|
|
21
22
|
type ControlPlaneBootstrapOptions,
|
|
22
23
|
bootstrapControlPlane,
|
|
@@ -58,7 +59,7 @@ describe('bootstrapControlPlane', () => {
|
|
|
58
59
|
|
|
59
60
|
afterEach(() => {
|
|
60
61
|
db.$client.close();
|
|
61
|
-
|
|
62
|
+
resetTestDbPath();
|
|
62
63
|
try {
|
|
63
64
|
rmSync(dir, { recursive: true, force: true });
|
|
64
65
|
} catch {
|
|
@@ -88,6 +89,126 @@ describe('bootstrapControlPlane', () => {
|
|
|
88
89
|
expect(configValue(db, 'ssh.public_key')).toBe(PUBLIC_KEY);
|
|
89
90
|
});
|
|
90
91
|
|
|
92
|
+
test('hands discovery every address a deployed resolver advertises (celilo#1239)', async () => {
|
|
93
|
+
// The recurrence gate's second half: the refusal only works if bootstrap
|
|
94
|
+
// actually tells discovery what to refuse. Seed capability rows the way a
|
|
95
|
+
// deployed resolver provider (technitium) and its secondary
|
|
96
|
+
// (knot-unbound-secondary) write them, including the CIDR suffix IPAM
|
|
97
|
+
// resolves target_ip to. Selection is by the declared `fleet_resolver`
|
|
98
|
+
// marker in the data, never the capability name.
|
|
99
|
+
const [module] = db
|
|
100
|
+
.insert(modules)
|
|
101
|
+
.values({
|
|
102
|
+
id: 'technitium',
|
|
103
|
+
name: 'technitium',
|
|
104
|
+
version: '1.0.0',
|
|
105
|
+
manifestData: {},
|
|
106
|
+
sourcePath: '/test/technitium',
|
|
107
|
+
})
|
|
108
|
+
.returning()
|
|
109
|
+
.all();
|
|
110
|
+
db.insert(capabilities)
|
|
111
|
+
.values([
|
|
112
|
+
{
|
|
113
|
+
moduleId: module.id,
|
|
114
|
+
capabilityName: 'dns_internal',
|
|
115
|
+
version: '1.0.0',
|
|
116
|
+
data: {
|
|
117
|
+
fleet_resolver: true,
|
|
118
|
+
server: { ip: '10.0.10.13', internal_ip: '192.168.0.151/24' },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
moduleId: module.id,
|
|
123
|
+
capabilityName: 'dns_internal_secondary',
|
|
124
|
+
version: '1.0.0',
|
|
125
|
+
data: { fleet_resolver: true, server: { ip: '10.0.20.14' } },
|
|
126
|
+
},
|
|
127
|
+
])
|
|
128
|
+
.run();
|
|
129
|
+
|
|
130
|
+
const seen: string[][] = [];
|
|
131
|
+
await run({
|
|
132
|
+
discoverDnsImpl: (fleetResolverIps) => {
|
|
133
|
+
seen.push([...fleetResolverIps]);
|
|
134
|
+
return DNS;
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(seen).toHaveLength(1);
|
|
139
|
+
expect(seen[0].sort()).toEqual(['10.0.10.13', '192.168.0.151/24', '10.0.20.14'].sort());
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a resolver capability without the declared marker is not a fleet resolver', async () => {
|
|
143
|
+
// Selection is by the DECLARED marker, not the capability name. A row
|
|
144
|
+
// named like a resolver but not declaring fleet_resolver must be ignored,
|
|
145
|
+
// otherwise renaming this to a name check would pass every other test
|
|
146
|
+
// here and silently re-open celilo#1239 to name-shaped drift.
|
|
147
|
+
const [module] = db
|
|
148
|
+
.insert(modules)
|
|
149
|
+
.values({
|
|
150
|
+
id: 'technitium',
|
|
151
|
+
name: 'technitium',
|
|
152
|
+
version: '1.0.0',
|
|
153
|
+
manifestData: {},
|
|
154
|
+
sourcePath: '/test/technitium',
|
|
155
|
+
})
|
|
156
|
+
.returning()
|
|
157
|
+
.all();
|
|
158
|
+
db.insert(capabilities)
|
|
159
|
+
.values({
|
|
160
|
+
moduleId: module.id,
|
|
161
|
+
capabilityName: 'dns_internal',
|
|
162
|
+
version: '1.0.0',
|
|
163
|
+
data: { server: { ip: '10.0.10.13', internal_ip: '192.168.0.151/24' } },
|
|
164
|
+
})
|
|
165
|
+
.run();
|
|
166
|
+
|
|
167
|
+
const seen: string[][] = [];
|
|
168
|
+
await run({
|
|
169
|
+
discoverDnsImpl: (fleetResolverIps) => {
|
|
170
|
+
seen.push([...fleetResolverIps]);
|
|
171
|
+
return DNS;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(seen).toEqual([[]]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('a capability of any name declaring the marker is a fleet resolver', async () => {
|
|
179
|
+
// The other half of the same proof: a NEW resolver provider needs no core
|
|
180
|
+
// change. Its capability name is irrelevant; the declaration is what counts.
|
|
181
|
+
const [module] = db
|
|
182
|
+
.insert(modules)
|
|
183
|
+
.values({
|
|
184
|
+
id: 'technitium',
|
|
185
|
+
name: 'technitium',
|
|
186
|
+
version: '1.0.0',
|
|
187
|
+
manifestData: {},
|
|
188
|
+
sourcePath: '/test/technitium',
|
|
189
|
+
})
|
|
190
|
+
.returning()
|
|
191
|
+
.all();
|
|
192
|
+
db.insert(capabilities)
|
|
193
|
+
.values({
|
|
194
|
+
moduleId: module.id,
|
|
195
|
+
capabilityName: 'some_future_resolver',
|
|
196
|
+
version: '1.0.0',
|
|
197
|
+
data: { fleet_resolver: true, server: { ip: '10.0.30.15' } },
|
|
198
|
+
})
|
|
199
|
+
.run();
|
|
200
|
+
|
|
201
|
+
const seen: string[][] = [];
|
|
202
|
+
await run({
|
|
203
|
+
discoverDnsImpl: (fleetResolverIps) => {
|
|
204
|
+
seen.push([...fleetResolverIps]);
|
|
205
|
+
return DNS;
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(seen).toEqual([['10.0.30.15']]);
|
|
210
|
+
});
|
|
211
|
+
|
|
91
212
|
test('returns what each step found, so the caller renders and this does not', async () => {
|
|
92
213
|
const result = await run();
|
|
93
214
|
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import { type Bus, defineEvents, openBus } from '@celilo/event-bus';
|
|
35
35
|
import { getEventBusPath } from '../config/paths';
|
|
36
36
|
import type { DbClient } from '../db/client';
|
|
37
|
+
import { capabilities } from '../db/schema';
|
|
37
38
|
import { type DnsServers, discoverDns } from './dns-discovery';
|
|
38
39
|
import { type FleetFinding, checkDispatcher } from './fleet-checks';
|
|
39
40
|
import { type FleetKey, ensureFleetKey } from './fleet-key';
|
|
@@ -50,10 +51,55 @@ import { initializeSystem } from './system-init';
|
|
|
50
51
|
const DISPATCHER_ATTEMPTS = 5;
|
|
51
52
|
const DISPATCHER_INTERVAL_MS = 1_000;
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The marker a resolver provider declares in its capability data to say "the
|
|
56
|
+
* addresses I advertise are a resolver celilo itself deployed". Core reads
|
|
57
|
+
* this declaration and names no capability (module-business-barrier Scan B).
|
|
58
|
+
* Declared today by knot-unbound-internal, knot-unbound-secondary and
|
|
59
|
+
* technitium (celilo#1239).
|
|
60
|
+
*/
|
|
61
|
+
const FLEET_RESOLVER_MARKER = 'fleet_resolver';
|
|
62
|
+
|
|
63
|
+
/** The declared shape of a resolver capability's data. Values checked before use. */
|
|
64
|
+
type ResolverCapabilityData = {
|
|
65
|
+
fleet_resolver?: unknown;
|
|
66
|
+
server?: { ip?: unknown; internal_ip?: unknown };
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Every address a deployed resolver provider advertises for itself.
|
|
71
|
+
*
|
|
72
|
+
* A capability row counts when its data declares {@link FLEET_RESOLVER_MARKER};
|
|
73
|
+
* the capability NAME is irrelevant to this selection, so a new resolver
|
|
74
|
+
* provider needs no core change. Discovery hands the addresses to
|
|
75
|
+
* {@link discoverDns} so a resolv.conf the provider's own aspect wrote is
|
|
76
|
+
* refused rather than adopted as the fleet's upstream (celilo#1239). Addresses
|
|
77
|
+
* come from the declared data only (`server.ip`, `server.internal_ip`). Values
|
|
78
|
+
* may carry a CIDR suffix; discovery normalises both sides before comparing.
|
|
79
|
+
*/
|
|
80
|
+
export function fleetResolverAddresses(db: DbClient): string[] {
|
|
81
|
+
const addresses: string[] = [];
|
|
82
|
+
const rows = db.select({ data: capabilities.data }).from(capabilities).all();
|
|
83
|
+
for (const row of rows) {
|
|
84
|
+
const data = row.data as ResolverCapabilityData | undefined;
|
|
85
|
+
if (data?.[FLEET_RESOLVER_MARKER] !== true) continue;
|
|
86
|
+
for (const value of [data.server?.ip, data.server?.internal_ip]) {
|
|
87
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
88
|
+
addresses.push(value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return addresses;
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
export interface ControlPlaneBootstrapOptions {
|
|
54
96
|
db: DbClient;
|
|
55
|
-
/**
|
|
56
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Injectable so tests drive the whole sequence without a host or a bus.
|
|
99
|
+
* Receives the addresses of resolvers celilo itself deployed, which the
|
|
100
|
+
* discovery must refuse (celilo#1239).
|
|
101
|
+
*/
|
|
102
|
+
discoverDnsImpl?: (fleetResolverIps: readonly string[]) => DnsServers;
|
|
57
103
|
ensureFleetKeyImpl?: () => FleetKey;
|
|
58
104
|
discoverNetworkImpl?: (db: DbClient) => NetworkDiscoveryResult;
|
|
59
105
|
/**
|
|
@@ -152,7 +198,8 @@ export async function bootstrapControlPlane(
|
|
|
152
198
|
): Promise<ControlPlaneBootstrapResult> {
|
|
153
199
|
const {
|
|
154
200
|
db,
|
|
155
|
-
discoverDnsImpl =
|
|
201
|
+
discoverDnsImpl = (fleetResolverIps: readonly string[]): DnsServers =>
|
|
202
|
+
discoverDns(undefined, { fleetResolverIps }),
|
|
156
203
|
ensureFleetKeyImpl = ensureFleetKey,
|
|
157
204
|
discoverNetworkImpl = discoverAndRecordNetwork,
|
|
158
205
|
probeDispatcher = probeOwnDispatcher,
|
|
@@ -160,7 +207,7 @@ export async function bootstrapControlPlane(
|
|
|
160
207
|
dispatcherAttempts = DISPATCHER_ATTEMPTS,
|
|
161
208
|
} = options;
|
|
162
209
|
|
|
163
|
-
const dns = discoverDnsImpl();
|
|
210
|
+
const dns = discoverDnsImpl(fleetResolverAddresses(db));
|
|
164
211
|
const fleetKey = ensureFleetKeyImpl();
|
|
165
212
|
|
|
166
213
|
initializeSystem(db, {
|
|
@@ -15,6 +15,7 @@ import { closeDb, getDb } from '../db/client';
|
|
|
15
15
|
import { runMigrations } from '../db/migrate';
|
|
16
16
|
import { modules } from '../db/schema';
|
|
17
17
|
import type { ModuleManifest } from '../manifest/schema';
|
|
18
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
18
19
|
import {
|
|
19
20
|
type CrossModuleIndex,
|
|
20
21
|
applyCrossModuleWriteRoot,
|
|
@@ -70,7 +71,7 @@ describe('materializeCrossModuleRoot', () => {
|
|
|
70
71
|
|
|
71
72
|
afterEach(() => {
|
|
72
73
|
closeDb();
|
|
73
|
-
|
|
74
|
+
resetTestDbPath();
|
|
74
75
|
process.env.CELILO_DATA_DIR = undefined;
|
|
75
76
|
try {
|
|
76
77
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -174,8 +174,14 @@ export async function runPreflight(
|
|
|
174
174
|
for (const variable of manifest.variables.owns) {
|
|
175
175
|
if (!variable.required) continue;
|
|
176
176
|
|
|
177
|
-
// Infrastructure/terraform variables are auto-derived during deploy
|
|
178
|
-
|
|
177
|
+
// Infrastructure/terraform variables are auto-derived during deploy;
|
|
178
|
+
// hook-owned variables are discovered by the module's own hooks at
|
|
179
|
+
// runtime (hook-owned-state D2). None of the three is operator input.
|
|
180
|
+
if (
|
|
181
|
+
variable.source === 'infrastructure' ||
|
|
182
|
+
variable.source === 'terraform' ||
|
|
183
|
+
variable.source === 'hook'
|
|
184
|
+
) {
|
|
179
185
|
continue;
|
|
180
186
|
}
|
|
181
187
|
|
|
@@ -21,6 +21,7 @@ import { join } from 'node:path';
|
|
|
21
21
|
import { type DbClient, getDb } from '../db/client';
|
|
22
22
|
import { modules, secrets } from '../db/schema';
|
|
23
23
|
import type { ModuleManifest } from '../manifest/schema';
|
|
24
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
24
25
|
import { findMissingSecrets } from './config-interview';
|
|
25
26
|
import { findMissingBuildArtifacts, findMissingRequiredVariables } from './deploy-validation';
|
|
26
27
|
|
|
@@ -89,7 +90,7 @@ describe('findMissingRequiredVariables (deploy path)', () => {
|
|
|
89
90
|
|
|
90
91
|
afterEach(() => {
|
|
91
92
|
rmSync(tempDir, { recursive: true, force: true });
|
|
92
|
-
|
|
93
|
+
resetTestDbPath();
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
// The bug we're guarding against: the deploy used to drop type +
|
|
@@ -129,6 +130,28 @@ describe('findMissingRequiredVariables (deploy path)', () => {
|
|
|
129
130
|
expect(missing[0].value_label).toBeUndefined();
|
|
130
131
|
});
|
|
131
132
|
|
|
133
|
+
// Hook-owned-state task 2.3 (design D2): a `source: hook` variable is
|
|
134
|
+
// discovered by the module's own hook at runtime. It is never operator
|
|
135
|
+
// input, so an unwritten one must not read as missing configuration — not
|
|
136
|
+
// for the interview, and not as a deploy-blocking error.
|
|
137
|
+
test('a source: hook variable is never reported missing, even required and unwritten', async () => {
|
|
138
|
+
const manifest = {
|
|
139
|
+
celilo_contract: '1.0',
|
|
140
|
+
variables: {
|
|
141
|
+
owns: [
|
|
142
|
+
{
|
|
143
|
+
name: 'public_ip',
|
|
144
|
+
type: 'string',
|
|
145
|
+
required: true,
|
|
146
|
+
source: 'hook',
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
} as unknown as ModuleManifest;
|
|
151
|
+
const missing = await findMissingRequiredVariables('testmod', manifest, db);
|
|
152
|
+
expect(missing).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
132
155
|
test('configured secrets are not reported as missing', async () => {
|
|
133
156
|
db.insert(secrets)
|
|
134
157
|
.values({
|
|
@@ -188,7 +211,7 @@ describe('findMissingSecrets (shared)', () => {
|
|
|
188
211
|
|
|
189
212
|
afterEach(() => {
|
|
190
213
|
rmSync(tempDir, { recursive: true, force: true });
|
|
191
|
-
|
|
214
|
+
resetTestDbPath();
|
|
192
215
|
});
|
|
193
216
|
|
|
194
217
|
test('accepts loose unknown[] declares (manifest read off disk)', async () => {
|
|
@@ -430,6 +430,14 @@ export async function findMissingRequiredVariables(
|
|
|
430
430
|
continue; // These are auto-populated from Terraform outputs
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
+
// Hook-owned variables are discovered by the module's own hooks at
|
|
434
|
+
// runtime (hook-owned-state D2). An unwritten one is not a missing
|
|
435
|
+
// configuration value — prompting for it or failing validation would
|
|
436
|
+
// ask the operator to guess what a hook has not learned yet.
|
|
437
|
+
if (variable.source === 'hook') {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
433
441
|
// Check if variable is configured
|
|
434
442
|
let isConfigured = false;
|
|
435
443
|
|
|
@@ -90,4 +90,58 @@ describe('discoverDns', () => {
|
|
|
90
90
|
test('returns the public fallback on a host with no resolver files at all', () => {
|
|
91
91
|
expect(discoverDns(host({}))).toEqual({ primary: '1.1.1.1', fallback: '1.1.1.1' });
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
// The recurrence gate for celilo#1239 (bead ce-dxr8). The dns-client-config
|
|
95
|
+
// aspect of a deployed dns_internal provider rewrites /etc/resolv.conf to
|
|
96
|
+
// name that provider. Discovery reading the file back adopts the fleet's own
|
|
97
|
+
// resolver as the fleet's upstream and writes it into dns.primary /
|
|
98
|
+
// dns.fallback, where it reaches every LXC's permanent birth nameserver
|
|
99
|
+
// line. Proven to fail against the accepting implementation first.
|
|
100
|
+
describe('refusing resolvers celilo itself installed', () => {
|
|
101
|
+
// technitium's advertised dns_internal addresses: its dmz zone address and
|
|
102
|
+
// (once advertised) the firewall ingress `internal`-zone systems use.
|
|
103
|
+
const FLEET = ['10.0.10.13', '192.168.0.151'];
|
|
104
|
+
|
|
105
|
+
test('refuses a resolv.conf naming a deployed dns_internal provider address', () => {
|
|
106
|
+
const servers = discoverDns(host({ [ETC]: `nameserver ${FLEET[0]}\n` }), {
|
|
107
|
+
fleetResolverIps: FLEET,
|
|
108
|
+
});
|
|
109
|
+
expect(servers).toEqual({ primary: '1.1.1.1', fallback: '1.1.1.1' });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('keeps the real upstreams beside a refused fleet resolver', () => {
|
|
113
|
+
const servers = discoverDns(host({ [ETC]: `nameserver ${FLEET[0]}\nnameserver 9.9.9.9\n` }), {
|
|
114
|
+
fleetResolverIps: FLEET,
|
|
115
|
+
});
|
|
116
|
+
expect(servers).toEqual({ primary: '9.9.9.9', fallback: '1.1.1.1' });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('falls through to /etc/resolv.conf when the systemd file holds only fleet resolvers', () => {
|
|
120
|
+
const servers = discoverDns(
|
|
121
|
+
host({
|
|
122
|
+
[SYSTEMD]: `nameserver ${FLEET[0]}\n`,
|
|
123
|
+
[ETC]: 'nameserver 9.9.9.9\n',
|
|
124
|
+
}),
|
|
125
|
+
{ fleetResolverIps: FLEET },
|
|
126
|
+
);
|
|
127
|
+
expect(servers).toEqual({ primary: '9.9.9.9', fallback: '1.1.1.1' });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('matches a capability address that carries a CIDR suffix', () => {
|
|
131
|
+
// Capability data advertises `192.168.0.151/24` (server.ip resolves from
|
|
132
|
+
// target_ip); the resolv.conf entry is the bare address.
|
|
133
|
+
const servers = discoverDns(host({ [ETC]: `nameserver ${FLEET[1]}\n` }), {
|
|
134
|
+
fleetResolverIps: [`${FLEET[1]}/24`],
|
|
135
|
+
});
|
|
136
|
+
expect(servers).toEqual({ primary: '1.1.1.1', fallback: '1.1.1.1' });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('accepts a private upstream that is not a fleet resolver', () => {
|
|
140
|
+
// A home-lab upstream on the operator's own network is legitimate. The
|
|
141
|
+
// refusal is scoped to resolvers celilo deployed, not to privateness.
|
|
142
|
+
expect(
|
|
143
|
+
discoverDns(host({ [ETC]: 'nameserver 192.168.0.1\n' }), { fleetResolverIps: FLEET }),
|
|
144
|
+
).toEqual({ primary: '192.168.0.1', fallback: '1.1.1.1' });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
93
147
|
});
|
|
@@ -72,18 +72,60 @@ export type ResolverFileReader = (path: string) => string | null;
|
|
|
72
72
|
const readResolverFile: ResolverFileReader = (path) =>
|
|
73
73
|
existsSync(path) ? readFileSync(path, 'utf-8') : null;
|
|
74
74
|
|
|
75
|
+
/** Options for {@link discoverDns}. */
|
|
76
|
+
export interface DiscoverDnsOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Addresses of resolvers celilo itself deployed, as advertised in the
|
|
79
|
+
* capability data of providers that declare the `fleet_resolver` marker
|
|
80
|
+
* (`server.ip`, `server.internal_ip`). Entries may carry a CIDR suffix.
|
|
81
|
+
*/
|
|
82
|
+
fleetResolverIps?: readonly string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Strip a CIDR suffix: `192.168.0.151/24` → `192.168.0.151`. */
|
|
86
|
+
export function bareIp(address: string): string {
|
|
87
|
+
return address.split('/')[0];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Drop nameservers that are resolvers celilo itself installed.
|
|
92
|
+
*
|
|
93
|
+
* A deployed dns_internal provider's base-module aspect rewrites this box's
|
|
94
|
+
* `/etc/resolv.conf` to name that provider. Reading the file back afterwards
|
|
95
|
+
* adopts the fleet's own resolver as the fleet's upstream and writes it into
|
|
96
|
+
* `dns.primary` / `dns.fallback` (celilo#1239), where it reaches every LXC's
|
|
97
|
+
* permanent birth nameserver line. A resolver answering the fleet is not an
|
|
98
|
+
* upstream for the fleet; refusing it here is what keeps discovery honest
|
|
99
|
+
* once the aspect has run.
|
|
100
|
+
*/
|
|
101
|
+
export function excludeFleetResolvers(
|
|
102
|
+
nameservers: string[],
|
|
103
|
+
fleetResolverIps: readonly string[] = [],
|
|
104
|
+
): string[] {
|
|
105
|
+
if (fleetResolverIps.length === 0) return nameservers;
|
|
106
|
+
const fleet = new Set(fleetResolverIps.map(bareIp));
|
|
107
|
+
return nameservers.filter((ns) => !fleet.has(ns));
|
|
108
|
+
}
|
|
109
|
+
|
|
75
110
|
/**
|
|
76
111
|
* Read this box's upstream resolvers.
|
|
77
112
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
113
|
+
* Loopback stubs and resolvers celilo itself installed ({@link DiscoverDnsOptions.fleetResolverIps})
|
|
114
|
+
* are refused, so a file holding only refused entries falls through to the
|
|
115
|
+
* next one. `/etc/resolv.conf` is the last resort and whatever survives the
|
|
116
|
+
* refusal is accepted, including the public fallback, because there is
|
|
117
|
+
* nothing further to consult.
|
|
81
118
|
*/
|
|
82
|
-
export function discoverDns(
|
|
119
|
+
export function discoverDns(
|
|
120
|
+
read: ResolverFileReader = readResolverFile,
|
|
121
|
+
options: DiscoverDnsOptions = {},
|
|
122
|
+
): DnsServers {
|
|
83
123
|
for (const path of RESOLVER_FILES) {
|
|
84
124
|
const contents = read(path);
|
|
85
125
|
if (contents === null) continue;
|
|
86
|
-
const servers = chooseDns(
|
|
126
|
+
const servers = chooseDns(
|
|
127
|
+
excludeFleetResolvers(parseNameservers(contents), options.fleetResolverIps),
|
|
128
|
+
);
|
|
87
129
|
if (servers.primary !== PUBLIC_FALLBACK || path === '/etc/resolv.conf') {
|
|
88
130
|
return servers;
|
|
89
131
|
}
|
|
@@ -8,6 +8,7 @@ import { eq } from 'drizzle-orm';
|
|
|
8
8
|
import type { DbClient } from '../db/client';
|
|
9
9
|
import { dnsInternalRecords, modules } from '../db/schema';
|
|
10
10
|
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
11
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
11
12
|
import {
|
|
12
13
|
listDnsInternalRecords,
|
|
13
14
|
listViewOverrides,
|
|
@@ -42,7 +43,7 @@ describe('dns-internal-records ledger', () => {
|
|
|
42
43
|
});
|
|
43
44
|
afterEach(() => {
|
|
44
45
|
db.$client.close();
|
|
45
|
-
|
|
46
|
+
resetTestDbPath();
|
|
46
47
|
try {
|
|
47
48
|
rmSync(dir, { recursive: true, force: true });
|
|
48
49
|
} catch {
|
|
@@ -248,7 +249,7 @@ describe('provider cascade (celilo#1010)', () => {
|
|
|
248
249
|
|
|
249
250
|
afterEach(() => {
|
|
250
251
|
db.$client.close();
|
|
251
|
-
|
|
252
|
+
resetTestDbPath();
|
|
252
253
|
rmSync(dir, { recursive: true, force: true });
|
|
253
254
|
});
|
|
254
255
|
|
|
@@ -6,6 +6,7 @@ import type { DnsRecordRequest, HookLogger } from '@celilo/capabilities';
|
|
|
6
6
|
import { closeDb, getDb } from '../db/client';
|
|
7
7
|
import { runMigrations } from '../db/migrate';
|
|
8
8
|
import { capabilities, moduleConfigs, modules, webRoutes } from '../db/schema';
|
|
9
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
9
10
|
import { backfillWebRouteDns } from './dns-provider-backfill';
|
|
10
11
|
|
|
11
12
|
const silentLogger: HookLogger = {
|
|
@@ -26,7 +27,7 @@ describe('backfillWebRouteDns (ISS-0029)', () => {
|
|
|
26
27
|
|
|
27
28
|
afterEach(() => {
|
|
28
29
|
closeDb();
|
|
29
|
-
|
|
30
|
+
resetTestDbPath();
|
|
30
31
|
try {
|
|
31
32
|
rmSync(dir, { recursive: true, force: true });
|
|
32
33
|
} catch {
|
|
@@ -14,6 +14,7 @@ import type { DnsRegistrarCapability } from '@celilo/capabilities';
|
|
|
14
14
|
import { eq } from 'drizzle-orm';
|
|
15
15
|
import { type DbClient, getDb } from '../db/client';
|
|
16
16
|
import { modules } from '../db/schema';
|
|
17
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
17
18
|
import {
|
|
18
19
|
listDnsRegistrations,
|
|
19
20
|
recordDnsRegistration,
|
|
@@ -44,7 +45,7 @@ describe('dns_registrations ledger', () => {
|
|
|
44
45
|
|
|
45
46
|
afterEach(() => {
|
|
46
47
|
rmSync(tempDir, { recursive: true, force: true });
|
|
47
|
-
|
|
48
|
+
resetTestDbPath();
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
test('record + list roundtrip; a re-assert is an upsert, not a second row', () => {
|
|
@@ -8,6 +8,7 @@ import { moduleConfigs, modules, secrets } from '../db/schema';
|
|
|
8
8
|
import type { Ensure } from '../manifest/schema';
|
|
9
9
|
import { decryptSecret } from '../secrets/encryption';
|
|
10
10
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
11
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
11
12
|
import {
|
|
12
13
|
findEnsureOnProvider,
|
|
13
14
|
interviewForEnsureInputs,
|
|
@@ -56,7 +57,7 @@ describe('interviewForEnsureInputs', () => {
|
|
|
56
57
|
|
|
57
58
|
afterEach(() => {
|
|
58
59
|
rmSync(tempDir, { recursive: true, force: true });
|
|
59
|
-
|
|
60
|
+
resetTestDbPath();
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
test('appends to array config and sets secret object key', async () => {
|
|
@@ -185,7 +186,7 @@ describe('findEnsureOnProvider', () => {
|
|
|
185
186
|
|
|
186
187
|
afterEach(() => {
|
|
187
188
|
rmSync(tempDir, { recursive: true, force: true });
|
|
188
|
-
|
|
189
|
+
resetTestDbPath();
|
|
189
190
|
});
|
|
190
191
|
|
|
191
192
|
test('finds ensure by id on a registered provider', () => {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../db/schema';
|
|
15
15
|
import type { ModuleManifest } from '../manifest/schema';
|
|
16
16
|
import { setupTestDatabaseAt } from '../test-utils/database';
|
|
17
|
+
import { resetTestDbPath } from '../test-utils/db-path';
|
|
17
18
|
import { ensureInboundSubscriber, ensureSweepSubscriber } from './alerting/monitors';
|
|
18
19
|
import { ensureBackupSweepSubscriber } from './backup-sweep';
|
|
19
20
|
import { getDaemonUnitPath } from './events-daemon';
|
|
@@ -269,7 +270,7 @@ describe('checkSubscribers + checkCapabilityProviders', () => {
|
|
|
269
270
|
afterEach(() => {
|
|
270
271
|
bus.close();
|
|
271
272
|
db.$client.close();
|
|
272
|
-
|
|
273
|
+
resetTestDbPath();
|
|
273
274
|
process.env.EVENT_BUS_DB = undefined;
|
|
274
275
|
try {
|
|
275
276
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -978,7 +979,7 @@ describe('runFleetChecks includes host liveness (#728)', () => {
|
|
|
978
979
|
afterEach(() => {
|
|
979
980
|
bus.close();
|
|
980
981
|
db.$client.close();
|
|
981
|
-
|
|
982
|
+
resetTestDbPath();
|
|
982
983
|
process.env.EVENT_BUS_DB = undefined;
|
|
983
984
|
try {
|
|
984
985
|
rmSync(dir, { recursive: true, force: true });
|