@go-to-k/cdkd 0.290.33 → 0.290.35
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-BCpZ70Ws.js → asg-provider-BguZAQtF.js} +2 -2
- package/dist/{asg-provider-BCpZ70Ws.js.map → asg-provider-BguZAQtF.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-B6TcGQSU.js → deploy-engine-DhZtBBq0.js} +597 -71
- package/dist/deploy-engine-DhZtBBq0.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-D75-DVWT.js → program-COiS7GZJ.js} +1159 -118
- package/dist/{program-D75-DVWT.js.map → program-COiS7GZJ.js.map} +1 -1
- package/dist/{version-xHGBnQbJ.js → version-BIpCWEQx.js} +2 -2
- package/dist/{version-xHGBnQbJ.js.map → version-BIpCWEQx.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-B6TcGQSU.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as isExportAliasCollision, $i as StackTerminationProtectionError, $n as maskSecretsInText, $r as stripControlChars, $t as withSharedDrainBudget, A as makeCanonicalizePropertiesFn, Ai as resolveBucketRegion, An as refuseMalformedResourcesForDestroy, Ar as AssetModeResolver, At as getAccountInfo, B as isWarmThroughputDecrease, Bi as DeployCancelledError, Bn as TemplateParser, Br as buildDenyExternalAccessPolicy, Bt as assertRegionMatch, C as PRE_DELETE_SNAPSHOT_TYPES, Ci as expectedOwnerParam, Cn as malformedStateRefusalMessage, Cr as AssetPublisher, Ct as disableInstanceApiTermination, D as isFinalSnapshotError, Di as AssemblyReader, Dn as refuseMalformedOutputs, Dr as createAssetRedirectResolver, Dt as carriesFetchableDynamicReference, E as createPreDeleteFinalSnapshot, Ei as derivePartitionAndUrlSuffix, En as refuseMalformedNestedChildOutputs, Er as buildAssetRedirectMap, Et as carriesDynamicReference, F as isStatefulRecreateTargetForReplace, Fn as findActionableSilentDrops, Fr as isCrossRegionRedirect, Ft as refStateLookupFromResource, G as cyan, Gn as carriesSecretMask, Gr as dockerSpawnEnvWithSensitive, Gt as configStringRefusal, H as toFiniteNumber, Hi as IntrinsicResolutionRefusalError, Hn as STATE_SOURCED_CROSS_GENERATION_RULES, Hr as describeDockerCapturedOutput, Ht as coerceCfnInteger, I as isStatefulRecreateTargetSync, Ii as CdkdError, In as findSilentDropProperties, Ir as parseBootstrapMarker, It as isSettledInstanceState, J as red, Ji as PartialFailureError, Jn as errorCauseChain, Jr as partitionSensitiveEnv, Jt as requireConfigArray, K as gray, Ki as MalformedProducerRecordRefusalError, Kn as createSecretMasker, Kr as formatDockerLoginError, Kt as readConfigString, L as renderStatefulReason, Ln as describeTypeWithThrottleRetry, Lr as readBootstrapMarkerBody, Lt as WAFv2WebACLProvider, M as makeSiblingClaimReader, Mi as getAwsClients, Mn as repairMalformedOutputsForReadOnly, Mr as assertAssetBucketRegion, Mt as isNamelessDynamicReferenceError, N as planOrphanAdoption, Ni as resetAwsClients, Nn as repairMalformedResourcePropertiesForReadOnly, Nr as ensureAssetStorage, Nt as isUnboundTemplateParameter, O as refusesFinalSnapshot, Oi as processStackMessages, On as refuseMalformedOutputsForDestroy, Or as loadPublishableAssetManifest, Ot as cfnRefValueFromPhysicalId, P as MULTI_REGION_RECREATE_BLOCKED_TYPES, Pi as setAwsClients, Pn as repairMalformedResourcesForReadOnly, Pr as getBootstrapMarkerKey, Pt as parameterTypeMayLoseSecretIdentity, Q as exportAliasCollisionScrubWarning, Qi as StackHasActiveImportsError, Qn as maskSecretsInError, Qr as escapeRegExp, Qt as producerRegionsFromState, R as WARM_THROUGHPUT_MEMBERS, Ri as CrossAccountSecretRefusalError, Rn as withRetry, Rr as validateAssetBucketName, Rt as normalizeAwsTagsToCfn, S as ATOMIC_FINAL_SNAPSHOT_TYPES, Si as decodeListingKey, Sn as malformedResourcesWarning, Sr as shouldRetainResource, St as deleteSkipReason, T as ccRoutedFinalSnapshotError, Ti as canonicalizeRegion, Tn as refuseDivergentRecordRegionForDestroy, Tr as WorkGraph, Tt as IntrinsicFunctionResolver, U as formatResourceLine, Ui as LocalInvokeBuildError$1, Un as STATE_SOURCED_READBACK_RULES, Ur as describeDockerExecFailure, Ut as configBooleanRefusal, V as toCfnInteger, Vi as DynamicReferenceRegionAmbiguousError, Vn as STATE_SOURCED_BASELINE_RULES, Vr as buildDockerImage, Vt as coerceCfnBoolean, W as bold, Wi as LocalStartServiceError, Wn as TEMPLATE_SOURCED_RULES, Wr as describeDockerFailure, Wt as configIntegerRefusal, X as collectDeclaredOutputNames, Xi as ResourceTimeoutError, Xn as isSingleDynamicReferenceToken, Xr as runDockerForeground, Xt as requireConfigString, Y as yellow, Yi as ProvisioningError, Yn as identityKeyFor, Yr as redactDockerArgvValues, Yt as requireConfigObject, Z as collectPublishedOutputNames, Zi as ResourceUpdateNotSupportedError, Zn as markSameGenerationBag, Zr as runDockerStreaming, Zt as classifyReplaySecretRegion, _ as bagHoldsSecretExpression, _i as CFN_TEMPLATE_URL_LIMIT, _n as malformedLocalOutputsWarning, _r as importableOutputKeys, _t as CloudControlProvider, a as DeploymentEventsStore, aa as withErrorHandling, ai as getLegacyStateBucketName, an as applyRoleArnIfSet, ar as S3StateBackend, at as collectInlinePolicyNamesManagedBySiblings, b as mergeNoChangeOutputs, bi as uploadCfnTemplate, bn as malformedRenderedContainersWarning, br as orphansAfterRollback, bt as UNSPECIFIED_SKIP_REASON, c as replayFailedOperations, ca as isThrottlingError, ci as resolveCaptureObservedState, cn as callerEnvCredentials, cr as buildForceUnlockCommand, ct as wouldReturnToSdkProvider, d as updatePartialMessage, da as markRedactedCause, di as resolveStateBucketWithDefaultAndSource, dn as INTRINSIC_KEYS, dr as formatLockExpiry, dt as maskerOrIdentity, ei as AssetManifestLoader, en as s3BucketArn, er as recordMaskOnlyValue, et as secretBearing, f as updatePartialReason, fa as retryClassificationText, fi as resolveUseCdkBootstrapAssets, fn as STATE_RESOURCES_MALFORMED, fr as shellQuote, ft as beginCommandInterruptScope, g as computeImplicitDeleteEdges, gi as CFN_TEMPLATE_BODY_LIMIT, gn as malformedExportSourceWarning, gr as hasReadableExportSet, gt as startInterruptWatch, h as IMPLICIT_DELETE_DEPENDENCIES, hi as ssmResolvedValueType, hn as malformedExportNamesWarning, hr as exportNamesCarriedFrom, ht as isInterruptedWaitError, i as DeploymentEventsReader, ia as normalizeAwsError, ii as getDefaultStateBucketName, in as s3BucketWebsiteUrl, ir as LockManager, it as IAMRoleProvider, j as extractDeploymentEventError, ji as AwsClients, jn as refuseMalformedState, jr as BOOTSTRAP_MARKER_PREFIX, jt as isImpossibleEmptyStoredAttribute, k as unsupportedFinalSnapshotError, kn as refuseMalformedResourcePropertiesForOrphan, kr as rewriteTemplateAssetReferences, kt as coerceParameterTypedValue, l as replayRollback, la as isTransientServerError, li as resolveSkipPrefix, ln as envCredentialsAreAssumedRole, lr as buildLockContentionMessage, lt as createMaskedRetryLogger, m as maskingRetryLogger, ma as safeStringify, mi as warnDeprecatedNoPrefixCliFlag, mn as hasReadableResources, mr as DEFAULT_STATE_PREFIX, mt as interruptWatchListenerCount, n as DEFAULT_RESOURCE_WARN_AFTER_MS, ni as Synthesizer, nn as s3BucketDualStackDomainName, nr as redactSecretsForState, nt as secretSafeKeyDisplay, o as planFailedOps, oa as isMarkedNonRetryable, oi as resolveApp, on as AWS_CREDENTIAL_ENV_KEYS, or as rebuildClientForBucketRegion, ot as clearOnUpdateRemoval, p as withResourceDeadline, pa as describeAwsFailure, pi as stateBucketExistenceConfirmed, pn as hasReadableOutputs, pr as CUSTOM_RESOURCE_RESPONSE_PREFIX, pt as endCommandInterruptScope, q as green, qi as NestedStackChildDirectDestroyError, qn as dynamicReferenceTokens, qr as getDockerCmd, qt as replayWarn, r as DeployEngine, ri as synthesisStatusMessage, rn as s3BucketRegionalDomainName, rr as scrubResourceRecord, rt as getCurrentResourceSecrets, s as planRollback, sa as isRetryableTransientError, si as resolveAutoAssetStorage, sn as applyCallerIdentityCredentials, sr as UNREPRODUCIBLE_LOCK_CLAUSE, st as ProviderRegistry, t as DEFAULT_RESOURCE_TIMEOUT_MS, ta as SynthesisError, ti as getDockerImageBySourceHash, tn as s3BucketDomainName, tr as recoverMaskedOutput, tt as secretBearingStateKeyWarning, u as resolveReplacementOldType, ua as markNonRetryable, ui as resolveStateBucketWithDefault, un as DiffCalculator, ur as forceQuitRecoveryClause, ut as maskDeep, v as isSecretBearingReferenceString, vi as MIGRATE_TMP_PREFIX, vn as malformedOutputsRefusalMessage, vr as importableOutputs, vt as isWaitAbandonedError, w as buildFinalSnapshotIdentifier, wi as PARTITION_TABLE, wn as producerRecordKey, wr as stringifyValue, wt as isTerminationProtectionPropagationError, x as bindingSkippedOutputs, xi as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, xn as malformedResourcePropertiesWarning, xr as orphansCarriedFrom, xt as deleteIndeterminateGuards, y as keptWholeReasonText, yi as findLargeInlineResources, yn as malformedOutputsWarning, yr as isReadableBag, yt as slowCcOperationTimeoutMs, z as coerceWarmThroughput$1, zn as DagBuilder, zr as validateContainerRepoName, zt as resolveExplicitPhysicalId } from "./deploy-engine-DhZtBBq0.js";
|
|
2
2
|
import { C as displayAwsMessage, D as truncateCodePoints, E as isPasteableIdent, S as UNRENDERABLE, T as displaySafe, _ as withSkipPrefix, b as SECRET_REF_MAX_CODE_POINTS, 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, w as displayIdent, x as STACK_REF_MAX_CODE_POINTS } from "./logger-BX4pJk0T.js";
|
|
3
3
|
import { n as awsClientDefaults } from "./aws-client-defaults-BoIxyCir.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-BIpCWEQx.js";
|
|
5
|
+
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-BguZAQtF.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";
|
|
@@ -2758,16 +2758,13 @@ const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
|
|
|
2758
2758
|
* is spelled once, or the note could name ids the validator does not refuse.
|
|
2759
2759
|
*
|
|
2760
2760
|
* The two are NOT the same set, and deliberately so. The refusal fires when
|
|
2761
|
-
* EITHER the state record or the template says nested, because
|
|
2762
|
-
*
|
|
2763
|
-
*
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
2767
|
-
*
|
|
2768
|
-
* template contains. Which type governs was argued from plausibility twice and
|
|
2769
|
-
* settled by reading `analyzer/diff-calculator.ts` and `deploy-engine.ts`'s
|
|
2770
|
-
* `oldDeleteProvider`; do not re-derive it from either name. Other modules spell it
|
|
2761
|
+
* EITHER the state record or the template says nested, because a recreate has
|
|
2762
|
+
* two halves with two types (issue #2668): the STATE's type routes the delete
|
|
2763
|
+
* of the old resource — `NestedStackProvider` derives `<parent>~<logicalId>`
|
|
2764
|
+
* from it — and the TEMPLATE's type routes the create, which for a nested row
|
|
2765
|
+
* is a whole child-stack deploy. Either half alone is outside what a recreate
|
|
2766
|
+
* is built for. The note keys on the TEMPLATE alone, because it is telling the
|
|
2767
|
+
* user what their current template contains. Other modules spell it
|
|
2771
2768
|
* for themselves rather than sharing one export: the only EXPORTED copy lives
|
|
2772
2769
|
* in `src/cli/commands/retire-cfn-stack.ts`, and importing a CLI command module
|
|
2773
2770
|
* from the deployment layer would invert the dependency direction. (No count of
|
|
@@ -3382,6 +3379,297 @@ function analyzePinCcApiReachability(pinnedLogicalIds, stacks) {
|
|
|
3382
3379
|
};
|
|
3383
3380
|
}
|
|
3384
3381
|
|
|
3382
|
+
//#endregion
|
|
3383
|
+
//#region src/utils/nested-template-cycle.ts
|
|
3384
|
+
/**
|
|
3385
|
+
* Static, READ-ONLY validation of a nested-template tree, run BEFORE anything
|
|
3386
|
+
* is deployed from it (issue go-to-k/cdkd#3247).
|
|
3387
|
+
*
|
|
3388
|
+
* `NestedStackProvider` follows `Metadata['aws:asset:path']` from one nested
|
|
3389
|
+
* template to the next and runs a whole child `DeployEngine` per level. A
|
|
3390
|
+
* relative path that resolves back onto the same nesting chain therefore
|
|
3391
|
+
* DEPLOYS every level of the cycle until S3's key limit stops it. Threading an
|
|
3392
|
+
* ancestor chain through the provider's recursion would only refuse at the
|
|
3393
|
+
* first repeat, by which point every level above it is already provisioned.
|
|
3394
|
+
* Everything the walk follows is on disk before the first child engine exists,
|
|
3395
|
+
* so the whole subtree is validated up front instead.
|
|
3396
|
+
*
|
|
3397
|
+
* Two properties are copied from the `cdkd diff --recursive` guard (issue
|
|
3398
|
+
* go-to-k/cdkd#3239), because re-deriving them gets them wrong:
|
|
3399
|
+
*
|
|
3400
|
+
* - The refused shape is a repeat along ONE root-to-node chain, never a repeat
|
|
3401
|
+
* anywhere in the tree: two sibling rows may name the same child template,
|
|
3402
|
+
* and that diamond is a legitimate assembly.
|
|
3403
|
+
* - It is a refusal, not a depth cap. A cyclic assembly has no correct deploy.
|
|
3404
|
+
*
|
|
3405
|
+
* `Condition`s are deliberately NOT evaluated: a row whose condition would be
|
|
3406
|
+
* false at deploy time still counts. A self-including template that terminates
|
|
3407
|
+
* through a condition is refused here exactly as the diff walk refuses it, so
|
|
3408
|
+
* the two commands agree about which assemblies are well-formed.
|
|
3409
|
+
*
|
|
3410
|
+
* A leaf apart from `display-safe.ts`, so any layer may import it.
|
|
3411
|
+
*/
|
|
3412
|
+
const NESTED_STACK_TYPE$1 = "AWS::CloudFormation::Stack";
|
|
3413
|
+
/**
|
|
3414
|
+
* Same predicate `NestedStackProvider` applies per level. The `startsWith('/')`
|
|
3415
|
+
* arm covers a POSIX-style absolute path consumed on Windows, where
|
|
3416
|
+
* `path.isAbsolute` alone answers `false`.
|
|
3417
|
+
*/
|
|
3418
|
+
function isAbsoluteAssetPath(p) {
|
|
3419
|
+
return path.isAbsolute(p) || p.startsWith("/");
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* What "the same template" means ON A CHAIN: the file's name inside its REAL
|
|
3423
|
+
* directory.
|
|
3424
|
+
*
|
|
3425
|
+
* The directory is `realpath`ed because a symlinked DIRECTORY (`d -> .`) makes
|
|
3426
|
+
* every level's joined path a new, longer string (`d/a.json`, `d/d/a.json`,
|
|
3427
|
+
* ...) for one and the same file, which a comparison of resolved path strings
|
|
3428
|
+
* never sees repeat. There are finitely many (real directory, name) pairs, so
|
|
3429
|
+
* an endless descent must repeat one, however it is spelled.
|
|
3430
|
+
*
|
|
3431
|
+
* It is NOT what decides a template's children. Those resolve LEXICALLY
|
|
3432
|
+
* against the directory the template was reached in (`path.join` folds `d/..`
|
|
3433
|
+
* without following the link), so one identity reached through two spellings
|
|
3434
|
+
* can have two different subtrees. That is why the clean-subtree memo in
|
|
3435
|
+
* `findNestedTemplateTreeDefect` is keyed on the lexical path instead, and why
|
|
3436
|
+
* an identity repeat is a conservative refusal rather than an exact one: with
|
|
3437
|
+
* a symlinked directory AND a `..` row it can refuse a tree that would have
|
|
3438
|
+
* terminated. Such an assembly is hand-built by definition.
|
|
3439
|
+
*
|
|
3440
|
+
* Falls back to `path.resolve` for a directory that does not exist; the walk
|
|
3441
|
+
* stops there anyway.
|
|
3442
|
+
*/
|
|
3443
|
+
function templateIdentity(templatePath) {
|
|
3444
|
+
const dir = path.dirname(templatePath);
|
|
3445
|
+
let realDir;
|
|
3446
|
+
try {
|
|
3447
|
+
realDir = fs.realpathSync(dir);
|
|
3448
|
+
} catch {
|
|
3449
|
+
realDir = path.resolve(dir);
|
|
3450
|
+
}
|
|
3451
|
+
return path.join(realDir, path.basename(templatePath));
|
|
3452
|
+
}
|
|
3453
|
+
/**
|
|
3454
|
+
* The nested-stack rows of a parsed template that name a child template file.
|
|
3455
|
+
*
|
|
3456
|
+
* THE ONE SPELLING, shared by this module's walk and by
|
|
3457
|
+
* `NestedStackProvider.indexGrandchildTemplates`, because the walk is only a
|
|
3458
|
+
* guard if it follows exactly the rows the deploy follows. Two hand-written
|
|
3459
|
+
* copies disagreed on an ARRAY-valued `Resources`: `Object.entries` indexes an
|
|
3460
|
+
* array as `'0'`, `'1'`, ..., the deploy followed those rows, and a walk that
|
|
3461
|
+
* skipped them accepted a cyclic tree. So this takes whatever `Object.entries`
|
|
3462
|
+
* takes, and does not pre-judge the container's shape.
|
|
3463
|
+
*/
|
|
3464
|
+
function listNestedTemplateRows(template) {
|
|
3465
|
+
if (template === null || typeof template !== "object") return [];
|
|
3466
|
+
const resources = template.Resources;
|
|
3467
|
+
if (resources === null || resources === void 0) return [];
|
|
3468
|
+
const rows = [];
|
|
3469
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
3470
|
+
const row = resource;
|
|
3471
|
+
if (row?.Type !== NESTED_STACK_TYPE$1) continue;
|
|
3472
|
+
const assetPath = row.Metadata?.["aws:asset:path"];
|
|
3473
|
+
if (typeof assetPath !== "string" || assetPath.length === 0) continue;
|
|
3474
|
+
rows.push({
|
|
3475
|
+
logicalId,
|
|
3476
|
+
assetPath
|
|
3477
|
+
});
|
|
3478
|
+
}
|
|
3479
|
+
return rows;
|
|
3480
|
+
}
|
|
3481
|
+
/**
|
|
3482
|
+
* The rows of the template FILE at `templatePath`. Unreadable or unparseable
|
|
3483
|
+
* input yields `undefined`: reporting that is the job of the site that
|
|
3484
|
+
* actually loads the template, with its own message, and a walk that cannot
|
|
3485
|
+
* descend cannot loop.
|
|
3486
|
+
*/
|
|
3487
|
+
function readNestedRows(templatePath) {
|
|
3488
|
+
let parsed;
|
|
3489
|
+
try {
|
|
3490
|
+
parsed = JSON.parse(fs.readFileSync(templatePath, "utf-8"));
|
|
3491
|
+
} catch {
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
return listNestedTemplateRows(parsed);
|
|
3495
|
+
}
|
|
3496
|
+
/**
|
|
3497
|
+
* Deeper than this cannot deploy at all, so it is refused rather than walked.
|
|
3498
|
+
* A child's state key is `cdkd/<root>~<id>~<id>.../<region>/state.json`; each
|
|
3499
|
+
* level adds at least two bytes (`~` plus a one-character logical id) and S3
|
|
3500
|
+
* caps a key at 1024 bytes. The bound exists so a hostile chain of tens of
|
|
3501
|
+
* thousands of DISTINCT templates is a readable refusal instead of a
|
|
3502
|
+
* `RangeError` from the recursion below. It is not a cycle heuristic: a cycle
|
|
3503
|
+
* is refused at its first repeat, long before this.
|
|
3504
|
+
*/
|
|
3505
|
+
const MAX_NESTING_DEPTH = 512;
|
|
3506
|
+
/**
|
|
3507
|
+
* Rows the walk will follow before it refuses the tree as too large.
|
|
3508
|
+
*
|
|
3509
|
+
* The clean-subtree memo is keyed on the LEXICAL path (it has to be, see
|
|
3510
|
+
* `templateIdentity`), and symlinked directories give one file arbitrarily
|
|
3511
|
+
* many lexical spellings: with `d1 -> .` and `d2 -> .`, rows naming
|
|
3512
|
+
* `t.json`, `d1/t.json` and `d2/t.json` at every level multiply the spellings
|
|
3513
|
+
* per level while no chain ever repeats an identity. Such a walk is finite but
|
|
3514
|
+
* exponential, so it needs a budget rather than a better key. Far above any
|
|
3515
|
+
* real assembly: a CDK app's nested templates number in the tens, and a memo
|
|
3516
|
+
* hit costs one row, not a subtree.
|
|
3517
|
+
*/
|
|
3518
|
+
const MAX_ROWS_FOLLOWED = 1e4;
|
|
3519
|
+
/**
|
|
3520
|
+
* Walk every nested template reachable from `nestedTemplates` (logical id ->
|
|
3521
|
+
* template file path, the shape `AssemblyReader` and the provider's own
|
|
3522
|
+
* per-level index produce) and return the first defect, or `undefined` when
|
|
3523
|
+
* the tree is well-formed.
|
|
3524
|
+
*
|
|
3525
|
+
* `ancestorTemplatePaths` seeds the chain with templates ABOVE the entry rows,
|
|
3526
|
+
* for a caller that knows them. Detection does not depend on it: a cycle
|
|
3527
|
+
* through an unseeded ancestor is followed once around and refused when it
|
|
3528
|
+
* re-enters the first template the walk did see.
|
|
3529
|
+
*/
|
|
3530
|
+
function findNestedTemplateTreeDefect(nestedTemplates, ancestorTemplatePaths = []) {
|
|
3531
|
+
const onChain = /* @__PURE__ */ new Set();
|
|
3532
|
+
for (const p of ancestorTemplatePaths) onChain.add(templateIdentity(p));
|
|
3533
|
+
const clean = /* @__PURE__ */ new Set();
|
|
3534
|
+
const chain = [];
|
|
3535
|
+
let rowsFollowed = 0;
|
|
3536
|
+
const visit = (logicalId, templatePath) => {
|
|
3537
|
+
if (typeof templatePath !== "string") return void 0;
|
|
3538
|
+
const identity = templateIdentity(templatePath);
|
|
3539
|
+
const lexical = path.resolve(templatePath);
|
|
3540
|
+
chain.push({
|
|
3541
|
+
logicalId,
|
|
3542
|
+
templatePath: identity
|
|
3543
|
+
});
|
|
3544
|
+
try {
|
|
3545
|
+
if (onChain.has(identity)) return {
|
|
3546
|
+
kind: "cycle",
|
|
3547
|
+
chain: [...chain]
|
|
3548
|
+
};
|
|
3549
|
+
if (chain.length > 512) return {
|
|
3550
|
+
kind: "too-deep",
|
|
3551
|
+
chain: [...chain]
|
|
3552
|
+
};
|
|
3553
|
+
if (++rowsFollowed > 1e4) return {
|
|
3554
|
+
kind: "too-large",
|
|
3555
|
+
chain: [...chain]
|
|
3556
|
+
};
|
|
3557
|
+
if (clean.has(lexical)) return void 0;
|
|
3558
|
+
const rows = readNestedRows(templatePath);
|
|
3559
|
+
if (rows === void 0) return void 0;
|
|
3560
|
+
onChain.add(identity);
|
|
3561
|
+
try {
|
|
3562
|
+
const dir = path.dirname(templatePath);
|
|
3563
|
+
for (const row of rows) {
|
|
3564
|
+
if (isAbsoluteAssetPath(row.assetPath)) return {
|
|
3565
|
+
kind: "absolute-path",
|
|
3566
|
+
chain: [...chain],
|
|
3567
|
+
logicalId: row.logicalId,
|
|
3568
|
+
assetPath: row.assetPath
|
|
3569
|
+
};
|
|
3570
|
+
const defect = visit(row.logicalId, path.join(dir, row.assetPath));
|
|
3571
|
+
if (defect) return defect;
|
|
3572
|
+
}
|
|
3573
|
+
} finally {
|
|
3574
|
+
onChain.delete(identity);
|
|
3575
|
+
}
|
|
3576
|
+
clean.add(lexical);
|
|
3577
|
+
return;
|
|
3578
|
+
} finally {
|
|
3579
|
+
chain.pop();
|
|
3580
|
+
}
|
|
3581
|
+
};
|
|
3582
|
+
for (const [logicalId, templatePath] of Object.entries(nestedTemplates)) {
|
|
3583
|
+
const defect = visit(logicalId, templatePath);
|
|
3584
|
+
if (defect) return defect;
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
/** Hops rendered in full before the middle of a long chain is elided. */
|
|
3588
|
+
const MAX_RENDERED_HOPS = 8;
|
|
3589
|
+
/**
|
|
3590
|
+
* `'A' (/x/a.json) -> 'B' (/x/b.json)`. Every interpolation goes through
|
|
3591
|
+
* `displaySafe`: this text exists FOR a hand-modified assembly, so a logical id
|
|
3592
|
+
* (a template key) and a path (derived from `aws:asset:path`) are both
|
|
3593
|
+
* attacker-controlled, and a bare `Error` message is not sanitized downstream.
|
|
3594
|
+
* A long chain keeps both ends, since the entry row and the closing row are
|
|
3595
|
+
* the two a reader needs.
|
|
3596
|
+
*/
|
|
3597
|
+
function renderChain(chain) {
|
|
3598
|
+
const hop = (h) => `'${displaySafe(h.logicalId)}' (${displaySafe(h.templatePath)})`;
|
|
3599
|
+
if (chain.length <= MAX_RENDERED_HOPS) return chain.map(hop).join(" -> ");
|
|
3600
|
+
const keep = MAX_RENDERED_HOPS / 2;
|
|
3601
|
+
return [
|
|
3602
|
+
...chain.slice(0, keep).map(hop),
|
|
3603
|
+
`... ${chain.length - 8} more ...`,
|
|
3604
|
+
...chain.slice(-4).map(hop)
|
|
3605
|
+
].join(" -> ");
|
|
3606
|
+
}
|
|
3607
|
+
/**
|
|
3608
|
+
* The refusal text for a defect found under `stackName`. `action` completes
|
|
3609
|
+
* "Refusing to ..." so each caller states what it declined to do.
|
|
3610
|
+
*/
|
|
3611
|
+
function renderNestedTemplateTreeDefect(defect, stackName, action) {
|
|
3612
|
+
const provenance = `CDK emits an acyclic nested template tree with relative asset paths, so this indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to ${action}.`;
|
|
3613
|
+
const owner = (chain) => {
|
|
3614
|
+
const ids = chain.slice(0, -1).map((h) => h.logicalId);
|
|
3615
|
+
const keep = MAX_RENDERED_HOPS / 2;
|
|
3616
|
+
const shown = ids.length <= MAX_RENDERED_HOPS ? ids : [
|
|
3617
|
+
...ids.slice(0, keep),
|
|
3618
|
+
`...${ids.length - 8} more...`,
|
|
3619
|
+
...ids.slice(-4)
|
|
3620
|
+
];
|
|
3621
|
+
return displaySafe([stackName, ...shown].join("~"));
|
|
3622
|
+
};
|
|
3623
|
+
if (defect.kind === "cycle") {
|
|
3624
|
+
const closing = defect.chain[defect.chain.length - 1];
|
|
3625
|
+
return `The nested template tree under stack '${displaySafe(stackName)}' contains a cycle: ${renderChain(defect.chain)}. Nested stack '${displaySafe(closing.logicalId)}' (declared in stack '${owner(defect.chain)}') resolves to a template that is already on that nesting chain, so its Metadata['aws:asset:path'] closes a cycle. ${provenance}`;
|
|
3626
|
+
}
|
|
3627
|
+
if (defect.kind === "too-large") return `The nested template tree under stack '${displaySafe(stackName)}' has more than ${MAX_ROWS_FOLLOWED} nested-stack rows to follow (the walk stopped at ${renderChain(defect.chain)}). That is far beyond any CDK-generated assembly. Symlinked directories can give one template file many paths, which multiplies the tree without ever repeating on one chain. Refusing to ${action}.`;
|
|
3628
|
+
if (defect.kind === "too-deep") return `The nested template tree under stack '${displaySafe(stackName)}' nests more than ${512} levels deep: ${renderChain(defect.chain)}. No tree that deep can deploy, because each level lengthens the child's state key and S3 caps a key at 1024 bytes. ${provenance}`;
|
|
3629
|
+
return `The nested template tree under stack '${displaySafe(stackName)}' has nested stack '${displaySafe(defect.logicalId)}' (reached through ${renderChain(defect.chain)}) with Metadata['aws:asset:path']='${displaySafe(defect.assetPath)}' which is absolute. ${provenance}`;
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
//#endregion
|
|
3633
|
+
//#region src/cli/commands/nested-template-preflight.ts
|
|
3634
|
+
/**
|
|
3635
|
+
* Refuse a malformed nested-template tree (a cycle, an absolute
|
|
3636
|
+
* `aws:asset:path` below the top level, a chain too deep or a tree too large
|
|
3637
|
+
* to deploy) for every SELECTED stack, before `cdkd deploy` does anything a
|
|
3638
|
+
* refusal would have to undo (issue go-to-k/cdkd#3449).
|
|
3639
|
+
*
|
|
3640
|
+
* `NestedStackProvider` runs the same walk per nested-stack row
|
|
3641
|
+
* (go-to-k/cdkd#3247), which is the earliest point the provider owns: by then
|
|
3642
|
+
* the stack's assets are published, its lock is held, and any root resource
|
|
3643
|
+
* that does not depend on the nested row may already be created and must be
|
|
3644
|
+
* rolled back. Everything the walk reads is on disk as soon as synth returns,
|
|
3645
|
+
* so the CLI asks first and the deploy never starts. The provider call stays
|
|
3646
|
+
* as the guard for a context this command did not build.
|
|
3647
|
+
*
|
|
3648
|
+
* Read-only and synchronous: a few file reads per nested template, none at all
|
|
3649
|
+
* for a stack without nested-stack rows. One walk covers all of a stack's
|
|
3650
|
+
* top-level rows, so the walker's row budget is per STACK here and per row in
|
|
3651
|
+
* the provider; no CDK-generated assembly comes near either.
|
|
3652
|
+
*
|
|
3653
|
+
* The root template's own path is not seeded into the walk (`StackInfo` does
|
|
3654
|
+
* not carry it). Detection does not depend on the seed: a child that points
|
|
3655
|
+
* back at the root template is followed once around and refused when the walk
|
|
3656
|
+
* re-enters the first nested template it saw.
|
|
3657
|
+
*
|
|
3658
|
+
* `SynthesisError` because the defect is in the synthesized assembly, like the
|
|
3659
|
+
* absolute-path refusal `AssemblyReader` raises for a top-level row.
|
|
3660
|
+
* `markNonRetryable` because the message carries template-controlled text the
|
|
3661
|
+
* substring-matching retry classifier must never read, wherever the error ends
|
|
3662
|
+
* up being caught.
|
|
3663
|
+
*/
|
|
3664
|
+
function refuseMalformedNestedTemplateTrees(stacks) {
|
|
3665
|
+
for (const stack of stacks) {
|
|
3666
|
+
if (!stack.nestedTemplates) continue;
|
|
3667
|
+
const defect = findNestedTemplateTreeDefect(stack.nestedTemplates);
|
|
3668
|
+
if (!defect) continue;
|
|
3669
|
+
throw markNonRetryable(new SynthesisError(renderNestedTemplateTreeDefect(defect, stack.stackName, "start the deploy; nothing has been published or provisioned")));
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3385
3673
|
//#endregion
|
|
3386
3674
|
//#region src/cli/commands/confirm-prompt.ts
|
|
3387
3675
|
/**
|
|
@@ -16655,6 +16943,216 @@ function isRecord$1(value) {
|
|
|
16655
16943
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16656
16944
|
}
|
|
16657
16945
|
|
|
16946
|
+
//#endregion
|
|
16947
|
+
//#region src/provisioning/dynamodb-contributor-insights.ts
|
|
16948
|
+
/**
|
|
16949
|
+
* The `AWS::DynamoDB::Table` `ContributorInsightsSpecification` rules, in ONE
|
|
16950
|
+
* spelling for the table-level block and the per-index
|
|
16951
|
+
* (`GlobalSecondaryIndexes[].ContributorInsightsSpecification`) one (issue
|
|
16952
|
+
* [#1782](https://github.com/go-to-k/cdkd/issues/1782)).
|
|
16953
|
+
*
|
|
16954
|
+
* Neither block rides `CreateTable` / `UpdateTable`: DynamoDB applies it
|
|
16955
|
+
* through `UpdateContributorInsights` and reports it through
|
|
16956
|
+
* `DescribeContributorInsights`, and both calls address an index by adding
|
|
16957
|
+
* `IndexName`. The SDK's `GlobalSecondaryIndex` shape has NO such member, so a
|
|
16958
|
+
* per-index block left inside the forwarded index entry is discarded by the
|
|
16959
|
+
* serializer — a template asking for per-index Contributor Insights deployed
|
|
16960
|
+
* green with the feature off.
|
|
16961
|
+
*
|
|
16962
|
+
* Pure, so the write plan and the drift read-back gate cannot answer "does
|
|
16963
|
+
* this index declare the block" differently.
|
|
16964
|
+
*/
|
|
16965
|
+
/** The CFn property name, shared by the table-level and the per-index block. */
|
|
16966
|
+
const CONTRIBUTOR_INSIGHTS_KEY = "ContributorInsightsSpecification";
|
|
16967
|
+
/**
|
|
16968
|
+
* Read a `ContributorInsightsSpecification` block.
|
|
16969
|
+
*
|
|
16970
|
+
* An ABSENT `Enabled` reads as `false` — the member is required by the CFn
|
|
16971
|
+
* schema, and disabling is the answer that cannot start a billed feature.
|
|
16972
|
+
*/
|
|
16973
|
+
function readContributorInsightsSpec(value, path) {
|
|
16974
|
+
if (value === void 0 || value === null) return { kind: "absent" };
|
|
16975
|
+
const refusal = configBooleanRefusal(value, "Enabled", path) ?? configStringRefusal(value, "Mode", "", path);
|
|
16976
|
+
if (refusal !== void 0) return {
|
|
16977
|
+
kind: "unusable",
|
|
16978
|
+
reason: refusal
|
|
16979
|
+
};
|
|
16980
|
+
const block = value;
|
|
16981
|
+
const enabled = coerceCfnBoolean(block["Enabled"]) ?? false;
|
|
16982
|
+
const mode = block["Mode"];
|
|
16983
|
+
return typeof mode === "string" && mode.length > 0 ? {
|
|
16984
|
+
kind: "usable",
|
|
16985
|
+
enabled,
|
|
16986
|
+
mode
|
|
16987
|
+
} : {
|
|
16988
|
+
kind: "usable",
|
|
16989
|
+
enabled
|
|
16990
|
+
};
|
|
16991
|
+
}
|
|
16992
|
+
/**
|
|
16993
|
+
* The call one block needs, given the block the PREVIOUS side recorded.
|
|
16994
|
+
*
|
|
16995
|
+
* - `absent` with a DECLARED previous (usable or not) is a template REMOVAL and
|
|
16996
|
+
* DISABLES — the rule this provider's other post-ACTIVE blocks (PITR, TTL,
|
|
16997
|
+
* Kinesis) and the table-level block already follow. Because the rule is
|
|
16998
|
+
* symmetric in its two sides, a rollback — which replays `update()` with the
|
|
16999
|
+
* sides SWAPPED — restores the old setting through the same arms.
|
|
17000
|
+
* - `unusable` issues NOTHING and reports through `onUnusable`: the live
|
|
17001
|
+
* setting is left alone rather than overwritten with a guess.
|
|
17002
|
+
* - An unchanged usable block issues nothing. `mode` takes part only while
|
|
17003
|
+
* enabled, since it is never sent with `DISABLE`.
|
|
17004
|
+
*/
|
|
17005
|
+
function planContributorInsightsOp(desired, previous, onUnusable) {
|
|
17006
|
+
if (desired.kind === "unusable") {
|
|
17007
|
+
onUnusable(desired.reason);
|
|
17008
|
+
return;
|
|
17009
|
+
}
|
|
17010
|
+
if (desired.kind === "absent") return previous.kind === "absent" ? void 0 : { action: "DISABLE" };
|
|
17011
|
+
if (previous.kind === "usable" && previous.enabled === desired.enabled && (!desired.enabled || previous.mode === desired.mode)) return;
|
|
17012
|
+
if (!desired.enabled) return { action: "DISABLE" };
|
|
17013
|
+
return desired.mode === void 0 ? { action: "ENABLE" } : {
|
|
17014
|
+
action: "ENABLE",
|
|
17015
|
+
mode: desired.mode
|
|
17016
|
+
};
|
|
17017
|
+
}
|
|
17018
|
+
/**
|
|
17019
|
+
* The path a per-index refusal names. Deliberately WITHOUT the index name: the
|
|
17020
|
+
* name is a RESOLVED template value, the caller's message already carries it
|
|
17021
|
+
* through a whole-value mask, and a copy embedded here would reach only the
|
|
17022
|
+
* substring mask, which ignores a name shorter than its minimum needle.
|
|
17023
|
+
*/
|
|
17024
|
+
const INDEX_CONTRIBUTOR_INSIGHTS_PATH = `GlobalSecondaryIndexes[].${CONTRIBUTOR_INSIGHTS_KEY}`;
|
|
17025
|
+
/** Index a CFn `GlobalSecondaryIndexes` value by `IndexName`. */
|
|
17026
|
+
function indexEntriesByName(value) {
|
|
17027
|
+
const byName = /* @__PURE__ */ new Map();
|
|
17028
|
+
if (!Array.isArray(value)) return byName;
|
|
17029
|
+
for (const entry of value) {
|
|
17030
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
17031
|
+
const name = entry["IndexName"];
|
|
17032
|
+
if (typeof name === "string") byName.set(name, entry);
|
|
17033
|
+
}
|
|
17034
|
+
return byName;
|
|
17035
|
+
}
|
|
17036
|
+
/**
|
|
17037
|
+
* The per-index calls an index list needs, in DESIRED order.
|
|
17038
|
+
*
|
|
17039
|
+
* Only an index the DESIRED list still names can produce a call: an index the
|
|
17040
|
+
* update removed takes its Contributor Insights rules with it, and a call
|
|
17041
|
+
* naming it fails with `ResourceNotFoundException`. An index that is NEW in
|
|
17042
|
+
* the desired list has an `absent` previous side, so its declared block is
|
|
17043
|
+
* always applied.
|
|
17044
|
+
*/
|
|
17045
|
+
function planIndexContributorInsightsOps(desiredIndexes, previousIndexes, onUnusable) {
|
|
17046
|
+
const previousByName = indexEntriesByName(previousIndexes);
|
|
17047
|
+
const ops = [];
|
|
17048
|
+
for (const [indexName, entry] of indexEntriesByName(desiredIndexes)) {
|
|
17049
|
+
const path = INDEX_CONTRIBUTOR_INSIGHTS_PATH;
|
|
17050
|
+
const op = planContributorInsightsOp(readContributorInsightsSpec(entry[CONTRIBUTOR_INSIGHTS_KEY], path), readContributorInsightsSpec(previousByName.get(indexName)?.[CONTRIBUTOR_INSIGHTS_KEY], path), (reason) => onUnusable(indexName, reason));
|
|
17051
|
+
if (op !== void 0) ops.push({
|
|
17052
|
+
indexName,
|
|
17053
|
+
...op
|
|
17054
|
+
});
|
|
17055
|
+
}
|
|
17056
|
+
return ops;
|
|
17057
|
+
}
|
|
17058
|
+
/**
|
|
17059
|
+
* Every refusal a CREATE must raise before `CreateTable` goes out: the
|
|
17060
|
+
* table-level block's and each index's. A create has no live setting to leave
|
|
17061
|
+
* alone, so an unreadable block there is a template error, not a skip.
|
|
17062
|
+
*/
|
|
17063
|
+
function contributorInsightsRefusals(tableBlock, indexes) {
|
|
17064
|
+
const refusals = [];
|
|
17065
|
+
const table = readContributorInsightsSpec(tableBlock, CONTRIBUTOR_INSIGHTS_KEY);
|
|
17066
|
+
if (table.kind === "unusable") refusals.push(table.reason);
|
|
17067
|
+
if (!Array.isArray(indexes)) return refusals;
|
|
17068
|
+
indexes.forEach((entry, position) => {
|
|
17069
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return;
|
|
17070
|
+
const read = readContributorInsightsSpec(entry[CONTRIBUTOR_INSIGHTS_KEY], `GlobalSecondaryIndexes[${position}].${CONTRIBUTOR_INSIGHTS_KEY}`);
|
|
17071
|
+
if (read.kind === "unusable") refusals.push(read.reason);
|
|
17072
|
+
});
|
|
17073
|
+
return refusals;
|
|
17074
|
+
}
|
|
17075
|
+
/**
|
|
17076
|
+
* One index entry WITHOUT its `ContributorInsightsSpecification`, for the
|
|
17077
|
+
* `CreateTable` forward.
|
|
17078
|
+
*
|
|
17079
|
+
* Explicit so nothing relies on the SDK serializer discarding a member its
|
|
17080
|
+
* `GlobalSecondaryIndex` shape does not have. Returns the entry UNCHANGED —
|
|
17081
|
+
* identity included — when there is nothing to strip, and never mutates it: the
|
|
17082
|
+
* bag is the caller's resolved template, which the engine records into state.
|
|
17083
|
+
*/
|
|
17084
|
+
function stripIndexContributorInsights(entry) {
|
|
17085
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
|
|
17086
|
+
if (!("ContributorInsightsSpecification" in entry)) return entry;
|
|
17087
|
+
const { [CONTRIBUTOR_INSIGHTS_KEY]: _stripped, ...rest } = entry;
|
|
17088
|
+
return rest;
|
|
17089
|
+
}
|
|
17090
|
+
/**
|
|
17091
|
+
* Does this DESIRED index entry declare a block cdkd would act on — the gate
|
|
17092
|
+
* for the per-index drift read-back, which costs one API call per index?
|
|
17093
|
+
*
|
|
17094
|
+
* The same reader the write plan runs, so an `unusable` block (never sent) is
|
|
17095
|
+
* never read back either.
|
|
17096
|
+
*/
|
|
17097
|
+
function indexDeclaresContributorInsights(desiredEntry) {
|
|
17098
|
+
return readContributorInsightsSpec(desiredEntry?.[CONTRIBUTOR_INSIGHTS_KEY], "").kind === "usable";
|
|
17099
|
+
}
|
|
17100
|
+
/** The setting a `ContributorInsightsStatus` stands for, if any. */
|
|
17101
|
+
function enabledForStatus(status, transientAsTarget) {
|
|
17102
|
+
if (status === "ENABLED" || transientAsTarget && status === "ENABLING") return true;
|
|
17103
|
+
if (status === "DISABLED" || transientAsTarget && status === "DISABLING") return false;
|
|
17104
|
+
}
|
|
17105
|
+
/**
|
|
17106
|
+
* Reverse-map the TABLE-level `DescribeContributorInsights` answer to the CFn
|
|
17107
|
+
* block, or `undefined` when there is nothing stable to report.
|
|
17108
|
+
*
|
|
17109
|
+
* Only the terminal statuses are mapped, and `Mode` is emitted whenever AWS
|
|
17110
|
+
* reports one for an enabled table — the block is a TOP-LEVEL key, which the
|
|
17111
|
+
* comparator walks by the baseline's own keys. `Mode` is never emitted for a
|
|
17112
|
+
* disabled block: it is not sent with `DISABLE`, and a disabled block carrying
|
|
17113
|
+
* one is a CFn-invalid placeholder.
|
|
17114
|
+
*/
|
|
17115
|
+
function reverseMapTableContributorInsights(status, mode) {
|
|
17116
|
+
const enabled = enabledForStatus(status, false);
|
|
17117
|
+
if (enabled === void 0) return void 0;
|
|
17118
|
+
const block = { Enabled: enabled };
|
|
17119
|
+
if (enabled && mode !== void 0) block["Mode"] = mode;
|
|
17120
|
+
return block;
|
|
17121
|
+
}
|
|
17122
|
+
/**
|
|
17123
|
+
* Reverse-map one PER-INDEX `DescribeContributorInsights` answer, shaped by
|
|
17124
|
+
* the block the DESIRED entry declares. `undefined` when there is nothing to
|
|
17125
|
+
* report or the declared block is not one cdkd acts on.
|
|
17126
|
+
*
|
|
17127
|
+
* The per-index block sits inside an ARRAY entry, which the comparator matches
|
|
17128
|
+
* as a whole and by exact key set, so everything here exists to make the
|
|
17129
|
+
* read-back equal the declaration whenever AWS holds what was declared:
|
|
17130
|
+
*
|
|
17131
|
+
* - `ENABLING` / `DISABLING` read as their TARGET. The deploy does not wait
|
|
17132
|
+
* for the toggle, so the post-deploy `observedProperties` capture usually
|
|
17133
|
+
* sees `ENABLING`; omitting the block would freeze an entry the settled
|
|
17134
|
+
* index can never equal.
|
|
17135
|
+
* - `Mode` is emitted only when the declared block carries one: AWS reports a
|
|
17136
|
+
* mode for every enabled rule set (its default when none was sent). While
|
|
17137
|
+
* `ENABLING`, a mode AWS does not report yet reads as the declared one —
|
|
17138
|
+
* the one that was just sent.
|
|
17139
|
+
* - `Enabled` keeps the declared SPELLING when the template wrote a string
|
|
17140
|
+
* (`'true'`), since the comparator does not coerce.
|
|
17141
|
+
*/
|
|
17142
|
+
function reverseMapIndexContributorInsights(status, mode, declaredBlock) {
|
|
17143
|
+
const declared = readContributorInsightsSpec(declaredBlock, INDEX_CONTRIBUTOR_INSIGHTS_PATH);
|
|
17144
|
+
if (declared.kind !== "usable") return void 0;
|
|
17145
|
+
const enabled = enabledForStatus(status, true);
|
|
17146
|
+
if (enabled === void 0) return void 0;
|
|
17147
|
+
const declaredEnabled = declaredBlock["Enabled"];
|
|
17148
|
+
const block = { Enabled: typeof declaredEnabled !== "string" ? enabled : declared.enabled === enabled ? declaredEnabled : String(enabled) };
|
|
17149
|
+
if (enabled && declared.mode !== void 0) {
|
|
17150
|
+
const liveMode = mode ?? (status === "ENABLING" ? declared.mode : void 0);
|
|
17151
|
+
if (liveMode !== void 0) block["Mode"] = liveMode;
|
|
17152
|
+
}
|
|
17153
|
+
return block;
|
|
17154
|
+
}
|
|
17155
|
+
|
|
16658
17156
|
//#endregion
|
|
16659
17157
|
//#region src/provisioning/dynamodb-index-busy-delete.ts
|
|
16660
17158
|
/**
|
|
@@ -18479,16 +18977,10 @@ function warmThroughputAlreadyMatches(desired, live) {
|
|
|
18479
18977
|
* to decide with and every block is emitted, cleaned of its AWS-managed
|
|
18480
18978
|
* members.
|
|
18481
18979
|
*
|
|
18482
|
-
*
|
|
18483
|
-
*
|
|
18484
|
-
*
|
|
18485
|
-
*
|
|
18486
|
-
* ALSO dropped on the write side, since `create()` forwards the CFn blob to
|
|
18487
|
-
* `CreateTable` and the SDK serializer discards the unknown member — still
|
|
18488
|
-
* true after round 6 taught that path to coerce `WarmThroughput`, because the
|
|
18489
|
-
* entry is rebuilt with a SPREAD and every other member, known or not, rides
|
|
18490
|
-
* along unchanged. Filed as issue
|
|
18491
|
-
* [#1782](https://github.com/go-to-k/cdkd/issues/1782).
|
|
18980
|
+
* CFn's per-index `ContributorInsightsSpecification` is NOT mapped here: it
|
|
18981
|
+
* has no `DescribeTable` counterpart, so `readCurrentState` adds it afterwards
|
|
18982
|
+
* from a gated per-index `DescribeContributorInsights` (issue
|
|
18983
|
+
* [#1782](https://github.com/go-to-k/cdkd/issues/1782)).
|
|
18492
18984
|
*/
|
|
18493
18985
|
function reverseMapSecondaryIndex(live, desired, bagInformative) {
|
|
18494
18986
|
const out = {};
|
|
@@ -18754,6 +19246,10 @@ var DynamoDBTableProvider = class {
|
|
|
18754
19246
|
let tableCreated = false;
|
|
18755
19247
|
try {
|
|
18756
19248
|
const billingMode = requireConfigString(properties["BillingMode"], "PROVISIONED", "AWS::DynamoDB::Table BillingMode", replayWarn(this.logger, context));
|
|
19249
|
+
if (context?.replayingState !== true) {
|
|
19250
|
+
const [refusal] = contributorInsightsRefusals(properties["ContributorInsightsSpecification"], properties["GlobalSecondaryIndexes"]);
|
|
19251
|
+
if (refusal !== void 0) throw new Error(`${refusal}. Fix the template value`);
|
|
19252
|
+
}
|
|
18757
19253
|
const createParams = {
|
|
18758
19254
|
TableName: tableName,
|
|
18759
19255
|
KeySchema: keySchema,
|
|
@@ -18778,7 +19274,7 @@ var DynamoDBTableProvider = class {
|
|
|
18778
19274
|
};
|
|
18779
19275
|
if (properties["GlobalSecondaryIndexes"]) {
|
|
18780
19276
|
const declaredGsis = properties["GlobalSecondaryIndexes"];
|
|
18781
|
-
createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexThroughputForCreate(logicalId, entry, maskSecrets)) : declaredGsis;
|
|
19277
|
+
createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexThroughputForCreate(logicalId, stripIndexContributorInsights(entry), maskSecrets)) : declaredGsis;
|
|
18782
19278
|
}
|
|
18783
19279
|
if (properties["LocalSecondaryIndexes"]) createParams.LocalSecondaryIndexes = properties["LocalSecondaryIndexes"];
|
|
18784
19280
|
const sse = mapSSESpecification(properties["SSESpecification"]);
|
|
@@ -18795,7 +19291,8 @@ var DynamoDBTableProvider = class {
|
|
|
18795
19291
|
await this.applyPointInTimeRecovery(tableName, properties["PointInTimeRecoverySpecification"], void 0, maskSecrets);
|
|
18796
19292
|
await this.applyTimeToLive(tableName, properties["TimeToLiveSpecification"]);
|
|
18797
19293
|
await this.applyKinesisStreamingDestination(tableName, properties["KinesisStreamSpecification"], void 0, maskSecrets);
|
|
18798
|
-
await this.applyContributorInsights(tableName, properties["ContributorInsightsSpecification"]);
|
|
19294
|
+
await this.applyContributorInsights(tableName, properties["ContributorInsightsSpecification"], void 0, maskSecrets);
|
|
19295
|
+
await this.applyIndexContributorInsights(tableName, properties["GlobalSecondaryIndexes"], void 0, maskSecrets);
|
|
18799
19296
|
this.logger.debug(`Successfully created DynamoDB table ${logicalId}: ${tableName}`);
|
|
18800
19297
|
return {
|
|
18801
19298
|
physicalId: tableName,
|
|
@@ -19030,7 +19527,8 @@ var DynamoDBTableProvider = class {
|
|
|
19030
19527
|
await this.applyResourcePolicy(table.TableArn, properties["ResourcePolicy"], previousProperties["ResourcePolicy"]);
|
|
19031
19528
|
}
|
|
19032
19529
|
if (JSON.stringify(properties["KinesisStreamSpecification"]) !== JSON.stringify(previousProperties["KinesisStreamSpecification"])) await this.applyKinesisStreamingDestination(physicalId, properties["KinesisStreamSpecification"], previousProperties["KinesisStreamSpecification"], maskSecrets);
|
|
19033
|
-
if (JSON.stringify(properties["ContributorInsightsSpecification"]) !== JSON.stringify(previousProperties["ContributorInsightsSpecification"])) await this.applyContributorInsights(physicalId, properties["ContributorInsightsSpecification"], previousProperties["ContributorInsightsSpecification"]);
|
|
19530
|
+
if (JSON.stringify(properties["ContributorInsightsSpecification"]) !== JSON.stringify(previousProperties["ContributorInsightsSpecification"])) await this.applyContributorInsights(physicalId, properties["ContributorInsightsSpecification"], previousProperties["ContributorInsightsSpecification"], maskSecrets);
|
|
19531
|
+
await this.applyIndexContributorInsights(physicalId, properties["GlobalSecondaryIndexes"], previousProperties["GlobalSecondaryIndexes"], maskSecrets);
|
|
19034
19532
|
return {
|
|
19035
19533
|
physicalId,
|
|
19036
19534
|
wasReplaced: false,
|
|
@@ -19446,24 +19944,53 @@ var DynamoDBTableProvider = class {
|
|
|
19446
19944
|
*
|
|
19447
19945
|
* Called from both `create()` (after the table is ACTIVE) and `update()`
|
|
19448
19946
|
* (only when the value changed). On `update()`-side removal — template drops
|
|
19449
|
-
* the block but it was present before — insights is disabled.
|
|
19947
|
+
* the block but it was present before — insights is disabled. The block is
|
|
19948
|
+
* read by the reader the per-index twin uses
|
|
19949
|
+
* ({@link applyIndexContributorInsights}), so the two cannot disagree about
|
|
19950
|
+
* one shape: an unreadable block is announced and SKIPPED rather than read as
|
|
19951
|
+
* a default (`Boolean('false')` used to ENABLE what the template disabled).
|
|
19450
19952
|
*/
|
|
19451
|
-
async applyContributorInsights(tableName, spec, previousSpec) {
|
|
19452
|
-
|
|
19453
|
-
|
|
19454
|
-
if (
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
|
|
19953
|
+
async applyContributorInsights(tableName, spec, previousSpec, maskSecrets) {
|
|
19954
|
+
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
19955
|
+
const op = planContributorInsightsOp(readContributorInsightsSpec(spec, CONTRIBUTOR_INSIGHTS_KEY), readContributorInsightsSpec(previousSpec, CONTRIBUTOR_INSIGHTS_KEY), (reason) => warn(`DynamoDB table ${tableName}: ${reason}, so Contributor Insights was left as it is. Fix the template value and re-deploy.`));
|
|
19956
|
+
if (op === void 0) return;
|
|
19957
|
+
await this.sendContributorInsightsOp(tableName, op, maskSecrets);
|
|
19958
|
+
}
|
|
19959
|
+
/**
|
|
19960
|
+
* Apply every per-index `ContributorInsightsSpecification`
|
|
19961
|
+
* (`GlobalSecondaryIndexes[].ContributorInsightsSpecification`, issue
|
|
19962
|
+
* [#1782](https://github.com/go-to-k/cdkd/issues/1782)) — the table-level
|
|
19963
|
+
* call with `IndexName` added, one call per index whose block changed.
|
|
19964
|
+
*
|
|
19965
|
+
* The block is not a member of the SDK's `GlobalSecondaryIndex`, so until
|
|
19966
|
+
* this existed it was dropped by the serializer on `CreateTable` and never
|
|
19967
|
+
* sent by `applyGsiUpdates`: the deploy was green and the feature was off.
|
|
19968
|
+
*
|
|
19969
|
+
* `UpdateContributorInsights` answers `ResourceNotFoundException` for an
|
|
19970
|
+
* index that is not ACTIVE yet, which {@link retryOnTransientControlPlane}
|
|
19971
|
+
* does not retry, so the index wait runs FIRST — and only when there is a
|
|
19972
|
+
* call to make, so an ordinary update pays no extra `DescribeTable`.
|
|
19973
|
+
* `previousIndexes` is `undefined` on `create()`. Removal and rollback
|
|
19974
|
+
* semantics live on {@link planContributorInsightsOp}.
|
|
19975
|
+
*/
|
|
19976
|
+
async applyIndexContributorInsights(tableName, desiredIndexes, previousIndexes, maskSecrets) {
|
|
19977
|
+
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
19978
|
+
const ops = planIndexContributorInsightsOps(desiredIndexes, previousIndexes, (indexName, reason) => warn(`${this.indexScopeAt(indexName, tableName, maskSecrets)}: ${reason}, so its Contributor Insights was left as it is. Fix the template value and re-deploy.`));
|
|
19979
|
+
if (ops.length === 0) return;
|
|
19980
|
+
await this.waitForTableAndIndexesActive(tableName);
|
|
19981
|
+
for (const op of ops) await this.sendContributorInsightsOp(tableName, op, maskSecrets);
|
|
19982
|
+
}
|
|
19983
|
+
/** Issue ONE `UpdateContributorInsights`, against the table or one index. */
|
|
19984
|
+
async sendContributorInsightsOp(tableName, op, maskSecrets) {
|
|
19985
|
+
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
19986
|
+
const target = op.indexName === void 0 ? `DynamoDB table ${tableName}` : this.indexScopeAt(op.indexName, tableName, maskSecrets);
|
|
19461
19987
|
await this.retryOnTransientControlPlane(() => this.dynamoDBClient.send(new UpdateContributorInsightsCommand({
|
|
19462
19988
|
TableName: tableName,
|
|
19463
|
-
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19989
|
+
...op.indexName !== void 0 ? { IndexName: op.indexName } : {},
|
|
19990
|
+
ContributorInsightsAction: op.action,
|
|
19991
|
+
...op.mode !== void 0 ? { ContributorInsightsMode: op.mode } : {}
|
|
19992
|
+
})), `set ContributorInsights on ${target}`);
|
|
19993
|
+
debug(`Set ContributorInsightsAction=${op.action}${op.mode !== void 0 ? ` Mode=${op.mode}` : ""} on ${target}`);
|
|
19467
19994
|
}
|
|
19468
19995
|
/**
|
|
19469
19996
|
* Poll DescribeTable until the table reaches ACTIVE status
|
|
@@ -20444,32 +20971,35 @@ var DynamoDBTableProvider = class {
|
|
|
20444
20971
|
* level: it is order-SIGNIFICANT (HASH before RANGE), so sorting it would
|
|
20445
20972
|
* HIDE a real key change rather than remove a phantom one.
|
|
20446
20973
|
*
|
|
20447
|
-
* `GlobalSecondaryIndexes` / `LocalSecondaryIndexes`
|
|
20448
|
-
*
|
|
20449
|
-
* both
|
|
20450
|
-
*
|
|
20451
|
-
*
|
|
20452
|
-
* the parent's path (`drift-normalize.ts`),
|
|
20453
|
-
*
|
|
20454
|
-
*
|
|
20455
|
-
*
|
|
20456
|
-
*
|
|
20457
|
-
*
|
|
20458
|
-
* [#
|
|
20459
|
-
*
|
|
20460
|
-
*
|
|
20461
|
-
*
|
|
20462
|
-
*
|
|
20463
|
-
*
|
|
20464
|
-
*
|
|
20465
|
-
*
|
|
20466
|
-
*
|
|
20467
|
-
*
|
|
20468
|
-
* `observedProperties` path, where both sides come from this same readback.
|
|
20974
|
+
* `GlobalSecondaryIndexes` / `LocalSecondaryIndexes` ARE declared, in the
|
|
20975
|
+
* LEAF-ONLY form (`LEAF_ONLY_PATH_SUFFIX`, `[]`, from
|
|
20976
|
+
* [#1799](https://github.com/go-to-k/cdkd/pull/1799)): both are sets keyed
|
|
20977
|
+
* by `IndexName`, and `DescribeTable` does not return them in the template's
|
|
20978
|
+
* order. A plain SUBTREE entry would be wrong — the unordered walk descends
|
|
20979
|
+
* into array elements giving each the parent's path (`drift-normalize.ts`),
|
|
20980
|
+
* so `'GlobalSecondaryIndexes'` would also sort each index's `KeySchema` and
|
|
20981
|
+
* reverse the sentence above at the index level. The leaf-only form sorts
|
|
20982
|
+
* the list and stops there.
|
|
20983
|
+
*
|
|
20984
|
+
* Measured on real AWS by issue
|
|
20985
|
+
* [#1782](https://github.com/go-to-k/cdkd/issues/1782)'s two-index fixture:
|
|
20986
|
+
* a table created with `[giA, giB]` read back as `[giB, giA]`, so a
|
|
20987
|
+
* `properties` baseline reported whole-list drift although every member
|
|
20988
|
+
* round-tripped. The phantom pre-dates that issue — the #1767 arm only ever
|
|
20989
|
+
* had ONE index — and was not reachable against an `observedProperties`
|
|
20990
|
+
* baseline, where both sides come from the same readback. Both comparison
|
|
20991
|
+
* sides are sorted, by a key-order-independent serialization of the whole
|
|
20992
|
+
* entry, so a baseline captured in AWS's order by an earlier binary keeps
|
|
20993
|
+
* converging; a REAL difference still reports the list, since entries that
|
|
20994
|
+
* differ cannot be made equal by reordering them.
|
|
20469
20995
|
*/
|
|
20470
20996
|
getDriftUnorderedPaths(resourceType) {
|
|
20471
20997
|
if (resourceType !== "AWS::DynamoDB::Table") return [];
|
|
20472
|
-
return [
|
|
20998
|
+
return [
|
|
20999
|
+
"AttributeDefinitions",
|
|
21000
|
+
"GlobalSecondaryIndexes[]",
|
|
21001
|
+
"LocalSecondaryIndexes[]"
|
|
21002
|
+
];
|
|
20473
21003
|
}
|
|
20474
21004
|
/**
|
|
20475
21005
|
* `WarmThroughput` is reported by AWS for EVERY table, whether or not the
|
|
@@ -20614,7 +21144,9 @@ var DynamoDBTableProvider = class {
|
|
|
20614
21144
|
* caller passes). It is consulted for the table-level `WarmThroughput` — see
|
|
20615
21145
|
* {@link getDriftUnknownPaths} for the measurement and the reasoning — and,
|
|
20616
21146
|
* since issue #1767, for the per-index throughput blocks of each secondary
|
|
20617
|
-
* index, matched by `IndexName`.
|
|
21147
|
+
* index, matched by `IndexName`. Since issue #1782 it also gates the per-index
|
|
21148
|
+
* `ContributorInsightsSpecification` read, which costs one
|
|
21149
|
+
* `DescribeContributorInsights` per declaring index.
|
|
20618
21150
|
*/
|
|
20619
21151
|
async readCurrentState(physicalId, _logicalId, _resourceType, properties) {
|
|
20620
21152
|
try {
|
|
@@ -20713,15 +21245,31 @@ var DynamoDBTableProvider = class {
|
|
|
20713
21245
|
}
|
|
20714
21246
|
try {
|
|
20715
21247
|
const ciResp = await this.dynamoDBClient.send(new DescribeContributorInsightsCommand({ TableName: physicalId }));
|
|
20716
|
-
const
|
|
20717
|
-
if (
|
|
20718
|
-
const cspec = { Enabled: status === "ENABLED" };
|
|
20719
|
-
if (status === "ENABLED" && ciResp.ContributorInsightsMode !== void 0) cspec["Mode"] = ciResp.ContributorInsightsMode;
|
|
20720
|
-
result["ContributorInsightsSpecification"] = cspec;
|
|
20721
|
-
}
|
|
21248
|
+
const cspec = reverseMapTableContributorInsights(ciResp.ContributorInsightsStatus, ciResp.ContributorInsightsMode);
|
|
21249
|
+
if (cspec !== void 0) result["ContributorInsightsSpecification"] = cspec;
|
|
20722
21250
|
} catch (err) {
|
|
20723
21251
|
this.logger.debug(`Could not read ContributorInsights for ${physicalId}: ${describeAwsFailure(err).detail}`);
|
|
20724
21252
|
}
|
|
21253
|
+
const emittedGsis = result["GlobalSecondaryIndexes"];
|
|
21254
|
+
if (bagInformative && Array.isArray(emittedGsis)) {
|
|
21255
|
+
const desiredByName = desiredIndexEntriesByName(properties?.["GlobalSecondaryIndexes"]);
|
|
21256
|
+
for (const emitted of emittedGsis) {
|
|
21257
|
+
const indexName = emitted["IndexName"];
|
|
21258
|
+
if (typeof indexName !== "string") continue;
|
|
21259
|
+
const desiredEntry = desiredByName.get(indexName);
|
|
21260
|
+
if (!indexDeclaresContributorInsights(desiredEntry)) continue;
|
|
21261
|
+
try {
|
|
21262
|
+
const indexResp = await this.dynamoDBClient.send(new DescribeContributorInsightsCommand({
|
|
21263
|
+
TableName: physicalId,
|
|
21264
|
+
IndexName: indexName
|
|
21265
|
+
}));
|
|
21266
|
+
const block = reverseMapIndexContributorInsights(indexResp.ContributorInsightsStatus, indexResp.ContributorInsightsMode, desiredEntry?.[CONTRIBUTOR_INSIGHTS_KEY]);
|
|
21267
|
+
if (block !== void 0) emitted[CONTRIBUTOR_INSIGHTS_KEY] = block;
|
|
21268
|
+
} catch (err) {
|
|
21269
|
+
this.logger.debug(`Could not read ContributorInsights for index ${indexName} of ${physicalId}: ${describeAwsFailure(err).detail}`);
|
|
21270
|
+
}
|
|
21271
|
+
}
|
|
21272
|
+
}
|
|
20725
21273
|
return result;
|
|
20726
21274
|
} catch (err) {
|
|
20727
21275
|
if (err instanceof ResourceNotFoundException$1) return void 0;
|
|
@@ -26356,7 +26904,8 @@ var SSMParameterProvider = class {
|
|
|
26356
26904
|
* question cdkd can answer correctly — and would hand `cdkd drift` a baseline
|
|
26357
26905
|
* AWS does not hold. Dropping the `Arn` instead degrades to the resolver's LOUD
|
|
26358
26906
|
* `*Arn` shape-guard failure, which is exactly the pre-fix behavior, and the
|
|
26359
|
-
* resource's next UPDATE re-records it
|
|
26907
|
+
* resource's next UPDATE re-records it (as does the deploy engine's re-read
|
|
26908
|
+
* on a `Fn::GetAtt` miss, issue #1852). A loud missing value beats a quiet
|
|
26360
26909
|
* wrong one — the same reasoning the `fabricated`-account arm above applies.
|
|
26361
26910
|
*
|
|
26362
26911
|
* Two honest bounds on that "next update re-records it". `getAccountInfo`
|
|
@@ -41121,10 +41670,14 @@ var RDSProvider = class {
|
|
|
41121
41670
|
async importDBInstance(input) {
|
|
41122
41671
|
const explicit = resolveExplicitPhysicalId(input, "DBInstanceIdentifier");
|
|
41123
41672
|
if (explicit) try {
|
|
41124
|
-
await this.getClient().send(new DescribeDBInstancesCommand({ DBInstanceIdentifier: explicit }));
|
|
41673
|
+
const described = (await this.getClient().send(new DescribeDBInstancesCommand({ DBInstanceIdentifier: explicit }))).DBInstances?.[0];
|
|
41125
41674
|
return {
|
|
41126
41675
|
physicalId: explicit,
|
|
41127
|
-
attributes: {
|
|
41676
|
+
attributes: definedAttributes({
|
|
41677
|
+
"Endpoint.Address": described?.Endpoint?.Address,
|
|
41678
|
+
"Endpoint.Port": stringifyIfAssigned(described?.Endpoint?.Port),
|
|
41679
|
+
Arn: described?.DBInstanceArn
|
|
41680
|
+
})
|
|
41128
41681
|
};
|
|
41129
41682
|
} catch (err) {
|
|
41130
41683
|
if (err.name === "DBInstanceNotFoundFault") return null;
|
|
@@ -42714,10 +43267,14 @@ var DocDBProvider = class {
|
|
|
42714
43267
|
async importDBInstance(input) {
|
|
42715
43268
|
const explicit = resolveExplicitPhysicalId(input, "DBInstanceIdentifier");
|
|
42716
43269
|
if (explicit) try {
|
|
42717
|
-
await this.getClient().send(new DescribeDBInstancesCommand$1({ DBInstanceIdentifier: explicit }));
|
|
43270
|
+
const described = (await this.getClient().send(new DescribeDBInstancesCommand$1({ DBInstanceIdentifier: explicit }))).DBInstances?.[0];
|
|
42718
43271
|
return {
|
|
42719
43272
|
physicalId: explicit,
|
|
42720
|
-
attributes: {
|
|
43273
|
+
attributes: definedAttributes({
|
|
43274
|
+
"Endpoint.Address": described?.Endpoint?.Address,
|
|
43275
|
+
"Endpoint.Port": stringifyIfAssigned(described?.Endpoint?.Port),
|
|
43276
|
+
Arn: described?.DBInstanceArn
|
|
43277
|
+
})
|
|
42721
43278
|
};
|
|
42722
43279
|
} catch (err) {
|
|
42723
43280
|
if (err.name === "DBInstanceNotFoundFault") return null;
|
|
@@ -43357,10 +43914,14 @@ var NeptuneProvider = class {
|
|
|
43357
43914
|
async importDBInstance(input) {
|
|
43358
43915
|
const explicit = resolveExplicitPhysicalId(input, "DBInstanceIdentifier");
|
|
43359
43916
|
if (explicit) try {
|
|
43360
|
-
await this.getClient().send(new DescribeDBInstancesCommand$2({ DBInstanceIdentifier: explicit }));
|
|
43917
|
+
const described = (await this.getClient().send(new DescribeDBInstancesCommand$2({ DBInstanceIdentifier: explicit }))).DBInstances?.[0];
|
|
43361
43918
|
return {
|
|
43362
43919
|
physicalId: explicit,
|
|
43363
|
-
attributes: {
|
|
43920
|
+
attributes: definedAttributes({
|
|
43921
|
+
"Endpoint.Address": described?.Endpoint?.Address,
|
|
43922
|
+
"Endpoint.Port": stringifyIfAssigned(described?.Endpoint?.Port),
|
|
43923
|
+
Arn: described?.DBInstanceArn
|
|
43924
|
+
})
|
|
43364
43925
|
};
|
|
43365
43926
|
} catch (err) {
|
|
43366
43927
|
if (err.name === "DBInstanceNotFoundFault") return null;
|
|
@@ -48076,7 +48637,7 @@ var AppSyncProvider = class {
|
|
|
48076
48637
|
return await this.buildAppSyncArn(suffix, regionOverride);
|
|
48077
48638
|
} catch (error) {
|
|
48078
48639
|
if (!omitArnOnFailure) throw error;
|
|
48079
|
-
this.logger.warn(`Imported ${resourceType} (${physicalId}) but its ARN attribute was NOT recorded: ${describeAwsFailure(error).detail}. Ref will resolve to the compound physical id, and Fn::GetAtt on the ARN will fail, until the resource's next update heals the record.`);
|
|
48640
|
+
this.logger.warn(`Imported ${resourceType} (${physicalId}) but its ARN attribute was NOT recorded: ${describeAwsFailure(error).detail}. Ref will resolve to the compound physical id, and Fn::GetAtt on the ARN will fail, until the resource's next update — or a later deploy that needs the attribute and re-reads it from AWS — heals the record.`);
|
|
48080
48641
|
return;
|
|
48081
48642
|
}
|
|
48082
48643
|
};
|
|
@@ -48164,7 +48725,7 @@ var AppSyncProvider = class {
|
|
|
48164
48725
|
try {
|
|
48165
48726
|
return await this.childRefAttributes(resourceType, physicalId, region, true) ?? {};
|
|
48166
48727
|
} catch (error) {
|
|
48167
|
-
this.logger.warn(`Imported ${resourceType} (${physicalId}) but could not build its ARN attribute: ${describeAwsFailure(error).detail}. Ref will resolve to the compound physical id, and Fn::GetAtt on the ARN will fail, until the resource's next update heals the record.`);
|
|
48728
|
+
this.logger.warn(`Imported ${resourceType} (${physicalId}) but could not build its ARN attribute: ${describeAwsFailure(error).detail}. Ref will resolve to the compound physical id, and Fn::GetAtt on the ARN will fail, until the resource's next update — or a later deploy that needs the attribute and re-reads it from AWS — heals the record.`);
|
|
48168
48729
|
return {};
|
|
48169
48730
|
}
|
|
48170
48731
|
}
|
|
@@ -63555,7 +64116,7 @@ function nestedStackChildFailureMessage(childStackName, errorCount, alsoSkippedC
|
|
|
63555
64116
|
* `path.isAbsolute()` alone. Exported for unit testing.
|
|
63556
64117
|
*/
|
|
63557
64118
|
function isAbsoluteCrossPlatform$1(p) {
|
|
63558
|
-
return
|
|
64119
|
+
return isAbsoluteAssetPath(p);
|
|
63559
64120
|
}
|
|
63560
64121
|
/**
|
|
63561
64122
|
* Provider for `AWS::CloudFormation::Stack` — cdkd's recursive nested-stack
|
|
@@ -63642,14 +64203,14 @@ var NestedStackProvider = class {
|
|
|
63642
64203
|
const ctx = this.requireContext();
|
|
63643
64204
|
this.requireDeployContext(ctx, "create");
|
|
63644
64205
|
const childTemplatePath = ctx.nestedTemplates[logicalId];
|
|
63645
|
-
if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' under parent '${ctx.parentStackName}'. Verify the synth output emits Metadata['aws:asset:path'] on this resource (CDK 2.x cdk.NestedStack does so by default).`);
|
|
63646
|
-
|
|
64206
|
+
if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${displaySafe(logicalId)}' under parent '${displaySafe(ctx.parentStackName)}'. Verify the synth output emits Metadata['aws:asset:path'] on this resource (CDK 2.x cdk.NestedStack does so by default).`);
|
|
64207
|
+
this.refuseMalformedNestedTemplateTree(ctx, logicalId, childTemplatePath);
|
|
64208
|
+
const { template: childTemplate, grandchildTemplates } = this.readChildTemplate(childTemplatePath, ctx.assetRedirect);
|
|
63647
64209
|
const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
|
|
63648
64210
|
const childRegion = ctx.parentRegion;
|
|
63649
64211
|
const childParameters = this.extractParameters(properties);
|
|
63650
|
-
const grandchildTemplates = this.indexGrandchildTemplates(childTemplate, childTemplatePath);
|
|
63651
64212
|
const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
|
|
63652
|
-
this.logger.info(`Deploying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
|
|
64213
|
+
this.logger.info(`Deploying nested stack ${displaySafe(childStackName)} (logicalId=${displaySafe(logicalId)}, ${resourceCount} resource(s))`);
|
|
63653
64214
|
await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
|
|
63654
64215
|
const childOutputs = await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion);
|
|
63655
64216
|
return {
|
|
@@ -63662,14 +64223,14 @@ var NestedStackProvider = class {
|
|
|
63662
64223
|
const ctx = this.requireContext();
|
|
63663
64224
|
this.requireDeployContext(ctx, "update");
|
|
63664
64225
|
const childTemplatePath = ctx.nestedTemplates[logicalId];
|
|
63665
|
-
if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${logicalId}' on update.`);
|
|
63666
|
-
|
|
64226
|
+
if (!childTemplatePath) throw new Error(`Nested template file not found for AWS::CloudFormation::Stack '${displaySafe(logicalId)}' on update.`);
|
|
64227
|
+
this.refuseMalformedNestedTemplateTree(ctx, logicalId, childTemplatePath);
|
|
64228
|
+
const { template: childTemplate, grandchildTemplates } = this.readChildTemplate(childTemplatePath, ctx.assetRedirect);
|
|
63667
64229
|
const childStackName = this.deriveChildStackName(ctx.parentStackName, logicalId);
|
|
63668
64230
|
const childRegion = ctx.parentRegion;
|
|
63669
64231
|
const childParameters = this.extractParameters(properties);
|
|
63670
|
-
const grandchildTemplates = this.indexGrandchildTemplates(childTemplate, childTemplatePath);
|
|
63671
64232
|
const resourceCount = Object.keys(childTemplate.Resources ?? {}).length;
|
|
63672
|
-
this.logger.info(`Updating nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
|
|
64233
|
+
this.logger.info(`Updating nested stack ${displaySafe(childStackName)} (logicalId=${displaySafe(logicalId)}, ${resourceCount} resource(s))`);
|
|
63673
64234
|
await this.runChildDeploy(ctx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates);
|
|
63674
64235
|
const updatedOutputs = await this.readChildOutputsAsAttributes(ctx, childStackName, childRegion);
|
|
63675
64236
|
return {
|
|
@@ -63685,12 +64246,12 @@ var NestedStackProvider = class {
|
|
|
63685
64246
|
const childRegion = ctx.parentRegion;
|
|
63686
64247
|
const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
|
|
63687
64248
|
if (!childStateData) {
|
|
63688
|
-
this.logger.debug(`Nested stack ${childStackName} has no state — treating delete as idempotent success.`);
|
|
64249
|
+
this.logger.debug(`Nested stack ${displaySafe(childStackName)} has no state — treating delete as idempotent success.`);
|
|
63689
64250
|
return;
|
|
63690
64251
|
}
|
|
63691
64252
|
refuseMalformedResourcesForDestroy(childStateData.state, childStackName, childRegion);
|
|
63692
64253
|
const resourceCount = Object.keys(childStateData.state.resources).length;
|
|
63693
|
-
this.logger.info(`Destroying nested stack ${childStackName} (logicalId=${logicalId}, ${resourceCount} resource(s))`);
|
|
64254
|
+
this.logger.info(`Destroying nested stack ${displaySafe(childStackName)} (logicalId=${displaySafe(logicalId)}, ${resourceCount} resource(s))`);
|
|
63694
64255
|
const childCtx = {
|
|
63695
64256
|
...ctx,
|
|
63696
64257
|
parentStackName: childStackName,
|
|
@@ -63726,12 +64287,12 @@ var NestedStackProvider = class {
|
|
|
63726
64287
|
if (childResult.interrupted) causes.push("was interrupted");
|
|
63727
64288
|
return {
|
|
63728
64289
|
outcome: "skipped",
|
|
63729
|
-
reason: `nested stack ${childStackName} ${causes.join(" and ")}`
|
|
64290
|
+
reason: `nested stack ${displaySafe(childStackName)} ${causes.join(" and ")}`
|
|
63730
64291
|
};
|
|
63731
64292
|
}
|
|
63732
64293
|
}
|
|
63733
64294
|
async getAttribute(_physicalId, _resourceType, attributeName) {
|
|
63734
|
-
throw new Error(`AWS::CloudFormation::Stack: attribute '${attributeName}' is not in the recorded Outputs map. Only 'Outputs.<Key>' references to declared Output names on the child template are supported.`);
|
|
64295
|
+
throw new Error(`AWS::CloudFormation::Stack: attribute '${displaySafe(attributeName)}' is not in the recorded Outputs map. Only 'Outputs.<Key>' references to declared Output names on the child template are supported.`);
|
|
63735
64296
|
}
|
|
63736
64297
|
async runChildDeploy(parentCtx, logicalId, childStackName, childRegion, childTemplate, childParameters, grandchildTemplates) {
|
|
63737
64298
|
const inheritedSecrets = getCurrentResourceSecrets();
|
|
@@ -63782,7 +64343,7 @@ var NestedStackProvider = class {
|
|
|
63782
64343
|
*/
|
|
63783
64344
|
async readChildOutputsAsAttributes(ctx, childStackName, childRegion) {
|
|
63784
64345
|
const childStateData = await ctx.stateBackend.getState(childStackName, childRegion);
|
|
63785
|
-
if (!childStateData) throw new Error(`Child stack state '${childStackName}' not found after deploy — NestedStackProvider invariant violated.`);
|
|
64346
|
+
if (!childStateData) throw new Error(`Child stack state '${displaySafe(childStackName)}' not found after deploy — NestedStackProvider invariant violated.`);
|
|
63786
64347
|
refuseMalformedNestedChildOutputs(childStateData.state, childStackName, childRegion);
|
|
63787
64348
|
const attributes = this.buildOutputsAttributes(childStateData.state.outputs ?? {});
|
|
63788
64349
|
const noEchoAttributeNames = [];
|
|
@@ -63807,6 +64368,40 @@ var NestedStackProvider = class {
|
|
|
63807
64368
|
requireDeployContext(ctx, op) {
|
|
63808
64369
|
if (!ctx.nestedTemplates || !ctx.dagBuilder || !ctx.diffCalculator) throw new Error(`NestedStackProvider.${op}: deploy-mode context fields (nestedTemplates / dagBuilder / diffCalculator) are missing. This usually means a destroy-mode entry point called into create/update by mistake.`);
|
|
63809
64370
|
}
|
|
64371
|
+
/**
|
|
64372
|
+
* Validate the WHOLE nested-template subtree under this row before the first
|
|
64373
|
+
* child engine is built (issue go-to-k/cdkd#3247).
|
|
64374
|
+
*
|
|
64375
|
+
* Up front rather than as an ancestor chain threaded through the recursion,
|
|
64376
|
+
* because here a level is not a read, it is a DEPLOY: refusing at the first
|
|
64377
|
+
* repeat would leave every level above it provisioned, with a state record
|
|
64378
|
+
* each, for the rollback to unwind. Every file the recursion would follow is
|
|
64379
|
+
* already on disk, so the top-most row's call sees the entire cycle and
|
|
64380
|
+
* nothing below it ever starts. Deeper levels re-run the same walk over
|
|
64381
|
+
* their own (already proven) subtree; it is a few file reads next to a
|
|
64382
|
+
* deploy, and it keeps the guard independent of who built the context.
|
|
64383
|
+
*
|
|
64384
|
+
* This is the earliest point THIS FILE owns. The parent stack's other
|
|
64385
|
+
* resources may already be in flight when its nested-stack row is
|
|
64386
|
+
* dispatched; they are unwound by the parent's ordinary rollback. No level of
|
|
64387
|
+
* the cyclic tree is deployed.
|
|
64388
|
+
*
|
|
64389
|
+
* It also reports an absolute `aws:asset:path` at ANY depth, for the same
|
|
64390
|
+
* reason: `indexGrandchildTemplates` refuses one too, but only on reaching
|
|
64391
|
+
* that level, after the levels above it deployed. That check stays as the
|
|
64392
|
+
* per-level backstop.
|
|
64393
|
+
*
|
|
64394
|
+
* `markNonRetryable`: the verdict is computed from files no retry changes,
|
|
64395
|
+
* while the message carries template-controlled text (logical ids, paths)
|
|
64396
|
+
* that the SUBSTRING-matching retry classifier would otherwise read. Latent
|
|
64397
|
+
* while `disableOuterRetry` is set, and declared anyway, like the sibling
|
|
64398
|
+
* marks in this file.
|
|
64399
|
+
*/
|
|
64400
|
+
refuseMalformedNestedTemplateTree(ctx, logicalId, childTemplatePath) {
|
|
64401
|
+
const defect = findNestedTemplateTreeDefect({ [logicalId]: childTemplatePath });
|
|
64402
|
+
if (!defect) return;
|
|
64403
|
+
throw markNonRetryable(/* @__PURE__ */ new Error(`NestedStackProvider: ${renderNestedTemplateTreeDefect(defect, ctx.parentStackName, "deploy any level of it")}`));
|
|
64404
|
+
}
|
|
63810
64405
|
deriveChildStackName(parentStackName, nestedLogicalId) {
|
|
63811
64406
|
return `${parentStackName}~${nestedLogicalId}`;
|
|
63812
64407
|
}
|
|
@@ -63851,35 +64446,50 @@ var NestedStackProvider = class {
|
|
|
63851
64446
|
* points at `[2]` rather than at the whole parameter.
|
|
63852
64447
|
*/
|
|
63853
64448
|
refuseNonScalarParameter(k, offender, where) {
|
|
63854
|
-
throw new Error(`NestedStackProvider: child Parameter '${k}'${where} resolved to a non-scalar value (type=${offender === null ? "null" : typeof offender}). Parameters must be scalars (string / number / boolean), or an ARRAY of them from a list-typed parameter, by the time they reach the provider — an unresolved intrinsic here means IntrinsicFunctionResolver upstream did not handle the value, which is a bug. Surface the unresolved input rather than silently coercing to '[object Object]'.`);
|
|
64449
|
+
throw new Error(`NestedStackProvider: child Parameter '${displaySafe(k)}'${where} resolved to a non-scalar value (type=${offender === null ? "null" : typeof offender}). Parameters must be scalars (string / number / boolean), or an ARRAY of them from a list-typed parameter, by the time they reach the provider — an unresolved intrinsic here means IntrinsicFunctionResolver upstream did not handle the value, which is a bug. Surface the unresolved input rather than silently coercing to '[object Object]'.`);
|
|
63855
64450
|
}
|
|
64451
|
+
/**
|
|
64452
|
+
* The child template as the child engine deploys it, plus the grandchild
|
|
64453
|
+
* template paths indexed from it.
|
|
64454
|
+
*
|
|
64455
|
+
* The index is taken BEFORE the asset-reference rewrite, and returning both
|
|
64456
|
+
* from here is what keeps that order from being undone by a caller. The
|
|
64457
|
+
* rewrite walks every string in the template, `Metadata['aws:asset:path']`
|
|
64458
|
+
* included, and a path segment that spells a bootstrap bucket name is
|
|
64459
|
+
* rewritten like any other occurrence. `aws:asset:path` is a LOCAL file
|
|
64460
|
+
* path, never an asset-storage reference; indexing after the rewrite made
|
|
64461
|
+
* the deploy follow a different file than the one
|
|
64462
|
+
* `refuseMalformedNestedTemplateTree` had validated from the raw file, which
|
|
64463
|
+
* is a way around that guard (issue go-to-k/cdkd#3247).
|
|
64464
|
+
*/
|
|
63856
64465
|
readChildTemplate(templatePath, assetRedirect) {
|
|
63857
64466
|
let raw;
|
|
63858
64467
|
try {
|
|
63859
64468
|
raw = fs.readFileSync(templatePath, "utf-8");
|
|
63860
64469
|
} catch (err) {
|
|
63861
|
-
throw new Error(`Failed to read nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
64470
|
+
throw new Error(`Failed to read nested template at ${displaySafe(templatePath)}: ${displaySafe(err instanceof Error ? err.message : String(err))}`);
|
|
63862
64471
|
}
|
|
63863
64472
|
let template;
|
|
63864
64473
|
try {
|
|
63865
64474
|
template = JSON.parse(raw);
|
|
63866
64475
|
} catch (err) {
|
|
63867
|
-
throw new Error(`Failed to parse nested template at ${templatePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
64476
|
+
throw new Error(`Failed to parse nested template at ${displaySafe(templatePath)}: ${displaySafe(err instanceof Error ? err.message : String(err))}`);
|
|
63868
64477
|
}
|
|
64478
|
+
const grandchildTemplates = this.indexGrandchildTemplates(template, templatePath);
|
|
63869
64479
|
if (assetRedirect) {
|
|
63870
64480
|
const n = rewriteTemplateAssetReferences(template, assetRedirect);
|
|
63871
|
-
if (n > 0) this.logger.debug(`Rewrote ${n} asset reference(s) to cdkd asset storage in nested template ${templatePath}`);
|
|
64481
|
+
if (n > 0) this.logger.debug(`Rewrote ${n} asset reference(s) to cdkd asset storage in nested template ${displaySafe(templatePath)}`);
|
|
63872
64482
|
}
|
|
63873
|
-
return
|
|
64483
|
+
return {
|
|
64484
|
+
template,
|
|
64485
|
+
grandchildTemplates
|
|
64486
|
+
};
|
|
63874
64487
|
}
|
|
63875
64488
|
indexGrandchildTemplates(childTemplate, childTemplatePath) {
|
|
63876
64489
|
const dir = path.dirname(childTemplatePath);
|
|
63877
|
-
const result =
|
|
63878
|
-
for (const
|
|
63879
|
-
if (
|
|
63880
|
-
const assetPath = resource.Metadata?.["aws:asset:path"];
|
|
63881
|
-
if (typeof assetPath !== "string" || assetPath.length === 0) continue;
|
|
63882
|
-
if (isAbsoluteCrossPlatform$1(assetPath)) throw new Error(`NestedStackProvider: nested-stack '${grandLogicalId}' has Metadata['aws:asset:path']='${assetPath}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`);
|
|
64490
|
+
const result = Object.create(null);
|
|
64491
|
+
for (const { logicalId: grandLogicalId, assetPath } of listNestedTemplateRows(childTemplate)) {
|
|
64492
|
+
if (isAbsoluteCrossPlatform$1(assetPath)) throw markNonRetryable(/* @__PURE__ */ new Error(`NestedStackProvider: nested-stack '${displaySafe(grandLogicalId)}' has Metadata['aws:asset:path']='${displaySafe(assetPath)}' which is absolute. CDK emits relative asset paths for nested templates; an absolute path indicates the synth output was hand-modified or generated by a non-CDK toolchain. Refusing to load.`));
|
|
63883
64493
|
result[grandLogicalId] = path.join(dir, assetPath);
|
|
63884
64494
|
}
|
|
63885
64495
|
return result;
|
|
@@ -64931,6 +65541,7 @@ async function deployCommand(stacks, options) {
|
|
|
64931
65541
|
strict: options.strict === true,
|
|
64932
65542
|
ignoreErrors: options.ignoreErrors === true
|
|
64933
65543
|
});
|
|
65544
|
+
refuseMalformedNestedTemplateTrees(targetStacks);
|
|
64934
65545
|
await synthesizer.expandMacrosForStacks(targetStacks, synthOptions);
|
|
64935
65546
|
const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
|
|
64936
65547
|
const stsClient = new STSClient({
|
|
@@ -81555,6 +82166,15 @@ function safeRoleArn(value) {
|
|
|
81555
82166
|
* the Snapshot label would otherwise promise a final snapshot the run is about
|
|
81556
82167
|
* to skip — a data-loss-relevant lie in the one preview the user reads.
|
|
81557
82168
|
*/
|
|
82169
|
+
/**
|
|
82170
|
+
* The type(s) a reversed replacement touches (issue #2668): `NEW -> OLD` when
|
|
82171
|
+
* the replacement changed the resource's `Type` — the replay deletes the first
|
|
82172
|
+
* and re-creates the second — and the single type otherwise.
|
|
82173
|
+
*/
|
|
82174
|
+
function replacementTypes(op) {
|
|
82175
|
+
const routing = resolveReplacementOldType(op);
|
|
82176
|
+
return routing.ok && routing.oldType !== op.resourceType ? `${safe(op.resourceType)} -> ${safe(routing.oldType)}` : safe(op.resourceType);
|
|
82177
|
+
}
|
|
81558
82178
|
function actionLabel(item, skipFinalSnapshot) {
|
|
81559
82179
|
const { op, action, replacement } = item;
|
|
81560
82180
|
const rep = replacement ? " [replacement occurred, best-effort revert]" : "";
|
|
@@ -81564,11 +82184,15 @@ function actionLabel(item, skipFinalSnapshot) {
|
|
|
81564
82184
|
case "orphan-retain": return ` - orphan ${safe(op.logicalId)} (${safe(op.resourceType)}) [DeletionPolicy Retain — left in AWS]`;
|
|
81565
82185
|
case "orphan-flag": return ` - orphan ${safe(op.logicalId)} (${safe(op.resourceType)}) [--orphan]`;
|
|
81566
82186
|
case "revert": return ` - revert ${safe(op.logicalId)} (${safe(op.resourceType)})${rep}`;
|
|
81567
|
-
case "reverse-replacement": return item.retainsNewResource ? ` - reverse-replace ${safe(op.logicalId)} (${
|
|
81568
|
-
case "reverse-replacement-readopt": return item.retainsNewResource ? ` - reverse-replace ${safe(op.logicalId)} (${
|
|
82187
|
+
case "reverse-replacement": return item.retainsNewResource ? ` - reverse-replace ${safe(op.logicalId)} (${replacementTypes(op)}) [re-create old resource; new one RETAINED (UpdateReplacePolicy: Retain) and left untracked — REFUSED instead if the re-create collides with the name the retained resource still holds]` : ` - reverse-replace ${safe(op.logicalId)} (${replacementTypes(op)}) [re-create old resource, delete new]`;
|
|
82188
|
+
case "reverse-replacement-readopt": return item.retainsNewResource ? ` - reverse-replace ${safe(op.logicalId)} (${replacementTypes(op)}) [re-adopt retained old resource; new one RETAINED (UpdateReplacePolicy: Retain) and left untracked]` : ` - reverse-replace ${safe(op.logicalId)} (${replacementTypes(op)}) [delete new, re-adopt retained old resource]`;
|
|
81569
82189
|
case "unrecoverable-delete": return ` - (cannot restore) ${safe(op.logicalId)} (${safe(op.resourceType)}) — was DELETED, unrecoverable`;
|
|
81570
82190
|
case "skip-mismatch": return ` - skip ${safe(op.logicalId)} (${safe(op.resourceType)}) — physical id changed, needs manual attention`;
|
|
81571
82191
|
case "skip-absent": return ` - skip ${safe(op.logicalId)} (${safe(op.resourceType)}) — no longer in state`;
|
|
82192
|
+
case "refuse-replacement-routing": {
|
|
82193
|
+
const routing = resolveReplacementOldType(op);
|
|
82194
|
+
return ` - (REFUSED) ${safe(op.logicalId)} (${safe(op.resourceType)}) — cannot reverse the replacement: ${routing.ok ? "its old type could not be routed" : routing.reason}`;
|
|
82195
|
+
}
|
|
81572
82196
|
case "skip-already-done": return ` - skip ${safe(op.logicalId)} (${safe(op.resourceType)}) — already reverted`;
|
|
81573
82197
|
}
|
|
81574
82198
|
}
|
|
@@ -81588,6 +82212,7 @@ function failedActionLabel(item, skipFinalSnapshot) {
|
|
|
81588
82212
|
case "skip-failed-unknown": return ` - skip ${safe(op.logicalId)} (${safe(op.resourceType)}) — failed CREATE recorded no physical id`;
|
|
81589
82213
|
case "skip-failed-noop": return ` - skip ${safe(op.logicalId)} (${safe(op.resourceType)}) — failed ${safe(op.changeType)} left nothing to revert`;
|
|
81590
82214
|
case "skip-failed-absent": return ` - skip ${safe(op.logicalId)} (${safe(op.resourceType)}) — no previous state available`;
|
|
82215
|
+
case "skip-failed-type-change": return ` - skip ${safe(op.logicalId)} (${safe(op.previousState?.resourceType)} -> ${safe(op.resourceType)}) — failed Type change is a replacement, no in-place revert exists`;
|
|
81591
82216
|
}
|
|
81592
82217
|
}
|
|
81593
82218
|
/**
|
|
@@ -84616,6 +85241,368 @@ function createForceUnlockCommand() {
|
|
|
84616
85241
|
return cmd;
|
|
84617
85242
|
}
|
|
84618
85243
|
|
|
85244
|
+
//#endregion
|
|
85245
|
+
//#region src/cli/commands/import-deployed-parameters.ts
|
|
85246
|
+
/**
|
|
85247
|
+
* UNVERIFIABLE INPUTS for `cdkd import`'s observed-baseline refusal (issue
|
|
85248
|
+
* [#2854](https://github.com/go-to-k/cdkd/issues/2854)).
|
|
85249
|
+
*
|
|
85250
|
+
* `cdkd import` accepts no parameter values, so every template parameter is
|
|
85251
|
+
* bound to its `Default`. When the resources were deployed by a CloudFormation
|
|
85252
|
+
* stack, the value CloudFormation actually used can differ — and the ordinary
|
|
85253
|
+
* secret-carrying shape is a placeholder `Default` (`CHANGEME`) over a DEPLOYED
|
|
85254
|
+
* value of `{{resolve:secretsmanager:...}}`. The persisted `properties` then
|
|
85255
|
+
* spell no reference anywhere, the position-based redaction of the observed
|
|
85256
|
+
* capture has no evidence, and the decrypted readback would be persisted.
|
|
85257
|
+
*
|
|
85258
|
+
* The remedy here is deliberately PREMISE-INDEPENDENT. It never looks for a
|
|
85259
|
+
* `{{resolve:` opener in what `DescribeStacks` returns (AWS documents
|
|
85260
|
+
* `ParameterValue` as "the input value", which says the reference comes back
|
|
85261
|
+
* literally, but nothing in this module relies on it). It asks one question —
|
|
85262
|
+
* "is the deployed value PROVABLY the value this import bound?" — and treats
|
|
85263
|
+
* every other answer as divergent: a literal reference, the `****` a `NoEcho`
|
|
85264
|
+
* parameter is masked to, a value AWS resolved, a parameter missing from the
|
|
85265
|
+
* response, a `DescribeStacks` that failed. All of those differ from the bound
|
|
85266
|
+
* placeholder, so all of them refuse.
|
|
85267
|
+
*
|
|
85268
|
+
* THE DEPLOYED VALUES ARE COMPARISON-ONLY. They are never bound into the
|
|
85269
|
+
* resolve, never logged, never put in an error message, and never persisted:
|
|
85270
|
+
* binding them is the one design that would leak if AWS ever returned a
|
|
85271
|
+
* resolved value. {@link DeployedParameters} holds them in a `#private` field so
|
|
85272
|
+
* that a stray `JSON.stringify` / `util.inspect` / template literal of the
|
|
85273
|
+
* object renders nothing, and an SDK failure is reported by its error NAME
|
|
85274
|
+
* alone, because an SDK message can echo request input.
|
|
85275
|
+
*
|
|
85276
|
+
* TAINT THROUGH ANOTHER RESOURCE'S ATTRIBUTES IS FOLLOWED within one stack: the
|
|
85277
|
+
* walk records the target of every `Fn::GetAtt` / `${A.Attr}`, and the caller
|
|
85278
|
+
* closes the refused set over those targets, so a resource reading an attribute
|
|
85279
|
+
* of a refused resource (`{Fn::GetAtt: [SsmParam, Value]}` into a Lambda
|
|
85280
|
+
* environment — what CDK's `param.stringValue` emits) is refused with it. A
|
|
85281
|
+
* plain `Ref` of a resource is NOT followed: it yields the physical id, and a
|
|
85282
|
+
* secret used as a resource NAME is already disclosed by the identifier itself.
|
|
85283
|
+
*
|
|
85284
|
+
* KNOWN RESIDUAL, not followed: taint ACROSS the nested-stack boundary. A
|
|
85285
|
+
* parent property reading `Fn::GetAtt: [Child, Outputs.X]`, where the child's
|
|
85286
|
+
* output derives from a divergent CHILD parameter, is judged on the parent's
|
|
85287
|
+
* own parameters only — the root is resolved before any child is read.
|
|
85288
|
+
*/
|
|
85289
|
+
/**
|
|
85290
|
+
* What CloudFormation reports a stack was deployed with. Opaque on purpose:
|
|
85291
|
+
* the only question it answers is {@link DeployedParameters.provablyEquals}.
|
|
85292
|
+
*/
|
|
85293
|
+
var DeployedParameters = class DeployedParameters {
|
|
85294
|
+
#entries;
|
|
85295
|
+
constructor(entries) {
|
|
85296
|
+
this.#entries = entries;
|
|
85297
|
+
}
|
|
85298
|
+
/** `DescribeStacks` answered; `entries` is its `Parameters` list. */
|
|
85299
|
+
static fromDescribeStacks(parameters) {
|
|
85300
|
+
const entries = /* @__PURE__ */ new Map();
|
|
85301
|
+
for (const parameter of parameters) {
|
|
85302
|
+
if (typeof parameter.ParameterKey !== "string") continue;
|
|
85303
|
+
entries.set(parameter.ParameterKey, {
|
|
85304
|
+
value: parameter.ParameterValue,
|
|
85305
|
+
resolvedValue: parameter.ResolvedValue
|
|
85306
|
+
});
|
|
85307
|
+
}
|
|
85308
|
+
return new DeployedParameters(entries);
|
|
85309
|
+
}
|
|
85310
|
+
/** The deployed values could not be read: NOTHING is provable. */
|
|
85311
|
+
static unavailable() {
|
|
85312
|
+
return new DeployedParameters(void 0);
|
|
85313
|
+
}
|
|
85314
|
+
/**
|
|
85315
|
+
* Whether the deployed value of `name` is provably the value the import
|
|
85316
|
+
* BOUND. The deployed string goes through the SAME coercion the resolver
|
|
85317
|
+
* applied to the bound one (`Number`, `CommaDelimitedList`, `List<...>`), so
|
|
85318
|
+
* `'1'` equals a bound `1` and `'a, b'` equals a bound `['a', 'b']`. For an
|
|
85319
|
+
* SSM-typed parameter the resolver binds the RESOLVED value, so that is the
|
|
85320
|
+
* side compared — `ParameterValue` is only the key there.
|
|
85321
|
+
*/
|
|
85322
|
+
provablyEquals(name, type, bound) {
|
|
85323
|
+
const entry = this.#entries?.get(name);
|
|
85324
|
+
if (entry === void 0 || typeof type !== "string") return false;
|
|
85325
|
+
const ssmType = type.startsWith("AWS::SSM::Parameter::Value");
|
|
85326
|
+
const deployed = ssmType ? entry.resolvedValue : entry.value;
|
|
85327
|
+
if (typeof deployed !== "string") return false;
|
|
85328
|
+
const comparedType = ssmType ? ssmResolvedValueType(type) : type;
|
|
85329
|
+
return stableEquals(comparedType === void 0 ? deployed : coerceParameterTypedValue(deployed, comparedType), bound);
|
|
85330
|
+
}
|
|
85331
|
+
};
|
|
85332
|
+
/** Structural equality for the scalar / scalar-array values a parameter binds to. */
|
|
85333
|
+
function stableEquals(a, b) {
|
|
85334
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
85335
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
85336
|
+
return a.every((element, index) => stableEquals(element, b[index]));
|
|
85337
|
+
}
|
|
85338
|
+
if (typeof a === "string" && typeof b === "number") return Number.isFinite(b) && a === String(b);
|
|
85339
|
+
if (typeof a === "number" || typeof b === "number") return typeof a === "number" && typeof b === "number" && Number.isFinite(a) && a === b;
|
|
85340
|
+
if (typeof a === "string" && typeof b === "boolean") return a === String(b);
|
|
85341
|
+
return typeof a === "string" && typeof b === "string" && a === b;
|
|
85342
|
+
}
|
|
85343
|
+
async function readDeployedParameters(cfnStackName, cfnClient, logger, displayName, options) {
|
|
85344
|
+
try {
|
|
85345
|
+
const stack = (await cfnClient.send(new DescribeStacksCommand({ StackName: cfnStackName }))).Stacks?.[0];
|
|
85346
|
+
if (!stack) {
|
|
85347
|
+
logger.warn(unavailableWarning(displayName, "the stack was not returned"));
|
|
85348
|
+
return DeployedParameters.unavailable();
|
|
85349
|
+
}
|
|
85350
|
+
return DeployedParameters.fromDescribeStacks(stack.Parameters ?? []);
|
|
85351
|
+
} catch (err) {
|
|
85352
|
+
if (options?.absentStackIsNoSource === true && err instanceof Error && err.name === "ValidationError" && /does not exist/i.test(err.message)) return;
|
|
85353
|
+
const name = err instanceof Error && /^[A-Za-z0-9_.]{1,80}$/.test(err.name) ? err.name : "error";
|
|
85354
|
+
logger.warn(unavailableWarning(displayName, name));
|
|
85355
|
+
return DeployedParameters.unavailable();
|
|
85356
|
+
}
|
|
85357
|
+
}
|
|
85358
|
+
function unavailableWarning(displayName, reason) {
|
|
85359
|
+
return `Could not read the deployed parameter values of CloudFormation stack '${displayName}' (${reason}). 'cdkd import' needs the cloudformation:DescribeStacks permission to prove that a template parameter was deployed with its 'Default'; without it every resource that depends on a template parameter is imported WITHOUT an observed drift baseline. Review 'cdkd diff' before the next 'cdkd deploy'.`;
|
|
85360
|
+
}
|
|
85361
|
+
/**
|
|
85362
|
+
* The declared parameters whose deployed value is NOT provably the bound one.
|
|
85363
|
+
* A parameter the import could not bind at all (no `Default`, an unreferenced
|
|
85364
|
+
* or failed SSM default, the empty fallback bag) is divergent by definition.
|
|
85365
|
+
*/
|
|
85366
|
+
function divergentParameterNames(template, bound, deployed) {
|
|
85367
|
+
const divergent = /* @__PURE__ */ new Set();
|
|
85368
|
+
const declared = template.Parameters;
|
|
85369
|
+
if (declared === void 0 || declared === null || typeof declared !== "object") return divergent;
|
|
85370
|
+
for (const [name, definition] of Object.entries(declared)) {
|
|
85371
|
+
const record = definition !== null && typeof definition === "object" ? definition : void 0;
|
|
85372
|
+
const type = record?.["Type"];
|
|
85373
|
+
const noEcho = record?.["NoEcho"];
|
|
85374
|
+
if (noEcho === true || typeof noEcho === "string" && noEcho.toLowerCase() === "true") {
|
|
85375
|
+
divergent.add(name);
|
|
85376
|
+
continue;
|
|
85377
|
+
}
|
|
85378
|
+
if (!Object.hasOwn(bound, name) || !deployed.provablyEquals(name, type, bound[name])) divergent.add(name);
|
|
85379
|
+
}
|
|
85380
|
+
return divergent;
|
|
85381
|
+
}
|
|
85382
|
+
/**
|
|
85383
|
+
* Every intrinsic the dependence walk knows how to read. An `Fn::`-prefixed
|
|
85384
|
+
* key OUTSIDE this list is unclassifiable: it may name a parameter in a way
|
|
85385
|
+
* the generic recursion cannot see, so the walk fails closed on it.
|
|
85386
|
+
*/
|
|
85387
|
+
const KNOWN_INTRINSICS = /* @__PURE__ */ new Set([
|
|
85388
|
+
"Fn::GetAtt",
|
|
85389
|
+
"Fn::Join",
|
|
85390
|
+
"Fn::Sub",
|
|
85391
|
+
"Fn::Select",
|
|
85392
|
+
"Fn::Split",
|
|
85393
|
+
"Fn::If",
|
|
85394
|
+
"Fn::Equals",
|
|
85395
|
+
"Fn::And",
|
|
85396
|
+
"Fn::Or",
|
|
85397
|
+
"Fn::Not",
|
|
85398
|
+
"Fn::ImportValue",
|
|
85399
|
+
"Fn::GetStackOutput",
|
|
85400
|
+
"Fn::FindInMap",
|
|
85401
|
+
"Fn::Base64",
|
|
85402
|
+
"Fn::GetAZs",
|
|
85403
|
+
"Fn::Cidr"
|
|
85404
|
+
]);
|
|
85405
|
+
/** `${Name}` / `${Name.Attr}` in an `Fn::Sub` string; `${!Literal}` is an escape. */
|
|
85406
|
+
const SUB_VARIABLE = /\$\{([^!}][^}]*)\}/g;
|
|
85407
|
+
/**
|
|
85408
|
+
* Which names (parameters, but also resources and pseudo parameters — the
|
|
85409
|
+
* caller intersects with the divergent set) the RAW `Properties` of a resource
|
|
85410
|
+
* depend on, plus its resource-level `Condition`.
|
|
85411
|
+
*
|
|
85412
|
+
* Generic by design: instead of modelling each intrinsic's argument grammar it
|
|
85413
|
+
* recurses into EVERY value, so a `Ref` nested in an `Fn::Join` / `Fn::Select`
|
|
85414
|
+
* / `Fn::Split` operand, an `Fn::FindInMap` key, or an `Fn::Sub` variable-map
|
|
85415
|
+
* value is found without being named. Only the three forms that name something
|
|
85416
|
+
* by BARE STRING need their own arm: `Ref`, `Fn::Sub`'s template text, and a
|
|
85417
|
+
* condition name (`Fn::If`'s first argument, and `Condition` inside the
|
|
85418
|
+
* `Conditions` section), which is followed transitively through
|
|
85419
|
+
* `template.Conditions`.
|
|
85420
|
+
*
|
|
85421
|
+
* May throw `RangeError` on a bag deeper than the call stack; the caller wraps
|
|
85422
|
+
* it fail-closed, like the discard walk beside it.
|
|
85423
|
+
*/
|
|
85424
|
+
function collectParameterDependencies(properties, resourceCondition, template) {
|
|
85425
|
+
const names = /* @__PURE__ */ new Set();
|
|
85426
|
+
const attributeTargets = /* @__PURE__ */ new Set();
|
|
85427
|
+
const visitedConditions = /* @__PURE__ */ new Set();
|
|
85428
|
+
let unclassifiable = false;
|
|
85429
|
+
const conditions = template.Conditions !== null && typeof template.Conditions === "object" ? template.Conditions : {};
|
|
85430
|
+
const followCondition = (name) => {
|
|
85431
|
+
if (typeof name !== "string") {
|
|
85432
|
+
unclassifiable = true;
|
|
85433
|
+
return;
|
|
85434
|
+
}
|
|
85435
|
+
if (visitedConditions.has(name)) return;
|
|
85436
|
+
visitedConditions.add(name);
|
|
85437
|
+
if (!Object.hasOwn(conditions, name)) {
|
|
85438
|
+
unclassifiable = true;
|
|
85439
|
+
return;
|
|
85440
|
+
}
|
|
85441
|
+
walk(conditions[name], true);
|
|
85442
|
+
};
|
|
85443
|
+
const walk = (node, inConditions) => {
|
|
85444
|
+
if (node === null || typeof node !== "object") return;
|
|
85445
|
+
if (Array.isArray(node)) {
|
|
85446
|
+
for (const element of node) walk(element, inConditions);
|
|
85447
|
+
return;
|
|
85448
|
+
}
|
|
85449
|
+
for (const [key, value] of Object.entries(node)) {
|
|
85450
|
+
if (key === "Ref") {
|
|
85451
|
+
if (typeof value === "string") names.add(value);
|
|
85452
|
+
else unclassifiable = true;
|
|
85453
|
+
continue;
|
|
85454
|
+
}
|
|
85455
|
+
if (key === "Fn::Sub") {
|
|
85456
|
+
const text = Array.isArray(value) ? value[0] : value;
|
|
85457
|
+
if (typeof text !== "string" || Array.isArray(value) && value.length !== 2) unclassifiable = true;
|
|
85458
|
+
else for (const match of text.matchAll(SUB_VARIABLE)) {
|
|
85459
|
+
const variable = (match[1] ?? "").trim();
|
|
85460
|
+
names.add(variable);
|
|
85461
|
+
const dot = variable.indexOf(".");
|
|
85462
|
+
if (dot > 0) {
|
|
85463
|
+
names.add(variable.slice(0, dot));
|
|
85464
|
+
attributeTargets.add(variable.slice(0, dot));
|
|
85465
|
+
}
|
|
85466
|
+
}
|
|
85467
|
+
if (Array.isArray(value)) walk(value[1], inConditions);
|
|
85468
|
+
continue;
|
|
85469
|
+
}
|
|
85470
|
+
if (key === "Fn::GetAtt") {
|
|
85471
|
+
const target = Array.isArray(value) ? value[0] : value;
|
|
85472
|
+
if (typeof target === "string") {
|
|
85473
|
+
const dot = target.indexOf(".");
|
|
85474
|
+
const logicalId = Array.isArray(value) || dot <= 0 ? target : target.slice(0, dot);
|
|
85475
|
+
names.add(logicalId);
|
|
85476
|
+
attributeTargets.add(logicalId);
|
|
85477
|
+
} else unclassifiable = true;
|
|
85478
|
+
walk(value, inConditions);
|
|
85479
|
+
continue;
|
|
85480
|
+
}
|
|
85481
|
+
if (key === "Fn::If") {
|
|
85482
|
+
if (Array.isArray(value) && value.length === 3) {
|
|
85483
|
+
followCondition(value[0]);
|
|
85484
|
+
walk(value[1], inConditions);
|
|
85485
|
+
walk(value[2], inConditions);
|
|
85486
|
+
} else unclassifiable = true;
|
|
85487
|
+
continue;
|
|
85488
|
+
}
|
|
85489
|
+
if (key === "Condition" && inConditions) {
|
|
85490
|
+
followCondition(value);
|
|
85491
|
+
continue;
|
|
85492
|
+
}
|
|
85493
|
+
if (key.startsWith("Fn::") && !KNOWN_INTRINSICS.has(key)) unclassifiable = true;
|
|
85494
|
+
walk(value, inConditions);
|
|
85495
|
+
}
|
|
85496
|
+
};
|
|
85497
|
+
walk(properties, false);
|
|
85498
|
+
if (resourceCondition !== void 0) followCondition(resourceCondition);
|
|
85499
|
+
return {
|
|
85500
|
+
names,
|
|
85501
|
+
attributeTargets,
|
|
85502
|
+
unclassifiable
|
|
85503
|
+
};
|
|
85504
|
+
}
|
|
85505
|
+
/**
|
|
85506
|
+
* The divergent parameters that ANYTHING in the template names. A declared
|
|
85507
|
+
* parameter nothing names cannot have shaped a resource — and CDK's
|
|
85508
|
+
* `BootstrapVersion` is exactly that on every default-synthesized stack: SSM
|
|
85509
|
+
* typed, never bound by the resolver because nothing references it, hence
|
|
85510
|
+
* always "divergent". Left in, it would arm the unclassifiable-shape refusal on
|
|
85511
|
+
* stacks whose every real parameter is proven.
|
|
85512
|
+
*
|
|
85513
|
+
* "Names" is the UNION of this module's walk and `TemplateParser`'s reference
|
|
85514
|
+
* scan — the same scan the resolver uses to decide an SSM-typed parameter is
|
|
85515
|
+
* unreferenced — so the two cannot disagree about `BootstrapVersion`. An
|
|
85516
|
+
* unclassifiable shape does NOT keep the whole set here: CloudFormation names a
|
|
85517
|
+
* parameter only through `Ref`, `Fn::Sub` text or a condition, and the generic
|
|
85518
|
+
* recursion reads those inside any intrinsic, known or not. A walk that THROWS
|
|
85519
|
+
* keeps the whole set (fail closed).
|
|
85520
|
+
*/
|
|
85521
|
+
function reachableDivergentParameters(template, divergent, rawBags) {
|
|
85522
|
+
if (divergent.size === 0) return /* @__PURE__ */ new Set();
|
|
85523
|
+
try {
|
|
85524
|
+
const roots = [
|
|
85525
|
+
rawBags,
|
|
85526
|
+
template.Resources,
|
|
85527
|
+
template.Outputs,
|
|
85528
|
+
template.Conditions
|
|
85529
|
+
];
|
|
85530
|
+
const named = new Set(collectParameterDependencies(roots, void 0, template).names);
|
|
85531
|
+
const parser = new TemplateParser();
|
|
85532
|
+
for (const root of roots) {
|
|
85533
|
+
if (root === null || typeof root !== "object") continue;
|
|
85534
|
+
for (const name of parser.extractReferences(root)) named.add(name);
|
|
85535
|
+
}
|
|
85536
|
+
return new Set([...divergent].filter((name) => named.has(name)));
|
|
85537
|
+
} catch {
|
|
85538
|
+
return new Set(divergent);
|
|
85539
|
+
}
|
|
85540
|
+
}
|
|
85541
|
+
/** Whether any DECLARED parameter is named by anything in the template. */
|
|
85542
|
+
function namesAnyDeclaredParameter(template) {
|
|
85543
|
+
const declared = template.Parameters;
|
|
85544
|
+
if (declared === null || typeof declared !== "object") return false;
|
|
85545
|
+
const names = new Set(Object.keys(declared));
|
|
85546
|
+
return names.size > 0 && reachableDivergentParameters(template, names, []).size > 0;
|
|
85547
|
+
}
|
|
85548
|
+
/**
|
|
85549
|
+
* The per-stack ARM 4 verdict. Each resource is judged on its TEMPLATE
|
|
85550
|
+
* definition (`Properties` + `Condition`) AND on the raw bag the state record
|
|
85551
|
+
* carries, unioned: a record preserved by a selective merge holds a previous
|
|
85552
|
+
* run's RESOLVED literal (`Value: 'CHANGEME'`), which names no parameter, so
|
|
85553
|
+
* judging it on the state bag alone would leave nothing to carry to the
|
|
85554
|
+
* freshly imported resource that reads its attribute. Template resources that
|
|
85555
|
+
* are not in state at all are judged too, for the same reason.
|
|
85556
|
+
*
|
|
85557
|
+
* Seeds are direct dependents and unclassifiable bags (a bag that cannot be
|
|
85558
|
+
* read cannot be vouched for, and neither can what reads its attributes); the
|
|
85559
|
+
* refused set is then closed over ATTRIBUTE reads, to a fixpoint.
|
|
85560
|
+
*/
|
|
85561
|
+
function computeParameterTaint(template, stateBags, divergent) {
|
|
85562
|
+
const refused = /* @__PURE__ */ new Set();
|
|
85563
|
+
const unclassifiable = /* @__PURE__ */ new Set();
|
|
85564
|
+
const parametersHit = /* @__PURE__ */ new Set();
|
|
85565
|
+
if (divergent.size === 0) return {
|
|
85566
|
+
refused,
|
|
85567
|
+
unclassifiable,
|
|
85568
|
+
parametersHit
|
|
85569
|
+
};
|
|
85570
|
+
const templateResources = template.Resources !== null && typeof template.Resources === "object" ? template.Resources : {};
|
|
85571
|
+
const readers = /* @__PURE__ */ new Map();
|
|
85572
|
+
for (const logicalId of /* @__PURE__ */ new Set([...Object.keys(templateResources), ...stateBags.keys()])) {
|
|
85573
|
+
const definition = Object.hasOwn(templateResources, logicalId) ? templateResources[logicalId] : void 0;
|
|
85574
|
+
const record = definition !== null && typeof definition === "object" ? definition : {};
|
|
85575
|
+
try {
|
|
85576
|
+
const dependencies = collectParameterDependencies([record["Properties"], stateBags.get(logicalId)], record["Condition"], template);
|
|
85577
|
+
readers.set(logicalId, new Set(dependencies.attributeTargets));
|
|
85578
|
+
const hits = [...dependencies.names].filter((name) => divergent.has(name));
|
|
85579
|
+
for (const name of hits) parametersHit.add(name);
|
|
85580
|
+
if (hits.length > 0) refused.add(logicalId);
|
|
85581
|
+
else if (dependencies.unclassifiable) {
|
|
85582
|
+
refused.add(logicalId);
|
|
85583
|
+
unclassifiable.add(logicalId);
|
|
85584
|
+
}
|
|
85585
|
+
} catch {
|
|
85586
|
+
refused.add(logicalId);
|
|
85587
|
+
unclassifiable.add(logicalId);
|
|
85588
|
+
}
|
|
85589
|
+
}
|
|
85590
|
+
for (let grew = refused.size > 0; grew;) {
|
|
85591
|
+
grew = false;
|
|
85592
|
+
for (const [logicalId, targets] of readers) {
|
|
85593
|
+
if (refused.has(logicalId)) continue;
|
|
85594
|
+
if (![...targets].some((target) => refused.has(target))) continue;
|
|
85595
|
+
refused.add(logicalId);
|
|
85596
|
+
grew = true;
|
|
85597
|
+
}
|
|
85598
|
+
}
|
|
85599
|
+
return {
|
|
85600
|
+
refused,
|
|
85601
|
+
unclassifiable,
|
|
85602
|
+
parametersHit
|
|
85603
|
+
};
|
|
85604
|
+
}
|
|
85605
|
+
|
|
84619
85606
|
//#endregion
|
|
84620
85607
|
//#region src/cli/commands/import.ts
|
|
84621
85608
|
async function importCommand(stackArg, options) {
|
|
@@ -84818,7 +85805,8 @@ async function importCommand(stackArg, options) {
|
|
|
84818
85805
|
}
|
|
84819
85806
|
}
|
|
84820
85807
|
const stackState = buildStackState(stackInfo.stackName, targetRegion, rows, templateParser, stateTemplate, existingState, selectiveMode);
|
|
84821
|
-
await
|
|
85808
|
+
const deployedParameters = !namesAnyDeclaredParameter(stateTemplate) ? void 0 : migrationCfnStackName !== void 0 ? await readDeployedParameters(migrationCfnStackName, awsClients.cloudFormation, logger, displaySafe(migrationCfnStackName)) : await readDeployedParameters(stackInfo.stackName, awsClients.cloudFormation, logger, displaySafe(stackInfo.stackName), { absentStackIsNoSource: true });
|
|
85809
|
+
await captureObservedForImportedResources(stackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger, deployedParameters)), rebuiltLogicalIdsFrom(rows, stateTemplate));
|
|
84822
85810
|
const saveOptions = {};
|
|
84823
85811
|
if (existingEtag) saveOptions.expectedEtag = existingEtag;
|
|
84824
85812
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
@@ -84838,7 +85826,8 @@ async function importCommand(stackArg, options) {
|
|
|
84838
85826
|
lockOwner: owner,
|
|
84839
85827
|
accountId: accountIdForNestedSynth,
|
|
84840
85828
|
logger,
|
|
84841
|
-
assetRedirect
|
|
85829
|
+
assetRedirect,
|
|
85830
|
+
cfnClient: awsClients.cloudFormation
|
|
84842
85831
|
});
|
|
84843
85832
|
if (migrationCfnStackName) {
|
|
84844
85833
|
const orphaned = resources.length - importedRows.length;
|
|
@@ -85286,7 +86275,7 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85286
86275
|
* the clear.
|
|
85287
86276
|
*
|
|
85288
86277
|
* The predicate is deliberately CONSERVATIVE — it over-refuses — and that is a
|
|
85289
|
-
* decision a review round paid for.
|
|
86278
|
+
* decision a review round paid for. Four arms:
|
|
85290
86279
|
*
|
|
85291
86280
|
* - **Resolution THREW** — refuse, unconditionally. A throw means the
|
|
85292
86281
|
* resolver could not finish, so nothing about the persisted bag is
|
|
@@ -85327,6 +86316,15 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85327
86316
|
* opener text and no intrinsic anywhere, because every way a discarded
|
|
85328
86317
|
* subtree could have SOURCED a reference is an intrinsic.
|
|
85329
86318
|
*
|
|
86319
|
+
* - **An UNVERIFIABLE INPUT** (ARM 4, issue
|
|
86320
|
+
* [#2854](https://github.com/go-to-k/cdkd/issues/2854)). The raw bag
|
|
86321
|
+
* depends on a template parameter whose DEPLOYED value — read from the
|
|
86322
|
+
* backing CloudFormation stack's `DescribeStacks`, root and every nested
|
|
86323
|
+
* child alike — is not provably the `Default` this import bound. Runs only
|
|
86324
|
+
* when `deployedParameters` is supplied; the comparison, the dependence
|
|
86325
|
+
* walk and the comparison-only rule for the deployed values live in
|
|
86326
|
+
* `import-deployed-parameters.ts`.
|
|
86327
|
+
*
|
|
85330
86328
|
* The cost of a false refusal is one resource's drift baseline until its next
|
|
85331
86329
|
* deploy; the cost of a false pass is a plaintext secret in `state.json`. So an
|
|
85332
86330
|
* unmeasurable bag refuses too.
|
|
@@ -85341,8 +86339,21 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85341
86339
|
* issue and none of them proven to be all of them:
|
|
85342
86340
|
*
|
|
85343
86341
|
* - a parameter binds to a placeholder `Default` while the DEPLOYED value was
|
|
85344
|
-
* the reference, so no opener exists in any template this walk is handed
|
|
85345
|
-
* [#2854](https://github.com/go-to-k/cdkd/issues/2854)
|
|
86342
|
+
* the reference, so no opener exists in any template this walk is handed
|
|
86343
|
+
* ([#2854](https://github.com/go-to-k/cdkd/issues/2854)). ARM 4 below
|
|
86344
|
+
* refuses this when a CloudFormation stack backs the import (its
|
|
86345
|
+
* `DescribeStacks` parameters are the comparison); what REMAINS is an
|
|
86346
|
+
* import with NO CloudFormation stack, where no deployed-parameter source
|
|
86347
|
+
* exists, taint across the nested-stack `Outputs` boundary
|
|
86348
|
+
* (`import-deployed-parameters.ts` states it), and the NEXT DEPLOY: the
|
|
86349
|
+
* refusal marker is cleared by any UPDATE that rebuilds the record, and
|
|
86350
|
+
* `cdkd deploy` binds the same placeholder `Default`, so an UPDATE that does
|
|
86351
|
+
* not rewrite the placeholder-bound property captures the readback against
|
|
86352
|
+
* it ([#3462](https://github.com/go-to-k/cdkd/issues/3462);
|
|
86353
|
+
* `deploy-engine.ts` owns the fix). ARM 4 also WIDENS the population of
|
|
86354
|
+
* [#2872](https://github.com/go-to-k/cdkd/issues/2872): a record preserved
|
|
86355
|
+
* by a selective merge that a pre-#2854 import already gave a plaintext
|
|
86356
|
+
* baseline is now refused, and a refusal leaves that old baseline in place;
|
|
85346
86357
|
* - `redactSecretsForState` cannot PAIR the readback against the source at a
|
|
85347
86358
|
* position the source carries NO leaf for -- an observed KEY beside a paired
|
|
85348
86359
|
* one -- so there is nothing to refuse from and the value scan has no needle
|
|
@@ -85374,7 +86385,7 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85374
86385
|
* resolver, since the plaintext reaches the message through the resolver's
|
|
85375
86386
|
* own throw rather than through anything this function writes.
|
|
85376
86387
|
*/
|
|
85377
|
-
async function resolveImportedProperties(stackState, template, region, stateBackend, logger) {
|
|
86388
|
+
async function resolveImportedProperties(stackState, template, region, stateBackend, logger, deployedParameters) {
|
|
85378
86389
|
const unsafeObservedBaselineLogicalIds = /* @__PURE__ */ new Set();
|
|
85379
86390
|
const entries = Object.entries(stackState.resources ?? {});
|
|
85380
86391
|
if (entries.length === 0) return unsafeObservedBaselineLogicalIds;
|
|
@@ -85402,6 +86413,24 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85402
86413
|
logger.debug(`Template condition evaluation failed during import-time property resolution: ${err instanceof Error ? err.message : String(err)} — continuing without conditions.`);
|
|
85403
86414
|
}
|
|
85404
86415
|
const unboundParameterNames = Object.keys(template.Parameters ?? {}).filter((name) => isUnboundTemplateParameter(name, template, parameters));
|
|
86416
|
+
let divergentParameters;
|
|
86417
|
+
if (deployedParameters !== void 0) try {
|
|
86418
|
+
divergentParameters = reachableDivergentParameters(template, divergentParameterNames(template, parameters, deployedParameters), entries.map(([, resource]) => resource.properties));
|
|
86419
|
+
} catch {
|
|
86420
|
+
divergentParameters = new Set(Object.keys(template.Parameters ?? {}));
|
|
86421
|
+
}
|
|
86422
|
+
let parameterTaint;
|
|
86423
|
+
if (divergentParameters !== void 0 && divergentParameters.size > 0) try {
|
|
86424
|
+
parameterTaint = computeParameterTaint(template, new Map(entries.map(([logicalId, resource]) => [logicalId, resource.properties])), divergentParameters);
|
|
86425
|
+
} catch (err) {
|
|
86426
|
+
logger.debug(`observed-baseline parameter-dependence walk failed for '${displaySafe(stackState.stackName)}': ${err instanceof Error ? err.name : typeof err} — refusing every baseline in the stack fail-closed.`);
|
|
86427
|
+
const all = new Set(entries.map(([logicalId]) => logicalId));
|
|
86428
|
+
parameterTaint = {
|
|
86429
|
+
refused: all,
|
|
86430
|
+
unclassifiable: all,
|
|
86431
|
+
parametersHit: /* @__PURE__ */ new Set()
|
|
86432
|
+
};
|
|
86433
|
+
}
|
|
85405
86434
|
const baseContext = {
|
|
85406
86435
|
template,
|
|
85407
86436
|
resources: stackState.resources,
|
|
@@ -85425,7 +86454,8 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85425
86454
|
threw = true;
|
|
85426
86455
|
}
|
|
85427
86456
|
if (recordedSecretValues.size > 0 && resource.attributes !== void 0) resource.attributes = redactSecretsForState(resource.attributes, recordedSecretValues);
|
|
85428
|
-
|
|
86457
|
+
const dependsOnDivergent = parameterTaint?.refused.has(logicalId) === true;
|
|
86458
|
+
if (threw || dependsOnDivergent) {
|
|
85429
86459
|
unsafeObservedBaselineLogicalIds.add(logicalId);
|
|
85430
86460
|
continue;
|
|
85431
86461
|
}
|
|
@@ -85440,6 +86470,15 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85440
86470
|
}
|
|
85441
86471
|
if (rawOpeners === void 0 || persistedOpeners === void 0 || persistedOpeners < rawOpeners || discardsNonInertSubtree) unsafeObservedBaselineLogicalIds.add(logicalId);
|
|
85442
86472
|
}
|
|
86473
|
+
if (parameterTaint !== void 0) {
|
|
86474
|
+
const inState = new Set(entries.map(([logicalId]) => logicalId));
|
|
86475
|
+
const refusedHere = [...parameterTaint.refused].filter((id) => inState.has(id));
|
|
86476
|
+
const unreadable = refusedHere.filter((id) => parameterTaint.unclassifiable.has(id));
|
|
86477
|
+
if (refusedHere.length > 0) {
|
|
86478
|
+
const named = [...parameterTaint.parametersHit].sort().map((name) => displaySafe(name));
|
|
86479
|
+
logger.warn(`${refusedHere.length} resource(s) in '${displaySafe(stackState.stackName)}' depend, directly or through another resource's attributes, on template parameter(s) whose deployed CloudFormation value could not be proven equal to the template 'Default' 'cdkd import' binds` + (named.length > 0 ? ` (${named.join(", ")})` : "") + (unreadable.length > 0 ? ` — ${unreadable.length} of them because their properties could not be checked for such a dependence` : "") + ". Their recorded properties hold the 'Default', NOT the deployed value, and no observed drift baseline was captured for them by this import (the live value may be a secret this import cannot position a redaction for). Review 'cdkd diff' before the next 'cdkd deploy'.");
|
|
86480
|
+
}
|
|
86481
|
+
}
|
|
85443
86482
|
return unsafeObservedBaselineLogicalIds;
|
|
85444
86483
|
}
|
|
85445
86484
|
/** The opener every dynamic reference starts with, secret-bearing or not. */
|
|
@@ -85801,7 +86840,7 @@ async function captureObservedForImportedResources(stackState, providerRegistry,
|
|
|
85801
86840
|
}
|
|
85802
86841
|
if (unsafeObservedBaselineLogicalIds.has(logicalId)) {
|
|
85803
86842
|
resource.observedBaselineRefused = true;
|
|
85804
|
-
logger.debug(`observedProperties capture SKIPPED for imported ${logicalId} (${resource.resourceType}): the recorded properties
|
|
86843
|
+
logger.debug(`observedProperties capture SKIPPED for imported ${logicalId} (${resource.resourceType}): the recorded properties cannot be shown to spell every dynamic reference the deployed resource was built from, 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.`);
|
|
85805
86844
|
return;
|
|
85806
86845
|
}
|
|
85807
86846
|
delete resource.observedBaselineRefused;
|
|
@@ -85984,7 +87023,8 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
85984
87023
|
childStackState.parentStack = parentStackName;
|
|
85985
87024
|
childStackState.parentLogicalId = childLogicalId;
|
|
85986
87025
|
childStackState.parentRegion = parentRegion;
|
|
85987
|
-
|
|
87026
|
+
const childDeployedParameters = namesAnyDeclaredParameter(childStateTemplate) ? await readDeployedParameters(childTreeNode.stackName, args.cfnClient, logger, displaySafe(childStackName)) : void 0;
|
|
87027
|
+
await captureObservedForImportedResources(childStackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger, childDeployedParameters)), rebuiltLogicalIdsFrom(rows, childTemplate));
|
|
85988
87028
|
await stateBackend.saveState(childStackName, childRegion, childStackState);
|
|
85989
87029
|
logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
|
|
85990
87030
|
if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
|
|
@@ -86000,7 +87040,8 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
86000
87040
|
lockOwner,
|
|
86001
87041
|
accountId,
|
|
86002
87042
|
logger,
|
|
86003
|
-
assetRedirect
|
|
87043
|
+
assetRedirect,
|
|
87044
|
+
cfnClient: args.cfnClient
|
|
86004
87045
|
});
|
|
86005
87046
|
} finally {
|
|
86006
87047
|
await lockManager.releaseLock(childStackName, childRegion).catch((err) => {
|
|
@@ -95673,4 +96714,4 @@ function buildProgram() {
|
|
|
95673
96714
|
|
|
95674
96715
|
//#endregion
|
|
95675
96716
|
export { buildProgram };
|
|
95676
|
-
//# sourceMappingURL=program-
|
|
96717
|
+
//# sourceMappingURL=program-COiS7GZJ.js.map
|