@go-to-k/cdkd 0.278.18 → 0.278.20
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-DHs3PmZU.js → asg-provider-OB2CvUwn.js} +2 -2
- package/dist/{asg-provider-DHs3PmZU.js.map → asg-provider-OB2CvUwn.js.map} +1 -1
- package/dist/cli.js +451 -56
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-D9TPJSZY.js → deploy-engine-Bv3IEbrU.js} +4 -4
- package/dist/{deploy-engine-D9TPJSZY.js.map → deploy-engine-Bv3IEbrU.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
|
|
2
|
+
import { $ as isThrottlingError, $t as setAwsClients, A as red, At as getLegacyStateBucketName, B as isTerminationProtectionPropagationError, Bt as CFN_TEMPLATE_BODY_LIMIT, C as isStatefulRecreateTargetSync, Ct as runDockerForeground, D as cyan, Dt as Synthesizer, E as bold, Et as getDockerImageBySourceHash, F as ProviderRegistry, Ft as resolveStateBucketWithDefault, G as normalizeAwsTagsToCfn, Gt as expectedOwnerParam, H as cfnRefValueFromPhysicalId, Ht as MIGRATE_TMP_PREFIX, I as findActionableSilentDrops, It as resolveStateBucketWithDefaultAndSource, J as applyRoleArnIfSet, K as resolveExplicitPhysicalId, Kt as AssemblyReader, L as CloudControlProvider, Lt as resolveUseCdkBootstrapAssets, M as IAMRoleProvider, Mt as resolveAutoAssetStorage, N as collectInlinePolicyNamesManagedBySiblings, Nt as resolveCaptureObservedState, O as gray, Ot as synthesisStatusMessage, P as clearOnUpdateRemoval, Pt as resolveSkipPrefix, Q as isRetryableTransientError, Qt as resetAwsClients, R as slowCcOperationTimeoutMs, Rt as stateBucketExistenceConfirmed, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, Sn as withErrorHandling, St as getDockerCmd, T as formatResourceLine, Tt as AssetManifestLoader, U as refStateLookupFromResource, Ut as findLargeInlineResources, V as IntrinsicFunctionResolver, Vt as CFN_TEMPLATE_URL_LIMIT, W as WAFv2WebACLProvider, Wt as uploadCfnTemplate, X as describeTypeWithThrottleRetry, Xt as AwsClients, Y as DiffCalculator, Yt as resolveBucketRegion, Z as withRetry, Zt as getAwsClients, _ as createPreDeleteFinalSnapshot, _t as parseBootstrapMarker, a as DeploymentEventsStore, an as LocalInvokeBuildError$1, at as shouldRetainResource, b as unsupportedFinalSnapshotError, bt as buildDockerImage, c as replayFailedOperations, ct as WorkGraph, d as IMPLICIT_DELETE_DEPENDENCIES, dn as PartialFailureError, dt as loadPublishableAssetManifest, et as DagBuilder, f as computeImplicitDeleteEdges, fn as ProvisioningError, ft as rewriteTemplateAssetReferences, g as ccRoutedFinalSnapshotError, gn as StackTerminationProtectionError, gt as getBootstrapMarkerKey, h as buildFinalSnapshotIdentifier, hn as StackHasActiveImportsError, ht as ensureAssetStorage, i as DeploymentEventsReader, in as DeployCancelledError, it as rebuildClientForBucketRegion, j as yellow, jt as resolveApp, k as green, kt as getDefaultStateBucketName, l as replayRollback, ln as MissingCdkCliError, lt as buildAssetRedirectMap, m as PRE_DELETE_SNAPSHOT_TYPES, mn as ResourceUpdateNotSupportedError, mt as BOOTSTRAP_MARKER_PREFIX, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as LockManager, o as planFailedOps, on as LocalMigrateError, ot as AssetPublisher, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as ResourceTimeoutError, pt as AssetModeResolver, q as assertRegionMatch, qt as processStackMessages, r as DeployEngine, rt as S3StateBackend, s as planRollback, sn as LocalStartServiceError, st as stringifyValue, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as CdkdError, tt as TemplateParser, u as withResourceDeadline, un as NestedStackChildDirectDestroyError, ut as createAssetRedirectResolver, v as isFinalSnapshotError, vt as validateAssetBucketName, w as renderStatefulReason, wt as runDockerStreaming, x as extractDeploymentEventError, xn as normalizeAwsError, xt as formatDockerLoginError, y as refusesFinalSnapshot, yt as validateContainerRepoName, z as disableInstanceApiTermination, zt as warnDeprecatedNoPrefixCliFlag } from "./deploy-engine-Bv3IEbrU.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BYMEE-BS.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-OB2CvUwn.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, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, 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";
|
|
@@ -12269,6 +12269,11 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12269
12269
|
}
|
|
12270
12270
|
}));
|
|
12271
12271
|
}
|
|
12272
|
+
if (billingMode === "PROVISIONED") try {
|
|
12273
|
+
await this.reconcileAutoScalingTargets(tableName, [], collectAutoScalingTargets(properties, currentRegion), currentRegion);
|
|
12274
|
+
} catch (autoScalingErr) {
|
|
12275
|
+
this.logger.warn(`Auto-scaling registration failed for ${tableName}: ${autoScalingErr instanceof Error ? autoScalingErr.message : String(autoScalingErr)}. The table was created successfully; re-run the deploy to register the scaling targets.`);
|
|
12276
|
+
}
|
|
12272
12277
|
this.logger.debug(`Successfully created DynamoDB GlobalTable ${logicalId}: ${tableName}`);
|
|
12273
12278
|
return {
|
|
12274
12279
|
physicalId: tableName,
|
|
@@ -12442,12 +12447,12 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12442
12447
|
BillingMode: newBilling
|
|
12443
12448
|
};
|
|
12444
12449
|
if (newBilling === "PROVISIONED") {
|
|
12445
|
-
billingUpdate.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion);
|
|
12446
|
-
const provisionedIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling);
|
|
12450
|
+
billingUpdate.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion, "seed");
|
|
12451
|
+
const provisionedIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "seed");
|
|
12447
12452
|
const previousCfnIndexes = previousProperties["GlobalSecondaryIndexes"];
|
|
12448
12453
|
const existingIndexNames = new Set((Array.isArray(previousCfnIndexes) ? previousCfnIndexes : []).map((entry) => entry?.["IndexName"]).filter((name) => typeof name === "string"));
|
|
12449
12454
|
const previousByIndexName = /* @__PURE__ */ new Map();
|
|
12450
|
-
for (const gsi of toSdkGlobalSecondaryIndexes(previousProperties, currentRegion, newBilling)) if (gsi.IndexName) previousByIndexName.set(gsi.IndexName, gsi);
|
|
12455
|
+
for (const gsi of toSdkGlobalSecondaryIndexes(previousProperties, currentRegion, newBilling, "seed")) if (gsi.IndexName) previousByIndexName.set(gsi.IndexName, gsi);
|
|
12451
12456
|
const byIndexName = new Map(previousByIndexName);
|
|
12452
12457
|
const survivingIndexNames = /* @__PURE__ */ new Set();
|
|
12453
12458
|
for (const gsi of provisionedIndexes) {
|
|
@@ -12476,7 +12481,14 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12476
12481
|
const writeAutoScalingOld = (previousProperties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
|
|
12477
12482
|
const writeAutoScalingNew = (properties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
|
|
12478
12483
|
const effectiveNewAutoScaling = oldBilling === "PROVISIONED" && newBilling === "PAY_PER_REQUEST" ? void 0 : writeAutoScalingNew;
|
|
12479
|
-
|
|
12484
|
+
const autoScalingApplied = /* @__PURE__ */ new Set();
|
|
12485
|
+
if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(writeAutoScalingOld, effectiveNewAutoScaling)) {
|
|
12486
|
+
await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", writeAutoScalingOld, effectiveNewAutoScaling);
|
|
12487
|
+
autoScalingApplied.add(autoScalingTargetKey({
|
|
12488
|
+
dimension: "dynamodb:table:WriteCapacityUnits",
|
|
12489
|
+
region: currentRegion
|
|
12490
|
+
}));
|
|
12491
|
+
}
|
|
12480
12492
|
const replicaDiff = diffReplicas(previousProperties["Replicas"] ?? [], properties["Replicas"] ?? []);
|
|
12481
12493
|
for (const replica of replicaDiff.removed) {
|
|
12482
12494
|
const region = replica["Region"];
|
|
@@ -12485,6 +12497,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12485
12497
|
if (removedReadAutoScaling) {
|
|
12486
12498
|
const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
|
|
12487
12499
|
await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", removedReadAutoScaling, void 0, regionalAutoScalingClient);
|
|
12500
|
+
autoScalingApplied.add(autoScalingTargetKey({
|
|
12501
|
+
dimension: "dynamodb:table:ReadCapacityUnits",
|
|
12502
|
+
region
|
|
12503
|
+
}));
|
|
12488
12504
|
}
|
|
12489
12505
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
12490
12506
|
TableName: physicalId,
|
|
@@ -12502,6 +12518,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12502
12518
|
if (newBilling === "PROVISIONED" && newReadAutoScaling) {
|
|
12503
12519
|
const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
|
|
12504
12520
|
await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", void 0, newReadAutoScaling, regionalAutoScalingClient);
|
|
12521
|
+
autoScalingApplied.add(autoScalingTargetKey({
|
|
12522
|
+
dimension: "dynamodb:table:ReadCapacityUnits",
|
|
12523
|
+
region
|
|
12524
|
+
}));
|
|
12505
12525
|
}
|
|
12506
12526
|
}
|
|
12507
12527
|
for (const replica of replicaDiff.modified) {
|
|
@@ -12517,6 +12537,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12517
12537
|
if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(oldReadAutoScaling, effectiveNewReadAutoScaling)) {
|
|
12518
12538
|
const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
|
|
12519
12539
|
await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", oldReadAutoScaling, effectiveNewReadAutoScaling, regionalAutoScalingClient);
|
|
12540
|
+
autoScalingApplied.add(autoScalingTargetKey({
|
|
12541
|
+
dimension: "dynamodb:table:ReadCapacityUnits",
|
|
12542
|
+
region
|
|
12543
|
+
}));
|
|
12520
12544
|
}
|
|
12521
12545
|
const updateAction = { RegionName: region };
|
|
12522
12546
|
if (replica["KMSMasterKeyId"] !== void 0) updateAction.KMSMasterKeyId = replica["KMSMasterKeyId"];
|
|
@@ -12535,6 +12559,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12535
12559
|
}
|
|
12536
12560
|
const previousSdkIndexes = toSdkGlobalSecondaryIndexes(previousProperties, currentRegion, oldBilling);
|
|
12537
12561
|
const gsiDiff = diffGlobalSecondaryIndexes(previousSdkIndexes, toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling));
|
|
12562
|
+
const rawOnDemandDeclarations = collectRawOnDemandDeclarations(properties, currentRegion);
|
|
12538
12563
|
const previousSdkByName = /* @__PURE__ */ new Map();
|
|
12539
12564
|
for (const prev of previousSdkIndexes) if (prev.IndexName) previousSdkByName.set(prev.IndexName, prev);
|
|
12540
12565
|
for (const gsi of gsiDiff.removed) {
|
|
@@ -12564,22 +12589,27 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12564
12589
|
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
12565
12590
|
}
|
|
12566
12591
|
for (const gsi of gsiDiff.modified) {
|
|
12567
|
-
if (
|
|
12592
|
+
if (typeof gsi.IndexName !== "string" || gsi.IndexName.length === 0) continue;
|
|
12568
12593
|
if (gsiHandledByBillingFlip.has(gsi.IndexName)) continue;
|
|
12569
12594
|
const billingFlipped = oldBilling !== newBilling;
|
|
12570
12595
|
const update = { IndexName: gsi.IndexName };
|
|
12571
12596
|
const previousOnDemand = previousSdkByName.get(gsi.IndexName)?.OnDemandThroughput;
|
|
12597
|
+
const declared = rawOnDemandDeclarations.get(gsi.IndexName);
|
|
12572
12598
|
const onDemand = { ...gsi.OnDemandThroughput };
|
|
12599
|
+
const unresolvedMembers = [];
|
|
12573
12600
|
if (!billingFlipped && previousOnDemand) {
|
|
12574
|
-
if (previousOnDemand.MaxReadRequestUnits !== void 0 && onDemand.MaxReadRequestUnits === void 0)
|
|
12575
|
-
|
|
12601
|
+
if (previousOnDemand.MaxReadRequestUnits !== void 0 && onDemand.MaxReadRequestUnits === void 0) if (declared?.readDeclared) unresolvedMembers.push("MaxReadRequestUnits");
|
|
12602
|
+
else onDemand.MaxReadRequestUnits = ON_DEMAND_LIMIT_RESET;
|
|
12603
|
+
if (previousOnDemand.MaxWriteRequestUnits !== void 0 && onDemand.MaxWriteRequestUnits === void 0) if (declared?.writeDeclared) unresolvedMembers.push("MaxWriteRequestUnits");
|
|
12604
|
+
else onDemand.MaxWriteRequestUnits = ON_DEMAND_LIMIT_RESET;
|
|
12576
12605
|
}
|
|
12606
|
+
if (unresolvedMembers.length > 0) this.logger.warn(`GSI '${gsi.IndexName}' on ${physicalId}: the template declares ${unresolvedMembers.join(" / ")} but the value did not resolve to a number, so the on-demand limit was left UNCHANGED on AWS rather than applied or cleared. This is usually an unresolved intrinsic in the template; resolve it to a literal to change the ceiling, or remove the property entirely to clear it.`);
|
|
12577
12607
|
if (Object.keys(onDemand).length > 0) update.OnDemandThroughput = onDemand;
|
|
12578
12608
|
if (!billingFlipped && gsi.ProvisionedThroughput) update.ProvisionedThroughput = gsi.ProvisionedThroughput;
|
|
12579
12609
|
const previousSdk = previousSdkByName.get(gsi.IndexName);
|
|
12580
12610
|
if (gsi.WarmThroughput !== void 0 && !deepEqual$1(gsi.WarmThroughput, previousSdk?.WarmThroughput)) update.WarmThroughput = gsi.WarmThroughput;
|
|
12581
12611
|
if (!update.ProvisionedThroughput && !update.OnDemandThroughput && !update.WarmThroughput) {
|
|
12582
|
-
if (!billingFlipped) 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.`);
|
|
12612
|
+
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.`);
|
|
12583
12613
|
continue;
|
|
12584
12614
|
}
|
|
12585
12615
|
const gsiUpdate = { Update: update };
|
|
@@ -12589,6 +12619,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12589
12619
|
}));
|
|
12590
12620
|
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
12591
12621
|
}
|
|
12622
|
+
if (newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") {
|
|
12623
|
+
if (gsiDiff.added.length > 0) await this.waitForIndexesActive(physicalId, logicalId);
|
|
12624
|
+
await this.reconcileAutoScalingTargets(physicalId, collectAutoScalingTargets(previousProperties, currentRegion), newBilling === "PAY_PER_REQUEST" ? [] : collectAutoScalingTargets(properties, currentRegion), currentRegion, autoScalingApplied);
|
|
12625
|
+
}
|
|
12592
12626
|
if (!deepEqual$1(properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"])) {
|
|
12593
12627
|
const sendTtl = async (cmd) => {
|
|
12594
12628
|
try {
|
|
@@ -12759,11 +12793,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12759
12793
|
* create-only field with no corresponding `RegisterScalableTarget`
|
|
12760
12794
|
* surface; `readAutoScalingSettings` also explicitly skips it.
|
|
12761
12795
|
*/
|
|
12762
|
-
async applyAutoScalingDiff(tableName, dimension, oldSettings, newSettings, client) {
|
|
12763
|
-
const
|
|
12764
|
-
const
|
|
12765
|
-
const
|
|
12766
|
-
const resourceId = `table/${tableName}`;
|
|
12796
|
+
async applyAutoScalingDiff(tableName, dimension, oldSettings, newSettings, client, indexName) {
|
|
12797
|
+
const metricType = dimension.endsWith("WriteCapacityUnits") ? "DynamoDBWriteCapacityUtilization" : "DynamoDBReadCapacityUtilization";
|
|
12798
|
+
const resourceId = autoScalingResourceId(tableName, indexName);
|
|
12799
|
+
const policyName = `${metricType}:${resourceId}`;
|
|
12767
12800
|
const asClient = client ?? await this.getLocalAutoScalingClient();
|
|
12768
12801
|
const oldEnabled = oldSettings !== void 0 && oldSettings !== null;
|
|
12769
12802
|
const newEnabled = newSettings !== void 0 && newSettings !== null;
|
|
@@ -12776,13 +12809,16 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12776
12809
|
return;
|
|
12777
12810
|
}
|
|
12778
12811
|
try {
|
|
12779
|
-
await asClient.send(new RegisterScalableTargetCommand({
|
|
12812
|
+
await withRetry(() => asClient.send(new RegisterScalableTargetCommand({
|
|
12780
12813
|
ServiceNamespace: "dynamodb",
|
|
12781
12814
|
ResourceId: resourceId,
|
|
12782
12815
|
ScalableDimension: dimension,
|
|
12783
12816
|
MinCapacity: minCapacity,
|
|
12784
12817
|
MaxCapacity: maxCapacity
|
|
12785
|
-
}))
|
|
12818
|
+
})), `${resourceId} (${dimension})`, {
|
|
12819
|
+
isRetryable: (_message, error) => isThrottlingError(error),
|
|
12820
|
+
...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
|
|
12821
|
+
});
|
|
12786
12822
|
} catch (err) {
|
|
12787
12823
|
this.logger.warn(`Could not register auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling register-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --min-capacity ${minCapacity} --max-capacity ${maxCapacity}`);
|
|
12788
12824
|
return;
|
|
@@ -12801,14 +12837,17 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12801
12837
|
if (tttCfg["ScaleOutCooldown"] !== void 0) targetTrackingConfig["ScaleOutCooldown"] = Number(tttCfg["ScaleOutCooldown"]);
|
|
12802
12838
|
if (tttCfg["DisableScaleIn"] !== void 0) targetTrackingConfig["DisableScaleIn"] = Boolean(tttCfg["DisableScaleIn"]);
|
|
12803
12839
|
try {
|
|
12804
|
-
await asClient.send(new PutScalingPolicyCommand({
|
|
12840
|
+
await withRetry(() => asClient.send(new PutScalingPolicyCommand({
|
|
12805
12841
|
PolicyName: policyName,
|
|
12806
12842
|
ServiceNamespace: "dynamodb",
|
|
12807
12843
|
ResourceId: resourceId,
|
|
12808
12844
|
ScalableDimension: dimension,
|
|
12809
12845
|
PolicyType: "TargetTrackingScaling",
|
|
12810
12846
|
TargetTrackingScalingPolicyConfiguration: targetTrackingConfig
|
|
12811
|
-
}))
|
|
12847
|
+
})), `${policyName}`, {
|
|
12848
|
+
isRetryable: (_message, error) => isThrottlingError(error),
|
|
12849
|
+
...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
|
|
12850
|
+
});
|
|
12812
12851
|
this.logger.debug(`Upserted auto-scaling policy ${policyName} on ${tableName} (${dimension})`);
|
|
12813
12852
|
} catch (err) {
|
|
12814
12853
|
this.logger.warn(`Could not put auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling put-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --policy-type TargetTrackingScaling`);
|
|
@@ -12843,6 +12882,130 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12843
12882
|
}
|
|
12844
12883
|
}
|
|
12845
12884
|
/**
|
|
12885
|
+
* Resolve the application-autoscaling client that owns a given region's
|
|
12886
|
+
* targets. The local region goes through the cached client; every other
|
|
12887
|
+
* region gets the per-region cached client.
|
|
12888
|
+
*/
|
|
12889
|
+
async autoScalingClientForRegion(region, localRegion) {
|
|
12890
|
+
return region === localRegion ? await this.getLocalAutoScalingClient() : this.getRegionalAutoScalingClient(region);
|
|
12891
|
+
}
|
|
12892
|
+
/**
|
|
12893
|
+
* Probe which of the given targets are ALREADY registered, so the
|
|
12894
|
+
* reconcile can skip the ones that are both present and unchanged
|
|
12895
|
+
* (Issue #1419).
|
|
12896
|
+
*
|
|
12897
|
+
* Per region: `DescribeScalableTargets` (batched + paginated) AND
|
|
12898
|
+
* `DescribeScalingPolicies`, returning the set of `autoScalingTargetKey`s
|
|
12899
|
+
* for which BOTH a target and a target-tracking policy exist.
|
|
12900
|
+
*
|
|
12901
|
+
* Both halves are required. A target whose `PutScalingPolicy` failed is
|
|
12902
|
+
* registered but scales nothing, and `applyAutoScalingDiff` swallows that
|
|
12903
|
+
* failure into a WARN — so probing the target alone would mark it present
|
|
12904
|
+
* and skip it on every later deploy, which is the same silent never-scales
|
|
12905
|
+
* gap this whole change exists to close, one level down.
|
|
12906
|
+
*
|
|
12907
|
+
* Best-effort: on ANY failure it returns `null`, which the caller reads as
|
|
12908
|
+
* "presence unknown" and falls back to upserting everything — an extra
|
|
12909
|
+
* idempotent call is always safer than skipping a registration that was
|
|
12910
|
+
* never made.
|
|
12911
|
+
*/
|
|
12912
|
+
async probeRegisteredTargets(tableName, specs, localRegion) {
|
|
12913
|
+
const byRegion = /* @__PURE__ */ new Map();
|
|
12914
|
+
for (const spec of specs) {
|
|
12915
|
+
const list = byRegion.get(spec.region);
|
|
12916
|
+
if (list) list.push(spec);
|
|
12917
|
+
else byRegion.set(spec.region, [spec]);
|
|
12918
|
+
}
|
|
12919
|
+
const indexPrefix = `${autoScalingResourceId(tableName)}/index/`;
|
|
12920
|
+
const keyOf = (resourceId, dimension, region) => {
|
|
12921
|
+
return `${region}|${dimension}|${resourceId.startsWith(indexPrefix) ? resourceId.slice(indexPrefix.length) : ""}`;
|
|
12922
|
+
};
|
|
12923
|
+
const present = /* @__PURE__ */ new Set();
|
|
12924
|
+
for (const [region, regionSpecs] of byRegion) {
|
|
12925
|
+
const resourceIds = [...new Set(regionSpecs.map((s) => autoScalingResourceId(tableName, s.indexName)))];
|
|
12926
|
+
const client = await this.autoScalingClientForRegion(region, localRegion);
|
|
12927
|
+
const targets = /* @__PURE__ */ new Set();
|
|
12928
|
+
const policies = /* @__PURE__ */ new Set();
|
|
12929
|
+
for (let i = 0; i < resourceIds.length; i += DESCRIBE_SCALABLE_TARGETS_BATCH) {
|
|
12930
|
+
const batch = resourceIds.slice(i, i + DESCRIBE_SCALABLE_TARGETS_BATCH);
|
|
12931
|
+
try {
|
|
12932
|
+
let nextToken;
|
|
12933
|
+
do {
|
|
12934
|
+
const res = await client.send(new DescribeScalableTargetsCommand({
|
|
12935
|
+
ServiceNamespace: "dynamodb",
|
|
12936
|
+
ResourceIds: batch,
|
|
12937
|
+
...nextToken ? { NextToken: nextToken } : {}
|
|
12938
|
+
}));
|
|
12939
|
+
for (const target of res.ScalableTargets ?? []) {
|
|
12940
|
+
if (!target.ResourceId || !target.ScalableDimension) continue;
|
|
12941
|
+
targets.add(keyOf(target.ResourceId, target.ScalableDimension, region));
|
|
12942
|
+
}
|
|
12943
|
+
nextToken = res.NextToken;
|
|
12944
|
+
} while (nextToken);
|
|
12945
|
+
let policyToken;
|
|
12946
|
+
do {
|
|
12947
|
+
const res = await client.send(new DescribeScalingPoliciesCommand({
|
|
12948
|
+
ServiceNamespace: "dynamodb",
|
|
12949
|
+
...policyToken ? { NextToken: policyToken } : {}
|
|
12950
|
+
}));
|
|
12951
|
+
for (const policy of res.ScalingPolicies ?? []) {
|
|
12952
|
+
if (!policy.ResourceId || !policy.ScalableDimension) continue;
|
|
12953
|
+
if (policy.PolicyType !== "TargetTrackingScaling") continue;
|
|
12954
|
+
policies.add(keyOf(policy.ResourceId, policy.ScalableDimension, region));
|
|
12955
|
+
}
|
|
12956
|
+
policyToken = res.NextToken;
|
|
12957
|
+
} while (policyToken);
|
|
12958
|
+
} catch (err) {
|
|
12959
|
+
this.logger.debug(`Could not probe existing auto-scaling targets on ${tableName} in ${region}: ${err instanceof Error ? err.message : String(err)} — re-asserting every target instead.`);
|
|
12960
|
+
return null;
|
|
12961
|
+
}
|
|
12962
|
+
}
|
|
12963
|
+
for (const key of targets) if (policies.has(key)) present.add(key);
|
|
12964
|
+
}
|
|
12965
|
+
return present;
|
|
12966
|
+
}
|
|
12967
|
+
/**
|
|
12968
|
+
* Reconcile the application-autoscaling targets a CFn template declares
|
|
12969
|
+
* against the ones its previous version declared (Issue #1419).
|
|
12970
|
+
*
|
|
12971
|
+
* **A desired target is re-asserted even when the template did not
|
|
12972
|
+
* change, unless it is known to already exist.** A purely diff-gated
|
|
12973
|
+
* register would never backfill the targets that are the entire subject
|
|
12974
|
+
* of this issue: on a table deployed before this fix the settings are
|
|
12975
|
+
* byte-identical on both sides of every subsequent deploy, so a diff
|
|
12976
|
+
* check sees no change and leaves the dimension unregistered forever.
|
|
12977
|
+
*
|
|
12978
|
+
* The naive form of that — upsert everything, every deploy — costs
|
|
12979
|
+
* `2 x (1 + N_gsi x (1 + N_replica))` serial round trips on EVERY deploy
|
|
12980
|
+
* of an autoscaled table (a 20-GSI, 3-replica table would spend well over
|
|
12981
|
+
* a hundred sequential calls re-asserting a steady state). So presence is
|
|
12982
|
+
* probed first with one batched `DescribeScalableTargets` per region, and
|
|
12983
|
+
* a target that already exists AND whose settings are unchanged is
|
|
12984
|
+
* skipped. When the probe fails, presence is unknown and everything is
|
|
12985
|
+
* upserted — the correct direction to fail.
|
|
12986
|
+
*/
|
|
12987
|
+
async reconcileAutoScalingTargets(tableName, previousSpecs, desiredSpecs, localRegion, alreadyApplied = /* @__PURE__ */ new Set()) {
|
|
12988
|
+
const previousSpecsToUse = previousSpecs.filter((s) => !alreadyApplied.has(autoScalingTargetKey(s)));
|
|
12989
|
+
const desiredSpecsToUse = desiredSpecs.filter((s) => !alreadyApplied.has(autoScalingTargetKey(s)));
|
|
12990
|
+
const previousByKey = /* @__PURE__ */ new Map();
|
|
12991
|
+
for (const spec of previousSpecsToUse) previousByKey.set(autoScalingTargetKey(spec), spec);
|
|
12992
|
+
const registered = desiredSpecsToUse.length > 0 && previousSpecsToUse.length > 0 ? await this.probeRegisteredTargets(tableName, desiredSpecsToUse, localRegion) : /* @__PURE__ */ new Set();
|
|
12993
|
+
const handled = /* @__PURE__ */ new Set();
|
|
12994
|
+
for (const spec of desiredSpecsToUse) {
|
|
12995
|
+
const key = autoScalingTargetKey(spec);
|
|
12996
|
+
handled.add(key);
|
|
12997
|
+
const previous = previousByKey.get(key);
|
|
12998
|
+
if (registered?.has(key) && previous !== void 0 && deepEqual$1(previous.settings, spec.settings)) continue;
|
|
12999
|
+
const client = await this.autoScalingClientForRegion(spec.region, localRegion);
|
|
13000
|
+
await this.applyAutoScalingDiff(tableName, spec.dimension, previous?.settings, spec.settings, client, spec.indexName);
|
|
13001
|
+
}
|
|
13002
|
+
for (const [key, spec] of previousByKey) {
|
|
13003
|
+
if (handled.has(key)) continue;
|
|
13004
|
+
const client = await this.autoScalingClientForRegion(spec.region, localRegion);
|
|
13005
|
+
await this.applyAutoScalingDiff(tableName, spec.dimension, spec.settings, void 0, client, spec.indexName);
|
|
13006
|
+
}
|
|
13007
|
+
}
|
|
13008
|
+
/**
|
|
12846
13009
|
* Delete a DynamoDB Global Table.
|
|
12847
13010
|
*
|
|
12848
13011
|
* Order is load-bearing:
|
|
@@ -12883,12 +13046,15 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12883
13046
|
throw new ProvisioningError(`Could not resolve client region for DynamoDB GlobalTable ${logicalId} delete — would risk dropping the local replica`, resourceType, logicalId, physicalId, cause);
|
|
12884
13047
|
}
|
|
12885
13048
|
try {
|
|
12886
|
-
const
|
|
13049
|
+
const describe = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
|
|
13050
|
+
const replicas = describe.Table?.Replicas ?? [];
|
|
13051
|
+
const tableIndexNames = (describe.Table?.GlobalSecondaryIndexes ?? []).map((gsi) => gsi.IndexName).filter((name) => typeof name === "string");
|
|
12887
13052
|
for (const replica of replicas) {
|
|
12888
13053
|
const region = replica.RegionName;
|
|
12889
13054
|
if (!region || region === currentRegion) continue;
|
|
12890
13055
|
try {
|
|
12891
13056
|
await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", {}, void 0, this.getRegionalAutoScalingClient(region));
|
|
13057
|
+
for (const indexName of tableIndexNames) await this.applyAutoScalingDiff(physicalId, "dynamodb:index:ReadCapacityUnits", {}, void 0, this.getRegionalAutoScalingClient(region), indexName);
|
|
12892
13058
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
12893
13059
|
TableName: physicalId,
|
|
12894
13060
|
ReplicaUpdates: [{ Delete: { RegionName: region } }]
|
|
@@ -12901,6 +13067,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12901
13067
|
const localAsClient = await this.getLocalAutoScalingClient();
|
|
12902
13068
|
await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", {}, void 0, localAsClient);
|
|
12903
13069
|
await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", {}, void 0, localAsClient);
|
|
13070
|
+
for (const indexName of tableIndexNames) {
|
|
13071
|
+
await this.applyAutoScalingDiff(physicalId, "dynamodb:index:WriteCapacityUnits", {}, void 0, localAsClient, indexName);
|
|
13072
|
+
await this.applyAutoScalingDiff(physicalId, "dynamodb:index:ReadCapacityUnits", {}, void 0, localAsClient, indexName);
|
|
13073
|
+
}
|
|
12904
13074
|
} catch (describeErr) {
|
|
12905
13075
|
if (!(describeErr instanceof ResourceNotFoundException$1)) {
|
|
12906
13076
|
const cause = describeErr instanceof Error ? describeErr : void 0;
|
|
@@ -13284,6 +13454,34 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
13284
13454
|
throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
13285
13455
|
}
|
|
13286
13456
|
/**
|
|
13457
|
+
* Wait until every GSI reports `IndexStatus: ACTIVE` (Issue #1419).
|
|
13458
|
+
*
|
|
13459
|
+
* `waitForTableActiveAfterUpdate` only checks `TableStatus`, and a GSI
|
|
13460
|
+
* added by `UpdateTable` leaves the TABLE ACTIVE while the index itself
|
|
13461
|
+
* is still `CREATING` — application-autoscaling rejects a target whose
|
|
13462
|
+
* resource is not ready, so registering right after the GSI-add loop
|
|
13463
|
+
* would skip the very index the deploy just introduced.
|
|
13464
|
+
*
|
|
13465
|
+
* **Best-effort by design**: on timeout it warns and returns rather than
|
|
13466
|
+
* throwing. A missed registration self-heals on the next deploy (the
|
|
13467
|
+
* reconcile re-asserts anything not already present), whereas throwing
|
|
13468
|
+
* here would fail a deploy whose actual resources are all correct. Index
|
|
13469
|
+
* backfill on a large table can take a long time, hence the 15-minute cap.
|
|
13470
|
+
*/
|
|
13471
|
+
async waitForIndexesActive(tableName, logicalId) {
|
|
13472
|
+
const maxAttempts = 900;
|
|
13473
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
13474
|
+
try {
|
|
13475
|
+
if (((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? []).every((gsi) => gsi.IndexStatus === "ACTIVE")) return;
|
|
13476
|
+
} catch (err) {
|
|
13477
|
+
this.logger.debug(`DescribeTable while waiting for indexes on ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13478
|
+
return;
|
|
13479
|
+
}
|
|
13480
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
13481
|
+
}
|
|
13482
|
+
this.logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts}s; auto-scaling registration for a still-building index may have been skipped. The next deploy re-asserts it.`);
|
|
13483
|
+
}
|
|
13484
|
+
/**
|
|
13287
13485
|
* Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
|
|
13288
13486
|
* Replica provisioning typically takes 1–5 min; cap at 10 min.
|
|
13289
13487
|
*/
|
|
@@ -13339,26 +13537,6 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
13339
13537
|
* GlobalSecondaryIndexes, TableClassOverride — differ from the old shape).
|
|
13340
13538
|
*/
|
|
13341
13539
|
/**
|
|
13342
|
-
* Derive the per-call `ProvisionedThroughput` shape required by
|
|
13343
|
-
* `CreateTableCommand` / `UpdateTableCommand` when BillingMode flips to
|
|
13344
|
-
* PROVISIONED. Shared between create() and the BillingMode-flip path in
|
|
13345
|
-
* update() so a user template's non-default read/write capacity is
|
|
13346
|
-
* preserved consistently across both code paths.
|
|
13347
|
-
*
|
|
13348
|
-
* Source of truth (CFn `AWS::DynamoDB::GlobalTable` shape):
|
|
13349
|
-
* - WriteCapacityUnits → `properties.WriteProvisionedThroughputSettings`
|
|
13350
|
-
* (top-level on the table). That block's ONLY member is
|
|
13351
|
-
* `WriteCapacityAutoScalingSettings` — write capacity on a GlobalTable is
|
|
13352
|
-
* always auto-scaled, so there is no literal `WriteCapacityUnits` to
|
|
13353
|
-
* prefer. `CreateTable` needs one concrete number, so the auto-scaling
|
|
13354
|
-
* block's `SeedCapacity` (documented as the value used until the scaling
|
|
13355
|
-
* policies exist) is taken before `MinCapacity`; 5 if the block is absent.
|
|
13356
|
-
* - ReadCapacityUnits → `Replicas[?Region==<region>].ReadProvisionedThroughputSettings`
|
|
13357
|
-
* (per-replica, the deploy region's setting). That block DOES carry a
|
|
13358
|
-
* literal `ReadCapacityUnits`, which is taken before its auto-scaling
|
|
13359
|
-
* `SeedCapacity` / `MinCapacity`; 5 if absent.
|
|
13360
|
-
*/
|
|
13361
|
-
/**
|
|
13362
13540
|
* Extract the local replica's `DeletionProtectionEnabled` from a CFn
|
|
13363
13541
|
* properties shape. The `AWS::DynamoDB::GlobalTable` CFn schema places
|
|
13364
13542
|
* the field inside `Replicas[?Region==<region>].DeletionProtectionEnabled`;
|
|
@@ -13378,10 +13556,30 @@ function extractLocalDeletionProtection(props, region) {
|
|
|
13378
13556
|
const topLevel = props["DeletionProtectionEnabled"];
|
|
13379
13557
|
return typeof topLevel === "boolean" ? topLevel : void 0;
|
|
13380
13558
|
}
|
|
13381
|
-
|
|
13559
|
+
/**
|
|
13560
|
+
* Derive the per-call `ProvisionedThroughput` shape required by
|
|
13561
|
+
* `CreateTableCommand` / `UpdateTableCommand` when BillingMode is
|
|
13562
|
+
* PROVISIONED. Shared between create() and the BillingMode-flip path in
|
|
13563
|
+
* update() so a user template's non-default read/write capacity is
|
|
13564
|
+
* preserved consistently across both code paths.
|
|
13565
|
+
*
|
|
13566
|
+
* Source of truth (CFn `AWS::DynamoDB::GlobalTable` shape):
|
|
13567
|
+
* - WriteCapacityUnits → `properties.WriteProvisionedThroughputSettings`
|
|
13568
|
+
* (top-level on the table). That block's ONLY member is
|
|
13569
|
+
* `WriteCapacityAutoScalingSettings` — write capacity on a GlobalTable is
|
|
13570
|
+
* always auto-scaled, so there is no literal `WriteCapacityUnits` to
|
|
13571
|
+
* prefer. `CreateTable` needs one concrete number; which member of the
|
|
13572
|
+
* auto-scaling block supplies it is context-dependent — see
|
|
13573
|
+
* {@link CapacitySource}, which `source` selects. 5 if the block is absent.
|
|
13574
|
+
* - ReadCapacityUnits → `Replicas[?Region==<region>].ReadProvisionedThroughputSettings`
|
|
13575
|
+
* (per-replica, the deploy region's setting). That block DOES carry a
|
|
13576
|
+
* literal `ReadCapacityUnits`, which is taken before its auto-scaling
|
|
13577
|
+
* members; 5 if absent.
|
|
13578
|
+
*/
|
|
13579
|
+
function derivePerCallProvisionedThroughput(properties, region, source = "min") {
|
|
13382
13580
|
return {
|
|
13383
|
-
ReadCapacityUnits: deriveReadCapacityUnits(asRecord((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS,
|
|
13384
|
-
WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(properties["WriteProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS
|
|
13581
|
+
ReadCapacityUnits: deriveReadCapacityUnits(asRecord((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
|
|
13582
|
+
WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(properties["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
|
|
13385
13583
|
};
|
|
13386
13584
|
}
|
|
13387
13585
|
/**
|
|
@@ -13389,6 +13587,110 @@ function derivePerCallProvisionedThroughput(properties, region) {
|
|
|
13389
13587
|
* capacity value at all. Matches the pre-existing table-level default.
|
|
13390
13588
|
*/
|
|
13391
13589
|
const DEFAULT_CAPACITY_UNITS = 5;
|
|
13590
|
+
/**
|
|
13591
|
+
* Stable identity for an {@link AutoScalingTargetSpec}, used to diff the
|
|
13592
|
+
* previous template's targets against the desired ones.
|
|
13593
|
+
*/
|
|
13594
|
+
function autoScalingTargetKey(spec) {
|
|
13595
|
+
return `${spec.region}|${spec.dimension}|${spec.indexName ?? ""}`;
|
|
13596
|
+
}
|
|
13597
|
+
/**
|
|
13598
|
+
* The application-autoscaling `ResourceId` for a DynamoDB table or one of
|
|
13599
|
+
* its indexes. ONE spelling, shared by the register / policy / deregister
|
|
13600
|
+
* calls, the presence probe, and the target key — a divergence between any
|
|
13601
|
+
* two of those would silently register one target and look up another.
|
|
13602
|
+
*/
|
|
13603
|
+
function autoScalingResourceId(tableName, indexName) {
|
|
13604
|
+
return indexName ? `table/${tableName}/index/${indexName}` : `table/${tableName}`;
|
|
13605
|
+
}
|
|
13606
|
+
/**
|
|
13607
|
+
* Test seam for the auto-scaling throttle retry's backoff, mirroring
|
|
13608
|
+
* `describeTypeRetryDelays`. Production leaves `sleep` undefined so
|
|
13609
|
+
* `withRetry`'s real schedule applies; a test injects a no-op so the ~47s
|
|
13610
|
+
* budget does not have to be waited out.
|
|
13611
|
+
*/
|
|
13612
|
+
const autoScalingRetryDelays = {};
|
|
13613
|
+
/**
|
|
13614
|
+
* Resource ids per `DescribeScalableTargets` request. Kept at the API's
|
|
13615
|
+
* documented per-page ceiling rather than a larger guess — the request is
|
|
13616
|
+
* paginated anyway, and over-asking risks a `ValidationException` that would
|
|
13617
|
+
* degrade the probe to "presence unknown" for the whole region.
|
|
13618
|
+
*/
|
|
13619
|
+
const DESCRIBE_SCALABLE_TARGETS_BATCH = 50;
|
|
13620
|
+
/**
|
|
13621
|
+
* Collect every application-autoscaling target a CFn GlobalTable properties
|
|
13622
|
+
* bag declares (Issue #1419).
|
|
13623
|
+
*
|
|
13624
|
+
* Where each dimension lives in the CFn shape — this is NOT symmetric, and
|
|
13625
|
+
* the asymmetry is the reason the per-index targets were missed:
|
|
13626
|
+
*
|
|
13627
|
+
* | dimension | CFn source | region |
|
|
13628
|
+
* | ------------------------------------ | ----------------------------------------------------------------------------- | ------ |
|
|
13629
|
+
* | `dynamodb:table:WriteCapacityUnits` | `WriteProvisionedThroughputSettings.WriteCapacityAutoScalingSettings` | local |
|
|
13630
|
+
* | `dynamodb:index:WriteCapacityUnits` | `GlobalSecondaryIndexes[].WriteProvisionedThroughputSettings.…` | local |
|
|
13631
|
+
* | `dynamodb:table:ReadCapacityUnits` | `Replicas[].ReadProvisionedThroughputSettings.ReadCapacityAutoScalingSettings` | replica's |
|
|
13632
|
+
* | `dynamodb:index:ReadCapacityUnits` | `Replicas[].GlobalSecondaryIndexes[].ReadProvisionedThroughputSettings.…` | replica's |
|
|
13633
|
+
*
|
|
13634
|
+
* WRITE capacity on a global table is owned by the source region only, so
|
|
13635
|
+
* both write dimensions register in the LOCAL region. READ capacity is
|
|
13636
|
+
* per-replica: each replica registers its own target with its own region's
|
|
13637
|
+
* application-autoscaling endpoint — including the LOCAL replica, whose
|
|
13638
|
+
* read target was never registered by any code path before this change
|
|
13639
|
+
* (`update()`'s replica loops all `continue` on the local region).
|
|
13640
|
+
*
|
|
13641
|
+
* A replica entry with no `Region` is skipped rather than defaulted: a
|
|
13642
|
+
* target registered against the wrong region's control plane is invisible
|
|
13643
|
+
* to every later diff and would leak on destroy.
|
|
13644
|
+
*/
|
|
13645
|
+
function collectAutoScalingTargets(properties, localRegion) {
|
|
13646
|
+
const specs = [];
|
|
13647
|
+
const tableWrite = asRecord(asRecord(properties["WriteProvisionedThroughputSettings"])?.["WriteCapacityAutoScalingSettings"]);
|
|
13648
|
+
if (tableWrite) specs.push({
|
|
13649
|
+
dimension: "dynamodb:table:WriteCapacityUnits",
|
|
13650
|
+
region: localRegion,
|
|
13651
|
+
settings: tableWrite
|
|
13652
|
+
});
|
|
13653
|
+
const cfnIndexes = properties["GlobalSecondaryIndexes"];
|
|
13654
|
+
if (Array.isArray(cfnIndexes)) for (const entry of cfnIndexes) {
|
|
13655
|
+
const gsi = asRecord(entry);
|
|
13656
|
+
const indexName = gsi?.["IndexName"];
|
|
13657
|
+
if (!gsi || typeof indexName !== "string") continue;
|
|
13658
|
+
const indexWrite = asRecord(asRecord(gsi["WriteProvisionedThroughputSettings"])?.["WriteCapacityAutoScalingSettings"]);
|
|
13659
|
+
if (indexWrite) specs.push({
|
|
13660
|
+
dimension: "dynamodb:index:WriteCapacityUnits",
|
|
13661
|
+
indexName,
|
|
13662
|
+
region: localRegion,
|
|
13663
|
+
settings: indexWrite
|
|
13664
|
+
});
|
|
13665
|
+
}
|
|
13666
|
+
const replicas = properties["Replicas"];
|
|
13667
|
+
if (Array.isArray(replicas)) for (const replicaEntry of replicas) {
|
|
13668
|
+
const replica = asRecord(replicaEntry);
|
|
13669
|
+
const region = replica?.["Region"];
|
|
13670
|
+
if (!replica || typeof region !== "string") continue;
|
|
13671
|
+
const replicaRead = asRecord(asRecord(replica["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"]);
|
|
13672
|
+
if (replicaRead) specs.push({
|
|
13673
|
+
dimension: "dynamodb:table:ReadCapacityUnits",
|
|
13674
|
+
region,
|
|
13675
|
+
settings: replicaRead
|
|
13676
|
+
});
|
|
13677
|
+
const replicaIndexes = replica["GlobalSecondaryIndexes"];
|
|
13678
|
+
if (!Array.isArray(replicaIndexes)) continue;
|
|
13679
|
+
for (const entry of replicaIndexes) {
|
|
13680
|
+
const replicaIndex = asRecord(entry);
|
|
13681
|
+
const indexName = replicaIndex?.["IndexName"];
|
|
13682
|
+
if (!replicaIndex || typeof indexName !== "string") continue;
|
|
13683
|
+
const indexRead = asRecord(asRecord(replicaIndex["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"]);
|
|
13684
|
+
if (indexRead) specs.push({
|
|
13685
|
+
dimension: "dynamodb:index:ReadCapacityUnits",
|
|
13686
|
+
indexName,
|
|
13687
|
+
region,
|
|
13688
|
+
settings: indexRead
|
|
13689
|
+
});
|
|
13690
|
+
}
|
|
13691
|
+
}
|
|
13692
|
+
return specs;
|
|
13693
|
+
}
|
|
13392
13694
|
/** Narrow an unknown CFn sub-object to a record (or `undefined`). */
|
|
13393
13695
|
function asRecord(value) {
|
|
13394
13696
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
@@ -13417,38 +13719,48 @@ function toFiniteNumber(value) {
|
|
|
13417
13719
|
* member — `WriteCapacityAutoScalingSettings` — i.e. write capacity on a
|
|
13418
13720
|
* GlobalTable is ALWAYS auto-scaled; there is no literal `WriteCapacityUnits`
|
|
13419
13721
|
* (that is why CDK 2.244's `TableV2` rejects `Capacity.fixed()` for write).
|
|
13420
|
-
* `CreateTable` / `UpdateTable` still require a concrete starting number
|
|
13421
|
-
*
|
|
13422
|
-
*
|
|
13722
|
+
* `CreateTable` / `UpdateTable` still require a concrete starting number;
|
|
13723
|
+
* which member of the auto-scaling block supplies it is context-dependent
|
|
13724
|
+
* (`MinCapacity` on a create, `SeedCapacity` on a billing-mode flip) — see
|
|
13725
|
+
* {@link CapacitySource}, which `source` selects.
|
|
13423
13726
|
*
|
|
13424
13727
|
* A literal `WriteCapacityUnits` is still honored first: the CFn schema
|
|
13425
13728
|
* forbids it, but hand-authored / imported templates and cdkd state written
|
|
13426
13729
|
* before this helper existed can carry it, and honoring it is strictly more
|
|
13427
13730
|
* faithful than ignoring it.
|
|
13731
|
+
*
|
|
13732
|
+
* `source` selects between the two contexts CloudFormation distinguishes —
|
|
13733
|
+
* see {@link CapacitySource}. Defaults to `'min'`, the common case.
|
|
13428
13734
|
*/
|
|
13429
|
-
function deriveWriteCapacityUnits(settings) {
|
|
13735
|
+
function deriveWriteCapacityUnits(settings, source = "min") {
|
|
13430
13736
|
if (!settings) return void 0;
|
|
13431
13737
|
const literal = toFiniteNumber(settings["WriteCapacityUnits"]);
|
|
13432
13738
|
if (literal !== void 0) return literal;
|
|
13433
13739
|
const autoScaling = asRecord(settings["WriteCapacityAutoScalingSettings"]);
|
|
13434
13740
|
if (!autoScaling) return void 0;
|
|
13435
|
-
return
|
|
13741
|
+
return pickAutoScalingCapacity(autoScaling, source);
|
|
13436
13742
|
}
|
|
13437
13743
|
/**
|
|
13438
13744
|
* Derive a single READ capacity number from a CFn
|
|
13439
13745
|
* `ReadProvisionedThroughputSettings` block (both the per-replica shape,
|
|
13440
13746
|
* which carries `ReadCapacityUnits` + `ReadCapacityAutoScalingSettings`, and
|
|
13441
13747
|
* the GSI/table-level `GlobalReadProvisionedThroughputSettings` shape, which
|
|
13442
|
-
* carries only `ReadCapacityUnits`). Same
|
|
13443
|
-
* write side.
|
|
13748
|
+
* carries only `ReadCapacityUnits`). Same {@link CapacitySource} precedence
|
|
13749
|
+
* as the write side.
|
|
13444
13750
|
*/
|
|
13445
|
-
function deriveReadCapacityUnits(settings) {
|
|
13751
|
+
function deriveReadCapacityUnits(settings, source = "min") {
|
|
13446
13752
|
if (!settings) return void 0;
|
|
13447
13753
|
const literal = toFiniteNumber(settings["ReadCapacityUnits"]);
|
|
13448
13754
|
if (literal !== void 0) return literal;
|
|
13449
13755
|
const autoScaling = asRecord(settings["ReadCapacityAutoScalingSettings"]);
|
|
13450
13756
|
if (!autoScaling) return void 0;
|
|
13451
|
-
return
|
|
13757
|
+
return pickAutoScalingCapacity(autoScaling, source);
|
|
13758
|
+
}
|
|
13759
|
+
/** Resolve a `*CapacityAutoScalingSettings` block to one number. */
|
|
13760
|
+
function pickAutoScalingCapacity(autoScaling, source) {
|
|
13761
|
+
const min = toFiniteNumber(autoScaling["MinCapacity"]);
|
|
13762
|
+
if (source === "min") return min ?? toFiniteNumber(autoScaling["SeedCapacity"]);
|
|
13763
|
+
return toFiniteNumber(autoScaling["SeedCapacity"]) ?? min;
|
|
13452
13764
|
}
|
|
13453
13765
|
/**
|
|
13454
13766
|
* Translate the CFn `AWS::DynamoDB::GlobalTable` `GlobalSecondaryIndexes[]`
|
|
@@ -13487,7 +13799,7 @@ function deriveReadCapacityUnits(settings) {
|
|
|
13487
13799
|
* written before this fix (and hand-authored templates) can carry them, and
|
|
13488
13800
|
* silently re-deriving over an explicit value would be a regression.
|
|
13489
13801
|
*/
|
|
13490
|
-
function toSdkGlobalSecondaryIndexes(properties, region, billingMode) {
|
|
13802
|
+
function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "min") {
|
|
13491
13803
|
const rawIndexes = properties["GlobalSecondaryIndexes"];
|
|
13492
13804
|
if (rawIndexes !== void 0 && !Array.isArray(rawIndexes)) throw new Error(`AWS::DynamoDB::GlobalTable GlobalSecondaryIndexes must be an array, got ${typeof rawIndexes} (${JSON.stringify(rawIndexes)?.slice(0, 200)}). An unresolved intrinsic here would otherwise deploy a table with no indexes.`);
|
|
13493
13805
|
const cfnIndexes = rawIndexes ?? [];
|
|
@@ -13519,8 +13831,8 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode) {
|
|
|
13519
13831
|
const explicitOnDemand = asRecord(gsi["OnDemandThroughput"]);
|
|
13520
13832
|
if (explicitProvisioned) sdk.ProvisionedThroughput = explicitProvisioned;
|
|
13521
13833
|
else if (billingMode === "PROVISIONED") sdk.ProvisionedThroughput = {
|
|
13522
|
-
ReadCapacityUnits: deriveReadCapacityUnits(asRecord(localEntry?.["ReadProvisionedThroughputSettings"])) ?? deriveReadCapacityUnits(asRecord(gsi["ReadProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS,
|
|
13523
|
-
WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(gsi["WriteProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS
|
|
13834
|
+
ReadCapacityUnits: deriveReadCapacityUnits(asRecord(localEntry?.["ReadProvisionedThroughputSettings"]), source) ?? deriveReadCapacityUnits(asRecord(gsi["ReadProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS,
|
|
13835
|
+
WriteCapacityUnits: deriveWriteCapacityUnits(asRecord(gsi["WriteProvisionedThroughputSettings"]), source) ?? DEFAULT_CAPACITY_UNITS
|
|
13524
13836
|
};
|
|
13525
13837
|
if (explicitOnDemand) sdk.OnDemandThroughput = explicitOnDemand;
|
|
13526
13838
|
else if (billingMode !== "PROVISIONED") {
|
|
@@ -13536,6 +13848,89 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode) {
|
|
|
13536
13848
|
return result;
|
|
13537
13849
|
}
|
|
13538
13850
|
/**
|
|
13851
|
+
* True when a CFn sub-object is nothing but an unresolved intrinsic
|
|
13852
|
+
* (`{"Fn::If": […]}` / `{"Ref": …}`), i.e. the template DID declare the block
|
|
13853
|
+
* but its value could not be resolved to a shape with members.
|
|
13854
|
+
*
|
|
13855
|
+
* Needed because presence is otherwise tested on the MEMBER key: an
|
|
13856
|
+
* intrinsic-valued block IS a record, so `asRecord` succeeds while
|
|
13857
|
+
* `['MaxReadRequestUnits']` is undefined — reporting "not declared" and firing
|
|
13858
|
+
* the destructive `-1` reset on a ceiling the template was trying to SET. That
|
|
13859
|
+
* is issue #1440 one level up from where the member test looks.
|
|
13860
|
+
*/
|
|
13861
|
+
function isUnresolvedIntrinsicBlock(value) {
|
|
13862
|
+
const record = asRecord(value);
|
|
13863
|
+
if (!record) return false;
|
|
13864
|
+
const keys = Object.keys(record);
|
|
13865
|
+
return keys.length > 0 && keys.every((k) => k === "Ref" || k.startsWith("Fn::"));
|
|
13866
|
+
}
|
|
13867
|
+
/**
|
|
13868
|
+
* Per-index RAW presence of the two per-GSI on-demand members, keyed by
|
|
13869
|
+
* `IndexName` (issue #1440).
|
|
13870
|
+
*
|
|
13871
|
+
* The per-GSI reset needs to know "did the template DROP this member", and the
|
|
13872
|
+
* only faithful answer is whether the CFn key is THERE — not whether its value
|
|
13873
|
+
* coerced. `toSdkGlobalSecondaryIndexes` runs every value through
|
|
13874
|
+
* {@link toFiniteNumber}, which returns `undefined` for a present-but-
|
|
13875
|
+
* unparseable value (an unresolved `{Ref: …}`, `''`, an object). Deciding
|
|
13876
|
+
* "dropped" from that collapsed view sent the `-1` reset for a member the
|
|
13877
|
+
* template was actively trying to SET, silently clearing the ceiling — the same
|
|
13878
|
+
* silent-wrong-action class the reset exists to remove, and the exact hazard
|
|
13879
|
+
* the table-level path was corrected for in #1434's PR.
|
|
13880
|
+
*
|
|
13881
|
+
* So this walks the same two sources as `toSdkGlobalSecondaryIndexes` — the
|
|
13882
|
+
* top-level GSI for the write half, the LOCAL replica's index entry (with the
|
|
13883
|
+
* GSI-level spelling as the documented fallback) for the read half — and
|
|
13884
|
+
* reports presence only. Keeping it beside that function is deliberate: the
|
|
13885
|
+
* two must agree on where each member lives, and a divergence between them is
|
|
13886
|
+
* exactly what would re-open the bug.
|
|
13887
|
+
*
|
|
13888
|
+
* A missing entry means "not declared", which lets the reset FIRE — i.e. the
|
|
13889
|
+
* DESTRUCTIVE direction, not a conservative one. Two DIFFERENT mechanisms keep
|
|
13890
|
+
* that safe, and neither is "the translation validates everything":
|
|
13891
|
+
* - a non-array `GlobalSecondaryIndexes` is refused LOUDLY by
|
|
13892
|
+
* `toSdkGlobalSecondaryIndexes` before any of this runs;
|
|
13893
|
+
* - a malformed per-ENTRY shape is NOT refused — it is passed through
|
|
13894
|
+
* untouched so AWS surfaces the real error — but such an entry carries no
|
|
13895
|
+
* usable `IndexName`, so the reset loop skips it before consulting this map.
|
|
13896
|
+
* Do not relax either behavior on the assumption that this helper degrades
|
|
13897
|
+
* safely on its own; it does not.
|
|
13898
|
+
*/
|
|
13899
|
+
function collectRawOnDemandDeclarations(properties, region) {
|
|
13900
|
+
const out = /* @__PURE__ */ new Map();
|
|
13901
|
+
const rawIndexes = properties["GlobalSecondaryIndexes"];
|
|
13902
|
+
if (!Array.isArray(rawIndexes)) return out;
|
|
13903
|
+
const replicas = properties["Replicas"];
|
|
13904
|
+
const localReplicaIndexes = asRecord(Array.isArray(replicas) ? replicas.find((r) => asRecord(r)?.["Region"] === region) : void 0)?.["GlobalSecondaryIndexes"];
|
|
13905
|
+
const localByName = /* @__PURE__ */ new Map();
|
|
13906
|
+
if (Array.isArray(localReplicaIndexes)) for (const entry of localReplicaIndexes) {
|
|
13907
|
+
const record = asRecord(entry);
|
|
13908
|
+
const name = record?.["IndexName"];
|
|
13909
|
+
if (record && typeof name === "string") localByName.set(name, record);
|
|
13910
|
+
}
|
|
13911
|
+
for (const entry of rawIndexes) {
|
|
13912
|
+
const gsi = asRecord(entry);
|
|
13913
|
+
const name = gsi?.["IndexName"];
|
|
13914
|
+
if (!gsi || typeof name !== "string") continue;
|
|
13915
|
+
const localEntry = localByName.get(name);
|
|
13916
|
+
const explicitOnDemand = asRecord(gsi["OnDemandThroughput"]);
|
|
13917
|
+
if (explicitOnDemand) {
|
|
13918
|
+
out.set(name, {
|
|
13919
|
+
readDeclared: explicitOnDemand["MaxReadRequestUnits"] !== void 0,
|
|
13920
|
+
writeDeclared: explicitOnDemand["MaxWriteRequestUnits"] !== void 0
|
|
13921
|
+
});
|
|
13922
|
+
continue;
|
|
13923
|
+
}
|
|
13924
|
+
const readBlocks = [localEntry?.["ReadOnDemandThroughputSettings"], gsi["ReadOnDemandThroughputSettings"]];
|
|
13925
|
+
const writeBlock = gsi["WriteOnDemandThroughputSettings"];
|
|
13926
|
+
out.set(name, {
|
|
13927
|
+
readDeclared: readBlocks.some((block) => asRecord(block)?.["MaxReadRequestUnits"] !== void 0 || isUnresolvedIntrinsicBlock(block)),
|
|
13928
|
+
writeDeclared: asRecord(writeBlock)?.["MaxWriteRequestUnits"] !== void 0 || isUnresolvedIntrinsicBlock(writeBlock)
|
|
13929
|
+
});
|
|
13930
|
+
}
|
|
13931
|
+
return out;
|
|
13932
|
+
}
|
|
13933
|
+
/**
|
|
13539
13934
|
* Translate a CFn `Replicas[].GlobalSecondaryIndexes[]` blob (the
|
|
13540
13935
|
* `ReplicaGlobalSecondaryIndexSpecification` shape) into the SDK's
|
|
13541
13936
|
* `ReplicaGlobalSecondaryIndex[]` used by `CreateReplicationGroupMemberAction`
|
|
@@ -65020,7 +65415,7 @@ function createMigrateCommand() {
|
|
|
65020
65415
|
*/
|
|
65021
65416
|
function buildProgram() {
|
|
65022
65417
|
const program = new Command();
|
|
65023
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.
|
|
65418
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.20");
|
|
65024
65419
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
65025
65420
|
const { profile } = actionCommand.optsWithGlobals();
|
|
65026
65421
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|