@go-to-k/cdkd 0.283.28 → 0.283.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-DzaowTd_.js → asg-provider-BpVWOeU3.js} +2 -2
- package/dist/{asg-provider-DzaowTd_.js.map → asg-provider-BpVWOeU3.js.map} +1 -1
- package/dist/cli.js +388 -161
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-5rA6lMn8.js → deploy-engine-Bb4ECtz3.js} +3 -3
- package/dist/{deploy-engine-5rA6lMn8.js.map → deploy-engine-Bb4ECtz3.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as coerceCfnBoolean, $t as resolveApp, A as cyan, An as LocalInvokeBuildError$1, At as createAssetRedirectResolver, B as findSilentDropProperties, Bn as StackHasActiveImportsError, Bt as validateContainerRepoName, C as makeCanonicalizePropertiesFn, Cn as resetAwsClients, Ct as S3StateBackend, D as renderStatefulReason, Dt as stringifyValue, E as isStatefulRecreateTargetSync, En as CdkdError, Et as AssetPublisher, F as IAMRoleProvider, Fn as NestedStackChildDirectDestroyError, Ft as BOOTSTRAP_MARKER_PREFIX, G as IntrinsicFunctionResolver, Gt as runDockerForeground, H as slowCcOperationTimeoutMs, Ht as buildDockerImage, I as collectInlinePolicyNamesManagedBySiblings, In as PartialFailureError, It as ensureAssetStorage, J as refStateLookupFromResource, Jn as isMarkedNonRetryable, Jt as getDockerImageBySourceHash, K as cfnRefValueFromPhysicalId, Kn as normalizeAwsError, Kt as runDockerStreaming, L as clearOnUpdateRemoval, Ln as ProvisioningError, Lt as getBootstrapMarkerKey, M as green, Mn as LocalStartServiceError, Mt as rewriteTemplateAssetReferences, N as red, Nt as escapeRegExp, O as formatResourceLine, Ot as WorkGraph, P as yellow, Pn as MissingCdkCliError, Pt as AssetModeResolver, Q as assertRegionMatch, Qt as getLegacyStateBucketName, R as ProviderRegistry, Rn as ResourceTimeoutError, Rt as parseBootstrapMarker, S as unsupportedFinalSnapshotError, Sn as getAwsClients, St as LockManager, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tt as shouldRetainResource, U as disableInstanceApiTermination, Ut as formatDockerLoginError, V as CloudControlProvider, Vn as StackTerminationProtectionError, Vt as buildDenyExternalAccessPolicy, W as isTerminationProtectionPropagationError, Wt as getDockerCmd, X as normalizeAwsTagsToCfn, Xn as isThrottlingError, Xt as synthesisStatusMessage, Y as WAFv2WebACLProvider, Yn as isRetryableTransientError, Yt as Synthesizer, Z as resolveExplicitPhysicalId, Zn as markNonRetryable, Zt as getDefaultStateBucketName, _ as buildFinalSnapshotIdentifier, _n as AssemblyReader, _t as INTRINSIC_KEYS, a as DeploymentEventsStore, an as resolveUseCdkBootstrapAssets, at as requireConfigObject, b as isFinalSnapshotError, bn as resolveBucketRegion, bt as DagBuilder, c as replayFailedOperations, cn as CFN_TEMPLATE_BODY_LIMIT, ct as redactSecretsForState, d as deleteSkipReason, dn as findLargeInlineResources, dt as s3BucketDomainName, en as resolveAutoAssetStorage, et as configBooleanRefusal, f as withResourceDeadline, fn as uploadCfnTemplate, ft as s3BucketDualStackDomainName, g as PRE_DELETE_SNAPSHOT_TYPES, gn as derivePartitionAndUrlSuffix, gt as DiffCalculator, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as canonicalizeRegion, ht as applyRoleArnIfSet, i as DeploymentEventsReader, in as resolveStateBucketWithDefaultAndSource, it as requireConfigArray, j as gray, jn as LocalMigrateError, jt as loadPublishableAssetManifest, k as bold, kn as DeployCancelledError, kt as buildAssetRedirectMap, l as replayRollback, ln as CFN_TEMPLATE_URL_LIMIT, lt as scrubResourceRecord, m as computeImplicitDeleteEdges, mn as PARTITION_TABLE, mt as s3BucketWebsiteUrl, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveSkipPrefix, nt as readConfigString, o as planFailedOps, on as stateBucketExistenceConfirmed, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as expectedOwnerParam, pt as s3BucketRegionalDomainName, q as getAccountInfo, qn as withErrorHandling, qt as AssetManifestLoader, r as DeployEngine, rn as resolveStateBucketWithDefault, rt as replayWarn, s as planRollback, sn as warnDeprecatedNoPrefixCliFlag, st as TEMPLATE_SOURCED_READBACK_RULES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveCaptureObservedState, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as MIGRATE_TMP_PREFIX, ut as s3BucketArn, v as ccRoutedFinalSnapshotError, vn as processStackMessages, vt as describeTypeWithThrottleRetry, w as extractDeploymentEventError, wn as setAwsClients, wt as rebuildClientForBucketRegion, x as refusesFinalSnapshot, xn as AwsClients, xt as TemplateParser, y as createPreDeleteFinalSnapshot, yt as withRetry, z as findActionableSilentDrops, zn as ResourceUpdateNotSupportedError, zt as validateAssetBucketName } from "./deploy-engine-
|
|
2
|
+
import { $ as coerceCfnBoolean, $t as resolveApp, A as cyan, An as LocalInvokeBuildError$1, At as createAssetRedirectResolver, B as findSilentDropProperties, Bn as StackHasActiveImportsError, Bt as validateContainerRepoName, C as makeCanonicalizePropertiesFn, Cn as resetAwsClients, Ct as S3StateBackend, D as renderStatefulReason, Dt as stringifyValue, E as isStatefulRecreateTargetSync, En as CdkdError, Et as AssetPublisher, F as IAMRoleProvider, Fn as NestedStackChildDirectDestroyError, Ft as BOOTSTRAP_MARKER_PREFIX, G as IntrinsicFunctionResolver, Gt as runDockerForeground, H as slowCcOperationTimeoutMs, Ht as buildDockerImage, I as collectInlinePolicyNamesManagedBySiblings, In as PartialFailureError, It as ensureAssetStorage, J as refStateLookupFromResource, Jn as isMarkedNonRetryable, Jt as getDockerImageBySourceHash, K as cfnRefValueFromPhysicalId, Kn as normalizeAwsError, Kt as runDockerStreaming, L as clearOnUpdateRemoval, Ln as ProvisioningError, Lt as getBootstrapMarkerKey, M as green, Mn as LocalStartServiceError, Mt as rewriteTemplateAssetReferences, N as red, Nt as escapeRegExp, O as formatResourceLine, Ot as WorkGraph, P as yellow, Pn as MissingCdkCliError, Pt as AssetModeResolver, Q as assertRegionMatch, Qt as getLegacyStateBucketName, R as ProviderRegistry, Rn as ResourceTimeoutError, Rt as parseBootstrapMarker, S as unsupportedFinalSnapshotError, Sn as getAwsClients, St as LockManager, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tt as shouldRetainResource, U as disableInstanceApiTermination, Ut as formatDockerLoginError, V as CloudControlProvider, Vn as StackTerminationProtectionError, Vt as buildDenyExternalAccessPolicy, W as isTerminationProtectionPropagationError, Wt as getDockerCmd, X as normalizeAwsTagsToCfn, Xn as isThrottlingError, Xt as synthesisStatusMessage, Y as WAFv2WebACLProvider, Yn as isRetryableTransientError, Yt as Synthesizer, Z as resolveExplicitPhysicalId, Zn as markNonRetryable, Zt as getDefaultStateBucketName, _ as buildFinalSnapshotIdentifier, _n as AssemblyReader, _t as INTRINSIC_KEYS, a as DeploymentEventsStore, an as resolveUseCdkBootstrapAssets, at as requireConfigObject, b as isFinalSnapshotError, bn as resolveBucketRegion, bt as DagBuilder, c as replayFailedOperations, cn as CFN_TEMPLATE_BODY_LIMIT, ct as redactSecretsForState, d as deleteSkipReason, dn as findLargeInlineResources, dt as s3BucketDomainName, en as resolveAutoAssetStorage, et as configBooleanRefusal, f as withResourceDeadline, fn as uploadCfnTemplate, ft as s3BucketDualStackDomainName, g as PRE_DELETE_SNAPSHOT_TYPES, gn as derivePartitionAndUrlSuffix, gt as DiffCalculator, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as canonicalizeRegion, ht as applyRoleArnIfSet, i as DeploymentEventsReader, in as resolveStateBucketWithDefaultAndSource, it as requireConfigArray, j as gray, jn as LocalMigrateError, jt as loadPublishableAssetManifest, k as bold, kn as DeployCancelledError, kt as buildAssetRedirectMap, l as replayRollback, ln as CFN_TEMPLATE_URL_LIMIT, lt as scrubResourceRecord, m as computeImplicitDeleteEdges, mn as PARTITION_TABLE, mt as s3BucketWebsiteUrl, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveSkipPrefix, nt as readConfigString, o as planFailedOps, on as stateBucketExistenceConfirmed, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as expectedOwnerParam, pt as s3BucketRegionalDomainName, q as getAccountInfo, qn as withErrorHandling, qt as AssetManifestLoader, r as DeployEngine, rn as resolveStateBucketWithDefault, rt as replayWarn, s as planRollback, sn as warnDeprecatedNoPrefixCliFlag, st as TEMPLATE_SOURCED_READBACK_RULES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveCaptureObservedState, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as MIGRATE_TMP_PREFIX, ut as s3BucketArn, v as ccRoutedFinalSnapshotError, vn as processStackMessages, vt as describeTypeWithThrottleRetry, w as extractDeploymentEventError, wn as setAwsClients, wt as rebuildClientForBucketRegion, x as refusesFinalSnapshot, xn as AwsClients, xt as TemplateParser, y as createPreDeleteFinalSnapshot, yt as withRetry, z as findActionableSilentDrops, zn as ResourceUpdateNotSupportedError, zt as validateAssetBucketName } from "./deploy-engine-Bb4ECtz3.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-BpVWOeU3.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -14192,6 +14192,274 @@ function isWarmThroughputDecrease(desired, live) {
|
|
|
14192
14192
|
return sawDecrease;
|
|
14193
14193
|
}
|
|
14194
14194
|
|
|
14195
|
+
//#endregion
|
|
14196
|
+
//#region src/provisioning/dynamodb-index-busy-delete.ts
|
|
14197
|
+
/**
|
|
14198
|
+
* The index-busy `DeleteTable` rule that `AWS::DynamoDB::Table` and
|
|
14199
|
+
* `AWS::DynamoDB::GlobalTable` both need, in ONE spelling.
|
|
14200
|
+
*
|
|
14201
|
+
* AWS refuses a `DeleteTable` while any of the table's global secondary
|
|
14202
|
+
* indexes is mid-transition:
|
|
14203
|
+
*
|
|
14204
|
+
* Attempt to change a resource which is still in use: Cannot delete table
|
|
14205
|
+
* while indexes are being created, updated, or deleted.
|
|
14206
|
+
*
|
|
14207
|
+
* The condition is TRANSIENT and self-resolving — a real
|
|
14208
|
+
* `dynamodb-globaltable` destroy (us-east-1, 2026-08-13) lost one table to it
|
|
14209
|
+
* and the very next `cdkd destroy` succeeded with no other change — and
|
|
14210
|
+
* application auto-scaling can start an index capacity change at any moment,
|
|
14211
|
+
* so any table with an autoscaled GSI can hit it. Surfacing it as a hard
|
|
14212
|
+
* `PartialFailureError` with state preserved makes the user re-run a destroy
|
|
14213
|
+
* for something that clears itself in seconds.
|
|
14214
|
+
*
|
|
14215
|
+
* **Provenance, stated precisely.** The rule shipped for
|
|
14216
|
+
* `AWS::DynamoDB::GlobalTable` first (issue #1830, PR #1930), provider-locally
|
|
14217
|
+
* — deliberately, since `src/deployment/retryable-errors.ts` was owned by
|
|
14218
|
+
* another lane at the time. Issue #1931 is the sibling `AWS::DynamoDB::Table`
|
|
14219
|
+
* gap that PR left: its `delete()` had no retry at all. Rather than write the
|
|
14220
|
+
* classifier, the budget and the warning a second time, they were LIFTED here
|
|
14221
|
+
* and both providers now read them.
|
|
14222
|
+
*
|
|
14223
|
+
* What did NOT change for `GlobalTable` is its AWS-FACING behaviour: the call
|
|
14224
|
+
* order, the budgets, the #1521 pre-delete gate and the replica teardown are
|
|
14225
|
+
* identical. Two LOG LINES did change, deliberately, and the claim is scoped
|
|
14226
|
+
* rather than blanket because a blanket one is falsifiable by the diff: the
|
|
14227
|
+
* unclassified-error arm now names the error CLASS at warn and keeps AWS's raw
|
|
14228
|
+
* text at debug (it leaked an account id and an assumed-role ARN at default
|
|
14229
|
+
* verbosity), and {@link indexBusyRetryWarning} was reworded so its own
|
|
14230
|
+
* arithmetic reads consistently. That provider's suite was updated to match.
|
|
14231
|
+
*
|
|
14232
|
+
* Living here rather than in either provider is the point: the classifier is a
|
|
14233
|
+
* single regex whose PRECISION is the whole safety argument (see
|
|
14234
|
+
* {@link INDEX_BUSY_DELETE_MESSAGE}), and the retry budget only makes sense
|
|
14235
|
+
* read together with the re-arm poll it multiplies. Two files spelling either
|
|
14236
|
+
* independently is two chances for a later "fix" to move one of them while the
|
|
14237
|
+
* sibling type silently keeps the old answer. Same class as
|
|
14238
|
+
* `dynamodb-warm-throughput.ts`, and the same reason.
|
|
14239
|
+
*
|
|
14240
|
+
* Deliberately NOT here: what only one provider has. `GlobalTable` keeps its
|
|
14241
|
+
* pre-delete #1521 gate (a 15-minute settle wait ahead of the retry loop, sized
|
|
14242
|
+
* for a wait that runs once) and its replica / auto-scaling teardown; the
|
|
14243
|
+
* `Table` provider has neither. What IS shared is everything a second
|
|
14244
|
+
* implementation would otherwise re-derive: the predicate, the classifier, the
|
|
14245
|
+
* two budgets, the proceed note, the warning text and the retry loop itself.
|
|
14246
|
+
*
|
|
14247
|
+
* Issues: #1830 / #1521 (GlobalTable, PR #1930), #1931 (Table).
|
|
14248
|
+
*/
|
|
14249
|
+
/**
|
|
14250
|
+
* The GSI states during which AWS refuses another `UpdateTable` or a
|
|
14251
|
+
* `DeleteTable` on the table (issue #1521) — a table is ACTIVE while one of
|
|
14252
|
+
* its indexes is still transitioning, which is what made the serialized
|
|
14253
|
+
* update sequence race and what left a table ORPHANED when the follow-up
|
|
14254
|
+
* destroy hit the same rule.
|
|
14255
|
+
*
|
|
14256
|
+
* `DELETING` is included because a dropped index keeps reporting until it is
|
|
14257
|
+
* actually gone. The test is on the TRANSITIONAL values rather than on
|
|
14258
|
+
* `!== 'ACTIVE'`: an absent or unrecognized status must not park the caller
|
|
14259
|
+
* for its whole cap on a table that is fine.
|
|
14260
|
+
*/
|
|
14261
|
+
const INDEX_TRANSITIONAL_STATUSES = /* @__PURE__ */ new Set([
|
|
14262
|
+
"CREATING",
|
|
14263
|
+
"UPDATING",
|
|
14264
|
+
"DELETING"
|
|
14265
|
+
]);
|
|
14266
|
+
function hasTransitionalIndex(indexes) {
|
|
14267
|
+
return (indexes ?? []).some((gsi) => INDEX_TRANSITIONAL_STATUSES.has(gsi.IndexStatus ?? ""));
|
|
14268
|
+
}
|
|
14269
|
+
/**
|
|
14270
|
+
* AWS's refusal when `DeleteTable` lands while one of the table's GSIs is
|
|
14271
|
+
* mid-transition (issue #1830). Observed verbatim on a real destroy:
|
|
14272
|
+
*
|
|
14273
|
+
* Attempt to change a resource which is still in use: Cannot delete table
|
|
14274
|
+
* while indexes are being created, updated, or deleted.
|
|
14275
|
+
*
|
|
14276
|
+
* Matched on the leading clause rather than on the whole sentence: the
|
|
14277
|
+
* trailing status list is AWS wording that can gain a state without changing
|
|
14278
|
+
* what the message MEANS, and the leading clause is already specific enough
|
|
14279
|
+
* that nothing else in the DynamoDB surface produces it. Case-insensitive
|
|
14280
|
+
* because the text arrives inside a wrapper prefix whose casing is not ours.
|
|
14281
|
+
*
|
|
14282
|
+
* Deliberately NOT keyed on the exception NAME: AWS reports this as a plain
|
|
14283
|
+
* `ResourceInUseException`, which is the same name it uses for genuinely
|
|
14284
|
+
* terminal conflicts (deleting a table that is `CREATING`, creating one that
|
|
14285
|
+
* already exists). Retrying on the name would turn those into a 47s stall
|
|
14286
|
+
* before the same failure.
|
|
14287
|
+
*/
|
|
14288
|
+
const INDEX_BUSY_DELETE_MESSAGE = /cannot delete table while indexes are being/i;
|
|
14289
|
+
function isIndexBusyDeleteError(message) {
|
|
14290
|
+
return INDEX_BUSY_DELETE_MESSAGE.test(message);
|
|
14291
|
+
}
|
|
14292
|
+
/**
|
|
14293
|
+
* Retry budget for the index-busy `DeleteTable` refusal (issue #1830).
|
|
14294
|
+
*
|
|
14295
|
+
* Left at `withRetry`'s own default of 8 rather than raised. Named rather than
|
|
14296
|
+
* inlined because passing it at all is what opts this call out of
|
|
14297
|
+
* `withRetry`'s dense IAM-propagation schedule, which would be wrong for a
|
|
14298
|
+
* condition measured in seconds-to-minutes rather than sub-second.
|
|
14299
|
+
*
|
|
14300
|
+
* The count MULTIPLIES the per-attempt re-arm poll below, so the two have to
|
|
14301
|
+
* be read together — see {@link DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS} for the
|
|
14302
|
+
* arithmetic and for why the poll is not a 15-minute one.
|
|
14303
|
+
*/
|
|
14304
|
+
const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
|
|
14305
|
+
/**
|
|
14306
|
+
* `DescribeTable` polls (~1s apart) the index-busy `DeleteTable` retry spends
|
|
14307
|
+
* re-arming BEFORE each retry (issue #1830) — it returns on the first one that
|
|
14308
|
+
* reports every index settled.
|
|
14309
|
+
*
|
|
14310
|
+
* Bounded well under the 15-minute default `GlobalTable`'s pre-delete #1521
|
|
14311
|
+
* gate uses, because THAT default is sized for a wait that runs once while
|
|
14312
|
+
* this one runs per retry: the caller's wall clock is
|
|
14313
|
+
* `DELETE_INDEX_BUSY_MAX_RETRIES x this + withRetry's ~47s of backoff`, and
|
|
14314
|
+
* `destroy-runner.ts` runs the delete under a per-resource deadline (30 min by
|
|
14315
|
+
* default; neither DynamoDB provider declares a `getMinResourceTimeoutMs` to
|
|
14316
|
+
* lift it). At 900 polls the product was ~2h, so a genuinely stuck index
|
|
14317
|
+
* produced a 30-minute wait ending in a generic `ResourceTimeoutError` that
|
|
14318
|
+
* never mentions indexes. At 60 the LOOP's worst case is ~8.8 min and the user
|
|
14319
|
+
* gets AWS's own actionable sentence instead.
|
|
14320
|
+
*
|
|
14321
|
+
* **What that deadline actually wraps**, since the arithmetic depends on it:
|
|
14322
|
+
* NOT one `delete()` but `destroy-runner.ts`'s whole outer retry loop, which
|
|
14323
|
+
* calls `delete()` up to 4 times (`maxAttempts = 3`) inside the one deadline.
|
|
14324
|
+
* Four times ~8.8 min would blow straight through 30 min. It does not, and the
|
|
14325
|
+
* reason is load-bearing rather than incidental: that outer loop only retries
|
|
14326
|
+
* when `isRetryableTransientError` says so, and this refusal matches no entry
|
|
14327
|
+
* in `RETRYABLE_ERROR_MESSAGE_PATTERNS` (verified against the real classifier —
|
|
14328
|
+
* both AWS's raw sentence and the `Failed to delete DynamoDB table ...` wrap
|
|
14329
|
+
* return false), so the outer loop runs `delete()` exactly ONCE for it and the
|
|
14330
|
+
* budget below is not re-multiplied. Adding this message to those patterns
|
|
14331
|
+
* would silently make the worst case ~35 min — over the deadline.
|
|
14332
|
+
*
|
|
14333
|
+
* The FIRST attempt is unaffected — it does not re-arm at all.
|
|
14334
|
+
*/
|
|
14335
|
+
const DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS = 60;
|
|
14336
|
+
/**
|
|
14337
|
+
* What proceeding costs when a DELETE-path index-settle wait ends WITHOUT
|
|
14338
|
+
* confirming — passed as {@link waitForIndexesSettled}'s `proceedNote` by every
|
|
14339
|
+
* delete-path caller (`GlobalTable`'s #1521 pre-delete gate, and the #1830 /
|
|
14340
|
+
* #1931 retry re-arm on both providers).
|
|
14341
|
+
*
|
|
14342
|
+
* The note is caller-supplied because the consequence genuinely differs: the
|
|
14343
|
+
* delete path has a backstop — `DeleteTable` follows immediately and AWS
|
|
14344
|
+
* refuses it again if the index is still transitioning — while
|
|
14345
|
+
* `GlobalTable`'s auto-scaling caller has none (`reconcileAutoScalingTargets`
|
|
14346
|
+
* skips an index that is not ready and nothing downstream notices). One
|
|
14347
|
+
* sentence per caller, so neither claims the other's safety net.
|
|
14348
|
+
*/
|
|
14349
|
+
const DELETE_INDEX_WAIT_PROCEED_NOTE = `the DeleteTable goes ahead anyway and AWS's own refusal stays the backstop.`;
|
|
14350
|
+
/**
|
|
14351
|
+
* Poll `DescribeTable` until no GSI is transitioning, bounded by
|
|
14352
|
+
* `maxAttempts` (~1s apart).
|
|
14353
|
+
*
|
|
14354
|
+
* **Best-effort by design**: on timeout it warns and returns rather than
|
|
14355
|
+
* throwing. Index backfill on a large table can take a long time, and a throw
|
|
14356
|
+
* here would fail a caller whose actual resources are all correct — on the
|
|
14357
|
+
* delete path it would STRAND the resource, which is the failure this whole
|
|
14358
|
+
* module exists to remove.
|
|
14359
|
+
*
|
|
14360
|
+
* `maxAttempts` and `proceedNote` are required rather than defaulted: the two
|
|
14361
|
+
* callers differ on both (a wait that runs ONCE versus one that runs per retry
|
|
14362
|
+
* and multiplies; a caller with a backstop versus one without), and a default
|
|
14363
|
+
* would silently give one caller the other's answer.
|
|
14364
|
+
*/
|
|
14365
|
+
async function waitForIndexesSettled(opts) {
|
|
14366
|
+
const { tableName, logicalId, logger, describeTable, maxAttempts, proceedNote } = opts;
|
|
14367
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
14368
|
+
try {
|
|
14369
|
+
if (!hasTransitionalIndex((await describeTable()).Table?.GlobalSecondaryIndexes ?? [])) return;
|
|
14370
|
+
} catch (err) {
|
|
14371
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
14372
|
+
if (isThrottlingError(err)) {
|
|
14373
|
+
logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
|
|
14374
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
14375
|
+
continue;
|
|
14376
|
+
}
|
|
14377
|
+
if (err instanceof ResourceNotFoundException$1) {
|
|
14378
|
+
logger.debug(`Table ${tableName} (${logicalId}) no longer exists while waiting for indexes`);
|
|
14379
|
+
return;
|
|
14380
|
+
}
|
|
14381
|
+
const errorName = err instanceof Error ? err.name : typeof err;
|
|
14382
|
+
logger.debug(`DescribeTable failed while waiting for indexes on ${tableName} (${logicalId}): ${message}`);
|
|
14383
|
+
logger.warn(`DescribeTable failed (${errorName}) while waiting for indexes on ${tableName} (${logicalId}), so cdkd stopped waiting; ${proceedNote} Re-run with --verbose for AWS's own message.`);
|
|
14384
|
+
return;
|
|
14385
|
+
}
|
|
14386
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
14387
|
+
}
|
|
14388
|
+
logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts} DescribeTable polls (~1s apart, so a little over ${maxAttempts}s of wall clock); ${proceedNote}`);
|
|
14389
|
+
}
|
|
14390
|
+
/**
|
|
14391
|
+
* The ONE line the retry prints at default verbosity, for both providers.
|
|
14392
|
+
*
|
|
14393
|
+
* `withRetry` announces its retries through `opts.logger?.debug`, so without
|
|
14394
|
+
* this a delete spending minutes re-polling printed nothing naming the cause —
|
|
14395
|
+
* and the bounded budget above can end in a timeout that has to be diagnosable
|
|
14396
|
+
* from an ordinary run's output.
|
|
14397
|
+
*
|
|
14398
|
+
* `attemptNumber` is the attempt now STARTING (2 for the first retry). The
|
|
14399
|
+
* remaining-attempt count is DERIVED from it and never spelled out: `withRetry`
|
|
14400
|
+
* runs `1 + maxRetries` attempts in total, and writing the number by hand is
|
|
14401
|
+
* how this line came to promise 8 further attempts when 7 were left.
|
|
14402
|
+
*
|
|
14403
|
+
* The per-attempt settle budget is attached to "this attempt plus the N more"
|
|
14404
|
+
* rather than to the N alone, because the re-arm runs before the attempt this
|
|
14405
|
+
* very line announces too — 8 re-arms for 7 remaining attempts. Saying "N more
|
|
14406
|
+
* attempts, each preceded by ..." made one sentence quote two different counts.
|
|
14407
|
+
*
|
|
14408
|
+
* It must NOT carry AWS's own sentence: the `dynamodb-globaltable` integ greps
|
|
14409
|
+
* the destroy log for `Cannot delete table while indexes are being` to prove
|
|
14410
|
+
* AWS really refused, and a cdkd-authored copy would satisfy that grep without
|
|
14411
|
+
* the refusal having happened.
|
|
14412
|
+
*/
|
|
14413
|
+
function indexBusyRetryWarning(opts) {
|
|
14414
|
+
const remainingAttempts = 9 - opts.attemptNumber;
|
|
14415
|
+
return `DynamoDB ${opts.typeLabel} ${opts.logicalId}: AWS refused DeleteTable on ${opts.physicalId} because a global secondary index is still being created, updated or deleted — application auto-scaling can start an index capacity change at any moment, including during this destroy. Waiting for the index to settle and retrying (this attempt plus up to ${remainingAttempts} more attempts; each one first waits up to ${60} DescribeTable polls ~1s apart — a little over a minute of settling — before its DeleteTable). Nothing is wrong with the table; a large index backfill can outlast this budget, in which case the destroy fails with AWS's own message and re-running it succeeds once the index is ACTIVE.`;
|
|
14416
|
+
}
|
|
14417
|
+
/**
|
|
14418
|
+
* Issue the caller's `DeleteTable`, retrying ONLY the transient index-busy
|
|
14419
|
+
* refusal and re-arming on the CONDITION between attempts.
|
|
14420
|
+
*
|
|
14421
|
+
* Retried here rather than by widening the shared
|
|
14422
|
+
* `RETRYABLE_ERROR_MESSAGE_PATTERNS`: the same `ResourceInUseException` text
|
|
14423
|
+
* also surfaces on CREATE / UPDATE paths where it is NOT self-clearing, and a
|
|
14424
|
+
* `DeleteTable` is the one call site that can legitimately wait it out. Same
|
|
14425
|
+
* scoping rationale as `isNameCooldownError` / `isRecreateRetryableError`.
|
|
14426
|
+
*
|
|
14427
|
+
* `reArm` runs BEFORE every retry and never before the first attempt. It is the
|
|
14428
|
+
* caller's because the poll needs the caller's client, but every caller is
|
|
14429
|
+
* expected to bound it with {@link DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS}: an
|
|
14430
|
+
* index backfill outlasts any fixed backoff grid, while a settle poll returns
|
|
14431
|
+
* on its first `DescribeTable` once the index is ACTIVE.
|
|
14432
|
+
*
|
|
14433
|
+
* `sleepSeam` is read per SLEEP rather than once when the options bag is built,
|
|
14434
|
+
* so a test that installs the no-op from INSIDE the first refusal — i.e. after
|
|
14435
|
+
* `delete()` has already built the bag — still gets it. A value captured at
|
|
14436
|
+
* construction time would be `undefined` there and would silently pay
|
|
14437
|
+
* `withRetry`'s real ~47s schedule. Passing `sleep` unconditionally is safe:
|
|
14438
|
+
* `withRetry`'s dense-schedule detection keys on `maxRetries` /
|
|
14439
|
+
* `initialDelayMs` / `maxDelayMs` / `isRetryable`, all of which this caller
|
|
14440
|
+
* already sets.
|
|
14441
|
+
*/
|
|
14442
|
+
async function deleteTableWithIndexBusyRetry(opts) {
|
|
14443
|
+
let deleteAttempts = 0;
|
|
14444
|
+
await withRetry(async () => {
|
|
14445
|
+
if (deleteAttempts++ > 0) {
|
|
14446
|
+
if (deleteAttempts === 2) opts.logger.warn(indexBusyRetryWarning({
|
|
14447
|
+
typeLabel: opts.typeLabel,
|
|
14448
|
+
logicalId: opts.logicalId,
|
|
14449
|
+
physicalId: opts.physicalId,
|
|
14450
|
+
attemptNumber: deleteAttempts
|
|
14451
|
+
}));
|
|
14452
|
+
await opts.reArm();
|
|
14453
|
+
}
|
|
14454
|
+
await opts.deleteTable();
|
|
14455
|
+
}, opts.logicalId, {
|
|
14456
|
+
maxRetries: 8,
|
|
14457
|
+
isRetryable: (message) => isIndexBusyDeleteError(message),
|
|
14458
|
+
logger: opts.logger,
|
|
14459
|
+
sleep: (ms) => opts.sleepSeam.sleep ? opts.sleepSeam.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
|
|
14460
|
+
});
|
|
14461
|
+
}
|
|
14462
|
+
|
|
14195
14463
|
//#endregion
|
|
14196
14464
|
//#region src/provisioning/providers/dynamodb-table-provider.ts
|
|
14197
14465
|
/**
|
|
@@ -14720,6 +14988,18 @@ function desiredIndexEntriesByName(value) {
|
|
|
14720
14988
|
}
|
|
14721
14989
|
return byName;
|
|
14722
14990
|
}
|
|
14991
|
+
/**
|
|
14992
|
+
* Test seam for the index-busy `DeleteTable` retry's backoff (issue #1931),
|
|
14993
|
+
* mirroring the sibling provider's export of the same name. Production leaves
|
|
14994
|
+
* `sleep` undefined so `withRetry`'s real schedule applies; a test injects a
|
|
14995
|
+
* no-op so the ~47s budget does not have to be waited out.
|
|
14996
|
+
*
|
|
14997
|
+
* One seam PER PROVIDER rather than one on the shared module: a `Table` test
|
|
14998
|
+
* that silences this backoff must not also silence the `GlobalTable` one, since
|
|
14999
|
+
* the two suites run in the same worker and a suite that stopped paying a delay
|
|
15000
|
+
* it meant to exercise fails silently — it just gets faster.
|
|
15001
|
+
*/
|
|
15002
|
+
const deleteTableRetryDelays$1 = {};
|
|
14723
15003
|
var DynamoDBTableProvider = class {
|
|
14724
15004
|
dynamoDBClient;
|
|
14725
15005
|
logger = getLogger().child("DynamoDBTableProvider");
|
|
@@ -15054,7 +15334,24 @@ var DynamoDBTableProvider = class {
|
|
|
15054
15334
|
if (!(flipError instanceof ResourceNotFoundException$1)) this.logger.debug(`Could not disable DeletionProtectionEnabled on ${physicalId}: ${flipError instanceof Error ? flipError.message : String(flipError)}`);
|
|
15055
15335
|
}
|
|
15056
15336
|
try {
|
|
15057
|
-
await
|
|
15337
|
+
await deleteTableWithIndexBusyRetry({
|
|
15338
|
+
logicalId,
|
|
15339
|
+
physicalId,
|
|
15340
|
+
typeLabel: "table",
|
|
15341
|
+
logger: this.logger,
|
|
15342
|
+
deleteTable: async () => {
|
|
15343
|
+
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
15344
|
+
},
|
|
15345
|
+
reArm: () => waitForIndexesSettled({
|
|
15346
|
+
tableName: physicalId,
|
|
15347
|
+
logicalId,
|
|
15348
|
+
logger: this.logger,
|
|
15349
|
+
describeTable: () => this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId })),
|
|
15350
|
+
maxAttempts: 60,
|
|
15351
|
+
proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE
|
|
15352
|
+
}),
|
|
15353
|
+
sleepSeam: deleteTableRetryDelays$1
|
|
15354
|
+
});
|
|
15058
15355
|
this.logger.debug(`Successfully deleted DynamoDB table ${logicalId}`);
|
|
15059
15356
|
} catch (error) {
|
|
15060
15357
|
if (error instanceof ResourceNotFoundException$1) {
|
|
@@ -17482,24 +17779,19 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
17482
17779
|
this.logger.debug(`Waiting for indexes on ${physicalId} to settle before DeleteTable (issue #1521)`);
|
|
17483
17780
|
await this.waitForIndexesActive(physicalId, logicalId, { proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE });
|
|
17484
17781
|
}
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
const remainingAttempts = 1 + DELETE_INDEX_BUSY_MAX_RETRIES - deleteAttempts;
|
|
17490
|
-
this.logger.warn(`DynamoDB GlobalTable ${logicalId}: AWS refused DeleteTable on ${physicalId} because a global secondary index is still being created, updated or deleted — application auto-scaling can start an index capacity change at any moment, including during this destroy. Waiting for the index to settle and retrying (up to ${remainingAttempts} more attempts, each preceded by up to ${DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS} DescribeTable polls ~1s apart — a little over a minute of settling per attempt). Nothing is wrong with the table; a large index backfill can outlast this budget, in which case the destroy fails with AWS's own message and re-running it succeeds once the index is ACTIVE.`);
|
|
17491
|
-
}
|
|
17492
|
-
await this.waitForIndexesActive(physicalId, logicalId, {
|
|
17493
|
-
maxAttempts: DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS,
|
|
17494
|
-
proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE
|
|
17495
|
-
});
|
|
17496
|
-
}
|
|
17497
|
-
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
17498
|
-
}, logicalId, {
|
|
17499
|
-
maxRetries: DELETE_INDEX_BUSY_MAX_RETRIES,
|
|
17500
|
-
isRetryable: (message) => isIndexBusyDeleteError(message),
|
|
17782
|
+
await deleteTableWithIndexBusyRetry({
|
|
17783
|
+
logicalId,
|
|
17784
|
+
physicalId,
|
|
17785
|
+
typeLabel: "GlobalTable",
|
|
17501
17786
|
logger: this.logger,
|
|
17502
|
-
|
|
17787
|
+
deleteTable: async () => {
|
|
17788
|
+
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
17789
|
+
},
|
|
17790
|
+
reArm: () => this.waitForIndexesActive(physicalId, logicalId, {
|
|
17791
|
+
maxAttempts: 60,
|
|
17792
|
+
proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE
|
|
17793
|
+
}),
|
|
17794
|
+
sleepSeam: deleteTableRetryDelays
|
|
17503
17795
|
});
|
|
17504
17796
|
await this.waitForTableGone(physicalId, logicalId);
|
|
17505
17797
|
this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
|
|
@@ -18137,28 +18429,14 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
18137
18429
|
* passes {@link DELETE_INDEX_WAIT_PROCEED_NOTE}.
|
|
18138
18430
|
*/
|
|
18139
18431
|
async waitForIndexesActive(tableName, logicalId, opts) {
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
this.logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
|
|
18149
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
18150
|
-
continue;
|
|
18151
|
-
}
|
|
18152
|
-
if (err instanceof ResourceNotFoundException$1) {
|
|
18153
|
-
this.logger.debug(`Table ${tableName} (${logicalId}) no longer exists while waiting for indexes`);
|
|
18154
|
-
return;
|
|
18155
|
-
}
|
|
18156
|
-
this.logger.warn(`DescribeTable failed while waiting for indexes on ${tableName} (${logicalId}), so cdkd stopped waiting; ${proceedNote} DescribeTable error: ${message}`);
|
|
18157
|
-
return;
|
|
18158
|
-
}
|
|
18159
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
18160
|
-
}
|
|
18161
|
-
this.logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts} DescribeTable polls (~1s apart, so a little over ${maxAttempts}s of wall clock); ${proceedNote}`);
|
|
18432
|
+
await waitForIndexesSettled({
|
|
18433
|
+
tableName,
|
|
18434
|
+
logicalId,
|
|
18435
|
+
logger: this.logger,
|
|
18436
|
+
describeTable: () => this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName })),
|
|
18437
|
+
maxAttempts: opts?.maxAttempts ?? 900,
|
|
18438
|
+
proceedNote: opts?.proceedNote ?? "auto-scaling registration for a still-building index may have been skipped. The next deploy re-asserts it."
|
|
18439
|
+
});
|
|
18162
18440
|
}
|
|
18163
18441
|
/**
|
|
18164
18442
|
* Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
|
|
@@ -18439,100 +18717,6 @@ function asRecord$1(value) {
|
|
|
18439
18717
|
*/
|
|
18440
18718
|
const ON_DEMAND_LIMIT_RESET = -1;
|
|
18441
18719
|
/**
|
|
18442
|
-
* The GSI states during which AWS refuses another `UpdateTable` or a
|
|
18443
|
-
* `DeleteTable` on the table (issue #1521) — a table is ACTIVE while one of
|
|
18444
|
-
* its indexes is still transitioning, which is what made the serialized
|
|
18445
|
-
* update sequence race and what left a table ORPHANED when the follow-up
|
|
18446
|
-
* destroy hit the same rule.
|
|
18447
|
-
*
|
|
18448
|
-
* `DELETING` is included because a dropped index keeps reporting until it is
|
|
18449
|
-
* actually gone. The test is on the TRANSITIONAL values rather than on
|
|
18450
|
-
* `!== 'ACTIVE'`: an absent or unrecognized status must not park the caller
|
|
18451
|
-
* for its whole cap on a table that is fine.
|
|
18452
|
-
*/
|
|
18453
|
-
const INDEX_TRANSITIONAL_STATUSES = /* @__PURE__ */ new Set([
|
|
18454
|
-
"CREATING",
|
|
18455
|
-
"UPDATING",
|
|
18456
|
-
"DELETING"
|
|
18457
|
-
]);
|
|
18458
|
-
function hasTransitionalIndex(indexes) {
|
|
18459
|
-
return (indexes ?? []).some((gsi) => INDEX_TRANSITIONAL_STATUSES.has(gsi.IndexStatus ?? ""));
|
|
18460
|
-
}
|
|
18461
|
-
/**
|
|
18462
|
-
* AWS's refusal when `DeleteTable` lands while one of the table's GSIs is
|
|
18463
|
-
* mid-transition (issue #1830). Observed verbatim on a real destroy:
|
|
18464
|
-
*
|
|
18465
|
-
* Attempt to change a resource which is still in use: Cannot delete table
|
|
18466
|
-
* while indexes are being created, updated, or deleted.
|
|
18467
|
-
*
|
|
18468
|
-
* Matched on the leading clause rather than on the whole sentence: the
|
|
18469
|
-
* trailing status list is AWS wording that can gain a state without changing
|
|
18470
|
-
* what the message MEANS, and the leading clause is already specific enough
|
|
18471
|
-
* that nothing else in the DynamoDB surface produces it. Case-insensitive
|
|
18472
|
-
* because the text arrives inside a wrapper prefix whose casing is not ours.
|
|
18473
|
-
*
|
|
18474
|
-
* Deliberately NOT keyed on the exception NAME: AWS reports this as a plain
|
|
18475
|
-
* `ResourceInUseException`, which is the same name it uses for genuinely
|
|
18476
|
-
* terminal conflicts (deleting a table that is `CREATING`, creating one that
|
|
18477
|
-
* already exists). Retrying on the name would turn those into a 47s stall
|
|
18478
|
-
* before the same failure.
|
|
18479
|
-
*/
|
|
18480
|
-
const INDEX_BUSY_DELETE_MESSAGE = /cannot delete table while indexes are being/i;
|
|
18481
|
-
function isIndexBusyDeleteError(message) {
|
|
18482
|
-
return INDEX_BUSY_DELETE_MESSAGE.test(message);
|
|
18483
|
-
}
|
|
18484
|
-
/**
|
|
18485
|
-
* Retry budget for the index-busy `DeleteTable` refusal (issue #1830).
|
|
18486
|
-
*
|
|
18487
|
-
* Left at `withRetry`'s own default of 8 rather than raised. Named rather than
|
|
18488
|
-
* inlined because passing it at all is what opts this call out of
|
|
18489
|
-
* `withRetry`'s dense IAM-propagation schedule, which would be wrong for a
|
|
18490
|
-
* condition measured in seconds-to-minutes rather than sub-second.
|
|
18491
|
-
*
|
|
18492
|
-
* The count MULTIPLIES the per-attempt re-arm poll below, so the two have to
|
|
18493
|
-
* be read together — see {@link DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS} for the
|
|
18494
|
-
* arithmetic and for why the poll is not the 15-minute one.
|
|
18495
|
-
*/
|
|
18496
|
-
const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
|
|
18497
|
-
/**
|
|
18498
|
-
* Seconds the index-busy `DeleteTable` retry spends re-arming BEFORE each
|
|
18499
|
-
* retry (issue #1830) — one `DescribeTable` per second, and it returns on the
|
|
18500
|
-
* first one that reports every index settled.
|
|
18501
|
-
*
|
|
18502
|
-
* Bounded well under
|
|
18503
|
-
* {@link DynamoDBGlobalTableProvider['waitForIndexesActive']}'s 15-minute
|
|
18504
|
-
* default because THAT default is sized for a wait that runs once, while this
|
|
18505
|
-
* one runs per retry: the caller's wall clock is
|
|
18506
|
-
* `DELETE_INDEX_BUSY_MAX_RETRIES x this + withRetry's ~47s of backoff`, and
|
|
18507
|
-
* `destroy-runner.ts` caps a single `delete()` at the per-resource deadline
|
|
18508
|
-
* (30 min by default; this provider declares no `getMinResourceTimeoutMs` to
|
|
18509
|
-
* lift it). At 900s the product was ~2h, so a genuinely stuck index produced a
|
|
18510
|
-
* 30-minute wait ending in a generic `ResourceTimeoutError` that never
|
|
18511
|
-
* mentions indexes. At 60s the worst case is ~8.8 min and the user gets AWS's
|
|
18512
|
-
* own actionable sentence instead.
|
|
18513
|
-
*
|
|
18514
|
-
* The FIRST attempt is unaffected — it does not re-arm at all, and the #1521
|
|
18515
|
-
* pre-delete gate ahead of the loop keeps its full 15-minute poll.
|
|
18516
|
-
*/
|
|
18517
|
-
const DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS = 60;
|
|
18518
|
-
/**
|
|
18519
|
-
* What proceeding costs when a DELETE-path index-settle wait ends WITHOUT
|
|
18520
|
-
* confirming — passed as
|
|
18521
|
-
* {@link DynamoDBGlobalTableProvider['waitForIndexesActive']}'s `proceedNote`
|
|
18522
|
-
* by BOTH delete-path callers (the #1521 pre-delete gate and the #1830 retry
|
|
18523
|
-
* re-arm).
|
|
18524
|
-
*
|
|
18525
|
-
* The default note that helper falls back to is the auto-scaling caller's, and
|
|
18526
|
-
* it is not merely unhelpful here but wrong: nothing on the delete path
|
|
18527
|
-
* re-asserts anything on a later deploy. Conversely the delete path really
|
|
18528
|
-
* does have a backstop — `DeleteTable` follows immediately and AWS refuses it
|
|
18529
|
-
* again if the index is still transitioning — which the auto-scaling caller
|
|
18530
|
-
* does not: `reconcileAutoScalingTargets` skips an index that is not ready and
|
|
18531
|
-
* nothing downstream notices. One sentence per caller, so neither claims the
|
|
18532
|
-
* other's safety net.
|
|
18533
|
-
*/
|
|
18534
|
-
const DELETE_INDEX_WAIT_PROCEED_NOTE = `the DeleteTable goes ahead anyway and AWS's own refusal stays the backstop.`;
|
|
18535
|
-
/**
|
|
18536
18720
|
* Build the {@link ThroughputDiagnostic} for a `WarmThroughput` block that did
|
|
18537
18721
|
* not survive coercion intact, or `undefined` when it did.
|
|
18538
18722
|
*
|
|
@@ -38659,6 +38843,56 @@ const MFA_FACTOR_SMS = "SMS_MFA";
|
|
|
38659
38843
|
const MFA_FACTOR_SOFTWARE_TOKEN = "SOFTWARE_TOKEN_MFA";
|
|
38660
38844
|
const MFA_FACTOR_EMAIL_OTP = "EMAIL_OTP";
|
|
38661
38845
|
/**
|
|
38846
|
+
* Read `EnabledMfas` as the pair (recognized list, "the template asked for a
|
|
38847
|
+
* factor at all").
|
|
38848
|
+
*
|
|
38849
|
+
* The second half exists because `Array.isArray` is not the same question as
|
|
38850
|
+
* "did the user declare a factor". A hand-written YAML scalar
|
|
38851
|
+
* (`EnabledMfas: SOFTWARE_TOKEN_MFA`) or an intrinsic that resolves to a String
|
|
38852
|
+
* parameter is a factor DECLARATION that happens to be mis-shaped. Treating it
|
|
38853
|
+
* as absence lets the MfaConfiguration default resolve to `OFF`, which AWS
|
|
38854
|
+
* ACCEPTS — so the pool ships with MFA disabled and the declared factor
|
|
38855
|
+
* dropped, silently. Treating it as intent keeps the default at `OPTIONAL`
|
|
38856
|
+
* instead, which AWS refuses loudly — unless the template pinned
|
|
38857
|
+
* `MfaConfiguration: OFF`, or another factor is enabled. The warning in
|
|
38858
|
+
* `buildMfaConfigRequest` reports which of those three actually happened.
|
|
38859
|
+
* Same reasoning as the unrecognized-spelling case there; this is that hole
|
|
38860
|
+
* reached through the SHAPE instead of the spelling.
|
|
38861
|
+
*/
|
|
38862
|
+
function readEnabledMfas(properties) {
|
|
38863
|
+
const raw = properties["EnabledMfas"];
|
|
38864
|
+
if (Array.isArray(raw)) {
|
|
38865
|
+
const factors = raw;
|
|
38866
|
+
return {
|
|
38867
|
+
factors,
|
|
38868
|
+
declaresFactor: factors.length > 0,
|
|
38869
|
+
malformed: false
|
|
38870
|
+
};
|
|
38871
|
+
}
|
|
38872
|
+
const malformed = raw !== void 0 && raw !== null && raw !== "";
|
|
38873
|
+
return {
|
|
38874
|
+
factors: void 0,
|
|
38875
|
+
declaresFactor: malformed,
|
|
38876
|
+
malformed
|
|
38877
|
+
};
|
|
38878
|
+
}
|
|
38879
|
+
/**
|
|
38880
|
+
* True when any MFA-config-API-routed property is present, i.e. a
|
|
38881
|
+
* `SetUserPoolMfaConfig` call will run post-create. When true, `create()` must
|
|
38882
|
+
* NOT forward `MfaConfiguration` to `CreateUserPool`: AWS rejects
|
|
38883
|
+
* `CreateUserPool` with `MfaConfiguration: ON/OPTIONAL` unless the pool already
|
|
38884
|
+
* has SMS configured (+ phone_number auto-verification) OR software-token MFA
|
|
38885
|
+
* enabled — but software-token / email-OTP MFA can only be enabled via the
|
|
38886
|
+
* post-create `SetUserPoolMfaConfig` call, not on `CreateUserPool`. So the
|
|
38887
|
+
* correct sequence is: `CreateUserPool` WITHOUT `MfaConfiguration` (defaults
|
|
38888
|
+
* OFF) -> `SetUserPoolMfaConfig` sets `MfaConfiguration` + the factor blocks
|
|
38889
|
+
* together (the factor satisfies the MFA requirement, no SMS needed). This
|
|
38890
|
+
* mirrors how CloudFormation/CDK sequence the two calls.
|
|
38891
|
+
*/
|
|
38892
|
+
function hasMfaConfigProps(properties) {
|
|
38893
|
+
return readEnabledMfas(properties).declaresFactor || !!properties["EmailAuthenticationMessage"] || !!properties["EmailAuthenticationSubject"] || !!properties["WebAuthnRelyingPartyID"] || !!properties["WebAuthnUserVerification"];
|
|
38894
|
+
}
|
|
38895
|
+
/**
|
|
38662
38896
|
* Build the `SetUserPoolMfaConfig` request from the CFn-level MFA properties,
|
|
38663
38897
|
* or return `undefined` when none of the MFA-config-API-routed properties are
|
|
38664
38898
|
* present (so the caller skips the extra control-plane call entirely).
|
|
@@ -38674,37 +38908,21 @@ const MFA_FACTOR_EMAIL_OTP = "EMAIL_OTP";
|
|
|
38674
38908
|
* omitted `MfaConfiguration` defaults to OFF on the wire — which resets the
|
|
38675
38909
|
* pool to MFA-disabled and makes AWS reject (or silently drop) the per-factor
|
|
38676
38910
|
* sub-blocks we are trying to enable. Use the template's `MfaConfiguration`
|
|
38677
|
-
* when present;
|
|
38678
|
-
*
|
|
38679
|
-
*
|
|
38911
|
+
* when present; when the template omitted it, default by whether this call
|
|
38912
|
+
* enables an MFA FACTOR — `OPTIONAL` if it does, `OFF` (CloudFormation's own
|
|
38913
|
+
* default) if it does not. The body comment on that decision explains why both
|
|
38914
|
+
* halves of the factor test are load-bearing.
|
|
38680
38915
|
*/
|
|
38681
|
-
|
|
38682
|
-
|
|
38683
|
-
* `SetUserPoolMfaConfig` call will run post-create. When true, `create()` must
|
|
38684
|
-
* NOT forward `MfaConfiguration` to `CreateUserPool`: AWS rejects
|
|
38685
|
-
* `CreateUserPool` with `MfaConfiguration: ON/OPTIONAL` unless the pool already
|
|
38686
|
-
* has SMS configured (+ phone_number auto-verification) OR software-token MFA
|
|
38687
|
-
* enabled — but software-token / email-OTP MFA can only be enabled via the
|
|
38688
|
-
* post-create `SetUserPoolMfaConfig` call, not on `CreateUserPool`. So the
|
|
38689
|
-
* correct sequence is: `CreateUserPool` WITHOUT `MfaConfiguration` (defaults
|
|
38690
|
-
* OFF) -> `SetUserPoolMfaConfig` sets `MfaConfiguration` + the factor blocks
|
|
38691
|
-
* together (the factor satisfies the MFA requirement, no SMS needed). This
|
|
38692
|
-
* mirrors how CloudFormation/CDK sequence the two calls.
|
|
38693
|
-
*/
|
|
38694
|
-
function hasMfaConfigProps(properties) {
|
|
38695
|
-
const enabledMfas = Array.isArray(properties["EnabledMfas"]) ? properties["EnabledMfas"] : void 0;
|
|
38696
|
-
return enabledMfas !== void 0 && enabledMfas.length > 0 || !!properties["EmailAuthenticationMessage"] || !!properties["EmailAuthenticationSubject"] || !!properties["WebAuthnRelyingPartyID"] || !!properties["WebAuthnUserVerification"];
|
|
38697
|
-
}
|
|
38698
|
-
function buildMfaConfigRequest(physicalId, properties) {
|
|
38699
|
-
const enabledMfas = Array.isArray(properties["EnabledMfas"]) ? properties["EnabledMfas"] : void 0;
|
|
38916
|
+
function buildMfaConfigRequest(physicalId, properties, logger) {
|
|
38917
|
+
const { factors: enabledMfas, declaresFactor, malformed: enabledMfasMalformed } = readEnabledMfas(properties);
|
|
38700
38918
|
const emailMessage = properties["EmailAuthenticationMessage"] || void 0;
|
|
38701
38919
|
const emailSubject = properties["EmailAuthenticationSubject"] || void 0;
|
|
38702
38920
|
const webAuthnRpId = properties["WebAuthnRelyingPartyID"] || void 0;
|
|
38703
38921
|
const webAuthnUserVerification = properties["WebAuthnUserVerification"] || void 0;
|
|
38704
38922
|
if (!hasMfaConfigProps(properties)) return void 0;
|
|
38705
38923
|
const request = { UserPoolId: physicalId };
|
|
38706
|
-
request.MfaConfiguration = properties["MfaConfiguration"] ?? "OPTIONAL";
|
|
38707
38924
|
const factors = new Set(enabledMfas ?? []);
|
|
38925
|
+
const unrecognizedFactors = [...factors].filter((factor) => factor !== MFA_FACTOR_SMS && factor !== MFA_FACTOR_SOFTWARE_TOKEN && factor !== MFA_FACTOR_EMAIL_OTP);
|
|
38708
38926
|
if (factors.has(MFA_FACTOR_SOFTWARE_TOKEN)) request.SoftwareTokenMfaConfiguration = { Enabled: true };
|
|
38709
38927
|
if (factors.has(MFA_FACTOR_SMS)) request.SmsMfaConfiguration = { ...properties["SmsConfiguration"] ? { SmsConfiguration: properties["SmsConfiguration"] } : {} };
|
|
38710
38928
|
if (factors.has(MFA_FACTOR_EMAIL_OTP) || emailMessage !== void 0 || emailSubject !== void 0) request.EmailMfaConfiguration = {
|
|
@@ -38715,6 +38933,15 @@ function buildMfaConfigRequest(physicalId, properties) {
|
|
|
38715
38933
|
...webAuthnRpId !== void 0 ? { RelyingPartyId: webAuthnRpId } : {},
|
|
38716
38934
|
...webAuthnUserVerification !== void 0 ? { UserVerification: webAuthnUserVerification } : {}
|
|
38717
38935
|
};
|
|
38936
|
+
const enablesMfaFactor = declaresFactor || request.EmailMfaConfiguration !== void 0;
|
|
38937
|
+
request.MfaConfiguration = properties["MfaConfiguration"] ?? (enablesMfaFactor ? "OPTIONAL" : "OFF");
|
|
38938
|
+
const dropped = unrecognizedFactors.map((f) => JSON.stringify(f) ?? String(f));
|
|
38939
|
+
if (enabledMfasMalformed) dropped.push(`${JSON.stringify(properties["EnabledMfas"])} (not a list)`);
|
|
38940
|
+
if (dropped.length > 0) {
|
|
38941
|
+
const anyFactorBlock = request.SmsMfaConfiguration !== void 0 || request.SoftwareTokenMfaConfiguration !== void 0 || request.EmailMfaConfiguration !== void 0;
|
|
38942
|
+
const consequence = request.MfaConfiguration === "OFF" ? `MfaConfiguration is OFF, so this pool deploys with MFA DISABLED` : anyFactorBlock ? `MfaConfiguration is ${request.MfaConfiguration} and another factor block IS sent, so these entries are silently ignored rather than failing the call on their own` : `MfaConfiguration is ${request.MfaConfiguration} with no factor enabled, so AWS rejects this call rather than deploying the pool with MFA disabled`;
|
|
38943
|
+
logger?.warn(`UserPool ${physicalId}: EnabledMfas entries ${dropped.join(", ")} do not map to an MFA factor block (known: ${MFA_FACTOR_SMS}, ${MFA_FACTOR_SOFTWARE_TOKEN}, ${MFA_FACTOR_EMAIL_OTP}); no factor is enabled from them. ${consequence}.`);
|
|
38944
|
+
}
|
|
38718
38945
|
return request;
|
|
38719
38946
|
}
|
|
38720
38947
|
/**
|
|
@@ -38852,7 +39079,7 @@ var CognitoUserPoolProvider = class {
|
|
|
38852
39079
|
* (mirrors DynamoDBTableProvider.retryOnTransientControlPlane).
|
|
38853
39080
|
*/
|
|
38854
39081
|
async applyMfaConfig(physicalId, properties) {
|
|
38855
|
-
const request = buildMfaConfigRequest(physicalId, properties);
|
|
39082
|
+
const request = buildMfaConfigRequest(physicalId, properties, this.logger);
|
|
38856
39083
|
if (!request) return;
|
|
38857
39084
|
await this.retryOnTransientControlPlane(() => this.getClient().send(new SetUserPoolMfaConfigCommand(request)), `SetUserPoolMfaConfig(${physicalId})`);
|
|
38858
39085
|
}
|
|
@@ -80107,7 +80334,7 @@ function createMigrateCommand() {
|
|
|
80107
80334
|
*/
|
|
80108
80335
|
function buildProgram() {
|
|
80109
80336
|
const program = new Command();
|
|
80110
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.
|
|
80337
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.30");
|
|
80111
80338
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
80112
80339
|
const { profile } = actionCommand.optsWithGlobals();
|
|
80113
80340
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|