@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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Module Data Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages data exchange between modules, including:
|
|
5
|
+
* - Storing/retrieving configuration data from function calls
|
|
6
|
+
* - Encrypting sensitive data (secrets)
|
|
7
|
+
* - Resolving variables in parameters
|
|
8
|
+
* - Looking up capability providers
|
|
9
|
+
*
|
|
10
|
+
* Future: Full function call orchestration, lifecycle management
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { and, eq } from 'drizzle-orm';
|
|
14
|
+
import { type DbClient, createDbClient } from '../db/client';
|
|
15
|
+
import { capabilities, moduleConfigs, secrets } from '../db/schema';
|
|
16
|
+
import { decryptSecret, encryptSecret } from '../secrets/encryption';
|
|
17
|
+
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
18
|
+
import { buildResolutionContext } from '../variables/context';
|
|
19
|
+
import { resolveTemplate } from '../variables/resolver';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration data type
|
|
23
|
+
*/
|
|
24
|
+
export interface ConfigData {
|
|
25
|
+
key: string;
|
|
26
|
+
value: string | number | boolean | unknown[] | Record<string, unknown>;
|
|
27
|
+
isSecret: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Capability lookup result
|
|
32
|
+
*/
|
|
33
|
+
export interface CapabilityInfo {
|
|
34
|
+
moduleId: string;
|
|
35
|
+
capabilityName: string;
|
|
36
|
+
version: string;
|
|
37
|
+
data: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Cross-module data manager
|
|
42
|
+
* Orchestrates data exchange between modules
|
|
43
|
+
*/
|
|
44
|
+
export class CrossModuleDataManager {
|
|
45
|
+
private db: DbClient;
|
|
46
|
+
private masterKey: Buffer | null = null;
|
|
47
|
+
|
|
48
|
+
constructor(db?: DbClient) {
|
|
49
|
+
this.db = db || createDbClient();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Initialize manager (load master key)
|
|
54
|
+
* Execution function - performs I/O
|
|
55
|
+
*/
|
|
56
|
+
async initialize(): Promise<void> {
|
|
57
|
+
this.masterKey = await getOrCreateMasterKey();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensure master key is loaded
|
|
62
|
+
* Policy function - validates state
|
|
63
|
+
*/
|
|
64
|
+
private ensureMasterKey(): Buffer {
|
|
65
|
+
if (!this.masterKey) {
|
|
66
|
+
throw new Error('CrossModuleDataManager not initialized. Call initialize() first.');
|
|
67
|
+
}
|
|
68
|
+
return this.masterKey;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Store configuration data for a module
|
|
73
|
+
* Handles both regular config and secrets
|
|
74
|
+
*
|
|
75
|
+
* Execution function - performs database writes and encryption
|
|
76
|
+
*
|
|
77
|
+
* @param moduleId - Module ID
|
|
78
|
+
* @param key - Configuration key
|
|
79
|
+
* @param value - Configuration value
|
|
80
|
+
* @param isSecret - Whether to encrypt the value
|
|
81
|
+
*/
|
|
82
|
+
async storeConfigData(
|
|
83
|
+
moduleId: string,
|
|
84
|
+
key: string,
|
|
85
|
+
value: string | number | boolean | unknown[] | Record<string, unknown>,
|
|
86
|
+
isSecret = false,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
if (isSecret) {
|
|
89
|
+
await this.storeSecret(moduleId, key, String(value));
|
|
90
|
+
} else {
|
|
91
|
+
await this.storeConfig(moduleId, key, value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Store regular (non-secret) configuration
|
|
97
|
+
* Execution function - performs database write
|
|
98
|
+
*/
|
|
99
|
+
private async storeConfig(
|
|
100
|
+
moduleId: string,
|
|
101
|
+
key: string,
|
|
102
|
+
value: string | number | boolean | unknown[] | Record<string, unknown>,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const isComplex = typeof value === 'object' && value !== null;
|
|
105
|
+
|
|
106
|
+
const existing = this.db
|
|
107
|
+
.select()
|
|
108
|
+
.from(moduleConfigs)
|
|
109
|
+
.where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
|
|
110
|
+
.get();
|
|
111
|
+
|
|
112
|
+
if (existing) {
|
|
113
|
+
// Update existing
|
|
114
|
+
this.db
|
|
115
|
+
.update(moduleConfigs)
|
|
116
|
+
.set({
|
|
117
|
+
value: isComplex ? '' : String(value),
|
|
118
|
+
valueJson: isComplex ? JSON.stringify(value) : null,
|
|
119
|
+
updatedAt: new Date(Date.now()),
|
|
120
|
+
})
|
|
121
|
+
.where(eq(moduleConfigs.id, existing.id))
|
|
122
|
+
.run();
|
|
123
|
+
} else {
|
|
124
|
+
// Insert new
|
|
125
|
+
this.db
|
|
126
|
+
.insert(moduleConfigs)
|
|
127
|
+
.values({
|
|
128
|
+
moduleId,
|
|
129
|
+
key,
|
|
130
|
+
value: isComplex ? '' : String(value),
|
|
131
|
+
valueJson: isComplex ? JSON.stringify(value) : null,
|
|
132
|
+
})
|
|
133
|
+
.run();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Store secret (encrypted)
|
|
139
|
+
* Execution function - performs encryption and database write
|
|
140
|
+
*
|
|
141
|
+
* @param moduleId - Module ID
|
|
142
|
+
* @param name - Secret name
|
|
143
|
+
* @param value - Secret value (plaintext)
|
|
144
|
+
*/
|
|
145
|
+
async storeSecret(moduleId: string, name: string, value: string): Promise<void> {
|
|
146
|
+
const masterKey = this.ensureMasterKey();
|
|
147
|
+
|
|
148
|
+
// Encrypt the secret
|
|
149
|
+
const encrypted = encryptSecret(value, masterKey);
|
|
150
|
+
|
|
151
|
+
// Check if secret already exists
|
|
152
|
+
const existing = this.db
|
|
153
|
+
.select()
|
|
154
|
+
.from(secrets)
|
|
155
|
+
.where(and(eq(secrets.moduleId, moduleId), eq(secrets.name, name)))
|
|
156
|
+
.get();
|
|
157
|
+
|
|
158
|
+
if (existing) {
|
|
159
|
+
// Update existing
|
|
160
|
+
this.db
|
|
161
|
+
.update(secrets)
|
|
162
|
+
.set({
|
|
163
|
+
encryptedValue: encrypted.encryptedValue,
|
|
164
|
+
iv: encrypted.iv,
|
|
165
|
+
authTag: encrypted.authTag,
|
|
166
|
+
updatedAt: new Date(Date.now()),
|
|
167
|
+
})
|
|
168
|
+
.where(eq(secrets.id, existing.id))
|
|
169
|
+
.run();
|
|
170
|
+
} else {
|
|
171
|
+
// Insert new
|
|
172
|
+
this.db
|
|
173
|
+
.insert(secrets)
|
|
174
|
+
.values({
|
|
175
|
+
moduleId,
|
|
176
|
+
name,
|
|
177
|
+
encryptedValue: encrypted.encryptedValue,
|
|
178
|
+
iv: encrypted.iv,
|
|
179
|
+
authTag: encrypted.authTag,
|
|
180
|
+
})
|
|
181
|
+
.run();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get configuration data (non-secret)
|
|
187
|
+
* Execution function - performs database read
|
|
188
|
+
*
|
|
189
|
+
* @param moduleId - Module ID
|
|
190
|
+
* @param key - Configuration key
|
|
191
|
+
* @returns Configuration value or null if not found
|
|
192
|
+
*/
|
|
193
|
+
getConfigData(
|
|
194
|
+
moduleId: string,
|
|
195
|
+
key: string,
|
|
196
|
+
): string | number | boolean | unknown[] | Record<string, unknown> | null {
|
|
197
|
+
const config = this.db
|
|
198
|
+
.select()
|
|
199
|
+
.from(moduleConfigs)
|
|
200
|
+
.where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key)))
|
|
201
|
+
.get();
|
|
202
|
+
|
|
203
|
+
if (!config) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Return complex type from valueJson
|
|
208
|
+
if (config.valueJson) {
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(config.valueJson);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Failed to parse config value for ${moduleId}.${key}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Return primitive type from value
|
|
219
|
+
// Parse to correct type
|
|
220
|
+
if (config.value === 'true') return true;
|
|
221
|
+
if (config.value === 'false') return false;
|
|
222
|
+
|
|
223
|
+
const num = Number(config.value);
|
|
224
|
+
if (!Number.isNaN(num)) {
|
|
225
|
+
return num;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return config.value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get secret (decrypted)
|
|
233
|
+
* Execution function - performs database read and decryption
|
|
234
|
+
*
|
|
235
|
+
* @param moduleId - Module ID
|
|
236
|
+
* @param name - Secret name
|
|
237
|
+
* @returns Decrypted secret value or null if not found
|
|
238
|
+
*/
|
|
239
|
+
getSecret(moduleId: string, name: string): string | null {
|
|
240
|
+
const masterKey = this.ensureMasterKey();
|
|
241
|
+
|
|
242
|
+
const secret = this.db
|
|
243
|
+
.select()
|
|
244
|
+
.from(secrets)
|
|
245
|
+
.where(and(eq(secrets.moduleId, moduleId), eq(secrets.name, name)))
|
|
246
|
+
.get();
|
|
247
|
+
|
|
248
|
+
if (!secret) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Decrypt and return
|
|
253
|
+
return decryptSecret(
|
|
254
|
+
{
|
|
255
|
+
encryptedValue: secret.encryptedValue,
|
|
256
|
+
iv: secret.iv,
|
|
257
|
+
authTag: secret.authTag,
|
|
258
|
+
},
|
|
259
|
+
masterKey,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get all secrets for a module (decrypted)
|
|
265
|
+
* Execution function - performs database read and decryption
|
|
266
|
+
*
|
|
267
|
+
* @param moduleId - Module ID
|
|
268
|
+
* @returns Record of secret name -> decrypted value
|
|
269
|
+
*/
|
|
270
|
+
getAllSecrets(moduleId: string): Record<string, string> {
|
|
271
|
+
const masterKey = this.ensureMasterKey();
|
|
272
|
+
|
|
273
|
+
const secretRecords = this.db
|
|
274
|
+
.select()
|
|
275
|
+
.from(secrets)
|
|
276
|
+
.where(eq(secrets.moduleId, moduleId))
|
|
277
|
+
.all();
|
|
278
|
+
|
|
279
|
+
const result: Record<string, string> = {};
|
|
280
|
+
|
|
281
|
+
for (const secret of secretRecords) {
|
|
282
|
+
result[secret.name] = decryptSecret(
|
|
283
|
+
{
|
|
284
|
+
encryptedValue: secret.encryptedValue,
|
|
285
|
+
iv: secret.iv,
|
|
286
|
+
authTag: secret.authTag,
|
|
287
|
+
},
|
|
288
|
+
masterKey,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Look up which module provides a capability
|
|
297
|
+
* Execution function - performs database query
|
|
298
|
+
*
|
|
299
|
+
* @param capabilityName - Capability name (e.g., 'dns_registrar', 'idp')
|
|
300
|
+
* @returns Capability info or null if not found
|
|
301
|
+
*/
|
|
302
|
+
findCapabilityProvider(capabilityName: string): CapabilityInfo | null {
|
|
303
|
+
const capability = this.db
|
|
304
|
+
.select()
|
|
305
|
+
.from(capabilities)
|
|
306
|
+
.where(eq(capabilities.capabilityName, capabilityName))
|
|
307
|
+
.get();
|
|
308
|
+
|
|
309
|
+
if (!capability) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
moduleId: capability.moduleId,
|
|
315
|
+
capabilityName: capability.capabilityName,
|
|
316
|
+
version: capability.version,
|
|
317
|
+
data: capability.data as Record<string, unknown>,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Resolve variables in parameters
|
|
323
|
+
* Uses the variable resolver to handle $self:, $system:, $capability:, etc.
|
|
324
|
+
*
|
|
325
|
+
* Execution function - performs database reads for variable resolution
|
|
326
|
+
*
|
|
327
|
+
* @param moduleId - Module ID for $self: resolution
|
|
328
|
+
* @param params - Parameters with variables to resolve
|
|
329
|
+
* @returns Resolved parameters
|
|
330
|
+
*/
|
|
331
|
+
async resolveParameters(
|
|
332
|
+
moduleId: string,
|
|
333
|
+
params: Record<string, unknown>,
|
|
334
|
+
): Promise<Record<string, unknown>> {
|
|
335
|
+
// Build resolution context
|
|
336
|
+
const context = await buildResolutionContext(moduleId, this.db);
|
|
337
|
+
|
|
338
|
+
// Resolve each parameter value
|
|
339
|
+
const resolved: Record<string, unknown> = {};
|
|
340
|
+
|
|
341
|
+
for (const [key, value] of Object.entries(params)) {
|
|
342
|
+
if (typeof value === 'string') {
|
|
343
|
+
const result = await resolveTemplate(value, context, this.db);
|
|
344
|
+
if (!result.success) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`Failed to resolve parameter '${key}': ${result.errors.map((e) => e.error).join(', ')}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
resolved[key] = result.content;
|
|
350
|
+
} else if (Array.isArray(value)) {
|
|
351
|
+
// Resolve each array element
|
|
352
|
+
resolved[key] = await Promise.all(
|
|
353
|
+
value.map(async (item) => {
|
|
354
|
+
if (typeof item === 'string') {
|
|
355
|
+
const result = await resolveTemplate(item, context, this.db);
|
|
356
|
+
if (!result.success) {
|
|
357
|
+
throw new Error(`Failed to resolve array element in '${key}'`);
|
|
358
|
+
}
|
|
359
|
+
return result.content;
|
|
360
|
+
}
|
|
361
|
+
return item;
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
365
|
+
// Recursively resolve object properties
|
|
366
|
+
resolved[key] = await this.resolveParameters(moduleId, value as Record<string, unknown>);
|
|
367
|
+
} else {
|
|
368
|
+
// Primitive value - keep as-is
|
|
369
|
+
resolved[key] = value;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return resolved;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Generate TSIG key for DNS dynamic updates
|
|
378
|
+
* Planning function (Rule 10.1) - decides what type of key to generate
|
|
379
|
+
*
|
|
380
|
+
* @param algorithm - TSIG algorithm (default: hmac-sha256)
|
|
381
|
+
* @returns Base64-encoded TSIG key
|
|
382
|
+
*/
|
|
383
|
+
generateTSIGKey(algorithm = 'hmac-sha256'): string {
|
|
384
|
+
// TSIG keys are typically 256-bit (32 bytes) for hmac-sha256
|
|
385
|
+
const keyLength = algorithm === 'hmac-sha256' ? 32 : 32;
|
|
386
|
+
const key = new Uint8Array(keyLength);
|
|
387
|
+
crypto.getRandomValues(key);
|
|
388
|
+
return btoa(String.fromCharCode(...key));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Generate secure random secret
|
|
393
|
+
* Planning function - decides length and encoding
|
|
394
|
+
*
|
|
395
|
+
* @param length - Length in bytes (default: 32)
|
|
396
|
+
* @param encoding - Encoding format (default: hex)
|
|
397
|
+
* @returns Random secret
|
|
398
|
+
*/
|
|
399
|
+
generateSecret(length = 32, encoding: 'hex' | 'base64' = 'hex'): string {
|
|
400
|
+
const bytes = new Uint8Array(length);
|
|
401
|
+
crypto.getRandomValues(bytes);
|
|
402
|
+
|
|
403
|
+
if (encoding === 'base64') {
|
|
404
|
+
return btoa(String.fromCharCode(...bytes));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Hex encoding
|
|
408
|
+
return Array.from(bytes)
|
|
409
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
410
|
+
.join('');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ansible Execution Service
|
|
3
|
+
*
|
|
4
|
+
* Executes Ansible playbooks with vault password and streaming progress
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { appendFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { log } from '../cli/prompts';
|
|
11
|
+
import { getVaultPassword } from '../secrets/vault';
|
|
12
|
+
import { shellEscape } from '../utils/shell';
|
|
13
|
+
import { executeBuildWithProgress } from './build-stream';
|
|
14
|
+
|
|
15
|
+
export interface AnsibleResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
output: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Execute Ansible playbook with streaming progress
|
|
23
|
+
* Execution function - performs software deployment
|
|
24
|
+
*
|
|
25
|
+
* @param generatedPath - Path to generated module artifacts
|
|
26
|
+
* @returns Ansible execution result
|
|
27
|
+
*/
|
|
28
|
+
export async function executeAnsible(
|
|
29
|
+
generatedPath: string,
|
|
30
|
+
options?: { noInteractive?: boolean },
|
|
31
|
+
): Promise<AnsibleResult> {
|
|
32
|
+
const ansibleDir = join(generatedPath, 'ansible');
|
|
33
|
+
const inventoryPath = join(ansibleDir, 'inventory', 'hosts.ini');
|
|
34
|
+
const playbookPath = join(ansibleDir, 'playbook.yml');
|
|
35
|
+
|
|
36
|
+
// Get Ansible Vault password
|
|
37
|
+
const vaultPassword = await getVaultPassword();
|
|
38
|
+
|
|
39
|
+
// Write vault password to temp file (same approach as ansible/secrets.ts)
|
|
40
|
+
// Using /dev/stdin doesn't work reliably — Ansible tries to execute it as a
|
|
41
|
+
// script and hits permission errors on some platforms.
|
|
42
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'celilo-vault-'));
|
|
43
|
+
const passwordPath = join(tempDir, 'vault-pass');
|
|
44
|
+
await writeFile(passwordPath, vaultPassword, { mode: 0o600 });
|
|
45
|
+
|
|
46
|
+
const logPath = join(generatedPath, 'deploy.log');
|
|
47
|
+
log.info('Deploying software...');
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Execute ansible-playbook with streaming progress
|
|
51
|
+
// Note: Paths escaped for shell execution (macOS paths contain spaces)
|
|
52
|
+
const result = await executeBuildWithProgress({
|
|
53
|
+
command: 'ansible-playbook',
|
|
54
|
+
args: [
|
|
55
|
+
'-i',
|
|
56
|
+
shellEscape(inventoryPath),
|
|
57
|
+
'--vault-password-file',
|
|
58
|
+
shellEscape(passwordPath),
|
|
59
|
+
shellEscape(playbookPath),
|
|
60
|
+
],
|
|
61
|
+
cwd: ansibleDir,
|
|
62
|
+
title: 'Deploying software',
|
|
63
|
+
noInteractive: options?.noInteractive,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Persist full output to log file for debugging
|
|
67
|
+
const timestamp = new Date().toISOString();
|
|
68
|
+
const logHeader = `\n--- Ansible deploy ${timestamp} ---\n`;
|
|
69
|
+
await appendFile(logPath, logHeader + result.output, 'utf-8');
|
|
70
|
+
|
|
71
|
+
if (!result.success) {
|
|
72
|
+
log.warn(`Full deploy log: ${logPath}`);
|
|
73
|
+
return {
|
|
74
|
+
success: false,
|
|
75
|
+
output: result.output,
|
|
76
|
+
error: result.error || 'Ansible deployment failed',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
log.success('Software deployed');
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
output: result.output,
|
|
84
|
+
};
|
|
85
|
+
} finally {
|
|
86
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Planning Service
|
|
3
|
+
*
|
|
4
|
+
* Determines what deployment steps are needed based on current state
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { eq } from 'drizzle-orm';
|
|
9
|
+
import type { DbClient } from '../db/client';
|
|
10
|
+
import { moduleConfigs, moduleInfrastructure } from '../db/schema';
|
|
11
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
12
|
+
|
|
13
|
+
export interface DeploymentPlan {
|
|
14
|
+
needsTerraform: boolean;
|
|
15
|
+
needsSSHWait: boolean;
|
|
16
|
+
ansiblePlaybook: string;
|
|
17
|
+
targetHost: {
|
|
18
|
+
hostname: string;
|
|
19
|
+
ip: string;
|
|
20
|
+
user: string;
|
|
21
|
+
};
|
|
22
|
+
infrastructure?: {
|
|
23
|
+
type: 'machine' | 'container_service';
|
|
24
|
+
machineId?: string;
|
|
25
|
+
serviceId?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determine what deployment steps are needed
|
|
31
|
+
* Planning function - analyzes state and returns plan
|
|
32
|
+
*
|
|
33
|
+
* - Machine infrastructure: skip Terraform (machine already exists)
|
|
34
|
+
* - Container service: run Terraform (need to provision container)
|
|
35
|
+
*
|
|
36
|
+
* @param moduleId - Module identifier
|
|
37
|
+
* @param generatedPath - Path to generated artifacts
|
|
38
|
+
* @param manifest - Module manifest
|
|
39
|
+
* @param db - Database connection
|
|
40
|
+
* @returns Deployment plan
|
|
41
|
+
*/
|
|
42
|
+
export async function planDeployment(
|
|
43
|
+
moduleId: string,
|
|
44
|
+
generatedPath: string,
|
|
45
|
+
_manifest: ModuleManifest,
|
|
46
|
+
db: DbClient,
|
|
47
|
+
): Promise<DeploymentPlan> {
|
|
48
|
+
const infrastructure = await db
|
|
49
|
+
.select()
|
|
50
|
+
.from(moduleInfrastructure)
|
|
51
|
+
.where(eq(moduleInfrastructure.moduleId, moduleId))
|
|
52
|
+
.get();
|
|
53
|
+
|
|
54
|
+
if (!infrastructure) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`No infrastructure selected for module ${moduleId}. Run 'celilo module generate ${moduleId}' first to select infrastructure (container service or machine) and generate templates.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let needsTerraform: boolean;
|
|
61
|
+
let needsSSHWait: boolean;
|
|
62
|
+
|
|
63
|
+
if (infrastructure.infrastructureType === 'machine') {
|
|
64
|
+
// Machine infrastructure: skip Terraform (machine already exists)
|
|
65
|
+
// No SSH wait needed (machine already running)
|
|
66
|
+
needsTerraform = false;
|
|
67
|
+
needsSSHWait = false;
|
|
68
|
+
} else if (infrastructure.infrastructureType === 'container_service') {
|
|
69
|
+
// Container service: run Terraform to provision container
|
|
70
|
+
needsTerraform = true;
|
|
71
|
+
needsSSHWait = true;
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error(`Unknown infrastructure type: ${infrastructure.infrastructureType}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract target host information from module config
|
|
77
|
+
const targetHost = await extractTargetHost(moduleId, db);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
needsTerraform,
|
|
81
|
+
needsSSHWait,
|
|
82
|
+
ansiblePlaybook: join(generatedPath, 'ansible', 'playbook.yml'),
|
|
83
|
+
targetHost,
|
|
84
|
+
infrastructure: infrastructure
|
|
85
|
+
? {
|
|
86
|
+
type: infrastructure.infrastructureType as 'machine' | 'container_service',
|
|
87
|
+
machineId: infrastructure.machineId || undefined,
|
|
88
|
+
serviceId: infrastructure.serviceId || undefined,
|
|
89
|
+
}
|
|
90
|
+
: undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract target host information from module configuration
|
|
96
|
+
* Execution function - queries database
|
|
97
|
+
*
|
|
98
|
+
* Must be called AFTER infrastructure variable resolution
|
|
99
|
+
* to pick up infrastructure-derived variables like ip.primary
|
|
100
|
+
*
|
|
101
|
+
* @param moduleId - Module identifier
|
|
102
|
+
* @param db - Database connection
|
|
103
|
+
* @returns Target host information
|
|
104
|
+
*/
|
|
105
|
+
export async function extractTargetHost(
|
|
106
|
+
moduleId: string,
|
|
107
|
+
db: DbClient,
|
|
108
|
+
): Promise<{ hostname: string; ip: string; user: string }> {
|
|
109
|
+
// Get module configuration
|
|
110
|
+
const configs = await db
|
|
111
|
+
.select()
|
|
112
|
+
.from(moduleConfigs)
|
|
113
|
+
.where(eq(moduleConfigs.moduleId, moduleId))
|
|
114
|
+
.all();
|
|
115
|
+
|
|
116
|
+
// Build config map
|
|
117
|
+
const configMap = new Map<string, string>();
|
|
118
|
+
for (const config of configs) {
|
|
119
|
+
const value = config.valueJson || config.value;
|
|
120
|
+
if (value) {
|
|
121
|
+
configMap.set(config.key, value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract hostname
|
|
126
|
+
const hostname = configMap.get('hostname') || moduleId;
|
|
127
|
+
|
|
128
|
+
// Extract IP - support infrastructure-derived variables
|
|
129
|
+
// Priority: container_ip > ip.primary > vps_ip (backward compatibility)
|
|
130
|
+
let ip = '';
|
|
131
|
+
const containerIp = configMap.get('container_ip');
|
|
132
|
+
const ipPrimary = configMap.get('ip.primary');
|
|
133
|
+
const vpsIp = configMap.get('vps_ip');
|
|
134
|
+
|
|
135
|
+
if (containerIp) {
|
|
136
|
+
// Container IP format: "10.0.10.10/24" - extract just IP
|
|
137
|
+
ip = containerIp.split('/')[0];
|
|
138
|
+
} else if (ipPrimary) {
|
|
139
|
+
// Infrastructure-derived IP (already plain format)
|
|
140
|
+
ip = ipPrimary;
|
|
141
|
+
} else if (vpsIp) {
|
|
142
|
+
ip = vpsIp;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Extract ansible user (defaults to root)
|
|
146
|
+
const user = configMap.get('ansible_user') || 'root';
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
hostname,
|
|
150
|
+
ip,
|
|
151
|
+
user,
|
|
152
|
+
};
|
|
153
|
+
}
|