@go-to-k/cdkd 0.288.2 → 0.288.3
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-DUlRn84u.js → asg-provider-CQrHElrl.js} +2 -2
- package/dist/{asg-provider-DUlRn84u.js.map → asg-provider-CQrHElrl.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-Cxt3gn8N.js → deploy-engine-CfxcC3q3.js} +524 -18
- package/dist/deploy-engine-CfxcC3q3.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-BRon74Fc.js → program-BXH3FeeV.js} +702 -74
- package/dist/{program-BRon74Fc.js.map → program-BXH3FeeV.js.map} +1 -1
- package/dist/{version-BRByznpy.js → version-CEyZvTcB.js} +2 -2
- package/dist/{version-BRByznpy.js.map → version-CEyZvTcB.js.map} +1 -1
- package/package.json +3 -13
- package/dist/deploy-engine-Cxt3gn8N.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as ProviderRegistry, $n as redactDockerArgvValues, $r as ResourceUpdateNotSupportedError, $t as STATE_SOURCED_READBACK_RULES, A as renderStatefulReason, An as createAssetRedirectResolver, Ar as AssemblyReader, At as configStringRefusal, B as red, Bn as readBootstrapMarkerBody, Bt as s3BucketDualStackDomainName, C as refusesFinalSnapshot, Cn as importableOutputKeys, Cr as uploadCfnTemplate, Ct as refStateLookupFromResource, D as MULTI_REGION_RECREATE_BLOCKED_TYPES, Dn as stringifyValue, Dr as PARTITION_TABLE, Dt as assertRegionMatch, E as extractDeploymentEventError, En as AssetPublisher, Er as expectedOwnerParam, Et as resolveExplicitPhysicalId, F as formatResourceLine, Fn as assertAssetBucketRegion, Fr as getAwsClients, Ft as requireConfigString, G as isExportAliasCollision, Gn as buildDockerImage, Gr as IntrinsicResolutionRefusalError, Gt as INTRINSIC_KEYS, H as collectDeclaredOutputNames, Hn as validateContainerRepoName, Ht as s3BucketWebsiteUrl, I as bold, In as ensureAssetStorage, Ir as resetAwsClients, It as classifyReplaySecretRegion, J as stateKeySecretExposure, Jn as describeDockerFailure, Jt as describeTypeWithThrottleRetry, K as secretBearingStateKeyWarning, Kn as describeDockerCapturedOutput, Kr as LocalInvokeBuildError$1, Kt as findActionableSilentDrops, L as cyan, Ln as getBootstrapMarkerKey, Lr as setAwsClients, Lt as producerRegionsFromState, M as coerceWarmThroughput$1, Mn as rewriteTemplateAssetReferences, Mt as replayWarn, N as isWarmThroughputDecrease, Nn as AssetModeResolver, Nr as resolveBucketRegion, Nt as requireConfigArray, O as isStatefulRecreateTargetForReplace, On as WorkGraph, Or as canonicalizeRegion, Ot as coerceCfnBoolean, P as toFiniteNumber, Pn as BOOTSTRAP_MARKER_PREFIX, Pr as AwsClients, Pt as requireConfigObject, Q as clearOnUpdateRemoval, Qn as partitionSensitiveEnv, Qr as ResourceTimeoutError, Qt as STATE_SOURCED_CROSS_GENERATION_RULES, R as gray, Rn as isCrossRegionRedirect, Rt as s3BucketArn, S as isFinalSnapshotError, Sn as exportNamesCarriedFrom, Sr as findLargeInlineResources, St as parameterTypeMayLoseSecretIdentity, T as makeCanonicalizePropertiesFn, Tn as shouldRetainResource, Tr as displaySafe, Tt as normalizeAwsTagsToCfn, U as collectPublishedOutputNames, Un as buildDenyExternalAccessPolicy, Ur as DeployCancelledError, Ut as applyRoleArnIfSet, V as yellow, Vn as validateAssetBucketName, Vr as CrossAccountSecretRefusalError, Vt as s3BucketRegionalDomainName, W as exportAliasCollisionScrubWarning, Wn as describeAwsFailure, Wr as DynamicReferenceRegionAmbiguousError, Wt as DiffCalculator, X as IAMRoleProvider, Xn as formatDockerLoginError, Xr as PartialFailureError, Xt as DagBuilder, Y as getCurrentResourceSecrets, Yn as dockerSpawnEnvWithSensitive, Yr as NestedStackChildDirectDestroyError, Yt as withRetry, Z as collectInlinePolicyNamesManagedBySiblings, Zn as getDockerCmd, Zr as ProvisioningError, Zt as TemplateParser, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _n as buildLockContentionMessage, _r as stateBucketExistenceConfirmed, _t as carriesDynamicReference, a as DeploymentEventsStore, an as isSingleDynamicReferenceToken, ar as getDockerImageBySourceHash, at as endCommandInterruptScope, b as ccRoutedFinalSnapshotError, bn as CUSTOM_RESOURCE_RESPONSE_PREFIX, br as CFN_TEMPLATE_URL_LIMIT, bt as getAccountInfo, c as replayFailedOperations, ci as isMarkedNonRetryable, cn as recordMaskOnlyValue, cr as getDefaultStateBucketName, ct as startInterruptWatch, d as updatePartialReason, di as isTransientServerError, dn as scrubResourceRecord, dr as resolveAutoAssetStorage, dt as UNSPECIFIED_SKIP_REASON, ei as StackHasActiveImportsError, en as TEMPLATE_SOURCED_RULES, er as runDockerForeground, et as wouldReturnToSdkProvider, f as withResourceDeadline, fi as markNonRetryable, fn as LockManager, fr as resolveCaptureObservedState, ft as deleteIndeterminateGuards, g as bindingSkippedOutputs, gn as buildForceUnlockCommand, gr as resolveUseCdkBootstrapAssets, gt as IntrinsicFunctionResolver, h as computeImplicitDeleteEdges, hn as UNRENDERABLE, hr as resolveStateBucketWithDefaultAndSource, ht as isTerminationProtectionPropagationError, i as DeploymentEventsReader, in as errorCauseChain, ir as AssetManifestLoader, it as beginCommandInterruptScope, j as WARM_THROUGHPUT_MEMBERS, jn as loadPublishableAssetManifest, jr as processStackMessages, jt as readConfigString, k as isStatefulRecreateTargetSync, kn as buildAssetRedirectMap, kr as derivePartitionAndUrlSuffix, kt as configBooleanRefusal, l as replayRollback, li as isRetryableTransientError, ln as recoverMaskedOutput, lr as getLegacyStateBucketName, lt as CloudControlProvider, m as IMPLICIT_DELETE_DEPENDENCIES, mi as retryClassificationText, mn as rebuildClientForBucketRegion, mr as resolveStateBucketWithDefault, mt as disableInstanceApiTermination, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as createSecretMasker, nr as escapeRegExp, nt as maskDeep, o as planFailedOps, oi as normalizeAwsError, on as maskSecretsInError, or as Synthesizer, ot as interruptWatchListenerCount, p as maskingRetryLogger, pi as markRedactedCause, pn as S3StateBackend, pr as resolveSkipPrefix, pt as deleteSkipReason, q as secretSafeKeyDisplay, qn as describeDockerExecFailure, qr as LocalStartServiceError, qt as findSilentDropProperties, r as DeployEngine, rn as dynamicReferenceTokens, rr as stripControlChars, rt as maskerOrIdentity, s as planRollback, si as withErrorHandling, sn as maskSecretsInText, sr as synthesisStatusMessage, st as isInterruptedWaitError, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as StackTerminationProtectionError, tn as carriesSecretMask, tr as runDockerStreaming, tt as createMaskedRetryLogger, u as updatePartialMessage, ui as isThrottlingError, un as redactSecretsForState, ur as resolveApp, ut as slowCcOperationTimeoutMs, v as PRE_DELETE_SNAPSHOT_TYPES, vn as forceQuitRecoveryClause, vr as warnDeprecatedNoPrefixCliFlag, vt as cfnRefValueFromPhysicalId, w as unsupportedFinalSnapshotError, wn as importableOutputs, wr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, wt as WAFv2WebACLProvider, x as createPreDeleteFinalSnapshot, xn as DEFAULT_STATE_PREFIX, xr as MIGRATE_TMP_PREFIX, xt as isUnboundTemplateParameter, y as buildFinalSnapshotIdentifier, yn as shellQuote, yr as CFN_TEMPLATE_BODY_LIMIT, yt as coerceParameterTypedValue, z as green, zn as parseBootstrapMarker, zr as CdkdError, zt as s3BucketDomainName } from "./deploy-engine-CfxcC3q3.js";
|
|
2
2
|
import { _ as withStackName, c as PATTERN_B_NAME_OPTIONS, f as generateResourceName, g as withSkipPrefix, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as getCurrentStackName, n as getLogger, o as runStackBuffered, p as generateResourceNameWithFallback, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES } from "./logger-Dw-3y48G.js";
|
|
3
3
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
4
|
-
import { t as getCdkdVersion } from "./version-
|
|
5
|
-
import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as getCdkdVersion } from "./version-CEyZvTcB.js";
|
|
5
|
+
import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-CQrHElrl.js";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
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, NotFound, 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";
|
|
@@ -3470,6 +3470,67 @@ function mapsEqual(a, b) {
|
|
|
3470
3470
|
}
|
|
3471
3471
|
return true;
|
|
3472
3472
|
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Describe a `JSON.parse` failure WITHOUT echoing the body (issue
|
|
3475
|
+
* [#2667](https://github.com/go-to-k/cdkd/issues/2667) review).
|
|
3476
|
+
*
|
|
3477
|
+
* V8 embeds a window of the INPUT in its parse error, so interpolating
|
|
3478
|
+
* `err.message` republishes whatever bytes sat near the syntax error. Measured
|
|
3479
|
+
* on node v24.19.0:
|
|
3480
|
+
*
|
|
3481
|
+
* ```
|
|
3482
|
+
* JSON.parse('{"exports":{"K":{"value":hunter2SECRET}}}')
|
|
3483
|
+
* -> Unexpected token 'h', ..."K":{"value":hunter2SEC"... is not valid JSON
|
|
3484
|
+
* ```
|
|
3485
|
+
*
|
|
3486
|
+
* This index holds resolved Output VALUES, and for the unscrubbed state
|
|
3487
|
+
* `cdkd scrub` targets those are exactly the plaintext — which then reaches a
|
|
3488
|
+
* `logger.error` and the command's failure message, i.e. `--dry-run --fail` CI
|
|
3489
|
+
* logs. Same class as the `import.ts` parse-snippet leak issue
|
|
3490
|
+
* [#2829](https://github.com/go-to-k/cdkd/issues/2829) closed.
|
|
3491
|
+
*
|
|
3492
|
+
* What survives is the SIZE, plus the OFFSET WHEN V8 STATES ONE — neither is a
|
|
3493
|
+
* function of the body's content, and both help an operator holding the object
|
|
3494
|
+
* locate the damage. The offset is extracted by pattern rather than passed
|
|
3495
|
+
* through, so a message worded differently yields no offset instead of leaking
|
|
3496
|
+
* the rest of the sentence. The two numbers are in DIFFERENT UNITS and the
|
|
3497
|
+
* message says so: V8 counts `at position` in UTF-16 code units while the size
|
|
3498
|
+
* is UTF-8 bytes, so `'{ n\u00f6t json'` reports position 2 and 11 bytes. Measured on node v24.19.0: the `Expected ...`
|
|
3499
|
+
* family carries `at position N` and the `Unexpected token 'X', ...snippet...`
|
|
3500
|
+
* family — the one that embeds the body, i.e. the case this exists for — does
|
|
3501
|
+
* NOT, so the size is what is always present and the offset is a bonus. Both
|
|
3502
|
+
* arms are driven by tests.
|
|
3503
|
+
*/
|
|
3504
|
+
function describeParseFailure(err, bytes) {
|
|
3505
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
3506
|
+
const at = /at position (\d+)/.exec(raw);
|
|
3507
|
+
return `invalid JSON${at ? ` at code-unit position ${at[1]}` : ""}; ${bytes} byte(s) read`;
|
|
3508
|
+
}
|
|
3509
|
+
/**
|
|
3510
|
+
* UTF-8 byte length of a decoded body, for {@link describeParseFailure}.
|
|
3511
|
+
*
|
|
3512
|
+
* `body.length` counts UTF-16 CODE UNITS, so reporting it as "byte(s)" is
|
|
3513
|
+
* wrong for any non-ASCII index — an export name outside the BMP counts 2
|
|
3514
|
+
* where the object holds 4 (issue #2667 review). The number is meant to help
|
|
3515
|
+
* an operator match the message against the object S3 holds, so it has to be
|
|
3516
|
+
* the same unit S3 reports.
|
|
3517
|
+
*/
|
|
3518
|
+
function utf8ByteLength(body) {
|
|
3519
|
+
return Buffer.byteLength(body, "utf8");
|
|
3520
|
+
}
|
|
3521
|
+
/**
|
|
3522
|
+
* Describe an `indexVersion` that is not a usable number, WITHOUT rendering it.
|
|
3523
|
+
*
|
|
3524
|
+
* `String(x)` renders a body-controlled value verbatim for two of the three
|
|
3525
|
+
* non-number shapes — measured on node v24.19.0: a JSON string yields itself
|
|
3526
|
+
* and a one-element array yields its element, while only an object degrades to
|
|
3527
|
+
* `[object Object]`. The field is attacker-controlled in the same sense the
|
|
3528
|
+
* rest of the body is, so the message reports its TYPE and, for a number, its
|
|
3529
|
+
* value — the only shape whose value is safe because it is a number.
|
|
3530
|
+
*/
|
|
3531
|
+
function describeIndexVersion(value) {
|
|
3532
|
+
return typeof value === "number" ? String(value) : `a non-numeric value (${typeof value})`;
|
|
3533
|
+
}
|
|
3473
3534
|
const DEFAULT_OPTIONS = {
|
|
3474
3535
|
maxWriteRetries: 5,
|
|
3475
3536
|
initialBackoffMs: 100,
|
|
@@ -3597,12 +3658,57 @@ var ExportIndexStore = class {
|
|
|
3597
3658
|
await this.enqueueWrite("update", () => this.applyStackUpdate(stackName, producerRegion, outputs));
|
|
3598
3659
|
}
|
|
3599
3660
|
/**
|
|
3661
|
+
* A snapshot of the persisted index, read WITHOUT the rebuild that
|
|
3662
|
+
* {@link lookup} triggers on a missing or corrupt object.
|
|
3663
|
+
*
|
|
3664
|
+
* Every S3 call THIS METHOD makes is a GET, so a caller that must perform no
|
|
3665
|
+
* S3 write (`cdkd scrub --dry-run`) can read the entries here — with ONE
|
|
3666
|
+
* documented exception, which such a caller has to know about: when another
|
|
3667
|
+
* caller on the same instance already has a `doLoad` in flight, the first
|
|
3668
|
+
* line below awaits IT, and `doLoad` rebuilds (a PUT) on a missing or
|
|
3669
|
+
* corrupt object. Unreachable from `cdkd scrub`, which never calls `lookup`
|
|
3670
|
+
* and drives one store per region serially — but the guarantee is the thing
|
|
3671
|
+
* a future no-write caller would rely on, so it is stated with its bound
|
|
3672
|
+
* rather than unconditionally.
|
|
3673
|
+
*
|
|
3674
|
+
* THREE OUTCOMES, and only ONE of them is `undefined`. A MISSING object
|
|
3675
|
+
* returns `undefined`: there is nothing to audit and nothing to repair. A
|
|
3676
|
+
* body that does not PARSE, and an `indexVersion` this binary cannot
|
|
3677
|
+
* interpret, both THROW — because the caller's question is "what does this
|
|
3678
|
+
* index hold", and for those two the honest answer is "this call could not
|
|
3679
|
+
* find out". Collapsing the corrupt case into `undefined` reads as an empty
|
|
3680
|
+
* index, so a caller reports nothing over bytes it never interpreted, which
|
|
3681
|
+
* for `cdkd scrub` is a green gate over a file that may still hold the
|
|
3682
|
+
* plaintext (issue [#2667](https://github.com/go-to-k/cdkd/issues/2667)) —
|
|
3683
|
+
* the exact shape that issue exists to close, one level down. `doLoad`'s own
|
|
3684
|
+
* corrupt arm is unaffected: it REBUILDS, which is a write, and is why this
|
|
3685
|
+
* read cannot share it.
|
|
3686
|
+
*
|
|
3687
|
+
* The returned map is a COPY, and a `patchEntry` on this same instance
|
|
3688
|
+
* afterwards writes under the etag this read cached rather than re-reading.
|
|
3689
|
+
*/
|
|
3690
|
+
async readPersistedEntries() {
|
|
3691
|
+
if (this.loadState.kind === "loading") await this.loadState.promise;
|
|
3692
|
+
if (this.loadState.kind === "loaded") return new Map(this.loadState.entries);
|
|
3693
|
+
const outcome = await this.loadPersisted();
|
|
3694
|
+
if (outcome.kind === "missing") return void 0;
|
|
3695
|
+
if (outcome.kind === "corrupt") throw new Error(`Exports index at ${this.indexKey()} could not be parsed (${describeParseFailure(outcome.error, outcome.bytes)}). Its contents were NOT examined.`);
|
|
3696
|
+
return new Map(outcome.entries);
|
|
3697
|
+
}
|
|
3698
|
+
/**
|
|
3600
3699
|
* Patch a single entry into the index after a `lookup` miss fell back
|
|
3601
3700
|
* to a state.json scan and found the value. Lightweight write that
|
|
3602
3701
|
* does NOT require a full rebuild.
|
|
3702
|
+
*
|
|
3703
|
+
* Returns whether the index now holds `entry` — `false` when the write
|
|
3704
|
+
* was abandoned (a non-retryable S3 failure, or an exhausted If-Match
|
|
3705
|
+
* retry budget), which `runWithRetry` logs and does not throw. A caller
|
|
3706
|
+
* for which an unwritten entry is the outcome it must report — `cdkd
|
|
3707
|
+
* scrub`, where the entry keeps whatever value it held — reads this
|
|
3708
|
+
* return; the deploy path treats the index as a perf hint and ignores it.
|
|
3603
3709
|
*/
|
|
3604
|
-
async patchEntry(exportName, entry) {
|
|
3605
|
-
|
|
3710
|
+
async patchEntry(exportName, entry, opts = {}) {
|
|
3711
|
+
return this.enqueueWrite("patch", () => this.applyPatch(exportName, entry, opts.requireOwner));
|
|
3606
3712
|
}
|
|
3607
3713
|
/**
|
|
3608
3714
|
* Drop all entries owned by `(stackName, producerRegion)`. Used after
|
|
@@ -3623,7 +3729,7 @@ var ExportIndexStore = class {
|
|
|
3623
3729
|
*/
|
|
3624
3730
|
async enqueueWrite(label, op) {
|
|
3625
3731
|
const next = this.writeChain.then(() => this.runWithRetry(label, op));
|
|
3626
|
-
this.writeChain = next.
|
|
3732
|
+
this.writeChain = next.then(() => {}, () => {});
|
|
3627
3733
|
return next;
|
|
3628
3734
|
}
|
|
3629
3735
|
/**
|
|
@@ -3658,31 +3764,54 @@ var ExportIndexStore = class {
|
|
|
3658
3764
|
};
|
|
3659
3765
|
await promise;
|
|
3660
3766
|
}
|
|
3767
|
+
/**
|
|
3768
|
+
* GET the index object and, when it parses, install it as `loadState`.
|
|
3769
|
+
* Performs no PUT: the missing / corrupt REBUILD lives one level up in
|
|
3770
|
+
* {@link doLoad}, so {@link readPersistedEntries} can share this parse
|
|
3771
|
+
* without writing. A `indexVersion` above this binary's still throws
|
|
3772
|
+
* here rather than reporting `corrupt` — a rebuild would overwrite an
|
|
3773
|
+
* index a newer binary wrote with this binary's view of it.
|
|
3774
|
+
*/
|
|
3775
|
+
async loadPersisted() {
|
|
3776
|
+
const raw = await this.readIndexRaw();
|
|
3777
|
+
if (raw === null) return { kind: "missing" };
|
|
3778
|
+
const { body, etag } = raw;
|
|
3779
|
+
let parsed;
|
|
3780
|
+
try {
|
|
3781
|
+
parsed = JSON.parse(body);
|
|
3782
|
+
} catch (err) {
|
|
3783
|
+
return {
|
|
3784
|
+
kind: "corrupt",
|
|
3785
|
+
error: err,
|
|
3786
|
+
bytes: utf8ByteLength(body)
|
|
3787
|
+
};
|
|
3788
|
+
}
|
|
3789
|
+
if (typeof parsed.indexVersion !== "number" || parsed.indexVersion > 1) throw new Error(`Exports index uses indexVersion ${describeIndexVersion(parsed.indexVersion)} which is newer than this cdkd binary supports (max ${1}). Upgrade cdkd.`);
|
|
3790
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3791
|
+
for (const [name, entry] of Object.entries(parsed.exports ?? {})) entries.set(name, entry);
|
|
3792
|
+
this.loadState = {
|
|
3793
|
+
kind: "loaded",
|
|
3794
|
+
etag,
|
|
3795
|
+
entries
|
|
3796
|
+
};
|
|
3797
|
+
return {
|
|
3798
|
+
kind: "loaded",
|
|
3799
|
+
entries
|
|
3800
|
+
};
|
|
3801
|
+
}
|
|
3661
3802
|
async doLoad() {
|
|
3662
3803
|
try {
|
|
3663
|
-
const
|
|
3664
|
-
if (
|
|
3804
|
+
const outcome = await this.loadPersisted();
|
|
3805
|
+
if (outcome.kind === "missing") {
|
|
3665
3806
|
this.logger.info("Exports index missing; rebuilding from state.json files");
|
|
3666
3807
|
await this.rebuild();
|
|
3667
3808
|
return;
|
|
3668
3809
|
}
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
try {
|
|
3672
|
-
parsed = JSON.parse(body);
|
|
3673
|
-
} catch (err) {
|
|
3674
|
-
this.logger.warn(`Exports index corrupt (${err instanceof Error ? err.message : String(err)}); rebuilding from state.json files`);
|
|
3810
|
+
if (outcome.kind === "corrupt") {
|
|
3811
|
+
this.logger.warn(`Exports index corrupt (${outcome.error instanceof Error ? outcome.error.message : String(outcome.error)}); rebuilding from state.json files`);
|
|
3675
3812
|
await this.rebuild();
|
|
3676
3813
|
return;
|
|
3677
3814
|
}
|
|
3678
|
-
if (typeof parsed.indexVersion !== "number" || parsed.indexVersion > 1) throw new Error(`Exports index uses indexVersion ${String(parsed.indexVersion)} which is newer than this cdkd binary supports (max ${1}). Upgrade cdkd.`);
|
|
3679
|
-
const entries = /* @__PURE__ */ new Map();
|
|
3680
|
-
for (const [name, entry] of Object.entries(parsed.exports ?? {})) entries.set(name, entry);
|
|
3681
|
-
this.loadState = {
|
|
3682
|
-
kind: "loaded",
|
|
3683
|
-
etag,
|
|
3684
|
-
entries
|
|
3685
|
-
};
|
|
3686
3815
|
} catch (err) {
|
|
3687
3816
|
this.loadState = { kind: "unloaded" };
|
|
3688
3817
|
throw err;
|
|
@@ -3764,7 +3893,7 @@ var ExportIndexStore = class {
|
|
|
3764
3893
|
}
|
|
3765
3894
|
async applyStackUpdate(stackName, producerRegion, outputs) {
|
|
3766
3895
|
await this.ensureLoaded();
|
|
3767
|
-
if (this.loadState.kind !== "loaded") return;
|
|
3896
|
+
if (this.loadState.kind !== "loaded") return false;
|
|
3768
3897
|
const next = new Map(this.loadState.entries);
|
|
3769
3898
|
for (const [name, entry] of next) if (entry.producerStack === stackName && entry.producerRegion === producerRegion) next.delete(name);
|
|
3770
3899
|
for (const [name, value] of Object.entries(outputs)) {
|
|
@@ -3775,8 +3904,8 @@ var ExportIndexStore = class {
|
|
|
3775
3904
|
producerRegion
|
|
3776
3905
|
});
|
|
3777
3906
|
}
|
|
3778
|
-
if (mapsEqual(this.loadState.entries, next)) return;
|
|
3779
|
-
|
|
3907
|
+
if (mapsEqual(this.loadState.entries, next)) return true;
|
|
3908
|
+
return this.persist(next);
|
|
3780
3909
|
}
|
|
3781
3910
|
/**
|
|
3782
3911
|
* Two stacks publishing one export name (issue #2193). CloudFormation
|
|
@@ -3793,27 +3922,34 @@ var ExportIndexStore = class {
|
|
|
3793
3922
|
const safeName = displaySafe(exportName);
|
|
3794
3923
|
this.logger.warn(`Export '${safeName}' is published by both '${existing.producerStack}' (${existing.producerRegion}) and '${stackName}' (${producerRegion}); the exports index keeps the latest writer, so an Fn::ImportValue on it binds to whichever deployed last. CloudFormation refuses a second producer of the same export name — rename one of the two exports. (If '${existing.producerStack}' predates cdkd's state schema v9, this is its stale entry for a plain output of that name, and its next deploy clears it.)`);
|
|
3795
3924
|
}
|
|
3796
|
-
async applyPatch(exportName, entry) {
|
|
3925
|
+
async applyPatch(exportName, entry, requireOwner) {
|
|
3797
3926
|
await this.ensureLoaded();
|
|
3798
|
-
if (this.loadState.kind !== "loaded") return;
|
|
3927
|
+
if (this.loadState.kind !== "loaded") return false;
|
|
3928
|
+
if (requireOwner) {
|
|
3929
|
+
const current = this.loadState.entries.get(exportName);
|
|
3930
|
+
if (!current || current.producerStack !== requireOwner.producerStack || current.producerRegion !== requireOwner.producerRegion) {
|
|
3931
|
+
this.logger.warn(`Exports index entry ownership changed under a patch; refusing to write it. Expected producer '${displaySafe(requireOwner.producerStack)}' (${displaySafe(requireOwner.producerRegion)}), found ${current ? `'${displaySafe(current.producerStack)}' (${displaySafe(current.producerRegion)})` : "no entry"}.`);
|
|
3932
|
+
return false;
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3799
3935
|
const next = new Map(this.loadState.entries);
|
|
3800
3936
|
next.set(exportName, entry);
|
|
3801
|
-
|
|
3937
|
+
return this.persist(next);
|
|
3802
3938
|
}
|
|
3803
3939
|
async applyRemoveStack(stackName, producerRegion) {
|
|
3804
3940
|
await this.ensureLoaded();
|
|
3805
|
-
if (this.loadState.kind !== "loaded") return;
|
|
3941
|
+
if (this.loadState.kind !== "loaded") return false;
|
|
3806
3942
|
const next = new Map(this.loadState.entries);
|
|
3807
3943
|
let changed = false;
|
|
3808
3944
|
for (const [name, entry] of next) if (entry.producerStack === stackName && entry.producerRegion === producerRegion) {
|
|
3809
3945
|
next.delete(name);
|
|
3810
3946
|
changed = true;
|
|
3811
3947
|
}
|
|
3812
|
-
if (!changed) return;
|
|
3813
|
-
|
|
3948
|
+
if (!changed) return true;
|
|
3949
|
+
return this.persist(next);
|
|
3814
3950
|
}
|
|
3815
3951
|
async persist(entries) {
|
|
3816
|
-
if (this.loadState.kind !== "loaded") return;
|
|
3952
|
+
if (this.loadState.kind !== "loaded") return false;
|
|
3817
3953
|
const file = {
|
|
3818
3954
|
indexVersion: 1,
|
|
3819
3955
|
region: this.region,
|
|
@@ -3826,12 +3962,20 @@ var ExportIndexStore = class {
|
|
|
3826
3962
|
etag,
|
|
3827
3963
|
entries
|
|
3828
3964
|
};
|
|
3965
|
+
return true;
|
|
3829
3966
|
}
|
|
3967
|
+
/**
|
|
3968
|
+
* Run `op` under the If-Match retry budget. `true` means `op` reported the
|
|
3969
|
+
* index now holds what it was asked to write (a PUT, or a no-op skip whose
|
|
3970
|
+
* result was already there); `false` means this store abandoned the write
|
|
3971
|
+
* and logged the warn below. It does NOT throw on either arm — `state.json`
|
|
3972
|
+
* is the canonical record for the deploy path, and a caller that needs the
|
|
3973
|
+
* failure surfaced reads the return value (see {@link patchEntry}).
|
|
3974
|
+
*/
|
|
3830
3975
|
async runWithRetry(label, op) {
|
|
3831
3976
|
let lastErr;
|
|
3832
3977
|
for (let attempt = 0; attempt < this.opts.maxWriteRetries; attempt++) try {
|
|
3833
|
-
await op();
|
|
3834
|
-
return;
|
|
3978
|
+
return await op();
|
|
3835
3979
|
} catch (err) {
|
|
3836
3980
|
lastErr = err;
|
|
3837
3981
|
if (this.isPreconditionFailed(err)) {
|
|
@@ -3841,9 +3985,10 @@ var ExportIndexStore = class {
|
|
|
3841
3985
|
continue;
|
|
3842
3986
|
}
|
|
3843
3987
|
this.logger.warn(`Exports index ${label} failed (non-retryable): ${err instanceof Error ? err.message : String(err)}; continuing without index update`);
|
|
3844
|
-
return;
|
|
3988
|
+
return false;
|
|
3845
3989
|
}
|
|
3846
3990
|
this.logger.warn(`Exports index ${label} exhausted ${this.opts.maxWriteRetries} retries due to concurrent writers; continuing without index update. Last error: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
3991
|
+
return false;
|
|
3847
3992
|
}
|
|
3848
3993
|
isNoSuchKey(err) {
|
|
3849
3994
|
if (!err || typeof err !== "object") return false;
|
|
@@ -31089,11 +31234,13 @@ var ApiGatewayV2Provider = class {
|
|
|
31089
31234
|
const apiId = response.ApiId;
|
|
31090
31235
|
const apiEndpoint = response.ApiEndpoint;
|
|
31091
31236
|
this.logger.debug(`Successfully created API Gateway V2 Api ${logicalId}: ${apiId}`);
|
|
31237
|
+
const executeApiArn = await this.buildExecuteApiArn(apiId);
|
|
31092
31238
|
return {
|
|
31093
31239
|
physicalId: apiId,
|
|
31094
31240
|
attributes: {
|
|
31095
31241
|
ApiId: apiId,
|
|
31096
|
-
ApiEndpoint: apiEndpoint
|
|
31242
|
+
ApiEndpoint: apiEndpoint,
|
|
31243
|
+
...executeApiArn !== void 0 ? { ExecuteApiArn: executeApiArn } : {}
|
|
31097
31244
|
}
|
|
31098
31245
|
};
|
|
31099
31246
|
} catch (error) {
|
|
@@ -31117,6 +31264,52 @@ var ApiGatewayV2Provider = class {
|
|
|
31117
31264
|
throw new ProvisioningError(`Failed to delete API Gateway V2 Api ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
31118
31265
|
}
|
|
31119
31266
|
}
|
|
31267
|
+
/**
|
|
31268
|
+
* `arn:{partition}:execute-api:{region}:{account}:{apiId}` — the ARN AWS
|
|
31269
|
+
* documents for an API Gateway v2 API, which no API call returns.
|
|
31270
|
+
*
|
|
31271
|
+
* Refuses to record a FABRICATED account, exactly as
|
|
31272
|
+
* `SSMParameterProvider.buildParameterArn` does: an ARN naming a placeholder
|
|
31273
|
+
* account is worse than an absent one, because it is persisted into
|
|
31274
|
+
* `state.json` and outlives the deploy that produced it. The partition is
|
|
31275
|
+
* DERIVED through the same closed region mapping `${AWS::Partition}` uses,
|
|
31276
|
+
* and the region segment is canonicalized — defence in depth rather than the
|
|
31277
|
+
* only fold, since `getAccountInfo` already canonicalizes what it returns
|
|
31278
|
+
* (the issue #1795 / #1814 class: an unfolded `US-EAST-1` matches no IAM
|
|
31279
|
+
* policy and is persisted).
|
|
31280
|
+
*
|
|
31281
|
+
* **NEVER THROWS, and callers depend on it.** Every call site is INSIDE the
|
|
31282
|
+
* `try` of an operation that has already created or found the API, so an
|
|
31283
|
+
* escape here would report a failed create over a resource that exists —
|
|
31284
|
+
* an orphan. A failure means the attribute is absent, which is the state
|
|
31285
|
+
* every deploy before this change was in.
|
|
31286
|
+
*
|
|
31287
|
+
* Takes NO `SecretMaskingContext`, where its SSM twin takes one, and the
|
|
31288
|
+
* difference is not an oversight: every interpolated value here is
|
|
31289
|
+
* non-secret BY CONSTRUCTION — an AWS-minted api id (or, on import, the
|
|
31290
|
+
* CLI's own `--resource` argument), a derived partition, a region, an
|
|
31291
|
+
* account id — and the catch names the error CLASS rather than its message.
|
|
31292
|
+
* `SSMParameterProvider.buildParameterArn` needs a masker because it
|
|
31293
|
+
* interpolates `error.message`, which quotes a resolved parameter value
|
|
31294
|
+
* back. **Widening this catch to `error.message` therefore requires
|
|
31295
|
+
* threading the masker in the same change**; the invariant is what makes
|
|
31296
|
+
* the omission safe, not the current absence of a caller.
|
|
31297
|
+
*/
|
|
31298
|
+
async buildExecuteApiArn(apiId) {
|
|
31299
|
+
try {
|
|
31300
|
+
const region = await this.getClient().config.region();
|
|
31301
|
+
const accountInfo = await getAccountInfo(region);
|
|
31302
|
+
if (accountInfo.fabricated) {
|
|
31303
|
+
this.logger.warn(`Cannot determine the AWS account (STS is unreachable, and the resolved account id is a placeholder), so the ExecuteApiArn attribute for API ${apiId} would be fabricated and is NOT recorded. An Fn::GetAtt on it will fail until a later deploy resolves the account.`);
|
|
31304
|
+
return;
|
|
31305
|
+
}
|
|
31306
|
+
const { partition } = derivePartitionAndUrlSuffix(accountInfo.region);
|
|
31307
|
+
return `arn:${partition}:execute-api:${canonicalizeRegion(accountInfo.region)}:${accountInfo.accountId}:${apiId}`;
|
|
31308
|
+
} catch (error) {
|
|
31309
|
+
this.logger.warn(`Could not build the ExecuteApiArn attribute for API ${apiId} (${error instanceof Error ? error.name : typeof error}), so it is NOT recorded. An Fn::GetAtt on it will fail until a later deploy records it.`);
|
|
31310
|
+
return;
|
|
31311
|
+
}
|
|
31312
|
+
}
|
|
31120
31313
|
getApiAttribute(physicalId, attributeName) {
|
|
31121
31314
|
if (attributeName === "ApiId") return physicalId;
|
|
31122
31315
|
}
|
|
@@ -31541,10 +31734,15 @@ var ApiGatewayV2Provider = class {
|
|
|
31541
31734
|
}
|
|
31542
31735
|
const explicit = resolveExplicitPhysicalId(input, null);
|
|
31543
31736
|
if (explicit) try {
|
|
31544
|
-
await this.getClient().send(new GetApiCommand({ ApiId: explicit }));
|
|
31737
|
+
const got = await this.getClient().send(new GetApiCommand({ ApiId: explicit }));
|
|
31738
|
+
const executeApiArn = await this.buildExecuteApiArn(explicit);
|
|
31545
31739
|
return {
|
|
31546
31740
|
physicalId: explicit,
|
|
31547
|
-
attributes: {
|
|
31741
|
+
attributes: {
|
|
31742
|
+
ApiId: explicit,
|
|
31743
|
+
...got.ApiEndpoint !== void 0 ? { ApiEndpoint: got.ApiEndpoint } : {},
|
|
31744
|
+
...executeApiArn !== void 0 ? { ExecuteApiArn: executeApiArn } : {}
|
|
31745
|
+
}
|
|
31548
31746
|
};
|
|
31549
31747
|
} catch (err) {
|
|
31550
31748
|
if (err instanceof NotFoundException$3) return null;
|
|
@@ -31623,11 +31821,25 @@ var ApiGatewayV2Provider = class {
|
|
|
31623
31821
|
}
|
|
31624
31822
|
this.logger.debug(`Updating API Gateway V2 Api ${logicalId}: ${physicalId}`);
|
|
31625
31823
|
try {
|
|
31626
|
-
|
|
31824
|
+
let apiEndpoint;
|
|
31825
|
+
if (changed) apiEndpoint = (await this.getClient().send(new UpdateApiCommand(input))).ApiEndpoint;
|
|
31627
31826
|
if (corsRemoved) {
|
|
31628
31827
|
this.logger.debug(`Clearing CORS configuration for Api ${logicalId}: ${physicalId}`);
|
|
31629
31828
|
await this.getClient().send(new DeleteCorsConfigurationCommand({ ApiId: physicalId }));
|
|
31630
31829
|
}
|
|
31830
|
+
if (apiEndpoint !== void 0) {
|
|
31831
|
+
const executeApiArn = await this.buildExecuteApiArn(physicalId);
|
|
31832
|
+
if (executeApiArn !== void 0) return {
|
|
31833
|
+
physicalId,
|
|
31834
|
+
wasReplaced: false,
|
|
31835
|
+
attributes: {
|
|
31836
|
+
ApiId: physicalId,
|
|
31837
|
+
ApiEndpoint: apiEndpoint,
|
|
31838
|
+
ExecuteApiArn: executeApiArn
|
|
31839
|
+
}
|
|
31840
|
+
};
|
|
31841
|
+
this.logger.debug(`Skipping the ExecuteApiArn heal for API ${physicalId}: the ARN could not be built. The previously recorded attributes are kept.`);
|
|
31842
|
+
}
|
|
31631
31843
|
return {
|
|
31632
31844
|
physicalId,
|
|
31633
31845
|
wasReplaced: false
|
|
@@ -63333,12 +63545,30 @@ function valuesEqual(a, b) {
|
|
|
63333
63545
|
* the expression that state stores post-redaction.
|
|
63334
63546
|
*
|
|
63335
63547
|
* `storedOutputs` is the bag currently in state. It is consulted for exactly
|
|
63336
|
-
*
|
|
63337
|
-
* #1942 at that branch
|
|
63548
|
+
* two decisions — the LITERAL `Export.Name` of a secret-bearing stack, see issue
|
|
63549
|
+
* #1942 at that branch, and whether a skipped-output record still applies, see
|
|
63550
|
+
* issue #2740 below — and never merged into the result: everything else here
|
|
63338
63551
|
* previews what the template says, and reading state anywhere else would make
|
|
63339
63552
|
* the preview agree with the past instead of with the next deploy.
|
|
63340
|
-
|
|
63341
|
-
|
|
63553
|
+
*
|
|
63554
|
+
* `skippedOutputKeys` (issue #2740) are the keys the LAST DEPLOY could not
|
|
63555
|
+
* resolve and skipped, already narrowed by the caller on BOTH halves of the
|
|
63556
|
+
* binding rule: the recorded digest still matches today's template, AND no
|
|
63557
|
+
* resource the output references is changing on this run
|
|
63558
|
+
* (`bindingSkippedOutputs`, over a pre-resolution snapshot and this run's
|
|
63559
|
+
* resource diff — see `skipped-outputs.ts` for why the snapshot is the
|
|
63560
|
+
* caller's and why the change map cannot come from the digest). Such a key,
|
|
63561
|
+
* while still absent from `storedOutputs`, is previewed as ABSENT — skipped
|
|
63562
|
+
* without resolving, no bag entry, no row — because that is what the next
|
|
63563
|
+
* deploy will leave in state: this resolver runs
|
|
63564
|
+
* with `skipDynamicReferences`, so a failure that happened inside a secret
|
|
63565
|
+
* lookup would otherwise assemble cleanly and preview an `ADD` the deploy will
|
|
63566
|
+
* never perform. NOT a resolution failure: the sibling outputs are previewed
|
|
63567
|
+
* normally, so a genuine change beside the broken output still renders and
|
|
63568
|
+
* `--fail` still exits 1 for it. The key appearing in state (a later deploy
|
|
63569
|
+
* resolved it) ends the suppression.
|
|
63570
|
+
*/
|
|
63571
|
+
async function resolveTemplateOutputs(template, resolveFn, conditions, storedOutputs, skippedOutputKeys) {
|
|
63342
63572
|
const logger = getLogger().child("OutputsDiff");
|
|
63343
63573
|
const outputs = Object.create(null);
|
|
63344
63574
|
const exportNames = /* @__PURE__ */ new Set();
|
|
@@ -63379,6 +63609,10 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
63379
63609
|
logger.debug(`Skipping output ${outputKey} — condition ${output.Condition} is false`);
|
|
63380
63610
|
continue;
|
|
63381
63611
|
}
|
|
63612
|
+
if (skippedOutputKeys?.has(outputKey) && !(storedOutputs !== void 0 && Object.prototype.hasOwnProperty.call(storedOutputs, outputKey))) {
|
|
63613
|
+
logger.debug(`Diff previewing output ${stripControlChars(outputKey)} as absent — the last deploy could not resolve it and its template inputs are unchanged`);
|
|
63614
|
+
continue;
|
|
63615
|
+
}
|
|
63382
63616
|
const sourceUsedSub = templateUsesSub(output.Value);
|
|
63383
63617
|
let value;
|
|
63384
63618
|
try {
|
|
@@ -64706,6 +64940,10 @@ function tokenValueForComparison(token, declaredType) {
|
|
|
64706
64940
|
async function computeStackDiff(currentState, template, region, stackName, stateBackend, diffCalculator, options = {}) {
|
|
64707
64941
|
const { parameters, canonicalizeProperties, cfnFallback, inheritSecretBearingTemplate } = options;
|
|
64708
64942
|
const intrinsicResolver = new IntrinsicFunctionResolver(region, { cfnFallback: cfnFallback ?? true });
|
|
64943
|
+
const outputsDigestSource = structuredClone({
|
|
64944
|
+
...template,
|
|
64945
|
+
Resources: {}
|
|
64946
|
+
});
|
|
64709
64947
|
let mergedParameters = parameters;
|
|
64710
64948
|
let parametersBound = false;
|
|
64711
64949
|
const tokenParameterNames = new Set(Object.entries(parameters ?? {}).filter(([, value]) => carriesDynamicReference(value)).map(([name]) => name));
|
|
@@ -64755,7 +64993,9 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
64755
64993
|
skipDynamicReferences: true
|
|
64756
64994
|
});
|
|
64757
64995
|
const changes = await diffCalculator.calculateDiff(currentState, effectiveTemplate, resolveFn, canonicalizeProperties);
|
|
64758
|
-
const
|
|
64996
|
+
const changedLogicalIds = new Set([...changes.values()].filter((c) => c.changeType !== "NO_CHANGE").map((c) => c.logicalId));
|
|
64997
|
+
const bindingSkipped = bindingSkippedOutputs(outputsDigestSource, currentState.skippedOutputs, changedLogicalIds);
|
|
64998
|
+
const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions, currentState.outputs, bindingSkipped);
|
|
64759
64999
|
let outputChanges = [];
|
|
64760
65000
|
if (resolved.resolutionFailed) {
|
|
64761
65001
|
if (computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys, {
|
|
@@ -65399,6 +65639,24 @@ var ScrubNeededError = class ScrubNeededError extends CdkdError {
|
|
|
65399
65639
|
* leak" from "scrub refused to look" — the two call for opposite responses
|
|
65400
65640
|
* (rotate the secret vs. re-spell the reference and re-run).
|
|
65401
65641
|
*/
|
|
65642
|
+
/**
|
|
65643
|
+
* An internal invariant of the exports-index repair, thrown where this code
|
|
65644
|
+
* believes a case is unreachable (issue #2667 review).
|
|
65645
|
+
*
|
|
65646
|
+
* A distinct CLASS because the repair runs inside the per-stack `try` whose
|
|
65647
|
+
* `catch` attributes everything to "the exports index could not be READ": a
|
|
65648
|
+
* plain `Error` there is reported as an S3 read failure, sending an operator to
|
|
65649
|
+
* their bucket policy over a bug in cdkd. That catch rethrows this class
|
|
65650
|
+
* instead, so it surfaces as the per-stack failure it actually is.
|
|
65651
|
+
*/
|
|
65652
|
+
var ScrubIndexInvariantError = class ScrubIndexInvariantError extends CdkdError {
|
|
65653
|
+
exitCode = 2;
|
|
65654
|
+
constructor(message) {
|
|
65655
|
+
super(message, "SCRUB_EXPORT_INDEX_INVARIANT");
|
|
65656
|
+
this.name = "ScrubIndexInvariantError";
|
|
65657
|
+
Object.setPrototypeOf(this, ScrubIndexInvariantError.prototype);
|
|
65658
|
+
}
|
|
65659
|
+
};
|
|
65402
65660
|
var ScrubRefusalError = class ScrubRefusalError extends CdkdError {
|
|
65403
65661
|
exitCode = 2;
|
|
65404
65662
|
constructor(message, code) {
|
|
@@ -65443,6 +65701,140 @@ function scrubStacksFailedError(failures) {
|
|
|
65443
65701
|
return new ScrubRefusalError(`${failures.length} stack(s) could not be scrubbed: ${failures.map((f) => f.stackName).join(", ")}. Each one's reason was logged as it happened — see the 'Scrub of <stack> failed:' line above for it.`, "SCRUB_STACKS_FAILED");
|
|
65444
65702
|
}
|
|
65445
65703
|
/**
|
|
65704
|
+
* Which of `entries` this stack's post-scrub `outputs` bag disagrees with.
|
|
65705
|
+
*
|
|
65706
|
+
* OWNERSHIP is read off the entry (`producerStack` / `producerRegion`), never
|
|
65707
|
+
* inferred from the name: a pre-v9 state record makes `importableOutputKeys`
|
|
65708
|
+
* return every output key (`src/types/state.ts`), so a rebuild can key an entry
|
|
65709
|
+
* by a plain output name, and on a name collision the index keeps the latest
|
|
65710
|
+
* writer — so a name alone says nothing about whose entry it is.
|
|
65711
|
+
*
|
|
65712
|
+
* The rule is CONVERGENCE, not a match against scrub's plaintext map. The map
|
|
65713
|
+
* is built by LIVE RESOLUTION against Secrets Manager / SSM, so it holds the
|
|
65714
|
+
* secret's value AS IT IS NOW; an index entry written before a rotation holds
|
|
65715
|
+
* the value as it was THEN, and the two are different strings. Keying on
|
|
65716
|
+
* `state.outputs` instead means the rule reads the record a redeploy and
|
|
65717
|
+
* `ExportIndexStore.rebuild()` both write, and the plaintext map is not
|
|
65718
|
+
* consulted here at all.
|
|
65719
|
+
*
|
|
65720
|
+
* The lookup runs in ONE direction, and the inverse is not available: an
|
|
65721
|
+
* `Export.Name` alias is written into the outputs bag itself
|
|
65722
|
+
* (`deploy-engine.ts`, beside `resolvedExportNames.push(exportName)`), so an
|
|
65723
|
+
* index entry NAME is a key of that bag — while `ExportIndexEntry` carries no
|
|
65724
|
+
* output-key field, so nothing maps an entry back to the output it came from.
|
|
65725
|
+
*
|
|
65726
|
+
* WHAT CAN STILL GO WRONG. A name absent from `outputs` is reported and
|
|
65727
|
+
* nothing is written for it, so an entry in that position keeps its value —
|
|
65728
|
+
* `outputs` is `undefined` here whenever the stack has no state record at all,
|
|
65729
|
+
* which puts every one of its entries in that arm. An entry whose state value
|
|
65730
|
+
* carries no `{{resolve:` is left alone, so a plaintext an index entry holds
|
|
65731
|
+
* whose state counterpart is also plaintext is not reported by this pass and
|
|
65732
|
+
* not written by it. And `entries` is one region's index: an entry in another
|
|
65733
|
+
* region's `exports.json` is outside this call.
|
|
65734
|
+
*/
|
|
65735
|
+
function planExportIndexRepair(entries, stackName, producerRegion, outputs) {
|
|
65736
|
+
const examined = [];
|
|
65737
|
+
const findings = [];
|
|
65738
|
+
for (const [exportName, entry] of entries) {
|
|
65739
|
+
if (entry.producerStack !== stackName || entry.producerRegion !== producerRegion) continue;
|
|
65740
|
+
examined.push(exportName);
|
|
65741
|
+
if (outputs === void 0 || !Object.hasOwn(outputs, exportName)) {
|
|
65742
|
+
findings.push({
|
|
65743
|
+
kind: "absent",
|
|
65744
|
+
exportName
|
|
65745
|
+
});
|
|
65746
|
+
continue;
|
|
65747
|
+
}
|
|
65748
|
+
const stateValue = outputs[exportName];
|
|
65749
|
+
if (!carriesDynamicReference(stateValue)) continue;
|
|
65750
|
+
if (JSON.stringify(entry.value) === JSON.stringify(stateValue)) continue;
|
|
65751
|
+
findings.push({
|
|
65752
|
+
kind: "converge",
|
|
65753
|
+
exportName,
|
|
65754
|
+
stateValue
|
|
65755
|
+
});
|
|
65756
|
+
}
|
|
65757
|
+
return {
|
|
65758
|
+
examined,
|
|
65759
|
+
findings
|
|
65760
|
+
};
|
|
65761
|
+
}
|
|
65762
|
+
/**
|
|
65763
|
+
* The error a run ends with when `state.json` was rewritten and the exports
|
|
65764
|
+
* index was not (issue #2667).
|
|
65765
|
+
*
|
|
65766
|
+
* EXPLICIT rather than a warn: an entry this run went to converge and did not
|
|
65767
|
+
* keeps the value it held, in a region-wide object every principal with
|
|
65768
|
+
* `s3:GetObject` on the state bucket can read. `ExportIndexStore.patchEntry`
|
|
65769
|
+
* does not throw on a non-retryable S3 failure or an exhausted If-Match budget
|
|
65770
|
+
* — it logs and returns `false` — so without this the command would exit 0
|
|
65771
|
+
* over exactly that.
|
|
65772
|
+
*
|
|
65773
|
+
* Naming the remainder is what makes the re-run instruction actionable: the
|
|
65774
|
+
* convergence rule reads `state.outputs`, which a completed half already
|
|
65775
|
+
* matches, so a second invocation of the same command decides `converge` only
|
|
65776
|
+
* for the entries still differing.
|
|
65777
|
+
*/
|
|
65778
|
+
function exportIndexIncompleteError(unwritten, unreadable, dryRun) {
|
|
65779
|
+
const parts = [dryRun ? "no state written (--dry-run)" : "state.json complete"];
|
|
65780
|
+
if (unwritten.length > 0) parts.push(`${unwritten.length} exports index entr${unwritten.length === 1 ? "y" : "ies"} unwritten (${unwritten.map((u) => `${u.shown} in ${u.region}`).join(", ")})`);
|
|
65781
|
+
if (unreadable.length > 0) parts.push(`${unreadable.length} region(s) whose exports index could not be read (${unreadable.map((u) => `${u.region}: ${u.reason}`).join("; ")})`);
|
|
65782
|
+
return new ScrubRefusalError(`${parts.join("; ")}. Re-run the same command once the cause is cleared: each entry is written only while its value differs from the producer's state.outputs, so a re-run writes the remainder and leaves the rest alone.`, "SCRUB_EXPORT_INDEX_INCOMPLETE");
|
|
65783
|
+
}
|
|
65784
|
+
/**
|
|
65785
|
+
* Converge one scrubbed stack's entries in one region's exports index.
|
|
65786
|
+
*
|
|
65787
|
+
* Run as its OWN step by `scrubCommand`, after `scrubStack` returned — never
|
|
65788
|
+
* from inside the resolver. `scrubStack`'s resolve contexts deliberately
|
|
65789
|
+
* withhold `exportIndex` so `resolveImportValue`'s scan arm cannot `patchEntry`
|
|
65790
|
+
* as a side effect of resolution, at a point in the run nothing chose; that
|
|
65791
|
+
* withholding is unchanged, and this is a separate call with its own condition.
|
|
65792
|
+
*
|
|
65793
|
+
* Under `dryRun` no `patchEntry` is issued. The READ still happens:
|
|
65794
|
+
* `readPersistedEntries` is a GET that does not rebuild, so the audit half runs
|
|
65795
|
+
* without an S3 write.
|
|
65796
|
+
*
|
|
65797
|
+
* TWO PROPERTIES OF THE WRITE a reader will ask about. It runs OUTSIDE the
|
|
65798
|
+
* stack lock — `scrubStack` releases that in its own `finally` — because the
|
|
65799
|
+
* lock guards `state.json`, one key per stack, while `exports.json` is shared
|
|
65800
|
+
* region-wide and carries its own If-Match optimistic lock plus bounded retry;
|
|
65801
|
+
* that is the same concurrency control the deploy path's index write relies on.
|
|
65802
|
+
* And it is ONE PutObject PER ENTRY rather than one for the batch: `patchEntry`
|
|
65803
|
+
* is the operation that changes a value without touching membership, and a
|
|
65804
|
+
* remediation command pays that round trip so a failure names the entry it
|
|
65805
|
+
* could not write.
|
|
65806
|
+
*/
|
|
65807
|
+
async function repairExportIndexForStack(store, stackName, producerRegion, outputs, opts) {
|
|
65808
|
+
const entries = await store.readPersistedEntries();
|
|
65809
|
+
if (!entries) return {
|
|
65810
|
+
examined: [],
|
|
65811
|
+
findings: [],
|
|
65812
|
+
unwritten: []
|
|
65813
|
+
};
|
|
65814
|
+
const plan = planExportIndexRepair(entries, stackName, producerRegion, outputs);
|
|
65815
|
+
const unwritten = [];
|
|
65816
|
+
if (opts.dryRun) return {
|
|
65817
|
+
...plan,
|
|
65818
|
+
unwritten
|
|
65819
|
+
};
|
|
65820
|
+
for (const finding of plan.findings) {
|
|
65821
|
+
if (finding.kind !== "converge") continue;
|
|
65822
|
+
const entry = entries.get(finding.exportName);
|
|
65823
|
+
if (!entry) throw new ScrubIndexInvariantError("exports index repair: a planned entry is absent from the snapshot it was planned from");
|
|
65824
|
+
if (!await store.patchEntry(finding.exportName, {
|
|
65825
|
+
...entry,
|
|
65826
|
+
value: finding.stateValue
|
|
65827
|
+
}, { requireOwner: {
|
|
65828
|
+
producerStack: stackName,
|
|
65829
|
+
producerRegion
|
|
65830
|
+
} })) unwritten.push(finding.exportName);
|
|
65831
|
+
}
|
|
65832
|
+
return {
|
|
65833
|
+
...plan,
|
|
65834
|
+
unwritten
|
|
65835
|
+
};
|
|
65836
|
+
}
|
|
65837
|
+
/**
|
|
65446
65838
|
* `cdkd scrub` — rewrite persisted state so any resolved secret dynamic
|
|
65447
65839
|
* reference is stored as its UNRESOLVED expression rather than the plaintext
|
|
65448
65840
|
* value (GHSA fix). "Secret" here is whatever the RESOLVER classifies as one,
|
|
@@ -65462,10 +65854,14 @@ function scrubStacksFailedError(failures) {
|
|
|
65462
65854
|
* re-resolves each resource's properties to learn the resolved secret VALUES
|
|
65463
65855
|
* (recorded, never printed or re-persisted), and replaces those values in the
|
|
65464
65856
|
* state record's `properties` / `attributes` / `observedProperties` with the
|
|
65465
|
-
* expression. No AWS resource is created, updated, or deleted
|
|
65466
|
-
* is
|
|
65467
|
-
*
|
|
65468
|
-
*
|
|
65857
|
+
* expression. No AWS resource is created, updated, or deleted. What it WRITES
|
|
65858
|
+
* is the state bucket: each targeted stack's state.json, and — since issue
|
|
65859
|
+
* [#2667](https://github.com/go-to-k/cdkd/issues/2667) — the entries a scrubbed
|
|
65860
|
+
* stack publishes in the shared exports index at
|
|
65861
|
+
* `{prefix}/_index/{region}/exports.json`, as a separate step after that
|
|
65862
|
+
* stack's state.json write. This is why it is a top-level command and not
|
|
65863
|
+
* `cdkd state scrub` — the `cdkd state ...` family operates on the state bucket
|
|
65864
|
+
* alone and deliberately needs no CDK code.
|
|
65469
65865
|
*
|
|
65470
65866
|
* IMPORTANT: scrubbing does not un-expose an already-leaked secret. A value
|
|
65471
65867
|
* that was stored in plaintext should be treated as compromised and ROTATED at
|
|
@@ -65567,6 +65963,20 @@ async function scrubCommand(stacks, options) {
|
|
|
65567
65963
|
let totalStacksWithUnscrubbableKeys = 0;
|
|
65568
65964
|
let totalStacksWithUnverifiableReads = 0;
|
|
65569
65965
|
const failures = [];
|
|
65966
|
+
const exportIndexStores = /* @__PURE__ */ new Map();
|
|
65967
|
+
const exportIndexFor = (indexRegion) => {
|
|
65968
|
+
const existing = exportIndexStores.get(indexRegion);
|
|
65969
|
+
if (existing) return existing;
|
|
65970
|
+
const store = new ExportIndexStore(stateS3.s3, stateBucket, options.statePrefix, indexRegion, stateBackend);
|
|
65971
|
+
exportIndexStores.set(indexRegion, store);
|
|
65972
|
+
return store;
|
|
65973
|
+
};
|
|
65974
|
+
const examinedIndexNames = /* @__PURE__ */ new Map();
|
|
65975
|
+
let totalIndexEntriesConverged = 0;
|
|
65976
|
+
let totalIndexEntriesAbsent = 0;
|
|
65977
|
+
let totalIndexEntriesUnexamined = 0;
|
|
65978
|
+
const indexUnwritten = [];
|
|
65979
|
+
const indexUnreadable = [];
|
|
65570
65980
|
for (const stack of targetStacks) {
|
|
65571
65981
|
const stackRegion = stack.region || region;
|
|
65572
65982
|
let scrubbed;
|
|
@@ -65583,10 +65993,51 @@ async function scrubCommand(stacks, options) {
|
|
|
65583
65993
|
if (err instanceof Error && err.stack) logger.debug("Stack trace:", err.stack);
|
|
65584
65994
|
continue;
|
|
65585
65995
|
}
|
|
65996
|
+
let indexConverged = 0;
|
|
65997
|
+
try {
|
|
65998
|
+
const repair = await repairExportIndexForStack(exportIndexFor(stackRegion), stack.stackName, stackRegion, scrubbed.outputs, { dryRun: options.dryRun ?? false });
|
|
65999
|
+
const seen = examinedIndexNames.get(stackRegion) ?? /* @__PURE__ */ new Set();
|
|
66000
|
+
for (const name of repair.examined) seen.add(name);
|
|
66001
|
+
examinedIndexNames.set(stackRegion, seen);
|
|
66002
|
+
const named = (exportName) => {
|
|
66003
|
+
const shown = scrubbed.exportNameDisplay(exportName);
|
|
66004
|
+
switch (shown.kind) {
|
|
66005
|
+
case "safe": return `'${shown.text}'`;
|
|
66006
|
+
case "masked": return `(masked: "${shown.text}")`;
|
|
66007
|
+
case "withheld": return "(name withheld: it holds a secret this run recorded)";
|
|
66008
|
+
}
|
|
66009
|
+
};
|
|
66010
|
+
const unwrittenNames = new Set(repair.unwritten);
|
|
66011
|
+
for (const exportName of repair.unwritten) indexUnwritten.push({
|
|
66012
|
+
region: stackRegion,
|
|
66013
|
+
shown: named(exportName)
|
|
66014
|
+
});
|
|
66015
|
+
for (const finding of repair.findings) if (finding.kind === "converge") {
|
|
66016
|
+
indexConverged++;
|
|
66017
|
+
if (!options.dryRun && unwrittenNames.has(finding.exportName)) logger.warn(`Exports index entry ${named(finding.exportName)} (${stackRegion}) differs from ${stack.stackName}'s state.outputs and could NOT be written — it keeps the value it holds.`);
|
|
66018
|
+
else {
|
|
66019
|
+
totalIndexEntriesConverged++;
|
|
66020
|
+
logger.info(`${options.dryRun ? "Would converge" : "Converged"} exports index entry ${named(finding.exportName)} (${stackRegion}) to ${stack.stackName}'s state.outputs value.`);
|
|
66021
|
+
}
|
|
66022
|
+
} else {
|
|
66023
|
+
totalIndexEntriesAbsent++;
|
|
66024
|
+
logger.warn(`Exports index entry ${named(finding.exportName)} (${stackRegion}) is published by ${stack.stackName}, whose state.outputs has no key of that name — nothing was written for it and it keeps the value it holds. Redeploy ${stack.stackName} to rewrite the index from its own outputs.`);
|
|
66025
|
+
}
|
|
66026
|
+
} catch (err) {
|
|
66027
|
+
if (err instanceof ScrubIndexInvariantError) throw err;
|
|
66028
|
+
if (!indexUnreadable.some((u) => u.region === stackRegion)) {
|
|
66029
|
+
const reason = displaySafe(err instanceof Error ? err.message : String(err));
|
|
66030
|
+
indexUnreadable.push({
|
|
66031
|
+
region: stackRegion,
|
|
66032
|
+
reason
|
|
66033
|
+
});
|
|
66034
|
+
logger.error(`Exports index for ${stackRegion} could not be read (first seen while scrubbing ${stack.stackName}): ${reason}`);
|
|
66035
|
+
}
|
|
66036
|
+
}
|
|
65586
66037
|
if (scrubbed.recordsChanged > 0) {
|
|
65587
66038
|
totalStacksScrubbed++;
|
|
65588
66039
|
logger.info(`${options.dryRun ? "Would scrub" : "Scrubbed"} ${scrubbed.recordsChanged} resource record(s) in ${stack.stackName}`);
|
|
65589
|
-
} else if (scrubbed.secretBearingKeys === 0 && scrubbed.unverifiableReads === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
|
|
66040
|
+
} else if (scrubbed.secretBearingKeys === 0 && scrubbed.unverifiableReads === 0 && indexConverged === 0) logger.info(`No plaintext secrets found in ${stack.stackName}`);
|
|
65590
66041
|
if (scrubbed.unverifiableReads > 0) {
|
|
65591
66042
|
totalStacksWithUnverifiableReads++;
|
|
65592
66043
|
logger.warn(`${scrubbed.unverifiableReads} cross-stack read(s) in ${stack.stackName} could NOT be verified — cdkd declines them by design (see the warnings above), so this stack is not reported clean.`);
|
|
@@ -65596,7 +66047,28 @@ async function scrubCommand(stacks, options) {
|
|
|
65596
66047
|
logger.warn(`${scrubbed.secretBearingKeys} output KEY(s) in ${stack.stackName} hold plaintext and CANNOT be scrubbed — rename the Export.Name and redeploy (see the warning above).`);
|
|
65597
66048
|
}
|
|
65598
66049
|
}
|
|
65599
|
-
|
|
66050
|
+
for (const [indexRegion, store] of exportIndexStores) {
|
|
66051
|
+
if (indexUnreadable.some((u) => u.region === indexRegion)) continue;
|
|
66052
|
+
let entries;
|
|
66053
|
+
try {
|
|
66054
|
+
entries = await store.readPersistedEntries();
|
|
66055
|
+
} catch (err) {
|
|
66056
|
+
const reason = displaySafe(err instanceof Error ? err.message : String(err));
|
|
66057
|
+
indexUnreadable.push({
|
|
66058
|
+
region: indexRegion,
|
|
66059
|
+
reason
|
|
66060
|
+
});
|
|
66061
|
+
logger.error(`Exports index for ${indexRegion} could not be read for the coverage report: ${reason}`);
|
|
66062
|
+
continue;
|
|
66063
|
+
}
|
|
66064
|
+
if (!entries) continue;
|
|
66065
|
+
const examined = examinedIndexNames.get(indexRegion) ?? /* @__PURE__ */ new Set();
|
|
66066
|
+
const unexamined = [...entries.keys()].filter((name) => !examined.has(name));
|
|
66067
|
+
if (unexamined.length === 0) continue;
|
|
66068
|
+
totalIndexEntriesUnexamined += unexamined.length;
|
|
66069
|
+
logger.info(`Exports index (${indexRegion}): ${unexamined.length} entr${unexamined.length === 1 ? "y is" : "ies are"} published by a producer this run did not scrub, so nothing here read ${unexamined.length === 1 ? "its" : "their"} value. Coverage composes across the apps sharing this bucket and region — run each app's own cdkd scrub to reach its entries.`);
|
|
66070
|
+
}
|
|
66071
|
+
if (totalStacksScrubbed === 0 && totalStacksWithUnscrubbableKeys === 0 && totalStacksWithUnverifiableReads === 0 && totalIndexEntriesConverged === 0 && indexUnwritten.length === 0 && indexUnreadable.length === 0) {
|
|
65600
66072
|
if (failures.length === 0) {
|
|
65601
66073
|
logger.info("\nNo plaintext secrets found in any target stack state. Nothing to scrub.");
|
|
65602
66074
|
return;
|
|
@@ -65607,16 +66079,25 @@ async function scrubCommand(stacks, options) {
|
|
|
65607
66079
|
const failureNote = failures.length > 0 ? ` ${failures.length} stack(s) could NOT be scrubbed at all and are NOT covered by this summary — see the errors above.` : "";
|
|
65608
66080
|
const keyNote = totalStacksWithUnscrubbableKeys > 0 ? ` ${totalStacksWithUnscrubbableKeys} stack(s) hold plaintext in an output KEY, which cdkd scrub cannot rewrite — rename that output's Export.Name and redeploy.` : "";
|
|
65609
66081
|
const unverifiableNote = totalStacksWithUnverifiableReads > 0 ? ` ${totalStacksWithUnverifiableReads} stack(s) carry a cross-stack read cdkd declines to perform, so their imported values could NOT be checked — export a non-secret value (e.g. the secret's ARN) from the producer, or reference it from within its own account.` : "";
|
|
66082
|
+
const indexNote = (() => {
|
|
66083
|
+
const parts = [];
|
|
66084
|
+
if (totalIndexEntriesConverged > 0) parts.push(options.dryRun ? ` ${totalIndexEntriesConverged} exports index entr${totalIndexEntriesConverged === 1 ? "y" : "ies"} differ from the producer's scrubbed state.outputs and would be converged to it (--dry-run, nothing written).` : ` ${totalIndexEntriesConverged} exports index entr${totalIndexEntriesConverged === 1 ? "y" : "ies"} converged to the producer's state.outputs value. exports.json is written with the same PutObject state.json is, so on a VERSIONED bucket the pre-repair body survives as a noncurrent version and stays readable with GetObject and a VersionId.`);
|
|
66085
|
+
if (totalIndexEntriesAbsent > 0) parts.push(` ${totalIndexEntriesAbsent} exports index entr${totalIndexEntriesAbsent === 1 ? "y is" : "ies are"} published by a scrubbed stack under a name its state.outputs does not carry; nothing was written for ${totalIndexEntriesAbsent === 1 ? "it" : "them"} — redeploy that producer.`);
|
|
66086
|
+
if (totalIndexEntriesUnexamined > 0) parts.push(` ${totalIndexEntriesUnexamined} exports index entr${totalIndexEntriesUnexamined === 1 ? "y" : "ies"} belong to producers outside this run and were NOT examined.`);
|
|
66087
|
+
return parts.join("");
|
|
66088
|
+
})();
|
|
65610
66089
|
if (options.dryRun) {
|
|
65611
|
-
if (totalStacksScrubbed > 0) logger.info(`\nPlan: ${totalStacksScrubbed} stack(s) hold plaintext secrets and would be scrubbed (--dry-run, no state written).${keyNote}${unverifiableNote}${failureNote} ROTATE any exposed secret in Secrets Manager.`);
|
|
65612
|
-
else logger.info(`\nPlan:
|
|
66090
|
+
if (totalStacksScrubbed > 0) logger.info(`\nPlan: ${totalStacksScrubbed} stack(s) hold plaintext secrets and would be scrubbed (--dry-run, no state written).${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret in Secrets Manager.`);
|
|
66091
|
+
else logger.info(`\nPlan: no state record can be rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
65613
66092
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
66093
|
+
if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, true);
|
|
65614
66094
|
if (options.fail) throw new ScrubNeededError();
|
|
65615
66095
|
return;
|
|
65616
66096
|
}
|
|
65617
|
-
if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The CURRENT state.json no longer holds the plaintext. Where the state bucket is VERSIONED — which cdkd bootstrap enables — the pre-scrub body survives as a noncurrent version, stays readable with GetObject and a VersionId, and scrub does not purge it. So a value that was ever persisted must be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating); rotation is what makes any surviving version harmless.${keyNote}${unverifiableNote}${failureNote}`);
|
|
65618
|
-
else logger.info(`\
|
|
66097
|
+
if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The CURRENT state.json no longer holds the plaintext. Where the state bucket is VERSIONED — which cdkd bootstrap enables — the pre-scrub body survives as a noncurrent version, stays readable with GetObject and a VersionId, and scrub does not purge it. So a value that was ever persisted must be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating); rotation is what makes any surviving version harmless.${keyNote}${unverifiableNote}${indexNote}${failureNote}`);
|
|
66098
|
+
else logger.info(`\nNo state record was rewritten.${keyNote}${unverifiableNote}${indexNote}${failureNote} ROTATE any exposed secret.`);
|
|
65619
66099
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
66100
|
+
if (indexUnwritten.length > 0 || indexUnreadable.length > 0) throw exportIndexIncompleteError(indexUnwritten, indexUnreadable, false);
|
|
65620
66101
|
if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0)) throw new ScrubNeededError();
|
|
65621
66102
|
}
|
|
65622
66103
|
/**
|
|
@@ -67240,7 +67721,8 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67240
67721
|
recordsChanged: 0,
|
|
67241
67722
|
secretsFound: 0,
|
|
67242
67723
|
secretBearingKeys: 0,
|
|
67243
|
-
unverifiableReads: 0
|
|
67724
|
+
unverifiableReads: 0,
|
|
67725
|
+
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
67244
67726
|
};
|
|
67245
67727
|
}
|
|
67246
67728
|
const state = loaded.state;
|
|
@@ -67383,7 +67865,9 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67383
67865
|
recordsChanged: 0,
|
|
67384
67866
|
secretsFound: 0,
|
|
67385
67867
|
secretBearingKeys: secretBearingKeys.length,
|
|
67386
|
-
unverifiableReads: prePassFindings.unverifiable.length
|
|
67868
|
+
unverifiableReads: prePassFindings.unverifiable.length,
|
|
67869
|
+
outputs: state.outputs,
|
|
67870
|
+
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
67387
67871
|
};
|
|
67388
67872
|
let recordsChanged = 0;
|
|
67389
67873
|
const newResources = {};
|
|
@@ -67402,8 +67886,9 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67402
67886
|
const newOutputs = redactUnaccountedOutputs(outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsSourceUntrusted ? void 0 : outputsTemplateSource, TEMPLATE_SOURCED_RULES) : state.outputs, state.outputs, accountedOutputKeys, allRecordedSecrets(outputSecrets, perResourceSecrets));
|
|
67403
67887
|
if (JSON.stringify(newOutputs) !== JSON.stringify(state.outputs)) recordsChanged++;
|
|
67404
67888
|
if (recordsChanged > 0 && !opts.dryRun) {
|
|
67889
|
+
const { skippedOutputs: _droppedByScrub, ...carriedState } = state;
|
|
67405
67890
|
const nextState = {
|
|
67406
|
-
...
|
|
67891
|
+
...carriedState,
|
|
67407
67892
|
resources: newResources,
|
|
67408
67893
|
outputs: newOutputs,
|
|
67409
67894
|
lastModified: Date.now()
|
|
@@ -67414,7 +67899,9 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67414
67899
|
recordsChanged,
|
|
67415
67900
|
secretsFound: totalSecrets,
|
|
67416
67901
|
secretBearingKeys: secretBearingKeys.length,
|
|
67417
|
-
unverifiableReads: prePassFindings.unverifiable.length
|
|
67902
|
+
unverifiableReads: prePassFindings.unverifiable.length,
|
|
67903
|
+
outputs: newOutputs,
|
|
67904
|
+
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
67418
67905
|
};
|
|
67419
67906
|
} catch (err) {
|
|
67420
67907
|
throw maskSecretsInError(err, allRecordedSecrets(outputSecrets, perResourceSecrets));
|
|
@@ -69759,8 +70246,9 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
|
|
|
69759
70246
|
properties: redactedBaseline
|
|
69760
70247
|
};
|
|
69761
70248
|
}
|
|
70249
|
+
const { skippedOutputs: _droppedByAccept, ...carriedState } = report.state;
|
|
69762
70250
|
const newState = {
|
|
69763
|
-
...
|
|
70251
|
+
...carriedState,
|
|
69764
70252
|
resources,
|
|
69765
70253
|
lastModified: Date.now()
|
|
69766
70254
|
};
|
|
@@ -70580,8 +71068,9 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
70580
71068
|
};
|
|
70581
71069
|
}
|
|
70582
71070
|
if (recordedCount === 0) continue;
|
|
71071
|
+
const { skippedOutputs: _droppedByRevert, ...carriedState } = report.state;
|
|
70583
71072
|
const newState = {
|
|
70584
|
-
...
|
|
71073
|
+
...carriedState,
|
|
70585
71074
|
resources,
|
|
70586
71075
|
lastModified: Date.now()
|
|
70587
71076
|
};
|
|
@@ -76371,7 +76860,7 @@ async function stateRefreshObservedCommand(stackArgs, options) {
|
|
|
76371
76860
|
* Shared rather than constructed per resource because nothing writes to it:
|
|
76372
76861
|
* `redactSecretsForState` only reads its argument.
|
|
76373
76862
|
*/
|
|
76374
|
-
const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
|
|
76863
|
+
const NO_RECORDED_SECRETS$1 = /* @__PURE__ */ new Map();
|
|
76375
76864
|
/**
|
|
76376
76865
|
* Warn before `cdkd state orphan` force-releases a lock that is still LIVE.
|
|
76377
76866
|
*
|
|
@@ -76484,7 +76973,7 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
76484
76973
|
unsupported++;
|
|
76485
76974
|
return;
|
|
76486
76975
|
}
|
|
76487
|
-
resource.observedProperties = redactSecretsForState(observed, NO_RECORDED_SECRETS, resource.properties ?? {}, STATE_SOURCED_READBACK_RULES);
|
|
76976
|
+
resource.observedProperties = redactSecretsForState(observed, NO_RECORDED_SECRETS$1, resource.properties ?? {}, STATE_SOURCED_READBACK_RULES);
|
|
76488
76977
|
refreshed++;
|
|
76489
76978
|
} catch (err) {
|
|
76490
76979
|
failed++;
|
|
@@ -76494,6 +76983,7 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
76494
76983
|
await Promise.all(tasks);
|
|
76495
76984
|
});
|
|
76496
76985
|
state.lastModified = Date.now();
|
|
76986
|
+
if (refreshed > 0) delete state.skippedOutputs;
|
|
76497
76987
|
const saveOptions = { expectedEtag: etag };
|
|
76498
76988
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
76499
76989
|
await stateBackend.saveState(stackName, region, state, saveOptions);
|
|
@@ -76785,8 +77275,9 @@ async function rollbackCommand(stackArg, options) {
|
|
|
76785
77275
|
};
|
|
76786
77276
|
let currentEtag = stateData.etag;
|
|
76787
77277
|
const saveState = async () => {
|
|
77278
|
+
const { skippedOutputs: _droppedByRollback, ...carriedState } = baseState;
|
|
76788
77279
|
const next = () => ({
|
|
76789
|
-
...
|
|
77280
|
+
...carriedState,
|
|
76790
77281
|
version: 9,
|
|
76791
77282
|
region,
|
|
76792
77283
|
resources: { ...stateResources },
|
|
@@ -78830,9 +79321,10 @@ async function rewriteResourceReferences(state, orphanLogicalIds, providerRegist
|
|
|
78830
79321
|
}
|
|
78831
79322
|
const newOutputs = {};
|
|
78832
79323
|
for (const [name, value] of Object.entries(state.outputs ?? {})) newOutputs[name] = await rewriteValue(value, `outputs.${name}`, `<output:${name}>`, orphanSet, fetcher, rewrites, unresolvable);
|
|
79324
|
+
const { skippedOutputs: _droppedByOrphanRewrite, ...carriedState } = state;
|
|
78833
79325
|
return {
|
|
78834
79326
|
state: {
|
|
78835
|
-
...
|
|
79327
|
+
...carriedState,
|
|
78836
79328
|
resources: newResources,
|
|
78837
79329
|
outputs: newOutputs,
|
|
78838
79330
|
lastModified: Date.now()
|
|
@@ -79771,8 +80263,7 @@ async function importCommand(stackArg, options) {
|
|
|
79771
80263
|
}
|
|
79772
80264
|
}
|
|
79773
80265
|
const stackState = buildStackState(stackInfo.stackName, targetRegion, rows, templateParser, stateTemplate, existingState, selectiveMode);
|
|
79774
|
-
await resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger);
|
|
79775
|
-
await captureObservedForImportedResources(stackState, providerRegistry, logger);
|
|
80266
|
+
await captureObservedForImportedResources(stackState, providerRegistry, logger, await resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger));
|
|
79776
80267
|
const saveOptions = {};
|
|
79777
80268
|
if (existingEtag) saveOptions.expectedEtag = existingEtag;
|
|
79778
80269
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
@@ -80205,14 +80696,90 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
80205
80696
|
* already-stored, which on the v3 baseline is already resolved-shape
|
|
80206
80697
|
* from a prior import / deploy, so re-resolving is a no-op.
|
|
80207
80698
|
*
|
|
80699
|
+
* RETURNS the logical ids for which an `observedProperties` baseline must NOT
|
|
80700
|
+
* be captured, because this walk cannot vouch that the persisted `properties`
|
|
80701
|
+
* still SPELL the dynamic reference the template had.
|
|
80702
|
+
* `captureObservedForImportedResources` refuses to write a baseline for those
|
|
80703
|
+
* (issue [#2828](https://github.com/go-to-k/cdkd/issues/2828) review); the
|
|
80704
|
+
* redaction it performs is POSITION-based, so a bag that no longer spells the
|
|
80705
|
+
* reference gives it no evidence and the DECRYPTED readback is persisted in
|
|
80706
|
+
* the clear.
|
|
80707
|
+
*
|
|
80708
|
+
* The predicate is deliberately CONSERVATIVE — it over-refuses — and that is a
|
|
80709
|
+
* decision a review round paid for. Two arms:
|
|
80710
|
+
*
|
|
80711
|
+
* - **Resolution THREW** — refuse, unconditionally. A throw means the
|
|
80712
|
+
* resolver could not finish, so nothing about the persisted bag is
|
|
80713
|
+
* trustworthy evidence. This is BEHAVIOURALLY IDENTICAL to the arm this PR
|
|
80714
|
+
* first shipped: two attempts to sharpen it in between each shipped a leak
|
|
80715
|
+
* (the wrapped-complete-token shape, then the
|
|
80716
|
+
* reference-sourced-from-outside-the-bag shape), and saying so is the
|
|
80717
|
+
* strongest available form of "every sharpening leaked". The arm's own
|
|
80718
|
+
* comment carries both measurements.
|
|
80719
|
+
* - **Resolution SUCCEEDED but LOST an opener.** An `Fn::If` whose condition
|
|
80720
|
+
* was DOWNGRADED resolves without throwing and selects the branch AWS did
|
|
80721
|
+
* not take, so `properties` hold the false-branch literal and no reference
|
|
80722
|
+
* at all. The downgrade needs a parameter with NO `Default` — `cdkd import`
|
|
80723
|
+
* accepts no parameter VALUES from the user, but a `Default`-carrying
|
|
80724
|
+
* parameter IS bound (issue #2321's retry), so only an unbindable one leaves
|
|
80725
|
+
* `evaluateConditions` catching per condition and recording `false`.
|
|
80726
|
+
*
|
|
80727
|
+
* ARM 2 IS KNOWN OVER-BROAD for a PUBLIC `{{resolve:ssm:...}}` reference: a
|
|
80728
|
+
* `String` / `StringList` parameter is stored RESOLVED (issue #1901), so its
|
|
80729
|
+
* opener disappears across the resolve and the resource is refused although
|
|
80730
|
+
* nothing was ever secret, costing it a drift baseline on a common config
|
|
80731
|
+
* pattern. Threading the resolve's own `recordedSecretValues` into the
|
|
80732
|
+
* capture corrects it -- measured on issue
|
|
80733
|
+
* [#2852](https://github.com/go-to-k/cdkd/issues/2852), which carries that
|
|
80734
|
+
* remedy -- because a populated map makes absence from it real evidence of
|
|
80735
|
+
* a public parameter, exactly as the deploy path already relies on.
|
|
80736
|
+
*
|
|
80737
|
+
* The cost of a false refusal is one resource's drift baseline until its next
|
|
80738
|
+
* deploy; the cost of a false pass is a plaintext secret in `state.json`. So an
|
|
80739
|
+
* unmeasurable bag refuses too.
|
|
80740
|
+
*
|
|
80741
|
+
* WHAT CAN STILL GO WRONG. No claim is made here about what this closes, and
|
|
80742
|
+
* that is deliberate: successive review rounds falsified a closure claim, then
|
|
80743
|
+
* a closed-set list, then a "reduces" claim -- the mechanism survived every
|
|
80744
|
+
* round of measurement and the SENTENCES about it did not. What follows states only
|
|
80745
|
+
* the DANGER direction, which can become false only once the residue is gone.
|
|
80746
|
+
*
|
|
80747
|
+
* PLAINTEXT CAN STILL BE PERSISTED by this capture. Known ways, each with its
|
|
80748
|
+
* issue and none of them proven to be all of them:
|
|
80749
|
+
*
|
|
80750
|
+
* - the resolve drops a reference, or trades one away, in a manner the opener
|
|
80751
|
+
* comparison below cannot see --
|
|
80752
|
+
* [#2850](https://github.com/go-to-k/cdkd/issues/2850);
|
|
80753
|
+
* - a parameter binds to a placeholder `Default` while the DEPLOYED value was
|
|
80754
|
+
* the reference, so no opener exists in any template this walk is handed --
|
|
80755
|
+
* [#2854](https://github.com/go-to-k/cdkd/issues/2854);
|
|
80756
|
+
* - `redactSecretsForState` cannot PAIR the readback against the source and
|
|
80757
|
+
* returns the bag unchanged: a reshaped container, an identity key AWS
|
|
80758
|
+
* normalised, a readback key the source lacks, an array whose anchors do not
|
|
80759
|
+
* corroborate -- [#2852](https://github.com/go-to-k/cdkd/issues/2852), which
|
|
80760
|
+
* carries the measured probe table;
|
|
80761
|
+
* - the readback holds a secret with NO counterpart in the source at all, set
|
|
80762
|
+
* out of band -- [#2868](https://github.com/go-to-k/cdkd/issues/2868), filed
|
|
80763
|
+
* as a remit question rather than a defect in this mechanism.
|
|
80764
|
+
*
|
|
80765
|
+
* WHY NO EXHAUSTIVE LIST IS OFFERED, as mechanism rather than framing: this
|
|
80766
|
+
* redaction's only evidence is a POST-HOC comparison between the persisted bag
|
|
80767
|
+
* and the readback, so anything that comparison cannot see is trusted.
|
|
80768
|
+
* Establishing more needs PROVENANCE carried out of the resolver or the
|
|
80769
|
+
* deployer, which is where the structural remedy for all of the above lives.
|
|
80770
|
+
*
|
|
80771
|
+
* The set is returned rather than recorded on the state, because it describes
|
|
80772
|
+
* THIS run's resolution and nothing downstream of the save has any use for it.
|
|
80773
|
+
*
|
|
80208
80774
|
* Exported for unit testing — internal to the command flow otherwise. The
|
|
80209
80775
|
* masking this walk applies (issue #2803) is only provable against the REAL
|
|
80210
80776
|
* resolver, since the plaintext reaches the message through the resolver's
|
|
80211
80777
|
* own throw rather than through anything this function writes.
|
|
80212
80778
|
*/
|
|
80213
80779
|
async function resolveImportedProperties(stackState, template, region, stateBackend, logger) {
|
|
80780
|
+
const unsafeObservedBaselineLogicalIds = /* @__PURE__ */ new Set();
|
|
80214
80781
|
const entries = Object.entries(stackState.resources);
|
|
80215
|
-
if (entries.length === 0) return;
|
|
80782
|
+
if (entries.length === 0) return unsafeObservedBaselineLogicalIds;
|
|
80216
80783
|
const resolver = new IntrinsicFunctionResolver(region);
|
|
80217
80784
|
let parameters = {};
|
|
80218
80785
|
let conditions = {};
|
|
@@ -80247,8 +80814,9 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
80247
80814
|
};
|
|
80248
80815
|
for (const [logicalId, resource] of entries) {
|
|
80249
80816
|
const recordedSecretValues = /* @__PURE__ */ new Map();
|
|
80817
|
+
const unresolvedProperties = resource.properties ?? {};
|
|
80818
|
+
let threw = false;
|
|
80250
80819
|
try {
|
|
80251
|
-
const unresolvedProperties = resource.properties ?? {};
|
|
80252
80820
|
const resolved = await resolver.resolve(unresolvedProperties, {
|
|
80253
80821
|
...baseContext,
|
|
80254
80822
|
recordedSecretValues
|
|
@@ -80256,7 +80824,46 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
80256
80824
|
resource.properties = recordedSecretValues.size > 0 ? redactSecretsForState(resolved, recordedSecretValues, unresolvedProperties) : resolved;
|
|
80257
80825
|
} catch (err) {
|
|
80258
80826
|
logger.warn(`Failed to resolve intrinsics in Properties for imported resource '${logicalId}' (${resource.resourceType}): ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}. State will be written with the raw intrinsic shape, which may cause 'cdkd destroy' to fail on this resource — re-import once every referenced sibling is in state, or remove this resource via 'cdkd state orphan'.` + (unboundParameterNames.length > 0 ? ` This template also declares parameter(s) with no 'Default' that an import cannot bind (${unboundParameterNames.join(", ")}), and 'cdkd import' accepts no parameter values — if this property was built from one of those, re-importing a sibling will not change it: give the parameter a 'Default' in the template and re-import, or correct the recorded properties before the next 'cdkd deploy'.` : ""));
|
|
80827
|
+
threw = true;
|
|
80259
80828
|
}
|
|
80829
|
+
if (threw) {
|
|
80830
|
+
unsafeObservedBaselineLogicalIds.add(logicalId);
|
|
80831
|
+
continue;
|
|
80832
|
+
}
|
|
80833
|
+
const rawOpeners = countDynamicReferenceOpeners(unresolvedProperties);
|
|
80834
|
+
const persistedOpeners = countDynamicReferenceOpeners(resource.properties);
|
|
80835
|
+
if (rawOpeners === void 0 || persistedOpeners === void 0 || persistedOpeners < rawOpeners) unsafeObservedBaselineLogicalIds.add(logicalId);
|
|
80836
|
+
}
|
|
80837
|
+
return unsafeObservedBaselineLogicalIds;
|
|
80838
|
+
}
|
|
80839
|
+
/** The opener every dynamic reference starts with, secret-bearing or not. */
|
|
80840
|
+
const DYNAMIC_REFERENCE_OPENER = "{{resolve:";
|
|
80841
|
+
/**
|
|
80842
|
+
* How many dynamic-reference OPENERS a property bag carries, counted over its
|
|
80843
|
+
* JSON serialization rather than by walking string leaves — which is the whole
|
|
80844
|
+
* point on the RAW side: a token assembled by `Fn::Join` is SPLIT across array
|
|
80845
|
+
* elements, so `{{resolve:secretsmanager:` survives as a fragment that no scan
|
|
80846
|
+
* for a COMPLETE token would see. `secretValueFromJson` renders exactly that
|
|
80847
|
+
* shape, so the split form is the common one, not the exotic one.
|
|
80848
|
+
*
|
|
80849
|
+
* `undefined` when the bag cannot be serialized. Callers treat that as "cannot
|
|
80850
|
+
* vouch for this bag" rather than as zero — the only safe direction, since the
|
|
80851
|
+
* cost of a false refusal is a drift baseline and the cost of a false pass is a
|
|
80852
|
+
* plaintext secret in `state.json`.
|
|
80853
|
+
*/
|
|
80854
|
+
function countDynamicReferenceOpeners(bag) {
|
|
80855
|
+
let text;
|
|
80856
|
+
try {
|
|
80857
|
+
text = JSON.stringify(bag ?? {}) ?? "";
|
|
80858
|
+
} catch {
|
|
80859
|
+
return;
|
|
80860
|
+
}
|
|
80861
|
+
let count = 0;
|
|
80862
|
+
for (let from = 0;;) {
|
|
80863
|
+
const at = text.indexOf(DYNAMIC_REFERENCE_OPENER, from);
|
|
80864
|
+
if (at === -1) return count;
|
|
80865
|
+
count++;
|
|
80866
|
+
from = at + 10;
|
|
80260
80867
|
}
|
|
80261
80868
|
}
|
|
80262
80869
|
function printSummary(rows) {
|
|
@@ -80348,6 +80955,14 @@ function collectMultiple(value, previous) {
|
|
|
80348
80955
|
return [...previous ?? [], value];
|
|
80349
80956
|
}
|
|
80350
80957
|
/**
|
|
80958
|
+
* The empty secrets map the observed-capture redaction below passes. Shared
|
|
80959
|
+
* and module-level because `redactSecretsForState` only ever READS its map,
|
|
80960
|
+
* and because an empty one is not an oversight here but the POSITION-only
|
|
80961
|
+
* configuration the call site's note argues for — the same constant
|
|
80962
|
+
* `cdkd state refresh-observed` passes for the same reason.
|
|
80963
|
+
*/
|
|
80964
|
+
const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
|
|
80965
|
+
/**
|
|
80351
80966
|
* Populate `observedProperties` for every resource in a freshly-built
|
|
80352
80967
|
* import StackState by calling the matching provider's
|
|
80353
80968
|
* `readCurrentState`. Mirrors what `cdkd deploy` does after each
|
|
@@ -80366,11 +80981,25 @@ function collectMultiple(value, previous) {
|
|
|
80366
80981
|
* (incremental rollout — see `ResourceProvider.readCurrentState`'s
|
|
80367
80982
|
* doc-comment) keep `observedProperties: undefined`; the drift comparator
|
|
80368
80983
|
* falls back to `properties` for those, matching pre-v3 behavior.
|
|
80984
|
+
*
|
|
80985
|
+
* Exported for unit testing -- internal to the command flow otherwise. The
|
|
80986
|
+
* input-space matrix drives THIS function rather than replaying its
|
|
80987
|
+
* `redactSecretsForState` call by hand, so a change to the arguments it passes
|
|
80988
|
+
* cannot leave that fence green.
|
|
80989
|
+
*
|
|
80990
|
+
* Every captured bag is REDACTED before it lands on the record, and a resource
|
|
80991
|
+
* whose redaction POSITION SOURCE is unusable is skipped entirely — see the
|
|
80992
|
+
* call site's own note (issue
|
|
80993
|
+
* [#2828](https://github.com/go-to-k/cdkd/issues/2828)).
|
|
80369
80994
|
*/
|
|
80370
|
-
async function captureObservedForImportedResources(stackState, providerRegistry, logger) {
|
|
80995
|
+
async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds) {
|
|
80371
80996
|
const entries = Object.entries(stackState.resources);
|
|
80372
80997
|
if (entries.length === 0) return;
|
|
80373
80998
|
await Promise.all(entries.map(async ([logicalId, resource]) => {
|
|
80999
|
+
if (unsafeObservedBaselineLogicalIds.has(logicalId)) {
|
|
81000
|
+
logger.debug(`observedProperties capture SKIPPED for imported ${logicalId} (${resource.resourceType}): the recorded properties no longer spell the template's dynamic reference, so they cannot position a redaction — capturing an AWS readback against them could persist a resolved secret in plaintext. Drift will compare against the recorded properties for this resource until the next successful deploy.`);
|
|
81001
|
+
return;
|
|
81002
|
+
}
|
|
80374
81003
|
try {
|
|
80375
81004
|
const provider = providerRegistry.getProviderFor({
|
|
80376
81005
|
resourceType: resource.resourceType,
|
|
@@ -80378,9 +81007,9 @@ async function captureObservedForImportedResources(stackState, providerRegistry,
|
|
|
80378
81007
|
}).provider;
|
|
80379
81008
|
if (!provider.readCurrentState) return;
|
|
80380
81009
|
const observed = await provider.readCurrentState(resource.physicalId, logicalId, resource.resourceType, resource.properties ?? {}, buildReadCurrentStateContext(stackState, logicalId));
|
|
80381
|
-
if (observed !== void 0) resource.observedProperties = observed;
|
|
81010
|
+
if (observed !== void 0) resource.observedProperties = redactSecretsForState(observed, NO_RECORDED_SECRETS, resource.properties ?? {}, STATE_SOURCED_READBACK_RULES);
|
|
80382
81011
|
} catch (err) {
|
|
80383
|
-
logger.debug(`observedProperties capture for imported ${logicalId} (${resource.resourceType}) failed: ${err instanceof Error ? err.
|
|
81012
|
+
logger.debug(`observedProperties capture for imported ${logicalId} (${resource.resourceType}) failed: ${err instanceof Error ? err.name : typeof err} — drift will fall back to template properties for this resource until the next successful deploy.`);
|
|
80384
81013
|
}
|
|
80385
81014
|
}));
|
|
80386
81015
|
}
|
|
@@ -80550,8 +81179,7 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
80550
81179
|
childStackState.parentStack = parentStackName;
|
|
80551
81180
|
childStackState.parentLogicalId = childLogicalId;
|
|
80552
81181
|
childStackState.parentRegion = parentRegion;
|
|
80553
|
-
await resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger);
|
|
80554
|
-
await captureObservedForImportedResources(childStackState, providerRegistry, logger);
|
|
81182
|
+
await captureObservedForImportedResources(childStackState, providerRegistry, logger, await resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger));
|
|
80555
81183
|
await stateBackend.saveState(childStackName, childRegion, childStackState);
|
|
80556
81184
|
logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
|
|
80557
81185
|
if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
|
|
@@ -89782,4 +90410,4 @@ function buildProgram() {
|
|
|
89782
90410
|
|
|
89783
90411
|
//#endregion
|
|
89784
90412
|
export { buildProgram };
|
|
89785
|
-
//# sourceMappingURL=program-
|
|
90413
|
+
//# sourceMappingURL=program-BXH3FeeV.js.map
|