@go-to-k/cdkd 0.86.0 → 0.87.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,90 @@ 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 phase1Template = filterTemplateForImport(template, phase1Imports);
79516
+ await executeImportChangeSet(
79517
+ awsClients.cloudFormation,
79518
+ cfnStackName,
79519
+ phase1Template,
79520
+ phase1Imports
79521
+ );
79490
79522
  logger.info(
79491
- `\u2713 CloudFormation stack '${cfnStackName}' created via IMPORT. ${plan.length} resource(s) are now managed by CloudFormation.`
79523
+ `\u2713 Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`
79492
79524
  );
79525
+ if (phase2Creates.length > 0) {
79526
+ try {
79527
+ await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, template);
79528
+ logger.info(`\u2713 Phase 2: ${phase2Creates.length} non-importable resource(s) CREATEd.`);
79529
+ } catch (err) {
79530
+ const msg = err instanceof Error ? err.message : String(err);
79531
+ throw new Error(
79532
+ `Phase 1 (IMPORT) succeeded; phase 2 (UPDATE) failed: ${msg}
79533
+
79534
+ 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:
79535
+ 1. Fix the failure cause (typically an onCreate Lambda error).
79536
+ 2. Re-run the phase 2 UPDATE manually with the full synth template:
79537
+ aws cloudformation create-change-set --stack-name ${cfnStackName} \\
79538
+ --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\
79539
+ --template-body file://<full-template.json>
79540
+ 3. Once phase 2 succeeds, run: cdkd state orphan ${resolvedStackName}
79541
+ to clean up cdkd's stale state record.`
79542
+ );
79543
+ }
79544
+ }
79493
79545
  await stateBackend.deleteState(resolvedStackName, targetRegion);
79494
79546
  logger.info(
79495
79547
  `cdkd state for '${resolvedStackName}' (${targetRegion}) removed. Manage the stack with 'cdk deploy' or 'aws cloudformation' from here on.`
@@ -79585,13 +79637,17 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
79585
79637
  throw err;
79586
79638
  }
79587
79639
  }
79640
+ function isPhase2CreatableType(resourceType) {
79641
+ return resourceType.startsWith("Custom::");
79642
+ }
79588
79643
  async function buildImportPlan(state, template, cfnClient) {
79589
79644
  const templateResources = template["Resources"];
79590
79645
  if (!templateResources || typeof templateResources !== "object" || Array.isArray(templateResources)) {
79591
79646
  throw new Error("Template has no Resources section.");
79592
79647
  }
79593
- const plan = [];
79594
- const skipped = [];
79648
+ const phase1Imports = [];
79649
+ const phase2Creates = [];
79650
+ const blocked = [];
79595
79651
  const identifierCache = /* @__PURE__ */ new Map();
79596
79652
  for (const [logicalId, raw] of Object.entries(templateResources)) {
79597
79653
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
@@ -79600,19 +79656,24 @@ async function buildImportPlan(state, template, cfnClient) {
79600
79656
  const resourceType = resource.Type ?? "";
79601
79657
  if (!resourceType)
79602
79658
  continue;
79659
+ if (resourceType === "AWS::CDK::Metadata") {
79660
+ continue;
79661
+ }
79603
79662
  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
- });
79663
+ if (isPhase2CreatableType(resourceType)) {
79664
+ phase2Creates.push({ logicalId, resourceType });
79665
+ } else {
79666
+ blocked.push({
79667
+ logicalId,
79668
+ resourceType,
79669
+ reason: "CloudFormation cannot import or recreate this resource type"
79670
+ });
79671
+ }
79611
79672
  continue;
79612
79673
  }
79613
79674
  const stateEntry = state.resources[logicalId];
79614
79675
  if (!stateEntry || !stateEntry.physicalId) {
79615
- skipped.push({
79676
+ blocked.push({
79616
79677
  logicalId,
79617
79678
  resourceType,
79618
79679
  reason: "no entry in cdkd state (resource is in template but was not deployed by cdkd)"
@@ -79628,21 +79689,21 @@ async function buildImportPlan(state, template, cfnClient) {
79628
79689
  identifierCache
79629
79690
  );
79630
79691
  } catch (err) {
79631
- skipped.push({
79692
+ blocked.push({
79632
79693
  logicalId,
79633
79694
  resourceType,
79634
79695
  reason: "could not resolve resource identifier: " + (err instanceof Error ? err.message : String(err))
79635
79696
  });
79636
79697
  continue;
79637
79698
  }
79638
- plan.push({
79699
+ phase1Imports.push({
79639
79700
  logicalId,
79640
79701
  resourceType,
79641
79702
  physicalId: stateEntry.physicalId,
79642
79703
  resourceIdentifier
79643
79704
  });
79644
79705
  }
79645
- return { plan, skipped };
79706
+ return { phase1Imports, phase2Creates, blocked };
79646
79707
  }
79647
79708
  async function resolveResourceIdentifier(resourceType, physicalId, cfnClient, cache2) {
79648
79709
  let entry = cache2.get(resourceType);
@@ -79831,6 +79892,68 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79831
79892
  throw err;
79832
79893
  }
79833
79894
  }
79895
+ async function executeUpdateChangeSet(cfnClient, stackName, template) {
79896
+ const logger = getLogger();
79897
+ const changeSetName = `cdkd-phase2-${Date.now()}`;
79898
+ const templateBody = JSON.stringify(template, null, 2);
79899
+ if (templateBody.length > 51200) {
79900
+ throw new Error(
79901
+ `Full template is ${templateBody.length} bytes, over the 51,200-byte inline TemplateBody limit for phase-2 UPDATE. TemplateURL upload is not yet implemented.`
79902
+ );
79903
+ }
79904
+ logger.info(
79905
+ `Creating UPDATE changeset '${changeSetName}' for phase 2 (${templateBody.length} bytes)...`
79906
+ );
79907
+ try {
79908
+ await cfnClient.send(
79909
+ new CreateChangeSetCommand({
79910
+ StackName: stackName,
79911
+ ChangeSetName: changeSetName,
79912
+ ChangeSetType: "UPDATE",
79913
+ TemplateBody: templateBody,
79914
+ Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
79915
+ })
79916
+ );
79917
+ } catch (err) {
79918
+ const msg = err instanceof Error ? err.message : String(err);
79919
+ throw new Error(`Failed to create UPDATE changeset: ${msg}`);
79920
+ }
79921
+ try {
79922
+ await waitUntilChangeSetCreateComplete(
79923
+ { client: cfnClient, maxWaitTime: 600 },
79924
+ { StackName: stackName, ChangeSetName: changeSetName }
79925
+ );
79926
+ } catch (err) {
79927
+ try {
79928
+ const desc = await cfnClient.send(
79929
+ new DescribeChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })
79930
+ );
79931
+ const reason = desc.StatusReason ?? "unknown";
79932
+ await cfnClient.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })).catch(() => {
79933
+ });
79934
+ throw new Error(`UPDATE changeset FAILED: ${reason}`);
79935
+ } catch (innerErr) {
79936
+ if (innerErr instanceof Error && innerErr.message.startsWith("UPDATE changeset FAILED")) {
79937
+ throw innerErr;
79938
+ }
79939
+ throw err;
79940
+ }
79941
+ }
79942
+ logger.info(`Executing UPDATE changeset...`);
79943
+ try {
79944
+ await cfnClient.send(
79945
+ new ExecuteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })
79946
+ );
79947
+ await waitUntilStackUpdateComplete2(
79948
+ { client: cfnClient, maxWaitTime: 3600 },
79949
+ { StackName: stackName }
79950
+ );
79951
+ } catch (err) {
79952
+ await cfnClient.send(new DeleteChangeSetCommand({ StackName: stackName, ChangeSetName: changeSetName })).catch(() => {
79953
+ });
79954
+ throw err;
79955
+ }
79956
+ }
79834
79957
  function refuseTransientContextIfUnsafe(options) {
79835
79958
  const overrides = options.context ?? [];
79836
79959
  if (overrides.length === 0)
@@ -79900,6 +80023,10 @@ function createExportCommand() {
79900
80023
  "--accept-transient-context",
79901
80024
  "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
80025
  false
80026
+ ).option(
80027
+ "--include-non-importable",
80028
+ "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.",
80029
+ false
79903
80030
  ).action(withErrorHandling(exportCommand));
79904
80031
  [...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
79905
80032
  (opt) => cmd.addOption(opt)
@@ -79938,7 +80065,7 @@ function reorderArgs(argv) {
79938
80065
  }
79939
80066
  async function main() {
79940
80067
  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");
80068
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.87.0");
79942
80069
  program.addCommand(createBootstrapCommand());
79943
80070
  program.addCommand(createSynthCommand());
79944
80071
  program.addCommand(createListCommand());