@go-to-k/cdkd 0.288.2 → 0.288.4
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-TexsJufw.js} +2 -2
- package/dist/{asg-provider-DUlRn84u.js.map → asg-provider-TexsJufw.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-Cxt3gn8N.js → deploy-engine-BiPxTgKT.js} +988 -47
- package/dist/deploy-engine-BiPxTgKT.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-Ci4Y5UO9.js} +788 -153
- package/dist/{program-BRon74Fc.js.map → program-Ci4Y5UO9.js.map} +1 -1
- package/dist/{version-BRByznpy.js → version-BmM4QJ9t.js} +2 -2
- package/dist/{version-BRByznpy.js.map → version-BmM4QJ9t.js.map} +1 -1
- package/package.json +3 -13
- package/dist/deploy-engine-Cxt3gn8N.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { _ as withStackName, d as applyDefaultNameForFallback, f as generateResourceName, h as looksLikeCdkdGeneratedName, m as getCurrentStackName, n as getLogger, p as generateResourceNameWithFallback, r as isStdoutReservedForPayload, s as getLiveRenderer } from "./logger-Dw-3y48G.js";
|
|
2
2
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
3
|
-
import { t as getCdkdVersion } from "./version-
|
|
3
|
+
import { t as getCdkdVersion } from "./version-BmM4QJ9t.js";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { CreateBucketCommand, DeleteObjectCommand, DeleteObjectsCommand, GetBucketLocationCommand, GetBucketReplicationCommand, GetObjectCommand, HeadBucketCommand, HeadObjectCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchKey, PutBucketEncryptionCommand, PutBucketPolicyCommand, PutObjectCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
7
7
|
import { CloudControlClient, CreateResourceCommand, DeleteResourceCommand, GetResourceCommand, GetResourceRequestStatusCommand, ListResourcesCommand, UpdateResourceCommand } from "@aws-sdk/client-cloudcontrol";
|
|
8
8
|
import { AttachRolePolicyCommand, CreateRoleCommand, DeleteRoleCommand, DeleteRolePermissionsBoundaryCommand, DeleteRolePolicyCommand, DetachRolePolicyCommand, GetRoleCommand, GetRolePolicyCommand, IAMClient, ListAttachedRolePoliciesCommand, ListInstanceProfilesForRoleCommand, ListRolePoliciesCommand, ListRoleTagsCommand, NoSuchEntityException, PutRolePermissionsBoundaryCommand, PutRolePolicyCommand, RemoveRoleFromInstanceProfileCommand, TagRoleCommand, UntagRoleCommand, UpdateAssumeRolePolicyCommand, UpdateRoleCommand } from "@aws-sdk/client-iam";
|
|
@@ -4017,6 +4017,18 @@ async function expectedOwnerParam(client) {
|
|
|
4017
4017
|
* A caller whose value has a KNOWN ASCII charset (a stack name, an AWS region)
|
|
4018
4018
|
* should pass `asciiOnly`, which is a positive allowlist and therefore has no
|
|
4019
4019
|
* such residual at all.
|
|
4020
|
+
*
|
|
4021
|
+
* ONE CALLER DELIBERATELY GOES WIDER, and it is recorded here so an editor of
|
|
4022
|
+
* the residual note above knows a second module now disagrees with it.
|
|
4023
|
+
* `src/deployment/outputs-export-alias.ts` deletes a class derived from
|
|
4024
|
+
* `\p{Cc}` / `\p{Cf}` / `\p{Zl}` / `\p{Zp}` /
|
|
4025
|
+
* `\p{Default_Ignorable_Code_Point}`, because on THAT path the subject is a possibly
|
|
4026
|
+
* secret-bearing name in an operator's log: a plaintext split by a zero-width
|
|
4027
|
+
* character is READ as if it were contiguous, so it is disclosed without any
|
|
4028
|
+
* paste, and the command-forgery reasoning above does not transfer (issue
|
|
4029
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)). Nothing here changes
|
|
4030
|
+
* -- widening this helper would alter every caller that merely wants a
|
|
4031
|
+
* terminal-safe string.
|
|
4020
4032
|
*/
|
|
4021
4033
|
function displaySafe(value, opts) {
|
|
4022
4034
|
if (value === void 0 || value === null) return "";
|
|
@@ -9101,6 +9113,18 @@ function importableOutputs(state) {
|
|
|
9101
9113
|
function exportNamesCarriedFrom(previous) {
|
|
9102
9114
|
return previous.exportNames === void 0 ? {} : { exportNames: previous.exportNames };
|
|
9103
9115
|
}
|
|
9116
|
+
/**
|
|
9117
|
+
* The `skippedOutputs` field to write when a save carries the PREVIOUS
|
|
9118
|
+
* record's `outputs` bag forward instead of re-resolving it (issue #2740). The
|
|
9119
|
+
* record describes that bag — which keys the deploy that wrote it could not
|
|
9120
|
+
* resolve — so it travels with the bag, exactly like {@link
|
|
9121
|
+
* exportNamesCarriedFrom}: absent stays absent, present stays as it was. A
|
|
9122
|
+
* writer that re-resolves outputs does NOT call this; it writes the set the
|
|
9123
|
+
* resolution produced, or omits the field when nothing was skipped.
|
|
9124
|
+
*/
|
|
9125
|
+
function skippedOutputsCarriedFrom(previous) {
|
|
9126
|
+
return previous.skippedOutputs === void 0 ? {} : { skippedOutputs: previous.skippedOutputs };
|
|
9127
|
+
}
|
|
9104
9128
|
|
|
9105
9129
|
//#endregion
|
|
9106
9130
|
//#region src/types/rollback-journal.ts
|
|
@@ -16687,6 +16711,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
16687
16711
|
"RoleArn"
|
|
16688
16712
|
]),
|
|
16689
16713
|
silentDrop: /* @__PURE__ */ new Map([
|
|
16714
|
+
["CapacityProviderConfiguration", "not yet implemented by cdkd"],
|
|
16690
16715
|
["FilesystemConfigurations", "not yet implemented by cdkd"],
|
|
16691
16716
|
["RequestHeaderConfiguration", "not yet implemented by cdkd"],
|
|
16692
16717
|
["Tags", "not yet implemented by cdkd"]
|
|
@@ -16805,7 +16830,8 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
16805
16830
|
silentDrop: /* @__PURE__ */ new Map([
|
|
16806
16831
|
["EvaluationCriteria", "Absent from both the SDK PutMetricAlarm input and the aws-cdk-lib CfnAlarm L1 (a newer CFn-schema-only property ahead of SDK/CDK support); no wire path to forward it and no CDK app can emit it."],
|
|
16807
16832
|
["EvaluationInterval", "Absent from both the SDK PutMetricAlarm input and the aws-cdk-lib CfnAlarm L1 (a newer CFn-schema-only property ahead of SDK/CDK support); no wire path to forward it and no CDK app can emit it."],
|
|
16808
|
-
["EvaluationWindow", "not yet implemented by cdkd"]
|
|
16833
|
+
["EvaluationWindow", "not yet implemented by cdkd"],
|
|
16834
|
+
["WarmUpConfiguration", "not yet implemented by cdkd"]
|
|
16809
16835
|
]),
|
|
16810
16836
|
createOnlyDrops: /* @__PURE__ */ new Set()
|
|
16811
16837
|
}],
|
|
@@ -17633,7 +17659,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
17633
17659
|
}],
|
|
17634
17660
|
["AWS::Glue::Connection", {
|
|
17635
17661
|
handled: /* @__PURE__ */ new Set(["CatalogId", "ConnectionInput"]),
|
|
17636
|
-
silentDrop: /* @__PURE__ */ new Map(),
|
|
17662
|
+
silentDrop: /* @__PURE__ */ new Map([["Tags", "not yet implemented by cdkd"]]),
|
|
17637
17663
|
createOnlyDrops: /* @__PURE__ */ new Set()
|
|
17638
17664
|
}],
|
|
17639
17665
|
["AWS::Glue::Crawler", {
|
|
@@ -18197,7 +18223,6 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
18197
18223
|
["UseLatestRestorableTime", "not yet implemented by cdkd"]
|
|
18198
18224
|
]),
|
|
18199
18225
|
createOnlyDrops: /* @__PURE__ */ new Set([
|
|
18200
|
-
"AvailabilityZones",
|
|
18201
18226
|
"ClusterScalabilityType",
|
|
18202
18227
|
"DBSystemId",
|
|
18203
18228
|
"EngineMode",
|
|
@@ -19897,6 +19922,113 @@ function s3BucketWebsiteUrl(bucketName, region) {
|
|
|
19897
19922
|
return `http://${bucketName}.s3-website${S3_WEBSITE_ENDPOINT_LEGACY_DASH_REGIONS.has(folded) ? "-" : "."}${folded}.${urlSuffix}`;
|
|
19898
19923
|
}
|
|
19899
19924
|
|
|
19925
|
+
//#endregion
|
|
19926
|
+
//#region src/deployment/drain-budget.ts
|
|
19927
|
+
/**
|
|
19928
|
+
* The drain BUDGET for one intrinsic resolution (issue
|
|
19929
|
+
* [#2563](https://github.com/go-to-k/cdkd/issues/2563)).
|
|
19930
|
+
*
|
|
19931
|
+
* Its own module rather than a corner of `intrinsic-function-resolver.ts`,
|
|
19932
|
+
* and that is forced rather than tidy: 70+ unit files `vi.mock` the resolver
|
|
19933
|
+
* module, and the ones that REPLACE its exports rather than spreading
|
|
19934
|
+
* `...actual` see no new export -- so an opener declared there, whether as a
|
|
19935
|
+
* resolver method or as a module-level function, reddens most of
|
|
19936
|
+
* `tests/unit/deployment` the moment `deploy-engine.ts` imports it (measured,
|
|
19937
|
+
* both ways). A module the engine and the resolver both import, and nobody
|
|
19938
|
+
* mocks, is the shape that works.
|
|
19939
|
+
*/
|
|
19940
|
+
/**
|
|
19941
|
+
* Named `drainDeadlines` from when it held one, and kept because the name is
|
|
19942
|
+
* cited from several comments and a rename is churn this issue does not need.
|
|
19943
|
+
* It holds a {@link DrainBudget} -- remaining WAIT, not an instant.
|
|
19944
|
+
*/
|
|
19945
|
+
const drainDeadlines = new AsyncLocalStorage();
|
|
19946
|
+
/**
|
|
19947
|
+
* Run `fn` under ONE drain budget (issue #2563), inheriting the caller's if
|
|
19948
|
+
* there is one.
|
|
19949
|
+
*
|
|
19950
|
+
* A module-level function rather than a resolver METHOD, deliberately: the
|
|
19951
|
+
* engine's callers mock the resolver wholesale, and a new method on that
|
|
19952
|
+
* surface would make every such mock throw before it resolved anything
|
|
19953
|
+
* (measured -- most of `tests/unit/deployment` red).
|
|
19954
|
+
*
|
|
19955
|
+
* Why a caller needs this. `resolve` opens a budget per CALL, and a caller
|
|
19956
|
+
* that resolves in a LOOP therefore gets one cap per iteration: the outputs
|
|
19957
|
+
* pass walks `template.Outputs` sequentially, so the DRAIN WAIT it could
|
|
19958
|
+
* spend before `saveState`, with the S3 lock held, was `#outputs x` the cap
|
|
19959
|
+
* rather than the cap. CloudFormation allows 200 outputs. Wrapping the loop
|
|
19960
|
+
* here bounds the total drain WAIT under it at one budget.
|
|
19961
|
+
*
|
|
19962
|
+
* The budget is REMAINING milliseconds of wait, not a deadline, and that
|
|
19963
|
+
* distinction is the whole of it: an absolute deadline is spent by wall
|
|
19964
|
+
* clock, so ordinary resolution time between drains burns it although
|
|
19965
|
+
* nothing drained -- an output failing with a 5 ms sibling would leave a
|
|
19966
|
+
* later one with zero grace after 60 s of clean AWS work. Charged only while
|
|
19967
|
+
* a drain is actually waiting, and only once for nested drains whose waits
|
|
19968
|
+
* overlap, an iteration that spent nothing keeps its full grace.
|
|
19969
|
+
*
|
|
19970
|
+
* WHAT IT DOES NOT BOUND, since the cap is not a deadline on the pass: the
|
|
19971
|
+
* cap is armed by a REJECTION and bounds only the extra wait a drain takes
|
|
19972
|
+
* after one. Ordinary resolution time is outside it entirely -- a lookup
|
|
19973
|
+
* that hangs with no sibling rejection anywhere is unbounded here exactly
|
|
19974
|
+
* as it was before this issue, and `withResourceDeadline` does not reach
|
|
19975
|
+
* the outputs pass. So this wrap bounds aggregate drain grace, not the
|
|
19976
|
+
* hold.
|
|
19977
|
+
*
|
|
19978
|
+
* THE TRADE, stated because it is real: iterations that actually WAIT spend
|
|
19979
|
+
* the shared budget, so a later one can find it exhausted and its drain get
|
|
19980
|
+
* no grace. That is the same exposure issue
|
|
19981
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814) records for nesting,
|
|
19982
|
+
* now reachable across a loop as well -- and on the `Export.Name` leg the
|
|
19983
|
+
* cost is a DROPPED write rather than a late one, since that block's
|
|
19984
|
+
* `nameSecrets` is a per-iteration local. What bounds it is that only real
|
|
19985
|
+
* waiting spends the budget: reaching zero takes a full cap of drain wait
|
|
19986
|
+
* inside one pass, not merely a slow pass. It is taken deliberately -- an
|
|
19987
|
+
* unbounded hold on a deploy's state save costs a first deploy every
|
|
19988
|
+
* resource it just created -- and the caller chooses, since a loop that
|
|
19989
|
+
* would rather buy grace per iteration simply does not wrap.
|
|
19990
|
+
*
|
|
19991
|
+
* `evaluateConditions` deliberately does NOT wrap its loop: a failed
|
|
19992
|
+
* condition is downgraded and evaluation continues, it runs before any
|
|
19993
|
+
* resource is provisioned, and one condition's slow parts should
|
|
19994
|
+
* not spend the next one's budget. Its aggregate is `#conditions x` the cap,
|
|
19995
|
+
* with the deploy lock already held -- less severe than the outputs pass,
|
|
19996
|
+
* where the state save is the thing waiting, but not lock-free.
|
|
19997
|
+
*
|
|
19998
|
+
* The other resolve LOOPS in the tree, assessed rather than assumed -- and
|
|
19999
|
+
* the first version of this note got two of them wrong, so each is stated
|
|
20000
|
+
* with what was checked:
|
|
20001
|
+
*
|
|
20002
|
+
* - `cdkd scrub` (`scrub.ts`): lock acquired above, `saveState` downstream,
|
|
20003
|
+
* and its resolve loops -- the resources loop, two output loops, and
|
|
20004
|
+
* `resolveCrossStackReads`, a per-leaf loop the other three each invoke.
|
|
20005
|
+
* One budget is hoisted over all of them. That wrap is INLINE rather than
|
|
20006
|
+
* around a callee, so the callee-keyed table in
|
|
20007
|
+
* `intrinsic-resolver-concurrent-drain.test.ts` cannot see it; an
|
|
20008
|
+
* owner-keyed case in that same file fences it instead.
|
|
20009
|
+
* - `cdkd import` (`import.ts`): `acquireLock` at the root and per child,
|
|
20010
|
+
* `resolveImportedProperties` loops the resources, `saveState` follows.
|
|
20011
|
+
* WRAPPED at both call sites.
|
|
20012
|
+
* - `cdkd export` (`export.ts`): child locks acquired before
|
|
20013
|
+
* `buildResolvedParametersPerStack`, which loops `resolve` over the
|
|
20014
|
+
* intrinsic parameters. WRAPPED.
|
|
20015
|
+
* - `diff-recursive.ts`: loops `resolve` over a parent's parameters with no
|
|
20016
|
+
* lock and no state write -- a read-only diff. LEFT per-call, since a
|
|
20017
|
+
* shared budget would buy no safety and would spend one resolution's
|
|
20018
|
+
* grace on the next.
|
|
20019
|
+
*
|
|
20020
|
+
* The drain is what issue #2563 adds, so the wait in all four is new and
|
|
20021
|
+
* bounding it where it can hold something is part of adding it.
|
|
20022
|
+
*/
|
|
20023
|
+
async function withSharedDrainBudget(fn) {
|
|
20024
|
+
if (drainDeadlines.getStore() !== void 0) return await fn();
|
|
20025
|
+
return await drainDeadlines.run({
|
|
20026
|
+
remaining: void 0,
|
|
20027
|
+
waiting: 0,
|
|
20028
|
+
since: void 0
|
|
20029
|
+
}, fn);
|
|
20030
|
+
}
|
|
20031
|
+
|
|
19900
20032
|
//#endregion
|
|
19901
20033
|
//#region src/deployment/secret-region-classification.ts
|
|
19902
20034
|
/**
|
|
@@ -22120,6 +22252,170 @@ const MAX_LISTED_AVAILABLE_OUTPUTS = 10;
|
|
|
22120
22252
|
*/
|
|
22121
22253
|
const dynamicReferenceRetryDelays = {};
|
|
22122
22254
|
/**
|
|
22255
|
+
* How long {@link allSettledKeepingFirstRejection} waits for the remaining
|
|
22256
|
+
* parts AFTER a rejection is in hand. Double the largest FIXED wait in this
|
|
22257
|
+
* file (the `Fn::GetAtt` `Ipv6CidrBlocks` poll's sleep budget, 15 attempts
|
|
22258
|
+
* x 2 s), which makes it a hang guard rather than a schedule — not a
|
|
22259
|
+
* guarantee that healthy work fits inside it; see the function's own note.
|
|
22260
|
+
*
|
|
22261
|
+
* It is the budget for one CALL of {@link IntrinsicFunctionResolver.resolve}
|
|
22262
|
+
* and everything nested under it: nested drains share the REMAINING wait, so
|
|
22263
|
+
* a template's nesting depth cannot multiply it. It is NOT a bound on a caller
|
|
22264
|
+
* that resolves in a LOOP -- each iteration opens its own budget unless the
|
|
22265
|
+
* caller wraps the loop in {@link withSharedDrainBudget}, which the outputs
|
|
22266
|
+
* pass does and `evaluateConditions` deliberately does not (its aggregate is
|
|
22267
|
+
* `#conditions x` this, before any resource is provisioned but with the
|
|
22268
|
+
* deploy lock already held).
|
|
22269
|
+
*/
|
|
22270
|
+
const DRAIN_AFTER_REJECTION_MS = 6e4;
|
|
22271
|
+
/** Sentinel for "the cap expired", distinguishable from any resolved value. */
|
|
22272
|
+
const CAP_EXPIRED = Symbol("drain-cap-expired");
|
|
22273
|
+
/**
|
|
22274
|
+
* Test seam: overriding `ms` lets a unit test drive the cap without a real
|
|
22275
|
+
* minute of waiting (mirrors {@link dynamicReferenceRetryDelays}).
|
|
22276
|
+
*/
|
|
22277
|
+
const concurrentDrainCap = {};
|
|
22278
|
+
/**
|
|
22279
|
+
* `Promise.all`'s RESULT and its choice of error, with `Promise.allSettled`'s
|
|
22280
|
+
* TIMING: every promise started here has settled before this returns, and
|
|
22281
|
+
* before it throws unless the cap below expires first (issue
|
|
22282
|
+
* [#2563](https://github.com/go-to-k/cdkd/issues/2563)).
|
|
22283
|
+
*
|
|
22284
|
+
* Why the resolver needs that. Resolving a secret dynamic reference RECORDS
|
|
22285
|
+
* `plaintext -> expression` into `context.recordedSecretValues` just before
|
|
22286
|
+
* its promise settles, and that map is what every masking and redaction site
|
|
22287
|
+
* downstream uses as its needle set. Under a bare `Promise.all` a rejecting
|
|
22288
|
+
* part surfaces IMMEDIATELY, so a caller's `catch` / `finally` can run while a
|
|
22289
|
+
* sibling part is still in flight: `DeployEngine`'s `Export.Name` block copies
|
|
22290
|
+
* its private map into the pass map in exactly such a `finally`, and the
|
|
22291
|
+
* sibling's recording then lands in the private map after the copy and reaches
|
|
22292
|
+
* nothing. Draining here fixes it for every caller at once, which a
|
|
22293
|
+
* consumer-side drain cannot — `cdkd scrub`'s shared-map view (issue
|
|
22294
|
+
* [#2531](https://github.com/go-to-k/cdkd/issues/2531)) lets a late write land
|
|
22295
|
+
* whenever it happens but still cannot make it land before the next consumer
|
|
22296
|
+
* runs.
|
|
22297
|
+
*
|
|
22298
|
+
* THE ERROR IS SELECTED BY TIME, NOT BY INPUT ORDER, which is what `Promise.all`
|
|
22299
|
+
* does and what a naive `Promise.allSettled` + "first rejected entry" would
|
|
22300
|
+
* silently change: with two parts rejecting out of input order, the entry scan
|
|
22301
|
+
* reports the LATER one. Each promise gets its own `catch`, so the callbacks
|
|
22302
|
+
* fire in rejection order and the first assignment wins.
|
|
22303
|
+
*
|
|
22304
|
+
* Every input is `catch`-ed, so nothing here can raise an unhandled rejection
|
|
22305
|
+
* while the drain waits.
|
|
22306
|
+
*
|
|
22307
|
+
* THE WAIT IS CAPPED ONCE A REJECTION IS IN HAND. The drain exists to let a
|
|
22308
|
+
* sibling finish RECORDING, and that is worth a wait — but `resolveOutputs`
|
|
22309
|
+
* runs at `deploy-engine.ts`'s worst moment: after every resource has been
|
|
22310
|
+
* created in AWS, before the final `saveState`, with the S3 lock held and its
|
|
22311
|
+
* heartbeat pushing `expiresAt` forward. An unbounded wait there costs a deploy
|
|
22312
|
+
* its state and its lock, and on a FIRST deploy (`currentEtag` undefined, so
|
|
22313
|
+
* the incremental saves were no-ops) every created resource becomes invisible
|
|
22314
|
+
* to cdkd. Nothing else bounds it: `withRetry` caps ATTEMPTS not duration, no
|
|
22315
|
+
* `requestTimeout` is configured, and `withResourceDeadline` wraps
|
|
22316
|
+
* `provisionResourceBody` — which does bound the resource path, property
|
|
22317
|
+
* resolution included, but not the outputs pass. So once a rejection is
|
|
22318
|
+
* recorded the remaining settles race {@link DRAIN_AFTER_REJECTION_MS}, and the
|
|
22319
|
+
* recorded rejection is thrown when it expires.
|
|
22320
|
+
*
|
|
22321
|
+
* The cap is a HANG GUARD, and it does not claim to be more. It is sized
|
|
22322
|
+
* against the largest fixed wait in this file — the `Fn::GetAtt`
|
|
22323
|
+
* `Ipv6CidrBlocks` poll's sleep budget, 15 attempts x 2 s, about 30 s — but
|
|
22324
|
+
* that budget is a floor, not a ceiling: the poll also awaits 15 AWS calls,
|
|
22325
|
+
* and one part can drive several lookups in sequence through object
|
|
22326
|
+
* properties or `Fn::Sub` variables. Two healthy shapes measured on review
|
|
22327
|
+
* already exceed 60 s — three sequential `Ipv6CidrBlocks` polls in one part
|
|
22328
|
+
* is 3 x (15 x 2 s) = 90 s with no hang and no throttling, and five throttled
|
|
22329
|
+
* dynamic references is 5 x (1+2+4+8 s) = 75 s at
|
|
22330
|
+
* `MAX_DYNAMIC_REFERENCE_THROTTLE_RETRIES` = 4. Healthy work CAN therefore
|
|
22331
|
+
* outlast the cap, and when it does its recording lands after the rejection
|
|
22332
|
+
* was released, which is the window this whole function exists to close.
|
|
22333
|
+
* Nothing here cancels that sibling — it keeps running and can record
|
|
22334
|
+
* arbitrarily later — so what the cap bounds is the WAIT, not the lateness.
|
|
22335
|
+
* That is the trade taken deliberately: a bounded wait with a late record
|
|
22336
|
+
* still possible beyond it, against an unbounded hold on a deploy's state
|
|
22337
|
+
* save.
|
|
22338
|
+
*
|
|
22339
|
+
* AND A DRAIN CAN GET NO GRACE AT ALL. The budget is shared REMAINING wait,
|
|
22340
|
+
* so a drain that arms once it is spent gets
|
|
22341
|
+
* `Math.max(0, remaining - openWindow)` = 0 and releases its rejection on the
|
|
22342
|
+
* next macrotask. That applies to any drain NOT ALREADY ARMED when a rejection
|
|
22343
|
+
* reaches it: the ordinary case is an inner drain that spends the full budget
|
|
22344
|
+
* and throws, whose every not-yet-armed ancestor then arms against an
|
|
22345
|
+
* exhausted remaining-wait budget. What it does NOT mean is that a fast sibling is exposed: a
|
|
22346
|
+
* sibling still pending at that moment has itself been running at least the
|
|
22347
|
+
* budget. What it means is that the sibling's own RECORDING gets no wait --
|
|
22348
|
+
* a lookup begun late inside a long-running part is fast in itself and still
|
|
22349
|
+
* lands after the rejection was released. That reasoning covers the NESTED
|
|
22350
|
+
* ancestor case; a caller that wraps a LOOP widens it, because a later
|
|
22351
|
+
* iteration starts FRESH siblings against a budget an earlier one already
|
|
22352
|
+
* spent and those need not be long-running at all. So the exposure does not
|
|
22353
|
+
* require the "healthy work slower than 60 s" shape the sizing paragraph
|
|
22354
|
+
* describes; that shape is the cheapest way to reach it with no nesting and
|
|
22355
|
+
* no wrapped loop, not the floor.
|
|
22356
|
+
* Residual: issue
|
|
22357
|
+
* [#2814](https://github.com/go-to-k/cdkd/issues/2814).
|
|
22358
|
+
*
|
|
22359
|
+
* ONE REMAINING CONSEQUENCE, deliberate and pinned by a case rather than only
|
|
22360
|
+
* described: the earliest-in-time rule holds PER INVOCATION, not across
|
|
22361
|
+
* NESTED resolutions. For a join whose parts are `[listWithAnEarlyFailure,
|
|
22362
|
+
* laterFailure]`, the inner list's drain holds its own rejection while its
|
|
22363
|
+
* slow sibling finishes, so the outer join captures the later failure first
|
|
22364
|
+
* and reports that instead. That is not only cosmetic: the retry classifiers
|
|
22365
|
+
* DO read the message (`retryClassificationText` feeds
|
|
22366
|
+
* `isRetryableTransientError`, whose `RETRYABLE_ERROR_MESSAGE_PATTERNS` is an
|
|
22367
|
+
* explicit substring table), so swapping which failure surfaces can swap a
|
|
22368
|
+
* transient verdict for a terminal one. What does not change is that the
|
|
22369
|
+
* resolution FAILS: both are genuine failures of the same resolve, and the
|
|
22370
|
+
* selection was already timing-dependent — `Promise.all` reports whichever
|
|
22371
|
+
* lost the race. The drain adds a systematic bias toward the SHALLOWER
|
|
22372
|
+
* failure where the old race was arbitrary. Residual: issue
|
|
22373
|
+
* [#2805](https://github.com/go-to-k/cdkd/issues/2805).
|
|
22374
|
+
*/
|
|
22375
|
+
async function allSettledKeepingFirstRejection(promises) {
|
|
22376
|
+
let rejection;
|
|
22377
|
+
const shared = drainDeadlines.getStore();
|
|
22378
|
+
let armCap;
|
|
22379
|
+
const guarded = promises.map((promise) => promise.catch((error) => {
|
|
22380
|
+
if (rejection === void 0) {
|
|
22381
|
+
rejection = { error };
|
|
22382
|
+
armCap?.();
|
|
22383
|
+
}
|
|
22384
|
+
}));
|
|
22385
|
+
let capTimer;
|
|
22386
|
+
let charged = false;
|
|
22387
|
+
const capped = new Promise((resolve) => {
|
|
22388
|
+
armCap = () => {
|
|
22389
|
+
const budget = concurrentDrainCap.ms ?? DRAIN_AFTER_REJECTION_MS;
|
|
22390
|
+
let wait = budget;
|
|
22391
|
+
if (shared !== void 0) {
|
|
22392
|
+
shared.remaining ??= budget;
|
|
22393
|
+
const openWindow = shared.since === void 0 ? 0 : Date.now() - shared.since;
|
|
22394
|
+
wait = Math.max(0, shared.remaining - openWindow);
|
|
22395
|
+
if (shared.waiting === 0) shared.since = Date.now();
|
|
22396
|
+
shared.waiting += 1;
|
|
22397
|
+
charged = true;
|
|
22398
|
+
}
|
|
22399
|
+
capTimer = setTimeout(() => resolve(CAP_EXPIRED), wait);
|
|
22400
|
+
};
|
|
22401
|
+
});
|
|
22402
|
+
try {
|
|
22403
|
+
const outcome = await Promise.race([Promise.all(guarded), capped]);
|
|
22404
|
+
if (rejection !== void 0) throw rejection.error;
|
|
22405
|
+
if (outcome === CAP_EXPIRED) throw markNonRetryable(/* @__PURE__ */ new Error("drain cap expired with no rejection recorded"));
|
|
22406
|
+
return outcome;
|
|
22407
|
+
} finally {
|
|
22408
|
+
if (capTimer !== void 0) clearTimeout(capTimer);
|
|
22409
|
+
if (charged && shared !== void 0) {
|
|
22410
|
+
shared.waiting -= 1;
|
|
22411
|
+
if (shared.waiting === 0 && shared.since !== void 0) {
|
|
22412
|
+
shared.remaining = Math.max(0, (shared.remaining ?? 0) - (Date.now() - shared.since));
|
|
22413
|
+
shared.since = void 0;
|
|
22414
|
+
}
|
|
22415
|
+
}
|
|
22416
|
+
}
|
|
22417
|
+
}
|
|
22418
|
+
/**
|
|
22123
22419
|
* Is `region` safe to build an AWS SDK client from?
|
|
22124
22420
|
*
|
|
22125
22421
|
* This is a SECURITY gate, not an AWS region registry, and the distinction
|
|
@@ -23022,7 +23318,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23022
23318
|
* Resolve all intrinsic functions in a value
|
|
23023
23319
|
*/
|
|
23024
23320
|
async resolve(value, context) {
|
|
23025
|
-
return await this.resolveValue(value, context);
|
|
23321
|
+
return await withSharedDrainBudget(() => this.resolveValue(value, context));
|
|
23026
23322
|
}
|
|
23027
23323
|
/**
|
|
23028
23324
|
* Evaluate all conditions in the template
|
|
@@ -23064,7 +23360,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23064
23360
|
}
|
|
23065
23361
|
};
|
|
23066
23362
|
for (const name of Object.keys(templateConditions)) try {
|
|
23067
|
-
await evaluateByName(name);
|
|
23363
|
+
await withSharedDrainBudget(() => evaluateByName(name));
|
|
23068
23364
|
} catch (error) {
|
|
23069
23365
|
this.logger.warn(this.maskSecretsForLog(`Failed to evaluate condition ${name}: ${error instanceof Error ? error.message : String(error)}, assuming false`, maskingContext));
|
|
23070
23366
|
conditions[name] = false;
|
|
@@ -23080,7 +23376,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23080
23376
|
if (typeof value === "string" && value.includes("{{resolve:")) return await this.resolveDynamicReferences(value, context);
|
|
23081
23377
|
return value;
|
|
23082
23378
|
}
|
|
23083
|
-
if (Array.isArray(value)) return (await
|
|
23379
|
+
if (Array.isArray(value)) return (await allSettledKeepingFirstRejection(value.map((v) => this.resolveValue(v, context)))).filter((v) => v !== AWS_NO_VALUE);
|
|
23084
23380
|
const obj = value;
|
|
23085
23381
|
if ("Ref" in obj) return await this.resolveRef(obj["Ref"], context);
|
|
23086
23382
|
if ("Fn::GetAtt" in obj) return await this.resolveGetAtt(obj["Fn::GetAtt"], context);
|
|
@@ -23693,6 +23989,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23693
23989
|
case "ApiId": return physicalId;
|
|
23694
23990
|
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
23695
23991
|
}
|
|
23992
|
+
if (resourceType === "AWS::ApiGatewayV2::Api") switch (attributeName) {
|
|
23993
|
+
case "ExecuteApiArn": return `arn:${partition}:execute-api:${region}:${accountId}:${physicalId}`;
|
|
23994
|
+
case "ApiId": return physicalId;
|
|
23995
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
23996
|
+
}
|
|
23696
23997
|
if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
|
|
23697
23998
|
case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
|
|
23698
23999
|
case "Id": return physicalId;
|
|
@@ -23902,7 +24203,7 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23902
24203
|
let values = rawValues;
|
|
23903
24204
|
if (!Array.isArray(values)) values = await this.resolveValue(values, context);
|
|
23904
24205
|
if (!Array.isArray(values)) throw new Error(`Fn::Join's second argument must be a list (an array literal or a list-returning intrinsic such as Fn::Cidr / Fn::GetAZs / Fn::Split / a Ref to a list-typed parameter — any List<...> type or CommaDelimitedList), but resolved to ${typeof values}`);
|
|
23905
|
-
let result = (await
|
|
24206
|
+
let result = (await allSettledKeepingFirstRejection(values.map(async (v) => {
|
|
23906
24207
|
const resolved = await this.resolveValue(v, context);
|
|
23907
24208
|
return String(resolved);
|
|
23908
24209
|
}))).join(delimiter);
|
|
@@ -26642,7 +26943,7 @@ var CloudControlProvider = class {
|
|
|
26642
26943
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
26643
26944
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
26644
26945
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
26645
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
26946
|
+
const { ASGProvider } = await import("./asg-provider-TexsJufw.js").then((n) => n.n);
|
|
26646
26947
|
const asgProvider = new ASGProvider();
|
|
26647
26948
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
26648
26949
|
}
|
|
@@ -31119,6 +31420,134 @@ function isExportAliasCollision(exportName, outputKey, ownedOutputNames) {
|
|
|
31119
31420
|
*/
|
|
31120
31421
|
const MIN_SECRET_NEEDLE = 4;
|
|
31121
31422
|
/**
|
|
31423
|
+
* Characters deleted before any secret containment test on this path, and
|
|
31424
|
+
* deleted from the text that gets PRINTED — ONE class, because the verdict and
|
|
31425
|
+
* the printed text have to live in the same string space (issue
|
|
31426
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)).
|
|
31427
|
+
*
|
|
31428
|
+
* WHY A THIRD CLASS RATHER THAN EITHER SANITISER'S. `stripControlChars`
|
|
31429
|
+
* DELETES its class; `displaySafe` REPLACES its own with a space. Composing
|
|
31430
|
+
* them — which is what every site here used to do — leaves three different
|
|
31431
|
+
* strings in play: the raw key the verdict was taken from, the sanitised key
|
|
31432
|
+
* that was printed, and the masked one in between. A recorded secret split by
|
|
31433
|
+
* a DELETED character is absent from the raw key and contiguous in the printed
|
|
31434
|
+
* one, so the verdict said `safe` over text that held the plaintext; a secret
|
|
31435
|
+
* split by a REPLACED one is absent from both and prints one character short
|
|
31436
|
+
* of the plaintext, which a `not.toContain(SECRET)` assertion cannot see.
|
|
31437
|
+
* Measured across both classes: eight of ten characters reconstituted the
|
|
31438
|
+
* secret verbatim, and the remaining two printed it minus one character.
|
|
31439
|
+
*
|
|
31440
|
+
* So the fix is not another arm on the check — it is removing the second and
|
|
31441
|
+
* third string. Everything below happens in `canonicalForSecretScan` space.
|
|
31442
|
+
*
|
|
31443
|
+
* THE CLASS IS DERIVED FROM UNICODE CATEGORIES, NOT ENUMERATED. A hand list
|
|
31444
|
+
* was written first -- both sanitisers' classes plus the four residuals
|
|
31445
|
+
* `display-safe.ts` names -- and review measured it arbitrary: `U+2060`
|
|
31446
|
+
* (WORD JOINER) verdicted `safe` and printed visibly-contiguous plaintext
|
|
31447
|
+
* while `U+FEFF` was caught, and `U+2060` is the character Unicode itself
|
|
31448
|
+
* designates as the replacement for `U+FEFF` in that role. Eight more did the
|
|
31449
|
+
* same (`U+00AD`, `U+180E`, `U+FE0F`, `U+3164`, `U+2061`, `U+FFFB`, `U+115F`,
|
|
31450
|
+
* `U+17B4`). A list of the invisibles somebody happened to think of is not a
|
|
31451
|
+
* boundary; the general categories are.
|
|
31452
|
+
*
|
|
31453
|
+
* - `\p{Cc}` control and `\p{Cf}` format: the C0 / C1 and DEL ranges, the bidi
|
|
31454
|
+
* marks / embeddings / overrides / isolates, the zero-width set, `U+FEFF`,
|
|
31455
|
+
* `U+00AD`, `U+061C`, and the invisible-operator block.
|
|
31456
|
+
* - `\p{Zl}` / `\p{Zp}`: `U+2028` / `U+2029`, the two `displaySafe` REPLACES.
|
|
31457
|
+
* - `\p{Default_Ignorable_Code_Point}`: Unicode's own INTENT-TO-BE-IGNORED
|
|
31458
|
+
* property, which carries the variation selectors, the Mongolian free
|
|
31459
|
+
* variation selectors, the Hangul fillers and `U+034F` COMBINING GRAPHEME
|
|
31460
|
+
* JOINER.
|
|
31461
|
+
*
|
|
31462
|
+
* - `\p{Me}` ENCLOSING marks. The same zero-advance-width shape as `\p{Mn}`
|
|
31463
|
+
* below, and INCLUDED rather than deferred because the cost argument that
|
|
31464
|
+
* defers `\p{Mn}` does not transfer: `\p{Me}` is about a dozen code points
|
|
31465
|
+
* with no legitimate use in a resource name.
|
|
31466
|
+
*
|
|
31467
|
+
* NAMED RESIDUAL, because `\p{Default_Ignorable_Code_Point}` is Unicode's
|
|
31468
|
+
* INTENT-TO-BE-IGNORED property and NOT "everything that renders as nothing"
|
|
31469
|
+
* -- an earlier revision of this comment claimed the latter and review refuted
|
|
31470
|
+
* it. NONSPACING marks (`\p{Mn}`) carry zero advance width, so a secret split
|
|
31471
|
+
* by one renders contiguous while this class keeps it: measured with `U+09BC`,
|
|
31472
|
+
* which verdicts `safe` AND publishes the alias. Not widened here, because
|
|
31473
|
+
* `\p{Mn}` is the diacritics of Devanagari, Arabic, Hebrew and Vietnamese and
|
|
31474
|
+
* deleting it would mangle legitimate names for every user. Tracked as issue
|
|
31475
|
+
* [#2889](https://github.com/go-to-k/cdkd/issues/2889), which also carries the
|
|
31476
|
+
* homoglyph question. `origin/main` behaves identically, so this is a recorded
|
|
31477
|
+
* residual rather than something this change introduced.
|
|
31478
|
+
*
|
|
31479
|
+
* THE THIRD BULLET REPLACED A HAND TAIL, and the hand tail was measured
|
|
31480
|
+
* leaking. A first cut listed the ranges by hand -- `FE00-FE0F`,
|
|
31481
|
+
* `E0100-E01EF`, `180B-180D`, `115F`, `1160`, `3164`, `FFA0`, `17B4`, `17B5`
|
|
31482
|
+
* -- and review found `U+034F` outside it, printing `hunter2hunter2` in a
|
|
31483
|
+
* `warn` line under a `safe` verdict. It also spelled `180B-180D` while
|
|
31484
|
+
* calling itself "the Mongolian FVS", missing `U+180F`, which Unicode 14
|
|
31485
|
+
* added. Naming the PROPERTY is what makes the next such addition arrive
|
|
31486
|
+
* covered instead of arriving as another finding.
|
|
31487
|
+
*
|
|
31488
|
+
* WIDENED HERE AND NOT IN `display-safe.ts` because the two files answer
|
|
31489
|
+
* different questions. `displaySafe` is about a terminal rendering a value,
|
|
31490
|
+
* and its recorded reason for keeping these is command forgery -- where a
|
|
31491
|
+
* character that renders as nothing changes nothing. That reason does not
|
|
31492
|
+
* transfer to a secret: a plaintext split by an invisible character is READ by
|
|
31493
|
+
* a human exactly as if it were contiguous, so disclosure needs no paste.
|
|
31494
|
+
*
|
|
31495
|
+
* `tests/unit/deployment/secret-scan-class-superset.test.ts` fences this as a
|
|
31496
|
+
* SUPERSET of both sanitisers' classes by SCANNING CODE POINTS rather than by
|
|
31497
|
+
* comparing source text, so a future edit to either sanitiser that this class
|
|
31498
|
+
* does not cover reds -- the failure mode a hand list has and a derivation
|
|
31499
|
+
* does not.
|
|
31500
|
+
*/
|
|
31501
|
+
const SECRET_SCAN_INVISIBLES = /[\p{Cc}\p{Cf}\p{Me}\p{Zl}\p{Zp}\p{Default_Ignorable_Code_Point}]/gu;
|
|
31502
|
+
/**
|
|
31503
|
+
* The one string space in which this module tests for, masks, and prints a
|
|
31504
|
+
* possibly-secret-bearing name.
|
|
31505
|
+
*
|
|
31506
|
+
* `.trim()` mirrors `displaySafe`, whose trim this replaces on these paths. It
|
|
31507
|
+
* is applied to the TEXT only -- see {@link canonicalNeedle}, where trimming
|
|
31508
|
+
* would silently shorten a recorded secret.
|
|
31509
|
+
*/
|
|
31510
|
+
function canonicalForSecretScan(text) {
|
|
31511
|
+
return text.replace(SECRET_SCAN_INVISIBLES, "").trim();
|
|
31512
|
+
}
|
|
31513
|
+
/**
|
|
31514
|
+
* A recorded secret as a NEEDLE in canonical space.
|
|
31515
|
+
*
|
|
31516
|
+
* STRIPPED BUT NOT TRIMMED. That is not an oversight of the symmetry with
|
|
31517
|
+
* {@link canonicalForSecretScan} but the deliberate asymmetry between a
|
|
31518
|
+
* haystack and a needle: whitespace at the edge of a recorded secret is part
|
|
31519
|
+
* of the secret and sits in the MIDDLE of a longer key, so trimming the needle
|
|
31520
|
+
* stops it matching there. Measured -- a recorded `"ab "` embedded in
|
|
31521
|
+
* `x-ab -y` was masked before this change and came back `safe` after it.
|
|
31522
|
+
*
|
|
31523
|
+
* Canonicalising the needle at all is what let the raw-key fallback arm go
|
|
31524
|
+
* away: a secret whose own plaintext carries a stripped character used to
|
|
31525
|
+
* survive in the raw key and be destroyed in the sanitised one, so it could be
|
|
31526
|
+
* DETECTED only from the raw form and MASKED in neither -- a permanent
|
|
31527
|
+
* `withheld`. Here it is both found and masked.
|
|
31528
|
+
*/
|
|
31529
|
+
function canonicalNeedle(plaintext) {
|
|
31530
|
+
return plaintext.replace(SECRET_SCAN_INVISIBLES, "");
|
|
31531
|
+
}
|
|
31532
|
+
/**
|
|
31533
|
+
* The recorded secrets as NEEDLES, keyed by their canonical form.
|
|
31534
|
+
*
|
|
31535
|
+
* A needle whose canonical form is EMPTY is dropped: canonicalisation can turn
|
|
31536
|
+
* a non-empty recorded value into `''`, and `maskEveryOccurrence` splitting on
|
|
31537
|
+
* `''` interleaves the mask between every character of the key. The resolver
|
|
31538
|
+
* never records an empty secret, so nothing upstream guards this. Measured
|
|
31539
|
+
* without the guard: `OrdinaryKey` came back as
|
|
31540
|
+
* `O***r***d***i***n***a***r***y***K***e***y`.
|
|
31541
|
+
*/
|
|
31542
|
+
function canonicalNeedles(secrets) {
|
|
31543
|
+
const out = /* @__PURE__ */ new Map();
|
|
31544
|
+
for (const [plaintext, expression] of secrets ?? []) {
|
|
31545
|
+
const needle = canonicalNeedle(plaintext);
|
|
31546
|
+
if (needle.length > 0) out.set(needle, expression);
|
|
31547
|
+
}
|
|
31548
|
+
return out;
|
|
31549
|
+
}
|
|
31550
|
+
/**
|
|
31122
31551
|
* Which recorded secrets are visible in `text`, or `undefined` when none is.
|
|
31123
31552
|
*
|
|
31124
31553
|
* ONE rule for both callers below, because they were inconsistent and the
|
|
@@ -31143,8 +31572,14 @@ const MIN_SECRET_NEEDLE = 4;
|
|
|
31143
31572
|
*/
|
|
31144
31573
|
function secretsPresentIn(text, secrets) {
|
|
31145
31574
|
if (!secrets || secrets.size === 0) return void 0;
|
|
31575
|
+
const haystack = canonicalForSecretScan(text);
|
|
31146
31576
|
const exposure = /* @__PURE__ */ new Map();
|
|
31147
|
-
for (const [plaintext, expression] of secrets)
|
|
31577
|
+
for (const [plaintext, expression] of secrets) {
|
|
31578
|
+
const needle = canonicalNeedle(plaintext);
|
|
31579
|
+
const canonicalHit = haystack === needle || needle.length >= MIN_SECRET_NEEDLE && haystack.includes(needle);
|
|
31580
|
+
const rawHit = text === plaintext || plaintext.length >= MIN_SECRET_NEEDLE && text.includes(plaintext);
|
|
31581
|
+
if (canonicalHit || rawHit) exposure.set(plaintext, expression);
|
|
31582
|
+
}
|
|
31148
31583
|
return exposure.size > 0 ? exposure : void 0;
|
|
31149
31584
|
}
|
|
31150
31585
|
/**
|
|
@@ -31234,10 +31669,76 @@ function maskEveryOccurrence(text, exposure) {
|
|
|
31234
31669
|
* unchanged. stderr is a reader like any other, so the invariant is absolute: a
|
|
31235
31670
|
* message must never claim a masking it did not perform.
|
|
31236
31671
|
*/
|
|
31237
|
-
function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
31238
|
-
const
|
|
31239
|
-
const
|
|
31240
|
-
|
|
31672
|
+
function secretBearingExportNameWarning(outputKey, exportName, exposure, secrets) {
|
|
31673
|
+
const corpus = secrets ?? exposure;
|
|
31674
|
+
const name = secretSafeKeyDisplay(exportName, corpus, exposure);
|
|
31675
|
+
const shown = name.kind === "masked" ? `(masked: "${name.text}") ` : "";
|
|
31676
|
+
const ownerForceMask = /* @__PURE__ */ new Map();
|
|
31677
|
+
for (const [plaintext, expression] of exposure) if (plaintext === outputKey || canonicalNeedle(plaintext) === canonicalForSecretScan(outputKey) || plaintext.length >= MIN_SECRET_NEEDLE) ownerForceMask.set(plaintext, expression);
|
|
31678
|
+
return `Output ${displayTextOrWithheld(secretSafeKeyDisplay(outputKey, corpus, ownerForceMask))} has an Export.Name that resolves to a value containing a secret ${shown}— skipping the export alias. An export name becomes a key in state.json and in the exports index, and redaction rewrites VALUES only, so publishing it would persist the secret in plaintext. Use a non-secret Export.Name.`;
|
|
31679
|
+
}
|
|
31680
|
+
/**
|
|
31681
|
+
* Test `key` for recorded secret plaintext and return how it may be shown.
|
|
31682
|
+
*
|
|
31683
|
+
* Reuses {@link stateKeySecretExposure} and the same `maskEveryOccurrence` the
|
|
31684
|
+
* warnings below use, rather than restating either: two spellings of "is this
|
|
31685
|
+
* key safe to print" would disagree on the boundary cases those two encode
|
|
31686
|
+
* (the whole-key match for a sub-floor needle, longest-needle-first masking).
|
|
31687
|
+
*
|
|
31688
|
+
* CANONICAL SPACE, not a composition of the two sanitisers. An earlier
|
|
31689
|
+
* revision of this comment described running `stripControlChars` and then
|
|
31690
|
+
* `displaySafe`, with a table of what each one touches and a note that the
|
|
31691
|
+
* ORDER was load-bearing. That composition WAS the defect (issue
|
|
31692
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874)): it leaves three
|
|
31693
|
+
* strings in play -- the raw key the verdict came from, the sanitised key that
|
|
31694
|
+
* was printed, and the masked one between them -- and `stripControlChars`
|
|
31695
|
+
* DELETES, so a plaintext split by one of its characters is absent from the
|
|
31696
|
+
* first and contiguous in the second. See {@link SECRET_SCAN_INVISIBLES} for
|
|
31697
|
+
* the class that replaced it and why it is derived rather than enumerated.
|
|
31698
|
+
*/
|
|
31699
|
+
function secretSafeKeyDisplay(key, secrets, forceMask) {
|
|
31700
|
+
const shown = canonicalForSecretScan(key);
|
|
31701
|
+
const exposure = stateKeySecretExposure(key, secrets);
|
|
31702
|
+
const mask = canonicalNeedles(forceMask);
|
|
31703
|
+
for (const [plaintext, expression] of exposure ?? []) {
|
|
31704
|
+
const needle = canonicalNeedle(plaintext);
|
|
31705
|
+
if (needle.length > 0) mask.set(needle, expression);
|
|
31706
|
+
}
|
|
31707
|
+
if (mask.size === 0) return {
|
|
31708
|
+
kind: "safe",
|
|
31709
|
+
text: shown
|
|
31710
|
+
};
|
|
31711
|
+
const masked = maskEveryOccurrence(shown, mask);
|
|
31712
|
+
if (masked === shown) return exposure ? { kind: "withheld" } : {
|
|
31713
|
+
kind: "safe",
|
|
31714
|
+
text: shown
|
|
31715
|
+
};
|
|
31716
|
+
if (stateKeySecretExposure(masked, secrets)) return { kind: "withheld" };
|
|
31717
|
+
return {
|
|
31718
|
+
kind: "masked",
|
|
31719
|
+
text: masked
|
|
31720
|
+
};
|
|
31721
|
+
}
|
|
31722
|
+
/**
|
|
31723
|
+
* The display for a name plus the verdict its CALLER needs, as one value.
|
|
31724
|
+
*
|
|
31725
|
+
* Exists so a caller cannot take the verdict from one call and the text from
|
|
31726
|
+
* another — the shape of the bug in {@link secretSafeKeyDisplay}'s own callers
|
|
31727
|
+
* (issue #2874), one level up.
|
|
31728
|
+
*/
|
|
31729
|
+
function secretBearing(display) {
|
|
31730
|
+
return display.kind !== "safe";
|
|
31731
|
+
}
|
|
31732
|
+
/**
|
|
31733
|
+
* What a message prints in place of a name it may not show.
|
|
31734
|
+
*
|
|
31735
|
+
* Deliberately not name-shaped and never quoted as if it were a key: a reader
|
|
31736
|
+
* has to be able to tell this is the tool declining, not an odd export name.
|
|
31737
|
+
*/
|
|
31738
|
+
const WITHHELD_NAME_DISPLAY = "<name withheld: contains a secret>";
|
|
31739
|
+
/** The text of a display, or {@link WITHHELD_NAME_DISPLAY} when there is none. */
|
|
31740
|
+
function displayTextOrWithheld(display) {
|
|
31741
|
+
return display.kind === "withheld" ? WITHHELD_NAME_DISPLAY : display.text;
|
|
31241
31742
|
}
|
|
31242
31743
|
/**
|
|
31243
31744
|
* Warning for a state KEY that already holds secret plaintext — the residue an
|
|
@@ -31250,8 +31751,9 @@ function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
|
31250
31751
|
* redeploy, which rewrites `state.outputs` wholesale and republishes the index.
|
|
31251
31752
|
* Reported so the `--dry-run --fail` CI gate stops calling such a state clean.
|
|
31252
31753
|
*/
|
|
31253
|
-
function secretBearingStateKeyWarning(stackName,
|
|
31254
|
-
|
|
31754
|
+
function secretBearingStateKeyWarning(stackName, display) {
|
|
31755
|
+
const clause = display.kind === "masked" ? `(masked: "${display.text}") ` : `(the name is withheld: masking it would leave the secret readable) `;
|
|
31756
|
+
return `State for ${canonicalForSecretScan(stackName)} holds an output KEY that renders a secret ${clause}— cdkd scrub cannot rewrite a key, only a value, because the key IS the export name consumers resolve by. Give that output a non-secret Export.Name and redeploy: the next deploy replaces state.outputs and the exports index entirely. ROTATE the exposed secret.`;
|
|
31255
31757
|
}
|
|
31256
31758
|
/**
|
|
31257
31759
|
* Warning for an `Export.Name` colliding with an output NAME it does not own.
|
|
@@ -31260,13 +31762,18 @@ function secretBearingStateKeyWarning(stackName, key, exposure) {
|
|
|
31260
31762
|
* says which value survives — the export is skipped, so the key keeps the
|
|
31261
31763
|
* output's own value.
|
|
31262
31764
|
*
|
|
31263
|
-
*
|
|
31264
|
-
*
|
|
31265
|
-
*
|
|
31266
|
-
|
|
31267
|
-
|
|
31268
|
-
|
|
31269
|
-
|
|
31765
|
+
* MASKED HERE, and the bound that used to excuse it is named rather than
|
|
31766
|
+
* relied on. This message prints a name that MATCHED a declared output name --
|
|
31767
|
+
* template text -- and a secret-bearing `Export.Name` is refused by
|
|
31768
|
+
* {@link secretBearingExportNameWarning} upstream. But that is SOMEBODY ELSE'S
|
|
31769
|
+
* VERDICT, and this site is reached from exactly the arm taken when it MISSED:
|
|
31770
|
+
* before issue [#2874](https://github.com/go-to-k/cdkd/issues/2874)
|
|
31771
|
+
* canonicalised the containment scan, it missed for eight of ten invisible
|
|
31772
|
+
* characters, and this message printed the plaintext verbatim.
|
|
31773
|
+
*/
|
|
31774
|
+
function exportAliasCollisionWarning(outputKey, exportName, secrets) {
|
|
31775
|
+
const shown = displayTextOrWithheld(secretSafeKeyDisplay(exportName, secrets));
|
|
31776
|
+
const from = displayTextOrWithheld(secretSafeKeyDisplay(outputKey, secrets));
|
|
31270
31777
|
return `Output ${from} exports as "${shown}", which is also the name of another output in this stack — skipping the export alias, so output ${shown} keeps its own value and the export is not published. A consumer's Fn::ImportValue on "${shown}" therefore resolves to output ${shown}, NOT to ${from} (CloudFormation would publish both). Rename the export, or the colliding output.`;
|
|
31271
31778
|
}
|
|
31272
31779
|
/**
|
|
@@ -31281,12 +31788,12 @@ function exportAliasCollisionWarning(outputKey, exportName) {
|
|
|
31281
31788
|
* {@link collectDeclaredOutputNames}.
|
|
31282
31789
|
*/
|
|
31283
31790
|
function exportAliasCollisionScrubWarning(outputKey, exportName, secrets) {
|
|
31284
|
-
const
|
|
31285
|
-
|
|
31286
|
-
|
|
31287
|
-
}
|
|
31288
|
-
const
|
|
31289
|
-
return `Output ${
|
|
31791
|
+
const nameDisplay = (name) => secretSafeKeyDisplay(name, secrets);
|
|
31792
|
+
const exportDisplay = nameDisplay(exportName);
|
|
31793
|
+
const shown = displayTextOrWithheld(exportDisplay);
|
|
31794
|
+
const storedUnder = exportDisplay.kind === "withheld" ? "the stored value under that name" : `the stored value under "${shown}"`;
|
|
31795
|
+
const ownerDisplay = nameDisplay(outputKey);
|
|
31796
|
+
return `Output ${ownerDisplay.kind === "withheld" && exportDisplay.kind === "withheld" ? "<the owning output, name withheld: contains a secret>" : displayTextOrWithheld(ownerDisplay)} exports as ${exportDisplay.kind === "withheld" ? shown : `"${shown}"`}, which is also the name of another output in this stack — state cannot say which of the two ${storedUnder} came from, so that key is redacted by value match instead of by template position, and two references resolving to the same value could still collapse there. Rename the export, or the colliding output, and redeploy.`;
|
|
31290
31797
|
}
|
|
31291
31798
|
|
|
31292
31799
|
//#endregion
|
|
@@ -32682,6 +33189,397 @@ async function createReplicationGroupFinalSnapshot(client, replicationGroupId, l
|
|
|
32682
33189
|
});
|
|
32683
33190
|
}
|
|
32684
33191
|
|
|
33192
|
+
//#endregion
|
|
33193
|
+
//#region src/analyzer/skipped-outputs.ts
|
|
33194
|
+
/**
|
|
33195
|
+
* The record `cdkd deploy` leaves behind for an Output it could NOT resolve
|
|
33196
|
+
* and SKIPPED, and the digest `cdkd diff` compares it against (issue
|
|
33197
|
+
* [#2740](https://github.com/go-to-k/cdkd/issues/2740)).
|
|
33198
|
+
*
|
|
33199
|
+
* The deploy side (`DeployEngine.handleOutputResolutionFailure`, default arm)
|
|
33200
|
+
* warns, stores `undefined` for the key and moves on, so a re-resolved
|
|
33201
|
+
* `state.outputs` lacks it (the no-change path keeps the previous bag whole
|
|
33202
|
+
* when any output fails, so a key that resolved on an EARLIER deploy can keep
|
|
33203
|
+
* its value beside a record — the reader checks absence first and ignores the
|
|
33204
|
+
* record then). The diff side resolves outputs with `skipDynamicReferences`, so a
|
|
33205
|
+
* failure that happens INSIDE a secret lookup — a JSON key the secret does not
|
|
33206
|
+
* hold, a reference assembled from another secret's value — does not reproduce
|
|
33207
|
+
* there: the value assembles into its token, the key is absent from state, and
|
|
33208
|
+
* `computeOutputsDiff` pushed an `ADD` the deploy would never perform, on every
|
|
33209
|
+
* run of an unchanged stack. The diff cannot tell from assembly alone that the
|
|
33210
|
+
* deploy would fail (a composed string carrying a secret reference is a
|
|
33211
|
+
* legitimate output), so it learns what the deploy learned instead:
|
|
33212
|
+
* `StackState.skippedOutputs` maps each skipped key to a digest of the
|
|
33213
|
+
* template inputs its resolution read, and the diff trusts the record only
|
|
33214
|
+
* while that digest is unchanged.
|
|
33215
|
+
*
|
|
33216
|
+
* ## What the digest covers, and why
|
|
33217
|
+
*
|
|
33218
|
+
* `skippedOutputDigest` hashes the output's OWN entry (`Value`, `Export`,
|
|
33219
|
+
* `Condition`, whatever else it declares) together with every top-level
|
|
33220
|
+
* template section EXCEPT `Resources` and `Outputs`:
|
|
33221
|
+
*
|
|
33222
|
+
* - The entry itself: repairing the `Value` expression is the obvious repair.
|
|
33223
|
+
* The `Export.Name` is in it too, because the alias pass routes an
|
|
33224
|
+
* `Export.Name` failure through the same handler, which blanks the output's
|
|
33225
|
+
* own key even when its value resolved — so a repaired or removed export
|
|
33226
|
+
* name must invalidate the record as well.
|
|
33227
|
+
* - `Parameters` (a `Ref`-built reference repaired through a parameter
|
|
33228
|
+
* default), `Conditions` (an `Fn::If` whose branch flips), `Mappings` (an
|
|
33229
|
+
* `Fn::FindInMap`-built key), and the remaining sections for the same
|
|
33230
|
+
* reason, spelled as one rule rather than a list that drifts: everything an
|
|
33231
|
+
* output's resolution can read besides resources. One VALUE inside them is
|
|
33232
|
+
* excluded — the `Default` of a `NoEcho: true` parameter, hashed as a
|
|
33233
|
+
* constant; see `withNoEchoDefaultsMasked` for why. That is the POSITION,
|
|
33234
|
+
* not the parameter: its `AllowedValues`, and any literal an author puts in
|
|
33235
|
+
* `Conditions`, `Rules` or `Metadata`, are hashed as before.
|
|
33236
|
+
* - NOT `Resources`: hashing the section would invalidate the record on every
|
|
33237
|
+
* unrelated resource edit. The output CAN be repaired from that side —
|
|
33238
|
+
* `collectSkippedOutputs` records both skip arms, and the quiet one is an
|
|
33239
|
+
* `Fn::GetAtt` whose attribute `constructAttribute` could not build — so a
|
|
33240
|
+
* digest alone would keep binding while the next deploy publishes the row
|
|
33241
|
+
* and its `Export.Name`, the phantom's inverse. {@link
|
|
33242
|
+
* bindingSkippedOutputs} closes that with the CHANGE MAP rather than the
|
|
33243
|
+
* digest: a key whose entry references a logical id with a pending resource
|
|
33244
|
+
* change does not bind. The diff has that map (it ran the resource diff
|
|
33245
|
+
* first); the deploy does not need it, because a stack with any resource
|
|
33246
|
+
* change takes the path that re-resolves every output and rewrites the
|
|
33247
|
+
* record. REFERENCE, not repair: whether an edit could actually make the
|
|
33248
|
+
* output resolvable is undecidable from a template, so an unrelated edit to
|
|
33249
|
+
* a referenced resource un-binds too and the key can preview as an `ADD`
|
|
33250
|
+
* again. The cost is bounded — a run with a changed resource is already
|
|
33251
|
+
* reporting that row, so `--fail` exits 1 regardless — while the case it
|
|
33252
|
+
* buys depends on whether the diff can resolve the output at all. For an
|
|
33253
|
+
* output reading an attribute that does not exist yet — the SECOND skip arm
|
|
33254
|
+
* this record covers, not the secret lookup issue #2740 was filed on — it is
|
|
33255
|
+
* the VERDICT and not the row: unresolvable in
|
|
33256
|
+
* both readings, so bound the diff reports the outputs settled while the
|
|
33257
|
+
* next deploy is about to publish that key and its `Export.Name`. For an
|
|
33258
|
+
* output the diff CAN resolve, whose reference is to a name rather than to
|
|
33259
|
+
* a pending attribute (an `Fn::Sub` over an SSM parameter's name, say), the
|
|
33260
|
+
* row IS the difference and reappears as an `ADD` the moment the record
|
|
33261
|
+
* stops binding.
|
|
33262
|
+
* - NOT the sibling `Outputs`: a CloudFormation output cannot reference
|
|
33263
|
+
* another output, so a sibling change cannot repair this one, and excluding
|
|
33264
|
+
* them keeps an unrelated output edit from producing a one-run phantom.
|
|
33265
|
+
*
|
|
33266
|
+
* What the digest CANNOT see is a repair outside the template — the secret
|
|
33267
|
+
* gained the JSON key, the SSM parameter was created, a parameter VALUE
|
|
33268
|
+
* handed to the engine rather than declared changed (a nested stack's inputs
|
|
33269
|
+
* from its parent, `DeployEngineOptions.parameters`; `Parameters`
|
|
33270
|
+
* DECLARATIONS are hashed, supplied values are not), or **cdkd itself was
|
|
33271
|
+
* upgraded**: the quiet skip arm is `constructAttribute` returning nothing,
|
|
33272
|
+
* so a provider that gains that attribute repairs the output while neither
|
|
33273
|
+
* the template nor any resource moves.
|
|
33274
|
+
*
|
|
33275
|
+
* They are blind spots for three different reasons, and only the first is
|
|
33276
|
+
* about knowledge. The secret's key set and the SSM parameter's existence
|
|
33277
|
+
* need the external lookup the diff deliberately refuses to make. A supplied
|
|
33278
|
+
* parameter VALUE is not one of those — a nested stack's inputs are resolved
|
|
33279
|
+
* and forwarded to the child diff (`resolveChildStackParameters`) — it is
|
|
33280
|
+
* simply outside what the digest hashes, because hashing values would make
|
|
33281
|
+
* the record depend on a caller's arguments rather than on the template. And
|
|
33282
|
+
* the writing binary's version is knowable and merely unstored: keeping it
|
|
33283
|
+
* beside the digest and un-binding on mismatch would close that one. All four
|
|
33284
|
+
* end the same way: the record keeps binding until the next deploy re-resolves
|
|
33285
|
+
* the output, and that deploy publishes the key — unless a SIBLING output is
|
|
33286
|
+
* still unresolved on a run with no resource change, where the engine keeps
|
|
33287
|
+
* the previous outputs bag wholesale and the key stays unpublished for a
|
|
33288
|
+
* further run (go-to-k/cdkd#2771, pre-existing and not introduced here).
|
|
33289
|
+
* Documented as the accepted limitation in `docs/cli-diff.md`; it is narrower
|
|
33290
|
+
* than the pre-#2740 behaviour, where the diff was wrong on EVERY run.
|
|
33291
|
+
*
|
|
33292
|
+
* A repair on the RESOURCE side is NOT on that list either, but the reason is
|
|
33293
|
+
* narrower than it first looks. The change map above closes it only when the
|
|
33294
|
+
* repair arrives WITH a template resource change, which is what makes the
|
|
33295
|
+
* resource show on the diff at all. An OUT-OF-BAND state write does not:
|
|
33296
|
+
* every writer that rebuilds state outside a deploy DROPS the record rather
|
|
33297
|
+
* than carry it (one enumerated exception, the partial-destroy snapshot, is
|
|
33298
|
+
* explained with the field) — `cdkd import` refreshes `attributes` for the
|
|
33299
|
+
* resources it imports, `cdkd drift --accept` rewrites the properties an
|
|
33300
|
+
* attribute may be built from, `cdkd rollback`'s replacement arm rebuilds a
|
|
33301
|
+
* record with the attributes a FRESH create returned, and the rest are listed
|
|
33302
|
+
* with the field.
|
|
33303
|
+
* Dropping returns those keys to pre-#2740 behaviour until the next deploy
|
|
33304
|
+
* recomputes the record — a row where the diff can resolve them, the ordinary
|
|
33305
|
+
* whole-section suppression where it cannot — which is what keeps a
|
|
33306
|
+
* resource-side repair out of the list above.
|
|
33307
|
+
*
|
|
33308
|
+
* The rule is flat and it is meant to be: EVERY writer that rebuilds state
|
|
33309
|
+
* outside a deploy drops the record, the partial-destroy snapshot being the
|
|
33310
|
+
* one enumerated exception — the full list lives with the field, in
|
|
33311
|
+
* `StackState.skippedOutputs`. Three per-writer arguments for carrying it were
|
|
33312
|
+
* written during review, which is the reason for the flat rule rather than an
|
|
33313
|
+
* aside: two were shown wrong (substituting a value repairs an output whose
|
|
33314
|
+
* enclosing intrinsic was choking on what was there; scrubbing a plaintext
|
|
33315
|
+
* back to its expression rewrites a string outputs read verbatim) and the
|
|
33316
|
+
* third — deleting a property EXPOSING an attribute of the same name — could
|
|
33317
|
+
* not be settled either way, which argues for flatness at least as strongly.
|
|
33318
|
+
*
|
|
33319
|
+
* ## Where the digest must be taken from
|
|
33320
|
+
*
|
|
33321
|
+
* The template AS HANDED to the engine / to `computeStackDiff` — before
|
|
33322
|
+
* parameter binding, condition evaluation or any output resolution. The
|
|
33323
|
+
* INVARIANT, and the reason both callers `structuredClone` rather than pass
|
|
33324
|
+
* their live object: **no resolution may be visible to the digest, on either
|
|
33325
|
+
* side, and both sides must take it at the same point of their own flow.**
|
|
33326
|
+
* Two independent reasons, neither of which depends on any particular
|
|
33327
|
+
* resolver doing the writing: a digest that could see a resolved value would
|
|
33328
|
+
* (1) differ between the two sides, which computes a record the diff can
|
|
33329
|
+
* never re-derive, and (2) fingerprint a secret into `state.json`, since a
|
|
33330
|
+
* resolved value can be a decrypted one.
|
|
33331
|
+
*
|
|
33332
|
+
* The clone is therefore NOT dead weight even while no resolver mutates its
|
|
33333
|
+
* input. It used to be load-bearing against a live rewrite — `resolveSub`
|
|
33334
|
+
* substituted into the caller's two-argument `Fn::Sub` variable map until
|
|
33335
|
+
* go-to-k/cdkd#2764 gave it a fresh object — and the protection was never
|
|
33336
|
+
* derived from that call site: it is derived from the invariant above, which
|
|
33337
|
+
* a future in-place optimisation anywhere in either flow would silently
|
|
33338
|
+
* violate. `tests/unit/cli/diff-recursive-skipped-outputs.test.ts` pins the
|
|
33339
|
+
* current no-mutation contract from the other direction (the template handed
|
|
33340
|
+
* to `computeStackDiff` comes back byte-identical), so reintroducing a
|
|
33341
|
+
* rewrite reds a test rather than moving a digest;
|
|
33342
|
+
* `tests/unit/deployment/deploy-engine-skipped-outputs.test.ts` INJECTS a
|
|
33343
|
+
* rewrite through its resolver mock and requires the recorded digest to equal
|
|
33344
|
+
* a fresh parse's, which pins the ordering itself.
|
|
33345
|
+
*
|
|
33346
|
+
* Given that contract the hash covers template text only. It is over
|
|
33347
|
+
* canonical JSON (object keys sorted at every level), so a template
|
|
33348
|
+
* re-serialised with keys in another order digests identically.
|
|
33349
|
+
*
|
|
33350
|
+
* ## What the diff does with a binding record
|
|
33351
|
+
*
|
|
33352
|
+
* It previews the key as ABSENT — no row — because that is exactly what the
|
|
33353
|
+
* deploy will leave in state. It does NOT flag the section as failed: a
|
|
33354
|
+
* sibling output that genuinely changed still renders, and `--fail` still
|
|
33355
|
+
* exits 1 for it. A record whose digest no longer matches is simply ignored:
|
|
33356
|
+
* the output is previewed under the ordinary rules again (usually an `ADD`;
|
|
33357
|
+
* an intrinsic `Export.Name` the diff still cannot resolve keeps omitting the
|
|
33358
|
+
* section, as before), the next deploy re-decides it —
|
|
33359
|
+
* publishing it if the repair took, or recording it again under the new
|
|
33360
|
+
* digest — and the diff follows that decision.
|
|
33361
|
+
*/
|
|
33362
|
+
/**
|
|
33363
|
+
* JSON with object keys sorted at every level. Arrays keep their order (an
|
|
33364
|
+
* `Fn::Join` list is positional). `undefined` members are dropped, as
|
|
33365
|
+
* `JSON.stringify` drops them, so a parsed template and its in-memory twin
|
|
33366
|
+
* digest identically.
|
|
33367
|
+
*/
|
|
33368
|
+
function canonicalJson(value) {
|
|
33369
|
+
return JSON.stringify(value, (_key, v) => {
|
|
33370
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
33371
|
+
const sorted = Object.create(null);
|
|
33372
|
+
for (const k of Object.keys(v).sort()) sorted[k] = v[k];
|
|
33373
|
+
return sorted;
|
|
33374
|
+
}
|
|
33375
|
+
return v;
|
|
33376
|
+
});
|
|
33377
|
+
}
|
|
33378
|
+
/** The constant a `NoEcho` `Default` is hashed as. Its text is irrelevant; that
|
|
33379
|
+
* it is CONSTANT is the point. */
|
|
33380
|
+
const NO_ECHO_DEFAULT_MASK = "<cdkd:noecho-default>";
|
|
33381
|
+
/**
|
|
33382
|
+
* `Parameters` with every `NoEcho: true` parameter's `Default` replaced by a
|
|
33383
|
+
* constant, for hashing only.
|
|
33384
|
+
*
|
|
33385
|
+
* The pre-resolution snapshot already keeps RESOLVED secret values out of the
|
|
33386
|
+
* digest. An authored `Default` under `NoEcho: true` is not one of those — it
|
|
33387
|
+
* is template text, and it went into the hash. With the rest of the
|
|
33388
|
+
* non-`Resources` sections known, a reader of `state.json` holds a confirm
|
|
33389
|
+
* ORACLE for a low-entropy default: guess it, recompute, compare.
|
|
33390
|
+
*
|
|
33391
|
+
* This does NOT claim the value is otherwise absent from state — a parameter a
|
|
33392
|
+
* resource reads can persist its RESOLVED default in that resource's
|
|
33393
|
+
* properties, and parameter `NoEcho` is outside the state-redaction model
|
|
33394
|
+
* either way. What it claims is narrower and still worth having: this record
|
|
33395
|
+
* must not ADD an oracle of its own for the case where the value is not
|
|
33396
|
+
* already there.
|
|
33397
|
+
*
|
|
33398
|
+
* The cost, stated rather than hidden: changing ONLY such a default no longer
|
|
33399
|
+
* moves the digest, so the record keeps binding through that edit until the
|
|
33400
|
+
* next deploy re-resolves the output. That is the same bounded staleness the
|
|
33401
|
+
* documented blind spots carry.
|
|
33402
|
+
*
|
|
33403
|
+
* Everything else about the parameter is still hashed — its `Type`, its
|
|
33404
|
+
* `AllowedValues`, the `NoEcho` flag itself — so a parameter appearing,
|
|
33405
|
+
* disappearing or changing shape still un-binds. Unmatched parameter content
|
|
33406
|
+
* hashes exactly as before: the map is rebuilt, but the canonicaliser sorts
|
|
33407
|
+
* keys and ignores prototypes, so a template with no `NoEcho` default gets the
|
|
33408
|
+
* digest it always had.
|
|
33409
|
+
*/
|
|
33410
|
+
function withNoEchoDefaultsMasked(parameters) {
|
|
33411
|
+
if (parameters === null || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
|
|
33412
|
+
const masked = Object.create(null);
|
|
33413
|
+
for (const [name, decl] of Object.entries(parameters)) {
|
|
33414
|
+
const body = decl;
|
|
33415
|
+
masked[name] = typeof body === "object" && body !== null && body["NoEcho"] === true && "Default" in body ? {
|
|
33416
|
+
...body,
|
|
33417
|
+
Default: NO_ECHO_DEFAULT_MASK
|
|
33418
|
+
} : decl;
|
|
33419
|
+
}
|
|
33420
|
+
return masked;
|
|
33421
|
+
}
|
|
33422
|
+
/**
|
|
33423
|
+
* The digest recorded in `StackState.skippedOutputs[outputKey]` by the deploy
|
|
33424
|
+
* and recomputed by the diff — see the module doc for what it covers. Both
|
|
33425
|
+
* sides call THIS function, so the coverage is spelled once.
|
|
33426
|
+
*
|
|
33427
|
+
* `template` must be the pre-resolution snapshot the module doc describes.
|
|
33428
|
+
*/
|
|
33429
|
+
function skippedOutputDigest(template, outputKey) {
|
|
33430
|
+
const sections = Object.create(null);
|
|
33431
|
+
for (const [section, body] of Object.entries(template)) {
|
|
33432
|
+
if (section === "Resources" || section === "Outputs") continue;
|
|
33433
|
+
sections[section] = section === "Parameters" ? withNoEchoDefaultsMasked(body) : body;
|
|
33434
|
+
}
|
|
33435
|
+
const entry = template.Outputs?.[outputKey];
|
|
33436
|
+
return createHash("sha256").update(canonicalJson({
|
|
33437
|
+
output: entry,
|
|
33438
|
+
template: sections
|
|
33439
|
+
})).digest("hex");
|
|
33440
|
+
}
|
|
33441
|
+
/**
|
|
33442
|
+
* The record a deploy writes for the bag `resolveOutputs` produced: every key
|
|
33443
|
+
* whose value is `undefined`, mapped to its digest. Two arms write that value:
|
|
33444
|
+
* the default arm of `handleOutputResolutionFailure` (the resolver THREW —
|
|
33445
|
+
* warned, and refused under `--strict-getatt`), and a resolver that returned
|
|
33446
|
+
* `undefined` outright without throwing (an attribute it could construct
|
|
33447
|
+
* nothing for, `constructAttribute`'s empty arm — no warn, no strict refusal).
|
|
33448
|
+
* Both leave the key out of a re-resolved bag (the no-change path may keep a
|
|
33449
|
+
* previous value under it, see the module doc), and that path's
|
|
33450
|
+
* `resolutionFailed` treats both the same, so this record does too: the
|
|
33451
|
+
* question it answers is "will the next deploy leave this key absent?", and
|
|
33452
|
+
* the answer is yes either way. `undefined` (the field OMITTED) when nothing
|
|
33453
|
+
* was skipped, so a record that carries the field says at least one output
|
|
33454
|
+
* was skipped.
|
|
33455
|
+
*
|
|
33456
|
+
* `template` must be the pre-resolution snapshot (see the module doc).
|
|
33457
|
+
*/
|
|
33458
|
+
function collectSkippedOutputs(template, resolvedOutputs) {
|
|
33459
|
+
let record;
|
|
33460
|
+
for (const [outputKey, value] of Object.entries(resolvedOutputs)) {
|
|
33461
|
+
if (value !== void 0) continue;
|
|
33462
|
+
record ??= Object.create(null);
|
|
33463
|
+
record[outputKey] = skippedOutputDigest(template, outputKey);
|
|
33464
|
+
}
|
|
33465
|
+
return record;
|
|
33466
|
+
}
|
|
33467
|
+
/**
|
|
33468
|
+
* Every resource logical id an output entry could reference, from `Ref`,
|
|
33469
|
+
* either `Fn::GetAtt` spelling, and an `Fn::Sub` placeholder, walked to any
|
|
33470
|
+
* depth over the whole entry (`Value`, `Export.Name` and anything else it
|
|
33471
|
+
* declares).
|
|
33472
|
+
*
|
|
33473
|
+
* A deliberate SUPERSET: an `Fn::Sub` placeholder naming a template
|
|
33474
|
+
* PARAMETER, or a `Ref` to one, lands here too, since telling the two apart
|
|
33475
|
+
* needs the `Parameters` section and the answer only ever widens the set. The
|
|
33476
|
+
* one consumer intersects it with the ids that actually have a pending
|
|
33477
|
+
* RESOURCE change, so a name that is not a resource cannot match, and a
|
|
33478
|
+
* parameter deliberately named after a changed resource merely un-binds one
|
|
33479
|
+
* record — the direction that previews a row rather than hiding one. Pseudo
|
|
33480
|
+
* parameters (`AWS::…`) and the `${!Literal}` escape are dropped.
|
|
33481
|
+
*/
|
|
33482
|
+
function referencedLogicalIds(entry) {
|
|
33483
|
+
const ids = /* @__PURE__ */ new Set();
|
|
33484
|
+
const addName = (name) => {
|
|
33485
|
+
const logicalId = name.split(".")[0];
|
|
33486
|
+
if (logicalId !== void 0 && logicalId !== "" && !logicalId.startsWith("AWS::")) ids.add(logicalId);
|
|
33487
|
+
};
|
|
33488
|
+
const walk = (value) => {
|
|
33489
|
+
if (Array.isArray(value)) {
|
|
33490
|
+
for (const item of value) walk(item);
|
|
33491
|
+
return;
|
|
33492
|
+
}
|
|
33493
|
+
if (value === null || typeof value !== "object") return;
|
|
33494
|
+
const obj = value;
|
|
33495
|
+
if (typeof obj["Ref"] === "string") addName(obj["Ref"]);
|
|
33496
|
+
const getAtt = obj["Fn::GetAtt"];
|
|
33497
|
+
if (typeof getAtt === "string") addName(getAtt);
|
|
33498
|
+
else if (Array.isArray(getAtt) && typeof getAtt[0] === "string") addName(getAtt[0]);
|
|
33499
|
+
for (const [key, nested] of Object.entries(obj)) {
|
|
33500
|
+
if (key === "Fn::Sub") {
|
|
33501
|
+
walkSub(nested);
|
|
33502
|
+
continue;
|
|
33503
|
+
}
|
|
33504
|
+
walk(nested);
|
|
33505
|
+
}
|
|
33506
|
+
};
|
|
33507
|
+
/**
|
|
33508
|
+
* `Fn::Sub` in both spellings. Handled here rather than by the generic walk
|
|
33509
|
+
* because the two-argument form's second element is a VARIABLE MAP, not a
|
|
33510
|
+
* template fragment: its KEYS are variable names, so letting the generic
|
|
33511
|
+
* walk see `{ Ref: 'Unrelated' }` there would read a variable literally
|
|
33512
|
+
* named `Ref` as a `Ref` intrinsic. The map's VALUES are walked, since the
|
|
33513
|
+
* resolver resolves them.
|
|
33514
|
+
*/
|
|
33515
|
+
const walkSub = (sub) => {
|
|
33516
|
+
const isPair = Array.isArray(sub);
|
|
33517
|
+
const subTemplate = typeof sub === "string" ? sub : isPair ? sub[0] : void 0;
|
|
33518
|
+
const variableMap = isPair && sub[1] !== null && typeof sub[1] === "object" ? sub[1] : void 0;
|
|
33519
|
+
if (typeof subTemplate === "string") {
|
|
33520
|
+
const declaredVars = new Set(variableMap === void 0 ? [] : Object.keys(variableMap));
|
|
33521
|
+
for (const [, placeholder] of subTemplate.matchAll(/\$\{([^}]*)\}/g)) {
|
|
33522
|
+
if (placeholder === void 0 || placeholder.startsWith("!")) continue;
|
|
33523
|
+
if (declaredVars.has(placeholder)) continue;
|
|
33524
|
+
addName(placeholder);
|
|
33525
|
+
}
|
|
33526
|
+
}
|
|
33527
|
+
if (isPair) for (const [index, element] of sub.entries()) {
|
|
33528
|
+
if (index === 1) {
|
|
33529
|
+
if (variableMap !== void 0) for (const declared of Object.values(variableMap)) walk(declared);
|
|
33530
|
+
continue;
|
|
33531
|
+
}
|
|
33532
|
+
walk(element);
|
|
33533
|
+
}
|
|
33534
|
+
else walk(sub);
|
|
33535
|
+
};
|
|
33536
|
+
walk(entry);
|
|
33537
|
+
return ids;
|
|
33538
|
+
}
|
|
33539
|
+
/**
|
|
33540
|
+
* The keys of a `StackState.skippedOutputs` record that still BIND against
|
|
33541
|
+
* today's template: recorded digest equal to the digest of the same key over
|
|
33542
|
+
* `pristineTemplate`, which must be the pre-resolution snapshot the module doc
|
|
33543
|
+
* describes. A key the template no longer declares is skipped outright — the
|
|
33544
|
+
* record is inert for it, and the next deploy clears it. The diff passes the
|
|
33545
|
+
* result to `resolveTemplateOutputs`, which adds the one check only state can
|
|
33546
|
+
* answer — that the key is still absent from the stored bag.
|
|
33547
|
+
*
|
|
33548
|
+
* `changedLogicalIds` is the second half of the binding rule, and the one the
|
|
33549
|
+
* digest structurally cannot carry (see the module doc's `Resources` bullet):
|
|
33550
|
+
* the logical ids this run's resource diff reports as changing. An output
|
|
33551
|
+
* whose entry references one of them does NOT bind, because the deploy that
|
|
33552
|
+
* follows takes the changed-resources path, re-resolves every output, and may
|
|
33553
|
+
* publish the very key the record would have hidden. Omitted by a caller with
|
|
33554
|
+
* no resource diff in hand — every such caller today is a test asserting the
|
|
33555
|
+
* digest half alone.
|
|
33556
|
+
*/
|
|
33557
|
+
function bindingSkippedOutputs(pristineTemplate, record, changedLogicalIds) {
|
|
33558
|
+
const binding = /* @__PURE__ */ new Set();
|
|
33559
|
+
if (record === null || record === void 0) return binding;
|
|
33560
|
+
const declared = pristineTemplate.Outputs ?? {};
|
|
33561
|
+
for (const [outputKey, digest] of Object.entries(record)) {
|
|
33562
|
+
if (!Object.prototype.hasOwnProperty.call(declared, outputKey)) continue;
|
|
33563
|
+
const entry = declared[outputKey];
|
|
33564
|
+
if (digest !== skippedOutputDigest(pristineTemplate, outputKey)) continue;
|
|
33565
|
+
if (changedLogicalIds !== void 0 && [...referencedLogicalIds(entry)].some((id) => changedLogicalIds.has(id))) continue;
|
|
33566
|
+
binding.add(outputKey);
|
|
33567
|
+
}
|
|
33568
|
+
return binding;
|
|
33569
|
+
}
|
|
33570
|
+
/**
|
|
33571
|
+
* Whether two records — either possibly absent — describe the same skipped
|
|
33572
|
+
* set with the same digests. The no-change deploy path persists on a
|
|
33573
|
+
* difference here: it is the ONLY save trigger for this field on that path,
|
|
33574
|
+
* because a skipped output switches the outputs-changed trigger off.
|
|
33575
|
+
*/
|
|
33576
|
+
function skippedOutputsEqual(a, b) {
|
|
33577
|
+
const aKeys = a === null || a === void 0 ? [] : Object.keys(a);
|
|
33578
|
+
const bKeys = b === null || b === void 0 ? [] : Object.keys(b);
|
|
33579
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
33580
|
+
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a?.[k] === b?.[k]);
|
|
33581
|
+
}
|
|
33582
|
+
|
|
32685
33583
|
//#endregion
|
|
32686
33584
|
//#region src/analyzer/implicit-delete-deps.ts
|
|
32687
33585
|
/**
|
|
@@ -35198,8 +36096,17 @@ var DeployEngine = class {
|
|
|
35198
36096
|
* secret collapse onto whichever expression was recorded last, exactly as two
|
|
35199
36097
|
* resource properties did before #1904. Captured in `resolveOutputs` at the
|
|
35200
36098
|
* same point `outputSecrets` is accumulated. Reset per `deploy()`.
|
|
36099
|
+
*
|
|
36100
|
+
* Null-prototype for the same reason as the outputs bag `resolveOutputs`
|
|
36101
|
+
* builds two lines away: both are keyed by TEMPLATE-CONTROLLED output names,
|
|
36102
|
+
* so an output called `__proto__` would replace this bag's prototype and a
|
|
36103
|
+
* later lookup of a name it never stored would inherit from it. Only the
|
|
36104
|
+
* RESET in `deploy()` is pinned — reverting this initialiser alone survives
|
|
36105
|
+
* the suite, measured, because `deploy()` replaces the bag before anything
|
|
36106
|
+
* writes to it, so the initialiser matches the reset rather than standing as
|
|
36107
|
+
* a second guard.
|
|
35201
36108
|
*/
|
|
35202
|
-
outputsTemplateSource =
|
|
36109
|
+
outputsTemplateSource = Object.create(null);
|
|
35203
36110
|
/**
|
|
35204
36111
|
* The export aliases the last `resolveOutputs` pass WROTE into its bag
|
|
35205
36112
|
* (issue #2193) — exactly the keys `outputs[exportName] = value` landed on,
|
|
@@ -35212,6 +36119,18 @@ var DeployEngine = class {
|
|
|
35212
36119
|
*/
|
|
35213
36120
|
resolvedExportNames = [];
|
|
35214
36121
|
/**
|
|
36122
|
+
* The outputs the last `resolveOutputs` pass could NOT resolve and SKIPPED
|
|
36123
|
+
* (the resolver threw under the default arm of
|
|
36124
|
+
* `handleOutputResolutionFailure`, or returned `undefined` outright — see
|
|
36125
|
+
* `collectSkippedOutputs`), each mapped to the
|
|
36126
|
+
* digest `cdkd diff` compares against (issue #2740, `StackState.
|
|
36127
|
+
* skippedOutputs`). `undefined` when nothing was skipped, so the saves that
|
|
36128
|
+
* persist the resolved bag spread it as-is and the field stays omitted. Same
|
|
36129
|
+
* lifetime rule as `resolvedExportNames`: reset at the top of every
|
|
36130
|
+
* `resolveOutputs`, meaningful only right after that call returns.
|
|
36131
|
+
*/
|
|
36132
|
+
skippedOutputs;
|
|
36133
|
+
/**
|
|
35215
36134
|
* Whether {@link outputsTemplateSource} may be used to POSITION the outputs
|
|
35216
36135
|
* redaction. False once an outputs pass threw partway: the post-loop
|
|
35217
36136
|
* name pass never ran, so the bag holds only the alias keys written before
|
|
@@ -35298,8 +36217,9 @@ var DeployEngine = class {
|
|
|
35298
36217
|
this.perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
35299
36218
|
this.attemptedResolvedProps = /* @__PURE__ */ new Map();
|
|
35300
36219
|
this.outputSecrets = /* @__PURE__ */ new Map();
|
|
35301
|
-
this.outputsTemplateSource =
|
|
36220
|
+
this.outputsTemplateSource = Object.create(null);
|
|
35302
36221
|
this.outputsSourceUsable = true;
|
|
36222
|
+
this.skippedOutputs = void 0;
|
|
35303
36223
|
this.retainedOldOnReplacement = /* @__PURE__ */ new Set();
|
|
35304
36224
|
this.resolver.resetPhysicalIdFallbackCount();
|
|
35305
36225
|
return withStackName(stackName, () => this.doDeploy(stackName, template));
|
|
@@ -35783,6 +36703,10 @@ var DeployEngine = class {
|
|
|
35783
36703
|
} catch {}
|
|
35784
36704
|
this.kickOffAutoRefreshObservedProperties(currentState.resources);
|
|
35785
36705
|
this.logger.debug(`Template has ${Object.keys(template.Resources || {}).length} resources`);
|
|
36706
|
+
const outputsDigestSource = structuredClone({
|
|
36707
|
+
...template,
|
|
36708
|
+
Resources: {}
|
|
36709
|
+
});
|
|
35786
36710
|
const parameterValues = await this.resolver.resolveParameters(template, this.options.parameters, { ...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets } });
|
|
35787
36711
|
this.logger.debug(`Resolved ${Object.keys(parameterValues).length} parameters: ${Object.keys(parameterValues).join(", ")}`);
|
|
35788
36712
|
const context = this.buildResolverContext({
|
|
@@ -35826,7 +36750,7 @@ var DeployEngine = class {
|
|
|
35826
36750
|
this.logger.info("No changes detected. Stack is up to date.");
|
|
35827
36751
|
let persistedOutputs = currentState.outputs ?? {};
|
|
35828
36752
|
if (!this.options.dryRun) {
|
|
35829
|
-
const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions));
|
|
36753
|
+
const resolvedOutputs = this.redactOutputs(await withSharedDrainBudget(() => this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, outputsDigestSource, parameterValues, conditions)));
|
|
35830
36754
|
const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
|
|
35831
36755
|
const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
|
|
35832
36756
|
const currentEffectiveExports = new Set(importableOutputKeys(currentState));
|
|
@@ -35835,7 +36759,8 @@ var DeployEngine = class {
|
|
|
35835
36759
|
if (resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs)) this.logger.warn("Outputs changed but one or more could not be resolved; keeping the previously persisted outputs. A downstream Fn::ImportValue may fail until the next deploy.");
|
|
35836
36760
|
const observedRefresh = this.observedCaptureTasks.size > 0;
|
|
35837
36761
|
if (observedRefresh) await this.drainObservedCaptures(currentState.resources);
|
|
35838
|
-
|
|
36762
|
+
const skippedOutputsChanged = !skippedOutputsEqual(currentState.skippedOutputs, this.skippedOutputs);
|
|
36763
|
+
if (observedRefresh || outputsChanged || exportSetChanged || skippedOutputsChanged) try {
|
|
35839
36764
|
const refreshedState = {
|
|
35840
36765
|
version: 9,
|
|
35841
36766
|
region: this.stackRegion,
|
|
@@ -35843,6 +36768,7 @@ var DeployEngine = class {
|
|
|
35843
36768
|
resources: currentState.resources,
|
|
35844
36769
|
outputs: outputsChanged ? resolvedOutputs : persistedOutputs,
|
|
35845
36770
|
...resolutionFailed ? exportNamesCarriedFrom(currentState) : { exportNames: [...this.resolvedExportNames] },
|
|
36771
|
+
...this.skippedOutputs && { skippedOutputs: { ...this.skippedOutputs } },
|
|
35846
36772
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
35847
36773
|
lastModified: Date.now()
|
|
35848
36774
|
};
|
|
@@ -35855,7 +36781,8 @@ var DeployEngine = class {
|
|
|
35855
36781
|
if (outputsChanged) this.logger.info("Persisted Outputs-only change (no resource diff).");
|
|
35856
36782
|
else this.logger.debug("Persisted export-set change (no outputs-value diff, no-change path, #2193)");
|
|
35857
36783
|
if (this.exportIndexStore) await this.exportIndexStore.updateForStack(stackName, this.stackRegion, importableOutputs(refreshedState));
|
|
35858
|
-
} else this.logger.debug("Persisted refreshed observedProperties (no-change path)");
|
|
36784
|
+
} else if (observedRefresh) this.logger.debug("Persisted refreshed observedProperties (no-change path)");
|
|
36785
|
+
else this.logger.debug("Persisted skipped-outputs record (no outputs-value diff, no-change path, #2740)");
|
|
35859
36786
|
} catch (saveError) {
|
|
35860
36787
|
this.logger.warn(`Failed to persist no-change state update: ${saveError instanceof Error ? saveError.message : String(saveError)} — drift baseline / outputs will be re-resolved on next deploy.`);
|
|
35861
36788
|
}
|
|
@@ -35897,7 +36824,7 @@ var DeployEngine = class {
|
|
|
35897
36824
|
current: 0,
|
|
35898
36825
|
total: createChanges.length + updateChanges.length + deleteChanges.length
|
|
35899
36826
|
};
|
|
35900
|
-
const { state: newState, actualCounts } = await this.executeDeployment(effectiveTemplate, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending);
|
|
36827
|
+
const { state: newState, actualCounts } = await this.executeDeployment(effectiveTemplate, currentState, changes, dag, executionLevels, stackName, outputsDigestSource, parameterValues, conditions, currentEtag, progress, migrationPending);
|
|
35901
36828
|
await this.drainObservedCaptures(newState.resources);
|
|
35902
36829
|
const newEtag = await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(newState));
|
|
35903
36830
|
this.logger.debug(`State saved (ETag: ${newEtag})`);
|
|
@@ -35939,7 +36866,7 @@ var DeployEngine = class {
|
|
|
35939
36866
|
* - DELETE follows reverse dependency order (a node starts as soon as all
|
|
35940
36867
|
* resources that depend ON it have finished deleting)
|
|
35941
36868
|
*/
|
|
35942
|
-
async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
|
|
36869
|
+
async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, outputsDigestSource, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
|
|
35943
36870
|
const concurrency = this.options.concurrency;
|
|
35944
36871
|
const newResources = { ...currentState.resources };
|
|
35945
36872
|
const actualCounts = {
|
|
@@ -35965,6 +36892,7 @@ var DeployEngine = class {
|
|
|
35965
36892
|
resources: newResources,
|
|
35966
36893
|
outputs: currentState.outputs,
|
|
35967
36894
|
...exportNamesCarriedFrom(currentState),
|
|
36895
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
35968
36896
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
35969
36897
|
lastModified: Date.now()
|
|
35970
36898
|
};
|
|
@@ -36097,6 +37025,7 @@ var DeployEngine = class {
|
|
|
36097
37025
|
resources: newResources,
|
|
36098
37026
|
outputs: currentState.outputs,
|
|
36099
37027
|
...exportNamesCarriedFrom(currentState),
|
|
37028
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
36100
37029
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
36101
37030
|
lastModified: Date.now()
|
|
36102
37031
|
};
|
|
@@ -36133,6 +37062,7 @@ var DeployEngine = class {
|
|
|
36133
37062
|
resources: newResources,
|
|
36134
37063
|
outputs: currentState.outputs,
|
|
36135
37064
|
...exportNamesCarriedFrom(currentState),
|
|
37065
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
36136
37066
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
36137
37067
|
lastModified: Date.now()
|
|
36138
37068
|
};
|
|
@@ -36150,6 +37080,7 @@ var DeployEngine = class {
|
|
|
36150
37080
|
resources: newResources,
|
|
36151
37081
|
outputs: currentState.outputs,
|
|
36152
37082
|
...exportNamesCarriedFrom(currentState),
|
|
37083
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
36153
37084
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
36154
37085
|
lastModified: Date.now()
|
|
36155
37086
|
};
|
|
@@ -36164,7 +37095,7 @@ var DeployEngine = class {
|
|
|
36164
37095
|
}
|
|
36165
37096
|
let outputs;
|
|
36166
37097
|
try {
|
|
36167
|
-
outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
|
|
37098
|
+
outputs = await withSharedDrainBudget(() => this.resolveOutputs(template, newResources, stackName, outputsDigestSource, parameterValues, conditions));
|
|
36168
37099
|
const resolvedOutputsBeforeRedaction = outputs;
|
|
36169
37100
|
outputs = this.redactOutputs(outputs);
|
|
36170
37101
|
this.rememberRecoverableMaskedOutputs(stackName, resolvedOutputsBeforeRedaction, outputs);
|
|
@@ -36181,6 +37112,7 @@ var DeployEngine = class {
|
|
|
36181
37112
|
resources: newResources,
|
|
36182
37113
|
outputs,
|
|
36183
37114
|
exportNames: [...this.resolvedExportNames],
|
|
37115
|
+
...this.skippedOutputs && { skippedOutputs: { ...this.skippedOutputs } },
|
|
36184
37116
|
...this.recordedImports.length > 0 && { imports: [...this.recordedImports] },
|
|
36185
37117
|
...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
|
|
36186
37118
|
lastModified: Date.now()
|
|
@@ -36219,6 +37151,7 @@ var DeployEngine = class {
|
|
|
36219
37151
|
resources: newResources,
|
|
36220
37152
|
outputs: currentState.outputs,
|
|
36221
37153
|
...exportNamesCarriedFrom(currentState),
|
|
37154
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
36222
37155
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
36223
37156
|
lastModified: Date.now()
|
|
36224
37157
|
});
|
|
@@ -37360,9 +38293,16 @@ var DeployEngine = class {
|
|
|
37360
38293
|
* opposite sides of the same question. `secrets` is the outputs pass's own
|
|
37361
38294
|
* map: everything recorded before this handler runs, an `Export.Name`
|
|
37362
38295
|
* resolution's entries included (its `finally` merges them back before the
|
|
37363
|
-
* `catch` reaches here).
|
|
37364
|
-
*
|
|
37365
|
-
*
|
|
38296
|
+
* `catch` reaches here). Since issue #2563 a still-pending concurrent part
|
|
38297
|
+
* is in the PASS bag before this handler runs: the resolver drains every
|
|
38298
|
+
* part it started before a rejection reaches a caller. Not
|
|
38299
|
+
* unconditionally, and the weaker claim is the true one -- the drain is
|
|
38300
|
+
* bounded, and since the outputs pass wraps BOTH its loops in one budget
|
|
38301
|
+
* the bound spans the whole pass rather than one resolution: an early
|
|
38302
|
+
* failing output can leave a later `Export.Name` drain with no wait at all,
|
|
38303
|
+
* so a late record needs only a leg that had not finished recording by
|
|
38304
|
+
* then rather than one that outlived a full cap. (`inheritedSecrets` is the parent's, and no
|
|
38305
|
+
* resolution writes to it.)
|
|
37366
38306
|
*
|
|
37367
38307
|
* The strict arm's `cause` is masked as an OBJECT, through
|
|
37368
38308
|
* `maskSecretsInError` — a clone of each `Error` link `errorCauseChain`
|
|
@@ -37399,10 +38339,10 @@ var DeployEngine = class {
|
|
|
37399
38339
|
* rather than whatever the declaration order happened to have recorded by
|
|
37400
38340
|
* then (issue #1919).
|
|
37401
38341
|
*/
|
|
37402
|
-
async resolveOutputs(template, resources, stackName, parameterValues, conditions) {
|
|
38342
|
+
async resolveOutputs(template, resources, stackName, digestSource, parameterValues, conditions) {
|
|
37403
38343
|
this.resolvedExportNames = [];
|
|
37404
38344
|
if (!template.Outputs) return {};
|
|
37405
|
-
const outputs =
|
|
38345
|
+
const outputs = Object.create(null);
|
|
37406
38346
|
const context = this.buildResolverContext({
|
|
37407
38347
|
template,
|
|
37408
38348
|
resources,
|
|
@@ -37447,8 +38387,8 @@ var DeployEngine = class {
|
|
|
37447
38387
|
}
|
|
37448
38388
|
if (typeof exportName !== "string") continue;
|
|
37449
38389
|
const exposure = exportNameSecretExposure(exportName, nameSecrets, context.recordedSecretValues);
|
|
37450
|
-
if (exposure) this.logger.warn(secretBearingExportNameWarning(outputKey, exportName, exposure));
|
|
37451
|
-
else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) this.logger.warn(exportAliasCollisionWarning(outputKey, exportName));
|
|
38390
|
+
if (exposure) this.logger.warn(secretBearingExportNameWarning(outputKey, exportName, exposure, context.recordedSecretValues));
|
|
38391
|
+
else if (isExportAliasCollision(exportName, outputKey, publishedOutputNames)) this.logger.warn(exportAliasCollisionWarning(outputKey, exportName, outputsPassSecrets));
|
|
37452
38392
|
else {
|
|
37453
38393
|
outputs[exportName] = value;
|
|
37454
38394
|
if (!this.resolvedExportNames.includes(exportName)) this.resolvedExportNames.push(exportName);
|
|
@@ -37467,6 +38407,7 @@ var DeployEngine = class {
|
|
|
37467
38407
|
if (!publishedOutputNames.has(outputKey)) continue;
|
|
37468
38408
|
this.outputsTemplateSource[outputKey] = output.Value;
|
|
37469
38409
|
}
|
|
38410
|
+
this.skippedOutputs = collectSkippedOutputs(digestSource, outputs);
|
|
37470
38411
|
return markSameGenerationBag(outputs);
|
|
37471
38412
|
}
|
|
37472
38413
|
buildDisplayOutputs(template, resolvedOutputs) {
|
|
@@ -37481,5 +38422,5 @@ var DeployEngine = class {
|
|
|
37481
38422
|
};
|
|
37482
38423
|
|
|
37483
38424
|
//#endregion
|
|
37484
|
-
export {
|
|
37485
|
-
//# sourceMappingURL=deploy-engine-
|
|
38425
|
+
export { ProviderRegistry as $, partitionSensitiveEnv as $n, ResourceTimeoutError as $r, STATE_SOURCED_CROSS_GENERATION_RULES as $t, renderStatefulReason as A, buildAssetRedirectMap as An, derivePartitionAndUrlSuffix as Ar, configStringRefusal as At, red as B, parseBootstrapMarker as Bn, CdkdError as Br, s3BucketDomainName as Bt, refusesFinalSnapshot as C, exportNamesCarriedFrom as Cn, findLargeInlineResources as Cr, refStateLookupFromResource as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, AssetPublisher as Dn, expectedOwnerParam as Dr, assertRegionMatch as Dt, extractDeploymentEventError as E, shouldRetainResource as En, displaySafe as Er, resolveExplicitPhysicalId as Et, formatResourceLine as F, BOOTSTRAP_MARKER_PREFIX as Fn, AwsClients as Fr, requireConfigString as Ft, isExportAliasCollision as G, describeAwsFailure as Gn, DynamicReferenceRegionAmbiguousError as Gr, DiffCalculator as Gt, collectDeclaredOutputNames as H, validateAssetBucketName as Hn, CrossAccountSecretRefusalError as Hr, s3BucketRegionalDomainName as Ht, bold as I, assertAssetBucketRegion as In, getAwsClients as Ir, classifyReplaySecretRegion as It, secretSafeKeyDisplay as J, describeDockerExecFailure as Jn, LocalStartServiceError as Jr, findSilentDropProperties as Jt, secretBearing as K, buildDockerImage as Kn, IntrinsicResolutionRefusalError as Kr, INTRINSIC_KEYS as Kt, cyan as L, ensureAssetStorage as Ln, resetAwsClients as Lr, producerRegionsFromState as Lt, coerceWarmThroughput as M, loadPublishableAssetManifest as Mn, processStackMessages as Mr, replayWarn as Mt, isWarmThroughputDecrease as N, rewriteTemplateAssetReferences as Nn, clearBucketRegionCache as Nr, requireConfigArray as Nt, isStatefulRecreateTargetForReplace as O, stringifyValue as On, PARTITION_TABLE as Or, coerceCfnBoolean as Ot, toFiniteNumber as P, AssetModeResolver as Pn, resolveBucketRegion as Pr, requireConfigObject as Pt, clearOnUpdateRemoval as Q, getDockerCmd as Qn, ProvisioningError as Qr, TemplateParser as Qt, gray as R, getBootstrapMarkerKey as Rn, setAwsClients as Rr, withSharedDrainBudget as Rt, isFinalSnapshotError as S, DEFAULT_STATE_PREFIX as Sn, MIGRATE_TMP_PREFIX as Sr, parameterTypeMayLoseSecretIdentity as St, makeCanonicalizePropertiesFn as T, importableOutputs as Tn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as Tr, normalizeAwsTagsToCfn as Tt, collectPublishedOutputNames as U, validateContainerRepoName as Un, DependencyError as Ur, s3BucketWebsiteUrl as Ut, yellow as V, readBootstrapMarkerBody as Vn, ConfigError as Vr, s3BucketDualStackDomainName as Vt, exportAliasCollisionScrubWarning as W, buildDenyExternalAccessPolicy as Wn, DeployCancelledError as Wr, applyRoleArnIfSet as Wt, IAMRoleProvider as X, dockerSpawnEnvWithSensitive as Xn, NestedStackChildDirectDestroyError as Xr, withRetry as Xt, getCurrentResourceSecrets as Y, describeDockerFailure as Yn, LockError as Yr, describeTypeWithThrottleRetry as Yt, collectInlinePolicyNamesManagedBySiblings as Z, formatDockerLoginError as Zn, PartialFailureError as Zr, DagBuilder as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, buildForceUnlockCommand as _n, resolveUseCdkBootstrapAssets as _r, carriesDynamicReference as _t, DeploymentEventsStore as a, formatError as ai, errorCauseChain as an, AssetManifestLoader as ar, endCommandInterruptScope as at, ccRoutedFinalSnapshotError as b, shellQuote as bn, CFN_TEMPLATE_BODY_LIMIT as br, getAccountInfo as bt, replayFailedOperations as c, withErrorHandling as ci, maskSecretsInText as cn, synthesisStatusMessage as cr, startInterruptWatch as ct, updatePartialReason as d, isThrottlingError as di, redactSecretsForState as dn, resolveApp as dr, UNSPECIFIED_SKIP_REASON as dt, ResourceUpdateNotSupportedError as ei, STATE_SOURCED_READBACK_RULES as en, redactDockerArgvValues as er, wouldReturnToSdkProvider as et, withResourceDeadline as f, isTransientServerError as fi, scrubResourceRecord as fn, resolveAutoAssetStorage as fr, deleteIndeterminateGuards as ft, bindingSkippedOutputs as g, __exportAll as gi, UNRENDERABLE as gn, resolveStateBucketWithDefaultAndSource as gr, IntrinsicFunctionResolver as gt, computeImplicitDeleteEdges as h, retryClassificationText as hi, rebuildClientForBucketRegion as hn, resolveStateBucketWithDefault as hr, isTerminationProtectionPropagationError as ht, DeploymentEventsReader as i, SynthesisError as ii, dynamicReferenceTokens as in, stripControlChars as ir, beginCommandInterruptScope as it, WARM_THROUGHPUT_MEMBERS as j, createAssetRedirectResolver as jn, AssemblyReader as jr, readConfigString as jt, isStatefulRecreateTargetSync as k, WorkGraph as kn, canonicalizeRegion as kr, configBooleanRefusal as kt, replayRollback as l, isMarkedNonRetryable as li, recordMaskOnlyValue as ln, getDefaultStateBucketName as lr, CloudControlProvider as lt, IMPLICIT_DELETE_DEPENDENCIES as m, markRedactedCause as mi, S3StateBackend as mn, resolveSkipPrefix as mr, disableInstanceApiTermination as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, StackTerminationProtectionError as ni, carriesSecretMask as nn, runDockerStreaming as nr, maskDeep as nt, planFailedOps as o, isCdkdError as oi, isSingleDynamicReferenceToken as on, getDockerImageBySourceHash as or, interruptWatchListenerCount as ot, maskingRetryLogger as p, markNonRetryable as pi, LockManager as pn, resolveCaptureObservedState as pr, deleteSkipReason as pt, secretBearingStateKeyWarning as q, describeDockerCapturedOutput as qn, LocalInvokeBuildError as qr, findActionableSilentDrops as qt, DeployEngine as r, StateError as ri, createSecretMasker as rn, escapeRegExp$1 as rr, maskerOrIdentity as rt, planRollback as s, normalizeAwsError as si, maskSecretsInError as sn, Synthesizer as sr, isInterruptedWaitError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, StackHasActiveImportsError as ti, TEMPLATE_SOURCED_RULES as tn, runDockerForeground as tr, createMaskedRetryLogger as tt, updatePartialMessage as u, isRetryableTransientError as ui, recoverMaskedOutput as un, getLegacyStateBucketName as ur, slowCcOperationTimeoutMs as ut, PRE_DELETE_SNAPSHOT_TYPES as v, buildLockContentionMessage as vn, stateBucketExistenceConfirmed as vr, cfnRefValueFromPhysicalId as vt, unsupportedFinalSnapshotError as w, importableOutputKeys as wn, uploadCfnTemplate as wr, WAFv2WebACLProvider as wt, createPreDeleteFinalSnapshot as x, CUSTOM_RESOURCE_RESPONSE_PREFIX as xn, CFN_TEMPLATE_URL_LIMIT as xr, isUnboundTemplateParameter as xt, buildFinalSnapshotIdentifier as y, forceQuitRecoveryClause as yn, warnDeprecatedNoPrefixCliFlag as yr, coerceParameterTypedValue as yt, green as z, isCrossRegionRedirect as zn, AssetError as zr, s3BucketArn as zt };
|
|
38426
|
+
//# sourceMappingURL=deploy-engine-BiPxTgKT.js.map
|