@devramps/cli 0.1.1 → 0.1.3
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 +434 -116
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -74,6 +74,113 @@ 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
|
+
completed = 0;
|
|
79
|
+
total = 0;
|
|
80
|
+
label;
|
|
81
|
+
barWidth = 30;
|
|
82
|
+
lastLineCount = 0;
|
|
83
|
+
inProgressResources = /* @__PURE__ */ new Map();
|
|
84
|
+
isTTY;
|
|
85
|
+
constructor(label, total) {
|
|
86
|
+
this.label = label;
|
|
87
|
+
this.total = total;
|
|
88
|
+
this.isTTY = process.stdout.isTTY ?? false;
|
|
89
|
+
this.render();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Update progress with resource status
|
|
93
|
+
*/
|
|
94
|
+
update(completed, resourceStatus) {
|
|
95
|
+
this.completed = completed;
|
|
96
|
+
if (resourceStatus) {
|
|
97
|
+
if (resourceStatus.status === "in_progress") {
|
|
98
|
+
this.inProgressResources.set(resourceStatus.logicalId, resourceStatus);
|
|
99
|
+
} else {
|
|
100
|
+
this.inProgressResources.delete(resourceStatus.logicalId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this.render();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Set a resource as in-progress
|
|
107
|
+
*/
|
|
108
|
+
setInProgress(logicalId, resourceType) {
|
|
109
|
+
this.inProgressResources.set(logicalId, {
|
|
110
|
+
logicalId,
|
|
111
|
+
resourceType,
|
|
112
|
+
status: "in_progress"
|
|
113
|
+
});
|
|
114
|
+
this.render();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Mark a resource as complete (removes from in-progress)
|
|
118
|
+
*/
|
|
119
|
+
setComplete(logicalId) {
|
|
120
|
+
this.inProgressResources.delete(logicalId);
|
|
121
|
+
this.render();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Clear the progress bar from the terminal (only for TTY)
|
|
125
|
+
*/
|
|
126
|
+
clear() {
|
|
127
|
+
if (!this.isTTY) return;
|
|
128
|
+
for (let i = 0; i < this.lastLineCount; i++) {
|
|
129
|
+
process.stdout.write("\x1B[A\x1B[2K");
|
|
130
|
+
}
|
|
131
|
+
this.lastLineCount = 0;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Finish the progress bar (don't clear - leave final state visible)
|
|
135
|
+
*/
|
|
136
|
+
finish() {
|
|
137
|
+
this.clear();
|
|
138
|
+
this.inProgressResources.clear();
|
|
139
|
+
const percentage = this.total > 0 ? this.completed / this.total : 0;
|
|
140
|
+
const filled = Math.round(this.barWidth * percentage);
|
|
141
|
+
const empty = this.barWidth - filled;
|
|
142
|
+
const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
|
|
143
|
+
const count = chalk.cyan(`${this.completed}/${this.total}`);
|
|
144
|
+
const labelText = chalk.bold(this.label);
|
|
145
|
+
console.log(`${labelText} ${bar} ${count} resources`);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Render the progress bar with in-progress resources
|
|
149
|
+
*/
|
|
150
|
+
render() {
|
|
151
|
+
this.clear();
|
|
152
|
+
const lines = [];
|
|
153
|
+
const percentage = this.total > 0 ? this.completed / this.total : 0;
|
|
154
|
+
const filled = Math.round(this.barWidth * percentage);
|
|
155
|
+
const empty = this.barWidth - filled;
|
|
156
|
+
const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(empty));
|
|
157
|
+
const count = chalk.cyan(`${this.completed}/${this.total}`);
|
|
158
|
+
const labelText = chalk.bold(this.label);
|
|
159
|
+
lines.push(`${labelText} ${bar} ${count} resources`);
|
|
160
|
+
const inProgress = Array.from(this.inProgressResources.values()).slice(0, 8);
|
|
161
|
+
if (inProgress.length > 0) {
|
|
162
|
+
for (const resource of inProgress) {
|
|
163
|
+
const spinner = chalk.yellow("\u22EF");
|
|
164
|
+
const type = chalk.gray(resource.resourceType);
|
|
165
|
+
lines.push(` ${spinner} ${resource.logicalId} ${type}`);
|
|
166
|
+
}
|
|
167
|
+
const remaining = this.inProgressResources.size - inProgress.length;
|
|
168
|
+
if (remaining > 0) {
|
|
169
|
+
lines.push(chalk.gray(` ... and ${remaining} more in progress`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (this.isTTY) {
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
process.stdout.write(line + "\n");
|
|
175
|
+
}
|
|
176
|
+
this.lastLineCount = lines.length;
|
|
177
|
+
} else {
|
|
178
|
+
if (this.completed === 0 || this.completed === this.total) {
|
|
179
|
+
console.log(lines[0]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
77
184
|
function setVerbose(enabled) {
|
|
78
185
|
verboseMode = enabled;
|
|
79
186
|
}
|
|
@@ -134,6 +241,18 @@ function table(rows) {
|
|
|
134
241
|
function newline() {
|
|
135
242
|
console.log();
|
|
136
243
|
}
|
|
244
|
+
function resourceProgress(stackName, resourceId, resourceType, status, reason) {
|
|
245
|
+
const stackLabel = chalk.cyan(`[${stackName}]`);
|
|
246
|
+
const typeLabel = chalk.gray(resourceType);
|
|
247
|
+
if (status === "in_progress") {
|
|
248
|
+
console.log(`${stackLabel} ${chalk.yellow("\u22EF")} ${resourceId} ${typeLabel}`);
|
|
249
|
+
} else if (status === "complete") {
|
|
250
|
+
console.log(`${stackLabel} ${chalk.green("\u2714")} ${resourceId} ${typeLabel}`);
|
|
251
|
+
} else if (status === "failed") {
|
|
252
|
+
const reasonText = reason ? ` - ${reason}` : "";
|
|
253
|
+
console.log(`${stackLabel} ${chalk.red("\u2716")} ${resourceId} ${typeLabel}${reasonText}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
137
256
|
|
|
138
257
|
// src/aws/credentials.ts
|
|
139
258
|
var DEFAULT_REGION = "us-east-1";
|
|
@@ -234,13 +353,12 @@ import {
|
|
|
234
353
|
CloudFormationClient,
|
|
235
354
|
DescribeStacksCommand,
|
|
236
355
|
DescribeStackResourcesCommand,
|
|
356
|
+
DescribeStackEventsCommand,
|
|
237
357
|
CreateStackCommand,
|
|
238
358
|
UpdateStackCommand,
|
|
239
359
|
CreateChangeSetCommand,
|
|
240
360
|
DescribeChangeSetCommand,
|
|
241
361
|
DeleteChangeSetCommand,
|
|
242
|
-
waitUntilStackCreateComplete,
|
|
243
|
-
waitUntilStackUpdateComplete,
|
|
244
362
|
waitUntilChangeSetCreateComplete,
|
|
245
363
|
ChangeSetType
|
|
246
364
|
} from "@aws-sdk/client-cloudformation";
|
|
@@ -280,7 +398,7 @@ async function previewStackChanges(options) {
|
|
|
280
398
|
const stackStatus = await getStackStatus(stackName, credentials, region);
|
|
281
399
|
const changeSetName = `devramps-preview-${Date.now()}`;
|
|
282
400
|
if (!stackStatus.exists) {
|
|
283
|
-
info(` Stack ${stackName} will be created (new stack)`);
|
|
401
|
+
info(` Stack ${stackName} will be created (new stack) in account ${options.accountId} (${region || "default region"})`);
|
|
284
402
|
return;
|
|
285
403
|
}
|
|
286
404
|
try {
|
|
@@ -303,7 +421,7 @@ async function previewStackChanges(options) {
|
|
|
303
421
|
ChangeSetName: changeSetName
|
|
304
422
|
})
|
|
305
423
|
);
|
|
306
|
-
logStackChanges(stackName, changeSetResponse.Changes || [], stackStatus.exists);
|
|
424
|
+
logStackChanges(stackName, changeSetResponse.Changes || [], stackStatus.exists, options.accountId, region);
|
|
307
425
|
await client.send(
|
|
308
426
|
new DeleteChangeSetCommand({
|
|
309
427
|
StackName: stackName,
|
|
@@ -328,13 +446,13 @@ async function previewStackChanges(options) {
|
|
|
328
446
|
verbose(` Could not preview changes for ${stackName}: ${errorMessage}`);
|
|
329
447
|
}
|
|
330
448
|
}
|
|
331
|
-
function logStackChanges(stackName, changes, isUpdate) {
|
|
449
|
+
function logStackChanges(stackName, changes, isUpdate, accountId, region) {
|
|
332
450
|
if (changes.length === 0) {
|
|
333
451
|
verbose(` Stack ${stackName}: No changes`);
|
|
334
452
|
return;
|
|
335
453
|
}
|
|
336
454
|
const action = isUpdate ? "update" : "create";
|
|
337
|
-
info(` Stack ${stackName} will ${action} ${changes.length} resource(s):`);
|
|
455
|
+
info(` Stack ${stackName} will ${action} ${changes.length} resource(s) in account ${accountId} (${region || "default region"}):`);
|
|
338
456
|
for (const change of changes) {
|
|
339
457
|
const resourceChange = change.ResourceChange;
|
|
340
458
|
if (!resourceChange) continue;
|
|
@@ -361,21 +479,137 @@ function getActionSymbol(action) {
|
|
|
361
479
|
return " ";
|
|
362
480
|
}
|
|
363
481
|
}
|
|
482
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set([
|
|
483
|
+
"CREATE_COMPLETE",
|
|
484
|
+
"CREATE_FAILED",
|
|
485
|
+
"DELETE_COMPLETE",
|
|
486
|
+
"DELETE_FAILED",
|
|
487
|
+
"ROLLBACK_COMPLETE",
|
|
488
|
+
"ROLLBACK_FAILED",
|
|
489
|
+
"UPDATE_COMPLETE",
|
|
490
|
+
"UPDATE_FAILED",
|
|
491
|
+
"UPDATE_ROLLBACK_COMPLETE",
|
|
492
|
+
"UPDATE_ROLLBACK_FAILED"
|
|
493
|
+
]);
|
|
494
|
+
var SUCCESS_STATES = /* @__PURE__ */ new Set([
|
|
495
|
+
"CREATE_COMPLETE",
|
|
496
|
+
"UPDATE_COMPLETE"
|
|
497
|
+
]);
|
|
498
|
+
function isResourceComplete(status) {
|
|
499
|
+
if (!status) return false;
|
|
500
|
+
return status.includes("_COMPLETE") && !status.includes("ROLLBACK");
|
|
501
|
+
}
|
|
502
|
+
async function waitForStackWithProgress(client, stackName, operationStartTime, totalResources, maxWaitTime = 600, showProgress = true) {
|
|
503
|
+
const seenEventIds = /* @__PURE__ */ new Set();
|
|
504
|
+
const completedResources = /* @__PURE__ */ new Set();
|
|
505
|
+
const inProgressResources = /* @__PURE__ */ new Map();
|
|
506
|
+
const startTime = Date.now();
|
|
507
|
+
const pollInterval = 3e3;
|
|
508
|
+
const progressBar = showProgress ? new ProgressBar(stackName, totalResources) : null;
|
|
509
|
+
try {
|
|
510
|
+
while (true) {
|
|
511
|
+
if (Date.now() - startTime > maxWaitTime * 1e3) {
|
|
512
|
+
throw new Error(`Stack operation timed out after ${maxWaitTime} seconds`);
|
|
513
|
+
}
|
|
514
|
+
const stackResponse = await client.send(
|
|
515
|
+
new DescribeStacksCommand({ StackName: stackName })
|
|
516
|
+
);
|
|
517
|
+
const stack = stackResponse.Stacks?.[0];
|
|
518
|
+
if (!stack) {
|
|
519
|
+
throw new Error(`Stack ${stackName} not found`);
|
|
520
|
+
}
|
|
521
|
+
const eventsResponse = await client.send(
|
|
522
|
+
new DescribeStackEventsCommand({ StackName: stackName })
|
|
523
|
+
);
|
|
524
|
+
const newEvents = (eventsResponse.StackEvents || []).filter((event) => {
|
|
525
|
+
if (!event.Timestamp || event.Timestamp < operationStartTime) return false;
|
|
526
|
+
if (!event.EventId || seenEventIds.has(event.EventId)) return false;
|
|
527
|
+
return true;
|
|
528
|
+
}).reverse();
|
|
529
|
+
for (const event of newEvents) {
|
|
530
|
+
if (event.EventId) {
|
|
531
|
+
seenEventIds.add(event.EventId);
|
|
532
|
+
}
|
|
533
|
+
const logicalId = event.LogicalResourceId;
|
|
534
|
+
const resourceType = event.ResourceType || "Unknown";
|
|
535
|
+
const status = event.ResourceStatus || "";
|
|
536
|
+
if (logicalId && logicalId !== stackName) {
|
|
537
|
+
if (status.includes("IN_PROGRESS")) {
|
|
538
|
+
inProgressResources.set(logicalId, { resourceType });
|
|
539
|
+
if (progressBar) {
|
|
540
|
+
const resourceStatus = {
|
|
541
|
+
logicalId,
|
|
542
|
+
resourceType,
|
|
543
|
+
status: "in_progress"
|
|
544
|
+
};
|
|
545
|
+
progressBar.update(completedResources.size, resourceStatus);
|
|
546
|
+
} else {
|
|
547
|
+
resourceProgress(stackName, logicalId, resourceType, "in_progress");
|
|
548
|
+
}
|
|
549
|
+
} else if (isResourceComplete(status)) {
|
|
550
|
+
completedResources.add(logicalId);
|
|
551
|
+
inProgressResources.delete(logicalId);
|
|
552
|
+
if (progressBar) {
|
|
553
|
+
const resourceStatus = {
|
|
554
|
+
logicalId,
|
|
555
|
+
resourceType,
|
|
556
|
+
status: "complete"
|
|
557
|
+
};
|
|
558
|
+
progressBar.update(completedResources.size, resourceStatus);
|
|
559
|
+
} else {
|
|
560
|
+
resourceProgress(stackName, logicalId, resourceType, "complete");
|
|
561
|
+
}
|
|
562
|
+
} else if (status.includes("FAILED") || status.includes("ROLLBACK")) {
|
|
563
|
+
inProgressResources.delete(logicalId);
|
|
564
|
+
if (progressBar) {
|
|
565
|
+
const resourceStatus = {
|
|
566
|
+
logicalId,
|
|
567
|
+
resourceType,
|
|
568
|
+
status: "failed",
|
|
569
|
+
reason: event.ResourceStatusReason
|
|
570
|
+
};
|
|
571
|
+
progressBar.update(completedResources.size, resourceStatus);
|
|
572
|
+
} else {
|
|
573
|
+
resourceProgress(stackName, logicalId, resourceType, "failed", event.ResourceStatusReason);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const currentStatus = stack.StackStatus || "";
|
|
579
|
+
if (TERMINAL_STATES.has(currentStatus)) {
|
|
580
|
+
if (progressBar) {
|
|
581
|
+
progressBar.finish();
|
|
582
|
+
}
|
|
583
|
+
if (SUCCESS_STATES.has(currentStatus)) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
throw new Error(`Stack operation failed with status: ${currentStatus}`);
|
|
587
|
+
}
|
|
588
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
589
|
+
}
|
|
590
|
+
} catch (error2) {
|
|
591
|
+
if (progressBar) {
|
|
592
|
+
progressBar.finish();
|
|
593
|
+
}
|
|
594
|
+
throw error2;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
364
597
|
async function deployStack(options) {
|
|
365
|
-
const { stackName, template, accountId, region, credentials } = options;
|
|
598
|
+
const { stackName, template, accountId, region, credentials, showProgress = true } = options;
|
|
366
599
|
const client = new CloudFormationClient({
|
|
367
600
|
credentials,
|
|
368
601
|
region
|
|
369
602
|
});
|
|
370
603
|
const templateBody = JSON.stringify(template);
|
|
604
|
+
const resourceCount = Object.keys(template.Resources || {}).length;
|
|
371
605
|
try {
|
|
372
606
|
const stackStatus = await getStackStatus(stackName, credentials, region);
|
|
373
607
|
if (stackStatus.exists) {
|
|
374
608
|
verbose(`Stack ${stackName} exists, updating...`);
|
|
375
|
-
await updateStack(client, stackName, templateBody, accountId);
|
|
609
|
+
await updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
|
|
376
610
|
} else {
|
|
377
611
|
verbose(`Stack ${stackName} does not exist, creating...`);
|
|
378
|
-
await createStack(client, stackName, templateBody, accountId);
|
|
612
|
+
await createStack(client, stackName, templateBody, accountId, resourceCount, showProgress);
|
|
379
613
|
}
|
|
380
614
|
} catch (error2) {
|
|
381
615
|
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
@@ -386,7 +620,8 @@ async function deployStack(options) {
|
|
|
386
620
|
throw new CloudFormationError(stackName, accountId, errorMessage);
|
|
387
621
|
}
|
|
388
622
|
}
|
|
389
|
-
async function createStack(client, stackName, templateBody, accountId) {
|
|
623
|
+
async function createStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
|
|
624
|
+
const operationStartTime = /* @__PURE__ */ new Date();
|
|
390
625
|
await client.send(
|
|
391
626
|
new CreateStackCommand({
|
|
392
627
|
StackName: stackName,
|
|
@@ -398,14 +633,12 @@ async function createStack(client, stackName, templateBody, accountId) {
|
|
|
398
633
|
]
|
|
399
634
|
})
|
|
400
635
|
);
|
|
401
|
-
|
|
402
|
-
await
|
|
403
|
-
{ client, maxWaitTime: 600 },
|
|
404
|
-
{ StackName: stackName }
|
|
405
|
-
);
|
|
636
|
+
info(`Creating stack ${stackName}...`);
|
|
637
|
+
await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
|
|
406
638
|
success(`Stack ${stackName} created successfully in account ${accountId}`);
|
|
407
639
|
}
|
|
408
|
-
async function updateStack(client, stackName, templateBody, accountId) {
|
|
640
|
+
async function updateStack(client, stackName, templateBody, accountId, resourceCount, showProgress = true) {
|
|
641
|
+
const operationStartTime = /* @__PURE__ */ new Date();
|
|
409
642
|
await client.send(
|
|
410
643
|
new UpdateStackCommand({
|
|
411
644
|
StackName: stackName,
|
|
@@ -413,11 +646,8 @@ async function updateStack(client, stackName, templateBody, accountId) {
|
|
|
413
646
|
Capabilities: ["CAPABILITY_NAMED_IAM"]
|
|
414
647
|
})
|
|
415
648
|
);
|
|
416
|
-
|
|
417
|
-
await
|
|
418
|
-
{ client, maxWaitTime: 600 },
|
|
419
|
-
{ StackName: stackName }
|
|
420
|
-
);
|
|
649
|
+
info(`Updating stack ${stackName}...`);
|
|
650
|
+
await waitForStackWithProgress(client, stackName, operationStartTime, resourceCount, 600, showProgress);
|
|
421
651
|
success(`Stack ${stackName} updated successfully in account ${accountId}`);
|
|
422
652
|
}
|
|
423
653
|
async function readExistingStack(stackName, accountId, region, credentials) {
|
|
@@ -473,49 +703,6 @@ async function readExistingStack(stackName, accountId, region, credentials) {
|
|
|
473
703
|
}
|
|
474
704
|
}
|
|
475
705
|
|
|
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
706
|
// src/auth/browser-auth.ts
|
|
520
707
|
import express from "express";
|
|
521
708
|
import open from "open";
|
|
@@ -1154,6 +1341,16 @@ function getArtifactId(artifact) {
|
|
|
1154
1341
|
return artifact.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1155
1342
|
}
|
|
1156
1343
|
|
|
1344
|
+
// src/aws/oidc-provider.ts
|
|
1345
|
+
import {
|
|
1346
|
+
IAMClient,
|
|
1347
|
+
GetOpenIDConnectProviderCommand,
|
|
1348
|
+
ListOpenIDConnectProvidersCommand
|
|
1349
|
+
} from "@aws-sdk/client-iam";
|
|
1350
|
+
function getOidcThumbprint() {
|
|
1351
|
+
return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1157
1354
|
// src/templates/common.ts
|
|
1158
1355
|
var STANDARD_TAGS = [
|
|
1159
1356
|
{ Key: "CreatedBy", Value: "DevRamps" },
|
|
@@ -1366,6 +1563,9 @@ function getPipelineStackName(pipelineSlug) {
|
|
|
1366
1563
|
function getStageStackName(pipelineSlug, stageName) {
|
|
1367
1564
|
return truncateName(`DevRamps-${pipelineSlug}-${stageName}-Stage`, CF_STACK_MAX_LENGTH);
|
|
1368
1565
|
}
|
|
1566
|
+
function getAccountStackName() {
|
|
1567
|
+
return "DevRamps-Account-Bootstrap";
|
|
1568
|
+
}
|
|
1369
1569
|
function getKmsKeyAlias(orgSlug) {
|
|
1370
1570
|
return `alias/devramps-${normalizeName(orgSlug)}`;
|
|
1371
1571
|
}
|
|
@@ -1821,6 +2021,21 @@ function generatePipelineStackTemplate(options) {
|
|
|
1821
2021
|
return template;
|
|
1822
2022
|
}
|
|
1823
2023
|
|
|
2024
|
+
// src/templates/account-stack.ts
|
|
2025
|
+
function generateAccountStackTemplate() {
|
|
2026
|
+
const template = createBaseTemplate(
|
|
2027
|
+
"DevRamps Account Bootstrap Stack - Creates OIDC provider for the account"
|
|
2028
|
+
);
|
|
2029
|
+
addOidcProviderResource(template, false);
|
|
2030
|
+
template.Outputs = {
|
|
2031
|
+
OIDCProviderArn: {
|
|
2032
|
+
Description: "ARN of the OIDC provider",
|
|
2033
|
+
Value: { "Fn::GetAtt": ["DevRampsOIDCProvider", "Arn"] }
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
return template;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1824
2039
|
// src/permissions/eks-deploy.ts
|
|
1825
2040
|
var EKS_DEPLOY_PERMISSIONS = {
|
|
1826
2041
|
actions: [
|
|
@@ -2008,7 +2223,6 @@ function generateStageStackTemplate(options) {
|
|
|
2008
2223
|
const template = createBaseTemplate(
|
|
2009
2224
|
`DevRamps Stage Stack for ${pipelineSlug}/${stageName}`
|
|
2010
2225
|
);
|
|
2011
|
-
addOidcProviderResource(template, true);
|
|
2012
2226
|
const roleName = generateStageRoleName(pipelineSlug, stageName);
|
|
2013
2227
|
const trustPolicy = buildStageTrustPolicy(accountId, orgSlug, pipelineSlug, stageName);
|
|
2014
2228
|
const policies = buildStagePolicies(steps, additionalPolicies);
|
|
@@ -2054,6 +2268,7 @@ function generateStageStackTemplate(options) {
|
|
|
2054
2268
|
);
|
|
2055
2269
|
s3Outputs[artifact.name] = { resourceId };
|
|
2056
2270
|
}
|
|
2271
|
+
const oidcProviderArn = `arn:aws:iam::${accountId}:oidc-provider/${OIDC_PROVIDER_URL}`;
|
|
2057
2272
|
template.Outputs = {
|
|
2058
2273
|
StageRoleArn: {
|
|
2059
2274
|
Description: "ARN of the stage deployment role",
|
|
@@ -2065,8 +2280,8 @@ function generateStageStackTemplate(options) {
|
|
|
2065
2280
|
Value: { Ref: "StageDeploymentRole" }
|
|
2066
2281
|
},
|
|
2067
2282
|
OIDCProviderArn: {
|
|
2068
|
-
Description: "ARN of the OIDC provider",
|
|
2069
|
-
Value:
|
|
2283
|
+
Description: "ARN of the OIDC provider (created by Account Bootstrap stack)",
|
|
2284
|
+
Value: oidcProviderArn
|
|
2070
2285
|
},
|
|
2071
2286
|
PipelineSlug: {
|
|
2072
2287
|
Description: "Pipeline slug",
|
|
@@ -2286,7 +2501,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2286
2501
|
}
|
|
2287
2502
|
const orgStackName = getOrgStackName(orgSlug);
|
|
2288
2503
|
const orgStack = {
|
|
2289
|
-
stackType: "Org"
|
|
2504
|
+
stackType: "Org" /* ORG */,
|
|
2290
2505
|
stackName: orgStackName,
|
|
2291
2506
|
accountId: cicdAccountId,
|
|
2292
2507
|
region: cicdRegion,
|
|
@@ -2300,7 +2515,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2300
2515
|
const filteredArtifacts = filterArtifactsForPipelineStack(artifacts);
|
|
2301
2516
|
const stackName = getPipelineStackName(pipeline.slug);
|
|
2302
2517
|
pipelineStacks.push({
|
|
2303
|
-
stackType: "Pipeline"
|
|
2518
|
+
stackType: "Pipeline" /* PIPELINE */,
|
|
2304
2519
|
stackName,
|
|
2305
2520
|
accountId: cicdAccountId,
|
|
2306
2521
|
region: cicdRegion,
|
|
@@ -2310,6 +2525,38 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2310
2525
|
bundleArtifacts: filteredArtifacts.bundle
|
|
2311
2526
|
});
|
|
2312
2527
|
}
|
|
2528
|
+
const accountStacks = [];
|
|
2529
|
+
const accountStackName = getAccountStackName();
|
|
2530
|
+
const accountsWithStacks = /* @__PURE__ */ new Set();
|
|
2531
|
+
for (const pipeline of pipelines) {
|
|
2532
|
+
for (const stage of pipeline.stages) {
|
|
2533
|
+
if (accountsWithStacks.has(stage.account_id)) {
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
accountsWithStacks.add(stage.account_id);
|
|
2537
|
+
let accountCredentials;
|
|
2538
|
+
try {
|
|
2539
|
+
if (stage.account_id !== currentAccountId) {
|
|
2540
|
+
const assumed = await assumeRoleForAccount({
|
|
2541
|
+
targetAccountId: stage.account_id,
|
|
2542
|
+
currentAccountId,
|
|
2543
|
+
targetRoleName
|
|
2544
|
+
});
|
|
2545
|
+
accountCredentials = assumed?.credentials;
|
|
2546
|
+
}
|
|
2547
|
+
} catch {
|
|
2548
|
+
verbose(`Could not assume role in ${stage.account_id} for status check`);
|
|
2549
|
+
}
|
|
2550
|
+
accountStacks.push({
|
|
2551
|
+
stackType: "Account" /* ACCOUNT */,
|
|
2552
|
+
stackName: accountStackName,
|
|
2553
|
+
accountId: stage.account_id,
|
|
2554
|
+
region: cicdRegion,
|
|
2555
|
+
// Deploy in CI/CD region for consistency
|
|
2556
|
+
action: await determineStackAction(accountStackName, accountCredentials, cicdRegion)
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2313
2560
|
const stageStacks = [];
|
|
2314
2561
|
for (const pipeline of pipelines) {
|
|
2315
2562
|
const artifacts = pipelineArtifacts.get(pipeline.slug);
|
|
@@ -2329,7 +2576,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2329
2576
|
verbose(`Could not assume role in ${stage.account_id} for status check`);
|
|
2330
2577
|
}
|
|
2331
2578
|
stageStacks.push({
|
|
2332
|
-
stackType: "Stage"
|
|
2579
|
+
stackType: "Stage" /* STAGE */,
|
|
2333
2580
|
stackName,
|
|
2334
2581
|
accountId: stage.account_id,
|
|
2335
2582
|
region: stage.region,
|
|
@@ -2350,6 +2597,7 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2350
2597
|
cicdRegion,
|
|
2351
2598
|
orgStack,
|
|
2352
2599
|
pipelineStacks,
|
|
2600
|
+
accountStacks,
|
|
2353
2601
|
stageStacks
|
|
2354
2602
|
};
|
|
2355
2603
|
}
|
|
@@ -2368,79 +2616,126 @@ async function showDryRunPlan(plan) {
|
|
|
2368
2616
|
info(`CI/CD Account: ${plan.cicdAccountId}`);
|
|
2369
2617
|
info(`CI/CD Region: ${plan.cicdRegion}`);
|
|
2370
2618
|
newline();
|
|
2371
|
-
info("
|
|
2619
|
+
info("Org Stack:");
|
|
2372
2620
|
info(` ${plan.orgStack.action}: ${plan.orgStack.stackName}`);
|
|
2373
|
-
info(` Account: ${plan.orgStack.accountId}`);
|
|
2621
|
+
info(` Account: ${plan.orgStack.accountId}, Region: ${plan.orgStack.region}`);
|
|
2374
2622
|
info(` Target accounts with bucket access: ${plan.orgStack.targetAccountIds.length}`);
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2623
|
+
if (plan.pipelineStacks.length > 0) {
|
|
2624
|
+
newline();
|
|
2625
|
+
info("Pipeline Stacks:");
|
|
2626
|
+
for (const stack of plan.pipelineStacks) {
|
|
2627
|
+
info(` ${stack.action}: ${stack.stackName}`);
|
|
2628
|
+
info(` Account: ${stack.accountId}, Region: ${stack.region}`);
|
|
2629
|
+
info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
|
|
2630
|
+
}
|
|
2380
2631
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2632
|
+
if (plan.accountStacks.length > 0) {
|
|
2633
|
+
newline();
|
|
2634
|
+
info("Account Stacks:");
|
|
2635
|
+
for (const stack of plan.accountStacks) {
|
|
2636
|
+
info(` ${stack.action}: ${stack.stackName}`);
|
|
2637
|
+
info(` Account: ${stack.accountId}, Region: ${stack.region} (OIDC provider)`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (plan.stageStacks.length > 0) {
|
|
2641
|
+
newline();
|
|
2642
|
+
info("Stage Stacks:");
|
|
2643
|
+
for (const stack of plan.stageStacks) {
|
|
2644
|
+
info(` ${stack.action}: ${stack.stackName}`);
|
|
2645
|
+
info(` Account: ${stack.accountId}, Region: ${stack.region}`);
|
|
2646
|
+
info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
|
|
2647
|
+
}
|
|
2387
2648
|
}
|
|
2388
|
-
const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
|
|
2649
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
|
|
2389
2650
|
newline();
|
|
2390
|
-
info(`Total stacks to deploy: ${totalStacks}`);
|
|
2651
|
+
info(`Total stacks to deploy (in parallel): ${totalStacks}`);
|
|
2391
2652
|
}
|
|
2392
2653
|
async function confirmDeploymentPlan(plan) {
|
|
2393
|
-
const totalStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
|
|
2654
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
|
|
2394
2655
|
newline();
|
|
2395
2656
|
info(`About to deploy ${totalStacks} stack(s):`);
|
|
2396
2657
|
info(` - 1 Org stack (${plan.orgStack.action})`);
|
|
2397
2658
|
info(` - ${plan.pipelineStacks.length} Pipeline stack(s)`);
|
|
2659
|
+
info(` - ${plan.accountStacks.length} Account stack(s) (OIDC provider)`);
|
|
2398
2660
|
info(` - ${plan.stageStacks.length} Stage stack(s)`);
|
|
2399
2661
|
return confirmDeployment({
|
|
2400
2662
|
orgSlug: plan.orgSlug,
|
|
2401
2663
|
stacks: [
|
|
2402
2664
|
{ ...plan.orgStack, pipelineSlug: "org", steps: [], additionalPoliciesCount: 0 },
|
|
2403
2665
|
...plan.pipelineStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 })),
|
|
2666
|
+
...plan.accountStacks.map((s) => ({ ...s, pipelineSlug: "account", steps: [], additionalPoliciesCount: 0 })),
|
|
2404
2667
|
...plan.stageStacks.map((s) => ({ ...s, steps: s.steps.map((st) => st.name), additionalPoliciesCount: s.additionalPolicies.length }))
|
|
2405
2668
|
]
|
|
2406
2669
|
});
|
|
2407
2670
|
}
|
|
2408
2671
|
async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, currentAccountId, options) {
|
|
2409
2672
|
const results = { success: 0, failed: 0 };
|
|
2673
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
|
|
2410
2674
|
newline();
|
|
2411
|
-
header("
|
|
2412
|
-
|
|
2413
|
-
await deployOrgStack(plan, pipelines, authData, currentAccountId, options);
|
|
2414
|
-
results.success++;
|
|
2415
|
-
success("Org stack deployed successfully");
|
|
2416
|
-
} catch (error2) {
|
|
2417
|
-
results.failed++;
|
|
2418
|
-
error(`Org stack failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
2419
|
-
throw error2;
|
|
2420
|
-
}
|
|
2675
|
+
header("Deploying All Stacks");
|
|
2676
|
+
info(`Deploying ${totalStacks} stack(s) in parallel...`);
|
|
2421
2677
|
newline();
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2678
|
+
const orgPromise = (async () => {
|
|
2679
|
+
try {
|
|
2680
|
+
await deployOrgStack(plan, pipelines, authData, currentAccountId, options);
|
|
2681
|
+
return { stack: plan.orgStack.stackName, success: true };
|
|
2682
|
+
} catch (error2) {
|
|
2683
|
+
return {
|
|
2684
|
+
stack: plan.orgStack.stackName,
|
|
2685
|
+
success: false,
|
|
2686
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
})();
|
|
2690
|
+
const pipelinePromises = plan.pipelineStacks.map(async (stack) => {
|
|
2425
2691
|
try {
|
|
2426
2692
|
await deployPipelineStack(stack, authData, currentAccountId, options);
|
|
2427
|
-
|
|
2428
|
-
results.success++;
|
|
2693
|
+
return { stack: stack.stackName, success: true };
|
|
2429
2694
|
} catch (error2) {
|
|
2430
|
-
|
|
2431
|
-
|
|
2695
|
+
return {
|
|
2696
|
+
stack: stack.stackName,
|
|
2697
|
+
success: false,
|
|
2698
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
2699
|
+
};
|
|
2432
2700
|
}
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2701
|
+
});
|
|
2702
|
+
const accountPromises = plan.accountStacks.map(async (stack) => {
|
|
2703
|
+
try {
|
|
2704
|
+
await deployAccountStack(stack, currentAccountId, options);
|
|
2705
|
+
return { stack: `${stack.stackName} (${stack.accountId})`, success: true };
|
|
2706
|
+
} catch (error2) {
|
|
2707
|
+
return {
|
|
2708
|
+
stack: `${stack.stackName} (${stack.accountId})`,
|
|
2709
|
+
success: false,
|
|
2710
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
const stagePromises = plan.stageStacks.map(async (stack) => {
|
|
2438
2715
|
try {
|
|
2439
2716
|
await deployStageStack(stack, authData, currentAccountId, options);
|
|
2440
|
-
|
|
2441
|
-
results.success++;
|
|
2717
|
+
return { stack: stack.stackName, success: true };
|
|
2442
2718
|
} catch (error2) {
|
|
2443
|
-
|
|
2719
|
+
return {
|
|
2720
|
+
stack: stack.stackName,
|
|
2721
|
+
success: false,
|
|
2722
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2726
|
+
const allResults = await Promise.all([
|
|
2727
|
+
orgPromise,
|
|
2728
|
+
...pipelinePromises,
|
|
2729
|
+
...accountPromises,
|
|
2730
|
+
...stagePromises
|
|
2731
|
+
]);
|
|
2732
|
+
newline();
|
|
2733
|
+
for (const result of allResults) {
|
|
2734
|
+
if (result.success) {
|
|
2735
|
+
success(`${result.stack} deployed`);
|
|
2736
|
+
results.success++;
|
|
2737
|
+
} else {
|
|
2738
|
+
error(`${result.stack} failed: ${result.error}`);
|
|
2444
2739
|
results.failed++;
|
|
2445
2740
|
}
|
|
2446
2741
|
}
|
|
@@ -2489,7 +2784,9 @@ async function deployOrgStack(plan, pipelines, authData, currentAccountId, optio
|
|
|
2489
2784
|
template,
|
|
2490
2785
|
accountId: cicdAccountId,
|
|
2491
2786
|
region: cicdRegion,
|
|
2492
|
-
credentials
|
|
2787
|
+
credentials,
|
|
2788
|
+
showProgress: false
|
|
2789
|
+
// Disable progress bar for parallel deployment
|
|
2493
2790
|
};
|
|
2494
2791
|
await previewStackChanges(deployOptions);
|
|
2495
2792
|
await deployStack(deployOptions);
|
|
@@ -2512,7 +2809,28 @@ async function deployPipelineStack(stack, authData, currentAccountId, options) {
|
|
|
2512
2809
|
template,
|
|
2513
2810
|
accountId: cicdAccountId,
|
|
2514
2811
|
region: cicdRegion,
|
|
2515
|
-
credentials
|
|
2812
|
+
credentials,
|
|
2813
|
+
showProgress: false
|
|
2814
|
+
// Disable progress bar for parallel deployment
|
|
2815
|
+
};
|
|
2816
|
+
await previewStackChanges(deployOptions);
|
|
2817
|
+
await deployStack(deployOptions);
|
|
2818
|
+
}
|
|
2819
|
+
async function deployAccountStack(stack, currentAccountId, options) {
|
|
2820
|
+
const credentials = stack.accountId !== currentAccountId ? (await assumeRoleForAccount({
|
|
2821
|
+
targetAccountId: stack.accountId,
|
|
2822
|
+
currentAccountId,
|
|
2823
|
+
targetRoleName: options.targetAccountRoleName
|
|
2824
|
+
}))?.credentials : void 0;
|
|
2825
|
+
const template = generateAccountStackTemplate();
|
|
2826
|
+
const deployOptions = {
|
|
2827
|
+
stackName: stack.stackName,
|
|
2828
|
+
template,
|
|
2829
|
+
accountId: stack.accountId,
|
|
2830
|
+
region: stack.region,
|
|
2831
|
+
credentials,
|
|
2832
|
+
showProgress: false
|
|
2833
|
+
// Disable progress bar for parallel deployment
|
|
2516
2834
|
};
|
|
2517
2835
|
await previewStackChanges(deployOptions);
|
|
2518
2836
|
await deployStack(deployOptions);
|
|
@@ -2523,8 +2841,6 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
|
|
|
2523
2841
|
currentAccountId,
|
|
2524
2842
|
targetRoleName: options.targetAccountRoleName
|
|
2525
2843
|
}))?.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
2844
|
const template = generateStageStackTemplate({
|
|
2529
2845
|
pipelineSlug: stack.pipelineSlug,
|
|
2530
2846
|
stageName: stack.stageName,
|
|
@@ -2540,7 +2856,9 @@ async function deployStageStack(stack, authData, currentAccountId, options) {
|
|
|
2540
2856
|
template,
|
|
2541
2857
|
accountId: stack.accountId,
|
|
2542
2858
|
region: stack.region,
|
|
2543
|
-
credentials
|
|
2859
|
+
credentials,
|
|
2860
|
+
showProgress: false
|
|
2861
|
+
// Disable progress bar for parallel deployment
|
|
2544
2862
|
};
|
|
2545
2863
|
await previewStackChanges(deployOptions);
|
|
2546
2864
|
await deployStack(deployOptions);
|