@go-to-k/cdkd 0.146.1 → 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 +10 -5
- package/dist/cli.js +134 -13
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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`
|
|
585
|
-
[#464](https://github.com/go-to-k/cdkd/issues/464)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
@@ -54035,11 +54035,6 @@ function createLocalCommand() {
|
|
|
54035
54035
|
*
|
|
54036
54036
|
* - `AWS::CDK::Metadata` is a CDK sentinel; not a real AWS resource and
|
|
54037
54037
|
* CFn refuses to import it.
|
|
54038
|
-
* - `AWS::CloudFormation::Stack` is a nested stack reference. Fresh
|
|
54039
|
-
* `cdkd deploy` of nested stacks IS supported (issue #459), but
|
|
54040
|
-
* moving an existing nested-stack hierarchy from cdkd back into
|
|
54041
|
-
* CloudFormation via `cdkd export` is deferred to the
|
|
54042
|
-
* [#464](https://github.com/go-to-k/cdkd/issues/464) follow-up.
|
|
54043
54038
|
* - `AWS::CloudFormation::CustomResource` is the CFn resource type CDK
|
|
54044
54039
|
* emits for `new cdk.CustomResource(...)` when no `resourceType` is
|
|
54045
54040
|
* passed. Functionally identical to `Custom::*` — Lambda-backed,
|
|
@@ -54049,11 +54044,19 @@ function createLocalCommand() {
|
|
|
54049
54044
|
* custom-resource state — invocation history lives in the provider
|
|
54050
54045
|
* Lambda, not in AWS resource state, so there is nothing to import.
|
|
54051
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
|
+
*
|
|
54052
54055
|
* The list is intentionally narrow. Other resource types CFn may not yet
|
|
54053
54056
|
* support for import are surfaced as errors by the CreateChangeSet call
|
|
54054
54057
|
* itself; we do not try to maintain a closed allowlist here.
|
|
54055
54058
|
*/
|
|
54056
|
-
const NEVER_IMPORTABLE_TYPES = new Set(["AWS::CDK::Metadata"
|
|
54059
|
+
const NEVER_IMPORTABLE_TYPES = new Set(["AWS::CDK::Metadata"]);
|
|
54057
54060
|
function isNeverImportableType(resourceType) {
|
|
54058
54061
|
if (NEVER_IMPORTABLE_TYPES.has(resourceType)) return true;
|
|
54059
54062
|
if (isCustomResourceType(resourceType)) return true;
|
|
@@ -54324,12 +54327,24 @@ async function exportCommand(stackArg, options) {
|
|
|
54324
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.`);
|
|
54325
54328
|
}
|
|
54326
54329
|
try {
|
|
54327
|
-
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 });
|
|
54328
54331
|
if (blocked.length > 0) {
|
|
54329
54332
|
logger.error("The following resources block migration:");
|
|
54330
54333
|
for (const b of blocked) logger.error(` - ${b.logicalId} (${b.resourceType}): ${b.reason}`);
|
|
54331
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.`);
|
|
54332
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
|
+
}
|
|
54333
54348
|
if (phase1Imports.length === 0 && phase2Creates.length === 0 && recreateBeforePhase2.length === 0) {
|
|
54334
54349
|
logger.warn("No resources to migrate — cdkd state is empty.");
|
|
54335
54350
|
return;
|
|
@@ -54496,10 +54511,11 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
|
|
|
54496
54511
|
* `AWS::CloudFormation::Stack` (nested stacks) is intentionally NOT in
|
|
54497
54512
|
* this set: CFn would CREATE a duplicate nested stack rather than adopt
|
|
54498
54513
|
* the existing one, which would conflict with whatever the cdkd state
|
|
54499
|
-
* thought it owned.
|
|
54500
|
-
*
|
|
54501
|
-
*
|
|
54502
|
-
*
|
|
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.
|
|
54503
54519
|
*
|
|
54504
54520
|
* Exported for unit testing.
|
|
54505
54521
|
*/
|
|
@@ -54507,6 +54523,86 @@ function isPhase2CreatableType(resourceType) {
|
|
|
54507
54523
|
return isCustomResourceType(resourceType);
|
|
54508
54524
|
}
|
|
54509
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
|
+
/**
|
|
54510
54606
|
* Build the import plan from cdkd state + the synthesized template.
|
|
54511
54607
|
*
|
|
54512
54608
|
* Classifies every template resource into one of:
|
|
@@ -54522,14 +54618,22 @@ function isPhase2CreatableType(resourceType) {
|
|
|
54522
54618
|
* phases so CFn's phase-2 CREATE doesn't collide. When the user
|
|
54523
54619
|
* passes `--no-recreate-import-unsupported`, these are moved to
|
|
54524
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.
|
|
54525
54628
|
* - `blocked`: anything else. A non-empty `blocked` aborts the run.
|
|
54526
54629
|
*/
|
|
54527
|
-
async function buildImportPlan(state, template, cfnClient, options = { recreateImportUnsupported: true }) {
|
|
54630
|
+
async function buildImportPlan(state, template, cfnClient, parentStackName, options = { recreateImportUnsupported: true }) {
|
|
54528
54631
|
const templateResources = template["Resources"];
|
|
54529
54632
|
if (!templateResources || typeof templateResources !== "object" || Array.isArray(templateResources)) throw new Error("Template has no Resources section.");
|
|
54530
54633
|
const phase1Imports = [];
|
|
54531
54634
|
const phase2Creates = [];
|
|
54532
54635
|
const recreateBeforePhase2 = [];
|
|
54636
|
+
const nestedStackRows = [];
|
|
54533
54637
|
const blocked = [];
|
|
54534
54638
|
const identifierCache = /* @__PURE__ */ new Map();
|
|
54535
54639
|
for (const [logicalId, raw] of Object.entries(templateResources)) {
|
|
@@ -54537,6 +54641,22 @@ async function buildImportPlan(state, template, cfnClient, options = { recreateI
|
|
|
54537
54641
|
const resourceType = raw.Type ?? "";
|
|
54538
54642
|
if (!resourceType) continue;
|
|
54539
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
|
+
}
|
|
54540
54660
|
if (isNeverImportableType(resourceType)) {
|
|
54541
54661
|
if (isPhase2CreatableType(resourceType)) phase2Creates.push({
|
|
54542
54662
|
logicalId,
|
|
@@ -54595,6 +54715,7 @@ async function buildImportPlan(state, template, cfnClient, options = { recreateI
|
|
|
54595
54715
|
phase1Imports,
|
|
54596
54716
|
phase2Creates,
|
|
54597
54717
|
recreateBeforePhase2,
|
|
54718
|
+
nestedStackRows,
|
|
54598
54719
|
blocked
|
|
54599
54720
|
};
|
|
54600
54721
|
}
|
|
@@ -56472,7 +56593,7 @@ function reorderArgs(argv) {
|
|
|
56472
56593
|
*/
|
|
56473
56594
|
async function main() {
|
|
56474
56595
|
const program = new Command();
|
|
56475
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
56596
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.147.0");
|
|
56476
56597
|
program.addCommand(createBootstrapCommand());
|
|
56477
56598
|
program.addCommand(createSynthCommand());
|
|
56478
56599
|
program.addCommand(createListCommand());
|