@go-to-k/cdkd 0.108.0 → 0.109.0

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
@@ -35,7 +35,7 @@ import { Command, Option } from "commander";
35
35
  import { writeFileSync as writeFileSync$1 } from "fs";
36
36
  import { join as join$1 } from "path";
37
37
  import * as zlib from "node:zlib";
38
- import { ApplicationAutoScalingClient, DescribeScalableTargetsCommand, DescribeScalingPoliciesCommand } from "@aws-sdk/client-application-auto-scaling";
38
+ import { ApplicationAutoScalingClient, DeleteScalingPolicyCommand, DeregisterScalableTargetCommand, DescribeScalableTargetsCommand, DescribeScalingPoliciesCommand, PutScalingPolicyCommand, RegisterScalableTargetCommand } from "@aws-sdk/client-application-auto-scaling";
39
39
  import { ApiGatewayV2Client, CreateApiCommand, CreateAuthorizerCommand as CreateAuthorizerCommand$1, CreateIntegrationCommand, CreateRouteCommand as CreateRouteCommand$1, CreateStageCommand as CreateStageCommand$1, DeleteApiCommand, DeleteAuthorizerCommand as DeleteAuthorizerCommand$1, DeleteIntegrationCommand, DeleteRouteCommand as DeleteRouteCommand$1, DeleteStageCommand as DeleteStageCommand$1, GetApiCommand, GetApisCommand, GetAuthorizerCommand as GetAuthorizerCommand$1, GetIntegrationCommand, GetRouteCommand, GetStageCommand as GetStageCommand$1, NotFoundException as NotFoundException$3, UpdateApiCommand, UpdateAuthorizerCommand as UpdateAuthorizerCommand$1, UpdateIntegrationCommand, UpdateRouteCommand, UpdateStageCommand as UpdateStageCommand$1 } from "@aws-sdk/client-apigatewayv2";
40
40
  import { CreateStateMachineCommand, DeleteStateMachineCommand, DescribeStateMachineCommand, ListStateMachinesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$9, SFNClient, StateMachineDoesNotExist, TagResourceCommand as TagResourceCommand$10, UntagResourceCommand as UntagResourceCommand$9, UpdateStateMachineCommand } from "@aws-sdk/client-sfn";
41
41
  import { CreateClusterCommand, CreateServiceCommand, DeleteClusterCommand, DeleteServiceCommand, DeregisterTaskDefinitionCommand, DescribeClustersCommand, DescribeServicesCommand, DescribeTaskDefinitionCommand, ECSClient, ListClustersCommand, ListServicesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$10, PutClusterCapacityProvidersCommand, RegisterTaskDefinitionCommand, TagResourceCommand as TagResourceCommand$11, UntagResourceCommand as UntagResourceCommand$10, UpdateClusterCommand, UpdateServiceCommand } from "@aws-sdk/client-ecs";
@@ -7984,6 +7984,20 @@ var DynamoDBGlobalTableProvider = class {
7984
7984
  return client;
7985
7985
  }
7986
7986
  /**
7987
+ * Lazy-init + cache the local-region `ApplicationAutoScalingClient`.
7988
+ * Previously `applyAutoScalingDiff` constructed a fresh client per
7989
+ * call when no `client` arg was passed, leaking SDK clients (each
7990
+ * holds its own HTTP agent) on multi-stack runs with many
7991
+ * GlobalTables (PR #403 review minor #4).
7992
+ */
7993
+ localAutoScalingClient;
7994
+ async getLocalAutoScalingClient() {
7995
+ if (this.localAutoScalingClient) return this.localAutoScalingClient;
7996
+ const region = await this.dynamoDBClient.config.region() ?? "";
7997
+ this.localAutoScalingClient = new ApplicationAutoScalingClient({ region });
7998
+ return this.localAutoScalingClient;
7999
+ }
8000
+ /**
7987
8001
  * Construct the regional table ARN for a cross-region replica of a
7988
8002
  * GlobalTable. AWS replicates the same `TableName` across every
7989
8003
  * replica region, with each replica's ARN differing only in the
@@ -8237,10 +8251,19 @@ var DynamoDBGlobalTableProvider = class {
8237
8251
  await this.dynamoDBClient.send(new UpdateTableCommand(billingUpdate));
8238
8252
  await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
8239
8253
  }
8254
+ const writeAutoScalingOld = (previousProperties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
8255
+ const writeAutoScalingNew = (properties["WriteProvisionedThroughputSettings"] ?? {})["WriteCapacityAutoScalingSettings"];
8256
+ const effectiveNewAutoScaling = oldBilling === "PROVISIONED" && newBilling === "PAY_PER_REQUEST" ? void 0 : writeAutoScalingNew;
8257
+ if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(writeAutoScalingOld, effectiveNewAutoScaling)) await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", writeAutoScalingOld, effectiveNewAutoScaling);
8240
8258
  const replicaDiff = diffReplicas(previousProperties["Replicas"] ?? [], properties["Replicas"] ?? []);
8241
8259
  for (const replica of replicaDiff.removed) {
8242
8260
  const region = replica["Region"];
8243
8261
  if (!region || region === currentRegion) continue;
8262
+ const removedReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
8263
+ if (removedReadAutoScaling) {
8264
+ const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
8265
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", removedReadAutoScaling, void 0, regionalAutoScalingClient);
8266
+ }
8244
8267
  await this.dynamoDBClient.send(new UpdateTableCommand({
8245
8268
  TableName: physicalId,
8246
8269
  ReplicaUpdates: [{ Delete: { RegionName: region } }]
@@ -8251,11 +8274,17 @@ var DynamoDBGlobalTableProvider = class {
8251
8274
  const region = replica["Region"];
8252
8275
  if (!region || region === currentRegion) continue;
8253
8276
  await this.addReplica(physicalId, replica, region, logicalId);
8277
+ const newReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
8278
+ if (newBilling === "PROVISIONED" && newReadAutoScaling) {
8279
+ const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
8280
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", void 0, newReadAutoScaling, regionalAutoScalingClient);
8281
+ }
8254
8282
  }
8255
8283
  for (const replica of replicaDiff.modified) {
8256
8284
  const region = replica["Region"];
8257
8285
  if (!region || region === currentRegion) continue;
8258
- const oldReplicaTags = (previousProperties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["Tags"];
8286
+ const oldReplica = (previousProperties["Replicas"] ?? []).find((r) => r["Region"] === region);
8287
+ const oldReplicaTags = oldReplica?.["Tags"];
8259
8288
  const newReplicaTags = replica["Tags"];
8260
8289
  if (!deepEqual$1(oldReplicaTags, newReplicaTags)) if (tableArn) {
8261
8290
  const replicaArn = this.replicaArnForRegion(tableArn, region);
@@ -8267,6 +8296,13 @@ var DynamoDBGlobalTableProvider = class {
8267
8296
  }
8268
8297
  else this.logger.warn(`Could not derive replica ARN for region ${region} from ${tableArn} — skipping Tags propagation for ${physicalId}`);
8269
8298
  } else this.logger.warn(`Local DescribeTable returned no TableArn — cannot propagate Tags to cross-region replica ${region} of ${physicalId}`);
8299
+ const oldReadAutoScaling = (oldReplica?.["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
8300
+ const newReadAutoScaling = (replica["ReadProvisionedThroughputSettings"] ?? {})["ReadCapacityAutoScalingSettings"];
8301
+ const effectiveNewReadAutoScaling = newBilling === "PAY_PER_REQUEST" ? void 0 : newReadAutoScaling;
8302
+ if ((newBilling === "PROVISIONED" || oldBilling === "PROVISIONED") && !deepEqual$1(oldReadAutoScaling, effectiveNewReadAutoScaling)) {
8303
+ const regionalAutoScalingClient = this.getRegionalAutoScalingClient(region);
8304
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", oldReadAutoScaling, effectiveNewReadAutoScaling, regionalAutoScalingClient);
8305
+ }
8270
8306
  const updateAction = { RegionName: region };
8271
8307
  if (replica["KMSMasterKeyId"] !== void 0) updateAction.KMSMasterKeyId = replica["KMSMasterKeyId"];
8272
8308
  if (replica["GlobalSecondaryIndexes"]) updateAction.GlobalSecondaryIndexes = replica["GlobalSecondaryIndexes"];
@@ -8405,6 +8441,127 @@ var DynamoDBGlobalTableProvider = class {
8405
8441
  }
8406
8442
  }
8407
8443
  /**
8444
+ * Apply an application-autoscaling diff for one (tableName, dimension)
8445
+ * pair (Issue #402 / PR closing Issue #395's deferred items).
8446
+ *
8447
+ * Dimension is either:
8448
+ * - `dynamodb:table:WriteCapacityUnits` (table-level write capacity),
8449
+ * paired with the local-region autoscaling client.
8450
+ * - `dynamodb:table:ReadCapacityUnits` (per-replica read capacity),
8451
+ * paired with a region-scoped autoscaling client returned by
8452
+ * `getRegionalAutoScalingClient`.
8453
+ *
8454
+ * Diff semantics (idempotent upsert, per Issue #402 spec):
8455
+ * - new auto-scaling settings present (Min/Max + TargetValue) →
8456
+ * `RegisterScalableTarget` (upsert) + `PutScalingPolicy` (upsert).
8457
+ * AWS's `RegisterScalableTarget` accepts no-op Min/Max changes
8458
+ * silently, and `PutScalingPolicy` is idempotent on the same
8459
+ * policy name (`DynamoDB{Write,Read}CapacityUtilization:table/<name>`).
8460
+ * - old auto-scaling settings present but new ones absent (= null) →
8461
+ * `DeleteScalingPolicy` + `DeregisterScalableTarget`. AWS rejects
8462
+ * `DeregisterScalableTarget` on a still-policy-attached target,
8463
+ * so the policy must be deleted FIRST.
8464
+ * - neither side has auto-scaling settings → no-op.
8465
+ *
8466
+ * **Best-effort**: any AWS error is logged at WARN with the per-API
8467
+ * recovery command and the deploy continues — auto-scaling drift is
8468
+ * recoverable on the next deploy, and an aborted deploy is much worse
8469
+ * UX than a transient auto-scaling miss. This matches the cross-region
8470
+ * Tags propagation contract (PR #393).
8471
+ *
8472
+ * **Policy naming** matches AWS's CDK / console default:
8473
+ * `DynamoDBWriteCapacityUtilization:table/<table-name>` (write)
8474
+ * `DynamoDBReadCapacityUtilization:table/<table-name>` (read)
8475
+ * so re-imports against a console-created target also match.
8476
+ *
8477
+ * **`SeedCapacity` is intentionally NOT forwarded** — it is a CFn
8478
+ * create-only field with no corresponding `RegisterScalableTarget`
8479
+ * surface; `readAutoScalingSettings` also explicitly skips it.
8480
+ */
8481
+ async applyAutoScalingDiff(tableName, dimension, oldSettings, newSettings, client) {
8482
+ const isWrite = dimension === "dynamodb:table:WriteCapacityUnits";
8483
+ const policyName = isWrite ? `DynamoDBWriteCapacityUtilization:table/${tableName}` : `DynamoDBReadCapacityUtilization:table/${tableName}`;
8484
+ const metricType = isWrite ? "DynamoDBWriteCapacityUtilization" : "DynamoDBReadCapacityUtilization";
8485
+ const resourceId = `table/${tableName}`;
8486
+ const asClient = client ?? await this.getLocalAutoScalingClient();
8487
+ const oldEnabled = oldSettings !== void 0 && oldSettings !== null;
8488
+ const newEnabled = newSettings !== void 0 && newSettings !== null;
8489
+ if (!oldEnabled && !newEnabled) return;
8490
+ if (newEnabled) {
8491
+ const minCapacity = Number(newSettings["MinCapacity"] ?? 0);
8492
+ const maxCapacity = Number(newSettings["MaxCapacity"] ?? 0);
8493
+ if (!Number.isFinite(minCapacity) || !Number.isFinite(maxCapacity)) {
8494
+ this.logger.warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be numbers, got ${String(newSettings["MinCapacity"])} / ${String(newSettings["MaxCapacity"])}`);
8495
+ return;
8496
+ }
8497
+ try {
8498
+ await asClient.send(new RegisterScalableTargetCommand({
8499
+ ServiceNamespace: "dynamodb",
8500
+ ResourceId: resourceId,
8501
+ ScalableDimension: dimension,
8502
+ MinCapacity: minCapacity,
8503
+ MaxCapacity: maxCapacity
8504
+ }));
8505
+ } catch (err) {
8506
+ 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}`);
8507
+ return;
8508
+ }
8509
+ const tttCfg = newSettings["TargetTrackingScalingPolicyConfiguration"] ?? {};
8510
+ const targetValue = Number(tttCfg["TargetValue"]);
8511
+ if (!Number.isFinite(targetValue)) {
8512
+ this.logger.warn(`Auto-scaling target registered on ${tableName} (${dimension}) but TargetValue is missing or non-numeric — skipping PutScalingPolicy. Provide TargetTrackingScalingPolicyConfiguration.TargetValue in the template.`);
8513
+ return;
8514
+ }
8515
+ const targetTrackingConfig = {
8516
+ PredefinedMetricSpecification: { PredefinedMetricType: metricType },
8517
+ TargetValue: targetValue
8518
+ };
8519
+ if (tttCfg["ScaleInCooldown"] !== void 0) targetTrackingConfig["ScaleInCooldown"] = Number(tttCfg["ScaleInCooldown"]);
8520
+ if (tttCfg["ScaleOutCooldown"] !== void 0) targetTrackingConfig["ScaleOutCooldown"] = Number(tttCfg["ScaleOutCooldown"]);
8521
+ if (tttCfg["DisableScaleIn"] !== void 0) targetTrackingConfig["DisableScaleIn"] = Boolean(tttCfg["DisableScaleIn"]);
8522
+ try {
8523
+ await asClient.send(new PutScalingPolicyCommand({
8524
+ PolicyName: policyName,
8525
+ ServiceNamespace: "dynamodb",
8526
+ ResourceId: resourceId,
8527
+ ScalableDimension: dimension,
8528
+ PolicyType: "TargetTrackingScaling",
8529
+ TargetTrackingScalingPolicyConfiguration: targetTrackingConfig
8530
+ }));
8531
+ this.logger.debug(`Upserted auto-scaling policy ${policyName} on ${tableName} (${dimension})`);
8532
+ } catch (err) {
8533
+ 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`);
8534
+ }
8535
+ return;
8536
+ }
8537
+ const isObjectNotFound = (err) => {
8538
+ if (!(err instanceof Error)) return false;
8539
+ const name = err.name ?? "";
8540
+ const msg = err.message ?? "";
8541
+ return name === "ObjectNotFoundException" || msg.includes("No scaling policy found") || msg.includes("No scalable target found");
8542
+ };
8543
+ try {
8544
+ await asClient.send(new DeleteScalingPolicyCommand({
8545
+ PolicyName: policyName,
8546
+ ServiceNamespace: "dynamodb",
8547
+ ResourceId: resourceId,
8548
+ ScalableDimension: dimension
8549
+ }));
8550
+ } catch (err) {
8551
+ if (!isObjectNotFound(err)) this.logger.warn(`Could not delete auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling delete-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
8552
+ }
8553
+ try {
8554
+ await asClient.send(new DeregisterScalableTargetCommand({
8555
+ ServiceNamespace: "dynamodb",
8556
+ ResourceId: resourceId,
8557
+ ScalableDimension: dimension
8558
+ }));
8559
+ this.logger.debug(`Deregistered auto-scaling target ${resourceId} (${dimension})`);
8560
+ } catch (err) {
8561
+ if (!isObjectNotFound(err)) this.logger.warn(`Could not deregister auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling deregister-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
8562
+ }
8563
+ }
8564
+ /**
8408
8565
  * Delete a DynamoDB Global Table.
8409
8566
  *
8410
8567
  * Order is load-bearing:
@@ -8450,6 +8607,7 @@ var DynamoDBGlobalTableProvider = class {
8450
8607
  const region = replica.RegionName;
8451
8608
  if (!region || region === currentRegion) continue;
8452
8609
  try {
8610
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", {}, void 0, this.getRegionalAutoScalingClient(region));
8453
8611
  await this.dynamoDBClient.send(new UpdateTableCommand({
8454
8612
  TableName: physicalId,
8455
8613
  ReplicaUpdates: [{ Delete: { RegionName: region } }]
@@ -8459,6 +8617,9 @@ var DynamoDBGlobalTableProvider = class {
8459
8617
  if (!(replicaErr instanceof ResourceNotFoundException$1)) throw replicaErr;
8460
8618
  }
8461
8619
  }
8620
+ const localAsClient = await this.getLocalAutoScalingClient();
8621
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:ReadCapacityUnits", {}, void 0, localAsClient);
8622
+ await this.applyAutoScalingDiff(physicalId, "dynamodb:table:WriteCapacityUnits", {}, void 0, localAsClient);
8462
8623
  } catch (describeErr) {
8463
8624
  if (!(describeErr instanceof ResourceNotFoundException$1)) {
8464
8625
  const cause = describeErr instanceof Error ? describeErr : void 0;
@@ -45026,7 +45187,7 @@ function reorderArgs(argv) {
45026
45187
  */
45027
45188
  async function main() {
45028
45189
  const program = new Command();
45029
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.108.0");
45190
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.109.0");
45030
45191
  program.addCommand(createBootstrapCommand());
45031
45192
  program.addCommand(createSynthCommand());
45032
45193
  program.addCommand(createListCommand());