@go-to-k/cdkd 0.29.0 → 0.30.1

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
@@ -1304,14 +1304,14 @@ async function resolveStateBucketWithDefaultAndSource(cliBucket, region) {
1304
1304
  const logger = getLogger();
1305
1305
  logger.debug("No state bucket specified, resolving default from account...");
1306
1306
  const { GetCallerIdentityCommand: GetCallerIdentityCommand11 } = await import("@aws-sdk/client-sts");
1307
- const { S3Client: S3Client11 } = await import("@aws-sdk/client-s3");
1307
+ const { S3Client: S3Client12 } = await import("@aws-sdk/client-s3");
1308
1308
  const { getAwsClients: getAwsClients2 } = await Promise.resolve().then(() => (init_aws_clients(), aws_clients_exports));
1309
1309
  const awsClients = getAwsClients2();
1310
1310
  const identity = await awsClients.sts.send(new GetCallerIdentityCommand11({}));
1311
1311
  const accountId = identity.Account;
1312
1312
  const newName = getDefaultStateBucketName(accountId);
1313
1313
  const legacyName = getLegacyStateBucketName(accountId, region);
1314
- const probe = new S3Client11({ region: "us-east-1" });
1314
+ const probe = new S3Client12({ region: "us-east-1" });
1315
1315
  try {
1316
1316
  const newExists = await bucketExists(probe, newName);
1317
1317
  const legacyExists = await bucketExists(probe, legacyName);
@@ -12966,7 +12966,9 @@ import {
12966
12966
  GetFunctionCommand,
12967
12967
  ListFunctionsCommand,
12968
12968
  ListTagsCommand,
12969
- ResourceNotFoundException
12969
+ ResourceNotFoundException,
12970
+ waitUntilFunctionActiveV2,
12971
+ waitUntilFunctionUpdatedV2
12970
12972
  } from "@aws-sdk/client-lambda";
12971
12973
  import {
12972
12974
  DescribeNetworkInterfacesCommand,
@@ -13009,6 +13011,14 @@ var LambdaFunctionProvider = class {
13009
13011
  eniWaitTimeoutMs = 10 * 60 * 1e3;
13010
13012
  eniWaitInitialDelayMs = 1e4;
13011
13013
  eniWaitMaxDelayMs = 3e4;
13014
+ // Budget for the post-CreateFunction / post-Update wait that blocks until
13015
+ // Configuration.State === 'Active' and LastUpdateStatus === 'Successful'.
13016
+ // Must NOT be skipped by --no-wait: downstream resources (Custom Resource
13017
+ // Invokes, EventSourceMappings, etc.) cannot operate against a function
13018
+ // still in Pending / InProgress, so this is required for correctness, not
13019
+ // a "convenience wait" like CloudFront / RDS readiness.
13020
+ // Seconds (the SDK waiter contract is seconds, not ms).
13021
+ functionReadyMaxWaitSeconds = 10 * 60;
13012
13022
  // delstack-style ENI cleanup tunables.
13013
13023
  // - initial sleep: gives AWS time to publish post-detach ENI state via
13014
13024
  // DescribeNetworkInterfaces (right after the update, the API can return
@@ -13077,6 +13087,7 @@ var LambdaFunctionProvider = class {
13077
13087
  Tags: tags
13078
13088
  };
13079
13089
  const response = await this.lambdaClient.send(new CreateFunctionCommand(createParams));
13090
+ await this.waitForFunctionActive(logicalId, resourceType, functionName);
13080
13091
  this.logger.debug(`Successfully created Lambda function ${logicalId}: ${functionName}`);
13081
13092
  return {
13082
13093
  physicalId: response.FunctionName || functionName,
@@ -13086,6 +13097,9 @@ var LambdaFunctionProvider = class {
13086
13097
  }
13087
13098
  };
13088
13099
  } catch (error) {
13100
+ if (error instanceof ProvisioningError) {
13101
+ throw error;
13102
+ }
13089
13103
  const cause = error instanceof Error ? error : void 0;
13090
13104
  throw new ProvisioningError(
13091
13105
  `Failed to create Lambda function ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
@@ -13142,6 +13156,7 @@ var LambdaFunctionProvider = class {
13142
13156
  };
13143
13157
  await this.lambdaClient.send(new UpdateFunctionConfigurationCommand(configParams));
13144
13158
  this.logger.debug(`Updated configuration for Lambda function ${physicalId}`);
13159
+ await this.waitForFunctionUpdated(logicalId, resourceType, physicalId);
13145
13160
  }
13146
13161
  const newCode = properties["Code"];
13147
13162
  const oldCode = previousProperties["Code"];
@@ -13157,6 +13172,7 @@ var LambdaFunctionProvider = class {
13157
13172
  };
13158
13173
  await this.lambdaClient.send(new UpdateFunctionCodeCommand(codeParams));
13159
13174
  this.logger.debug(`Updated code for Lambda function ${physicalId}`);
13175
+ await this.waitForFunctionUpdated(logicalId, resourceType, physicalId);
13160
13176
  }
13161
13177
  const getResponse = await this.lambdaClient.send(
13162
13178
  new GetFunctionCommand({ FunctionName: physicalId })
@@ -13170,6 +13186,9 @@ var LambdaFunctionProvider = class {
13170
13186
  }
13171
13187
  };
13172
13188
  } catch (error) {
13189
+ if (error instanceof ProvisioningError) {
13190
+ throw error;
13191
+ }
13173
13192
  const cause = error instanceof Error ? error : void 0;
13174
13193
  throw new ProvisioningError(
13175
13194
  `Failed to update Lambda function ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
@@ -13336,6 +13355,58 @@ var LambdaFunctionProvider = class {
13336
13355
  * Timeout is a soft warning — downstream Subnet/SG deletion has its own
13337
13356
  * retries.
13338
13357
  */
13358
+ /**
13359
+ * Block until the function's State === 'Active'.
13360
+ *
13361
+ * Used after CreateFunction. Wraps the SDK's built-in
13362
+ * `waitUntilFunctionActiveV2` (acceptors: SUCCESS=Active, FAILURE=Failed,
13363
+ * RETRY=Pending). The waiter throws on FAILURE / TIMEOUT — both surface
13364
+ * as `ProvisioningError` with the function-name as physicalId so the
13365
+ * deploy engine's per-resource error handling treats them identically to
13366
+ * a CreateFunction failure.
13367
+ */
13368
+ async waitForFunctionActive(logicalId, resourceType, functionName) {
13369
+ try {
13370
+ await waitUntilFunctionActiveV2(
13371
+ { client: this.lambdaClient, maxWaitTime: this.functionReadyMaxWaitSeconds },
13372
+ { FunctionName: functionName }
13373
+ );
13374
+ } catch (error) {
13375
+ const cause = error instanceof Error ? error : void 0;
13376
+ throw new ProvisioningError(
13377
+ `Lambda function ${logicalId} did not reach Active state: ${error instanceof Error ? error.message : String(error)}`,
13378
+ resourceType,
13379
+ logicalId,
13380
+ functionName,
13381
+ cause
13382
+ );
13383
+ }
13384
+ }
13385
+ /**
13386
+ * Block until the function's LastUpdateStatus === 'Successful'.
13387
+ *
13388
+ * Used after UpdateFunctionConfiguration / UpdateFunctionCode. Wraps the
13389
+ * SDK's `waitUntilFunctionUpdatedV2` (acceptors: SUCCESS=Successful,
13390
+ * FAILURE=Failed, RETRY=InProgress). Same error-wrapping contract as
13391
+ * `waitForFunctionActive`.
13392
+ */
13393
+ async waitForFunctionUpdated(logicalId, resourceType, functionName) {
13394
+ try {
13395
+ await waitUntilFunctionUpdatedV2(
13396
+ { client: this.lambdaClient, maxWaitTime: this.functionReadyMaxWaitSeconds },
13397
+ { FunctionName: functionName }
13398
+ );
13399
+ } catch (error) {
13400
+ const cause = error instanceof Error ? error : void 0;
13401
+ throw new ProvisioningError(
13402
+ `Lambda function ${logicalId} update did not complete: ${error instanceof Error ? error.message : String(error)}`,
13403
+ resourceType,
13404
+ logicalId,
13405
+ functionName,
13406
+ cause
13407
+ );
13408
+ }
13409
+ }
13339
13410
  /**
13340
13411
  * Poll GetFunction until LastUpdateStatus is no longer `InProgress`.
13341
13412
  *
@@ -13348,6 +13419,12 @@ var LambdaFunctionProvider = class {
13348
13419
  * Bounded by eniWaitTimeoutMs (10min) and treated as a soft warning on
13349
13420
  * timeout: the subsequent ENI cleanup loop and downstream retries cover
13350
13421
  * the residual edge case.
13422
+ *
13423
+ * NOTE: deliberately separate from `waitForFunctionUpdated` (which uses
13424
+ * the SDK's `waitUntilFunctionUpdatedV2` and throws on FAILURE). The
13425
+ * pre-delete path needs a more lenient acceptor: if a prior update
13426
+ * failed, we still want to proceed with DeleteFunction rather than
13427
+ * abort, because the function is going away anyway.
13351
13428
  */
13352
13429
  async waitForLambdaUpdateCompleted(functionName) {
13353
13430
  const start = Date.now();
@@ -35330,6 +35407,7 @@ import {
35330
35407
  waitUntilStackUpdateComplete,
35331
35408
  waitUntilStackDeleteComplete
35332
35409
  } from "@aws-sdk/client-cloudformation";
35410
+ import { S3Client as S3Client11, PutObjectCommand as PutObjectCommand5, DeleteObjectCommand as DeleteObjectCommand4 } from "@aws-sdk/client-s3";
35333
35411
  var STABLE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
35334
35412
  "CREATE_COMPLETE",
35335
35413
  "UPDATE_COMPLETE",
@@ -35338,9 +35416,11 @@ var STABLE_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
35338
35416
  "IMPORT_ROLLBACK_COMPLETE"
35339
35417
  ]);
35340
35418
  var TEMPLATE_BODY_LIMIT = 51200;
35419
+ var TEMPLATE_URL_LIMIT = 1048576;
35420
+ var MIGRATE_TMP_PREFIX = "cdkd-migrate-tmp";
35341
35421
  async function retireCloudFormationStack(options) {
35342
35422
  const logger = getLogger();
35343
- const { cfnStackName, cfnClient, yes } = options;
35423
+ const { cfnStackName, cfnClient, yes, stateBucket, s3ClientOpts } = options;
35344
35424
  logger.info(`[1/4] Inspecting CloudFormation stack '${cfnStackName}'...`);
35345
35425
  const desc = await cfnClient.send(new DescribeStacksCommand({ StackName: cfnStackName }));
35346
35426
  const stack = desc.Stacks?.[0];
@@ -35370,39 +35450,69 @@ async function retireCloudFormationStack(options) {
35370
35450
  return { outcome: "cancelled" };
35371
35451
  }
35372
35452
  }
35373
- let updateRan = false;
35374
35453
  if (!modified) {
35375
35454
  logger.info(`[2/4] Template already has Retain on every resource \u2014 skipping UpdateStack.`);
35376
35455
  } else {
35377
35456
  logger.info(`[2/4] Injected DeletionPolicy=Retain and UpdateReplacePolicy=Retain.`);
35378
- if (newBody.length > TEMPLATE_BODY_LIMIT) {
35457
+ if (newBody.length > TEMPLATE_URL_LIMIT) {
35379
35458
  throw new Error(
35380
- `Modified template is ${newBody.length} bytes, exceeds the inline UpdateStack TemplateBody limit (${TEMPLATE_BODY_LIMIT}). cdkd state has already been written; retire the stack manually with: (1) edit the template to add DeletionPolicy: Retain and UpdateReplacePolicy: Retain to every resource, (2) UpdateStack with the modified template via S3 TemplateURL, (3) DeleteStack. Inline TemplateURL fallback is a planned follow-up.`
35459
+ `Modified template is ${newBody.length} bytes, exceeds the CloudFormation UpdateStack TemplateURL limit (${TEMPLATE_URL_LIMIT}). cdkd state has already been written; retire the stack manually with (1) shrink the template, then (2) UpdateStack with Retain policies, (3) DeleteStack \u2014 or split the stack and retry.`
35381
35460
  );
35382
35461
  }
35383
- logger.info(`[3/4] Updating CloudFormation stack with Retain policies...`);
35384
- try {
35385
- await cfnClient.send(
35386
- new UpdateStackCommand({
35387
- StackName: cfnStackName,
35388
- TemplateBody: newBody,
35389
- Capabilities: capabilities
35390
- })
35462
+ let updateInput;
35463
+ let s3Cleanup;
35464
+ if (newBody.length <= TEMPLATE_BODY_LIMIT) {
35465
+ updateInput = { TemplateBody: newBody };
35466
+ } else {
35467
+ logger.info(
35468
+ ` Template is ${newBody.length} bytes (over ${TEMPLATE_BODY_LIMIT} inline limit) \u2014 uploading to state bucket '${stateBucket}'.`
35391
35469
  );
35392
- updateRan = true;
35393
- } catch (err) {
35394
- const msg = err instanceof Error ? err.message : String(err);
35395
- if (/No updates are to be performed/i.test(msg)) {
35396
- logger.info(` CloudFormation reports no updates needed \u2014 proceeding to delete.`);
35397
- } else {
35398
- throw err;
35399
- }
35470
+ const uploaded = await uploadTemplateForUpdateStack({
35471
+ bucket: stateBucket,
35472
+ body: newBody,
35473
+ cfnStackName,
35474
+ ...s3ClientOpts && { s3ClientOpts }
35475
+ });
35476
+ updateInput = { TemplateURL: uploaded.url };
35477
+ s3Cleanup = uploaded.cleanup;
35400
35478
  }
35401
- if (updateRan) {
35402
- await waitUntilStackUpdateComplete(
35403
- { client: cfnClient, maxWaitTime: 1800 },
35404
- { StackName: cfnStackName }
35405
- );
35479
+ try {
35480
+ logger.info(`[3/4] Updating CloudFormation stack with Retain policies...`);
35481
+ let updateRan = false;
35482
+ try {
35483
+ await cfnClient.send(
35484
+ new UpdateStackCommand({
35485
+ StackName: cfnStackName,
35486
+ ...updateInput,
35487
+ Capabilities: capabilities
35488
+ })
35489
+ );
35490
+ updateRan = true;
35491
+ } catch (err) {
35492
+ const msg = err instanceof Error ? err.message : String(err);
35493
+ if (/No updates are to be performed/i.test(msg)) {
35494
+ logger.info(` CloudFormation reports no updates needed \u2014 proceeding to delete.`);
35495
+ } else {
35496
+ throw err;
35497
+ }
35498
+ }
35499
+ if (updateRan) {
35500
+ await waitUntilStackUpdateComplete(
35501
+ { client: cfnClient, maxWaitTime: 1800 },
35502
+ { StackName: cfnStackName }
35503
+ );
35504
+ }
35505
+ } finally {
35506
+ if (s3Cleanup) {
35507
+ try {
35508
+ await s3Cleanup();
35509
+ } catch (cleanupErr) {
35510
+ const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
35511
+ logger.warn(
35512
+ `Failed to delete temporary template upload from '${stateBucket}'. Clean up manually under prefix '${MIGRATE_TMP_PREFIX}/'. Cause: ${msg}`
35513
+ );
35514
+ }
35515
+ }
35406
35516
  }
35407
35517
  }
35408
35518
  logger.info(`[4/4] Deleting CloudFormation stack '${cfnStackName}' (resources retained)...`);
@@ -35416,6 +35526,41 @@ async function retireCloudFormationStack(options) {
35416
35526
  );
35417
35527
  return { outcome: modified ? "retired" : "no-template-change" };
35418
35528
  }
35529
+ async function uploadTemplateForUpdateStack(args) {
35530
+ const { bucket, body, cfnStackName, s3ClientOpts } = args;
35531
+ const region = await resolveBucketRegion(bucket, {
35532
+ ...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
35533
+ ...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
35534
+ });
35535
+ const s3 = new S3Client11({
35536
+ region,
35537
+ ...s3ClientOpts?.profile && { profile: s3ClientOpts.profile },
35538
+ ...s3ClientOpts?.credentials && { credentials: s3ClientOpts.credentials }
35539
+ });
35540
+ const key = `${MIGRATE_TMP_PREFIX}/${cfnStackName}/${Date.now()}.json`;
35541
+ try {
35542
+ await s3.send(
35543
+ new PutObjectCommand5({
35544
+ Bucket: bucket,
35545
+ Key: key,
35546
+ Body: body,
35547
+ ContentType: "application/json"
35548
+ })
35549
+ );
35550
+ } catch (err) {
35551
+ s3.destroy();
35552
+ throw err;
35553
+ }
35554
+ const url = `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
35555
+ const cleanup = async () => {
35556
+ try {
35557
+ await s3.send(new DeleteObjectCommand4({ Bucket: bucket, Key: key }));
35558
+ } finally {
35559
+ s3.destroy();
35560
+ }
35561
+ };
35562
+ return { url, cleanup };
35563
+ }
35419
35564
  function injectRetainPolicies(templateBody, cfnStackName) {
35420
35565
  let parsed;
35421
35566
  try {
@@ -35695,7 +35840,13 @@ async function importCommand(stackArg, options) {
35695
35840
  await retireCloudFormationStack({
35696
35841
  cfnStackName: migrationCfnStackName,
35697
35842
  cfnClient: awsClients.cloudFormation,
35698
- yes: options.yes
35843
+ yes: options.yes,
35844
+ // Reuse cdkd's state bucket as transient storage for the
35845
+ // Retain-injected template when it exceeds the 51,200-byte
35846
+ // inline UpdateStack limit. Forward `--profile` so the
35847
+ // upload identity matches the one that just wrote cdkd state.
35848
+ stateBucket,
35849
+ ...options.profile && { s3ClientOpts: { profile: options.profile } }
35699
35850
  });
35700
35851
  }
35701
35852
  } finally {
@@ -35988,7 +36139,7 @@ function reorderArgs(argv) {
35988
36139
  }
35989
36140
  async function main() {
35990
36141
  const program = new Command13();
35991
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.29.0");
36142
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.30.1");
35992
36143
  program.addCommand(createBootstrapCommand());
35993
36144
  program.addCommand(createSynthCommand());
35994
36145
  program.addCommand(createListCommand());