@go-to-k/cdkd 0.146.0 → 0.147.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/README.md CHANGED
@@ -581,11 +581,16 @@ The Custom Resource Lambda must be idempotent AND must POST to
581
581
  `event.ResponseURL` per the cfn-response protocol. Without the flag,
582
582
  the command refuses to proceed and the user is expected to destroy
583
583
  the offending resources (or accept abandoning them) first. Nested
584
- `AWS::CloudFormation::Stack` references block in this release
585
- [#464](https://github.com/go-to-k/cdkd/issues/464) will lift the
586
- restriction. Fresh `cdkd deploy` of nested stacks works via
587
- [#459](https://github.com/go-to-k/cdkd/issues/459); only the
588
- cdkd CFn migration direction is deferred.
584
+ `AWS::CloudFormation::Stack` rows have partial support as of
585
+ [#464](https://github.com/go-to-k/cdkd/issues/464) PR B1: `cdkd export`
586
+ recursively walks the cdkd state tree, validates every parent → child
587
+ link, and surfaces the full leaf-first migration scope to the user;
588
+ the CFn-side `--include-nested-stacks` IMPORT changeset submission
589
+ itself is deferred to PR B2, so the command warns on `--dry-run` /
590
+ hard-errors on real run with a clear pointer + workaround (keep on
591
+ cdkd, or destroy children leaf-first via `cdkd state destroy <child>`
592
+ and re-export the flattened parent). Fresh `cdkd deploy` of nested
593
+ stacks works via [#459](https://github.com/go-to-k/cdkd/issues/459).
589
594
 
590
595
  ```bash
591
596
  cdkd export MyStack # confirmation prompt; CFn stack name = cdkd stack name
package/dist/cli.js CHANGED
@@ -31721,6 +31721,18 @@ function getCurrentNestedStackContext() {
31721
31721
  //#endregion
31722
31722
  //#region src/provisioning/providers/nested-stack-provider.ts
31723
31723
  /**
31724
+ * Returns `true` when `p` is absolute on the current platform OR begins
31725
+ * with a forward slash. The `startsWith('/')` arm covers the cross-platform
31726
+ * leak where `path.isAbsolute('/abs/foo')` returns `true` on POSIX but
31727
+ * `false` on Windows (Windows absolute paths require a drive letter or
31728
+ * UNC root) — a POSIX-style absolute path embedded in a synth template
31729
+ * generated on Linux and consumed on a Windows host would slip past
31730
+ * `path.isAbsolute()` alone. Exported for unit testing.
31731
+ */
31732
+ function isAbsoluteCrossPlatform(p) {
31733
+ return path.isAbsolute(p) || p.startsWith("/");
31734
+ }
31735
+ /**
31724
31736
  * Provider for `AWS::CloudFormation::Stack` — cdkd's recursive nested-stack
31725
31737
  * adapter. Issue [#459](https://github.com/go-to-k/cdkd/issues/459); see
31726
31738
  * [docs/design/459-nested-stacks.md](../../../docs/design/459-nested-stacks.md)
@@ -31940,7 +31952,7 @@ var NestedStackProvider = class {
31940
31952
  if (resource?.Type !== "AWS::CloudFormation::Stack") continue;
31941
31953
  const assetPath = resource.Metadata?.["aws:asset:path"];
31942
31954
  if (typeof assetPath !== "string" || assetPath.length === 0) continue;
31943
- if (path.isAbsolute(assetPath)) throw new Error(`NestedStackProvider: nested-stack '${grandLogicalId}' has Metadata['aws:asset:path']='${assetPath}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`);
31955
+ if (isAbsoluteCrossPlatform(assetPath)) throw new Error(`NestedStackProvider: nested-stack '${grandLogicalId}' has Metadata['aws:asset:path']='${assetPath}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`);
31944
31956
  result[grandLogicalId] = path.join(dir, assetPath);
31945
31957
  }
31946
31958
  return result;
@@ -54023,11 +54035,6 @@ function createLocalCommand() {
54023
54035
  *
54024
54036
  * - `AWS::CDK::Metadata` is a CDK sentinel; not a real AWS resource and
54025
54037
  * CFn refuses to import it.
54026
- * - `AWS::CloudFormation::Stack` is a nested stack reference. Fresh
54027
- * `cdkd deploy` of nested stacks IS supported (issue #459), but
54028
- * moving an existing nested-stack hierarchy from cdkd back into
54029
- * CloudFormation via `cdkd export` is deferred to the
54030
- * [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
54031
54038
  * - `AWS::CloudFormation::CustomResource` is the CFn resource type CDK
54032
54039
  * emits for `new cdk.CustomResource(...)` when no `resourceType` is
54033
54040
  * passed. Functionally identical to `Custom::*` — Lambda-backed,
@@ -54037,11 +54044,19 @@ function createLocalCommand() {
54037
54044
  * custom-resource state — invocation history lives in the provider
54038
54045
  * Lambda, not in AWS resource state, so there is nothing to import.
54039
54046
  *
54047
+ * `AWS::CloudFormation::Stack` is **intentionally NOT in this set** — it is
54048
+ * handled by a dedicated branch in {@link buildImportPlan} that routes the
54049
+ * row through the nested-stack tree walker (issue
54050
+ * [#464](https://github.com/go-to-k/cdkd/issues/464) PR B1). The CFn-side
54051
+ * `--include-nested-stacks` IMPORT changeset submission is tracked under
54052
+ * PR B2 — until that lands, the orchestrator hard-errors at submission
54053
+ * time with a clear pointer.
54054
+ *
54040
54055
  * The list is intentionally narrow. Other resource types CFn may not yet
54041
54056
  * support for import are surfaced as errors by the CreateChangeSet call
54042
54057
  * itself; we do not try to maintain a closed allowlist here.
54043
54058
  */
54044
- const NEVER_IMPORTABLE_TYPES = new Set(["AWS::CDK::Metadata", "AWS::CloudFormation::Stack"]);
54059
+ const NEVER_IMPORTABLE_TYPES = new Set(["AWS::CDK::Metadata"]);
54045
54060
  function isNeverImportableType(resourceType) {
54046
54061
  if (NEVER_IMPORTABLE_TYPES.has(resourceType)) return true;
54047
54062
  if (isCustomResourceType(resourceType)) return true;
@@ -54312,12 +54327,24 @@ async function exportCommand(stackArg, options) {
54312
54327
  if (!await lockManager.acquireLock(resolvedStackName, targetRegion, owner, "export")) throw new Error(`Could not acquire lock for stack '${resolvedStackName}' (${targetRegion}) — another cdkd process holds it. Wait for it to finish, or run 'cdkd force-unlock ${resolvedStackName}' if you are certain no other process is active.`);
54313
54328
  }
54314
54329
  try {
54315
- const { phase1Imports, phase2Creates, recreateBeforePhase2, blocked } = await buildImportPlan(state, template, awsClients.cloudFormation, { recreateImportUnsupported: options.recreateImportUnsupported });
54330
+ const { phase1Imports, phase2Creates, recreateBeforePhase2, nestedStackRows, blocked } = await buildImportPlan(state, template, awsClients.cloudFormation, resolvedStackName, { recreateImportUnsupported: options.recreateImportUnsupported });
54316
54331
  if (blocked.length > 0) {
54317
54332
  logger.error("The following resources block migration:");
54318
54333
  for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
54319
54334
  throw new Error(`${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.`);
54320
54335
  }
54336
+ if (nestedStackRows.length > 0) {
54337
+ const leafFirst = flattenCdkdStateTreeLeafFirst(await buildCdkdStateStackTree(resolvedStackName, targetRegion, stateBackend, state));
54338
+ logger.info(`Stack '${resolvedStackName}' contains ${nestedStackRows.length} top-level nested stack row(s); cdkd state tree spans ${leafFirst.length} stack(s) total (leaf-first migration order):`);
54339
+ for (const node of leafFirst) logger.info(` - cdkd/${node.stackName}/${node.region}/state.json`);
54340
+ const deferralMessage = `cdkd export does not yet submit nested-stack trees to CloudFormation (${nestedStackRows.length} top-level nested-stack row(s) detected; ${leafFirst.length} cdkd state record(s) in the tree). Recursive --include-nested-stacks IMPORT changeset submission is tracked under issue #464 PR B2. Workarounds until PR B2 ships:\n - Keep the stack on cdkd (recommended — nested-stack deploy / destroy / drift already work via the SDK provider path).\n - OR destroy the nested children first via 'cdkd state destroy <child>' (leaf-first), then re-run 'cdkd export <parent>' against the flattened parent.`;
54341
+ if (options.dryRun) {
54342
+ logger.warn(deferralMessage);
54343
+ logger.info("--dry-run: no CloudFormation changeset will be created.");
54344
+ return;
54345
+ }
54346
+ throw new Error(deferralMessage);
54347
+ }
54321
54348
  if (phase1Imports.length === 0 && phase2Creates.length === 0 && recreateBeforePhase2.length === 0) {
54322
54349
  logger.warn("No resources to migrate — cdkd state is empty.");
54323
54350
  return;
@@ -54484,10 +54511,11 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
54484
54511
  * `AWS::CloudFormation::Stack` (nested stacks) is intentionally NOT in
54485
54512
  * this set: CFn would CREATE a duplicate nested stack rather than adopt
54486
54513
  * the existing one, which would conflict with whatever the cdkd state
54487
- * thought it owned. Fresh `cdkd deploy` of nested stacks is supported
54488
- * via the recursive `NestedStackProvider` (#459), but `cdkd export`
54489
- * adoption back into CloudFormation is deferred to the
54490
- * [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
54514
+ * thought it owned. Nested-stack rows are handled by a dedicated branch
54515
+ * in {@link buildImportPlan} (issue
54516
+ * [#464](https://github.com/go-to-k/cdkd/issues/464) PR B1) that routes
54517
+ * the row to the cdkd-state-side tree walker. The CFn-side
54518
+ * `--include-nested-stacks` IMPORT changeset submission lands in PR B2.
54491
54519
  *
54492
54520
  * Exported for unit testing.
54493
54521
  */
@@ -54495,6 +54523,86 @@ function isPhase2CreatableType(resourceType) {
54495
54523
  return isCustomResourceType(resourceType);
54496
54524
  }
54497
54525
  /**
54526
+ * Recursively load the cdkd-state tree rooted at `(rootStackName, region)`.
54527
+ * For every `AWS::CloudFormation::Stack` row in each level's
54528
+ * `state.resources`, derives the child's v6 state key
54529
+ * (`<parent>~<childLogicalId>`) and loads the child state file from S3,
54530
+ * then recurses.
54531
+ *
54532
+ * Throws if any expected child state file is missing — the parent's state
54533
+ * lists a nested-stack row but the child's `cdkd/<parent>~<child>/<region>/state.json`
54534
+ * does not exist. That state-tree inconsistency must be resolved before
54535
+ * any export attempt (typically via `cdkd state orphan <parent>` and a
54536
+ * full re-deploy, or by completing whatever partial operation left the
54537
+ * tree torn).
54538
+ *
54539
+ * Children at each level are loaded sequentially today (not `Promise.all`)
54540
+ * because the failure-mode shape is "fail fast on the first missing
54541
+ * child" rather than "list every missing child" — a single missing-child
54542
+ * pointer is enough for the user to identify the root cause without
54543
+ * fanning out N parallel `getState` calls on a torn tree. PR B2 may
54544
+ * revisit this once the per-child template-fetch / preprocessing /
54545
+ * upload pass dominates the wall-clock cost.
54546
+ *
54547
+ * Exported so the walker can be unit-tested independently of the orchestrator.
54548
+ *
54549
+ * Callers that already loaded the root state record up front (the typical
54550
+ * orchestrator shape — `exportCommand` always reads it via
54551
+ * `stateBackend.getState` before plan-build) can pass it via the optional
54552
+ * `prefetchedRootState` argument to save one S3 round-trip per export run.
54553
+ * Otherwise the walker fetches it itself, matching the standalone-use
54554
+ * signature.
54555
+ */
54556
+ async function buildCdkdStateStackTree(rootStackName, region, stateBackend, prefetchedRootState) {
54557
+ let rootState;
54558
+ if (prefetchedRootState !== void 0) rootState = prefetchedRootState;
54559
+ else {
54560
+ const rootResult = await stateBackend.getState(rootStackName, region);
54561
+ if (!rootResult) throw new Error(`No cdkd state found for stack '${rootStackName}' (${region}). Cannot build nested-stack tree.`);
54562
+ rootState = rootResult.state;
54563
+ }
54564
+ return walkCdkdStateStackTree(rootStackName, region, rootState, stateBackend);
54565
+ }
54566
+ async function walkCdkdStateStackTree(stackName, region, state, stateBackend) {
54567
+ const nestedChildren = /* @__PURE__ */ new Map();
54568
+ for (const [logicalId, resource] of Object.entries(state.resources)) {
54569
+ if (resource.resourceType !== "AWS::CloudFormation::Stack") continue;
54570
+ const childStackName = `${stackName}~${logicalId}`;
54571
+ const childResult = await stateBackend.getState(childStackName, region);
54572
+ if (!childResult) throw new Error(`cdkd state is missing nested-child '${childStackName}' (${region}). Parent stack '${stackName}' lists '${logicalId}' as an ${NESTED_STACK_RESOURCE_TYPE} row but no child state file exists at 'cdkd/${childStackName}/${region}/state.json'. The cdkd state tree is inconsistent — re-deploy the parent stack to refresh, or run 'cdkd state orphan ${stackName}' and re-import.`);
54573
+ if (childResult.state.region !== void 0 && childResult.state.region !== region) throw new Error(`cdkd state region mismatch: nested-child '${childStackName}' has state.region='${childResult.state.region}' but its parent '${stackName}' is being walked against region='${region}'. AWS does not support cross-region nested stacks; the state tree appears corrupt — re-deploy the parent stack to refresh.`);
54574
+ nestedChildren.set(logicalId, await walkCdkdStateStackTree(childStackName, region, childResult.state, stateBackend));
54575
+ }
54576
+ return {
54577
+ stackName,
54578
+ region,
54579
+ state,
54580
+ nestedChildren
54581
+ };
54582
+ }
54583
+ /**
54584
+ * Flatten a {@link CdkdStateStackTree} into a depth-first list of
54585
+ * `(stackName, region)` pairs in **leaf-first** order — same order PR B2's
54586
+ * state-cleanup pass will use to delete each adopted stack's cdkd state
54587
+ * after the CFn-side IMPORT succeeds (so a mid-walk failure leaves the
54588
+ * earlier parent's state intact for retry).
54589
+ *
54590
+ * Exported so callers (orchestrator user-facing summary, unit tests) can
54591
+ * iterate the tree in the same order without re-implementing the walk.
54592
+ */
54593
+ function flattenCdkdStateTreeLeafFirst(tree) {
54594
+ const out = [];
54595
+ walk(tree);
54596
+ return out;
54597
+ function walk(node) {
54598
+ for (const child of node.nestedChildren.values()) walk(child);
54599
+ out.push({
54600
+ stackName: node.stackName,
54601
+ region: node.region
54602
+ });
54603
+ }
54604
+ }
54605
+ /**
54498
54606
  * Build the import plan from cdkd state + the synthesized template.
54499
54607
  *
54500
54608
  * Classifies every template resource into one of:
@@ -54510,14 +54618,22 @@ function isPhase2CreatableType(resourceType) {
54510
54618
  * phases so CFn's phase-2 CREATE doesn't collide. When the user
54511
54619
  * passes `--no-recreate-import-unsupported`, these are moved to
54512
54620
  * `blocked` instead.
54621
+ * - `nestedStackRows`: `AWS::CloudFormation::Stack` rows in the parent
54622
+ * template. Surfaced separately because they are handled by the
54623
+ * nested-stack tree walker, not the per-resource IMPORT path
54624
+ * (issue [#464](https://github.com/go-to-k/cdkd/issues/464) PR B1 + PR B2).
54625
+ * The orchestrator currently hard-errors when any are present —
54626
+ * CFn-side `--include-nested-stacks` IMPORT changeset submission
54627
+ * lands in PR B2.
54513
54628
  * - `blocked`: anything else. A non-empty `blocked` aborts the run.
54514
54629
  */
54515
- async function buildImportPlan(state, template, cfnClient, options = { recreateImportUnsupported: true }) {
54630
+ async function buildImportPlan(state, template, cfnClient, parentStackName, options = { recreateImportUnsupported: true }) {
54516
54631
  const templateResources = template["Resources"];
54517
54632
  if (!templateResources || typeof templateResources !== "object" || Array.isArray(templateResources)) throw new Error("Template has no Resources section.");
54518
54633
  const phase1Imports = [];
54519
54634
  const phase2Creates = [];
54520
54635
  const recreateBeforePhase2 = [];
54636
+ const nestedStackRows = [];
54521
54637
  const blocked = [];
54522
54638
  const identifierCache = /* @__PURE__ */ new Map();
54523
54639
  for (const [logicalId, raw] of Object.entries(templateResources)) {
@@ -54525,6 +54641,22 @@ async function buildImportPlan(state, template, cfnClient, options = { recreateI
54525
54641
  const resourceType = raw.Type ?? "";
54526
54642
  if (!resourceType) continue;
54527
54643
  if (resourceType === "AWS::CDK::Metadata") continue;
54644
+ if (resourceType === "AWS::CloudFormation::Stack") {
54645
+ const parentRow = state.resources[logicalId];
54646
+ if (!parentRow || parentRow.resourceType !== "AWS::CloudFormation::Stack") {
54647
+ blocked.push({
54648
+ logicalId,
54649
+ resourceType,
54650
+ reason: `template has AWS::CloudFormation::Stack row '${logicalId}' but cdkd state has no matching nested-stack entry on parent '${parentStackName}'. Re-deploy or re-import the parent stack to refresh state.`
54651
+ });
54652
+ continue;
54653
+ }
54654
+ nestedStackRows.push({
54655
+ logicalId,
54656
+ childStackName: `${parentStackName}~${logicalId}`
54657
+ });
54658
+ continue;
54659
+ }
54528
54660
  if (isNeverImportableType(resourceType)) {
54529
54661
  if (isPhase2CreatableType(resourceType)) phase2Creates.push({
54530
54662
  logicalId,
@@ -54583,6 +54715,7 @@ async function buildImportPlan(state, template, cfnClient, options = { recreateI
54583
54715
  phase1Imports,
54584
54716
  phase2Creates,
54585
54717
  recreateBeforePhase2,
54718
+ nestedStackRows,
54586
54719
  blocked
54587
54720
  };
54588
54721
  }
@@ -56460,7 +56593,7 @@ function reorderArgs(argv) {
56460
56593
  */
56461
56594
  async function main() {
56462
56595
  const program = new Command();
56463
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.146.0");
56596
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.147.0");
56464
56597
  program.addCommand(createBootstrapCommand());
56465
56598
  program.addCommand(createSynthCommand());
56466
56599
  program.addCommand(createListCommand());