@celilo/cli 0.3.30 → 0.4.0-alpha.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/0005_module_operations.sql +12 -0
- package/drizzle/0006_base_module_aspects.sql +15 -0
- package/drizzle/0007_module_systems.sql +17 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +6 -5
- package/schemas/system_config.json +14 -28
- package/src/ansible/inventory.test.ts +46 -62
- package/src/ansible/inventory.ts +48 -25
- package/src/capabilities/registration.ts +25 -7
- package/src/capabilities/validation.test.ts +30 -0
- package/src/capabilities/validation.ts +8 -0
- package/src/cli/backup-rename.test.ts +95 -0
- package/src/cli/cli.test.ts +17 -23
- package/src/cli/command-registry.ts +199 -0
- package/src/cli/commands/backup-list.ts +1 -1
- package/src/cli/commands/events.ts +96 -0
- package/src/cli/commands/machine-add.ts +103 -59
- package/src/cli/commands/module-import.ts +153 -4
- package/src/cli/commands/module-remove.ts +86 -17
- package/src/cli/commands/module-status.ts +6 -2
- package/src/cli/commands/publish/alpha.test.ts +185 -0
- package/src/cli/commands/publish/alpha.ts +226 -0
- package/src/cli/commands/publish/changesets.test.ts +89 -0
- package/src/cli/commands/publish/changesets.ts +144 -0
- package/src/cli/commands/publish/consumer-pins.test.ts +155 -0
- package/src/cli/commands/publish/consumer-pins.ts +149 -0
- package/src/cli/commands/publish/execute.ts +131 -0
- package/src/cli/commands/publish/global-install.test.ts +154 -0
- package/src/cli/commands/publish/global-install.ts +171 -0
- package/src/cli/commands/publish/helpers.ts +227 -0
- package/src/cli/commands/publish/index.ts +365 -0
- package/src/cli/commands/publish/module-registry.test.ts +40 -0
- package/src/cli/commands/publish/module-registry.ts +64 -0
- package/src/cli/commands/publish/plan.ts +107 -0
- package/src/cli/commands/publish/preflight.ts +238 -0
- package/src/cli/commands/publish/types.ts +264 -0
- package/src/cli/commands/publish/workspace.test.ts +323 -0
- package/src/cli/commands/publish/workspace.ts +596 -0
- package/src/cli/commands/restore.ts +126 -0
- package/src/cli/commands/storage-add-local.ts +1 -1
- package/src/cli/commands/storage-add-s3.ts +1 -1
- package/src/cli/commands/subscribers-add.ts +68 -0
- package/src/cli/commands/subscribers-list.ts +48 -0
- package/src/cli/commands/subscribers-remove.ts +38 -0
- package/src/cli/commands/subscribers-serve.ts +77 -0
- package/src/cli/commands/subscribers-status.ts +33 -0
- package/src/cli/commands/subscribers-test.ts +71 -0
- package/src/cli/commands/system-apply-config-equivalence.test.ts +108 -0
- package/src/cli/commands/system-apply-config.test.ts +70 -0
- package/src/cli/commands/system-apply-config.ts +130 -0
- package/src/cli/commands/system-audit.ts +2 -1
- package/src/cli/commands/system-init-deprecation.test.ts +90 -0
- package/src/cli/commands/system-init.ts +36 -70
- package/src/cli/commands/system-update.ts +3 -2
- package/src/cli/completion.ts +22 -1
- package/src/cli/index.ts +214 -6
- package/src/cli/interactive-config.test.ts +19 -0
- package/src/cli/restore-command.test.ts +131 -0
- package/src/db/client.ts +42 -0
- package/src/db/schema.test.ts +13 -16
- package/src/db/schema.ts +161 -9
- package/src/hooks/capability-loader-firewall.test.ts +6 -15
- package/src/hooks/capability-loader.test.ts +2 -3
- package/src/hooks/capability-loader.ts +36 -2
- package/src/hooks/define-hook.test.ts +4 -0
- package/src/hooks/executor.test.ts +18 -0
- package/src/hooks/executor.ts +21 -2
- package/src/hooks/load-hook-config.test.ts +26 -24
- package/src/hooks/load-hook-config.ts +11 -2
- package/src/hooks/run-named-hook.ts +16 -0
- package/src/hooks/types.ts +9 -1
- package/src/manifest/contracts/v1.ts +70 -0
- package/src/manifest/schema.ts +262 -16
- package/src/manifest/validate-privileged.test.ts +84 -0
- package/src/manifest/validate.test.ts +156 -0
- package/src/manifest/validate.ts +69 -0
- package/src/module/import.ts +12 -0
- package/src/services/aspect-approvals.test.ts +231 -0
- package/src/services/aspect-approvals.ts +120 -0
- package/src/services/aspect-runner.test.ts +493 -0
- package/src/services/aspect-runner.ts +438 -0
- package/src/services/aspect-template-resolver.test.ts +101 -0
- package/src/services/aspect-template-resolver.ts +122 -0
- package/src/services/backup-create.ts +104 -25
- package/src/services/backup-envelope-roundtrip.test.ts +199 -0
- package/src/services/backup-in-flight-refusal.test.ts +163 -0
- package/src/services/backup-manifest.test.ts +115 -0
- package/src/services/backup-manifest.ts +163 -0
- package/src/services/backup-restore.ts +154 -19
- package/src/services/build-bus/delivery-events.ts +92 -0
- package/src/services/build-bus/event-factory.ts +54 -0
- package/src/services/build-bus/fan-out.test.ts +279 -0
- package/src/services/build-bus/fan-out.ts +161 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +157 -0
- package/src/services/build-bus/hook-dispatch.test.ts +207 -0
- package/src/services/build-bus/hook-dispatch.ts +198 -0
- package/src/services/build-bus/hook-dispatcher.ts +115 -0
- package/src/services/build-bus/index.ts +41 -0
- package/src/services/build-bus/receiver-server.test.ts +179 -0
- package/src/services/build-bus/receiver-server.ts +159 -0
- package/src/services/build-bus/status.test.ts +212 -0
- package/src/services/build-bus/status.ts +213 -0
- package/src/services/build-bus/subscriber-store.ts +113 -0
- package/src/services/celilo-events.test.ts +70 -0
- package/src/services/celilo-events.ts +92 -0
- package/src/services/celilo-mgmt-hooks.test.ts +296 -0
- package/src/services/config-interview.ts +13 -95
- package/src/services/cross-module-data-manager.ts +2 -31
- package/src/services/cross-module-read.test.ts +250 -0
- package/src/services/cross-module-read.ts +232 -0
- package/src/services/deploy-validation.ts +7 -0
- package/src/services/deployed-systems.test.ts +235 -0
- package/src/services/deployed-systems.ts +308 -0
- package/src/services/dns-provider-backfill.ts +75 -0
- package/src/services/health-runner.ts +19 -3
- package/src/services/infrastructure-variable-resolver.test.ts +6 -32
- package/src/services/infrastructure-variable-resolver.ts +3 -13
- package/src/services/machine-detector.ts +104 -48
- package/src/services/machine-pool.ts +145 -2
- package/src/services/module-config.ts +78 -120
- package/src/services/module-deploy.ts +113 -40
- package/src/services/module-operations.test.ts +154 -0
- package/src/services/module-operations.ts +154 -0
- package/src/services/module-subscriptions.test.ts +58 -0
- package/src/services/module-subscriptions.ts +24 -1
- package/src/services/module-types-generator.test.ts +3 -3
- package/src/services/module-types-generator.ts +7 -2
- package/src/services/proxmox-reconcile.test.ts +333 -0
- package/src/services/proxmox-reconcile.ts +156 -0
- package/src/services/proxmox-state-recovery.ts +3 -24
- package/src/services/restore-from-file.test.ts +177 -0
- package/src/services/restore-from-file.ts +355 -0
- package/src/services/restore-preflight.test.ts +127 -0
- package/src/services/restore-preflight.ts +118 -0
- package/src/services/storage-providers/s3.ts +10 -2
- package/src/services/system-identity.ts +30 -0
- package/src/services/system-init.test.ts +64 -21
- package/src/services/system-init.ts +28 -26
- package/src/templates/generator.test.ts +7 -16
- package/src/templates/generator.ts +28 -115
- package/src/test-utils/integration.ts +5 -2
- package/src/types/infrastructure.ts +8 -0
- package/src/variables/computed/computed-integration.test.ts +191 -0
- package/src/variables/computed/computed.test.ts +177 -0
- package/src/variables/computed/evaluate.ts +271 -0
- package/src/variables/computed/marker.ts +53 -0
- package/src/variables/computed/parse.ts +262 -0
- package/src/variables/computed/provider-lookup.ts +130 -0
- package/src/variables/context.test.ts +89 -28
- package/src/variables/context.ts +196 -191
- package/src/variables/parser.ts +3 -3
- package/src/variables/resolver.test.ts +61 -0
- package/src/variables/resolver.ts +81 -0
- package/src/variables/types.ts +23 -1
- package/src/services/dns-auto-register.ts +0 -211
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior tests for the celilo-mgmt module's on_backup / on_restore
|
|
3
|
+
* hooks (Phase 3 of v2/SYSTEM_BACKUP_TERRAFORM_STATE.md).
|
|
4
|
+
*
|
|
5
|
+
* We import the hook scripts directly and call their handlers with a
|
|
6
|
+
* synthesized HookContext, then assert on the side-effects (files
|
|
7
|
+
* written to backup_dir / restore staging dirs). This catches
|
|
8
|
+
* regressions in the hook bodies without spinning up the full
|
|
9
|
+
* deploy/backup pipeline.
|
|
10
|
+
*
|
|
11
|
+
* What this DOESN'T test: invokeHook validation, end-to-end backup
|
|
12
|
+
* envelope round-trip. Those live in backup-envelope-roundtrip.test.ts
|
|
13
|
+
* and (eventually) a fresh-box e2e.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
17
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import type { HookContext } from '@celilo/capabilities';
|
|
22
|
+
|
|
23
|
+
// Hooks under test — imported via the scripts dir adjacent to the
|
|
24
|
+
// modules/celilo-mgmt/manifest.yml. The dynamic import keeps the test
|
|
25
|
+
// runnable from the apps/celilo working dir.
|
|
26
|
+
const HOOK_DIR = join(import.meta.dirname, '../../../../modules/celilo-mgmt/scripts');
|
|
27
|
+
|
|
28
|
+
interface BackupHookOutput {
|
|
29
|
+
artifact_count: number;
|
|
30
|
+
size_bytes: number;
|
|
31
|
+
schema_version: string;
|
|
32
|
+
}
|
|
33
|
+
interface RestoreHookOutput {
|
|
34
|
+
restored_items: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildLogger(): {
|
|
38
|
+
logger: HookContext['logger'];
|
|
39
|
+
messages: string[];
|
|
40
|
+
} {
|
|
41
|
+
const messages: string[] = [];
|
|
42
|
+
const logger: HookContext['logger'] = {
|
|
43
|
+
info: (m: string) => messages.push(`info: ${m}`),
|
|
44
|
+
warn: (m: string) => messages.push(`warn: ${m}`),
|
|
45
|
+
error: (m: string) => messages.push(`error: ${m}`),
|
|
46
|
+
success: (m: string) => messages.push(`success: ${m}`),
|
|
47
|
+
};
|
|
48
|
+
return { logger, messages };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildContext(extras: Record<string, unknown>): HookContext {
|
|
52
|
+
const { logger } = buildLogger();
|
|
53
|
+
return {
|
|
54
|
+
logger,
|
|
55
|
+
config: {
|
|
56
|
+
db_path: '', // populated by caller via env
|
|
57
|
+
hostname: 'test-mgmt',
|
|
58
|
+
target_ip: '127.0.0.1',
|
|
59
|
+
event_bus_socket: '',
|
|
60
|
+
listen_port: 8123,
|
|
61
|
+
dns_primary: '1.1.1.1',
|
|
62
|
+
dns_fallback: '',
|
|
63
|
+
network_dmz_subnet: '',
|
|
64
|
+
network_app_subnet: '',
|
|
65
|
+
network_secure_subnet: '',
|
|
66
|
+
network_internal_subnet: '',
|
|
67
|
+
ssh_public_key: '',
|
|
68
|
+
install_docker: false,
|
|
69
|
+
install_terraform: false,
|
|
70
|
+
} as Record<string, unknown>,
|
|
71
|
+
secrets: {},
|
|
72
|
+
capabilities: {},
|
|
73
|
+
debug: false,
|
|
74
|
+
screenshotDir: '',
|
|
75
|
+
...extras,
|
|
76
|
+
} as HookContext;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('celilo-mgmt on_backup', () => {
|
|
80
|
+
let dir: string;
|
|
81
|
+
let backupDir: string;
|
|
82
|
+
let crossModuleRoot: string;
|
|
83
|
+
let dbPath: string;
|
|
84
|
+
let keyPath: string;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
dir = mkdtempSync(join(tmpdir(), 'celilo-mgmt-backup-test-'));
|
|
88
|
+
backupDir = join(dir, 'backup-dir');
|
|
89
|
+
mkdirSync(backupDir, { recursive: true });
|
|
90
|
+
crossModuleRoot = join(dir, 'cross-module-root');
|
|
91
|
+
mkdirSync(join(crossModuleRoot, 'modules', 'caddy', 'terraform'), { recursive: true });
|
|
92
|
+
writeFileSync(
|
|
93
|
+
join(crossModuleRoot, 'modules', 'caddy', 'terraform', 'terraform.tfstate'),
|
|
94
|
+
'{"state":"caddy"}',
|
|
95
|
+
);
|
|
96
|
+
writeFileSync(
|
|
97
|
+
join(crossModuleRoot, 'index.json'),
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
schemaVersion: '1.0',
|
|
100
|
+
generatedAt: '2026-05-20T00:00:00Z',
|
|
101
|
+
modules: [{ id: 'caddy', version: '0.0.1', terraformStateDir: 'modules/caddy/terraform' }],
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
dbPath = join(dir, 'celilo.db');
|
|
106
|
+
writeFileSync(dbPath, 'fake-sqlite-bytes-for-test');
|
|
107
|
+
process.env.CELILO_DB_PATH = dbPath;
|
|
108
|
+
|
|
109
|
+
keyPath = join(dir, 'master.key');
|
|
110
|
+
writeFileSync(keyPath, 'fake-master-key-32-bytes-padding!');
|
|
111
|
+
process.env.CELILO_MASTER_KEY_PATH = keyPath;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
116
|
+
process.env.CELILO_MASTER_KEY_PATH = undefined;
|
|
117
|
+
try {
|
|
118
|
+
rmSync(dir, { recursive: true, force: true });
|
|
119
|
+
} catch {
|
|
120
|
+
/* ignore */
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('writes celilo.db, master.key, machine-pool.json, and cross_module_state into backup_dir', async () => {
|
|
125
|
+
const { default: hook } = await import(`${HOOK_DIR}/on_backup.ts`);
|
|
126
|
+
const ctx = buildContext({
|
|
127
|
+
backup_dir: backupDir,
|
|
128
|
+
cross_module_root: crossModuleRoot,
|
|
129
|
+
});
|
|
130
|
+
const result = (await hook(ctx)) as BackupHookOutput;
|
|
131
|
+
|
|
132
|
+
expect(existsSync(join(backupDir, 'celilo.db'))).toBe(true);
|
|
133
|
+
expect(existsSync(join(backupDir, 'master.key'))).toBe(true);
|
|
134
|
+
expect(existsSync(join(backupDir, 'machine-pool.json'))).toBe(true);
|
|
135
|
+
expect(
|
|
136
|
+
existsSync(
|
|
137
|
+
join(backupDir, 'cross_module_state', 'modules', 'caddy', 'terraform', 'terraform.tfstate'),
|
|
138
|
+
),
|
|
139
|
+
).toBe(true);
|
|
140
|
+
expect(existsSync(join(backupDir, 'cross_module_state', 'index.json'))).toBe(true);
|
|
141
|
+
|
|
142
|
+
expect(result.schema_version).toBe('1.0');
|
|
143
|
+
expect(result.artifact_count).toBeGreaterThan(0);
|
|
144
|
+
expect(result.size_bytes).toBeGreaterThan(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('machine-pool.json is valid JSON (array)', async () => {
|
|
148
|
+
const { default: hook } = await import(`${HOOK_DIR}/on_backup.ts`);
|
|
149
|
+
await hook(buildContext({ backup_dir: backupDir, cross_module_root: crossModuleRoot }));
|
|
150
|
+
|
|
151
|
+
const machinePool = JSON.parse(readFileSync(join(backupDir, 'machine-pool.json'), 'utf-8'));
|
|
152
|
+
expect(Array.isArray(machinePool)).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('proceeds (with a warning) when cross_module_root is missing', async () => {
|
|
156
|
+
const { default: hook } = await import(`${HOOK_DIR}/on_backup.ts`);
|
|
157
|
+
const result = (await hook(
|
|
158
|
+
buildContext({ backup_dir: backupDir }), // no cross_module_root
|
|
159
|
+
)) as BackupHookOutput;
|
|
160
|
+
|
|
161
|
+
expect(existsSync(join(backupDir, 'celilo.db'))).toBe(true);
|
|
162
|
+
expect(existsSync(join(backupDir, 'cross_module_state'))).toBe(false);
|
|
163
|
+
expect(result.schema_version).toBe('1.0');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('proceeds (with a warning) when master.key is missing on disk', async () => {
|
|
167
|
+
rmSync(keyPath);
|
|
168
|
+
const { default: hook } = await import(`${HOOK_DIR}/on_backup.ts`);
|
|
169
|
+
const result = (await hook(
|
|
170
|
+
buildContext({ backup_dir: backupDir, cross_module_root: crossModuleRoot }),
|
|
171
|
+
)) as BackupHookOutput;
|
|
172
|
+
|
|
173
|
+
expect(existsSync(join(backupDir, 'master.key'))).toBe(false);
|
|
174
|
+
expect(existsSync(join(backupDir, 'celilo.db'))).toBe(true);
|
|
175
|
+
expect(result.artifact_count).toBeGreaterThan(0);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('celilo-mgmt on_restore', () => {
|
|
180
|
+
let dir: string;
|
|
181
|
+
let restoreDir: string;
|
|
182
|
+
let crossModuleWriteRoot: string;
|
|
183
|
+
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
dir = mkdtempSync(join(tmpdir(), 'celilo-mgmt-restore-test-'));
|
|
186
|
+
restoreDir = join(dir, 'restore-dir');
|
|
187
|
+
mkdirSync(restoreDir, { recursive: true });
|
|
188
|
+
crossModuleWriteRoot = join(dir, 'cross-module-write-root');
|
|
189
|
+
mkdirSync(crossModuleWriteRoot, { recursive: true });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
afterEach(() => {
|
|
193
|
+
try {
|
|
194
|
+
rmSync(dir, { recursive: true, force: true });
|
|
195
|
+
} catch {
|
|
196
|
+
/* ignore */
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
function plantArtifact(): void {
|
|
201
|
+
writeFileSync(join(restoreDir, 'celilo.db'), 'restored-db-bytes');
|
|
202
|
+
writeFileSync(join(restoreDir, 'master.key'), 'restored-master-key');
|
|
203
|
+
writeFileSync(join(restoreDir, 'machine-pool.json'), '[]');
|
|
204
|
+
mkdirSync(join(restoreDir, 'cross_module_state', 'modules', 'caddy', 'terraform'), {
|
|
205
|
+
recursive: true,
|
|
206
|
+
});
|
|
207
|
+
writeFileSync(
|
|
208
|
+
join(restoreDir, 'cross_module_state', 'modules', 'caddy', 'terraform', 'terraform.tfstate'),
|
|
209
|
+
'{"state":"caddy-restored"}',
|
|
210
|
+
);
|
|
211
|
+
mkdirSync(join(restoreDir, 'cross_module_state', 'modules', 'homebridge', 'terraform'), {
|
|
212
|
+
recursive: true,
|
|
213
|
+
});
|
|
214
|
+
writeFileSync(
|
|
215
|
+
join(
|
|
216
|
+
restoreDir,
|
|
217
|
+
'cross_module_state',
|
|
218
|
+
'modules',
|
|
219
|
+
'homebridge',
|
|
220
|
+
'terraform',
|
|
221
|
+
'terraform.tfstate',
|
|
222
|
+
),
|
|
223
|
+
'{"state":"homebridge-restored"}',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
it('moves cross-module terraform state into cross_module_write_root for the framework apply', async () => {
|
|
228
|
+
plantArtifact();
|
|
229
|
+
const { default: hook } = await import(`${HOOK_DIR}/on_restore.ts`);
|
|
230
|
+
const result = (await hook(
|
|
231
|
+
buildContext({
|
|
232
|
+
restore_dir: restoreDir,
|
|
233
|
+
schema_version: '1.0',
|
|
234
|
+
cross_module_write_root: crossModuleWriteRoot,
|
|
235
|
+
}),
|
|
236
|
+
)) as RestoreHookOutput;
|
|
237
|
+
|
|
238
|
+
expect(
|
|
239
|
+
existsSync(join(crossModuleWriteRoot, 'modules', 'caddy', 'terraform', 'terraform.tfstate')),
|
|
240
|
+
).toBe(true);
|
|
241
|
+
expect(
|
|
242
|
+
existsSync(
|
|
243
|
+
join(crossModuleWriteRoot, 'modules', 'homebridge', 'terraform', 'terraform.tfstate'),
|
|
244
|
+
),
|
|
245
|
+
).toBe(true);
|
|
246
|
+
|
|
247
|
+
// 2 cross-module modules + 2 system files (db + master.key)
|
|
248
|
+
expect(result.restored_items).toBe(4);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('stages celilo.db + master.key under restore_dir/system/ for Phase 4 wrapper', async () => {
|
|
252
|
+
plantArtifact();
|
|
253
|
+
const { default: hook } = await import(`${HOOK_DIR}/on_restore.ts`);
|
|
254
|
+
await hook(
|
|
255
|
+
buildContext({
|
|
256
|
+
restore_dir: restoreDir,
|
|
257
|
+
schema_version: '1.0',
|
|
258
|
+
cross_module_write_root: crossModuleWriteRoot,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(existsSync(join(restoreDir, 'system', 'celilo.db'))).toBe(true);
|
|
263
|
+
expect(existsSync(join(restoreDir, 'system', 'master.key'))).toBe(true);
|
|
264
|
+
expect(readFileSync(join(restoreDir, 'system', 'celilo.db'), 'utf-8')).toBe(
|
|
265
|
+
'restored-db-bytes',
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('skips cross-module restore (with a warning) when cross_module_write_root is missing', async () => {
|
|
270
|
+
plantArtifact();
|
|
271
|
+
const { default: hook } = await import(`${HOOK_DIR}/on_restore.ts`);
|
|
272
|
+
const result = (await hook(
|
|
273
|
+
buildContext({ restore_dir: restoreDir, schema_version: '1.0' }), // no cross_module_write_root
|
|
274
|
+
)) as RestoreHookOutput;
|
|
275
|
+
|
|
276
|
+
// Only system files staged; cross-module skipped.
|
|
277
|
+
expect(result.restored_items).toBe(2);
|
|
278
|
+
expect(existsSync(join(restoreDir, 'system', 'celilo.db'))).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('tolerates an artifact with no cross_module_state/ subdir', async () => {
|
|
282
|
+
// No plantArtifact call's cross-module branch — write only system files.
|
|
283
|
+
writeFileSync(join(restoreDir, 'celilo.db'), 'db');
|
|
284
|
+
writeFileSync(join(restoreDir, 'master.key'), 'key');
|
|
285
|
+
const { default: hook } = await import(`${HOOK_DIR}/on_restore.ts`);
|
|
286
|
+
const result = (await hook(
|
|
287
|
+
buildContext({
|
|
288
|
+
restore_dir: restoreDir,
|
|
289
|
+
schema_version: '1.0',
|
|
290
|
+
cross_module_write_root: crossModuleWriteRoot,
|
|
291
|
+
}),
|
|
292
|
+
)) as RestoreHookOutput;
|
|
293
|
+
|
|
294
|
+
expect(result.restored_items).toBe(2);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type SecretRequiredPayload,
|
|
18
18
|
busInterview,
|
|
19
19
|
} from './bus-interview';
|
|
20
|
+
import { parseStoredConfigValue, upsertModuleConfig } from './module-config';
|
|
20
21
|
import { getSecretMetadata } from './secret-schema-loader';
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -245,20 +246,7 @@ export async function autoDeriveMachineConfig(
|
|
|
245
246
|
|
|
246
247
|
log.success(`${variable.name} = ${derived} (auto-derived from ${machine.hostname})`);
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
.insert(moduleConfigs)
|
|
250
|
-
.values({
|
|
251
|
-
moduleId,
|
|
252
|
-
key: variable.name,
|
|
253
|
-
value: derived,
|
|
254
|
-
valueJson: null,
|
|
255
|
-
createdAt: new Date(),
|
|
256
|
-
updatedAt: new Date(),
|
|
257
|
-
})
|
|
258
|
-
.onConflictDoUpdate({
|
|
259
|
-
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
260
|
-
set: { value: derived, updatedAt: new Date() },
|
|
261
|
-
});
|
|
249
|
+
upsertModuleConfig(db, moduleId, variable.name, derived);
|
|
262
250
|
configured.push(variable.name);
|
|
263
251
|
|
|
264
252
|
// Handle per_selection follow-ups (e.g., zone_ip_* from zone list)
|
|
@@ -273,20 +261,7 @@ export async function autoDeriveMachineConfig(
|
|
|
273
261
|
|
|
274
262
|
if (followUpValue !== null) {
|
|
275
263
|
log.success(`${followUpKey} = ${followUpValue} (auto-derived from ${machine.hostname})`);
|
|
276
|
-
|
|
277
|
-
.insert(moduleConfigs)
|
|
278
|
-
.values({
|
|
279
|
-
moduleId,
|
|
280
|
-
key: followUpKey,
|
|
281
|
-
value: followUpValue,
|
|
282
|
-
valueJson: null,
|
|
283
|
-
createdAt: new Date(),
|
|
284
|
-
updatedAt: new Date(),
|
|
285
|
-
})
|
|
286
|
-
.onConflictDoUpdate({
|
|
287
|
-
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
288
|
-
set: { value: followUpValue, updatedAt: new Date() },
|
|
289
|
-
});
|
|
264
|
+
upsertModuleConfig(db, moduleId, followUpKey, followUpValue);
|
|
290
265
|
configured.push(followUpKey);
|
|
291
266
|
}
|
|
292
267
|
}
|
|
@@ -328,20 +303,7 @@ export async function interviewForMissingConfig(
|
|
|
328
303
|
const selectedValues = derived.split(',');
|
|
329
304
|
|
|
330
305
|
// Store the main variable
|
|
331
|
-
|
|
332
|
-
.insert(moduleConfigs)
|
|
333
|
-
.values({
|
|
334
|
-
moduleId,
|
|
335
|
-
key: variable.name,
|
|
336
|
-
value: derived,
|
|
337
|
-
valueJson: null,
|
|
338
|
-
createdAt: new Date(),
|
|
339
|
-
updatedAt: new Date(),
|
|
340
|
-
})
|
|
341
|
-
.onConflictDoUpdate({
|
|
342
|
-
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
343
|
-
set: { value: derived, updatedAt: new Date() },
|
|
344
|
-
});
|
|
306
|
+
upsertModuleConfig(db, moduleId, variable.name, derived);
|
|
345
307
|
configured.push(variable.name);
|
|
346
308
|
|
|
347
309
|
// Handle per_selection follow-ups
|
|
@@ -361,20 +323,7 @@ export async function interviewForMissingConfig(
|
|
|
361
323
|
log.info(
|
|
362
324
|
`✓ ${followUpKey} = ${followUpValue} (from machine ${earmarkedMachine.hostname})`,
|
|
363
325
|
);
|
|
364
|
-
|
|
365
|
-
.insert(moduleConfigs)
|
|
366
|
-
.values({
|
|
367
|
-
moduleId,
|
|
368
|
-
key: followUpKey,
|
|
369
|
-
value: followUpValue,
|
|
370
|
-
valueJson: null,
|
|
371
|
-
createdAt: new Date(),
|
|
372
|
-
updatedAt: new Date(),
|
|
373
|
-
})
|
|
374
|
-
.onConflictDoUpdate({
|
|
375
|
-
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
376
|
-
set: { value: followUpValue, updatedAt: new Date() },
|
|
377
|
-
});
|
|
326
|
+
upsertModuleConfig(db, moduleId, followUpKey, followUpValue);
|
|
378
327
|
configured.push(followUpKey);
|
|
379
328
|
} else {
|
|
380
329
|
// Can't derive — bus-mediated prompt (responder
|
|
@@ -404,20 +353,7 @@ export async function interviewForMissingConfig(
|
|
|
404
353
|
}
|
|
405
354
|
|
|
406
355
|
// Simple (non-multi-select) derived variable
|
|
407
|
-
|
|
408
|
-
.insert(moduleConfigs)
|
|
409
|
-
.values({
|
|
410
|
-
moduleId,
|
|
411
|
-
key: variable.name,
|
|
412
|
-
value: derived,
|
|
413
|
-
valueJson: null,
|
|
414
|
-
createdAt: new Date(),
|
|
415
|
-
updatedAt: new Date(),
|
|
416
|
-
})
|
|
417
|
-
.onConflictDoUpdate({
|
|
418
|
-
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
419
|
-
set: { value: derived, updatedAt: new Date() },
|
|
420
|
-
});
|
|
356
|
+
upsertModuleConfig(db, moduleId, variable.name, derived);
|
|
421
357
|
configured.push(variable.name);
|
|
422
358
|
continue;
|
|
423
359
|
}
|
|
@@ -850,14 +786,7 @@ export async function readModuleConfigKey(
|
|
|
850
786
|
.where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
|
|
851
787
|
.get();
|
|
852
788
|
if (!row) return undefined;
|
|
853
|
-
|
|
854
|
-
try {
|
|
855
|
-
return JSON.parse(row.valueJson);
|
|
856
|
-
} catch {
|
|
857
|
-
return row.value;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
return row.value;
|
|
789
|
+
return parseStoredConfigValue(row);
|
|
861
790
|
}
|
|
862
791
|
|
|
863
792
|
export async function writeModuleConfigKey(
|
|
@@ -866,23 +795,12 @@ export async function writeModuleConfigKey(
|
|
|
866
795
|
value: unknown,
|
|
867
796
|
db: DbClient,
|
|
868
797
|
): Promise<void> {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
key,
|
|
876
|
-
value: stringValue,
|
|
877
|
-
valueJson,
|
|
878
|
-
createdAt: new Date(),
|
|
879
|
-
updatedAt: new Date(),
|
|
880
|
-
})
|
|
881
|
-
.onConflictDoUpdate({
|
|
882
|
-
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
883
|
-
set: { value: stringValue, valueJson, updatedAt: new Date() },
|
|
884
|
-
})
|
|
885
|
-
.run();
|
|
798
|
+
upsertModuleConfig(
|
|
799
|
+
db,
|
|
800
|
+
moduleId,
|
|
801
|
+
key,
|
|
802
|
+
value as string | number | boolean | unknown[] | Record<string, unknown>,
|
|
803
|
+
);
|
|
886
804
|
}
|
|
887
805
|
|
|
888
806
|
export async function readModuleSecretKey(
|
|
@@ -17,6 +17,7 @@ import { decryptSecret, encryptSecret } from '../secrets/encryption';
|
|
|
17
17
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
18
18
|
import { buildResolutionContext } from '../variables/context';
|
|
19
19
|
import { resolveTemplate } from '../variables/resolver';
|
|
20
|
+
import { upsertModuleConfig } from './module-config';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Configuration data type
|
|
@@ -101,37 +102,7 @@ export class CrossModuleDataManager {
|
|
|
101
102
|
key: string,
|
|
102
103
|
value: string | number | boolean | unknown[] | Record<string, unknown>,
|
|
103
104
|
): Promise<void> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const existing = this.db
|
|
107
|
-
.select()
|
|
108
|
-
.from(moduleConfigs)
|
|
109
|
-
.where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
|
|
110
|
-
.get();
|
|
111
|
-
|
|
112
|
-
if (existing) {
|
|
113
|
-
// Update existing
|
|
114
|
-
this.db
|
|
115
|
-
.update(moduleConfigs)
|
|
116
|
-
.set({
|
|
117
|
-
value: isComplex ? '' : String(value),
|
|
118
|
-
valueJson: isComplex ? JSON.stringify(value) : null,
|
|
119
|
-
updatedAt: new Date(Date.now()),
|
|
120
|
-
})
|
|
121
|
-
.where(eq(moduleConfigs.id, existing.id))
|
|
122
|
-
.run();
|
|
123
|
-
} else {
|
|
124
|
-
// Insert new
|
|
125
|
-
this.db
|
|
126
|
-
.insert(moduleConfigs)
|
|
127
|
-
.values({
|
|
128
|
-
moduleId,
|
|
129
|
-
key,
|
|
130
|
-
value: isComplex ? '' : String(value),
|
|
131
|
-
valueJson: isComplex ? JSON.stringify(value) : null,
|
|
132
|
-
})
|
|
133
|
-
.run();
|
|
134
|
-
}
|
|
105
|
+
upsertModuleConfig(this.db, moduleId, key, value);
|
|
135
106
|
}
|
|
136
107
|
|
|
137
108
|
/**
|