@go-to-k/cdkd 0.1.0 → 0.3.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
@@ -3533,6 +3533,60 @@ var TemplateParser = class {
3533
3533
  }
3534
3534
  };
3535
3535
 
3536
+ // src/analyzer/lambda-vpc-deps.ts
3537
+ function extractLambdaVpcDeleteDeps(resources) {
3538
+ const edges = [];
3539
+ const seen = /* @__PURE__ */ new Set();
3540
+ for (const [lambdaId, resource] of Object.entries(resources)) {
3541
+ if (resource.Type !== "AWS::Lambda::Function")
3542
+ continue;
3543
+ const vpcConfig = (resource.Properties ?? {})["VpcConfig"];
3544
+ if (!isObject(vpcConfig))
3545
+ continue;
3546
+ const targets = /* @__PURE__ */ new Set();
3547
+ collectRefIds(vpcConfig["SubnetIds"], targets);
3548
+ collectRefIds(vpcConfig["SecurityGroupIds"], targets);
3549
+ for (const targetId of targets) {
3550
+ if (targetId === lambdaId)
3551
+ continue;
3552
+ if (!(targetId in resources))
3553
+ continue;
3554
+ const key = `${lambdaId}\0${targetId}`;
3555
+ if (seen.has(key))
3556
+ continue;
3557
+ seen.add(key);
3558
+ edges.push({ before: lambdaId, after: targetId });
3559
+ }
3560
+ }
3561
+ return edges;
3562
+ }
3563
+ function isObject(v) {
3564
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3565
+ }
3566
+ function collectRefIds(value, out) {
3567
+ if (value === null || value === void 0)
3568
+ return;
3569
+ if (Array.isArray(value)) {
3570
+ for (const item of value)
3571
+ collectRefIds(item, out);
3572
+ return;
3573
+ }
3574
+ if (!isObject(value))
3575
+ return;
3576
+ if (typeof value["Ref"] === "string") {
3577
+ const ref = value["Ref"];
3578
+ if (!ref.startsWith("AWS::"))
3579
+ out.add(ref);
3580
+ return;
3581
+ }
3582
+ if (Array.isArray(value["Fn::GetAtt"])) {
3583
+ const arr = value["Fn::GetAtt"];
3584
+ if (typeof arr[0] === "string")
3585
+ out.add(arr[0]);
3586
+ return;
3587
+ }
3588
+ }
3589
+
3536
3590
  // src/analyzer/dag-builder.ts
3537
3591
  var { Graph, alg } = graphlib;
3538
3592
  var IAM_ROLE_POLICY_TYPES = /* @__PURE__ */ new Set([
@@ -3581,6 +3635,7 @@ var DagBuilder = class {
3581
3635
  }
3582
3636
  this.logger.debug(`Dependency graph built: ${resourceIds.length} nodes, ${edgeCount} edges`);
3583
3637
  edgeCount += this.addCustomResourcePolicyEdges(graph, template);
3638
+ edgeCount += this.addLambdaVpcEdges(graph, template);
3584
3639
  if (!alg.isAcyclic(graph)) {
3585
3640
  const cycles = this.findCycles(graph);
3586
3641
  throw new DependencyError(
@@ -3773,6 +3828,41 @@ var DagBuilder = class {
3773
3828
  }
3774
3829
  return added;
3775
3830
  }
3831
+ /**
3832
+ * Add edges from Subnets / SecurityGroups referenced by an
3833
+ * AWS::Lambda::Function VpcConfig to the Lambda itself.
3834
+ *
3835
+ * Same direction as a normal `Ref`-derived edge (Subnet -> Lambda), so for
3836
+ * deploy this just duplicates what extractDependencies already produced.
3837
+ * The point is robustness: if a future template massages the VpcConfig
3838
+ * shape in a way the recursive extractor doesn't anticipate, this pass
3839
+ * still ties the Lambda to its networking resources so that the
3840
+ * deletion-time reverse traversal continues to delete Lambda before
3841
+ * Subnet / SecurityGroup.
3842
+ *
3843
+ * Returns the number of NEW edges added (existing edges are skipped).
3844
+ */
3845
+ addLambdaVpcEdges(graph, template) {
3846
+ const edges = extractLambdaVpcDeleteDeps(template.Resources);
3847
+ if (edges.length === 0)
3848
+ return 0;
3849
+ let added = 0;
3850
+ for (const edge of edges) {
3851
+ const depId = edge.after;
3852
+ const dependentId = edge.before;
3853
+ if (!graph.hasNode(depId) || !graph.hasNode(dependentId))
3854
+ continue;
3855
+ if (graph.hasEdge(depId, dependentId))
3856
+ continue;
3857
+ graph.setEdge(depId, dependentId);
3858
+ added++;
3859
+ this.logger.debug(`Added implicit edge (lambda vpc): ${depId} -> ${dependentId}`);
3860
+ }
3861
+ if (added > 0) {
3862
+ this.logger.debug(`Added ${added} implicit edges for Lambda VpcConfig`);
3863
+ }
3864
+ return added;
3865
+ }
3776
3866
  isCustomResourceType(type) {
3777
3867
  return type === "AWS::CloudFormation::CustomResource" || type.startsWith("Custom::");
3778
3868
  }
@@ -4688,8 +4778,8 @@ var IntrinsicFunctionResolver = class {
4688
4778
  return resource.attributes?.["CidrBlock"] || resource.properties?.["CidrBlock"];
4689
4779
  case "Ipv6CidrBlocks": {
4690
4780
  try {
4691
- const { EC2Client: EC2Client8, DescribeVpcsCommand: DescribeVpcsCommand3 } = await import("@aws-sdk/client-ec2");
4692
- const ec2 = new EC2Client8({ region: this.resolverRegion });
4781
+ const { EC2Client: EC2Client9, DescribeVpcsCommand: DescribeVpcsCommand3 } = await import("@aws-sdk/client-ec2");
4782
+ const ec2 = new EC2Client9({ region: this.resolverRegion });
4693
4783
  const maxAttempts = 15;
4694
4784
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4695
4785
  const resp = await ec2.send(new DescribeVpcsCommand3({ VpcIds: [physicalId] }));
@@ -10897,9 +10987,11 @@ import {
10897
10987
  GetFunctionCommand,
10898
10988
  ResourceNotFoundException
10899
10989
  } from "@aws-sdk/client-lambda";
10990
+ import { DescribeNetworkInterfacesCommand } from "@aws-sdk/client-ec2";
10900
10991
  init_aws_clients();
10901
10992
  var LambdaFunctionProvider = class {
10902
10993
  lambdaClient;
10994
+ ec2Client;
10903
10995
  logger = getLogger().child("LambdaFunctionProvider");
10904
10996
  handledProperties = /* @__PURE__ */ new Map([
10905
10997
  [
@@ -10919,13 +11011,22 @@ var LambdaFunctionProvider = class {
10919
11011
  "Architectures",
10920
11012
  "PackageType",
10921
11013
  "TracingConfig",
10922
- "EphemeralStorage"
11014
+ "EphemeralStorage",
11015
+ "VpcConfig"
10923
11016
  ])
10924
11017
  ]
10925
11018
  ]);
11019
+ // ENI detach polling configuration (overridable for tests).
11020
+ // Lambda VPC ENI detach is async and can take 20-40 minutes in the worst case;
11021
+ // we poll up to 10 minutes and then warn-and-continue, since downstream Subnet/SG
11022
+ // deletion has its own retry logic that handles a small remaining window.
11023
+ eniWaitTimeoutMs = 10 * 60 * 1e3;
11024
+ eniWaitInitialDelayMs = 1e4;
11025
+ eniWaitMaxDelayMs = 3e4;
10926
11026
  constructor() {
10927
11027
  const awsClients = getAwsClients();
10928
11028
  this.lambdaClient = awsClients.lambda;
11029
+ this.ec2Client = awsClients.ec2;
10929
11030
  }
10930
11031
  /**
10931
11032
  * Create a Lambda function
@@ -10973,6 +11074,7 @@ var LambdaFunctionProvider = class {
10973
11074
  PackageType: properties["PackageType"],
10974
11075
  TracingConfig: properties["TracingConfig"],
10975
11076
  EphemeralStorage: properties["EphemeralStorage"],
11077
+ VpcConfig: this.buildVpcConfig(properties["VpcConfig"]),
10976
11078
  Tags: tags
10977
11079
  };
10978
11080
  const response = await this.lambdaClient.send(new CreateFunctionCommand(createParams));
@@ -11011,7 +11113,8 @@ var LambdaFunctionProvider = class {
11011
11113
  "Environment",
11012
11114
  "Layers",
11013
11115
  "TracingConfig",
11014
- "EphemeralStorage"
11116
+ "EphemeralStorage",
11117
+ "VpcConfig"
11015
11118
  ];
11016
11119
  let hasConfigChanges = false;
11017
11120
  for (const field of configFields) {
@@ -11032,7 +11135,11 @@ var LambdaFunctionProvider = class {
11032
11135
  Environment: properties["Environment"],
11033
11136
  Layers: properties["Layers"],
11034
11137
  TracingConfig: properties["TracingConfig"],
11035
- EphemeralStorage: properties["EphemeralStorage"]
11138
+ EphemeralStorage: properties["EphemeralStorage"],
11139
+ VpcConfig: this.buildVpcConfigForUpdate(
11140
+ properties["VpcConfig"],
11141
+ previousProperties["VpcConfig"]
11142
+ )
11036
11143
  };
11037
11144
  await this.lambdaClient.send(new UpdateFunctionConfigurationCommand(configParams));
11038
11145
  this.logger.debug(`Updated configuration for Lambda function ${physicalId}`);
@@ -11076,9 +11183,19 @@ var LambdaFunctionProvider = class {
11076
11183
  }
11077
11184
  /**
11078
11185
  * Delete a Lambda function
11186
+ *
11187
+ * For VPC-enabled Lambda functions, AWS detaches the hyperplane ENIs
11188
+ * asynchronously after DeleteFunction returns. If we let downstream
11189
+ * resource deletion (Subnet / SecurityGroup) proceed immediately, those
11190
+ * deletions fail with "has dependencies" / "has a dependent object".
11191
+ *
11192
+ * To smooth this out, when properties carry a VpcConfig with subnets or
11193
+ * security groups, we poll DescribeNetworkInterfaces for the function's
11194
+ * managed ENIs and only return once they are gone (or the timeout elapses).
11079
11195
  */
11080
- async delete(logicalId, physicalId, resourceType, _properties) {
11196
+ async delete(logicalId, physicalId, resourceType, properties) {
11081
11197
  this.logger.debug(`Deleting Lambda function ${logicalId}: ${physicalId}`);
11198
+ const hasVpcConfig = this.hasVpcConfig(properties?.["VpcConfig"]);
11082
11199
  try {
11083
11200
  await this.lambdaClient.send(new DeleteFunctionCommand({ FunctionName: physicalId }));
11084
11201
  this.logger.debug(`Successfully deleted Lambda function ${logicalId}`);
@@ -11096,6 +11213,153 @@ var LambdaFunctionProvider = class {
11096
11213
  cause
11097
11214
  );
11098
11215
  }
11216
+ if (hasVpcConfig) {
11217
+ await this.waitForLambdaEnisDetached(physicalId);
11218
+ }
11219
+ }
11220
+ /**
11221
+ * Build Lambda VpcConfig parameter from CDK properties.
11222
+ *
11223
+ * Returns undefined when VpcConfig is unset, so the SDK leaves the function
11224
+ * outside any VPC. Returns an empty config (no subnets, no SGs) when caller
11225
+ * explicitly clears it on update — that detaches the function from its VPC.
11226
+ */
11227
+ buildVpcConfig(raw) {
11228
+ if (raw === void 0 || raw === null) {
11229
+ return void 0;
11230
+ }
11231
+ if (typeof raw !== "object") {
11232
+ return void 0;
11233
+ }
11234
+ const vpc = raw;
11235
+ const result = {};
11236
+ if (Array.isArray(vpc["SubnetIds"])) {
11237
+ result.SubnetIds = vpc["SubnetIds"];
11238
+ }
11239
+ if (Array.isArray(vpc["SecurityGroupIds"])) {
11240
+ result.SecurityGroupIds = vpc["SecurityGroupIds"];
11241
+ }
11242
+ if (typeof vpc["Ipv6AllowedForDualStack"] === "boolean") {
11243
+ result.Ipv6AllowedForDualStack = vpc["Ipv6AllowedForDualStack"];
11244
+ }
11245
+ return result;
11246
+ }
11247
+ /**
11248
+ * Build VpcConfig for an update call, accounting for VPC detach.
11249
+ *
11250
+ * UpdateFunctionConfiguration treats an absent VpcConfig as "no change",
11251
+ * so omitting it cannot move a function out of its existing VPC. To
11252
+ * detach we must explicitly send empty SubnetIds / SecurityGroupIds.
11253
+ */
11254
+ buildVpcConfigForUpdate(newRaw, previousRaw) {
11255
+ const next = this.buildVpcConfig(newRaw);
11256
+ if (next) {
11257
+ return next;
11258
+ }
11259
+ if (this.hasVpcConfig(previousRaw)) {
11260
+ return { SubnetIds: [], SecurityGroupIds: [] };
11261
+ }
11262
+ return void 0;
11263
+ }
11264
+ /**
11265
+ * Determine whether the function actually attaches to a VPC, i.e. has at
11266
+ * least one Subnet ID. A bare VpcConfig with empty arrays does not create
11267
+ * any ENIs, so we skip the wait in that case.
11268
+ */
11269
+ hasVpcConfig(raw) {
11270
+ if (raw === void 0 || raw === null || typeof raw !== "object") {
11271
+ return false;
11272
+ }
11273
+ const vpc = raw;
11274
+ const subnets = vpc["SubnetIds"];
11275
+ return Array.isArray(subnets) && subnets.length > 0;
11276
+ }
11277
+ /**
11278
+ * Poll DescribeNetworkInterfaces until the Lambda-managed ENIs for the
11279
+ * given function are gone, or the configured timeout elapses.
11280
+ *
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.
11284
+ *
11285
+ * Polling: starts at eniWaitInitialDelayMs (10s), exponential backoff up
11286
+ * 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.
11291
+ */
11292
+ async waitForLambdaEnisDetached(functionName) {
11293
+ const start = Date.now();
11294
+ let delay = this.eniWaitInitialDelayMs;
11295
+ let attempt = 0;
11296
+ this.logger.debug(
11297
+ `Waiting for Lambda VPC ENIs to detach for function ${functionName} (timeout ${this.eniWaitTimeoutMs}ms)`
11298
+ );
11299
+ const descriptionNeedle = `AWS Lambda VPC ENI`;
11300
+ const functionNamePattern = new RegExp(`(^|-)${escapeRegExp(functionName)}(-|$)`);
11301
+ for (; ; ) {
11302
+ attempt++;
11303
+ let count;
11304
+ try {
11305
+ count = await this.countLambdaEnis(descriptionNeedle, functionNamePattern);
11306
+ } catch (error) {
11307
+ this.logger.warn(
11308
+ `DescribeNetworkInterfaces failed while waiting for Lambda ENIs of ${functionName}: ${error instanceof Error ? error.message : String(error)}`
11309
+ );
11310
+ count = -1;
11311
+ }
11312
+ if (count === 0) {
11313
+ this.logger.debug(
11314
+ `Lambda ENIs for ${functionName} fully detached after ${attempt} poll(s) / ${Date.now() - start}ms`
11315
+ );
11316
+ return;
11317
+ }
11318
+ const elapsed = Date.now() - start;
11319
+ if (elapsed >= this.eniWaitTimeoutMs) {
11320
+ 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.`
11322
+ );
11323
+ return;
11324
+ }
11325
+ const remaining = this.eniWaitTimeoutMs - elapsed;
11326
+ const sleepMs = Math.min(delay, remaining);
11327
+ await this.sleep(sleepMs);
11328
+ delay = Math.min(delay * 2, this.eniWaitMaxDelayMs);
11329
+ }
11330
+ }
11331
+ /**
11332
+ * Count remaining Lambda-managed ENIs for the given function, paginating
11333
+ * through DescribeNetworkInterfaces and filtering on Description substring.
11334
+ *
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`.
11337
+ */
11338
+ async countLambdaEnis(descriptionNeedle, functionNamePattern) {
11339
+ let nextToken;
11340
+ let count = 0;
11341
+ do {
11342
+ const resp = await this.ec2Client.send(
11343
+ new DescribeNetworkInterfacesCommand({
11344
+ Filters: [
11345
+ // Lambda hyperplane ENIs are owned by the Lambda service principal.
11346
+ { Name: "requester-id", Values: ["*:awslambda_*"] }
11347
+ ],
11348
+ NextToken: nextToken
11349
+ })
11350
+ );
11351
+ for (const ni of resp.NetworkInterfaces ?? []) {
11352
+ const desc = ni.Description ?? "";
11353
+ if (desc.includes(descriptionNeedle) && functionNamePattern.test(desc)) {
11354
+ count++;
11355
+ }
11356
+ }
11357
+ nextToken = resp.NextToken;
11358
+ } while (nextToken);
11359
+ return count;
11360
+ }
11361
+ sleep(ms) {
11362
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
11099
11363
  }
11100
11364
  /**
11101
11365
  * Build Lambda Code parameter from CDK properties
@@ -11182,6 +11446,9 @@ var LambdaFunctionProvider = class {
11182
11446
  return (crc ^ 4294967295) >>> 0;
11183
11447
  }
11184
11448
  };
11449
+ function escapeRegExp(input) {
11450
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11451
+ }
11185
11452
 
11186
11453
  // src/provisioning/providers/lambda-permission-provider.ts
11187
11454
  import {
@@ -13505,6 +13772,8 @@ import {
13505
13772
  DeleteSecurityGroupCommand,
13506
13773
  AuthorizeSecurityGroupIngressCommand,
13507
13774
  RevokeSecurityGroupIngressCommand,
13775
+ AuthorizeSecurityGroupEgressCommand,
13776
+ RevokeSecurityGroupEgressCommand,
13508
13777
  CreateTagsCommand,
13509
13778
  DescribeSubnetsCommand as DescribeSubnetsCommand2,
13510
13779
  DescribeSecurityGroupsCommand as DescribeSecurityGroupsCommand2,
@@ -13554,7 +13823,14 @@ var EC2Provider = class {
13554
13823
  ["AWS::EC2::SubnetRouteTableAssociation", /* @__PURE__ */ new Set(["SubnetId", "RouteTableId"])],
13555
13824
  [
13556
13825
  "AWS::EC2::SecurityGroup",
13557
- /* @__PURE__ */ new Set(["GroupDescription", "GroupName", "VpcId", "SecurityGroupIngress", "Tags"])
13826
+ /* @__PURE__ */ new Set([
13827
+ "GroupDescription",
13828
+ "GroupName",
13829
+ "VpcId",
13830
+ "SecurityGroupIngress",
13831
+ "SecurityGroupEgress",
13832
+ "Tags"
13833
+ ])
13558
13834
  ],
13559
13835
  [
13560
13836
  "AWS::EC2::SecurityGroupIngress",
@@ -13565,7 +13841,8 @@ var EC2Provider = class {
13565
13841
  "ToPort",
13566
13842
  "CidrIp",
13567
13843
  "Description",
13568
- "SourceSecurityGroupId"
13844
+ "SourceSecurityGroupId",
13845
+ "SourceSecurityGroupOwnerId"
13569
13846
  ])
13570
13847
  ],
13571
13848
  [
@@ -13664,7 +13941,13 @@ var EC2Provider = class {
13664
13941
  case "AWS::EC2::SubnetRouteTableAssociation":
13665
13942
  return this.updateSubnetRouteTableAssociation(logicalId, physicalId);
13666
13943
  case "AWS::EC2::SecurityGroup":
13667
- return this.updateSecurityGroup(logicalId, physicalId, resourceType, properties);
13944
+ return this.updateSecurityGroup(
13945
+ logicalId,
13946
+ physicalId,
13947
+ resourceType,
13948
+ properties,
13949
+ previousProperties
13950
+ );
13668
13951
  case "AWS::EC2::SecurityGroupIngress":
13669
13952
  return this.updateSecurityGroupIngress(
13670
13953
  logicalId,
@@ -14389,6 +14672,34 @@ var EC2Provider = class {
14389
14672
  );
14390
14673
  }
14391
14674
  }
14675
+ const egressRules = properties["SecurityGroupEgress"];
14676
+ if (egressRules && Array.isArray(egressRules)) {
14677
+ try {
14678
+ await this.ec2Client.send(
14679
+ new RevokeSecurityGroupEgressCommand({
14680
+ GroupId: groupId,
14681
+ IpPermissions: [
14682
+ {
14683
+ IpProtocol: "-1",
14684
+ IpRanges: [{ CidrIp: "0.0.0.0/0" }]
14685
+ }
14686
+ ]
14687
+ })
14688
+ );
14689
+ } catch (error) {
14690
+ if (!this.isNotFoundError(error)) {
14691
+ throw error;
14692
+ }
14693
+ }
14694
+ for (const rule of egressRules) {
14695
+ await this.ec2Client.send(
14696
+ new AuthorizeSecurityGroupEgressCommand({
14697
+ GroupId: groupId,
14698
+ IpPermissions: [this.buildIpPermission(rule, "egress")]
14699
+ })
14700
+ );
14701
+ }
14702
+ }
14392
14703
  this.logger.debug(`Successfully created SecurityGroup ${logicalId}: ${groupId}`);
14393
14704
  return {
14394
14705
  physicalId: groupId,
@@ -14408,10 +14719,22 @@ var EC2Provider = class {
14408
14719
  );
14409
14720
  }
14410
14721
  }
14411
- async updateSecurityGroup(logicalId, physicalId, resourceType, properties) {
14722
+ async updateSecurityGroup(logicalId, physicalId, resourceType, properties, previousProperties) {
14412
14723
  this.logger.debug(`Updating SecurityGroup ${logicalId}: ${physicalId}`);
14413
14724
  try {
14414
14725
  await this.applyTags(physicalId, properties, logicalId);
14726
+ await this.applySecurityGroupRuleDiff(
14727
+ physicalId,
14728
+ previousProperties["SecurityGroupIngress"] ?? [],
14729
+ properties["SecurityGroupIngress"] ?? [],
14730
+ "ingress"
14731
+ );
14732
+ await this.applySecurityGroupRuleDiff(
14733
+ physicalId,
14734
+ previousProperties["SecurityGroupEgress"] ?? [],
14735
+ properties["SecurityGroupEgress"] ?? [],
14736
+ "egress"
14737
+ );
14415
14738
  this.logger.debug(`Successfully updated SecurityGroup ${logicalId}`);
14416
14739
  return {
14417
14740
  physicalId,
@@ -14769,9 +15092,13 @@ var EC2Provider = class {
14769
15092
  }
14770
15093
  // ─── Helpers ──────────────────────────────────────────────────────
14771
15094
  /**
14772
- * Build an IpPermission object from CloudFormation-style properties
15095
+ * Build an IpPermission object from CloudFormation-style properties.
15096
+ *
15097
+ * The EC2 IpPermission shape is identical for ingress and egress; only the
15098
+ * CFn property names that point to the "other" security group differ
15099
+ * (SourceSecurityGroupId vs DestinationSecurityGroupId).
14773
15100
  */
14774
- buildIpPermission(properties) {
15101
+ buildIpPermission(properties, direction = "ingress") {
14775
15102
  const ipProtocol = properties["IpProtocol"] ?? "-1";
14776
15103
  const fromPort = properties["FromPort"];
14777
15104
  const toPort = properties["ToPort"];
@@ -14781,6 +15108,7 @@ var EC2Provider = class {
14781
15108
  if (toPort !== void 0)
14782
15109
  permission.ToPort = toPort;
14783
15110
  const cidrIp = properties["CidrIp"];
15111
+ const cidrIpv6 = properties["CidrIpv6"];
14784
15112
  const description = properties["Description"];
14785
15113
  if (cidrIp) {
14786
15114
  const ipRange = { CidrIp: cidrIp };
@@ -14788,17 +15116,124 @@ var EC2Provider = class {
14788
15116
  ipRange.Description = description;
14789
15117
  permission.IpRanges = [ipRange];
14790
15118
  }
14791
- const sourceSecurityGroupId = properties["SourceSecurityGroupId"];
14792
- if (sourceSecurityGroupId) {
15119
+ if (cidrIpv6) {
15120
+ const ipv6Range = { CidrIpv6: cidrIpv6 };
15121
+ if (description)
15122
+ ipv6Range.Description = description;
15123
+ permission.Ipv6Ranges = [ipv6Range];
15124
+ }
15125
+ const peerGroupId = direction === "egress" ? properties["DestinationSecurityGroupId"] : properties["SourceSecurityGroupId"];
15126
+ if (peerGroupId) {
14793
15127
  const groupPair = {
14794
- GroupId: sourceSecurityGroupId
15128
+ GroupId: peerGroupId
14795
15129
  };
15130
+ if (direction === "ingress") {
15131
+ const peerOwnerId = properties["SourceSecurityGroupOwnerId"];
15132
+ if (peerOwnerId)
15133
+ groupPair.UserId = peerOwnerId;
15134
+ }
14796
15135
  if (description)
14797
15136
  groupPair.Description = description;
14798
15137
  permission.UserIdGroupPairs = [groupPair];
14799
15138
  }
15139
+ const prefixListId = direction === "egress" ? properties["DestinationPrefixListId"] : properties["SourcePrefixListId"];
15140
+ if (prefixListId) {
15141
+ const prefixEntry = {
15142
+ PrefixListId: prefixListId
15143
+ };
15144
+ if (description)
15145
+ prefixEntry.Description = description;
15146
+ permission.PrefixListIds = [prefixEntry];
15147
+ }
14800
15148
  return permission;
14801
15149
  }
15150
+ /**
15151
+ * Compute the diff between two sets of SecurityGroup rule definitions
15152
+ * (ingress or egress) and apply the resulting authorize/revoke calls.
15153
+ *
15154
+ * Rules are identified by a deterministic key derived from their full
15155
+ * shape — protocol, ports, CIDR, peer group, prefix list, description —
15156
+ * so updating any of those fields counts as a replacement (revoke + authorize).
15157
+ */
15158
+ async applySecurityGroupRuleDiff(groupId, previousRules, nextRules, direction) {
15159
+ const ruleKey = (rule) => {
15160
+ const peerKey = direction === "egress" ? rule["DestinationSecurityGroupId"] : rule["SourceSecurityGroupId"];
15161
+ const prefixKey = direction === "egress" ? rule["DestinationPrefixListId"] : rule["SourcePrefixListId"];
15162
+ const peerOwner = direction === "ingress" ? rule["SourceSecurityGroupOwnerId"] : void 0;
15163
+ return JSON.stringify({
15164
+ p: rule["IpProtocol"] ?? "-1",
15165
+ f: rule["FromPort"] ?? null,
15166
+ t: rule["ToPort"] ?? null,
15167
+ c4: rule["CidrIp"] ?? null,
15168
+ c6: rule["CidrIpv6"] ?? null,
15169
+ peer: peerKey ?? null,
15170
+ peerOwner: peerOwner ?? null,
15171
+ pl: prefixKey ?? null,
15172
+ d: rule["Description"] ?? null
15173
+ });
15174
+ };
15175
+ const prevByKey = /* @__PURE__ */ new Map();
15176
+ for (const rule of previousRules)
15177
+ prevByKey.set(ruleKey(rule), rule);
15178
+ const nextByKey = /* @__PURE__ */ new Map();
15179
+ for (const rule of nextRules)
15180
+ nextByKey.set(ruleKey(rule), rule);
15181
+ const toRevoke = [];
15182
+ for (const [key, rule] of prevByKey) {
15183
+ if (!nextByKey.has(key))
15184
+ toRevoke.push(rule);
15185
+ }
15186
+ const toAuthorize = [];
15187
+ for (const [key, rule] of nextByKey) {
15188
+ if (!prevByKey.has(key))
15189
+ toAuthorize.push(rule);
15190
+ }
15191
+ for (const rule of toRevoke) {
15192
+ try {
15193
+ if (direction === "egress") {
15194
+ await this.ec2Client.send(
15195
+ new RevokeSecurityGroupEgressCommand({
15196
+ GroupId: groupId,
15197
+ IpPermissions: [this.buildIpPermission(rule, "egress")]
15198
+ })
15199
+ );
15200
+ } else {
15201
+ await this.ec2Client.send(
15202
+ new RevokeSecurityGroupIngressCommand({
15203
+ GroupId: groupId,
15204
+ IpPermissions: [this.buildIpPermission(rule, "ingress")]
15205
+ })
15206
+ );
15207
+ }
15208
+ } catch (error) {
15209
+ if (!this.isNotFoundError(error))
15210
+ throw error;
15211
+ }
15212
+ }
15213
+ for (const rule of toAuthorize) {
15214
+ try {
15215
+ if (direction === "egress") {
15216
+ await this.ec2Client.send(
15217
+ new AuthorizeSecurityGroupEgressCommand({
15218
+ GroupId: groupId,
15219
+ IpPermissions: [this.buildIpPermission(rule, "egress")]
15220
+ })
15221
+ );
15222
+ } else {
15223
+ await this.ec2Client.send(
15224
+ new AuthorizeSecurityGroupIngressCommand({
15225
+ GroupId: groupId,
15226
+ IpPermissions: [this.buildIpPermission(rule, "ingress")]
15227
+ })
15228
+ );
15229
+ }
15230
+ } catch (error) {
15231
+ if (!(error instanceof Error && error.message.includes("already exists"))) {
15232
+ throw error;
15233
+ }
15234
+ }
15235
+ }
15236
+ }
14802
15237
  // ─── AWS::EC2::NetworkAcl ────────────────────────────────────────
14803
15238
  async createNetworkAcl(logicalId, resourceType, properties) {
14804
15239
  this.logger.debug(`Creating NetworkAcl ${logicalId}`);
@@ -16719,7 +17154,7 @@ var CloudFrontDistributionProvider = class {
16719
17154
  this.logger.debug(`Created CloudFront Distribution: ${distributionId} (${domainName})`);
16720
17155
  if (process.env["CDKD_NO_WAIT"] !== "true") {
16721
17156
  this.logger.debug(`Waiting for Distribution ${distributionId} to reach Deployed status...`);
16722
- await this.waitForDistributionDeployed(distributionId);
17157
+ await this.waitForDistributionStable(distributionId);
16723
17158
  }
16724
17159
  return {
16725
17160
  physicalId: distributionId,
@@ -16829,7 +17264,7 @@ var CloudFrontDistributionProvider = class {
16829
17264
  })
16830
17265
  );
16831
17266
  etag = updateResponse.ETag;
16832
- await this.waitForDistributionDeployed(physicalId);
17267
+ await this.waitForDistributionStable(physicalId, false);
16833
17268
  const refreshResponse = await this.cloudFrontClient.send(
16834
17269
  new GetDistributionConfigCommand({ Id: physicalId })
16835
17270
  );
@@ -16873,10 +17308,16 @@ var CloudFrontDistributionProvider = class {
16873
17308
  throw new Error(`Unsupported attribute: ${attributeName} for AWS::CloudFront::Distribution`);
16874
17309
  }
16875
17310
  /**
16876
- * Wait for a distribution to reach "Deployed" status.
17311
+ * Wait for a distribution to reach a stable state.
17312
+ *
17313
+ * "Stable" means Status === 'Deployed'. When `expectedEnabled` is provided,
17314
+ * we additionally require DistributionConfig.Enabled === expectedEnabled —
17315
+ * this guards against CloudFront's eventually-consistent reads that can
17316
+ * briefly return the pre-update snapshot after UpdateDistribution returns.
17317
+ *
16877
17318
  * Uses exponential backoff polling.
16878
17319
  */
16879
- async waitForDistributionDeployed(distributionId) {
17320
+ async waitForDistributionStable(distributionId, expectedEnabled) {
16880
17321
  const maxAttempts = 60;
16881
17322
  let delay = 5e3;
16882
17323
  const maxDelay = 3e4;
@@ -16897,12 +17338,16 @@ var CloudFrontDistributionProvider = class {
16897
17338
  new GetDistributionCommand({ Id: distributionId })
16898
17339
  );
16899
17340
  const status = response.Distribution?.Status;
16900
- if (status === "Deployed") {
16901
- this.logger.debug(`Distribution ${distributionId} is now Deployed`);
17341
+ const enabled = response.Distribution?.DistributionConfig?.Enabled;
17342
+ const enabledMatches = expectedEnabled === void 0 || enabled === expectedEnabled;
17343
+ if (status === "Deployed" && enabledMatches) {
17344
+ this.logger.debug(
17345
+ `Distribution ${distributionId} is stable (Status=Deployed, Enabled=${enabled})`
17346
+ );
16902
17347
  return;
16903
17348
  }
16904
17349
  this.logger.debug(
16905
- `Distribution ${distributionId} status: ${status} (attempt ${attempt}/${maxAttempts})`
17350
+ `Distribution ${distributionId} status: ${status}, enabled: ${enabled}` + (expectedEnabled === void 0 ? "" : ` (waiting for Enabled=${expectedEnabled})`) + ` (attempt ${attempt}/${maxAttempts})`
16906
17351
  );
16907
17352
  const sleepEnd = Date.now() + delay;
16908
17353
  while (Date.now() < sleepEnd && !interrupted) {
@@ -16911,7 +17356,7 @@ var CloudFrontDistributionProvider = class {
16911
17356
  delay = Math.min(delay * 1.5, maxDelay);
16912
17357
  }
16913
17358
  this.logger.debug(
16914
- `Distribution ${distributionId} did not reach Deployed status within timeout, proceeding with deletion attempt`
17359
+ `Distribution ${distributionId} did not reach stable state within timeout, proceeding with next step`
16915
17360
  );
16916
17361
  } finally {
16917
17362
  process.removeListener("SIGINT", sigintHandler);
@@ -24137,7 +24582,7 @@ import {
24137
24582
  DeleteObjectsCommand as DeleteObjectsCommand2
24138
24583
  } from "@aws-sdk/client-s3";
24139
24584
  import { GetCallerIdentityCommand as GetCallerIdentityCommand7 } from "@aws-sdk/client-sts";
24140
- import { EC2Client as EC2Client7, DescribeAvailabilityZonesCommand as DescribeAvailabilityZonesCommand3 } from "@aws-sdk/client-ec2";
24585
+ import { EC2Client as EC2Client8, DescribeAvailabilityZonesCommand as DescribeAvailabilityZonesCommand3 } from "@aws-sdk/client-ec2";
24141
24586
  init_aws_clients();
24142
24587
  var S3DirectoryBucketProvider = class {
24143
24588
  s3Client;
@@ -24155,7 +24600,7 @@ var S3DirectoryBucketProvider = class {
24155
24600
  }
24156
24601
  getEc2Client() {
24157
24602
  if (!this.ec2Client) {
24158
- this.ec2Client = new EC2Client7(this.providerRegion ? { region: this.providerRegion } : {});
24603
+ this.ec2Client = new EC2Client8(this.providerRegion ? { region: this.providerRegion } : {});
24159
24604
  }
24160
24605
  return this.ec2Client;
24161
24606
  }
@@ -25053,15 +25498,141 @@ function registerAllProviders(registry) {
25053
25498
  registry.register("AWS::S3Tables::Table", s3TablesProvider);
25054
25499
  }
25055
25500
 
25501
+ // src/deployment/dag-executor.ts
25502
+ var DagExecutor = class {
25503
+ nodes = /* @__PURE__ */ new Map();
25504
+ logger = getLogger().child("DagExecutor");
25505
+ add(node) {
25506
+ this.nodes.set(node.id, node);
25507
+ }
25508
+ has(id) {
25509
+ return this.nodes.has(id);
25510
+ }
25511
+ size() {
25512
+ return this.nodes.size;
25513
+ }
25514
+ values() {
25515
+ return this.nodes.values();
25516
+ }
25517
+ async execute(concurrency, fn, cancelled = () => false) {
25518
+ let active = 0;
25519
+ const errors = [];
25520
+ return new Promise((resolve4, reject) => {
25521
+ const dispatch = () => {
25522
+ let changed = true;
25523
+ while (changed) {
25524
+ changed = false;
25525
+ for (const node of this.nodes.values()) {
25526
+ if (node.state !== "pending")
25527
+ continue;
25528
+ const hasFailedDep = [...node.dependencies].some((depId) => {
25529
+ const dep = this.nodes.get(depId);
25530
+ return dep && (dep.state === "failed" || dep.state === "skipped");
25531
+ });
25532
+ if (hasFailedDep) {
25533
+ node.state = "skipped";
25534
+ changed = true;
25535
+ this.logger.debug(`Skipped ${node.id}: dependency failed or was skipped`);
25536
+ }
25537
+ }
25538
+ }
25539
+ const ready = [];
25540
+ for (const node of this.nodes.values()) {
25541
+ if (node.state !== "pending")
25542
+ continue;
25543
+ const depsReady = [...node.dependencies].every((depId) => {
25544
+ const dep = this.nodes.get(depId);
25545
+ return !dep || dep.state === "completed";
25546
+ });
25547
+ if (depsReady)
25548
+ ready.push(node);
25549
+ }
25550
+ if (!cancelled()) {
25551
+ for (const node of ready) {
25552
+ if (active >= concurrency)
25553
+ break;
25554
+ node.state = "running";
25555
+ active++;
25556
+ fn(node).then(() => {
25557
+ node.state = "completed";
25558
+ }).catch((error) => {
25559
+ node.state = "failed";
25560
+ errors.push({ id: node.id, error });
25561
+ }).finally(() => {
25562
+ active--;
25563
+ dispatch();
25564
+ });
25565
+ }
25566
+ }
25567
+ if (active === 0) {
25568
+ if (errors.length > 0) {
25569
+ reject(errors[0].error);
25570
+ return;
25571
+ }
25572
+ const stillPending = [...this.nodes.values()].some((n) => n.state === "pending");
25573
+ if (stillPending && !cancelled()) {
25574
+ const pending = [...this.nodes.values()].filter((n) => n.state === "pending").map((n) => n.id);
25575
+ reject(
25576
+ new Error(
25577
+ `Deadlock detected: ${pending.length} node(s) stuck with unresolvable dependencies (${pending.join(", ")})`
25578
+ )
25579
+ );
25580
+ return;
25581
+ }
25582
+ resolve4();
25583
+ }
25584
+ };
25585
+ dispatch();
25586
+ });
25587
+ }
25588
+ };
25589
+
25590
+ // src/analyzer/implicit-delete-deps.ts
25591
+ var IMPLICIT_DELETE_DEPENDENCIES = {
25592
+ // IGW must be deleted AFTER VPCGatewayAttachment
25593
+ "AWS::EC2::InternetGateway": ["AWS::EC2::VPCGatewayAttachment"],
25594
+ // EventBus must be deleted AFTER Rules on that bus
25595
+ "AWS::Events::EventBus": ["AWS::Events::Rule"],
25596
+ // Athena workgroup must be deleted AFTER its named queries
25597
+ "AWS::Athena::WorkGroup": ["AWS::Athena::NamedQuery"],
25598
+ // CloudFront managed-policy-style resources must be deleted AFTER
25599
+ // any Distribution that references them
25600
+ "AWS::CloudFront::ResponseHeadersPolicy": ["AWS::CloudFront::Distribution"],
25601
+ "AWS::CloudFront::CachePolicy": ["AWS::CloudFront::Distribution"],
25602
+ "AWS::CloudFront::OriginAccessControl": ["AWS::CloudFront::Distribution"],
25603
+ // VPC must be deleted AFTER all VPC-dependent resources
25604
+ "AWS::EC2::VPC": [
25605
+ "AWS::EC2::Subnet",
25606
+ "AWS::EC2::SecurityGroup",
25607
+ "AWS::EC2::InternetGateway",
25608
+ "AWS::EC2::EgressOnlyInternetGateway",
25609
+ "AWS::EC2::VPCGatewayAttachment",
25610
+ "AWS::EC2::RouteTable"
25611
+ ],
25612
+ // Subnet must be deleted AFTER any Lambda that may still hold an ENI
25613
+ // in it. Lambda DELETE returns immediately but the ENI is detached
25614
+ // asynchronously by AWS, so deleting the Subnet first races the detach
25615
+ // and yields "DependencyViolation".
25616
+ "AWS::EC2::Subnet": ["AWS::EC2::SubnetRouteTableAssociation", "AWS::Lambda::Function"],
25617
+ // RouteTable must be deleted AFTER Route and Association
25618
+ "AWS::EC2::RouteTable": ["AWS::EC2::Route", "AWS::EC2::SubnetRouteTableAssociation"],
25619
+ // SecurityGroup must be deleted AFTER any Lambda whose ENI is bound
25620
+ // to it (same ENI-detach race as Subnet above).
25621
+ "AWS::EC2::SecurityGroup": [
25622
+ "AWS::EC2::SecurityGroupIngress",
25623
+ "AWS::EC2::SecurityGroupEgress",
25624
+ "AWS::Lambda::Function"
25625
+ ]
25626
+ };
25627
+
25056
25628
  // src/deployment/deploy-engine.ts
25057
- import pLimit from "p-limit";
25058
25629
  var InterruptedError = class extends Error {
25059
25630
  constructor() {
25060
25631
  super("Deployment interrupted by user (Ctrl+C)");
25061
25632
  this.name = "InterruptedError";
25062
25633
  }
25063
25634
  };
25064
- var DeployEngine = class _DeployEngine {
25635
+ var DeployEngine = class {
25065
25636
  constructor(stateBackend, lockManager, dagBuilder, diffCalculator, providerRegistry, options = {}, stackRegion) {
25066
25637
  this.stateBackend = stateBackend;
25067
25638
  this.lockManager = lockManager;
@@ -25187,6 +25758,7 @@ var DeployEngine = class _DeployEngine {
25187
25758
  template,
25188
25759
  currentState,
25189
25760
  changes,
25761
+ dag,
25190
25762
  executionLevels,
25191
25763
  stackName,
25192
25764
  parameterValues,
@@ -25219,13 +25791,16 @@ var DeployEngine = class _DeployEngine {
25219
25791
  }
25220
25792
  }
25221
25793
  /**
25222
- * Execute deployment by processing resources in DAG order
25794
+ * Execute deployment by processing resources via event-driven DAG dispatch.
25223
25795
  *
25224
- * Important: DELETE operations are executed in reverse dependency order,
25225
- * while CREATE/UPDATE follow normal dependency order.
25226
- */
25227
- async executeDeployment(template, currentState, changes, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
25228
- const limit = pLimit(this.options.concurrency);
25796
+ * - CREATE/UPDATE follow forward dependency order (a node starts as soon as
25797
+ * ALL of its dependencies are completed — does not wait for unrelated
25798
+ * siblings in the same "level")
25799
+ * - DELETE follows reverse dependency order (a node starts as soon as all
25800
+ * resources that depend ON it have finished deleting)
25801
+ */
25802
+ async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
25803
+ const concurrency = this.options.concurrency;
25229
25804
  const newResources = { ...currentState.resources };
25230
25805
  const actualCounts = { created: 0, updated: 0, deleted: 0, skipped: 0 };
25231
25806
  const completedOperations = [];
@@ -25256,32 +25831,36 @@ var DeployEngine = class _DeployEngine {
25256
25831
  Array.from(changes.entries()).filter(([_, change]) => change.changeType === "DELETE").map(([logicalId]) => logicalId)
25257
25832
  );
25258
25833
  try {
25259
- for (let levelIndex = 0; levelIndex < executionLevels.length; levelIndex++) {
25260
- if (this.interrupted) {
25261
- throw new InterruptedError();
25262
- }
25263
- const levelNodes = executionLevels[levelIndex];
25264
- if (!levelNodes)
25834
+ const createUpdateIds = [];
25835
+ for (const [id, change] of changes.entries()) {
25836
+ if (deleteChanges.has(id))
25265
25837
  continue;
25266
- const level = levelNodes.filter((id) => {
25267
- if (deleteChanges.has(id))
25268
- return false;
25269
- const change = changes.get(id);
25270
- return !!change && change.changeType !== "NO_CHANGE";
25271
- });
25272
- if (level.length === 0)
25838
+ if (change.changeType === "NO_CHANGE")
25273
25839
  continue;
25840
+ createUpdateIds.push(id);
25841
+ }
25842
+ if (createUpdateIds.length > 0) {
25274
25843
  this.logger.info(
25275
- `Level ${levelIndex + 1}/${executionLevels.length} (${level.length} resources)`
25844
+ `Deploying ${createUpdateIds.length} resource(s) (DAG: ${executionLevels.length} levels, max parallel: ${concurrency})`
25276
25845
  );
25277
- const results = await Promise.allSettled(
25278
- level.map(
25279
- (logicalId) => limit(async () => {
25280
- const change = changes.get(logicalId);
25281
- if (!change || change.changeType === "NO_CHANGE") {
25282
- this.logger.debug(`Skipping ${logicalId} (no change)`);
25283
- return;
25284
- }
25846
+ const createUpdateExecutor = new DagExecutor();
25847
+ const provisionable = new Set(createUpdateIds);
25848
+ for (const id of createUpdateIds) {
25849
+ const allDeps = this.dagBuilder.getDirectDependencies(dag, id);
25850
+ const deps = new Set(allDeps.filter((d) => provisionable.has(d)));
25851
+ createUpdateExecutor.add({
25852
+ id,
25853
+ dependencies: deps,
25854
+ state: "pending",
25855
+ data: changes.get(id)
25856
+ });
25857
+ }
25858
+ try {
25859
+ await createUpdateExecutor.execute(
25860
+ concurrency,
25861
+ async (node) => {
25862
+ const logicalId = node.id;
25863
+ const change = node.data;
25285
25864
  const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
25286
25865
  try {
25287
25866
  await this.provisionResource(
@@ -25308,30 +25887,36 @@ var DeployEngine = class _DeployEngine {
25308
25887
  properties: newResources[logicalId]?.properties
25309
25888
  });
25310
25889
  saveStateAfterResource(logicalId);
25311
- })
25312
- )
25313
- );
25314
- await saveChain;
25315
- const failures = results.filter((r) => r.status === "rejected");
25316
- if (failures.length > 0) {
25317
- throw failures[0].reason;
25890
+ },
25891
+ () => this.interrupted
25892
+ );
25893
+ } finally {
25894
+ await saveChain;
25895
+ }
25896
+ if (this.interrupted && this.hasPending(createUpdateExecutor)) {
25897
+ throw new InterruptedError();
25318
25898
  }
25319
25899
  }
25320
25900
  if (deleteChanges.size > 0) {
25321
25901
  this.logger.info(`Deleting ${deleteChanges.size} resource(s)`);
25322
- const deletionLevels = this.buildDeletionLevels(deleteChanges, currentState);
25323
- for (let levelIndex = 0; levelIndex < deletionLevels.length; levelIndex++) {
25324
- if (this.interrupted) {
25325
- throw new InterruptedError();
25326
- }
25327
- const level = deletionLevels[levelIndex];
25328
- if (level.length === 0)
25329
- continue;
25330
- const deleteResults = await Promise.allSettled(
25331
- level.map(
25332
- (logicalId) => limit(async () => {
25333
- const change = changes.get(logicalId);
25334
- const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
25902
+ const deleteDeps = this.buildDeletionDependencies(deleteChanges, currentState);
25903
+ const deleteExecutor = new DagExecutor();
25904
+ for (const id of deleteChanges) {
25905
+ deleteExecutor.add({
25906
+ id,
25907
+ dependencies: deleteDeps.get(id) ?? /* @__PURE__ */ new Set(),
25908
+ state: "pending",
25909
+ data: changes.get(id)
25910
+ });
25911
+ }
25912
+ try {
25913
+ await deleteExecutor.execute(
25914
+ concurrency,
25915
+ async (node) => {
25916
+ const logicalId = node.id;
25917
+ const change = node.data;
25918
+ const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
25919
+ try {
25335
25920
  await this.provisionResource(
25336
25921
  logicalId,
25337
25922
  change,
@@ -25343,23 +25928,25 @@ var DeployEngine = class _DeployEngine {
25343
25928
  actualCounts,
25344
25929
  progress
25345
25930
  );
25346
- completedOperations.push({
25347
- logicalId,
25348
- changeType: "DELETE",
25349
- resourceType: change.resourceType,
25350
- previousState
25351
- });
25352
- saveStateAfterResource(logicalId);
25353
- })
25354
- )
25931
+ } catch (provisionError) {
25932
+ this.interrupted = true;
25933
+ throw provisionError;
25934
+ }
25935
+ completedOperations.push({
25936
+ logicalId,
25937
+ changeType: "DELETE",
25938
+ resourceType: change.resourceType,
25939
+ previousState
25940
+ });
25941
+ saveStateAfterResource(logicalId);
25942
+ },
25943
+ () => this.interrupted
25355
25944
  );
25945
+ } finally {
25356
25946
  await saveChain;
25357
- const deleteFailures = deleteResults.filter(
25358
- (r) => r.status === "rejected"
25359
- );
25360
- if (deleteFailures.length > 0) {
25361
- throw deleteFailures[0].reason;
25362
- }
25947
+ }
25948
+ if (this.interrupted && this.hasPending(deleteExecutor)) {
25949
+ throw new InterruptedError();
25363
25950
  }
25364
25951
  }
25365
25952
  } catch (error) {
@@ -25453,12 +26040,12 @@ var DeployEngine = class _DeployEngine {
25453
26040
  * - UPDATE → update back to previous properties
25454
26041
  * - DELETE → cannot rollback (resource already deleted), log warning
25455
26042
  *
25456
- * Resources created in the same DAG level may have dependencies between them
25457
- * (e.g., IAM Policy depends on IAM Role). When rolling back CREATEs (deleting),
25458
- * dependent resources must be deleted before their dependencies. This method
25459
- * sorts CREATE rollback operations using dependency information from state,
25460
- * then processes UPDATE/DELETE rollbacks, and finally processes sorted CREATE
25461
- * rollback deletions.
26043
+ * Resources completed concurrently in the dispatcher may have dependencies
26044
+ * between them (e.g., IAM Policy depends on IAM Role). When rolling back
26045
+ * CREATEs (deleting), dependent resources must be deleted before their
26046
+ * dependencies. This method sorts CREATE rollback operations using dependency
26047
+ * information from state, then processes UPDATE/DELETE rollbacks, and finally
26048
+ * processes sorted CREATE rollback deletions.
25462
26049
  */
25463
26050
  async performRollback(completedOperations, stateResources, _stackName) {
25464
26051
  if (completedOperations.length === 0) {
@@ -25491,7 +26078,7 @@ var DeployEngine = class _DeployEngine {
25491
26078
  * Sort CREATE rollback operations so that resources depending on others
25492
26079
  * are deleted first (reverse dependency order).
25493
26080
  *
25494
- * Uses state dependencies to build deletion levels, similar to buildDeletionLevels.
26081
+ * Uses state dependencies to determine reverse-dependency order, similar to buildDeletionDependencies.
25495
26082
  */
25496
26083
  sortRollbackCreates(createOps, stateResources) {
25497
26084
  const opMap = /* @__PURE__ */ new Map();
@@ -25882,48 +26469,34 @@ var DeployEngine = class _DeployEngine {
25882
26469
  const deps = parser.extractDependencies(resource);
25883
26470
  return deps.size > 0 ? [...deps] : void 0;
25884
26471
  }
26472
+ // Type-based implicit deletion ordering rules are defined in
26473
+ // src/analyzer/implicit-delete-deps.ts so the deploy DELETE phase and the
26474
+ // standalone destroy command apply the same rules.
25885
26475
  /**
25886
- * Implicit dependency map for correct deletion order.
26476
+ * Build a per-resource map of "must be deleted before me" dependencies for
26477
+ * the DELETE phase, derived from state-recorded dependencies plus implicit
26478
+ * type-based ordering rules.
25887
26479
  *
25888
- * Key = resource type that must be deleted AFTER all value types are deleted.
25889
- * Value = resource types that must be deleted BEFORE the key type.
25890
- *
25891
- * Example: InternetGateway depends on VPCGatewayAttachment being deleted first,
25892
- * because AWS won't let you delete an IGW while it's still attached to a VPC.
25893
- */
25894
- static IMPLICIT_DELETE_DEPENDENCIES = {
25895
- // IGW must be deleted AFTER VPCGatewayAttachment
25896
- "AWS::EC2::InternetGateway": ["AWS::EC2::VPCGatewayAttachment"],
25897
- // EventBus must be deleted AFTER Rules on that bus
25898
- "AWS::Events::EventBus": ["AWS::Events::Rule"],
25899
- // VPC must be deleted AFTER all VPC-dependent resources
25900
- "AWS::EC2::VPC": [
25901
- "AWS::EC2::Subnet",
25902
- "AWS::EC2::SecurityGroup",
25903
- "AWS::EC2::InternetGateway",
25904
- "AWS::EC2::EgressOnlyInternetGateway",
25905
- "AWS::EC2::VPCGatewayAttachment",
25906
- "AWS::EC2::RouteTable"
25907
- ],
25908
- // Subnet must be deleted AFTER RouteTableAssociation
25909
- "AWS::EC2::Subnet": ["AWS::EC2::SubnetRouteTableAssociation"],
25910
- // RouteTable must be deleted AFTER Route and Association
25911
- "AWS::EC2::RouteTable": ["AWS::EC2::Route", "AWS::EC2::SubnetRouteTableAssociation"],
25912
- // SecurityGroup must be deleted AFTER SecurityGroupIngress/Egress
25913
- "AWS::EC2::SecurityGroup": ["AWS::EC2::SecurityGroupIngress", "AWS::EC2::SecurityGroupEgress"]
25914
- };
26480
+ * For a resource X, the returned set contains every resource Y such that Y
26481
+ * must finish deleting before X starts i.e., Y depends on X (or is otherwise
26482
+ * required to vanish first per implicit type rules).
26483
+ */
25915
26484
  /**
25916
- * Build deletion levels from state dependencies (reverse topological order).
25917
- * Resources that are depended upon by others are deleted LAST.
26485
+ * Returns true if the executor still has un-started pending nodes —
26486
+ * used to distinguish "SIGINT cancelled real work" from "SIGINT landed
26487
+ * after all nodes already completed" (the latter should not error).
25918
26488
  */
25919
- buildDeletionLevels(deleteIds, state) {
26489
+ hasPending(executor) {
26490
+ for (const node of executor.values()) {
26491
+ if (node.state === "pending")
26492
+ return true;
26493
+ }
26494
+ return false;
26495
+ }
26496
+ buildDeletionDependencies(deleteIds, state) {
25920
26497
  const dependedBy = /* @__PURE__ */ new Map();
25921
- const inDegree = /* @__PURE__ */ new Map();
25922
26498
  for (const id of deleteIds) {
25923
- if (!dependedBy.has(id))
25924
- dependedBy.set(id, /* @__PURE__ */ new Set());
25925
- if (!inDegree.has(id))
25926
- inDegree.set(id, 0);
26499
+ dependedBy.set(id, /* @__PURE__ */ new Set());
25927
26500
  }
25928
26501
  for (const id of deleteIds) {
25929
26502
  const resource = state.resources[id];
@@ -25932,38 +26505,11 @@ var DeployEngine = class _DeployEngine {
25932
26505
  for (const dep of resource.dependencies) {
25933
26506
  if (!deleteIds.has(dep))
25934
26507
  continue;
25935
- if (!dependedBy.has(dep))
25936
- dependedBy.set(dep, /* @__PURE__ */ new Set());
25937
26508
  dependedBy.get(dep).add(id);
25938
- inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
25939
26509
  }
25940
26510
  }
25941
26511
  this.addImplicitDeleteDependencies(deleteIds, state, dependedBy);
25942
- const levels = [];
25943
- let remaining = new Set(deleteIds);
25944
- while (remaining.size > 0) {
25945
- const level = [];
25946
- for (const id of remaining) {
25947
- const dependents = dependedBy.get(id);
25948
- const hasPendingDependents = dependents ? [...dependents].some((d) => remaining.has(d)) : false;
25949
- if (!hasPendingDependents) {
25950
- level.push(id);
25951
- }
25952
- }
25953
- if (level.length === 0) {
25954
- this.logger.warn(
25955
- `Circular dependency detected in delete order, deleting remaining ${remaining.size} resources`
25956
- );
25957
- levels.push([...remaining]);
25958
- break;
25959
- }
25960
- levels.push(level);
25961
- remaining = new Set([...remaining].filter((id) => !level.includes(id)));
25962
- }
25963
- this.logger.debug(
25964
- `Delete order: ${levels.length} levels - ${levels.map((l, i) => `L${i + 1}(${l.length})`).join(", ")}`
25965
- );
25966
- return levels;
26512
+ return dependedBy;
25967
26513
  }
25968
26514
  /**
25969
26515
  * Add implicit delete dependency edges based on resource type relationships.
@@ -25991,7 +26537,7 @@ var DeployEngine = class _DeployEngine {
25991
26537
  const resource = state.resources[id];
25992
26538
  if (!resource)
25993
26539
  continue;
25994
- const mustDeleteAfter = _DeployEngine.IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
26540
+ const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
25995
26541
  if (!mustDeleteAfter)
25996
26542
  continue;
25997
26543
  for (const depType of mustDeleteAfter) {
@@ -26777,27 +27323,6 @@ Acquiring lock for stack ${stackName}...`);
26777
27323
  }
26778
27324
  };
26779
27325
  }
26780
- const implicitDeleteDeps = {
26781
- "AWS::EC2::InternetGateway": ["AWS::EC2::VPCGatewayAttachment"],
26782
- "AWS::Events::EventBus": ["AWS::Events::Rule"],
26783
- "AWS::Athena::WorkGroup": ["AWS::Athena::NamedQuery"],
26784
- "AWS::CloudFront::ResponseHeadersPolicy": ["AWS::CloudFront::Distribution"],
26785
- "AWS::CloudFront::CachePolicy": ["AWS::CloudFront::Distribution"],
26786
- "AWS::CloudFront::OriginAccessControl": ["AWS::CloudFront::Distribution"],
26787
- "AWS::EC2::VPC": [
26788
- "AWS::EC2::Subnet",
26789
- "AWS::EC2::SecurityGroup",
26790
- "AWS::EC2::InternetGateway",
26791
- "AWS::EC2::VPCGatewayAttachment",
26792
- "AWS::EC2::RouteTable"
26793
- ],
26794
- "AWS::EC2::Subnet": ["AWS::EC2::SubnetRouteTableAssociation"],
26795
- "AWS::EC2::RouteTable": ["AWS::EC2::Route", "AWS::EC2::SubnetRouteTableAssociation"],
26796
- "AWS::EC2::SecurityGroup": [
26797
- "AWS::EC2::SecurityGroupIngress",
26798
- "AWS::EC2::SecurityGroupEgress"
26799
- ]
26800
- };
26801
27326
  const typeToLogicalIds = /* @__PURE__ */ new Map();
26802
27327
  for (const [logicalId, resource] of Object.entries(currentState.resources)) {
26803
27328
  const ids = typeToLogicalIds.get(resource.resourceType) ?? [];
@@ -26805,7 +27330,7 @@ Acquiring lock for stack ${stackName}...`);
26805
27330
  typeToLogicalIds.set(resource.resourceType, ids);
26806
27331
  }
26807
27332
  for (const [logicalId, resource] of Object.entries(currentState.resources)) {
26808
- const mustDeleteAfter = implicitDeleteDeps[resource.resourceType];
27333
+ const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
26809
27334
  if (!mustDeleteAfter)
26810
27335
  continue;
26811
27336
  for (const depType of mustDeleteAfter) {
@@ -27029,7 +27554,7 @@ function reorderArgs(argv) {
27029
27554
  }
27030
27555
  async function main() {
27031
27556
  const program = new Command8();
27032
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.1.0");
27557
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.3.0");
27033
27558
  program.addCommand(createBootstrapCommand());
27034
27559
  program.addCommand(createSynthCommand());
27035
27560
  program.addCommand(createDeployCommand());