@go-to-k/cdkd 0.288.7 → 0.289.0
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 +24 -14
- package/dist/{asg-provider-CRxRmEzr.js → asg-provider-1XVtAjhm.js} +3 -3
- package/dist/{asg-provider-CRxRmEzr.js.map → asg-provider-1XVtAjhm.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-BQUjVO_m.js → deploy-engine-B6pEfNd9.js} +406 -31
- package/dist/deploy-engine-B6pEfNd9.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{logger-Dw-3y48G.js → logger-D2Md1kTq.js} +77 -2
- package/dist/logger-D2Md1kTq.js.map +1 -0
- package/dist/{program-BEvh5nqs.js → program-Bg60hZhr.js} +651 -100
- package/dist/{program-BEvh5nqs.js.map → program-Bg60hZhr.js.map} +1 -1
- package/dist/{version-DPtrXz0m.js → version-CCKo6fK2.js} +2 -2
- package/dist/{version-DPtrXz0m.js.map → version-CCKo6fK2.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-BQUjVO_m.js.map +0 -1
- package/dist/logger-Dw-3y48G.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { _ as
|
|
1
|
+
import { $ as collectInlinePolicyNamesManagedBySiblings, $n as describeDockerCapturedOutput, $r as LocalInvokeBuildError$1, $t as DagBuilder, A as isStatefulRecreateTargetForReplace, An as orphansAfterRollback, Ar as uploadCfnTemplate, At as coerceCfnBoolean, B as gray, Bn as AssetModeResolver, Br as resolveBucketRegion, Bt as withSharedDrainBudget, C as refusesFinalSnapshot, Cn as forceQuitRecoveryClause, Cr as resolveUseCdkBootstrapAssets, Ct as isUnboundTemplateParameter, D as makeSiblingClaimReader, Dn as exportNamesCarriedFrom, Dr as CFN_TEMPLATE_URL_LIMIT, Dt as normalizeAwsTagsToCfn, E as extractDeploymentEventError, En as DEFAULT_STATE_PREFIX, Er as CFN_TEMPLATE_BODY_LIMIT, Et as WAFv2WebACLProvider, F as isWarmThroughputDecrease, Fn as WorkGraph, Fr as canonicalizeRegion, Ft as requireConfigArray, G as collectPublishedOutputNames, Gn as isCrossRegionRedirect, Gt as s3BucketWebsiteUrl, H as red, Hn as assertAssetBucketRegion, Hr as getAwsClients, Ht as s3BucketDomainName, I as toFiniteNumber, In as buildAssetRedirectMap, Ir as derivePartitionAndUrlSuffix, It as requireConfigObject, J as secretBearing, Jn as validateAssetBucketName, Jr as CrossAccountSecretRefusalError, Jt as INTRINSIC_KEYS, K as exportAliasCollisionScrubWarning, Kn as parseBootstrapMarker, Kr as CdkdError, Kt as applyRoleArnIfSet, L as formatResourceLine, Ln as createAssetRedirectResolver, Lr as AssemblyReader, Lt as requireConfigString, M as renderStatefulReason, Mn as shouldRetainResource, Mr as displaySafe, Mt as configStringRefusal, N as WARM_THROUGHPUT_MEMBERS, Nn as AssetPublisher, Nr as expectedOwnerParam, Nt as readConfigString, O as planOrphanAdoption, On as importableOutputKeys, Or as MIGRATE_TMP_PREFIX, Ot as resolveExplicitPhysicalId, P as coerceWarmThroughput$1, Pn as stringifyValue, Pr as PARTITION_TABLE, Pt as replayWarn, Q as IAMRoleProvider, Qn as buildDockerImage, Qr as IntrinsicResolutionRefusalError, Qt as withRetry, R as bold, Rn as loadPublishableAssetManifest, Rr as processStackMessages, Rt as classifyReplaySecretRegion, S as isFinalSnapshotError, Sn as buildLockContentionMessage, Sr as resolveStateBucketWithDefaultAndSource, St as getAccountInfo, T as makeCanonicalizePropertiesFn, Tn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Tr as warnDeprecatedNoPrefixCliFlag, Tt as refStateLookupFromResource, U as yellow, Un as ensureAssetStorage, Ur as resetAwsClients, Ut as s3BucketDualStackDomainName, V as green, Vn as BOOTSTRAP_MARKER_PREFIX, Vr as AwsClients, Vt as s3BucketArn, W as collectDeclaredOutputNames, Wn as getBootstrapMarkerKey, Wr as setAwsClients, Wt as s3BucketRegionalDomainName, X as secretSafeKeyDisplay, Xn as buildDenyExternalAccessPolicy, Xr as DeployCancelledError, Xt as findSilentDropProperties, Y as secretBearingStateKeyWarning, Yn as validateContainerRepoName, Yt as findActionableSilentDrops, Z as getCurrentResourceSecrets, Zn as describeAwsFailure, Zr as DynamicReferenceRegionAmbiguousError, Zt as describeTypeWithThrottleRetry, _ as ATOMIC_FINAL_SNAPSHOT_TYPES, _i as isThrottlingError, _n as LockManager, _r as resolveApp, _t as isTerminationProtectionPropagationError, a as DeploymentEventsStore, ai as ResourceTimeoutError, an as carriesSecretMask, ar as partitionSensitiveEnv, at as maskerOrIdentity, b as ccRoutedFinalSnapshotError, bi as markRedactedCause, bn as UNRENDERABLE, br as resolveSkipPrefix, bt as cfnRefValueFromPhysicalId, c as replayFailedOperations, ci as StackTerminationProtectionError, cn as errorCauseChain, cr as runDockerStreaming, ct as interruptWatchListenerCount, d as updatePartialReason, dn as maskSecretsInError, dr as AssetManifestLoader, dt as CloudControlProvider, ei as LocalStartServiceError, en as TemplateParser, er as describeDockerExecFailure, et as clearOnUpdateRemoval, f as withResourceDeadline, fn as maskSecretsInText, fr as getDockerImageBySourceHash, ft as slowCcOperationTimeoutMs, g as bindingSkippedOutputs, gi as isRetryableTransientError, gn as scrubResourceRecord, gr as getLegacyStateBucketName, gt as disableInstanceApiTermination, h as computeImplicitDeleteEdges, hi as isMarkedNonRetryable, hn as redactSecretsForState, hr as getDefaultStateBucketName, ht as deleteSkipReason, i as DeploymentEventsReader, ii as ProvisioningError, in as TEMPLATE_SOURCED_RULES, ir as getDockerCmd, it as maskDeep, j as isStatefulRecreateTargetSync, jn as orphansCarriedFrom, jr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, jt as configBooleanRefusal, k as MULTI_REGION_RECREATE_BLOCKED_TYPES, kn as importableOutputs, kr as findLargeInlineResources, kt as assertRegionMatch, l as replayRollback, ln as identityKeyFor, lr as escapeRegExp, lt as isInterruptedWaitError, m as IMPLICIT_DELETE_DEPENDENCIES, mi as withErrorHandling, mn as recoverMaskedOutput, mr as synthesisStatusMessage, mt as deleteIndeterminateGuards, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as NestedStackChildDirectDestroyError, nn as STATE_SOURCED_CROSS_GENERATION_RULES, nr as dockerSpawnEnvWithSensitive, nt as wouldReturnToSdkProvider, o as planFailedOps, oi as ResourceUpdateNotSupportedError, on as createSecretMasker, or as redactDockerArgvValues, ot as beginCommandInterruptScope, p as maskingRetryLogger, pi as normalizeAwsError, pn as recordMaskOnlyValue, pr as Synthesizer, pt as UNSPECIFIED_SKIP_REASON, q as isExportAliasCollision, qn as readBootstrapMarkerBody, qt as DiffCalculator, r as DeployEngine, ri as PartialFailureError, rn as STATE_SOURCED_READBACK_RULES, rr as formatDockerLoginError, rt as createMaskedRetryLogger, s as planRollback, si as StackHasActiveImportsError, sn as dynamicReferenceTokens, sr as runDockerForeground, st as endCommandInterruptScope, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as STATE_SOURCED_BASELINE_RULES, tr as describeDockerFailure, tt as ProviderRegistry, u as updatePartialMessage, un as isSingleDynamicReferenceToken, ur as stripControlChars, ut as startInterruptWatch, v as PRE_DELETE_SNAPSHOT_TYPES, vi as isTransientServerError, vn as S3StateBackend, vr as resolveAutoAssetStorage, vt as IntrinsicFunctionResolver, w as unsupportedFinalSnapshotError, wn as shellQuote, wr as stateBucketExistenceConfirmed, wt as parameterTypeMayLoseSecretIdentity, x as createPreDeleteFinalSnapshot, xi as retryClassificationText, xn as buildForceUnlockCommand, xr as resolveStateBucketWithDefault, xt as coerceParameterTypedValue, y as buildFinalSnapshotIdentifier, yi as markNonRetryable, yn as rebuildClientForBucketRegion, yr as resolveCaptureObservedState, yt as carriesDynamicReference, z as cyan, zn as rewriteTemplateAssetReferences, zt as producerRegionsFromState } from "./deploy-engine-B6pEfNd9.js";
|
|
2
|
+
import { _ as withSkipPrefix, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName } from "./logger-D2Md1kTq.js";
|
|
3
3
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
4
|
-
import { t as getCdkdVersion } from "./version-
|
|
5
|
-
import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as getCdkdVersion } from "./version-CCKo6fK2.js";
|
|
5
|
+
import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-1XVtAjhm.js";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
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, NotFound, 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";
|
|
@@ -692,15 +692,70 @@ const allowUnsupportedTypesOption = new Option("--allow-unsupported-types <types
|
|
|
692
692
|
* time so the user sees the error immediately.
|
|
693
693
|
*/
|
|
694
694
|
const RESOURCE_PROPERTY_FORMAT = /^[A-Z][A-Za-z0-9]+(::[A-Z][A-Za-z0-9]+)+:[A-Z][A-Za-z0-9]*$/;
|
|
695
|
-
|
|
695
|
+
/**
|
|
696
|
+
* The flag the errors below name themselves after.
|
|
697
|
+
*
|
|
698
|
+
* Both spellings share one parser, so the message has to say which one the user
|
|
699
|
+
* typed — quoting the canonical name at someone who typed the deprecated alias
|
|
700
|
+
* sends them looking for a flag they did not use.
|
|
701
|
+
*/
|
|
702
|
+
function parseRouteEntries(value, previous, flag) {
|
|
696
703
|
const parsed = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
697
704
|
for (const token of parsed) {
|
|
698
|
-
if (!RESOURCE_PROPERTY_FORMAT.test(token)) throw new Error(`Invalid
|
|
699
|
-
if (token.startsWith("Custom::")) throw new Error(`Invalid
|
|
705
|
+
if (!RESOURCE_PROPERTY_FORMAT.test(token)) throw new Error(`Invalid ${flag} value "${token}": expected <ResourceType>:<PropertyName> with PascalCase on both halves (e.g. AWS::Lambda::Function:RuntimeManagementConfig).`);
|
|
706
|
+
if (token.startsWith("Custom::")) throw new Error(`Invalid ${flag} value "${token}": Custom:: resources are routed through cfn-response and have no write-side silent drop at cdkd, so the flag would have no effect. Use --allow-unsupported-types for type-level escape hatches instead.`);
|
|
700
707
|
}
|
|
701
708
|
return [...previous ?? [], ...parsed];
|
|
702
709
|
}
|
|
703
|
-
|
|
710
|
+
function parsePreferSdkRouteToken(value, previous) {
|
|
711
|
+
return parseRouteEntries(value, previous, "--prefer-sdk-route");
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Whether the deprecated alias has already warned this process.
|
|
715
|
+
*
|
|
716
|
+
* Commander calls the parser ONCE PER OCCURRENCE, and this flag is repeatable —
|
|
717
|
+
* so without this a user passing it three times gets the deprecation notice
|
|
718
|
+
* three times, which reads as three separate problems.
|
|
719
|
+
*/
|
|
720
|
+
let deprecatedAliasWarned = false;
|
|
721
|
+
function parseAllowUnsupportedPropertiesToken(value, previous) {
|
|
722
|
+
if (!deprecatedAliasWarned) {
|
|
723
|
+
deprecatedAliasWarned = true;
|
|
724
|
+
process.stderr.write("--allow-unsupported-properties is deprecated and will be removed in the first minor release after 2026-12-01; use --prefer-sdk-route, which takes the same <ResourceType>:<PropertyName> tokens and does the same thing. The old name reads as the opposite of its behaviour: it does not make the property reach AWS, it keeps the resource on the SDK provider and accepts that the value is NOT written.\n");
|
|
725
|
+
}
|
|
726
|
+
return parseRouteEntries(value, previous, "--allow-unsupported-properties");
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* `--prefer-sdk-route` — keep a resource on its SDK provider instead of letting
|
|
730
|
+
* an unwired property auto-route it to Cloud Control (issue
|
|
731
|
+
* [#3000](https://github.com/go-to-k/cdkd/issues/3000)).
|
|
732
|
+
*
|
|
733
|
+
* RENAMED from `--allow-unsupported-properties`, which read as the opposite of
|
|
734
|
+
* what it does and rhymed with `--allow-unsupported-types`, a flag that runs the
|
|
735
|
+
* OTHER way: that one ENABLES a Cloud Control attempt, this one DISABLES the
|
|
736
|
+
* Cloud Control auto-route. Two independent readers took the old name backwards
|
|
737
|
+
* in one session, one of them while writing 44 issue bodies describing it.
|
|
738
|
+
*
|
|
739
|
+
* `prefer`, not a negative or a guarantee, and the reason is mechanical rather
|
|
740
|
+
* than stylistic. The allow set is per `<Type>:<Prop>` while ROUTING is per
|
|
741
|
+
* RESOURCE, so one un-allowed sibling drop on the same resource sends the whole
|
|
742
|
+
* resource to Cloud Control anyway — and Cloud Control then forwards the full
|
|
743
|
+
* map, so the named property reaches AWS after all. A name promising "do not
|
|
744
|
+
* route" would be a promise this flag cannot keep; `prefer` is the one modality
|
|
745
|
+
* that is true in both cases.
|
|
746
|
+
*
|
|
747
|
+
* The help text therefore carries what the name deliberately does not: that the
|
|
748
|
+
* values go UNWRITTEN when the preference is honoured, and that it is ignored
|
|
749
|
+
* when the resource routes to Cloud Control for another reason.
|
|
750
|
+
*/
|
|
751
|
+
const preferSdkRouteOption = new Option("--prefer-sdk-route <entries>", "Comma-separated <ResourceType>:<PropertyName> tokens to keep on the SDK provider instead of auto-routing the resource to Cloud Control. The named values are then NOT written to AWS — the deployed resource is missing the field. Ignored for a resource that routes to Cloud Control for another reason (a sibling unwired property, or a sticky cc-api state record), where the values are written after all. Example: --prefer-sdk-route AWS::Lambda::Function:RuntimeManagementConfig").argParser(parsePreferSdkRouteToken);
|
|
752
|
+
/**
|
|
753
|
+
* The deprecated spelling. HIDDEN from `--help` so new users meet only the
|
|
754
|
+
* successor, but still parsed so existing scripts keep working; it warns once
|
|
755
|
+
* per process and feeds the SAME option name, so nothing downstream can tell
|
|
756
|
+
* which spelling was used.
|
|
757
|
+
*/
|
|
758
|
+
const allowUnsupportedPropertiesOption = new Option("--allow-unsupported-properties <entries>", "Deprecated alias for --prefer-sdk-route.").argParser(parseAllowUnsupportedPropertiesToken).hideHelp();
|
|
704
759
|
/**
|
|
705
760
|
* Issue [#615] — `--recreate-via-cc-api <LogicalId>` (repeatable). Each
|
|
706
761
|
* named resource is destroyed + recreated this deploy via Cloud Control
|
|
@@ -724,7 +779,7 @@ function parseRecreateViaCcApiToken(value, previous) {
|
|
|
724
779
|
if (!LOGICAL_ID_FORMAT.test(token)) throw new Error(`Invalid --recreate-via-cc-api value "${value}": expected a CloudFormation logical id (alphanumeric, starts with a letter, max 255 chars). One --recreate-via-cc-api flag per resource — repeat the flag for additional targets.`);
|
|
725
780
|
return [...previous ?? [], token];
|
|
726
781
|
}
|
|
727
|
-
const recreateViaCcApiOption = new Option("--recreate-via-cc-api <logicalId>", "Destroy + recreate the named resource (by CloudFormation logical id) via Cloud Control API in this deploy, so a top-level CFn property cdkd would otherwise silently drop reaches AWS via CC. Repeatable — pass the flag once per resource. Per-resource opt-in (no bulk / no per-stack shortcut) so the destroy-and-recreate cost is acknowledged for each target. Stateful resource types (RDS, DynamoDB, S3, EFS, ...) refuse unless --force-stateful-recreation is ALSO passed (two-flag protection). Cannot be combined with --
|
|
782
|
+
const recreateViaCcApiOption = new Option("--recreate-via-cc-api <logicalId>", "Destroy + recreate the named resource (by CloudFormation logical id) via Cloud Control API in this deploy, so a top-level CFn property cdkd would otherwise silently drop reaches AWS via CC. Repeatable — pass the flag once per resource. Per-resource opt-in (no bulk / no per-stack shortcut) so the destroy-and-recreate cost is acknowledged for each target. Stateful resource types (RDS, DynamoDB, S3, EFS, ...) refuse unless --force-stateful-recreation is ALSO passed (two-flag protection). Cannot be combined with --prefer-sdk-route on the same resource type and property.").argParser(parseRecreateViaCcApiToken);
|
|
728
783
|
/**
|
|
729
784
|
* Issue [#651] — `--recreate-via-sdk-provider <LogicalId>` is the reverse
|
|
730
785
|
* direction of `--recreate-via-cc-api`. Once a resource is sticky on
|
|
@@ -897,6 +952,7 @@ const deployOptions = [
|
|
|
897
952
|
aggressiveVpcParallelOption,
|
|
898
953
|
new Option("-e, --exclusively", "Only deploy requested stacks, do not include dependencies").default(false),
|
|
899
954
|
allowUnsupportedTypesOption,
|
|
955
|
+
preferSdkRouteOption,
|
|
900
956
|
allowUnsupportedPropertiesOption,
|
|
901
957
|
recreateViaCcApiOption,
|
|
902
958
|
recreateViaSdkProviderOption,
|
|
@@ -2758,8 +2814,8 @@ function renderRecreateTargetsErrors(validation) {
|
|
|
2758
2814
|
}
|
|
2759
2815
|
if (validation.ambiguousIntent.length > 0) {
|
|
2760
2816
|
if (lines.length > 0) lines.push("");
|
|
2761
|
-
lines.push(`Ambiguous intent — ${validation.ambiguousIntent.length} resource(s) are named in BOTH --recreate-via-cc-api and --
|
|
2762
|
-
for (const overlap of validation.ambiguousIntent) lines.push(` - ${overlap.logicalId} (${overlap.resourceType}) — both --recreate-via-cc-api ${overlap.logicalId} (would migrate to CC, honoring ${overlap.property}) AND --
|
|
2817
|
+
lines.push(`Ambiguous intent — ${validation.ambiguousIntent.length} resource(s) are named in BOTH --recreate-via-cc-api and --prefer-sdk-route with the same Type:Prop on a silent-drop property the template uses:`);
|
|
2818
|
+
for (const overlap of validation.ambiguousIntent) lines.push(` - ${overlap.logicalId} (${overlap.resourceType}) — both --recreate-via-cc-api ${overlap.logicalId} (would migrate to CC, honoring ${overlap.property}) AND --prefer-sdk-route ${overlap.resourceType}:${overlap.property} (would keep on SDK, accepting silent drop)`);
|
|
2763
2819
|
lines.push(` Fix: pick ONE strategy per resource.`);
|
|
2764
2820
|
}
|
|
2765
2821
|
if (validation.blockedStatefulTargets.length > 0) {
|
|
@@ -2809,9 +2865,9 @@ function renderRecreateTargetsErrors(validation) {
|
|
|
2809
2865
|
}
|
|
2810
2866
|
if (validation.ambiguousIntentSdk.length > 0) {
|
|
2811
2867
|
if (lines.length > 0) lines.push("");
|
|
2812
|
-
lines.push(`Inverse ambiguous intent — ${validation.ambiguousIntentSdk.length} --recreate-via-sdk-provider target(s) would IMMEDIATELY be re-routed back to Cloud Control after the recreate because their template uses silent-drop properties NOT in --
|
|
2868
|
+
lines.push(`Inverse ambiguous intent — ${validation.ambiguousIntentSdk.length} --recreate-via-sdk-provider target(s) would IMMEDIATELY be re-routed back to Cloud Control after the recreate because their template uses silent-drop properties NOT in --prefer-sdk-route:`);
|
|
2813
2869
|
for (const overlap of validation.ambiguousIntentSdk) lines.push(` - ${overlap.logicalId} (${overlap.resourceType}) — template uses ${overlap.property}; the default-on CC auto-route would re-route the recreated resource back to CC immediately`);
|
|
2814
|
-
lines.push(" Fix: pass --
|
|
2870
|
+
lines.push(" Fix: pass --prefer-sdk-route <Type>:<Prop> for each silent-drop property so the recreated resource stays on SDK with the property explicitly dropped. Or drop --recreate-via-sdk-provider — the resource already routes via CC and honors the property.");
|
|
2815
2871
|
}
|
|
2816
2872
|
return lines.length > 0 ? lines.join("\n") : null;
|
|
2817
2873
|
}
|
|
@@ -61046,8 +61102,9 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
61046
61102
|
};
|
|
61047
61103
|
const guardIndeterminateTargets = /* @__PURE__ */ new Set();
|
|
61048
61104
|
const resourceCount = Object.keys(state.resources).length;
|
|
61105
|
+
const orphanCount = (state.orphans ?? []).length;
|
|
61049
61106
|
const regionForState = state.region ?? ctx.baseRegion;
|
|
61050
|
-
if (resourceCount === 0) {
|
|
61107
|
+
if (resourceCount === 0 && orphanCount === 0) {
|
|
61051
61108
|
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
61052
61109
|
let emptyLockHeld = false;
|
|
61053
61110
|
let emptyInterrupted = false;
|
|
@@ -61093,7 +61150,9 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
61093
61150
|
emptyLockHeld = true;
|
|
61094
61151
|
try {
|
|
61095
61152
|
const recheck = await ctx.stateBackend.getState(stackName, regionForState);
|
|
61096
|
-
|
|
61153
|
+
const recheckResources = recheck ? Object.keys(recheck.state.resources).length : 0;
|
|
61154
|
+
const recheckOrphans = recheck ? (recheck.state.orphans ?? []).length : 0;
|
|
61155
|
+
if (!(!recheck || recheckResources === 0 && recheckOrphans === 0)) throw new Error(`Stack '${stackName}' (${regionForState}) was empty when this run started but now has ${recheckResources} resource(s) and ${recheckOrphans} rollback-orphaned resource(s) — another cdkd process wrote to it. Re-run the destroy to act on the current state.`);
|
|
61097
61156
|
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
61098
61157
|
logger.info(`${green("✓")} State deleted`);
|
|
61099
61158
|
} finally {
|
|
@@ -61116,6 +61175,12 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
61116
61175
|
logger.info(`\nResources to be deleted (${resourceCount}):`);
|
|
61117
61176
|
for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
61118
61177
|
const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
|
|
61178
|
+
const orphansAtDestroy = state.orphans ?? [];
|
|
61179
|
+
if (orphansAtDestroy.length > 0) {
|
|
61180
|
+
logger.info(`\n${orphansAtDestroy.length} resource(s) are still in AWS from an earlier failed deploy. cdkd left them there because their DeletionPolicy is Retain (CDK's RemovalPolicy.RETAIN), and has been tracking them so a later deploy of this stack could adopt them back instead of colliding with their names.`);
|
|
61181
|
+
logger.info("Destroying this stack deletes that record. The resources stay in AWS, may continue to incur charges, and cdkd will no longer know about them:");
|
|
61182
|
+
for (const entry of orphansAtDestroy) logger.info(` - ${entry.logicalId} (${entry.state.resourceType}) ${entry.state.physicalId}`);
|
|
61183
|
+
}
|
|
61119
61184
|
if (!ctx.skipConfirmation) {
|
|
61120
61185
|
if (process.stdin.isTTY !== true) throw new CdkdError(`The destroy confirmation prompt for stack "${stackName}" cannot run in a non-interactive environment. Pass --yes / -y to confirm the destroy (cdkd destroy also accepts -f / --force), or run the command from a real terminal.`, "NON_INTERACTIVE_CONFIRM");
|
|
61121
61186
|
const rl = readline$1.createInterface({
|
|
@@ -63165,7 +63230,8 @@ async function deployCommand(stacks, options) {
|
|
|
63165
63230
|
registerAllProviders(stackProviderRegistry);
|
|
63166
63231
|
stackProviderRegistry.setCustomResourceResponseBucket(stateBucket);
|
|
63167
63232
|
if (options.allowUnsupportedTypes?.length) stackProviderRegistry.allowUnsupportedTypes(options.allowUnsupportedTypes);
|
|
63168
|
-
|
|
63233
|
+
const preferSdkRoute = [...options.preferSdkRoute ?? [], ...options.allowUnsupportedProperties ?? []];
|
|
63234
|
+
if (preferSdkRoute.length) stackProviderRegistry.allowUnsupportedProperties(preferSdkRoute);
|
|
63169
63235
|
const eventRecorder = startRunRecorder({
|
|
63170
63236
|
backend: stackStateBackend,
|
|
63171
63237
|
stackName: stackInfo.stackName,
|
|
@@ -63186,7 +63252,7 @@ async function deployCommand(stacks, options) {
|
|
|
63186
63252
|
const syncValidation = validateRecreateTargets({
|
|
63187
63253
|
template: stackInfo.template,
|
|
63188
63254
|
state: stateForRecreateCheck?.state ?? {
|
|
63189
|
-
version:
|
|
63255
|
+
version: 10,
|
|
63190
63256
|
stackName: stackInfo.stackName,
|
|
63191
63257
|
region: stackRegion,
|
|
63192
63258
|
resources: {},
|
|
@@ -63195,7 +63261,7 @@ async function deployCommand(stacks, options) {
|
|
|
63195
63261
|
},
|
|
63196
63262
|
recreateViaCcApi: options.recreateViaCcApi ?? [],
|
|
63197
63263
|
recreateViaSdkProvider: options.recreateViaSdkProvider ?? [],
|
|
63198
|
-
allowUnsupportedProperties: new Set(options.allowUnsupportedProperties ?? []),
|
|
63264
|
+
allowUnsupportedProperties: /* @__PURE__ */ new Set([...options.preferSdkRoute ?? [], ...options.allowUnsupportedProperties ?? []]),
|
|
63199
63265
|
forceStatefulRecreation: options.forceStatefulRecreation ?? false,
|
|
63200
63266
|
hasSdkProvider: (rt) => stackProviderRegistry.getProviderType(rt) === "sdk"
|
|
63201
63267
|
});
|
|
@@ -64849,7 +64915,7 @@ async function loadStateOrEmpty(stackName, region, stateBackend) {
|
|
|
64849
64915
|
region,
|
|
64850
64916
|
resources: {},
|
|
64851
64917
|
outputs: {},
|
|
64852
|
-
version:
|
|
64918
|
+
version: 10,
|
|
64853
64919
|
lastModified: Date.now()
|
|
64854
64920
|
};
|
|
64855
64921
|
}
|
|
@@ -64996,7 +65062,24 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
64996
65062
|
...conditions && { conditions },
|
|
64997
65063
|
skipDynamicReferences: true
|
|
64998
65064
|
});
|
|
64999
|
-
|
|
65065
|
+
let adoptedOrphans = [];
|
|
65066
|
+
let adoptedRecords = {};
|
|
65067
|
+
let blocking = [];
|
|
65068
|
+
let stateForDiff = currentState;
|
|
65069
|
+
if (currentState.orphans?.length && options.previewOrphanAdoption) {
|
|
65070
|
+
const plan = await options.previewOrphanAdoption(currentState, effectiveTemplate, stackName, region);
|
|
65071
|
+
adoptedOrphans = Object.keys(plan.adopted);
|
|
65072
|
+
adoptedRecords = plan.adopted;
|
|
65073
|
+
blocking = plan.refusals;
|
|
65074
|
+
if (adoptedOrphans.length > 0) stateForDiff = {
|
|
65075
|
+
...currentState,
|
|
65076
|
+
resources: {
|
|
65077
|
+
...currentState.resources,
|
|
65078
|
+
...plan.adopted
|
|
65079
|
+
}
|
|
65080
|
+
};
|
|
65081
|
+
}
|
|
65082
|
+
const changes = await diffCalculator.calculateDiff(stateForDiff, effectiveTemplate, resolveFn, canonicalizeProperties);
|
|
65000
65083
|
const changedLogicalIds = new Set([...changes.values()].filter((c) => c.changeType !== "NO_CHANGE").map((c) => c.logicalId));
|
|
65001
65084
|
const bindingSkipped = bindingSkippedOutputs(outputsDigestSource, currentState.skippedOutputs, changedLogicalIds);
|
|
65002
65085
|
const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions, currentState.outputs, bindingSkipped);
|
|
@@ -65012,7 +65095,10 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
65012
65095
|
});
|
|
65013
65096
|
return {
|
|
65014
65097
|
changes,
|
|
65015
|
-
outputChanges
|
|
65098
|
+
outputChanges,
|
|
65099
|
+
adoptedOrphans,
|
|
65100
|
+
adoptedRecords,
|
|
65101
|
+
blocking
|
|
65016
65102
|
};
|
|
65017
65103
|
}
|
|
65018
65104
|
/**
|
|
@@ -65075,22 +65161,32 @@ async function resolveChildStackParameters(parentStackRow, parentTemplate, paren
|
|
|
65075
65161
|
* fail at deploy time.
|
|
65076
65162
|
*/
|
|
65077
65163
|
async function buildDiffTree(args) {
|
|
65078
|
-
const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback } = args;
|
|
65164
|
+
const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback, previewOrphanAdoption } = args;
|
|
65079
65165
|
const state = await loadStateOrEmpty(stackName, region, stateBackend);
|
|
65080
65166
|
const secretBearingAbove = parentHasSecretReference === true || templateHasSecretDynamicReference(template);
|
|
65081
|
-
const { changes, outputChanges } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, {
|
|
65167
|
+
const { changes, outputChanges, adoptedOrphans, adoptedRecords, blocking } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, {
|
|
65082
65168
|
...parameters && { parameters },
|
|
65083
65169
|
...canonicalizeProperties && { canonicalizeProperties },
|
|
65084
65170
|
...cfnFallback !== void 0 && { cfnFallback },
|
|
65171
|
+
...previewOrphanAdoption && { previewOrphanAdoption },
|
|
65085
65172
|
inheritSecretBearingTemplate: false
|
|
65086
65173
|
});
|
|
65174
|
+
const stateAfterAdoption = Object.keys(adoptedRecords).length > 0 ? {
|
|
65175
|
+
...state,
|
|
65176
|
+
resources: {
|
|
65177
|
+
...state.resources,
|
|
65178
|
+
...adoptedRecords
|
|
65179
|
+
}
|
|
65180
|
+
} : state;
|
|
65087
65181
|
const node = {
|
|
65088
65182
|
stackName,
|
|
65089
65183
|
displayName,
|
|
65090
65184
|
region,
|
|
65091
65185
|
changes,
|
|
65092
|
-
ccApiRoutes: collectCcApiRoutes(template,
|
|
65186
|
+
ccApiRoutes: collectCcApiRoutes(template, stateAfterAdoption),
|
|
65093
65187
|
outputChanges,
|
|
65188
|
+
adoptedOrphans,
|
|
65189
|
+
blocking,
|
|
65094
65190
|
children: []
|
|
65095
65191
|
};
|
|
65096
65192
|
if (!recursive) return node;
|
|
@@ -65104,7 +65200,7 @@ async function buildDiffTree(args) {
|
|
|
65104
65200
|
const childTemplate = readNestedTemplate(childTemplatePath);
|
|
65105
65201
|
if (assetRedirect) rewriteTemplateAssetReferences(childTemplate, assetRedirect);
|
|
65106
65202
|
const grandchildTemplates = indexNestedChildTemplates(childTemplate, childTemplatePath);
|
|
65107
|
-
const childParameters = await resolveChildStackParameters(resource, template,
|
|
65203
|
+
const childParameters = await resolveChildStackParameters(resource, template, stateAfterAdoption, region, stackName, stateBackend, parameters, cfnFallback);
|
|
65108
65204
|
node.children.push(await buildDiffTree({
|
|
65109
65205
|
stackName: childStackName,
|
|
65110
65206
|
displayName: childStackName,
|
|
@@ -65118,6 +65214,7 @@ async function buildDiffTree(args) {
|
|
|
65118
65214
|
...canonicalizeProperties && { canonicalizeProperties },
|
|
65119
65215
|
...assetRedirect && { assetRedirect },
|
|
65120
65216
|
...cfnFallback !== void 0 && { cfnFallback },
|
|
65217
|
+
...previewOrphanAdoption && { previewOrphanAdoption },
|
|
65121
65218
|
parentHasSecretReference: secretBearingAbove
|
|
65122
65219
|
}));
|
|
65123
65220
|
}
|
|
@@ -65143,6 +65240,8 @@ async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculat
|
|
|
65143
65240
|
region,
|
|
65144
65241
|
changes,
|
|
65145
65242
|
ccApiRoutes: /* @__PURE__ */ new Map(),
|
|
65243
|
+
adoptedOrphans: [],
|
|
65244
|
+
blocking: [],
|
|
65146
65245
|
outputChanges,
|
|
65147
65246
|
children: []
|
|
65148
65247
|
};
|
|
@@ -65209,9 +65308,41 @@ function collectCcApiRoutes(template, state) {
|
|
|
65209
65308
|
/** True when this node has at least one real (non-`NO_CHANGE`) change. */
|
|
65210
65309
|
function nodeHasChanges(node) {
|
|
65211
65310
|
for (const change of node.changes.values()) if (change.changeType !== "NO_CHANGE") return true;
|
|
65212
|
-
return node.outputChanges.length > 0;
|
|
65311
|
+
return node.outputChanges.length > 0 || node.adoptedOrphans.length > 0;
|
|
65213
65312
|
}
|
|
65214
65313
|
/** True when this node OR any descendant has a real change (tree-wide drift detector for `--fail`). */
|
|
65314
|
+
/**
|
|
65315
|
+
* How many deploy refusals the whole tree carries (issue go-to-k/cdkd#2943).
|
|
65316
|
+
*
|
|
65317
|
+
* A COUNT rather than a boolean because the caller's message quotes it.
|
|
65318
|
+
* {@link treeIsWorthRendering} below IS a boolean built on this, added when
|
|
65319
|
+
* review found the render gate gated on changes alone — it derives from this
|
|
65320
|
+
* count rather than walking the tree again, so there is still one recursion to
|
|
65321
|
+
* keep in step.
|
|
65322
|
+
*
|
|
65323
|
+
* Deliberately SEPARATE from {@link treeHasChanges}: `--fail` answers "is
|
|
65324
|
+
* there a delta", and a refusal is not a delta — a user who runs
|
|
65325
|
+
* `cdkd diff --fail` in CI to detect drift must not have that signal merged
|
|
65326
|
+
* with "the deploy cannot start", which is true whether or not anything
|
|
65327
|
+
* changed.
|
|
65328
|
+
*/
|
|
65329
|
+
function countBlocking(node) {
|
|
65330
|
+
return node.blocking.length + node.children.reduce((n, c) => n + countBlocking(c), 0);
|
|
65331
|
+
}
|
|
65332
|
+
/**
|
|
65333
|
+
* Whether `cdkd diff` should render this tree's block at all (issue
|
|
65334
|
+
* go-to-k/cdkd#2943).
|
|
65335
|
+
*
|
|
65336
|
+
* A named predicate rather than an inline conjunction at the call site,
|
|
65337
|
+
* because the two halves are easy to get wrong INDEPENDENTLY and were:
|
|
65338
|
+
* {@link renderDiffTree} was written to print a `Blocking` section for a node
|
|
65339
|
+
* with no changes, and the caller then skipped the render on
|
|
65340
|
+
* `!treeHasChanges` alone — so a changeless refusal would print "No changes
|
|
65341
|
+
* detected" and exit non-zero citing reasons that were never printed.
|
|
65342
|
+
*/
|
|
65343
|
+
function treeIsWorthRendering(node) {
|
|
65344
|
+
return treeHasChanges(node) || countBlocking(node) > 0;
|
|
65345
|
+
}
|
|
65215
65346
|
function treeHasChanges(node) {
|
|
65216
65347
|
if (nodeHasChanges(node)) return true;
|
|
65217
65348
|
return node.children.some(treeHasChanges);
|
|
@@ -65248,6 +65379,8 @@ function diffTreeToJson(node) {
|
|
|
65248
65379
|
...change.changeType !== "REMOVE" ? { newValue: change.newValue } : {},
|
|
65249
65380
|
export: change.isExport
|
|
65250
65381
|
})),
|
|
65382
|
+
adoptedOrphans: node.adoptedOrphans,
|
|
65383
|
+
blocking: node.blocking,
|
|
65251
65384
|
children: node.children.map(diffTreeToJson)
|
|
65252
65385
|
};
|
|
65253
65386
|
}
|
|
@@ -65340,11 +65473,22 @@ function renderDiffValue(own, filtered, indent, isNewSide) {
|
|
|
65340
65473
|
* #614's auto-fallback decision at plan time. DELETE lines are not annotated
|
|
65341
65474
|
* — the delete routing is recorded on each resource's `provisionedBy` state
|
|
65342
65475
|
* field rather than re-derived from the template.
|
|
65476
|
+
*
|
|
65477
|
+
* `adoptedOrphans` annotates the rows whose resource is in this diff only
|
|
65478
|
+
* because a rollback-orphan record was verified and spliced in (issue
|
|
65479
|
+
* go-to-k/cdkd#2943). Without it the row is indistinguishable from an ordinary
|
|
65480
|
+
* update, and the user last saw that resource FAIL and leave state — so an
|
|
65481
|
+
* unannotated `[~]` reads as cdkd having quietly kept managing it. Both
|
|
65482
|
+
* annotations can apply to one row; adoption is printed first because it is
|
|
65483
|
+
* why the row exists at all, where the routing describes how it will be
|
|
65484
|
+
* carried out.
|
|
65343
65485
|
*/
|
|
65344
|
-
function renderChangeLines(changes, logFn, ccApiRoutes) {
|
|
65486
|
+
function renderChangeLines(changes, logFn, ccApiRoutes, adoptedOrphans) {
|
|
65345
65487
|
let createCount = 0;
|
|
65346
65488
|
let updateCount = 0;
|
|
65347
65489
|
let deleteCount = 0;
|
|
65490
|
+
const adopted = new Set(adoptedOrphans ?? []);
|
|
65491
|
+
const annotateAdoption = (logicalId) => adopted.has(logicalId) ? " [adopted from a rollback orphan]" : "";
|
|
65348
65492
|
const annotateRouting = (logicalId) => {
|
|
65349
65493
|
const props = ccApiRoutes?.get(logicalId);
|
|
65350
65494
|
if (!props || props.length === 0) return "";
|
|
@@ -65354,11 +65498,11 @@ function renderChangeLines(changes, logFn, ccApiRoutes) {
|
|
|
65354
65498
|
for (const [logicalId, change] of changes.entries()) switch (change.changeType) {
|
|
65355
65499
|
case "CREATE":
|
|
65356
65500
|
createCount++;
|
|
65357
|
-
logFn(` [+] ${logicalId} (${change.resourceType})${annotateRouting(logicalId)}`);
|
|
65501
|
+
logFn(` [+] ${logicalId} (${change.resourceType})${annotateAdoption(logicalId)}${annotateRouting(logicalId)}`);
|
|
65358
65502
|
break;
|
|
65359
65503
|
case "UPDATE":
|
|
65360
65504
|
updateCount++;
|
|
65361
|
-
logFn(` [~] ${logicalId} (${change.resourceType})${annotateRouting(logicalId)}`);
|
|
65505
|
+
logFn(` [~] ${logicalId} (${change.resourceType})${annotateAdoption(logicalId)}${annotateRouting(logicalId)}`);
|
|
65362
65506
|
if (change.propertyChanges && change.propertyChanges.length > 0) for (const propChange of change.propertyChanges) {
|
|
65363
65507
|
const requiresReplace = propChange.requiresReplacement ? " [requires replacement]" : "";
|
|
65364
65508
|
const propagated = propChange.replacementPropagated ? " [replacement propagated]" : "";
|
|
@@ -65466,12 +65610,18 @@ function renderOutputChangeLines(outputChanges, logFn) {
|
|
|
65466
65610
|
* (the A4 `state show --show-nested` convention, full `~`-joined name).
|
|
65467
65611
|
*/
|
|
65468
65612
|
function renderDiffTree(node, isRoot, logFn) {
|
|
65469
|
-
|
|
65470
|
-
|
|
65471
|
-
|
|
65613
|
+
const hasChanges = nodeHasChanges(node);
|
|
65614
|
+
if (hasChanges || node.blocking.length > 0) logFn(isRoot ? `\nStack ${node.stackName}:` : `\nNested stack: ${node.displayName}`);
|
|
65615
|
+
if (hasChanges) {
|
|
65616
|
+
const { create, update, delete: del } = renderChangeLines(node.changes, logFn, node.ccApiRoutes, node.adoptedOrphans);
|
|
65472
65617
|
const outputs = renderOutputChangeLines(node.outputChanges, logFn);
|
|
65473
65618
|
logFn(`\n${create} to create, ${update} to update, ${del} to delete`);
|
|
65474
65619
|
if (outputs.add + outputs.change + outputs.remove > 0) logFn(`${outputs.add} output(s) to add, ${outputs.change} to change, ${outputs.remove} to remove`);
|
|
65620
|
+
if (node.adoptedOrphans.length > 0) logFn(`${node.adoptedOrphans.length} resource(s) to adopt from a previous rollback: ${node.adoptedOrphans.map(stripControlChars).join(", ")}`);
|
|
65621
|
+
}
|
|
65622
|
+
if (node.blocking.length > 0) {
|
|
65623
|
+
logFn("\n Blocking (cdkd deploy will refuse):");
|
|
65624
|
+
for (const reason of node.blocking) logFn(` ! ${stripControlChars(reason)}`);
|
|
65475
65625
|
}
|
|
65476
65626
|
for (const child of node.children) renderDiffTree(child, false, logFn);
|
|
65477
65627
|
}
|
|
@@ -65493,6 +65643,35 @@ var DiffDetectedError = class DiffDetectedError extends CdkdError {
|
|
|
65493
65643
|
}
|
|
65494
65644
|
};
|
|
65495
65645
|
/**
|
|
65646
|
+
* Signals that the preview completed and `cdkd deploy` would REFUSE to start
|
|
65647
|
+
* (issue go-to-k/cdkd#2943) — today, a rollback-orphan record whose physical
|
|
65648
|
+
* id another cdkd stack already manages.
|
|
65649
|
+
*
|
|
65650
|
+
* Exit **3**, and neither 1 nor 2, for reasons that are about what a caller
|
|
65651
|
+
* can conclude:
|
|
65652
|
+
*
|
|
65653
|
+
* - **1** is `--fail`'s "a change was detected". A refusal is not a change,
|
|
65654
|
+
* and merging them would make a CI job that gates on drift report the same
|
|
65655
|
+
* code for "there is work to do" and "the work cannot begin".
|
|
65656
|
+
* - **2** is this CLI's partial-failure family, documented as "work
|
|
65657
|
+
* completed, re-running typically resolves it" (`docs/cli-reference.md`).
|
|
65658
|
+
* A refusal is the opposite: re-running changes nothing until a human
|
|
65659
|
+
* resolves the ownership conflict.
|
|
65660
|
+
*
|
|
65661
|
+
* It is NOT silent: unlike `--fail`, this is not a flag the user opted into,
|
|
65662
|
+
* so the reason has to be visible even when the diff report scrolled away.
|
|
65663
|
+
* The `Blocking` section already printed the per-record detail; this adds the
|
|
65664
|
+
* one line that says the run cannot proceed.
|
|
65665
|
+
*/
|
|
65666
|
+
var DeployRefusalPreviewError = class DeployRefusalPreviewError extends CdkdError {
|
|
65667
|
+
exitCode = 3;
|
|
65668
|
+
constructor(count) {
|
|
65669
|
+
super(`cdkd deploy would refuse to start: ${count} blocking condition(s) reported above. Resolve them, or run \`cdkd state\` to inspect the conflicting stack.`, "DEPLOY_REFUSAL_PREVIEW");
|
|
65670
|
+
this.name = "DeployRefusalPreviewError";
|
|
65671
|
+
Object.setPrototypeOf(this, DeployRefusalPreviewError.prototype);
|
|
65672
|
+
}
|
|
65673
|
+
};
|
|
65674
|
+
/**
|
|
65496
65675
|
* Diff command implementation
|
|
65497
65676
|
*/
|
|
65498
65677
|
async function diffCommand(stacks, options) {
|
|
@@ -65553,6 +65732,34 @@ async function diffCommand(stacks, options) {
|
|
|
65553
65732
|
const diffProviderRegistry = new ProviderRegistry();
|
|
65554
65733
|
registerAllProviders(diffProviderRegistry);
|
|
65555
65734
|
const canonicalizeProperties = makeCanonicalizePropertiesFn(diffProviderRegistry);
|
|
65735
|
+
const previewOrphanAdoption = async (state, effectiveTemplate, orphanStackName, orphanRegion) => {
|
|
65736
|
+
const outcome = await planOrphanAdoption({
|
|
65737
|
+
records: state.orphans ?? [],
|
|
65738
|
+
managedLogicalIds: new Set(Object.keys(state.resources)),
|
|
65739
|
+
template: effectiveTemplate,
|
|
65740
|
+
stackName: orphanStackName,
|
|
65741
|
+
region: orphanRegion,
|
|
65742
|
+
getProvider: (resourceType, provisionedBy) => diffProviderRegistry.getProviderFor({
|
|
65743
|
+
resourceType,
|
|
65744
|
+
provisionedBy
|
|
65745
|
+
}).provider,
|
|
65746
|
+
nameProperties: (resourceType) => {
|
|
65747
|
+
const property = explicitNamePropertyFor(resourceType);
|
|
65748
|
+
return property ? [property] : [];
|
|
65749
|
+
},
|
|
65750
|
+
readSiblingClaims: makeSiblingClaimReader({
|
|
65751
|
+
stateBackend,
|
|
65752
|
+
selfStackName: orphanStackName,
|
|
65753
|
+
selfRegion: orphanRegion,
|
|
65754
|
+
logger
|
|
65755
|
+
}),
|
|
65756
|
+
logger
|
|
65757
|
+
});
|
|
65758
|
+
return {
|
|
65759
|
+
adopted: outcome.adopted,
|
|
65760
|
+
refusals: outcome.refusals
|
|
65761
|
+
};
|
|
65762
|
+
};
|
|
65556
65763
|
const recursive = options.recursive ?? false;
|
|
65557
65764
|
const resolveAssetRedirect = createAssetRedirectResolver({
|
|
65558
65765
|
stateBackend,
|
|
@@ -65581,17 +65788,20 @@ async function diffCommand(stacks, options) {
|
|
|
65581
65788
|
diffCalculator,
|
|
65582
65789
|
canonicalizeProperties,
|
|
65583
65790
|
...assetRedirect && { assetRedirect },
|
|
65584
|
-
...options.cfnFallback === false && { cfnFallback: false }
|
|
65791
|
+
...options.cfnFallback === false && { cfnFallback: false },
|
|
65792
|
+
previewOrphanAdoption
|
|
65585
65793
|
}));
|
|
65586
65794
|
}
|
|
65587
65795
|
if (options.json) process.stdout.write(`${JSON.stringify(trees.map(diffTreeToJson), null, 2)}\n`);
|
|
65588
65796
|
else for (const tree of trees) {
|
|
65589
|
-
if (!
|
|
65797
|
+
if (!treeIsWorthRendering(tree)) {
|
|
65590
65798
|
logger.info(`\n✓ No changes detected for stack ${tree.stackName}`);
|
|
65591
65799
|
continue;
|
|
65592
65800
|
}
|
|
65593
65801
|
renderDiffTree(tree, true, (msg) => logger.info(msg));
|
|
65594
65802
|
}
|
|
65803
|
+
const blockingCount = trees.reduce((n, tree) => n + countBlocking(tree), 0);
|
|
65804
|
+
if (blockingCount > 0) throw new DeployRefusalPreviewError(blockingCount);
|
|
65595
65805
|
if (options.fail && trees.some(treeHasChanges)) throw new DiffDetectedError();
|
|
65596
65806
|
} finally {
|
|
65597
65807
|
awsClients.destroy();
|
|
@@ -66109,10 +66319,9 @@ async function scrubCommand(stacks, options) {
|
|
|
66109
66319
|
* (issue #2005) — the outputs' own map plus every resource's, filtered to the
|
|
66110
66320
|
* values long enough to be a safe needle.
|
|
66111
66321
|
*
|
|
66112
|
-
*
|
|
66113
|
-
*
|
|
66114
|
-
*
|
|
66115
|
-
* value cannot rewrite another resource's coinciding literal (the collision the
|
|
66322
|
+
* Built here rather than kept as each pass's input everywhere — `outputSecrets`
|
|
66323
|
+
* and `perResourceSecrets` are deliberately SEPARATE bags so one resource's
|
|
66324
|
+
* secret value cannot rewrite another's coinciding literal (the collision the
|
|
66116
66325
|
* deploy engine's `perResourceSecrets` doc describes), and widening the bag the
|
|
66117
66326
|
* RESOURCE walk uses would re-open exactly that.
|
|
66118
66327
|
*
|
|
@@ -66198,9 +66407,10 @@ async function scrubCommand(stacks, options) {
|
|
|
66198
66407
|
* command can. Refusing the substring arm here would trade the irreversible
|
|
66199
66408
|
* failure for the reversible one in the wrong direction.
|
|
66200
66409
|
*/
|
|
66201
|
-
function allRecordedSecrets(outputSecrets, perResourceSecrets) {
|
|
66410
|
+
function allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets) {
|
|
66202
66411
|
const union = /* @__PURE__ */ new Map();
|
|
66203
66412
|
for (const recorded of perResourceSecrets.values()) for (const [value, expression] of recorded) union.set(value, expression);
|
|
66413
|
+
for (const recorded of orphanSecrets.values()) for (const [value, expression] of recorded) union.set(value, expression);
|
|
66204
66414
|
for (const [value, expression] of outputSecrets) union.set(value, expression);
|
|
66205
66415
|
for (const value of union.keys()) if (value.length < 4) union.delete(value);
|
|
66206
66416
|
return union;
|
|
@@ -67721,6 +67931,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67721
67931
|
if (acquired) await lockManager.acquireLockWithRetry(stack.stackName, region, void 0, "scrub");
|
|
67722
67932
|
const perResourceSecrets = /* @__PURE__ */ new Map();
|
|
67723
67933
|
const outputSecrets = /* @__PURE__ */ new Map();
|
|
67934
|
+
/**
|
|
67935
|
+
* Needles derived from each rollback-orphan record's OWN recorded bag
|
|
67936
|
+
* (issue go-to-k/cdkd#2943), keyed by logical id — per-record for the same
|
|
67937
|
+
* reason `perResourceSecrets` is per-resource: one record's secret must not
|
|
67938
|
+
* rewrite another's coinciding literal.
|
|
67939
|
+
*/
|
|
67940
|
+
const orphanSecrets = /* @__PURE__ */ new Map();
|
|
67724
67941
|
const prePassFindings = { unverifiable: [] };
|
|
67725
67942
|
try {
|
|
67726
67943
|
const loaded = await stateBackend.getState(stack.stackName, region);
|
|
@@ -67801,6 +68018,27 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67801
68018
|
}
|
|
67802
68019
|
perResourceTemplateProps.set(logicalId, templateResource.Properties);
|
|
67803
68020
|
}
|
|
68021
|
+
for (const record of state.orphans ?? []) {
|
|
68022
|
+
const recordedSecretValues = /* @__PURE__ */ new Map();
|
|
68023
|
+
orphanSecrets.set(record.logicalId, recordedSecretValues);
|
|
68024
|
+
const resolveInput = await pinCrossRegionSecrets({
|
|
68025
|
+
properties: record.state.properties,
|
|
68026
|
+
attributes: record.state.attributes ?? {},
|
|
68027
|
+
observedProperties: record.state.observedProperties ?? {}
|
|
68028
|
+
}, stack.stackName, {
|
|
68029
|
+
stackRegion: region,
|
|
68030
|
+
producerRegions,
|
|
68031
|
+
resolvers,
|
|
68032
|
+
recordedSecretValues,
|
|
68033
|
+
origin: `orphan record '${record.logicalId}'`
|
|
68034
|
+
});
|
|
68035
|
+
try {
|
|
68036
|
+
await resolver.resolve(resolveInput, resolverContext(recordedSecretValues));
|
|
68037
|
+
} catch (err) {
|
|
68038
|
+
if (isRegionAmbiguousRefusal(err)) throw err;
|
|
68039
|
+
logger.debug(`Resolution of orphan record ${record.logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
|
|
68040
|
+
}
|
|
68041
|
+
}
|
|
67804
68042
|
for (const [name, output] of Object.entries(templateOutputs)) {
|
|
67805
68043
|
const declaredExportName = output.Export?.Name;
|
|
67806
68044
|
let exportName = declaredExportName;
|
|
@@ -67871,7 +68109,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67871
68109
|
secretBearingKeys.push(key);
|
|
67872
68110
|
logger.warn(secretBearingStateKeyWarning(stack.stackName, display));
|
|
67873
68111
|
}
|
|
67874
|
-
const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0);
|
|
68112
|
+
const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0) + [...orphanSecrets.values()].reduce((n, m) => n + m.size, 0);
|
|
67875
68113
|
if (totalSecrets === 0) return {
|
|
67876
68114
|
recordsChanged: 0,
|
|
67877
68115
|
secretsFound: 0,
|
|
@@ -67894,13 +68132,27 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67894
68132
|
if (JSON.stringify(scrubbed) !== JSON.stringify(record)) recordsChanged++;
|
|
67895
68133
|
newResources[logicalId] = scrubbed;
|
|
67896
68134
|
}
|
|
67897
|
-
const
|
|
68135
|
+
const newOrphans = (state.orphans ?? []).map((record) => {
|
|
68136
|
+
const own = orphanSecrets.get(record.logicalId);
|
|
68137
|
+
const needles = new Map(allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
|
|
68138
|
+
for (const [plaintext, expression] of own ?? []) needles.set(plaintext, expression);
|
|
68139
|
+
if (needles.size === 0) return record;
|
|
68140
|
+
const scrubbed = scrubResourceRecord(record.state, needles, void 0, STATE_SOURCED_CROSS_GENERATION_RULES);
|
|
68141
|
+
if (JSON.stringify(scrubbed) === JSON.stringify(record.state)) return record;
|
|
68142
|
+
recordsChanged++;
|
|
68143
|
+
return {
|
|
68144
|
+
...record,
|
|
68145
|
+
state: scrubbed
|
|
68146
|
+
};
|
|
68147
|
+
});
|
|
68148
|
+
const newOutputs = redactUnaccountedOutputs(outputSecrets.size > 0 ? redactSecretsForState(state.outputs, outputSecrets, outputsSourceUntrusted ? void 0 : outputsTemplateSource, TEMPLATE_SOURCED_RULES) : state.outputs, state.outputs, accountedOutputKeys, allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
|
|
67898
68149
|
if (JSON.stringify(newOutputs) !== JSON.stringify(state.outputs)) recordsChanged++;
|
|
67899
68150
|
if (recordsChanged > 0 && !opts.dryRun) {
|
|
67900
68151
|
const { skippedOutputs: _droppedByScrub, ...carriedState } = state;
|
|
67901
68152
|
const nextState = {
|
|
67902
68153
|
...carriedState,
|
|
67903
68154
|
resources: newResources,
|
|
68155
|
+
...state.orphans !== void 0 && { orphans: newOrphans },
|
|
67904
68156
|
outputs: newOutputs,
|
|
67905
68157
|
lastModified: Date.now()
|
|
67906
68158
|
};
|
|
@@ -67915,7 +68167,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67915
68167
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
67916
68168
|
};
|
|
67917
68169
|
} catch (err) {
|
|
67918
|
-
throw maskSecretsInError(err, allRecordedSecrets(outputSecrets, perResourceSecrets));
|
|
68170
|
+
throw maskSecretsInError(err, allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
|
|
67919
68171
|
} finally {
|
|
67920
68172
|
if (acquired) await lockManager.releaseLock(stack.stackName, region).catch((err) => {
|
|
67921
68173
|
logger.warn(`Failed to release lock for ${stack.stackName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -68826,6 +69078,17 @@ function stripWalk(value) {
|
|
|
68826
69078
|
//#endregion
|
|
68827
69079
|
//#region src/cli/commands/drift.ts
|
|
68828
69080
|
/**
|
|
69081
|
+
* The schema version at which `ResourceState.observedBaselineRefused` arrived.
|
|
69082
|
+
* A record BELOW it could not carry a refusal, which is what
|
|
69083
|
+
* {@link warnIfPreV10BaselineGap} keys on (issue
|
|
69084
|
+
* [#3011](https://github.com/go-to-k/cdkd/issues/3011)).
|
|
69085
|
+
*
|
|
69086
|
+
* A literal rather than `STATE_SCHEMA_VERSION_CURRENT`: this is a FIXED
|
|
69087
|
+
* historical boundary, and binding it to "current" would silently stop warning
|
|
69088
|
+
* about v10 records on the next bump — the population it names does not move.
|
|
69089
|
+
*/
|
|
69090
|
+
const PRE_V10_BASELINE_WARNING_FLOOR = 10;
|
|
69091
|
+
/**
|
|
68829
69092
|
* Route one outcome to the handler for its variant.
|
|
68830
69093
|
*
|
|
68831
69094
|
* The mechanism issue [#2135](https://github.com/go-to-k/cdkd/issues/2135) asks
|
|
@@ -69047,6 +69310,10 @@ const UNCOMPARED_REASONS = {
|
|
|
69047
69310
|
kind: "unknown",
|
|
69048
69311
|
phrase: "not compared AT ALL: the read or comparison failed"
|
|
69049
69312
|
},
|
|
69313
|
+
baselineRefused: {
|
|
69314
|
+
kind: "unknown",
|
|
69315
|
+
phrase: "not compared AT ALL: a `cdkd import` run refused to capture their observed baseline"
|
|
69316
|
+
},
|
|
69050
69317
|
refused: {
|
|
69051
69318
|
kind: "unknown",
|
|
69052
69319
|
phrase: "only PARTIALLY compared: cdkd refused to resolve a dynamic reference their state records"
|
|
@@ -69592,7 +69859,8 @@ function notComparedReason(cause) {
|
|
|
69592
69859
|
return {
|
|
69593
69860
|
refused: "cdkd refused to resolve a dynamic reference its state records (spell the reference as a full ARN, which names its region)",
|
|
69594
69861
|
unresolvedToken: "its state records a `{{resolve:...}}` spelling cdkd resolves for nobody (permanent; a re-run cannot clear it)",
|
|
69595
|
-
readFailed: "the read or comparison threw, so NONE of its properties were compared"
|
|
69862
|
+
readFailed: "the read or comparison threw, so NONE of its properties were compared",
|
|
69863
|
+
baselineRefused: "a `cdkd import` run refused to capture its observed baseline, so the only baseline available is the recorded properties that refusal already found untrustworthy (deploy a change to this resource to restore one)"
|
|
69596
69864
|
}[cause];
|
|
69597
69865
|
}
|
|
69598
69866
|
/**
|
|
@@ -69980,6 +70248,15 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
69980
70248
|
});
|
|
69981
70249
|
continue;
|
|
69982
70250
|
}
|
|
70251
|
+
if (resource.observedBaselineRefused === true) {
|
|
70252
|
+
outcomes.push({
|
|
70253
|
+
kind: "notCompared",
|
|
70254
|
+
logicalId,
|
|
70255
|
+
resourceType: resource.resourceType,
|
|
70256
|
+
notComparedCause: "baselineRefused"
|
|
70257
|
+
});
|
|
70258
|
+
continue;
|
|
70259
|
+
}
|
|
69983
70260
|
try {
|
|
69984
70261
|
let aws;
|
|
69985
70262
|
if (provider.readCurrentState) aws = await provider.readCurrentState(resource.physicalId, logicalId, resource.resourceType, resource.properties ?? {}, buildReadCurrentStateContext(state, logicalId));
|
|
@@ -70106,17 +70383,56 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
70106
70383
|
continue;
|
|
70107
70384
|
}
|
|
70108
70385
|
}
|
|
70386
|
+
const warnings = warnIfPreV10BaselineGap(state, outcomes, logger);
|
|
70109
70387
|
return {
|
|
70110
70388
|
stackName,
|
|
70111
70389
|
region,
|
|
70112
70390
|
outcomes,
|
|
70113
70391
|
state,
|
|
70114
70392
|
etag: result.etag,
|
|
70115
|
-
migrationPending: result.migrationPending ?? false
|
|
70393
|
+
migrationPending: result.migrationPending ?? false,
|
|
70394
|
+
warnings
|
|
70116
70395
|
};
|
|
70117
70396
|
});
|
|
70118
70397
|
}
|
|
70119
70398
|
/**
|
|
70399
|
+
* Warn once for a stack whose state PREDATES the refused-baseline marker
|
|
70400
|
+
* (`ResourceState.observedBaselineRefused`, state schema v10) and still has a
|
|
70401
|
+
* resource without a drift baseline (issue
|
|
70402
|
+
* [#3011](https://github.com/go-to-k/cdkd/issues/3011)).
|
|
70403
|
+
*
|
|
70404
|
+
* Both conjuncts are load-bearing, and neither is a heuristic:
|
|
70405
|
+
*
|
|
70406
|
+
* - `version < 10` is not "probably imported" — it is the exact statement that
|
|
70407
|
+
* this record could not carry a refusal even if one was made. A v10 record
|
|
70408
|
+
* that omits the field is a record nothing refused, and warning there would
|
|
70409
|
+
* be false.
|
|
70410
|
+
* - the missing baseline is what makes the warning ACTIONABLE. A pre-v10 stack
|
|
70411
|
+
* where every resource has a baseline has nothing for drift to fall back to
|
|
70412
|
+
* `properties` for, so it cannot hit the disclosure.
|
|
70413
|
+
*
|
|
70414
|
+
* Exported for unit testing -- internal to the drift flow otherwise.
|
|
70415
|
+
*/
|
|
70416
|
+
function warnIfPreV10BaselineGap(state, outcomes, logger) {
|
|
70417
|
+
if ((state.version ?? 1) >= PRE_V10_BASELINE_WARNING_FLOOR) return [];
|
|
70418
|
+
const compared = new Set(outcomes.flatMap((o) => matchOutcome(o, {
|
|
70419
|
+
drifted: (d) => [d.logicalId],
|
|
70420
|
+
clean: (c) => [c.logicalId],
|
|
70421
|
+
notCompared: (n) => [n.logicalId],
|
|
70422
|
+
unsupported: () => [],
|
|
70423
|
+
skipped: () => []
|
|
70424
|
+
})));
|
|
70425
|
+
const affected = Object.entries(state.resources ?? {}).filter(([id, r]) => r.observedProperties === void 0 && compared.has(id)).map(([id]) => id);
|
|
70426
|
+
if (affected.length === 0) return [];
|
|
70427
|
+
const shown = affected.slice(0, 10).join(", ");
|
|
70428
|
+
const rest = affected.length > 10 ? `, and ${affected.length - 10} more` : "";
|
|
70429
|
+
const finding = `${state.stackName} (${state.region ?? "unknown region"}): this stack's state predates cdkd's refused-baseline marker (schema v${state.version ?? 1}; the marker arrived in v${PRE_V10_BASELINE_WARNING_FLOOR}), and ${affected.length} of its compared resource(s) have no drift baseline: ${shown}${rest}. If any were adopted by 'cdkd import' and that import REFUSED to capture a baseline, cdkd cannot tell — so it compares them against their recorded properties, which can hold a placeholder the deployed stack never used and can surface a live secret in this report. Re-run 'cdkd import' for this stack to put the record right.`;
|
|
70430
|
+
const note = " Note: ANY write to this stack's state — including this run's --accept / --revert, or a deploy that changes nothing about the resources above — re-stamps it at the current schema version and SILENCES this warning without fixing those records. Only a re-import, or a deploy that actually CHANGES a listed resource, repairs one.";
|
|
70431
|
+
logger.warn(finding);
|
|
70432
|
+
logger.warn(note);
|
|
70433
|
+
return [finding, note];
|
|
70434
|
+
}
|
|
70435
|
+
/**
|
|
70120
70436
|
* Set a value at a dotted path inside a plain object, creating intermediate
|
|
70121
70437
|
* objects as needed. Mirrors `lodash.set` for the subset of paths the drift
|
|
70122
70438
|
* comparator actually emits — dotted nested keys, no array indices.
|
|
@@ -70226,6 +70542,10 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
|
|
|
70226
70542
|
for (const outcome of driftedOutcomes) {
|
|
70227
70543
|
const existing = resources[outcome.logicalId];
|
|
70228
70544
|
if (!existing) continue;
|
|
70545
|
+
if (existing.observedBaselineRefused === true) {
|
|
70546
|
+
logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): NOT accepted — a 'cdkd import' run refused to capture this resource's observed-properties baseline, because its recorded properties can no longer position the secret redaction. Accepting would write the AWS readback into those properties, which can persist a resolved secret into state.json in plaintext. Deploy a change to this resource to restore a baseline first.`);
|
|
70547
|
+
continue;
|
|
70548
|
+
}
|
|
70229
70549
|
const hasObserved = existing.observedProperties !== void 0;
|
|
70230
70550
|
const baselineSource = hasObserved ? existing.observedProperties : existing.properties ?? {};
|
|
70231
70551
|
const newBaseline = JSON.parse(JSON.stringify(baselineSource));
|
|
@@ -71389,6 +71709,11 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
71389
71709
|
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): resource missing from state; skipped.`);
|
|
71390
71710
|
return;
|
|
71391
71711
|
}
|
|
71712
|
+
if (stateResource.observedBaselineRefused === true) {
|
|
71713
|
+
totalUnresolvable++;
|
|
71714
|
+
logger.warn(` ! ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): NOT reverted — a 'cdkd import' run refused to capture this resource's observed-properties baseline, so the only baseline available is its recorded properties, which the refusal already found untrustworthy. Reverting from them could overwrite a live value (a resolved secret among them) with a placeholder the deployed stack never used. Deploy a change to this resource to restore a baseline first.`);
|
|
71715
|
+
return;
|
|
71716
|
+
}
|
|
71392
71717
|
const provider = providerRegistry.getProviderFor({
|
|
71393
71718
|
resourceType: outcome.resourceType,
|
|
71394
71719
|
provisionedBy: stateResource.provisionedBy
|
|
@@ -71571,6 +71896,10 @@ function printAcceptPlan(reports, out) {
|
|
|
71571
71896
|
const lines = [];
|
|
71572
71897
|
let plannedWrites = 0;
|
|
71573
71898
|
for (const o of drifted) {
|
|
71899
|
+
if (report.state.resources[o.logicalId]?.observedBaselineRefused === true) {
|
|
71900
|
+
lines.push(` ~ ${o.logicalId} (${o.resourceType})\n SKIPPED — a 'cdkd import' run refused this resource's observed-properties baseline; accepting would write the AWS readback into properties it already found untrustworthy. Deploy a change to this resource first.\n`);
|
|
71901
|
+
continue;
|
|
71902
|
+
}
|
|
71574
71903
|
lines.push(` ~ ${o.logicalId} (${o.resourceType})\n`);
|
|
71575
71904
|
for (const change of o.changes) {
|
|
71576
71905
|
const refusal = acceptRefusalReason(change, o.maskedPaths);
|
|
@@ -71603,6 +71932,10 @@ function printRevertPlan(reports, out) {
|
|
|
71603
71932
|
if (drifted.length === 0) continue;
|
|
71604
71933
|
out.write(`\nPlan (--revert): push cdkd state values back into AWS for ${report.stackName} (${report.region}):\n`);
|
|
71605
71934
|
for (const o of drifted) {
|
|
71935
|
+
if (report.state.resources[o.logicalId]?.observedBaselineRefused === true) {
|
|
71936
|
+
out.write(` ! ${o.logicalId} (${o.resourceType}): NOT reverted — a 'cdkd import' run refused this resource's observed-properties baseline, so the only baseline available is the one that refusal already found untrustworthy. Deploy a change to this resource first.\n`);
|
|
71937
|
+
continue;
|
|
71938
|
+
}
|
|
71606
71939
|
const word = o.changes.length === 1 ? "property path" : "property paths";
|
|
71607
71940
|
out.write(` → provider.update on ${o.logicalId} (${o.resourceType}): revert ${o.changes.length} ${word}\n`);
|
|
71608
71941
|
for (const change of o.changes) out.write(` ${change.path}: ${formatScalar(change.awsValue)} -> ${formatScalar(change.stateValue)}\n`);
|
|
@@ -71709,12 +72042,13 @@ function writeJsonReport(reports) {
|
|
|
71709
72042
|
const notCompared = notComparedOutcomes(r).map(({ outcome, cause }) => ({
|
|
71710
72043
|
logicalId: outcome.logicalId,
|
|
71711
72044
|
type: outcome.resourceType,
|
|
71712
|
-
referencesUnresolved: cause !== "readFailed",
|
|
72045
|
+
referencesUnresolved: cause !== "readFailed" && cause !== "baselineRefused",
|
|
71713
72046
|
cause
|
|
71714
72047
|
}));
|
|
71715
72048
|
return {
|
|
71716
72049
|
stack: r.stackName,
|
|
71717
72050
|
region: r.region,
|
|
72051
|
+
...r.warnings.length > 0 && { warnings: r.warnings },
|
|
71718
72052
|
drifted,
|
|
71719
72053
|
clean,
|
|
71720
72054
|
notSupported,
|
|
@@ -71753,7 +72087,7 @@ function writeHumanReport(reports) {
|
|
|
71753
72087
|
const checked = inspected - notCompared.length;
|
|
71754
72088
|
if (drifted.length === 0) {
|
|
71755
72089
|
if (notCompared.length === 0 && checked === 0 && report.outcomes.length > 0) process.stdout.write(`⚠ ${report.stackName} (${report.region}): no drift detected, but NOTHING was compared — 0 of ${report.outcomes.length} resource${report.outcomes.length === 1 ? "" : "s"} checked (${unsupported.length} unsupported, ${skippedCount} skipped)\n`);
|
|
71756
|
-
else 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.some((n) => n.cause === "readFailed") ? `(${notCompared.length} not fully compared), ` : `(${notCompared.length} only partially compared), `) + `${unsupported.length} unsupported\n`);
|
|
72090
|
+
else 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.some((n) => n.cause === "readFailed" || n.cause === "baselineRefused") ? `(${notCompared.length} not fully compared), ` : `(${notCompared.length} only partially compared), `) + `${unsupported.length} unsupported\n`);
|
|
71757
72091
|
else process.stdout.write(`✓ ${report.stackName} (${report.region}): no drift detected (${checked} resource${checked === 1 ? "" : "s"} checked, ${unsupported.length} unsupported)\n`);
|
|
71758
72092
|
} else {
|
|
71759
72093
|
const word = drifted.length === 1 ? "resource" : "resources";
|
|
@@ -71768,9 +72102,12 @@ function writeHumanReport(reports) {
|
|
|
71768
72102
|
}
|
|
71769
72103
|
}
|
|
71770
72104
|
if (notCompared.length > 0) {
|
|
71771
|
-
const
|
|
71772
|
-
const
|
|
71773
|
-
|
|
72105
|
+
const notComparedAtAll = notCompared.filter((n) => n.cause === "readFailed" || n.cause === "baselineRefused").length;
|
|
72106
|
+
const readFailedCount = notCompared.filter((n) => n.cause === "readFailed").length;
|
|
72107
|
+
const baselineRefusedCount = notCompared.filter((n) => n.cause === "baselineRefused").length;
|
|
72108
|
+
const referenceCaused = notCompared.length - notComparedAtAll;
|
|
72109
|
+
const atAllCause = [...readFailedCount > 0 ? ["the read or comparison failed"] : [], ...baselineRefusedCount > 0 ? ["an import refused their observed baseline"] : []].join("; ");
|
|
72110
|
+
process.stdout.write(notComparedAtAll === 0 ? `\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` : `\n ${notCompared.length} resource(s) NOT fully compared — ${notComparedAtAll} not compared AT ALL (${atAllCause})` + (referenceCaused > 0 ? `, ${referenceCaused} only PARTIALLY compared (a dynamic reference cdkd could not, or refused to, resolve)` : "") + `:\n`);
|
|
71774
72111
|
for (const { outcome, cause } of notCompared) process.stdout.write(` ! ${outcome.logicalId} (${outcome.resourceType}) — ${notComparedReason(cause)}\n`);
|
|
71775
72112
|
}
|
|
71776
72113
|
if (unsupported.length > 0) {
|
|
@@ -75015,9 +75352,18 @@ function reportDriftBaselineGaps(state, logger) {
|
|
|
75015
75352
|
logger.warn(`cdkd state schema is v${state.version} (pre-observedProperties). cdkd drift cannot reliably compare against AWS for this stack; the next \`cdk deploy\` after migration may surface spurious changes if AWS has drifted from the template. Run \`cdkd state refresh-observed ${state.stackName}\` (or any redeploy) before export to capture an AWS-current baseline.`);
|
|
75016
75353
|
return;
|
|
75017
75354
|
}
|
|
75018
|
-
|
|
75019
|
-
|
|
75020
|
-
if (
|
|
75355
|
+
const refused = missing.filter(([, r]) => r.observedBaselineRefused === true);
|
|
75356
|
+
const refreshable = missing.filter(([, r]) => r.observedBaselineRefused !== true);
|
|
75357
|
+
if (refreshable.length > 0) {
|
|
75358
|
+
logger.warn(`${refreshable.length} of ${entries.length} resource(s) in cdkd state lack an AWS-current baseline (observedProperties). cdkd drift may produce false positives for them; the next \`cdk deploy\` after migration may surface unexpected changes. Run \`cdkd state refresh-observed ${state.stackName}\` to capture a baseline before export, then \`cdkd drift\` to verify the stack matches AWS.`);
|
|
75359
|
+
for (const [logicalId] of refreshable.slice(0, 10)) logger.warn(` ${logicalId}`);
|
|
75360
|
+
if (refreshable.length > 10) logger.warn(` ... and ${refreshable.length - 10} more`);
|
|
75361
|
+
}
|
|
75362
|
+
if (refused.length > 0) {
|
|
75363
|
+
logger.warn(`${refused.length} of ${entries.length} resource(s) had their baseline REFUSED by a \`cdkd import\` run, because their recorded properties can no longer position the secret redaction. \`cdkd state refresh-observed\` will decline them too — capturing a readback against those properties could persist a resolved secret into state.json in plaintext. Deploy a change to each one to restore its baseline.`);
|
|
75364
|
+
for (const [logicalId] of refused.slice(0, 10)) logger.warn(` ${logicalId}`);
|
|
75365
|
+
if (refused.length > 10) logger.warn(` ... and ${refused.length - 10} more`);
|
|
75366
|
+
}
|
|
75021
75367
|
}
|
|
75022
75368
|
/**
|
|
75023
75369
|
* Walk every stack other than `exportingStackName` looking for
|
|
@@ -76374,10 +76720,10 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
76374
76720
|
if (options.long) {
|
|
76375
76721
|
const lines = [];
|
|
76376
76722
|
for (const detail of details) {
|
|
76377
|
-
lines.push(detail.logicalId);
|
|
76378
|
-
lines.push(` Type: ${detail.resourceType}`);
|
|
76379
|
-
lines.push(` PhysicalID: ${
|
|
76380
|
-
lines.push(` Dependencies: ${detail.dependencies
|
|
76723
|
+
lines.push(stripControlChars(detail.logicalId));
|
|
76724
|
+
lines.push(` Type: ${formatAttributeValue(detail.resourceType)}`);
|
|
76725
|
+
lines.push(` PhysicalID: ${formatAttributeValue(detail.physicalId)}`);
|
|
76726
|
+
lines.push(` Dependencies: ${formatDependencyList(detail.dependencies)}`);
|
|
76381
76727
|
const attrEntries = Object.entries(detail.attributes);
|
|
76382
76728
|
if (attrEntries.length === 0) lines.push(" Attributes: (none)");
|
|
76383
76729
|
else {
|
|
@@ -76390,15 +76736,27 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
76390
76736
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
76391
76737
|
return;
|
|
76392
76738
|
}
|
|
76393
|
-
const
|
|
76394
|
-
|
|
76395
|
-
|
|
76739
|
+
const rows = details.map((d) => ({
|
|
76740
|
+
logicalId: stripControlChars(d.logicalId),
|
|
76741
|
+
resourceType: formatAttributeValue(d.resourceType),
|
|
76742
|
+
physicalId: formatAttributeValue(d.physicalId)
|
|
76743
|
+
}));
|
|
76744
|
+
const idWidth = Math.max(...rows.map((r) => r.logicalId.length));
|
|
76745
|
+
const typeWidth = Math.max(...rows.map((r) => r.resourceType.length));
|
|
76746
|
+
for (const row of rows) process.stdout.write(`${row.logicalId.padEnd(idWidth)} ${row.resourceType.padEnd(typeWidth)} ${row.physicalId}\n`);
|
|
76396
76747
|
} finally {
|
|
76397
76748
|
setup.dispose();
|
|
76398
76749
|
}
|
|
76399
76750
|
}
|
|
76400
76751
|
/**
|
|
76401
|
-
* Render
|
|
76752
|
+
* Render one record-derived value into a human row.
|
|
76753
|
+
*
|
|
76754
|
+
* Named for its first caller and used well beyond an attribute now: a state or
|
|
76755
|
+
* lock record is read as an unchecked cast, so a field declared `string` can
|
|
76756
|
+
* hold anything, and the bare strip throws on it and empties the whole render.
|
|
76757
|
+
* This survives that, and for an actual string it is byte-identical to the
|
|
76758
|
+
* strip — which is why it is the default choice for a record FIELD, and the bare
|
|
76759
|
+
* strip is kept only where the value is a string by construction.
|
|
76402
76760
|
*
|
|
76403
76761
|
* Scalar values render as-is; objects/arrays are JSON-encoded inline so a
|
|
76404
76762
|
* resource block stays compact even when an attribute is structured.
|
|
@@ -76417,16 +76775,24 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
76417
76775
|
* pretty-prints — `JSON.stringify` is called without an indent, so a newline
|
|
76418
76776
|
* here is content injected by the value, not layout.
|
|
76419
76777
|
*
|
|
76420
|
-
* Only the human path routes through this
|
|
76421
|
-
*
|
|
76422
|
-
* `
|
|
76778
|
+
* Only the human path routes through this, which is the same split
|
|
76779
|
+
* `renderOutputChangeLines` makes. `--json` is the OTHER side of it rather than
|
|
76780
|
+
* a byte-faithful echo: `state show --json` reserializes the parsed record and
|
|
76781
|
+
* the lock it prints was sanitised upstream, and `state resources --json` emits
|
|
76782
|
+
* a constructed summary with defaults filled in. What `--json` does not do is
|
|
76783
|
+
* pass a value through THIS function.
|
|
76423
76784
|
*/
|
|
76424
76785
|
function formatAttributeValue(value) {
|
|
76425
76786
|
if (value === null) return "null";
|
|
76426
76787
|
if (value === void 0) return "undefined";
|
|
76427
76788
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return stripControlChars(String(value));
|
|
76428
|
-
|
|
76429
|
-
|
|
76789
|
+
let json;
|
|
76790
|
+
try {
|
|
76791
|
+
json = JSON.stringify(value);
|
|
76792
|
+
} catch {
|
|
76793
|
+
json = void 0;
|
|
76794
|
+
}
|
|
76795
|
+
return json === void 0 ? UNSERIALIZABLE : stripControlChars(json);
|
|
76430
76796
|
}
|
|
76431
76797
|
/**
|
|
76432
76798
|
* Render a duration in milliseconds as `1m23s` / `45s`.
|
|
@@ -76437,7 +76803,65 @@ function formatDuration(ms) {
|
|
|
76437
76803
|
return `${Math.floor(seconds / 60)}m${seconds % 60}s`;
|
|
76438
76804
|
}
|
|
76439
76805
|
/**
|
|
76806
|
+
* Render a whole `Dependencies` row from a list that is an unchecked cast.
|
|
76807
|
+
*
|
|
76808
|
+
* It owns the `(none)` case as well as the values, so that no caller touches
|
|
76809
|
+
* `.length` on a value whose type it cannot trust — reading `.length` is itself
|
|
76810
|
+
* a throw site, and `"A".length > 0` would pass an emptiness test and then
|
|
76811
|
+
* throw inside `join`, which is a method of an ARRAY. So the test is
|
|
76812
|
+
* array-ness. Elements are rendered ONE AT A TIME through the field guard
|
|
76813
|
+
* rather than joined by it: an element that throws on STRING COERCION
|
|
76814
|
+
* (`{"toString": null}` is valid JSON, and `JSON.stringify` handles it fine)
|
|
76815
|
+
* throws INSIDE `join`, where no guard wrapping the result can catch it.
|
|
76816
|
+
*
|
|
76817
|
+
* ABSENT and `null` part company here, deliberately. An absent field is a
|
|
76818
|
+
* record saying nothing, which `(none)` states correctly; a `null` is a record
|
|
76819
|
+
* saying `null`, and printing `(none)` over it would claim something the record
|
|
76820
|
+
* does not. JSON can express `null` here and a hand-edited record can carry it,
|
|
76821
|
+
* so the difference is worth the surprise of seeing `Dependencies: null`.
|
|
76822
|
+
*/
|
|
76823
|
+
function formatDependencyList(deps) {
|
|
76824
|
+
if (Array.isArray(deps)) return deps.length > 0 ? deps.map((dep) => formatAttributeValue(dep)).join(", ") : "(none)";
|
|
76825
|
+
return deps === void 0 ? "(none)" : formatAttributeValue(deps);
|
|
76826
|
+
}
|
|
76827
|
+
/**
|
|
76828
|
+
* Render `Last Modified` from a record whose `lastModified` is an unchecked cast.
|
|
76829
|
+
*
|
|
76830
|
+
* `new Date(x).toISOString()` throws a RangeError on a value it cannot date —
|
|
76831
|
+
* an out-of-range number, most non-numeric strings — and that empties the whole
|
|
76832
|
+
* render, the failure `formatAttributeValue` exists to stop everywhere else in
|
|
76833
|
+
* this block. `new Date` ITSELF throws a TypeError on an object whose
|
|
76834
|
+
* `toString` is unusable (`{"toString": null}` is valid JSON), which is why the
|
|
76835
|
+
* construction is inside the `try` and not only the formatting.
|
|
76836
|
+
*
|
|
76837
|
+
* Only the throw is guarded: the value still reaches `new Date` as-is, so every
|
|
76838
|
+
* input the previous line already dated renders the same ISO string — an ISO or
|
|
76839
|
+
* `YYYY-MM-DD` string, `null` and a boolean included. Anything else falls
|
|
76840
|
+
* through to the guard the other fields take.
|
|
76841
|
+
*/
|
|
76842
|
+
function formatLastModified(value) {
|
|
76843
|
+
try {
|
|
76844
|
+
return new Date(value).toISOString();
|
|
76845
|
+
} catch {
|
|
76846
|
+
return formatAttributeValue(value);
|
|
76847
|
+
}
|
|
76848
|
+
}
|
|
76849
|
+
/**
|
|
76440
76850
|
* Render lock metadata for the `state show` block.
|
|
76851
|
+
*
|
|
76852
|
+
* This row takes NO display guard — `Version` is the other, for its own reason —
|
|
76853
|
+
* and here the reason is upstream: `LockManager.getLockRecord` already passes
|
|
76854
|
+
* `owner` and `operation` through `displaySafe`, which coerces and replaces the
|
|
76855
|
+
* whole control class with spaces. They arrive here as control-free strings, so
|
|
76856
|
+
* a guard would be redundant — and a test for one could only be written by
|
|
76857
|
+
* mocking the read that sanitises them, which would pin nothing about the real
|
|
76858
|
+
* path. `displaySafe`'s own `String(value)` is unguarded, so a value that throws
|
|
76859
|
+
* on coercion fails THERE, before this function; that is go-to-k/cdkd#2947 and
|
|
76860
|
+
* not something a guard here could catch either.
|
|
76861
|
+
*
|
|
76862
|
+
* `expiresAt` is declared a number and is not guaranteed to be one, but it
|
|
76863
|
+
* reaches the row only through subtraction and `formatDuration`, so no character
|
|
76864
|
+
* the record carries can survive into the output either.
|
|
76441
76865
|
*/
|
|
76442
76866
|
function formatLockSummary(lockInfo) {
|
|
76443
76867
|
if (!lockInfo) return "unlocked";
|
|
@@ -76459,8 +76883,8 @@ function createStateResourcesCommand() {
|
|
|
76459
76883
|
* `cdkd state show <stack>` command implementation
|
|
76460
76884
|
*
|
|
76461
76885
|
* Renders the full state record for one stack: stack-level metadata, lock
|
|
76462
|
-
* status, outputs, and every resource (including properties).
|
|
76463
|
-
* most verbose `state` subcommand — use `state list` / `state resources` for
|
|
76886
|
+
* status, outputs, skipped outputs, and every resource (including properties).
|
|
76887
|
+
* The deepest / most verbose `state` subcommand — use `state list` / `state resources` for
|
|
76464
76888
|
* lighter inspection.
|
|
76465
76889
|
*
|
|
76466
76890
|
* When the same stack name has state in multiple regions, requires
|
|
@@ -76473,8 +76897,9 @@ function createStateResourcesCommand() {
|
|
|
76473
76897
|
* - `--show-nested` (issue #555 A4): when the stack contains
|
|
76474
76898
|
* `AWS::CloudFormation::Stack` rows, recursively load every child state
|
|
76475
76899
|
* record (`cdkd/<parent>~<childLogicalId>/<region>/state.json`) and append
|
|
76476
|
-
* its block after the parent's.
|
|
76477
|
-
*
|
|
76900
|
+
* its block after the parent's. With no nested children the rendered
|
|
76901
|
+
* blocks are the same, though a `Skipped outputs:` explanation moves to
|
|
76902
|
+
* the end of the tree.
|
|
76478
76903
|
*/
|
|
76479
76904
|
async function stateShowCommand(stackName, options) {
|
|
76480
76905
|
const logger = getLogger();
|
|
@@ -76503,27 +76928,82 @@ async function stateShowCommand(stackName, options) {
|
|
|
76503
76928
|
}, null, 2)}\n`);
|
|
76504
76929
|
return;
|
|
76505
76930
|
}
|
|
76506
|
-
process.stdout.write(`${renderStateBlock(stateResult.state, lockInfo).join("\n")}\n`);
|
|
76931
|
+
process.stdout.write(`${renderStateBlock(stateResult.state, lockInfo, true).join("\n")}\n`);
|
|
76507
76932
|
} finally {
|
|
76508
76933
|
setup.dispose();
|
|
76509
76934
|
}
|
|
76510
76935
|
}
|
|
76511
76936
|
/**
|
|
76937
|
+
* How much of a `skippedOutputs` digest the text view shows. A prefix
|
|
76938
|
+
* DIFFERENCE proves two records differ, which is what a reader wants at a
|
|
76939
|
+
* glance; a prefix MATCH proves nothing, so an exact comparison belongs to
|
|
76940
|
+
* `--json`, which carries the digest whole.
|
|
76941
|
+
*/
|
|
76942
|
+
const SKIPPED_DIGEST_PREVIEW_LEN = 12;
|
|
76943
|
+
/** What `formatAttributeValue` prints for a value `JSON.stringify` refuses. */
|
|
76944
|
+
const UNSERIALIZABLE = "(unserializable)";
|
|
76945
|
+
/**
|
|
76946
|
+
* The `skippedOutputs` rows a state record owes, sorted by key.
|
|
76947
|
+
*/
|
|
76948
|
+
function sortedSkippedOutputs(state) {
|
|
76949
|
+
return Object.entries(state.skippedOutputs ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
76950
|
+
}
|
|
76951
|
+
/** Whether one record renders the block. The ONE copy of that condition. */
|
|
76952
|
+
function rendersSkippedBlock(state) {
|
|
76953
|
+
return Object.keys(state.skippedOutputs ?? {}).length > 0;
|
|
76954
|
+
}
|
|
76955
|
+
/**
|
|
76956
|
+
* The explanation printed under a `Skipped outputs:` block.
|
|
76957
|
+
*
|
|
76958
|
+
* At column ZERO, unlike the rows it follows. At the rows' two-space indent a
|
|
76959
|
+
* reader — or a `grep '^ '` — takes these lines for another key. It also
|
|
76960
|
+
* quotes `Outputs:` mid-sentence, so a test matching that as a SUBSTRING has
|
|
76961
|
+
* to anchor on the section's own line to mean anything.
|
|
76962
|
+
*
|
|
76963
|
+
* Emitted ONCE per render rather than per block: under `--show-nested` the
|
|
76964
|
+
* same explanation would otherwise repeat for every child that skipped
|
|
76965
|
+
* anything.
|
|
76966
|
+
*
|
|
76967
|
+
* That placement is why the opening sentence NAMES its referent rather than
|
|
76968
|
+
* saying "these". At the end of a tree render it follows every rendered stack
|
|
76969
|
+
* block, so it is not adjacent to the block it explains and a bare
|
|
76970
|
+
* demonstrative reads as belonging to whichever stack printed last. The
|
|
76971
|
+
* single-stack path still has the adjacency; the wording holds in both.
|
|
76972
|
+
*
|
|
76973
|
+
* It does not claim `Outputs:` is on the screen. That block is omitted when
|
|
76974
|
+
* the bag is empty, which includes the first-deploy case this record exists
|
|
76975
|
+
* for — every output failed, so `outputs` is `{}` while `skippedOutputs` is
|
|
76976
|
+
* populated.
|
|
76977
|
+
*/
|
|
76978
|
+
function skippedOutputsLegend() {
|
|
76979
|
+
return [
|
|
76980
|
+
"",
|
|
76981
|
+
"The last deploy could not resolve the keys listed under `Skipped outputs:`",
|
|
76982
|
+
"above, and recorded a digest of their template inputs. While the record",
|
|
76983
|
+
"binds and a key is still absent from the stored outputs, `cdkd diff`",
|
|
76984
|
+
"previews it as ABSENT — no row, no warning. A key whose earlier value",
|
|
76985
|
+
"was retained is also stored under `Outputs:` here, where the record does",
|
|
76986
|
+
"NOT suppress it: the ordinary rules apply, up to `cdkd diff` suppressing",
|
|
76987
|
+
"its whole Outputs section if the key still cannot resolve.",
|
|
76988
|
+
"Binding rule: `bindingSkippedOutputs` in src/analyzer/skipped-outputs.ts."
|
|
76989
|
+
];
|
|
76990
|
+
}
|
|
76991
|
+
/**
|
|
76512
76992
|
* Render one stack's state record as a human-readable multi-line block —
|
|
76513
76993
|
* the shared body used by both the single-stack default output and each
|
|
76514
76994
|
* nested child rendered under `--show-nested`.
|
|
76515
76995
|
*/
|
|
76516
|
-
function renderStateBlock(state, lockInfo) {
|
|
76996
|
+
function renderStateBlock(state, lockInfo, withLegend) {
|
|
76517
76997
|
const lines = [];
|
|
76518
|
-
lines.push(`Stack: ${state.stackName}`);
|
|
76519
|
-
if (state.region) lines.push(` Region: ${state.region}`);
|
|
76998
|
+
lines.push(`Stack: ${formatAttributeValue(state.stackName)}`);
|
|
76999
|
+
if (state.region) lines.push(` Region: ${formatAttributeValue(state.region)}`);
|
|
76520
77000
|
lines.push(` Version: ${state.version}`);
|
|
76521
|
-
lines.push(` Last Modified: ${
|
|
77001
|
+
lines.push(` Last Modified: ${formatLastModified(state.lastModified)}`);
|
|
76522
77002
|
lines.push(` Lock: ${formatLockSummary(lockInfo)}`);
|
|
76523
77003
|
if (state.parentStack !== void 0) {
|
|
76524
|
-
const parentRegionStr = state.parentRegion ? ` (${state.parentRegion})` : "";
|
|
76525
|
-
const logicalIdStr = state.parentLogicalId !== void 0 ? `, logical id: ${state.parentLogicalId}` : "";
|
|
76526
|
-
lines.push(` Parent: ${state.parentStack}${parentRegionStr}${logicalIdStr}`);
|
|
77004
|
+
const parentRegionStr = state.parentRegion ? ` (${formatAttributeValue(state.parentRegion)})` : "";
|
|
77005
|
+
const logicalIdStr = state.parentLogicalId !== void 0 ? `, logical id: ${formatAttributeValue(state.parentLogicalId)}` : "";
|
|
77006
|
+
lines.push(` Parent: ${formatAttributeValue(state.parentStack)}${parentRegionStr}${logicalIdStr}`);
|
|
76527
77007
|
}
|
|
76528
77008
|
const outputEntries = Object.entries(state.outputs ?? {});
|
|
76529
77009
|
if (outputEntries.length > 0) {
|
|
@@ -76531,18 +77011,28 @@ function renderStateBlock(state, lockInfo) {
|
|
|
76531
77011
|
lines.push("Outputs:");
|
|
76532
77012
|
for (const [k, v] of outputEntries) lines.push(` ${stripControlChars(k)}: ${formatAttributeValue(v)}`);
|
|
76533
77013
|
}
|
|
77014
|
+
if (rendersSkippedBlock(state)) {
|
|
77015
|
+
lines.push("");
|
|
77016
|
+
lines.push("Skipped outputs:");
|
|
77017
|
+
for (const [k, digest] of sortedSkippedOutputs(state)) {
|
|
77018
|
+
const fullDigest = formatAttributeValue(digest);
|
|
77019
|
+
const shownDigest = fullDigest.length > SKIPPED_DIGEST_PREVIEW_LEN && fullDigest !== UNSERIALIZABLE ? `${fullDigest.slice(0, SKIPPED_DIGEST_PREVIEW_LEN)}…` : fullDigest;
|
|
77020
|
+
lines.push(` ${stripControlChars(k)}: ${shownDigest}`);
|
|
77021
|
+
}
|
|
77022
|
+
if (withLegend) lines.push(...skippedOutputsLegend());
|
|
77023
|
+
}
|
|
76534
77024
|
const resourceEntries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
76535
77025
|
lines.push("");
|
|
76536
77026
|
lines.push(`Resources (${resourceEntries.length}):`);
|
|
76537
77027
|
for (const [logicalId, resource] of resourceEntries) {
|
|
76538
77028
|
lines.push("");
|
|
76539
|
-
lines.push(logicalId);
|
|
76540
|
-
lines.push(` Type: ${resource.resourceType}`);
|
|
76541
|
-
lines.push(` PhysicalID: ${
|
|
77029
|
+
lines.push(stripControlChars(logicalId));
|
|
77030
|
+
lines.push(` Type: ${formatAttributeValue(resource.resourceType)}`);
|
|
77031
|
+
lines.push(` PhysicalID: ${formatAttributeValue(resource.physicalId)}`);
|
|
76542
77032
|
const provisionedBy = resource.provisionedBy ?? "(sdk, legacy default)";
|
|
76543
|
-
lines.push(` ProvisionedBy: ${provisionedBy}`);
|
|
76544
|
-
|
|
76545
|
-
lines.push(` Dependencies: ${
|
|
77033
|
+
lines.push(` ProvisionedBy: ${formatAttributeValue(provisionedBy)}`);
|
|
77034
|
+
if (resource.observedBaselineRefused === true) lines.push(" ObservedBaseline: REFUSED by 'cdkd import' — no baseline will be captured (deploy a change to this resource to restore one)");
|
|
77035
|
+
lines.push(` Dependencies: ${formatDependencyList(resource.dependencies)}`);
|
|
76546
77036
|
const attrEntries = Object.entries(resource.attributes ?? {});
|
|
76547
77037
|
if (attrEntries.length === 0) lines.push(" Attributes: (none)");
|
|
76548
77038
|
else {
|
|
@@ -76598,6 +77088,14 @@ async function loadLocksForTree(tree, lockManager, rootLock) {
|
|
|
76598
77088
|
return decorate(tree);
|
|
76599
77089
|
}
|
|
76600
77090
|
/**
|
|
77091
|
+
* Whether any node in this tree owes a `Skipped outputs:` block, and so
|
|
77092
|
+
* whether the tree render owes the legend once at its end. Asked through
|
|
77093
|
+
* {@link rendersSkippedBlock}, the same predicate the block's own guard uses.
|
|
77094
|
+
*/
|
|
77095
|
+
function treeOwesSkippedLegend(node) {
|
|
77096
|
+
return rendersSkippedBlock(node.state) || node.children.some(treeOwesSkippedLegend);
|
|
77097
|
+
}
|
|
77098
|
+
/**
|
|
76601
77099
|
* Render the parent's block followed by every descendant in DFS order,
|
|
76602
77100
|
* each separated by a blank line and prefixed with a `Nested stack: ...`
|
|
76603
77101
|
* header so the user can scan the tree top-down in one screen.
|
|
@@ -76608,15 +77106,16 @@ async function loadLocksForTree(tree, lockManager, rootLock) {
|
|
|
76608
77106
|
*/
|
|
76609
77107
|
function renderTreeWithChildren(root) {
|
|
76610
77108
|
const lines = [];
|
|
76611
|
-
lines.push(...renderStateBlock(root.state, root.lock));
|
|
77109
|
+
lines.push(...renderStateBlock(root.state, root.lock, false));
|
|
76612
77110
|
appendDescendants(root.children, lines);
|
|
77111
|
+
if (treeOwesSkippedLegend(root)) lines.push(...skippedOutputsLegend());
|
|
76613
77112
|
return lines;
|
|
76614
77113
|
}
|
|
76615
77114
|
function appendDescendants(children, out) {
|
|
76616
77115
|
for (const child of children) {
|
|
76617
77116
|
out.push("");
|
|
76618
|
-
out.push(`Nested stack: ${child.stackName}`);
|
|
76619
|
-
out.push(...renderStateBlock(child.state, child.lock));
|
|
77117
|
+
out.push(`Nested stack: ${stripControlChars(child.stackName)}`);
|
|
77118
|
+
out.push(...renderStateBlock(child.state, child.lock, false));
|
|
76620
77119
|
appendDescendants(child.children, out);
|
|
76621
77120
|
}
|
|
76622
77121
|
}
|
|
@@ -77268,6 +77767,7 @@ async function stateRefreshObservedCommand(stackArgs, options) {
|
|
|
77268
77767
|
let totalRefreshed = 0;
|
|
77269
77768
|
let totalUnsupported = 0;
|
|
77270
77769
|
let totalFailed = 0;
|
|
77770
|
+
let totalRefusedBaseline = 0;
|
|
77271
77771
|
for (const target of targets) {
|
|
77272
77772
|
if (!target.region) throw new Error(`Stack '${target.stackName}' has only a legacy state record without a region. Run 'cdkd deploy ${target.stackName}' (or any cdkd write) first to migrate it to the region-scoped layout, then re-run refresh-observed.`);
|
|
77273
77773
|
const counts = await refreshObservedForStack(target.stackName, target.region, setup.stateBackend, setup.lockManager, providerRegistry, {
|
|
@@ -77282,9 +77782,11 @@ async function stateRefreshObservedCommand(stackArgs, options) {
|
|
|
77282
77782
|
totalRefreshed += counts.refreshed;
|
|
77283
77783
|
totalUnsupported += counts.unsupported;
|
|
77284
77784
|
totalFailed += counts.failed;
|
|
77785
|
+
totalRefusedBaseline += counts.refusedBaseline;
|
|
77285
77786
|
}
|
|
77286
77787
|
const summary = options.dryRun ? `Plan: ${totalRefreshed} resource(s) would be refreshed, ${totalUnsupported} unsupported, ${totalFailed} would fail (--dry-run, no state was written)` : `Done: ${totalRefreshed} resource(s) refreshed, ${totalUnsupported} unsupported, ${totalFailed} failed`;
|
|
77287
77788
|
logger.info(`\n${summary}`);
|
|
77789
|
+
if (totalRefusedBaseline > 0) logger.warn(`${totalRefusedBaseline} resource(s) ${options.dryRun ? "would NOT be refreshed" : "were NOT refreshed"}: a 'cdkd import' run refused to capture their observed-properties baseline, because their recorded properties can no longer position the secret redaction — refreshing against those properties could persist a resolved secret into state.json in plaintext. A deploy that actually CHANGES one of them restores its baseline — a NO_CHANGE deploy does not, and re-running this command will refuse them again. Drift compares against their recorded properties until then.`);
|
|
77288
77790
|
if (totalFailed > 0) throw new PartialFailureError(`Refresh completed with ${totalFailed} per-resource readCurrentState failure(s). Affected resources keep their previous observedProperties (or no observedProperties at all). Re-run 'cdkd state refresh-observed' to retry.`);
|
|
77289
77791
|
} finally {
|
|
77290
77792
|
setup.dispose();
|
|
@@ -77351,13 +77853,19 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
77351
77853
|
return {
|
|
77352
77854
|
refreshed: 0,
|
|
77353
77855
|
unsupported: 0,
|
|
77354
|
-
failed: 0
|
|
77856
|
+
failed: 0,
|
|
77857
|
+
refusedBaseline: 0
|
|
77355
77858
|
};
|
|
77356
77859
|
}
|
|
77357
77860
|
if (opts.dryRun) {
|
|
77358
77861
|
let wouldRefresh = 0;
|
|
77359
77862
|
let wouldUnsupported = 0;
|
|
77863
|
+
let wouldRefuse = 0;
|
|
77360
77864
|
for (const [, resource] of entries) {
|
|
77865
|
+
if (resource.observedBaselineRefused === true) {
|
|
77866
|
+
wouldRefuse++;
|
|
77867
|
+
continue;
|
|
77868
|
+
}
|
|
77361
77869
|
let provider;
|
|
77362
77870
|
try {
|
|
77363
77871
|
provider = providerRegistry.getProviderFor({
|
|
@@ -77371,11 +77879,12 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
77371
77879
|
if (provider.readCurrentState) wouldRefresh++;
|
|
77372
77880
|
else wouldUnsupported++;
|
|
77373
77881
|
}
|
|
77374
|
-
logger.info(`Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported`);
|
|
77882
|
+
logger.info(`Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported` + (wouldRefuse > 0 ? `, ${wouldRefuse} refused (import baseline refusal)` : ""));
|
|
77375
77883
|
return {
|
|
77376
77884
|
refreshed: wouldRefresh,
|
|
77377
77885
|
unsupported: wouldUnsupported,
|
|
77378
|
-
failed: 0
|
|
77886
|
+
failed: 0,
|
|
77887
|
+
refusedBaseline: wouldRefuse
|
|
77379
77888
|
};
|
|
77380
77889
|
}
|
|
77381
77890
|
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
@@ -77389,8 +77898,13 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
77389
77898
|
let refreshed = 0;
|
|
77390
77899
|
let unsupported = 0;
|
|
77391
77900
|
let failed = 0;
|
|
77901
|
+
let refusedBaseline = 0;
|
|
77392
77902
|
await withStackName(stackName, async () => {
|
|
77393
77903
|
const tasks = entries.map(async ([logicalId, resource]) => {
|
|
77904
|
+
if (resource.observedBaselineRefused === true) {
|
|
77905
|
+
refusedBaseline++;
|
|
77906
|
+
return;
|
|
77907
|
+
}
|
|
77394
77908
|
if (providerRegistry.shouldSkipResource(resource.resourceType)) {
|
|
77395
77909
|
unsupported++;
|
|
77396
77910
|
return;
|
|
@@ -77429,11 +77943,12 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
77429
77943
|
const saveOptions = { expectedEtag: etag };
|
|
77430
77944
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
77431
77945
|
await stateBackend.saveState(stackName, region, state, saveOptions);
|
|
77432
|
-
logger.info(`✓ ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed`);
|
|
77946
|
+
logger.info(`✓ ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed` + (refusedBaseline > 0 ? `, ${refusedBaseline} refused (import baseline refusal)` : ""));
|
|
77433
77947
|
return {
|
|
77434
77948
|
refreshed,
|
|
77435
77949
|
unsupported,
|
|
77436
|
-
failed
|
|
77950
|
+
failed,
|
|
77951
|
+
refusedBaseline
|
|
77437
77952
|
};
|
|
77438
77953
|
} finally {
|
|
77439
77954
|
await lockManager.releaseLock(stackName, region).catch((err) => {
|
|
@@ -77674,6 +78189,7 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77674
78189
|
if (!stateData) throw new Error(`Rollback journal exists for '${stackName}' (${region}) but its state.json is missing (keys: ${setup.prefix}/${stackName}/${region}/state.json and .../rollback-journal.json). State appears corrupted — inspect the bucket manually.`);
|
|
77675
78190
|
const baseState = stateData.state;
|
|
77676
78191
|
const stateResources = { ...baseState.resources };
|
|
78192
|
+
const mintedOrphans = [];
|
|
77677
78193
|
const orphanLogicalIds = new Set(options.orphan ?? []);
|
|
77678
78194
|
const newestSegment = journal.segments[journal.segments.length - 1];
|
|
77679
78195
|
if (newestSegment.roleArn && !options.roleArn) logger.info(`Note: the failed deploy ran with --role-arn ${newestSegment.roleArn}; this rollback is running with ambient credentials (pass --role-arn to match).`);
|
|
@@ -77720,9 +78236,10 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77720
78236
|
const { skippedOutputs: _droppedByRollback, ...carriedState } = baseState;
|
|
77721
78237
|
const next = () => ({
|
|
77722
78238
|
...carriedState,
|
|
77723
|
-
version:
|
|
78239
|
+
version: 10,
|
|
77724
78240
|
region,
|
|
77725
78241
|
resources: { ...stateResources },
|
|
78242
|
+
...orphansAfterRollback(baseState, mintedOrphans),
|
|
77726
78243
|
lastModified: Date.now()
|
|
77727
78244
|
});
|
|
77728
78245
|
try {
|
|
@@ -77764,7 +78281,8 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77764
78281
|
const failedResult = await replayFailedOperations(segment.failedOperations, stateResources, stackName, ctx, {
|
|
77765
78282
|
afterOp: saveState,
|
|
77766
78283
|
isInterrupted: () => interrupted,
|
|
77767
|
-
emitEnvelope: segment.operations.length === 0
|
|
78284
|
+
emitEnvelope: segment.operations.length === 0,
|
|
78285
|
+
onOrphan: (record) => mintedOrphans.push(record)
|
|
77768
78286
|
});
|
|
77769
78287
|
failedOpFailures = failedResult.failures;
|
|
77770
78288
|
failedOpWarnings = failedResult.warnings;
|
|
@@ -77785,7 +78303,8 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77785
78303
|
const replayResult = await replayRollback(segment.operations, stateResources, stackName, ctx, {
|
|
77786
78304
|
orphanLogicalIds,
|
|
77787
78305
|
afterOp: saveState,
|
|
77788
|
-
isInterrupted: () => interrupted
|
|
78306
|
+
isInterrupted: () => interrupted,
|
|
78307
|
+
onOrphan: (record) => mintedOrphans.push(record)
|
|
77789
78308
|
});
|
|
77790
78309
|
return {
|
|
77791
78310
|
failures: replayResult.failures + failedOpFailures,
|
|
@@ -77806,7 +78325,8 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77806
78325
|
} finally {
|
|
77807
78326
|
await eventRecorder.finalize(totalFailures > 0 || interrupted ? "FAILED" : "SUCCEEDED");
|
|
77808
78327
|
}
|
|
77809
|
-
|
|
78328
|
+
const survivingOrphans = orphansAfterRollback(baseState, mintedOrphans).orphans ?? [];
|
|
78329
|
+
if (journal.segments.length === 0 && oldestInitialDeploy && Object.keys(stateResources).length === 0 && survivingOrphans.length === 0) {
|
|
77810
78330
|
await setup.stateBackend.deleteState(stackName, region);
|
|
77811
78331
|
logger.info(`State for '${stackName}' (${region}) removed (stack fully rolled back).`);
|
|
77812
78332
|
}
|
|
@@ -80793,7 +81313,7 @@ async function importCommand(stackArg, options) {
|
|
|
80793
81313
|
}
|
|
80794
81314
|
}
|
|
80795
81315
|
const stackState = buildStackState(stackInfo.stackName, targetRegion, rows, templateParser, stateTemplate, existingState, selectiveMode);
|
|
80796
|
-
await captureObservedForImportedResources(stackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger)));
|
|
81316
|
+
await captureObservedForImportedResources(stackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger)), rebuiltLogicalIdsFrom(rows, stateTemplate));
|
|
80797
81317
|
const saveOptions = {};
|
|
80798
81318
|
if (existingEtag) saveOptions.expectedEtag = existingEtag;
|
|
80799
81319
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
@@ -81109,6 +81629,30 @@ function collectImportableResources(template) {
|
|
|
81109
81629
|
return out;
|
|
81110
81630
|
}
|
|
81111
81631
|
/**
|
|
81632
|
+
* The logical ids whose `ResourceState` a run REBUILT from the template — the
|
|
81633
|
+
* discriminator `captureObservedForImportedResources` needs for schema v10's
|
|
81634
|
+
* `observedBaselineRefused` (issue
|
|
81635
|
+
* [#2944](https://github.com/go-to-k/cdkd/issues/2944)).
|
|
81636
|
+
*
|
|
81637
|
+
* It is a function, exported and tested, rather than an inline
|
|
81638
|
+
* `rows.filter(...)` at each call site, because the predicate is the whole
|
|
81639
|
+
* safety property: `buildStackState` overwrites exactly the `'imported'` rows
|
|
81640
|
+
* and PRESERVES every other record in a selective merge, so a predicate that
|
|
81641
|
+
* drifts wider (`rows.map(...)`, or admitting another outcome) silently
|
|
81642
|
+
* re-opens the fifth-writer leak — a preserved record's stale, distrusted
|
|
81643
|
+
* `properties` would be treated as this run's resolution, captured against,
|
|
81644
|
+
* and its refusal cleared. Two inline copies could also drift apart from each
|
|
81645
|
+
* other and from `buildStackState`'s own test.
|
|
81646
|
+
*
|
|
81647
|
+
* Every non-`'imported'` outcome is EXCLUDED, including the ones that sound
|
|
81648
|
+
* harmless: `skipped-out-of-scope` is precisely the selective-merge case, and
|
|
81649
|
+
* `failed` / `skipped-not-found` / `skipped-no-impl` leave whatever the record
|
|
81650
|
+
* already held.
|
|
81651
|
+
*/
|
|
81652
|
+
function rebuiltLogicalIdsFrom(rows, template) {
|
|
81653
|
+
return new Set(rows.filter((r) => r.outcome === "imported" && r.physicalId !== void 0 && template.Resources[r.logicalId] !== void 0).map((r) => r.logicalId));
|
|
81654
|
+
}
|
|
81655
|
+
/**
|
|
81112
81656
|
* Compose a `StackState` from the per-resource import outcomes plus
|
|
81113
81657
|
* dependency info recovered from the template.
|
|
81114
81658
|
*
|
|
@@ -81148,12 +81692,13 @@ function buildStackState(stackName, region, rows, templateParser, template, exis
|
|
|
81148
81692
|
};
|
|
81149
81693
|
}
|
|
81150
81694
|
return {
|
|
81151
|
-
version:
|
|
81695
|
+
version: 10,
|
|
81152
81696
|
stackName,
|
|
81153
81697
|
region,
|
|
81154
81698
|
resources,
|
|
81155
81699
|
outputs: existingState?.outputs ?? {},
|
|
81156
81700
|
...existingState ? exportNamesCarriedFrom(existingState) : { exportNames: [] },
|
|
81701
|
+
...existingState ? orphansCarriedFrom(existingState) : {},
|
|
81157
81702
|
lastModified: Date.now()
|
|
81158
81703
|
};
|
|
81159
81704
|
}
|
|
@@ -81741,14 +82286,20 @@ const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
|
|
|
81741
82286
|
* call site's own note (issue
|
|
81742
82287
|
* [#2828](https://github.com/go-to-k/cdkd/issues/2828)).
|
|
81743
82288
|
*/
|
|
81744
|
-
async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds) {
|
|
82289
|
+
async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds, rebuiltLogicalIds) {
|
|
81745
82290
|
const entries = Object.entries(stackState.resources);
|
|
81746
82291
|
if (entries.length === 0) return;
|
|
81747
82292
|
await Promise.all(entries.map(async ([logicalId, resource]) => {
|
|
82293
|
+
if (resource.observedBaselineRefused === true && !rebuiltLogicalIds.has(logicalId)) {
|
|
82294
|
+
logger.debug(`observedProperties capture SKIPPED for preserved ${logicalId} (${resource.resourceType}): a previous 'cdkd import' run refused this record's baseline and this run did not re-import it, so its recorded properties are still the ones that refusal distrusted. Deploy a change to this resource to restore a baseline.`);
|
|
82295
|
+
return;
|
|
82296
|
+
}
|
|
81748
82297
|
if (unsafeObservedBaselineLogicalIds.has(logicalId)) {
|
|
82298
|
+
resource.observedBaselineRefused = true;
|
|
81749
82299
|
logger.debug(`observedProperties capture SKIPPED for imported ${logicalId} (${resource.resourceType}): the recorded properties no longer spell the template's dynamic reference, so they cannot position a redaction — capturing an AWS readback against them could persist a resolved secret in plaintext. Drift will compare against the recorded properties for this resource until the next successful deploy.`);
|
|
81750
82300
|
return;
|
|
81751
82301
|
}
|
|
82302
|
+
delete resource.observedBaselineRefused;
|
|
81752
82303
|
try {
|
|
81753
82304
|
const provider = providerRegistry.getProviderFor({
|
|
81754
82305
|
resourceType: resource.resourceType,
|
|
@@ -81928,7 +82479,7 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
81928
82479
|
childStackState.parentStack = parentStackName;
|
|
81929
82480
|
childStackState.parentLogicalId = childLogicalId;
|
|
81930
82481
|
childStackState.parentRegion = parentRegion;
|
|
81931
|
-
await captureObservedForImportedResources(childStackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger)));
|
|
82482
|
+
await captureObservedForImportedResources(childStackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger)), rebuiltLogicalIdsFrom(rows, childTemplate));
|
|
81932
82483
|
await stateBackend.saveState(childStackName, childRegion, childStackState);
|
|
81933
82484
|
logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
|
|
81934
82485
|
if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
|
|
@@ -91159,4 +91710,4 @@ function buildProgram() {
|
|
|
91159
91710
|
|
|
91160
91711
|
//#endregion
|
|
91161
91712
|
export { buildProgram };
|
|
91162
|
-
//# sourceMappingURL=program-
|
|
91713
|
+
//# sourceMappingURL=program-Bg60hZhr.js.map
|