@celilo/cli 0.1.4 → 0.1.6
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/0004_caddy_hostname_list.sql +25 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +9 -2
- package/src/ansible/inventory.test.ts +9 -8
- package/src/ansible/inventory.ts +9 -7
- package/src/capabilities/public-web-helpers.test.ts +2 -2
- package/src/capabilities/public-web-publish.test.ts +45 -12
- package/src/capabilities/registration.test.ts +6 -6
- package/src/capabilities/well-known.test.ts +2 -2
- package/src/capabilities/well-known.ts +5 -5
- package/src/cli/cli.test.ts +2 -2
- package/src/cli/command-registry.ts +146 -3
- package/src/cli/command-tree-parser.test.ts +1 -1
- package/src/cli/command-tree-parser.ts +9 -8
- package/src/cli/commands/hook-run.ts +15 -66
- package/src/cli/commands/module-audit.ts +14 -44
- package/src/cli/commands/module-deploy.ts +4 -1
- package/src/cli/commands/module-import-registry.test.ts +115 -0
- package/src/cli/commands/module-import.ts +106 -22
- package/src/cli/commands/module-publish.test.ts +235 -0
- package/src/cli/commands/module-publish.ts +234 -0
- package/src/cli/commands/module-remove.ts +82 -2
- package/src/cli/commands/module-search.ts +57 -0
- package/src/cli/commands/module-secret-get.ts +59 -0
- package/src/cli/commands/module-show.ts +1 -1
- package/src/cli/commands/module-terraform-unlock.ts +57 -0
- package/src/cli/commands/module-verify.test.ts +59 -0
- package/src/cli/commands/module-verify.ts +53 -0
- package/src/cli/commands/status.ts +30 -20
- package/src/cli/commands/system-audit.test.ts +138 -0
- package/src/cli/commands/system-audit.ts +571 -0
- package/src/cli/commands/system-update.ts +391 -0
- package/src/cli/completion.ts +15 -1
- package/src/cli/fuel-gauge.ts +68 -3
- package/src/cli/generate-zsh-completion.ts +13 -3
- package/src/cli/index.ts +112 -5
- package/src/cli/parser.ts +11 -0
- package/src/cli/prompts.ts +36 -5
- package/src/cli/tui/audit-state.test.ts +246 -0
- package/src/cli/tui/audit-state.ts +525 -0
- package/src/cli/tui/audit-tui.test.tsx +135 -0
- package/src/cli/tui/audit-tui.tsx +624 -0
- package/src/cli/tui/celebration.tsx +29 -0
- package/src/cli/tui/clipboard.test.ts +94 -0
- package/src/cli/tui/clipboard.ts +101 -0
- package/src/cli/tui/icons.ts +22 -0
- package/src/cli/tui/keybar.tsx +65 -0
- package/src/cli/tui/keymap.test.ts +105 -0
- package/src/cli/tui/keymap.ts +70 -0
- package/src/cli/tui/modals/analyzing.tsx +75 -0
- package/src/cli/tui/modals/celebration.tsx +44 -0
- package/src/cli/tui/modals/reaudit-prompt.tsx +35 -0
- package/src/cli/tui/modals/remediate.tsx +44 -0
- package/src/cli/tui/modals.test.ts +137 -0
- package/src/cli/tui/mouse.test.ts +78 -0
- package/src/cli/tui/mouse.ts +114 -0
- package/src/cli/tui/panes/categories.tsx +62 -0
- package/src/cli/tui/panes/command-log.tsx +87 -0
- package/src/cli/tui/panes/detail.tsx +175 -0
- package/src/cli/tui/panes/findings.tsx +97 -0
- package/src/cli/tui/panes/summary.tsx +64 -0
- package/src/cli/tui/spawn.ts +130 -0
- package/src/cli/tui/theme.ts +42 -0
- package/src/cli/tui/wrap.test.ts +43 -0
- package/src/cli/tui/wrap.ts +45 -0
- package/src/cli/types.ts +5 -0
- package/src/db/client.ts +55 -2
- package/src/db/schema.test.ts +3 -3
- package/src/db/schema.ts +26 -17
- package/src/hooks/capability-loader.ts +135 -72
- package/src/hooks/define-hook.test.ts +11 -3
- package/src/hooks/executor.ts +22 -1
- package/src/hooks/load-hook-config.test.ts +165 -0
- package/src/hooks/load-hook-config.ts +60 -0
- package/src/hooks/logger.ts +42 -12
- package/src/hooks/run-named-hook.ts +128 -0
- package/src/hooks/types.ts +19 -0
- package/src/manifest/ensure-schema.test.ts +115 -0
- package/src/manifest/schema.ts +76 -0
- package/src/manifest/template-validator.test.ts +1 -1
- package/src/manifest/template-validator.ts +1 -1
- package/src/manifest/validate.test.ts +1 -1
- package/src/module/import.ts +20 -12
- package/src/module/packaging/build.ts +121 -25
- package/src/module/packaging/release-metadata.test.ts +103 -0
- package/src/module/packaging/release-metadata.ts +145 -0
- package/src/registry/client.test.ts +228 -0
- package/src/registry/client.ts +157 -0
- package/src/services/audit/backups.test.ts +233 -0
- package/src/services/audit/backups.ts +128 -0
- package/src/services/audit/capability-abi.test.ts +153 -0
- package/src/services/audit/capability-abi.ts +204 -0
- package/src/services/audit/cli-version.test.ts +60 -0
- package/src/services/audit/cli-version.ts +87 -0
- package/src/services/audit/health.test.ts +84 -0
- package/src/services/audit/health.ts +43 -0
- package/src/services/audit/index.test.ts +99 -0
- package/src/services/audit/index.ts +118 -0
- package/src/services/audit/machines-reachable.test.ts +87 -0
- package/src/services/audit/machines-reachable.ts +87 -0
- package/src/services/audit/module-configs.test.ts +131 -0
- package/src/services/audit/module-configs.ts +80 -0
- package/src/services/audit/module-versions.test.ts +99 -0
- package/src/services/audit/module-versions.ts +154 -0
- package/src/services/audit/schema.test.ts +68 -0
- package/src/services/audit/schema.ts +115 -0
- package/src/services/audit/secrets-decryptable.test.ts +82 -0
- package/src/services/audit/secrets-decryptable.ts +97 -0
- package/src/services/audit/services-credentials.test.ts +54 -0
- package/src/services/audit/services-credentials.ts +64 -0
- package/src/services/audit/services-reachable.test.ts +60 -0
- package/src/services/audit/services-reachable.ts +64 -0
- package/src/services/audit/terraform-plan.test.ts +127 -0
- package/src/services/audit/terraform-plan.ts +153 -0
- package/src/services/audit/types.test.ts +36 -0
- package/src/services/audit/types.ts +90 -0
- package/src/services/audit/unconfigured-modules.test.ts +48 -0
- package/src/services/audit/unconfigured-modules.ts +71 -0
- package/src/services/audit/undeployed-modules.test.ts +66 -0
- package/src/services/audit/undeployed-modules.ts +72 -0
- package/src/services/build-stream.ts +122 -122
- package/src/services/config-interview.ts +407 -2
- package/src/services/deploy-ansible.ts +73 -7
- package/src/services/deploy-planner.ts +5 -5
- package/src/services/deploy-preflight.ts +45 -4
- package/src/services/deploy-terraform.ts +31 -24
- package/src/services/deploy-validation.ts +167 -23
- package/src/services/dns-auto-register.ts +4 -4
- package/src/services/ensure-interview.test.ts +245 -0
- package/src/services/health-runner.ts +110 -38
- package/src/services/infrastructure-variable-resolver.test.ts +1 -1
- package/src/services/infrastructure-variable-resolver.ts +3 -3
- package/src/services/module-build.ts +11 -13
- package/src/services/module-deploy.ts +372 -61
- package/src/services/proxmox-state-recovery.ts +6 -6
- package/src/services/ssh-key-manager.test.ts +1 -1
- package/src/services/ssh-key-manager.ts +3 -2
- package/src/services/terraform-env.ts +62 -0
- package/src/services/update/dep-graph.test.ts +214 -0
- package/src/services/update/dep-graph.ts +215 -0
- package/src/services/update/orchestrator.test.ts +463 -0
- package/src/services/update/orchestrator.ts +359 -0
- package/src/services/update/progress.ts +49 -0
- package/src/services/update/self-update.test.ts +68 -0
- package/src/services/update/self-update.ts +57 -0
- package/src/services/update/types.ts +94 -0
- package/src/templates/generator.test.ts +3 -3
- package/src/templates/generator.ts +43 -2
- package/src/test-utils/completion-harness.test.ts +1 -1
- package/src/test-utils/completion-harness.ts +4 -4
- package/src/variables/capability-self-ref.test.ts +203 -0
- package/src/variables/context.test.ts +31 -31
- package/src/variables/context.ts +65 -17
- package/src/variables/declarative-derivation.test.ts +306 -0
- package/src/variables/declarative-derivation.ts +4 -2
- package/src/variables/parser.test.ts +64 -9
- package/src/variables/parser.ts +47 -6
- package/src/variables/resolver.test.ts +14 -14
- package/src/variables/resolver.ts +27 -9
- package/src/variables/types.ts +1 -1
- package/tsconfig.json +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync, statSync } from 'node:fs';
|
|
3
|
-
import { chmod, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { chmod, cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join, relative } from 'node:path';
|
|
5
5
|
import { and, eq } from 'drizzle-orm';
|
|
6
6
|
import { generateInventory } from '../ansible/inventory';
|
|
@@ -209,6 +209,31 @@ export async function readTemplateFiles(
|
|
|
209
209
|
* @param outputPath - Base output path
|
|
210
210
|
* @param files - Generated files to write
|
|
211
211
|
*/
|
|
212
|
+
/**
|
|
213
|
+
* Copy each `ansible/roles/<role>/files/` directory from the module source
|
|
214
|
+
* to the generated output, verbatim (preserving binary content + mode bits).
|
|
215
|
+
*
|
|
216
|
+
* These hold Ansible role-local static assets — they may be binaries and
|
|
217
|
+
* don't need template variable resolution, so the standard template
|
|
218
|
+
* pipeline (which is utf-8) can't handle them.
|
|
219
|
+
*/
|
|
220
|
+
export async function copyAnsibleRoleFilesDirs(
|
|
221
|
+
modulePath: string,
|
|
222
|
+
outputPath: string,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
const rolesDir = join(modulePath, 'ansible', 'roles');
|
|
225
|
+
if (!existsSync(rolesDir)) return;
|
|
226
|
+
const roles = await readdir(rolesDir, { withFileTypes: true });
|
|
227
|
+
for (const role of roles) {
|
|
228
|
+
if (!role.isDirectory()) continue;
|
|
229
|
+
const srcFilesDir = join(rolesDir, role.name, 'files');
|
|
230
|
+
if (!existsSync(srcFilesDir)) continue;
|
|
231
|
+
const destFilesDir = join(outputPath, 'ansible', 'roles', role.name, 'files');
|
|
232
|
+
await mkdir(dirname(destFilesDir), { recursive: true });
|
|
233
|
+
await cp(srcFilesDir, destFilesDir, { recursive: true, preserveTimestamps: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
212
237
|
export async function writeGeneratedFiles(
|
|
213
238
|
outputPath: string,
|
|
214
239
|
files: GeneratedFile[],
|
|
@@ -621,7 +646,7 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
621
646
|
// biome-ignore lint/style/noNonNullAssertion: value is NOT NULL in schema, guaranteed to exist
|
|
622
647
|
context.selfConfig.vmid = String(vmidConfig.value!);
|
|
623
648
|
// biome-ignore lint/style/noNonNullAssertion: value is NOT NULL in schema, guaranteed to exist
|
|
624
|
-
context.selfConfig.
|
|
649
|
+
context.selfConfig.target_ip = ipConfig.value!;
|
|
625
650
|
}
|
|
626
651
|
|
|
627
652
|
// Add infrastructure properties to context (target_node, lxc_template, storage)
|
|
@@ -913,6 +938,22 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
913
938
|
};
|
|
914
939
|
}
|
|
915
940
|
|
|
941
|
+
// Execution: Copy Ansible role-local `files/` directories verbatim.
|
|
942
|
+
// These hold static assets (binaries, certs, scripts, blobs) used by
|
|
943
|
+
// Ansible's `copy` module. They're outside the template pipeline because
|
|
944
|
+
// (a) they don't need variable resolution and (b) they may be binary —
|
|
945
|
+
// utf-8 round-tripping would corrupt them. The template pipeline only
|
|
946
|
+
// handles .tpl/.j2/.yml/.yaml/.tf files.
|
|
947
|
+
try {
|
|
948
|
+
await copyAnsibleRoleFilesDirs(modulePath, outputPath);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
return {
|
|
951
|
+
success: false,
|
|
952
|
+
error: 'Failed to copy Ansible role files/ directories',
|
|
953
|
+
details: error,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
|
|
916
957
|
// Execution: Copy capability-provided Ansible tasks into generated output
|
|
917
958
|
// When a module requires capabilities that provide Ansible tasks (e.g., dns_registrar),
|
|
918
959
|
// copy those tasks so the consumer's playbook can include them.
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { expect } from 'bun:test';
|
|
9
|
-
import {
|
|
9
|
+
import type { CLIContext } from './cli-context';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Test harness for CLI completion
|
|
13
13
|
*/
|
|
14
14
|
export class CompletionHarness {
|
|
15
|
-
constructor(private cli:
|
|
15
|
+
constructor(private cli: CLIContext) {}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Get completions for a given set of words
|
|
@@ -26,10 +26,10 @@ export class CompletionHarness {
|
|
|
26
26
|
// If partial=false, we're completing the next word after all current words
|
|
27
27
|
const currentIndex = partial && words.length > 0 ? words.length - 1 : words.length;
|
|
28
28
|
try {
|
|
29
|
-
const result =
|
|
29
|
+
const result = await this.cli.run(`--get-completions ${words.join(' ')} ${currentIndex}`);
|
|
30
30
|
|
|
31
31
|
// Parse completion output (one per line)
|
|
32
|
-
return result.split('\n').filter(Boolean);
|
|
32
|
+
return result.stdout.split('\n').filter(Boolean);
|
|
33
33
|
} catch (_error) {
|
|
34
34
|
// Completion errors return empty array
|
|
35
35
|
return [];
|
|
@@ -9,6 +9,7 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
9
9
|
import { tmpdir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { createDbClient } from '@/db/client';
|
|
12
|
+
import { buildResolutionContext } from './context';
|
|
12
13
|
import { resolveVariable } from './resolver';
|
|
13
14
|
import type { ResolutionContext } from './types';
|
|
14
15
|
|
|
@@ -217,4 +218,206 @@ describe('Capability data $self: variable resolution', () => {
|
|
|
217
218
|
expect(result.error).toContain("has not configured 'ip.primary'");
|
|
218
219
|
}
|
|
219
220
|
});
|
|
221
|
+
|
|
222
|
+
test('buildResolutionContext resolves capability-derived variable even when DB has raw unresolved $self: value', async () => {
|
|
223
|
+
// Regression: hook config was assembled from raw DB reads without applying
|
|
224
|
+
// capability derivation. When lunacycle stores primary_domain as "$self:primary_domain"
|
|
225
|
+
// (because the DB guard skipped persisting the resolved value), the hook
|
|
226
|
+
// received the literal template string instead of "iamtheinternet.org".
|
|
227
|
+
// After the fix, buildResolutionContext always returns the resolved value in selfConfig.
|
|
228
|
+
const testDir = mkdtempSync(join(tmpdir(), 'test-cap-self-raw-'));
|
|
229
|
+
testDirs.push(testDir);
|
|
230
|
+
const db = createDbClient({ path: join(testDir, 'test.db') });
|
|
231
|
+
|
|
232
|
+
db.$client
|
|
233
|
+
.prepare(
|
|
234
|
+
'INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)',
|
|
235
|
+
)
|
|
236
|
+
.run(
|
|
237
|
+
'namecheap',
|
|
238
|
+
'Namecheap',
|
|
239
|
+
'1.0.0',
|
|
240
|
+
'/tmp/modules/namecheap',
|
|
241
|
+
'VERIFIED',
|
|
242
|
+
JSON.stringify({ id: 'namecheap', name: 'Namecheap', version: '1.0.0' }),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
db.$client
|
|
246
|
+
.prepare('INSERT INTO module_configs (module_id, key, value) VALUES (?, ?, ?)')
|
|
247
|
+
.run('namecheap', 'primary_domain', 'iamtheinternet.org');
|
|
248
|
+
|
|
249
|
+
db.$client
|
|
250
|
+
.prepare(
|
|
251
|
+
'INSERT INTO capabilities (module_id, capability_name, version, data) VALUES (?, ?, ?, ?)',
|
|
252
|
+
)
|
|
253
|
+
.run(
|
|
254
|
+
'namecheap',
|
|
255
|
+
'dns_registrar',
|
|
256
|
+
'2.0.0',
|
|
257
|
+
JSON.stringify({ primary_domain: '$self:primary_domain' }),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const consumerManifest = {
|
|
261
|
+
id: 'lunacycle',
|
|
262
|
+
name: 'LunaCycle',
|
|
263
|
+
version: '1.0.0',
|
|
264
|
+
celilo_contract: '1.0',
|
|
265
|
+
variables: {
|
|
266
|
+
owns: [
|
|
267
|
+
{
|
|
268
|
+
name: 'primary_domain',
|
|
269
|
+
type: 'string',
|
|
270
|
+
required: false,
|
|
271
|
+
source: 'capability',
|
|
272
|
+
derive_from: '$capability:dns_registrar.primary_domain',
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
db.$client
|
|
279
|
+
.prepare(
|
|
280
|
+
'INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)',
|
|
281
|
+
)
|
|
282
|
+
.run(
|
|
283
|
+
'lunacycle',
|
|
284
|
+
'LunaCycle',
|
|
285
|
+
'1.0.0',
|
|
286
|
+
'/tmp/modules/lunacycle',
|
|
287
|
+
'INSTALLED',
|
|
288
|
+
JSON.stringify(consumerManifest),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Simulate raw DB state: primary_domain stored as unresolved template string
|
|
292
|
+
db.$client
|
|
293
|
+
.prepare('INSERT INTO module_configs (module_id, key, value) VALUES (?, ?, ?)')
|
|
294
|
+
.run('lunacycle', 'primary_domain', '$self:primary_domain');
|
|
295
|
+
|
|
296
|
+
const context = await buildResolutionContext('lunacycle', db);
|
|
297
|
+
|
|
298
|
+
// selfConfig should have the resolved value, not the raw DB "$self:primary_domain"
|
|
299
|
+
expect(context.selfConfig.primary_domain).toBe('iamtheinternet.org');
|
|
300
|
+
|
|
301
|
+
// Simulate the hook installConfigMap merge logic from module-deploy.ts:
|
|
302
|
+
// Raw DB read produces the unresolved string; the fix overwrites it from context.selfConfig
|
|
303
|
+
const installConfigMap: Record<string, unknown> = { primary_domain: '$self:primary_domain' };
|
|
304
|
+
for (const [key, value] of Object.entries(context.selfConfig)) {
|
|
305
|
+
if (
|
|
306
|
+
!(key in installConfigMap) ||
|
|
307
|
+
(typeof installConfigMap[key] === 'string' &&
|
|
308
|
+
(installConfigMap[key] as string).startsWith('$'))
|
|
309
|
+
) {
|
|
310
|
+
installConfigMap[key] = value;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
expect(installConfigMap.primary_domain).toBe('iamtheinternet.org');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('hook config merge does not overwrite user-set values with capability-derived ones', async () => {
|
|
317
|
+
// If a user explicitly sets a value that happens to match a capability-derived variable,
|
|
318
|
+
// their value (which does NOT start with $) should be preserved.
|
|
319
|
+
const installConfigMap: Record<string, unknown> = {
|
|
320
|
+
primary_domain: 'my-custom-domain.com', // user-set, no $ prefix
|
|
321
|
+
other_key: '$self:unresolved', // raw template — should be overwritten
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const resolvedSelfConfig: Record<string, string> = {
|
|
325
|
+
primary_domain: 'iamtheinternet.org', // capability-derived value
|
|
326
|
+
other_key: 'resolved-value',
|
|
327
|
+
new_key: 'auto-derived',
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
for (const [key, value] of Object.entries(resolvedSelfConfig)) {
|
|
331
|
+
if (
|
|
332
|
+
!(key in installConfigMap) ||
|
|
333
|
+
(typeof installConfigMap[key] === 'string' &&
|
|
334
|
+
(installConfigMap[key] as string).startsWith('$'))
|
|
335
|
+
) {
|
|
336
|
+
installConfigMap[key] = value;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// User-set value preserved (no $ prefix → not overwritten)
|
|
341
|
+
expect(installConfigMap.primary_domain).toBe('my-custom-domain.com');
|
|
342
|
+
// Unresolved template overwritten
|
|
343
|
+
expect(installConfigMap.other_key).toBe('resolved-value');
|
|
344
|
+
// New keys from context added
|
|
345
|
+
expect(installConfigMap.new_key).toBe('auto-derived');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('buildResolutionContext resolves $self: refs in capability data for consumer module', async () => {
|
|
349
|
+
// Regression test: lunacycle's primary_domain (source: capability, derive_from:
|
|
350
|
+
// "$capability:dns_registrar.primary_domain") should resolve to the provider's
|
|
351
|
+
// actual configured value, not the raw "$self:primary_domain" template string.
|
|
352
|
+
const testDir = mkdtempSync(join(tmpdir(), 'test-cap-self-ctx-'));
|
|
353
|
+
testDirs.push(testDir);
|
|
354
|
+
const db = createDbClient({ path: join(testDir, 'test.db') });
|
|
355
|
+
|
|
356
|
+
// Provider module (namecheap) with primary_domain configured
|
|
357
|
+
db.$client
|
|
358
|
+
.prepare(
|
|
359
|
+
'INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)',
|
|
360
|
+
)
|
|
361
|
+
.run(
|
|
362
|
+
'namecheap',
|
|
363
|
+
'Namecheap',
|
|
364
|
+
'1.0.0',
|
|
365
|
+
'/tmp/modules/namecheap',
|
|
366
|
+
'VERIFIED',
|
|
367
|
+
JSON.stringify({ id: 'namecheap', name: 'Namecheap', version: '1.0.0' }),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
db.$client
|
|
371
|
+
.prepare('INSERT INTO module_configs (module_id, key, value) VALUES (?, ?, ?)')
|
|
372
|
+
.run('namecheap', 'primary_domain', 'iamtheinternet.org');
|
|
373
|
+
|
|
374
|
+
// Capability data stored with unresolved $self: reference (as registered at import time)
|
|
375
|
+
db.$client
|
|
376
|
+
.prepare(
|
|
377
|
+
'INSERT INTO capabilities (module_id, capability_name, version, data) VALUES (?, ?, ?, ?)',
|
|
378
|
+
)
|
|
379
|
+
.run(
|
|
380
|
+
'namecheap',
|
|
381
|
+
'dns_registrar',
|
|
382
|
+
'2.0.0',
|
|
383
|
+
JSON.stringify({ provider: 'namecheap', primary_domain: '$self:primary_domain' }),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Consumer module (lunacycle) with capability-derived primary_domain
|
|
387
|
+
const consumerManifest = {
|
|
388
|
+
id: 'lunacycle',
|
|
389
|
+
name: 'LunaCycle',
|
|
390
|
+
version: '1.0.0',
|
|
391
|
+
celilo_contract: '1.0',
|
|
392
|
+
variables: {
|
|
393
|
+
owns: [
|
|
394
|
+
{
|
|
395
|
+
name: 'primary_domain',
|
|
396
|
+
type: 'string',
|
|
397
|
+
required: false,
|
|
398
|
+
source: 'capability',
|
|
399
|
+
derive_from: '$capability:dns_registrar.primary_domain',
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
db.$client
|
|
406
|
+
.prepare(
|
|
407
|
+
'INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)',
|
|
408
|
+
)
|
|
409
|
+
.run(
|
|
410
|
+
'lunacycle',
|
|
411
|
+
'LunaCycle',
|
|
412
|
+
'1.0.0',
|
|
413
|
+
'/tmp/modules/lunacycle',
|
|
414
|
+
'INSTALLED',
|
|
415
|
+
JSON.stringify(consumerManifest),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const context = await buildResolutionContext('lunacycle', db);
|
|
419
|
+
|
|
420
|
+
// primary_domain should be resolved to the provider's actual value
|
|
421
|
+
expect(context.selfConfig.primary_domain).toBe('iamtheinternet.org');
|
|
422
|
+
});
|
|
220
423
|
});
|
|
@@ -166,7 +166,7 @@ describe('Variable Context', () => {
|
|
|
166
166
|
|
|
167
167
|
db.insert(moduleConfigs)
|
|
168
168
|
.values([
|
|
169
|
-
{ moduleId: 'homebridge', key: '
|
|
169
|
+
{ moduleId: 'homebridge', key: 'target_ip', value: '192.168.0.50' },
|
|
170
170
|
{ moduleId: 'homebridge', key: 'hostname', value: 'homebridge' },
|
|
171
171
|
])
|
|
172
172
|
.run();
|
|
@@ -174,7 +174,7 @@ describe('Variable Context', () => {
|
|
|
174
174
|
const context = await buildResolutionContext('homebridge', db);
|
|
175
175
|
|
|
176
176
|
expect(context.moduleId).toBe('homebridge');
|
|
177
|
-
expect(context.selfConfig.
|
|
177
|
+
expect(context.selfConfig.target_ip).toBe('192.168.0.50');
|
|
178
178
|
expect(context.selfConfig.hostname).toBe('homebridge');
|
|
179
179
|
// Auto-derived variables
|
|
180
180
|
expect(context.selfConfig['inventory.ansible_host']).toBe('192.168.0.50');
|
|
@@ -262,7 +262,7 @@ describe('Variable Context', () => {
|
|
|
262
262
|
);
|
|
263
263
|
|
|
264
264
|
db.insert(moduleConfigs)
|
|
265
|
-
.values({ moduleId: 'caddy', key: '
|
|
265
|
+
.values({ moduleId: 'caddy', key: 'target_ip', value: '10.0.20.10' })
|
|
266
266
|
.run();
|
|
267
267
|
|
|
268
268
|
db.insert(secrets)
|
|
@@ -290,7 +290,7 @@ describe('Variable Context', () => {
|
|
|
290
290
|
|
|
291
291
|
const context = await buildResolutionContext('caddy', db);
|
|
292
292
|
|
|
293
|
-
expect(context.selfConfig.
|
|
293
|
+
expect(context.selfConfig.target_ip).toBe('10.0.20.10');
|
|
294
294
|
expect(context.secrets.ssl_cert).toBe('cert_data');
|
|
295
295
|
expect(context.capabilities.dns_external).toBeDefined();
|
|
296
296
|
expect(context.systemConfig['dns.primary']).toBe('192.168.0.1');
|
|
@@ -303,7 +303,7 @@ describe('Variable Context', () => {
|
|
|
303
303
|
// Should have auto-derived inventory variables
|
|
304
304
|
expect(context.selfConfig['inventory.ansible_user']).toBe('root');
|
|
305
305
|
expect(context.selfConfig['inventory.groups']).toBe('empty-module');
|
|
306
|
-
// Should not have ansible_host (no
|
|
306
|
+
// Should not have ansible_host (no target_ip)
|
|
307
307
|
expect(context.selfConfig['inventory.ansible_host']).toBeUndefined();
|
|
308
308
|
expect(context.secrets).toEqual({});
|
|
309
309
|
expect(context.capabilities).toEqual({});
|
|
@@ -367,18 +367,18 @@ describe('Variable Context', () => {
|
|
|
367
367
|
expect(context.selfConfig['requires.machine.memory']).toBeUndefined();
|
|
368
368
|
});
|
|
369
369
|
|
|
370
|
-
test('should auto-derive inventory variables from
|
|
370
|
+
test('should auto-derive inventory variables from target_ip', async () => {
|
|
371
371
|
db.$client.run(
|
|
372
372
|
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('test-module', 'Test', '1.0.0', '/path', '{}')`,
|
|
373
373
|
);
|
|
374
374
|
|
|
375
375
|
db.insert(moduleConfigs)
|
|
376
|
-
.values({ moduleId: 'test-module', key: '
|
|
376
|
+
.values({ moduleId: 'test-module', key: 'target_ip', value: '10.0.10.10/24' })
|
|
377
377
|
.run();
|
|
378
378
|
|
|
379
379
|
const context = await buildResolutionContext('test-module', db);
|
|
380
380
|
|
|
381
|
-
// Should strip CIDR from
|
|
381
|
+
// Should strip CIDR from target_ip
|
|
382
382
|
expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.10.10');
|
|
383
383
|
expect(context.selfConfig['inventory.ansible_user']).toBe('root');
|
|
384
384
|
expect(context.selfConfig['inventory.groups']).toBe('test-module');
|
|
@@ -391,7 +391,7 @@ describe('Variable Context', () => {
|
|
|
391
391
|
|
|
392
392
|
db.insert(moduleConfigs)
|
|
393
393
|
.values([
|
|
394
|
-
{ moduleId: 'custom', key: '
|
|
394
|
+
{ moduleId: 'custom', key: 'target_ip', value: '10.0.20.10' },
|
|
395
395
|
{ moduleId: 'custom', key: 'inventory.ansible_user', value: 'admin' },
|
|
396
396
|
])
|
|
397
397
|
.run();
|
|
@@ -403,13 +403,13 @@ describe('Variable Context', () => {
|
|
|
403
403
|
expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.20.10');
|
|
404
404
|
});
|
|
405
405
|
|
|
406
|
-
test('should handle
|
|
406
|
+
test('should handle target_ip without CIDR', async () => {
|
|
407
407
|
db.$client.run(
|
|
408
408
|
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('no-cidr', 'No CIDR', '1.0.0', '/path', '{}')`,
|
|
409
409
|
);
|
|
410
410
|
|
|
411
411
|
db.insert(moduleConfigs)
|
|
412
|
-
.values({ moduleId: 'no-cidr', key: '
|
|
412
|
+
.values({ moduleId: 'no-cidr', key: 'target_ip', value: '192.168.1.100' })
|
|
413
413
|
.run();
|
|
414
414
|
|
|
415
415
|
const context = await buildResolutionContext('no-cidr', db);
|
|
@@ -418,8 +418,8 @@ describe('Variable Context', () => {
|
|
|
418
418
|
expect(context.selfConfig['inventory.ansible_host']).toBe('192.168.1.100');
|
|
419
419
|
});
|
|
420
420
|
|
|
421
|
-
test('should auto-allocate IPAM resources when module declares vmid and
|
|
422
|
-
// Create module with manifest that declares vmid and
|
|
421
|
+
test('should auto-allocate IPAM resources when module declares vmid and target_ip', async () => {
|
|
422
|
+
// Create module with manifest that declares vmid and target_ip
|
|
423
423
|
db.insert(modules)
|
|
424
424
|
.values({
|
|
425
425
|
id: 'auto-module',
|
|
@@ -433,7 +433,7 @@ describe('Variable Context', () => {
|
|
|
433
433
|
variables: {
|
|
434
434
|
owns: [
|
|
435
435
|
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
436
|
-
{ name: '
|
|
436
|
+
{ name: 'target_ip', type: 'string', required: true, source: 'user' },
|
|
437
437
|
],
|
|
438
438
|
},
|
|
439
439
|
requires: {
|
|
@@ -447,9 +447,9 @@ describe('Variable Context', () => {
|
|
|
447
447
|
|
|
448
448
|
const context = await buildResolutionContext('auto-module', db);
|
|
449
449
|
|
|
450
|
-
// Should have auto-allocated vmid and
|
|
450
|
+
// Should have auto-allocated vmid and target_ip
|
|
451
451
|
expect(context.selfConfig.vmid).toBe('2100');
|
|
452
|
-
expect(context.selfConfig.
|
|
452
|
+
expect(context.selfConfig.target_ip).toBe('10.0.10.10/24');
|
|
453
453
|
|
|
454
454
|
// Verify allocation persisted to database
|
|
455
455
|
const allocations = await db.select().from(ipAllocations).all();
|
|
@@ -465,7 +465,7 @@ describe('Variable Context', () => {
|
|
|
465
465
|
.where(eq(moduleConfigs.moduleId, 'auto-module'))
|
|
466
466
|
.all();
|
|
467
467
|
const vmidConfig = configs.find((c) => c.key === 'vmid');
|
|
468
|
-
const ipConfig = configs.find((c) => c.key === '
|
|
468
|
+
const ipConfig = configs.find((c) => c.key === 'target_ip');
|
|
469
469
|
expect(vmidConfig?.value).toBe('2100');
|
|
470
470
|
expect(ipConfig?.value).toBe('10.0.10.10/24');
|
|
471
471
|
});
|
|
@@ -485,7 +485,7 @@ describe('Variable Context', () => {
|
|
|
485
485
|
variables: {
|
|
486
486
|
owns: [
|
|
487
487
|
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
488
|
-
{ name: '
|
|
488
|
+
{ name: 'target_ip', type: 'string', required: true, source: 'user' },
|
|
489
489
|
],
|
|
490
490
|
},
|
|
491
491
|
},
|
|
@@ -506,14 +506,14 @@ describe('Variable Context', () => {
|
|
|
506
506
|
|
|
507
507
|
// Should reuse existing allocation
|
|
508
508
|
expect(context.selfConfig.vmid).toBe('2150');
|
|
509
|
-
expect(context.selfConfig.
|
|
509
|
+
expect(context.selfConfig.target_ip).toBe('10.0.10.50/24');
|
|
510
510
|
|
|
511
511
|
// Should not create duplicate allocation
|
|
512
512
|
const allocations = await db.select().from(ipAllocations).all();
|
|
513
513
|
expect(allocations).toHaveLength(1);
|
|
514
514
|
});
|
|
515
515
|
|
|
516
|
-
test('should skip IPAM allocation if vmid and
|
|
516
|
+
test('should skip IPAM allocation if vmid and target_ip already configured', async () => {
|
|
517
517
|
// Create module with existing config
|
|
518
518
|
db.insert(modules)
|
|
519
519
|
.values({
|
|
@@ -528,7 +528,7 @@ describe('Variable Context', () => {
|
|
|
528
528
|
variables: {
|
|
529
529
|
owns: [
|
|
530
530
|
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
531
|
-
{ name: '
|
|
531
|
+
{ name: 'target_ip', type: 'string', required: true, source: 'user' },
|
|
532
532
|
],
|
|
533
533
|
},
|
|
534
534
|
},
|
|
@@ -538,7 +538,7 @@ describe('Variable Context', () => {
|
|
|
538
538
|
db.insert(moduleConfigs)
|
|
539
539
|
.values([
|
|
540
540
|
{ moduleId: 'manual-config', key: 'vmid', value: '9999' },
|
|
541
|
-
{ moduleId: 'manual-config', key: '
|
|
541
|
+
{ moduleId: 'manual-config', key: 'target_ip', value: '192.168.99.99/24' },
|
|
542
542
|
])
|
|
543
543
|
.run();
|
|
544
544
|
|
|
@@ -546,15 +546,15 @@ describe('Variable Context', () => {
|
|
|
546
546
|
|
|
547
547
|
// Should use existing config
|
|
548
548
|
expect(context.selfConfig.vmid).toBe('9999');
|
|
549
|
-
expect(context.selfConfig.
|
|
549
|
+
expect(context.selfConfig.target_ip).toBe('192.168.99.99/24');
|
|
550
550
|
|
|
551
551
|
// Should not create allocation
|
|
552
552
|
const allocations = await db.select().from(ipAllocations).all();
|
|
553
553
|
expect(allocations).toHaveLength(0);
|
|
554
554
|
});
|
|
555
555
|
|
|
556
|
-
test('should skip IPAM allocation for VPS modules without
|
|
557
|
-
// Create VPS module (no
|
|
556
|
+
test('should skip IPAM allocation for VPS modules without target_ip', async () => {
|
|
557
|
+
// Create VPS module (no target_ip variable)
|
|
558
558
|
db.insert(modules)
|
|
559
559
|
.values({
|
|
560
560
|
id: 'vps-module',
|
|
@@ -576,7 +576,7 @@ describe('Variable Context', () => {
|
|
|
576
576
|
|
|
577
577
|
// Should not allocate IPAM resources
|
|
578
578
|
expect(context.selfConfig.vmid).toBeUndefined();
|
|
579
|
-
expect(context.selfConfig.
|
|
579
|
+
expect(context.selfConfig.target_ip).toBeUndefined();
|
|
580
580
|
|
|
581
581
|
const allocations = await db.select().from(ipAllocations).all();
|
|
582
582
|
expect(allocations).toHaveLength(0);
|
|
@@ -597,7 +597,7 @@ describe('Variable Context', () => {
|
|
|
597
597
|
variables: {
|
|
598
598
|
owns: [
|
|
599
599
|
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
600
|
-
{ name: '
|
|
600
|
+
{ name: 'target_ip', type: 'string', required: true, source: 'user' },
|
|
601
601
|
],
|
|
602
602
|
},
|
|
603
603
|
},
|
|
@@ -618,7 +618,7 @@ describe('Variable Context', () => {
|
|
|
618
618
|
variables: {
|
|
619
619
|
owns: [
|
|
620
620
|
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
621
|
-
{ name: '
|
|
621
|
+
{ name: 'target_ip', type: 'string', required: true, source: 'user' },
|
|
622
622
|
],
|
|
623
623
|
},
|
|
624
624
|
},
|
|
@@ -634,8 +634,8 @@ describe('Variable Context', () => {
|
|
|
634
634
|
expect(context2.selfConfig.vmid).toBe('2101');
|
|
635
635
|
|
|
636
636
|
// Should have sequential IPs
|
|
637
|
-
expect(context1.selfConfig.
|
|
638
|
-
expect(context2.selfConfig.
|
|
637
|
+
expect(context1.selfConfig.target_ip).toBe('10.0.10.10/24');
|
|
638
|
+
expect(context2.selfConfig.target_ip).toBe('10.0.10.11/24');
|
|
639
639
|
|
|
640
640
|
const allocations = await db.select().from(ipAllocations).all();
|
|
641
641
|
expect(allocations).toHaveLength(2);
|
|
@@ -1254,7 +1254,7 @@ describe('Variable Context', () => {
|
|
|
1254
1254
|
|
|
1255
1255
|
test('should auto-derive inventory variables in buildContextFromData', () => {
|
|
1256
1256
|
const context = buildContextFromData('my-module', {
|
|
1257
|
-
selfConfig: {
|
|
1257
|
+
selfConfig: { target_ip: '10.0.30.15/24', hostname: 'my-host' },
|
|
1258
1258
|
});
|
|
1259
1259
|
|
|
1260
1260
|
expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.30.15');
|