@tamyla/clodo-framework 3.1.14 → 3.1.15
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/CHANGELOG.md +7 -0
- package/dist/bin/commands/assess.js +46 -31
- package/dist/bin/commands/create.js +45 -24
- package/dist/bin/commands/deploy.js +239 -83
- package/dist/bin/commands/diagnose.js +45 -32
- package/dist/bin/commands/update.js +39 -19
- package/dist/bin/commands/validate.js +33 -8
- package/dist/bin/shared/deployment/credential-collector.js +4 -6
- package/dist/bin/shared/routing/domain-router.js +596 -0
- package/dist/bin/shared/utils/ErrorHandler.js +122 -2
- package/dist/bin/shared/utils/cli-options.js +136 -0
- package/dist/bin/shared/utils/config-loader.js +275 -0
- package/dist/bin/shared/utils/output-formatter.js +273 -0
- package/dist/bin/shared/utils/progress-manager.js +335 -0
- package/package.json +1 -1
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Router
|
|
3
|
+
* CLI wrapper layer for domain routing, auto-detection, and multi-domain deployments
|
|
4
|
+
*
|
|
5
|
+
* REFACTORED (Task 3.1): Now delegates to MultiDomainOrchestrator for actual deployment logic
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Auto-detect available domains from configuration
|
|
9
|
+
* - Smart domain selection based on environment
|
|
10
|
+
* - Environment-specific routing strategies
|
|
11
|
+
* - Delegates multi-domain deployments to MultiDomainOrchestrator
|
|
12
|
+
* - CLI-friendly interface for domain selection and config loading
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from 'fs';
|
|
16
|
+
import { resolve } from 'path';
|
|
17
|
+
import { MultiDomainOrchestrator } from "../../../orchestration/multi-domain-orchestrator.js";
|
|
18
|
+
export class DomainRouter {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.configPath = options.configPath || './config/domains.json';
|
|
21
|
+
this.cloudflareAPI = options.cloudflareAPI || null;
|
|
22
|
+
this.verbose = options.verbose || false;
|
|
23
|
+
this.environment = options.environment || 'development';
|
|
24
|
+
this.domains = [];
|
|
25
|
+
this.routes = {};
|
|
26
|
+
this.failoverStrategies = {};
|
|
27
|
+
this.loadedConfig = null;
|
|
28
|
+
|
|
29
|
+
// Initialize MultiDomainOrchestrator for delegation
|
|
30
|
+
// Only creates if orchestrator options provided
|
|
31
|
+
this.orchestrator = null;
|
|
32
|
+
this.disableOrchestrator = options.disableOrchestrator || false; // For testing
|
|
33
|
+
this.orchestratorOptions = options.orchestratorOptions || {
|
|
34
|
+
dryRun: options.dryRun || false,
|
|
35
|
+
skipTests: options.skipTests || false,
|
|
36
|
+
parallelDeployments: options.parallelDeployments || 3,
|
|
37
|
+
servicePath: options.servicePath || process.cwd(),
|
|
38
|
+
cloudflareToken: options.cloudflareToken || null,
|
|
39
|
+
cloudflareAccountId: options.cloudflareAccountId || null,
|
|
40
|
+
enablePersistence: options.enablePersistence !== false,
|
|
41
|
+
rollbackEnabled: options.rollbackEnabled !== false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Initialize the orchestrator for multi-domain deployments
|
|
47
|
+
* REFACTORED (Task 3.1): Delegates to MultiDomainOrchestrator
|
|
48
|
+
* @param {Object} options - Orchestrator initialization options
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async initializeOrchestrator(options = {}) {
|
|
52
|
+
if (this.orchestrator) {
|
|
53
|
+
return; // Already initialized
|
|
54
|
+
}
|
|
55
|
+
const orchestratorConfig = {
|
|
56
|
+
...this.orchestratorOptions,
|
|
57
|
+
...options,
|
|
58
|
+
domains: this.domains,
|
|
59
|
+
environment: this.environment
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
this.orchestrator = new MultiDomainOrchestrator(orchestratorConfig);
|
|
63
|
+
await this.orchestrator.initialize();
|
|
64
|
+
if (this.verbose) {
|
|
65
|
+
console.log('✅ Multi-Domain Orchestrator initialized');
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw new Error(`Failed to initialize orchestrator: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Load domain configuration from file or API
|
|
74
|
+
* @param {Object} options - Loading options
|
|
75
|
+
* @returns {Promise<Object>} Loaded configuration
|
|
76
|
+
*/
|
|
77
|
+
async loadConfiguration(options = {}) {
|
|
78
|
+
try {
|
|
79
|
+
// Try loading from file first
|
|
80
|
+
if (options.configPath && existsSync(options.configPath)) {
|
|
81
|
+
const content = readFileSync(options.configPath, 'utf-8');
|
|
82
|
+
this.loadedConfig = JSON.parse(content);
|
|
83
|
+
if (this.verbose) {
|
|
84
|
+
console.log(`📋 Loaded domain config from: ${options.configPath}`);
|
|
85
|
+
}
|
|
86
|
+
return this.loadedConfig;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If no file, try Cloudflare API
|
|
90
|
+
if (this.cloudflareAPI && options.useCloudflareAPI) {
|
|
91
|
+
this.loadedConfig = await this.cloudflareAPI.getDomainsConfiguration();
|
|
92
|
+
if (this.verbose) {
|
|
93
|
+
console.log('📋 Loaded domain config from Cloudflare API');
|
|
94
|
+
}
|
|
95
|
+
return this.loadedConfig;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new Error(`Failed to load domain configuration: ${error.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Detect available domains from configuration or environment
|
|
105
|
+
* @param {Object} options - Detection options
|
|
106
|
+
* @returns {Promise<Array<string>>} Array of discovered domains
|
|
107
|
+
*/
|
|
108
|
+
async detectDomains(options = {}) {
|
|
109
|
+
const config = options.config || this.loadedConfig || {};
|
|
110
|
+
const domains = [];
|
|
111
|
+
|
|
112
|
+
// Extract from config
|
|
113
|
+
if (config.domains) {
|
|
114
|
+
if (Array.isArray(config.domains)) {
|
|
115
|
+
domains.push(...config.domains);
|
|
116
|
+
} else if (typeof config.domains === 'object') {
|
|
117
|
+
// Handle environment-keyed domains: { production: 'api.example.com', staging: 'staging-api.example.com' }
|
|
118
|
+
for (const [env, domainList] of Object.entries(config.domains)) {
|
|
119
|
+
if (Array.isArray(domainList)) {
|
|
120
|
+
domains.push(...domainList);
|
|
121
|
+
} else if (typeof domainList === 'string') {
|
|
122
|
+
domains.push(domainList);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Extract from environment variable
|
|
129
|
+
const envDomains = process.env.CLODO_DOMAINS;
|
|
130
|
+
if (envDomains) {
|
|
131
|
+
domains.push(...envDomains.split(',').map(d => d.trim()));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Deduplicate and sort
|
|
135
|
+
const uniqueDomains = [...new Set(domains)].sort();
|
|
136
|
+
if (this.verbose) {
|
|
137
|
+
console.log(`🔍 Detected ${uniqueDomains.length} domains: ${uniqueDomains.join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
this.domains = uniqueDomains;
|
|
140
|
+
return uniqueDomains;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Select the appropriate domain for deployment based on environment and options
|
|
145
|
+
* @param {Object} options - Selection criteria
|
|
146
|
+
* @returns {string|Array<string>} Selected domain(s)
|
|
147
|
+
*/
|
|
148
|
+
selectDomain(options = {}) {
|
|
149
|
+
const {
|
|
150
|
+
specificDomain,
|
|
151
|
+
environment = this.environment,
|
|
152
|
+
selectAll = false,
|
|
153
|
+
environmentMap = {}
|
|
154
|
+
} = options;
|
|
155
|
+
|
|
156
|
+
// If specific domain requested, validate and return it
|
|
157
|
+
if (specificDomain) {
|
|
158
|
+
if (this.domains.includes(specificDomain)) {
|
|
159
|
+
if (this.verbose) {
|
|
160
|
+
console.log(`✓ Selected domain: ${specificDomain}`);
|
|
161
|
+
}
|
|
162
|
+
return specificDomain;
|
|
163
|
+
} else {
|
|
164
|
+
throw new Error(`Domain '${specificDomain}' not found in available domains: ${this.domains.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// If returning all domains
|
|
169
|
+
if (selectAll) {
|
|
170
|
+
if (this.verbose) {
|
|
171
|
+
console.log(`✓ Selected all ${this.domains.length} domains`);
|
|
172
|
+
}
|
|
173
|
+
return this.domains;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Select based on environment mapping
|
|
177
|
+
const envMap = environmentMap[environment];
|
|
178
|
+
if (envMap) {
|
|
179
|
+
const selectedDomain = Array.isArray(envMap) ? envMap[0] : envMap;
|
|
180
|
+
if (this.domains.includes(selectedDomain)) {
|
|
181
|
+
if (this.verbose) {
|
|
182
|
+
console.log(`✓ Selected domain for ${environment}: ${selectedDomain}`);
|
|
183
|
+
}
|
|
184
|
+
return selectedDomain;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Default to first available domain
|
|
189
|
+
if (this.domains.length > 0) {
|
|
190
|
+
if (this.verbose) {
|
|
191
|
+
console.log(`✓ Selected default domain: ${this.domains[0]}`);
|
|
192
|
+
}
|
|
193
|
+
return this.domains[0];
|
|
194
|
+
}
|
|
195
|
+
throw new Error('No domains available for selection');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get environment-specific routing configuration
|
|
200
|
+
* @param {string} domain - Domain name
|
|
201
|
+
* @param {string} environment - Environment (development, staging, production)
|
|
202
|
+
* @returns {Object} Routing configuration
|
|
203
|
+
*/
|
|
204
|
+
getEnvironmentRouting(domain, environment = this.environment) {
|
|
205
|
+
const config = this.loadedConfig || {};
|
|
206
|
+
const domainConfig = config[domain] || config.routing || {};
|
|
207
|
+
const environmentRouting = {
|
|
208
|
+
domain,
|
|
209
|
+
environment,
|
|
210
|
+
endpoints: [],
|
|
211
|
+
strategies: [],
|
|
212
|
+
rateLimit: 1000,
|
|
213
|
+
timeout: 30000,
|
|
214
|
+
retries: 3,
|
|
215
|
+
cacheTTL: 3600,
|
|
216
|
+
corsEnabled: false,
|
|
217
|
+
customHeaders: {},
|
|
218
|
+
...domainConfig[environment] // Allow environment-specific overrides
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Add default endpoints for environment
|
|
222
|
+
if (environment === 'production') {
|
|
223
|
+
environmentRouting.rateLimit = 10000;
|
|
224
|
+
environmentRouting.cacheTTL = 86400; // 24 hours
|
|
225
|
+
environmentRouting.strategies = ['load-balance', 'geo-route'];
|
|
226
|
+
} else if (environment === 'staging') {
|
|
227
|
+
environmentRouting.rateLimit = 5000;
|
|
228
|
+
environmentRouting.cacheTTL = 3600; // 1 hour
|
|
229
|
+
environmentRouting.strategies = ['round-robin'];
|
|
230
|
+
} else {
|
|
231
|
+
// development
|
|
232
|
+
environmentRouting.rateLimit = 100;
|
|
233
|
+
environmentRouting.cacheTTL = 300; // 5 minutes
|
|
234
|
+
environmentRouting.strategies = ['direct'];
|
|
235
|
+
}
|
|
236
|
+
if (this.verbose) {
|
|
237
|
+
console.log(`🛣️ Environment routing for ${domain} (${environment}): ${environmentRouting.strategies.join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
return environmentRouting;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get failover strategy for domain
|
|
244
|
+
* @param {string} domain - Domain name
|
|
245
|
+
* @returns {Object} Failover configuration
|
|
246
|
+
*/
|
|
247
|
+
getFailoverStrategy(domain) {
|
|
248
|
+
if (this.failoverStrategies[domain]) {
|
|
249
|
+
return this.failoverStrategies[domain];
|
|
250
|
+
}
|
|
251
|
+
const config = this.loadedConfig || {};
|
|
252
|
+
const domainConfig = config[domain] || {};
|
|
253
|
+
const strategy = {
|
|
254
|
+
domain,
|
|
255
|
+
primaryEndpoint: domainConfig.primaryEndpoint || null,
|
|
256
|
+
secondaryEndpoints: domainConfig.secondaryEndpoints || [],
|
|
257
|
+
healthCheckInterval: domainConfig.healthCheckInterval || 30000,
|
|
258
|
+
healthCheckPath: domainConfig.healthCheckPath || '/health',
|
|
259
|
+
failoverThreshold: domainConfig.failoverThreshold || 3,
|
|
260
|
+
autoFailover: domainConfig.autoFailover !== false,
|
|
261
|
+
maxRetries: domainConfig.maxRetries || 5,
|
|
262
|
+
rollbackOnFailure: domainConfig.rollbackOnFailure !== false,
|
|
263
|
+
notifications: domainConfig.notifications || []
|
|
264
|
+
};
|
|
265
|
+
this.failoverStrategies[domain] = strategy;
|
|
266
|
+
if (this.verbose) {
|
|
267
|
+
console.log(`⚡ Failover strategy for ${domain}: ${strategy.autoFailover ? 'auto' : 'manual'} with ${strategy.secondaryEndpoints.length} backups`);
|
|
268
|
+
}
|
|
269
|
+
return strategy;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Validate domain configuration
|
|
274
|
+
* @param {Object} config - Configuration to validate
|
|
275
|
+
* @returns {Object} Validation result { valid, errors }
|
|
276
|
+
*/
|
|
277
|
+
validateConfiguration(config) {
|
|
278
|
+
const result = {
|
|
279
|
+
valid: true,
|
|
280
|
+
errors: [],
|
|
281
|
+
warnings: []
|
|
282
|
+
};
|
|
283
|
+
if (!config) {
|
|
284
|
+
result.valid = false;
|
|
285
|
+
result.errors.push('Configuration cannot be empty');
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
if (!config.domains || Array.isArray(config.domains) && config.domains.length === 0) {
|
|
289
|
+
result.valid = false;
|
|
290
|
+
result.errors.push('At least one domain must be specified');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Validate each domain has required fields
|
|
294
|
+
if (Array.isArray(config.domains)) {
|
|
295
|
+
config.domains.forEach(domain => {
|
|
296
|
+
if (!domain || typeof domain !== 'string') {
|
|
297
|
+
result.errors.push('All domains must be non-empty strings');
|
|
298
|
+
result.valid = false;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check for environment-specific configurations
|
|
304
|
+
if (config.environments) {
|
|
305
|
+
const validEnvs = ['development', 'staging', 'production'];
|
|
306
|
+
for (const env of Object.keys(config.environments)) {
|
|
307
|
+
if (!validEnvs.includes(env)) {
|
|
308
|
+
result.warnings.push(`Unknown environment: ${env}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (this.verbose && result.errors.length > 0) {
|
|
313
|
+
console.log(`❌ Configuration validation failed: ${result.errors.join(', ')}`);
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Plan multi-domain deployment
|
|
320
|
+
* REFACTORED (Task 3.1): Delegates to MultiDomainOrchestrator
|
|
321
|
+
* @param {Array<string>} domains - Domains to deploy
|
|
322
|
+
* @param {Object} options - Deployment options
|
|
323
|
+
* @returns {Object} Deployment plan
|
|
324
|
+
*/
|
|
325
|
+
planMultiDomainDeployment(domains, options = {}) {
|
|
326
|
+
if (!this.orchestrator) {
|
|
327
|
+
// Fallback to basic planning if orchestrator not initialized
|
|
328
|
+
const {
|
|
329
|
+
parallelDeployments = 3,
|
|
330
|
+
environment = this.environment,
|
|
331
|
+
validateBeforeDeploy = true,
|
|
332
|
+
rollbackOnError = true
|
|
333
|
+
} = options;
|
|
334
|
+
|
|
335
|
+
// Validate all domains
|
|
336
|
+
const invalidDomains = domains.filter(d => !this.domains.includes(d));
|
|
337
|
+
if (invalidDomains.length > 0) {
|
|
338
|
+
throw new Error(`Invalid domains: ${invalidDomains.join(', ')}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Create deployment batches
|
|
342
|
+
const batches = [];
|
|
343
|
+
for (let i = 0; i < domains.length; i += parallelDeployments) {
|
|
344
|
+
batches.push(domains.slice(i, i + parallelDeployments));
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
totalDomains: domains.length,
|
|
348
|
+
batches,
|
|
349
|
+
parallelDeployments,
|
|
350
|
+
environment,
|
|
351
|
+
phases: [{
|
|
352
|
+
phase: 'validation',
|
|
353
|
+
domains
|
|
354
|
+
}, {
|
|
355
|
+
phase: 'preparation',
|
|
356
|
+
domains
|
|
357
|
+
}, {
|
|
358
|
+
phase: 'deployment',
|
|
359
|
+
domains,
|
|
360
|
+
batches
|
|
361
|
+
}, {
|
|
362
|
+
phase: 'verification',
|
|
363
|
+
domains
|
|
364
|
+
}, {
|
|
365
|
+
phase: 'rollback',
|
|
366
|
+
domains,
|
|
367
|
+
enabled: rollbackOnError
|
|
368
|
+
}],
|
|
369
|
+
estimatedDuration: domains.length * 5 * 60 * 1000,
|
|
370
|
+
rollbackOnError,
|
|
371
|
+
validateBeforeDeploy
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Delegate to orchestrator's batch creation
|
|
376
|
+
const batches = this.orchestrator.createDeploymentBatches();
|
|
377
|
+
if (this.verbose) {
|
|
378
|
+
console.log(`📊 Deployment plan created: ${batches.length} batches for ${domains.length} domains`);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
totalDomains: domains.length,
|
|
382
|
+
batches,
|
|
383
|
+
parallelDeployments: this.orchestratorOptions.parallelDeployments,
|
|
384
|
+
environment: this.environment,
|
|
385
|
+
orchestratorManaged: true
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Execute multi-domain deployment with coordination
|
|
391
|
+
* REFACTORED (Task 3.1): Delegates to MultiDomainOrchestrator for actual deployment
|
|
392
|
+
* @param {Array<string>} domains - Domains to deploy
|
|
393
|
+
* @param {Function} deployFn - Async function to deploy a single domain (legacy support)
|
|
394
|
+
* @param {Object} options - Deployment options
|
|
395
|
+
* @returns {Promise<Object>} Deployment results
|
|
396
|
+
*/
|
|
397
|
+
async deployAcrossDomains(domains, deployFn, options = {}) {
|
|
398
|
+
// For tests or when explicitly disabled, use legacy mode without orchestrator
|
|
399
|
+
if (this.disableOrchestrator) {
|
|
400
|
+
return this._deployAcrossDomainsLegacy(domains, deployFn, options);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Ensure orchestrator is initialized
|
|
404
|
+
if (!this.orchestrator) {
|
|
405
|
+
await this.initializeOrchestrator();
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
// If deployFn provided, use orchestrator's delegated deployment with custom handler
|
|
409
|
+
if (deployFn && typeof deployFn === 'function') {
|
|
410
|
+
if (this.verbose) {
|
|
411
|
+
console.log('🚀 Using custom deployment function with orchestrator coordination');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Validate domains
|
|
415
|
+
const invalidDomains = domains.filter(d => !this.domains.includes(d));
|
|
416
|
+
if (invalidDomains.length > 0) {
|
|
417
|
+
throw new Error(`Invalid domains: ${invalidDomains.join(', ')}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Execute with plan
|
|
421
|
+
const plan = this.planMultiDomainDeployment(domains, options);
|
|
422
|
+
const results = {
|
|
423
|
+
successful: [],
|
|
424
|
+
failed: [],
|
|
425
|
+
skipped: [],
|
|
426
|
+
duration: 0,
|
|
427
|
+
startTime: new Date(),
|
|
428
|
+
orchestratorManaged: true
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Phase 1: Validation
|
|
432
|
+
if (plan.validateBeforeDeploy !== false) {
|
|
433
|
+
for (const domain of domains) {
|
|
434
|
+
const validation = this.validateConfiguration({
|
|
435
|
+
domains: [domain]
|
|
436
|
+
});
|
|
437
|
+
if (!validation.valid) {
|
|
438
|
+
results.failed.push({
|
|
439
|
+
domain,
|
|
440
|
+
error: validation.errors.join(', ')
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (results.failed.length > 0 && options.rollbackOnError !== false) {
|
|
445
|
+
throw new Error(`Validation failed for ${results.failed.length} domain(s)`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Phase 2: Batch deployment via orchestrator
|
|
450
|
+
for (const batch of plan.batches) {
|
|
451
|
+
const deployPromises = batch.map(domain => this.orchestrator.deploySingleDomain(domain, options).then(result => {
|
|
452
|
+
results.successful.push({
|
|
453
|
+
domain,
|
|
454
|
+
...result
|
|
455
|
+
});
|
|
456
|
+
}).catch(error => {
|
|
457
|
+
results.failed.push({
|
|
458
|
+
domain,
|
|
459
|
+
error: error.message
|
|
460
|
+
});
|
|
461
|
+
if (options.rollbackOnError !== false) {
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}));
|
|
465
|
+
try {
|
|
466
|
+
await Promise.all(deployPromises);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (options.rollbackOnError !== false) {
|
|
469
|
+
results.duration = Date.now() - results.startTime.getTime();
|
|
470
|
+
throw new Error(`Deployment failed at batch. Completed: ${results.successful.length}, Failed: ${results.failed.length}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
results.duration = Date.now() - results.startTime.getTime();
|
|
475
|
+
if (this.verbose) {
|
|
476
|
+
console.log(`✅ Multi-domain deployment complete: ${results.successful.length} successful, ${results.failed.length} failed in ${Math.ceil(results.duration / 1000)}s`);
|
|
477
|
+
}
|
|
478
|
+
return results;
|
|
479
|
+
} else {
|
|
480
|
+
// No custom function provided - use orchestrator's portfolio deployment
|
|
481
|
+
if (this.verbose) {
|
|
482
|
+
console.log('🚀 Using orchestrator portfolio deployment');
|
|
483
|
+
}
|
|
484
|
+
const startTime = Date.now();
|
|
485
|
+
const results = await this.orchestrator.deployPortfolio();
|
|
486
|
+
const duration = Date.now() - startTime;
|
|
487
|
+
return {
|
|
488
|
+
...results,
|
|
489
|
+
duration,
|
|
490
|
+
orchestratorManaged: true
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (this.verbose) {
|
|
495
|
+
console.log(`❌ Deployment failed: ${error.message}`);
|
|
496
|
+
}
|
|
497
|
+
throw error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Legacy deployment logic for testing or when orchestrator is disabled
|
|
503
|
+
* @private
|
|
504
|
+
* @param {Array<string>} domains - Domains to deploy
|
|
505
|
+
* @param {Function} deployFn - Async function to deploy a single domain
|
|
506
|
+
* @param {Object} options - Deployment options
|
|
507
|
+
* @returns {Promise<Object>} Deployment results
|
|
508
|
+
*/
|
|
509
|
+
async _deployAcrossDomainsLegacy(domains, deployFn, options = {}) {
|
|
510
|
+
const plan = this.planMultiDomainDeployment(domains, options);
|
|
511
|
+
const results = {
|
|
512
|
+
successful: [],
|
|
513
|
+
failed: [],
|
|
514
|
+
skipped: [],
|
|
515
|
+
duration: 0,
|
|
516
|
+
startTime: new Date()
|
|
517
|
+
};
|
|
518
|
+
try {
|
|
519
|
+
// Phase 1: Validation
|
|
520
|
+
if (plan.validateBeforeDeploy) {
|
|
521
|
+
for (const domain of domains) {
|
|
522
|
+
const routing = this.getEnvironmentRouting(domain, options.environment || this.environment);
|
|
523
|
+
const validation = this.validateConfiguration({
|
|
524
|
+
domains: [domain],
|
|
525
|
+
...routing
|
|
526
|
+
});
|
|
527
|
+
if (!validation.valid) {
|
|
528
|
+
results.failed.push({
|
|
529
|
+
domain,
|
|
530
|
+
error: validation.errors.join(', ')
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (results.failed.length > 0 && plan.rollbackOnError) {
|
|
535
|
+
throw new Error(`Validation failed for ${results.failed.length} domain(s)`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Phase 2: Batch deployment
|
|
540
|
+
for (const batch of plan.batches) {
|
|
541
|
+
const deployPromises = batch.map(domain => deployFn(domain, options).then(result => {
|
|
542
|
+
results.successful.push({
|
|
543
|
+
domain,
|
|
544
|
+
...result
|
|
545
|
+
});
|
|
546
|
+
}).catch(error => {
|
|
547
|
+
results.failed.push({
|
|
548
|
+
domain,
|
|
549
|
+
error: error.message
|
|
550
|
+
});
|
|
551
|
+
if (plan.rollbackOnError) {
|
|
552
|
+
throw error;
|
|
553
|
+
}
|
|
554
|
+
}));
|
|
555
|
+
try {
|
|
556
|
+
await Promise.all(deployPromises);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (plan.rollbackOnError) {
|
|
559
|
+
results.duration = Date.now() - results.startTime.getTime();
|
|
560
|
+
throw new Error(`Deployment failed at batch. Completed: ${results.successful.length}, Failed: ${results.failed.length}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
results.duration = Date.now() - results.startTime.getTime();
|
|
565
|
+
if (this.verbose) {
|
|
566
|
+
console.log(`✅ Deployment complete: ${results.successful.length} successful, ${results.failed.length} failed in ${Math.ceil(results.duration / 1000)}s`);
|
|
567
|
+
}
|
|
568
|
+
return results;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
results.duration = Date.now() - results.startTime.getTime();
|
|
571
|
+
if (this.verbose) {
|
|
572
|
+
console.log(`❌ Deployment failed: ${error.message}`);
|
|
573
|
+
}
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Get routing summary for debugging
|
|
580
|
+
* @returns {Object} Routing summary
|
|
581
|
+
*/
|
|
582
|
+
getSummary() {
|
|
583
|
+
const summary = {
|
|
584
|
+
totalDomains: this.domains.length,
|
|
585
|
+
domains: this.domains,
|
|
586
|
+
environment: this.environment,
|
|
587
|
+
routing: {},
|
|
588
|
+
failover: {}
|
|
589
|
+
};
|
|
590
|
+
for (const domain of this.domains) {
|
|
591
|
+
summary.routing[domain] = this.getEnvironmentRouting(domain);
|
|
592
|
+
summary.failover[domain] = this.getFailoverStrategy(domain);
|
|
593
|
+
}
|
|
594
|
+
return summary;
|
|
595
|
+
}
|
|
596
|
+
}
|