@go-to-k/cdkd 0.283.32 → 0.283.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-CVoisQ0L.js → asg-provider-ID5-zAwf.js} +2 -2
- package/dist/{asg-provider-CVoisQ0L.js.map → asg-provider-ID5-zAwf.js.map} +1 -1
- package/dist/cli.js +492 -25
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-rYTNzst0.js → deploy-engine-Cg54H4pJ.js} +4 -4
- package/dist/{deploy-engine-rYTNzst0.js.map → deploy-engine-Cg54H4pJ.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as getAccountInfo, $t as
|
|
2
|
+
import { $ as getAccountInfo, $t as formatDockerLoginError, A as cyan, An as resolveBucketRegion, At as TemplateParser, B as stateKeySecretExposure, Bn as LocalInvokeBuildError$1, Bt as loadPublishableAssetManifest, C as makeCanonicalizePropertiesFn, Cn as expectedOwnerParam, Ct as s3BucketWebsiteUrl, D as renderStatefulReason, Dn as AssemblyReader, Dt as describeTypeWithThrottleRetry, E as isStatefulRecreateTargetSync, En as derivePartitionAndUrlSuffix, Et as INTRINSIC_KEYS, F as collectDeclaredOutputNames, Ft as AssetPublisher, G as findActionableSilentDrops, Gn as NestedStackChildDirectDestroyError, Gt as BOOTSTRAP_MARKER_PREFIX, H as collectInlinePolicyNamesManagedBySiblings, Hn as LocalStartServiceError, Ht as escapeRegExp, I as collectPublishedOutputNames, In as CdkdError, It as stringifyValue, J as slowCcOperationTimeoutMs, Jn as ResourceTimeoutError, Jt as parseBootstrapMarker, K as findSilentDropProperties, Kn as PartialFailureError, Kt as ensureAssetStorage, L as exportAliasCollisionScrubWarning, Lt as WorkGraph, M as green, Mn as getAwsClients, Mt as S3StateBackend, N as red, Nn as resetAwsClients, Nt as rebuildClientForBucketRegion, O as formatResourceLine, On as processStackMessages, Ot as withRetry, P as yellow, Pn as setAwsClients, Pt as shouldRetainResource, Q as cfnRefValueFromPhysicalId, Qt as buildDockerImage, R as isExportAliasCollision, Rt as buildAssetRedirectMap, S as unsupportedFinalSnapshotError, Sn as uploadCfnTemplate, St as s3BucketRegionalDomainName, T as MULTI_REGION_RECREATE_BLOCKED_TYPES, Tn as canonicalizeRegion, Tt as DiffCalculator, U as clearOnUpdateRemoval, Ut as stripControlChars, V as IAMRoleProvider, Vn as LocalMigrateError, Vt as rewriteTemplateAssetReferences, W as ProviderRegistry, Wn as MissingCdkCliError, Wt as AssetModeResolver, X as isTerminationProtectionPropagationError, Xn as StackHasActiveImportsError, Xt as validateContainerRepoName, Y as disableInstanceApiTermination, Yn as ResourceUpdateNotSupportedError, Yt as validateAssetBucketName, Z as IntrinsicFunctionResolver, Zn as StackTerminationProtectionError, Zt as buildDenyExternalAccessPolicy, _ as buildFinalSnapshotIdentifier, _n as warnDeprecatedNoPrefixCliFlag, _t as redactSecretsForState, a as DeploymentEventsStore, an as Synthesizer, ar as isRetryableTransientError, at as coerceCfnBoolean, b as isFinalSnapshotError, bn as MIGRATE_TMP_PREFIX, bt as s3BucketDomainName, c as replayFailedOperations, cn as getLegacyStateBucketName, ct as readConfigString, d as deleteSkipReason, dn as resolveCaptureObservedState, dt as requireConfigObject, en as getDockerCmd, et as refStateLookupFromResource, f as withResourceDeadline, fn as resolveSkipPrefix, ft as requireConfigString, g as PRE_DELETE_SNAPSHOT_TYPES, gn as stateBucketExistenceConfirmed, gt as maskSecretsInText, h as ATOMIC_FINAL_SNAPSHOT_TYPES, hn as resolveUseCdkBootstrapAssets, ht as TEMPLATE_SOURCED_RULES, i as DeploymentEventsReader, in as getDockerImageBySourceHash, ir as isMarkedNonRetryable, it as assertRegionMatch, j as gray, jn as AwsClients, jt as LockManager, k as bold, kt as DagBuilder, l as replayRollback, ln as resolveApp, lt as replayWarn, m as computeImplicitDeleteEdges, mn as resolveStateBucketWithDefaultAndSource, mt as STATE_SOURCED_READBACK_RULES, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as runDockerStreaming, nr as normalizeAwsError, nt as normalizeAwsTagsToCfn, o as planFailedOps, on as synthesisStatusMessage, or as isThrottlingError, ot as configBooleanRefusal, p as IMPLICIT_DELETE_DEPENDENCIES, pn as resolveStateBucketWithDefault, pt as STATE_SOURCED_CROSS_GENERATION_RULES, q as CloudControlProvider, qn as ProvisioningError, qt as getBootstrapMarkerKey, r as DeployEngine, rn as AssetManifestLoader, rr as withErrorHandling, rt as resolveExplicitPhysicalId, s as planRollback, sn as getDefaultStateBucketName, sr as markNonRetryable, st as configStringRefusal, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as runDockerForeground, tt as WAFv2WebACLProvider, u as UNSPECIFIED_SKIP_REASON, un as resolveAutoAssetStorage, ut as requireConfigArray, v as ccRoutedFinalSnapshotError, vn as CFN_TEMPLATE_BODY_LIMIT, vt as scrubResourceRecord, w as extractDeploymentEventError, wn as PARTITION_TABLE, wt as applyRoleArnIfSet, x as refusesFinalSnapshot, xn as findLargeInlineResources, xt as s3BucketDualStackDomainName, y as createPreDeleteFinalSnapshot, yn as CFN_TEMPLATE_URL_LIMIT, yt as s3BucketArn, z as secretBearingStateKeyWarning, zn as DeployCancelledError, zt as createAssetRedirectResolver } from "./deploy-engine-Cg54H4pJ.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-ID5-zAwf.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";
|
|
@@ -60644,6 +60644,325 @@ function createIamPrincipalUniqueIdResolver(awsClients) {
|
|
|
60644
60644
|
};
|
|
60645
60645
|
}
|
|
60646
60646
|
/**
|
|
60647
|
+
* Does any string anywhere in `value` carry a `{{resolve:...}}` dynamic
|
|
60648
|
+
* reference?
|
|
60649
|
+
*
|
|
60650
|
+
* Cheap pre-check so the overwhelming majority of resources — which reference
|
|
60651
|
+
* no secret at all — never build a resolver context and never pay a deep clone
|
|
60652
|
+
* of their property bag.
|
|
60653
|
+
*/
|
|
60654
|
+
function containsDynamicReference(value) {
|
|
60655
|
+
if (typeof value === "string") return value.includes("{{resolve:");
|
|
60656
|
+
if (Array.isArray(value)) return value.some(containsDynamicReference);
|
|
60657
|
+
if (value !== null && typeof value === "object") return Object.values(value).some(containsDynamicReference);
|
|
60658
|
+
return false;
|
|
60659
|
+
}
|
|
60660
|
+
/**
|
|
60661
|
+
* Byte-for-byte the threshold `secret-redaction.ts` applies when it builds its
|
|
60662
|
+
* substring needles (`MIN_NEEDLE_LENGTH`). Copied rather than imported because
|
|
60663
|
+
* that constant is module-private there and this file may not widen its
|
|
60664
|
+
* exports; the two must agree or this function would call a leaf secret-bearing
|
|
60665
|
+
* that `redactSecretsForState` will not redact.
|
|
60666
|
+
*/
|
|
60667
|
+
const MIN_SECRET_NEEDLE_LENGTH = 4;
|
|
60668
|
+
/**
|
|
60669
|
+
* Does `value` hold a plaintext this pass has recorded as a secret?
|
|
60670
|
+
*
|
|
60671
|
+
* Two branches with deliberately different strictness, and the asymmetry is the
|
|
60672
|
+
* point. A WHOLE-value match is exact evidence at any length, so it is accepted
|
|
60673
|
+
* unconditionally. A SUBSTRING match is only evidence when the plaintext is
|
|
60674
|
+
* long enough to be distinctive: a 1-3 character secret (a JSON key holding
|
|
60675
|
+
* `"0"`) occurs inside unrelated values constantly, and accepting it would mark
|
|
60676
|
+
* half the resource's paths secret-bearing — masking values that are not
|
|
60677
|
+
* secrets and, worse, making `--accept` refuse them.
|
|
60678
|
+
*
|
|
60679
|
+
* So this errs toward NOT marking on the substring branch, and that direction
|
|
60680
|
+
* is chosen rather than inherited: `redactSecretsForState` applies the same
|
|
60681
|
+
* threshold when it builds needles, so a sub-threshold plaintext is not
|
|
60682
|
+
* substituted there either. Marking a path this function's caller cannot
|
|
60683
|
+
* actually redact would produce a `***` with no mechanism behind it.
|
|
60684
|
+
*/
|
|
60685
|
+
function carriesRecordedSecret(value, secrets) {
|
|
60686
|
+
if (value === "") return false;
|
|
60687
|
+
if (secrets.has(value)) return true;
|
|
60688
|
+
for (const plaintext of secrets.keys()) if (plaintext.length >= MIN_SECRET_NEEDLE_LENGTH && value.includes(plaintext)) return true;
|
|
60689
|
+
return false;
|
|
60690
|
+
}
|
|
60691
|
+
/**
|
|
60692
|
+
* Is this unresolvable token one whose value is a SECRET by definition?
|
|
60693
|
+
*
|
|
60694
|
+
* `ssm-secure` is the only such spelling, and it is decidable with no lookup:
|
|
60695
|
+
* CloudFormation defines exactly three dynamic-reference services (`ssm`,
|
|
60696
|
+
* `ssm-secure`, `secretsmanager`), cdkd resolves the other two, and
|
|
60697
|
+
* `ssm-secure` is the encrypted one.
|
|
60698
|
+
*
|
|
60699
|
+
* Anything else reaching the survivor arm is not a CloudFormation dynamic
|
|
60700
|
+
* reference at all — it is text that merely looks like one — and treating it as
|
|
60701
|
+
* secret is not the safe direction it appears to be: the path would be masked
|
|
60702
|
+
* to `***`, refused by `--accept`, and pinned to the live value by
|
|
60703
|
+
* {@link preserveLiveValuesAtUnresolvedTokens}, which is permanently stuck
|
|
60704
|
+
* drift with no remedy the user can apply. Such a token is still REPORTED; it
|
|
60705
|
+
* just does not claim to be a secret.
|
|
60706
|
+
*/
|
|
60707
|
+
function isSecretBySpelling(token) {
|
|
60708
|
+
return token.startsWith("{{resolve:ssm-secure:");
|
|
60709
|
+
}
|
|
60710
|
+
/**
|
|
60711
|
+
* Every `{{resolve:...}}` token still present in `value`.
|
|
60712
|
+
*
|
|
60713
|
+
* Used to NAME an unresolvable reference without quoting the string it sits in.
|
|
60714
|
+
* That string is a partially substituted leaf: `resolveDynamicReferences`
|
|
60715
|
+
* substitutes token by token, so a leaf holding two references comes back with
|
|
60716
|
+
* the resolvable one already replaced by its PLAINTEXT. Interpolating it into a
|
|
60717
|
+
* log line prints the secret — from the command whose purpose is not to. A
|
|
60718
|
+
* token is a reference NAME and carries no value, so it is always safe to show.
|
|
60719
|
+
*/
|
|
60720
|
+
function survivingDynamicReferences(value) {
|
|
60721
|
+
return value.match(/\{\{resolve:[^}]+\}\}/g) ?? [];
|
|
60722
|
+
}
|
|
60723
|
+
/**
|
|
60724
|
+
* Record the dotted path of every leaf holding a `{{resolve:...}}` string.
|
|
60725
|
+
*
|
|
60726
|
+
* The OFFLINE seed for {@link SecretPathSet}: it needs no AWS call, so it is
|
|
60727
|
+
* what remains when resolution fails. Issue #1900's mechanism applied to
|
|
60728
|
+
* positions instead of values — the record's own bags already say WHERE the
|
|
60729
|
+
* references are, and that is the half of the answer a failed lookup does not
|
|
60730
|
+
* take away.
|
|
60731
|
+
*
|
|
60732
|
+
* Necessarily coarser than the resolved answer, because a `{{resolve:ssm:...}}`
|
|
60733
|
+
* naming a plain `String` parameter is PUBLIC config and only the resolver's
|
|
60734
|
+
* TYPE verdict can say so. Marking such a path secret-bearing over-masks it,
|
|
60735
|
+
* which is why the seed is used ONLY on the failure path, where the alternative
|
|
60736
|
+
* is not masking at all.
|
|
60737
|
+
*/
|
|
60738
|
+
function collectDynamicReferencePaths(value, into, path = "") {
|
|
60739
|
+
if (typeof value === "string") {
|
|
60740
|
+
if (value.includes("{{resolve:")) into.add(path);
|
|
60741
|
+
return;
|
|
60742
|
+
}
|
|
60743
|
+
if (Array.isArray(value)) {
|
|
60744
|
+
value.forEach((item, i) => collectDynamicReferencePaths(item, into, path === "" ? String(i) : `${path}.${i}`));
|
|
60745
|
+
return;
|
|
60746
|
+
}
|
|
60747
|
+
if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) collectDynamicReferencePaths(v, into, path === "" ? k : `${path}.${k}`);
|
|
60748
|
+
}
|
|
60749
|
+
/**
|
|
60750
|
+
* Re-resolve the SECRET dynamic references (`{{resolve:secretsmanager:...}}`,
|
|
60751
|
+
* and an `{{resolve:ssm:...}}` naming a `SecureString`) held by a bag read back
|
|
60752
|
+
* out of cdkd STATE (issue #1914).
|
|
60753
|
+
*
|
|
60754
|
+
* State stores the unresolved EXPRESSION — that is the GHSA-p5qg-v9gv-hc7w
|
|
60755
|
+
* redaction — while a provider's `readCurrentState` snapshot necessarily holds
|
|
60756
|
+
* the resolved PLAINTEXT, because that is what AWS was given at deploy time.
|
|
60757
|
+
* Nothing in `cdkd drift` reconciled the two, and all three of the command's
|
|
60758
|
+
* modes broke on it: the comparison reported permanent phantom drift and
|
|
60759
|
+
* printed the plaintext as the AWS-current side, `--accept` persisted that
|
|
60760
|
+
* plaintext back into `state.json`, and `--revert` shipped the literal
|
|
60761
|
+
* `{{resolve:...}}` string to the live resource.
|
|
60762
|
+
*
|
|
60763
|
+
* The counterpart of the rollback replay's `resolveReplayProps`
|
|
60764
|
+
* (`src/deployment/rollback-executor.ts`), and for the same reason: both
|
|
60765
|
+
* commands are synth-free, so the expression string in the persisted record is
|
|
60766
|
+
* the only thing there is to resolve from. Every plaintext is recorded into
|
|
60767
|
+
* `secrets` so the caller can redact it back out of anything it PRINTS or
|
|
60768
|
+
* PERSISTS, and every path that produced one into `secretPaths` so the caller
|
|
60769
|
+
* can act on a value it does NOT recognise there — see {@link redactDriftChanges}.
|
|
60770
|
+
*
|
|
60771
|
+
* A `{{resolve:...}}` token that SURVIVES the pass is reported through
|
|
60772
|
+
* `onUnresolved` and left in place — it is not an error. The resolver's
|
|
60773
|
+
* unsupported-service arm (`ssm-secure:` is the live example) warns and returns
|
|
60774
|
+
* the literal, and `cdkd deploy` resolves through the very same code, so AWS is
|
|
60775
|
+
* ALREADY holding that literal string and state records it. Replaying it is
|
|
60776
|
+
* therefore a correct no-op, and failing the resource over it would take every
|
|
60777
|
+
* other drifted property on that resource down with it (plus exit 2). The
|
|
60778
|
+
* report exists so the user learns cdkd is shipping a token it cannot resolve,
|
|
60779
|
+
* which is a real thing to know and not a reason to stop.
|
|
60780
|
+
*
|
|
60781
|
+
* Only the TOKENS are reported, never the leaf that held them: the leaf is
|
|
60782
|
+
* partially substituted by this point, so a mixed `secretsmanager` +
|
|
60783
|
+
* `ssm-secure` string comes back carrying real plaintext.
|
|
60784
|
+
*
|
|
60785
|
+
* Returns the input by identity when it holds no dynamic reference, so the
|
|
60786
|
+
* non-secret path is unchanged down to object identity.
|
|
60787
|
+
*/
|
|
60788
|
+
async function resolveStateSecretExpressions(props, resolver, secrets, options = {}) {
|
|
60789
|
+
if (!containsDynamicReference(props)) return props;
|
|
60790
|
+
const { secretPaths, onUnresolved } = options;
|
|
60791
|
+
const ctx = {
|
|
60792
|
+
template: { Resources: {} },
|
|
60793
|
+
resources: {},
|
|
60794
|
+
recordedSecretValues: secrets
|
|
60795
|
+
};
|
|
60796
|
+
const walk = async (v, path) => {
|
|
60797
|
+
if (typeof v === "string") {
|
|
60798
|
+
if (!v.includes("{{resolve:")) return v;
|
|
60799
|
+
const resolved = await resolver.resolveDynamicReferences(v, ctx);
|
|
60800
|
+
const survivors = survivingDynamicReferences(resolved).filter((t) => v.includes(t));
|
|
60801
|
+
if (survivors.length > 0) {
|
|
60802
|
+
if (survivors.some(isSecretBySpelling)) secretPaths?.add(path);
|
|
60803
|
+
onUnresolved?.(survivors, path);
|
|
60804
|
+
}
|
|
60805
|
+
if (carriesRecordedSecret(resolved, secrets)) secretPaths?.add(path);
|
|
60806
|
+
return resolved;
|
|
60807
|
+
}
|
|
60808
|
+
if (Array.isArray(v)) {
|
|
60809
|
+
const out = new Array(v.length);
|
|
60810
|
+
for (let i = 0; i < v.length; i++) out[i] = await walk(v[i], path === "" ? String(i) : `${path}.${i}`);
|
|
60811
|
+
return out;
|
|
60812
|
+
}
|
|
60813
|
+
if (v !== null && typeof v === "object") {
|
|
60814
|
+
const out = {};
|
|
60815
|
+
for (const [k, val] of Object.entries(v)) out[k] = await walk(val, path === "" ? k : `${path}.${k}`);
|
|
60816
|
+
return out;
|
|
60817
|
+
}
|
|
60818
|
+
return v;
|
|
60819
|
+
};
|
|
60820
|
+
return await walk(props, "");
|
|
60821
|
+
}
|
|
60822
|
+
/**
|
|
60823
|
+
* Is a drift reported at `path` positioned on, or above, a known secret?
|
|
60824
|
+
*
|
|
60825
|
+
* The PREFIX direction is the one that matters: a secret sits at a leaf (or at
|
|
60826
|
+
* an array the comparator never descends), while a drift can be reported at any
|
|
60827
|
+
* ancestor of it — `Environment` rather than
|
|
60828
|
+
* `Environment.Variables.SECRET_PASSWORD` — whenever the two sides disagree
|
|
60829
|
+
* about the shape rather than the value. The reverse containment is checked too
|
|
60830
|
+
* and costs nothing.
|
|
60831
|
+
*/
|
|
60832
|
+
function isSecretBearingPath(path, secretPaths) {
|
|
60833
|
+
if (secretPaths.size === 0) return false;
|
|
60834
|
+
for (const secretPath of secretPaths) if (secretPath === path || secretPath.startsWith(`${path}.`) || path.startsWith(`${secretPath}.`)) return true;
|
|
60835
|
+
return false;
|
|
60836
|
+
}
|
|
60837
|
+
/**
|
|
60838
|
+
* Redact one side of a drift entry.
|
|
60839
|
+
*
|
|
60840
|
+
* Two passes, and the second is the one issue #1914's review turned up. The
|
|
60841
|
+
* VALUE pass rewrites any recorded plaintext back onto its own
|
|
60842
|
+
* `{{resolve:...}}` expression, which is exact and is all that is needed while
|
|
60843
|
+
* the secret AWS holds is the secret the reference resolves to today.
|
|
60844
|
+
*
|
|
60845
|
+
* It stops being enough the moment those two differ, and they differ for the
|
|
60846
|
+
* most ordinary reason there is: a Secrets Manager ROTATION. The deployed
|
|
60847
|
+
* resource then still holds the PREVIOUS secret while the re-resolved baseline
|
|
60848
|
+
* holds the new one, so the path drifts and the AWS side matches no key in a
|
|
60849
|
+
* map built from today's value — it is a real secret that would be printed
|
|
60850
|
+
* verbatim and persisted by `--accept`. An out-of-band edit is
|
|
60851
|
+
* indistinguishable from it at this layer, so the POSITION decides: at a path
|
|
60852
|
+
* known to carry a secret, the only value ever shown is the expression itself,
|
|
60853
|
+
* and anything else becomes {@link SECRET_MASK}.
|
|
60854
|
+
*
|
|
60855
|
+
* `undefined` and `null` are exempt — an absent or null key discloses nothing,
|
|
60856
|
+
* and masking them would turn "AWS does not have this key" into "AWS has
|
|
60857
|
+
* something secret here", which is both wrong and less useful.
|
|
60858
|
+
*/
|
|
60859
|
+
function redactDriftValue(value, secrets, secretBearing) {
|
|
60860
|
+
const redacted = redactSecretsForState(value, secrets);
|
|
60861
|
+
if (!secretBearing) return redacted;
|
|
60862
|
+
if (redacted === void 0 || redacted === null) return redacted;
|
|
60863
|
+
if (typeof redacted === "string" && isWholeDynamicReference(redacted)) return redacted;
|
|
60864
|
+
return "***";
|
|
60865
|
+
}
|
|
60866
|
+
/**
|
|
60867
|
+
* Is the WHOLE string a single `{{resolve:...}}` token and nothing else?
|
|
60868
|
+
*
|
|
60869
|
+
* Byte-for-byte `secret-redaction.ts`'s `isSingleDynamicReferenceToken`,
|
|
60870
|
+
* including the `[^{}]*` character class. The difference from `[^}]+` is
|
|
60871
|
+
* narrow and worth stating correctly: it is a `{` INSIDE the token, e.g.
|
|
60872
|
+
* `{{resolve:ssm:/a{b}}`. (A previous version of this comment claimed
|
|
60873
|
+
* `{{resolve:a}}{{resolve:b}}` would pass under `[^}]+` — it would not, since
|
|
60874
|
+
* `[^}]+` cannot span the `}` in the middle either.) Matching the sibling
|
|
60875
|
+
* exactly is what matters here: this predicate decides whether a value may be
|
|
60876
|
+
* shown INSTEAD of `SECRET_MASK`, and `redactByPath` uses its copy to decide
|
|
60877
|
+
* whether a leaf is already an expression, so a disagreement would let one of
|
|
60878
|
+
* them treat as a token what the other treats as data. Copied rather than
|
|
60879
|
+
* imported because that helper is module-private and this file may not widen
|
|
60880
|
+
* that module's exports.
|
|
60881
|
+
*/
|
|
60882
|
+
function isWholeDynamicReference(value) {
|
|
60883
|
+
return /^\{\{resolve:[^{}]*\}\}$/.test(value);
|
|
60884
|
+
}
|
|
60885
|
+
/**
|
|
60886
|
+
* Why `--accept` must not persist this drift, or `undefined` when it may.
|
|
60887
|
+
*
|
|
60888
|
+
* Shared by the WRITE (`runAccept`) and the PLAN (`printAcceptPlan`) so the two
|
|
60889
|
+
* cannot disagree — a `--dry-run` that promises a write the real run refuses is
|
|
60890
|
+
* worse than either behaviour on its own.
|
|
60891
|
+
*
|
|
60892
|
+
* Two refusals, both about a value cdkd could not identify rather than about
|
|
60893
|
+
* secrecy as such:
|
|
60894
|
+
*
|
|
60895
|
+
* - A masked VALUE says AWS holds something at a known-secret path that is not
|
|
60896
|
+
* what the reference resolves to. Writing `***` would corrupt the baseline
|
|
60897
|
+
* and make the next deploy push the literal mask at AWS.
|
|
60898
|
+
* - A masked PATH says the readback answered with a map KEYED by a secret.
|
|
60899
|
+
* `setAtPath` would then create a key literally named `***` — and when
|
|
60900
|
+
* `awsValue` is `undefined` it would INSERT that key rather than removing the
|
|
60901
|
+
* real one, so the value check alone does not cover it.
|
|
60902
|
+
*
|
|
60903
|
+
* The path check is what makes the value exemptions safe: `redactDriftValue`
|
|
60904
|
+
* lets `undefined` / `null` / a whole expression through unmasked, which is
|
|
60905
|
+
* right for a value and says nothing about the path it sits at.
|
|
60906
|
+
*/
|
|
60907
|
+
function acceptRefusalReason(change, maskedPaths) {
|
|
60908
|
+
if (!maskedPaths.has(change.path)) return void 0;
|
|
60909
|
+
if (change.path.includes("***")) return "its property NAME came back carrying a secret value, so cdkd cannot name the key to write — and no redaction pass can clear it either, since they all walk values and never keys. ROTATE the secret; that is the only remedy";
|
|
60910
|
+
if (change.awsValue === void 0) return "AWS no longer reports it, and accepting an absence DELETES the key — which at or above a secret dynamic reference would erase the reference out of cdkd state. Use --revert to push it back, or re-deploy";
|
|
60911
|
+
return "it carries a secret dynamic reference and the value AWS currently holds is not the one the reference resolves to";
|
|
60912
|
+
}
|
|
60913
|
+
/**
|
|
60914
|
+
* Redact resolved secret plaintext out of the drift entries that get REPORTED
|
|
60915
|
+
* (issue #1914).
|
|
60916
|
+
*
|
|
60917
|
+
* Both sides need it, for different reasons. `stateValue` comes from the
|
|
60918
|
+
* re-resolved comparison baseline, so at a secret leaf it now holds the
|
|
60919
|
+
* plaintext the state record deliberately does not store. `awsValue` is the
|
|
60920
|
+
* live readback, which holds that plaintext by construction. Either one
|
|
60921
|
+
* reaching `writeHumanReport` / `writeJsonReport` / the `--accept` and
|
|
60922
|
+
* `--revert` plans is the disclosure the redaction exists to prevent.
|
|
60923
|
+
*
|
|
60924
|
+
* Redacting `awsValue` also settles what `--accept` PERSISTS: that is the value
|
|
60925
|
+
* it writes into the baseline, so a secret one arrives at the state write
|
|
60926
|
+
* already carrying its expression — or, when it could not be recognised, as
|
|
60927
|
+
* {@link SECRET_MASK}, which `runAccept` refuses to persist.
|
|
60928
|
+
*
|
|
60929
|
+
* The PATH is redacted too. `redactSecretsForState` walks values and never
|
|
60930
|
+
* object KEYS, so a readback that answers with a map keyed by a secret (a
|
|
60931
|
+
* Lambda env var NAMED with one) renders as
|
|
60932
|
+
* `Environment.Variables.<plaintext>` in every printer. It is masked here
|
|
60933
|
+
* rather than assumed impossible, and a masked path also marks the change
|
|
60934
|
+
* secret-bearing, so `--accept` will not write a key it can no longer name.
|
|
60935
|
+
*
|
|
60936
|
+
* Returns the input array by identity when there is nothing to act on.
|
|
60937
|
+
*/
|
|
60938
|
+
function redactDriftChanges(changes, secrets, secretPaths) {
|
|
60939
|
+
if (secrets.size === 0 && secretPaths.size === 0) return {
|
|
60940
|
+
changes,
|
|
60941
|
+
maskedPaths: /* @__PURE__ */ new Set()
|
|
60942
|
+
};
|
|
60943
|
+
const maskedPaths = /* @__PURE__ */ new Set();
|
|
60944
|
+
const redacted = [];
|
|
60945
|
+
for (const change of changes) {
|
|
60946
|
+
const maskedPath = maskSecretsInText(change.path, secrets);
|
|
60947
|
+
const positionIsSecret = isSecretBearingPath(change.path, secretPaths);
|
|
60948
|
+
const nameCarriesSecret = maskedPath !== change.path;
|
|
60949
|
+
const secretBearing = positionIsSecret || nameCarriesSecret;
|
|
60950
|
+
if (secretPaths.has(change.path) && change.awsValue === void 0) continue;
|
|
60951
|
+
const stateValue = redactDriftValue(change.stateValue, secrets, secretBearing);
|
|
60952
|
+
const awsValue = redactDriftValue(change.awsValue, secrets, secretBearing);
|
|
60953
|
+
if (maskedPath !== change.path || secretBearing && awsValue === "***" || secretBearing && change.awsValue === void 0) maskedPaths.add(maskedPath);
|
|
60954
|
+
redacted.push({
|
|
60955
|
+
path: maskedPath,
|
|
60956
|
+
stateValue,
|
|
60957
|
+
awsValue
|
|
60958
|
+
});
|
|
60959
|
+
}
|
|
60960
|
+
return {
|
|
60961
|
+
changes: redacted,
|
|
60962
|
+
maskedPaths
|
|
60963
|
+
};
|
|
60964
|
+
}
|
|
60965
|
+
/**
|
|
60647
60966
|
* Run drift detection for one stack and shape the per-resource outcomes
|
|
60648
60967
|
* into a {@link StackDriftReport}. The state object + etag are stored on
|
|
60649
60968
|
* the report so `--accept` can write back without a re-read, and
|
|
@@ -60656,6 +60975,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
60656
60975
|
return await withStackName(stackName, async () => {
|
|
60657
60976
|
const outcomes = [];
|
|
60658
60977
|
const state = result.state;
|
|
60978
|
+
const logger = getLogger();
|
|
60979
|
+
const secretResolver = new IntrinsicFunctionResolver(region);
|
|
60659
60980
|
const entries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
60660
60981
|
for (const [logicalId, resource] of entries) {
|
|
60661
60982
|
if (providerRegistry.shouldSkipResource(resource.resourceType)) continue;
|
|
@@ -60715,19 +61036,46 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
60715
61036
|
const unorderedPaths = provider.getDriftUnorderedPaths ? provider.getDriftUnorderedPaths(resource.resourceType) : [];
|
|
60716
61037
|
const useObserved = resource.observedProperties !== void 0;
|
|
60717
61038
|
const baseline = useObserved ? resource.observedProperties : resource.properties ?? {};
|
|
61039
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
61040
|
+
const secretPaths = /* @__PURE__ */ new Set();
|
|
61041
|
+
const seededSecretPaths = /* @__PURE__ */ new Set();
|
|
61042
|
+
collectDynamicReferencePaths(baseline, seededSecretPaths);
|
|
61043
|
+
if (useObserved) collectDynamicReferencePaths(resource.properties ?? {}, seededSecretPaths);
|
|
61044
|
+
const unresolvedTokens = /* @__PURE__ */ new Set();
|
|
61045
|
+
const noteUnresolved = (tokens) => {
|
|
61046
|
+
for (const token of tokens) unresolvedTokens.add(token);
|
|
61047
|
+
};
|
|
61048
|
+
let comparisonBaseline = baseline;
|
|
61049
|
+
let secretResolutionFailed = false;
|
|
61050
|
+
try {
|
|
61051
|
+
comparisonBaseline = await resolveStateSecretExpressions(baseline, secretResolver, secrets, {
|
|
61052
|
+
secretPaths,
|
|
61053
|
+
onUnresolved: noteUnresolved
|
|
61054
|
+
});
|
|
61055
|
+
if (useObserved) await resolveStateSecretExpressions(resource.properties ?? {}, secretResolver, secrets, {
|
|
61056
|
+
secretPaths,
|
|
61057
|
+
onUnresolved: noteUnresolved
|
|
61058
|
+
});
|
|
61059
|
+
} catch (err) {
|
|
61060
|
+
secretResolutionFailed = true;
|
|
61061
|
+
comparisonBaseline = baseline;
|
|
61062
|
+
logger.warn(`${logicalId} (${resource.resourceType}): could not resolve the dynamic reference(s) this resource's state records, so its secret-bearing properties are NOT compared — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
|
|
61063
|
+
secrets.clear();
|
|
61064
|
+
}
|
|
61065
|
+
if (unresolvedTokens.size > 0) logger.warn(`${logicalId} (${resource.resourceType}): cdkd cannot resolve ${maskSecretsInText([...unresolvedTokens].join(", "), secrets)} — those properties are NOT compared. A revert leaves a property whose WHOLE value is one of these tokens untouched; where a token is EMBEDDED in a longer string, the revert writes that string with the token literal, exactly as 'cdkd deploy' does — so a resolved value AWS holds there WOULD be overwritten. cdkd resolves 'secretsmanager' and 'ssm' references only.`);
|
|
60718
61066
|
const observedIgnorePaths = useObserved ? undeclaredEmptyObservedKeys(resource.observedProperties, resource.properties ?? {}) : [];
|
|
60719
|
-
const normalized = await canonicalizePrincipalUniqueIds(
|
|
61067
|
+
const normalized = await canonicalizePrincipalUniqueIds(comparisonBaseline, aws, resolvePrincipalUniqueId);
|
|
60720
61068
|
const protocolNormalized = canonicalizeIpProtocols(normalized.baseline, normalized.aws, resource.resourceType);
|
|
60721
61069
|
const canonicalized = provider.canonicalizeDriftProperties ? {
|
|
60722
61070
|
baseline: provider.canonicalizeDriftProperties(resource.resourceType, protocolNormalized.baseline),
|
|
60723
61071
|
aws: provider.canonicalizeDriftProperties(resource.resourceType, protocolNormalized.aws)
|
|
60724
61072
|
} : protocolNormalized;
|
|
60725
|
-
const
|
|
61073
|
+
const reported = redactDriftChanges(calculateResourceDrift(canonicalized.baseline, canonicalized.aws, {
|
|
60726
61074
|
ignorePaths: observedIgnorePaths.length ? [...ignorePaths, ...observedIgnorePaths] : ignorePaths,
|
|
60727
61075
|
unionWalkObjects: useObserved,
|
|
60728
61076
|
unorderedPaths
|
|
60729
|
-
});
|
|
60730
|
-
if (changes.length === 0) outcomes.push({
|
|
61077
|
+
}), secrets, secretResolutionFailed ? seededSecretPaths : secretPaths);
|
|
61078
|
+
if (reported.changes.length === 0) outcomes.push({
|
|
60731
61079
|
kind: "clean",
|
|
60732
61080
|
logicalId,
|
|
60733
61081
|
resourceType: resource.resourceType
|
|
@@ -60736,8 +61084,10 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
60736
61084
|
kind: "drifted",
|
|
60737
61085
|
logicalId,
|
|
60738
61086
|
resourceType: resource.resourceType,
|
|
60739
|
-
|
|
60740
|
-
awsProperties: aws
|
|
61087
|
+
...reported,
|
|
61088
|
+
awsProperties: aws,
|
|
61089
|
+
secrets,
|
|
61090
|
+
referencesUnresolved: secretResolutionFailed || unresolvedTokens.size > 0
|
|
60741
61091
|
});
|
|
60742
61092
|
}
|
|
60743
61093
|
return {
|
|
@@ -60779,6 +61129,20 @@ function buildReadCurrentStateContext(state, excludedLogicalId) {
|
|
|
60779
61129
|
}
|
|
60780
61130
|
return { siblings };
|
|
60781
61131
|
}
|
|
61132
|
+
/**
|
|
61133
|
+
* Read the value at a dotted path, or `undefined` when any segment is missing.
|
|
61134
|
+
* The read counterpart of {@link setAtPath}, and it parses the same subset:
|
|
61135
|
+
* the drift comparator only synthesizes paths through plain objects.
|
|
61136
|
+
*/
|
|
61137
|
+
function getAtPath(source, path) {
|
|
61138
|
+
if (path.length === 0) return source;
|
|
61139
|
+
let cursor = source;
|
|
61140
|
+
for (const segment of path.split(".")) {
|
|
61141
|
+
if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) return void 0;
|
|
61142
|
+
cursor = cursor[segment];
|
|
61143
|
+
}
|
|
61144
|
+
return cursor;
|
|
61145
|
+
}
|
|
60782
61146
|
function setAtPath(target, path, value) {
|
|
60783
61147
|
if (path.length === 0) return;
|
|
60784
61148
|
const segments = path.split(".");
|
|
@@ -60831,13 +61195,27 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
|
|
|
60831
61195
|
const hasObserved = existing.observedProperties !== void 0;
|
|
60832
61196
|
const baselineSource = hasObserved ? existing.observedProperties : existing.properties ?? {};
|
|
60833
61197
|
const newBaseline = JSON.parse(JSON.stringify(baselineSource));
|
|
60834
|
-
|
|
61198
|
+
const accepted = [];
|
|
61199
|
+
for (const change of outcome.changes) {
|
|
61200
|
+
const refusal = acceptRefusalReason(change, outcome.maskedPaths);
|
|
61201
|
+
if (refusal !== void 0) {
|
|
61202
|
+
logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): not accepting '${change.path}' — ${refusal}, so cdkd will not write it to state. Run 'cdkd drift ${report.stackName} --revert' to push the referenced value back to AWS, or re-deploy if the reference changed.`);
|
|
61203
|
+
continue;
|
|
61204
|
+
}
|
|
61205
|
+
setAtPath(newBaseline, change.path, change.awsValue);
|
|
61206
|
+
accepted.push(change);
|
|
61207
|
+
}
|
|
61208
|
+
const redactedBaseline = redactSecretsForState(newBaseline, outcome.secrets, existing.properties ?? {}, STATE_SOURCED_READBACK_RULES);
|
|
61209
|
+
for (const change of accepted) {
|
|
61210
|
+
if (deepEqualUnordered(getAtPath(redactedBaseline, change.path), change.awsValue)) continue;
|
|
61211
|
+
logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): '${change.path}' was NOT recorded — cdkd state holds an unresolved '{{resolve:...}}' reference at that property, and the reference wins over an accepted value. Change the template if that reference is wrong.`);
|
|
61212
|
+
}
|
|
60835
61213
|
resources[outcome.logicalId] = hasObserved ? {
|
|
60836
61214
|
...existing,
|
|
60837
|
-
observedProperties:
|
|
61215
|
+
observedProperties: redactedBaseline
|
|
60838
61216
|
} : {
|
|
60839
61217
|
...existing,
|
|
60840
|
-
properties:
|
|
61218
|
+
properties: redactedBaseline
|
|
60841
61219
|
};
|
|
60842
61220
|
}
|
|
60843
61221
|
const newState = {
|
|
@@ -61198,6 +61576,57 @@ function mergeUntemplatedValue(awsValue, desiredValue) {
|
|
|
61198
61576
|
return merged;
|
|
61199
61577
|
}
|
|
61200
61578
|
/**
|
|
61579
|
+
* Replace every leaf of a revert payload that still holds a `{{resolve:...}}`
|
|
61580
|
+
* token with the value AWS currently has at that position (issue #1914,
|
|
61581
|
+
* round-3 review).
|
|
61582
|
+
*
|
|
61583
|
+
* The round-3 change downgraded an unresolvable reference from a per-resource
|
|
61584
|
+
* failure to a warning, on the premise that `cdkd deploy` resolves through the
|
|
61585
|
+
* resolver's same unsupported-service arm — so AWS already holds the literal
|
|
61586
|
+
* and replaying it is a no-op. **That premise holds only for records cdkd
|
|
61587
|
+
* deployed.** CloudFormation resolves `ssm-secure` SERVER-side, so a record
|
|
61588
|
+
* adopted by `cdkd import --migrate-from-cloudformation` has the literal token
|
|
61589
|
+
* in state while the live resource holds the PLAINTEXT. `diffAt` skips such a
|
|
61590
|
+
* leaf, so it never drifts — but `buildRevertNewProperties` overlays the whole
|
|
61591
|
+
* top-level subtree when any SIBLING key drifts, which would push the literal
|
|
61592
|
+
* token over a resolved value. That is defect 1 of this issue surviving in a
|
|
61593
|
+
* narrower case, on a live AWS write.
|
|
61594
|
+
*
|
|
61595
|
+
* Deciding by PROVENANCE would need a flag state does not carry. Deciding by
|
|
61596
|
+
* what AWS actually holds needs nothing and is exact in both directions: for a
|
|
61597
|
+
* cdkd-deployed record AWS holds the token, so copying it back is the same
|
|
61598
|
+
* no-op the premise described; for a migrated record AWS holds the resolved
|
|
61599
|
+
* value, which is left untouched.
|
|
61600
|
+
*
|
|
61601
|
+
* Falls back to KEEPING the token wherever AWS has nothing at that position —
|
|
61602
|
+
* that is what `cdkd deploy` sends, so it is the safe residual rather than
|
|
61603
|
+
* dropping a property the resource may require. Array descent is positional
|
|
61604
|
+
* and bails to that same residual on any length mismatch, since a reordered
|
|
61605
|
+
* readback cannot be positioned against.
|
|
61606
|
+
*/
|
|
61607
|
+
function preserveLiveValuesAtUnresolvedTokens(send, awsProperties, secrets) {
|
|
61608
|
+
const walk = (value, live) => {
|
|
61609
|
+
if (typeof value === "string" && value.includes("{{resolve:")) {
|
|
61610
|
+
if (!isWholeDynamicReference(value)) return value;
|
|
61611
|
+
if (live === void 0) return value;
|
|
61612
|
+
if (typeof live === "string" && live.length >= MIN_SECRET_NEEDLE_LENGTH && isSecretBySpelling(value)) secrets.set(live, value);
|
|
61613
|
+
return live;
|
|
61614
|
+
}
|
|
61615
|
+
if (Array.isArray(value)) {
|
|
61616
|
+
const liveItems = Array.isArray(live) && live.length === value.length ? live : void 0;
|
|
61617
|
+
return value.map((item, i) => walk(item, liveItems?.[i]));
|
|
61618
|
+
}
|
|
61619
|
+
if (value !== null && typeof value === "object") {
|
|
61620
|
+
const liveObject = isPlainRecord(live) ? live : void 0;
|
|
61621
|
+
const out = {};
|
|
61622
|
+
for (const [k, v] of Object.entries(value)) out[k] = walk(v, liveObject?.[k]);
|
|
61623
|
+
return out;
|
|
61624
|
+
}
|
|
61625
|
+
return value;
|
|
61626
|
+
};
|
|
61627
|
+
return walk(send, awsProperties);
|
|
61628
|
+
}
|
|
61629
|
+
/**
|
|
61201
61630
|
* Build the `newProperties` object passed to `provider.update` during
|
|
61202
61631
|
* `--revert`. Strategy:
|
|
61203
61632
|
*
|
|
@@ -61371,11 +61800,13 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
61371
61800
|
let totalFailed = 0;
|
|
61372
61801
|
let totalUnsupported = 0;
|
|
61373
61802
|
let totalSucceeded = 0;
|
|
61803
|
+
let totalUnresolvable = 0;
|
|
61374
61804
|
for (const report of reports) {
|
|
61375
61805
|
const driftedOutcomes = report.outcomes.filter((o) => o.kind === "drifted");
|
|
61376
61806
|
if (driftedOutcomes.length === 0) continue;
|
|
61377
61807
|
await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert");
|
|
61378
61808
|
const narrowedByLogicalId = /* @__PURE__ */ new Map();
|
|
61809
|
+
const revertSecretResolver = new IntrinsicFunctionResolver(report.region);
|
|
61379
61810
|
try {
|
|
61380
61811
|
await runWithConcurrency(driftedOutcomes.map((outcome) => async () => {
|
|
61381
61812
|
const stateResource = report.state.resources[outcome.logicalId];
|
|
@@ -61388,28 +61819,53 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
61388
61819
|
resourceType: outcome.resourceType,
|
|
61389
61820
|
provisionedBy: stateResource.provisionedBy
|
|
61390
61821
|
}).provider;
|
|
61391
|
-
const
|
|
61392
|
-
const
|
|
61822
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
61823
|
+
const revertBaseline = stateResource.observedProperties ?? stateResource.properties ?? {};
|
|
61824
|
+
const unresolvedTokens = /* @__PURE__ */ new Set();
|
|
61825
|
+
const noteUnresolved = (tokens) => {
|
|
61826
|
+
for (const token of tokens) unresolvedTokens.add(token);
|
|
61827
|
+
};
|
|
61828
|
+
let desiredProperties;
|
|
61393
61829
|
try {
|
|
61394
|
-
|
|
61830
|
+
desiredProperties = await resolveStateSecretExpressions(revertBaseline, revertSecretResolver, secrets, { onUnresolved: noteUnresolved });
|
|
61831
|
+
if (stateResource.observedProperties !== void 0) await resolveStateSecretExpressions(stateResource.properties ?? {}, revertSecretResolver, secrets, { onUnresolved: noteUnresolved });
|
|
61832
|
+
} catch (err) {
|
|
61833
|
+
totalUnresolvable++;
|
|
61834
|
+
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not re-resolve the dynamic reference(s) this resource's state records — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
|
|
61835
|
+
return;
|
|
61836
|
+
}
|
|
61837
|
+
const unrevertablePaths = outcome.changes.map((c) => c.path).filter((path) => (path.split(".", 1)[0] ?? "").includes("***"));
|
|
61838
|
+
if (unrevertablePaths.length > 0) logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): cannot revert ${unrevertablePaths.join(", ")} — cdkd cannot name the property, so it is left as AWS has it. ROTATE the secret that leaked into the property name.`);
|
|
61839
|
+
if (unresolvedTokens.size > 0) logger.warn(` ! [revert] ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): cdkd cannot resolve ${maskSecretsInText([...unresolvedTokens].join(", "), secrets)} — a property whose WHOLE value is one of these tokens is left UNCHANGED by this revert. Where a token is EMBEDDED in a longer string, that string is written with the token literal, exactly as 'cdkd deploy' does, so a resolved value AWS holds there is overwritten.`);
|
|
61840
|
+
let newProperties;
|
|
61841
|
+
try {
|
|
61842
|
+
const overlaid = buildRevertNewProperties(outcome.changes, desiredProperties, outcome.awsProperties, { preserveUntemplated: stateResource.observedProperties === void 0 });
|
|
61843
|
+
newProperties = unresolvedTokens.size > 0 ? preserveLiveValuesAtUnresolvedTokens(overlaid, outcome.awsProperties, secrets) : overlaid;
|
|
61844
|
+
} catch (err) {
|
|
61845
|
+
totalFailed++;
|
|
61846
|
+
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not build the revert payload — ${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
|
|
61847
|
+
return;
|
|
61848
|
+
}
|
|
61849
|
+
try {
|
|
61850
|
+
const updateResult = await withRetry(() => provider.update(outcome.logicalId, stateResource.physicalId, outcome.resourceType, newProperties, outcome.awsProperties, { desiredFromAwsReadback: true }), outcome.logicalId, { logger: { debug: (msg) => logger.debug(maskSecretsInText(msg, secrets)) } });
|
|
61395
61851
|
totalSucceeded++;
|
|
61396
61852
|
logger.info(` ✓ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted.`);
|
|
61397
61853
|
try {
|
|
61398
61854
|
if (updateResult?.effectiveProperties) {
|
|
61399
61855
|
const delta = collectNarrowedTopLevelKeys(newProperties, updateResult.effectiveProperties);
|
|
61400
|
-
if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, delta);
|
|
61856
|
+
if (Object.keys(delta).length > 0) narrowedByLogicalId.set(outcome.logicalId, redactSecretsForState(delta, secrets, revertBaseline, STATE_SOURCED_READBACK_RULES));
|
|
61401
61857
|
}
|
|
61402
61858
|
} catch (captureErr) {
|
|
61403
|
-
logger.warn(` ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted, but the provider's reported effective properties could not be read — ${captureErr instanceof Error ? captureErr.message : String(captureErr)}`);
|
|
61859
|
+
logger.warn(` ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): reverted, but the provider's reported effective properties could not be read — ${maskSecretsInText(captureErr instanceof Error ? captureErr.message : String(captureErr), secrets)}`);
|
|
61404
61860
|
}
|
|
61405
61861
|
} catch (err) {
|
|
61406
61862
|
if (err instanceof ResourceUpdateNotSupportedError) {
|
|
61407
61863
|
totalUnsupported++;
|
|
61408
|
-
logger.warn(` ⊘ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not revert — ${err.message}`);
|
|
61864
|
+
logger.warn(` ⊘ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not revert — ${maskSecretsInText(err.message, secrets)}`);
|
|
61409
61865
|
return;
|
|
61410
61866
|
}
|
|
61411
61867
|
totalFailed++;
|
|
61412
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
61868
|
+
const msg = maskSecretsInText(err instanceof Error ? err.message : String(err), secrets);
|
|
61413
61869
|
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): AWS update failed — ${msg}`);
|
|
61414
61870
|
}
|
|
61415
61871
|
}), concurrency);
|
|
@@ -61467,10 +61923,11 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
61467
61923
|
}
|
|
61468
61924
|
const summaryParts = [`${totalSucceeded} reverted`];
|
|
61469
61925
|
if (totalUnsupported > 0) summaryParts.push(`${totalUnsupported} update-not-supported`);
|
|
61926
|
+
if (totalUnresolvable > 0) summaryParts.push(`${totalUnresolvable} reference-unresolvable`);
|
|
61470
61927
|
if (totalFailed > 0) summaryParts.push(`${totalFailed} failed`);
|
|
61471
61928
|
logger.info(`\nRevert summary: ${summaryParts.join(", ")}.`);
|
|
61472
61929
|
if (totalUnsupported > 0) logger.warn(`${totalUnsupported} resource(s) cannot be reverted in place — re-deploy the stack with cdkd deploy --replace, or destroy + redeploy to push the cdkd-state values back into AWS.`);
|
|
61473
|
-
if (totalFailed > 0 || totalUnsupported > 0) throw new PartialFailureError(`Revert completed with ${totalFailed + totalUnsupported} resource error(s) (${totalFailed} AWS update failure(s), ${totalUnsupported} update-not-supported). Re-run 'cdkd drift <stack>' to see the remaining drift, then 'cdkd drift <stack> --revert' to retry.`);
|
|
61930
|
+
if (totalFailed > 0 || totalUnsupported > 0 || totalUnresolvable > 0) throw new PartialFailureError(`Revert completed with ${totalFailed + totalUnsupported + totalUnresolvable} resource error(s) (${totalFailed} AWS update failure(s), ${totalUnsupported} update-not-supported, ${totalUnresolvable} whose dynamic reference(s) could not be resolved — those never reached provider.update; grant the caller secretsmanager:GetSecretValue / ssm:GetParameter, or fix the reference). Re-run 'cdkd drift <stack>' to see the remaining drift, then 'cdkd drift <stack> --revert' to retry.`);
|
|
61474
61931
|
}
|
|
61475
61932
|
/**
|
|
61476
61933
|
* Print the planned state mutations for `--accept` (no AWS calls). One
|
|
@@ -61485,7 +61942,14 @@ function printAcceptPlan(reports) {
|
|
|
61485
61942
|
process.stdout.write(`\nPlan (--accept): update cdkd state for ${report.stackName} (${report.region}):\n`);
|
|
61486
61943
|
for (const o of drifted) {
|
|
61487
61944
|
process.stdout.write(` ~ ${o.logicalId} (${o.resourceType})\n`);
|
|
61488
|
-
for (const change of o.changes)
|
|
61945
|
+
for (const change of o.changes) {
|
|
61946
|
+
const refusal = acceptRefusalReason(change, o.maskedPaths);
|
|
61947
|
+
if (refusal !== void 0) {
|
|
61948
|
+
process.stdout.write(` ${change.path}: SKIPPED — ${refusal}\n`);
|
|
61949
|
+
continue;
|
|
61950
|
+
}
|
|
61951
|
+
process.stdout.write(` ${change.path}: ${formatScalar(change.stateValue)} -> ${formatScalar(change.awsValue)}\n`);
|
|
61952
|
+
}
|
|
61489
61953
|
}
|
|
61490
61954
|
}
|
|
61491
61955
|
}
|
|
@@ -61506,20 +61970,23 @@ function printRevertPlan(reports) {
|
|
|
61506
61970
|
const stateResource = report.state.resources[o.logicalId];
|
|
61507
61971
|
if (stateResource) {
|
|
61508
61972
|
const preserved = findRevertPreservedTagKeys(o.changes, stateResource.observedProperties ?? stateResource.properties ?? {}, o.awsProperties);
|
|
61509
|
-
|
|
61973
|
+
const cannotMaskKeys = o.referencesUnresolved;
|
|
61974
|
+
if (cannotMaskKeys && preserved.length > 0) process.stdout.write(` ! ${preserved.length} AWS-authored tag(s) will be preserved, but cdkd could not resolve this resource's dynamic reference(s), so their names are withheld — they come from the live readback and cannot be checked for secrets without them.\n`);
|
|
61975
|
+
if (!cannotMaskKeys && preserved.length > 0) {
|
|
61510
61976
|
const tagWord = preserved.length === 1 ? "tag" : "tags";
|
|
61511
61977
|
process.stdout.write(` ! reverting this tag list KEEPS ${preserved.length} AWS-authored ${tagWord} the baseline does not carry:\n`);
|
|
61512
|
-
for (const path of preserved) process.stdout.write(` ${path}\n`);
|
|
61978
|
+
for (const path of preserved) process.stdout.write(` ${maskSecretsInText(path, o.secrets)}\n`);
|
|
61513
61979
|
const othersRevert = stateResource.observedProperties === void 0 ? "" : `Every other tag reverts normally. `;
|
|
61514
61980
|
process.stdout.write(` ${othersRevert}A service may require these (ECS needs AmazonECSManaged for managed scaling); 'aws:'-prefixed keys are AWS-reserved and cannot be removed by hand.\n`);
|
|
61515
61981
|
}
|
|
61516
61982
|
}
|
|
61517
61983
|
if (stateResource && stateResource.observedProperties === void 0) {
|
|
61518
61984
|
const unbaselined = findRevertUnbaselinedAwsKeys(o.changes, stateResource.properties ?? {}, o.awsProperties);
|
|
61519
|
-
if (unbaselined.length > 0) {
|
|
61985
|
+
if (o.referencesUnresolved && unbaselined.length > 0) process.stdout.write(` ! ${unbaselined.length} AWS-authored value(s) will be left untouched, but cdkd could not resolve this resource's dynamic reference(s), so their paths are withheld — they come from the live readback and cannot be checked for secrets without them.\n`);
|
|
61986
|
+
else if (unbaselined.length > 0) {
|
|
61520
61987
|
const word = unbaselined.length === 1 ? "value" : "values";
|
|
61521
61988
|
process.stdout.write(` ! this resource has no observed-capture baseline, so the revert pushes the raw TEMPLATE and LEAVES ${unbaselined.length} AWS-authored ${word} untouched:\n`);
|
|
61522
|
-
for (const path of unbaselined) process.stdout.write(` ${path}\n`);
|
|
61989
|
+
for (const path of unbaselined) process.stdout.write(` ${maskSecretsInText(path, o.secrets)}\n`);
|
|
61523
61990
|
process.stdout.write(` The template does not declare these, so cdkd cannot tell an AWS-authored value from an out-of-band change and will not reset either (issue #1626). Run 'cdkd state refresh-observed ${report.stackName}' (or re-deploy) to populate observedProperties if you want them reverted too.\n`);
|
|
61524
61991
|
}
|
|
61525
61992
|
}
|
|
@@ -80414,7 +80881,7 @@ function createMigrateCommand() {
|
|
|
80414
80881
|
*/
|
|
80415
80882
|
function buildProgram() {
|
|
80416
80883
|
const program = new Command();
|
|
80417
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.
|
|
80884
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.283.33");
|
|
80418
80885
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
80419
80886
|
const { profile } = actionCommand.optsWithGlobals();
|
|
80420
80887
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|