@go-to-k/cdkd 0.82.0 → 0.83.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
@@ -70040,6 +70040,76 @@ import { dirname as dirname7 } from "node:path";
70040
70040
  import * as path2 from "node:path";
70041
70041
  import { Command as Command16, Option as Option9 } from "commander";
70042
70042
 
70043
+ // src/cli/commands/local-state-loader.ts
70044
+ init_aws_clients();
70045
+ async function loadStateForStack(stackName, synthRegion, opts) {
70046
+ const logger = getLogger();
70047
+ const prefix = opts.logPrefix ?? "--from-state";
70048
+ const region = opts.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1";
70049
+ let stateBucket;
70050
+ try {
70051
+ stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, region);
70052
+ } catch (err) {
70053
+ logger.warn(
70054
+ `${prefix}: could not resolve state bucket: ${err instanceof Error ? err.message : String(err)}. Falling back.`
70055
+ );
70056
+ return void 0;
70057
+ }
70058
+ const awsClients = new AwsClients({
70059
+ ...opts.region !== void 0 && { region: opts.region },
70060
+ ...opts.profile !== void 0 && { profile: opts.profile }
70061
+ });
70062
+ setAwsClients(awsClients);
70063
+ try {
70064
+ const stateConfig = { bucket: stateBucket, prefix: opts.statePrefix };
70065
+ const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
70066
+ ...opts.region !== void 0 && { region: opts.region },
70067
+ ...opts.profile !== void 0 && { profile: opts.profile }
70068
+ });
70069
+ await stateBackend.verifyBucketExists();
70070
+ const refs = (await stateBackend.listStacks()).filter((r) => r.stackName === stackName);
70071
+ if (refs.length === 0) {
70072
+ logger.warn(
70073
+ `${prefix}: no cdkd state found for stack '${stackName}' in bucket '${stateBucket}'. Was it deployed via 'cdkd deploy'? Falling back.`
70074
+ );
70075
+ return void 0;
70076
+ }
70077
+ let targetRegion;
70078
+ if (opts.stackRegion) {
70079
+ const found = refs.find((r) => r.region === opts.stackRegion);
70080
+ if (!found) {
70081
+ const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
70082
+ logger.warn(
70083
+ `${prefix}: stack '${stackName}' has no state in region '${opts.stackRegion}' (available: ${seen}). Falling back.`
70084
+ );
70085
+ return void 0;
70086
+ }
70087
+ targetRegion = opts.stackRegion;
70088
+ } else if (synthRegion && refs.some((r) => r.region === synthRegion)) {
70089
+ targetRegion = synthRegion;
70090
+ } else if (refs.length === 1) {
70091
+ targetRegion = refs[0].region ?? synthRegion ?? region;
70092
+ } else {
70093
+ const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
70094
+ logger.warn(
70095
+ `${prefix}: stack '${stackName}' has state in multiple regions (${seen}). Re-run with --stack-region <region>. Falling back.`
70096
+ );
70097
+ return void 0;
70098
+ }
70099
+ const stateData = await stateBackend.getState(stackName, targetRegion);
70100
+ if (!stateData) {
70101
+ logger.warn(
70102
+ `${prefix}: state record for '${stackName}' (${targetRegion}) returned empty. Falling back.`
70103
+ );
70104
+ return void 0;
70105
+ }
70106
+ logger.debug(`${prefix}: loaded state for ${stackName} (${targetRegion})`);
70107
+ return { state: stateData.state, region: targetRegion };
70108
+ } finally {
70109
+ awsClients.destroy();
70110
+ }
70111
+ }
70112
+
70043
70113
  // src/local/lambda-resolver.ts
70044
70114
  import { existsSync as existsSync4, statSync as statSync3 } from "node:fs";
70045
70115
  import { dirname, isAbsolute, resolve as resolve4 } from "node:path";
@@ -71217,8 +71287,23 @@ function extractHashFromImageUri(imageUri) {
71217
71287
  return match?.[1];
71218
71288
  }
71219
71289
 
71220
- // src/cli/commands/local-invoke.ts
71221
- init_aws_clients();
71290
+ // src/utils/single-flight.ts
71291
+ function singleFlight(fn, onError) {
71292
+ let promise;
71293
+ return async () => {
71294
+ if (!promise) {
71295
+ promise = (async () => {
71296
+ try {
71297
+ await fn();
71298
+ } catch (err) {
71299
+ if (onError)
71300
+ onError(err);
71301
+ }
71302
+ })();
71303
+ }
71304
+ await promise;
71305
+ };
71306
+ }
71222
71307
 
71223
71308
  // src/cli/commands/local-start-api.ts
71224
71309
  import { cpSync, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
@@ -76047,21 +76132,12 @@ async function localStartApiCommand(options) {
76047
76132
  });
76048
76133
  logger.info(`Watching ${options.output} (and ${lastAssetPaths.value.length} asset dir(s))`);
76049
76134
  }
76050
- let shuttingDown = false;
76135
+ let shutdownStarted = false;
76136
+ let firstSignal;
76137
+ let firstExitCode = 0;
76051
76138
  let forceExitArmed = false;
76052
- const shutdown = async (signal, exitCode) => {
76053
- if (shuttingDown) {
76054
- if (!forceExitArmed) {
76055
- forceExitArmed = true;
76056
- logger.warn(
76057
- `Received second ${signal}; force-exiting. Orphan containers may remain \u2014 run 'docker ps --filter name=cdkd-local-' and 'docker rm -f' to clean up.`
76058
- );
76059
- process.exit(130);
76060
- }
76061
- return;
76062
- }
76063
- shuttingDown = true;
76064
- logger.info(`Received ${signal}, shutting down...`);
76139
+ const runCleanup = singleFlight(async () => {
76140
+ logger.info(`Received ${firstSignal}, shutting down...`);
76065
76141
  if (watcher) {
76066
76142
  try {
76067
76143
  await watcher.close();
@@ -76109,7 +76185,23 @@ async function localStartApiCommand(options) {
76109
76185
  );
76110
76186
  }
76111
76187
  }
76112
- process.exit(exitCode);
76188
+ });
76189
+ const shutdown = async (signal, exitCode) => {
76190
+ if (shutdownStarted) {
76191
+ if (!forceExitArmed) {
76192
+ forceExitArmed = true;
76193
+ logger.warn(
76194
+ `Received second ${signal}; force-exiting. Orphan containers may remain \u2014 run 'docker ps --filter name=cdkd-local-' and 'docker rm -f' to clean up.`
76195
+ );
76196
+ process.exit(130);
76197
+ }
76198
+ return;
76199
+ }
76200
+ shutdownStarted = true;
76201
+ firstSignal = signal;
76202
+ firstExitCode = exitCode;
76203
+ await runCleanup();
76204
+ process.exit(firstExitCode);
76113
76205
  };
76114
76206
  process.on("SIGINT", () => {
76115
76207
  void shutdown("SIGINT", 130);
@@ -76621,6 +76713,83 @@ var EcsTaskResolutionError = class _EcsTaskResolutionError extends Error {
76621
76713
  Object.setPrototypeOf(this, _EcsTaskResolutionError.prototype);
76622
76714
  }
76623
76715
  };
76716
+ function derivePartitionAndUrlSuffix(region) {
76717
+ if (region.startsWith("cn-"))
76718
+ return { partition: "aws-cn", urlSuffix: "amazonaws.com.cn" };
76719
+ if (region.startsWith("us-gov-"))
76720
+ return { partition: "aws-us-gov", urlSuffix: "amazonaws.com" };
76721
+ if (region.startsWith("us-iso-"))
76722
+ return { partition: "aws-iso", urlSuffix: "c2s.ic.gov" };
76723
+ if (region.startsWith("us-isob-"))
76724
+ return { partition: "aws-iso-b", urlSuffix: "sc2s.sgov.gov" };
76725
+ return { partition: "aws", urlSuffix: "amazonaws.com" };
76726
+ }
76727
+ function detectEcsImageResolutionNeeds(stack) {
76728
+ const resources = stack.template.Resources ?? {};
76729
+ let needsPseudoParameters = false;
76730
+ let needsStateResources = false;
76731
+ for (const res of Object.values(resources)) {
76732
+ if (res.Type !== "AWS::ECS::TaskDefinition")
76733
+ continue;
76734
+ const props = res.Properties ?? {};
76735
+ const containers = Array.isArray(props["ContainerDefinitions"]) ? props["ContainerDefinitions"] : [];
76736
+ for (const c of containers) {
76737
+ if (!c || typeof c !== "object")
76738
+ continue;
76739
+ const image = c["Image"];
76740
+ const need = inspectImageForSubstitutions(image, resources);
76741
+ if (need.pseudo)
76742
+ needsPseudoParameters = true;
76743
+ if (need.state)
76744
+ needsStateResources = true;
76745
+ if (needsPseudoParameters && needsStateResources)
76746
+ break;
76747
+ }
76748
+ if (needsPseudoParameters && needsStateResources)
76749
+ break;
76750
+ }
76751
+ return { needsPseudoParameters, needsStateResources };
76752
+ }
76753
+ function inspectImageForSubstitutions(image, resources) {
76754
+ if (!image || typeof image !== "object")
76755
+ return { pseudo: false, state: false };
76756
+ const obj = image;
76757
+ const getAtt = obj["Fn::GetAtt"];
76758
+ if (getAtt !== void 0) {
76759
+ let lid;
76760
+ if (Array.isArray(getAtt) && typeof getAtt[0] === "string")
76761
+ lid = getAtt[0];
76762
+ else if (typeof getAtt === "string")
76763
+ lid = getAtt.split(".")[0];
76764
+ if (lid && resources[lid]?.Type === "AWS::ECR::Repository") {
76765
+ return { pseudo: false, state: true };
76766
+ }
76767
+ }
76768
+ let sub;
76769
+ const subRaw = obj["Fn::Sub"];
76770
+ if (typeof subRaw === "string")
76771
+ sub = subRaw;
76772
+ else if (Array.isArray(subRaw) && typeof subRaw[0] === "string")
76773
+ sub = subRaw[0];
76774
+ if (!sub)
76775
+ return { pseudo: false, state: false };
76776
+ let pseudo = false;
76777
+ let state = false;
76778
+ const placeholderRegex = /\$\{([^}]+)\}/g;
76779
+ let m;
76780
+ while ((m = placeholderRegex.exec(sub)) !== null) {
76781
+ const key = m[1];
76782
+ if (key.startsWith("AWS::")) {
76783
+ pseudo = true;
76784
+ continue;
76785
+ }
76786
+ const dot = key.indexOf(".");
76787
+ const lid = dot === -1 ? key : key.slice(0, dot);
76788
+ if (resources[lid]?.Type === "AWS::ECR::Repository")
76789
+ state = true;
76790
+ }
76791
+ return { pseudo, state };
76792
+ }
76624
76793
  function parseEcsTarget(target) {
76625
76794
  if (typeof target !== "string" || target.length === 0) {
76626
76795
  throw new EcsTaskResolutionError(
@@ -76642,7 +76811,7 @@ function parseEcsTarget(target) {
76642
76811
  }
76643
76812
  return { stackPattern: null, pathOrId: target, isPath: false };
76644
76813
  }
76645
- function resolveEcsTaskTarget(target, stacks) {
76814
+ function resolveEcsTaskTarget(target, stacks, context) {
76646
76815
  if (stacks.length === 0) {
76647
76816
  throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
76648
76817
  }
@@ -76685,7 +76854,7 @@ function resolveEcsTaskTarget(target, stacks) {
76685
76854
  `Resource '${logicalId}' in ${stack.stackName} is ${resource.Type}, not an AWS::ECS::TaskDefinition.`
76686
76855
  );
76687
76856
  }
76688
- return extractTaskDefinitionProperties(stack, logicalId, resource);
76857
+ return extractTaskDefinitionProperties(stack, logicalId, resource, context);
76689
76858
  }
76690
76859
  function pickStack2(parsed, stacks) {
76691
76860
  if (parsed.stackPattern === null) {
@@ -76708,7 +76877,7 @@ function pickStack2(parsed, stacks) {
76708
76877
  }
76709
76878
  return matched[0];
76710
76879
  }
76711
- function extractTaskDefinitionProperties(stack, logicalId, resource) {
76880
+ function extractTaskDefinitionProperties(stack, logicalId, resource, context) {
76712
76881
  const props = resource.Properties ?? {};
76713
76882
  const warnings = [];
76714
76883
  const family = pickString(props["Family"]) ?? logicalId;
@@ -76735,7 +76904,7 @@ function extractTaskDefinitionProperties(stack, logicalId, resource) {
76735
76904
  throw new EcsTaskResolutionError(`Task definition '${logicalId}' has no ContainerDefinitions.`);
76736
76905
  }
76737
76906
  const containers = rawContainers.map(
76738
- (c, idx) => parseContainerDefinition(c, idx, logicalId, resources, stack)
76907
+ (c, idx) => parseContainerDefinition(c, idx, logicalId, resources, stack, context)
76739
76908
  );
76740
76909
  const rawVolumes = props["Volumes"];
76741
76910
  const volumes = Array.isArray(rawVolumes) ? rawVolumes.map((v, idx) => parseVolume(v, idx, logicalId)) : [];
@@ -76781,7 +76950,7 @@ function parseRuntimePlatform(value) {
76781
76950
  return void 0;
76782
76951
  return { cpuArchitecture: cpu, operatingSystemFamily: os };
76783
76952
  }
76784
- function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack) {
76953
+ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack, context) {
76785
76954
  if (!raw || typeof raw !== "object") {
76786
76955
  throw new EcsTaskResolutionError(
76787
76956
  `Task '${taskLogicalId}' ContainerDefinitions[${idx}] is not an object.`
@@ -76794,7 +76963,7 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack) {
76794
76963
  `Task '${taskLogicalId}' ContainerDefinitions[${idx}] has no Name.`
76795
76964
  );
76796
76965
  }
76797
- const image = parseContainerImage(c["Image"], name, taskLogicalId, resources, stack);
76966
+ const image = parseContainerImage(c["Image"], name, taskLogicalId, resources, stack, context);
76798
76967
  const command = pickStringArray2(c["Command"]);
76799
76968
  const entryPoint = pickStringArray2(c["EntryPoint"]);
76800
76969
  const workingDirectory = pickString(c["WorkingDirectory"]);
@@ -76940,7 +77109,11 @@ function parseContainerDefinition(raw, idx, taskLogicalId, resources, stack) {
76940
77109
  out.readonlyRootFilesystem = readonlyRootFilesystem;
76941
77110
  return out;
76942
77111
  }
76943
- function parseContainerImage(raw, containerName, taskLogicalId, resources, _stack) {
77112
+ function parseContainerImage(raw, containerName, taskLogicalId, resources, _stack, context) {
77113
+ const getAttImage = tryResolveImageGetAtt(raw, resources, context);
77114
+ if (getAttImage) {
77115
+ return classifyResolvedImage(getAttImage);
77116
+ }
76944
77117
  const flat = extractImageString(raw);
76945
77118
  if (!flat) {
76946
77119
  throw new EcsTaskResolutionError(
@@ -76954,23 +77127,123 @@ function parseContainerImage(raw, containerName, taskLogicalId, resources, _stac
76954
77127
  out.assetHash = hashMatch[1];
76955
77128
  return out;
76956
77129
  }
76957
- const ecrMatch = /^(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com(?:\.cn)?\//.exec(flat);
76958
- if (ecrMatch) {
76959
- return { kind: "ecr", uri: flat, account: ecrMatch[1], region: ecrMatch[2] };
76960
- }
76961
- if (flat.includes("${") && flat.includes("AWS::AccountId")) {
77130
+ const substituted = substituteImagePlaceholders(flat, resources, context);
77131
+ if (substituted.includes("${")) {
77132
+ const unresolvedRepoRef = findUnresolvedEcrRepositoryRef(substituted, resources);
77133
+ if (unresolvedRepoRef) {
77134
+ throw new EcsTaskResolutionError(
77135
+ `Container '${containerName}' in task '${taskLogicalId}' references same-stack ECR repository '${unresolvedRepoRef}'. cdkd local run-task v1 cannot resolve the repository URI without state \u2014 pass --from-state (the stack must have been deployed via cdkd deploy), build via ContainerImage.fromAsset, or pin a public image.`
77136
+ );
77137
+ }
77138
+ if (substituted.includes("AWS::")) {
77139
+ throw new EcsTaskResolutionError(
77140
+ `Container '${containerName}' in task '${taskLogicalId}' has an Image that references AWS pseudo parameters (${substituted}). cdkd could not resolve them: confirm AWS credentials are configured so STS GetCallerIdentity succeeds, and that --region / AWS_REGION names the target region. Workaround: build the image locally (ContainerImage.fromAsset) or pin a public image.`
77141
+ );
77142
+ }
76962
77143
  throw new EcsTaskResolutionError(
76963
- `Container '${containerName}' in task '${taskLogicalId}' has an Image that references AWS pseudo parameters (${flat}). cdkd local run-task v1 cannot resolve account-scoped ECR repos at synth time. Build the image locally (CDK ContainerImage.fromAsset) or pin to a public image to test locally.`
77144
+ `Container '${containerName}' in task '${taskLogicalId}' has an Image with unresolved \${...} placeholders (${substituted}). cdkd local run-task v1 only resolves AWS pseudo parameters and same-stack AWS::ECR::Repository refs.`
76964
77145
  );
76965
77146
  }
76966
- for (const [refLogicalId, res] of Object.entries(resources)) {
76967
- if (res.Type === "AWS::ECR::Repository" && flat.includes(refLogicalId)) {
76968
- throw new EcsTaskResolutionError(
76969
- `Container '${containerName}' in task '${taskLogicalId}' references same-stack ECR repository '${refLogicalId}'. cdkd local run-task v1 cannot resolve the repository URI without state \u2014 build via ContainerImage.fromAsset or pin a public image.`
76970
- );
77147
+ const ecrMatch = /^(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com(?:\.cn)?\//.exec(substituted);
77148
+ if (ecrMatch) {
77149
+ return { kind: "ecr", uri: substituted, account: ecrMatch[1], region: ecrMatch[2] };
77150
+ }
77151
+ return { kind: "public", uri: substituted };
77152
+ }
77153
+ function findUnresolvedEcrRepositoryRef(substituted, resources) {
77154
+ const placeholderRegex = /\$\{([^}]+)\}/g;
77155
+ let m;
77156
+ while ((m = placeholderRegex.exec(substituted)) !== null) {
77157
+ const key = m[1];
77158
+ if (key.startsWith("AWS::"))
77159
+ continue;
77160
+ const dot = key.indexOf(".");
77161
+ const lid = dot === -1 ? key : key.slice(0, dot);
77162
+ if (resources[lid]?.Type === "AWS::ECR::Repository")
77163
+ return lid;
77164
+ }
77165
+ return void 0;
77166
+ }
77167
+ function classifyResolvedImage(uri) {
77168
+ if (uri.includes("cdk-hnb659fds-container-assets-")) {
77169
+ const hashMatch = /:([a-f0-9]{8,})$/.exec(uri);
77170
+ const out = { kind: "cdk-asset" };
77171
+ if (hashMatch)
77172
+ out.assetHash = hashMatch[1];
77173
+ return out;
77174
+ }
77175
+ const ecrMatch = /^(\d{12})\.dkr\.ecr\.([^.]+)\.amazonaws\.com(?:\.cn)?\//.exec(uri);
77176
+ if (ecrMatch) {
77177
+ return { kind: "ecr", uri, account: ecrMatch[1], region: ecrMatch[2] };
77178
+ }
77179
+ return { kind: "public", uri };
77180
+ }
77181
+ function substituteImagePlaceholders(flat, resources, context) {
77182
+ if (!flat.includes("${"))
77183
+ return flat;
77184
+ return flat.replace(/\$\{([^}]+)\}/g, (full, key) => {
77185
+ if (context?.pseudoParameters) {
77186
+ if (key === "AWS::AccountId" && context.pseudoParameters.accountId) {
77187
+ return context.pseudoParameters.accountId;
77188
+ }
77189
+ if (key === "AWS::Region" && context.pseudoParameters.region) {
77190
+ return context.pseudoParameters.region;
77191
+ }
77192
+ if (key === "AWS::Partition" && context.pseudoParameters.partition) {
77193
+ return context.pseudoParameters.partition;
77194
+ }
77195
+ if (key === "AWS::URLSuffix" && context.pseudoParameters.urlSuffix) {
77196
+ return context.pseudoParameters.urlSuffix;
77197
+ }
77198
+ }
77199
+ if (context?.stateResources) {
77200
+ const dot = key.indexOf(".");
77201
+ const logicalId = dot === -1 ? key : key.slice(0, dot);
77202
+ const refResource = resources[logicalId];
77203
+ const stateEntry = context.stateResources[logicalId];
77204
+ if (refResource?.Type === "AWS::ECR::Repository" && stateEntry) {
77205
+ if (dot === -1) {
77206
+ return stateEntry.physicalId;
77207
+ }
77208
+ const attr = key.slice(dot + 1);
77209
+ const cached = stateEntry.attributes?.[attr];
77210
+ if (typeof cached === "string")
77211
+ return cached;
77212
+ }
77213
+ }
77214
+ return full;
77215
+ });
77216
+ }
77217
+ function tryResolveImageGetAtt(raw, resources, context) {
77218
+ if (!raw || typeof raw !== "object")
77219
+ return void 0;
77220
+ const obj = raw;
77221
+ const arg = obj["Fn::GetAtt"];
77222
+ if (arg === void 0)
77223
+ return void 0;
77224
+ let logicalId;
77225
+ let attr;
77226
+ if (Array.isArray(arg) && arg.length === 2 && typeof arg[0] === "string" && typeof arg[1] === "string") {
77227
+ logicalId = arg[0];
77228
+ attr = arg[1];
77229
+ } else if (typeof arg === "string") {
77230
+ const dot = arg.indexOf(".");
77231
+ if (dot > 0 && dot < arg.length - 1) {
77232
+ logicalId = arg.slice(0, dot);
77233
+ attr = arg.slice(dot + 1);
76971
77234
  }
76972
77235
  }
76973
- return { kind: "public", uri: flat };
77236
+ if (!logicalId || !attr)
77237
+ return void 0;
77238
+ const refResource = resources[logicalId];
77239
+ if (refResource?.Type !== "AWS::ECR::Repository")
77240
+ return void 0;
77241
+ if (attr !== "RepositoryUri" && attr !== "Arn")
77242
+ return void 0;
77243
+ const cached = context?.stateResources?.[logicalId]?.attributes?.[attr];
77244
+ if (typeof cached === "string" && cached.length > 0)
77245
+ return cached;
77246
+ return void 0;
76974
77247
  }
76975
77248
  function extractImageString(value) {
76976
77249
  if (typeof value === "string" && value.length > 0)
@@ -77049,6 +77322,7 @@ function parseVolume(raw, idx, taskLogicalId) {
77049
77322
  }
77050
77323
  return { name, kind: "host" };
77051
77324
  }
77325
+ var TASK_ROLE_ACCOUNT_PLACEHOLDER = "${AWS::AccountId}";
77052
77326
  function resolveRoleArn(value, resources) {
77053
77327
  if (value === void 0 || value === null)
77054
77328
  return void 0;
@@ -77057,24 +77331,23 @@ function resolveRoleArn(value, resources) {
77057
77331
  if (typeof value !== "object")
77058
77332
  return void 0;
77059
77333
  const obj = value;
77334
+ let refLogicalId;
77060
77335
  if ("Ref" in obj && typeof obj["Ref"] === "string") {
77061
- const refLogicalId = obj["Ref"];
77062
- const role = resources[refLogicalId];
77063
- if (role?.Type === "AWS::IAM::Role") {
77064
- return void 0;
77065
- }
77066
- }
77067
- if ("Fn::GetAtt" in obj) {
77336
+ refLogicalId = obj["Ref"];
77337
+ } else if ("Fn::GetAtt" in obj) {
77068
77338
  const arg = obj["Fn::GetAtt"];
77069
77339
  if (Array.isArray(arg) && typeof arg[0] === "string") {
77070
- const refLogicalId = arg[0];
77071
- const role = resources[refLogicalId];
77072
- if (role?.Type === "AWS::IAM::Role") {
77073
- return void 0;
77074
- }
77340
+ const attr = typeof arg[1] === "string" ? arg[1] : "";
77341
+ if (attr === "" || attr === "Arn")
77342
+ refLogicalId = arg[0];
77075
77343
  }
77076
77344
  }
77077
- return void 0;
77345
+ if (refLogicalId === void 0)
77346
+ return void 0;
77347
+ const role = resources[refLogicalId];
77348
+ if (role?.Type !== "AWS::IAM::Role")
77349
+ return void 0;
77350
+ return `arn:aws:iam::${TASK_ROLE_ACCOUNT_PLACEHOLDER}:role/${refLogicalId}`;
77078
77351
  }
77079
77352
  function pickString(value) {
77080
77353
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -77911,7 +78184,8 @@ async function localRunTaskCommand(target, options) {
77911
78184
  ...Object.keys(context).length > 0 && { context }
77912
78185
  };
77913
78186
  const { stacks } = await synthesizer.synthesize(synthOpts);
77914
- const task = resolveEcsTaskTarget(target, stacks);
78187
+ const imageContext = await buildEcsImageResolutionContext(target, stacks, options);
78188
+ const task = resolveEcsTaskTarget(target, stacks, imageContext);
77915
78189
  logger.info(
77916
78190
  `Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`
77917
78191
  );
@@ -77930,10 +78204,10 @@ async function localRunTaskCommand(target, options) {
77930
78204
  if (options.assumeTaskRole === true) {
77931
78205
  if (!task.taskRoleArn) {
77932
78206
  throw new Error(
77933
- `--assume-task-role passed without an ARN but the task definition's TaskRoleArn could not be resolved statically. Pass the ARN explicitly: --assume-task-role <arn>`
78207
+ `--assume-task-role passed without an ARN but the task definition has no resolvable TaskRoleArn. Either the task definition does not set TaskRoleArn, or it points at a resource cdkd cannot resolve to an IAM Role at synth time. Pass the ARN explicitly: --assume-task-role <arn>`
77934
78208
  );
77935
78209
  }
77936
- resolvedRoleArn = task.taskRoleArn;
78210
+ resolvedRoleArn = await resolvePlaceholderAccount(task.taskRoleArn, options.region);
77937
78211
  assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
77938
78212
  } else if (typeof options.assumeTaskRole === "string") {
77939
78213
  resolvedRoleArn = options.assumeTaskRole;
@@ -77981,6 +78255,24 @@ async function localRunTaskCommand(target, options) {
77981
78255
  await cleanup();
77982
78256
  }
77983
78257
  }
78258
+ async function resolvePlaceholderAccount(arn, region) {
78259
+ if (!arn.includes(TASK_ROLE_ACCOUNT_PLACEHOLDER))
78260
+ return arn;
78261
+ const { STSClient: STSClient11, GetCallerIdentityCommand: GetCallerIdentityCommand12 } = await import("@aws-sdk/client-sts");
78262
+ const sts = new STSClient11({ ...region && { region } });
78263
+ try {
78264
+ const identity = await sts.send(new GetCallerIdentityCommand12({}));
78265
+ const account = identity.Account;
78266
+ if (!account) {
78267
+ throw new Error(
78268
+ `--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'. Pass the ARN explicitly: --assume-task-role <arn>`
78269
+ );
78270
+ }
78271
+ return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
78272
+ } finally {
78273
+ sts.destroy();
78274
+ }
78275
+ }
77984
78276
  async function assumeTaskRole(roleArn, region) {
77985
78277
  const { STSClient: STSClient11, AssumeRoleCommand: AssumeRoleCommand2 } = await import("@aws-sdk/client-sts");
77986
78278
  const sts = new STSClient11({ ...region && { region } });
@@ -78005,6 +78297,80 @@ async function assumeTaskRole(roleArn, region) {
78005
78297
  sts.destroy();
78006
78298
  }
78007
78299
  }
78300
+ async function buildEcsImageResolutionContext(target, stacks, options) {
78301
+ const logger = getLogger();
78302
+ const parsed = parseEcsTarget(target);
78303
+ const candidate = pickCandidateStack(parsed.stackPattern, stacks);
78304
+ if (!candidate)
78305
+ return void 0;
78306
+ const needs = detectEcsImageResolutionNeeds(candidate);
78307
+ if (!needs.needsPseudoParameters && !needs.needsStateResources)
78308
+ return void 0;
78309
+ const ctx = {};
78310
+ if (needs.needsPseudoParameters) {
78311
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
78312
+ if (!region) {
78313
+ logger.warn(
78314
+ "Container Image references ${AWS::Region} but cdkd could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack."
78315
+ );
78316
+ }
78317
+ let accountId;
78318
+ try {
78319
+ accountId = await resolveCallerAccountId(region);
78320
+ } catch (err) {
78321
+ logger.warn(
78322
+ `Container Image references \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; the resolver will surface its existing error.`
78323
+ );
78324
+ }
78325
+ const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
78326
+ ctx.pseudoParameters = {
78327
+ ...accountId !== void 0 && { accountId },
78328
+ ...region !== void 0 && { region },
78329
+ ...partitionAndSuffix && {
78330
+ partition: partitionAndSuffix.partition,
78331
+ urlSuffix: partitionAndSuffix.urlSuffix
78332
+ }
78333
+ };
78334
+ }
78335
+ if (options.fromState && needs.needsStateResources) {
78336
+ const loaded = await loadStateForStack(candidate.stackName, candidate.region, {
78337
+ ...options.stackRegion !== void 0 && { stackRegion: options.stackRegion },
78338
+ ...options.stateBucket !== void 0 && { stateBucket: options.stateBucket },
78339
+ statePrefix: options.statePrefix,
78340
+ ...options.region !== void 0 && { region: options.region },
78341
+ ...options.profile !== void 0 && { profile: options.profile }
78342
+ });
78343
+ if (loaded) {
78344
+ ctx.stateResources = loaded.state.resources;
78345
+ }
78346
+ } else if (!options.fromState && needs.needsStateResources) {
78347
+ logger.warn(
78348
+ "Container Image references a same-stack AWS::ECR::Repository. Pass --from-state to substitute the deployed repository URI (requires the stack to have been deployed via cdkd deploy). Otherwise the resolver will surface its existing error."
78349
+ );
78350
+ }
78351
+ return ctx;
78352
+ }
78353
+ function pickCandidateStack(stackPattern, stacks) {
78354
+ if (stackPattern === null) {
78355
+ if (stacks.length === 1)
78356
+ return stacks[0];
78357
+ return void 0;
78358
+ }
78359
+ const matched = matchStacks(stacks, [stackPattern]);
78360
+ if (matched.length === 1)
78361
+ return matched[0];
78362
+ return void 0;
78363
+ }
78364
+ async function resolveCallerAccountId(region) {
78365
+ const { STSClient: STSClient11, GetCallerIdentityCommand: GetCallerIdentityCommand12 } = await import("@aws-sdk/client-sts");
78366
+ const sts = new STSClient11({ ...region && { region } });
78367
+ try {
78368
+ const identity = await sts.send(new GetCallerIdentityCommand12({}));
78369
+ return identity.Account;
78370
+ } finally {
78371
+ sts.destroy();
78372
+ }
78373
+ }
78008
78374
  function readEnvOverridesFile2(filePath) {
78009
78375
  if (!filePath)
78010
78376
  return void 0;
@@ -78072,8 +78438,20 @@ function createLocalRunTaskCommand() {
78072
78438
  "--detach",
78073
78439
  "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle."
78074
78440
  ).default(false)
78441
+ ).addOption(
78442
+ new Option8(
78443
+ "--from-state",
78444
+ "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default \u2014 only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag."
78445
+ ).default(false)
78446
+ ).addOption(
78447
+ new Option8(
78448
+ "--stack-region <region>",
78449
+ "Region of the cdkd state record to read (used with --from-state when the same stack name has state in multiple regions)."
78450
+ )
78075
78451
  ).action(withErrorHandling(localRunTaskCommand));
78076
- [...commonOptions, ...appOptions, ...contextOptions].forEach((opt) => cmd.addOption(opt));
78452
+ [...commonOptions, ...appOptions, ...contextOptions, ...stateOptions].forEach(
78453
+ (opt) => cmd.addOption(opt)
78454
+ );
78077
78455
  cmd.addOption(deprecatedRegionOption);
78078
78456
  return cmd;
78079
78457
  }
@@ -78089,44 +78467,49 @@ async function localInvokeCommand(target, options) {
78089
78467
  let containerId;
78090
78468
  let stopLogs;
78091
78469
  let sigintHandler;
78092
- const cleanup = async () => {
78093
- if (stopLogs) {
78094
- try {
78095
- stopLogs();
78096
- } catch (err) {
78097
- getLogger().debug(
78098
- `streamLogs stop failed: ${err instanceof Error ? err.message : String(err)}`
78099
- );
78470
+ const cleanup = singleFlight(
78471
+ async () => {
78472
+ if (stopLogs) {
78473
+ try {
78474
+ stopLogs();
78475
+ } catch (err) {
78476
+ getLogger().debug(
78477
+ `streamLogs stop failed: ${err instanceof Error ? err.message : String(err)}`
78478
+ );
78479
+ }
78100
78480
  }
78101
- }
78102
- if (containerId) {
78103
- try {
78104
- await removeContainer(containerId);
78105
- } catch (err) {
78106
- getLogger().debug(
78107
- `removeContainer(${containerId}) failed: ${err instanceof Error ? err.message : String(err)}`
78108
- );
78481
+ if (containerId) {
78482
+ try {
78483
+ await removeContainer(containerId);
78484
+ } catch (err) {
78485
+ getLogger().debug(
78486
+ `removeContainer(${containerId}) failed: ${err instanceof Error ? err.message : String(err)}`
78487
+ );
78488
+ }
78109
78489
  }
78110
- }
78111
- if (imagePlan?.inlineTmpDir) {
78112
- try {
78113
- rmSync3(imagePlan.inlineTmpDir, { recursive: true, force: true });
78114
- } catch (err) {
78115
- getLogger().debug(
78116
- `Failed to remove inline-code tmpdir ${imagePlan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`
78117
- );
78490
+ if (imagePlan?.inlineTmpDir) {
78491
+ try {
78492
+ rmSync3(imagePlan.inlineTmpDir, { recursive: true, force: true });
78493
+ } catch (err) {
78494
+ getLogger().debug(
78495
+ `Failed to remove inline-code tmpdir ${imagePlan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`
78496
+ );
78497
+ }
78118
78498
  }
78119
- }
78120
- if (imagePlan?.layersTmpDir) {
78121
- try {
78122
- rmSync3(imagePlan.layersTmpDir, { recursive: true, force: true });
78123
- } catch (err) {
78124
- getLogger().debug(
78125
- `Failed to remove merged-layers tmpdir ${imagePlan.layersTmpDir}: ${err instanceof Error ? err.message : String(err)}`
78126
- );
78499
+ if (imagePlan?.layersTmpDir) {
78500
+ try {
78501
+ rmSync3(imagePlan.layersTmpDir, { recursive: true, force: true });
78502
+ } catch (err) {
78503
+ getLogger().debug(
78504
+ `Failed to remove merged-layers tmpdir ${imagePlan.layersTmpDir}: ${err instanceof Error ? err.message : String(err)}`
78505
+ );
78506
+ }
78127
78507
  }
78508
+ },
78509
+ (err) => {
78510
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
78128
78511
  }
78129
- };
78512
+ );
78130
78513
  try {
78131
78514
  await applyRoleArnIfSet({ roleArn: options.roleArn, region: options.region });
78132
78515
  await ensureDockerAvailable();
@@ -78477,72 +78860,6 @@ function materializeInlineCode2(handler, source, fileExtension) {
78477
78860
  writeFileSync6(filePath, source, "utf-8");
78478
78861
  return dir;
78479
78862
  }
78480
- async function loadStateForStack(stackName, synthRegion, opts) {
78481
- const logger = getLogger();
78482
- const region = opts.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? synthRegion ?? "us-east-1";
78483
- let stateBucket;
78484
- try {
78485
- stateBucket = await resolveStateBucketWithDefault(opts.stateBucket, region);
78486
- } catch (err) {
78487
- logger.warn(
78488
- `--from-state: could not resolve state bucket: ${err instanceof Error ? err.message : String(err)}. Falling back to PR 1 warn-and-drop semantics.`
78489
- );
78490
- return void 0;
78491
- }
78492
- const awsClients = new AwsClients({
78493
- ...opts.region !== void 0 && { region: opts.region },
78494
- ...opts.profile !== void 0 && { profile: opts.profile }
78495
- });
78496
- setAwsClients(awsClients);
78497
- try {
78498
- const stateConfig = { bucket: stateBucket, prefix: opts.statePrefix };
78499
- const stateBackend = new S3StateBackend(awsClients.s3, stateConfig, {
78500
- ...opts.region !== void 0 && { region: opts.region },
78501
- ...opts.profile !== void 0 && { profile: opts.profile }
78502
- });
78503
- await stateBackend.verifyBucketExists();
78504
- const refs = (await stateBackend.listStacks()).filter((r) => r.stackName === stackName);
78505
- if (refs.length === 0) {
78506
- logger.warn(
78507
- `--from-state: no cdkd state found for stack '${stackName}' in bucket '${stateBucket}'. Was it deployed via 'cdkd deploy'? Falling back to PR 1 warn-and-drop semantics.`
78508
- );
78509
- return void 0;
78510
- }
78511
- let targetRegion;
78512
- if (opts.stackRegion) {
78513
- const found = refs.find((r) => r.region === opts.stackRegion);
78514
- if (!found) {
78515
- const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
78516
- logger.warn(
78517
- `--from-state: stack '${stackName}' has no state in region '${opts.stackRegion}' (available: ${seen}). Falling back.`
78518
- );
78519
- return void 0;
78520
- }
78521
- targetRegion = opts.stackRegion;
78522
- } else if (synthRegion && refs.some((r) => r.region === synthRegion)) {
78523
- targetRegion = synthRegion;
78524
- } else if (refs.length === 1) {
78525
- targetRegion = refs[0].region ?? synthRegion ?? region;
78526
- } else {
78527
- const seen = refs.map((r) => r.region ?? "(legacy)").join(", ");
78528
- logger.warn(
78529
- `--from-state: stack '${stackName}' has state in multiple regions (${seen}). Re-run with --stack-region <region>. Falling back.`
78530
- );
78531
- return void 0;
78532
- }
78533
- const stateData = await stateBackend.getState(stackName, targetRegion);
78534
- if (!stateData) {
78535
- logger.warn(
78536
- `--from-state: state record for '${stackName}' (${targetRegion}) returned empty. Falling back.`
78537
- );
78538
- return void 0;
78539
- }
78540
- logger.debug(`--from-state: loaded state for ${stackName} (${targetRegion})`);
78541
- return { state: stateData.state, region: targetRegion };
78542
- } finally {
78543
- awsClients.destroy();
78544
- }
78545
- }
78546
78863
  function suggestAssumeRoleFromState(state, logicalId) {
78547
78864
  const logger = getLogger();
78548
78865
  const lambda = state.resources[logicalId];
@@ -78658,7 +78975,7 @@ function reorderArgs(argv) {
78658
78975
  }
78659
78976
  async function main() {
78660
78977
  const program = new Command17();
78661
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.82.0");
78978
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.83.0");
78662
78979
  program.addCommand(createBootstrapCommand());
78663
78980
  program.addCommand(createSynthCommand());
78664
78981
  program.addCommand(createListCommand());