@go-to-k/cdkd 0.290.6 → 0.290.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{asg-provider-CsdVn4cg.js → asg-provider-B4m3magB.js} +3 -3
- package/dist/{asg-provider-CsdVn4cg.js.map → asg-provider-B4m3magB.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-BU7n9_qY.js → deploy-engine-Bq0TJ9iN.js} +11 -7
- package/dist/{deploy-engine-BU7n9_qY.js.map → deploy-engine-Bq0TJ9iN.js.map} +1 -1
- package/dist/index.js +2 -2
- package/dist/{logger-th1myfgh.js → logger-H8n5Qjhq.js} +162 -38
- package/dist/logger-H8n5Qjhq.js.map +1 -0
- package/dist/{program-Cl3fCD6-.js → program-C7UAuWDp.js} +635 -70
- package/dist/{program-Cl3fCD6-.js.map → program-C7UAuWDp.js.map} +1 -1
- package/dist/{version-BvSdNpMz.js → version-CFdjI1Tq.js} +2 -2
- package/dist/{version-BvSdNpMz.js.map → version-CFdjI1Tq.js.map} +1 -1
- package/package.json +1 -1
- package/dist/logger-th1myfgh.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import {
|
|
1
|
+
import { $ as secretBearing, $n as ensureAssetStorage, $r as setAwsClients, $t as s3BucketRegionalDomainName, A as extractDeploymentEventError, Ai as retryClassificationText, An as buildForceUnlockCommand, Ar as resolveSkipPrefix, At as parameterTypeMayLoseSecretIdentity, B as toCfnInteger, Bn as orphansAfterRollback, Br as uploadCfnTemplate, Bt as configIntegerRefusal, C as buildFinalSnapshotIdentifier, Ci as withErrorHandling, Cn as recordMaskOnlyValue, Cr as Synthesizer, Ct as IntrinsicFunctionResolver, D as refusesFinalSnapshot, Di as isTransientServerError, Dn as LockManager, Dr as resolveApp, Dt as getAccountInfo, E as isFinalSnapshotError, Ei as isThrottlingError, En as scrubResourceRecord, Er as getLegacyStateBucketName, Et as coerceParameterTypedValue, F as isStatefulRecreateTargetSync, Fn as CUSTOM_RESOURCE_RESPONSE_PREFIX, Fr as warnDeprecatedNoPrefixCliFlag, Ft as resolveExplicitPhysicalId, G as gray, Gn as WorkGraph, Gr as derivePartitionAndUrlSuffix, Gt as requireConfigObject, H as formatResourceLine, Hn as shouldRetainResource, Hr as expectedOwnerParam, Ht as readConfigString, I as renderStatefulReason, In as DEFAULT_STATE_PREFIX, Ir as CFN_TEMPLATE_BODY_LIMIT, It as assertRegionMatch, J as yellow, Jn as loadPublishableAssetManifest, Jt as producerRegionsFromState, K as green, Kn as buildAssetRedirectMap, Kr as AssemblyReader, Kt as requireConfigString, L as WARM_THROUGHPUT_MEMBERS, Ln as exportNamesCarriedFrom, Lr as CFN_TEMPLATE_URL_LIMIT, Lt as coerceCfnBoolean, M as planOrphanAdoption, Mn as forceQuitRecoveryClause, Mr as resolveStateBucketWithDefaultAndSource, Mt as isSettledInstanceState, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as formatLockExpiry, Nr as resolveUseCdkBootstrapAssets, Nt as WAFv2WebACLProvider, O as unsupportedFinalSnapshotError, Oi as markNonRetryable, On as S3StateBackend, Or as resolveAutoAssetStorage, Ot as isImpossibleEmptyStoredAttribute, P as isStatefulRecreateTargetForReplace, Pn as shellQuote, Pr as stateBucketExistenceConfirmed, Pt as normalizeAwsTagsToCfn, Q as isExportAliasCollision, Qn as assertAssetBucketRegion, Qr as resetAwsClients, Qt as s3BucketDualStackDomainName, R as coerceWarmThroughput$1, Rn as importableOutputKeys, Rr as MIGRATE_TMP_PREFIX, Rt as coerceCfnInteger, S as PRE_DELETE_SNAPSHOT_TYPES, Si as normalizeAwsError, Sn as maskSecretsInText, Sr as getDockerImageBySourceHash, St as isTerminationProtectionPropagationError, T as createPreDeleteFinalSnapshot, Ti as isRetryableTransientError, Tn as redactSecretsForState, Tr as getDefaultStateBucketName, Tt as cfnRefValueFromPhysicalId, U as bold, Un as AssetPublisher, Ur as PARTITION_TABLE, Ut as replayWarn, V as toFiniteNumber, Vn as orphansCarriedFrom, Vr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Vt as configStringRefusal, W as cyan, Wn as stringifyValue, Wr as canonicalizeRegion, Wt as requireConfigArray, X as collectPublishedOutputNames, Xn as AssetModeResolver, Xr as AwsClients, Xt as s3BucketArn, Y as collectDeclaredOutputNames, Yn as rewriteTemplateAssetReferences, Yr as resolveBucketRegion, Yt as withSharedDrainBudget, Z as exportAliasCollisionScrubWarning, Zn as BOOTSTRAP_MARKER_PREFIX, Zr as getAwsClients, Zt as s3BucketDomainName, _ as isSecretBearingReferenceString, _i as StackTerminationProtectionError, _n as errorCauseChain, _r as runDockerForeground, _t as slowCcOperationTimeoutMs, a as DeploymentEventsStore, ai as DeployCancelledError, an as findSilentDropProperties, ar as validateContainerRepoName, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bn as markSameGenerationBag, br as stripControlChars, bt as deleteSkipReason, c as replayFailedOperations, ci as LocalInvokeBuildError$1, cn as DagBuilder, cr as buildDockerImage, ct as createMaskedRetryLogger, d as updatePartialReason, di as NestedStackChildDirectDestroyError, dn as STATE_SOURCED_CROSS_GENERATION_RULES, dr as describeDockerFailure, dt as beginCommandInterruptScope, en as s3BucketWebsiteUrl, er as getBootstrapMarkerKey, et as secretBearingStateKeyWarning, f as withResourceDeadline, fi as PartialFailureError, fn as STATE_SOURCED_READBACK_RULES, fr as dockerSpawnEnvWithSensitive, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as StackHasActiveImportsError, gn as dynamicReferenceTokens, gr as redactDockerArgvValues, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as ResourceUpdateNotSupportedError, hn as createSecretMasker, hr as partitionSensitiveEnv, ht as startInterruptWatch, i as DeploymentEventsReader, in as findActionableSilentDrops, ir as validateAssetBucketName, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, jn as buildLockContentionMessage, jr as resolveStateBucketWithDefault, jt as refStateLookupFromResource, k as makeCanonicalizePropertiesFn, ki as markRedactedCause, kn as rebuildClientForBucketRegion, kr as resolveCaptureObservedState, kt as isUnboundTemplateParameter, l as replayRollback, li as LocalStartServiceError, ln as TemplateParser, lr as describeDockerCapturedOutput, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as ResourceTimeoutError, mn as carriesSecretMask, mr as getDockerCmd, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as DiffCalculator, nr as parseBootstrapMarker, nt as getCurrentResourceSecrets, o as planFailedOps, oi as DynamicReferenceRegionAmbiguousError, on as describeTypeWithThrottleRetry, or as buildDenyExternalAccessPolicy, ot as ProviderRegistry, p as maskingRetryLogger, pi as ProvisioningError, pn as TEMPLATE_SOURCED_RULES, pr as formatDockerLoginError, pt as interruptWatchListenerCount, q as red, qn as createAssetRedirectResolver, qr as processStackMessages, qt as classifyReplaySecretRegion, r as DeployEngine, ri as CrossAccountSecretRefusalError, rn as INTRINSIC_KEYS, rr as readBootstrapMarkerBody, rt as IAMRoleProvider, s as planRollback, si as IntrinsicResolutionRefusalError, sn as withRetry, sr as describeAwsFailure, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as CdkdError, tn as applyRoleArnIfSet, tr as isCrossRegionRedirect, tt as secretSafeKeyDisplay, u as updatePartialMessage, un as STATE_SOURCED_BASELINE_RULES, ur as describeDockerExecFailure, ut as maskerOrIdentity, v as keptWholeReasonText, vn as identityKeyFor, vr as runDockerStreaming, vt as UNSPECIFIED_SKIP_REASON, w as ccRoutedFinalSnapshotError, wi as isMarkedNonRetryable, wn as recoverMaskedOutput, wr as synthesisStatusMessage, wt as carriesDynamicReference, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xn as maskSecretsInError, xr as AssetManifestLoader, xt as disableInstanceApiTermination, y as mergeNoChangeOutputs, yn as isSingleDynamicReferenceToken, yr as escapeRegExp, yt as deleteIndeterminateGuards, z as isWarmThroughputDecrease, zn as importableOutputs, zr as findLargeInlineResources, zt as configBooleanRefusal } from "./deploy-engine-Bq0TJ9iN.js";
|
|
2
|
+
import { C as truncateCodePoints, S as displaySafe, _ as withSkipPrefix, b as UNRENDERABLE, 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, x as displayIdent, y as STACK_REF_MAX_CODE_POINTS } from "./logger-H8n5Qjhq.js";
|
|
3
3
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
4
|
-
import { t as getCdkdVersion } from "./version-
|
|
5
|
-
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as getCdkdVersion } from "./version-CFdjI1Tq.js";
|
|
5
|
+
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-B4m3magB.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";
|
|
@@ -64114,6 +64114,40 @@ function containsSecretDynamicReference(value) {
|
|
|
64114
64114
|
function templateHasSecretDynamicReference(template) {
|
|
64115
64115
|
return containsSecretDynamicReference(template);
|
|
64116
64116
|
}
|
|
64117
|
+
/**
|
|
64118
|
+
* Can a condition verdict reach an output's VALUE in this template?
|
|
64119
|
+
*
|
|
64120
|
+
* The no-change merge preview (issue #3101) must not carry a failed output a
|
|
64121
|
+
* condition verdict could change, because `cdkd diff` evaluates `Conditions`
|
|
64122
|
+
* best-effort and its verdict can differ from the deploy's. An output never
|
|
64123
|
+
* reads a resource's template properties (a `Ref` or `Fn::GetAtt` reads the
|
|
64124
|
+
* stored record), so a verdict reaches its value only through the output's own
|
|
64125
|
+
* `Condition`, or an `Fn::If` / `{ Condition: ... }` outside `Resources`: in an
|
|
64126
|
+
* output, or in a mapping `Fn::FindInMap` returns. `Conditions` is skipped as
|
|
64127
|
+
* well, since a reference there composes a verdict and reaches an output only
|
|
64128
|
+
* through one of those. A verdict that prunes or keeps a resource differently
|
|
64129
|
+
* changes the resource set instead; where that leaves the diff seeing no change
|
|
64130
|
+
* the deploy sees, the warning naming every carried output already says the
|
|
64131
|
+
* next deploy may write a different value for it.
|
|
64132
|
+
*
|
|
64133
|
+
* Not "the template declares `Conditions`": CDK attaches `CDKMetadataAvailable`
|
|
64134
|
+
* to its `AWS::CDK::Metadata` resource in every env-agnostic stack, and that
|
|
64135
|
+
* test refused the preview for the default app shape.
|
|
64136
|
+
*/
|
|
64137
|
+
function templateLetsConditionsReachOutputs(template) {
|
|
64138
|
+
for (const output of Object.values(template.Outputs ?? {})) if (output.Condition !== void 0) return true;
|
|
64139
|
+
return Object.entries(template).some(([section, value]) => section !== "Resources" && section !== "Conditions" && readsConditionVerdict(value));
|
|
64140
|
+
}
|
|
64141
|
+
/** An `Fn::If`, or a `{ Condition: ... }` reference, anywhere under `value`. */
|
|
64142
|
+
function readsConditionVerdict(value) {
|
|
64143
|
+
if (value === null || typeof value !== "object") return false;
|
|
64144
|
+
if (Array.isArray(value)) return value.some(readsConditionVerdict);
|
|
64145
|
+
const record = value;
|
|
64146
|
+
if (Object.prototype.hasOwnProperty.call(record, "Fn::If")) return true;
|
|
64147
|
+
const keys = Object.keys(record);
|
|
64148
|
+
if (keys.length === 1 && keys[0] === "Condition") return true;
|
|
64149
|
+
return Object.values(record).some(readsConditionVerdict);
|
|
64150
|
+
}
|
|
64117
64151
|
/** Structural equality, mirroring the deploy engine's `outputMapsEqual` leaf rule. */
|
|
64118
64152
|
function valuesEqual(a, b) {
|
|
64119
64153
|
if (a === b) return true;
|
|
@@ -64193,6 +64227,8 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64193
64227
|
const outputs = Object.create(null);
|
|
64194
64228
|
const exportNames = /* @__PURE__ */ new Set();
|
|
64195
64229
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
64230
|
+
const failedOutputKeys = /* @__PURE__ */ new Set();
|
|
64231
|
+
let failuresMirrorDeploy = true;
|
|
64196
64232
|
const secretSourceKeys = /* @__PURE__ */ new Set();
|
|
64197
64233
|
const declaredKeys = /* @__PURE__ */ new Set();
|
|
64198
64234
|
const templateHasSecretReference = containsSecretDynamicReference(template);
|
|
@@ -64202,6 +64238,8 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64202
64238
|
outputs,
|
|
64203
64239
|
exportNames,
|
|
64204
64240
|
failedKeys,
|
|
64241
|
+
failedOutputKeys,
|
|
64242
|
+
failuresMirrorDeploy,
|
|
64205
64243
|
secretSourceKeys,
|
|
64206
64244
|
declaredKeys,
|
|
64207
64245
|
templateHasSecretReference,
|
|
@@ -64226,7 +64264,7 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64226
64264
|
}
|
|
64227
64265
|
for (const [outputKey, output] of Object.entries(template.Outputs)) {
|
|
64228
64266
|
if (output.Condition !== void 0 && conditions?.[output.Condition] === false) {
|
|
64229
|
-
logger.debug(`Skipping output ${outputKey} — condition ${output.Condition} is false`);
|
|
64267
|
+
logger.debug(`Skipping output ${stripControlChars(outputKey)} — condition ${stripControlChars(String(output.Condition))} is false`);
|
|
64230
64268
|
continue;
|
|
64231
64269
|
}
|
|
64232
64270
|
if (skippedOutputKeys?.has(outputKey) && !(storedOutputs !== void 0 && Object.prototype.hasOwnProperty.call(storedOutputs, outputKey))) {
|
|
@@ -64238,15 +64276,18 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64238
64276
|
try {
|
|
64239
64277
|
value = await resolveFn(structuredClone(output.Value));
|
|
64240
64278
|
} catch (error) {
|
|
64241
|
-
logger.debug(`Diff could not resolve output ${outputKey}: ${String(error)}`);
|
|
64279
|
+
logger.debug(`Diff could not resolve output ${stripControlChars(outputKey)}: ${stripControlChars(String(error))}`);
|
|
64242
64280
|
resolutionFailed = true;
|
|
64243
64281
|
recordFailure(outputKey, output);
|
|
64282
|
+
failedOutputKeys.add(outputKey);
|
|
64244
64283
|
continue;
|
|
64245
64284
|
}
|
|
64246
64285
|
if (isUnresolvedValue(value, sourceUsedSub)) {
|
|
64247
|
-
logger.debug(`Diff left output ${outputKey} unresolved (references a pending resource)`);
|
|
64286
|
+
logger.debug(`Diff left output ${stripControlChars(outputKey)} unresolved (references a pending resource)`);
|
|
64248
64287
|
resolutionFailed = true;
|
|
64249
64288
|
recordFailure(outputKey, output);
|
|
64289
|
+
if (value === void 0) failedOutputKeys.add(outputKey);
|
|
64290
|
+
else failuresMirrorDeploy = false;
|
|
64250
64291
|
continue;
|
|
64251
64292
|
}
|
|
64252
64293
|
outputs[outputKey] = value;
|
|
@@ -64257,8 +64298,9 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64257
64298
|
if (typeof exportName !== "string") try {
|
|
64258
64299
|
exportName = await resolveFn(structuredClone(exportName));
|
|
64259
64300
|
} catch (error) {
|
|
64260
|
-
logger.debug(`Diff could not resolve Export.Name of ${outputKey}: ${String(error)}`);
|
|
64301
|
+
logger.debug(`Diff could not resolve Export.Name of ${stripControlChars(outputKey)}: ${stripControlChars(String(error))}`);
|
|
64261
64302
|
resolutionFailed = true;
|
|
64303
|
+
failuresMirrorDeploy = false;
|
|
64262
64304
|
continue;
|
|
64263
64305
|
}
|
|
64264
64306
|
if (typeof exportName === "string" && !isUnresolvedValue(exportName, exportSourceUsedSub)) {
|
|
@@ -64272,18 +64314,24 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64272
64314
|
logger.debug(`Diff cannot decide the export alias of ${stripControlChars(outputKey)} — a literal name may contain a resolved secret and state does not hold that key`);
|
|
64273
64315
|
failedKeys.add(exportName);
|
|
64274
64316
|
resolutionFailed = true;
|
|
64317
|
+
failuresMirrorDeploy = false;
|
|
64275
64318
|
}
|
|
64276
64319
|
} else {
|
|
64277
64320
|
outputs[exportName] = value;
|
|
64278
64321
|
exportNames.add(exportName);
|
|
64279
64322
|
}
|
|
64280
|
-
} else
|
|
64323
|
+
} else {
|
|
64324
|
+
resolutionFailed = true;
|
|
64325
|
+
failuresMirrorDeploy = false;
|
|
64326
|
+
}
|
|
64281
64327
|
}
|
|
64282
64328
|
}
|
|
64283
64329
|
return {
|
|
64284
64330
|
outputs,
|
|
64285
64331
|
exportNames,
|
|
64286
64332
|
failedKeys,
|
|
64333
|
+
failedOutputKeys,
|
|
64334
|
+
failuresMirrorDeploy,
|
|
64287
64335
|
secretSourceKeys,
|
|
64288
64336
|
declaredKeys,
|
|
64289
64337
|
templateHasSecretReference,
|
|
@@ -64325,6 +64373,15 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64325
64373
|
* Withholding is therefore record-level. The change is still REPORTED — only
|
|
64326
64374
|
* the value is withheld — so `--fail` and the exports story are unaffected.
|
|
64327
64375
|
*
|
|
64376
|
+
* A third input forces the same verdict: `forceLegacyRecord`, set by the
|
|
64377
|
+
* no-change merge preview (issue #3101) when a value it carried from state for
|
|
64378
|
+
* a failed output, an export alias included, is not itself a secret expression.
|
|
64379
|
+
* The first signal reads a carried key's desired side, which is then the stored
|
|
64380
|
+
* value, so evidence only its resolved value held is gone; and where a resolved
|
|
64381
|
+
* secret expression can come from (a stored attribute, a parameter, another
|
|
64382
|
+
* stack's outputs) is not enumerable. Nothing short of the carried key's own stored expression can
|
|
64383
|
+
* excuse it — the one value the per-key veto below would have excused.
|
|
64384
|
+
*
|
|
64328
64385
|
* ## The key today's template cannot account for (issue #1948)
|
|
64329
64386
|
*
|
|
64330
64387
|
* Neither signal above covers an output DELETED from the template, and an
|
|
@@ -64383,7 +64440,7 @@ async function resolveTemplateOutputs(template, resolveFn, conditions, storedOut
|
|
|
64383
64440
|
function computeOutputsDiff(current, desired, exportNames, secretSourceKeys = /* @__PURE__ */ new Set(), unaccountableScan = {}) {
|
|
64384
64441
|
const changes = [];
|
|
64385
64442
|
const currentBag = current ?? {};
|
|
64386
|
-
const legacyRecord = Object.entries(currentBag).some(([name, oldValue]) => {
|
|
64443
|
+
const legacyRecord = unaccountableScan.forceLegacyRecord === true || Object.entries(currentBag).some(([name, oldValue]) => {
|
|
64387
64444
|
if (typeof oldValue === "string" && isSecretBearingReferenceString(oldValue)) return false;
|
|
64388
64445
|
return containsSecretDynamicReference(desired[name]) || secretSourceKeys.has(name);
|
|
64389
64446
|
});
|
|
@@ -65363,8 +65420,28 @@ const STATE_RESOURCES_MALFORMED = "STATE_RESOURCES_MALFORMED";
|
|
|
65363
65420
|
* nowhere. The predicate is the plain-object test the bag's own type implies.
|
|
65364
65421
|
*/
|
|
65365
65422
|
function hasReadableResources(state) {
|
|
65366
|
-
|
|
65367
|
-
|
|
65423
|
+
return isReadableBag(state.resources);
|
|
65424
|
+
}
|
|
65425
|
+
/**
|
|
65426
|
+
* The plain-object test itself, without the `resources` field bound to it.
|
|
65427
|
+
*
|
|
65428
|
+
* Exported because {@link hasReadableResources} is not the only caller any
|
|
65429
|
+
* more: `cdkd state show` and `cdkd state resources` walk FOUR more record
|
|
65430
|
+
* containers with `Object.entries` — `outputs`, `skippedOutputs`, and each
|
|
65431
|
+
* resource's `attributes` and `properties` — and a non-object in any of them
|
|
65432
|
+
* fabricates rows exactly as the `resources` bag did (go-to-k/cdkd#3187).
|
|
65433
|
+
* Spelling the same test a second time there is what this export exists to
|
|
65434
|
+
* prevent: the two could then drift, and the one that drifted would fabricate
|
|
65435
|
+
* again while looking guarded.
|
|
65436
|
+
*
|
|
65437
|
+
* What it does NOT decide is whether a given container is malformed. That is a
|
|
65438
|
+
* per-container call, because absence means different things: an absent
|
|
65439
|
+
* `resources` bag is a defect ({@link hasReadableResources} says so), while an
|
|
65440
|
+
* absent `skippedOutputs` is the normal pre-#2740 record. Each caller pairs
|
|
65441
|
+
* this predicate with its own absence rule.
|
|
65442
|
+
*/
|
|
65443
|
+
function isReadableBag(container) {
|
|
65444
|
+
return typeof container === "object" && container !== null && !Array.isArray(container);
|
|
65368
65445
|
}
|
|
65369
65446
|
/**
|
|
65370
65447
|
* The shared explanation, in the terms the reader needs: what is wrong, what
|
|
@@ -65457,6 +65534,65 @@ function repairMalformedResourcesForReadOnly(state) {
|
|
|
65457
65534
|
function malformedStateRefusalMessage(stackName, region) {
|
|
65458
65535
|
return `${malformedStateDetail(stackName, region)} This command can WRITE state, so it refuses rather than continuing: saving over a record whose resource map could not be read would replace the evidence with a well-formed empty one and lose it permanently. Repair or remove the record first.`;
|
|
65459
65536
|
}
|
|
65537
|
+
/**
|
|
65538
|
+
* The warning a view emits when it emptied one or more {@link
|
|
65539
|
+
* RenderedStateContainer}s it could not walk (go-to-k/cdkd#3187).
|
|
65540
|
+
*
|
|
65541
|
+
* Deliberately NOT {@link malformedResourcesWarning}'s text, and the reason is
|
|
65542
|
+
* narrower than it looks. That one tells the reader not to run `cdkd deploy` /
|
|
65543
|
+
* `cdkd destroy` because an unreadable `resources` MAP is indistinguishable
|
|
65544
|
+
* from an empty stack: deploy would re-CREATE everything and destroy would
|
|
65545
|
+
* delete nothing. No container named here can produce that specific confusion —
|
|
65546
|
+
* the resource SET is still readable — so borrowing that sentence would attach
|
|
65547
|
+
* a re-create-the-world warning to a record whose resource list is intact.
|
|
65548
|
+
*
|
|
65549
|
+
* It is NOT that these containers are display-only. `properties` is read by the
|
|
65550
|
+
* deploy change calculation (`src/analyzer/diff-calculator.ts`), which imports
|
|
65551
|
+
* nothing from this module, and a non-object there compares unequal to any
|
|
65552
|
+
* desired object and yields a spurious property-change set — filed as
|
|
65553
|
+
* go-to-k/cdkd#3191. An earlier revision of this comment asserted the
|
|
65554
|
+
* display-only premise, which would have read as licence to drop a guard on
|
|
65555
|
+
* that path (review of go-to-k/cdkd#3190).
|
|
65556
|
+
*
|
|
65557
|
+
* What stays the same is the remedy: `--json` is the mode that shows the
|
|
65558
|
+
* stored value.
|
|
65559
|
+
*
|
|
65560
|
+
* ONE warning per record however many containers it names — a stack whose 500
|
|
65561
|
+
* resources all carry a hand-edited `properties` gets one line, not 500. The
|
|
65562
|
+
* caller FILTERS a fixed order (`RENDERED_CONTAINER_ORDER`), so the text is
|
|
65563
|
+
* stable across records. Not `.sort()` — that order is deliberately not
|
|
65564
|
+
* alphabetical, and its own JSDoc says so.
|
|
65565
|
+
*
|
|
65566
|
+
* Both identifiers are sanitized and then shell-quoted, and the command is
|
|
65567
|
+
* emitted LAST and UNWRAPPED, for the reasons {@link safeIdentifier}'s own note
|
|
65568
|
+
* gives.
|
|
65569
|
+
*
|
|
65570
|
+
* The container NAMES take {@link safeIdentifier} too — the SAME helper, so
|
|
65571
|
+
* they cannot drift from it — but NOT `shellQuote`, because they appear in the
|
|
65572
|
+
* PROSE and never inside the command this text tells the reader to run; the
|
|
65573
|
+
* command carries the two identifiers and nothing else.
|
|
65574
|
+
*
|
|
65575
|
+
* Sanitizing them is not dead code written for a case that cannot happen. The
|
|
65576
|
+
* union above is closed at COMPILE time, so without this the guarantee would
|
|
65577
|
+
* live in a comment, and the day a caller derives a name from a record instead
|
|
65578
|
+
* of from a literal an unsanitized element could forge a line, render as empty
|
|
65579
|
+
* quotes naming nothing, or run to kilobytes and push the remedy command off
|
|
65580
|
+
* the reader's screen. `safeIdentifier` closes all three and is the identity on
|
|
65581
|
+
* all four literals, so no user-visible text moves.
|
|
65582
|
+
*
|
|
65583
|
+
* What it does NOT close, stated rather than reassured away: a forged name made
|
|
65584
|
+
* only of printable ASCII still renders verbatim inside its quotes and could
|
|
65585
|
+
* read as prose. That residual is bounded to the PROSE — the command the reader
|
|
65586
|
+
* pastes is built from the two shell-quoted identifiers alone — and closing it
|
|
65587
|
+
* would mean JSON-quoting a name in the one place the text is meant to read as
|
|
65588
|
+
* English (review of go-to-k/cdkd#3190).
|
|
65589
|
+
*/
|
|
65590
|
+
function malformedRenderedContainersWarning(stackName, region, containers) {
|
|
65591
|
+
const stack = safeIdentifier(stackName);
|
|
65592
|
+
const reg = safeIdentifier(region);
|
|
65593
|
+
const names = containers.map((name) => `'${safeIdentifier(name)}'`).join(", ");
|
|
65594
|
+
return `State for ${shellQuote(stack)} (${shellQuote(reg)}) has a non-object ${names} — the record is malformed or truncated. 'Object.entries' walks a string or a list as readily as a map, so rendering one INVENTS a row per character or element. Continuing with it EMPTY: this view shows no rows there, which is not the same as the record holding none. A per-resource container is named once however many resources hold one. See the stored values with: cdkd state show ${shellQuote(stack)} --stack-region ${shellQuote(reg)} --json`;
|
|
65595
|
+
}
|
|
65460
65596
|
/** The warning a caller of {@link repairMalformedResourcesForReadOnly} emits. */
|
|
65461
65597
|
function malformedResourcesWarning(stackName, region) {
|
|
65462
65598
|
return `${malformedStateDetail(stackName, region)} Continuing with an EMPTY resource set: this command's output describes zero resources, which is not the same as the stack having none.`;
|
|
@@ -65720,7 +65856,7 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
65720
65856
|
const tokenParameterNames = new Set(Object.entries(parameters ?? {}).filter(([, value]) => carriesDynamicReference(value)).map(([name]) => name));
|
|
65721
65857
|
for (const name of tokenParameterNames) {
|
|
65722
65858
|
const declaredType = (template.Parameters?.[name])?.Type;
|
|
65723
|
-
if (typeof declaredType === "string" && parameterTypeMayLoseSecretIdentity(declaredType)) logger.warn(`Stack ${stackName}: parameter '${name}' is declared 'Type: ${declaredType}' and is fed a secret dynamic reference. 'cdkd deploy' refuses this when the coercion actually destroys the plaintext (a comma-bearing secret in a list-typed parameter) — declare it 'Type: String' — and this diff compares the unresolved reference, shaped by the declared type only where that keeps every part of it a string.`);
|
|
65859
|
+
if (typeof declaredType === "string" && parameterTypeMayLoseSecretIdentity(declaredType)) logger.warn(`Stack ${stripControlChars(stackName)}: parameter '${stripControlChars(name)}' is declared 'Type: ${stripControlChars(declaredType)}' and is fed a secret dynamic reference. 'cdkd deploy' refuses this when the coercion actually destroys the plaintext (a comma-bearing secret in a list-typed parameter) — declare it 'Type: String' — and this diff compares the unresolved reference, shaped by the declared type only where that keeps every part of it a string.`);
|
|
65724
65860
|
}
|
|
65725
65861
|
try {
|
|
65726
65862
|
const userParameters = {};
|
|
@@ -65784,16 +65920,36 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
65784
65920
|
const changedLogicalIds = new Set([...changes.values()].filter((c) => c.changeType !== "NO_CHANGE").map((c) => c.logicalId));
|
|
65785
65921
|
const bindingSkipped = bindingSkippedOutputs(outputsDigestSource, currentState.skippedOutputs, changedLogicalIds);
|
|
65786
65922
|
const resolved = await resolveTemplateOutputs(effectiveTemplate, resolveFn, conditions, currentState.outputs, bindingSkipped);
|
|
65787
|
-
|
|
65788
|
-
|
|
65789
|
-
if (computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys, {
|
|
65790
|
-
declaredKeys: resolved.declaredKeys,
|
|
65791
|
-
templateHasSecretReference: resolved.templateHasSecretReference || inheritSecretBearingTemplate === true
|
|
65792
|
-
}).filter((change) => !resolved.failedKeys.has(change.name)).length > 0) logger.warn(`Outputs of stack ${stackName} may have changed, but one or more could not be resolved against current state — omitting the Outputs section from this diff. It is usually an output referencing a resource this deploy has yet to create.`);
|
|
65793
|
-
} else outputChanges = computeOutputsDiff(currentState.outputs, resolved.outputs, resolved.exportNames, resolved.secretSourceKeys, {
|
|
65923
|
+
const templateHasSecretReference = resolved.templateHasSecretReference || inheritSecretBearingTemplate === true;
|
|
65924
|
+
const diffOutputsAgainst = (desired, exportNames, forceLegacyRecord = false) => computeOutputsDiff(currentState.outputs, desired, exportNames, resolved.secretSourceKeys, {
|
|
65794
65925
|
declaredKeys: resolved.declaredKeys,
|
|
65795
|
-
templateHasSecretReference
|
|
65926
|
+
templateHasSecretReference,
|
|
65927
|
+
forceLegacyRecord
|
|
65796
65928
|
});
|
|
65929
|
+
let outputChanges = [];
|
|
65930
|
+
if (!resolved.resolutionFailed) outputChanges = diffOutputsAgainst(resolved.outputs, resolved.exportNames);
|
|
65931
|
+
else {
|
|
65932
|
+
const resourcesChange = diffCalculator.hasChanges(changes);
|
|
65933
|
+
const merge = resolved.failuresMirrorDeploy && conditions !== void 0 && !templateLetsConditionsReachOutputs(template) && !resourcesChange ? mergeNoChangeOutputs({
|
|
65934
|
+
persisted: currentState.outputs ?? {},
|
|
65935
|
+
resolved: withFailedOutputsUndefined(resolved.outputs, resolved.failedOutputKeys),
|
|
65936
|
+
declaredOutputs: effectiveTemplate.Outputs,
|
|
65937
|
+
previousExportNames: new Set(importableOutputKeys(currentState)),
|
|
65938
|
+
resolvedExportNames: [...resolved.exportNames]
|
|
65939
|
+
}) : void 0;
|
|
65940
|
+
if (merge?.kind === "merged") {
|
|
65941
|
+
const storedOutputs = currentState.outputs ?? {};
|
|
65942
|
+
const withheld = merge.carriedKeys.some((key) => !isSecretBearingReferenceString(storedOutputs[key]));
|
|
65943
|
+
outputChanges = diffOutputsAgainst(merge.outputs, new Set(merge.exportNames), withheld);
|
|
65944
|
+
const failed = [...resolved.failedOutputKeys];
|
|
65945
|
+
const isStored = (key) => Object.prototype.hasOwnProperty.call(storedOutputs, key);
|
|
65946
|
+
const compared = failed.filter(isStored);
|
|
65947
|
+
const unstored = failed.filter((key) => !isStored(key));
|
|
65948
|
+
const names = (keys) => keys.map(stripControlChars).join(", ");
|
|
65949
|
+
const explainWithheld = withheld && outputChanges.some((change) => change.oldValueRedacted === true);
|
|
65950
|
+
logger.warn(`Outputs of stack ${stripControlChars(stackName)}: ${failed.length} output(s) could not be resolved for this diff.` + (compared.length > 0 ? ` Compared at their stored values: ${names(compared)}.` : "") + (unstored.length > 0 ? ` No stored value under their own names: ${names(unstored)}.` : "") + ` The next deploy may write a value for any of them it can resolve.` + (explainWithheld ? ` Previous values in this Outputs section are withheld because a value carried from state for a failed output is not a secret reference, so this diff cannot rule out legacy plaintext in state. That reason no longer applies once every failed output resolves, though other legacy-plaintext checks still can withhold them.` : ""));
|
|
65951
|
+
} else if (diffOutputsAgainst(resolved.outputs, resolved.exportNames).filter((change) => !resolved.failedKeys.has(change.name)).length > 0) logger.warn(`Outputs of stack ${stripControlChars(stackName)} may have changed, but one or more could not be resolved against current state — omitting the Outputs section from this diff. ` + (merge?.kind === "kept" ? keptWholeReasonText(merge.reason) : resourcesChange ? `It is usually an output referencing a resource this deploy has yet to create.` : `The stack has no resource change; this diff cannot preview what the deploy does with the outputs that failed here.`));
|
|
65952
|
+
}
|
|
65797
65953
|
return {
|
|
65798
65954
|
changes,
|
|
65799
65955
|
outputChanges,
|
|
@@ -65803,6 +65959,21 @@ async function computeStackDiff(currentState, template, region, stackName, state
|
|
|
65803
65959
|
};
|
|
65804
65960
|
}
|
|
65805
65961
|
/**
|
|
65962
|
+
* The preview's bag in the shape `mergeNoChangeOutputs` reads from the deploy
|
|
65963
|
+
* engine: each failed OUTPUT present with the value `undefined`
|
|
65964
|
+
* (issue #3101). This resolver drops a failed key while the deploy keeps it,
|
|
65965
|
+
* and the merge tells "failed, keep the stored value" from "not produced,
|
|
65966
|
+
* remove it" by exactly that presence. A fresh null-prototype bag, like both
|
|
65967
|
+
* bags it stands between, so a resolved `__proto__` export alias survives the
|
|
65968
|
+
* copy as a data key.
|
|
65969
|
+
*/
|
|
65970
|
+
function withFailedOutputsUndefined(outputs, failedOutputKeys) {
|
|
65971
|
+
const bag = Object.create(null);
|
|
65972
|
+
for (const [key, value] of Object.entries(outputs)) bag[key] = value;
|
|
65973
|
+
for (const key of failedOutputKeys) bag[key] = void 0;
|
|
65974
|
+
return bag;
|
|
65975
|
+
}
|
|
65976
|
+
/**
|
|
65806
65977
|
* Resolve a nested-stack child's input `Parameters` (declared on the parent's
|
|
65807
65978
|
* `AWS::CloudFormation::Stack` row under `Properties.Parameters`) to scalar
|
|
65808
65979
|
* values against the PARENT's deployed state + already-resolved parameters.
|
|
@@ -66312,7 +66483,7 @@ function renderOutputChangeLines(outputChanges, logFn) {
|
|
|
66312
66483
|
*/
|
|
66313
66484
|
function renderDiffTree(node, isRoot, logFn) {
|
|
66314
66485
|
const hasChanges = nodeHasChanges(node);
|
|
66315
|
-
if (hasChanges || node.blocking.length > 0) logFn(isRoot ? `\nStack ${node.stackName}:` : `\nNested stack: ${node.displayName}`);
|
|
66486
|
+
if (hasChanges || node.blocking.length > 0) logFn(isRoot ? `\nStack ${stripControlChars(node.stackName)}:` : `\nNested stack: ${stripControlChars(node.displayName)}`);
|
|
66316
66487
|
if (hasChanges) {
|
|
66317
66488
|
const { create, update, delete: del } = renderChangeLines(node.changes, logFn, node.ccApiRoutes, node.adoptedOrphans);
|
|
66318
66489
|
const outputs = renderOutputChangeLines(node.outputChanges, logFn);
|
|
@@ -73789,8 +73960,12 @@ function createStateMigrateCommand() {
|
|
|
73789
73960
|
* or destroyed out-of-band) are reported at the root level so they stay
|
|
73790
73961
|
* visible to `cdkd state list` rather than vanishing silently.
|
|
73791
73962
|
*
|
|
73792
|
-
*
|
|
73793
|
-
*
|
|
73963
|
+
* Every node on a parent LOOP lands at the root rather than building an
|
|
73964
|
+
* infinite tree. A self-link (parent equals self) is the one-node loop. A
|
|
73965
|
+
* longer one (A names B, B names A) used to be linked as given, so each member
|
|
73966
|
+
* was filed as the other's child, none was a root, and the whole loop vanished
|
|
73967
|
+
* from both the text and the JSON view. A node hanging off a loop still sits
|
|
73968
|
+
* under its parent, which is now visible (issue #3069).
|
|
73794
73969
|
*
|
|
73795
73970
|
* The roots and every child list are sorted alphabetically by `stackName`,
|
|
73796
73971
|
* then by `region` (legacy `undefined` last), so output is stable across
|
|
@@ -73803,14 +73978,27 @@ function buildStackTree(entries) {
|
|
|
73803
73978
|
...entry,
|
|
73804
73979
|
children: []
|
|
73805
73980
|
});
|
|
73981
|
+
const parentOf = (node) => node.parentStack === void 0 ? void 0 : byKey.get(refKey(node.parentStack, node.parentRegion));
|
|
73982
|
+
const loopMembers = /* @__PURE__ */ new Set();
|
|
73983
|
+
const settled = /* @__PURE__ */ new Set();
|
|
73984
|
+
for (const start of byKey.values()) {
|
|
73985
|
+
const path = [];
|
|
73986
|
+
const onPath = /* @__PURE__ */ new Map();
|
|
73987
|
+
let current = start;
|
|
73988
|
+
while (current !== void 0 && !settled.has(current) && !onPath.has(current)) {
|
|
73989
|
+
onPath.set(current, path.length);
|
|
73990
|
+
path.push(current);
|
|
73991
|
+
current = parentOf(current);
|
|
73992
|
+
}
|
|
73993
|
+
if (current !== void 0 && onPath.has(current)) for (const member of path.slice(onPath.get(current))) loopMembers.add(member);
|
|
73994
|
+
for (const node of path) settled.add(node);
|
|
73995
|
+
}
|
|
73806
73996
|
const roots = [];
|
|
73807
73997
|
for (const node of byKey.values()) {
|
|
73808
|
-
|
|
73809
|
-
|
|
73810
|
-
|
|
73811
|
-
|
|
73812
|
-
continue;
|
|
73813
|
-
}
|
|
73998
|
+
const parent = parentOf(node);
|
|
73999
|
+
if (parent && !loopMembers.has(node)) {
|
|
74000
|
+
parent.children.push(node);
|
|
74001
|
+
continue;
|
|
73814
74002
|
}
|
|
73815
74003
|
roots.push(node);
|
|
73816
74004
|
}
|
|
@@ -75413,7 +75601,13 @@ async function buildCdkdStateStackTree(rootStackName, region, stateBackend, pref
|
|
|
75413
75601
|
}
|
|
75414
75602
|
async function walkCdkdStateStackTree(stackName, region, state, stateBackend) {
|
|
75415
75603
|
const nestedChildren = /* @__PURE__ */ new Map();
|
|
75416
|
-
|
|
75604
|
+
if (!hasReadableResources(state)) return {
|
|
75605
|
+
stackName,
|
|
75606
|
+
region,
|
|
75607
|
+
state,
|
|
75608
|
+
nestedChildren
|
|
75609
|
+
};
|
|
75610
|
+
for (const [logicalId, entry] of Object.entries(state.resources)) {
|
|
75417
75611
|
if (entry?.resourceType !== "AWS::CloudFormation::Stack") continue;
|
|
75418
75612
|
const childStackName = `${stackName}~${logicalId}`;
|
|
75419
75613
|
const childResult = await stateBackend.getState(childStackName, region);
|
|
@@ -77456,14 +77650,81 @@ function createExportCommand() {
|
|
|
77456
77650
|
//#endregion
|
|
77457
77651
|
//#region src/cli/commands/state.ts
|
|
77458
77652
|
/**
|
|
77459
|
-
*
|
|
77460
|
-
*
|
|
77653
|
+
* The reasons a `--long` row carries for a failed read. FIXED text, never the
|
|
77654
|
+
* caught error's message: `getState`'s invalid-JSON refusal interpolates V8's
|
|
77655
|
+
* `SyntaxError`, which quotes bytes of the state body, and anyone with
|
|
77656
|
+
* `s3:PutObject` on the bucket chooses those bytes. Sanitizing or truncating
|
|
77657
|
+
* that message does not redact it, so a planted plaintext would reach both
|
|
77658
|
+
* `--long` and `--long --json` (issue #3069). `cdkd state show` for the one
|
|
77659
|
+
* stack is where the specific error is reported.
|
|
77660
|
+
*
|
|
77661
|
+
* The failure is deliberately not classified further: `getState` wraps a parse
|
|
77662
|
+
* refusal, the unsupported-version refusal and an AWS read failure in the same
|
|
77663
|
+
* `StateError`, so any finer reason would be a guess about the class.
|
|
77664
|
+
*/
|
|
77665
|
+
const STATE_READ_FAILED_REASON = "state record could not be read; run `cdkd state show` for the error";
|
|
77666
|
+
const LOCK_READ_FAILED_REASON = "lock could not be read; run `cdkd state show` for the error";
|
|
77667
|
+
/**
|
|
77668
|
+
* The lock reason for a LEGACY row (no region). `cdkd state show` refuses such
|
|
77669
|
+
* a record before it reads the lock, so pointing there would send the user to
|
|
77670
|
+
* a command that never shows the error.
|
|
77671
|
+
*/
|
|
77672
|
+
const LEGACY_LOCK_READ_FAILED_REASON = "lock could not be read";
|
|
77673
|
+
/**
|
|
77674
|
+
* The record-side reason for a record that WAS read but whose `resources` is
|
|
77675
|
+
* present and not a JSON object. `parseStateBody` validates nothing inside the
|
|
77676
|
+
* root, so `Object.keys` would count a planted string per character or a list
|
|
77677
|
+
* per element -- a made-up number, and for a string of millions of characters
|
|
77678
|
+
* an array of that many keys, built outside {@link readOrFailure}'s `try`. It
|
|
77679
|
+
* reuses `stateReadError` so `resourceCount` stays `null` exactly when a reason
|
|
77680
|
+
* is set: the text row never prints `Resources: null`, and the warning counts
|
|
77681
|
+
* the row (issue #3069).
|
|
77682
|
+
*/
|
|
77683
|
+
const RESOURCES_MALFORMED_REASON = "resources is not a JSON object; run `cdkd state show --json` to see the record";
|
|
77684
|
+
/**
|
|
77685
|
+
* A record's resource count, or `null` when its `resources` is not a JSON
|
|
77686
|
+
* object ({@link RESOURCES_MALFORMED_REASON}). An absent or `null` bag counts
|
|
77687
|
+
* as zero, the tolerance `docs/cli-state.md` documents for it.
|
|
77688
|
+
*/
|
|
77689
|
+
function resourceCountOrNull(resources) {
|
|
77690
|
+
if (resources === void 0 || resources === null) return 0;
|
|
77691
|
+
if (typeof resources !== "object" || Array.isArray(resources)) return null;
|
|
77692
|
+
return Object.keys(resources).length;
|
|
77693
|
+
}
|
|
77694
|
+
/**
|
|
77695
|
+
* Run one read and report whether it threw, WITHOUT keeping the error. The
|
|
77696
|
+
* error is dropped on purpose ({@link STATE_READ_FAILED_REASON} says why): a
|
|
77697
|
+
* caller that never holds it cannot copy it into a row.
|
|
77698
|
+
*/
|
|
77699
|
+
async function readOrFailure(read) {
|
|
77700
|
+
try {
|
|
77701
|
+
return {
|
|
77702
|
+
ok: true,
|
|
77703
|
+
value: await read()
|
|
77704
|
+
};
|
|
77705
|
+
} catch {
|
|
77706
|
+
return { ok: false };
|
|
77707
|
+
}
|
|
77708
|
+
}
|
|
77709
|
+
/**
|
|
77710
|
+
* A record's `lastModified` as an ISO string, or `null` when it is not a
|
|
77711
|
+
* timestamp `Date` can represent. `parseStateBody` does not validate the
|
|
77712
|
+
* field, and `toISOString()` THROWS a `RangeError` for any number outside
|
|
77713
|
+
* JavaScript's date range (`1e300`, `NaN`-producing values, `-1e20`). That
|
|
77714
|
+
* throw sits AFTER {@link readOrFailure} reported success, so without this
|
|
77715
|
+
* check one planted record would still reject the whole `--long` listing
|
|
77716
|
+
* (issue #3069, security review).
|
|
77461
77717
|
*/
|
|
77462
|
-
function
|
|
77463
|
-
|
|
77718
|
+
function isoTimestampOrNull(value) {
|
|
77719
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
77720
|
+
const date = new Date(value);
|
|
77721
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
77464
77722
|
}
|
|
77465
77723
|
/**
|
|
77466
|
-
*
|
|
77724
|
+
* Render `Stack` or `Stack (region)` for a line a terminal renders: a
|
|
77725
|
+
* CONFIRMATION PROMPT's sentence, and every `state list` text view -- the
|
|
77726
|
+
* default one-reference-per-line listing and the `--long` / `--tree` rows
|
|
77727
|
+
* (issue #3069). The unsanitized twin it replaced lost its last caller there.
|
|
77467
77728
|
*
|
|
77468
77729
|
* Both halves come from an S3 key segment (or, for a legacy record, a state
|
|
77469
77730
|
* body), so both are attacker-influenced in exactly the way issue #2170 round
|
|
@@ -77472,14 +77733,74 @@ function formatStackRef(ref) {
|
|
|
77472
77733
|
* PROMPT is strictly worse than one in an error, because the sentence it
|
|
77473
77734
|
* corrupts is the one the operator answers `y` to.
|
|
77474
77735
|
*
|
|
77475
|
-
*
|
|
77476
|
-
*
|
|
77477
|
-
*
|
|
77478
|
-
*
|
|
77479
|
-
* `()`
|
|
77736
|
+
* BOTH halves go through `displayIdent`, not the bare `safe()` allowlist below
|
|
77737
|
+
* (issue #3164). The allowlist closes line FORGERY and nothing else: it is the
|
|
77738
|
+
* identity on printable ASCII, and this function's ` (` / `)` is cdkd's OWN
|
|
77739
|
+
* annotation of the line, so a planted 2-segment legacy key
|
|
77740
|
+
* `cdkd/ProdStack (us-east-1)/state.json` yields a region-LESS ref whose
|
|
77741
|
+
* `stackName` is literally `ProdStack (us-east-1)` and renders BYTE-EQUAL to
|
|
77742
|
+
* the genuine `ProdStack` in `us-east-1`. A `while read -r ref` cleanup loop —
|
|
77743
|
+
* the consumer the sanitization exists for — cannot tell the two apart, so it
|
|
77744
|
+
* acts on the reference the operator believes is real and the planted record
|
|
77745
|
+
* survives the sweep. The join in the two prompt callers is a second boundary
|
|
77746
|
+
* of the same kind: they `join(', ')`, which a name carrying `, ` forges an
|
|
77747
|
+
* extra entry in -- and quoting closes that, since a space is outside
|
|
77748
|
+
* `PLAIN_IDENT`.
|
|
77749
|
+
*
|
|
77750
|
+
* A BARE `,` is NOT closed, and an earlier revision of this comment was wrong
|
|
77751
|
+
* to call it harmless: the formatter supplies the space that completes the
|
|
77752
|
+
* separator, so `ProdStack,` renders `ProdStack, (us-east-1)` and a two-target
|
|
77753
|
+
* list reads as THREE entries. Only `state refresh-observed` prints a count
|
|
77754
|
+
* beside its list; `state orphan`'s banner prints none, so there nothing on
|
|
77755
|
+
* screen contradicts the forged entry -- the worse of the two. Removing `,`
|
|
77756
|
+
* from `PLAIN_IDENT` would close it and was tried; it regresses a legitimate
|
|
77757
|
+
* IAM role ARN, whose role-name segment allows `[\w+=,.@-]`. Recorded on
|
|
77758
|
+
* go-to-k/cdkd#3179 rather than traded for that.
|
|
77759
|
+
*
|
|
77760
|
+
* `displayIdent` makes the boundary VISIBLE by JSON-quoting anything that is
|
|
77761
|
+
* not a plain identifier, so the row above reads `"ProdStack (us-east-1)"` and
|
|
77762
|
+
* no longer collides. It is applied to the REGION half too: a region is an S3
|
|
77763
|
+
* key segment (or, for a legacy record, the state body via `readLegacyRegion`),
|
|
77764
|
+
* so it is attacker-chosen in exactly the same way, and a planted region alone
|
|
77765
|
+
* renders `Decoy (x) (us-east-1)` — the same spoof from the right-hand side.
|
|
77766
|
+
*
|
|
77767
|
+
* Every legitimate row is byte-identical, because `displayIdent` is the
|
|
77768
|
+
* identity on a plain identifier and CloudFormation stack names
|
|
77769
|
+
* (`Parent~Child` included) and AWS region codes all are — so no fixture, no
|
|
77770
|
+
* script's grep and no round-trip into `cdkd state show` changes. It keeps
|
|
77771
|
+
* `safe()`'s `UNRENDERABLE` fallback for a value sanitising leaves empty, since
|
|
77772
|
+
* an empty `()` would read as "no region" rather than "a region cdkd will not
|
|
77773
|
+
* print".
|
|
77774
|
+
*
|
|
77775
|
+
* The NAME half passes `STACK_REF_MAX_CODE_POINTS` rather than taking
|
|
77776
|
+
* `displayIdent`'s 255 default, and that is load-bearing for the paragraph
|
|
77777
|
+
* above rather than a tuning knob: a cdkd state-record name is not a
|
|
77778
|
+
* CloudFormation stack name, because `deriveChildStackName` appends
|
|
77779
|
+
* `~<logicalId>` per nesting level, so a legitimate deep nested-stack child
|
|
77780
|
+
* exceeds 255 and would be rendered CUT — a byte change on a legitimate row, in
|
|
77781
|
+
* the middle of a line `while read -r ref` consumes. The REGION half keeps the
|
|
77782
|
+
* default; an AWS region code is at most 25 characters.
|
|
77783
|
+
*
|
|
77784
|
+
* The consequence is taken at ALL SIX callers, the confirmation prompt
|
|
77785
|
+
* included, rather than at the listing alone: guarding one site and leaving
|
|
77786
|
+
* five is the per-site spelling `safe()`'s own comment below records having
|
|
77787
|
+
* failed twice. In the prompt a quoted spoof is the most valuable of the six —
|
|
77788
|
+
* that sentence is the one an operator answers `y` to.
|
|
77789
|
+
*
|
|
77790
|
+
* What the quoting is NOT: shell-safe. A JSON string literal is visually
|
|
77791
|
+
* indistinguishable from a shell DOUBLE-quoted argument, in which `$(...)`,
|
|
77792
|
+
* backticks and `!` still expand — so `"Prod$(touch /tmp/pwn)" (us-east-1)`
|
|
77793
|
+
* must not be pasted into a command line, and this rendering makes no claim
|
|
77794
|
+
* that it may be. The repo's answer for a value that has to survive a command
|
|
77795
|
+
* line is elsewhere and is SUPPRESSION, not quoting: `buildForceUnlockCommand`
|
|
77796
|
+
* declines to print a command at all unless sanitisation was the identity, and
|
|
77797
|
+
* `rollback-executor.ts`'s `PASTEABLE_LOGICAL_ID` records why identity alone is
|
|
77798
|
+
* still not enough (`~user` and `=x` expand). What this function provides is a
|
|
77799
|
+
* visible BOUNDARY for a value a human is reading, which is a different job.
|
|
77480
77800
|
*/
|
|
77481
77801
|
function formatStackRefSafe(ref) {
|
|
77482
|
-
|
|
77802
|
+
const name = displayIdent(ref.stackName, { maxCodePoints: STACK_REF_MAX_CODE_POINTS });
|
|
77803
|
+
return ref.region ? `${name} (${displayIdent(ref.region)})` : name;
|
|
77483
77804
|
}
|
|
77484
77805
|
/**
|
|
77485
77806
|
* One spelling of "this value came from an S3 key or a state record, and is
|
|
@@ -77504,6 +77825,29 @@ function formatStackRefSafe(ref) {
|
|
|
77504
77825
|
* no-op on every legitimate input while an S3 key admits any UTF-8. Free-form
|
|
77505
77826
|
* text (a parser's own message, an AWS error) takes the denylist class
|
|
77506
77827
|
* instead; `display-safe.ts`'s header draws that line.
|
|
77828
|
+
*
|
|
77829
|
+
* It is the WEAKER of this file's two spellings and stays so deliberately.
|
|
77830
|
+
* `formatStackRefSafe` above uses `displayIdent` instead, because it renders
|
|
77831
|
+
* cdkd's own ` (region)` annotation right beside the value and the allowlist
|
|
77832
|
+
* cannot stop a value from carrying that annotation itself (issue #3164).
|
|
77833
|
+
* Widening this helper is tracked as go-to-k/cdkd#3179 rather than done here,
|
|
77834
|
+
* for reasons that are per-SITE rather than uniform, so read that issue's
|
|
77835
|
+
* table rather than generalising from this paragraph: most of these sites are
|
|
77836
|
+
* REFUSALS, where the surrounding `'...'` is at least SOME boundary -- but not
|
|
77837
|
+
* all of them are, and ` Region: ${safe(...)}` in the `--long` view below has
|
|
77838
|
+
* no surrounding anything -- while `Run 'cdkd deploy <stack>'` sites are
|
|
77839
|
+
* COMMAND HINTS, where quoting is not this repo's answer at all:
|
|
77840
|
+
* `buildForceUnlockCommand` SUPPRESSES the whole command instead.
|
|
77841
|
+
*
|
|
77842
|
+
* `grep safe(` does NOT enumerate the class, in BOTH directions. Some sites
|
|
77843
|
+
* spell `displaySafe(..., { asciiOnly: true })` inline rather than calling this
|
|
77844
|
+
* helper (`warnOnLiveForeignLock` renders its own `stack (region)` that way,
|
|
77845
|
+
* inside the same `state orphan` output); others sanitise NOTHING
|
|
77846
|
+
* (`stateRefreshObservedCommand` interpolates raw `listStacks` values into its
|
|
77847
|
+
* refusals, including a third `cdkd deploy` hint, and `stateDestroyCommand`
|
|
77848
|
+
* writes raw names into its `--all` confirmation list); and `describeStateKey`
|
|
77849
|
+
* (`state-file-keys.ts`) renders the same shape from raw key segments.
|
|
77850
|
+
* go-to-k/cdkd#3179 enumerates them all; a grep of this helper does not.
|
|
77507
77851
|
*/
|
|
77508
77852
|
function safe$1(value) {
|
|
77509
77853
|
return displaySafe(value, { asciiOnly: true }) || "<unrenderable>";
|
|
@@ -77621,7 +77965,7 @@ async function stateListCommand(options) {
|
|
|
77621
77965
|
return;
|
|
77622
77966
|
}
|
|
77623
77967
|
if (!options.long && !options.json) {
|
|
77624
|
-
for (const ref of refs) process.stdout.write(`${
|
|
77968
|
+
for (const ref of refs) process.stdout.write(`${formatStackRefSafe(ref)}\n`);
|
|
77625
77969
|
return;
|
|
77626
77970
|
}
|
|
77627
77971
|
if (options.json && !options.long) {
|
|
@@ -77634,30 +77978,35 @@ async function stateListCommand(options) {
|
|
|
77634
77978
|
}
|
|
77635
77979
|
const details = await Promise.all(refs.map(async (ref) => {
|
|
77636
77980
|
const lookupRegion = ref.region ?? "";
|
|
77637
|
-
const [
|
|
77638
|
-
const state =
|
|
77981
|
+
const [stateRead, lockRead] = await Promise.all([readOrFailure(() => lookupRegion ? setup.stateBackend.getState(ref.stackName, lookupRegion) : Promise.resolve(null)), readOrFailure(() => setup.lockManager.isLocked(ref.stackName, ref.region))]);
|
|
77982
|
+
const state = stateRead.ok ? stateRead.value?.state : void 0;
|
|
77983
|
+
const resourceCount = !stateRead.ok ? null : state ? resourceCountOrNull(state.resources) : 0;
|
|
77639
77984
|
return {
|
|
77640
77985
|
stackName: ref.stackName,
|
|
77641
77986
|
region: ref.region ?? null,
|
|
77642
|
-
resourceCount
|
|
77643
|
-
lastModified: state
|
|
77644
|
-
locked
|
|
77987
|
+
resourceCount,
|
|
77988
|
+
lastModified: state ? isoTimestampOrNull(state.lastModified) : null,
|
|
77989
|
+
locked: lockRead.ok ? lockRead.value : null,
|
|
77990
|
+
stateReadError: !stateRead.ok ? STATE_READ_FAILED_REASON : resourceCount === null ? RESOURCES_MALFORMED_REASON : null,
|
|
77991
|
+
lockReadError: lockRead.ok ? null : ref.region ? LOCK_READ_FAILED_REASON : LEGACY_LOCK_READ_FAILED_REASON
|
|
77645
77992
|
};
|
|
77646
77993
|
}));
|
|
77994
|
+
const degraded = details.filter((d) => d.stateReadError !== null || d.lockReadError !== null);
|
|
77995
|
+
if (degraded.length > 0) logger.warn(`${degraded.length} of ${details.length} stack(s) could not be fully read or counted; their rows say why.`);
|
|
77647
77996
|
if (options.json) {
|
|
77648
77997
|
process.stdout.write(`${JSON.stringify(details, null, 2)}\n`);
|
|
77649
77998
|
return;
|
|
77650
77999
|
}
|
|
77651
78000
|
const lines = [];
|
|
77652
78001
|
for (const detail of details) {
|
|
77653
|
-
lines.push(
|
|
78002
|
+
lines.push(formatStackRefSafe({
|
|
77654
78003
|
stackName: detail.stackName,
|
|
77655
78004
|
...detail.region ? { region: detail.region } : {}
|
|
77656
78005
|
}));
|
|
77657
|
-
lines.push(` Region: ${detail.region
|
|
77658
|
-
lines.push(` Resources: ${detail.resourceCount}`);
|
|
78006
|
+
lines.push(` Region: ${detail.region === null ? "(legacy)" : safe$1(detail.region)}`);
|
|
78007
|
+
lines.push(` Resources: ${detail.stateReadError !== null ? `unknown (${detail.stateReadError})` : String(detail.resourceCount)}`);
|
|
77659
78008
|
lines.push(` Last Modified: ${detail.lastModified ?? "unknown"}`);
|
|
77660
|
-
lines.push(` Lock: ${detail.locked ? "locked" : "unlocked"}`);
|
|
78009
|
+
lines.push(` Lock: ${detail.lockReadError !== null ? `unknown (${detail.lockReadError})` : detail.locked ? "locked" : "unlocked"}`);
|
|
77661
78010
|
lines.push("");
|
|
77662
78011
|
}
|
|
77663
78012
|
if (lines.length > 0) {
|
|
@@ -77694,12 +78043,13 @@ async function renderTreeMode(refs, stateBackend, asJson) {
|
|
|
77694
78043
|
region: ref.region
|
|
77695
78044
|
};
|
|
77696
78045
|
}
|
|
78046
|
+
const linkIsValid = typeof state?.parentStack === "string" && (state.parentRegion === void 0 || typeof state.parentRegion === "string");
|
|
77697
78047
|
return {
|
|
77698
78048
|
stackName: ref.stackName,
|
|
77699
78049
|
region: ref.region,
|
|
77700
|
-
...
|
|
77701
|
-
...state?.parentLogicalId
|
|
77702
|
-
...state?.parentRegion
|
|
78050
|
+
...linkIsValid && { parentStack: state.parentStack },
|
|
78051
|
+
...linkIsValid && typeof state?.parentLogicalId === "string" && { parentLogicalId: state.parentLogicalId },
|
|
78052
|
+
...linkIsValid && typeof state?.parentRegion === "string" && { parentRegion: state.parentRegion }
|
|
77703
78053
|
};
|
|
77704
78054
|
}));
|
|
77705
78055
|
const roots = buildStackTree(entries);
|
|
@@ -77708,7 +78058,7 @@ async function renderTreeMode(refs, stateBackend, asJson) {
|
|
|
77708
78058
|
return;
|
|
77709
78059
|
}
|
|
77710
78060
|
if (roots.length === 0) return;
|
|
77711
|
-
const rendered = renderStackTreeAscii(roots, (node) =>
|
|
78061
|
+
const rendered = renderStackTreeAscii(roots, (node) => formatStackRefSafe({
|
|
77712
78062
|
stackName: node.stackName,
|
|
77713
78063
|
...node.region ? { region: node.region } : {}
|
|
77714
78064
|
}));
|
|
@@ -77751,7 +78101,9 @@ async function stateResourcesCommand(stackName, options) {
|
|
|
77751
78101
|
if (!ref.region) throw new Error(`Stack '${safe$1(stackName)}' has only a legacy state record without a region. Run 'cdkd deploy ${safe$1(stackName)}' (or any cdkd write) to migrate it to the region-scoped layout, then re-run this command.`);
|
|
77752
78102
|
const stateResult = await setup.stateBackend.getState(stackName, ref.region);
|
|
77753
78103
|
if (!stateResult) throw new Error(`No state found for stack '${safe$1(stackName)}' (${safe$1(ref.region)}) in s3://${setup.bucket}/${setup.prefix}/. Run 'cdkd state list' to see available stacks.`);
|
|
77754
|
-
|
|
78104
|
+
if (repairMalformedResourcesForReadOnly(stateResult.state)) logger.warn(malformedResourcesWarning(stackName, ref.region));
|
|
78105
|
+
if (options.long || options.json) repairRenderedContainers(stateResult.state, RESOURCES_RENDERED_CONTAINERS, stackName, ref.region, logger);
|
|
78106
|
+
const resources = stateResult.state.resources;
|
|
77755
78107
|
const details = Object.entries(resources).map(([logicalId, entry]) => {
|
|
77756
78108
|
const resource = entry ?? {};
|
|
77757
78109
|
return {
|
|
@@ -77959,9 +78311,11 @@ async function stateShowCommand(stackName, options) {
|
|
|
77959
78311
|
if (options.showNested) {
|
|
77960
78312
|
const treeWithLocks = await loadLocksForTree(await buildCdkdStateStackTree(stackName, ref.region, setup.stateBackend, stateResult.state), setup.lockManager, lockInfo);
|
|
77961
78313
|
if (options.json) {
|
|
78314
|
+
warnUnreadableTreeNodes(treeWithLocks, logger);
|
|
77962
78315
|
process.stdout.write(`${JSON.stringify(treeToShowJson(treeWithLocks), null, 2)}\n`);
|
|
77963
78316
|
return;
|
|
77964
78317
|
}
|
|
78318
|
+
repairTreeForTextRender(treeWithLocks, logger);
|
|
77965
78319
|
const lines = renderTreeWithChildren(treeWithLocks);
|
|
77966
78320
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
77967
78321
|
return;
|
|
@@ -77973,6 +78327,7 @@ async function stateShowCommand(stackName, options) {
|
|
|
77973
78327
|
}, null, 2)}\n`);
|
|
77974
78328
|
return;
|
|
77975
78329
|
}
|
|
78330
|
+
repairRecordForTextRender(stateResult.state, stackName, ref.region, logger);
|
|
77976
78331
|
process.stdout.write(`${renderStateBlock(stateResult.state, lockInfo, true).join("\n")}\n`);
|
|
77977
78332
|
} finally {
|
|
77978
78333
|
setup.dispose();
|
|
@@ -77989,6 +78344,11 @@ const SKIPPED_DIGEST_PREVIEW_LEN = 12;
|
|
|
77989
78344
|
const UNSERIALIZABLE = "(unserializable)";
|
|
77990
78345
|
/**
|
|
77991
78346
|
* The `skippedOutputs` rows a state record owes, sorted by key.
|
|
78347
|
+
*
|
|
78348
|
+
* The `?? {}` covers an ABSENT field — every pre-#2740 record, plus the writers
|
|
78349
|
+
* that drop it. A non-object one is emptied at the render entry instead
|
|
78350
|
+
* (`repairRenderedContainers`, issue go-to-k/cdkd#3187), so this walk and
|
|
78351
|
+
* {@link rendersSkippedBlock}'s cannot disagree about how many rows exist.
|
|
77992
78352
|
*/
|
|
77993
78353
|
function sortedSkippedOutputs(state) {
|
|
77994
78354
|
return Object.entries(state.skippedOutputs ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
@@ -78034,9 +78394,186 @@ function skippedOutputsLegend() {
|
|
|
78034
78394
|
];
|
|
78035
78395
|
}
|
|
78036
78396
|
/**
|
|
78397
|
+
* The order {@link repairRenderedContainers} names repaired containers in.
|
|
78398
|
+
*
|
|
78399
|
+
* Fixed here rather than taken from the order they were found in, so two
|
|
78400
|
+
* records that lost the same containers produce the same warning line and a
|
|
78401
|
+
* test can assert the text rather than a set.
|
|
78402
|
+
*/
|
|
78403
|
+
const RENDERED_CONTAINER_ORDER = [
|
|
78404
|
+
"outputs",
|
|
78405
|
+
"skippedOutputs",
|
|
78406
|
+
"attributes",
|
|
78407
|
+
"properties"
|
|
78408
|
+
];
|
|
78409
|
+
/** Every container {@link renderStateBlock}'s text walks. */
|
|
78410
|
+
const SHOW_RENDERED_CONTAINERS = new Set(RENDERED_CONTAINER_ORDER);
|
|
78411
|
+
/**
|
|
78412
|
+
* What `cdkd state resources` walks: the attribute bag alone.
|
|
78413
|
+
*
|
|
78414
|
+
* `properties` is deliberately absent — that command excludes them from every
|
|
78415
|
+
* mode (`docs/cli-state.md` says so and `stateResourcesCommand`'s own doc
|
|
78416
|
+
* repeats it), so warning about a container it never renders would report a
|
|
78417
|
+
* defect the user cannot see in the output in front of them. `outputs` and
|
|
78418
|
+
* `skippedOutputs` are absent for the same reason.
|
|
78419
|
+
*/
|
|
78420
|
+
const RESOURCES_RENDERED_CONTAINERS = /* @__PURE__ */ new Set(["attributes"]);
|
|
78421
|
+
/**
|
|
78422
|
+
* Empty every container this view walks but cannot read, and warn ONCE naming
|
|
78423
|
+
* the ones that were emptied (issue go-to-k/cdkd#3187).
|
|
78424
|
+
*
|
|
78425
|
+
* `renderStateBlock` and `stateResourcesCommand --long` walk four more
|
|
78426
|
+
* containers with `Object.entries` than the `resources` bag go-to-k/cdkd#3185
|
|
78427
|
+
* guarded, and `Object.entries` takes a string, a list, a number and a boolean
|
|
78428
|
+
* as readily as an object. Measured on the shipped bundle: `cdkd state show`
|
|
78429
|
+
* over a record whose `outputs` is a 5,000,000-character string and whose
|
|
78430
|
+
* `resources` bag is HEALTHY costs ~1616 ms and ~1010 MB RSS and emits
|
|
78431
|
+
* 5,000,002 lines; the same shape in `skippedOutputs` costs ~2436 ms and
|
|
78432
|
+
* ~2148 MB. `--show-nested` pays that per node.
|
|
78433
|
+
*
|
|
78434
|
+
* AT THE RENDER ENTRY, not at the four loops, which is the rule
|
|
78435
|
+
* `src/state/malformed-resources-bag.ts`'s header records and the measurement
|
|
78436
|
+
* behind it. Here the reason the entry wins is narrower but the same in kind:
|
|
78437
|
+
* the per-resource containers are reached from inside the resource loop, so a
|
|
78438
|
+
* guard written there is one decision repeated per resource per container, and
|
|
78439
|
+
* the `?? {}` already sitting at each of them is exactly the spelling that
|
|
78440
|
+
* looks like a guard and is not.
|
|
78441
|
+
*
|
|
78442
|
+
* **Absent is NOT malformed here, and that is a different call from the one
|
|
78443
|
+
* {@link hasReadableResources} makes.** `undefined` and `null` both mean "no
|
|
78444
|
+
* such container" for all four, and the `?? {}` at each walk has always
|
|
78445
|
+
* rendered the nullish case as empty, so none of them can fabricate a row.
|
|
78446
|
+
* An unreadable `resources` bag is judged the other way because
|
|
78447
|
+
* `Object.entries(null)` THROWS and takes the whole render with it.
|
|
78448
|
+
*
|
|
78449
|
+
* The stronger half of the reason covers TWO of the four, not all of them, and
|
|
78450
|
+
* saying so is the point: `skippedOutputs?` and `attributes?` are OPTIONAL in
|
|
78451
|
+
* `src/types/state.ts`, so cdkd itself writes records without them and warning
|
|
78452
|
+
* would be a false positive on healthy state. `outputs` and `properties` are
|
|
78453
|
+
* REQUIRED there, so a nullish one IS a malformed record and this exemption
|
|
78454
|
+
* rests on the weaker reason alone — it cannot invent a row, and it renders
|
|
78455
|
+
* identically to the empty bag the reader would otherwise see. Warning on a
|
|
78456
|
+
* nullish REQUIRED container would be defensible; it is not done because the
|
|
78457
|
+
* harm this guard exists for is fabrication (review of go-to-k/cdkd#3190).
|
|
78458
|
+
*
|
|
78459
|
+
* Callers must have repaired the `resources` bag first; both do — one line up
|
|
78460
|
+
* in `repairRecordForTextRender`, and ~30 lines up in `stateResourcesCommand`,
|
|
78461
|
+
* where the mode gate sits between them.
|
|
78462
|
+
* The bag test below is that ORDER made local rather than a fallback: an
|
|
78463
|
+
* unreadable bag is emptied by the repair, so the record renders zero resources
|
|
78464
|
+
* and owns no per-resource container to repair. It reads
|
|
78465
|
+
* {@link isReadableBag} rather than {@link hasReadableResources} on purpose —
|
|
78466
|
+
* the latter is the probe `tests/unit/cli/state-record-shape.test.ts` counts to
|
|
78467
|
+
* measure the nested walk, and a second caller here would silently inflate it.
|
|
78468
|
+
*/
|
|
78469
|
+
function repairRenderedContainers(state, walked, stackName, region, logger) {
|
|
78470
|
+
const repaired = /* @__PURE__ */ new Set();
|
|
78471
|
+
const emptyIfUnwalkable = (owner, name) => {
|
|
78472
|
+
if (!walked.has(name)) return;
|
|
78473
|
+
const container = owner[name];
|
|
78474
|
+
if (container === void 0 || container === null) return;
|
|
78475
|
+
if (isReadableBag(container)) return;
|
|
78476
|
+
owner[name] = {};
|
|
78477
|
+
repaired.add(name);
|
|
78478
|
+
};
|
|
78479
|
+
const record = state;
|
|
78480
|
+
emptyIfUnwalkable(record, "outputs");
|
|
78481
|
+
emptyIfUnwalkable(record, "skippedOutputs");
|
|
78482
|
+
if ((walked.has("attributes") || walked.has("properties")) && isReadableBag(state.resources)) for (const entry of Object.values(state.resources)) {
|
|
78483
|
+
if (!isReadableBag(entry)) continue;
|
|
78484
|
+
const fields = entry;
|
|
78485
|
+
emptyIfUnwalkable(fields, "attributes");
|
|
78486
|
+
emptyIfUnwalkable(fields, "properties");
|
|
78487
|
+
}
|
|
78488
|
+
if (repaired.size === 0) return;
|
|
78489
|
+
logger.warn(malformedRenderedContainersWarning(stackName, region, RENDERED_CONTAINER_ORDER.filter((name) => repaired.has(name))));
|
|
78490
|
+
}
|
|
78491
|
+
/**
|
|
78492
|
+
* Give the TEXT render a readable `resources` bag and readable value
|
|
78493
|
+
* containers, and warn when either was a repair rather than a no-op.
|
|
78494
|
+
*
|
|
78495
|
+
* {@link renderStateBlock} walks the bag with `Object.entries`, which takes a
|
|
78496
|
+
* string, a list, a number and a boolean as readily as an object. A planted
|
|
78497
|
+
* `"abcdef"` rendered six `[index, character]` pairs as six resources that do
|
|
78498
|
+
* not exist, under a `Resources (6):` header — and the cost grows with the
|
|
78499
|
+
* planted length, since one pair is allocated per character.
|
|
78500
|
+
*
|
|
78501
|
+
* Called AFTER every `--json` branch rather than at the load, which is this
|
|
78502
|
+
* command's one departure from the rule `src/state/malformed-resources-bag.ts`
|
|
78503
|
+
* states, and is deliberate: `cdkd state show --json` is the command
|
|
78504
|
+
* `cdkd state list --long` names as the way to SEE a record it could not count,
|
|
78505
|
+
* so repairing above that branch would hand the operator a well-formed
|
|
78506
|
+
* `"resources": {}` and delete the evidence they came for.
|
|
78507
|
+
*
|
|
78508
|
+
* That placement is only safe because ONE thing reads the bag earlier — the
|
|
78509
|
+
* `--show-nested` walker — and `walkCdkdStateStackTree` now returns a childless
|
|
78510
|
+
* node on an unreadable bag before it dereferences one. The comment at that
|
|
78511
|
+
* guard carries what the walker did with such a bag before, in both directions:
|
|
78512
|
+
* a scalar-element bag yielded no children but still allocated per element, and
|
|
78513
|
+
* a LIST of resource objects hard-failed. Do not move this call up, and do not
|
|
78514
|
+
* drop that guard: either one alone leaves the other half live.
|
|
78515
|
+
*
|
|
78516
|
+
* The guard is in the WALKER and not at its caller because the walker recurses.
|
|
78517
|
+
* A caller-side test covers the root record only, which is what an earlier cut
|
|
78518
|
+
* of this fix shipped — a healthy root naming a nested child still walked that
|
|
78519
|
+
* child's bag unguarded.
|
|
78520
|
+
*
|
|
78521
|
+
* The container pass runs SECOND and under the same `--json` placement, for one
|
|
78522
|
+
* reason each. Second, because {@link repairRenderedContainers} reads the
|
|
78523
|
+
* resource entries and the repair above is what guarantees there is a map to
|
|
78524
|
+
* read them from. Under the same placement, because `--json` emits the record
|
|
78525
|
+
* as parsed and a non-object `outputs` is evidence there exactly as a
|
|
78526
|
+
* non-object `resources` bag is — and because the payload carries the stored
|
|
78527
|
+
* value whole, it fabricates nothing to emit it.
|
|
78528
|
+
*/
|
|
78529
|
+
function repairRecordForTextRender(state, stackName, region, logger) {
|
|
78530
|
+
if (repairMalformedResourcesForReadOnly(state)) logger.warn(malformedResourcesWarning(stackName, region));
|
|
78531
|
+
repairRenderedContainers(state, SHOW_RENDERED_CONTAINERS, stackName, region, logger);
|
|
78532
|
+
}
|
|
78533
|
+
/**
|
|
78534
|
+
* The same repair for every node {@link renderTreeWithChildren} will render,
|
|
78535
|
+
* the root included. Each node carries its own record, so a CHILD with a
|
|
78536
|
+
* malformed bag fabricates rows exactly as the root does; one warning per
|
|
78537
|
+
* repaired record names which stack it came from.
|
|
78538
|
+
*
|
|
78539
|
+
* Both identifiers are strings by construction rather than record fields: the
|
|
78540
|
+
* walker builds a child's name as `` `${parent}~${logicalId}` `` and threads the
|
|
78541
|
+
* region it walked against, having already refused any child whose recorded
|
|
78542
|
+
* region disagrees.
|
|
78543
|
+
*/
|
|
78544
|
+
function repairTreeForTextRender(node, logger) {
|
|
78545
|
+
repairRecordForTextRender(node.state, node.stackName, node.region, logger);
|
|
78546
|
+
for (const child of node.children) repairTreeForTextRender(child, logger);
|
|
78547
|
+
}
|
|
78548
|
+
/**
|
|
78549
|
+
* Say which nodes the walk could not read, WITHOUT repairing any of them.
|
|
78550
|
+
*
|
|
78551
|
+
* `--show-nested --json` is the one mode that must not repair — it exists to
|
|
78552
|
+
* show the operator the stored record — but "cannot repair" is not "must stay
|
|
78553
|
+
* silent". An unreadable bag makes the walk return that node childless, and a
|
|
78554
|
+
* childless node is byte-indistinguishable from a genuine leaf: a consumer
|
|
78555
|
+
* enumerating `children` reads a CUT subtree as a complete one. The text views
|
|
78556
|
+
* do not have this problem, because a cut node renders `Resources (0):` where
|
|
78557
|
+
* the operator expected rows.
|
|
78558
|
+
*
|
|
78559
|
+
* `logger.warn` writes to stderr, and `stateShowCommand` has already called
|
|
78560
|
+
* `reserveStdoutForPayload()` for this branch, so this cannot corrupt the JSON.
|
|
78561
|
+
*/
|
|
78562
|
+
function warnUnreadableTreeNodes(node, logger) {
|
|
78563
|
+
if (!hasReadableResources(node.state)) logger.warn(malformedResourcesWarning(node.stackName, node.region));
|
|
78564
|
+
for (const child of node.children) warnUnreadableTreeNodes(child, logger);
|
|
78565
|
+
}
|
|
78566
|
+
/**
|
|
78037
78567
|
* Render one stack's state record as a human-readable multi-line block —
|
|
78038
78568
|
* the shared body used by both the single-stack default output and each
|
|
78039
78569
|
* nested child rendered under `--show-nested`.
|
|
78570
|
+
*
|
|
78571
|
+
* Every caller must have run {@link repairRecordForTextRender} over the
|
|
78572
|
+
* record first (`repairTreeForTextRender` does it per node for the tree): the
|
|
78573
|
+
* resource walk below is what fabricates rows from a non-object bag, and the
|
|
78574
|
+
* `state.ts names renderStateBlock in CODE exactly 1 + 3 times` case in
|
|
78575
|
+
* `tests/unit/cli/state-record-shape.test.ts` refuses a fourth call site so
|
|
78576
|
+
* that choice cannot be made silently.
|
|
78040
78577
|
*/
|
|
78041
78578
|
function renderStateBlock(state, lockInfo, withLegend) {
|
|
78042
78579
|
const lines = [];
|
|
@@ -78067,7 +78604,7 @@ function renderStateBlock(state, lockInfo, withLegend) {
|
|
|
78067
78604
|
}
|
|
78068
78605
|
if (withLegend) lines.push(...skippedOutputsLegend());
|
|
78069
78606
|
}
|
|
78070
|
-
const resourceEntries = Object.entries(state.resources
|
|
78607
|
+
const resourceEntries = Object.entries(state.resources).map(([logicalId, entry]) => [logicalId, entry ?? {}]).sort(([a], [b]) => a.localeCompare(b));
|
|
78071
78608
|
lines.push("");
|
|
78072
78609
|
lines.push(`Resources (${resourceEntries.length}):`);
|
|
78073
78610
|
for (const [logicalId, resource] of resourceEntries) {
|
|
@@ -79148,11 +79685,39 @@ function snapshotNote(resourceType, effectiveProvisionedBy, skipFinalSnapshot) {
|
|
|
79148
79685
|
* one calls `displaySafe()` directly and takes the DENYLIST -- the same class
|
|
79149
79686
|
* `formatError` picks for a `cause`, and for the same reason. `grep displaySafe(`
|
|
79150
79687
|
* answers how many; a count written here was wrong on its first revision.
|
|
79688
|
+
*
|
|
79689
|
+
* Scope is answered by grepping BOTH `safe(` and `safeStack(` -- the latter is
|
|
79690
|
+
* not matched by the former, and a sentence naming only one understates the
|
|
79691
|
+
* population by twelve.
|
|
79151
79692
|
*/
|
|
79152
79693
|
function safe(value) {
|
|
79153
79694
|
return displayIdent(value);
|
|
79154
79695
|
}
|
|
79155
79696
|
/**
|
|
79697
|
+
* `safe()` for a cdkd STATE-RECORD STACK NAME specifically, which is the one
|
|
79698
|
+
* value class in this file whose legitimate grammar runs past `displayIdent`'s
|
|
79699
|
+
* 255-code-point default (issue #3164).
|
|
79700
|
+
*
|
|
79701
|
+
* A stack name here is not a CloudFormation stack name: `deriveChildStackName`
|
|
79702
|
+
* appends `~<logicalId>` per nesting level, and with CDK's ~60-character
|
|
79703
|
+
* generated nested-stack logical ids a legitimate child passes 255 around the
|
|
79704
|
+
* fourth level. Cutting one is a byte change on a LEGITIMATE value, and this
|
|
79705
|
+
* file is the worst place for it -- three of its renders are
|
|
79706
|
+
* `re-run 'cdkd rollback <stack>'` COPY-PASTE hints and one is the
|
|
79707
|
+
* confirmation prompt, so a cut name hands an operator an unrunnable command
|
|
79708
|
+
* mid-incident.
|
|
79709
|
+
*
|
|
79710
|
+
* It is a NAMED helper rather than a `maxCodePoints` argument repeated per
|
|
79711
|
+
* site, because a per-site spelling of exactly this rule is what issue #3164
|
|
79712
|
+
* exists to stop: the first cut of that fix widened ONE of this file's twelve
|
|
79713
|
+
* stack-name renders and left eleven cut. Every value that is NOT a stack name --
|
|
79714
|
+
* a region (at most 25 characters), a logical id, a resource type, a change
|
|
79715
|
+
* type -- keeps `safe()` and its tighter default.
|
|
79716
|
+
*/
|
|
79717
|
+
function safeStack(value) {
|
|
79718
|
+
return displayIdent(value, { maxCodePoints: STACK_REF_MAX_CODE_POINTS });
|
|
79719
|
+
}
|
|
79720
|
+
/**
|
|
79156
79721
|
* Human label for a planned rollback action (plan preview). `skipFinalSnapshot`
|
|
79157
79722
|
* is threaded in because the classifier is pure (it cannot see CLI flags) and
|
|
79158
79723
|
* the Snapshot label would otherwise promise a final snapshot the run is about
|
|
@@ -79231,7 +79796,7 @@ async function rollbackCommand(stackArg, options) {
|
|
|
79231
79796
|
return;
|
|
79232
79797
|
}
|
|
79233
79798
|
if (scoped.length > 1) {
|
|
79234
|
-
const list = scoped.map((c) => ` - ${
|
|
79799
|
+
const list = scoped.map((c) => ` - ${safeStack(c.stackName)} (${safe(c.region)})`).join("\n");
|
|
79235
79800
|
throw new Error(`Multiple stacks have a rollback journal. Pick one:\n${list}\nRe-run 'cdkd rollback <stack>' (add --stack-region if the same name spans regions).`);
|
|
79236
79801
|
}
|
|
79237
79802
|
ref = scoped[0];
|
|
@@ -79264,8 +79829,8 @@ async function rollbackCommand(stackArg, options) {
|
|
|
79264
79829
|
try {
|
|
79265
79830
|
const stateData = await setup.stateBackend.getState(stackName, region);
|
|
79266
79831
|
const journal = await setup.stateBackend.loadRollbackJournal(stackName, region);
|
|
79267
|
-
if (!journal || journal.segments.length === 0) throw new Error(`Nothing to roll back for '${
|
|
79268
|
-
if (!stateData) throw new Error(`Rollback journal exists for '${
|
|
79832
|
+
if (!journal || journal.segments.length === 0) throw new Error(`Nothing to roll back for '${safeStack(stackName)}' (${safe(region)}). Run 'cdkd deploy' to (re)deploy, or 'cdkd destroy' to clean up.`);
|
|
79833
|
+
if (!stateData) throw new Error(`Rollback journal exists for '${safeStack(stackName)}' (${safe(region)}) but its state.json is missing (keys: ${safe(setup.prefix)}/${safeStack(stackName)}/${safe(region)}/state.json and .../rollback-journal.json). State appears corrupted — inspect the bucket manually.`);
|
|
79269
79834
|
const baseState = stateData.state;
|
|
79270
79835
|
refuseMalformedState(baseState, stackName, region);
|
|
79271
79836
|
const stateResources = { ...baseState.resources };
|
|
@@ -79273,7 +79838,7 @@ async function rollbackCommand(stackArg, options) {
|
|
|
79273
79838
|
const orphanLogicalIds = new Set(options.orphan ?? []);
|
|
79274
79839
|
const newestSegment = journal.segments[journal.segments.length - 1];
|
|
79275
79840
|
if (newestSegment.roleArn && !options.roleArn) logger.info(`Note: the failed deploy ran with --role-arn ${safe(newestSegment.roleArn)}; this rollback is running with ambient credentials (pass --role-arn to match).`);
|
|
79276
|
-
logger.info(`\nRollback plan for '${
|
|
79841
|
+
logger.info(`\nRollback plan for '${safeStack(stackName)}' (${safe(region)}):`);
|
|
79277
79842
|
const planStateView = { ...stateResources };
|
|
79278
79843
|
for (let s = journal.segments.length - 1; s >= 0; s--) {
|
|
79279
79844
|
const segment = journal.segments[s];
|
|
@@ -79291,7 +79856,7 @@ async function rollbackCommand(stackArg, options) {
|
|
|
79291
79856
|
}
|
|
79292
79857
|
logger.info("");
|
|
79293
79858
|
if (!skipConfirmation) {
|
|
79294
|
-
if (!await confirm(`Roll back '${
|
|
79859
|
+
if (!await confirm(`Roll back '${safeStack(stackName)}' (${safe(region)})?`)) {
|
|
79295
79860
|
logger.info("Rollback cancelled");
|
|
79296
79861
|
return;
|
|
79297
79862
|
}
|
|
@@ -79329,7 +79894,7 @@ async function rollbackCommand(stackArg, options) {
|
|
|
79329
79894
|
const fresh = await setup.stateBackend.getState(stackName, region);
|
|
79330
79895
|
currentEtag = await setup.stateBackend.saveState(stackName, region, next(), { ...fresh?.etag !== void 0 && { expectedEtag: fresh.etag } });
|
|
79331
79896
|
} catch (retryError) {
|
|
79332
|
-
logger.warn(`Failed to persist state after a rollback operation: ${displaySafe(retryError instanceof Error ? retryError.message : String(retryError))}. The resource was reverted in AWS; re-run 'cdkd rollback ${
|
|
79897
|
+
logger.warn(`Failed to persist state after a rollback operation: ${displaySafe(retryError instanceof Error ? retryError.message : String(retryError))}. The resource was reverted in AWS; re-run 'cdkd rollback ${safeStack(stackName)}' to reconcile state.`);
|
|
79333
79898
|
}
|
|
79334
79899
|
}
|
|
79335
79900
|
};
|
|
@@ -79408,16 +79973,16 @@ async function rollbackCommand(stackArg, options) {
|
|
|
79408
79973
|
const survivingOrphans = orphansAfterRollback(baseState, mintedOrphans).orphans ?? [];
|
|
79409
79974
|
if (journal.segments.length === 0 && oldestInitialDeploy && Object.keys(stateResources).length === 0 && survivingOrphans.length === 0) {
|
|
79410
79975
|
await setup.stateBackend.deleteState(stackName, region);
|
|
79411
|
-
logger.info(`State for '${
|
|
79976
|
+
logger.info(`State for '${safeStack(stackName)}' (${safe(region)}) removed (stack fully rolled back).`);
|
|
79412
79977
|
}
|
|
79413
|
-
if (interrupted) throw new PartialFailureError(`Rollback interrupted. Journal preserved — re-run 'cdkd rollback ${
|
|
79414
|
-
if (totalFailures > 0) throw new PartialFailureError(`Rollback completed with ${totalFailures} failed operation(s). Journal preserved — re-run 'cdkd rollback ${
|
|
79978
|
+
if (interrupted) throw new PartialFailureError(`Rollback interrupted. Journal preserved — re-run 'cdkd rollback ${safeStack(stackName)}' to finish.`);
|
|
79979
|
+
if (totalFailures > 0) throw new PartialFailureError(`Rollback completed with ${totalFailures} failed operation(s). Journal preserved — re-run 'cdkd rollback ${safeStack(stackName)}' to retry.`);
|
|
79415
79980
|
if (totalWarnings > 0) throw new PartialFailureError(`Rollback completed with ${totalWarnings} skipped/unrecoverable operation(s) (see warnings above).`);
|
|
79416
|
-
logger.info(`\nRollback of '${
|
|
79981
|
+
logger.info(`\nRollback of '${safeStack(stackName)}' (${safe(region)}) complete.`);
|
|
79417
79982
|
} finally {
|
|
79418
79983
|
try {
|
|
79419
79984
|
await setup.lockManager.releaseLock(stackName, region).catch((err) => {
|
|
79420
|
-
logger.warn(`Failed to release lock for '${
|
|
79985
|
+
logger.warn(`Failed to release lock for '${safeStack(stackName)}' (${safe(region)}): ${displaySafe(err instanceof Error ? err.message : String(err))}`);
|
|
79421
79986
|
});
|
|
79422
79987
|
} finally {
|
|
79423
79988
|
process.removeListener("SIGINT", sigintHandler);
|
|
@@ -92908,4 +93473,4 @@ function buildProgram() {
|
|
|
92908
93473
|
|
|
92909
93474
|
//#endregion
|
|
92910
93475
|
export { buildProgram };
|
|
92911
|
-
//# sourceMappingURL=program-
|
|
93476
|
+
//# sourceMappingURL=program-C7UAuWDp.js.map
|