@go-to-k/cdkd 0.86.0 → 0.88.0

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/cli.js CHANGED
@@ -79270,7 +79270,8 @@ import {
79270
79270
  DescribeTypeCommand,
79271
79271
  DeleteChangeSetCommand,
79272
79272
  waitUntilChangeSetCreateComplete,
79273
- waitUntilStackImportComplete
79273
+ waitUntilStackImportComplete,
79274
+ waitUntilStackUpdateComplete as waitUntilStackUpdateComplete2
79274
79275
  } from "@aws-sdk/client-cloudformation";
79275
79276
  init_aws_clients();
79276
79277
  var NEVER_IMPORTABLE_TYPES = /* @__PURE__ */ new Set([
@@ -79457,39 +79458,106 @@ async function exportCommand(stackArg, options) {
79457
79458
  }
79458
79459
  }
79459
79460
  try {
79460
- const { plan, skipped } = await buildImportPlan(state, template, awsClients.cloudFormation);
79461
- if (skipped.length > 0) {
79461
+ const { phase1Imports, phase2Creates, blocked } = await buildImportPlan(
79462
+ state,
79463
+ template,
79464
+ awsClients.cloudFormation
79465
+ );
79466
+ if (blocked.length > 0) {
79467
+ logger.error("The following resources block migration:");
79468
+ for (const b of blocked) {
79469
+ logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
79470
+ }
79471
+ throw new Error(
79472
+ `${blocked.length} resource(s) block migration. Either destroy them first (cdkd destroy / cdkd state destroy cherry-picked), or remove them from the CDK app and re-synthesize.`
79473
+ );
79474
+ }
79475
+ if (phase2Creates.length > 0 && !options.includeNonImportable) {
79462
79476
  logger.error("The following resources cannot be imported into CloudFormation:");
79463
- for (const s of skipped) {
79464
- logger.error(` - ${s.logicalId} (${s.resourceType}): ${s.reason}`);
79477
+ for (const p of phase2Creates) {
79478
+ logger.error(` - ${p.logicalId} (${p.resourceType}): CFn cannot import this type`);
79465
79479
  }
79466
79480
  throw new Error(
79467
- `${skipped.length} resource(s) cannot be imported. CloudFormation IMPORT requires every template resource to map to an importable AWS resource. Either destroy these resources first (cdkd destroy / cdkd state destroy cherry-picked), or accept abandoning them by removing them from the CDK app and re-synthesizing.`
79481
+ `${phase2Creates.length} non-importable resource(s) detected (Custom::*). Pass --include-non-importable to run a 2-phase migration: phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones (re-invoking each Custom Resource's backing Lambda onCreate handler \u2014 make sure those are idempotent). Or destroy these resources first.`
79468
79482
  );
79469
79483
  }
79470
- if (plan.length === 0) {
79471
- logger.warn("No resources to import \u2014 cdkd state is empty.");
79484
+ if (phase1Imports.length === 0 && phase2Creates.length === 0) {
79485
+ logger.warn("No resources to migrate \u2014 cdkd state is empty.");
79472
79486
  return;
79473
79487
  }
79474
- printPlan(plan, cfnStackName);
79488
+ if (phase1Imports.length === 0) {
79489
+ throw new Error(
79490
+ "No importable resources in the template. CloudFormation IMPORT changeset requires at least one importable resource for phase 1."
79491
+ );
79492
+ }
79493
+ printPlan(phase1Imports, cfnStackName);
79494
+ if (phase2Creates.length > 0) {
79495
+ logger.info(`Phase 2 will CREATE ${phase2Creates.length} non-importable resource(s):`);
79496
+ for (const p of phase2Creates) {
79497
+ logger.info(` ${p.logicalId} (${p.resourceType})`);
79498
+ }
79499
+ logger.info("");
79500
+ }
79475
79501
  if (options.dryRun) {
79476
79502
  logger.info("--dry-run: no CloudFormation changeset will be created.");
79477
79503
  return;
79478
79504
  }
79479
79505
  if (!options.yes) {
79506
+ const phase2Note = phase2Creates.length > 0 ? ` Phase 2 will then CREATE ${phase2Creates.length} non-importable resource(s) (invoking each Custom Resource's onCreate handler).` : "";
79480
79507
  const ok = await confirmPrompt6(
79481
- `Create CloudFormation stack '${cfnStackName}' by importing ${plan.length} resource(s) from cdkd state '${resolvedStackName}' (${targetRegion})? AWS resources are unchanged. cdkd state for '${resolvedStackName}' will be deleted on success.`
79508
+ `Create CloudFormation stack '${cfnStackName}' by importing ${phase1Imports.length} resource(s) from cdkd state '${resolvedStackName}' (${targetRegion})?` + phase2Note + ` AWS resources are unchanged on import. cdkd state for '${resolvedStackName}' will be deleted on success.`
79482
79509
  );
79483
79510
  if (!ok) {
79484
79511
  logger.info("Migration cancelled. cdkd state and CloudFormation are unchanged.");
79485
79512
  return;
79486
79513
  }
79487
79514
  }
79488
- const filteredTemplate = filterTemplateForImport(template, plan);
79489
- await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, filteredTemplate, plan);
79515
+ const userParameters = parseParameterOverrides(options.parameter);
79516
+ const { parameters: cfnParameters, missing } = resolveTemplateParameters(
79517
+ template,
79518
+ userParameters
79519
+ );
79520
+ if (missing.length > 0) {
79521
+ throw new Error(
79522
+ `Template requires parameter(s) without defaults: ${missing.join(", ")}. Pass each one as --parameter Key=Value (or set a Default in the CDK code).`
79523
+ );
79524
+ }
79525
+ const phase1Template = filterTemplateForImport(template, phase1Imports);
79526
+ await executeImportChangeSet(
79527
+ awsClients.cloudFormation,
79528
+ cfnStackName,
79529
+ phase1Template,
79530
+ phase1Imports,
79531
+ cfnParameters
79532
+ );
79490
79533
  logger.info(
79491
- `\u2713 CloudFormation stack '${cfnStackName}' created via IMPORT. ${plan.length} resource(s) are now managed by CloudFormation.`
79534
+ `\u2713 Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`
79492
79535
  );
79536
+ if (phase2Creates.length > 0) {
79537
+ try {
79538
+ await executeUpdateChangeSet(
79539
+ awsClients.cloudFormation,
79540
+ cfnStackName,
79541
+ template,
79542
+ cfnParameters
79543
+ );
79544
+ logger.info(`\u2713 Phase 2: ${phase2Creates.length} non-importable resource(s) CREATEd.`);
79545
+ } catch (err) {
79546
+ const msg = err instanceof Error ? err.message : String(err);
79547
+ throw new Error(
79548
+ `Phase 1 (IMPORT) succeeded; phase 2 (UPDATE) failed: ${msg}
79549
+
79550
+ The CloudFormation stack '${cfnStackName}' now contains the imported resources but is missing the ${phase2Creates.length} non-importable resource(s). cdkd state is UNCHANGED so you can inspect what's in it, but DO NOT run \`cdkd deploy\` against this stack (the imported resources are now CFn-managed). To recover:
79551
+ 1. Fix the failure cause (typically an onCreate Lambda error).
79552
+ 2. Re-run the phase 2 UPDATE manually with the full synth template:
79553
+ aws cloudformation create-change-set --stack-name ${cfnStackName} \\
79554
+ --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\
79555
+ --template-body file://<full-template.json>
79556
+ 3. Once phase 2 succeeds, run: cdkd state orphan ${resolvedStackName}
79557
+ to clean up cdkd's stale state record.`
79558
+ );
79559
+ }
79560
+ }
79493
79561
  await stateBackend.deleteState(resolvedStackName, targetRegion);
79494
79562
  logger.info(
79495
79563
  `cdkd state for '${resolvedStackName}' (${targetRegion}) removed. Manage the stack with 'cdk deploy' or 'aws cloudformation' from here on.`
@@ -79585,13 +79653,17 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
79585
79653
  throw err;
79586
79654
  }
79587
79655
  }
79656
+ function isPhase2CreatableType(resourceType) {
79657
+ return resourceType.startsWith("Custom::");
79658
+ }
79588
79659
  async function buildImportPlan(state, template, cfnClient) {
79589
79660
  const templateResources = template["Resources"];
79590
79661
  if (!templateResources || typeof templateResources !== "object" || Array.isArray(templateResources)) {
79591
79662
  throw new Error("Template has no Resources section.");
79592
79663
  }
79593
- const plan = [];
79594
- const skipped = [];
79664
+ const phase1Imports = [];
79665
+ const phase2Creates = [];
79666
+ const blocked = [];
79595
79667
  const identifierCache = /* @__PURE__ */ new Map();
79596
79668
  for (const [logicalId, raw] of Object.entries(templateResources)) {
79597
79669
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
@@ -79600,19 +79672,24 @@ async function buildImportPlan(state, template, cfnClient) {
79600
79672
  const resourceType = resource.Type ?? "";
79601
79673
  if (!resourceType)
79602
79674
  continue;
79675
+ if (resourceType === "AWS::CDK::Metadata") {
79676
+ continue;
79677
+ }
79603
79678
  if (isNeverImportableType(resourceType)) {
79604
- if (resourceType === "AWS::CDK::Metadata")
79605
- continue;
79606
- skipped.push({
79607
- logicalId,
79608
- resourceType,
79609
- reason: "CloudFormation IMPORT does not support this resource type"
79610
- });
79679
+ if (isPhase2CreatableType(resourceType)) {
79680
+ phase2Creates.push({ logicalId, resourceType });
79681
+ } else {
79682
+ blocked.push({
79683
+ logicalId,
79684
+ resourceType,
79685
+ reason: "CloudFormation cannot import or recreate this resource type"
79686
+ });
79687
+ }
79611
79688
  continue;
79612
79689
  }
79613
79690
  const stateEntry = state.resources[logicalId];
79614
79691
  if (!stateEntry || !stateEntry.physicalId) {
79615
- skipped.push({
79692
+ blocked.push({
79616
79693
  logicalId,
79617
79694
  resourceType,
79618
79695
  reason: "no entry in cdkd state (resource is in template but was not deployed by cdkd)"
@@ -79628,21 +79705,21 @@ async function buildImportPlan(state, template, cfnClient) {
79628
79705
  identifierCache
79629
79706
  );
79630
79707
  } catch (err) {
79631
- skipped.push({
79708
+ blocked.push({
79632
79709
  logicalId,
79633
79710
  resourceType,
79634
79711
  reason: "could not resolve resource identifier: " + (err instanceof Error ? err.message : String(err))
79635
79712
  });
79636
79713
  continue;
79637
79714
  }
79638
- plan.push({
79715
+ phase1Imports.push({
79639
79716
  logicalId,
79640
79717
  resourceType,
79641
79718
  physicalId: stateEntry.physicalId,
79642
79719
  resourceIdentifier
79643
79720
  });
79644
79721
  }
79645
- return { plan, skipped };
79722
+ return { phase1Imports, phase2Creates, blocked };
79646
79723
  }
79647
79724
  async function resolveResourceIdentifier(resourceType, physicalId, cfnClient, cache2) {
79648
79725
  let entry = cache2.get(resourceType);
@@ -79701,6 +79778,64 @@ async function fetchPrimaryIdentifier(resourceType, cfnClient) {
79701
79778
  `primary identifier unknown (DescribeType returned no usable schema and no fallback is registered). Add ${resourceType} to PRIMARY_IDENTIFIER_FALLBACK in export.ts, or open an issue.`
79702
79779
  );
79703
79780
  }
79781
+ function parseParameterOverrides(tokens) {
79782
+ const map = {};
79783
+ if (!tokens)
79784
+ return map;
79785
+ for (const t of tokens) {
79786
+ const eq = t.indexOf("=");
79787
+ if (eq < 1) {
79788
+ throw new Error(
79789
+ `Invalid --parameter '${t}': expected 'Key=Value' (e.g. --parameter Env=prod)`
79790
+ );
79791
+ }
79792
+ const key = t.slice(0, eq).trim();
79793
+ const value = t.slice(eq + 1);
79794
+ if (!key) {
79795
+ throw new Error(`Invalid --parameter '${t}': key is empty`);
79796
+ }
79797
+ map[key] = value;
79798
+ }
79799
+ return map;
79800
+ }
79801
+ function resolveTemplateParameters(template, userOverrides) {
79802
+ const tplParams = template["Parameters"];
79803
+ if (!tplParams || typeof tplParams !== "object" || Array.isArray(tplParams)) {
79804
+ const stray = Object.keys(userOverrides);
79805
+ if (stray.length > 0) {
79806
+ throw new Error(
79807
+ `--parameter override(s) supplied (${stray.join(", ")}) but template has no Parameters section.`
79808
+ );
79809
+ }
79810
+ return { parameters: [], missing: [] };
79811
+ }
79812
+ const parameters = [];
79813
+ const missing = [];
79814
+ const known = /* @__PURE__ */ new Set();
79815
+ for (const [name, raw] of Object.entries(tplParams)) {
79816
+ known.add(name);
79817
+ const def = raw ?? {};
79818
+ const override = userOverrides[name];
79819
+ if (override !== void 0) {
79820
+ parameters.push({ ParameterKey: name, ParameterValue: override });
79821
+ continue;
79822
+ }
79823
+ if ("Default" in def) {
79824
+ const value = typeof def.Default === "string" ? def.Default : String(def.Default);
79825
+ parameters.push({ ParameterKey: name, ParameterValue: value });
79826
+ continue;
79827
+ }
79828
+ missing.push(name);
79829
+ }
79830
+ for (const name of Object.keys(userOverrides)) {
79831
+ if (!known.has(name)) {
79832
+ throw new Error(
79833
+ `--parameter override '${name}' does not match any parameter in the synthesized template (template declares: ${[...known].join(", ") || "(none)"})`
79834
+ );
79835
+ }
79836
+ }
79837
+ return { parameters, missing };
79838
+ }
79704
79839
  function filterTemplateForImport(template, plan) {
79705
79840
  const allow = new Set(plan.map((p) => p.logicalId));
79706
79841
  const original = template["Resources"];
@@ -79760,7 +79895,7 @@ function printPlan(plan, cfnStackName) {
79760
79895
  }
79761
79896
  logger.info("");
79762
79897
  }
79763
- async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79898
+ async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters) {
79764
79899
  const logger = getLogger();
79765
79900
  const changeSetName = `cdkd-migrate-${Date.now()}`;
79766
79901
  const templateBody = JSON.stringify(template, null, 2);
@@ -79785,6 +79920,7 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79785
79920
  ChangeSetType: "IMPORT",
79786
79921
  TemplateBody: templateBody,
79787
79922
  ResourcesToImport: resourcesToImport,
79923
+ ...parameters.length > 0 && { Parameters: parameters },
79788
79924
  // CDK templates routinely require CAPABILITY_IAM /
79789
79925
  // CAPABILITY_NAMED_IAM. Forward both so the user does not have to
79790
79926
  // re-discover and re-pass them.
@@ -79831,6 +79967,69 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79831
79967
  throw err;
79832
79968
  }
79833
79969
  }
79970
+ async function executeUpdateChangeSet(cfnClient, stackName, template, parameters) {
79971
+ const logger = getLogger();
79972
+ const changeSetName = `cdkd-phase2-${Date.now()}`;
79973
+ const templateBody = JSON.stringify(template, null, 2);
79974
+ if (templateBody.length > 51200) {
79975
+ throw new Error(
79976
+ `Full template is ${templateBody.length} bytes, over the 51,200-byte inline TemplateBody limit for phase-2 UPDATE. TemplateURL upload is not yet implemented.`
79977
+ );
79978
+ }
79979
+ logger.info(
79980
+ `Creating UPDATE changeset '${changeSetName}' for phase 2 (${templateBody.length} bytes)...`
79981
+ );
79982
+ try {
79983
+ await cfnClient.send(
79984
+ new CreateChangeSetCommand({
79985
+ StackName: stackName,
79986
+ ChangeSetName: changeSetName,
79987
+ ChangeSetType: "UPDATE",
79988
+ TemplateBody: templateBody,
79989
+ ...parameters.length > 0 && { Parameters: parameters },
79990
+ Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
79991
+ })
79992
+ );
79993
+ } catch (err) {
79994
+ const msg = err instanceof Error ? err.message : String(err);
79995
+ throw new Error(`Failed to create UPDATE changeset: ${msg}`);
79996
+ }
79997
+ try {
79998
+ await waitUntilChangeSetCreateComplete(
79999
+ { client: cfnClient, maxWaitTime: 600 },
80000
+ { StackName: stackName, ChangeSetName: changeSetName }
80001
+ );
80002
+ } catch (err) {
80003
+ try {
80004
+ const desc = await cfnClient.send(
80005
+ new DescribeChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })
80006
+ );
80007
+ const reason = desc.StatusReason ?? "unknown";
80008
+ await cfnClient.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })).catch(() => {
80009
+ });
80010
+ throw new Error(`UPDATE changeset FAILED: ${reason}`);
80011
+ } catch (innerErr) {
80012
+ if (innerErr instanceof Error && innerErr.message.startsWith("UPDATE changeset FAILED")) {
80013
+ throw innerErr;
80014
+ }
80015
+ throw err;
80016
+ }
80017
+ }
80018
+ logger.info(`Executing UPDATE changeset...`);
80019
+ try {
80020
+ await cfnClient.send(
80021
+ new ExecuteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })
80022
+ );
80023
+ await waitUntilStackUpdateComplete2(
80024
+ { client: cfnClient, maxWaitTime: 3600 },
80025
+ { StackName: stackName }
80026
+ );
80027
+ } catch (err) {
80028
+ await cfnClient.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })).catch(() => {
80029
+ });
80030
+ throw err;
80031
+ }
80032
+ }
79834
80033
  function refuseTransientContextIfUnsafe(options) {
79835
80034
  const overrides = options.context ?? [];
79836
80035
  if (overrides.length === 0)
@@ -79900,6 +80099,13 @@ function createExportCommand() {
79900
80099
  "--accept-transient-context",
79901
80100
  "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.",
79902
80101
  false
80102
+ ).option(
80103
+ "--include-non-importable",
80104
+ "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.",
80105
+ false
80106
+ ).option(
80107
+ "--parameter <key=value...>",
80108
+ "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value."
79903
80109
  ).action(withErrorHandling(exportCommand));
79904
80110
  [...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
79905
80111
  (opt) => cmd.addOption(opt)
@@ -79938,7 +80144,7 @@ function reorderArgs(argv) {
79938
80144
  }
79939
80145
  async function main() {
79940
80146
  const program = new Command18();
79941
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.86.0");
80147
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.88.0");
79942
80148
  program.addCommand(createBootstrapCommand());
79943
80149
  program.addCommand(createSynthCommand());
79944
80150
  program.addCommand(createListCommand());