@devramps/cli 0.1.19 → 0.1.21
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 +230 -35
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -237,6 +237,8 @@ var MultiStackProgress = class {
|
|
|
237
237
|
return "ACCT";
|
|
238
238
|
case "stage":
|
|
239
239
|
return "STAGE";
|
|
240
|
+
case "import":
|
|
241
|
+
return "IMPORT";
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
/**
|
|
@@ -1481,6 +1483,26 @@ function getArtifactId(artifact) {
|
|
|
1481
1483
|
}
|
|
1482
1484
|
return artifact.name.toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1483
1485
|
}
|
|
1486
|
+
function extractImportSourceAccounts(artifacts) {
|
|
1487
|
+
const accountIds = /* @__PURE__ */ new Set();
|
|
1488
|
+
for (const artifact of artifacts.docker) {
|
|
1489
|
+
if (artifact.type !== "DEVRAMPS:DOCKER:IMPORT") continue;
|
|
1490
|
+
const url = artifact.params?.source_image_url;
|
|
1491
|
+
if (!url) continue;
|
|
1492
|
+
const match = url.match(/^(\d+)\.dkr\.ecr\./);
|
|
1493
|
+
if (match) {
|
|
1494
|
+
accountIds.add(match[1]);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
for (const artifact of artifacts.bundle) {
|
|
1498
|
+
if (artifact.type !== "DEVRAMPS:BUNDLE:IMPORT") continue;
|
|
1499
|
+
const account = artifact.params?.source_account;
|
|
1500
|
+
if (account && typeof account === "string") {
|
|
1501
|
+
accountIds.add(account);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return Array.from(accountIds).map((accountId) => ({ accountId }));
|
|
1505
|
+
}
|
|
1484
1506
|
|
|
1485
1507
|
// src/aws/oidc-provider.ts
|
|
1486
1508
|
import {
|
|
@@ -1697,6 +1719,12 @@ function getStageStackName(pipelineSlug, stageName) {
|
|
|
1697
1719
|
function getAccountStackName() {
|
|
1698
1720
|
return "DevRamps-Account-Bootstrap";
|
|
1699
1721
|
}
|
|
1722
|
+
function getImportStackName(pipelineSlug) {
|
|
1723
|
+
return truncateName(`DevRamps-${pipelineSlug}-Import`, CF_STACK_MAX_LENGTH);
|
|
1724
|
+
}
|
|
1725
|
+
function generateImportRoleName(pipelineSlug) {
|
|
1726
|
+
return truncateName(`DevRamps-${pipelineSlug}-ImportRole`, IAM_ROLE_MAX_LENGTH);
|
|
1727
|
+
}
|
|
1700
1728
|
function getKmsKeyAlias(orgSlug) {
|
|
1701
1729
|
return `alias/devramps-${normalizeName(orgSlug)}`;
|
|
1702
1730
|
}
|
|
@@ -2071,7 +2099,9 @@ function buildOrgRolePolicies(orgSlug) {
|
|
|
2071
2099
|
Effect: "Allow",
|
|
2072
2100
|
Action: [
|
|
2073
2101
|
"s3:ListBucket",
|
|
2074
|
-
"s3:GetBucketLocation"
|
|
2102
|
+
"s3:GetBucketLocation",
|
|
2103
|
+
"s3:GetObject",
|
|
2104
|
+
"s3:PutObject"
|
|
2075
2105
|
],
|
|
2076
2106
|
Resource: "*"
|
|
2077
2107
|
},
|
|
@@ -2528,6 +2558,74 @@ function buildStagePolicies(steps, additionalPolicies) {
|
|
|
2528
2558
|
return policies;
|
|
2529
2559
|
}
|
|
2530
2560
|
|
|
2561
|
+
// src/templates/import-stack.ts
|
|
2562
|
+
function generateImportStackTemplate(options) {
|
|
2563
|
+
const { pipelineSlug, orgSlug, accountId, oidcProviderUrl } = options;
|
|
2564
|
+
const template = createBaseTemplate(
|
|
2565
|
+
`DevRamps Import Stack for ${pipelineSlug} - grants read access for artifact imports`
|
|
2566
|
+
);
|
|
2567
|
+
const roleName = generateImportRoleName(pipelineSlug);
|
|
2568
|
+
const trustPolicy = buildOidcTrustPolicy(accountId, `org:${orgSlug}/cicd`, oidcProviderUrl);
|
|
2569
|
+
const policies = buildImportRolePolicies();
|
|
2570
|
+
template.Resources.ImportRole = createIamRoleResource(
|
|
2571
|
+
roleName,
|
|
2572
|
+
trustPolicy,
|
|
2573
|
+
policies,
|
|
2574
|
+
[
|
|
2575
|
+
{ Key: "Pipeline", Value: pipelineSlug },
|
|
2576
|
+
{ Key: "Organization", Value: orgSlug }
|
|
2577
|
+
]
|
|
2578
|
+
);
|
|
2579
|
+
template.Outputs = {
|
|
2580
|
+
ImportRoleArn: {
|
|
2581
|
+
Description: "ARN of the import role",
|
|
2582
|
+
Value: { "Fn::GetAtt": ["ImportRole", "Arn"] }
|
|
2583
|
+
},
|
|
2584
|
+
ImportRoleName: {
|
|
2585
|
+
Description: "Name of the import role",
|
|
2586
|
+
Value: { Ref: "ImportRole" }
|
|
2587
|
+
},
|
|
2588
|
+
PipelineSlug: {
|
|
2589
|
+
Description: "Pipeline slug",
|
|
2590
|
+
Value: pipelineSlug
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
return template;
|
|
2594
|
+
}
|
|
2595
|
+
function buildImportRolePolicies() {
|
|
2596
|
+
return [
|
|
2597
|
+
{
|
|
2598
|
+
PolicyName: "DevRampsImportPolicy",
|
|
2599
|
+
PolicyDocument: {
|
|
2600
|
+
Version: "2012-10-17",
|
|
2601
|
+
Statement: [
|
|
2602
|
+
{
|
|
2603
|
+
Sid: "AllowECRRead",
|
|
2604
|
+
Effect: "Allow",
|
|
2605
|
+
Action: [
|
|
2606
|
+
"ecr:GetAuthorizationToken",
|
|
2607
|
+
"ecr:DescribeImages",
|
|
2608
|
+
"ecr:BatchGetImage",
|
|
2609
|
+
"ecr:GetDownloadUrlForLayer",
|
|
2610
|
+
"ecr:BatchCheckLayerAvailability"
|
|
2611
|
+
],
|
|
2612
|
+
Resource: "*"
|
|
2613
|
+
},
|
|
2614
|
+
{
|
|
2615
|
+
Sid: "AllowS3Read",
|
|
2616
|
+
Effect: "Allow",
|
|
2617
|
+
Action: [
|
|
2618
|
+
"s3:GetObject",
|
|
2619
|
+
"s3:HeadObject"
|
|
2620
|
+
],
|
|
2621
|
+
Resource: "*"
|
|
2622
|
+
}
|
|
2623
|
+
]
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
];
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2531
2629
|
// src/merge/index.ts
|
|
2532
2630
|
var MERGE_STRATEGIES = /* @__PURE__ */ new Map();
|
|
2533
2631
|
var bucketPolicyStrategy = new BucketPolicyMergeStrategy();
|
|
@@ -2716,33 +2814,50 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2716
2814
|
region: cicdRegion,
|
|
2717
2815
|
action: await determineStackAction(accountStackName, cicdCredentials, cicdRegion)
|
|
2718
2816
|
});
|
|
2817
|
+
const importSourceAccountsByPipeline = /* @__PURE__ */ new Map();
|
|
2719
2818
|
for (const pipeline of pipelines) {
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2819
|
+
const artifacts = pipelineArtifacts.get(pipeline.slug);
|
|
2820
|
+
const importSources = extractImportSourceAccounts(artifacts);
|
|
2821
|
+
if (importSources.length > 0) {
|
|
2822
|
+
importSourceAccountsByPipeline.set(
|
|
2823
|
+
pipeline.slug,
|
|
2824
|
+
importSources.map((s) => s.accountId)
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
const addAccountStackIfNew = async (accountId) => {
|
|
2829
|
+
if (accountsWithStacks.has(accountId)) return;
|
|
2830
|
+
accountsWithStacks.add(accountId);
|
|
2831
|
+
let accountCredentials;
|
|
2832
|
+
try {
|
|
2833
|
+
if (accountId !== currentAccountId) {
|
|
2834
|
+
const assumed = await assumeRoleForAccount({
|
|
2835
|
+
targetAccountId: accountId,
|
|
2836
|
+
currentAccountId,
|
|
2837
|
+
targetRoleName
|
|
2838
|
+
});
|
|
2839
|
+
accountCredentials = assumed?.credentials;
|
|
2737
2840
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2841
|
+
} catch {
|
|
2842
|
+
verbose(`Could not assume role in ${accountId} for status check`);
|
|
2843
|
+
}
|
|
2844
|
+
accountStacks.push({
|
|
2845
|
+
stackType: "Account" /* ACCOUNT */,
|
|
2846
|
+
stackName: accountStackName,
|
|
2847
|
+
accountId,
|
|
2848
|
+
region: cicdRegion,
|
|
2849
|
+
// Deploy in CI/CD region for consistency
|
|
2850
|
+
action: await determineStackAction(accountStackName, accountCredentials, cicdRegion)
|
|
2851
|
+
});
|
|
2852
|
+
};
|
|
2853
|
+
for (const pipeline of pipelines) {
|
|
2854
|
+
for (const stage of pipeline.stages) {
|
|
2855
|
+
await addAccountStackIfNew(stage.account_id);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
for (const [, sourceAccountIds] of importSourceAccountsByPipeline) {
|
|
2859
|
+
for (const accountId of sourceAccountIds) {
|
|
2860
|
+
await addAccountStackIfNew(accountId);
|
|
2746
2861
|
}
|
|
2747
2862
|
}
|
|
2748
2863
|
const stageStacks = [];
|
|
@@ -2779,6 +2894,35 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2779
2894
|
});
|
|
2780
2895
|
}
|
|
2781
2896
|
}
|
|
2897
|
+
const importStacks = [];
|
|
2898
|
+
for (const [pipelineSlug, sourceAccountIds] of importSourceAccountsByPipeline) {
|
|
2899
|
+
const importStackName = getImportStackName(pipelineSlug);
|
|
2900
|
+
for (const sourceAccountId of sourceAccountIds) {
|
|
2901
|
+
let importCredentials;
|
|
2902
|
+
try {
|
|
2903
|
+
if (sourceAccountId !== currentAccountId) {
|
|
2904
|
+
const assumed = await assumeRoleForAccount({
|
|
2905
|
+
targetAccountId: sourceAccountId,
|
|
2906
|
+
currentAccountId,
|
|
2907
|
+
targetRoleName
|
|
2908
|
+
});
|
|
2909
|
+
importCredentials = assumed?.credentials;
|
|
2910
|
+
}
|
|
2911
|
+
} catch {
|
|
2912
|
+
verbose(`Could not assume role in ${sourceAccountId} for status check`);
|
|
2913
|
+
}
|
|
2914
|
+
importStacks.push({
|
|
2915
|
+
stackType: "Import" /* IMPORT */,
|
|
2916
|
+
stackName: importStackName,
|
|
2917
|
+
accountId: sourceAccountId,
|
|
2918
|
+
region: cicdRegion,
|
|
2919
|
+
// Deploy in CI/CD region (IAM is global)
|
|
2920
|
+
action: await determineStackAction(importStackName, importCredentials, cicdRegion),
|
|
2921
|
+
pipelineSlug,
|
|
2922
|
+
orgSlug
|
|
2923
|
+
});
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2782
2926
|
return {
|
|
2783
2927
|
orgSlug,
|
|
2784
2928
|
cicdAccountId,
|
|
@@ -2786,7 +2930,8 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
|
|
|
2786
2930
|
orgStack,
|
|
2787
2931
|
pipelineStacks,
|
|
2788
2932
|
accountStacks,
|
|
2789
|
-
stageStacks
|
|
2933
|
+
stageStacks,
|
|
2934
|
+
importStacks
|
|
2790
2935
|
};
|
|
2791
2936
|
}
|
|
2792
2937
|
async function determineStackAction(stackName, credentials, region) {
|
|
@@ -2834,34 +2979,46 @@ async function showDryRunPlan(plan) {
|
|
|
2834
2979
|
info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
|
|
2835
2980
|
}
|
|
2836
2981
|
}
|
|
2837
|
-
|
|
2982
|
+
if (plan.importStacks.length > 0) {
|
|
2983
|
+
newline();
|
|
2984
|
+
info("Import Stacks:");
|
|
2985
|
+
for (const stack of plan.importStacks) {
|
|
2986
|
+
info(` ${stack.action}: ${stack.stackName}`);
|
|
2987
|
+
info(` Account: ${stack.accountId}, Region: ${stack.region} (import role)`);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length + plan.importStacks.length;
|
|
2991
|
+
const phase2Stacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length + plan.importStacks.length;
|
|
2838
2992
|
newline();
|
|
2839
2993
|
info(`Total stacks to deploy: ${totalStacks}`);
|
|
2840
2994
|
info(` Phase 1: ${plan.accountStacks.length} Account stack(s) (deployed first)`);
|
|
2841
|
-
info(` Phase 2: ${
|
|
2995
|
+
info(` Phase 2: ${phase2Stacks} Org/Pipeline/Stage/Import stack(s) (deployed in parallel after Phase 1)`);
|
|
2842
2996
|
}
|
|
2843
2997
|
async function confirmDeploymentPlan(plan) {
|
|
2844
|
-
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
|
|
2998
|
+
const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length + plan.importStacks.length;
|
|
2845
2999
|
newline();
|
|
2846
3000
|
info(`About to deploy ${totalStacks} stack(s):`);
|
|
2847
3001
|
info(` - 1 Org stack (${plan.orgStack.action})`);
|
|
2848
3002
|
info(` - ${plan.pipelineStacks.length} Pipeline stack(s)`);
|
|
2849
3003
|
info(` - ${plan.accountStacks.length} Account stack(s) (OIDC provider)`);
|
|
2850
3004
|
info(` - ${plan.stageStacks.length} Stage stack(s)`);
|
|
3005
|
+
if (plan.importStacks.length > 0) {
|
|
3006
|
+
info(` - ${plan.importStacks.length} Import stack(s)`);
|
|
3007
|
+
}
|
|
2851
3008
|
return confirmDeployment({
|
|
2852
3009
|
orgSlug: plan.orgSlug,
|
|
2853
3010
|
stacks: [
|
|
2854
3011
|
{ ...plan.orgStack, pipelineSlug: "org", steps: [], additionalPoliciesCount: 0 },
|
|
2855
3012
|
...plan.pipelineStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 })),
|
|
2856
3013
|
...plan.accountStacks.map((s) => ({ ...s, pipelineSlug: "account", steps: [], additionalPoliciesCount: 0 })),
|
|
2857
|
-
...plan.stageStacks.map((s) => ({ ...s, steps: s.steps.map((st) => st.name), additionalPoliciesCount: s.additionalPolicies.length }))
|
|
3014
|
+
...plan.stageStacks.map((s) => ({ ...s, steps: s.steps.map((st) => st.name), additionalPoliciesCount: s.additionalPolicies.length })),
|
|
3015
|
+
...plan.importStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 }))
|
|
2858
3016
|
]
|
|
2859
3017
|
});
|
|
2860
3018
|
}
|
|
2861
3019
|
async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, currentAccountId, options, oidcProviderUrl) {
|
|
2862
3020
|
const results = { success: 0, failed: 0 };
|
|
2863
|
-
const
|
|
2864
|
-
const remainingStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
|
|
3021
|
+
const remainingStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length + plan.importStacks.length;
|
|
2865
3022
|
newline();
|
|
2866
3023
|
header("Phase 1: Deploying Account Bootstrap Stacks");
|
|
2867
3024
|
info(`Deploying ${plan.accountStacks.length} account stack(s) in parallel...`);
|
|
@@ -2903,7 +3060,7 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
2903
3060
|
process.exit(1);
|
|
2904
3061
|
}
|
|
2905
3062
|
newline();
|
|
2906
|
-
header("Phase 2: Deploying Org, Pipeline, and
|
|
3063
|
+
header("Phase 2: Deploying Org, Pipeline, Stage, and Import Stacks");
|
|
2907
3064
|
info(`Deploying ${remainingStacks} stack(s) in parallel...`);
|
|
2908
3065
|
newline();
|
|
2909
3066
|
const mainProgress = getMultiStackProgress();
|
|
@@ -2916,6 +3073,9 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
2916
3073
|
const resourceCount = stack.dockerArtifacts.length + stack.bundleArtifacts.length + 2;
|
|
2917
3074
|
mainProgress.addStack(stack.stackName, "stage", stack.accountId, stack.region, resourceCount);
|
|
2918
3075
|
}
|
|
3076
|
+
for (const stack of plan.importStacks) {
|
|
3077
|
+
mainProgress.addStack(stack.stackName, "import", stack.accountId, stack.region, 1);
|
|
3078
|
+
}
|
|
2919
3079
|
mainProgress.start();
|
|
2920
3080
|
const orgPromise = (async () => {
|
|
2921
3081
|
try {
|
|
@@ -2953,10 +3113,23 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
|
|
|
2953
3113
|
};
|
|
2954
3114
|
}
|
|
2955
3115
|
});
|
|
3116
|
+
const importPromises = plan.importStacks.map(async (stack) => {
|
|
3117
|
+
try {
|
|
3118
|
+
await deployImportStack(stack, currentAccountId, options, oidcProviderUrl);
|
|
3119
|
+
return { stack: `${stack.stackName} (${stack.accountId})`, success: true };
|
|
3120
|
+
} catch (error2) {
|
|
3121
|
+
return {
|
|
3122
|
+
stack: `${stack.stackName} (${stack.accountId})`,
|
|
3123
|
+
success: false,
|
|
3124
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
});
|
|
2956
3128
|
const mainResults = await Promise.all([
|
|
2957
3129
|
orgPromise,
|
|
2958
3130
|
...pipelinePromises,
|
|
2959
|
-
...stagePromises
|
|
3131
|
+
...stagePromises,
|
|
3132
|
+
...importPromises
|
|
2960
3133
|
]);
|
|
2961
3134
|
clearMultiStackProgress();
|
|
2962
3135
|
newline();
|
|
@@ -3089,6 +3262,28 @@ async function deployStageStack(stack, authData, currentAccountId, options, oidc
|
|
|
3089
3262
|
await previewStackChanges(deployOptions);
|
|
3090
3263
|
await deployStack(deployOptions);
|
|
3091
3264
|
}
|
|
3265
|
+
async function deployImportStack(stack, currentAccountId, options, oidcProviderUrl) {
|
|
3266
|
+
const credentials = stack.accountId !== currentAccountId ? (await assumeRoleForAccount({
|
|
3267
|
+
targetAccountId: stack.accountId,
|
|
3268
|
+
currentAccountId,
|
|
3269
|
+
targetRoleName: options.targetAccountRoleName
|
|
3270
|
+
}))?.credentials : void 0;
|
|
3271
|
+
const template = generateImportStackTemplate({
|
|
3272
|
+
pipelineSlug: stack.pipelineSlug,
|
|
3273
|
+
orgSlug: stack.orgSlug,
|
|
3274
|
+
accountId: stack.accountId,
|
|
3275
|
+
oidcProviderUrl
|
|
3276
|
+
});
|
|
3277
|
+
const deployOptions = {
|
|
3278
|
+
stackName: stack.stackName,
|
|
3279
|
+
template,
|
|
3280
|
+
accountId: stack.accountId,
|
|
3281
|
+
region: stack.region,
|
|
3282
|
+
credentials
|
|
3283
|
+
};
|
|
3284
|
+
await previewStackChanges(deployOptions);
|
|
3285
|
+
await deployStack(deployOptions);
|
|
3286
|
+
}
|
|
3092
3287
|
|
|
3093
3288
|
// src/index.ts
|
|
3094
3289
|
program.name("devramps").description("DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines").version("0.1.0");
|