@go-to-k/cdkd 0.288.2 → 0.288.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-DUlRn84u.js → asg-provider-CQrHElrl.js} +2 -2
- package/dist/{asg-provider-DUlRn84u.js.map → asg-provider-CQrHElrl.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-Cxt3gn8N.js → deploy-engine-CfxcC3q3.js} +524 -18
- package/dist/deploy-engine-CfxcC3q3.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-BRon74Fc.js → program-BXH3FeeV.js} +702 -74
- package/dist/{program-BRon74Fc.js.map → program-BXH3FeeV.js.map} +1 -1
- package/dist/{version-BRByznpy.js → version-CEyZvTcB.js} +2 -2
- package/dist/{version-BRByznpy.js.map → version-CEyZvTcB.js.map} +1 -1
- package/package.json +3 -13
- package/dist/deploy-engine-Cxt3gn8N.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
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-CEyZvTcB.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";
|
|
@@ -9101,6 +9101,18 @@ function importableOutputs(state) {
|
|
|
9101
9101
|
function exportNamesCarriedFrom(previous) {
|
|
9102
9102
|
return previous.exportNames === void 0 ? {} : { exportNames: previous.exportNames };
|
|
9103
9103
|
}
|
|
9104
|
+
/**
|
|
9105
|
+
* The `skippedOutputs` field to write when a save carries the PREVIOUS
|
|
9106
|
+
* record's `outputs` bag forward instead of re-resolving it (issue #2740). The
|
|
9107
|
+
* record describes that bag — which keys the deploy that wrote it could not
|
|
9108
|
+
* resolve — so it travels with the bag, exactly like {@link
|
|
9109
|
+
* exportNamesCarriedFrom}: absent stays absent, present stays as it was. A
|
|
9110
|
+
* writer that re-resolves outputs does NOT call this; it writes the set the
|
|
9111
|
+
* resolution produced, or omits the field when nothing was skipped.
|
|
9112
|
+
*/
|
|
9113
|
+
function skippedOutputsCarriedFrom(previous) {
|
|
9114
|
+
return previous.skippedOutputs === void 0 ? {} : { skippedOutputs: previous.skippedOutputs };
|
|
9115
|
+
}
|
|
9104
9116
|
|
|
9105
9117
|
//#endregion
|
|
9106
9118
|
//#region src/types/rollback-journal.ts
|
|
@@ -16687,6 +16699,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
16687
16699
|
"RoleArn"
|
|
16688
16700
|
]),
|
|
16689
16701
|
silentDrop: /* @__PURE__ */ new Map([
|
|
16702
|
+
["CapacityProviderConfiguration", "not yet implemented by cdkd"],
|
|
16690
16703
|
["FilesystemConfigurations", "not yet implemented by cdkd"],
|
|
16691
16704
|
["RequestHeaderConfiguration", "not yet implemented by cdkd"],
|
|
16692
16705
|
["Tags", "not yet implemented by cdkd"]
|
|
@@ -16805,7 +16818,8 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
16805
16818
|
silentDrop: /* @__PURE__ */ new Map([
|
|
16806
16819
|
["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
16820
|
["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"]
|
|
16821
|
+
["EvaluationWindow", "not yet implemented by cdkd"],
|
|
16822
|
+
["WarmUpConfiguration", "not yet implemented by cdkd"]
|
|
16809
16823
|
]),
|
|
16810
16824
|
createOnlyDrops: /* @__PURE__ */ new Set()
|
|
16811
16825
|
}],
|
|
@@ -17633,7 +17647,7 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
17633
17647
|
}],
|
|
17634
17648
|
["AWS::Glue::Connection", {
|
|
17635
17649
|
handled: /* @__PURE__ */ new Set(["CatalogId", "ConnectionInput"]),
|
|
17636
|
-
silentDrop: /* @__PURE__ */ new Map(),
|
|
17650
|
+
silentDrop: /* @__PURE__ */ new Map([["Tags", "not yet implemented by cdkd"]]),
|
|
17637
17651
|
createOnlyDrops: /* @__PURE__ */ new Set()
|
|
17638
17652
|
}],
|
|
17639
17653
|
["AWS::Glue::Crawler", {
|
|
@@ -18197,7 +18211,6 @@ const PROPERTY_COVERAGE_BY_TYPE = /* @__PURE__ */ new Map([
|
|
|
18197
18211
|
["UseLatestRestorableTime", "not yet implemented by cdkd"]
|
|
18198
18212
|
]),
|
|
18199
18213
|
createOnlyDrops: /* @__PURE__ */ new Set([
|
|
18200
|
-
"AvailabilityZones",
|
|
18201
18214
|
"ClusterScalabilityType",
|
|
18202
18215
|
"DBSystemId",
|
|
18203
18216
|
"EngineMode",
|
|
@@ -23693,6 +23706,11 @@ var IntrinsicFunctionResolver = class IntrinsicFunctionResolver {
|
|
|
23693
23706
|
case "ApiId": return physicalId;
|
|
23694
23707
|
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
23695
23708
|
}
|
|
23709
|
+
if (resourceType === "AWS::ApiGatewayV2::Api") switch (attributeName) {
|
|
23710
|
+
case "ExecuteApiArn": return `arn:${partition}:execute-api:${region}:${accountId}:${physicalId}`;
|
|
23711
|
+
case "ApiId": return physicalId;
|
|
23712
|
+
default: return this.guardedPhysicalIdFallback(logicalId, attributeName, resourceType, physicalId);
|
|
23713
|
+
}
|
|
23696
23714
|
if (resourceType === "AWS::ServiceDiscovery::PrivateDnsNamespace" || resourceType === "AWS::ServiceDiscovery::HttpNamespace" || resourceType === "AWS::ServiceDiscovery::PublicDnsNamespace") switch (attributeName) {
|
|
23697
23715
|
case "Arn": return `arn:${partition}:servicediscovery:${region}:${accountId}:namespace/${physicalId}`;
|
|
23698
23716
|
case "Id": return physicalId;
|
|
@@ -26642,7 +26660,7 @@ var CloudControlProvider = class {
|
|
|
26642
26660
|
const indeterminateGuard = await this.confirmDeleteTargetIdentity(logicalId, resourceType, physicalId, context);
|
|
26643
26661
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
26644
26662
|
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-
|
|
26663
|
+
const { ASGProvider } = await import("./asg-provider-CQrHElrl.js").then((n) => n.n);
|
|
26646
26664
|
const asgProvider = new ASGProvider();
|
|
26647
26665
|
return withIndeterminateGuard(await asgProvider.delete(logicalId, physicalId, resourceType, _properties, context), indeterminateGuard);
|
|
26648
26666
|
}
|
|
@@ -31240,6 +31258,67 @@ function secretBearingExportNameWarning(outputKey, exportName, exposure) {
|
|
|
31240
31258
|
return `Output ${stripControlChars(outputKey)} 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.`;
|
|
31241
31259
|
}
|
|
31242
31260
|
/**
|
|
31261
|
+
* Test `key` for recorded secret plaintext and return how it may be shown.
|
|
31262
|
+
*
|
|
31263
|
+
* Reuses {@link stateKeySecretExposure} and the same `maskEveryOccurrence` the
|
|
31264
|
+
* warnings below use, rather than restating either: two spellings of "is this
|
|
31265
|
+
* key safe to print" would disagree on the boundary cases those two encode
|
|
31266
|
+
* (the whole-key match for a sub-floor needle, longest-needle-first masking).
|
|
31267
|
+
*
|
|
31268
|
+
* SANITISED WITH BOTH helpers, because neither is a superset of the other —
|
|
31269
|
+
* measured, after a first cut swapped one for the other and silently traded
|
|
31270
|
+
* one class of character for another (issue #2667 review):
|
|
31271
|
+
*
|
|
31272
|
+
* | input | `stripControlChars` | `displaySafe` |
|
|
31273
|
+
* | -------- | ------------------- | ------------- |
|
|
31274
|
+
* | `U+200E` | removed | KEPT |
|
|
31275
|
+
* | `U+200F` | removed | KEPT |
|
|
31276
|
+
* | `U+2028` | KEPT | replaced |
|
|
31277
|
+
* | `U+2029` | KEPT | replaced |
|
|
31278
|
+
*
|
|
31279
|
+
* `U+2028` / `U+2029` matter because this text is PERSISTED and re-rendered by
|
|
31280
|
+
* JSON and web log viewers that treat both as line terminators — the CI-log
|
|
31281
|
+
* surface this masking exists to protect, where an `Fn::Sub`-built export name
|
|
31282
|
+
* carrying one could forge a log line (`display-safe.ts` states that
|
|
31283
|
+
* rationale). `U+200E` / `U+200F` are the bidi MARKS, named as residuals in
|
|
31284
|
+
* that same file; they reorder rendered text without terminating a line. On a
|
|
31285
|
+
* path whose subject is a possibly-secret-bearing name in an operator's log,
|
|
31286
|
+
* neither loss is worth taking, and composing costs nothing.
|
|
31287
|
+
*
|
|
31288
|
+
* ORDER IS LOAD-BEARING, and for the OVERLAP set — not for the marks, which
|
|
31289
|
+
* an earlier revision of this comment named and measurement contradicted.
|
|
31290
|
+
* `displaySafe` never touches `U+200E` / `U+200F`, so those give identical
|
|
31291
|
+
* output either way. Where the two classes OVERLAP — `U+0000`-`U+001F`,
|
|
31292
|
+
* `U+007F`-`U+009F`, `U+202A`-`U+202E`, `U+2066`-`U+2069` — `stripControlChars`
|
|
31293
|
+
* DELETES while `displaySafe` replaces with a space, so strip-then-display
|
|
31294
|
+
* yields `"ab"` and display-then-strip yields `"a b"`. Stripping first keeps a
|
|
31295
|
+
* name carrying them from being padded out.
|
|
31296
|
+
*
|
|
31297
|
+
* `displaySafe` also `.trim()`s, which `stripControlChars` alone did not: a
|
|
31298
|
+
* display-shape change for a key with leading or trailing whitespace. Stated
|
|
31299
|
+
* because it is a real difference, not hidden.
|
|
31300
|
+
*
|
|
31301
|
+
* The sibling warnings in this file still use `stripControlChars` ALONE and
|
|
31302
|
+
* carry the `U+2028` half of the gap; widening that helper, or converting
|
|
31303
|
+
* them, changes call sites this issue does not touch, so it is filed as issue
|
|
31304
|
+
* [#2874](https://github.com/go-to-k/cdkd/issues/2874) rather than done here.
|
|
31305
|
+
*/
|
|
31306
|
+
function secretSafeKeyDisplay(key, secrets) {
|
|
31307
|
+
const sanitise = (text) => displaySafe(stripControlChars(text));
|
|
31308
|
+
const shown = sanitise(key);
|
|
31309
|
+
const exposure = stateKeySecretExposure(shown, secrets) ?? stateKeySecretExposure(key, secrets);
|
|
31310
|
+
if (!exposure) return {
|
|
31311
|
+
kind: "safe",
|
|
31312
|
+
text: shown
|
|
31313
|
+
};
|
|
31314
|
+
const masked = sanitise(maskEveryOccurrence(shown, exposure));
|
|
31315
|
+
if (masked === shown) return { kind: "withheld" };
|
|
31316
|
+
return {
|
|
31317
|
+
kind: "masked",
|
|
31318
|
+
text: masked
|
|
31319
|
+
};
|
|
31320
|
+
}
|
|
31321
|
+
/**
|
|
31243
31322
|
* Warning for a state KEY that already holds secret plaintext — the residue an
|
|
31244
31323
|
* EARLIER binary left when it published an export name that resolved to one.
|
|
31245
31324
|
*
|
|
@@ -32682,6 +32761,397 @@ async function createReplicationGroupFinalSnapshot(client, replicationGroupId, l
|
|
|
32682
32761
|
});
|
|
32683
32762
|
}
|
|
32684
32763
|
|
|
32764
|
+
//#endregion
|
|
32765
|
+
//#region src/analyzer/skipped-outputs.ts
|
|
32766
|
+
/**
|
|
32767
|
+
* The record `cdkd deploy` leaves behind for an Output it could NOT resolve
|
|
32768
|
+
* and SKIPPED, and the digest `cdkd diff` compares it against (issue
|
|
32769
|
+
* [#2740](https://github.com/go-to-k/cdkd/issues/2740)).
|
|
32770
|
+
*
|
|
32771
|
+
* The deploy side (`DeployEngine.handleOutputResolutionFailure`, default arm)
|
|
32772
|
+
* warns, stores `undefined` for the key and moves on, so a re-resolved
|
|
32773
|
+
* `state.outputs` lacks it (the no-change path keeps the previous bag whole
|
|
32774
|
+
* when any output fails, so a key that resolved on an EARLIER deploy can keep
|
|
32775
|
+
* its value beside a record — the reader checks absence first and ignores the
|
|
32776
|
+
* record then). The diff side resolves outputs with `skipDynamicReferences`, so a
|
|
32777
|
+
* failure that happens INSIDE a secret lookup — a JSON key the secret does not
|
|
32778
|
+
* hold, a reference assembled from another secret's value — does not reproduce
|
|
32779
|
+
* there: the value assembles into its token, the key is absent from state, and
|
|
32780
|
+
* `computeOutputsDiff` pushed an `ADD` the deploy would never perform, on every
|
|
32781
|
+
* run of an unchanged stack. The diff cannot tell from assembly alone that the
|
|
32782
|
+
* deploy would fail (a composed string carrying a secret reference is a
|
|
32783
|
+
* legitimate output), so it learns what the deploy learned instead:
|
|
32784
|
+
* `StackState.skippedOutputs` maps each skipped key to a digest of the
|
|
32785
|
+
* template inputs its resolution read, and the diff trusts the record only
|
|
32786
|
+
* while that digest is unchanged.
|
|
32787
|
+
*
|
|
32788
|
+
* ## What the digest covers, and why
|
|
32789
|
+
*
|
|
32790
|
+
* `skippedOutputDigest` hashes the output's OWN entry (`Value`, `Export`,
|
|
32791
|
+
* `Condition`, whatever else it declares) together with every top-level
|
|
32792
|
+
* template section EXCEPT `Resources` and `Outputs`:
|
|
32793
|
+
*
|
|
32794
|
+
* - The entry itself: repairing the `Value` expression is the obvious repair.
|
|
32795
|
+
* The `Export.Name` is in it too, because the alias pass routes an
|
|
32796
|
+
* `Export.Name` failure through the same handler, which blanks the output's
|
|
32797
|
+
* own key even when its value resolved — so a repaired or removed export
|
|
32798
|
+
* name must invalidate the record as well.
|
|
32799
|
+
* - `Parameters` (a `Ref`-built reference repaired through a parameter
|
|
32800
|
+
* default), `Conditions` (an `Fn::If` whose branch flips), `Mappings` (an
|
|
32801
|
+
* `Fn::FindInMap`-built key), and the remaining sections for the same
|
|
32802
|
+
* reason, spelled as one rule rather than a list that drifts: everything an
|
|
32803
|
+
* output's resolution can read besides resources. One VALUE inside them is
|
|
32804
|
+
* excluded — the `Default` of a `NoEcho: true` parameter, hashed as a
|
|
32805
|
+
* constant; see `withNoEchoDefaultsMasked` for why. That is the POSITION,
|
|
32806
|
+
* not the parameter: its `AllowedValues`, and any literal an author puts in
|
|
32807
|
+
* `Conditions`, `Rules` or `Metadata`, are hashed as before.
|
|
32808
|
+
* - NOT `Resources`: hashing the section would invalidate the record on every
|
|
32809
|
+
* unrelated resource edit. The output CAN be repaired from that side —
|
|
32810
|
+
* `collectSkippedOutputs` records both skip arms, and the quiet one is an
|
|
32811
|
+
* `Fn::GetAtt` whose attribute `constructAttribute` could not build — so a
|
|
32812
|
+
* digest alone would keep binding while the next deploy publishes the row
|
|
32813
|
+
* and its `Export.Name`, the phantom's inverse. {@link
|
|
32814
|
+
* bindingSkippedOutputs} closes that with the CHANGE MAP rather than the
|
|
32815
|
+
* digest: a key whose entry references a logical id with a pending resource
|
|
32816
|
+
* change does not bind. The diff has that map (it ran the resource diff
|
|
32817
|
+
* first); the deploy does not need it, because a stack with any resource
|
|
32818
|
+
* change takes the path that re-resolves every output and rewrites the
|
|
32819
|
+
* record. REFERENCE, not repair: whether an edit could actually make the
|
|
32820
|
+
* output resolvable is undecidable from a template, so an unrelated edit to
|
|
32821
|
+
* a referenced resource un-binds too and the key can preview as an `ADD`
|
|
32822
|
+
* again. The cost is bounded — a run with a changed resource is already
|
|
32823
|
+
* reporting that row, so `--fail` exits 1 regardless — while the case it
|
|
32824
|
+
* buys depends on whether the diff can resolve the output at all. For an
|
|
32825
|
+
* output reading an attribute that does not exist yet — the SECOND skip arm
|
|
32826
|
+
* this record covers, not the secret lookup issue #2740 was filed on — it is
|
|
32827
|
+
* the VERDICT and not the row: unresolvable in
|
|
32828
|
+
* both readings, so bound the diff reports the outputs settled while the
|
|
32829
|
+
* next deploy is about to publish that key and its `Export.Name`. For an
|
|
32830
|
+
* output the diff CAN resolve, whose reference is to a name rather than to
|
|
32831
|
+
* a pending attribute (an `Fn::Sub` over an SSM parameter's name, say), the
|
|
32832
|
+
* row IS the difference and reappears as an `ADD` the moment the record
|
|
32833
|
+
* stops binding.
|
|
32834
|
+
* - NOT the sibling `Outputs`: a CloudFormation output cannot reference
|
|
32835
|
+
* another output, so a sibling change cannot repair this one, and excluding
|
|
32836
|
+
* them keeps an unrelated output edit from producing a one-run phantom.
|
|
32837
|
+
*
|
|
32838
|
+
* What the digest CANNOT see is a repair outside the template — the secret
|
|
32839
|
+
* gained the JSON key, the SSM parameter was created, a parameter VALUE
|
|
32840
|
+
* handed to the engine rather than declared changed (a nested stack's inputs
|
|
32841
|
+
* from its parent, `DeployEngineOptions.parameters`; `Parameters`
|
|
32842
|
+
* DECLARATIONS are hashed, supplied values are not), or **cdkd itself was
|
|
32843
|
+
* upgraded**: the quiet skip arm is `constructAttribute` returning nothing,
|
|
32844
|
+
* so a provider that gains that attribute repairs the output while neither
|
|
32845
|
+
* the template nor any resource moves.
|
|
32846
|
+
*
|
|
32847
|
+
* They are blind spots for three different reasons, and only the first is
|
|
32848
|
+
* about knowledge. The secret's key set and the SSM parameter's existence
|
|
32849
|
+
* need the external lookup the diff deliberately refuses to make. A supplied
|
|
32850
|
+
* parameter VALUE is not one of those — a nested stack's inputs are resolved
|
|
32851
|
+
* and forwarded to the child diff (`resolveChildStackParameters`) — it is
|
|
32852
|
+
* simply outside what the digest hashes, because hashing values would make
|
|
32853
|
+
* the record depend on a caller's arguments rather than on the template. And
|
|
32854
|
+
* the writing binary's version is knowable and merely unstored: keeping it
|
|
32855
|
+
* beside the digest and un-binding on mismatch would close that one. All four
|
|
32856
|
+
* end the same way: the record keeps binding until the next deploy re-resolves
|
|
32857
|
+
* the output, and that deploy publishes the key — unless a SIBLING output is
|
|
32858
|
+
* still unresolved on a run with no resource change, where the engine keeps
|
|
32859
|
+
* the previous outputs bag wholesale and the key stays unpublished for a
|
|
32860
|
+
* further run (go-to-k/cdkd#2771, pre-existing and not introduced here).
|
|
32861
|
+
* Documented as the accepted limitation in `docs/cli-diff.md`; it is narrower
|
|
32862
|
+
* than the pre-#2740 behaviour, where the diff was wrong on EVERY run.
|
|
32863
|
+
*
|
|
32864
|
+
* A repair on the RESOURCE side is NOT on that list either, but the reason is
|
|
32865
|
+
* narrower than it first looks. The change map above closes it only when the
|
|
32866
|
+
* repair arrives WITH a template resource change, which is what makes the
|
|
32867
|
+
* resource show on the diff at all. An OUT-OF-BAND state write does not:
|
|
32868
|
+
* every writer that rebuilds state outside a deploy DROPS the record rather
|
|
32869
|
+
* than carry it (one enumerated exception, the partial-destroy snapshot, is
|
|
32870
|
+
* explained with the field) — `cdkd import` refreshes `attributes` for the
|
|
32871
|
+
* resources it imports, `cdkd drift --accept` rewrites the properties an
|
|
32872
|
+
* attribute may be built from, `cdkd rollback`'s replacement arm rebuilds a
|
|
32873
|
+
* record with the attributes a FRESH create returned, and the rest are listed
|
|
32874
|
+
* with the field.
|
|
32875
|
+
* Dropping returns those keys to pre-#2740 behaviour until the next deploy
|
|
32876
|
+
* recomputes the record — a row where the diff can resolve them, the ordinary
|
|
32877
|
+
* whole-section suppression where it cannot — which is what keeps a
|
|
32878
|
+
* resource-side repair out of the list above.
|
|
32879
|
+
*
|
|
32880
|
+
* The rule is flat and it is meant to be: EVERY writer that rebuilds state
|
|
32881
|
+
* outside a deploy drops the record, the partial-destroy snapshot being the
|
|
32882
|
+
* one enumerated exception — the full list lives with the field, in
|
|
32883
|
+
* `StackState.skippedOutputs`. Three per-writer arguments for carrying it were
|
|
32884
|
+
* written during review, which is the reason for the flat rule rather than an
|
|
32885
|
+
* aside: two were shown wrong (substituting a value repairs an output whose
|
|
32886
|
+
* enclosing intrinsic was choking on what was there; scrubbing a plaintext
|
|
32887
|
+
* back to its expression rewrites a string outputs read verbatim) and the
|
|
32888
|
+
* third — deleting a property EXPOSING an attribute of the same name — could
|
|
32889
|
+
* not be settled either way, which argues for flatness at least as strongly.
|
|
32890
|
+
*
|
|
32891
|
+
* ## Where the digest must be taken from
|
|
32892
|
+
*
|
|
32893
|
+
* The template AS HANDED to the engine / to `computeStackDiff` — before
|
|
32894
|
+
* parameter binding, condition evaluation or any output resolution. The
|
|
32895
|
+
* INVARIANT, and the reason both callers `structuredClone` rather than pass
|
|
32896
|
+
* their live object: **no resolution may be visible to the digest, on either
|
|
32897
|
+
* side, and both sides must take it at the same point of their own flow.**
|
|
32898
|
+
* Two independent reasons, neither of which depends on any particular
|
|
32899
|
+
* resolver doing the writing: a digest that could see a resolved value would
|
|
32900
|
+
* (1) differ between the two sides, which computes a record the diff can
|
|
32901
|
+
* never re-derive, and (2) fingerprint a secret into `state.json`, since a
|
|
32902
|
+
* resolved value can be a decrypted one.
|
|
32903
|
+
*
|
|
32904
|
+
* The clone is therefore NOT dead weight even while no resolver mutates its
|
|
32905
|
+
* input. It used to be load-bearing against a live rewrite — `resolveSub`
|
|
32906
|
+
* substituted into the caller's two-argument `Fn::Sub` variable map until
|
|
32907
|
+
* go-to-k/cdkd#2764 gave it a fresh object — and the protection was never
|
|
32908
|
+
* derived from that call site: it is derived from the invariant above, which
|
|
32909
|
+
* a future in-place optimisation anywhere in either flow would silently
|
|
32910
|
+
* violate. `tests/unit/cli/diff-recursive-skipped-outputs.test.ts` pins the
|
|
32911
|
+
* current no-mutation contract from the other direction (the template handed
|
|
32912
|
+
* to `computeStackDiff` comes back byte-identical), so reintroducing a
|
|
32913
|
+
* rewrite reds a test rather than moving a digest;
|
|
32914
|
+
* `tests/unit/deployment/deploy-engine-skipped-outputs.test.ts` INJECTS a
|
|
32915
|
+
* rewrite through its resolver mock and requires the recorded digest to equal
|
|
32916
|
+
* a fresh parse's, which pins the ordering itself.
|
|
32917
|
+
*
|
|
32918
|
+
* Given that contract the hash covers template text only. It is over
|
|
32919
|
+
* canonical JSON (object keys sorted at every level), so a template
|
|
32920
|
+
* re-serialised with keys in another order digests identically.
|
|
32921
|
+
*
|
|
32922
|
+
* ## What the diff does with a binding record
|
|
32923
|
+
*
|
|
32924
|
+
* It previews the key as ABSENT — no row — because that is exactly what the
|
|
32925
|
+
* deploy will leave in state. It does NOT flag the section as failed: a
|
|
32926
|
+
* sibling output that genuinely changed still renders, and `--fail` still
|
|
32927
|
+
* exits 1 for it. A record whose digest no longer matches is simply ignored:
|
|
32928
|
+
* the output is previewed under the ordinary rules again (usually an `ADD`;
|
|
32929
|
+
* an intrinsic `Export.Name` the diff still cannot resolve keeps omitting the
|
|
32930
|
+
* section, as before), the next deploy re-decides it —
|
|
32931
|
+
* publishing it if the repair took, or recording it again under the new
|
|
32932
|
+
* digest — and the diff follows that decision.
|
|
32933
|
+
*/
|
|
32934
|
+
/**
|
|
32935
|
+
* JSON with object keys sorted at every level. Arrays keep their order (an
|
|
32936
|
+
* `Fn::Join` list is positional). `undefined` members are dropped, as
|
|
32937
|
+
* `JSON.stringify` drops them, so a parsed template and its in-memory twin
|
|
32938
|
+
* digest identically.
|
|
32939
|
+
*/
|
|
32940
|
+
function canonicalJson(value) {
|
|
32941
|
+
return JSON.stringify(value, (_key, v) => {
|
|
32942
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
32943
|
+
const sorted = Object.create(null);
|
|
32944
|
+
for (const k of Object.keys(v).sort()) sorted[k] = v[k];
|
|
32945
|
+
return sorted;
|
|
32946
|
+
}
|
|
32947
|
+
return v;
|
|
32948
|
+
});
|
|
32949
|
+
}
|
|
32950
|
+
/** The constant a `NoEcho` `Default` is hashed as. Its text is irrelevant; that
|
|
32951
|
+
* it is CONSTANT is the point. */
|
|
32952
|
+
const NO_ECHO_DEFAULT_MASK = "<cdkd:noecho-default>";
|
|
32953
|
+
/**
|
|
32954
|
+
* `Parameters` with every `NoEcho: true` parameter's `Default` replaced by a
|
|
32955
|
+
* constant, for hashing only.
|
|
32956
|
+
*
|
|
32957
|
+
* The pre-resolution snapshot already keeps RESOLVED secret values out of the
|
|
32958
|
+
* digest. An authored `Default` under `NoEcho: true` is not one of those — it
|
|
32959
|
+
* is template text, and it went into the hash. With the rest of the
|
|
32960
|
+
* non-`Resources` sections known, a reader of `state.json` holds a confirm
|
|
32961
|
+
* ORACLE for a low-entropy default: guess it, recompute, compare.
|
|
32962
|
+
*
|
|
32963
|
+
* This does NOT claim the value is otherwise absent from state — a parameter a
|
|
32964
|
+
* resource reads can persist its RESOLVED default in that resource's
|
|
32965
|
+
* properties, and parameter `NoEcho` is outside the state-redaction model
|
|
32966
|
+
* either way. What it claims is narrower and still worth having: this record
|
|
32967
|
+
* must not ADD an oracle of its own for the case where the value is not
|
|
32968
|
+
* already there.
|
|
32969
|
+
*
|
|
32970
|
+
* The cost, stated rather than hidden: changing ONLY such a default no longer
|
|
32971
|
+
* moves the digest, so the record keeps binding through that edit until the
|
|
32972
|
+
* next deploy re-resolves the output. That is the same bounded staleness the
|
|
32973
|
+
* documented blind spots carry.
|
|
32974
|
+
*
|
|
32975
|
+
* Everything else about the parameter is still hashed — its `Type`, its
|
|
32976
|
+
* `AllowedValues`, the `NoEcho` flag itself — so a parameter appearing,
|
|
32977
|
+
* disappearing or changing shape still un-binds. Unmatched parameter content
|
|
32978
|
+
* hashes exactly as before: the map is rebuilt, but the canonicaliser sorts
|
|
32979
|
+
* keys and ignores prototypes, so a template with no `NoEcho` default gets the
|
|
32980
|
+
* digest it always had.
|
|
32981
|
+
*/
|
|
32982
|
+
function withNoEchoDefaultsMasked(parameters) {
|
|
32983
|
+
if (parameters === null || typeof parameters !== "object" || Array.isArray(parameters)) return parameters;
|
|
32984
|
+
const masked = Object.create(null);
|
|
32985
|
+
for (const [name, decl] of Object.entries(parameters)) {
|
|
32986
|
+
const body = decl;
|
|
32987
|
+
masked[name] = typeof body === "object" && body !== null && body["NoEcho"] === true && "Default" in body ? {
|
|
32988
|
+
...body,
|
|
32989
|
+
Default: NO_ECHO_DEFAULT_MASK
|
|
32990
|
+
} : decl;
|
|
32991
|
+
}
|
|
32992
|
+
return masked;
|
|
32993
|
+
}
|
|
32994
|
+
/**
|
|
32995
|
+
* The digest recorded in `StackState.skippedOutputs[outputKey]` by the deploy
|
|
32996
|
+
* and recomputed by the diff — see the module doc for what it covers. Both
|
|
32997
|
+
* sides call THIS function, so the coverage is spelled once.
|
|
32998
|
+
*
|
|
32999
|
+
* `template` must be the pre-resolution snapshot the module doc describes.
|
|
33000
|
+
*/
|
|
33001
|
+
function skippedOutputDigest(template, outputKey) {
|
|
33002
|
+
const sections = Object.create(null);
|
|
33003
|
+
for (const [section, body] of Object.entries(template)) {
|
|
33004
|
+
if (section === "Resources" || section === "Outputs") continue;
|
|
33005
|
+
sections[section] = section === "Parameters" ? withNoEchoDefaultsMasked(body) : body;
|
|
33006
|
+
}
|
|
33007
|
+
const entry = template.Outputs?.[outputKey];
|
|
33008
|
+
return createHash("sha256").update(canonicalJson({
|
|
33009
|
+
output: entry,
|
|
33010
|
+
template: sections
|
|
33011
|
+
})).digest("hex");
|
|
33012
|
+
}
|
|
33013
|
+
/**
|
|
33014
|
+
* The record a deploy writes for the bag `resolveOutputs` produced: every key
|
|
33015
|
+
* whose value is `undefined`, mapped to its digest. Two arms write that value:
|
|
33016
|
+
* the default arm of `handleOutputResolutionFailure` (the resolver THREW —
|
|
33017
|
+
* warned, and refused under `--strict-getatt`), and a resolver that returned
|
|
33018
|
+
* `undefined` outright without throwing (an attribute it could construct
|
|
33019
|
+
* nothing for, `constructAttribute`'s empty arm — no warn, no strict refusal).
|
|
33020
|
+
* Both leave the key out of a re-resolved bag (the no-change path may keep a
|
|
33021
|
+
* previous value under it, see the module doc), and that path's
|
|
33022
|
+
* `resolutionFailed` treats both the same, so this record does too: the
|
|
33023
|
+
* question it answers is "will the next deploy leave this key absent?", and
|
|
33024
|
+
* the answer is yes either way. `undefined` (the field OMITTED) when nothing
|
|
33025
|
+
* was skipped, so a record that carries the field says at least one output
|
|
33026
|
+
* was skipped.
|
|
33027
|
+
*
|
|
33028
|
+
* `template` must be the pre-resolution snapshot (see the module doc).
|
|
33029
|
+
*/
|
|
33030
|
+
function collectSkippedOutputs(template, resolvedOutputs) {
|
|
33031
|
+
let record;
|
|
33032
|
+
for (const [outputKey, value] of Object.entries(resolvedOutputs)) {
|
|
33033
|
+
if (value !== void 0) continue;
|
|
33034
|
+
record ??= Object.create(null);
|
|
33035
|
+
record[outputKey] = skippedOutputDigest(template, outputKey);
|
|
33036
|
+
}
|
|
33037
|
+
return record;
|
|
33038
|
+
}
|
|
33039
|
+
/**
|
|
33040
|
+
* Every resource logical id an output entry could reference, from `Ref`,
|
|
33041
|
+
* either `Fn::GetAtt` spelling, and an `Fn::Sub` placeholder, walked to any
|
|
33042
|
+
* depth over the whole entry (`Value`, `Export.Name` and anything else it
|
|
33043
|
+
* declares).
|
|
33044
|
+
*
|
|
33045
|
+
* A deliberate SUPERSET: an `Fn::Sub` placeholder naming a template
|
|
33046
|
+
* PARAMETER, or a `Ref` to one, lands here too, since telling the two apart
|
|
33047
|
+
* needs the `Parameters` section and the answer only ever widens the set. The
|
|
33048
|
+
* one consumer intersects it with the ids that actually have a pending
|
|
33049
|
+
* RESOURCE change, so a name that is not a resource cannot match, and a
|
|
33050
|
+
* parameter deliberately named after a changed resource merely un-binds one
|
|
33051
|
+
* record — the direction that previews a row rather than hiding one. Pseudo
|
|
33052
|
+
* parameters (`AWS::…`) and the `${!Literal}` escape are dropped.
|
|
33053
|
+
*/
|
|
33054
|
+
function referencedLogicalIds(entry) {
|
|
33055
|
+
const ids = /* @__PURE__ */ new Set();
|
|
33056
|
+
const addName = (name) => {
|
|
33057
|
+
const logicalId = name.split(".")[0];
|
|
33058
|
+
if (logicalId !== void 0 && logicalId !== "" && !logicalId.startsWith("AWS::")) ids.add(logicalId);
|
|
33059
|
+
};
|
|
33060
|
+
const walk = (value) => {
|
|
33061
|
+
if (Array.isArray(value)) {
|
|
33062
|
+
for (const item of value) walk(item);
|
|
33063
|
+
return;
|
|
33064
|
+
}
|
|
33065
|
+
if (value === null || typeof value !== "object") return;
|
|
33066
|
+
const obj = value;
|
|
33067
|
+
if (typeof obj["Ref"] === "string") addName(obj["Ref"]);
|
|
33068
|
+
const getAtt = obj["Fn::GetAtt"];
|
|
33069
|
+
if (typeof getAtt === "string") addName(getAtt);
|
|
33070
|
+
else if (Array.isArray(getAtt) && typeof getAtt[0] === "string") addName(getAtt[0]);
|
|
33071
|
+
for (const [key, nested] of Object.entries(obj)) {
|
|
33072
|
+
if (key === "Fn::Sub") {
|
|
33073
|
+
walkSub(nested);
|
|
33074
|
+
continue;
|
|
33075
|
+
}
|
|
33076
|
+
walk(nested);
|
|
33077
|
+
}
|
|
33078
|
+
};
|
|
33079
|
+
/**
|
|
33080
|
+
* `Fn::Sub` in both spellings. Handled here rather than by the generic walk
|
|
33081
|
+
* because the two-argument form's second element is a VARIABLE MAP, not a
|
|
33082
|
+
* template fragment: its KEYS are variable names, so letting the generic
|
|
33083
|
+
* walk see `{ Ref: 'Unrelated' }` there would read a variable literally
|
|
33084
|
+
* named `Ref` as a `Ref` intrinsic. The map's VALUES are walked, since the
|
|
33085
|
+
* resolver resolves them.
|
|
33086
|
+
*/
|
|
33087
|
+
const walkSub = (sub) => {
|
|
33088
|
+
const isPair = Array.isArray(sub);
|
|
33089
|
+
const subTemplate = typeof sub === "string" ? sub : isPair ? sub[0] : void 0;
|
|
33090
|
+
const variableMap = isPair && sub[1] !== null && typeof sub[1] === "object" ? sub[1] : void 0;
|
|
33091
|
+
if (typeof subTemplate === "string") {
|
|
33092
|
+
const declaredVars = new Set(variableMap === void 0 ? [] : Object.keys(variableMap));
|
|
33093
|
+
for (const [, placeholder] of subTemplate.matchAll(/\$\{([^}]*)\}/g)) {
|
|
33094
|
+
if (placeholder === void 0 || placeholder.startsWith("!")) continue;
|
|
33095
|
+
if (declaredVars.has(placeholder)) continue;
|
|
33096
|
+
addName(placeholder);
|
|
33097
|
+
}
|
|
33098
|
+
}
|
|
33099
|
+
if (isPair) for (const [index, element] of sub.entries()) {
|
|
33100
|
+
if (index === 1) {
|
|
33101
|
+
if (variableMap !== void 0) for (const declared of Object.values(variableMap)) walk(declared);
|
|
33102
|
+
continue;
|
|
33103
|
+
}
|
|
33104
|
+
walk(element);
|
|
33105
|
+
}
|
|
33106
|
+
else walk(sub);
|
|
33107
|
+
};
|
|
33108
|
+
walk(entry);
|
|
33109
|
+
return ids;
|
|
33110
|
+
}
|
|
33111
|
+
/**
|
|
33112
|
+
* The keys of a `StackState.skippedOutputs` record that still BIND against
|
|
33113
|
+
* today's template: recorded digest equal to the digest of the same key over
|
|
33114
|
+
* `pristineTemplate`, which must be the pre-resolution snapshot the module doc
|
|
33115
|
+
* describes. A key the template no longer declares is skipped outright — the
|
|
33116
|
+
* record is inert for it, and the next deploy clears it. The diff passes the
|
|
33117
|
+
* result to `resolveTemplateOutputs`, which adds the one check only state can
|
|
33118
|
+
* answer — that the key is still absent from the stored bag.
|
|
33119
|
+
*
|
|
33120
|
+
* `changedLogicalIds` is the second half of the binding rule, and the one the
|
|
33121
|
+
* digest structurally cannot carry (see the module doc's `Resources` bullet):
|
|
33122
|
+
* the logical ids this run's resource diff reports as changing. An output
|
|
33123
|
+
* whose entry references one of them does NOT bind, because the deploy that
|
|
33124
|
+
* follows takes the changed-resources path, re-resolves every output, and may
|
|
33125
|
+
* publish the very key the record would have hidden. Omitted by a caller with
|
|
33126
|
+
* no resource diff in hand — every such caller today is a test asserting the
|
|
33127
|
+
* digest half alone.
|
|
33128
|
+
*/
|
|
33129
|
+
function bindingSkippedOutputs(pristineTemplate, record, changedLogicalIds) {
|
|
33130
|
+
const binding = /* @__PURE__ */ new Set();
|
|
33131
|
+
if (record === null || record === void 0) return binding;
|
|
33132
|
+
const declared = pristineTemplate.Outputs ?? {};
|
|
33133
|
+
for (const [outputKey, digest] of Object.entries(record)) {
|
|
33134
|
+
if (!Object.prototype.hasOwnProperty.call(declared, outputKey)) continue;
|
|
33135
|
+
const entry = declared[outputKey];
|
|
33136
|
+
if (digest !== skippedOutputDigest(pristineTemplate, outputKey)) continue;
|
|
33137
|
+
if (changedLogicalIds !== void 0 && [...referencedLogicalIds(entry)].some((id) => changedLogicalIds.has(id))) continue;
|
|
33138
|
+
binding.add(outputKey);
|
|
33139
|
+
}
|
|
33140
|
+
return binding;
|
|
33141
|
+
}
|
|
33142
|
+
/**
|
|
33143
|
+
* Whether two records — either possibly absent — describe the same skipped
|
|
33144
|
+
* set with the same digests. The no-change deploy path persists on a
|
|
33145
|
+
* difference here: it is the ONLY save trigger for this field on that path,
|
|
33146
|
+
* because a skipped output switches the outputs-changed trigger off.
|
|
33147
|
+
*/
|
|
33148
|
+
function skippedOutputsEqual(a, b) {
|
|
33149
|
+
const aKeys = a === null || a === void 0 ? [] : Object.keys(a);
|
|
33150
|
+
const bKeys = b === null || b === void 0 ? [] : Object.keys(b);
|
|
33151
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
33152
|
+
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a?.[k] === b?.[k]);
|
|
33153
|
+
}
|
|
33154
|
+
|
|
32685
33155
|
//#endregion
|
|
32686
33156
|
//#region src/analyzer/implicit-delete-deps.ts
|
|
32687
33157
|
/**
|
|
@@ -35198,8 +35668,17 @@ var DeployEngine = class {
|
|
|
35198
35668
|
* secret collapse onto whichever expression was recorded last, exactly as two
|
|
35199
35669
|
* resource properties did before #1904. Captured in `resolveOutputs` at the
|
|
35200
35670
|
* same point `outputSecrets` is accumulated. Reset per `deploy()`.
|
|
35671
|
+
*
|
|
35672
|
+
* Null-prototype for the same reason as the outputs bag `resolveOutputs`
|
|
35673
|
+
* builds two lines away: both are keyed by TEMPLATE-CONTROLLED output names,
|
|
35674
|
+
* so an output called `__proto__` would replace this bag's prototype and a
|
|
35675
|
+
* later lookup of a name it never stored would inherit from it. Only the
|
|
35676
|
+
* RESET in `deploy()` is pinned — reverting this initialiser alone survives
|
|
35677
|
+
* the suite, measured, because `deploy()` replaces the bag before anything
|
|
35678
|
+
* writes to it, so the initialiser matches the reset rather than standing as
|
|
35679
|
+
* a second guard.
|
|
35201
35680
|
*/
|
|
35202
|
-
outputsTemplateSource =
|
|
35681
|
+
outputsTemplateSource = Object.create(null);
|
|
35203
35682
|
/**
|
|
35204
35683
|
* The export aliases the last `resolveOutputs` pass WROTE into its bag
|
|
35205
35684
|
* (issue #2193) — exactly the keys `outputs[exportName] = value` landed on,
|
|
@@ -35212,6 +35691,18 @@ var DeployEngine = class {
|
|
|
35212
35691
|
*/
|
|
35213
35692
|
resolvedExportNames = [];
|
|
35214
35693
|
/**
|
|
35694
|
+
* The outputs the last `resolveOutputs` pass could NOT resolve and SKIPPED
|
|
35695
|
+
* (the resolver threw under the default arm of
|
|
35696
|
+
* `handleOutputResolutionFailure`, or returned `undefined` outright — see
|
|
35697
|
+
* `collectSkippedOutputs`), each mapped to the
|
|
35698
|
+
* digest `cdkd diff` compares against (issue #2740, `StackState.
|
|
35699
|
+
* skippedOutputs`). `undefined` when nothing was skipped, so the saves that
|
|
35700
|
+
* persist the resolved bag spread it as-is and the field stays omitted. Same
|
|
35701
|
+
* lifetime rule as `resolvedExportNames`: reset at the top of every
|
|
35702
|
+
* `resolveOutputs`, meaningful only right after that call returns.
|
|
35703
|
+
*/
|
|
35704
|
+
skippedOutputs;
|
|
35705
|
+
/**
|
|
35215
35706
|
* Whether {@link outputsTemplateSource} may be used to POSITION the outputs
|
|
35216
35707
|
* redaction. False once an outputs pass threw partway: the post-loop
|
|
35217
35708
|
* name pass never ran, so the bag holds only the alias keys written before
|
|
@@ -35298,8 +35789,9 @@ var DeployEngine = class {
|
|
|
35298
35789
|
this.perResourceTemplateProps = /* @__PURE__ */ new Map();
|
|
35299
35790
|
this.attemptedResolvedProps = /* @__PURE__ */ new Map();
|
|
35300
35791
|
this.outputSecrets = /* @__PURE__ */ new Map();
|
|
35301
|
-
this.outputsTemplateSource =
|
|
35792
|
+
this.outputsTemplateSource = Object.create(null);
|
|
35302
35793
|
this.outputsSourceUsable = true;
|
|
35794
|
+
this.skippedOutputs = void 0;
|
|
35303
35795
|
this.retainedOldOnReplacement = /* @__PURE__ */ new Set();
|
|
35304
35796
|
this.resolver.resetPhysicalIdFallbackCount();
|
|
35305
35797
|
return withStackName(stackName, () => this.doDeploy(stackName, template));
|
|
@@ -35783,6 +36275,10 @@ var DeployEngine = class {
|
|
|
35783
36275
|
} catch {}
|
|
35784
36276
|
this.kickOffAutoRefreshObservedProperties(currentState.resources);
|
|
35785
36277
|
this.logger.debug(`Template has ${Object.keys(template.Resources || {}).length} resources`);
|
|
36278
|
+
const outputsDigestSource = structuredClone({
|
|
36279
|
+
...template,
|
|
36280
|
+
Resources: {}
|
|
36281
|
+
});
|
|
35786
36282
|
const parameterValues = await this.resolver.resolveParameters(template, this.options.parameters, { ...this.options.inheritedSecrets && this.options.inheritedSecrets.size > 0 && { inheritedSecrets: this.options.inheritedSecrets } });
|
|
35787
36283
|
this.logger.debug(`Resolved ${Object.keys(parameterValues).length} parameters: ${Object.keys(parameterValues).join(", ")}`);
|
|
35788
36284
|
const context = this.buildResolverContext({
|
|
@@ -35826,7 +36322,7 @@ var DeployEngine = class {
|
|
|
35826
36322
|
this.logger.info("No changes detected. Stack is up to date.");
|
|
35827
36323
|
let persistedOutputs = currentState.outputs ?? {};
|
|
35828
36324
|
if (!this.options.dryRun) {
|
|
35829
|
-
const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, parameterValues, conditions));
|
|
36325
|
+
const resolvedOutputs = this.redactOutputs(await this.resolveOutputs(effectiveTemplate, currentState.resources, stackName, outputsDigestSource, parameterValues, conditions));
|
|
35830
36326
|
const resolutionFailed = Object.values(resolvedOutputs).some((v) => v === void 0);
|
|
35831
36327
|
const outputsChanged = !resolutionFailed && !outputMapsEqual(persistedOutputs, resolvedOutputs);
|
|
35832
36328
|
const currentEffectiveExports = new Set(importableOutputKeys(currentState));
|
|
@@ -35835,7 +36331,8 @@ var DeployEngine = class {
|
|
|
35835
36331
|
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
36332
|
const observedRefresh = this.observedCaptureTasks.size > 0;
|
|
35837
36333
|
if (observedRefresh) await this.drainObservedCaptures(currentState.resources);
|
|
35838
|
-
|
|
36334
|
+
const skippedOutputsChanged = !skippedOutputsEqual(currentState.skippedOutputs, this.skippedOutputs);
|
|
36335
|
+
if (observedRefresh || outputsChanged || exportSetChanged || skippedOutputsChanged) try {
|
|
35839
36336
|
const refreshedState = {
|
|
35840
36337
|
version: 9,
|
|
35841
36338
|
region: this.stackRegion,
|
|
@@ -35843,6 +36340,7 @@ var DeployEngine = class {
|
|
|
35843
36340
|
resources: currentState.resources,
|
|
35844
36341
|
outputs: outputsChanged ? resolvedOutputs : persistedOutputs,
|
|
35845
36342
|
...resolutionFailed ? exportNamesCarriedFrom(currentState) : { exportNames: [...this.resolvedExportNames] },
|
|
36343
|
+
...this.skippedOutputs && { skippedOutputs: { ...this.skippedOutputs } },
|
|
35846
36344
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
35847
36345
|
lastModified: Date.now()
|
|
35848
36346
|
};
|
|
@@ -35855,7 +36353,8 @@ var DeployEngine = class {
|
|
|
35855
36353
|
if (outputsChanged) this.logger.info("Persisted Outputs-only change (no resource diff).");
|
|
35856
36354
|
else this.logger.debug("Persisted export-set change (no outputs-value diff, no-change path, #2193)");
|
|
35857
36355
|
if (this.exportIndexStore) await this.exportIndexStore.updateForStack(stackName, this.stackRegion, importableOutputs(refreshedState));
|
|
35858
|
-
} else this.logger.debug("Persisted refreshed observedProperties (no-change path)");
|
|
36356
|
+
} else if (observedRefresh) this.logger.debug("Persisted refreshed observedProperties (no-change path)");
|
|
36357
|
+
else this.logger.debug("Persisted skipped-outputs record (no outputs-value diff, no-change path, #2740)");
|
|
35859
36358
|
} catch (saveError) {
|
|
35860
36359
|
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
36360
|
}
|
|
@@ -35897,7 +36396,7 @@ var DeployEngine = class {
|
|
|
35897
36396
|
current: 0,
|
|
35898
36397
|
total: createChanges.length + updateChanges.length + deleteChanges.length
|
|
35899
36398
|
};
|
|
35900
|
-
const { state: newState, actualCounts } = await this.executeDeployment(effectiveTemplate, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending);
|
|
36399
|
+
const { state: newState, actualCounts } = await this.executeDeployment(effectiveTemplate, currentState, changes, dag, executionLevels, stackName, outputsDigestSource, parameterValues, conditions, currentEtag, progress, migrationPending);
|
|
35901
36400
|
await this.drainObservedCaptures(newState.resources);
|
|
35902
36401
|
const newEtag = await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(newState));
|
|
35903
36402
|
this.logger.debug(`State saved (ETag: ${newEtag})`);
|
|
@@ -35939,7 +36438,7 @@ var DeployEngine = class {
|
|
|
35939
36438
|
* - DELETE follows reverse dependency order (a node starts as soon as all
|
|
35940
36439
|
* resources that depend ON it have finished deleting)
|
|
35941
36440
|
*/
|
|
35942
|
-
async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
|
|
36441
|
+
async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, outputsDigestSource, parameterValues, conditions, currentEtag, progress, migrationPending = false) {
|
|
35943
36442
|
const concurrency = this.options.concurrency;
|
|
35944
36443
|
const newResources = { ...currentState.resources };
|
|
35945
36444
|
const actualCounts = {
|
|
@@ -35965,6 +36464,7 @@ var DeployEngine = class {
|
|
|
35965
36464
|
resources: newResources,
|
|
35966
36465
|
outputs: currentState.outputs,
|
|
35967
36466
|
...exportNamesCarriedFrom(currentState),
|
|
36467
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
35968
36468
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
35969
36469
|
lastModified: Date.now()
|
|
35970
36470
|
};
|
|
@@ -36097,6 +36597,7 @@ var DeployEngine = class {
|
|
|
36097
36597
|
resources: newResources,
|
|
36098
36598
|
outputs: currentState.outputs,
|
|
36099
36599
|
...exportNamesCarriedFrom(currentState),
|
|
36600
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
36100
36601
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
36101
36602
|
lastModified: Date.now()
|
|
36102
36603
|
};
|
|
@@ -36133,6 +36634,7 @@ var DeployEngine = class {
|
|
|
36133
36634
|
resources: newResources,
|
|
36134
36635
|
outputs: currentState.outputs,
|
|
36135
36636
|
...exportNamesCarriedFrom(currentState),
|
|
36637
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
36136
36638
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
36137
36639
|
lastModified: Date.now()
|
|
36138
36640
|
};
|
|
@@ -36150,6 +36652,7 @@ var DeployEngine = class {
|
|
|
36150
36652
|
resources: newResources,
|
|
36151
36653
|
outputs: currentState.outputs,
|
|
36152
36654
|
...exportNamesCarriedFrom(currentState),
|
|
36655
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
36153
36656
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
36154
36657
|
lastModified: Date.now()
|
|
36155
36658
|
};
|
|
@@ -36164,7 +36667,7 @@ var DeployEngine = class {
|
|
|
36164
36667
|
}
|
|
36165
36668
|
let outputs;
|
|
36166
36669
|
try {
|
|
36167
|
-
outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
|
|
36670
|
+
outputs = await this.resolveOutputs(template, newResources, stackName, outputsDigestSource, parameterValues, conditions);
|
|
36168
36671
|
const resolvedOutputsBeforeRedaction = outputs;
|
|
36169
36672
|
outputs = this.redactOutputs(outputs);
|
|
36170
36673
|
this.rememberRecoverableMaskedOutputs(stackName, resolvedOutputsBeforeRedaction, outputs);
|
|
@@ -36181,6 +36684,7 @@ var DeployEngine = class {
|
|
|
36181
36684
|
resources: newResources,
|
|
36182
36685
|
outputs,
|
|
36183
36686
|
exportNames: [...this.resolvedExportNames],
|
|
36687
|
+
...this.skippedOutputs && { skippedOutputs: { ...this.skippedOutputs } },
|
|
36184
36688
|
...this.recordedImports.length > 0 && { imports: [...this.recordedImports] },
|
|
36185
36689
|
...this.recordedOutputReads.length > 0 && { outputReads: [...this.recordedOutputReads] },
|
|
36186
36690
|
lastModified: Date.now()
|
|
@@ -36219,6 +36723,7 @@ var DeployEngine = class {
|
|
|
36219
36723
|
resources: newResources,
|
|
36220
36724
|
outputs: currentState.outputs,
|
|
36221
36725
|
...exportNamesCarriedFrom(currentState),
|
|
36726
|
+
...skippedOutputsCarriedFrom(currentState),
|
|
36222
36727
|
...crossStackReadsForPartialSave(currentState, this.recordedImports, this.recordedOutputReads),
|
|
36223
36728
|
lastModified: Date.now()
|
|
36224
36729
|
});
|
|
@@ -37399,10 +37904,10 @@ var DeployEngine = class {
|
|
|
37399
37904
|
* rather than whatever the declaration order happened to have recorded by
|
|
37400
37905
|
* then (issue #1919).
|
|
37401
37906
|
*/
|
|
37402
|
-
async resolveOutputs(template, resources, stackName, parameterValues, conditions) {
|
|
37907
|
+
async resolveOutputs(template, resources, stackName, digestSource, parameterValues, conditions) {
|
|
37403
37908
|
this.resolvedExportNames = [];
|
|
37404
37909
|
if (!template.Outputs) return {};
|
|
37405
|
-
const outputs =
|
|
37910
|
+
const outputs = Object.create(null);
|
|
37406
37911
|
const context = this.buildResolverContext({
|
|
37407
37912
|
template,
|
|
37408
37913
|
resources,
|
|
@@ -37467,6 +37972,7 @@ var DeployEngine = class {
|
|
|
37467
37972
|
if (!publishedOutputNames.has(outputKey)) continue;
|
|
37468
37973
|
this.outputsTemplateSource[outputKey] = output.Value;
|
|
37469
37974
|
}
|
|
37975
|
+
this.skippedOutputs = collectSkippedOutputs(digestSource, outputs);
|
|
37470
37976
|
return markSameGenerationBag(outputs);
|
|
37471
37977
|
}
|
|
37472
37978
|
buildDisplayOutputs(template, resolvedOutputs) {
|
|
@@ -37481,5 +37987,5 @@ var DeployEngine = class {
|
|
|
37481
37987
|
};
|
|
37482
37988
|
|
|
37483
37989
|
//#endregion
|
|
37484
|
-
export {
|
|
37485
|
-
//# sourceMappingURL=deploy-engine-
|
|
37990
|
+
export { ProviderRegistry as $, redactDockerArgvValues as $n, ResourceUpdateNotSupportedError as $r, STATE_SOURCED_READBACK_RULES as $t, renderStatefulReason as A, createAssetRedirectResolver as An, AssemblyReader as Ar, configStringRefusal as At, red as B, readBootstrapMarkerBody as Bn, ConfigError as Br, s3BucketDualStackDomainName as Bt, refusesFinalSnapshot as C, importableOutputKeys as Cn, uploadCfnTemplate as Cr, refStateLookupFromResource as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, stringifyValue as Dn, PARTITION_TABLE as Dr, assertRegionMatch as Dt, extractDeploymentEventError as E, AssetPublisher as En, expectedOwnerParam as Er, resolveExplicitPhysicalId as Et, formatResourceLine as F, assertAssetBucketRegion as Fn, getAwsClients as Fr, requireConfigString as Ft, isExportAliasCollision as G, buildDockerImage as Gn, IntrinsicResolutionRefusalError as Gr, INTRINSIC_KEYS as Gt, collectDeclaredOutputNames as H, validateContainerRepoName as Hn, DependencyError as Hr, s3BucketWebsiteUrl as Ht, bold as I, ensureAssetStorage as In, resetAwsClients as Ir, classifyReplaySecretRegion as It, stateKeySecretExposure as J, describeDockerFailure as Jn, LockError as Jr, describeTypeWithThrottleRetry as Jt, secretBearingStateKeyWarning as K, describeDockerCapturedOutput as Kn, LocalInvokeBuildError as Kr, findActionableSilentDrops as Kt, cyan as L, getBootstrapMarkerKey as Ln, setAwsClients as Lr, producerRegionsFromState as Lt, coerceWarmThroughput as M, rewriteTemplateAssetReferences as Mn, clearBucketRegionCache as Mr, replayWarn as Mt, isWarmThroughputDecrease as N, AssetModeResolver as Nn, resolveBucketRegion as Nr, requireConfigArray as Nt, isStatefulRecreateTargetForReplace as O, WorkGraph as On, canonicalizeRegion as Or, coerceCfnBoolean as Ot, toFiniteNumber as P, BOOTSTRAP_MARKER_PREFIX as Pn, AwsClients as Pr, requireConfigObject as Pt, clearOnUpdateRemoval as Q, partitionSensitiveEnv as Qn, ResourceTimeoutError as Qr, STATE_SOURCED_CROSS_GENERATION_RULES as Qt, gray as R, isCrossRegionRedirect as Rn, AssetError as Rr, s3BucketArn as Rt, isFinalSnapshotError as S, exportNamesCarriedFrom as Sn, findLargeInlineResources as Sr, parameterTypeMayLoseSecretIdentity as St, makeCanonicalizePropertiesFn as T, shouldRetainResource as Tn, displaySafe as Tr, normalizeAwsTagsToCfn as Tt, collectPublishedOutputNames as U, buildDenyExternalAccessPolicy as Un, DeployCancelledError as Ur, applyRoleArnIfSet as Ut, yellow as V, validateAssetBucketName as Vn, CrossAccountSecretRefusalError as Vr, s3BucketRegionalDomainName as Vt, exportAliasCollisionScrubWarning as W, describeAwsFailure as Wn, DynamicReferenceRegionAmbiguousError as Wr, DiffCalculator as Wt, IAMRoleProvider as X, formatDockerLoginError as Xn, PartialFailureError as Xr, DagBuilder as Xt, getCurrentResourceSecrets as Y, dockerSpawnEnvWithSensitive as Yn, NestedStackChildDirectDestroyError as Yr, withRetry as Yt, collectInlinePolicyNamesManagedBySiblings as Z, getDockerCmd as Zn, ProvisioningError as Zr, TemplateParser as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, buildLockContentionMessage as _n, stateBucketExistenceConfirmed as _r, carriesDynamicReference as _t, DeploymentEventsStore as a, isCdkdError as ai, isSingleDynamicReferenceToken as an, getDockerImageBySourceHash as ar, endCommandInterruptScope as at, ccRoutedFinalSnapshotError as b, CUSTOM_RESOURCE_RESPONSE_PREFIX as bn, CFN_TEMPLATE_URL_LIMIT as br, getAccountInfo as bt, replayFailedOperations as c, isMarkedNonRetryable as ci, recordMaskOnlyValue as cn, getDefaultStateBucketName as cr, startInterruptWatch as ct, updatePartialReason as d, isTransientServerError as di, scrubResourceRecord as dn, resolveAutoAssetStorage as dr, UNSPECIFIED_SKIP_REASON as dt, StackHasActiveImportsError as ei, TEMPLATE_SOURCED_RULES as en, runDockerForeground as er, wouldReturnToSdkProvider as et, withResourceDeadline as f, markNonRetryable as fi, LockManager as fn, resolveCaptureObservedState as fr, deleteIndeterminateGuards as ft, bindingSkippedOutputs as g, buildForceUnlockCommand as gn, resolveUseCdkBootstrapAssets as gr, IntrinsicFunctionResolver as gt, computeImplicitDeleteEdges as h, __exportAll as hi, UNRENDERABLE as hn, resolveStateBucketWithDefaultAndSource as hr, isTerminationProtectionPropagationError as ht, DeploymentEventsReader as i, formatError as ii, errorCauseChain as in, AssetManifestLoader as ir, beginCommandInterruptScope as it, WARM_THROUGHPUT_MEMBERS as j, loadPublishableAssetManifest as jn, processStackMessages as jr, readConfigString as jt, isStatefulRecreateTargetSync as k, buildAssetRedirectMap as kn, derivePartitionAndUrlSuffix as kr, configBooleanRefusal as kt, replayRollback as l, isRetryableTransientError as li, recoverMaskedOutput as ln, getLegacyStateBucketName as lr, CloudControlProvider as lt, IMPLICIT_DELETE_DEPENDENCIES as m, retryClassificationText as mi, rebuildClientForBucketRegion as mn, resolveStateBucketWithDefault as mr, disableInstanceApiTermination as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, StateError as ni, createSecretMasker as nn, escapeRegExp$1 as nr, maskDeep as nt, planFailedOps as o, normalizeAwsError as oi, maskSecretsInError as on, Synthesizer as or, interruptWatchListenerCount as ot, maskingRetryLogger as p, markRedactedCause as pi, S3StateBackend as pn, resolveSkipPrefix as pr, deleteSkipReason as pt, secretSafeKeyDisplay as q, describeDockerExecFailure as qn, LocalStartServiceError as qr, findSilentDropProperties as qt, DeployEngine as r, SynthesisError as ri, dynamicReferenceTokens as rn, stripControlChars as rr, maskerOrIdentity as rt, planRollback as s, withErrorHandling as si, maskSecretsInText as sn, synthesisStatusMessage as sr, isInterruptedWaitError as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, StackTerminationProtectionError as ti, carriesSecretMask as tn, runDockerStreaming as tr, createMaskedRetryLogger as tt, updatePartialMessage as u, isThrottlingError as ui, redactSecretsForState as un, resolveApp as ur, slowCcOperationTimeoutMs as ut, PRE_DELETE_SNAPSHOT_TYPES as v, forceQuitRecoveryClause as vn, warnDeprecatedNoPrefixCliFlag as vr, cfnRefValueFromPhysicalId as vt, unsupportedFinalSnapshotError as w, importableOutputs as wn, CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION as wr, WAFv2WebACLProvider as wt, createPreDeleteFinalSnapshot as x, DEFAULT_STATE_PREFIX as xn, MIGRATE_TMP_PREFIX as xr, isUnboundTemplateParameter as xt, buildFinalSnapshotIdentifier as y, shellQuote as yn, CFN_TEMPLATE_BODY_LIMIT as yr, coerceParameterTypedValue as yt, green as z, parseBootstrapMarker as zn, CdkdError as zr, s3BucketDomainName as zt };
|
|
37991
|
+
//# sourceMappingURL=deploy-engine-CfxcC3q3.js.map
|