@go-to-k/cdkd 0.145.1 → 0.146.1
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 +527 -30
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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 (
|
|
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;
|
|
@@ -36196,6 +36208,16 @@ function parseCfnTemplateWithFormat(text) {
|
|
|
36196
36208
|
//#endregion
|
|
36197
36209
|
//#region src/cli/commands/retire-cfn-stack.ts
|
|
36198
36210
|
/**
|
|
36211
|
+
* Resource type for a CloudFormation nested-stack child. Hoisted as a
|
|
36212
|
+
* constant so the recursive walker, the Retain-injection skip rule, and
|
|
36213
|
+
* the import-side short-circuit all reference the same literal — a typo
|
|
36214
|
+
* in any one of them would otherwise silently break nested-stack support
|
|
36215
|
+
* (the offending row would either be missed by the tree walk or have
|
|
36216
|
+
* Retain injected on it, both of which would orphan the child stack
|
|
36217
|
+
* record on AWS at retire time).
|
|
36218
|
+
*/
|
|
36219
|
+
const NESTED_STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
|
|
36220
|
+
/**
|
|
36199
36221
|
* UpdateStack TemplateBody hard limit (51,200 bytes). Templates larger than
|
|
36200
36222
|
* this are uploaded to cdkd's state S3 bucket and submitted via `TemplateURL`
|
|
36201
36223
|
* instead — see {@link uploadTemplateForUpdateStack}.
|
|
@@ -36242,7 +36264,8 @@ const TEMPLATE_URL_LIMIT = CFN_TEMPLATE_URL_LIMIT;
|
|
|
36242
36264
|
*/
|
|
36243
36265
|
async function retireCloudFormationStack(options) {
|
|
36244
36266
|
const logger = getLogger();
|
|
36245
|
-
const { cfnStackName, cfnClient, yes, stateBucket, s3ClientOpts } = options;
|
|
36267
|
+
const { cfnStackName, cfnClient, yes, stateBucket, s3ClientOpts, resourceTree } = options;
|
|
36268
|
+
if (resourceTree && resourceTree.stackName !== cfnStackName) throw new Error(`retireCloudFormationStack: caller-supplied resourceTree.stackName='${resourceTree.stackName}' does not match cfnStackName='${cfnStackName}'. The tree's root must be the stack being retired.`);
|
|
36246
36269
|
logger.info(`[1/4] Inspecting CloudFormation stack '${cfnStackName}'...`);
|
|
36247
36270
|
const stack = (await cfnClient.send(new DescribeStacksCommand({ StackName: cfnStackName }))).Stacks?.[0];
|
|
36248
36271
|
if (!stack) throw new Error(`CloudFormation stack '${cfnStackName}' not found.`);
|
|
@@ -36254,15 +36277,54 @@ async function retireCloudFormationStack(options) {
|
|
|
36254
36277
|
TemplateStage: "Original"
|
|
36255
36278
|
}));
|
|
36256
36279
|
if (!tpl.TemplateBody) throw new Error(`GetTemplate returned no body for '${cfnStackName}'.`);
|
|
36257
|
-
const
|
|
36280
|
+
const nestedCleanups = [];
|
|
36281
|
+
const hasNestedChildren = templateContainsNestedStackRows(tpl.TemplateBody);
|
|
36282
|
+
let newBody;
|
|
36283
|
+
let modified;
|
|
36284
|
+
let format;
|
|
36285
|
+
if (hasNestedChildren) {
|
|
36286
|
+
const tree = resourceTree ?? await getCloudFormationResourceTree(cfnStackName, cfnClient);
|
|
36287
|
+
try {
|
|
36288
|
+
const recursive = await injectRetainPoliciesRecursive(tpl.TemplateBody, cfnStackName, tree, {
|
|
36289
|
+
cfnClient,
|
|
36290
|
+
stateBucket,
|
|
36291
|
+
...s3ClientOpts && { s3ClientOpts }
|
|
36292
|
+
});
|
|
36293
|
+
newBody = recursive.body;
|
|
36294
|
+
modified = recursive.modified;
|
|
36295
|
+
format = recursive.format;
|
|
36296
|
+
nestedCleanups.push(...recursive.cleanups);
|
|
36297
|
+
} catch (err) {
|
|
36298
|
+
if (err instanceof RecursiveRetainInjectionError) for (const cleanup of err.cleanups) try {
|
|
36299
|
+
await cleanup();
|
|
36300
|
+
} catch (cleanupErr) {
|
|
36301
|
+
const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
36302
|
+
logger.warn(`Failed to delete partial nested-template upload from '${stateBucket}' during error recovery. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
|
|
36303
|
+
}
|
|
36304
|
+
throw err;
|
|
36305
|
+
}
|
|
36306
|
+
} else ({body: newBody, modified, format} = injectRetainPolicies(tpl.TemplateBody, cfnStackName));
|
|
36258
36307
|
if (!yes) {
|
|
36259
36308
|
if (!await confirmPrompt$2(`Set DeletionPolicy=Retain and UpdateReplacePolicy=Retain on every resource in CloudFormation stack '${cfnStackName}', then delete the stack? AWS resources will NOT be deleted (cdkd state has been written).`)) {
|
|
36260
36309
|
logger.info("CloudFormation stack retirement cancelled. cdkd state is unaffected.");
|
|
36310
|
+
for (const cleanup of nestedCleanups) try {
|
|
36311
|
+
await cleanup();
|
|
36312
|
+
} catch (cleanupErr) {
|
|
36313
|
+
const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
36314
|
+
logger.warn(`Failed to delete temporary nested-template upload from '${stateBucket}' during cancel cleanup. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
|
|
36315
|
+
}
|
|
36261
36316
|
return { outcome: "cancelled" };
|
|
36262
36317
|
}
|
|
36263
36318
|
}
|
|
36264
|
-
if (!modified)
|
|
36265
|
-
|
|
36319
|
+
if (!modified) {
|
|
36320
|
+
logger.info(`[2/4] Template already has Retain on every resource — skipping UpdateStack.`);
|
|
36321
|
+
if (nestedCleanups.length > 0) for (const cleanup of nestedCleanups) try {
|
|
36322
|
+
await cleanup();
|
|
36323
|
+
} catch (cleanupErr) {
|
|
36324
|
+
const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
36325
|
+
logger.warn(`Failed to delete temporary nested-template upload from '${stateBucket}'. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
|
|
36326
|
+
}
|
|
36327
|
+
} else {
|
|
36266
36328
|
logger.info(`[2/4] Injected DeletionPolicy=Retain and UpdateReplacePolicy=Retain.`);
|
|
36267
36329
|
if (newBody.length > TEMPLATE_URL_LIMIT) throw new Error(`Modified template is ${newBody.length} bytes, exceeds the CloudFormation UpdateStack TemplateURL limit (${TEMPLATE_URL_LIMIT}). cdkd state has already been written; retire the stack manually with (1) shrink the template, then (2) UpdateStack with Retain policies, (3) DeleteStack — or split the stack and retry.`);
|
|
36268
36330
|
let updateInput;
|
|
@@ -36305,8 +36367,9 @@ async function retireCloudFormationStack(options) {
|
|
|
36305
36367
|
maxWaitTime: 1800
|
|
36306
36368
|
}, { StackName: cfnStackName });
|
|
36307
36369
|
} finally {
|
|
36308
|
-
|
|
36309
|
-
|
|
36370
|
+
const allCleanups = [...s3Cleanup ? [s3Cleanup] : [], ...nestedCleanups];
|
|
36371
|
+
for (const cleanup of allCleanups) try {
|
|
36372
|
+
await cleanup();
|
|
36310
36373
|
} catch (cleanupErr) {
|
|
36311
36374
|
const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
36312
36375
|
logger.warn(`Failed to delete temporary template upload from '${stateBucket}'. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`);
|
|
@@ -36374,11 +36437,66 @@ function injectRetainPolicies(templateBody, cfnStackName) {
|
|
|
36374
36437
|
throw new Error(`Template for '${cfnStackName}' is not a valid CloudFormation template. cdkd's --migrate-from-cloudformation flow supports both JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Cause: ${err instanceof Error ? err.message : String(err)}`);
|
|
36375
36438
|
}
|
|
36376
36439
|
if (!("Resources" in parsed) || typeof parsed["Resources"] !== "object" || parsed["Resources"] === null || Array.isArray(parsed["Resources"])) throw new Error(`Template for '${cfnStackName}' has no Resources section — refusing to retire.`);
|
|
36377
|
-
|
|
36440
|
+
const modified = injectRetainPoliciesOnParsedResources(parsed["Resources"]);
|
|
36441
|
+
return {
|
|
36442
|
+
body: stringifyCfnTemplate(parsed, format),
|
|
36443
|
+
modified,
|
|
36444
|
+
format
|
|
36445
|
+
};
|
|
36446
|
+
}
|
|
36447
|
+
/**
|
|
36448
|
+
* Cheap sniff: does the template body contain any `AWS::CloudFormation::Stack`
|
|
36449
|
+
* resource rows? Parses + walks `Resources` once (no recursive descent),
|
|
36450
|
+
* tolerating parse failures by returning `false` (the caller's downstream
|
|
36451
|
+
* parse step will surface the same error with a richer message). Used by
|
|
36452
|
+
* {@link retireCloudFormationStack} to decide whether to lazily build the
|
|
36453
|
+
* recursive {@link CfnStackResourceTree} — non-nested templates skip the
|
|
36454
|
+
* extra `DescribeStackResources` round-trip entirely.
|
|
36455
|
+
*/
|
|
36456
|
+
function templateContainsNestedStackRows(templateBody) {
|
|
36457
|
+
let parsed;
|
|
36458
|
+
try {
|
|
36459
|
+
parsed = parseCfnTemplate(templateBody);
|
|
36460
|
+
} catch {
|
|
36461
|
+
return false;
|
|
36462
|
+
}
|
|
36378
36463
|
const resources = parsed["Resources"];
|
|
36464
|
+
if (!resources || typeof resources !== "object" || Array.isArray(resources)) return false;
|
|
36465
|
+
for (const resource of Object.values(resources)) {
|
|
36466
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
|
|
36467
|
+
if (resource["Type"] === "AWS::CloudFormation::Stack") return true;
|
|
36468
|
+
}
|
|
36469
|
+
return false;
|
|
36470
|
+
}
|
|
36471
|
+
/**
|
|
36472
|
+
* Mutate an already-parsed `Resources` map in place: set `DeletionPolicy: Retain`
|
|
36473
|
+
* and `UpdateReplacePolicy: Retain` on every leaf resource that doesn't
|
|
36474
|
+
* already have them. Returns the `modified` flag so the caller can decide
|
|
36475
|
+
* whether the round-trip is worth a `stringifyCfnTemplate` re-serialization
|
|
36476
|
+
* (and whether `UpdateStack` is needed at all).
|
|
36477
|
+
*
|
|
36478
|
+
* `AWS::CloudFormation::Stack` resources are **intentionally skipped** — see
|
|
36479
|
+
* issue [#464](https://github.com/go-to-k/cdkd/issues/464) and the design at
|
|
36480
|
+
* [docs/design/464-nested-stacks-export-import.md](../../../docs/design/464-nested-stacks-export-import.md)
|
|
36481
|
+
* §3.4. Retain on a nested-stack row tells AWS CFn's parent-side `DeleteStack`
|
|
36482
|
+
* to NOT cascade-delete the child stack record at all, which would leave a
|
|
36483
|
+
* stranded child stack record on AWS that the user has to clean up manually.
|
|
36484
|
+
* We want the cascade to descend (so child stack records get deleted as the
|
|
36485
|
+
* tree unwinds) while every child's own leaf resources stay because THEIR
|
|
36486
|
+
* Retain was injected on the previous recursion level — the recursive
|
|
36487
|
+
* Retain injection in {@link injectRetainPoliciesRecursive} relies on this
|
|
36488
|
+
* skip rule to behave correctly.
|
|
36489
|
+
*
|
|
36490
|
+
* Shared between the single-template path ({@link injectRetainPolicies}) and
|
|
36491
|
+
* the per-level walk inside {@link injectRetainPoliciesRecursive} so both
|
|
36492
|
+
* sites apply the exact same skip rule with no risk of drift.
|
|
36493
|
+
*/
|
|
36494
|
+
function injectRetainPoliciesOnParsedResources(resources) {
|
|
36495
|
+
let modified = false;
|
|
36379
36496
|
for (const [, resource] of Object.entries(resources)) {
|
|
36380
36497
|
if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
|
|
36381
36498
|
const r = resource;
|
|
36499
|
+
if (r["Type"] === "AWS::CloudFormation::Stack") continue;
|
|
36382
36500
|
if (r["DeletionPolicy"] !== "Retain") {
|
|
36383
36501
|
r["DeletionPolicy"] = "Retain";
|
|
36384
36502
|
modified = true;
|
|
@@ -36388,6 +36506,119 @@ function injectRetainPolicies(templateBody, cfnStackName) {
|
|
|
36388
36506
|
modified = true;
|
|
36389
36507
|
}
|
|
36390
36508
|
}
|
|
36509
|
+
return modified;
|
|
36510
|
+
}
|
|
36511
|
+
/**
|
|
36512
|
+
* Recursive variant of {@link injectRetainPolicies} that handles a parent
|
|
36513
|
+
* template containing one or more `AWS::CloudFormation::Stack` rows. Issue
|
|
36514
|
+
* [#464](https://github.com/go-to-k/cdkd/issues/464); see
|
|
36515
|
+
* [docs/design/464-nested-stacks-export-import.md](../../../docs/design/464-nested-stacks-export-import.md)
|
|
36516
|
+
* §3.4 for the design rationale.
|
|
36517
|
+
*
|
|
36518
|
+
* For each nested-stack row in the parent template:
|
|
36519
|
+
* 1. `GetTemplate` Original-stage on the corresponding child stack ARN (read
|
|
36520
|
+
* from the supplied {@link CfnStackResourceTree}, populated up front by
|
|
36521
|
+
* {@link getCloudFormationResourceTree}).
|
|
36522
|
+
* 2. Recursively process the child template — Retain injection on its
|
|
36523
|
+
* leaves AND further recursion into any grandchildren.
|
|
36524
|
+
* 3. If the recursion produced a modified child body, upload the modified
|
|
36525
|
+
* body to the cdkd state bucket via the shared {@link uploadCfnTemplate}
|
|
36526
|
+
* helper and rewrite the parent's `Properties.TemplateURL` for that
|
|
36527
|
+
* row to the uploaded URL — so the parent's eventual `UpdateStack`
|
|
36528
|
+
* cascades into the child with the Retain-bearing template.
|
|
36529
|
+
*
|
|
36530
|
+
* After all nested-stack rows are processed, the parent's own leaf
|
|
36531
|
+
* resources get Retain injected via {@link injectRetainPoliciesOnParsedResources}
|
|
36532
|
+
* — the SAME helper the single-template path uses, applying the same
|
|
36533
|
+
* "skip `AWS::CloudFormation::Stack` rows" rule (those rows must NOT have
|
|
36534
|
+
* Retain or cascade-delete won't descend — see the helper's doc-comment).
|
|
36535
|
+
*
|
|
36536
|
+
* The returned `cleanups` array carries one S3 delete callback per
|
|
36537
|
+
* transient child-template upload made anywhere in the recursive walk.
|
|
36538
|
+
* The caller drains it in a `finally` block around the parent
|
|
36539
|
+
* `UpdateStack` call — CFn fetches each child template synchronously
|
|
36540
|
+
* during `UpdateStack`, so the transient objects can be reaped as soon
|
|
36541
|
+
* as that call returns (success OR failure). When the helper itself
|
|
36542
|
+
* throws mid-walk, the partial uploads accumulated so far are returned
|
|
36543
|
+
* via a thrown {@link RecursiveRetainInjectionError} so the outer
|
|
36544
|
+
* `finally` can still reap them — losing the throw stack vs. the
|
|
36545
|
+
* cleanups would have leaked transient S3 objects.
|
|
36546
|
+
*
|
|
36547
|
+
* Exported so the recursive call can be unit-tested end-to-end (the AWS
|
|
36548
|
+
* round-trips are mocked at the SDK boundary, but the recursion shape
|
|
36549
|
+
* itself is the load-bearing piece).
|
|
36550
|
+
*/
|
|
36551
|
+
async function injectRetainPoliciesRecursive(rootTemplateBody, rootCfnStackName, rootTree, deps) {
|
|
36552
|
+
const cleanups = [];
|
|
36553
|
+
try {
|
|
36554
|
+
return {
|
|
36555
|
+
...await injectRetainPoliciesRecursiveInternal(rootTemplateBody, rootCfnStackName, rootTree, deps, cleanups),
|
|
36556
|
+
cleanups
|
|
36557
|
+
};
|
|
36558
|
+
} catch (err) {
|
|
36559
|
+
throw new RecursiveRetainInjectionError(err instanceof Error ? err.message : String(err), cleanups, err instanceof Error ? err : void 0);
|
|
36560
|
+
}
|
|
36561
|
+
}
|
|
36562
|
+
/**
|
|
36563
|
+
* Thrown by {@link injectRetainPoliciesRecursive} when the recursive walk
|
|
36564
|
+
* fails partway through. Carries the array of cleanup callbacks for every
|
|
36565
|
+
* successful transient S3 upload made BEFORE the failure so the outer
|
|
36566
|
+
* `finally` in `retireCloudFormationStack` can still reap them.
|
|
36567
|
+
*
|
|
36568
|
+
* Without this, a mid-walk error would lose every accumulated cleanup —
|
|
36569
|
+
* the error path would skip the parent's `finally` block that drains
|
|
36570
|
+
* `nestedCleanups`, leaking N transient S3 objects per failed retire.
|
|
36571
|
+
*/
|
|
36572
|
+
var RecursiveRetainInjectionError = class extends Error {
|
|
36573
|
+
cleanups;
|
|
36574
|
+
cause;
|
|
36575
|
+
constructor(message, cleanups, cause) {
|
|
36576
|
+
super(message);
|
|
36577
|
+
this.name = "RecursiveRetainInjectionError";
|
|
36578
|
+
this.cleanups = cleanups;
|
|
36579
|
+
if (cause) this.cause = cause;
|
|
36580
|
+
}
|
|
36581
|
+
};
|
|
36582
|
+
async function injectRetainPoliciesRecursiveInternal(templateBody, cfnStackName, tree, deps, cleanups) {
|
|
36583
|
+
const format = detectTemplateFormat(templateBody);
|
|
36584
|
+
let parsed;
|
|
36585
|
+
try {
|
|
36586
|
+
parsed = parseCfnTemplate(templateBody);
|
|
36587
|
+
} catch (err) {
|
|
36588
|
+
throw new Error(`Template for '${cfnStackName}' is not a valid CloudFormation template. cdkd's --migrate-from-cloudformation flow supports both JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Cause: ${err instanceof Error ? err.message : String(err)}`);
|
|
36589
|
+
}
|
|
36590
|
+
if (!("Resources" in parsed) || typeof parsed["Resources"] !== "object" || parsed["Resources"] === null || Array.isArray(parsed["Resources"])) throw new Error(`Template for '${cfnStackName}' has no Resources section — refusing to retire.`);
|
|
36591
|
+
const resources = parsed["Resources"];
|
|
36592
|
+
let modified = false;
|
|
36593
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
36594
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
|
|
36595
|
+
const r = resource;
|
|
36596
|
+
if (r["Type"] !== "AWS::CloudFormation::Stack") continue;
|
|
36597
|
+
const childNode = tree.nested.get(logicalId);
|
|
36598
|
+
if (!childNode) throw new Error(`Template for '${cfnStackName}' has nested-stack '${logicalId}' (Type: ${NESTED_STACK_RESOURCE_TYPE}) but the CloudFormation resource tree has no matching child entry. AWS may have removed the child since DescribeStackResources ran, or the template was hand-edited mid-flight — re-run \`cdkd import --migrate-from-cloudformation\` to refresh.`);
|
|
36599
|
+
const childTpl = await deps.cfnClient.send(new GetTemplateCommand({
|
|
36600
|
+
StackName: childNode.physicalId,
|
|
36601
|
+
TemplateStage: "Original"
|
|
36602
|
+
}));
|
|
36603
|
+
if (!childTpl.TemplateBody) throw new Error(`GetTemplate returned no body for nested stack '${logicalId}' (physicalId='${childNode.physicalId}', parent='${cfnStackName}').`);
|
|
36604
|
+
const childResult = await injectRetainPoliciesRecursiveInternal(childTpl.TemplateBody, childNode.stackName, childNode, deps, cleanups);
|
|
36605
|
+
if (childResult.modified) {
|
|
36606
|
+
if (childResult.body.length > TEMPLATE_URL_LIMIT) throw new Error(`Modified nested-stack template for '${logicalId}' is ${childResult.body.length} bytes, exceeds the CloudFormation TemplateURL limit (${TEMPLATE_URL_LIMIT}). cdkd state has already been written for the root parent; retire the stack manually with (1) shrink the child template, then (2) UpdateStack with Retain policies on the parent and each child, (3) DeleteStack on the parent.`);
|
|
36607
|
+
const uploaded = await uploadCfnTemplate({
|
|
36608
|
+
bucket: deps.stateBucket,
|
|
36609
|
+
body: childResult.body,
|
|
36610
|
+
stackName: `${cfnStackName}__nested__${logicalId}`,
|
|
36611
|
+
format: childResult.format,
|
|
36612
|
+
...deps.s3ClientOpts && { s3ClientOpts: deps.s3ClientOpts }
|
|
36613
|
+
});
|
|
36614
|
+
cleanups.push(uploaded.cleanup);
|
|
36615
|
+
const propsRaw = r["Properties"];
|
|
36616
|
+
const props = propsRaw && typeof propsRaw === "object" && !Array.isArray(propsRaw) ? propsRaw : r["Properties"] = {};
|
|
36617
|
+
props["TemplateURL"] = uploaded.url;
|
|
36618
|
+
modified = true;
|
|
36619
|
+
}
|
|
36620
|
+
}
|
|
36621
|
+
if (injectRetainPoliciesOnParsedResources(resources)) modified = true;
|
|
36391
36622
|
return {
|
|
36392
36623
|
body: stringifyCfnTemplate(parsed, format),
|
|
36393
36624
|
modified,
|
|
@@ -36395,28 +36626,56 @@ function injectRetainPolicies(templateBody, cfnStackName) {
|
|
|
36395
36626
|
};
|
|
36396
36627
|
}
|
|
36397
36628
|
/**
|
|
36398
|
-
*
|
|
36399
|
-
*
|
|
36400
|
-
*
|
|
36401
|
-
*
|
|
36402
|
-
* `
|
|
36403
|
-
* we can't add it ourselves either).
|
|
36629
|
+
* Recursive variant of {@link getCloudFormationResourceMapping} that returns
|
|
36630
|
+
* the full nested-stack tree rooted at `rootStackName`. For each
|
|
36631
|
+
* `AWS::CloudFormation::Stack` row in the root's resources, recursively
|
|
36632
|
+
* calls `DescribeStackResources(<child ARN>)` (AWS accepts the ARN as
|
|
36633
|
+
* `StackName`) to populate the child node, and so on to arbitrary depth.
|
|
36404
36634
|
*
|
|
36405
|
-
*
|
|
36406
|
-
*
|
|
36407
|
-
*
|
|
36408
|
-
*
|
|
36409
|
-
*
|
|
36410
|
-
*
|
|
36635
|
+
* Issue [#464](https://github.com/go-to-k/cdkd/issues/464): the recursive
|
|
36636
|
+
* `cdkd import --migrate-from-cloudformation` flow uses this once at the
|
|
36637
|
+
* top of the import command to drive BOTH (a) per-child state writes
|
|
36638
|
+
* under the v6 state-key shape `cdkd/<parent>~<child>/<region>/state.json`
|
|
36639
|
+
* AND (b) the recursive `injectRetainPoliciesRecursive` walk that retires
|
|
36640
|
+
* the whole tree on AWS without orphaning any resources.
|
|
36641
|
+
*
|
|
36642
|
+
* Children at every level are fetched in parallel via `Promise.all` so an
|
|
36643
|
+
* N-stack-wide tree only takes log(depth) round-trips of wall-clock time
|
|
36644
|
+
* instead of N — load-bearing when a CDK app spreads micro-services across
|
|
36645
|
+
* many nested children.
|
|
36646
|
+
*
|
|
36647
|
+
* Tree node `physicalId` is the AWS-side identifier accepted by every
|
|
36648
|
+
* CFn API call (`DescribeStackResources` / `GetTemplate`). For the root,
|
|
36649
|
+
* that's the user-supplied stack name; for nested children, the child
|
|
36650
|
+
* stack ARN.
|
|
36411
36651
|
*/
|
|
36412
|
-
async function
|
|
36413
|
-
|
|
36414
|
-
|
|
36652
|
+
async function getCloudFormationResourceTree(rootStackName, cfnClient) {
|
|
36653
|
+
return walkCfnStackTree(rootStackName, rootStackName, cfnClient);
|
|
36654
|
+
}
|
|
36655
|
+
async function walkCfnStackTree(stackName, physicalId, cfnClient) {
|
|
36656
|
+
const resp = await cfnClient.send(new DescribeStackResourcesCommand({ StackName: physicalId }));
|
|
36657
|
+
const resources = /* @__PURE__ */ new Map();
|
|
36658
|
+
const nestedChildren = [];
|
|
36415
36659
|
for (const r of resp.StackResources ?? []) {
|
|
36416
36660
|
if (!r.LogicalResourceId || !r.PhysicalResourceId) continue;
|
|
36417
|
-
|
|
36661
|
+
resources.set(r.LogicalResourceId, r.PhysicalResourceId);
|
|
36662
|
+
if (r.ResourceType === "AWS::CloudFormation::Stack") nestedChildren.push({
|
|
36663
|
+
logicalId: r.LogicalResourceId,
|
|
36664
|
+
childArn: r.PhysicalResourceId
|
|
36665
|
+
});
|
|
36418
36666
|
}
|
|
36419
|
-
|
|
36667
|
+
const childPairs = await Promise.all(nestedChildren.map(async ({ logicalId, childArn }) => ({
|
|
36668
|
+
logicalId,
|
|
36669
|
+
node: await walkCfnStackTree(childArn, childArn, cfnClient)
|
|
36670
|
+
})));
|
|
36671
|
+
const nested = /* @__PURE__ */ new Map();
|
|
36672
|
+
for (const { logicalId, node } of childPairs) nested.set(logicalId, node);
|
|
36673
|
+
return {
|
|
36674
|
+
stackName,
|
|
36675
|
+
physicalId,
|
|
36676
|
+
resources,
|
|
36677
|
+
nested
|
|
36678
|
+
};
|
|
36420
36679
|
}
|
|
36421
36680
|
async function confirmPrompt$2(prompt) {
|
|
36422
36681
|
const rl = readline.createInterface({
|
|
@@ -36499,26 +36758,42 @@ async function importCommand(stackArg, options) {
|
|
|
36499
36758
|
const resources = collectImportableResources(template);
|
|
36500
36759
|
const templateLogicalIds = new Set(resources.map((r) => r.logicalId));
|
|
36501
36760
|
logger.info(`Found ${resources.length} resource(s) in template`);
|
|
36761
|
+
let migrationTree;
|
|
36502
36762
|
if (migrationCfnStackName) {
|
|
36503
|
-
logger.info(`Resolving physical IDs from CloudFormation stack '${migrationCfnStackName}'...`);
|
|
36504
|
-
|
|
36763
|
+
logger.info(`Resolving physical IDs from CloudFormation stack '${migrationCfnStackName}' (recursive)...`);
|
|
36764
|
+
migrationTree = await getCloudFormationResourceTree(migrationCfnStackName, awsClients.cloudFormation);
|
|
36765
|
+
const cfnMapping = migrationTree.resources;
|
|
36505
36766
|
let derived = 0;
|
|
36506
36767
|
let skippedNonImportable = 0;
|
|
36768
|
+
let skippedNestedStackRow = 0;
|
|
36507
36769
|
for (const [logicalId, physicalId] of cfnMapping) {
|
|
36508
36770
|
if (!templateLogicalIds.has(logicalId)) {
|
|
36509
36771
|
skippedNonImportable++;
|
|
36510
36772
|
continue;
|
|
36511
36773
|
}
|
|
36774
|
+
if (template.Resources[logicalId]?.Type === "AWS::CloudFormation::Stack") {
|
|
36775
|
+
skippedNestedStackRow++;
|
|
36776
|
+
continue;
|
|
36777
|
+
}
|
|
36512
36778
|
if (!overrides.has(logicalId)) {
|
|
36513
36779
|
overrides.set(logicalId, physicalId);
|
|
36514
36780
|
derived++;
|
|
36515
36781
|
}
|
|
36516
36782
|
}
|
|
36517
|
-
const overriddenByUser = cfnMapping.size - derived - skippedNonImportable;
|
|
36783
|
+
const overriddenByUser = cfnMapping.size - derived - skippedNonImportable - skippedNestedStackRow;
|
|
36518
36784
|
const detail = [];
|
|
36519
36785
|
if (overriddenByUser > 0) detail.push(`${overriddenByUser} already overridden by --resource`);
|
|
36520
36786
|
if (skippedNonImportable > 0) detail.push(`${skippedNonImportable} non-importable (e.g. CDKMetadata)`);
|
|
36787
|
+
if (skippedNestedStackRow > 0) detail.push(`${skippedNestedStackRow} nested-stack row(s) handled separately`);
|
|
36521
36788
|
logger.info(`Resolved ${derived} physical ID(s) from CloudFormation` + (detail.length > 0 ? ` (${detail.join(", ")})` : ""));
|
|
36789
|
+
validateNestedStackShape(template, migrationTree, stackInfo.stackName, stackInfo.nestedTemplates ?? {});
|
|
36790
|
+
}
|
|
36791
|
+
let accountIdForNestedSynth;
|
|
36792
|
+
if (migrationTree && migrationTree.nested.size > 0) {
|
|
36793
|
+
const { GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
36794
|
+
const identity = await awsClients.sts.send(new GetCallerIdentityCommand({}));
|
|
36795
|
+
if (!identity.Account) throw new Error("STS GetCallerIdentity returned no Account — cdkd needs the account ID to synthesize cdkd-local ARNs for nested-stack rows. Verify the active AWS credentials are valid (e.g. `aws sts get-caller-identity`).");
|
|
36796
|
+
accountIdForNestedSynth = identity.Account;
|
|
36522
36797
|
}
|
|
36523
36798
|
const selectiveMode = overrides.size > 0 && !options.auto && !options.migrateFromCloudformation;
|
|
36524
36799
|
if (selectiveMode) logger.info(`Selective mode: only importing the ${overrides.size} resource(s) you listed (${[...overrides.keys()].join(", ")}). Pass --auto to also tag-import the rest.`);
|
|
@@ -36549,6 +36824,15 @@ async function importCommand(stackArg, options) {
|
|
|
36549
36824
|
});
|
|
36550
36825
|
continue;
|
|
36551
36826
|
}
|
|
36827
|
+
if (resource.Type === "AWS::CloudFormation::Stack" && migrationTree && migrationTree.nested.has(logicalId) && accountIdForNestedSynth) {
|
|
36828
|
+
rows.push({
|
|
36829
|
+
logicalId,
|
|
36830
|
+
resourceType: resource.Type,
|
|
36831
|
+
outcome: "imported",
|
|
36832
|
+
physicalId: synthesizeNestedStackArn(targetRegion, accountIdForNestedSynth, stackInfo.stackName, logicalId)
|
|
36833
|
+
});
|
|
36834
|
+
continue;
|
|
36835
|
+
}
|
|
36552
36836
|
const outcome = await importOne({
|
|
36553
36837
|
logicalId,
|
|
36554
36838
|
resource,
|
|
@@ -36590,6 +36874,19 @@ async function importCommand(stackArg, options) {
|
|
|
36590
36874
|
await stateBackend.saveState(stackInfo.stackName, targetRegion, stackState, saveOptions);
|
|
36591
36875
|
logger.info(`✓ State written: ${stackInfo.stackName} (${targetRegion})`);
|
|
36592
36876
|
logger.info(` ${importedRows.length} resource(s) imported. Run 'cdkd diff' to see how the imported state lines up with the template.`);
|
|
36877
|
+
if (migrationCfnStackName && migrationTree && accountIdForNestedSynth) await importNestedStackChildrenRecursive({
|
|
36878
|
+
parentStackName: stackInfo.stackName,
|
|
36879
|
+
parentRegion: targetRegion,
|
|
36880
|
+
parentNestedTemplates: stackInfo.nestedTemplates ?? {},
|
|
36881
|
+
parentTree: migrationTree,
|
|
36882
|
+
stateBackend,
|
|
36883
|
+
lockManager,
|
|
36884
|
+
providerRegistry,
|
|
36885
|
+
templateParser,
|
|
36886
|
+
lockOwner: owner,
|
|
36887
|
+
accountId: accountIdForNestedSynth,
|
|
36888
|
+
logger
|
|
36889
|
+
});
|
|
36593
36890
|
if (migrationCfnStackName) {
|
|
36594
36891
|
const orphaned = resources.length - importedRows.length;
|
|
36595
36892
|
if (orphaned > 0) logger.warn(`--migrate-from-cloudformation: ${orphaned} of ${resources.length} template resource(s) were NOT imported into cdkd. After the CloudFormation stack is retired, those resources remain in AWS but are unmanaged by both CloudFormation and cdkd.`);
|
|
@@ -36598,7 +36895,8 @@ async function importCommand(stackArg, options) {
|
|
|
36598
36895
|
cfnClient: awsClients.cloudFormation,
|
|
36599
36896
|
yes: options.yes,
|
|
36600
36897
|
stateBucket,
|
|
36601
|
-
...options.profile && { s3ClientOpts: { profile: options.profile } }
|
|
36898
|
+
...options.profile && { s3ClientOpts: { profile: options.profile } },
|
|
36899
|
+
...migrationTree && { resourceTree: migrationTree }
|
|
36602
36900
|
});
|
|
36603
36901
|
}
|
|
36604
36902
|
} finally {
|
|
@@ -37047,6 +37345,205 @@ async function captureObservedForImportedResources(stackState, providerRegistry,
|
|
|
37047
37345
|
}
|
|
37048
37346
|
}));
|
|
37049
37347
|
}
|
|
37348
|
+
/**
|
|
37349
|
+
* Synthesize the cdkd-local ARN that `NestedStackProvider.create` would write
|
|
37350
|
+
* for a nested-stack resource (design [docs/design/459-nested-stacks.md](../../../docs/design/459-nested-stacks.md)
|
|
37351
|
+
* §3, issue [#464](https://github.com/go-to-k/cdkd/issues/464) §6). Partition
|
|
37352
|
+
* `cdkd-local` is load-bearing — any consumer that misuses this value as a
|
|
37353
|
+
* real AWS ARN fails loudly with "Invalid ARN partition: cdkd-local" rather
|
|
37354
|
+
* than silently using a non-ARN string. The format MUST match
|
|
37355
|
+
* `NestedStackProvider.synthesizeArn` so an import-then-deploy cycle does
|
|
37356
|
+
* not surface phantom property changes on the nested-stack row.
|
|
37357
|
+
*/
|
|
37358
|
+
function synthesizeNestedStackArn(region, accountId, parentStackName, logicalId) {
|
|
37359
|
+
return `arn:cdkd-local:${region}:${accountId}:nested-stack/${parentStackName}/${logicalId}`;
|
|
37360
|
+
}
|
|
37361
|
+
/**
|
|
37362
|
+
* Validate the parent template ↔ AWS CFn tree shape consistency before any
|
|
37363
|
+
* destructive walk begins. Three failure modes are surfaced up front so
|
|
37364
|
+
* the user gets one clear error instead of a partial-success state file
|
|
37365
|
+
* graveyard:
|
|
37366
|
+
*
|
|
37367
|
+
* 1. Synth template has `AWS::CloudFormation::Stack` row at logical id X
|
|
37368
|
+
* but AWS tree has no child at X — likely the user added a new
|
|
37369
|
+
* nested child to the CDK code without running `cdk deploy` first.
|
|
37370
|
+
* 2. AWS tree has child at logical id Y but synth template has no
|
|
37371
|
+
* matching row — the user removed a nested child from the CDK code
|
|
37372
|
+
* but the live CFn stack still has it (or AWS removed it
|
|
37373
|
+
* mid-flight).
|
|
37374
|
+
* 3. Synth's `nestedTemplates` index is missing a path for some nested
|
|
37375
|
+
* row — usually means CDK 2.x didn't emit `Metadata['aws:asset:path']`
|
|
37376
|
+
* on the row (older CDK versions, or a hand-edited template). Without
|
|
37377
|
+
* the local template file we can't enumerate the child's resources
|
|
37378
|
+
* for the per-child state write.
|
|
37379
|
+
*
|
|
37380
|
+
* Mirrors the upstream `cdk import` mismatch UX — "import refuses on a
|
|
37381
|
+
* shape mismatch, fix the shape and re-run."
|
|
37382
|
+
*/
|
|
37383
|
+
function validateNestedStackShape(template, tree, parentStackName, nestedTemplates) {
|
|
37384
|
+
const templateNestedIds = /* @__PURE__ */ new Set();
|
|
37385
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) if (resource.Type === "AWS::CloudFormation::Stack") templateNestedIds.add(logicalId);
|
|
37386
|
+
const treeNestedIds = new Set(tree.nested.keys());
|
|
37387
|
+
const inTemplateMissingFromAws = [];
|
|
37388
|
+
for (const id of templateNestedIds) if (!treeNestedIds.has(id)) inTemplateMissingFromAws.push(id);
|
|
37389
|
+
const inAwsMissingFromTemplate = [];
|
|
37390
|
+
for (const id of treeNestedIds) if (!templateNestedIds.has(id)) inAwsMissingFromTemplate.push(id);
|
|
37391
|
+
const inTemplateMissingNestedTemplatePath = [];
|
|
37392
|
+
for (const id of templateNestedIds) if (!nestedTemplates[id]) inTemplateMissingNestedTemplatePath.push(id);
|
|
37393
|
+
const problems = [];
|
|
37394
|
+
if (inTemplateMissingFromAws.length > 0) problems.push(`template has nested-stack row(s) not present in CloudFormation: [${inTemplateMissingFromAws.join(", ")}] — run \`cdk deploy\` first so the AWS-side stack matches the synth template`);
|
|
37395
|
+
if (inAwsMissingFromTemplate.length > 0) problems.push(`CloudFormation has nested-child stack(s) not present in the synth template: [${inAwsMissingFromTemplate.join(", ")}] — the CDK code was edited to remove these children, but the live CFn stack still has them. Run \`cdk deploy\` to apply the removal, or revert the CDK edit`);
|
|
37396
|
+
if (inTemplateMissingNestedTemplatePath.length > 0) problems.push(`synth cloud assembly is missing nested-template asset paths for row(s) [${inTemplateMissingNestedTemplatePath.join(", ")}] — verify CDK 2.x \`cdk.NestedStack\` emits Metadata['aws:asset:path'] (default behavior)`);
|
|
37397
|
+
if (problems.length > 0) throw new Error(`cdkd import --migrate-from-cloudformation: parent stack '${parentStackName}' template ↔ CloudFormation shape mismatch:\n - ${problems.join("\n - ")}`);
|
|
37398
|
+
}
|
|
37399
|
+
/**
|
|
37400
|
+
* After the root parent stack's cdkd state is written, walk the
|
|
37401
|
+
* `migrationTree.nested` map and adopt every nested child into its own
|
|
37402
|
+
* v6-keyed state file (`cdkd/<parent>~<childLogicalId>/<region>/state.json`).
|
|
37403
|
+
* Recurses into grandchildren via the same walker.
|
|
37404
|
+
*
|
|
37405
|
+
* Per child:
|
|
37406
|
+
* 1. Acquire the child's lock. Order across the full tree is
|
|
37407
|
+
* **parent-first acquire, leaves-first release** (each level's
|
|
37408
|
+
* `finally` releases its child lock before sibling iteration
|
|
37409
|
+
* continues, and the root lock is the outermost — held by
|
|
37410
|
+
* `runImport`'s `try` / `finally`). This is the conventional
|
|
37411
|
+
* hierarchical lock pattern: parent-first acquire prevents a
|
|
37412
|
+
* second cdkd import from racing past the root lock; leaves-first
|
|
37413
|
+
* release on success/failure means a mid-walk error never strands
|
|
37414
|
+
* a child lock past its scope. Design §3.3 wording ("leaves first,
|
|
37415
|
+
* parent last") is preserved as the RELEASE order; the acquire
|
|
37416
|
+
* order is parent-first to keep the lock graph deadlock-free.
|
|
37417
|
+
* 2. Read the child template body from the synth cloud assembly via
|
|
37418
|
+
* `parentNestedTemplates[<childLogicalId>]` (populated by
|
|
37419
|
+
* AssemblyReader at synth time — see {@link AssemblyReader.parseStack}).
|
|
37420
|
+
* 3. Enumerate the child's importable resources (same filter as the
|
|
37421
|
+
* root's `collectImportableResources` — drop `AWS::CDK::Metadata`,
|
|
37422
|
+
* short-circuit `AWS::CloudFormation::Stack` rows to synth ARNs).
|
|
37423
|
+
* 4. For each child resource: dispatch through `importOne` with the
|
|
37424
|
+
* child's `(logicalId → physicalId)` overrides from `childTree.resources`.
|
|
37425
|
+
* 5. Build the child's `StackState` with `parentStack` / `parentLogicalId`
|
|
37426
|
+
* / `parentRegion` populated per state schema v6.
|
|
37427
|
+
* 6. Save state via `stateBackend.saveState('<parent>~<childLogicalId>', ...)`.
|
|
37428
|
+
* 7. Recurse into grandchildren.
|
|
37429
|
+
*
|
|
37430
|
+
* Lock release happens in REVERSE acquisition order in the outer
|
|
37431
|
+
* `finally` of each child — leaves-first acquire / parent-last release on
|
|
37432
|
+
* success, AND parent-last release on failure. Per memory rule
|
|
37433
|
+
* `feedback_destructive_state_test_coverage.md`, the lock-release order
|
|
37434
|
+
* is verified by unit test.
|
|
37435
|
+
*/
|
|
37436
|
+
async function importNestedStackChildrenRecursive(args) {
|
|
37437
|
+
const { parentStackName, parentRegion, parentNestedTemplates, parentTree, stateBackend, lockManager, providerRegistry, templateParser, lockOwner, accountId, logger } = args;
|
|
37438
|
+
for (const [childLogicalId, childTreeNode] of parentTree.nested) {
|
|
37439
|
+
const childStackName = `${parentStackName}~${childLogicalId}`;
|
|
37440
|
+
const childRegion = parentRegion;
|
|
37441
|
+
const childTemplatePath = parentNestedTemplates[childLogicalId];
|
|
37442
|
+
if (!childTemplatePath) throw new Error(`cdkd import --migrate-from-cloudformation: missing nested-template path for '${childLogicalId}' under parent '${parentStackName}' — validateNestedStackShape should have rejected this; please file a bug.`);
|
|
37443
|
+
logger.info(`Adopting nested stack '${childLogicalId}' as cdkd stack '${childStackName}' (${childRegion})...`);
|
|
37444
|
+
const childTemplate = readNestedChildTemplate(childTemplatePath, childLogicalId);
|
|
37445
|
+
await lockManager.acquireLock(childStackName, childRegion, lockOwner, "import");
|
|
37446
|
+
try {
|
|
37447
|
+
const childResources = collectImportableResources(childTemplate);
|
|
37448
|
+
const childTemplateLogicalIds = new Set(childResources.map((r) => r.logicalId));
|
|
37449
|
+
const childOverrides = /* @__PURE__ */ new Map();
|
|
37450
|
+
for (const [logicalId, physicalId] of childTreeNode.resources) {
|
|
37451
|
+
if (!childTemplateLogicalIds.has(logicalId)) continue;
|
|
37452
|
+
if (childTemplate.Resources[logicalId]?.Type === "AWS::CloudFormation::Stack") continue;
|
|
37453
|
+
childOverrides.set(logicalId, physicalId);
|
|
37454
|
+
}
|
|
37455
|
+
const rows = [];
|
|
37456
|
+
for (const { logicalId, resource } of childResources) {
|
|
37457
|
+
if (resource.Type === "AWS::CloudFormation::Stack" && childTreeNode.nested.has(logicalId)) {
|
|
37458
|
+
rows.push({
|
|
37459
|
+
logicalId,
|
|
37460
|
+
resourceType: resource.Type,
|
|
37461
|
+
outcome: "imported",
|
|
37462
|
+
physicalId: synthesizeNestedStackArn(childRegion, accountId, childStackName, logicalId)
|
|
37463
|
+
});
|
|
37464
|
+
continue;
|
|
37465
|
+
}
|
|
37466
|
+
const outcome = await importOne({
|
|
37467
|
+
logicalId,
|
|
37468
|
+
resource,
|
|
37469
|
+
stackName: childStackName,
|
|
37470
|
+
region: childRegion,
|
|
37471
|
+
providerRegistry,
|
|
37472
|
+
override: childOverrides.get(logicalId),
|
|
37473
|
+
overrides: childOverrides
|
|
37474
|
+
});
|
|
37475
|
+
rows.push(outcome);
|
|
37476
|
+
}
|
|
37477
|
+
const childStackState = buildStackState(childStackName, childRegion, rows, templateParser, childTemplate, null, false);
|
|
37478
|
+
childStackState.parentStack = parentStackName;
|
|
37479
|
+
childStackState.parentLogicalId = childLogicalId;
|
|
37480
|
+
childStackState.parentRegion = parentRegion;
|
|
37481
|
+
await resolveImportedProperties(childStackState, childTemplate, childRegion, stateBackend, logger);
|
|
37482
|
+
await captureObservedForImportedResources(childStackState, providerRegistry, logger);
|
|
37483
|
+
await stateBackend.saveState(childStackName, childRegion, childStackState);
|
|
37484
|
+
logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
|
|
37485
|
+
if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
|
|
37486
|
+
parentStackName: childStackName,
|
|
37487
|
+
parentRegion: childRegion,
|
|
37488
|
+
parentNestedTemplates: indexGrandchildTemplatePaths(childTemplate, childTemplatePath),
|
|
37489
|
+
parentTree: childTreeNode,
|
|
37490
|
+
stateBackend,
|
|
37491
|
+
lockManager,
|
|
37492
|
+
providerRegistry,
|
|
37493
|
+
templateParser,
|
|
37494
|
+
lockOwner,
|
|
37495
|
+
accountId,
|
|
37496
|
+
logger
|
|
37497
|
+
});
|
|
37498
|
+
} finally {
|
|
37499
|
+
await lockManager.releaseLock(childStackName, childRegion).catch((err) => {
|
|
37500
|
+
logger.warn(`Failed to release lock for nested stack '${childStackName}' (${childRegion}): ${err instanceof Error ? err.message : String(err)}`);
|
|
37501
|
+
});
|
|
37502
|
+
}
|
|
37503
|
+
}
|
|
37504
|
+
}
|
|
37505
|
+
/**
|
|
37506
|
+
* Read a nested child's template body from the synth cloud assembly's
|
|
37507
|
+
* sibling file (the path AssemblyReader populates from
|
|
37508
|
+
* `Metadata['aws:asset:path']`). Wraps the I/O + JSON parse failures with
|
|
37509
|
+
* an actionable error that names the offending child logical id.
|
|
37510
|
+
*/
|
|
37511
|
+
function readNestedChildTemplate(templatePath, childLogicalId) {
|
|
37512
|
+
let raw;
|
|
37513
|
+
try {
|
|
37514
|
+
raw = readFileSync(templatePath, "utf-8");
|
|
37515
|
+
} catch (err) {
|
|
37516
|
+
throw new Error(`Failed to read nested-stack template for '${childLogicalId}' at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
37517
|
+
}
|
|
37518
|
+
try {
|
|
37519
|
+
return JSON.parse(raw);
|
|
37520
|
+
} catch (err) {
|
|
37521
|
+
throw new Error(`Failed to parse nested-stack template for '${childLogicalId}' at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
37522
|
+
}
|
|
37523
|
+
}
|
|
37524
|
+
/**
|
|
37525
|
+
* Index a child template's `AWS::CloudFormation::Stack` rows by their
|
|
37526
|
+
* `aws:asset:path` Metadata, mirroring what {@link AssemblyReader.parseStack}
|
|
37527
|
+
* does for the root parent. Grandchild template files are sibling .nested
|
|
37528
|
+
* files of the child template file in the same cdk.out subdirectory.
|
|
37529
|
+
*
|
|
37530
|
+
* Refuses absolute paths for the same reason `NestedStackProvider.indexGrandchildTemplates`
|
|
37531
|
+
* does: an absolute path indicates the synth output was hand-modified or
|
|
37532
|
+
* generated by a non-CDK toolchain — `path.join(dir, '/abs/foo')` would
|
|
37533
|
+
* silently bypass our `dir` resolution and point outside cdk.out.
|
|
37534
|
+
*/
|
|
37535
|
+
function indexGrandchildTemplatePaths(childTemplate, childTemplatePath) {
|
|
37536
|
+
const dir = path.dirname(childTemplatePath);
|
|
37537
|
+
const result = {};
|
|
37538
|
+
for (const [grandLogicalId, resource] of Object.entries(childTemplate.Resources)) {
|
|
37539
|
+
if (resource.Type !== "AWS::CloudFormation::Stack") continue;
|
|
37540
|
+
const assetPath = resource.Metadata?.["aws:asset:path"];
|
|
37541
|
+
if (typeof assetPath !== "string" || assetPath.length === 0) continue;
|
|
37542
|
+
if (path.isAbsolute(assetPath)) throw new Error(`cdkd import --migrate-from-cloudformation: grandchild nested-stack '${grandLogicalId}' has Metadata['aws:asset:path']='${assetPath}' which is absolute. CDK emits relative asset paths for nested templates.`);
|
|
37543
|
+
result[grandLogicalId] = path.join(dir, assetPath);
|
|
37544
|
+
}
|
|
37545
|
+
return result;
|
|
37546
|
+
}
|
|
37050
37547
|
|
|
37051
37548
|
//#endregion
|
|
37052
37549
|
//#region src/cli/commands/local-state-loader.ts
|
|
@@ -55975,7 +56472,7 @@ function reorderArgs(argv) {
|
|
|
55975
56472
|
*/
|
|
55976
56473
|
async function main() {
|
|
55977
56474
|
const program = new Command();
|
|
55978
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
56475
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.146.1");
|
|
55979
56476
|
program.addCommand(createBootstrapCommand());
|
|
55980
56477
|
program.addCommand(createSynthCommand());
|
|
55981
56478
|
program.addCommand(createListCommand());
|