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,892 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Temporary Container Registry Manager - TypeScript Version
|
|
3
|
+
*
|
|
4
|
+
* This builds on the excellent logic from registry-manager.js but adapts it
|
|
5
|
+
* for the new registry-aware SDL generation flow.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AkashProfile } from './schemas/profiles.js';
|
|
9
|
+
import type { IKadiLogger } from './types/external.js';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
import debug from 'debug';
|
|
16
|
+
|
|
17
|
+
import { TunneledContainerRegistry } from '@kadi.build/container-registry-ability';
|
|
18
|
+
|
|
19
|
+
// @ts-ignore - config resolver from tunnel-services (JavaScript)
|
|
20
|
+
import { resolveTunnelConfig } from '@kadi.build/tunnel-services';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Debug logger for registry operations
|
|
24
|
+
*/
|
|
25
|
+
const log = debug('kadi:registry');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Container mapping that tracks original image to registry URL transformation
|
|
29
|
+
*/
|
|
30
|
+
interface ContainerMapping {
|
|
31
|
+
originalImage: string; // e.g., "my-app" or "localhost/my-app"
|
|
32
|
+
serviceName: string; // e.g., "frontend"
|
|
33
|
+
registryUrl: string; // e.g., "temp-registry.serveo.net/my-app:latest"
|
|
34
|
+
repoName: string; // e.g., "my-app"
|
|
35
|
+
imageTag: string; // e.g., "latest"
|
|
36
|
+
actualAlias: string; // e.g., "my-app" (sanitized for registry)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Registry credentials for SDL generation
|
|
41
|
+
*/
|
|
42
|
+
interface RegistryCredentials {
|
|
43
|
+
host: string; // e.g., "temp-registry.serveo.net"
|
|
44
|
+
username: string; // e.g., access key
|
|
45
|
+
password: string; // e.g., secret key
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Registry information for container registry instance
|
|
50
|
+
*
|
|
51
|
+
* Aligned with @kadi.build/container-registry-ability RegistryInfo type.
|
|
52
|
+
*/
|
|
53
|
+
import type {
|
|
54
|
+
RegistryInfo as CRARegistryInfo,
|
|
55
|
+
ContainerInfo as CRAContainerInfo,
|
|
56
|
+
} from '@kadi.build/container-registry-ability';
|
|
57
|
+
|
|
58
|
+
type RegistryInfo = CRARegistryInfo;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Registry URL components including local and tunnel endpoints
|
|
62
|
+
*/
|
|
63
|
+
interface RegistryUrls {
|
|
64
|
+
localUrl: string; // e.g., "http://localhost:3000"
|
|
65
|
+
localDomain: string; // e.g., "localhost:3000"
|
|
66
|
+
tunnelUrl: string | null; // e.g., "https://abc123.serveo.net" or null
|
|
67
|
+
tunnelDomain: string | null; // e.g., "abc123.serveo.net" or null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Configuration options for starting the temporary registry
|
|
72
|
+
*
|
|
73
|
+
* Controls registry server, tunnel service, and lifecycle behavior.
|
|
74
|
+
*/
|
|
75
|
+
interface RegistryOptions {
|
|
76
|
+
/** Port number for the local registry server (default: 3000) */
|
|
77
|
+
port?: number;
|
|
78
|
+
|
|
79
|
+
/** Tunnel service to expose registry publicly ('kadi', 'ngrok', 'serveo', or 'localtunnel') */
|
|
80
|
+
tunnelService?: 'kadi' | 'ngrok' | 'serveo' | 'localtunnel';
|
|
81
|
+
|
|
82
|
+
/** Container engine to use for loading images ('docker' or 'podman') */
|
|
83
|
+
containerEngine?: 'docker' | 'podman';
|
|
84
|
+
|
|
85
|
+
/** Duration in milliseconds before auto-shutdown (default: 600000 = 10 minutes) */
|
|
86
|
+
durationMs?: number;
|
|
87
|
+
|
|
88
|
+
/** Enable automatic shutdown after downloads complete (default: true) */
|
|
89
|
+
autoShutdown?: boolean;
|
|
90
|
+
|
|
91
|
+
/** Authentication token for tunnel service (required for some services like ngrok) */
|
|
92
|
+
tunnelAuthToken?: string;
|
|
93
|
+
|
|
94
|
+
/** Region for tunnel service (e.g., 'us', 'eu', 'ap') */
|
|
95
|
+
tunnelRegion?: string;
|
|
96
|
+
|
|
97
|
+
/** Protocol for tunnel service ('http' or 'https') */
|
|
98
|
+
tunnelProtocol?: string;
|
|
99
|
+
|
|
100
|
+
/** Custom subdomain for tunnel (may require paid plan) */
|
|
101
|
+
tunnelSubdomain?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Container information returned by the registry
|
|
106
|
+
*
|
|
107
|
+
* Represents a container that has been added to the temporary registry.
|
|
108
|
+
* The alias is the sanitized name used to reference the container in the registry.
|
|
109
|
+
*/
|
|
110
|
+
interface ContainerInfo {
|
|
111
|
+
/** Sanitized container alias used in the registry (e.g., "my-app") */
|
|
112
|
+
alias: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export class TemporaryContainerRegistryManager {
|
|
116
|
+
private logger: IKadiLogger;
|
|
117
|
+
private registry: TunneledContainerRegistry | null = null;
|
|
118
|
+
private registryInfo: RegistryInfo | null = null;
|
|
119
|
+
|
|
120
|
+
// Core data: maps original image names to their registry URLs
|
|
121
|
+
private containerMappings = new Map<string, ContainerMapping>();
|
|
122
|
+
|
|
123
|
+
constructor(logger: IKadiLogger) {
|
|
124
|
+
this.logger = logger;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Start temporary registry using TunneledContainerRegistry
|
|
129
|
+
*
|
|
130
|
+
* Creates a local container registry on the specified port and exposes it publicly
|
|
131
|
+
* via a tunnel service (kadi, ngrok, serveo, or localtunnel). The registry is used to make
|
|
132
|
+
* local Docker images accessible to Akash providers during deployment.
|
|
133
|
+
*
|
|
134
|
+
* @param options - Configuration options for registry and tunnel
|
|
135
|
+
* @returns Promise that resolves when registry is running and accessible
|
|
136
|
+
* @throws Error if registry fails to start or tunnel cannot be established
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* await manager.startTemporaryRegistry({
|
|
141
|
+
* port: 3000,
|
|
142
|
+
* tunnelService: 'serveo',
|
|
143
|
+
* containerEngine: 'docker',
|
|
144
|
+
* durationMs: 600000,
|
|
145
|
+
* autoShutdown: false
|
|
146
|
+
* });
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
async startTemporaryRegistry(options: RegistryOptions): Promise<void> {
|
|
150
|
+
if (this.registry) {
|
|
151
|
+
log('Temporary registry already running');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log('Starting temporary container registry...');
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// ----------------------------------------------------------------
|
|
159
|
+
// Resolve tunnel configuration using config.yml walk-up + vault
|
|
160
|
+
// + .env fallback + process.env overrides.
|
|
161
|
+
//
|
|
162
|
+
// This replaces the previous dotenv-based loading. The tunnel
|
|
163
|
+
// config resolver (from @kadi.build/tunnel-services) handles:
|
|
164
|
+
// 1. config.yml "tunnel" section (non-secret settings)
|
|
165
|
+
// 2. secrets.toml vault "tunnel" for tokens (via secret-ability)
|
|
166
|
+
// 3. .env file walk-up (fallback when no vault found)
|
|
167
|
+
// 4. process.env overrides (always win)
|
|
168
|
+
// ----------------------------------------------------------------
|
|
169
|
+
const tunnelResolved = await resolveTunnelConfig();
|
|
170
|
+
const { config: tunnelCfg, secrets: tunnelSecrets } = tunnelResolved;
|
|
171
|
+
|
|
172
|
+
const resolvedTunnelService = options.tunnelService || tunnelCfg.default_service || 'kadi';
|
|
173
|
+
|
|
174
|
+
// Determine tunnel auth token: explicit option > vault > process.env fallback
|
|
175
|
+
const resolvedAuthToken =
|
|
176
|
+
options.tunnelAuthToken ||
|
|
177
|
+
tunnelSecrets.kadi_token ||
|
|
178
|
+
tunnelSecrets.ngrok_token;
|
|
179
|
+
|
|
180
|
+
const envNgrokRegion = tunnelCfg.ngrok_region || tunnelCfg.region || process.env.NGROK_REGION || undefined;
|
|
181
|
+
const envNgrokProtocol = tunnelCfg.ngrok_protocol || process.env.NGROK_PROTOCOL || undefined;
|
|
182
|
+
|
|
183
|
+
// Warn if kadi tunnel is selected but no token found
|
|
184
|
+
if (resolvedTunnelService === 'kadi' && !resolvedAuthToken) {
|
|
185
|
+
this.logger.log(` ⚠ KADI_TUNNEL_TOKEN not set — kadi tunnel (frpc) requires a token`);
|
|
186
|
+
this.logger.log(` Set via: kadi secret set -v tunnel KADI_TUNNEL_TOKEN`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ----------------------------------------------------------------
|
|
190
|
+
// CRITICAL: Force-write resolved tunnel values into process.env.
|
|
191
|
+
//
|
|
192
|
+
// Some downstream packages (CRA → S3HttpServer → FileSharingServer)
|
|
193
|
+
// may still reference process.env for tunnel config during the
|
|
194
|
+
// transition period. We propagate resolved values to ensure
|
|
195
|
+
// compatibility.
|
|
196
|
+
//
|
|
197
|
+
// INFRASTRUCTURE DEFAULTS:
|
|
198
|
+
// The kadi tunnel architecture is WSS-only (no direct TCP on port
|
|
199
|
+
// 7000). We ensure transport/wssControlHost defaults are always set.
|
|
200
|
+
// ----------------------------------------------------------------
|
|
201
|
+
const KADI_DEFAULTS = {
|
|
202
|
+
KADI_TUNNEL_SERVER: 'broker.kadi.build',
|
|
203
|
+
KADI_TUNNEL_DOMAIN: 'tunnel.kadi.build',
|
|
204
|
+
KADI_TUNNEL_MODE: 'frpc',
|
|
205
|
+
KADI_TUNNEL_TRANSPORT: 'wss',
|
|
206
|
+
KADI_TUNNEL_WSS_HOST: 'tunnel-control.kadi.build',
|
|
207
|
+
KADI_TUNNEL_PORT: '7000',
|
|
208
|
+
KADI_TUNNEL_SSH_PORT: '2200',
|
|
209
|
+
KADI_AGENT_ID: 'kadi',
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const envVarsToPropagate: Record<string, string | undefined> = {
|
|
213
|
+
KADI_TUNNEL_TOKEN: resolvedAuthToken,
|
|
214
|
+
KADI_TUNNEL_SERVER: tunnelCfg.server_addr || process.env.KADI_TUNNEL_SERVER || KADI_DEFAULTS.KADI_TUNNEL_SERVER,
|
|
215
|
+
KADI_TUNNEL_DOMAIN: tunnelCfg.tunnel_domain || process.env.KADI_TUNNEL_DOMAIN || KADI_DEFAULTS.KADI_TUNNEL_DOMAIN,
|
|
216
|
+
KADI_TUNNEL_MODE: tunnelCfg.mode || process.env.KADI_TUNNEL_MODE || KADI_DEFAULTS.KADI_TUNNEL_MODE,
|
|
217
|
+
KADI_TUNNEL_TRANSPORT: tunnelCfg.transport || process.env.KADI_TUNNEL_TRANSPORT || KADI_DEFAULTS.KADI_TUNNEL_TRANSPORT,
|
|
218
|
+
KADI_TUNNEL_WSS_HOST: tunnelCfg.wss_control_host || process.env.KADI_TUNNEL_WSS_HOST || KADI_DEFAULTS.KADI_TUNNEL_WSS_HOST,
|
|
219
|
+
KADI_TUNNEL_PORT: tunnelCfg.server_port?.toString() || process.env.KADI_TUNNEL_PORT || KADI_DEFAULTS.KADI_TUNNEL_PORT,
|
|
220
|
+
KADI_TUNNEL_SSH_PORT: tunnelCfg.ssh_port?.toString() || process.env.KADI_TUNNEL_SSH_PORT || KADI_DEFAULTS.KADI_TUNNEL_SSH_PORT,
|
|
221
|
+
KADI_AGENT_ID: tunnelCfg.agent_id || process.env.KADI_AGENT_ID || KADI_DEFAULTS.KADI_AGENT_ID,
|
|
222
|
+
NGROK_AUTH_TOKEN: tunnelSecrets.ngrok_token,
|
|
223
|
+
NGROK_REGION: envNgrokRegion as string | undefined,
|
|
224
|
+
NGROK_PROTOCOL: envNgrokProtocol as string | undefined,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
for (const [key, value] of Object.entries(envVarsToPropagate)) {
|
|
228
|
+
if (value) {
|
|
229
|
+
process.env[key] = value;
|
|
230
|
+
log('Env set: %s=%s', key, key.includes('TOKEN') ? '***' : value);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
log('Tunnel env: server=%s domain=%s mode=%s transport=%s wssHost=%s',
|
|
235
|
+
process.env.KADI_TUNNEL_SERVER, process.env.KADI_TUNNEL_DOMAIN,
|
|
236
|
+
process.env.KADI_TUNNEL_MODE, process.env.KADI_TUNNEL_TRANSPORT,
|
|
237
|
+
process.env.KADI_TUNNEL_WSS_HOST);
|
|
238
|
+
|
|
239
|
+
// Create TunneledContainerRegistry instance
|
|
240
|
+
this.registry = new TunneledContainerRegistry({
|
|
241
|
+
port: options.port || 3000,
|
|
242
|
+
tunnelService: resolvedTunnelService,
|
|
243
|
+
tunnelOptions: {
|
|
244
|
+
// Allow CLI options to override env if provided
|
|
245
|
+
authToken: resolvedAuthToken,
|
|
246
|
+
region: options.tunnelRegion || envNgrokRegion,
|
|
247
|
+
protocol: options.tunnelProtocol || envNgrokProtocol,
|
|
248
|
+
subdomain: options.tunnelSubdomain,
|
|
249
|
+
// Control fallback order: kadi (primary) → ngrok → localtunnel → pinggy → localhost.run → serveo
|
|
250
|
+
managerOptions: {
|
|
251
|
+
fallbackServices: process.env.TUNNEL_FALLBACK_SERVICES || 'ngrok,localtunnel,pinggy,localhost.run,serveo'
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
enableMonitoring: false,
|
|
255
|
+
// Disable verbose logging - only show errors
|
|
256
|
+
// User can enable with DEBUG=kadi:* environment variable if needed
|
|
257
|
+
enableLogging: false,
|
|
258
|
+
logLevel: 'error',
|
|
259
|
+
// Enable auto-shutdown by default to cleanup resources after deployment completes
|
|
260
|
+
// Can be disabled by passing options.autoShutdown = false
|
|
261
|
+
autoShutdown: options.autoShutdown ?? true,
|
|
262
|
+
containerType: options.containerEngine,
|
|
263
|
+
duration: options.durationMs
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Start the registry (includes tunnel establishment)
|
|
267
|
+
await this.registry.start();
|
|
268
|
+
this.registryInfo = this.registry.getRegistryInfo();
|
|
269
|
+
|
|
270
|
+
// Ensure we got valid registry info
|
|
271
|
+
if (!this.registryInfo) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
'Failed to get registry information after starting registry'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Ensure at minimum we have a local URL (tunnel URL is optional)
|
|
278
|
+
if (!this.registryInfo.localUrl) {
|
|
279
|
+
throw new Error('Registry started but no local URL available');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Report tunnel status to user
|
|
283
|
+
if (this.registryInfo.tunnelUrl) {
|
|
284
|
+
this.logger.log(` ✓ Tunnel connected: ${this.registryInfo.tunnelUrl}`);
|
|
285
|
+
|
|
286
|
+
// Detect if kadi tunnel was requested but a fallback service was used
|
|
287
|
+
const tunnelUrl = this.registryInfo.tunnelUrl;
|
|
288
|
+
const isKadiFallback = resolvedTunnelService === 'kadi' && (
|
|
289
|
+
tunnelUrl.includes('serveo') ||
|
|
290
|
+
tunnelUrl.includes('ngrok') ||
|
|
291
|
+
tunnelUrl.includes('localtunnel') ||
|
|
292
|
+
tunnelUrl.includes('pinggy')
|
|
293
|
+
);
|
|
294
|
+
if (isKadiFallback) {
|
|
295
|
+
this.logger.log(` ⚠ KADI tunnel failed — fell back to alternate service`);
|
|
296
|
+
this.logger.log(` This usually means frpc is not installed or couldn't connect.`);
|
|
297
|
+
this.logger.log(` Install frpc: brew install frpc (macOS) or see https://github.com/fatedier/frp/releases`);
|
|
298
|
+
this.logger.log(` Run with DEBUG=kadi:* for full tunnel diagnostics.`);
|
|
299
|
+
log('KADI tunnel fallback detected. Requested: %s, actual URL: %s', resolvedTunnelService, tunnelUrl);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
this.logger.log(` ⚠ No tunnel URL — registry only accessible locally at ${this.registryInfo.localUrl}`);
|
|
303
|
+
this.logger.log(` Akash providers will NOT be able to pull images!`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Determine the primary registry URL (prefer tunnel over local)
|
|
307
|
+
const primaryRegistryUrl =
|
|
308
|
+
this.registryInfo.tunnelUrl || this.registryInfo.localUrl;
|
|
309
|
+
log('Registry started at: %s', primaryRegistryUrl);
|
|
310
|
+
|
|
311
|
+
await this.displayRegistryAccessInformation();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
await this.stopTemporaryRegistry();
|
|
314
|
+
throw new Error(`Failed to start temporary registry: ${error}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Display registry access information including domain and credentials
|
|
320
|
+
*
|
|
321
|
+
* Reused from temporary-container-registry.ts with the same fallback logic
|
|
322
|
+
*/
|
|
323
|
+
private async displayRegistryAccessInformation(): Promise<void> {
|
|
324
|
+
if (!this.registryInfo) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
// Try to get and display command help (same as temporary-container-registry.ts)
|
|
330
|
+
const commandHelp = await this.registry!.generateCommandHelp();
|
|
331
|
+
|
|
332
|
+
const registryDomain =
|
|
333
|
+
commandHelp?.registry?.registryDomain ||
|
|
334
|
+
(this.registryInfo.tunnelUrl || this.registryInfo.localUrl).replace(
|
|
335
|
+
/^https?:\/\//,
|
|
336
|
+
''
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
log('Registry domain: %s', registryDomain);
|
|
340
|
+
|
|
341
|
+
if (this.registryInfo.credentials) {
|
|
342
|
+
log('Access key: %s', this.registryInfo.credentials.accessKey);
|
|
343
|
+
log('Secret key: [hidden]');
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// Fallback display if command generation fails (same as temporary-container-registry.ts)
|
|
347
|
+
if (this.registryInfo.credentials) {
|
|
348
|
+
log('Registry credentials available');
|
|
349
|
+
log('Username: %s', this.registryInfo.credentials.accessKey);
|
|
350
|
+
log('Password: [hidden]');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check if a container image exists locally
|
|
357
|
+
*
|
|
358
|
+
* Uses `docker images -q` or `podman images -q` to check if an image exists
|
|
359
|
+
* in the local container engine.
|
|
360
|
+
*
|
|
361
|
+
* - `docker images -q <image>` returns the image hash if it exists
|
|
362
|
+
* - Returns empty string if image doesn't exist
|
|
363
|
+
* - Works for both tagged and untagged images
|
|
364
|
+
* - Handles shorthand names correctly (e.g., "nginx" vs "docker.io/library/nginx")
|
|
365
|
+
*
|
|
366
|
+
* @param imageName - Full image name with tag (e.g., "my-app:latest")
|
|
367
|
+
* @param engine - Container engine to use
|
|
368
|
+
* @returns True if image exists locally, false otherwise
|
|
369
|
+
*/
|
|
370
|
+
private checkImageExistsLocally(
|
|
371
|
+
imageName: string,
|
|
372
|
+
engine: 'docker' | 'podman'
|
|
373
|
+
): boolean {
|
|
374
|
+
try {
|
|
375
|
+
const result = execSync(`${engine} images -q ${imageName}`, {
|
|
376
|
+
encoding: 'utf8',
|
|
377
|
+
stdio: ['pipe', 'pipe', 'pipe'] // Suppress stderr
|
|
378
|
+
});
|
|
379
|
+
return result.trim().length > 0;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
// Command failed (engine not available or other error)
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Add local images from AkashProfile to the temporary registry
|
|
388
|
+
*
|
|
389
|
+
* **Simplified Logic (Reality-Based Detection):**
|
|
390
|
+
*
|
|
391
|
+
* Instead of guessing if an image is "local" based on name patterns
|
|
392
|
+
* (like checking if it has "/" in the name), we simply check if the
|
|
393
|
+
* image actually exists locally using `docker images -q <image>`.
|
|
394
|
+
*
|
|
395
|
+
* This approach:
|
|
396
|
+
* 1. Is more accurate (checks reality, not heuristics)
|
|
397
|
+
* 2. Is simpler (one check instead of multiple conditions)
|
|
398
|
+
* 3. Handles edge cases automatically:
|
|
399
|
+
* - Docker Hub shorthand ("nginx" → exists remotely, not locally)
|
|
400
|
+
* - Custom registries with default namespaces
|
|
401
|
+
* - Images that "look local" but are actually remote
|
|
402
|
+
*
|
|
403
|
+
* **Decision Flow:**
|
|
404
|
+
* - Image exists locally → Add to temporary registry and make publicly accessible
|
|
405
|
+
* - Image doesn't exist locally → Treat as remote reference (don't add to registry)
|
|
406
|
+
*
|
|
407
|
+
* **Why this works:**
|
|
408
|
+
* The temporary registry is ONLY needed for images that exist locally but need
|
|
409
|
+
* to be made publicly accessible to Akash providers. If an image doesn't exist
|
|
410
|
+
* locally, either:
|
|
411
|
+
* - It's a remote image (providers can pull it directly)
|
|
412
|
+
* - It doesn't exist anywhere (deployment will fail later with clear error)
|
|
413
|
+
*
|
|
414
|
+
* This prevents us from starting unnecessary infrastructure (tunnel, S3 server)
|
|
415
|
+
* and provides better error messages.
|
|
416
|
+
*/
|
|
417
|
+
async addLocalImagesToTemporaryRegistry(
|
|
418
|
+
loadedProfile: AkashProfile,
|
|
419
|
+
containerEngine: 'docker' | 'podman'
|
|
420
|
+
): Promise<void> {
|
|
421
|
+
log('Checking which service images exist locally...');
|
|
422
|
+
|
|
423
|
+
const serviceEntries = Object.entries(loadedProfile.services);
|
|
424
|
+
let localCount = 0;
|
|
425
|
+
|
|
426
|
+
for (const [serviceName, serviceConfig] of serviceEntries) {
|
|
427
|
+
// Type assertion: Zod validates this is AkashService
|
|
428
|
+
const typedService = serviceConfig as { image: string };
|
|
429
|
+
const imageName = typedService.image;
|
|
430
|
+
|
|
431
|
+
// Check if image exists locally (reality-based, not name-based)
|
|
432
|
+
if (this.checkImageExistsLocally(imageName, containerEngine)) {
|
|
433
|
+
localCount++;
|
|
434
|
+
// Image exists locally → add to temporary registry
|
|
435
|
+
this.logger.log(` 📦 ${serviceName}: ${imageName} (local → pushing to registry)`);
|
|
436
|
+
log('Image found locally for service %s: %s', serviceName, imageName);
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
// Use the intelligent fallback strategy from registry-manager.js
|
|
440
|
+
const mapping = await this.addContainerIntelligently(
|
|
441
|
+
imageName,
|
|
442
|
+
serviceName,
|
|
443
|
+
containerEngine
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// Store the mapping for later SDL generation
|
|
447
|
+
this.containerMappings.set(imageName, mapping);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
this.logger.error(
|
|
450
|
+
`❌ Failed to add local image '${imageName}' for service '${serviceName}': ${(error as Error).message}`
|
|
451
|
+
);
|
|
452
|
+
throw error; // Stop deployment if we can't host a required local image
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
// Image doesn't exist locally → treat as remote
|
|
456
|
+
log(`Skipping remote image: ${serviceName}: ${imageName}`);
|
|
457
|
+
log('Image not found locally, treating as remote for service %s: %s', serviceName, imageName);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (localCount > 0) {
|
|
462
|
+
this.logger.log(` ✓ ${localCount} local image(s) pushed to registry`);
|
|
463
|
+
}
|
|
464
|
+
log('Successfully processed %d local images', this.containerMappings.size);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Intelligent container addition with fallback strategies
|
|
469
|
+
*
|
|
470
|
+
* Adapted from registry-manager.js addContainerIntelligently but enhanced
|
|
471
|
+
* for TypeScript and our new flow.
|
|
472
|
+
*/
|
|
473
|
+
private async addContainerIntelligently(
|
|
474
|
+
imageName: string,
|
|
475
|
+
serviceName: string,
|
|
476
|
+
containerEngine: 'docker' | 'podman'
|
|
477
|
+
): Promise<ContainerMapping> {
|
|
478
|
+
// Parse image name (reuse logic from registry-manager.js)
|
|
479
|
+
const repoName = imageName.includes(':')
|
|
480
|
+
? imageName.split(':')[0]
|
|
481
|
+
: imageName;
|
|
482
|
+
const imageTag = imageName.includes(':')
|
|
483
|
+
? imageName.split(':')[1]
|
|
484
|
+
: 'latest';
|
|
485
|
+
|
|
486
|
+
log('Attempting to add container: %s', imageName);
|
|
487
|
+
|
|
488
|
+
// Strategy 1: Try to find and use tar file from kadi-build
|
|
489
|
+
// (Reuse findKadiBuildTarFile logic from registry-manager.js)
|
|
490
|
+
const tarPath = this.findKadiBuildTarFile(imageName);
|
|
491
|
+
if (tarPath) {
|
|
492
|
+
try {
|
|
493
|
+
log('Loading container from tar file: %s', tarPath);
|
|
494
|
+
|
|
495
|
+
const containerInfo = await this.registry!.addContainer({
|
|
496
|
+
type: 'tar',
|
|
497
|
+
name: repoName,
|
|
498
|
+
path: tarPath
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
log('Container loaded from tar file with alias: %s', containerInfo.alias);
|
|
502
|
+
|
|
503
|
+
return this.createContainerMapping(
|
|
504
|
+
imageName,
|
|
505
|
+
serviceName,
|
|
506
|
+
containerInfo,
|
|
507
|
+
repoName,
|
|
508
|
+
imageTag
|
|
509
|
+
);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
log('Failed to load from tar file: %s', (error as Error).message);
|
|
512
|
+
log('Falling back to container engine...');
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Strategy 2: Try to add from container engine (docker/podman)
|
|
517
|
+
// (Reuse logic from registry-manager.js)
|
|
518
|
+
try {
|
|
519
|
+
log('Attempting to add from %s engine: %s', containerEngine, imageName);
|
|
520
|
+
|
|
521
|
+
const containerInfo = await this.registry!.addContainer({
|
|
522
|
+
type: containerEngine,
|
|
523
|
+
name: repoName,
|
|
524
|
+
image: imageName
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
log('Container loaded from %s with alias: %s', containerEngine, containerInfo.alias);
|
|
528
|
+
|
|
529
|
+
return this.createContainerMapping(
|
|
530
|
+
imageName,
|
|
531
|
+
serviceName,
|
|
532
|
+
containerInfo,
|
|
533
|
+
repoName,
|
|
534
|
+
imageTag
|
|
535
|
+
);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
log('Failed to load from %s: %s', containerEngine, (error as Error).message);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Strategy 3: Show helpful error message (reuse from registry-manager.js)
|
|
541
|
+
this.showKadiBuildSuggestion(imageName, containerEngine);
|
|
542
|
+
throw new Error(
|
|
543
|
+
`Could not add container ${imageName}. See suggestions above.`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Create container mapping for registry URLs
|
|
549
|
+
*
|
|
550
|
+
* Transforms a local image reference into a complete registry URL mapping
|
|
551
|
+
* that can be used in SDL generation. Includes service name tracking for
|
|
552
|
+
* better debugging and error messages.
|
|
553
|
+
*
|
|
554
|
+
* @param originalImage - Original image name from agent.json (e.g., "my-app")
|
|
555
|
+
* @param serviceName - Service name from profile (e.g., "frontend")
|
|
556
|
+
* @param containerInfo - Container info from registry with alias
|
|
557
|
+
* @param repoName - Repository name extracted from image
|
|
558
|
+
* @param imageTag - Image tag (e.g., "latest")
|
|
559
|
+
* @returns Complete container mapping with registry URL
|
|
560
|
+
*/
|
|
561
|
+
private async createContainerMapping(
|
|
562
|
+
originalImage: string,
|
|
563
|
+
serviceName: string,
|
|
564
|
+
containerInfo: ContainerInfo,
|
|
565
|
+
repoName: string,
|
|
566
|
+
imageTag: string
|
|
567
|
+
): Promise<ContainerMapping> {
|
|
568
|
+
const actualAlias = containerInfo.alias;
|
|
569
|
+
const registryUrls = await this.registry!.getRegistryUrls();
|
|
570
|
+
const registryDomain = this.getPreferredDomain();
|
|
571
|
+
const registryImageUrl = `${registryDomain}/${actualAlias}:${imageTag}`;
|
|
572
|
+
|
|
573
|
+
const mapping: ContainerMapping = {
|
|
574
|
+
originalImage,
|
|
575
|
+
serviceName,
|
|
576
|
+
registryUrl: registryImageUrl,
|
|
577
|
+
repoName,
|
|
578
|
+
imageTag,
|
|
579
|
+
actualAlias
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
log('Container available at: %s', registryImageUrl);
|
|
583
|
+
|
|
584
|
+
// Verify container is accessible (reuse verification logic)
|
|
585
|
+
this.verifyContainerInRegistry(actualAlias);
|
|
586
|
+
|
|
587
|
+
return mapping;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* NEW METHOD: Get public image URL for a specific service
|
|
592
|
+
*
|
|
593
|
+
* This is the key method that SDL generator will call:
|
|
594
|
+
* "What's the registry URL I should use for this image?"
|
|
595
|
+
*/
|
|
596
|
+
getPublicImageUrl(serviceName: string, originalImage: string): string | null {
|
|
597
|
+
// Look for mapping by original image name
|
|
598
|
+
const mapping = this.containerMappings.get(originalImage);
|
|
599
|
+
|
|
600
|
+
if (mapping && mapping.serviceName === serviceName) {
|
|
601
|
+
// Debug logging - could make this conditional in the future
|
|
602
|
+
// this.logger.log(`🔍 Found registry URL for ${serviceName}:${originalImage} → ${mapping.registryUrl}`);
|
|
603
|
+
return mapping.registryUrl;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Debug logging - could make this conditional in the future
|
|
607
|
+
// this.logger.log(`🔍 No registry URL found for ${serviceName}:${originalImage} (not a local image or not processed)`);
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Get registry credentials for SDL generation
|
|
613
|
+
*
|
|
614
|
+
* The SDL generator calls this to get credentials for local images
|
|
615
|
+
*/
|
|
616
|
+
getRegistryCredentials(): RegistryCredentials | null {
|
|
617
|
+
if (!this.registryInfo?.credentials) {
|
|
618
|
+
log('No registry credentials available');
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
log('Registry info - tunnel: %s, local: %s',
|
|
623
|
+
this.registryInfo.tunnelUrl,
|
|
624
|
+
this.registryInfo.localUrl
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const host = this.getRegistryDomain();
|
|
628
|
+
|
|
629
|
+
// Convert to the format expected by SDL generation
|
|
630
|
+
const credentials = {
|
|
631
|
+
host: host,
|
|
632
|
+
username: this.registryInfo.credentials.accessKey,
|
|
633
|
+
password: this.registryInfo.credentials.secretKey
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
log('Returning registry credentials - host: %s, username: %s',
|
|
637
|
+
credentials.host,
|
|
638
|
+
credentials.username
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
return credentials;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Check if an image is local (reuse from existing code)
|
|
646
|
+
*/
|
|
647
|
+
private isLocalImage(image: string): boolean {
|
|
648
|
+
if (!image) return false;
|
|
649
|
+
|
|
650
|
+
// Local images don't contain registry domains or start with localhost
|
|
651
|
+
return (
|
|
652
|
+
!image.includes('/') ||
|
|
653
|
+
image.startsWith('localhost/') ||
|
|
654
|
+
image.startsWith('127.0.0.1/')
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Find tar file from kadi-build export directory
|
|
660
|
+
*
|
|
661
|
+
* Reused from temporary-container-registry.ts - excellent implementation
|
|
662
|
+
* that searches for container tar files exported by kadi-build in common locations.
|
|
663
|
+
*/
|
|
664
|
+
private findKadiBuildTarFile(imageName: string): string | null {
|
|
665
|
+
// Generate the expected filename pattern
|
|
666
|
+
// Example: "agent-a:0.0.1" -> "agent-a-0.0.1.tar"
|
|
667
|
+
const expectedFilename = `${imageName.replace(/[^a-zA-Z0-9.-]/g, '-')}.tar`;
|
|
668
|
+
|
|
669
|
+
// Common locations where kadi-build saves tar files
|
|
670
|
+
const possiblePaths: string[] = [
|
|
671
|
+
// User's home directory .kadi cache (primary location)
|
|
672
|
+
path.join(
|
|
673
|
+
os.homedir(),
|
|
674
|
+
'.kadi',
|
|
675
|
+
'tmp',
|
|
676
|
+
'container-registry-exports',
|
|
677
|
+
'containers'
|
|
678
|
+
),
|
|
679
|
+
// Current working directory exports (backup location)
|
|
680
|
+
path.join(process.cwd(), 'container-exports'),
|
|
681
|
+
// Temporary directory exports (fallback location)
|
|
682
|
+
path.join(os.tmpdir(), 'container-registry-exports', 'containers')
|
|
683
|
+
];
|
|
684
|
+
|
|
685
|
+
for (const basePath of possiblePaths) {
|
|
686
|
+
const fullPath = path.join(basePath, expectedFilename);
|
|
687
|
+
|
|
688
|
+
if (fs.existsSync(fullPath)) {
|
|
689
|
+
log('Found tar file for %s: %s', imageName, fullPath);
|
|
690
|
+
return fullPath;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
log('Checked: %s - not found', fullPath);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Debug: this.logger.log(`❌ No tar file found for ${imageName}`);
|
|
697
|
+
// Debug: this.logger.log(` Expected filename: ${expectedFilename}`);
|
|
698
|
+
// Debug: this.logger.log(` Searched in: ${possiblePaths.join(', ')}`);
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Show helpful suggestion to run kadi-build
|
|
704
|
+
*
|
|
705
|
+
* Adapted from registry-manager.js showKadiBuildSuggestion method
|
|
706
|
+
*/
|
|
707
|
+
private showKadiBuildSuggestion(
|
|
708
|
+
imageName: string,
|
|
709
|
+
containerType: string
|
|
710
|
+
): void {
|
|
711
|
+
this.logger.log(`\n🔧 CONTAINER NOT FOUND: ${imageName}`);
|
|
712
|
+
this.logger.log(`\nThis could mean:`);
|
|
713
|
+
this.logger.log(` • The container hasn't been built yet`);
|
|
714
|
+
this.logger.log(` • The container name doesn't match what's available`);
|
|
715
|
+
this.logger.log(` • The container was built with a different tool`);
|
|
716
|
+
|
|
717
|
+
this.logger.log(`\n💡 SUGGESTED SOLUTIONS:`);
|
|
718
|
+
this.logger.log(`\n1. Build the container first:`);
|
|
719
|
+
this.logger.log(
|
|
720
|
+
` kadi build --tag ${imageName} --engine ${containerType}`
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
this.logger.log(`\n2. Check available containers:`);
|
|
724
|
+
this.logger.log(
|
|
725
|
+
` ${containerType} images | grep ${imageName.split(':')[0]}`
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
this.logger.log(
|
|
729
|
+
`\n3. Verify the image name in your agent.json matches the built container`
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
this.logger.log(`\n4. If using a different tag, ensure it exists:`);
|
|
733
|
+
this.logger.log(` ${containerType} images ${imageName.split(':')[0]}`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Verify container is accessible in the registry
|
|
738
|
+
*
|
|
739
|
+
* Checks that a container with the given alias is present in the registry.
|
|
740
|
+
* Logs a warning if not found and shows available containers for debugging.
|
|
741
|
+
*
|
|
742
|
+
* @param actualAlias - Container alias to verify
|
|
743
|
+
*/
|
|
744
|
+
private verifyContainerInRegistry(actualAlias: string): void {
|
|
745
|
+
try {
|
|
746
|
+
const containers = this.registry!.listContainers();
|
|
747
|
+
const foundContainer = containers.find(
|
|
748
|
+
(c) => c.alias === actualAlias
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
if (foundContainer) {
|
|
752
|
+
log('Verified container in registry with alias: %s', foundContainer.alias);
|
|
753
|
+
} else {
|
|
754
|
+
this.logger.warn(
|
|
755
|
+
` ⚠️ Warning: Container not found in registry with alias: ${actualAlias}`
|
|
756
|
+
);
|
|
757
|
+
this.logger.log(
|
|
758
|
+
` Available containers: ${containers.map((c) => c.alias ?? c.name).join(', ')}`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
} catch (error) {
|
|
762
|
+
// Silently ignore verification errors - registry might not support listing
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Get the registry domain (without protocol)
|
|
768
|
+
*
|
|
769
|
+
* Reused from registry-manager.js with fallback logic
|
|
770
|
+
*/
|
|
771
|
+
private getRegistryDomain(): string {
|
|
772
|
+
// First try to get from our own getRegistryUrls() method
|
|
773
|
+
try {
|
|
774
|
+
const preferredDomain = this.getPreferredDomain();
|
|
775
|
+
if (preferredDomain) {
|
|
776
|
+
log('Registry domain from getRegistryUrls: %s', preferredDomain);
|
|
777
|
+
return preferredDomain;
|
|
778
|
+
}
|
|
779
|
+
} catch (error) {
|
|
780
|
+
log('Could not get registry URLs: %s', error);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Fallback to parsing from registry info
|
|
784
|
+
if (this.registryInfo) {
|
|
785
|
+
const url = this.registryInfo.tunnelUrl || this.registryInfo.localUrl;
|
|
786
|
+
if (url) {
|
|
787
|
+
const domain = url.replace(/^https?:\/\//, '');
|
|
788
|
+
log('Registry domain from registryInfo: %s', domain);
|
|
789
|
+
return domain;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
this.logger.error('❌ Could not determine registry domain!');
|
|
794
|
+
return '';
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Get registry URL components including local and tunnel endpoints
|
|
799
|
+
*/
|
|
800
|
+
private getRegistryUrls(): RegistryUrls {
|
|
801
|
+
if (!this.registryInfo) {
|
|
802
|
+
throw new Error('Registry not started');
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const localUrl = this.registryInfo.localUrl;
|
|
806
|
+
const tunnelUrl = this.registryInfo.tunnelUrl || null;
|
|
807
|
+
|
|
808
|
+
// Extract domains (remove protocol)
|
|
809
|
+
const localDomain = localUrl.replace(/^https?:\/\//, '');
|
|
810
|
+
const tunnelDomain = tunnelUrl
|
|
811
|
+
? tunnelUrl.replace(/^https?:\/\//, '')
|
|
812
|
+
: null;
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
localUrl,
|
|
816
|
+
localDomain,
|
|
817
|
+
tunnelUrl,
|
|
818
|
+
tunnelDomain
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Get the preferred URL (tunnel if available, otherwise local)
|
|
824
|
+
*/
|
|
825
|
+
private getPreferredUrl(): string {
|
|
826
|
+
const urls = this.getRegistryUrls();
|
|
827
|
+
return urls.tunnelUrl || urls.localUrl;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Get the preferred domain (tunnel if available, otherwise local)
|
|
832
|
+
*/
|
|
833
|
+
private getPreferredDomain(): string {
|
|
834
|
+
const urls = this.getRegistryUrls();
|
|
835
|
+
return urls.tunnelDomain || urls.localDomain;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Check if registry is running
|
|
840
|
+
*/
|
|
841
|
+
isRunning(): boolean {
|
|
842
|
+
return this.registry !== null && this.registryInfo !== null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Display container mappings for debugging
|
|
847
|
+
*
|
|
848
|
+
* Enhanced version that shows the new mapping structure
|
|
849
|
+
*/
|
|
850
|
+
async displayContainerMappings(): Promise<void> {
|
|
851
|
+
this.logger.log('\n🔍 Container image mappings:');
|
|
852
|
+
|
|
853
|
+
if (this.containerMappings.size === 0) {
|
|
854
|
+
this.logger.log(' No local images processed');
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
for (const [originalImage, mapping] of this.containerMappings) {
|
|
859
|
+
this.logger.log(` ${mapping.serviceName}:`);
|
|
860
|
+
this.logger.log(` Original: ${originalImage}`);
|
|
861
|
+
this.logger.log(` Registry: ${mapping.registryUrl}`);
|
|
862
|
+
this.logger.log(` Alias: ${mapping.actualAlias}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Stop the temporary registry and cleanup resources
|
|
868
|
+
*
|
|
869
|
+
* Reused from temporary-container-registry.ts with proper cleanup
|
|
870
|
+
*/
|
|
871
|
+
async stopTemporaryRegistry(): Promise<void> {
|
|
872
|
+
if (!this.registry) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
log('Stopping temporary registry...');
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
// Stop the registry instance
|
|
880
|
+
await this.registry.stop();
|
|
881
|
+
|
|
882
|
+
// Clear state
|
|
883
|
+
this.registry = null;
|
|
884
|
+
this.registryInfo = null;
|
|
885
|
+
this.containerMappings.clear();
|
|
886
|
+
|
|
887
|
+
log('Temporary registry stopped and cleaned up');
|
|
888
|
+
} catch (error) {
|
|
889
|
+
this.logger.warn(`⚠️ Error during registry cleanup: ${error}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|