@celilo/cli 0.5.0-alpha.1 → 0.5.0-alpha.11
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/0009_dns_registrations.sql +13 -0
- package/drizzle/0010_dns_internal_records.sql +12 -0
- package/drizzle/0011_backups_name.sql +1 -0
- package/drizzle/meta/_journal.json +22 -1
- package/package.json +3 -3
- package/src/ansible/inventory.test.ts +10 -10
- package/src/ansible/validation.test.ts +25 -15
- package/src/api-clients/proxmox.test.ts +211 -1
- package/src/api-clients/proxmox.ts +399 -8
- package/src/cli/command-registry.ts +83 -6
- package/src/cli/commands/backup-delete.ts +10 -7
- package/src/cli/commands/backup-import.ts +11 -8
- package/src/cli/commands/backup-restore.ts +11 -8
- package/src/cli/commands/dns.ts +57 -0
- package/src/cli/commands/events.test.ts +4 -4
- package/src/cli/commands/events.ts +89 -24
- package/src/cli/commands/machine-add.ts +178 -163
- package/src/cli/commands/machine-remove.ts +10 -7
- package/src/cli/commands/module-config.test.ts +78 -0
- package/src/cli/commands/module-config.ts +18 -3
- package/src/cli/commands/module-import.ts +9 -5
- package/src/cli/commands/module-publish.ts +24 -0
- package/src/cli/commands/module-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.test.ts +37 -0
- package/src/cli/commands/module-upgrade.ts +26 -6
- package/src/cli/commands/proxmox-node-list.ts +101 -0
- package/src/cli/commands/proxmox-template-selection.ts +16 -15
- package/src/cli/commands/proxmox-vm-template-build.ts +171 -0
- package/src/cli/commands/publish/alpha.test.ts +26 -0
- package/src/cli/commands/publish/alpha.ts +23 -0
- package/src/cli/commands/publish/types.ts +7 -2
- package/src/cli/commands/publish/workspace.ts +11 -1
- package/src/cli/commands/restore.ts +29 -0
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +283 -209
- package/src/cli/commands/service-reconfigure.test.ts +115 -0
- package/src/cli/commands/service-reconfigure.ts +252 -129
- package/src/cli/commands/service-remove.ts +19 -13
- package/src/cli/commands/service-verify.ts +9 -10
- package/src/cli/commands/storage-add-local.ts +120 -107
- package/src/cli/commands/storage-add-s3.ts +145 -131
- package/src/cli/commands/storage-remove.ts +11 -8
- package/src/cli/commands/system-doctor.ts +135 -40
- package/src/cli/commands/system-init.ts +119 -128
- package/src/cli/commands/system-migrate.test.ts +40 -0
- package/src/cli/commands/system-migrate.ts +65 -0
- package/src/cli/completion.ts +23 -0
- package/src/cli/index.ts +91 -7
- package/src/cli/service-credential.ts +54 -0
- package/src/config/paths.test.ts +61 -48
- package/src/db/client.ts +15 -146
- package/src/db/migrate.ts +14 -6
- package/src/db/schema-introspection.ts +88 -0
- package/src/db/schema.ts +74 -0
- package/src/hooks/capability-loader-firewall.test.ts +3 -3
- package/src/hooks/capability-loader.ts +43 -2
- package/src/hooks/run-named-hook.ts +28 -2
- package/src/hooks/types.ts +2 -1
- package/src/infrastructure/property-extractor.test.ts +15 -0
- package/src/infrastructure/property-extractor.ts +12 -0
- package/src/manifest/contracts/v1.ts +16 -0
- package/src/manifest/schema.ts +17 -0
- package/src/manifest/validate.test.ts +53 -0
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-interview.ts +232 -0
- package/src/services/bus-secret-flow.test.ts +2 -2
- package/src/services/celilo-mgmt-hooks.test.ts +3 -2
- package/src/services/deploy-preflight.ts +25 -0
- package/src/services/deploy-validation.test.ts +54 -4
- package/src/services/deploy-validation.ts +27 -36
- package/src/services/dns-internal-records.test.ts +126 -0
- package/src/services/dns-internal-records.ts +119 -0
- package/src/services/dns-provider-backfill.test.ts +2 -2
- package/src/services/dns-provider-backfill.ts +14 -2
- package/src/services/dns-registrations.test.ts +120 -0
- package/src/services/dns-registrations.ts +108 -0
- package/src/services/events-daemon.test.ts +59 -0
- package/src/services/events-daemon.ts +191 -57
- package/src/services/fleet-checks.test.ts +508 -0
- package/src/services/fleet-checks.ts +678 -0
- package/src/services/module-build.test.ts +43 -38
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +7 -6
- package/src/services/module-subscriptions.test.ts +88 -0
- package/src/services/module-subscriptions.ts +50 -1
- package/src/services/module-validator/bundled-deps.test.ts +55 -0
- package/src/services/module-validator/bundled-deps.ts +115 -0
- package/src/services/module-validator/capability-versions.test.ts +1 -1
- package/src/services/placement-reconcile.test.ts +86 -0
- package/src/services/placement-reconcile.ts +108 -0
- package/src/services/programmatic-responder.ts +34 -0
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +92 -12
- package/src/templates/generator.ts +165 -80
- package/src/test-utils/fixtures.test.ts +1 -1
- package/src/test-utils/integration-guard.ts +33 -0
- package/src/types/infrastructure.ts +6 -0
- package/src/variables/computed/computed-integration.test.ts +3 -3
- package/src/variables/computed/computed.test.ts +5 -5
- package/src/variables/declarative-derivation.test.ts +6 -6
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal split-horizon DNS A-record ledger (designs/CELILO_DOCTOR_FLEET_DRIFT.md
|
|
3
|
+
* Phase 4, ISS-0094 / ISS-0111).
|
|
4
|
+
*
|
|
5
|
+
* The dns_internal capability (technitium/knot) registers records straight
|
|
6
|
+
* into the resolver's own DB, leaving celilo no offline record of what it
|
|
7
|
+
* asked to be served. This ledger — the internal-DNS sibling of
|
|
8
|
+
* `dns_registrations` — records every successful `registerRecord({type:'A'})`
|
|
9
|
+
* so `celilo system doctor` can assert, without a live resolver probe, that
|
|
10
|
+
* service hostnames resolve to the firewall natIp (LAN-reachable) rather than
|
|
11
|
+
* a zone-side container IP a LAN device can't route to.
|
|
12
|
+
*
|
|
13
|
+
* Row lifecycle is FK cascade — records die with their provider or consumer.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { DnsInternalCapability, DnsRecordRequest } from '@celilo/capabilities';
|
|
17
|
+
import { and, eq } from 'drizzle-orm';
|
|
18
|
+
import type { DbClient } from '../db/client';
|
|
19
|
+
import { dnsInternalRecords } from '../db/schema';
|
|
20
|
+
|
|
21
|
+
export interface DnsInternalRecordRow {
|
|
22
|
+
host: string;
|
|
23
|
+
ip: string;
|
|
24
|
+
providerModuleId: string;
|
|
25
|
+
consumerModuleId: string;
|
|
26
|
+
registeredAt: Date;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function listDnsInternalRecords(
|
|
30
|
+
db: DbClient,
|
|
31
|
+
options: { providerModuleId?: string } = {},
|
|
32
|
+
): DnsInternalRecordRow[] {
|
|
33
|
+
const query = db
|
|
34
|
+
.select({
|
|
35
|
+
host: dnsInternalRecords.host,
|
|
36
|
+
ip: dnsInternalRecords.ip,
|
|
37
|
+
providerModuleId: dnsInternalRecords.providerModuleId,
|
|
38
|
+
consumerModuleId: dnsInternalRecords.consumerModuleId,
|
|
39
|
+
registeredAt: dnsInternalRecords.registeredAt,
|
|
40
|
+
})
|
|
41
|
+
.from(dnsInternalRecords);
|
|
42
|
+
return options.providerModuleId
|
|
43
|
+
? query.where(eq(dnsInternalRecords.providerModuleId, options.providerModuleId)).all()
|
|
44
|
+
: query.all();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function recordDnsInternalRecord(
|
|
48
|
+
db: DbClient,
|
|
49
|
+
record: { providerModuleId: string; consumerModuleId: string; host: string; ip: string },
|
|
50
|
+
): void {
|
|
51
|
+
db.insert(dnsInternalRecords)
|
|
52
|
+
.values({
|
|
53
|
+
providerModuleId: record.providerModuleId,
|
|
54
|
+
consumerModuleId: record.consumerModuleId,
|
|
55
|
+
host: record.host,
|
|
56
|
+
ip: record.ip,
|
|
57
|
+
registeredAt: new Date(),
|
|
58
|
+
})
|
|
59
|
+
.onConflictDoUpdate({
|
|
60
|
+
target: [dnsInternalRecords.providerModuleId, dnsInternalRecords.host],
|
|
61
|
+
set: {
|
|
62
|
+
consumerModuleId: record.consumerModuleId,
|
|
63
|
+
ip: record.ip,
|
|
64
|
+
registeredAt: new Date(),
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
.run();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function removeDnsInternalRecord(
|
|
71
|
+
db: DbClient,
|
|
72
|
+
record: { providerModuleId: string; host: string },
|
|
73
|
+
): void {
|
|
74
|
+
db.delete(dnsInternalRecords)
|
|
75
|
+
.where(
|
|
76
|
+
and(
|
|
77
|
+
eq(dnsInternalRecords.providerModuleId, record.providerModuleId),
|
|
78
|
+
eq(dnsInternalRecords.host, record.host),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
.run();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Wrap a dns_internal interface so A-record register/delete calls are
|
|
86
|
+
* mirrored into the ledger. Only A records are tracked — they're the
|
|
87
|
+
* host→IP mapping the natIp drift check reasons about; CNAMEs/others pass
|
|
88
|
+
* through unrecorded. A throw from the underlying call propagates before
|
|
89
|
+
* any ledger write, so only a confirmed register/delete earns a row change.
|
|
90
|
+
*/
|
|
91
|
+
export function withDnsInternalLedger(
|
|
92
|
+
iface: DnsInternalCapability,
|
|
93
|
+
ctx: { db: DbClient; providerModuleId: string; consumerModuleId: string },
|
|
94
|
+
): DnsInternalCapability {
|
|
95
|
+
const isA = (request: DnsRecordRequest) => request.type.toUpperCase() === 'A';
|
|
96
|
+
return {
|
|
97
|
+
...iface,
|
|
98
|
+
async registerRecord(request: DnsRecordRequest): Promise<void> {
|
|
99
|
+
await iface.registerRecord(request);
|
|
100
|
+
if (isA(request)) {
|
|
101
|
+
recordDnsInternalRecord(ctx.db, {
|
|
102
|
+
providerModuleId: ctx.providerModuleId,
|
|
103
|
+
consumerModuleId: ctx.consumerModuleId,
|
|
104
|
+
host: request.host,
|
|
105
|
+
ip: request.value,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async deleteRecord(request: DnsRecordRequest): Promise<void> {
|
|
110
|
+
await iface.deleteRecord(request);
|
|
111
|
+
if (isA(request)) {
|
|
112
|
+
removeDnsInternalRecord(ctx.db, {
|
|
113
|
+
providerModuleId: ctx.providerModuleId,
|
|
114
|
+
host: request.host,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -105,14 +105,14 @@ describe('backfillWebRouteDns (ISS-0029)', () => {
|
|
|
105
105
|
seedFirewall('100.64.0.1');
|
|
106
106
|
seedRoute('apt.celilo.computer', '/');
|
|
107
107
|
seedRoute('apt.celilo.computer', '/-/publish'); // same host, different path → deduped
|
|
108
|
-
seedRoute('registry.
|
|
108
|
+
seedRoute('registry.example.net', '/');
|
|
109
109
|
|
|
110
110
|
const calls: DnsRecordRequest[] = [];
|
|
111
111
|
await backfillWebRouteDns('technitium', getDb(), silentLogger, stubLoader(calls));
|
|
112
112
|
|
|
113
113
|
expect(calls.map((c) => c.host).sort()).toEqual([
|
|
114
114
|
'apt.celilo.computer',
|
|
115
|
-
'registry.
|
|
115
|
+
'registry.example.net',
|
|
116
116
|
]);
|
|
117
117
|
expect(calls.every((c) => c.value === '100.64.0.1' && c.type === 'A')).toBe(true);
|
|
118
118
|
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { DnsInternalCapability, HookLogger } from '@celilo/capabilities';
|
|
18
|
-
import { and, eq } from 'drizzle-orm';
|
|
18
|
+
import { and, eq, inArray, or } from 'drizzle-orm';
|
|
19
19
|
import type { DbClient } from '../db/client';
|
|
20
20
|
import { capabilities as capabilitiesTable, modules, webRoutes } from '../db/schema';
|
|
21
21
|
import { loadCapabilityFunctions, resolveFirewallNatIp } from '../hooks/capability-loader';
|
|
@@ -54,7 +54,19 @@ export async function backfillProviderDns(
|
|
|
54
54
|
): Promise<void> {
|
|
55
55
|
logger.info(`dns_internal provider '${moduleId}' deployed — backfilling DNS for all systems`);
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// ISS-0009: register only DEPLOYED systems, never IMPORTED-but-undeployed
|
|
58
|
+
// modules (those have a hostname/target_ip configured but nothing serving at
|
|
59
|
+
// that address — registering an A record for them is a phantom record). A
|
|
60
|
+
// deployed system is INSTALLED or VERIFIED. The provider itself is already
|
|
61
|
+
// INSTALLED by the time backfill runs (module-deploy transitions state before
|
|
62
|
+
// calling this), but we union its id explicitly so the provider's own
|
|
63
|
+
// self-record is registered even if that transition order ever changes —
|
|
64
|
+
// critical in daemonless contexts where no dispatcher delivers system.created.
|
|
65
|
+
const deployed = db
|
|
66
|
+
.select()
|
|
67
|
+
.from(modules)
|
|
68
|
+
.where(or(inArray(modules.state, ['INSTALLED', 'VERIFIED']), eq(modules.id, moduleId)))
|
|
69
|
+
.all();
|
|
58
70
|
const failures: string[] = [];
|
|
59
71
|
|
|
60
72
|
// One register per deployed system across all modules — a module with N
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the DNS registration ledger
|
|
3
|
+
* (designs/DISPATCHER_DAEMON_AND_TIMER_EVENTS.md B2): upsert semantics,
|
|
4
|
+
* the registerHost recording wrapper (success-only), refresh stamping,
|
|
5
|
+
* and FK-cascade cleanup when a module is removed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import type { DnsRegistrarCapability } from '@celilo/capabilities';
|
|
13
|
+
import { eq } from 'drizzle-orm';
|
|
14
|
+
import { type DbClient, getDb } from '../db/client';
|
|
15
|
+
import { modules } from '../db/schema';
|
|
16
|
+
import {
|
|
17
|
+
listDnsRegistrations,
|
|
18
|
+
recordDnsRegistration,
|
|
19
|
+
stampDnsRegistrationsRefreshed,
|
|
20
|
+
withDnsRegistrationLedger,
|
|
21
|
+
} from './dns-registrations';
|
|
22
|
+
|
|
23
|
+
describe('dns_registrations ledger', () => {
|
|
24
|
+
let tempDir: string;
|
|
25
|
+
let db: DbClient;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tempDir = mkdtempSync(join(tmpdir(), 'celilo-dnsreg-'));
|
|
29
|
+
process.env.CELILO_DB_PATH = join(tempDir, 'test.db');
|
|
30
|
+
db = getDb();
|
|
31
|
+
for (const id of ['namecheap', 'caddy']) {
|
|
32
|
+
db.insert(modules)
|
|
33
|
+
.values({
|
|
34
|
+
id,
|
|
35
|
+
name: id,
|
|
36
|
+
sourcePath: join(tempDir, id),
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
manifestData: { celilo_contract: '1.0', id, name: id, version: '1.0.0' },
|
|
39
|
+
})
|
|
40
|
+
.run();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
46
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('record + list roundtrip; upsert replaces ip and consumer', () => {
|
|
50
|
+
recordDnsRegistration(db, {
|
|
51
|
+
providerModuleId: 'namecheap',
|
|
52
|
+
consumerModuleId: 'caddy',
|
|
53
|
+
fqdn: 'www.example.net',
|
|
54
|
+
ip: '198.51.100.7',
|
|
55
|
+
});
|
|
56
|
+
recordDnsRegistration(db, {
|
|
57
|
+
providerModuleId: 'namecheap',
|
|
58
|
+
consumerModuleId: 'caddy',
|
|
59
|
+
fqdn: 'www.example.net',
|
|
60
|
+
ip: '198.51.100.8',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const rows = listDnsRegistrations(db, { providerModuleId: 'namecheap' });
|
|
64
|
+
expect(rows.length).toBe(1);
|
|
65
|
+
expect(rows[0].fqdn).toBe('www.example.net');
|
|
66
|
+
expect(rows[0].ip).toBe('198.51.100.8');
|
|
67
|
+
expect(rows[0].consumerModuleId).toBe('caddy');
|
|
68
|
+
expect(rows[0].refreshedAt).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('stampDnsRegistrationsRefreshed sets refreshedAt for the provider', () => {
|
|
72
|
+
recordDnsRegistration(db, {
|
|
73
|
+
providerModuleId: 'namecheap',
|
|
74
|
+
consumerModuleId: 'caddy',
|
|
75
|
+
fqdn: 'git.example.net',
|
|
76
|
+
ip: null,
|
|
77
|
+
});
|
|
78
|
+
stampDnsRegistrationsRefreshed(db, 'namecheap');
|
|
79
|
+
const [row] = listDnsRegistrations(db, { providerModuleId: 'namecheap' });
|
|
80
|
+
expect(row.refreshedAt).not.toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('rows die with the consumer module (FK cascade)', () => {
|
|
84
|
+
recordDnsRegistration(db, {
|
|
85
|
+
providerModuleId: 'namecheap',
|
|
86
|
+
consumerModuleId: 'caddy',
|
|
87
|
+
fqdn: 'www.example.net',
|
|
88
|
+
ip: '198.51.100.7',
|
|
89
|
+
});
|
|
90
|
+
db.delete(modules).where(eq(modules.id, 'caddy')).run();
|
|
91
|
+
expect(listDnsRegistrations(db).length).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('withDnsRegistrationLedger records successes only', async () => {
|
|
95
|
+
const calls: string[] = [];
|
|
96
|
+
const fake: DnsRegistrarCapability = {
|
|
97
|
+
async registerHost(request) {
|
|
98
|
+
calls.push(request.fqdn);
|
|
99
|
+
const success = !request.fqdn.startsWith('fail.');
|
|
100
|
+
return { success, outputs: {}, duration: 1 };
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const wrapped = withDnsRegistrationLedger(fake, {
|
|
104
|
+
db,
|
|
105
|
+
providerModuleId: 'namecheap',
|
|
106
|
+
consumerModuleId: 'caddy',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await wrapped.registerHost({ fqdn: 'www.example.net', ip: '198.51.100.7' });
|
|
110
|
+
await wrapped.registerHost({ fqdn: 'fail.example.net', ip: '198.51.100.7' });
|
|
111
|
+
await wrapped.registerHost({ fqdn: 'auto.example.net' });
|
|
112
|
+
|
|
113
|
+
expect(calls.length).toBe(3);
|
|
114
|
+
const rows = listDnsRegistrations(db);
|
|
115
|
+
const fqdns = rows.map((r) => r.fqdn).sort();
|
|
116
|
+
expect(fqdns).toEqual(['auto.example.net', 'www.example.net']);
|
|
117
|
+
const auto = rows.find((r) => r.fqdn === 'auto.example.net');
|
|
118
|
+
expect(auto?.ip).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DNS registration ledger operations
|
|
3
|
+
* (designs/DISPATCHER_DAEMON_AND_TIMER_EVENTS.md B2/B3).
|
|
4
|
+
*
|
|
5
|
+
* The capability loader records every successful
|
|
6
|
+
* dns_registrar.registerHost here; the run-hook path reads the ledger
|
|
7
|
+
* back to feed a provider's `refresh_registrations` hook, and `celilo
|
|
8
|
+
* dns registrations` lists it for the operator. Row lifecycle is FK
|
|
9
|
+
* cascade — registrations die with their provider or consumer module.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DnsRegistrarCapability, HookResult } from '@celilo/capabilities';
|
|
13
|
+
import { eq } from 'drizzle-orm';
|
|
14
|
+
import type { DbClient } from '../db/client';
|
|
15
|
+
import { dnsRegistrations } from '../db/schema';
|
|
16
|
+
|
|
17
|
+
export interface DnsRegistrationRow {
|
|
18
|
+
fqdn: string;
|
|
19
|
+
/** null = the provider auto-detected the request's source IP. */
|
|
20
|
+
ip: string | null;
|
|
21
|
+
providerModuleId: string;
|
|
22
|
+
consumerModuleId: string;
|
|
23
|
+
registeredAt: Date;
|
|
24
|
+
refreshedAt: Date | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function listDnsRegistrations(
|
|
28
|
+
db: DbClient,
|
|
29
|
+
options: { providerModuleId?: string } = {},
|
|
30
|
+
): DnsRegistrationRow[] {
|
|
31
|
+
const query = db
|
|
32
|
+
.select({
|
|
33
|
+
fqdn: dnsRegistrations.fqdn,
|
|
34
|
+
ip: dnsRegistrations.ip,
|
|
35
|
+
providerModuleId: dnsRegistrations.providerModuleId,
|
|
36
|
+
consumerModuleId: dnsRegistrations.consumerModuleId,
|
|
37
|
+
registeredAt: dnsRegistrations.registeredAt,
|
|
38
|
+
refreshedAt: dnsRegistrations.refreshedAt,
|
|
39
|
+
})
|
|
40
|
+
.from(dnsRegistrations);
|
|
41
|
+
const rows = options.providerModuleId
|
|
42
|
+
? query.where(eq(dnsRegistrations.providerModuleId, options.providerModuleId)).all()
|
|
43
|
+
: query.all();
|
|
44
|
+
return rows;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function recordDnsRegistration(
|
|
48
|
+
db: DbClient,
|
|
49
|
+
registration: {
|
|
50
|
+
providerModuleId: string;
|
|
51
|
+
consumerModuleId: string;
|
|
52
|
+
fqdn: string;
|
|
53
|
+
ip: string | null;
|
|
54
|
+
},
|
|
55
|
+
): void {
|
|
56
|
+
db.insert(dnsRegistrations)
|
|
57
|
+
.values({
|
|
58
|
+
providerModuleId: registration.providerModuleId,
|
|
59
|
+
consumerModuleId: registration.consumerModuleId,
|
|
60
|
+
fqdn: registration.fqdn,
|
|
61
|
+
ip: registration.ip,
|
|
62
|
+
registeredAt: new Date(),
|
|
63
|
+
})
|
|
64
|
+
.onConflictDoUpdate({
|
|
65
|
+
target: [dnsRegistrations.providerModuleId, dnsRegistrations.fqdn],
|
|
66
|
+
set: {
|
|
67
|
+
consumerModuleId: registration.consumerModuleId,
|
|
68
|
+
ip: registration.ip,
|
|
69
|
+
registeredAt: new Date(),
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
.run();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Stamp every row of a provider as refreshed (successful refresh hook). */
|
|
76
|
+
export function stampDnsRegistrationsRefreshed(db: DbClient, providerModuleId: string): void {
|
|
77
|
+
db.update(dnsRegistrations)
|
|
78
|
+
.set({ refreshedAt: new Date() })
|
|
79
|
+
.where(eq(dnsRegistrations.providerModuleId, providerModuleId))
|
|
80
|
+
.run();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wrap a dns_registrar interface so successful registerHost calls are
|
|
85
|
+
* recorded in the ledger. Failures and MissingProviderInputError
|
|
86
|
+
* interview throws pass through untouched — only a confirmed
|
|
87
|
+
* registration earns a row.
|
|
88
|
+
*/
|
|
89
|
+
export function withDnsRegistrationLedger(
|
|
90
|
+
registrar: DnsRegistrarCapability,
|
|
91
|
+
ctx: { db: DbClient; providerModuleId: string; consumerModuleId: string },
|
|
92
|
+
): DnsRegistrarCapability {
|
|
93
|
+
return {
|
|
94
|
+
...registrar,
|
|
95
|
+
async registerHost(request): Promise<HookResult> {
|
|
96
|
+
const result = await registrar.registerHost(request);
|
|
97
|
+
if (result.success && request.fqdn) {
|
|
98
|
+
recordDnsRegistration(ctx.db, {
|
|
99
|
+
providerModuleId: ctx.providerModuleId,
|
|
100
|
+
consumerModuleId: ctx.consumerModuleId,
|
|
101
|
+
fqdn: request.fqdn,
|
|
102
|
+
ip: request.ip ?? null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
readInstalledUnit,
|
|
9
9
|
renderLaunchdPlist,
|
|
10
10
|
renderSystemdUnit,
|
|
11
|
+
resolveRunAsUser,
|
|
11
12
|
uninstallDaemon,
|
|
12
13
|
} from './events-daemon';
|
|
13
14
|
|
|
@@ -19,6 +20,7 @@ describe('renderSystemdUnit', () => {
|
|
|
19
20
|
pollMs: 1000,
|
|
20
21
|
concurrency: 4,
|
|
21
22
|
home: '/home/op',
|
|
23
|
+
scope: 'user',
|
|
22
24
|
});
|
|
23
25
|
expect(out).toContain(
|
|
24
26
|
'ExecStart=/usr/local/bin/celilo events run --poll-ms 1000 --concurrency 4',
|
|
@@ -26,6 +28,7 @@ describe('renderSystemdUnit', () => {
|
|
|
26
28
|
expect(out).toContain('Environment=EVENT_BUS_DB=/var/lib/celilo/events.db');
|
|
27
29
|
expect(out).toContain('Restart=on-failure');
|
|
28
30
|
expect(out).toContain('WantedBy=default.target');
|
|
31
|
+
expect(out).not.toContain('User=');
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
it('honors --poll-ms and --concurrency overrides', () => {
|
|
@@ -35,9 +38,26 @@ describe('renderSystemdUnit', () => {
|
|
|
35
38
|
pollMs: 250,
|
|
36
39
|
concurrency: 8,
|
|
37
40
|
home: '/h',
|
|
41
|
+
scope: 'user',
|
|
38
42
|
});
|
|
39
43
|
expect(out).toContain('--poll-ms 250 --concurrency 8');
|
|
40
44
|
});
|
|
45
|
+
|
|
46
|
+
it('system scope sets explicit User= and multi-user.target', () => {
|
|
47
|
+
const out = renderSystemdUnit({
|
|
48
|
+
celiloPath: '/usr/local/bin/celilo',
|
|
49
|
+
busDbPath: '/var/celilo/events.db',
|
|
50
|
+
pollMs: 1000,
|
|
51
|
+
concurrency: 4,
|
|
52
|
+
home: '/root',
|
|
53
|
+
scope: 'system',
|
|
54
|
+
runAsUser: 'celilo',
|
|
55
|
+
});
|
|
56
|
+
expect(out).toContain('User=celilo');
|
|
57
|
+
expect(out).toContain('WantedBy=multi-user.target');
|
|
58
|
+
expect(out).toContain('journalctl -u celilo-events.service');
|
|
59
|
+
expect(out).not.toContain('journalctl --user');
|
|
60
|
+
});
|
|
41
61
|
});
|
|
42
62
|
|
|
43
63
|
describe('renderLaunchdPlist', () => {
|
|
@@ -48,6 +68,7 @@ describe('renderLaunchdPlist', () => {
|
|
|
48
68
|
pollMs: 1000,
|
|
49
69
|
concurrency: 4,
|
|
50
70
|
home: '/Users/op',
|
|
71
|
+
scope: 'user',
|
|
51
72
|
});
|
|
52
73
|
expect(out).toContain('<string>/Users/op/Library/Logs/celilo-events.out.log</string>');
|
|
53
74
|
expect(out).toContain('<string>/Users/op/Library/Logs/celilo-events.err.log</string>');
|
|
@@ -55,6 +76,24 @@ describe('renderLaunchdPlist', () => {
|
|
|
55
76
|
expect(out).toContain('<string>com.celilo.events</string>');
|
|
56
77
|
expect(out).toContain('<key>RunAtLoad</key>');
|
|
57
78
|
expect(out).toContain('<key>KeepAlive</key>');
|
|
79
|
+
expect(out).not.toContain('<key>UserName</key>');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('system scope sets UserName and logs under /Library/Logs', () => {
|
|
83
|
+
const out = renderLaunchdPlist({
|
|
84
|
+
celiloPath: '/c',
|
|
85
|
+
busDbPath: '/db',
|
|
86
|
+
pollMs: 1000,
|
|
87
|
+
concurrency: 4,
|
|
88
|
+
home: '/Users/op',
|
|
89
|
+
scope: 'system',
|
|
90
|
+
runAsUser: 'celilo',
|
|
91
|
+
});
|
|
92
|
+
expect(out).toContain('<key>UserName</key>');
|
|
93
|
+
expect(out).toContain('<string>celilo</string>');
|
|
94
|
+
expect(out).toContain('<string>/Library/Logs/celilo-events.out.log</string>');
|
|
95
|
+
expect(out).toContain('<string>/Library/Logs/celilo-events.err.log</string>');
|
|
96
|
+
expect(out).not.toContain('/Users/op/Library/Logs');
|
|
58
97
|
});
|
|
59
98
|
});
|
|
60
99
|
|
|
@@ -69,6 +108,24 @@ describe('getDaemonUnitPath', () => {
|
|
|
69
108
|
'/Users/op/Library/LaunchAgents/com.celilo.events.plist',
|
|
70
109
|
);
|
|
71
110
|
});
|
|
111
|
+
it('returns the system paths for system scope', () => {
|
|
112
|
+
expect(getDaemonUnitPath('linux', '/home/op', 'system')).toBe(
|
|
113
|
+
'/etc/systemd/system/celilo-events.service',
|
|
114
|
+
);
|
|
115
|
+
expect(getDaemonUnitPath('darwin', '/Users/op', 'system')).toBe(
|
|
116
|
+
'/Library/LaunchDaemons/com.celilo.events.plist',
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('resolveRunAsUser', () => {
|
|
122
|
+
it('honors an explicit override', () => {
|
|
123
|
+
expect(resolveRunAsUser('/var/celilo/events.db', 'celilo')).toBe('celilo');
|
|
124
|
+
});
|
|
125
|
+
it('falls back to the current user when the state dir is missing', () => {
|
|
126
|
+
const me = resolveRunAsUser('/no/such/dir/events.db');
|
|
127
|
+
expect(me.length).toBeGreaterThan(0);
|
|
128
|
+
});
|
|
72
129
|
});
|
|
73
130
|
|
|
74
131
|
describe('installDaemon / uninstallDaemon roundtrip', () => {
|
|
@@ -99,6 +156,8 @@ describe('installDaemon / uninstallDaemon roundtrip', () => {
|
|
|
99
156
|
busDbPath: '/var/lib/celilo/events.db',
|
|
100
157
|
});
|
|
101
158
|
expect(existsSync(installed.unitPath)).toBe(true);
|
|
159
|
+
expect(installed.scope).toBe('user');
|
|
160
|
+
expect(installed.runAsUser).toBeUndefined();
|
|
102
161
|
expect(installed.unitPath).toBe(join(home, '.config/systemd/user/celilo-events.service'));
|
|
103
162
|
expect(installed.nextSteps[0]).toContain('systemctl --user daemon-reload');
|
|
104
163
|
|