@go-to-k/cdkd 0.126.0 → 0.128.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
@@ -34749,6 +34749,143 @@ function createStateCommand() {
34749
34749
  return cmd;
34750
34750
  }
34751
34751
 
34752
+ //#endregion
34753
+ //#region src/cli/upload-cfn-template.ts
34754
+ /**
34755
+ * CloudFormation `TemplateBody` hard limit (51,200 bytes). Templates larger
34756
+ * than this cannot be submitted inline and must be uploaded to S3 and
34757
+ * referenced via `TemplateURL` instead — see {@link uploadCfnTemplate}.
34758
+ */
34759
+ const CFN_TEMPLATE_BODY_LIMIT = 51200;
34760
+ /**
34761
+ * CloudFormation `TemplateURL` hard limit (1 MB / 1,048,576 bytes).
34762
+ * Templates larger than this are structurally unsubmittable through any
34763
+ * CloudFormation API — no S3 indirection helps. The caller surfaces a
34764
+ * pre-flight error pointing the user at template-splitting (nested stacks)
34765
+ * or shrinking inline asset payloads (`lambda.Code.fromAsset`).
34766
+ */
34767
+ const CFN_TEMPLATE_URL_LIMIT = 1048576;
34768
+ /**
34769
+ * Shared S3 key prefix for transient CFn templates uploaded by `cdkd import
34770
+ * --migrate-from-cloudformation` and `cdkd export`. Kept distinct from
34771
+ * cdkd's `cdkd/` state prefix so `state list` / `state info` never conflate
34772
+ * transient migration artifacts with persisted stack state. The prefix is
34773
+ * intentionally human-grep-able — leftovers (if cleanup fails) point
34774
+ * straight at the offending stack name.
34775
+ *
34776
+ * Re-used by both commands so operator-facing audit trails (CloudTrail
34777
+ * records of the migrate-tmp uploads) stay consistent across the two
34778
+ * flows.
34779
+ */
34780
+ const MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
34781
+ /**
34782
+ * Upload a CFn template body to the cdkd state bucket and return both a
34783
+ * virtual-hosted-style HTTPS URL CloudFormation can fetch via
34784
+ * `TemplateURL` and a `cleanup` callback that deletes the object (and
34785
+ * destroys the S3 client).
34786
+ *
34787
+ * The state bucket's actual region is resolved via `GetBucketLocation`
34788
+ * (cached per-process) so the upload client and the URL match the
34789
+ * bucket's region — the calling CLI's profile region is irrelevant here.
34790
+ *
34791
+ * Cleanup is the caller's responsibility: invoke `cleanup` in a `finally`
34792
+ * around the CFn call. CloudFormation copies the template into its own
34793
+ * internal storage during the synchronous `CreateChangeSet` /
34794
+ * `UpdateStack` API call, so the S3 object is no longer needed after that
34795
+ * call returns (success or failure).
34796
+ *
34797
+ * Shared between `cdkd import --migrate-from-cloudformation` (via
34798
+ * `retire-cfn-stack.ts`) and `cdkd export` (via `commands/export.ts`) so
34799
+ * the upload + cleanup contract is single-sourced.
34800
+ */
34801
+ async function uploadCfnTemplate(args) {
34802
+ const { bucket, body, stackName, format, s3ClientOpts } = args;
34803
+ const region = await resolveBucketRegion(bucket, {
34804
+ ...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
34805
+ ...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
34806
+ });
34807
+ const s3 = new S3Client({
34808
+ region,
34809
+ ...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
34810
+ ...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
34811
+ });
34812
+ const ext = format === "yaml" ? "yaml" : "json";
34813
+ const contentType = format === "yaml" ? "application/x-yaml" : "application/json";
34814
+ const key = `${MIGRATE_TMP_PREFIX}/${stackName}/${Date.now()}.${ext}`;
34815
+ try {
34816
+ await s3.send(new PutObjectCommand({
34817
+ Bucket: bucket,
34818
+ Key: key,
34819
+ Body: body,
34820
+ ContentType: contentType
34821
+ }));
34822
+ } catch (err) {
34823
+ s3.destroy();
34824
+ throw err;
34825
+ }
34826
+ const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
34827
+ const cleanup = async () => {
34828
+ try {
34829
+ await s3.send(new DeleteObjectCommand({
34830
+ Bucket: bucket,
34831
+ Key: key
34832
+ }));
34833
+ } finally {
34834
+ s3.destroy();
34835
+ }
34836
+ };
34837
+ return {
34838
+ url,
34839
+ cleanup
34840
+ };
34841
+ }
34842
+ /**
34843
+ * Threshold (in bytes) above which a single resource's serialized
34844
+ * `Properties` block is considered an "inline payload" worth surfacing as
34845
+ * a contributor to a template that exceeds the 1 MB CFn `TemplateURL`
34846
+ * ceiling. 4 KB matches the typical inline `Code.ZipFile` Lambda payload
34847
+ * that pushes a multi-resource CDK app over the wire-format limit.
34848
+ */
34849
+ const LARGE_INLINE_RESOURCE_THRESHOLD = 4096;
34850
+ /**
34851
+ * Walk a CFn template and surface every resource whose serialized
34852
+ * `Properties` block exceeds {@link LARGE_INLINE_RESOURCE_THRESHOLD}.
34853
+ * Used to build the actionable "offending resources" list in the
34854
+ * pre-flight error when a template exceeds the 1 MB `TemplateURL`
34855
+ * ceiling — typical culprits are inline `Code.ZipFile` Lambdas, inline
34856
+ * StepFunctions definitions, or large `AWS::CloudFormation::Stack`
34857
+ * bodies.
34858
+ *
34859
+ * Returns entries sorted by `approxBytes` descending so the user sees
34860
+ * the biggest contributor first. A non-CFn-template input (no
34861
+ * `Resources` object) returns an empty array.
34862
+ */
34863
+ function findLargeInlineResources(template, threshold = LARGE_INLINE_RESOURCE_THRESHOLD) {
34864
+ const result = [];
34865
+ const resources = template["Resources"];
34866
+ if (!resources || typeof resources !== "object" || Array.isArray(resources)) return result;
34867
+ for (const [logicalId, resource] of Object.entries(resources)) {
34868
+ if (!resource || typeof resource !== "object" || Array.isArray(resource)) continue;
34869
+ const r = resource;
34870
+ const resourceType = typeof r["Type"] === "string" ? r["Type"] : "<unknown>";
34871
+ const properties = r["Properties"];
34872
+ if (properties === void 0 || properties === null) continue;
34873
+ let approxBytes;
34874
+ try {
34875
+ approxBytes = JSON.stringify(properties).length;
34876
+ } catch {
34877
+ continue;
34878
+ }
34879
+ if (approxBytes >= threshold) result.push({
34880
+ logicalId,
34881
+ resourceType,
34882
+ approxBytes
34883
+ });
34884
+ }
34885
+ result.sort((a, b) => b.approxBytes - a.approxBytes);
34886
+ return result;
34887
+ }
34888
+
34752
34889
  //#endregion
34753
34890
  //#region src/cli/yaml-cfn.ts
34754
34891
  /**
@@ -35068,22 +35205,16 @@ const STABLE_TERMINAL_STATUSES = new Set([
35068
35205
  * UpdateStack TemplateBody hard limit (51,200 bytes). Templates larger than
35069
35206
  * this are uploaded to cdkd's state S3 bucket and submitted via `TemplateURL`
35070
35207
  * instead — see {@link uploadTemplateForUpdateStack}.
35208
+ *
35209
+ * Re-exported from `upload-cfn-template.ts` as the shared source of truth.
35071
35210
  */
35072
- const TEMPLATE_BODY_LIMIT = 51200;
35211
+ const TEMPLATE_BODY_LIMIT = CFN_TEMPLATE_BODY_LIMIT;
35073
35212
  /**
35074
35213
  * UpdateStack TemplateURL hard limit (1 MB / 1,048,576 bytes). Templates
35075
35214
  * larger than this cannot be submitted at all and require manual
35076
35215
  * intervention.
35077
35216
  */
35078
- const TEMPLATE_URL_LIMIT = 1048576;
35079
- /**
35080
- * S3 key prefix for the transient Retain-injected template uploaded by the
35081
- * `--migrate-from-cloudformation` flow when the template exceeds the inline
35082
- * 51,200-byte limit. Kept distinct from cdkd's `cdkd/` state prefix so
35083
- * `state list` / `state info` never conflate transient migration artifacts
35084
- * with persisted state.
35085
- */
35086
- const MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
35217
+ const TEMPLATE_URL_LIMIT = CFN_TEMPLATE_URL_LIMIT;
35087
35218
  /**
35088
35219
  * Retire a CloudFormation stack whose resources have just been adopted into
35089
35220
  * cdkd state. The 4-step procedure is the one AWS recommends for handing
@@ -35211,45 +35342,13 @@ async function retireCloudFormationStack(options) {
35211
35342
  * Exported for unit testing.
35212
35343
  */
35213
35344
  async function uploadTemplateForUpdateStack(args) {
35214
- const { bucket, body, cfnStackName, format, s3ClientOpts } = args;
35215
- const region = await resolveBucketRegion(bucket, {
35216
- ...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
35217
- ...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
35345
+ return uploadCfnTemplate({
35346
+ bucket: args.bucket,
35347
+ body: args.body,
35348
+ stackName: args.cfnStackName,
35349
+ ...args.format && { format: args.format },
35350
+ ...args.s3ClientOpts && { s3ClientOpts: args.s3ClientOpts }
35218
35351
  });
35219
- const s3 = new S3Client({
35220
- region,
35221
- ...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
35222
- ...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
35223
- });
35224
- const ext = format === "yaml" ? "yaml" : "json";
35225
- const contentType = format === "yaml" ? "application/x-yaml" : "application/json";
35226
- const key = `${MIGRATE_TMP_PREFIX}/${cfnStackName}/${Date.now()}.${ext}`;
35227
- try {
35228
- await s3.send(new PutObjectCommand({
35229
- Bucket: bucket,
35230
- Key: key,
35231
- Body: body,
35232
- ContentType: contentType
35233
- }));
35234
- } catch (err) {
35235
- s3.destroy();
35236
- throw err;
35237
- }
35238
- const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
35239
- const cleanup = async () => {
35240
- try {
35241
- await s3.send(new DeleteObjectCommand({
35242
- Bucket: bucket,
35243
- Key: key
35244
- }));
35245
- } finally {
35246
- s3.destroy();
35247
- }
35248
- };
35249
- return {
35250
- url,
35251
- cleanup
35252
- };
35253
35352
  }
35254
35353
  /**
35255
35354
  * Parse a CloudFormation template body (JSON or YAML), set
@@ -36503,7 +36602,7 @@ function extractLambdaProperties(stack, logicalId, resource, resources) {
36503
36602
  const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
36504
36603
  const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
36505
36604
  const code = props["Code"] ?? {};
36506
- const imageUri = extractImageUri(code["ImageUri"], logicalId, stack.stackName, resources);
36605
+ const imageUri = extractImageUri$1(code["ImageUri"], logicalId, stack.stackName, resources);
36507
36606
  if (imageUri !== void 0) return extractImageLambdaProperties({
36508
36607
  stack,
36509
36608
  logicalId,
@@ -36600,7 +36699,7 @@ function extractEphemeralStorageMb(props, logicalId) {
36600
36699
  * for genuinely unrecognized shapes so the caller's downstream ZIP-vs-
36601
36700
  * IMAGE branching can route to its existing error path.
36602
36701
  */
36603
- function extractImageUri(value, logicalId, stackName, resources) {
36702
+ function extractImageUri$1(value, logicalId, stackName, resources) {
36604
36703
  if (typeof value === "string" && value.length > 0) return value;
36605
36704
  if (value && typeof value === "object" && !Array.isArray(value)) {
36606
36705
  const obj = value;
@@ -40248,31 +40347,56 @@ function createContainerPool(specs, options) {
40248
40347
  /**
40249
40348
  * Spin up one new container for the given Lambda spec. Returns a
40250
40349
  * handle the caller can write into the entry's data structures.
40350
+ *
40351
+ * Branches on `spec.kind`:
40352
+ * - `'zip'`: bind-mount the function's local code dir at
40353
+ * `/var/task` (or `/var/runtime` for `provided.*` runtimes),
40354
+ * base image from `public.ecr.aws/lambda/<lang>:<v>`, CMD =
40355
+ * `[<Handler>]`.
40356
+ * - `'image'`: no code bind-mount (image already includes the
40357
+ * code), base image is the pre-built local tag, CMD =
40358
+ * `ImageConfig.Command` (may be empty), optional EntryPoint /
40359
+ * WorkingDirectory / --platform applied verbatim.
40251
40360
  */
40252
40361
  async function startOne(spec) {
40253
- const image = resolveRuntimeImage(spec.lambda.runtime);
40254
40362
  const hostPort = await pickFreePort();
40255
40363
  const name = `cdkd-local-${spec.lambda.logicalId}-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
40256
- logger.debug(`Starting container ${name} for ${spec.lambda.logicalId} on ${spec.containerHost}:${hostPort}`);
40257
- const optMount = spec.optDir ? [{
40258
- hostPath: spec.optDir,
40259
- containerPath: "/opt",
40260
- readOnly: true
40261
- }] : [];
40262
- const containerCodePath = resolveRuntimeCodeMountPath(spec.lambda.runtime);
40263
- const containerId = await runDetached({
40264
- image,
40265
- mounts: [{
40266
- hostPath: spec.codeDir,
40267
- containerPath: containerCodePath,
40364
+ logger.debug(`Starting container ${name} for ${spec.lambda.logicalId} (kind=${spec.kind}) on ${spec.containerHost}:${hostPort}`);
40365
+ let containerId;
40366
+ if (spec.kind === "zip") {
40367
+ const optMount = spec.optDir ? [{
40368
+ hostPath: spec.optDir,
40369
+ containerPath: "/opt",
40268
40370
  readOnly: true
40269
- }],
40270
- extraMounts: optMount,
40371
+ }] : [];
40372
+ const containerCodePath = resolveRuntimeCodeMountPath(spec.lambda.runtime);
40373
+ containerId = await runDetached({
40374
+ image: resolveRuntimeImage(spec.lambda.runtime),
40375
+ mounts: [{
40376
+ hostPath: spec.codeDir,
40377
+ containerPath: containerCodePath,
40378
+ readOnly: true
40379
+ }],
40380
+ extraMounts: optMount,
40381
+ env: spec.env,
40382
+ cmd: [spec.lambda.handler],
40383
+ hostPort,
40384
+ host: spec.containerHost,
40385
+ name,
40386
+ ...spec.debugPort !== void 0 && { debugPort: spec.debugPort },
40387
+ ...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs }
40388
+ });
40389
+ } else containerId = await runDetached({
40390
+ image: spec.image,
40391
+ mounts: [],
40271
40392
  env: spec.env,
40272
- cmd: [spec.lambda.handler],
40393
+ cmd: spec.command,
40273
40394
  hostPort,
40274
40395
  host: spec.containerHost,
40275
40396
  name,
40397
+ platform: spec.platform,
40398
+ ...spec.entryPoint !== void 0 && { entryPoint: spec.entryPoint },
40399
+ ...spec.workingDir !== void 0 && { workingDir: spec.workingDir },
40276
40400
  ...spec.debugPort !== void 0 && { debugPort: spec.debugPort },
40277
40401
  ...spec.tmpfs !== void 0 && { tmpfs: spec.tmpfs }
40278
40402
  });
@@ -44043,12 +44167,13 @@ async function localStartApiCommand(target, options) {
44043
44167
  inlineTmpDirs,
44044
44168
  layerTmpDirs,
44045
44169
  stateByStack,
44170
+ skipPull: options.pull === false,
44046
44171
  ...options.layerRoleArn !== void 0 && { layerRoleArn: options.layerRoleArn }
44047
44172
  });
44048
44173
  specs.set(logicalId, spec);
44049
44174
  }
44050
44175
  const distinctImages = /* @__PURE__ */ new Set();
44051
- for (const spec of specs.values()) distinctImages.add(resolveRuntimeImage(spec.lambda.runtime));
44176
+ for (const spec of specs.values()) if (spec.kind === "zip") distinctImages.add(resolveRuntimeImage(spec.lambda.runtime));
44052
44177
  for (const image of distinctImages) await pullImage(image, options.pull === false);
44053
44178
  return {
44054
44179
  routes: routesWithAuth,
@@ -44077,13 +44202,23 @@ async function localStartApiCommand(target, options) {
44077
44202
  /**
44078
44203
  * Compute the watched-asset list from a spec map. Pure helper —
44079
44204
  * keeps the side-effect (`lastAssetPaths.value = ...`) confined to
44080
- * the post-swap call sites (initial boot + post-reload). `codeDir`
44081
- * is either the unzipped asset directory or the inline-code tmpdir;
44082
- * both are watch-worthy.
44205
+ * the post-swap call sites (initial boot + post-reload). For ZIP
44206
+ * Lambdas `codeDir` is either the unzipped asset directory or the
44207
+ * inline-code tmpdir; both are watch-worthy. IMAGE Lambdas
44208
+ * (`kind: 'image'`) don't have a host-side bind-mount source — the
44209
+ * code is baked into the docker image at build time. Their build
44210
+ * context (Dockerfile + source directory) is rebuilt on every
44211
+ * reload via `synthesizeAndBuild` → `buildContainerSpec` →
44212
+ * `resolveContainerImageForStartApi`, so a source edit DOES trigger
44213
+ * rebuild AND the deterministic `image` tag changes — but watching
44214
+ * the build-context dir explicitly here is deferred to a follow-up
44215
+ * (the watched-asset list is currently sourced from `cdk.out/`
44216
+ * which transitively covers most container-Lambda asset dirs since
44217
+ * `cdk synth` re-stages them on every synth call).
44083
44218
  */
44084
44219
  const computeAssetPaths = (specs) => {
44085
44220
  const assetPaths = /* @__PURE__ */ new Set();
44086
- for (const spec of specs.values()) assetPaths.add(spec.codeDir);
44221
+ for (const spec of specs.values()) if (spec.kind === "zip") assetPaths.add(spec.codeDir);
44087
44222
  return [...assetPaths];
44088
44223
  };
44089
44224
  const initialMaterial = await synthesizeAndBuild();
@@ -44356,10 +44491,19 @@ function warnIamRoutes(routesWithAuth) {
44356
44491
  * missing, runtime not supported).
44357
44492
  */
44358
44493
  async function buildContainerSpec(args) {
44359
- const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, layerRoleArn } = args;
44494
+ const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn } = args;
44360
44495
  const lambda = resolveLambdaByLogicalId(logicalId, stacks);
44361
- const codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
44362
- const optDir = await materializeLambdaLayers$1(lambda.layers, layerTmpDirs, layerRoleArn);
44496
+ let codeDir;
44497
+ let optDir;
44498
+ let imageRef;
44499
+ let platform;
44500
+ if (lambda.kind === "zip") {
44501
+ codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
44502
+ optDir = await materializeLambdaLayers$1(lambda.layers, layerTmpDirs, layerRoleArn);
44503
+ } else {
44504
+ imageRef = (await resolveContainerImageForStartApi(lambda, skipPull)).imageRef;
44505
+ platform = architectureToPlatform(lambda.architecture);
44506
+ }
44363
44507
  let templateEnv = getTemplateEnv$1(lambda.resource);
44364
44508
  const stateBundle = stateByStack.get(lambda.stack.stackName);
44365
44509
  let stateAudit;
@@ -44399,7 +44543,8 @@ async function buildContainerSpec(args) {
44399
44543
  target: "/tmp",
44400
44544
  sizeMb: lambda.ephemeralStorageMb
44401
44545
  } : void 0;
44402
- return {
44546
+ if (lambda.kind === "zip") return {
44547
+ kind: "zip",
44403
44548
  lambda,
44404
44549
  codeDir,
44405
44550
  env: dockerEnv,
@@ -44408,6 +44553,60 @@ async function buildContainerSpec(args) {
44408
44553
  ...debugPort !== void 0 && { debugPort },
44409
44554
  ...tmpfs !== void 0 && { tmpfs }
44410
44555
  };
44556
+ return {
44557
+ kind: "image",
44558
+ lambda,
44559
+ image: imageRef,
44560
+ platform,
44561
+ command: lambda.imageConfig.command ?? [],
44562
+ ...lambda.imageConfig.entryPoint !== void 0 && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
44563
+ ...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory },
44564
+ env: dockerEnv,
44565
+ containerHost,
44566
+ ...debugPort !== void 0 && { debugPort },
44567
+ ...tmpfs !== void 0 && { tmpfs }
44568
+ };
44569
+ }
44570
+ /**
44571
+ * Resolve a container Lambda's local docker image — local build from
44572
+ * `cdk.out` asset manifest first, ECR-pull fallback when the asset
44573
+ * manifest has no matching entry. Mirrors `cdkd local invoke`'s
44574
+ * `resolveContainerImagePlan` shape; the start-api server doesn't
44575
+ * need the no-build flag (deterministic-tag cache reuse is automatic
44576
+ * across reloads because the per-Lambda tag is content-addressed).
44577
+ *
44578
+ * Same-account / same-region only on the ECR-pull path (matches the
44579
+ * `cdkd local invoke` PR 5 of #224 boundary). Cross-account /
44580
+ * cross-region ECR pull is the W2-1 deferred follow-up.
44581
+ */
44582
+ async function resolveContainerImageForStartApi(lambda, skipPull) {
44583
+ const logger = getLogger();
44584
+ const localBuild = await resolveLocalBuildPlan$1(lambda);
44585
+ if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, { architecture: lambda.architecture }) };
44586
+ if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI cdkd can authenticate against. Re-synthesize the CDK app (so cdk.out includes the build context) or deploy the image to ECR first.`);
44587
+ logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
44588
+ return { imageRef: await pullEcrImage(lambda.imageUri, { skipPull }) };
44589
+ }
44590
+ /**
44591
+ * Look up the docker image asset that backs a container Lambda.
44592
+ * Returns `undefined` when the asset manifest has no matching entry —
44593
+ * the caller falls back to the ECR-pull path.
44594
+ *
44595
+ * Mirrors `local-invoke.ts:resolveLocalBuildPlan`; kept separate so
44596
+ * the two commands evolve their asset-lookup heuristics independently.
44597
+ */
44598
+ async function resolveLocalBuildPlan$1(lambda) {
44599
+ const manifestPath = lambda.stack.assetManifestPath;
44600
+ if (!manifestPath) return void 0;
44601
+ const cdkOutDir = path.dirname(manifestPath);
44602
+ const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
44603
+ if (!manifest) return void 0;
44604
+ const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
44605
+ if (!entry) return void 0;
44606
+ return {
44607
+ asset: entry.asset,
44608
+ cdkOutDir
44609
+ };
44411
44610
  }
44412
44611
  /**
44413
44612
  * Build the `/opt` bind-mount source for a Lambda's layers. Mirrors
@@ -44469,15 +44668,23 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
44469
44668
  const resource = stack.template.Resources?.[logicalId];
44470
44669
  if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
44471
44670
  const props = resource.Properties ?? {};
44472
- const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
44473
- const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
44474
44671
  const memoryMb = typeof props["MemorySize"] === "number" ? props["MemorySize"] : 128;
44475
44672
  const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
44476
- if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property. Container-image Lambdas (Code.ImageUri) are not supported in cdkd local start-api v1.`);
44477
- if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
44478
44673
  const code = props["Code"] ?? {};
44479
- const imageUri = code["ImageUri"];
44480
- if (typeof imageUri === "string" || typeof imageUri === "object" && imageUri !== null && "Fn::Sub" in imageUri) throw new Error(`Lambda '${logicalId}' uses Code.ImageUri (container-image Lambda). 'cdkd local start-api' v1 supports ZIP Lambdas only — container-image support is deferred to a follow-up PR. Use 'cdkd local invoke' to exercise this function locally.`);
44674
+ const imageUri = extractImageUri(code["ImageUri"]);
44675
+ if (imageUri !== void 0) return resolveImageLambda({
44676
+ stack,
44677
+ logicalId,
44678
+ resource,
44679
+ props,
44680
+ memoryMb,
44681
+ timeoutSec,
44682
+ imageUri
44683
+ });
44684
+ const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
44685
+ const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
44686
+ if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. cdkd local start-api cannot tell if this is a ZIP or a container Lambda.`);
44687
+ if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
44481
44688
  const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
44482
44689
  let codePath = null;
44483
44690
  if (!inlineCode) codePath = resolveAssetCodePath(stack, logicalId, resource);
@@ -44501,6 +44708,66 @@ function resolveLambdaByLogicalId(logicalId, stacks) {
44501
44708
  throw new Error(`No AWS::Lambda::Function resource named '${logicalId}' found in target stacks. This is likely a synthesis bug — the route-discovery phase resolved a route to this logical ID.`);
44502
44709
  }
44503
44710
  /**
44711
+ * Extract `Code.ImageUri` across the shapes CDK actually synthesizes.
44712
+ * Mirrors the simpler subset of `lambda-resolver.ts:extractImageUri`
44713
+ * scoped to the shapes `cdkd local start-api` consumes — flat string
44714
+ * and `Fn::Sub` (the canonical asset shape for
44715
+ * `lambda.DockerImageCode.fromImageAsset`). `Fn::Join` shapes for
44716
+ * `lambda.DockerImageCode.fromEcr` are deferred to a follow-up: the
44717
+ * start-api boot flow doesn't yet load cdkd state up front, and the
44718
+ * `Fn::Join` resolver needs it to recover same-stack ECR repository
44719
+ * URIs. When the user hits the unsupported shape, the downstream
44720
+ * resolveLocalBuildPlan / pullEcrImage path surfaces a clear error.
44721
+ *
44722
+ * Returns `undefined` when the field is absent or non-recognized,
44723
+ * which routes the caller to the ZIP branch (with its existing
44724
+ * "no Runtime / no Handler" validations).
44725
+ */
44726
+ function extractImageUri(value) {
44727
+ if (typeof value === "string" && value.length > 0) return value;
44728
+ if (value && typeof value === "object" && !Array.isArray(value)) {
44729
+ const sub = value["Fn::Sub"];
44730
+ if (typeof sub === "string" && sub.length > 0) return sub;
44731
+ if (Array.isArray(sub) && typeof sub[0] === "string") return sub[0];
44732
+ }
44733
+ }
44734
+ /**
44735
+ * Build the IMAGE-variant `ResolvedStartApiLambda` from a Lambda
44736
+ * template entry with `Code.ImageUri`. Mirrors
44737
+ * `lambda-resolver.ts:extractImageLambdaProperties` but trimmed to the
44738
+ * fields `cdkd local start-api` actually consumes.
44739
+ */
44740
+ function resolveImageLambda(args) {
44741
+ const { stack, logicalId, resource, props, memoryMb, timeoutSec, imageUri } = args;
44742
+ const rawImageConfig = props["ImageConfig"] ?? {};
44743
+ const imageConfig = {};
44744
+ if (Array.isArray(rawImageConfig["Command"])) imageConfig.command = rawImageConfig["Command"].filter((s) => typeof s === "string");
44745
+ if (Array.isArray(rawImageConfig["EntryPoint"])) imageConfig.entryPoint = rawImageConfig["EntryPoint"].filter((s) => typeof s === "string");
44746
+ if (typeof rawImageConfig["WorkingDirectory"] === "string") imageConfig.workingDirectory = rawImageConfig["WorkingDirectory"];
44747
+ const arches = props["Architectures"];
44748
+ let architecture = "x86_64";
44749
+ if (Array.isArray(arches) && arches.length > 0) {
44750
+ const first = arches[0];
44751
+ if (first === "arm64") architecture = "arm64";
44752
+ else if (first === "x86_64") architecture = "x86_64";
44753
+ else throw new Error(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'. cdkd local start-api supports x86_64 and arm64.`);
44754
+ }
44755
+ const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
44756
+ return {
44757
+ kind: "image",
44758
+ stack,
44759
+ logicalId,
44760
+ resource,
44761
+ memoryMb,
44762
+ timeoutSec,
44763
+ imageUri,
44764
+ imageConfig,
44765
+ architecture,
44766
+ layers: [],
44767
+ ...ephemeralStorageMb !== void 0 && { ephemeralStorageMb }
44768
+ };
44769
+ }
44770
+ /**
44504
44771
  * Locate the Lambda's local code directory using the CDK-blessed
44505
44772
  * `Metadata['aws:asset:path']` hint. Bind-mounted directly at
44506
44773
  * `/var/task` (read-only) by the docker-runner.
@@ -47011,7 +47278,10 @@ async function exportCommand(stackArg, options) {
47011
47278
  const phase1Template = filterTemplateForImport(template, phase1Imports);
47012
47279
  const injectedCount = injectDeletionPolicyForImport(phase1Template);
47013
47280
  if (injectedCount > 0) logger.info(`Injected DeletionPolicy: Delete on ${injectedCount} resource(s) without an explicit DeletionPolicy (required by CFn IMPORT — matches the CDK/CFn default for resources without RemovalPolicy).`);
47014
- await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters, templateFormat);
47281
+ await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters, templateFormat, {
47282
+ stateBucket,
47283
+ ...options.profile && { s3ClientOpts: { profile: options.profile } }
47284
+ });
47015
47285
  logger.info(`✓ Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`);
47016
47286
  if (recreateBeforePhase2.length > 0) for (const entry of recreateBeforePhase2) {
47017
47287
  const handler = PRE_DELETE_HANDLERS[entry.resourceType];
@@ -47028,7 +47298,10 @@ async function exportCommand(stackArg, options) {
47028
47298
  const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
47029
47299
  if (phase2Count > 0) try {
47030
47300
  const phase2Template = applyImportOverlayForPhase2(template, phase1Imports);
47031
- await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat);
47301
+ await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat, {
47302
+ stateBucket,
47303
+ ...options.profile && { s3ClientOpts: { profile: options.profile } }
47304
+ });
47032
47305
  const parts = [];
47033
47306
  if (phase2Creates.length > 0) parts.push(`${phase2Creates.length} non-importable resource(s) CREATEd`);
47034
47307
  if (recreateBeforePhase2.length > 0) parts.push(`${recreateBeforePhase2.length} IMPORT-unsupported resource(s) re-CREATEd`);
@@ -47644,7 +47917,84 @@ function printPlan(plan, cfnStackName) {
47644
47917
  }
47645
47918
  logger.info("");
47646
47919
  }
47647
- async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters, templateFormat = "json") {
47920
+ /**
47921
+ * Decide whether to submit a CFn changeset's template inline via
47922
+ * `TemplateBody` or upload it to the cdkd state bucket and submit via
47923
+ * `TemplateURL`. Returns a discriminated outcome:
47924
+ *
47925
+ * - `kind: 'inline'` — payload <= 51,200 bytes; pass `TemplateBody`
47926
+ * directly. No S3 round-trip; the caller's `finally` cleanup is a
47927
+ * no-op.
47928
+ * - `kind: 'url'` — payload in (51,200, 1,048,576] bytes; helper
47929
+ * uploaded to `cdkd-migrate-tmp/<stackName>/<ts>.{json,yaml}`. The
47930
+ * caller MUST invoke the returned `cleanup` in a `finally` so the
47931
+ * transient object is deleted regardless of CFn success / failure.
47932
+ *
47933
+ * Payloads > 1,048,576 bytes throw pre-flight (the 1 MB ceiling applies
47934
+ * to every CFn API surface; no S3 indirection helps). The error names the
47935
+ * top inline-payload contributors so the user knows what to shrink.
47936
+ *
47937
+ * `phaseLabel` is interpolated into the pre-flight error so the user
47938
+ * sees which phase tripped the ceiling ("phase-1 IMPORT" vs "phase-2
47939
+ * UPDATE").
47940
+ *
47941
+ * When `uploadOpts` is undefined (e.g. unit tests that exercise only the
47942
+ * inline path), templates over the inline limit throw with a clear
47943
+ * "no upload bucket configured" error rather than silently failing on a
47944
+ * downstream CFn rejection.
47945
+ *
47946
+ * `templateFormat` (default `'json'`) drives the transient S3 object's
47947
+ * key suffix + Content-Type so YAML-authored templates stay YAML on the
47948
+ * wire (`.yaml` / `application/x-yaml`).
47949
+ */
47950
+ async function selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, phaseLabel, templateFormat = "json") {
47951
+ const logger = getLogger();
47952
+ if (templateBody.length <= 51200) return {
47953
+ kind: "inline",
47954
+ templateBody,
47955
+ cleanup: async () => void 0
47956
+ };
47957
+ if (templateBody.length > 1048576) {
47958
+ const offenders = findLargeInlineResources(template);
47959
+ let detail = "";
47960
+ if (offenders.length > 0) {
47961
+ detail = `\nLargest inline payloads (move these to lambda.Code.fromAsset or split into nested stacks):\n${offenders.slice(0, 10).map((o) => ` - ${o.logicalId} (${o.resourceType}): ~${o.approxBytes} bytes`).join("\n")}`;
47962
+ if (offenders.length > 10) detail += `\n (and ${offenders.length - 10} more above the 4096-byte threshold)`;
47963
+ }
47964
+ throw new Error(`${phaseLabel} template is ${templateBody.length} bytes, over the ${CFN_TEMPLATE_URL_LIMIT}-byte CloudFormation TemplateURL limit. Templates that large cannot be submitted to CloudFormation — shrink inline payloads (e.g. inline Lambda Code.ZipFile larger than ~4 KB → switch to lambda.Code.fromAsset) or split the stack into nested AWS::CloudFormation::Stack resources.${detail}`);
47965
+ }
47966
+ if (!uploadOpts) throw new Error(`${phaseLabel} template is ${templateBody.length} bytes, over the inline ${CFN_TEMPLATE_BODY_LIMIT}-byte TemplateBody limit, but no upload bucket was provided to selectChangeSetTemplateSource — pass uploadOpts to enable the TemplateURL upload path.`);
47967
+ logger.info(` Template is ${templateBody.length} bytes (over ${CFN_TEMPLATE_BODY_LIMIT} inline limit) — uploading to state bucket '${uploadOpts.stateBucket}'.`);
47968
+ const uploaded = await uploadCfnTemplate({
47969
+ bucket: uploadOpts.stateBucket,
47970
+ body: templateBody,
47971
+ stackName,
47972
+ format: templateFormat,
47973
+ ...uploadOpts.s3ClientOpts && { s3ClientOpts: uploadOpts.s3ClientOpts }
47974
+ });
47975
+ return {
47976
+ kind: "url",
47977
+ templateUrl: uploaded.url,
47978
+ cleanup: uploaded.cleanup
47979
+ };
47980
+ }
47981
+ /**
47982
+ * Best-effort wrapper around the `cleanup` callback returned by
47983
+ * {@link selectChangeSetTemplateSource}. Mirrors the warn-on-failure
47984
+ * pattern from `retireCloudFormationStack` so a stranded `cdkd-migrate-tmp/`
47985
+ * object never blocks the calling command — it lives under an obviously
47986
+ * named prefix and can be reaped manually.
47987
+ */
47988
+ async function runTemplateUploadCleanup(cleanup, bucket) {
47989
+ const logger = getLogger();
47990
+ try {
47991
+ await cleanup();
47992
+ } catch (cleanupErr) {
47993
+ const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
47994
+ logger.warn(`Failed to delete temporary template upload from '${bucket}'. Clean up manually under prefix 'cdkd-migrate-tmp/'. Cause: ${msg}`);
47995
+ }
47996
+ }
47997
+ async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters, templateFormat = "json", uploadOpts) {
47648
47998
  const logger = getLogger();
47649
47999
  const changeSetName = `cdkd-migrate-${Date.now()}`;
47650
48000
  const templateBody = stringifyCfnTemplate(template, templateFormat);
@@ -47654,67 +48004,71 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan, para
47654
48004
  ResourceIdentifier: entry.resourceIdentifier
47655
48005
  }));
47656
48006
  logger.info(`Creating IMPORT changeset '${changeSetName}' for stack '${stackName}' (${plan.length} resource(s), ${templateBody.length} bytes)...`);
47657
- if (templateBody.length > 51200) throw new Error(`Filtered template is ${templateBody.length} bytes, over the 51,200-byte inline TemplateBody limit. Templates that large require TemplateURL upload (not yet implemented for cdkd export; please file an issue if you hit this).`);
48007
+ const source = await selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, "Filtered phase-1 IMPORT", templateFormat);
47658
48008
  try {
47659
- await cfnClient.send(new CreateChangeSetCommand({
47660
- StackName: stackName,
47661
- ChangeSetName: changeSetName,
47662
- ChangeSetType: "IMPORT",
47663
- TemplateBody: templateBody,
47664
- ResourcesToImport: resourcesToImport,
47665
- ...parameters.length > 0 && { Parameters: parameters },
47666
- Capabilities: [
47667
- "CAPABILITY_IAM",
47668
- "CAPABILITY_NAMED_IAM",
47669
- "CAPABILITY_AUTO_EXPAND"
47670
- ]
47671
- }));
47672
- } catch (err) {
47673
- const msg = err instanceof Error ? err.message : String(err);
47674
- throw new Error(`Failed to create IMPORT changeset: ${msg}`);
47675
- }
47676
- try {
47677
- await waitUntilChangeSetCreateComplete({
47678
- client: cfnClient,
47679
- maxWaitTime: 600
47680
- }, {
47681
- StackName: stackName,
47682
- ChangeSetName: changeSetName
47683
- });
47684
- } catch (err) {
47685
48009
  try {
47686
- const reason = (await cfnClient.send(new DescribeChangeSetCommand({
48010
+ await cfnClient.send(new CreateChangeSetCommand({
48011
+ StackName: stackName,
48012
+ ChangeSetName: changeSetName,
48013
+ ChangeSetType: "IMPORT",
48014
+ ...source.kind === "inline" ? { TemplateBody: source.templateBody } : { TemplateURL: source.templateUrl },
48015
+ ResourcesToImport: resourcesToImport,
48016
+ ...parameters.length > 0 && { Parameters: parameters },
48017
+ Capabilities: [
48018
+ "CAPABILITY_IAM",
48019
+ "CAPABILITY_NAMED_IAM",
48020
+ "CAPABILITY_AUTO_EXPAND"
48021
+ ]
48022
+ }));
48023
+ } catch (err) {
48024
+ const msg = err instanceof Error ? err.message : String(err);
48025
+ throw new Error(`Failed to create IMPORT changeset: ${msg}`);
48026
+ }
48027
+ try {
48028
+ await waitUntilChangeSetCreateComplete({
48029
+ client: cfnClient,
48030
+ maxWaitTime: 600
48031
+ }, {
47687
48032
  StackName: stackName,
47688
48033
  ChangeSetName: changeSetName
47689
- }))).StatusReason ?? "unknown";
48034
+ });
48035
+ } catch (err) {
48036
+ try {
48037
+ const reason = (await cfnClient.send(new DescribeChangeSetCommand({
48038
+ StackName: stackName,
48039
+ ChangeSetName: changeSetName
48040
+ }))).StatusReason ?? "unknown";
48041
+ await cfnClient.send(new DeleteChangeSetCommand({
48042
+ StackName: stackName,
48043
+ ChangeSetName: changeSetName
48044
+ })).catch(() => {});
48045
+ throw new Error(`IMPORT changeset FAILED: ${reason}`);
48046
+ } catch (innerErr) {
48047
+ if (innerErr instanceof Error && innerErr.message.startsWith("IMPORT changeset FAILED")) throw innerErr;
48048
+ throw err;
48049
+ }
48050
+ }
48051
+ logger.info(`Executing IMPORT changeset...`);
48052
+ try {
48053
+ await cfnClient.send(new ExecuteChangeSetCommand({
48054
+ StackName: stackName,
48055
+ ChangeSetName: changeSetName
48056
+ }));
48057
+ await waitUntilStackImportComplete({
48058
+ client: cfnClient,
48059
+ maxWaitTime: 3600
48060
+ }, { StackName: stackName });
48061
+ } catch (err) {
48062
+ const failureSummary = await collectImportFailureSummary(cfnClient, stackName).catch(() => "");
47690
48063
  await cfnClient.send(new DeleteChangeSetCommand({
47691
48064
  StackName: stackName,
47692
48065
  ChangeSetName: changeSetName
47693
48066
  })).catch(() => {});
47694
- throw new Error(`IMPORT changeset FAILED: ${reason}`);
47695
- } catch (innerErr) {
47696
- if (innerErr instanceof Error && innerErr.message.startsWith("IMPORT changeset FAILED")) throw innerErr;
48067
+ if (failureSummary) throw new Error(`IMPORT changeset failed:\n${failureSummary}`, { cause: err });
47697
48068
  throw err;
47698
48069
  }
47699
- }
47700
- logger.info(`Executing IMPORT changeset...`);
47701
- try {
47702
- await cfnClient.send(new ExecuteChangeSetCommand({
47703
- StackName: stackName,
47704
- ChangeSetName: changeSetName
47705
- }));
47706
- await waitUntilStackImportComplete({
47707
- client: cfnClient,
47708
- maxWaitTime: 3600
47709
- }, { StackName: stackName });
47710
- } catch (err) {
47711
- const failureSummary = await collectImportFailureSummary(cfnClient, stackName).catch(() => "");
47712
- await cfnClient.send(new DeleteChangeSetCommand({
47713
- StackName: stackName,
47714
- ChangeSetName: changeSetName
47715
- })).catch(() => {});
47716
- if (failureSummary) throw new Error(`IMPORT changeset failed:\n${failureSummary}`, { cause: err });
47717
- throw err;
48070
+ } finally {
48071
+ await runTemplateUploadCleanup(source.cleanup, uploadOpts?.stateBucket ?? "");
47718
48072
  }
47719
48073
  }
47720
48074
  /**
@@ -47758,69 +48112,73 @@ async function collectImportFailureSummary(cfnClient, stackName) {
47758
48112
  * (cdkd state is intentionally NOT deleted between phases, so a phase-2
47759
48113
  * failure leaves a recoverable state).
47760
48114
  */
47761
- async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json") {
48115
+ async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json", uploadOpts) {
47762
48116
  const logger = getLogger();
47763
48117
  const changeSetName = `cdkd-phase2-${Date.now()}`;
47764
48118
  const templateBody = stringifyCfnTemplate(template, templateFormat);
47765
- if (templateBody.length > 51200) throw new Error(`Full template is ${templateBody.length} bytes, over the 51,200-byte inline TemplateBody limit for phase-2 UPDATE. TemplateURL upload is not yet implemented.`);
47766
48119
  logger.info(`Creating UPDATE changeset '${changeSetName}' for phase 2 (${templateBody.length} bytes)...`);
48120
+ const source = await selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, "Phase-2 UPDATE", templateFormat);
47767
48121
  try {
47768
- await cfnClient.send(new CreateChangeSetCommand({
47769
- StackName: stackName,
47770
- ChangeSetName: changeSetName,
47771
- ChangeSetType: "UPDATE",
47772
- TemplateBody: templateBody,
47773
- ...parameters.length > 0 && { Parameters: parameters },
47774
- Capabilities: [
47775
- "CAPABILITY_IAM",
47776
- "CAPABILITY_NAMED_IAM",
47777
- "CAPABILITY_AUTO_EXPAND"
47778
- ]
47779
- }));
47780
- } catch (err) {
47781
- const msg = err instanceof Error ? err.message : String(err);
47782
- throw new Error(`Failed to create UPDATE changeset: ${msg}`);
47783
- }
47784
- try {
47785
- await waitUntilChangeSetCreateComplete({
47786
- client: cfnClient,
47787
- maxWaitTime: 600
47788
- }, {
47789
- StackName: stackName,
47790
- ChangeSetName: changeSetName
47791
- });
47792
- } catch (err) {
47793
48122
  try {
47794
- const reason = (await cfnClient.send(new DescribeChangeSetCommand({
48123
+ await cfnClient.send(new CreateChangeSetCommand({
48124
+ StackName: stackName,
48125
+ ChangeSetName: changeSetName,
48126
+ ChangeSetType: "UPDATE",
48127
+ ...source.kind === "inline" ? { TemplateBody: source.templateBody } : { TemplateURL: source.templateUrl },
48128
+ ...parameters.length > 0 && { Parameters: parameters },
48129
+ Capabilities: [
48130
+ "CAPABILITY_IAM",
48131
+ "CAPABILITY_NAMED_IAM",
48132
+ "CAPABILITY_AUTO_EXPAND"
48133
+ ]
48134
+ }));
48135
+ } catch (err) {
48136
+ const msg = err instanceof Error ? err.message : String(err);
48137
+ throw new Error(`Failed to create UPDATE changeset: ${msg}`);
48138
+ }
48139
+ try {
48140
+ await waitUntilChangeSetCreateComplete({
48141
+ client: cfnClient,
48142
+ maxWaitTime: 600
48143
+ }, {
47795
48144
  StackName: stackName,
47796
48145
  ChangeSetName: changeSetName
47797
- }))).StatusReason ?? "unknown";
48146
+ });
48147
+ } catch (err) {
48148
+ try {
48149
+ const reason = (await cfnClient.send(new DescribeChangeSetCommand({
48150
+ StackName: stackName,
48151
+ ChangeSetName: changeSetName
48152
+ }))).StatusReason ?? "unknown";
48153
+ await cfnClient.send(new DeleteChangeSetCommand({
48154
+ StackName: stackName,
48155
+ ChangeSetName: changeSetName
48156
+ })).catch(() => {});
48157
+ throw new Error(`UPDATE changeset FAILED: ${reason}`);
48158
+ } catch (innerErr) {
48159
+ if (innerErr instanceof Error && innerErr.message.startsWith("UPDATE changeset FAILED")) throw innerErr;
48160
+ throw err;
48161
+ }
48162
+ }
48163
+ logger.info(`Executing UPDATE changeset...`);
48164
+ try {
48165
+ await cfnClient.send(new ExecuteChangeSetCommand({
48166
+ StackName: stackName,
48167
+ ChangeSetName: changeSetName
48168
+ }));
48169
+ await waitUntilStackUpdateComplete({
48170
+ client: cfnClient,
48171
+ maxWaitTime: 3600
48172
+ }, { StackName: stackName });
48173
+ } catch (err) {
47798
48174
  await cfnClient.send(new DeleteChangeSetCommand({
47799
48175
  StackName: stackName,
47800
48176
  ChangeSetName: changeSetName
47801
48177
  })).catch(() => {});
47802
- throw new Error(`UPDATE changeset FAILED: ${reason}`);
47803
- } catch (innerErr) {
47804
- if (innerErr instanceof Error && innerErr.message.startsWith("UPDATE changeset FAILED")) throw innerErr;
47805
48178
  throw err;
47806
48179
  }
47807
- }
47808
- logger.info(`Executing UPDATE changeset...`);
47809
- try {
47810
- await cfnClient.send(new ExecuteChangeSetCommand({
47811
- StackName: stackName,
47812
- ChangeSetName: changeSetName
47813
- }));
47814
- await waitUntilStackUpdateComplete({
47815
- client: cfnClient,
47816
- maxWaitTime: 3600
47817
- }, { StackName: stackName });
47818
- } catch (err) {
47819
- await cfnClient.send(new DeleteChangeSetCommand({
47820
- StackName: stackName,
47821
- ChangeSetName: changeSetName
47822
- })).catch(() => {});
47823
- throw err;
48180
+ } finally {
48181
+ await runTemplateUploadCleanup(source.cleanup, uploadOpts?.stateBucket ?? "");
47824
48182
  }
47825
48183
  }
47826
48184
  /**
@@ -47939,7 +48297,7 @@ function reorderArgs(argv) {
47939
48297
  */
47940
48298
  async function main() {
47941
48299
  const program = new Command();
47942
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.126.0");
48300
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.128.0");
47943
48301
  program.addCommand(createBootstrapCommand());
47944
48302
  program.addCommand(createSynthCommand());
47945
48303
  program.addCommand(createListCommand());