@devramps/cli 0.1.1 → 0.1.2
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/dist/index.js +340 -86
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -74,6 +74,80 @@ var CloudFormationError = class extends DevRampsError {
|
|
|
74
74
|
// src/utils/logger.ts
|
|
75
75
|
import chalk from "chalk";
|
|
76
76
|
var verboseMode = false;
|
|
77
|
+
var ProgressBar = class {
|
|
78
|
+
current = 0;
|
|
79
|
+
total = 0;
|
|
80
|
+
label;
|
|
81
|
+
barWidth = 30;
|
|
82
|
+
lastLineCount = 0;
|
|
83
|
+
eventLines = [];
|
|
84
|
+
maxVisibleEvents = 5;
|
|
85
|
+
constructor(label, total) {
|
|
86
|
+
this.label = label;
|
|
87
|
+
this.total = total;
|
|
88
|
+
this.render();
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Update progress and re-render
|
|
92
|
+
*/
|
|
93
|
+
update(current, eventMessage) {
|
|
94
|
+
this.current = current;
|
|
95
|
+
if (eventMessage) {
|
|
96
|
+
this.eventLines.push(eventMessage);
|
|
97
|
+
if (this.eventLines.length > this.maxVisibleEvents) {
|
|
98
|
+
this.eventLines.shift();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.render();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Add an event message without changing progress
|
|
105
|
+
*/
|
|
106
|
+
addEvent(message) {
|
|
107
|
+
this.eventLines.push(message);
|
|
108
|
+
if (this.eventLines.length > this.maxVisibleEvents) {
|
|
109
|
+
this.eventLines.shift();
|
|
110
|
+
}
|
|
111
|
+
this.render();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Clear the progress bar from the terminal
|
|
115
|
+
*/
|
|
116
|
+
clear() {
|
|
117
|
+
for (let i = 0; i < this.lastLineCount; i++) {
|
|
118
|
+
process.stdout.write("\x1B[A\x1B[2K");
|
|
119
|
+
}
|
|
120
|
+
this.lastLineCount = 0;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Finish and clear the progress bar
|
|
124
|
+
*/
|
|
125
|
+
finish() {
|
|
126
|
+
this.clear();
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Render the progress bar
|
|
130
|
+
*/
|
|
131
|
+
render() {
|
|
132
|
+
this.clear();
|
|
133
|
+
const lines = [];
|
|
134
|
+
for (const event of this.eventLines) {
|
|
135
|
+
lines.push(event);
|
|
136
|
+
}
|
|
137
|
+
const percentage = this.total > 0 ? this.current / this.total : 0;
|
|
138
|
+
const filled = Math.round(this.barWidth * percentage);
|
|
139
|
+
const empty = this.barWidth - filled;
|
|
140
|
+
const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
|
|
141
|
+
const count = chalk.cyan(`${this.current}/${this.total}`);
|
|
142
|
+
const labelText = chalk.bold(this.label);
|
|
143
|
+
lines.push(`${labelText} ${bar} ${count} resources`);
|
|
144
|
+
lines.push("");
|
|
145
|
+
for (const line of lines) {
|
|
146
|
+
process.stdout.write(line + "\n");
|
|
147
|
+
}
|
|
148
|
+
this.lastLineCount = lines.length;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
77
151
|
function setVerbose(enabled) {
|
|
78
152
|
verboseMode = enabled;
|
|
79
153
|
}
|
|
@@ -234,13 +308,12 @@ import {
|
|
|
234
308
|
CloudFormationClient,
|
|
235
309
|
DescribeStacksCommand,
|
|
236
310
|
DescribeStackResourcesCommand,
|
|
311
|
+
DescribeStackEventsCommand,
|
|
237
312
|
CreateStackCommand,
|
|
238
313
|
UpdateStackCommand,
|
|
239
314
|
CreateChangeSetCommand,
|
|
240
315
|
DescribeChangeSetCommand,
|
|
241
316
|
DeleteChangeSetCommand,
|
|
242
|
-
waitUntilStackCreateComplete,
|
|
243
|
-
waitUntilStackUpdateComplete,
|
|
244
317
|
waitUntilChangeSetCreateComplete,
|
|
245
318
|
ChangeSetType
|
|
246
319
|
} from "@aws-sdk/client-cloudformation";
|
|
@@ -361,21 +434,114 @@ function getActionSymbol(action) {
|
|
|
361
434
|
return " ";
|
|
362
435
|
}
|
|
363
436
|
}
|
|
437
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set([
|
|
438
|
+
"CREATE_COMPLETE",
|
|
439
|
+
"CREATE_FAILED",
|
|
440
|
+
"DELETE_COMPLETE",
|
|
441
|
+
"DELETE_FAILED",
|
|
442
|
+
"ROLLBACK_COMPLETE",
|
|
443
|
+
"ROLLBACK_FAILED",
|
|
444
|
+
"UPDATE_COMPLETE",
|
|
445
|
+
"UPDATE_FAILED",
|
|
446
|
+
"UPDATE_ROLLBACK_COMPLETE",
|
|
447
|
+
"UPDATE_ROLLBACK_FAILED"
|
|
448
|
+
]);
|
|
449
|
+
var SUCCESS_STATES = /* @__PURE__ */ new Set([
|
|
450
|
+
"CREATE_COMPLETE",
|
|
451
|
+
"UPDATE_COMPLETE"
|
|
452
|
+
]);
|
|
453
|
+
function getStatusSymbol(status) {
|
|
454
|
+
if (!status) return "?";
|
|
455
|
+
if (status.includes("COMPLETE") && !status.includes("ROLLBACK")) return "\u2714";
|
|
456
|
+
if (status.includes("FAILED") || status.includes("ROLLBACK")) return "\u2716";
|
|
457
|
+
if (status.includes("IN_PROGRESS")) return "\u22EF";
|
|
458
|
+
return "?";
|
|
459
|
+
}
|
|
460
|
+
function formatStackEvent(event) {
|
|
461
|
+
const symbol = getStatusSymbol(event.ResourceStatus);
|
|
462
|
+
const resourceType = event.ResourceType || "Unknown";
|
|
463
|
+
const logicalId = event.LogicalResourceId || "Unknown";
|
|
464
|
+
const status = event.ResourceStatus || "Unknown";
|
|
465
|
+
const reason = event.ResourceStatusReason ? ` - ${event.ResourceStatusReason}` : "";
|
|
466
|
+
return ` ${symbol} ${resourceType} (${logicalId}): ${status}${reason}`;
|
|
467
|
+
}
|
|
468
|
+
function isResourceComplete(status) {
|
|
469
|
+
if (!status) return false;
|
|
470
|
+
return status.includes("_COMPLETE") && !status.includes("ROLLBACK");
|
|
471
|
+
}
|
|
472
|
+
async function waitForStackWithProgress(client, stackName, operationStartTime, totalResources, maxWaitTime = 600, showProgress = true) {
|
|
473
|
+
const seenEventIds = /* @__PURE__ */ new Set();
|
|
474
|
+
const completedResources = /* @__PURE__ */ new Set();
|
|
475
|
+
const startTime = Date.now();
|
|
476
|
+
const pollInterval = 3e3;
|
|
477
|
+
const progressBar = showProgress ? new ProgressBar(stackName, totalResources) : null;
|
|
478
|
+
try {
|
|
479
|
+
while (true) {
|
|
480
|
+
if (Date.now() - startTime > maxWaitTime * 1e3) {
|
|
481
|
+
throw new Error(`Stack operation timed out after ${maxWaitTime} seconds`);
|
|
482
|
+
}
|
|
483
|
+
const stackResponse = await client.send(
|
|
484
|
+
new DescribeStacksCommand({ StackName: stackName })
|
|
485
|
+
);
|
|
486
|
+
const stack = stackResponse.Stacks?.[0];
|
|
487
|
+
if (!stack) {
|
|
488
|
+
throw new Error(`Stack ${stackName} not found`);
|
|
489
|
+
}
|
|
490
|
+
const eventsResponse = await client.send(
|
|
491
|
+
new DescribeStackEventsCommand({ StackName: stackName })
|
|
492
|
+
);
|
|
493
|
+
const newEvents = (eventsResponse.StackEvents || []).filter((event) => {
|
|
494
|
+
if (!event.Timestamp || event.Timestamp < operationStartTime) return false;
|
|
495
|
+
if (!event.EventId || seenEventIds.has(event.EventId)) return false;
|
|
496
|
+
return true;
|
|
497
|
+
}).reverse();
|
|
498
|
+
for (const event of newEvents) {
|
|
499
|
+
if (event.EventId) {
|
|
500
|
+
seenEventIds.add(event.EventId);
|
|
501
|
+
}
|
|
502
|
+
const logicalId = event.LogicalResourceId;
|
|
503
|
+
if (logicalId && logicalId !== stackName && isResourceComplete(event.ResourceStatus)) {
|
|
504
|
+
completedResources.add(logicalId);
|
|
505
|
+
}
|
|
506
|
+
if (progressBar) {
|
|
507
|
+
progressBar.update(completedResources.size, formatStackEvent(event));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const currentStatus = stack.StackStatus || "";
|
|
511
|
+
if (TERMINAL_STATES.has(currentStatus)) {
|
|
512
|
+
if (progressBar) {
|
|
513
|
+
progressBar.finish();
|
|
514
|
+
}
|
|
515
|
+
if (SUCCESS_STATES.has(currentStatus)) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
throw new Error(`Stack operation failed with status: ${currentStatus}`);
|
|
519
|
+
}
|
|
520
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
521
|
+
}
|
|
522
|
+
} catch (error2) {
|
|
523
|
+
if (progressBar) {
|
|
524
|
+
progressBar.finish();
|
|
525
|
+
}
|
|
526
|
+
throw error2;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
364
529
|
async function deployStack(options) {
|
|
365
|
-
const { stackName, template, accountId, region, credentials } = options;
|
|
530
|
+
const { stackName, template, accountId, region, credentials, showProgress = true } = options;
|
|
366
531
|
const client = new CloudFormationClient({
|
|
367
532
|
credentials,
|
|
368
533
|
region
|
|
369
534
|
});
|
|
370
535
|
const templateBody = JSON.stringify(template);
|
|
536
|
+
const resourceCount = Object.keys(template.Resources || {}).length;
|
|
371
537
|
try {
|
|
372
538
|
const stackStatus = await getStackStatus(stackName, credentials, region);
|
|
373
539
|
if (stackStatus.exists) {
|
|
374
540
|
verbose(`Stack ${stackName} exists, updating...`);
|
|
375
|
-
await updateStack(client, stackName, templateBody, accountId);
|
|
541
|
+
await updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
|
|
376
542
|
} else {
|
|
377
543
|
verbose(`Stack ${stackName} does not exist, creating...`);
|
|
378
|
-
await createStack(client, stackName, templateBody, accountId);
|
|
544
|
+
await createStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
|
|
379
545
|
}
|
|
380
546
|
} catch (error2) {
|
|
381
547
|
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
@@ -386,7 +552,8 @@ async function deployStack(options) {
|
|
|
386
552
|
throw new CloudFormationError(stackName, accountId, errorMessage);
|
|
387
553
|
}
|
|
388
554
|
}
|
|
389
|
-
async function createStack(client, stackName, templateBody, accountId) {
|
|
555
|
+
async function createStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
|
|
556
|
+
const operationStartTime = /* @__PURE__ */ new Date();
|
|
390
557
|
await client.send(
|
|
391
558
|
new CreateStackCommand({
|
|
392
559
|
StackName: stackName,
|
|
@@ -398,14 +565,12 @@ async function createStack(client, stackName, templateBody, accountId) {
|
|
|
398
565
|
]
|
|
399
566
|
})
|
|
400
567
|
);
|
|
401
|
-
|
|
402
|
-
await
|
|
403
|
-
{ client, maxWaitTime: 600 },
|
|
404
|
-
{ StackName: stackName }
|
|
405
|
-
);
|
|
568
|
+
info(`Creating stack ${stackName}...`);
|
|
569
|
+
await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
|
|
406
570
|
success(`Stack ${stackName} created successfully in account ${accountId}`);
|
|
407
571
|
}
|
|
408
|
-
async function updateStack(client, stackName, templateBody, accountId) {
|
|
572
|
+
async function updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
|
|
573
|
+
const operationStartTime = /* @__PURE__ */ new Date();
|
|
409
574
|
await client.send(
|
|
410
575
|
new UpdateStackCommand({
|
|
411
576
|
StackName: stackName,
|
|
@@ -413,11 +578,8 @@ async function updateStack(client, stackName, templateBody, accountId) {
|
|
|
413
578
|
Capabilities: ["CAPABILITY_NAMED_IAM"]
|
|
414
579
|
})
|
|
415
580
|
);
|
|
416
|
-
|
|
417
|
-
await
|
|
418
|
-
{ client, maxWaitTime: 600 },
|
|
419
|
-
{ StackName: stackName }
|
|
420
|
-
);
|
|
581
|
+
info(`Updating stack ${stackName}...`);
|
|
582
|
+
await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
|
|
421
583
|
success(`Stack ${stackName} updated successfully in account ${accountId}`);
|
|
422
584
|
}
|
|
423
585
|
async function readExistingStack(stackName, accountId, region, credentials) {
|
|
@@ -473,49 +635,6 @@ async function readExistingStack(stackName, accountId, region, credentials) {
|
|
|
473
635
|
}
|
|
474
636
|
}
|
|
475
637
|
|
|
476
|
-
// src/aws/oidc-provider.ts
|
|
477
|
-
import {
|
|
478
|
-
IAMClient,
|
|
479
|
-
GetOpenIDConnectProviderCommand,
|
|
480
|
-
ListOpenIDConnectProvidersCommand
|
|
481
|
-
} from "@aws-sdk/client-iam";
|
|
482
|
-
async function checkOidcProviderExists(credentials, region) {
|
|
483
|
-
const client = new IAMClient({
|
|
484
|
-
credentials,
|
|
485
|
-
region
|
|
486
|
-
});
|
|
487
|
-
try {
|
|
488
|
-
const response = await client.send(new ListOpenIDConnectProvidersCommand({}));
|
|
489
|
-
const providers = response.OpenIDConnectProviderList || [];
|
|
490
|
-
for (const provider of providers) {
|
|
491
|
-
if (!provider.Arn) continue;
|
|
492
|
-
try {
|
|
493
|
-
const providerDetails = await client.send(
|
|
494
|
-
new GetOpenIDConnectProviderCommand({
|
|
495
|
-
OpenIDConnectProviderArn: provider.Arn
|
|
496
|
-
})
|
|
497
|
-
);
|
|
498
|
-
if (providerDetails.Url?.includes(OIDC_PROVIDER_URL)) {
|
|
499
|
-
verbose(`Found existing OIDC provider: ${provider.Arn}`);
|
|
500
|
-
return {
|
|
501
|
-
exists: true,
|
|
502
|
-
arn: provider.Arn
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
} catch {
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
verbose(`No existing OIDC provider found for ${OIDC_PROVIDER_URL}`);
|
|
509
|
-
return { exists: false };
|
|
510
|
-
} catch (error2) {
|
|
511
|
-
verbose(`Error checking OIDC providers: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
512
|
-
return { exists: false };
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
function getOidcThumbprint() {
|
|
516
|
-
return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
517
|
-
}
|
|
518
|
-
|
|
519
638
|
// src/auth/browser-auth.ts
|
|
520
639
|
import express from "express";
|
|
521
640
|
import open from "open";
|
|
@@ -1154,6 +1273,16 @@ function getArtifactId(artifact) {
|
|
|
1154
1273
|
return artifact.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1155
1274
|
}
|
|
1156
1275
|
|
|
1276
|
+
// src/aws/oidc-provider.ts
|
|
1277
|
+
import {
|
|
1278
|
+
IAMClient,
|
|
1279
|
+
GetOpenIDConnectProviderCommand,
|
|
1280
|
+
ListOpenIDConnectProvidersCommand
|
|
1281
|
+
} from "@aws-sdk/client-iam";
|
|
1282
|
+
function getOidcThumbprint() {
|
|
1283
|
+
return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1157
1286
|
// src/templates/common.ts
|
|
1158
1287
|
var STANDARD_TAGS = [
|
|
1159
1288
|
{ Key: "CreatedBy", Value: "DevRamps" },
|
|
@@ -1366,6 +1495,9 @@ function getPipelineStackName(pipelineSlug) {
|
|
|
1366
1495
|
function getStageStackName(pipelineSlug, stageName) {
|
|
1367
1496
|
return truncateName(`DevRamps-${pipelineSlug}-${stageName}-Stage`, CF_STACK_MAX_LENGTH);
|
|
1368
1497
|
}
|
|
1498
|
+
function getAccountStackName() {
|
|
1499
|
+
return "DevRamps-Account-Bootstrap";
|
|
1500
|
+
}
|
|
1369
1501
|
function getKmsKeyAlias(orgSlug) {
|
|
1370
1502
|
return `alias/devramps-${normalizeName(orgSlug)}`;
|
|
1371
1503
|
}
|
|
@@ -1821,6 +1953,21 @@ function generatePipelineStackTemplate(options) {
|
|
|
1821
1953
|
return template;
|
|
1822
1954
|
}
|
|
1823
1955
|
|
|
1956
|
+
// src/templates/account-stack.ts
|
|
1957
|
+
function generateAccountStackTemplate() {
|
|
1958
|
+
const template = createBaseTemplate(
|
|
1959
|
+
"DevRamps Account Bootstrap Stack - Creates OIDC provider for the account"
|
|
1960
|
+
);
|
|
1961
|
+
addOidcProviderResource(template, false);
|
|
1962
|
+
template.Outputs = {
|
|
1963
|
+
OIDCProviderArn: {
|
|
1964
|
+
Description: "ARN of the OIDC provider",
|
|
1965
|
+
Value: { "Fn::GetAtt": ["DevRampsOIDCProvider", "Arn"] }
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
return template;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1824
1971
|
// src/permissions/eks-deploy.ts
|
|
1825
1972
|
var EKS_DEPLOY_PERMISSIONS = {
|
|
1826
1973
|
actions: [
|
|
@@ -2008,7 +2155,6 @@ function generateStageStackTemplate(options) {
|
|
|
2008
2155
|
const template = createBaseTemplate(
|
|
2009
2156
|
`DevRamps Stage Stack for ${pipelineSlug}/${stageName}`
|
|
2010
2157
|
);
|
|
2011
|
-
addOidcProviderResource(template, true);
|
|
2012
2158
|
const roleName = generateStageRoleName(pipelineSlug, stageName);
|
|
2013
2159
|
const trustPolicy = buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, stageName);
|
|
2014
2160
|
const policies = buildStagePolicies(steps, additionalPolicies);
|
|
@@ -2054,6 +2200,7 @@ function generateStageStackTemplate(options) {
|
|
|
2054
2200
|
);
|
|
2055
2201
|
s3Outputs[artifact.name] = { resourceId };
|
|
2056
2202
|
}
|
|
2203
|
+
const oidcProviderArn = `arn:aws:iam::${accountId}:oidc-provider/${OIDC_PROVIDER_URL}`;
|
|
2057
2204
|
template.Outputs = {
|
|
2058
2205
|
StageRoleArn: {
|
|
2059
2206
|
Description: "ARN of the stage deployment role",
|
|
@@ -2065,8 +2212,8 @@ function generateStageStackTemplate(options) {
|
|
|
2065
2212
|
Value: { Ref: "StageDeploymentRole" }
|
|
2066
2213
|
},
|
|
2067
2214
|
OIDCProviderArn: {
|
|
2068
|
-
Description: "ARN of the OIDC provider",
|
|
2069
|
-
Value:
|
|
2215
|
+
Description: "ARN of the OIDC provider (created by Account Bootstrap stack)",
|
|
2216
|
+
Value: oidcProviderArn
|
|
2070
2217
|
},
|
|
2071
2218
|
PipelineSlug: {
|
|
2072
2219
|
Description: "Pipeline slug",
|
|
@@ -2286,7 +2433,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2286
2433
|
}
|
|
2287
2434
|
const orgStackName = getOrgStackName(orgSlug);
|
|
2288
2435
|
const orgStack = {
|
|
2289
|
-
stackType: "Org"
|
|
2436
|
+
stackType: "Org" /* ORG */,
|
|
2290
2437
|
stackName: orgStackName,
|
|
2291
2438
|
accountId: cicdAccountId,
|
|
2292
2439
|
region: cicdRegion,
|
|
@@ -2300,7 +2447,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2300
2447
|
const filteredArtifacts = filterArtifactsForPipelineStack(artifacts);
|
|
2301
2448
|
const stackName = getPipelineStackName(pipeline.slug);
|
|
2302
2449
|
pipelineStacks.push({
|
|
2303
|
-
stackType: "Pipeline"
|
|
2450
|
+
stackType: "Pipeline" /* PIPELINE */,
|
|
2304
2451
|
stackName,
|
|
2305
2452
|
accountId: cicdAccountId,
|
|
2306
2453
|
region: cicdRegion,
|
|
@@ -2310,6 +2457,38 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2310
2457
|
bundleArtifacts: filteredArtifacts.bundle
|
|
2311
2458
|
});
|
|
2312
2459
|
}
|
|
2460
|
+
const accountStacks = [];
|
|
2461
|
+
const accountStackName = getAccountStackName();
|
|
2462
|
+
const accountsWithStacks = /* @__PURE__ */ new Set();
|
|
2463
|
+
for (const pipeline of pipelines) {
|
|
2464
|
+
for (const stage of pipeline.stages) {
|
|
2465
|
+
if (accountsWithStacks.has(stage.account_id)) {
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
accountsWithStacks.add(stage.account_id);
|
|
2469
|
+
let accountCredentials;
|
|
2470
|
+
try {
|
|
2471
|
+
if (stage.account_id !== currentAccountId) {
|
|
2472
|
+
const assumed = await assumeRoleForAccount({
|
|
2473
|
+
targetAccountId: stage.account_id,
|
|
2474
|
+
currentAccountId,
|
|
2475
|
+
targetRoleName
|
|
2476
|
+
});
|
|
2477
|
+
accountCredentials = assumed?.credentials;
|
|
2478
|
+
}
|
|
2479
|
+
} catch {
|
|
2480
|
+
verbose(`Could not assume role in ${stage.account_id} for status check`);
|
|
2481
|
+
}
|
|
2482
|
+
accountStacks.push({
|
|
2483
|
+
stackType: "Account" /* ACCOUNT */,
|
|
2484
|
+
stackName: accountStackName,
|
|
2485
|
+
accountId: stage.account_id,
|
|
2486
|
+
region: cicdRegion,
|
|
2487
|
+
// Deploy in CI/CD region for consistency
|
|
2488
|
+
action: await determineStackAction(accountStackName, accountCredentials, cicdRegion)
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2313
2492
|
const stageStacks = [];
|
|
2314
2493
|
for (const pipeline of pipelines) {
|
|
2315
2494
|
const artifacts = pipelineArtifacts.get(pipeline.slug);
|
|
@@ -2329,7 +2508,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2329
2508
|
verbose(`Could not assume role in ${stage.account_id} for status check`);
|
|
2330
2509
|
}
|
|
2331
2510
|
stageStacks.push({
|
|
2332
|
-
stackType: "Stage"
|
|
2511
|
+
stackType: "Stage" /* STAGE */,
|
|
2333
2512
|
stackName,
|
|
2334
2513
|
accountId: stage.account_id,
|
|
2335
2514
|
region: stage.region,
|
|
@@ -2350,6 +2529,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2350
2529
|
cicdRegion,
|
|
2351
2530
|
orgStack,
|
|
2352
2531
|
pipelineStacks,
|
|
2532
|
+
accountStacks,
|
|
2353
2533
|
stageStacks
|
|
2354
2534
|
};
|
|
2355
2535
|
}
|
|
@@ -2373,34 +2553,40 @@ async function showDryRunPlan(plan) {
|
|
|
2373
2553
|
info(` Account: ${plan.orgStack.accountId}`);
|
|
2374
2554
|
info(` Target accounts with bucket access: ${plan.orgStack.targetAccountIds.length}`);
|
|
2375
2555
|
newline();
|
|
2376
|
-
info("Phase 2: Pipeline Stacks");
|
|
2556
|
+
info("Phase 2: Pipeline & Account Stacks (parallel)");
|
|
2377
2557
|
for (const stack of plan.pipelineStacks) {
|
|
2378
2558
|
info(` ${stack.action}: ${stack.stackName}`);
|
|
2379
2559
|
info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
|
|
2380
2560
|
}
|
|
2561
|
+
for (const stack of plan.accountStacks) {
|
|
2562
|
+
info(` ${stack.action}: ${stack.stackName}`);
|
|
2563
|
+
info(` Account: ${stack.accountId} (OIDC provider)`);
|
|
2564
|
+
}
|
|
2381
2565
|
newline();
|
|
2382
|
-
info("Phase 3: Stage Stacks");
|
|
2566
|
+
info("Phase 3: Stage Stacks (parallel)");
|
|
2383
2567
|
for (const stack of plan.stageStacks) {
|
|
2384
2568
|
info(` ${stack.action}: ${stack.stackName}`);
|
|
2385
2569
|
info(` Account: ${stack.accountId}, Region: ${stack.region}`);
|
|
2386
2570
|
info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
|
|
2387
2571
|
}
|
|
2388
|
-
const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
|
|
2572
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
|
|
2389
2573
|
newline();
|
|
2390
2574
|
info(`Total stacks to deploy: ${totalStacks}`);
|
|
2391
2575
|
}
|
|
2392
2576
|
async function confirmDeploymentPlan(plan) {
|
|
2393
|
-
const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
|
|
2577
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
|
|
2394
2578
|
newline();
|
|
2395
2579
|
info(`About to deploy ${totalStacks} stack(s):`);
|
|
2396
2580
|
info(` - 1 Org stack (${plan.orgStack.action})`);
|
|
2397
2581
|
info(` - ${plan.pipelineStacks.length} Pipeline stack(s)`);
|
|
2582
|
+
info(` - ${plan.accountStacks.length} Account stack(s) (OIDC provider)`);
|
|
2398
2583
|
info(` - ${plan.stageStacks.length} Stage stack(s)`);
|
|
2399
2584
|
return confirmDeployment({
|
|
2400
2585
|
orgSlug: plan.orgSlug,
|
|
2401
2586
|
stacks: [
|
|
2402
2587
|
{ ...plan.orgStack, pipelineSlug: "org", steps: [], additionalPoliciesCount: 0 },
|
|
2403
2588
|
...plan.pipelineStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 })),
|
|
2589
|
+
...plan.accountStacks.map((s) => ({ ...s, pipelineSlug: "account", steps: [], additionalPoliciesCount: 0 })),
|
|
2404
2590
|
...plan.stageStacks.map((s) => ({ ...s, steps: s.steps.map((st) => st.name), additionalPoliciesCount: s.additionalPolicies.length }))
|
|
2405
2591
|
]
|
|
2406
2592
|
});
|
|
@@ -2419,28 +2605,75 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
2419
2605
|
throw error2;
|
|
2420
2606
|
}
|
|
2421
2607
|
newline();
|
|
2422
|
-
header("Phase 2: Pipeline Stacks");
|
|
2423
|
-
|
|
2424
|
-
|
|
2608
|
+
header("Phase 2: Pipeline & Account Stacks (parallel)");
|
|
2609
|
+
const totalPhase2Stacks = plan.pipelineStacks.length + plan.accountStacks.length;
|
|
2610
|
+
info(`Deploying ${totalPhase2Stacks} stack(s) in parallel...`);
|
|
2611
|
+
newline();
|
|
2612
|
+
const pipelinePromises = plan.pipelineStacks.map(async (stack) => {
|
|
2425
2613
|
try {
|
|
2426
2614
|
await deployPipelineStack(stack, authData, currentAccountId, options);
|
|
2427
|
-
|
|
2428
|
-
results.success++;
|
|
2615
|
+
return { stack: stack.stackName, success: true };
|
|
2429
2616
|
} catch (error2) {
|
|
2430
|
-
|
|
2617
|
+
return {
|
|
2618
|
+
stack: stack.stackName,
|
|
2619
|
+
success: false,
|
|
2620
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
});
|
|
2624
|
+
const accountPromises = plan.accountStacks.map(async (stack) => {
|
|
2625
|
+
try {
|
|
2626
|
+
await deployAccountStack(stack, currentAccountId, options);
|
|
2627
|
+
return { stack: `${stack.stackName} (${stack.accountId})`, success: true };
|
|
2628
|
+
} catch (error2) {
|
|
2629
|
+
return {
|
|
2630
|
+
stack: `${stack.stackName} (${stack.accountId})`,
|
|
2631
|
+
success: false,
|
|
2632
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
});
|
|
2636
|
+
const pipelineResults = await Promise.all(pipelinePromises);
|
|
2637
|
+
const accountResults = await Promise.all(accountPromises);
|
|
2638
|
+
newline();
|
|
2639
|
+
let accountStacksFailed = false;
|
|
2640
|
+
for (const result of [...pipelineResults, ...accountResults]) {
|
|
2641
|
+
if (result.success) {
|
|
2642
|
+
success(`${result.stack} deployed`);
|
|
2643
|
+
results.success++;
|
|
2644
|
+
} else {
|
|
2645
|
+
error(`${result.stack} failed: ${result.error}`);
|
|
2431
2646
|
results.failed++;
|
|
2432
2647
|
}
|
|
2433
2648
|
}
|
|
2649
|
+
accountStacksFailed = accountResults.some((r) => !r.success);
|
|
2650
|
+
if (accountStacksFailed) {
|
|
2651
|
+
warn("Some Account stacks failed. Stage stacks may fail if their account OIDC provider did not deploy.");
|
|
2652
|
+
}
|
|
2434
2653
|
newline();
|
|
2435
|
-
header("Phase 3: Stage Stacks");
|
|
2436
|
-
|
|
2437
|
-
|
|
2654
|
+
header("Phase 3: Stage Stacks (parallel)");
|
|
2655
|
+
info(`Deploying ${plan.stageStacks.length} stack(s) in parallel...`);
|
|
2656
|
+
newline();
|
|
2657
|
+
const stagePromises = plan.stageStacks.map(async (stack) => {
|
|
2438
2658
|
try {
|
|
2439
2659
|
await deployStageStack(stack, authData, currentAccountId, options);
|
|
2440
|
-
|
|
2441
|
-
results.success++;
|
|
2660
|
+
return { stack: stack.stackName, success: true };
|
|
2442
2661
|
} catch (error2) {
|
|
2443
|
-
|
|
2662
|
+
return {
|
|
2663
|
+
stack: stack.stackName,
|
|
2664
|
+
success: false,
|
|
2665
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
const phase3Results = await Promise.all(stagePromises);
|
|
2670
|
+
newline();
|
|
2671
|
+
for (const result of phase3Results) {
|
|
2672
|
+
if (result.success) {
|
|
2673
|
+
success(`${result.stack} deployed`);
|
|
2674
|
+
results.success++;
|
|
2675
|
+
} else {
|
|
2676
|
+
error(`${result.stack} failed: ${result.error}`);
|
|
2444
2677
|
results.failed++;
|
|
2445
2678
|
}
|
|
2446
2679
|
}
|
|
@@ -2512,7 +2745,28 @@ async function deployPipelineStack(stack, authData, currentAccountId, options) {
|
|
|
2512
2745
|
template,
|
|
2513
2746
|
accountId: cicdAccountId,
|
|
2514
2747
|
region: cicdRegion,
|
|
2515
|
-
credentials
|
|
2748
|
+
credentials,
|
|
2749
|
+
showProgress: false
|
|
2750
|
+
// Disable progress bar for parallel deployment
|
|
2751
|
+
};
|
|
2752
|
+
await previewStackChanges(deployOptions);
|
|
2753
|
+
await deployStack(deployOptions);
|
|
2754
|
+
}
|
|
2755
|
+
async function deployAccountStack(stack, currentAccountId, options) {
|
|
2756
|
+
const credentials = stack.accountId !== currentAccountId ? (await assumeRoleForAccount({
|
|
2757
|
+
targetAccountId: stack.accountId,
|
|
2758
|
+
currentAccountId,
|
|
2759
|
+
targetRoleName: options.targetAccountRoleName
|
|
2760
|
+
}))?.credentials : void 0;
|
|
2761
|
+
const template = generateAccountStackTemplate();
|
|
2762
|
+
const deployOptions = {
|
|
2763
|
+
stackName: stack.stackName,
|
|
2764
|
+
template,
|
|
2765
|
+
accountId: stack.accountId,
|
|
2766
|
+
region: stack.region,
|
|
2767
|
+
credentials,
|
|
2768
|
+
showProgress: false
|
|
2769
|
+
// Disable progress bar for parallel deployment
|
|
2516
2770
|
};
|
|
2517
2771
|
await previewStackChanges(deployOptions);
|
|
2518
2772
|
await deployStack(deployOptions);
|
|
@@ -2523,8 +2777,6 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
|
|
|
2523
2777
|
currentAccountId,
|
|
2524
2778
|
targetRoleName: options.targetAccountRoleName
|
|
2525
2779
|
}))?.credentials : void 0;
|
|
2526
|
-
const oidcInfo = await checkOidcProviderExists(credentials, stack.region);
|
|
2527
|
-
verbose(`OIDC provider in ${stack.accountId}: ${oidcInfo.exists ? "exists" : "will be created"}`);
|
|
2528
2780
|
const template = generateStageStackTemplate({
|
|
2529
2781
|
pipelineSlug: stack.pipelineSlug,
|
|
2530
2782
|
stageName: stack.stageName,
|
|
@@ -2540,7 +2792,9 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
|
|
|
2540
2792
|
template,
|
|
2541
2793
|
accountId: stack.accountId,
|
|
2542
2794
|
region: stack.region,
|
|
2543
|
-
credentials
|
|
2795
|
+
credentials,
|
|
2796
|
+
showProgress: false
|
|
2797
|
+
// Disable progress bar for parallel deployment
|
|
2544
2798
|
};
|
|
2545
2799
|
await previewStackChanges(deployOptions);
|
|
2546
2800
|
await deployStack(deployOptions);
|