@go-to-k/cdkd 0.283.32 → 0.283.34
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-CVoisQ0L.js → asg-provider-B1AQpZfU.js} +2 -2
- package/dist/{asg-provider-CVoisQ0L.js.map → asg-provider-B1AQpZfU.js.map} +1 -1
- package/dist/cli.js +626 -71
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-rYTNzst0.js → deploy-engine-DlEg9thJ.js} +4 -4
- package/dist/{deploy-engine-rYTNzst0.js.map → deploy-engine-DlEg9thJ.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as getAccountInfo, $t as
|
|
2
|
+
import { $ as getAccountInfo, $t as formatDockerLoginError, A as cyan, An as resolveBucketRegion, At as TemplateParser, B as stateKeySecretExposure, Bn as LocalInvokeBuildError$1, Bt as loadPublishableAssetManifest, C as makeCanonicalizePropertiesFn, Cn as expectedOwnerParam, Ct as s3BucketWebsiteUrl, D as renderStatefulReason, Dn as AssemblyReader, Dt as describeTypeWithThrottleRetry, E as isStatefulRecreateTargetSync, En as derivePartitionAndUrlSuffix, Et as INTRINSIC_KEYS, F as collectDeclaredOutputNames, Ft as AssetPublisher, G as findActionableSilentDrops, Gn as NestedStackChildDirectDestroyError, Gt as BOOTSTRAP_MARKER_PREFIX, H as collectInlinePolicyNamesManagedBySiblings, Hn as LocalStartServiceError, Ht as escapeRegExp, I as collectPublishedOutputNames, In as CdkdError, It as stringifyValue, J as slowCcOperationTimeoutMs, Jn as ResourceTimeoutError, Jt as parseBootstrapMarker, K as findSilentDropProperties, Kn as PartialFailureError, Kt as ensureAssetStorage, L as exportAliasCollisionScrubWarning, Lt as WorkGraph, M as green, Mn as getAwsClients, Mt as S3StateBackend, N as red, Nn as resetAwsClients, Nt as rebuildClientForBucketRegion, O as formatResourceLine, On as processStackMessages, Ot as withRetry, P as yellow, Pn as setAwsClients, Pt as shouldRetainResource, Q as cfnRefValueFromPhysicalId, Qt as buildDockerImage, R as isExportAliasCollision, Rt as buildAssetRedirectMap, S as unsupportedFinalSnapshotError, Sn as uploadCfnTemplate, St as s3BucketRegionalDomainName, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as canonicalizeRegion, Tt as DiffCalculator, U as clearOnUpdateRemoval, Ut as stripControlChars, V as IAMRoleProvider, Vn as LocalMigrateError, Vt as rewriteTemplateAssetReferences, W as ProviderRegistry, Wn as MissingCdkCliError, Wt as AssetModeResolver, X as isTerminationProtectionPropagationError, Xn as StackHasActiveImportsError, Xt as validateContainerRepoName, Y as disableInstanceApiTermination, Yn as ResourceUpdateNotSupportedError, Yt as validateAssetBucketName, Z as IntrinsicFunctionResolver, Zn as StackTerminationProtectionError, Zt as buildDenyExternalAccessPolicy, _ as buildFinalSnapshotIdentifier, _n as warnDeprecatedNoPrefixCliFlag, _t as redactSecretsForState, a as DeploymentEventsStore, an as Synthesizer, ar as isRetryableTransientError, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as MIGRATE_TMP_PREFIX, bt as s3BucketDomainName, c as replayFailedOperations, cn as getLegacyStateBucketName, ct as readConfigString, d as deleteSkipReason, dn as resolveCaptureObservedState, dt as requireConfigObject, en as getDockerCmd, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveSkipPrefix, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as stateBucketExistenceConfirmed, gt as maskSecretsInText, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveUseCdkBootstrapAssets, ht as TEMPLATE_SOURCED_RULES, i as DeploymentEventsReader, in as getDockerImageBySourceHash, ir as isMarkedNonRetryable, it as assertRegionMatch, j as gray, jn as AwsClients, jt as LockManager, k as bold, kt as DagBuilder, l as replayRollback, ln as resolveApp, lt as replayWarn, m as computeImplicitDeleteEdges, mn as resolveStateBucketWithDefaultAndSource, mt as STATE_SOURCED_READBACK_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as runDockerStreaming, nr as normalizeAwsError, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as synthesisStatusMessage, or as isThrottlingError, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveStateBucketWithDefault, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ProvisioningError, qt as getBootstrapMarkerKey, r as DeployEngine, rn as AssetManifestLoader, rr as withErrorHandling, rt as resolveExplicitPhysicalId, s as planRollback, sn as getDefaultStateBucketName, sr as markNonRetryable, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as runDockerForeground, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveAutoAssetStorage, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as CFN_TEMPLATE_BODY_LIMIT, vt as scrubResourceRecord, w as extractDeploymentEventError, wn as PARTITION_TABLE, wt as applyRoleArnIfSet, x as refusesFinalSnapshot, xn as findLargeInlineResources, xt as s3BucketDualStackDomainName, y as createPreDeleteFinalSnapshot, yn as CFN_TEMPLATE_URL_LIMIT, yt as s3BucketArn, z as secretBearingStateKeyWarning, zn as DeployCancelledError, zt as createAssetRedirectResolver } from "./deploy-engine-DlEg9thJ.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-B1AQpZfU.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";
|
|
@@ -14282,27 +14282,15 @@ function hasTransitionalIndex(indexes) {
|
|
|
14282
14282
|
* Deliberately NOT keyed on the exception NAME: AWS reports this as a plain
|
|
14283
14283
|
* `ResourceInUseException`, which is the same name it uses for genuinely
|
|
14284
14284
|
* terminal conflicts (deleting a table that is `CREATING`, creating one that
|
|
14285
|
-
* already exists). Retrying on the name would
|
|
14286
|
-
*
|
|
14285
|
+
* already exists). Retrying on the name would spend the WHOLE budget on them —
|
|
14286
|
+
* a full settle poll plus a backoff step per attempt, i.e. minutes — before
|
|
14287
|
+
* failing identically.
|
|
14287
14288
|
*/
|
|
14288
14289
|
const INDEX_BUSY_DELETE_MESSAGE = /cannot delete table while indexes are being/i;
|
|
14289
14290
|
function isIndexBusyDeleteError(message) {
|
|
14290
14291
|
return INDEX_BUSY_DELETE_MESSAGE.test(message);
|
|
14291
14292
|
}
|
|
14292
14293
|
/**
|
|
14293
|
-
* Retry budget for the index-busy `DeleteTable` refusal (issue #1830).
|
|
14294
|
-
*
|
|
14295
|
-
* Left at `withRetry`'s own default of 8 rather than raised. Named rather than
|
|
14296
|
-
* inlined because passing it at all is what opts this call out of
|
|
14297
|
-
* `withRetry`'s dense IAM-propagation schedule, which would be wrong for a
|
|
14298
|
-
* condition measured in seconds-to-minutes rather than sub-second.
|
|
14299
|
-
*
|
|
14300
|
-
* The count MULTIPLIES the per-attempt re-arm poll below, so the two have to
|
|
14301
|
-
* be read together — see {@link DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS} for the
|
|
14302
|
-
* arithmetic and for why the poll is not a 15-minute one.
|
|
14303
|
-
*/
|
|
14304
|
-
const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
|
|
14305
|
-
/**
|
|
14306
14294
|
* `DescribeTable` polls (~1s apart) the index-busy `DeleteTable` retry spends
|
|
14307
14295
|
* re-arming BEFORE each retry (issue #1830) — it returns on the first one that
|
|
14308
14296
|
* reports every index settled.
|
|
@@ -14310,30 +14298,57 @@ const DELETE_INDEX_BUSY_MAX_RETRIES = 8;
|
|
|
14310
14298
|
* Bounded well under the 15-minute default `GlobalTable`'s pre-delete #1521
|
|
14311
14299
|
* gate uses, because THAT default is sized for a wait that runs once while
|
|
14312
14300
|
* this one runs per retry: the caller's wall clock is
|
|
14313
|
-
* `
|
|
14314
|
-
*
|
|
14315
|
-
*
|
|
14316
|
-
*
|
|
14317
|
-
*
|
|
14318
|
-
*
|
|
14319
|
-
*
|
|
14301
|
+
* `its retry budget x this x ~1.2s per poll + withRetry's backoff`, and `destroy-runner.ts` runs the delete under a per-resource
|
|
14302
|
+
* deadline (30 min by default; neither DynamoDB provider declares a
|
|
14303
|
+
* `getMinResourceTimeoutMs` to lift it). At 900 polls the product would be
|
|
14304
|
+
* ~4.2h, so a genuinely stuck index would produce a 30-minute wait ending in a
|
|
14305
|
+
* generic `ResourceTimeoutError` that never mentions indexes. At 60 the LOOP's
|
|
14306
|
+
* worst case is ~18.4 min on `AWS::DynamoDB::Table` and ~10.4 min on
|
|
14307
|
+
* `AWS::DynamoDB::GlobalTable` — see {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES}
|
|
14308
|
+
* for every term of that product and for why the two budgets differ — and the
|
|
14309
|
+
* user gets AWS's own actionable sentence instead.
|
|
14320
14310
|
*
|
|
14321
14311
|
* **What that deadline actually wraps**, since the arithmetic depends on it:
|
|
14322
14312
|
* NOT one `delete()` but `destroy-runner.ts`'s whole outer retry loop, which
|
|
14323
14313
|
* calls `delete()` up to 4 times (`maxAttempts = 3`) inside the one deadline.
|
|
14324
|
-
* Four times ~
|
|
14314
|
+
* Four times ~18.4 min would blow straight through 30 min. It does not, and the
|
|
14325
14315
|
* reason is load-bearing rather than incidental: that outer loop only retries
|
|
14326
14316
|
* when `isRetryableTransientError` says so, and this refusal matches no entry
|
|
14327
14317
|
* in `RETRYABLE_ERROR_MESSAGE_PATTERNS` (verified against the real classifier —
|
|
14328
14318
|
* both AWS's raw sentence and the `Failed to delete DynamoDB table ...` wrap
|
|
14329
14319
|
* return false), so the outer loop runs `delete()` exactly ONCE for it and the
|
|
14330
14320
|
* budget below is not re-multiplied. Adding this message to those patterns
|
|
14331
|
-
* would silently make the worst case ~
|
|
14321
|
+
* would silently make the worst case ~74 min on `AWS::DynamoDB::Table` — well
|
|
14322
|
+
* over the deadline.
|
|
14332
14323
|
*
|
|
14333
14324
|
* The FIRST attempt is unaffected — it does not re-arm at all.
|
|
14334
14325
|
*/
|
|
14335
14326
|
const DELETE_INDEX_BUSY_REARM_MAX_ATTEMPTS = 60;
|
|
14336
14327
|
/**
|
|
14328
|
+
* Sleep between two `DescribeTable` polls of {@link waitForIndexesSettled}.
|
|
14329
|
+
*
|
|
14330
|
+
* Named rather than inlined because it is the unit every poll budget on the
|
|
14331
|
+
* DynamoDB DELETE path is denominated in — a poll count only becomes a wall
|
|
14332
|
+
* clock through this, and the deadline arguments at
|
|
14333
|
+
* {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES} multiply it. Each of those counts
|
|
14334
|
+
* READS it, which is what makes the claim checkable rather than a convention:
|
|
14335
|
+
* this module's 60-poll re-arm; `GlobalTable`'s 900-poll #1521 pre-delete gate
|
|
14336
|
+
* (and the same 900-poll default its auto-scaling caller takes, since both go
|
|
14337
|
+
* through `waitForIndexesActive` -> {@link waitForIndexesSettled});
|
|
14338
|
+
* `GlobalTable`'s `waitForReplicaGone` and `waitForTableGone` at 600 polls
|
|
14339
|
+
* each; and the `Table` provider's 60-poll `waitForTableActiveAfterUpdate`.
|
|
14340
|
+
*
|
|
14341
|
+
* Deliberately NOT claimed for the whole file: `waitForReplicaActive` keeps its
|
|
14342
|
+
* own `1000`, and is left alone because it runs on the CREATE / UPDATE path,
|
|
14343
|
+
* which no arithmetic here prices.
|
|
14344
|
+
*
|
|
14345
|
+
* It is a FLOOR on the per-poll cost, never the whole of it — each poll also
|
|
14346
|
+
* pays a `DescribeTable` round trip, which is why the live run's 60-poll
|
|
14347
|
+
* re-arm took ~72s and not 60s, and why the arithmetic above prices a poll at
|
|
14348
|
+
* ~1.2s.
|
|
14349
|
+
*/
|
|
14350
|
+
const INDEX_SETTLE_POLL_INTERVAL_MS = 1e3;
|
|
14351
|
+
/**
|
|
14337
14352
|
* What proceeding costs when a DELETE-path index-settle wait ends WITHOUT
|
|
14338
14353
|
* confirming — passed as {@link waitForIndexesSettled}'s `proceedNote` by every
|
|
14339
14354
|
* delete-path caller (`GlobalTable`'s #1521 pre-delete gate, and the #1830 /
|
|
@@ -14371,7 +14386,7 @@ async function waitForIndexesSettled(opts) {
|
|
|
14371
14386
|
const message = err instanceof Error ? err.message : String(err);
|
|
14372
14387
|
if (isThrottlingError(err)) {
|
|
14373
14388
|
logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
|
|
14374
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
14389
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
14375
14390
|
continue;
|
|
14376
14391
|
}
|
|
14377
14392
|
if (err instanceof ResourceNotFoundException$1) {
|
|
@@ -14383,9 +14398,10 @@ async function waitForIndexesSettled(opts) {
|
|
|
14383
14398
|
logger.warn(`DescribeTable failed (${errorName}) while waiting for indexes on ${tableName} (${logicalId}), so cdkd stopped waiting; ${proceedNote} Re-run with --verbose for AWS's own message.`);
|
|
14384
14399
|
return;
|
|
14385
14400
|
}
|
|
14386
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
14401
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
14387
14402
|
}
|
|
14388
|
-
|
|
14403
|
+
const pollSeconds = INDEX_SETTLE_POLL_INTERVAL_MS / 1e3;
|
|
14404
|
+
logger.warn(`Indexes on ${tableName} (${logicalId}) did not all reach ACTIVE within ${maxAttempts} DescribeTable polls (~${pollSeconds}s apart, so a little over ${maxAttempts * pollSeconds}s of wall clock); ${proceedNote}`);
|
|
14389
14405
|
}
|
|
14390
14406
|
/**
|
|
14391
14407
|
* The ONE line the retry prints at default verbosity, for both providers.
|
|
@@ -14395,15 +14411,50 @@ async function waitForIndexesSettled(opts) {
|
|
|
14395
14411
|
* and the bounded budget above can end in a timeout that has to be diagnosable
|
|
14396
14412
|
* from an ordinary run's output.
|
|
14397
14413
|
*
|
|
14398
|
-
* `attemptNumber` is the attempt now STARTING (2 for the first retry)
|
|
14399
|
-
*
|
|
14400
|
-
*
|
|
14401
|
-
*
|
|
14414
|
+
* `attemptNumber` is the attempt now STARTING (2 for the first retry), and
|
|
14415
|
+
* `maxRetries` is the CALLER's budget — the two types no longer share one (see
|
|
14416
|
+
* {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES}), so a module-level constant here
|
|
14417
|
+
* would print the other type's promise. The remaining-attempt count is DERIVED
|
|
14418
|
+
* from the pair and never spelled out: `withRetry` runs `1 + maxRetries`
|
|
14419
|
+
* attempts in total, and writing the number by hand is how this line came to
|
|
14420
|
+
* promise 8 further attempts when 7 were left. Both figures are CLAMPED — the
|
|
14421
|
+
* attempt count at zero, the minutes at one, since the announced attempt
|
|
14422
|
+
* re-arms too: the function is exported, and an `attemptNumber` past the budget
|
|
14423
|
+
* otherwise printed "up to 0 more attempts" and then went negative.
|
|
14402
14424
|
*
|
|
14403
14425
|
* The per-attempt settle budget is attached to "this attempt plus the N more"
|
|
14404
14426
|
* rather than to the N alone, because the re-arm runs before the attempt this
|
|
14405
|
-
* very line announces too —
|
|
14406
|
-
* attempts, each preceded by ..." made one sentence
|
|
14427
|
+
* very line announces too — 14 re-arms for 13 remaining attempts on the `Table`
|
|
14428
|
+
* budget. Saying "N more attempts, each preceded by ..." made one sentence
|
|
14429
|
+
* quote two different counts.
|
|
14430
|
+
*
|
|
14431
|
+
* It also states how long the loop will keep GOING, as a floor (issue #1950).
|
|
14432
|
+
* At the `Table` budget the loop can run for the better part of twenty minutes,
|
|
14433
|
+
* and a user watching a destroy sit on one table for that long needs to tell
|
|
14434
|
+
* SETTLING from STUCK — an attempt count alone does not say how long it will
|
|
14435
|
+
* take.
|
|
14436
|
+
*
|
|
14437
|
+
* The wording is "will not give up for at least ~N more minutes", NOT "can keep
|
|
14438
|
+
* retrying for at least ~N". The re-arm returns on the first `DescribeTable`
|
|
14439
|
+
* that reports the indexes settled, so a table clearing in 90s prints this line
|
|
14440
|
+
* and then finishes immediately: read as a floor on the WAIT it would push the
|
|
14441
|
+
* user toward Ctrl-C, which is the opposite of what the line is for. It is a
|
|
14442
|
+
* floor on cdkd's PATIENCE, which is what the reader needs to decide whether to
|
|
14443
|
+
* keep waiting.
|
|
14444
|
+
*
|
|
14445
|
+
* A floor rather than an estimate because {@link INDEX_SETTLE_POLL_INTERVAL_MS}
|
|
14446
|
+
* is only the sleep: each poll also pays a `DescribeTable` round trip and
|
|
14447
|
+
* `withRetry` sleeps its backoff between attempts, so the true patience is
|
|
14448
|
+
* longer than the figure printed.
|
|
14449
|
+
*
|
|
14450
|
+
* The floor is conditioned on those waits RUNNING, which the sentence says out
|
|
14451
|
+
* loud rather than assuming. {@link waitForIndexesSettled} returns early on any
|
|
14452
|
+
* non-throttle `DescribeTable` failure (a describe-denied principal that can
|
|
14453
|
+
* still delete, say) and returns at once whenever the GSI-status proxy
|
|
14454
|
+
* disagrees with AWS's real refusal predicate — the gap named above. In those
|
|
14455
|
+
* runs every remaining retry pays only backoff, and cdkd gives up in ~1.6 min
|
|
14456
|
+
* having announced ~14. Unconditional, the figure would be plainly false
|
|
14457
|
+
* there.
|
|
14407
14458
|
*
|
|
14408
14459
|
* It must NOT carry AWS's own sentence: the `dynamodb-globaltable` integ greps
|
|
14409
14460
|
* the destroy log for `Cannot delete table while indexes are being` to prove
|
|
@@ -14411,8 +14462,10 @@ async function waitForIndexesSettled(opts) {
|
|
|
14411
14462
|
* the refusal having happened.
|
|
14412
14463
|
*/
|
|
14413
14464
|
function indexBusyRetryWarning(opts) {
|
|
14414
|
-
const remainingAttempts =
|
|
14415
|
-
|
|
14465
|
+
const remainingAttempts = Math.max(0, 1 + opts.maxRetries - opts.attemptNumber);
|
|
14466
|
+
const settleFloorMinutes = Math.max(1, Math.round((remainingAttempts + 1) * 60 * INDEX_SETTLE_POLL_INTERVAL_MS / 6e4));
|
|
14467
|
+
const pollSeconds = INDEX_SETTLE_POLL_INTERVAL_MS / 1e3;
|
|
14468
|
+
return `DynamoDB ${opts.typeLabel} ${opts.logicalId}: AWS refused DeleteTable on ${opts.physicalId} because a global secondary index is still being created, updated or deleted — application auto-scaling can start an index capacity change at any moment, including during this destroy. Waiting for the index to settle and retrying (this attempt plus up to ${remainingAttempts} more attempts; each one first waits up to ${60} DescribeTable polls ~${pollSeconds}s apart before its DeleteTable, so cdkd will not give up for at least ~${settleFloorMinutes} more minutes while those settle waits run). Nothing is wrong with the table; a large index backfill can outlast this budget, in which case the destroy fails with AWS's own message and re-running it succeeds once the index is ACTIVE.`;
|
|
14416
14469
|
}
|
|
14417
14470
|
/**
|
|
14418
14471
|
* Issue the caller's `DeleteTable`, retrying ONLY the transient index-busy
|
|
@@ -14430,11 +14483,19 @@ function indexBusyRetryWarning(opts) {
|
|
|
14430
14483
|
* index backfill outlasts any fixed backoff grid, while a settle poll returns
|
|
14431
14484
|
* on its first `DescribeTable` once the index is ACTIVE.
|
|
14432
14485
|
*
|
|
14486
|
+
* `maxRetries` is the CALLER's, not this module's: the two DynamoDB types have
|
|
14487
|
+
* different amounts of the per-resource deadline left by the time they reach
|
|
14488
|
+
* this loop, so they carry different budgets (see
|
|
14489
|
+
* {@link TABLE_DELETE_INDEX_BUSY_MAX_RETRIES} for both derivations). Passing it
|
|
14490
|
+
* rather than reading a module constant is also what keeps the two providers'
|
|
14491
|
+
* budgets visible at their call sites.
|
|
14492
|
+
*
|
|
14433
14493
|
* `sleepSeam` is read per SLEEP rather than once when the options bag is built,
|
|
14434
14494
|
* so a test that installs the no-op from INSIDE the first refusal — i.e. after
|
|
14435
14495
|
* `delete()` has already built the bag — still gets it. A value captured at
|
|
14436
14496
|
* construction time would be `undefined` there and would silently pay
|
|
14437
|
-
* `withRetry`'s real
|
|
14497
|
+
* `withRetry`'s real backoff schedule (95s of sleep at the `Table` budget, 47s
|
|
14498
|
+
* at the `GlobalTable` one, on top of a full settle poll per attempt). Passing `sleep` unconditionally is safe:
|
|
14438
14499
|
* `withRetry`'s dense-schedule detection keys on `maxRetries` /
|
|
14439
14500
|
* `initialDelayMs` / `maxDelayMs` / `isRetryable`, all of which this caller
|
|
14440
14501
|
* already sets.
|
|
@@ -14447,13 +14508,14 @@ async function deleteTableWithIndexBusyRetry(opts) {
|
|
|
14447
14508
|
typeLabel: opts.typeLabel,
|
|
14448
14509
|
logicalId: opts.logicalId,
|
|
14449
14510
|
physicalId: opts.physicalId,
|
|
14450
|
-
attemptNumber: deleteAttempts
|
|
14511
|
+
attemptNumber: deleteAttempts,
|
|
14512
|
+
maxRetries: opts.maxRetries
|
|
14451
14513
|
}));
|
|
14452
14514
|
await opts.reArm();
|
|
14453
14515
|
}
|
|
14454
14516
|
await opts.deleteTable();
|
|
14455
14517
|
}, opts.logicalId, {
|
|
14456
|
-
maxRetries:
|
|
14518
|
+
maxRetries: opts.maxRetries,
|
|
14457
14519
|
isRetryable: (message) => isIndexBusyDeleteError(message),
|
|
14458
14520
|
logger: opts.logger,
|
|
14459
14521
|
sleep: (ms) => opts.sleepSeam.sleep ? opts.sleepSeam.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
|
|
@@ -14992,7 +15054,8 @@ function desiredIndexEntriesByName(value) {
|
|
|
14992
15054
|
* Test seam for the index-busy `DeleteTable` retry's backoff (issue #1931),
|
|
14993
15055
|
* mirroring the sibling provider's export of the same name. Production leaves
|
|
14994
15056
|
* `sleep` undefined so `withRetry`'s real schedule applies; a test injects a
|
|
14995
|
-
* no-op so the ~
|
|
15057
|
+
* no-op so the ~95s of backoff at this type's budget
|
|
15058
|
+
* (`TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`) does not have to be waited out.
|
|
14996
15059
|
*
|
|
14997
15060
|
* One seam PER PROVIDER rather than one on the shared module: a `Table` test
|
|
14998
15061
|
* that silences this backoff must not also silence the `GlobalTable` one, since
|
|
@@ -15210,7 +15273,7 @@ var DynamoDBTableProvider = class {
|
|
|
15210
15273
|
}
|
|
15211
15274
|
await this.dynamoDBClient.send(new UpdateTableCommand(updateInput));
|
|
15212
15275
|
if (billingMode !== liveBillingMode) await this.waitForTableAndIndexesActive(physicalId, BILLING_FLIP_ACTIVE_WAIT_ATTEMPTS);
|
|
15213
|
-
else await this.waitForTableActiveAfterUpdate(physicalId,
|
|
15276
|
+
else await this.waitForTableActiveAfterUpdate(physicalId, 60);
|
|
15214
15277
|
this.logger.debug(`Updated BillingMode/ProvisionedThroughput/TableClass on DynamoDB table ${physicalId}`);
|
|
15215
15278
|
}
|
|
15216
15279
|
if (JSON.stringify(properties["OnDemandThroughput"]) !== JSON.stringify(previousProperties["OnDemandThroughput"])) {
|
|
@@ -15338,6 +15401,7 @@ var DynamoDBTableProvider = class {
|
|
|
15338
15401
|
logicalId,
|
|
15339
15402
|
physicalId,
|
|
15340
15403
|
typeLabel: "table",
|
|
15404
|
+
maxRetries: 14,
|
|
15341
15405
|
logger: this.logger,
|
|
15342
15406
|
deleteTable: async () => {
|
|
15343
15407
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
@@ -15697,10 +15761,10 @@ var DynamoDBTableProvider = class {
|
|
|
15697
15761
|
* blanket raise would turn every genuinely wedged capacity edit into a
|
|
15698
15762
|
* 10-minute hang.
|
|
15699
15763
|
*/
|
|
15700
|
-
async waitForTableActiveAfterUpdate(tableName, maxAttempts =
|
|
15764
|
+
async waitForTableActiveAfterUpdate(tableName, maxAttempts = 60) {
|
|
15701
15765
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
15702
15766
|
if ((await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.TableStatus === "ACTIVE") return;
|
|
15703
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
15767
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
15704
15768
|
}
|
|
15705
15769
|
throw new Error(`Table ${tableName} did not reach ACTIVE status within ${maxAttempts} seconds after UpdateTable`);
|
|
15706
15770
|
}
|
|
@@ -17783,6 +17847,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
17783
17847
|
logicalId,
|
|
17784
17848
|
physicalId,
|
|
17785
17849
|
typeLabel: "GlobalTable",
|
|
17850
|
+
maxRetries: 8,
|
|
17786
17851
|
logger: this.logger,
|
|
17787
17852
|
deleteTable: async () => {
|
|
17788
17853
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
@@ -18456,10 +18521,10 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
18456
18521
|
* Delete replica update. Replica deletion typically takes 1–5 min;
|
|
18457
18522
|
* cap at 10 min.
|
|
18458
18523
|
*/
|
|
18459
|
-
async waitForReplicaGone(tableName, region, logicalId, maxAttempts =
|
|
18524
|
+
async waitForReplicaGone(tableName, region, logicalId, maxAttempts = REPLICA_GONE_WAIT_ATTEMPTS) {
|
|
18460
18525
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
18461
18526
|
if (!(await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region)) return;
|
|
18462
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
18527
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
18463
18528
|
} catch (err) {
|
|
18464
18529
|
if (err instanceof ResourceNotFoundException$1) return;
|
|
18465
18530
|
throw err;
|
|
@@ -18480,7 +18545,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
18480
18545
|
async waitForTableGone(tableName, logicalId, maxAttempts = 600) {
|
|
18481
18546
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
|
|
18482
18547
|
await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
|
|
18483
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
18548
|
+
await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
|
|
18484
18549
|
} catch (err) {
|
|
18485
18550
|
if (err instanceof ResourceNotFoundException$1) return;
|
|
18486
18551
|
throw err;
|
|
@@ -18575,10 +18640,33 @@ function autoScalingResourceId(tableName, indexName) {
|
|
|
18575
18640
|
*/
|
|
18576
18641
|
const autoScalingRetryDelays = {};
|
|
18577
18642
|
/**
|
|
18643
|
+
* Polls `waitForReplicaGone` spends waiting for a replica to disappear.
|
|
18644
|
+
*
|
|
18645
|
+
* Named rather than inlined because it is a TERM of the per-resource deadline
|
|
18646
|
+
* arithmetic on this type's delete path (see
|
|
18647
|
+
* `GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`), which prices it through
|
|
18648
|
+
* {@link INDEX_SETTLE_POLL_INTERVAL_MS} — the sleep this loop now reads rather
|
|
18649
|
+
* than spelling its own `1000`, so the two cannot drift while that arithmetic
|
|
18650
|
+
* claims they agree.
|
|
18651
|
+
*/
|
|
18652
|
+
const REPLICA_GONE_WAIT_ATTEMPTS = 600;
|
|
18653
|
+
/**
|
|
18654
|
+
* Polls `waitForTableGone` spends confirming the table is actually gone.
|
|
18655
|
+
*
|
|
18656
|
+
* EXPORTED, unlike its replica sibling, because the delete-path budget fence
|
|
18657
|
+
* reads it: this wait runs AFTER the index-busy retry loop and only when that
|
|
18658
|
+
* loop SUCCEEDS, which is what keeps it out of the loop's own worst case and
|
|
18659
|
+
* puts it into the late-success one (both spelled out on
|
|
18660
|
+
* `GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`).
|
|
18661
|
+
*/
|
|
18662
|
+
const TABLE_GONE_WAIT_ATTEMPTS = 600;
|
|
18663
|
+
/**
|
|
18578
18664
|
* Test seam for the index-busy `DeleteTable` retry's backoff (issue #1830),
|
|
18579
18665
|
* mirroring {@link autoScalingRetryDelays}. Production leaves `sleep`
|
|
18580
18666
|
* undefined so `withRetry`'s real schedule applies; a test injects a no-op so
|
|
18581
|
-
* the ~47s
|
|
18667
|
+
* the ~47s of backoff at this type's budget
|
|
18668
|
+
* (`GLOBAL_TABLE_DELETE_INDEX_BUSY_MAX_RETRIES`) does not have to be waited
|
|
18669
|
+
* out. Kept SEPARATE from the
|
|
18582
18670
|
* auto-scaling seam so a test can slow one path without silencing the other.
|
|
18583
18671
|
*/
|
|
18584
18672
|
const deleteTableRetryDelays = {};
|
|
@@ -60644,6 +60732,325 @@ function createIamPrincipalUniqueIdResolver(awsClients) {
|
|
|
60644
60732
|
};
|
|
60645
60733
|
}
|
|
60646
60734
|
/**
|
|
60735
|
+
* Does any string anywhere in `value` carry a `{{resolve:...}}` dynamic
|
|
60736
|
+
* reference?
|
|
60737
|
+
*
|
|
60738
|
+
* Cheap pre-check so the overwhelming majority of resources — which reference
|
|
60739
|
+
* no secret at all — never build a resolver context and never pay a deep clone
|
|
60740
|
+
* of their property bag.
|
|
60741
|
+
*/
|
|
60742
|
+
function containsDynamicReference(value) {
|
|
60743
|
+
if (typeof value === "string") return value.includes("{{resolve:");
|
|
60744
|
+
if (Array.isArray(value)) return value.some(containsDynamicReference);
|
|
60745
|
+
if (value !== null && typeof value === "object") return Object.values(value).some(containsDynamicReference);
|
|
60746
|
+
return false;
|
|
60747
|
+
}
|
|
60748
|
+
/**
|
|
60749
|
+
* Byte-for-byte the threshold `secret-redaction.ts` applies when it builds its
|
|
60750
|
+
* substring needles (`MIN_NEEDLE_LENGTH`). Copied rather than imported because
|
|
60751
|
+
* that constant is module-private there and this file may not widen its
|
|
60752
|
+
* exports; the two must agree or this function would call a leaf secret-bearing
|
|
60753
|
+
* that `redactSecretsForState` will not redact.
|
|
60754
|
+
*/
|
|
60755
|
+
const MIN_SECRET_NEEDLE_LENGTH = 4;
|
|
60756
|
+
/**
|
|
60757
|
+
* Does `value` hold a plaintext this pass has recorded as a secret?
|
|
60758
|
+
*
|
|
60759
|
+
* Two branches with deliberately different strictness, and the asymmetry is the
|
|
60760
|
+
* point. A WHOLE-value match is exact evidence at any length, so it is accepted
|
|
60761
|
+
* unconditionally. A SUBSTRING match is only evidence when the plaintext is
|
|
60762
|
+
* long enough to be distinctive: a 1-3 character secret (a JSON key holding
|
|
60763
|
+
* `"0"`) occurs inside unrelated values constantly, and accepting it would mark
|
|
60764
|
+
* half the resource's paths secret-bearing — masking values that are not
|
|
60765
|
+
* secrets and, worse, making `--accept` refuse them.
|
|
60766
|
+
*
|
|
60767
|
+
* So this errs toward NOT marking on the substring branch, and that direction
|
|
60768
|
+
* is chosen rather than inherited: `redactSecretsForState` applies the same
|
|
60769
|
+
* threshold when it builds needles, so a sub-threshold plaintext is not
|
|
60770
|
+
* substituted there either. Marking a path this function's caller cannot
|
|
60771
|
+
* actually redact would produce a `***` with no mechanism behind it.
|
|
60772
|
+
*/
|
|
60773
|
+
function carriesRecordedSecret(value, secrets) {
|
|
60774
|
+
if (value === "") return false;
|
|
60775
|
+
if (secrets.has(value)) return true;
|
|
60776
|
+
for (const plaintext of secrets.keys()) if (plaintext.length >= MIN_SECRET_NEEDLE_LENGTH && value.includes(plaintext)) return true;
|
|
60777
|
+
return false;
|
|
60778
|
+
}
|
|
60779
|
+
/**
|
|
60780
|
+
* Is this unresolvable token one whose value is a SECRET by definition?
|
|
60781
|
+
*
|
|
60782
|
+
* `ssm-secure` is the only such spelling, and it is decidable with no lookup:
|
|
60783
|
+
* CloudFormation defines exactly three dynamic-reference services (`ssm`,
|
|
60784
|
+
* `ssm-secure`, `secretsmanager`), cdkd resolves the other two, and
|
|
60785
|
+
* `ssm-secure` is the encrypted one.
|
|
60786
|
+
*
|
|
60787
|
+
* Anything else reaching the survivor arm is not a CloudFormation dynamic
|
|
60788
|
+
* reference at all — it is text that merely looks like one — and treating it as
|
|
60789
|
+
* secret is not the safe direction it appears to be: the path would be masked
|
|
60790
|
+
* to `***`, refused by `--accept`, and pinned to the live value by
|
|
60791
|
+
* {@link preserveLiveValuesAtUnresolvedTokens}, which is permanently stuck
|
|
60792
|
+
* drift with no remedy the user can apply. Such a token is still REPORTED; it
|
|
60793
|
+
* just does not claim to be a secret.
|
|
60794
|
+
*/
|
|
60795
|
+
function isSecretBySpelling(token) {
|
|
60796
|
+
return token.startsWith("{{resolve:ssm-secure:");
|
|
60797
|
+
}
|
|
60798
|
+
/**
|
|
60799
|
+
* Every `{{resolve:...}}` token still present in `value`.
|
|
60800
|
+
*
|
|
60801
|
+
* Used to NAME an unresolvable reference without quoting the string it sits in.
|
|
60802
|
+
* That string is a partially substituted leaf: `resolveDynamicReferences`
|
|
60803
|
+
* substitutes token by token, so a leaf holding two references comes back with
|
|
60804
|
+
* the resolvable one already replaced by its PLAINTEXT. Interpolating it into a
|
|
60805
|
+
* log line prints the secret — from the command whose purpose is not to. A
|
|
60806
|
+
* token is a reference NAME and carries no value, so it is always safe to show.
|
|
60807
|
+
*/
|
|
60808
|
+
function survivingDynamicReferences(value) {
|
|
60809
|
+
return value.match(/\{\{resolve:[^}]+\}\}/g) ?? [];
|
|
60810
|
+
}
|
|
60811
|
+
/**
|
|
60812
|
+
* Record the dotted path of every leaf holding a `{{resolve:...}}` string.
|
|
60813
|
+
*
|
|
60814
|
+
* The OFFLINE seed for {@link SecretPathSet}: it needs no AWS call, so it is
|
|
60815
|
+
* what remains when resolution fails. Issue #1900's mechanism applied to
|
|
60816
|
+
* positions instead of values — the record's own bags already say WHERE the
|
|
60817
|
+
* references are, and that is the half of the answer a failed lookup does not
|
|
60818
|
+
* take away.
|
|
60819
|
+
*
|
|
60820
|
+
* Necessarily coarser than the resolved answer, because a `{{resolve:ssm:...}}`
|
|
60821
|
+
* naming a plain `String` parameter is PUBLIC config and only the resolver's
|
|
60822
|
+
* TYPE verdict can say so. Marking such a path secret-bearing over-masks it,
|
|
60823
|
+
* which is why the seed is used ONLY on the failure path, where the alternative
|
|
60824
|
+
* is not masking at all.
|
|
60825
|
+
*/
|
|
60826
|
+
function collectDynamicReferencePaths(value, into, path = "") {
|
|
60827
|
+
if (typeof value === "string") {
|
|
60828
|
+
if (value.includes("{{resolve:")) into.add(path);
|
|
60829
|
+
return;
|
|
60830
|
+
}
|
|
60831
|
+
if (Array.isArray(value)) {
|
|
60832
|
+
value.forEach((item, i) => collectDynamicReferencePaths(item, into, path === "" ? String(i) : `${path}.${i}`));
|
|
60833
|
+
return;
|
|
60834
|
+
}
|
|
60835
|
+
if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) collectDynamicReferencePaths(v, into, path === "" ? k : `${path}.${k}`);
|
|
60836
|
+
}
|
|
60837
|
+
/**
|
|
60838
|
+
* Re-resolve the SECRET dynamic references (`{{resolve:secretsmanager:...}}`,
|
|
60839
|
+
* and an `{{resolve:ssm:...}}` naming a `SecureString`) held by a bag read back
|
|
60840
|
+
* out of cdkd STATE (issue #1914).
|
|
60841
|
+
*
|
|
60842
|
+
* State stores the unresolved EXPRESSION — that is the GHSA-p5qg-v9gv-hc7w
|
|
60843
|
+
* redaction — while a provider's `readCurrentState` snapshot necessarily holds
|
|
60844
|
+
* the resolved PLAINTEXT, because that is what AWS was given at deploy time.
|
|
60845
|
+
* Nothing in `cdkd drift` reconciled the two, and all three of the command's
|
|
60846
|
+
* modes broke on it: the comparison reported permanent phantom drift and
|
|
60847
|
+
* printed the plaintext as the AWS-current side, `--accept` persisted that
|
|
60848
|
+
* plaintext back into `state.json`, and `--revert` shipped the literal
|
|
60849
|
+
* `{{resolve:...}}` string to the live resource.
|
|
60850
|
+
*
|
|
60851
|
+
* The counterpart of the rollback replay's `resolveReplayProps`
|
|
60852
|
+
* (`src/deployment/rollback-executor.ts`), and for the same reason: both
|
|
60853
|
+
* commands are synth-free, so the expression string in the persisted record is
|
|
60854
|
+
* the only thing there is to resolve from. Every plaintext is recorded into
|
|
60855
|
+
* `secrets` so the caller can redact it back out of anything it PRINTS or
|
|
60856
|
+
* PERSISTS, and every path that produced one into `secretPaths` so the caller
|
|
60857
|
+
* can act on a value it does NOT recognise there — see {@link redactDriftChanges}.
|
|
60858
|
+
*
|
|
60859
|
+
* A `{{resolve:...}}` token that SURVIVES the pass is reported through
|
|
60860
|
+
* `onUnresolved` and left in place — it is not an error. The resolver's
|
|
60861
|
+
* unsupported-service arm (`ssm-secure:` is the live example) warns and returns
|
|
60862
|
+
* the literal, and `cdkd deploy` resolves through the very same code, so AWS is
|
|
60863
|
+
* ALREADY holding that literal string and state records it. Replaying it is
|
|
60864
|
+
* therefore a correct no-op, and failing the resource over it would take every
|
|
60865
|
+
* other drifted property on that resource down with it (plus exit 2). The
|
|
60866
|
+
* report exists so the user learns cdkd is shipping a token it cannot resolve,
|
|
60867
|
+
* which is a real thing to know and not a reason to stop.
|
|
60868
|
+
*
|
|
60869
|
+
* Only the TOKENS are reported, never the leaf that held them: the leaf is
|
|
60870
|
+
* partially substituted by this point, so a mixed `secretsmanager` +
|
|
60871
|
+
* `ssm-secure` string comes back carrying real plaintext.
|
|
60872
|
+
*
|
|
60873
|
+
* Returns the input by identity when it holds no dynamic reference, so the
|
|
60874
|
+
* non-secret path is unchanged down to object identity.
|
|
60875
|
+
*/
|
|
60876
|
+
async function resolveStateSecretExpressions(props, resolver, secrets, options = {}) {
|
|
60877
|
+
if (!containsDynamicReference(props)) return props;
|
|
60878
|
+
const { secretPaths, onUnresolved } = options;
|
|
60879
|
+
const ctx = {
|
|
60880
|
+
template: { Resources: {} },
|
|
60881
|
+
resources: {},
|
|
60882
|
+
recordedSecretValues: secrets
|
|
60883
|
+
};
|
|
60884
|
+
const walk = async (v, path) => {
|
|
60885
|
+
if (typeof v === "string") {
|
|
60886
|
+
if (!v.includes("{{resolve:")) return v;
|
|
60887
|
+
const resolved = await resolver.resolveDynamicReferences(v, ctx);
|
|
60888
|
+
const survivors = survivingDynamicReferences(resolved).filter((t) => v.includes(t));
|
|
60889
|
+
if (survivors.length > 0) {
|
|
60890
|
+
if (survivors.some(isSecretBySpelling)) secretPaths?.add(path);
|
|
60891
|
+
onUnresolved?.(survivors, path);
|
|
60892
|
+
}
|
|
60893
|
+
if (carriesRecordedSecret(resolved, secrets)) secretPaths?.add(path);
|
|
60894
|
+
return resolved;
|
|
60895
|
+
}
|
|
60896
|
+
if (Array.isArray(v)) {
|
|
60897
|
+
const out = new Array(v.length);
|
|
60898
|
+
for (let i = 0; i < v.length; i++) out[i] = await walk(v[i], path === "" ? String(i) : `${path}.${i}`);
|
|
60899
|
+
return out;
|
|
60900
|
+
}
|
|
60901
|
+
if (v !== null && typeof v === "object") {
|
|
60902
|
+
const out = {};
|
|
60903
|
+
for (const [k, val] of Object.entries(v)) out[k] = await walk(val, path === "" ? k : `${path}.${k}`);
|
|
60904
|
+
return out;
|
|
60905
|
+
}
|
|
60906
|
+
return v;
|
|
60907
|
+
};
|
|
60908
|
+
return await walk(props, "");
|
|
60909
|
+
}
|
|
60910
|
+
/**
|
|
60911
|
+
* Is a drift reported at `path` positioned on, or above, a known secret?
|
|
60912
|
+
*
|
|
60913
|
+
* The PREFIX direction is the one that matters: a secret sits at a leaf (or at
|
|
60914
|
+
* an array the comparator never descends), while a drift can be reported at any
|
|
60915
|
+
* ancestor of it — `Environment` rather than
|
|
60916
|
+
* `Environment.Variables.SECRET_PASSWORD` — whenever the two sides disagree
|
|
60917
|
+
* about the shape rather than the value. The reverse containment is checked too
|
|
60918
|
+
* and costs nothing.
|
|
60919
|
+
*/
|
|
60920
|
+
function isSecretBearingPath(path, secretPaths) {
|
|
60921
|
+
if (secretPaths.size === 0) return false;
|
|
60922
|
+
for (const secretPath of secretPaths) if (secretPath === path || secretPath.startsWith(`${path}.`) || path.startsWith(`${secretPath}.`)) return true;
|
|
60923
|
+
return false;
|
|
60924
|
+
}
|
|
60925
|
+
/**
|
|
60926
|
+
* Redact one side of a drift entry.
|
|
60927
|
+
*
|
|
60928
|
+
* Two passes, and the second is the one issue #1914's review turned up. The
|
|
60929
|
+
* VALUE pass rewrites any recorded plaintext back onto its own
|
|
60930
|
+
* `{{resolve:...}}` expression, which is exact and is all that is needed while
|
|
60931
|
+
* the secret AWS holds is the secret the reference resolves to today.
|
|
60932
|
+
*
|
|
60933
|
+
* It stops being enough the moment those two differ, and they differ for the
|
|
60934
|
+
* most ordinary reason there is: a Secrets Manager ROTATION. The deployed
|
|
60935
|
+
* resource then still holds the PREVIOUS secret while the re-resolved baseline
|
|
60936
|
+
* holds the new one, so the path drifts and the AWS side matches no key in a
|
|
60937
|
+
* map built from today's value — it is a real secret that would be printed
|
|
60938
|
+
* verbatim and persisted by `--accept`. An out-of-band edit is
|
|
60939
|
+
* indistinguishable from it at this layer, so the POSITION decides: at a path
|
|
60940
|
+
* known to carry a secret, the only value ever shown is the expression itself,
|
|
60941
|
+
* and anything else becomes {@link SECRET_MASK}.
|
|
60942
|
+
*
|
|
60943
|
+
* `undefined` and `null` are exempt — an absent or null key discloses nothing,
|
|
60944
|
+
* and masking them would turn "AWS does not have this key" into "AWS has
|
|
60945
|
+
* something secret here", which is both wrong and less useful.
|
|
60946
|
+
*/
|
|
60947
|
+
function redactDriftValue(value, secrets, secretBearing) {
|
|
60948
|
+
const redacted = redactSecretsForState(value, secrets);
|
|
60949
|
+
if (!secretBearing) return redacted;
|
|
60950
|
+
if (redacted === void 0 || redacted === null) return redacted;
|
|
60951
|
+
if (typeof redacted === "string" && isWholeDynamicReference(redacted)) return redacted;
|
|
60952
|
+
return "***";
|
|
60953
|
+
}
|
|
60954
|
+
/**
|
|
60955
|
+
* Is the WHOLE string a single `{{resolve:...}}` token and nothing else?
|
|
60956
|
+
*
|
|
60957
|
+
* Byte-for-byte `secret-redaction.ts`'s `isSingleDynamicReferenceToken`,
|
|
60958
|
+
* including the `[^{}]*` character class. The difference from `[^}]+` is
|
|
60959
|
+
* narrow and worth stating correctly: it is a `{` INSIDE the token, e.g.
|
|
60960
|
+
* `{{resolve:ssm:/a{b}}`. (A previous version of this comment claimed
|
|
60961
|
+
* `{{resolve:a}}{{resolve:b}}` would pass under `[^}]+` — it would not, since
|
|
60962
|
+
* `[^}]+` cannot span the `}` in the middle either.) Matching the sibling
|
|
60963
|
+
* exactly is what matters here: this predicate decides whether a value may be
|
|
60964
|
+
* shown INSTEAD of `SECRET_MASK`, and `redactByPath` uses its copy to decide
|
|
60965
|
+
* whether a leaf is already an expression, so a disagreement would let one of
|
|
60966
|
+
* them treat as a token what the other treats as data. Copied rather than
|
|
60967
|
+
* imported because that helper is module-private and this file may not widen
|
|
60968
|
+
* that module's exports.
|
|
60969
|
+
*/
|
|
60970
|
+
function isWholeDynamicReference(value) {
|
|
60971
|
+
return /^\{\{resolve:[^{}]*\}\}$/.test(value);
|
|
60972
|
+
}
|
|
60973
|
+
/**
|
|
60974
|
+
* Why `--accept` must not persist this drift, or `undefined` when it may.
|
|
60975
|
+
*
|
|
60976
|
+
* Shared by the WRITE (`runAccept`) and the PLAN (`printAcceptPlan`) so the two
|
|
60977
|
+
* cannot disagree — a `--dry-run` that promises a write the real run refuses is
|
|
60978
|
+
* worse than either behaviour on its own.
|
|
60979
|
+
*
|
|
60980
|
+
* Two refusals, both about a value cdkd could not identify rather than about
|
|
60981
|
+
* secrecy as such:
|
|
60982
|
+
*
|
|
60983
|
+
* - A masked VALUE says AWS holds something at a known-secret path that is not
|
|
60984
|
+
* what the reference resolves to. Writing `***` would corrupt the baseline
|
|
60985
|
+
* and make the next deploy push the literal mask at AWS.
|
|
60986
|
+
* - A masked PATH says the readback answered with a map KEYED by a secret.
|
|
60987
|
+
* `setAtPath` would then create a key literally named `***` — and when
|
|
60988
|
+
* `awsValue` is `undefined` it would INSERT that key rather than removing the
|
|
60989
|
+
* real one, so the value check alone does not cover it.
|
|
60990
|
+
*
|
|
60991
|
+
* The path check is what makes the value exemptions safe: `redactDriftValue`
|
|
60992
|
+
* lets `undefined` / `null` / a whole expression through unmasked, which is
|
|
60993
|
+
* right for a value and says nothing about the path it sits at.
|
|
60994
|
+
*/
|
|
60995
|
+
function acceptRefusalReason(change, maskedPaths) {
|
|
60996
|
+
if (!maskedPaths.has(change.path)) return void 0;
|
|
60997
|
+
if (change.path.includes("***")) return "its property NAME came back carrying a secret value, so cdkd cannot name the key to write — and no redaction pass can clear it either, since they all walk values and never keys. ROTATE the secret; that is the only remedy";
|
|
60998
|
+
if (change.awsValue === void 0) return "AWS no longer reports it, and accepting an absence DELETES the key — which at or above a secret dynamic reference would erase the reference out of cdkd state. Use --revert to push it back, or re-deploy";
|
|
60999
|
+
return "it carries a secret dynamic reference and the value AWS currently holds is not the one the reference resolves to";
|
|
61000
|
+
}
|
|
61001
|
+
/**
|
|
61002
|
+
* Redact resolved secret plaintext out of the drift entries that get REPORTED
|
|
61003
|
+
* (issue #1914).
|
|
61004
|
+
*
|
|
61005
|
+
* Both sides need it, for different reasons. `stateValue` comes from the
|
|
61006
|
+
* re-resolved comparison baseline, so at a secret leaf it now holds the
|
|
61007
|
+
* plaintext the state record deliberately does not store. `awsValue` is the
|
|
61008
|
+
* live readback, which holds that plaintext by construction. Either one
|
|
61009
|
+
* reaching `writeHumanReport` / `writeJsonReport` / the `--accept` and
|
|
61010
|
+
* `--revert` plans is the disclosure the redaction exists to prevent.
|
|
61011
|
+
*
|
|
61012
|
+
* Redacting `awsValue` also settles what `--accept` PERSISTS: that is the value
|
|
61013
|
+
* it writes into the baseline, so a secret one arrives at the state write
|
|
61014
|
+
* already carrying its expression — or, when it could not be recognised, as
|
|
61015
|
+
* {@link SECRET_MASK}, which `runAccept` refuses to persist.
|
|
61016
|
+
*
|
|
61017
|
+
* The PATH is redacted too. `redactSecretsForState` walks values and never
|
|
61018
|
+
* object KEYS, so a readback that answers with a map keyed by a secret (a
|
|
61019
|
+
* Lambda env var NAMED with one) renders as
|
|
61020
|
+
* `Environment.Variables.<plaintext>` in every printer. It is masked here
|
|
61021
|
+
* rather than assumed impossible, and a masked path also marks the change
|
|
61022
|
+
* secret-bearing, so `--accept` will not write a key it can no longer name.
|
|
61023
|
+
*
|
|
61024
|
+
* Returns the input array by identity when there is nothing to act on.
|
|
61025
|
+
*/
|
|
61026
|
+
function redactDriftChanges(changes, secrets, secretPaths) {
|
|
61027
|
+
if (secrets.size === 0 && secretPaths.size === 0) return {
|
|
61028
|
+
changes,
|
|
61029
|
+
maskedPaths: /* @__PURE__ */ new Set()
|
|
61030
|
+
};
|
|
61031
|
+
const maskedPaths = /* @__PURE__ */ new Set();
|
|
61032
|
+
const redacted = [];
|
|
61033
|
+
for (const change of changes) {
|
|
61034
|
+
const maskedPath = maskSecretsInText(change.path, secrets);
|
|
61035
|
+
const positionIsSecret = isSecretBearingPath(change.path, secretPaths);
|
|
61036
|
+
const nameCarriesSecret = maskedPath !== change.path;
|
|
61037
|
+
const secretBearing = positionIsSecret || nameCarriesSecret;
|
|
61038
|
+
if (secretPaths.has(change.path) && change.awsValue === void 0) continue;
|
|
61039
|
+
const stateValue = redactDriftValue(change.stateValue, secrets, secretBearing);
|
|
61040
|
+
const awsValue = redactDriftValue(change.awsValue, secrets, secretBearing);
|
|
61041
|
+
if (maskedPath !== change.path || secretBearing && awsValue === "***" || secretBearing && change.awsValue === void 0) maskedPaths.add(maskedPath);
|
|
61042
|
+
redacted.push({
|
|
61043
|
+
path: maskedPath,
|
|
61044
|
+
stateValue,
|
|
61045
|
+
awsValue
|
|
61046
|
+
});
|
|
61047
|
+
}
|
|
61048
|
+
return {
|
|
61049
|
+
changes: redacted,
|
|
61050
|
+
maskedPaths
|
|
61051
|
+
};
|
|
61052
|
+
}
|
|
61053
|
+
/**
|
|
60647
61054
|
* Run drift detection for one stack and shape the per-resource outcomes
|
|
60648
61055
|
* into a {@link StackDriftReport}. The state object + etag are stored on
|
|
60649
61056
|
* the report so `--accept` can write back without a re-read, and
|
|
@@ -60656,6 +61063,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
60656
61063
|
return await withStackName(stackName, async () => {
|
|
60657
61064
|
const outcomes = [];
|
|
60658
61065
|
const state = result.state;
|
|
61066
|
+
const logger = getLogger();
|
|
61067
|
+
const secretResolver = new IntrinsicFunctionResolver(region);
|
|
60659
61068
|
const entries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
60660
61069
|
for (const [logicalId, resource] of entries) {
|
|
60661
61070
|
if (providerRegistry.shouldSkipResource(resource.resourceType)) continue;
|
|
@@ -60715,19 +61124,46 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
60715
61124
|
const unorderedPaths = provider.getDriftUnorderedPaths ? provider.getDriftUnorderedPaths(resource.resourceType) : [];
|
|
60716
61125
|
const useObserved = resource.observedProperties !== void 0;
|
|
60717
61126
|
const baseline = useObserved ? resource.observedProperties : resource.properties ?? {};
|
|
61127
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
61128
|
+
const secretPaths = /* @__PURE__ */ new Set();
|
|
61129
|
+
const seededSecretPaths = /* @__PURE__ */ new Set();
|
|
61130
|
+
collectDynamicReferencePaths(baseline, seededSecretPaths);
|
|
61131
|
+
if (useObserved) collectDynamicReferencePaths(resource.properties ?? {}, seededSecretPaths);
|
|
61132
|
+
const unresolvedTokens = /* @__PURE__ */ new Set();
|
|
61133
|
+
const noteUnresolved = (tokens) => {
|
|
61134
|
+
for (const token of tokens) unresolvedTokens.add(token);
|
|
61135
|
+
};
|
|
61136
|
+
let comparisonBaseline = baseline;
|
|
61137
|
+
let secretResolutionFailed = false;
|
|
61138
|
+
try {
|
|
61139
|
+
comparisonBaseline = await resolveStateSecretExpressions(baseline, secretResolver, secrets, {
|
|
61140
|
+
secretPaths,
|
|
61141
|
+
onUnresolved: noteUnresolved
|
|
61142
|
+
});
|
|
61143
|
+
if (useObserved) await resolveStateSecretExpressions(resource.properties ?? {}, secretResolver, secrets, {
|
|
61144
|
+
secretPaths,
|
|
61145
|
+
onUnresolved: noteUnresolved
|
|
61146
|
+
});
|
|
61147
|
+
} catch (err) {
|
|
61148
|
+
secretResolutionFailed = true;
|
|
61149
|
+
comparisonBaseline = baseline;
|
|
61150
|
+
logger.warn(`${logicalId} (${resource.resourceType}): could not resolve the dynamic reference(s) this resource's state records, so its secret-bearing properties are NOT compared — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
|
|
61151
|
+
secrets.clear();
|
|
61152
|
+
}
|
|
61153
|
+
if (unresolvedTokens.size > 0) logger.warn(`${logicalId} (${resource.resourceType}): cdkd cannot resolve ${maskSecretsInText([...unresolvedTokens].join(", "), secrets)} — those properties are NOT compared. A revert leaves a property whose WHOLE value is one of these tokens untouched; where a token is EMBEDDED in a longer string, the revert writes that string with the token literal, exactly as 'cdkd deploy' does — so a resolved value AWS holds there WOULD be overwritten. cdkd resolves 'secretsmanager' and 'ssm' references only.`);
|
|
60718
61154
|
const observedIgnorePaths = useObserved ? undeclaredEmptyObservedKeys(resource.observedProperties, resource.properties ?? {}) : [];
|
|
60719
|
-
const normalized = await canonicalizePrincipalUniqueIds(
|
|
61155
|
+
const normalized = await canonicalizePrincipalUniqueIds(comparisonBaseline, aws, resolvePrincipalUniqueId);
|
|
60720
61156
|
const protocolNormalized = canonicalizeIpProtocols(normalized.baseline, normalized.aws, resource.resourceType);
|
|
60721
61157
|
const canonicalized = provider.canonicalizeDriftProperties ? {
|
|
60722
61158
|
baseline: provider.canonicalizeDriftProperties(resource.resourceType, protocolNormalized.baseline),
|
|
60723
61159
|
aws: provider.canonicalizeDriftProperties(resource.resourceType, protocolNormalized.aws)
|
|
60724
61160
|
} : protocolNormalized;
|
|
60725
|
-
const
|
|
61161
|
+
const reported = redactDriftChanges(calculateResourceDrift(canonicalized.baseline, canonicalized.aws, {
|
|
60726
61162
|
ignorePaths: observedIgnorePaths.length ? [...ignorePaths, ...observedIgnorePaths] : ignorePaths,
|
|
60727
61163
|
unionWalkObjects: useObserved,
|
|
60728
61164
|
unorderedPaths
|
|
60729
|
-
});
|
|
60730
|
-
if (changes.length === 0) outcomes.push({
|
|
61165
|
+
}), secrets, secretResolutionFailed ? seededSecretPaths : secretPaths);
|
|
61166
|
+
if (reported.changes.length === 0) outcomes.push({
|
|
60731
61167
|
kind: "clean",
|
|
60732
61168
|
logicalId,
|
|
60733
61169
|
resourceType: resource.resourceType
|
|
@@ -60736,8 +61172,10 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
60736
61172
|
kind: "drifted",
|
|
60737
61173
|
logicalId,
|
|
60738
61174
|
resourceType: resource.resourceType,
|
|
60739
|
-
|
|
60740
|
-
awsProperties: aws
|
|
61175
|
+
...reported,
|
|
61176
|
+
awsProperties: aws,
|
|
61177
|
+
secrets,
|
|
61178
|
+
referencesUnresolved: secretResolutionFailed || unresolvedTokens.size > 0
|
|
60741
61179
|
});
|
|
60742
61180
|
}
|
|
60743
61181
|
return {
|
|
@@ -60779,6 +61217,20 @@ function buildReadCurrentStateContext(state, excludedLogicalId) {
|
|
|
60779
61217
|
}
|
|
60780
61218
|
return { siblings };
|
|
60781
61219
|
}
|
|
61220
|
+
/**
|
|
61221
|
+
* Read the value at a dotted path, or `undefined` when any segment is missing.
|
|
61222
|
+
* The read counterpart of {@link setAtPath}, and it parses the same subset:
|
|
61223
|
+
* the drift comparator only synthesizes paths through plain objects.
|
|
61224
|
+
*/
|
|
61225
|
+
function getAtPath(source, path) {
|
|
61226
|
+
if (path.length === 0) return source;
|
|
61227
|
+
let cursor = source;
|
|
61228
|
+
for (const segment of path.split(".")) {
|
|
61229
|
+
if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) return void 0;
|
|
61230
|
+
cursor = cursor[segment];
|
|
61231
|
+
}
|
|
61232
|
+
return cursor;
|
|
61233
|
+
}
|
|
60782
61234
|
function setAtPath(target, path, value) {
|
|
60783
61235
|
if (path.length === 0) return;
|
|
60784
61236
|
const segments = path.split(".");
|
|
@@ -60831,13 +61283,27 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
|
|
|
60831
61283
|
const hasObserved = existing.observedProperties !== void 0;
|
|
60832
61284
|
const baselineSource = hasObserved ? existing.observedProperties : existing.properties ?? {};
|
|
60833
61285
|
const newBaseline = JSON.parse(JSON.stringify(baselineSource));
|
|
60834
|
-
|
|
61286
|
+
const accepted = [];
|
|
61287
|
+
for (const change of outcome.changes) {
|
|
61288
|
+
const refusal = acceptRefusalReason(change, outcome.maskedPaths);
|
|
61289
|
+
if (refusal !== void 0) {
|
|
61290
|
+
logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): not accepting '${change.path}' — ${refusal}, so cdkd will not write it to state. Run 'cdkd drift ${report.stackName} --revert' to push the referenced value back to AWS, or re-deploy if the reference changed.`);
|
|
61291
|
+
continue;
|
|
61292
|
+
}
|
|
61293
|
+
setAtPath(newBaseline, change.path, change.awsValue);
|
|
61294
|
+
accepted.push(change);
|
|
61295
|
+
}
|
|
61296
|
+
const redactedBaseline = redactSecretsForState(newBaseline, outcome.secrets, existing.properties ?? {}, STATE_SOURCED_READBACK_RULES);
|
|
61297
|
+
for (const change of accepted) {
|
|
61298
|
+
if (deepEqualUnordered(getAtPath(redactedBaseline, change.path), change.awsValue)) continue;
|
|
61299
|
+
logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): '${change.path}' was NOT recorded — cdkd state holds an unresolved '{{resolve:...}}' reference at that property, and the reference wins over an accepted value. Change the template if that reference is wrong.`);
|
|
61300
|
+
}
|
|
60835
61301
|
resources[outcome.logicalId] = hasObserved ? {
|
|
60836
61302
|
...existing,
|
|
60837
|
-
observedProperties:
|
|
61303
|
+
observedProperties: redactedBaseline
|
|
60838
61304
|
} : {
|
|
60839
61305
|
...existing,
|
|
60840
|
-
properties:
|
|
61306
|
+
properties: redactedBaseline
|
|
60841
61307
|
};
|
|
60842
61308
|
}
|
|
60843
61309
|
const newState = {
|
|
@@ -61198,6 +61664,57 @@ function mergeUntemplatedValue(awsValue, desiredValue) {
|
|
|
61198
61664
|
return merged;
|
|
61199
61665
|
}
|
|
61200
61666
|
/**
|
|
61667
|
+
* Replace every leaf of a revert payload that still holds a `{{resolve:...}}`
|
|
61668
|
+
* token with the value AWS currently has at that position (issue #1914,
|
|
61669
|
+
* round-3 review).
|
|
61670
|
+
*
|
|
61671
|
+
* The round-3 change downgraded an unresolvable reference from a per-resource
|
|
61672
|
+
* failure to a warning, on the premise that `cdkd deploy` resolves through the
|
|
61673
|
+
* resolver's same unsupported-service arm — so AWS already holds the literal
|
|
61674
|
+
* and replaying it is a no-op. **That premise holds only for records cdkd
|
|
61675
|
+
* deployed.** CloudFormation resolves `ssm-secure` SERVER-side, so a record
|
|
61676
|
+
* adopted by `cdkd import --migrate-from-cloudformation` has the literal token
|
|
61677
|
+
* in state while the live resource holds the PLAINTEXT. `diffAt` skips such a
|
|
61678
|
+
* leaf, so it never drifts — but `buildRevertNewProperties` overlays the whole
|
|
61679
|
+
* top-level subtree when any SIBLING key drifts, which would push the literal
|
|
61680
|
+
* token over a resolved value. That is defect 1 of this issue surviving in a
|
|
61681
|
+
* narrower case, on a live AWS write.
|
|
61682
|
+
*
|
|
61683
|
+
* Deciding by PROVENANCE would need a flag state does not carry. Deciding by
|
|
61684
|
+
* what AWS actually holds needs nothing and is exact in both directions: for a
|
|
61685
|
+
* cdkd-deployed record AWS holds the token, so copying it back is the same
|
|
61686
|
+
* no-op the premise described; for a migrated record AWS holds the resolved
|
|
61687
|
+
* value, which is left untouched.
|
|
61688
|
+
*
|
|
61689
|
+
* Falls back to KEEPING the token wherever AWS has nothing at that position —
|
|
61690
|
+
* that is what `cdkd deploy` sends, so it is the safe residual rather than
|
|
61691
|
+
* dropping a property the resource may require. Array descent is positional
|
|
61692
|
+
* and bails to that same residual on any length mismatch, since a reordered
|
|
61693
|
+
* readback cannot be positioned against.
|
|
61694
|
+
*/
|
|
61695
|
+
function preserveLiveValuesAtUnresolvedTokens(send, awsProperties, secrets) {
|
|
61696
|
+
const walk = (value, live) => {
|
|
61697
|
+
if (typeof value === "string" && value.includes("{{resolve:")) {
|
|
61698
|
+
if (!isWholeDynamicReference(value)) return value;
|
|
61699
|
+
if (live === void 0) return value;
|
|
61700
|
+
if (typeof live === "string" && live.length >= MIN_SECRET_NEEDLE_LENGTH && isSecretBySpelling(value)) secrets.set(live, value);
|
|
61701
|
+
return live;
|
|
61702
|
+
}
|
|
61703
|
+
if (Array.isArray(value)) {
|
|
61704
|
+
const liveItems = Array.isArray(live) && live.length === value.length ? live : void 0;
|
|
61705
|
+
return value.map((item, i) => walk(item, liveItems?.[i]));
|
|
61706
|
+
}
|
|
61707
|
+
if (value !== null && typeof value === "object") {
|
|
61708
|
+
const liveObject = isPlainRecord(live) ? live : void 0;
|
|
61709
|
+
const out = {};
|
|
61710
|
+
for (const [k, v] of Object.entries(value)) out[k] = walk(v, liveObject?.[k]);
|
|
61711
|
+
return out;
|
|
61712
|
+
}
|
|
61713
|
+
return value;
|
|
61714
|
+
};
|
|
61715
|
+
return walk(send, awsProperties);
|
|
61716
|
+
}
|
|
61717
|
+
/**
|
|
61201
61718
|
* Build the `newProperties` object passed to `provider.update` during
|
|
61202
61719
|
* `--revert`. Strategy:
|
|
61203
61720
|
*
|
|
@@ -61371,11 +61888,13 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
61371
61888
|
let totalFailed = 0;
|
|
61372
61889
|
let totalUnsupported = 0;
|
|
61373
61890
|
let totalSucceeded = 0;
|
|
61891
|
+
let totalUnresolvable = 0;
|
|
61374
61892
|
for (const report of reports) {
|
|
61375
61893
|
const driftedOutcomes = report.outcomes.filter((o) => o.kind === "drifted");
|
|
61376
61894
|
if (driftedOutcomes.length === 0) continue;
|
|
61377
61895
|
await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert");
|
|
61378
61896
|
const narrowedByLogicalId = /* @__PURE__ */ new Map();
|
|
61897
|
+
const revertSecretResolver = new IntrinsicFunctionResolver(report.region);
|
|
61379
61898
|
try {
|
|
61380
61899
|
await runWithConcurrency(driftedOutcomes.map((outcome) => async () => {
|
|
61381
61900
|
const stateResource = report.state.resources[outcome.logicalId];
|
|
@@ -61388,28 +61907,53 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
61388
61907
|
resourceType: outcome.resourceType,
|
|
61389
61908
|
provisionedBy: stateResource.provisionedBy
|
|
61390
61909
|
}).provider;
|
|
61391
|
-
const
|
|
61392
|
-
const
|
|
61910
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
61911
|
+
const revertBaseline = stateResource.observedProperties ?? stateResource.properties ?? {};
|
|
61912
|
+
const unresolvedTokens = /* @__PURE__ */ new Set();
|
|
61913
|
+
const noteUnresolved = (tokens) => {
|
|
61914
|
+
for (const token of tokens) unresolvedTokens.add(token);
|
|
61915
|
+
};
|
|
61916
|
+
let desiredProperties;
|
|
61917
|
+
try {
|
|
61918
|
+
desiredProperties = await resolveStateSecretExpressions(revertBaseline, revertSecretResolver, secrets, { onUnresolved: noteUnresolved });
|
|
61919
|
+
if (stateResource.observedProperties !== void 0) await resolveStateSecretExpressions(stateResource.properties ?? {}, revertSecretResolver, secrets, { onUnresolved: noteUnresolved });
|
|
61920
|
+
} catch (err) {
|
|
61921
|
+
totalUnresolvable++;
|
|
61922
|
+
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not re-resolve the dynamic reference(s) this resource's state records — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
|
|
61923
|
+
return;
|
|
61924
|
+
}
|
|
61925
|
+
const unrevertablePaths = outcome.changes.map((c) => c.path).filter((path) => (path.split(".", 1)[0] ?? "").includes("***"));
|
|
61926
|
+
if (unrevertablePaths.length > 0) logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): cannot revert ${unrevertablePaths.join(", ")} — cdkd cannot name the property, so it is left as AWS has it. ROTATE the secret that leaked into the property name.`);
|
|
61927
|
+
if (unresolvedTokens.size > 0) logger.warn(` ! [revert] ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): cdkd cannot resolve ${maskSecretsInText([...unresolvedTokens].join(", "), secrets)} — a property whose WHOLE value is one of these tokens is left UNCHANGED by this revert. Where a token is EMBEDDED in a longer string, that string is written with the token literal, exactly as 'cdkd deploy' does, so a resolved value AWS holds there is overwritten.`);
|
|
61928
|
+
let newProperties;
|
|
61929
|
+
try {
|
|
61930
|
+
const overlaid = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties, { preserveUntemplated: stateResource.observedProperties === void 0 });
|
|
61931
|
+
newProperties = unresolvedTokens.size > 0 ? preserveLiveValuesAtUnresolvedTokens(overlaid, outcome.awsProperties, secrets) : overlaid;
|
|
61932
|
+
} catch (err) {
|
|
61933
|
+
totalFailed++;
|
|
61934
|
+
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not build the revert payload — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
|
|
61935
|
+
return;
|
|
61936
|
+
}
|
|
61393
61937
|
try {
|
|
61394
|
-
const updateResult = await withRetry(() => provider.update(outcome.logicalId, stateResource.physicalId, outcome.resourceType, newProperties, outcome.awsProperties, { desiredFromAwsReadback: true }), outcome.logicalId, { logger: { debug: (msg) => logger.debug(msg) } });
|
|
61938
|
+
const updateResult = await withRetry(() => provider.update(outcome.logicalId, stateResource.physicalId, outcome.resourceType, newProperties, outcome.awsProperties, { desiredFromAwsReadback: true }), outcome.logicalId, { logger: { debug: (msg) => logger.debug(maskSecretsInText(msg, secrets)) } });
|
|
61395
61939
|
totalSucceeded++;
|
|
61396
61940
|
logger.info(` ✓ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted.`);
|
|
61397
61941
|
try {
|
|
61398
61942
|
if (updateResult?.effectiveProperties) {
|
|
61399
61943
|
const delta = collectNarrowedTopLevelKeys(newProperties, updateResult.effectiveProperties);
|
|
61400
|
-
if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, delta);
|
|
61944
|
+
if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, redactSecretsForState(delta, secrets, revertBaseline, STATE_SOURCED_READBACK_RULES));
|
|
61401
61945
|
}
|
|
61402
61946
|
} catch (captureErr) {
|
|
61403
|
-
logger.warn(` ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted, but the provider's reported effective properties could not be read — ${captureErr instanceof Error ? captureErr.message : String(captureErr)}`);
|
|
61947
|
+
logger.warn(` ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted, but the provider's reported effective properties could not be read — ${maskSecretsInText(captureErr instanceof Error ? captureErr.message : String(captureErr), secrets)}`);
|
|
61404
61948
|
}
|
|
61405
61949
|
} catch (err) {
|
|
61406
61950
|
if (err instanceof ResourceUpdateNotSupportedError) {
|
|
61407
61951
|
totalUnsupported++;
|
|
61408
|
-
logger.warn(` ⊘ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not revert — ${err.message}`);
|
|
61952
|
+
logger.warn(` ⊘ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not revert — ${maskSecretsInText(err.message, secrets)}`);
|
|
61409
61953
|
return;
|
|
61410
61954
|
}
|
|
61411
61955
|
totalFailed++;
|
|
61412
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
61956
|
+
const msg = maskSecretsInText(err instanceof Error ? err.message : String(err), secrets);
|
|
61413
61957
|
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): AWS update failed — ${msg}`);
|
|
61414
61958
|
}
|
|
61415
61959
|
}), concurrency);
|
|
@@ -61467,10 +62011,11 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
61467
62011
|
}
|
|
61468
62012
|
const summaryParts = [`${totalSucceeded} reverted`];
|
|
61469
62013
|
if (totalUnsupported > 0) summaryParts.push(`${totalUnsupported} update-not-supported`);
|
|
62014
|
+
if (totalUnresolvable > 0) summaryParts.push(`${totalUnresolvable} reference-unresolvable`);
|
|
61470
62015
|
if (totalFailed > 0) summaryParts.push(`${totalFailed} failed`);
|
|
61471
62016
|
logger.info(`\nRevert summary: ${summaryParts.join(", ")}.`);
|
|
61472
62017
|
if (totalUnsupported > 0) logger.warn(`${totalUnsupported} resource(s) cannot be reverted in place — re-deploy the stack with cdkd deploy --replace, or destroy + redeploy to push the cdkd-state values back into AWS.`);
|
|
61473
|
-
if (totalFailed > 0 || totalUnsupported > 0) throw new PartialFailureError(`Revert completed with ${totalFailed + totalUnsupported} resource error(s) (${totalFailed} AWS update failure(s), ${totalUnsupported} update-not-supported). Re-run 'cdkd drift <stack>' to see the remaining drift, then 'cdkd drift <stack> --revert' to retry.`);
|
|
62018
|
+
if (totalFailed > 0 || totalUnsupported > 0 || totalUnresolvable > 0) throw new PartialFailureError(`Revert completed with ${totalFailed + totalUnsupported + totalUnresolvable} resource error(s) (${totalFailed} AWS update failure(s), ${totalUnsupported} update-not-supported, ${totalUnresolvable} whose dynamic reference(s) could not be resolved — those never reached provider.update; grant the caller secretsmanager:GetSecretValue / ssm:GetParameter, or fix the reference). Re-run 'cdkd drift <stack>' to see the remaining drift, then 'cdkd drift <stack> --revert' to retry.`);
|
|
61474
62019
|
}
|
|
61475
62020
|
/**
|
|
61476
62021
|
* Print the planned state mutations for `--accept` (no AWS calls). One
|
|
@@ -61485,7 +62030,14 @@ function printAcceptPlan(reports) {
|
|
|
61485
62030
|
process.stdout.write(`\nPlan (--accept): update cdkd state for ${report.stackName} (${report.region}):\n`);
|
|
61486
62031
|
for (const o of drifted) {
|
|
61487
62032
|
process.stdout.write(` ~ ${o.logicalId} (${o.resourceType})\n`);
|
|
61488
|
-
for (const change of o.changes)
|
|
62033
|
+
for (const change of o.changes) {
|
|
62034
|
+
const refusal = acceptRefusalReason(change, o.maskedPaths);
|
|
62035
|
+
if (refusal !== void 0) {
|
|
62036
|
+
process.stdout.write(` ${change.path}: SKIPPED — ${refusal}\n`);
|
|
62037
|
+
continue;
|
|
62038
|
+
}
|
|
62039
|
+
process.stdout.write(` ${change.path}: ${formatScalar(change.stateValue)} -> ${formatScalar(change.awsValue)}\n`);
|
|
62040
|
+
}
|
|
61489
62041
|
}
|
|
61490
62042
|
}
|
|
61491
62043
|
}
|
|
@@ -61506,20 +62058,23 @@ function printRevertPlan(reports) {
|
|
|
61506
62058
|
const stateResource = report.state.resources[o.logicalId];
|
|
61507
62059
|
if (stateResource) {
|
|
61508
62060
|
const preserved = findRevertPreservedTagKeys(o.changes, stateResource.observedProperties ?? stateResource.properties ?? {}, o.awsProperties);
|
|
61509
|
-
|
|
62061
|
+
const cannotMaskKeys = o.referencesUnresolved;
|
|
62062
|
+
if (cannotMaskKeys && preserved.length > 0) process.stdout.write(` ! ${preserved.length} AWS-authored tag(s) will be preserved, but cdkd could not resolve this resource's dynamic reference(s), so their names are withheld — they come from the live readback and cannot be checked for secrets without them.\n`);
|
|
62063
|
+
if (!cannotMaskKeys && preserved.length > 0) {
|
|
61510
62064
|
const tagWord = preserved.length === 1 ? "tag" : "tags";
|
|
61511
62065
|
process.stdout.write(` ! reverting this tag list KEEPS ${preserved.length} AWS-authored ${tagWord} the baseline does not carry:\n`);
|
|
61512
|
-
for (const path of preserved) process.stdout.write(` ${path}\n`);
|
|
62066
|
+
for (const path of preserved) process.stdout.write(` ${maskSecretsInText(path, o.secrets)}\n`);
|
|
61513
62067
|
const othersRevert = stateResource.observedProperties === void 0 ? "" : `Every other tag reverts normally. `;
|
|
61514
62068
|
process.stdout.write(` ${othersRevert}A service may require these (ECS needs AmazonECSManaged for managed scaling); 'aws:'-prefixed keys are AWS-reserved and cannot be removed by hand.\n`);
|
|
61515
62069
|
}
|
|
61516
62070
|
}
|
|
61517
62071
|
if (stateResource && stateResource.observedProperties === void 0) {
|
|
61518
62072
|
const unbaselined = findRevertUnbaselinedAwsKeys(o.changes, stateResource.properties ?? {}, o.awsProperties);
|
|
61519
|
-
if (unbaselined.length > 0) {
|
|
62073
|
+
if (o.referencesUnresolved && unbaselined.length > 0) process.stdout.write(` ! ${unbaselined.length} AWS-authored value(s) will be left untouched, but cdkd could not resolve this resource's dynamic reference(s), so their paths are withheld — they come from the live readback and cannot be checked for secrets without them.\n`);
|
|
62074
|
+
else if (unbaselined.length > 0) {
|
|
61520
62075
|
const word = unbaselined.length === 1 ? "value" : "values";
|
|
61521
62076
|
process.stdout.write(` ! this resource has no observed-capture baseline, so the revert pushes the raw TEMPLATE and LEAVES ${unbaselined.length} AWS-authored ${word} untouched:\n`);
|
|
61522
|
-
for (const path of unbaselined) process.stdout.write(` ${path}\n`);
|
|
62077
|
+
for (const path of unbaselined) process.stdout.write(` ${maskSecretsInText(path, o.secrets)}\n`);
|
|
61523
62078
|
process.stdout.write(` The template does not declare these, so cdkd cannot tell an AWS-authored value from an out-of-band change and will not reset either (issue #1626). Run 'cdkd state refresh-observed ${report.stackName}' (or re-deploy) to populate observedProperties if you want them reverted too.\n`);
|
|
61524
62079
|
}
|
|
61525
62080
|
}
|
|
@@ -80414,7 +80969,7 @@ function createMigrateCommand() {
|
|
|
80414
80969
|
*/
|
|
80415
80970
|
function buildProgram() {
|
|
80416
80971
|
const program = new Command();
|
|
80417
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.
|
|
80972
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.34");
|
|
80418
80973
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
80419
80974
|
const { profile } = actionCommand.optsWithGlobals();
|
|
80420
80975
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|