@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.
Files changed (267) hide show
  1. package/README.md +1566 -0
  2. package/bin/celilo +16 -0
  3. package/drizzle/0000_complex_puma.sql +179 -0
  4. package/drizzle/0001_dizzy_wolfpack.sql +2 -0
  5. package/drizzle/0002_web_routes.sql +16 -0
  6. package/drizzle/0003_backup_storage.sql +32 -0
  7. package/drizzle/meta/0000_snapshot.json +1151 -0
  8. package/drizzle/meta/0001_snapshot.json +1167 -0
  9. package/drizzle/meta/0002_snapshot.json +1257 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +64 -0
  12. package/schemas/system_config.json +106 -0
  13. package/src/__integration__/container-services-cli.integration.test.ts +246 -0
  14. package/src/ansible/dependencies.test.ts +309 -0
  15. package/src/ansible/dependencies.ts +896 -0
  16. package/src/ansible/inventory.test.ts +463 -0
  17. package/src/ansible/inventory.ts +445 -0
  18. package/src/ansible/secrets.ts +222 -0
  19. package/src/ansible/validation.test.ts +92 -0
  20. package/src/ansible/validation.ts +272 -0
  21. package/src/api-clients/digitalocean.ts +94 -0
  22. package/src/api-clients/proxmox.ts +655 -0
  23. package/src/capabilities/logging-wrapper.test.ts +217 -0
  24. package/src/capabilities/lookup.test.ts +149 -0
  25. package/src/capabilities/lookup.ts +89 -0
  26. package/src/capabilities/public-web-helpers.test.ts +198 -0
  27. package/src/capabilities/public-web-publish.test.ts +458 -0
  28. package/src/capabilities/registration.test.ts +395 -0
  29. package/src/capabilities/registration.ts +200 -0
  30. package/src/capabilities/route-validation.test.ts +121 -0
  31. package/src/capabilities/route-validation.ts +96 -0
  32. package/src/capabilities/secret-ref.test.ts +313 -0
  33. package/src/capabilities/secret-validation.ts +157 -0
  34. package/src/capabilities/secrets.test.ts +750 -0
  35. package/src/capabilities/secrets.ts +244 -0
  36. package/src/capabilities/validation.test.ts +613 -0
  37. package/src/capabilities/validation.ts +160 -0
  38. package/src/capabilities/well-known.test.ts +238 -0
  39. package/src/capabilities/well-known.ts +222 -0
  40. package/src/cli/cli.test.ts +654 -0
  41. package/src/cli/command-registry.ts +742 -0
  42. package/src/cli/command-tree-parser.test.ts +180 -0
  43. package/src/cli/command-tree-parser.ts +193 -0
  44. package/src/cli/commands/backup-create.ts +137 -0
  45. package/src/cli/commands/backup-delete.ts +74 -0
  46. package/src/cli/commands/backup-import.ts +97 -0
  47. package/src/cli/commands/backup-list.ts +132 -0
  48. package/src/cli/commands/backup-name.ts +73 -0
  49. package/src/cli/commands/backup-prune.ts +98 -0
  50. package/src/cli/commands/backup-restore.ts +122 -0
  51. package/src/cli/commands/capability-info.ts +121 -0
  52. package/src/cli/commands/capability-list.ts +47 -0
  53. package/src/cli/commands/completion.ts +87 -0
  54. package/src/cli/commands/hook-run.ts +176 -0
  55. package/src/cli/commands/ipam.ts +607 -0
  56. package/src/cli/commands/machine-add.ts +235 -0
  57. package/src/cli/commands/machine-earmark.ts +82 -0
  58. package/src/cli/commands/machine-list.ts +77 -0
  59. package/src/cli/commands/machine-remove.ts +90 -0
  60. package/src/cli/commands/machine-status.ts +131 -0
  61. package/src/cli/commands/module-audit.ts +51 -0
  62. package/src/cli/commands/module-build.ts +60 -0
  63. package/src/cli/commands/module-config.ts +170 -0
  64. package/src/cli/commands/module-deploy.ts +71 -0
  65. package/src/cli/commands/module-generate.ts +236 -0
  66. package/src/cli/commands/module-health.ts +108 -0
  67. package/src/cli/commands/module-import.ts +80 -0
  68. package/src/cli/commands/module-list.ts +43 -0
  69. package/src/cli/commands/module-logs.ts +73 -0
  70. package/src/cli/commands/module-remove.ts +162 -0
  71. package/src/cli/commands/module-show.ts +208 -0
  72. package/src/cli/commands/module-status.ts +131 -0
  73. package/src/cli/commands/module-types.ts +189 -0
  74. package/src/cli/commands/module-upgrade.ts +192 -0
  75. package/src/cli/commands/package.ts +68 -0
  76. package/src/cli/commands/secret-list.ts +99 -0
  77. package/src/cli/commands/secret-set.ts +134 -0
  78. package/src/cli/commands/service-add-digitalocean.ts +133 -0
  79. package/src/cli/commands/service-add-proxmox.ts +342 -0
  80. package/src/cli/commands/service-config-get.ts +83 -0
  81. package/src/cli/commands/service-config-set.ts +145 -0
  82. package/src/cli/commands/service-list.ts +74 -0
  83. package/src/cli/commands/service-reconfigure.ts +230 -0
  84. package/src/cli/commands/service-remove.ts +103 -0
  85. package/src/cli/commands/service-verify.ts +240 -0
  86. package/src/cli/commands/status.ts +216 -0
  87. package/src/cli/commands/storage-add-local.ts +106 -0
  88. package/src/cli/commands/storage-add-s3.ts +114 -0
  89. package/src/cli/commands/storage-list.ts +72 -0
  90. package/src/cli/commands/storage-remove.ts +54 -0
  91. package/src/cli/commands/storage-set-default.ts +44 -0
  92. package/src/cli/commands/storage-verify.ts +54 -0
  93. package/src/cli/commands/system-config.ts +168 -0
  94. package/src/cli/commands/system-init.ts +314 -0
  95. package/src/cli/commands/system-secret-get.ts +98 -0
  96. package/src/cli/commands/system-secret-set.ts +76 -0
  97. package/src/cli/commands/system-vault-password.ts +34 -0
  98. package/src/cli/completion.test.ts +37 -0
  99. package/src/cli/completion.ts +482 -0
  100. package/src/cli/fuel-gauge.test.ts +208 -0
  101. package/src/cli/fuel-gauge.ts +405 -0
  102. package/src/cli/generate-zsh-completion.test.ts +95 -0
  103. package/src/cli/generate-zsh-completion.ts +497 -0
  104. package/src/cli/index.ts +1583 -0
  105. package/src/cli/interactive-config.test.ts +201 -0
  106. package/src/cli/interactive-config.ts +62 -0
  107. package/src/cli/parser.test.ts +227 -0
  108. package/src/cli/parser.ts +244 -0
  109. package/src/cli/prompts.test.ts +33 -0
  110. package/src/cli/prompts.ts +121 -0
  111. package/src/cli/types.ts +38 -0
  112. package/src/cli/validators.test.ts +235 -0
  113. package/src/cli/validators.ts +188 -0
  114. package/src/config/env.ts +41 -0
  115. package/src/config/paths.test.ts +172 -0
  116. package/src/config/paths.ts +108 -0
  117. package/src/db/client.ts +190 -0
  118. package/src/db/migrate.ts +30 -0
  119. package/src/db/schema.test.ts +221 -0
  120. package/src/db/schema.ts +434 -0
  121. package/src/hooks/capability-loader-firewall.test.ts +246 -0
  122. package/src/hooks/capability-loader.test.ts +100 -0
  123. package/src/hooks/capability-loader.ts +520 -0
  124. package/src/hooks/define-hook.test.ts +488 -0
  125. package/src/hooks/executor.test.ts +462 -0
  126. package/src/hooks/executor.ts +469 -0
  127. package/src/hooks/logger.test.ts +54 -0
  128. package/src/hooks/logger.ts +95 -0
  129. package/src/hooks/test-fixtures/failing-hook.ts +13 -0
  130. package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
  131. package/src/hooks/test-fixtures/success-hook.ts +20 -0
  132. package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
  133. package/src/hooks/test-fixtures/void-hook.ts +13 -0
  134. package/src/hooks/types.ts +89 -0
  135. package/src/infrastructure/property-extractor.test.ts +194 -0
  136. package/src/infrastructure/property-extractor.ts +151 -0
  137. package/src/ipam/allocator.test.ts +442 -0
  138. package/src/ipam/allocator.ts +369 -0
  139. package/src/ipam/auto-allocator.test.ts +247 -0
  140. package/src/ipam/auto-allocator.ts +270 -0
  141. package/src/ipam/subnet-parser.test.ts +107 -0
  142. package/src/ipam/subnet-parser.ts +136 -0
  143. package/src/manifest/contracts/index.ts +61 -0
  144. package/src/manifest/contracts/v1.ts +118 -0
  145. package/src/manifest/json-schema-roundtrip.test.ts +99 -0
  146. package/src/manifest/schema.ts +367 -0
  147. package/src/manifest/template-validator.test.ts +231 -0
  148. package/src/manifest/template-validator.ts +322 -0
  149. package/src/manifest/validate.test.ts +1180 -0
  150. package/src/manifest/validate.ts +415 -0
  151. package/src/module/import.test.ts +355 -0
  152. package/src/module/import.ts +676 -0
  153. package/src/module/packaging/audit.ts +169 -0
  154. package/src/module/packaging/build.ts +228 -0
  155. package/src/module/packaging/checksum.ts +41 -0
  156. package/src/module/packaging/extract.ts +234 -0
  157. package/src/module/packaging/signature.ts +47 -0
  158. package/src/secrets/encryption.test.ts +284 -0
  159. package/src/secrets/encryption.ts +162 -0
  160. package/src/secrets/generators.test.ts +112 -0
  161. package/src/secrets/generators.ts +127 -0
  162. package/src/secrets/master-key.test.ts +159 -0
  163. package/src/secrets/master-key.ts +114 -0
  164. package/src/secrets/storage.test.ts +115 -0
  165. package/src/secrets/storage.ts +106 -0
  166. package/src/secrets/vault.test.ts +35 -0
  167. package/src/secrets/vault.ts +42 -0
  168. package/src/services/backup-create.ts +532 -0
  169. package/src/services/backup-metadata.ts +198 -0
  170. package/src/services/backup-restore.ts +229 -0
  171. package/src/services/backup-retention.ts +84 -0
  172. package/src/services/backup-storage.ts +281 -0
  173. package/src/services/build-stream.test.ts +122 -0
  174. package/src/services/build-stream.ts +201 -0
  175. package/src/services/config-interview.ts +694 -0
  176. package/src/services/container-service.test.ts +298 -0
  177. package/src/services/container-service.ts +401 -0
  178. package/src/services/cross-module-data-manager.test.ts +405 -0
  179. package/src/services/cross-module-data-manager.ts +412 -0
  180. package/src/services/deploy-ansible.ts +88 -0
  181. package/src/services/deploy-planner.ts +153 -0
  182. package/src/services/deploy-preflight.ts +274 -0
  183. package/src/services/deploy-ssh.ts +131 -0
  184. package/src/services/deploy-terraform.test.ts +55 -0
  185. package/src/services/deploy-terraform.ts +445 -0
  186. package/src/services/deploy-validation.ts +311 -0
  187. package/src/services/dns-auto-register.ts +211 -0
  188. package/src/services/health-runner.ts +184 -0
  189. package/src/services/infrastructure-selector.test.ts +485 -0
  190. package/src/services/infrastructure-selector.ts +245 -0
  191. package/src/services/infrastructure-variable-resolver.test.ts +751 -0
  192. package/src/services/infrastructure-variable-resolver.ts +234 -0
  193. package/src/services/machine-detector.ts +328 -0
  194. package/src/services/machine-pool.test.ts +405 -0
  195. package/src/services/machine-pool.ts +316 -0
  196. package/src/services/manifest-validation.ts +120 -0
  197. package/src/services/module-build.test.ts +290 -0
  198. package/src/services/module-build.ts +431 -0
  199. package/src/services/module-config.test.ts +237 -0
  200. package/src/services/module-config.ts +298 -0
  201. package/src/services/module-deploy.ts +862 -0
  202. package/src/services/module-types-drift.test.ts +73 -0
  203. package/src/services/module-types-generator.test.ts +288 -0
  204. package/src/services/module-types-generator.ts +189 -0
  205. package/src/services/proxmox-state-recovery.ts +140 -0
  206. package/src/services/schema-validation.ts +155 -0
  207. package/src/services/secret-schema-loader.test.ts +311 -0
  208. package/src/services/secret-schema-loader.ts +239 -0
  209. package/src/services/ssh-key-manager.test.ts +283 -0
  210. package/src/services/ssh-key-manager.ts +193 -0
  211. package/src/services/storage-providers/local.ts +105 -0
  212. package/src/services/storage-providers/s3.ts +182 -0
  213. package/src/services/storage-providers/types.ts +24 -0
  214. package/src/services/system-config-schema-types.ts +25 -0
  215. package/src/services/system-config-validator.test.ts +160 -0
  216. package/src/services/system-config-validator.ts +74 -0
  217. package/src/services/system-init.test.ts +153 -0
  218. package/src/services/system-init.ts +253 -0
  219. package/src/services/terraform-safety.ts +174 -0
  220. package/src/services/zone-detector.test.ts +110 -0
  221. package/src/services/zone-detector.ts +102 -0
  222. package/src/services/zone-policy.test.ts +97 -0
  223. package/src/services/zone-policy.ts +126 -0
  224. package/src/templates/generator.test.ts +645 -0
  225. package/src/templates/generator.ts +1119 -0
  226. package/src/templates/types.ts +62 -0
  227. package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
  228. package/src/test-utils/cli-context-interactive.test.ts +152 -0
  229. package/src/test-utils/cli-context-server.test.ts +66 -0
  230. package/src/test-utils/cli-context.test.ts +273 -0
  231. package/src/test-utils/cli-context.ts +677 -0
  232. package/src/test-utils/cli-result.test.ts +282 -0
  233. package/src/test-utils/cli-result.ts +241 -0
  234. package/src/test-utils/cli.ts +55 -0
  235. package/src/test-utils/completion-harness.test.ts +126 -0
  236. package/src/test-utils/completion-harness.ts +82 -0
  237. package/src/test-utils/database.test.ts +182 -0
  238. package/src/test-utils/database.ts +126 -0
  239. package/src/test-utils/filesystem.test.ts +208 -0
  240. package/src/test-utils/filesystem.ts +142 -0
  241. package/src/test-utils/fixtures.test.ts +123 -0
  242. package/src/test-utils/fixtures.ts +160 -0
  243. package/src/test-utils/golden-diff.ts +197 -0
  244. package/src/test-utils/index.ts +77 -0
  245. package/src/test-utils/integration.ts +81 -0
  246. package/src/test-utils/module-fixtures.ts +468 -0
  247. package/src/test-utils/modules.test.ts +144 -0
  248. package/src/test-utils/modules.ts +183 -0
  249. package/src/test-utils/setup-test-db.ts +90 -0
  250. package/src/test-utils/value-extractor.test.ts +231 -0
  251. package/src/test-utils/value-extractor.ts +228 -0
  252. package/src/types/infrastructure.ts +157 -0
  253. package/src/utils/shell.test.ts +365 -0
  254. package/src/utils/shell.ts +159 -0
  255. package/src/validation/schemas.ts +166 -0
  256. package/src/variables/ansible-resolver.test.ts +142 -0
  257. package/src/variables/ansible-resolver.ts +69 -0
  258. package/src/variables/capability-self-ref.test.ts +220 -0
  259. package/src/variables/context.test.ts +1265 -0
  260. package/src/variables/context.ts +624 -0
  261. package/src/variables/declarative-derivation.test.ts +743 -0
  262. package/src/variables/declarative-derivation.ts +200 -0
  263. package/src/variables/parser.test.ts +231 -0
  264. package/src/variables/parser.ts +76 -0
  265. package/src/variables/resolver.test.ts +458 -0
  266. package/src/variables/resolver.ts +282 -0
  267. package/src/variables/types.ts +59 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Hook type definitions
3
+ *
4
+ * Types for the module lifecycle hook system.
5
+ * Hooks allow modules to execute custom logic at deployment milestones
6
+ * (e.g., container_created triggers web automation for API key creation).
7
+ */
8
+
9
+ /**
10
+ * Hook definition from module manifest.
11
+ *
12
+ * Inputs and outputs are no longer carried by the manifest — they live in the
13
+ * contract registry (see `apps/celilo/src/manifest/contracts/v1.ts`),
14
+ * keyed by the manifest's `celilo_contract` version. The executor resolves
15
+ * the signature at runtime.
16
+ */
17
+ export interface HookDefinition {
18
+ /** Path to hook script relative to module directory */
19
+ script: string;
20
+ /** Total timeout in milliseconds (default: 60000) */
21
+ timeout?: number;
22
+ /** If true, hook needs interactive terminal (no FuelGauge wrapping) */
23
+ interactive?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Logger interface provided to hook scripts
28
+ */
29
+ export interface HookLogger {
30
+ info(message: string): void;
31
+ warn(message: string): void;
32
+ error(message: string): void;
33
+ success(message: string): void;
34
+ }
35
+
36
+ /**
37
+ * Context passed to hook scripts
38
+ */
39
+ export interface HookContext {
40
+ /** Module configuration values */
41
+ config: Record<string, unknown>;
42
+ /** Module secret values (decrypted) */
43
+ secrets: Record<string, string>;
44
+ /** Logger for reporting progress */
45
+ logger: HookLogger;
46
+ /** Run in debug mode (e.g., Playwright headless: false) */
47
+ debug: boolean;
48
+ /** Directory for saving screenshots on failure */
49
+ screenshotDir: string;
50
+ /** Capability function interfaces from provider modules (e.g., context.capabilities.dns_registrar) */
51
+ capabilities: Record<string, unknown>;
52
+ /** Dynamic inputs specified in manifest (e.g., vps_ip) */
53
+ [key: string]: unknown;
54
+ }
55
+
56
+ /**
57
+ * Result returned from hook execution
58
+ */
59
+ export interface HookResult {
60
+ success: boolean;
61
+ outputs: Record<string, unknown>;
62
+ error?: string;
63
+ /** Path to failure screenshot, if captured */
64
+ screenshotPath?: string;
65
+ /** Duration in milliseconds */
66
+ duration: number;
67
+ }
68
+
69
+ /**
70
+ * Supported lifecycle hook names.
71
+ *
72
+ * Must stay in sync with the canonical hook list in
73
+ * `apps/celilo/src/manifest/contracts/v1.ts` and with the schema's
74
+ * `hooks` block in `apps/celilo/src/manifest/schema.ts`.
75
+ */
76
+ export type HookName =
77
+ | 'container_created'
78
+ | 'on_install'
79
+ | 'on_uninstall'
80
+ | 'health_check'
81
+ | 'validate_config'
82
+ | 'on_backup'
83
+ | 'on_backup_analyze'
84
+ | 'on_restore';
85
+
86
+ /**
87
+ * Hook manifest section - maps hook names to definitions
88
+ */
89
+ export type HookManifest = Partial<Record<HookName, HookDefinition>>;
@@ -0,0 +1,194 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { Machine } from '@/types/infrastructure';
3
+ import {
4
+ type ProxmoxProviderConfig,
5
+ extractMachineProperties,
6
+ extractProxmoxProperties,
7
+ extractTerraformProperties,
8
+ } from './property-extractor';
9
+
10
+ // Test fixture for Proxmox provider config
11
+ const mockProxmoxConfig: ProxmoxProviderConfig = {
12
+ default_target_node: 'node2',
13
+ lxc_template: 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst',
14
+ storage: 'local-lvm',
15
+ };
16
+
17
+ describe('extractMachineProperties', () => {
18
+ test('extracts properties from machine record', () => {
19
+ const machine: Machine = {
20
+ id: 'machine-123',
21
+ hostname: 'dns-ext',
22
+ ipAddress: '203.0.113.42',
23
+ sshUser: 'root',
24
+ sshKeyEncrypted: 'encrypted-key',
25
+ hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 20 },
26
+ zone: 'external',
27
+ role: 'host',
28
+ interfaces: [],
29
+ assignedModuleIds: [],
30
+ createdAt: new Date(),
31
+ updatedAt: new Date(),
32
+ };
33
+
34
+ const properties = extractMachineProperties(machine);
35
+
36
+ expect(properties).toEqual({
37
+ 'ip.primary': '203.0.113.42',
38
+ hostname: 'dns-ext',
39
+ id: 'machine-123',
40
+ });
41
+ });
42
+
43
+ test('translates from camelCase to dot notation', () => {
44
+ const machine: Machine = {
45
+ id: 'machine-456',
46
+ hostname: 'test-host',
47
+ ipAddress: '192.168.1.100', // camelCase in code
48
+ sshUser: 'ubuntu',
49
+ sshKeyEncrypted: 'encrypted',
50
+ hardware: { cpu_cores: 1, memory_mb: 1024, disk_gb: 10 },
51
+ zone: 'internal',
52
+ role: 'host',
53
+ interfaces: [],
54
+ assignedModuleIds: [],
55
+ createdAt: new Date(),
56
+ updatedAt: new Date(),
57
+ };
58
+
59
+ const properties = extractMachineProperties(machine);
60
+
61
+ // Verify camelCase → dot notation translation
62
+ expect(properties['ip.primary']).toBe('192.168.1.100');
63
+ expect(properties.ipAddress).toBeUndefined(); // Should not have camelCase key
64
+ });
65
+ });
66
+
67
+ describe('extractProxmoxProperties', () => {
68
+ test('extracts properties from IPAM allocation', () => {
69
+ const properties = extractProxmoxProperties(100, '10.0.10.5', 'homebridge', mockProxmoxConfig);
70
+
71
+ expect(properties).toEqual({
72
+ 'ip.primary': '10.0.10.5',
73
+ hostname: 'homebridge',
74
+ id: '100',
75
+ target_node: 'node2',
76
+ lxc_template: 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst',
77
+ storage: 'local-lvm',
78
+ });
79
+ });
80
+
81
+ test('converts vmid number to string', () => {
82
+ const properties = extractProxmoxProperties(12345, '10.0.20.15', 'caddy', mockProxmoxConfig);
83
+
84
+ expect(properties.id).toBe('12345');
85
+ expect(typeof properties.id).toBe('string');
86
+ });
87
+
88
+ test('uses container IP as primary IP', () => {
89
+ const properties = extractProxmoxProperties(100, '10.0.10.5', 'test', mockProxmoxConfig);
90
+
91
+ // Container IP (from IPAM) becomes the primary IP
92
+ expect(properties['ip.primary']).toBe('10.0.10.5');
93
+ });
94
+
95
+ test('includes provider config properties', () => {
96
+ const properties = extractProxmoxProperties(100, '10.0.10.5', 'test', mockProxmoxConfig);
97
+
98
+ expect(properties.target_node).toBe('node2');
99
+ expect(properties.lxc_template).toBe(
100
+ 'local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst',
101
+ );
102
+ expect(properties.storage).toBe('local-lvm');
103
+ });
104
+ });
105
+
106
+ describe('extractTerraformProperties', () => {
107
+ test('extracts properties from unwrapped Terraform output', () => {
108
+ const terraformOutputs = {
109
+ droplet_ip: '203.0.113.42',
110
+ droplet_id: '123456789',
111
+ };
112
+
113
+ const properties = extractTerraformProperties(terraformOutputs, 'dns-external');
114
+
115
+ expect(properties).toEqual({
116
+ 'ip.primary': '203.0.113.42',
117
+ hostname: 'dns-external',
118
+ id: '123456789',
119
+ });
120
+ });
121
+
122
+ test('extracts properties from wrapped Terraform output format', () => {
123
+ // Terraform output -json format wraps values in objects
124
+ const terraformOutputs = {
125
+ droplet_ip: {
126
+ value: '198.51.100.50',
127
+ type: 'string',
128
+ sensitive: false,
129
+ },
130
+ droplet_id: {
131
+ value: '987654321',
132
+ type: 'string',
133
+ sensitive: false,
134
+ },
135
+ };
136
+
137
+ const properties = extractTerraformProperties(terraformOutputs, 'web-server');
138
+
139
+ expect(properties).toEqual({
140
+ 'ip.primary': '198.51.100.50',
141
+ hostname: 'web-server',
142
+ id: '987654321',
143
+ });
144
+ });
145
+
146
+ test('handles numeric droplet ID', () => {
147
+ const terraformOutputs = {
148
+ droplet_ip: '203.0.113.42',
149
+ droplet_id: {
150
+ value: 123456789, // Number instead of string
151
+ type: 'number',
152
+ sensitive: false,
153
+ },
154
+ };
155
+
156
+ const properties = extractTerraformProperties(terraformOutputs, 'test');
157
+
158
+ expect(properties.id).toBe('123456789');
159
+ expect(typeof properties.id).toBe('string');
160
+ });
161
+
162
+ test('throws on missing droplet_ip', () => {
163
+ const terraformOutputs = {
164
+ droplet_id: '123456789',
165
+ // droplet_ip missing
166
+ };
167
+
168
+ expect(() => extractTerraformProperties(terraformOutputs, 'test')).toThrow(
169
+ 'Missing required Terraform outputs',
170
+ );
171
+ });
172
+
173
+ test('throws on missing droplet_id', () => {
174
+ const terraformOutputs = {
175
+ droplet_ip: '203.0.113.42',
176
+ // droplet_id missing
177
+ };
178
+
179
+ expect(() => extractTerraformProperties(terraformOutputs, 'test')).toThrow(
180
+ 'Missing required Terraform outputs',
181
+ );
182
+ });
183
+
184
+ test('throws on invalid output format', () => {
185
+ const terraformOutputs = {
186
+ droplet_ip: { invalid: 'format' }, // Missing 'value' key
187
+ droplet_id: '123456789',
188
+ };
189
+
190
+ expect(() => extractTerraformProperties(terraformOutputs, 'test')).toThrow(
191
+ 'Invalid Terraform output format',
192
+ );
193
+ });
194
+ });
@@ -0,0 +1,151 @@
1
+ import type { Machine } from '@/types/infrastructure';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * Zod schema for Terraform output values
6
+ * Terraform can return plain values or wrapped in { value, type, sensitive }
7
+ * Validates external data from terraform output -json (Rule 3.7)
8
+ */
9
+ const TerraformOutputValueSchema = z.union([
10
+ z.string(),
11
+ z.number(),
12
+ z.object({
13
+ value: z.union([z.string(), z.number()]),
14
+ type: z.string().optional(),
15
+ sensitive: z.boolean().optional(),
16
+ }),
17
+ ]);
18
+
19
+ /**
20
+ * Zod schema for Digital Ocean Terraform outputs
21
+ * Expects droplet_ip and droplet_id outputs from Terraform
22
+ */
23
+ const DigitalOceanOutputSchema = z.object({
24
+ droplet_ip: TerraformOutputValueSchema,
25
+ droplet_id: TerraformOutputValueSchema,
26
+ });
27
+
28
+ /**
29
+ * Extract infrastructure properties from a machine.
30
+ * Translates from TypeScript camelCase to dot notation user-facing format.
31
+ *
32
+ * @param machine - Machine record from database
33
+ * @returns Infrastructure properties with dot notation keys
34
+ */
35
+ export function extractMachineProperties(machine: Machine): Record<string, string> {
36
+ return {
37
+ 'ip.primary': machine.ipAddress,
38
+ hostname: machine.hostname,
39
+ id: machine.id,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Proxmox provider configuration shape
45
+ */
46
+ export interface ProxmoxProviderConfig {
47
+ default_target_node: string;
48
+ lxc_template: string;
49
+ storage: string;
50
+ }
51
+
52
+ /**
53
+ * Extract infrastructure properties from IPAM allocation (Proxmox).
54
+ * Used when container service allocates IP via IPAM before Terraform runs.
55
+ *
56
+ * @param vmid - Proxmox VMID allocated by IPAM
57
+ * @param containerIp - Container IP allocated by IPAM
58
+ * @param hostname - Container hostname
59
+ * @param providerConfig - Proxmox provider configuration from container service
60
+ * @returns Infrastructure properties with dot notation keys
61
+ */
62
+ export function extractProxmoxProperties(
63
+ vmid: number,
64
+ containerIp: string,
65
+ hostname: string,
66
+ providerConfig: ProxmoxProviderConfig,
67
+ ): Record<string, string> {
68
+ return {
69
+ 'ip.primary': containerIp,
70
+ hostname: hostname,
71
+ id: vmid.toString(),
72
+ target_node: providerConfig.default_target_node,
73
+ lxc_template: providerConfig.lxc_template,
74
+ storage: providerConfig.storage,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Extract infrastructure properties from Terraform outputs (Digital Ocean).
80
+ * Parses Terraform's JSON output format which can wrap values in objects.
81
+ * Validates output structure before extraction (Rule 3.7).
82
+ *
83
+ * @param outputs - Terraform output JSON (from `terraform output -json`)
84
+ * @param hostname - Container hostname
85
+ * @returns Infrastructure properties with dot notation keys
86
+ */
87
+ export function extractTerraformProperties(
88
+ outputs: Record<string, unknown>,
89
+ hostname: string,
90
+ ): Record<string, string> {
91
+ // Validate structure matches Digital Ocean expectations
92
+ let validated: z.infer<typeof DigitalOceanOutputSchema>;
93
+ try {
94
+ validated = DigitalOceanOutputSchema.parse(outputs);
95
+ } catch (error) {
96
+ if (error instanceof z.ZodError) {
97
+ // Check for missing top-level fields (completely absent from outputs)
98
+ const topLevelErrors = error.errors.filter((e) => e.path.length === 1);
99
+ const hasUndefinedFields = topLevelErrors.some((e) => {
100
+ if (e.code === 'invalid_union') {
101
+ // For union errors, check if the field is actually undefined
102
+ const fieldName = e.path[0] as string;
103
+ return !(fieldName in outputs); // Field is truly missing
104
+ }
105
+ return e.code === 'invalid_type' && e.received === 'undefined';
106
+ });
107
+
108
+ if (hasUndefinedFields) {
109
+ throw new Error(
110
+ 'Missing required Terraform outputs: droplet_ip and droplet_id must be defined',
111
+ );
112
+ }
113
+
114
+ // Invalid format (field exists but doesn't match expected types)
115
+ throw new Error(`Invalid Terraform output format: ${JSON.stringify(outputs)}`);
116
+ }
117
+ throw error;
118
+ }
119
+
120
+ // Terraform output format can be:
121
+ // { "droplet_ip": "203.0.113.42" } OR
122
+ // { "droplet_ip": { "value": "203.0.113.42", "type": "string", "sensitive": false } }
123
+
124
+ const unwrapOutput = (value: unknown): string => {
125
+ if (typeof value === 'string') {
126
+ return value;
127
+ }
128
+ if (typeof value === 'number') {
129
+ return value.toString();
130
+ }
131
+ if (typeof value === 'object' && value !== null && 'value' in value) {
132
+ const wrapped = value as { value: unknown };
133
+ if (typeof wrapped.value === 'string') {
134
+ return wrapped.value;
135
+ }
136
+ if (typeof wrapped.value === 'number') {
137
+ return wrapped.value.toString();
138
+ }
139
+ }
140
+ throw new Error(`Invalid Terraform output format: ${JSON.stringify(value)}`);
141
+ };
142
+
143
+ const dropletIp = unwrapOutput(validated.droplet_ip);
144
+ const dropletId = unwrapOutput(validated.droplet_id);
145
+
146
+ return {
147
+ 'ip.primary': dropletIp,
148
+ hostname: hostname,
149
+ id: dropletId,
150
+ };
151
+ }