@celilo/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,1265 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { rm } from 'node:fs/promises';
|
|
4
|
+
import { eq } from 'drizzle-orm';
|
|
5
|
+
import { type DbClient, createDbClient } from '../db/client';
|
|
6
|
+
import {
|
|
7
|
+
capabilities,
|
|
8
|
+
ipAllocations,
|
|
9
|
+
moduleConfigs,
|
|
10
|
+
modules,
|
|
11
|
+
secrets,
|
|
12
|
+
systemConfig,
|
|
13
|
+
} from '../db/schema';
|
|
14
|
+
import { buildContextFromData, buildResolutionContext } from './context';
|
|
15
|
+
|
|
16
|
+
const TEST_DB_PATH = './test-context.db';
|
|
17
|
+
|
|
18
|
+
describe('Variable Context', () => {
|
|
19
|
+
let db: DbClient;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
db = createDbClient({ path: TEST_DB_PATH });
|
|
23
|
+
|
|
24
|
+
// Create tables
|
|
25
|
+
db.$client.run(`
|
|
26
|
+
CREATE TABLE IF NOT EXISTS modules (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
name TEXT NOT NULL,
|
|
29
|
+
version TEXT NOT NULL,
|
|
30
|
+
description TEXT,
|
|
31
|
+
state TEXT NOT NULL DEFAULT 'IMPORTED',
|
|
32
|
+
manifest_data TEXT NOT NULL,
|
|
33
|
+
source_path TEXT NOT NULL,
|
|
34
|
+
imported_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
35
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
36
|
+
error_message TEXT
|
|
37
|
+
)
|
|
38
|
+
`);
|
|
39
|
+
|
|
40
|
+
db.$client.run(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS module_configs (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
module_id TEXT NOT NULL,
|
|
44
|
+
key TEXT NOT NULL,
|
|
45
|
+
value TEXT NOT NULL,
|
|
46
|
+
value_json TEXT,
|
|
47
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
48
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
49
|
+
UNIQUE(module_id, key),
|
|
50
|
+
FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
|
|
51
|
+
)
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
db.$client.run(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS capabilities (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
module_id TEXT NOT NULL,
|
|
58
|
+
capability_name TEXT NOT NULL,
|
|
59
|
+
version TEXT NOT NULL,
|
|
60
|
+
data TEXT NOT NULL,
|
|
61
|
+
registered_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
62
|
+
FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
|
|
63
|
+
)
|
|
64
|
+
`);
|
|
65
|
+
|
|
66
|
+
db.$client.run(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS secrets (
|
|
68
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
+
module_id TEXT NOT NULL,
|
|
70
|
+
name TEXT NOT NULL,
|
|
71
|
+
encrypted_value TEXT NOT NULL,
|
|
72
|
+
iv TEXT NOT NULL,
|
|
73
|
+
auth_tag TEXT NOT NULL,
|
|
74
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
75
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
76
|
+
FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
db.$client.run(`
|
|
81
|
+
CREATE TABLE IF NOT EXISTS system_config (
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
key TEXT NOT NULL UNIQUE,
|
|
84
|
+
value TEXT NOT NULL,
|
|
85
|
+
description TEXT,
|
|
86
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
87
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
88
|
+
)
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
db.$client.run(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS ip_allocations (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
module_id TEXT NOT NULL,
|
|
95
|
+
vmid INTEGER NOT NULL UNIQUE,
|
|
96
|
+
container_ip TEXT NOT NULL UNIQUE,
|
|
97
|
+
zone TEXT NOT NULL,
|
|
98
|
+
allocated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
99
|
+
FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
|
|
100
|
+
)
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
db.$client.run(`
|
|
104
|
+
CREATE TABLE IF NOT EXISTS vmid_reservations (
|
|
105
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
106
|
+
vmid INTEGER NOT NULL UNIQUE,
|
|
107
|
+
reason TEXT NOT NULL,
|
|
108
|
+
reserved_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
109
|
+
)
|
|
110
|
+
`);
|
|
111
|
+
|
|
112
|
+
db.$client.run(`
|
|
113
|
+
CREATE TABLE IF NOT EXISTS ip_reservations (
|
|
114
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
115
|
+
ip_start TEXT NOT NULL,
|
|
116
|
+
ip_end TEXT,
|
|
117
|
+
zone TEXT NOT NULL,
|
|
118
|
+
reason TEXT NOT NULL,
|
|
119
|
+
reserved_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
120
|
+
)
|
|
121
|
+
`);
|
|
122
|
+
|
|
123
|
+
// Insert network config for DMZ zone (required for IPAM allocation)
|
|
124
|
+
db.insert(systemConfig)
|
|
125
|
+
.values([
|
|
126
|
+
{ key: 'network.dmz.subnet', value: '10.0.10.0/24' },
|
|
127
|
+
{ key: 'network.dmz.gateway', value: '10.0.10.1' },
|
|
128
|
+
{ key: 'network.dmz.vlan', value: '10' },
|
|
129
|
+
{ key: 'network.dmz.bridge', value: 'vmbr0' },
|
|
130
|
+
// App zone config
|
|
131
|
+
{ key: 'network.app.subnet', value: '10.0.20.0/24' },
|
|
132
|
+
{ key: 'network.app.gateway', value: '10.0.20.1' },
|
|
133
|
+
{ key: 'network.app.vlan', value: '20' },
|
|
134
|
+
{ key: 'network.app.bridge', value: 'vmbr0' },
|
|
135
|
+
// Secure zone config
|
|
136
|
+
{ key: 'network.secure.subnet', value: '10.0.30.0/24' },
|
|
137
|
+
{ key: 'network.secure.gateway', value: '10.0.30.1' },
|
|
138
|
+
{ key: 'network.secure.vlan', value: '30' },
|
|
139
|
+
{ key: 'network.secure.bridge', value: 'vmbr0' },
|
|
140
|
+
])
|
|
141
|
+
.run();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
afterEach(async () => {
|
|
145
|
+
db.$client.close();
|
|
146
|
+
|
|
147
|
+
if (existsSync(TEST_DB_PATH)) {
|
|
148
|
+
await rm(TEST_DB_PATH);
|
|
149
|
+
}
|
|
150
|
+
const walPath = `${TEST_DB_PATH}-wal`;
|
|
151
|
+
const shmPath = `${TEST_DB_PATH}-shm`;
|
|
152
|
+
if (existsSync(walPath)) {
|
|
153
|
+
await rm(walPath);
|
|
154
|
+
}
|
|
155
|
+
if (existsSync(shmPath)) {
|
|
156
|
+
await rm(shmPath);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('buildResolutionContext', () => {
|
|
161
|
+
test('should build context with module configs', async () => {
|
|
162
|
+
// Insert module first (for foreign key)
|
|
163
|
+
db.$client.run(
|
|
164
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('homebridge', 'Homebridge', '1.0.0', '/path', '{}')`,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
db.insert(moduleConfigs)
|
|
168
|
+
.values([
|
|
169
|
+
{ moduleId: 'homebridge', key: 'container_ip', value: '192.168.0.50' },
|
|
170
|
+
{ moduleId: 'homebridge', key: 'hostname', value: 'homebridge' },
|
|
171
|
+
])
|
|
172
|
+
.run();
|
|
173
|
+
|
|
174
|
+
const context = await buildResolutionContext('homebridge', db);
|
|
175
|
+
|
|
176
|
+
expect(context.moduleId).toBe('homebridge');
|
|
177
|
+
expect(context.selfConfig.container_ip).toBe('192.168.0.50');
|
|
178
|
+
expect(context.selfConfig.hostname).toBe('homebridge');
|
|
179
|
+
// Auto-derived variables
|
|
180
|
+
expect(context.selfConfig['inventory.ansible_host']).toBe('192.168.0.50');
|
|
181
|
+
expect(context.selfConfig['inventory.ansible_user']).toBe('root');
|
|
182
|
+
expect(context.selfConfig['inventory.groups']).toBe('homebridge');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('should build context with secrets', async () => {
|
|
186
|
+
db.$client.run(
|
|
187
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('homebridge', 'Homebridge', '1.0.0', '/path', '{}')`,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
db.insert(secrets)
|
|
191
|
+
.values([
|
|
192
|
+
{
|
|
193
|
+
moduleId: 'homebridge',
|
|
194
|
+
name: 'api_key',
|
|
195
|
+
encryptedValue: 'secret123',
|
|
196
|
+
iv: 'iv',
|
|
197
|
+
authTag: 'tag',
|
|
198
|
+
},
|
|
199
|
+
])
|
|
200
|
+
.run();
|
|
201
|
+
|
|
202
|
+
const context = await buildResolutionContext('homebridge', db);
|
|
203
|
+
|
|
204
|
+
expect(context.secrets).toEqual({
|
|
205
|
+
api_key: 'secret123',
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('should build context with capabilities', async () => {
|
|
210
|
+
db.$client.run(
|
|
211
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('dns-external', 'DNS', '1.0.0', '/path', '{}')`,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
db.insert(capabilities)
|
|
215
|
+
.values([
|
|
216
|
+
{
|
|
217
|
+
moduleId: 'dns-external',
|
|
218
|
+
capabilityName: 'dns_external',
|
|
219
|
+
version: '1.0.0',
|
|
220
|
+
data: {
|
|
221
|
+
nameserver: 'ns1.example.com',
|
|
222
|
+
zone: 'example.com',
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
])
|
|
226
|
+
.run();
|
|
227
|
+
|
|
228
|
+
const context = await buildResolutionContext('homebridge', db);
|
|
229
|
+
|
|
230
|
+
expect(context.capabilities).toEqual({
|
|
231
|
+
dns_external: {
|
|
232
|
+
nameserver: 'ns1.example.com',
|
|
233
|
+
zone: 'example.com',
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('should load system config from database', async () => {
|
|
239
|
+
db.$client.run(
|
|
240
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('homebridge', 'Homebridge', '1.0.0', '/path', '{}')`,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
db.$client.run(
|
|
244
|
+
`INSERT INTO system_config (key, value) VALUES ('dns.primary', '192.168.0.1')`,
|
|
245
|
+
);
|
|
246
|
+
db.$client.run(
|
|
247
|
+
`INSERT INTO system_config (key, value) VALUES ('network.domain', 'homelab.local')`,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const context = await buildResolutionContext('homebridge', db);
|
|
251
|
+
|
|
252
|
+
expect(context.systemConfig['dns.primary']).toBe('192.168.0.1');
|
|
253
|
+
expect(context.systemConfig['network.domain']).toBe('homelab.local');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('should build context with all data sources', async () => {
|
|
257
|
+
db.$client.run(
|
|
258
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('caddy', 'Caddy', '1.0.0', '/path', '{}')`,
|
|
259
|
+
);
|
|
260
|
+
db.$client.run(
|
|
261
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('dns-external', 'DNS', '1.0.0', '/path', '{}')`,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
db.insert(moduleConfigs)
|
|
265
|
+
.values({ moduleId: 'caddy', key: 'container_ip', value: '10.0.20.10' })
|
|
266
|
+
.run();
|
|
267
|
+
|
|
268
|
+
db.insert(secrets)
|
|
269
|
+
.values({
|
|
270
|
+
moduleId: 'caddy',
|
|
271
|
+
name: 'ssl_cert',
|
|
272
|
+
encryptedValue: 'cert_data',
|
|
273
|
+
iv: 'iv',
|
|
274
|
+
authTag: 'tag',
|
|
275
|
+
})
|
|
276
|
+
.run();
|
|
277
|
+
|
|
278
|
+
db.insert(capabilities)
|
|
279
|
+
.values({
|
|
280
|
+
moduleId: 'dns-external',
|
|
281
|
+
capabilityName: 'dns_external',
|
|
282
|
+
version: '1.0.0',
|
|
283
|
+
data: { nameserver: 'ns1.example.com' },
|
|
284
|
+
})
|
|
285
|
+
.run();
|
|
286
|
+
|
|
287
|
+
db.$client.run(
|
|
288
|
+
`INSERT INTO system_config (key, value) VALUES ('dns.primary', '192.168.0.1')`,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const context = await buildResolutionContext('caddy', db);
|
|
292
|
+
|
|
293
|
+
expect(context.selfConfig.container_ip).toBe('10.0.20.10');
|
|
294
|
+
expect(context.secrets.ssl_cert).toBe('cert_data');
|
|
295
|
+
expect(context.capabilities.dns_external).toBeDefined();
|
|
296
|
+
expect(context.systemConfig['dns.primary']).toBe('192.168.0.1');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('should return empty maps for module with no data', async () => {
|
|
300
|
+
const context = await buildResolutionContext('empty-module', db);
|
|
301
|
+
|
|
302
|
+
expect(context.moduleId).toBe('empty-module');
|
|
303
|
+
// Should have auto-derived inventory variables
|
|
304
|
+
expect(context.selfConfig['inventory.ansible_user']).toBe('root');
|
|
305
|
+
expect(context.selfConfig['inventory.groups']).toBe('empty-module');
|
|
306
|
+
// Should not have ansible_host (no container_ip)
|
|
307
|
+
expect(context.selfConfig['inventory.ansible_host']).toBeUndefined();
|
|
308
|
+
expect(context.secrets).toEqual({});
|
|
309
|
+
expect(context.capabilities).toEqual({});
|
|
310
|
+
expect(context.systemConfig).toBeDefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('should include VM resources from manifest', async () => {
|
|
314
|
+
// Create module with VM resources
|
|
315
|
+
db.insert(modules)
|
|
316
|
+
.values({
|
|
317
|
+
id: 'grafana',
|
|
318
|
+
name: 'Grafana',
|
|
319
|
+
version: '1.0.0',
|
|
320
|
+
sourcePath: '/test/grafana',
|
|
321
|
+
manifestData: {
|
|
322
|
+
id: 'grafana',
|
|
323
|
+
name: 'Grafana',
|
|
324
|
+
version: '1.0.0',
|
|
325
|
+
requires: {
|
|
326
|
+
machine: {
|
|
327
|
+
cpu: 2,
|
|
328
|
+
memory: 2048,
|
|
329
|
+
disk: 20,
|
|
330
|
+
storage: 'local-lvm',
|
|
331
|
+
zone: 'app',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
.run();
|
|
337
|
+
|
|
338
|
+
const context = await buildResolutionContext('grafana', db);
|
|
339
|
+
|
|
340
|
+
expect(context.selfConfig['requires.machine.cpu']).toBe('2');
|
|
341
|
+
expect(context.selfConfig['requires.machine.memory']).toBe('2048');
|
|
342
|
+
expect(context.selfConfig['requires.machine.disk']).toBe('20');
|
|
343
|
+
expect(context.selfConfig['requires.machine.storage']).toBe('local-lvm');
|
|
344
|
+
expect(context.selfConfig['requires.machine.zone']).toBe('app');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('should handle module without VM resources', async () => {
|
|
348
|
+
// Create module without VM resources
|
|
349
|
+
db.insert(modules)
|
|
350
|
+
.values({
|
|
351
|
+
id: 'simple',
|
|
352
|
+
name: 'Simple Module',
|
|
353
|
+
version: '1.0.0',
|
|
354
|
+
sourcePath: '/test/simple',
|
|
355
|
+
manifestData: {
|
|
356
|
+
id: 'simple',
|
|
357
|
+
name: 'Simple Module',
|
|
358
|
+
version: '1.0.0',
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
.run();
|
|
362
|
+
|
|
363
|
+
const context = await buildResolutionContext('simple', db);
|
|
364
|
+
|
|
365
|
+
// Should not have requires.machine keys
|
|
366
|
+
expect(context.selfConfig['requires.machine.cpu']).toBeUndefined();
|
|
367
|
+
expect(context.selfConfig['requires.machine.memory']).toBeUndefined();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test('should auto-derive inventory variables from container_ip', async () => {
|
|
371
|
+
db.$client.run(
|
|
372
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('test-module', 'Test', '1.0.0', '/path', '{}')`,
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
db.insert(moduleConfigs)
|
|
376
|
+
.values({ moduleId: 'test-module', key: 'container_ip', value: '10.0.10.10/24' })
|
|
377
|
+
.run();
|
|
378
|
+
|
|
379
|
+
const context = await buildResolutionContext('test-module', db);
|
|
380
|
+
|
|
381
|
+
// Should strip CIDR from container_ip
|
|
382
|
+
expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.10.10');
|
|
383
|
+
expect(context.selfConfig['inventory.ansible_user']).toBe('root');
|
|
384
|
+
expect(context.selfConfig['inventory.groups']).toBe('test-module');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('should allow overriding ansible_user', async () => {
|
|
388
|
+
db.$client.run(
|
|
389
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('custom', 'Custom', '1.0.0', '/path', '{}')`,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
db.insert(moduleConfigs)
|
|
393
|
+
.values([
|
|
394
|
+
{ moduleId: 'custom', key: 'container_ip', value: '10.0.20.10' },
|
|
395
|
+
{ moduleId: 'custom', key: 'inventory.ansible_user', value: 'admin' },
|
|
396
|
+
])
|
|
397
|
+
.run();
|
|
398
|
+
|
|
399
|
+
const context = await buildResolutionContext('custom', db);
|
|
400
|
+
|
|
401
|
+
// Explicit config should override default
|
|
402
|
+
expect(context.selfConfig['inventory.ansible_user']).toBe('admin');
|
|
403
|
+
expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.20.10');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('should handle container_ip without CIDR', async () => {
|
|
407
|
+
db.$client.run(
|
|
408
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('no-cidr', 'No CIDR', '1.0.0', '/path', '{}')`,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
db.insert(moduleConfigs)
|
|
412
|
+
.values({ moduleId: 'no-cidr', key: 'container_ip', value: '192.168.1.100' })
|
|
413
|
+
.run();
|
|
414
|
+
|
|
415
|
+
const context = await buildResolutionContext('no-cidr', db);
|
|
416
|
+
|
|
417
|
+
// Should work without CIDR notation
|
|
418
|
+
expect(context.selfConfig['inventory.ansible_host']).toBe('192.168.1.100');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('should auto-allocate IPAM resources when module declares vmid and container_ip', async () => {
|
|
422
|
+
// Create module with manifest that declares vmid and container_ip
|
|
423
|
+
db.insert(modules)
|
|
424
|
+
.values({
|
|
425
|
+
id: 'auto-module',
|
|
426
|
+
name: 'Auto Module',
|
|
427
|
+
version: '1.0.0',
|
|
428
|
+
sourcePath: '/test/auto',
|
|
429
|
+
manifestData: {
|
|
430
|
+
id: 'auto-module',
|
|
431
|
+
name: 'Auto Module',
|
|
432
|
+
version: '1.0.0',
|
|
433
|
+
variables: {
|
|
434
|
+
owns: [
|
|
435
|
+
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
436
|
+
{ name: 'container_ip', type: 'string', required: true, source: 'user' },
|
|
437
|
+
],
|
|
438
|
+
},
|
|
439
|
+
requires: {
|
|
440
|
+
machine: {
|
|
441
|
+
zone: 'dmz',
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
})
|
|
446
|
+
.run();
|
|
447
|
+
|
|
448
|
+
const context = await buildResolutionContext('auto-module', db);
|
|
449
|
+
|
|
450
|
+
// Should have auto-allocated vmid and container_ip
|
|
451
|
+
expect(context.selfConfig.vmid).toBe('2100');
|
|
452
|
+
expect(context.selfConfig.container_ip).toBe('10.0.10.10/24');
|
|
453
|
+
|
|
454
|
+
// Verify allocation persisted to database
|
|
455
|
+
const allocations = await db.select().from(ipAllocations).all();
|
|
456
|
+
expect(allocations).toHaveLength(1);
|
|
457
|
+
expect(allocations[0].moduleId).toBe('auto-module');
|
|
458
|
+
expect(allocations[0].vmid).toBe(2100);
|
|
459
|
+
expect(allocations[0].containerIp).toBe('10.0.10.10/24');
|
|
460
|
+
|
|
461
|
+
// Verify config persisted to database
|
|
462
|
+
const configs = await db
|
|
463
|
+
.select()
|
|
464
|
+
.from(moduleConfigs)
|
|
465
|
+
.where(eq(moduleConfigs.moduleId, 'auto-module'))
|
|
466
|
+
.all();
|
|
467
|
+
const vmidConfig = configs.find((c) => c.key === 'vmid');
|
|
468
|
+
const ipConfig = configs.find((c) => c.key === 'container_ip');
|
|
469
|
+
expect(vmidConfig?.value).toBe('2100');
|
|
470
|
+
expect(ipConfig?.value).toBe('10.0.10.10/24');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test('should reuse existing allocation if already allocated', async () => {
|
|
474
|
+
// Create module
|
|
475
|
+
db.insert(modules)
|
|
476
|
+
.values({
|
|
477
|
+
id: 'existing-alloc',
|
|
478
|
+
name: 'Existing Allocation',
|
|
479
|
+
version: '1.0.0',
|
|
480
|
+
sourcePath: '/test/existing',
|
|
481
|
+
manifestData: {
|
|
482
|
+
id: 'existing-alloc',
|
|
483
|
+
name: 'Existing Allocation',
|
|
484
|
+
version: '1.0.0',
|
|
485
|
+
variables: {
|
|
486
|
+
owns: [
|
|
487
|
+
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
488
|
+
{ name: 'container_ip', type: 'string', required: true, source: 'user' },
|
|
489
|
+
],
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
})
|
|
493
|
+
.run();
|
|
494
|
+
|
|
495
|
+
// Pre-create allocation
|
|
496
|
+
db.insert(ipAllocations)
|
|
497
|
+
.values({
|
|
498
|
+
moduleId: 'existing-alloc',
|
|
499
|
+
vmid: 2150,
|
|
500
|
+
containerIp: '10.0.10.50/24',
|
|
501
|
+
zone: 'dmz',
|
|
502
|
+
})
|
|
503
|
+
.run();
|
|
504
|
+
|
|
505
|
+
const context = await buildResolutionContext('existing-alloc', db);
|
|
506
|
+
|
|
507
|
+
// Should reuse existing allocation
|
|
508
|
+
expect(context.selfConfig.vmid).toBe('2150');
|
|
509
|
+
expect(context.selfConfig.container_ip).toBe('10.0.10.50/24');
|
|
510
|
+
|
|
511
|
+
// Should not create duplicate allocation
|
|
512
|
+
const allocations = await db.select().from(ipAllocations).all();
|
|
513
|
+
expect(allocations).toHaveLength(1);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('should skip IPAM allocation if vmid and container_ip already configured', async () => {
|
|
517
|
+
// Create module with existing config
|
|
518
|
+
db.insert(modules)
|
|
519
|
+
.values({
|
|
520
|
+
id: 'manual-config',
|
|
521
|
+
name: 'Manual Config',
|
|
522
|
+
version: '1.0.0',
|
|
523
|
+
sourcePath: '/test/manual',
|
|
524
|
+
manifestData: {
|
|
525
|
+
id: 'manual-config',
|
|
526
|
+
name: 'Manual Config',
|
|
527
|
+
version: '1.0.0',
|
|
528
|
+
variables: {
|
|
529
|
+
owns: [
|
|
530
|
+
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
531
|
+
{ name: 'container_ip', type: 'string', required: true, source: 'user' },
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
})
|
|
536
|
+
.run();
|
|
537
|
+
|
|
538
|
+
db.insert(moduleConfigs)
|
|
539
|
+
.values([
|
|
540
|
+
{ moduleId: 'manual-config', key: 'vmid', value: '9999' },
|
|
541
|
+
{ moduleId: 'manual-config', key: 'container_ip', value: '192.168.99.99/24' },
|
|
542
|
+
])
|
|
543
|
+
.run();
|
|
544
|
+
|
|
545
|
+
const context = await buildResolutionContext('manual-config', db);
|
|
546
|
+
|
|
547
|
+
// Should use existing config
|
|
548
|
+
expect(context.selfConfig.vmid).toBe('9999');
|
|
549
|
+
expect(context.selfConfig.container_ip).toBe('192.168.99.99/24');
|
|
550
|
+
|
|
551
|
+
// Should not create allocation
|
|
552
|
+
const allocations = await db.select().from(ipAllocations).all();
|
|
553
|
+
expect(allocations).toHaveLength(0);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test('should skip IPAM allocation for VPS modules without container_ip', async () => {
|
|
557
|
+
// Create VPS module (no container_ip variable)
|
|
558
|
+
db.insert(modules)
|
|
559
|
+
.values({
|
|
560
|
+
id: 'vps-module',
|
|
561
|
+
name: 'VPS Module',
|
|
562
|
+
version: '1.0.0',
|
|
563
|
+
sourcePath: '/test/vps',
|
|
564
|
+
manifestData: {
|
|
565
|
+
id: 'vps-module',
|
|
566
|
+
name: 'VPS Module',
|
|
567
|
+
version: '1.0.0',
|
|
568
|
+
variables: {
|
|
569
|
+
owns: [{ name: 'vps_ip', type: 'string', required: true, source: 'user' }],
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
})
|
|
573
|
+
.run();
|
|
574
|
+
|
|
575
|
+
const context = await buildResolutionContext('vps-module', db);
|
|
576
|
+
|
|
577
|
+
// Should not allocate IPAM resources
|
|
578
|
+
expect(context.selfConfig.vmid).toBeUndefined();
|
|
579
|
+
expect(context.selfConfig.container_ip).toBeUndefined();
|
|
580
|
+
|
|
581
|
+
const allocations = await db.select().from(ipAllocations).all();
|
|
582
|
+
expect(allocations).toHaveLength(0);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('should allocate sequential VMIDs for multiple modules', async () => {
|
|
586
|
+
// Create first module
|
|
587
|
+
db.insert(modules)
|
|
588
|
+
.values({
|
|
589
|
+
id: 'module1',
|
|
590
|
+
name: 'Module 1',
|
|
591
|
+
version: '1.0.0',
|
|
592
|
+
sourcePath: '/test/p1',
|
|
593
|
+
manifestData: {
|
|
594
|
+
id: 'module1',
|
|
595
|
+
name: 'Module 1',
|
|
596
|
+
version: '1.0.0',
|
|
597
|
+
variables: {
|
|
598
|
+
owns: [
|
|
599
|
+
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
600
|
+
{ name: 'container_ip', type: 'string', required: true, source: 'user' },
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
})
|
|
605
|
+
.run();
|
|
606
|
+
|
|
607
|
+
// Create second module
|
|
608
|
+
db.insert(modules)
|
|
609
|
+
.values({
|
|
610
|
+
id: 'module2',
|
|
611
|
+
name: 'Module 2',
|
|
612
|
+
version: '1.0.0',
|
|
613
|
+
sourcePath: '/test/p2',
|
|
614
|
+
manifestData: {
|
|
615
|
+
id: 'module2',
|
|
616
|
+
name: 'Module 2',
|
|
617
|
+
version: '1.0.0',
|
|
618
|
+
variables: {
|
|
619
|
+
owns: [
|
|
620
|
+
{ name: 'vmid', type: 'integer', required: true, source: 'user' },
|
|
621
|
+
{ name: 'container_ip', type: 'string', required: true, source: 'user' },
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
})
|
|
626
|
+
.run();
|
|
627
|
+
|
|
628
|
+
// Allocate for both
|
|
629
|
+
const context1 = await buildResolutionContext('module1', db);
|
|
630
|
+
const context2 = await buildResolutionContext('module2', db);
|
|
631
|
+
|
|
632
|
+
// Should have sequential VMIDs
|
|
633
|
+
expect(context1.selfConfig.vmid).toBe('2100');
|
|
634
|
+
expect(context2.selfConfig.vmid).toBe('2101');
|
|
635
|
+
|
|
636
|
+
// Should have sequential IPs
|
|
637
|
+
expect(context1.selfConfig.container_ip).toBe('10.0.10.10/24');
|
|
638
|
+
expect(context2.selfConfig.container_ip).toBe('10.0.10.11/24');
|
|
639
|
+
|
|
640
|
+
const allocations = await db.select().from(ipAllocations).all();
|
|
641
|
+
expect(allocations).toHaveLength(2);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('should auto-assign hostname from well-known capability', async () => {
|
|
645
|
+
// Create module providing public_web capability
|
|
646
|
+
db.insert(modules)
|
|
647
|
+
.values({
|
|
648
|
+
id: 'caddy',
|
|
649
|
+
name: 'Caddy',
|
|
650
|
+
version: '1.0.0',
|
|
651
|
+
sourcePath: '/test/caddy',
|
|
652
|
+
manifestData: {
|
|
653
|
+
id: 'caddy',
|
|
654
|
+
name: 'Caddy',
|
|
655
|
+
version: '1.0.0',
|
|
656
|
+
provides: {
|
|
657
|
+
capabilities: [
|
|
658
|
+
{
|
|
659
|
+
name: 'public_web',
|
|
660
|
+
version: '1.0.0',
|
|
661
|
+
data: {},
|
|
662
|
+
},
|
|
663
|
+
],
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
})
|
|
667
|
+
.run();
|
|
668
|
+
|
|
669
|
+
const context = await buildResolutionContext('caddy', db);
|
|
670
|
+
|
|
671
|
+
// Should auto-assign canonical hostname
|
|
672
|
+
expect(context.selfConfig.hostname).toBe('www');
|
|
673
|
+
|
|
674
|
+
// Verify persisted to database
|
|
675
|
+
const configs = await db
|
|
676
|
+
.select()
|
|
677
|
+
.from(moduleConfigs)
|
|
678
|
+
.where(eq(moduleConfigs.moduleId, 'caddy'))
|
|
679
|
+
.all();
|
|
680
|
+
const hostnameConfig = configs.find((c) => c.key === 'hostname');
|
|
681
|
+
expect(hostnameConfig?.value).toBe('www');
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test('should auto-assign zone from well-known capability', async () => {
|
|
685
|
+
// Create module providing auth capability (requires secure zone)
|
|
686
|
+
db.insert(modules)
|
|
687
|
+
.values({
|
|
688
|
+
id: 'authentik',
|
|
689
|
+
name: 'Authentik',
|
|
690
|
+
version: '1.0.0',
|
|
691
|
+
sourcePath: '/test/authentik',
|
|
692
|
+
manifestData: {
|
|
693
|
+
id: 'authentik',
|
|
694
|
+
name: 'Authentik',
|
|
695
|
+
version: '1.0.0',
|
|
696
|
+
provides: {
|
|
697
|
+
capabilities: [
|
|
698
|
+
{
|
|
699
|
+
name: 'auth',
|
|
700
|
+
version: '1.0.0',
|
|
701
|
+
data: {},
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
})
|
|
707
|
+
.run();
|
|
708
|
+
|
|
709
|
+
const context = await buildResolutionContext('authentik', db);
|
|
710
|
+
|
|
711
|
+
// Should auto-assign required zone
|
|
712
|
+
expect(context.selfConfig.zone).toBe('secure');
|
|
713
|
+
expect(context.selfConfig.hostname).toBe('auth');
|
|
714
|
+
|
|
715
|
+
// Verify persisted to database
|
|
716
|
+
const configs = await db
|
|
717
|
+
.select()
|
|
718
|
+
.from(moduleConfigs)
|
|
719
|
+
.where(eq(moduleConfigs.moduleId, 'authentik'))
|
|
720
|
+
.all();
|
|
721
|
+
const zoneConfig = configs.find((c) => c.key === 'zone');
|
|
722
|
+
expect(zoneConfig?.value).toBe('secure');
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test('should respect explicit hostname override', async () => {
|
|
726
|
+
// Create module with manual hostname config
|
|
727
|
+
db.insert(modules)
|
|
728
|
+
.values({
|
|
729
|
+
id: 'custom-web',
|
|
730
|
+
name: 'Custom Web',
|
|
731
|
+
version: '1.0.0',
|
|
732
|
+
sourcePath: '/test/custom',
|
|
733
|
+
manifestData: {
|
|
734
|
+
id: 'custom-web',
|
|
735
|
+
name: 'Custom Web',
|
|
736
|
+
version: '1.0.0',
|
|
737
|
+
provides: {
|
|
738
|
+
capabilities: [
|
|
739
|
+
{
|
|
740
|
+
name: 'public_web',
|
|
741
|
+
version: '1.0.0',
|
|
742
|
+
data: {},
|
|
743
|
+
},
|
|
744
|
+
],
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
})
|
|
748
|
+
.run();
|
|
749
|
+
|
|
750
|
+
db.insert(moduleConfigs)
|
|
751
|
+
.values({ moduleId: 'custom-web', key: 'hostname', value: 'custom' })
|
|
752
|
+
.run();
|
|
753
|
+
|
|
754
|
+
const context = await buildResolutionContext('custom-web', db);
|
|
755
|
+
|
|
756
|
+
// Should use explicit config
|
|
757
|
+
expect(context.selfConfig.hostname).toBe('custom');
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Note: Hostname conflict and zone enforcement tests moved to import-time validation
|
|
761
|
+
// See test-integration/module/zero-config.test.ts and src/module/import.test.ts
|
|
762
|
+
// These validations now happen during module import via validateWellKnownCapabilities()
|
|
763
|
+
|
|
764
|
+
test('should skip well-known assignment for non-well-known capabilities', async () => {
|
|
765
|
+
// Create module with custom capability
|
|
766
|
+
db.insert(modules)
|
|
767
|
+
.values({
|
|
768
|
+
id: 'custom-module',
|
|
769
|
+
name: 'Custom Module',
|
|
770
|
+
version: '1.0.0',
|
|
771
|
+
sourcePath: '/test/custom',
|
|
772
|
+
manifestData: {
|
|
773
|
+
id: 'custom-module',
|
|
774
|
+
name: 'Custom Module',
|
|
775
|
+
version: '1.0.0',
|
|
776
|
+
provides: {
|
|
777
|
+
capabilities: [{ name: 'custom_capability', version: '1.0.0', data: {} }],
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
})
|
|
781
|
+
.run();
|
|
782
|
+
|
|
783
|
+
const context = await buildResolutionContext('custom-module', db);
|
|
784
|
+
|
|
785
|
+
// Should not auto-assign anything
|
|
786
|
+
expect(context.selfConfig.hostname).toBeUndefined();
|
|
787
|
+
expect(context.selfConfig.zone).toBeUndefined();
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test('should prioritize first well-known capability when multiple provided', async () => {
|
|
791
|
+
// Create module providing multiple well-known capabilities
|
|
792
|
+
db.insert(modules)
|
|
793
|
+
.values({
|
|
794
|
+
id: 'multi-cap',
|
|
795
|
+
name: 'Multi Cap',
|
|
796
|
+
version: '1.0.0',
|
|
797
|
+
sourcePath: '/test/multi',
|
|
798
|
+
manifestData: {
|
|
799
|
+
id: 'multi-cap',
|
|
800
|
+
name: 'Multi Cap',
|
|
801
|
+
version: '1.0.0',
|
|
802
|
+
provides: {
|
|
803
|
+
capabilities: [
|
|
804
|
+
{ name: 'public_web', version: '1.0.0', data: {} }, // First
|
|
805
|
+
{ name: 'auth', version: '1.0.0', data: {} }, // Second
|
|
806
|
+
],
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
})
|
|
810
|
+
.run();
|
|
811
|
+
|
|
812
|
+
const context = await buildResolutionContext('multi-cap', db);
|
|
813
|
+
|
|
814
|
+
// Should use first capability (public_web)
|
|
815
|
+
expect(context.selfConfig.hostname).toBe('www');
|
|
816
|
+
expect(context.selfConfig.zone).toBe('dmz');
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test('should auto-apply VM resource defaults from manifest', async () => {
|
|
820
|
+
// Create module with VM resource defaults
|
|
821
|
+
db.insert(modules)
|
|
822
|
+
.values({
|
|
823
|
+
id: 'app-with-resources',
|
|
824
|
+
name: 'App With Resources',
|
|
825
|
+
version: '1.0.0',
|
|
826
|
+
sourcePath: '/test/app',
|
|
827
|
+
manifestData: {
|
|
828
|
+
id: 'app-with-resources',
|
|
829
|
+
name: 'App With Resources',
|
|
830
|
+
version: '1.0.0',
|
|
831
|
+
requires: {
|
|
832
|
+
machine: {
|
|
833
|
+
cpu: 2,
|
|
834
|
+
memory: 2048,
|
|
835
|
+
disk: 20,
|
|
836
|
+
storage: 'local-lvm',
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
})
|
|
841
|
+
.run();
|
|
842
|
+
|
|
843
|
+
const context = await buildResolutionContext('app-with-resources', db);
|
|
844
|
+
|
|
845
|
+
// Should auto-apply all VM resource defaults
|
|
846
|
+
expect(context.selfConfig.cores).toBe('2');
|
|
847
|
+
expect(context.selfConfig.memory).toBe('2048');
|
|
848
|
+
expect(context.selfConfig.disk).toBe('20');
|
|
849
|
+
expect(context.selfConfig.storage).toBe('local-lvm');
|
|
850
|
+
|
|
851
|
+
// Verify persisted to database
|
|
852
|
+
const configs = await db
|
|
853
|
+
.select()
|
|
854
|
+
.from(moduleConfigs)
|
|
855
|
+
.where(eq(moduleConfigs.moduleId, 'app-with-resources'))
|
|
856
|
+
.all();
|
|
857
|
+
|
|
858
|
+
expect(configs.find((c) => c.key === 'cores')?.value).toBe('2');
|
|
859
|
+
expect(configs.find((c) => c.key === 'memory')?.value).toBe('2048');
|
|
860
|
+
expect(configs.find((c) => c.key === 'disk')?.value).toBe('20');
|
|
861
|
+
expect(configs.find((c) => c.key === 'storage')?.value).toBe('local-lvm');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test('should respect user overrides for VM resources', async () => {
|
|
865
|
+
// Create module with VM resource defaults
|
|
866
|
+
db.insert(modules)
|
|
867
|
+
.values({
|
|
868
|
+
id: 'custom-resources',
|
|
869
|
+
name: 'Custom Resources',
|
|
870
|
+
version: '1.0.0',
|
|
871
|
+
sourcePath: '/test/custom',
|
|
872
|
+
manifestData: {
|
|
873
|
+
id: 'custom-resources',
|
|
874
|
+
name: 'Custom Resources',
|
|
875
|
+
version: '1.0.0',
|
|
876
|
+
requires: {
|
|
877
|
+
machine: {
|
|
878
|
+
cpu: 2,
|
|
879
|
+
memory: 2048,
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
})
|
|
884
|
+
.run();
|
|
885
|
+
|
|
886
|
+
// User explicitly sets higher memory
|
|
887
|
+
db.insert(moduleConfigs)
|
|
888
|
+
.values([
|
|
889
|
+
{ moduleId: 'custom-resources', key: 'cores', value: '4' },
|
|
890
|
+
{ moduleId: 'custom-resources', key: 'memory', value: '4096' },
|
|
891
|
+
])
|
|
892
|
+
.run();
|
|
893
|
+
|
|
894
|
+
const context = await buildResolutionContext('custom-resources', db);
|
|
895
|
+
|
|
896
|
+
// Should use user overrides, not manifest defaults
|
|
897
|
+
expect(context.selfConfig.cores).toBe('4');
|
|
898
|
+
expect(context.selfConfig.memory).toBe('4096');
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test('should handle modules without VM resources', async () => {
|
|
902
|
+
// Create module with no VM resources section
|
|
903
|
+
db.insert(modules)
|
|
904
|
+
.values({
|
|
905
|
+
id: 'no-resources',
|
|
906
|
+
name: 'No Resources',
|
|
907
|
+
version: '1.0.0',
|
|
908
|
+
sourcePath: '/test/no-res',
|
|
909
|
+
manifestData: {
|
|
910
|
+
id: 'no-resources',
|
|
911
|
+
name: 'No Resources',
|
|
912
|
+
version: '1.0.0',
|
|
913
|
+
},
|
|
914
|
+
})
|
|
915
|
+
.run();
|
|
916
|
+
|
|
917
|
+
const context = await buildResolutionContext('no-resources', db);
|
|
918
|
+
|
|
919
|
+
// Should not have any VM resource values
|
|
920
|
+
expect(context.selfConfig.cores).toBeUndefined();
|
|
921
|
+
expect(context.selfConfig.memory).toBeUndefined();
|
|
922
|
+
expect(context.selfConfig.disk).toBeUndefined();
|
|
923
|
+
expect(context.selfConfig.storage).toBeUndefined();
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test('should only apply defined VM resource fields', async () => {
|
|
927
|
+
// Create module with partial VM resources
|
|
928
|
+
db.insert(modules)
|
|
929
|
+
.values({
|
|
930
|
+
id: 'partial-resources',
|
|
931
|
+
name: 'Partial Resources',
|
|
932
|
+
version: '1.0.0',
|
|
933
|
+
sourcePath: '/test/partial',
|
|
934
|
+
manifestData: {
|
|
935
|
+
id: 'partial-resources',
|
|
936
|
+
name: 'Partial Resources',
|
|
937
|
+
version: '1.0.0',
|
|
938
|
+
requires: {
|
|
939
|
+
machine: {
|
|
940
|
+
cpu: 1,
|
|
941
|
+
memory: 512,
|
|
942
|
+
// No disk or storage specified
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
})
|
|
947
|
+
.run();
|
|
948
|
+
|
|
949
|
+
const context = await buildResolutionContext('partial-resources', db);
|
|
950
|
+
|
|
951
|
+
// Should apply specified fields
|
|
952
|
+
expect(context.selfConfig.cores).toBe('1');
|
|
953
|
+
expect(context.selfConfig.memory).toBe('512');
|
|
954
|
+
|
|
955
|
+
// Should not have unspecified fields
|
|
956
|
+
expect(context.selfConfig.disk).toBeUndefined();
|
|
957
|
+
expect(context.selfConfig.storage).toBeUndefined();
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
test('should auto-derive network config from dmz zone', async () => {
|
|
961
|
+
// Create module in DMZ zone
|
|
962
|
+
db.insert(modules)
|
|
963
|
+
.values({
|
|
964
|
+
id: 'dmz-module',
|
|
965
|
+
name: 'DMZ Module',
|
|
966
|
+
version: '1.0.0',
|
|
967
|
+
sourcePath: '/test/dmz',
|
|
968
|
+
manifestData: {
|
|
969
|
+
id: 'dmz-module',
|
|
970
|
+
name: 'DMZ Module',
|
|
971
|
+
version: '1.0.0',
|
|
972
|
+
requires: {
|
|
973
|
+
machine: {
|
|
974
|
+
zone: 'dmz',
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
})
|
|
979
|
+
.run();
|
|
980
|
+
|
|
981
|
+
const context = await buildResolutionContext('dmz-module', db);
|
|
982
|
+
|
|
983
|
+
// Should auto-derive all network config from DMZ zone
|
|
984
|
+
expect(context.selfConfig.gateway).toBe('10.0.10.1');
|
|
985
|
+
expect(context.selfConfig.vlan).toBe('10');
|
|
986
|
+
expect(context.selfConfig.subnet).toBe('10.0.10.0/24');
|
|
987
|
+
expect(context.selfConfig.bridge).toBe('vmbr0');
|
|
988
|
+
|
|
989
|
+
// Verify persisted to database
|
|
990
|
+
const configs = await db
|
|
991
|
+
.select()
|
|
992
|
+
.from(moduleConfigs)
|
|
993
|
+
.where(eq(moduleConfigs.moduleId, 'dmz-module'))
|
|
994
|
+
.all();
|
|
995
|
+
|
|
996
|
+
expect(configs.find((c) => c.key === 'gateway')?.value).toBe('10.0.10.1');
|
|
997
|
+
expect(configs.find((c) => c.key === 'vlan')?.value).toBe('10');
|
|
998
|
+
expect(configs.find((c) => c.key === 'subnet')?.value).toBe('10.0.10.0/24');
|
|
999
|
+
expect(configs.find((c) => c.key === 'bridge')?.value).toBe('vmbr0');
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
test('should auto-derive network config from app zone', async () => {
|
|
1003
|
+
// Create module in app zone
|
|
1004
|
+
db.insert(modules)
|
|
1005
|
+
.values({
|
|
1006
|
+
id: 'app-module',
|
|
1007
|
+
name: 'App Module',
|
|
1008
|
+
version: '1.0.0',
|
|
1009
|
+
sourcePath: '/test/app',
|
|
1010
|
+
manifestData: {
|
|
1011
|
+
id: 'app-module',
|
|
1012
|
+
name: 'App Module',
|
|
1013
|
+
version: '1.0.0',
|
|
1014
|
+
requires: {
|
|
1015
|
+
machine: {
|
|
1016
|
+
zone: 'app',
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
})
|
|
1021
|
+
.run();
|
|
1022
|
+
|
|
1023
|
+
const context = await buildResolutionContext('app-module', db);
|
|
1024
|
+
|
|
1025
|
+
// Should auto-derive network config from app zone
|
|
1026
|
+
expect(context.selfConfig.gateway).toBe('10.0.20.1');
|
|
1027
|
+
expect(context.selfConfig.vlan).toBe('20');
|
|
1028
|
+
expect(context.selfConfig.subnet).toBe('10.0.20.0/24');
|
|
1029
|
+
expect(context.selfConfig.bridge).toBe('vmbr0');
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
test('should auto-derive network config from secure zone', async () => {
|
|
1033
|
+
// Create module in secure zone
|
|
1034
|
+
db.insert(modules)
|
|
1035
|
+
.values({
|
|
1036
|
+
id: 'secure-module',
|
|
1037
|
+
name: 'Secure Module',
|
|
1038
|
+
version: '1.0.0',
|
|
1039
|
+
sourcePath: '/test/secure',
|
|
1040
|
+
manifestData: {
|
|
1041
|
+
id: 'secure-module',
|
|
1042
|
+
name: 'Secure Module',
|
|
1043
|
+
version: '1.0.0',
|
|
1044
|
+
requires: {
|
|
1045
|
+
machine: {
|
|
1046
|
+
zone: 'secure',
|
|
1047
|
+
},
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
})
|
|
1051
|
+
.run();
|
|
1052
|
+
|
|
1053
|
+
const context = await buildResolutionContext('secure-module', db);
|
|
1054
|
+
|
|
1055
|
+
// Should auto-derive network config from secure zone
|
|
1056
|
+
expect(context.selfConfig.gateway).toBe('10.0.30.1');
|
|
1057
|
+
expect(context.selfConfig.vlan).toBe('30');
|
|
1058
|
+
expect(context.selfConfig.subnet).toBe('10.0.30.0/24');
|
|
1059
|
+
expect(context.selfConfig.bridge).toBe('vmbr0');
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test('should use zone from config if specified', async () => {
|
|
1063
|
+
// Create module with zone in config (not manifest)
|
|
1064
|
+
db.insert(modules)
|
|
1065
|
+
.values({
|
|
1066
|
+
id: 'config-zone',
|
|
1067
|
+
name: 'Config Zone',
|
|
1068
|
+
version: '1.0.0',
|
|
1069
|
+
sourcePath: '/test/config-zone',
|
|
1070
|
+
manifestData: {
|
|
1071
|
+
id: 'config-zone',
|
|
1072
|
+
name: 'Config Zone',
|
|
1073
|
+
version: '1.0.0',
|
|
1074
|
+
},
|
|
1075
|
+
})
|
|
1076
|
+
.run();
|
|
1077
|
+
|
|
1078
|
+
db.insert(moduleConfigs).values({ moduleId: 'config-zone', key: 'zone', value: 'app' }).run();
|
|
1079
|
+
|
|
1080
|
+
const context = await buildResolutionContext('config-zone', db);
|
|
1081
|
+
|
|
1082
|
+
// Should use zone from config and derive network settings
|
|
1083
|
+
expect(context.selfConfig.gateway).toBe('10.0.20.1');
|
|
1084
|
+
expect(context.selfConfig.vlan).toBe('20');
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test('should respect user overrides for network config', async () => {
|
|
1088
|
+
// Create module with zone
|
|
1089
|
+
db.insert(modules)
|
|
1090
|
+
.values({
|
|
1091
|
+
id: 'custom-network',
|
|
1092
|
+
name: 'Custom Network',
|
|
1093
|
+
version: '1.0.0',
|
|
1094
|
+
sourcePath: '/test/custom-net',
|
|
1095
|
+
manifestData: {
|
|
1096
|
+
id: 'custom-network',
|
|
1097
|
+
name: 'Custom Network',
|
|
1098
|
+
version: '1.0.0',
|
|
1099
|
+
requires: {
|
|
1100
|
+
machine: {
|
|
1101
|
+
zone: 'dmz',
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
})
|
|
1106
|
+
.run();
|
|
1107
|
+
|
|
1108
|
+
// User explicitly sets different gateway
|
|
1109
|
+
db.insert(moduleConfigs)
|
|
1110
|
+
.values([
|
|
1111
|
+
{ moduleId: 'custom-network', key: 'gateway', value: '10.0.10.254' },
|
|
1112
|
+
{ moduleId: 'custom-network', key: 'vlan', value: '100' },
|
|
1113
|
+
])
|
|
1114
|
+
.run();
|
|
1115
|
+
|
|
1116
|
+
const context = await buildResolutionContext('custom-network', db);
|
|
1117
|
+
|
|
1118
|
+
// Should use user overrides, not zone defaults
|
|
1119
|
+
expect(context.selfConfig.gateway).toBe('10.0.10.254');
|
|
1120
|
+
expect(context.selfConfig.vlan).toBe('100');
|
|
1121
|
+
|
|
1122
|
+
// Should still auto-derive unset fields
|
|
1123
|
+
expect(context.selfConfig.subnet).toBe('10.0.10.0/24');
|
|
1124
|
+
expect(context.selfConfig.bridge).toBe('vmbr0');
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
test('should skip zone-based networking for external zone', async () => {
|
|
1128
|
+
// Create VPS module (external zone)
|
|
1129
|
+
db.insert(modules)
|
|
1130
|
+
.values({
|
|
1131
|
+
id: 'vps-external',
|
|
1132
|
+
name: 'VPS External',
|
|
1133
|
+
version: '1.0.0',
|
|
1134
|
+
sourcePath: '/test/vps',
|
|
1135
|
+
manifestData: {
|
|
1136
|
+
id: 'vps-external',
|
|
1137
|
+
name: 'VPS External',
|
|
1138
|
+
version: '1.0.0',
|
|
1139
|
+
requires: {
|
|
1140
|
+
machine: {
|
|
1141
|
+
zone: 'external',
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
})
|
|
1146
|
+
.run();
|
|
1147
|
+
|
|
1148
|
+
const context = await buildResolutionContext('vps-external', db);
|
|
1149
|
+
|
|
1150
|
+
// Should not auto-derive network config for external zone
|
|
1151
|
+
expect(context.selfConfig.gateway).toBeUndefined();
|
|
1152
|
+
expect(context.selfConfig.vlan).toBeUndefined();
|
|
1153
|
+
expect(context.selfConfig.subnet).toBeUndefined();
|
|
1154
|
+
expect(context.selfConfig.bridge).toBeUndefined();
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
test('should handle missing system network config gracefully', async () => {
|
|
1158
|
+
// Create module in zone without system config
|
|
1159
|
+
db.insert(modules)
|
|
1160
|
+
.values({
|
|
1161
|
+
id: 'no-net-config',
|
|
1162
|
+
name: 'No Net Config',
|
|
1163
|
+
version: '1.0.0',
|
|
1164
|
+
sourcePath: '/test/no-net',
|
|
1165
|
+
manifestData: {
|
|
1166
|
+
id: 'no-net-config',
|
|
1167
|
+
name: 'No Net Config',
|
|
1168
|
+
version: '1.0.0',
|
|
1169
|
+
requires: {
|
|
1170
|
+
machine: {
|
|
1171
|
+
zone: 'dmz',
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
},
|
|
1175
|
+
})
|
|
1176
|
+
.run();
|
|
1177
|
+
|
|
1178
|
+
// Remove all network config from system
|
|
1179
|
+
await db.delete(systemConfig).run();
|
|
1180
|
+
|
|
1181
|
+
const context = await buildResolutionContext('no-net-config', db);
|
|
1182
|
+
|
|
1183
|
+
// Should not fail, just not have network config
|
|
1184
|
+
expect(context.selfConfig.gateway).toBeUndefined();
|
|
1185
|
+
expect(context.selfConfig.vlan).toBeUndefined();
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
test('should handle module without zone', async () => {
|
|
1189
|
+
// Create module without zone specified
|
|
1190
|
+
db.insert(modules)
|
|
1191
|
+
.values({
|
|
1192
|
+
id: 'no-zone',
|
|
1193
|
+
name: 'No Zone',
|
|
1194
|
+
version: '1.0.0',
|
|
1195
|
+
sourcePath: '/test/no-zone',
|
|
1196
|
+
manifestData: {
|
|
1197
|
+
id: 'no-zone',
|
|
1198
|
+
name: 'No Zone',
|
|
1199
|
+
version: '1.0.0',
|
|
1200
|
+
},
|
|
1201
|
+
})
|
|
1202
|
+
.run();
|
|
1203
|
+
|
|
1204
|
+
const context = await buildResolutionContext('no-zone', db);
|
|
1205
|
+
|
|
1206
|
+
// Should not auto-derive network config (no zone)
|
|
1207
|
+
expect(context.selfConfig.gateway).toBeUndefined();
|
|
1208
|
+
expect(context.selfConfig.vlan).toBeUndefined();
|
|
1209
|
+
expect(context.selfConfig.subnet).toBeUndefined();
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
describe('buildContextFromData', () => {
|
|
1214
|
+
test('should build context from explicit data', () => {
|
|
1215
|
+
const context = buildContextFromData('test-module', {
|
|
1216
|
+
selfConfig: { ip: '192.168.0.50' },
|
|
1217
|
+
secrets: { key: 'secret' },
|
|
1218
|
+
capabilities: { dns: { server: 'ns1' } },
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
expect(context.moduleId).toBe('test-module');
|
|
1222
|
+
expect(context.selfConfig.ip).toBe('192.168.0.50');
|
|
1223
|
+
// Auto-derived variables
|
|
1224
|
+
expect(context.selfConfig['inventory.ansible_user']).toBe('root');
|
|
1225
|
+
expect(context.selfConfig['inventory.groups']).toBe('test-module');
|
|
1226
|
+
expect(context.secrets).toEqual({ key: 'secret' });
|
|
1227
|
+
expect(context.capabilities).toEqual({ dns: { server: 'ns1' } });
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
test('should use provided system config', () => {
|
|
1231
|
+
const context = buildContextFromData('test-module', {
|
|
1232
|
+
systemConfig: { 'dns.primary': '192.168.0.1', 'network.domain': 'homelab.local' },
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
expect(context.systemConfig['dns.primary']).toBe('192.168.0.1');
|
|
1236
|
+
expect(context.systemConfig['network.domain']).toBe('homelab.local');
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
test('should have empty system config when none provided', () => {
|
|
1240
|
+
const context = buildContextFromData('test-module');
|
|
1241
|
+
|
|
1242
|
+
expect(context.systemConfig).toEqual({});
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
test('should return empty maps when no data provided', () => {
|
|
1246
|
+
const context = buildContextFromData('test-module');
|
|
1247
|
+
|
|
1248
|
+
// Should still have auto-derived inventory variables
|
|
1249
|
+
expect(context.selfConfig['inventory.ansible_user']).toBe('root');
|
|
1250
|
+
expect(context.selfConfig['inventory.groups']).toBe('test-module');
|
|
1251
|
+
expect(context.secrets).toEqual({});
|
|
1252
|
+
expect(context.capabilities).toEqual({});
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
test('should auto-derive inventory variables in buildContextFromData', () => {
|
|
1256
|
+
const context = buildContextFromData('my-module', {
|
|
1257
|
+
selfConfig: { container_ip: '10.0.30.15/24', hostname: 'my-host' },
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
expect(context.selfConfig['inventory.ansible_host']).toBe('10.0.30.15');
|
|
1261
|
+
expect(context.selfConfig['inventory.ansible_user']).toBe('root');
|
|
1262
|
+
expect(context.selfConfig['inventory.groups']).toBe('my-module');
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
});
|