@go-to-k/cdkd 0.290.34 → 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-BTxYWlwu.js → asg-provider-BguZAQtF.js} +2 -2
- package/dist/{asg-provider-BTxYWlwu.js.map → asg-provider-BguZAQtF.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-efiVJ-rX.js → deploy-engine-DhZtBBq0.js} +4 -4
- package/dist/{deploy-engine-efiVJ-rX.js.map → deploy-engine-DhZtBBq0.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-Dh1J573K.js → program-COiS7GZJ.js} +748 -72
- package/dist/{program-Dh1J573K.js.map → program-COiS7GZJ.js.map} +1 -1
- package/dist/{version-CpyoQ3PO.js → version-BIpCWEQx.js} +2 -2
- package/dist/{version-CpyoQ3PO.js.map → version-BIpCWEQx.js.map} +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as isExportAliasCollision, $n as maskSecretsInText, $r as stripControlChars, $t as withSharedDrainBudget, A as makeCanonicalizePropertiesFn, Ai 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";
|
|
@@ -16943,6 +16943,216 @@ function isRecord$1(value) {
|
|
|
16943
16943
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16944
16944
|
}
|
|
16945
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
|
+
|
|
16946
17156
|
//#endregion
|
|
16947
17157
|
//#region src/provisioning/dynamodb-index-busy-delete.ts
|
|
16948
17158
|
/**
|
|
@@ -18767,16 +18977,10 @@ function warmThroughputAlreadyMatches(desired, live) {
|
|
|
18767
18977
|
* to decide with and every block is emitted, cleaned of its AWS-managed
|
|
18768
18978
|
* members.
|
|
18769
18979
|
*
|
|
18770
|
-
*
|
|
18771
|
-
*
|
|
18772
|
-
*
|
|
18773
|
-
*
|
|
18774
|
-
* ALSO dropped on the write side, since `create()` forwards the CFn blob to
|
|
18775
|
-
* `CreateTable` and the SDK serializer discards the unknown member — still
|
|
18776
|
-
* true after round 6 taught that path to coerce `WarmThroughput`, because the
|
|
18777
|
-
* entry is rebuilt with a SPREAD and every other member, known or not, rides
|
|
18778
|
-
* along unchanged. Filed as issue
|
|
18779
|
-
* [#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)).
|
|
18780
18984
|
*/
|
|
18781
18985
|
function reverseMapSecondaryIndex(live, desired, bagInformative) {
|
|
18782
18986
|
const out = {};
|
|
@@ -19042,6 +19246,10 @@ var DynamoDBTableProvider = class {
|
|
|
19042
19246
|
let tableCreated = false;
|
|
19043
19247
|
try {
|
|
19044
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
|
+
}
|
|
19045
19253
|
const createParams = {
|
|
19046
19254
|
TableName: tableName,
|
|
19047
19255
|
KeySchema: keySchema,
|
|
@@ -19066,7 +19274,7 @@ var DynamoDBTableProvider = class {
|
|
|
19066
19274
|
};
|
|
19067
19275
|
if (properties["GlobalSecondaryIndexes"]) {
|
|
19068
19276
|
const declaredGsis = properties["GlobalSecondaryIndexes"];
|
|
19069
|
-
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;
|
|
19070
19278
|
}
|
|
19071
19279
|
if (properties["LocalSecondaryIndexes"]) createParams.LocalSecondaryIndexes = properties["LocalSecondaryIndexes"];
|
|
19072
19280
|
const sse = mapSSESpecification(properties["SSESpecification"]);
|
|
@@ -19083,7 +19291,8 @@ var DynamoDBTableProvider = class {
|
|
|
19083
19291
|
await this.applyPointInTimeRecovery(tableName, properties["PointInTimeRecoverySpecification"], void 0, maskSecrets);
|
|
19084
19292
|
await this.applyTimeToLive(tableName, properties["TimeToLiveSpecification"]);
|
|
19085
19293
|
await this.applyKinesisStreamingDestination(tableName, properties["KinesisStreamSpecification"], void 0, maskSecrets);
|
|
19086
|
-
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);
|
|
19087
19296
|
this.logger.debug(`Successfully created DynamoDB table ${logicalId}: ${tableName}`);
|
|
19088
19297
|
return {
|
|
19089
19298
|
physicalId: tableName,
|
|
@@ -19318,7 +19527,8 @@ var DynamoDBTableProvider = class {
|
|
|
19318
19527
|
await this.applyResourcePolicy(table.TableArn, properties["ResourcePolicy"], previousProperties["ResourcePolicy"]);
|
|
19319
19528
|
}
|
|
19320
19529
|
if (JSON.stringify(properties["KinesisStreamSpecification"]) !== JSON.stringify(previousProperties["KinesisStreamSpecification"])) await this.applyKinesisStreamingDestination(physicalId, properties["KinesisStreamSpecification"], previousProperties["KinesisStreamSpecification"], maskSecrets);
|
|
19321
|
-
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);
|
|
19322
19532
|
return {
|
|
19323
19533
|
physicalId,
|
|
19324
19534
|
wasReplaced: false,
|
|
@@ -19734,24 +19944,53 @@ var DynamoDBTableProvider = class {
|
|
|
19734
19944
|
*
|
|
19735
19945
|
* Called from both `create()` (after the table is ACTIVE) and `update()`
|
|
19736
19946
|
* (only when the value changed). On `update()`-side removal — template drops
|
|
19737
|
-
* 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).
|
|
19738
19952
|
*/
|
|
19739
|
-
async applyContributorInsights(tableName, spec, previousSpec) {
|
|
19740
|
-
|
|
19741
|
-
|
|
19742
|
-
if (
|
|
19743
|
-
|
|
19744
|
-
|
|
19745
|
-
|
|
19746
|
-
|
|
19747
|
-
|
|
19748
|
-
|
|
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);
|
|
19749
19987
|
await this.retryOnTransientControlPlane(() => this.dynamoDBClient.send(new UpdateContributorInsightsCommand({
|
|
19750
19988
|
TableName: tableName,
|
|
19751
|
-
|
|
19752
|
-
|
|
19753
|
-
|
|
19754
|
-
|
|
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}`);
|
|
19755
19994
|
}
|
|
19756
19995
|
/**
|
|
19757
19996
|
* Poll DescribeTable until the table reaches ACTIVE status
|
|
@@ -20732,32 +20971,35 @@ var DynamoDBTableProvider = class {
|
|
|
20732
20971
|
* level: it is order-SIGNIFICANT (HASH before RANGE), so sorting it would
|
|
20733
20972
|
* HIDE a real key change rather than remove a phantom one.
|
|
20734
20973
|
*
|
|
20735
|
-
* `GlobalSecondaryIndexes` / `LocalSecondaryIndexes`
|
|
20736
|
-
*
|
|
20737
|
-
* both
|
|
20738
|
-
*
|
|
20739
|
-
*
|
|
20740
|
-
* the parent's path (`drift-normalize.ts`),
|
|
20741
|
-
*
|
|
20742
|
-
*
|
|
20743
|
-
*
|
|
20744
|
-
*
|
|
20745
|
-
*
|
|
20746
|
-
* [#
|
|
20747
|
-
*
|
|
20748
|
-
*
|
|
20749
|
-
*
|
|
20750
|
-
*
|
|
20751
|
-
*
|
|
20752
|
-
*
|
|
20753
|
-
*
|
|
20754
|
-
*
|
|
20755
|
-
*
|
|
20756
|
-
* `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.
|
|
20757
20995
|
*/
|
|
20758
20996
|
getDriftUnorderedPaths(resourceType) {
|
|
20759
20997
|
if (resourceType !== "AWS::DynamoDB::Table") return [];
|
|
20760
|
-
return [
|
|
20998
|
+
return [
|
|
20999
|
+
"AttributeDefinitions",
|
|
21000
|
+
"GlobalSecondaryIndexes[]",
|
|
21001
|
+
"LocalSecondaryIndexes[]"
|
|
21002
|
+
];
|
|
20761
21003
|
}
|
|
20762
21004
|
/**
|
|
20763
21005
|
* `WarmThroughput` is reported by AWS for EVERY table, whether or not the
|
|
@@ -20902,7 +21144,9 @@ var DynamoDBTableProvider = class {
|
|
|
20902
21144
|
* caller passes). It is consulted for the table-level `WarmThroughput` — see
|
|
20903
21145
|
* {@link getDriftUnknownPaths} for the measurement and the reasoning — and,
|
|
20904
21146
|
* since issue #1767, for the per-index throughput blocks of each secondary
|
|
20905
|
-
* 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.
|
|
20906
21150
|
*/
|
|
20907
21151
|
async readCurrentState(physicalId, _logicalId, _resourceType, properties) {
|
|
20908
21152
|
try {
|
|
@@ -21001,15 +21245,31 @@ var DynamoDBTableProvider = class {
|
|
|
21001
21245
|
}
|
|
21002
21246
|
try {
|
|
21003
21247
|
const ciResp = await this.dynamoDBClient.send(new DescribeContributorInsightsCommand({ TableName: physicalId }));
|
|
21004
|
-
const
|
|
21005
|
-
if (
|
|
21006
|
-
const cspec = { Enabled: status === "ENABLED" };
|
|
21007
|
-
if (status === "ENABLED" && ciResp.ContributorInsightsMode !== void 0) cspec["Mode"] = ciResp.ContributorInsightsMode;
|
|
21008
|
-
result["ContributorInsightsSpecification"] = cspec;
|
|
21009
|
-
}
|
|
21248
|
+
const cspec = reverseMapTableContributorInsights(ciResp.ContributorInsightsStatus, ciResp.ContributorInsightsMode);
|
|
21249
|
+
if (cspec !== void 0) result["ContributorInsightsSpecification"] = cspec;
|
|
21010
21250
|
} catch (err) {
|
|
21011
21251
|
this.logger.debug(`Could not read ContributorInsights for ${physicalId}: ${describeAwsFailure(err).detail}`);
|
|
21012
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
|
+
}
|
|
21013
21273
|
return result;
|
|
21014
21274
|
} catch (err) {
|
|
21015
21275
|
if (err instanceof ResourceNotFoundException$1) return void 0;
|
|
@@ -84981,6 +85241,368 @@ function createForceUnlockCommand() {
|
|
|
84981
85241
|
return cmd;
|
|
84982
85242
|
}
|
|
84983
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
|
+
|
|
84984
85606
|
//#endregion
|
|
84985
85607
|
//#region src/cli/commands/import.ts
|
|
84986
85608
|
async function importCommand(stackArg, options) {
|
|
@@ -85183,7 +85805,8 @@ async function importCommand(stackArg, options) {
|
|
|
85183
85805
|
}
|
|
85184
85806
|
}
|
|
85185
85807
|
const stackState = buildStackState(stackInfo.stackName, targetRegion, rows, templateParser, stateTemplate, existingState, selectiveMode);
|
|
85186
|
-
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));
|
|
85187
85810
|
const saveOptions = {};
|
|
85188
85811
|
if (existingEtag) saveOptions.expectedEtag = existingEtag;
|
|
85189
85812
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
@@ -85203,7 +85826,8 @@ async function importCommand(stackArg, options) {
|
|
|
85203
85826
|
lockOwner: owner,
|
|
85204
85827
|
accountId: accountIdForNestedSynth,
|
|
85205
85828
|
logger,
|
|
85206
|
-
assetRedirect
|
|
85829
|
+
assetRedirect,
|
|
85830
|
+
cfnClient: awsClients.cloudFormation
|
|
85207
85831
|
});
|
|
85208
85832
|
if (migrationCfnStackName) {
|
|
85209
85833
|
const orphaned = resources.length - importedRows.length;
|
|
@@ -85651,7 +86275,7 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85651
86275
|
* the clear.
|
|
85652
86276
|
*
|
|
85653
86277
|
* The predicate is deliberately CONSERVATIVE — it over-refuses — and that is a
|
|
85654
|
-
* decision a review round paid for.
|
|
86278
|
+
* decision a review round paid for. Four arms:
|
|
85655
86279
|
*
|
|
85656
86280
|
* - **Resolution THREW** — refuse, unconditionally. A throw means the
|
|
85657
86281
|
* resolver could not finish, so nothing about the persisted bag is
|
|
@@ -85692,6 +86316,15 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85692
86316
|
* opener text and no intrinsic anywhere, because every way a discarded
|
|
85693
86317
|
* subtree could have SOURCED a reference is an intrinsic.
|
|
85694
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
|
+
*
|
|
85695
86328
|
* The cost of a false refusal is one resource's drift baseline until its next
|
|
85696
86329
|
* deploy; the cost of a false pass is a plaintext secret in `state.json`. So an
|
|
85697
86330
|
* unmeasurable bag refuses too.
|
|
@@ -85706,8 +86339,21 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85706
86339
|
* issue and none of them proven to be all of them:
|
|
85707
86340
|
*
|
|
85708
86341
|
* - a parameter binds to a placeholder `Default` while the DEPLOYED value was
|
|
85709
|
-
* the reference, so no opener exists in any template this walk is handed
|
|
85710
|
-
* [#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;
|
|
85711
86357
|
* - `redactSecretsForState` cannot PAIR the readback against the source at a
|
|
85712
86358
|
* position the source carries NO leaf for -- an observed KEY beside a paired
|
|
85713
86359
|
* one -- so there is nothing to refuse from and the value scan has no needle
|
|
@@ -85739,7 +86385,7 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85739
86385
|
* resolver, since the plaintext reaches the message through the resolver's
|
|
85740
86386
|
* own throw rather than through anything this function writes.
|
|
85741
86387
|
*/
|
|
85742
|
-
async function resolveImportedProperties(stackState, template, region, stateBackend, logger) {
|
|
86388
|
+
async function resolveImportedProperties(stackState, template, region, stateBackend, logger, deployedParameters) {
|
|
85743
86389
|
const unsafeObservedBaselineLogicalIds = /* @__PURE__ */ new Set();
|
|
85744
86390
|
const entries = Object.entries(stackState.resources ?? {});
|
|
85745
86391
|
if (entries.length === 0) return unsafeObservedBaselineLogicalIds;
|
|
@@ -85767,6 +86413,24 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85767
86413
|
logger.debug(`Template condition evaluation failed during import-time property resolution: ${err instanceof Error ? err.message : String(err)} — continuing without conditions.`);
|
|
85768
86414
|
}
|
|
85769
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
|
+
}
|
|
85770
86434
|
const baseContext = {
|
|
85771
86435
|
template,
|
|
85772
86436
|
resources: stackState.resources,
|
|
@@ -85790,7 +86454,8 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85790
86454
|
threw = true;
|
|
85791
86455
|
}
|
|
85792
86456
|
if (recordedSecretValues.size > 0 && resource.attributes !== void 0) resource.attributes = redactSecretsForState(resource.attributes, recordedSecretValues);
|
|
85793
|
-
|
|
86457
|
+
const dependsOnDivergent = parameterTaint?.refused.has(logicalId) === true;
|
|
86458
|
+
if (threw || dependsOnDivergent) {
|
|
85794
86459
|
unsafeObservedBaselineLogicalIds.add(logicalId);
|
|
85795
86460
|
continue;
|
|
85796
86461
|
}
|
|
@@ -85805,6 +86470,15 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85805
86470
|
}
|
|
85806
86471
|
if (rawOpeners === void 0 || persistedOpeners === void 0 || persistedOpeners < rawOpeners || discardsNonInertSubtree) unsafeObservedBaselineLogicalIds.add(logicalId);
|
|
85807
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
|
+
}
|
|
85808
86482
|
return unsafeObservedBaselineLogicalIds;
|
|
85809
86483
|
}
|
|
85810
86484
|
/** The opener every dynamic reference starts with, secret-bearing or not. */
|
|
@@ -86166,7 +86840,7 @@ async function captureObservedForImportedResources(stackState, providerRegistry,
|
|
|
86166
86840
|
}
|
|
86167
86841
|
if (unsafeObservedBaselineLogicalIds.has(logicalId)) {
|
|
86168
86842
|
resource.observedBaselineRefused = true;
|
|
86169
|
-
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.`);
|
|
86170
86844
|
return;
|
|
86171
86845
|
}
|
|
86172
86846
|
delete resource.observedBaselineRefused;
|
|
@@ -86349,7 +87023,8 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
86349
87023
|
childStackState.parentStack = parentStackName;
|
|
86350
87024
|
childStackState.parentLogicalId = childLogicalId;
|
|
86351
87025
|
childStackState.parentRegion = parentRegion;
|
|
86352
|
-
|
|
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));
|
|
86353
87028
|
await stateBackend.saveState(childStackName, childRegion, childStackState);
|
|
86354
87029
|
logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
|
|
86355
87030
|
if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
|
|
@@ -86365,7 +87040,8 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
86365
87040
|
lockOwner,
|
|
86366
87041
|
accountId,
|
|
86367
87042
|
logger,
|
|
86368
|
-
assetRedirect
|
|
87043
|
+
assetRedirect,
|
|
87044
|
+
cfnClient: args.cfnClient
|
|
86369
87045
|
});
|
|
86370
87046
|
} finally {
|
|
86371
87047
|
await lockManager.releaseLock(childStackName, childRegion).catch((err) => {
|
|
@@ -96038,4 +96714,4 @@ function buildProgram() {
|
|
|
96038
96714
|
|
|
96039
96715
|
//#endregion
|
|
96040
96716
|
export { buildProgram };
|
|
96041
|
-
//# sourceMappingURL=program-
|
|
96717
|
+
//# sourceMappingURL=program-COiS7GZJ.js.map
|