@go-to-k/cdkd 0.94.1 → 0.94.3

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
@@ -17838,6 +17838,14 @@ var PATTERN_B_RESOURCE_TYPES = [
17838
17838
  "AWS::ElasticLoadBalancingV2::LoadBalancer",
17839
17839
  "AWS::ElasticLoadBalancingV2::TargetGroup"
17840
17840
  ];
17841
+ var PATTERN_B_NAME_PROPERTIES = {
17842
+ "AWS::IAM::Role": "RoleName",
17843
+ "AWS::IAM::User": "UserName",
17844
+ "AWS::IAM::Group": "GroupName",
17845
+ "AWS::IAM::InstanceProfile": "InstanceProfileName",
17846
+ "AWS::ElasticLoadBalancingV2::LoadBalancer": "Name",
17847
+ "AWS::ElasticLoadBalancingV2::TargetGroup": "Name"
17848
+ };
17841
17849
  function generateResourceName(name, options) {
17842
17850
  const {
17843
17851
  maxLength,
@@ -65532,6 +65540,12 @@ function findPendingPrefixRenames(stackName, state) {
65532
65540
  continue;
65533
65541
  if (!resource.physicalId.startsWith(prefix))
65534
65542
  continue;
65543
+ const nameProperty = PATTERN_B_NAME_PROPERTIES[resource.resourceType];
65544
+ if (!nameProperty)
65545
+ continue;
65546
+ const userSuppliedName = resource.properties?.[nameProperty];
65547
+ if (typeof userSuppliedName !== "string" || userSuppliedName === "")
65548
+ continue;
65535
65549
  const newPhysicalId = resource.physicalId.slice(prefix.length);
65536
65550
  if (newPhysicalId.length === 0)
65537
65551
  continue;
@@ -80130,6 +80144,29 @@ function readStringProperty(properties, key, resourceType) {
80130
80144
  }
80131
80145
  return v;
80132
80146
  }
80147
+ var IMPORT_UNSUPPORTED_RECREATABLE_TYPES = /* @__PURE__ */ new Set([
80148
+ "AWS::ApiGatewayV2::Stage"
80149
+ ]);
80150
+ var PRE_DELETE_HANDLERS = {
80151
+ "AWS::ApiGatewayV2::Stage": async (entry) => {
80152
+ const { ApiGatewayV2Client: ApiGatewayV2Client2, DeleteStageCommand: DeleteStageCommand3, NotFoundException: NotFoundException7 } = await import("@aws-sdk/client-apigatewayv2");
80153
+ const apiId = entry.properties["ApiId"];
80154
+ if (typeof apiId !== "string" || !apiId) {
80155
+ throw new Error(
80156
+ `cdkd state's properties for ${entry.logicalId} (${entry.resourceType}) is missing 'ApiId'`
80157
+ );
80158
+ }
80159
+ const client = new ApiGatewayV2Client2({});
80160
+ try {
80161
+ await client.send(new DeleteStageCommand3({ ApiId: apiId, StageName: entry.physicalId }));
80162
+ } catch (err) {
80163
+ if (err instanceof NotFoundException7) {
80164
+ return;
80165
+ }
80166
+ throw err;
80167
+ }
80168
+ }
80169
+ };
80133
80170
  async function exportCommand(stackArg, options) {
80134
80171
  const logger = getLogger();
80135
80172
  if (options.verbose) {
@@ -80243,10 +80280,13 @@ async function exportCommand(stackArg, options) {
80243
80280
  }
80244
80281
  }
80245
80282
  try {
80246
- const { phase1Imports, phase2Creates, blocked } = await buildImportPlan(
80283
+ const { phase1Imports, phase2Creates, recreateBeforePhase2, blocked } = await buildImportPlan(
80247
80284
  state,
80248
80285
  template,
80249
- awsClients.cloudFormation
80286
+ awsClients.cloudFormation,
80287
+ {
80288
+ recreateImportUnsupported: options.recreateImportUnsupported
80289
+ }
80250
80290
  );
80251
80291
  if (blocked.length > 0) {
80252
80292
  logger.error("The following resources block migration:");
@@ -80266,7 +80306,7 @@ async function exportCommand(stackArg, options) {
80266
80306
  `${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
80307
  );
80268
80308
  }
80269
- if (phase1Imports.length === 0 && phase2Creates.length === 0) {
80309
+ if (phase1Imports.length === 0 && phase2Creates.length === 0 && recreateBeforePhase2.length === 0) {
80270
80310
  logger.warn("No resources to migrate \u2014 cdkd state is empty.");
80271
80311
  return;
80272
80312
  }
@@ -80283,14 +80323,28 @@ async function exportCommand(stackArg, options) {
80283
80323
  }
80284
80324
  logger.info("");
80285
80325
  }
80326
+ if (recreateBeforePhase2.length > 0) {
80327
+ logger.info(
80328
+ `Phase 2 will also re-CREATE ${recreateBeforePhase2.length} IMPORT-unsupported resource(s) after cdkd deletes the AWS-side resource:`
80329
+ );
80330
+ for (const r of recreateBeforePhase2) {
80331
+ logger.info(` ${r.logicalId} (${r.resourceType}) \u2014 physicalId: ${r.physicalId}`);
80332
+ }
80333
+ logger.info(
80334
+ " 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."
80335
+ );
80336
+ logger.info("");
80337
+ }
80286
80338
  if (options.dryRun) {
80287
80339
  logger.info("--dry-run: no CloudFormation changeset will be created.");
80288
80340
  return;
80289
80341
  }
80290
80342
  if (!options.yes) {
80291
80343
  const phase2Note = phase2Creates.length > 0 ? ` Phase 2 will then CREATE ${phase2Creates.length} non-importable resource(s) (invoking each Custom Resource's onCreate handler).` : "";
80344
+ 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).` : "";
80345
+ const unchangedClaim = recreateBeforePhase2.length > 0 ? ` All other AWS resources are unchanged on import.` : ` AWS resources are unchanged on import.`;
80292
80346
  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.`
80347
+ `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
80348
  );
80295
80349
  if (!ok) {
80296
80350
  logger.info("Migration cancelled. cdkd state and CloudFormation are unchanged.");
@@ -80344,7 +80398,42 @@ async function exportCommand(stackArg, options) {
80344
80398
  logger.info(
80345
80399
  `\u2713 Phase 1: CloudFormation stack '${cfnStackName}' created via IMPORT. ${phase1Imports.length} resource(s) imported.`
80346
80400
  );
80347
- if (phase2Creates.length > 0) {
80401
+ if (recreateBeforePhase2.length > 0) {
80402
+ for (const entry of recreateBeforePhase2) {
80403
+ const handler = PRE_DELETE_HANDLERS[entry.resourceType];
80404
+ if (!handler) {
80405
+ throw new Error(
80406
+ `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.`
80407
+ );
80408
+ }
80409
+ logger.info(
80410
+ `Pre-deleting AWS resource for ${entry.logicalId} (${entry.resourceType}) so CFn can re-CREATE in phase 2...`
80411
+ );
80412
+ try {
80413
+ await handler(entry);
80414
+ logger.info(` \u2713 deleted ${entry.physicalId}`);
80415
+ } catch (err) {
80416
+ const msg = err instanceof Error ? err.message : String(err);
80417
+ throw new Error(
80418
+ `Phase 1 (IMPORT) succeeded; pre-delete of ${entry.logicalId} (${entry.resourceType}, physicalId: ${entry.physicalId}) failed: ${msg}
80419
+
80420
+ 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:
80421
+ 1. Fix the failure cause (typically IAM permissions for the underlying AWS API \u2014 e.g. apigatewayv2:DeleteStage for AWS::ApiGatewayV2::Stage).
80422
+ 2. Delete the remaining AWS-side IMPORT-unsupported resources by hand:
80423
+ aws apigatewayv2 delete-stage --api-id <ApiId> --stage-name <StageName>
80424
+ (one per entry in the pre-delete list logged above).
80425
+ 3. Run the phase-2 UPDATE manually with the full synth template:
80426
+ aws cloudformation create-change-set --stack-name ${cfnStackName} \\
80427
+ --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\
80428
+ --template-body file://<full-template.json>
80429
+ 4. Once phase 2 succeeds, run: cdkd state orphan ${resolvedStackName}
80430
+ to clean up cdkd's stale state record.`
80431
+ );
80432
+ }
80433
+ }
80434
+ }
80435
+ const phase2Count = phase2Creates.length + recreateBeforePhase2.length;
80436
+ if (phase2Count > 0) {
80348
80437
  try {
80349
80438
  await executeUpdateChangeSet(
80350
80439
  awsClients.cloudFormation,
@@ -80352,14 +80441,23 @@ async function exportCommand(stackArg, options) {
80352
80441
  template,
80353
80442
  cfnParameters
80354
80443
  );
80355
- logger.info(`\u2713 Phase 2: ${phase2Creates.length} non-importable resource(s) CREATEd.`);
80444
+ const parts = [];
80445
+ if (phase2Creates.length > 0) {
80446
+ parts.push(`${phase2Creates.length} non-importable resource(s) CREATEd`);
80447
+ }
80448
+ if (recreateBeforePhase2.length > 0) {
80449
+ parts.push(`${recreateBeforePhase2.length} IMPORT-unsupported resource(s) re-CREATEd`);
80450
+ }
80451
+ logger.info(`\u2713 Phase 2: ${parts.join("; ")}.`);
80356
80452
  } catch (err) {
80357
80453
  const msg = err instanceof Error ? err.message : String(err);
80454
+ 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.
80455
+ ` : "";
80358
80456
  throw new Error(
80359
80457
  `Phase 1 (IMPORT) succeeded; phase 2 (UPDATE) failed: ${msg}
80360
80458
 
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).
80459
+ 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:
80460
+ ` + recreateNote + ` 1. Fix the failure cause (typically an onCreate Lambda error).
80363
80461
  2. Re-run the phase 2 UPDATE manually with the full synth template:
80364
80462
  aws cloudformation create-change-set --stack-name ${cfnStackName} \\
80365
80463
  --change-set-name cdkd-phase2-retry --change-set-type UPDATE \\
@@ -80467,13 +80565,14 @@ async function assertCfnStackAbsent(cfnClient, stackName) {
80467
80565
  function isPhase2CreatableType(resourceType) {
80468
80566
  return isCustomResourceType(resourceType);
80469
80567
  }
80470
- async function buildImportPlan(state, template, cfnClient) {
80568
+ async function buildImportPlan(state, template, cfnClient, options = { recreateImportUnsupported: true }) {
80471
80569
  const templateResources = template["Resources"];
80472
80570
  if (!templateResources || typeof templateResources !== "object" || Array.isArray(templateResources)) {
80473
80571
  throw new Error("Template has no Resources section.");
80474
80572
  }
80475
80573
  const phase1Imports = [];
80476
80574
  const phase2Creates = [];
80575
+ const recreateBeforePhase2 = [];
80477
80576
  const blocked = [];
80478
80577
  const identifierCache = /* @__PURE__ */ new Map();
80479
80578
  for (const [logicalId, raw] of Object.entries(templateResources)) {
@@ -80507,6 +80606,23 @@ async function buildImportPlan(state, template, cfnClient) {
80507
80606
  });
80508
80607
  continue;
80509
80608
  }
80609
+ if (IMPORT_UNSUPPORTED_RECREATABLE_TYPES.has(resourceType)) {
80610
+ if (options.recreateImportUnsupported) {
80611
+ recreateBeforePhase2.push({
80612
+ logicalId,
80613
+ resourceType,
80614
+ physicalId: stateEntry.physicalId,
80615
+ properties: stateEntry.properties ?? {}
80616
+ });
80617
+ } else {
80618
+ blocked.push({
80619
+ logicalId,
80620
+ resourceType,
80621
+ 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).`
80622
+ });
80623
+ }
80624
+ continue;
80625
+ }
80510
80626
  let resolved;
80511
80627
  try {
80512
80628
  resolved = await resolveResourceIdentifier(
@@ -80532,7 +80648,7 @@ async function buildImportPlan(state, template, cfnClient) {
80532
80648
  propertiesOverlay: resolved.propertiesOverlay ?? resolved.resourceIdentifier
80533
80649
  });
80534
80650
  }
80535
- return { phase1Imports, phase2Creates, blocked };
80651
+ return { phase1Imports, phase2Creates, recreateBeforePhase2, blocked };
80536
80652
  }
80537
80653
  async function resolveResourceIdentifier(resourceType, physicalId, properties, cfnClient, cache2) {
80538
80654
  let entry = cache2.get(resourceType);
@@ -81019,6 +81135,9 @@ function createExportCommand() {
81019
81135
  "--strict-cross-stack",
81020
81136
  "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
81137
  false
81138
+ ).option(
81139
+ "--no-recreate-import-unsupported",
81140
+ "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
81141
  ).action(withErrorHandling(exportCommand));
81023
81142
  [...commonOptions, ...appOptions, ...stateOptions, ...contextOptions].forEach(
81024
81143
  (opt) => cmd.addOption(opt)
@@ -81057,7 +81176,7 @@ function reorderArgs(argv) {
81057
81176
  }
81058
81177
  async function main() {
81059
81178
  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");
81179
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.94.3");
81061
81180
  program.addCommand(createBootstrapCommand());
81062
81181
  program.addCommand(createSynthCommand());
81063
81182
  program.addCommand(createListCommand());