@go-to-k/cdkd 0.278.19 → 0.278.21
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-DaP2cw8z.js → asg-provider-BN1QMZlz.js} +2 -2
- package/dist/{asg-provider-DaP2cw8z.js.map → asg-provider-BN1QMZlz.js.map} +1 -1
- package/dist/cli.js +456 -79
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DOQs235O.js → deploy-engine-dvS1K_ET.js} +4 -4
- package/dist/{deploy-engine-DOQs235O.js.map → deploy-engine-dvS1K_ET.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-dvS1K_ET.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-BN1QMZlz.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"];
|
|
@@ -12595,6 +12619,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12595
12619
|
}));
|
|
12596
12620
|
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
12597
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
|
+
}
|
|
12598
12626
|
if (!deepEqual$1(properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"])) {
|
|
12599
12627
|
const sendTtl = async (cmd) => {
|
|
12600
12628
|
try {
|
|
@@ -12765,11 +12793,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12765
12793
|
* create-only field with no corresponding `RegisterScalableTarget`
|
|
12766
12794
|
* surface; `readAutoScalingSettings` also explicitly skips it.
|
|
12767
12795
|
*/
|
|
12768
|
-
async applyAutoScalingDiff(tableName, dimension, oldSettings, newSettings, client) {
|
|
12769
|
-
const
|
|
12770
|
-
const
|
|
12771
|
-
const
|
|
12772
|
-
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}`;
|
|
12773
12800
|
const asClient = client ?? await this.getLocalAutoScalingClient();
|
|
12774
12801
|
const oldEnabled = oldSettings !== void 0 && oldSettings !== null;
|
|
12775
12802
|
const newEnabled = newSettings !== void 0 && newSettings !== null;
|
|
@@ -12782,13 +12809,16 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12782
12809
|
return;
|
|
12783
12810
|
}
|
|
12784
12811
|
try {
|
|
12785
|
-
await asClient.send(new RegisterScalableTargetCommand({
|
|
12812
|
+
await withRetry(() => asClient.send(new RegisterScalableTargetCommand({
|
|
12786
12813
|
ServiceNamespace: "dynamodb",
|
|
12787
12814
|
ResourceId: resourceId,
|
|
12788
12815
|
ScalableDimension: dimension,
|
|
12789
12816
|
MinCapacity: minCapacity,
|
|
12790
12817
|
MaxCapacity: maxCapacity
|
|
12791
|
-
}))
|
|
12818
|
+
})), `${resourceId} (${dimension})`, {
|
|
12819
|
+
isRetryable: (_message, error) => isThrottlingError(error),
|
|
12820
|
+
...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
|
|
12821
|
+
});
|
|
12792
12822
|
} catch (err) {
|
|
12793
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}`);
|
|
12794
12824
|
return;
|
|
@@ -12807,14 +12837,17 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12807
12837
|
if (tttCfg["ScaleOutCooldown"] !== void 0) targetTrackingConfig["ScaleOutCooldown"] = Number(tttCfg["ScaleOutCooldown"]);
|
|
12808
12838
|
if (tttCfg["DisableScaleIn"] !== void 0) targetTrackingConfig["DisableScaleIn"] = Boolean(tttCfg["DisableScaleIn"]);
|
|
12809
12839
|
try {
|
|
12810
|
-
await asClient.send(new PutScalingPolicyCommand({
|
|
12840
|
+
await withRetry(() => asClient.send(new PutScalingPolicyCommand({
|
|
12811
12841
|
PolicyName: policyName,
|
|
12812
12842
|
ServiceNamespace: "dynamodb",
|
|
12813
12843
|
ResourceId: resourceId,
|
|
12814
12844
|
ScalableDimension: dimension,
|
|
12815
12845
|
PolicyType: "TargetTrackingScaling",
|
|
12816
12846
|
TargetTrackingScalingPolicyConfiguration: targetTrackingConfig
|
|
12817
|
-
}))
|
|
12847
|
+
})), `${policyName}`, {
|
|
12848
|
+
isRetryable: (_message, error) => isThrottlingError(error),
|
|
12849
|
+
...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
|
|
12850
|
+
});
|
|
12818
12851
|
this.logger.debug(`Upserted auto-scaling policy ${policyName} on ${tableName} (${dimension})`);
|
|
12819
12852
|
} catch (err) {
|
|
12820
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`);
|
|
@@ -12849,6 +12882,130 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12849
12882
|
}
|
|
12850
12883
|
}
|
|
12851
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
|
+
/**
|
|
12852
13009
|
* Delete a DynamoDB Global Table.
|
|
12853
13010
|
*
|
|
12854
13011
|
* Order is load-bearing:
|
|
@@ -12889,12 +13046,15 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12889
13046
|
throw new ProvisioningError(`Could not resolve client region for DynamoDB GlobalTable ${logicalId} delete — would risk dropping the local replica`, resourceType, logicalId, physicalId, cause);
|
|
12890
13047
|
}
|
|
12891
13048
|
try {
|
|
12892
|
-
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");
|
|
12893
13052
|
for (const replica of replicas) {
|
|
12894
13053
|
const region = replica.RegionName;
|
|
12895
13054
|
if (!region || region === currentRegion) continue;
|
|
12896
13055
|
try {
|
|
12897
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);
|
|
12898
13058
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
12899
13059
|
TableName: physicalId,
|
|
12900
13060
|
ReplicaUpdates: [{ Delete: { RegionName: region } }]
|
|
@@ -12907,6 +13067,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12907
13067
|
const localAsClient = await this.getLocalAutoScalingClient();
|
|
12908
13068
|
await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", {}, void 0, localAsClient);
|
|
12909
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
|
+
}
|
|
12910
13074
|
} catch (describeErr) {
|
|
12911
13075
|
if (!(describeErr instanceof ResourceNotFoundException$1)) {
|
|
12912
13076
|
const cause = describeErr instanceof Error ? describeErr : void 0;
|
|
@@ -13290,6 +13454,34 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
13290
13454
|
throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
13291
13455
|
}
|
|
13292
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
|
+
/**
|
|
13293
13485
|
* Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
|
|
13294
13486
|
* Replica provisioning typically takes 1–5 min; cap at 10 min.
|
|
13295
13487
|
*/
|
|
@@ -13345,26 +13537,6 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
13345
13537
|
* GlobalSecondaryIndexes, TableClassOverride — differ from the old shape).
|
|
13346
13538
|
*/
|
|
13347
13539
|
/**
|
|
13348
|
-
* Derive the per-call `ProvisionedThroughput` shape required by
|
|
13349
|
-
* `CreateTableCommand` / `UpdateTableCommand` when BillingMode flips to
|
|
13350
|
-
* PROVISIONED. Shared between create() and the BillingMode-flip path in
|
|
13351
|
-
* update() so a user template's non-default read/write capacity is
|
|
13352
|
-
* preserved consistently across both code paths.
|
|
13353
|
-
*
|
|
13354
|
-
* Source of truth (CFn `AWS::DynamoDB::GlobalTable` shape):
|
|
13355
|
-
* - WriteCapacityUnits → `properties.WriteProvisionedThroughputSettings`
|
|
13356
|
-
* (top-level on the table). That block's ONLY member is
|
|
13357
|
-
* `WriteCapacityAutoScalingSettings` — write capacity on a GlobalTable is
|
|
13358
|
-
* always auto-scaled, so there is no literal `WriteCapacityUnits` to
|
|
13359
|
-
* prefer. `CreateTable` needs one concrete number, so the auto-scaling
|
|
13360
|
-
* block's `SeedCapacity` (documented as the value used until the scaling
|
|
13361
|
-
* policies exist) is taken before `MinCapacity`; 5 if the block is absent.
|
|
13362
|
-
* - ReadCapacityUnits → `Replicas[?Region==<region>].ReadProvisionedThroughputSettings`
|
|
13363
|
-
* (per-replica, the deploy region's setting). That block DOES carry a
|
|
13364
|
-
* literal `ReadCapacityUnits`, which is taken before its auto-scaling
|
|
13365
|
-
* `SeedCapacity` / `MinCapacity`; 5 if absent.
|
|
13366
|
-
*/
|
|
13367
|
-
/**
|
|
13368
13540
|
* Extract the local replica's `DeletionProtectionEnabled` from a CFn
|
|
13369
13541
|
* properties shape. The `AWS::DynamoDB::GlobalTable` CFn schema places
|
|
13370
13542
|
* the field inside `Replicas[?Region==<region>].DeletionProtectionEnabled`;
|
|
@@ -13384,10 +13556,30 @@ function extractLocalDeletionProtection(props, region) {
|
|
|
13384
13556
|
const topLevel = props["DeletionProtectionEnabled"];
|
|
13385
13557
|
return typeof topLevel === "boolean" ? topLevel : void 0;
|
|
13386
13558
|
}
|
|
13387
|
-
|
|
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") {
|
|
13388
13580
|
return {
|
|
13389
|
-
ReadCapacityUnits: deriveReadCapacityUnits(asRecord((properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS,
|
|
13390
|
-
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
|
|
13391
13583
|
};
|
|
13392
13584
|
}
|
|
13393
13585
|
/**
|
|
@@ -13395,6 +13587,110 @@ function derivePerCallProvisionedThroughput(properties, region) {
|
|
|
13395
13587
|
* capacity value at all. Matches the pre-existing table-level default.
|
|
13396
13588
|
*/
|
|
13397
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
|
+
}
|
|
13398
13694
|
/** Narrow an unknown CFn sub-object to a record (or `undefined`). */
|
|
13399
13695
|
function asRecord(value) {
|
|
13400
13696
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
@@ -13423,38 +13719,48 @@ function toFiniteNumber(value) {
|
|
|
13423
13719
|
* member — `WriteCapacityAutoScalingSettings` — i.e. write capacity on a
|
|
13424
13720
|
* GlobalTable is ALWAYS auto-scaled; there is no literal `WriteCapacityUnits`
|
|
13425
13721
|
* (that is why CDK 2.244's `TableV2` rejects `Capacity.fixed()` for write).
|
|
13426
|
-
* `CreateTable` / `UpdateTable` still require a concrete starting number
|
|
13427
|
-
*
|
|
13428
|
-
*
|
|
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.
|
|
13429
13726
|
*
|
|
13430
13727
|
* A literal `WriteCapacityUnits` is still honored first: the CFn schema
|
|
13431
13728
|
* forbids it, but hand-authored / imported templates and cdkd state written
|
|
13432
13729
|
* before this helper existed can carry it, and honoring it is strictly more
|
|
13433
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.
|
|
13434
13734
|
*/
|
|
13435
|
-
function deriveWriteCapacityUnits(settings) {
|
|
13735
|
+
function deriveWriteCapacityUnits(settings, source = "min") {
|
|
13436
13736
|
if (!settings) return void 0;
|
|
13437
13737
|
const literal = toFiniteNumber(settings["WriteCapacityUnits"]);
|
|
13438
13738
|
if (literal !== void 0) return literal;
|
|
13439
13739
|
const autoScaling = asRecord(settings["WriteCapacityAutoScalingSettings"]);
|
|
13440
13740
|
if (!autoScaling) return void 0;
|
|
13441
|
-
return
|
|
13741
|
+
return pickAutoScalingCapacity(autoScaling, source);
|
|
13442
13742
|
}
|
|
13443
13743
|
/**
|
|
13444
13744
|
* Derive a single READ capacity number from a CFn
|
|
13445
13745
|
* `ReadProvisionedThroughputSettings` block (both the per-replica shape,
|
|
13446
13746
|
* which carries `ReadCapacityUnits` + `ReadCapacityAutoScalingSettings`, and
|
|
13447
13747
|
* the GSI/table-level `GlobalReadProvisionedThroughputSettings` shape, which
|
|
13448
|
-
* carries only `ReadCapacityUnits`). Same
|
|
13449
|
-
* write side.
|
|
13748
|
+
* carries only `ReadCapacityUnits`). Same {@link CapacitySource} precedence
|
|
13749
|
+
* as the write side.
|
|
13450
13750
|
*/
|
|
13451
|
-
function deriveReadCapacityUnits(settings) {
|
|
13751
|
+
function deriveReadCapacityUnits(settings, source = "min") {
|
|
13452
13752
|
if (!settings) return void 0;
|
|
13453
13753
|
const literal = toFiniteNumber(settings["ReadCapacityUnits"]);
|
|
13454
13754
|
if (literal !== void 0) return literal;
|
|
13455
13755
|
const autoScaling = asRecord(settings["ReadCapacityAutoScalingSettings"]);
|
|
13456
13756
|
if (!autoScaling) return void 0;
|
|
13457
|
-
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;
|
|
13458
13764
|
}
|
|
13459
13765
|
/**
|
|
13460
13766
|
* Translate the CFn `AWS::DynamoDB::GlobalTable` `GlobalSecondaryIndexes[]`
|
|
@@ -13493,7 +13799,7 @@ function deriveReadCapacityUnits(settings) {
|
|
|
13493
13799
|
* written before this fix (and hand-authored templates) can carry them, and
|
|
13494
13800
|
* silently re-deriving over an explicit value would be a regression.
|
|
13495
13801
|
*/
|
|
13496
|
-
function toSdkGlobalSecondaryIndexes(properties, region, billingMode) {
|
|
13802
|
+
function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "min") {
|
|
13497
13803
|
const rawIndexes = properties["GlobalSecondaryIndexes"];
|
|
13498
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.`);
|
|
13499
13805
|
const cfnIndexes = rawIndexes ?? [];
|
|
@@ -13525,8 +13831,8 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode) {
|
|
|
13525
13831
|
const explicitOnDemand = asRecord(gsi["OnDemandThroughput"]);
|
|
13526
13832
|
if (explicitProvisioned) sdk.ProvisionedThroughput = explicitProvisioned;
|
|
13527
13833
|
else if (billingMode === "PROVISIONED") sdk.ProvisionedThroughput = {
|
|
13528
|
-
ReadCapacityUnits: deriveReadCapacityUnits(asRecord(localEntry?.["ReadProvisionedThroughputSettings"])) ?? deriveReadCapacityUnits(asRecord(gsi["ReadProvisionedThroughputSettings"])) ?? DEFAULT_CAPACITY_UNITS,
|
|
13529
|
-
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
|
|
13530
13836
|
};
|
|
13531
13837
|
if (explicitOnDemand) sdk.OnDemandThroughput = explicitOnDemand;
|
|
13532
13838
|
else if (billingMode !== "PROVISIONED") {
|
|
@@ -32713,13 +33019,14 @@ var GlueProvider = class {
|
|
|
32713
33019
|
const tableName = tableInput["Name"] ?? properties["Name"];
|
|
32714
33020
|
if (!tableName) throw new ProvisioningError(`TableInput.Name or top-level Name is required for Glue Table ${logicalId}`, resourceType, logicalId);
|
|
32715
33021
|
const catalogId = properties["CatalogId"];
|
|
33022
|
+
assertIcebergTableInputAbsent(logicalId, resourceType, properties);
|
|
32716
33023
|
const openTableFormatInput = properties["OpenTableFormatInput"];
|
|
32717
33024
|
try {
|
|
32718
33025
|
await this.getClient().send(new CreateTableCommand$1({
|
|
32719
33026
|
CatalogId: catalogId,
|
|
32720
33027
|
DatabaseName: databaseName,
|
|
32721
33028
|
TableInput: this.buildTableInput(tableInput, tableName),
|
|
32722
|
-
...openTableFormatInput !== void 0 && { OpenTableFormatInput:
|
|
33029
|
+
...openTableFormatInput !== void 0 && { OpenTableFormatInput: openTableFormatInput }
|
|
32723
33030
|
}));
|
|
32724
33031
|
const physicalId = `${databaseName}|${tableName}`;
|
|
32725
33032
|
this.logger.debug(`Successfully created Glue Table ${logicalId}: ${physicalId}`);
|
|
@@ -32739,6 +33046,8 @@ var GlueProvider = class {
|
|
|
32739
33046
|
const tableInput = properties["TableInput"];
|
|
32740
33047
|
if (!tableInput) throw new ProvisioningError(`TableInput is required for Glue Table update ${logicalId}`, resourceType, logicalId, physicalId);
|
|
32741
33048
|
const catalogId = properties["CatalogId"];
|
|
33049
|
+
const updateIcebergKey = findIcebergTableInputKey(properties);
|
|
33050
|
+
if (updateIcebergKey !== void 0) this.logger.warn(icebergTableInputRefusalMessage(logicalId, updateIcebergKey, "update"));
|
|
32742
33051
|
try {
|
|
32743
33052
|
await this.getClient().send(new UpdateTableCommand$1({
|
|
32744
33053
|
CatalogId: catalogId,
|
|
@@ -33294,32 +33603,100 @@ function sleep$1(ms) {
|
|
|
33294
33603
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
33295
33604
|
}
|
|
33296
33605
|
/**
|
|
33297
|
-
*
|
|
33298
|
-
* `OpenTableFormatInput
|
|
33299
|
-
*
|
|
33300
|
-
*
|
|
33301
|
-
*
|
|
33302
|
-
|
|
33303
|
-
|
|
33304
|
-
|
|
33305
|
-
*
|
|
33306
|
-
*
|
|
33307
|
-
*
|
|
33308
|
-
*
|
|
33309
|
-
*
|
|
33310
|
-
|
|
33311
|
-
|
|
33312
|
-
|
|
33313
|
-
|
|
33314
|
-
|
|
33315
|
-
|
|
33316
|
-
return
|
|
33317
|
-
|
|
33318
|
-
|
|
33319
|
-
|
|
33320
|
-
|
|
33321
|
-
|
|
33322
|
-
|
|
33606
|
+
* Both spellings of the Iceberg table spec cdkd refuses inside
|
|
33607
|
+
* `OpenTableFormatInput.IcebergInput`: the CFn registry schema's
|
|
33608
|
+
* `IcebergTableInput` and `@aws-sdk/client-glue`'s `CreateIcebergTableInput`.
|
|
33609
|
+
* A hand-written template can carry either, and neither is deployable, so the
|
|
33610
|
+
* pre-flight matches both and names whichever it found.
|
|
33611
|
+
*/
|
|
33612
|
+
const ICEBERG_TABLE_INPUT_KEYS = ["IcebergTableInput", "CreateIcebergTableInput"];
|
|
33613
|
+
/**
|
|
33614
|
+
* Return whichever {@link ICEBERG_TABLE_INPUT_KEYS} spelling the template
|
|
33615
|
+
* carries under `OpenTableFormatInput.IcebergInput`, or `undefined`.
|
|
33616
|
+
*
|
|
33617
|
+
* Non-object shapes at either level (an unresolved intrinsic) are treated as
|
|
33618
|
+
* "absent" so AWS surfaces the real validation error rather than this refusal.
|
|
33619
|
+
*/
|
|
33620
|
+
function findIcebergTableInputKey(properties) {
|
|
33621
|
+
const openTableFormatInput = properties["OpenTableFormatInput"];
|
|
33622
|
+
if (typeof openTableFormatInput !== "object" || openTableFormatInput === null || Array.isArray(openTableFormatInput)) return;
|
|
33623
|
+
const iceberg = openTableFormatInput["IcebergInput"];
|
|
33624
|
+
if (typeof iceberg !== "object" || iceberg === null || Array.isArray(iceberg)) return;
|
|
33625
|
+
return ICEBERG_TABLE_INPUT_KEYS.find((key) => key in iceberg);
|
|
33626
|
+
}
|
|
33627
|
+
/**
|
|
33628
|
+
* PRE-FLIGHT REFUSAL of `AWS::Glue::Table`
|
|
33629
|
+
* `OpenTableFormatInput.IcebergInput.IcebergTableInput` (issue #1454).
|
|
33630
|
+
*
|
|
33631
|
+
* **This is a deliberate PARITY DIVERGENCE, and it was a conscious choice.**
|
|
33632
|
+
* cdkd's compatibility target is CloudFormation, and CloudFormation does not
|
|
33633
|
+
* validate this property — it forwards it and lets the deploy roll back. cdkd
|
|
33634
|
+
* refuses it EARLIER and with a better message instead. The justification is
|
|
33635
|
+
* that no user is losing a working deployment: the live probe on issue #1408
|
|
33636
|
+
* (2026-08-09, us-east-1 — 5 raw `glue:CreateTable` shapes plus 5
|
|
33637
|
+
* `AWS::Glue::Table` CloudFormation stacks) proved the property is undeployable
|
|
33638
|
+
* on BOTH paths.
|
|
33639
|
+
*
|
|
33640
|
+
* - **The raw `glue:CreateTable` API — the call cdkd itself makes** — rejects
|
|
33641
|
+
* every `CreateIcebergTableInput` shape: without a
|
|
33642
|
+
* `TableInput.StorageDescriptor` it fails `Location information cannot be
|
|
33643
|
+
* null while creating an iceberg table`, and with one it fails `Table
|
|
33644
|
+
* metadata information present at multiple parts of input request`. The
|
|
33645
|
+
* spec's own `Location` is never read. So this is NOT merely cdkd declining
|
|
33646
|
+
* what CloudFormation declines — cdkd's own path cannot deploy it either.
|
|
33647
|
+
* - **CloudFormation** rolls back all three variants with `Table metadata is
|
|
33648
|
+
* expected only via TableInput or via IcebergTableInputProperties inside
|
|
33649
|
+
* OpenTableFormatInput` — naming a property that exists in NEITHER the CFn
|
|
33650
|
+
* registry schema (`IcebergInput.IcebergTableInput`) NOR
|
|
33651
|
+
* `@aws-sdk/client-glue` (`IcebergInput.CreateIcebergTableInput`). That
|
|
33652
|
+
* three-way contract mismatch is an AWS-side bug.
|
|
33653
|
+
*
|
|
33654
|
+
* Because there is no shape in which the property works, forwarding it can only
|
|
33655
|
+
* produce a late, cryptic AWS error. Failing fast with the working shape spelled
|
|
33656
|
+
* out is strictly more useful.
|
|
33657
|
+
*
|
|
33658
|
+
* **CREATE only — update WARNS instead.** See the comment at the `updateTable`
|
|
33659
|
+
* call site: rollback replays from cdkd STATE, and a table created by a
|
|
33660
|
+
* pre-#1390 build carries the key in its state record, so refusing on update
|
|
33661
|
+
* would make such a table unrollbackable with no template-side remedy. cdkd
|
|
33662
|
+
* does not wire Glue's update-only `UpdateOpenTableFormatInput` shape and
|
|
33663
|
+
* `buildTableInput` is an explicit allow-list, so warning there forwards
|
|
33664
|
+
* nothing and loses no protection. One rollback arm is still exposed — the
|
|
33665
|
+
* reverse-replacement re-create replays a state record through `create()`;
|
|
33666
|
+
* see issue #1463 and the note at the `updateTable` call site.
|
|
33667
|
+
*
|
|
33668
|
+
* **Known bypass: the sticky Cloud Control route.** This is a GlueProvider
|
|
33669
|
+
* pre-flight, so it only runs on the SDK route. When a table's state record
|
|
33670
|
+
* carries `provisionedBy: 'cc-api'`, `ProviderRegistry.getProviderFor` sticky-
|
|
33671
|
+
* routes it to `CloudControlProvider`, which forwards the property to
|
|
33672
|
+
* CloudFormation and gets AWS's rollback instead of this message. That route is
|
|
33673
|
+
* opt-in only (`--recreate-via-cc-api`, or a legacy state record) —
|
|
33674
|
+
* `AWS::Glue::Table` has an empty `silentDrop` map, so no ordinary deploy takes
|
|
33675
|
+
* it. The outcome there is still a failure, just a later and less helpful one,
|
|
33676
|
+
* which is why this is documented rather than duplicated into the CC provider.
|
|
33677
|
+
*
|
|
33678
|
+
* **WARNING before relaxing this refusal.** cdkd used to RENAME the CFn
|
|
33679
|
+
* `IcebergTableInput` to the SDK's `CreateIcebergTableInput` (issue #1390); that
|
|
33680
|
+
* rename existed because the AWS SDK v3 serializer DROPS unknown members, so the
|
|
33681
|
+
* CFn spelling silently discarded the entire Iceberg spec while `CreateTable`
|
|
33682
|
+
* reported success. The rename was removed here as unreachable once this
|
|
33683
|
+
* refusal landed. If AWS ever ships a deployable shape and this check is
|
|
33684
|
+
* relaxed, the rename MUST be restored in the same change — otherwise the
|
|
33685
|
+
* silent-drop bug of #1390 comes straight back.
|
|
33686
|
+
*/
|
|
33687
|
+
function assertIcebergTableInputAbsent(logicalId, resourceType, properties, physicalId) {
|
|
33688
|
+
const key = findIcebergTableInputKey(properties);
|
|
33689
|
+
if (key === void 0) return;
|
|
33690
|
+
throw new ProvisioningError(icebergTableInputRefusalMessage(logicalId, key, "create"), resourceType, logicalId, physicalId);
|
|
33691
|
+
}
|
|
33692
|
+
/**
|
|
33693
|
+
* The shared #1454 message body, so the CREATE refusal and the UPDATE warning
|
|
33694
|
+
* cannot drift apart. `mode` only changes the lead clause — everything after it
|
|
33695
|
+
* (the probe evidence and the working shape) is identical, because the user's
|
|
33696
|
+
* next action is the same either way.
|
|
33697
|
+
*/
|
|
33698
|
+
function icebergTableInputRefusalMessage(logicalId, key, mode) {
|
|
33699
|
+
return `AWS::Glue::Table ${logicalId}: OpenTableFormatInput.IcebergInput.${key} ${mode === "create" ? `cannot be deployed by AWS in any shape, so cdkd refuses it before calling Glue (issue #1454).` : "cannot be deployed by AWS in any shape and is IGNORED on update — cdkd does not wire Glue's update-only UpdateOpenTableFormatInput shape, so it forwards nothing here and only warns (issue #1454). A CREATE of this table would be refused outright."}\n Live-probed 2026-08-09 in us-east-1 (issue #1408): glue:CreateTable — the API cdkd calls — rejects every Iceberg table spec ("Location information cannot be null while creating an iceberg table" without a TableInput.StorageDescriptor, "Table metadata information present at multiple parts of input request" with one), and CloudFormation rolls the same template back with "Table metadata is expected only via TableInput or via IcebergTableInputProperties inside OpenTableFormatInput" — a property name that exists in neither the CFn registry schema (IcebergInput.IcebergTableInput) nor @aws-sdk/client-glue (IcebergInput.CreateIcebergTableInput).\n Working shape — put the table metadata in TableInput and leave IcebergInput carrying only the create-time directive:\n TableInput: { Name, TableType: 'EXTERNAL_TABLE', StorageDescriptor: { Location: 's3://your-bucket/prefix/', Columns: [...] } }\n OpenTableFormatInput: { IcebergInput: { MetadataOperation: 'CREATE' } } // Version: '2' is also accepted\n Glue then writes the Iceberg metadata itself — the created table comes back with Parameters.table_type = ICEBERG and a populated Parameters.metadata_location.`;
|
|
33323
33700
|
}
|
|
33324
33701
|
/**
|
|
33325
33702
|
* Build the SDK `EncryptionConfiguration` from the CFn-shape input
|
|
@@ -65109,7 +65486,7 @@ function createMigrateCommand() {
|
|
|
65109
65486
|
*/
|
|
65110
65487
|
function buildProgram() {
|
|
65111
65488
|
const program = new Command();
|
|
65112
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.
|
|
65489
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.21");
|
|
65113
65490
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
65114
65491
|
const { profile } = actionCommand.optsWithGlobals();
|
|
65115
65492
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|