@go-to-k/cdkd 0.126.0 → 0.127.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
@@ -47011,7 +47110,10 @@ async function exportCommand(stackArg, options) {
47011
47110
  const phase1Template = filterTemplateForImport(template, phase1Imports);
47012
47111
  const injectedCount = injectDeletionPolicyForImport(phase1Template);
47013
47112
  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);
47113
+ await executeImportChangeSet(awsClients.cloudFormation, cfnStackName, phase1Template, phase1Imports, cfnParameters, templateFormat, {
47114
+ stateBucket,
47115
+ ...options.profile && { s3ClientOpts: { profile: options.profile } }
47116
+ });
47015
47117
  logger.info(`✓ Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`);
47016
47118
  if (recreateBeforePhase2.length > 0) for (const entry of recreateBeforePhase2) {
47017
47119
  const handler = PRE_DELETE_HANDLERS[entry.resourceType];
@@ -47028,7 +47130,10 @@ async function exportCommand(stackArg, options) {
47028
47130
  const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
47029
47131
  if (phase2Count > 0) try {
47030
47132
  const phase2Template = applyImportOverlayForPhase2(template, phase1Imports);
47031
- await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat);
47133
+ await executeUpdateChangeSet(awsClients.cloudFormation, cfnStackName, phase2Template, cfnParameters, templateFormat, {
47134
+ stateBucket,
47135
+ ...options.profile && { s3ClientOpts: { profile: options.profile } }
47136
+ });
47032
47137
  const parts = [];
47033
47138
  if (phase2Creates.length > 0) parts.push(`${phase2Creates.length} non-importable resource(s) CREATEd`);
47034
47139
  if (recreateBeforePhase2.length > 0) parts.push(`${recreateBeforePhase2.length} IMPORT-unsupported resource(s) re-CREATEd`);
@@ -47644,7 +47749,84 @@ function printPlan(plan, cfnStackName) {
47644
47749
  }
47645
47750
  logger.info("");
47646
47751
  }
47647
- async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters, templateFormat = "json") {
47752
+ /**
47753
+ * Decide whether to submit a CFn changeset's template inline via
47754
+ * `TemplateBody` or upload it to the cdkd state bucket and submit via
47755
+ * `TemplateURL`. Returns a discriminated outcome:
47756
+ *
47757
+ * - `kind: 'inline'` — payload <= 51,200 bytes; pass `TemplateBody`
47758
+ * directly. No S3 round-trip; the caller's `finally` cleanup is a
47759
+ * no-op.
47760
+ * - `kind: 'url'` — payload in (51,200, 1,048,576] bytes; helper
47761
+ * uploaded to `cdkd-migrate-tmp/<stackName>/<ts>.{json,yaml}`. The
47762
+ * caller MUST invoke the returned `cleanup` in a `finally` so the
47763
+ * transient object is deleted regardless of CFn success / failure.
47764
+ *
47765
+ * Payloads > 1,048,576 bytes throw pre-flight (the 1 MB ceiling applies
47766
+ * to every CFn API surface; no S3 indirection helps). The error names the
47767
+ * top inline-payload contributors so the user knows what to shrink.
47768
+ *
47769
+ * `phaseLabel` is interpolated into the pre-flight error so the user
47770
+ * sees which phase tripped the ceiling ("phase-1 IMPORT" vs "phase-2
47771
+ * UPDATE").
47772
+ *
47773
+ * When `uploadOpts` is undefined (e.g. unit tests that exercise only the
47774
+ * inline path), templates over the inline limit throw with a clear
47775
+ * "no upload bucket configured" error rather than silently failing on a
47776
+ * downstream CFn rejection.
47777
+ *
47778
+ * `templateFormat` (default `'json'`) drives the transient S3 object's
47779
+ * key suffix + Content-Type so YAML-authored templates stay YAML on the
47780
+ * wire (`.yaml` / `application/x-yaml`).
47781
+ */
47782
+ async function selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, phaseLabel, templateFormat = "json") {
47783
+ const logger = getLogger();
47784
+ if (templateBody.length <= 51200) return {
47785
+ kind: "inline",
47786
+ templateBody,
47787
+ cleanup: async () => void 0
47788
+ };
47789
+ if (templateBody.length > 1048576) {
47790
+ const offenders = findLargeInlineResources(template);
47791
+ let detail = "";
47792
+ if (offenders.length > 0) {
47793
+ 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")}`;
47794
+ if (offenders.length > 10) detail += `\n (and ${offenders.length - 10} more above the 4096-byte threshold)`;
47795
+ }
47796
+ 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}`);
47797
+ }
47798
+ 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.`);
47799
+ logger.info(` Template is ${templateBody.length} bytes (over ${CFN_TEMPLATE_BODY_LIMIT} inline limit) — uploading to state bucket '${uploadOpts.stateBucket}'.`);
47800
+ const uploaded = await uploadCfnTemplate({
47801
+ bucket: uploadOpts.stateBucket,
47802
+ body: templateBody,
47803
+ stackName,
47804
+ format: templateFormat,
47805
+ ...uploadOpts.s3ClientOpts && { s3ClientOpts: uploadOpts.s3ClientOpts }
47806
+ });
47807
+ return {
47808
+ kind: "url",
47809
+ templateUrl: uploaded.url,
47810
+ cleanup: uploaded.cleanup
47811
+ };
47812
+ }
47813
+ /**
47814
+ * Best-effort wrapper around the `cleanup` callback returned by
47815
+ * {@link selectChangeSetTemplateSource}. Mirrors the warn-on-failure
47816
+ * pattern from `retireCloudFormationStack` so a stranded `cdkd-migrate-tmp/`
47817
+ * object never blocks the calling command — it lives under an obviously
47818
+ * named prefix and can be reaped manually.
47819
+ */
47820
+ async function runTemplateUploadCleanup(cleanup, bucket) {
47821
+ const logger = getLogger();
47822
+ try {
47823
+ await cleanup();
47824
+ } catch (cleanupErr) {
47825
+ const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
47826
+ logger.warn(`Failed to delete temporary template upload from '${bucket}'. Clean up manually under prefix 'cdkd-migrate-tmp/'. Cause: ${msg}`);
47827
+ }
47828
+ }
47829
+ async function executeImportChangeSet(cfnClient, stackName, template, plan, parameters, templateFormat = "json", uploadOpts) {
47648
47830
  const logger = getLogger();
47649
47831
  const changeSetName = `cdkd-migrate-${Date.now()}`;
47650
47832
  const templateBody = stringifyCfnTemplate(template, templateFormat);
@@ -47654,67 +47836,71 @@ async function executeImportChangeSet(cfnClient, stackName, template, plan, para
47654
47836
  ResourceIdentifier: entry.resourceIdentifier
47655
47837
  }));
47656
47838
  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).`);
47658
- 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
- }
47839
+ const source = await selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, "Filtered phase-1 IMPORT", templateFormat);
47676
47840
  try {
47677
- await waitUntilChangeSetCreateComplete({
47678
- client: cfnClient,
47679
- maxWaitTime: 600
47680
- }, {
47681
- StackName: stackName,
47682
- ChangeSetName: changeSetName
47683
- });
47684
- } catch (err) {
47685
47841
  try {
47686
- const reason = (await cfnClient.send(new DescribeChangeSetCommand({
47842
+ await cfnClient.send(new CreateChangeSetCommand({
47843
+ StackName: stackName,
47844
+ ChangeSetName: changeSetName,
47845
+ ChangeSetType: "IMPORT",
47846
+ ...source.kind === "inline" ? { TemplateBody: source.templateBody } : { TemplateURL: source.templateUrl },
47847
+ ResourcesToImport: resourcesToImport,
47848
+ ...parameters.length > 0 && { Parameters: parameters },
47849
+ Capabilities: [
47850
+ "CAPABILITY_IAM",
47851
+ "CAPABILITY_NAMED_IAM",
47852
+ "CAPABILITY_AUTO_EXPAND"
47853
+ ]
47854
+ }));
47855
+ } catch (err) {
47856
+ const msg = err instanceof Error ? err.message : String(err);
47857
+ throw new Error(`Failed to create IMPORT changeset: ${msg}`);
47858
+ }
47859
+ try {
47860
+ await waitUntilChangeSetCreateComplete({
47861
+ client: cfnClient,
47862
+ maxWaitTime: 600
47863
+ }, {
47864
+ StackName: stackName,
47865
+ ChangeSetName: changeSetName
47866
+ });
47867
+ } catch (err) {
47868
+ try {
47869
+ const reason = (await cfnClient.send(new DescribeChangeSetCommand({
47870
+ StackName: stackName,
47871
+ ChangeSetName: changeSetName
47872
+ }))).StatusReason ?? "unknown";
47873
+ await cfnClient.send(new DeleteChangeSetCommand({
47874
+ StackName: stackName,
47875
+ ChangeSetName: changeSetName
47876
+ })).catch(() => {});
47877
+ throw new Error(`IMPORT changeset FAILED: ${reason}`);
47878
+ } catch (innerErr) {
47879
+ if (innerErr instanceof Error && innerErr.message.startsWith("IMPORT changeset FAILED")) throw innerErr;
47880
+ throw err;
47881
+ }
47882
+ }
47883
+ logger.info(`Executing IMPORT changeset...`);
47884
+ try {
47885
+ await cfnClient.send(new ExecuteChangeSetCommand({
47687
47886
  StackName: stackName,
47688
47887
  ChangeSetName: changeSetName
47689
- }))).StatusReason ?? "unknown";
47888
+ }));
47889
+ await waitUntilStackImportComplete({
47890
+ client: cfnClient,
47891
+ maxWaitTime: 3600
47892
+ }, { StackName: stackName });
47893
+ } catch (err) {
47894
+ const failureSummary = await collectImportFailureSummary(cfnClient, stackName).catch(() => "");
47690
47895
  await cfnClient.send(new DeleteChangeSetCommand({
47691
47896
  StackName: stackName,
47692
47897
  ChangeSetName: changeSetName
47693
47898
  })).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;
47899
+ if (failureSummary) throw new Error(`IMPORT changeset failed:\n${failureSummary}`, { cause: err });
47697
47900
  throw err;
47698
47901
  }
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;
47902
+ } finally {
47903
+ await runTemplateUploadCleanup(source.cleanup, uploadOpts?.stateBucket ?? "");
47718
47904
  }
47719
47905
  }
47720
47906
  /**
@@ -47758,69 +47944,73 @@ async function collectImportFailureSummary(cfnClient, stackName) {
47758
47944
  * (cdkd state is intentionally NOT deleted between phases, so a phase-2
47759
47945
  * failure leaves a recoverable state).
47760
47946
  */
47761
- async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json") {
47947
+ async function executeUpdateChangeSet(cfnClient, stackName, template, parameters, templateFormat = "json", uploadOpts) {
47762
47948
  const logger = getLogger();
47763
47949
  const changeSetName = `cdkd-phase2-${Date.now()}`;
47764
47950
  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
47951
  logger.info(`Creating UPDATE changeset '${changeSetName}' for phase 2 (${templateBody.length} bytes)...`);
47952
+ const source = await selectChangeSetTemplateSource(template, templateBody, uploadOpts, stackName, "Phase-2 UPDATE", templateFormat);
47767
47953
  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
47954
  try {
47794
- const reason = (await cfnClient.send(new DescribeChangeSetCommand({
47955
+ await cfnClient.send(new CreateChangeSetCommand({
47956
+ StackName: stackName,
47957
+ ChangeSetName: changeSetName,
47958
+ ChangeSetType: "UPDATE",
47959
+ ...source.kind === "inline" ? { TemplateBody: source.templateBody } : { TemplateURL: source.templateUrl },
47960
+ ...parameters.length > 0 && { Parameters: parameters },
47961
+ Capabilities: [
47962
+ "CAPABILITY_IAM",
47963
+ "CAPABILITY_NAMED_IAM",
47964
+ "CAPABILITY_AUTO_EXPAND"
47965
+ ]
47966
+ }));
47967
+ } catch (err) {
47968
+ const msg = err instanceof Error ? err.message : String(err);
47969
+ throw new Error(`Failed to create UPDATE changeset: ${msg}`);
47970
+ }
47971
+ try {
47972
+ await waitUntilChangeSetCreateComplete({
47973
+ client: cfnClient,
47974
+ maxWaitTime: 600
47975
+ }, {
47976
+ StackName: stackName,
47977
+ ChangeSetName: changeSetName
47978
+ });
47979
+ } catch (err) {
47980
+ try {
47981
+ const reason = (await cfnClient.send(new DescribeChangeSetCommand({
47982
+ StackName: stackName,
47983
+ ChangeSetName: changeSetName
47984
+ }))).StatusReason ?? "unknown";
47985
+ await cfnClient.send(new DeleteChangeSetCommand({
47986
+ StackName: stackName,
47987
+ ChangeSetName: changeSetName
47988
+ })).catch(() => {});
47989
+ throw new Error(`UPDATE changeset FAILED: ${reason}`);
47990
+ } catch (innerErr) {
47991
+ if (innerErr instanceof Error && innerErr.message.startsWith("UPDATE changeset FAILED")) throw innerErr;
47992
+ throw err;
47993
+ }
47994
+ }
47995
+ logger.info(`Executing UPDATE changeset...`);
47996
+ try {
47997
+ await cfnClient.send(new ExecuteChangeSetCommand({
47795
47998
  StackName: stackName,
47796
47999
  ChangeSetName: changeSetName
47797
- }))).StatusReason ?? "unknown";
48000
+ }));
48001
+ await waitUntilStackUpdateComplete({
48002
+ client: cfnClient,
48003
+ maxWaitTime: 3600
48004
+ }, { StackName: stackName });
48005
+ } catch (err) {
47798
48006
  await cfnClient.send(new DeleteChangeSetCommand({
47799
48007
  StackName: stackName,
47800
48008
  ChangeSetName: changeSetName
47801
48009
  })).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
48010
  throw err;
47806
48011
  }
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;
48012
+ } finally {
48013
+ await runTemplateUploadCleanup(source.cleanup, uploadOpts?.stateBucket ?? "");
47824
48014
  }
47825
48015
  }
47826
48016
  /**
@@ -47939,7 +48129,7 @@ function reorderArgs(argv) {
47939
48129
  */
47940
48130
  async function main() {
47941
48131
  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");
48132
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.127.0");
47943
48133
  program.addCommand(createBootstrapCommand());
47944
48134
  program.addCommand(createSynthCommand());
47945
48135
  program.addCommand(createListCommand());