@go-to-k/cdkd 0.283.26 → 0.283.27
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-CshsD-Pi.js → asg-provider-uNJDVURJ.js} +2 -2
- package/dist/{asg-provider-CshsD-Pi.js.map → asg-provider-uNJDVURJ.js.map} +1 -1
- package/dist/cli.js +414 -24
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-B6z9CCAL.js → deploy-engine-CsvKYG0d.js} +29 -21
- package/dist/deploy-engine-CsvKYG0d.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-B6z9CCAL.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as coerceCfnBoolean, $t as
|
|
2
|
+
import { $ as coerceCfnBoolean, $t as resolveApp, A as cyan, An as LocalInvokeBuildError$1, At as createAssetRedirectResolver, B as findSilentDropProperties, Bn as StackHasActiveImportsError, Bt as validateContainerRepoName, C as makeCanonicalizePropertiesFn, Cn as resetAwsClients, Ct as S3StateBackend, D as renderStatefulReason, Dt as stringifyValue, E as isStatefulRecreateTargetSync, En as CdkdError, Et as AssetPublisher, F as IAMRoleProvider, Fn as NestedStackChildDirectDestroyError, Ft as BOOTSTRAP_MARKER_PREFIX, G as IntrinsicFunctionResolver, Gt as runDockerForeground, H as slowCcOperationTimeoutMs, Ht as buildDockerImage, I as collectInlinePolicyNamesManagedBySiblings, In as PartialFailureError, It as ensureAssetStorage, J as refStateLookupFromResource, Jn as isMarkedNonRetryable, Jt as getDockerImageBySourceHash, K as cfnRefValueFromPhysicalId, Kn as normalizeAwsError, Kt as runDockerStreaming, L as clearOnUpdateRemoval, Ln as ProvisioningError, Lt as getBootstrapMarkerKey, M as green, Mn as LocalStartServiceError, Mt as rewriteTemplateAssetReferences, N as red, Nt as escapeRegExp, O as formatResourceLine, Ot as WorkGraph, P as yellow, Pn as MissingCdkCliError, Pt as AssetModeResolver, Q as assertRegionMatch, Qt as getLegacyStateBucketName, R as ProviderRegistry, Rn as ResourceTimeoutError, Rt as parseBootstrapMarker, S as unsupportedFinalSnapshotError, Sn as getAwsClients, St as LockManager, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tt as shouldRetainResource, U as disableInstanceApiTermination, Ut as formatDockerLoginError, V as CloudControlProvider, Vn as StackTerminationProtectionError, Vt as buildDenyExternalAccessPolicy, W as isTerminationProtectionPropagationError, Wt as getDockerCmd, X as normalizeAwsTagsToCfn, Xn as isThrottlingError, Xt as synthesisStatusMessage, Y as WAFv2WebACLProvider, Yn as isRetryableTransientError, Yt as Synthesizer, Z as resolveExplicitPhysicalId, Zn as markNonRetryable, Zt as getDefaultStateBucketName, _ as buildFinalSnapshotIdentifier, _n as AssemblyReader, _t as INTRINSIC_KEYS, a as DeploymentEventsStore, an as resolveUseCdkBootstrapAssets, at as requireConfigObject, b as isFinalSnapshotError, bn as resolveBucketRegion, bt as DagBuilder, c as replayFailedOperations, cn as CFN_TEMPLATE_BODY_LIMIT, ct as redactSecretsForState, d as deleteSkipReason, dn as findLargeInlineResources, dt as s3BucketDomainName, en as resolveAutoAssetStorage, et as configBooleanRefusal, f as withResourceDeadline, fn as uploadCfnTemplate, ft as s3BucketDualStackDomainName, g as PRE_DELETE_SNAPSHOT_TYPES, gn as derivePartitionAndUrlSuffix, gt as DiffCalculator, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as canonicalizeRegion, ht as applyRoleArnIfSet, i as DeploymentEventsReader, in as resolveStateBucketWithDefaultAndSource, it as requireConfigArray, j as gray, jn as LocalMigrateError, jt as loadPublishableAssetManifest, k as bold, kn as DeployCancelledError, kt as buildAssetRedirectMap, l as replayRollback, ln as CFN_TEMPLATE_URL_LIMIT, lt as scrubResourceRecord, m as computeImplicitDeleteEdges, mn as PARTITION_TABLE, mt as s3BucketWebsiteUrl, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as resolveSkipPrefix, nt as readConfigString, o as planFailedOps, on as stateBucketExistenceConfirmed, ot as requireConfigString, p as IMPLICIT_DELETE_DEPENDENCIES, pn as expectedOwnerParam, pt as s3BucketRegionalDomainName, q as getAccountInfo, qn as withErrorHandling, qt as AssetManifestLoader, r as DeployEngine, rn as resolveStateBucketWithDefault, rt as replayWarn, s as planRollback, sn as warnDeprecatedNoPrefixCliFlag, st as TEMPLATE_SOURCED_READBACK_RULES, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as resolveCaptureObservedState, tt as configStringRefusal, u as UNSPECIFIED_SKIP_REASON, un as MIGRATE_TMP_PREFIX, ut as s3BucketArn, v as ccRoutedFinalSnapshotError, vn as processStackMessages, vt as describeTypeWithThrottleRetry, w as extractDeploymentEventError, wn as setAwsClients, wt as rebuildClientForBucketRegion, x as refusesFinalSnapshot, xn as AwsClients, xt as TemplateParser, y as createPreDeleteFinalSnapshot, yt as withRetry, z as findActionableSilentDrops, zn as ResourceUpdateNotSupportedError, zt as validateAssetBucketName } from "./deploy-engine-CsvKYG0d.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-uNJDVURJ.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
|
|
@@ -56744,6 +56744,307 @@ function createDeployCommand() {
|
|
|
56744
56744
|
return cmd;
|
|
56745
56745
|
}
|
|
56746
56746
|
|
|
56747
|
+
//#endregion
|
|
56748
|
+
//#region src/analyzer/outputs-diff.ts
|
|
56749
|
+
/**
|
|
56750
|
+
* A `${...}` placeholder `Fn::Sub` did NOT substitute.
|
|
56751
|
+
*
|
|
56752
|
+
* `resolveSub` catches a genuine `Ref` / `Fn::GetAtt` miss, WARNS, and keeps the
|
|
56753
|
+
* literal `${Foo}` in the output string — it neither throws nor leaves an
|
|
56754
|
+
* intrinsic object behind. So a half-substituted `Fn::Sub` reaches this module
|
|
56755
|
+
* looking like an ordinary resolved string, and comparing it against state
|
|
56756
|
+
* reports a phantom change whose printed value is `"arn:...${NewBucket}"`.
|
|
56757
|
+
*
|
|
56758
|
+
* Only consulted for a value whose TEMPLATE source actually contained an
|
|
56759
|
+
* `Fn::Sub` (see {@link templateUsesSub}). Applying it to every string
|
|
56760
|
+
* over-matches badly: an IAM policy body with `${aws:username}`, a UserData
|
|
56761
|
+
* snippet with a shell `${VAR}`, or any literal a user wrote would set
|
|
56762
|
+
* `resolutionFailed` and suppress the WHOLE Outputs section for that stack, on
|
|
56763
|
+
* every run, forever.
|
|
56764
|
+
*
|
|
56765
|
+
* Within `Fn::Sub`-sourced values one false positive remains and is accepted:
|
|
56766
|
+
* `${!Literal}` is `Fn::Sub`'s ESCAPE and resolves to the literal text
|
|
56767
|
+
* `${Literal}`, which post-resolution is indistinguishable from a placeholder
|
|
56768
|
+
* that failed to substitute. That case merely SUPPRESSES the section (the
|
|
56769
|
+
* pre-#1921 behavior) whereas the alternative is a phantom reported every run.
|
|
56770
|
+
*/
|
|
56771
|
+
const UNSUBSTITUTED_SUB_PLACEHOLDER = /\$\{[^}]*\}/;
|
|
56772
|
+
/**
|
|
56773
|
+
* True when `value` is, or anywhere CONTAINS, something the diff could not
|
|
56774
|
+
* fully resolve against current state.
|
|
56775
|
+
*
|
|
56776
|
+
* The deploy engine's twin check is `Object.values(bag).some(v => v ===
|
|
56777
|
+
* undefined)` — its resolver THROWS on failure and the catch stores `undefined`.
|
|
56778
|
+
* The diff's resolver is best-effort and fails in three further ways that all
|
|
56779
|
+
* have to count as "unresolved" here, because anything that slips through is
|
|
56780
|
+
* compared against state and reported as a change the apply will never make:
|
|
56781
|
+
*
|
|
56782
|
+
* 1. `undefined` — the SAME signal the deploy side keys on. `resolve` returns it
|
|
56783
|
+
* WITHOUT throwing for a constructible-but-unknown attribute
|
|
56784
|
+
* (`AWS::DynamoDB::Table.StreamArn`, `AWS::IAM::Policy.PolicyId`,
|
|
56785
|
+
* `AWS::EC2::SecurityGroup.VpcId`, ...). `JSON.stringify` drops an
|
|
56786
|
+
* `undefined`-valued key, so state can never hold one and such an output
|
|
56787
|
+
* would otherwise report a PERMANENT phantom `ADD` printing `new: undefined`.
|
|
56788
|
+
* 2. A symbol — `Ref: AWS::NoValue` resolves to a sentinel symbol, which
|
|
56789
|
+
* `resolveValue` strips only INSIDE arrays / objects; a top-level
|
|
56790
|
+
* `Fn::If` selecting `AWS::NoValue` returns it bare. Not persistable, so
|
|
56791
|
+
* again a permanent phantom.
|
|
56792
|
+
* 3. A surviving intrinsic OBJECT, or — only when the template source used
|
|
56793
|
+
* `Fn::Sub` — an unsubstituted placeholder STRING (see
|
|
56794
|
+
* {@link UNSUBSTITUTED_SUB_PLACEHOLDER}).
|
|
56795
|
+
*
|
|
56796
|
+
* DEEP rather than shallow: an unresolved leaf can sit anywhere inside an array
|
|
56797
|
+
* or object an outer intrinsic already built.
|
|
56798
|
+
*/
|
|
56799
|
+
function isUnresolvedValue(value, sourceUsedSub) {
|
|
56800
|
+
if (value === void 0) return true;
|
|
56801
|
+
if (typeof value === "symbol") return true;
|
|
56802
|
+
if (typeof value === "string") return sourceUsedSub && UNSUBSTITUTED_SUB_PLACEHOLDER.test(value);
|
|
56803
|
+
if (value === null || typeof value !== "object") return false;
|
|
56804
|
+
if (Array.isArray(value)) return value.some((v) => isUnresolvedValue(v, sourceUsedSub));
|
|
56805
|
+
const keys = Object.keys(value);
|
|
56806
|
+
if (keys.length === 1 && INTRINSIC_KEYS.has(keys[0])) return true;
|
|
56807
|
+
return Object.values(value).some((v) => isUnresolvedValue(v, sourceUsedSub));
|
|
56808
|
+
}
|
|
56809
|
+
/**
|
|
56810
|
+
* True when this output's RAW template value contains an `Fn::Sub` anywhere —
|
|
56811
|
+
* including nested inside an `Fn::Join` / `Fn::If`, since those resolve their
|
|
56812
|
+
* arguments and a laundered placeholder from an inner `Fn::Sub` surfaces in the
|
|
56813
|
+
* outer result.
|
|
56814
|
+
*/
|
|
56815
|
+
function templateUsesSub(templateValue) {
|
|
56816
|
+
if (templateValue === null || typeof templateValue !== "object") return false;
|
|
56817
|
+
if (Array.isArray(templateValue)) return templateValue.some(templateUsesSub);
|
|
56818
|
+
const record = templateValue;
|
|
56819
|
+
if (Object.prototype.hasOwnProperty.call(record, "Fn::Sub")) return true;
|
|
56820
|
+
return Object.values(record).some(templateUsesSub);
|
|
56821
|
+
}
|
|
56822
|
+
/**
|
|
56823
|
+
* True when `value` is a string carrying a SECRET-BEARING CloudFormation
|
|
56824
|
+
* dynamic reference — `{{resolve:secretsmanager:` or `{{resolve:ssm-secure:`,
|
|
56825
|
+
* the two spellings that are secret regardless of what they point at.
|
|
56826
|
+
*
|
|
56827
|
+
* The diff resolves with `skipDynamicReferences`, so a secret-bearing output
|
|
56828
|
+
* arrives here as its unresolved `{{resolve:...}}` expression — which is also
|
|
56829
|
+
* what post-GHSA state stores. A state record written by an OLDER binary can
|
|
56830
|
+
* still hold the resolved PLAINTEXT instead (that mismatch is exactly what
|
|
56831
|
+
* `cdkd scrub` exists to repair), and printing it is this module's problem
|
|
56832
|
+
* because it is the first code path that DISPLAYS a stored output value.
|
|
56833
|
+
*/
|
|
56834
|
+
function isSecretDynamicReference(value) {
|
|
56835
|
+
if (typeof value !== "string") return false;
|
|
56836
|
+
return value.includes("{{resolve:secretsmanager:") || value.includes("{{resolve:ssm-secure:");
|
|
56837
|
+
}
|
|
56838
|
+
/** Structural equality, mirroring the deploy engine's `outputMapsEqual` leaf rule. */
|
|
56839
|
+
function valuesEqual(a, b) {
|
|
56840
|
+
if (a === b) return true;
|
|
56841
|
+
if (a == null || b == null) return a === b;
|
|
56842
|
+
if (typeof a !== typeof b) return false;
|
|
56843
|
+
if (typeof a !== "object") return false;
|
|
56844
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
56845
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
56846
|
+
return a.every((v, i) => valuesEqual(v, b[i]));
|
|
56847
|
+
}
|
|
56848
|
+
const ao = a;
|
|
56849
|
+
const bo = b;
|
|
56850
|
+
const ak = Object.keys(ao);
|
|
56851
|
+
if (ak.length !== Object.keys(bo).length) return false;
|
|
56852
|
+
for (const k of ak) {
|
|
56853
|
+
if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
|
|
56854
|
+
if (!valuesEqual(ao[k], bo[k])) return false;
|
|
56855
|
+
}
|
|
56856
|
+
return true;
|
|
56857
|
+
}
|
|
56858
|
+
/**
|
|
56859
|
+
* Resolve a template's `Outputs` section into the exact bag shape the deploy
|
|
56860
|
+
* engine persists to `StackState.outputs` (issue #1921).
|
|
56861
|
+
*
|
|
56862
|
+
* This is the preview twin of `DeployEngine.resolveOutputs`, and the three
|
|
56863
|
+
* semantics below are load-bearing for parity — a preview that builds a
|
|
56864
|
+
* DIFFERENT bag than the apply persists reports a phantom change on every run,
|
|
56865
|
+
* which is this issue's own bug class inverted. `tests/unit/analyzer/
|
|
56866
|
+
* outputs-diff.test.ts` fences each one against drift on the deploy side.
|
|
56867
|
+
*
|
|
56868
|
+
* 1. An output whose `Condition` evaluates false is SKIPPED, not resolved —
|
|
56869
|
+
* CloudFormation simply does not create it (mirrors the resource side's
|
|
56870
|
+
* `filterResourcesByCondition`; an unknown condition name is kept).
|
|
56871
|
+
* 2. An `Export.Name` is stored as a SECOND key holding the same value, so
|
|
56872
|
+
* `Fn::ImportValue` can find it by export name. Both keys land in state, so
|
|
56873
|
+
* both must land here.
|
|
56874
|
+
* 3. Resolution is best-effort and never throws — a diff must never harden into
|
|
56875
|
+
* an error where the deploy would have succeeded.
|
|
56876
|
+
*
|
|
56877
|
+
* The parity claim is scoped to the deploy engine's NO-CHANGE branch, which is
|
|
56878
|
+
* the one this preview stands in for. Its changed-resources branch resolves
|
|
56879
|
+
* outputs with no `resolutionFailed` gate at all — correctly, because by then
|
|
56880
|
+
* every resource exists and resolution is expected to succeed, whereas a diff
|
|
56881
|
+
* runs BEFORE any of them are created.
|
|
56882
|
+
*
|
|
56883
|
+
* Deliberately does NOT redact secrets the way the deploy side does: this
|
|
56884
|
+
* resolver runs with `skipDynamicReferences`, so a `{{resolve:...}}` reference
|
|
56885
|
+
* is never resolved to plaintext in the first place and the bag already holds
|
|
56886
|
+
* the expression that state stores post-redaction.
|
|
56887
|
+
*/
|
|
56888
|
+
async function resolveTemplateOutputs(template, resolveFn, conditions) {
|
|
56889
|
+
const logger = getLogger().child("OutputsDiff");
|
|
56890
|
+
const outputs = {};
|
|
56891
|
+
const exportNames = /* @__PURE__ */ new Set();
|
|
56892
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
56893
|
+
const secretSourceKeys = /* @__PURE__ */ new Set();
|
|
56894
|
+
let resolutionFailed = false;
|
|
56895
|
+
if (!template.Outputs) return {
|
|
56896
|
+
outputs,
|
|
56897
|
+
exportNames,
|
|
56898
|
+
failedKeys,
|
|
56899
|
+
secretSourceKeys,
|
|
56900
|
+
resolutionFailed
|
|
56901
|
+
};
|
|
56902
|
+
/**
|
|
56903
|
+
* Record a key the resolver DROPPED — plus the literal `Export.Name` alias the
|
|
56904
|
+
* deploy would have written alongside it. Both are absent from the bag, so
|
|
56905
|
+
* both would otherwise read as phantom REMOVEs and make the suppression
|
|
56906
|
+
* warning fire on the ordinary pending-resource case.
|
|
56907
|
+
*/
|
|
56908
|
+
const recordFailure = (outputKey, output) => {
|
|
56909
|
+
failedKeys.add(outputKey);
|
|
56910
|
+
if (typeof output.Export?.Name === "string") failedKeys.add(output.Export.Name);
|
|
56911
|
+
};
|
|
56912
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
56913
|
+
if (!isSecretDynamicReference(output.Value)) continue;
|
|
56914
|
+
secretSourceKeys.add(outputKey);
|
|
56915
|
+
if (typeof output.Export?.Name === "string") secretSourceKeys.add(output.Export.Name);
|
|
56916
|
+
}
|
|
56917
|
+
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
56918
|
+
if (output.Condition !== void 0 && conditions?.[output.Condition] === false) {
|
|
56919
|
+
logger.debug(`Skipping output ${outputKey} — condition ${output.Condition} is false`);
|
|
56920
|
+
continue;
|
|
56921
|
+
}
|
|
56922
|
+
const sourceUsedSub = templateUsesSub(output.Value);
|
|
56923
|
+
let value;
|
|
56924
|
+
try {
|
|
56925
|
+
value = await resolveFn(structuredClone(output.Value));
|
|
56926
|
+
} catch (error) {
|
|
56927
|
+
logger.debug(`Diff could not resolve output ${outputKey}: ${String(error)}`);
|
|
56928
|
+
resolutionFailed = true;
|
|
56929
|
+
recordFailure(outputKey, output);
|
|
56930
|
+
continue;
|
|
56931
|
+
}
|
|
56932
|
+
if (isUnresolvedValue(value, sourceUsedSub)) {
|
|
56933
|
+
logger.debug(`Diff left output ${outputKey} unresolved (references a pending resource)`);
|
|
56934
|
+
resolutionFailed = true;
|
|
56935
|
+
recordFailure(outputKey, output);
|
|
56936
|
+
continue;
|
|
56937
|
+
}
|
|
56938
|
+
outputs[outputKey] = value;
|
|
56939
|
+
if (output.Export?.Name) {
|
|
56940
|
+
let exportName = output.Export.Name;
|
|
56941
|
+
const exportSourceUsedSub = templateUsesSub(output.Export.Name);
|
|
56942
|
+
if (typeof exportName !== "string") try {
|
|
56943
|
+
exportName = await resolveFn(structuredClone(exportName));
|
|
56944
|
+
} catch (error) {
|
|
56945
|
+
logger.debug(`Diff could not resolve Export.Name of ${outputKey}: ${String(error)}`);
|
|
56946
|
+
resolutionFailed = true;
|
|
56947
|
+
continue;
|
|
56948
|
+
}
|
|
56949
|
+
if (typeof exportName === "string" && !isUnresolvedValue(exportName, exportSourceUsedSub)) {
|
|
56950
|
+
outputs[exportName] = value;
|
|
56951
|
+
exportNames.add(exportName);
|
|
56952
|
+
} else resolutionFailed = true;
|
|
56953
|
+
}
|
|
56954
|
+
}
|
|
56955
|
+
return {
|
|
56956
|
+
outputs,
|
|
56957
|
+
exportNames,
|
|
56958
|
+
failedKeys,
|
|
56959
|
+
secretSourceKeys,
|
|
56960
|
+
resolutionFailed
|
|
56961
|
+
};
|
|
56962
|
+
}
|
|
56963
|
+
/**
|
|
56964
|
+
* Compare the persisted Outputs bag against the one the next deploy would
|
|
56965
|
+
* write, key by key (issue #1921).
|
|
56966
|
+
*
|
|
56967
|
+
* Compares BAG KEYS rather than template output names on purpose. The bag is
|
|
56968
|
+
* what actually lands in `StackState.outputs` AND what
|
|
56969
|
+
* `ExportIndexStore.updateForStack` publishes, so a key-level comparison is
|
|
56970
|
+
* exactly the `outputMapsEqual` predicate the deploy engine gates its persist
|
|
56971
|
+
* on — no interpretive layer in between that could drift from apply semantics.
|
|
56972
|
+
* The practical payoff is that the motivating #875 case shows the user the
|
|
56973
|
+
* literal export name their downstream `Fn::ImportValue` needs.
|
|
56974
|
+
*
|
|
56975
|
+
* ## Withholding legacy secret plaintext
|
|
56976
|
+
*
|
|
56977
|
+
* This is the first code path that DISPLAYS a stored output value, and
|
|
56978
|
+
* `cdkd diff` is routinely run in CI — so a value that turns out to be secret
|
|
56979
|
+
* plaintext would land in build logs. `StackState.outputs` is only guaranteed
|
|
56980
|
+
* redacted for records written after the GHSA fix; an older binary persisted
|
|
56981
|
+
* the RESOLVED plaintext, which is the condition `cdkd scrub` repairs.
|
|
56982
|
+
*
|
|
56983
|
+
* Two independent signals identify such a record, and BOTH are needed:
|
|
56984
|
+
*
|
|
56985
|
+
* - the desired side is still a secret-bearing expression per
|
|
56986
|
+
* {@link isSecretDynamicReference} (this resolver runs with
|
|
56987
|
+
* `skipDynamicReferences`) while the stored side is not; and
|
|
56988
|
+
* - `secretSourceKeys` — the template itself declares the key's value as such a
|
|
56989
|
+
* reference. This one reaches cases the first cannot: a
|
|
56990
|
+
* condition-skipped secret output, or one deleted from the template, has NO
|
|
56991
|
+
* desired side at all and would otherwise print in full as a REMOVE row.
|
|
56992
|
+
*
|
|
56993
|
+
* A hit on either makes the WHOLE record suspect, not just that key: the
|
|
56994
|
+
* record was written by a pre-GHSA binary, so every value in it is unredacted.
|
|
56995
|
+
* Withholding is therefore record-level. The change is still REPORTED — only
|
|
56996
|
+
* the value is withheld — so `--fail` and the exports story are unaffected.
|
|
56997
|
+
*/
|
|
56998
|
+
function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /* @__PURE__ */ new Set()) {
|
|
56999
|
+
const changes = [];
|
|
57000
|
+
const currentBag = current ?? {};
|
|
57001
|
+
const legacyRecord = Object.entries(currentBag).some(([name, oldValue]) => {
|
|
57002
|
+
if (isSecretDynamicReference(oldValue)) return false;
|
|
57003
|
+
return isSecretDynamicReference(desired[name]) || secretSourceKeys.has(name);
|
|
57004
|
+
});
|
|
57005
|
+
const push = (change) => {
|
|
57006
|
+
if (legacyRecord && change.changeType !== "ADD") {
|
|
57007
|
+
const { oldValue: _dropped, ...rest } = change;
|
|
57008
|
+
changes.push({
|
|
57009
|
+
...rest,
|
|
57010
|
+
oldValueRedacted: true
|
|
57011
|
+
});
|
|
57012
|
+
return;
|
|
57013
|
+
}
|
|
57014
|
+
changes.push(change);
|
|
57015
|
+
};
|
|
57016
|
+
for (const [name, newValue] of Object.entries(desired)) {
|
|
57017
|
+
const isExport = exportNames.has(name);
|
|
57018
|
+
if (!Object.prototype.hasOwnProperty.call(currentBag, name)) {
|
|
57019
|
+
changes.push({
|
|
57020
|
+
name,
|
|
57021
|
+
changeType: "ADD",
|
|
57022
|
+
newValue,
|
|
57023
|
+
isExport
|
|
57024
|
+
});
|
|
57025
|
+
continue;
|
|
57026
|
+
}
|
|
57027
|
+
const oldValue = currentBag[name];
|
|
57028
|
+
if (!valuesEqual(oldValue, newValue)) push({
|
|
57029
|
+
name,
|
|
57030
|
+
changeType: "MODIFY",
|
|
57031
|
+
oldValue,
|
|
57032
|
+
newValue,
|
|
57033
|
+
isExport
|
|
57034
|
+
});
|
|
57035
|
+
}
|
|
57036
|
+
for (const [name, oldValue] of Object.entries(currentBag)) {
|
|
57037
|
+
if (Object.prototype.hasOwnProperty.call(desired, name)) continue;
|
|
57038
|
+
push({
|
|
57039
|
+
name,
|
|
57040
|
+
changeType: "REMOVE",
|
|
57041
|
+
oldValue,
|
|
57042
|
+
isExport: false
|
|
57043
|
+
});
|
|
57044
|
+
}
|
|
57045
|
+
return changes;
|
|
57046
|
+
}
|
|
57047
|
+
|
|
56747
57048
|
//#endregion
|
|
56748
57049
|
//#region src/cli/cfn-stack-states.ts
|
|
56749
57050
|
/**
|
|
@@ -57705,6 +58006,11 @@ async function loadStateOrEmpty(stackName, region, stateBackend) {
|
|
|
57705
58006
|
* resolver so changes buried inside intrinsics (e.g. `Fn::Join` literal
|
|
57706
58007
|
* args) are detected against resolved values in state.
|
|
57707
58008
|
*
|
|
58009
|
+
* Also computes the `Outputs` delta (issue #1921) — an Outputs-only change has
|
|
58010
|
+
* a byte-identical `Resources` section, so without it such a stack previews as
|
|
58011
|
+
* "No changes detected" while the apply persists new outputs and republishes
|
|
58012
|
+
* the exports index.
|
|
58013
|
+
*
|
|
57708
58014
|
* Pure with respect to AWS state mutation — only reads state (the resolver
|
|
57709
58015
|
* may read producer state for `Fn::ImportValue` / `Fn::GetStackOutput`).
|
|
57710
58016
|
*/
|
|
@@ -57749,7 +58055,16 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
57749
58055
|
...conditions && { conditions },
|
|
57750
58056
|
skipDynamicReferences: true
|
|
57751
58057
|
});
|
|
57752
|
-
|
|
58058
|
+
const changes = await diffCalculator.calculateDiff(currentState, effectiveTemplate, resolveFn, canonicalizeProperties);
|
|
58059
|
+
const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions);
|
|
58060
|
+
let outputChanges = [];
|
|
58061
|
+
if (resolved.resolutionFailed) {
|
|
58062
|
+
if (computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys).filter((change) => !resolved.failedKeys.has(change.name)).length > 0) logger.warn(`Outputs of stack ${stackName} may have changed, but one or more could not be resolved against current state — omitting the Outputs section from this diff. It is usually an output referencing a resource this deploy has yet to create.`);
|
|
58063
|
+
} else outputChanges = computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys);
|
|
58064
|
+
return {
|
|
58065
|
+
changes,
|
|
58066
|
+
outputChanges
|
|
58067
|
+
};
|
|
57753
58068
|
}
|
|
57754
58069
|
/**
|
|
57755
58070
|
* Resolve a nested-stack child's input `Parameters` (declared on the parent's
|
|
@@ -57812,12 +58127,14 @@ async function resolveChildStackParameters(parentStackRow, parentTemplate, paren
|
|
|
57812
58127
|
async function buildDiffTree(args) {
|
|
57813
58128
|
const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parameters, canonicalizeProperties, assetRedirect, cfnFallback } = args;
|
|
57814
58129
|
const state = await loadStateOrEmpty(stackName, region, stateBackend);
|
|
58130
|
+
const { changes, outputChanges } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, parameters, canonicalizeProperties, cfnFallback);
|
|
57815
58131
|
const node = {
|
|
57816
58132
|
stackName,
|
|
57817
58133
|
displayName,
|
|
57818
58134
|
region,
|
|
57819
|
-
changes
|
|
58135
|
+
changes,
|
|
57820
58136
|
ccApiRoutes: collectCcApiRoutes(template, state),
|
|
58137
|
+
outputChanges,
|
|
57821
58138
|
children: []
|
|
57822
58139
|
};
|
|
57823
58140
|
if (!recursive) return node;
|
|
@@ -57862,12 +58179,14 @@ async function buildDiffTree(args) {
|
|
|
57862
58179
|
*/
|
|
57863
58180
|
async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculator) {
|
|
57864
58181
|
const state = await loadStateOrEmpty(stackName, region, stateBackend);
|
|
58182
|
+
const { changes, outputChanges } = await computeStackDiff(state, EMPTY_TEMPLATE, region, stackName, stateBackend, diffCalculator);
|
|
57865
58183
|
const node = {
|
|
57866
58184
|
stackName,
|
|
57867
58185
|
displayName: stackName,
|
|
57868
58186
|
region,
|
|
57869
|
-
changes
|
|
58187
|
+
changes,
|
|
57870
58188
|
ccApiRoutes: /* @__PURE__ */ new Map(),
|
|
58189
|
+
outputChanges,
|
|
57871
58190
|
children: []
|
|
57872
58191
|
};
|
|
57873
58192
|
for (const [logicalId, resource] of Object.entries(state.resources)) {
|
|
@@ -57925,7 +58244,7 @@ function collectCcApiRoutes(template, state) {
|
|
|
57925
58244
|
/** True when this node has at least one real (non-`NO_CHANGE`) change. */
|
|
57926
58245
|
function nodeHasChanges(node) {
|
|
57927
58246
|
for (const change of node.changes.values()) if (change.changeType !== "NO_CHANGE") return true;
|
|
57928
|
-
return
|
|
58247
|
+
return node.outputChanges.length > 0;
|
|
57929
58248
|
}
|
|
57930
58249
|
/** True when this node OR any descendant has a real change (tree-wide drift detector for `--fail`). */
|
|
57931
58250
|
function treeHasChanges(node) {
|
|
@@ -57956,26 +58275,17 @@ function diffTreeToJson(node) {
|
|
|
57956
58275
|
stack: node.stackName,
|
|
57957
58276
|
region: node.region,
|
|
57958
58277
|
changes,
|
|
58278
|
+
outputChanges: node.outputChanges.map((change) => ({
|
|
58279
|
+
name: change.name,
|
|
58280
|
+
changeType: change.changeType,
|
|
58281
|
+
...change.changeType !== "ADD" && !change.oldValueRedacted ? { oldValue: change.oldValue } : {},
|
|
58282
|
+
...change.oldValueRedacted ? { oldValueRedacted: true } : {},
|
|
58283
|
+
...change.changeType !== "REMOVE" ? { newValue: change.newValue } : {},
|
|
58284
|
+
export: change.isExport
|
|
58285
|
+
})),
|
|
57959
58286
|
children: node.children.map(diffTreeToJson)
|
|
57960
58287
|
};
|
|
57961
58288
|
}
|
|
57962
|
-
const INTRINSIC_KEYS = /* @__PURE__ */ new Set([
|
|
57963
|
-
"Ref",
|
|
57964
|
-
"Fn::Sub",
|
|
57965
|
-
"Fn::GetAtt",
|
|
57966
|
-
"Fn::Join",
|
|
57967
|
-
"Fn::Select",
|
|
57968
|
-
"Fn::Split",
|
|
57969
|
-
"Fn::If",
|
|
57970
|
-
"Fn::ImportValue",
|
|
57971
|
-
"Fn::FindInMap",
|
|
57972
|
-
"Fn::Base64",
|
|
57973
|
-
"Fn::GetAZs",
|
|
57974
|
-
"Fn::Equals",
|
|
57975
|
-
"Fn::And",
|
|
57976
|
-
"Fn::Or",
|
|
57977
|
-
"Fn::Not"
|
|
57978
|
-
]);
|
|
57979
58289
|
function isIntrinsic(value) {
|
|
57980
58290
|
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return false;
|
|
57981
58291
|
const keys = Object.keys(value);
|
|
@@ -58111,6 +58421,84 @@ function renderChangeLines(changes, logFn, ccApiRoutes) {
|
|
|
58111
58421
|
delete: deleteCount
|
|
58112
58422
|
};
|
|
58113
58423
|
}
|
|
58424
|
+
/** Stand-in printed instead of a withheld legacy-plaintext output value. */
|
|
58425
|
+
const REDACTED_LEGACY_PLAINTEXT = "<redacted: legacy plaintext in state — run `cdkd scrub`>";
|
|
58426
|
+
/**
|
|
58427
|
+
* Strip characters that let a template-controlled string manipulate the
|
|
58428
|
+
* TERMINAL rather than merely appear in it: C0 (incl. ESC, so no ANSI sequence
|
|
58429
|
+
* survives), DEL, C1 (the 8-bit CSI forms some terminals still honor), and the
|
|
58430
|
+
* bidi / format overrides that visually reorder a rendered line.
|
|
58431
|
+
*
|
|
58432
|
+
* Applied only on the human-render path. The `--json` payload is deliberately
|
|
58433
|
+
* left byte-faithful: it is a machine interface, an export NAME is data a
|
|
58434
|
+
* consumer may match on, and silently mutating it there would trade a real
|
|
58435
|
+
* correctness regression for a display concern belonging to whatever renders
|
|
58436
|
+
* the JSON. `JSON.stringify` already escapes everything below 0x20.
|
|
58437
|
+
*/
|
|
58438
|
+
function stripControlChars(value) {
|
|
58439
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
58440
|
+
}
|
|
58441
|
+
/**
|
|
58442
|
+
* The same guard for an already-JSON-SERIALIZED value: C1 and the bidi marks
|
|
58443
|
+
* only, deliberately NOT C0.
|
|
58444
|
+
*
|
|
58445
|
+
* `JSON.stringify` escapes every character below 0x20 that occurs INSIDE a
|
|
58446
|
+
* string, so the only C0 left on this path is the pretty-printer's own
|
|
58447
|
+
* structural newlines — stripping those collapses a multi-line value onto a
|
|
58448
|
+
* single line. What `JSON.stringify` passes through unchanged is the C1 range
|
|
58449
|
+
* and the bidi overrides, and those are what this removes.
|
|
58450
|
+
*/
|
|
58451
|
+
function stripDisplayOnlyChars(value) {
|
|
58452
|
+
return value.replace(/[\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "");
|
|
58453
|
+
}
|
|
58454
|
+
/**
|
|
58455
|
+
* Render one node's Outputs delta (issue #1921) via `logFn`, returning the
|
|
58456
|
+
* per-kind counts. Emits nothing when there is no delta, so an unchanged
|
|
58457
|
+
* Outputs section adds no noise to a resource-only diff.
|
|
58458
|
+
*
|
|
58459
|
+
* Rows are keyed by the PERSISTED bag key, which is what `StackState.outputs`
|
|
58460
|
+
* holds and what the exports index publishes. An output carrying an
|
|
58461
|
+
* `Export.Name` therefore shows TWO rows — its logical name and its export
|
|
58462
|
+
* name — because the deploy writes both keys. That is the useful half for the
|
|
58463
|
+
* motivating case: the `[export]`-tagged row IS the string a downstream
|
|
58464
|
+
* `Fn::ImportValue` resolves, so the user can match it against the consumer
|
|
58465
|
+
* that was failing with "export not found".
|
|
58466
|
+
*/
|
|
58467
|
+
function renderOutputChangeLines(outputChanges, logFn) {
|
|
58468
|
+
const counts = {
|
|
58469
|
+
add: 0,
|
|
58470
|
+
change: 0,
|
|
58471
|
+
remove: 0
|
|
58472
|
+
};
|
|
58473
|
+
if (outputChanges.length === 0) return counts;
|
|
58474
|
+
logFn("\n Outputs:");
|
|
58475
|
+
const indent = " ";
|
|
58476
|
+
const render = (value) => stripDisplayOnlyChars((JSON.stringify(value, null, 2) ?? "undefined").replace(/\n/g, `\n${indent}`));
|
|
58477
|
+
const renderOld = (change) => change.oldValueRedacted ? REDACTED_LEGACY_PLAINTEXT : render(change.oldValue);
|
|
58478
|
+
for (const change of outputChanges) {
|
|
58479
|
+
const exported = change.isExport ? " [export]" : "";
|
|
58480
|
+
const name = stripControlChars(change.name);
|
|
58481
|
+
switch (change.changeType) {
|
|
58482
|
+
case "ADD":
|
|
58483
|
+
counts.add++;
|
|
58484
|
+
logFn(` [+] ${name}${exported}`);
|
|
58485
|
+
logFn(` new: ${render(change.newValue)}`);
|
|
58486
|
+
break;
|
|
58487
|
+
case "MODIFY":
|
|
58488
|
+
counts.change++;
|
|
58489
|
+
logFn(` [~] ${name}${exported}`);
|
|
58490
|
+
logFn(` old: ${renderOld(change)}`);
|
|
58491
|
+
logFn(` new: ${render(change.newValue)}`);
|
|
58492
|
+
break;
|
|
58493
|
+
case "REMOVE":
|
|
58494
|
+
counts.remove++;
|
|
58495
|
+
logFn(` [-] ${name}${exported}`);
|
|
58496
|
+
logFn(` old: ${renderOld(change)}`);
|
|
58497
|
+
break;
|
|
58498
|
+
}
|
|
58499
|
+
}
|
|
58500
|
+
return counts;
|
|
58501
|
+
}
|
|
58114
58502
|
/**
|
|
58115
58503
|
* Render a diff tree (root + nested children, DFS) via `logFn`. Only nodes
|
|
58116
58504
|
* that actually have changes get a block — unchanged nested children are
|
|
@@ -58123,7 +58511,9 @@ function renderDiffTree(node, isRoot, logFn) {
|
|
|
58123
58511
|
if (nodeHasChanges(node)) {
|
|
58124
58512
|
logFn(isRoot ? `\nStack ${node.stackName}:` : `\nNested stack: ${node.displayName}`);
|
|
58125
58513
|
const { create, update, delete: del } = renderChangeLines(node.changes, logFn, node.ccApiRoutes);
|
|
58514
|
+
const outputs = renderOutputChangeLines(node.outputChanges, logFn);
|
|
58126
58515
|
logFn(`\n${create} to create, ${update} to update, ${del} to delete`);
|
|
58516
|
+
if (outputs.add + outputs.change + outputs.remove > 0) logFn(`${outputs.add} output(s) to add, ${outputs.change} to change, ${outputs.remove} to remove`);
|
|
58127
58517
|
}
|
|
58128
58518
|
for (const child of node.children) renderDiffTree(child, false, logFn);
|
|
58129
58519
|
}
|
|
@@ -79390,7 +79780,7 @@ function createMigrateCommand() {
|
|
|
79390
79780
|
*/
|
|
79391
79781
|
function buildProgram() {
|
|
79392
79782
|
const program = new Command();
|
|
79393
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.
|
|
79783
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.27");
|
|
79394
79784
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
79395
79785
|
const { profile } = actionCommand.optsWithGlobals();
|
|
79396
79786
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|