@devramps/cli 0.1.20 → 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.
Files changed (2) hide show
  1. package/dist/index.js +227 -34
  2. 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
  }
@@ -2530,6 +2558,74 @@ function buildStagePolicies(steps, additionalPolicies) {
2530
2558
  return policies;
2531
2559
  }
2532
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
+
2533
2629
  // src/merge/index.ts
2534
2630
  var MERGE_STRATEGIES = /* @__PURE__ */ new Map();
2535
2631
  var bucketPolicyStrategy = new BucketPolicyMergeStrategy();
@@ -2718,33 +2814,50 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2718
2814
  region: cicdRegion,
2719
2815
  action: await determineStackAction(accountStackName, cicdCredentials, cicdRegion)
2720
2816
  });
2817
+ const importSourceAccountsByPipeline = /* @__PURE__ */ new Map();
2721
2818
  for (const pipeline of pipelines) {
2722
- for (const stage of pipeline.stages) {
2723
- if (accountsWithStacks.has(stage.account_id)) {
2724
- continue;
2725
- }
2726
- accountsWithStacks.add(stage.account_id);
2727
- let accountCredentials;
2728
- try {
2729
- if (stage.account_id !== currentAccountId) {
2730
- const assumed = await assumeRoleForAccount({
2731
- targetAccountId: stage.account_id,
2732
- currentAccountId,
2733
- targetRoleName
2734
- });
2735
- accountCredentials = assumed?.credentials;
2736
- }
2737
- } catch {
2738
- verbose(`Could not assume role in ${stage.account_id} for status check`);
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;
2739
2840
  }
2740
- accountStacks.push({
2741
- stackType: "Account" /* ACCOUNT */,
2742
- stackName: accountStackName,
2743
- accountId: stage.account_id,
2744
- region: cicdRegion,
2745
- // Deploy in CI/CD region for consistency
2746
- action: await determineStackAction(accountStackName, accountCredentials, cicdRegion)
2747
- });
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);
2748
2861
  }
2749
2862
  }
2750
2863
  const stageStacks = [];
@@ -2781,6 +2894,35 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2781
2894
  });
2782
2895
  }
2783
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
+ }
2784
2926
  return {
2785
2927
  orgSlug,
2786
2928
  cicdAccountId,
@@ -2788,7 +2930,8 @@ async function buildDeploymentPlan(pipelines, pipelineArtifacts, authData, curre
2788
2930
  orgStack,
2789
2931
  pipelineStacks,
2790
2932
  accountStacks,
2791
- stageStacks
2933
+ stageStacks,
2934
+ importStacks
2792
2935
  };
2793
2936
  }
2794
2937
  async function determineStackAction(stackName, credentials, region) {
@@ -2836,34 +2979,46 @@ async function showDryRunPlan(plan) {
2836
2979
  info(` ECR repos: ${stack.dockerArtifacts.length}, S3 buckets: ${stack.bundleArtifacts.length}`);
2837
2980
  }
2838
2981
  }
2839
- const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
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;
2840
2992
  newline();
2841
2993
  info(`Total stacks to deploy: ${totalStacks}`);
2842
2994
  info(` Phase 1: ${plan.accountStacks.length} Account stack(s) (deployed first)`);
2843
- info(` Phase 2: ${1 + plan.pipelineStacks.length + plan.stageStacks.length} Org/Pipeline/Stage stack(s) (deployed in parallel after Phase 1)`);
2995
+ info(` Phase 2: ${phase2Stacks} Org/Pipeline/Stage/Import stack(s) (deployed in parallel after Phase 1)`);
2844
2996
  }
2845
2997
  async function confirmDeploymentPlan(plan) {
2846
- 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;
2847
2999
  newline();
2848
3000
  info(`About to deploy ${totalStacks} stack(s):`);
2849
3001
  info(` - 1 Org stack (${plan.orgStack.action})`);
2850
3002
  info(` - ${plan.pipelineStacks.length} Pipeline stack(s)`);
2851
3003
  info(` - ${plan.accountStacks.length} Account stack(s) (OIDC provider)`);
2852
3004
  info(` - ${plan.stageStacks.length} Stage stack(s)`);
3005
+ if (plan.importStacks.length > 0) {
3006
+ info(` - ${plan.importStacks.length} Import stack(s)`);
3007
+ }
2853
3008
  return confirmDeployment({
2854
3009
  orgSlug: plan.orgSlug,
2855
3010
  stacks: [
2856
3011
  { ...plan.orgStack, pipelineSlug: "org", steps: [], additionalPoliciesCount: 0 },
2857
3012
  ...plan.pipelineStacks.map((s) => ({ ...s, steps: [], additionalPoliciesCount: 0 })),
2858
3013
  ...plan.accountStacks.map((s) => ({ ...s, pipelineSlug: "account", steps: [], additionalPoliciesCount: 0 })),
2859
- ...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 }))
2860
3016
  ]
2861
3017
  });
2862
3018
  }
2863
3019
  async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, currentAccountId, options, oidcProviderUrl) {
2864
3020
  const results = { success: 0, failed: 0 };
2865
- const totalStacks = 1 + plan.pipelineStacks.length + plan.accountStacks.length + plan.stageStacks.length;
2866
- const remainingStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length;
3021
+ const remainingStacks = 1 + plan.pipelineStacks.length + plan.stageStacks.length + plan.importStacks.length;
2867
3022
  newline();
2868
3023
  header("Phase 1: Deploying Account Bootstrap Stacks");
2869
3024
  info(`Deploying ${plan.accountStacks.length} account stack(s) in parallel...`);
@@ -2905,7 +3060,7 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2905
3060
  process.exit(1);
2906
3061
  }
2907
3062
  newline();
2908
- header("Phase 2: Deploying Org, Pipeline, and Stage Stacks");
3063
+ header("Phase 2: Deploying Org, Pipeline, Stage, and Import Stacks");
2909
3064
  info(`Deploying ${remainingStacks} stack(s) in parallel...`);
2910
3065
  newline();
2911
3066
  const mainProgress = getMultiStackProgress();
@@ -2918,6 +3073,9 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2918
3073
  const resourceCount = stack.dockerArtifacts.length + stack.bundleArtifacts.length + 2;
2919
3074
  mainProgress.addStack(stack.stackName, "stage", stack.accountId, stack.region, resourceCount);
2920
3075
  }
3076
+ for (const stack of plan.importStacks) {
3077
+ mainProgress.addStack(stack.stackName, "import", stack.accountId, stack.region, 1);
3078
+ }
2921
3079
  mainProgress.start();
2922
3080
  const orgPromise = (async () => {
2923
3081
  try {
@@ -2955,10 +3113,23 @@ async function executeDeployment(plan, pipelines, pipelineArtifacts, authData, c
2955
3113
  };
2956
3114
  }
2957
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
+ });
2958
3128
  const mainResults = await Promise.all([
2959
3129
  orgPromise,
2960
3130
  ...pipelinePromises,
2961
- ...stagePromises
3131
+ ...stagePromises,
3132
+ ...importPromises
2962
3133
  ]);
2963
3134
  clearMultiStackProgress();
2964
3135
  newline();
@@ -3091,6 +3262,28 @@ async function deployStageStack(stack, authData, currentAccountId, options, oidc
3091
3262
  await previewStackChanges(deployOptions);
3092
3263
  await deployStack(deployOptions);
3093
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
+ }
3094
3287
 
3095
3288
  // src/index.ts
3096
3289
  program.name("devramps").description("DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines").version("0.1.0");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devramps/cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "DevRamps CLI - Bootstrap AWS infrastructure for CI/CD pipelines",
5
5
  "main": "dist/index.js",
6
6
  "bin": {