@go-to-k/cdkd 0.257.2 → 0.257.4

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.
@@ -2,7 +2,7 @@ import { a as getLiveRenderer, d as generateResourceNameWithFallback, l as apply
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
4
4
  import { CloudControlClient, CreateResourceCommand, DeleteResourceCommand, GetResourceCommand, GetResourceRequestStatusCommand, ListResourcesCommand, UpdateResourceCommand } from "@aws-sdk/client-cloudcontrol";
5
- import { AttachRolePolicyCommand, CreateRoleCommand, DeleteRoleCommand, DeleteRolePermissionsBoundaryCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, GetRolePolicyCommand, IAMClient, ListAttachedRolePoliciesCommand, ListInstanceProfilesForRoleCommand, ListRolePoliciesCommand, ListRoleTagsCommand, ListRolesCommand, NoSuchEntityException, PutRolePermissionsBoundaryCommand, PutRolePolicyCommand, RemoveRoleFromInstanceProfileCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand, UpdateRoleCommand } from "@aws-sdk/client-iam";
5
+ import { AttachRolePolicyCommand, CreateRoleCommand, DeleteRoleCommand, DeleteRolePermissionsBoundaryCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, GetRolePolicyCommand, IAMClient, ListAttachedRolePoliciesCommand, ListInstanceProfilesForRoleCommand, ListRolePoliciesCommand, ListRoleTagsCommand, NoSuchEntityException, PutRolePermissionsBoundaryCommand, PutRolePolicyCommand, RemoveRoleFromInstanceProfileCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand, UpdateRoleCommand } from "@aws-sdk/client-iam";
6
6
  import { SQSClient } from "@aws-sdk/client-sqs";
7
7
  import { PublishCommand, SNSClient } from "@aws-sdk/client-sns";
8
8
  import { GetFunctionCommand, GetFunctionUrlConfigCommand, InvokeCommand, LambdaClient, UpdateFunctionConfigurationCommand, waitUntilFunctionActiveV2, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
@@ -35,7 +35,7 @@ import { DescribeDBClustersCommand, DescribeDBInstancesCommand, RDSClient } from
35
35
  import { DescribeReplicationGroupsCommand, ElastiCacheClient } from "@aws-sdk/client-elasticache";
36
36
  import { DescribeClustersCommand, RedshiftClient } from "@aws-sdk/client-redshift";
37
37
  import { DescribeDomainCommand, OpenSearchClient } from "@aws-sdk/client-opensearch";
38
- import { CreateWebACLCommand, DeleteWebACLCommand, GetWebACLCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$11, ListWebACLsCommand, TagResourceCommand as TagResourceCommand$12, UntagResourceCommand as UntagResourceCommand$12, UpdateWebACLCommand, WAFNonexistentItemException, WAFV2Client } from "@aws-sdk/client-wafv2";
38
+ import { CreateWebACLCommand, DeleteWebACLCommand, GetWebACLCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$11, TagResourceCommand as TagResourceCommand$12, UntagResourceCommand as UntagResourceCommand$12, UpdateWebACLCommand, WAFNonexistentItemException, WAFV2Client } from "@aws-sdk/client-wafv2";
39
39
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
40
40
 
41
41
  //#region \0rolldown/runtime.js
@@ -2488,6 +2488,25 @@ function findLargeInlineResources(template, threshold = LARGE_INLINE_RESOURCE_TH
2488
2488
  const WAITER_MAX_WAIT_SECONDS = 600;
2489
2489
  const PARAMETER_PLACEHOLDER = "cdkd-macro-expand-placeholder";
2490
2490
  /**
2491
+ * AWS's managed pre-deployment validation hooks
2492
+ * (`AWS::EarlyValidation::*`, e.g. `ResourceExistenceCheck`) can
2493
+ * reject the transient expansion changeset INTERMITTENTLY (issue
2494
+ * #1151: two consecutive rejections followed by a clean pass on the
2495
+ * same template minutes later, with the same named resources
2496
+ * existing throughout). The changeset is never executed — cdkd only
2497
+ * reads the Processed-stage template — so a validation-hook rejection
2498
+ * carries no real risk and is worth retrying with a fresh transient
2499
+ * stack before failing the whole run.
2500
+ */
2501
+ const EARLY_VALIDATION_MAX_ATTEMPTS = 3;
2502
+ const EARLY_VALIDATION_RETRY_BASE_DELAY_MS = 2e3;
2503
+ /** Matches the changeset FAILED StatusReason emitted by the hook family. */
2504
+ function isEarlyValidationRejection(err) {
2505
+ return err instanceof MacroExpansionError && /AWS::EarlyValidation::/.test(err.message);
2506
+ }
2507
+ /** Test seam: overridable sleep so retry tests don't wait wall-clock. */
2508
+ const retryDelays = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
2509
+ /**
2491
2510
  * Capabilities sent on every `CreateChangeSet` call:
2492
2511
  *
2493
2512
  * - `CAPABILITY_AUTO_EXPAND` is the load-bearing one — required for CFn
@@ -2583,6 +2602,19 @@ const PARAMETER_TYPE_PLACEHOLDERS = {
2583
2602
  async function expandMacros(template, opts) {
2584
2603
  const logger = getLogger().child("MacroExpander");
2585
2604
  if (!containsMacro(template)) return template;
2605
+ for (let attempt = 1;; attempt++) try {
2606
+ return await expandMacrosAttempt(template, opts, logger);
2607
+ } catch (err) {
2608
+ if (attempt < EARLY_VALIDATION_MAX_ATTEMPTS && isEarlyValidationRejection(err)) {
2609
+ const delayMs = EARLY_VALIDATION_RETRY_BASE_DELAY_MS * 2 ** (attempt - 1);
2610
+ logger.warn(`Macro expansion changeset was rejected by an AWS EarlyValidation hook (attempt ${attempt}/${EARLY_VALIDATION_MAX_ATTEMPTS}); this rejection is known to be intermittent — retrying with a fresh transient stack in ${delayMs / 1e3}s...`);
2611
+ await retryDelays.sleep(delayMs);
2612
+ continue;
2613
+ }
2614
+ throw err;
2615
+ }
2616
+ }
2617
+ async function expandMacrosAttempt(template, opts, logger) {
2586
2618
  const macros = enumerateMacros(template);
2587
2619
  logger.debug(`Macro expansion: detected transforms [${macros.join(", ")}], starting CFn round-trip...`);
2588
2620
  const transientStackName = `cdkd-macro-expand-${randomUUID().slice(0, 16)}`;
@@ -3131,19 +3163,7 @@ var Synthesizer = class {
3131
3163
  this.logger.debug(`Using pre-synthesized cloud assembly at ${appPath}`);
3132
3164
  const manifest = this.assemblyReader.readManifest(appPath);
3133
3165
  const stacks = this.assemblyReader.getAllStacks(appPath, manifest);
3134
- const presynthRegion = options.region || process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"];
3135
- let presynthAccountId;
3136
- try {
3137
- const stsClient = new STSClient({ ...presynthRegion && { region: presynthRegion } });
3138
- presynthAccountId = (await stsClient.send(new GetCallerIdentityCommand({}))).Account;
3139
- stsClient.destroy();
3140
- } catch {
3141
- this.logger.debug("Could not resolve AWS account ID via STS (pre-synth branch)");
3142
- }
3143
- await this.expandMacrosForStacks(stacks, options, {
3144
- region: presynthRegion,
3145
- ...presynthAccountId && { accountId: presynthAccountId }
3146
- });
3166
+ if (!options.deferMacroExpansion) await this.expandMacrosForStacks(stacks, options);
3147
3167
  this.logger.debug(`Loaded ${stacks.length} stack(s) from pre-synthesized assembly`);
3148
3168
  return {
3149
3169
  manifest,
@@ -3162,7 +3182,8 @@ var Synthesizer = class {
3162
3182
  "aws:cdk:version-reporting": true,
3163
3183
  "aws:cdk:bundling-stacks": ["**"]
3164
3184
  };
3165
- const region = options.region || process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"];
3185
+ const explicitRegion = options.region || process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"];
3186
+ const region = explicitRegion || await resolveSdkDefaultRegion(options.profile);
3166
3187
  let accountId;
3167
3188
  try {
3168
3189
  const stsClient = new STSClient({ ...region && { region } });
@@ -3196,8 +3217,8 @@ var Synthesizer = class {
3196
3217
  const manifest = this.assemblyReader.readManifest(outputDir);
3197
3218
  if (!manifest.missing || manifest.missing.length === 0) {
3198
3219
  const stacks = this.assemblyReader.getAllStacks(outputDir, manifest);
3199
- await this.expandMacrosForStacks(stacks, options, {
3200
- region,
3220
+ if (!options.deferMacroExpansion) await this.expandMacrosForStacks(stacks, options, {
3221
+ region: explicitRegion,
3201
3222
  ...accountId && { accountId }
3202
3223
  });
3203
3224
  this.logger.debug(`Synthesis complete: ${stacks.length} stack(s)`);
@@ -3218,10 +3239,16 @@ var Synthesizer = class {
3218
3239
  }
3219
3240
  }
3220
3241
  /**
3221
- * List stack names in CDK app
3242
+ * List stack names in CDK app. Names come straight from the
3243
+ * manifest, so the macro-expansion pre-pass is skipped entirely —
3244
+ * listing a macro app needs no AWS region or CloudFormation access
3245
+ * (issue #1150).
3222
3246
  */
3223
3247
  async listStacks(options) {
3224
- return (await this.synthesize(options)).stacks.map((s) => s.stackName);
3248
+ return (await this.synthesize({
3249
+ ...options,
3250
+ deferMacroExpansion: true
3251
+ })).stacks.map((s) => s.stackName);
3225
3252
  }
3226
3253
  /**
3227
3254
  * Per-stack macro-expansion pass (Issue #463). Mutates each stack's
@@ -3230,23 +3257,36 @@ var Synthesizer = class {
3230
3257
  * analyzer / provisioner pipeline consumes the templates, so every
3231
3258
  * downstream stage sees the post-expansion shape.
3232
3259
  *
3233
- * Skipped silently when no stack carries a macro — pure no-op cost.
3234
- * When a macro IS detected and the caller did NOT thread a
3235
- * region into the synthesizer, falls back to resolving region from
3236
- * the synthesized stack's environment (set by `cdk.Stack.region`).
3237
- * Throws `SynthesisError` when no region can be resolved (the
3238
- * upstream caller treats it as a synth failure) and propagates
3239
- * `MacroExpansionError` (from {@link expandMacros}) on any CFn-side
3240
- * failure during the round-trip.
3260
+ * PUBLIC since issue #1150: selection-aware commands (deploy / diff)
3261
+ * synthesize with `deferMacroExpansion: true` and invoke this
3262
+ * directly with ONLY the stacks they will actually consume, so an
3263
+ * unselected macro-carrying sibling can never fail or slow the run.
3264
+ *
3265
+ * Skipped silently when no stack carries a macro pure no-op cost
3266
+ * (in particular, no STS hop). When a macro IS detected and the
3267
+ * caller did NOT thread a resolved region/account, both are
3268
+ * resolved here (STS for the default state bucket; the region chain
3269
+ * below for the CFn client). Throws `SynthesisError` when no region
3270
+ * can be resolved (the upstream caller treats it as a synth
3271
+ * failure) and propagates `MacroExpansionError` (from {@link
3272
+ * expandMacros}) on any CFn-side failure during the round-trip.
3241
3273
  */
3242
3274
  async expandMacrosForStacks(stacks, options, resolved) {
3243
3275
  const stacksWithMacros = stacks.filter((s) => containsMacro(s.template));
3244
3276
  if (stacksWithMacros.length === 0) return;
3245
- const region = resolved?.region || options.region || process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"] || stacksWithMacros[0]?.region;
3277
+ const region = resolved?.region || options.region || process.env["AWS_REGION"] || process.env["AWS_DEFAULT_REGION"] || stacksWithMacros[0]?.region || await resolveSdkDefaultRegion(options.profile);
3246
3278
  if (!region) throw new SynthesisError(`Stack(s) [${stacksWithMacros.map((s) => s.stackName).join(", ")}] use CloudFormation macros (Transform / Fn::Transform) but cdkd could not resolve an AWS region for the expansion round-trip. Set AWS_REGION, pass --region <r>, or set env: { region: '<r>' } in your CDK Stack constructor.`);
3279
+ let accountId = resolved?.accountId;
3280
+ if (resolved === void 0 && !options.stateBucket) try {
3281
+ const stsClient = new STSClient({ region });
3282
+ accountId = (await stsClient.send(new GetCallerIdentityCommand({}))).Account;
3283
+ stsClient.destroy();
3284
+ } catch {
3285
+ this.logger.debug("Could not resolve AWS account ID via STS (macro expansion)");
3286
+ }
3247
3287
  let stateBucket;
3248
3288
  if (options.stateBucket) stateBucket = options.stateBucket;
3249
- else if (resolved?.accountId) stateBucket = `cdkd-state-${resolved.accountId}`;
3289
+ else if (accountId) stateBucket = `cdkd-state-${accountId}`;
3250
3290
  else {
3251
3291
  const oversize = stacksWithMacros.find((s) => JSON.stringify(s.template).length > 51200);
3252
3292
  if (oversize) throw new SynthesisError(`Stack '${oversize.stackName}' uses CloudFormation macros AND its serialized template exceeds the 51,200-byte inline TemplateBody limit, so cdkd must upload the template to S3 for the transient expansion changeset. cdkd could not resolve a state bucket: STS GetCallerIdentity failed AND --state-bucket was not provided. Pass --state-bucket <name> (cdkd uses the same bucket as cdkd deploy state storage; typically 'cdkd-state-<accountId>').`);
@@ -3268,6 +3308,31 @@ var Synthesizer = class {
3268
3308
  }
3269
3309
  };
3270
3310
  /**
3311
+ * Resolve the AWS SDK's own default region (env vars, shared config
3312
+ * file profile region, etc.) — the same chain every cdkd AWS client
3313
+ * consults implicitly when constructed without an explicit region.
3314
+ * Returns `undefined` when the chain yields nothing so callers can
3315
+ * fall through to their own hard-error (issue #1149: the
3316
+ * macro-expansion pre-pass previously checked only explicit
3317
+ * option/env sources and hard-errored for users whose region lives in
3318
+ * `~/.aws/config`).
3319
+ *
3320
+ * Implemented by asking a throwaway STS client for its resolved
3321
+ * region provider — that is exactly the chain the real provisioning
3322
+ * clients use, so the two can never diverge.
3323
+ */
3324
+ async function resolveSdkDefaultRegion(profile) {
3325
+ let client;
3326
+ try {
3327
+ client = new STSClient({ ...profile && { profile } });
3328
+ return await client.config.region() || void 0;
3329
+ } catch {
3330
+ return;
3331
+ } finally {
3332
+ client?.destroy?.();
3333
+ }
3334
+ }
3335
+ /**
3271
3336
  * Check if two sets contain the same elements
3272
3337
  */
3273
3338
  function setsEqual(a, b) {
@@ -8158,21 +8223,6 @@ function resolveExplicitPhysicalId(input, nameProperty) {
8158
8223
  }
8159
8224
  }
8160
8225
  /**
8161
- * The standard tag CDK puts on every deployed resource — its construct path
8162
- * within the app, e.g. `MyStack/MyConstruct/MyBucket`. Used as the lookup key
8163
- * when no explicit name is in the template.
8164
- */
8165
- const CDK_PATH_TAG = "aws:cdk:path";
8166
- /**
8167
- * Match an AWS resource's tag set against the CDK path the template carries.
8168
- * Returns true if the resource was deployed by the same CDK construct.
8169
- */
8170
- function matchesCdkPath(tags, cdkPath) {
8171
- if (!tags || !cdkPath) return false;
8172
- for (const t of tags) if (t.Key === "aws:cdk:path" && t.Value === cdkPath) return true;
8173
- return false;
8174
- }
8175
- /**
8176
8226
  * Re-shape an AWS tag list (any of the common shapes — array of `{Key, Value}`,
8177
8227
  * map keyed by tag name, or v2-style array of `{TagKey, TagValue}`) into the
8178
8228
  * canonical CFn shape (`Array<{Key, Value}>`) that cdkd state holds, with
@@ -8215,330 +8265,6 @@ function normalizeAwsTagsToCfn(tags) {
8215
8265
  return out;
8216
8266
  }
8217
8267
 
8218
- //#endregion
8219
- //#region src/deployment/retryable-errors.ts
8220
- /**
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`.
8300
- *
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.
8311
- */
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;
8322
- }
8323
- /**
8324
- * Determine whether an AWS error should be retried.
8325
- *
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}
8332
- */
8333
- function isRetryableTransientError(error, message) {
8334
- if (isThrottlingError(error)) return true;
8335
- return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
8336
- }
8337
-
8338
- //#endregion
8339
- //#region src/deployment/retry.ts
8340
- /**
8341
- * Retry helper for resource provisioning operations that hit transient
8342
- * AWS eventual-consistency errors (IAM propagation, Lambda Pending state,
8343
- * dependency violations, etc.).
8344
- *
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`.
8348
- */
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));
8377
- }
8378
- }
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
8268
  //#endregion
8543
8269
  //#region src/provisioning/providers/wafv2-provider.ts
8544
8270
  /**
@@ -8830,12 +8556,6 @@ var WAFv2WebACLProvider = class {
8830
8556
  * Lookup order:
8831
8557
  * 1. `--resource <id>=<arn>` override → verify with `GetWebACL` (parses
8832
8558
  * Name/Id/Scope back out of the ARN).
8833
- * 2. Walk `ListWebACLs(Scope)` → match `aws:cdk:path` via
8834
- * `ListTagsForResource(ResourceARN)` (returns
8835
- * `TagInfoForResource.TagList`, standard `Key`/`Value` shape).
8836
- *
8837
- * `Scope` is required by `ListWebACLs` — read from
8838
- * `Properties.Scope` (`REGIONAL` is the default).
8839
8559
  */
8840
8560
  async import(input) {
8841
8561
  if (input.knownPhysicalId) try {
@@ -8853,31 +8573,7 @@ var WAFv2WebACLProvider = class {
8853
8573
  if (err instanceof WAFNonexistentItemException) return null;
8854
8574
  throw err;
8855
8575
  }
8856
- const scope = input.properties["Scope"] || "REGIONAL";
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
8868
- };
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
- };
8576
+ return null;
8881
8577
  }
8882
8578
  };
8883
8579
 
@@ -11343,7 +11039,7 @@ var CloudControlProvider = class {
11343
11039
  this.logger.debug(`Deleting resource ${logicalId} (${resourceType}), physical ID: ${physicalId}`);
11344
11040
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
11345
11041
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
11346
- const { ASGProvider } = await import("./asg-provider-4KjJhMTY.js").then((n) => n.n);
11042
+ const { ASGProvider } = await import("./asg-provider-BhmMKcnJ.js").then((n) => n.n);
11347
11043
  await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
11348
11044
  return;
11349
11045
  }
@@ -15673,12 +15369,6 @@ var IAMRoleProvider = class {
15673
15369
  * Lookup order:
15674
15370
  * 1. `--resource` override or `Properties.RoleName` → use directly,
15675
15371
  * verify via `GetRole`.
15676
- * 2. `ListRoles` + `ListRoleTags`, match `aws:cdk:path` tag.
15677
- *
15678
- * `ListRoles` is paginated and IAM is global (no region scoping), so this
15679
- * walks every role in the account once. Acceptable for the cardinalities
15680
- * we expect (typically <100 roles per account); larger accounts may want
15681
- * to provide `--resource` overrides instead.
15682
15372
  */
15683
15373
  async import(input) {
15684
15374
  const explicit = resolveExplicitPhysicalId(input, "RoleName");
@@ -15692,32 +15382,7 @@ var IAMRoleProvider = class {
15692
15382
  if (err instanceof NoSuchEntityException) return null;
15693
15383
  throw err;
15694
15384
  }
15695
- const match = await importTagWalk({
15696
- cdkPath: input.cdkPath,
15697
- logicalId: input.logicalId,
15698
- listPage: async (marker) => {
15699
- const list = await this.iamClient.send(new ListRolesCommand({ ...marker && { Marker: marker } }));
15700
- return {
15701
- items: list.Roles,
15702
- nextMarker: list.IsTruncated ? list.Marker : void 0
15703
- };
15704
- },
15705
- describe: async (role) => {
15706
- if (!role.RoleName) return void 0;
15707
- try {
15708
- return await this.iamClient.send(new ListRoleTagsCommand({ RoleName: role.RoleName }));
15709
- } catch (err) {
15710
- if (err instanceof NoSuchEntityException) return void 0;
15711
- throw err;
15712
- }
15713
- },
15714
- tagsOf: (tags) => tags.Tags
15715
- });
15716
- if (!match) return null;
15717
- return {
15718
- physicalId: match.summary.RoleName,
15719
- attributes: {}
15720
- };
15385
+ return null;
15721
15386
  }
15722
15387
  };
15723
15388
  /**
@@ -16213,6 +15878,167 @@ function computeImplicitDeleteEdges(resources) {
16213
15878
  return edges;
16214
15879
  }
16215
15880
 
15881
+ //#endregion
15882
+ //#region src/deployment/retryable-errors.ts
15883
+ /**
15884
+ * Patterns that mark an AWS error as a transient/retryable failure.
15885
+ * Each entry is a substring match against the error message; all of these
15886
+ * are situations where the same call typically succeeds after a short delay
15887
+ * because of eventual consistency or just-created-dependency propagation.
15888
+ */
15889
+ const RETRYABLE_ERROR_MESSAGE_PATTERNS = [
15890
+ "cannot be assumed",
15891
+ "Firehose is unable to assume role",
15892
+ "is unable to assume provided role",
15893
+ "role defined for the function",
15894
+ "not authorized to perform",
15895
+ "execution role",
15896
+ "trust policy",
15897
+ "Role validation failed",
15898
+ "does not have required permissions",
15899
+ "Trusted Entity",
15900
+ "currently in the following state: Pending",
15901
+ "has dependencies and cannot be deleted",
15902
+ "can't be deleted since it has",
15903
+ "DependencyViolation",
15904
+ "does not exist",
15905
+ "Schema is currently being altered",
15906
+ "Invalid principal in policy",
15907
+ "Policy Error: PrincipalNotFound",
15908
+ "Invalid value for the parameter Policy",
15909
+ "required permissions for: ENHANCED_MONITORING",
15910
+ "Caught ServiceAccessDeniedException",
15911
+ "permissions required to assume the role",
15912
+ "authorized to assume the provided role",
15913
+ "conflicting conditional operation",
15914
+ "scheduled for deletion",
15915
+ "Cannot access stream",
15916
+ "Please ensure the role can perform",
15917
+ "KMS key is invalid for CreateGrant",
15918
+ "Policy contains a statement with one or more invalid principals",
15919
+ "Invalid IAM Instance Profile",
15920
+ "Invalid InstanceProfile",
15921
+ "Failed to authorize instance profile",
15922
+ "Could not deliver test message",
15923
+ "wait 60 seconds",
15924
+ "concurrent update operation",
15925
+ "because it is in use",
15926
+ "Rate exceeded"
15927
+ ];
15928
+ /**
15929
+ * HTTP status codes that always indicate a transient failure worth retrying.
15930
+ * 429 = Too Many Requests (throttle), 503 = Service Unavailable.
15931
+ */
15932
+ const RETRYABLE_HTTP_STATUS_CODES = /* @__PURE__ */ new Set([429, 503]);
15933
+ /**
15934
+ * AWS SDK v3 canonical throttling error names. Mirrors
15935
+ * `@aws-sdk/service-error-classification`'s `THROTTLING_ERROR_CODES` — any
15936
+ * error (or wrapped cause) whose `name` is one of these is a transient rate-
15937
+ * limit rejection worth retrying with backoff. Detecting by NAME is more
15938
+ * robust than by HTTP status because most AWS throttles surface as HTTP 400
15939
+ * (not 429) with the throttling signal carried only in the error code / name
15940
+ * (e.g. SSM `ThrottlingException` for the `Rate exceeded` message).
15941
+ */
15942
+ const THROTTLING_ERROR_NAMES = /* @__PURE__ */ new Set([
15943
+ "BandwidthLimitExceeded",
15944
+ "EC2ThrottledException",
15945
+ "LimitExceededException",
15946
+ "PriorRequestNotComplete",
15947
+ "ProvisionedThroughputExceededException",
15948
+ "RequestLimitExceeded",
15949
+ "RequestThrottled",
15950
+ "RequestThrottledException",
15951
+ "SlowDown",
15952
+ "ThrottledException",
15953
+ "Throttling",
15954
+ "ThrottlingException",
15955
+ "TooManyRequestsException",
15956
+ "TransactionInProgressException"
15957
+ ]);
15958
+ /**
15959
+ * Walk the error + its `.cause` chain (bounded) looking for a rate-limit
15960
+ * signal — either an AWS SDK v3 throttling error `name`
15961
+ * ({@link THROTTLING_ERROR_NAMES}) or a retryable HTTP status
15962
+ * ({@link RETRYABLE_HTTP_STATUS_CODES}) on `$metadata`.
15963
+ *
15964
+ * cdkd wraps the original AWS error in a `ProvisioningError`, so the signal is
15965
+ * typically one cause-link deep; the bounded walk also tolerates SDK errors
15966
+ * that nest a `$response`/cause without exploding on a cyclic chain.
15967
+ *
15968
+ * BOTH signals are checked at EVERY depth. An earlier version checked the name
15969
+ * to depth 5 but the HTTP status only at depths 0 and 1, so a 429 nested two
15970
+ * links deep was missed.
15971
+ */
15972
+ function isThrottlingError(error) {
15973
+ let current = error;
15974
+ for (let depth = 0; depth < 5 && current != null; depth++) {
15975
+ const name = current.name;
15976
+ if (typeof name === "string" && THROTTLING_ERROR_NAMES.has(name)) return true;
15977
+ const status = current.$metadata?.httpStatusCode;
15978
+ if (status !== void 0 && RETRYABLE_HTTP_STATUS_CODES.has(status)) return true;
15979
+ current = current.cause;
15980
+ }
15981
+ return false;
15982
+ }
15983
+ /**
15984
+ * Determine whether an AWS error should be retried.
15985
+ *
15986
+ * Checks (in order):
15987
+ * 1. Rate-limit signal on the error or any wrapped cause — throttling error
15988
+ * `name` or retryable HTTP status (most AWS throttles are HTTP 400, not
15989
+ * 429, so the name check carries most of the weight). See
15990
+ * {@link isThrottlingError}.
15991
+ * 2. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
15992
+ */
15993
+ function isRetryableTransientError(error, message) {
15994
+ if (isThrottlingError(error)) return true;
15995
+ return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
15996
+ }
15997
+
15998
+ //#endregion
15999
+ //#region src/deployment/retry.ts
16000
+ /**
16001
+ * Retry helper for resource provisioning operations that hit transient
16002
+ * AWS eventual-consistency errors (IAM propagation, Lambda Pending state,
16003
+ * dependency violations, etc.).
16004
+ *
16005
+ * Extracted from DeployEngine so the backoff schedule can be unit-tested
16006
+ * in isolation. The retryable-error classifier itself lives in
16007
+ * `./retryable-errors.ts`.
16008
+ */
16009
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16010
+ /**
16011
+ * Run `operation`, retrying transient failures with exponential backoff
16012
+ * capped at `maxDelayMs`.
16013
+ *
16014
+ * Backoff at the defaults (initialDelayMs=1_000, maxDelayMs=8_000, maxRetries=8):
16015
+ * 1s -> 2s -> 4s -> 8s -> 8s -> 8s -> 8s -> 8s (cumulative 47s)
16016
+ *
16017
+ * Non-retryable errors are rethrown immediately. The transient-error
16018
+ * classifier is `isRetryableTransientError` from ./retryable-errors.ts.
16019
+ */
16020
+ async function withRetry(operation, logicalId, opts = {}) {
16021
+ const maxRetries = opts.maxRetries ?? 8;
16022
+ const initialDelayMs = opts.initialDelayMs ?? 1e3;
16023
+ const maxDelayMs = opts.maxDelayMs ?? 8e3;
16024
+ const sleep = opts.sleep ?? defaultSleep;
16025
+ let lastError;
16026
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
16027
+ return await operation();
16028
+ } catch (error) {
16029
+ lastError = error;
16030
+ const message = error instanceof Error ? error.message : String(error);
16031
+ if (!(opts.isRetryable ? opts.isRetryable(message, error) : isRetryableTransientError(error, message)) || attempt >= maxRetries) throw error;
16032
+ const delay = Math.min(initialDelayMs * Math.pow(2, attempt), maxDelayMs);
16033
+ opts.logger?.debug(` ⏳ Retrying ${logicalId} in ${delay / 1e3}s (attempt ${attempt + 1}/${maxRetries}) - ${message}`);
16034
+ for (let waited = 0; waited < delay; waited += 1e3) {
16035
+ if (opts.isInterrupted?.()) throw opts.onInterrupted ? opts.onInterrupted() : /* @__PURE__ */ new Error("Interrupted");
16036
+ await sleep(Math.min(1e3, delay - waited));
16037
+ }
16038
+ }
16039
+ throw lastError;
16040
+ }
16041
+
16216
16042
  //#endregion
16217
16043
  //#region src/deployment/resource-deadline.ts
16218
16044
  /**
@@ -17447,8 +17273,9 @@ var DeployEngine = class {
17447
17273
  const recreateFlagged = recreateViaCcApi || recreateViaSdkProvider;
17448
17274
  const needsReplacement = propertyDrivenReplacement || recreateFlagged;
17449
17275
  const dependencies = this.extractAllDependencies(template, logicalId);
17276
+ const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
17450
17277
  if (needsReplacement) {
17451
- if (propertyDrivenReplacement && !recreateFlagged) {
17278
+ if (propertyDrivenReplacement && !recreateFlagged && updateReplacePolicy !== "Retain") {
17452
17279
  const statefulReason = isStatefulRecreateTargetForReplace(resourceType, currentProps);
17453
17280
  if (statefulReason && this.options.forceStatefulRecreation !== true) {
17454
17281
  const immutableProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
@@ -17468,7 +17295,6 @@ var DeployEngine = class {
17468
17295
  });
17469
17296
  const replaceProvider = replaceDecision.provider;
17470
17297
  const replaceProps = replaceDecision.provisionedBy === "cc-api" ? this.preparePropertiesForCcApi(resourceType, resolvedProps, logicalId) : resolvedProps;
17471
- const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
17472
17298
  const oldDeleteProvider = this.providerRegistry.getProviderFor({
17473
17299
  resourceType,
17474
17300
  provisionedBy: currentResource.provisionedBy
@@ -17860,5 +17686,5 @@ var DeployEngine = class {
17860
17686
  };
17861
17687
 
17862
17688
  //#endregion
17863
- export { AssetModeResolver as $, StackTerminationProtectionError as $t, importTagWalk as A, expectedOwnerParam as At, DagBuilder as B, ConfigError as Bt, slowCcOperationTimeoutMs as C, resolveUseCdkBootstrapAssets as Ct, cfnRefValueFromPhysicalId as D, MIGRATE_TMP_PREFIX as Dt, IntrinsicFunctionResolver as E, CFN_TEMPLATE_URL_LIMIT as Et, normalizeAwsTagsToCfn as F, getAwsClients as Ft, shouldRetainResource as G, LockError as Gt, LockManager as H, LocalInvokeBuildError as Ht, resolveExplicitPhysicalId as I, resetAwsClients as It, WorkGraph as J, PartialFailureError as Jt, AssetPublisher as K, MissingCdkCliError as Kt, assertRegionMatch as L, setAwsClients as Lt, isRetryableTransientError as M, clearBucketRegionCache as Mt, CDK_PATH_TAG as N, resolveBucketRegion as Nt, refStateLookupFromResource as O, findLargeInlineResources as Ot, matchesCdkPath as P, AwsClients as Pt, rewriteTemplateAssetReferences as Q, StackHasActiveImportsError as Qt, applyRoleArnIfSet as R, AssetError as Rt, CloudControlProvider as S, resolveStateBucketWithDefaultAndSource as St, isTerminationProtectionPropagationError as T, CFN_TEMPLATE_BODY_LIMIT as Tt, S3StateBackend as U, LocalMigrateError as Ut, TemplateParser as V, DependencyError as Vt, rebuildClientForBucketRegion as W, LocalStartServiceError as Wt, createAssetRedirectResolver as X, ResourceTimeoutError as Xt, buildAssetRedirectMap as Y, ProvisioningError as Yt, loadPublishableAssetManifest as Z, ResourceUpdateNotSupportedError as Zt, yellow as _, resolveApp as _t, IMPLICIT_DELETE_DEPENDENCIES as a, withErrorHandling as an, validateContainerRepoName as at, ProviderRegistry as b, resolveSkipPrefix as bt, MULTI_REGION_RECREATE_BLOCKED_TYPES as c, getDockerCmd as ct, formatResourceLine as d, AssetManifestLoader as dt, StateError as en, BOOTSTRAP_MARKER_PREFIX as et, bold as f, getDockerImageBySourceHash as ft, red as g, getLegacyStateBucketName as gt, green as h, getDefaultStateBucketName as ht, withResourceDeadline as i, normalizeAwsError as in, validateAssetBucketName as it, withRetry as j, AssemblyReader as jt, WAFv2WebACLProvider as k, uploadCfnTemplate as kt, isStatefulRecreateTargetSync as l, runDockerForeground as lt, gray as m, synthesisStatusMessage as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatError as nn, getBootstrapMarkerKey as nt, computeImplicitDeleteEdges as o, __exportAll as on, buildDockerImage as ot, cyan as p, Synthesizer as pt, stringifyValue as q, NestedStackChildDirectDestroyError as qt, DeployEngine as r, isCdkdError as rn, parseBootstrapMarker as rt, extractDeploymentEventError as s, formatDockerLoginError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, SynthesisError as tn, ensureAssetStorage as tt, renderStatefulReason as u, runDockerStreaming as ut, IAMRoleProvider as v, resolveAutoAssetStorage as vt, disableInstanceApiTermination as w, warnDeprecatedNoPrefixCliFlag as wt, findActionableSilentDrops as x, resolveStateBucketWithDefault as xt, collectInlinePolicyNamesManagedBySiblings as y, resolveCaptureObservedState as yt, DiffCalculator as z, CdkdError as zt };
17864
- //# sourceMappingURL=deploy-engine-wmvXbyoB.js.map
17689
+ export { getBootstrapMarkerKey as $, formatError as $t, refStateLookupFromResource as A, resolveBucketRegion as At, S3StateBackend as B, LocalMigrateError as Bt, findActionableSilentDrops as C, CFN_TEMPLATE_URL_LIMIT as Ct, isTerminationProtectionPropagationError as D, expectedOwnerParam as Dt, disableInstanceApiTermination as E, uploadCfnTemplate as Et, applyRoleArnIfSet as F, AssetError as Ft, WorkGraph as G, PartialFailureError as Gt, shouldRetainResource as H, LockError as Ht, DiffCalculator as I, CdkdError as It, loadPublishableAssetManifest as J, ResourceUpdateNotSupportedError as Jt, buildAssetRedirectMap as K, ProvisioningError as Kt, DagBuilder as L, ConfigError as Lt, normalizeAwsTagsToCfn as M, getAwsClients as Mt, resolveExplicitPhysicalId as N, resetAwsClients as Nt, IntrinsicFunctionResolver as O, AssemblyReader as Ot, assertRegionMatch as P, setAwsClients as Pt, ensureAssetStorage as Q, SynthesisError as Qt, TemplateParser as R, DependencyError as Rt, ProviderRegistry as S, CFN_TEMPLATE_BODY_LIMIT as St, slowCcOperationTimeoutMs as T, findLargeInlineResources as Tt, AssetPublisher as U, MissingCdkCliError as Ut, rebuildClientForBucketRegion as V, LocalStartServiceError as Vt, stringifyValue as W, NestedStackChildDirectDestroyError as Wt, AssetModeResolver as X, StackTerminationProtectionError as Xt, rewriteTemplateAssetReferences as Y, StackHasActiveImportsError as Yt, BOOTSTRAP_MARKER_PREFIX as Z, StateError as Zt, green as _, resolveSkipPrefix as _t, withRetry as a, getDockerCmd as at, IAMRoleProvider as b, resolveUseCdkBootstrapAssets as bt, computeImplicitDeleteEdges as c, AssetManifestLoader as ct, isStatefulRecreateTargetSync as d, synthesisStatusMessage as dt, isCdkdError as en, parseBootstrapMarker as et, renderStatefulReason as f, getDefaultStateBucketName as ft, gray as g, resolveCaptureObservedState as gt, cyan as h, resolveAutoAssetStorage as ht, withResourceDeadline as i, formatDockerLoginError as it, WAFv2WebACLProvider as j, AwsClients as jt, cfnRefValueFromPhysicalId as k, clearBucketRegionCache as kt, extractDeploymentEventError as l, getDockerImageBySourceHash as lt, bold as m, resolveApp as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, withErrorHandling as nn, validateContainerRepoName as nt, isRetryableTransientError as o, runDockerForeground as ot, formatResourceLine as p, getLegacyStateBucketName as pt, createAssetRedirectResolver as q, ResourceTimeoutError as qt, DeployEngine as r, __exportAll as rn, buildDockerImage as rt, IMPLICIT_DELETE_DEPENDENCIES as s, runDockerStreaming as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, normalizeAwsError as tn, validateAssetBucketName as tt, MULTI_REGION_RECREATE_BLOCKED_TYPES as u, Synthesizer as ut, red as v, resolveStateBucketWithDefault as vt, CloudControlProvider as w, MIGRATE_TMP_PREFIX as wt, collectInlinePolicyNamesManagedBySiblings as x, warnDeprecatedNoPrefixCliFlag as xt, yellow as y, resolveStateBucketWithDefaultAndSource as yt, LockManager as z, LocalInvokeBuildError as zt };
17690
+ //# sourceMappingURL=deploy-engine-DMpr3qtv.js.map