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