@go-to-k/cdkd 0.284.49 → 0.284.51
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-BBqefiXQ.js → asg-provider-CynKJdi4.js} +2 -2
- package/dist/{asg-provider-BBqefiXQ.js.map → asg-provider-CynKJdi4.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-D-NZ_16N.js → deploy-engine-Dnp730rc.js} +3 -3
- package/dist/{deploy-engine-D-NZ_16N.js.map → deploy-engine-Dnp730rc.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-Bd0gnMR0.js → program-DjH_cfVU.js} +428 -4
- package/dist/{program-Bd0gnMR0.js.map → program-DjH_cfVU.js.map} +1 -1
- package/dist/{version-DZ6zMwCR.js → version-CJG_PFNb.js} +2 -2
- package/dist/{version-DZ6zMwCR.js.map → version-CJG_PFNb.js.map} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $n as setAwsClients, Bt as DiffCalculator, Cr as normalizeAwsError, Gt as TemplateParser, Jn as clearBucketRegionCache, Jt as S3StateBackend, Kn as AssemblyReader, Kt as LockManager, Qn as resetAwsClients, Sr as isCdkdError, W as IAMRoleProvider, Wt as DagBuilder, Xn as AwsClients, Yn as resolveBucketRegion, Zn as getAwsClients, Zt as AssetPublisher, br as SynthesisError, er as AssetError, et as CloudControlProvider, ir as DependencyError, mr as ProvisioningError, nr as ConfigError, q as ProviderRegistry, r as DeployEngine, tr as CdkdError, ur as LockError, wn as Synthesizer, xr as formatError, yr as StateError } from "./deploy-engine-
|
|
1
|
+
import { $n as setAwsClients, Bt as DiffCalculator, Cr as normalizeAwsError, Gt as TemplateParser, Jn as clearBucketRegionCache, Jt as S3StateBackend, Kn as AssemblyReader, Kt as LockManager, Qn as resetAwsClients, Sr as isCdkdError, W as IAMRoleProvider, Wt as DagBuilder, Xn as AwsClients, Yn as resolveBucketRegion, Zn as getAwsClients, Zt as AssetPublisher, br as SynthesisError, er as AssetError, et as CloudControlProvider, ir as DependencyError, mr as ProvisioningError, nr as ConfigError, q as ProviderRegistry, r as DeployEngine, tr as CdkdError, ur as LockError, wn as Synthesizer, xr as formatError, yr as StateError } from "./deploy-engine-Dnp730rc.js";
|
|
2
2
|
import { n as getLogger, r as setLogger, t as ConsoleLogger } from "./logger-zRrlbaQt.js";
|
|
3
3
|
|
|
4
4
|
export { AssemblyReader, AssetError, AssetPublisher, AwsClients, CdkdError, CloudControlProvider, ConfigError, ConsoleLogger, DagBuilder, DependencyError, DeployEngine, DiffCalculator, IAMRoleProvider, LockError, LockManager, ProviderRegistry, ProvisioningError, S3StateBackend, StateError, SynthesisError, Synthesizer, TemplateParser, clearBucketRegionCache, formatError, getAwsClients, getLogger, isCdkdError, normalizeAwsError, resetAwsClients, resolveBucketRegion, setAwsClients, setLogger };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $ as startInterruptWatch, $n as setAwsClients, $t as WorkGraph, A as renderStatefulReason, An as resolveCaptureObservedState, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as findLargeInlineResources, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as getDockerImageBySourceHash, Cr as normalizeAwsError, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as getLegacyStateBucketName, Dr as isThrottlingError, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as getDefaultStateBucketName, Er as isRetryableTransientError, Et as errorCauseChain, F as green, Fn as stateBucketExistenceConfirmed, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as derivePartitionAndUrlSuffix, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as expectedOwnerParam, Ht as describeTypeWithThrottleRetry, I as red, In as warnDeprecatedNoPrefixCliFlag, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jt as S3StateBackend, K as clearOnUpdateRemoval, Kn as AssemblyReader, Kt as LockManager, L as yellow, Ln as CFN_TEMPLATE_BODY_LIMIT, Lt as s3BucketRegionalDomainName, M as bold, Mn as resolveStateBucketWithDefault, Mt as classifyReplaySecretRegion, N as cyan, Nn as resolveStateBucketWithDefaultAndSource, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveApp, Or as markNonRetryable, Ot as maskSecretsInError, P as gray, Pn as resolveUseCdkBootstrapAssets, Pt as s3BucketArn, Q as isInterruptedWaitError, Qn as resetAwsClients, Qt as stringifyValue, R as collectDeclaredOutputNames, Rn as CFN_TEMPLATE_URL_LIMIT, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as AssetManifestLoader, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as synthesisStatusMessage, Tr as isMarkedNonRetryable, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as PARTITION_TABLE, Ut as withRetry, V as isExportAliasCollision, Vn as uploadCfnTemplate, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as canonicalizeRegion, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as AwsClients, Xt as shouldRetainResource, Y as findSilentDropProperties, Yn as resolveBucketRegion, Yt as rebuildClientForBucketRegion, Z as endCommandInterruptScope, Zn as getAwsClients, Zt as AssetPublisher, _ as computeImplicitDeleteEdges, _n as formatDockerLoginError, _r as StackHasActiveImportsError, _t as replayWarn, a as DeploymentEventsStore, an as stripControlChars, ar as DeployCancelledError, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as runDockerForeground, bt as requireConfigString, c as replayFailedOperations, cn as ensureAssetStorage, cr as LocalMigrateError, ct as refStateLookupFromResource, d as updatePartialReason, dn as readBootstrapMarkerBody, dr as MissingCdkCliError, dt as resolveExplicitPhysicalId, en as buildAssetRedirectMap, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateAssetBucketName, fr as NestedStackChildDirectDestroyError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as dockerSpawnEnvWithSensitive, gr as ResourceUpdateNotSupportedError, gt as readConfigString, h as maskingRetryLogger, hn as buildDockerImage, hr as ResourceTimeoutError, ht as configStringRefusal, i as DeploymentEventsReader, in as escapeRegExp, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveSkipPrefix, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveAutoAssetStorage, kt as maskSecretsInText, l as replayRollback, ln as getBootstrapMarkerKey, lr as LocalStartServiceError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDenyExternalAccessPolicy, mr as ProvisioningError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as loadPublishableAssetManifest, nt as disableInstanceApiTermination, o as planFailedOps, on as AssetModeResolver, or as DynamicReferenceRegionAmbiguousError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as validateContainerRepoName, pr as PartialFailureError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as processStackMessages, qt as displaySafe, r as DeployEngine, rn as rewriteTemplateAssetReferences, rr as CrossAccountSecretRefusalError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as BOOTSTRAP_MARKER_PREFIX, sr as LocalInvokeBuildError$1, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as createAssetRedirectResolver, tr as CdkdError, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as parseBootstrapMarker, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as getDockerCmd, vr as StackTerminationProtectionError, vt as requireConfigArray, w as refusesFinalSnapshot, wn as Synthesizer, wr as withErrorHandling, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as runDockerStreaming, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as partitionSensitiveEnv, yt as requireConfigObject, z as collectPublishedOutputNames, zn as MIGRATE_TMP_PREFIX, zt as applyRoleArnIfSet } from "./deploy-engine-
|
|
1
|
+
import { $ as startInterruptWatch, $n as setAwsClients, $t as WorkGraph, A as renderStatefulReason, An as resolveCaptureObservedState, At as redactSecretsForState, B as exportAliasCollisionScrubWarning, Bn as findLargeInlineResources, Bt as DiffCalculator, C as isFinalSnapshotError, Cn as getDockerImageBySourceHash, Cr as normalizeAwsError, Ct as TEMPLATE_SOURCED_RULES, D as extractDeploymentEventError, Dn as getLegacyStateBucketName, Dr as isThrottlingError, Dt as isSingleDynamicReferenceToken, E as makeCanonicalizePropertiesFn, En as getDefaultStateBucketName, Er as isRetryableTransientError, Et as errorCauseChain, F as green, Fn as stateBucketExistenceConfirmed, Ft as s3BucketDomainName, G as collectInlinePolicyNamesManagedBySiblings, Gn as derivePartitionAndUrlSuffix, Gt as TemplateParser, H as secretBearingStateKeyWarning, Hn as expectedOwnerParam, Ht as describeTypeWithThrottleRetry, I as red, In as warnDeprecatedNoPrefixCliFlag, It as s3BucketDualStackDomainName, J as findActionableSilentDrops, Jt as S3StateBackend, K as clearOnUpdateRemoval, Kn as AssemblyReader, Kt as LockManager, L as yellow, Ln as CFN_TEMPLATE_BODY_LIMIT, Lt as s3BucketRegionalDomainName, M as bold, Mn as resolveStateBucketWithDefault, Mt as classifyReplaySecretRegion, N as cyan, Nn as resolveStateBucketWithDefaultAndSource, Nt as producerRegionsFromState, O as MULTI_REGION_RECREATE_BLOCKED_TYPES, On as resolveApp, Or as markNonRetryable, Ot as maskSecretsInError, P as gray, Pn as resolveUseCdkBootstrapAssets, Pt as s3BucketArn, Q as isInterruptedWaitError, Qn as resetAwsClients, Qt as stringifyValue, R as collectDeclaredOutputNames, Rn as CFN_TEMPLATE_URL_LIMIT, Rt as s3BucketWebsiteUrl, S as createPreDeleteFinalSnapshot, Sn as AssetManifestLoader, St as STATE_SOURCED_READBACK_RULES, T as unsupportedFinalSnapshotError, Tn as synthesisStatusMessage, Tr as isMarkedNonRetryable, Tt as dynamicReferenceTokens, U as stateKeySecretExposure, Un as PARTITION_TABLE, Ut as withRetry, V as isExportAliasCollision, Vn as uploadCfnTemplate, Vt as INTRINSIC_KEYS, W as IAMRoleProvider, Wn as canonicalizeRegion, Wt as DagBuilder, X as beginCommandInterruptScope, Xn as AwsClients, Xt as shouldRetainResource, Y as findSilentDropProperties, Yn as resolveBucketRegion, Yt as rebuildClientForBucketRegion, Z as endCommandInterruptScope, Zn as getAwsClients, Zt as AssetPublisher, _ as computeImplicitDeleteEdges, _n as formatDockerLoginError, _r as StackHasActiveImportsError, _t as replayWarn, a as DeploymentEventsStore, an as stripControlChars, ar as DeployCancelledError, at as carriesDynamicReference, b as buildFinalSnapshotIdentifier, bn as runDockerForeground, bt as requireConfigString, c as replayFailedOperations, cn as ensureAssetStorage, cr as LocalMigrateError, ct as refStateLookupFromResource, d as updatePartialReason, dn as readBootstrapMarkerBody, dr as MissingCdkCliError, dt as resolveExplicitPhysicalId, en as buildAssetRedirectMap, et as CloudControlProvider, f as UNSPECIFIED_SKIP_REASON, fn as validateAssetBucketName, fr as NestedStackChildDirectDestroyError, ft as assertRegionMatch, g as IMPLICIT_DELETE_DEPENDENCIES, gn as dockerSpawnEnvWithSensitive, gr as ResourceUpdateNotSupportedError, gt as readConfigString, h as maskingRetryLogger, hn as buildDockerImage, hr as ResourceTimeoutError, ht as configStringRefusal, i as DeploymentEventsReader, in as escapeRegExp, it as IntrinsicFunctionResolver, j as formatResourceLine, jn as resolveSkipPrefix, jt as scrubResourceRecord, k as isStatefulRecreateTargetSync, kn as resolveAutoAssetStorage, kt as maskSecretsInText, l as replayRollback, ln as getBootstrapMarkerKey, lr as LocalStartServiceError, lt as WAFv2WebACLProvider, m as withResourceDeadline, mn as buildDenyExternalAccessPolicy, mr as ProvisioningError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as loadPublishableAssetManifest, nt as disableInstanceApiTermination, o as planFailedOps, on as AssetModeResolver, or as DynamicReferenceRegionAmbiguousError, ot as cfnRefValueFromPhysicalId, p as deleteSkipReason, pn as validateContainerRepoName, pr as PartialFailureError, pt as coerceCfnBoolean, q as ProviderRegistry, qn as processStackMessages, qt as displaySafe, r as DeployEngine, rn as rewriteTemplateAssetReferences, rr as CrossAccountSecretRefusalError, rt as isTerminationProtectionPropagationError, s as planRollback, sn as BOOTSTRAP_MARKER_PREFIX, sr as LocalInvokeBuildError$1, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as createAssetRedirectResolver, tr as CdkdError, tt as slowCcOperationTimeoutMs, u as updatePartialMessage, un as parseBootstrapMarker, ut as normalizeAwsTagsToCfn, v as ATOMIC_FINAL_SNAPSHOT_TYPES, vn as getDockerCmd, vr as StackTerminationProtectionError, vt as requireConfigArray, w as refusesFinalSnapshot, wn as Synthesizer, wr as withErrorHandling, wt as createSecretMasker, x as ccRoutedFinalSnapshotError, xn as runDockerStreaming, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as PRE_DELETE_SNAPSHOT_TYPES, yn as partitionSensitiveEnv, yt as requireConfigObject, z as collectPublishedOutputNames, zn as MIGRATE_TMP_PREFIX, zt as applyRoleArnIfSet } from "./deploy-engine-Dnp730rc.js";
|
|
2
2
|
import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
|
|
3
|
-
import { t as getCdkdVersion } from "./version-
|
|
4
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
3
|
+
import { t as getCdkdVersion } from "./version-CJG_PFNb.js";
|
|
4
|
+
import { t as ASGProvider } from "./asg-provider-CynKJdi4.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
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, 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";
|
|
@@ -15887,6 +15887,155 @@ function autoScalingRetriesWithinDeleteBudget(budget, defaultMaxRetries) {
|
|
|
15887
15887
|
const AUTO_SCALING_TEARDOWN_RESERVE_MS = 20 * 6e4;
|
|
15888
15888
|
/** `withRetry`'s capped backoff step (`maxDelayMs`), the cost of one retry. */
|
|
15889
15889
|
const AUTO_SCALING_RETRY_STEP_MS = 8e3;
|
|
15890
|
+
/**
|
|
15891
|
+
* {@link ProtectionFlipRecord}s keyed by the resource they describe, so a
|
|
15892
|
+
* `delete()` that is RE-ENTERED remembers what an earlier attempt did.
|
|
15893
|
+
*
|
|
15894
|
+
* Shaped after `ElapsedBudgetRegistry` in `../../utils/elapsed-budget.ts` and
|
|
15895
|
+
* keyed with the same {@link deleteBudgetKey}, because it is the same re-entry:
|
|
15896
|
+
* `destroy-runner.ts` wraps its outer retry loop — up to four `delete()` calls
|
|
15897
|
+
* — in ONE deadline, and re-invokes `delete()` for anything it classes as
|
|
15898
|
+
* retryable. A record created per call cannot see that. Attempt 1 observes the
|
|
15899
|
+
* guard ON, flips it off and fails on a throttle; attempt 2's pre-flip
|
|
15900
|
+
* `DescribeTable` now reports the guard OFF (attempt 1 is why), so a per-call
|
|
15901
|
+
* record starts and stays `false` and a TERMINAL failure on attempt 2
|
|
15902
|
+
* compensates nothing. That is precisely the issue's retry-then-fail case
|
|
15903
|
+
* (#1978).
|
|
15904
|
+
*
|
|
15905
|
+
* A provider INSTANCE FIELD would be wrong for the same reason the budget is
|
|
15906
|
+
* not one: providers are singletons serving concurrent resources, so one field
|
|
15907
|
+
* would carry another table's flip. The key qualifies the physical id by
|
|
15908
|
+
* region, exactly as the budget's does.
|
|
15909
|
+
*
|
|
15910
|
+
* Entries are RETAINED on a throw — that is the point — and released by the
|
|
15911
|
+
* caller on a terminal outcome (a completed delete, or a NotFound), so the map
|
|
15912
|
+
* holds at most one entry per in-flight resource.
|
|
15913
|
+
*/
|
|
15914
|
+
var ProtectionFlipRegistry = class {
|
|
15915
|
+
records = /* @__PURE__ */ new Map();
|
|
15916
|
+
/**
|
|
15917
|
+
* The record for `key`, creating it on first use and REUSING it on re-entry.
|
|
15918
|
+
*
|
|
15919
|
+
* `reuseWithinMs` bounds how long an entry may be reused for and is passed
|
|
15920
|
+
* the same `DELETE_BUDGET_REUSE_WINDOW_MS` the allowance uses: past the
|
|
15921
|
+
* deadline the whole path is sized against, that deadline has certainly
|
|
15922
|
+
* fired, so a caller arriving now belongs to a NEW operation and must not
|
|
15923
|
+
* inherit a previous one's flip. Without it a retained record would make a
|
|
15924
|
+
* much later destroy of the same table re-enable a guard it never touched.
|
|
15925
|
+
*/
|
|
15926
|
+
acquire(key, reuseWithinMs, clock = monotonicNowMs) {
|
|
15927
|
+
const existing = this.records.get(key);
|
|
15928
|
+
if (existing) {
|
|
15929
|
+
if (existing.age.elapsedMs() <= reuseWithinMs) return existing.record;
|
|
15930
|
+
this.records.delete(key);
|
|
15931
|
+
}
|
|
15932
|
+
const created = {
|
|
15933
|
+
record: {
|
|
15934
|
+
flippedOffByThisRun: false,
|
|
15935
|
+
deleteAccepted: false
|
|
15936
|
+
},
|
|
15937
|
+
age: new ElapsedBudget(reuseWithinMs, clock)
|
|
15938
|
+
};
|
|
15939
|
+
this.records.set(key, created);
|
|
15940
|
+
return created.record;
|
|
15941
|
+
}
|
|
15942
|
+
/** Drop `key`'s record — call on a TERMINAL outcome, never between retries. */
|
|
15943
|
+
release(key) {
|
|
15944
|
+
this.records.delete(key);
|
|
15945
|
+
}
|
|
15946
|
+
/** Live entries; exists so a test can prove release actually released. */
|
|
15947
|
+
get size() {
|
|
15948
|
+
return this.records.size;
|
|
15949
|
+
}
|
|
15950
|
+
clear() {
|
|
15951
|
+
this.records.clear();
|
|
15952
|
+
}
|
|
15953
|
+
};
|
|
15954
|
+
/**
|
|
15955
|
+
* Whether `error` ends the delete for good, i.e. whether `destroy-runner.ts`'s
|
|
15956
|
+
* outer loop will NOT re-enter `delete()` for it.
|
|
15957
|
+
*
|
|
15958
|
+
* This is the gate on the compensation, and it is the issue's own point:
|
|
15959
|
+
* re-enabling the guard after a RETRYABLE failure would flip the flag back and
|
|
15960
|
+
* forth across a retry sequence, since the next `delete()` immediately turns it
|
|
15961
|
+
* off again. Compensation belongs on the terminal failure only.
|
|
15962
|
+
*
|
|
15963
|
+
* The predicate MIRRORS `destroy-runner.ts`'s re-entry condition
|
|
15964
|
+
* (`!isMarkedNonRetryable && (isRetryableTransientError || 'Too Many
|
|
15965
|
+
* Requests')`) rather than inventing a second classification, and it is applied
|
|
15966
|
+
* to the error the provider is about to THROW — the same wrapped
|
|
15967
|
+
* `ProvisioningError` message the outer loop will classify — so the two cannot
|
|
15968
|
+
* disagree about who gets re-entered.
|
|
15969
|
+
*
|
|
15970
|
+
* A user abort is terminal here even though nothing classifies it: the run is
|
|
15971
|
+
* being torn down, so no re-entry is coming and the guard would otherwise stay
|
|
15972
|
+
* down. That is the Ctrl-C route recorded on the issue.
|
|
15973
|
+
*
|
|
15974
|
+
* KNOWN NARROWINGS, all deliberate. Each leaves the guard off in a case this
|
|
15975
|
+
* mechanism does not reach; none of them is silent about it here.
|
|
15976
|
+
*
|
|
15977
|
+
* 1. **Attempt-cap exhaustion.** A genuinely retryable failure that exhausts
|
|
15978
|
+
* the outer loop's attempt cap ends the run with the guard still off,
|
|
15979
|
+
* because the provider cannot see which attempt is the last one.
|
|
15980
|
+
* Compensating on every attempt instead would trade that residue for an
|
|
15981
|
+
* `UpdateTable` pair per retry against a table AWS is already throttling.
|
|
15982
|
+
* 2. **The per-resource DEADLINE route.** `src/deployment/resource-deadline.ts`
|
|
15983
|
+
* rejects the OUTER promise on its timer and does NOT cancel what it
|
|
15984
|
+
* wraps — the provider's own `await` never settles as a rejection, so no
|
|
15985
|
+
* `ResourceTimeoutError` ever enters `delete()`'s `catch` and nothing here
|
|
15986
|
+
* runs for it. The provider keeps polling behind a run that has already
|
|
15987
|
+
* reported failure (the same non-cancelling shape issue #1955 documents),
|
|
15988
|
+
* and if that poll eventually succeeds the table is gone anyway. Closing
|
|
15989
|
+
* this would mean making the deadline cancel — a change to a mechanism
|
|
15990
|
+
* every provider shares — so it is recorded, not fixed here.
|
|
15991
|
+
* 3. **The compensation itself is unbounded.** `reEnable` is one `UpdateTable`
|
|
15992
|
+
* with no timeout of its own, so on Ctrl-C it adds one SDK call per flipped
|
|
15993
|
+
* table to a teardown the user has already asked to end. Left unbounded on
|
|
15994
|
+
* purpose: an `UpdateTable` is a single control-plane round trip (no
|
|
15995
|
+
* polling, no wait for ACTIVE), the SDK's own retry/timeout config already
|
|
15996
|
+
* applies to it, and it is the ONLY thing standing between a Ctrl-C and a
|
|
15997
|
+
* live table with its guard stripped — a timeout short enough to be felt
|
|
15998
|
+
* during teardown would mostly convert successful restores into the "could
|
|
15999
|
+
* NOT re-enable" line. Revisit if the compensation ever grows a WAIT.
|
|
16000
|
+
*/
|
|
16001
|
+
function isTerminalDeleteFailure(error) {
|
|
16002
|
+
if (isInterruptedWaitError(error)) return true;
|
|
16003
|
+
if (isMarkedNonRetryable(error)) return true;
|
|
16004
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16005
|
+
return !(isRetryableTransientError(error, message) || message.includes("Too Many Requests"));
|
|
16006
|
+
}
|
|
16007
|
+
/**
|
|
16008
|
+
* Put `DeletionProtectionEnabled` back after a `--remove-protection` flip whose
|
|
16009
|
+
* delete then failed terminally (issue #1978).
|
|
16010
|
+
*
|
|
16011
|
+
* BEST-EFFORT, and it says so. Three properties, each of which the issue names:
|
|
16012
|
+
*
|
|
16013
|
+
* - **It never masks the original error.** This function does not throw —
|
|
16014
|
+
* including when the re-enable itself fails — because it runs inside the
|
|
16015
|
+
* `catch` that is about to re-throw the delete failure, and a secondary
|
|
16016
|
+
* write that throws would REPLACE the reported outcome with its own. The
|
|
16017
|
+
* delete failure stays the outcome; this is a secondary line.
|
|
16018
|
+
* - **It never edits the primary message either.** Splicing the re-enable's
|
|
16019
|
+
* text into the thrown error would change what `isRetryableTransientError`
|
|
16020
|
+
* substring-matches on, so a compensation failure carrying a throttle phrase
|
|
16021
|
+
* could flip a terminal delete failure into a retryable one and re-run the
|
|
16022
|
+
* whole path. The narration goes to the logger, not into the throw.
|
|
16023
|
+
* - **It names the table when it fails.** A silent failure here leaves a LIVE
|
|
16024
|
+
* table with its guard down, which is the exact residue this mechanism
|
|
16025
|
+
* exists to remove, so the message carries the physical id and the one
|
|
16026
|
+
* command that fixes it.
|
|
16027
|
+
*/
|
|
16028
|
+
async function compensateRemovedDeletionProtection(opts) {
|
|
16029
|
+
if (!opts.flip.flippedOffByThisRun) return;
|
|
16030
|
+
if (opts.flip.deleteAccepted) return;
|
|
16031
|
+
if (!isTerminalDeleteFailure(opts.error)) return;
|
|
16032
|
+
try {
|
|
16033
|
+
await opts.reEnable();
|
|
16034
|
+
opts.logger.warn(`DynamoDB ${opts.typeLabel} ${opts.logicalId}: the delete failed after --remove-protection had turned DeletionProtectionEnabled off, so it was re-enabled on ${opts.physicalId}. The delete failure below is the outcome.`);
|
|
16035
|
+
} catch (reEnableError) {
|
|
16036
|
+
opts.logger.error(`DynamoDB ${opts.typeLabel} ${opts.logicalId}: could NOT re-enable DeletionProtectionEnabled on ${opts.physicalId} after the delete failed — that table is LIVE with its deletion protection still off. Restore it with: aws dynamodb update-table --table-name ${opts.physicalId} --deletion-protection-enabled. (${reEnableError instanceof Error ? reEnableError.message : String(reEnableError)})`);
|
|
16037
|
+
}
|
|
16038
|
+
}
|
|
15890
16039
|
|
|
15891
16040
|
//#endregion
|
|
15892
16041
|
//#region src/provisioning/providers/dynamodb-table-provider.ts
|
|
@@ -16488,6 +16637,17 @@ var DynamoDBTableProvider = class {
|
|
|
16488
16637
|
* provider instance.
|
|
16489
16638
|
*/
|
|
16490
16639
|
deleteBudgets = new ElapsedBudgetRegistry();
|
|
16640
|
+
/**
|
|
16641
|
+
* What each in-flight delete did to `DeletionProtectionEnabled` (issue
|
|
16642
|
+
* #1978), keyed exactly as the allowance above and for exactly the same
|
|
16643
|
+
* reason: `destroy-runner.ts` re-enters `delete()` after a retryable failure,
|
|
16644
|
+
* and the second attempt's pre-flip `DescribeTable` sees the guard already
|
|
16645
|
+
* off BECAUSE the first attempt turned it off. A record created per call
|
|
16646
|
+
* would therefore read "nothing to undo" on precisely the retry-then-fail
|
|
16647
|
+
* case the issue names. Keyed rather than a plain field for the budget's
|
|
16648
|
+
* reason too — one provider instance serves a whole level's tables at once.
|
|
16649
|
+
*/
|
|
16650
|
+
protectionFlips = new ProtectionFlipRegistry();
|
|
16491
16651
|
constructor() {
|
|
16492
16652
|
const awsClients = getAwsClients();
|
|
16493
16653
|
this.dynamoDBClient = awsClients.dynamoDB;
|
|
@@ -16807,15 +16967,60 @@ var DynamoDBTableProvider = class {
|
|
|
16807
16967
|
}
|
|
16808
16968
|
/**
|
|
16809
16969
|
* Delete a DynamoDB table
|
|
16970
|
+
*
|
|
16971
|
+
* A thin wrapper around {@link deleteTableResource} whose only job is the
|
|
16972
|
+
* `--remove-protection` compensation (issue #1978): if this run turned
|
|
16973
|
+
* `DeletionProtectionEnabled` off and the delete then ended for good — a
|
|
16974
|
+
* terminal AWS refusal, or a Ctrl-C landing anywhere after the flip — the
|
|
16975
|
+
* guard goes back on before the failure is re-thrown. Without it the run ends
|
|
16976
|
+
* with a LIVE table whose protection cdkd silently stripped: the failure is
|
|
16977
|
+
* loud, the side effect is not.
|
|
16978
|
+
*
|
|
16979
|
+
* It is a WRAPPER rather than a `catch` inside the body because the window is
|
|
16980
|
+
* not one `try`. Between the flip and a successful delete this provider can
|
|
16981
|
+
* throw from the index-settled re-arm, from the retry loop, and from an
|
|
16982
|
+
* interrupted wait, and the same shape on the sibling `GlobalTable` provider
|
|
16983
|
+
* spans a replica teardown as well. Catching at the boundary covers every
|
|
16984
|
+
* exit; the {@link ProtectionFlipRecord} — not the position of the catch — is
|
|
16985
|
+
* what decides whether there is anything to undo.
|
|
16810
16986
|
*/
|
|
16811
16987
|
async delete(logicalId, physicalId, resourceType, _properties, context) {
|
|
16988
|
+
const flip = this.protectionFlips.acquire(deleteBudgetKey(physicalId, context?.expectedRegion), DYNAMODB_DELETE_MIN_RESOURCE_TIMEOUT_MS, resolveDynamoDbDeleteBudgetClock());
|
|
16989
|
+
try {
|
|
16990
|
+
await this.deleteTableResource(logicalId, physicalId, resourceType, context, flip);
|
|
16991
|
+
} catch (error) {
|
|
16992
|
+
await compensateRemovedDeletionProtection({
|
|
16993
|
+
flip,
|
|
16994
|
+
error,
|
|
16995
|
+
logicalId,
|
|
16996
|
+
physicalId,
|
|
16997
|
+
typeLabel: "table",
|
|
16998
|
+
logger: this.logger,
|
|
16999
|
+
reEnable: async () => {
|
|
17000
|
+
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
17001
|
+
TableName: physicalId,
|
|
17002
|
+
DeletionProtectionEnabled: true
|
|
17003
|
+
}));
|
|
17004
|
+
}
|
|
17005
|
+
});
|
|
17006
|
+
throw error;
|
|
17007
|
+
}
|
|
17008
|
+
}
|
|
17009
|
+
async deleteTableResource(logicalId, physicalId, resourceType, context, flip) {
|
|
16812
17010
|
this.logger.debug(`Deleting DynamoDB table ${logicalId}: ${physicalId}`);
|
|
16813
17011
|
const budget = this.deleteBudgets.acquire(deleteBudgetKey(physicalId, context?.expectedRegion), resolveDynamoDbDeleteBudgetMs(), resolveDynamoDbDeleteBudgetClock(), DYNAMODB_DELETE_MIN_RESOURCE_TIMEOUT_MS);
|
|
16814
17012
|
if (context?.removeProtection === true) try {
|
|
17013
|
+
let observedProtectionOn = false;
|
|
17014
|
+
try {
|
|
17015
|
+
observedProtectionOn = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table?.DeletionProtectionEnabled === true;
|
|
17016
|
+
} catch (observeError) {
|
|
17017
|
+
this.logger.debug(`Could not read DeletionProtectionEnabled on ${physicalId} before disabling it: ${observeError instanceof Error ? observeError.message : String(observeError)}`);
|
|
17018
|
+
}
|
|
16815
17019
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
16816
17020
|
TableName: physicalId,
|
|
16817
17021
|
DeletionProtectionEnabled: false
|
|
16818
17022
|
}));
|
|
17023
|
+
if (observedProtectionOn) flip.flippedOffByThisRun = true;
|
|
16819
17024
|
this.logger.debug(`Disabled DeletionProtectionEnabled on DynamoDB table ${logicalId}, waiting for ACTIVE`);
|
|
16820
17025
|
try {
|
|
16821
17026
|
await this.waitForTableActiveAfterUpdate(physicalId, 60, budget);
|
|
@@ -16841,6 +17046,7 @@ var DynamoDBTableProvider = class {
|
|
|
16841
17046
|
logger: this.logger,
|
|
16842
17047
|
deleteTable: async () => {
|
|
16843
17048
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
17049
|
+
flip.deleteAccepted = true;
|
|
16844
17050
|
},
|
|
16845
17051
|
reArm: () => waitForIndexesSettled({
|
|
16846
17052
|
tableName: physicalId,
|
|
@@ -16859,11 +17065,13 @@ var DynamoDBTableProvider = class {
|
|
|
16859
17065
|
});
|
|
16860
17066
|
this.logger.debug(`Successfully deleted DynamoDB table ${logicalId}`);
|
|
16861
17067
|
this.deleteBudgets.release(deleteBudgetKey(physicalId, context?.expectedRegion));
|
|
17068
|
+
this.protectionFlips.release(deleteBudgetKey(physicalId, context?.expectedRegion));
|
|
16862
17069
|
} catch (error) {
|
|
16863
17070
|
if (error instanceof ResourceNotFoundException$1) {
|
|
16864
17071
|
assertRegionMatch(await this.dynamoDBClient.config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
|
|
16865
17072
|
this.logger.debug(`DynamoDB table ${physicalId} does not exist, skipping deletion`);
|
|
16866
17073
|
this.deleteBudgets.release(deleteBudgetKey(physicalId, context?.expectedRegion));
|
|
17074
|
+
this.protectionFlips.release(deleteBudgetKey(physicalId, context?.expectedRegion));
|
|
16867
17075
|
return;
|
|
16868
17076
|
}
|
|
16869
17077
|
const cause = error instanceof Error ? error : void 0;
|
|
@@ -18064,6 +18272,17 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
18064
18272
|
* terms it closes.
|
|
18065
18273
|
*/
|
|
18066
18274
|
deleteBudgets = new ElapsedBudgetRegistry();
|
|
18275
|
+
/**
|
|
18276
|
+
* What each in-flight delete did to `DeletionProtectionEnabled` (issue
|
|
18277
|
+
* #1978), keyed exactly as the allowance above and for exactly the same
|
|
18278
|
+
* reason: `destroy-runner.ts` re-enters `delete()` after a retryable failure,
|
|
18279
|
+
* and the second attempt's pre-flip `DescribeTable` sees the guard already
|
|
18280
|
+
* off BECAUSE the first attempt turned it off. A record created per call
|
|
18281
|
+
* would therefore read "nothing to undo" on precisely the retry-then-fail
|
|
18282
|
+
* case the issue names. Keyed rather than a plain field for the budget's
|
|
18283
|
+
* reason too — one provider instance serves a whole level's tables at once.
|
|
18284
|
+
*/
|
|
18285
|
+
protectionFlips = new ProtectionFlipRegistry();
|
|
18067
18286
|
handledProperties = /* @__PURE__ */ new Map([["AWS::DynamoDB::GlobalTable", /* @__PURE__ */ new Set([
|
|
18068
18287
|
"TableName",
|
|
18069
18288
|
"KeySchema",
|
|
@@ -19332,13 +19551,42 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
19332
19551
|
* strip a still-existing resource from state.
|
|
19333
19552
|
*/
|
|
19334
19553
|
async delete(logicalId, physicalId, resourceType, _properties, context) {
|
|
19554
|
+
const flip = this.protectionFlips.acquire(deleteBudgetKey(physicalId, context?.expectedRegion), DYNAMODB_DELETE_MIN_RESOURCE_TIMEOUT_MS, resolveDynamoDbDeleteBudgetClock());
|
|
19555
|
+
try {
|
|
19556
|
+
await this.deleteGlobalTableResource(logicalId, physicalId, resourceType, context, flip);
|
|
19557
|
+
} catch (error) {
|
|
19558
|
+
await compensateRemovedDeletionProtection({
|
|
19559
|
+
flip,
|
|
19560
|
+
error,
|
|
19561
|
+
logicalId,
|
|
19562
|
+
physicalId,
|
|
19563
|
+
typeLabel: "GlobalTable",
|
|
19564
|
+
logger: this.logger,
|
|
19565
|
+
reEnable: async () => {
|
|
19566
|
+
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
19567
|
+
TableName: physicalId,
|
|
19568
|
+
DeletionProtectionEnabled: true
|
|
19569
|
+
}));
|
|
19570
|
+
}
|
|
19571
|
+
});
|
|
19572
|
+
throw error;
|
|
19573
|
+
}
|
|
19574
|
+
}
|
|
19575
|
+
async deleteGlobalTableResource(logicalId, physicalId, resourceType, context, flip) {
|
|
19335
19576
|
this.logger.debug(`Deleting DynamoDB GlobalTable ${logicalId}: ${physicalId}`);
|
|
19336
19577
|
const budget = this.deleteBudgets.acquire(deleteBudgetKey(physicalId, context?.expectedRegion), resolveDynamoDbDeleteBudgetMs(), resolveDynamoDbDeleteBudgetClock(), DYNAMODB_DELETE_MIN_RESOURCE_TIMEOUT_MS);
|
|
19337
19578
|
if (context?.removeProtection === true) try {
|
|
19579
|
+
let observedProtectionOn = false;
|
|
19580
|
+
try {
|
|
19581
|
+
observedProtectionOn = (await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }))).Table?.DeletionProtectionEnabled === true;
|
|
19582
|
+
} catch (observeError) {
|
|
19583
|
+
this.logger.debug(`Could not read DeletionProtectionEnabled on ${physicalId} before disabling it: ${observeError instanceof Error ? observeError.message : String(observeError)}`);
|
|
19584
|
+
}
|
|
19338
19585
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
19339
19586
|
TableName: physicalId,
|
|
19340
19587
|
DeletionProtectionEnabled: false
|
|
19341
19588
|
}));
|
|
19589
|
+
if (observedProtectionOn) flip.flippedOffByThisRun = true;
|
|
19342
19590
|
this.logger.debug(`Disabled DeletionProtectionEnabled on ${logicalId}, waiting for ACTIVE`);
|
|
19343
19591
|
try {
|
|
19344
19592
|
await this.waitForTableActiveAfterUpdate(physicalId, logicalId, 600, budget);
|
|
@@ -19407,6 +19655,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
19407
19655
|
logger: this.logger,
|
|
19408
19656
|
deleteTable: async () => {
|
|
19409
19657
|
await this.dynamoDBClient.send(new DeleteTableCommand({ TableName: physicalId }));
|
|
19658
|
+
flip.deleteAccepted = true;
|
|
19410
19659
|
},
|
|
19411
19660
|
reArm: () => this.waitForIndexesActive(physicalId, logicalId, {
|
|
19412
19661
|
maxAttempts: 60,
|
|
@@ -19423,11 +19672,13 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
19423
19672
|
await this.waitForTableGone(physicalId, logicalId, 600, budget);
|
|
19424
19673
|
this.logger.debug(`Successfully deleted DynamoDB GlobalTable ${logicalId}`);
|
|
19425
19674
|
this.deleteBudgets.release(deleteBudgetKey(physicalId, context?.expectedRegion));
|
|
19675
|
+
this.protectionFlips.release(deleteBudgetKey(physicalId, context?.expectedRegion));
|
|
19426
19676
|
} catch (error) {
|
|
19427
19677
|
if (error instanceof ResourceNotFoundException$1) {
|
|
19428
19678
|
assertRegionMatch(await this.dynamoDBClient.config.region(), context?.expectedRegion, resourceType, logicalId, physicalId);
|
|
19429
19679
|
this.logger.debug(`DynamoDB GlobalTable ${physicalId} does not exist, skipping`);
|
|
19430
19680
|
this.deleteBudgets.release(deleteBudgetKey(physicalId, context?.expectedRegion));
|
|
19681
|
+
this.protectionFlips.release(deleteBudgetKey(physicalId, context?.expectedRegion));
|
|
19431
19682
|
return;
|
|
19432
19683
|
}
|
|
19433
19684
|
const cause = error instanceof Error ? error : void 0;
|
|
@@ -65558,6 +65809,10 @@ var DriftDetectedError = class DriftDetectedError extends CdkdError {
|
|
|
65558
65809
|
* a partially-failed `--revert` still exits 2 through `PartialFailureError`) --
|
|
65559
65810
|
* those modes report per-resource refusals on their own paths, and changing them
|
|
65560
65811
|
* would alter what a remediation run means, which is not what this closes.
|
|
65812
|
+
* Issue [#2208](https://github.com/go-to-k/cdkd/issues/2208) re-examined that
|
|
65813
|
+
* scoping and KEPT it: what those modes owed was not this exit code but an
|
|
65814
|
+
* honest SENTENCE, since their no-drift line claimed a clean bill of health for
|
|
65815
|
+
* a stack they had not read. See {@link incompleteRemediationMessage}.
|
|
65561
65816
|
*
|
|
65562
65817
|
* WHAT DOES NOT TRIGGER IT: a stack whose properties reference
|
|
65563
65818
|
* `{{resolve:ssm-secure:...}}` still exits 0. Those properties genuinely are not
|
|
@@ -65652,6 +65907,171 @@ function outcomeExitSignal(outcome) {
|
|
|
65652
65907
|
});
|
|
65653
65908
|
}
|
|
65654
65909
|
/**
|
|
65910
|
+
* What {@link incompleteRemediationMessage} says about each reason, and WHICH
|
|
65911
|
+
* CLAIM it is allowed to make about it.
|
|
65912
|
+
*
|
|
65913
|
+
* An exhaustive `Record`, for the same reason {@link notComparedReason} is one:
|
|
65914
|
+
* adding a cause must be a COMPILE ERROR here rather than falling into an
|
|
65915
|
+
* `else` that describes it as something it is not. The first cut of this
|
|
65916
|
+
* function had that `else`, and it was worse than a wrong default -- it
|
|
65917
|
+
* described any unknown cause as a dynamic-reference problem, while
|
|
65918
|
+
* {@link outcomeExitSignal} deliberately routes every NEW cause to the
|
|
65919
|
+
* incomplete side. The two together meant the next cause added would be
|
|
65920
|
+
* reported, at once, to the widest audience and under the wrong name. Carrying
|
|
65921
|
+
* `kind` in the SAME record rather than a second one beside it means a new
|
|
65922
|
+
* reason cannot be given a phrase while quietly inheriting a claim.
|
|
65923
|
+
*
|
|
65924
|
+
* `kind` exists because one blanket tail overclaimed. The line used to end
|
|
65925
|
+
* `..., so cdkd does not know whether they drifted`, which is true of a
|
|
65926
|
+
* throttled read and FALSE of a `Custom::*` resource -- for that one, issue
|
|
65927
|
+
* [#323](https://github.com/go-to-k/cdkd/issues/323)'s position is that drift is
|
|
65928
|
+
* not APPLICABLE, not that cdkd is uncertain. A stack with one throttle and five
|
|
65929
|
+
* `Custom::S3AutoDeleteObjects` would have claimed uncertainty about all six.
|
|
65930
|
+
*
|
|
65931
|
+
* The insertion ORDER is the order the phrases are emitted in, so the line is
|
|
65932
|
+
* deterministic without a second list to keep in sync: "not compared at all"
|
|
65933
|
+
* first, then the partial ones, then the two structural reasons.
|
|
65934
|
+
*
|
|
65935
|
+
* The wording splits on "was ANY of it compared", which is the same split the
|
|
65936
|
+
* human report's `NOT fully compared` heading makes -- a `readFailed` resource
|
|
65937
|
+
* is not "partially" anything.
|
|
65938
|
+
*/
|
|
65939
|
+
const UNCOMPARED_REASONS = {
|
|
65940
|
+
readFailed: {
|
|
65941
|
+
kind: "unknown",
|
|
65942
|
+
phrase: "not compared AT ALL: the read or comparison failed"
|
|
65943
|
+
},
|
|
65944
|
+
refused: {
|
|
65945
|
+
kind: "unknown",
|
|
65946
|
+
phrase: "only PARTIALLY compared: cdkd refused to resolve a dynamic reference their state records"
|
|
65947
|
+
},
|
|
65948
|
+
unresolvedToken: {
|
|
65949
|
+
kind: "unknown",
|
|
65950
|
+
phrase: "only PARTIALLY compared: their state records a `{{resolve:...}}` spelling cdkd resolves for nobody, which no re-run can clear"
|
|
65951
|
+
},
|
|
65952
|
+
unsupported: {
|
|
65953
|
+
kind: "byDesign",
|
|
65954
|
+
phrase: "not compared AT ALL: their provider does not support drift detection yet"
|
|
65955
|
+
},
|
|
65956
|
+
skipped: {
|
|
65957
|
+
kind: "byDesign",
|
|
65958
|
+
phrase: "not compared AT ALL: cdkd does not drift-check the type"
|
|
65959
|
+
}
|
|
65960
|
+
};
|
|
65961
|
+
/**
|
|
65962
|
+
* The sentence lead each {@link UncomparedKind} group is reported under.
|
|
65963
|
+
*
|
|
65964
|
+
* Exhaustive for the same reason the record above is, and SEPARATE from it
|
|
65965
|
+
* because the lead is per GROUP while the phrase is per reason: writing the
|
|
65966
|
+
* claim into each phrase would put five copies of two sentences in the file,
|
|
65967
|
+
* and five copies is how two of them come to disagree.
|
|
65968
|
+
*
|
|
65969
|
+
* Insertion order is emit order, and `unknown` comes first deliberately: it is
|
|
65970
|
+
* the half the user can do something about.
|
|
65971
|
+
*/
|
|
65972
|
+
const UNCOMPARED_KIND_LEADS = {
|
|
65973
|
+
unknown: "cdkd does not know whether these drifted",
|
|
65974
|
+
byDesign: "Not drift-checked by cdkd at all, which is a coverage limit rather than uncertainty"
|
|
65975
|
+
};
|
|
65976
|
+
/**
|
|
65977
|
+
* How many resources across every report ended UNCOMPARED, per reason.
|
|
65978
|
+
*
|
|
65979
|
+
* The `notCompared` population is read through {@link notComparedOutcomes} --
|
|
65980
|
+
* the single spelling both renderings already share -- and the other two come
|
|
65981
|
+
* from an exhaustive `matchOutcome`, so a new outcome variant cannot join the
|
|
65982
|
+
* "nothing to say about it" side by omission. That is the same guarantee
|
|
65983
|
+
* {@link outcomeExitSignal} carries at the exit code, applied to the count.
|
|
65984
|
+
*/
|
|
65985
|
+
function uncomparedTally(reports) {
|
|
65986
|
+
const tally = /* @__PURE__ */ new Map();
|
|
65987
|
+
const bump = (reason) => {
|
|
65988
|
+
tally.set(reason, (tally.get(reason) ?? 0) + 1);
|
|
65989
|
+
};
|
|
65990
|
+
for (const report of reports) {
|
|
65991
|
+
for (const { cause } of notComparedOutcomes(report)) bump(cause);
|
|
65992
|
+
for (const outcome of report.outcomes) matchOutcome(outcome, {
|
|
65993
|
+
drifted: () => {},
|
|
65994
|
+
clean: () => {},
|
|
65995
|
+
notCompared: () => {},
|
|
65996
|
+
unsupported: () => bump("unsupported"),
|
|
65997
|
+
skipped: () => bump("skipped")
|
|
65998
|
+
});
|
|
65999
|
+
}
|
|
66000
|
+
return tally;
|
|
66001
|
+
}
|
|
66002
|
+
/**
|
|
66003
|
+
* The lines `--accept` / `--revert` print INSTEAD of `No drift detected --
|
|
66004
|
+
* nothing to accept.` when the run found no drift but did not manage to compare
|
|
66005
|
+
* everything ([issue #2208](https://github.com/go-to-k/cdkd/issues/2208)).
|
|
66006
|
+
*
|
|
66007
|
+
* MESSAGE-ONLY, and the EXIT CODE deliberately stays `0`. The remediation
|
|
66008
|
+
* modes' exit codes are a documented user contract -- #2108 scoped its `2` to
|
|
66009
|
+
* detection-only mode on purpose, and {@link DriftComparisonIncompleteError}'s
|
|
66010
|
+
* note says why: "changing them would alter what a remediation run means".
|
|
66011
|
+
* Nothing about that reasoning expired. The defect this closes is entirely in
|
|
66012
|
+
* what the run REPORTS: `--accept` / `--revert` already correctly leave an
|
|
66013
|
+
* uncompared resource alone (both iterate the drifted outcomes only, asserted
|
|
66014
|
+
* in `tests/unit/cli/drift-per-resource-failure.test.ts`), so the state and AWS
|
|
66015
|
+
* are right and only the sentence is wrong. Making the remediation path exit
|
|
66016
|
+
* non-zero would break the CI of everyone running `cdkd drift --accept` over a
|
|
66017
|
+
* stack that hits a throttle, to fix a wording problem.
|
|
66018
|
+
*
|
|
66019
|
+
* What the lines have to carry, since the exit code will not carry it: that the
|
|
66020
|
+
* comparison was INCOMPLETE (never that no drift was detected), HOW MANY
|
|
66021
|
+
* resources were not compared and WHY, WHICH of those cdkd is actually
|
|
66022
|
+
* uncertain about, and the pointer to the detection-only run, which is the mode
|
|
66023
|
+
* whose exit code does report it (`2`).
|
|
66024
|
+
*
|
|
66025
|
+
* THE TRIGGER AND THE COUNT ARE DIFFERENT POPULATIONS, deliberately, and
|
|
66026
|
+
* collapsing them is the defect review round 1 found here.
|
|
66027
|
+
*
|
|
66028
|
+
* - The TRIGGER is `anyIncomplete`, i.e. {@link outcomeExitSignal}'s
|
|
66029
|
+
* `incomplete`. It is narrow on purpose: a stack whose ONLY uncompared
|
|
66030
|
+
* resource holds a `{{resolve:ssm-secure:...}}` token must not start
|
|
66031
|
+
* shouting on every run about a comparison no action of the user's can ever
|
|
66032
|
+
* complete -- the same CI-forever hazard that cause is kept out of the exit
|
|
66033
|
+
* code on.
|
|
66034
|
+
* - The COUNT and the LABELS, once the line is triggered, cover EVERY
|
|
66035
|
+
* uncompared resource, each named by its own reason (see
|
|
66036
|
+
* {@link uncomparedTally}). A resource that was not compared was not
|
|
66037
|
+
* compared, whatever the reason, and counting only the clearable ones
|
|
66038
|
+
* printed `1 of 3` on a stack the report a few lines above called
|
|
66039
|
+
* `2 resource(s) NOT fully compared` -- two lines disagreeing about one
|
|
66040
|
+
* run, with the newer and quieter one being the one written to stop a
|
|
66041
|
+
* command from being quietly reassuring.
|
|
66042
|
+
*
|
|
66043
|
+
* WHAT `N` AND `M` ARE, stated because the alternatives are all defensible and
|
|
66044
|
+
* silence about the choice is what made the first cut wrong. `M` is the total
|
|
66045
|
+
* number of resource outcomes, and `N` is every one of them that was not
|
|
66046
|
+
* compared -- so `unsupported` and `skipped` land in `N`, WITH their own
|
|
66047
|
+
* phrase, rather than sitting only in the denominator where they would inflate
|
|
66048
|
+
* `M` and go unnamed. On a remediation path, erring LOUD is the correct
|
|
66049
|
+
* direction. Note this makes `N` deliberately WIDER than the report's
|
|
66050
|
+
* `NOT fully compared` heading, which counts the reference / read population
|
|
66051
|
+
* only and reports `drift unknown` separately: the question this line answers
|
|
66052
|
+
* is #2154's, "was everything actually compared", not "how many entries are in
|
|
66053
|
+
* that block". What being in `N` does NOT buy is a uniform claim about the
|
|
66054
|
+
* resource -- see {@link UNCOMPARED_REASONS}'s `kind`, and see
|
|
66055
|
+
* {@link UncomparedReason} for why counting a `skipped` resource here is not a
|
|
66056
|
+
* reversal of #323's silence.
|
|
66057
|
+
*/
|
|
66058
|
+
function incompleteRemediationMessage(reports, mode) {
|
|
66059
|
+
const tally = uncomparedTally(reports);
|
|
66060
|
+
const total = reports.reduce((n, report) => n + report.outcomes.length, 0);
|
|
66061
|
+
const uncompared = [...tally.values()].reduce((a, b) => a + b, 0);
|
|
66062
|
+
const kinds = Object.keys(UNCOMPARED_KIND_LEADS);
|
|
66063
|
+
const reasons = Object.keys(UNCOMPARED_REASONS);
|
|
66064
|
+
const groups = kinds.flatMap((kind) => {
|
|
66065
|
+
const parts = reasons.filter((reason) => UNCOMPARED_REASONS[reason].kind === kind && (tally.get(reason) ?? 0) > 0).map((reason) => `${tally.get(reason)} ${UNCOMPARED_REASONS[reason].phrase}`);
|
|
66066
|
+
return parts.length === 0 ? [] : [`${UNCOMPARED_KIND_LEADS[kind]} — ${parts.join("; ")}.`];
|
|
66067
|
+
});
|
|
66068
|
+
return [
|
|
66069
|
+
`Comparison INCOMPLETE — nothing to ${mode}, and that is NOT a clean bill of health: ${uncompared} of ${total} resource(s) could not be compared.`,
|
|
66070
|
+
...groups,
|
|
66071
|
+
`Re-run 'cdkd drift' without --${mode} to see which resources and why — a detection-only run exits 2 while a comparison is incomplete.`
|
|
66072
|
+
];
|
|
66073
|
+
}
|
|
66074
|
+
/**
|
|
65655
66075
|
* `cdkd drift [<stack>...]` command implementation.
|
|
65656
66076
|
*
|
|
65657
66077
|
* Three operating modes (mutually exclusive):
|
|
@@ -65728,6 +66148,10 @@ async function driftCommand(stacks, options) {
|
|
|
65728
66148
|
return;
|
|
65729
66149
|
}
|
|
65730
66150
|
if (!drifted) {
|
|
66151
|
+
if (anyIncomplete) {
|
|
66152
|
+
for (const line of incompleteRemediationMessage(reports, options.accept ? "accept" : "revert")) logger.info(line);
|
|
66153
|
+
return;
|
|
66154
|
+
}
|
|
65731
66155
|
logger.info(options.accept ? "No drift detected — nothing to accept." : "No drift detected — nothing to revert.");
|
|
65732
66156
|
return;
|
|
65733
66157
|
}
|
|
@@ -86808,4 +87232,4 @@ function buildProgram() {
|
|
|
86808
87232
|
|
|
86809
87233
|
//#endregion
|
|
86810
87234
|
export { buildProgram };
|
|
86811
|
-
//# sourceMappingURL=program-
|
|
87235
|
+
//# sourceMappingURL=program-DjH_cfVU.js.map
|