@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,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
|
+
}
|