@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,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Pre-flight Check
|
|
3
|
+
*
|
|
4
|
+
* Fast validation that catches deployment-blocking issues BEFORE
|
|
5
|
+
* spinning up infrastructure. Designed to run in ~100ms, not ~120s.
|
|
6
|
+
*
|
|
7
|
+
* Checks:
|
|
8
|
+
* 1. Module exists and has a valid manifest
|
|
9
|
+
* 2. All required capabilities have installed providers
|
|
10
|
+
* 3. All required variables have values (from config, derivation,
|
|
11
|
+
* or capability chain)
|
|
12
|
+
* 4. Capability derivation chains resolve to actual values (not
|
|
13
|
+
* unresolved templates like $self:primary_domain)
|
|
14
|
+
* 5. Infrastructure is available for the module's zone (machine or
|
|
15
|
+
* container service exists)
|
|
16
|
+
*
|
|
17
|
+
* Does NOT:
|
|
18
|
+
* - Run template generation
|
|
19
|
+
* - Run Ansible or Terraform
|
|
20
|
+
* - Start any containers
|
|
21
|
+
* - Modify any state
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { eq } from 'drizzle-orm';
|
|
25
|
+
import type { DbClient } from '../db/client';
|
|
26
|
+
import { getDb } from '../db/client';
|
|
27
|
+
import { capabilities, moduleConfigs, modules, secrets } from '../db/schema';
|
|
28
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
29
|
+
import { buildResolutionContext } from '../variables/context';
|
|
30
|
+
|
|
31
|
+
export interface PreflightResult {
|
|
32
|
+
success: boolean;
|
|
33
|
+
moduleId: string;
|
|
34
|
+
errors: PreflightError[];
|
|
35
|
+
warnings: PreflightWarning[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PreflightError {
|
|
39
|
+
category:
|
|
40
|
+
| 'missing-config'
|
|
41
|
+
| 'missing-capability'
|
|
42
|
+
| 'unresolved-template'
|
|
43
|
+
| 'no-infrastructure'
|
|
44
|
+
| 'missing-secret';
|
|
45
|
+
message: string;
|
|
46
|
+
variable?: string;
|
|
47
|
+
suggestion?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PreflightWarning {
|
|
51
|
+
category: string;
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run a fast pre-flight check for a module deployment.
|
|
57
|
+
*
|
|
58
|
+
* Returns structured errors that describe exactly what's wrong and
|
|
59
|
+
* how to fix it, without actually attempting the deployment.
|
|
60
|
+
*/
|
|
61
|
+
export async function runPreflight(
|
|
62
|
+
moduleId: string,
|
|
63
|
+
db: DbClient = getDb(),
|
|
64
|
+
): Promise<PreflightResult> {
|
|
65
|
+
const errors: PreflightError[] = [];
|
|
66
|
+
const warnings: PreflightWarning[] = [];
|
|
67
|
+
|
|
68
|
+
// 1. Module exists?
|
|
69
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
70
|
+
if (!module) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
moduleId,
|
|
74
|
+
errors: [
|
|
75
|
+
{
|
|
76
|
+
category: 'missing-config',
|
|
77
|
+
message: `Module '${moduleId}' not found`,
|
|
78
|
+
suggestion: 'Run: celilo module import <path>',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
warnings: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
86
|
+
|
|
87
|
+
// 2. Required capabilities have providers?
|
|
88
|
+
if (manifest.requires?.capabilities) {
|
|
89
|
+
for (const cap of manifest.requires.capabilities) {
|
|
90
|
+
const provider = db
|
|
91
|
+
.select()
|
|
92
|
+
.from(capabilities)
|
|
93
|
+
.where(eq(capabilities.capabilityName, cap.name))
|
|
94
|
+
.get();
|
|
95
|
+
if (!provider) {
|
|
96
|
+
errors.push({
|
|
97
|
+
category: 'missing-capability',
|
|
98
|
+
message: `Required capability '${cap.name}' has no provider installed`,
|
|
99
|
+
suggestion: `Deploy a module that provides '${cap.name}' first`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. Required variables have values?
|
|
106
|
+
const configRows = db
|
|
107
|
+
.select()
|
|
108
|
+
.from(moduleConfigs)
|
|
109
|
+
.where(eq(moduleConfigs.moduleId, moduleId))
|
|
110
|
+
.all();
|
|
111
|
+
const configMap = new Map(
|
|
112
|
+
configRows.map((c) => [c.key, c.valueJson ? JSON.parse(c.valueJson) : c.value]),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const secretRows = db.select().from(secrets).where(eq(secrets.moduleId, moduleId)).all();
|
|
116
|
+
const secretNames = new Set(secretRows.map((s) => s.name));
|
|
117
|
+
|
|
118
|
+
if (manifest.variables?.owns) {
|
|
119
|
+
for (const variable of manifest.variables.owns) {
|
|
120
|
+
if (!variable.required) continue;
|
|
121
|
+
|
|
122
|
+
// Infrastructure/terraform variables are auto-derived during deploy
|
|
123
|
+
if (variable.source === 'infrastructure' || variable.source === 'terraform') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hasValue = configMap.has(variable.name);
|
|
128
|
+
const hasDeriveFrom = !!variable.derive_from;
|
|
129
|
+
const hasDefault = variable.default !== undefined;
|
|
130
|
+
|
|
131
|
+
if (!hasValue && !hasDeriveFrom && !hasDefault) {
|
|
132
|
+
errors.push({
|
|
133
|
+
category: 'missing-config',
|
|
134
|
+
message: `Required variable '${variable.name}' is not configured`,
|
|
135
|
+
variable: variable.name,
|
|
136
|
+
suggestion: `celilo module config set ${moduleId} ${variable.name} <value>`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check secrets
|
|
143
|
+
if (manifest.secrets?.declares) {
|
|
144
|
+
for (const secret of manifest.secrets.declares) {
|
|
145
|
+
if (!secret.required) continue;
|
|
146
|
+
const hasSecret = secretNames.has(secret.name);
|
|
147
|
+
const hasGenerate = !!secret.generate;
|
|
148
|
+
if (!hasSecret && !hasGenerate) {
|
|
149
|
+
errors.push({
|
|
150
|
+
category: 'missing-secret',
|
|
151
|
+
message: `Required secret '${secret.name}' is not set`,
|
|
152
|
+
variable: secret.name,
|
|
153
|
+
suggestion: `celilo module secret set ${moduleId} ${secret.name} <value>`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 4. Check for unresolved template strings in configured values
|
|
160
|
+
// This catches the bug where capability derivation chains produce
|
|
161
|
+
// raw template strings like "$self:primary_domain" instead of
|
|
162
|
+
// actual values.
|
|
163
|
+
try {
|
|
164
|
+
const context = await buildResolutionContext(moduleId, db);
|
|
165
|
+
for (const [key, value] of Object.entries(context.selfConfig)) {
|
|
166
|
+
if (
|
|
167
|
+
typeof value === 'string' &&
|
|
168
|
+
(value.includes('$self:') ||
|
|
169
|
+
value.includes('$system:') ||
|
|
170
|
+
value.includes('$capability:') ||
|
|
171
|
+
value.includes('$secret:'))
|
|
172
|
+
) {
|
|
173
|
+
errors.push({
|
|
174
|
+
category: 'unresolved-template',
|
|
175
|
+
message: `Variable '${key}' has unresolved template: ${value}`,
|
|
176
|
+
variable: key,
|
|
177
|
+
suggestion: `Set it explicitly: celilo module config set ${moduleId} ${key} <value>`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
// Resolution context may fail — that's informative too
|
|
183
|
+
warnings.push({
|
|
184
|
+
category: 'resolution',
|
|
185
|
+
message: `Variable resolution warning: ${error instanceof Error ? error.message : String(error)}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 5. Infrastructure availability (for modules that need it)
|
|
190
|
+
if (manifest.requires?.machine) {
|
|
191
|
+
const zone = manifest.requires.machine.zone;
|
|
192
|
+
if (zone) {
|
|
193
|
+
// Simplified check: is there a machine or service for the module's zone?
|
|
194
|
+
// The full infrastructure selector (selectInfrastructure) needs a Module
|
|
195
|
+
// object, which is more than we want for a fast pre-flight. Instead,
|
|
196
|
+
// check if the deploy-validation flow would fail at the infrastructure
|
|
197
|
+
// selection step by looking for machines/services in the zone directly.
|
|
198
|
+
try {
|
|
199
|
+
const { listMachines } = await import('./machine-pool');
|
|
200
|
+
const machines = await listMachines();
|
|
201
|
+
const { listContainerServices } = await import('./container-service');
|
|
202
|
+
const services = await listContainerServices();
|
|
203
|
+
|
|
204
|
+
const hasMachine = machines.some((m) => m.zone === zone || m.earmarkedModule === moduleId);
|
|
205
|
+
const hasService = services.some((s) => {
|
|
206
|
+
const zones = s.zones || [];
|
|
207
|
+
return zones.includes(zone);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!hasMachine && !hasService) {
|
|
211
|
+
errors.push({
|
|
212
|
+
category: 'no-infrastructure',
|
|
213
|
+
message: `No infrastructure available for zone '${zone}'`,
|
|
214
|
+
suggestion: `Add a machine: celilo machine add <ip> --zone ${zone}\nOr add a container service: celilo service add proxmox`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Machine/service queries may fail — non-fatal for preflight
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
success: errors.length === 0,
|
|
225
|
+
moduleId,
|
|
226
|
+
errors,
|
|
227
|
+
warnings,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Format a preflight result as a human-readable string.
|
|
233
|
+
*/
|
|
234
|
+
export function formatPreflightResult(result: PreflightResult): string {
|
|
235
|
+
if (result.success) {
|
|
236
|
+
const warnCount = result.warnings.length;
|
|
237
|
+
return warnCount > 0
|
|
238
|
+
? `Pre-flight check passed with ${warnCount} warning(s)`
|
|
239
|
+
: 'Pre-flight check passed';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const lines: string[] = [`Pre-flight check failed for '${result.moduleId}':`, ''];
|
|
243
|
+
|
|
244
|
+
for (const error of result.errors) {
|
|
245
|
+
lines.push(` ${errorIcon(error.category)} ${error.message}`);
|
|
246
|
+
if (error.suggestion) {
|
|
247
|
+
lines.push(` Fix: ${error.suggestion}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (result.warnings.length > 0) {
|
|
252
|
+
lines.push('');
|
|
253
|
+
for (const warning of result.warnings) {
|
|
254
|
+
lines.push(` ⚠ ${warning.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function errorIcon(category: PreflightError['category']): string {
|
|
262
|
+
switch (category) {
|
|
263
|
+
case 'missing-config':
|
|
264
|
+
return '✗';
|
|
265
|
+
case 'missing-capability':
|
|
266
|
+
return '◯';
|
|
267
|
+
case 'unresolved-template':
|
|
268
|
+
return '⟲';
|
|
269
|
+
case 'no-infrastructure':
|
|
270
|
+
return '▢';
|
|
271
|
+
case 'missing-secret':
|
|
272
|
+
return '🔑';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH Readiness Service
|
|
3
|
+
*
|
|
4
|
+
* Waits for SSH to become available on target host with exponential backoff
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { FuelGauge } from '../cli/fuel-gauge';
|
|
9
|
+
import { log } from '../cli/prompts';
|
|
10
|
+
|
|
11
|
+
export interface SSHResult {
|
|
12
|
+
success: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
attempts?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wait for SSH to become available on target host
|
|
19
|
+
* Execution function - polls SSH connectivity with backoff
|
|
20
|
+
*
|
|
21
|
+
* @param host - Target host IP address
|
|
22
|
+
* @param user - SSH user (default: root)
|
|
23
|
+
* @param timeoutSeconds - Timeout in seconds (default: 120)
|
|
24
|
+
* @returns SSH readiness result
|
|
25
|
+
*/
|
|
26
|
+
export async function waitForSSH(
|
|
27
|
+
host: string,
|
|
28
|
+
user = 'root',
|
|
29
|
+
timeoutSeconds = 120,
|
|
30
|
+
): Promise<SSHResult> {
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
33
|
+
let attempt = 0;
|
|
34
|
+
|
|
35
|
+
const gauge = new FuelGauge(`Waiting for SSH access to ${user}@${host}`);
|
|
36
|
+
gauge.start();
|
|
37
|
+
|
|
38
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
39
|
+
attempt++;
|
|
40
|
+
|
|
41
|
+
gauge.addOutput(`Attempt ${attempt} - connecting to ${user}@${host}...`);
|
|
42
|
+
|
|
43
|
+
const connected = await trySSHConnection(host, user);
|
|
44
|
+
|
|
45
|
+
if (connected) {
|
|
46
|
+
gauge.stop(true);
|
|
47
|
+
log.success(`SSH ready after ${attempt} attempt${attempt === 1 ? '' : 's'}`);
|
|
48
|
+
return { success: true, attempts: attempt };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Calculate backoff delay: 1s, 1.5s, 2.25s, ..., max 10s
|
|
52
|
+
const backoffDelay = Math.min(1000 * 1.5 ** (attempt - 1), 10000);
|
|
53
|
+
await sleep(backoffDelay);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
gauge.stop(false);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: `SSH connection timeout after ${timeoutSeconds} seconds (${attempt} attempts)\n\nTarget: ${user}@${host}\n\nPossible causes:\n - Container failed to start\n - SSH service not configured\n - Firewall blocking connection\n - Wrong IP address`,
|
|
61
|
+
attempts: attempt,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Try SSH connection to host
|
|
67
|
+
* Execution function - attempts single SSH connection
|
|
68
|
+
*
|
|
69
|
+
* @param host - Target host IP
|
|
70
|
+
* @param user - SSH user
|
|
71
|
+
* @returns True if connection successful
|
|
72
|
+
*/
|
|
73
|
+
async function trySSHConnection(host: string, user: string): Promise<boolean> {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
// Try SSH connection with short timeout
|
|
76
|
+
const child = spawn(
|
|
77
|
+
'ssh',
|
|
78
|
+
[
|
|
79
|
+
'-o',
|
|
80
|
+
'ConnectTimeout=5',
|
|
81
|
+
'-o',
|
|
82
|
+
'StrictHostKeyChecking=no',
|
|
83
|
+
'-o',
|
|
84
|
+
'UserKnownHostsFile=/dev/null',
|
|
85
|
+
'-o',
|
|
86
|
+
'LogLevel=ERROR',
|
|
87
|
+
`${user}@${host}`,
|
|
88
|
+
'echo',
|
|
89
|
+
'ready',
|
|
90
|
+
],
|
|
91
|
+
{
|
|
92
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
let output = '';
|
|
97
|
+
|
|
98
|
+
child.stdout.on('data', (data) => {
|
|
99
|
+
output += data.toString();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
child.on('close', (exitCode) => {
|
|
103
|
+
// Success if command executed and returned "ready"
|
|
104
|
+
if (exitCode === 0 && output.trim() === 'ready') {
|
|
105
|
+
resolve(true);
|
|
106
|
+
} else {
|
|
107
|
+
resolve(false);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.on('error', () => {
|
|
112
|
+
resolve(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Timeout after 6 seconds
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
child.kill();
|
|
118
|
+
resolve(false);
|
|
119
|
+
}, 6000);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Sleep for specified milliseconds
|
|
125
|
+
* Utility function
|
|
126
|
+
*
|
|
127
|
+
* @param ms - Milliseconds to sleep
|
|
128
|
+
*/
|
|
129
|
+
function sleep(ms: number): Promise<void> {
|
|
130
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
131
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, test } from 'bun:test';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const TEST_TERRAFORM_DIR = '/tmp/test-terraform-outputs';
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
// Create test directory
|
|
10
|
+
await mkdir(TEST_TERRAFORM_DIR, { recursive: true });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
// Clean up test directory
|
|
15
|
+
if (existsSync(TEST_TERRAFORM_DIR)) {
|
|
16
|
+
await rm(TEST_TERRAFORM_DIR, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('parseTerraformOutputs', () => {
|
|
21
|
+
test('parses wrapped Terraform output format', async () => {
|
|
22
|
+
// Create a mock terraform state directory
|
|
23
|
+
const tfDir = join(TEST_TERRAFORM_DIR, 'wrapped');
|
|
24
|
+
await mkdir(tfDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
// Mock terraform output -json by creating a state file
|
|
27
|
+
// (In reality, terraform reads from .tfstate, but we'll mock the command)
|
|
28
|
+
// We can't actually test this without mocking executeBuildWithProgress
|
|
29
|
+
|
|
30
|
+
// For now, skip this test - it requires mocking the terraform command
|
|
31
|
+
// The function is tested indirectly through integration tests
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('returns null when no outputs defined', async () => {
|
|
35
|
+
// This would require mocking terraform command that returns "no outputs"
|
|
36
|
+
// Skip for now - tested through integration tests
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('throws on invalid JSON output', async () => {
|
|
40
|
+
// This would require mocking terraform command that returns invalid JSON
|
|
41
|
+
// Skip for now - tested through integration tests
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Note: These tests are placeholders. The parseTerraformOutputs function
|
|
46
|
+
// calls executeBuildWithProgress which executes actual terraform commands.
|
|
47
|
+
// Proper testing requires either:
|
|
48
|
+
// 1. Mocking executeBuildWithProgress (refactor needed)
|
|
49
|
+
// 2. Integration tests with real terraform (slow)
|
|
50
|
+
// 3. Dependency injection of the executor (architectural change)
|
|
51
|
+
//
|
|
52
|
+
// We'll rely on:
|
|
53
|
+
// - Manual testing with real modules
|
|
54
|
+
// - Integration tests in test-integration/
|
|
55
|
+
// - Unit tests of property-extractor (already passing)
|