@go-to-k/cdkd 0.151.1 → 0.152.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -36677,6 +36677,29 @@ function flattenCdkdStateTreeLeafFirst(tree) {
36677
36677
  }
36678
36678
  }
36679
36679
  /**
36680
+ * Flatten a {@link CdkdStateStackTree} into a depth-first list of
36681
+ * `(stackName, region)` pairs in **root-first** order — the inverse of
36682
+ * {@link flattenCdkdStateTreeLeafFirst}. Used by the parameter-resolution
36683
+ * pre-pass in `runPerStackImportLoop` (issue #464 follow-up): each child's
36684
+ * intrinsic-valued Parameters resolve against its parent's already-resolved
36685
+ * Parameters, so the parent must be visited first.
36686
+ *
36687
+ * Exported so callers (orchestrator parameter pre-pass, unit tests) can
36688
+ * iterate the tree in the same order without re-implementing the walk.
36689
+ */
36690
+ function flattenCdkdStateTreeRootFirst(tree) {
36691
+ const out = [];
36692
+ walk(tree);
36693
+ return out;
36694
+ function walk(node) {
36695
+ out.push({
36696
+ stackName: node.stackName,
36697
+ region: node.region
36698
+ });
36699
+ for (const child of node.nestedChildren.values()) walk(child);
36700
+ }
36701
+ }
36702
+ /**
36680
36703
  * Map a cdkd stack name to a CloudFormation-compatible stack name.
36681
36704
  *
36682
36705
  * Required because the cdkd v6 state-key form `<parent>~<childLogicalId>`
@@ -37607,12 +37630,21 @@ async function collectImportFailureSummary(cfnClient, stackName) {
37607
37630
  * resolution. The tag accumulates across retries — harmless and easy to
37608
37631
  * reap manually under the `cdkd:` prefix.
37609
37632
  *
37633
+ * `parameters` must be the SAME Parameters the stack was just IMPORTed
37634
+ * with: a `UsePreviousTemplate: true` update on a stack that declares
37635
+ * Parameters (especially no-`Default` ones fed by a parent-side `Ref`)
37636
+ * is rejected by CFn with `Parameters: [X] must have values` unless every
37637
+ * Parameter is re-supplied. We re-send the resolved values verbatim (a
37638
+ * true no-op — same template, same params, only the flip Tag changes).
37639
+ * Empty array = the stack has no Parameters, so the field is omitted.
37640
+ *
37610
37641
  * Exported for unit testing.
37611
37642
  */
37612
- async function flipStackToUpdateComplete(cfnClient, cfnStackName) {
37643
+ async function flipStackToUpdateComplete(cfnClient, cfnStackName, parameters) {
37613
37644
  await cfnClient.send(new UpdateStackCommand({
37614
37645
  StackName: cfnStackName,
37615
37646
  UsePreviousTemplate: true,
37647
+ ...parameters.length > 0 && { Parameters: parameters },
37616
37648
  Tags: [{
37617
37649
  Key: "cdkd:nested-export-flip",
37618
37650
  Value: `${(/* @__PURE__ */ new Date()).toISOString()}-${randomUUID().slice(0, 8)}`
@@ -37664,15 +37696,16 @@ async function fetchCfnStackTemplate(cfnClient, cfnStackName) {
37664
37696
  * cdkd must forward those values — otherwise the child template's
37665
37697
  * `Parameters` block goes unresolved and CFn rejects the changeset.
37666
37698
  *
37667
- * Only literal-string Parameter values are forwarded today. Intrinsic
37668
- * values (`{Ref: ...}` / `{Fn::GetAtt: ...}` referencing the parent's
37669
- * own resources) are skipped with a `logger.warn`; if the child Parameter
37670
- * has no `Default`, CFn will reject the IMPORT with a clear error. The
37671
- * intrinsic-resolution path is a deferred follow-up (tracked under #464
37672
- * post-PR-B2 backlog) because resolving parent-side intrinsics at
37673
- * leaf-IMPORT time requires the deploy engine's full
37674
- * `IntrinsicFunctionResolver` against the parent's own (yet-to-be-IMPORTed)
37675
- * state not in PR B2's scope.
37699
+ * This helper only classifies literal-string / number / boolean values
37700
+ * (forwarded directly) vs. intrinsic values (`{Ref: ...}` /
37701
+ * `{Fn::GetAtt: ...}` referencing the parent's own resources), which it
37702
+ * reports in `intrinsicSkipped`. Intrinsic resolution itself is layered on
37703
+ * top by {@link resolveChildImportParameters}, which runs the deploy
37704
+ * engine's `IntrinsicFunctionResolver` against the parent's state and only
37705
+ * falls back to "skip" (the original behavior) when the resolver cannot
37706
+ * handle a value. Callers that want resolved Parameters should use
37707
+ * {@link resolveChildImportParameters}; this sync helper stays for the
37708
+ * literal-classification step and its existing unit tests.
37676
37709
  *
37677
37710
  * Exported for unit testing.
37678
37711
  */
@@ -37714,6 +37747,140 @@ function extractChildImportParameters(parentTemplate, childLogicalId) {
37714
37747
  };
37715
37748
  }
37716
37749
  /**
37750
+ * Async wrapper around {@link extractChildImportParameters} that resolves
37751
+ * intrinsic-valued (`{Ref: ...}` / `{Fn::GetAtt: ...}` / `Fn::Sub` etc.)
37752
+ * Parameters via the deploy engine's {@link IntrinsicFunctionResolver}
37753
+ * before falling back to the warn+skip path. This is the closing-loop
37754
+ * for the per-stack IMPORT design: CFn's atomic nested-stack create
37755
+ * implicitly resolved these against the parent's own scope, but cdkd's
37756
+ * leaf-first per-stack IMPORT loop submits each child as a standalone
37757
+ * stack — so AWS has no parent context to resolve against, and cdkd must
37758
+ * do the resolution itself.
37759
+ *
37760
+ * Behavior (Option 1 — CDK-compatible graceful degradation):
37761
+ * - For each entry that {@link extractChildImportParameters} reported as
37762
+ * `intrinsicSkipped`, look up the original intrinsic value in the
37763
+ * parent template and call `resolver.resolve(value, parentContext)`.
37764
+ * - On success: coerce the resolved scalar (string / number / boolean /
37765
+ * array-of-strings) to a CFn Parameter string and append to `params`.
37766
+ * - On throw OR on unsupported result shape (object / null / undefined):
37767
+ * keep the key in `intrinsicSkipped` — the orchestrator's existing
37768
+ * warn-then-fall-back-to-child-Default path takes over (matches the
37769
+ * pre-resolver behavior for unresolvable cases, so adding the resolver
37770
+ * never makes a working scenario regress).
37771
+ *
37772
+ * The `parentContext` must be built from the PARENT's data — its template,
37773
+ * its cdkd state's `resources` (for `Ref` to parent resources and
37774
+ * `Fn::GetAtt` to parent resource attributes), and its already-resolved
37775
+ * Parameter values (for `Ref` to parent Parameters). For nested grandparent
37776
+ * chains, the parent's resolved Parameters themselves come from the
37777
+ * top-down pre-pass in {@link buildResolvedParametersPerStack} — that's
37778
+ * why parameter resolution must happen ROOT-FIRST (parent before child),
37779
+ * even though the IMPORT loop itself is leaf-first.
37780
+ *
37781
+ * Exported for unit testing.
37782
+ */
37783
+ async function resolveChildImportParameters(parentTemplate, parentResolverContext, childLogicalId, resolver) {
37784
+ const base = extractChildImportParameters(parentTemplate, childLogicalId);
37785
+ if (base.intrinsicSkipped.length === 0) return base;
37786
+ const resources = parentTemplate["Resources"];
37787
+ if (!resources || typeof resources !== "object" || Array.isArray(resources)) return base;
37788
+ const row = resources[childLogicalId];
37789
+ if (!row || typeof row !== "object" || Array.isArray(row)) return base;
37790
+ const props = row.Properties;
37791
+ if (!props || typeof props !== "object" || Array.isArray(props)) return base;
37792
+ const rawParams = props.Parameters;
37793
+ if (!rawParams || typeof rawParams !== "object" || Array.isArray(rawParams)) return base;
37794
+ const resolvedParams = [...base.params];
37795
+ const stillSkipped = [];
37796
+ for (const key of base.intrinsicSkipped) {
37797
+ const intrinsicValue = rawParams[key];
37798
+ try {
37799
+ const result = await resolver.resolve(intrinsicValue, parentResolverContext);
37800
+ if (result === void 0 || result === null) {
37801
+ stillSkipped.push(key);
37802
+ continue;
37803
+ }
37804
+ if (typeof result === "string") resolvedParams.push({
37805
+ ParameterKey: key,
37806
+ ParameterValue: result
37807
+ });
37808
+ else if (typeof result === "number" || typeof result === "boolean") resolvedParams.push({
37809
+ ParameterKey: key,
37810
+ ParameterValue: String(result)
37811
+ });
37812
+ else if (Array.isArray(result)) resolvedParams.push({
37813
+ ParameterKey: key,
37814
+ ParameterValue: result.map((e) => String(e)).join(",")
37815
+ });
37816
+ else stillSkipped.push(key);
37817
+ } catch {
37818
+ stillSkipped.push(key);
37819
+ }
37820
+ }
37821
+ return {
37822
+ params: resolvedParams,
37823
+ intrinsicSkipped: stillSkipped
37824
+ };
37825
+ }
37826
+ /**
37827
+ * Top-down (root-first) pre-pass that resolves each cdkd stack's
37828
+ * child-IMPORT Parameters BEFORE the leaf-first IMPORT loop runs (issue
37829
+ * #464 follow-up — intrinsic Parameter resolution at leaf-IMPORT time).
37830
+ *
37831
+ * Why a separate pre-pass instead of resolving inline in the leaf-first
37832
+ * loop: a child's intrinsic-valued Parameter (`{Ref: ParentParam}`) resolves
37833
+ * against its PARENT's resolved Parameter values, and for a 3-level tree the
37834
+ * middle stack's Parameters are themselves resolved from the root's. So the
37835
+ * resolution dependency runs root → leaf, the opposite direction from the
37836
+ * IMPORT submission order (leaf → root). Resolving everything up front in
37837
+ * root-first order means each child always sees its parent's
37838
+ * already-resolved Parameters in `paramsByCdkdName`.
37839
+ *
37840
+ * Returns:
37841
+ * - `paramsByCdkdName`: cdkdName → the CFn Parameters to submit for that
37842
+ * stack's IMPORT. The root maps to `rootParameters` verbatim (CLI
37843
+ * overrides + template defaults, resolved by the caller already); each
37844
+ * non-root maps to its parent-extracted + intrinsic-resolved values.
37845
+ * - `intrinsicSkippedByCdkdName`: cdkdName → the Parameter names that
37846
+ * STILL could not be resolved (resolver threw / unsupported shape), for
37847
+ * the orchestrator's per-stack warn + aggregate summary.
37848
+ *
37849
+ * Exported for unit testing.
37850
+ */
37851
+ async function buildResolvedParametersPerStack(args) {
37852
+ const paramsByCdkdName = /* @__PURE__ */ new Map();
37853
+ const intrinsicSkippedByCdkdName = /* @__PURE__ */ new Map();
37854
+ paramsByCdkdName.set(args.rootStackName, args.rootParameters);
37855
+ const nodeByName = new Map(args.perStackNodes.map((n) => [n.cdkdName, n]));
37856
+ for (const ref of flattenCdkdStateTreeRootFirst(args.tree)) {
37857
+ if (ref.stackName === args.rootStackName) continue;
37858
+ const node = nodeByName.get(ref.stackName);
37859
+ if (!node) throw new Error(`buildResolvedParametersPerStack: tree node '${ref.stackName}' has no per-stack template/state entry. This is a cdkd bug.`);
37860
+ const parentLogicalId = node.state.parentLogicalId;
37861
+ const parentStackName = node.state.parentStack;
37862
+ if (!parentLogicalId || !parentStackName) throw new Error(`buildResolvedParametersPerStack: child '${node.cdkdName}' state is missing parentLogicalId / parentStack (v6 fields). Re-deploy or re-import the parent stack.`);
37863
+ const parentNode = nodeByName.get(parentStackName);
37864
+ if (!parentNode) throw new Error(`buildResolvedParametersPerStack: child '${node.cdkdName}' references parent '${parentStackName}' which is not in the per-stack node list — tree shape is inconsistent.`);
37865
+ const parentParamValues = {};
37866
+ for (const p of paramsByCdkdName.get(parentStackName) ?? []) if (p.ParameterKey !== void 0) parentParamValues[p.ParameterKey] = p.ParameterValue;
37867
+ const parentResolverContext = {
37868
+ template: parentNode.template,
37869
+ resources: parentNode.state.resources,
37870
+ parameters: parentParamValues,
37871
+ stackName: parentNode.cdkdName,
37872
+ ...args.stateBackend && { stateBackend: args.stateBackend }
37873
+ };
37874
+ const { params, intrinsicSkipped } = await resolveChildImportParameters(parentNode.template, parentResolverContext, parentLogicalId, args.resolver);
37875
+ paramsByCdkdName.set(node.cdkdName, params);
37876
+ if (intrinsicSkipped.length > 0) intrinsicSkippedByCdkdName.set(node.cdkdName, intrinsicSkipped);
37877
+ }
37878
+ return {
37879
+ paramsByCdkdName,
37880
+ intrinsicSkippedByCdkdName
37881
+ };
37882
+ }
37883
+ /**
37717
37884
  * Per-stack IMPORT loop driving `cdkd export` for nested-stack trees
37718
37885
  * (issue #464 PR B2). Submits one IMPORT changeset per cdkd-managed stack
37719
37886
  * in leaf-first order. For non-leaf parents, each `AWS::CloudFormation::Stack`
@@ -37854,26 +38021,25 @@ async function runPerStackImportLoop(args) {
37854
38021
  const cfnArnByCdkdName = /* @__PURE__ */ new Map();
37855
38022
  const uploadCleanups = [];
37856
38023
  const importedStacks = [];
37857
- const sessionIntrinsicSkipped = /* @__PURE__ */ new Map();
38024
+ const paramResolver = new IntrinsicFunctionResolver(rootRegion);
38025
+ const { paramsByCdkdName, intrinsicSkippedByCdkdName: sessionIntrinsicSkipped } = await buildResolvedParametersPerStack({
38026
+ rootStackName,
38027
+ rootParameters,
38028
+ perStackNodes: perStackPlans.map((p) => ({
38029
+ cdkdName: p.cdkdName,
38030
+ template: p.template,
38031
+ state: p.state
38032
+ })),
38033
+ tree,
38034
+ resolver: paramResolver,
38035
+ stateBackend: deps.stateBackend
38036
+ });
38037
+ for (const [cdkdName, skipped] of sessionIntrinsicSkipped) logger.warn(` Child '${cdkdName}': could not resolve intrinsic-valued Parameter(s) ${skipped.join(", ")} at IMPORT time. The child template's Parameter Default values must cover these — otherwise CFn will reject the IMPORT.`);
37858
38038
  try {
37859
38039
  for (let i = 0; i < perStackPlans.length; i++) {
37860
38040
  const plan = perStackPlans[i];
37861
38041
  logger.info(`[${i + 1}/${perStackPlans.length}] Importing cdkd stack '${plan.cdkdName}' → CFn stack '${plan.cfnName}' (${plan.phase1Imports.length} leaf, ${plan.nestedStackRows.length} nested-child adoption)`);
37862
- let stackParameters;
37863
- if (plan.cdkdName === rootStackName) stackParameters = rootParameters;
37864
- else {
37865
- const parentLogicalId = plan.state.parentLogicalId;
37866
- const parentStackName = plan.state.parentStack;
37867
- if (!parentLogicalId || !parentStackName) throw new Error(`runPerStackImportLoop: child '${plan.cdkdName}' state is missing parentLogicalId / parentStack (v6 fields). Re-deploy or re-import the parent stack to refresh.`);
37868
- const parentPlan = perStackPlans.find((p) => p.cdkdName === parentStackName);
37869
- if (!parentPlan) throw new Error(`runPerStackImportLoop: child '${plan.cdkdName}' references parent '${parentStackName}' which is not in the per-stack plan list — tree shape is inconsistent.`);
37870
- const extracted = extractChildImportParameters(parentPlan.template, parentLogicalId);
37871
- if (extracted.intrinsicSkipped.length > 0) {
37872
- sessionIntrinsicSkipped.set(plan.cdkdName, extracted.intrinsicSkipped);
37873
- logger.warn(` Child '${plan.cdkdName}': skipping intrinsic-valued Parameter(s) ${extracted.intrinsicSkipped.join(", ")} (intrinsic-resolution at leaf-IMPORT time is a deferred follow-up). The child template's Parameter Default values must cover these — otherwise CFn will reject the IMPORT.`);
37874
- }
37875
- stackParameters = extracted.params;
37876
- }
38042
+ const stackParameters = paramsByCdkdName.get(plan.cdkdName) ?? [];
37877
38043
  const phase1ATemplate = filterTemplateForImport(plan.template, plan.phase1Imports);
37878
38044
  const phase1AResources = plan.phase1Imports.map((entry) => ({
37879
38045
  ResourceType: entry.resourceType,
@@ -37900,7 +38066,7 @@ async function runPerStackImportLoop(args) {
37900
38066
  logger.info(` ✓ Phase 1A: CFn stack '${plan.cfnName}' created via IMPORT (${plan.phase1Imports.length} leaf resource(s)).`);
37901
38067
  if (plan.cdkdName !== rootStackName) {
37902
38068
  logger.info(` Flipping '${plan.cfnName}' to UPDATE_COMPLETE so it can be adopted as a nested member by its parent's Phase 1B...`);
37903
- await flipStackToUpdateComplete(deps.cfnClient, plan.cfnName);
38069
+ await flipStackToUpdateComplete(deps.cfnClient, plan.cfnName, stackParameters);
37904
38070
  }
37905
38071
  const rewrittenNestedRows = /* @__PURE__ */ new Map();
37906
38072
  if (plan.nestedStackRows.length > 0) {
@@ -37941,7 +38107,7 @@ async function runPerStackImportLoop(args) {
37941
38107
  logger.info(` ✓ Phase 1B: parent '${plan.cfnName}' adopted ${plan.nestedStackRows.length} nested child(ren) via UPDATE-IMPORT.`);
37942
38108
  if (plan.cdkdName !== rootStackName) {
37943
38109
  logger.info(` Flipping '${plan.cfnName}' back to UPDATE_COMPLETE so its own parent's Phase 1B can adopt it...`);
37944
- await flipStackToUpdateComplete(deps.cfnClient, plan.cfnName);
38110
+ await flipStackToUpdateComplete(deps.cfnClient, plan.cfnName, stackParameters);
37945
38111
  }
37946
38112
  }
37947
38113
  if (plan.recreateBeforePhase2.length > 0) for (const entry of plan.recreateBeforePhase2) {
@@ -37980,7 +38146,7 @@ async function runPerStackImportLoop(args) {
37980
38146
  }
37981
38147
  if (sessionIntrinsicSkipped.size > 0) {
37982
38148
  const detail = [...sessionIntrinsicSkipped.entries()].map(([cdkdName, params]) => `${cdkdName} (${params.join(", ")})`).join("; ");
37983
- logger.warn(`${sessionIntrinsicSkipped.size} stack(s) had intrinsic-valued Parameter(s) skipped at IMPORT time: ${detail}. Verify each child template's Parameter Default values cover these before the first 'cdk deploy' — intrinsic resolution at leaf-IMPORT time is a deferred follow-up.`);
38149
+ logger.warn(`${sessionIntrinsicSkipped.size} stack(s) had intrinsic-valued Parameter(s) that cdkd could not resolve at IMPORT time: ${detail}. Verify each child template's Parameter Default values cover these before the first 'cdk deploy'.`);
37984
38150
  }
37985
38151
  return {
37986
38152
  outcome: "success",
@@ -57875,7 +58041,7 @@ function reorderArgs(argv) {
57875
58041
  */
57876
58042
  async function main() {
57877
58043
  const program = new Command();
57878
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.151.1");
58044
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.152.0");
57879
58045
  program.addCommand(createBootstrapCommand());
57880
58046
  program.addCommand(createSynthCommand());
57881
58047
  program.addCommand(createListCommand());