@go-to-k/cdkd 0.30.2 → 0.31.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/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
  - **Diff calculation**: Self-implemented resource/property-level diff between desired template and current state
25
25
  - **S3-based state management**: No DynamoDB required, uses S3 conditional writes for locking
26
26
  - **DAG-based parallelization**: Analyze `Ref`/`Fn::GetAtt` dependencies and execute in parallel
27
- - **`--no-wait` for async resources**: Skip the multi-minute wait on CloudFront / RDS / ElastiCache and return as soon as the create call returns (CloudFormation always blocks)
27
+ - **`--no-wait` for async resources**: Skip the multi-minute wait on CloudFront / RDS / ElastiCache / NAT Gateway and return as soon as the create call returns (CloudFormation always blocks)
28
28
 
29
29
  > **Note**: Resource types not covered by either SDK Providers or Cloud Control API cannot be deployed with cdkd. If you encounter an unsupported resource type, deployment will fail with a clear error message.
30
30
 
@@ -372,11 +372,11 @@ cdkd state destroy MyStack --region us-east-1
372
372
 
373
373
  ## `--no-wait`: skip async-resource waits
374
374
 
375
- CloudFront Distributions, RDS Clusters/Instances, and ElastiCache
376
- typically take 3–15 minutes for AWS to fully propagate. By default
377
- cdkd waits for them to reach a ready state — the same behavior as
378
- CloudFormation. Pass `--no-wait` to return as soon as the create call
379
- returns:
375
+ CloudFront Distributions, RDS Clusters/Instances, ElastiCache, and
376
+ NAT Gateways typically take 1–15 minutes for AWS to fully provision.
377
+ By default cdkd waits for them to reach a ready state — the same
378
+ behavior as CloudFormation. Pass `--no-wait` to return as soon as the
379
+ create call returns:
380
380
 
381
381
  ```bash
382
382
  cdkd deploy --no-wait
@@ -386,6 +386,25 @@ The resource is fully functional once AWS finishes the async
386
386
  deployment in the background. CloudFormation has no equivalent — once
387
387
  you submit a stack, you wait for everything.
388
388
 
389
+ NAT Gateway is included as of v0.31. Provisioning typically takes
390
+ 1–2 minutes and is the dominant cost in many VPC stacks; with
391
+ `cdkd deploy --no-wait`, `CreateNatGateway` returns the `NatGatewayId`
392
+ immediately and dependent Routes that only reference the ID can
393
+ proceed against a still-`pending` gateway. AWS continues NAT
394
+ provisioning asynchronously after the deploy returns. Use this only
395
+ when nothing in the deploy flow needs actual NAT-routed egress (e.g.
396
+ no Lambda invoked during deploy that hits the internet).
397
+
398
+ `--no-wait` is **deploy-only**. `cdkd destroy` always waits for NAT
399
+ Gateway to reach `deleted` state — while the gateway is in
400
+ `deleting` AWS keeps the ENI / EIP / route-table associations
401
+ attached, so any concurrent `DeleteSubnet` / `DeleteInternetGateway`
402
+ / `DeleteVpc` returns `DependencyViolation` and the destroy enters a
403
+ retry storm. Other `--no-wait` resources (CloudFront / RDS /
404
+ ElastiCache) don't apply to destroy either — their providers are
405
+ already non-blocking on delete because they're leaves in the destroy
406
+ DAG.
407
+
389
408
  ## Other CLI flags
390
409
 
391
410
  For concurrency knobs (`--concurrency`, `--stack-concurrency`,
@@ -395,6 +414,24 @@ per-resource timeout flags (`--resource-warn-after`,
395
414
  and the rationale for the 30m default), see
396
415
  **[docs/cli-reference.md](docs/cli-reference.md)**.
397
416
 
417
+ ## Exit codes
418
+
419
+ cdkd commands distinguish three outcomes via the process exit code so
420
+ CI / bench scripts can react without grepping log output:
421
+
422
+ | Exit | Meaning |
423
+ |------|---------|
424
+ | `0` | Success — command completed and no resources are in an error state |
425
+ | `1` | Command-level failure — auth error, bad arguments, synth crash, unhandled exception |
426
+ | `2` | **Partial failure** — work completed but one or more resources failed (state.json is preserved, re-running typically resolves it) |
427
+
428
+ Exit `2` is currently emitted by `cdkd destroy` and `cdkd state
429
+ destroy` when one or more per-resource deletes fail. The summary line
430
+ also switches from `✓ Stack X destroyed` to `⚠ Stack X partially
431
+ destroyed (...). State preserved — re-run 'cdkd destroy' / 'cdkd
432
+ state destroy' to clean up.` so the visual marker matches the exit
433
+ code.
434
+
398
435
  ## Example
399
436
 
400
437
  ```typescript
package/dist/cli.js CHANGED
@@ -606,6 +606,10 @@ function validateResourceTimeouts(opts) {
606
606
  }
607
607
  }
608
608
  }
609
+ var noWaitOption = new Option(
610
+ "--no-wait",
611
+ "Skip waiting for async resources to stabilize (CloudFront, RDS, ElastiCache, NAT Gateway)"
612
+ );
609
613
  var deployOptions = [
610
614
  new Option("--concurrency <number>", "Maximum concurrent resource operations").default(10).argParser((value) => parseInt(value, 10)),
611
615
  new Option("--stack-concurrency <number>", "Maximum concurrent stack deployments").default(4).argParser((value) => parseInt(value, 10)),
@@ -617,7 +621,7 @@ var deployOptions = [
617
621
  new Option("--dry-run", "Show changes without applying").default(false),
618
622
  new Option("--skip-assets", "Skip asset publishing").default(false),
619
623
  new Option("--no-rollback", "Skip rollback on deployment failure"),
620
- new Option("--no-wait", "Skip waiting for async resources (CloudFront, RDS, etc.)"),
624
+ noWaitOption,
621
625
  new Option(
622
626
  "-e, --exclusively",
623
627
  "Only deploy requested stacks, do not include dependencies"
@@ -1166,6 +1170,14 @@ var DependencyError = class _DependencyError extends CdkdError {
1166
1170
  Object.setPrototypeOf(this, _DependencyError.prototype);
1167
1171
  }
1168
1172
  };
1173
+ var PartialFailureError = class _PartialFailureError extends CdkdError {
1174
+ exitCode = 2;
1175
+ constructor(message, cause) {
1176
+ super(message, "PARTIAL_FAILURE", cause);
1177
+ this.name = "PartialFailureError";
1178
+ Object.setPrototypeOf(this, _PartialFailureError.prototype);
1179
+ }
1180
+ };
1169
1181
  function isCdkdError(error) {
1170
1182
  return error instanceof CdkdError;
1171
1183
  }
@@ -1189,7 +1201,8 @@ function handleError(error) {
1189
1201
  if (error instanceof Error && error.stack) {
1190
1202
  logger.debug("Stack trace:", error.stack);
1191
1203
  }
1192
- process.exit(1);
1204
+ const exitCode = error instanceof PartialFailureError ? error.exitCode : 1;
1205
+ process.exit(exitCode);
1193
1206
  }
1194
1207
  function withErrorHandling(fn) {
1195
1208
  return async (...args) => {
@@ -16790,6 +16803,11 @@ import {
16790
16803
  DeleteInternetGatewayCommand,
16791
16804
  AttachInternetGatewayCommand,
16792
16805
  DetachInternetGatewayCommand,
16806
+ CreateNatGatewayCommand,
16807
+ DeleteNatGatewayCommand,
16808
+ DescribeNatGatewaysCommand,
16809
+ waitUntilNatGatewayAvailable,
16810
+ waitUntilNatGatewayDeleted,
16793
16811
  CreateRouteTableCommand,
16794
16812
  DeleteRouteTableCommand,
16795
16813
  CreateRouteCommand,
@@ -16835,6 +16853,20 @@ var EC2Provider = class {
16835
16853
  ],
16836
16854
  ["AWS::EC2::InternetGateway", /* @__PURE__ */ new Set(["Tags"])],
16837
16855
  ["AWS::EC2::VPCGatewayAttachment", /* @__PURE__ */ new Set(["VpcId", "InternetGatewayId"])],
16856
+ [
16857
+ "AWS::EC2::NatGateway",
16858
+ /* @__PURE__ */ new Set([
16859
+ "AllocationId",
16860
+ "SubnetId",
16861
+ "ConnectivityType",
16862
+ "PrivateIpAddress",
16863
+ "SecondaryAllocationIds",
16864
+ "SecondaryPrivateIpAddresses",
16865
+ "SecondaryPrivateIpAddressCount",
16866
+ "MaxDrainDurationSeconds",
16867
+ "Tags"
16868
+ ])
16869
+ ],
16838
16870
  ["AWS::EC2::RouteTable", /* @__PURE__ */ new Set(["VpcId", "Tags"])],
16839
16871
  [
16840
16872
  "AWS::EC2::Route",
@@ -16922,6 +16954,8 @@ var EC2Provider = class {
16922
16954
  return this.createInternetGateway(logicalId, resourceType, properties);
16923
16955
  case "AWS::EC2::VPCGatewayAttachment":
16924
16956
  return this.createVpcGatewayAttachment(logicalId, resourceType, properties);
16957
+ case "AWS::EC2::NatGateway":
16958
+ return this.createNatGateway(logicalId, resourceType, properties);
16925
16959
  case "AWS::EC2::RouteTable":
16926
16960
  return this.createRouteTable(logicalId, resourceType, properties);
16927
16961
  case "AWS::EC2::Route":
@@ -16958,6 +16992,8 @@ var EC2Provider = class {
16958
16992
  return this.updateInternetGateway(logicalId, physicalId);
16959
16993
  case "AWS::EC2::VPCGatewayAttachment":
16960
16994
  return this.updateVpcGatewayAttachment(logicalId, physicalId);
16995
+ case "AWS::EC2::NatGateway":
16996
+ return this.updateNatGateway(logicalId, physicalId);
16961
16997
  case "AWS::EC2::RouteTable":
16962
16998
  return this.updateRouteTable(logicalId, physicalId);
16963
16999
  case "AWS::EC2::Route":
@@ -17011,6 +17047,8 @@ var EC2Provider = class {
17011
17047
  return this.deleteInternetGateway(logicalId, physicalId, resourceType, context);
17012
17048
  case "AWS::EC2::VPCGatewayAttachment":
17013
17049
  return this.deleteVpcGatewayAttachment(logicalId, physicalId, resourceType, context);
17050
+ case "AWS::EC2::NatGateway":
17051
+ return this.deleteNatGateway(logicalId, physicalId, resourceType, context);
17014
17052
  case "AWS::EC2::RouteTable":
17015
17053
  return this.deleteRouteTable(logicalId, physicalId, resourceType, context);
17016
17054
  case "AWS::EC2::Route":
@@ -17579,6 +17617,118 @@ var EC2Provider = class {
17579
17617
  );
17580
17618
  }
17581
17619
  }
17620
+ // ─── AWS::EC2::NatGateway ─────────────────────────────────────────
17621
+ //
17622
+ // CloudFormation parity: by default we wait for the new NAT gateway to
17623
+ // reach `available` state before marking the resource created. NAT
17624
+ // provisioning takes ~1–2 minutes (often the longest single step in a
17625
+ // VPC stack). Pass `--no-wait` to skip the wait — `CreateNatGateway`
17626
+ // returns the `NatGatewayId` immediately so dependent Routes /
17627
+ // Subnets that only need the ID can proceed against a still-`pending`
17628
+ // gateway. Anything that requires actual NAT-routed egress (e.g. a
17629
+ // Lambda invocation that hits the internet during deploy) must not
17630
+ // rely on the gateway being live; with `--no-wait`, AWS continues
17631
+ // provisioning asynchronously after the deploy returns.
17632
+ async createNatGateway(logicalId, resourceType, properties) {
17633
+ this.logger.debug(`Creating NatGateway ${logicalId}`);
17634
+ const subnetId = properties["SubnetId"];
17635
+ if (!subnetId) {
17636
+ throw new ProvisioningError(
17637
+ `SubnetId is required for NatGateway ${logicalId}`,
17638
+ resourceType,
17639
+ logicalId
17640
+ );
17641
+ }
17642
+ try {
17643
+ const response = await this.ec2Client.send(
17644
+ new CreateNatGatewayCommand({
17645
+ SubnetId: subnetId,
17646
+ AllocationId: properties["AllocationId"],
17647
+ ConnectivityType: properties["ConnectivityType"] ?? void 0,
17648
+ PrivateIpAddress: properties["PrivateIpAddress"],
17649
+ SecondaryAllocationIds: properties["SecondaryAllocationIds"],
17650
+ SecondaryPrivateIpAddresses: properties["SecondaryPrivateIpAddresses"],
17651
+ SecondaryPrivateIpAddressCount: properties["SecondaryPrivateIpAddressCount"]
17652
+ })
17653
+ );
17654
+ const natGatewayId = response.NatGateway.NatGatewayId;
17655
+ await this.applyTags(natGatewayId, properties, logicalId);
17656
+ if (process.env["CDKD_NO_WAIT"] !== "true") {
17657
+ this.logger.debug(`Waiting for NatGateway ${natGatewayId} to reach available state...`);
17658
+ await waitUntilNatGatewayAvailable(
17659
+ // 15-min cap matches AWS's documented worst case for NAT
17660
+ // provisioning. Per-resource `--resource-timeout` (default
17661
+ // 30 min) still bounds the outer call as a backstop.
17662
+ { client: this.ec2Client, maxWaitTime: 15 * 60 },
17663
+ { NatGatewayIds: [natGatewayId] }
17664
+ );
17665
+ this.logger.debug(`NatGateway ${natGatewayId} is available`);
17666
+ } else {
17667
+ this.logger.debug(
17668
+ `NatGateway ${natGatewayId} created (skipping available-state wait per --no-wait)`
17669
+ );
17670
+ }
17671
+ this.logger.debug(`Successfully created NatGateway ${logicalId}: ${natGatewayId}`);
17672
+ return {
17673
+ physicalId: natGatewayId,
17674
+ attributes: {
17675
+ NatGatewayId: natGatewayId
17676
+ }
17677
+ };
17678
+ } catch (error) {
17679
+ const cause = error instanceof Error ? error : void 0;
17680
+ throw new ProvisioningError(
17681
+ `Failed to create NatGateway ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
17682
+ resourceType,
17683
+ logicalId,
17684
+ void 0,
17685
+ cause
17686
+ );
17687
+ }
17688
+ }
17689
+ updateNatGateway(logicalId, physicalId) {
17690
+ this.logger.debug(`Updating NatGateway ${logicalId}: ${physicalId} (no-op)`);
17691
+ return Promise.resolve({ physicalId, wasReplaced: false });
17692
+ }
17693
+ async deleteNatGateway(logicalId, physicalId, resourceType, context) {
17694
+ this.logger.debug(`Deleting NatGateway ${logicalId}: ${physicalId}`);
17695
+ try {
17696
+ await this.ec2Client.send(new DeleteNatGatewayCommand({ NatGatewayId: physicalId }));
17697
+ } catch (error) {
17698
+ if (this.isNotFoundError(error)) {
17699
+ const clientRegion = await this.ec2Client.config.region();
17700
+ assertRegionMatch(
17701
+ clientRegion,
17702
+ context?.expectedRegion,
17703
+ resourceType,
17704
+ logicalId,
17705
+ physicalId
17706
+ );
17707
+ this.logger.debug(`NatGateway ${physicalId} does not exist, skipping deletion`);
17708
+ return;
17709
+ }
17710
+ const cause = error instanceof Error ? error : void 0;
17711
+ throw new ProvisioningError(
17712
+ `Failed to delete NatGateway ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
17713
+ resourceType,
17714
+ logicalId,
17715
+ physicalId,
17716
+ cause
17717
+ );
17718
+ }
17719
+ this.logger.debug(`Waiting for NatGateway ${physicalId} to reach deleted state...`);
17720
+ try {
17721
+ await waitUntilNatGatewayDeleted(
17722
+ { client: this.ec2Client, maxWaitTime: 15 * 60 },
17723
+ { NatGatewayIds: [physicalId] }
17724
+ );
17725
+ } catch (error) {
17726
+ this.logger.warn(
17727
+ `Wait for NatGateway ${physicalId} deletion did not complete cleanly: ${error instanceof Error ? error.message : String(error)} \u2014 proceeding with downstream delete steps`
17728
+ );
17729
+ }
17730
+ this.logger.debug(`Successfully deleted NatGateway ${logicalId}`);
17731
+ }
17582
17732
  // ─── AWS::EC2::RouteTable ─────────────────────────────────────────
17583
17733
  async createRouteTable(logicalId, resourceType, properties) {
17584
17734
  this.logger.debug(`Creating RouteTable ${logicalId}`);
@@ -18788,6 +18938,15 @@ var EC2Provider = class {
18788
18938
  const sg = resp.SecurityGroups?.[0];
18789
18939
  return sg?.GroupId ? { physicalId: sg.GroupId, attributes: {} } : null;
18790
18940
  }
18941
+ case "AWS::EC2::NatGateway": {
18942
+ const resp = await this.ec2Client.send(
18943
+ new DescribeNatGatewaysCommand({
18944
+ Filter: [{ Name: `tag:${CDK_PATH_TAG}`, Values: [input.cdkPath] }]
18945
+ })
18946
+ );
18947
+ const gw = resp.NatGateways?.find((g) => g.State !== "deleted" && g.State !== "deleting");
18948
+ return gw?.NatGatewayId ? { physicalId: gw.NatGatewayId, attributes: {} } : null;
18949
+ }
18791
18950
  default:
18792
18951
  return null;
18793
18952
  }
@@ -18816,6 +18975,13 @@ var EC2Provider = class {
18816
18975
  );
18817
18976
  return resp.SecurityGroups?.[0] ? { physicalId, attributes: {} } : null;
18818
18977
  }
18978
+ case "AWS::EC2::NatGateway": {
18979
+ const resp = await this.ec2Client.send(
18980
+ new DescribeNatGatewaysCommand({ NatGatewayIds: [physicalId] })
18981
+ );
18982
+ const gw = resp.NatGateways?.find((g) => g.State !== "deleted" && g.State !== "deleting");
18983
+ return gw ? { physicalId, attributes: {} } : null;
18984
+ }
18819
18985
  default:
18820
18986
  return null;
18821
18987
  }
@@ -31139,6 +31305,7 @@ function registerAllProviders(registry) {
31139
31305
  registry.register("AWS::EC2::Subnet", ec2Provider);
31140
31306
  registry.register("AWS::EC2::InternetGateway", ec2Provider);
31141
31307
  registry.register("AWS::EC2::VPCGatewayAttachment", ec2Provider);
31308
+ registry.register("AWS::EC2::NatGateway", ec2Provider);
31142
31309
  registry.register("AWS::EC2::RouteTable", ec2Provider);
31143
31310
  registry.register("AWS::EC2::Route", ec2Provider);
31144
31311
  registry.register("AWS::EC2::SubnetRouteTableAssociation", ec2Provider);
@@ -33390,10 +33557,17 @@ Acquiring lock for stack ${stackName}...`);
33390
33557
  } else {
33391
33558
  logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
33392
33559
  }
33393
- logger.info(
33394
- `
33560
+ if (result.errorCount === 0) {
33561
+ logger.info(
33562
+ `
33395
33563
  \u2713 Stack ${stackName} destroyed (${result.deletedCount} deleted, ${result.errorCount} errors)`
33396
- );
33564
+ );
33565
+ } else {
33566
+ logger.warn(
33567
+ `
33568
+ \u26A0 Stack ${stackName} partially destroyed (${result.deletedCount} deleted, ${result.errorCount} errors). State preserved \u2014 re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`
33569
+ );
33570
+ }
33397
33571
  } finally {
33398
33572
  renderer.stop();
33399
33573
  logger.debug("Releasing lock...");
@@ -33513,6 +33687,7 @@ async function destroyCommand(stackArgs, options) {
33513
33687
  arr.push(ref);
33514
33688
  stateRefsByName.set(ref.stackName, arr);
33515
33689
  }
33690
+ let totalErrors = 0;
33516
33691
  for (const stackName of stackNames) {
33517
33692
  logger.info(`
33518
33693
  Preparing to destroy stack: ${stackName}`);
@@ -33543,7 +33718,7 @@ Preparing to destroy stack: ${stackName}`);
33543
33718
  logger.warn(`No state found for stack ${stackName}, skipping`);
33544
33719
  continue;
33545
33720
  }
33546
- await runDestroyForStack(stackName, stateResult.state, {
33721
+ const result = await runDestroyForStack(stackName, stateResult.state, {
33547
33722
  stateBackend,
33548
33723
  lockManager,
33549
33724
  providerRegistry,
@@ -33565,6 +33740,12 @@ Preparing to destroy stack: ${stackName}`);
33565
33740
  resourceTimeoutByType: options.resourceTimeout.perTypeMs
33566
33741
  }
33567
33742
  });
33743
+ totalErrors += result.errorCount;
33744
+ }
33745
+ if (totalErrors > 0) {
33746
+ throw new PartialFailureError(
33747
+ `Destroy completed with ${totalErrors} resource error(s). State preserved \u2014 inspect 'cdkd state show <stack>' and re-run 'cdkd destroy' to retry.`
33748
+ );
33568
33749
  }
33569
33750
  } finally {
33570
33751
  awsClients.destroy();
@@ -35262,8 +35443,8 @@ Preparing to destroy stack: ${stackName}${ref.region ? ` (${ref.region})` : ""}`
35262
35443
  }
35263
35444
  }
35264
35445
  if (totalErrors > 0) {
35265
- throw new Error(
35266
- `Destroy completed with ${totalErrors} resource error(s). Inspect 'cdkd state show <stack>' and re-run.`
35446
+ throw new PartialFailureError(
35447
+ `Destroy completed with ${totalErrors} resource error(s). State preserved \u2014 inspect 'cdkd state show <stack>' and re-run 'cdkd state destroy' to retry.`
35267
35448
  );
35268
35449
  }
35269
35450
  } finally {
@@ -36172,7 +36353,7 @@ function reorderArgs(argv) {
36172
36353
  }
36173
36354
  async function main() {
36174
36355
  const program = new Command13();
36175
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.30.2");
36356
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.31.1");
36176
36357
  program.addCommand(createBootstrapCommand());
36177
36358
  program.addCommand(createSynthCommand());
36178
36359
  program.addCommand(createListCommand());