@go-to-k/cdkd 0.94.1 → 0.94.2

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
@@ -80130,6 +80130,29 @@ function readStringProperty(properties, key, resourceType) {
80130
80130
  }
80131
80131
  return v;
80132
80132
  }
80133
+ var IMPORT_UNSUPPORTED_RECREATABLE_TYPES = /* @__PURE__ */ new Set([
80134
+ "AWS::ApiGatewayV2::Stage"
80135
+ ]);
80136
+ var PRE_DELETE_HANDLERS = {
80137
+ "AWS::ApiGatewayV2::Stage": async (entry) => {
80138
+ const { ApiGatewayV2Client: ApiGatewayV2Client2, DeleteStageCommand: DeleteStageCommand3, NotFoundException: NotFoundException7 } = await import("@aws-sdk/client-apigatewayv2");
80139
+ const apiId = entry.properties["ApiId"];
80140
+ if (typeof apiId !== "string" || !apiId) {
80141
+ throw new Error(
80142
+ `cdkd state's properties for ${entry.logicalId} (${entry.resourceType}) is missing 'ApiId'`
80143
+ );
80144
+ }
80145
+ const client = new ApiGatewayV2Client2({});
80146
+ try {
80147
+ await client.send(new DeleteStageCommand3({ ApiId: apiId, StageName: entry.physicalId }));
80148
+ } catch (err) {
80149
+ if (err instanceof NotFoundException7) {
80150
+ return;
80151
+ }
80152
+ throw err;
80153
+ }
80154
+ }
80155
+ };
80133
80156
  async function exportCommand(stackArg, options) {
80134
80157
  const logger = getLogger();
80135
80158
  if (options.verbose) {
@@ -80243,10 +80266,13 @@ async function exportCommand(stackArg, options) {
80243
80266
  }
80244
80267
  }
80245
80268
  try {
80246
- const { phase1Imports, phase2Creates, blocked } = await buildImportPlan(
80269
+ const { phase1Imports, phase2Creates, recreateBeforePhase2, blocked } = await buildImportPlan(
80247
80270
  state,
80248
80271
  template,
80249
- awsClients.cloudFormation
80272
+ awsClients.cloudFormation,
80273
+ {
80274
+ recreateImportUnsupported: options.recreateImportUnsupported
80275
+ }
80250
80276
  );
80251
80277
  if (blocked.length > 0) {
80252
80278
  logger.error("The following resources block migration:");
@@ -80266,7 +80292,7 @@ async function exportCommand(stackArg, options) {
80266
80292
  `${phase2Creates.length} non-importable resource(s) detected (Custom::*). Pass --include-non-importable to run a 2-phase migration: phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones (re-invoking each Custom Resource's backing Lambda onCreate handler \u2014 make sure those are idempotent). Or destroy these resources first.`
80267
80293
  );
80268
80294
  }
80269
- if (phase1Imports.length === 0 && phase2Creates.length === 0) {
80295
+ if (phase1Imports.length === 0 && phase2Creates.length === 0 && recreateBeforePhase2.length === 0) {
80270
80296
  logger.warn("No resources to migrate \u2014 cdkd state is empty.");
80271
80297
  return;
80272
80298
  }
@@ -80283,14 +80309,28 @@ async function exportCommand(stackArg, options) {
80283
80309
  }
80284
80310
  logger.info("");
80285
80311
  }
80312
+ if (recreateBeforePhase2.length > 0) {
80313
+ logger.info(
80314
+ `Phase 2 will also re-CREATE ${recreateBeforePhase2.length} IMPORT-unsupported resource(s) after cdkd deletes the AWS-side resource:`
80315
+ );
80316
+ for (const r of recreateBeforePhase2) {
80317
+ logger.info(` ${r.logicalId} (${r.resourceType}) \u2014 physicalId: ${r.physicalId}`);
80318
+ }
80319
+ logger.info(
80320
+ " Brief unavailability window (~10s for Stage; HttpApi endpoint URL is unchanged because it embeds ApiId, not StageName). Pass --no-recreate-import-unsupported to block instead."
80321
+ );
80322
+ logger.info("");
80323
+ }
80286
80324
  if (options.dryRun) {
80287
80325
  logger.info("--dry-run: no CloudFormation changeset will be created.");
80288
80326
  return;
80289
80327
  }
80290
80328
  if (!options.yes) {
80291
80329
  const phase2Note = phase2Creates.length > 0 ? ` Phase 2 will then CREATE ${phase2Creates.length} non-importable resource(s) (invoking each Custom Resource's onCreate handler).` : "";
80330
+ const recreateNote = recreateBeforePhase2.length > 0 ? ` cdkd will also DELETE ${recreateBeforePhase2.length} AWS resource(s) between phases so CFn can re-CREATE them in phase 2 (brief unavailability window \u2014 see plan above for the affected resources).` : "";
80331
+ const unchangedClaim = recreateBeforePhase2.length > 0 ? ` All other AWS resources are unchanged on import.` : ` AWS resources are unchanged on import.`;
80292
80332
  const ok = await confirmPrompt6(
80293
- `Create CloudFormation stack '${cfnStackName}' by importing ${phase1Imports.length} resource(s) from cdkd state '${resolvedStackName}' (${targetRegion})?` + phase2Note + ` AWS resources are unchanged on import. cdkd state for '${resolvedStackName}' will be deleted on success.`
80333
+ `Create CloudFormation stack '${cfnStackName}' by importing ${phase1Imports.length} resource(s) from cdkd state '${resolvedStackName}' (${targetRegion})?` + phase2Note + recreateNote + unchangedClaim + ` cdkd state for '${resolvedStackName}' will be deleted on success.`
80294
80334
  );
80295
80335
  if (!ok) {
80296
80336
  logger.info("Migration cancelled. cdkd state and CloudFormation are unchanged.");
@@ -80344,7 +80384,42 @@ async function exportCommand(stackArg, options) {
80344
80384
  logger.info(
80345
80385
  `\u2713 Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`
80346
80386
  );
80347
- if (phase2Creates.length > 0) {
80387
+ if (recreateBeforePhase2.length > 0) {
80388
+ for (const entry of recreateBeforePhase2) {
80389
+ const handler = PRE_DELETE_HANDLERS[entry.resourceType];
80390
+ if (!handler) {
80391
+ throw new Error(
80392
+ `No pre-delete handler registered for ${entry.resourceType} (${entry.logicalId}). This is a cdkd bug \u2014 the resource is in IMPORT_UNSUPPORTED_RECREATABLE_TYPES but lacks a PRE_DELETE_HANDLERS entry. Phase 1 IMPORT already succeeded; cdkd state is intact. To recover, delete the AWS resource manually and run the phase 2 UPDATE.`
80393
+ );
80394
+ }
80395
+ logger.info(
80396
+ `Pre-deleting AWS resource for ${entry.logicalId} (${entry.resourceType}) so CFn can re-CREATE in phase 2...`
80397
+ );
80398
+ try {
80399
+ await handler(entry);
80400
+ logger.info(` \u2713 deleted ${entry.physicalId}`);
80401
+ } catch (err) {
80402
+ const msg = err instanceof Error ? err.message : String(err);
80403
+ throw new Error(
80404
+ `Phase 1 (IMPORT) succeeded; pre-delete of ${entry.logicalId} (${entry.resourceType}, physicalId: ${entry.physicalId}) failed: ${msg}
80405
+
80406
+ The CloudFormation stack '${cfnStackName}' contains the phase-1 imports but the IMPORT-unsupported resources (${recreateBeforePhase2.length} total) still exist in AWS unmanaged. cdkd state is UNCHANGED. Re-running \`cdkd export\` does NOT work \u2014 the existing-stack check rejects it. To recover manually:
80407
+ 1. Fix the failure cause (typically IAM permissions for the underlying AWS API \u2014 e.g. apigatewayv2:DeleteStage for AWS::ApiGatewayV2::Stage).
80408
+ 2. Delete the remaining AWS-side IMPORT-unsupported resources by hand:
80409
+ aws apigatewayv2 delete-stage --api-id <ApiId> --stage-name <StageName>
80410
+ (one per entry in the pre-delete list logged above).
80411
+ 3. Run the phase-2 UPDATE manually with the full synth template:
80412
+ aws cloudformation create-change-set --stack-name ${cfnStackName} \\
80413
+ --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\
80414
+ --template-body file://<full-template.json>
80415
+ 4. Once phase 2 succeeds, run: cdkd state orphan ${resolvedStackName}
80416
+ to clean up cdkd's stale state record.`
80417
+ );
80418
+ }
80419
+ }
80420
+ }
80421
+ const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
80422
+ if (phase2Count > 0) {
80348
80423
  try {
80349
80424
  await executeUpdateChangeSet(
80350
80425
  awsClients.cloudFormation,
@@ -80352,14 +80427,23 @@ async function exportCommand(stackArg, options) {
80352
80427
  template,
80353
80428
  cfnParameters
80354
80429
  );
80355
- logger.info(`\u2713 Phase 2: ${phase2Creates.length} non-importable resource(s) CREATEd.`);
80430
+ const parts = [];
80431
+ if (phase2Creates.length > 0) {
80432
+ parts.push(`${phase2Creates.length} non-importable resource(s) CREATEd`);
80433
+ }
80434
+ if (recreateBeforePhase2.length > 0) {
80435
+ parts.push(`${recreateBeforePhase2.length} IMPORT-unsupported resource(s) re-CREATEd`);
80436
+ }
80437
+ logger.info(`\u2713 Phase 2: ${parts.join("; ")}.`);
80356
80438
  } catch (err) {
80357
80439
  const msg = err instanceof Error ? err.message : String(err);
80440
+ const recreateNote = recreateBeforePhase2.length > 0 ? ` - cdkd deleted ${recreateBeforePhase2.length} AWS resource(s) before phase 2 (Stage etc.). They are gone in AWS but absent from the CFn stack. Running phase 2 UPDATE manually will CFn-CREATE them fresh.
80441
+ ` : "";
80358
80442
  throw new Error(
80359
80443
  `Phase 1 (IMPORT) succeeded; phase 2 (UPDATE) failed: ${msg}
80360
80444
 
80361
- The CloudFormation stack '${cfnStackName}' now contains the imported resources but is missing the ${phase2Creates.length} non-importable resource(s). cdkd state is UNCHANGED so you can inspect what's in it, but DO NOT run \`cdkd deploy\` against this stack (the imported resources are now CFn-managed). To recover:
80362
- 1. Fix the failure cause (typically an onCreate Lambda error).
80445
+ The CloudFormation stack '${cfnStackName}' now contains the imported resources but is missing ${phase2Count} resource(s) (${phase2Creates.length} Custom Resource(s) + ${recreateBeforePhase2.length} IMPORT-unsupported). cdkd state is UNCHANGED so you can inspect what's in it, but DO NOT run \`cdkd deploy\` against this stack (the imported resources are now CFn-managed). To recover:
80446
+ ` + recreateNote + ` 1. Fix the failure cause (typically an onCreate Lambda error).
80363
80447
  2. Re-run the phase 2 UPDATE manually with the full synth template:
80364
80448
  aws cloudformation create-change-set --stack-name ${cfnStackName} \\
80365
80449
  --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\
@@ -80467,13 +80551,14 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
80467
80551
  function isPhase2CreatableType(resourceType) {
80468
80552
  return isCustomResourceType(resourceType);
80469
80553
  }
80470
- async function buildImportPlan(state, template, cfnClient) {
80554
+ async function buildImportPlan(state, template, cfnClient, options = { recreateImportUnsupported: true }) {
80471
80555
  const templateResources = template["Resources"];
80472
80556
  if (!templateResources || typeof templateResources !== "object" || Array.isArray(templateResources)) {
80473
80557
  throw new Error("Template has no Resources section.");
80474
80558
  }
80475
80559
  const phase1Imports = [];
80476
80560
  const phase2Creates = [];
80561
+ const recreateBeforePhase2 = [];
80477
80562
  const blocked = [];
80478
80563
  const identifierCache = /* @__PURE__ */ new Map();
80479
80564
  for (const [logicalId, raw] of Object.entries(templateResources)) {
@@ -80507,6 +80592,23 @@ async function buildImportPlan(state, template, cfnClient) {
80507
80592
  });
80508
80593
  continue;
80509
80594
  }
80595
+ if (IMPORT_UNSUPPORTED_RECREATABLE_TYPES.has(resourceType)) {
80596
+ if (options.recreateImportUnsupported) {
80597
+ recreateBeforePhase2.push({
80598
+ logicalId,
80599
+ resourceType,
80600
+ physicalId: stateEntry.physicalId,
80601
+ properties: stateEntry.properties ?? {}
80602
+ });
80603
+ } else {
80604
+ blocked.push({
80605
+ logicalId,
80606
+ resourceType,
80607
+ reason: `AWS CloudFormation does not support ${resourceType} in IMPORT changesets (${resourceType} has no IMPORT handler). Re-run without --no-recreate-import-unsupported to let cdkd delete the AWS-side resource before phase 2 (CFn will then CREATE it fresh; brief unavailability window).`
80608
+ });
80609
+ }
80610
+ continue;
80611
+ }
80510
80612
  let resolved;
80511
80613
  try {
80512
80614
  resolved = await resolveResourceIdentifier(
@@ -80532,7 +80634,7 @@ async function buildImportPlan(state, template, cfnClient) {
80532
80634
  propertiesOverlay: resolved.propertiesOverlay ?? resolved.resourceIdentifier
80533
80635
  });
80534
80636
  }
80535
- return { phase1Imports, phase2Creates, blocked };
80637
+ return { phase1Imports, phase2Creates, recreateBeforePhase2, blocked };
80536
80638
  }
80537
80639
  async function resolveResourceIdentifier(resourceType, physicalId, properties, cfnClient, cache2) {
80538
80640
  let entry = cache2.get(resourceType);
@@ -81019,6 +81121,9 @@ function createExportCommand() {
81019
81121
  "--strict-cross-stack",
81020
81122
  "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns but proceeds \u2014 the user is expected to migrate the consumer stacks in a follow-up.",
81021
81123
  false
81124
+ ).option(
81125
+ "--no-recreate-import-unsupported",
81126
+ "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (currently only AWS::ApiGatewayV2::Stage, emitted by CDK HttpApi). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead."
81022
81127
  ).action(withErrorHandling(exportCommand));
81023
81128
  [...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
81024
81129
  (opt) => cmd.addOption(opt)
@@ -81057,7 +81162,7 @@ function reorderArgs(argv) {
81057
81162
  }
81058
81163
  async function main() {
81059
81164
  const program = new Command18();
81060
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.94.1");
81165
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.94.2");
81061
81166
  program.addCommand(createBootstrapCommand());
81062
81167
  program.addCommand(createSynthCommand());
81063
81168
  program.addCommand(createListCommand());