@devramps/cli 0.1.0 → 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 +345 -87
- 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";
|
|
@@ -279,6 +352,10 @@ async function previewStackChanges(options) {
|
|
|
279
352
|
const templateBody = JSON.stringify(template);
|
|
280
353
|
const stackStatus = await getStackStatus(stackName, credentials, region);
|
|
281
354
|
const changeSetName = `devramps-preview-${Date.now()}`;
|
|
355
|
+
if (!stackStatus.exists) {
|
|
356
|
+
info(` Stack ${stackName} will be created (new stack)`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
282
359
|
try {
|
|
283
360
|
await client.send(
|
|
284
361
|
new CreateChangeSetCommand({
|
|
@@ -286,7 +363,7 @@ async function previewStackChanges(options) {
|
|
|
286
363
|
ChangeSetName: changeSetName,
|
|
287
364
|
TemplateBody: templateBody,
|
|
288
365
|
Capabilities: ["CAPABILITY_NAMED_IAM"],
|
|
289
|
-
ChangeSetType:
|
|
366
|
+
ChangeSetType: ChangeSetType.UPDATE
|
|
290
367
|
})
|
|
291
368
|
);
|
|
292
369
|
await waitUntilChangeSetCreateComplete(
|
|
@@ -357,21 +434,114 @@ function getActionSymbol(action) {
|
|
|
357
434
|
return " ";
|
|
358
435
|
}
|
|
359
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
|
+
}
|
|
360
529
|
async function deployStack(options) {
|
|
361
|
-
const { stackName, template, accountId, region, credentials } = options;
|
|
530
|
+
const { stackName, template, accountId, region, credentials, showProgress = true } = options;
|
|
362
531
|
const client = new CloudFormationClient({
|
|
363
532
|
credentials,
|
|
364
533
|
region
|
|
365
534
|
});
|
|
366
535
|
const templateBody = JSON.stringify(template);
|
|
536
|
+
const resourceCount = Object.keys(template.Resources || {}).length;
|
|
367
537
|
try {
|
|
368
538
|
const stackStatus = await getStackStatus(stackName, credentials, region);
|
|
369
539
|
if (stackStatus.exists) {
|
|
370
540
|
verbose(`Stack ${stackName} exists, updating...`);
|
|
371
|
-
await updateStack(client, stackName, templateBody, accountId);
|
|
541
|
+
await updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
|
|
372
542
|
} else {
|
|
373
543
|
verbose(`Stack ${stackName} does not exist, creating...`);
|
|
374
|
-
await createStack(client, stackName, templateBody, accountId);
|
|
544
|
+
await createStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
|
|
375
545
|
}
|
|
376
546
|
} catch (error2) {
|
|
377
547
|
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
@@ -382,7 +552,8 @@ async function deployStack(options) {
|
|
|
382
552
|
throw new CloudFormationError(stackName, accountId, errorMessage);
|
|
383
553
|
}
|
|
384
554
|
}
|
|
385
|
-
async function createStack(client, stackName, templateBody, accountId) {
|
|
555
|
+
async function createStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
|
|
556
|
+
const operationStartTime = /* @__PURE__ */ new Date();
|
|
386
557
|
await client.send(
|
|
387
558
|
new CreateStackCommand({
|
|
388
559
|
StackName: stackName,
|
|
@@ -394,14 +565,12 @@ async function createStack(client, stackName, templateBody, accountId) {
|
|
|
394
565
|
]
|
|
395
566
|
})
|
|
396
567
|
);
|
|
397
|
-
|
|
398
|
-
await
|
|
399
|
-
{ client, maxWaitTime: 600 },
|
|
400
|
-
{ StackName: stackName }
|
|
401
|
-
);
|
|
568
|
+
info(`Creating stack ${stackName}...`);
|
|
569
|
+
await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
|
|
402
570
|
success(`Stack ${stackName} created successfully in account ${accountId}`);
|
|
403
571
|
}
|
|
404
|
-
async function updateStack(client, stackName, templateBody, accountId) {
|
|
572
|
+
async function updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
|
|
573
|
+
const operationStartTime = /* @__PURE__ */ new Date();
|
|
405
574
|
await client.send(
|
|
406
575
|
new UpdateStackCommand({
|
|
407
576
|
StackName: stackName,
|
|
@@ -409,11 +578,8 @@ async function updateStack(client, stackName, templateBody, accountId) {
|
|
|
409
578
|
Capabilities: ["CAPABILITY_NAMED_IAM"]
|
|
410
579
|
})
|
|
411
580
|
);
|
|
412
|
-
|
|
413
|
-
await
|
|
414
|
-
{ client, maxWaitTime: 600 },
|
|
415
|
-
{ StackName: stackName }
|
|
416
|
-
);
|
|
581
|
+
info(`Updating stack ${stackName}...`);
|
|
582
|
+
await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
|
|
417
583
|
success(`Stack ${stackName} updated successfully in account ${accountId}`);
|
|
418
584
|
}
|
|
419
585
|
async function readExistingStack(stackName, accountId, region, credentials) {
|
|
@@ -469,49 +635,6 @@ async function readExistingStack(stackName, accountId, region, credentials) {
|
|
|
469
635
|
}
|
|
470
636
|
}
|
|
471
637
|
|
|
472
|
-
// src/aws/oidc-provider.ts
|
|
473
|
-
import {
|
|
474
|
-
IAMClient,
|
|
475
|
-
GetOpenIDConnectProviderCommand,
|
|
476
|
-
ListOpenIDConnectProvidersCommand
|
|
477
|
-
} from "@aws-sdk/client-iam";
|
|
478
|
-
async function checkOidcProviderExists(credentials, region) {
|
|
479
|
-
const client = new IAMClient({
|
|
480
|
-
credentials,
|
|
481
|
-
region
|
|
482
|
-
});
|
|
483
|
-
try {
|
|
484
|
-
const response = await client.send(new ListOpenIDConnectProvidersCommand({}));
|
|
485
|
-
const providers = response.OpenIDConnectProviderList || [];
|
|
486
|
-
for (const provider of providers) {
|
|
487
|
-
if (!provider.Arn) continue;
|
|
488
|
-
try {
|
|
489
|
-
const providerDetails = await client.send(
|
|
490
|
-
new GetOpenIDConnectProviderCommand({
|
|
491
|
-
OpenIDConnectProviderArn: provider.Arn
|
|
492
|
-
})
|
|
493
|
-
);
|
|
494
|
-
if (providerDetails.Url?.includes(OIDC_PROVIDER_URL)) {
|
|
495
|
-
verbose(`Found existing OIDC provider: ${provider.Arn}`);
|
|
496
|
-
return {
|
|
497
|
-
exists: true,
|
|
498
|
-
arn: provider.Arn
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
} catch {
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
verbose(`No existing OIDC provider found for ${OIDC_PROVIDER_URL}`);
|
|
505
|
-
return { exists: false };
|
|
506
|
-
} catch (error2) {
|
|
507
|
-
verbose(`Error checking OIDC providers: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
508
|
-
return { exists: false };
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
function getOidcThumbprint() {
|
|
512
|
-
return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
513
|
-
}
|
|
514
|
-
|
|
515
638
|
// src/auth/browser-auth.ts
|
|
516
639
|
import express from "express";
|
|
517
640
|
import open from "open";
|
|
@@ -1150,6 +1273,16 @@ function getArtifactId(artifact) {
|
|
|
1150
1273
|
return artifact.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1151
1274
|
}
|
|
1152
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
|
+
|
|
1153
1286
|
// src/templates/common.ts
|
|
1154
1287
|
var STANDARD_TAGS = [
|
|
1155
1288
|
{ Key: "CreatedBy", Value: "DevRamps" },
|
|
@@ -1362,6 +1495,9 @@ function getPipelineStackName(pipelineSlug) {
|
|
|
1362
1495
|
function getStageStackName(pipelineSlug, stageName) {
|
|
1363
1496
|
return truncateName(`DevRamps-${pipelineSlug}-${stageName}-Stage`, CF_STACK_MAX_LENGTH);
|
|
1364
1497
|
}
|
|
1498
|
+
function getAccountStackName() {
|
|
1499
|
+
return "DevRamps-Account-Bootstrap";
|
|
1500
|
+
}
|
|
1365
1501
|
function getKmsKeyAlias(orgSlug) {
|
|
1366
1502
|
return `alias/devramps-${normalizeName(orgSlug)}`;
|
|
1367
1503
|
}
|
|
@@ -1817,6 +1953,21 @@ function generatePipelineStackTemplate(options) {
|
|
|
1817
1953
|
return template;
|
|
1818
1954
|
}
|
|
1819
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
|
+
|
|
1820
1971
|
// src/permissions/eks-deploy.ts
|
|
1821
1972
|
var EKS_DEPLOY_PERMISSIONS = {
|
|
1822
1973
|
actions: [
|
|
@@ -2004,7 +2155,6 @@ function generateStageStackTemplate(options) {
|
|
|
2004
2155
|
const template = createBaseTemplate(
|
|
2005
2156
|
`DevRamps Stage Stack for ${pipelineSlug}/${stageName}`
|
|
2006
2157
|
);
|
|
2007
|
-
addOidcProviderResource(template, true);
|
|
2008
2158
|
const roleName = generateStageRoleName(pipelineSlug, stageName);
|
|
2009
2159
|
const trustPolicy = buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, stageName);
|
|
2010
2160
|
const policies = buildStagePolicies(steps, additionalPolicies);
|
|
@@ -2050,6 +2200,7 @@ function generateStageStackTemplate(options) {
|
|
|
2050
2200
|
);
|
|
2051
2201
|
s3Outputs[artifact.name] = { resourceId };
|
|
2052
2202
|
}
|
|
2203
|
+
const oidcProviderArn = `arn:aws:iam::${accountId}:oidc-provider/${OIDC_PROVIDER_URL}`;
|
|
2053
2204
|
template.Outputs = {
|
|
2054
2205
|
StageRoleArn: {
|
|
2055
2206
|
Description: "ARN of the stage deployment role",
|
|
@@ -2061,8 +2212,8 @@ function generateStageStackTemplate(options) {
|
|
|
2061
2212
|
Value: { Ref: "StageDeploymentRole" }
|
|
2062
2213
|
},
|
|
2063
2214
|
OIDCProviderArn: {
|
|
2064
|
-
Description: "ARN of the OIDC provider",
|
|
2065
|
-
Value:
|
|
2215
|
+
Description: "ARN of the OIDC provider (created by Account Bootstrap stack)",
|
|
2216
|
+
Value: oidcProviderArn
|
|
2066
2217
|
},
|
|
2067
2218
|
PipelineSlug: {
|
|
2068
2219
|
Description: "Pipeline slug",
|
|
@@ -2282,7 +2433,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2282
2433
|
}
|
|
2283
2434
|
const orgStackName = getOrgStackName(orgSlug);
|
|
2284
2435
|
const orgStack = {
|
|
2285
|
-
stackType: "Org"
|
|
2436
|
+
stackType: "Org" /* ORG */,
|
|
2286
2437
|
stackName: orgStackName,
|
|
2287
2438
|
accountId: cicdAccountId,
|
|
2288
2439
|
region: cicdRegion,
|
|
@@ -2296,7 +2447,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2296
2447
|
const filteredArtifacts = filterArtifactsForPipelineStack(artifacts);
|
|
2297
2448
|
const stackName = getPipelineStackName(pipeline.slug);
|
|
2298
2449
|
pipelineStacks.push({
|
|
2299
|
-
stackType: "Pipeline"
|
|
2450
|
+
stackType: "Pipeline" /* PIPELINE */,
|
|
2300
2451
|
stackName,
|
|
2301
2452
|
accountId: cicdAccountId,
|
|
2302
2453
|
region: cicdRegion,
|
|
@@ -2306,6 +2457,38 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2306
2457
|
bundleArtifacts: filteredArtifacts.bundle
|
|
2307
2458
|
});
|
|
2308
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
|
+
}
|
|
2309
2492
|
const stageStacks = [];
|
|
2310
2493
|
for (const pipeline of pipelines) {
|
|
2311
2494
|
const artifacts = pipelineArtifacts.get(pipeline.slug);
|
|
@@ -2325,7 +2508,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2325
2508
|
verbose(`Could not assume role in ${stage.account_id} for status check`);
|
|
2326
2509
|
}
|
|
2327
2510
|
stageStacks.push({
|
|
2328
|
-
stackType: "Stage"
|
|
2511
|
+
stackType: "Stage" /* STAGE */,
|
|
2329
2512
|
stackName,
|
|
2330
2513
|
accountId: stage.account_id,
|
|
2331
2514
|
region: stage.region,
|
|
@@ -2346,6 +2529,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2346
2529
|
cicdRegion,
|
|
2347
2530
|
orgStack,
|
|
2348
2531
|
pipelineStacks,
|
|
2532
|
+
accountStacks,
|
|
2349
2533
|
stageStacks
|
|
2350
2534
|
};
|
|
2351
2535
|
}
|
|
@@ -2369,34 +2553,40 @@ async function showDryRunPlan(plan) {
|
|
|
2369
2553
|
info(` Account: ${plan.orgStack.accountId}`);
|
|
2370
2554
|
info(` Target accounts with bucket access: ${plan.orgStack.targetAccountIds.length}`);
|
|
2371
2555
|
newline();
|
|
2372
|
-
info("Phase 2: Pipeline Stacks");
|
|
2556
|
+
info("Phase 2: Pipeline & Account Stacks (parallel)");
|
|
2373
2557
|
for (const stack of plan.pipelineStacks) {
|
|
2374
2558
|
info(` ${stack.action}: ${stack.stackName}`);
|
|
2375
2559
|
info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
|
|
2376
2560
|
}
|
|
2561
|
+
for (const stack of plan.accountStacks) {
|
|
2562
|
+
info(` ${stack.action}: ${stack.stackName}`);
|
|
2563
|
+
info(` Account: ${stack.accountId} (OIDC provider)`);
|
|
2564
|
+
}
|
|
2377
2565
|
newline();
|
|
2378
|
-
info("Phase 3: Stage Stacks");
|
|
2566
|
+
info("Phase 3: Stage Stacks (parallel)");
|
|
2379
2567
|
for (const stack of plan.stageStacks) {
|
|
2380
2568
|
info(` ${stack.action}: ${stack.stackName}`);
|
|
2381
2569
|
info(` Account: ${stack.accountId}, Region: ${stack.region}`);
|
|
2382
2570
|
info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
|
|
2383
2571
|
}
|
|
2384
|
-
const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
|
|
2572
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
|
|
2385
2573
|
newline();
|
|
2386
2574
|
info(`Total stacks to deploy: ${totalStacks}`);
|
|
2387
2575
|
}
|
|
2388
2576
|
async function confirmDeploymentPlan(plan) {
|
|
2389
|
-
const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
|
|
2577
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
|
|
2390
2578
|
newline();
|
|
2391
2579
|
info(`About to deploy ${totalStacks} stack(s):`);
|
|
2392
2580
|
info(` - 1 Org stack (${plan.orgStack.action})`);
|
|
2393
2581
|
info(` - ${plan.pipelineStacks.length} Pipeline stack(s)`);
|
|
2582
|
+
info(` - ${plan.accountStacks.length} Account stack(s) (OIDC provider)`);
|
|
2394
2583
|
info(` - ${plan.stageStacks.length} Stage stack(s)`);
|
|
2395
2584
|
return confirmDeployment({
|
|
2396
2585
|
orgSlug: plan.orgSlug,
|
|
2397
2586
|
stacks: [
|
|
2398
2587
|
{ ...plan.orgStack, pipelineSlug: "org", steps: [], additionalPoliciesCount: 0 },
|
|
2399
2588
|
...plan.pipelineStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 })),
|
|
2589
|
+
...plan.accountStacks.map((s) => ({ ...s, pipelineSlug: "account", steps: [], additionalPoliciesCount: 0 })),
|
|
2400
2590
|
...plan.stageStacks.map((s) => ({ ...s, steps: s.steps.map((st) => st.name), additionalPoliciesCount: s.additionalPolicies.length }))
|
|
2401
2591
|
]
|
|
2402
2592
|
});
|
|
@@ -2415,28 +2605,75 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
2415
2605
|
throw error2;
|
|
2416
2606
|
}
|
|
2417
2607
|
newline();
|
|
2418
|
-
header("Phase 2: Pipeline Stacks");
|
|
2419
|
-
|
|
2420
|
-
|
|
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) => {
|
|
2421
2613
|
try {
|
|
2422
2614
|
await deployPipelineStack(stack, authData, currentAccountId, options);
|
|
2423
|
-
|
|
2424
|
-
|
|
2615
|
+
return { stack: stack.stackName, success: true };
|
|
2616
|
+
} catch (error2) {
|
|
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 };
|
|
2425
2628
|
} catch (error2) {
|
|
2426
|
-
|
|
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}`);
|
|
2427
2646
|
results.failed++;
|
|
2428
2647
|
}
|
|
2429
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
|
+
}
|
|
2430
2653
|
newline();
|
|
2431
|
-
header("Phase 3: Stage Stacks");
|
|
2432
|
-
|
|
2433
|
-
|
|
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) => {
|
|
2434
2658
|
try {
|
|
2435
2659
|
await deployStageStack(stack, authData, currentAccountId, options);
|
|
2436
|
-
|
|
2437
|
-
results.success++;
|
|
2660
|
+
return { stack: stack.stackName, success: true };
|
|
2438
2661
|
} catch (error2) {
|
|
2439
|
-
|
|
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}`);
|
|
2440
2677
|
results.failed++;
|
|
2441
2678
|
}
|
|
2442
2679
|
}
|
|
@@ -2508,7 +2745,28 @@ async function deployPipelineStack(stack, authData, currentAccountId, options) {
|
|
|
2508
2745
|
template,
|
|
2509
2746
|
accountId: cicdAccountId,
|
|
2510
2747
|
region: cicdRegion,
|
|
2511
|
-
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
|
|
2512
2770
|
};
|
|
2513
2771
|
await previewStackChanges(deployOptions);
|
|
2514
2772
|
await deployStack(deployOptions);
|
|
@@ -2519,8 +2777,6 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
|
|
|
2519
2777
|
currentAccountId,
|
|
2520
2778
|
targetRoleName: options.targetAccountRoleName
|
|
2521
2779
|
}))?.credentials : void 0;
|
|
2522
|
-
const oidcInfo = await checkOidcProviderExists(credentials, stack.region);
|
|
2523
|
-
verbose(`OIDC provider in ${stack.accountId}: ${oidcInfo.exists ? "exists" : "will be created"}`);
|
|
2524
2780
|
const template = generateStageStackTemplate({
|
|
2525
2781
|
pipelineSlug: stack.pipelineSlug,
|
|
2526
2782
|
stageName: stack.stageName,
|
|
@@ -2536,7 +2792,9 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
|
|
|
2536
2792
|
template,
|
|
2537
2793
|
accountId: stack.accountId,
|
|
2538
2794
|
region: stack.region,
|
|
2539
|
-
credentials
|
|
2795
|
+
credentials,
|
|
2796
|
+
showProgress: false
|
|
2797
|
+
// Disable progress bar for parallel deployment
|
|
2540
2798
|
};
|
|
2541
2799
|
await previewStackChanges(deployOptions);
|
|
2542
2800
|
await deployStack(deployOptions);
|