@go-to-k/cdkd 0.283.33 → 0.283.35
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-ID5-zAwf.js → asg-provider-xC0UiCjt.js} +2 -2
- package/dist/{asg-provider-ID5-zAwf.js.map → asg-provider-xC0UiCjt.js.map} +1 -1
- package/dist/cli.js +429 -64
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-Cg54H4pJ.js → deploy-engine-fBYrdrmB.js} +3 -3
- package/dist/{deploy-engine-Cg54H4pJ.js.map → deploy-engine-fBYrdrmB.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 getAccountInfo, $t as formatDockerLoginError, A as cyan, An as resolveBucketRegion, At as TemplateParser, B as stateKeySecretExposure, Bn as LocalInvokeBuildError$1, Bt as loadPublishableAssetManifest, C as makeCanonicalizePropertiesFn, Cn as expectedOwnerParam, Ct as s3BucketWebsiteUrl, D as renderStatefulReason, Dn as AssemblyReader, Dt as describeTypeWithThrottleRetry, E as isStatefulRecreateTargetSync, En as derivePartitionAndUrlSuffix, Et as INTRINSIC_KEYS, F as collectDeclaredOutputNames, Ft as AssetPublisher, G as findActionableSilentDrops, Gn as NestedStackChildDirectDestroyError, Gt as BOOTSTRAP_MARKER_PREFIX, H as collectInlinePolicyNamesManagedBySiblings, Hn as LocalStartServiceError, Ht as escapeRegExp, I as collectPublishedOutputNames, In as CdkdError, It as stringifyValue, J as slowCcOperationTimeoutMs, Jn as ResourceTimeoutError, Jt as parseBootstrapMarker, K as findSilentDropProperties, Kn as PartialFailureError, Kt as ensureAssetStorage, L as exportAliasCollisionScrubWarning, Lt as WorkGraph, M as green, Mn as getAwsClients, Mt as S3StateBackend, N as red, Nn as resetAwsClients, Nt as rebuildClientForBucketRegion, O as formatResourceLine, On as processStackMessages, Ot as withRetry, P as yellow, Pn as setAwsClients, Pt as shouldRetainResource, Q as cfnRefValueFromPhysicalId, Qt as buildDockerImage, R as isExportAliasCollision, Rt as buildAssetRedirectMap, S as unsupportedFinalSnapshotError, Sn as uploadCfnTemplate, St as s3BucketRegionalDomainName, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as canonicalizeRegion, Tt as DiffCalculator, U as clearOnUpdateRemoval, Ut as stripControlChars, V as IAMRoleProvider, Vn as LocalMigrateError, Vt as rewriteTemplateAssetReferences, W as ProviderRegistry, Wn as MissingCdkCliError, Wt as AssetModeResolver, X as isTerminationProtectionPropagationError, Xn as StackHasActiveImportsError, Xt as validateContainerRepoName, Y as disableInstanceApiTermination, Yn as ResourceUpdateNotSupportedError, Yt as validateAssetBucketName, Z as IntrinsicFunctionResolver, Zn as StackTerminationProtectionError, Zt as buildDenyExternalAccessPolicy, _ as buildFinalSnapshotIdentifier, _n as warnDeprecatedNoPrefixCliFlag, _t as redactSecretsForState, a as DeploymentEventsStore, an as Synthesizer, ar as isRetryableTransientError, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as MIGRATE_TMP_PREFIX, bt as s3BucketDomainName, c as replayFailedOperations, cn as getLegacyStateBucketName, ct as readConfigString, d as deleteSkipReason, dn as resolveCaptureObservedState, dt as requireConfigObject, en as getDockerCmd, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveSkipPrefix, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as stateBucketExistenceConfirmed, gt as maskSecretsInText, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveUseCdkBootstrapAssets, ht as TEMPLATE_SOURCED_RULES, i as DeploymentEventsReader, in as getDockerImageBySourceHash, ir as isMarkedNonRetryable, it as assertRegionMatch, j as gray, jn as AwsClients, jt as LockManager, k as bold, kt as DagBuilder, l as replayRollback, ln as resolveApp, lt as replayWarn, m as computeImplicitDeleteEdges, mn as resolveStateBucketWithDefaultAndSource, mt as STATE_SOURCED_READBACK_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as runDockerStreaming, nr as normalizeAwsError, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as synthesisStatusMessage, or as isThrottlingError, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveStateBucketWithDefault, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ProvisioningError, qt as getBootstrapMarkerKey, r as DeployEngine, rn as AssetManifestLoader, rr as withErrorHandling, rt as resolveExplicitPhysicalId, s as planRollback, sn as getDefaultStateBucketName, sr as markNonRetryable, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as runDockerForeground, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveAutoAssetStorage, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as CFN_TEMPLATE_BODY_LIMIT, vt as scrubResourceRecord, w as extractDeploymentEventError, wn as PARTITION_TABLE, wt as applyRoleArnIfSet, x as refusesFinalSnapshot, xn as findLargeInlineResources, xt as s3BucketDualStackDomainName, y as createPreDeleteFinalSnapshot, yn as CFN_TEMPLATE_URL_LIMIT, yt as s3BucketArn, z as secretBearingStateKeyWarning, zn as DeployCancelledError, zt as createAssetRedirectResolver } from "./deploy-engine-
|
|
2
|
+
import { $ as getAccountInfo, $t as formatDockerLoginError, A as cyan, An as resolveBucketRegion, At as TemplateParser, B as stateKeySecretExposure, Bn as LocalInvokeBuildError$1, Bt as loadPublishableAssetManifest, C as makeCanonicalizePropertiesFn, Cn as expectedOwnerParam, Ct as s3BucketWebsiteUrl, D as renderStatefulReason, Dn as AssemblyReader, Dt as describeTypeWithThrottleRetry, E as isStatefulRecreateTargetSync, En as derivePartitionAndUrlSuffix, Et as INTRINSIC_KEYS, F as collectDeclaredOutputNames, Ft as AssetPublisher, G as findActionableSilentDrops, Gn as NestedStackChildDirectDestroyError, Gt as BOOTSTRAP_MARKER_PREFIX, H as collectInlinePolicyNamesManagedBySiblings, Hn as LocalStartServiceError, Ht as escapeRegExp, I as collectPublishedOutputNames, In as CdkdError, It as stringifyValue, J as slowCcOperationTimeoutMs, Jn as ResourceTimeoutError, Jt as parseBootstrapMarker, K as findSilentDropProperties, Kn as PartialFailureError, Kt as ensureAssetStorage, L as exportAliasCollisionScrubWarning, Lt as WorkGraph, M as green, Mn as getAwsClients, Mt as S3StateBackend, N as red, Nn as resetAwsClients, Nt as rebuildClientForBucketRegion, O as formatResourceLine, On as processStackMessages, Ot as withRetry, P as yellow, Pn as setAwsClients, Pt as shouldRetainResource, Q as cfnRefValueFromPhysicalId, Qt as buildDockerImage, R as isExportAliasCollision, Rt as buildAssetRedirectMap, S as unsupportedFinalSnapshotError, Sn as uploadCfnTemplate, St as s3BucketRegionalDomainName, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as canonicalizeRegion, Tt as DiffCalculator, U as clearOnUpdateRemoval, Ut as stripControlChars, V as IAMRoleProvider, Vn as LocalMigrateError, Vt as rewriteTemplateAssetReferences, W as ProviderRegistry, Wn as MissingCdkCliError, Wt as AssetModeResolver, X as isTerminationProtectionPropagationError, Xn as StackHasActiveImportsError, Xt as validateContainerRepoName, Y as disableInstanceApiTermination, Yn as ResourceUpdateNotSupportedError, Yt as validateAssetBucketName, Z as IntrinsicFunctionResolver, Zn as StackTerminationProtectionError, Zt as buildDenyExternalAccessPolicy, _ as buildFinalSnapshotIdentifier, _n as warnDeprecatedNoPrefixCliFlag, _t as redactSecretsForState, a as DeploymentEventsStore, an as Synthesizer, ar as isRetryableTransientError, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as MIGRATE_TMP_PREFIX, bt as s3BucketDomainName, c as replayFailedOperations, cn as getLegacyStateBucketName, ct as readConfigString, d as deleteSkipReason, dn as resolveCaptureObservedState, dt as requireConfigObject, en as getDockerCmd, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveSkipPrefix, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as stateBucketExistenceConfirmed, gt as maskSecretsInText, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveUseCdkBootstrapAssets, ht as TEMPLATE_SOURCED_RULES, i as DeploymentEventsReader, in as getDockerImageBySourceHash, ir as isMarkedNonRetryable, it as assertRegionMatch, j as gray, jn as AwsClients, jt as LockManager, k as bold, kt as DagBuilder, l as replayRollback, ln as resolveApp, lt as replayWarn, m as computeImplicitDeleteEdges, mn as resolveStateBucketWithDefaultAndSource, mt as STATE_SOURCED_READBACK_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as runDockerStreaming, nr as normalizeAwsError, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as synthesisStatusMessage, or as isThrottlingError, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveStateBucketWithDefault, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ProvisioningError, qt as getBootstrapMarkerKey, r as DeployEngine, rn as AssetManifestLoader, rr as withErrorHandling, rt as resolveExplicitPhysicalId, s as planRollback, sn as getDefaultStateBucketName, sr as markNonRetryable, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as runDockerForeground, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveAutoAssetStorage, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as CFN_TEMPLATE_BODY_LIMIT, vt as scrubResourceRecord, w as extractDeploymentEventError, wn as PARTITION_TABLE, wt as applyRoleArnIfSet, x as refusesFinalSnapshot, xn as findLargeInlineResources, xt as s3BucketDualStackDomainName, y as createPreDeleteFinalSnapshot, yn as CFN_TEMPLATE_URL_LIMIT, yt as s3BucketArn, z as secretBearingStateKeyWarning, zn as DeployCancelledError, zt as createAssetRedirectResolver } from "./deploy-engine-fBYrdrmB.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-xC0UiCjt.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";
|
|
@@ -14282,27 +14282,15 @@ function hasTransitionalIndex(indexes) {
|
|
|
14282
14282
|
* Deliberately NOT keyed on the exception NAME: AWS reports this as a plain
|
|
14283
14283
|
* `ResourceInUseException`, which is the same name it uses for genuinely
|
|
14284
14284
|
* terminal conflicts (deleting a table that is `CREATING`, creating one that
|
|
14285
|
-
* already exists). Retrying on the name would
|
|
14286
|
-
*
|
|
14285
|
+
* already exists). Retrying on the name would spend the WHOLE budget on them —
|
|
14286
|
+
* a full settle poll plus a backoff step per attempt, i.e. minutes — before
|
|
14287
|
+
* failing identically.
|
|
14287
14288
|
*/
|
|
14288
14289
|
const INDEX_BUSY_DELETE_MESSAGE = /cannot delete table while indexes are being/i;
|
|
14289
14290
|
function isIndexBusyDeleteError(message) {
|
|
14290
14291
|
return INDEX_BUSY_DELETE_MESSAGE.test(message);
|
|
14291
14292
|
}
|
|
14292
14293
|
/**
|
|
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
14294
|
* `DescribeTable` polls (~1s apart) the index-busy `DeleteTable` retry spends
|
|
14307
14295
|
* re-arming BEFORE each retry (issue #1830) — it returns on the first one that
|
|
14308
14296
|
* reports every index settled.
|
|
@@ -14310,30 +14298,57 @@ const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
|
|
|
14310
14298
|
* Bounded well under the 15-minute default `GlobalTable`'s pre-delete #1521
|
|
14311
14299
|
* gate uses, because THAT default is sized for a wait that runs once while
|
|
14312
14300
|
* this one runs per retry: the caller's wall clock is
|
|
14313
|
-
* `
|
|
14314
|
-
*
|
|
14315
|
-
*
|
|
14316
|
-
*
|
|
14317
|
-
*
|
|
14318
|
-
*
|
|
14319
|
-
*
|
|
14301
|
+
* `its retry budget x this x ~1.2s per poll + withRetry's backoff`, and `destroy-runner.ts` runs the delete under a per-resource
|
|
14302
|
+
* deadline (30 min by default; neither DynamoDB provider declares a
|
|
14303
|
+
* `getMinResourceTimeoutMs` to lift it). At 900 polls the product would be
|
|
14304
|
+
* ~4.2h, so a genuinely stuck index would produce a 30-minute wait ending in a
|
|
14305
|
+
* generic `ResourceTimeoutError` that never mentions indexes. At 60 the LOOP's
|
|
14306
|
+
* worst case is ~18.4 min on `AWS::DynamoDB::Table` and ~10.4 min on
|
|
14307
|
+
* `AWS::DynamoDB::GlobalTable` — see {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES}
|
|
14308
|
+
* for every term of that product and for why the two budgets differ — and the
|
|
14309
|
+
* user gets AWS's own actionable sentence instead.
|
|
14320
14310
|
*
|
|
14321
14311
|
* **What that deadline actually wraps**, since the arithmetic depends on it:
|
|
14322
14312
|
* NOT one `delete()` but `destroy-runner.ts`'s whole outer retry loop, which
|
|
14323
14313
|
* calls `delete()` up to 4 times (`maxAttempts = 3`) inside the one deadline.
|
|
14324
|
-
* Four times ~
|
|
14314
|
+
* Four times ~18.4 min would blow straight through 30 min. It does not, and the
|
|
14325
14315
|
* reason is load-bearing rather than incidental: that outer loop only retries
|
|
14326
14316
|
* when `isRetryableTransientError` says so, and this refusal matches no entry
|
|
14327
14317
|
* in `RETRYABLE_ERROR_MESSAGE_PATTERNS` (verified against the real classifier —
|
|
14328
14318
|
* both AWS's raw sentence and the `Failed to delete DynamoDB table ...` wrap
|
|
14329
14319
|
* return false), so the outer loop runs `delete()` exactly ONCE for it and the
|
|
14330
14320
|
* budget below is not re-multiplied. Adding this message to those patterns
|
|
14331
|
-
* would silently make the worst case ~
|
|
14321
|
+
* would silently make the worst case ~74 min on `AWS::DynamoDB::Table` — well
|
|
14322
|
+
* over the deadline.
|
|
14332
14323
|
*
|
|
14333
14324
|
* The FIRST attempt is unaffected — it does not re-arm at all.
|
|
14334
14325
|
*/
|
|
14335
14326
|
const DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS = 60;
|
|
14336
14327
|
/**
|
|
14328
|
+
* Sleep between two `DescribeTable` polls of {@link waitForIndexesSettled}.
|
|
14329
|
+
*
|
|
14330
|
+
* Named rather than inlined because it is the unit every poll budget on the
|
|
14331
|
+
* DynamoDB DELETE path is denominated in — a poll count only becomes a wall
|
|
14332
|
+
* clock through this, and the deadline arguments at
|
|
14333
|
+
* {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES} multiply it. Each of those counts
|
|
14334
|
+
* READS it, which is what makes the claim checkable rather than a convention:
|
|
14335
|
+
* this module's 60-poll re-arm; `GlobalTable`'s 900-poll #1521 pre-delete gate
|
|
14336
|
+
* (and the same 900-poll default its auto-scaling caller takes, since both go
|
|
14337
|
+
* through `waitForIndexesActive` -> {@link waitForIndexesSettled});
|
|
14338
|
+
* `GlobalTable`'s `waitForReplicaGone` and `waitForTableGone` at 600 polls
|
|
14339
|
+
* each; and the `Table` provider's 60-poll `waitForTableActiveAfterUpdate`.
|
|
14340
|
+
*
|
|
14341
|
+
* Deliberately NOT claimed for the whole file: `waitForReplicaActive` keeps its
|
|
14342
|
+
* own `1000`, and is left alone because it runs on the CREATE / UPDATE path,
|
|
14343
|
+
* which no arithmetic here prices.
|
|
14344
|
+
*
|
|
14345
|
+
* It is a FLOOR on the per-poll cost, never the whole of it — each poll also
|
|
14346
|
+
* pays a `DescribeTable` round trip, which is why the live run's 60-poll
|
|
14347
|
+
* re-arm took ~72s and not 60s, and why the arithmetic above prices a poll at
|
|
14348
|
+
* ~1.2s.
|
|
14349
|
+
*/
|
|
14350
|
+
const INDEX_SETTLE_POLL_INTERVAL_MS = 1e3;
|
|
14351
|
+
/**
|
|
14337
14352
|
* What proceeding costs when a DELETE-path index-settle wait ends WITHOUT
|
|
14338
14353
|
* confirming — passed as {@link waitForIndexesSettled}'s `proceedNote` by every
|
|
14339
14354
|
* delete-path caller (`GlobalTable`'s #1521 pre-delete gate, and the #1830 /
|
|
@@ -14371,7 +14386,7 @@ async function waitForIndexesSettled(opts) {
|
|
|
14371
14386
|
const message = err instanceof Error ? err.message : String(err);
|
|
14372
14387
|
if (isThrottlingError(err)) {
|
|
14373
14388
|
logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
|
|
14374
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
14389
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
14375
14390
|
continue;
|
|
14376
14391
|
}
|
|
14377
14392
|
if (err instanceof ResourceNotFoundException$1) {
|
|
@@ -14383,9 +14398,10 @@ async function waitForIndexesSettled(opts) {
|
|
|
14383
14398
|
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
14399
|
return;
|
|
14385
14400
|
}
|
|
14386
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
14401
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
14387
14402
|
}
|
|
14388
|
-
|
|
14403
|
+
const pollSeconds = INDEX_SETTLE_POLL_INTERVAL_MS / 1e3;
|
|
14404
|
+
logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts} DescribeTable polls (~${pollSeconds}s apart, so a little over ${maxAttempts * pollSeconds}s of wall clock); ${proceedNote}`);
|
|
14389
14405
|
}
|
|
14390
14406
|
/**
|
|
14391
14407
|
* The ONE line the retry prints at default verbosity, for both providers.
|
|
@@ -14395,15 +14411,50 @@ async function waitForIndexesSettled(opts) {
|
|
|
14395
14411
|
* and the bounded budget above can end in a timeout that has to be diagnosable
|
|
14396
14412
|
* from an ordinary run's output.
|
|
14397
14413
|
*
|
|
14398
|
-
* `attemptNumber` is the attempt now STARTING (2 for the first retry)
|
|
14399
|
-
*
|
|
14400
|
-
*
|
|
14401
|
-
*
|
|
14414
|
+
* `attemptNumber` is the attempt now STARTING (2 for the first retry), and
|
|
14415
|
+
* `maxRetries` is the CALLER's budget — the two types no longer share one (see
|
|
14416
|
+
* {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES}), so a module-level constant here
|
|
14417
|
+
* would print the other type's promise. The remaining-attempt count is DERIVED
|
|
14418
|
+
* from the pair and never spelled out: `withRetry` runs `1 + maxRetries`
|
|
14419
|
+
* attempts in total, and writing the number by hand is how this line came to
|
|
14420
|
+
* promise 8 further attempts when 7 were left. Both figures are CLAMPED — the
|
|
14421
|
+
* attempt count at zero, the minutes at one, since the announced attempt
|
|
14422
|
+
* re-arms too: the function is exported, and an `attemptNumber` past the budget
|
|
14423
|
+
* otherwise printed "up to 0 more attempts" and then went negative.
|
|
14402
14424
|
*
|
|
14403
14425
|
* The per-attempt settle budget is attached to "this attempt plus the N more"
|
|
14404
14426
|
* rather than to the N alone, because the re-arm runs before the attempt this
|
|
14405
|
-
* very line announces too —
|
|
14406
|
-
* attempts, each preceded by ..." made one sentence
|
|
14427
|
+
* very line announces too — 14 re-arms for 13 remaining attempts on the `Table`
|
|
14428
|
+
* budget. Saying "N more attempts, each preceded by ..." made one sentence
|
|
14429
|
+
* quote two different counts.
|
|
14430
|
+
*
|
|
14431
|
+
* It also states how long the loop will keep GOING, as a floor (issue #1950).
|
|
14432
|
+
* At the `Table` budget the loop can run for the better part of twenty minutes,
|
|
14433
|
+
* and a user watching a destroy sit on one table for that long needs to tell
|
|
14434
|
+
* SETTLING from STUCK — an attempt count alone does not say how long it will
|
|
14435
|
+
* take.
|
|
14436
|
+
*
|
|
14437
|
+
* The wording is "will not give up for at least ~N more minutes", NOT "can keep
|
|
14438
|
+
* retrying for at least ~N". The re-arm returns on the first `DescribeTable`
|
|
14439
|
+
* that reports the indexes settled, so a table clearing in 90s prints this line
|
|
14440
|
+
* and then finishes immediately: read as a floor on the WAIT it would push the
|
|
14441
|
+
* user toward Ctrl-C, which is the opposite of what the line is for. It is a
|
|
14442
|
+
* floor on cdkd's PATIENCE, which is what the reader needs to decide whether to
|
|
14443
|
+
* keep waiting.
|
|
14444
|
+
*
|
|
14445
|
+
* A floor rather than an estimate because {@link INDEX_SETTLE_POLL_INTERVAL_MS}
|
|
14446
|
+
* is only the sleep: each poll also pays a `DescribeTable` round trip and
|
|
14447
|
+
* `withRetry` sleeps its backoff between attempts, so the true patience is
|
|
14448
|
+
* longer than the figure printed.
|
|
14449
|
+
*
|
|
14450
|
+
* The floor is conditioned on those waits RUNNING, which the sentence says out
|
|
14451
|
+
* loud rather than assuming. {@link waitForIndexesSettled} returns early on any
|
|
14452
|
+
* non-throttle `DescribeTable` failure (a describe-denied principal that can
|
|
14453
|
+
* still delete, say) and returns at once whenever the GSI-status proxy
|
|
14454
|
+
* disagrees with AWS's real refusal predicate — the gap named above. In those
|
|
14455
|
+
* runs every remaining retry pays only backoff, and cdkd gives up in ~1.6 min
|
|
14456
|
+
* having announced ~14. Unconditional, the figure would be plainly false
|
|
14457
|
+
* there.
|
|
14407
14458
|
*
|
|
14408
14459
|
* It must NOT carry AWS's own sentence: the `dynamodb-globaltable` integ greps
|
|
14409
14460
|
* the destroy log for `Cannot delete table while indexes are being` to prove
|
|
@@ -14411,8 +14462,10 @@ async function waitForIndexesSettled(opts) {
|
|
|
14411
14462
|
* the refusal having happened.
|
|
14412
14463
|
*/
|
|
14413
14464
|
function indexBusyRetryWarning(opts) {
|
|
14414
|
-
const remainingAttempts =
|
|
14415
|
-
|
|
14465
|
+
const remainingAttempts = Math.max(0, 1 + opts.maxRetries - opts.attemptNumber);
|
|
14466
|
+
const settleFloorMinutes = Math.max(1, Math.round((remainingAttempts + 1) * 60 * INDEX_SETTLE_POLL_INTERVAL_MS / 6e4));
|
|
14467
|
+
const pollSeconds = INDEX_SETTLE_POLL_INTERVAL_MS / 1e3;
|
|
14468
|
+
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 ~${pollSeconds}s apart before its DeleteTable, so cdkd will not give up for at least ~${settleFloorMinutes} more minutes while those settle waits run). 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
14469
|
}
|
|
14417
14470
|
/**
|
|
14418
14471
|
* Issue the caller's `DeleteTable`, retrying ONLY the transient index-busy
|
|
@@ -14430,11 +14483,19 @@ function indexBusyRetryWarning(opts) {
|
|
|
14430
14483
|
* index backfill outlasts any fixed backoff grid, while a settle poll returns
|
|
14431
14484
|
* on its first `DescribeTable` once the index is ACTIVE.
|
|
14432
14485
|
*
|
|
14486
|
+
* `maxRetries` is the CALLER's, not this module's: the two DynamoDB types have
|
|
14487
|
+
* different amounts of the per-resource deadline left by the time they reach
|
|
14488
|
+
* this loop, so they carry different budgets (see
|
|
14489
|
+
* {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES} for both derivations). Passing it
|
|
14490
|
+
* rather than reading a module constant is also what keeps the two providers'
|
|
14491
|
+
* budgets visible at their call sites.
|
|
14492
|
+
*
|
|
14433
14493
|
* `sleepSeam` is read per SLEEP rather than once when the options bag is built,
|
|
14434
14494
|
* so a test that installs the no-op from INSIDE the first refusal — i.e. after
|
|
14435
14495
|
* `delete()` has already built the bag — still gets it. A value captured at
|
|
14436
14496
|
* construction time would be `undefined` there and would silently pay
|
|
14437
|
-
* `withRetry`'s real
|
|
14497
|
+
* `withRetry`'s real backoff schedule (95s of sleep at the `Table` budget, 47s
|
|
14498
|
+
* at the `GlobalTable` one, on top of a full settle poll per attempt). Passing `sleep` unconditionally is safe:
|
|
14438
14499
|
* `withRetry`'s dense-schedule detection keys on `maxRetries` /
|
|
14439
14500
|
* `initialDelayMs` / `maxDelayMs` / `isRetryable`, all of which this caller
|
|
14440
14501
|
* already sets.
|
|
@@ -14447,13 +14508,14 @@ async function deleteTableWithIndexBusyRetry(opts) {
|
|
|
14447
14508
|
typeLabel: opts.typeLabel,
|
|
14448
14509
|
logicalId: opts.logicalId,
|
|
14449
14510
|
physicalId: opts.physicalId,
|
|
14450
|
-
attemptNumber: deleteAttempts
|
|
14511
|
+
attemptNumber: deleteAttempts,
|
|
14512
|
+
maxRetries: opts.maxRetries
|
|
14451
14513
|
}));
|
|
14452
14514
|
await opts.reArm();
|
|
14453
14515
|
}
|
|
14454
14516
|
await opts.deleteTable();
|
|
14455
14517
|
}, opts.logicalId, {
|
|
14456
|
-
maxRetries:
|
|
14518
|
+
maxRetries: opts.maxRetries,
|
|
14457
14519
|
isRetryable: (message) => isIndexBusyDeleteError(message),
|
|
14458
14520
|
logger: opts.logger,
|
|
14459
14521
|
sleep: (ms) => opts.sleepSeam.sleep ? opts.sleepSeam.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
|
|
@@ -14992,7 +15054,8 @@ function desiredIndexEntriesByName(value) {
|
|
|
14992
15054
|
* Test seam for the index-busy `DeleteTable` retry's backoff (issue #1931),
|
|
14993
15055
|
* mirroring the sibling provider's export of the same name. Production leaves
|
|
14994
15056
|
* `sleep` undefined so `withRetry`'s real schedule applies; a test injects a
|
|
14995
|
-
* no-op so the ~
|
|
15057
|
+
* no-op so the ~95s of backoff at this type's budget
|
|
15058
|
+
* (`TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`) does not have to be waited out.
|
|
14996
15059
|
*
|
|
14997
15060
|
* One seam PER PROVIDER rather than one on the shared module: a `Table` test
|
|
14998
15061
|
* that silences this backoff must not also silence the `GlobalTable` one, since
|
|
@@ -15210,7 +15273,7 @@ var DynamoDBTableProvider = class {
|
|
|
15210
15273
|
}
|
|
15211
15274
|
await this.dynamoDBClient.send(new UpdateTableCommand(updateInput));
|
|
15212
15275
|
if (billingMode !== liveBillingMode) await this.waitForTableAndIndexesActive(physicalId, BILLING_FLIP_ACTIVE_WAIT_ATTEMPTS);
|
|
15213
|
-
else await this.waitForTableActiveAfterUpdate(physicalId,
|
|
15276
|
+
else await this.waitForTableActiveAfterUpdate(physicalId, 60);
|
|
15214
15277
|
this.logger.debug(`Updated BillingMode/ProvisionedThroughput/TableClass on DynamoDB table ${physicalId}`);
|
|
15215
15278
|
}
|
|
15216
15279
|
if (JSON.stringify(properties["OnDemandThroughput"]) !== JSON.stringify(previousProperties["OnDemandThroughput"])) {
|
|
@@ -15338,6 +15401,7 @@ var DynamoDBTableProvider = class {
|
|
|
15338
15401
|
logicalId,
|
|
15339
15402
|
physicalId,
|
|
15340
15403
|
typeLabel: "table",
|
|
15404
|
+
maxRetries: 14,
|
|
15341
15405
|
logger: this.logger,
|
|
15342
15406
|
deleteTable: async () => {
|
|
15343
15407
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
@@ -15697,10 +15761,10 @@ var DynamoDBTableProvider = class {
|
|
|
15697
15761
|
* blanket raise would turn every genuinely wedged capacity edit into a
|
|
15698
15762
|
* 10-minute hang.
|
|
15699
15763
|
*/
|
|
15700
|
-
async waitForTableActiveAfterUpdate(tableName, maxAttempts =
|
|
15764
|
+
async waitForTableActiveAfterUpdate(tableName, maxAttempts = 60) {
|
|
15701
15765
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
15702
15766
|
if ((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.TableStatus === "ACTIVE") return;
|
|
15703
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
15767
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
15704
15768
|
}
|
|
15705
15769
|
throw new Error(`Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds after UpdateTable`);
|
|
15706
15770
|
}
|
|
@@ -17783,6 +17847,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
17783
17847
|
logicalId,
|
|
17784
17848
|
physicalId,
|
|
17785
17849
|
typeLabel: "GlobalTable",
|
|
17850
|
+
maxRetries: 8,
|
|
17786
17851
|
logger: this.logger,
|
|
17787
17852
|
deleteTable: async () => {
|
|
17788
17853
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
@@ -18456,10 +18521,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
18456
18521
|
* Delete replica update. Replica deletion typically takes 1–5 min;
|
|
18457
18522
|
* cap at 10 min.
|
|
18458
18523
|
*/
|
|
18459
|
-
async waitForReplicaGone(tableName, region, logicalId, maxAttempts =
|
|
18524
|
+
async waitForReplicaGone(tableName, region, logicalId, maxAttempts = REPLICA_GONE_WAIT_ATTEMPTS) {
|
|
18460
18525
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
18461
18526
|
if (!(await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region)) return;
|
|
18462
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
18527
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
18463
18528
|
} catch (err) {
|
|
18464
18529
|
if (err instanceof ResourceNotFoundException$1) return;
|
|
18465
18530
|
throw err;
|
|
@@ -18480,7 +18545,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
18480
18545
|
async waitForTableGone(tableName, logicalId, maxAttempts = 600) {
|
|
18481
18546
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
18482
18547
|
await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
|
|
18483
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
18548
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
18484
18549
|
} catch (err) {
|
|
18485
18550
|
if (err instanceof ResourceNotFoundException$1) return;
|
|
18486
18551
|
throw err;
|
|
@@ -18575,10 +18640,33 @@ function autoScalingResourceId(tableName, indexName) {
|
|
|
18575
18640
|
*/
|
|
18576
18641
|
const autoScalingRetryDelays = {};
|
|
18577
18642
|
/**
|
|
18643
|
+
* Polls `waitForReplicaGone` spends waiting for a replica to disappear.
|
|
18644
|
+
*
|
|
18645
|
+
* Named rather than inlined because it is a TERM of the per-resource deadline
|
|
18646
|
+
* arithmetic on this type's delete path (see
|
|
18647
|
+
* `GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`), which prices it through
|
|
18648
|
+
* {@link INDEX_SETTLE_POLL_INTERVAL_MS} — the sleep this loop now reads rather
|
|
18649
|
+
* than spelling its own `1000`, so the two cannot drift while that arithmetic
|
|
18650
|
+
* claims they agree.
|
|
18651
|
+
*/
|
|
18652
|
+
const REPLICA_GONE_WAIT_ATTEMPTS = 600;
|
|
18653
|
+
/**
|
|
18654
|
+
* Polls `waitForTableGone` spends confirming the table is actually gone.
|
|
18655
|
+
*
|
|
18656
|
+
* EXPORTED, unlike its replica sibling, because the delete-path budget fence
|
|
18657
|
+
* reads it: this wait runs AFTER the index-busy retry loop and only when that
|
|
18658
|
+
* loop SUCCEEDS, which is what keeps it out of the loop's own worst case and
|
|
18659
|
+
* puts it into the late-success one (both spelled out on
|
|
18660
|
+
* `GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`).
|
|
18661
|
+
*/
|
|
18662
|
+
const TABLE_GONE_WAIT_ATTEMPTS = 600;
|
|
18663
|
+
/**
|
|
18578
18664
|
* Test seam for the index-busy `DeleteTable` retry's backoff (issue #1830),
|
|
18579
18665
|
* mirroring {@link autoScalingRetryDelays}. Production leaves `sleep`
|
|
18580
18666
|
* undefined so `withRetry`'s real schedule applies; a test injects a no-op so
|
|
18581
|
-
* the ~47s
|
|
18667
|
+
* the ~47s of backoff at this type's budget
|
|
18668
|
+
* (`GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`) does not have to be waited
|
|
18669
|
+
* out. Kept SEPARATE from the
|
|
18582
18670
|
* auto-scaling seam so a test can slow one path without silencing the other.
|
|
18583
18671
|
*/
|
|
18584
18672
|
const deleteTableRetryDelays = {};
|
|
@@ -38843,6 +38931,46 @@ const MFA_FACTOR_SMS = "SMS_MFA";
|
|
|
38843
38931
|
const MFA_FACTOR_SOFTWARE_TOKEN = "SOFTWARE_TOKEN_MFA";
|
|
38844
38932
|
const MFA_FACTOR_EMAIL_OTP = "EMAIL_OTP";
|
|
38845
38933
|
/**
|
|
38934
|
+
* CFn path every `MfaConfiguration` shape refusal in this provider reports.
|
|
38935
|
+
* One constant so the three sites that READ the property -- `create`,
|
|
38936
|
+
* `update`, and `narrowMfaConfiguration` (the diff / effectiveProperties
|
|
38937
|
+
* side) -- cannot word the same fault differently. `buildMfaConfigRequest`
|
|
38938
|
+
* performs no read of its own; it is handed the already-guarded value.
|
|
38939
|
+
*/
|
|
38940
|
+
const MFA_CONFIGURATION_PATH = "AWS::Cognito::UserPool MfaConfiguration";
|
|
38941
|
+
function classifyDeclaredMfaConfiguration(raw) {
|
|
38942
|
+
if (raw === void 0) return "absent";
|
|
38943
|
+
if (typeof raw !== "string") return "refused";
|
|
38944
|
+
return raw.trim() === "" ? "blank" : "usable";
|
|
38945
|
+
}
|
|
38946
|
+
/**
|
|
38947
|
+
* Read the template's `MfaConfiguration` through the shape guard AND fold a
|
|
38948
|
+
* TRIMMED-BLANK value to absence.
|
|
38949
|
+
*
|
|
38950
|
+
* The fold is the load-bearing half. `requireConfigString` short-circuits on
|
|
38951
|
+
* `fallback === '' && typeof value === 'string'` (config-shape.ts), so against
|
|
38952
|
+
* the blank fallback this site uses it returns ANY string verbatim --
|
|
38953
|
+
* including `' '`. That value is truthy, so it rode the
|
|
38954
|
+
* `if (mfaConfiguration && ...)` gates onto `CreateUserPool` / `UpdateUserPool`
|
|
38955
|
+
* and the `|| default` in `buildMfaConfigRequest` onto `SetUserPoolMfaConfig`,
|
|
38956
|
+
* where AWS rejects the enum and the deploy fails -- while the blank-value
|
|
38957
|
+
* warning promised "the pool deploys with the default MFA configuration
|
|
38958
|
+
* instead". Two spellings of blank behaved differently and the message
|
|
38959
|
+
* described only one of them.
|
|
38960
|
+
*
|
|
38961
|
+
* Folding here rather than widening the guard keeps the change local to the
|
|
38962
|
+
* one site whose fallback is blank, and makes the wire behavior match what the
|
|
38963
|
+
* warning says: a blank declares nothing, so the default applies.
|
|
38964
|
+
*
|
|
38965
|
+
* Every read site MUST go through this, including `narrowMfaConfiguration` --
|
|
38966
|
+
* a diff side that folded differently from the wire is the phantom drift this
|
|
38967
|
+
* whole mechanism exists to remove.
|
|
38968
|
+
*/
|
|
38969
|
+
function readDeclaredMfaConfiguration(raw, options) {
|
|
38970
|
+
const value = requireConfigString(raw, "", MFA_CONFIGURATION_PATH, options);
|
|
38971
|
+
return value.trim() === "" ? "" : value;
|
|
38972
|
+
}
|
|
38973
|
+
/**
|
|
38846
38974
|
* Read `EnabledMfas` as the pair (recognized list, "the template asked for a
|
|
38847
38975
|
* factor at all").
|
|
38848
38976
|
*
|
|
@@ -38866,14 +38994,16 @@ function readEnabledMfas(properties) {
|
|
|
38866
38994
|
return {
|
|
38867
38995
|
factors,
|
|
38868
38996
|
declaresFactor: factors.length > 0,
|
|
38869
|
-
malformed: false
|
|
38997
|
+
malformed: false,
|
|
38998
|
+
blank: false
|
|
38870
38999
|
};
|
|
38871
39000
|
}
|
|
38872
39001
|
const malformed = raw !== void 0 && raw !== null && raw !== "";
|
|
38873
39002
|
return {
|
|
38874
39003
|
factors: void 0,
|
|
38875
39004
|
declaresFactor: malformed,
|
|
38876
|
-
malformed
|
|
39005
|
+
malformed,
|
|
39006
|
+
blank: raw === ""
|
|
38877
39007
|
};
|
|
38878
39008
|
}
|
|
38879
39009
|
/**
|
|
@@ -38912,9 +39042,17 @@ function hasMfaConfigProps(properties) {
|
|
|
38912
39042
|
* enables an MFA FACTOR — `OPTIONAL` if it does, `OFF` (CloudFormation's own
|
|
38913
39043
|
* default) if it does not. The body comment on that decision explains why both
|
|
38914
39044
|
* halves of the factor test are load-bearing.
|
|
39045
|
+
*
|
|
39046
|
+
* `declaredMfaConfiguration` is the caller's ALREADY-GUARDED read of
|
|
39047
|
+
* `properties['MfaConfiguration']` (issue #1925 item 2), with `''` meaning
|
|
39048
|
+
* "the template declared none, or declared a value the shape guard refused".
|
|
39049
|
+
* The read happens at the caller so it runs ONCE per deploy path — the value
|
|
39050
|
+
* is also needed for the `CreateUserPool` / `UpdateUserPool` forward — and so
|
|
39051
|
+
* the create path can REFUSE a malformed value while the update path only
|
|
39052
|
+
* warns, which is the split `requireConfigString`'s `onUnusable` encodes.
|
|
38915
39053
|
*/
|
|
38916
|
-
function buildMfaConfigRequest(physicalId, properties, logger) {
|
|
38917
|
-
const { factors: enabledMfas, declaresFactor, malformed: enabledMfasMalformed } = readEnabledMfas(properties);
|
|
39054
|
+
function buildMfaConfigRequest(physicalId, properties, logger, declaredMfaConfiguration = "") {
|
|
39055
|
+
const { factors: enabledMfas, declaresFactor, malformed: enabledMfasMalformed, blank: enabledMfasBlank } = readEnabledMfas(properties);
|
|
38918
39056
|
const emailMessage = properties["EmailAuthenticationMessage"] || void 0;
|
|
38919
39057
|
const emailSubject = properties["EmailAuthenticationSubject"] || void 0;
|
|
38920
39058
|
const webAuthnRpId = properties["WebAuthnRelyingPartyID"] || void 0;
|
|
@@ -38934,17 +39072,83 @@ function buildMfaConfigRequest(physicalId, properties, logger) {
|
|
|
38934
39072
|
...webAuthnUserVerification !== void 0 ? { UserVerification: webAuthnUserVerification } : {}
|
|
38935
39073
|
};
|
|
38936
39074
|
const enablesMfaFactor = declaresFactor || request.EmailMfaConfiguration !== void 0;
|
|
38937
|
-
request.MfaConfiguration =
|
|
39075
|
+
request.MfaConfiguration = declaredMfaConfiguration || (enablesMfaFactor ? "OPTIONAL" : "OFF");
|
|
39076
|
+
if (enabledMfasBlank) logger?.warn(`UserPool ${physicalId}: EnabledMfas is an empty string, which declares no MFA factor and is treated as absent — a mangled Fn::Join or an empty String parameter collapses to this shape. Nothing is enabled from it and MfaConfiguration is ${request.MfaConfiguration}; declare the factor names as a list (e.g. ["${MFA_FACTOR_SOFTWARE_TOKEN}"]) if a factor was intended.`);
|
|
39077
|
+
const anyFactorBlock = request.SmsMfaConfiguration !== void 0 || request.SoftwareTokenMfaConfiguration !== void 0 || request.EmailMfaConfiguration !== void 0;
|
|
39078
|
+
if (request.MfaConfiguration === "OFF" && anyFactorBlock) logger?.warn(`UserPool ${physicalId}: MfaConfiguration is pinned to OFF while an MFA factor block is configured. The factor configuration is still sent, but the pool deploys with MFA DISABLED. Remove MfaConfiguration (or set it to ON / OPTIONAL) to enable the declared factor.`);
|
|
38938
39079
|
const dropped = unrecognizedFactors.map((f) => JSON.stringify(f) ?? String(f));
|
|
38939
39080
|
if (enabledMfasMalformed) dropped.push(`${JSON.stringify(properties["EnabledMfas"])} (not a list)`);
|
|
38940
39081
|
if (dropped.length > 0) {
|
|
38941
|
-
const anyFactorBlock = request.SmsMfaConfiguration !== void 0 || request.SoftwareTokenMfaConfiguration !== void 0 || request.EmailMfaConfiguration !== void 0;
|
|
38942
39082
|
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
39083
|
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
39084
|
}
|
|
38945
39085
|
return request;
|
|
38946
39086
|
}
|
|
38947
39087
|
/**
|
|
39088
|
+
* The `MfaConfiguration` cdkd actually SENDS for this bag, or `undefined` when
|
|
39089
|
+
* no call carries the field at all (nothing declared and no MFA-routed
|
|
39090
|
+
* property, so AWS applies its own default of OFF).
|
|
39091
|
+
*
|
|
39092
|
+
* Exactly one call ever carries it: `SetUserPoolMfaConfig` owns it whenever any
|
|
39093
|
+
* MFA-routed property is present -- and always sets it, because that API is a
|
|
39094
|
+
* full replace -- otherwise the `CreateUserPool` / `UpdateUserPool` forward
|
|
39095
|
+
* carries it, and only when it is non-blank.
|
|
39096
|
+
*
|
|
39097
|
+
* Delegates to {@link buildMfaConfigRequest} rather than re-deriving the
|
|
39098
|
+
* OPTIONAL/OFF default, so the recorded value and the wire value cannot
|
|
39099
|
+
* disagree. That function is pure and synchronous, which is what lets
|
|
39100
|
+
* `canonicalizeDesiredProperties` -- which runs inside the diff -- call it too.
|
|
39101
|
+
* The empty `physicalId` and absent logger are unused on this path: every
|
|
39102
|
+
* message it can emit is behind `logger?.`, so passing none makes the call
|
|
39103
|
+
* silent, which is required of the diff side.
|
|
39104
|
+
*/
|
|
39105
|
+
function resolveSentMfaConfiguration(properties, declaredMfaConfiguration) {
|
|
39106
|
+
const request = buildMfaConfigRequest("", properties, void 0, declaredMfaConfiguration);
|
|
39107
|
+
if (request) return request.MfaConfiguration;
|
|
39108
|
+
return declaredMfaConfiguration || void 0;
|
|
39109
|
+
}
|
|
39110
|
+
/**
|
|
39111
|
+
* Replace a DECLARED-but-unusable `MfaConfiguration` with the value cdkd
|
|
39112
|
+
* actually sends, or drop the key when nothing is sent. Returns the input
|
|
39113
|
+
* object unchanged when nothing applies, so the common case compares
|
|
39114
|
+
* byte-for-byte as before.
|
|
39115
|
+
*
|
|
39116
|
+
* ONE helper feeding BOTH sides of the decision, per `.claude/rules/
|
|
39117
|
+
* providers.md`: the provisioning path returns it as `effectiveProperties` so
|
|
39118
|
+
* STATE describes what AWS holds, and `canonicalizeDesiredProperties` applies
|
|
39119
|
+
* it to the DESIRED bag so the next diff compares the same thing. Shipping
|
|
39120
|
+
* only the first half is worse than shipping neither -- the template would keep
|
|
39121
|
+
* declaring the malformed value, and every later deploy would read the
|
|
39122
|
+
* narrowing back as a user-made change.
|
|
39123
|
+
*
|
|
39124
|
+
* Without this, all three substitution arms (the replay-CREATE downgrade, the
|
|
39125
|
+
* update-path warn-and-default, and the blank-string default) recorded the
|
|
39126
|
+
* declared `null` / `''` while AWS held `OPTIONAL` / `OFF` -- permanent phantom
|
|
39127
|
+
* drift that `cdkd drift --revert` re-issues forever with nothing to apply.
|
|
39128
|
+
*
|
|
39129
|
+
* A value that is merely ABSENT is deliberately NOT filled in. Reaching for
|
|
39130
|
+
* `effectiveProperties` is licensed only where the provider KNOWS it
|
|
39131
|
+
* substituted something it was told, and an omitted property was never a
|
|
39132
|
+
* declaration to contradict; writing the resolved default into the DESIRED
|
|
39133
|
+
* baseline there would also disable the #1160 absent-field removal derivation,
|
|
39134
|
+
* which reads that side.
|
|
39135
|
+
*/
|
|
39136
|
+
function narrowMfaConfiguration(properties) {
|
|
39137
|
+
const raw = properties["MfaConfiguration"];
|
|
39138
|
+
if (raw === void 0) return properties;
|
|
39139
|
+
const sent = resolveSentMfaConfiguration(properties, readDeclaredMfaConfiguration(raw, { onUnusable: () => {} }));
|
|
39140
|
+
if (sent === raw) return properties;
|
|
39141
|
+
if (sent === void 0) {
|
|
39142
|
+
const withoutMfaConfiguration = { ...properties };
|
|
39143
|
+
delete withoutMfaConfiguration["MfaConfiguration"];
|
|
39144
|
+
return withoutMfaConfiguration;
|
|
39145
|
+
}
|
|
39146
|
+
return {
|
|
39147
|
+
...properties,
|
|
39148
|
+
MfaConfiguration: sent
|
|
39149
|
+
};
|
|
39150
|
+
}
|
|
39151
|
+
/**
|
|
38948
39152
|
* AWS Cognito User Pool Provider
|
|
38949
39153
|
*
|
|
38950
39154
|
* Implements resource provisioning for AWS::Cognito::UserPool using the Cognito SDK.
|
|
@@ -38988,6 +39192,50 @@ var CognitoUserPoolProvider = class {
|
|
|
38988
39192
|
"WebAuthnUserVerification"
|
|
38989
39193
|
])]]);
|
|
38990
39194
|
unhandledByDesign = /* @__PURE__ */ new Map([["AWS::Cognito::UserPool", /* @__PURE__ */ new Map([["WebAuthnFactorConfiguration", "No SDK wire path: @aws-sdk/client-cognito-identity-provider has no field accepting SINGLE_FACTOR | MULTI_FACTOR_WITH_USER_VERIFICATION (not on CreateUserPool/UpdateUserPool, nor SetUserPoolMfaConfig.WebAuthnConfiguration which only carries RelyingPartyId/UserVerification); CC-API-registry-only property"]])]]);
|
|
39195
|
+
/**
|
|
39196
|
+
* Warn when `MfaConfiguration` is declared as a BLANK string (issue #1925
|
|
39197
|
+
* item 2, review round 2).
|
|
39198
|
+
*
|
|
39199
|
+
* `requireConfigString` accepts any string against a blank fallback, so `''`
|
|
39200
|
+
* passes the shape guard and is then substituted by the OPTIONAL/OFF default
|
|
39201
|
+
* downstream. That is the exact silent-default class the guard exists to
|
|
39202
|
+
* kill, reached one shape further in: on main a WebAuthn-only pool declaring
|
|
39203
|
+
* `MfaConfiguration: ''` failed LOUDLY with an AWS enum rejection, and with
|
|
39204
|
+
* the guard alone it would deploy MFA OFF with no message anywhere. A
|
|
39205
|
+
* collapsed `Fn::Join` or an empty `String` parameter produces exactly this.
|
|
39206
|
+
*
|
|
39207
|
+
* The twin of the blank-`EnabledMfas` warning one property over, and warned
|
|
39208
|
+
* for the same reason it is: the value stays absence ON THE WIRE (the
|
|
39209
|
+
* substitution is what CloudFormation's own default does), so only the
|
|
39210
|
+
* silence is removed.
|
|
39211
|
+
*
|
|
39212
|
+
* `.trim()` matches the fold in {@link readDeclaredMfaConfiguration}, and the
|
|
39213
|
+
* two must agree: `requireConfigString` does NOT treat whitespace-only as
|
|
39214
|
+
* blank against the blank fallback this site uses, so before that fold a
|
|
39215
|
+
* `' '` was warned about here and then sent to AWS verbatim -- this message
|
|
39216
|
+
* describing a default that never applied.
|
|
39217
|
+
*/
|
|
39218
|
+
warnOnBlankMfaConfiguration(raw) {
|
|
39219
|
+
if (typeof raw !== "string" || raw.trim() !== "") return;
|
|
39220
|
+
this.logger.warn("AWS::Cognito::UserPool MfaConfiguration is an empty string, which declares no MFA mode and is treated as absent -- a collapsed Fn::Join or an empty String parameter produces this shape. The pool deploys with the default MFA configuration instead; declare ON / OPTIONAL / OFF explicitly, or omit the property, to say which was meant.");
|
|
39221
|
+
}
|
|
39222
|
+
/**
|
|
39223
|
+
* The DIFF-side half of the `MfaConfiguration` substitution record.
|
|
39224
|
+
*
|
|
39225
|
+
* Applied by `DiffCalculator` to BOTH comparison sides, so a template that
|
|
39226
|
+
* declares a malformed / blank `MfaConfiguration` compares equal to the state
|
|
39227
|
+
* record holding the value cdkd actually sent for it. Without this half, the
|
|
39228
|
+
* `effectiveProperties` above would make every later deploy read the
|
|
39229
|
+
* substitution back as a user-made change and re-run the update forever.
|
|
39230
|
+
*
|
|
39231
|
+
* Shares ONE helper with the provisioning path rather than re-deriving the
|
|
39232
|
+
* rule, per `.claude/rules/providers.md` -- state and template narrowed by
|
|
39233
|
+
* different code is how this fix would become the bug.
|
|
39234
|
+
*/
|
|
39235
|
+
canonicalizeDesiredProperties(resourceType, properties) {
|
|
39236
|
+
if (resourceType !== "AWS::Cognito::UserPool") return properties;
|
|
39237
|
+
return narrowMfaConfiguration(properties);
|
|
39238
|
+
}
|
|
38991
39239
|
getClient() {
|
|
38992
39240
|
if (!this.cognitoClient) this.cognitoClient = new CognitoIdentityProviderClient(this.providerRegion ? { region: this.providerRegion } : {});
|
|
38993
39241
|
return this.cognitoClient;
|
|
@@ -39008,9 +39256,16 @@ var CognitoUserPoolProvider = class {
|
|
|
39008
39256
|
/**
|
|
39009
39257
|
* Create a Cognito User Pool
|
|
39010
39258
|
*/
|
|
39011
|
-
async create(logicalId, resourceType, properties) {
|
|
39259
|
+
async create(logicalId, resourceType, properties, context) {
|
|
39012
39260
|
this.logger.debug(`Creating Cognito User Pool ${logicalId}`);
|
|
39013
39261
|
const poolName = properties["UserPoolName"] || generateResourceName(logicalId, { maxLength: 128 });
|
|
39262
|
+
let mfaConfiguration;
|
|
39263
|
+
try {
|
|
39264
|
+
mfaConfiguration = readDeclaredMfaConfiguration(properties["MfaConfiguration"], replayWarn(this.logger, context));
|
|
39265
|
+
this.warnOnBlankMfaConfiguration(properties["MfaConfiguration"]);
|
|
39266
|
+
} catch (error) {
|
|
39267
|
+
throw new ProvisioningError(error instanceof Error ? error.message : String(error), resourceType, logicalId, poolName, error instanceof Error ? error : void 0);
|
|
39268
|
+
}
|
|
39014
39269
|
let createdUserPoolId;
|
|
39015
39270
|
try {
|
|
39016
39271
|
const createParams = { PoolName: poolName };
|
|
@@ -39022,7 +39277,7 @@ var CognitoUserPoolProvider = class {
|
|
|
39022
39277
|
}
|
|
39023
39278
|
if (properties["Schema"]) createParams.Schema = properties["Schema"];
|
|
39024
39279
|
if (properties["LambdaConfig"]) createParams.LambdaConfig = properties["LambdaConfig"];
|
|
39025
|
-
if (
|
|
39280
|
+
if (mfaConfiguration && !hasMfaConfigProps(properties)) createParams.MfaConfiguration = mfaConfiguration;
|
|
39026
39281
|
if (properties["UserPoolTags"]) createParams.UserPoolTags = properties["UserPoolTags"];
|
|
39027
39282
|
if (properties["AdminCreateUserConfig"]) createParams.AdminCreateUserConfig = properties["AdminCreateUserConfig"];
|
|
39028
39283
|
if (properties["AccountRecoverySetting"]) createParams.AccountRecoverySetting = properties["AccountRecoverySetting"];
|
|
@@ -39049,8 +39304,9 @@ var CognitoUserPoolProvider = class {
|
|
|
39049
39304
|
const { urlSuffix } = derivePartitionAndUrlSuffix(region);
|
|
39050
39305
|
const providerName = `cognito-idp.${region}.${urlSuffix}/${userPoolId}`;
|
|
39051
39306
|
const providerUrl = `https://cognito-idp.${region}.${urlSuffix}/${userPoolId}`;
|
|
39052
|
-
await this.applyMfaConfig(userPoolId, properties);
|
|
39307
|
+
await this.applyMfaConfig(userPoolId, properties, mfaConfiguration);
|
|
39053
39308
|
this.logger.debug(`Successfully created Cognito User Pool ${logicalId}: ${userPoolId}`);
|
|
39309
|
+
const effectiveProperties = narrowMfaConfiguration(properties);
|
|
39054
39310
|
return {
|
|
39055
39311
|
physicalId: userPoolId,
|
|
39056
39312
|
attributes: {
|
|
@@ -39058,7 +39314,8 @@ var CognitoUserPoolProvider = class {
|
|
|
39058
39314
|
ProviderName: providerName,
|
|
39059
39315
|
ProviderURL: providerUrl,
|
|
39060
39316
|
UserPoolId: userPoolId
|
|
39061
|
-
}
|
|
39317
|
+
},
|
|
39318
|
+
...effectiveProperties !== properties ? { effectiveProperties } : {}
|
|
39062
39319
|
};
|
|
39063
39320
|
} catch (error) {
|
|
39064
39321
|
if (createdUserPoolId) try {
|
|
@@ -39078,10 +39335,109 @@ var CognitoUserPoolProvider = class {
|
|
|
39078
39335
|
* control-plane writes on a freshly-created pool can briefly conflict
|
|
39079
39336
|
* (mirrors DynamoDBTableProvider.retryOnTransientControlPlane).
|
|
39080
39337
|
*/
|
|
39081
|
-
async applyMfaConfig(physicalId, properties) {
|
|
39082
|
-
const request = buildMfaConfigRequest(physicalId, properties, this.logger);
|
|
39338
|
+
async applyMfaConfig(physicalId, properties, declaredMfaConfiguration, options = {}) {
|
|
39339
|
+
const request = buildMfaConfigRequest(physicalId, properties, this.logger, declaredMfaConfiguration);
|
|
39083
39340
|
if (!request) return;
|
|
39084
39341
|
await this.retryOnTransientControlPlane(() => this.getClient().send(new SetUserPoolMfaConfigCommand(request)), `SetUserPoolMfaConfig(${physicalId})`);
|
|
39342
|
+
this.reportMfaConfigurationOff(physicalId, request, declaredMfaConfiguration, options);
|
|
39343
|
+
}
|
|
39344
|
+
/**
|
|
39345
|
+
* Say so when this deploy sends `MfaConfiguration=OFF` without the template
|
|
39346
|
+
* having asked for OFF (issue #1925, third item).
|
|
39347
|
+
*
|
|
39348
|
+
* Everything here keys on the BUILT request, so there is exactly one
|
|
39349
|
+
* definition of "this deploy resolves to OFF", and on `declaredMfaConfiguration
|
|
39350
|
+
* === ''`, i.e. the template did not pin a usable value. `create()` passes
|
|
39351
|
+
* neither a live value nor a `declaredKind`, so it reports nothing -- correct,
|
|
39352
|
+
* since a fresh pool has no value to downgrade.
|
|
39353
|
+
*
|
|
39354
|
+
* Three outcomes rather than one, because the live value can be KNOWN,
|
|
39355
|
+
* known-and-harmless, or UNAVAILABLE, and only the first is a downgrade that
|
|
39356
|
+
* can be named:
|
|
39357
|
+
*
|
|
39358
|
+
* - live `ON` / `OPTIONAL` -- a real downgrade; name the value being lost.
|
|
39359
|
+
* - live `OFF` -- not a downgrade at all; say nothing. This is the arm the
|
|
39360
|
+
* live-value test protects, and it is load-bearing in the SILENT
|
|
39361
|
+
* direction: without it every already-OFF pool and every failed probe
|
|
39362
|
+
* warned, the latter reading `live value was undefined`.
|
|
39363
|
+
* - live UNKNOWN (the probe failed) -- the case that made this necessary.
|
|
39364
|
+
* A malformed but TRUTHY value used to reach `UpdateUserPool`, AWS
|
|
39365
|
+
* rejected the enum, MFA stayed as it was and the deploy failed; now the
|
|
39366
|
+
* guard substitutes and the deploy exits 0 with MFA off. On a role lacking
|
|
39367
|
+
* `cognito-idp:GetUserPoolMfaConfig` -- exactly the role this arm exists
|
|
39368
|
+
* for -- the probe returns nothing, so without this the ONLY path that got
|
|
39369
|
+
* quieter than main would say nothing at all.
|
|
39370
|
+
*/
|
|
39371
|
+
reportMfaConfigurationOff(physicalId, request, declaredMfaConfiguration, options) {
|
|
39372
|
+
if (declaredMfaConfiguration !== "" || request.MfaConfiguration !== "OFF") return;
|
|
39373
|
+
const kind = options.declaredKind ?? "absent";
|
|
39374
|
+
const cause = kind === "refused" ? "the declared MfaConfiguration was refused as malformed (see the warning above), so this update sent MfaConfiguration=OFF instead" : kind === "blank" ? "the declared MfaConfiguration is blank, so it is treated as absent and this update sent MfaConfiguration=OFF" : "the template declares no MfaConfiguration, so this update sent MfaConfiguration=OFF (CloudFormation's own default)";
|
|
39375
|
+
const remedy = (value) => kind === "absent" ? `Declare MfaConfiguration: ${value} in the template to keep it.` : `Repair MfaConfiguration to a literal ${value} to keep it.`;
|
|
39376
|
+
if (options.liveMfaConfiguration === "ON" || options.liveMfaConfiguration === "OPTIONAL") {
|
|
39377
|
+
this.logger.warn(`UserPool ${physicalId}: ${cause} to a pool whose live value was ${options.liveMfaConfiguration} — SetUserPoolMfaConfig is a full replace, so MFA is now OFF. ${remedy(options.liveMfaConfiguration)}`);
|
|
39378
|
+
return;
|
|
39379
|
+
}
|
|
39380
|
+
if (options.liveReadFailed !== true) return;
|
|
39381
|
+
const declaredClause = kind === "absent" ? `the template declares no MfaConfiguration` : `the declared MfaConfiguration could not be used`;
|
|
39382
|
+
this.logger.warn(`UserPool ${physicalId}: ${declaredClause}, so this update sent MfaConfiguration=OFF — and the pool's previous MFA setting could not be read, so cdkd cannot say whether that turned MFA off. Grant cognito-idp:GetUserPoolMfaConfig to the deploy role to have this reported precisely. ${remedy("ON or OPTIONAL")}`);
|
|
39383
|
+
}
|
|
39384
|
+
/**
|
|
39385
|
+
* Read the pool's live `MfaConfiguration` for the undeclared-downgrade
|
|
39386
|
+
* announcement (issue #1925, third item), or `undefined` when it cannot be
|
|
39387
|
+
* determined.
|
|
39388
|
+
*
|
|
39389
|
+
* `SetUserPoolMfaConfig` is a full replace and this provider re-issues it on
|
|
39390
|
+
* every update carrying an MFA-routed property, unconditioned on the MFA
|
|
39391
|
+
* configuration having CHANGED. So a WebAuthn-only template applied to a pool
|
|
39392
|
+
* whose live `MfaConfiguration` is `ON` / `OPTIONAL` — console drift, or a
|
|
39393
|
+
* `cdkd import` of an MFA-enabled pool — writes `OFF` on the next unrelated
|
|
39394
|
+
* property change. That is defensible template-is-truth / CloudFormation
|
|
39395
|
+
* parity and is NOT changed here; only its silence is.
|
|
39396
|
+
*
|
|
39397
|
+
* **Called BEFORE `UpdateUserPool`, defensively — NOT because AWS resets the
|
|
39398
|
+
* field.** MEASURED us-east-1 2026-08-18: a pool at `ON` with software-token
|
|
39399
|
+
* enabled keeps `ON` through an `UpdateUserPool` that omits
|
|
39400
|
+
* `MfaConfiguration`, confirmed by both `get-user-pool-mfa-config` and
|
|
39401
|
+
* `describe-user-pool`, with controls proving the call really ran (a
|
|
39402
|
+
* `--auto-verified-attributes` change in the same request applied) and that
|
|
39403
|
+
* the field is writable here rather than ignored (an explicit
|
|
39404
|
+
* `--mfa-configuration OFF` did reset it). So AWS's blanket "unspecified
|
|
39405
|
+
* parameters are set to their default value" holds field by field —
|
|
39406
|
+
* `AutoVerifiedAttributes` DOES reset, `MfaConfiguration` does not — and
|
|
39407
|
+
* there is no MFA-OFF window to design around.
|
|
39408
|
+
*
|
|
39409
|
+
* The earlier ordering was reached from that doc sentence alone and is kept
|
|
39410
|
+
* only because it is strictly safer and already tested: reading first cannot
|
|
39411
|
+
* report a value this same call clobbered, whatever AWS does later. Do not
|
|
39412
|
+
* re-derive a redesign from the doc sentence — it has been measured.
|
|
39413
|
+
*
|
|
39414
|
+
* Best-effort by construction: the read exists only to ANNOUNCE, so a
|
|
39415
|
+
* failure must not fail the update the user asked for.
|
|
39416
|
+
*
|
|
39417
|
+
* **This adds `cognito-idp:GetUserPoolMfaConfig` to the permissions a deploy
|
|
39418
|
+
* role wants.** The action was previously needed only by `readCurrentState`
|
|
39419
|
+
* (drift / import), so a role scoped to deploying could legitimately lack it.
|
|
39420
|
+
*
|
|
39421
|
+
* A failure is REPORTED rather than swallowed, but not here: this method's
|
|
39422
|
+
* gate is a deliberate SUPERSET of the announcement condition (see the call
|
|
39423
|
+
* site in `update()`), so warning at the point of failure fired on templates
|
|
39424
|
+
* that structurally cannot downgrade -- an `EnabledMfas` with no
|
|
39425
|
+
* `MfaConfiguration` resolves to OPTIONAL, and a least-privileged role saw
|
|
39426
|
+
* "cdkd cannot say whether it turns MFA off" on every deploy of it. The flag
|
|
39427
|
+
* returned here is consumed by `reportMfaConfigurationOff`, which speaks only
|
|
39428
|
+
* where the request actually resolves to OFF.
|
|
39429
|
+
*/
|
|
39430
|
+
async readLiveMfaConfiguration(physicalId) {
|
|
39431
|
+
try {
|
|
39432
|
+
const live = await this.getClient().send(new GetUserPoolMfaConfigCommand({ UserPoolId: physicalId }));
|
|
39433
|
+
return live.MfaConfiguration !== void 0 ? {
|
|
39434
|
+
value: live.MfaConfiguration,
|
|
39435
|
+
failed: false
|
|
39436
|
+
} : { failed: false };
|
|
39437
|
+
} catch (error) {
|
|
39438
|
+
this.logger.debug(`GetUserPoolMfaConfig failed for UserPool ${physicalId} (${error instanceof Error ? error.name : typeof error}): ${error instanceof Error ? error.message : String(error)}`);
|
|
39439
|
+
return { failed: true };
|
|
39440
|
+
}
|
|
39085
39441
|
}
|
|
39086
39442
|
/**
|
|
39087
39443
|
* Retry a Cognito control-plane call on transient "settling" errors. A
|
|
@@ -39111,6 +39467,9 @@ var CognitoUserPoolProvider = class {
|
|
|
39111
39467
|
*/
|
|
39112
39468
|
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
39113
39469
|
this.logger.debug(`Updating Cognito User Pool ${logicalId}: ${physicalId}`);
|
|
39470
|
+
const mfaConfiguration = readDeclaredMfaConfiguration(properties["MfaConfiguration"], { onUnusable: (message) => this.logger.warn(message) });
|
|
39471
|
+
this.warnOnBlankMfaConfiguration(properties["MfaConfiguration"]);
|
|
39472
|
+
const liveMfa = mfaConfiguration === "" && hasMfaConfigProps(properties) ? await this.readLiveMfaConfiguration(physicalId) : void 0;
|
|
39114
39473
|
try {
|
|
39115
39474
|
const updateParams = { UserPoolId: physicalId };
|
|
39116
39475
|
if (properties["Policies"]) {
|
|
@@ -39119,7 +39478,7 @@ var CognitoUserPoolProvider = class {
|
|
|
39119
39478
|
}
|
|
39120
39479
|
if (properties["LambdaConfig"]) updateParams.LambdaConfig = properties["LambdaConfig"];
|
|
39121
39480
|
if (properties["AutoVerifiedAttributes"]) updateParams.AutoVerifiedAttributes = properties["AutoVerifiedAttributes"];
|
|
39122
|
-
if (properties
|
|
39481
|
+
if (mfaConfiguration && !hasMfaConfigProps(properties)) updateParams.MfaConfiguration = mfaConfiguration;
|
|
39123
39482
|
if (properties["AdminCreateUserConfig"]) updateParams.AdminCreateUserConfig = properties["AdminCreateUserConfig"];
|
|
39124
39483
|
if (properties["AccountRecoverySetting"]) updateParams.AccountRecoverySetting = properties["AccountRecoverySetting"];
|
|
39125
39484
|
if (properties["UserPoolTags"]) updateParams.UserPoolTags = properties["UserPoolTags"];
|
|
@@ -39137,13 +39496,18 @@ var CognitoUserPoolProvider = class {
|
|
|
39137
39496
|
if (properties["UserPoolTier"]) updateParams.UserPoolTier = properties["UserPoolTier"];
|
|
39138
39497
|
await this.getClient().send(new UpdateUserPoolCommand(updateParams));
|
|
39139
39498
|
await this.reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, properties["Schema"], previousProperties["Schema"]);
|
|
39140
|
-
await this.applyMfaConfig(physicalId, properties
|
|
39499
|
+
await this.applyMfaConfig(physicalId, properties, mfaConfiguration, {
|
|
39500
|
+
...liveMfa?.value !== void 0 ? { liveMfaConfiguration: liveMfa.value } : {},
|
|
39501
|
+
...liveMfa?.failed === true ? { liveReadFailed: true } : {},
|
|
39502
|
+
declaredKind: classifyDeclaredMfaConfiguration(properties["MfaConfiguration"])
|
|
39503
|
+
});
|
|
39141
39504
|
this.logger.debug(`Successfully updated Cognito User Pool ${logicalId}`);
|
|
39142
39505
|
const userPool = (await this.getClient().send(new DescribeUserPoolCommand({ UserPoolId: physicalId }))).UserPool;
|
|
39143
39506
|
const region = await this.getClient().config.region();
|
|
39144
39507
|
const { urlSuffix } = derivePartitionAndUrlSuffix(region);
|
|
39145
39508
|
const providerName = `cognito-idp.${region}.${urlSuffix}/${physicalId}`;
|
|
39146
39509
|
const providerUrl = `https://cognito-idp.${region}.${urlSuffix}/${physicalId}`;
|
|
39510
|
+
const effectiveProperties = narrowMfaConfiguration(properties);
|
|
39147
39511
|
return {
|
|
39148
39512
|
physicalId,
|
|
39149
39513
|
wasReplaced: false,
|
|
@@ -39152,7 +39516,8 @@ var CognitoUserPoolProvider = class {
|
|
|
39152
39516
|
ProviderName: providerName,
|
|
39153
39517
|
ProviderURL: providerUrl,
|
|
39154
39518
|
UserPoolId: physicalId
|
|
39155
|
-
}
|
|
39519
|
+
},
|
|
39520
|
+
...effectiveProperties !== properties ? { effectiveProperties } : {}
|
|
39156
39521
|
};
|
|
39157
39522
|
} catch (error) {
|
|
39158
39523
|
if (error instanceof ResourceUpdateNotSupportedError || error instanceof ProvisioningError) throw error;
|
|
@@ -80881,7 +81246,7 @@ function createMigrateCommand() {
|
|
|
80881
81246
|
*/
|
|
80882
81247
|
function buildProgram() {
|
|
80883
81248
|
const program = new Command();
|
|
80884
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.
|
|
81249
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.35");
|
|
80885
81250
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
80886
81251
|
const { profile } = actionCommand.optsWithGlobals();
|
|
80887
81252
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|