@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,445 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { log } from '../cli/prompts';
|
|
3
|
+
import { executeBuildWithProgress } from './build-stream';
|
|
4
|
+
import { validateTerraformPlanSafety } from './terraform-safety';
|
|
5
|
+
|
|
6
|
+
export interface TerraformResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
output: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
exitCode?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute Terraform workflow with safety validation and streaming progress
|
|
15
|
+
* Execution function - performs infrastructure provisioning
|
|
16
|
+
*
|
|
17
|
+
* @param generatedPath - Path to generated module artifacts
|
|
18
|
+
* @param phases - Phase tracking object to update during execution
|
|
19
|
+
* @param extraEnvVars - Optional environment variables (e.g., TF_VAR_* for credentials)
|
|
20
|
+
* @returns Terraform execution result
|
|
21
|
+
*/
|
|
22
|
+
export async function executeTerraform(
|
|
23
|
+
generatedPath: string,
|
|
24
|
+
phases?: {
|
|
25
|
+
terraformInit?: boolean;
|
|
26
|
+
terraformPlan?: boolean;
|
|
27
|
+
terraformApply?: boolean;
|
|
28
|
+
},
|
|
29
|
+
extraEnvVars?: Record<string, string>,
|
|
30
|
+
options?: { noInteractive?: boolean },
|
|
31
|
+
): Promise<TerraformResult> {
|
|
32
|
+
const terraformDir = join(generatedPath, 'terraform');
|
|
33
|
+
|
|
34
|
+
// Build environment variables for Terraform
|
|
35
|
+
const terraformEnv: Record<string, string> = {
|
|
36
|
+
TF_IN_AUTOMATION: '1', // Disable interactive prompts
|
|
37
|
+
...extraEnvVars, // Merge in any additional env vars (e.g., TF_VAR_* credentials)
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const noInteractive = options?.noInteractive;
|
|
41
|
+
|
|
42
|
+
// 1. Always run terraform init -upgrade
|
|
43
|
+
// This ensures provider versions are updated when templates change
|
|
44
|
+
// It's idempotent and safe to run on every deployment
|
|
45
|
+
const initResult = await executeTerraformCommand(
|
|
46
|
+
'init',
|
|
47
|
+
['-upgrade'],
|
|
48
|
+
terraformDir,
|
|
49
|
+
'Initializing Terraform',
|
|
50
|
+
terraformEnv,
|
|
51
|
+
noInteractive,
|
|
52
|
+
);
|
|
53
|
+
if (!initResult.success) {
|
|
54
|
+
if (phases) phases.terraformInit = false;
|
|
55
|
+
const errorMessage = parseTerraformError(initResult.output);
|
|
56
|
+
return { ...initResult, error: errorMessage };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (phases) phases.terraformInit = true;
|
|
60
|
+
|
|
61
|
+
// 2. Run terraform plan with streaming progress
|
|
62
|
+
let planResult = await executeBuildWithProgress({
|
|
63
|
+
command: 'terraform',
|
|
64
|
+
args: ['plan', '-detailed-exitcode', '-no-color'],
|
|
65
|
+
cwd: terraformDir,
|
|
66
|
+
title: 'Planning infrastructure',
|
|
67
|
+
env: terraformEnv,
|
|
68
|
+
noInteractive,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Auto-recover from stale state lock (e.g., interrupted previous deploy)
|
|
72
|
+
if (!planResult.success && planResult.output.includes('Error acquiring the state lock')) {
|
|
73
|
+
const unlocked = await autoForceUnlock(
|
|
74
|
+
terraformDir,
|
|
75
|
+
planResult.output,
|
|
76
|
+
terraformEnv,
|
|
77
|
+
noInteractive,
|
|
78
|
+
);
|
|
79
|
+
if (unlocked) {
|
|
80
|
+
log.info('Retrying plan after lock recovery...');
|
|
81
|
+
planResult = await executeBuildWithProgress({
|
|
82
|
+
command: 'terraform',
|
|
83
|
+
args: ['plan', '-detailed-exitcode', '-no-color'],
|
|
84
|
+
cwd: terraformDir,
|
|
85
|
+
title: 'Planning infrastructure',
|
|
86
|
+
env: terraformEnv,
|
|
87
|
+
noInteractive,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Terraform plan exit codes:
|
|
93
|
+
// 0 = no changes
|
|
94
|
+
// 1 = error
|
|
95
|
+
// 2 = changes present (success!)
|
|
96
|
+
const planExitCode = planResult.exitCode;
|
|
97
|
+
|
|
98
|
+
if (planExitCode !== 0 && planExitCode !== 2) {
|
|
99
|
+
if (phases) phases.terraformPlan = false;
|
|
100
|
+
|
|
101
|
+
// Parse common errors into clear messages
|
|
102
|
+
const errorMessage = parseTerraformError(planResult.output);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
output: planResult.output,
|
|
107
|
+
error: errorMessage,
|
|
108
|
+
exitCode: planExitCode,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (phases) phases.terraformPlan = true;
|
|
113
|
+
|
|
114
|
+
// Check exit code: 0 = no changes, 2 = changes present
|
|
115
|
+
if (planExitCode === 0) {
|
|
116
|
+
log.message(' No changes needed');
|
|
117
|
+
if (phases) phases.terraformApply = true;
|
|
118
|
+
return { success: true, output: planResult.output, exitCode: 0 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Exit code 2 = changes present, validate safety
|
|
122
|
+
const safetyCheck = validateTerraformPlanSafety(planResult.output);
|
|
123
|
+
if (!safetyCheck.safe) {
|
|
124
|
+
if (phases) phases.terraformApply = false;
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
output: planResult.output,
|
|
128
|
+
error: safetyCheck.error,
|
|
129
|
+
exitCode: 2,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 3. Run terraform apply with streaming progress
|
|
134
|
+
let applyResult = await executeTerraformCommand(
|
|
135
|
+
'apply',
|
|
136
|
+
['-auto-approve'],
|
|
137
|
+
terraformDir,
|
|
138
|
+
'Applying infrastructure',
|
|
139
|
+
terraformEnv,
|
|
140
|
+
noInteractive,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Auto-recover from stale state lock during apply
|
|
144
|
+
if (!applyResult.success && applyResult.output.includes('Error acquiring the state lock')) {
|
|
145
|
+
const unlocked = await autoForceUnlock(
|
|
146
|
+
terraformDir,
|
|
147
|
+
applyResult.output,
|
|
148
|
+
terraformEnv,
|
|
149
|
+
noInteractive,
|
|
150
|
+
);
|
|
151
|
+
if (unlocked) {
|
|
152
|
+
log.info('Retrying apply after lock recovery...');
|
|
153
|
+
applyResult = await executeTerraformCommand(
|
|
154
|
+
'apply',
|
|
155
|
+
['-auto-approve'],
|
|
156
|
+
terraformDir,
|
|
157
|
+
'Applying infrastructure',
|
|
158
|
+
terraformEnv,
|
|
159
|
+
noInteractive,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!applyResult.success) {
|
|
165
|
+
// Check if failure is due to "already exists" - this happens when Terraform
|
|
166
|
+
// loses connection during creation but the resource was actually created
|
|
167
|
+
if (applyResult.output.includes('already exists')) {
|
|
168
|
+
log.warn('Resource already exists - attempting automatic recovery...');
|
|
169
|
+
|
|
170
|
+
// Try to import existing resource into state
|
|
171
|
+
const importResult = await attemptAutoImport(terraformDir, applyResult.output, terraformEnv);
|
|
172
|
+
|
|
173
|
+
if (importResult.success) {
|
|
174
|
+
log.success('Resource imported into Terraform state');
|
|
175
|
+
log.message(' Retrying deployment...');
|
|
176
|
+
|
|
177
|
+
// Retry apply after successful import
|
|
178
|
+
const retryResult = await executeTerraformCommand(
|
|
179
|
+
'apply',
|
|
180
|
+
['-auto-approve'],
|
|
181
|
+
terraformDir,
|
|
182
|
+
'Applying infrastructure',
|
|
183
|
+
terraformEnv,
|
|
184
|
+
noInteractive,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (!retryResult.success) {
|
|
188
|
+
if (phases) phases.terraformApply = false;
|
|
189
|
+
return retryResult;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (phases) phases.terraformApply = true;
|
|
193
|
+
log.success('Infrastructure deployed (recovered from state drift)');
|
|
194
|
+
return retryResult;
|
|
195
|
+
}
|
|
196
|
+
log.error('Automatic recovery failed - manual import required');
|
|
197
|
+
if (phases) phases.terraformApply = false;
|
|
198
|
+
return {
|
|
199
|
+
...applyResult,
|
|
200
|
+
error: `${applyResult.error}\n\nAutomatic recovery failed. Manual fix:\ncd "${terraformDir}"\nterraform import ${importResult.suggestion || '<resource>'}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (phases) phases.terraformApply = false;
|
|
205
|
+
return applyResult;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (phases) phases.terraformApply = true;
|
|
209
|
+
log.success('Infrastructure deployed');
|
|
210
|
+
return applyResult;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Attempt to automatically import a resource that already exists
|
|
215
|
+
* Parses Terraform error output to determine resource type and ID, then imports it
|
|
216
|
+
*
|
|
217
|
+
* @param terraformDir - Terraform working directory
|
|
218
|
+
* @param errorOutput - Terraform error output containing "already exists"
|
|
219
|
+
* @param terraformEnv - Environment variables including provider credentials
|
|
220
|
+
* @returns Import result with success status and suggestion
|
|
221
|
+
*/
|
|
222
|
+
async function attemptAutoImport(
|
|
223
|
+
terraformDir: string,
|
|
224
|
+
errorOutput: string,
|
|
225
|
+
terraformEnv: Record<string, string>,
|
|
226
|
+
): Promise<{ success: boolean; suggestion?: string }> {
|
|
227
|
+
// Parse error message to extract resource info
|
|
228
|
+
// Example: "CT 200 already exists on node 'node2'"
|
|
229
|
+
// Example: "with proxmox_lxc.homebridge"
|
|
230
|
+
|
|
231
|
+
const resourceMatch = errorOutput.match(/with\s+([\w_]+\.\w+)/);
|
|
232
|
+
if (!resourceMatch) {
|
|
233
|
+
return { success: false, suggestion: '<resource> <id>' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const resourceName = resourceMatch[1]; // e.g., "proxmox_lxc.homebridge"
|
|
237
|
+
|
|
238
|
+
// Try to extract resource ID from error message
|
|
239
|
+
let resourceId: string | null = null;
|
|
240
|
+
|
|
241
|
+
// LXC container: "CT 200 already exists on node 'node2'" -> "node2/lxc/200"
|
|
242
|
+
const lxcMatch = errorOutput.match(/CT (\d+) already exists on node '(\w+)'/);
|
|
243
|
+
if (lxcMatch) {
|
|
244
|
+
const vmid = lxcMatch[1];
|
|
245
|
+
const node = lxcMatch[2];
|
|
246
|
+
resourceId = `${node}/lxc/${vmid}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// VM: "VM 100 already exists" -> need to determine format
|
|
250
|
+
// Add more patterns as needed for other resource types
|
|
251
|
+
|
|
252
|
+
if (!resourceId) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
suggestion: `${resourceName} <id>`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Attempt the import
|
|
260
|
+
log.info(` Importing ${resourceName} as ${resourceId}...`);
|
|
261
|
+
|
|
262
|
+
const result = await executeBuildWithProgress({
|
|
263
|
+
command: 'terraform',
|
|
264
|
+
args: ['import', resourceName, resourceId],
|
|
265
|
+
cwd: terraformDir,
|
|
266
|
+
title: 'Importing existing resource',
|
|
267
|
+
env: {
|
|
268
|
+
...terraformEnv,
|
|
269
|
+
TF_IN_AUTOMATION: '1',
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
success: result.success,
|
|
275
|
+
suggestion: `${resourceName} ${resourceId}`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Execute a single Terraform command with streaming output
|
|
281
|
+
* Execution function - runs terraform command with fuel-gauge progress
|
|
282
|
+
*
|
|
283
|
+
* @param command - Terraform subcommand (init, apply, etc.)
|
|
284
|
+
* @param args - Additional arguments
|
|
285
|
+
* @param cwd - Working directory
|
|
286
|
+
* @param title - Progress indicator title
|
|
287
|
+
* @param env - Environment variables
|
|
288
|
+
* @returns Execution result
|
|
289
|
+
*/
|
|
290
|
+
/**
|
|
291
|
+
* Auto-recover from a stale Terraform state lock.
|
|
292
|
+
* Parses the lock ID from the error output and runs force-unlock.
|
|
293
|
+
*/
|
|
294
|
+
async function autoForceUnlock(
|
|
295
|
+
terraformDir: string,
|
|
296
|
+
errorOutput: string,
|
|
297
|
+
terraformEnv: Record<string, string>,
|
|
298
|
+
noInteractive?: boolean,
|
|
299
|
+
): Promise<boolean> {
|
|
300
|
+
const lockIdMatch = errorOutput.match(/ID:\s+([0-9a-f-]+)/);
|
|
301
|
+
if (!lockIdMatch) {
|
|
302
|
+
log.warn('State lock detected but could not parse lock ID');
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const lockId = lockIdMatch[1];
|
|
307
|
+
log.warn(`Stale state lock detected (${lockId}) -- auto-recovering...`);
|
|
308
|
+
|
|
309
|
+
const result = await executeBuildWithProgress({
|
|
310
|
+
command: 'terraform',
|
|
311
|
+
args: ['force-unlock', '-force', lockId],
|
|
312
|
+
cwd: terraformDir,
|
|
313
|
+
title: 'Removing stale state lock',
|
|
314
|
+
env: { ...terraformEnv, TF_IN_AUTOMATION: '1' },
|
|
315
|
+
noInteractive,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
if (result.success) {
|
|
319
|
+
log.success('State lock removed');
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
log.error(`Failed to remove state lock: ${result.error || result.output}`);
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Parse Terraform error output into a clear, actionable message
|
|
329
|
+
*/
|
|
330
|
+
function parseTerraformError(output: string): string {
|
|
331
|
+
// Proxmox unreachable
|
|
332
|
+
if (output.includes('dial tcp') && output.includes('connect: operation timed out')) {
|
|
333
|
+
const ipMatch = output.match(/dial tcp ([^:]+:\d+)/);
|
|
334
|
+
const target = ipMatch ? ipMatch[1] : 'unknown';
|
|
335
|
+
return `Proxmox server unreachable at ${target}\n\nCheck:\n - Is the Proxmox server running?\n - Can this machine reach ${target}?\n - Is a firewall blocking the connection?`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Proxmox auth error
|
|
339
|
+
if (output.includes('401') && output.includes('proxmox')) {
|
|
340
|
+
return 'Proxmox authentication failed\n\nCheck:\n - API token ID and secret in service config\n - Token permissions in Proxmox';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Digital Ocean auth error
|
|
344
|
+
if (output.includes('401') && output.includes('digitalocean')) {
|
|
345
|
+
return 'Digital Ocean authentication failed\n\nCheck:\n - API token in service config\n - Token has write permissions';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Connection refused
|
|
349
|
+
if (output.includes('connection refused')) {
|
|
350
|
+
const ipMatch = output.match(/dial tcp ([^:]+:\d+)/);
|
|
351
|
+
const target = ipMatch ? ipMatch[1] : 'unknown';
|
|
352
|
+
return `Connection refused to ${target}\n\nThe server is not accepting connections on that port.`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// DNS resolution failure
|
|
356
|
+
if (output.includes('no such host') || output.includes('could not resolve')) {
|
|
357
|
+
return 'DNS resolution failed for the infrastructure provider\n\nCheck your network connection and DNS settings.';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Proxmox permission denied (e.g., API token can't set certain LXC feature flags)
|
|
361
|
+
if (output.includes('Permission check failed') && output.includes('proxmox')) {
|
|
362
|
+
const reasonMatch = output.match(/Permission check failed \(([^)]+)\)/);
|
|
363
|
+
const reason = reasonMatch ? reasonMatch[1] : 'insufficient permissions';
|
|
364
|
+
return `Proxmox permission denied: ${reason}\n\nYour API token lacks the required privileges.\n\nCheck:\n - Token permissions in Proxmox (Datacenter → Permissions → API Tokens)\n - Some operations (e.g., LXC feature flags like keyctl) require root@pam\n - Consider removing unsupported features from the module's Terraform template`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Terraform registry unreachable (provider download failure during init)
|
|
368
|
+
if (
|
|
369
|
+
output.includes('registry.terraform.io') &&
|
|
370
|
+
(output.includes('request canceled') ||
|
|
371
|
+
output.includes('Timeout exceeded') ||
|
|
372
|
+
output.includes('could not connect'))
|
|
373
|
+
) {
|
|
374
|
+
return 'Cannot reach Terraform registry (registry.terraform.io)\n\nTerraform needs internet access to download provider plugins.\n\nCheck:\n - Network connectivity from the machine running Celilo\n - DNS resolution: nslookup registry.terraform.io\n - Firewall rules allowing outbound HTTPS (port 443)';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Generic: extract the Error: line
|
|
378
|
+
const errorLineMatch = output.match(/Error: (.+?)(?:\n|$)/);
|
|
379
|
+
if (errorLineMatch) {
|
|
380
|
+
return errorLineMatch[1].trim();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return `Terraform failed:\n${output.substring(0, 300)}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function executeTerraformCommand(
|
|
387
|
+
command: string,
|
|
388
|
+
args: string[],
|
|
389
|
+
cwd: string,
|
|
390
|
+
title: string,
|
|
391
|
+
env: Record<string, string>,
|
|
392
|
+
noInteractive?: boolean,
|
|
393
|
+
): Promise<TerraformResult> {
|
|
394
|
+
// Execute with streaming progress
|
|
395
|
+
const result = await executeBuildWithProgress({
|
|
396
|
+
command: 'terraform',
|
|
397
|
+
args: [command, ...args],
|
|
398
|
+
cwd,
|
|
399
|
+
title,
|
|
400
|
+
env,
|
|
401
|
+
noInteractive,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
success: result.success,
|
|
406
|
+
output: result.output,
|
|
407
|
+
error: result.error,
|
|
408
|
+
exitCode: result.exitCode,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Parse Terraform outputs after successful apply
|
|
414
|
+
* Execution function - queries Terraform state for outputs
|
|
415
|
+
*
|
|
416
|
+
* @param terraformDir - Terraform working directory
|
|
417
|
+
* @returns Parsed Terraform outputs or null if none exist
|
|
418
|
+
*/
|
|
419
|
+
export async function parseTerraformOutputs(
|
|
420
|
+
terraformDir: string,
|
|
421
|
+
): Promise<Record<string, unknown> | null> {
|
|
422
|
+
const result = await executeBuildWithProgress({
|
|
423
|
+
command: 'terraform',
|
|
424
|
+
args: ['output', '-json'],
|
|
425
|
+
cwd: terraformDir,
|
|
426
|
+
title: 'Reading Terraform outputs',
|
|
427
|
+
env: { TF_IN_AUTOMATION: '1' },
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (!result.success) {
|
|
431
|
+
// No outputs defined is not an error - return null
|
|
432
|
+
if (result.output.includes('no outputs')) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
throw new Error(`Failed to read Terraform outputs: ${result.error}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
return JSON.parse(result.output) as Record<string, unknown>;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`Failed to parse Terraform outputs JSON: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|