@go-to-k/cdkd 0.3.0 → 0.3.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
@@ -10987,7 +10987,10 @@ import {
10987
10987
  GetFunctionCommand,
10988
10988
  ResourceNotFoundException
10989
10989
  } from "@aws-sdk/client-lambda";
10990
- import { DescribeNetworkInterfacesCommand } from "@aws-sdk/client-ec2";
10990
+ import {
10991
+ DescribeNetworkInterfacesCommand,
10992
+ DeleteNetworkInterfaceCommand
10993
+ } from "@aws-sdk/client-ec2";
10991
10994
  init_aws_clients();
10992
10995
  var LambdaFunctionProvider = class {
10993
10996
  lambdaClient;
@@ -11196,6 +11199,24 @@ var LambdaFunctionProvider = class {
11196
11199
  async delete(logicalId, physicalId, resourceType, properties) {
11197
11200
  this.logger.debug(`Deleting Lambda function ${logicalId}: ${physicalId}`);
11198
11201
  const hasVpcConfig = this.hasVpcConfig(properties?.["VpcConfig"]);
11202
+ if (hasVpcConfig) {
11203
+ try {
11204
+ await this.lambdaClient.send(
11205
+ new UpdateFunctionConfigurationCommand({
11206
+ FunctionName: physicalId,
11207
+ VpcConfig: { SubnetIds: [], SecurityGroupIds: [] }
11208
+ })
11209
+ );
11210
+ this.logger.debug(`Detached VPC config from Lambda ${physicalId} before deletion`);
11211
+ } catch (error) {
11212
+ if (error instanceof ResourceNotFoundException) {
11213
+ return;
11214
+ }
11215
+ this.logger.warn(
11216
+ `Pre-delete VPC detach failed for ${physicalId}: ${error instanceof Error ? error.message : String(error)} \u2014 continuing with delete`
11217
+ );
11218
+ }
11219
+ }
11199
11220
  try {
11200
11221
  await this.lambdaClient.send(new DeleteFunctionCommand({ FunctionName: physicalId }));
11201
11222
  this.logger.debug(`Successfully deleted Lambda function ${logicalId}`);
@@ -11214,7 +11235,7 @@ var LambdaFunctionProvider = class {
11214
11235
  );
11215
11236
  }
11216
11237
  if (hasVpcConfig) {
11217
- await this.waitForLambdaEnisDetached(physicalId);
11238
+ await this.cleanupLambdaEnis(physicalId);
11218
11239
  }
11219
11240
  }
11220
11241
  /**
@@ -11275,50 +11296,78 @@ var LambdaFunctionProvider = class {
11275
11296
  return Array.isArray(subnets) && subnets.length > 0;
11276
11297
  }
11277
11298
  /**
11278
- * Poll DescribeNetworkInterfaces until the Lambda-managed ENIs for the
11279
- * given function are gone, or the configured timeout elapses.
11299
+ * Clean up Lambda-managed ENIs for the given function: list, then attempt
11300
+ * DeleteNetworkInterface on each. Repeat until no matching ENIs remain
11301
+ * or the configured timeout elapses.
11302
+ *
11303
+ * Why direct delete (not just wait): an `available` ENI still counts as a
11304
+ * Subnet / SecurityGroup dependency, so DeleteSubnet / DeleteSecurityGroup
11305
+ * fail until the ENI itself is gone. AWS's eventual cleanup of unused
11306
+ * Lambda hyperplane ENIs can take well over an hour, which is far longer
11307
+ * than any reasonable destroy budget. Calling DeleteNetworkInterface
11308
+ * ourselves (best-effort) clears `available` ENIs in seconds.
11280
11309
  *
11281
- * Lambda VPC ENIs carry a Description like:
11282
- * "AWS Lambda VPC ENI-<functionName>-<uuid>"
11283
- * We match on a substring to be tolerant of format drift.
11310
+ * In-use ENIs (e.g. immediately after the pre-delete VPC detach) cannot
11311
+ * be deleted yet — we swallow that error and retry on the next iteration
11312
+ * once they transition to `available`.
11313
+ *
11314
+ * Lambda VPC ENI Descriptions follow the pattern
11315
+ * "AWS Lambda VPC ENI-<functionName>"
11316
+ * (and historically "AWS Lambda VPC ENI-<functionName>-<uuid>"). We
11317
+ * narrow the query with a `requester-id` filter and then match the
11318
+ * function name as a hyphen-bounded token to avoid false positives like
11319
+ * "myfn" matching for function "fn".
11284
11320
  *
11285
11321
  * Polling: starts at eniWaitInitialDelayMs (10s), exponential backoff up
11286
11322
  * to eniWaitMaxDelayMs (30s), bounded by eniWaitTimeoutMs (10min).
11287
- *
11288
- * Timeout is treated as a soft warning: detach can legitimately take 20-40
11289
- * minutes in degraded conditions, and downstream Subnet/SG deletion has
11290
- * its own retries to handle the residual window.
11323
+ * Timeout is a soft warning — downstream Subnet/SG deletion has its own
11324
+ * retries.
11291
11325
  */
11292
- async waitForLambdaEnisDetached(functionName) {
11326
+ async cleanupLambdaEnis(functionName) {
11293
11327
  const start = Date.now();
11294
11328
  let delay = this.eniWaitInitialDelayMs;
11295
11329
  let attempt = 0;
11296
11330
  this.logger.debug(
11297
- `Waiting for Lambda VPC ENIs to detach for function ${functionName} (timeout ${this.eniWaitTimeoutMs}ms)`
11331
+ `Cleaning up Lambda VPC ENIs for function ${functionName} (timeout ${this.eniWaitTimeoutMs}ms)`
11298
11332
  );
11299
- const descriptionNeedle = `AWS Lambda VPC ENI`;
11300
- const functionNamePattern = new RegExp(`(^|-)${escapeRegExp(functionName)}(-|$)`);
11301
11333
  for (; ; ) {
11302
11334
  attempt++;
11303
- let count;
11335
+ let enis = [];
11336
+ let listFailed = false;
11304
11337
  try {
11305
- count = await this.countLambdaEnis(descriptionNeedle, functionNamePattern);
11338
+ enis = await this.listLambdaEnis(functionName);
11306
11339
  } catch (error) {
11307
11340
  this.logger.warn(
11308
- `DescribeNetworkInterfaces failed while waiting for Lambda ENIs of ${functionName}: ${error instanceof Error ? error.message : String(error)}`
11341
+ `DescribeNetworkInterfaces failed while cleaning up Lambda ENIs of ${functionName}: ${error instanceof Error ? error.message : String(error)}`
11309
11342
  );
11310
- count = -1;
11343
+ listFailed = true;
11311
11344
  }
11312
- if (count === 0) {
11345
+ if (!listFailed && enis.length === 0) {
11313
11346
  this.logger.debug(
11314
- `Lambda ENIs for ${functionName} fully detached after ${attempt} poll(s) / ${Date.now() - start}ms`
11347
+ `Lambda ENIs for ${functionName} fully cleaned up after ${attempt} attempt(s) / ${Date.now() - start}ms`
11315
11348
  );
11316
11349
  return;
11317
11350
  }
11351
+ if (enis.length > 0) {
11352
+ await Promise.all(
11353
+ enis.map(async (eni) => {
11354
+ try {
11355
+ await this.ec2Client.send(
11356
+ new DeleteNetworkInterfaceCommand({ NetworkInterfaceId: eni.id })
11357
+ );
11358
+ this.logger.debug(`Deleted Lambda ENI ${eni.id} for ${functionName}`);
11359
+ } catch (error) {
11360
+ this.logger.debug(
11361
+ `ENI ${eni.id} (status=${eni.status}) not yet deletable: ${error instanceof Error ? error.message : String(error)}`
11362
+ );
11363
+ }
11364
+ })
11365
+ );
11366
+ }
11318
11367
  const elapsed = Date.now() - start;
11319
11368
  if (elapsed >= this.eniWaitTimeoutMs) {
11320
11369
  this.logger.warn(
11321
- `Timeout (${this.eniWaitTimeoutMs}ms) waiting for Lambda VPC ENIs of ${functionName} to detach (remaining: ${count >= 0 ? count : "unknown"}). Continuing \u2014 downstream Subnet/SG deletion will retry as needed.`
11370
+ `Timeout (${this.eniWaitTimeoutMs}ms) cleaning up Lambda VPC ENIs of ${functionName} (${enis.length} remaining). Continuing \u2014 downstream Subnet/SG deletion will retry as needed.`
11322
11371
  );
11323
11372
  return;
11324
11373
  }
@@ -11329,15 +11378,16 @@ var LambdaFunctionProvider = class {
11329
11378
  }
11330
11379
  }
11331
11380
  /**
11332
- * Count remaining Lambda-managed ENIs for the given function, paginating
11333
- * through DescribeNetworkInterfaces and filtering on Description substring.
11381
+ * List Lambda-managed ENIs for the given function, paginating through
11382
+ * DescribeNetworkInterfaces and filtering on Description substring.
11334
11383
  *
11335
- * Server-side filter (`description`) does not support wildcards in EC2's API,
11336
- * so we filter client-side after narrowing on `requester-id` + `status`.
11384
+ * Server-side filter (`description`) does not support wildcards on this
11385
+ * API, so we narrow with `requester-id` + match Description client-side.
11337
11386
  */
11338
- async countLambdaEnis(descriptionNeedle, functionNamePattern) {
11387
+ async listLambdaEnis(functionName) {
11388
+ const enis = [];
11389
+ const descriptionPrefix = "AWS Lambda VPC ENI-";
11339
11390
  let nextToken;
11340
- let count = 0;
11341
11391
  do {
11342
11392
  const resp = await this.ec2Client.send(
11343
11393
  new DescribeNetworkInterfacesCommand({
@@ -11350,13 +11400,17 @@ var LambdaFunctionProvider = class {
11350
11400
  );
11351
11401
  for (const ni of resp.NetworkInterfaces ?? []) {
11352
11402
  const desc = ni.Description ?? "";
11353
- if (desc.includes(descriptionNeedle) && functionNamePattern.test(desc)) {
11354
- count++;
11403
+ if (!ni.NetworkInterfaceId || !desc.startsWith(descriptionPrefix)) {
11404
+ continue;
11405
+ }
11406
+ const token = desc.slice(descriptionPrefix.length);
11407
+ if (functionName === token || functionName.startsWith(`${token}-`)) {
11408
+ enis.push({ id: ni.NetworkInterfaceId, status: ni.Status ?? "unknown" });
11355
11409
  }
11356
11410
  }
11357
11411
  nextToken = resp.NextToken;
11358
11412
  } while (nextToken);
11359
- return count;
11413
+ return enis;
11360
11414
  }
11361
11415
  sleep(ms) {
11362
11416
  return new Promise((resolve4) => setTimeout(resolve4, ms));
@@ -11446,9 +11500,6 @@ var LambdaFunctionProvider = class {
11446
11500
  return (crc ^ 4294967295) >>> 0;
11447
11501
  }
11448
11502
  };
11449
- function escapeRegExp(input) {
11450
- return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11451
- }
11452
11503
 
11453
11504
  // src/provisioning/providers/lambda-permission-provider.ts
11454
11505
  import {
@@ -27554,7 +27605,7 @@ function reorderArgs(argv) {
27554
27605
  }
27555
27606
  async function main() {
27556
27607
  const program = new Command8();
27557
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.3.0");
27608
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.3.2");
27558
27609
  program.addCommand(createBootstrapCommand());
27559
27610
  program.addCommand(createSynthCommand());
27560
27611
  program.addCommand(createDeployCommand());