@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,624 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getWellKnownCapability, isWellKnown } from '../capabilities/well-known';
|
|
3
|
+
import { getDb } from '../db/client';
|
|
4
|
+
import type { DbClient } from '../db/client';
|
|
5
|
+
import {
|
|
6
|
+
capabilities,
|
|
7
|
+
moduleConfigs,
|
|
8
|
+
modules,
|
|
9
|
+
secrets,
|
|
10
|
+
systemConfig,
|
|
11
|
+
systemSecrets,
|
|
12
|
+
} from '../db/schema';
|
|
13
|
+
import { allocateResources, getAllocation } from '../ipam/allocator';
|
|
14
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
15
|
+
import { decryptSecret } from '../secrets/encryption';
|
|
16
|
+
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
17
|
+
import { applyDeclarativeDerivations } from './declarative-derivation';
|
|
18
|
+
import type { ResolutionContext } from './types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Strip CIDR notation from IP address
|
|
22
|
+
* Policy function - pure string manipulation
|
|
23
|
+
*
|
|
24
|
+
* @param ipWithCidr - IP address with optional CIDR (e.g., "10.0.10.10/24")
|
|
25
|
+
* @returns IP address without CIDR (e.g., "10.0.10.10")
|
|
26
|
+
*/
|
|
27
|
+
function stripCidr(ipWithCidr: string): string {
|
|
28
|
+
const slashIndex = ipWithCidr.indexOf('/');
|
|
29
|
+
if (slashIndex === -1) {
|
|
30
|
+
return ipWithCidr;
|
|
31
|
+
}
|
|
32
|
+
return ipWithCidr.slice(0, slashIndex);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Auto-assign hostname and zone from well-known capabilities
|
|
37
|
+
* Policy function - derives values from capability registry
|
|
38
|
+
*
|
|
39
|
+
* Zero-config systems
|
|
40
|
+
* If module provides a well-known capability, auto-assign:
|
|
41
|
+
* - hostname from canonical_hostname
|
|
42
|
+
* - zone from required_zone
|
|
43
|
+
*
|
|
44
|
+
* Only assigns if not already set (explicit config wins)
|
|
45
|
+
*
|
|
46
|
+
* @param manifest - Module manifest
|
|
47
|
+
* @param selfConfig - Current module configuration
|
|
48
|
+
* @param db - Database client for hostname conflict detection
|
|
49
|
+
* @returns Object with assigned hostname and zone (if any)
|
|
50
|
+
* @throws Error if hostname conflict or zone enforcement fails
|
|
51
|
+
*/
|
|
52
|
+
async function autoAssignFromWellKnown(
|
|
53
|
+
manifest: ModuleManifest,
|
|
54
|
+
selfConfig: Record<string, string>,
|
|
55
|
+
_moduleId: string,
|
|
56
|
+
_db: DbClient,
|
|
57
|
+
): Promise<{ hostname?: string; zone?: string }> {
|
|
58
|
+
const providedCapabilities = manifest.provides?.capabilities ?? [];
|
|
59
|
+
const result: { hostname?: string; zone?: string } = {};
|
|
60
|
+
|
|
61
|
+
// Find first well-known capability (priority order)
|
|
62
|
+
// Note: Capability uniqueness and zone enforcement are validated at import time
|
|
63
|
+
for (const capability of providedCapabilities) {
|
|
64
|
+
if (!isWellKnown(capability.name)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const wellKnown = getWellKnownCapability(capability.name);
|
|
69
|
+
|
|
70
|
+
// Determine current zone (from config or manifest)
|
|
71
|
+
const currentZone = selfConfig.zone || manifest.requires?.machine?.zone;
|
|
72
|
+
|
|
73
|
+
// Auto-assign hostname if not already set
|
|
74
|
+
if (!selfConfig.hostname) {
|
|
75
|
+
result.hostname = wellKnown.canonical_hostname;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Auto-assign zone if not already set (in config or manifest)
|
|
79
|
+
if (!currentZone) {
|
|
80
|
+
result.zone = wellKnown.required_zone;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Only process first well-known capability (deterministic)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Determine if module needs IPAM auto-allocation
|
|
92
|
+
* Policy function - checks manifest and config
|
|
93
|
+
*
|
|
94
|
+
* A module needs IPAM if:
|
|
95
|
+
* 1. It declares vmid and container_ip variables (container-based), AND
|
|
96
|
+
* 2. These values are not already set in module config
|
|
97
|
+
*
|
|
98
|
+
* @param manifest - Module manifest
|
|
99
|
+
* @param selfConfig - Current module configuration
|
|
100
|
+
* @returns true if IPAM allocation needed
|
|
101
|
+
*/
|
|
102
|
+
function needsIpamAllocation(
|
|
103
|
+
manifest: ModuleManifest,
|
|
104
|
+
selfConfig: Record<string, string>,
|
|
105
|
+
): boolean {
|
|
106
|
+
const variables = manifest.variables?.owns ?? [];
|
|
107
|
+
|
|
108
|
+
const hasVmid = variables.some((v) => v.name === 'vmid');
|
|
109
|
+
const hasContainerIp = variables.some((v) => v.name === 'container_ip');
|
|
110
|
+
|
|
111
|
+
// Module must declare both vmid and container_ip to be container-based
|
|
112
|
+
if (!hasVmid || !hasContainerIp) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if already allocated (both must be present)
|
|
117
|
+
const vmidSet = selfConfig.vmid !== undefined && selfConfig.vmid !== '';
|
|
118
|
+
const ipSet = selfConfig.container_ip !== undefined && selfConfig.container_ip !== '';
|
|
119
|
+
|
|
120
|
+
// Need allocation if either is missing
|
|
121
|
+
return !vmidSet || !ipSet;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Determine network zone for module
|
|
126
|
+
* Policy function - extracts zone from manifest or config
|
|
127
|
+
*
|
|
128
|
+
* Zone determination priority:
|
|
129
|
+
* 1. Explicit zone in module config (user override)
|
|
130
|
+
* 2. VM resources zone in manifest
|
|
131
|
+
* 3. Default to 'dmz'
|
|
132
|
+
*
|
|
133
|
+
* @param manifest - Module manifest
|
|
134
|
+
* @param selfConfig - Current module configuration
|
|
135
|
+
* @returns Network zone ('dmz', 'app', 'secure', or 'internal')
|
|
136
|
+
*/
|
|
137
|
+
function determineModuleZone(
|
|
138
|
+
manifest: ModuleManifest,
|
|
139
|
+
selfConfig: Record<string, string>,
|
|
140
|
+
): 'dmz' | 'app' | 'secure' | 'internal' {
|
|
141
|
+
// Priority 1: Explicit zone in config
|
|
142
|
+
if (selfConfig.zone) {
|
|
143
|
+
const zone = selfConfig.zone;
|
|
144
|
+
if (zone === 'dmz' || zone === 'app' || zone === 'secure' || zone === 'internal') {
|
|
145
|
+
return zone;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Priority 2: VM resources zone in manifest
|
|
150
|
+
const vmZone = manifest.requires?.machine?.zone;
|
|
151
|
+
if (vmZone === 'dmz' || vmZone === 'app' || vmZone === 'secure' || vmZone === 'internal') {
|
|
152
|
+
return vmZone;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Default: dmz (public-facing services)
|
|
156
|
+
return 'dmz';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Auto-derive inventory variables from module configuration
|
|
161
|
+
* Policy function - derives values from existing config
|
|
162
|
+
*
|
|
163
|
+
* These variables are automatically available in Ansible templates:
|
|
164
|
+
* - inventory.hostname: Derived from hostname variable
|
|
165
|
+
* - inventory.ansible_host: Derived from container_ip (strips CIDR) or vps_ip
|
|
166
|
+
* - inventory.ansible_user: Defaults to "root"
|
|
167
|
+
* - inventory.groups: Derived from module ID
|
|
168
|
+
*
|
|
169
|
+
* @param moduleId - Module ID
|
|
170
|
+
* @param selfConfig - Module configuration
|
|
171
|
+
* @returns Additional derived variables to merge into selfConfig
|
|
172
|
+
*/
|
|
173
|
+
function autoDeriveInventoryVariables(
|
|
174
|
+
moduleId: string,
|
|
175
|
+
selfConfig: Record<string, string>,
|
|
176
|
+
): Record<string, string> {
|
|
177
|
+
const derived: Record<string, string> = {};
|
|
178
|
+
|
|
179
|
+
// Auto-derive hostname from hostname variable
|
|
180
|
+
if (selfConfig.hostname) {
|
|
181
|
+
derived['inventory.hostname'] = selfConfig.hostname;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Auto-derive ansible_host from container_ip (strips CIDR) or vps_ip
|
|
185
|
+
if (selfConfig.container_ip) {
|
|
186
|
+
derived['inventory.ansible_host'] = stripCidr(selfConfig.container_ip);
|
|
187
|
+
} else if (selfConfig.vps_ip) {
|
|
188
|
+
// VPS-based modules use vps_ip directly (no CIDR to strip)
|
|
189
|
+
derived['inventory.ansible_host'] = selfConfig.vps_ip;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Auto-derive ansible_user (default: root)
|
|
193
|
+
// Can be overridden by module config
|
|
194
|
+
if (!selfConfig['inventory.ansible_user']) {
|
|
195
|
+
derived['inventory.ansible_user'] = 'root';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Auto-derive groups from module ID
|
|
199
|
+
// Format: module ID becomes the primary group
|
|
200
|
+
derived['inventory.groups'] = moduleId;
|
|
201
|
+
|
|
202
|
+
return derived;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Build resolution context for a module
|
|
207
|
+
*
|
|
208
|
+
* Execution function (Rule 10.1) - performs database queries
|
|
209
|
+
*
|
|
210
|
+
* @param moduleId - Module to build context for
|
|
211
|
+
* @param db - Database client (optional, for testing)
|
|
212
|
+
* @returns Resolution context with all data sources
|
|
213
|
+
*/
|
|
214
|
+
export async function buildResolutionContext(
|
|
215
|
+
moduleId: string,
|
|
216
|
+
db = getDb(),
|
|
217
|
+
): Promise<ResolutionContext> {
|
|
218
|
+
// Fetch module manifest for VM resources
|
|
219
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
220
|
+
|
|
221
|
+
// Fetch module configuration (self)
|
|
222
|
+
const configRows = db
|
|
223
|
+
.select()
|
|
224
|
+
.from(moduleConfigs)
|
|
225
|
+
.where(eq(moduleConfigs.moduleId, moduleId))
|
|
226
|
+
.all();
|
|
227
|
+
|
|
228
|
+
const selfConfig: Record<string, string> = {};
|
|
229
|
+
for (const row of configRows) {
|
|
230
|
+
// Handle complex types (stored in valueJson)
|
|
231
|
+
if (row.valueJson) {
|
|
232
|
+
selfConfig[row.key] = row.valueJson; // Store JSON string
|
|
233
|
+
} else {
|
|
234
|
+
selfConfig[row.key] = row.value;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Well-known capability auto-assignment
|
|
239
|
+
// Auto-assign hostname and zone if module provides well-known capability
|
|
240
|
+
if (module?.manifestData) {
|
|
241
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
242
|
+
const assigned = await autoAssignFromWellKnown(manifest, selfConfig, moduleId, db);
|
|
243
|
+
|
|
244
|
+
// Store assigned values in module config
|
|
245
|
+
if (assigned.hostname) {
|
|
246
|
+
await db
|
|
247
|
+
.insert(moduleConfigs)
|
|
248
|
+
.values({ moduleId, key: 'hostname', value: assigned.hostname })
|
|
249
|
+
.onConflictDoUpdate({
|
|
250
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
251
|
+
set: { value: assigned.hostname },
|
|
252
|
+
});
|
|
253
|
+
selfConfig.hostname = assigned.hostname;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (assigned.zone) {
|
|
257
|
+
await db
|
|
258
|
+
.insert(moduleConfigs)
|
|
259
|
+
.values({ moduleId, key: 'zone', value: assigned.zone })
|
|
260
|
+
.onConflictDoUpdate({
|
|
261
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
262
|
+
set: { value: assigned.zone },
|
|
263
|
+
});
|
|
264
|
+
selfConfig.zone = assigned.zone;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Variable defaults
|
|
269
|
+
// Auto-apply default values from variable declarations if not already configured
|
|
270
|
+
if (module?.manifestData) {
|
|
271
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
272
|
+
const variables = manifest.variables?.owns ?? [];
|
|
273
|
+
|
|
274
|
+
for (const variable of variables) {
|
|
275
|
+
// Only apply if variable has a default and config doesn't already have it
|
|
276
|
+
if (variable.default !== undefined && !selfConfig[variable.name]) {
|
|
277
|
+
const valueStr = String(variable.default);
|
|
278
|
+
|
|
279
|
+
await db
|
|
280
|
+
.insert(moduleConfigs)
|
|
281
|
+
.values({ moduleId, key: variable.name, value: valueStr })
|
|
282
|
+
.onConflictDoUpdate({
|
|
283
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
284
|
+
set: { value: valueStr },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
selfConfig[variable.name] = valueStr;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// VM resource defaults
|
|
293
|
+
// Auto-apply VM resource defaults from manifest if not already configured
|
|
294
|
+
if (module?.manifestData) {
|
|
295
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
296
|
+
const machineResources = manifest.requires?.machine;
|
|
297
|
+
|
|
298
|
+
if (machineResources) {
|
|
299
|
+
// Map manifest fields to module variable names and apply defaults
|
|
300
|
+
const resourceMappings: Array<{
|
|
301
|
+
manifestKey: keyof typeof machineResources;
|
|
302
|
+
configKey: string;
|
|
303
|
+
}> = [
|
|
304
|
+
{ manifestKey: 'cpu', configKey: 'cores' }, // manifest.requires.machine.cpu → cores variable
|
|
305
|
+
{ manifestKey: 'memory', configKey: 'memory' },
|
|
306
|
+
{ manifestKey: 'disk', configKey: 'disk' },
|
|
307
|
+
{ manifestKey: 'storage', configKey: 'storage' },
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
for (const { manifestKey, configKey } of resourceMappings) {
|
|
311
|
+
const value = machineResources[manifestKey];
|
|
312
|
+
|
|
313
|
+
// Only apply if manifest specifies a value and config doesn't already have it
|
|
314
|
+
if (value !== undefined && !selfConfig[configKey]) {
|
|
315
|
+
const valueStr = String(value);
|
|
316
|
+
|
|
317
|
+
await db
|
|
318
|
+
.insert(moduleConfigs)
|
|
319
|
+
.values({ moduleId, key: configKey, value: valueStr })
|
|
320
|
+
.onConflictDoUpdate({
|
|
321
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
322
|
+
set: { value: valueStr },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
selfConfig[configKey] = valueStr;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// IPAM auto-allocation
|
|
332
|
+
// If module declares vmid/container_ip but they're not configured, allocate automatically
|
|
333
|
+
if (module?.manifestData) {
|
|
334
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
335
|
+
|
|
336
|
+
if (needsIpamAllocation(manifest, selfConfig)) {
|
|
337
|
+
const zone = determineModuleZone(manifest, selfConfig);
|
|
338
|
+
|
|
339
|
+
// Use transaction to ensure atomicity
|
|
340
|
+
await db.transaction(async (tx) => {
|
|
341
|
+
// Check if allocation already exists in ipAllocations table
|
|
342
|
+
const existing = await getAllocation(moduleId, tx);
|
|
343
|
+
|
|
344
|
+
let vmid: number;
|
|
345
|
+
let containerIp: string;
|
|
346
|
+
|
|
347
|
+
if (existing) {
|
|
348
|
+
// Use existing allocation
|
|
349
|
+
vmid = existing.vmid;
|
|
350
|
+
containerIp = existing.containerIp;
|
|
351
|
+
} else {
|
|
352
|
+
// Allocate new resources (persists to ipAllocations)
|
|
353
|
+
const allocation = await allocateResources(moduleId, zone, tx);
|
|
354
|
+
vmid = allocation.vmid;
|
|
355
|
+
containerIp = allocation.containerIp;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Ensure values are in module config (upsert to handle existing keys)
|
|
359
|
+
await tx
|
|
360
|
+
.insert(moduleConfigs)
|
|
361
|
+
.values({ moduleId, key: 'vmid', value: String(vmid) })
|
|
362
|
+
.onConflictDoUpdate({
|
|
363
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
364
|
+
set: { value: String(vmid) },
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await tx
|
|
368
|
+
.insert(moduleConfigs)
|
|
369
|
+
.values({ moduleId, key: 'container_ip', value: containerIp })
|
|
370
|
+
.onConflictDoUpdate({
|
|
371
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
372
|
+
set: { value: containerIp },
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Update selfConfig with allocated values
|
|
376
|
+
selfConfig.vmid = String(vmid);
|
|
377
|
+
selfConfig.container_ip = containerIp;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Add machine requirements from manifest to selfConfig so templates can
|
|
383
|
+
// reference them via $self:requires.machine.<field>
|
|
384
|
+
if (module?.manifestData) {
|
|
385
|
+
const manifest = module.manifestData as Record<string, unknown>;
|
|
386
|
+
const requires = manifest.requires as Record<string, unknown> | undefined;
|
|
387
|
+
if (requires?.machine) {
|
|
388
|
+
const machineRequires = requires.machine as Record<string, unknown>;
|
|
389
|
+
for (const [key, value] of Object.entries(machineRequires)) {
|
|
390
|
+
selfConfig[`requires.machine.${key}`] = String(value);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Fetch secrets (encrypted values should be decrypted by caller)
|
|
396
|
+
const secretRows = db.select().from(secrets).where(eq(secrets.moduleId, moduleId)).all();
|
|
397
|
+
|
|
398
|
+
const secretsMap: Record<string, string> = {};
|
|
399
|
+
for (const row of secretRows) {
|
|
400
|
+
secretsMap[row.name] = row.encryptedValue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Fetch all capabilities (for capability variables)
|
|
404
|
+
const capabilityRows = db.select().from(capabilities).all();
|
|
405
|
+
|
|
406
|
+
const capabilitiesMap: Record<string, Record<string, unknown>> = {};
|
|
407
|
+
for (const row of capabilityRows) {
|
|
408
|
+
// Parse JSON data if it's a string
|
|
409
|
+
let data: unknown;
|
|
410
|
+
if (typeof row.data === 'string') {
|
|
411
|
+
try {
|
|
412
|
+
data = JSON.parse(row.data);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Failed to parse capability data for ${row.capabilityName}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
data = row.data;
|
|
420
|
+
}
|
|
421
|
+
capabilitiesMap[row.capabilityName] = data as Record<string, unknown>;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Fetch system configuration (for $system: variables)
|
|
425
|
+
const systemConfigRows = db.select().from(systemConfig).all();
|
|
426
|
+
|
|
427
|
+
const systemConfigMap: Record<string, string> = {};
|
|
428
|
+
for (const row of systemConfigRows) {
|
|
429
|
+
systemConfigMap[row.key] = row.value;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Fetch system secrets (for $system_secret: variables)
|
|
433
|
+
const systemSecretsMap: Record<string, string> = {};
|
|
434
|
+
try {
|
|
435
|
+
const systemSecretRows = db.select().from(systemSecrets).all();
|
|
436
|
+
|
|
437
|
+
if (systemSecretRows.length > 0) {
|
|
438
|
+
// Get master key for decryption
|
|
439
|
+
const masterKey = await getOrCreateMasterKey();
|
|
440
|
+
|
|
441
|
+
for (const row of systemSecretRows) {
|
|
442
|
+
try {
|
|
443
|
+
const decrypted = decryptSecret(
|
|
444
|
+
{
|
|
445
|
+
encryptedValue: row.encryptedValue,
|
|
446
|
+
iv: row.iv,
|
|
447
|
+
authTag: row.authTag,
|
|
448
|
+
},
|
|
449
|
+
masterKey,
|
|
450
|
+
);
|
|
451
|
+
systemSecretsMap[row.key] = decrypted;
|
|
452
|
+
} catch (err) {
|
|
453
|
+
// Log error but continue with other secrets
|
|
454
|
+
console.error(`Failed to decrypt system secret ${row.key}:`, err);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} catch (_err) {
|
|
459
|
+
// Table might not exist in test/old databases - that's okay
|
|
460
|
+
// System secrets are optional
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Zone-based networking
|
|
464
|
+
// Auto-derive network config from zone (gateway, vlan, subnet, bridge)
|
|
465
|
+
if (module?.manifestData) {
|
|
466
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
467
|
+
const zone = selfConfig.zone || manifest.requires?.machine?.zone;
|
|
468
|
+
|
|
469
|
+
// If zone from manifest but not in selfConfig, store it as first-class config
|
|
470
|
+
if (zone && !selfConfig.zone) {
|
|
471
|
+
await db
|
|
472
|
+
.insert(moduleConfigs)
|
|
473
|
+
.values({ moduleId, key: 'zone', value: zone })
|
|
474
|
+
.onConflictDoUpdate({
|
|
475
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
476
|
+
set: { value: zone },
|
|
477
|
+
});
|
|
478
|
+
selfConfig.zone = zone;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Only apply for container-based zones (not external/VPS)
|
|
482
|
+
if (zone && zone !== 'external') {
|
|
483
|
+
const networkFields = ['gateway', 'vlan', 'subnet', 'bridge'];
|
|
484
|
+
|
|
485
|
+
for (const field of networkFields) {
|
|
486
|
+
// Only apply if not already configured by user
|
|
487
|
+
if (!selfConfig[field]) {
|
|
488
|
+
const systemConfigKey = `network.${zone}.${field}`;
|
|
489
|
+
const value = systemConfigMap[systemConfigKey];
|
|
490
|
+
|
|
491
|
+
if (value) {
|
|
492
|
+
await db
|
|
493
|
+
.insert(moduleConfigs)
|
|
494
|
+
.values({ moduleId, key: field, value })
|
|
495
|
+
.onConflictDoUpdate({
|
|
496
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
497
|
+
set: { value },
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
selfConfig[field] = value;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Declarative variable derivation
|
|
508
|
+
// Apply template-based derivations from manifest
|
|
509
|
+
if (module?.manifestData) {
|
|
510
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
511
|
+
const context: ResolutionContext = {
|
|
512
|
+
moduleId,
|
|
513
|
+
selfConfig,
|
|
514
|
+
systemConfig: systemConfigMap,
|
|
515
|
+
systemSecrets: systemSecretsMap,
|
|
516
|
+
secrets: secretsMap,
|
|
517
|
+
capabilities: capabilitiesMap,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Snapshot values before derivation so we can detect changes
|
|
521
|
+
const preDeriveValues: Record<string, string> = { ...selfConfig };
|
|
522
|
+
|
|
523
|
+
// Apply declarative derivations from manifest
|
|
524
|
+
applyDeclarativeDerivations(manifest, context);
|
|
525
|
+
|
|
526
|
+
// Persist derived values to database
|
|
527
|
+
// For capability/infrastructure-sourced variables, always update since
|
|
528
|
+
// the upstream value may have changed.
|
|
529
|
+
const rederivableSources = new Set(['capability', 'infrastructure']);
|
|
530
|
+
const declaredVars = manifest.variables?.owns ?? [];
|
|
531
|
+
|
|
532
|
+
for (const [key, value] of Object.entries(context.selfConfig)) {
|
|
533
|
+
const decl = declaredVars.find((v) => v.name === key);
|
|
534
|
+
const isNew = preDeriveValues[key] === undefined;
|
|
535
|
+
const isChanged =
|
|
536
|
+
decl?.source && rederivableSources.has(decl.source) && preDeriveValues[key] !== value;
|
|
537
|
+
|
|
538
|
+
// Don't persist values that still contain unresolved template
|
|
539
|
+
// variables ($self:, $system:, $capability:). This happens when
|
|
540
|
+
// capability data contains template references that can't be
|
|
541
|
+
// fully resolved in the consuming module's context — e.g.,
|
|
542
|
+
// $capability:dns_registrar.primary_domain resolves to
|
|
543
|
+
// namecheap's capability data "$self:primary_domain", but
|
|
544
|
+
// $self: in that context refers to namecheap, not the consumer.
|
|
545
|
+
// Persisting the raw template would poison the config with an
|
|
546
|
+
// unresolvable string. Keep the user-set value (if any) instead.
|
|
547
|
+
if (
|
|
548
|
+
typeof value === 'string' &&
|
|
549
|
+
(value.includes('$self:') ||
|
|
550
|
+
value.includes('$system:') ||
|
|
551
|
+
value.includes('$capability:') ||
|
|
552
|
+
value.includes('$secret:'))
|
|
553
|
+
) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (isNew || isChanged) {
|
|
558
|
+
await db
|
|
559
|
+
.insert(moduleConfigs)
|
|
560
|
+
.values({ moduleId, key, value })
|
|
561
|
+
.onConflictDoUpdate({
|
|
562
|
+
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
563
|
+
set: { value },
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
selfConfig[key] = value;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Auto-derive inventory variables
|
|
572
|
+
const derivedVars = autoDeriveInventoryVariables(moduleId, selfConfig);
|
|
573
|
+
|
|
574
|
+
// Merge derived variables into selfConfig
|
|
575
|
+
// Explicit config takes precedence over derived values
|
|
576
|
+
const finalSelfConfig = { ...derivedVars, ...selfConfig };
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
moduleId,
|
|
580
|
+
selfConfig: finalSelfConfig,
|
|
581
|
+
systemConfig: systemConfigMap,
|
|
582
|
+
systemSecrets: systemSecretsMap,
|
|
583
|
+
secrets: secretsMap,
|
|
584
|
+
capabilities: capabilitiesMap,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Build resolution context from explicit data (for testing)
|
|
590
|
+
*
|
|
591
|
+
* Policy function - no database access
|
|
592
|
+
*
|
|
593
|
+
* @param moduleId - Module ID
|
|
594
|
+
* @param data - Explicit data sources
|
|
595
|
+
* @returns Resolution context
|
|
596
|
+
*/
|
|
597
|
+
export function buildContextFromData(
|
|
598
|
+
moduleId: string,
|
|
599
|
+
data: {
|
|
600
|
+
selfConfig?: Record<string, string>;
|
|
601
|
+
systemConfig?: Record<string, string>;
|
|
602
|
+
systemSecrets?: Record<string, string>;
|
|
603
|
+
secrets?: Record<string, string>;
|
|
604
|
+
capabilities?: Record<string, Record<string, unknown>>;
|
|
605
|
+
} = {},
|
|
606
|
+
): ResolutionContext {
|
|
607
|
+
const selfConfig = data.selfConfig ?? {};
|
|
608
|
+
|
|
609
|
+
// Auto-derive inventory variables (same as buildResolutionContext)
|
|
610
|
+
const derivedVars = autoDeriveInventoryVariables(moduleId, selfConfig);
|
|
611
|
+
|
|
612
|
+
// Merge derived variables into selfConfig
|
|
613
|
+
// Explicit config takes precedence over derived values
|
|
614
|
+
const finalSelfConfig = { ...derivedVars, ...selfConfig };
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
moduleId,
|
|
618
|
+
selfConfig: finalSelfConfig,
|
|
619
|
+
systemConfig: data.systemConfig ?? {},
|
|
620
|
+
systemSecrets: data.systemSecrets ?? {},
|
|
621
|
+
secrets: data.secrets ?? {},
|
|
622
|
+
capabilities: data.capabilities ?? {},
|
|
623
|
+
};
|
|
624
|
+
}
|