@go-to-k/cdkd 0.284.26 → 0.284.28
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/README.md +6 -0
- package/dist/{asg-provider-CS5j-5z6.js → asg-provider-DWL4tZYx.js} +2 -2
- package/dist/{asg-provider-CS5j-5z6.js.map → asg-provider-DWL4tZYx.js.map} +1 -1
- package/dist/cli.js +310 -23
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DNCs229s.js → deploy-engine-DP_MejgP.js} +4 -4
- package/dist/{deploy-engine-DNCs229s.js.map → deploy-engine-DP_MejgP.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
|
|
2
|
+
import { $ as endCommandInterruptScope, $n as LocalInvokeBuildError$1, $t as rewriteTemplateAssetReferences, A as MULTI_REGION_RECREATE_BLOCKED_TYPES, An as warnDeprecatedNoPrefixCliFlag, At as scrubResourceRecord, B as collectDeclaredOutputNames, Bn as AssemblyReader, Bt as withRetry, C as ccRoutedFinalSnapshotError, Cn as resolveAutoAssetStorage, Ct as STATE_SOURCED_READBACK_RULES, D as unsupportedFinalSnapshotError, Dn as resolveStateBucketWithDefaultAndSource, Dt as isSingleDynamicReferenceToken, E as refusesFinalSnapshot, En as resolveStateBucketWithDefault, Et as dynamicReferenceTokens, F as cyan, Fn as uploadCfnTemplate, Ft as s3BucketWebsiteUrl, G as stateKeySecretExposure, Gn as getAwsClients, Gt as rebuildClientForBucketRegion, H as exportAliasCollisionScrubWarning, Ht as TemplateParser, I as gray, In as expectedOwnerParam, It as applyRoleArnIfSet, J as clearOnUpdateRemoval, Jt as stringifyValue, K as IAMRoleProvider, Kn as resetAwsClients, Kt as shouldRetainResource, L as green, Ln as PARTITION_TABLE, Lt as DiffCalculator, M as renderStatefulReason, Mn as CFN_TEMPLATE_URL_LIMIT, Mt as s3BucketDomainName, N as formatResourceLine, Nn as MIGRATE_TMP_PREFIX, Nt as s3BucketDualStackDomainName, O as makeCanonicalizePropertiesFn, On as resolveUseCdkBootstrapAssets, Ot as maskSecretsInText, P as bold, Pn as findLargeInlineResources, Pt as s3BucketRegionalDomainName, Q as beginCommandInterruptScope, Qn as DeployCancelledError, Qt as loadPublishableAssetManifest, R as red, Rn as canonicalizeRegion, Rt as INTRINSIC_KEYS, S as buildFinalSnapshotIdentifier, Sn as resolveApp, St as STATE_SOURCED_CROSS_GENERATION_RULES, T as isFinalSnapshotError, Tn as resolveSkipPrefix, Tt as createSecretMasker, U as isExportAliasCollision, Un as resolveBucketRegion, Ut as LockManager, V as collectPublishedOutputNames, Vn as processStackMessages, Vt as DagBuilder, W as secretBearingStateKeyWarning, Wn as AwsClients, Wt as S3StateBackend, X as findActionableSilentDrops, Xt as buildAssetRedirectMap, Y as ProviderRegistry, Yn as CdkdError, Yt as WorkGraph, Z as findSilentDropProperties, Zt as createAssetRedirectResolver, _ as maskingRetryLogger, _n as getDockerImageBySourceHash, _r as isMarkedNonRetryable, _t as readConfigString, a as DeploymentEventsStore, an as getBootstrapMarkerKey, ar as PartialFailureError, at as isTerminationProtectionPropagationError, b as ATOMIC_FINAL_SNAPSHOT_TYPES, bn as getDefaultStateBucketName, br as markNonRetryable, bt as requireConfigObject, c as planRollback, cn as validateAssetBucketName, cr as ResourceUpdateNotSupportedError, ct as getAccountInfo, d as replayRollback, dn as buildDockerImage, dt as normalizeAwsTagsToCfn, en as escapeRegExp, er as LocalMigrateError, et as isInterruptedWaitError, f as updatePartialMessage, fn as formatDockerLoginError, ft as resolveExplicitPhysicalId, g as withResourceDeadline, gn as AssetManifestLoader, gr as withErrorHandling, gt as configStringRefusal, h as deleteSkipReason, hn as runDockerStreaming, hr as normalizeAwsError, ht as configBooleanRefusal, i as DeploymentEventsReader, in as ensureAssetStorage, ir as NestedStackChildDirectDestroyError, it as disableInstanceApiTermination, j as isStatefulRecreateTargetSync, jn as CFN_TEMPLATE_BODY_LIMIT, jt as s3BucketArn, k as extractDeploymentEventError, kn as stateBucketExistenceConfirmed, kt as redactSecretsForState, l as producerRegionsFromState, ln as validateContainerRepoName, lr as StackHasActiveImportsError, lt as refStateLookupFromResource, m as UNSPECIFIED_SKIP_REASON, mn as runDockerForeground, mt as coerceCfnBoolean, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as AssetModeResolver, nt as CloudControlProvider, o as classifyReplaySecretRegion, on as parseBootstrapMarker, or as ProvisioningError, ot as IntrinsicFunctionResolver, p as updatePartialReason, pn as getDockerCmd, pt as assertRegionMatch, q as collectInlinePolicyNamesManagedBySiblings, qn as setAwsClients, qt as AssetPublisher, r as DeployEngine, rn as BOOTSTRAP_MARKER_PREFIX, rr as MissingCdkCliError, rt as slowCcOperationTimeoutMs, s as planFailedOps, sn as readBootstrapMarkerBody, sr as ResourceTimeoutError, st as cfnRefValueFromPhysicalId, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as stripControlChars, tr as LocalStartServiceError, tt as startInterruptWatch, u as replayFailedOperations, un as buildDenyExternalAccessPolicy, ur as StackTerminationProtectionError, ut as WAFv2WebACLProvider, v as IMPLICIT_DELETE_DEPENDENCIES, vn as Synthesizer, vr as isRetryableTransientError, vt as replayWarn, w as createPreDeleteFinalSnapshot, wn as resolveCaptureObservedState, wt as TEMPLATE_SOURCED_RULES, x as PRE_DELETE_SNAPSHOT_TYPES, xn as getLegacyStateBucketName, xt as requireConfigString, y as computeImplicitDeleteEdges, yn as synthesisStatusMessage, yr as isThrottlingError, yt as requireConfigArray, z as yellow, zn as derivePartitionAndUrlSuffix, zt as describeTypeWithThrottleRetry } from "./deploy-engine-DP_MejgP.js";
|
|
3
3
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, 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-DWL4tZYx.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";
|
|
@@ -63648,6 +63648,90 @@ var DriftDetectedError = class DriftDetectedError extends CdkdError {
|
|
|
63648
63648
|
}
|
|
63649
63649
|
};
|
|
63650
63650
|
/**
|
|
63651
|
+
* Detection found NO drift, but cdkd DELIBERATELY REFUSED to compare at least
|
|
63652
|
+
* one resource's secret-bearing properties (issue #2108).
|
|
63653
|
+
*
|
|
63654
|
+
* SCOPED TO REFUSALS, not to everything the report calls `notCompared`. The
|
|
63655
|
+
* roll-up also contains resources whose only problem is a surviving
|
|
63656
|
+
* `{{resolve:ssm-secure:...}}` token — a pre-existing population cdkd resolves
|
|
63657
|
+
* for nobody, which can never clear on a re-run, and which this change did not
|
|
63658
|
+
* create. Exiting non-zero for them would break `cdkd drift` in CI forever over
|
|
63659
|
+
* an unrelated defect, so the exit reads `comparisonRefused` and the report
|
|
63660
|
+
* reads `referencesUnresolved`. See that field's note.
|
|
63661
|
+
*
|
|
63662
|
+
* WHY A NON-ZERO EXIT, and why it is a PRESERVATION rather than a new signal.
|
|
63663
|
+
* Pre-#2108 that population resolved the reference in the WRONG region, the
|
|
63664
|
+
* baseline could never equal what AWS held, and the resource was reported
|
|
63665
|
+
* `drifted` -- so `cdkd drift` exited 1 and every CI gate keyed on the exit code
|
|
63666
|
+
* fired. #2108 correctly stopped comparing against a foreign region's plaintext,
|
|
63667
|
+
* which made the same population report `clean`; without this the command would
|
|
63668
|
+
* exit 0 and print `no drift detected` for a stack whose secret-bearing
|
|
63669
|
+
* properties were never looked at. Round 1 of that change surfaced the refusal
|
|
63670
|
+
* in `--json` (`notCompared`) and in the human report, but the exit code -- the
|
|
63671
|
+
* signal most CI gates actually read -- still said "pass".
|
|
63672
|
+
*
|
|
63673
|
+
* WHY EXIT 2 RATHER THAN 1. `2` is this repo's established "work completed but
|
|
63674
|
+
* something was SKIPPED" code (`cdkd destroy` / `cdkd deploy` / `cdkd rollback`,
|
|
63675
|
+
* and this command's own `--revert` partial failure), and the three outcomes are
|
|
63676
|
+
* genuinely distinct: nothing drifted and everything was compared (0), drift was
|
|
63677
|
+
* detected (1), nothing drifted but the comparison was incomplete (2). Drift
|
|
63678
|
+
* WINS when both hold -- a drifted resource is the stronger, actionable signal
|
|
63679
|
+
* and keeps exiting 1 exactly as it does today.
|
|
63680
|
+
*
|
|
63681
|
+
* SCOPE: detection-only mode. `--accept` / `--revert` keep their documented exit
|
|
63682
|
+
* codes (an `--accept` that refuses a secret-bearing property still exits 0, and
|
|
63683
|
+
* a partially-failed `--revert` still exits 2 through `PartialFailureError`) --
|
|
63684
|
+
* those modes report per-resource refusals on their own paths, and changing them
|
|
63685
|
+
* would alter what a remediation run means, which is not what this closes.
|
|
63686
|
+
*
|
|
63687
|
+
* WHAT DOES NOT TRIGGER IT: a stack whose properties reference
|
|
63688
|
+
* `{{resolve:ssm-secure:...}}` still exits 0. Those properties genuinely are not
|
|
63689
|
+
* compared and the report says so, but nothing REFUSED them — cdkd has never
|
|
63690
|
+
* resolved that spelling — so the condition is permanent, unclearable by any
|
|
63691
|
+
* action the user can take, and predates this change. It is reported as
|
|
63692
|
+
* information and kept out of the exit code.
|
|
63693
|
+
*
|
|
63694
|
+
* Carries no message of its own for the same reason {@link DriftDetectedError}
|
|
63695
|
+
* does -- the report was already printed.
|
|
63696
|
+
*/
|
|
63697
|
+
var DriftComparisonRefusedError = class DriftComparisonRefusedError extends CdkdError {
|
|
63698
|
+
silent = true;
|
|
63699
|
+
exitCode = 2;
|
|
63700
|
+
constructor() {
|
|
63701
|
+
super("drift comparison refused", "DRIFT_COMPARISON_REFUSED");
|
|
63702
|
+
this.name = "DriftComparisonRefusedError";
|
|
63703
|
+
Object.setPrototypeOf(this, DriftComparisonRefusedError.prototype);
|
|
63704
|
+
}
|
|
63705
|
+
};
|
|
63706
|
+
/**
|
|
63707
|
+
* Every outcome on one report that was only PARTIALLY compared -- `drifted` or
|
|
63708
|
+
* `clean`, carrying `referencesUnresolved` (issue #2108).
|
|
63709
|
+
*
|
|
63710
|
+
* ONE spelling, because there are now THREE readers that must agree: the
|
|
63711
|
+
* `--json` `notCompared` roll-up, the human report's `PARTIALLY compared` block,
|
|
63712
|
+
* and the EXIT CODE. A predicate spelled per reader is how the payload and the
|
|
63713
|
+
* exit code come to disagree about the same run.
|
|
63714
|
+
*/
|
|
63715
|
+
function notComparedOutcomes(report) {
|
|
63716
|
+
return report.outcomes.filter((o) => (o.kind === "drifted" || o.kind === "clean") && o.referencesUnresolved);
|
|
63717
|
+
}
|
|
63718
|
+
/**
|
|
63719
|
+
* Every outcome whose comparison cdkd DELIBERATELY REFUSED (issue #2108) — the
|
|
63720
|
+
* strict subset of {@link notComparedOutcomes} that drives the EXIT CODE.
|
|
63721
|
+
*
|
|
63722
|
+
* Deliberately NOT the same predicate, and the asymmetry is the point: the
|
|
63723
|
+
* REPORT covers everything that was not compared, because as information both
|
|
63724
|
+
* populations matter equally, while the exit code covers only the population
|
|
63725
|
+
* this change created. Driving it off `referencesUnresolved` would exit
|
|
63726
|
+
* non-zero forever for every stack holding an `{{resolve:ssm-secure:...}}`
|
|
63727
|
+
* reference — permanent, unclearable, and unrelated to this issue. See the
|
|
63728
|
+
* `comparisonRefused` field note, and issue
|
|
63729
|
+
* [#2135](https://github.com/go-to-k/cdkd/issues/2135) for the structural fix.
|
|
63730
|
+
*/
|
|
63731
|
+
function refusedComparisonOutcomes(report) {
|
|
63732
|
+
return report.outcomes.filter((o) => (o.kind === "drifted" || o.kind === "clean") && o.comparisonRefused);
|
|
63733
|
+
}
|
|
63734
|
+
/**
|
|
63651
63735
|
* `cdkd drift [<stack>...]` command implementation.
|
|
63652
63736
|
*
|
|
63653
63737
|
* Three operating modes (mutually exclusive):
|
|
@@ -63716,8 +63800,10 @@ async function driftCommand(stacks, options) {
|
|
|
63716
63800
|
if (options.json) writeJsonReport(reports);
|
|
63717
63801
|
else writeHumanReport(reports);
|
|
63718
63802
|
const drifted = reports.some((r) => r.outcomes.some((o) => o.kind === "drifted"));
|
|
63803
|
+
const anyRefused = reports.some((r) => refusedComparisonOutcomes(r).length > 0);
|
|
63719
63804
|
if (!options.accept && !options.revert) {
|
|
63720
63805
|
if (drifted) throw new DriftDetectedError();
|
|
63806
|
+
if (anyRefused) throw new DriftComparisonRefusedError();
|
|
63721
63807
|
return;
|
|
63722
63808
|
}
|
|
63723
63809
|
if (!drifted) {
|
|
@@ -63938,6 +64024,164 @@ function collectDynamicReferencePaths(value, into, path = "") {
|
|
|
63938
64024
|
if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) collectDynamicReferencePaths(v, into, path === "" ? k : `${path}.${k}`);
|
|
63939
64025
|
}
|
|
63940
64026
|
/**
|
|
64027
|
+
* The drift command's resolvers for ONE stack: the stack's own, plus one pinned
|
|
64028
|
+
* sibling per FOREIGN region an ARN-named reference asks for (issue
|
|
64029
|
+
* [#2108](https://github.com/go-to-k/cdkd/issues/2108)).
|
|
64030
|
+
*
|
|
64031
|
+
* The structural twin of the rollback replay's `ReplayResolvers`
|
|
64032
|
+
* (`src/deployment/rollback-executor.ts`), and deliberately a SEPARATE class
|
|
64033
|
+
* rather than an export of that one: the two are ~20 lines of caching around a
|
|
64034
|
+
* constructor, and exporting the replay's would put a rollback-internal name on
|
|
64035
|
+
* the drift command's contract for no behavioural gain. What is SHARED is the
|
|
64036
|
+
* part with a decision in it — `classifyReplaySecretRegion`, imported.
|
|
64037
|
+
*
|
|
64038
|
+
* One instance per STACK, not per resource and not per op — the resolved-value
|
|
64039
|
+
* cache lives on the resolver INSTANCE since issue
|
|
64040
|
+
* [#1933](https://github.com/go-to-k/cdkd/issues/1933), so a resolver per
|
|
64041
|
+
* resource would re-fetch every referenced secret once per resource. The pinned
|
|
64042
|
+
* siblings are cached for the same reason: a 50-resource stack sharing one
|
|
64043
|
+
* foreign ARN must pay one `GetSecretValue`, not fifty.
|
|
64044
|
+
*
|
|
64045
|
+
* A pinned sibling is a PLAIN resolver, deliberately NOT the resolver class's
|
|
64046
|
+
* own `producerRegionGuest`, and the argument is `ReplayResolvers`' verbatim:
|
|
64047
|
+
* {@link forRegion} is reached ONLY from a `named-region` verdict, which
|
|
64048
|
+
* `classifyReplaySecretRegion` returns only when the SECRET_ID / parameter name
|
|
64049
|
+
* starts with `arn:` and carries a region. So a pinned sibling only ever
|
|
64050
|
+
* resolves an expression whose KEY EMBEDS THE REGION IT IS BEING RESOLVED IN,
|
|
64051
|
+
* and the process-global `recordedSecretExpressions` store — keyed by the
|
|
64052
|
+
* expression string alone — cannot have two regions sharing that key. If a
|
|
64053
|
+
* future change ever routes a region-LESS expression here, the argument dies
|
|
64054
|
+
* with it and the sibling needs the guest flag.
|
|
64055
|
+
*/
|
|
64056
|
+
var DriftSecretResolvers = class {
|
|
64057
|
+
/** The stack's own resolver — every `local` verdict resolves through this. */
|
|
64058
|
+
primary;
|
|
64059
|
+
pinned = /* @__PURE__ */ new Map();
|
|
64060
|
+
stackRegion;
|
|
64061
|
+
constructor(stackRegion) {
|
|
64062
|
+
this.stackRegion = stackRegion;
|
|
64063
|
+
this.primary = new IntrinsicFunctionResolver(stackRegion);
|
|
64064
|
+
}
|
|
64065
|
+
/** The resolver that must answer for `region` — `primary` when it is the stack's own. */
|
|
64066
|
+
forRegion(region) {
|
|
64067
|
+
const target = canonicalizeRegion(region);
|
|
64068
|
+
if (target === canonicalizeRegion(this.stackRegion)) return this.primary;
|
|
64069
|
+
const cached = this.pinned.get(target);
|
|
64070
|
+
if (cached) return cached;
|
|
64071
|
+
const scoped = new IntrinsicFunctionResolver(target);
|
|
64072
|
+
this.pinned.set(target, scoped);
|
|
64073
|
+
return scoped;
|
|
64074
|
+
}
|
|
64075
|
+
};
|
|
64076
|
+
/**
|
|
64077
|
+
* A DELIBERATE refusal to resolve a state-recorded dynamic reference (issue
|
|
64078
|
+
* #2108) — as opposed to a failure to READ one.
|
|
64079
|
+
*
|
|
64080
|
+
* The distinction is load-bearing for what the user is told: "could not
|
|
64081
|
+
* resolve" sends a reader hunting for a missing `secretsmanager:GetSecretValue`
|
|
64082
|
+
* grant, while a refusal is cdkd declining to resolve a reference it CAN read
|
|
64083
|
+
* but cannot attribute to a region. Both call sites that catch
|
|
64084
|
+
* `resolveStateSecretExpressions` pick their wording from this class.
|
|
64085
|
+
*
|
|
64086
|
+
* DEFINED POSITIVELY, and that is the whole point of the class existing rather
|
|
64087
|
+
* than a second `err.code === ...` comparison at each site. Both sites used to
|
|
64088
|
+
* enumerate the one code they knew (`DRIFT_SECRET_REGION_AMBIGUOUS`) while
|
|
64089
|
+
* `DRIFT_SECRET_TOKEN_SCAN_MISMATCH` — whose own message says "Refusing rather
|
|
64090
|
+
* than resolving" — silently took the read-failure wording. Enumerating bad
|
|
64091
|
+
* shapes loses that race every time a third refusal is added; a refusal that
|
|
64092
|
+
* has to declare itself one cannot be added without answering the question.
|
|
64093
|
+
*
|
|
64094
|
+
* The `code` stays per-refusal: it is what the message and any future
|
|
64095
|
+
* programmatic consumer key on. What this class adds is the CATEGORY.
|
|
64096
|
+
*/
|
|
64097
|
+
var DriftSecretRefusalError = class DriftSecretRefusalError extends CdkdError {
|
|
64098
|
+
/** Marker read by {@link isDriftSecretRefusal}; see the class note. */
|
|
64099
|
+
driftSecretRefusal = true;
|
|
64100
|
+
constructor(message, code) {
|
|
64101
|
+
super(message, code);
|
|
64102
|
+
this.name = "DriftSecretRefusalError";
|
|
64103
|
+
Object.setPrototypeOf(this, DriftSecretRefusalError.prototype);
|
|
64104
|
+
}
|
|
64105
|
+
};
|
|
64106
|
+
/**
|
|
64107
|
+
* Whether an error thrown out of `resolveStateSecretExpressions` is a
|
|
64108
|
+
* deliberate REFUSAL rather than a failed read. See
|
|
64109
|
+
* {@link DriftSecretRefusalError}.
|
|
64110
|
+
*/
|
|
64111
|
+
function isDriftSecretRefusal(err) {
|
|
64112
|
+
return err instanceof DriftSecretRefusalError;
|
|
64113
|
+
}
|
|
64114
|
+
/**
|
|
64115
|
+
* The refusal a region-AMBIGUOUS drift reference throws (issue #2108).
|
|
64116
|
+
*
|
|
64117
|
+
* A plain throw: BOTH call sites already wrap `resolveStateSecretExpressions`
|
|
64118
|
+
* in a per-resource catch that degrades instead of aborting the command, and
|
|
64119
|
+
* the degradation each one performs is exactly the fail-closed behaviour this
|
|
64120
|
+
* refusal wants.
|
|
64121
|
+
*
|
|
64122
|
+
* - DETECTION falls back to the UNRESOLVED baseline, whose `{{resolve:...}}`
|
|
64123
|
+
* leaves `calculateResourceDrift` skips — so the secret-bearing property is
|
|
64124
|
+
* NOT COMPARED rather than compared against a foreign region's plaintext.
|
|
64125
|
+
* That is strictly better than the phantom drift this issue reports, and the
|
|
64126
|
+
* offline `seededSecretPaths` still masks the position.
|
|
64127
|
+
* - REVERT counts the resource as unresolvable and returns BEFORE
|
|
64128
|
+
* `provider.update`, so nothing is written. Refusing is strictly better than
|
|
64129
|
+
* the alternative it replaces: resolving a producer-region reference against
|
|
64130
|
+
* the consumer's region does not fail, it succeeds with the WRONG credential
|
|
64131
|
+
* and writes it to a live resource.
|
|
64132
|
+
*
|
|
64133
|
+
* Names the reference, the regions and the remedy. Never a resolved value:
|
|
64134
|
+
* nothing has been resolved when this is thrown (the refusal runs over the
|
|
64135
|
+
* WHOLE leaf before any reference in it is fetched), and the expression is the
|
|
64136
|
+
* same string `state.json` already stores in the clear.
|
|
64137
|
+
*/
|
|
64138
|
+
function regionAmbiguousDriftSecretError(logicalId, propertyPath, secretName, foreignProducerRegions, consumerRegion) {
|
|
64139
|
+
const where = propertyPath === "" ? "" : ` property '${propertyPath}'`;
|
|
64140
|
+
return new DriftSecretRefusalError(`${logicalId}${where}: the secret reference '${secretName}' carries no region of its own, and this stack read across a region boundary (producer region(s) on record: ${foreignProducerRegions.join(", ")}), so it may have been resolved in one of those rather than in '${consumerRegion}'. A secret of the same name in two regions is two independent values, so cdkd would compare against — and with --revert WRITE — the WRONG secret. Refusing instead. Spell the reference as a full ARN, which names its region and is resolved there, then re-run 'cdkd drift'.`, "DRIFT_SECRET_REGION_AMBIGUOUS");
|
|
64141
|
+
}
|
|
64142
|
+
/**
|
|
64143
|
+
* Re-resolve one LEAF string, sending each `{{resolve:...}}` reference in it to
|
|
64144
|
+
* the region {@link classifyReplaySecretRegion} says must answer (issue #2108).
|
|
64145
|
+
*
|
|
64146
|
+
* Mirrors the rollback replay's `resolveLeafByRegion` down to the ordering, and
|
|
64147
|
+
* for the same reasons:
|
|
64148
|
+
*
|
|
64149
|
+
* - Refuses FIRST, over the whole leaf, before any reference is fetched. A leaf
|
|
64150
|
+
* can splice several references together, and resolving the safe ones first
|
|
64151
|
+
* would leave half a credential fetched — and CACHED, and recorded as a
|
|
64152
|
+
* redaction needle — for a resource that is about to be refused anyway.
|
|
64153
|
+
* - With no foreign-region reference (every leaf on every pre-#2108 code path)
|
|
64154
|
+
* the leaf goes to `resolveDynamicReferences` WHOLE, byte-for-byte as before.
|
|
64155
|
+
* That method collects its matches from the ORIGINAL string, so a resolved
|
|
64156
|
+
* plaintext that is itself token-shaped is never re-resolved (issue #1917),
|
|
64157
|
+
* and this change does not want to relitigate any of it.
|
|
64158
|
+
* - With one, the leaf is rebuilt segment by segment so each reference can be
|
|
64159
|
+
* resolved by its OWN region's resolver, since `resolveDynamicReferences`
|
|
64160
|
+
* resolves every token in the string it is handed with the one resolver it
|
|
64161
|
+
* is called on. Each token is resolved ALONE and concatenated, so no resolved
|
|
64162
|
+
* value is re-scanned for tokens either.
|
|
64163
|
+
*
|
|
64164
|
+
* `dynamicReferenceTokens` returns the tokens in order and non-overlapping, so
|
|
64165
|
+
* walking the leaf with a moving `indexOf` cursor reproduces their positions
|
|
64166
|
+
* exactly, duplicates included.
|
|
64167
|
+
*/
|
|
64168
|
+
async function resolveDriftLeafByRegion(leaf, propertyPath, logicalId, consumerRegion, producerRegions, resolvers, ctx) {
|
|
64169
|
+
const verdicts = dynamicReferenceTokens(leaf).map((token) => [token, classifyReplaySecretRegion(token, consumerRegion, producerRegions)]);
|
|
64170
|
+
for (const [, verdict] of verdicts) if (verdict.kind === "ambiguous") throw regionAmbiguousDriftSecretError(logicalId, propertyPath, verdict.secretName, verdict.foreignProducerRegions, consumerRegion);
|
|
64171
|
+
if (!verdicts.some(([, verdict]) => verdict.kind === "named-region")) return await resolvers.primary.resolveDynamicReferences(leaf, ctx);
|
|
64172
|
+
let out = "";
|
|
64173
|
+
let cursor = 0;
|
|
64174
|
+
for (const [token, verdict] of verdicts) {
|
|
64175
|
+
const at = leaf.indexOf(token, cursor);
|
|
64176
|
+
if (at < 0) throw new DriftSecretRefusalError(`${logicalId}${propertyPath === "" ? "" : ` property '${propertyPath}'`}: could not locate a scanned dynamic reference in the value it was scanned from. Refusing rather than resolving it in '${consumerRegion}', which would be the wrong region for a reference that names another one. This is an internal invariant failure — please report it with the resource type and property path.`, "DRIFT_SECRET_TOKEN_SCAN_MISMATCH");
|
|
64177
|
+
out += leaf.slice(cursor, at);
|
|
64178
|
+
const resolver = verdict.kind === "named-region" ? resolvers.forRegion(verdict.region) : resolvers.primary;
|
|
64179
|
+
out += await resolver.resolveDynamicReferences(token, ctx);
|
|
64180
|
+
cursor = at + token.length;
|
|
64181
|
+
}
|
|
64182
|
+
return out + leaf.slice(cursor);
|
|
64183
|
+
}
|
|
64184
|
+
/**
|
|
63941
64185
|
* Re-resolve the SECRET dynamic references (`{{resolve:secretsmanager:...}}`,
|
|
63942
64186
|
* and an `{{resolve:ssm:...}}` naming a `SecureString`) held by a bag read back
|
|
63943
64187
|
* out of cdkd STATE (issue #1914).
|
|
@@ -63975,10 +64219,15 @@ function collectDynamicReferencePaths(value, into, path = "") {
|
|
|
63975
64219
|
*
|
|
63976
64220
|
* Returns the input by identity when it holds no dynamic reference, so the
|
|
63977
64221
|
* non-secret path is unchanged down to object identity.
|
|
64222
|
+
*
|
|
64223
|
+
* Issue [#2108](https://github.com/go-to-k/cdkd/issues/2108): every reference
|
|
64224
|
+
* is now routed to the region that must ANSWER for it rather than to the
|
|
64225
|
+
* consumer's, through {@link DriftSecretResolvers} — see
|
|
64226
|
+
* {@link resolveDriftLeafByRegion}.
|
|
63978
64227
|
*/
|
|
63979
|
-
async function resolveStateSecretExpressions(props,
|
|
64228
|
+
async function resolveStateSecretExpressions(props, resolvers, secrets, options) {
|
|
63980
64229
|
if (!containsDynamicReference(props)) return props;
|
|
63981
|
-
const { secretPaths, onUnresolved } = options;
|
|
64230
|
+
const { secretPaths, onUnresolved, producerRegions, logicalId, consumerRegion } = options;
|
|
63982
64231
|
const ctx = {
|
|
63983
64232
|
template: { Resources: {} },
|
|
63984
64233
|
resources: {},
|
|
@@ -63987,7 +64236,7 @@ async function resolveStateSecretExpressions(props, resolver, secrets, options =
|
|
|
63987
64236
|
const walk = async (v, path) => {
|
|
63988
64237
|
if (typeof v === "string") {
|
|
63989
64238
|
if (!v.includes("{{resolve:")) return v;
|
|
63990
|
-
const resolved = await
|
|
64239
|
+
const resolved = await resolveDriftLeafByRegion(v, path, logicalId, consumerRegion, producerRegions, resolvers, ctx);
|
|
63991
64240
|
const survivors = survivingDynamicReferences(resolved).filter((t) => v.includes(t));
|
|
63992
64241
|
if (survivors.length > 0) {
|
|
63993
64242
|
if (survivors.some(isSecretBySpelling)) secretPaths?.add(path);
|
|
@@ -64148,7 +64397,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
64148
64397
|
const outcomes = [];
|
|
64149
64398
|
const state = result.state;
|
|
64150
64399
|
const logger = getLogger();
|
|
64151
|
-
const
|
|
64400
|
+
const secretResolvers = new DriftSecretResolvers(region);
|
|
64401
|
+
const producerRegions = producerRegionsFromState(state);
|
|
64152
64402
|
const entries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
64153
64403
|
for (const [logicalId, resource] of entries) {
|
|
64154
64404
|
if (providerRegistry.shouldSkipResource(resource.resourceType)) continue;
|
|
@@ -64220,18 +64470,25 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
64220
64470
|
let comparisonBaseline = baseline;
|
|
64221
64471
|
let secretResolutionFailed = false;
|
|
64222
64472
|
try {
|
|
64223
|
-
comparisonBaseline = await resolveStateSecretExpressions(baseline,
|
|
64473
|
+
comparisonBaseline = await resolveStateSecretExpressions(baseline, secretResolvers, secrets, {
|
|
64224
64474
|
secretPaths,
|
|
64225
|
-
onUnresolved: noteUnresolved
|
|
64475
|
+
onUnresolved: noteUnresolved,
|
|
64476
|
+
logicalId,
|
|
64477
|
+
consumerRegion: region,
|
|
64478
|
+
producerRegions
|
|
64226
64479
|
});
|
|
64227
|
-
if (useObserved) await resolveStateSecretExpressions(resource.properties ?? {},
|
|
64480
|
+
if (useObserved) await resolveStateSecretExpressions(resource.properties ?? {}, secretResolvers, secrets, {
|
|
64228
64481
|
secretPaths,
|
|
64229
|
-
onUnresolved: noteUnresolved
|
|
64482
|
+
onUnresolved: noteUnresolved,
|
|
64483
|
+
logicalId,
|
|
64484
|
+
consumerRegion: region,
|
|
64485
|
+
producerRegions
|
|
64230
64486
|
});
|
|
64231
64487
|
} catch (err) {
|
|
64232
64488
|
secretResolutionFailed = true;
|
|
64233
64489
|
comparisonBaseline = baseline;
|
|
64234
|
-
|
|
64490
|
+
const refused = isDriftSecretRefusal(err);
|
|
64491
|
+
logger.warn(`${logicalId} (${resource.resourceType}): ` + (refused ? "refused to resolve a dynamic reference this resource's state records, so its secret-bearing properties are NOT compared — " : "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)}`);
|
|
64235
64492
|
secrets.clear();
|
|
64236
64493
|
}
|
|
64237
64494
|
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.`);
|
|
@@ -64247,10 +64504,14 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
64247
64504
|
unionWalkObjects: useObserved,
|
|
64248
64505
|
unorderedPaths
|
|
64249
64506
|
}), secrets, secretResolutionFailed ? seededSecretPaths : secretPaths);
|
|
64507
|
+
const referencesUnresolved = secretResolutionFailed || unresolvedTokens.size > 0;
|
|
64508
|
+
const comparisonRefused = secretResolutionFailed;
|
|
64250
64509
|
if (reported.changes.length === 0) outcomes.push({
|
|
64251
64510
|
kind: "clean",
|
|
64252
64511
|
logicalId,
|
|
64253
|
-
resourceType: resource.resourceType
|
|
64512
|
+
resourceType: resource.resourceType,
|
|
64513
|
+
referencesUnresolved,
|
|
64514
|
+
comparisonRefused
|
|
64254
64515
|
});
|
|
64255
64516
|
else outcomes.push({
|
|
64256
64517
|
kind: "drifted",
|
|
@@ -64259,7 +64520,8 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
64259
64520
|
...reported,
|
|
64260
64521
|
awsProperties: aws,
|
|
64261
64522
|
secrets,
|
|
64262
|
-
referencesUnresolved
|
|
64523
|
+
referencesUnresolved,
|
|
64524
|
+
comparisonRefused
|
|
64263
64525
|
});
|
|
64264
64526
|
}
|
|
64265
64527
|
return {
|
|
@@ -64978,7 +65240,8 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
64978
65240
|
if (driftedOutcomes.length === 0) continue;
|
|
64979
65241
|
await lockManager.acquireLock(report.stackName, report.region, owner, "drift-revert");
|
|
64980
65242
|
const narrowedByLogicalId = /* @__PURE__ */ new Map();
|
|
64981
|
-
const
|
|
65243
|
+
const revertSecretResolvers = new DriftSecretResolvers(report.region);
|
|
65244
|
+
const revertProducerRegions = producerRegionsFromState(report.state);
|
|
64982
65245
|
try {
|
|
64983
65246
|
await runWithConcurrency(driftedOutcomes.map((outcome) => async () => {
|
|
64984
65247
|
const stateResource = report.state.resources[outcome.logicalId];
|
|
@@ -64999,11 +65262,21 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
64999
65262
|
};
|
|
65000
65263
|
let desiredProperties;
|
|
65001
65264
|
try {
|
|
65002
|
-
desiredProperties = await resolveStateSecretExpressions(revertBaseline,
|
|
65003
|
-
|
|
65265
|
+
desiredProperties = await resolveStateSecretExpressions(revertBaseline, revertSecretResolvers, secrets, {
|
|
65266
|
+
onUnresolved: noteUnresolved,
|
|
65267
|
+
logicalId: outcome.logicalId,
|
|
65268
|
+
consumerRegion: report.region,
|
|
65269
|
+
producerRegions: revertProducerRegions
|
|
65270
|
+
});
|
|
65271
|
+
if (stateResource.observedProperties !== void 0) await resolveStateSecretExpressions(stateResource.properties ?? {}, revertSecretResolvers, secrets, {
|
|
65272
|
+
onUnresolved: noteUnresolved,
|
|
65273
|
+
logicalId: outcome.logicalId,
|
|
65274
|
+
consumerRegion: report.region,
|
|
65275
|
+
producerRegions: revertProducerRegions
|
|
65276
|
+
});
|
|
65004
65277
|
} catch (err) {
|
|
65005
65278
|
totalUnresolvable++;
|
|
65006
|
-
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): could not re-resolve the dynamic reference(s) this resource's state records —
|
|
65279
|
+
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): ` + (isDriftSecretRefusal(err) ? `refused to re-resolve a dynamic reference this resource's state records — ` : `could not re-resolve the dynamic reference(s) this resource's state records — `) + `${maskSecretsInText(err instanceof Error ? err.message : String(err), secrets)}`);
|
|
65007
65280
|
return;
|
|
65008
65281
|
}
|
|
65009
65282
|
const unrevertablePaths = outcome.changes.map((c) => c.path).filter((path) => (path.split(".", 1)[0] ?? "").includes("***"));
|
|
@@ -65204,9 +65477,15 @@ function writeJsonReport(reports) {
|
|
|
65204
65477
|
const drifted = r.outcomes.filter((o) => o.kind === "drifted").map((o) => ({
|
|
65205
65478
|
logicalId: o.logicalId,
|
|
65206
65479
|
type: o.resourceType,
|
|
65207
|
-
changes: o.changes
|
|
65480
|
+
changes: o.changes,
|
|
65481
|
+
referencesUnresolved: o.referencesUnresolved
|
|
65208
65482
|
}));
|
|
65209
65483
|
const clean = r.outcomes.filter((o) => o.kind === "clean").map((o) => ({
|
|
65484
|
+
logicalId: o.logicalId,
|
|
65485
|
+
type: o.resourceType,
|
|
65486
|
+
referencesUnresolved: o.referencesUnresolved
|
|
65487
|
+
}));
|
|
65488
|
+
const notCompared = notComparedOutcomes(r).map((o) => ({
|
|
65210
65489
|
logicalId: o.logicalId,
|
|
65211
65490
|
type: o.resourceType
|
|
65212
65491
|
}));
|
|
@@ -65224,7 +65503,8 @@ function writeJsonReport(reports) {
|
|
|
65224
65503
|
drifted,
|
|
65225
65504
|
clean,
|
|
65226
65505
|
notSupported,
|
|
65227
|
-
skipped
|
|
65506
|
+
skipped,
|
|
65507
|
+
notCompared
|
|
65228
65508
|
};
|
|
65229
65509
|
});
|
|
65230
65510
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
@@ -65234,8 +65514,11 @@ function writeHumanReport(reports) {
|
|
|
65234
65514
|
const drifted = report.outcomes.filter((o) => o.kind === "drifted");
|
|
65235
65515
|
const unsupported = report.outcomes.filter((o) => o.kind === "unsupported");
|
|
65236
65516
|
const skippedCount = report.outcomes.filter((o) => o.kind === "skipped").length;
|
|
65237
|
-
const
|
|
65238
|
-
|
|
65517
|
+
const inspected = report.outcomes.length - skippedCount;
|
|
65518
|
+
const notCompared = notComparedOutcomes(report);
|
|
65519
|
+
const checked = inspected - notCompared.length;
|
|
65520
|
+
if (drifted.length === 0) if (notCompared.length > 0) process.stdout.write(`⚠ ${report.stackName} (${report.region}): no drift detected, but ${checked} of ${inspected} resource${inspected === 1 ? "" : "s"} fully checked (${notCompared.length} only partially compared, ${unsupported.length} unsupported)\n`);
|
|
65521
|
+
else process.stdout.write(`✓ ${report.stackName} (${report.region}): no drift detected (${checked} resource${checked === 1 ? "" : "s"} checked, ${unsupported.length} unsupported)\n`);
|
|
65239
65522
|
else {
|
|
65240
65523
|
const word = drifted.length === 1 ? "resource" : "resources";
|
|
65241
65524
|
process.stdout.write(`\n⚠ ${report.stackName} (${report.region}): drift detected on ${drifted.length} ${word}\n\n`);
|
|
@@ -65248,6 +65531,10 @@ function writeHumanReport(reports) {
|
|
|
65248
65531
|
process.stdout.write("\n");
|
|
65249
65532
|
}
|
|
65250
65533
|
}
|
|
65534
|
+
if (notCompared.length > 0) {
|
|
65535
|
+
process.stdout.write(`\n ${notCompared.length} resource(s) only PARTIALLY compared — cdkd could not, or refused to, resolve a dynamic reference their state records, so their secret-bearing properties were NOT compared:\n`);
|
|
65536
|
+
for (const o of notCompared) process.stdout.write(` ! ${o.logicalId} (${o.resourceType})\n`);
|
|
65537
|
+
}
|
|
65251
65538
|
if (unsupported.length > 0) {
|
|
65252
65539
|
process.stdout.write(`\n ${unsupported.length} resource(s) reported as drift unknown — provider does not yet support drift detection:\n`);
|
|
65253
65540
|
for (const o of unsupported) process.stdout.write(` ? ${o.logicalId} (${o.resourceType})\n`);
|
|
@@ -65276,7 +65563,7 @@ function stackRegionOption$2() {
|
|
|
65276
65563
|
* Create the `drift` command.
|
|
65277
65564
|
*/
|
|
65278
65565
|
function createDriftCommand() {
|
|
65279
|
-
const cmd = new Command("drift").description("Detect drift between cdkd state and AWS reality. Exits 0 when
|
|
65566
|
+
const cmd = new Command("drift").description("Detect drift between cdkd state and AWS reality. Exits 0 when nothing drifted, 1 when drift is detected, and 2 when nothing drifted but cdkd REFUSED to compare a resource, because a dynamic reference its state records could not be attributed to a region. Pass --accept to update cdkd state from AWS, or --revert to push cdkd state values back into AWS.").argument("[stacks...]", "Stack name(s) to check (physical CloudFormation names)").option("--all", "Check every stack in the state bucket", false).option("--json", "Output as JSON", false).option("--accept", "Update cdkd state with the AWS-current values for every drifted property (state ← AWS). Mutually exclusive with --revert.", false).option("--revert", "Push cdkd state values back into AWS via provider.update for every drifted resource (AWS ← state). Mutually exclusive with --accept.", false).option("--dry-run", "Print the planned mutations without acquiring a lock or hitting AWS / S3. Honored by --accept and --revert.", false).option("--concurrency <number>", "Maximum concurrent provider.update calls during --revert", (value) => parseInt(value, 10), 4).addOption(stackRegionOption$2()).action(withErrorHandling(driftCommand));
|
|
65280
65567
|
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
65281
65568
|
cmd.addOption(deprecatedRegionOption);
|
|
65282
65569
|
return cmd;
|
|
@@ -84259,7 +84546,7 @@ function createMigrateCommand() {
|
|
|
84259
84546
|
*/
|
|
84260
84547
|
function buildProgram() {
|
|
84261
84548
|
const program = new Command();
|
|
84262
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.
|
|
84549
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.28");
|
|
84263
84550
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
84264
84551
|
const { profile } = actionCommand.optsWithGlobals();
|
|
84265
84552
|
if (profile !== void 0) process.env["AWS_PROFILE"] = profile;
|