@go-to-k/cdkd 0.283.8 → 0.283.10
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-Tu4sDbhu.js → asg-provider-CrBdHIgB.js} +2 -2
- package/dist/{asg-provider-Tu4sDbhu.js.map → asg-provider-CrBdHIgB.js.map} +1 -1
- package/dist/cli.js +913 -40
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-D95wmKN8.js → deploy-engine-Bvr31OQ0.js} +539 -516
- package/dist/deploy-engine-Bvr31OQ0.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-D95wmKN8.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as coerceCfnBoolean, $t as
|
|
2
|
+
import { $ as coerceCfnBoolean, $t as resolveStateBucketWithDefaultAndSource, A as cyan, An as NestedStackChildDirectDestroyError, At as BOOTSTRAP_MARKER_PREFIX, B as findSilentDropProperties, Bt as runDockerForeground, C as makeCanonicalizePropertiesFn, Ct as stringifyValue, D as renderStatefulReason, Dn as LocalStartServiceError, Dt as loadPublishableAssetManifest, E as isStatefulRecreateTargetSync, En as LocalMigrateError, Et as createAssetRedirectResolver, F as IAMRoleProvider, Fn as StackHasActiveImportsError, Ft as validateContainerRepoName, G as IntrinsicFunctionResolver, Gn as isThrottlingError, Gt as synthesisStatusMessage, H as slowCcOperationTimeoutMs, Hn as withErrorHandling, Ht as AssetManifestLoader, I as collectInlinePolicyNamesManagedBySiblings, In as StackTerminationProtectionError, It as buildDenyExternalAccessPolicy, J as refStateLookupFromResource, Jt as resolveApp, K as cfnRefValueFromPhysicalId, Kn as markNonRetryable, Kt as getDefaultStateBucketName, L as clearOnUpdateRemoval, Lt as buildDockerImage, M as green, Mn as ProvisioningError, Mt as getBootstrapMarkerKey, N as red, Nn as ResourceTimeoutError, Nt as parseBootstrapMarker, O as formatResourceLine, Ot as rewriteTemplateAssetReferences, P as yellow, Pn as ResourceUpdateNotSupportedError, Pt as validateAssetBucketName, Q as assertRegionMatch, Qt as resolveStateBucketWithDefault, R as ProviderRegistry, Rt as formatDockerLoginError, S as unsupportedFinalSnapshotError, St as AssetPublisher, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as LocalInvokeBuildError$1, Tt as buildAssetRedirectMap, U as disableInstanceApiTermination, Un as isMarkedNonRetryable, Ut as getDockerImageBySourceHash, V as CloudControlProvider, Vn as normalizeAwsError, Vt as runDockerStreaming, W as isTerminationProtectionPropagationError, Wn as isRetryableTransientError, Wt as Synthesizer, X as normalizeAwsTagsToCfn, Xt as resolveCaptureObservedState, Y as WAFv2WebACLProvider, Yt as resolveAutoAssetStorage, Z as resolveExplicitPhysicalId, Zt as resolveSkipPrefix, _ as buildFinalSnapshotIdentifier, _n as getAwsClients, _t as TemplateParser, a as DeploymentEventsStore, an as MIGRATE_TMP_PREFIX, at as requireConfigObject, b as isFinalSnapshotError, bt as rebuildClientForBucketRegion, c as replayFailedOperations, cn as expectedOwnerParam, ct as s3BucketDomainName, d as deleteSkipReason, dn as derivePartitionAndUrlSuffix, dt as s3BucketWebsiteUrl, en as resolveUseCdkBootstrapAssets, et as configBooleanRefusal, f as withResourceDeadline, fn as AssemblyReader, ft as applyRoleArnIfSet, g as PRE_DELETE_SNAPSHOT_TYPES, gn as AwsClients, gt as DagBuilder, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveBucketRegion, ht as withRetry, i as DeploymentEventsReader, in as CFN_TEMPLATE_URL_LIMIT, it as requireConfigArray, j as gray, jn as PartialFailureError, jt as ensureAssetStorage, k as bold, kn as MissingCdkCliError, kt as AssetModeResolver, l as replayRollback, ln as PARTITION_TABLE, lt as s3BucketDualStackDomainName, m as computeImplicitDeleteEdges, mt as describeTypeWithThrottleRetry, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as warnDeprecatedNoPrefixCliFlag, nt as readConfigString, o as planFailedOps, on as findLargeInlineResources, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as processStackMessages, pt as DiffCalculator, q as getAccountInfo, qt as getLegacyStateBucketName, r as DeployEngine, rn as CFN_TEMPLATE_BODY_LIMIT, rt as replayWarn, s as planRollback, sn as uploadCfnTemplate, st as s3BucketArn, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as stateBucketExistenceConfirmed, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as canonicalizeRegion, ut as s3BucketRegionalDomainName, v as ccRoutedFinalSnapshotError, vn as resetAwsClients, vt as LockManager, w as extractDeploymentEventError, wn as DeployCancelledError, wt as WorkGraph, x as refusesFinalSnapshot, xn as CdkdError, xt as shouldRetainResource, y as createPreDeleteFinalSnapshot, yn as setAwsClients, yt as S3StateBackend, z as findActionableSilentDrops, zt as getDockerCmd } from "./deploy-engine-Bvr31OQ0.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-CrBdHIgB.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";
|
|
@@ -14107,10 +14107,149 @@ const BILLING_FLIP_ACTIVE_WAIT_ATTEMPTS = 600;
|
|
|
14107
14107
|
* though the template had asked, and `--revert` can never clear it because the
|
|
14108
14108
|
* write gate skips the falsy value. Keep this the ONLY spelling of the rule.
|
|
14109
14109
|
*
|
|
14110
|
-
*
|
|
14110
|
+
* It USED to be bare truthiness, "because that is what the wire has always
|
|
14111
|
+
* done". PR review of issue #1768 showed that is not a rule, it is the absence
|
|
14112
|
+
* of one: `WarmThroughput: {}` and `WarmThroughput: 'nonsense'` are both truthy,
|
|
14113
|
+
* so they went out as `UpdateTable{WarmThroughput: {}}` /
|
|
14114
|
+
* `{WarmThroughput: 'nonsense'}` — a call that can only be rejected, and one
|
|
14115
|
+
* cdkd would repeat on every deploy. Harmless-looking at the table level (one
|
|
14116
|
+
* doomed call, loudly); newly REACHABLE per index once issue #1768's per-index
|
|
14117
|
+
* send path landed, where the same value is emitted once per index. So the rule
|
|
14118
|
+
* is now: at least ONE of the two user-settable members must resolve to a
|
|
14119
|
+
* finite number.
|
|
14120
|
+
*
|
|
14121
|
+
* Still not `!== undefined`, and still ONE spelling for the write sites and the
|
|
14122
|
+
* drift side both: a value this refuses is a value cdkd does not send, so the
|
|
14123
|
+
* readback must not emit AWS's computed value as though the template had asked
|
|
14124
|
+
* for it. A YAML-borne numeric STRING is accepted, via {@link capacityNumber},
|
|
14125
|
+
* because `'12000'` genuinely means 12000.
|
|
14111
14126
|
*/
|
|
14112
14127
|
function isSendableWarmThroughput(value) {
|
|
14113
|
-
return
|
|
14128
|
+
return coerceWarmThroughput(value) !== void 0;
|
|
14129
|
+
}
|
|
14130
|
+
/**
|
|
14131
|
+
* Turn a template-declared `WarmThroughput` into the NUMERIC spec cdkd puts on
|
|
14132
|
+
* the wire, or `undefined` when nothing in it is usable (PR review round 5).
|
|
14133
|
+
*
|
|
14134
|
+
* The send sites used to forward the declared value VERBATIM while
|
|
14135
|
+
* {@link isSendableWarmThroughput} accepted a numeric STRING, so the predicate
|
|
14136
|
+
* blessed the one shape it exists to stop: `{ReadUnitsPerSecond: '12000'}` went
|
|
14137
|
+
* out as `"WarmThroughput":{"ReadUnitsPerSecond":"12000"}` — a string in a Long
|
|
14138
|
+
* field, which DynamoDB rejects — and the drift side then answered "declared"
|
|
14139
|
+
* for it, so the readback emitted AWS's numeric value and the table drifted on
|
|
14140
|
+
* every run. Coercing rather than dropping string support is the better answer
|
|
14141
|
+
* for a stringly-typed CFn template (`'12000'` genuinely means 12000, and
|
|
14142
|
+
* `create()`'s table-level `ProvisionedThroughput` already coerces one line
|
|
14143
|
+
* over), and it makes the quoted form WORK instead of merely warning.
|
|
14144
|
+
*
|
|
14145
|
+
* Per MEMBER, so a bad one cannot take a good one with it, and never `NaN`:
|
|
14146
|
+
* {@link capacityNumber} accepts a number or a numeric string and rejects
|
|
14147
|
+
* everything else, so a member that does not resolve is OMITTED from the spec
|
|
14148
|
+
* and reported in `dropped` for the caller to announce. AWS accepts a
|
|
14149
|
+
* one-member `WarmThroughput` (measured us-east-1, 2026-08-13: an
|
|
14150
|
+
* `UpdateTable` carrying only `ReadUnitsPerSecond` is a valid request shape —
|
|
14151
|
+
* it was refused for being a DECREASE, not for its shape), so a partial send
|
|
14152
|
+
* is a real request rather than a malformed one.
|
|
14153
|
+
*
|
|
14154
|
+
* {@link isSendableWarmThroughput} is defined AS this function's success, so
|
|
14155
|
+
* the drift-side gate and the write-side coercion cannot answer differently —
|
|
14156
|
+
* structurally, not by hand. That identity is exact at the BLOCK level and
|
|
14157
|
+
* ONLY there (PR review round 6, where the earlier wording was measured and
|
|
14158
|
+
* found broader than the truth): both sides ask "is any member usable", so a
|
|
14159
|
+
* block cdkd sends is a block drift compares, and a block cdkd refuses is one
|
|
14160
|
+
* drift ignores.
|
|
14161
|
+
*
|
|
14162
|
+
* PER MEMBER it does NOT hold, and the residual is real:
|
|
14163
|
+
* `{ReadUnitsPerSecond: 12000, WriteUnitsPerSecond: {Ref: 'Unset'}}` sends only
|
|
14164
|
+
* `ReadUnitsPerSecond`, while `readCurrentState` emits AWS's computed value for
|
|
14165
|
+
* BOTH members and `getDriftUnknownPaths` leaves the path compared — so AWS's
|
|
14166
|
+
* `WriteUnitsPerSecond`, which cdkd never sent, is compared against the
|
|
14167
|
+
* template forever. It fails OPEN — the difference is REPORTED by `cdkd drift`,
|
|
14168
|
+
* never hidden — and {@link DynamoDBTableProvider.coerceWarmThroughputForSend}
|
|
14169
|
+
* names the dropped member when the value is APPLIED.
|
|
14170
|
+
*
|
|
14171
|
+
* How loud, precisely (measured, PR review round 7 — an earlier version of this
|
|
14172
|
+
* sentence said "on every deploy" and was falsified by the code two hundred
|
|
14173
|
+
* lines down): every warn in this provider sits behind a CHANGE gate, so the
|
|
14174
|
+
* drop is announced on the deploy that INTRODUCES it and is silent on every
|
|
14175
|
+
* repeat deploy of the same value, while the drift report persists. Loud once,
|
|
14176
|
+
* then a standing drift entry — not a per-deploy nag, and not silent either.
|
|
14177
|
+
* Closing the residual means emitting per-member, which is a `readCurrentState`
|
|
14178
|
+
* shape change with its own drift-baseline migration — deliberately not folded
|
|
14179
|
+
* into this issue.
|
|
14180
|
+
*
|
|
14181
|
+
* NOT a drift fix either way: state records what the TEMPLATE said, so a
|
|
14182
|
+
* `properties` baseline still holds `'12000'` against a numeric readback and
|
|
14183
|
+
* the comparator's `deepEqual` is strict about that. It is the generic
|
|
14184
|
+
* stringly-typed-CFn class (every numeric property has it), not something this
|
|
14185
|
+
* coercion can reach from the write side.
|
|
14186
|
+
*/
|
|
14187
|
+
function coerceWarmThroughput(value) {
|
|
14188
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
14189
|
+
const bag = value;
|
|
14190
|
+
const spec = {};
|
|
14191
|
+
const dropped = [];
|
|
14192
|
+
let usable = 0;
|
|
14193
|
+
for (const member of ["ReadUnitsPerSecond", "WriteUnitsPerSecond"]) {
|
|
14194
|
+
if (bag[member] === void 0) continue;
|
|
14195
|
+
const n = capacityNumber(bag[member]);
|
|
14196
|
+
if (n === void 0) {
|
|
14197
|
+
dropped.push(member);
|
|
14198
|
+
continue;
|
|
14199
|
+
}
|
|
14200
|
+
spec[member] = n;
|
|
14201
|
+
usable++;
|
|
14202
|
+
}
|
|
14203
|
+
if (usable === 0) return void 0;
|
|
14204
|
+
return {
|
|
14205
|
+
spec,
|
|
14206
|
+
dropped
|
|
14207
|
+
};
|
|
14208
|
+
}
|
|
14209
|
+
/**
|
|
14210
|
+
* Is this a `WarmThroughput` the user WROTE that the tightened send rule then
|
|
14211
|
+
* refuses (PR review of issue #1768)?
|
|
14212
|
+
*
|
|
14213
|
+
* The set of values whose OUTCOME this issue changed, and the exact set the
|
|
14214
|
+
* write sites must warn about. Tightening {@link isSendableWarmThroughput}
|
|
14215
|
+
* closed a doomed-call class, and silently: a template declaring
|
|
14216
|
+
* `{ ReadUnitsPerSecnd: 20000 }` (a member typo), `'nonsense'`, `{}`, or an
|
|
14217
|
+
* unresolved intrinsic used to reach AWS and be REJECTED BY NAME, and now
|
|
14218
|
+
* vanishes with no warning, no debug line, and a green deploy — the "loud
|
|
14219
|
+
* failure for a quiet lie" trade the adopted-index arm in `applyGsiUpdates`
|
|
14220
|
+
* refuses, and out of step with every other skip in this file
|
|
14221
|
+
* ({@link DynamoDBTableProvider.skipZeroCapacityIndexUpdate},
|
|
14222
|
+
* {@link DynamoDBTableProvider.skipWarmThroughputDecrease}), which all warn.
|
|
14223
|
+
*
|
|
14224
|
+
* ABSENT is silent, because that is the ordinary case — the overwhelming
|
|
14225
|
+
* majority of templates declare no warm throughput at all and must not be
|
|
14226
|
+
* nagged. FALSY-but-present (`null`, `''`, `false`, `0`) is silent too: those
|
|
14227
|
+
* were skipped silently BEFORE this issue as well (`Boolean(value)` was the
|
|
14228
|
+
* whole rule), so they are not a behavior this PR changed and warning about
|
|
14229
|
+
* them would be a new noise source, not a restored signal.
|
|
14230
|
+
*/
|
|
14231
|
+
function isRefusedWarmThroughput(value) {
|
|
14232
|
+
if (value === void 0) return false;
|
|
14233
|
+
if (!value) return false;
|
|
14234
|
+
return !isSendableWarmThroughput(value);
|
|
14235
|
+
}
|
|
14236
|
+
/**
|
|
14237
|
+
* Does the DESIRED bag carry enough for the drift side to answer "what did the
|
|
14238
|
+
* template declare"?
|
|
14239
|
+
*
|
|
14240
|
+
* An ABSENT or EMPTY bag answers NO: `drift.ts` passes `properties ?? {}`, and
|
|
14241
|
+
* other callers pass nothing at all, so `{}` means "nothing recorded" rather
|
|
14242
|
+
* than "this key is absent" (the contract on {@link ResourceProvider.getDriftUnknownPaths}).
|
|
14243
|
+
* Every gate below falls back to the pre-gate behavior on a NO, since dropping
|
|
14244
|
+
* a key on the strength of a bag that was never populated is unrecoverable
|
|
14245
|
+
* phantom drift in the other direction.
|
|
14246
|
+
*
|
|
14247
|
+
* Named once and shared by the table-level `WarmThroughput` gate (issue #1760)
|
|
14248
|
+
* and the per-index gates (issue #1767) so the two cannot answer the
|
|
14249
|
+
* "uninformative bag" question differently.
|
|
14250
|
+
*/
|
|
14251
|
+
function desiredBagIsInformative(properties) {
|
|
14252
|
+
return properties !== void 0 && Object.keys(properties).length > 0;
|
|
14114
14253
|
}
|
|
14115
14254
|
/**
|
|
14116
14255
|
* Does this desired-property bag DECLARE a table-level `WarmThroughput`?
|
|
@@ -14133,8 +14272,267 @@ function isSendableWarmThroughput(value) {
|
|
|
14133
14272
|
* phantom drift, while the residual is a loud, per-resource revert failure.
|
|
14134
14273
|
*/
|
|
14135
14274
|
function declaresWarmThroughput(properties) {
|
|
14136
|
-
if (
|
|
14137
|
-
return isSendableWarmThroughput(properties["WarmThroughput"]);
|
|
14275
|
+
if (!desiredBagIsInformative(properties)) return true;
|
|
14276
|
+
return properties !== void 0 && isSendableWarmThroughput(properties["WarmThroughput"]);
|
|
14277
|
+
}
|
|
14278
|
+
/**
|
|
14279
|
+
* Is the WarmThroughput this update would SEND a DECREASE of what AWS already
|
|
14280
|
+
* holds (issue #1768)?
|
|
14281
|
+
*
|
|
14282
|
+
* Measured live (us-east-1, 2026-08-13) against a table AWS reports
|
|
14283
|
+
* `{ReadUnitsPerSecond: 12000, WriteUnitsPerSecond: 4000}` for:
|
|
14284
|
+
*
|
|
14285
|
+
* ```
|
|
14286
|
+
* UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000, WriteUnitsPerSecond: 2000}
|
|
14287
|
+
* ValidationException: One or more parameter values were invalid: Requested
|
|
14288
|
+
* ReadUnitsPerSecond for WarmThroughput for table is lower than current
|
|
14289
|
+
* WarmThroughput, decreasing WarmThroughput is not supported
|
|
14290
|
+
* UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000} -> same rejection
|
|
14291
|
+
* UpdateTable WarmThroughput={WriteUnitsPerSecond: 2000} -> same rejection, naming WriteUnitsPerSecond
|
|
14292
|
+
* UpdateTable WarmThroughput={12000, 4000} (re-assert) -> ACCEPTED
|
|
14293
|
+
* ```
|
|
14294
|
+
*
|
|
14295
|
+
* So the value is one AWS raises with the table's traffic and never lowers,
|
|
14296
|
+
* and a decrease is REJECTED rather than accepted-and-ignored — which is what
|
|
14297
|
+
* the issue said to measure before choosing, because the two produce different
|
|
14298
|
+
* correct answers. The caller SKIPS the call on a true here.
|
|
14299
|
+
*
|
|
14300
|
+
* Rules, each one chosen so the skip is right for EVERY `update()` caller
|
|
14301
|
+
* (the deploy engine, `cdkd drift --revert`, and the rollback executor's two
|
|
14302
|
+
* revert arms — none of which can make AWS lower the value, so none of them
|
|
14303
|
+
* loses anything a doomed call would have achieved):
|
|
14304
|
+
*
|
|
14305
|
+
* - Only the members the desired side DECLARES are examined; an omitted member
|
|
14306
|
+
* is not sent and cannot be a decrease.
|
|
14307
|
+
* - EVERY declared member must be at-or-below its live counterpart AND at
|
|
14308
|
+
* least one strictly below. A MIXED request (one member up, one down) still
|
|
14309
|
+
* goes out: AWS rejects it by name, which is the pre-fix behavior and better
|
|
14310
|
+
* than silently dropping the increase the user asked for.
|
|
14311
|
+
* - Anything not resolvable to a finite number fails OPEN (the call is still
|
|
14312
|
+
* issued): the worst case is the pre-fix behavior, whereas a false positive
|
|
14313
|
+
* would silently drop a real increase. Only the LIVE side can actually reach
|
|
14314
|
+
* that arm now (PR review round 8): the desired side is a COERCED spec, so a
|
|
14315
|
+
* malformed template value or an unresolved intrinsic has already been
|
|
14316
|
+
* dropped by {@link coerceWarmThroughput} and cannot arrive here. The check
|
|
14317
|
+
* is kept for the live side, which is whatever `DescribeTable` returned.
|
|
14318
|
+
*/
|
|
14319
|
+
function isWarmThroughputDecrease(desired, live) {
|
|
14320
|
+
if (live === void 0) return false;
|
|
14321
|
+
let sawDecrease = false;
|
|
14322
|
+
for (const member of ["ReadUnitsPerSecond", "WriteUnitsPerSecond"]) {
|
|
14323
|
+
if (desired[member] === void 0) continue;
|
|
14324
|
+
const wanted = capacityNumber(desired[member]);
|
|
14325
|
+
const current = capacityNumber(live[member]);
|
|
14326
|
+
if (wanted === void 0 || current === void 0) return false;
|
|
14327
|
+
if (wanted > current) return false;
|
|
14328
|
+
if (wanted < current) sawDecrease = true;
|
|
14329
|
+
}
|
|
14330
|
+
return sawDecrease;
|
|
14331
|
+
}
|
|
14332
|
+
/**
|
|
14333
|
+
* Does the warm throughput AWS currently holds already equal what this update
|
|
14334
|
+
* would request (issue #1768 PR review)?
|
|
14335
|
+
*
|
|
14336
|
+
* The warm-throughput twin of {@link liveCapacityAlreadyMatches}, and compared
|
|
14337
|
+
* the same way and for the same reason: the live side is a
|
|
14338
|
+
* `...WarmThroughputDescription` carrying an AWS-managed `Status` alongside the
|
|
14339
|
+
* two units, so a structural compare against the two-member desired object
|
|
14340
|
+
* could never match and the suppression would be dead code that only LOOKED
|
|
14341
|
+
* safe.
|
|
14342
|
+
*
|
|
14343
|
+
* Only the members the desired side DECLARES are compared — an omitted member
|
|
14344
|
+
* is not part of the request, so it cannot make the request differ. Anything
|
|
14345
|
+
* unresolvable fails OPEN (the call is still issued): the worst case is a
|
|
14346
|
+
* redundant `UpdateTable`, whereas a false MATCH silently drops a
|
|
14347
|
+
* warm-throughput raise the user asked for. As in
|
|
14348
|
+
* {@link isWarmThroughputDecrease}, only the LIVE side can reach that arm now
|
|
14349
|
+
* that the desired side is a coerced spec.
|
|
14350
|
+
*
|
|
14351
|
+
* Deliberately NOT applied to the TABLE-level branch, which still sends an
|
|
14352
|
+
* equal re-assert: that is measured-accepted by AWS (us-east-1, 2026-08-13 —
|
|
14353
|
+
* re-asserting `{12000, 4000}` returns a normal `TableDescription`), it is ONE
|
|
14354
|
+
* call rather than one per index, and its own change gate already requires the
|
|
14355
|
+
* value to differ from the recorded previous. The per-index path is where the
|
|
14356
|
+
* redundancy multiplies and where the carried #1767 residual reaches it.
|
|
14357
|
+
*/
|
|
14358
|
+
function warmThroughputAlreadyMatches(desired, live) {
|
|
14359
|
+
if (live === void 0) return false;
|
|
14360
|
+
let compared = 0;
|
|
14361
|
+
for (const member of ["ReadUnitsPerSecond", "WriteUnitsPerSecond"]) {
|
|
14362
|
+
if (desired[member] === void 0) continue;
|
|
14363
|
+
const wanted = capacityNumber(desired[member]);
|
|
14364
|
+
const current = capacityNumber(live[member]);
|
|
14365
|
+
if (wanted === void 0 || current === void 0) return false;
|
|
14366
|
+
if (wanted !== current) return false;
|
|
14367
|
+
compared++;
|
|
14368
|
+
}
|
|
14369
|
+
return compared > 0;
|
|
14370
|
+
}
|
|
14371
|
+
/**
|
|
14372
|
+
* Reverse-map one live secondary-index description to its CloudFormation
|
|
14373
|
+
* shape (issue #1767).
|
|
14374
|
+
*
|
|
14375
|
+
* `DescribeTable` describes an index with a superset of the CFn members —
|
|
14376
|
+
* measured live, us-east-1 2026-08-13, on a `PAY_PER_REQUEST` table with one
|
|
14377
|
+
* GSI and no capacity settings:
|
|
14378
|
+
*
|
|
14379
|
+
* ```json
|
|
14380
|
+
* { "IndexName": "gsi1",
|
|
14381
|
+
* "KeySchema": [{ "AttributeName": "gsipk", "KeyType": "HASH" }],
|
|
14382
|
+
* "Projection": { "ProjectionType": "ALL" },
|
|
14383
|
+
* "IndexStatus": "CREATING", "Backfilling": true,
|
|
14384
|
+
* "ProvisionedThroughput": { "NumberOfDecreasesToday": 0, "ReadCapacityUnits": 0, "WriteCapacityUnits": 0 },
|
|
14385
|
+
* "IndexSizeBytes": 0, "ItemCount": 0,
|
|
14386
|
+
* "IndexArn": "arn:aws:dynamodb:...:table/.../index/gsi1",
|
|
14387
|
+
* "WarmThroughput": { "ReadUnitsPerSecond": 12000, "WriteUnitsPerSecond": 4000, "Status": "UPDATING" } }
|
|
14388
|
+
* ```
|
|
14389
|
+
*
|
|
14390
|
+
* NONE of `IndexStatus` / `Backfilling` / `IndexSizeBytes` / `ItemCount` /
|
|
14391
|
+
* `IndexArn` is a CFn property, the `ProvisionedThroughput` block is AWS's
|
|
14392
|
+
* `{0, 0}` on-demand placeholder (the #1571 trap), and the per-index
|
|
14393
|
+
* `WarmThroughput` is the AWS-computed value the table-level gate already
|
|
14394
|
+
* refuses to surface (issue #1760). Forwarding the description verbatim put
|
|
14395
|
+
* all of them into the drift comparison, and — unlike a top-level key the
|
|
14396
|
+
* baseline lacks — they are members of an ARRAY the baseline DOES carry, which
|
|
14397
|
+
* `deepEqual` compares positionally, so every one of them participated.
|
|
14398
|
+
*
|
|
14399
|
+
* The mapper is therefore an ALLOW-LIST rather than a deny-list: a member AWS
|
|
14400
|
+
* adds later is dropped by construction instead of silently joining the
|
|
14401
|
+
* comparison.
|
|
14402
|
+
*
|
|
14403
|
+
* `desired` is the DESIRED bag's entry for the SAME `IndexName` (matched by
|
|
14404
|
+
* name, never by position — the list is an unordered set, see
|
|
14405
|
+
* {@link DynamoDBTableProvider.getDriftUnorderedPaths}). The three throughput
|
|
14406
|
+
* blocks are emitted only when that entry declares them, the #1760 shape one
|
|
14407
|
+
* nesting level down; `bagInformative` false means the caller supplied no bag
|
|
14408
|
+
* to decide with and every block is emitted, cleaned of its AWS-managed
|
|
14409
|
+
* members.
|
|
14410
|
+
*
|
|
14411
|
+
* Known residual, unchanged by this fix: CFn's per-index
|
|
14412
|
+
* `ContributorInsightsSpecification` has no `DescribeTable` counterpart (it
|
|
14413
|
+
* needs a per-index `DescribeContributorInsights` call), so a template
|
|
14414
|
+
* declaring one still cannot converge against a `properties` baseline. It is
|
|
14415
|
+
* ALSO dropped on the write side, since `create()` forwards the CFn blob to
|
|
14416
|
+
* `CreateTable` and the SDK serializer discards the unknown member — still
|
|
14417
|
+
* true after round 6 taught that path to coerce `WarmThroughput`, because the
|
|
14418
|
+
* entry is rebuilt with a SPREAD and every other member, known or not, rides
|
|
14419
|
+
* along unchanged. Filed as issue
|
|
14420
|
+
* [#1782](https://github.com/go-to-k/cdkd/issues/1782).
|
|
14421
|
+
*/
|
|
14422
|
+
function reverseMapSecondaryIndex(live, desired, bagInformative) {
|
|
14423
|
+
const out = {};
|
|
14424
|
+
if (live.IndexName !== void 0) out["IndexName"] = live.IndexName;
|
|
14425
|
+
if (live.KeySchema !== void 0) out["KeySchema"] = live.KeySchema.map((element) => ({ ...element }));
|
|
14426
|
+
if (live.Projection !== void 0) {
|
|
14427
|
+
const projection = {};
|
|
14428
|
+
if (live.Projection.ProjectionType !== void 0) projection["ProjectionType"] = live.Projection.ProjectionType;
|
|
14429
|
+
if (live.Projection.NonKeyAttributes !== void 0) projection["NonKeyAttributes"] = [...live.Projection.NonKeyAttributes];
|
|
14430
|
+
if (Object.keys(projection).length > 0) out["Projection"] = projection;
|
|
14431
|
+
}
|
|
14432
|
+
if ("ProvisionedThroughput" in live && indexDeclares(desired, "ProvisionedThroughput", bagInformative) && live.ProvisionedThroughput) {
|
|
14433
|
+
const provisioned = live.ProvisionedThroughput;
|
|
14434
|
+
const pt = {};
|
|
14435
|
+
if (provisioned.ReadCapacityUnits !== void 0) pt["ReadCapacityUnits"] = provisioned.ReadCapacityUnits;
|
|
14436
|
+
if (provisioned.WriteCapacityUnits !== void 0) pt["WriteCapacityUnits"] = provisioned.WriteCapacityUnits;
|
|
14437
|
+
if (Object.keys(pt).length > 0) out["ProvisionedThroughput"] = pt;
|
|
14438
|
+
}
|
|
14439
|
+
if ("OnDemandThroughput" in live && indexDeclares(desired, "OnDemandThroughput", bagInformative) && live.OnDemandThroughput) {
|
|
14440
|
+
const onDemand = live.OnDemandThroughput;
|
|
14441
|
+
const odt = {};
|
|
14442
|
+
if (onDemand.MaxReadRequestUnits !== void 0) odt["MaxReadRequestUnits"] = onDemand.MaxReadRequestUnits;
|
|
14443
|
+
if (onDemand.MaxWriteRequestUnits !== void 0) odt["MaxWriteRequestUnits"] = onDemand.MaxWriteRequestUnits;
|
|
14444
|
+
if (Object.keys(odt).length > 0) out["OnDemandThroughput"] = odt;
|
|
14445
|
+
}
|
|
14446
|
+
if ("WarmThroughput" in live && indexDeclares(desired, "WarmThroughput", bagInformative) && live.WarmThroughput) {
|
|
14447
|
+
const warm = live.WarmThroughput;
|
|
14448
|
+
const wt = {};
|
|
14449
|
+
if (warm.ReadUnitsPerSecond !== void 0) wt["ReadUnitsPerSecond"] = warm.ReadUnitsPerSecond;
|
|
14450
|
+
if (warm.WriteUnitsPerSecond !== void 0) wt["WriteUnitsPerSecond"] = warm.WriteUnitsPerSecond;
|
|
14451
|
+
if (Object.keys(wt).length > 0) out["WarmThroughput"] = wt;
|
|
14452
|
+
}
|
|
14453
|
+
return out;
|
|
14454
|
+
}
|
|
14455
|
+
/**
|
|
14456
|
+
* Does the DESIRED entry for one index declare this block (issue #1767)?
|
|
14457
|
+
*
|
|
14458
|
+
* The question is always "would cdkd SEND this block", because that is what
|
|
14459
|
+
* makes AWS's readback value explainable by the template — so each block is
|
|
14460
|
+
* answered by the write rule that actually governs it, never by a second
|
|
14461
|
+
* spelling of one:
|
|
14462
|
+
*
|
|
14463
|
+
* - `WarmThroughput` routes through {@link isSendableWarmThroughput}, the
|
|
14464
|
+
* predicate EVERY per-index write path calls — the `Create` / `Update` GSI
|
|
14465
|
+
* actions in `applyGsiUpdates` AND `create()`'s `CreateTable` forward, which
|
|
14466
|
+
* maps each entry through
|
|
14467
|
+
* {@link DynamoDBTableProvider.coerceIndexWarmThroughputForCreate}. It is
|
|
14468
|
+
* TIGHTER than truthiness (at least one member resolving to a finite
|
|
14469
|
+
* number), so `WarmThroughput: {}` is neither sent nor emitted on any path.
|
|
14470
|
+
* An earlier version of this bullet carved out the TABLE-create path as an
|
|
14471
|
+
* exception, and that exception was a DEFECT rather than a design: it also
|
|
14472
|
+
* put a quoted `'12000'` on the wire as a string in a Long field, so one
|
|
14473
|
+
* template failed on a fresh create and succeeded on a later GSI add. Fixed
|
|
14474
|
+
* in PR review round 6; the carve-out is gone because the divergence is.
|
|
14475
|
+
* - `ProvisionedThroughput` / `OnDemandThroughput` keep TRUTHINESS, which is
|
|
14476
|
+
* what their write path does: those two members ARE still forwarded verbatim
|
|
14477
|
+
* (the create-path mapper rewrites only `WarmThroughput`), and
|
|
14478
|
+
* `applyGsiUpdates` gates on `gsi.ProvisionedThroughput` alone. Widening
|
|
14479
|
+
* `isSendableWarmThroughput` over them would be wrong twice over — it reads
|
|
14480
|
+
* members those blocks do not have (`ReadUnitsPerSecond` vs
|
|
14481
|
+
* `ReadCapacityUnits`), so every declared capacity would read as undeclared.
|
|
14482
|
+
*
|
|
14483
|
+
* An UNINFORMATIVE bag answers TRUE for every block, keeping the pre-#1767
|
|
14484
|
+
* behavior for a caller that supplied nothing to decide with. A bag that IS
|
|
14485
|
+
* informative but names no entry for this index answers FALSE: the index
|
|
14486
|
+
* exists in AWS and not in the template, so nothing cdkd sent can explain any
|
|
14487
|
+
* of its throughput blocks.
|
|
14488
|
+
*/
|
|
14489
|
+
function indexDeclares(desired, key, bagInformative) {
|
|
14490
|
+
if (!bagInformative) return true;
|
|
14491
|
+
const value = desired?.[key];
|
|
14492
|
+
return key === "WarmThroughput" ? isSendableWarmThroughput(value) : Boolean(value);
|
|
14493
|
+
}
|
|
14494
|
+
/**
|
|
14495
|
+
* Is this value an index list that DECLARES at least one index (issue #1767)?
|
|
14496
|
+
*
|
|
14497
|
+
* The ONE spelling of that question, shared by {@link desiredIndexEntriesByName}
|
|
14498
|
+
* (which decides what each live index is matched against) and by
|
|
14499
|
+
* {@link DynamoDBTableProvider.getDriftUnknownPaths} (which decides whether the
|
|
14500
|
+
* list is compared at all). Spelled independently they disagreed: a plain
|
|
14501
|
+
* truthiness test reads `[]` and an unresolved `{Fn::If: [...]}` as DECLARED —
|
|
14502
|
+
* so the list stayed in the comparison while the mapper, which needs an ARRAY
|
|
14503
|
+
* to match names against, treated the same value as declaring nothing and
|
|
14504
|
+
* stripped every throughput block from the readback. That is a one-sided
|
|
14505
|
+
* difference manufactured by the pair, which is the failure this file's
|
|
14506
|
+
* `isSendableWarmThroughput` header exists to prevent.
|
|
14507
|
+
*
|
|
14508
|
+
* Consequence worth stating: a template declaring `GlobalSecondaryIndexes: []`
|
|
14509
|
+
* now has that path IGNORED by the comparator, so an index created out of band
|
|
14510
|
+
* on such a table is not reported. That is the same answer the readback gives
|
|
14511
|
+
* (nothing the template declared can explain that index), and consistency
|
|
14512
|
+
* between the two is worth more than detection on a shape where the two used to
|
|
14513
|
+
* contradict each other.
|
|
14514
|
+
*/
|
|
14515
|
+
function declaresIndexList(value) {
|
|
14516
|
+
return Array.isArray(value) && value.length > 0;
|
|
14517
|
+
}
|
|
14518
|
+
/**
|
|
14519
|
+
* Index the DESIRED bag's secondary-index list by `IndexName` (issue #1767).
|
|
14520
|
+
*
|
|
14521
|
+
* Returns an empty map for anything {@link declaresIndexList} rejects — an
|
|
14522
|
+
* empty array, an unresolved intrinsic, a mis-nested template value — which
|
|
14523
|
+
* routes every live index through {@link indexDeclares}'s "declares nothing"
|
|
14524
|
+
* arm. Sharing that predicate with `getDriftUnknownPaths` is what keeps the
|
|
14525
|
+
* emission and the comparison from answering differently for the same bag.
|
|
14526
|
+
*/
|
|
14527
|
+
function desiredIndexEntriesByName(value) {
|
|
14528
|
+
const byName = /* @__PURE__ */ new Map();
|
|
14529
|
+
if (!declaresIndexList(value)) return byName;
|
|
14530
|
+
for (const entry of value) {
|
|
14531
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
14532
|
+
const name = entry["IndexName"];
|
|
14533
|
+
if (typeof name === "string") byName.set(name, entry);
|
|
14534
|
+
}
|
|
14535
|
+
return byName;
|
|
14138
14536
|
}
|
|
14139
14537
|
var DynamoDBTableProvider = class {
|
|
14140
14538
|
dynamoDBClient;
|
|
@@ -14192,12 +14590,17 @@ var DynamoDBTableProvider = class {
|
|
|
14192
14590
|
};
|
|
14193
14591
|
}
|
|
14194
14592
|
if (properties["OnDemandThroughput"]) createParams.OnDemandThroughput = properties["OnDemandThroughput"];
|
|
14195
|
-
|
|
14593
|
+
const createWarmThroughput = this.coerceWarmThroughputForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"]);
|
|
14594
|
+
if (createWarmThroughput) createParams.WarmThroughput = createWarmThroughput;
|
|
14595
|
+
else this.warnRefusedWarmThroughput(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"]);
|
|
14196
14596
|
if (properties["StreamSpecification"]) createParams.StreamSpecification = {
|
|
14197
14597
|
StreamEnabled: true,
|
|
14198
14598
|
StreamViewType: properties["StreamSpecification"]["StreamViewType"]
|
|
14199
14599
|
};
|
|
14200
|
-
if (properties["GlobalSecondaryIndexes"])
|
|
14600
|
+
if (properties["GlobalSecondaryIndexes"]) {
|
|
14601
|
+
const declaredGsis = properties["GlobalSecondaryIndexes"];
|
|
14602
|
+
createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexWarmThroughputForCreate(logicalId, entry)) : declaredGsis;
|
|
14603
|
+
}
|
|
14201
14604
|
if (properties["LocalSecondaryIndexes"]) createParams.LocalSecondaryIndexes = properties["LocalSecondaryIndexes"];
|
|
14202
14605
|
const sse = mapSSESpecification(properties["SSESpecification"]);
|
|
14203
14606
|
if (sse && Object.keys(sse).length > 0) createParams.SSESpecification = sse;
|
|
@@ -14355,10 +14758,12 @@ var DynamoDBTableProvider = class {
|
|
|
14355
14758
|
}
|
|
14356
14759
|
}
|
|
14357
14760
|
if (JSON.stringify(properties["WarmThroughput"]) !== JSON.stringify(previousProperties["WarmThroughput"])) {
|
|
14358
|
-
|
|
14761
|
+
this.warnRefusedWarmThroughput(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"]);
|
|
14762
|
+
const warmThroughputToSend = this.coerceWarmThroughputForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["WarmThroughput"]);
|
|
14763
|
+
if (warmThroughputToSend !== void 0 && !this.skipWarmThroughputDecrease(logicalId, physicalId, warmThroughputToSend, table?.WarmThroughput)) {
|
|
14359
14764
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
14360
14765
|
TableName: physicalId,
|
|
14361
|
-
WarmThroughput:
|
|
14766
|
+
WarmThroughput: warmThroughputToSend
|
|
14362
14767
|
}));
|
|
14363
14768
|
await this.waitForTableActiveAfterUpdate(physicalId);
|
|
14364
14769
|
this.logger.debug(`Updated WarmThroughput on DynamoDB table ${physicalId}`);
|
|
@@ -14822,6 +15227,32 @@ var DynamoDBTableProvider = class {
|
|
|
14822
15227
|
* between GSI mutations because a freshly-created index keeps backfilling
|
|
14823
15228
|
* after the table itself returns to ACTIVE, and AWS rejects the next GSI op
|
|
14824
15229
|
* until the prior index settles.
|
|
15230
|
+
*
|
|
15231
|
+
* `WarmThroughput.Status` is deliberately NOT part of the predicate, and that
|
|
15232
|
+
* is MEASURED rather than assumed (PR review of issue #1768 raised it as a
|
|
15233
|
+
* suspected race, since the per-index warm-throughput send path this provider
|
|
15234
|
+
* gained has no other reason to wait). Live, us-east-1 2026-08-13, on a
|
|
15235
|
+
* PAY_PER_REQUEST table with two ACTIVE GSIs: raising `gsi1`'s warm
|
|
15236
|
+
* throughput 12000/4000 -> 14000/5000 left
|
|
15237
|
+
*
|
|
15238
|
+
* ```
|
|
15239
|
+
* TableStatus: ACTIVE | gsi1: IndexStatus ACTIVE, WarmThroughput.Status UPDATING
|
|
15240
|
+
* | gsi2: IndexStatus ACTIVE, WarmThroughput.Status ACTIVE
|
|
15241
|
+
* ```
|
|
15242
|
+
*
|
|
15243
|
+
* for 90+ seconds — so this predicate IS satisfied while a warm update is
|
|
15244
|
+
* still settling, exactly as the review described. What does not follow is
|
|
15245
|
+
* the failure: with `gsi1` in that state, all three next-op shapes were
|
|
15246
|
+
* ACCEPTED, no `ResourceInUseException` —
|
|
15247
|
+
*
|
|
15248
|
+
* - a warm-throughput `Update` on the OTHER index (`gsi2`);
|
|
15249
|
+
* - a warm-throughput `Update` on the SAME index (`gsi1`);
|
|
15250
|
+
* - a GSI `Create` (`gsi3`), the op AWS gates most strictly.
|
|
15251
|
+
*
|
|
15252
|
+
* So warm throughput settles OUT OF BAND of the index lifecycle, and adding
|
|
15253
|
+
* it to this predicate would only make every GSI op wait on a status that
|
|
15254
|
+
* blocks nothing — minutes of wait budget spent per op, with a real risk of
|
|
15255
|
+
* exhausting it on a table whose warm status lingers.
|
|
14825
15256
|
*/
|
|
14826
15257
|
async waitForTableAndIndexesActive(tableName, maxAttempts = 1800) {
|
|
14827
15258
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -14872,32 +15303,223 @@ var DynamoDBTableProvider = class {
|
|
|
14872
15303
|
if (!gsi.KeySchema) throw new ProvisioningError(`GlobalSecondaryIndex ${name} on DynamoDB table ${logicalId} is missing KeySchema`, resourceType, logicalId, physicalId);
|
|
14873
15304
|
if (live !== void 0 && live.IndexStatus !== "DELETING") {
|
|
14874
15305
|
this.logger.warn(`GSI ${name} on DynamoDB table ${physicalId} already exists in AWS but is absent from cdkd's recorded previous state, so its Create is being skipped. This usually means an earlier deploy created the index and then failed before state was written.`);
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
15306
|
+
const adopted = { IndexName: name };
|
|
15307
|
+
let adoptedHasMember = false;
|
|
15308
|
+
if (liveCapacityComparable && gsi.ProvisionedThroughput && !handledByBillingFlip.has(name) && !liveCapacityAlreadyMatches(live.ProvisionedThroughput, gsi.ProvisionedThroughput) && !this.skipZeroCapacityIndexUpdate(name, physicalId, gsi.ProvisionedThroughput)) {
|
|
15309
|
+
adopted.ProvisionedThroughput = gsi.ProvisionedThroughput;
|
|
15310
|
+
adoptedHasMember = true;
|
|
15311
|
+
}
|
|
15312
|
+
const adoptedWarm = this.warmThroughputOpFor(name, physicalId, gsi, void 0, live);
|
|
15313
|
+
if (adoptedWarm) {
|
|
15314
|
+
adopted.WarmThroughput = adoptedWarm;
|
|
15315
|
+
adoptedHasMember = true;
|
|
15316
|
+
}
|
|
15317
|
+
if (adoptedHasMember) ops.push({ Update: adopted });
|
|
14879
15318
|
this.logger.warn(`GSI ${name} was adopted from AWS rather than created, so cdkd could not verify its KeySchema / Projection match the template. Run \`cdkd drift ${physicalId}\` to confirm, and \`cdkd drift --accept\` if AWS is the source of truth.`);
|
|
14880
15319
|
continue;
|
|
14881
15320
|
}
|
|
15321
|
+
this.warnRefusedWarmThroughput(`GSI ${name} on DynamoDB table ${physicalId}`, gsi.WarmThroughput);
|
|
15322
|
+
const createWarm = this.coerceWarmThroughputForSend(`GSI ${name} on DynamoDB table ${physicalId}`, gsi.WarmThroughput);
|
|
14882
15323
|
ops.push({ Create: {
|
|
14883
15324
|
IndexName: name,
|
|
14884
15325
|
KeySchema: gsi.KeySchema,
|
|
14885
15326
|
Projection: gsi.Projection,
|
|
14886
15327
|
...gsi.ProvisionedThroughput ? { ProvisionedThroughput: gsi.ProvisionedThroughput } : {},
|
|
14887
|
-
...gsi.OnDemandThroughput ? { OnDemandThroughput: gsi.OnDemandThroughput } : {}
|
|
15328
|
+
...gsi.OnDemandThroughput ? { OnDemandThroughput: gsi.OnDemandThroughput } : {},
|
|
15329
|
+
...createWarm ? { WarmThroughput: createWarm } : {}
|
|
14888
15330
|
} });
|
|
14889
15331
|
} else {
|
|
14890
15332
|
const before = prevByName.get(name);
|
|
14891
15333
|
if (JSON.stringify(before.KeySchema) !== JSON.stringify(gsi.KeySchema) || JSON.stringify(before.Projection) !== JSON.stringify(gsi.Projection)) throw new ProvisioningError(`GlobalSecondaryIndex ${name} on DynamoDB table ${logicalId} changed its KeySchema or Projection, which DynamoDB cannot modify in place. Rename the index (so it is dropped and re-created) or replace the table.`, resourceType, logicalId, physicalId);
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
ProvisionedThroughput
|
|
14896
|
-
|
|
15334
|
+
const update = { IndexName: name };
|
|
15335
|
+
let updateHasMember = false;
|
|
15336
|
+
if (gsi.ProvisionedThroughput && !handledByBillingFlip.has(name) && JSON.stringify(before.ProvisionedThroughput) !== JSON.stringify(gsi.ProvisionedThroughput)) {
|
|
15337
|
+
if (liveCapacityComparable && liveCapacityAlreadyMatches(liveIndexByName?.get(name)?.ProvisionedThroughput, gsi.ProvisionedThroughput)) this.logger.debug(`GSI ${name} on DynamoDB table ${physicalId} already carries the requested capacity in AWS; skipping its throughput Update`);
|
|
15338
|
+
else if (!this.skipZeroCapacityIndexUpdate(name, physicalId, gsi.ProvisionedThroughput)) {
|
|
15339
|
+
update.ProvisionedThroughput = gsi.ProvisionedThroughput;
|
|
15340
|
+
updateHasMember = true;
|
|
15341
|
+
}
|
|
15342
|
+
}
|
|
15343
|
+
const warm = this.warmThroughputOpFor(name, physicalId, gsi, before, liveIndexByName?.get(name));
|
|
15344
|
+
if (warm) {
|
|
15345
|
+
update.WarmThroughput = warm;
|
|
15346
|
+
updateHasMember = true;
|
|
15347
|
+
}
|
|
15348
|
+
if (updateHasMember) ops.push({ Update: update });
|
|
14897
15349
|
}
|
|
14898
15350
|
await this.runGsiOps(physicalId, ops, desiredAttributeDefinitions);
|
|
14899
15351
|
}
|
|
14900
15352
|
/**
|
|
15353
|
+
* The NUMERIC `WarmThroughput` spec to put on the wire, announcing any member
|
|
15354
|
+
* that had to be dropped (PR review round 5).
|
|
15355
|
+
*
|
|
15356
|
+
* The single send-side entry point for all four write sites, so a template's
|
|
15357
|
+
* quoted `'12000'` reaches AWS as `12000` rather than as a string in a Long
|
|
15358
|
+
* field. Returns `undefined` for a value {@link isSendableWarmThroughput}
|
|
15359
|
+
* refuses — the caller's existing refusal warning covers that case, and the
|
|
15360
|
+
* two messages are disjoint by construction: this one fires only when
|
|
15361
|
+
* something DID resolve.
|
|
15362
|
+
*/
|
|
15363
|
+
coerceWarmThroughputForSend(scope, value) {
|
|
15364
|
+
const coerced = coerceWarmThroughput(value);
|
|
15365
|
+
if (coerced === void 0) return void 0;
|
|
15366
|
+
if (coerced.dropped.length > 0) this.logger.warn(`${scope}: WarmThroughput member(s) ${coerced.dropped.join(", ")} in ${JSON.stringify(value)} are not a number DynamoDB accepts, so they were dropped from the request, which leaves ${JSON.stringify(coerced.spec)}. Check for an unresolved intrinsic or a non-numeric value.`);
|
|
15367
|
+
return coerced.spec;
|
|
15368
|
+
}
|
|
15369
|
+
/**
|
|
15370
|
+
* One `CreateTable` index entry with its `WarmThroughput` coerced (PR review
|
|
15371
|
+
* round 6).
|
|
15372
|
+
*
|
|
15373
|
+
* `create()` forwards the declared `GlobalSecondaryIndexes` array to
|
|
15374
|
+
* `CreateTable`, so it is a SEND SITE for the same per-index value the
|
|
15375
|
+
* update path coerces — and it was the one the round-5 sweep missed. Entries
|
|
15376
|
+
* are rebuilt rather than mutated: the input bag belongs to the caller (the
|
|
15377
|
+
* resolved template, which the engine also records into state), and a
|
|
15378
|
+
* provider that edits it in place would change what state reports cdkd sent.
|
|
15379
|
+
*
|
|
15380
|
+
* An entry that is not a plain object, or that declares no `WarmThroughput`,
|
|
15381
|
+
* is returned UNCHANGED — including the identity of the object, so the
|
|
15382
|
+
* common case allocates nothing.
|
|
15383
|
+
*/
|
|
15384
|
+
coerceIndexWarmThroughputForCreate(logicalId, entry) {
|
|
15385
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
|
|
15386
|
+
const declared = entry["WarmThroughput"];
|
|
15387
|
+
if (declared === void 0) return entry;
|
|
15388
|
+
const scope = `GSI ${entry.IndexName ?? "<unnamed>"} on AWS::DynamoDB::Table ${logicalId}`;
|
|
15389
|
+
const coerced = this.coerceWarmThroughputForSend(scope, declared);
|
|
15390
|
+
if (coerced === void 0) {
|
|
15391
|
+
this.warnRefusedWarmThroughput(scope, declared);
|
|
15392
|
+
const { WarmThroughput: _dropped, ...rest } = entry;
|
|
15393
|
+
return rest;
|
|
15394
|
+
}
|
|
15395
|
+
return {
|
|
15396
|
+
...entry,
|
|
15397
|
+
WarmThroughput: coerced
|
|
15398
|
+
};
|
|
15399
|
+
}
|
|
15400
|
+
/**
|
|
15401
|
+
* Announce a `WarmThroughput` the send rule REFUSES (PR review of issue
|
|
15402
|
+
* #1768) — the missing half of tightening {@link isSendableWarmThroughput}.
|
|
15403
|
+
*
|
|
15404
|
+
* Called at all FOUR write sites (`create()`, the table-level `update()`
|
|
15405
|
+
* branch, the GSI `Create` action, and {@link warmThroughputOpFor}) because
|
|
15406
|
+
* the value vanishes at each of them independently. `scope` names the
|
|
15407
|
+
* resource or the index so a table with several GSIs says WHICH one.
|
|
15408
|
+
*
|
|
15409
|
+
* Silent for an ABSENT or FALSY value — see {@link isRefusedWarmThroughput}
|
|
15410
|
+
* for why those two are not this warning's business.
|
|
15411
|
+
*/
|
|
15412
|
+
warnRefusedWarmThroughput(scope, value) {
|
|
15413
|
+
if (!isRefusedWarmThroughput(value)) return;
|
|
15414
|
+
this.logger.warn(`${scope}: the declared WarmThroughput ${JSON.stringify(value)} carries no usable ReadUnitsPerSecond or WriteUnitsPerSecond, so it was NOT sent — DynamoDB accepts only those two members and rejects a request without either. Check for a misspelled member name or an unresolved intrinsic. Nothing else on this resource was affected.`);
|
|
15415
|
+
}
|
|
15416
|
+
/**
|
|
15417
|
+
* Should this per-index capacity `Update` be SKIPPED because the value is
|
|
15418
|
+
* AWS's on-demand `{0, 0}` placeholder rather than a capacity anyone asked
|
|
15419
|
+
* for (issue #1767 review)?
|
|
15420
|
+
*
|
|
15421
|
+
* Newly reachable BECAUSE of the #1767 readback change, on the transition
|
|
15422
|
+
* residual `getDriftUnknownPaths` deliberately carries. A pre-#1767
|
|
15423
|
+
* `observedProperties` baseline holds the whole `DescribeTable` index
|
|
15424
|
+
* description, including the `{NumberOfDecreasesToday: 0, ReadCapacityUnits:
|
|
15425
|
+
* 0, WriteCapacityUnits: 0}` block AWS reports for every index of a
|
|
15426
|
+
* PAY_PER_REQUEST table (the #1571 trap). `cdkd drift --revert` hands that
|
|
15427
|
+
* blob to `update()` as the DESIRED side against the trimmed readback as the
|
|
15428
|
+
* previous side, so the values differ, `liveCapacityComparable` is false on
|
|
15429
|
+
* PAY_PER_REQUEST (which disables the #1630 idempotency skip), and the op
|
|
15430
|
+
* that goes out asks AWS for capacity 0 — which AWS rejects, since the
|
|
15431
|
+
* minimum is 1. Before #1767 both sides carried the same blob, compared
|
|
15432
|
+
* equal, and no revert was ever offered.
|
|
15433
|
+
*
|
|
15434
|
+
* Deliberately NOT gated on the live billing mode. A `{0, 0}` request is
|
|
15435
|
+
* refused by AWS in EITHER mode, so gating would leave the same doomed call
|
|
15436
|
+
* reachable for a PROVISIONED table whose stale baseline carries the
|
|
15437
|
+
* placeholder. Scoped tightly to BOTH members resolving to 0: any other
|
|
15438
|
+
* capacity — including a half-declared or malformed one — still goes out and
|
|
15439
|
+
* is answered by AWS, the fail-OPEN direction this file uses everywhere.
|
|
15440
|
+
*
|
|
15441
|
+
* CALLER-BLIND, decided rather than overlooked (PR review). A TEMPLATE-borne
|
|
15442
|
+
* `{0, 0}` warn-skips here while `create()` fails loudly on the same value,
|
|
15443
|
+
* which reads like the "loud failure for a quiet lie" trade the adopted-index
|
|
15444
|
+
* arm below forbids. It stands for three reasons:
|
|
15445
|
+
*
|
|
15446
|
+
* - The discriminator that EXISTS does not separate the callers that matter.
|
|
15447
|
+
* `ResourceProvider.update` DOES take an optional `UpdateContext` — an
|
|
15448
|
+
* earlier draft of this comment claimed the signature carries no context
|
|
15449
|
+
* at all, which is simply false (`s3-bucket-provider.ts` consumes it
|
|
15450
|
+
* today) — but its one field, `desiredFromAwsReadback`, is set ONLY by
|
|
15451
|
+
* `cdkd drift --revert` (`src/cli/commands/drift.ts`). The rollback
|
|
15452
|
+
* executor's revert arms deliberately pass NO context: their desired bag
|
|
15453
|
+
* is `previousState.properties`, a TEMPLATE recorded earlier, so they are
|
|
15454
|
+
* indistinguishable from an ordinary template deploy — and THAT is the
|
|
15455
|
+
* pair this decision turns on. Knowing the value came from a readback
|
|
15456
|
+
* would not license throwing on the other two.
|
|
15457
|
+
* - With the caller unknowable, the repo's rule for the UPDATE path is
|
|
15458
|
+
* WARN-never-throw (issues #1545 / #1552): the desired bag here can BE a
|
|
15459
|
+
* historical cdkd state record, and a refusal would make the table
|
|
15460
|
+
* un-updatable and un-rollbackable with no template-side remedy.
|
|
15461
|
+
* - The lie is bounded and self-surfacing. AWS never accepted `{0, 0}` at
|
|
15462
|
+
* create either, so no live table can be holding it; and where the value
|
|
15463
|
+
* would actually matter — a PROVISIONED table, whose indexes hold a real
|
|
15464
|
+
* capacity — recording `{0, 0}` diverges from the readback and `cdkd drift`
|
|
15465
|
+
* REPORTS it. On a PAY_PER_REQUEST table the recorded `{0, 0}` matches what
|
|
15466
|
+
* `DescribeTable` reports for every index anyway, so there is no divergence
|
|
15467
|
+
* to hide.
|
|
15468
|
+
*/
|
|
15469
|
+
skipZeroCapacityIndexUpdate(indexName, physicalId, requested) {
|
|
15470
|
+
if (requested === void 0) return false;
|
|
15471
|
+
if (capacityNumber(requested.ReadCapacityUnits) !== 0 || capacityNumber(requested.WriteCapacityUnits) !== 0) return false;
|
|
15472
|
+
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.`);
|
|
15473
|
+
return true;
|
|
15474
|
+
}
|
|
15475
|
+
/**
|
|
15476
|
+
* The per-index `WarmThroughput` this update should send, or `undefined`
|
|
15477
|
+
* (issue #1768, one nesting level down from the table-level branch).
|
|
15478
|
+
*
|
|
15479
|
+
* `applyGsiUpdates` used to send only `ProvisionedThroughput` /
|
|
15480
|
+
* `OnDemandThroughput` on both of its arms, while `readCurrentState` emits a
|
|
15481
|
+
* declared per-index `WarmThroughput` — so a template declaring one had it
|
|
15482
|
+
* silently dropped on every index add / change, `cdkd drift` reported the
|
|
15483
|
+
* difference forever, and `--revert` emitted no op at all and exited 0
|
|
15484
|
+
* claiming success. That silent-success shape is worse than the loud
|
|
15485
|
+
* table-level dead end #1768 fixed.
|
|
15486
|
+
*
|
|
15487
|
+
* The rules mirror the table-level branch exactly:
|
|
15488
|
+
* - only a value the write side would SEND at all
|
|
15489
|
+
* ({@link isSendableWarmThroughput});
|
|
15490
|
+
* - only when it CHANGED against the recorded previous entry (an absent
|
|
15491
|
+
* previous — the adopted-index arm — is treated as changed, since nothing
|
|
15492
|
+
* was recorded to compare with);
|
|
15493
|
+
* - never a DECREASE. Measured live (us-east-1, 2026-08-13) on a GSI AWS
|
|
15494
|
+
* reports `{12000, 4000}` for: a `WarmThroughput`-only `Update` action
|
|
15495
|
+
* with `{12000, 4000}` is ACCEPTED (so the action shape needs no
|
|
15496
|
+
* `ProvisionedThroughput` companion), while `{6000, 2000}` is rejected
|
|
15497
|
+
* with `Requested ReadUnitsPerSecond for WarmThroughput for index gsi1 is
|
|
15498
|
+
* lower than current WarmThroughput, decreasing WarmThroughput is not
|
|
15499
|
+
* supported`.
|
|
15500
|
+
*/
|
|
15501
|
+
warmThroughputOpFor(indexName, physicalId, desired, previous, live) {
|
|
15502
|
+
const requested = desired.WarmThroughput;
|
|
15503
|
+
if (previous !== void 0 && JSON.stringify(previous.WarmThroughput) === JSON.stringify(requested)) return;
|
|
15504
|
+
const sendable = this.coerceWarmThroughputForSend(`GSI ${indexName} on DynamoDB table ${physicalId}`, requested);
|
|
15505
|
+
if (sendable === void 0) {
|
|
15506
|
+
this.warnRefusedWarmThroughput(`GSI ${indexName} on DynamoDB table ${physicalId}`, requested);
|
|
15507
|
+
return;
|
|
15508
|
+
}
|
|
15509
|
+
if (warmThroughputAlreadyMatches(sendable, live?.WarmThroughput)) {
|
|
15510
|
+
this.logger.debug(`GSI ${indexName} on DynamoDB table ${physicalId} already carries the requested warm throughput in AWS; skipping its WarmThroughput Update`);
|
|
15511
|
+
return;
|
|
15512
|
+
}
|
|
15513
|
+
if (isWarmThroughputDecrease(sendable, live?.WarmThroughput)) {
|
|
15514
|
+
this.logger.warn(`GSI ${indexName} on DynamoDB table ${physicalId}: the requested WarmThroughput ${JSON.stringify(sendable)} is lower than the ${JSON.stringify({
|
|
15515
|
+
ReadUnitsPerSecond: live?.WarmThroughput?.ReadUnitsPerSecond,
|
|
15516
|
+
WriteUnitsPerSecond: live?.WarmThroughput?.WriteUnitsPerSecond
|
|
15517
|
+
})} DynamoDB currently holds for that index. Warm throughput only ever RISES with an index's traffic and cannot be lowered — AWS rejects the UpdateTable with "decreasing WarmThroughput is not supported" — so it was NOT sent and every other change on this table still applied. On a deploy this warning may be the only signal you get: a successful deploy re-captures AWS's current value as the drift baseline. If your template declares this index's WarmThroughput, set it to the value AWS holds (or remove it, which AWS treats as keeping the last-set value); if it does not, the value came from a cdkd state record and 'cdkd deploy' (or 'cdkd drift --accept') refreshes it.`);
|
|
15518
|
+
return;
|
|
15519
|
+
}
|
|
15520
|
+
return sendable;
|
|
15521
|
+
}
|
|
15522
|
+
/**
|
|
14901
15523
|
* Issue a list of `GlobalSecondaryIndexUpdates` ops, one per `UpdateTable`,
|
|
14902
15524
|
* waiting for the table AND every index to return to ACTIVE between each.
|
|
14903
15525
|
*
|
|
@@ -14947,6 +15569,33 @@ var DynamoDBTableProvider = class {
|
|
|
14947
15569
|
}
|
|
14948
15570
|
}
|
|
14949
15571
|
/**
|
|
15572
|
+
* Should this update SKIP its `WarmThroughput` call because the value would
|
|
15573
|
+
* DECREASE what AWS already holds (issue #1768)?
|
|
15574
|
+
*
|
|
15575
|
+
* Thin logging wrapper over the pure {@link isWarmThroughputDecrease}, kept
|
|
15576
|
+
* on the class so the warning is emitted at exactly the one site that skips.
|
|
15577
|
+
*
|
|
15578
|
+
* The warning is what makes the skip honest, and its WORDING is checked
|
|
15579
|
+
* against the code rather than assumed (PR review): the first version
|
|
15580
|
+
* promised `cdkd drift` would "keep reporting the difference", which is TRUE
|
|
15581
|
+
* on the `drift --revert` path — nothing is recorded, so the observed
|
|
15582
|
+
* baseline still holds the declared value — and FALSE on the DEPLOY path.
|
|
15583
|
+
* A successful deploy re-captures `observedProperties` through this same
|
|
15584
|
+
* `readCurrentState` (`deploy-engine.ts`'s `kickOffObservedCapture`), which
|
|
15585
|
+
* for a DECLARING template emits AWS's CURRENT value, so the very deploy
|
|
15586
|
+
* that warned equalises the comparison and the next `cdkd drift` reports the
|
|
15587
|
+
* resource clean. The message therefore says the warning may be the only
|
|
15588
|
+
* signal, and names both numbers so the template edit needs no second look.
|
|
15589
|
+
*/
|
|
15590
|
+
skipWarmThroughputDecrease(logicalId, physicalId, desired, live) {
|
|
15591
|
+
if (!isWarmThroughputDecrease(desired, live)) return false;
|
|
15592
|
+
this.logger.warn(`AWS::DynamoDB::Table ${logicalId}: the requested WarmThroughput ${JSON.stringify(desired)} is lower than the ${JSON.stringify({
|
|
15593
|
+
ReadUnitsPerSecond: live?.ReadUnitsPerSecond,
|
|
15594
|
+
WriteUnitsPerSecond: live?.WriteUnitsPerSecond
|
|
15595
|
+
})} DynamoDB currently holds for table ${physicalId}. Warm throughput only ever RISES with a table's traffic and cannot be lowered — AWS rejects the UpdateTable with "decreasing WarmThroughput is not supported" — so it was NOT sent and every other change on this resource still applied. On a deploy this warning may be the only signal you get: a successful deploy re-captures AWS's current value as the drift baseline, so the next 'cdkd drift' reports this table clean (a 'cdkd drift --revert' records nothing and keeps reporting it). To make cdkd and AWS agree, set WarmThroughput to the value AWS holds (or remove the property, which AWS treats as keeping the last-set value).`);
|
|
15596
|
+
return true;
|
|
15597
|
+
}
|
|
15598
|
+
/**
|
|
14950
15599
|
* `AttributeDefinitions` is a SET, keyed by `AttributeName`: CFn accepts the
|
|
14951
15600
|
* members in any order and `DescribeTable` returns them in an order matching
|
|
14952
15601
|
* neither the request nor the template. Measured live (us-east-1,
|
|
@@ -14966,6 +15615,29 @@ var DynamoDBTableProvider = class {
|
|
|
14966
15615
|
* `KeySchema` is deliberately NOT declared, at the table level or the index
|
|
14967
15616
|
* level: it is order-SIGNIFICANT (HASH before RANGE), so sorting it would
|
|
14968
15617
|
* HIDE a real key change rather than remove a phantom one.
|
|
15618
|
+
*
|
|
15619
|
+
* `GlobalSecondaryIndexes` / `LocalSecondaryIndexes` are NOT declared either,
|
|
15620
|
+
* and that is a MECHANISM limit rather than a judgement about the lists —
|
|
15621
|
+
* both really are sets keyed by `IndexName` (issue #1767 proposes declaring
|
|
15622
|
+
* them). Every entry here is a SUBTREE declaration, and unlike
|
|
15623
|
+
* `getDriftUnknownPaths` this walk DESCENDS INTO ARRAY ELEMENTS giving each
|
|
15624
|
+
* the parent's path (`drift-normalize.ts`), so a `'GlobalSecondaryIndexes'`
|
|
15625
|
+
* entry also reaches `GlobalSecondaryIndexes.KeySchema` and would sort the
|
|
15626
|
+
* per-index key schema — reversing the sentence above at the index level
|
|
15627
|
+
* only. Issue #1767 calls the sort and the member reverse-map separable;
|
|
15628
|
+
* this change ships the reverse-map, and the ordering half needed a
|
|
15629
|
+
* leaf-only form of the declaration first (issue
|
|
15630
|
+
* [#1783](https://github.com/go-to-k/cdkd/issues/1783)). **That form now
|
|
15631
|
+
* EXISTS** — `LEAF_ONLY_PATH_SUFFIX` (`[]`) landed in
|
|
15632
|
+
* [#1799](https://github.com/go-to-k/cdkd/pull/1799), so
|
|
15633
|
+
* `'GlobalSecondaryIndexes[]'` would claim the list alone without reaching
|
|
15634
|
+
* the per-index `KeySchema`. It is deliberately NOT declared here yet:
|
|
15635
|
+
* adopting it is its own change with its own real-AWS verification, tracked
|
|
15636
|
+
* as issue [#1812](https://github.com/go-to-k/cdkd/issues/1812). Consequence
|
|
15637
|
+
* until then, stated so it is not mistaken for solved: an index list AWS
|
|
15638
|
+
* returns in a different ORDER than the template declared is still phantom
|
|
15639
|
+
* drift against a `properties` baseline. It is not reachable on the ordinary
|
|
15640
|
+
* `observedProperties` path, where both sides come from this same readback.
|
|
14969
15641
|
*/
|
|
14970
15642
|
getDriftUnorderedPaths(resourceType) {
|
|
14971
15643
|
if (resourceType !== "AWS::DynamoDB::Table") return [];
|
|
@@ -15009,11 +15681,72 @@ var DynamoDBTableProvider = class {
|
|
|
15009
15681
|
* `TlsConfig` (issue #1602): an absent / empty bag falls back to the
|
|
15010
15682
|
* type-level answer of COMPARING, since hiding a real drift is the worse
|
|
15011
15683
|
* failure.
|
|
15684
|
+
*
|
|
15685
|
+
* `GlobalSecondaryIndexes` / `LocalSecondaryIndexes` join it under the SAME
|
|
15686
|
+
* rule for the SAME reason (issue #1767), one nesting level up: the readback
|
|
15687
|
+
* now emits the reverse-mapped CFn shape, so every `observedProperties`
|
|
15688
|
+
* baseline an earlier binary wrote — which carries `IndexStatus` /
|
|
15689
|
+
* `ItemCount` / `IndexArn` / the on-demand `{0, 0}` placeholder / the
|
|
15690
|
+
* computed per-index `WarmThroughput` — no longer equals it. Ignoring the
|
|
15691
|
+
* path on both sides when the template declares NO index list covers the
|
|
15692
|
+
* transition and the steady state for that population, exactly as the
|
|
15693
|
+
* `WarmThroughput` arm does: an index list the template never declared is
|
|
15694
|
+
* AWS-authored (created out of band, or by a sibling), which is the #1498
|
|
15695
|
+
* class.
|
|
15696
|
+
*
|
|
15697
|
+
* Known residual, deliberately NOT covered, because covering it costs more
|
|
15698
|
+
* than it buys: a table whose template DOES declare indexes keeps comparing,
|
|
15699
|
+
* so its already-written observed baseline reports a one-sided
|
|
15700
|
+
* `GlobalSecondaryIndexes` difference until the next deploy re-captures it
|
|
15701
|
+
* (or `cdkd drift --accept` does). `cdkd drift --revert` is the third thing a
|
|
15702
|
+
* user may reach for on that report, and for the shapes measured here it
|
|
15703
|
+
* applies NOTHING to AWS — which is a property of three separate skips, not
|
|
15704
|
+
* "by construction", and the distinction is load-bearing because this PR
|
|
15705
|
+
* briefly broke it. The index names match, so no Create / Delete is derived;
|
|
15706
|
+
* the stale blob's `{0, 0}` capacity is refused by
|
|
15707
|
+
* {@link skipZeroCapacityIndexUpdate}; and its per-index `WarmThroughput` —
|
|
15708
|
+
* which the same PR taught `applyGsiUpdates` to SEND (issue #1768), turning
|
|
15709
|
+
* an earlier "by construction" wording false the moment it landed — is
|
|
15710
|
+
* refused by {@link warmThroughputAlreadyMatches}, since a stale blob carries
|
|
15711
|
+
* exactly what AWS holds. Remove any of the three and the residual stops
|
|
15712
|
+
* being harmless: the revert starts issuing one redundant `UpdateTable` plus
|
|
15713
|
+
* a full index-ACTIVE wait per GSI, or a doomed one.
|
|
15714
|
+
*
|
|
15715
|
+
* "Applies nothing" is NOT the same as "reaches those skips" (PR review round
|
|
15716
|
+
* 5). `applyGsiUpdates` compares `KeySchema` / `Projection` between the
|
|
15717
|
+
* recorded previous and the desired entry BEFORE any of them and THROWS on a
|
|
15718
|
+
* difference, because DynamoDB cannot modify either in place. On this path
|
|
15719
|
+
* that compare is a trimmed readback against a stale full description, and
|
|
15720
|
+
* both members survive the #1767 reverse-map unchanged — `KeySchema`
|
|
15721
|
+
* verbatim, `Projection` rebuilt member-for-member — so for every shape
|
|
15722
|
+
* exercised here it matches and the throw does not fire. It is named because
|
|
15723
|
+
* it is the outcome an enumeration of skips would otherwise hide: a stale
|
|
15724
|
+
* blob whose `Projection` AWS has since re-normalized would fail the revert
|
|
15725
|
+
* loudly rather than no-op it. Suppressing the residual instead would mean ignoring the
|
|
15726
|
+
* whole array for the population that HAS indexes — i.e. never detecting an
|
|
15727
|
+
* out-of-band index add / remove / capacity change again, permanently, to
|
|
15728
|
+
* remove a one-time report. No PATH can express the middle ground:
|
|
15729
|
+
* `isIgnoredPath` is never asked about a path that crosses an array
|
|
15730
|
+
* (`diffAt` compares arrays wholesale via `deepEqual`), so a per-MEMBER
|
|
15731
|
+
* ignore path does not exist — the same wall issue #1742 records for the
|
|
15732
|
+
* `AWS::DynamoDB::GlobalTable` twin. **A non-path seam now EXISTS**:
|
|
15733
|
+
* `ResourceProvider.canonicalizeDriftProperties`, applied to BOTH comparison
|
|
15734
|
+
* sides, landed in [#1799](https://github.com/go-to-k/cdkd/pull/1799) (which
|
|
15735
|
+
* closed issue [#1784](https://github.com/go-to-k/cdkd/issues/1784)) and can
|
|
15736
|
+
* strip the AWS-managed member from each bag — converging an already-written
|
|
15737
|
+
* record with a post-fix readback, with no ignore-path and no lost
|
|
15738
|
+
* detection. It is deliberately NOT adopted here yet: doing so is its own
|
|
15739
|
+
* change with its own real-AWS verification, tracked as issue
|
|
15740
|
+
* [#1812](https://github.com/go-to-k/cdkd/issues/1812).
|
|
15012
15741
|
*/
|
|
15013
15742
|
getDriftUnknownPaths(resourceType, properties) {
|
|
15014
15743
|
if (resourceType !== "AWS::DynamoDB::Table") return [];
|
|
15015
|
-
|
|
15016
|
-
|
|
15744
|
+
const paths = [];
|
|
15745
|
+
if (!declaresWarmThroughput(properties)) paths.push("WarmThroughput");
|
|
15746
|
+
if (desiredBagIsInformative(properties)) {
|
|
15747
|
+
for (const key of ["GlobalSecondaryIndexes", "LocalSecondaryIndexes"]) if (properties !== void 0 && !declaresIndexList(properties[key])) paths.push(key);
|
|
15748
|
+
}
|
|
15749
|
+
return paths;
|
|
15017
15750
|
}
|
|
15018
15751
|
/**
|
|
15019
15752
|
* Read the AWS-current DynamoDB table configuration in CFn-property shape.
|
|
@@ -15027,18 +15760,18 @@ var DynamoDBTableProvider = class {
|
|
|
15027
15760
|
* - StreamSpecification's CFn shape includes only `StreamViewType`; the
|
|
15028
15761
|
* API response carries `StreamEnabled` too. We surface both since the
|
|
15029
15762
|
* drift comparator only descends into keys present in state.
|
|
15030
|
-
* - GSI / LSI in the API response include `IndexStatus`, `
|
|
15031
|
-
* `IndexArn` and sizing fields cdkd never sets
|
|
15032
|
-
* VERBATIM
|
|
15033
|
-
* NOT, and issue #1767 quotes that sentence as the bug:
|
|
15034
|
-
* `calculateResourceDrift`'s state-keys-only walk, which
|
|
15035
|
-
* absent TOP-LEVEL key. These are members of an array
|
|
15036
|
-
* baseline DOES carry, and arrays compare positionally,
|
|
15037
|
-
* AWS-managed member
|
|
15038
|
-
* an in-use table with
|
|
15039
|
-
* `observedProperties` on the
|
|
15040
|
-
* reverse-
|
|
15041
|
-
*
|
|
15763
|
+
* - GSI / LSI in the API response include `IndexStatus`, `Backfilling`,
|
|
15764
|
+
* `ItemCount`, `IndexArn` and sizing fields cdkd never sets. They used to
|
|
15765
|
+
* be forwarded VERBATIM under a comment claiming "the comparator filters
|
|
15766
|
+
* them" — it does NOT, and issue #1767 quotes that sentence as the bug:
|
|
15767
|
+
* the filter is `calculateResourceDrift`'s state-keys-only walk, which
|
|
15768
|
+
* only reaches an absent TOP-LEVEL key. These are members of an array
|
|
15769
|
+
* under a key the baseline DOES carry, and arrays compare positionally,
|
|
15770
|
+
* so every AWS-managed member participated — and `ItemCount` MOVES as
|
|
15771
|
+
* data lands, so an in-use table with any index drifted against its own
|
|
15772
|
+
* `observedProperties` on the ORDINARY path, on a schedule set by its own
|
|
15773
|
+
* write traffic. Each entry is now reverse-mapped to its CFn shape by
|
|
15774
|
+
* {@link reverseMapSecondaryIndex}.
|
|
15042
15775
|
*
|
|
15043
15776
|
* Returns `undefined` when the table is gone (`ResourceNotFoundException`).
|
|
15044
15777
|
*
|
|
@@ -15050,8 +15783,10 @@ var DynamoDBTableProvider = class {
|
|
|
15050
15783
|
* template carries them).
|
|
15051
15784
|
*
|
|
15052
15785
|
* `properties` is the DESIRED bag (the state-recorded template intent every
|
|
15053
|
-
* caller passes)
|
|
15054
|
-
*
|
|
15786
|
+
* caller passes). It is consulted for the table-level `WarmThroughput` — see
|
|
15787
|
+
* {@link getDriftUnknownPaths} for the measurement and the reasoning — and,
|
|
15788
|
+
* since issue #1767, for the per-index throughput blocks of each secondary
|
|
15789
|
+
* index, matched by `IndexName`.
|
|
15055
15790
|
*/
|
|
15056
15791
|
async readCurrentState(physicalId, _logicalId, _resourceType, properties) {
|
|
15057
15792
|
try {
|
|
@@ -15082,8 +15817,15 @@ var DynamoDBTableProvider = class {
|
|
|
15082
15817
|
StreamEnabled: true,
|
|
15083
15818
|
StreamViewType: table.StreamSpecification.StreamViewType
|
|
15084
15819
|
};
|
|
15085
|
-
|
|
15086
|
-
if (table.
|
|
15820
|
+
const bagInformative = desiredBagIsInformative(properties);
|
|
15821
|
+
if (table.GlobalSecondaryIndexes && table.GlobalSecondaryIndexes.length > 0) {
|
|
15822
|
+
const desiredByName = desiredIndexEntriesByName(properties?.["GlobalSecondaryIndexes"]);
|
|
15823
|
+
result["GlobalSecondaryIndexes"] = table.GlobalSecondaryIndexes.map((live) => reverseMapSecondaryIndex(live, live.IndexName === void 0 ? void 0 : desiredByName.get(live.IndexName), bagInformative));
|
|
15824
|
+
}
|
|
15825
|
+
if (table.LocalSecondaryIndexes && table.LocalSecondaryIndexes.length > 0) {
|
|
15826
|
+
const desiredByName = desiredIndexEntriesByName(properties?.["LocalSecondaryIndexes"]);
|
|
15827
|
+
result["LocalSecondaryIndexes"] = table.LocalSecondaryIndexes.map((live) => reverseMapSecondaryIndex(live, live.IndexName === void 0 ? void 0 : desiredByName.get(live.IndexName), bagInformative));
|
|
15828
|
+
}
|
|
15087
15829
|
if (table.SSEDescription?.Status === "ENABLED") {
|
|
15088
15830
|
const sse = { SSEEnabled: true };
|
|
15089
15831
|
if (table.SSEDescription.KMSMasterKeyArn !== void 0) sse["KMSMasterKeyId"] = table.SSEDescription.KMSMasterKeyArn;
|
|
@@ -15182,6 +15924,23 @@ var DynamoDBTableProvider = class {
|
|
|
15182
15924
|
//#endregion
|
|
15183
15925
|
//#region src/provisioning/providers/dynamodb-globaltable-provider.ts
|
|
15184
15926
|
/**
|
|
15927
|
+
* The CLOSED path table {@link DynamoDBGlobalTableProvider.canonicalizeDriftProperties}
|
|
15928
|
+
* strips from BOTH drift comparison sides (issue #1742).
|
|
15929
|
+
*
|
|
15930
|
+
* One entry today: the AWS-computed `GlobalSecondaryIndexes[].WarmThroughput`.
|
|
15931
|
+
* A table rather than an inline check so a second per-array-element member
|
|
15932
|
+
* lands as a row instead of a second walk — and so the scope is READABLE as a
|
|
15933
|
+
* list of paths, which is what "closed table" means here. Deliberately does
|
|
15934
|
+
* NOT include `LocalSecondaryIndexes` (this provider's readback assigns the
|
|
15935
|
+
* raw SDK descriptions there, a wider divergence than one member) or
|
|
15936
|
+
* `Replicas[].GlobalSecondaryIndexes` (whose entries carry only the read-half
|
|
15937
|
+
* throughput blocks — no `WarmThroughput` is emitted or accepted there).
|
|
15938
|
+
*/
|
|
15939
|
+
const DRIFT_STRIPPED_INDEX_MEMBERS = [{
|
|
15940
|
+
listKey: "GlobalSecondaryIndexes",
|
|
15941
|
+
member: "WarmThroughput"
|
|
15942
|
+
}];
|
|
15943
|
+
/**
|
|
15185
15944
|
* AWS DynamoDB GlobalTable Provider
|
|
15186
15945
|
*
|
|
15187
15946
|
* Implements resource provisioning for AWS::DynamoDB::GlobalTable using the
|
|
@@ -16635,7 +17394,12 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
16635
17394
|
const entry = { IndexName: gsi.IndexName };
|
|
16636
17395
|
if (gsi.KeySchema) entry["KeySchema"] = gsi.KeySchema;
|
|
16637
17396
|
if (gsi.Projection) entry["Projection"] = gsi.Projection;
|
|
16638
|
-
if (gsi.WarmThroughput)
|
|
17397
|
+
if (gsi.WarmThroughput) {
|
|
17398
|
+
const warm = {};
|
|
17399
|
+
if (gsi.WarmThroughput.ReadUnitsPerSecond !== void 0) warm["ReadUnitsPerSecond"] = gsi.WarmThroughput.ReadUnitsPerSecond;
|
|
17400
|
+
if (gsi.WarmThroughput.WriteUnitsPerSecond !== void 0) warm["WriteUnitsPerSecond"] = gsi.WarmThroughput.WriteUnitsPerSecond;
|
|
17401
|
+
if (Object.keys(warm).length > 0) entry["WarmThroughput"] = warm;
|
|
17402
|
+
}
|
|
16639
17403
|
const replicaEntry = { IndexName: gsi.IndexName };
|
|
16640
17404
|
if (billingMode === "PROVISIONED") {
|
|
16641
17405
|
const writeAutoScaling = await this.readAutoScalingSettings(tableNameForSubs, "dynamodb:index:WriteCapacityUnits", void 0, gsi.IndexName);
|
|
@@ -16888,6 +17652,115 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
16888
17652
|
return ["AttributeDefinitions"];
|
|
16889
17653
|
}
|
|
16890
17654
|
/**
|
|
17655
|
+
* Strip the AWS-computed per-index `WarmThroughput` from BOTH drift
|
|
17656
|
+
* comparison sides (issue #1742 defect 2, on the #1784 seam).
|
|
17657
|
+
*
|
|
17658
|
+
* `DescribeTable` reports a `WarmThroughput` for EVERY index whether or not
|
|
17659
|
+
* the template asked for one — a default `{ReadUnitsPerSecond: 12000,
|
|
17660
|
+
* WriteUnitsPerSecond: 4000, Status: 'ACTIVE'}`, measured us-east-1
|
|
17661
|
+
* 2026-08-13 on the `rollback-replay-effective-props` fixture — so on a
|
|
17662
|
+
* `properties`-only baseline (what a reverse-replacement rollback leaves
|
|
17663
|
+
* behind, since `rollback-executor.ts` strips `observedProperties`) the
|
|
17664
|
+
* member is a permanent one-sided difference: `cdkd drift` reports the table
|
|
17665
|
+
* forever and `--revert` re-issues calls for it.
|
|
17666
|
+
*
|
|
17667
|
+
* Why this mechanism and not the two cheaper ones:
|
|
17668
|
+
*
|
|
17669
|
+
* - `getDriftUnknownPaths` (the #1760 answer for the sibling type's
|
|
17670
|
+
* TOP-LEVEL member) cannot express it. `calculateResourceDrift` compares
|
|
17671
|
+
* arrays wholesale, so `isIgnoredPath` is never asked about a path that
|
|
17672
|
+
* crosses one; the only expressible suppression is the WHOLE
|
|
17673
|
+
* `GlobalSecondaryIndexes` subtree, i.e. never detecting an index add /
|
|
17674
|
+
* remove / capacity change again — the #1420 stopgap already refused.
|
|
17675
|
+
* - Gating the READBACK emission on the desired side (this issue's own
|
|
17676
|
+
* second candidate answer) removes the phantom for the `properties`
|
|
17677
|
+
* baseline and CREATES one for the far larger `observedProperties`
|
|
17678
|
+
* population: every bag already in S3 was written by a binary that emitted
|
|
17679
|
+
* the computed member, so the first `cdkd drift` after upgrading compares
|
|
17680
|
+
* a baseline that HAS it against a readback that does not, and reports the
|
|
17681
|
+
* whole array on an untouched table. It does not self-heal — the observed
|
|
17682
|
+
* capture only runs on CREATE / UPDATE. That attempt was implemented,
|
|
17683
|
+
* reviewed and REVERTED; do not re-propose it.
|
|
17684
|
+
*
|
|
17685
|
+
* Stripping from BOTH sides is what converges the two populations at once: a
|
|
17686
|
+
* stale observed record and a fresh readback both lose the member, and a
|
|
17687
|
+
* `properties` baseline that never had it now faces an AWS side without it.
|
|
17688
|
+
*
|
|
17689
|
+
* ACCEPTED COST, stated because it is a real loss: a template that DECLARES
|
|
17690
|
+
* a per-index `WarmThroughput` no longer has changes to it REPORTED by
|
|
17691
|
+
* `cdkd drift`. The value is still SENT (`toSdkGlobalSecondaryIndexes`
|
|
17692
|
+
* forwards it on create and on a new index), so this is a detection gap, not
|
|
17693
|
+
* a delivery one, and it is bounded by what cdkd could do about a difference
|
|
17694
|
+
* anyway: warm throughput only ever GROWS on the AWS side and cannot be
|
|
17695
|
+
* lowered, so `--revert` would issue a decrease AWS rejects — the residual
|
|
17696
|
+
* issue #1768 records for the sibling type's declared arm. A per-index
|
|
17697
|
+
* declared-gate is not expressible here in any case: this hook sees ONE bag
|
|
17698
|
+
* with no `side` argument and no reference to the desired side, which is the
|
|
17699
|
+
* symmetry the #1784 contract requires and the reason one-sided
|
|
17700
|
+
* normalization is refused.
|
|
17701
|
+
*
|
|
17702
|
+
* Scope is a CLOSED table rather than a walk for any key named
|
|
17703
|
+
* `WarmThroughput`: `Replicas[].GlobalSecondaryIndexes[]` entries carry only
|
|
17704
|
+
* the read-half throughput blocks (no `WarmThroughput` is emitted or
|
|
17705
|
+
* accepted there), and a blanket strip would remove a member from a path
|
|
17706
|
+
* nobody measured. The table is a FLAT list of `{listKey, member}` pairs —
|
|
17707
|
+
* the type is hardcoded one line below, not a key of it — and nothing
|
|
17708
|
+
* mechanically fences it against `readCurrentState`: a future per-replica
|
|
17709
|
+
* `WarmThroughput` emission would desync silently, so a member added to the
|
|
17710
|
+
* readback needs a row added here in the same change.
|
|
17711
|
+
*
|
|
17712
|
+
* The `resourceType` guard is kept but is UNREACHABLE, and the earlier
|
|
17713
|
+
* claim that it followed the #1784 CC-API-fallback caveat was WRONG — that
|
|
17714
|
+
* caveat is about the BAG SHAPE when a type has no `readCurrentState`, and
|
|
17715
|
+
* never produces a foreign `resourceType`. `drift.ts` resolves the provider
|
|
17716
|
+
* by `(resourceType, provisionedBy)` and then passes that SAME type back in,
|
|
17717
|
+
* and this provider is registered for exactly one type, so the `!==` arm
|
|
17718
|
+
* cannot fire. It stays as a cheap shape guard for a future second
|
|
17719
|
+
* registration, not because anything routes a foreign type here.
|
|
17720
|
+
*
|
|
17721
|
+
* UNCOVERED POPULATION, stated because the fix does NOT reach it: a
|
|
17722
|
+
* cc-api-routed GlobalTable gets neither half. Top-level `WarmThroughput` is
|
|
17723
|
+
* a `silentDrop` property for this type, so a template declaring it
|
|
17724
|
+
* auto-routes the whole resource through Cloud Control (the #614 routing)
|
|
17725
|
+
* and the route is STICKY — `drift.ts` then resolves `provider` to
|
|
17726
|
+
* `CloudControlProvider`, this hook is never invoked, and the AWS side comes
|
|
17727
|
+
* from CC's `GetResource`. So the `properties`-only-baseline phantom drift
|
|
17728
|
+
* this closes survives there. Pre-existing, not introduced here.
|
|
17729
|
+
*
|
|
17730
|
+
* Note the trigger is the TOP-LEVEL `WarmThroughput`, NOT the per-index
|
|
17731
|
+
* member this strips: a template declaring only the per-index member keeps
|
|
17732
|
+
* `GlobalSecondaryIndexes` (a handled property), stays SDK-routed, and IS
|
|
17733
|
+
* covered. Do not read this bound as "warm-throughput templates are
|
|
17734
|
+
* uncovered" — an earlier draft of this comment said so and was wrong.
|
|
17735
|
+
*
|
|
17736
|
+
* Pure, non-mutating, and identity-returning when nothing applies: the input
|
|
17737
|
+
* bag is the caller's state record / readback, an unaffected table pays
|
|
17738
|
+
* nothing, and only the index entries that actually carry the member are
|
|
17739
|
+
* rebuilt.
|
|
17740
|
+
*/
|
|
17741
|
+
canonicalizeDriftProperties(resourceType, properties) {
|
|
17742
|
+
if (resourceType !== "AWS::DynamoDB::GlobalTable") return properties;
|
|
17743
|
+
let result = properties;
|
|
17744
|
+
for (const { listKey, member } of DRIFT_STRIPPED_INDEX_MEMBERS) {
|
|
17745
|
+
const list = result[listKey];
|
|
17746
|
+
if (!Array.isArray(list)) continue;
|
|
17747
|
+
let stripped = false;
|
|
17748
|
+
const canonical = list.map((element) => {
|
|
17749
|
+
const entry = asRecord$1(element);
|
|
17750
|
+
if (!entry || !(member in entry)) return element;
|
|
17751
|
+
stripped = true;
|
|
17752
|
+
const copy = { ...entry };
|
|
17753
|
+
delete copy[member];
|
|
17754
|
+
return copy;
|
|
17755
|
+
});
|
|
17756
|
+
if (stripped) result = {
|
|
17757
|
+
...result,
|
|
17758
|
+
[listKey]: canonical
|
|
17759
|
+
};
|
|
17760
|
+
}
|
|
17761
|
+
return result;
|
|
17762
|
+
}
|
|
17763
|
+
/**
|
|
16891
17764
|
* Adopt an existing DynamoDB GlobalTable into cdkd state.
|
|
16892
17765
|
*
|
|
16893
17766
|
* Lookup order:
|
|
@@ -77189,7 +78062,7 @@ function createMigrateCommand() {
|
|
|
77189
78062
|
*/
|
|
77190
78063
|
function buildProgram() {
|
|
77191
78064
|
const program = new Command();
|
|
77192
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.
|
|
78065
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.10");
|
|
77193
78066
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
77194
78067
|
const { profile } = actionCommand.optsWithGlobals();
|
|
77195
78068
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|