@go-to-k/cdkd 0.278.36 → 0.278.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-5hYG8VHd.js → asg-provider-CaulrRij.js} +2 -2
- package/dist/{asg-provider-5hYG8VHd.js.map → asg-provider-CaulrRij.js.map} +1 -1
- package/dist/cli.js +485 -28
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-iHGY-Q_U.js → deploy-engine-DIvnBeMj.js} +3 -3
- package/dist/{deploy-engine-iHGY-Q_U.js.map → deploy-engine-DIvnBeMj.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as describeTypeWithThrottleRetry, $t as AwsClients, A as red, At as Synthesizer, B as isTerminationProtectionPropagationError, Bt as resolveUseCdkBootstrapAssets, C as isStatefulRecreateTargetSync, Ct as buildDockerImage, D as cyan, Dt as runDockerStreaming, E as bold, Et as runDockerForeground, F as ProviderRegistry, Ft as resolveAutoAssetStorage, G as normalizeAwsTagsToCfn, Gt as MIGRATE_TMP_PREFIX, H as cfnRefValueFromPhysicalId, Ht as warnDeprecatedNoPrefixCliFlag, I as findActionableSilentDrops, It as resolveCaptureObservedState, J as readConfigString, Jt as expectedOwnerParam, K as resolveExplicitPhysicalId, Kt as findLargeInlineResources, L as CloudControlProvider, Lt as resolveSkipPrefix, M as IAMRoleProvider, Mt as getDefaultStateBucketName, N as collectInlinePolicyNamesManagedBySiblings, Nt as getLegacyStateBucketName, O as gray, Ot as AssetManifestLoader, P as clearOnUpdateRemoval, Pt as resolveApp, Q as DiffCalculator, Qt as resolveBucketRegion, R as slowCcOperationTimeoutMs, Rt as resolveStateBucketWithDefault, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, St as validateContainerRepoName, T as formatResourceLine, Tn as withErrorHandling, Tt as getDockerCmd, U as refStateLookupFromResource, Ut as CFN_TEMPLATE_BODY_LIMIT, V as IntrinsicFunctionResolver, Vt as stateBucketExistenceConfirmed, W as WAFv2WebACLProvider, Wt as CFN_TEMPLATE_URL_LIMIT, X as requireConfigString, Xt as processStackMessages, Y as requireConfigArray, Yt as AssemblyReader, Z as applyRoleArnIfSet, _ as createPreDeleteFinalSnapshot, _n as ResourceUpdateNotSupportedError, _t as BOOTSTRAP_MARKER_PREFIX, a as DeploymentEventsStore, at as LockManager, b as unsupportedFinalSnapshotError, bt as parseBootstrapMarker, c as replayFailedOperations, cn as LocalInvokeBuildError$1, ct as shouldRetainResource, d as IMPLICIT_DELETE_DEPENDENCIES, dt as WorkGraph, en as getAwsClients, et as withRetry, f as computeImplicitDeleteEdges, fn as MissingCdkCliError, ft as buildAssetRedirectMap, g as ccRoutedFinalSnapshotError, gn as ResourceTimeoutError, gt as AssetModeResolver, h as buildFinalSnapshotIdentifier, hn as ProvisioningError, ht as rewriteTemplateAssetReferences, i as DeploymentEventsReader, in as CdkdError, it as TemplateParser, j as yellow, jt as synthesisStatusMessage, k as green, kt as getDockerImageBySourceHash, l as replayRollback, ln as LocalMigrateError, lt as AssetPublisher, m as PRE_DELETE_SNAPSHOT_TYPES, mn as PartialFailureError, mt as loadPublishableAssetManifest, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as setAwsClients, nt as isThrottlingError, o as planFailedOps, ot as S3StateBackend, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as NestedStackChildDirectDestroyError, pt as createAssetRedirectResolver, q as assertRegionMatch, qt as uploadCfnTemplate, r as DeployEngine, rt as DagBuilder, s as planRollback, sn as DeployCancelledError, st as rebuildClientForBucketRegion, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resetAwsClients, tt as isRetryableTransientError, u as withResourceDeadline, un as LocalStartServiceError, ut as stringifyValue, v as isFinalSnapshotError, vn as StackHasActiveImportsError, vt as ensureAssetStorage, w as renderStatefulReason, wn as normalizeAwsError, wt as formatDockerLoginError, x as extractDeploymentEventError, xt as validateAssetBucketName, y as refusesFinalSnapshot, yn as StackTerminationProtectionError, yt as getBootstrapMarkerKey, z as disableInstanceApiTermination, zt as resolveStateBucketWithDefaultAndSource } from "./deploy-engine-
|
|
2
|
+
import { $ as describeTypeWithThrottleRetry, $t as AwsClients, A as red, At as Synthesizer, B as isTerminationProtectionPropagationError, Bt as resolveUseCdkBootstrapAssets, C as isStatefulRecreateTargetSync, Ct as buildDockerImage, D as cyan, Dt as runDockerStreaming, E as bold, Et as runDockerForeground, F as ProviderRegistry, Ft as resolveAutoAssetStorage, G as normalizeAwsTagsToCfn, Gt as MIGRATE_TMP_PREFIX, H as cfnRefValueFromPhysicalId, Ht as warnDeprecatedNoPrefixCliFlag, I as findActionableSilentDrops, It as resolveCaptureObservedState, J as readConfigString, Jt as expectedOwnerParam, K as resolveExplicitPhysicalId, Kt as findLargeInlineResources, L as CloudControlProvider, Lt as resolveSkipPrefix, M as IAMRoleProvider, Mt as getDefaultStateBucketName, N as collectInlinePolicyNamesManagedBySiblings, Nt as getLegacyStateBucketName, O as gray, Ot as AssetManifestLoader, P as clearOnUpdateRemoval, Pt as resolveApp, Q as DiffCalculator, Qt as resolveBucketRegion, R as slowCcOperationTimeoutMs, Rt as resolveStateBucketWithDefault, S as MULTI_REGION_RECREATE_BLOCKED_TYPES, St as validateContainerRepoName, T as formatResourceLine, Tn as withErrorHandling, Tt as getDockerCmd, U as refStateLookupFromResource, Ut as CFN_TEMPLATE_BODY_LIMIT, V as IntrinsicFunctionResolver, Vt as stateBucketExistenceConfirmed, W as WAFv2WebACLProvider, Wt as CFN_TEMPLATE_URL_LIMIT, X as requireConfigString, Xt as processStackMessages, Y as requireConfigArray, Yt as AssemblyReader, Z as applyRoleArnIfSet, _ as createPreDeleteFinalSnapshot, _n as ResourceUpdateNotSupportedError, _t as BOOTSTRAP_MARKER_PREFIX, a as DeploymentEventsStore, at as LockManager, b as unsupportedFinalSnapshotError, bt as parseBootstrapMarker, c as replayFailedOperations, cn as LocalInvokeBuildError$1, ct as shouldRetainResource, d as IMPLICIT_DELETE_DEPENDENCIES, dt as WorkGraph, en as getAwsClients, et as withRetry, f as computeImplicitDeleteEdges, fn as MissingCdkCliError, ft as buildAssetRedirectMap, g as ccRoutedFinalSnapshotError, gn as ResourceTimeoutError, gt as AssetModeResolver, h as buildFinalSnapshotIdentifier, hn as ProvisioningError, ht as rewriteTemplateAssetReferences, i as DeploymentEventsReader, in as CdkdError, it as TemplateParser, j as yellow, jt as synthesisStatusMessage, k as green, kt as getDockerImageBySourceHash, l as replayRollback, ln as LocalMigrateError, lt as AssetPublisher, m as PRE_DELETE_SNAPSHOT_TYPES, mn as PartialFailureError, mt as loadPublishableAssetManifest, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as setAwsClients, nt as isThrottlingError, o as planFailedOps, ot as S3StateBackend, p as ATOMIC_FINAL_SNAPSHOT_TYPES, pn as NestedStackChildDirectDestroyError, pt as createAssetRedirectResolver, q as assertRegionMatch, qt as uploadCfnTemplate, r as DeployEngine, rt as DagBuilder, s as planRollback, sn as DeployCancelledError, st as rebuildClientForBucketRegion, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resetAwsClients, tt as isRetryableTransientError, u as withResourceDeadline, un as LocalStartServiceError, ut as stringifyValue, v as isFinalSnapshotError, vn as StackHasActiveImportsError, vt as ensureAssetStorage, w as renderStatefulReason, wn as normalizeAwsError, wt as formatDockerLoginError, x as extractDeploymentEventError, xt as validateAssetBucketName, y as refusesFinalSnapshot, yn as StackTerminationProtectionError, yt as getBootstrapMarkerKey, z as disableInstanceApiTermination, zt as resolveStateBucketWithDefaultAndSource } from "./deploy-engine-DIvnBeMj.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as withSkipPrefix, i as runStackBuffered, n as getLogger, o as PATTERN_B_NAME_OPTIONS, p as withStackName, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-BYMEE-BS.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-CaulrRij.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, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, 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";
|
|
8
|
-
import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachRolePolicyCommand, AttachUserPolicyCommand, CreateAccessKeyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreatePolicyCommand, CreatePolicyVersionCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeletePolicyCommand, DeletePolicyVersionCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachRolePolicyCommand, DetachUserPolicyCommand, GetAccessKeyLastUsedCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetPolicyCommand, GetPolicyVersionCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListEntitiesForPolicyCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListPoliciesCommand, ListPolicyTagsCommand, ListPolicyVersionsCommand, ListUserPoliciesCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagPolicyCommand, TagUserCommand, UntagPolicyCommand, UntagUserCommand, UpdateAccessKeyCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
|
|
8
|
+
import { AddRoleToInstanceProfileCommand, AddUserToGroupCommand, AttachGroupPolicyCommand, AttachRolePolicyCommand, AttachUserPolicyCommand, CreateAccessKeyCommand, CreateGroupCommand, CreateInstanceProfileCommand, CreateLoginProfileCommand, CreatePolicyCommand, CreatePolicyVersionCommand, CreateUserCommand, DeleteAccessKeyCommand, DeleteGroupCommand, DeleteGroupPolicyCommand, DeleteInstanceProfileCommand, DeleteLoginProfileCommand, DeletePolicyCommand, DeletePolicyVersionCommand, DeleteRolePolicyCommand, DeleteUserCommand, DeleteUserPermissionsBoundaryCommand, DeleteUserPolicyCommand, DetachGroupPolicyCommand, DetachRolePolicyCommand, DetachUserPolicyCommand, GetAccessKeyLastUsedCommand, GetGroupCommand, GetGroupPolicyCommand, GetInstanceProfileCommand, GetPolicyCommand, GetPolicyVersionCommand, GetRoleCommand, GetRolePolicyCommand, GetUserCommand, GetUserPolicyCommand, IAMClient, ListAccessKeysCommand, ListAttachedGroupPoliciesCommand, ListAttachedUserPoliciesCommand, ListEntitiesForPolicyCommand, ListGroupPoliciesCommand, ListGroupsForUserCommand, ListPoliciesCommand, ListPolicyTagsCommand, ListPolicyVersionsCommand, ListUserPoliciesCommand, NoSuchEntityException, PutGroupPolicyCommand, PutRolePolicyCommand, PutUserPermissionsBoundaryCommand, PutUserPolicyCommand, RemoveRoleFromInstanceProfileCommand, RemoveUserFromGroupCommand, TagPolicyCommand, TagUserCommand, UntagPolicyCommand, UntagUserCommand, UpdateAccessKeyCommand, UpdateLoginProfileCommand } from "@aws-sdk/client-iam";
|
|
9
9
|
import { CreateQueueCommand, DeleteQueueCommand, GetQueueAttributesCommand, GetQueueUrlCommand, ListQueueTagsCommand, QueueDoesNotExist, SQSClient, SetQueueAttributesCommand, TagQueueCommand, UntagQueueCommand } from "@aws-sdk/client-sqs";
|
|
10
10
|
import { CreateTopicCommand, DeleteTopicCommand, GetSubscriptionAttributesCommand, GetTopicAttributesCommand, InvalidParameterException, ListSubscriptionsByTopicCommand, ListTagsForResourceCommand, ListTopicsCommand, NotFoundException, SNSClient, SetTopicAttributesCommand, SubscribeCommand, TagResourceCommand, UnsubscribeCommand, UntagResourceCommand } from "@aws-sdk/client-sns";
|
|
11
11
|
import { AddPermissionCommand, CreateEventSourceMappingCommand, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteEventSourceMappingCommand, DeleteFunctionCommand, DeleteFunctionConcurrencyCommand, DeleteFunctionEventInvokeConfigCommand, DeleteFunctionUrlConfigCommand, DeleteLayerVersionCommand, GetEventSourceMappingCommand, GetFunctionCommand, GetFunctionConcurrencyCommand, GetFunctionEventInvokeConfigCommand, GetFunctionRecursionConfigCommand, GetFunctionUrlConfigCommand, GetLayerVersionByArnCommand, GetPolicyCommand as GetPolicyCommand$1, LambdaClient, ListTagsCommand, PublishLayerVersionCommand, PutFunctionConcurrencyCommand, PutFunctionEventInvokeConfigCommand, PutFunctionRecursionConfigCommand, RemovePermissionCommand, ResourceNotFoundException, TagResourceCommand as TagResourceCommand$1, UntagResourceCommand as UntagResourceCommand$1, UpdateEventSourceMappingCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
|
|
@@ -12315,7 +12315,8 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12315
12315
|
AttributeDefinitions: attributeDefinitions,
|
|
12316
12316
|
BillingMode: billingMode
|
|
12317
12317
|
};
|
|
12318
|
-
|
|
12318
|
+
const diagnostics = [];
|
|
12319
|
+
if (billingMode === "PROVISIONED") createParams.ProvisionedThroughput = derivePerCallProvisionedThroughput(properties, currentRegion, "min", diagnostics);
|
|
12319
12320
|
const streamSpecInput = properties["StreamSpecification"];
|
|
12320
12321
|
const needsStream = replicas.some((r) => r["Region"] !== currentRegion) || replicas.length > 1;
|
|
12321
12322
|
if (streamSpecInput != null) {
|
|
@@ -12336,7 +12337,6 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12336
12337
|
StreamViewType: "NEW_AND_OLD_IMAGES"
|
|
12337
12338
|
};
|
|
12338
12339
|
}
|
|
12339
|
-
const diagnostics = [];
|
|
12340
12340
|
let sdkIndexes;
|
|
12341
12341
|
try {
|
|
12342
12342
|
sdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, billingMode, "min", diagnostics);
|
|
@@ -12552,7 +12552,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
12552
12552
|
const onDemandCeilingLive = oldBilling === "PAY_PER_REQUEST" && newBilling === "PAY_PER_REQUEST";
|
|
12553
12553
|
const diagnostics = [];
|
|
12554
12554
|
const desiredCeilings = collectTableOnDemandCeilings(properties, currentRegion, diagnostics);
|
|
12555
|
-
const
|
|
12555
|
+
const flippingToProvisioned = oldBilling !== newBilling && newBilling === "PROVISIONED";
|
|
12556
|
+
if (flippingToProvisioned) derivePerCallProvisionedThroughput(properties, currentRegion, "seed", diagnostics);
|
|
12557
|
+
const desiredSdkIndexes = toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "min", flippingToProvisioned ? void 0 : diagnostics);
|
|
12558
|
+
if (flippingToProvisioned) toSdkGlobalSecondaryIndexes(properties, currentRegion, newBilling, "seed", diagnostics);
|
|
12556
12559
|
for (const replica of properties["Replicas"] ?? []) {
|
|
12557
12560
|
const replicaRegion = asRecord$1(replica)?.["Region"];
|
|
12558
12561
|
if (typeof replicaRegion !== "string" || replicaRegion === currentRegion) continue;
|
|
@@ -13232,9 +13235,11 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
13232
13235
|
const cause = regionErr instanceof Error ? regionErr : void 0;
|
|
13233
13236
|
throw new ProvisioningError(`Could not resolve client region for DynamoDB GlobalTable ${logicalId} delete — would risk dropping the local replica`, resourceType, logicalId, physicalId, cause);
|
|
13234
13237
|
}
|
|
13238
|
+
let preDeleteIndexes;
|
|
13235
13239
|
try {
|
|
13236
13240
|
const describe = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
|
|
13237
13241
|
const replicas = describe.Table?.Replicas ?? [];
|
|
13242
|
+
preDeleteIndexes = describe.Table?.GlobalSecondaryIndexes;
|
|
13238
13243
|
const tableIndexNames = (describe.Table?.GlobalSecondaryIndexes ?? []).map((gsi) => gsi.IndexName).filter((name) => typeof name === "string");
|
|
13239
13244
|
for (const replica of replicas) {
|
|
13240
13245
|
const region = replica.RegionName;
|
|
@@ -13265,6 +13270,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
13265
13270
|
}
|
|
13266
13271
|
}
|
|
13267
13272
|
try {
|
|
13273
|
+
if (hasTransitionalIndex(preDeleteIndexes)) {
|
|
13274
|
+
this.logger.debug(`Waiting for indexes on ${physicalId} to settle before DeleteTable (issue #1521)`);
|
|
13275
|
+
await this.waitForIndexesActive(physicalId, logicalId);
|
|
13276
|
+
}
|
|
13268
13277
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
13269
13278
|
await this.waitForTableGone(physicalId, logicalId);
|
|
13270
13279
|
this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
|
|
@@ -13679,34 +13688,78 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
13679
13688
|
* `waitForTableActive`, this tolerates any non-terminal status — the
|
|
13680
13689
|
* table may already be ACTIVE on the no-op path (already-disabled
|
|
13681
13690
|
* protection) or transition through UPDATING.
|
|
13691
|
+
*
|
|
13692
|
+
* **A table is ACTIVE while one of its INDEXES is still transitioning**, and
|
|
13693
|
+
* the next `UpdateTable` in the serialized sequence is then rejected with
|
|
13694
|
+
* `Attempt to change a resource which is still in use: Index is being
|
|
13695
|
+
* updated` (issue #1521). That is not hypothetical: it broke the
|
|
13696
|
+
* `PAY_PER_REQUEST -> PROVISIONED` flip that also drops a GSI — AWS applies
|
|
13697
|
+
* the flip to every index, and cdkd issued the index DELETE while the index
|
|
13698
|
+
* was still absorbing it. Reproduced on pristine `main`, three runs out of
|
|
13699
|
+
* four on 2026-08-10, and the failed deploy's own `cdkd destroy` then
|
|
13700
|
+
* ORPHANED the table for the same reason.
|
|
13701
|
+
*
|
|
13702
|
+
* So the wait has TWO conditions read off the SAME response (an extra
|
|
13703
|
+
* `DescribeTable` per wait would double this path's API traffic for
|
|
13704
|
+
* something the response already carries) — but they keep DIFFERENT failure
|
|
13705
|
+
* semantics, and conflating them is a real regression rather than a
|
|
13706
|
+
* simplification. A table that never reaches ACTIVE is a hard error: nothing
|
|
13707
|
+
* can proceed. A still-transitioning INDEX is best-effort: `IndexStatus`
|
|
13708
|
+
* stays `CREATING` for the whole BACKFILL of a newly added GSI, which on a
|
|
13709
|
+
* populated table routinely outlives any cap — so throwing there would fail
|
|
13710
|
+
* a deploy whose resources are all correct, and would do it on an unrelated
|
|
13711
|
+
* tag-only edit that merely ran while an earlier index was still building.
|
|
13712
|
+
* Past the cap the index half warns and proceeds, leaving AWS's own
|
|
13713
|
+
* rejection as the backstop, which is the behavior it had before #1521 for
|
|
13714
|
+
* every case except the one the wait exists to fix.
|
|
13682
13715
|
*/
|
|
13683
13716
|
async waitForTableActiveAfterUpdate(tableName, logicalId, maxAttempts = 600) {
|
|
13717
|
+
let tableReachedActive = false;
|
|
13684
13718
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
13685
|
-
|
|
13719
|
+
const response = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
|
|
13720
|
+
if (response.Table?.TableStatus === "ACTIVE") {
|
|
13721
|
+
tableReachedActive = true;
|
|
13722
|
+
if (!hasTransitionalIndex(response.Table?.GlobalSecondaryIndexes)) return;
|
|
13723
|
+
}
|
|
13686
13724
|
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
13687
13725
|
}
|
|
13726
|
+
if (tableReachedActive) {
|
|
13727
|
+
this.logger.warn(`Indexes on ${tableName} (${logicalId}) were still transitioning after ${maxAttempts}s — a large index backfill can outlive this wait. Proceeding; AWS rejects the next call if it is still too early.`);
|
|
13728
|
+
return;
|
|
13729
|
+
}
|
|
13688
13730
|
throw new ProvisioningError(`Table ${tableName} did not reach ACTIVE within ${maxAttempts}s after UpdateTable`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
|
|
13689
13731
|
}
|
|
13690
13732
|
/**
|
|
13691
13733
|
* Wait until every GSI reports `IndexStatus: ACTIVE` (Issue #1419).
|
|
13692
13734
|
*
|
|
13693
|
-
* `
|
|
13694
|
-
*
|
|
13695
|
-
* is
|
|
13696
|
-
* resource is not ready, so registering right after the GSI-add loop
|
|
13735
|
+
* A GSI added by `UpdateTable` leaves the TABLE ACTIVE while the index
|
|
13736
|
+
* itself is still `CREATING` — application-autoscaling rejects a target
|
|
13737
|
+
* whose resource is not ready, so registering right after the GSI-add loop
|
|
13697
13738
|
* would skip the very index the deploy just introduced.
|
|
13698
13739
|
*
|
|
13740
|
+
* Since issue #1521 the same distinction is enforced INSIDE
|
|
13741
|
+
* {@link waitForTableActiveAfterUpdate} (which now reads both conditions off
|
|
13742
|
+
* its own response), so this helper is no longer the only place that knows
|
|
13743
|
+
* it. It remains the explicit wait for the two callers that have no
|
|
13744
|
+
* table-status wait to piggyback on: the autoscaling registration above and
|
|
13745
|
+
* the pre-`DeleteTable` gate, which AWS rejects while an index is
|
|
13746
|
+
* transitioning. An index that is `DELETING` still reports, so the shared
|
|
13747
|
+
* {@link hasTransitionalIndex} predicate covers `CREATING` / `UPDATING` /
|
|
13748
|
+
* `DELETING` alike.
|
|
13749
|
+
*
|
|
13699
13750
|
* **Best-effort by design**: on timeout it warns and returns rather than
|
|
13700
13751
|
* throwing. A missed registration self-heals on the next deploy (the
|
|
13701
13752
|
* reconcile re-asserts anything not already present), whereas throwing
|
|
13702
13753
|
* here would fail a deploy whose actual resources are all correct. Index
|
|
13703
13754
|
* backfill on a large table can take a long time, hence the 15-minute cap.
|
|
13755
|
+
* The caller that follows it with a mutating call keeps AWS's own error as
|
|
13756
|
+
* the backstop for the case where even that cap is not enough.
|
|
13704
13757
|
*/
|
|
13705
13758
|
async waitForIndexesActive(tableName, logicalId) {
|
|
13706
13759
|
const maxAttempts = 900;
|
|
13707
13760
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
13708
13761
|
try {
|
|
13709
|
-
if (((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? [])
|
|
13762
|
+
if (!hasTransitionalIndex((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.GlobalSecondaryIndexes ?? [])) return;
|
|
13710
13763
|
} catch (err) {
|
|
13711
13764
|
this.logger.debug(`DescribeTable while waiting for indexes on ${tableName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
13712
13765
|
return;
|
|
@@ -13810,10 +13863,17 @@ function extractLocalDeletionProtection(props, region) {
|
|
|
13810
13863
|
* literal `ReadCapacityUnits`, which is taken before its auto-scaling
|
|
13811
13864
|
* members; 5 if absent.
|
|
13812
13865
|
*/
|
|
13813
|
-
function derivePerCallProvisionedThroughput(properties, region, source = "min") {
|
|
13866
|
+
function derivePerCallProvisionedThroughput(properties, region, source = "min", diagnostics) {
|
|
13867
|
+
const rawReplicas = properties["Replicas"];
|
|
13868
|
+
const rawRead = (Array.isArray(rawReplicas) ? rawReplicas : []).find((r) => asRecord$1(r)?.["Region"] === region)?.["ReadProvisionedThroughputSettings"];
|
|
13869
|
+
const rawWrite = properties["WriteProvisionedThroughputSettings"];
|
|
13870
|
+
const read = deriveReadCapacityUnits(asRecord$1(rawRead), source);
|
|
13871
|
+
const write = deriveWriteCapacityUnits(asRecord$1(rawWrite), source);
|
|
13872
|
+
if (read === void 0) reportUnresolvedProvisionedCapacity(rawRead, "Read", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, { blockName: "Replicas[local].ReadProvisionedThroughputSettings" });
|
|
13873
|
+
if (write === void 0) reportUnresolvedProvisionedCapacity(rawWrite, "Write", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, { blockName: "WriteProvisionedThroughputSettings" });
|
|
13814
13874
|
return {
|
|
13815
|
-
ReadCapacityUnits:
|
|
13816
|
-
WriteCapacityUnits:
|
|
13875
|
+
ReadCapacityUnits: read ?? DEFAULT_CAPACITY_UNITS,
|
|
13876
|
+
WriteCapacityUnits: write ?? DEFAULT_CAPACITY_UNITS
|
|
13817
13877
|
};
|
|
13818
13878
|
}
|
|
13819
13879
|
/**
|
|
@@ -13968,6 +14028,26 @@ function asRecord$1(value) {
|
|
|
13968
14028
|
* read-back comparison must therefore expect ABSENCE, not this value.
|
|
13969
14029
|
*/
|
|
13970
14030
|
const ON_DEMAND_LIMIT_RESET = -1;
|
|
14031
|
+
/**
|
|
14032
|
+
* The GSI states during which AWS refuses another `UpdateTable` or a
|
|
14033
|
+
* `DeleteTable` on the table (issue #1521) — a table is ACTIVE while one of
|
|
14034
|
+
* its indexes is still transitioning, which is what made the serialized
|
|
14035
|
+
* update sequence race and what left a table ORPHANED when the follow-up
|
|
14036
|
+
* destroy hit the same rule.
|
|
14037
|
+
*
|
|
14038
|
+
* `DELETING` is included because a dropped index keeps reporting until it is
|
|
14039
|
+
* actually gone. The test is on the TRANSITIONAL values rather than on
|
|
14040
|
+
* `!== 'ACTIVE'`: an absent or unrecognized status must not park the caller
|
|
14041
|
+
* for its whole cap on a table that is fine.
|
|
14042
|
+
*/
|
|
14043
|
+
const INDEX_TRANSITIONAL_STATUSES = /* @__PURE__ */ new Set([
|
|
14044
|
+
"CREATING",
|
|
14045
|
+
"UPDATING",
|
|
14046
|
+
"DELETING"
|
|
14047
|
+
]);
|
|
14048
|
+
function hasTransitionalIndex(indexes) {
|
|
14049
|
+
return (indexes ?? []).some((gsi) => INDEX_TRANSITIONAL_STATUSES.has(gsi.IndexStatus ?? ""));
|
|
14050
|
+
}
|
|
13971
14051
|
/** Coerce a CFn numeric (CFn is stringly-typed) to a finite number. */
|
|
13972
14052
|
function toFiniteNumber(value) {
|
|
13973
14053
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
@@ -14058,6 +14138,94 @@ function reportUnresolvedRawMember(rawValue, diagnostics, context) {
|
|
|
14058
14138
|
});
|
|
14059
14139
|
}
|
|
14060
14140
|
/**
|
|
14141
|
+
* Which member of a `*ProvisionedThroughputSettings` block to blame when the
|
|
14142
|
+
* derivation came back `undefined` (issue #1511).
|
|
14143
|
+
*
|
|
14144
|
+
* The derivation reports only "no number", so the member is re-derived here in
|
|
14145
|
+
* the SAME order {@link deriveWriteCapacityUnits} / {@link deriveReadCapacityUnits}
|
|
14146
|
+
* / {@link pickAutoScalingCapacity} consult members — the literal first, then
|
|
14147
|
+
* the auto-scaling block's `source`-selected member. Reaching this function at
|
|
14148
|
+
* all means every one of them failed, so the first DECLARED one is the value
|
|
14149
|
+
* the user was trying to set.
|
|
14150
|
+
*
|
|
14151
|
+
* `relPath` is relative to the block, `''` meaning the block ITSELF is the
|
|
14152
|
+
* unusable value (a string / array / bare unresolved intrinsic, or an object
|
|
14153
|
+
* carrying no capacity member at all).
|
|
14154
|
+
*
|
|
14155
|
+
* Returns `undefined` for an ABSENT block: defaulting there is the documented
|
|
14156
|
+
* pre-existing behavior for a template that never asked for a capacity, and
|
|
14157
|
+
* warning about it would shout on every deploy of a table that is fine.
|
|
14158
|
+
*/
|
|
14159
|
+
function blameUnresolvedProvisionedMember(rawSettings, side, source) {
|
|
14160
|
+
if (rawSettings === void 0) return void 0;
|
|
14161
|
+
const settings = asRecord$1(rawSettings);
|
|
14162
|
+
if (!settings || isUnresolvedIntrinsicBlock(settings)) return {
|
|
14163
|
+
relPath: "",
|
|
14164
|
+
value: rawSettings
|
|
14165
|
+
};
|
|
14166
|
+
const literal = settings[`${side}CapacityUnits`];
|
|
14167
|
+
if (literal !== void 0) return {
|
|
14168
|
+
relPath: `${side}CapacityUnits`,
|
|
14169
|
+
value: literal
|
|
14170
|
+
};
|
|
14171
|
+
const autoKey = `${side}CapacityAutoScalingSettings`;
|
|
14172
|
+
const rawAutoScaling = settings[autoKey];
|
|
14173
|
+
if (rawAutoScaling === void 0) return {
|
|
14174
|
+
relPath: "",
|
|
14175
|
+
value: rawSettings
|
|
14176
|
+
};
|
|
14177
|
+
const autoScaling = asRecord$1(rawAutoScaling);
|
|
14178
|
+
if (!autoScaling || isUnresolvedIntrinsicBlock(autoScaling)) return {
|
|
14179
|
+
relPath: autoKey,
|
|
14180
|
+
value: rawAutoScaling
|
|
14181
|
+
};
|
|
14182
|
+
const order = source === "seed" ? ["SeedCapacity", "MinCapacity"] : ["MinCapacity", "SeedCapacity"];
|
|
14183
|
+
for (const member of order) {
|
|
14184
|
+
const value = autoScaling[member];
|
|
14185
|
+
if (value !== void 0) return {
|
|
14186
|
+
relPath: `${autoKey}.${member}`,
|
|
14187
|
+
value
|
|
14188
|
+
};
|
|
14189
|
+
}
|
|
14190
|
+
return {
|
|
14191
|
+
relPath: autoKey,
|
|
14192
|
+
value: rawAutoScaling
|
|
14193
|
+
};
|
|
14194
|
+
}
|
|
14195
|
+
/**
|
|
14196
|
+
* Report a DECLARED provisioned capacity the template could not resolve — the
|
|
14197
|
+
* PROVISIONED twin of {@link reportUnresolvedRawMember} (issue #1511).
|
|
14198
|
+
*
|
|
14199
|
+
* The two sides need different wording because they have different outcomes,
|
|
14200
|
+
* which is exactly why #1503 deferred this half. An unreadable ON-DEMAND
|
|
14201
|
+
* ceiling is SUPPRESSED (nothing is sent, the live ceiling stands), so its
|
|
14202
|
+
* diagnostic says "nothing was sent". A provisioned table cannot suppress —
|
|
14203
|
+
* AWS requires `ProvisionedThroughput` on the table and on every GSI of it —
|
|
14204
|
+
* so the derivation falls through to {@link DEFAULT_CAPACITY_UNITS} and a
|
|
14205
|
+
* table the template explicitly sized is deployed at 5/5. That is a throttling
|
|
14206
|
+
* symptom days later rather than an error, which is what makes naming it here
|
|
14207
|
+
* worth a warning at all. The one provisioned site that CAN suppress is a
|
|
14208
|
+
* replica's `ProvisionedThroughputOverride` (absent = inherit the source
|
|
14209
|
+
* table), hence the `suppressed` outcome arm.
|
|
14210
|
+
*
|
|
14211
|
+
* Caller decides WHEN: only when the derivation returned `undefined` AND no
|
|
14212
|
+
* explicit SDK-shaped member is standing in for it, so the warning always
|
|
14213
|
+
* describes what actually reached AWS.
|
|
14214
|
+
*/
|
|
14215
|
+
function reportUnresolvedProvisionedCapacity(rawSettings, side, source, outcome, diagnostics, context) {
|
|
14216
|
+
if (!diagnostics) return;
|
|
14217
|
+
const blame = blameUnresolvedProvisionedMember(rawSettings, side, source);
|
|
14218
|
+
if (!blame) return;
|
|
14219
|
+
const path = blame.relPath ? `${context.blockName}.${blame.relPath}` : context.blockName;
|
|
14220
|
+
const consequence = "appliedUnits" in outcome ? `so cdkd sent ${outcome.appliedUnits} capacity units instead (its default — AWS requires a concrete provisioned capacity here, so there was nothing to suppress)` : outcome.suppressed;
|
|
14221
|
+
diagnostics.push({
|
|
14222
|
+
kind: "unresolved-member",
|
|
14223
|
+
...context.indexName !== void 0 && { indexName: context.indexName },
|
|
14224
|
+
member: blame.relPath ? blame.relPath.slice(blame.relPath.lastIndexOf(".") + 1) : context.blockName.slice(context.blockName.lastIndexOf(".") + 1),
|
|
14225
|
+
message: `${path} is declared but did not resolve to a number (${JSON.stringify(blame.value)?.slice(0, 80)}), ${consequence}. This is usually an unresolved intrinsic; resolve it to a literal to apply the capacity you declared.`
|
|
14226
|
+
});
|
|
14227
|
+
}
|
|
14228
|
+
/**
|
|
14061
14229
|
* Derive a single WRITE capacity number from a CFn
|
|
14062
14230
|
* `WriteProvisionedThroughputSettings` block.
|
|
14063
14231
|
*
|
|
@@ -14188,14 +14356,32 @@ function toSdkGlobalSecondaryIndexes(properties, region, billingMode, source = "
|
|
|
14188
14356
|
member: "OnDemandThroughput",
|
|
14189
14357
|
message: "an explicit OnDemandThroughput block was dropped because the table's BillingMode is PROVISIONED; AWS rejects on-demand ceilings on a provisioned table. Remove the block, or set BillingMode: PAY_PER_REQUEST."
|
|
14190
14358
|
});
|
|
14191
|
-
if (billingMode === "PROVISIONED")
|
|
14192
|
-
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
|
|
14197
|
-
|
|
14198
|
-
|
|
14359
|
+
if (billingMode === "PROVISIONED") {
|
|
14360
|
+
const rawLocalRead = localEntry?.["ReadProvisionedThroughputSettings"];
|
|
14361
|
+
const rawGsiRead = gsi["ReadProvisionedThroughputSettings"];
|
|
14362
|
+
const rawWrite = gsi["WriteProvisionedThroughputSettings"];
|
|
14363
|
+
const derivedRead = deriveReadCapacityUnits(asRecord$1(rawLocalRead), source) ?? deriveReadCapacityUnits(asRecord$1(rawGsiRead), source);
|
|
14364
|
+
const derivedWrite = deriveWriteCapacityUnits(asRecord$1(rawWrite), source);
|
|
14365
|
+
const explicitCovers = (member) => explicitProvisioned !== void 0 && toFiniteNumber(explicitProvisioned[member]) !== void 0;
|
|
14366
|
+
if (derivedRead === void 0 && !explicitCovers("ReadCapacityUnits")) {
|
|
14367
|
+
const fromReplica = rawLocalRead !== void 0;
|
|
14368
|
+
reportUnresolvedProvisionedCapacity(fromReplica ? rawLocalRead : rawGsiRead, "Read", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, {
|
|
14369
|
+
...typeof indexName === "string" && { indexName },
|
|
14370
|
+
blockName: fromReplica ? "Replicas[local].GlobalSecondaryIndexes[].ReadProvisionedThroughputSettings" : "ReadProvisionedThroughputSettings"
|
|
14371
|
+
});
|
|
14372
|
+
}
|
|
14373
|
+
if (derivedWrite === void 0 && !explicitCovers("WriteCapacityUnits")) reportUnresolvedProvisionedCapacity(rawWrite, "Write", source, { appliedUnits: DEFAULT_CAPACITY_UNITS }, diagnostics, {
|
|
14374
|
+
...typeof indexName === "string" && { indexName },
|
|
14375
|
+
blockName: "WriteProvisionedThroughputSettings"
|
|
14376
|
+
});
|
|
14377
|
+
sdk.ProvisionedThroughput = mergeExplicitThroughputBlock(explicitProvisioned, {
|
|
14378
|
+
ReadCapacityUnits: derivedRead ?? DEFAULT_CAPACITY_UNITS,
|
|
14379
|
+
WriteCapacityUnits: derivedWrite ?? DEFAULT_CAPACITY_UNITS
|
|
14380
|
+
}, ["ReadCapacityUnits", "WriteCapacityUnits"], diagnostics, {
|
|
14381
|
+
...typeof indexName === "string" && { indexName },
|
|
14382
|
+
blockName: "ProvisionedThroughput"
|
|
14383
|
+
});
|
|
14384
|
+
} else {
|
|
14199
14385
|
const rawWrite = asRecord$1(gsi["WriteOnDemandThroughputSettings"])?.["MaxWriteRequestUnits"];
|
|
14200
14386
|
const rawReadLocal = asRecord$1(localEntry?.["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
|
|
14201
14387
|
const rawReadGsi = asRecord$1(gsi["ReadOnDemandThroughputSettings"])?.["MaxReadRequestUnits"];
|
|
@@ -14364,7 +14550,12 @@ function toSdkReplicaThroughputOverrides(cfn, billingMode, diagnostics, context)
|
|
|
14364
14550
|
message: "an explicit OnDemandThroughputOverride was dropped because the table's BillingMode is PROVISIONED; AWS rejects on-demand ceilings on a provisioned table."
|
|
14365
14551
|
});
|
|
14366
14552
|
if (billingMode === "PROVISIONED") {
|
|
14367
|
-
const
|
|
14553
|
+
const rawRead = cfn["ReadProvisionedThroughputSettings"];
|
|
14554
|
+
const readCapacity = deriveReadCapacityUnits(asRecord$1(rawRead));
|
|
14555
|
+
if (readCapacity === void 0 && toFiniteNumber(explicitProvisioned?.["ReadCapacityUnits"]) === void 0) reportUnresolvedProvisionedCapacity(rawRead, "Read", "min", { suppressed: "so no ProvisionedThroughputOverride was sent and the replica keeps the source table's read capacity" }, diagnostics, {
|
|
14556
|
+
...context,
|
|
14557
|
+
blockName: "ReadProvisionedThroughputSettings"
|
|
14558
|
+
});
|
|
14368
14559
|
const merged = mergeExplicitThroughputBlock(explicitProvisioned, readCapacity !== void 0 ? { ReadCapacityUnits: readCapacity } : void 0, ["ReadCapacityUnits"], diagnostics, {
|
|
14369
14560
|
...context,
|
|
14370
14561
|
blockName: "ProvisionedThroughputOverride"
|
|
@@ -48878,6 +49069,208 @@ function isPlainObject(value) {
|
|
|
48878
49069
|
return typeof value === "object" && value !== null;
|
|
48879
49070
|
}
|
|
48880
49071
|
|
|
49072
|
+
//#endregion
|
|
49073
|
+
//#region src/analyzer/drift-principal-normalize.ts
|
|
49074
|
+
/**
|
|
49075
|
+
* IAM principal-form canonicalization for the drift comparator (issue #1515).
|
|
49076
|
+
*
|
|
49077
|
+
* AWS renders an IAM role / user principal inside a resource policy in TWO
|
|
49078
|
+
* forms and picks between them on its own schedule:
|
|
49079
|
+
*
|
|
49080
|
+
* - the ARN — `arn:aws:iam::<acct>:role/<name>`
|
|
49081
|
+
* - the principal's UNIQUE ID — `AROA…` (role) / `AIDA…` (user)
|
|
49082
|
+
*
|
|
49083
|
+
* The unique-id form is substituted when the referenced principal is
|
|
49084
|
+
* transiently unresolvable at write time (it is being created or replaced) and
|
|
49085
|
+
* AWS re-renders the ARN once it resolves. cdkd's deploy-time
|
|
49086
|
+
* `observedProperties` capture stores whichever form the read returned at that
|
|
49087
|
+
* moment, so a later `cdkd drift` can compare `AROA…` against the ARN for the
|
|
49088
|
+
* very same principal and report PERMANENT phantom drift — `--revert` writes
|
|
49089
|
+
* the recorded form back, AWS re-canonicalizes on write, and the next run
|
|
49090
|
+
* reports the identical difference (the class of issues #1096 / #1498, on a
|
|
49091
|
+
* different property).
|
|
49092
|
+
*
|
|
49093
|
+
* Unlike the order-normalization passes in `drift-normalize.ts`, this one
|
|
49094
|
+
* CANNOT be pure: the mapping between the two forms is not derivable from the
|
|
49095
|
+
* string. So the shape detection here is pure and an injected
|
|
49096
|
+
* {@link PrincipalUniqueIdResolver} does the one lookup, which lets the whole
|
|
49097
|
+
* pass be unit-tested without AWS and keeps the API cost at zero for the
|
|
49098
|
+
* overwhelmingly common case — a policy with no unique-id principal makes NO
|
|
49099
|
+
* AWS call at all.
|
|
49100
|
+
*
|
|
49101
|
+
* Both sides are rewritten to the ARN form, never one side only. The race that
|
|
49102
|
+
* produces the mismatch is symmetric (the 2026-08-10 evidence on the
|
|
49103
|
+
* `drift-revert` fixture had the baseline holding `AROA…` and AWS the ARN, but
|
|
49104
|
+
* the reverse ordering is equally reachable), and normalizing a single side is
|
|
49105
|
+
* the mistake `drift-normalize.ts`'s header already records for the unordered
|
|
49106
|
+
* passes — it manufactures drift on the `properties`-fallback baseline.
|
|
49107
|
+
*
|
|
49108
|
+
* FAILING IS VISIBLE, never silent: when the resolver cannot answer (the role
|
|
49109
|
+
* was deleted — which is exactly when AWS keeps the unique id forever — the
|
|
49110
|
+
* caller lacks `iam:GetRole`, or the ARN names another ACCOUNT / IAM PATH, for
|
|
49111
|
+
* which the name-only `GetRole` API would otherwise answer about the caller's
|
|
49112
|
+
* own same-named role), both sides are left untouched and the drift is still
|
|
49113
|
+
* reported. This pass only ever removes a difference it has PROVEN to be two
|
|
49114
|
+
* spellings of one principal; it can never hide a real principal change.
|
|
49115
|
+
*
|
|
49116
|
+
* One downstream note: because the comparator's inputs are canonicalized,
|
|
49117
|
+
* `cdkd drift --accept` records the ARN form for a policy subtree that drifts
|
|
49118
|
+
* for some OTHER reason while also carrying a canonicalized principal — i.e.
|
|
49119
|
+
* the accepted baseline can hold a spelling AWS did not literally return. That
|
|
49120
|
+
* is benign (it is the form AWS re-canonicalizes to, and a later run
|
|
49121
|
+
* normalizes either way) but it is a real difference from "state records
|
|
49122
|
+
* exactly what AWS said", so it is written down rather than left to be
|
|
49123
|
+
* rediscovered.
|
|
49124
|
+
*
|
|
49125
|
+
* Known bound: a policy carried as a JSON STRING rather than a parsed object is
|
|
49126
|
+
* not walked. Re-serializing a parsed string would risk changing its formatting
|
|
49127
|
+
* and manufacturing a different phantom drift, so the pass is scoped to the
|
|
49128
|
+
* object shape — which is what `AWS::S3::BucketPolicy` and the other
|
|
49129
|
+
* `PolicyDocument`-typed properties carry in cdkd state.
|
|
49130
|
+
*/
|
|
49131
|
+
/**
|
|
49132
|
+
* An IAM unique id as it appears in a rendered policy. The prefix set is
|
|
49133
|
+
* deliberately narrow — `AROA` (role) and `AIDA` (user) are the only two that
|
|
49134
|
+
* can stand in for an ARN at a `Principal.AWS` position; `AGPA` (group),
|
|
49135
|
+
* `AKIA` (access key) and friends are never rendered there.
|
|
49136
|
+
*/
|
|
49137
|
+
const PRINCIPAL_UNIQUE_ID_RE = /^A(?:ROA|IDA)[A-Z0-9]{6,}$/;
|
|
49138
|
+
/** An IAM role / user ARN — the two shapes a unique id can stand in for. */
|
|
49139
|
+
const IAM_PRINCIPAL_ARN_RE = /^arn:[a-z0-9-]+:iam::\d{12}:(role|user)\/(.+)$/;
|
|
49140
|
+
/**
|
|
49141
|
+
* Split an IAM role / user principal ARN into what the lookup API needs, or
|
|
49142
|
+
* `undefined` when the string is not one (a service principal, an
|
|
49143
|
+
* `assumed-role` STS ARN, `*`, …).
|
|
49144
|
+
*
|
|
49145
|
+
* Exported so the resolver in `drift.ts` recognizes exactly the ARNs
|
|
49146
|
+
* {@link collectPrincipalArns} collects — one regex, so the collector and the
|
|
49147
|
+
* lookup cannot drift apart and leave an ARN gathered but never resolvable.
|
|
49148
|
+
*/
|
|
49149
|
+
function parseIamPrincipalArn(arn) {
|
|
49150
|
+
const match = IAM_PRINCIPAL_ARN_RE.exec(arn);
|
|
49151
|
+
if (!match) return void 0;
|
|
49152
|
+
const name = match[2].split("/").pop();
|
|
49153
|
+
if (!name) return void 0;
|
|
49154
|
+
return {
|
|
49155
|
+
kind: match[1],
|
|
49156
|
+
name
|
|
49157
|
+
};
|
|
49158
|
+
}
|
|
49159
|
+
/** Every string at a `Principal` / `NotPrincipal` position, at any depth. */
|
|
49160
|
+
function collectPrincipalStrings(value, out) {
|
|
49161
|
+
if (Array.isArray(value)) {
|
|
49162
|
+
for (const element of value) collectPrincipalStrings(element, out);
|
|
49163
|
+
return;
|
|
49164
|
+
}
|
|
49165
|
+
if (!value || typeof value !== "object") return;
|
|
49166
|
+
for (const [key, member] of Object.entries(value)) {
|
|
49167
|
+
if (key === "Principal" || key === "NotPrincipal") pushPrincipalValue(member, out);
|
|
49168
|
+
collectPrincipalStrings(member, out);
|
|
49169
|
+
}
|
|
49170
|
+
}
|
|
49171
|
+
/**
|
|
49172
|
+
* The strings a `Principal` / `NotPrincipal` value contributes: the bare string
|
|
49173
|
+
* form, and the `AWS` member of the object form (string or array). `Service` /
|
|
49174
|
+
* `Federated` / `CanonicalUser` are deliberately NOT read — none of them can
|
|
49175
|
+
* carry an IAM unique id or a role ARN.
|
|
49176
|
+
*/
|
|
49177
|
+
function pushPrincipalValue(value, out) {
|
|
49178
|
+
if (typeof value === "string") {
|
|
49179
|
+
out.push(value);
|
|
49180
|
+
return;
|
|
49181
|
+
}
|
|
49182
|
+
if (Array.isArray(value)) {
|
|
49183
|
+
for (const element of value) if (typeof element === "string") out.push(element);
|
|
49184
|
+
return;
|
|
49185
|
+
}
|
|
49186
|
+
if (!value || typeof value !== "object") return;
|
|
49187
|
+
const awsMember = value["AWS"];
|
|
49188
|
+
if (typeof awsMember === "string") out.push(awsMember);
|
|
49189
|
+
else if (Array.isArray(awsMember)) {
|
|
49190
|
+
for (const element of awsMember) if (typeof element === "string") out.push(element);
|
|
49191
|
+
}
|
|
49192
|
+
}
|
|
49193
|
+
/**
|
|
49194
|
+
* Both interesting principal kinds in ONE walk — the pass runs per resource on
|
|
49195
|
+
* both comparison sides, so walking the bag twice to answer two questions about
|
|
49196
|
+
* the same strings is pure cost.
|
|
49197
|
+
*/
|
|
49198
|
+
function collectPrincipalForms(value) {
|
|
49199
|
+
const strings = [];
|
|
49200
|
+
collectPrincipalStrings(value, strings);
|
|
49201
|
+
const uniqueIds = /* @__PURE__ */ new Set();
|
|
49202
|
+
const arns = /* @__PURE__ */ new Set();
|
|
49203
|
+
for (const s of strings) if (PRINCIPAL_UNIQUE_ID_RE.test(s)) uniqueIds.add(s);
|
|
49204
|
+
else if (IAM_PRINCIPAL_ARN_RE.test(s)) arns.add(s);
|
|
49205
|
+
return {
|
|
49206
|
+
uniqueIds,
|
|
49207
|
+
arns
|
|
49208
|
+
};
|
|
49209
|
+
}
|
|
49210
|
+
/**
|
|
49211
|
+
* Replace every principal string that is a KNOWN unique id with the ARN it was
|
|
49212
|
+
* proven to stand for. Only `Principal` / `NotPrincipal` positions are touched
|
|
49213
|
+
* — a unique id appearing in, say, a `Condition` value is left alone, where the
|
|
49214
|
+
* two forms are NOT interchangeable.
|
|
49215
|
+
*/
|
|
49216
|
+
function rewritePrincipalUniqueIds(value, arnByUniqueId) {
|
|
49217
|
+
if (Array.isArray(value)) return value.map((el) => rewritePrincipalUniqueIds(el, arnByUniqueId));
|
|
49218
|
+
if (!value || typeof value !== "object") return value;
|
|
49219
|
+
const out = {};
|
|
49220
|
+
for (const [key, member] of Object.entries(value)) out[key] = key === "Principal" || key === "NotPrincipal" ? rewritePrincipalValue(member, arnByUniqueId) : rewritePrincipalUniqueIds(member, arnByUniqueId);
|
|
49221
|
+
return out;
|
|
49222
|
+
}
|
|
49223
|
+
function rewritePrincipalValue(value, arnByUniqueId) {
|
|
49224
|
+
const swap = (s) => typeof s === "string" ? arnByUniqueId.get(s) ?? s : rewritePrincipalUniqueIds(s, arnByUniqueId);
|
|
49225
|
+
if (typeof value === "string") return swap(value);
|
|
49226
|
+
if (Array.isArray(value)) return value.map(swap);
|
|
49227
|
+
if (!value || typeof value !== "object") return value;
|
|
49228
|
+
const out = {};
|
|
49229
|
+
for (const [key, member] of Object.entries(value)) {
|
|
49230
|
+
if (key !== "AWS") {
|
|
49231
|
+
out[key] = rewritePrincipalUniqueIds(member, arnByUniqueId);
|
|
49232
|
+
continue;
|
|
49233
|
+
}
|
|
49234
|
+
out[key] = Array.isArray(member) ? member.map(swap) : swap(member);
|
|
49235
|
+
}
|
|
49236
|
+
return out;
|
|
49237
|
+
}
|
|
49238
|
+
/**
|
|
49239
|
+
* Canonicalize the two comparison sides so that a principal AWS rendered as a
|
|
49240
|
+
* unique id on one side and as an ARN on the other stops reading as drift.
|
|
49241
|
+
*
|
|
49242
|
+
* Short-circuits — with NO resolver call — unless BOTH a unique id and an IAM
|
|
49243
|
+
* ARN are present across the two sides, since a unique id with no candidate ARN
|
|
49244
|
+
* opposite it has nothing to be equal to.
|
|
49245
|
+
*/
|
|
49246
|
+
async function canonicalizePrincipalUniqueIds(baseline, aws, resolve) {
|
|
49247
|
+
const baselineForms = collectPrincipalForms(baseline);
|
|
49248
|
+
const awsForms = collectPrincipalForms(aws);
|
|
49249
|
+
const uniqueIds = /* @__PURE__ */ new Set([...baselineForms.uniqueIds, ...awsForms.uniqueIds]);
|
|
49250
|
+
if (uniqueIds.size === 0) return {
|
|
49251
|
+
baseline,
|
|
49252
|
+
aws
|
|
49253
|
+
};
|
|
49254
|
+
const arns = /* @__PURE__ */ new Set([...baselineForms.arns, ...awsForms.arns]);
|
|
49255
|
+
if (arns.size === 0) return {
|
|
49256
|
+
baseline,
|
|
49257
|
+
aws
|
|
49258
|
+
};
|
|
49259
|
+
const arnByUniqueId = /* @__PURE__ */ new Map();
|
|
49260
|
+
for (const arn of arns) {
|
|
49261
|
+
const uniqueId = await resolve(arn);
|
|
49262
|
+
if (uniqueId !== void 0 && uniqueIds.has(uniqueId)) arnByUniqueId.set(uniqueId, arn);
|
|
49263
|
+
}
|
|
49264
|
+
if (arnByUniqueId.size === 0) return {
|
|
49265
|
+
baseline,
|
|
49266
|
+
aws
|
|
49267
|
+
};
|
|
49268
|
+
return {
|
|
49269
|
+
baseline: rewritePrincipalUniqueIds(baseline, arnByUniqueId),
|
|
49270
|
+
aws: rewritePrincipalUniqueIds(aws, arnByUniqueId)
|
|
49271
|
+
};
|
|
49272
|
+
}
|
|
49273
|
+
|
|
48881
49274
|
//#endregion
|
|
48882
49275
|
//#region src/analyzer/drift-cc-api-deny-list.ts
|
|
48883
49276
|
/**
|
|
@@ -49092,11 +49485,12 @@ async function driftCommand(stacks, options) {
|
|
|
49092
49485
|
registerAllProviders(providerRegistry);
|
|
49093
49486
|
providerRegistry.setCustomResourceResponseBucket(bucket);
|
|
49094
49487
|
const ccApiFallback = new CloudControlProvider();
|
|
49488
|
+
const resolvePrincipalUniqueId = createIamPrincipalUniqueIdResolver(awsClients);
|
|
49095
49489
|
const targetRefs = resolveTargetRefs(stacks, await stateBackend.listStacks(), options);
|
|
49096
49490
|
const reports = [];
|
|
49097
49491
|
for (const ref of targetRefs) {
|
|
49098
49492
|
if (!ref.region) throw new Error(`Stack '${ref.stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${ref.stackName}' (or any cdkd write) to migrate it to the region-scoped layout, then re-run drift detection.`);
|
|
49099
|
-
const report = await runDriftForStack(ref.stackName, ref.region, stateBackend, providerRegistry, ccApiFallback);
|
|
49493
|
+
const report = await runDriftForStack(ref.stackName, ref.region, stateBackend, providerRegistry, ccApiFallback, resolvePrincipalUniqueId);
|
|
49100
49494
|
reports.push(report);
|
|
49101
49495
|
}
|
|
49102
49496
|
if (options.json) writeJsonReport(reports);
|
|
@@ -49158,13 +49552,75 @@ function resolveTargetRefs(stacks, stateRefs, options) {
|
|
|
49158
49552
|
return out;
|
|
49159
49553
|
}
|
|
49160
49554
|
/**
|
|
49555
|
+
* The live half of the #1515 principal canonicalization: resolve an IAM role /
|
|
49556
|
+
* user ARN to that principal's unique id (`AROA…` / `AIDA…`).
|
|
49557
|
+
*
|
|
49558
|
+
* Results are cached per ARN for the whole command — INCLUDING the failures, so
|
|
49559
|
+
* a deleted role (the case where AWS keeps the unique id forever, and therefore
|
|
49560
|
+
* the likeliest one to appear here) costs exactly one API call rather than one
|
|
49561
|
+
* per resource that references it.
|
|
49562
|
+
*
|
|
49563
|
+
* Every failure resolves to `undefined` rather than throwing: a missing
|
|
49564
|
+
* `iam:GetRole` permission, a cross-account principal, or a deleted role must
|
|
49565
|
+
* leave the comparison exactly as it was — the drift is then reported, which is
|
|
49566
|
+
* the safe direction. Drift detection must never fail because a cosmetic
|
|
49567
|
+
* normalization could not run.
|
|
49568
|
+
*
|
|
49569
|
+
* **The response ARN is compared back to the requested one, and that check is
|
|
49570
|
+
* what makes the "never hides a real change" claim true rather than aspirational.**
|
|
49571
|
+
* `GetRole` / `GetUser` are account-local and take a NAME, not an ARN, so the
|
|
49572
|
+
* lookup silently answers about the CALLER's account and about the wrong IAM
|
|
49573
|
+
* path: `arn:aws:iam::<other-acct>:role/Foo` and
|
|
49574
|
+
* `arn:aws:iam::<self>:role/prod/Foo` would both come back as this account's
|
|
49575
|
+
* root-path `Foo`, and the pass would then "prove" two DIFFERENT principals
|
|
49576
|
+
* equal and collapse a genuine drift. Round-tripping the ARN costs nothing and
|
|
49577
|
+
* turns both cases into an unresolved lookup, i.e. drift reported.
|
|
49578
|
+
*/
|
|
49579
|
+
function createIamPrincipalUniqueIdResolver(awsClients) {
|
|
49580
|
+
const cache = /* @__PURE__ */ new Map();
|
|
49581
|
+
let warnedOnDenied = false;
|
|
49582
|
+
return async (arn) => {
|
|
49583
|
+
if (cache.has(arn)) return cache.get(arn);
|
|
49584
|
+
let uniqueId;
|
|
49585
|
+
const principal = parseIamPrincipalArn(arn);
|
|
49586
|
+
if (principal) try {
|
|
49587
|
+
let entityArn;
|
|
49588
|
+
let entityId;
|
|
49589
|
+
if (principal.kind === "role") {
|
|
49590
|
+
const role = (await awsClients.iam.send(new GetRoleCommand({ RoleName: principal.name }))).Role;
|
|
49591
|
+
entityArn = role?.Arn;
|
|
49592
|
+
entityId = role?.RoleId;
|
|
49593
|
+
} else {
|
|
49594
|
+
const user = (await awsClients.iam.send(new GetUserCommand({ UserName: principal.name }))).User;
|
|
49595
|
+
entityArn = user?.Arn;
|
|
49596
|
+
entityId = user?.UserId;
|
|
49597
|
+
}
|
|
49598
|
+
if (entityArn !== void 0 && entityArn.toLowerCase() === arn.toLowerCase()) uniqueId = entityId;
|
|
49599
|
+
else getLogger().debug(`Principal ${arn} resolved to ${entityArn ?? "no entity"} — same name in this account or under another IAM path; leaving the policy principal comparison untouched.`);
|
|
49600
|
+
} catch (error) {
|
|
49601
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
49602
|
+
const name = error instanceof Error ? error.name : "";
|
|
49603
|
+
const status = error?.$metadata?.httpStatusCode;
|
|
49604
|
+
const denied = /^AccessDenied|^NotAuthorized/i.test(name) || status === 403;
|
|
49605
|
+
if (!warnedOnDenied && denied) {
|
|
49606
|
+
warnedOnDenied = true;
|
|
49607
|
+
getLogger().warn(`Cannot read IAM principals (${name || "access denied"}). A resource policy whose principal AWS rendered as a unique id (AROA…/AIDA…) may therefore report drift that is only a spelling difference; grant iam:GetRole / iam:GetUser to resolve it, or re-run with --verbose for the full error.`);
|
|
49608
|
+
}
|
|
49609
|
+
getLogger().debug(`Could not resolve the unique id of principal ${arn} (${message}); leaving the policy principal comparison untouched.`);
|
|
49610
|
+
if (isThrottlingError(error)) return void 0;
|
|
49611
|
+
}
|
|
49612
|
+
cache.set(arn, uniqueId);
|
|
49613
|
+
return uniqueId;
|
|
49614
|
+
};
|
|
49615
|
+
}
|
|
49616
|
+
/**
|
|
49161
49617
|
* Run drift detection for one stack and shape the per-resource outcomes
|
|
49162
49618
|
* into a {@link StackDriftReport}. The state object + etag are stored on
|
|
49163
49619
|
* the report so `--accept` can write back without a re-read, and
|
|
49164
49620
|
* `--revert` can pass the captured AWS-current snapshot to
|
|
49165
49621
|
* `provider.update` as the `previousProperties` argument.
|
|
49166
49622
|
*/
|
|
49167
|
-
async function runDriftForStack(stackName, region, stateBackend, providerRegistry, ccApiFallback) {
|
|
49623
|
+
async function runDriftForStack(stackName, region, stateBackend, providerRegistry, ccApiFallback, resolvePrincipalUniqueId) {
|
|
49168
49624
|
const result = await stateBackend.getState(stackName, region);
|
|
49169
49625
|
if (!result) throw new Error(`No state found for stack '${stackName}' (${region}). Run 'cdkd state list' to see available stacks.`);
|
|
49170
49626
|
return await withStackName(stackName, async () => {
|
|
@@ -49230,7 +49686,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
49230
49686
|
const useObserved = resource.observedProperties !== void 0;
|
|
49231
49687
|
const baseline = useObserved ? resource.observedProperties : resource.properties ?? {};
|
|
49232
49688
|
const observedIgnorePaths = useObserved ? undeclaredEmptyObservedKeys(resource.observedProperties, resource.properties ?? {}) : [];
|
|
49233
|
-
const
|
|
49689
|
+
const normalized = await canonicalizePrincipalUniqueIds(baseline, aws, resolvePrincipalUniqueId);
|
|
49690
|
+
const changes = calculateResourceDrift(normalized.baseline, normalized.aws, {
|
|
49234
49691
|
ignorePaths: observedIgnorePaths.length ? [...ignorePaths, ...observedIgnorePaths] : ignorePaths,
|
|
49235
49692
|
unionWalkObjects: useObserved,
|
|
49236
49693
|
unorderedPaths
|
|
@@ -66706,7 +67163,7 @@ function createMigrateCommand() {
|
|
|
66706
67163
|
*/
|
|
66707
67164
|
function buildProgram() {
|
|
66708
67165
|
const program = new Command();
|
|
66709
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.
|
|
67166
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.278.38");
|
|
66710
67167
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
66711
67168
|
const { profile } = actionCommand.optsWithGlobals();
|
|
66712
67169
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|