@go-to-k/cdkd 0.220.4 → 0.221.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
@@ -1553,7 +1553,7 @@ const FLUSH_INTERVAL_MS = 2e3;
1553
1553
  const FLUSH_EVENT_THRESHOLD = 50;
1554
1554
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
1555
1555
  function getCdkdVersion() {
1556
- return "0.220.4";
1556
+ return "0.221.0";
1557
1557
  }
1558
1558
  /**
1559
1559
  * Generate a time-sortable unique run id, e.g.
@@ -19738,18 +19738,175 @@ var ECSProvider = class {
19738
19738
  };
19739
19739
  }
19740
19740
  /**
19741
- * Convert CFn Volumes to ECS SDK format
19741
+ * Convert CFn Volumes to ECS SDK format.
19742
+ *
19743
+ * Every nested volume-configuration block is PascalCase in the CFn
19744
+ * template and camelCase in the ECS SDK input, so each sub-block runs
19745
+ * through a dedicated converter — the same PascalCase->camelCase trap
19746
+ * already fixed for the ContainerDefinitions sub-arrays
19747
+ * (convertEnvironment / convertSecrets / convertMountPoints etc.).
19748
+ * Before issue #815, `Host` / `EFSVolumeConfiguration` were cast through
19749
+ * raw (so their nested keys reached the SDK still PascalCase) and
19750
+ * `DockerVolumeConfiguration` / `FSxWindowsFileServerVolumeConfiguration`
19751
+ * were not mapped at all (silently dropped).
19742
19752
  */
19743
19753
  convertVolumes(volumes) {
19744
19754
  if (!volumes) return void 0;
19745
19755
  return volumes.map((v) => ({
19746
19756
  name: v["Name"],
19747
- host: v["Host"],
19748
- efsVolumeConfiguration: v["EFSVolumeConfiguration"],
19757
+ host: this.convertVolumeHost(v["Host"]),
19758
+ dockerVolumeConfiguration: this.convertDockerVolumeConfiguration(v["DockerVolumeConfiguration"]),
19759
+ efsVolumeConfiguration: this.convertEFSVolumeConfiguration(v["EFSVolumeConfiguration"]),
19760
+ fsxWindowsFileServerVolumeConfiguration: this.convertFSxWindowsVolumeConfiguration(v["FSxWindowsFileServerVolumeConfiguration"]),
19749
19761
  configuredAtLaunch: this.coerceBool(v["ConfiguredAtLaunch"])
19750
19762
  }));
19751
19763
  }
19752
19764
  /**
19765
+ * Convert CFn Volumes[].Host to ECS SDK format.
19766
+ * CFn: `{SourcePath}` -> SDK: `{sourcePath}`.
19767
+ */
19768
+ convertVolumeHost(host) {
19769
+ if (!host) return void 0;
19770
+ return { sourcePath: host["SourcePath"] };
19771
+ }
19772
+ /**
19773
+ * Convert CFn Volumes[].DockerVolumeConfiguration to ECS SDK format.
19774
+ * CFn: `{Scope, Autoprovision, Driver, DriverOpts, Labels}`
19775
+ * -> SDK: `{scope, autoprovision, driver, driverOpts, labels}`.
19776
+ */
19777
+ convertDockerVolumeConfiguration(config) {
19778
+ if (!config) return void 0;
19779
+ return {
19780
+ scope: config["Scope"],
19781
+ autoprovision: this.coerceBool(config["Autoprovision"]),
19782
+ driver: config["Driver"],
19783
+ driverOpts: config["DriverOpts"],
19784
+ labels: config["Labels"]
19785
+ };
19786
+ }
19787
+ /**
19788
+ * Convert CFn Volumes[].EFSVolumeConfiguration to ECS SDK format.
19789
+ * CFn: `{FilesystemId, RootDirectory, TransitEncryption,
19790
+ * TransitEncryptionPort, AuthorizationConfig}`
19791
+ * -> SDK: `{fileSystemId, rootDirectory, transitEncryption,
19792
+ * transitEncryptionPort, authorizationConfig}`.
19793
+ * Note the CFn property is `FilesystemId` (lowercase `s`) while the SDK
19794
+ * field is `fileSystemId` — they are not a simple first-letter case flip.
19795
+ */
19796
+ convertEFSVolumeConfiguration(config) {
19797
+ if (!config) return void 0;
19798
+ return {
19799
+ fileSystemId: config["FilesystemId"],
19800
+ rootDirectory: config["RootDirectory"],
19801
+ transitEncryption: config["TransitEncryption"],
19802
+ transitEncryptionPort: config["TransitEncryptionPort"] !== void 0 ? Number(config["TransitEncryptionPort"]) : void 0,
19803
+ authorizationConfig: this.convertEFSAuthorizationConfig(config["AuthorizationConfig"])
19804
+ };
19805
+ }
19806
+ /**
19807
+ * Convert CFn EFSVolumeConfiguration.AuthorizationConfig to ECS SDK format.
19808
+ * CFn: `{AccessPointId, IAM}` -> SDK: `{accessPointId, iam}`.
19809
+ * Note the CFn key is `IAM` (all caps), NOT `Iam` — not a simple
19810
+ * first-letter case flip (verified against the CDK L1 `IAM` mapping).
19811
+ */
19812
+ convertEFSAuthorizationConfig(config) {
19813
+ if (!config) return void 0;
19814
+ return {
19815
+ accessPointId: config["AccessPointId"],
19816
+ iam: config["IAM"]
19817
+ };
19818
+ }
19819
+ /**
19820
+ * Convert CFn Volumes[].FSxWindowsFileServerVolumeConfiguration to ECS
19821
+ * SDK format.
19822
+ * CFn: `{FileSystemId, RootDirectory, AuthorizationConfig}`
19823
+ * -> SDK: `{fileSystemId, rootDirectory, authorizationConfig}`.
19824
+ */
19825
+ convertFSxWindowsVolumeConfiguration(config) {
19826
+ if (!config) return void 0;
19827
+ return {
19828
+ fileSystemId: config["FileSystemId"],
19829
+ rootDirectory: config["RootDirectory"],
19830
+ authorizationConfig: this.convertFSxWindowsAuthorizationConfig(config["AuthorizationConfig"])
19831
+ };
19832
+ }
19833
+ /**
19834
+ * Convert CFn FSxWindowsFileServerVolumeConfiguration.AuthorizationConfig
19835
+ * to ECS SDK format.
19836
+ * CFn: `{CredentialsParameter, Domain}`
19837
+ * -> SDK: `{credentialsParameter, domain}`.
19838
+ */
19839
+ convertFSxWindowsAuthorizationConfig(config) {
19840
+ if (!config) return void 0;
19841
+ return {
19842
+ credentialsParameter: config["CredentialsParameter"],
19843
+ domain: config["Domain"]
19844
+ };
19845
+ }
19846
+ /**
19847
+ * Convert the camelCase SDK `volumes` shape returned by
19848
+ * DescribeTaskDefinition back to the PascalCase CFn template form, so the
19849
+ * `readCurrentState` snapshot matches the deploy-time template
19850
+ * representation for drift comparison (issue #815). Only volume keys
19851
+ * present on the SDK side are emitted, so a future field cdkd does not
19852
+ * map cannot surface as phantom drift. TaskDefinitions are immutable
19853
+ * replace-only today, so this is forward-looking normalization.
19854
+ */
19855
+ volumesToCfn(volumes) {
19856
+ if (!volumes) return [];
19857
+ return volumes.map((v) => {
19858
+ const out = {};
19859
+ if (v.name !== void 0) out["Name"] = v.name;
19860
+ if (v.host !== void 0) {
19861
+ const host = {};
19862
+ if (v.host.sourcePath !== void 0) host["SourcePath"] = v.host.sourcePath;
19863
+ out["Host"] = host;
19864
+ }
19865
+ if (v.dockerVolumeConfiguration !== void 0) {
19866
+ const d = v.dockerVolumeConfiguration;
19867
+ const docker = {};
19868
+ if (d.scope !== void 0) docker["Scope"] = d.scope;
19869
+ if (d.autoprovision !== void 0) docker["Autoprovision"] = d.autoprovision;
19870
+ if (d.driver !== void 0) docker["Driver"] = d.driver;
19871
+ if (d.driverOpts !== void 0) docker["DriverOpts"] = d.driverOpts;
19872
+ if (d.labels !== void 0) docker["Labels"] = d.labels;
19873
+ out["DockerVolumeConfiguration"] = docker;
19874
+ }
19875
+ if (v.efsVolumeConfiguration !== void 0) {
19876
+ const e = v.efsVolumeConfiguration;
19877
+ const efs = {};
19878
+ if (e.fileSystemId !== void 0) efs["FilesystemId"] = e.fileSystemId;
19879
+ if (e.rootDirectory !== void 0) efs["RootDirectory"] = e.rootDirectory;
19880
+ if (e.transitEncryption !== void 0) efs["TransitEncryption"] = e.transitEncryption;
19881
+ if (e.transitEncryptionPort !== void 0) efs["TransitEncryptionPort"] = e.transitEncryptionPort;
19882
+ if (e.authorizationConfig !== void 0) {
19883
+ const a = e.authorizationConfig;
19884
+ const auth = {};
19885
+ if (a.accessPointId !== void 0) auth["AccessPointId"] = a.accessPointId;
19886
+ if (a.iam !== void 0) auth["IAM"] = a.iam;
19887
+ efs["AuthorizationConfig"] = auth;
19888
+ }
19889
+ out["EFSVolumeConfiguration"] = efs;
19890
+ }
19891
+ if (v.fsxWindowsFileServerVolumeConfiguration !== void 0) {
19892
+ const f = v.fsxWindowsFileServerVolumeConfiguration;
19893
+ const fsx = {};
19894
+ if (f.fileSystemId !== void 0) fsx["FileSystemId"] = f.fileSystemId;
19895
+ if (f.rootDirectory !== void 0) fsx["RootDirectory"] = f.rootDirectory;
19896
+ if (f.authorizationConfig !== void 0) {
19897
+ const a = f.authorizationConfig;
19898
+ const auth = {};
19899
+ if (a.credentialsParameter !== void 0) auth["CredentialsParameter"] = a.credentialsParameter;
19900
+ if (a.domain !== void 0) auth["Domain"] = a.domain;
19901
+ fsx["AuthorizationConfig"] = auth;
19902
+ }
19903
+ out["FSxWindowsFileServerVolumeConfiguration"] = fsx;
19904
+ }
19905
+ if (v.configuredAtLaunch !== void 0) out["ConfiguredAtLaunch"] = v.configuredAtLaunch;
19906
+ return out;
19907
+ });
19908
+ }
19909
+ /**
19753
19910
  * Coerce a CFn boolean property to a real boolean at the wire boundary.
19754
19911
  * CFn templates can carry booleans as the strings "true" / "false"
19755
19912
  * (e.g. via Fn::Sub / parameter plumbing), so the SDK input must
@@ -19928,7 +20085,7 @@ var ECSProvider = class {
19928
20085
  result["RequiresCompatibilities"] = td.requiresCompatibilities ? [...td.requiresCompatibilities] : [];
19929
20086
  if (td.executionRoleArn !== void 0) result["ExecutionRoleArn"] = td.executionRoleArn;
19930
20087
  if (td.taskRoleArn !== void 0) result["TaskRoleArn"] = td.taskRoleArn;
19931
- result["Volumes"] = td.volumes ?? [];
20088
+ result["Volumes"] = this.volumesToCfn(td.volumes);
19932
20089
  result["PlacementConstraints"] = td.placementConstraints ?? [];
19933
20090
  if (td.runtimePlatform) result["RuntimePlatform"] = td.runtimePlatform;
19934
20091
  if (td.proxyConfiguration) result["ProxyConfiguration"] = td.proxyConfiguration;
@@ -34259,7 +34416,8 @@ async function runDestroyForStack(stackName, state, ctx) {
34259
34416
  skippedEmpty: false,
34260
34417
  deletedCount: 0,
34261
34418
  retainedCount: 0,
34262
- errorCount: 0
34419
+ errorCount: 0,
34420
+ interrupted: false
34263
34421
  };
34264
34422
  const resourceCount = Object.keys(state.resources).length;
34265
34423
  const regionForState = state.region ?? ctx.baseRegion;
@@ -34352,6 +34510,20 @@ async function runDestroyForStack(stackName, state, ctx) {
34352
34510
  };
34353
34511
  const renderer = getLiveRenderer();
34354
34512
  renderer.start();
34513
+ let draining = false;
34514
+ const sigintHandler = () => {
34515
+ if (draining) {
34516
+ ctx.lockManager.releaseLock(stackName, regionForState).catch(() => {});
34517
+ process.stderr.write(`\nForce-quit: stack lock may not be released. If the next run reports a lock, run: cdkd force-unlock ${stackName}\n`);
34518
+ process.exit(130);
34519
+ }
34520
+ draining = true;
34521
+ renderer.printAbove(() => {
34522
+ process.stderr.write("\nInterrupted — finishing in-flight deletes, then flushing state and releasing the lock (press Ctrl-C again to force-quit)...\n");
34523
+ });
34524
+ };
34525
+ process.setMaxListeners(Math.max(process.getMaxListeners(), 100));
34526
+ process.on("SIGINT", sigintHandler);
34355
34527
  try {
34356
34528
  logger.info("Building dependency graph...");
34357
34529
  const template = {
@@ -34393,11 +34565,16 @@ async function runDestroyForStack(stackName, state, ctx) {
34393
34565
  const executionLevels = dagBuilder.getExecutionLevels(graph);
34394
34566
  logger.debug(`Dependency graph: ${executionLevels.length} level(s)`);
34395
34567
  for (let levelIndex = executionLevels.length - 1; levelIndex >= 0; levelIndex--) {
34568
+ if (draining) {
34569
+ logger.debug("Interrupted (draining) — not scheduling further deletion levels");
34570
+ break;
34571
+ }
34396
34572
  const level = executionLevels[levelIndex];
34397
34573
  if (!level) continue;
34398
34574
  logger.debug(`Deletion level ${executionLevels.length - levelIndex}/${executionLevels.length} (${level.length} resources)`);
34399
34575
  const stackRegion = state.region ?? ctx.baseRegion;
34400
34576
  const deletePromises = level.map(async (logicalId) => {
34577
+ if (draining) return;
34401
34578
  const resource = state.resources[logicalId];
34402
34579
  if (!resource) {
34403
34580
  logger.warn(`Resource ${logicalId} not found in state, skipping`);
@@ -34534,8 +34711,10 @@ async function runDestroyForStack(stackName, state, ctx) {
34534
34711
  });
34535
34712
  await Promise.all(deletePromises);
34536
34713
  }
34714
+ result.interrupted = draining;
34537
34715
  await saveChain;
34538
- if (result.errorCount === 0) {
34716
+ const preserveState = result.errorCount > 0 || result.interrupted;
34717
+ if (!preserveState) {
34539
34718
  await ctx.stateBackend.deleteState(stackName, regionForState);
34540
34719
  logger.debug("State deleted");
34541
34720
  if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
@@ -34545,12 +34724,15 @@ async function runDestroyForStack(stackName, state, ctx) {
34545
34724
  } catch (error) {
34546
34725
  logger.warn(`Failed to persist remaining state after partial destroy: ${error instanceof Error ? error.message : String(error)}. The state file may still list already-deleted resources; a re-run resolves them idempotently.`);
34547
34726
  }
34548
- logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
34727
+ if (result.interrupted) logger.warn(`Destroy interrupted — ${Object.keys(remainingResources).length} resource(s) not deleted. State preserved.`);
34728
+ else logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
34549
34729
  }
34550
34730
  const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
34551
- if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
34731
+ if (!preserveState) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
34732
+ else if (result.interrupted && result.errorCount === 0) logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} destroy interrupted`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to finish.`);
34552
34733
  else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
34553
34734
  } finally {
34735
+ process.removeListener("SIGINT", sigintHandler);
34554
34736
  renderer.stop();
34555
34737
  await saveChain;
34556
34738
  logger.debug("Releasing lock...");
@@ -38165,6 +38347,7 @@ async function destroyCommand(stackArgs, options) {
38165
38347
  } else throw new Error("Could not determine which stacks belong to this app. Specify stack names explicitly, use --all, or ensure --app / cdk.json is configured.");
38166
38348
  const stackPatterns = stackArgs.length > 0 ? stackArgs : options.stack ? [options.stack] : [];
38167
38349
  let totalErrors = 0;
38350
+ let interrupted = false;
38168
38351
  let stackNames;
38169
38352
  if (options.all) stackNames = candidateStacks.map((s) => s.stackName);
38170
38353
  else if (stackPatterns.length > 0) stackNames = matchStacks(candidateStacks, stackPatterns).map((s) => s.stackName);
@@ -38286,6 +38469,7 @@ async function destroyCommand(stackArgs, options) {
38286
38469
  eventRecorder
38287
38470
  }));
38288
38471
  totalErrors += result.errorCount;
38472
+ if (result.interrupted) interrupted = true;
38289
38473
  destroyRunResult = result.errorCount > 0 ? "FAILED" : "SUCCEEDED";
38290
38474
  eventRecorder.record({
38291
38475
  eventType: "RUN_FINISHED",
@@ -38305,8 +38489,10 @@ async function destroyCommand(stackArgs, options) {
38305
38489
  } finally {
38306
38490
  await eventRecorder.finalize(destroyRunResult);
38307
38491
  }
38492
+ if (interrupted) break;
38308
38493
  }
38309
38494
  if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd destroy' to retry.`);
38495
+ if (interrupted) throw new PartialFailureError(`Destroy interrupted by Ctrl-C. State preserved — re-run 'cdkd destroy' to finish.`);
38310
38496
  } finally {
38311
38497
  awsClients.destroy();
38312
38498
  }
@@ -42743,6 +42929,7 @@ async function stateDestroyCommand(stackArgs, options) {
42743
42929
  }
42744
42930
  logger.info(`Found ${stackNames.length} stack(s) to destroy: ${stackNames.join(", ")}`);
42745
42931
  let totalErrors = 0;
42932
+ let interrupted = false;
42746
42933
  for (const stackName of stackNames) {
42747
42934
  const refs = stateRefs.filter((r) => r.stackName === stackName);
42748
42935
  let targets;
@@ -42800,9 +42987,13 @@ async function stateDestroyCommand(stackArgs, options) {
42800
42987
  ...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
42801
42988
  }));
42802
42989
  totalErrors += result.errorCount;
42990
+ if (result.interrupted) interrupted = true;
42991
+ if (interrupted) break;
42803
42992
  }
42993
+ if (interrupted) break;
42804
42994
  }
42805
42995
  if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd state destroy' to retry.`);
42996
+ if (interrupted) throw new PartialFailureError(`Destroy interrupted by Ctrl-C. State preserved — re-run 'cdkd state destroy' to finish.`);
42806
42997
  } finally {
42807
42998
  setup.dispose();
42808
42999
  }
@@ -53633,7 +53824,7 @@ function reorderArgs(argv) {
53633
53824
  async function main() {
53634
53825
  installPipeCloseHandler();
53635
53826
  const program = new Command();
53636
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.220.4");
53827
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.221.0");
53637
53828
  program.addCommand(createBootstrapCommand());
53638
53829
  program.addCommand(createSynthCommand());
53639
53830
  program.addCommand(createListCommand());