@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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as DagBuilder, A as red, At as resolveApp, B as isTerminationProtectionPropagationError, Bt as CFN_TEMPLATE_URL_LIMIT, C as isStatefulRecreateTargetSync, Ct as runDockerStreaming, D as cyan, Dt as synthesisStatusMessage, E as bold, Et as Synthesizer, F as ProviderRegistry, Ft as resolveStateBucketWithDefaultAndSource, G as normalizeAwsTagsToCfn, Gt as AssemblyReader, H as cfnRefValueFromPhysicalId, Ht as findLargeInlineResources, I as findActionableSilentDrops, It as resolveUseCdkBootstrapAssets, J as applyRoleArnIfSet, Jt as resolveBucketRegion, K as resolveExplicitPhysicalId, Kt as processStackMessages, L as CloudControlProvider, Lt as stateBucketExistenceConfirmed, M as IAMRoleProvider, Mt as resolveCaptureObservedState, N as collectInlinePolicyNamesManagedBySiblings, Nt as resolveSkipPrefix, O as gray, Ot as getDefaultStateBucketName, P as clearOnUpdateRemoval, Pt as resolveStateBucketWithDefault, Q as isRetryableTransientError, Qt as setAwsClients, R as slowCcOperationTimeoutMs, Rt as warnDeprecatedNoPrefixCliFlag, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, St as runDockerForeground, T as formatResourceLine, Tt as getDockerImageBySourceHash, U as refStateLookupFromResource, Ut as uploadCfnTemplate, V as IntrinsicFunctionResolver, Vt as MIGRATE_TMP_PREFIX, W as WAFv2WebACLProvider, Wt as expectedOwnerParam, X as describeTypeWithThrottleRetry, Xt as getAwsClients, Y as DiffCalculator, Yt as AwsClients, Z as withRetry, Zt as resetAwsClients, _ as createPreDeleteFinalSnapshot, _t as validateAssetBucketName, a as DeploymentEventsStore, an as LocalMigrateError, at as AssetPublisher, b as unsupportedFinalSnapshotError, bn as normalizeAwsError, bt as formatDockerLoginError, c as replayFailedOperations, cn as MissingCdkCliError, ct as buildAssetRedirectMap, d as IMPLICIT_DELETE_DEPENDENCIES, dn as ProvisioningError, dt as rewriteTemplateAssetReferences, en as CdkdError, et as TemplateParser, f as computeImplicitDeleteEdges, fn as ResourceTimeoutError, ft as AssetModeResolver, g as ccRoutedFinalSnapshotError, gt as parseBootstrapMarker, h as buildFinalSnapshotIdentifier, hn as StackTerminationProtectionError, ht as getBootstrapMarkerKey, i as DeploymentEventsReader, in as LocalInvokeBuildError$1, it as shouldRetainResource, j as yellow, jt as resolveAutoAssetStorage, k as green, kt as getLegacyStateBucketName, l as replayRollback, ln as NestedStackChildDirectDestroyError, lt as createAssetRedirectResolver, m as PRE_DELETE_SNAPSHOT_TYPES, mn as StackHasActiveImportsError, mt as ensureAssetStorage, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nt as S3StateBackend, o as planFailedOps, on as LocalStartServiceError, ot as stringifyValue, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as ResourceUpdateNotSupportedError, pt as BOOTSTRAP_MARKER_PREFIX, q as assertRegionMatch, r as DeployEngine, rn as DeployCancelledError, rt as rebuildClientForBucketRegion, s as planRollback, st as WorkGraph, t as DEFAULT_RESOURCE_TIMEOUT_MS, tt as LockManager, u as withResourceDeadline, un as PartialFailureError, ut as loadPublishableAssetManifest, v as isFinalSnapshotError, vt as validateContainerRepoName, w as renderStatefulReason, wt as AssetManifestLoader, x as extractDeploymentEventError, xn as withErrorHandling, xt as getDockerCmd, y as refusesFinalSnapshot, yt as buildDockerImage, z as disableInstanceApiTermination, zt as CFN_TEMPLATE_BODY_LIMIT } from "./deploy-engine-DOQs235O.js";
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-DaP2cw8z.js";
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
- if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(writeAutoScalingOld, effectiveNewAutoScaling)) await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", writeAutoScalingOld, effectiveNewAutoScaling);
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 isWrite = dimension === "dynamodb:table:WriteCapacityUnits";
12770
- const policyName = isWrite ? `DynamoDBWriteCapacityUtilization:table/${tableName}` : `DynamoDBReadCapacityUtilization:table/${tableName}`;
12771
- const metricType = isWrite ? "DynamoDBWriteCapacityUtilization" : "DynamoDBReadCapacityUtilization";
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 replicas = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table?.Replicas ?? [];
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
- function derivePerCallProvisionedThroughput(properties, region) {
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, so
13427
- * we take `SeedCapacity` ("the initial provisioned capacity units") when the
13428
- * template supplies it, else `MinCapacity`.
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 toFiniteNumber(autoScaling["SeedCapacity"]) ?? toFiniteNumber(autoScaling["MinCapacity"]);
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 Seed-before-Min precedence as the
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 toFiniteNumber(autoScaling["SeedCapacity"]) ?? toFiniteNumber(autoScaling["MinCapacity"]);
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: toSdkOpenTableFormatInput(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
- * Convert the CFn `AWS::Glue::Table.OpenTableFormatInput` blob to the SDK
33298
- * `OpenTableFormatInput` shape.
33299
- *
33300
- * Every key matches the SDK model except one: CFn's
33301
- * `IcebergInput.IcebergTableInput` is the SDK's
33302
- * `IcebergInput.CreateIcebergTableInput` (@aws-sdk/client-glue `models_1.d.ts`
33303
- * `IcebergInput`). The AWS SDK v3 serializer drops unknown members, so leaving
33304
- * the CFn spelling in place silently discarded the ENTIRE Iceberg table spec
33305
- * (`Location` / `Schema` / `PartitionSpec` / `WriteOrder` / `Properties`) while
33306
- * `CreateTable` still reported success. The renamed object's own members match
33307
- * CFn 1:1, so this is a single key rename.
33308
- *
33309
- * Non-object inputs (an unresolved intrinsic) pass through untouched so AWS
33310
- * surfaces the real validation error.
33311
- */
33312
- function toSdkOpenTableFormatInput(input) {
33313
- const iceberg = input["IcebergInput"];
33314
- if (typeof iceberg !== "object" || iceberg === null || Array.isArray(iceberg) || !("IcebergTableInput" in iceberg)) return input;
33315
- const { IcebergTableInput: icebergTableInput, ...rest } = iceberg;
33316
- return {
33317
- ...input,
33318
- IcebergInput: {
33319
- ...rest,
33320
- CreateIcebergTableInput: icebergTableInput
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.19");
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;