@go-to-k/cdkd 0.87.0 → 0.89.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
@@ -79381,6 +79381,7 @@ async function exportCommand(stackArg, options) {
79381
79381
  let template;
79382
79382
  let resolvedStackName;
79383
79383
  let synthedRegion;
79384
+ let allSynthStacks = [];
79384
79385
  if (options.template) {
79385
79386
  if (!stackArg) {
79386
79387
  throw new Error(
@@ -79424,6 +79425,10 @@ async function exportCommand(stackArg, options) {
79424
79425
  template = stackInfo.template;
79425
79426
  resolvedStackName = stackInfo.stackName;
79426
79427
  synthedRegion = stackInfo.region;
79428
+ allSynthStacks = result.stacks.map((s) => ({
79429
+ stackName: s.stackName,
79430
+ template: s.template
79431
+ }));
79427
79432
  }
79428
79433
  const cfnStackName = options.cfnStackName ?? resolvedStackName;
79429
79434
  const targetRegion = await pickStackRegion2(
@@ -79512,19 +79517,55 @@ async function exportCommand(stackArg, options) {
79512
79517
  return;
79513
79518
  }
79514
79519
  }
79520
+ reportDriftBaselineGaps(state, logger);
79521
+ if (allSynthStacks.length > 0) {
79522
+ const crossRefs = scanCrossStackReferences(allSynthStacks, resolvedStackName);
79523
+ if (crossRefs.length > 0) {
79524
+ const lines = crossRefs.map(
79525
+ (r) => ` ${r.consumerStackName} \u2192 ${resolvedStackName}.${r.outputName} at ${r.location}`
79526
+ );
79527
+ if (options.strictCrossStack) {
79528
+ throw new Error(
79529
+ `Refusing to export: ${crossRefs.length} cross-stack reference(s) to ${resolvedStackName} found in sibling stacks. After migration, those references will break (cdkd's Fn::GetStackOutput reads cdkd state; the migrated stack's outputs live in CFn). Migrate consumers first, or remove the references, or drop --strict-cross-stack to proceed with a warning:
79530
+ ` + lines.join("\n")
79531
+ );
79532
+ }
79533
+ logger.warn(
79534
+ `${crossRefs.length} cross-stack reference(s) to '${resolvedStackName}' from sibling stacks. These will break the next time those stacks deploy via cdkd (cdkd's Fn::GetStackOutput resolver reads cdkd state; the migrated stack's outputs are now in CFn). Plan multi-stack migrations from the leaves up.`
79535
+ );
79536
+ for (const line of lines)
79537
+ logger.warn(line);
79538
+ }
79539
+ }
79540
+ const userParameters = parseParameterOverrides(options.parameter);
79541
+ const { parameters: cfnParameters, missing } = resolveTemplateParameters(
79542
+ template,
79543
+ userParameters
79544
+ );
79545
+ if (missing.length > 0) {
79546
+ throw new Error(
79547
+ `Template requires parameter(s) without defaults: ${missing.join(", ")}. Pass each one as --parameter Key=Value (or set a Default in the CDK code).`
79548
+ );
79549
+ }
79515
79550
  const phase1Template = filterTemplateForImport(template, phase1Imports);
79516
79551
  await executeImportChangeSet(
79517
79552
  awsClients.cloudFormation,
79518
79553
  cfnStackName,
79519
79554
  phase1Template,
79520
- phase1Imports
79555
+ phase1Imports,
79556
+ cfnParameters
79521
79557
  );
79522
79558
  logger.info(
79523
79559
  `\u2713 Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`
79524
79560
  );
79525
79561
  if (phase2Creates.length > 0) {
79526
79562
  try {
79527
- await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, template);
79563
+ await executeUpdateChangeSet(
79564
+ awsClients.cloudFormation,
79565
+ cfnStackName,
79566
+ template,
79567
+ cfnParameters
79568
+ );
79528
79569
  logger.info(`\u2713 Phase 2: ${phase2Creates.length} non-importable resource(s) CREATEd.`);
79529
79570
  } catch (err) {
79530
79571
  const msg = err instanceof Error ? err.message : String(err);
@@ -79762,6 +79803,64 @@ async function fetchPrimaryIdentifier(resourceType, cfnClient) {
79762
79803
  `primary identifier unknown (DescribeType returned no usable schema and no fallback is registered). Add ${resourceType} to PRIMARY_IDENTIFIER_FALLBACK in export.ts, or open an issue.`
79763
79804
  );
79764
79805
  }
79806
+ function parseParameterOverrides(tokens) {
79807
+ const map = {};
79808
+ if (!tokens)
79809
+ return map;
79810
+ for (const t of tokens) {
79811
+ const eq = t.indexOf("=");
79812
+ if (eq < 1) {
79813
+ throw new Error(
79814
+ `Invalid --parameter '${t}': expected 'Key=Value' (e.g. --parameter Env=prod)`
79815
+ );
79816
+ }
79817
+ const key = t.slice(0, eq).trim();
79818
+ const value = t.slice(eq + 1);
79819
+ if (!key) {
79820
+ throw new Error(`Invalid --parameter '${t}': key is empty`);
79821
+ }
79822
+ map[key] = value;
79823
+ }
79824
+ return map;
79825
+ }
79826
+ function resolveTemplateParameters(template, userOverrides) {
79827
+ const tplParams = template["Parameters"];
79828
+ if (!tplParams || typeof tplParams !== "object" || Array.isArray(tplParams)) {
79829
+ const stray = Object.keys(userOverrides);
79830
+ if (stray.length > 0) {
79831
+ throw new Error(
79832
+ `--parameter override(s) supplied (${stray.join(", ")}) but template has no Parameters section.`
79833
+ );
79834
+ }
79835
+ return { parameters: [], missing: [] };
79836
+ }
79837
+ const parameters = [];
79838
+ const missing = [];
79839
+ const known = /* @__PURE__ */ new Set();
79840
+ for (const [name, raw] of Object.entries(tplParams)) {
79841
+ known.add(name);
79842
+ const def = raw ?? {};
79843
+ const override = userOverrides[name];
79844
+ if (override !== void 0) {
79845
+ parameters.push({ ParameterKey: name, ParameterValue: override });
79846
+ continue;
79847
+ }
79848
+ if ("Default" in def) {
79849
+ const value = typeof def.Default === "string" ? def.Default : String(def.Default);
79850
+ parameters.push({ ParameterKey: name, ParameterValue: value });
79851
+ continue;
79852
+ }
79853
+ missing.push(name);
79854
+ }
79855
+ for (const name of Object.keys(userOverrides)) {
79856
+ if (!known.has(name)) {
79857
+ throw new Error(
79858
+ `--parameter override '${name}' does not match any parameter in the synthesized template (template declares: ${[...known].join(", ") || "(none)"})`
79859
+ );
79860
+ }
79861
+ }
79862
+ return { parameters, missing };
79863
+ }
79765
79864
  function filterTemplateForImport(template, plan) {
79766
79865
  const allow = new Set(plan.map((p) => p.logicalId));
79767
79866
  const original = template["Resources"];
@@ -79788,6 +79887,76 @@ function filterTemplateForImport(template, plan) {
79788
79887
  }
79789
79888
  return result;
79790
79889
  }
79890
+ function reportDriftBaselineGaps(state, logger) {
79891
+ const entries = Object.entries(state.resources ?? {});
79892
+ if (entries.length === 0)
79893
+ return;
79894
+ const missing = entries.filter(([, r]) => r.observedProperties === void 0);
79895
+ if (missing.length === 0)
79896
+ return;
79897
+ if (state.version !== void 0 && state.version < 3) {
79898
+ logger.warn(
79899
+ `cdkd state schema is v${state.version} (pre-observedProperties). cdkd drift cannot reliably compare against AWS for this stack; the next \`cdk deploy\` after migration may surface spurious changes if AWS has drifted from the template. Run \`cdkd state refresh-observed ${state.stackName}\` (or any redeploy) before export to capture an AWS-current baseline.`
79900
+ );
79901
+ return;
79902
+ }
79903
+ logger.warn(
79904
+ `${missing.length} of ${entries.length} resource(s) in cdkd state lack an AWS-current baseline (observedProperties). cdkd drift may produce false positives for them; the next \`cdk deploy\` after migration may surface unexpected changes. Run \`cdkd state refresh-observed ${state.stackName}\` to capture a baseline before export, then \`cdkd drift\` to verify the stack matches AWS.`
79905
+ );
79906
+ for (const [logicalId] of missing.slice(0, 10)) {
79907
+ logger.warn(` ${logicalId}`);
79908
+ }
79909
+ if (missing.length > 10) {
79910
+ logger.warn(` ... and ${missing.length - 10} more`);
79911
+ }
79912
+ }
79913
+ function scanCrossStackReferences(stacks, exportingStackName) {
79914
+ const refs = [];
79915
+ for (const stack of stacks) {
79916
+ if (stack.stackName === exportingStackName)
79917
+ continue;
79918
+ walkForGetStackOutput(stack.template, "", (ref) => {
79919
+ if (ref.stackName === exportingStackName) {
79920
+ refs.push({
79921
+ consumerStackName: stack.stackName,
79922
+ outputName: ref.outputName,
79923
+ location: ref.location
79924
+ });
79925
+ }
79926
+ });
79927
+ }
79928
+ return refs;
79929
+ }
79930
+ function walkForGetStackOutput(node, path3, emit) {
79931
+ if (!node || typeof node !== "object")
79932
+ return;
79933
+ if (Array.isArray(node)) {
79934
+ node.forEach((item, i) => walkForGetStackOutput(item, `${path3}[${i}]`, emit));
79935
+ return;
79936
+ }
79937
+ const obj = node;
79938
+ const intrinsic = obj["Fn::GetStackOutput"];
79939
+ if (intrinsic !== void 0) {
79940
+ if (intrinsic && typeof intrinsic === "object" && !Array.isArray(intrinsic)) {
79941
+ const i = intrinsic;
79942
+ const stackName = typeof i["StackName"] === "string" ? i["StackName"] : void 0;
79943
+ const outputName = typeof i["OutputName"] === "string" ? i["OutputName"] : void 0;
79944
+ if (stackName && outputName) {
79945
+ emit({ stackName, outputName, location: path3 });
79946
+ }
79947
+ } else if (Array.isArray(intrinsic) && intrinsic.length === 2) {
79948
+ const arr = intrinsic;
79949
+ const stackName = arr[0];
79950
+ const outputName = arr[1];
79951
+ if (typeof stackName === "string" && typeof outputName === "string") {
79952
+ emit({ stackName, outputName, location: path3 });
79953
+ }
79954
+ }
79955
+ }
79956
+ for (const [key, value] of Object.entries(obj)) {
79957
+ walkForGetStackOutput(value, path3 ? `${path3}.${key}` : key, emit);
79958
+ }
79959
+ }
79791
79960
  function referencesOnly(node, allow) {
79792
79961
  if (!node || typeof node !== "object")
79793
79962
  return true;
@@ -79821,7 +79990,7 @@ function printPlan(plan, cfnStackName) {
79821
79990
  }
79822
79991
  logger.info("");
79823
79992
  }
79824
- async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79993
+ async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters) {
79825
79994
  const logger = getLogger();
79826
79995
  const changeSetName = `cdkd-migrate-${Date.now()}`;
79827
79996
  const templateBody = JSON.stringify(template, null, 2);
@@ -79846,6 +80015,7 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79846
80015
  ChangeSetType: "IMPORT",
79847
80016
  TemplateBody: templateBody,
79848
80017
  ResourcesToImport: resourcesToImport,
80018
+ ...parameters.length > 0 && { Parameters: parameters },
79849
80019
  // CDK templates routinely require CAPABILITY_IAM /
79850
80020
  // CAPABILITY_NAMED_IAM. Forward both so the user does not have to
79851
80021
  // re-discover and re-pass them.
@@ -79892,7 +80062,7 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan) {
79892
80062
  throw err;
79893
80063
  }
79894
80064
  }
79895
- async function executeUpdateChangeSet(cfnClient, stackName, template) {
80065
+ async function executeUpdateChangeSet(cfnClient, stackName, template, parameters) {
79896
80066
  const logger = getLogger();
79897
80067
  const changeSetName = `cdkd-phase2-${Date.now()}`;
79898
80068
  const templateBody = JSON.stringify(template, null, 2);
@@ -79911,6 +80081,7 @@ async function executeUpdateChangeSet(cfnClient, stackName, template) {
79911
80081
  ChangeSetName: changeSetName,
79912
80082
  ChangeSetType: "UPDATE",
79913
80083
  TemplateBody: templateBody,
80084
+ ...parameters.length > 0 && { Parameters: parameters },
79914
80085
  Capabilities: ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
79915
80086
  })
79916
80087
  );
@@ -80027,6 +80198,13 @@ function createExportCommand() {
80027
80198
  "--include-non-importable",
80028
80199
  "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.",
80029
80200
  false
80201
+ ).option(
80202
+ "--parameter <key=value...>",
80203
+ "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value."
80204
+ ).option(
80205
+ "--strict-cross-stack",
80206
+ "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns but proceeds \u2014 the user is expected to migrate the consumer stacks in a follow-up.",
80207
+ false
80030
80208
  ).action(withErrorHandling(exportCommand));
80031
80209
  [...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
80032
80210
  (opt) => cmd.addOption(opt)
@@ -80065,7 +80243,7 @@ function reorderArgs(argv) {
80065
80243
  }
80066
80244
  async function main() {
80067
80245
  const program = new Command18();
80068
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.87.0");
80246
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.89.0");
80069
80247
  program.addCommand(createBootstrapCommand());
80070
80248
  program.addCommand(createSynthCommand());
80071
80249
  program.addCommand(createListCommand());