@go-to-k/cdkd 0.256.0 → 0.257.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.
@@ -8216,146 +8216,470 @@ function normalizeAwsTagsToCfn(tags) {
8216
8216
  }
8217
8217
 
8218
8218
  //#endregion
8219
- //#region src/provisioning/providers/wafv2-provider.ts
8219
+ //#region src/deployment/retryable-errors.ts
8220
8220
  /**
8221
- * Translate the empty-string placeholder `readCurrentState` emits for an
8222
- * absent `Description` to `undefined` before shipping it to AWS.
8221
+ * Patterns that mark an AWS error as a transient/retryable failure.
8222
+ * Each entry is a substring match against the error message; all of these
8223
+ * are situations where the same call typically succeeds after a short delay
8224
+ * because of eventual consistency or just-created-dependency propagation.
8225
+ */
8226
+ const RETRYABLE_ERROR_MESSAGE_PATTERNS = [
8227
+ "cannot be assumed",
8228
+ "Firehose is unable to assume role",
8229
+ "is unable to assume provided role",
8230
+ "role defined for the function",
8231
+ "not authorized to perform",
8232
+ "execution role",
8233
+ "trust policy",
8234
+ "Role validation failed",
8235
+ "does not have required permissions",
8236
+ "Trusted Entity",
8237
+ "currently in the following state: Pending",
8238
+ "has dependencies and cannot be deleted",
8239
+ "can't be deleted since it has",
8240
+ "DependencyViolation",
8241
+ "does not exist",
8242
+ "Schema is currently being altered",
8243
+ "Invalid principal in policy",
8244
+ "Policy Error: PrincipalNotFound",
8245
+ "Invalid value for the parameter Policy",
8246
+ "required permissions for: ENHANCED_MONITORING",
8247
+ "Caught ServiceAccessDeniedException",
8248
+ "permissions required to assume the role",
8249
+ "authorized to assume the provided role",
8250
+ "conflicting conditional operation",
8251
+ "scheduled for deletion",
8252
+ "Cannot access stream",
8253
+ "Please ensure the role can perform",
8254
+ "KMS key is invalid for CreateGrant",
8255
+ "Policy contains a statement with one or more invalid principals",
8256
+ "Invalid IAM Instance Profile",
8257
+ "Invalid InstanceProfile",
8258
+ "Failed to authorize instance profile",
8259
+ "Could not deliver test message",
8260
+ "wait 60 seconds",
8261
+ "concurrent update operation",
8262
+ "because it is in use",
8263
+ "Rate exceeded"
8264
+ ];
8265
+ /**
8266
+ * HTTP status codes that always indicate a transient failure worth retrying.
8267
+ * 429 = Too Many Requests (throttle), 503 = Service Unavailable.
8268
+ */
8269
+ const RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
8270
+ /**
8271
+ * AWS SDK v3 canonical throttling error names. Mirrors
8272
+ * `@aws-sdk/service-error-classification`'s `THROTTLING_ERROR_CODES` — any
8273
+ * error (or wrapped cause) whose `name` is one of these is a transient rate-
8274
+ * limit rejection worth retrying with backoff. Detecting by NAME is more
8275
+ * robust than by HTTP status because most AWS throttles surface as HTTP 400
8276
+ * (not 429) with the throttling signal carried only in the error code / name
8277
+ * (e.g. SSM `ThrottlingException` for the `Rate exceeded` message).
8278
+ */
8279
+ const THROTTLING_ERROR_NAMES = /* @__PURE__ */ new Set([
8280
+ "BandwidthLimitExceeded",
8281
+ "EC2ThrottledException",
8282
+ "LimitExceededException",
8283
+ "PriorRequestNotComplete",
8284
+ "ProvisionedThroughputExceededException",
8285
+ "RequestLimitExceeded",
8286
+ "RequestThrottled",
8287
+ "RequestThrottledException",
8288
+ "SlowDown",
8289
+ "ThrottledException",
8290
+ "Throttling",
8291
+ "ThrottlingException",
8292
+ "TooManyRequestsException",
8293
+ "TransactionInProgressException"
8294
+ ]);
8295
+ /**
8296
+ * Walk the error + its `.cause` chain (bounded) looking for a rate-limit
8297
+ * signal — either an AWS SDK v3 throttling error `name`
8298
+ * ({@link THROTTLING_ERROR_NAMES}) or a retryable HTTP status
8299
+ * ({@link RETRYABLE_HTTP_STATUS_CODES}) on `$metadata`.
8223
8300
  *
8224
- * `readCurrentState` always-emits `Description: ''` on a WebACL with no
8225
- * description (the comparator's top-level walk is state-keys-only, so
8226
- * the placeholder is required to detect a console-side description add).
8227
- * AWS WAFv2 `CreateWebACL` / `UpdateWebACL` reject `Description: ''`
8228
- * with `Member must have length greater than or equal to 1` (min 1 / max
8229
- * 256). On `cdkd drift --revert` the placeholder would round-trip
8230
- * through `update()` and surface as a hard AWS rejection, so we sanitize
8231
- * the wire-layer payload while keeping the read-side placeholder
8232
- * intact. This is the Class 2 pattern from
8233
- * `docs/provider-development.md § 3b`.
8301
+ * cdkd wraps the original AWS error in a `ProvisioningError`, so the signal is
8302
+ * typically one cause-link deep; the bounded walk also tolerates SDK errors
8303
+ * that nest a `$response`/cause without exploding on a cyclic chain.
8304
+ *
8305
+ * BOTH signals are checked at EVERY depth. An earlier version checked the name
8306
+ * to depth 5 but the HTTP status only at depths 0 and 1, so a 429 nested two
8307
+ * links deep was missed. This is the single shared cause-walk: the read-only
8308
+ * import tag walk (`src/provisioning/import-tag-walk.ts`) composes its own
8309
+ * classifier on top of this function rather than re-implementing the traversal,
8310
+ * so the two cannot drift apart again.
8234
8311
  */
8235
- function sanitizeDescription(value) {
8236
- if (value === void 0 || value === null) return void 0;
8237
- if (typeof value === "string" && value.length === 0) return void 0;
8238
- return value;
8312
+ function isThrottlingError(error) {
8313
+ let current = error;
8314
+ for (let depth = 0; depth < 5 && current != null; depth++) {
8315
+ const name = current.name;
8316
+ if (typeof name === "string" && THROTTLING_ERROR_NAMES.has(name)) return true;
8317
+ const status = current.$metadata?.httpStatusCode;
8318
+ if (status !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(status)) return true;
8319
+ current = current.cause;
8320
+ }
8321
+ return false;
8239
8322
  }
8240
8323
  /**
8241
- * Parse WAFv2 WebACL ARN to extract Id, Name, and Scope.
8242
- *
8243
- * ARN format:
8244
- * arn:aws:wafv2:{region}:{account}:regional/webacl/{name}/{id}
8245
- * arn:aws:wafv2:{region}:{account}:global/webacl/{name}/{id}
8324
+ * Determine whether an AWS error should be retried.
8246
8325
  *
8247
- * A short / malformed ARN yields `undefined` for `name` / `id` (the path
8248
- * segments simply are not there) callers must guard. `scope` is always
8249
- * defined (anything not `global` maps to `REGIONAL`).
8326
+ * Checks (in order):
8327
+ * 1. Rate-limit signal on the error or any wrapped cause throttling error
8328
+ * `name` or retryable HTTP status (most AWS throttles are HTTP 400, not
8329
+ * 429, so the name check carries most of the weight). See
8330
+ * {@link isThrottlingError}.
8331
+ * 2. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
8250
8332
  */
8251
- function parseWebACLArn(arn) {
8252
- const segments = arn.split(":").slice(5).join(":").split("/");
8253
- const scopeRaw = segments[0];
8254
- const name = segments[2];
8255
- return {
8256
- id: segments[3],
8257
- name,
8258
- scope: scopeRaw === "global" ? "CLOUDFRONT" : "REGIONAL"
8259
- };
8333
+ function isRetryableTransientError(error, message) {
8334
+ if (isThrottlingError(error)) return true;
8335
+ return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
8260
8336
  }
8337
+
8338
+ //#endregion
8339
+ //#region src/deployment/retry.ts
8261
8340
  /**
8262
- * AWS WAFv2 WebACL Provider
8341
+ * Retry helper for resource provisioning operations that hit transient
8342
+ * AWS eventual-consistency errors (IAM propagation, Lambda Pending state,
8343
+ * dependency violations, etc.).
8263
8344
  *
8264
- * Implements resource provisioning for AWS::WAFv2::WebACL using the WAFv2 SDK.
8265
- * WHY: WAFv2 CreateWebACL is synchronous - the CC API adds unnecessary polling
8266
- * overhead for an operation that completes immediately.
8267
- * This SDK provider eliminates that polling and returns instantly.
8345
+ * Extracted from DeployEngine so the backoff schedule can be unit-tested
8346
+ * in isolation. The retryable-error classifier itself lives in
8347
+ * `./retryable-errors.ts`.
8268
8348
  */
8269
- var WAFv2WebACLProvider = class {
8270
- wafv2Client;
8271
- providerRegion = process.env["AWS_REGION"];
8272
- logger = getLogger().child("WAFv2WebACLProvider");
8273
- handledProperties = /* @__PURE__ */ new Map([["AWS::WAFv2::WebACL", /* @__PURE__ */ new Set([
8274
- "Name",
8275
- "Scope",
8276
- "Tags",
8277
- "DefaultAction",
8278
- "Description",
8279
- "Rules",
8280
- "VisibilityConfig",
8281
- "CustomResponseBodies",
8282
- "CaptchaConfig",
8283
- "ChallengeConfig",
8284
- "TokenDomains",
8285
- "AssociationConfig"
8286
- ])]]);
8287
- getClient() {
8288
- if (!this.wafv2Client) this.wafv2Client = new WAFV2Client(this.providerRegion ? { region: this.providerRegion } : {});
8289
- return this.wafv2Client;
8290
- }
8291
- /**
8292
- * Create a WAFv2 WebACL
8293
- */
8294
- async create(logicalId, resourceType, properties) {
8295
- this.logger.debug(`Creating WAFv2 WebACL ${logicalId}`);
8296
- const name = properties["Name"] || generateResourceName(logicalId, { maxLength: 128 });
8297
- const scope = properties["Scope"] || "REGIONAL";
8298
- try {
8299
- const tags = [];
8300
- if (properties["Tags"]) {
8301
- const tagList = properties["Tags"];
8302
- for (const tag of tagList) tags.push({
8303
- Key: tag.Key,
8304
- Value: tag.Value
8305
- });
8306
- }
8307
- const summary = (await this.getClient().send(new CreateWebACLCommand({
8308
- Name: name,
8309
- Scope: scope,
8310
- DefaultAction: properties["DefaultAction"],
8311
- Description: sanitizeDescription(properties["Description"]),
8312
- Rules: properties["Rules"] || [],
8313
- VisibilityConfig: properties["VisibilityConfig"],
8314
- ...tags.length > 0 && { Tags: tags },
8315
- CustomResponseBodies: properties["CustomResponseBodies"],
8316
- CaptchaConfig: properties["CaptchaConfig"],
8317
- ChallengeConfig: properties["ChallengeConfig"],
8318
- TokenDomains: properties["TokenDomains"],
8319
- AssociationConfig: properties["AssociationConfig"]
8320
- }))).Summary;
8321
- if (!summary?.ARN || !summary?.Id) throw new Error("CreateWebACL did not return Summary with ARN and Id");
8322
- this.logger.debug(`Successfully created WAFv2 WebACL ${logicalId}: ${summary.ARN}`);
8323
- return {
8324
- physicalId: summary.ARN,
8325
- attributes: {
8326
- Arn: summary.ARN,
8327
- Id: summary.Id,
8328
- LabelNamespace: summary["LabelNamespace"]
8329
- }
8330
- };
8331
- } catch (error) {
8332
- if (error instanceof ProvisioningError) throw error;
8333
- const cause = error instanceof Error ? error : void 0;
8334
- throw new ProvisioningError(`Failed to create WAFv2 WebACL ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
8349
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8350
+ /**
8351
+ * Run `operation`, retrying transient failures with exponential backoff
8352
+ * capped at `maxDelayMs`.
8353
+ *
8354
+ * Backoff at the defaults (initialDelayMs=1_000, maxDelayMs=8_000, maxRetries=8):
8355
+ * 1s -> 2s -> 4s -> 8s -> 8s -> 8s -> 8s -> 8s (cumulative 47s)
8356
+ *
8357
+ * Non-retryable errors are rethrown immediately. The transient-error
8358
+ * classifier is `isRetryableTransientError` from ./retryable-errors.ts.
8359
+ */
8360
+ async function withRetry(operation, logicalId, opts = {}) {
8361
+ const maxRetries = opts.maxRetries ?? 8;
8362
+ const initialDelayMs = opts.initialDelayMs ?? 1e3;
8363
+ const maxDelayMs = opts.maxDelayMs ?? 8e3;
8364
+ const sleep = opts.sleep ?? defaultSleep;
8365
+ let lastError;
8366
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
8367
+ return await operation();
8368
+ } catch (error) {
8369
+ lastError = error;
8370
+ const message = error instanceof Error ? error.message : String(error);
8371
+ if (!(opts.isRetryable ? opts.isRetryable(message, error) : isRetryableTransientError(error, message)) || attempt >= maxRetries) throw error;
8372
+ const delay = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
8373
+ opts.logger?.debug(` ⏳ Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxRetries}) - ${message}`);
8374
+ for (let waited = 0; waited < delay; waited += 1e3) {
8375
+ if (opts.isInterrupted?.()) throw opts.onInterrupted ? opts.onInterrupted() : /* @__PURE__ */ new Error("Interrupted");
8376
+ await sleep(Math.min(1e3, delay - waited));
8335
8377
  }
8336
8378
  }
8337
- /**
8338
- * Update a WAFv2 WebACL
8339
- *
8340
- * Name and Scope are immutable - changes to those require replacement.
8341
- * UpdateWebACL requires LockToken obtained from GetWebACL.
8379
+ throw lastError;
8380
+ }
8381
+
8382
+ //#endregion
8383
+ //#region src/provisioning/import-tag-walk.ts
8384
+ /**
8385
+ * Shared, throttle-tolerant `aws:cdk:path` tag walk for `ResourceProvider.import`.
8386
+ *
8387
+ * Providers that adopt a resource without an explicit name property fall back
8388
+ * to step 3 of the lookup order documented in `./import-helpers.ts`: enumerate
8389
+ * the service's `List*`/`Describe*` pages, then issue ONE per-candidate read
8390
+ * (`DescribeX` / `ListTagsForResource`) to obtain the tag set — the list
8391
+ * summaries usually do not carry tags. That is an inherent **N+1** read
8392
+ * pattern: an account with many resources of the type produces one API call
8393
+ * per candidate in a tight loop, which is exactly the shape AWS rate-limits.
8394
+ *
8395
+ * Every provider previously hand-rolled this loop with NO backoff, so a single
8396
+ * throttled `Describe*` aborted the whole `cdkd import` run. This helper
8397
+ * centralises the loop and wraps BOTH the list page fetch and the per-candidate
8398
+ * describe in the deploy engine's `withRetry` with exponential backoff.
8399
+ *
8400
+ * ## Why a narrower classifier than `isRetryableTransientError`
8401
+ *
8402
+ * The deploy engine's classifier is tuned for the WRITE path: it also treats
8403
+ * eventual-consistency phrasings like `does not exist` / `not authorized to
8404
+ * perform` as transient, because a just-created dependency legitimately needs a
8405
+ * moment to propagate. On a read-only import walk those messages mean the
8406
+ * opposite — the candidate really is gone, or the caller's credentials really
8407
+ * lack the permission — and retrying them burns the full backoff budget per
8408
+ * candidate before surfacing the true error.
8409
+ *
8410
+ * So the walk retries throttling ONLY, reusing the deploy engine's throttle
8411
+ * tables verbatim ({@link THROTTLING_ERROR_NAMES} /
8412
+ * {@link RETRYABLE_HTTP_STATUS_CODES}) rather than maintaining a second copy.
8413
+ *
8414
+ * ## Batching
8415
+ *
8416
+ * Batched tag reads are deliberately NOT modelled here: the services this
8417
+ * helper currently serves (EMR `DescribeCluster`, DocDB
8418
+ * `ListTagsForResource`) expose only single-resource reads. A service that
8419
+ * genuinely offers a batch API (e.g. CodeCommit `BatchGetRepositories`) can
8420
+ * satisfy several candidates from one call inside its own `describe` callback,
8421
+ * or bypass the helper entirely.
8422
+ */
8423
+ /** Max number of retries after the first attempt, per API call in the walk. */
8424
+ const DEFAULT_MAX_RETRIES = 5;
8425
+ /** Initial backoff; each retry doubles it up to {@link DEFAULT_MAX_DELAY_MS}. */
8426
+ const DEFAULT_INITIAL_DELAY_MS = 500;
8427
+ /** Cap for the per-retry delay (0.5s -> 1s -> 2s -> 4s -> 5s, ~12.5s total). */
8428
+ const DEFAULT_MAX_DELAY_MS = 5e3;
8429
+ /**
8430
+ * Wall-clock ceiling for the WHOLE walk (all pages + all candidates), not just
8431
+ * one call. Backoff is per-call, so without this a sustained throttle against a
8432
+ * large account degrades into `(pages + candidates) x ~12.5s` of near-silent
8433
+ * retrying — ~42 minutes for 200 candidates, with no way to tell a slow walk
8434
+ * from a hung one. 10 minutes is generous for a healthy walk (a 200-candidate
8435
+ * DocDB account completes in seconds when AWS is not throttling) and bounds the
8436
+ * pathological case to something a user will wait through.
8437
+ */
8438
+ const DEFAULT_MAX_WALK_MS = 10 * 6e4;
8439
+ /**
8440
+ * Ceiling on pages fetched. A service that returns a non-advancing pagination
8441
+ * token (a bug, or a marker cdkd echoes back wrongly) would otherwise spin
8442
+ * forever. This is now the shared path every migrated provider runs on, so the
8443
+ * guard belongs here rather than in each caller.
8444
+ */
8445
+ const DEFAULT_MAX_PAGES = 1e3;
8446
+ /**
8447
+ * Whether an error hit during the read-only tag walk is a rate-limit rejection
8448
+ * worth backing off on.
8449
+ *
8450
+ * Delegates the error + `.cause` chain traversal to the deploy engine's
8451
+ * {@link isThrottlingError} (throttling error names + retryable HTTP statuses,
8452
+ * at every cause depth) and adds the canonical `Rate exceeded` message, which
8453
+ * several services return with an HTTP 400 and a service-specific error name.
8454
+ *
8455
+ * This is deliberately NARROWER than `isRetryableTransientError`: that
8456
+ * classifier also treats write-path eventual-consistency phrasings (`does not
8457
+ * exist`, `not authorized to perform`) as transient, because a just-created
8458
+ * dependency legitimately needs a moment to propagate. On a read-only walk
8459
+ * those mean the candidate really is gone or the credentials really lack the
8460
+ * permission, and retrying them burns the full backoff budget per candidate
8461
+ * before surfacing the true error.
8462
+ *
8463
+ * Exported for direct unit testing and for providers whose tag walk cannot use
8464
+ * {@link importTagWalk} verbatim.
8465
+ */
8466
+ function isThrottlingLikeError(error, message) {
8467
+ return isThrottlingError(error) || message.includes("Rate exceeded");
8468
+ }
8469
+ /**
8470
+ * Module-level test hooks for the walk's backoff.
8471
+ *
8472
+ * Providers do not expose a retry option on `import()`, so their wiring tests
8473
+ * cannot inject `retry.sleep` per call — without this hook every throttle-path
8474
+ * wiring test pays a real 0.5s+ backoff wait. Tests set `sleep` to a resolved
8475
+ * no-op in `beforeEach` (and clear it in `afterEach`); a per-call
8476
+ * `retry.sleep` still wins when supplied. Never set this in production code.
8477
+ */
8478
+ const importTagWalkTestHooks = {};
8479
+ /** Thrown when the walk exceeds its wall-clock budget or page ceiling. */
8480
+ var ImportTagWalkLimitError = class extends Error {
8481
+ constructor(message) {
8482
+ super(message);
8483
+ this.name = "ImportTagWalkLimitError";
8484
+ }
8485
+ };
8486
+ /**
8487
+ * Paginate `listPage`, `describe` each candidate, and return the first one
8488
+ * whose tags carry the requested `aws:cdk:path`. Returns `null` when no
8489
+ * candidate matches (or when `cdkPath` is empty).
8490
+ *
8491
+ * Both callbacks are individually retried with exponential backoff on
8492
+ * throttling errors, so one rate-limited call mid-walk no longer aborts the
8493
+ * whole import.
8494
+ */
8495
+ async function importTagWalk(options) {
8496
+ const { cdkPath, listPage, describe, tagsOf } = options;
8497
+ if (!cdkPath) return null;
8498
+ const logicalId = options.logicalId ?? "import";
8499
+ const logger = options.retry?.logger ?? getLogger();
8500
+ const maxWalkMs = options.retry?.maxWalkMs ?? DEFAULT_MAX_WALK_MS;
8501
+ const maxPages = options.retry?.maxPages ?? DEFAULT_MAX_PAGES;
8502
+ const sleep = options.retry?.sleep ?? importTagWalkTestHooks.sleep;
8503
+ const retryOpts = {
8504
+ maxRetries: options.retry?.maxRetries ?? DEFAULT_MAX_RETRIES,
8505
+ initialDelayMs: options.retry?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS,
8506
+ maxDelayMs: options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS,
8507
+ isRetryable: (message, error) => isThrottlingLikeError(error, message),
8508
+ logger,
8509
+ ...options.retry?.isInterrupted && { isInterrupted: options.retry.isInterrupted },
8510
+ ...options.retry?.onInterrupted && { onInterrupted: options.retry.onInterrupted },
8511
+ ...sleep && { sleep }
8512
+ };
8513
+ const startedAt = Date.now();
8514
+ const assertWithinBudget = (stage) => {
8515
+ const elapsed = Date.now() - startedAt;
8516
+ 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>.`);
8517
+ };
8518
+ let marker;
8519
+ let pages = 0;
8520
+ do {
8521
+ assertWithinBudget("listing candidates");
8522
+ 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>.`);
8523
+ const currentMarker = marker;
8524
+ const page = await withRetry(() => listPage(currentMarker), logicalId, retryOpts);
8525
+ for (const summary of page.items ?? []) {
8526
+ assertWithinBudget("reading candidate tags");
8527
+ const detail = await withRetry(() => describe(summary), logicalId, retryOpts);
8528
+ if (detail === void 0) {
8529
+ logger.debug(` ↷ Skipped an ${logicalId} import candidate: its detail lookup returned no result`);
8530
+ continue;
8531
+ }
8532
+ if (matchesCdkPath(tagsOf(detail, summary), cdkPath)) return {
8533
+ summary,
8534
+ detail
8535
+ };
8536
+ }
8537
+ marker = page.nextMarker;
8538
+ } while (marker);
8539
+ return null;
8540
+ }
8541
+
8542
+ //#endregion
8543
+ //#region src/provisioning/providers/wafv2-provider.ts
8544
+ /**
8545
+ * Translate the empty-string placeholder `readCurrentState` emits for an
8546
+ * absent `Description` to `undefined` before shipping it to AWS.
8547
+ *
8548
+ * `readCurrentState` always-emits `Description: ''` on a WebACL with no
8549
+ * description (the comparator's top-level walk is state-keys-only, so
8550
+ * the placeholder is required to detect a console-side description add).
8551
+ * AWS WAFv2 `CreateWebACL` / `UpdateWebACL` reject `Description: ''`
8552
+ * with `Member must have length greater than or equal to 1` (min 1 / max
8553
+ * 256). On `cdkd drift --revert` the placeholder would round-trip
8554
+ * through `update()` and surface as a hard AWS rejection, so we sanitize
8555
+ * the wire-layer payload while keeping the read-side placeholder
8556
+ * intact. This is the Class 2 pattern from
8557
+ * `docs/provider-development.md § 3b`.
8558
+ */
8559
+ function sanitizeDescription(value) {
8560
+ if (value === void 0 || value === null) return void 0;
8561
+ if (typeof value === "string" && value.length === 0) return void 0;
8562
+ return value;
8563
+ }
8564
+ /**
8565
+ * Parse WAFv2 WebACL ARN to extract Id, Name, and Scope.
8566
+ *
8567
+ * ARN format:
8568
+ * arn:aws:wafv2:{region}:{account}:regional/webacl/{name}/{id}
8569
+ * arn:aws:wafv2:{region}:{account}:global/webacl/{name}/{id}
8570
+ *
8571
+ * A short / malformed ARN yields `undefined` for `name` / `id` (the path
8572
+ * segments simply are not there) — callers must guard. `scope` is always
8573
+ * defined (anything not `global` maps to `REGIONAL`).
8574
+ */
8575
+ function parseWebACLArn(arn) {
8576
+ const segments = arn.split(":").slice(5).join(":").split("/");
8577
+ const scopeRaw = segments[0];
8578
+ const name = segments[2];
8579
+ return {
8580
+ id: segments[3],
8581
+ name,
8582
+ scope: scopeRaw === "global" ? "CLOUDFRONT" : "REGIONAL"
8583
+ };
8584
+ }
8585
+ /**
8586
+ * AWS WAFv2 WebACL Provider
8587
+ *
8588
+ * Implements resource provisioning for AWS::WAFv2::WebACL using the WAFv2 SDK.
8589
+ * WHY: WAFv2 CreateWebACL is synchronous - the CC API adds unnecessary polling
8590
+ * overhead for an operation that completes immediately.
8591
+ * This SDK provider eliminates that polling and returns instantly.
8592
+ */
8593
+ var WAFv2WebACLProvider = class {
8594
+ wafv2Client;
8595
+ providerRegion = process.env["AWS_REGION"];
8596
+ logger = getLogger().child("WAFv2WebACLProvider");
8597
+ handledProperties = /* @__PURE__ */ new Map([["AWS::WAFv2::WebACL", /* @__PURE__ */ new Set([
8598
+ "Name",
8599
+ "Scope",
8600
+ "Tags",
8601
+ "DefaultAction",
8602
+ "Description",
8603
+ "Rules",
8604
+ "VisibilityConfig",
8605
+ "CustomResponseBodies",
8606
+ "CaptchaConfig",
8607
+ "ChallengeConfig",
8608
+ "TokenDomains",
8609
+ "AssociationConfig"
8610
+ ])]]);
8611
+ getClient() {
8612
+ if (!this.wafv2Client) this.wafv2Client = new WAFV2Client(this.providerRegion ? { region: this.providerRegion } : {});
8613
+ return this.wafv2Client;
8614
+ }
8615
+ /**
8616
+ * Create a WAFv2 WebACL
8342
8617
  */
8343
- async update(logicalId, physicalId, resourceType, properties, previousProperties) {
8344
- this.logger.debug(`Updating WAFv2 WebACL ${logicalId}: ${physicalId}`);
8618
+ async create(logicalId, resourceType, properties) {
8619
+ this.logger.debug(`Creating WAFv2 WebACL ${logicalId}`);
8620
+ const name = properties["Name"] || generateResourceName(logicalId, { maxLength: 128 });
8621
+ const scope = properties["Scope"] || "REGIONAL";
8345
8622
  try {
8346
- const { id, name, scope } = parseWebACLArn(physicalId);
8347
- const getResponse = await this.getClient().send(new GetWebACLCommand({
8348
- Name: name,
8349
- Scope: scope,
8350
- Id: id
8351
- }));
8352
- const lockToken = getResponse.LockToken;
8353
- if (!lockToken) throw new Error("GetWebACL did not return LockToken");
8354
- await this.getClient().send(new UpdateWebACLCommand({
8623
+ const tags = [];
8624
+ if (properties["Tags"]) {
8625
+ const tagList = properties["Tags"];
8626
+ for (const tag of tagList) tags.push({
8627
+ Key: tag.Key,
8628
+ Value: tag.Value
8629
+ });
8630
+ }
8631
+ const summary = (await this.getClient().send(new CreateWebACLCommand({
8355
8632
  Name: name,
8356
8633
  Scope: scope,
8357
- Id: id,
8358
- LockToken: lockToken,
8634
+ DefaultAction: properties["DefaultAction"],
8635
+ Description: sanitizeDescription(properties["Description"]),
8636
+ Rules: properties["Rules"] || [],
8637
+ VisibilityConfig: properties["VisibilityConfig"],
8638
+ ...tags.length > 0 && { Tags: tags },
8639
+ CustomResponseBodies: properties["CustomResponseBodies"],
8640
+ CaptchaConfig: properties["CaptchaConfig"],
8641
+ ChallengeConfig: properties["ChallengeConfig"],
8642
+ TokenDomains: properties["TokenDomains"],
8643
+ AssociationConfig: properties["AssociationConfig"]
8644
+ }))).Summary;
8645
+ if (!summary?.ARN || !summary?.Id) throw new Error("CreateWebACL did not return Summary with ARN and Id");
8646
+ this.logger.debug(`Successfully created WAFv2 WebACL ${logicalId}: ${summary.ARN}`);
8647
+ return {
8648
+ physicalId: summary.ARN,
8649
+ attributes: {
8650
+ Arn: summary.ARN,
8651
+ Id: summary.Id,
8652
+ LabelNamespace: summary["LabelNamespace"]
8653
+ }
8654
+ };
8655
+ } catch (error) {
8656
+ if (error instanceof ProvisioningError) throw error;
8657
+ const cause = error instanceof Error ? error : void 0;
8658
+ throw new ProvisioningError(`Failed to create WAFv2 WebACL ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, void 0, cause);
8659
+ }
8660
+ }
8661
+ /**
8662
+ * Update a WAFv2 WebACL
8663
+ *
8664
+ * Name and Scope are immutable - changes to those require replacement.
8665
+ * UpdateWebACL requires LockToken obtained from GetWebACL.
8666
+ */
8667
+ async update(logicalId, physicalId, resourceType, properties, previousProperties) {
8668
+ this.logger.debug(`Updating WAFv2 WebACL ${logicalId}: ${physicalId}`);
8669
+ try {
8670
+ const { id, name, scope } = parseWebACLArn(physicalId);
8671
+ const getResponse = await this.getClient().send(new GetWebACLCommand({
8672
+ Name: name,
8673
+ Scope: scope,
8674
+ Id: id
8675
+ }));
8676
+ const lockToken = getResponse.LockToken;
8677
+ if (!lockToken) throw new Error("GetWebACL did not return LockToken");
8678
+ await this.getClient().send(new UpdateWebACLCommand({
8679
+ Name: name,
8680
+ Scope: scope,
8681
+ Id: id,
8682
+ LockToken: lockToken,
8359
8683
  DefaultAction: properties["DefaultAction"],
8360
8684
  Description: sanitizeDescription(properties["Description"]),
8361
8685
  Rules: properties["Rules"] || [],
@@ -8529,25 +8853,31 @@ var WAFv2WebACLProvider = class {
8529
8853
  if (err instanceof WAFNonexistentItemException) return null;
8530
8854
  throw err;
8531
8855
  }
8532
- if (!input.cdkPath) return null;
8533
8856
  const scope = input.properties["Scope"] || "REGIONAL";
8534
- let nextMarker;
8535
- do {
8536
- const list = await this.getClient().send(new ListWebACLsCommand({
8537
- Scope: scope,
8538
- ...nextMarker && { NextMarker: nextMarker }
8539
- }));
8540
- for (const item of list.WebACLs ?? []) {
8541
- if (!item.ARN) continue;
8542
- const tagList = (await this.getClient().send(new ListTagsForResourceCommand$11({ ResourceARN: item.ARN }))).TagInfoForResource?.TagList;
8543
- if (matchesCdkPath(tagList, input.cdkPath)) return {
8544
- physicalId: item.ARN,
8545
- attributes: {}
8857
+ const match = await importTagWalk({
8858
+ cdkPath: input.cdkPath,
8859
+ logicalId: input.logicalId,
8860
+ listPage: async (marker) => {
8861
+ const list = await this.getClient().send(new ListWebACLsCommand({
8862
+ Scope: scope,
8863
+ ...marker && { NextMarker: marker }
8864
+ }));
8865
+ return {
8866
+ items: list.WebACLs,
8867
+ nextMarker: list.NextMarker
8546
8868
  };
8547
- }
8548
- nextMarker = list.NextMarker;
8549
- } while (nextMarker);
8550
- return null;
8869
+ },
8870
+ describe: async (item) => {
8871
+ if (!item.ARN) return void 0;
8872
+ return await this.getClient().send(new ListTagsForResourceCommand$11({ ResourceARN: item.ARN }));
8873
+ },
8874
+ tagsOf: (tagsResp) => tagsResp.TagInfoForResource?.TagList
8875
+ });
8876
+ if (!match) return null;
8877
+ return {
8878
+ physicalId: match.summary.ARN,
8879
+ attributes: {}
8880
+ };
8551
8881
  }
8552
8882
  };
8553
8883
 
@@ -10954,7 +11284,7 @@ var CloudControlProvider = class {
10954
11284
  this.logger.debug(`Deleting resource ${logicalId} (${resourceType}), physical ID: ${physicalId}`);
10955
11285
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
10956
11286
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
10957
- const { ASGProvider } = await import("./asg-provider-DbIC4hWG.js").then((n) => n.n);
11287
+ const { ASGProvider } = await import("./asg-provider-6IZKZOKp.js").then((n) => n.n);
10958
11288
  await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
10959
11289
  return;
10960
11290
  }
@@ -14753,319 +15083,6 @@ function isCustomResource(resourceType) {
14753
15083
  return resourceType.startsWith("Custom::") || resourceType === "AWS::CloudFormation::CustomResource";
14754
15084
  }
14755
15085
 
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
-
15069
15086
  //#endregion
15070
15087
  //#region src/provisioning/providers/iam-role-provider.ts
15071
15088
  /**
@@ -17782,5 +17799,5 @@ var DeployEngine = class {
17782
17799
  };
17783
17800
 
17784
17801
  //#endregion
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
17802
+ export { BOOTSTRAP_MARKER_PREFIX as $, StateError as $t, withRetry as A, AssemblyReader as At, TemplateParser as B, DependencyError as Bt, disableInstanceApiTermination as C, warnDeprecatedNoPrefixCliFlag as Ct, refStateLookupFromResource as D, findLargeInlineResources as Dt, cfnRefValueFromPhysicalId 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, WAFv2WebACLProvider as O, uploadCfnTemplate as Ot, normalizeAwsTagsToCfn as P, getAwsClients as Pt, AssetModeResolver as Q, StackTerminationProtectionError as Qt, DiffCalculator as R, CdkdError as Rt, CloudControlProvider as S, resolveUseCdkBootstrapAssets as St, IntrinsicFunctionResolver 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, ProviderRegistry 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, isRetryableTransientError as j, clearBucketRegionCache as jt, importTagWalk 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, isTerminationProtectionPropagationError as w, CFN_TEMPLATE_BODY_LIMIT as wt, findActionableSilentDrops as x, resolveStateBucketWithDefaultAndSource as xt, collectInlinePolicyNamesManagedBySiblings as y, resolveSkipPrefix as yt, DagBuilder as z, ConfigError as zt };
17803
+ //# sourceMappingURL=deploy-engine-DOPBIRKR.js.map