@go-to-k/cdkd 0.283.27 → 0.283.28
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-uNJDVURJ.js → asg-provider-DzaowTd_.js} +2 -2
- package/dist/{asg-provider-uNJDVURJ.js.map → asg-provider-DzaowTd_.js.map} +1 -1
- package/dist/cli.js +457 -130
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-CsvKYG0d.js → deploy-engine-5rA6lMn8.js} +3 -3
- package/dist/{deploy-engine-CsvKYG0d.js.map → deploy-engine-5rA6lMn8.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-5rA6lMn8.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-DzaowTd_.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";
|
|
@@ -13989,6 +13989,209 @@ function isRecord$1(value) {
|
|
|
13989
13989
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13990
13990
|
}
|
|
13991
13991
|
|
|
13992
|
+
//#endregion
|
|
13993
|
+
//#region src/provisioning/dynamodb-warm-throughput.ts
|
|
13994
|
+
/**
|
|
13995
|
+
* The two DynamoDB `WarmThroughput` rules that `AWS::DynamoDB::Table` and
|
|
13996
|
+
* `AWS::DynamoDB::GlobalTable` both need, in ONE spelling.
|
|
13997
|
+
*
|
|
13998
|
+
* `WarmThroughput` is the same CloudFormation block on both types, and both
|
|
13999
|
+
* providers have to answer the same two questions about it before an
|
|
14000
|
+
* `UpdateTable` / `CreateTable` goes out:
|
|
14001
|
+
*
|
|
14002
|
+
* 1. **Is this block SENDABLE, and as what numbers?** CloudFormation is
|
|
14003
|
+
* stringly typed, so `{ReadUnitsPerSecond: '12000'}` — or anything that
|
|
14004
|
+
* came back from an `Fn::Sub` — arrives as a STRING and must not be
|
|
14005
|
+
* forwarded verbatim into a numeric `Long` field.
|
|
14006
|
+
* 2. **Would sending it LOWER what AWS already reports?** Warm throughput
|
|
14007
|
+
* only ever rises with a table's traffic and AWS REJECTS a call that
|
|
14008
|
+
* lowers it (`decreasing WarmThroughput is not supported`, measured live
|
|
14009
|
+
* us-east-1 2026-08-13 for issue #1768).
|
|
14010
|
+
*
|
|
14011
|
+
* **Provenance, stated precisely because it is easy to over-claim.** Exactly
|
|
14012
|
+
* ONE of these rules ever shipped: `AWS::DynamoDB::Table` got them in PR #1808
|
|
14013
|
+
* (issues #1760 / #1768), and the `AWS::DynamoDB::GlobalTable` side never
|
|
14014
|
+
* existed outside the change that created this file (issue #1857). So this is
|
|
14015
|
+
* the Table rule LIFTED — not two shipped rules reconciled, and no deployed
|
|
14016
|
+
* behaviour changed for either type when it moved here.
|
|
14017
|
+
*
|
|
14018
|
+
* What WAS compared is the lifted rule against the GlobalTable spelling
|
|
14019
|
+
* drafted alongside it, over the shapes both had to answer (a quoted numeric
|
|
14020
|
+
* string, a partially usable block, a mixed decrease/increase, an absent live
|
|
14021
|
+
* value, a zero, a negative, a non-numeric string, a whitespace string, an
|
|
14022
|
+
* explicit `undefined` / `null`, a boolean, an empty block, a scalar, an
|
|
14023
|
+
* array). {@link isWarmThroughputDecrease} agreed on every one; the coercion
|
|
14024
|
+
* agreed on all but a whitespace-only string, where a bare `Number(' ')` is
|
|
14025
|
+
* `0` rather than `NaN`. This module keeps the REFUSING answer for that shape,
|
|
14026
|
+
* since `' '` is not a capacity anyone declared and a warm throughput of 0
|
|
14027
|
+
* is not a request AWS can honour. That is a DRAFT reconciled against a
|
|
14028
|
+
* shipped rule, which is worth less than two shipped rules agreeing — the
|
|
14029
|
+
* reason to read the probe list as a design note rather than as field
|
|
14030
|
+
* evidence.
|
|
14031
|
+
*
|
|
14032
|
+
* Living here rather than in either provider is the point. A decrease guard is
|
|
14033
|
+
* three clauses that all FAIL OPEN for different reasons, and two files
|
|
14034
|
+
* spelling it independently is two chances for a later "fix" to change one of
|
|
14035
|
+
* them — at which point the sibling type silently keeps the old answer, and
|
|
14036
|
+
* nothing in the tree says which is intended. Same class as
|
|
14037
|
+
* `emr-configuration.ts`, and the same reason.
|
|
14038
|
+
*
|
|
14039
|
+
* Deliberately NOT here: everything only ONE provider has. The `Table` side's
|
|
14040
|
+
* `isSendableWarmThroughput` / `isRefusedWarmThroughput` /
|
|
14041
|
+
* `declaresWarmThroughput` / `warmThroughputAlreadyMatches` are its drift-side
|
|
14042
|
+
* and already-matches gates, which `GlobalTable` has no counterpart to (issue
|
|
14043
|
+
* #1742 strips the per-index `WarmThroughput` from BOTH of its drift
|
|
14044
|
+
* comparison sides unconditionally, so drift never asks the question there);
|
|
14045
|
+
* the `GlobalTable` side's `warmThroughputDiagnostic` builds a
|
|
14046
|
+
* `ThroughputDiagnostic` that only that provider's collector understands.
|
|
14047
|
+
* Moving a helper with one caller here would buy nothing and cost a hop.
|
|
14048
|
+
*
|
|
14049
|
+
* Issues: #1760 / #1768 (Table, PR #1808), #1857 (GlobalTable).
|
|
14050
|
+
*/
|
|
14051
|
+
/**
|
|
14052
|
+
* The two `WarmThroughput` members, in the ONE order every message, every
|
|
14053
|
+
* comparison and every emitted block uses. A shared order is what makes two
|
|
14054
|
+
* blocks carrying the same numbers compare equal by `deepEqual` and serialize
|
|
14055
|
+
* to the same wire bytes regardless of the order the template wrote them in.
|
|
14056
|
+
*/
|
|
14057
|
+
const WARM_THROUGHPUT_MEMBERS = ["ReadUnitsPerSecond", "WriteUnitsPerSecond"];
|
|
14058
|
+
/**
|
|
14059
|
+
* A CloudFormation-borne numeric property, or `undefined` when the value is
|
|
14060
|
+
* not a usable number.
|
|
14061
|
+
*
|
|
14062
|
+
* Plain `Number()` coercion is NOT good enough and the tree learned it three
|
|
14063
|
+
* separate times: `Number(null)`, `Number('')`, `Number([])`, `Number(false)`
|
|
14064
|
+
* and `Number(' ')` are all **0**, not `NaN` — so a live `0` would compare
|
|
14065
|
+
* EQUAL to a desired `null` / `''` / `[]` / `false`, and a whitespace-only
|
|
14066
|
+
* string would be forwarded as a request for zero units.
|
|
14067
|
+
*
|
|
14068
|
+
* A YAML-borne numeric STRING is still accepted, because that is a real
|
|
14069
|
+
* template shape and `'12000'` genuinely means 12000.
|
|
14070
|
+
*
|
|
14071
|
+
* EXPORTED, and not because warm throughput needs it exported. This exact rule
|
|
14072
|
+
* was hand-written three times — here, as `dynamodb-table-provider.ts`'s
|
|
14073
|
+
* `capacityNumber` (byte-identical), and as `dynamodb-globaltable-provider.ts`'s
|
|
14074
|
+
* `toFiniteNumber` (a different spelling of the same total function) — which is
|
|
14075
|
+
* precisely the divergence this module exists to stop: the next "fix" to one of
|
|
14076
|
+
* them would have left the other two answering differently, with nothing in the
|
|
14077
|
+
* tree saying which was intended. It reads `ReadCapacityUnits` /
|
|
14078
|
+
* `MaxReadRequestUnits` / `MinCapacity` as readily as it reads
|
|
14079
|
+
* `ReadUnitsPerSecond`; there is only ever one right answer for "is this
|
|
14080
|
+
* stringly-typed CFn value a number I can send?".
|
|
14081
|
+
*/
|
|
14082
|
+
function toFiniteNumber(value) {
|
|
14083
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
14084
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
14085
|
+
const n = Number(value);
|
|
14086
|
+
return Number.isFinite(n) ? n : void 0;
|
|
14087
|
+
}
|
|
14088
|
+
}
|
|
14089
|
+
/**
|
|
14090
|
+
* Coerce a CFn `WarmThroughput` block to the numeric shape the SDK's `Long`
|
|
14091
|
+
* fields accept, PER MEMBER.
|
|
14092
|
+
*
|
|
14093
|
+
* PER MEMBER, not whole-block, and that distinction is the point: a block
|
|
14094
|
+
* whose write half is an unresolved intrinsic still has a perfectly good read
|
|
14095
|
+
* half, and dropping both would silently discard a value the template really
|
|
14096
|
+
* did ask for. The dropped member is NAMED (`droppedMembers`) so the caller's
|
|
14097
|
+
* warning can say which half went missing instead of reporting the whole
|
|
14098
|
+
* property.
|
|
14099
|
+
*
|
|
14100
|
+
* A block with NO usable member yields `spec: undefined` — refused rather than
|
|
14101
|
+
* forwarded, because forwarding a malformed block surfaces as an opaque AWS
|
|
14102
|
+
* validation error naming neither cdkd nor the property. `droppedMembers` is
|
|
14103
|
+
* still populated in that case, so a REFUSAL message can name what it refused;
|
|
14104
|
+
* a block that is not an object at all (an unresolved `Fn::If`, a scalar, an
|
|
14105
|
+
* array) has no member to name and reports none, which is what lets a caller
|
|
14106
|
+
* word "one half went missing" differently from "the block is unusable".
|
|
14107
|
+
*
|
|
14108
|
+
* Pure: takes the raw bag, returns numbers, logs nothing. Each caller owns its
|
|
14109
|
+
* own message, so the wording stays consistent across that provider's send
|
|
14110
|
+
* sites without forcing one wording across both providers.
|
|
14111
|
+
*/
|
|
14112
|
+
function coerceWarmThroughput$1(raw) {
|
|
14113
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { droppedMembers: [] };
|
|
14114
|
+
const bag = raw;
|
|
14115
|
+
const spec = {};
|
|
14116
|
+
const droppedMembers = [];
|
|
14117
|
+
for (const member of WARM_THROUGHPUT_MEMBERS) {
|
|
14118
|
+
if (bag[member] === void 0) continue;
|
|
14119
|
+
const coerced = toFiniteNumber(bag[member]);
|
|
14120
|
+
if (coerced === void 0) {
|
|
14121
|
+
droppedMembers.push(member);
|
|
14122
|
+
continue;
|
|
14123
|
+
}
|
|
14124
|
+
spec[member] = coerced;
|
|
14125
|
+
}
|
|
14126
|
+
if (Object.keys(spec).length === 0) return { droppedMembers };
|
|
14127
|
+
return {
|
|
14128
|
+
spec,
|
|
14129
|
+
droppedMembers
|
|
14130
|
+
};
|
|
14131
|
+
}
|
|
14132
|
+
/**
|
|
14133
|
+
* Whether the COERCED desired `WarmThroughput` would LOWER what AWS already
|
|
14134
|
+
* reports.
|
|
14135
|
+
*
|
|
14136
|
+
* Measured live (us-east-1, 2026-08-13, issue #1768) against a table AWS
|
|
14137
|
+
* reports `{ReadUnitsPerSecond: 12000, WriteUnitsPerSecond: 4000}` for:
|
|
14138
|
+
*
|
|
14139
|
+
* ```
|
|
14140
|
+
* UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000, WriteUnitsPerSecond: 2000}
|
|
14141
|
+
* ValidationException: One or more parameter values were invalid: Requested
|
|
14142
|
+
* ReadUnitsPerSecond for WarmThroughput for table is lower than current
|
|
14143
|
+
* WarmThroughput, decreasing WarmThroughput is not supported
|
|
14144
|
+
* UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000} -> same rejection
|
|
14145
|
+
* UpdateTable WarmThroughput={WriteUnitsPerSecond: 2000} -> same rejection, naming WriteUnitsPerSecond
|
|
14146
|
+
* UpdateTable WarmThroughput={12000, 4000} (re-assert) -> ACCEPTED
|
|
14147
|
+
* ```
|
|
14148
|
+
*
|
|
14149
|
+
* So the value is one AWS raises with the table's traffic and never lowers,
|
|
14150
|
+
* and a decrease is REJECTED rather than accepted-and-ignored — which is what
|
|
14151
|
+
* had to be measured before choosing, because the two produce different
|
|
14152
|
+
* correct answers. The caller SKIPS the call on a true here.
|
|
14153
|
+
*
|
|
14154
|
+
* Evaluated on the COERCED spec, never on the raw bag: analysing the raw bag
|
|
14155
|
+
* makes the verdict describe a request that is not the one being sent (a
|
|
14156
|
+
* dropped member is not part of the call and must not be part of the
|
|
14157
|
+
* comparison).
|
|
14158
|
+
*
|
|
14159
|
+
* Semantics, all three chosen to FAIL OPEN — i.e. to let the call through and
|
|
14160
|
+
* leave AWS as the authority — because a false positive here silently drops a
|
|
14161
|
+
* legitimate INCREASE, which is a real capacity change the user asked for,
|
|
14162
|
+
* while a false negative merely reproduces the pre-fix behaviour of an
|
|
14163
|
+
* AWS-side rejection that names the property:
|
|
14164
|
+
* - DECLARED members only. An absent member is not a request to lower
|
|
14165
|
+
* anything, so it takes no part in the verdict.
|
|
14166
|
+
* - MIXED is not a decrease. One member below live and the other above means
|
|
14167
|
+
* the call carries a genuine increase; AWS decides.
|
|
14168
|
+
* - An absent or unusable LIVE counterpart is not a decrease. Without a
|
|
14169
|
+
* number to compare against there is no evidence of one. Only the LIVE side
|
|
14170
|
+
* can reach that arm: the desired side is a COERCED spec, so a malformed
|
|
14171
|
+
* template value has already been dropped by {@link coerceWarmThroughput}.
|
|
14172
|
+
*
|
|
14173
|
+
* A decrease therefore requires every declared member to be at-or-below live
|
|
14174
|
+
* AND at least one to be strictly below.
|
|
14175
|
+
*
|
|
14176
|
+
* The skip this drives is right for EVERY `update()` caller — the deploy
|
|
14177
|
+
* engine, `cdkd drift --revert`, and the rollback executor's two revert arms —
|
|
14178
|
+
* because none of them can make AWS lower the value, so none loses anything a
|
|
14179
|
+
* doomed call would have achieved.
|
|
14180
|
+
*/
|
|
14181
|
+
function isWarmThroughputDecrease(desired, live) {
|
|
14182
|
+
if (desired === void 0 || live === void 0) return false;
|
|
14183
|
+
let sawDecrease = false;
|
|
14184
|
+
for (const member of WARM_THROUGHPUT_MEMBERS) {
|
|
14185
|
+
if (desired[member] === void 0) continue;
|
|
14186
|
+
const wanted = toFiniteNumber(desired[member]);
|
|
14187
|
+
const current = toFiniteNumber(live[member]);
|
|
14188
|
+
if (wanted === void 0 || current === void 0) return false;
|
|
14189
|
+
if (wanted > current) return false;
|
|
14190
|
+
if (wanted < current) sawDecrease = true;
|
|
14191
|
+
}
|
|
14192
|
+
return sawDecrease;
|
|
14193
|
+
}
|
|
14194
|
+
|
|
13992
14195
|
//#endregion
|
|
13993
14196
|
//#region src/provisioning/providers/dynamodb-table-provider.ts
|
|
13994
14197
|
/**
|
|
@@ -14091,28 +14294,6 @@ function hasUsableTableCapacity(value) {
|
|
|
14091
14294
|
return true;
|
|
14092
14295
|
}
|
|
14093
14296
|
/**
|
|
14094
|
-
* A capacity member, or `undefined` when the value is not a usable number.
|
|
14095
|
-
*
|
|
14096
|
-
* `Number()` coercion is NOT good enough here and the first draft of this
|
|
14097
|
-
* helper shipped that bug: `Number(null)`, `Number('')`, `Number([])` and
|
|
14098
|
-
* `Number(false)` are all **0**, not `NaN` — so a live `0` would have compared
|
|
14099
|
-
* EQUAL to a desired `null` / `''` / `[]` / `false` and suppressed the call.
|
|
14100
|
-
* The blast radius on DynamoDB is small (a PROVISIONED index's capacity is
|
|
14101
|
-
* always >= 1), but the rule this encodes is copied to other providers, so it
|
|
14102
|
-
* is spelled correctly rather than relying on the type that happens to be
|
|
14103
|
-
* unreachable today.
|
|
14104
|
-
*
|
|
14105
|
-
* A YAML-borne numeric STRING is still accepted, because that is a real
|
|
14106
|
-
* template shape and `'5'` genuinely means 5.
|
|
14107
|
-
*/
|
|
14108
|
-
function capacityNumber(value) {
|
|
14109
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
14110
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
14111
|
-
const n = Number(value);
|
|
14112
|
-
return Number.isFinite(n) ? n : void 0;
|
|
14113
|
-
}
|
|
14114
|
-
}
|
|
14115
|
-
/**
|
|
14116
14297
|
* Does the capacity AWS currently holds for a GSI already equal the pair the
|
|
14117
14298
|
* template requests? (issue #1630)
|
|
14118
14299
|
*
|
|
@@ -14131,10 +14312,10 @@ function capacityNumber(value) {
|
|
|
14131
14312
|
*/
|
|
14132
14313
|
function liveCapacityAlreadyMatches(live, requested) {
|
|
14133
14314
|
if (live === void 0) return false;
|
|
14134
|
-
const liveRead =
|
|
14135
|
-
const liveWrite =
|
|
14136
|
-
const wantRead =
|
|
14137
|
-
const wantWrite =
|
|
14315
|
+
const liveRead = toFiniteNumber(live.ReadCapacityUnits);
|
|
14316
|
+
const liveWrite = toFiniteNumber(live.WriteCapacityUnits);
|
|
14317
|
+
const wantRead = toFiniteNumber(requested.ReadCapacityUnits);
|
|
14318
|
+
const wantWrite = toFiniteNumber(requested.WriteCapacityUnits);
|
|
14138
14319
|
if (liveRead === void 0 || liveWrite === void 0) return false;
|
|
14139
14320
|
if (wantRead === void 0 || wantWrite === void 0) return false;
|
|
14140
14321
|
return liveRead === wantRead && liveWrite === wantWrite;
|
|
@@ -14182,8 +14363,10 @@ const BILLING_FLIP_ACTIVE_WAIT_ATTEMPTS = 600;
|
|
|
14182
14363
|
* Still not `!== undefined`, and still ONE spelling for the write sites and the
|
|
14183
14364
|
* drift side both: a value this refuses is a value cdkd does not send, so the
|
|
14184
14365
|
* readback must not emit AWS's computed value as though the template had asked
|
|
14185
|
-
* for it. A YAML-borne numeric STRING is accepted,
|
|
14186
|
-
*
|
|
14366
|
+
* for it. A YAML-borne numeric STRING is accepted, by the shared coercion's own
|
|
14367
|
+
* number reader ({@link toFiniteNumber}, in `../dynamodb-warm-throughput.ts` —
|
|
14368
|
+
* the one this file's capacity paths read too), because `'12000'` genuinely
|
|
14369
|
+
* means 12000.
|
|
14187
14370
|
*/
|
|
14188
14371
|
function isSendableWarmThroughput(value) {
|
|
14189
14372
|
return coerceWarmThroughput(value) !== void 0;
|
|
@@ -14204,8 +14387,9 @@ function isSendableWarmThroughput(value) {
|
|
|
14204
14387
|
* over), and it makes the quoted form WORK instead of merely warning.
|
|
14205
14388
|
*
|
|
14206
14389
|
* Per MEMBER, so a bad one cannot take a good one with it, and never `NaN`:
|
|
14207
|
-
* {@link
|
|
14208
|
-
* everything else
|
|
14390
|
+
* the shared coercion's number reader is {@link toFiniteNumber} — a number or
|
|
14391
|
+
* a numeric string, everything else rejected — so a member that
|
|
14392
|
+
* does not resolve is OMITTED from the spec
|
|
14209
14393
|
* and reported in `dropped` for the caller to announce. AWS accepts a
|
|
14210
14394
|
* one-member `WarmThroughput` (measured us-east-1, 2026-08-13: an
|
|
14211
14395
|
* `UpdateTable` carrying only `ReadUnitsPerSecond` is a valid request shape —
|
|
@@ -14244,27 +14428,22 @@ function isSendableWarmThroughput(value) {
|
|
|
14244
14428
|
* the comparator's `deepEqual` is strict about that. It is the generic
|
|
14245
14429
|
* stringly-typed-CFn class (every numeric property has it), not something this
|
|
14246
14430
|
* coercion can reach from the write side.
|
|
14431
|
+
*
|
|
14432
|
+
* The RULE moved to `../dynamodb-warm-throughput.ts` when
|
|
14433
|
+
* `AWS::DynamoDB::GlobalTable` needed the same one (issue #1857); this is the
|
|
14434
|
+
* local adapter onto the shape this file's four call sites already read, so
|
|
14435
|
+
* the behaviour, the signature and the tests are unchanged. The shared
|
|
14436
|
+
* coercion always reports `droppedMembers`, including on a total refusal —
|
|
14437
|
+
* that arm is unreachable from here, since a `usable === 0` outcome maps to
|
|
14438
|
+
* `undefined` and the names go with it, which is exactly what this file did
|
|
14439
|
+
* before.
|
|
14247
14440
|
*/
|
|
14248
14441
|
function coerceWarmThroughput(value) {
|
|
14249
|
-
|
|
14250
|
-
|
|
14251
|
-
const spec = {};
|
|
14252
|
-
const dropped = [];
|
|
14253
|
-
let usable = 0;
|
|
14254
|
-
for (const member of ["ReadUnitsPerSecond", "WriteUnitsPerSecond"]) {
|
|
14255
|
-
if (bag[member] === void 0) continue;
|
|
14256
|
-
const n = capacityNumber(bag[member]);
|
|
14257
|
-
if (n === void 0) {
|
|
14258
|
-
dropped.push(member);
|
|
14259
|
-
continue;
|
|
14260
|
-
}
|
|
14261
|
-
spec[member] = n;
|
|
14262
|
-
usable++;
|
|
14263
|
-
}
|
|
14264
|
-
if (usable === 0) return void 0;
|
|
14442
|
+
const { spec, droppedMembers } = coerceWarmThroughput$1(value);
|
|
14443
|
+
if (spec === void 0) return void 0;
|
|
14265
14444
|
return {
|
|
14266
14445
|
spec,
|
|
14267
|
-
dropped
|
|
14446
|
+
dropped: [...droppedMembers]
|
|
14268
14447
|
};
|
|
14269
14448
|
}
|
|
14270
14449
|
/**
|
|
@@ -14337,60 +14516,6 @@ function declaresWarmThroughput(properties) {
|
|
|
14337
14516
|
return properties !== void 0 && isSendableWarmThroughput(properties["WarmThroughput"]);
|
|
14338
14517
|
}
|
|
14339
14518
|
/**
|
|
14340
|
-
* Is the WarmThroughput this update would SEND a DECREASE of what AWS already
|
|
14341
|
-
* holds (issue #1768)?
|
|
14342
|
-
*
|
|
14343
|
-
* Measured live (us-east-1, 2026-08-13) against a table AWS reports
|
|
14344
|
-
* `{ReadUnitsPerSecond: 12000, WriteUnitsPerSecond: 4000}` for:
|
|
14345
|
-
*
|
|
14346
|
-
* ```
|
|
14347
|
-
* UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000, WriteUnitsPerSecond: 2000}
|
|
14348
|
-
* ValidationException: One or more parameter values were invalid: Requested
|
|
14349
|
-
* ReadUnitsPerSecond for WarmThroughput for table is lower than current
|
|
14350
|
-
* WarmThroughput, decreasing WarmThroughput is not supported
|
|
14351
|
-
* UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000} -> same rejection
|
|
14352
|
-
* UpdateTable WarmThroughput={WriteUnitsPerSecond: 2000} -> same rejection, naming WriteUnitsPerSecond
|
|
14353
|
-
* UpdateTable WarmThroughput={12000, 4000} (re-assert) -> ACCEPTED
|
|
14354
|
-
* ```
|
|
14355
|
-
*
|
|
14356
|
-
* So the value is one AWS raises with the table's traffic and never lowers,
|
|
14357
|
-
* and a decrease is REJECTED rather than accepted-and-ignored — which is what
|
|
14358
|
-
* the issue said to measure before choosing, because the two produce different
|
|
14359
|
-
* correct answers. The caller SKIPS the call on a true here.
|
|
14360
|
-
*
|
|
14361
|
-
* Rules, each one chosen so the skip is right for EVERY `update()` caller
|
|
14362
|
-
* (the deploy engine, `cdkd drift --revert`, and the rollback executor's two
|
|
14363
|
-
* revert arms — none of which can make AWS lower the value, so none of them
|
|
14364
|
-
* loses anything a doomed call would have achieved):
|
|
14365
|
-
*
|
|
14366
|
-
* - Only the members the desired side DECLARES are examined; an omitted member
|
|
14367
|
-
* is not sent and cannot be a decrease.
|
|
14368
|
-
* - EVERY declared member must be at-or-below its live counterpart AND at
|
|
14369
|
-
* least one strictly below. A MIXED request (one member up, one down) still
|
|
14370
|
-
* goes out: AWS rejects it by name, which is the pre-fix behavior and better
|
|
14371
|
-
* than silently dropping the increase the user asked for.
|
|
14372
|
-
* - Anything not resolvable to a finite number fails OPEN (the call is still
|
|
14373
|
-
* issued): the worst case is the pre-fix behavior, whereas a false positive
|
|
14374
|
-
* would silently drop a real increase. Only the LIVE side can actually reach
|
|
14375
|
-
* that arm now (PR review round 8): the desired side is a COERCED spec, so a
|
|
14376
|
-
* malformed template value or an unresolved intrinsic has already been
|
|
14377
|
-
* dropped by {@link coerceWarmThroughput} and cannot arrive here. The check
|
|
14378
|
-
* is kept for the live side, which is whatever `DescribeTable` returned.
|
|
14379
|
-
*/
|
|
14380
|
-
function isWarmThroughputDecrease(desired, live) {
|
|
14381
|
-
if (live === void 0) return false;
|
|
14382
|
-
let sawDecrease = false;
|
|
14383
|
-
for (const member of ["ReadUnitsPerSecond", "WriteUnitsPerSecond"]) {
|
|
14384
|
-
if (desired[member] === void 0) continue;
|
|
14385
|
-
const wanted = capacityNumber(desired[member]);
|
|
14386
|
-
const current = capacityNumber(live[member]);
|
|
14387
|
-
if (wanted === void 0 || current === void 0) return false;
|
|
14388
|
-
if (wanted > current) return false;
|
|
14389
|
-
if (wanted < current) sawDecrease = true;
|
|
14390
|
-
}
|
|
14391
|
-
return sawDecrease;
|
|
14392
|
-
}
|
|
14393
|
-
/**
|
|
14394
14519
|
* Does the warm throughput AWS currently holds already equal what this update
|
|
14395
14520
|
* would request (issue #1768 PR review)?
|
|
14396
14521
|
*
|
|
@@ -14419,10 +14544,10 @@ function isWarmThroughputDecrease(desired, live) {
|
|
|
14419
14544
|
function warmThroughputAlreadyMatches(desired, live) {
|
|
14420
14545
|
if (live === void 0) return false;
|
|
14421
14546
|
let compared = 0;
|
|
14422
|
-
for (const member of
|
|
14547
|
+
for (const member of WARM_THROUGHPUT_MEMBERS) {
|
|
14423
14548
|
if (desired[member] === void 0) continue;
|
|
14424
|
-
const wanted =
|
|
14425
|
-
const current =
|
|
14549
|
+
const wanted = toFiniteNumber(desired[member]);
|
|
14550
|
+
const current = toFiniteNumber(live[member]);
|
|
14426
14551
|
if (wanted === void 0 || current === void 0) return false;
|
|
14427
14552
|
if (wanted !== current) return false;
|
|
14428
14553
|
compared++;
|
|
@@ -15529,7 +15654,7 @@ var DynamoDBTableProvider = class {
|
|
|
15529
15654
|
*/
|
|
15530
15655
|
skipZeroCapacityIndexUpdate(indexName, physicalId, requested) {
|
|
15531
15656
|
if (requested === void 0) return false;
|
|
15532
|
-
if (
|
|
15657
|
+
if (toFiniteNumber(requested.ReadCapacityUnits) !== 0 || toFiniteNumber(requested.WriteCapacityUnits) !== 0) return false;
|
|
15533
15658
|
this.logger.warn(`GSI ${indexName} on DynamoDB table ${physicalId}: the requested ProvisionedThroughput is {ReadCapacityUnits: 0, WriteCapacityUnits: 0}, which is AWS's on-demand placeholder rather than a capacity DynamoDB accepts (the minimum is 1), so no per-index throughput update was sent. This usually means the value came from a pre-#1767 cdkd state record — a \`cdkd drift --revert\` of such a record, or a rollback replaying it. Re-run \`cdkd deploy\` (or \`cdkd drift --accept\`) to refresh the record.`);
|
|
15534
15659
|
return true;
|
|
15535
15660
|
}
|
|
@@ -16425,6 +16550,35 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
16425
16550
|
}
|
|
16426
16551
|
}
|
|
16427
16552
|
/**
|
|
16553
|
+
* The `WarmThroughput` block to put on ONE `GlobalSecondaryIndexUpdate`, or
|
|
16554
|
+
* `undefined` when it must not be sent (issue #1857).
|
|
16555
|
+
*
|
|
16556
|
+
* ONE helper for all three `UpdateTable` send sites — the billing-flip
|
|
16557
|
+
* ride-along, the `added` loop's `Create`, and the `modified` loop's
|
|
16558
|
+
* `Update` — because three copies of a decrease guard is three chances to
|
|
16559
|
+
* word one differently, and the whole point of {@link isWarmThroughputDecrease}
|
|
16560
|
+
* is that the answer does not depend on which site asked.
|
|
16561
|
+
*
|
|
16562
|
+
* The `desired` argument is already COERCED (`toSdkGlobalSecondaryIndexes`
|
|
16563
|
+
* did it), so an unusable block never reaches here at all: it was refused
|
|
16564
|
+
* and reported during translation, and arrives as `undefined`. What is left
|
|
16565
|
+
* for this helper is the one question translation cannot answer, because it
|
|
16566
|
+
* needs live AWS state: would this call LOWER what AWS reports?
|
|
16567
|
+
*
|
|
16568
|
+
* The refusal is a WARN and a dropped member, not a throw. A decrease is a
|
|
16569
|
+
* template that has fallen behind a value AWS grew on its own; failing the
|
|
16570
|
+
* deploy over it would block every unrelated change to the same table, and
|
|
16571
|
+
* there is no template edit that "fixes" it other than raising a number the
|
|
16572
|
+
* user never chose.
|
|
16573
|
+
*/
|
|
16574
|
+
warmThroughputForSend(indexName, desired, liveWarmByIndexName, logicalId, physicalId) {
|
|
16575
|
+
if (!desired) return void 0;
|
|
16576
|
+
const live = liveWarmByIndexName.get(indexName);
|
|
16577
|
+
if (!isWarmThroughputDecrease(desired, live)) return desired;
|
|
16578
|
+
const format = (spec) => WARM_THROUGHPUT_MEMBERS.filter((m) => spec?.[m] !== void 0).map((m) => `${m}=${spec?.[m]}`).join(", ");
|
|
16579
|
+
this.logger.warn(`DynamoDB GlobalTable ${logicalId}: GSI '${indexName}' on ${physicalId}: WarmThroughput was NOT sent — the template asks for ${format(desired)} while AWS reports ${format(live)}, and DynamoDB rejects a decrease ("decreasing WarmThroughput is not supported"). Warm throughput only rises with a table's traffic, so a template below the live value can never be applied; raise it to at least the live value, or remove it. Every other change to this index is applied normally.`);
|
|
16580
|
+
}
|
|
16581
|
+
/**
|
|
16428
16582
|
* Add a single replica region. Issues one `UpdateTableCommand` with
|
|
16429
16583
|
* `ReplicaUpdates: [{ Create: { RegionName, ... } }]` and polls
|
|
16430
16584
|
* `DescribeTable` until the replica's `ReplicaStatus` flips to ACTIVE.
|
|
@@ -16510,6 +16664,16 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
16510
16664
|
return (props["Replicas"] ?? []).find((r) => r["Region"] === currentRegion)?.["Tags"];
|
|
16511
16665
|
};
|
|
16512
16666
|
if (tableArn) await this.applyTagDiff(tableArn, extractLocalTags(previousProperties), extractLocalTags(properties));
|
|
16667
|
+
const liveWarmByIndexName = /* @__PURE__ */ new Map();
|
|
16668
|
+
for (const liveGsi of describeResp.Table?.GlobalSecondaryIndexes ?? []) {
|
|
16669
|
+
const name = liveGsi.IndexName;
|
|
16670
|
+
const warm = liveGsi.WarmThroughput;
|
|
16671
|
+
if (typeof name !== "string" || !warm) continue;
|
|
16672
|
+
const spec = {};
|
|
16673
|
+
if (warm.ReadUnitsPerSecond !== void 0) spec.ReadUnitsPerSecond = warm.ReadUnitsPerSecond;
|
|
16674
|
+
if (warm.WriteUnitsPerSecond !== void 0) spec.WriteUnitsPerSecond = warm.WriteUnitsPerSecond;
|
|
16675
|
+
if (Object.keys(spec).length > 0) liveWarmByIndexName.set(name, spec);
|
|
16676
|
+
}
|
|
16513
16677
|
const recordedOldBilling = previousProperties["BillingMode"];
|
|
16514
16678
|
const recordedOldBillingAbsent = recordedOldBilling === void 0;
|
|
16515
16679
|
const recordedOldBillingUsable = typeof recordedOldBilling === "string" && recordedOldBilling.trim() !== "";
|
|
@@ -16659,11 +16823,11 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
16659
16823
|
const gsi = byIndexName.get(indexName);
|
|
16660
16824
|
if (!gsi?.ProvisionedThroughput) continue;
|
|
16661
16825
|
const previousWarm = previousByIndexName.get(indexName)?.WarmThroughput;
|
|
16662
|
-
const
|
|
16826
|
+
const warmToSend = gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousWarm) ? this.warmThroughputForSend(indexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId) : void 0;
|
|
16663
16827
|
indexUpdates.push({ Update: {
|
|
16664
16828
|
IndexName: indexName,
|
|
16665
16829
|
ProvisionedThroughput: gsi.ProvisionedThroughput,
|
|
16666
|
-
...
|
|
16830
|
+
...warmToSend && { WarmThroughput: warmToSend }
|
|
16667
16831
|
} });
|
|
16668
16832
|
if (survivingIndexNames.has(indexName)) gsiHandledByBillingFlip.add(indexName);
|
|
16669
16833
|
}
|
|
@@ -16798,13 +16962,14 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
16798
16962
|
}
|
|
16799
16963
|
for (const gsi of gsiDiff.added) {
|
|
16800
16964
|
if (!gsi.IndexName || !gsi.KeySchema || !gsi.Projection) continue;
|
|
16965
|
+
const addedWarmToSend = this.warmThroughputForSend(gsi.IndexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId);
|
|
16801
16966
|
const gsiUpdate = { Create: {
|
|
16802
16967
|
IndexName: gsi.IndexName,
|
|
16803
16968
|
KeySchema: gsi.KeySchema,
|
|
16804
16969
|
Projection: gsi.Projection,
|
|
16805
16970
|
...gsi.ProvisionedThroughput && { ProvisionedThroughput: gsi.ProvisionedThroughput },
|
|
16806
16971
|
...gsi.OnDemandThroughput && { OnDemandThroughput: gsi.OnDemandThroughput },
|
|
16807
|
-
...
|
|
16972
|
+
...addedWarmToSend && { WarmThroughput: addedWarmToSend }
|
|
16808
16973
|
} };
|
|
16809
16974
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
16810
16975
|
TableName: physicalId,
|
|
@@ -16831,9 +16996,14 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
16831
16996
|
if (Object.keys(onDemand).length > 0) update.OnDemandThroughput = onDemand;
|
|
16832
16997
|
if (!billingFlipped && gsi.ProvisionedThroughput) update.ProvisionedThroughput = gsi.ProvisionedThroughput;
|
|
16833
16998
|
const previousSdk = previousSdkByName.get(gsi.IndexName);
|
|
16834
|
-
|
|
16999
|
+
let warmDecreaseRefused = false;
|
|
17000
|
+
if (gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousSdk?.WarmThroughput)) {
|
|
17001
|
+
const warmToSend = this.warmThroughputForSend(gsi.IndexName, gsi.WarmThroughput, liveWarmByIndexName, logicalId, physicalId);
|
|
17002
|
+
if (warmToSend) update.WarmThroughput = warmToSend;
|
|
17003
|
+
else warmDecreaseRefused = true;
|
|
17004
|
+
}
|
|
16835
17005
|
if (!update.ProvisionedThroughput && !update.OnDemandThroughput && !update.WarmThroughput) {
|
|
16836
|
-
if (!billingFlipped && unresolvedMembers.length === 0) this.logger.warn(`GSI '${gsi.IndexName}' on ${physicalId} changed in a way DynamoDB's UpdateTable cannot express (KeySchema / Projection are immutable on an existing index). Recreate the index under a new name to apply the change.`);
|
|
17006
|
+
if (!billingFlipped && unresolvedMembers.length === 0 && !warmDecreaseRefused) this.logger.warn(`GSI '${gsi.IndexName}' on ${physicalId} changed in a way DynamoDB's UpdateTable cannot express (KeySchema / Projection are immutable on an existing index). Recreate the index under a new name to apply the change.`);
|
|
16837
17007
|
continue;
|
|
16838
17008
|
}
|
|
16839
17009
|
const gsiUpdate = { Update: update };
|
|
@@ -17310,9 +17480,27 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
17310
17480
|
try {
|
|
17311
17481
|
if (hasTransitionalIndex(preDeleteIndexes)) {
|
|
17312
17482
|
this.logger.debug(`Waiting for indexes on ${physicalId} to settle before DeleteTable (issue #1521)`);
|
|
17313
|
-
await this.waitForIndexesActive(physicalId, logicalId);
|
|
17483
|
+
await this.waitForIndexesActive(physicalId, logicalId, { proceedNote: DELETE_INDEX_WAIT_PROCEED_NOTE });
|
|
17314
17484
|
}
|
|
17315
|
-
|
|
17485
|
+
let deleteAttempts = 0;
|
|
17486
|
+
await withRetry(async () => {
|
|
17487
|
+
if (deleteAttempts++ > 0) {
|
|
17488
|
+
if (deleteAttempts === 2) {
|
|
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),
|
|
17501
|
+
logger: this.logger,
|
|
17502
|
+
sleep: (ms) => deleteTableRetryDelays.sleep ? deleteTableRetryDelays.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
|
|
17503
|
+
});
|
|
17316
17504
|
await this.waitForTableGone(physicalId, logicalId);
|
|
17317
17505
|
this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
|
|
17318
17506
|
} catch (error) {
|
|
@@ -17928,22 +18116,49 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
17928
18116
|
* throwing. A missed registration self-heals on the next deploy (the
|
|
17929
18117
|
* reconcile re-asserts anything not already present), whereas throwing
|
|
17930
18118
|
* here would fail a deploy whose actual resources are all correct. Index
|
|
17931
|
-
* backfill on a large table can take a long time, hence the 15-minute
|
|
17932
|
-
* The caller that follows it with a mutating call keeps AWS's
|
|
17933
|
-
* the backstop for the case where even that cap is not enough.
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
18119
|
+
* backfill on a large table can take a long time, hence the 15-minute
|
|
18120
|
+
* DEFAULT cap. The caller that follows it with a mutating call keeps AWS's
|
|
18121
|
+
* own error as the backstop for the case where even that cap is not enough.
|
|
18122
|
+
*
|
|
18123
|
+
* `opts.maxAttempts` exists because the cap is only right for a wait that
|
|
18124
|
+
* runs ONCE: the #1830 delete retry re-runs this per attempt, so the caller's
|
|
18125
|
+
* wall clock is the product and the default would blow past the per-resource
|
|
18126
|
+
* deadline `destroy-runner.ts` enforces.
|
|
18127
|
+
*
|
|
18128
|
+
* `opts.proceedNote` covers the OTHER thing that is caller-specific: what it
|
|
18129
|
+
* costs when the wait ends WITHOUT confirming. Both arms that end that way —
|
|
18130
|
+
* the timeout, and a `DescribeTable` that failed for a reason other than
|
|
18131
|
+
* throttling or a gone table — stop waiting and let the caller proceed, so
|
|
18132
|
+
* one note serves both. The consequences genuinely differ: on the delete
|
|
18133
|
+
* path AWS refuses the `DeleteTable` and that refusal is the backstop, while
|
|
18134
|
+
* the auto-scaling caller has NO backstop — `reconcileAutoScalingTargets`
|
|
18135
|
+
* simply skips an index that is not ready and nothing downstream refuses.
|
|
18136
|
+
* The default sentence is the auto-scaling one; every delete-path caller
|
|
18137
|
+
* passes {@link DELETE_INDEX_WAIT_PROCEED_NOTE}.
|
|
18138
|
+
*/
|
|
18139
|
+
async waitForIndexesActive(tableName, logicalId, opts) {
|
|
18140
|
+
const maxAttempts = opts?.maxAttempts ?? 900;
|
|
18141
|
+
const proceedNote = opts?.proceedNote ?? "auto-scaling registration for a still-building index may have been skipped. The next deploy re-asserts it.";
|
|
17937
18142
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
17938
18143
|
try {
|
|
17939
18144
|
if (!hasTransitionalIndex((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? [])) return;
|
|
17940
18145
|
} catch (err) {
|
|
17941
|
-
|
|
18146
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
18147
|
+
if (isThrottlingError(err)) {
|
|
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}`);
|
|
17942
18157
|
return;
|
|
17943
18158
|
}
|
|
17944
18159
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
17945
18160
|
}
|
|
17946
|
-
this.logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts}
|
|
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}`);
|
|
17947
18162
|
}
|
|
17948
18163
|
/**
|
|
17949
18164
|
* Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
|
|
@@ -18082,6 +18297,14 @@ function autoScalingResourceId(tableName, indexName) {
|
|
|
18082
18297
|
*/
|
|
18083
18298
|
const autoScalingRetryDelays = {};
|
|
18084
18299
|
/**
|
|
18300
|
+
* Test seam for the index-busy `DeleteTable` retry's backoff (issue #1830),
|
|
18301
|
+
* mirroring {@link autoScalingRetryDelays}. Production leaves `sleep`
|
|
18302
|
+
* undefined so `withRetry`'s real schedule applies; a test injects a no-op so
|
|
18303
|
+
* the ~47s budget does not have to be waited out. Kept SEPARATE from the
|
|
18304
|
+
* auto-scaling seam so a test can slow one path without silencing the other.
|
|
18305
|
+
*/
|
|
18306
|
+
const deleteTableRetryDelays = {};
|
|
18307
|
+
/**
|
|
18085
18308
|
* Resource ids per `DescribeScalableTargets` request. Kept at the API's
|
|
18086
18309
|
* documented per-page ceiling rather than a larger guess — the request is
|
|
18087
18310
|
* paginated anyway, and over-asking risks a `ValidationException` that would
|
|
@@ -18235,12 +18458,113 @@ const INDEX_TRANSITIONAL_STATUSES = /* @__PURE__ */ new Set([
|
|
|
18235
18458
|
function hasTransitionalIndex(indexes) {
|
|
18236
18459
|
return (indexes ?? []).some((gsi) => INDEX_TRANSITIONAL_STATUSES.has(gsi.IndexStatus ?? ""));
|
|
18237
18460
|
}
|
|
18238
|
-
/**
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
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
|
+
* Build the {@link ThroughputDiagnostic} for a `WarmThroughput` block that did
|
|
18537
|
+
* not survive coercion intact, or `undefined` when it did.
|
|
18538
|
+
*
|
|
18539
|
+
* Two distinct outcomes, worded differently because the remedy differs:
|
|
18540
|
+
* - PARTIAL — one member dropped, the other still sent. The call goes ahead;
|
|
18541
|
+
* the user needs to know which half silently did not.
|
|
18542
|
+
* - REFUSED — nothing sendable. No call is made for the property at all,
|
|
18543
|
+
* which is the point (issue #1857 defect 3): forwarding the malformed block
|
|
18544
|
+
* would surface as an AWS validation error that names neither cdkd nor the
|
|
18545
|
+
* property.
|
|
18546
|
+
*/
|
|
18547
|
+
function warmThroughputDiagnostic(raw, coercion, indexName) {
|
|
18548
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
18549
|
+
const scope = {
|
|
18550
|
+
kind: "unresolved-member",
|
|
18551
|
+
member: "WarmThroughput"
|
|
18552
|
+
};
|
|
18553
|
+
const dropped = coercion.droppedMembers.join(" / ");
|
|
18554
|
+
if (coercion.spec) {
|
|
18555
|
+
if (coercion.droppedMembers.length === 0) return void 0;
|
|
18556
|
+
const kept = WARM_THROUGHPUT_MEMBERS.filter((m) => coercion.spec?.[m] !== void 0).join(" / ");
|
|
18557
|
+
return {
|
|
18558
|
+
...scope,
|
|
18559
|
+
...indexName !== void 0 && { indexName },
|
|
18560
|
+
message: `WarmThroughput.${dropped} did not resolve to a number and was DROPPED; ${kept} is still sent. AWS types these as numbers, so a quoted "12000" is fine but an unresolved intrinsic or an object is not. The dropped member keeps its current AWS value until the template supplies a usable one.`
|
|
18561
|
+
};
|
|
18562
|
+
}
|
|
18563
|
+
return {
|
|
18564
|
+
...scope,
|
|
18565
|
+
...indexName !== void 0 && { indexName },
|
|
18566
|
+
message: `WarmThroughput is declared but no member resolved to a number${dropped ? ` (${dropped})` : ""}, so the whole block is REFUSED and not sent. Forwarding it would fail as an opaque AWS validation error naming neither cdkd nor the property. Set WarmThroughput.ReadUnitsPerSecond / WriteUnitsPerSecond to numbers, or remove the block. Nothing else about the index is affected.`
|
|
18567
|
+
};
|
|
18244
18568
|
}
|
|
18245
18569
|
/**
|
|
18246
18570
|
* Merge an explicitly-supplied, already-SDK-shaped throughput block over a
|
|
@@ -18535,7 +18859,10 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
|
|
|
18535
18859
|
KeySchema: gsi["KeySchema"],
|
|
18536
18860
|
Projection: gsi["Projection"]
|
|
18537
18861
|
};
|
|
18538
|
-
|
|
18862
|
+
const warmCoercion = coerceWarmThroughput$1(gsi["WarmThroughput"]);
|
|
18863
|
+
if (warmCoercion.spec) sdk.WarmThroughput = warmCoercion.spec;
|
|
18864
|
+
const warmDiagnostic = warmThroughputDiagnostic(gsi["WarmThroughput"], warmCoercion, typeof indexName === "string" ? indexName : void 0);
|
|
18865
|
+
if (warmDiagnostic) diagnostics?.push(warmDiagnostic);
|
|
18539
18866
|
const explicitProvisioned = asRecord$1(gsi["ProvisionedThroughput"]);
|
|
18540
18867
|
const explicitOnDemand = asRecord$1(gsi["OnDemandThroughput"]);
|
|
18541
18868
|
if (explicitProvisioned && billingMode !== "PROVISIONED") diagnostics?.push({
|
|
@@ -79780,7 +80107,7 @@ function createMigrateCommand() {
|
|
|
79780
80107
|
*/
|
|
79781
80108
|
function buildProgram() {
|
|
79782
80109
|
const program = new Command();
|
|
79783
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.
|
|
80110
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.28");
|
|
79784
80111
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
79785
80112
|
const { profile } = actionCommand.optsWithGlobals();
|
|
79786
80113
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|