@go-to-k/cdkd 0.103.2 → 0.105.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 +620 -82
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { CreateTopicCommand, DeleteTopicCommand, GetSubscriptionAttributesComman
|
|
|
9
9
|
import { AddPermissionCommand, CreateEventSourceMappingCommand, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteEventSourceMappingCommand, DeleteFunctionCommand, DeleteFunctionUrlConfigCommand, DeleteLayerVersionCommand, GetEventSourceMappingCommand, GetFunctionCommand, GetFunctionUrlConfigCommand, GetLayerVersionByArnCommand, GetPolicyCommand, LambdaClient, ListFunctionsCommand, ListLayersCommand, ListTagsCommand, PublishLayerVersionCommand, RemovePermissionCommand, ResourceNotFoundException, TagResourceCommand as TagResourceCommand$1, UntagResourceCommand as UntagResourceCommand$1, UpdateEventSourceMappingCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
|
|
10
10
|
import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
11
11
|
import { AssociateRouteTableCommand, AttachInternetGatewayCommand, AuthorizeSecurityGroupEgressCommand, AuthorizeSecurityGroupIngressCommand, CreateInternetGatewayCommand, CreateNatGatewayCommand, CreateNetworkAclCommand, CreateNetworkAclEntryCommand, CreateRouteCommand, CreateRouteTableCommand, CreateSecurityGroupCommand, CreateSubnetCommand, CreateTagsCommand, CreateVpcCommand, DeleteInternetGatewayCommand, DeleteNatGatewayCommand, DeleteNetworkAclCommand, DeleteNetworkAclEntryCommand, DeleteNetworkInterfaceCommand, DeleteRouteCommand, DeleteRouteTableCommand, DeleteSecurityGroupCommand, DeleteSubnetCommand, DeleteTagsCommand, DeleteVpcCommand, DescribeAvailabilityZonesCommand, DescribeInstanceAttributeCommand, DescribeInstancesCommand, DescribeInternetGatewaysCommand, DescribeNatGatewaysCommand, DescribeNetworkAclsCommand, DescribeNetworkInterfacesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVolumesCommand, DescribeVpcAttributeCommand, DescribeVpcsCommand, DetachInternetGatewayCommand, DisassociateRouteTableCommand, EC2Client, ModifyInstanceAttributeCommand, ModifySubnetAttributeCommand, ModifyVpcAttributeCommand, ReplaceNetworkAclAssociationCommand, RevokeSecurityGroupEgressCommand, RevokeSecurityGroupIngressCommand, RunInstancesCommand, TerminateInstancesCommand, waitUntilInstanceRunning, waitUntilInstanceTerminated, waitUntilNatGatewayAvailable, waitUntilNatGatewayDeleted } from "@aws-sdk/client-ec2";
|
|
12
|
-
import { CreateTableCommand, DeleteTableCommand, DescribeTableCommand, DynamoDBClient, ListTablesCommand, ListTagsOfResourceCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
|
|
12
|
+
import { CreateTableCommand, DeleteTableCommand, DescribeContinuousBackupsCommand, DescribeContributorInsightsCommand, DescribeKinesisStreamingDestinationCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DynamoDBClient, ListTablesCommand, ListTagsOfResourceCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
|
|
13
13
|
import { CloudFormationClient, CreateChangeSetCommand, DeleteChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, DescribeStackEventsCommand, DescribeStackResourcesCommand, DescribeStacksCommand, DescribeTypeCommand, ExecuteChangeSetCommand, GetTemplateCommand, UpdateStackCommand, waitUntilChangeSetCreateComplete, waitUntilStackDeleteComplete, waitUntilStackImportComplete, waitUntilStackUpdateComplete } from "@aws-sdk/client-cloudformation";
|
|
14
14
|
import { APIGatewayClient, CreateAuthorizerCommand, CreateDeploymentCommand, CreateResourceCommand, CreateStageCommand, DeleteAuthorizerCommand, DeleteDeploymentCommand, DeleteMethodCommand, DeleteResourceCommand, DeleteStageCommand, GetAccountCommand, GetAuthorizerCommand, GetDeploymentCommand, GetMethodCommand, GetResourceCommand, GetStageCommand, NotFoundException as NotFoundException$1, PutIntegrationCommand, PutIntegrationResponseCommand, PutMethodCommand, PutMethodResponseCommand, TagResourceCommand as TagResourceCommand$3, UntagResourceCommand as UntagResourceCommand$3, UpdateAccountCommand, UpdateAuthorizerCommand, UpdateMethodCommand, UpdateStageCommand } from "@aws-sdk/client-api-gateway";
|
|
15
15
|
import { CreateEventBusCommand, DeleteEventBusCommand, DeleteRuleCommand, DescribeEventBusCommand, DescribeRuleCommand, EventBridgeClient, ListEventBusesCommand, ListRulesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$1, ListTargetsByRuleCommand, PutRuleCommand, PutTargetsCommand, RemoveTargetsCommand, ResourceNotFoundException as ResourceNotFoundException$2, TagResourceCommand as TagResourceCommand$4, UntagResourceCommand as UntagResourceCommand$4, UpdateEventBusCommand } from "@aws-sdk/client-eventbridge";
|
|
@@ -35,6 +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, DescribeScalingPoliciesCommand } from "@aws-sdk/client-application-auto-scaling";
|
|
38
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";
|
|
39
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";
|
|
40
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";
|
|
@@ -7864,20 +7865,49 @@ var DynamoDBTableProvider = class {
|
|
|
7864
7865
|
* is the 2019.11.21 generation, which uses the regular DynamoDB CRUD API
|
|
7865
7866
|
* (`CreateTableCommand` + `UpdateTableCommand` with `ReplicaUpdates`).
|
|
7866
7867
|
*
|
|
7867
|
-
*
|
|
7868
|
-
* - `update()`
|
|
7869
|
-
*
|
|
7870
|
-
*
|
|
7871
|
-
* -
|
|
7872
|
-
*
|
|
7873
|
-
*
|
|
7868
|
+
* In-place update support (post-PR #384 follow-up):
|
|
7869
|
+
* - `update()` covers every mutable surface — Tags, DeletionProtection,
|
|
7870
|
+
* TableClass, SSE, StreamSpec, OnDemand throughput, BillingMode flip,
|
|
7871
|
+
* Replica add / remove / modify, GSI add / remove / modify, TTL toggle.
|
|
7872
|
+
* - The serialization is load-bearing: AWS's `UpdateTable` accepts only
|
|
7873
|
+
* ONE of `{BillingMode, ReplicaUpdates, GlobalSecondaryIndexUpdates}`
|
|
7874
|
+
* per call, so each category is its own SDK round-trip with a wait-for
|
|
7875
|
+
* -ACTIVE in between. Immutable property changes (TableName, KeySchema,
|
|
7876
|
+
* AttributeDefinitions removal, LocalSecondaryIndexes) throw
|
|
7877
|
+
* `ProvisioningError` naming the offending field — the deploy engine's
|
|
7878
|
+
* diff classification should catch these as REPLACEMENT before ever
|
|
7879
|
+
* calling `update()`, but the guard is defense-in-depth.
|
|
7874
7880
|
* - Per-replica drift (`ContributorInsightsSpecification` /
|
|
7875
|
-
* `PointInTimeRecoverySpecification` / `KinesisStreamSpecification`)
|
|
7876
|
-
*
|
|
7881
|
+
* `PointInTimeRecoverySpecification` / `KinesisStreamSpecification`)
|
|
7882
|
+
* is surfaced for BOTH the LOCAL replica AND cross-region replicas
|
|
7883
|
+
* via per-region SDK clients (cached in `regionalClientCache` for
|
|
7884
|
+
* the deploy run). Issue #389 lifted the v1 LOCAL-only limitation.
|
|
7885
|
+
* - Cross-region replica Tags propagation (Issue #389): when the
|
|
7886
|
+
* update path detects a Tags-only diff on a non-local replica,
|
|
7887
|
+
* cdkd resolves the replica's table ARN by swapping the region
|
|
7888
|
+
* segment of the local ARN and issues `TagResource` /
|
|
7889
|
+
* `UntagResource` against a per-region client.
|
|
7877
7890
|
*/
|
|
7878
7891
|
var DynamoDBGlobalTableProvider = class {
|
|
7879
7892
|
dynamoDBClient;
|
|
7880
7893
|
logger = getLogger().child("DynamoDBGlobalTableProvider");
|
|
7894
|
+
/**
|
|
7895
|
+
* Caches per-region `DynamoDBClient` instances for cross-region drift
|
|
7896
|
+
* reads (`readCurrentState`) and cross-region Tag propagation
|
|
7897
|
+
* (`update()`). Keyed by region string; reuses the default credential
|
|
7898
|
+
* chain. Lifetime is the provider instance — one deploy run.
|
|
7899
|
+
*/
|
|
7900
|
+
regionalClientCache = /* @__PURE__ */ new Map();
|
|
7901
|
+
/**
|
|
7902
|
+
* Caches `getAttribute(physicalId, attribute)` results for the lifetime
|
|
7903
|
+
* of this provider instance (one deploy run). Safe under the current
|
|
7904
|
+
* `update()` contract because `update()` cannot mid-deploy mutate
|
|
7905
|
+
* StreamArn / Arn / TableId — those are AWS-managed identifiers that
|
|
7906
|
+
* only change on REPLACEMENT (which destroys the provider instance).
|
|
7907
|
+
* If a future PR adds a stream toggle path that flips StreamArn on the
|
|
7908
|
+
* same physicalId, the cache must be invalidated on the matching
|
|
7909
|
+
* UpdateTable success.
|
|
7910
|
+
*/
|
|
7881
7911
|
attributeCache = /* @__PURE__ */ new Map();
|
|
7882
7912
|
handledProperties = new Map([["AWS::DynamoDB::GlobalTable", new Set([
|
|
7883
7913
|
"TableName",
|
|
@@ -7893,14 +7923,57 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
7893
7923
|
"TimeToLiveSpecification",
|
|
7894
7924
|
"WriteProvisionedThroughputSettings",
|
|
7895
7925
|
"WriteOnDemandThroughputSettings",
|
|
7896
|
-
"DeletionProtectionEnabled"
|
|
7897
|
-
"Tags"
|
|
7926
|
+
"DeletionProtectionEnabled"
|
|
7898
7927
|
])]]);
|
|
7899
7928
|
constructor() {
|
|
7900
7929
|
const awsClients = getAwsClients();
|
|
7901
7930
|
this.dynamoDBClient = awsClients.dynamoDB;
|
|
7902
7931
|
}
|
|
7903
7932
|
/**
|
|
7933
|
+
* Return a `DynamoDBClient` pinned to the given region, caching per
|
|
7934
|
+
* region for the lifetime of this provider instance. Uses the default
|
|
7935
|
+
* credential chain (env / shared config / IAM role) — no explicit
|
|
7936
|
+
* credential plumbing needed.
|
|
7937
|
+
*
|
|
7938
|
+
* Used by `readCurrentState` (cross-region per-replica sub-spec reads)
|
|
7939
|
+
* and `update()` (cross-region replica Tag propagation). Both code
|
|
7940
|
+
* paths fire region-scoped DynamoDB APIs (`DescribeContributorInsights`
|
|
7941
|
+
* / `DescribeContinuousBackups` / `DescribeKinesisStreamingDestination`
|
|
7942
|
+
* / `TagResource` / `UntagResource`) against the replica's region.
|
|
7943
|
+
*
|
|
7944
|
+
* The cache may return `this.dynamoDBClient` when `region` happens to
|
|
7945
|
+
* match the local client's region — in tests the local mock is
|
|
7946
|
+
* intercepted via `vi.mock('../../utils/aws-clients.js')` so reuse
|
|
7947
|
+
* is safe (and explicitly desired so the mock catches the call).
|
|
7948
|
+
*/
|
|
7949
|
+
getRegionalClient(region) {
|
|
7950
|
+
const cached = this.regionalClientCache.get(region);
|
|
7951
|
+
if (cached) return cached;
|
|
7952
|
+
const client = new DynamoDBClient({ region });
|
|
7953
|
+
this.regionalClientCache.set(region, client);
|
|
7954
|
+
return client;
|
|
7955
|
+
}
|
|
7956
|
+
/**
|
|
7957
|
+
* Construct the regional table ARN for a cross-region replica of a
|
|
7958
|
+
* GlobalTable. AWS replicates the same `TableName` across every
|
|
7959
|
+
* replica region, with each replica's ARN differing only in the
|
|
7960
|
+
* `:<region>:` segment. Cheaper than a second `DescribeTable` round-
|
|
7961
|
+
* trip on the regional client.
|
|
7962
|
+
*
|
|
7963
|
+
* Example:
|
|
7964
|
+
* local ARN: arn:aws:dynamodb:us-east-1:123:table/Foo
|
|
7965
|
+
* for eu-west-1 → arn:aws:dynamodb:eu-west-1:123:table/Foo
|
|
7966
|
+
*
|
|
7967
|
+
* Returns `undefined` when the local ARN is malformed (defensive —
|
|
7968
|
+
* downstream callers omit the offending operation rather than throw).
|
|
7969
|
+
*/
|
|
7970
|
+
replicaArnForRegion(localTableArn, targetRegion) {
|
|
7971
|
+
const segments = localTableArn.split(":");
|
|
7972
|
+
if (segments.length < 6) return void 0;
|
|
7973
|
+
segments[3] = targetRegion;
|
|
7974
|
+
return segments.join(":");
|
|
7975
|
+
}
|
|
7976
|
+
/**
|
|
7904
7977
|
* Create a DynamoDB Global Table (CDK TableV2).
|
|
7905
7978
|
*
|
|
7906
7979
|
* GlobalTable is built on the regular DynamoDB Table primitive: cdkd issues
|
|
@@ -7935,15 +8008,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
7935
8008
|
AttributeDefinitions: attributeDefinitions,
|
|
7936
8009
|
BillingMode: billingMode
|
|
7937
8010
|
};
|
|
7938
|
-
if (billingMode === "PROVISIONED")
|
|
7939
|
-
const writeAutoScaling = properties["WriteProvisionedThroughputSettings"]?.["WriteCapacityAutoScalingSettings"];
|
|
7940
|
-
const writeCapacity = Number(writeAutoScaling?.["MinCapacity"] ?? 5);
|
|
7941
|
-
const readAutoScaling = (replicas.find((r) => r["Region"] === currentRegion)?.["ReadProvisionedThroughputSettings"])?.["ReadCapacityAutoScalingSettings"];
|
|
7942
|
-
createParams.ProvisionedThroughput = {
|
|
7943
|
-
ReadCapacityUnits: Number(readAutoScaling?.["MinCapacity"] ?? 5),
|
|
7944
|
-
WriteCapacityUnits: writeCapacity
|
|
7945
|
-
};
|
|
7946
|
-
}
|
|
8011
|
+
if (billingMode === "PROVISIONED") createParams.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion);
|
|
7947
8012
|
const streamSpecInput = properties["StreamSpecification"];
|
|
7948
8013
|
const needsStream = replicas.some((r) => r["Region"] !== currentRegion) || replicas.length > 1;
|
|
7949
8014
|
if (streamSpecInput) createParams.StreamSpecification = {
|
|
@@ -7969,7 +8034,8 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
7969
8034
|
if (properties["TableClass"]) createParams.TableClass = properties["TableClass"];
|
|
7970
8035
|
const wodts = properties["WriteOnDemandThroughputSettings"];
|
|
7971
8036
|
if (wodts?.["MaxWriteRequestUnits"] !== void 0) createParams.OnDemandThroughput = { MaxWriteRequestUnits: Number(wodts["MaxWriteRequestUnits"]) };
|
|
7972
|
-
|
|
8037
|
+
const localReplicaTags = replicas.find((r) => r["Region"] === currentRegion)?.["Tags"];
|
|
8038
|
+
if (localReplicaTags && localReplicaTags.length > 0) createParams.Tags = localReplicaTags;
|
|
7973
8039
|
try {
|
|
7974
8040
|
await this.dynamoDBClient.send(new CreateTableCommand(createParams));
|
|
7975
8041
|
this.logger.debug(`CreateTable initiated for ${tableName}, waiting for ACTIVE`);
|
|
@@ -7978,11 +8044,11 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
7978
8044
|
throw new ProvisioningError(`Failed to create DynamoDB GlobalTable ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, tableName, cause);
|
|
7979
8045
|
}
|
|
7980
8046
|
try {
|
|
7981
|
-
const tableInfo = await this.waitForTableActive(tableName);
|
|
8047
|
+
const tableInfo = await this.waitForTableActive(tableName, logicalId);
|
|
7982
8048
|
for (const replica of replicas) {
|
|
7983
8049
|
const region = replica["Region"];
|
|
7984
8050
|
if (!region || region === currentRegion) continue;
|
|
7985
|
-
await this.addReplica(tableName, replica, region);
|
|
8051
|
+
await this.addReplica(tableName, replica, region, logicalId);
|
|
7986
8052
|
}
|
|
7987
8053
|
if (properties["TimeToLiveSpecification"]) {
|
|
7988
8054
|
const ttl = properties["TimeToLiveSpecification"];
|
|
@@ -8009,6 +8075,21 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8009
8075
|
} catch (wiringError) {
|
|
8010
8076
|
this.logger.warn(`Wiring failed after CreateTable for ${tableName}; attempting best-effort cleanup`);
|
|
8011
8077
|
try {
|
|
8078
|
+
const replicasForCleanup = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas ?? [];
|
|
8079
|
+
for (const replica of replicasForCleanup) {
|
|
8080
|
+
const region = replica.RegionName;
|
|
8081
|
+
if (!region || region === currentRegion) continue;
|
|
8082
|
+
try {
|
|
8083
|
+
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
8084
|
+
TableName: tableName,
|
|
8085
|
+
ReplicaUpdates: [{ Delete: { RegionName: region } }]
|
|
8086
|
+
}));
|
|
8087
|
+
await this.waitForReplicaGone(tableName, region, logicalId);
|
|
8088
|
+
} catch (replicaCleanupErr) {
|
|
8089
|
+
const msg = replicaCleanupErr instanceof Error ? replicaCleanupErr.message : String(replicaCleanupErr);
|
|
8090
|
+
this.logger.warn(`Partial-create cleanup: failed to drop replica ${region} on ${tableName}: ${msg}. Run: aws dynamodb update-table --table-name ${tableName} --replica-updates 'Delete={RegionName=${region}}' --region ${currentRegion}`);
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8012
8093
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: tableName }));
|
|
8013
8094
|
} catch (cleanupErr) {
|
|
8014
8095
|
const cleanupMsg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
@@ -8025,7 +8106,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8025
8106
|
* Capped at 10 minutes per replica (AWS Replica provisioning typically
|
|
8026
8107
|
* takes 1–5 min).
|
|
8027
8108
|
*/
|
|
8028
|
-
async addReplica(tableName, replica, region) {
|
|
8109
|
+
async addReplica(tableName, replica, region, logicalId) {
|
|
8029
8110
|
const create = { RegionName: region };
|
|
8030
8111
|
if (replica["KMSMasterKeyId"]) create.KMSMasterKeyId = replica["KMSMasterKeyId"];
|
|
8031
8112
|
if (replica["GlobalSecondaryIndexes"]) create.GlobalSecondaryIndexes = replica["GlobalSecondaryIndexes"];
|
|
@@ -8035,19 +8116,263 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8035
8116
|
TableName: tableName,
|
|
8036
8117
|
ReplicaUpdates: replicaUpdates
|
|
8037
8118
|
}));
|
|
8038
|
-
await this.waitForReplicaActive(tableName, region);
|
|
8119
|
+
await this.waitForReplicaActive(tableName, region, logicalId);
|
|
8120
|
+
}
|
|
8121
|
+
/**
|
|
8122
|
+
* Update a DynamoDB Global Table in place.
|
|
8123
|
+
*
|
|
8124
|
+
* AWS-side state-machine constraint: `UpdateTable` accepts only ONE of
|
|
8125
|
+
* `{BillingMode, ReplicaUpdates, GlobalSecondaryIndexUpdates}` per call,
|
|
8126
|
+
* so each category must serialize into its own SDK round-trip with a
|
|
8127
|
+
* `waitForTableActiveAfterUpdate` between every step. Order:
|
|
8128
|
+
* 1. Wait for current ACTIVE (defensive).
|
|
8129
|
+
* 2. Tags diff (TagResource / UntagResource — no wait needed).
|
|
8130
|
+
* 3. Non-conflicting flat fields (DeletionProtectionEnabled / TableClass
|
|
8131
|
+
* / SSESpecification / StreamSpecification / OnDemandThroughput)
|
|
8132
|
+
* in one combined `UpdateTableCommand`. Wait ACTIVE.
|
|
8133
|
+
* 4. BillingMode flip (separate UpdateTable). Wait ACTIVE.
|
|
8134
|
+
* 5. Replica diff (serial per Create / Update / Delete). Wait ACTIVE
|
|
8135
|
+
* after each.
|
|
8136
|
+
* 6. GSI diff (serial per Create / Update / Delete; new GSIs may need
|
|
8137
|
+
* additional AttributeDefinitions). Wait ACTIVE after each.
|
|
8138
|
+
* 7. TimeToLiveSpecification toggle.
|
|
8139
|
+
*
|
|
8140
|
+
* Immutable properties (TableName / KeySchema / AttributeDefinitions
|
|
8141
|
+
* removals / LocalSecondaryIndexes changes) throw `ProvisioningError`
|
|
8142
|
+
* naming the offending field — the deploy engine's diff classifier
|
|
8143
|
+
* should catch these as REPLACEMENT before ever calling `update()`,
|
|
8144
|
+
* but the guard is defense-in-depth.
|
|
8145
|
+
*/
|
|
8146
|
+
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
8147
|
+
this.logger.debug(`Updating DynamoDB GlobalTable ${logicalId}: ${physicalId}`);
|
|
8148
|
+
if (properties["TableName"] !== void 0 && previousProperties["TableName"] !== void 0 && properties["TableName"] !== previousProperties["TableName"]) throw new ProvisioningError(`TableName is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
|
|
8149
|
+
if (properties["KeySchema"] !== void 0 && previousProperties["KeySchema"] !== void 0 && !deepEqual$1(properties["KeySchema"], previousProperties["KeySchema"])) throw new ProvisioningError(`KeySchema is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
|
|
8150
|
+
if (properties["LocalSecondaryIndexes"] !== void 0 && previousProperties["LocalSecondaryIndexes"] !== void 0 && !deepEqual$1(properties["LocalSecondaryIndexes"], previousProperties["LocalSecondaryIndexes"])) throw new ProvisioningError(`LocalSecondaryIndexes is immutable on AWS::DynamoDB::GlobalTable; replacement required (deploy with --replace, or destroy + redeploy)`, resourceType, logicalId, physicalId);
|
|
8151
|
+
const oldAttrs = previousProperties["AttributeDefinitions"] ?? [];
|
|
8152
|
+
const newAttrs = properties["AttributeDefinitions"] ?? [];
|
|
8153
|
+
const removedAttrs = oldAttrs.filter((o) => !newAttrs.some((n) => n.AttributeName === o.AttributeName));
|
|
8154
|
+
if (removedAttrs.length > 0) throw new ProvisioningError(`AttributeDefinitions removals are immutable on AWS::DynamoDB::GlobalTable (offenders: ${removedAttrs.map((a) => a.AttributeName).join(", ")}); replacement required`, resourceType, logicalId, physicalId);
|
|
8155
|
+
const currentRegion = await this.dynamoDBClient.config.region() ?? "";
|
|
8156
|
+
try {
|
|
8157
|
+
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
8158
|
+
const tableArn = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table?.TableArn;
|
|
8159
|
+
const extractLocalTags = (props) => {
|
|
8160
|
+
return (props["Replicas"] ?? []).find((r) => r["Region"] === currentRegion)?.["Tags"];
|
|
8161
|
+
};
|
|
8162
|
+
if (tableArn) await this.applyTagDiff(tableArn, extractLocalTags(previousProperties), extractLocalTags(properties));
|
|
8163
|
+
const flatUpdate = { TableName: physicalId };
|
|
8164
|
+
let flatChanged = false;
|
|
8165
|
+
if (properties["DeletionProtectionEnabled"] !== previousProperties["DeletionProtectionEnabled"]) {
|
|
8166
|
+
flatUpdate.DeletionProtectionEnabled = Boolean(properties["DeletionProtectionEnabled"] ?? false);
|
|
8167
|
+
flatChanged = true;
|
|
8168
|
+
}
|
|
8169
|
+
if (properties["TableClass"] !== void 0 && properties["TableClass"] !== previousProperties["TableClass"]) {
|
|
8170
|
+
flatUpdate.TableClass = properties["TableClass"];
|
|
8171
|
+
flatChanged = true;
|
|
8172
|
+
}
|
|
8173
|
+
if (properties["SSESpecification"] !== void 0 && !deepEqual$1(properties["SSESpecification"], previousProperties["SSESpecification"])) {
|
|
8174
|
+
const sse = properties["SSESpecification"];
|
|
8175
|
+
flatUpdate.SSESpecification = {
|
|
8176
|
+
Enabled: sse["SSEEnabled"] !== void 0 ? Boolean(sse["SSEEnabled"]) : true,
|
|
8177
|
+
...sse["SSEType"] !== void 0 && { SSEType: sse["SSEType"] }
|
|
8178
|
+
};
|
|
8179
|
+
flatChanged = true;
|
|
8180
|
+
}
|
|
8181
|
+
if (properties["StreamSpecification"] !== void 0 && !deepEqual$1(properties["StreamSpecification"], previousProperties["StreamSpecification"])) {
|
|
8182
|
+
flatUpdate.StreamSpecification = {
|
|
8183
|
+
StreamEnabled: true,
|
|
8184
|
+
StreamViewType: properties["StreamSpecification"]["StreamViewType"]
|
|
8185
|
+
};
|
|
8186
|
+
flatChanged = true;
|
|
8187
|
+
}
|
|
8188
|
+
if (!deepEqual$1(properties["WriteOnDemandThroughputSettings"], previousProperties["WriteOnDemandThroughputSettings"])) {
|
|
8189
|
+
const wodts = properties["WriteOnDemandThroughputSettings"];
|
|
8190
|
+
if (wodts?.["MaxWriteRequestUnits"] !== void 0) {
|
|
8191
|
+
flatUpdate.OnDemandThroughput = { MaxWriteRequestUnits: Number(wodts["MaxWriteRequestUnits"]) };
|
|
8192
|
+
flatChanged = true;
|
|
8193
|
+
}
|
|
8194
|
+
}
|
|
8195
|
+
if (flatChanged) {
|
|
8196
|
+
await this.dynamoDBClient.send(new UpdateTableCommand(flatUpdate));
|
|
8197
|
+
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
8198
|
+
}
|
|
8199
|
+
const oldBilling = previousProperties["BillingMode"] ?? "PAY_PER_REQUEST";
|
|
8200
|
+
const newBilling = properties["BillingMode"] ?? "PAY_PER_REQUEST";
|
|
8201
|
+
if (oldBilling !== newBilling) {
|
|
8202
|
+
const billingUpdate = {
|
|
8203
|
+
TableName: physicalId,
|
|
8204
|
+
BillingMode: newBilling
|
|
8205
|
+
};
|
|
8206
|
+
if (newBilling === "PROVISIONED") billingUpdate.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion);
|
|
8207
|
+
await this.dynamoDBClient.send(new UpdateTableCommand(billingUpdate));
|
|
8208
|
+
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
8209
|
+
}
|
|
8210
|
+
const replicaDiff = diffReplicas(previousProperties["Replicas"] ?? [], properties["Replicas"] ?? []);
|
|
8211
|
+
for (const replica of replicaDiff.removed) {
|
|
8212
|
+
const region = replica["Region"];
|
|
8213
|
+
if (!region || region === currentRegion) continue;
|
|
8214
|
+
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
8215
|
+
TableName: physicalId,
|
|
8216
|
+
ReplicaUpdates: [{ Delete: { RegionName: region } }]
|
|
8217
|
+
}));
|
|
8218
|
+
await this.waitForReplicaGone(physicalId, region, logicalId);
|
|
8219
|
+
}
|
|
8220
|
+
for (const replica of replicaDiff.added) {
|
|
8221
|
+
const region = replica["Region"];
|
|
8222
|
+
if (!region || region === currentRegion) continue;
|
|
8223
|
+
await this.addReplica(physicalId, replica, region, logicalId);
|
|
8224
|
+
}
|
|
8225
|
+
for (const replica of replicaDiff.modified) {
|
|
8226
|
+
const region = replica["Region"];
|
|
8227
|
+
if (!region || region === currentRegion) continue;
|
|
8228
|
+
const oldReplicaTags = (previousProperties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["Tags"];
|
|
8229
|
+
const newReplicaTags = replica["Tags"];
|
|
8230
|
+
if (!deepEqual$1(oldReplicaTags, newReplicaTags)) if (tableArn) {
|
|
8231
|
+
const replicaArn = this.replicaArnForRegion(tableArn, region);
|
|
8232
|
+
if (replicaArn) try {
|
|
8233
|
+
const regionalClient = this.getRegionalClient(region);
|
|
8234
|
+
await this.applyTagDiffOnClient(regionalClient, replicaArn, oldReplicaTags, newReplicaTags);
|
|
8235
|
+
} catch (tagErr) {
|
|
8236
|
+
this.logger.warn(`Could not apply Tags diff to cross-region replica ${region} of ${physicalId}: ${tagErr instanceof Error ? tagErr.message : String(tagErr)}. The replica's Tags state will surface as drift until the next successful deploy.`);
|
|
8237
|
+
}
|
|
8238
|
+
else this.logger.warn(`Could not derive replica ARN for region ${region} from ${tableArn} — skipping Tags propagation for ${physicalId}`);
|
|
8239
|
+
} else this.logger.warn(`Local DescribeTable returned no TableArn — cannot propagate Tags to cross-region replica ${region} of ${physicalId}`);
|
|
8240
|
+
const updateAction = { RegionName: region };
|
|
8241
|
+
if (replica["KMSMasterKeyId"] !== void 0) updateAction.KMSMasterKeyId = replica["KMSMasterKeyId"];
|
|
8242
|
+
if (replica["GlobalSecondaryIndexes"]) updateAction.GlobalSecondaryIndexes = replica["GlobalSecondaryIndexes"];
|
|
8243
|
+
if (replica["TableClassOverride"]) updateAction.TableClassOverride = replica["TableClassOverride"];
|
|
8244
|
+
if (!(updateAction.KMSMasterKeyId !== void 0 || updateAction.GlobalSecondaryIndexes !== void 0 || updateAction.TableClassOverride !== void 0)) {
|
|
8245
|
+
this.logger.debug(`Cross-region replica ${region} of ${physicalId}: only Tags-style changes detected; UpdateReplica skipped (AWS rejects empty Update actions). Tags propagation handled above via per-region client.`);
|
|
8246
|
+
continue;
|
|
8247
|
+
}
|
|
8248
|
+
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
8249
|
+
TableName: physicalId,
|
|
8250
|
+
ReplicaUpdates: [{ Update: updateAction }]
|
|
8251
|
+
}));
|
|
8252
|
+
await this.waitForReplicaActive(physicalId, region, logicalId);
|
|
8253
|
+
}
|
|
8254
|
+
const gsiDiff = diffGlobalSecondaryIndexes(previousProperties["GlobalSecondaryIndexes"] ?? [], properties["GlobalSecondaryIndexes"] ?? []);
|
|
8255
|
+
for (const gsi of gsiDiff.removed) {
|
|
8256
|
+
if (!gsi.IndexName) continue;
|
|
8257
|
+
const gsiUpdate = { Delete: { IndexName: gsi.IndexName } };
|
|
8258
|
+
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
8259
|
+
TableName: physicalId,
|
|
8260
|
+
GlobalSecondaryIndexUpdates: [gsiUpdate]
|
|
8261
|
+
}));
|
|
8262
|
+
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
8263
|
+
}
|
|
8264
|
+
for (const gsi of gsiDiff.added) {
|
|
8265
|
+
if (!gsi.IndexName || !gsi.KeySchema || !gsi.Projection) continue;
|
|
8266
|
+
const gsiUpdate = { Create: {
|
|
8267
|
+
IndexName: gsi.IndexName,
|
|
8268
|
+
KeySchema: gsi.KeySchema,
|
|
8269
|
+
Projection: gsi.Projection,
|
|
8270
|
+
...gsi.ProvisionedThroughput && { ProvisionedThroughput: gsi.ProvisionedThroughput },
|
|
8271
|
+
...gsi.OnDemandThroughput && { OnDemandThroughput: gsi.OnDemandThroughput }
|
|
8272
|
+
} };
|
|
8273
|
+
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
8274
|
+
TableName: physicalId,
|
|
8275
|
+
AttributeDefinitions: newAttrs,
|
|
8276
|
+
GlobalSecondaryIndexUpdates: [gsiUpdate]
|
|
8277
|
+
}));
|
|
8278
|
+
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
8279
|
+
}
|
|
8280
|
+
for (const gsi of gsiDiff.modified) {
|
|
8281
|
+
if (!gsi.IndexName) continue;
|
|
8282
|
+
const gsiUpdate = { Update: {
|
|
8283
|
+
IndexName: gsi.IndexName,
|
|
8284
|
+
...gsi.ProvisionedThroughput && { ProvisionedThroughput: gsi.ProvisionedThroughput },
|
|
8285
|
+
...gsi.OnDemandThroughput && { OnDemandThroughput: gsi.OnDemandThroughput }
|
|
8286
|
+
} };
|
|
8287
|
+
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
8288
|
+
TableName: physicalId,
|
|
8289
|
+
GlobalSecondaryIndexUpdates: [gsiUpdate]
|
|
8290
|
+
}));
|
|
8291
|
+
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
8292
|
+
}
|
|
8293
|
+
if (!deepEqual$1(properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"])) {
|
|
8294
|
+
const ttl = properties["TimeToLiveSpecification"];
|
|
8295
|
+
if (ttl?.["AttributeName"]) await this.dynamoDBClient.send(new UpdateTimeToLiveCommand({
|
|
8296
|
+
TableName: physicalId,
|
|
8297
|
+
TimeToLiveSpecification: {
|
|
8298
|
+
Enabled: ttl["Enabled"] !== void 0 ? Boolean(ttl["Enabled"]) : true,
|
|
8299
|
+
AttributeName: ttl["AttributeName"]
|
|
8300
|
+
}
|
|
8301
|
+
}));
|
|
8302
|
+
else if (previousProperties["TimeToLiveSpecification"]) {
|
|
8303
|
+
const prevTtl = previousProperties["TimeToLiveSpecification"];
|
|
8304
|
+
if (prevTtl["AttributeName"]) await this.dynamoDBClient.send(new UpdateTimeToLiveCommand({
|
|
8305
|
+
TableName: physicalId,
|
|
8306
|
+
TimeToLiveSpecification: {
|
|
8307
|
+
Enabled: false,
|
|
8308
|
+
AttributeName: prevTtl["AttributeName"]
|
|
8309
|
+
}
|
|
8310
|
+
}));
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
const finalDescribe = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
|
|
8314
|
+
return {
|
|
8315
|
+
physicalId,
|
|
8316
|
+
wasReplaced: false,
|
|
8317
|
+
attributes: {
|
|
8318
|
+
Arn: finalDescribe.Table?.TableArn,
|
|
8319
|
+
TableId: finalDescribe.Table?.TableId,
|
|
8320
|
+
StreamArn: finalDescribe.Table?.LatestStreamArn,
|
|
8321
|
+
TableName: physicalId
|
|
8322
|
+
}
|
|
8323
|
+
};
|
|
8324
|
+
} catch (error) {
|
|
8325
|
+
if (error instanceof ProvisioningError) throw error;
|
|
8326
|
+
const cause = error instanceof Error ? error : void 0;
|
|
8327
|
+
throw new ProvisioningError(`Failed to update DynamoDB GlobalTable ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
8328
|
+
}
|
|
8039
8329
|
}
|
|
8040
8330
|
/**
|
|
8041
|
-
*
|
|
8331
|
+
* Apply a diff between old and new CFn-shape Tags arrays via DynamoDB's
|
|
8332
|
+
* `TagResource` / `UntagResource` APIs against the local client. Both
|
|
8333
|
+
* take the table ARN as `ResourceArn`.
|
|
8042
8334
|
*
|
|
8043
|
-
*
|
|
8044
|
-
*
|
|
8045
|
-
* distinct UpdateTable shapes and ordering rules. `cdkd drift --revert`
|
|
8046
|
-
* surfaces this as `ResourceUpdateNotSupportedError` (exit code 2);
|
|
8047
|
-
* the user falls back to `cdkd deploy --replace` or destroy + redeploy.
|
|
8335
|
+
* Local-replica convenience wrapper around `applyTagDiffOnClient` so
|
|
8336
|
+
* existing call sites stay unchanged.
|
|
8048
8337
|
*/
|
|
8049
|
-
async
|
|
8050
|
-
|
|
8338
|
+
async applyTagDiff(tableArn, oldTagsRaw, newTagsRaw) {
|
|
8339
|
+
await this.applyTagDiffOnClient(this.dynamoDBClient, tableArn, oldTagsRaw, newTagsRaw);
|
|
8340
|
+
}
|
|
8341
|
+
/**
|
|
8342
|
+
* Apply a Tags diff against the given `DynamoDBClient` (which may be
|
|
8343
|
+
* the local client or a per-region client returned by
|
|
8344
|
+
* `getRegionalClient`). Used by the local-replica path AND the
|
|
8345
|
+
* cross-region replica Tags propagation path (Issue #389).
|
|
8346
|
+
*/
|
|
8347
|
+
async applyTagDiffOnClient(client, tableArn, oldTagsRaw, newTagsRaw) {
|
|
8348
|
+
const toMap = (tags) => {
|
|
8349
|
+
const m = /* @__PURE__ */ new Map();
|
|
8350
|
+
for (const t of tags ?? []) if (t.Key !== void 0 && t.Value !== void 0) m.set(t.Key, t.Value);
|
|
8351
|
+
return m;
|
|
8352
|
+
};
|
|
8353
|
+
const oldMap = toMap(oldTagsRaw);
|
|
8354
|
+
const newMap = toMap(newTagsRaw);
|
|
8355
|
+
const tagsToAdd = [];
|
|
8356
|
+
for (const [k, v] of newMap) if (oldMap.get(k) !== v) tagsToAdd.push({
|
|
8357
|
+
Key: k,
|
|
8358
|
+
Value: v
|
|
8359
|
+
});
|
|
8360
|
+
const tagsToRemove = [];
|
|
8361
|
+
for (const k of oldMap.keys()) if (!newMap.has(k)) tagsToRemove.push(k);
|
|
8362
|
+
if (tagsToRemove.length > 0) {
|
|
8363
|
+
await client.send(new UntagResourceCommand$2({
|
|
8364
|
+
ResourceArn: tableArn,
|
|
8365
|
+
TagKeys: tagsToRemove
|
|
8366
|
+
}));
|
|
8367
|
+
this.logger.debug(`Removed ${tagsToRemove.length} tag(s) from DynamoDB GlobalTable ${tableArn}`);
|
|
8368
|
+
}
|
|
8369
|
+
if (tagsToAdd.length > 0) {
|
|
8370
|
+
await client.send(new TagResourceCommand$2({
|
|
8371
|
+
ResourceArn: tableArn,
|
|
8372
|
+
Tags: tagsToAdd
|
|
8373
|
+
}));
|
|
8374
|
+
this.logger.debug(`Added/updated ${tagsToAdd.length} tag(s) on DynamoDB GlobalTable ${tableArn}`);
|
|
8375
|
+
}
|
|
8051
8376
|
}
|
|
8052
8377
|
/**
|
|
8053
8378
|
* Delete a DynamoDB Global Table.
|
|
@@ -8075,7 +8400,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8075
8400
|
}));
|
|
8076
8401
|
this.logger.debug(`Disabled DeletionProtectionEnabled on ${logicalId}, waiting for ACTIVE`);
|
|
8077
8402
|
try {
|
|
8078
|
-
await this.waitForTableActiveAfterUpdate(physicalId);
|
|
8403
|
+
await this.waitForTableActiveAfterUpdate(physicalId, logicalId);
|
|
8079
8404
|
} catch (waitErr) {
|
|
8080
8405
|
this.logger.debug(`Could not wait for table ${physicalId} ACTIVE after protection flip: ${waitErr instanceof Error ? waitErr.message : String(waitErr)}`);
|
|
8081
8406
|
}
|
|
@@ -8099,7 +8424,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8099
8424
|
TableName: physicalId,
|
|
8100
8425
|
ReplicaUpdates: [{ Delete: { RegionName: region } }]
|
|
8101
8426
|
}));
|
|
8102
|
-
await this.waitForReplicaGone(physicalId, region);
|
|
8427
|
+
await this.waitForReplicaGone(physicalId, region, logicalId);
|
|
8103
8428
|
} catch (replicaErr) {
|
|
8104
8429
|
if (!(replicaErr instanceof ResourceNotFoundException$1)) throw replicaErr;
|
|
8105
8430
|
}
|
|
@@ -8112,7 +8437,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8112
8437
|
}
|
|
8113
8438
|
try {
|
|
8114
8439
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
8115
|
-
await this.waitForTableGone(physicalId);
|
|
8440
|
+
await this.waitForTableGone(physicalId, logicalId);
|
|
8116
8441
|
this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
|
|
8117
8442
|
} catch (error) {
|
|
8118
8443
|
if (error instanceof ResourceNotFoundException$1) {
|
|
@@ -8158,22 +8483,29 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8158
8483
|
/**
|
|
8159
8484
|
* Read the AWS-current DynamoDB GlobalTable configuration in CFn-property shape.
|
|
8160
8485
|
*
|
|
8161
|
-
* Reverse-maps `DescribeTable` + `ListTagsOfResource`
|
|
8162
|
-
* `
|
|
8486
|
+
* Reverse-maps `DescribeTable` + `ListTagsOfResource` + `DescribeTimeToLive`
|
|
8487
|
+
* + per-replica `DescribeContributorInsights` /
|
|
8488
|
+
* `DescribeContinuousBackups` / `DescribeKinesisStreamingDestination`
|
|
8489
|
+
* into the `AWS::DynamoDB::GlobalTable` property set.
|
|
8163
8490
|
*
|
|
8164
8491
|
* Type-discriminator gating (memory rule
|
|
8165
8492
|
* `feedback_always_emit_check_type_discriminator.md`):
|
|
8166
|
-
* - `ProvisionedThroughput`-bearing fields are only surfaced when
|
|
8167
|
-
* `BillingMode === 'PROVISIONED'`. Emitting placeholders on
|
|
8168
|
-
* PAY_PER_REQUEST tables (or vice versa) would fire false drift on
|
|
8169
|
-
* every clean run.
|
|
8170
8493
|
* - StreamSpecification / SSESpecification follow the existing
|
|
8171
8494
|
* DynamoDB::Table provider's Class 1 guard: only surfaced when AWS
|
|
8172
8495
|
* reports the feature actually enabled.
|
|
8173
|
-
*
|
|
8174
|
-
*
|
|
8175
|
-
*
|
|
8176
|
-
*
|
|
8496
|
+
* - `ProvisionedThroughput`-bearing fields are declared in
|
|
8497
|
+
* `getDriftUnknownPaths` and intentionally not emitted in v1 — the
|
|
8498
|
+
* reverse-mapping from AWS's `ProvisionedThroughput` shape into
|
|
8499
|
+
* CFn's `WriteProvisionedThroughputSettings` /
|
|
8500
|
+
* `ReadProvisionedThroughputSettings` wrappers (which carry
|
|
8501
|
+
* `WriteCapacityAutoScalingSettings` etc.) needs more work to round
|
|
8502
|
+
* -trip cleanly.
|
|
8503
|
+
*
|
|
8504
|
+
* Per-replica sub-specifications (`ContributorInsightsSpecification` /
|
|
8505
|
+
* `PointInTimeRecoverySpecification` / `KinesisStreamSpecification`)
|
|
8506
|
+
* are surfaced only for the LOCAL replica. Cross-region replicas
|
|
8507
|
+
* require per-region SDK clients (`new DynamoDBClient({region})`),
|
|
8508
|
+
* deferred to a follow-up PR.
|
|
8177
8509
|
*/
|
|
8178
8510
|
async readCurrentState(physicalId, _logicalId, _resourceType) {
|
|
8179
8511
|
try {
|
|
@@ -8196,19 +8528,54 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8196
8528
|
if (table.SSEDescription.SSEType !== void 0) sse["SSEType"] = table.SSEDescription.SSEType;
|
|
8197
8529
|
result["SSESpecification"] = sse;
|
|
8198
8530
|
}
|
|
8199
|
-
result["Replicas"] = (table.Replicas ?? []).map((r) => ({
|
|
8200
|
-
Region: r.RegionName,
|
|
8201
|
-
...r.KMSMasterKeyId !== void 0 && { KMSMasterKeyId: r.KMSMasterKeyId }
|
|
8202
|
-
}));
|
|
8203
8531
|
if (table.TableClassSummary?.TableClass) result["TableClass"] = table.TableClassSummary.TableClass;
|
|
8204
8532
|
if (table.DeletionProtectionEnabled !== void 0) result["DeletionProtectionEnabled"] = table.DeletionProtectionEnabled;
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
8209
|
-
|
|
8533
|
+
const currentRegion = await this.dynamoDBClient.config.region() ?? "";
|
|
8534
|
+
const tableNameForSubs = table.TableName ?? physicalId;
|
|
8535
|
+
result["Replicas"] = await Promise.all((table.Replicas ?? []).map(async (r) => {
|
|
8536
|
+
const entry = { Region: r.RegionName };
|
|
8537
|
+
if (r.KMSMasterKeyId !== void 0) entry["KMSMasterKeyId"] = r.KMSMasterKeyId;
|
|
8538
|
+
if (!r.RegionName) return entry;
|
|
8539
|
+
const isLocal = r.RegionName === currentRegion;
|
|
8540
|
+
const client = isLocal ? this.dynamoDBClient : this.getRegionalClient(r.RegionName);
|
|
8541
|
+
const regionLabel = r.RegionName;
|
|
8542
|
+
const replicaArn = isLocal ? table.TableArn : table.TableArn ? this.replicaArnForRegion(table.TableArn, r.RegionName) : void 0;
|
|
8543
|
+
const subs = await this.readReplicaSubSpecs(client, tableNameForSubs, regionLabel);
|
|
8544
|
+
Object.assign(entry, subs);
|
|
8545
|
+
if (replicaArn) try {
|
|
8546
|
+
entry["Tags"] = normalizeAwsTagsToCfn((await client.send(new ListTagsOfResourceCommand({ ResourceArn: replicaArn }))).Tags);
|
|
8547
|
+
} catch (tagErr) {
|
|
8548
|
+
if (tagErr instanceof ResourceNotFoundException$1) {
|
|
8549
|
+
if (isLocal) throw tagErr;
|
|
8550
|
+
this.logger.debug(`Cross-region replica ${regionLabel} returned RNF on ListTagsOfResource; omitting Tags`);
|
|
8551
|
+
entry["Tags"] = [];
|
|
8552
|
+
} else {
|
|
8553
|
+
this.logger.warn(`Could not fetch tags for DynamoDB GlobalTable ${tableNameForSubs} in ${regionLabel}: ${tagErr instanceof Error ? tagErr.message : String(tagErr)}`);
|
|
8554
|
+
entry["Tags"] = [];
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
else entry["Tags"] = [];
|
|
8558
|
+
return entry;
|
|
8559
|
+
}));
|
|
8560
|
+
if (table.OnDemandThroughput?.MaxWriteRequestUnits !== void 0) result["WriteOnDemandThroughputSettings"] = { MaxWriteRequestUnits: table.OnDemandThroughput.MaxWriteRequestUnits };
|
|
8561
|
+
else result["WriteOnDemandThroughputSettings"] = {};
|
|
8562
|
+
if (billingMode === "PROVISIONED") {
|
|
8563
|
+
const writeCapacity = table.ProvisionedThroughput?.WriteCapacityUnits;
|
|
8564
|
+
if (writeCapacity !== void 0) if (!await this.hasWriteAutoScalingPolicy(tableNameForSubs)) result["WriteProvisionedThroughputSettings"] = { WriteCapacityUnits: writeCapacity };
|
|
8565
|
+
else result["WriteProvisionedThroughputSettings"] = {};
|
|
8566
|
+
else result["WriteProvisionedThroughputSettings"] = {};
|
|
8567
|
+
} else result["WriteProvisionedThroughputSettings"] = {};
|
|
8568
|
+
try {
|
|
8569
|
+
const ttlDesc = (await this.dynamoDBClient.send(new DescribeTimeToLiveCommand({ TableName: tableNameForSubs }))).TimeToLiveDescription;
|
|
8570
|
+
const ttlStatus = ttlDesc?.TimeToLiveStatus;
|
|
8571
|
+
if (ttlStatus === "ENABLED" && ttlDesc?.AttributeName) result["TimeToLiveSpecification"] = {
|
|
8572
|
+
AttributeName: ttlDesc.AttributeName,
|
|
8573
|
+
Enabled: true
|
|
8574
|
+
};
|
|
8575
|
+
else if (ttlStatus === "DISABLED") {}
|
|
8576
|
+
} catch (ttlErr) {
|
|
8577
|
+
this.logger.debug(`Could not read TimeToLive for ${tableNameForSubs}: ${ttlErr instanceof Error ? ttlErr.message : String(ttlErr)}`);
|
|
8210
8578
|
}
|
|
8211
|
-
else result["Tags"] = [];
|
|
8212
8579
|
return result;
|
|
8213
8580
|
} catch (err) {
|
|
8214
8581
|
if (err instanceof ResourceNotFoundException$1) return void 0;
|
|
@@ -8216,25 +8583,101 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8216
8583
|
}
|
|
8217
8584
|
}
|
|
8218
8585
|
/**
|
|
8586
|
+
* Read per-replica sub-specifications against the given `DynamoDBClient`
|
|
8587
|
+
* (which may be the local client or a per-region client returned by
|
|
8588
|
+
* `getRegionalClient`):
|
|
8589
|
+
* - `ContributorInsightsSpecification` via `DescribeContributorInsights`
|
|
8590
|
+
* (table-level; GSI overrides are NOT surfaced in v1 — they would
|
|
8591
|
+
* require one call per GSI and a different CFn nesting under the
|
|
8592
|
+
* `Replicas[].GlobalSecondaryIndexes[]` shape).
|
|
8593
|
+
* - `PointInTimeRecoverySpecification` via `DescribeContinuousBackups`.
|
|
8594
|
+
* - `KinesisStreamSpecification` via
|
|
8595
|
+
* `DescribeKinesisStreamingDestination` (filtered to the local
|
|
8596
|
+
* region's destination when AWS reports more than one).
|
|
8597
|
+
*
|
|
8598
|
+
* Each call is best-effort: errors omit the offending key rather than
|
|
8599
|
+
* fail the whole drift read.
|
|
8600
|
+
*
|
|
8601
|
+
* Issue #389 lifted the LOCAL-only limitation by parameterizing the
|
|
8602
|
+
* client — the same reverse-mapping logic runs against any region's
|
|
8603
|
+
* client.
|
|
8604
|
+
*/
|
|
8605
|
+
async readReplicaSubSpecs(client, tableName, regionLabel) {
|
|
8606
|
+
const out = {};
|
|
8607
|
+
try {
|
|
8608
|
+
const ci = await client.send(new DescribeContributorInsightsCommand({ TableName: tableName }));
|
|
8609
|
+
if (ci.ContributorInsightsStatus) out["ContributorInsightsSpecification"] = { Enabled: ci.ContributorInsightsStatus === "ENABLED" };
|
|
8610
|
+
} catch (err) {
|
|
8611
|
+
this.logger.debug(`Could not read ContributorInsights for ${tableName} in ${regionLabel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8612
|
+
}
|
|
8613
|
+
try {
|
|
8614
|
+
const pitrStatus = (await client.send(new DescribeContinuousBackupsCommand({ TableName: tableName }))).ContinuousBackupsDescription?.PointInTimeRecoveryDescription?.PointInTimeRecoveryStatus;
|
|
8615
|
+
if (pitrStatus) out["PointInTimeRecoverySpecification"] = { PointInTimeRecoveryEnabled: pitrStatus === "ENABLED" };
|
|
8616
|
+
} catch (err) {
|
|
8617
|
+
this.logger.debug(`Could not read PointInTimeRecovery for ${tableName} in ${regionLabel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8618
|
+
}
|
|
8619
|
+
try {
|
|
8620
|
+
const active = ((await client.send(new DescribeKinesisStreamingDestinationCommand({ TableName: tableName }))).KinesisDataStreamDestinations ?? []).find((d) => d.DestinationStatus === "ACTIVE" || d.DestinationStatus === "ENABLING");
|
|
8621
|
+
if (active?.StreamArn) {
|
|
8622
|
+
const ksOut = { StreamArn: active.StreamArn };
|
|
8623
|
+
if (active.ApproximateCreationDateTimePrecision !== void 0) ksOut["ApproximateCreationDateTimePrecision"] = active.ApproximateCreationDateTimePrecision;
|
|
8624
|
+
out["KinesisStreamSpecification"] = ksOut;
|
|
8625
|
+
}
|
|
8626
|
+
} catch (err) {
|
|
8627
|
+
this.logger.debug(`Could not read KinesisStreamingDestination for ${tableName} in ${regionLabel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8628
|
+
}
|
|
8629
|
+
return out;
|
|
8630
|
+
}
|
|
8631
|
+
/**
|
|
8632
|
+
* Detect whether application-autoscaling has a scaling policy on this
|
|
8633
|
+
* table's WriteCapacityUnits dimension. Used to decide whether the
|
|
8634
|
+
* `WriteProvisionedThroughputSettings.WriteCapacityUnits` flat-value
|
|
8635
|
+
* surface in `readCurrentState` is safe to emit (auto-scaling
|
|
8636
|
+
* dynamically mutates capacity, so a flat snapshot would fire false
|
|
8637
|
+
* drift on every scale event — when an autoscaling policy is in play
|
|
8638
|
+
* we omit the flat surface so a follow-up PR can reverse-map the
|
|
8639
|
+
* full `WriteCapacityAutoScalingSettings` shape).
|
|
8640
|
+
*
|
|
8641
|
+
* Best-effort: errors return `false` (no autoscaling detected) so a
|
|
8642
|
+
* permissions gap on `application-autoscaling:DescribeScalingPolicies`
|
|
8643
|
+
* does not abort drift reads. Logged at debug.
|
|
8644
|
+
*/
|
|
8645
|
+
async hasWriteAutoScalingPolicy(tableName) {
|
|
8646
|
+
try {
|
|
8647
|
+
return ((await new ApplicationAutoScalingClient({ region: await this.dynamoDBClient.config.region() ?? "" }).send(new DescribeScalingPoliciesCommand({
|
|
8648
|
+
ServiceNamespace: "dynamodb",
|
|
8649
|
+
ResourceId: `table/${tableName}`,
|
|
8650
|
+
ScalableDimension: "dynamodb:table:WriteCapacityUnits"
|
|
8651
|
+
}))).ScalingPolicies ?? []).length > 0;
|
|
8652
|
+
} catch (err) {
|
|
8653
|
+
this.logger.debug(`Could not query application-autoscaling for ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
8654
|
+
return false;
|
|
8655
|
+
}
|
|
8656
|
+
}
|
|
8657
|
+
/**
|
|
8219
8658
|
* State property paths cdkd's GlobalTable readCurrentState cannot (yet)
|
|
8220
8659
|
* reverse-map. The drift comparator skips these so a templated value
|
|
8221
8660
|
* doesn't fire guaranteed false drift on every clean run.
|
|
8222
8661
|
*
|
|
8223
|
-
*
|
|
8224
|
-
*
|
|
8225
|
-
*
|
|
8226
|
-
* -
|
|
8227
|
-
*
|
|
8228
|
-
*
|
|
8229
|
-
*
|
|
8230
|
-
*
|
|
8662
|
+
* Issue #389 emptied this list:
|
|
8663
|
+
* - `WriteProvisionedThroughputSettings` (flat-WriteCapacityUnits
|
|
8664
|
+
* subset) is now reverse-mapped from `Table.ProvisionedThroughput`
|
|
8665
|
+
* when BillingMode is PROVISIONED AND application-autoscaling has
|
|
8666
|
+
* no policy on the WriteCapacityUnits dimension. Auto-scaling
|
|
8667
|
+
* cases emit an empty `{}` placeholder so the comparator does NOT
|
|
8668
|
+
* fire false drift on the literal `WriteCapacityUnits` subtree.
|
|
8669
|
+
* - `WriteOnDemandThroughputSettings` is reverse-mapped from
|
|
8670
|
+
* `Table.OnDemandThroughput.MaxWriteRequestUnits`. Always-emit
|
|
8671
|
+
* `{}` placeholder when AWS reports no override.
|
|
8672
|
+
* - `TimeToLiveSpecification` is reverse-mapped via
|
|
8673
|
+
* `DescribeTimeToLive` in `readCurrentState`.
|
|
8674
|
+
*
|
|
8675
|
+
* Returning an empty list is the canonical "no known-unknown paths"
|
|
8676
|
+
* shape — keeping the method present (rather than removing it) for
|
|
8677
|
+
* future additions and for backward compat on test reflection.
|
|
8231
8678
|
*/
|
|
8232
8679
|
getDriftUnknownPaths(_resourceType) {
|
|
8233
|
-
return [
|
|
8234
|
-
"TimeToLiveSpecification",
|
|
8235
|
-
"WriteProvisionedThroughputSettings",
|
|
8236
|
-
"WriteOnDemandThroughputSettings"
|
|
8237
|
-
];
|
|
8680
|
+
return [];
|
|
8238
8681
|
}
|
|
8239
8682
|
/**
|
|
8240
8683
|
* Adopt an existing DynamoDB GlobalTable into cdkd state.
|
|
@@ -8278,7 +8721,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8278
8721
|
} while (exclusiveStartTableName);
|
|
8279
8722
|
return null;
|
|
8280
8723
|
}
|
|
8281
|
-
async waitForTableActive(tableName, maxAttempts = 120) {
|
|
8724
|
+
async waitForTableActive(tableName, logicalId, maxAttempts = 120) {
|
|
8282
8725
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
8283
8726
|
const response = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
|
|
8284
8727
|
const status = response.Table?.TableStatus;
|
|
@@ -8288,10 +8731,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8288
8731
|
tableId: response.Table?.TableId,
|
|
8289
8732
|
streamArn: response.Table?.LatestStreamArn
|
|
8290
8733
|
};
|
|
8291
|
-
if (status !== "CREATING" && status !== "UPDATING") throw new
|
|
8734
|
+
if (status !== "CREATING" && status !== "UPDATING") throw new ProvisioningError(`Unexpected table status while waiting for ACTIVE on ${tableName}: ${status}`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
8292
8735
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
8293
8736
|
}
|
|
8294
|
-
throw new
|
|
8737
|
+
throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
8295
8738
|
}
|
|
8296
8739
|
/**
|
|
8297
8740
|
* Wait for the table to reach ACTIVE after an UpdateTable call. Unlike
|
|
@@ -8299,32 +8742,32 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8299
8742
|
* table may already be ACTIVE on the no-op path (already-disabled
|
|
8300
8743
|
* protection) or transition through UPDATING.
|
|
8301
8744
|
*/
|
|
8302
|
-
async waitForTableActiveAfterUpdate(tableName, maxAttempts =
|
|
8745
|
+
async waitForTableActiveAfterUpdate(tableName, logicalId, maxAttempts = 600) {
|
|
8303
8746
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
8304
8747
|
if ((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.TableStatus === "ACTIVE") return;
|
|
8305
8748
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
8306
8749
|
}
|
|
8307
|
-
throw new
|
|
8750
|
+
throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
8308
8751
|
}
|
|
8309
8752
|
/**
|
|
8310
8753
|
* Wait until a specific replica's `ReplicaStatus` flips to ACTIVE.
|
|
8311
8754
|
* Replica provisioning typically takes 1–5 min; cap at 10 min.
|
|
8312
8755
|
*/
|
|
8313
|
-
async waitForReplicaActive(tableName, region, maxAttempts = 600) {
|
|
8756
|
+
async waitForReplicaActive(tableName, region, logicalId, maxAttempts = 600) {
|
|
8314
8757
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
8315
8758
|
const replica = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region);
|
|
8316
8759
|
if (replica?.ReplicaStatus === "ACTIVE") return;
|
|
8317
8760
|
this.logger.debug(`Replica ${region} status: ${replica?.ReplicaStatus} (attempt ${attempt}/${maxAttempts})`);
|
|
8318
8761
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
8319
8762
|
}
|
|
8320
|
-
throw new
|
|
8763
|
+
throw new ProvisioningError(`Replica ${region} for table ${tableName} did not reach ACTIVE within ${maxAttempts}s`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
8321
8764
|
}
|
|
8322
8765
|
/**
|
|
8323
8766
|
* Wait until a specific replica disappears from `Replicas[]` after a
|
|
8324
8767
|
* Delete replica update. Replica deletion typically takes 1–5 min;
|
|
8325
8768
|
* cap at 10 min.
|
|
8326
8769
|
*/
|
|
8327
|
-
async waitForReplicaGone(tableName, region, maxAttempts = 600) {
|
|
8770
|
+
async waitForReplicaGone(tableName, region, logicalId, maxAttempts = 600) {
|
|
8328
8771
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
8329
8772
|
if (!(await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region)) return;
|
|
8330
8773
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
@@ -8332,7 +8775,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8332
8775
|
if (err instanceof ResourceNotFoundException$1) return;
|
|
8333
8776
|
throw err;
|
|
8334
8777
|
}
|
|
8335
|
-
throw new
|
|
8778
|
+
throw new ProvisioningError(`Replica ${region} for table ${tableName} did not disappear within ${maxAttempts}s`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
8336
8779
|
}
|
|
8337
8780
|
/**
|
|
8338
8781
|
* Wait for `DescribeTable` to return `ResourceNotFoundException`,
|
|
@@ -8345,7 +8788,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8345
8788
|
* Typical small-table delete completes in 5–30s; cap at 10 min for
|
|
8346
8789
|
* worst-case large-table / replica-cascade scenarios.
|
|
8347
8790
|
*/
|
|
8348
|
-
async waitForTableGone(tableName, maxAttempts = 600) {
|
|
8791
|
+
async waitForTableGone(tableName, logicalId, maxAttempts = 600) {
|
|
8349
8792
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
8350
8793
|
await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
|
|
8351
8794
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
@@ -8353,9 +8796,104 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
8353
8796
|
if (err instanceof ResourceNotFoundException$1) return;
|
|
8354
8797
|
throw err;
|
|
8355
8798
|
}
|
|
8356
|
-
throw new
|
|
8799
|
+
throw new ProvisioningError(`Table ${tableName} did not disappear within ${maxAttempts}s`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
8357
8800
|
}
|
|
8358
8801
|
};
|
|
8802
|
+
/**
|
|
8803
|
+
* Diff CFn `Replicas[]` arrays. Keyed by `Region`. Returns adds, removes,
|
|
8804
|
+
* and modifies (entries whose other keys — KMSMasterKeyId,
|
|
8805
|
+
* GlobalSecondaryIndexes, TableClassOverride — differ from the old shape).
|
|
8806
|
+
*/
|
|
8807
|
+
/**
|
|
8808
|
+
* Derive the per-call `ProvisionedThroughput` shape required by
|
|
8809
|
+
* `CreateTableCommand` / `UpdateTableCommand` when BillingMode flips to
|
|
8810
|
+
* PROVISIONED. Shared between create() and the BillingMode-flip path in
|
|
8811
|
+
* update() so a user template's non-default read/write capacity is
|
|
8812
|
+
* preserved consistently across both code paths.
|
|
8813
|
+
*
|
|
8814
|
+
* Source of truth (CFn `AWS::DynamoDB::GlobalTable` shape):
|
|
8815
|
+
* - WriteCapacityUnits → `properties.WriteProvisionedThroughputSettings`
|
|
8816
|
+
* (top-level on the table). Literal `WriteCapacityUnits` wins over
|
|
8817
|
+
* auto-scaling `MinCapacity`; both default to 5 if absent.
|
|
8818
|
+
* - ReadCapacityUnits → `Replicas[?Region==<region>].ReadProvisionedThroughputSettings`
|
|
8819
|
+
* (per-replica, the deploy region's setting). Same literal-vs-auto-
|
|
8820
|
+
* scaling-vs-default-5 precedence.
|
|
8821
|
+
*/
|
|
8822
|
+
function derivePerCallProvisionedThroughput(properties, region) {
|
|
8823
|
+
const wps = properties["WriteProvisionedThroughputSettings"];
|
|
8824
|
+
const writeAutoScaling = wps?.["WriteCapacityAutoScalingSettings"];
|
|
8825
|
+
const writeCapacity = Number(wps?.["WriteCapacityUnits"] ?? writeAutoScaling?.["MinCapacity"] ?? 5);
|
|
8826
|
+
const localReadSettings = (properties["Replicas"] ?? []).find((r) => r["Region"] === region)?.["ReadProvisionedThroughputSettings"];
|
|
8827
|
+
const readAutoScaling = localReadSettings?.["ReadCapacityAutoScalingSettings"];
|
|
8828
|
+
return {
|
|
8829
|
+
ReadCapacityUnits: Number(localReadSettings?.["ReadCapacityUnits"] ?? readAutoScaling?.["MinCapacity"] ?? 5),
|
|
8830
|
+
WriteCapacityUnits: writeCapacity
|
|
8831
|
+
};
|
|
8832
|
+
}
|
|
8833
|
+
function diffReplicas(oldReplicas, newReplicas) {
|
|
8834
|
+
const oldByRegion = /* @__PURE__ */ new Map();
|
|
8835
|
+
const newByRegion = /* @__PURE__ */ new Map();
|
|
8836
|
+
for (const r of oldReplicas) {
|
|
8837
|
+
const region = r["Region"];
|
|
8838
|
+
if (region) oldByRegion.set(region, r);
|
|
8839
|
+
}
|
|
8840
|
+
for (const r of newReplicas) {
|
|
8841
|
+
const region = r["Region"];
|
|
8842
|
+
if (region) newByRegion.set(region, r);
|
|
8843
|
+
}
|
|
8844
|
+
const added = [];
|
|
8845
|
+
const removed = [];
|
|
8846
|
+
const modified = [];
|
|
8847
|
+
for (const [region, replica] of newByRegion) if (!oldByRegion.has(region)) added.push(replica);
|
|
8848
|
+
else if (!deepEqual$1(oldByRegion.get(region), replica)) modified.push(replica);
|
|
8849
|
+
for (const [region, replica] of oldByRegion) if (!newByRegion.has(region)) removed.push(replica);
|
|
8850
|
+
return {
|
|
8851
|
+
added,
|
|
8852
|
+
removed,
|
|
8853
|
+
modified
|
|
8854
|
+
};
|
|
8855
|
+
}
|
|
8856
|
+
/**
|
|
8857
|
+
* Diff CFn `GlobalSecondaryIndexes[]` arrays. Keyed by `IndexName`.
|
|
8858
|
+
* Modified = same IndexName but other fields (ProvisionedThroughput /
|
|
8859
|
+
* OnDemandThroughput) differ. KeySchema / Projection changes count as
|
|
8860
|
+
* "modified" too — AWS rejects those via UpdateGSI, but the diff caller
|
|
8861
|
+
* surfaces the AWS-side error rather than this helper second-guessing.
|
|
8862
|
+
*/
|
|
8863
|
+
function diffGlobalSecondaryIndexes(oldGsi, newGsi) {
|
|
8864
|
+
const oldByName = /* @__PURE__ */ new Map();
|
|
8865
|
+
const newByName = /* @__PURE__ */ new Map();
|
|
8866
|
+
for (const g of oldGsi) if (g.IndexName) oldByName.set(g.IndexName, g);
|
|
8867
|
+
for (const g of newGsi) if (g.IndexName) newByName.set(g.IndexName, g);
|
|
8868
|
+
const added = [];
|
|
8869
|
+
const removed = [];
|
|
8870
|
+
const modified = [];
|
|
8871
|
+
for (const [name, gsi] of newByName) if (!oldByName.has(name)) added.push(gsi);
|
|
8872
|
+
else if (!deepEqual$1(oldByName.get(name), gsi)) modified.push(gsi);
|
|
8873
|
+
for (const [name, gsi] of oldByName) if (!newByName.has(name)) removed.push(gsi);
|
|
8874
|
+
return {
|
|
8875
|
+
added,
|
|
8876
|
+
removed,
|
|
8877
|
+
modified
|
|
8878
|
+
};
|
|
8879
|
+
}
|
|
8880
|
+
/**
|
|
8881
|
+
* Structural equality via JSON.stringify. Both inputs are CFn-shape
|
|
8882
|
+
* POJOs (no functions, no symbols, no cycles), so JSON round-trip is
|
|
8883
|
+
* sufficient and free of the property-order pitfalls of deeper
|
|
8884
|
+
* comparators. Object property order from `Object.keys` is insertion
|
|
8885
|
+
* order in modern engines; AWS-SDK shapes are constructed by the SDK
|
|
8886
|
+
* in stable order, so this is safe in practice.
|
|
8887
|
+
*/
|
|
8888
|
+
function deepEqual$1(a, b) {
|
|
8889
|
+
if (a === b) return true;
|
|
8890
|
+
if (a === void 0 || b === void 0) return false;
|
|
8891
|
+
try {
|
|
8892
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
8893
|
+
} catch {
|
|
8894
|
+
return false;
|
|
8895
|
+
}
|
|
8896
|
+
}
|
|
8359
8897
|
|
|
8360
8898
|
//#endregion
|
|
8361
8899
|
//#region src/provisioning/providers/logs-loggroup-provider.ts
|
|
@@ -43625,7 +44163,7 @@ function reorderArgs(argv) {
|
|
|
43625
44163
|
*/
|
|
43626
44164
|
async function main() {
|
|
43627
44165
|
const program = new Command();
|
|
43628
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
44166
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.105.0");
|
|
43629
44167
|
program.addCommand(createBootstrapCommand());
|
|
43630
44168
|
program.addCommand(createSynthCommand());
|
|
43631
44169
|
program.addCommand(createListCommand());
|