@go-to-k/cdkd 0.290.34 → 0.290.36
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-DWY8_Kn4.js} +2 -2
- package/dist/{asg-provider-BTxYWlwu.js.map → asg-provider-DWY8_Kn4.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-efiVJ-rX.js → deploy-engine-tX_-qy5a.js} +22 -8
- package/dist/deploy-engine-tX_-qy5a.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-Dh1J573K.js → program-DcMcqr70.js} +1322 -119
- package/dist/{program-Dh1J573K.js.map → program-DcMcqr70.js.map} +1 -1
- package/dist/{version-CpyoQ3PO.js → version-BYmXwLpd.js} +2 -2
- package/dist/{version-CpyoQ3PO.js.map → version-BYmXwLpd.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-efiVJ-rX.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as isExportAliasCollision, $n as maskSecretsInText, $r as
|
|
1
|
+
import { $ as isExportAliasCollision, $i as StackHasActiveImportsError, $n as maskSecretsInText, $r as escapeRegExp, $t as withSharedDrainBudget, A as makeCanonicalizePropertiesFn, An as refuseMalformedResourcesForDestroy, Ar as rewriteTemplateAssetReferences, At as getAccountInfo, B as isWarmThroughputDecrease, Bn as TemplateParser, Br as validateContainerRepoName, Bt as assertRegionMatch, C as PRE_DELETE_SNAPSHOT_TYPES, Ci as decodeListingKey, Cn as malformedStateRefusalMessage, Cr as shouldRetainResource, Ct as disableInstanceApiTermination, D as isFinalSnapshotError, Di as derivePartitionAndUrlSuffix, Dn as refuseMalformedOutputs, Dr as buildAssetRedirectMap, Dt as carriesFetchableDynamicReference, E as createPreDeleteFinalSnapshot, Ei as canonicalizeRegion, En as refuseMalformedNestedChildOutputs, Er as WorkGraph, Et as carriesDynamicReference, F as isStatefulRecreateTargetForReplace, Fi as setAwsClients, Fn as findActionableSilentDrops, Fr as getBootstrapMarkerKey, Ft as refStateLookupFromResource, G as cyan, Gi as LocalStartServiceError, Gn as carriesSecretMask, Gr as describeDockerFailure, Gt as configStringRefusal, H as toFiniteNumber, Hi as DynamicReferenceRegionAmbiguousError, Hn as STATE_SOURCED_CROSS_GENERATION_RULES, Hr as buildDockerImage, Ht as coerceCfnInteger, I as isStatefulRecreateTargetSync, In as findSilentDropProperties, Ir as isCrossRegionRedirect, It as isSettledInstanceState, J as red, Ji as NestedStackChildDirectDestroyError, Jn as errorCauseChain, Jr as getDockerCmd, Jt as requireConfigArray, K as gray, Kn as createSecretMasker, Kr as dockerSpawnEnvWithSensitive, Kt as readConfigString, L as renderStatefulReason, Li as CdkdError, Ln as describeTypeWithThrottleRetry, Lr as parseBootstrapMarker, Lt as WAFv2WebACLProvider, M as makeSiblingClaimReader, Mi as AwsClients, Mn as repairMalformedOutputsForReadOnly, Mr as BOOTSTRAP_MARKER_PREFIX, Mt as isNamelessDynamicReferenceError, N as planOrphanAdoption, Ni as getAwsClients, Nn as repairMalformedResourcePropertiesForReadOnly, Nr as assertAssetBucketRegion, Nt as isUnboundTemplateParameter, O as refusesFinalSnapshot, Oi as AssemblyReader, On as refuseMalformedOutputsForDestroy, Or as createAssetRedirectResolver, Ot as cfnRefValueFromPhysicalId, P as MULTI_REGION_RECREATE_BLOCKED_TYPES, Pi as resetAwsClients, Pn as repairMalformedResourcesForReadOnly, Pr as ensureAssetStorage, Pt as parameterTypeMayLoseSecretIdentity, Q as exportAliasCollisionScrubWarning, Qi as ResourceUpdateNotSupportedError, Qn as maskSecretsInError, Qr as runDockerStreaming, Qt as producerRegionsFromState, R as WARM_THROUGHPUT_MEMBERS, Rn as withRetry, Rr as readBootstrapMarkerBody, Rt as normalizeAwsTagsToCfn, S as ATOMIC_FINAL_SNAPSHOT_TYPES, Si as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, Sn as malformedResourcesWarning, Sr as orphansCarriedFrom, St as deleteSkipReason, T as ccRoutedFinalSnapshotError, Ti as PARTITION_TABLE, Tn as refuseDivergentRecordRegionForDestroy, Tr as stringifyValue, Tt as IntrinsicFunctionResolver, U as formatResourceLine, Ui as IntrinsicResolutionRefusalError, Un as STATE_SOURCED_READBACK_RULES, Ur as describeDockerCapturedOutput, Ut as configBooleanRefusal, V as toCfnInteger, Vi as DeployCancelledError, Vn as STATE_SOURCED_BASELINE_RULES, Vr as buildDenyExternalAccessPolicy, Vt as coerceCfnBoolean, W as bold, Wi as LocalInvokeBuildError$1, Wn as TEMPLATE_SOURCED_RULES, Wr as describeDockerExecFailure, Wt as configIntegerRefusal, X as collectDeclaredOutputNames, Xi as ProvisioningError, Xn as isSingleDynamicReferenceToken, Xr as redactDockerArgvValues, Xt as requireConfigString, Y as yellow, Yi as PartialFailureError, Yn as identityKeyFor, Yr as partitionSensitiveEnv, Yt as requireConfigObject, Z as collectPublishedOutputNames, Zi as ResourceTimeoutError, Zn as markSameGenerationBag, Zr as runDockerForeground, Zt as classifyReplaySecretRegion, _ as bagHoldsSecretExpression, _i as CFN_TEMPLATE_BODY_LIMIT, _n as malformedLocalOutputsWarning, _r as hasUnverifiableParameterRefusal, _t as CloudControlProvider, a as DeploymentEventsStore, aa as normalizeAwsError, ai as getDefaultStateBucketName, an as applyRoleArnIfSet, ar as S3StateBackend, at as collectInlinePolicyNamesManagedBySiblings, b as mergeNoChangeOutputs, bi as findLargeInlineResources, bn as malformedRenderedContainersWarning, br as isReadableBag, bt as UNSPECIFIED_SKIP_REASON, c as replayFailedOperations, ca as isRetryableTransientError, ci as resolveAutoAssetStorage, cn as callerEnvCredentials, cr as buildForceUnlockCommand, ct as wouldReturnToSdkProvider, d as updatePartialMessage, da as markNonRetryable, di as resolveStateBucketWithDefault, dn as INTRINSIC_KEYS, dr as formatLockExpiry, dt as maskerOrIdentity, ea as StackTerminationProtectionError, ei as stripControlChars, en as s3BucketArn, er as recordMaskOnlyValue, et as secretBearing, f as updatePartialReason, fa as markRedactedCause, fi as resolveStateBucketWithDefaultAndSource, fn as STATE_RESOURCES_MALFORMED, fr as shellQuote, ft as beginCommandInterruptScope, g as computeImplicitDeleteEdges, gi as ssmResolvedValueType, gn as malformedExportSourceWarning, gr as hasReadableExportSet, gt as startInterruptWatch, h as IMPLICIT_DELETE_DEPENDENCIES, ha as safeStringify, hi as warnDeprecatedNoPrefixCliFlag, hn as malformedExportNamesWarning, hr as exportNamesCarriedFrom, ht as isInterruptedWaitError, i as DeploymentEventsReader, ii as synthesisStatusMessage, in as s3BucketWebsiteUrl, ir as LockManager, it as IAMRoleProvider, j as extractDeploymentEventError, ji as resolveBucketRegion, jn as refuseMalformedState, jr as AssetModeResolver, jt as isImpossibleEmptyStoredAttribute, k as unsupportedFinalSnapshotError, ki as processStackMessages, kn as refuseMalformedResourcePropertiesForOrphan, kr as loadPublishableAssetManifest, kt as coerceParameterTypedValue, l as replayRollback, la as isThrottlingError, li as resolveCaptureObservedState, ln as envCredentialsAreAssumedRole, lr as buildLockContentionMessage, lt as createMaskedRetryLogger, m as maskingRetryLogger, ma as describeAwsFailure, mi as stateBucketExistenceConfirmed, mn as hasReadableResources, mr as DEFAULT_STATE_PREFIX, mt as interruptWatchListenerCount, n as DEFAULT_RESOURCE_WARN_AFTER_MS, na as SynthesisError, ni as getDockerImageBySourceHash, nn as s3BucketDualStackDomainName, nr as redactSecretsForState, nt as secretSafeKeyDisplay, o as planFailedOps, oa as withErrorHandling, oi as getLegacyStateBucketName, on as AWS_CREDENTIAL_ENV_KEYS, or as rebuildClientForBucketRegion, ot as clearOnUpdateRemoval, p as withResourceDeadline, pa as retryClassificationText, pi as resolveUseCdkBootstrapAssets, pn as hasReadableOutputs, pr as CUSTOM_RESOURCE_RESPONSE_PREFIX, pt as endCommandInterruptScope, q as green, qi as MalformedProducerRecordRefusalError, qn as dynamicReferenceTokens, qr as formatDockerLoginError, qt as replayWarn, r as DeployEngine, ri as Synthesizer, rn as s3BucketRegionalDomainName, rr as scrubResourceRecord, rt as getCurrentResourceSecrets, s as planRollback, sa as isMarkedNonRetryable, si as resolveApp, sn as applyCallerIdentityCredentials, sr as UNREPRODUCIBLE_LOCK_CLAUSE, st as ProviderRegistry, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as AssetManifestLoader, tn as s3BucketDomainName, tr as recoverMaskedOutput, tt as secretBearingStateKeyWarning, u as resolveReplacementOldType, ua as isTransientServerError, ui as resolveSkipPrefix, un as DiffCalculator, ur as forceQuitRecoveryClause, ut as maskDeep, v as isSecretBearingReferenceString, vi as CFN_TEMPLATE_URL_LIMIT, vn as malformedOutputsRefusalMessage, vr as importableOutputKeys, vt as isWaitAbandonedError, w as buildFinalSnapshotIdentifier, wi as expectedOwnerParam, wn as producerRecordKey, wr as AssetPublisher, wt as isTerminationProtectionPropagationError, x as bindingSkippedOutputs, xi as uploadCfnTemplate, xn as malformedResourcePropertiesWarning, xr as orphansAfterRollback, xt as deleteIndeterminateGuards, y as keptWholeReasonText, yi as MIGRATE_TMP_PREFIX, yn as malformedOutputsWarning, yr as importableOutputs, yt as slowCcOperationTimeoutMs, z as coerceWarmThroughput$1, zi as CrossAccountSecretRefusalError, zn as DagBuilder, zr as validateAssetBucketName, zt as resolveExplicitPhysicalId } from "./deploy-engine-tX_-qy5a.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-BYmXwLpd.js";
|
|
5
|
+
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-DWY8_Kn4.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";
|
|
@@ -12,7 +12,7 @@ import { CreateTopicCommand, DeleteTopicCommand, GetSubscriptionAttributesComman
|
|
|
12
12
|
import { AddPermissionCommand, CreateEventSourceMappingCommand, CreateFunctionCommand, CreateFunctionUrlConfigCommand, DeleteEventSourceMappingCommand, DeleteFunctionCodeSigningConfigCommand, DeleteFunctionCommand, DeleteFunctionConcurrencyCommand, DeleteFunctionEventInvokeConfigCommand, DeleteFunctionUrlConfigCommand, DeleteLayerVersionCommand, GetEventSourceMappingCommand, GetFunctionCodeSigningConfigCommand, GetFunctionCommand, GetFunctionConcurrencyCommand, GetFunctionEventInvokeConfigCommand, GetFunctionRecursionConfigCommand, GetFunctionUrlConfigCommand, GetLayerVersionByArnCommand, GetPolicyCommand as GetPolicyCommand$1, GetRuntimeManagementConfigCommand, LambdaClient, ListTagsCommand, PublishLayerVersionCommand, PutFunctionCodeSigningConfigCommand, PutFunctionConcurrencyCommand, PutFunctionEventInvokeConfigCommand, PutFunctionRecursionConfigCommand, PutRuntimeManagementConfigCommand, RemovePermissionCommand, ResourceNotFoundException, TagResourceCommand as TagResourceCommand$1, UntagResourceCommand as UntagResourceCommand$1, UpdateEventSourceMappingCommand, UpdateFunctionCodeCommand, UpdateFunctionConfigurationCommand, UpdateFunctionUrlConfigCommand, waitUntilFunctionActiveV2, waitUntilFunctionUpdatedV2 } from "@aws-sdk/client-lambda";
|
|
13
13
|
import { AssumeRoleCommand, GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
|
|
14
14
|
import { AllocateAddressCommand, AssociateAddressCommand, AssociateIamInstanceProfileCommand, AssociateRouteTableCommand, AttachInternetGatewayCommand, AuthorizeSecurityGroupEgressCommand, AuthorizeSecurityGroupIngressCommand, CreateInternetGatewayCommand, CreateNatGatewayCommand, CreateNetworkAclCommand, CreateNetworkAclEntryCommand, CreateRouteCommand, CreateRouteTableCommand, CreateSecurityGroupCommand, CreateSubnetCommand, CreateTagsCommand, CreateVpcCommand, DeleteInternetGatewayCommand, DeleteNatGatewayCommand, DeleteNetworkAclCommand, DeleteNetworkAclEntryCommand, DeleteNetworkInterfaceCommand, DeleteRouteCommand, DeleteRouteTableCommand, DeleteSecurityGroupCommand, DeleteSubnetCommand, DeleteTagsCommand, DeleteVpcCommand, DescribeAddressesCommand, DescribeAvailabilityZonesCommand, DescribeIamInstanceProfileAssociationsCommand, DescribeInstanceAttributeCommand, DescribeInstanceCreditSpecificationsCommand, DescribeInstancesCommand, DescribeInternetGatewaysCommand, DescribeNatGatewaysCommand, DescribeNetworkAclsCommand, DescribeNetworkInterfacesCommand, DescribeRouteTablesCommand, DescribeSecurityGroupRulesCommand, DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVolumesCommand, DescribeVpcAttributeCommand, DescribeVpcsCommand, DetachInternetGatewayCommand, DisassociateAddressCommand, DisassociateRouteTableCommand, EC2Client, ModifyInstanceAttributeCommand, ModifyInstanceCreditSpecificationCommand, ModifyInstanceMetadataOptionsCommand, ModifySubnetAttributeCommand, ModifyVpcAttributeCommand, MonitorInstancesCommand, ReleaseAddressCommand, ReplaceNetworkAclAssociationCommand, RevokeSecurityGroupEgressCommand, RevokeSecurityGroupIngressCommand, RunInstancesCommand, TerminateInstancesCommand, UnmonitorInstancesCommand, waitUntilInstanceRunning, waitUntilInstanceTerminated, waitUntilNatGatewayAvailable, waitUntilNatGatewayDeleted } from "@aws-sdk/client-ec2";
|
|
15
|
-
import { CreateTableCommand, DeleteResourcePolicyCommand, DeleteTableCommand, DescribeContinuousBackupsCommand, DescribeContributorInsightsCommand, DescribeKinesisStreamingDestinationCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DisableKinesisStreamingDestinationCommand, DynamoDBClient, EnableKinesisStreamingDestinationCommand, GetResourcePolicyCommand, ListTagsOfResourceCommand, PutResourcePolicyCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateContinuousBackupsCommand, UpdateContributorInsightsCommand, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
|
|
15
|
+
import { CreateTableCommand, DeleteResourcePolicyCommand, DeleteTableCommand, DescribeContinuousBackupsCommand, DescribeContributorInsightsCommand, DescribeKinesisStreamingDestinationCommand, DescribeTableCommand, DescribeTimeToLiveCommand, DisableKinesisStreamingDestinationCommand, DynamoDBClient, EnableKinesisStreamingDestinationCommand, GetResourcePolicyCommand, ListTagsOfResourceCommand, PolicyNotFoundException, PutResourcePolicyCommand, ResourceNotFoundException as ResourceNotFoundException$1, TagResourceCommand as TagResourceCommand$2, UntagResourceCommand as UntagResourceCommand$2, UpdateContinuousBackupsCommand, UpdateContributorInsightsCommand, UpdateTableCommand, UpdateTimeToLiveCommand } from "@aws-sdk/client-dynamodb";
|
|
16
16
|
import { CloudFormationClient, CreateChangeSetCommand, DeleteChangeSetCommand, DeleteStackCommand, DescribeChangeSetCommand, DescribeStackEventsCommand, DescribeStackResourcesCommand, DescribeStacksCommand, ExecuteChangeSetCommand, GetTemplateCommand, UpdateStackCommand, waitUntilChangeSetCreateComplete, waitUntilStackDeleteComplete, waitUntilStackImportComplete, waitUntilStackUpdateComplete } from "@aws-sdk/client-cloudformation";
|
|
17
17
|
import { APIGatewayClient, CreateAuthorizerCommand, CreateDeploymentCommand, CreateResourceCommand, CreateStageCommand, DeleteAuthorizerCommand, DeleteDeploymentCommand, DeleteMethodCommand, DeleteResourceCommand, DeleteStageCommand, GetAccountCommand, GetAuthorizerCommand, GetDeploymentCommand, GetMethodCommand, GetResourceCommand, GetStageCommand, NotFoundException as NotFoundException$1, PutIntegrationCommand, PutIntegrationResponseCommand, PutMethodCommand, PutMethodResponseCommand, TagResourceCommand as TagResourceCommand$3, UntagResourceCommand as UntagResourceCommand$3, UpdateAccountCommand, UpdateAuthorizerCommand, UpdateMethodCommand, UpdateStageCommand } from "@aws-sdk/client-api-gateway";
|
|
18
18
|
import { CreateEventBusCommand, DeleteEventBusCommand, DeleteRuleCommand, DescribeEventBusCommand, DescribeRuleCommand, EventBridgeClient, ListRulesCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$1, ListTargetsByRuleCommand, PutRuleCommand, PutTargetsCommand, RemoveTargetsCommand, ResourceNotFoundException as ResourceNotFoundException$2, TagResourceCommand as TagResourceCommand$4, UntagResourceCommand as UntagResourceCommand$4, UpdateEventBusCommand } from "@aws-sdk/client-eventbridge";
|
|
@@ -7137,7 +7137,7 @@ function manualBucketDeletionClause(bucketName, maskSecrets) {
|
|
|
7137
7137
|
* disagree about what an `Id` is.
|
|
7138
7138
|
*/
|
|
7139
7139
|
function configItemId(item) {
|
|
7140
|
-
if (!isPlainObject$
|
|
7140
|
+
if (!isPlainObject$2(item)) return void 0;
|
|
7141
7141
|
const id = item["Id"];
|
|
7142
7142
|
if (typeof id === "string") return id;
|
|
7143
7143
|
if (typeof id === "number" && Number.isFinite(id)) return String(id);
|
|
@@ -7157,7 +7157,7 @@ function configItemId(item) {
|
|
|
7157
7157
|
function declaresBucketObjectLock(properties) {
|
|
7158
7158
|
return properties["ObjectLockEnabled"] === true || properties["ObjectLockEnabled"] === "true";
|
|
7159
7159
|
}
|
|
7160
|
-
function isPlainObject$
|
|
7160
|
+
function isPlainObject$2(value) {
|
|
7161
7161
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7162
7162
|
}
|
|
7163
7163
|
/**
|
|
@@ -7192,7 +7192,7 @@ function isFlattenedDestination(dest) {
|
|
|
7192
7192
|
* via the critic's `--providers-dir=` seam.
|
|
7193
7193
|
*/
|
|
7194
7194
|
function s3BucketDestinationPath(dest, destinationPath) {
|
|
7195
|
-
return isPlainObject$
|
|
7195
|
+
return isPlainObject$2(dest) && isFlattenedDestination(dest) ? destinationPath : `${destinationPath}.S3BucketDestination`;
|
|
7196
7196
|
}
|
|
7197
7197
|
/**
|
|
7198
7198
|
* The FLATTENED CFn `Destination` block cdkd actually SENDS for `declared`
|
|
@@ -7231,9 +7231,9 @@ function s3BucketDestinationPath(dest, destinationPath) {
|
|
|
7231
7231
|
* resolver's own refusal to report).
|
|
7232
7232
|
*/
|
|
7233
7233
|
function effectiveS3BucketDestination(declared, sentFormat) {
|
|
7234
|
-
if (!isPlainObject$
|
|
7234
|
+
if (!isPlainObject$2(declared)) return declared;
|
|
7235
7235
|
const bag = isFlattenedDestination(declared) ? declared : declared["S3BucketDestination"];
|
|
7236
|
-
if (!isPlainObject$
|
|
7236
|
+
if (!isPlainObject$2(bag)) return declared;
|
|
7237
7237
|
if (!bag["BucketArn"] && !bag["Bucket"]) return declared;
|
|
7238
7238
|
const out = {};
|
|
7239
7239
|
const bucketArn = bag["BucketArn"] ?? bag["Bucket"];
|
|
@@ -7364,7 +7364,7 @@ function effectiveInventoryItem(item, sent) {
|
|
|
7364
7364
|
const scheduleContainerUsable = configStringRefusal(declaredSchedule, "Frequency", INVENTORY_DEFAULT_SCHEDULE_FREQUENCY, "AWS::S3::Bucket InventoryConfigurations[].Schedule") === void 0;
|
|
7365
7365
|
const canFoldSchedule = sent?.frequency !== void 0 || scheduleFrequencyUsable || !declaresScheduleFrequency && scheduleContainerUsable;
|
|
7366
7366
|
if (canFoldSchedule) {
|
|
7367
|
-
const declaredFrequency = declaresScheduleFrequency ? item["ScheduleFrequency"] : isPlainObject$
|
|
7367
|
+
const declaredFrequency = declaresScheduleFrequency ? item["ScheduleFrequency"] : isPlainObject$2(declaredSchedule) && declaredSchedule["Frequency"] !== void 0 ? declaredSchedule["Frequency"] : INVENTORY_DEFAULT_SCHEDULE_FREQUENCY;
|
|
7368
7368
|
const frequency = sent?.frequency ?? declaredFrequency;
|
|
7369
7369
|
if (!Object.is(item["ScheduleFrequency"], frequency)) patch["ScheduleFrequency"] = frequency;
|
|
7370
7370
|
}
|
|
@@ -7395,9 +7395,9 @@ function effectiveInventoryItem(item, sent) {
|
|
|
7395
7395
|
*/
|
|
7396
7396
|
function effectiveAnalyticsItem(item, sent) {
|
|
7397
7397
|
const storageClassAnalysis = item["StorageClassAnalysis"];
|
|
7398
|
-
if (!isPlainObject$
|
|
7398
|
+
if (!isPlainObject$2(storageClassAnalysis)) return item;
|
|
7399
7399
|
const dataExport = storageClassAnalysis["DataExport"];
|
|
7400
|
-
if (!isPlainObject$
|
|
7400
|
+
if (!isPlainObject$2(dataExport)) return item;
|
|
7401
7401
|
const patch = {};
|
|
7402
7402
|
const outputSchemaVersion = sent?.outputSchemaVersion ?? declaredOrDefault(dataExport, "OutputSchemaVersion", ANALYTICS_DEFAULT_OUTPUT_SCHEMA_VERSION);
|
|
7403
7403
|
if (!Object.is(dataExport["OutputSchemaVersion"], outputSchemaVersion)) patch["OutputSchemaVersion"] = outputSchemaVersion;
|
|
@@ -7513,7 +7513,7 @@ const EVENTBRIDGE_DEFAULT_ENABLED = true;
|
|
|
7513
7513
|
* correct once every declared-but-unusable value has been refused.
|
|
7514
7514
|
*/
|
|
7515
7515
|
function eventBridgeEnabledOf(eb) {
|
|
7516
|
-
const declared = isPlainObject$
|
|
7516
|
+
const declared = isPlainObject$2(eb) ? eb["EventBridgeEnabled"] : void 0;
|
|
7517
7517
|
return coerceCfnBoolean(declared) ?? EVENTBRIDGE_DEFAULT_ENABLED;
|
|
7518
7518
|
}
|
|
7519
7519
|
/** The notification families whose empty declaration is never SENT (issue #1759). */
|
|
@@ -7620,7 +7620,7 @@ function foldNotificationEvents(item) {
|
|
|
7620
7620
|
* handling).
|
|
7621
7621
|
*/
|
|
7622
7622
|
function effectiveNotificationConfiguration(config) {
|
|
7623
|
-
if (!isPlainObject$
|
|
7623
|
+
if (!isPlainObject$2(config)) return config;
|
|
7624
7624
|
let out = config;
|
|
7625
7625
|
for (const [listKey, aliases] of NOTIFICATION_ARN_ALIASES) out = canonicalizeItemList(out, listKey, (item) => foldNotificationEvents(foldToleratedAliases(item, aliases)));
|
|
7626
7626
|
out = dropEmptyNotificationFamilies(out);
|
|
@@ -7677,8 +7677,8 @@ function foldEventBridgeConfiguration(config) {
|
|
|
7677
7677
|
if (eb === void 0) return config;
|
|
7678
7678
|
if (eb !== null && configBooleanRefusal(eb, "EventBridgeEnabled", EVENTBRIDGE_CONFIG_PATH)) return config;
|
|
7679
7679
|
const enabled = eb === null ? false : eventBridgeEnabledOf(eb);
|
|
7680
|
-
const declared = isPlainObject$
|
|
7681
|
-
if (isPlainObject$
|
|
7680
|
+
const declared = isPlainObject$2(eb) ? eb["EventBridgeEnabled"] : void 0;
|
|
7681
|
+
if (isPlainObject$2(eb) && Object.keys(eb).length === 1 && Object.is(declared, enabled)) return config;
|
|
7682
7682
|
return {
|
|
7683
7683
|
...config,
|
|
7684
7684
|
[EVENTBRIDGE_CONFIG_KEY]: { EventBridgeEnabled: enabled }
|
|
@@ -7758,7 +7758,7 @@ function deepSameValue(a, b) {
|
|
|
7758
7758
|
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
7759
7759
|
return a.every((element, index) => deepSameValue(element, b[index]));
|
|
7760
7760
|
}
|
|
7761
|
-
if (isPlainObject$
|
|
7761
|
+
if (isPlainObject$2(a) && isPlainObject$2(b)) {
|
|
7762
7762
|
const keysA = Object.keys(a);
|
|
7763
7763
|
if (keysA.length !== Object.keys(b).length) return false;
|
|
7764
7764
|
return keysA.every((key) => key in b && deepSameValue(a[key], b[key]));
|
|
@@ -7833,9 +7833,9 @@ function foldLegacySingularTransitions(rule) {
|
|
|
7833
7833
|
let out;
|
|
7834
7834
|
for (const [pluralKey, singularKey] of LEGACY_SINGULAR_TRANSITIONS) {
|
|
7835
7835
|
const singular = rule[singularKey];
|
|
7836
|
-
const hasSingular = isPlainObject$
|
|
7836
|
+
const hasSingular = isPlainObject$2(singular);
|
|
7837
7837
|
const plural = rule[pluralKey];
|
|
7838
|
-
const needsFilter = Array.isArray(plural) && plural.some((element) => !isPlainObject$
|
|
7838
|
+
const needsFilter = Array.isArray(plural) && plural.some((element) => !isPlainObject$2(element));
|
|
7839
7839
|
if (!hasSingular && !needsFilter) continue;
|
|
7840
7840
|
let collided = false;
|
|
7841
7841
|
const merged = mergeLegacySingular(plural, hasSingular ? singular : void 0, () => {
|
|
@@ -7943,7 +7943,7 @@ function foldLifecycleExpiration(rule) {
|
|
|
7943
7943
|
const iso = cfnDateToIso(expiration);
|
|
7944
7944
|
if (iso === void 0) return rule;
|
|
7945
7945
|
date = iso;
|
|
7946
|
-
} else if (isPlainObject$
|
|
7946
|
+
} else if (isPlainObject$2(expiration)) {
|
|
7947
7947
|
if (expiration["Days"] !== void 0) days = expiration["Days"];
|
|
7948
7948
|
if (expiration["Date"]) {
|
|
7949
7949
|
const iso = cfnDateToIso(expiration["Date"]);
|
|
@@ -7991,10 +7991,10 @@ function foldNoncurrentVersionExpiration(rule) {
|
|
|
7991
7991
|
const legacy = rule["NoncurrentVersionExpirationInDays"];
|
|
7992
7992
|
if (legacy === void 0) return rule;
|
|
7993
7993
|
const declaredObject = rule["NoncurrentVersionExpiration"];
|
|
7994
|
-
if (declaredObject !== void 0 && !isPlainObject$
|
|
7994
|
+
if (declaredObject !== void 0 && !isPlainObject$2(declaredObject)) return rule;
|
|
7995
7995
|
const out = { ...rule };
|
|
7996
7996
|
delete out["NoncurrentVersionExpirationInDays"];
|
|
7997
|
-
if (isPlainObject$
|
|
7997
|
+
if (isPlainObject$2(declaredObject)) return out;
|
|
7998
7998
|
const days = coerceCfnNumber(legacy);
|
|
7999
7999
|
if (days === void 0) return rule;
|
|
8000
8000
|
out["NoncurrentVersionExpiration"] = { NoncurrentDays: days };
|
|
@@ -8089,10 +8089,10 @@ function foldLifecycleScope(rule, useFilterForm) {
|
|
|
8089
8089
|
* @returns `config` BY IDENTITY when the declared bag IS what goes on the wire.
|
|
8090
8090
|
*/
|
|
8091
8091
|
function effectiveLifecycleConfiguration(config) {
|
|
8092
|
-
if (!isPlainObject$
|
|
8092
|
+
if (!isPlainObject$2(config)) return config;
|
|
8093
8093
|
const rules = config["Rules"];
|
|
8094
8094
|
if (!Array.isArray(rules)) return config;
|
|
8095
|
-
const objectRules = rules.filter(isPlainObject$
|
|
8095
|
+
const objectRules = rules.filter(isPlainObject$2);
|
|
8096
8096
|
if (objectRules.length !== rules.length || objectRules.length === 0) return config;
|
|
8097
8097
|
if (objectRules.some(lifecycleRuleRefused)) return config;
|
|
8098
8098
|
const useFilterForm = lifecycleUsesFilterForm(objectRules);
|
|
@@ -8178,7 +8178,7 @@ function canonicalizeItemList(properties, key, fold) {
|
|
|
8178
8178
|
if (!Array.isArray(list)) return properties;
|
|
8179
8179
|
let changed = false;
|
|
8180
8180
|
const mapped = list.map((element) => {
|
|
8181
|
-
if (!isPlainObject$
|
|
8181
|
+
if (!isPlainObject$2(element)) return element;
|
|
8182
8182
|
const next = fold(element);
|
|
8183
8183
|
if (next !== element) changed = true;
|
|
8184
8184
|
return next;
|
|
@@ -8198,7 +8198,7 @@ function describeValue$1(value, maskSecrets = (text) => text) {
|
|
|
8198
8198
|
if (value === null) return "null";
|
|
8199
8199
|
if (Array.isArray(value)) return "an array";
|
|
8200
8200
|
if (typeof value === "string") return `a string ${JSON.stringify(maskDeep(value, maskSecrets))}`;
|
|
8201
|
-
if (isPlainObject$
|
|
8201
|
+
if (isPlainObject$2(value)) return `an object with keys [${Object.keys(value).map((key) => maskSecrets(key)).join(", ")}]`;
|
|
8202
8202
|
return `a ${typeof value} (${maskSecrets(String(value))})`;
|
|
8203
8203
|
}
|
|
8204
8204
|
/**
|
|
@@ -8223,8 +8223,8 @@ function coerceCfnNumber(value) {
|
|
|
8223
8223
|
* StorageClass collision, and say so rather than dropping in silence.
|
|
8224
8224
|
*/
|
|
8225
8225
|
function mergeLegacySingular(plural, singular, onCollision) {
|
|
8226
|
-
const out = (Array.isArray(plural) ? plural : []).filter(isPlainObject$
|
|
8227
|
-
if (!isPlainObject$
|
|
8226
|
+
const out = (Array.isArray(plural) ? plural : []).filter(isPlainObject$2);
|
|
8227
|
+
if (!isPlainObject$2(singular)) return out;
|
|
8228
8228
|
const sc = singular["StorageClass"];
|
|
8229
8229
|
if (typeof sc === "string" && out.some((e) => e["StorageClass"] === sc)) {
|
|
8230
8230
|
onCollision(sc);
|
|
@@ -8722,7 +8722,7 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
8722
8722
|
if (!(exp?.["Days"] !== void 0 || exp?.["Date"] !== void 0)) sdkRule.Expiration = { ExpiredObjectDeleteMarker: true };
|
|
8723
8723
|
else this.logger.warn(`Lifecycle rule '${rule["Id"] ?? "<unnamed>"}' on ${bucketName} sets ExpiredObjectDeleteMarker alongside an expiration Days/Date; S3 forbids combining them, so the delete-marker cleanup was not applied.`);
|
|
8724
8724
|
}
|
|
8725
|
-
const nve = isPlainObject$
|
|
8725
|
+
const nve = isPlainObject$2(rule["NoncurrentVersionExpiration"]) ? rule["NoncurrentVersionExpiration"] : void 0;
|
|
8726
8726
|
const legacyNveDays = rule["NoncurrentVersionExpirationInDays"];
|
|
8727
8727
|
if (nve) sdkRule.NoncurrentVersionExpiration = {
|
|
8728
8728
|
NoncurrentDays: nve["NoncurrentDays"],
|
|
@@ -8747,7 +8747,7 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
8747
8747
|
const singularTransition = rule["Transition"];
|
|
8748
8748
|
const allTransitions = mergeLegacySingular(transitions, singularTransition, (sc) => this.logger.warn(`Lifecycle rule '${rule["Id"] ?? "<unnamed>"}' on ${bucketName} declares both Transitions and the legacy Transition for storage class ${sc}; S3 rejects duplicates, so the legacy singular was ignored.`));
|
|
8749
8749
|
if (allTransitions.length > 0) sdkRule.Transitions = allTransitions.map(toSdkTransition);
|
|
8750
|
-
const abort = isPlainObject$
|
|
8750
|
+
const abort = isPlainObject$2(rule["AbortIncompleteMultipartUpload"]) ? rule["AbortIncompleteMultipartUpload"] : void 0;
|
|
8751
8751
|
if (abort) sdkRule.AbortIncompleteMultipartUpload = { DaysAfterInitiation: abort["DaysAfterInitiation"] };
|
|
8752
8752
|
const { prefix, tagFilters, sizeGt, sizeLt } = lifecycleRuleScope(rule);
|
|
8753
8753
|
const hasTags = tagFilters != null && tagFilters.length > 0;
|
|
@@ -9081,10 +9081,10 @@ var S3BucketProvider = class S3BucketProvider {
|
|
|
9081
9081
|
throw new Error(message);
|
|
9082
9082
|
};
|
|
9083
9083
|
if (dest === void 0 || dest === null) return;
|
|
9084
|
-
if (!isPlainObject$
|
|
9084
|
+
if (!isPlainObject$2(dest)) return drop(`${destinationPath} must be an object (got ${describeValue$1(dest, maskSecrets)}) — check for an unresolved intrinsic or a mis-nested template value; the destination would otherwise be dropped from the request with no error`);
|
|
9085
9085
|
const bag = isFlattenedDestination(dest) ? dest : dest["S3BucketDestination"];
|
|
9086
9086
|
if (bag === void 0 || bag === null) return drop(`${destinationPath} carries neither a bucket (BucketArn / Bucket) nor a nested S3BucketDestination object (got ${describeValue$1(dest, maskSecrets)}) — the destination would otherwise be dropped from the request with no error`);
|
|
9087
|
-
if (!isPlainObject$
|
|
9087
|
+
if (!isPlainObject$2(bag)) return drop(`${nestedPath} must be an object (got ${describeValue$1(bag, maskSecrets)}) — check for an unresolved intrinsic or a mis-nested template value; the destination would otherwise be dropped from the request with no error`);
|
|
9088
9088
|
if (!bag["BucketArn"] && !bag["Bucket"]) return drop(`${bag === dest ? destinationPath : nestedPath} has no destination bucket (BucketArn / Bucket) (got ${describeValue$1(bag, maskSecrets)}) — the request would be rejected by S3, or silently carry no destination`);
|
|
9089
9089
|
return bag;
|
|
9090
9090
|
}
|
|
@@ -16943,6 +16943,511 @@ 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
|
+
|
|
17156
|
+
//#endregion
|
|
17157
|
+
//#region src/provisioning/dynamodb-stream-members.ts
|
|
17158
|
+
/**
|
|
17159
|
+
* The two `AWS::DynamoDB::Table` `StreamSpecification` members that are NOT
|
|
17160
|
+
* members of the SDK's `StreamSpecification`: `ResourcePolicy`
|
|
17161
|
+
* (`{ PolicyDocument }`, the resource-based policy of the STREAM) and `Tags`
|
|
17162
|
+
* (issue [#3458](https://github.com/go-to-k/cdkd/issues/3458)).
|
|
17163
|
+
*
|
|
17164
|
+
* `CreateTable` / `UpdateTable` take only `{ StreamEnabled, StreamViewType }`,
|
|
17165
|
+
* so a block carrying either member deployed green with neither applied — a
|
|
17166
|
+
* template granting access to a table's stream left the stream with no policy.
|
|
17167
|
+
* CloudFormation applies both against the STREAM arn (`LatestStreamArn`),
|
|
17168
|
+
* re-applies both after a `StreamViewType` change (which mints a NEW arn), and
|
|
17169
|
+
* REMOVES both when the member leaves a block whose stream stays (measured on
|
|
17170
|
+
* real CloudFormation; the measurements are on the issue).
|
|
17171
|
+
*
|
|
17172
|
+
* Pure, so the write plan, the create-time refusal and the drift read-back
|
|
17173
|
+
* gate cannot answer "does this block declare the member" differently.
|
|
17174
|
+
*/
|
|
17175
|
+
/** The CFn property holding both members. */
|
|
17176
|
+
const STREAM_SPECIFICATION_KEY = "StreamSpecification";
|
|
17177
|
+
/** The stream's resource-based policy member. */
|
|
17178
|
+
const STREAM_POLICY_KEY = "ResourcePolicy";
|
|
17179
|
+
/** The stream's tag-list member. */
|
|
17180
|
+
const STREAM_TAGS_KEY = "Tags";
|
|
17181
|
+
const POLICY_PATH = `${STREAM_SPECIFICATION_KEY}.${STREAM_POLICY_KEY}`;
|
|
17182
|
+
const TAGS_PATH = `${STREAM_SPECIFICATION_KEY}.${STREAM_TAGS_KEY}`;
|
|
17183
|
+
function isPlainObject$1(value) {
|
|
17184
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17185
|
+
}
|
|
17186
|
+
/** The message a `config-shape` guard would throw, or `undefined`. */
|
|
17187
|
+
function refusalOf(guard) {
|
|
17188
|
+
try {
|
|
17189
|
+
guard();
|
|
17190
|
+
return;
|
|
17191
|
+
} catch (error) {
|
|
17192
|
+
return describeAwsFailure(error).detail;
|
|
17193
|
+
}
|
|
17194
|
+
}
|
|
17195
|
+
/** The members of a `StreamSpecification` block, or none for a non-object. */
|
|
17196
|
+
function blockMembers(block) {
|
|
17197
|
+
return isPlainObject$1(block) ? block : {};
|
|
17198
|
+
}
|
|
17199
|
+
/**
|
|
17200
|
+
* Read `StreamSpecification.ResourcePolicy`.
|
|
17201
|
+
*
|
|
17202
|
+
* The CFn shape is `{ PolicyDocument: <object | JSON string> }` and the
|
|
17203
|
+
* document is REQUIRED, so a block without one is unreadable rather than "no
|
|
17204
|
+
* policy": reading it as absent would DELETE the live policy on an update.
|
|
17205
|
+
*/
|
|
17206
|
+
function readStreamPolicy(block) {
|
|
17207
|
+
const value = blockMembers(block)[STREAM_POLICY_KEY];
|
|
17208
|
+
if (value === void 0 || value === null) return { kind: "absent" };
|
|
17209
|
+
const containerRefusal = refusalOf(() => requireConfigObject(value, POLICY_PATH));
|
|
17210
|
+
if (containerRefusal !== void 0) return {
|
|
17211
|
+
kind: "unusable",
|
|
17212
|
+
reason: containerRefusal
|
|
17213
|
+
};
|
|
17214
|
+
const documentPath = `${POLICY_PATH}.PolicyDocument`;
|
|
17215
|
+
const document = value["PolicyDocument"];
|
|
17216
|
+
if (typeof document === "string") return document.trim() === "" ? {
|
|
17217
|
+
kind: "unusable",
|
|
17218
|
+
reason: `${documentPath} must not be a blank string`
|
|
17219
|
+
} : {
|
|
17220
|
+
kind: "usable",
|
|
17221
|
+
document
|
|
17222
|
+
};
|
|
17223
|
+
if (document === void 0 || document === null) return {
|
|
17224
|
+
kind: "unusable",
|
|
17225
|
+
reason: `${documentPath} is required when ${POLICY_PATH} is declared`
|
|
17226
|
+
};
|
|
17227
|
+
const documentRefusal = refusalOf(() => requireConfigObject(document, documentPath));
|
|
17228
|
+
if (documentRefusal !== void 0) return {
|
|
17229
|
+
kind: "unusable",
|
|
17230
|
+
reason: documentRefusal
|
|
17231
|
+
};
|
|
17232
|
+
return {
|
|
17233
|
+
kind: "usable",
|
|
17234
|
+
document: JSON.stringify(document)
|
|
17235
|
+
};
|
|
17236
|
+
}
|
|
17237
|
+
/**
|
|
17238
|
+
* Read `StreamSpecification.Tags`.
|
|
17239
|
+
*
|
|
17240
|
+
* Refusals name an entry by POSITION, never by its key: the key is a RESOLVED
|
|
17241
|
+
* template value, and a copy embedded here would reach only the substring
|
|
17242
|
+
* mask, which ignores a value shorter than its minimum needle.
|
|
17243
|
+
*
|
|
17244
|
+
* ONE unreadable entry makes the whole list unusable. Filtering it out would
|
|
17245
|
+
* send a list nobody declared, and on an update would untag a key the
|
|
17246
|
+
* unreadable entry may well have named.
|
|
17247
|
+
*/
|
|
17248
|
+
function readStreamTags(block) {
|
|
17249
|
+
const value = blockMembers(block)[STREAM_TAGS_KEY];
|
|
17250
|
+
if (value === void 0 || value === null) return { kind: "absent" };
|
|
17251
|
+
if (!Array.isArray(value)) return {
|
|
17252
|
+
kind: "unusable",
|
|
17253
|
+
reason: `${TAGS_PATH} must be an array of {Key, Value} entries`
|
|
17254
|
+
};
|
|
17255
|
+
const tags = /* @__PURE__ */ new Map();
|
|
17256
|
+
for (const [position, entry] of value.entries()) {
|
|
17257
|
+
const entryPath = `${TAGS_PATH}[${position}]`;
|
|
17258
|
+
const refusal = refusalOf(() => requireConfigObject(entry, entryPath)) ?? configStringRefusal(entry, "Key", "required", entryPath) ?? configStringRefusal(entry, "Value", "", entryPath);
|
|
17259
|
+
if (refusal !== void 0) return {
|
|
17260
|
+
kind: "unusable",
|
|
17261
|
+
reason: refusal
|
|
17262
|
+
};
|
|
17263
|
+
const { Key: key, Value: tagValue } = entry;
|
|
17264
|
+
if (typeof key !== "string") return {
|
|
17265
|
+
kind: "unusable",
|
|
17266
|
+
reason: `${entryPath}.Key is required`
|
|
17267
|
+
};
|
|
17268
|
+
tags.set(key, typeof tagValue === "string" ? tagValue : "");
|
|
17269
|
+
}
|
|
17270
|
+
return {
|
|
17271
|
+
kind: "usable",
|
|
17272
|
+
tags
|
|
17273
|
+
};
|
|
17274
|
+
}
|
|
17275
|
+
/**
|
|
17276
|
+
* Every refusal a CREATE must raise before `CreateTable` goes out. A create has
|
|
17277
|
+
* no live setting to leave alone, so an unreadable member there is a template
|
|
17278
|
+
* error, not a skip — and refusing after the table exists would create it only
|
|
17279
|
+
* to delete it again.
|
|
17280
|
+
*/
|
|
17281
|
+
function streamMemberRefusals(block) {
|
|
17282
|
+
const refusals = [];
|
|
17283
|
+
const policy = readStreamPolicy(block);
|
|
17284
|
+
if (policy.kind === "unusable") refusals.push(policy.reason);
|
|
17285
|
+
const tags = readStreamTags(block);
|
|
17286
|
+
if (tags.kind === "unusable") refusals.push(tags.reason);
|
|
17287
|
+
return refusals;
|
|
17288
|
+
}
|
|
17289
|
+
/**
|
|
17290
|
+
* The calls one update (or create) needs against the CURRENT stream arn.
|
|
17291
|
+
*
|
|
17292
|
+
* `freshStream` says the arn the calls will address was minted by THIS
|
|
17293
|
+
* operation — a create, an update-time enable, or the disable / re-enable a
|
|
17294
|
+
* `StreamViewType` change is applied as. A fresh stream holds nothing, so the
|
|
17295
|
+
* previous side is irrelevant there: every readable declared member is
|
|
17296
|
+
* applied, and nothing is deleted. Otherwise the stream is the one the previous
|
|
17297
|
+
* side was applied to, and the plan is the difference. `contentsUnknown` is
|
|
17298
|
+
* the third case: a live stream this operation did NOT mint and the previous
|
|
17299
|
+
* side does not describe (a re-run after a deploy that enabled the stream,
|
|
17300
|
+
* applied a policy, and failed before state recorded any of it). It may hold a
|
|
17301
|
+
* policy, so it is never planned as empty: a policy the desired side does not
|
|
17302
|
+
* declare is deleted, and a declared one is always put.
|
|
17303
|
+
*
|
|
17304
|
+
* On a stream the previous side describes:
|
|
17305
|
+
*
|
|
17306
|
+
* - a member `absent` with a DECLARED previous is a template REMOVAL and is
|
|
17307
|
+
* removed (CloudFormation does the same). Because the rule is symmetric in
|
|
17308
|
+
* its two sides, a rollback — which replays `update()` with the sides SWAPPED
|
|
17309
|
+
* — restores the old policy and tags through the same arms, a view-type
|
|
17310
|
+
* change included (the rollback mints yet another arn, and `freshStream`
|
|
17311
|
+
* applies the old members to it).
|
|
17312
|
+
* - an `unusable` member reaches this plan only from a STATE-borne caller (a
|
|
17313
|
+
* rollback replay, `drift --revert`): the provider refuses it on the template
|
|
17314
|
+
* path before any call. It reports through `onUnusable` and the live setting
|
|
17315
|
+
* is left alone rather than overwritten with a guess: no call on a stream
|
|
17316
|
+
* that stays, and the PREVIOUS member carried over onto a fresh one.
|
|
17317
|
+
* - an `unusable` PREVIOUS tag list cannot say which keys cdkd applied, so a
|
|
17318
|
+
* removal against it untags nothing.
|
|
17319
|
+
*
|
|
17320
|
+
* ORDER is part of the contract, because a resource policy is an access grant
|
|
17321
|
+
* and can condition on `aws:ResourceTag`: `deletePolicy` comes FIRST and
|
|
17322
|
+
* `putPolicy` LAST. A policy that CHANGES together with the tags is therefore
|
|
17323
|
+
* deleted before the first tag call and put back after the last, so neither
|
|
17324
|
+
* the old policy nor the new one is ever evaluated against the other side's
|
|
17325
|
+
* tag set — also when a call in between fails. The price is a window with NO
|
|
17326
|
+
* policy, which drops that policy's `Deny` statements along with its grants;
|
|
17327
|
+
* nothing here is atomic, and that window is the narrower of the two hazards.
|
|
17328
|
+
* An UNCHANGED policy stays in place across a tag-only change: both sides
|
|
17329
|
+
* declare it, and the two tag calls cannot be made one.
|
|
17330
|
+
*
|
|
17331
|
+
* The caller asks only while the DESIRED side has a stream: a disabled stream
|
|
17332
|
+
* is gone and takes both members with it, so nothing addresses its dead arn.
|
|
17333
|
+
*/
|
|
17334
|
+
function planStreamMemberOps(desiredBlock, previousBlock, options, onUnusable) {
|
|
17335
|
+
let desiredPolicy = readStreamPolicy(desiredBlock);
|
|
17336
|
+
let desiredTags = readStreamTags(desiredBlock);
|
|
17337
|
+
if (desiredPolicy.kind === "unusable") onUnusable(desiredPolicy.reason);
|
|
17338
|
+
if (desiredTags.kind === "unusable") onUnusable(desiredTags.reason);
|
|
17339
|
+
let previousPolicy = readStreamPolicy(previousBlock);
|
|
17340
|
+
let previousTags = readStreamTags(previousBlock);
|
|
17341
|
+
if (options.freshStream) {
|
|
17342
|
+
if (desiredPolicy.kind === "unusable" && previousPolicy.kind === "usable") desiredPolicy = previousPolicy;
|
|
17343
|
+
if (desiredTags.kind === "unusable" && previousTags.kind === "usable") desiredTags = previousTags;
|
|
17344
|
+
previousPolicy = { kind: "absent" };
|
|
17345
|
+
previousTags = { kind: "absent" };
|
|
17346
|
+
} else if (options.contentsUnknown === true) {
|
|
17347
|
+
previousPolicy = {
|
|
17348
|
+
kind: "unusable",
|
|
17349
|
+
reason: ""
|
|
17350
|
+
};
|
|
17351
|
+
previousTags = { kind: "absent" };
|
|
17352
|
+
}
|
|
17353
|
+
const tagOps = [];
|
|
17354
|
+
if (desiredTags.kind !== "unusable") {
|
|
17355
|
+
const desired = desiredTags.kind === "usable" ? desiredTags.tags : /* @__PURE__ */ new Map();
|
|
17356
|
+
const previous = previousTags.kind === "usable" ? previousTags.tags : /* @__PURE__ */ new Map();
|
|
17357
|
+
const keysToRemove = [...previous.keys()].filter((key) => !desired.has(key));
|
|
17358
|
+
if (keysToRemove.length > 0) tagOps.push({
|
|
17359
|
+
kind: "untag",
|
|
17360
|
+
keys: keysToRemove
|
|
17361
|
+
});
|
|
17362
|
+
const tagsToSet = [...desired].filter(([key, value]) => previous.get(key) !== value).map(([Key, Value]) => ({
|
|
17363
|
+
Key,
|
|
17364
|
+
Value
|
|
17365
|
+
}));
|
|
17366
|
+
if (tagsToSet.length > 0) tagOps.push({
|
|
17367
|
+
kind: "tag",
|
|
17368
|
+
tags: tagsToSet
|
|
17369
|
+
});
|
|
17370
|
+
}
|
|
17371
|
+
const policyPut = desiredPolicy.kind === "usable" && !(previousPolicy.kind === "usable" && samePolicyDocument(previousPolicy.document, desiredPolicy.document));
|
|
17372
|
+
const policyDelete = previousPolicy.kind !== "absent" && (desiredPolicy.kind === "absent" || policyPut && tagOps.length > 0);
|
|
17373
|
+
const ops = [];
|
|
17374
|
+
if (policyDelete) ops.push({ kind: "deletePolicy" });
|
|
17375
|
+
ops.push(...tagOps);
|
|
17376
|
+
if (policyPut && desiredPolicy.kind === "usable") ops.push({
|
|
17377
|
+
kind: "putPolicy",
|
|
17378
|
+
document: desiredPolicy.document
|
|
17379
|
+
});
|
|
17380
|
+
return ops;
|
|
17381
|
+
}
|
|
17382
|
+
/** Two policy JSON strings saying the same thing, whatever their key order. */
|
|
17383
|
+
function samePolicyDocument(left, right) {
|
|
17384
|
+
if (left === right) return true;
|
|
17385
|
+
try {
|
|
17386
|
+
return canonicalJson$1(JSON.parse(left)) === canonicalJson$1(JSON.parse(right));
|
|
17387
|
+
} catch {
|
|
17388
|
+
return false;
|
|
17389
|
+
}
|
|
17390
|
+
}
|
|
17391
|
+
/** The drift read-back gates: one API call each, so each is asked separately. */
|
|
17392
|
+
function streamDeclaresPolicy(desiredBlock) {
|
|
17393
|
+
return readStreamPolicy(desiredBlock).kind === "usable";
|
|
17394
|
+
}
|
|
17395
|
+
/** See {@link streamDeclaresPolicy}. */
|
|
17396
|
+
function streamDeclaresTags(desiredBlock) {
|
|
17397
|
+
return readStreamTags(desiredBlock).kind === "usable";
|
|
17398
|
+
}
|
|
17399
|
+
/**
|
|
17400
|
+
* Re-shape a `GetResourcePolicy` answer into the CFn member, in the DECLARED
|
|
17401
|
+
* spelling of `PolicyDocument`: a template may carry the document as an object
|
|
17402
|
+
* or as a JSON string, and the comparator does not convert between them.
|
|
17403
|
+
* `undefined` when AWS holds no policy.
|
|
17404
|
+
*/
|
|
17405
|
+
function reverseMapStreamPolicy(livePolicy, desiredBlock) {
|
|
17406
|
+
if (livePolicy === void 0 || livePolicy === "") return void 0;
|
|
17407
|
+
const declared = blockMembers(blockMembers(desiredBlock)[STREAM_POLICY_KEY])["PolicyDocument"];
|
|
17408
|
+
let parsed;
|
|
17409
|
+
try {
|
|
17410
|
+
parsed = JSON.parse(livePolicy);
|
|
17411
|
+
} catch {
|
|
17412
|
+
return { PolicyDocument: livePolicy };
|
|
17413
|
+
}
|
|
17414
|
+
if (typeof declared !== "string") return { PolicyDocument: parsed };
|
|
17415
|
+
let declaredParsed;
|
|
17416
|
+
try {
|
|
17417
|
+
declaredParsed = JSON.parse(declared);
|
|
17418
|
+
} catch {
|
|
17419
|
+
return { PolicyDocument: livePolicy };
|
|
17420
|
+
}
|
|
17421
|
+
return { PolicyDocument: canonicalJson$1(declaredParsed) === canonicalJson$1(parsed) ? declared : livePolicy };
|
|
17422
|
+
}
|
|
17423
|
+
/**
|
|
17424
|
+
* Does the live policy say what the DECLARED one says? The read-back's
|
|
17425
|
+
* "settled" test: `GetResourcePolicy` is eventually consistent and can answer
|
|
17426
|
+
* with the PREVIOUS policy right after a put, so an answer that differs from
|
|
17427
|
+
* the declaration is re-asked before it is believed.
|
|
17428
|
+
*/
|
|
17429
|
+
function streamPolicyMatchesDeclared(livePolicy, desiredBlock) {
|
|
17430
|
+
const declared = readStreamPolicy(desiredBlock);
|
|
17431
|
+
if (declared.kind !== "usable" || livePolicy === void 0) return false;
|
|
17432
|
+
try {
|
|
17433
|
+
return canonicalJson$1(JSON.parse(livePolicy)) === canonicalJson$1(JSON.parse(declared.document));
|
|
17434
|
+
} catch {
|
|
17435
|
+
return false;
|
|
17436
|
+
}
|
|
17437
|
+
}
|
|
17438
|
+
/** The tag twin of {@link streamPolicyMatchesDeclared}, over USER tags. */
|
|
17439
|
+
function streamTagsMatchDeclared(liveTags, desiredBlock) {
|
|
17440
|
+
const declared = readStreamTags(desiredBlock);
|
|
17441
|
+
if (declared.kind !== "usable" || liveTags.length !== declared.tags.size) return false;
|
|
17442
|
+
return liveTags.every((tag) => declared.tags.get(tag.Key) === tag.Value);
|
|
17443
|
+
}
|
|
17444
|
+
/** A key-order-independent serialization. */
|
|
17445
|
+
function canonicalJson$1(value) {
|
|
17446
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson$1).join(",")}]`;
|
|
17447
|
+
if (isPlainObject$1(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson$1(value[key])}`).join(",")}}`;
|
|
17448
|
+
return JSON.stringify(value) ?? "null";
|
|
17449
|
+
}
|
|
17450
|
+
|
|
16946
17451
|
//#endregion
|
|
16947
17452
|
//#region src/provisioning/dynamodb-index-busy-delete.ts
|
|
16948
17453
|
/**
|
|
@@ -18767,16 +19272,10 @@ function warmThroughputAlreadyMatches(desired, live) {
|
|
|
18767
19272
|
* to decide with and every block is emitted, cleaned of its AWS-managed
|
|
18768
19273
|
* members.
|
|
18769
19274
|
*
|
|
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).
|
|
19275
|
+
* CFn's per-index `ContributorInsightsSpecification` is NOT mapped here: it
|
|
19276
|
+
* has no `DescribeTable` counterpart, so `readCurrentState` adds it afterwards
|
|
19277
|
+
* from a gated per-index `DescribeContributorInsights` (issue
|
|
19278
|
+
* [#1782](https://github.com/go-to-k/cdkd/issues/1782)).
|
|
18780
19279
|
*/
|
|
18781
19280
|
function reverseMapSecondaryIndex(live, desired, bagInformative) {
|
|
18782
19281
|
const out = {};
|
|
@@ -19042,6 +19541,12 @@ var DynamoDBTableProvider = class {
|
|
|
19042
19541
|
let tableCreated = false;
|
|
19043
19542
|
try {
|
|
19044
19543
|
const billingMode = requireConfigString(properties["BillingMode"], "PROVISIONED", "AWS::DynamoDB::Table BillingMode", replayWarn(this.logger, context));
|
|
19544
|
+
if (context?.replayingState !== true) {
|
|
19545
|
+
const [refusal] = contributorInsightsRefusals(properties["ContributorInsightsSpecification"], properties["GlobalSecondaryIndexes"]);
|
|
19546
|
+
if (refusal !== void 0) throw new Error(`${refusal}. Fix the template value`);
|
|
19547
|
+
const [streamRefusal] = streamMemberRefusals(properties["StreamSpecification"]);
|
|
19548
|
+
if (streamRefusal !== void 0) throw new Error(`${streamRefusal}. Fix the template value`);
|
|
19549
|
+
}
|
|
19045
19550
|
const createParams = {
|
|
19046
19551
|
TableName: tableName,
|
|
19047
19552
|
KeySchema: keySchema,
|
|
@@ -19066,7 +19571,7 @@ var DynamoDBTableProvider = class {
|
|
|
19066
19571
|
};
|
|
19067
19572
|
if (properties["GlobalSecondaryIndexes"]) {
|
|
19068
19573
|
const declaredGsis = properties["GlobalSecondaryIndexes"];
|
|
19069
|
-
createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexThroughputForCreate(logicalId, entry, maskSecrets)) : declaredGsis;
|
|
19574
|
+
createParams.GlobalSecondaryIndexes = Array.isArray(declaredGsis) ? declaredGsis.map((entry) => this.coerceIndexThroughputForCreate(logicalId, stripIndexContributorInsights(entry), maskSecrets)) : declaredGsis;
|
|
19070
19575
|
}
|
|
19071
19576
|
if (properties["LocalSecondaryIndexes"]) createParams.LocalSecondaryIndexes = properties["LocalSecondaryIndexes"];
|
|
19072
19577
|
const sse = mapSSESpecification(properties["SSESpecification"]);
|
|
@@ -19083,7 +19588,9 @@ var DynamoDBTableProvider = class {
|
|
|
19083
19588
|
await this.applyPointInTimeRecovery(tableName, properties["PointInTimeRecoverySpecification"], void 0, maskSecrets);
|
|
19084
19589
|
await this.applyTimeToLive(tableName, properties["TimeToLiveSpecification"]);
|
|
19085
19590
|
await this.applyKinesisStreamingDestination(tableName, properties["KinesisStreamSpecification"], void 0, maskSecrets);
|
|
19086
|
-
await this.applyContributorInsights(tableName, properties["ContributorInsightsSpecification"]);
|
|
19591
|
+
await this.applyContributorInsights(tableName, properties["ContributorInsightsSpecification"], void 0, maskSecrets);
|
|
19592
|
+
await this.applyIndexContributorInsights(tableName, properties["GlobalSecondaryIndexes"], void 0, maskSecrets);
|
|
19593
|
+
await this.applyStreamMembers(logicalId, resourceType, tableName, tableInfo.streamArn, properties["StreamSpecification"], void 0, true, maskSecrets);
|
|
19087
19594
|
this.logger.debug(`Successfully created DynamoDB table ${logicalId}: ${tableName}`);
|
|
19088
19595
|
return {
|
|
19089
19596
|
physicalId: tableName,
|
|
@@ -19119,6 +19626,10 @@ var DynamoDBTableProvider = class {
|
|
|
19119
19626
|
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
19120
19627
|
debug(`Updating DynamoDB table ${logicalId}: ${maskSecrets(physicalId)}`);
|
|
19121
19628
|
try {
|
|
19629
|
+
if (context?.replayingState !== true && context?.desiredFromAwsReadback !== true && JSON.stringify(properties["StreamSpecification"]) !== JSON.stringify(previousProperties["StreamSpecification"])) {
|
|
19630
|
+
const [streamRefusal] = streamMemberRefusals(properties["StreamSpecification"]);
|
|
19631
|
+
if (streamRefusal !== void 0) throw new ProvisioningError(`${streamRefusal}. Nothing was applied to DynamoDB table ${logicalId}; fix the template value`, resourceType, logicalId, physicalId);
|
|
19632
|
+
}
|
|
19122
19633
|
const table = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table;
|
|
19123
19634
|
let latestStreamArn = table?.LatestStreamArn;
|
|
19124
19635
|
if (table?.TableArn) await this.applyTagDiff(table.TableArn, previousProperties["Tags"], properties["Tags"]);
|
|
@@ -19270,6 +19781,10 @@ var DynamoDBTableProvider = class {
|
|
|
19270
19781
|
if (JSON.stringify(properties["StreamSpecification"]) !== JSON.stringify(previousProperties["StreamSpecification"])) {
|
|
19271
19782
|
const newViewType = this.extractStreamViewType(properties["StreamSpecification"]);
|
|
19272
19783
|
const prevViewType = this.extractStreamViewType(previousProperties["StreamSpecification"]);
|
|
19784
|
+
const liveStream = table?.StreamSpecification;
|
|
19785
|
+
const preUpdateStreamArn = table?.LatestStreamArn;
|
|
19786
|
+
let mintedStream = false;
|
|
19787
|
+
let contentsUnknown = false;
|
|
19273
19788
|
if (newViewType && prevViewType && newViewType !== prevViewType) {
|
|
19274
19789
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
19275
19790
|
TableName: physicalId,
|
|
@@ -19284,9 +19799,11 @@ var DynamoDBTableProvider = class {
|
|
|
19284
19799
|
}
|
|
19285
19800
|
}));
|
|
19286
19801
|
await this.waitForTableActiveAfterUpdate(physicalId);
|
|
19287
|
-
latestStreamArn = reenable.TableDescription?.LatestStreamArn
|
|
19802
|
+
latestStreamArn = await this.resolveMintedStreamArn(physicalId, reenable.TableDescription?.LatestStreamArn, preUpdateStreamArn);
|
|
19803
|
+
mintedStream = true;
|
|
19288
19804
|
this.logger.debug(`Changed StreamViewType on DynamoDB table ${physicalId} to ${newViewType}`);
|
|
19289
|
-
} else if (newViewType)
|
|
19805
|
+
} else if (newViewType && liveStream?.StreamEnabled === true && liveStream.StreamViewType === newViewType) contentsUnknown = prevViewType !== newViewType;
|
|
19806
|
+
else if (newViewType) {
|
|
19290
19807
|
const enable = await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
19291
19808
|
TableName: physicalId,
|
|
19292
19809
|
StreamSpecification: {
|
|
@@ -19295,7 +19812,8 @@ var DynamoDBTableProvider = class {
|
|
|
19295
19812
|
}
|
|
19296
19813
|
}));
|
|
19297
19814
|
await this.waitForTableActiveAfterUpdate(physicalId);
|
|
19298
|
-
latestStreamArn = enable.TableDescription?.LatestStreamArn
|
|
19815
|
+
latestStreamArn = await this.resolveMintedStreamArn(physicalId, enable.TableDescription?.LatestStreamArn, preUpdateStreamArn);
|
|
19816
|
+
mintedStream = true;
|
|
19299
19817
|
this.logger.debug(`Enabled DynamoDB Stream on table ${physicalId} (${newViewType})`);
|
|
19300
19818
|
} else {
|
|
19301
19819
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
@@ -19306,6 +19824,7 @@ var DynamoDBTableProvider = class {
|
|
|
19306
19824
|
latestStreamArn = void 0;
|
|
19307
19825
|
this.logger.debug(`Disabled DynamoDB Stream on table ${physicalId}`);
|
|
19308
19826
|
}
|
|
19827
|
+
if (newViewType) await this.applyStreamMembers(logicalId, resourceType, physicalId, latestStreamArn, properties["StreamSpecification"], previousProperties["StreamSpecification"], mintedStream, maskSecrets, mintedStream ? preUpdateStreamArn : void 0, contentsUnknown);
|
|
19309
19828
|
}
|
|
19310
19829
|
if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip, currentLiveIndexNames, currentLiveIndexByName, liveCapacityComparable, effectiveBillingMode === "PAY_PER_REQUEST", maskSecrets);
|
|
19311
19830
|
if (JSON.stringify(properties["PointInTimeRecoverySpecification"]) !== JSON.stringify(previousProperties["PointInTimeRecoverySpecification"])) await this.applyPointInTimeRecovery(physicalId, properties["PointInTimeRecoverySpecification"], previousProperties["PointInTimeRecoverySpecification"], maskSecrets);
|
|
@@ -19318,7 +19837,8 @@ var DynamoDBTableProvider = class {
|
|
|
19318
19837
|
await this.applyResourcePolicy(table.TableArn, properties["ResourcePolicy"], previousProperties["ResourcePolicy"]);
|
|
19319
19838
|
}
|
|
19320
19839
|
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"]);
|
|
19840
|
+
if (JSON.stringify(properties["ContributorInsightsSpecification"]) !== JSON.stringify(previousProperties["ContributorInsightsSpecification"])) await this.applyContributorInsights(physicalId, properties["ContributorInsightsSpecification"], previousProperties["ContributorInsightsSpecification"], maskSecrets);
|
|
19841
|
+
await this.applyIndexContributorInsights(physicalId, properties["GlobalSecondaryIndexes"], previousProperties["GlobalSecondaryIndexes"], maskSecrets);
|
|
19322
19842
|
return {
|
|
19323
19843
|
physicalId,
|
|
19324
19844
|
wasReplaced: false,
|
|
@@ -19535,15 +20055,16 @@ var DynamoDBTableProvider = class {
|
|
|
19535
20055
|
* Backoff: ~2s,4s,8s,16s,30s,30s... bounded to ~2min total, which comfortably
|
|
19536
20056
|
* covers the few-second PITR-enable window.
|
|
19537
20057
|
*/
|
|
19538
|
-
async retryOnTransientControlPlane(op, label, maxAttempts = 8) {
|
|
20058
|
+
async retryOnTransientControlPlane(op, label, maxAttempts = 8, options = {}) {
|
|
19539
20059
|
let delayMs = 2e3;
|
|
19540
20060
|
for (let attempt = 1;; attempt++) try {
|
|
19541
20061
|
return await op();
|
|
19542
20062
|
} catch (error) {
|
|
19543
20063
|
const msg = describeAwsFailure(error).detail;
|
|
19544
20064
|
const name = error instanceof Error ? error.name : "";
|
|
19545
|
-
if (!(/being enabled|being updated|please retry later|backups are being/i.test(msg) || name === "ResourceInUseException" || name === "LimitExceededException") || attempt >= maxAttempts) throw error;
|
|
19546
|
-
|
|
20065
|
+
if (!(/being enabled|being updated|please retry later|backups are being/i.test(msg) || name === "ResourceInUseException" || name === "LimitExceededException" || options.alsoTransient?.(error) === true) || attempt >= maxAttempts) throw error;
|
|
20066
|
+
const retryLine = `Transient error on "${label}" (attempt ${attempt}/${maxAttempts}): ${msg} — retrying in ${delayMs}ms`;
|
|
20067
|
+
this.logger.debug(options.maskSecrets ? options.maskSecrets(retryLine) : retryLine);
|
|
19547
20068
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
19548
20069
|
delayMs = Math.min(delayMs * 2, 3e4);
|
|
19549
20070
|
}
|
|
@@ -19719,6 +20240,90 @@ var DynamoDBTableProvider = class {
|
|
|
19719
20240
|
return typeof viewType === "string" && viewType.length > 0 ? viewType : void 0;
|
|
19720
20241
|
}
|
|
19721
20242
|
/**
|
|
20243
|
+
* Apply `StreamSpecification.ResourcePolicy` / `.Tags` (issue
|
|
20244
|
+
* [#3458](https://github.com/go-to-k/cdkd/issues/3458)) against the STREAM
|
|
20245
|
+
* arn. Neither is a member of the SDK's `StreamSpecification`, so until this
|
|
20246
|
+
* existed both were dropped on `CreateTable` / `UpdateTable`: the deploy was
|
|
20247
|
+
* green and the stream had no policy.
|
|
20248
|
+
*
|
|
20249
|
+
* `streamArn` must be the table's CURRENT `LatestStreamArn` — after a
|
|
20250
|
+
* `StreamViewType` change that is the arn the re-enable minted, never the
|
|
20251
|
+
* recorded one. What to call, in which order, and the removal / rollback /
|
|
20252
|
+
* unreadable-member rules live on {@link planStreamMemberOps}.
|
|
20253
|
+
*
|
|
20254
|
+
* A plan with calls to make and no arn to make them against — or, when this
|
|
20255
|
+
* operation minted the stream, only the `deadStreamArn` read before it —
|
|
20256
|
+
* fails LOUDLY: a
|
|
20257
|
+
* silent skip would record the policy in state as applied, the next deploy
|
|
20258
|
+
* would see no difference, and the grant would stay missing for good.
|
|
20259
|
+
*
|
|
20260
|
+
* The policy text is never logged: it names principals and can carry a
|
|
20261
|
+
* resolved secret in a condition value.
|
|
20262
|
+
*/
|
|
20263
|
+
async applyStreamMembers(logicalId, resourceType, tableName, streamArn, desiredBlock, previousBlock, freshStream, maskSecrets, deadStreamArn, contentsUnknown = false) {
|
|
20264
|
+
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
20265
|
+
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
20266
|
+
const ops = planStreamMemberOps(desiredBlock, previousBlock, {
|
|
20267
|
+
freshStream,
|
|
20268
|
+
contentsUnknown
|
|
20269
|
+
}, (reason) => warn(`DynamoDB table ${tableName}: ${reason}, so that member of the stream was left as it is. The value came from a recorded state, which a template edit cannot reach.`));
|
|
20270
|
+
if (ops.length === 0) return;
|
|
20271
|
+
if (streamArn === void 0 || streamArn === "" || streamArn === deadStreamArn) throw new ProvisioningError(`Cannot apply StreamSpecification.ResourcePolicy / Tags for DynamoDB table ${logicalId}: DescribeTable returned no ${deadStreamArn === void 0 ? "" : "new "}LatestStreamArn`, resourceType, logicalId, tableName);
|
|
20272
|
+
for (const op of ops) {
|
|
20273
|
+
await this.sendStreamMemberOp(streamArn, op, freshStream, maskSecrets);
|
|
20274
|
+
debug(`Applied ${op.kind} to the stream of DynamoDB table ${tableName}`);
|
|
20275
|
+
}
|
|
20276
|
+
}
|
|
20277
|
+
/** Issue ONE {@link StreamMemberOp} against the stream arn. */
|
|
20278
|
+
async sendStreamMemberOp(streamArn, op, freshStream, maskSecrets) {
|
|
20279
|
+
const target = maskSecrets(streamArn);
|
|
20280
|
+
const retryOptions = {
|
|
20281
|
+
maskSecrets,
|
|
20282
|
+
...freshStream ? { alsoTransient: (error) => error instanceof ResourceNotFoundException$1 } : {}
|
|
20283
|
+
};
|
|
20284
|
+
const send = (call, label) => this.retryOnTransientControlPlane(call, label, void 0, retryOptions);
|
|
20285
|
+
switch (op.kind) {
|
|
20286
|
+
case "deletePolicy":
|
|
20287
|
+
try {
|
|
20288
|
+
await send(() => this.dynamoDBClient.send(new DeleteResourcePolicyCommand({ ResourceArn: streamArn })), `delete the stream ResourcePolicy of ${target}`);
|
|
20289
|
+
} catch (error) {
|
|
20290
|
+
if (!(error instanceof PolicyNotFoundException)) throw error;
|
|
20291
|
+
}
|
|
20292
|
+
return;
|
|
20293
|
+
case "untag":
|
|
20294
|
+
await send(() => this.dynamoDBClient.send(new UntagResourceCommand$2({
|
|
20295
|
+
ResourceArn: streamArn,
|
|
20296
|
+
TagKeys: op.keys
|
|
20297
|
+
})), `untag the stream ${target}`);
|
|
20298
|
+
return;
|
|
20299
|
+
case "tag":
|
|
20300
|
+
await send(() => this.dynamoDBClient.send(new TagResourceCommand$2({
|
|
20301
|
+
ResourceArn: streamArn,
|
|
20302
|
+
Tags: op.tags
|
|
20303
|
+
})), `tag the stream ${target}`);
|
|
20304
|
+
return;
|
|
20305
|
+
case "putPolicy":
|
|
20306
|
+
await send(() => this.dynamoDBClient.send(new PutResourcePolicyCommand({
|
|
20307
|
+
ResourceArn: streamArn,
|
|
20308
|
+
Policy: op.document
|
|
20309
|
+
})), `put the stream ResourcePolicy of ${target}`);
|
|
20310
|
+
return;
|
|
20311
|
+
}
|
|
20312
|
+
}
|
|
20313
|
+
/**
|
|
20314
|
+
* The arn of the stream an enabling `UpdateTable` just minted.
|
|
20315
|
+
*
|
|
20316
|
+
* The arn that call echoes is believed only when it DIFFERS from the one read
|
|
20317
|
+
* before the update: DynamoDB keeps reporting a disabled stream's arn as
|
|
20318
|
+
* `LatestStreamArn`, so an echo equal to it (or no echo) is re-read from
|
|
20319
|
+
* `DescribeTable` after the ACTIVE wait. Shared by the enable and the
|
|
20320
|
+
* view-type arms — a table whose stream was disabled by an earlier deploy is
|
|
20321
|
+
* in the same state as one mid view-type change.
|
|
20322
|
+
*/
|
|
20323
|
+
async resolveMintedStreamArn(tableName, echoedArn, preUpdateArn) {
|
|
20324
|
+
return echoedArn !== void 0 && echoedArn !== preUpdateArn ? echoedArn : this.describeLatestStreamArn(tableName);
|
|
20325
|
+
}
|
|
20326
|
+
/**
|
|
19722
20327
|
* Fetch the table's current `LatestStreamArn` via DescribeTable. Used as a
|
|
19723
20328
|
* fallback when an UpdateTable that enabled a stream did not echo the ARN
|
|
19724
20329
|
* back in its TableDescription, so `Fn::GetAtt [Table, StreamArn]` still
|
|
@@ -19734,24 +20339,53 @@ var DynamoDBTableProvider = class {
|
|
|
19734
20339
|
*
|
|
19735
20340
|
* Called from both `create()` (after the table is ACTIVE) and `update()`
|
|
19736
20341
|
* (only when the value changed). On `update()`-side removal — template drops
|
|
19737
|
-
* the block but it was present before — insights is disabled.
|
|
20342
|
+
* the block but it was present before — insights is disabled. The block is
|
|
20343
|
+
* read by the reader the per-index twin uses
|
|
20344
|
+
* ({@link applyIndexContributorInsights}), so the two cannot disagree about
|
|
20345
|
+
* one shape: an unreadable block is announced and SKIPPED rather than read as
|
|
20346
|
+
* a default (`Boolean('false')` used to ENABLE what the template disabled).
|
|
19738
20347
|
*/
|
|
19739
|
-
async applyContributorInsights(tableName, spec, previousSpec) {
|
|
19740
|
-
|
|
19741
|
-
|
|
19742
|
-
if (
|
|
19743
|
-
|
|
19744
|
-
|
|
19745
|
-
|
|
19746
|
-
|
|
19747
|
-
|
|
19748
|
-
|
|
20348
|
+
async applyContributorInsights(tableName, spec, previousSpec, maskSecrets) {
|
|
20349
|
+
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
20350
|
+
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.`));
|
|
20351
|
+
if (op === void 0) return;
|
|
20352
|
+
await this.sendContributorInsightsOp(tableName, op, maskSecrets);
|
|
20353
|
+
}
|
|
20354
|
+
/**
|
|
20355
|
+
* Apply every per-index `ContributorInsightsSpecification`
|
|
20356
|
+
* (`GlobalSecondaryIndexes[].ContributorInsightsSpecification`, issue
|
|
20357
|
+
* [#1782](https://github.com/go-to-k/cdkd/issues/1782)) — the table-level
|
|
20358
|
+
* call with `IndexName` added, one call per index whose block changed.
|
|
20359
|
+
*
|
|
20360
|
+
* The block is not a member of the SDK's `GlobalSecondaryIndex`, so until
|
|
20361
|
+
* this existed it was dropped by the serializer on `CreateTable` and never
|
|
20362
|
+
* sent by `applyGsiUpdates`: the deploy was green and the feature was off.
|
|
20363
|
+
*
|
|
20364
|
+
* `UpdateContributorInsights` answers `ResourceNotFoundException` for an
|
|
20365
|
+
* index that is not ACTIVE yet, which {@link retryOnTransientControlPlane}
|
|
20366
|
+
* does not retry, so the index wait runs FIRST — and only when there is a
|
|
20367
|
+
* call to make, so an ordinary update pays no extra `DescribeTable`.
|
|
20368
|
+
* `previousIndexes` is `undefined` on `create()`. Removal and rollback
|
|
20369
|
+
* semantics live on {@link planContributorInsightsOp}.
|
|
20370
|
+
*/
|
|
20371
|
+
async applyIndexContributorInsights(tableName, desiredIndexes, previousIndexes, maskSecrets) {
|
|
20372
|
+
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
20373
|
+
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.`));
|
|
20374
|
+
if (ops.length === 0) return;
|
|
20375
|
+
await this.waitForTableAndIndexesActive(tableName);
|
|
20376
|
+
for (const op of ops) await this.sendContributorInsightsOp(tableName, op, maskSecrets);
|
|
20377
|
+
}
|
|
20378
|
+
/** Issue ONE `UpdateContributorInsights`, against the table or one index. */
|
|
20379
|
+
async sendContributorInsightsOp(tableName, op, maskSecrets) {
|
|
20380
|
+
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
20381
|
+
const target = op.indexName === void 0 ? `DynamoDB table ${tableName}` : this.indexScopeAt(op.indexName, tableName, maskSecrets);
|
|
19749
20382
|
await this.retryOnTransientControlPlane(() => this.dynamoDBClient.send(new UpdateContributorInsightsCommand({
|
|
19750
20383
|
TableName: tableName,
|
|
19751
|
-
|
|
19752
|
-
|
|
19753
|
-
|
|
19754
|
-
|
|
20384
|
+
...op.indexName !== void 0 ? { IndexName: op.indexName } : {},
|
|
20385
|
+
ContributorInsightsAction: op.action,
|
|
20386
|
+
...op.mode !== void 0 ? { ContributorInsightsMode: op.mode } : {}
|
|
20387
|
+
})), `set ContributorInsights on ${target}`);
|
|
20388
|
+
debug(`Set ContributorInsightsAction=${op.action}${op.mode !== void 0 ? ` Mode=${op.mode}` : ""} on ${target}`);
|
|
19755
20389
|
}
|
|
19756
20390
|
/**
|
|
19757
20391
|
* Poll DescribeTable until the table reaches ACTIVE status
|
|
@@ -20732,32 +21366,35 @@ var DynamoDBTableProvider = class {
|
|
|
20732
21366
|
* level: it is order-SIGNIFICANT (HASH before RANGE), so sorting it would
|
|
20733
21367
|
* HIDE a real key change rather than remove a phantom one.
|
|
20734
21368
|
*
|
|
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.
|
|
21369
|
+
* `GlobalSecondaryIndexes` / `LocalSecondaryIndexes` ARE declared, in the
|
|
21370
|
+
* LEAF-ONLY form (`LEAF_ONLY_PATH_SUFFIX`, `[]`, from
|
|
21371
|
+
* [#1799](https://github.com/go-to-k/cdkd/pull/1799)): both are sets keyed
|
|
21372
|
+
* by `IndexName`, and `DescribeTable` does not return them in the template's
|
|
21373
|
+
* order. A plain SUBTREE entry would be wrong — the unordered walk descends
|
|
21374
|
+
* into array elements giving each the parent's path (`drift-normalize.ts`),
|
|
21375
|
+
* so `'GlobalSecondaryIndexes'` would also sort each index's `KeySchema` and
|
|
21376
|
+
* reverse the sentence above at the index level. The leaf-only form sorts
|
|
21377
|
+
* the list and stops there.
|
|
21378
|
+
*
|
|
21379
|
+
* Measured on real AWS by issue
|
|
21380
|
+
* [#1782](https://github.com/go-to-k/cdkd/issues/1782)'s two-index fixture:
|
|
21381
|
+
* a table created with `[giA, giB]` read back as `[giB, giA]`, so a
|
|
21382
|
+
* `properties` baseline reported whole-list drift although every member
|
|
21383
|
+
* round-tripped. The phantom pre-dates that issue — the #1767 arm only ever
|
|
21384
|
+
* had ONE index — and was not reachable against an `observedProperties`
|
|
21385
|
+
* baseline, where both sides come from the same readback. Both comparison
|
|
21386
|
+
* sides are sorted, by a key-order-independent serialization of the whole
|
|
21387
|
+
* entry, so a baseline captured in AWS's order by an earlier binary keeps
|
|
21388
|
+
* converging; a REAL difference still reports the list, since entries that
|
|
21389
|
+
* differ cannot be made equal by reordering them.
|
|
20757
21390
|
*/
|
|
20758
21391
|
getDriftUnorderedPaths(resourceType) {
|
|
20759
21392
|
if (resourceType !== "AWS::DynamoDB::Table") return [];
|
|
20760
|
-
return [
|
|
21393
|
+
return [
|
|
21394
|
+
"AttributeDefinitions",
|
|
21395
|
+
"GlobalSecondaryIndexes[]",
|
|
21396
|
+
"LocalSecondaryIndexes[]"
|
|
21397
|
+
];
|
|
20761
21398
|
}
|
|
20762
21399
|
/**
|
|
20763
21400
|
* `WarmThroughput` is reported by AWS for EVERY table, whether or not the
|
|
@@ -20873,9 +21510,12 @@ var DynamoDBTableProvider = class {
|
|
|
20873
21510
|
* shape differences are wrapping:
|
|
20874
21511
|
* - BillingMode lives under `BillingModeSummary.BillingMode` in the API
|
|
20875
21512
|
* response, but the CFn property is a flat `BillingMode` string.
|
|
20876
|
-
* - StreamSpecification's CFn shape
|
|
20877
|
-
* API response carries `StreamEnabled`
|
|
20878
|
-
*
|
|
21513
|
+
* - StreamSpecification's CFn shape is `StreamViewType`, `ResourcePolicy`
|
|
21514
|
+
* and `Tags`; the API response carries `StreamEnabled` and
|
|
21515
|
+
* `StreamViewType` only. `StreamEnabled` is surfaced too, since the
|
|
21516
|
+
* comparator descends a `properties` baseline by the keys state holds.
|
|
21517
|
+
* `ResourcePolicy` / `Tags` (issue #3458) live on the STREAM arn and are
|
|
21518
|
+
* read from it, each only when the desired block declares that member.
|
|
20879
21519
|
* - GSI / LSI in the API response include `IndexStatus`, `Backfilling`,
|
|
20880
21520
|
* `ItemCount`, `IndexArn` and sizing fields cdkd never sets. They used to
|
|
20881
21521
|
* be forwarded VERBATIM under a comment claiming "the comparator filters
|
|
@@ -20902,7 +21542,9 @@ var DynamoDBTableProvider = class {
|
|
|
20902
21542
|
* caller passes). It is consulted for the table-level `WarmThroughput` — see
|
|
20903
21543
|
* {@link getDriftUnknownPaths} for the measurement and the reasoning — and,
|
|
20904
21544
|
* since issue #1767, for the per-index throughput blocks of each secondary
|
|
20905
|
-
* index, matched by `IndexName`.
|
|
21545
|
+
* index, matched by `IndexName`. Since issue #1782 it also gates the per-index
|
|
21546
|
+
* `ContributorInsightsSpecification` read, which costs one
|
|
21547
|
+
* `DescribeContributorInsights` per declaring index.
|
|
20906
21548
|
*/
|
|
20907
21549
|
async readCurrentState(physicalId, _logicalId, _resourceType, properties) {
|
|
20908
21550
|
try {
|
|
@@ -20929,10 +21571,14 @@ var DynamoDBTableProvider = class {
|
|
|
20929
21571
|
if (table.WarmThroughput.WriteUnitsPerSecond !== void 0) wt["WriteUnitsPerSecond"] = table.WarmThroughput.WriteUnitsPerSecond;
|
|
20930
21572
|
if (Object.keys(wt).length > 0) result["WarmThroughput"] = wt;
|
|
20931
21573
|
}
|
|
20932
|
-
if (table.StreamSpecification?.StreamEnabled && table.StreamSpecification.StreamViewType)
|
|
20933
|
-
|
|
20934
|
-
|
|
20935
|
-
|
|
21574
|
+
if (table.StreamSpecification?.StreamEnabled && table.StreamSpecification.StreamViewType) {
|
|
21575
|
+
const streamBlock = {
|
|
21576
|
+
StreamEnabled: true,
|
|
21577
|
+
StreamViewType: table.StreamSpecification.StreamViewType
|
|
21578
|
+
};
|
|
21579
|
+
result["StreamSpecification"] = streamBlock;
|
|
21580
|
+
await this.readStreamMembers(physicalId, table.LatestStreamArn, properties?.["StreamSpecification"], streamBlock);
|
|
21581
|
+
}
|
|
20936
21582
|
const bagInformative = desiredBagIsInformative(properties);
|
|
20937
21583
|
if (table.GlobalSecondaryIndexes && table.GlobalSecondaryIndexes.length > 0) {
|
|
20938
21584
|
const desiredByName = desiredIndexEntriesByName(properties?.["GlobalSecondaryIndexes"]);
|
|
@@ -21001,15 +21647,31 @@ var DynamoDBTableProvider = class {
|
|
|
21001
21647
|
}
|
|
21002
21648
|
try {
|
|
21003
21649
|
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
|
-
}
|
|
21650
|
+
const cspec = reverseMapTableContributorInsights(ciResp.ContributorInsightsStatus, ciResp.ContributorInsightsMode);
|
|
21651
|
+
if (cspec !== void 0) result["ContributorInsightsSpecification"] = cspec;
|
|
21010
21652
|
} catch (err) {
|
|
21011
21653
|
this.logger.debug(`Could not read ContributorInsights for ${physicalId}: ${describeAwsFailure(err).detail}`);
|
|
21012
21654
|
}
|
|
21655
|
+
const emittedGsis = result["GlobalSecondaryIndexes"];
|
|
21656
|
+
if (bagInformative && Array.isArray(emittedGsis)) {
|
|
21657
|
+
const desiredByName = desiredIndexEntriesByName(properties?.["GlobalSecondaryIndexes"]);
|
|
21658
|
+
for (const emitted of emittedGsis) {
|
|
21659
|
+
const indexName = emitted["IndexName"];
|
|
21660
|
+
if (typeof indexName !== "string") continue;
|
|
21661
|
+
const desiredEntry = desiredByName.get(indexName);
|
|
21662
|
+
if (!indexDeclaresContributorInsights(desiredEntry)) continue;
|
|
21663
|
+
try {
|
|
21664
|
+
const indexResp = await this.dynamoDBClient.send(new DescribeContributorInsightsCommand({
|
|
21665
|
+
TableName: physicalId,
|
|
21666
|
+
IndexName: indexName
|
|
21667
|
+
}));
|
|
21668
|
+
const block = reverseMapIndexContributorInsights(indexResp.ContributorInsightsStatus, indexResp.ContributorInsightsMode, desiredEntry?.[CONTRIBUTOR_INSIGHTS_KEY]);
|
|
21669
|
+
if (block !== void 0) emitted[CONTRIBUTOR_INSIGHTS_KEY] = block;
|
|
21670
|
+
} catch (err) {
|
|
21671
|
+
this.logger.debug(`Could not read ContributorInsights for index ${indexName} of ${physicalId}: ${describeAwsFailure(err).detail}`);
|
|
21672
|
+
}
|
|
21673
|
+
}
|
|
21674
|
+
}
|
|
21013
21675
|
return result;
|
|
21014
21676
|
} catch (err) {
|
|
21015
21677
|
if (err instanceof ResourceNotFoundException$1) return void 0;
|
|
@@ -21017,6 +21679,92 @@ var DynamoDBTableProvider = class {
|
|
|
21017
21679
|
}
|
|
21018
21680
|
}
|
|
21019
21681
|
/**
|
|
21682
|
+
* How long an eventually-consistent stream-member read that does NOT match
|
|
21683
|
+
* the declaration is re-asked before it is believed: one entry per extra
|
|
21684
|
+
* attempt.
|
|
21685
|
+
*/
|
|
21686
|
+
streamMemberRereadDelaysMs = [
|
|
21687
|
+
500,
|
|
21688
|
+
1e3,
|
|
21689
|
+
2e3
|
|
21690
|
+
];
|
|
21691
|
+
/** Run `read`, re-asking after each configured delay until `settled` accepts the answer. */
|
|
21692
|
+
async reAskUntilSettled(read, settled) {
|
|
21693
|
+
let answer = await read();
|
|
21694
|
+
for (const delayMs of this.streamMemberRereadDelaysMs) {
|
|
21695
|
+
if (settled(answer)) break;
|
|
21696
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
21697
|
+
answer = await read();
|
|
21698
|
+
}
|
|
21699
|
+
return answer;
|
|
21700
|
+
}
|
|
21701
|
+
/**
|
|
21702
|
+
* Read `StreamSpecification.ResourcePolicy` / `.Tags` (issue #3458) off the
|
|
21703
|
+
* table's CURRENT stream arn into `streamBlock`.
|
|
21704
|
+
*
|
|
21705
|
+
* One API call per member, so each is GATED on the desired block declaring a
|
|
21706
|
+
* member cdkd would send — the same reader the write plan runs. The gate is
|
|
21707
|
+
* also what keeps an undeclared member out of an `observedProperties`
|
|
21708
|
+
* baseline, which is compared by the UNION of both sides' keys.
|
|
21709
|
+
*
|
|
21710
|
+
* No policy (`PolicyNotFoundException`) and an empty user tag set emit
|
|
21711
|
+
* NOTHING: that is "absent" in the template's own spelling, which a
|
|
21712
|
+
* declared member then reports as drift (a declared EMPTY list is the one
|
|
21713
|
+
* exception, and reads back as the empty list it is). `aws:`-prefixed system tags are
|
|
21714
|
+
* dropped like the table-level ones, and the list needs no unordered path —
|
|
21715
|
+
* the comparator sorts every `{Key, ...}` list on both sides. Any other
|
|
21716
|
+
* failed read omits the member and says why at debug level, like the sibling
|
|
21717
|
+
* reads of this method.
|
|
21718
|
+
*
|
|
21719
|
+
* Both reads are EVENTUALLY consistent — right after a write they can answer
|
|
21720
|
+
* "absent" or with the PREVIOUS value (their command docs say so) — and the
|
|
21721
|
+
* deploy captures `observedProperties` right after the writes. So an answer
|
|
21722
|
+
* that does not match the DECLARED member is re-asked a few times before it
|
|
21723
|
+
* is believed, or a lagging read would freeze a baseline the settled stream
|
|
21724
|
+
* can never equal. A member that really drifted costs those few seconds per
|
|
21725
|
+
* drift run.
|
|
21726
|
+
*
|
|
21727
|
+
* `cdkd drift --revert` replays through `update()`, where the read-back is
|
|
21728
|
+
* the PREVIOUS side and {@link planStreamMemberOps} derives the restore.
|
|
21729
|
+
*/
|
|
21730
|
+
async readStreamMembers(tableName, streamArn, desiredBlock, streamBlock) {
|
|
21731
|
+
if (streamArn === void 0 || streamArn === "") return;
|
|
21732
|
+
if (streamDeclaresPolicy(desiredBlock)) try {
|
|
21733
|
+
const livePolicy = await this.reAskUntilSettled(async () => {
|
|
21734
|
+
try {
|
|
21735
|
+
return (await this.dynamoDBClient.send(new GetResourcePolicyCommand({ ResourceArn: streamArn }))).Policy;
|
|
21736
|
+
} catch (err) {
|
|
21737
|
+
if (err instanceof PolicyNotFoundException) return void 0;
|
|
21738
|
+
throw err;
|
|
21739
|
+
}
|
|
21740
|
+
}, (answer) => streamPolicyMatchesDeclared(answer, desiredBlock));
|
|
21741
|
+
const member = reverseMapStreamPolicy(livePolicy, desiredBlock);
|
|
21742
|
+
if (member !== void 0) streamBlock[STREAM_POLICY_KEY] = member;
|
|
21743
|
+
} catch (err) {
|
|
21744
|
+
this.logger.debug(`Could not read the stream ResourcePolicy for ${tableName}: ${describeAwsFailure(err).detail}`);
|
|
21745
|
+
}
|
|
21746
|
+
if (streamDeclaresTags(desiredBlock)) try {
|
|
21747
|
+
const listOnce = async () => {
|
|
21748
|
+
const liveTags = [];
|
|
21749
|
+
let nextToken;
|
|
21750
|
+
do {
|
|
21751
|
+
const response = await this.dynamoDBClient.send(new ListTagsOfResourceCommand({
|
|
21752
|
+
ResourceArn: streamArn,
|
|
21753
|
+
NextToken: nextToken
|
|
21754
|
+
}));
|
|
21755
|
+
liveTags.push(...response.Tags ?? []);
|
|
21756
|
+
nextToken = response.NextToken;
|
|
21757
|
+
} while (nextToken !== void 0 && nextToken !== "");
|
|
21758
|
+
return normalizeAwsTagsToCfn(liveTags);
|
|
21759
|
+
};
|
|
21760
|
+
const tags = await this.reAskUntilSettled(listOnce, (answer) => streamTagsMatchDeclared(answer, desiredBlock));
|
|
21761
|
+
const declared = desiredBlock[STREAM_TAGS_KEY];
|
|
21762
|
+
if (tags.length > 0 || Array.isArray(declared) && declared.length === 0) streamBlock[STREAM_TAGS_KEY] = tags;
|
|
21763
|
+
} catch (err) {
|
|
21764
|
+
this.logger.debug(`Could not read the stream Tags for ${tableName}: ${describeAwsFailure(err).detail}`);
|
|
21765
|
+
}
|
|
21766
|
+
}
|
|
21767
|
+
/**
|
|
21020
21768
|
* Adopt an existing DynamoDB table into cdkd state.
|
|
21021
21769
|
*
|
|
21022
21770
|
* Lookup order:
|
|
@@ -84981,8 +85729,394 @@ function createForceUnlockCommand() {
|
|
|
84981
85729
|
return cmd;
|
|
84982
85730
|
}
|
|
84983
85731
|
|
|
85732
|
+
//#endregion
|
|
85733
|
+
//#region src/cli/commands/import-deployed-parameters.ts
|
|
85734
|
+
/**
|
|
85735
|
+
* UNVERIFIABLE INPUTS for `cdkd import`'s observed-baseline refusal (issue
|
|
85736
|
+
* [#2854](https://github.com/go-to-k/cdkd/issues/2854)).
|
|
85737
|
+
*
|
|
85738
|
+
* `cdkd import` accepts no parameter values, so every template parameter is
|
|
85739
|
+
* bound to its `Default`. When the resources were deployed by a CloudFormation
|
|
85740
|
+
* stack, the value CloudFormation actually used can differ — and the ordinary
|
|
85741
|
+
* secret-carrying shape is a placeholder `Default` (`CHANGEME`) over a DEPLOYED
|
|
85742
|
+
* value of `{{resolve:secretsmanager:...}}`. The persisted `properties` then
|
|
85743
|
+
* spell no reference anywhere, the position-based redaction of the observed
|
|
85744
|
+
* capture has no evidence, and the decrypted readback would be persisted.
|
|
85745
|
+
*
|
|
85746
|
+
* The remedy here is deliberately PREMISE-INDEPENDENT. It never looks for a
|
|
85747
|
+
* `{{resolve:` opener in what `DescribeStacks` returns (AWS documents
|
|
85748
|
+
* `ParameterValue` as "the input value", which says the reference comes back
|
|
85749
|
+
* literally, but nothing in this module relies on it). It asks one question —
|
|
85750
|
+
* "is the deployed value PROVABLY the value this import bound?" — and treats
|
|
85751
|
+
* every other answer as divergent: a literal reference, the `****` a `NoEcho`
|
|
85752
|
+
* parameter is masked to, a value AWS resolved, a parameter missing from the
|
|
85753
|
+
* response, a `DescribeStacks` that failed. All of those differ from the bound
|
|
85754
|
+
* placeholder, so all of them refuse.
|
|
85755
|
+
*
|
|
85756
|
+
* THE DEPLOYED VALUES ARE COMPARISON-ONLY. They are never bound into the
|
|
85757
|
+
* resolve, never logged, never put in an error message, and never persisted:
|
|
85758
|
+
* binding them is the one design that would leak if AWS ever returned a
|
|
85759
|
+
* resolved value. {@link DeployedParameters} holds them in a `#private` field so
|
|
85760
|
+
* that a stray `JSON.stringify` / `util.inspect` / template literal of the
|
|
85761
|
+
* object renders nothing, and an SDK failure is reported by its error NAME
|
|
85762
|
+
* alone, because an SDK message can echo request input.
|
|
85763
|
+
*
|
|
85764
|
+
* TAINT THROUGH ANOTHER RESOURCE'S ATTRIBUTES IS FOLLOWED within one stack: the
|
|
85765
|
+
* walk records the target of every `Fn::GetAtt` / `${A.Attr}`, and the caller
|
|
85766
|
+
* closes the refused set over those targets, so a resource reading an attribute
|
|
85767
|
+
* of a refused resource (`{Fn::GetAtt: [SsmParam, Value]}` into a Lambda
|
|
85768
|
+
* environment — what CDK's `param.stringValue` emits) is refused with it. A
|
|
85769
|
+
* plain `Ref` of a resource is NOT followed: it yields the physical id, and a
|
|
85770
|
+
* secret used as a resource NAME is already disclosed by the identifier itself.
|
|
85771
|
+
*
|
|
85772
|
+
* KNOWN RESIDUAL, not followed: taint ACROSS the nested-stack boundary. A
|
|
85773
|
+
* parent property reading `Fn::GetAtt: [Child, Outputs.X]`, where the child's
|
|
85774
|
+
* output derives from a divergent CHILD parameter, is judged on the parent's
|
|
85775
|
+
* own parameters only — the root is resolved before any child is read.
|
|
85776
|
+
*/
|
|
85777
|
+
/**
|
|
85778
|
+
* What CloudFormation reports a stack was deployed with. Opaque on purpose:
|
|
85779
|
+
* the only question it answers is {@link DeployedParameters.provablyEquals}.
|
|
85780
|
+
*/
|
|
85781
|
+
var DeployedParameters = class DeployedParameters {
|
|
85782
|
+
#entries;
|
|
85783
|
+
constructor(entries) {
|
|
85784
|
+
this.#entries = entries;
|
|
85785
|
+
}
|
|
85786
|
+
/** `DescribeStacks` answered; `entries` is its `Parameters` list. */
|
|
85787
|
+
static fromDescribeStacks(parameters) {
|
|
85788
|
+
const entries = /* @__PURE__ */ new Map();
|
|
85789
|
+
for (const parameter of parameters) {
|
|
85790
|
+
if (typeof parameter.ParameterKey !== "string") continue;
|
|
85791
|
+
entries.set(parameter.ParameterKey, {
|
|
85792
|
+
value: parameter.ParameterValue,
|
|
85793
|
+
resolvedValue: parameter.ResolvedValue
|
|
85794
|
+
});
|
|
85795
|
+
}
|
|
85796
|
+
return new DeployedParameters(entries);
|
|
85797
|
+
}
|
|
85798
|
+
/** The deployed values could not be read: NOTHING is provable. */
|
|
85799
|
+
static unavailable() {
|
|
85800
|
+
return new DeployedParameters(void 0);
|
|
85801
|
+
}
|
|
85802
|
+
/**
|
|
85803
|
+
* Whether the deployed value of `name` is provably the value the import
|
|
85804
|
+
* BOUND. The deployed string goes through the SAME coercion the resolver
|
|
85805
|
+
* applied to the bound one (`Number`, `CommaDelimitedList`, `List<...>`), so
|
|
85806
|
+
* `'1'` equals a bound `1` and `'a, b'` equals a bound `['a', 'b']`. For an
|
|
85807
|
+
* SSM-typed parameter the resolver binds the RESOLVED value, so that is the
|
|
85808
|
+
* side compared — `ParameterValue` is only the key there.
|
|
85809
|
+
*/
|
|
85810
|
+
provablyEquals(name, type, bound) {
|
|
85811
|
+
const entry = this.#entries?.get(name);
|
|
85812
|
+
if (entry === void 0 || typeof type !== "string") return false;
|
|
85813
|
+
const ssmType = type.startsWith("AWS::SSM::Parameter::Value");
|
|
85814
|
+
const deployed = ssmType ? entry.resolvedValue : entry.value;
|
|
85815
|
+
if (typeof deployed !== "string") return false;
|
|
85816
|
+
const comparedType = ssmType ? ssmResolvedValueType(type) : type;
|
|
85817
|
+
return stableEquals(comparedType === void 0 ? deployed : coerceParameterTypedValue(deployed, comparedType), bound);
|
|
85818
|
+
}
|
|
85819
|
+
};
|
|
85820
|
+
/** Structural equality for the scalar / scalar-array values a parameter binds to. */
|
|
85821
|
+
function stableEquals(a, b) {
|
|
85822
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
85823
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
85824
|
+
return a.every((element, index) => stableEquals(element, b[index]));
|
|
85825
|
+
}
|
|
85826
|
+
if (typeof a === "string" && typeof b === "number") return Number.isFinite(b) && a === String(b);
|
|
85827
|
+
if (typeof a === "number" || typeof b === "number") return typeof a === "number" && typeof b === "number" && Number.isFinite(a) && a === b;
|
|
85828
|
+
if (typeof a === "string" && typeof b === "boolean") return a === String(b);
|
|
85829
|
+
return typeof a === "string" && typeof b === "string" && a === b;
|
|
85830
|
+
}
|
|
85831
|
+
async function readDeployedParameters(cfnStackName, cfnClient, logger, displayName, options) {
|
|
85832
|
+
try {
|
|
85833
|
+
const stack = (await cfnClient.send(new DescribeStacksCommand({ StackName: cfnStackName }))).Stacks?.[0];
|
|
85834
|
+
if (!stack) {
|
|
85835
|
+
logger.warn(unavailableWarning(displayName, "the stack was not returned"));
|
|
85836
|
+
return DeployedParameters.unavailable();
|
|
85837
|
+
}
|
|
85838
|
+
return DeployedParameters.fromDescribeStacks(stack.Parameters ?? []);
|
|
85839
|
+
} catch (err) {
|
|
85840
|
+
if (options?.absentStackIsNoSource === true && err instanceof Error && err.name === "ValidationError" && /does not exist/i.test(err.message)) return;
|
|
85841
|
+
const name = err instanceof Error && /^[A-Za-z0-9_.]{1,80}$/.test(err.name) ? err.name : "error";
|
|
85842
|
+
logger.warn(unavailableWarning(displayName, name));
|
|
85843
|
+
return DeployedParameters.unavailable();
|
|
85844
|
+
}
|
|
85845
|
+
}
|
|
85846
|
+
function unavailableWarning(displayName, reason) {
|
|
85847
|
+
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'.`;
|
|
85848
|
+
}
|
|
85849
|
+
/**
|
|
85850
|
+
* The declared parameters whose deployed value is NOT provably the bound one.
|
|
85851
|
+
* A parameter the import could not bind at all (no `Default`, an unreferenced
|
|
85852
|
+
* or failed SSM default, the empty fallback bag) is divergent by definition.
|
|
85853
|
+
*/
|
|
85854
|
+
function divergentParameterNames(template, bound, deployed) {
|
|
85855
|
+
const divergent = /* @__PURE__ */ new Set();
|
|
85856
|
+
const declared = template.Parameters;
|
|
85857
|
+
if (declared === void 0 || declared === null || typeof declared !== "object") return divergent;
|
|
85858
|
+
for (const [name, definition] of Object.entries(declared)) {
|
|
85859
|
+
const record = definition !== null && typeof definition === "object" ? definition : void 0;
|
|
85860
|
+
const type = record?.["Type"];
|
|
85861
|
+
const noEcho = record?.["NoEcho"];
|
|
85862
|
+
if (noEcho === true || typeof noEcho === "string" && noEcho.toLowerCase() === "true") {
|
|
85863
|
+
divergent.add(name);
|
|
85864
|
+
continue;
|
|
85865
|
+
}
|
|
85866
|
+
if (!Object.hasOwn(bound, name) || !deployed.provablyEquals(name, type, bound[name])) divergent.add(name);
|
|
85867
|
+
}
|
|
85868
|
+
return divergent;
|
|
85869
|
+
}
|
|
85870
|
+
/**
|
|
85871
|
+
* Every intrinsic the dependence walk knows how to read. An `Fn::`-prefixed
|
|
85872
|
+
* key OUTSIDE this list is unclassifiable: it may name a parameter in a way
|
|
85873
|
+
* the generic recursion cannot see, so the walk fails closed on it.
|
|
85874
|
+
*/
|
|
85875
|
+
const KNOWN_INTRINSICS = /* @__PURE__ */ new Set([
|
|
85876
|
+
"Fn::GetAtt",
|
|
85877
|
+
"Fn::Join",
|
|
85878
|
+
"Fn::Sub",
|
|
85879
|
+
"Fn::Select",
|
|
85880
|
+
"Fn::Split",
|
|
85881
|
+
"Fn::If",
|
|
85882
|
+
"Fn::Equals",
|
|
85883
|
+
"Fn::And",
|
|
85884
|
+
"Fn::Or",
|
|
85885
|
+
"Fn::Not",
|
|
85886
|
+
"Fn::ImportValue",
|
|
85887
|
+
"Fn::GetStackOutput",
|
|
85888
|
+
"Fn::FindInMap",
|
|
85889
|
+
"Fn::Base64",
|
|
85890
|
+
"Fn::GetAZs",
|
|
85891
|
+
"Fn::Cidr"
|
|
85892
|
+
]);
|
|
85893
|
+
/** `${Name}` / `${Name.Attr}` in an `Fn::Sub` string; `${!Literal}` is an escape. */
|
|
85894
|
+
const SUB_VARIABLE = /\$\{([^!}][^}]*)\}/g;
|
|
85895
|
+
/**
|
|
85896
|
+
* Which names (parameters, but also resources and pseudo parameters — the
|
|
85897
|
+
* caller intersects with the divergent set) the RAW `Properties` of a resource
|
|
85898
|
+
* depend on, plus its resource-level `Condition`.
|
|
85899
|
+
*
|
|
85900
|
+
* Generic by design: instead of modelling each intrinsic's argument grammar it
|
|
85901
|
+
* recurses into EVERY value, so a `Ref` nested in an `Fn::Join` / `Fn::Select`
|
|
85902
|
+
* / `Fn::Split` operand, an `Fn::FindInMap` key, or an `Fn::Sub` variable-map
|
|
85903
|
+
* value is found without being named. Only the three forms that name something
|
|
85904
|
+
* by BARE STRING need their own arm: `Ref`, `Fn::Sub`'s template text, and a
|
|
85905
|
+
* condition name (`Fn::If`'s first argument, and `Condition` inside the
|
|
85906
|
+
* `Conditions` section), which is followed transitively through
|
|
85907
|
+
* `template.Conditions`.
|
|
85908
|
+
*
|
|
85909
|
+
* May throw `RangeError` on a bag deeper than the call stack; the caller wraps
|
|
85910
|
+
* it fail-closed, like the discard walk beside it.
|
|
85911
|
+
*/
|
|
85912
|
+
function collectParameterDependencies(properties, resourceCondition, template) {
|
|
85913
|
+
const names = /* @__PURE__ */ new Set();
|
|
85914
|
+
const attributeTargets = /* @__PURE__ */ new Set();
|
|
85915
|
+
const visitedConditions = /* @__PURE__ */ new Set();
|
|
85916
|
+
let unclassifiable = false;
|
|
85917
|
+
const conditions = template.Conditions !== null && typeof template.Conditions === "object" ? template.Conditions : {};
|
|
85918
|
+
const followCondition = (name) => {
|
|
85919
|
+
if (typeof name !== "string") {
|
|
85920
|
+
unclassifiable = true;
|
|
85921
|
+
return;
|
|
85922
|
+
}
|
|
85923
|
+
if (visitedConditions.has(name)) return;
|
|
85924
|
+
visitedConditions.add(name);
|
|
85925
|
+
if (!Object.hasOwn(conditions, name)) {
|
|
85926
|
+
unclassifiable = true;
|
|
85927
|
+
return;
|
|
85928
|
+
}
|
|
85929
|
+
walk(conditions[name], true);
|
|
85930
|
+
};
|
|
85931
|
+
const walk = (node, inConditions) => {
|
|
85932
|
+
if (node === null || typeof node !== "object") return;
|
|
85933
|
+
if (Array.isArray(node)) {
|
|
85934
|
+
for (const element of node) walk(element, inConditions);
|
|
85935
|
+
return;
|
|
85936
|
+
}
|
|
85937
|
+
for (const [key, value] of Object.entries(node)) {
|
|
85938
|
+
if (key === "Ref") {
|
|
85939
|
+
if (typeof value === "string") names.add(value);
|
|
85940
|
+
else unclassifiable = true;
|
|
85941
|
+
continue;
|
|
85942
|
+
}
|
|
85943
|
+
if (key === "Fn::Sub") {
|
|
85944
|
+
const text = Array.isArray(value) ? value[0] : value;
|
|
85945
|
+
if (typeof text !== "string" || Array.isArray(value) && value.length !== 2) unclassifiable = true;
|
|
85946
|
+
else for (const match of text.matchAll(SUB_VARIABLE)) {
|
|
85947
|
+
const variable = (match[1] ?? "").trim();
|
|
85948
|
+
names.add(variable);
|
|
85949
|
+
const dot = variable.indexOf(".");
|
|
85950
|
+
if (dot > 0) {
|
|
85951
|
+
names.add(variable.slice(0, dot));
|
|
85952
|
+
attributeTargets.add(variable.slice(0, dot));
|
|
85953
|
+
}
|
|
85954
|
+
}
|
|
85955
|
+
if (Array.isArray(value)) walk(value[1], inConditions);
|
|
85956
|
+
continue;
|
|
85957
|
+
}
|
|
85958
|
+
if (key === "Fn::GetAtt") {
|
|
85959
|
+
const target = Array.isArray(value) ? value[0] : value;
|
|
85960
|
+
if (typeof target === "string") {
|
|
85961
|
+
const dot = target.indexOf(".");
|
|
85962
|
+
const logicalId = Array.isArray(value) || dot <= 0 ? target : target.slice(0, dot);
|
|
85963
|
+
names.add(logicalId);
|
|
85964
|
+
attributeTargets.add(logicalId);
|
|
85965
|
+
} else unclassifiable = true;
|
|
85966
|
+
walk(value, inConditions);
|
|
85967
|
+
continue;
|
|
85968
|
+
}
|
|
85969
|
+
if (key === "Fn::If") {
|
|
85970
|
+
if (Array.isArray(value) && value.length === 3) {
|
|
85971
|
+
followCondition(value[0]);
|
|
85972
|
+
walk(value[1], inConditions);
|
|
85973
|
+
walk(value[2], inConditions);
|
|
85974
|
+
} else unclassifiable = true;
|
|
85975
|
+
continue;
|
|
85976
|
+
}
|
|
85977
|
+
if (key === "Condition" && inConditions) {
|
|
85978
|
+
followCondition(value);
|
|
85979
|
+
continue;
|
|
85980
|
+
}
|
|
85981
|
+
if (key.startsWith("Fn::") && !KNOWN_INTRINSICS.has(key)) unclassifiable = true;
|
|
85982
|
+
walk(value, inConditions);
|
|
85983
|
+
}
|
|
85984
|
+
};
|
|
85985
|
+
walk(properties, false);
|
|
85986
|
+
if (resourceCondition !== void 0) followCondition(resourceCondition);
|
|
85987
|
+
return {
|
|
85988
|
+
names,
|
|
85989
|
+
attributeTargets,
|
|
85990
|
+
unclassifiable
|
|
85991
|
+
};
|
|
85992
|
+
}
|
|
85993
|
+
/**
|
|
85994
|
+
* The divergent parameters that ANYTHING in the template names. A declared
|
|
85995
|
+
* parameter nothing names cannot have shaped a resource — and CDK's
|
|
85996
|
+
* `BootstrapVersion` is exactly that on every default-synthesized stack: SSM
|
|
85997
|
+
* typed, never bound by the resolver because nothing references it, hence
|
|
85998
|
+
* always "divergent". Left in, it would arm the unclassifiable-shape refusal on
|
|
85999
|
+
* stacks whose every real parameter is proven.
|
|
86000
|
+
*
|
|
86001
|
+
* "Names" is the UNION of this module's walk and `TemplateParser`'s reference
|
|
86002
|
+
* scan — the same scan the resolver uses to decide an SSM-typed parameter is
|
|
86003
|
+
* unreferenced — so the two cannot disagree about `BootstrapVersion`. An
|
|
86004
|
+
* unclassifiable shape does NOT keep the whole set here: CloudFormation names a
|
|
86005
|
+
* parameter only through `Ref`, `Fn::Sub` text or a condition, and the generic
|
|
86006
|
+
* recursion reads those inside any intrinsic, known or not. A walk that THROWS
|
|
86007
|
+
* keeps the whole set (fail closed).
|
|
86008
|
+
*/
|
|
86009
|
+
function reachableDivergentParameters(template, divergent, rawBags) {
|
|
86010
|
+
if (divergent.size === 0) return /* @__PURE__ */ new Set();
|
|
86011
|
+
try {
|
|
86012
|
+
const roots = [
|
|
86013
|
+
rawBags,
|
|
86014
|
+
template.Resources,
|
|
86015
|
+
template.Outputs,
|
|
86016
|
+
template.Conditions
|
|
86017
|
+
];
|
|
86018
|
+
const named = new Set(collectParameterDependencies(roots, void 0, template).names);
|
|
86019
|
+
const parser = new TemplateParser();
|
|
86020
|
+
for (const root of roots) {
|
|
86021
|
+
if (root === null || typeof root !== "object") continue;
|
|
86022
|
+
for (const name of parser.extractReferences(root)) named.add(name);
|
|
86023
|
+
}
|
|
86024
|
+
return new Set([...divergent].filter((name) => named.has(name)));
|
|
86025
|
+
} catch {
|
|
86026
|
+
return new Set(divergent);
|
|
86027
|
+
}
|
|
86028
|
+
}
|
|
86029
|
+
/** Whether any DECLARED parameter is named by anything in the template. */
|
|
86030
|
+
function namesAnyDeclaredParameter(template) {
|
|
86031
|
+
const declared = template.Parameters;
|
|
86032
|
+
if (declared === null || typeof declared !== "object") return false;
|
|
86033
|
+
const names = new Set(Object.keys(declared));
|
|
86034
|
+
return names.size > 0 && reachableDivergentParameters(template, names, []).size > 0;
|
|
86035
|
+
}
|
|
86036
|
+
/**
|
|
86037
|
+
* The per-stack ARM 4 verdict. Each resource is judged on its TEMPLATE
|
|
86038
|
+
* definition (`Properties` + `Condition`) AND on the raw bag the state record
|
|
86039
|
+
* carries, unioned: a record preserved by a selective merge holds a previous
|
|
86040
|
+
* run's RESOLVED literal (`Value: 'CHANGEME'`), which names no parameter, so
|
|
86041
|
+
* judging it on the state bag alone would leave nothing to carry to the
|
|
86042
|
+
* freshly imported resource that reads its attribute. Template resources that
|
|
86043
|
+
* are not in state at all are judged too, for the same reason.
|
|
86044
|
+
*
|
|
86045
|
+
* Seeds are direct dependents and unclassifiable bags (a bag that cannot be
|
|
86046
|
+
* read cannot be vouched for, and neither can what reads its attributes); the
|
|
86047
|
+
* refused set is then closed over ATTRIBUTE reads, to a fixpoint.
|
|
86048
|
+
*/
|
|
86049
|
+
function computeParameterTaint(template, stateBags, divergent) {
|
|
86050
|
+
const refused = /* @__PURE__ */ new Set();
|
|
86051
|
+
const unclassifiable = /* @__PURE__ */ new Set();
|
|
86052
|
+
const parametersHit = /* @__PURE__ */ new Set();
|
|
86053
|
+
if (divergent.size === 0) return {
|
|
86054
|
+
refused,
|
|
86055
|
+
unclassifiable,
|
|
86056
|
+
parametersHit
|
|
86057
|
+
};
|
|
86058
|
+
const templateResources = template.Resources !== null && typeof template.Resources === "object" ? template.Resources : {};
|
|
86059
|
+
const readers = /* @__PURE__ */ new Map();
|
|
86060
|
+
for (const logicalId of /* @__PURE__ */ new Set([...Object.keys(templateResources), ...stateBags.keys()])) {
|
|
86061
|
+
const definition = Object.hasOwn(templateResources, logicalId) ? templateResources[logicalId] : void 0;
|
|
86062
|
+
const record = definition !== null && typeof definition === "object" ? definition : {};
|
|
86063
|
+
try {
|
|
86064
|
+
const dependencies = collectParameterDependencies([record["Properties"], stateBags.get(logicalId)], record["Condition"], template);
|
|
86065
|
+
readers.set(logicalId, new Set(dependencies.attributeTargets));
|
|
86066
|
+
const hits = [...dependencies.names].filter((name) => divergent.has(name));
|
|
86067
|
+
for (const name of hits) parametersHit.add(name);
|
|
86068
|
+
if (hits.length > 0) refused.add(logicalId);
|
|
86069
|
+
else if (dependencies.unclassifiable) {
|
|
86070
|
+
refused.add(logicalId);
|
|
86071
|
+
unclassifiable.add(logicalId);
|
|
86072
|
+
}
|
|
86073
|
+
} catch {
|
|
86074
|
+
refused.add(logicalId);
|
|
86075
|
+
unclassifiable.add(logicalId);
|
|
86076
|
+
}
|
|
86077
|
+
}
|
|
86078
|
+
for (let grew = refused.size > 0; grew;) {
|
|
86079
|
+
grew = false;
|
|
86080
|
+
for (const [logicalId, targets] of readers) {
|
|
86081
|
+
if (refused.has(logicalId)) continue;
|
|
86082
|
+
if (![...targets].some((target) => refused.has(target))) continue;
|
|
86083
|
+
refused.add(logicalId);
|
|
86084
|
+
grew = true;
|
|
86085
|
+
}
|
|
86086
|
+
}
|
|
86087
|
+
return {
|
|
86088
|
+
refused,
|
|
86089
|
+
unclassifiable,
|
|
86090
|
+
parametersHit
|
|
86091
|
+
};
|
|
86092
|
+
}
|
|
86093
|
+
|
|
84984
86094
|
//#endregion
|
|
84985
86095
|
//#region src/cli/commands/import.ts
|
|
86096
|
+
/**
|
|
86097
|
+
* What {@link resolveImportedProperties} refused, as ONE value (issue #3462).
|
|
86098
|
+
*
|
|
86099
|
+
* It is still the set of logical ids whose observed baseline must not be
|
|
86100
|
+
* captured — every existing reader calls `.has()` — and it additionally carries
|
|
86101
|
+
* the two facts `captureObservedForImportedResources` needs to write, keep or
|
|
86102
|
+
* clear `ResourceState.observedBaselineRefusalReason`. They travel ON the set
|
|
86103
|
+
* rather than beside it, and the capture's parameter is THIS type, so a call
|
|
86104
|
+
* site that copies the ids into a plain `Set` — dropping both facts, and with
|
|
86105
|
+
* them the reason every deploy-side guard keys on — does not compile.
|
|
86106
|
+
*/
|
|
86107
|
+
var ObservedBaselineRefusals = class extends Set {
|
|
86108
|
+
/**
|
|
86109
|
+
* The subset ARM 4 named — directly, through an attribute read, or as an
|
|
86110
|
+
* unclassifiable bag — whether or not another arm refused it too.
|
|
86111
|
+
*/
|
|
86112
|
+
unverifiableParameter = /* @__PURE__ */ new Set();
|
|
86113
|
+
/**
|
|
86114
|
+
* Whether this run HAD deployed parameter values to compare against. Only
|
|
86115
|
+
* then does "ARM 4 did not name it" mean the placeholder was PROVEN; without
|
|
86116
|
+
* a source it means nothing was judged at all.
|
|
86117
|
+
*/
|
|
86118
|
+
hadDeployedParameterSource = false;
|
|
86119
|
+
};
|
|
84986
86120
|
async function importCommand(stackArg, options) {
|
|
84987
86121
|
const logger = getLogger();
|
|
84988
86122
|
if (options.verbose) {
|
|
@@ -85183,7 +86317,8 @@ async function importCommand(stackArg, options) {
|
|
|
85183
86317
|
}
|
|
85184
86318
|
}
|
|
85185
86319
|
const stackState = buildStackState(stackInfo.stackName, targetRegion, rows, templateParser, stateTemplate, existingState, selectiveMode);
|
|
85186
|
-
await
|
|
86320
|
+
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 });
|
|
86321
|
+
await captureObservedForImportedResources(stackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(stackState, stateTemplate, targetRegion, stateBackend, logger, deployedParameters)), rebuiltLogicalIdsFrom(rows, stateTemplate));
|
|
85187
86322
|
const saveOptions = {};
|
|
85188
86323
|
if (existingEtag) saveOptions.expectedEtag = existingEtag;
|
|
85189
86324
|
if (migrationPending) saveOptions.migrateLegacy = true;
|
|
@@ -85203,7 +86338,8 @@ async function importCommand(stackArg, options) {
|
|
|
85203
86338
|
lockOwner: owner,
|
|
85204
86339
|
accountId: accountIdForNestedSynth,
|
|
85205
86340
|
logger,
|
|
85206
|
-
assetRedirect
|
|
86341
|
+
assetRedirect,
|
|
86342
|
+
cfnClient: awsClients.cloudFormation
|
|
85207
86343
|
});
|
|
85208
86344
|
if (migrationCfnStackName) {
|
|
85209
86345
|
const orphaned = resources.length - importedRows.length;
|
|
@@ -85540,6 +86676,8 @@ function rebuiltLogicalIdsFrom(rows, template) {
|
|
|
85540
86676
|
* import flow never derives outputs (they're computed at deploy time from
|
|
85541
86677
|
* each resource's attributes), so even an auto-mode rebuild has no reason
|
|
85542
86678
|
* to wipe them.
|
|
86679
|
+
*
|
|
86680
|
+
* Exported for unit testing — internal to the command flow otherwise.
|
|
85543
86681
|
*/
|
|
85544
86682
|
function buildStackState(stackName, region, rows, templateParser, template, existingState, selectiveMode) {
|
|
85545
86683
|
const resources = selectiveMode && existingState ? { ...existingState.resources } : {};
|
|
@@ -85558,7 +86696,11 @@ function buildStackState(stackName, region, rows, templateParser, template, exis
|
|
|
85558
86696
|
properties: tmplResource.Properties ?? {},
|
|
85559
86697
|
attributes: rowAttributes ?? priorAttributes ?? {},
|
|
85560
86698
|
dependencies: deps,
|
|
85561
|
-
provisionedBy: "sdk"
|
|
86699
|
+
provisionedBy: "sdk",
|
|
86700
|
+
...prior && prior.physicalId === row.physicalId && hasUnverifiableParameterRefusal(prior) && {
|
|
86701
|
+
observedBaselineRefused: true,
|
|
86702
|
+
observedBaselineRefusalReason: "unverifiable-parameter"
|
|
86703
|
+
}
|
|
85562
86704
|
};
|
|
85563
86705
|
}
|
|
85564
86706
|
return {
|
|
@@ -85651,7 +86793,7 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85651
86793
|
* the clear.
|
|
85652
86794
|
*
|
|
85653
86795
|
* The predicate is deliberately CONSERVATIVE — it over-refuses — and that is a
|
|
85654
|
-
* decision a review round paid for.
|
|
86796
|
+
* decision a review round paid for. Four arms:
|
|
85655
86797
|
*
|
|
85656
86798
|
* - **Resolution THREW** — refuse, unconditionally. A throw means the
|
|
85657
86799
|
* resolver could not finish, so nothing about the persisted bag is
|
|
@@ -85692,6 +86834,15 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85692
86834
|
* opener text and no intrinsic anywhere, because every way a discarded
|
|
85693
86835
|
* subtree could have SOURCED a reference is an intrinsic.
|
|
85694
86836
|
*
|
|
86837
|
+
* - **An UNVERIFIABLE INPUT** (ARM 4, issue
|
|
86838
|
+
* [#2854](https://github.com/go-to-k/cdkd/issues/2854)). The raw bag
|
|
86839
|
+
* depends on a template parameter whose DEPLOYED value — read from the
|
|
86840
|
+
* backing CloudFormation stack's `DescribeStacks`, root and every nested
|
|
86841
|
+
* child alike — is not provably the `Default` this import bound. Runs only
|
|
86842
|
+
* when `deployedParameters` is supplied; the comparison, the dependence
|
|
86843
|
+
* walk and the comparison-only rule for the deployed values live in
|
|
86844
|
+
* `import-deployed-parameters.ts`.
|
|
86845
|
+
*
|
|
85695
86846
|
* The cost of a false refusal is one resource's drift baseline until its next
|
|
85696
86847
|
* deploy; the cost of a false pass is a plaintext secret in `state.json`. So an
|
|
85697
86848
|
* unmeasurable bag refuses too.
|
|
@@ -85706,8 +86857,22 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85706
86857
|
* issue and none of them proven to be all of them:
|
|
85707
86858
|
*
|
|
85708
86859
|
* - 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)
|
|
86860
|
+
* the reference, so no opener exists in any template this walk is handed
|
|
86861
|
+
* ([#2854](https://github.com/go-to-k/cdkd/issues/2854)). ARM 4 below
|
|
86862
|
+
* refuses this when a CloudFormation stack backs the import (its
|
|
86863
|
+
* `DescribeStacks` parameters are the comparison); what REMAINS is an
|
|
86864
|
+
* import with NO CloudFormation stack, where no deployed-parameter source
|
|
86865
|
+
* exists, taint across the nested-stack `Outputs` boundary
|
|
86866
|
+
* (`import-deployed-parameters.ts` states it). The NEXT DEPLOY and a
|
|
86867
|
+
* source-less RE-IMPORT used to be on this list
|
|
86868
|
+
* ([#3462](https://github.com/go-to-k/cdkd/issues/3462)): both bind the
|
|
86869
|
+
* same placeholder `Default`, so both now honour
|
|
86870
|
+
* `ResourceState.observedBaselineRefusalReason`, which this arm's verdict
|
|
86871
|
+
* is recorded as. A binary older than that field still clears the marker on
|
|
86872
|
+
* any UPDATE. ARM 4 also WIDENS the population of
|
|
86873
|
+
* [#2872](https://github.com/go-to-k/cdkd/issues/2872): a record preserved
|
|
86874
|
+
* by a selective merge that a pre-#2854 import already gave a plaintext
|
|
86875
|
+
* baseline is now refused, and a refusal leaves that old baseline in place;
|
|
85711
86876
|
* - `redactSecretsForState` cannot PAIR the readback against the source at a
|
|
85712
86877
|
* position the source carries NO leaf for -- an observed KEY beside a paired
|
|
85713
86878
|
* one -- so there is nothing to refuse from and the value scan has no needle
|
|
@@ -85732,15 +86897,17 @@ function defaultOnlyParameterTemplate(template) {
|
|
|
85732
86897
|
* deployer, which is where the structural remedy for all of the above lives.
|
|
85733
86898
|
*
|
|
85734
86899
|
* The set is returned rather than recorded on the state, because it describes
|
|
85735
|
-
* THIS run's resolution
|
|
86900
|
+
* THIS run's resolution; `captureObservedForImportedResources` turns it into
|
|
86901
|
+
* the two record fields that DO outlive the run.
|
|
85736
86902
|
*
|
|
85737
86903
|
* Exported for unit testing — internal to the command flow otherwise. The
|
|
85738
86904
|
* masking this walk applies (issue #2803) is only provable against the REAL
|
|
85739
86905
|
* resolver, since the plaintext reaches the message through the resolver's
|
|
85740
86906
|
* own throw rather than through anything this function writes.
|
|
85741
86907
|
*/
|
|
85742
|
-
async function resolveImportedProperties(stackState, template, region, stateBackend, logger) {
|
|
85743
|
-
const unsafeObservedBaselineLogicalIds =
|
|
86908
|
+
async function resolveImportedProperties(stackState, template, region, stateBackend, logger, deployedParameters) {
|
|
86909
|
+
const unsafeObservedBaselineLogicalIds = new ObservedBaselineRefusals();
|
|
86910
|
+
unsafeObservedBaselineLogicalIds.hadDeployedParameterSource = deployedParameters !== void 0;
|
|
85744
86911
|
const entries = Object.entries(stackState.resources ?? {});
|
|
85745
86912
|
if (entries.length === 0) return unsafeObservedBaselineLogicalIds;
|
|
85746
86913
|
const resolver = new IntrinsicFunctionResolver(region);
|
|
@@ -85767,6 +86934,24 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85767
86934
|
logger.debug(`Template condition evaluation failed during import-time property resolution: ${err instanceof Error ? err.message : String(err)} — continuing without conditions.`);
|
|
85768
86935
|
}
|
|
85769
86936
|
const unboundParameterNames = Object.keys(template.Parameters ?? {}).filter((name) => isUnboundTemplateParameter(name, template, parameters));
|
|
86937
|
+
let divergentParameters;
|
|
86938
|
+
if (deployedParameters !== void 0) try {
|
|
86939
|
+
divergentParameters = reachableDivergentParameters(template, divergentParameterNames(template, parameters, deployedParameters), entries.map(([, resource]) => resource.properties));
|
|
86940
|
+
} catch {
|
|
86941
|
+
divergentParameters = new Set(Object.keys(template.Parameters ?? {}));
|
|
86942
|
+
}
|
|
86943
|
+
let parameterTaint;
|
|
86944
|
+
if (divergentParameters !== void 0 && divergentParameters.size > 0) try {
|
|
86945
|
+
parameterTaint = computeParameterTaint(template, new Map(entries.map(([logicalId, resource]) => [logicalId, resource.properties])), divergentParameters);
|
|
86946
|
+
} catch (err) {
|
|
86947
|
+
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.`);
|
|
86948
|
+
const all = new Set(entries.map(([logicalId]) => logicalId));
|
|
86949
|
+
parameterTaint = {
|
|
86950
|
+
refused: all,
|
|
86951
|
+
unclassifiable: all,
|
|
86952
|
+
parametersHit: /* @__PURE__ */ new Set()
|
|
86953
|
+
};
|
|
86954
|
+
}
|
|
85770
86955
|
const baseContext = {
|
|
85771
86956
|
template,
|
|
85772
86957
|
resources: stackState.resources,
|
|
@@ -85790,7 +86975,9 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85790
86975
|
threw = true;
|
|
85791
86976
|
}
|
|
85792
86977
|
if (recordedSecretValues.size > 0 && resource.attributes !== void 0) resource.attributes = redactSecretsForState(resource.attributes, recordedSecretValues);
|
|
85793
|
-
|
|
86978
|
+
const dependsOnDivergent = parameterTaint?.refused.has(logicalId) === true;
|
|
86979
|
+
if (dependsOnDivergent) unsafeObservedBaselineLogicalIds.unverifiableParameter.add(logicalId);
|
|
86980
|
+
if (threw || dependsOnDivergent) {
|
|
85794
86981
|
unsafeObservedBaselineLogicalIds.add(logicalId);
|
|
85795
86982
|
continue;
|
|
85796
86983
|
}
|
|
@@ -85805,6 +86992,15 @@ async function resolveImportedProperties(stackState, template, region, stateBack
|
|
|
85805
86992
|
}
|
|
85806
86993
|
if (rawOpeners === void 0 || persistedOpeners === void 0 || persistedOpeners < rawOpeners || discardsNonInertSubtree) unsafeObservedBaselineLogicalIds.add(logicalId);
|
|
85807
86994
|
}
|
|
86995
|
+
if (parameterTaint !== void 0) {
|
|
86996
|
+
const inState = new Set(entries.map(([logicalId]) => logicalId));
|
|
86997
|
+
const refusedHere = [...parameterTaint.refused].filter((id) => inState.has(id));
|
|
86998
|
+
const unreadable = refusedHere.filter((id) => parameterTaint.unclassifiable.has(id));
|
|
86999
|
+
if (refusedHere.length > 0) {
|
|
87000
|
+
const named = [...parameterTaint.parametersHit].sort().map((name) => displaySafe(name));
|
|
87001
|
+
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'.");
|
|
87002
|
+
}
|
|
87003
|
+
}
|
|
85808
87004
|
return unsafeObservedBaselineLogicalIds;
|
|
85809
87005
|
}
|
|
85810
87006
|
/** The opener every dynamic reference starts with, secret-bearing or not. */
|
|
@@ -86161,15 +87357,20 @@ async function captureObservedForImportedResources(stackState, providerRegistry,
|
|
|
86161
87357
|
if (entries.length === 0) return;
|
|
86162
87358
|
await Promise.all(entries.map(async ([logicalId, resource]) => {
|
|
86163
87359
|
if (resource.observedBaselineRefused === true && !rebuiltLogicalIds.has(logicalId)) {
|
|
86164
|
-
logger.debug(`observedProperties capture SKIPPED for preserved ${logicalId} (${resource.resourceType}): a previous 'cdkd import' run refused this record's baseline and this run did not re-import it, so its recorded properties are still the ones that refusal distrusted.
|
|
87360
|
+
logger.debug(`observedProperties capture SKIPPED for preserved ${logicalId} (${resource.resourceType}): a previous 'cdkd import' run refused this record's baseline and this run did not re-import it, so its recorded properties are still the ones that refusal distrusted. Re-import it, or deploy a change to it, to restore a baseline — unless its refusal is an unverifiable-parameter one, which only a replacement or a proving re-import discharges.`);
|
|
86165
87361
|
return;
|
|
86166
87362
|
}
|
|
86167
|
-
|
|
87363
|
+
const refusals = unsafeObservedBaselineLogicalIds;
|
|
87364
|
+
const parameterRefusalStands = refusals.unverifiableParameter.has(logicalId) || hasUnverifiableParameterRefusal(resource) && !refusals.hadDeployedParameterSource;
|
|
87365
|
+
if (unsafeObservedBaselineLogicalIds.has(logicalId) || parameterRefusalStands) {
|
|
86168
87366
|
resource.observedBaselineRefused = true;
|
|
86169
|
-
|
|
87367
|
+
if (parameterRefusalStands) resource.observedBaselineRefusalReason = "unverifiable-parameter";
|
|
87368
|
+
else delete resource.observedBaselineRefusalReason;
|
|
87369
|
+
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 ${parameterRefusalStands ? `it is REPLACED, or re-imported while a CloudFormation stack can prove its parameters were deployed at their 'Default' — an in-place deploy binds the same 'Default' and cannot restore the baseline` : `the next successful deploy`}.`);
|
|
86170
87370
|
return;
|
|
86171
87371
|
}
|
|
86172
87372
|
delete resource.observedBaselineRefused;
|
|
87373
|
+
delete resource.observedBaselineRefusalReason;
|
|
86173
87374
|
try {
|
|
86174
87375
|
const provider = providerRegistry.getProviderFor({
|
|
86175
87376
|
resourceType: resource.resourceType,
|
|
@@ -86349,7 +87550,8 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
86349
87550
|
childStackState.parentStack = parentStackName;
|
|
86350
87551
|
childStackState.parentLogicalId = childLogicalId;
|
|
86351
87552
|
childStackState.parentRegion = parentRegion;
|
|
86352
|
-
|
|
87553
|
+
const childDeployedParameters = namesAnyDeclaredParameter(childStateTemplate) ? await readDeployedParameters(childTreeNode.stackName, args.cfnClient, logger, displaySafe(childStackName)) : void 0;
|
|
87554
|
+
await captureObservedForImportedResources(childStackState, providerRegistry, logger, await withSharedDrainBudget(() => resolveImportedProperties(childStackState, childStateTemplate, childRegion, stateBackend, logger, childDeployedParameters)), rebuiltLogicalIdsFrom(rows, childTemplate));
|
|
86353
87555
|
await stateBackend.saveState(childStackName, childRegion, childStackState);
|
|
86354
87556
|
logger.info(`✓ Nested stack state written: ${childStackName} (${childRegion}) — ${rows.filter((r) => r.outcome === "imported").length} resource(s) imported.`);
|
|
86355
87557
|
if (childTreeNode.nested.size > 0) await importNestedStackChildrenRecursive({
|
|
@@ -86365,7 +87567,8 @@ async function importNestedStackChildrenRecursive(args) {
|
|
|
86365
87567
|
lockOwner,
|
|
86366
87568
|
accountId,
|
|
86367
87569
|
logger,
|
|
86368
|
-
assetRedirect
|
|
87570
|
+
assetRedirect,
|
|
87571
|
+
cfnClient: args.cfnClient
|
|
86369
87572
|
});
|
|
86370
87573
|
} finally {
|
|
86371
87574
|
await lockManager.releaseLock(childStackName, childRegion).catch((err) => {
|
|
@@ -96038,4 +97241,4 @@ function buildProgram() {
|
|
|
96038
97241
|
|
|
96039
97242
|
//#endregion
|
|
96040
97243
|
export { buildProgram };
|
|
96041
|
-
//# sourceMappingURL=program-
|
|
97244
|
+
//# sourceMappingURL=program-DcMcqr70.js.map
|