@go-to-k/cdkd 0.26.0 → 0.28.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/README.md CHANGED
@@ -539,9 +539,6 @@ Both flags accept either form on each invocation:
539
539
  `TYPE` must look like `AWS::Service::Resource`; malformed types are rejected at parse time. `warn < timeout` is enforced both globally and per-type — so `--resource-warn-after AWS::X=10m --resource-timeout AWS::X=5m` is a parse-time error.
540
540
 
541
541
  ```bash
542
- # Bump the per-resource budget to one hour (matches the Custom Resource provider's polling cap)
543
- cdkd deploy --resource-timeout 1h
544
-
545
542
  # Surface "still running" warnings sooner on a fast-feedback dev loop
546
543
  cdkd deploy --resource-warn-after 90s --resource-timeout 10m
547
544
 
@@ -550,19 +547,25 @@ cdkd deploy \
550
547
  --resource-timeout 30m \
551
548
  --resource-timeout AWS::CloudFront::Distribution=1h \
552
549
  --resource-timeout AWS::RDS::DBCluster=1h30m
550
+
551
+ # Force Custom Resources to abort earlier than their 1h self-reported polling cap
552
+ cdkd deploy --resource-timeout AWS::CloudFormation::CustomResource=5m
553
553
  ```
554
554
 
555
555
  ### Why the default is 30m, not 1h
556
556
 
557
- cdkd's Custom Resource provider polls async handlers (`isCompleteHandler` pattern) for up to one hour before giving up. Setting the per-resource timeout to 1h by default would make a single hung Custom Resource hold the whole stack for an hour even though no other resource type ever needs more than a few minutes. A shorter default (`30m`) catches stuck operations faster, and stacks that legitimately rely on long-running Custom Resources opt into the higher budget explicitly with `--resource-timeout 1h`.
557
+ cdkd's Custom Resource provider polls async handlers (`isCompleteHandler` pattern) for up to one hour before giving up. Setting the per-resource timeout to 1h by default would make a single hung non-CR resource hold the whole stack for an hour even though no other resource type ever needs more than a few minutes. The 30m global default catches stuck operations faster.
558
+
559
+ For Custom Resources specifically, the provider self-reports its 1h polling cap to the engine via the `getMinResourceTimeoutMs()` interface — the deploy engine resolves the per-resource budget as `max(provider self-report, --resource-timeout global)`, so CR resources get their full hour automatically without the user having to remember `--resource-timeout 1h`. To force CR to abort earlier than its self-reported cap, pass an explicit per-type override (`--resource-timeout AWS::CloudFormation::CustomResource=5m`). Per-type overrides always win over the provider's self-report — they're the documented escape hatch.
558
560
 
559
- The error message on timeout names the resource, type, region, elapsed time, and operation, and reminds you to re-run with `--resource-timeout 1h` (or higher) for genuinely-long resources:
561
+ The error message on timeout names the resource, type, region, elapsed time, and operation, and reminds you that long-running resources self-report their needed budget when you see CR time out, the cause is genuinely the handler, not too-tight a default:
560
562
 
561
563
  ```text
562
564
  Resource MyBucket (AWS::S3::Bucket) in us-east-1 timed out after 30m during CREATE (elapsed 30m).
563
565
  This may indicate a stuck Cloud Control polling loop, hung Custom Resource, or
564
- slow ENI provisioning. Re-run with --resource-timeout 1h if the resource genuinely
565
- needs more time, or --verbose to see the underlying provider activity.
566
+ slow ENI provisioning. Re-run with --resource-timeout AWS::S3::Bucket=<DURATION>
567
+ to bump the budget for this resource type only, or --verbose to see the
568
+ underlying provider activity.
566
569
  ```
567
570
 
568
571
  Note: `--resource-warn-after` must be less than `--resource-timeout`. Reversed values are rejected at parse time.
package/dist/cli.js CHANGED
@@ -1133,8 +1133,9 @@ var ResourceTimeoutError = class _ResourceTimeoutError extends CdkdError {
1133
1133
  super(
1134
1134
  `Resource ${logicalId} (${resourceType}) in ${region} timed out after ${timeoutLabel} during ${operation} (elapsed ${elapsedLabel}).
1135
1135
  This may indicate a stuck Cloud Control polling loop, hung Custom Resource, or
1136
- slow ENI provisioning. Re-run with --resource-timeout 1h if the resource genuinely
1137
- needs more time, or --verbose to see the underlying provider activity.`,
1136
+ slow ENI provisioning. Re-run with --resource-timeout ${resourceType}=<DURATION>
1137
+ to bump the budget for this resource type only, or --verbose to see the
1138
+ underlying provider activity.`,
1138
1139
  "RESOURCE_TIMEOUT"
1139
1140
  );
1140
1141
  this.logicalId = logicalId;
@@ -7448,6 +7449,23 @@ var CustomResourceProvider = class _CustomResourceProvider {
7448
7449
  logger = getLogger().child("CustomResourceProvider");
7449
7450
  responseBucket;
7450
7451
  responsePrefix;
7452
+ /**
7453
+ * Opt out of the deploy engine's outer transient-error retry loop.
7454
+ *
7455
+ * The loop re-invokes `provider.create()` from the top on a transient
7456
+ * SDK error (IAM propagation, HTTP 429/503, etc.). Each invocation
7457
+ * generates a brand-new RequestId and a brand-new pre-signed S3
7458
+ * response URL via `prepareInvocation()`. If the underlying Lambda has
7459
+ * already started — e.g. an outer retry fired between the placeholder
7460
+ * `PutObject` and the `Invoke`, or after the `Invoke` returned but a
7461
+ * spurious downstream error fired — the first attempt's Lambda
7462
+ * response lands at an S3 key that nobody polls, hanging the deploy
7463
+ * until the polling timeout. The provider already polls with its own
7464
+ * exponential backoff for async patterns (CDK Provider framework with
7465
+ * isCompleteHandler), so an outer retry adds nothing but the multi-
7466
+ * key bug.
7467
+ */
7468
+ disableOuterRetry = true;
7451
7469
  /** Max time to wait for synchronous S3 response after Lambda invocation (30 seconds) */
7452
7470
  SYNC_RESPONSE_TIMEOUT_MS = 3e4;
7453
7471
  /** Max time to wait for async S3 response (CDK Provider framework with isCompleteHandler) */
@@ -7467,6 +7485,22 @@ var CustomResourceProvider = class _CustomResourceProvider {
7467
7485
  this.responsePrefix = config?.responsePrefix ?? "custom-resource-responses";
7468
7486
  this.asyncResponseTimeoutMs = config?.asyncResponseTimeoutMs ?? _CustomResourceProvider.DEFAULT_ASYNC_RESPONSE_TIMEOUT_MS;
7469
7487
  }
7488
+ /**
7489
+ * Self-reported minimum per-resource timeout.
7490
+ *
7491
+ * Custom Resource async invocations (CDK Provider framework with
7492
+ * `isCompleteHandler`) poll for up to `asyncResponseTimeoutMs`
7493
+ * (default 1 hour, matching CDK's `totalTimeout` default). The deploy
7494
+ * engine's global `--resource-timeout` default is 30 minutes, which
7495
+ * would abort a perfectly healthy CR mid-poll. By self-reporting the
7496
+ * polling cap, the engine lifts the deadline to `max(self-report,
7497
+ * global)` for CR resources only; a user-supplied per-type override
7498
+ * (`--resource-timeout AWS::CloudFormation::CustomResource=5m`) still
7499
+ * wins for explicit escape-hatching.
7500
+ */
7501
+ getMinResourceTimeoutMs() {
7502
+ return this.asyncResponseTimeoutMs;
7503
+ }
7470
7504
  /**
7471
7505
  * Set the S3 bucket for custom resource responses
7472
7506
  * Called by ProviderRegistry when state bucket is configured
@@ -8638,6 +8672,33 @@ var IAMRoleProvider = class {
8638
8672
  this.logger.debug(`Added/updated ${tagsToAdd.length} tags on role ${roleName}`);
8639
8673
  }
8640
8674
  }
8675
+ /**
8676
+ * Resolve a single `Fn::GetAtt` attribute for an existing IAM role.
8677
+ *
8678
+ * CloudFormation's `AWS::IAM::Role` exposes `Arn` and `RoleId`; both are
8679
+ * available from the `GetRole` response. See:
8680
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#aws-resource-iam-role-return-values
8681
+ *
8682
+ * Used by `cdkd orphan` to live-fetch attribute values that need to be
8683
+ * substituted into sibling references.
8684
+ */
8685
+ async getAttribute(physicalId, _resourceType, attributeName) {
8686
+ try {
8687
+ const resp = await this.iamClient.send(new GetRoleCommand({ RoleName: physicalId }));
8688
+ switch (attributeName) {
8689
+ case "Arn":
8690
+ return resp.Role?.Arn;
8691
+ case "RoleId":
8692
+ return resp.Role?.RoleId;
8693
+ default:
8694
+ return void 0;
8695
+ }
8696
+ } catch (err) {
8697
+ if (err instanceof NoSuchEntityException)
8698
+ return void 0;
8699
+ throw err;
8700
+ }
8701
+ }
8641
8702
  /**
8642
8703
  * Adopt an existing IAM role into cdkd state.
8643
8704
  *
@@ -10488,17 +10549,35 @@ var S3BucketProvider = class {
10488
10549
  return region || "us-east-1";
10489
10550
  }
10490
10551
  /**
10491
- * Build attributes for an S3 bucket
10552
+ * Build attributes for an S3 bucket.
10553
+ *
10554
+ * Covers every CloudFormation `Fn::GetAtt` return value for
10555
+ * `AWS::S3::Bucket`. All fields are derivable from `bucketName` + region —
10556
+ * no extra AWS API call is needed. See:
10557
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html#aws-properties-s3-bucket-return-values
10492
10558
  */
10493
10559
  async buildAttributes(bucketName) {
10494
10560
  const region = await this.getRegion();
10495
10561
  return {
10496
10562
  Arn: `arn:aws:s3:::${bucketName}`,
10497
10563
  DomainName: `${bucketName}.s3.amazonaws.com`,
10564
+ DualStackDomainName: `${bucketName}.s3.dualstack.${region}.amazonaws.com`,
10498
10565
  RegionalDomainName: `${bucketName}.s3.${region}.amazonaws.com`,
10499
10566
  WebsiteURL: `http://${bucketName}.s3-website-${region}.amazonaws.com`
10500
10567
  };
10501
10568
  }
10569
+ /**
10570
+ * Resolve a single `Fn::GetAtt` attribute for an existing bucket.
10571
+ *
10572
+ * Used by `cdkd orphan` to live-fetch attribute values that need to be
10573
+ * substituted into sibling references. All S3 Bucket attributes are
10574
+ * derivable from bucket name + region, so this avoids the round trip and
10575
+ * reuses the same templating as `buildAttributes`.
10576
+ */
10577
+ async getAttribute(physicalId, _resourceType, attributeName) {
10578
+ const attrs = await this.buildAttributes(physicalId);
10579
+ return attrs[attributeName];
10580
+ }
10502
10581
  /**
10503
10582
  * Apply versioning configuration if specified
10504
10583
  */
@@ -11793,6 +11872,43 @@ var SQSQueueProvider = class {
11793
11872
  return `arn:aws:sqs:unknown:unknown:${queueName}`;
11794
11873
  }
11795
11874
  }
11875
+ /**
11876
+ * Resolve a single `Fn::GetAtt` attribute for an existing SQS queue.
11877
+ *
11878
+ * CloudFormation's `AWS::SQS::Queue` exposes `Arn`, `QueueName` and
11879
+ * `QueueUrl`. The cdkd physicalId is the queue URL; `QueueUrl` and
11880
+ * `QueueName` are derivable from it without an AWS call, while `Arn`
11881
+ * requires `GetQueueAttributes`. See:
11882
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sqs-queues.html#aws-properties-sqs-queues-return-values
11883
+ *
11884
+ * Used by `cdkd orphan` to live-fetch attribute values that need to be
11885
+ * substituted into sibling references.
11886
+ */
11887
+ async getAttribute(physicalId, _resourceType, attributeName) {
11888
+ switch (attributeName) {
11889
+ case "QueueUrl":
11890
+ return physicalId;
11891
+ case "QueueName":
11892
+ return physicalId.substring(physicalId.lastIndexOf("/") + 1);
11893
+ case "Arn": {
11894
+ try {
11895
+ const resp = await this.sqsClient.send(
11896
+ new GetQueueAttributesCommand({
11897
+ QueueUrl: physicalId,
11898
+ AttributeNames: ["QueueArn"]
11899
+ })
11900
+ );
11901
+ return resp.Attributes?.["QueueArn"];
11902
+ } catch (err) {
11903
+ if (err instanceof QueueDoesNotExist)
11904
+ return void 0;
11905
+ throw err;
11906
+ }
11907
+ }
11908
+ default:
11909
+ return void 0;
11910
+ }
11911
+ }
11796
11912
  /**
11797
11913
  * Adopt an existing SQS queue into cdkd state.
11798
11914
  *
@@ -12328,6 +12444,28 @@ var SNSTopicProvider = class {
12328
12444
  );
12329
12445
  }
12330
12446
  }
12447
+ /**
12448
+ * Resolve a single `Fn::GetAtt` attribute for an existing SNS topic.
12449
+ *
12450
+ * CloudFormation's `AWS::SNS::Topic` exposes `TopicName` and `TopicArn`.
12451
+ * The cdkd physicalId is the topic ARN, so both are derivable without
12452
+ * an AWS call. See:
12453
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sns-topic.html#aws-properties-sns-topic-return-values
12454
+ *
12455
+ * Used by `cdkd orphan` to live-fetch attribute values that need to be
12456
+ * substituted into sibling references.
12457
+ */
12458
+ // eslint-disable-next-line @typescript-eslint/require-await -- consistent async signature with other providers
12459
+ async getAttribute(physicalId, _resourceType, attributeName) {
12460
+ switch (attributeName) {
12461
+ case "TopicArn":
12462
+ return physicalId;
12463
+ case "TopicName":
12464
+ return physicalId.split(":").pop();
12465
+ default:
12466
+ return void 0;
12467
+ }
12468
+ }
12331
12469
  /**
12332
12470
  * Adopt an existing SNS topic into cdkd state.
12333
12471
  *
@@ -13309,6 +13447,43 @@ var LambdaFunctionProvider = class {
13309
13447
  }
13310
13448
  return (crc ^ 4294967295) >>> 0;
13311
13449
  }
13450
+ /**
13451
+ * Resolve a single `Fn::GetAtt` attribute for an existing Lambda function.
13452
+ *
13453
+ * CloudFormation's `AWS::Lambda::Function` exposes `Arn`,
13454
+ * `SnapStartResponse.ApplyOn`, and `SnapStartResponse.OptimizationStatus`
13455
+ * as documented at
13456
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#aws-resource-lambda-function-return-values.
13457
+ *
13458
+ * All three live in the same `GetFunction` response (`Configuration.FunctionArn`
13459
+ * and `Configuration.SnapStart.{ApplyOn,OptimizationStatus}`), so a single API
13460
+ * call covers every supported attr. Used by `cdkd orphan` to live-fetch
13461
+ * attribute values that need to be substituted into sibling references.
13462
+ */
13463
+ async getAttribute(physicalId, _resourceType, attributeName) {
13464
+ if (attributeName !== "Arn" && attributeName !== "SnapStartResponse.ApplyOn" && attributeName !== "SnapStartResponse.OptimizationStatus") {
13465
+ return void 0;
13466
+ }
13467
+ try {
13468
+ const resp = await this.lambdaClient.send(
13469
+ new GetFunctionCommand({ FunctionName: physicalId })
13470
+ );
13471
+ switch (attributeName) {
13472
+ case "Arn":
13473
+ return resp.Configuration?.FunctionArn;
13474
+ case "SnapStartResponse.ApplyOn":
13475
+ return resp.Configuration?.SnapStart?.ApplyOn;
13476
+ case "SnapStartResponse.OptimizationStatus":
13477
+ return resp.Configuration?.SnapStart?.OptimizationStatus;
13478
+ default:
13479
+ return void 0;
13480
+ }
13481
+ } catch (err) {
13482
+ if (err instanceof ResourceNotFoundException)
13483
+ return void 0;
13484
+ throw err;
13485
+ }
13486
+ }
13312
13487
  /**
13313
13488
  * Adopt an existing Lambda function into cdkd state.
13314
13489
  *
@@ -13581,6 +13756,7 @@ var LambdaPermissionProvider = class {
13581
13756
  import {
13582
13757
  CreateFunctionUrlConfigCommand,
13583
13758
  DeleteFunctionUrlConfigCommand,
13759
+ GetFunctionUrlConfigCommand as GetFunctionUrlConfigCommand2,
13584
13760
  UpdateFunctionUrlConfigCommand,
13585
13761
  ResourceNotFoundException as ResourceNotFoundException3
13586
13762
  } from "@aws-sdk/client-lambda";
@@ -13708,6 +13884,36 @@ var LambdaUrlProvider = class {
13708
13884
  );
13709
13885
  }
13710
13886
  }
13887
+ /**
13888
+ * Resolve a single `Fn::GetAtt` attribute for an existing Lambda Function
13889
+ * URL.
13890
+ *
13891
+ * CloudFormation's `AWS::Lambda::Url` exposes `FunctionArn` and
13892
+ * `FunctionUrl`. Both come from `GetFunctionUrlConfig`. See:
13893
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-url.html#aws-resource-lambda-url-return-values
13894
+ *
13895
+ * Used by `cdkd orphan` to live-fetch attribute values that need to be
13896
+ * substituted into sibling references.
13897
+ */
13898
+ async getAttribute(physicalId, _resourceType, attributeName) {
13899
+ try {
13900
+ const resp = await this.lambdaClient.send(
13901
+ new GetFunctionUrlConfigCommand2({ FunctionName: physicalId })
13902
+ );
13903
+ switch (attributeName) {
13904
+ case "FunctionArn":
13905
+ return resp.FunctionArn;
13906
+ case "FunctionUrl":
13907
+ return resp.FunctionUrl;
13908
+ default:
13909
+ return void 0;
13910
+ }
13911
+ } catch (err) {
13912
+ if (err instanceof ResourceNotFoundException3)
13913
+ return void 0;
13914
+ throw err;
13915
+ }
13916
+ }
13711
13917
  /**
13712
13918
  * Adopt an existing Lambda Function URL into cdkd state.
13713
13919
  *
@@ -14419,6 +14625,40 @@ var DynamoDBTableProvider = class {
14419
14625
  }
14420
14626
  throw new Error(`Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds`);
14421
14627
  }
14628
+ /**
14629
+ * Resolve a single `Fn::GetAtt` attribute for an existing DynamoDB table.
14630
+ *
14631
+ * CloudFormation's `AWS::DynamoDB::Table` exposes `Arn`, `StreamArn`
14632
+ * (a.k.a. `LatestStreamArn` in the SDK; CFn returns the latest enabled
14633
+ * stream's ARN), and `LatestStreamLabel`. All three are sibling fields on
14634
+ * the same `DescribeTable` response, so a single API call covers every
14635
+ * supported attr. See:
14636
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#aws-resource-dynamodb-table-return-values
14637
+ *
14638
+ * Used by `cdkd orphan` to live-fetch attribute values that need to be
14639
+ * substituted into sibling references.
14640
+ */
14641
+ async getAttribute(physicalId, _resourceType, attributeName) {
14642
+ try {
14643
+ const resp = await this.dynamoDBClient.send(
14644
+ new DescribeTableCommand2({ TableName: physicalId })
14645
+ );
14646
+ switch (attributeName) {
14647
+ case "Arn":
14648
+ return resp.Table?.TableArn;
14649
+ case "StreamArn":
14650
+ return resp.Table?.LatestStreamArn;
14651
+ case "LatestStreamLabel":
14652
+ return resp.Table?.LatestStreamLabel;
14653
+ default:
14654
+ return void 0;
14655
+ }
14656
+ } catch (err) {
14657
+ if (err instanceof ResourceNotFoundException6)
14658
+ return void 0;
14659
+ throw err;
14660
+ }
14661
+ }
14422
14662
  /**
14423
14663
  * Adopt an existing DynamoDB table into cdkd state.
14424
14664
  *
@@ -14706,6 +14946,23 @@ var LogsLogGroupProvider = class {
14706
14946
  return `arn:aws:logs:unknown:unknown:log-group:${logGroupName}:*`;
14707
14947
  }
14708
14948
  }
14949
+ /**
14950
+ * Resolve a single `Fn::GetAtt` attribute for an existing log group.
14951
+ *
14952
+ * CloudFormation's `AWS::Logs::LogGroup` exposes only `Arn`. The ARN is
14953
+ * derivable from the log group name + account + region via the existing
14954
+ * `buildArn` helper. See:
14955
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-logs-loggroup.html#aws-resource-logs-loggroup-return-values
14956
+ *
14957
+ * Used by `cdkd orphan` to live-fetch attribute values that need to be
14958
+ * substituted into sibling references.
14959
+ */
14960
+ async getAttribute(physicalId, _resourceType, attributeName) {
14961
+ if (attributeName !== "Arn") {
14962
+ return void 0;
14963
+ }
14964
+ return this.buildArn(physicalId);
14965
+ }
14709
14966
  /**
14710
14967
  * Adopt an existing CloudWatch Logs log group into cdkd state.
14711
14968
  *
@@ -16743,29 +17000,63 @@ var EC2Provider = class {
16743
17000
  }
16744
17001
  }
16745
17002
  }
17003
+ /**
17004
+ * Resolve a single `Fn::GetAtt` attribute for an `AWS::EC2::VPC`.
17005
+ *
17006
+ * CloudFormation returns `CidrBlock`, `CidrBlockAssociations`,
17007
+ * `DefaultNetworkAcl`, `DefaultSecurityGroup`, and `Ipv6CidrBlocks`. See:
17008
+ * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html#aws-resource-ec2-vpc-return-values
17009
+ *
17010
+ * `DefaultNetworkAcl` and `DefaultSecurityGroup` previously returned wrong
17011
+ * values (DHCP options id and `undefined` respectively); the AWS console
17012
+ * surfaces these the same way as CFn — by filtering the relevant
17013
+ * `Describe*` API on `vpc-id` + the `default` flag.
17014
+ */
16746
17015
  async getVpcAttribute(physicalId, attributeName) {
16747
- if (attributeName === "VpcId")
16748
- return physicalId;
16749
17016
  try {
16750
- const response = await this.ec2Client.send(new DescribeVpcsCommand2({ VpcIds: [physicalId] }));
16751
- const vpc = response.Vpcs?.[0];
16752
- if (!vpc)
16753
- return void 0;
16754
17017
  switch (attributeName) {
16755
- case "CidrBlock":
16756
- return vpc.CidrBlock;
16757
- case "Ipv6CidrBlocks":
16758
- return vpc.Ipv6CidrBlockAssociationSet?.filter(
16759
- (a) => a.Ipv6CidrBlockState?.State === "associated"
16760
- ).map((a) => a.Ipv6CidrBlock) || [];
16761
- case "CidrBlockAssociations":
16762
- return vpc.CidrBlockAssociationSet?.map((a) => a.AssociationId) || [];
16763
- case "DefaultNetworkAcl":
16764
- return vpc.DhcpOptionsId;
16765
- case "DefaultSecurityGroup":
16766
- return void 0;
16767
- default:
16768
- return void 0;
17018
+ case "DefaultNetworkAcl": {
17019
+ const resp = await this.ec2Client.send(
17020
+ new DescribeNetworkAclsCommand({
17021
+ Filters: [
17022
+ { Name: "vpc-id", Values: [physicalId] },
17023
+ { Name: "default", Values: ["true"] }
17024
+ ]
17025
+ })
17026
+ );
17027
+ return resp.NetworkAcls?.[0]?.NetworkAclId;
17028
+ }
17029
+ case "DefaultSecurityGroup": {
17030
+ const resp = await this.ec2Client.send(
17031
+ new DescribeSecurityGroupsCommand2({
17032
+ Filters: [
17033
+ { Name: "vpc-id", Values: [physicalId] },
17034
+ { Name: "group-name", Values: ["default"] }
17035
+ ]
17036
+ })
17037
+ );
17038
+ return resp.SecurityGroups?.[0]?.GroupId;
17039
+ }
17040
+ default: {
17041
+ const response = await this.ec2Client.send(
17042
+ new DescribeVpcsCommand2({ VpcIds: [physicalId] })
17043
+ );
17044
+ const vpc = response.Vpcs?.[0];
17045
+ if (!vpc)
17046
+ return void 0;
17047
+ switch (attributeName) {
17048
+ case "CidrBlock":
17049
+ return vpc.CidrBlock;
17050
+ case "Ipv6CidrBlocks":
17051
+ return vpc.Ipv6CidrBlockAssociationSet?.filter(
17052
+ (a) => a.Ipv6CidrBlockState?.State === "associated"
17053
+ ).map((a) => a.Ipv6CidrBlock) || [];
17054
+ case "CidrBlockAssociations":
17055
+ return vpc.CidrBlockAssociationSet?.map((a) => a.AssociationId) || [];
17056
+ default:
17057
+ return void 0;
17058
+ }
17059
+ }
16769
17060
  }
16770
17061
  } catch {
16771
17062
  return void 0;
@@ -31633,8 +31924,11 @@ var DeployEngine = class {
31633
31924
  const baseLabel = `${verb} ${logicalId} (${resourceType})`;
31634
31925
  renderer.addTask(logicalId, baseLabel);
31635
31926
  const operationKind = change.changeType === "CREATE" ? "CREATE" : change.changeType === "DELETE" ? "DELETE" : "UPDATE";
31927
+ const provider = this.providerRegistry.getProvider(resourceType);
31928
+ const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
31636
31929
  const warnAfterMs = this.options.resourceWarnAfterByType?.[resourceType] ?? this.options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
31637
- const timeoutMs = this.options.resourceTimeoutByType?.[resourceType] ?? this.options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
31930
+ const globalTimeoutMs = this.options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
31931
+ const timeoutMs = this.options.resourceTimeoutByType?.[resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
31638
31932
  try {
31639
31933
  await withResourceDeadline(
31640
31934
  async () => {
@@ -31713,7 +32007,10 @@ var DeployEngine = class {
31713
32007
  const { provider: createProvider, properties: createProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
31714
32008
  const result = await this.withRetry(
31715
32009
  () => createProvider.create(logicalId, resourceType, createProps),
31716
- logicalId
32010
+ logicalId,
32011
+ void 0,
32012
+ void 0,
32013
+ provider
31717
32014
  );
31718
32015
  const dependencies = this.extractAllDependencies(template, logicalId);
31719
32016
  stateResources[logicalId] = {
@@ -31767,7 +32064,10 @@ var DeployEngine = class {
31767
32064
  const { provider: replaceProvider, properties: replaceProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
31768
32065
  const createResult = await this.withRetry(
31769
32066
  () => replaceProvider.create(logicalId, resourceType, replaceProps),
31770
- logicalId
32067
+ logicalId,
32068
+ void 0,
32069
+ void 0,
32070
+ provider
31771
32071
  );
31772
32072
  const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
31773
32073
  if (updateReplacePolicy === "Retain") {
@@ -31818,7 +32118,10 @@ var DeployEngine = class {
31818
32118
  updateProps,
31819
32119
  currentProps
31820
32120
  ),
31821
- logicalId
32121
+ logicalId,
32122
+ void 0,
32123
+ void 0,
32124
+ provider
31822
32125
  );
31823
32126
  } catch (updateError) {
31824
32127
  const msg = updateError instanceof Error ? updateError.message : String(updateError);
@@ -31847,7 +32150,10 @@ var DeployEngine = class {
31847
32150
  const { provider: replProvider, properties: replProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
31848
32151
  const createResult = await this.withRetry(
31849
32152
  () => replProvider.create(logicalId, resourceType, replProps),
31850
- logicalId
32153
+ logicalId,
32154
+ void 0,
32155
+ void 0,
32156
+ provider
31851
32157
  );
31852
32158
  result = {
31853
32159
  physicalId: createResult.physicalId,
@@ -31904,7 +32210,8 @@ var DeployEngine = class {
31904
32210
  logicalId,
31905
32211
  3,
31906
32212
  // fewer retries for DELETE
31907
- 5e3
32213
+ 5e3,
32214
+ provider
31908
32215
  );
31909
32216
  } catch (deleteError) {
31910
32217
  const msg = deleteError instanceof Error ? deleteError.message : String(deleteError);
@@ -32081,8 +32388,18 @@ var DeployEngine = class {
32081
32388
  * Thin wrapper over `withRetry` from ./retry.js that injects this engine's
32082
32389
  * SIGINT-aware interrupt check and logger. The actual backoff schedule
32083
32390
  * lives there.
32391
+ *
32392
+ * When the provider opts out via `disableOuterRetry`, the operation is
32393
+ * invoked exactly once and the retry loop is skipped entirely. The
32394
+ * Custom Resource provider uses this to avoid re-running its `create()`
32395
+ * — each invocation derives a fresh pre-signed S3 URL and RequestId,
32396
+ * so an outer retry leaves the previous attempt's Lambda response
32397
+ * stranded at an S3 key nobody polls.
32084
32398
  */
32085
- async withRetry(operation, logicalId, maxRetries, initialDelayMs) {
32399
+ async withRetry(operation, logicalId, maxRetries, initialDelayMs, provider) {
32400
+ if (provider?.disableOuterRetry) {
32401
+ return operation();
32402
+ }
32086
32403
  return withRetry(operation, logicalId, {
32087
32404
  ...maxRetries !== void 0 && { maxRetries },
32088
32405
  ...initialDelayMs !== void 0 && { initialDelayMs },
@@ -32762,16 +33079,19 @@ Acquiring lock for stack ${stackName}...`);
32762
33079
  logger.warn(`Resource ${logicalId} not found in state, skipping`);
32763
33080
  return;
32764
33081
  }
32765
- const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
32766
- const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? ctx.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
32767
33082
  const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
32768
33083
  renderer.addTask(logicalId, baseLabel);
32769
33084
  try {
32770
33085
  const provider = destroyProviderRegistry.getProvider(resource.resourceType);
33086
+ const providerMinTimeoutMs = provider.getMinResourceTimeoutMs?.() ?? 0;
33087
+ const warnAfterMs = ctx.resourceWarnAfterByType?.[resource.resourceType] ?? ctx.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
33088
+ const globalTimeoutMs = ctx.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
33089
+ const timeoutMs = ctx.resourceTimeoutByType?.[resource.resourceType] ?? Math.max(providerMinTimeoutMs, globalTimeoutMs);
32771
33090
  await withResourceDeadline(
32772
33091
  async () => {
33092
+ const maxAttempts = provider.disableOuterRetry ? 0 : 3;
32773
33093
  let lastDeleteError;
32774
- for (let attempt = 0; attempt <= 3; attempt++) {
33094
+ for (let attempt = 0; attempt <= maxAttempts; attempt++) {
32775
33095
  try {
32776
33096
  await provider.delete(
32777
33097
  logicalId,
@@ -32785,11 +33105,11 @@ Acquiring lock for stack ${stackName}...`);
32785
33105
  lastDeleteError = retryError;
32786
33106
  const msg = retryError instanceof Error ? retryError.message : String(retryError);
32787
33107
  const isRetryable = msg.includes("Too Many Requests") || msg.includes("has dependencies") || msg.includes("can't be deleted since") || msg.includes("DependencyViolation");
32788
- if (!isRetryable || attempt >= 3)
33108
+ if (!isRetryable || attempt >= maxAttempts)
32789
33109
  break;
32790
33110
  const delay = 5e3 * Math.pow(2, attempt);
32791
33111
  logger.debug(
32792
- ` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/3)`
33112
+ ` \u23F3 Retrying delete ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxAttempts})`
32793
33113
  );
32794
33114
  await new Promise((resolve4) => setTimeout(resolve4, delay));
32795
33115
  }
@@ -35316,7 +35636,7 @@ function reorderArgs(argv) {
35316
35636
  }
35317
35637
  async function main() {
35318
35638
  const program = new Command13();
35319
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.26.0");
35639
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.28.0");
35320
35640
  program.addCommand(createBootstrapCommand());
35321
35641
  program.addCommand(createSynthCommand());
35322
35642
  program.addCommand(createListCommand());