@go-to-k/cdkd 0.288.7 → 0.289.1
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-B3stERoz.js} +3 -3
- package/dist/{asg-provider-CRxRmEzr.js.map → asg-provider-B3stERoz.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-BQUjVO_m.js → deploy-engine-14EMrfBt.js} +406 -31
- package/dist/deploy-engine-14EMrfBt.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-CIn_e9xI.js} +704 -110
- package/dist/{program-BEvh5nqs.js.map → program-CIn_e9xI.js.map} +1 -1
- package/dist/{version-DPtrXz0m.js → version-BvvRGJMb.js} +2 -2
- package/dist/{version-DPtrXz0m.js.map → version-BvvRGJMb.js.map} +1 -1
- package/package.json +2 -2
- 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-14EMrfBt.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-BvvRGJMb.js";
|
|
5
|
+
import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-B3stERoz.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
|
}
|
|
@@ -6626,6 +6682,21 @@ function configItemId(item) {
|
|
|
6626
6682
|
if (typeof id === "string") return id;
|
|
6627
6683
|
if (typeof id === "number" && Number.isFinite(id)) return String(id);
|
|
6628
6684
|
}
|
|
6685
|
+
/**
|
|
6686
|
+
* Whether the template asks for a bucket-level Object Lock.
|
|
6687
|
+
*
|
|
6688
|
+
* Shared on purpose by the two sites that must agree: `create()` passes
|
|
6689
|
+
* `ObjectLockEnabledForBucket` to `CreateBucket` from it, and the create-path
|
|
6690
|
+
* Object Lock skip records `{ ObjectLockEnabled: 'Enabled' }` from it. Spelled
|
|
6691
|
+
* twice, the recorded value could claim a flag the bucket does not carry.
|
|
6692
|
+
*
|
|
6693
|
+
* Note the source: the BUCKET-level `ObjectLockEnabled`, never the one nested
|
|
6694
|
+
* inside `ObjectLockConfiguration` -- only the top-level property reaches
|
|
6695
|
+
* `CreateBucket`, and it is the only one AWS can report back.
|
|
6696
|
+
*/
|
|
6697
|
+
function declaresBucketObjectLock(properties) {
|
|
6698
|
+
return properties["ObjectLockEnabled"] === true || properties["ObjectLockEnabled"] === "true";
|
|
6699
|
+
}
|
|
6629
6700
|
function isPlainObject$1(value) {
|
|
6630
6701
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6631
6702
|
}
|
|
@@ -8913,28 +8984,46 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
8913
8984
|
*
|
|
8914
8985
|
* CFn property: ObjectLockConfiguration
|
|
8915
8986
|
* - ObjectLockEnabled: 'Enabled'
|
|
8916
|
-
* - Rule.DefaultRetention (Mode, Days, Years)
|
|
8987
|
+
* - Rule.DefaultRetention (Mode, Days, Years, DefaultEventHold)
|
|
8917
8988
|
* SDK: PutObjectLockConfiguration with ObjectLockConfiguration
|
|
8918
8989
|
*
|
|
8919
8990
|
* Note: ObjectLockEnabled at bucket level must be set at creation time.
|
|
8920
8991
|
* This method only applies the rule/default retention config post-creation.
|
|
8992
|
+
*
|
|
8993
|
+
* Returns whether the Put was issued. A malformed `DefaultEventHold`
|
|
8994
|
+
* container SKIPS the whole Put (see the guard below), and the callers owe
|
|
8995
|
+
* `effectiveProperties` the previous value when it does.
|
|
8921
8996
|
*/
|
|
8922
|
-
async applyObjectLockConfiguration(bucketName, config) {
|
|
8997
|
+
async applyObjectLockConfiguration(bucketName, config, onUnusable) {
|
|
8923
8998
|
const rule = config["Rule"];
|
|
8924
8999
|
const objectLockConfig = { ObjectLockEnabled: "Enabled" };
|
|
8925
9000
|
if (rule) {
|
|
8926
9001
|
const retention = rule["DefaultRetention"];
|
|
8927
|
-
if (retention)
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
9002
|
+
if (retention) {
|
|
9003
|
+
const declaredHold = retention["DefaultEventHold"];
|
|
9004
|
+
let eventHold;
|
|
9005
|
+
if (declaredHold != null) {
|
|
9006
|
+
const path = "AWS::S3::Bucket ObjectLockConfiguration.Rule.DefaultRetention.DefaultEventHold";
|
|
9007
|
+
eventHold = onUnusable ? requireConfigObject(declaredHold, path, { onUnusable }) : requireConfigObject(declaredHold, path);
|
|
9008
|
+
if (eventHold === void 0) return false;
|
|
9009
|
+
}
|
|
9010
|
+
objectLockConfig.Rule = { DefaultRetention: {
|
|
9011
|
+
Mode: retention["Mode"],
|
|
9012
|
+
Days: retention["Days"],
|
|
9013
|
+
Years: retention["Years"],
|
|
9014
|
+
DefaultEventHold: eventHold ? {
|
|
9015
|
+
Days: eventHold["Days"],
|
|
9016
|
+
Years: eventHold["Years"]
|
|
9017
|
+
} : void 0
|
|
9018
|
+
} };
|
|
9019
|
+
}
|
|
8932
9020
|
}
|
|
8933
9021
|
await this.s3Client.send(new PutObjectLockConfigurationCommand({
|
|
8934
9022
|
Bucket: bucketName,
|
|
8935
9023
|
ObjectLockConfiguration: objectLockConfig
|
|
8936
9024
|
}));
|
|
8937
9025
|
this.logger.debug(`Applied object lock configuration to bucket ${bucketName}`);
|
|
9026
|
+
return true;
|
|
8938
9027
|
}
|
|
8939
9028
|
/**
|
|
8940
9029
|
* Apply additional bucket configuration after creation.
|
|
@@ -9470,7 +9559,9 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
9470
9559
|
await this.s3Client.send(new DeleteBucketReplicationCommand({ Bucket: bucketName }));
|
|
9471
9560
|
this.logger.debug(`Deleted replication configuration on bucket ${bucketName}`);
|
|
9472
9561
|
});
|
|
9473
|
-
await this.diffSubConfig(bucketName, previousProperties["ObjectLockConfiguration"], properties["ObjectLockConfiguration"], async (cfg) =>
|
|
9562
|
+
await this.diffSubConfig(bucketName, previousProperties["ObjectLockConfiguration"], properties["ObjectLockConfiguration"], async (cfg) => {
|
|
9563
|
+
if (!await this.applyObjectLockConfiguration(bucketName, cfg, (m) => this.logger.warn(m))) retainPrevious("ObjectLockConfiguration");
|
|
9564
|
+
}, async () => {
|
|
9474
9565
|
await this.s3Client.send(new PutObjectLockConfigurationCommand({
|
|
9475
9566
|
Bucket: bucketName,
|
|
9476
9567
|
ObjectLockConfiguration: { ObjectLockEnabled: "Enabled" }
|
|
@@ -9615,7 +9706,9 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
9615
9706
|
if (!await this.applyReplicationConfiguration(bucketName, replConfig, replayOnUnusable)) overrides.set("ReplicationConfiguration", void 0);
|
|
9616
9707
|
}
|
|
9617
9708
|
const objectLockConfig = properties["ObjectLockConfiguration"];
|
|
9618
|
-
if (objectLockConfig)
|
|
9709
|
+
if (objectLockConfig) {
|
|
9710
|
+
if (!await this.applyObjectLockConfiguration(bucketName, objectLockConfig, replayOnUnusable)) overrides.set("ObjectLockConfiguration", declaresBucketObjectLock(properties) ? { ObjectLockEnabled: "Enabled" } : void 0);
|
|
9711
|
+
}
|
|
9619
9712
|
return overrides;
|
|
9620
9713
|
}
|
|
9621
9714
|
/**
|
|
@@ -9902,7 +9995,7 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
9902
9995
|
const region = await this.getRegion();
|
|
9903
9996
|
const canonicalRegion = canonicalizeRegion(region);
|
|
9904
9997
|
if (canonicalRegion !== "us-east-1") createParams.CreateBucketConfiguration = { LocationConstraint: canonicalRegion };
|
|
9905
|
-
if (properties
|
|
9998
|
+
if (declaresBucketObjectLock(properties)) createParams.ObjectLockEnabledForBucket = true;
|
|
9906
9999
|
const preflight = canonicalizeRegion(region) === "us-east-1" ? await this.probeBucketRegion(bucketName) : { kind: "absent" };
|
|
9907
10000
|
let createdNewBucket = false;
|
|
9908
10001
|
try {
|
|
@@ -10527,6 +10620,12 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
10527
10620
|
if (r.Mode !== void 0) retention["Mode"] = r.Mode;
|
|
10528
10621
|
if (r.Days !== void 0) retention["Days"] = r.Days;
|
|
10529
10622
|
if (r.Years !== void 0) retention["Years"] = r.Years;
|
|
10623
|
+
if (r.DefaultEventHold !== void 0) {
|
|
10624
|
+
const eventHold = {};
|
|
10625
|
+
if (r.DefaultEventHold.Days !== void 0) eventHold["Days"] = r.DefaultEventHold.Days;
|
|
10626
|
+
if (r.DefaultEventHold.Years !== void 0) eventHold["Years"] = r.DefaultEventHold.Years;
|
|
10627
|
+
retention["DefaultEventHold"] = eventHold;
|
|
10628
|
+
}
|
|
10530
10629
|
out["Rule"] = { DefaultRetention: retention };
|
|
10531
10630
|
}
|
|
10532
10631
|
return out;
|
|
@@ -61046,8 +61145,9 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
61046
61145
|
};
|
|
61047
61146
|
const guardIndeterminateTargets = /* @__PURE__ */ new Set();
|
|
61048
61147
|
const resourceCount = Object.keys(state.resources).length;
|
|
61148
|
+
const orphanCount = (state.orphans ?? []).length;
|
|
61049
61149
|
const regionForState = state.region ?? ctx.baseRegion;
|
|
61050
|
-
if (resourceCount === 0) {
|
|
61150
|
+
if (resourceCount === 0 && orphanCount === 0) {
|
|
61051
61151
|
logger.info(`Stack ${stackName} has no resources, cleaning up state...`);
|
|
61052
61152
|
let emptyLockHeld = false;
|
|
61053
61153
|
let emptyInterrupted = false;
|
|
@@ -61093,7 +61193,9 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
61093
61193
|
emptyLockHeld = true;
|
|
61094
61194
|
try {
|
|
61095
61195
|
const recheck = await ctx.stateBackend.getState(stackName, regionForState);
|
|
61096
|
-
|
|
61196
|
+
const recheckResources = recheck ? Object.keys(recheck.state.resources).length : 0;
|
|
61197
|
+
const recheckOrphans = recheck ? (recheck.state.orphans ?? []).length : 0;
|
|
61198
|
+
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
61199
|
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
61098
61200
|
logger.info(`${green("✓")} State deleted`);
|
|
61099
61201
|
} finally {
|
|
@@ -61116,6 +61218,12 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
61116
61218
|
logger.info(`\nResources to be deleted (${resourceCount}):`);
|
|
61117
61219
|
for (const [logicalId, resource] of Object.entries(state.resources)) logger.info(` - ${logicalId} (${resource.resourceType})`);
|
|
61118
61220
|
const protectedCount = ctx.removeProtection ? countProtectedResources(state) : 0;
|
|
61221
|
+
const orphansAtDestroy = state.orphans ?? [];
|
|
61222
|
+
if (orphansAtDestroy.length > 0) {
|
|
61223
|
+
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.`);
|
|
61224
|
+
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:");
|
|
61225
|
+
for (const entry of orphansAtDestroy) logger.info(` - ${entry.logicalId} (${entry.state.resourceType}) ${entry.state.physicalId}`);
|
|
61226
|
+
}
|
|
61119
61227
|
if (!ctx.skipConfirmation) {
|
|
61120
61228
|
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
61229
|
const rl = readline$1.createInterface({
|
|
@@ -63165,7 +63273,8 @@ async function deployCommand(stacks, options) {
|
|
|
63165
63273
|
registerAllProviders(stackProviderRegistry);
|
|
63166
63274
|
stackProviderRegistry.setCustomResourceResponseBucket(stateBucket);
|
|
63167
63275
|
if (options.allowUnsupportedTypes?.length) stackProviderRegistry.allowUnsupportedTypes(options.allowUnsupportedTypes);
|
|
63168
|
-
|
|
63276
|
+
const preferSdkRoute = [...options.preferSdkRoute ?? [], ...options.allowUnsupportedProperties ?? []];
|
|
63277
|
+
if (preferSdkRoute.length) stackProviderRegistry.allowUnsupportedProperties(preferSdkRoute);
|
|
63169
63278
|
const eventRecorder = startRunRecorder({
|
|
63170
63279
|
backend: stackStateBackend,
|
|
63171
63280
|
stackName: stackInfo.stackName,
|
|
@@ -63186,7 +63295,7 @@ async function deployCommand(stacks, options) {
|
|
|
63186
63295
|
const syncValidation = validateRecreateTargets({
|
|
63187
63296
|
template: stackInfo.template,
|
|
63188
63297
|
state: stateForRecreateCheck?.state ?? {
|
|
63189
|
-
version:
|
|
63298
|
+
version: 10,
|
|
63190
63299
|
stackName: stackInfo.stackName,
|
|
63191
63300
|
region: stackRegion,
|
|
63192
63301
|
resources: {},
|
|
@@ -63195,7 +63304,7 @@ async function deployCommand(stacks, options) {
|
|
|
63195
63304
|
},
|
|
63196
63305
|
recreateViaCcApi: options.recreateViaCcApi ?? [],
|
|
63197
63306
|
recreateViaSdkProvider: options.recreateViaSdkProvider ?? [],
|
|
63198
|
-
allowUnsupportedProperties: new Set(options.allowUnsupportedProperties ?? []),
|
|
63307
|
+
allowUnsupportedProperties: /* @__PURE__ */ new Set([...options.preferSdkRoute ?? [], ...options.allowUnsupportedProperties ?? []]),
|
|
63199
63308
|
forceStatefulRecreation: options.forceStatefulRecreation ?? false,
|
|
63200
63309
|
hasSdkProvider: (rt) => stackProviderRegistry.getProviderType(rt) === "sdk"
|
|
63201
63310
|
});
|
|
@@ -64849,7 +64958,7 @@ async function loadStateOrEmpty(stackName, region, stateBackend) {
|
|
|
64849
64958
|
region,
|
|
64850
64959
|
resources: {},
|
|
64851
64960
|
outputs: {},
|
|
64852
|
-
version:
|
|
64961
|
+
version: 10,
|
|
64853
64962
|
lastModified: Date.now()
|
|
64854
64963
|
};
|
|
64855
64964
|
}
|
|
@@ -64996,7 +65105,24 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
64996
65105
|
...conditions && { conditions },
|
|
64997
65106
|
skipDynamicReferences: true
|
|
64998
65107
|
});
|
|
64999
|
-
|
|
65108
|
+
let adoptedOrphans = [];
|
|
65109
|
+
let adoptedRecords = {};
|
|
65110
|
+
let blocking = [];
|
|
65111
|
+
let stateForDiff = currentState;
|
|
65112
|
+
if (currentState.orphans?.length && options.previewOrphanAdoption) {
|
|
65113
|
+
const plan = await options.previewOrphanAdoption(currentState, effectiveTemplate, stackName, region);
|
|
65114
|
+
adoptedOrphans = Object.keys(plan.adopted);
|
|
65115
|
+
adoptedRecords = plan.adopted;
|
|
65116
|
+
blocking = plan.refusals;
|
|
65117
|
+
if (adoptedOrphans.length > 0) stateForDiff = {
|
|
65118
|
+
...currentState,
|
|
65119
|
+
resources: {
|
|
65120
|
+
...currentState.resources,
|
|
65121
|
+
...plan.adopted
|
|
65122
|
+
}
|
|
65123
|
+
};
|
|
65124
|
+
}
|
|
65125
|
+
const changes = await diffCalculator.calculateDiff(stateForDiff, effectiveTemplate, resolveFn, canonicalizeProperties);
|
|
65000
65126
|
const changedLogicalIds = new Set([...changes.values()].filter((c) => c.changeType !== "NO_CHANGE").map((c) => c.logicalId));
|
|
65001
65127
|
const bindingSkipped = bindingSkippedOutputs(outputsDigestSource, currentState.skippedOutputs, changedLogicalIds);
|
|
65002
65128
|
const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions, currentState.outputs, bindingSkipped);
|
|
@@ -65012,7 +65138,10 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
65012
65138
|
});
|
|
65013
65139
|
return {
|
|
65014
65140
|
changes,
|
|
65015
|
-
outputChanges
|
|
65141
|
+
outputChanges,
|
|
65142
|
+
adoptedOrphans,
|
|
65143
|
+
adoptedRecords,
|
|
65144
|
+
blocking
|
|
65016
65145
|
};
|
|
65017
65146
|
}
|
|
65018
65147
|
/**
|
|
@@ -65075,22 +65204,32 @@ async function resolveChildStackParameters(parentStackRow, parentTemplate, paren
|
|
|
65075
65204
|
* fail at deploy time.
|
|
65076
65205
|
*/
|
|
65077
65206
|
async function buildDiffTree(args) {
|
|
65078
|
-
const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback } = args;
|
|
65207
|
+
const { stackName, displayName, region, template, nestedTemplates, recursive, stateBackend, diffCalculator, parentHasSecretReference, parameters, canonicalizeProperties, assetRedirect, cfnFallback, previewOrphanAdoption } = args;
|
|
65079
65208
|
const state = await loadStateOrEmpty(stackName, region, stateBackend);
|
|
65080
65209
|
const secretBearingAbove = parentHasSecretReference === true || templateHasSecretDynamicReference(template);
|
|
65081
|
-
const { changes, outputChanges } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, {
|
|
65210
|
+
const { changes, outputChanges, adoptedOrphans, adoptedRecords, blocking } = await computeStackDiff(state, template, region, stackName, stateBackend, diffCalculator, {
|
|
65082
65211
|
...parameters && { parameters },
|
|
65083
65212
|
...canonicalizeProperties && { canonicalizeProperties },
|
|
65084
65213
|
...cfnFallback !== void 0 && { cfnFallback },
|
|
65214
|
+
...previewOrphanAdoption && { previewOrphanAdoption },
|
|
65085
65215
|
inheritSecretBearingTemplate: false
|
|
65086
65216
|
});
|
|
65217
|
+
const stateAfterAdoption = Object.keys(adoptedRecords).length > 0 ? {
|
|
65218
|
+
...state,
|
|
65219
|
+
resources: {
|
|
65220
|
+
...state.resources,
|
|
65221
|
+
...adoptedRecords
|
|
65222
|
+
}
|
|
65223
|
+
} : state;
|
|
65087
65224
|
const node = {
|
|
65088
65225
|
stackName,
|
|
65089
65226
|
displayName,
|
|
65090
65227
|
region,
|
|
65091
65228
|
changes,
|
|
65092
|
-
ccApiRoutes: collectCcApiRoutes(template,
|
|
65229
|
+
ccApiRoutes: collectCcApiRoutes(template, stateAfterAdoption),
|
|
65093
65230
|
outputChanges,
|
|
65231
|
+
adoptedOrphans,
|
|
65232
|
+
blocking,
|
|
65094
65233
|
children: []
|
|
65095
65234
|
};
|
|
65096
65235
|
if (!recursive) return node;
|
|
@@ -65104,7 +65243,7 @@ async function buildDiffTree(args) {
|
|
|
65104
65243
|
const childTemplate = readNestedTemplate(childTemplatePath);
|
|
65105
65244
|
if (assetRedirect) rewriteTemplateAssetReferences(childTemplate, assetRedirect);
|
|
65106
65245
|
const grandchildTemplates = indexNestedChildTemplates(childTemplate, childTemplatePath);
|
|
65107
|
-
const childParameters = await resolveChildStackParameters(resource, template,
|
|
65246
|
+
const childParameters = await resolveChildStackParameters(resource, template, stateAfterAdoption, region, stackName, stateBackend, parameters, cfnFallback);
|
|
65108
65247
|
node.children.push(await buildDiffTree({
|
|
65109
65248
|
stackName: childStackName,
|
|
65110
65249
|
displayName: childStackName,
|
|
@@ -65118,6 +65257,7 @@ async function buildDiffTree(args) {
|
|
|
65118
65257
|
...canonicalizeProperties && { canonicalizeProperties },
|
|
65119
65258
|
...assetRedirect && { assetRedirect },
|
|
65120
65259
|
...cfnFallback !== void 0 && { cfnFallback },
|
|
65260
|
+
...previewOrphanAdoption && { previewOrphanAdoption },
|
|
65121
65261
|
parentHasSecretReference: secretBearingAbove
|
|
65122
65262
|
}));
|
|
65123
65263
|
}
|
|
@@ -65143,6 +65283,8 @@ async function buildDeletedSubtree(stackName, region, stateBackend, diffCalculat
|
|
|
65143
65283
|
region,
|
|
65144
65284
|
changes,
|
|
65145
65285
|
ccApiRoutes: /* @__PURE__ */ new Map(),
|
|
65286
|
+
adoptedOrphans: [],
|
|
65287
|
+
blocking: [],
|
|
65146
65288
|
outputChanges,
|
|
65147
65289
|
children: []
|
|
65148
65290
|
};
|
|
@@ -65209,9 +65351,41 @@ function collectCcApiRoutes(template, state) {
|
|
|
65209
65351
|
/** True when this node has at least one real (non-`NO_CHANGE`) change. */
|
|
65210
65352
|
function nodeHasChanges(node) {
|
|
65211
65353
|
for (const change of node.changes.values()) if (change.changeType !== "NO_CHANGE") return true;
|
|
65212
|
-
return node.outputChanges.length > 0;
|
|
65354
|
+
return node.outputChanges.length > 0 || node.adoptedOrphans.length > 0;
|
|
65213
65355
|
}
|
|
65214
65356
|
/** True when this node OR any descendant has a real change (tree-wide drift detector for `--fail`). */
|
|
65357
|
+
/**
|
|
65358
|
+
* How many deploy refusals the whole tree carries (issue go-to-k/cdkd#2943).
|
|
65359
|
+
*
|
|
65360
|
+
* A COUNT rather than a boolean because the caller's message quotes it.
|
|
65361
|
+
* {@link treeIsWorthRendering} below IS a boolean built on this, added when
|
|
65362
|
+
* review found the render gate gated on changes alone — it derives from this
|
|
65363
|
+
* count rather than walking the tree again, so there is still one recursion to
|
|
65364
|
+
* keep in step.
|
|
65365
|
+
*
|
|
65366
|
+
* Deliberately SEPARATE from {@link treeHasChanges}: `--fail` answers "is
|
|
65367
|
+
* there a delta", and a refusal is not a delta — a user who runs
|
|
65368
|
+
* `cdkd diff --fail` in CI to detect drift must not have that signal merged
|
|
65369
|
+
* with "the deploy cannot start", which is true whether or not anything
|
|
65370
|
+
* changed.
|
|
65371
|
+
*/
|
|
65372
|
+
function countBlocking(node) {
|
|
65373
|
+
return node.blocking.length + node.children.reduce((n, c) => n + countBlocking(c), 0);
|
|
65374
|
+
}
|
|
65375
|
+
/**
|
|
65376
|
+
* Whether `cdkd diff` should render this tree's block at all (issue
|
|
65377
|
+
* go-to-k/cdkd#2943).
|
|
65378
|
+
*
|
|
65379
|
+
* A named predicate rather than an inline conjunction at the call site,
|
|
65380
|
+
* because the two halves are easy to get wrong INDEPENDENTLY and were:
|
|
65381
|
+
* {@link renderDiffTree} was written to print a `Blocking` section for a node
|
|
65382
|
+
* with no changes, and the caller then skipped the render on
|
|
65383
|
+
* `!treeHasChanges` alone — so a changeless refusal would print "No changes
|
|
65384
|
+
* detected" and exit non-zero citing reasons that were never printed.
|
|
65385
|
+
*/
|
|
65386
|
+
function treeIsWorthRendering(node) {
|
|
65387
|
+
return treeHasChanges(node) || countBlocking(node) > 0;
|
|
65388
|
+
}
|
|
65215
65389
|
function treeHasChanges(node) {
|
|
65216
65390
|
if (nodeHasChanges(node)) return true;
|
|
65217
65391
|
return node.children.some(treeHasChanges);
|
|
@@ -65248,6 +65422,8 @@ function diffTreeToJson(node) {
|
|
|
65248
65422
|
...change.changeType !== "REMOVE" ? { newValue: change.newValue } : {},
|
|
65249
65423
|
export: change.isExport
|
|
65250
65424
|
})),
|
|
65425
|
+
adoptedOrphans: node.adoptedOrphans,
|
|
65426
|
+
blocking: node.blocking,
|
|
65251
65427
|
children: node.children.map(diffTreeToJson)
|
|
65252
65428
|
};
|
|
65253
65429
|
}
|
|
@@ -65340,11 +65516,22 @@ function renderDiffValue(own, filtered, indent, isNewSide) {
|
|
|
65340
65516
|
* #614's auto-fallback decision at plan time. DELETE lines are not annotated
|
|
65341
65517
|
* — the delete routing is recorded on each resource's `provisionedBy` state
|
|
65342
65518
|
* field rather than re-derived from the template.
|
|
65519
|
+
*
|
|
65520
|
+
* `adoptedOrphans` annotates the rows whose resource is in this diff only
|
|
65521
|
+
* because a rollback-orphan record was verified and spliced in (issue
|
|
65522
|
+
* go-to-k/cdkd#2943). Without it the row is indistinguishable from an ordinary
|
|
65523
|
+
* update, and the user last saw that resource FAIL and leave state — so an
|
|
65524
|
+
* unannotated `[~]` reads as cdkd having quietly kept managing it. Both
|
|
65525
|
+
* annotations can apply to one row; adoption is printed first because it is
|
|
65526
|
+
* why the row exists at all, where the routing describes how it will be
|
|
65527
|
+
* carried out.
|
|
65343
65528
|
*/
|
|
65344
|
-
function renderChangeLines(changes, logFn, ccApiRoutes) {
|
|
65529
|
+
function renderChangeLines(changes, logFn, ccApiRoutes, adoptedOrphans) {
|
|
65345
65530
|
let createCount = 0;
|
|
65346
65531
|
let updateCount = 0;
|
|
65347
65532
|
let deleteCount = 0;
|
|
65533
|
+
const adopted = new Set(adoptedOrphans ?? []);
|
|
65534
|
+
const annotateAdoption = (logicalId) => adopted.has(logicalId) ? " [adopted from a rollback orphan]" : "";
|
|
65348
65535
|
const annotateRouting = (logicalId) => {
|
|
65349
65536
|
const props = ccApiRoutes?.get(logicalId);
|
|
65350
65537
|
if (!props || props.length === 0) return "";
|
|
@@ -65354,11 +65541,11 @@ function renderChangeLines(changes, logFn, ccApiRoutes) {
|
|
|
65354
65541
|
for (const [logicalId, change] of changes.entries()) switch (change.changeType) {
|
|
65355
65542
|
case "CREATE":
|
|
65356
65543
|
createCount++;
|
|
65357
|
-
logFn(` [+] ${logicalId} (${change.resourceType})${annotateRouting(logicalId)}`);
|
|
65544
|
+
logFn(` [+] ${logicalId} (${change.resourceType})${annotateAdoption(logicalId)}${annotateRouting(logicalId)}`);
|
|
65358
65545
|
break;
|
|
65359
65546
|
case "UPDATE":
|
|
65360
65547
|
updateCount++;
|
|
65361
|
-
logFn(` [~] ${logicalId} (${change.resourceType})${annotateRouting(logicalId)}`);
|
|
65548
|
+
logFn(` [~] ${logicalId} (${change.resourceType})${annotateAdoption(logicalId)}${annotateRouting(logicalId)}`);
|
|
65362
65549
|
if (change.propertyChanges && change.propertyChanges.length > 0) for (const propChange of change.propertyChanges) {
|
|
65363
65550
|
const requiresReplace = propChange.requiresReplacement ? " [requires replacement]" : "";
|
|
65364
65551
|
const propagated = propChange.replacementPropagated ? " [replacement propagated]" : "";
|
|
@@ -65466,12 +65653,18 @@ function renderOutputChangeLines(outputChanges, logFn) {
|
|
|
65466
65653
|
* (the A4 `state show --show-nested` convention, full `~`-joined name).
|
|
65467
65654
|
*/
|
|
65468
65655
|
function renderDiffTree(node, isRoot, logFn) {
|
|
65469
|
-
|
|
65470
|
-
|
|
65471
|
-
|
|
65656
|
+
const hasChanges = nodeHasChanges(node);
|
|
65657
|
+
if (hasChanges || node.blocking.length > 0) logFn(isRoot ? `\nStack ${node.stackName}:` : `\nNested stack: ${node.displayName}`);
|
|
65658
|
+
if (hasChanges) {
|
|
65659
|
+
const { create, update, delete: del } = renderChangeLines(node.changes, logFn, node.ccApiRoutes, node.adoptedOrphans);
|
|
65472
65660
|
const outputs = renderOutputChangeLines(node.outputChanges, logFn);
|
|
65473
65661
|
logFn(`\n${create} to create, ${update} to update, ${del} to delete`);
|
|
65474
65662
|
if (outputs.add + outputs.change + outputs.remove > 0) logFn(`${outputs.add} output(s) to add, ${outputs.change} to change, ${outputs.remove} to remove`);
|
|
65663
|
+
if (node.adoptedOrphans.length > 0) logFn(`${node.adoptedOrphans.length} resource(s) to adopt from a previous rollback: ${node.adoptedOrphans.map(stripControlChars).join(", ")}`);
|
|
65664
|
+
}
|
|
65665
|
+
if (node.blocking.length > 0) {
|
|
65666
|
+
logFn("\n Blocking (cdkd deploy will refuse):");
|
|
65667
|
+
for (const reason of node.blocking) logFn(` ! ${stripControlChars(reason)}`);
|
|
65475
65668
|
}
|
|
65476
65669
|
for (const child of node.children) renderDiffTree(child, false, logFn);
|
|
65477
65670
|
}
|
|
@@ -65493,6 +65686,35 @@ var DiffDetectedError = class DiffDetectedError extends CdkdError {
|
|
|
65493
65686
|
}
|
|
65494
65687
|
};
|
|
65495
65688
|
/**
|
|
65689
|
+
* Signals that the preview completed and `cdkd deploy` would REFUSE to start
|
|
65690
|
+
* (issue go-to-k/cdkd#2943) — today, a rollback-orphan record whose physical
|
|
65691
|
+
* id another cdkd stack already manages.
|
|
65692
|
+
*
|
|
65693
|
+
* Exit **3**, and neither 1 nor 2, for reasons that are about what a caller
|
|
65694
|
+
* can conclude:
|
|
65695
|
+
*
|
|
65696
|
+
* - **1** is `--fail`'s "a change was detected". A refusal is not a change,
|
|
65697
|
+
* and merging them would make a CI job that gates on drift report the same
|
|
65698
|
+
* code for "there is work to do" and "the work cannot begin".
|
|
65699
|
+
* - **2** is this CLI's partial-failure family, documented as "work
|
|
65700
|
+
* completed, re-running typically resolves it" (`docs/cli-reference.md`).
|
|
65701
|
+
* A refusal is the opposite: re-running changes nothing until a human
|
|
65702
|
+
* resolves the ownership conflict.
|
|
65703
|
+
*
|
|
65704
|
+
* It is NOT silent: unlike `--fail`, this is not a flag the user opted into,
|
|
65705
|
+
* so the reason has to be visible even when the diff report scrolled away.
|
|
65706
|
+
* The `Blocking` section already printed the per-record detail; this adds the
|
|
65707
|
+
* one line that says the run cannot proceed.
|
|
65708
|
+
*/
|
|
65709
|
+
var DeployRefusalPreviewError = class DeployRefusalPreviewError extends CdkdError {
|
|
65710
|
+
exitCode = 3;
|
|
65711
|
+
constructor(count) {
|
|
65712
|
+
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");
|
|
65713
|
+
this.name = "DeployRefusalPreviewError";
|
|
65714
|
+
Object.setPrototypeOf(this, DeployRefusalPreviewError.prototype);
|
|
65715
|
+
}
|
|
65716
|
+
};
|
|
65717
|
+
/**
|
|
65496
65718
|
* Diff command implementation
|
|
65497
65719
|
*/
|
|
65498
65720
|
async function diffCommand(stacks, options) {
|
|
@@ -65553,6 +65775,34 @@ async function diffCommand(stacks, options) {
|
|
|
65553
65775
|
const diffProviderRegistry = new ProviderRegistry();
|
|
65554
65776
|
registerAllProviders(diffProviderRegistry);
|
|
65555
65777
|
const canonicalizeProperties = makeCanonicalizePropertiesFn(diffProviderRegistry);
|
|
65778
|
+
const previewOrphanAdoption = async (state, effectiveTemplate, orphanStackName, orphanRegion) => {
|
|
65779
|
+
const outcome = await planOrphanAdoption({
|
|
65780
|
+
records: state.orphans ?? [],
|
|
65781
|
+
managedLogicalIds: new Set(Object.keys(state.resources)),
|
|
65782
|
+
template: effectiveTemplate,
|
|
65783
|
+
stackName: orphanStackName,
|
|
65784
|
+
region: orphanRegion,
|
|
65785
|
+
getProvider: (resourceType, provisionedBy) => diffProviderRegistry.getProviderFor({
|
|
65786
|
+
resourceType,
|
|
65787
|
+
provisionedBy
|
|
65788
|
+
}).provider,
|
|
65789
|
+
nameProperties: (resourceType) => {
|
|
65790
|
+
const property = explicitNamePropertyFor(resourceType);
|
|
65791
|
+
return property ? [property] : [];
|
|
65792
|
+
},
|
|
65793
|
+
readSiblingClaims: makeSiblingClaimReader({
|
|
65794
|
+
stateBackend,
|
|
65795
|
+
selfStackName: orphanStackName,
|
|
65796
|
+
selfRegion: orphanRegion,
|
|
65797
|
+
logger
|
|
65798
|
+
}),
|
|
65799
|
+
logger
|
|
65800
|
+
});
|
|
65801
|
+
return {
|
|
65802
|
+
adopted: outcome.adopted,
|
|
65803
|
+
refusals: outcome.refusals
|
|
65804
|
+
};
|
|
65805
|
+
};
|
|
65556
65806
|
const recursive = options.recursive ?? false;
|
|
65557
65807
|
const resolveAssetRedirect = createAssetRedirectResolver({
|
|
65558
65808
|
stateBackend,
|
|
@@ -65581,17 +65831,20 @@ async function diffCommand(stacks, options) {
|
|
|
65581
65831
|
diffCalculator,
|
|
65582
65832
|
canonicalizeProperties,
|
|
65583
65833
|
...assetRedirect && { assetRedirect },
|
|
65584
|
-
...options.cfnFallback === false && { cfnFallback: false }
|
|
65834
|
+
...options.cfnFallback === false && { cfnFallback: false },
|
|
65835
|
+
previewOrphanAdoption
|
|
65585
65836
|
}));
|
|
65586
65837
|
}
|
|
65587
65838
|
if (options.json) process.stdout.write(`${JSON.stringify(trees.map(diffTreeToJson), null, 2)}\n`);
|
|
65588
65839
|
else for (const tree of trees) {
|
|
65589
|
-
if (!
|
|
65840
|
+
if (!treeIsWorthRendering(tree)) {
|
|
65590
65841
|
logger.info(`\n✓ No changes detected for stack ${tree.stackName}`);
|
|
65591
65842
|
continue;
|
|
65592
65843
|
}
|
|
65593
65844
|
renderDiffTree(tree, true, (msg) => logger.info(msg));
|
|
65594
65845
|
}
|
|
65846
|
+
const blockingCount = trees.reduce((n, tree) => n + countBlocking(tree), 0);
|
|
65847
|
+
if (blockingCount > 0) throw new DeployRefusalPreviewError(blockingCount);
|
|
65595
65848
|
if (options.fail && trees.some(treeHasChanges)) throw new DiffDetectedError();
|
|
65596
65849
|
} finally {
|
|
65597
65850
|
awsClients.destroy();
|
|
@@ -66109,10 +66362,9 @@ async function scrubCommand(stacks, options) {
|
|
|
66109
66362
|
* (issue #2005) — the outputs' own map plus every resource's, filtered to the
|
|
66110
66363
|
* values long enough to be a safe needle.
|
|
66111
66364
|
*
|
|
66112
|
-
*
|
|
66113
|
-
*
|
|
66114
|
-
*
|
|
66115
|
-
* value cannot rewrite another resource's coinciding literal (the collision the
|
|
66365
|
+
* Built here rather than kept as each pass's input everywhere — `outputSecrets`
|
|
66366
|
+
* and `perResourceSecrets` are deliberately SEPARATE bags so one resource's
|
|
66367
|
+
* secret value cannot rewrite another's coinciding literal (the collision the
|
|
66116
66368
|
* deploy engine's `perResourceSecrets` doc describes), and widening the bag the
|
|
66117
66369
|
* RESOURCE walk uses would re-open exactly that.
|
|
66118
66370
|
*
|
|
@@ -66198,9 +66450,10 @@ async function scrubCommand(stacks, options) {
|
|
|
66198
66450
|
* command can. Refusing the substring arm here would trade the irreversible
|
|
66199
66451
|
* failure for the reversible one in the wrong direction.
|
|
66200
66452
|
*/
|
|
66201
|
-
function allRecordedSecrets(outputSecrets, perResourceSecrets) {
|
|
66453
|
+
function allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets) {
|
|
66202
66454
|
const union = /* @__PURE__ */ new Map();
|
|
66203
66455
|
for (const recorded of perResourceSecrets.values()) for (const [value, expression] of recorded) union.set(value, expression);
|
|
66456
|
+
for (const recorded of orphanSecrets.values()) for (const [value, expression] of recorded) union.set(value, expression);
|
|
66204
66457
|
for (const [value, expression] of outputSecrets) union.set(value, expression);
|
|
66205
66458
|
for (const value of union.keys()) if (value.length < 4) union.delete(value);
|
|
66206
66459
|
return union;
|
|
@@ -67721,6 +67974,13 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67721
67974
|
if (acquired) await lockManager.acquireLockWithRetry(stack.stackName, region, void 0, "scrub");
|
|
67722
67975
|
const perResourceSecrets = /* @__PURE__ */ new Map();
|
|
67723
67976
|
const outputSecrets = /* @__PURE__ */ new Map();
|
|
67977
|
+
/**
|
|
67978
|
+
* Needles derived from each rollback-orphan record's OWN recorded bag
|
|
67979
|
+
* (issue go-to-k/cdkd#2943), keyed by logical id — per-record for the same
|
|
67980
|
+
* reason `perResourceSecrets` is per-resource: one record's secret must not
|
|
67981
|
+
* rewrite another's coinciding literal.
|
|
67982
|
+
*/
|
|
67983
|
+
const orphanSecrets = /* @__PURE__ */ new Map();
|
|
67724
67984
|
const prePassFindings = { unverifiable: [] };
|
|
67725
67985
|
try {
|
|
67726
67986
|
const loaded = await stateBackend.getState(stack.stackName, region);
|
|
@@ -67801,6 +68061,27 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67801
68061
|
}
|
|
67802
68062
|
perResourceTemplateProps.set(logicalId, templateResource.Properties);
|
|
67803
68063
|
}
|
|
68064
|
+
for (const record of state.orphans ?? []) {
|
|
68065
|
+
const recordedSecretValues = /* @__PURE__ */ new Map();
|
|
68066
|
+
orphanSecrets.set(record.logicalId, recordedSecretValues);
|
|
68067
|
+
const resolveInput = await pinCrossRegionSecrets({
|
|
68068
|
+
properties: record.state.properties,
|
|
68069
|
+
attributes: record.state.attributes ?? {},
|
|
68070
|
+
observedProperties: record.state.observedProperties ?? {}
|
|
68071
|
+
}, stack.stackName, {
|
|
68072
|
+
stackRegion: region,
|
|
68073
|
+
producerRegions,
|
|
68074
|
+
resolvers,
|
|
68075
|
+
recordedSecretValues,
|
|
68076
|
+
origin: `orphan record '${record.logicalId}'`
|
|
68077
|
+
});
|
|
68078
|
+
try {
|
|
68079
|
+
await resolver.resolve(resolveInput, resolverContext(recordedSecretValues));
|
|
68080
|
+
} catch (err) {
|
|
68081
|
+
if (isRegionAmbiguousRefusal(err)) throw err;
|
|
68082
|
+
logger.debug(`Resolution of orphan record ${record.logicalId} during scrub was partial: ${maskSecretsInText(err instanceof Error ? err.message : String(err), recordedSecretValues)}`);
|
|
68083
|
+
}
|
|
68084
|
+
}
|
|
67804
68085
|
for (const [name, output] of Object.entries(templateOutputs)) {
|
|
67805
68086
|
const declaredExportName = output.Export?.Name;
|
|
67806
68087
|
let exportName = declaredExportName;
|
|
@@ -67871,7 +68152,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67871
68152
|
secretBearingKeys.push(key);
|
|
67872
68153
|
logger.warn(secretBearingStateKeyWarning(stack.stackName, display));
|
|
67873
68154
|
}
|
|
67874
|
-
const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0);
|
|
68155
|
+
const totalSecrets = outputSecrets.size + [...perResourceSecrets.values()].reduce((n, m) => n + m.size, 0) + [...orphanSecrets.values()].reduce((n, m) => n + m.size, 0);
|
|
67875
68156
|
if (totalSecrets === 0) return {
|
|
67876
68157
|
recordsChanged: 0,
|
|
67877
68158
|
secretsFound: 0,
|
|
@@ -67894,13 +68175,27 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67894
68175
|
if (JSON.stringify(scrubbed) !== JSON.stringify(record)) recordsChanged++;
|
|
67895
68176
|
newResources[logicalId] = scrubbed;
|
|
67896
68177
|
}
|
|
67897
|
-
const
|
|
68178
|
+
const newOrphans = (state.orphans ?? []).map((record) => {
|
|
68179
|
+
const own = orphanSecrets.get(record.logicalId);
|
|
68180
|
+
const needles = new Map(allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
|
|
68181
|
+
for (const [plaintext, expression] of own ?? []) needles.set(plaintext, expression);
|
|
68182
|
+
if (needles.size === 0) return record;
|
|
68183
|
+
const scrubbed = scrubResourceRecord(record.state, needles, void 0, STATE_SOURCED_CROSS_GENERATION_RULES);
|
|
68184
|
+
if (JSON.stringify(scrubbed) === JSON.stringify(record.state)) return record;
|
|
68185
|
+
recordsChanged++;
|
|
68186
|
+
return {
|
|
68187
|
+
...record,
|
|
68188
|
+
state: scrubbed
|
|
68189
|
+
};
|
|
68190
|
+
});
|
|
68191
|
+
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
68192
|
if (JSON.stringify(newOutputs) !== JSON.stringify(state.outputs)) recordsChanged++;
|
|
67899
68193
|
if (recordsChanged > 0 && !opts.dryRun) {
|
|
67900
68194
|
const { skippedOutputs: _droppedByScrub, ...carriedState } = state;
|
|
67901
68195
|
const nextState = {
|
|
67902
68196
|
...carriedState,
|
|
67903
68197
|
resources: newResources,
|
|
68198
|
+
...state.orphans !== void 0 && { orphans: newOrphans },
|
|
67904
68199
|
outputs: newOutputs,
|
|
67905
68200
|
lastModified: Date.now()
|
|
67906
68201
|
};
|
|
@@ -67915,7 +68210,7 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
67915
68210
|
exportNameDisplay: (name) => secretSafeKeyDisplay(name, outputSecrets)
|
|
67916
68211
|
};
|
|
67917
68212
|
} catch (err) {
|
|
67918
|
-
throw maskSecretsInError(err, allRecordedSecrets(outputSecrets, perResourceSecrets));
|
|
68213
|
+
throw maskSecretsInError(err, allRecordedSecrets(outputSecrets, perResourceSecrets, orphanSecrets));
|
|
67919
68214
|
} finally {
|
|
67920
68215
|
if (acquired) await lockManager.releaseLock(stack.stackName, region).catch((err) => {
|
|
67921
68216
|
logger.warn(`Failed to release lock for ${stack.stackName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -68826,6 +69121,17 @@ function stripWalk(value) {
|
|
|
68826
69121
|
//#endregion
|
|
68827
69122
|
//#region src/cli/commands/drift.ts
|
|
68828
69123
|
/**
|
|
69124
|
+
* The schema version at which `ResourceState.observedBaselineRefused` arrived.
|
|
69125
|
+
* A record BELOW it could not carry a refusal, which is what
|
|
69126
|
+
* {@link warnIfPreV10BaselineGap} keys on (issue
|
|
69127
|
+
* [#3011](https://github.com/go-to-k/cdkd/issues/3011)).
|
|
69128
|
+
*
|
|
69129
|
+
* A literal rather than `STATE_SCHEMA_VERSION_CURRENT`: this is a FIXED
|
|
69130
|
+
* historical boundary, and binding it to "current" would silently stop warning
|
|
69131
|
+
* about v10 records on the next bump — the population it names does not move.
|
|
69132
|
+
*/
|
|
69133
|
+
const PRE_V10_BASELINE_WARNING_FLOOR = 10;
|
|
69134
|
+
/**
|
|
68829
69135
|
* Route one outcome to the handler for its variant.
|
|
68830
69136
|
*
|
|
68831
69137
|
* The mechanism issue [#2135](https://github.com/go-to-k/cdkd/issues/2135) asks
|
|
@@ -69047,6 +69353,10 @@ const UNCOMPARED_REASONS = {
|
|
|
69047
69353
|
kind: "unknown",
|
|
69048
69354
|
phrase: "not compared AT ALL: the read or comparison failed"
|
|
69049
69355
|
},
|
|
69356
|
+
baselineRefused: {
|
|
69357
|
+
kind: "unknown",
|
|
69358
|
+
phrase: "not compared AT ALL: a `cdkd import` run refused to capture their observed baseline"
|
|
69359
|
+
},
|
|
69050
69360
|
refused: {
|
|
69051
69361
|
kind: "unknown",
|
|
69052
69362
|
phrase: "only PARTIALLY compared: cdkd refused to resolve a dynamic reference their state records"
|
|
@@ -69592,7 +69902,8 @@ function notComparedReason(cause) {
|
|
|
69592
69902
|
return {
|
|
69593
69903
|
refused: "cdkd refused to resolve a dynamic reference its state records (spell the reference as a full ARN, which names its region)",
|
|
69594
69904
|
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"
|
|
69905
|
+
readFailed: "the read or comparison threw, so NONE of its properties were compared",
|
|
69906
|
+
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
69907
|
}[cause];
|
|
69597
69908
|
}
|
|
69598
69909
|
/**
|
|
@@ -69980,6 +70291,15 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
69980
70291
|
});
|
|
69981
70292
|
continue;
|
|
69982
70293
|
}
|
|
70294
|
+
if (resource.observedBaselineRefused === true) {
|
|
70295
|
+
outcomes.push({
|
|
70296
|
+
kind: "notCompared",
|
|
70297
|
+
logicalId,
|
|
70298
|
+
resourceType: resource.resourceType,
|
|
70299
|
+
notComparedCause: "baselineRefused"
|
|
70300
|
+
});
|
|
70301
|
+
continue;
|
|
70302
|
+
}
|
|
69983
70303
|
try {
|
|
69984
70304
|
let aws;
|
|
69985
70305
|
if (provider.readCurrentState) aws = await provider.readCurrentState(resource.physicalId, logicalId, resource.resourceType, resource.properties ?? {}, buildReadCurrentStateContext(state, logicalId));
|
|
@@ -70106,17 +70426,56 @@ async function runDriftForStack(stackName, region, stateBackend, providerRegistr
|
|
|
70106
70426
|
continue;
|
|
70107
70427
|
}
|
|
70108
70428
|
}
|
|
70429
|
+
const warnings = warnIfPreV10BaselineGap(state, outcomes, logger);
|
|
70109
70430
|
return {
|
|
70110
70431
|
stackName,
|
|
70111
70432
|
region,
|
|
70112
70433
|
outcomes,
|
|
70113
70434
|
state,
|
|
70114
70435
|
etag: result.etag,
|
|
70115
|
-
migrationPending: result.migrationPending ?? false
|
|
70436
|
+
migrationPending: result.migrationPending ?? false,
|
|
70437
|
+
warnings
|
|
70116
70438
|
};
|
|
70117
70439
|
});
|
|
70118
70440
|
}
|
|
70119
70441
|
/**
|
|
70442
|
+
* Warn once for a stack whose state PREDATES the refused-baseline marker
|
|
70443
|
+
* (`ResourceState.observedBaselineRefused`, state schema v10) and still has a
|
|
70444
|
+
* resource without a drift baseline (issue
|
|
70445
|
+
* [#3011](https://github.com/go-to-k/cdkd/issues/3011)).
|
|
70446
|
+
*
|
|
70447
|
+
* Both conjuncts are load-bearing, and neither is a heuristic:
|
|
70448
|
+
*
|
|
70449
|
+
* - `version < 10` is not "probably imported" — it is the exact statement that
|
|
70450
|
+
* this record could not carry a refusal even if one was made. A v10 record
|
|
70451
|
+
* that omits the field is a record nothing refused, and warning there would
|
|
70452
|
+
* be false.
|
|
70453
|
+
* - the missing baseline is what makes the warning ACTIONABLE. A pre-v10 stack
|
|
70454
|
+
* where every resource has a baseline has nothing for drift to fall back to
|
|
70455
|
+
* `properties` for, so it cannot hit the disclosure.
|
|
70456
|
+
*
|
|
70457
|
+
* Exported for unit testing -- internal to the drift flow otherwise.
|
|
70458
|
+
*/
|
|
70459
|
+
function warnIfPreV10BaselineGap(state, outcomes, logger) {
|
|
70460
|
+
if ((state.version ?? 1) >= PRE_V10_BASELINE_WARNING_FLOOR) return [];
|
|
70461
|
+
const compared = new Set(outcomes.flatMap((o) => matchOutcome(o, {
|
|
70462
|
+
drifted: (d) => [d.logicalId],
|
|
70463
|
+
clean: (c) => [c.logicalId],
|
|
70464
|
+
notCompared: (n) => [n.logicalId],
|
|
70465
|
+
unsupported: () => [],
|
|
70466
|
+
skipped: () => []
|
|
70467
|
+
})));
|
|
70468
|
+
const affected = Object.entries(state.resources ?? {}).filter(([id, r]) => r.observedProperties === void 0 && compared.has(id)).map(([id]) => id);
|
|
70469
|
+
if (affected.length === 0) return [];
|
|
70470
|
+
const shown = affected.slice(0, 10).join(", ");
|
|
70471
|
+
const rest = affected.length > 10 ? `, and ${affected.length - 10} more` : "";
|
|
70472
|
+
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.`;
|
|
70473
|
+
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.";
|
|
70474
|
+
logger.warn(finding);
|
|
70475
|
+
logger.warn(note);
|
|
70476
|
+
return [finding, note];
|
|
70477
|
+
}
|
|
70478
|
+
/**
|
|
70120
70479
|
* Set a value at a dotted path inside a plain object, creating intermediate
|
|
70121
70480
|
* objects as needed. Mirrors `lodash.set` for the subset of paths the drift
|
|
70122
70481
|
* comparator actually emits — dotted nested keys, no array indices.
|
|
@@ -70226,6 +70585,10 @@ async function runAccept(reports, stateBackend, stateConfig, awsClients, options
|
|
|
70226
70585
|
for (const outcome of driftedOutcomes) {
|
|
70227
70586
|
const existing = resources[outcome.logicalId];
|
|
70228
70587
|
if (!existing) continue;
|
|
70588
|
+
if (existing.observedBaselineRefused === true) {
|
|
70589
|
+
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.`);
|
|
70590
|
+
continue;
|
|
70591
|
+
}
|
|
70229
70592
|
const hasObserved = existing.observedProperties !== void 0;
|
|
70230
70593
|
const baselineSource = hasObserved ? existing.observedProperties : existing.properties ?? {};
|
|
70231
70594
|
const newBaseline = JSON.parse(JSON.stringify(baselineSource));
|
|
@@ -71389,6 +71752,11 @@ async function runRevert(reports, providerRegistry, stateBackend, stateConfig, a
|
|
|
71389
71752
|
logger.error(` ✗ ${report.stackName}/${outcome.logicalId} (${outcome.resourceType}): resource missing from state; skipped.`);
|
|
71390
71753
|
return;
|
|
71391
71754
|
}
|
|
71755
|
+
if (stateResource.observedBaselineRefused === true) {
|
|
71756
|
+
totalUnresolvable++;
|
|
71757
|
+
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.`);
|
|
71758
|
+
return;
|
|
71759
|
+
}
|
|
71392
71760
|
const provider = providerRegistry.getProviderFor({
|
|
71393
71761
|
resourceType: outcome.resourceType,
|
|
71394
71762
|
provisionedBy: stateResource.provisionedBy
|
|
@@ -71571,6 +71939,10 @@ function printAcceptPlan(reports, out) {
|
|
|
71571
71939
|
const lines = [];
|
|
71572
71940
|
let plannedWrites = 0;
|
|
71573
71941
|
for (const o of drifted) {
|
|
71942
|
+
if (report.state.resources[o.logicalId]?.observedBaselineRefused === true) {
|
|
71943
|
+
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`);
|
|
71944
|
+
continue;
|
|
71945
|
+
}
|
|
71574
71946
|
lines.push(` ~ ${o.logicalId} (${o.resourceType})\n`);
|
|
71575
71947
|
for (const change of o.changes) {
|
|
71576
71948
|
const refusal = acceptRefusalReason(change, o.maskedPaths);
|
|
@@ -71603,6 +71975,10 @@ function printRevertPlan(reports, out) {
|
|
|
71603
71975
|
if (drifted.length === 0) continue;
|
|
71604
71976
|
out.write(`\nPlan (--revert): push cdkd state values back into AWS for ${report.stackName} (${report.region}):\n`);
|
|
71605
71977
|
for (const o of drifted) {
|
|
71978
|
+
if (report.state.resources[o.logicalId]?.observedBaselineRefused === true) {
|
|
71979
|
+
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`);
|
|
71980
|
+
continue;
|
|
71981
|
+
}
|
|
71606
71982
|
const word = o.changes.length === 1 ? "property path" : "property paths";
|
|
71607
71983
|
out.write(` → provider.update on ${o.logicalId} (${o.resourceType}): revert ${o.changes.length} ${word}\n`);
|
|
71608
71984
|
for (const change of o.changes) out.write(` ${change.path}: ${formatScalar(change.awsValue)} -> ${formatScalar(change.stateValue)}\n`);
|
|
@@ -71709,12 +72085,13 @@ function writeJsonReport(reports) {
|
|
|
71709
72085
|
const notCompared = notComparedOutcomes(r).map(({ outcome, cause }) => ({
|
|
71710
72086
|
logicalId: outcome.logicalId,
|
|
71711
72087
|
type: outcome.resourceType,
|
|
71712
|
-
referencesUnresolved: cause !== "readFailed",
|
|
72088
|
+
referencesUnresolved: cause !== "readFailed" && cause !== "baselineRefused",
|
|
71713
72089
|
cause
|
|
71714
72090
|
}));
|
|
71715
72091
|
return {
|
|
71716
72092
|
stack: r.stackName,
|
|
71717
72093
|
region: r.region,
|
|
72094
|
+
...r.warnings.length > 0 && { warnings: r.warnings },
|
|
71718
72095
|
drifted,
|
|
71719
72096
|
clean,
|
|
71720
72097
|
notSupported,
|
|
@@ -71753,7 +72130,7 @@ function writeHumanReport(reports) {
|
|
|
71753
72130
|
const checked = inspected - notCompared.length;
|
|
71754
72131
|
if (drifted.length === 0) {
|
|
71755
72132
|
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`);
|
|
72133
|
+
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
72134
|
else process.stdout.write(`✓ ${report.stackName} (${report.region}): no drift detected (${checked} resource${checked === 1 ? "" : "s"} checked, ${unsupported.length} unsupported)\n`);
|
|
71758
72135
|
} else {
|
|
71759
72136
|
const word = drifted.length === 1 ? "resource" : "resources";
|
|
@@ -71768,9 +72145,12 @@ function writeHumanReport(reports) {
|
|
|
71768
72145
|
}
|
|
71769
72146
|
}
|
|
71770
72147
|
if (notCompared.length > 0) {
|
|
71771
|
-
const
|
|
71772
|
-
const
|
|
71773
|
-
|
|
72148
|
+
const notComparedAtAll = notCompared.filter((n) => n.cause === "readFailed" || n.cause === "baselineRefused").length;
|
|
72149
|
+
const readFailedCount = notCompared.filter((n) => n.cause === "readFailed").length;
|
|
72150
|
+
const baselineRefusedCount = notCompared.filter((n) => n.cause === "baselineRefused").length;
|
|
72151
|
+
const referenceCaused = notCompared.length - notComparedAtAll;
|
|
72152
|
+
const atAllCause = [...readFailedCount > 0 ? ["the read or comparison failed"] : [], ...baselineRefusedCount > 0 ? ["an import refused their observed baseline"] : []].join("; ");
|
|
72153
|
+
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
72154
|
for (const { outcome, cause } of notCompared) process.stdout.write(` ! ${outcome.logicalId} (${outcome.resourceType}) — ${notComparedReason(cause)}\n`);
|
|
71775
72155
|
}
|
|
71776
72156
|
if (unsupported.length > 0) {
|
|
@@ -75015,9 +75395,18 @@ function reportDriftBaselineGaps(state, logger) {
|
|
|
75015
75395
|
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
75396
|
return;
|
|
75017
75397
|
}
|
|
75018
|
-
|
|
75019
|
-
|
|
75020
|
-
if (
|
|
75398
|
+
const refused = missing.filter(([, r]) => r.observedBaselineRefused === true);
|
|
75399
|
+
const refreshable = missing.filter(([, r]) => r.observedBaselineRefused !== true);
|
|
75400
|
+
if (refreshable.length > 0) {
|
|
75401
|
+
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.`);
|
|
75402
|
+
for (const [logicalId] of refreshable.slice(0, 10)) logger.warn(` ${logicalId}`);
|
|
75403
|
+
if (refreshable.length > 10) logger.warn(` ... and ${refreshable.length - 10} more`);
|
|
75404
|
+
}
|
|
75405
|
+
if (refused.length > 0) {
|
|
75406
|
+
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.`);
|
|
75407
|
+
for (const [logicalId] of refused.slice(0, 10)) logger.warn(` ${logicalId}`);
|
|
75408
|
+
if (refused.length > 10) logger.warn(` ... and ${refused.length - 10} more`);
|
|
75409
|
+
}
|
|
75021
75410
|
}
|
|
75022
75411
|
/**
|
|
75023
75412
|
* Walk every stack other than `exportingStackName` looking for
|
|
@@ -76374,10 +76763,10 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
76374
76763
|
if (options.long) {
|
|
76375
76764
|
const lines = [];
|
|
76376
76765
|
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
|
|
76766
|
+
lines.push(stripControlChars(detail.logicalId));
|
|
76767
|
+
lines.push(` Type: ${formatAttributeValue(detail.resourceType)}`);
|
|
76768
|
+
lines.push(` PhysicalID: ${formatAttributeValue(detail.physicalId)}`);
|
|
76769
|
+
lines.push(` Dependencies: ${formatDependencyList(detail.dependencies)}`);
|
|
76381
76770
|
const attrEntries = Object.entries(detail.attributes);
|
|
76382
76771
|
if (attrEntries.length === 0) lines.push(" Attributes: (none)");
|
|
76383
76772
|
else {
|
|
@@ -76390,15 +76779,27 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
76390
76779
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
76391
76780
|
return;
|
|
76392
76781
|
}
|
|
76393
|
-
const
|
|
76394
|
-
|
|
76395
|
-
|
|
76782
|
+
const rows = details.map((d) => ({
|
|
76783
|
+
logicalId: stripControlChars(d.logicalId),
|
|
76784
|
+
resourceType: formatAttributeValue(d.resourceType),
|
|
76785
|
+
physicalId: formatAttributeValue(d.physicalId)
|
|
76786
|
+
}));
|
|
76787
|
+
const idWidth = Math.max(...rows.map((r) => r.logicalId.length));
|
|
76788
|
+
const typeWidth = Math.max(...rows.map((r) => r.resourceType.length));
|
|
76789
|
+
for (const row of rows) process.stdout.write(`${row.logicalId.padEnd(idWidth)} ${row.resourceType.padEnd(typeWidth)} ${row.physicalId}\n`);
|
|
76396
76790
|
} finally {
|
|
76397
76791
|
setup.dispose();
|
|
76398
76792
|
}
|
|
76399
76793
|
}
|
|
76400
76794
|
/**
|
|
76401
|
-
* Render
|
|
76795
|
+
* Render one record-derived value into a human row.
|
|
76796
|
+
*
|
|
76797
|
+
* Named for its first caller and used well beyond an attribute now: a state or
|
|
76798
|
+
* lock record is read as an unchecked cast, so a field declared `string` can
|
|
76799
|
+
* hold anything, and the bare strip throws on it and empties the whole render.
|
|
76800
|
+
* This survives that, and for an actual string it is byte-identical to the
|
|
76801
|
+
* strip — which is why it is the default choice for a record FIELD, and the bare
|
|
76802
|
+
* strip is kept only where the value is a string by construction.
|
|
76402
76803
|
*
|
|
76403
76804
|
* Scalar values render as-is; objects/arrays are JSON-encoded inline so a
|
|
76404
76805
|
* resource block stays compact even when an attribute is structured.
|
|
@@ -76417,16 +76818,24 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
76417
76818
|
* pretty-prints — `JSON.stringify` is called without an indent, so a newline
|
|
76418
76819
|
* here is content injected by the value, not layout.
|
|
76419
76820
|
*
|
|
76420
|
-
* Only the human path routes through this
|
|
76421
|
-
*
|
|
76422
|
-
* `
|
|
76821
|
+
* Only the human path routes through this, which is the same split
|
|
76822
|
+
* `renderOutputChangeLines` makes. `--json` is the OTHER side of it rather than
|
|
76823
|
+
* a byte-faithful echo: `state show --json` reserializes the parsed record and
|
|
76824
|
+
* the lock it prints was sanitised upstream, and `state resources --json` emits
|
|
76825
|
+
* a constructed summary with defaults filled in. What `--json` does not do is
|
|
76826
|
+
* pass a value through THIS function.
|
|
76423
76827
|
*/
|
|
76424
76828
|
function formatAttributeValue(value) {
|
|
76425
76829
|
if (value === null) return "null";
|
|
76426
76830
|
if (value === void 0) return "undefined";
|
|
76427
76831
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return stripControlChars(String(value));
|
|
76428
|
-
|
|
76429
|
-
|
|
76832
|
+
let json;
|
|
76833
|
+
try {
|
|
76834
|
+
json = JSON.stringify(value);
|
|
76835
|
+
} catch {
|
|
76836
|
+
json = void 0;
|
|
76837
|
+
}
|
|
76838
|
+
return json === void 0 ? UNSERIALIZABLE : stripControlChars(json);
|
|
76430
76839
|
}
|
|
76431
76840
|
/**
|
|
76432
76841
|
* Render a duration in milliseconds as `1m23s` / `45s`.
|
|
@@ -76437,7 +76846,65 @@ function formatDuration(ms) {
|
|
|
76437
76846
|
return `${Math.floor(seconds / 60)}m${seconds % 60}s`;
|
|
76438
76847
|
}
|
|
76439
76848
|
/**
|
|
76849
|
+
* Render a whole `Dependencies` row from a list that is an unchecked cast.
|
|
76850
|
+
*
|
|
76851
|
+
* It owns the `(none)` case as well as the values, so that no caller touches
|
|
76852
|
+
* `.length` on a value whose type it cannot trust — reading `.length` is itself
|
|
76853
|
+
* a throw site, and `"A".length > 0` would pass an emptiness test and then
|
|
76854
|
+
* throw inside `join`, which is a method of an ARRAY. So the test is
|
|
76855
|
+
* array-ness. Elements are rendered ONE AT A TIME through the field guard
|
|
76856
|
+
* rather than joined by it: an element that throws on STRING COERCION
|
|
76857
|
+
* (`{"toString": null}` is valid JSON, and `JSON.stringify` handles it fine)
|
|
76858
|
+
* throws INSIDE `join`, where no guard wrapping the result can catch it.
|
|
76859
|
+
*
|
|
76860
|
+
* ABSENT and `null` part company here, deliberately. An absent field is a
|
|
76861
|
+
* record saying nothing, which `(none)` states correctly; a `null` is a record
|
|
76862
|
+
* saying `null`, and printing `(none)` over it would claim something the record
|
|
76863
|
+
* does not. JSON can express `null` here and a hand-edited record can carry it,
|
|
76864
|
+
* so the difference is worth the surprise of seeing `Dependencies: null`.
|
|
76865
|
+
*/
|
|
76866
|
+
function formatDependencyList(deps) {
|
|
76867
|
+
if (Array.isArray(deps)) return deps.length > 0 ? deps.map((dep) => formatAttributeValue(dep)).join(", ") : "(none)";
|
|
76868
|
+
return deps === void 0 ? "(none)" : formatAttributeValue(deps);
|
|
76869
|
+
}
|
|
76870
|
+
/**
|
|
76871
|
+
* Render `Last Modified` from a record whose `lastModified` is an unchecked cast.
|
|
76872
|
+
*
|
|
76873
|
+
* `new Date(x).toISOString()` throws a RangeError on a value it cannot date —
|
|
76874
|
+
* an out-of-range number, most non-numeric strings — and that empties the whole
|
|
76875
|
+
* render, the failure `formatAttributeValue` exists to stop everywhere else in
|
|
76876
|
+
* this block. `new Date` ITSELF throws a TypeError on an object whose
|
|
76877
|
+
* `toString` is unusable (`{"toString": null}` is valid JSON), which is why the
|
|
76878
|
+
* construction is inside the `try` and not only the formatting.
|
|
76879
|
+
*
|
|
76880
|
+
* Only the throw is guarded: the value still reaches `new Date` as-is, so every
|
|
76881
|
+
* input the previous line already dated renders the same ISO string — an ISO or
|
|
76882
|
+
* `YYYY-MM-DD` string, `null` and a boolean included. Anything else falls
|
|
76883
|
+
* through to the guard the other fields take.
|
|
76884
|
+
*/
|
|
76885
|
+
function formatLastModified(value) {
|
|
76886
|
+
try {
|
|
76887
|
+
return new Date(value).toISOString();
|
|
76888
|
+
} catch {
|
|
76889
|
+
return formatAttributeValue(value);
|
|
76890
|
+
}
|
|
76891
|
+
}
|
|
76892
|
+
/**
|
|
76440
76893
|
* Render lock metadata for the `state show` block.
|
|
76894
|
+
*
|
|
76895
|
+
* This row takes NO display guard — `Version` is the other, for its own reason —
|
|
76896
|
+
* and here the reason is upstream: `LockManager.getLockRecord` already passes
|
|
76897
|
+
* `owner` and `operation` through `displaySafe`, which coerces and replaces the
|
|
76898
|
+
* whole control class with spaces. They arrive here as control-free strings, so
|
|
76899
|
+
* a guard would be redundant — and a test for one could only be written by
|
|
76900
|
+
* mocking the read that sanitises them, which would pin nothing about the real
|
|
76901
|
+
* path. `displaySafe`'s own `String(value)` is unguarded, so a value that throws
|
|
76902
|
+
* on coercion fails THERE, before this function; that is go-to-k/cdkd#2947 and
|
|
76903
|
+
* not something a guard here could catch either.
|
|
76904
|
+
*
|
|
76905
|
+
* `expiresAt` is declared a number and is not guaranteed to be one, but it
|
|
76906
|
+
* reaches the row only through subtraction and `formatDuration`, so no character
|
|
76907
|
+
* the record carries can survive into the output either.
|
|
76441
76908
|
*/
|
|
76442
76909
|
function formatLockSummary(lockInfo) {
|
|
76443
76910
|
if (!lockInfo) return "unlocked";
|
|
@@ -76459,8 +76926,8 @@ function createStateResourcesCommand() {
|
|
|
76459
76926
|
* `cdkd state show <stack>` command implementation
|
|
76460
76927
|
*
|
|
76461
76928
|
* 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
|
|
76929
|
+
* status, outputs, skipped outputs, and every resource (including properties).
|
|
76930
|
+
* The deepest / most verbose `state` subcommand — use `state list` / `state resources` for
|
|
76464
76931
|
* lighter inspection.
|
|
76465
76932
|
*
|
|
76466
76933
|
* When the same stack name has state in multiple regions, requires
|
|
@@ -76473,8 +76940,9 @@ function createStateResourcesCommand() {
|
|
|
76473
76940
|
* - `--show-nested` (issue #555 A4): when the stack contains
|
|
76474
76941
|
* `AWS::CloudFormation::Stack` rows, recursively load every child state
|
|
76475
76942
|
* record (`cdkd/<parent>~<childLogicalId>/<region>/state.json`) and append
|
|
76476
|
-
* its block after the parent's.
|
|
76477
|
-
*
|
|
76943
|
+
* its block after the parent's. With no nested children the rendered
|
|
76944
|
+
* blocks are the same, though a `Skipped outputs:` explanation moves to
|
|
76945
|
+
* the end of the tree.
|
|
76478
76946
|
*/
|
|
76479
76947
|
async function stateShowCommand(stackName, options) {
|
|
76480
76948
|
const logger = getLogger();
|
|
@@ -76503,27 +76971,82 @@ async function stateShowCommand(stackName, options) {
|
|
|
76503
76971
|
}, null, 2)}\n`);
|
|
76504
76972
|
return;
|
|
76505
76973
|
}
|
|
76506
|
-
process.stdout.write(`${renderStateBlock(stateResult.state, lockInfo).join("\n")}\n`);
|
|
76974
|
+
process.stdout.write(`${renderStateBlock(stateResult.state, lockInfo, true).join("\n")}\n`);
|
|
76507
76975
|
} finally {
|
|
76508
76976
|
setup.dispose();
|
|
76509
76977
|
}
|
|
76510
76978
|
}
|
|
76511
76979
|
/**
|
|
76980
|
+
* How much of a `skippedOutputs` digest the text view shows. A prefix
|
|
76981
|
+
* DIFFERENCE proves two records differ, which is what a reader wants at a
|
|
76982
|
+
* glance; a prefix MATCH proves nothing, so an exact comparison belongs to
|
|
76983
|
+
* `--json`, which carries the digest whole.
|
|
76984
|
+
*/
|
|
76985
|
+
const SKIPPED_DIGEST_PREVIEW_LEN = 12;
|
|
76986
|
+
/** What `formatAttributeValue` prints for a value `JSON.stringify` refuses. */
|
|
76987
|
+
const UNSERIALIZABLE = "(unserializable)";
|
|
76988
|
+
/**
|
|
76989
|
+
* The `skippedOutputs` rows a state record owes, sorted by key.
|
|
76990
|
+
*/
|
|
76991
|
+
function sortedSkippedOutputs(state) {
|
|
76992
|
+
return Object.entries(state.skippedOutputs ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
76993
|
+
}
|
|
76994
|
+
/** Whether one record renders the block. The ONE copy of that condition. */
|
|
76995
|
+
function rendersSkippedBlock(state) {
|
|
76996
|
+
return Object.keys(state.skippedOutputs ?? {}).length > 0;
|
|
76997
|
+
}
|
|
76998
|
+
/**
|
|
76999
|
+
* The explanation printed under a `Skipped outputs:` block.
|
|
77000
|
+
*
|
|
77001
|
+
* At column ZERO, unlike the rows it follows. At the rows' two-space indent a
|
|
77002
|
+
* reader — or a `grep '^ '` — takes these lines for another key. It also
|
|
77003
|
+
* quotes `Outputs:` mid-sentence, so a test matching that as a SUBSTRING has
|
|
77004
|
+
* to anchor on the section's own line to mean anything.
|
|
77005
|
+
*
|
|
77006
|
+
* Emitted ONCE per render rather than per block: under `--show-nested` the
|
|
77007
|
+
* same explanation would otherwise repeat for every child that skipped
|
|
77008
|
+
* anything.
|
|
77009
|
+
*
|
|
77010
|
+
* That placement is why the opening sentence NAMES its referent rather than
|
|
77011
|
+
* saying "these". At the end of a tree render it follows every rendered stack
|
|
77012
|
+
* block, so it is not adjacent to the block it explains and a bare
|
|
77013
|
+
* demonstrative reads as belonging to whichever stack printed last. The
|
|
77014
|
+
* single-stack path still has the adjacency; the wording holds in both.
|
|
77015
|
+
*
|
|
77016
|
+
* It does not claim `Outputs:` is on the screen. That block is omitted when
|
|
77017
|
+
* the bag is empty, which includes the first-deploy case this record exists
|
|
77018
|
+
* for — every output failed, so `outputs` is `{}` while `skippedOutputs` is
|
|
77019
|
+
* populated.
|
|
77020
|
+
*/
|
|
77021
|
+
function skippedOutputsLegend() {
|
|
77022
|
+
return [
|
|
77023
|
+
"",
|
|
77024
|
+
"The last deploy could not resolve the keys listed under `Skipped outputs:`",
|
|
77025
|
+
"above, and recorded a digest of their template inputs. While the record",
|
|
77026
|
+
"binds and a key is still absent from the stored outputs, `cdkd diff`",
|
|
77027
|
+
"previews it as ABSENT — no row, no warning. A key whose earlier value",
|
|
77028
|
+
"was retained is also stored under `Outputs:` here, where the record does",
|
|
77029
|
+
"NOT suppress it: the ordinary rules apply, up to `cdkd diff` suppressing",
|
|
77030
|
+
"its whole Outputs section if the key still cannot resolve.",
|
|
77031
|
+
"Binding rule: `bindingSkippedOutputs` in src/analyzer/skipped-outputs.ts."
|
|
77032
|
+
];
|
|
77033
|
+
}
|
|
77034
|
+
/**
|
|
76512
77035
|
* Render one stack's state record as a human-readable multi-line block —
|
|
76513
77036
|
* the shared body used by both the single-stack default output and each
|
|
76514
77037
|
* nested child rendered under `--show-nested`.
|
|
76515
77038
|
*/
|
|
76516
|
-
function renderStateBlock(state, lockInfo) {
|
|
77039
|
+
function renderStateBlock(state, lockInfo, withLegend) {
|
|
76517
77040
|
const lines = [];
|
|
76518
|
-
lines.push(`Stack: ${state.stackName}`);
|
|
76519
|
-
if (state.region) lines.push(` Region: ${state.region}`);
|
|
77041
|
+
lines.push(`Stack: ${formatAttributeValue(state.stackName)}`);
|
|
77042
|
+
if (state.region) lines.push(` Region: ${formatAttributeValue(state.region)}`);
|
|
76520
77043
|
lines.push(` Version: ${state.version}`);
|
|
76521
|
-
lines.push(` Last Modified: ${
|
|
77044
|
+
lines.push(` Last Modified: ${formatLastModified(state.lastModified)}`);
|
|
76522
77045
|
lines.push(` Lock: ${formatLockSummary(lockInfo)}`);
|
|
76523
77046
|
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}`);
|
|
77047
|
+
const parentRegionStr = state.parentRegion ? ` (${formatAttributeValue(state.parentRegion)})` : "";
|
|
77048
|
+
const logicalIdStr = state.parentLogicalId !== void 0 ? `, logical id: ${formatAttributeValue(state.parentLogicalId)}` : "";
|
|
77049
|
+
lines.push(` Parent: ${formatAttributeValue(state.parentStack)}${parentRegionStr}${logicalIdStr}`);
|
|
76527
77050
|
}
|
|
76528
77051
|
const outputEntries = Object.entries(state.outputs ?? {});
|
|
76529
77052
|
if (outputEntries.length > 0) {
|
|
@@ -76531,18 +77054,28 @@ function renderStateBlock(state, lockInfo) {
|
|
|
76531
77054
|
lines.push("Outputs:");
|
|
76532
77055
|
for (const [k, v] of outputEntries) lines.push(` ${stripControlChars(k)}: ${formatAttributeValue(v)}`);
|
|
76533
77056
|
}
|
|
77057
|
+
if (rendersSkippedBlock(state)) {
|
|
77058
|
+
lines.push("");
|
|
77059
|
+
lines.push("Skipped outputs:");
|
|
77060
|
+
for (const [k, digest] of sortedSkippedOutputs(state)) {
|
|
77061
|
+
const fullDigest = formatAttributeValue(digest);
|
|
77062
|
+
const shownDigest = fullDigest.length > SKIPPED_DIGEST_PREVIEW_LEN && fullDigest !== UNSERIALIZABLE ? `${fullDigest.slice(0, SKIPPED_DIGEST_PREVIEW_LEN)}…` : fullDigest;
|
|
77063
|
+
lines.push(` ${stripControlChars(k)}: ${shownDigest}`);
|
|
77064
|
+
}
|
|
77065
|
+
if (withLegend) lines.push(...skippedOutputsLegend());
|
|
77066
|
+
}
|
|
76534
77067
|
const resourceEntries = Object.entries(state.resources ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
76535
77068
|
lines.push("");
|
|
76536
77069
|
lines.push(`Resources (${resourceEntries.length}):`);
|
|
76537
77070
|
for (const [logicalId, resource] of resourceEntries) {
|
|
76538
77071
|
lines.push("");
|
|
76539
|
-
lines.push(logicalId);
|
|
76540
|
-
lines.push(` Type: ${resource.resourceType}`);
|
|
76541
|
-
lines.push(` PhysicalID: ${
|
|
77072
|
+
lines.push(stripControlChars(logicalId));
|
|
77073
|
+
lines.push(` Type: ${formatAttributeValue(resource.resourceType)}`);
|
|
77074
|
+
lines.push(` PhysicalID: ${formatAttributeValue(resource.physicalId)}`);
|
|
76542
77075
|
const provisionedBy = resource.provisionedBy ?? "(sdk, legacy default)";
|
|
76543
|
-
lines.push(` ProvisionedBy: ${provisionedBy}`);
|
|
76544
|
-
|
|
76545
|
-
lines.push(` Dependencies: ${
|
|
77076
|
+
lines.push(` ProvisionedBy: ${formatAttributeValue(provisionedBy)}`);
|
|
77077
|
+
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)");
|
|
77078
|
+
lines.push(` Dependencies: ${formatDependencyList(resource.dependencies)}`);
|
|
76546
77079
|
const attrEntries = Object.entries(resource.attributes ?? {});
|
|
76547
77080
|
if (attrEntries.length === 0) lines.push(" Attributes: (none)");
|
|
76548
77081
|
else {
|
|
@@ -76598,6 +77131,14 @@ async function loadLocksForTree(tree, lockManager, rootLock) {
|
|
|
76598
77131
|
return decorate(tree);
|
|
76599
77132
|
}
|
|
76600
77133
|
/**
|
|
77134
|
+
* Whether any node in this tree owes a `Skipped outputs:` block, and so
|
|
77135
|
+
* whether the tree render owes the legend once at its end. Asked through
|
|
77136
|
+
* {@link rendersSkippedBlock}, the same predicate the block's own guard uses.
|
|
77137
|
+
*/
|
|
77138
|
+
function treeOwesSkippedLegend(node) {
|
|
77139
|
+
return rendersSkippedBlock(node.state) || node.children.some(treeOwesSkippedLegend);
|
|
77140
|
+
}
|
|
77141
|
+
/**
|
|
76601
77142
|
* Render the parent's block followed by every descendant in DFS order,
|
|
76602
77143
|
* each separated by a blank line and prefixed with a `Nested stack: ...`
|
|
76603
77144
|
* header so the user can scan the tree top-down in one screen.
|
|
@@ -76608,15 +77149,16 @@ async function loadLocksForTree(tree, lockManager, rootLock) {
|
|
|
76608
77149
|
*/
|
|
76609
77150
|
function renderTreeWithChildren(root) {
|
|
76610
77151
|
const lines = [];
|
|
76611
|
-
lines.push(...renderStateBlock(root.state, root.lock));
|
|
77152
|
+
lines.push(...renderStateBlock(root.state, root.lock, false));
|
|
76612
77153
|
appendDescendants(root.children, lines);
|
|
77154
|
+
if (treeOwesSkippedLegend(root)) lines.push(...skippedOutputsLegend());
|
|
76613
77155
|
return lines;
|
|
76614
77156
|
}
|
|
76615
77157
|
function appendDescendants(children, out) {
|
|
76616
77158
|
for (const child of children) {
|
|
76617
77159
|
out.push("");
|
|
76618
|
-
out.push(`Nested stack: ${child.stackName}`);
|
|
76619
|
-
out.push(...renderStateBlock(child.state, child.lock));
|
|
77160
|
+
out.push(`Nested stack: ${stripControlChars(child.stackName)}`);
|
|
77161
|
+
out.push(...renderStateBlock(child.state, child.lock, false));
|
|
76620
77162
|
appendDescendants(child.children, out);
|
|
76621
77163
|
}
|
|
76622
77164
|
}
|
|
@@ -77268,6 +77810,7 @@ async function stateRefreshObservedCommand(stackArgs, options) {
|
|
|
77268
77810
|
let totalRefreshed = 0;
|
|
77269
77811
|
let totalUnsupported = 0;
|
|
77270
77812
|
let totalFailed = 0;
|
|
77813
|
+
let totalRefusedBaseline = 0;
|
|
77271
77814
|
for (const target of targets) {
|
|
77272
77815
|
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
77816
|
const counts = await refreshObservedForStack(target.stackName, target.region, setup.stateBackend, setup.lockManager, providerRegistry, {
|
|
@@ -77282,9 +77825,11 @@ async function stateRefreshObservedCommand(stackArgs, options) {
|
|
|
77282
77825
|
totalRefreshed += counts.refreshed;
|
|
77283
77826
|
totalUnsupported += counts.unsupported;
|
|
77284
77827
|
totalFailed += counts.failed;
|
|
77828
|
+
totalRefusedBaseline += counts.refusedBaseline;
|
|
77285
77829
|
}
|
|
77286
77830
|
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
77831
|
logger.info(`\n${summary}`);
|
|
77832
|
+
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
77833
|
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
77834
|
} finally {
|
|
77290
77835
|
setup.dispose();
|
|
@@ -77351,13 +77896,19 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
77351
77896
|
return {
|
|
77352
77897
|
refreshed: 0,
|
|
77353
77898
|
unsupported: 0,
|
|
77354
|
-
failed: 0
|
|
77899
|
+
failed: 0,
|
|
77900
|
+
refusedBaseline: 0
|
|
77355
77901
|
};
|
|
77356
77902
|
}
|
|
77357
77903
|
if (opts.dryRun) {
|
|
77358
77904
|
let wouldRefresh = 0;
|
|
77359
77905
|
let wouldUnsupported = 0;
|
|
77906
|
+
let wouldRefuse = 0;
|
|
77360
77907
|
for (const [, resource] of entries) {
|
|
77908
|
+
if (resource.observedBaselineRefused === true) {
|
|
77909
|
+
wouldRefuse++;
|
|
77910
|
+
continue;
|
|
77911
|
+
}
|
|
77361
77912
|
let provider;
|
|
77362
77913
|
try {
|
|
77363
77914
|
provider = providerRegistry.getProviderFor({
|
|
@@ -77371,11 +77922,12 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
77371
77922
|
if (provider.readCurrentState) wouldRefresh++;
|
|
77372
77923
|
else wouldUnsupported++;
|
|
77373
77924
|
}
|
|
77374
|
-
logger.info(`Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported`);
|
|
77925
|
+
logger.info(`Plan ${stackName} (${region}): ${wouldRefresh} resource(s) would be refreshed, ${wouldUnsupported} unsupported` + (wouldRefuse > 0 ? `, ${wouldRefuse} refused (import baseline refusal)` : ""));
|
|
77375
77926
|
return {
|
|
77376
77927
|
refreshed: wouldRefresh,
|
|
77377
77928
|
unsupported: wouldUnsupported,
|
|
77378
|
-
failed: 0
|
|
77929
|
+
failed: 0,
|
|
77930
|
+
refusedBaseline: wouldRefuse
|
|
77379
77931
|
};
|
|
77380
77932
|
}
|
|
77381
77933
|
const owner = `${process.env["USER"] || "unknown"}@${process.env["HOSTNAME"] || "host"}:${process.pid}`;
|
|
@@ -77389,8 +77941,13 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
77389
77941
|
let refreshed = 0;
|
|
77390
77942
|
let unsupported = 0;
|
|
77391
77943
|
let failed = 0;
|
|
77944
|
+
let refusedBaseline = 0;
|
|
77392
77945
|
await withStackName(stackName, async () => {
|
|
77393
77946
|
const tasks = entries.map(async ([logicalId, resource]) => {
|
|
77947
|
+
if (resource.observedBaselineRefused === true) {
|
|
77948
|
+
refusedBaseline++;
|
|
77949
|
+
return;
|
|
77950
|
+
}
|
|
77394
77951
|
if (providerRegistry.shouldSkipResource(resource.resourceType)) {
|
|
77395
77952
|
unsupported++;
|
|
77396
77953
|
return;
|
|
@@ -77429,11 +77986,12 @@ async function refreshObservedForStack(stackName, region, stateBackend, lockMana
|
|
|
77429
77986
|
const saveOptions = { expectedEtag: etag };
|
|
77430
77987
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
77431
77988
|
await stateBackend.saveState(stackName, region, state, saveOptions);
|
|
77432
|
-
logger.info(`✓ ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed`);
|
|
77989
|
+
logger.info(`✓ ${stackName} (${region}): ${refreshed} refreshed, ${unsupported} unsupported, ${failed} failed` + (refusedBaseline > 0 ? `, ${refusedBaseline} refused (import baseline refusal)` : ""));
|
|
77433
77990
|
return {
|
|
77434
77991
|
refreshed,
|
|
77435
77992
|
unsupported,
|
|
77436
|
-
failed
|
|
77993
|
+
failed,
|
|
77994
|
+
refusedBaseline
|
|
77437
77995
|
};
|
|
77438
77996
|
} finally {
|
|
77439
77997
|
await lockManager.releaseLock(stackName, region).catch((err) => {
|
|
@@ -77674,6 +78232,7 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77674
78232
|
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
78233
|
const baseState = stateData.state;
|
|
77676
78234
|
const stateResources = { ...baseState.resources };
|
|
78235
|
+
const mintedOrphans = [];
|
|
77677
78236
|
const orphanLogicalIds = new Set(options.orphan ?? []);
|
|
77678
78237
|
const newestSegment = journal.segments[journal.segments.length - 1];
|
|
77679
78238
|
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 +78279,10 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77720
78279
|
const { skippedOutputs: _droppedByRollback, ...carriedState } = baseState;
|
|
77721
78280
|
const next = () => ({
|
|
77722
78281
|
...carriedState,
|
|
77723
|
-
version:
|
|
78282
|
+
version: 10,
|
|
77724
78283
|
region,
|
|
77725
78284
|
resources: { ...stateResources },
|
|
78285
|
+
...orphansAfterRollback(baseState, mintedOrphans),
|
|
77726
78286
|
lastModified: Date.now()
|
|
77727
78287
|
});
|
|
77728
78288
|
try {
|
|
@@ -77764,7 +78324,8 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77764
78324
|
const failedResult = await replayFailedOperations(segment.failedOperations, stateResources, stackName, ctx, {
|
|
77765
78325
|
afterOp: saveState,
|
|
77766
78326
|
isInterrupted: () => interrupted,
|
|
77767
|
-
emitEnvelope: segment.operations.length === 0
|
|
78327
|
+
emitEnvelope: segment.operations.length === 0,
|
|
78328
|
+
onOrphan: (record) => mintedOrphans.push(record)
|
|
77768
78329
|
});
|
|
77769
78330
|
failedOpFailures = failedResult.failures;
|
|
77770
78331
|
failedOpWarnings = failedResult.warnings;
|
|
@@ -77785,7 +78346,8 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77785
78346
|
const replayResult = await replayRollback(segment.operations, stateResources, stackName, ctx, {
|
|
77786
78347
|
orphanLogicalIds,
|
|
77787
78348
|
afterOp: saveState,
|
|
77788
|
-
isInterrupted: () => interrupted
|
|
78349
|
+
isInterrupted: () => interrupted,
|
|
78350
|
+
onOrphan: (record) => mintedOrphans.push(record)
|
|
77789
78351
|
});
|
|
77790
78352
|
return {
|
|
77791
78353
|
failures: replayResult.failures + failedOpFailures,
|
|
@@ -77806,7 +78368,8 @@ async function rollbackCommand(stackArg, options) {
|
|
|
77806
78368
|
} finally {
|
|
77807
78369
|
await eventRecorder.finalize(totalFailures > 0 || interrupted ? "FAILED" : "SUCCEEDED");
|
|
77808
78370
|
}
|
|
77809
|
-
|
|
78371
|
+
const survivingOrphans = orphansAfterRollback(baseState, mintedOrphans).orphans ?? [];
|
|
78372
|
+
if (journal.segments.length === 0 && oldestInitialDeploy && Object.keys(stateResources).length === 0 && survivingOrphans.length === 0) {
|
|
77810
78373
|
await setup.stateBackend.deleteState(stackName, region);
|
|
77811
78374
|
logger.info(`State for '${stackName}' (${region}) removed (stack fully rolled back).`);
|
|
77812
78375
|
}
|
|
@@ -80793,7 +81356,7 @@ async function importCommand(stackArg, options) {
|
|
|
80793
81356
|
}
|
|
80794
81357
|
}
|
|
80795
81358
|
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)));
|
|
81359
|
+
await captureObservedForImportedResources(stackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger)), rebuiltLogicalIdsFrom(rows, stateTemplate));
|
|
80797
81360
|
const saveOptions = {};
|
|
80798
81361
|
if (existingEtag) saveOptions.expectedEtag = existingEtag;
|
|
80799
81362
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
@@ -81109,6 +81672,30 @@ function collectImportableResources(template) {
|
|
|
81109
81672
|
return out;
|
|
81110
81673
|
}
|
|
81111
81674
|
/**
|
|
81675
|
+
* The logical ids whose `ResourceState` a run REBUILT from the template — the
|
|
81676
|
+
* discriminator `captureObservedForImportedResources` needs for schema v10's
|
|
81677
|
+
* `observedBaselineRefused` (issue
|
|
81678
|
+
* [#2944](https://github.com/go-to-k/cdkd/issues/2944)).
|
|
81679
|
+
*
|
|
81680
|
+
* It is a function, exported and tested, rather than an inline
|
|
81681
|
+
* `rows.filter(...)` at each call site, because the predicate is the whole
|
|
81682
|
+
* safety property: `buildStackState` overwrites exactly the `'imported'` rows
|
|
81683
|
+
* and PRESERVES every other record in a selective merge, so a predicate that
|
|
81684
|
+
* drifts wider (`rows.map(...)`, or admitting another outcome) silently
|
|
81685
|
+
* re-opens the fifth-writer leak — a preserved record's stale, distrusted
|
|
81686
|
+
* `properties` would be treated as this run's resolution, captured against,
|
|
81687
|
+
* and its refusal cleared. Two inline copies could also drift apart from each
|
|
81688
|
+
* other and from `buildStackState`'s own test.
|
|
81689
|
+
*
|
|
81690
|
+
* Every non-`'imported'` outcome is EXCLUDED, including the ones that sound
|
|
81691
|
+
* harmless: `skipped-out-of-scope` is precisely the selective-merge case, and
|
|
81692
|
+
* `failed` / `skipped-not-found` / `skipped-no-impl` leave whatever the record
|
|
81693
|
+
* already held.
|
|
81694
|
+
*/
|
|
81695
|
+
function rebuiltLogicalIdsFrom(rows, template) {
|
|
81696
|
+
return new Set(rows.filter((r) => r.outcome === "imported" && r.physicalId !== void 0 && template.Resources[r.logicalId] !== void 0).map((r) => r.logicalId));
|
|
81697
|
+
}
|
|
81698
|
+
/**
|
|
81112
81699
|
* Compose a `StackState` from the per-resource import outcomes plus
|
|
81113
81700
|
* dependency info recovered from the template.
|
|
81114
81701
|
*
|
|
@@ -81148,12 +81735,13 @@ function buildStackState(stackName, region, rows, templateParser, template, exis
|
|
|
81148
81735
|
};
|
|
81149
81736
|
}
|
|
81150
81737
|
return {
|
|
81151
|
-
version:
|
|
81738
|
+
version: 10,
|
|
81152
81739
|
stackName,
|
|
81153
81740
|
region,
|
|
81154
81741
|
resources,
|
|
81155
81742
|
outputs: existingState?.outputs ?? {},
|
|
81156
81743
|
...existingState ? exportNamesCarriedFrom(existingState) : { exportNames: [] },
|
|
81744
|
+
...existingState ? orphansCarriedFrom(existingState) : {},
|
|
81157
81745
|
lastModified: Date.now()
|
|
81158
81746
|
};
|
|
81159
81747
|
}
|
|
@@ -81741,14 +82329,20 @@ const NO_RECORDED_SECRETS = /* @__PURE__ */ new Map();
|
|
|
81741
82329
|
* call site's own note (issue
|
|
81742
82330
|
* [#2828](https://github.com/go-to-k/cdkd/issues/2828)).
|
|
81743
82331
|
*/
|
|
81744
|
-
async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds) {
|
|
82332
|
+
async function captureObservedForImportedResources(stackState, providerRegistry, logger, unsafeObservedBaselineLogicalIds, rebuiltLogicalIds) {
|
|
81745
82333
|
const entries = Object.entries(stackState.resources);
|
|
81746
82334
|
if (entries.length === 0) return;
|
|
81747
82335
|
await Promise.all(entries.map(async ([logicalId, resource]) => {
|
|
82336
|
+
if (resource.observedBaselineRefused === true && !rebuiltLogicalIds.has(logicalId)) {
|
|
82337
|
+
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.`);
|
|
82338
|
+
return;
|
|
82339
|
+
}
|
|
81748
82340
|
if (unsafeObservedBaselineLogicalIds.has(logicalId)) {
|
|
82341
|
+
resource.observedBaselineRefused = true;
|
|
81749
82342
|
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
82343
|
return;
|
|
81751
82344
|
}
|
|
82345
|
+
delete resource.observedBaselineRefused;
|
|
81752
82346
|
try {
|
|
81753
82347
|
const provider = providerRegistry.getProviderFor({
|
|
81754
82348
|
resourceType: resource.resourceType,
|
|
@@ -81928,7 +82522,7 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
81928
82522
|
childStackState.parentStack = parentStackName;
|
|
81929
82523
|
childStackState.parentLogicalId = childLogicalId;
|
|
81930
82524
|
childStackState.parentRegion = parentRegion;
|
|
81931
|
-
await captureObservedForImportedResources(childStackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger)));
|
|
82525
|
+
await captureObservedForImportedResources(childStackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger)), rebuiltLogicalIdsFrom(rows, childTemplate));
|
|
81932
82526
|
await stateBackend.saveState(childStackName, childRegion, childStackState);
|
|
81933
82527
|
logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
|
|
81934
82528
|
if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
|
|
@@ -91159,4 +91753,4 @@ function buildProgram() {
|
|
|
91159
91753
|
|
|
91160
91754
|
//#endregion
|
|
91161
91755
|
export { buildProgram };
|
|
91162
|
-
//# sourceMappingURL=program-
|
|
91756
|
+
//# sourceMappingURL=program-CIn_e9xI.js.map
|