@go-to-k/cdkd 0.254.1 → 0.256.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.
@@ -8917,8 +8917,37 @@ function collectReferencedParameterNames(template) {
8917
8917
  var IntrinsicFunctionResolver = class {
8918
8918
  logger = getLogger().child("IntrinsicFunctionResolver");
8919
8919
  resolverRegion;
8920
- constructor(region) {
8920
+ strictGetAtt;
8921
+ /**
8922
+ * Number of unknown-attribute resolutions that fell back to the physical
8923
+ * ID (the warn path) since construction / the last
8924
+ * {@link resetPhysicalIdFallbackCount}. Counting semantics (issue #1111
8925
+ * item 3): the deploy engine resets this at the start of each `deploy()`
8926
+ * run AND again right before provisioning on the change path — the diff
8927
+ * phase resolves through this same counted resolver, so without the
8928
+ * second reset a fallback site on a to-be-updated resource would count
8929
+ * once during diff and again during provisioning (~2x distinct sites in
8930
+ * the summary). Each distinct fallback site therefore counts once per
8931
+ * run: the surfaced summary covers provisioning + output resolution on
8932
+ * the change path, diff + output resolution on the no-change path, and
8933
+ * diff only under --dry-run. Instance-scoped, so parallel stacks (each
8934
+ * with their own engine + resolver) never share a counter; for the same
8935
+ * reason a nested-stack CHILD engine's fallbacks are counted by the
8936
+ * child's own resolver and are NOT aggregated into the parent stack's
8937
+ * deploy summary.
8938
+ */
8939
+ physicalIdFallbackCount = 0;
8940
+ constructor(region, options) {
8921
8941
  this.resolverRegion = region || process.env["AWS_REGION"] || "us-east-1";
8942
+ this.strictGetAtt = options?.strictGetAtt ?? false;
8943
+ }
8944
+ /** Unknown-attribute physicalId fallbacks recorded since the last reset. */
8945
+ getPhysicalIdFallbackCount() {
8946
+ return this.physicalIdFallbackCount;
8947
+ }
8948
+ /** Reset the per-run fallback counter (called at the start of each deploy). */
8949
+ resetPhysicalIdFallbackCount() {
8950
+ this.physicalIdFallbackCount = 0;
8922
8951
  }
8923
8952
  /**
8924
8953
  * Resolve parameter values from template Parameters section
@@ -9210,19 +9239,19 @@ var IntrinsicFunctionResolver = class {
9210
9239
  if (resourceType === "AWS::DynamoDB::Table" || resourceType === "AWS::DynamoDB::GlobalTable") switch (attributeName) {
9211
9240
  case "Arn": return `arn:${partition}:dynamodb:${region}:${accountId}:table/${physicalId}`;
9212
9241
  case "StreamArn": return;
9213
- default: return physicalId;
9242
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9214
9243
  }
9215
9244
  if (resourceType === "AWS::S3::Bucket") switch (attributeName) {
9216
9245
  case "Arn": return `arn:${partition}:s3:::${physicalId}`;
9217
9246
  case "DomainName": return `${physicalId}.s3.amazonaws.com`;
9218
9247
  case "RegionalDomainName": return `${physicalId}.s3.${region}.amazonaws.com`;
9219
9248
  case "WebsiteURL": return `http://${physicalId}.s3-website-${region}.amazonaws.com`;
9220
- default: return physicalId;
9249
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9221
9250
  }
9222
9251
  if (resourceType === "AWS::IAM::Role") switch (attributeName) {
9223
9252
  case "Arn": return `arn:${partition}:iam::${accountId}:role/${physicalId}`;
9224
9253
  case "RoleId": return;
9225
- default: return physicalId;
9254
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9226
9255
  }
9227
9256
  if (resourceType === "AWS::EC2::VPC") switch (attributeName) {
9228
9257
  case "VpcId": return physicalId;
@@ -9252,37 +9281,38 @@ var IntrinsicFunctionResolver = class {
9252
9281
  return [];
9253
9282
  }
9254
9283
  case "DefaultSecurityGroup": return resource.attributes?.["DefaultSecurityGroup"] || physicalId;
9255
- default: return physicalId;
9284
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9256
9285
  }
9257
9286
  if (resourceType === "AWS::IAM::Policy") switch (attributeName) {
9258
9287
  case "Arn": return `arn:${partition}:iam::${accountId}:policy/${physicalId}`;
9259
9288
  case "PolicyId": return;
9260
- default: return physicalId;
9289
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9261
9290
  }
9262
9291
  if (resourceType === "AWS::IAM::User") switch (attributeName) {
9263
9292
  case "Arn": return `arn:${partition}:iam::${accountId}:user/${physicalId}`;
9264
- default: return physicalId;
9293
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9265
9294
  }
9266
9295
  if (resourceType === "AWS::IAM::Group") switch (attributeName) {
9267
9296
  case "Arn": return `arn:${partition}:iam::${accountId}:group/${physicalId}`;
9268
- default: return physicalId;
9297
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9269
9298
  }
9270
9299
  if (resourceType === "AWS::IAM::InstanceProfile") switch (attributeName) {
9271
9300
  case "Arn": return `arn:${partition}:iam::${accountId}:instance-profile/${physicalId}`;
9272
- default: return physicalId;
9301
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9273
9302
  }
9274
9303
  if (resourceType === "AWS::KMS::Key") switch (attributeName) {
9275
9304
  case "Arn": return `arn:${partition}:kms:${region}:${accountId}:key/${physicalId}`;
9276
9305
  case "KeyId": return physicalId;
9277
- default: return physicalId;
9306
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9278
9307
  }
9279
9308
  if (resourceType === "AWS::Cognito::UserPool") switch (attributeName) {
9280
9309
  case "Arn": return `arn:${partition}:cognito-idp:${region}:${accountId}:userpool/${physicalId}`;
9281
- default: return physicalId;
9310
+ case "UserPoolId": return physicalId;
9311
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9282
9312
  }
9283
9313
  if (resourceType === "AWS::Kinesis::Stream") switch (attributeName) {
9284
9314
  case "Arn": return `arn:${partition}:kinesis:${region}:${accountId}:stream/${physicalId}`;
9285
- default: return physicalId;
9315
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9286
9316
  }
9287
9317
  if (resourceType === "AWS::Events::Rule") switch (attributeName) {
9288
9318
  case "Arn": {
@@ -9292,36 +9322,36 @@ var IntrinsicFunctionResolver = class {
9292
9322
  const busName = bus.startsWith("arn:") ? bus.split("/").pop() || "" : bus;
9293
9323
  return busName ? `arn:${partition}:events:${region}:${accountId}:rule/${busName}/${physicalId}` : `arn:${partition}:events:${region}:${accountId}:rule/${physicalId}`;
9294
9324
  }
9295
- default: return physicalId;
9325
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9296
9326
  }
9297
9327
  if (resourceType === "AWS::Events::EventBus") switch (attributeName) {
9298
9328
  case "Arn": return `arn:${partition}:events:${region}:${accountId}:event-bus/${physicalId}`;
9299
9329
  case "Name": return physicalId;
9300
- default: return physicalId;
9330
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9301
9331
  }
9302
9332
  if (resourceType === "AWS::EFS::FileSystem") switch (attributeName) {
9303
9333
  case "Arn": return `arn:${partition}:elasticfilesystem:${region}:${accountId}:file-system/${physicalId}`;
9304
9334
  case "FileSystemId": return physicalId;
9305
- default: return physicalId;
9335
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9306
9336
  }
9307
9337
  if (resourceType === "AWS::KinesisFirehose::DeliveryStream") switch (attributeName) {
9308
9338
  case "Arn": return `arn:${partition}:firehose:${region}:${accountId}:deliverystream/${physicalId}`;
9309
- default: return physicalId;
9339
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9310
9340
  }
9311
9341
  if (resourceType === "AWS::CodeBuild::Project") switch (attributeName) {
9312
9342
  case "Arn": return `arn:${partition}:codebuild:${region}:${accountId}:project/${physicalId}`;
9313
- default: return physicalId;
9343
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9314
9344
  }
9315
9345
  if (resourceType === "AWS::CloudTrail::Trail") switch (attributeName) {
9316
9346
  case "Arn":
9317
9347
  if (physicalId.startsWith("arn:")) return physicalId;
9318
9348
  return `arn:${partition}:cloudtrail:${region}:${accountId}:trail/${physicalId}`;
9319
- default: return physicalId;
9349
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9320
9350
  }
9321
9351
  if (resourceType === "AWS::AppSync::GraphQLApi") switch (attributeName) {
9322
9352
  case "Arn": return `arn:${partition}:appsync:${region}:${accountId}:apis/${physicalId}`;
9323
9353
  case "ApiId": return physicalId;
9324
- default: return physicalId;
9354
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9325
9355
  }
9326
9356
  if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
9327
9357
  case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
@@ -9333,38 +9363,38 @@ var IntrinsicFunctionResolver = class {
9333
9363
  this.logger.warn(`Failed to fetch HostedZoneId for namespace ${physicalId}: ${error instanceof Error ? error.message : String(error)}`);
9334
9364
  return;
9335
9365
  }
9336
- default: return physicalId;
9366
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9337
9367
  }
9338
9368
  if (resourceType === "AWS::ServiceDiscovery::Service") switch (attributeName) {
9339
9369
  case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:service/${physicalId}`;
9340
9370
  case "Id": return physicalId;
9341
- default: return physicalId;
9371
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9342
9372
  }
9343
9373
  if (resourceType === "AWS::CloudWatch::Alarm") switch (attributeName) {
9344
9374
  case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
9345
- default: return physicalId;
9375
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9346
9376
  }
9347
9377
  if (resourceType === "AWS::CloudWatch::CompositeAlarm") switch (attributeName) {
9348
9378
  case "Arn": return `arn:${partition}:cloudwatch:${region}:${accountId}:alarm:${physicalId}`;
9349
- default: return physicalId;
9379
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9350
9380
  }
9351
9381
  if (resourceType === "AWS::RDS::DBInstance" || resourceType === "AWS::DocDB::DBInstance" || resourceType === "AWS::Neptune::DBInstance") switch (attributeName) {
9352
9382
  case "DBInstanceArn":
9353
9383
  case "Arn": return `arn:${partition}:rds:${region}:${accountId}:db:${physicalId}`;
9354
- default: return physicalId;
9384
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9355
9385
  }
9356
9386
  if (resourceType === "AWS::RDS::DBCluster" || resourceType === "AWS::DocDB::DBCluster" || resourceType === "AWS::Neptune::DBCluster") switch (attributeName) {
9357
9387
  case "DBClusterArn":
9358
9388
  case "Arn": return `arn:${partition}:rds:${region}:${accountId}:cluster:${physicalId}`;
9359
- default: return physicalId;
9389
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9360
9390
  }
9361
9391
  if (resourceType === "AWS::S3Express::DirectoryBucket") switch (attributeName) {
9362
9392
  case "Arn": return `arn:${partition}:s3express:${region}:${accountId}:bucket/${physicalId}`;
9363
- default: return physicalId;
9393
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9364
9394
  }
9365
9395
  if (resourceType === "AWS::Lambda::Function") switch (attributeName) {
9366
9396
  case "Arn": return `arn:${partition}:lambda:${region}:${accountId}:function:${physicalId}`;
9367
- default: return physicalId;
9397
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9368
9398
  }
9369
9399
  if (resourceType === "AWS::SQS::Queue") {
9370
9400
  let queueName = physicalId;
@@ -9376,25 +9406,26 @@ var IntrinsicFunctionResolver = class {
9376
9406
  case "Arn": return `arn:${partition}:sqs:${region}:${accountId}:${queueName}`;
9377
9407
  case "QueueUrl": return physicalId;
9378
9408
  case "QueueName": return queueName;
9379
- default: return physicalId;
9409
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9380
9410
  }
9381
9411
  }
9382
9412
  if (resourceType === "AWS::SNS::Topic") switch (attributeName) {
9383
9413
  case "TopicArn": return `arn:${partition}:sns:${region}:${accountId}:${physicalId}`;
9384
- default: return physicalId;
9414
+ case "TopicName": return physicalId;
9415
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9385
9416
  }
9386
9417
  if (resourceType === "AWS::Logs::LogGroup") switch (attributeName) {
9387
9418
  case "Arn": return `arn:${partition}:logs:${region}:${accountId}:log-group:${physicalId}:*`;
9388
- default: return physicalId;
9419
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9389
9420
  }
9390
9421
  if (resourceType === "AWS::ECR::Repository") switch (attributeName) {
9391
9422
  case "Arn": return `arn:${partition}:ecr:${region}:${accountId}:repository/${physicalId}`;
9392
9423
  case "RepositoryUri": return `${accountId}.dkr.ecr.${region}.amazonaws.com/${physicalId}`;
9393
- default: return physicalId;
9424
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9394
9425
  }
9395
9426
  if (resourceType === "AWS::ECS::Cluster") switch (attributeName) {
9396
9427
  case "Arn": return `arn:${partition}:ecs:${region}:${accountId}:cluster/${physicalId}`;
9397
- default: return physicalId;
9428
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9398
9429
  }
9399
9430
  if (resourceType === "AWS::ECS::Service") switch (attributeName) {
9400
9431
  case "Name": {
@@ -9407,18 +9438,30 @@ var IntrinsicFunctionResolver = class {
9407
9438
  if (pipeIdx >= 0) return physicalId.substring(pipeIdx + 1);
9408
9439
  return physicalId;
9409
9440
  }
9410
- default: return physicalId;
9441
+ case "ServiceArn": {
9442
+ const pipeIdx = physicalId.indexOf("|");
9443
+ if (pipeIdx < 0) return physicalId;
9444
+ const left = physicalId.substring(0, pipeIdx);
9445
+ if (left.includes(":service/")) return left;
9446
+ const clusterIdx = left.indexOf(":cluster/");
9447
+ if (clusterIdx < 0) return physicalId;
9448
+ const clusterName = left.substring(clusterIdx + 9);
9449
+ const serviceName = physicalId.substring(pipeIdx + 1);
9450
+ return `${left.substring(0, clusterIdx)}:service/${clusterName}/${serviceName}`;
9451
+ }
9452
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9411
9453
  }
9412
9454
  if (resourceType === "AWS::EC2::SecurityGroup") switch (attributeName) {
9413
9455
  case "GroupId": return physicalId;
9414
9456
  case "VpcId": return;
9415
- default: return physicalId;
9457
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9416
9458
  }
9417
9459
  if (resourceType === "AWS::EC2::Subnet") switch (attributeName) {
9418
9460
  case "SubnetId": return physicalId;
9419
- default: return physicalId;
9461
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9420
9462
  }
9421
9463
  if (resourceType === "AWS::EC2::Instance") switch (attributeName) {
9464
+ case "InstanceId": return physicalId;
9422
9465
  case "PrivateIp":
9423
9466
  case "PublicIp":
9424
9467
  case "PrivateDnsName":
@@ -9457,7 +9500,7 @@ var IntrinsicFunctionResolver = class {
9457
9500
  }
9458
9501
  return physicalId;
9459
9502
  }
9460
- default: return physicalId;
9503
+ default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9461
9504
  }
9462
9505
  if (resourceType === "AWS::EC2::LaunchTemplate") {
9463
9506
  if (attributeName === "LatestVersionNumber" || attributeName === "DefaultVersionNumber") {
@@ -9470,11 +9513,44 @@ var IntrinsicFunctionResolver = class {
9470
9513
  }
9471
9514
  return attributeName === "LatestVersionNumber" ? "$Latest" : "$Default";
9472
9515
  }
9473
- return physicalId;
9516
+ if (attributeName === "LaunchTemplateId") return physicalId;
9517
+ return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9474
9518
  }
9519
+ return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
9520
+ }
9521
+ /**
9522
+ * Shared unknown-attribute physicalId fallback (issues #1106 / #1111).
9523
+ *
9524
+ * Applied by BOTH the final unknown-type fallback of
9525
+ * {@link constructAttribute} and every per-type handler's
9526
+ * `default:`-for-an-unknown-attribute branch:
9527
+ *
9528
+ * - An attribute name ending in `Arn` whose fallback value is not
9529
+ * ARN-shaped (or ending in `Url` whose fallback is not an http(s) URL)
9530
+ * cannot be what CloudFormation would return — hard-fail. The #1103
9531
+ * incident shipped four resource NAMES into stack Outputs where ARNs
9532
+ * were requested, with a green deploy. A physicalId that already IS an
9533
+ * ARN / URL passes the shape check and remains a valid fallback.
9534
+ * - Under `--strict-getatt`, EVERY unknown-attribute fallback (any
9535
+ * suffix) is a hard error.
9536
+ * - Otherwise: `Alias` / `Endpoint` and every other suffix keep the
9537
+ * warn-and-return behavior (an alias or endpoint is
9538
+ * shape-indistinguishable from a plain name, so a hard-fail there
9539
+ * would risk failing correct deploys — false positives are
9540
+ * unacceptable). Each warn-and-return increments the per-run fallback
9541
+ * counter surfaced in the deploy summary.
9542
+ *
9543
+ * A `return physicalId` that is the CORRECT value for a KNOWN attribute
9544
+ * (e.g. `AWS::KMS::Key.KeyId`, `AWS::SNS::Topic.TopicName`) must NOT
9545
+ * route through this helper — those are explicit `case`s in the
9546
+ * per-type handlers.
9547
+ */
9548
+ guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId) {
9475
9549
  const expectsArnShape = attributeName.endsWith("Arn") && !physicalId.startsWith("arn:");
9476
9550
  const expectsUrlShape = attributeName.endsWith("Url") && !/^https?:\/\//.test(physicalId);
9477
9551
  if (expectsArnShape || expectsUrlShape) throw new Error(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}] for ${resourceType}: attributes are not enriched for this resource type, and the physical ID fallback "${physicalId}" is not ${expectsArnShape ? "an ARN (arn:...)" : "a URL (http(s)://...)"}. CloudFormation would return a different value here, so falling back to the physical ID would silently produce a wrong value (e.g. in stack Outputs). Avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${attributeName}.`);
9552
+ if (this.strictGetAtt) throw new Error(`Cannot resolve Fn::GetAtt [${logicalId}, ${attributeName}] for ${resourceType}: attributes are not enriched for this resource type, and --strict-getatt rejects the physical ID fallback "${physicalId}" (which may not be the value CloudFormation would return). Drop --strict-getatt to fall back with a warning, avoid this Fn::GetAtt, or file an issue at https://github.com/go-to-k/cdkd/issues so cdkd can enrich ${resourceType}.${attributeName}.`);
9553
+ this.physicalIdFallbackCount++;
9478
9554
  this.logger.warn(`Unknown attribute ${attributeName} for resource type ${resourceType}, returning physical ID`);
9479
9555
  return physicalId;
9480
9556
  }
@@ -10878,7 +10954,7 @@ var CloudControlProvider = class {
10878
10954
  this.logger.debug(`Deleting resource ${logicalId} (${resourceType}), physical ID: ${physicalId}`);
10879
10955
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
10880
10956
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
10881
- const { ASGProvider } = await import("./asg-provider-BgcH5ymP.js").then((n) => n.n);
10957
+ const { ASGProvider } = await import("./asg-provider-DbIC4hWG.js").then((n) => n.n);
10882
10958
  await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
10883
10959
  return;
10884
10960
  }
@@ -14677,6 +14753,319 @@ function isCustomResource(resourceType) {
14677
14753
  return resourceType.startsWith("Custom::") || resourceType === "AWS::CloudFormation::CustomResource";
14678
14754
  }
14679
14755
 
14756
+ //#endregion
14757
+ //#region src/deployment/retryable-errors.ts
14758
+ /**
14759
+ * Patterns that mark an AWS error as a transient/retryable failure.
14760
+ * Each entry is a substring match against the error message; all of these
14761
+ * are situations where the same call typically succeeds after a short delay
14762
+ * because of eventual consistency or just-created-dependency propagation.
14763
+ */
14764
+ const RETRYABLE_ERROR_MESSAGE_PATTERNS = [
14765
+ "cannot be assumed",
14766
+ "Firehose is unable to assume role",
14767
+ "is unable to assume provided role",
14768
+ "role defined for the function",
14769
+ "not authorized to perform",
14770
+ "execution role",
14771
+ "trust policy",
14772
+ "Role validation failed",
14773
+ "does not have required permissions",
14774
+ "Trusted Entity",
14775
+ "currently in the following state: Pending",
14776
+ "has dependencies and cannot be deleted",
14777
+ "can't be deleted since it has",
14778
+ "DependencyViolation",
14779
+ "does not exist",
14780
+ "Schema is currently being altered",
14781
+ "Invalid principal in policy",
14782
+ "Policy Error: PrincipalNotFound",
14783
+ "Invalid value for the parameter Policy",
14784
+ "required permissions for: ENHANCED_MONITORING",
14785
+ "Caught ServiceAccessDeniedException",
14786
+ "permissions required to assume the role",
14787
+ "authorized to assume the provided role",
14788
+ "conflicting conditional operation",
14789
+ "scheduled for deletion",
14790
+ "Cannot access stream",
14791
+ "Please ensure the role can perform",
14792
+ "KMS key is invalid for CreateGrant",
14793
+ "Policy contains a statement with one or more invalid principals",
14794
+ "Invalid IAM Instance Profile",
14795
+ "Invalid InstanceProfile",
14796
+ "Failed to authorize instance profile",
14797
+ "Could not deliver test message",
14798
+ "wait 60 seconds",
14799
+ "concurrent update operation",
14800
+ "because it is in use",
14801
+ "Rate exceeded"
14802
+ ];
14803
+ /**
14804
+ * HTTP status codes that always indicate a transient failure worth retrying.
14805
+ * 429 = Too Many Requests (throttle), 503 = Service Unavailable.
14806
+ */
14807
+ const RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
14808
+ /**
14809
+ * AWS SDK v3 canonical throttling error names. Mirrors
14810
+ * `@aws-sdk/service-error-classification`'s `THROTTLING_ERROR_CODES` — any
14811
+ * error (or wrapped cause) whose `name` is one of these is a transient rate-
14812
+ * limit rejection worth retrying with backoff. Detecting by NAME is more
14813
+ * robust than by HTTP status because most AWS throttles surface as HTTP 400
14814
+ * (not 429) with the throttling signal carried only in the error code / name
14815
+ * (e.g. SSM `ThrottlingException` for the `Rate exceeded` message).
14816
+ */
14817
+ const THROTTLING_ERROR_NAMES = /* @__PURE__ */ new Set([
14818
+ "BandwidthLimitExceeded",
14819
+ "EC2ThrottledException",
14820
+ "LimitExceededException",
14821
+ "PriorRequestNotComplete",
14822
+ "ProvisionedThroughputExceededException",
14823
+ "RequestLimitExceeded",
14824
+ "RequestThrottled",
14825
+ "RequestThrottledException",
14826
+ "SlowDown",
14827
+ "ThrottledException",
14828
+ "Throttling",
14829
+ "ThrottlingException",
14830
+ "TooManyRequestsException",
14831
+ "TransactionInProgressException"
14832
+ ]);
14833
+ /**
14834
+ * Walk the error + its `.cause` chain (bounded) looking for a rate-limit
14835
+ * signal — either an AWS SDK v3 throttling error `name`
14836
+ * ({@link THROTTLING_ERROR_NAMES}) or a retryable HTTP status
14837
+ * ({@link RETRYABLE_HTTP_STATUS_CODES}) on `$metadata`.
14838
+ *
14839
+ * cdkd wraps the original AWS error in a `ProvisioningError`, so the signal is
14840
+ * typically one cause-link deep; the bounded walk also tolerates SDK errors
14841
+ * that nest a `$response`/cause without exploding on a cyclic chain.
14842
+ *
14843
+ * BOTH signals are checked at EVERY depth. An earlier version checked the name
14844
+ * to depth 5 but the HTTP status only at depths 0 and 1, so a 429 nested two
14845
+ * links deep was missed. This is the single shared cause-walk: the read-only
14846
+ * import tag walk (`src/provisioning/import-tag-walk.ts`) composes its own
14847
+ * classifier on top of this function rather than re-implementing the traversal,
14848
+ * so the two cannot drift apart again.
14849
+ */
14850
+ function isThrottlingError(error) {
14851
+ let current = error;
14852
+ for (let depth = 0; depth < 5 && current != null; depth++) {
14853
+ const name = current.name;
14854
+ if (typeof name === "string" && THROTTLING_ERROR_NAMES.has(name)) return true;
14855
+ const status = current.$metadata?.httpStatusCode;
14856
+ if (status !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(status)) return true;
14857
+ current = current.cause;
14858
+ }
14859
+ return false;
14860
+ }
14861
+ /**
14862
+ * Determine whether an AWS error should be retried.
14863
+ *
14864
+ * Checks (in order):
14865
+ * 1. Rate-limit signal on the error or any wrapped cause — throttling error
14866
+ * `name` or retryable HTTP status (most AWS throttles are HTTP 400, not
14867
+ * 429, so the name check carries most of the weight). See
14868
+ * {@link isThrottlingError}.
14869
+ * 2. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
14870
+ */
14871
+ function isRetryableTransientError(error, message) {
14872
+ if (isThrottlingError(error)) return true;
14873
+ return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
14874
+ }
14875
+
14876
+ //#endregion
14877
+ //#region src/deployment/retry.ts
14878
+ /**
14879
+ * Retry helper for resource provisioning operations that hit transient
14880
+ * AWS eventual-consistency errors (IAM propagation, Lambda Pending state,
14881
+ * dependency violations, etc.).
14882
+ *
14883
+ * Extracted from DeployEngine so the backoff schedule can be unit-tested
14884
+ * in isolation. The retryable-error classifier itself lives in
14885
+ * `./retryable-errors.ts`.
14886
+ */
14887
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
14888
+ /**
14889
+ * Run `operation`, retrying transient failures with exponential backoff
14890
+ * capped at `maxDelayMs`.
14891
+ *
14892
+ * Backoff at the defaults (initialDelayMs=1_000, maxDelayMs=8_000, maxRetries=8):
14893
+ * 1s -> 2s -> 4s -> 8s -> 8s -> 8s -> 8s -> 8s (cumulative 47s)
14894
+ *
14895
+ * Non-retryable errors are rethrown immediately. The transient-error
14896
+ * classifier is `isRetryableTransientError` from ./retryable-errors.ts.
14897
+ */
14898
+ async function withRetry(operation, logicalId, opts = {}) {
14899
+ const maxRetries = opts.maxRetries ?? 8;
14900
+ const initialDelayMs = opts.initialDelayMs ?? 1e3;
14901
+ const maxDelayMs = opts.maxDelayMs ?? 8e3;
14902
+ const sleep = opts.sleep ?? defaultSleep;
14903
+ let lastError;
14904
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
14905
+ return await operation();
14906
+ } catch (error) {
14907
+ lastError = error;
14908
+ const message = error instanceof Error ? error.message : String(error);
14909
+ if (!(opts.isRetryable ? opts.isRetryable(message, error) : isRetryableTransientError(error, message)) || attempt >= maxRetries) throw error;
14910
+ const delay = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
14911
+ opts.logger?.debug(` ⏳ Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxRetries}) - ${message}`);
14912
+ for (let waited = 0; waited < delay; waited += 1e3) {
14913
+ if (opts.isInterrupted?.()) throw opts.onInterrupted ? opts.onInterrupted() : /* @__PURE__ */ new Error("Interrupted");
14914
+ await sleep(Math.min(1e3, delay - waited));
14915
+ }
14916
+ }
14917
+ throw lastError;
14918
+ }
14919
+
14920
+ //#endregion
14921
+ //#region src/provisioning/import-tag-walk.ts
14922
+ /**
14923
+ * Shared, throttle-tolerant `aws:cdk:path` tag walk for `ResourceProvider.import`.
14924
+ *
14925
+ * Providers that adopt a resource without an explicit name property fall back
14926
+ * to step 3 of the lookup order documented in `./import-helpers.ts`: enumerate
14927
+ * the service's `List*`/`Describe*` pages, then issue ONE per-candidate read
14928
+ * (`DescribeX` / `ListTagsForResource`) to obtain the tag set — the list
14929
+ * summaries usually do not carry tags. That is an inherent **N+1** read
14930
+ * pattern: an account with many resources of the type produces one API call
14931
+ * per candidate in a tight loop, which is exactly the shape AWS rate-limits.
14932
+ *
14933
+ * Every provider previously hand-rolled this loop with NO backoff, so a single
14934
+ * throttled `Describe*` aborted the whole `cdkd import` run. This helper
14935
+ * centralises the loop and wraps BOTH the list page fetch and the per-candidate
14936
+ * describe in the deploy engine's `withRetry` with exponential backoff.
14937
+ *
14938
+ * ## Why a narrower classifier than `isRetryableTransientError`
14939
+ *
14940
+ * The deploy engine's classifier is tuned for the WRITE path: it also treats
14941
+ * eventual-consistency phrasings like `does not exist` / `not authorized to
14942
+ * perform` as transient, because a just-created dependency legitimately needs a
14943
+ * moment to propagate. On a read-only import walk those messages mean the
14944
+ * opposite — the candidate really is gone, or the caller's credentials really
14945
+ * lack the permission — and retrying them burns the full backoff budget per
14946
+ * candidate before surfacing the true error.
14947
+ *
14948
+ * So the walk retries throttling ONLY, reusing the deploy engine's throttle
14949
+ * tables verbatim ({@link THROTTLING_ERROR_NAMES} /
14950
+ * {@link RETRYABLE_HTTP_STATUS_CODES}) rather than maintaining a second copy.
14951
+ *
14952
+ * ## Batching
14953
+ *
14954
+ * Batched tag reads are deliberately NOT modelled here: the services this
14955
+ * helper currently serves (EMR `DescribeCluster`, DocDB
14956
+ * `ListTagsForResource`) expose only single-resource reads. A service that
14957
+ * genuinely offers a batch API (e.g. CodeCommit `BatchGetRepositories`) can
14958
+ * satisfy several candidates from one call inside its own `describe` callback,
14959
+ * or bypass the helper entirely.
14960
+ */
14961
+ /** Max number of retries after the first attempt, per API call in the walk. */
14962
+ const DEFAULT_MAX_RETRIES = 5;
14963
+ /** Initial backoff; each retry doubles it up to {@link DEFAULT_MAX_DELAY_MS}. */
14964
+ const DEFAULT_INITIAL_DELAY_MS = 500;
14965
+ /** Cap for the per-retry delay (0.5s -> 1s -> 2s -> 4s -> 5s, ~12.5s total). */
14966
+ const DEFAULT_MAX_DELAY_MS = 5e3;
14967
+ /**
14968
+ * Wall-clock ceiling for the WHOLE walk (all pages + all candidates), not just
14969
+ * one call. Backoff is per-call, so without this a sustained throttle against a
14970
+ * large account degrades into `(pages + candidates) x ~12.5s` of near-silent
14971
+ * retrying — ~42 minutes for 200 candidates, with no way to tell a slow walk
14972
+ * from a hung one. 10 minutes is generous for a healthy walk (a 200-candidate
14973
+ * DocDB account completes in seconds when AWS is not throttling) and bounds the
14974
+ * pathological case to something a user will wait through.
14975
+ */
14976
+ const DEFAULT_MAX_WALK_MS = 10 * 6e4;
14977
+ /**
14978
+ * Ceiling on pages fetched. A service that returns a non-advancing pagination
14979
+ * token (a bug, or a marker cdkd echoes back wrongly) would otherwise spin
14980
+ * forever. This is now the shared path every migrated provider runs on, so the
14981
+ * guard belongs here rather than in each caller.
14982
+ */
14983
+ const DEFAULT_MAX_PAGES = 1e3;
14984
+ /**
14985
+ * Whether an error hit during the read-only tag walk is a rate-limit rejection
14986
+ * worth backing off on.
14987
+ *
14988
+ * Delegates the error + `.cause` chain traversal to the deploy engine's
14989
+ * {@link isThrottlingError} (throttling error names + retryable HTTP statuses,
14990
+ * at every cause depth) and adds the canonical `Rate exceeded` message, which
14991
+ * several services return with an HTTP 400 and a service-specific error name.
14992
+ *
14993
+ * This is deliberately NARROWER than `isRetryableTransientError`: that
14994
+ * classifier also treats write-path eventual-consistency phrasings (`does not
14995
+ * exist`, `not authorized to perform`) as transient, because a just-created
14996
+ * dependency legitimately needs a moment to propagate. On a read-only walk
14997
+ * those mean the candidate really is gone or the credentials really lack the
14998
+ * permission, and retrying them burns the full backoff budget per candidate
14999
+ * before surfacing the true error.
15000
+ *
15001
+ * Exported for direct unit testing and for providers whose tag walk cannot use
15002
+ * {@link importTagWalk} verbatim.
15003
+ */
15004
+ function isThrottlingLikeError(error, message) {
15005
+ return isThrottlingError(error) || message.includes("Rate exceeded");
15006
+ }
15007
+ /** Thrown when the walk exceeds its wall-clock budget or page ceiling. */
15008
+ var ImportTagWalkLimitError = class extends Error {
15009
+ constructor(message) {
15010
+ super(message);
15011
+ this.name = "ImportTagWalkLimitError";
15012
+ }
15013
+ };
15014
+ /**
15015
+ * Paginate `listPage`, `describe` each candidate, and return the first one
15016
+ * whose tags carry the requested `aws:cdk:path`. Returns `null` when no
15017
+ * candidate matches (or when `cdkPath` is empty).
15018
+ *
15019
+ * Both callbacks are individually retried with exponential backoff on
15020
+ * throttling errors, so one rate-limited call mid-walk no longer aborts the
15021
+ * whole import.
15022
+ */
15023
+ async function importTagWalk(options) {
15024
+ const { cdkPath, listPage, describe, tagsOf } = options;
15025
+ if (!cdkPath) return null;
15026
+ const logicalId = options.logicalId ?? "import";
15027
+ const logger = options.retry?.logger ?? getLogger();
15028
+ const maxWalkMs = options.retry?.maxWalkMs ?? DEFAULT_MAX_WALK_MS;
15029
+ const maxPages = options.retry?.maxPages ?? DEFAULT_MAX_PAGES;
15030
+ const retryOpts = {
15031
+ maxRetries: options.retry?.maxRetries ?? DEFAULT_MAX_RETRIES,
15032
+ initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS,
15033
+ maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS,
15034
+ isRetryable: (message, error) => isThrottlingLikeError(error, message),
15035
+ logger,
15036
+ ...options.retry?.isInterrupted && { isInterrupted: options.retry.isInterrupted },
15037
+ ...options.retry?.onInterrupted && { onInterrupted: options.retry.onInterrupted },
15038
+ ...options.retry?.sleep && { sleep: options.retry.sleep }
15039
+ };
15040
+ const startedAt = Date.now();
15041
+ const assertWithinBudget = (stage) => {
15042
+ const elapsed = Date.now() - startedAt;
15043
+ if (elapsed >= maxWalkMs) throw new ImportTagWalkLimitError(`Timed out looking up ${logicalId} by its aws:cdk:path tag after ${Math.round(elapsed / 1e3)}s (limit ${Math.round(maxWalkMs / 1e3)}s, while ${stage}). AWS is likely throttling the lookup; retry, or pass an explicit physical id with --resource ${logicalId}=<physicalId>.`);
15044
+ };
15045
+ let marker;
15046
+ let pages = 0;
15047
+ do {
15048
+ assertWithinBudget("listing candidates");
15049
+ if (++pages > maxPages) throw new ImportTagWalkLimitError(`Gave up looking up ${logicalId} by its aws:cdk:path tag after ${maxPages} pages (the service may be returning a non-advancing pagination token). Pass an explicit physical id with --resource ${logicalId}=<physicalId>.`);
15050
+ const currentMarker = marker;
15051
+ const page = await withRetry(() => listPage(currentMarker), logicalId, retryOpts);
15052
+ for (const summary of page.items ?? []) {
15053
+ assertWithinBudget("reading candidate tags");
15054
+ const detail = await withRetry(() => describe(summary), logicalId, retryOpts);
15055
+ if (detail === void 0) {
15056
+ logger.debug(` ↷ Skipped an ${logicalId} import candidate: its detail lookup returned no result`);
15057
+ continue;
15058
+ }
15059
+ if (matchesCdkPath(tagsOf(detail, summary), cdkPath)) return {
15060
+ summary,
15061
+ detail
15062
+ };
15063
+ }
15064
+ marker = page.nextMarker;
15065
+ } while (marker);
15066
+ return null;
15067
+ }
15068
+
14680
15069
  //#endregion
14681
15070
  //#region src/provisioning/providers/iam-role-provider.ts
14682
15071
  /**
@@ -15226,25 +15615,32 @@ var IAMRoleProvider = class {
15226
15615
  if (err instanceof NoSuchEntityException) return null;
15227
15616
  throw err;
15228
15617
  }
15229
- if (!input.cdkPath) return null;
15230
- let marker;
15231
- do {
15232
- const list = await this.iamClient.send(new ListRolesCommand({ ...marker && { Marker: marker } }));
15233
- for (const role of list.Roles ?? []) {
15234
- if (!role.RoleName) continue;
15618
+ const match = await importTagWalk({
15619
+ cdkPath: input.cdkPath,
15620
+ logicalId: input.logicalId,
15621
+ listPage: async (marker) => {
15622
+ const list = await this.iamClient.send(new ListRolesCommand({ ...marker && { Marker: marker } }));
15623
+ return {
15624
+ items: list.Roles,
15625
+ nextMarker: list.IsTruncated ? list.Marker : void 0
15626
+ };
15627
+ },
15628
+ describe: async (role) => {
15629
+ if (!role.RoleName) return void 0;
15235
15630
  try {
15236
- if (matchesCdkPath((await this.iamClient.send(new ListRoleTagsCommand({ RoleName: role.RoleName }))).Tags, input.cdkPath)) return {
15237
- physicalId: role.RoleName,
15238
- attributes: {}
15239
- };
15631
+ return await this.iamClient.send(new ListRoleTagsCommand({ RoleName: role.RoleName }));
15240
15632
  } catch (err) {
15241
- if (err instanceof NoSuchEntityException) continue;
15633
+ if (err instanceof NoSuchEntityException) return void 0;
15242
15634
  throw err;
15243
15635
  }
15244
- }
15245
- marker = list.IsTruncated ? list.Marker : void 0;
15246
- } while (marker);
15247
- return null;
15636
+ },
15637
+ tagsOf: (tags) => tags.Tags
15638
+ });
15639
+ if (!match) return null;
15640
+ return {
15641
+ physicalId: match.summary.RoleName,
15642
+ attributes: {}
15643
+ };
15248
15644
  }
15249
15645
  };
15250
15646
  /**
@@ -15740,170 +16136,6 @@ function computeImplicitDeleteEdges(resources) {
15740
16136
  return edges;
15741
16137
  }
15742
16138
 
15743
- //#endregion
15744
- //#region src/deployment/retryable-errors.ts
15745
- /**
15746
- * Patterns that mark an AWS error as a transient/retryable failure.
15747
- * Each entry is a substring match against the error message; all of these
15748
- * are situations where the same call typically succeeds after a short delay
15749
- * because of eventual consistency or just-created-dependency propagation.
15750
- */
15751
- const RETRYABLE_ERROR_MESSAGE_PATTERNS = [
15752
- "cannot be assumed",
15753
- "Firehose is unable to assume role",
15754
- "is unable to assume provided role",
15755
- "role defined for the function",
15756
- "not authorized to perform",
15757
- "execution role",
15758
- "trust policy",
15759
- "Role validation failed",
15760
- "does not have required permissions",
15761
- "Trusted Entity",
15762
- "currently in the following state: Pending",
15763
- "has dependencies and cannot be deleted",
15764
- "can't be deleted since it has",
15765
- "DependencyViolation",
15766
- "does not exist",
15767
- "Schema is currently being altered",
15768
- "Invalid principal in policy",
15769
- "Policy Error: PrincipalNotFound",
15770
- "Invalid value for the parameter Policy",
15771
- "required permissions for: ENHANCED_MONITORING",
15772
- "Caught ServiceAccessDeniedException",
15773
- "permissions required to assume the role",
15774
- "authorized to assume the provided role",
15775
- "conflicting conditional operation",
15776
- "scheduled for deletion",
15777
- "Cannot access stream",
15778
- "Please ensure the role can perform",
15779
- "KMS key is invalid for CreateGrant",
15780
- "Policy contains a statement with one or more invalid principals",
15781
- "Invalid IAM Instance Profile",
15782
- "Invalid InstanceProfile",
15783
- "Failed to authorize instance profile",
15784
- "Could not deliver test message",
15785
- "wait 60 seconds",
15786
- "concurrent update operation",
15787
- "because it is in use",
15788
- "Rate exceeded"
15789
- ];
15790
- /**
15791
- * HTTP status codes that always indicate a transient failure worth retrying.
15792
- * 429 = Too Many Requests (throttle), 503 = Service Unavailable.
15793
- */
15794
- const RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
15795
- /**
15796
- * AWS SDK v3 canonical throttling error names. Mirrors
15797
- * `@aws-sdk/service-error-classification`'s `THROTTLING_ERROR_CODES` — any
15798
- * error (or wrapped cause) whose `name` is one of these is a transient rate-
15799
- * limit rejection worth retrying with backoff. Detecting by NAME is more
15800
- * robust than by HTTP status because most AWS throttles surface as HTTP 400
15801
- * (not 429) with the throttling signal carried only in the error code / name
15802
- * (e.g. SSM `ThrottlingException` for the `Rate exceeded` message).
15803
- */
15804
- const THROTTLING_ERROR_NAMES = /* @__PURE__ */ new Set([
15805
- "BandwidthLimitExceeded",
15806
- "EC2ThrottledException",
15807
- "LimitExceededException",
15808
- "PriorRequestNotComplete",
15809
- "ProvisionedThroughputExceededException",
15810
- "RequestLimitExceeded",
15811
- "RequestThrottled",
15812
- "RequestThrottledException",
15813
- "SlowDown",
15814
- "ThrottledException",
15815
- "Throttling",
15816
- "ThrottlingException",
15817
- "TooManyRequestsException",
15818
- "TransactionInProgressException"
15819
- ]);
15820
- /**
15821
- * Walk the error + its `.cause` chain (bounded) looking for a rate-limit
15822
- * signal — either an AWS SDK v3 throttling error `name`
15823
- * ({@link THROTTLING_ERROR_NAMES}) or a retryable HTTP status
15824
- * ({@link RETRYABLE_HTTP_STATUS_CODES}) on `$metadata`.
15825
- *
15826
- * cdkd wraps the original AWS error in a `ProvisioningError`, so the signal is
15827
- * typically one cause-link deep; the bounded walk also tolerates SDK errors
15828
- * that nest a `$response`/cause without exploding on a cyclic chain.
15829
- *
15830
- * BOTH signals are checked at EVERY depth. An earlier version checked the name
15831
- * to depth 5 but the HTTP status only at depths 0 and 1, so a 429 nested two
15832
- * links deep was missed. This is the single shared cause-walk: the read-only
15833
- * import tag walk (`src/provisioning/import-tag-walk.ts`) composes its own
15834
- * classifier on top of this function rather than re-implementing the traversal,
15835
- * so the two cannot drift apart again.
15836
- */
15837
- function isThrottlingError(error) {
15838
- let current = error;
15839
- for (let depth = 0; depth < 5 && current != null; depth++) {
15840
- const name = current.name;
15841
- if (typeof name === "string" && THROTTLING_ERROR_NAMES.has(name)) return true;
15842
- const status = current.$metadata?.httpStatusCode;
15843
- if (status !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(status)) return true;
15844
- current = current.cause;
15845
- }
15846
- return false;
15847
- }
15848
- /**
15849
- * Determine whether an AWS error should be retried.
15850
- *
15851
- * Checks (in order):
15852
- * 1. Rate-limit signal on the error or any wrapped cause — throttling error
15853
- * `name` or retryable HTTP status (most AWS throttles are HTTP 400, not
15854
- * 429, so the name check carries most of the weight). See
15855
- * {@link isThrottlingError}.
15856
- * 2. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
15857
- */
15858
- function isRetryableTransientError(error, message) {
15859
- if (isThrottlingError(error)) return true;
15860
- return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
15861
- }
15862
-
15863
- //#endregion
15864
- //#region src/deployment/retry.ts
15865
- /**
15866
- * Retry helper for resource provisioning operations that hit transient
15867
- * AWS eventual-consistency errors (IAM propagation, Lambda Pending state,
15868
- * dependency violations, etc.).
15869
- *
15870
- * Extracted from DeployEngine so the backoff schedule can be unit-tested
15871
- * in isolation. The retryable-error classifier itself lives in
15872
- * `./retryable-errors.ts`.
15873
- */
15874
- const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15875
- /**
15876
- * Run `operation`, retrying transient failures with exponential backoff
15877
- * capped at `maxDelayMs`.
15878
- *
15879
- * Backoff at the defaults (initialDelayMs=1_000, maxDelayMs=8_000, maxRetries=8):
15880
- * 1s -> 2s -> 4s -> 8s -> 8s -> 8s -> 8s -> 8s (cumulative 47s)
15881
- *
15882
- * Non-retryable errors are rethrown immediately. The transient-error
15883
- * classifier is `isRetryableTransientError` from ./retryable-errors.ts.
15884
- */
15885
- async function withRetry(operation, logicalId, opts = {}) {
15886
- const maxRetries = opts.maxRetries ?? 8;
15887
- const initialDelayMs = opts.initialDelayMs ?? 1e3;
15888
- const maxDelayMs = opts.maxDelayMs ?? 8e3;
15889
- const sleep = opts.sleep ?? defaultSleep;
15890
- let lastError;
15891
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
15892
- return await operation();
15893
- } catch (error) {
15894
- lastError = error;
15895
- const message = error instanceof Error ? error.message : String(error);
15896
- if (!(opts.isRetryable ? opts.isRetryable(message, error) : isRetryableTransientError(error, message)) || attempt >= maxRetries) throw error;
15897
- const delay = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
15898
- opts.logger?.debug(` ⏳ Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxRetries}) - ${message}`);
15899
- for (let waited = 0; waited < delay; waited += 1e3) {
15900
- if (opts.isInterrupted?.()) throw opts.onInterrupted ? opts.onInterrupted() : /* @__PURE__ */ new Error("Interrupted");
15901
- await sleep(Math.min(1e3, delay - waited));
15902
- }
15903
- }
15904
- throw lastError;
15905
- }
15906
-
15907
16139
  //#endregion
15908
16140
  //#region src/deployment/resource-deadline.ts
15909
16141
  /**
@@ -16124,7 +16356,7 @@ var DeployEngine = class {
16124
16356
  this.options = options;
16125
16357
  this.stackRegion = stackRegion;
16126
16358
  this.exportIndexStore = exportIndexStore;
16127
- this.resolver = new IntrinsicFunctionResolver(stackRegion);
16359
+ this.resolver = new IntrinsicFunctionResolver(stackRegion, { strictGetAtt: options.strictGetAtt ?? false });
16128
16360
  this.options.concurrency = options.concurrency ?? 10;
16129
16361
  this.options.dryRun = options.dryRun ?? false;
16130
16362
  this.options.lockTimeout = options.lockTimeout ?? 300 * 1e3;
@@ -16139,6 +16371,7 @@ var DeployEngine = class {
16139
16371
  async deploy(stackName, template) {
16140
16372
  this.recordedImports = [];
16141
16373
  this.recordedOutputReads = [];
16374
+ this.resolver.resetPhysicalIdFallbackCount();
16142
16375
  return withStackName(stackName, () => this.doDeploy(stackName, template));
16143
16376
  }
16144
16377
  /**
@@ -16455,7 +16688,8 @@ var DeployEngine = class {
16455
16688
  deleted: 0,
16456
16689
  unchanged: Object.keys(currentState.resources).length,
16457
16690
  durationMs: Date.now() - startTime,
16458
- outputs: this.buildDisplayOutputs(template, persistedOutputs)
16691
+ outputs: this.buildDisplayOutputs(template, persistedOutputs),
16692
+ attributeFallbackCount: this.resolver.getPhysicalIdFallbackCount()
16459
16693
  };
16460
16694
  }
16461
16695
  const createChanges = this.diffCalculator.filterByType(changes, "CREATE");
@@ -16470,9 +16704,11 @@ var DeployEngine = class {
16470
16704
  updated: updateChanges.length,
16471
16705
  deleted: deleteChanges.length,
16472
16706
  unchanged: this.diffCalculator.filterByType(changes, "NO_CHANGE").length,
16473
- durationMs: Date.now() - startTime
16707
+ durationMs: Date.now() - startTime,
16708
+ attributeFallbackCount: this.resolver.getPhysicalIdFallbackCount()
16474
16709
  };
16475
16710
  }
16711
+ this.resolver.resetPhysicalIdFallbackCount();
16476
16712
  const progress = {
16477
16713
  current: 0,
16478
16714
  total: createChanges.length + updateChanges.length + deleteChanges.length
@@ -16491,7 +16727,8 @@ var DeployEngine = class {
16491
16727
  deleted: actualCounts.deleted,
16492
16728
  unchanged: unchangedCount,
16493
16729
  durationMs,
16494
- outputs: this.buildDisplayOutputs(template, newState.outputs ?? {})
16730
+ outputs: this.buildDisplayOutputs(template, newState.outputs ?? {}),
16731
+ attributeFallbackCount: this.resolver.getPhysicalIdFallbackCount()
16495
16732
  };
16496
16733
  } finally {
16497
16734
  renderer.stop();
@@ -16705,7 +16942,13 @@ var DeployEngine = class {
16705
16942
  }
16706
16943
  throw error;
16707
16944
  }
16708
- const outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
16945
+ let outputs;
16946
+ try {
16947
+ outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
16948
+ } catch (outputError) {
16949
+ await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
16950
+ throw outputError;
16951
+ }
16709
16952
  return {
16710
16953
  state: {
16711
16954
  version: 8,
@@ -16721,6 +16964,58 @@ var DeployEngine = class {
16721
16964
  };
16722
16965
  }
16723
16966
  /**
16967
+ * Persist state after provisioning fully succeeded but output resolution
16968
+ * threw (only reachable under `--strict-getatt`, whose promotion fires
16969
+ * AFTER the rollback catch block). The persisted shape mirrors the
16970
+ * success-path state EXCEPT for outputs:
16971
+ *
16972
+ * - `resources`: this run's provisioning result (`newResources`) — every
16973
+ * create/update/delete landed in AWS, so state must record it.
16974
+ * - `imports` / `outputReads`: this run's `recordedImports` /
16975
+ * `recordedOutputReads` (the provisioning that produced them succeeded;
16976
+ * dropping them would desync the strong-reference records from AWS —
16977
+ * matters on the update-deploy path where the pre-deploy snapshot may
16978
+ * be stale).
16979
+ * - `outputs`: the PREVIOUSLY persisted map — resolveOutputs threw before
16980
+ * producing a new one, mirroring what a resource-failure persist keeps.
16981
+ * The exports index is deliberately NOT updated (it stays consistent
16982
+ * with the old outputs that remain in state).
16983
+ *
16984
+ * ETag handling mirrors the post-rollback save: expected-ETag first (or
16985
+ * unconditional when `pendingMigration` — same as the per-resource save),
16986
+ * then a fresh-ETag retry, then warn. Best-effort: the deploy error being
16987
+ * rethrown is the primary signal; a failed save only warns.
16988
+ */
16989
+ async persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration) {
16990
+ const buildState = () => ({
16991
+ version: 8,
16992
+ region: this.stackRegion,
16993
+ stackName: currentState.stackName,
16994
+ resources: newResources,
16995
+ outputs: currentState.outputs,
16996
+ ...this.recordedImports.length > 0 && { imports: [...this.recordedImports] },
16997
+ ...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
16998
+ lastModified: Date.now()
16999
+ });
17000
+ try {
17001
+ const expectedEtag = pendingMigration ? void 0 : currentEtag;
17002
+ await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(buildState()), {
17003
+ ...expectedEtag !== void 0 && { expectedEtag },
17004
+ migrateLegacy: pendingMigration
17005
+ });
17006
+ this.logger.debug("State saved after output resolution failure");
17007
+ } catch (saveError) {
17008
+ this.logger.debug(`Retrying state save after output resolution failure (ETag mismatch): ${saveError instanceof Error ? saveError.message : String(saveError)}`);
17009
+ try {
17010
+ const freshEtag = (await this.stateBackend.getState(stackName, this.stackRegion))?.etag;
17011
+ await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(buildState()), { ...freshEtag !== void 0 && { expectedEtag: freshEtag } });
17012
+ this.logger.debug("State saved after output resolution failure (retry succeeded)");
17013
+ } catch (retryError) {
17014
+ this.logger.warn(`Failed to save state after output resolution failure: ${retryError instanceof Error ? retryError.message : String(retryError)} — resources were provisioned but not recorded; run deploy again to reconcile.`);
17015
+ }
17016
+ }
17017
+ }
17018
+ /**
16724
17019
  * Perform best-effort rollback of completed operations respecting dependencies
16725
17020
  *
16726
17021
  * - CREATE → delete the newly created resource (in reverse dependency order)
@@ -17468,6 +17763,7 @@ var DeployEngine = class {
17468
17763
  if (typeof exportName === "string") outputs[exportName] = value;
17469
17764
  }
17470
17765
  } catch (error) {
17766
+ if (this.options.strictGetAtt) throw new Error(`Failed to resolve output ${outputKey}: ${error instanceof Error ? error.message : String(error)} (--strict-getatt promotes output resolution failures to deploy errors; drop the flag to warn and skip the output instead)`);
17471
17767
  this.logger.warn(`Failed to resolve output ${outputKey}: ${String(error)}`);
17472
17768
  outputs[outputKey] = void 0;
17473
17769
  }
@@ -17486,5 +17782,5 @@ var DeployEngine = class {
17486
17782
  };
17487
17783
 
17488
17784
  //#endregion
17489
- export { BOOTSTRAP_MARKER_PREFIX as $, StateError as $t, refStateLookupFromResource as A, AssemblyReader as At, TemplateParser as B, DependencyError as Bt, ProviderRegistry as C, warnDeprecatedNoPrefixCliFlag as Ct, isTerminationProtectionPropagationError as D, findLargeInlineResources as Dt, disableInstanceApiTermination as E, MIGRATE_TMP_PREFIX as Et, resolveExplicitPhysicalId as F, resetAwsClients as Ft, AssetPublisher as G, MissingCdkCliError as Gt, S3StateBackend as H, LocalMigrateError as Ht, assertRegionMatch as I, setAwsClients as It, buildAssetRedirectMap as J, ProvisioningError as Jt, stringifyValue as K, NestedStackChildDirectDestroyError as Kt, applyRoleArnIfSet as L, AssetError as Lt, CDK_PATH_TAG as M, resolveBucketRegion as Mt, matchesCdkPath as N, AwsClients as Nt, IntrinsicFunctionResolver as O, uploadCfnTemplate as Ot, normalizeAwsTagsToCfn as P, getAwsClients as Pt, AssetModeResolver as Q, StackTerminationProtectionError as Qt, DiffCalculator as R, CdkdError as Rt, collectInlinePolicyNamesManagedBySiblings as S, resolveUseCdkBootstrapAssets as St, CloudControlProvider as T, CFN_TEMPLATE_URL_LIMIT as Tt, rebuildClientForBucketRegion as U, LocalStartServiceError as Ut, LockManager as V, LocalInvokeBuildError as Vt, shouldRetainResource as W, LockError as Wt, loadPublishableAssetManifest as X, ResourceUpdateNotSupportedError as Xt, createAssetRedirectResolver as Y, ResourceTimeoutError as Yt, rewriteTemplateAssetReferences as Z, StackHasActiveImportsError as Zt, gray as _, resolveAutoAssetStorage as _t, withRetry as a, __exportAll as an, buildDockerImage as at, yellow as b, resolveStateBucketWithDefault as bt, IMPLICIT_DELETE_DEPENDENCIES as c, runDockerForeground as ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as d, getDockerImageBySourceHash as dt, SynthesisError as en, ensureAssetStorage as et, isStatefulRecreateTargetSync as f, Synthesizer as ft, cyan as g, resolveApp as gt, bold as h, getLegacyStateBucketName as ht, withResourceDeadline as i, withErrorHandling as in, validateContainerRepoName as it, WAFv2WebACLProvider as j, clearBucketRegionCache as jt, cfnRefValueFromPhysicalId as k, expectedOwnerParam as kt, computeImplicitDeleteEdges as l, runDockerStreaming as lt, formatResourceLine as m, getDefaultStateBucketName as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, isCdkdError as nn, parseBootstrapMarker as nt, isRetryableTransientError as o, formatDockerLoginError as ot, renderStatefulReason as p, synthesisStatusMessage as pt, WorkGraph as q, PartialFailureError as qt, DeployEngine as r, normalizeAwsError as rn, validateAssetBucketName as rt, isThrottlingError as s, getDockerCmd as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, formatError as tn, getBootstrapMarkerKey as tt, extractDeploymentEventError as u, AssetManifestLoader as ut, green as v, resolveCaptureObservedState as vt, findActionableSilentDrops as w, CFN_TEMPLATE_BODY_LIMIT as wt, IAMRoleProvider as x, resolveStateBucketWithDefaultAndSource as xt, red as y, resolveSkipPrefix as yt, DagBuilder as z, ConfigError as zt };
17490
- //# sourceMappingURL=deploy-engine-d-cwg8zS.js.map
17785
+ export { BOOTSTRAP_MARKER_PREFIX as $, StateError as $t, refStateLookupFromResource as A, AssemblyReader as At, TemplateParser as B, DependencyError as Bt, ProviderRegistry as C, warnDeprecatedNoPrefixCliFlag as Ct, isTerminationProtectionPropagationError as D, findLargeInlineResources as Dt, disableInstanceApiTermination as E, MIGRATE_TMP_PREFIX as Et, resolveExplicitPhysicalId as F, resetAwsClients as Ft, AssetPublisher as G, MissingCdkCliError as Gt, S3StateBackend as H, LocalMigrateError as Ht, assertRegionMatch as I, setAwsClients as It, buildAssetRedirectMap as J, ProvisioningError as Jt, stringifyValue as K, NestedStackChildDirectDestroyError as Kt, applyRoleArnIfSet as L, AssetError as Lt, CDK_PATH_TAG as M, resolveBucketRegion as Mt, matchesCdkPath as N, AwsClients as Nt, IntrinsicFunctionResolver as O, uploadCfnTemplate as Ot, normalizeAwsTagsToCfn as P, getAwsClients as Pt, AssetModeResolver as Q, StackTerminationProtectionError as Qt, DiffCalculator as R, CdkdError as Rt, isRetryableTransientError as S, resolveUseCdkBootstrapAssets as St, CloudControlProvider as T, CFN_TEMPLATE_URL_LIMIT as Tt, rebuildClientForBucketRegion as U, LocalStartServiceError as Ut, LockManager as V, LocalInvokeBuildError as Vt, shouldRetainResource as W, LockError as Wt, loadPublishableAssetManifest as X, ResourceUpdateNotSupportedError as Xt, createAssetRedirectResolver as Y, ResourceTimeoutError as Yt, rewriteTemplateAssetReferences as Z, StackHasActiveImportsError as Zt, yellow as _, resolveAutoAssetStorage as _t, IMPLICIT_DELETE_DEPENDENCIES as a, __exportAll as an, buildDockerImage as at, importTagWalk as b, resolveStateBucketWithDefault as bt, MULTI_REGION_RECREATE_BLOCKED_TYPES as c, runDockerForeground as ct, formatResourceLine as d, getDockerImageBySourceHash as dt, SynthesisError as en, ensureAssetStorage as et, bold as f, Synthesizer as ft, red as g, resolveApp as gt, green as h, getLegacyStateBucketName as ht, withResourceDeadline as i, withErrorHandling as in, validateContainerRepoName as it, WAFv2WebACLProvider as j, clearBucketRegionCache as jt, cfnRefValueFromPhysicalId as k, expectedOwnerParam as kt, isStatefulRecreateTargetSync as l, runDockerStreaming as lt, gray as m, getDefaultStateBucketName as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, isCdkdError as nn, parseBootstrapMarker as nt, computeImplicitDeleteEdges as o, formatDockerLoginError as ot, cyan as p, synthesisStatusMessage as pt, WorkGraph as q, PartialFailureError as qt, DeployEngine as r, normalizeAwsError as rn, validateAssetBucketName as rt, extractDeploymentEventError as s, getDockerCmd as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, formatError as tn, getBootstrapMarkerKey as tt, renderStatefulReason as u, AssetManifestLoader as ut, IAMRoleProvider as v, resolveCaptureObservedState as vt, findActionableSilentDrops as w, CFN_TEMPLATE_BODY_LIMIT as wt, withRetry as x, resolveStateBucketWithDefaultAndSource as xt, collectInlinePolicyNamesManagedBySiblings as y, resolveSkipPrefix as yt, DagBuilder as z, ConfigError as zt };
17786
+ //# sourceMappingURL=deploy-engine-vUgwldVH.js.map