@go-to-k/cdkd 0.220.3 → 0.220.5

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.3";
1556
+ return "0.220.5";
1557
1557
  }
1558
1558
  /**
1559
1559
  * Generate a time-sortable unique run id, e.g.
@@ -1929,6 +1929,18 @@ function recordRunFailed(recorder, stackName, error) {
1929
1929
  * lock + bounded retry. After exhaustion, the writer logs warn and
1930
1930
  * continues; the index becomes stale until the next deploy/destroy
1931
1931
  * updates it.
1932
+ *
1933
+ * Cross-region state bucket: like `S3StateBackend` and `LockManager`, the
1934
+ * store tolerates a state bucket that lives in a different AWS region from
1935
+ * the CLI's base region. Before its first S3 operation it resolves the
1936
+ * bucket's actual region via `GetBucketLocation` and, if it differs from
1937
+ * the supplied client's region, builds a private replacement client for
1938
+ * that region (issue #819 — without this, every index write/remove against
1939
+ * a cross-region bucket failed with S3's 301 PermanentRedirect, surfacing
1940
+ * as `Exports index ... failed (non-retryable): ... must be addressed using
1941
+ * the specified endpoint`; the index was silently never maintained
1942
+ * cross-region, although the canonical state.json — written through the
1943
+ * already-region-corrected `S3StateBackend` — stayed correct).
1932
1944
  */
1933
1945
  /** Schema version for the exports index file. Separate from state.json's version. */
1934
1946
  const EXPORT_INDEX_VERSION = 1;
@@ -1977,6 +1989,9 @@ var ExportIndexStore = class {
1977
1989
  * where it actually matters. Reads (`lookup`) remain unsynchronized.
1978
1990
  */
1979
1991
  writeChain = Promise.resolve();
1992
+ /** Memoization for the one-time bucket-region resolution (issue #819). */
1993
+ clientResolved = false;
1994
+ resolveInFlight = null;
1980
1995
  constructor(s3Client, bucket, prefix, region, stateBackend, opts = {}) {
1981
1996
  this.s3Client = s3Client;
1982
1997
  this.bucket = bucket;
@@ -1993,6 +2008,72 @@ var ExportIndexStore = class {
1993
2008
  return `${this.prefix}/_index/${this.region}/exports.json`;
1994
2009
  }
1995
2010
  /**
2011
+ * Resolve the state bucket's actual region and, if it differs from the
2012
+ * supplied client's configured region, replace the client reference with
2013
+ * a new S3Client pointed at the bucket's region (issue #819).
2014
+ *
2015
+ * Mirrors `LockManager.ensureClientForBucket()` (issue #803) with the same
2016
+ * two deliberate differences from `S3StateBackend`:
2017
+ *
2018
+ * - The replacement client reuses the original client's resolved
2019
+ * credentials provider, so `--profile` / static credentials carry over
2020
+ * without the four `ExportIndexStore` call sites having to thread client
2021
+ * options.
2022
+ * - The original client is NOT destroyed. It is the shared `AwsClients.s3`
2023
+ * instance that other components (state backend, lock manager) still hold
2024
+ * a reference to.
2025
+ *
2026
+ * `resolveBucketRegion` caches per bucket name for the process lifetime, so
2027
+ * when the state backend / lock manager have already resolved the same
2028
+ * bucket this incurs no additional `GetBucketLocation` call.
2029
+ *
2030
+ * Best-effort: if the supplied client does not expose the SDK
2031
+ * `config.region()` shape (e.g. a hand-rolled test double) the resolution
2032
+ * is skipped and the original client is used unchanged.
2033
+ */
2034
+ async ensureClientForBucket() {
2035
+ if (this.clientResolved) return;
2036
+ if (this.resolveInFlight) return this.resolveInFlight;
2037
+ this.resolveInFlight = (async () => {
2038
+ try {
2039
+ const config = this.s3Client.config;
2040
+ if (!config || typeof config.region !== "function") {
2041
+ this.clientResolved = true;
2042
+ return;
2043
+ }
2044
+ const currentRegion = await config.region();
2045
+ const fallbackRegion = typeof currentRegion === "string" ? currentRegion : void 0;
2046
+ let probeCredentials;
2047
+ try {
2048
+ if (typeof config.credentials === "function") probeCredentials = await config.credentials();
2049
+ } catch {
2050
+ probeCredentials = void 0;
2051
+ }
2052
+ const bucketRegion = await resolveBucketRegion(this.bucket, {
2053
+ ...probeCredentials && { credentials: probeCredentials },
2054
+ ...fallbackRegion && { fallbackRegion }
2055
+ });
2056
+ if (bucketRegion !== currentRegion) {
2057
+ this.logger.debug(`State bucket '${this.bucket}' is in '${bucketRegion}' (exports-index client was '${String(currentRegion)}'); building a region-corrected S3 client for index operations.`);
2058
+ this.s3Client = new S3Client({
2059
+ region: bucketRegion,
2060
+ credentials: this.s3Client.config.credentials,
2061
+ logger: {
2062
+ debug: () => {},
2063
+ info: () => {},
2064
+ warn: () => {},
2065
+ error: () => {}
2066
+ }
2067
+ });
2068
+ }
2069
+ this.clientResolved = true;
2070
+ } finally {
2071
+ this.resolveInFlight = null;
2072
+ }
2073
+ })();
2074
+ return this.resolveInFlight;
2075
+ }
2076
+ /**
1996
2077
  * Look up an exported value by name. Returns the cached entry on hit,
1997
2078
  * or `undefined` on miss. The caller is responsible for falling back
1998
2079
  * to a state.json scan and (if found) patching the index via
@@ -2113,6 +2194,7 @@ var ExportIndexStore = class {
2113
2194
  }
2114
2195
  }
2115
2196
  async readIndexRaw() {
2197
+ await this.ensureClientForBucket();
2116
2198
  try {
2117
2199
  const response = await this.s3Client.send(new GetObjectCommand({
2118
2200
  Bucket: this.bucket,
@@ -2128,6 +2210,7 @@ var ExportIndexStore = class {
2128
2210
  }
2129
2211
  }
2130
2212
  async writeIndex(next, expectedEtag) {
2213
+ await this.ensureClientForBucket();
2131
2214
  const body = JSON.stringify(next, null, 2);
2132
2215
  return (await this.s3Client.send(new PutObjectCommand({
2133
2216
  Bucket: this.bucket,
@@ -19655,18 +19738,175 @@ var ECSProvider = class {
19655
19738
  };
19656
19739
  }
19657
19740
  /**
19658
- * 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).
19659
19752
  */
19660
19753
  convertVolumes(volumes) {
19661
19754
  if (!volumes) return void 0;
19662
19755
  return volumes.map((v) => ({
19663
19756
  name: v["Name"],
19664
- host: v["Host"],
19665
- 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"]),
19666
19761
  configuredAtLaunch: this.coerceBool(v["ConfiguredAtLaunch"])
19667
19762
  }));
19668
19763
  }
19669
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
+ /**
19670
19910
  * Coerce a CFn boolean property to a real boolean at the wire boundary.
19671
19911
  * CFn templates can carry booleans as the strings "true" / "false"
19672
19912
  * (e.g. via Fn::Sub / parameter plumbing), so the SDK input must
@@ -19845,7 +20085,7 @@ var ECSProvider = class {
19845
20085
  result["RequiresCompatibilities"] = td.requiresCompatibilities ? [...td.requiresCompatibilities] : [];
19846
20086
  if (td.executionRoleArn !== void 0) result["ExecutionRoleArn"] = td.executionRoleArn;
19847
20087
  if (td.taskRoleArn !== void 0) result["TaskRoleArn"] = td.taskRoleArn;
19848
- result["Volumes"] = td.volumes ?? [];
20088
+ result["Volumes"] = this.volumesToCfn(td.volumes);
19849
20089
  result["PlacementConstraints"] = td.placementConstraints ?? [];
19850
20090
  if (td.runtimePlatform) result["RuntimePlatform"] = td.runtimePlatform;
19851
20091
  if (td.proxyConfiguration) result["ProxyConfiguration"] = td.proxyConfiguration;
@@ -53550,7 +53790,7 @@ function reorderArgs(argv) {
53550
53790
  async function main() {
53551
53791
  installPipeCloseHandler();
53552
53792
  const program = new Command();
53553
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.220.3");
53793
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.220.5");
53554
53794
  program.addCommand(createBootstrapCommand());
53555
53795
  program.addCommand(createSynthCommand());
53556
53796
  program.addCommand(createListCommand());