kadi-deploy 0.19.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/.env.example +6 -0
- package/.prettierrc +6 -0
- package/README.md +589 -0
- package/agent.json +23 -0
- package/index.js +11 -0
- package/package.json +42 -0
- package/quick-command.txt +92 -0
- package/scripts/preflight.js +458 -0
- package/scripts/preflight.sh +300 -0
- package/src/cli/bid-selector.ts +222 -0
- package/src/cli/colors.ts +216 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/prompts.ts +190 -0
- package/src/cli/spinners.ts +165 -0
- package/src/commands/deploy-local.ts +475 -0
- package/src/commands/deploy.ts +1342 -0
- package/src/commands/down.ts +679 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/lock.ts +571 -0
- package/src/config/agent-loader.ts +177 -0
- package/src/config/index.ts +9 -0
- package/src/display/deployment-info.ts +220 -0
- package/src/display/pricing.ts +137 -0
- package/src/display/resources.ts +234 -0
- package/src/enhanced-registry-manager.ts +892 -0
- package/src/index.ts +307 -0
- package/src/infrastructure/registry.ts +269 -0
- package/src/schemas/profiles.ts +529 -0
- package/src/secrets/broker-urls.ts +109 -0
- package/src/secrets/handshake.ts +407 -0
- package/src/secrets/index.ts +69 -0
- package/src/secrets/inject-env.ts +171 -0
- package/src/secrets/nonce.ts +31 -0
- package/src/secrets/normalize.ts +204 -0
- package/src/secrets/prepare.ts +152 -0
- package/src/secrets/validate.ts +243 -0
- package/src/secrets/vault.ts +80 -0
- package/src/types/akash.ts +116 -0
- package/src/types/container-registry-ability.d.ts +158 -0
- package/src/types/external.ts +49 -0
- package/src/types.ts +211 -0
- package/src/utils/akt-price.ts +74 -0
- package/tests/agent-loader.test.ts +239 -0
- package/tests/autonomous.test.ts +244 -0
- package/tests/down.test.ts +1143 -0
- package/tests/lock.test.ts +1148 -0
- package/tests/nonce.test.ts +34 -0
- package/tests/normalize.test.ts +270 -0
- package/tests/secrets-schema.test.ts +301 -0
- package/tests/types.test.ts +198 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Command - Local Docker/Podman Integration
|
|
3
|
+
*
|
|
4
|
+
* Main deployment command that uses deploy-ability library for local deployments.
|
|
5
|
+
* This replaces the old targets/local/local.ts with a clean interface to deploy-ability.
|
|
6
|
+
*
|
|
7
|
+
* @module commands/deploy-local
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { deployToLocal } from '@kadi.build/deploy-ability/local';
|
|
11
|
+
import type {
|
|
12
|
+
LocalDeploymentOptions,
|
|
13
|
+
LocalDeploymentData,
|
|
14
|
+
LocalDryRunData
|
|
15
|
+
} from '@kadi.build/deploy-ability/types';
|
|
16
|
+
|
|
17
|
+
import { loadAgentConfig, getProfile, getFirstProfile } from '../config/agent-loader.js';
|
|
18
|
+
import {
|
|
19
|
+
waitForSecretRequest,
|
|
20
|
+
shareSecrets,
|
|
21
|
+
sendRejection,
|
|
22
|
+
readSecretsFromCli,
|
|
23
|
+
prepareSecretsForDeployment,
|
|
24
|
+
type WaitForSecretsResult,
|
|
25
|
+
} from '../secrets/index.js';
|
|
26
|
+
import { normalizeSecrets, hasAnySecrets, type NormalizedVaultSource } from '../secrets/normalize.js';
|
|
27
|
+
import { startSpinner, succeedSpinner, failSpinner } from '../cli/spinners.js';
|
|
28
|
+
import { confirmPrompt } from '../cli/prompts.js';
|
|
29
|
+
import { error as errorColor, warning, success as successColor, formatKeyValue, formatHeader, dim, bold } from '../cli/colors.js';
|
|
30
|
+
|
|
31
|
+
import type { DeploymentContext } from '../types.js';
|
|
32
|
+
import type { Ora } from 'ora';
|
|
33
|
+
import { buildLocalLock, writeLockFile } from './lock.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Executes local Docker/Podman deployment using deploy-ability
|
|
37
|
+
*
|
|
38
|
+
* This function orchestrates the complete local deployment workflow:
|
|
39
|
+
* 1. Load agent configuration
|
|
40
|
+
* 2. Resolve profile
|
|
41
|
+
* 3. Build deployment options
|
|
42
|
+
* 4. Deploy using deploy-ability
|
|
43
|
+
* 5. Display results
|
|
44
|
+
*
|
|
45
|
+
* @param ctx - Deployment context from CLI
|
|
46
|
+
*/
|
|
47
|
+
export async function executeLocalDeployment(ctx: DeploymentContext): Promise<void> {
|
|
48
|
+
const { logger, projectRoot, flags } = ctx;
|
|
49
|
+
|
|
50
|
+
// ----------------------------------------------------------------
|
|
51
|
+
// 1. Load agent configuration
|
|
52
|
+
// ----------------------------------------------------------------
|
|
53
|
+
const spinner = startSpinner('Loading agent configuration...');
|
|
54
|
+
|
|
55
|
+
let agentConfig;
|
|
56
|
+
try {
|
|
57
|
+
agentConfig = await loadAgentConfig(projectRoot);
|
|
58
|
+
succeedSpinner(spinner, `Loaded agent: ${agentConfig.name}`);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
failSpinner(spinner, 'Failed to load agent.json');
|
|
61
|
+
logger.error(errorColor((err as Error).message));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ----------------------------------------------------------------
|
|
66
|
+
// 2. Resolve profile
|
|
67
|
+
// ----------------------------------------------------------------
|
|
68
|
+
const profileName = flags.profile || getFirstProfile(agentConfig);
|
|
69
|
+
|
|
70
|
+
if (!profileName) {
|
|
71
|
+
logger.error('No deploy profiles found in agent.json');
|
|
72
|
+
logger.log('Add a deploy profile under the "deploy" field in agent.json');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const profile = getProfile(agentConfig, profileName);
|
|
77
|
+
if (!profile) {
|
|
78
|
+
logger.error(`Profile "${profileName}" not found in agent.json`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate it's a local profile
|
|
83
|
+
if (profile.target !== 'local') {
|
|
84
|
+
logger.error(`Profile "${profileName}" is not a local profile (target: ${profile.target})`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
logger.log(successColor(`✓ Using profile: ${profileName}`));
|
|
89
|
+
|
|
90
|
+
// ----------------------------------------------------------------
|
|
91
|
+
// 2.5. SECRETS INJECTION
|
|
92
|
+
// If profile declares secrets, inject KADI_* env vars into all services
|
|
93
|
+
// Supports both legacy single-vault and multi-vault configurations.
|
|
94
|
+
// ----------------------------------------------------------------
|
|
95
|
+
const normalizedSecrets = normalizeSecrets(profile.secrets as any);
|
|
96
|
+
const hasSecrets = hasAnySecrets(normalizedSecrets);
|
|
97
|
+
|
|
98
|
+
let deployNonce: string | undefined;
|
|
99
|
+
let brokerUrl: string | null = null;
|
|
100
|
+
let secretsVault: string | undefined;
|
|
101
|
+
let secretsSources: NormalizedVaultSource[] = [];
|
|
102
|
+
|
|
103
|
+
if (hasSecrets) {
|
|
104
|
+
const result = prepareSecretsForDeployment({
|
|
105
|
+
profile,
|
|
106
|
+
agentConfig,
|
|
107
|
+
projectRoot,
|
|
108
|
+
logger,
|
|
109
|
+
colors: { dim, success: successColor, error: errorColor },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!result) return;
|
|
113
|
+
|
|
114
|
+
deployNonce = result.deployNonce;
|
|
115
|
+
brokerUrl = result.brokerUrl;
|
|
116
|
+
secretsVault = result.secretsVault;
|
|
117
|
+
secretsSources = result.secretsSources;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ----------------------------------------------------------------
|
|
121
|
+
// 3. Build deployment options for deploy-ability
|
|
122
|
+
// ----------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
// Spinner reference for progress updates
|
|
125
|
+
let progressSpinner: Ora | null = null;
|
|
126
|
+
|
|
127
|
+
const deployOptions: LocalDeploymentOptions = {
|
|
128
|
+
projectRoot,
|
|
129
|
+
profile: profileName,
|
|
130
|
+
// Pass the transformed profile with injected env vars
|
|
131
|
+
loadedProfile: {
|
|
132
|
+
name: profileName,
|
|
133
|
+
profile,
|
|
134
|
+
agent: agentConfig,
|
|
135
|
+
projectRoot,
|
|
136
|
+
},
|
|
137
|
+
engine: profile.engine || flags.engine || 'docker',
|
|
138
|
+
dryRun: flags.dryRun || false,
|
|
139
|
+
verbose: flags.verbose || false,
|
|
140
|
+
|
|
141
|
+
// Progress callback - updates spinner with structured events
|
|
142
|
+
onProgress: (event) => {
|
|
143
|
+
if (progressSpinner && event.message) {
|
|
144
|
+
progressSpinner.text = event.message;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// Logger for deploy-ability to use (errors only, no progress messages)
|
|
149
|
+
logger: {
|
|
150
|
+
log: (msg: string) => logger.log(msg),
|
|
151
|
+
error: (msg: string) => logger.error(msg),
|
|
152
|
+
warn: (msg: string) => logger.log(warning(msg)),
|
|
153
|
+
debug: flags.verbose ? (msg: string) => logger.log(msg) : () => {}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ----------------------------------------------------------------
|
|
158
|
+
// 4. Execute deployment using deploy-ability
|
|
159
|
+
// ----------------------------------------------------------------
|
|
160
|
+
if (flags.dryRun) {
|
|
161
|
+
logger.log('\n' + warning('⚠️ DRY RUN MODE - No deployment will be created'));
|
|
162
|
+
} else if (!flags.yes) {
|
|
163
|
+
// Confirm deployment
|
|
164
|
+
const proceed = await confirmPrompt(
|
|
165
|
+
`Deploy locally using ${deployOptions.engine}?`,
|
|
166
|
+
true
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (!proceed) {
|
|
170
|
+
logger.log('Deployment cancelled');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ----------------------------------------------------------------
|
|
176
|
+
// 4. Start listening for secrets BEFORE deploying (to avoid race condition)
|
|
177
|
+
// The agent may request secrets immediately after starting, so we need
|
|
178
|
+
// to be subscribed before the container starts.
|
|
179
|
+
// ----------------------------------------------------------------
|
|
180
|
+
let secretsPromise: Promise<WaitForSecretsResult> | null = null;
|
|
181
|
+
|
|
182
|
+
if (hasSecrets && deployNonce && brokerUrl && !flags.dryRun) {
|
|
183
|
+
logger.log(dim(`\nConnecting to broker to listen for secret requests...`));
|
|
184
|
+
|
|
185
|
+
// Start listening NOW (don't await - let it run in background)
|
|
186
|
+
secretsPromise = waitForSecretRequest({
|
|
187
|
+
brokerUrl,
|
|
188
|
+
expectedNonce: deployNonce,
|
|
189
|
+
timeout: 5 * 60 * 1000, // 5 minutes
|
|
190
|
+
logger: {
|
|
191
|
+
log: (msg: string) => logger.log(dim(msg)),
|
|
192
|
+
error: (msg: string) => logger.error(errorColor(msg)),
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Wait for broker subscription before starting containers
|
|
197
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ----------------------------------------------------------------
|
|
201
|
+
// 5. Execute deployment using deploy-ability
|
|
202
|
+
// ----------------------------------------------------------------
|
|
203
|
+
progressSpinner = startSpinner(`Deploying locally with ${deployOptions.engine}...`);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const result = await deployToLocal(deployOptions);
|
|
207
|
+
|
|
208
|
+
if (!result.success) {
|
|
209
|
+
failSpinner(progressSpinner, 'Deployment failed');
|
|
210
|
+
logger.error(errorColor(result.error.message));
|
|
211
|
+
|
|
212
|
+
// Show context details (command, exit code, stderr)
|
|
213
|
+
const err = result.error as unknown as { context?: Record<string, unknown> };
|
|
214
|
+
if (err.context) {
|
|
215
|
+
if (err.context.error) logger.error(`Error: ${err.context.error}`);
|
|
216
|
+
if (err.context.stderr) logger.error(`Stderr: ${err.context.stderr}`);
|
|
217
|
+
if (err.context.exitCode !== undefined) logger.error(`Exit code: ${err.context.exitCode}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (flags.verbose && result.error.cause) {
|
|
221
|
+
logger.error('\nCause:', result.error.cause);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
succeedSpinner(progressSpinner, 'Deployment complete!');
|
|
228
|
+
|
|
229
|
+
// ----------------------------------------------------------------
|
|
230
|
+
// 6. Display results
|
|
231
|
+
// ----------------------------------------------------------------
|
|
232
|
+
const data = result.data;
|
|
233
|
+
|
|
234
|
+
if ('dryRun' in data && data.dryRun) {
|
|
235
|
+
// Dry run result
|
|
236
|
+
displayLocalDryRunInfo(profileName, data);
|
|
237
|
+
} else {
|
|
238
|
+
// Actual deployment result
|
|
239
|
+
const deploymentData = data as LocalDeploymentData;
|
|
240
|
+
|
|
241
|
+
if (flags.verbose) {
|
|
242
|
+
displayLocalDeploymentInfo(deploymentData);
|
|
243
|
+
} else {
|
|
244
|
+
displayLocalDeploymentSummary(deploymentData);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ----------------------------------------------------------------
|
|
248
|
+
// 6.5. Write deployment lock file for `kadi deploy down`
|
|
249
|
+
// ----------------------------------------------------------------
|
|
250
|
+
try {
|
|
251
|
+
const lock = buildLocalLock(profileName, deploymentData, flags.label);
|
|
252
|
+
const instanceId = await writeLockFile(projectRoot, lock);
|
|
253
|
+
logger.log(dim(` Instance: ${instanceId} (profile: ${profileName}${flags.label ? `, label: ${flags.label}` : ''})`));
|
|
254
|
+
logger.log(dim(` Use \`kadi deploy down --instance ${instanceId}\` to tear down.`));
|
|
255
|
+
if (flags.verbose) {
|
|
256
|
+
logger.log(dim('Lock file written: .kadi-deploy.lock'));
|
|
257
|
+
}
|
|
258
|
+
} catch (lockErr) {
|
|
259
|
+
// Non-fatal: deployment succeeded even if lock write fails
|
|
260
|
+
logger.log(warning(`Could not write lock file: ${(lockErr as Error).message}`));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ----------------------------------------------------------------
|
|
264
|
+
// 7. Wait for secret request result (we started listening earlier)
|
|
265
|
+
// ----------------------------------------------------------------
|
|
266
|
+
if (secretsPromise && brokerUrl && secretsVault) {
|
|
267
|
+
logger.log(dim('\n─────────────────────────────────────────────────────────────────'));
|
|
268
|
+
logger.log(bold('Waiting for agent to request secrets...'));
|
|
269
|
+
logger.log(dim(`Broker: ${brokerUrl}`));
|
|
270
|
+
logger.log(dim('Press Ctrl+C to skip (agent will not receive secrets)'));
|
|
271
|
+
logger.log(dim('─────────────────────────────────────────────────────────────────\n'));
|
|
272
|
+
|
|
273
|
+
// Handle Ctrl+C during secret wait - force exit cleanly
|
|
274
|
+
const sigintHandler = () => {
|
|
275
|
+
logger.log(warning('\n\nSkipping secret sharing (Ctrl+C)'));
|
|
276
|
+
process.exit(0);
|
|
277
|
+
};
|
|
278
|
+
process.on('SIGINT', sigintHandler);
|
|
279
|
+
|
|
280
|
+
const waitResult = await secretsPromise;
|
|
281
|
+
|
|
282
|
+
// Remove handler after secrets are done
|
|
283
|
+
process.off('SIGINT', sigintHandler);
|
|
284
|
+
|
|
285
|
+
if (waitResult.success && waitResult.request) {
|
|
286
|
+
const request = waitResult.request;
|
|
287
|
+
|
|
288
|
+
// Display request info
|
|
289
|
+
logger.log('');
|
|
290
|
+
logger.log(successColor('Agent connected and requesting secrets!'));
|
|
291
|
+
logger.log('');
|
|
292
|
+
logger.log(` Agent ID: ${request.agentId}`);
|
|
293
|
+
logger.log(` Required: ${request.required.join(', ')}`);
|
|
294
|
+
if (request.optional?.length) {
|
|
295
|
+
logger.log(` Optional: ${request.optional.join(', ')}`);
|
|
296
|
+
}
|
|
297
|
+
logger.log('');
|
|
298
|
+
|
|
299
|
+
// Prompt for approval (auto-approve with --yes flag)
|
|
300
|
+
const approved = flags.yes || await confirmPrompt('Share these secrets with the agent?', true);
|
|
301
|
+
|
|
302
|
+
if (!approved) {
|
|
303
|
+
logger.log(warning('\nSecret sharing declined.'));
|
|
304
|
+
// Notify agent so it can fail immediately instead of timing out
|
|
305
|
+
await sendRejection({
|
|
306
|
+
brokerUrl,
|
|
307
|
+
agentId: request.agentId,
|
|
308
|
+
reason: 'User declined to share secrets',
|
|
309
|
+
logger: {
|
|
310
|
+
log: (msg: string) => logger.log(dim(msg)),
|
|
311
|
+
error: (msg: string) => logger.error(errorColor(msg)),
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
} else {
|
|
315
|
+
const allRequestedKeys = [
|
|
316
|
+
...request.required,
|
|
317
|
+
...(request.optional || []),
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
// Multi-vault: read from each vault source
|
|
321
|
+
const secrets: Record<string, string> = {};
|
|
322
|
+
for (const source of secretsSources) {
|
|
323
|
+
const sourceKeys = [...source.required, ...source.optional]
|
|
324
|
+
.filter(k => allRequestedKeys.includes(k));
|
|
325
|
+
if (sourceKeys.length === 0) continue;
|
|
326
|
+
|
|
327
|
+
logger.log(dim(`\nReading secrets from vault '${source.vault}'...`));
|
|
328
|
+
const vaultSecrets = readSecretsFromCli({
|
|
329
|
+
keys: sourceKeys,
|
|
330
|
+
vault: source.vault,
|
|
331
|
+
cwd: projectRoot,
|
|
332
|
+
onError: (key, error) => {
|
|
333
|
+
logger.log(dim(` Failed to read '${key}' from vault '${source.vault}': ${error}`));
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
Object.assign(secrets, vaultSecrets);
|
|
337
|
+
}
|
|
338
|
+
const foundKeys = Object.keys(secrets);
|
|
339
|
+
|
|
340
|
+
const missingRequired = request.required.filter(
|
|
341
|
+
(key) => !secrets[key]
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
if (missingRequired.length > 0) {
|
|
345
|
+
const vaultNames = secretsSources.map(s => s.vault).join(', ');
|
|
346
|
+
logger.log(warning(`\nMissing required secrets in vault(s) '${vaultNames}': ${missingRequired.join(', ')}`));
|
|
347
|
+
logger.log(dim(`Use "kadi secret set <key> <value> -v <vault>" to add them.`));
|
|
348
|
+
} else if (foundKeys.length === 0) {
|
|
349
|
+
const vaultNames = secretsSources.map(s => s.vault).join(', ');
|
|
350
|
+
logger.log(warning(`\nNo secrets found in vault(s) '${vaultNames}'.`));
|
|
351
|
+
} else if (!request.publicKey) {
|
|
352
|
+
logger.log(warning('\nAgent did not provide public key for encryption.'));
|
|
353
|
+
} else {
|
|
354
|
+
logger.log(successColor(`\nSharing ${foundKeys.length} secret(s) with agent (encrypted)...`));
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
await shareSecrets({
|
|
358
|
+
brokerUrl,
|
|
359
|
+
agentId: request.agentId,
|
|
360
|
+
agentPublicKey: request.publicKey,
|
|
361
|
+
secrets,
|
|
362
|
+
logger: {
|
|
363
|
+
log: (msg: string) => logger.log(dim(msg)),
|
|
364
|
+
error: (msg: string) => logger.error(errorColor(msg)),
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
logger.log(successColor('Secrets shared successfully!'));
|
|
369
|
+
logger.log(dim(` Shared: ${foundKeys.join(', ')}`));
|
|
370
|
+
} catch (shareErr) {
|
|
371
|
+
logger.log(warning(`\nFailed to share secrets: ${(shareErr as Error).message}`));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} else if (waitResult.timedOut) {
|
|
376
|
+
logger.log(warning('\nTimeout waiting for agent to request secrets.'));
|
|
377
|
+
} else if (waitResult.error) {
|
|
378
|
+
logger.log(warning(`\nSecret handshake failed: ${waitResult.error}`));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
logger.log('');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
} catch (err) {
|
|
386
|
+
failSpinner(progressSpinner, 'Deployment failed');
|
|
387
|
+
logger.error(errorColor((err as Error).message));
|
|
388
|
+
|
|
389
|
+
if (flags.verbose) {
|
|
390
|
+
console.error(err);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Displays local deployment information
|
|
397
|
+
*
|
|
398
|
+
* @param data - Deployment data from deploy-ability
|
|
399
|
+
*/
|
|
400
|
+
function displayLocalDeploymentInfo(data: LocalDeploymentData): void {
|
|
401
|
+
const separator = '─'.repeat(70);
|
|
402
|
+
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log(successColor('✓ Local Deployment Successful!'));
|
|
405
|
+
console.log('');
|
|
406
|
+
console.log(separator);
|
|
407
|
+
console.log(formatHeader('Deployment Information'));
|
|
408
|
+
console.log(separator);
|
|
409
|
+
console.log('');
|
|
410
|
+
|
|
411
|
+
console.log(formatKeyValue('Profile', data.profile));
|
|
412
|
+
console.log(formatKeyValue('Engine', data.engine));
|
|
413
|
+
console.log(formatKeyValue('Network', data.network));
|
|
414
|
+
console.log(formatKeyValue('Compose Path', data.composePath));
|
|
415
|
+
console.log(formatKeyValue('Deployed At', data.deployedAt.toLocaleString()));
|
|
416
|
+
console.log('');
|
|
417
|
+
|
|
418
|
+
console.log(formatHeader('Services'));
|
|
419
|
+
console.log(separator);
|
|
420
|
+
console.log('');
|
|
421
|
+
|
|
422
|
+
for (const serviceName of data.services) {
|
|
423
|
+
const containerId = data.containers[serviceName];
|
|
424
|
+
const endpoint = data.endpoints[serviceName];
|
|
425
|
+
|
|
426
|
+
console.log(formatKeyValue(serviceName, ''));
|
|
427
|
+
console.log(` Container: ${containerId}`);
|
|
428
|
+
if (endpoint) {
|
|
429
|
+
console.log(` Endpoint: ${endpoint}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.log('');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Displays local deployment summary
|
|
438
|
+
*
|
|
439
|
+
* @param data - Deployment data from deploy-ability
|
|
440
|
+
*/
|
|
441
|
+
function displayLocalDeploymentSummary(data: LocalDeploymentData): void {
|
|
442
|
+
console.log('');
|
|
443
|
+
console.log(successColor(`✓ Deployed ${data.services.length} service(s) locally`));
|
|
444
|
+
|
|
445
|
+
for (const serviceName of data.services) {
|
|
446
|
+
const endpoint = data.endpoints[serviceName];
|
|
447
|
+
if (endpoint) {
|
|
448
|
+
console.log(` ${serviceName}: ${endpoint}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
console.log('');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Displays dry-run information for local deployment
|
|
457
|
+
*
|
|
458
|
+
* @param profileName - Profile name
|
|
459
|
+
* @param data - Dry-run data from deploy-ability
|
|
460
|
+
*/
|
|
461
|
+
function displayLocalDryRunInfo(profileName: string, data: LocalDryRunData): void {
|
|
462
|
+
console.log('');
|
|
463
|
+
console.log(formatHeader('Dry Run - Local Deployment Preview'));
|
|
464
|
+
console.log(` ${formatKeyValue('Profile', profileName)}`);
|
|
465
|
+
console.log(` ${formatKeyValue('Engine', data.engine)}`);
|
|
466
|
+
console.log(` ${formatKeyValue('Services', data.services.join(', '))}`);
|
|
467
|
+
console.log('');
|
|
468
|
+
console.log('Generated docker-compose.yml:');
|
|
469
|
+
console.log('─'.repeat(70));
|
|
470
|
+
console.log(data.composeFile);
|
|
471
|
+
console.log('─'.repeat(70));
|
|
472
|
+
console.log('');
|
|
473
|
+
console.log(warning('ℹ️ This is a dry run - no containers were started'));
|
|
474
|
+
console.log('');
|
|
475
|
+
}
|