@go-to-k/cdkd 0.286.3 → 0.287.0
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-uaOnRUMe.js → asg-provider-D10GWdiA.js} +146 -4
- package/dist/asg-provider-D10GWdiA.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine--rkIGhow.js → deploy-engine-B377YKBI.js} +1548 -266
- package/dist/deploy-engine-B377YKBI.js.map +1 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-CG34-sQd.js → program-D2Vykejc.js} +741 -323
- package/dist/{program-CG34-sQd.js.map → program-D2Vykejc.js.map} +1 -1
- package/dist/{version-D3md0Lde.js → version-Rs7TV7Gz.js} +2 -2
- package/dist/{version-D3md0Lde.js.map → version-Rs7TV7Gz.js.map} +1 -1
- package/package.json +2 -1
- package/dist/asg-provider-uaOnRUMe.js.map +0 -1
- package/dist/deploy-engine--rkIGhow.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as findActionableSilentDrops, $n as runDockerStreaming, $r as StackTerminationProtectionError, $t as carriesSecretMask, A as WARM_THROUGHPUT_MEMBERS, An as rewriteTemplateAssetReferences, At as configStringRefusal, B as yellow, Bn as validateContainerRepoName, Bt as s3BucketDualStackDomainName, C as unsupportedFinalSnapshotError, Cn as shouldRetainResource, Cr as displaySafe, Ct as refStateLookupFromResource, D as isStatefulRecreateTargetForReplace, Dn as buildAssetRedirectMap, Dr as derivePartitionAndUrlSuffix, Dt as assertRegionMatch, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as WorkGraph, Er as canonicalizeRegion, Et as resolveExplicitPhysicalId, F as bold, Fn as getBootstrapMarkerKey, Fr as setAwsClients, Ft as requireConfigString, G as secretBearingStateKeyWarning, Gn as describeDockerExecFailure, Gr as LocalStartServiceError, Gt as INTRINSIC_KEYS, H as collectPublishedOutputNames, Hn as describeAwsFailure, Hr as DynamicReferenceRegionAmbiguousError, Ht as s3BucketWebsiteUrl, I as cyan, In as isCrossRegionRedirect, It as classifyReplaySecretRegion, J as IAMRoleProvider, Jn as formatDockerLoginError, Jr as PartialFailureError, Jt as DagBuilder, K as stateKeySecretExposure, Kn as describeDockerFailure, Kt as describeTypeWithThrottleRetry, L as gray, Ln as parseBootstrapMarker, Lr as CdkdError, Lt as producerRegionsFromState, M as isWarmThroughputDecrease, Mn as BOOTSTRAP_MARKER_PREFIX, Mr as AwsClients, Mt as replayWarn, N as toFiniteNumber, Nn as assertAssetBucketRegion, Nr as getAwsClients, Nt as requireConfigArray, O as isStatefulRecreateTargetSync, On as createAssetRedirectResolver, Or as AssemblyReader, Ot as coerceCfnBoolean, P as formatResourceLine, Pn as ensureAssetStorage, Pr as resetAwsClients, Pt as requireConfigObject, Q as wouldReturnToSdkProvider, Qn as runDockerForeground, Qr as StackHasActiveImportsError, Qt as TEMPLATE_SOURCED_RULES, R as green, Rn as readBootstrapMarkerBody, Rt as s3BucketArn, S as refusesFinalSnapshot, Sn as importableOutputs, Sr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, St as parameterTypeMayLoseSecretIdentity, T as extractDeploymentEventError, Tn as stringifyValue, Tr as PARTITION_TABLE, Tt as normalizeAwsTagsToCfn, U as exportAliasCollisionScrubWarning, Un as buildDockerImage, Ur as IntrinsicResolutionRefusalError, Ut as applyRoleArnIfSet, V as collectDeclaredOutputNames, Vn as buildDenyExternalAccessPolicy, Vr as DeployCancelledError, Vt as s3BucketRegionalDomainName, W as isExportAliasCollision, Wn as describeDockerCapturedOutput, Wr as LocalInvokeBuildError$1, Wt as DiffCalculator, X as clearOnUpdateRemoval, Xn as partitionSensitiveEnv, Xr as ResourceTimeoutError, Xt as STATE_SOURCED_CROSS_GENERATION_RULES, Y as collectInlinePolicyNamesManagedBySiblings, Yn as getDockerCmd, Yr as ProvisioningError, Yt as TemplateParser, Z as ProviderRegistry, Zn as redactDockerArgvValues, Zr as ResourceUpdateNotSupportedError, Zt as STATE_SOURCED_READBACK_RULES, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as shellQuote, _r as CFN_TEMPLATE_BODY_LIMIT, _t as carriesDynamicReference, a as DeploymentEventsStore, ai as withErrorHandling, an as maskSecretsInText, ar as synthesisStatusMessage, at as endCommandInterruptScope, b as createPreDeleteFinalSnapshot, bn as exportNamesCarriedFrom, br as findLargeInlineResources, bt as getAccountInfo, c as replayFailedOperations, ci as isThrottlingError, cn as redactSecretsForState, cr as resolveApp, ct as startInterruptWatch, d as updatePartialReason, di as markRedactedCause, dn as S3StateBackend, dr as resolveSkipPrefix, dt as UNSPECIFIED_SKIP_REASON, en as createSecretMasker, er as escapeRegExp, et as findSilentDropProperties, f as withResourceDeadline, fi as retryClassificationText, fn as rebuildClientForBucketRegion, fr as resolveStateBucketWithDefault, ft as deleteIndeterminateGuards, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as forceQuitRecoveryClause, gr as warnDeprecatedNoPrefixCliFlag, gt as IntrinsicFunctionResolver, h as computeImplicitDeleteEdges, hn as buildLockContentionMessage, hr as stateBucketExistenceConfirmed, ht as isTerminationProtectionPropagationError, i as DeploymentEventsReader, ii as normalizeAwsError, in as maskSecretsInError, ir as Synthesizer, it as beginCommandInterruptScope, j as coerceWarmThroughput$1, jn as AssetModeResolver, jr as resolveBucketRegion, jt as readConfigString, k as renderStatefulReason, kn as loadPublishableAssetManifest, kr as processStackMessages, kt as configBooleanRefusal, l as replayRollback, li as isTransientServerError, ln as scrubResourceRecord, lr as resolveAutoAssetStorage, lt as CloudControlProvider, m as IMPLICIT_DELETE_DEPENDENCIES, mn as buildForceUnlockCommand, mr as resolveUseCdkBootstrapAssets, mt as disableInstanceApiTermination, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as errorCauseChain, nr as AssetManifestLoader, nt as maskDeep, o as planFailedOps, oi as isMarkedNonRetryable, on as recordMaskOnlyValue, or as getDefaultStateBucketName, ot as interruptWatchListenerCount, p as maskingRetryLogger, pn as UNRENDERABLE, pr as resolveStateBucketWithDefaultAndSource, pt as deleteSkipReason, q as getCurrentResourceSecrets, qn as dockerSpawnEnvWithSensitive, qr as NestedStackChildDirectDestroyError, qt as withRetry, r as DeployEngine, rn as isSingleDynamicReferenceToken, rr as getDockerImageBySourceHash, rt as maskerOrIdentity, s as planRollback, si as isRetryableTransientError, sn as recoverMaskedOutput, sr as getLegacyStateBucketName, st as isInterruptedWaitError, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as dynamicReferenceTokens, tr as stripControlChars, tt as createMaskedRetryLogger, u as updatePartialMessage, ui as markNonRetryable, un as LockManager, ur as resolveCaptureObservedState, ut as slowCcOperationTimeoutMs, v as buildFinalSnapshotIdentifier, vn as CUSTOM_RESOURCE_RESPONSE_PREFIX, vr as CFN_TEMPLATE_URL_LIMIT, vt as cfnRefValueFromPhysicalId, w as makeCanonicalizePropertiesFn, wn as AssetPublisher, wr as expectedOwnerParam, wt as WAFv2WebACLProvider, x as isFinalSnapshotError, xn as importableOutputKeys, xr as uploadCfnTemplate, xt as isUnboundTemplateParameter, y as ccRoutedFinalSnapshotError, yn as DEFAULT_STATE_PREFIX, yr as MIGRATE_TMP_PREFIX, yt as coerceParameterTypedValue, z as red, zn as validateAssetBucketName, zr as CrossAccountSecretRefusalError, zt as s3BucketDomainName } from "./deploy-engine-B377YKBI.js";
|
|
2
2
|
import { _ as withStackName, c as PATTERN_B_NAME_OPTIONS, f as generateResourceName, g as withSkipPrefix, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as getCurrentStackName, n as getLogger, o as runStackBuffered, p as generateResourceNameWithFallback, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES } from "./logger-Dw-3y48G.js";
|
|
3
3
|
import { t as awsClientDefaults } from "./aws-client-defaults-D-iYiIJ6.js";
|
|
4
|
-
import { t as getCdkdVersion } from "./version-
|
|
5
|
-
import { t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as getCdkdVersion } from "./version-Rs7TV7Gz.js";
|
|
5
|
+
import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-D10GWdiA.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";
|
|
@@ -745,6 +745,32 @@ function parseRecreateViaSdkProviderToken(value, previous) {
|
|
|
745
745
|
if (!LOGICAL_ID_FORMAT.test(token)) throw new Error(`Invalid --recreate-via-sdk-provider value "${value}": expected a CloudFormation logical id (alphanumeric, starts with a letter, max 255 chars). One --recreate-via-sdk-provider flag per resource — repeat the flag for additional targets.`);
|
|
746
746
|
return [...previous ?? [], token];
|
|
747
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* Issue [#2719] — `--pin-cc-api <logicalId>` keeps a named resource on the
|
|
750
|
+
* Cloud Control route for this deploy, suppressing the `'sdk-coverage'`
|
|
751
|
+
* sticky-exemption that would otherwise return it to its SDK provider.
|
|
752
|
+
*
|
|
753
|
+
* The lightweight counterpart to `--recreate-via-cc-api`: that one forces the
|
|
754
|
+
* CC layer by DESTROYING and recreating the resource, which is not something a
|
|
755
|
+
* user can do to a stateful resource just to decline a routing change. This
|
|
756
|
+
* one changes only which provider runs the update.
|
|
757
|
+
*
|
|
758
|
+
* Deliberately per-deploy rather than a state field. A persisted pin would be
|
|
759
|
+
* a v10 schema bump plus its migration integ, and nothing has yet shown a user
|
|
760
|
+
* needing the pin to survive a deploy they did not pass it on -- the flip is
|
|
761
|
+
* conditioned on the resource's own coverage, so the case it guards against is
|
|
762
|
+
* "I disagree with this specific deploy's routing", not a standing preference.
|
|
763
|
+
*
|
|
764
|
+
* It does NOT override a `'cc-broken'` exemption: honoring it there would pin
|
|
765
|
+
* the resource to a handler that cannot manage it, which is the bug those
|
|
766
|
+
* entries exist to escape.
|
|
767
|
+
*/
|
|
768
|
+
function parsePinCcApiToken(value, previous) {
|
|
769
|
+
const token = value.trim();
|
|
770
|
+
if (!LOGICAL_ID_FORMAT.test(token)) throw new Error(`Invalid --pin-cc-api value "${value}": expected a CloudFormation logical id (alphanumeric, starts with a letter, max 255 chars). One --pin-cc-api flag per resource — repeat the flag for additional targets.`);
|
|
771
|
+
return [...previous ?? [], token];
|
|
772
|
+
}
|
|
773
|
+
const pinCcApiOption = new Option("--pin-cc-api <logicalId>", "Keep the named resource (by CloudFormation logical id) on the Cloud Control route in this deploy, declining the automatic return to the SDK Provider that a type-level sdk-coverage exemption would otherwise apply to a resource recorded as provisionedBy: cc-api. Repeatable — pass the flag once per resource. Has no effect on a resource already on the SDK path, and does not override an exemption admitted because Cloud Control cannot manage the type.").argParser(parsePinCcApiToken);
|
|
748
774
|
const recreateViaSdkProviderOption = new Option("--recreate-via-sdk-provider <logicalId>", "Destroy + recreate the named resource (by CloudFormation logical id) via cdkd's SDK Provider in this deploy, so a resource currently sticky on provisionedBy: cc-api flips back to provisionedBy: sdk. Used after a #609 backfill release adds SDK coverage for a type the user originally needed CC for (e.g. Lambda LoggingConfig). Repeatable — pass the flag once per resource. Per-resource opt-in. Stateful resource types refuse unless --force-stateful-recreation is ALSO passed.").argParser(parseRecreateViaSdkProviderToken);
|
|
749
775
|
/**
|
|
750
776
|
* Issue [#615] — `--force-stateful-recreation` (boolean) confirms a
|
|
@@ -874,6 +900,7 @@ const deployOptions = [
|
|
|
874
900
|
allowUnsupportedPropertiesOption,
|
|
875
901
|
recreateViaCcApiOption,
|
|
876
902
|
recreateViaSdkProviderOption,
|
|
903
|
+
pinCcApiOption,
|
|
877
904
|
forceStatefulRecreationOption,
|
|
878
905
|
replaceOption,
|
|
879
906
|
useCdkBootstrapAssetsOption,
|
|
@@ -2579,6 +2606,31 @@ function isPromptAbortError(error) {
|
|
|
2579
2606
|
* intent** — does the user want SDK + silent drop, or CC migration?
|
|
2580
2607
|
* Fail fast and let the user pick one strategy per resource.
|
|
2581
2608
|
*/
|
|
2609
|
+
const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
|
|
2610
|
+
/**
|
|
2611
|
+
* The CFn type of a nested stack's row in its PARENT's template. It decides
|
|
2612
|
+
* both the refusal (`blockedNestedStackTargets`) and the evidence the
|
|
2613
|
+
* unknown-id note renders (`nestedStackLogicalIds`) — so within this module it
|
|
2614
|
+
* is spelled once, or the note could name ids the validator does not refuse.
|
|
2615
|
+
*
|
|
2616
|
+
* The two are NOT the same set, and deliberately so. The refusal fires when
|
|
2617
|
+
* EITHER the state record or the template says nested, because the two types
|
|
2618
|
+
* select different halves of the destructive path: the TEMPLATE's type picks
|
|
2619
|
+
* the provider that deletes the old resource (a Type change emits an UPDATE
|
|
2620
|
+
* carrying `resourceType: desiredResource.Type`, which is what
|
|
2621
|
+
* `oldDeleteProvider` resolves from), while the STATE's type is what
|
|
2622
|
+
* `NestedStackProvider` owns and derives `<parent>~<logicalId>` from. The note
|
|
2623
|
+
* keys on the TEMPLATE alone, because it is telling the user what their current
|
|
2624
|
+
* template contains. Which type governs was argued from plausibility twice and
|
|
2625
|
+
* settled by reading `analyzer/diff-calculator.ts` and `deploy-engine.ts`'s
|
|
2626
|
+
* `oldDeleteProvider`; do not re-derive it from either name. Other modules spell it
|
|
2627
|
+
* for themselves rather than sharing one export: the only EXPORTED copy lives
|
|
2628
|
+
* in `src/cli/commands/retire-cfn-stack.ts`, and importing a CLI command module
|
|
2629
|
+
* from the deployment layer would invert the dependency direction. (No count of
|
|
2630
|
+
* the other spellings is given here on purpose — an unfenced number in a
|
|
2631
|
+
* comment is a number that goes stale.)
|
|
2632
|
+
*/
|
|
2633
|
+
const NESTED_STACK_RESOURCE_TYPE$1 = "AWS::CloudFormation::Stack";
|
|
2582
2634
|
/**
|
|
2583
2635
|
* Plan-time validation of the user's recreate-via-cc-api list.
|
|
2584
2636
|
*
|
|
@@ -2592,7 +2644,6 @@ function isPromptAbortError(error) {
|
|
|
2592
2644
|
* Input order is preserved; duplicate logical ids in the user's input
|
|
2593
2645
|
* are deduplicated.
|
|
2594
2646
|
*/
|
|
2595
|
-
const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
|
|
2596
2647
|
function validateRecreateTargets(input) {
|
|
2597
2648
|
const seenCcApi = new Set(input.recreateViaCcApi);
|
|
2598
2649
|
const seenSdk = new Set(input.recreateViaSdkProvider ?? []);
|
|
@@ -2605,6 +2656,7 @@ function validateRecreateTargets(input) {
|
|
|
2605
2656
|
const ambiguousIntentSdk = [];
|
|
2606
2657
|
const blockedStatefulTargets = [];
|
|
2607
2658
|
const blockedMultiRegionTargets = [];
|
|
2659
|
+
const blockedNestedStackTargets = [];
|
|
2608
2660
|
const blockedAlreadySdk = [];
|
|
2609
2661
|
const blockedAlreadyCcApi = [];
|
|
2610
2662
|
const blockedNoSdkProvider = [];
|
|
@@ -2635,11 +2687,12 @@ function validateRecreateTargets(input) {
|
|
|
2635
2687
|
logicalId,
|
|
2636
2688
|
resourceType,
|
|
2637
2689
|
physicalId: recordedResource.physicalId,
|
|
2638
|
-
statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties),
|
|
2690
|
+
statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties, recordedResource.observedProperties),
|
|
2639
2691
|
direction
|
|
2640
2692
|
};
|
|
2641
2693
|
targets.push(target);
|
|
2642
2694
|
if (MULTI_REGION_RECREATE_BLOCKED_TYPES.has(resourceType)) blockedMultiRegionTargets.push(target);
|
|
2695
|
+
if (resourceType === NESTED_STACK_RESOURCE_TYPE$1 || templateResource.Type === NESTED_STACK_RESOURCE_TYPE$1) blockedNestedStackTargets.push(target);
|
|
2643
2696
|
if (direction === "to-cc-api") {
|
|
2644
2697
|
const actionableDrops = findActionableSilentDrops(resourceType, templateResource.Properties, EMPTY_ALLOW_SET$1);
|
|
2645
2698
|
for (const { property } of actionableDrops) {
|
|
@@ -2671,10 +2724,12 @@ function validateRecreateTargets(input) {
|
|
|
2671
2724
|
ambiguousIntentSdk,
|
|
2672
2725
|
blockedStatefulTargets,
|
|
2673
2726
|
blockedMultiRegionTargets,
|
|
2727
|
+
blockedNestedStackTargets,
|
|
2674
2728
|
blockedAlreadySdk,
|
|
2675
2729
|
blockedAlreadyCcApi,
|
|
2676
2730
|
blockedNoSdkProvider,
|
|
2677
|
-
conflictingDirections
|
|
2731
|
+
conflictingDirections,
|
|
2732
|
+
nestedStackLogicalIds: Object.entries(input.template.Resources ?? {}).filter(([, resource]) => resource?.Type === NESTED_STACK_RESOURCE_TYPE$1).map(([id]) => id)
|
|
2678
2733
|
};
|
|
2679
2734
|
}
|
|
2680
2735
|
/**
|
|
@@ -2691,6 +2746,8 @@ function renderRecreateTargetsErrors(validation) {
|
|
|
2691
2746
|
lines.push(`${FLAG_UMBRELLA} named ${validation.unknownLogicalIds.length} logical id(s) not present in the synth template:`);
|
|
2692
2747
|
for (const id of validation.unknownLogicalIds) lines.push(` - ${id}`);
|
|
2693
2748
|
lines.push(" Fix: confirm each id exists in the template (CDK display path is the parent; the logical id is the CFn-emitted name, e.g. cdkd synth | jq '.Resources | keys'). Recreate operates on the synth template's logical ids, not CDK display paths.");
|
|
2749
|
+
lines.push(" Note: each stack of this deploy validates this WHOLE flag list against its OWN template, so an id declared by a different stack of the same run is reported here as unknown. Name it in a deploy of only that stack.");
|
|
2750
|
+
if (validation.nestedStackLogicalIds.length > 0) lines.push(` Note: resources inside a nested stack are NOT addressable — the flags name logical ids of the stack being deployed, and this template's nested stack(s) (${validation.nestedStackLogicalIds.join(", ")}) carry their own. A logical id that a nested child happens to share with a top-level resource recreates the TOP-LEVEL one only, and the nested stack row itself is refused as a target.`);
|
|
2694
2751
|
}
|
|
2695
2752
|
if (validation.missingFromState.length > 0) {
|
|
2696
2753
|
if (lines.length > 0) lines.push("");
|
|
@@ -2709,6 +2766,16 @@ function renderRecreateTargetsErrors(validation) {
|
|
|
2709
2766
|
lines.push(`${FLAG_UMBRELLA} would destroy + recreate ${validation.blockedStatefulTargets.length} stateful resource(s). Recreate loses ALL data — no automatic data migration. Re-run with --force-stateful-recreation to acknowledge the data-loss footgun.`);
|
|
2710
2767
|
for (const blocked of validation.blockedStatefulTargets) lines.push(` - ${blocked.logicalId} (${blocked.resourceType}) — ${renderStatefulReason(blocked.statefulReason)}`);
|
|
2711
2768
|
}
|
|
2769
|
+
if (validation.blockedNestedStackTargets.length > 0) {
|
|
2770
|
+
if (lines.length > 0) lines.push("");
|
|
2771
|
+
lines.push(`${FLAG_UMBRELLA} refuses to operate on ${validation.blockedNestedStackTargets.length} nested-stack resource(s):`);
|
|
2772
|
+
for (const blocked of validation.blockedNestedStackTargets) {
|
|
2773
|
+
const templateType = validation.nestedStackLogicalIds.includes(blocked.logicalId) ? NESTED_STACK_RESOURCE_TYPE$1 : void 0;
|
|
2774
|
+
const shown = templateType && templateType !== blocked.resourceType ? `${blocked.resourceType} in state, ${templateType} in the template` : blocked.resourceType;
|
|
2775
|
+
lines.push(` - ${blocked.logicalId} (${shown})`);
|
|
2776
|
+
}
|
|
2777
|
+
lines.push(" Recreating one would DELETE the whole child stack — every resource it owns, with no per-resource confirmation — and re-create it through a layer that does not implement cdkd's nested-stack handling. Where cdkd holds no state for that child, the delete is a no-op instead and the old resource is silently left behind. There is no --force-stateful-recreation bypass. A resource inside a child is not addressable by these flags at all.");
|
|
2778
|
+
}
|
|
2712
2779
|
if (validation.blockedMultiRegionTargets.length > 0) {
|
|
2713
2780
|
if (lines.length > 0) lines.push("");
|
|
2714
2781
|
lines.push(`--recreate-via-cc-api / --recreate-via-sdk-provider refuses to operate on ${validation.blockedMultiRegionTargets.length} multi-region resource(s) — out of scope for v1 of these flags (the destroy + recreate cycle across replica regions is more involved than the single-region path):`);
|
|
@@ -3072,7 +3139,7 @@ async function promptRecreateConfirm(input) {
|
|
|
3072
3139
|
else if (toCcCount > 0) logger.warn(`--recreate-via-cc-api will destroy + recreate ${toCcCount} resource(s) via Cloud Control API on stack ${input.stackName}:`);
|
|
3073
3140
|
else logger.warn(`--recreate-via-sdk-provider will destroy + recreate ${toSdkCount} resource(s) via SDK Provider on stack ${input.stackName}:`);
|
|
3074
3141
|
for (const t of input.targets) {
|
|
3075
|
-
const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0) : null;
|
|
3142
|
+
const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0, void 0) : null;
|
|
3076
3143
|
const rederived = rederivedReason !== null;
|
|
3077
3144
|
const reason = t.statefulReason ?? rederivedReason;
|
|
3078
3145
|
const stateful = reason !== null;
|
|
@@ -3105,6 +3172,32 @@ async function promptRecreateConfirm(input) {
|
|
|
3105
3172
|
}
|
|
3106
3173
|
}
|
|
3107
3174
|
|
|
3175
|
+
//#endregion
|
|
3176
|
+
//#region src/cli/commands/pin-cc-api-reachability.ts
|
|
3177
|
+
function analyzePinCcApiReachability(pinnedLogicalIds, stacks) {
|
|
3178
|
+
const idSets = stacks.map((s) => ({
|
|
3179
|
+
stackName: s.stackName,
|
|
3180
|
+
ids: new Set(s.logicalIds)
|
|
3181
|
+
}));
|
|
3182
|
+
const unmatched = [];
|
|
3183
|
+
const partial = [];
|
|
3184
|
+
for (const logicalId of [...new Set(pinnedLogicalIds)]) {
|
|
3185
|
+
const appliesTo = idSets.filter((t) => t.ids.has(logicalId)).map((t) => t.stackName);
|
|
3186
|
+
const absentFrom = idSets.filter((t) => !t.ids.has(logicalId)).map((t) => t.stackName);
|
|
3187
|
+
if (appliesTo.length === 0) unmatched.push(logicalId);
|
|
3188
|
+
else if (absentFrom.length > 0) partial.push({
|
|
3189
|
+
logicalId,
|
|
3190
|
+
appliesTo,
|
|
3191
|
+
absentFrom
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
return {
|
|
3195
|
+
unmatched,
|
|
3196
|
+
partial,
|
|
3197
|
+
errorMessage: unmatched.length === 0 ? "" : `--pin-cc-api named ${unmatched.length} logical id(s) present in no stack of this run:\n` + unmatched.map((id) => ` - ${id}`).join("\n") + `\n Stacks in this run: ${idSets.map((t) => t.stackName).join(", ")}\n Fix: pass the CFn-emitted logical id, not the CDK display path (\`cdkd synth\`, then read \`.Resources | keys\`). A resource inside a NESTED child stack cannot be pinned from its parent's deploy — name it in a deploy of that child.`
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3108
3201
|
//#endregion
|
|
3109
3202
|
//#region src/cli/commands/confirm-prompt.ts
|
|
3110
3203
|
/**
|
|
@@ -15996,209 +16089,6 @@ function isRecord$1(value) {
|
|
|
15996
16089
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15997
16090
|
}
|
|
15998
16091
|
|
|
15999
|
-
//#endregion
|
|
16000
|
-
//#region src/provisioning/dynamodb-warm-throughput.ts
|
|
16001
|
-
/**
|
|
16002
|
-
* The two DynamoDB `WarmThroughput` rules that `AWS::DynamoDB::Table` and
|
|
16003
|
-
* `AWS::DynamoDB::GlobalTable` both need, in ONE spelling.
|
|
16004
|
-
*
|
|
16005
|
-
* `WarmThroughput` is the same CloudFormation block on both types, and both
|
|
16006
|
-
* providers have to answer the same two questions about it before an
|
|
16007
|
-
* `UpdateTable` / `CreateTable` goes out:
|
|
16008
|
-
*
|
|
16009
|
-
* 1. **Is this block SENDABLE, and as what numbers?** CloudFormation is
|
|
16010
|
-
* stringly typed, so `{ReadUnitsPerSecond: '12000'}` — or anything that
|
|
16011
|
-
* came back from an `Fn::Sub` — arrives as a STRING and must not be
|
|
16012
|
-
* forwarded verbatim into a numeric `Long` field.
|
|
16013
|
-
* 2. **Would sending it LOWER what AWS already reports?** Warm throughput
|
|
16014
|
-
* only ever rises with a table's traffic and AWS REJECTS a call that
|
|
16015
|
-
* lowers it (`decreasing WarmThroughput is not supported`, measured live
|
|
16016
|
-
* us-east-1 2026-08-13 for issue #1768).
|
|
16017
|
-
*
|
|
16018
|
-
* **Provenance, stated precisely because it is easy to over-claim.** Exactly
|
|
16019
|
-
* ONE of these rules ever shipped: `AWS::DynamoDB::Table` got them in PR #1808
|
|
16020
|
-
* (issues #1760 / #1768), and the `AWS::DynamoDB::GlobalTable` side never
|
|
16021
|
-
* existed outside the change that created this file (issue #1857). So this is
|
|
16022
|
-
* the Table rule LIFTED — not two shipped rules reconciled, and no deployed
|
|
16023
|
-
* behaviour changed for either type when it moved here.
|
|
16024
|
-
*
|
|
16025
|
-
* What WAS compared is the lifted rule against the GlobalTable spelling
|
|
16026
|
-
* drafted alongside it, over the shapes both had to answer (a quoted numeric
|
|
16027
|
-
* string, a partially usable block, a mixed decrease/increase, an absent live
|
|
16028
|
-
* value, a zero, a negative, a non-numeric string, a whitespace string, an
|
|
16029
|
-
* explicit `undefined` / `null`, a boolean, an empty block, a scalar, an
|
|
16030
|
-
* array). {@link isWarmThroughputDecrease} agreed on every one; the coercion
|
|
16031
|
-
* agreed on all but a whitespace-only string, where a bare `Number(' ')` is
|
|
16032
|
-
* `0` rather than `NaN`. This module keeps the REFUSING answer for that shape,
|
|
16033
|
-
* since `' '` is not a capacity anyone declared and a warm throughput of 0
|
|
16034
|
-
* is not a request AWS can honour. That is a DRAFT reconciled against a
|
|
16035
|
-
* shipped rule, which is worth less than two shipped rules agreeing — the
|
|
16036
|
-
* reason to read the probe list as a design note rather than as field
|
|
16037
|
-
* evidence.
|
|
16038
|
-
*
|
|
16039
|
-
* Living here rather than in either provider is the point. A decrease guard is
|
|
16040
|
-
* three clauses that all FAIL OPEN for different reasons, and two files
|
|
16041
|
-
* spelling it independently is two chances for a later "fix" to change one of
|
|
16042
|
-
* them — at which point the sibling type silently keeps the old answer, and
|
|
16043
|
-
* nothing in the tree says which is intended. Same class as
|
|
16044
|
-
* `emr-configuration.ts`, and the same reason.
|
|
16045
|
-
*
|
|
16046
|
-
* Deliberately NOT here: everything only ONE provider has. The `Table` side's
|
|
16047
|
-
* `isSendableWarmThroughput` / `isRefusedWarmThroughput` /
|
|
16048
|
-
* `declaresWarmThroughput` / `warmThroughputAlreadyMatches` are its drift-side
|
|
16049
|
-
* and already-matches gates, which `GlobalTable` has no counterpart to (issue
|
|
16050
|
-
* #1742 strips the per-index `WarmThroughput` from BOTH of its drift
|
|
16051
|
-
* comparison sides unconditionally, so drift never asks the question there);
|
|
16052
|
-
* the `GlobalTable` side's `warmThroughputDiagnostic` builds a
|
|
16053
|
-
* `ThroughputDiagnostic` that only that provider's collector understands.
|
|
16054
|
-
* Moving a helper with one caller here would buy nothing and cost a hop.
|
|
16055
|
-
*
|
|
16056
|
-
* Issues: #1760 / #1768 (Table, PR #1808), #1857 (GlobalTable).
|
|
16057
|
-
*/
|
|
16058
|
-
/**
|
|
16059
|
-
* The two `WarmThroughput` members, in the ONE order every message, every
|
|
16060
|
-
* comparison and every emitted block uses. A shared order is what makes two
|
|
16061
|
-
* blocks carrying the same numbers compare equal by `deepEqual` and serialize
|
|
16062
|
-
* to the same wire bytes regardless of the order the template wrote them in.
|
|
16063
|
-
*/
|
|
16064
|
-
const WARM_THROUGHPUT_MEMBERS = ["ReadUnitsPerSecond", "WriteUnitsPerSecond"];
|
|
16065
|
-
/**
|
|
16066
|
-
* A CloudFormation-borne numeric property, or `undefined` when the value is
|
|
16067
|
-
* not a usable number.
|
|
16068
|
-
*
|
|
16069
|
-
* Plain `Number()` coercion is NOT good enough and the tree learned it three
|
|
16070
|
-
* separate times: `Number(null)`, `Number('')`, `Number([])`, `Number(false)`
|
|
16071
|
-
* and `Number(' ')` are all **0**, not `NaN` — so a live `0` would compare
|
|
16072
|
-
* EQUAL to a desired `null` / `''` / `[]` / `false`, and a whitespace-only
|
|
16073
|
-
* string would be forwarded as a request for zero units.
|
|
16074
|
-
*
|
|
16075
|
-
* A YAML-borne numeric STRING is still accepted, because that is a real
|
|
16076
|
-
* template shape and `'12000'` genuinely means 12000.
|
|
16077
|
-
*
|
|
16078
|
-
* EXPORTED, and not because warm throughput needs it exported. This exact rule
|
|
16079
|
-
* was hand-written three times — here, as `dynamodb-table-provider.ts`'s
|
|
16080
|
-
* `capacityNumber` (byte-identical), and as `dynamodb-globaltable-provider.ts`'s
|
|
16081
|
-
* `toFiniteNumber` (a different spelling of the same total function) — which is
|
|
16082
|
-
* precisely the divergence this module exists to stop: the next "fix" to one of
|
|
16083
|
-
* them would have left the other two answering differently, with nothing in the
|
|
16084
|
-
* tree saying which was intended. It reads `ReadCapacityUnits` /
|
|
16085
|
-
* `MaxReadRequestUnits` / `MinCapacity` as readily as it reads
|
|
16086
|
-
* `ReadUnitsPerSecond`; there is only ever one right answer for "is this
|
|
16087
|
-
* stringly-typed CFn value a number I can send?".
|
|
16088
|
-
*/
|
|
16089
|
-
function toFiniteNumber(value) {
|
|
16090
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
16091
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
16092
|
-
const n = Number(value);
|
|
16093
|
-
return Number.isFinite(n) ? n : void 0;
|
|
16094
|
-
}
|
|
16095
|
-
}
|
|
16096
|
-
/**
|
|
16097
|
-
* Coerce a CFn `WarmThroughput` block to the numeric shape the SDK's `Long`
|
|
16098
|
-
* fields accept, PER MEMBER.
|
|
16099
|
-
*
|
|
16100
|
-
* PER MEMBER, not whole-block, and that distinction is the point: a block
|
|
16101
|
-
* whose write half is an unresolved intrinsic still has a perfectly good read
|
|
16102
|
-
* half, and dropping both would silently discard a value the template really
|
|
16103
|
-
* did ask for. The dropped member is NAMED (`droppedMembers`) so the caller's
|
|
16104
|
-
* warning can say which half went missing instead of reporting the whole
|
|
16105
|
-
* property.
|
|
16106
|
-
*
|
|
16107
|
-
* A block with NO usable member yields `spec: undefined` — refused rather than
|
|
16108
|
-
* forwarded, because forwarding a malformed block surfaces as an opaque AWS
|
|
16109
|
-
* validation error naming neither cdkd nor the property. `droppedMembers` is
|
|
16110
|
-
* still populated in that case, so a REFUSAL message can name what it refused;
|
|
16111
|
-
* a block that is not an object at all (an unresolved `Fn::If`, a scalar, an
|
|
16112
|
-
* array) has no member to name and reports none, which is what lets a caller
|
|
16113
|
-
* word "one half went missing" differently from "the block is unusable".
|
|
16114
|
-
*
|
|
16115
|
-
* Pure: takes the raw bag, returns numbers, logs nothing. Each caller owns its
|
|
16116
|
-
* own message, so the wording stays consistent across that provider's send
|
|
16117
|
-
* sites without forcing one wording across both providers.
|
|
16118
|
-
*/
|
|
16119
|
-
function coerceWarmThroughput$1(raw) {
|
|
16120
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { droppedMembers: [] };
|
|
16121
|
-
const bag = raw;
|
|
16122
|
-
const spec = {};
|
|
16123
|
-
const droppedMembers = [];
|
|
16124
|
-
for (const member of WARM_THROUGHPUT_MEMBERS) {
|
|
16125
|
-
if (bag[member] === void 0) continue;
|
|
16126
|
-
const coerced = toFiniteNumber(bag[member]);
|
|
16127
|
-
if (coerced === void 0) {
|
|
16128
|
-
droppedMembers.push(member);
|
|
16129
|
-
continue;
|
|
16130
|
-
}
|
|
16131
|
-
spec[member] = coerced;
|
|
16132
|
-
}
|
|
16133
|
-
if (Object.keys(spec).length === 0) return { droppedMembers };
|
|
16134
|
-
return {
|
|
16135
|
-
spec,
|
|
16136
|
-
droppedMembers
|
|
16137
|
-
};
|
|
16138
|
-
}
|
|
16139
|
-
/**
|
|
16140
|
-
* Whether the COERCED desired `WarmThroughput` would LOWER what AWS already
|
|
16141
|
-
* reports.
|
|
16142
|
-
*
|
|
16143
|
-
* Measured live (us-east-1, 2026-08-13, issue #1768) against a table AWS
|
|
16144
|
-
* reports `{ReadUnitsPerSecond: 12000, WriteUnitsPerSecond: 4000}` for:
|
|
16145
|
-
*
|
|
16146
|
-
* ```
|
|
16147
|
-
* UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000, WriteUnitsPerSecond: 2000}
|
|
16148
|
-
* ValidationException: One or more parameter values were invalid: Requested
|
|
16149
|
-
* ReadUnitsPerSecond for WarmThroughput for table is lower than current
|
|
16150
|
-
* WarmThroughput, decreasing WarmThroughput is not supported
|
|
16151
|
-
* UpdateTable WarmThroughput={ReadUnitsPerSecond: 6000} -> same rejection
|
|
16152
|
-
* UpdateTable WarmThroughput={WriteUnitsPerSecond: 2000} -> same rejection, naming WriteUnitsPerSecond
|
|
16153
|
-
* UpdateTable WarmThroughput={12000, 4000} (re-assert) -> ACCEPTED
|
|
16154
|
-
* ```
|
|
16155
|
-
*
|
|
16156
|
-
* So the value is one AWS raises with the table's traffic and never lowers,
|
|
16157
|
-
* and a decrease is REJECTED rather than accepted-and-ignored — which is what
|
|
16158
|
-
* had to be measured before choosing, because the two produce different
|
|
16159
|
-
* correct answers. The caller SKIPS the call on a true here.
|
|
16160
|
-
*
|
|
16161
|
-
* Evaluated on the COERCED spec, never on the raw bag: analysing the raw bag
|
|
16162
|
-
* makes the verdict describe a request that is not the one being sent (a
|
|
16163
|
-
* dropped member is not part of the call and must not be part of the
|
|
16164
|
-
* comparison).
|
|
16165
|
-
*
|
|
16166
|
-
* Semantics, all three chosen to FAIL OPEN — i.e. to let the call through and
|
|
16167
|
-
* leave AWS as the authority — because a false positive here silently drops a
|
|
16168
|
-
* legitimate INCREASE, which is a real capacity change the user asked for,
|
|
16169
|
-
* while a false negative merely reproduces the pre-fix behaviour of an
|
|
16170
|
-
* AWS-side rejection that names the property:
|
|
16171
|
-
* - DECLARED members only. An absent member is not a request to lower
|
|
16172
|
-
* anything, so it takes no part in the verdict.
|
|
16173
|
-
* - MIXED is not a decrease. One member below live and the other above means
|
|
16174
|
-
* the call carries a genuine increase; AWS decides.
|
|
16175
|
-
* - An absent or unusable LIVE counterpart is not a decrease. Without a
|
|
16176
|
-
* number to compare against there is no evidence of one. Only the LIVE side
|
|
16177
|
-
* can reach that arm: the desired side is a COERCED spec, so a malformed
|
|
16178
|
-
* template value has already been dropped by {@link coerceWarmThroughput}.
|
|
16179
|
-
*
|
|
16180
|
-
* A decrease therefore requires every declared member to be at-or-below live
|
|
16181
|
-
* AND at least one to be strictly below.
|
|
16182
|
-
*
|
|
16183
|
-
* The skip this drives is right for EVERY `update()` caller — the deploy
|
|
16184
|
-
* engine, `cdkd drift --revert`, and the rollback executor's two revert arms —
|
|
16185
|
-
* because none of them can make AWS lower the value, so none loses anything a
|
|
16186
|
-
* doomed call would have achieved.
|
|
16187
|
-
*/
|
|
16188
|
-
function isWarmThroughputDecrease(desired, live) {
|
|
16189
|
-
if (desired === void 0 || live === void 0) return false;
|
|
16190
|
-
let sawDecrease = false;
|
|
16191
|
-
for (const member of WARM_THROUGHPUT_MEMBERS) {
|
|
16192
|
-
if (desired[member] === void 0) continue;
|
|
16193
|
-
const wanted = toFiniteNumber(desired[member]);
|
|
16194
|
-
const current = toFiniteNumber(live[member]);
|
|
16195
|
-
if (wanted === void 0 || current === void 0) return false;
|
|
16196
|
-
if (wanted > current) return false;
|
|
16197
|
-
if (wanted < current) sawDecrease = true;
|
|
16198
|
-
}
|
|
16199
|
-
return sawDecrease;
|
|
16200
|
-
}
|
|
16201
|
-
|
|
16202
16092
|
//#endregion
|
|
16203
16093
|
//#region src/provisioning/dynamodb-index-busy-delete.ts
|
|
16204
16094
|
/**
|
|
@@ -19969,9 +19859,27 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
19969
19859
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
19970
19860
|
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
19971
19861
|
debug(`Updating DynamoDB GlobalTable ${logicalId}: ${maskSecrets(physicalId)}`);
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19862
|
+
const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
|
|
19863
|
+
const replaceRemedy = async () => {
|
|
19864
|
+
let guardRegion = "";
|
|
19865
|
+
try {
|
|
19866
|
+
guardRegion = await this.dynamoDBClient.config.region() ?? "";
|
|
19867
|
+
} catch {
|
|
19868
|
+
guardRegion = "";
|
|
19869
|
+
}
|
|
19870
|
+
return extractLocalDeletionProtection(previousProperties, guardRegion) === true ? protectedReplacementAdvice({
|
|
19871
|
+
evidence: "cdkd's recorded properties for this table carry DeletionProtectionEnabled: true for the deploy region",
|
|
19872
|
+
replaceFlags,
|
|
19873
|
+
disable: {
|
|
19874
|
+
before: "aws dynamodb update-table --table-name",
|
|
19875
|
+
identifier: physicalId,
|
|
19876
|
+
after: "--no-deletion-protection-enabled"
|
|
19877
|
+
}
|
|
19878
|
+
}) : `replacement required (deploy with ${replaceFlags}, or destroy + redeploy)`;
|
|
19879
|
+
};
|
|
19880
|
+
if (properties["TableName"] !== void 0 && previousProperties["TableName"] !== void 0 && properties["TableName"] !== previousProperties["TableName"]) throw new ProvisioningError(`TableName is immutable on AWS::DynamoDB::GlobalTable; ${await replaceRemedy()}`, resourceType, logicalId, physicalId);
|
|
19881
|
+
if (properties["KeySchema"] !== void 0 && previousProperties["KeySchema"] !== void 0 && !deepEqual$1(properties["KeySchema"], previousProperties["KeySchema"])) throw new ProvisioningError(`KeySchema is immutable on AWS::DynamoDB::GlobalTable; ${await replaceRemedy()}`, resourceType, logicalId, physicalId);
|
|
19882
|
+
if (properties["LocalSecondaryIndexes"] !== void 0 && previousProperties["LocalSecondaryIndexes"] !== void 0 && !deepEqual$1(properties["LocalSecondaryIndexes"], previousProperties["LocalSecondaryIndexes"])) throw new ProvisioningError(`LocalSecondaryIndexes is immutable on AWS::DynamoDB::GlobalTable; ${await replaceRemedy()}`, resourceType, logicalId, physicalId);
|
|
19975
19883
|
const oldAttrs = previousProperties["AttributeDefinitions"] ?? [];
|
|
19976
19884
|
const newAttrs = properties["AttributeDefinitions"] ?? [];
|
|
19977
19885
|
const desiredKeyAttrNames = collectDesiredKeyAttributeNames(properties);
|
|
@@ -19984,7 +19892,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
19984
19892
|
const describeResp = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
|
|
19985
19893
|
const tableArn = describeResp.Table?.TableArn;
|
|
19986
19894
|
const extractLocalTags = (props) => {
|
|
19987
|
-
return (props
|
|
19895
|
+
return localReplicaEntry(props, currentRegion)?.["Tags"];
|
|
19988
19896
|
};
|
|
19989
19897
|
if (tableArn) await this.applyTagDiff(tableArn, extractLocalTags(previousProperties), extractLocalTags(properties));
|
|
19990
19898
|
const liveWarmByIndexName = /* @__PURE__ */ new Map();
|
|
@@ -21034,7 +20942,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
21034
20942
|
return entry;
|
|
21035
20943
|
}));
|
|
21036
20944
|
result["Replicas"] = replicas;
|
|
21037
|
-
const
|
|
20945
|
+
const localReplica = replicas.find((r) => r["Region"] === currentRegion);
|
|
21038
20946
|
if (table.GlobalSecondaryIndexes && table.GlobalSecondaryIndexes.length > 0) {
|
|
21039
20947
|
const cfnIndexes = [];
|
|
21040
20948
|
const replicaIndexEntries = [];
|
|
@@ -21065,11 +20973,11 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
21065
20973
|
if (Object.keys(replicaEntry).length > 1) replicaIndexEntries.push(replicaEntry);
|
|
21066
20974
|
}
|
|
21067
20975
|
result["GlobalSecondaryIndexes"] = cfnIndexes;
|
|
21068
|
-
if (
|
|
20976
|
+
if (localReplica && replicaIndexEntries.length > 0) localReplica["GlobalSecondaryIndexes"] = replicaIndexEntries;
|
|
21069
20977
|
}
|
|
21070
20978
|
if (table.OnDemandThroughput?.MaxWriteRequestUnits !== void 0) result["WriteOnDemandThroughputSettings"] = { MaxWriteRequestUnits: table.OnDemandThroughput.MaxWriteRequestUnits };
|
|
21071
20979
|
else result["WriteOnDemandThroughputSettings"] = {};
|
|
21072
|
-
if (
|
|
20980
|
+
if (localReplica && table.OnDemandThroughput?.MaxReadRequestUnits !== void 0) localReplica["ReadOnDemandThroughputSettings"] = { MaxReadRequestUnits: table.OnDemandThroughput.MaxReadRequestUnits };
|
|
21073
20981
|
if (billingMode === "PROVISIONED") {
|
|
21074
20982
|
const autoScaling = await this.readAutoScalingSettings(tableNameForSubs, "dynamodb:table:WriteCapacityUnits");
|
|
21075
20983
|
if (autoScaling) result["WriteProvisionedThroughputSettings"] = { WriteCapacityAutoScalingSettings: autoScaling };
|
|
@@ -21638,10 +21546,29 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
21638
21546
|
}
|
|
21639
21547
|
};
|
|
21640
21548
|
/**
|
|
21641
|
-
*
|
|
21642
|
-
*
|
|
21643
|
-
*
|
|
21549
|
+
* The `Replicas[]` entry for `region`, or `undefined`.
|
|
21550
|
+
*
|
|
21551
|
+
* ONE predicate, because two readers of the same field had drifted: issue
|
|
21552
|
+
* [#2610]'s review round guarded `extractLocalDeletionProtection` against a
|
|
21553
|
+
* non-array `Replicas` and left `update()`'s inline `extractLocalTags` on the
|
|
21554
|
+
* old `(props['Replicas'] ?? []) as Array<...>` -- which is the one that
|
|
21555
|
+
* actually throws FIRST on the apply path, ~400 lines earlier in the same
|
|
21556
|
+
* method, so guarding only the other produced no visible change and no test
|
|
21557
|
+
* went red. A shared predicate is what stops the next reader inheriting the
|
|
21558
|
+
* unguarded spelling; `derivePerCallProvisionedThroughput` documents the same
|
|
21559
|
+
* hazard for the same field and guards independently.
|
|
21560
|
+
*
|
|
21561
|
+
* `Array.isArray`, not `?? []`: a non-array `Replicas` -- an unresolved
|
|
21562
|
+
* intrinsic, or a hand-edited state record -- makes `.find` throw a bare
|
|
21563
|
+
* `TypeError`, and since [#2610] one caller sits on the REFUSAL path, where a
|
|
21564
|
+
* throw REPLACES three informative `ProvisioningError`s with a stack trace.
|
|
21565
|
+
* The `r?.` guards a null ENTRY inside an otherwise-valid array.
|
|
21644
21566
|
*/
|
|
21567
|
+
function localReplicaEntry(props, region) {
|
|
21568
|
+
const raw = props["Replicas"];
|
|
21569
|
+
if (!Array.isArray(raw)) return void 0;
|
|
21570
|
+
return raw.find((r) => r?.["Region"] === region);
|
|
21571
|
+
}
|
|
21645
21572
|
/**
|
|
21646
21573
|
* Extract the local replica's `DeletionProtectionEnabled` from a CFn
|
|
21647
21574
|
* properties shape. The `AWS::DynamoDB::GlobalTable` CFn schema places
|
|
@@ -21657,7 +21584,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
21657
21584
|
* the SDK call.
|
|
21658
21585
|
*/
|
|
21659
21586
|
function extractLocalDeletionProtection(props, region) {
|
|
21660
|
-
const perReplica = (props
|
|
21587
|
+
const perReplica = localReplicaEntry(props, region)?.["DeletionProtectionEnabled"];
|
|
21661
21588
|
if (typeof perReplica === "boolean") return perReplica;
|
|
21662
21589
|
const topLevel = props["DeletionProtectionEnabled"];
|
|
21663
21590
|
return typeof topLevel === "boolean" ? topLevel : void 0;
|
|
@@ -22916,6 +22843,11 @@ function collectTableOnDemandCeilings(properties, region, diagnostics, maskSecre
|
|
|
22916
22843
|
}
|
|
22917
22844
|
};
|
|
22918
22845
|
}
|
|
22846
|
+
/**
|
|
22847
|
+
* Diff CFn `Replicas[]` arrays. Keyed by `Region`. Returns adds, removes,
|
|
22848
|
+
* and modifies (entries whose other keys — KMSMasterKeyId,
|
|
22849
|
+
* GlobalSecondaryIndexes, TableClassOverride — differ from the old shape).
|
|
22850
|
+
*/
|
|
22919
22851
|
function diffReplicas(oldReplicas, newReplicas) {
|
|
22920
22852
|
const oldByRegion = /* @__PURE__ */ new Map();
|
|
22921
22853
|
const newByRegion = /* @__PURE__ */ new Map();
|
|
@@ -22984,6 +22916,75 @@ function deepEqual$1(a, b) {
|
|
|
22984
22916
|
//#endregion
|
|
22985
22917
|
//#region src/provisioning/providers/logs-loggroup-provider.ts
|
|
22986
22918
|
/**
|
|
22919
|
+
* Refuse a `RetentionInDays` the send path cannot USE (issue [#2521]).
|
|
22920
|
+
*
|
|
22921
|
+
* **The scope of this refusal is a TEST, not a paragraph.** Every review round
|
|
22922
|
+
* on issue [#2521] found a hole in a PROSE statement of it — a coercible
|
|
22923
|
+
* negative that fell through to `DeleteRetentionPolicy`, an unusable value
|
|
22924
|
+
* silenced by the wrong change gate, wrong cells in the hand-written table
|
|
22925
|
+
* that replaced those sentences, and then the one non-finite number
|
|
22926
|
+
* JavaScript makes FALSY. No count is given here: the count itself drifted
|
|
22927
|
+
* between this comment and the changelog, which is the same failure one level
|
|
22928
|
+
* up. The table now lives in
|
|
22929
|
+
* `tests/unit/provisioning/logs-loggroup-provider-retention-coercion.test.ts`
|
|
22930
|
+
* as `CREATE_MATRIX` / `UPDATE_MATRIX`: one row per (value, previous) pair,
|
|
22931
|
+
* each asserting which AWS call goes out, each carrying what the pre-#2521
|
|
22932
|
+
* code did. Read a change against those rows and add one for any pair they do
|
|
22933
|
+
* not cover; do not restate the rule here, because a fourth restatement is
|
|
22934
|
+
* the thing that keeps being wrong.
|
|
22935
|
+
*
|
|
22936
|
+
* THREE refusal CASES across TWO `throw` statements — the non-finite guard
|
|
22937
|
+
* clause at the top of the body has its own, and the other two share the
|
|
22938
|
+
* second, differing only in the `detail` clause. Only the WHY of each:
|
|
22939
|
+
*
|
|
22940
|
+
* - **a non-finite NUMBER** (`NaN`, `Infinity`, `-Infinity`). It sits ahead
|
|
22941
|
+
* of the falsy return because `NaN` is FALSY: without it that value exits
|
|
22942
|
+
* early, fails the `> 0` send test and reaches `DeleteRetentionPolicy`,
|
|
22943
|
+
* while its two siblings — truthy — were already refused. Leaving one
|
|
22944
|
+
* member of a family out is the inconsistency the negative arm below
|
|
22945
|
+
* introduced, so the arm names the family.
|
|
22946
|
+
* - **not a number at all** (`' '`, `'abc'`, `[]`, `{}`, `true`). The
|
|
22947
|
+
* pre-coercion code tested `if (properties['RetentionInDays'])` and handed
|
|
22948
|
+
* the raw value to `PutRetentionPolicyCommand`, so CloudWatch Logs rejected
|
|
22949
|
+
* it. Refusing moves that rejection one layer earlier and names the
|
|
22950
|
+
* property.
|
|
22951
|
+
* - **a NEGATIVE number** (`-1`, `'-1'`). It coerces, so a refusal gated on
|
|
22952
|
+
* coercibility alone let it through; it then failed the `> 0` send test and
|
|
22953
|
+
* fell into the UPDATE path's `else`, which issues
|
|
22954
|
+
* `DeleteRetentionPolicy` — a template typo SILENTLY REMOVING a live
|
|
22955
|
+
* retention where the old code sent `-1` and failed loudly.
|
|
22956
|
+
*
|
|
22957
|
+
* The FALSY values pass through, as they did before — `0`, `''`, `false`,
|
|
22958
|
+
* `null` and absent; `NaN` is the one exception, refused above because it is a
|
|
22959
|
+
* malformed number rather than a spelling of "no retention".
|
|
22960
|
+
*
|
|
22961
|
+
* `'0'` is TRUTHY, so it reaches this function and passes too: zero is
|
|
22962
|
+
* what {@link LogsLogGroupProvider.readCurrentState} records for a group with
|
|
22963
|
+
* no retention policy, and CloudFormation coerces `'0'` to `0`, so routing the
|
|
22964
|
+
* string to the same arm as the number is the consistency the coercion is for.
|
|
22965
|
+
* On the UPDATE path against a live positive retention that is a real change —
|
|
22966
|
+
* it DELETES where the old code sent and was rejected — and `UPDATE_MATRIX`
|
|
22967
|
+
* pins it.
|
|
22968
|
+
*
|
|
22969
|
+
* The message names the value's TYPE and never the value. A provider's
|
|
22970
|
+
* `properties` bag arrives with dynamic references already RESOLVED, so a
|
|
22971
|
+
* `{{resolve:secretsmanager:...}}` scalar is PLAINTEXT by the time it reaches
|
|
22972
|
+
* here, and this function has no masker to thread — `create()` takes no
|
|
22973
|
+
* `CreateContext` and `applyUpdate` no `UpdateContext`
|
|
22974
|
+
* (`.claude/rules/provider-masking.md`). Interpolating it as
|
|
22975
|
+
* `${JSON.stringify(raw)}` was the first draft and
|
|
22976
|
+
* `vp run audit:provider-secret-mask:check` refused it — but read that as one
|
|
22977
|
+
* SPELLING being fenced, not the rule: the critic's population is
|
|
22978
|
+
* interpolated `JSON.stringify` calls, so a bare `${raw}` here would leave it
|
|
22979
|
+
* green. Do not put the value back without threading the capability first.
|
|
22980
|
+
*/
|
|
22981
|
+
function assertUsableRetention(raw, coerced, logicalId, resourceType) {
|
|
22982
|
+
if (typeof raw === "number" && !Number.isFinite(raw)) throw new ProvisioningError(`${logicalId} (${resourceType}): RetentionInDays must be a non-negative number or a numeric string (CloudFormation coerces '30' to 30); the template declares a non-finite number`, resourceType, logicalId);
|
|
22983
|
+
if (!raw) return;
|
|
22984
|
+
if (coerced !== void 0 && coerced >= 0) return;
|
|
22985
|
+
throw new ProvisioningError(`${logicalId} (${resourceType}): RetentionInDays must be a non-negative number or a numeric string (CloudFormation coerces '30' to 30); ${coerced === void 0 ? `the template's value is of type ${Array.isArray(raw) ? "array" : typeof raw} and does not parse as a finite number` : "the template declares a negative number"}`, resourceType, logicalId);
|
|
22986
|
+
}
|
|
22987
|
+
/**
|
|
22987
22988
|
* AWS CloudWatch Logs LogGroup Provider
|
|
22988
22989
|
*
|
|
22989
22990
|
* Implements resource provisioning for AWS::Logs::LogGroup using the CloudWatch Logs SDK.
|
|
@@ -23064,8 +23065,10 @@ var LogsLogGroupProvider = class {
|
|
|
23064
23065
|
else throw createError;
|
|
23065
23066
|
}
|
|
23066
23067
|
try {
|
|
23067
|
-
const
|
|
23068
|
-
|
|
23068
|
+
const rawRetention = properties["RetentionInDays"];
|
|
23069
|
+
const retentionInDays = toFiniteNumber(rawRetention);
|
|
23070
|
+
assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
|
|
23071
|
+
if (retentionInDays !== void 0 && retentionInDays > 0) await this.logsClient.send(new PutRetentionPolicyCommand({
|
|
23069
23072
|
logGroupName,
|
|
23070
23073
|
retentionInDays
|
|
23071
23074
|
}));
|
|
@@ -23135,14 +23138,14 @@ var LogsLogGroupProvider = class {
|
|
|
23135
23138
|
*/
|
|
23136
23139
|
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
23137
23140
|
try {
|
|
23138
|
-
return await this.applyUpdate(logicalId, physicalId, properties, previousProperties);
|
|
23141
|
+
return await this.applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties);
|
|
23139
23142
|
} catch (error) {
|
|
23140
23143
|
if (error instanceof CdkdError) throw error;
|
|
23141
23144
|
const cause = error instanceof Error ? error : void 0;
|
|
23142
23145
|
throw new ProvisioningError(`Failed to update log group ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
23143
23146
|
}
|
|
23144
23147
|
}
|
|
23145
|
-
async applyUpdate(logicalId, physicalId, properties, previousProperties) {
|
|
23148
|
+
async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
23146
23149
|
this.logger.debug(`Updating log group ${logicalId}: ${physicalId}`);
|
|
23147
23150
|
const normalizeClass = (v) => typeof v === "string" && v.length > 0 ? v : "STANDARD";
|
|
23148
23151
|
const prevClass = normalizeClass(previousProperties["LogGroupClass"]);
|
|
@@ -23151,6 +23154,13 @@ var LogsLogGroupProvider = class {
|
|
|
23151
23154
|
const replaceFlags = "--replace --force-stateful-recreation";
|
|
23152
23155
|
throw new ResourceUpdateNotSupportedError("AWS::Logs::LogGroup", logicalId, `the LogGroupClass ('${prevClass}' -> '${nextClass}') cannot be changed after creation. ${isTruthyCfnBoolean(previousProperties["DeletionProtectionEnabled"]) ? `cdkd's recorded properties for this log group carry DeletionProtectionEnabled, so ${replaceFlags} alone will NOT succeed while AWS still has it on: the replacement normally deletes the log group, AWS refuses that delete while protection is on, and cdkd deploy has no --remove-protection flag to clear it (only cdkd destroy and cdkd state destroy act on one). Read "Deletion protection blocks a replacement" in docs/cli-deploy-safety.md BEFORE you disable anything: whether disabling helps at all, and what the flag ends up as, depend on your UpdateReplacePolicy and on whether the deploy completes — neither of which this refusal can see. Then disable deletion protection — \`aws logs put-log-group-deletion-protection --log-group-identifier '${physicalId}' --no-deletion-protection-enabled\`, or via the console — and re-deploy with ${replaceFlags} to delete + recreate the log group under the new class (its stored log events are lost). Setting DeletionProtectionEnabled: false in the template does NOT clear it in the same deploy: this refusal fires before that property is applied, so that route needs its own deploy with the LogGroupClass change reverted. Or revert the LogGroupClass change and keep the current class.` : `Re-deploy with ${replaceFlags} to delete + recreate the log group under the new class (its stored log events are lost), or revert the LogGroupClass change.`} Note --force-stateful-recreation has NO per-resource granularity: it clears the data guard for every replacement target in the run`);
|
|
23153
23156
|
}
|
|
23157
|
+
const rawRetention = properties["RetentionInDays"];
|
|
23158
|
+
const rawPreviousRetention = previousProperties["RetentionInDays"];
|
|
23159
|
+
const renderedRetention = JSON.stringify(rawRetention);
|
|
23160
|
+
const sameRawRetention = Object.is(rawRetention, rawPreviousRetention) || renderedRetention !== void 0 && renderedRetention !== "null" && renderedRetention === JSON.stringify(rawPreviousRetention);
|
|
23161
|
+
const retentionInDays = toFiniteNumber(rawRetention);
|
|
23162
|
+
const oldRetentionInDays = toFiniteNumber(rawPreviousRetention);
|
|
23163
|
+
if (!sameRawRetention) assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
|
|
23154
23164
|
const newKmsKeyId = properties["KmsKeyId"];
|
|
23155
23165
|
if (newKmsKeyId !== previousProperties["KmsKeyId"]) {
|
|
23156
23166
|
if (newKmsKeyId) await this.logsClient.send(new AssociateKmsKeyCommand({
|
|
@@ -23160,8 +23170,7 @@ var LogsLogGroupProvider = class {
|
|
|
23160
23170
|
else await this.logsClient.send(new DisassociateKmsKeyCommand({ logGroupName: physicalId }));
|
|
23161
23171
|
this.logger.debug(`Updated KMS key association for log group ${physicalId}`);
|
|
23162
23172
|
}
|
|
23163
|
-
|
|
23164
|
-
if (retentionInDays !== previousProperties["RetentionInDays"]) if (retentionInDays) await this.logsClient.send(new PutRetentionPolicyCommand({
|
|
23173
|
+
if (retentionInDays !== oldRetentionInDays || !sameRawRetention && Boolean(rawPreviousRetention) && oldRetentionInDays === void 0) if (retentionInDays !== void 0 && retentionInDays > 0) await this.logsClient.send(new PutRetentionPolicyCommand({
|
|
23165
23174
|
logGroupName: physicalId,
|
|
23166
23175
|
retentionInDays
|
|
23167
23176
|
}));
|
|
@@ -37258,7 +37267,15 @@ var ELBv2Provider = class {
|
|
|
37258
37267
|
for (const [k, v] of Object.entries(p)) if (!handledKeys.has(k)) out[k] = v;
|
|
37259
37268
|
return out;
|
|
37260
37269
|
};
|
|
37261
|
-
if (JSON.stringify(stripHandled(properties)) !== JSON.stringify(stripHandled(previousProperties))) throw new ResourceUpdateNotSupportedError("AWS::ElasticLoadBalancingV2::LoadBalancer", logicalId, "ELBv2 LoadBalancer Name / Type / Scheme are immutable on AWS — none of the ELBv2 Modify* / Set* APIs accept these fields; they are fixed at creation. cdkd handles LoadBalancerAttributes / Subnets / SubnetMappings / SecurityGroups / IpAddressType / Tags in-place
|
|
37270
|
+
if (JSON.stringify(stripHandled(properties)) !== JSON.stringify(stripHandled(previousProperties))) throw new ResourceUpdateNotSupportedError("AWS::ElasticLoadBalancingV2::LoadBalancer", logicalId, "ELBv2 LoadBalancer Name / Type / Scheme are immutable on AWS — none of the ELBv2 Modify* / Set* APIs accept these fields; they are fixed at creation. cdkd handles LoadBalancerAttributes / Subnets / SubnetMappings / SecurityGroups / IpAddressType / Tags in-place. " + (this.normalizeAttributes(previousProperties["LoadBalancerAttributes"]).some((a) => a.Key === "deletion_protection.enabled" && isTruthyCfnBoolean(a.Value)) ? protectedReplacementAdvice({
|
|
37271
|
+
evidence: "cdkd's recorded properties for this load balancer carry LoadBalancerAttributes deletion_protection.enabled=true",
|
|
37272
|
+
replaceFlags: "cdkd deploy --replace",
|
|
37273
|
+
disable: {
|
|
37274
|
+
before: "aws elbv2 modify-load-balancer-attributes --load-balancer-arn",
|
|
37275
|
+
identifier: physicalId,
|
|
37276
|
+
after: "--attributes Key=deletion_protection.enabled,Value=false"
|
|
37277
|
+
}
|
|
37278
|
+
}) : "For Name / Type / Scheme re-deploy with cdkd deploy --replace, or destroy + redeploy the stack."));
|
|
37262
37279
|
const submittedAttrs = this.diffAttributes(this.normalizeAttributes(properties["LoadBalancerAttributes"]), this.normalizeAttributes(previousProperties["LoadBalancerAttributes"]), this.attributeRemovalResolver(LOAD_BALANCER_ATTRIBUTE_DEFAULTS));
|
|
37263
37280
|
if (submittedAttrs.length > 0) {
|
|
37264
37281
|
await this.getClient().send(new ModifyLoadBalancerAttributesCommand({
|
|
@@ -43897,7 +43914,7 @@ var CognitoUserPoolProvider = class {
|
|
|
43897
43914
|
if (properties["SmsVerificationMessage"] !== void 0) updateParams.SmsVerificationMessage = properties["SmsVerificationMessage"];
|
|
43898
43915
|
if (properties["UserPoolTier"]) updateParams.UserPoolTier = properties["UserPoolTier"];
|
|
43899
43916
|
await this.getClient().send(new UpdateUserPoolCommand(updateParams));
|
|
43900
|
-
await this.reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, properties["Schema"], previousProperties["Schema"]);
|
|
43917
|
+
await this.reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, properties["Schema"], previousProperties["Schema"], (properties["DeletionProtection"] ? properties["DeletionProtection"] : previousProperties["DeletionProtection"]) === "ACTIVE");
|
|
43901
43918
|
await this.applyMfaConfig(physicalId, properties, mfaConfiguration, {
|
|
43902
43919
|
...liveMfa?.value !== void 0 ? { liveMfaConfiguration: liveMfa.value } : {},
|
|
43903
43920
|
...liveMfa?.failed === true ? { liveReadFailed: true } : {},
|
|
@@ -43939,7 +43956,7 @@ var CognitoUserPoolProvider = class {
|
|
|
43939
43956
|
* attribute, requires replacement -> ResourceUpdateNotSupportedError.
|
|
43940
43957
|
* A byte-identical Schema is a no-op (no AddCustomAttributes call).
|
|
43941
43958
|
*/
|
|
43942
|
-
async reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, newSchema, oldSchema) {
|
|
43959
|
+
async reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, newSchema, oldSchema, deletionProtected = false) {
|
|
43943
43960
|
const newAttrs = newSchema ?? [];
|
|
43944
43961
|
const oldAttrs = oldSchema ?? [];
|
|
43945
43962
|
for (const attr of newAttrs) if (attr.Name === void 0) throw new ProvisioningError(`Cognito User Pool ${logicalId} has a Schema attribute with no Name — every Schema entry must have a Name`, resourceType, logicalId, physicalId);
|
|
@@ -43964,7 +43981,20 @@ var CognitoUserPoolProvider = class {
|
|
|
43964
43981
|
...modified.map((n) => `modified attribute '${n}'`),
|
|
43965
43982
|
...addedStandard.map((n) => `added standard attribute '${n}'`)
|
|
43966
43983
|
];
|
|
43967
|
-
if (immutableChanges.length > 0)
|
|
43984
|
+
if (immutableChanges.length > 0) {
|
|
43985
|
+
const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
|
|
43986
|
+
const remedy = deletionProtected ? protectedReplacementAdvice({
|
|
43987
|
+
evidence: "cdkd's properties for this user pool carry DeletionProtection: ACTIVE",
|
|
43988
|
+
replaceFlags,
|
|
43989
|
+
disable: {
|
|
43990
|
+
before: "aws cognito-idp update-user-pool --user-pool-id",
|
|
43991
|
+
identifier: physicalId,
|
|
43992
|
+
after: "--deletion-protection INACTIVE",
|
|
43993
|
+
caveat: "Note UpdateUserPool's behaviour on an omitted member differs per field, and is unmeasured for DeletionProtection, so send your complete pool configuration alongside that flag rather than the flag alone."
|
|
43994
|
+
}
|
|
43995
|
+
}) : `AWS::Cognito::UserPool is a stateful type, so re-run with ${replaceFlags} to recreate it (this deletes all users in the pool).`;
|
|
43996
|
+
throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `the Schema change (${immutableChanges.join("; ")}) is immutable on AWS — AWS can only ADD custom attributes in place, so removing or modifying an attribute requires recreating the pool. ${remedy}`);
|
|
43997
|
+
}
|
|
43968
43998
|
const addedCustom = added.filter((a) => a.Name !== void 0 && !STANDARD_USER_POOL_ATTRIBUTES.has(a.Name));
|
|
43969
43999
|
if (addedCustom.length === 0) return;
|
|
43970
44000
|
this.logger.debug(`Adding ${addedCustom.length} custom attribute(s) to ${physicalId}: ` + addedCustom.map((a) => a.Name).join(", "));
|
|
@@ -52048,7 +52078,7 @@ var SchedulerScheduleProvider = class {
|
|
|
52048
52078
|
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
52049
52079
|
const groupName = this.groupNameOf(properties);
|
|
52050
52080
|
const previousGroupName = this.groupNameOf(previousProperties);
|
|
52051
|
-
if (groupName !== previousGroupName) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `GroupName addresses the schedule (${previousGroupName ?? "default"} -> ${groupName ?? "default"}); re-run with \`cdkd deploy --replace
|
|
52081
|
+
if (groupName !== previousGroupName) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `GroupName addresses the schedule (${previousGroupName ?? "default"} -> ${groupName ?? "default"}); re-run with \`cdkd deploy --replace\` to recreate it in the new group (--replace is a boolean flag and takes no resource id; it applies to every resource in the run whose in-place update is refused)`);
|
|
52052
52082
|
this.logger.debug(`Updating Schedule ${logicalId}: ${physicalId}${groupName ? ` (group: ${groupName})` : ""}`);
|
|
52053
52083
|
try {
|
|
52054
52084
|
const input = {
|
|
@@ -52070,7 +52100,11 @@ var SchedulerScheduleProvider = class {
|
|
|
52070
52100
|
}
|
|
52071
52101
|
async delete(logicalId, physicalId, resourceType, properties, context) {
|
|
52072
52102
|
const groupName = this.groupNameOf(properties);
|
|
52073
|
-
if (properties === void 0)
|
|
52103
|
+
if (properties === void 0) {
|
|
52104
|
+
const safeId = displaySafe(physicalId, { asciiOnly: true });
|
|
52105
|
+
const manualHint = safeId && safeId === physicalId ? `If the schedule lives in a custom group, delete it manually: aws scheduler delete-schedule --name ${shellQuote(safeId)} --group-name <group>` : "If the schedule lives in a custom group, delete it manually via the console: the name recorded for it cannot be reproduced safely on a command line.";
|
|
52106
|
+
this.logger.warn(`State record for Schedule ${logicalId} carries no properties — deleting '${safeId || "<unrenderable>"}' from the default group. ${manualHint}`);
|
|
52107
|
+
}
|
|
52074
52108
|
this.logger.debug(`Deleting Schedule ${logicalId}: ${physicalId}${groupName ? ` (group: ${groupName})` : ""}`);
|
|
52075
52109
|
try {
|
|
52076
52110
|
await this.getClient().send(new DeleteScheduleCommand({
|
|
@@ -52254,7 +52288,7 @@ var EFSProvider = class {
|
|
|
52254
52288
|
]) {
|
|
52255
52289
|
const next = properties[key];
|
|
52256
52290
|
const prev = previousProperties[key];
|
|
52257
|
-
if (next !== void 0 && prev !== void 0 && JSON.stringify(next) !== JSON.stringify(prev)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EFS FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
|
|
52291
|
+
if (next !== void 0 && prev !== void 0 && JSON.stringify(next) !== JSON.stringify(prev)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EFS FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace --force-stateful-recreation (the type is in cdkd's stateful-recreate guard set, so a bare --replace is refused a second time with STATEFUL_REPLACE_BLOCKED), or destroy + redeploy the stack.`);
|
|
52258
52292
|
}
|
|
52259
52293
|
const newThroughputMode = properties["ThroughputMode"];
|
|
52260
52294
|
const newProvisioned = properties["ProvisionedThroughputInMibps"];
|
|
@@ -53375,7 +53409,7 @@ var FSxFileSystemProvider = class {
|
|
|
53375
53409
|
for (const key of TOP_LEVEL_IMMUTABLE_PROPS) {
|
|
53376
53410
|
const next = properties[key];
|
|
53377
53411
|
const prev = previousProperties[key];
|
|
53378
|
-
if (JSON.stringify(next ?? null) !== JSON.stringify(prev ?? null)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
|
|
53412
|
+
if (JSON.stringify(next ?? null) !== JSON.stringify(prev ?? null)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${key} is immutable on AWS — UpdateFileSystem does not accept ${key}; the property is fixed at creation. Re-deploy with cdkd deploy --replace --force-stateful-recreation (the type is in cdkd's stateful-recreate guard set, so a bare --replace is refused a second time with STATEFUL_REPLACE_BLOCKED), or destroy + redeploy the stack.`);
|
|
53379
53413
|
}
|
|
53380
53414
|
const variantConfigKey = this.detectVariantConfigKey(properties, previousProperties);
|
|
53381
53415
|
const { diff: variantDiff, hasMutableDiff: variantHasMutableDiff } = variantConfigKey ? this.computeVariantConfigDiff(resourceType, logicalId, variantConfigKey, properties[variantConfigKey] ?? {}, previousProperties[variantConfigKey] ?? {}) : {
|
|
@@ -53456,7 +53490,7 @@ var FSxFileSystemProvider = class {
|
|
|
53456
53490
|
const nextVal = next[key];
|
|
53457
53491
|
const prevVal = prev[key];
|
|
53458
53492
|
if (JSON.stringify(nextVal) === JSON.stringify(prevVal)) continue;
|
|
53459
|
-
if (!mutable.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${configKey}.${key} is immutable on AWS — UpdateFileSystem cannot change it after creation. Re-deploy with cdkd deploy --replace, or destroy + redeploy the stack.`);
|
|
53493
|
+
if (!mutable.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS FSx FileSystem ${configKey}.${key} is immutable on AWS — UpdateFileSystem cannot change it after creation. Re-deploy with cdkd deploy --replace --force-stateful-recreation (the type is in cdkd's stateful-recreate guard set, so a bare --replace is refused a second time with STATEFUL_REPLACE_BLOCKED), or destroy + redeploy the stack.`);
|
|
53460
53494
|
const ctx = {
|
|
53461
53495
|
resourceType,
|
|
53462
53496
|
logicalId,
|
|
@@ -54417,6 +54451,16 @@ var EMRClusterProvider = class {
|
|
|
54417
54451
|
const changed = (key) => !jsonEqual$2(properties[key], previousProperties[key]);
|
|
54418
54452
|
const nextInstances = properties["Instances"] ?? {};
|
|
54419
54453
|
const prevInstances = previousProperties["Instances"] ?? {};
|
|
54454
|
+
const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
|
|
54455
|
+
const replaceRemedy = () => toBoolean(prevInstances["TerminationProtected"]) === true ? protectedReplacementAdvice({
|
|
54456
|
+
evidence: "cdkd's recorded properties for this cluster carry Instances.TerminationProtected: true",
|
|
54457
|
+
replaceFlags,
|
|
54458
|
+
disable: {
|
|
54459
|
+
before: "aws emr modify-cluster-attributes --cluster-id",
|
|
54460
|
+
identifier: physicalId,
|
|
54461
|
+
after: "--no-termination-protected"
|
|
54462
|
+
}
|
|
54463
|
+
}) : `Re-deploy with ${replaceFlags}, or destroy + redeploy the stack.`;
|
|
54420
54464
|
let terminationProtectedChanged = false;
|
|
54421
54465
|
if (changed("Instances")) {
|
|
54422
54466
|
const instanceKeys = /* @__PURE__ */ new Set([...Object.keys(nextInstances), ...Object.keys(prevInstances)]);
|
|
@@ -54426,7 +54470,7 @@ var EMRClusterProvider = class {
|
|
|
54426
54470
|
terminationProtectedChanged = true;
|
|
54427
54471
|
continue;
|
|
54428
54472
|
}
|
|
54429
|
-
throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster Instances.${key} is immutable on AWS — a running cluster's instance topology / networking cannot be changed in place.
|
|
54473
|
+
throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster Instances.${key} is immutable on AWS — a running cluster's instance topology / networking cannot be changed in place. ${replaceRemedy()}`);
|
|
54430
54474
|
}
|
|
54431
54475
|
}
|
|
54432
54476
|
for (const key of Object.keys({
|
|
@@ -54435,7 +54479,7 @@ var EMRClusterProvider = class {
|
|
|
54435
54479
|
})) {
|
|
54436
54480
|
if (key === "Instances") continue;
|
|
54437
54481
|
if (!changed(key)) continue;
|
|
54438
|
-
if (!MUTABLE_TOP_LEVEL_PROPS.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster ${key} is immutable on AWS — it is fixed at cluster creation.
|
|
54482
|
+
if (!MUTABLE_TOP_LEVEL_PROPS.has(key)) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `AWS EMR Cluster ${key} is immutable on AWS — it is fixed at cluster creation. ${replaceRemedy()}`);
|
|
54439
54483
|
}
|
|
54440
54484
|
const visibleChanged = changed("VisibleToAllUsers");
|
|
54441
54485
|
const stepConcurrencyChanged = changed("StepConcurrencyLevel");
|
|
@@ -58447,7 +58491,7 @@ var DLMLifecyclePolicyProvider = class {
|
|
|
58447
58491
|
}
|
|
58448
58492
|
}
|
|
58449
58493
|
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
58450
|
-
if (JSON.stringify(properties["DefaultPolicy"]) !== JSON.stringify(previousProperties["DefaultPolicy"])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId,
|
|
58494
|
+
if (JSON.stringify(properties["DefaultPolicy"]) !== JSON.stringify(previousProperties["DefaultPolicy"])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, "DefaultPolicy cannot be changed in place; re-run with `cdkd deploy --replace` to recreate the policy (--replace is a boolean flag and takes no resource id; it applies to every resource in the run whose in-place update is refused)");
|
|
58451
58495
|
this.logger.debug(`Updating DLM Lifecycle Policy ${logicalId} (${physicalId})`);
|
|
58452
58496
|
try {
|
|
58453
58497
|
await this.getClient().send(new UpdateLifecyclePolicyCommand({
|
|
@@ -58647,7 +58691,7 @@ var S3VectorsProvider = class {
|
|
|
58647
58691
|
* is surfaced as `ResourceUpdateNotSupportedError`.
|
|
58648
58692
|
*/
|
|
58649
58693
|
async updateVectorBucket(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
58650
|
-
for (const createOnly of ["VectorBucketName", "EncryptionConfiguration"]) if (JSON.stringify(properties[createOnly]) !== JSON.stringify(previousProperties[createOnly])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `'${createOnly}' is immutable (create-only) on VectorBucket '${physicalId}'; a change requires replacement (cdkd deploy --replace)`);
|
|
58694
|
+
for (const createOnly of ["VectorBucketName", "EncryptionConfiguration"]) if (JSON.stringify(properties[createOnly]) !== JSON.stringify(previousProperties[createOnly])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId, `'${createOnly}' is immutable (create-only) on VectorBucket '${physicalId}'; a change requires replacement (cdkd deploy --replace --force-stateful-recreation — the type is in cdkd's stateful-recreate guard set, so a bare --replace is refused a second time with STATEFUL_REPLACE_BLOCKED)`);
|
|
58651
58695
|
const oldTags = this.cfnTagsToRecord(previousProperties["Tags"]);
|
|
58652
58696
|
const newTags = this.cfnTagsToRecord(properties["Tags"]);
|
|
58653
58697
|
const toSet = {};
|
|
@@ -62686,8 +62730,7 @@ async function deployCommand(stacks, options) {
|
|
|
62686
62730
|
skipPrefix,
|
|
62687
62731
|
yes: options.yes
|
|
62688
62732
|
});
|
|
62689
|
-
let
|
|
62690
|
-
let recreateViaSdkProviderTargets;
|
|
62733
|
+
let recreateTargets;
|
|
62691
62734
|
if (options.recreateViaCcApi?.length || options.recreateViaSdkProvider?.length) {
|
|
62692
62735
|
const stateForRecreateCheck = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
|
|
62693
62736
|
const validation = await probeAndRevalidateStateful({
|
|
@@ -62716,9 +62759,12 @@ async function deployCommand(stacks, options) {
|
|
|
62716
62759
|
});
|
|
62717
62760
|
const errorBlock = renderRecreateTargetsErrors(validation);
|
|
62718
62761
|
if (errorBlock) throw new CdkdError(errorBlock, "RECREATE_TARGETS_INVALID");
|
|
62719
|
-
|
|
62720
|
-
|
|
62721
|
-
|
|
62762
|
+
recreateTargets = {
|
|
62763
|
+
stackName: stackInfo.stackName,
|
|
62764
|
+
viaCcApi: new Set(validation.targets.filter((t) => t.direction === "to-cc-api").map((t) => t.logicalId)),
|
|
62765
|
+
viaSdkProvider: new Set(validation.targets.filter((t) => t.direction === "to-sdk").map((t) => t.logicalId))
|
|
62766
|
+
};
|
|
62767
|
+
if (recreateTargets.viaCcApi.size > 0 || recreateTargets.viaSdkProvider.size > 0) {
|
|
62722
62768
|
const downstreamConsumers = await findDownstreamConsumers({
|
|
62723
62769
|
producerStack: stackInfo.stackName,
|
|
62724
62770
|
producerRegion: stackRegion,
|
|
@@ -62743,8 +62789,11 @@ async function deployCommand(stacks, options) {
|
|
|
62743
62789
|
...assetRedirect && { assetRedirect },
|
|
62744
62790
|
...eventRecorder && { eventRecorder },
|
|
62745
62791
|
...migrationGate && { onCurrentStateLoaded: migrationGate },
|
|
62746
|
-
...
|
|
62747
|
-
|
|
62792
|
+
...options.pinCcApi?.length && { pinCcApi: {
|
|
62793
|
+
stackName: stackInfo.stackName,
|
|
62794
|
+
logicalIds: new Set(options.pinCcApi)
|
|
62795
|
+
} },
|
|
62796
|
+
...recreateTargets && (recreateTargets.viaCcApi.size > 0 || recreateTargets.viaSdkProvider.size > 0) && { recreateTargets },
|
|
62748
62797
|
...options.replace && { replace: true },
|
|
62749
62798
|
...options.forceStatefulRecreation && { forceStatefulRecreation: true },
|
|
62750
62799
|
...options.skipFinalSnapshot && { skipFinalSnapshot: true },
|
|
@@ -62821,6 +62870,18 @@ async function deployCommand(stacks, options) {
|
|
|
62821
62870
|
setAwsClients(awsClients);
|
|
62822
62871
|
}
|
|
62823
62872
|
};
|
|
62873
|
+
if (options.pinCcApi?.length && options.recreateViaSdkProvider?.length) {
|
|
62874
|
+
const both = options.pinCcApi.filter((id) => (options.recreateViaSdkProvider ?? []).includes(id));
|
|
62875
|
+
if (both.length > 0) throw new CdkdError(`--pin-cc-api and --recreate-via-sdk-provider both named: ${both.join(", ")}. They are opposite requests — the first keeps the resource on Cloud Control, the second destroys and recreates it via the SDK provider. Pass one.`, "PIN_CC_API_INVALID");
|
|
62876
|
+
}
|
|
62877
|
+
if (options.pinCcApi?.length) {
|
|
62878
|
+
const reach = analyzePinCcApiReachability(options.pinCcApi, targetStacks.map((st) => ({
|
|
62879
|
+
stackName: st.stackName,
|
|
62880
|
+
logicalIds: Object.keys(st.template.Resources ?? {})
|
|
62881
|
+
})));
|
|
62882
|
+
if (reach.unmatched.length > 0) throw new CdkdError(reach.errorMessage, "PIN_CC_API_INVALID");
|
|
62883
|
+
for (const partial of reach.partial) logger.info(`--pin-cc-api ${partial.logicalId}: applies to ${partial.appliesTo.join(", ")}; not declared by ${partial.absentFrom.join(", ")}`);
|
|
62884
|
+
}
|
|
62824
62885
|
await workGraph.execute({
|
|
62825
62886
|
"asset-build": options.imageBuildConcurrency,
|
|
62826
62887
|
"asset-publish": options.assetPublishConcurrency,
|
|
@@ -64189,6 +64250,13 @@ function isStackNotFoundError(err) {
|
|
|
64189
64250
|
|
|
64190
64251
|
//#endregion
|
|
64191
64252
|
//#region src/cli/commands/diff-recursive.ts
|
|
64253
|
+
/**
|
|
64254
|
+
* The one spelling of the routing token for a resource leaving Cloud Control
|
|
64255
|
+
* (issue #2719). A literal, because `collectCcApiRoutes` writes it and
|
|
64256
|
+
* `annotateRouting` special-cases it, and the integ fixture greps for it --
|
|
64257
|
+
* three places that must agree on a string.
|
|
64258
|
+
*/
|
|
64259
|
+
const SDK_MIGRATION_TOKEN = "returning to SDK provider";
|
|
64192
64260
|
const logger = getLogger().child("DiffRecursive");
|
|
64193
64261
|
/** Every `Ref` target reachable from a value, collected in one walk. */
|
|
64194
64262
|
function collectRefTargets(value, refs) {
|
|
@@ -64645,7 +64713,15 @@ function collectCcApiRoutes(template, state) {
|
|
|
64645
64713
|
hits.set(logicalId, drops.map((d) => d.property));
|
|
64646
64714
|
continue;
|
|
64647
64715
|
}
|
|
64648
|
-
|
|
64716
|
+
const record = state.resources[logicalId];
|
|
64717
|
+
if (record?.provisionedBy === "cc-api") {
|
|
64718
|
+
const leaving = wouldReturnToSdkProvider({
|
|
64719
|
+
resourceType: resource.Type,
|
|
64720
|
+
desiredProperties: resource.Properties ?? {},
|
|
64721
|
+
previousProperties: record.properties
|
|
64722
|
+
});
|
|
64723
|
+
hits.set(logicalId, [leaving ? SDK_MIGRATION_TOKEN : "sticky"]);
|
|
64724
|
+
}
|
|
64649
64725
|
}
|
|
64650
64726
|
return hits;
|
|
64651
64727
|
}
|
|
@@ -64791,6 +64867,7 @@ function renderChangeLines(changes, logFn, ccApiRoutes) {
|
|
|
64791
64867
|
const annotateRouting = (logicalId) => {
|
|
64792
64868
|
const props = ccApiRoutes?.get(logicalId);
|
|
64793
64869
|
if (!props || props.length === 0) return "";
|
|
64870
|
+
if (props.length === 1 && props[0] === "returning to SDK provider") return ` [${SDK_MIGRATION_TOKEN}]`;
|
|
64794
64871
|
return ` [via CC API: ${props.join(", ")}]`;
|
|
64795
64872
|
};
|
|
64796
64873
|
for (const [logicalId, change] of changes.entries()) switch (change.changeType) {
|
|
@@ -65301,7 +65378,7 @@ async function scrubCommand(stacks, options) {
|
|
|
65301
65378
|
if (options.fail) throw new ScrubNeededError();
|
|
65302
65379
|
return;
|
|
65303
65380
|
}
|
|
65304
|
-
if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The
|
|
65381
|
+
if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The CURRENT state.json no longer holds the plaintext. Where the state bucket is VERSIONED — which cdkd bootstrap enables — the pre-scrub body survives as a noncurrent version, stays readable with GetObject and a VersionId, and scrub does not purge it. So a value that was ever persisted must be treated as compromised — ROTATE it in Secrets Manager (scrub matches the current value, so scrub BEFORE rotating); rotation is what makes any surviving version harmless.${keyNote}${unverifiableNote}${failureNote}`);
|
|
65305
65382
|
else logger.info(`\nNothing could be rewritten.${keyNote}${unverifiableNote}${failureNote} ROTATE any exposed secret.`);
|
|
65306
65383
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
65307
65384
|
if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0)) throw new ScrubNeededError();
|
|
@@ -66501,6 +66578,108 @@ function isRegionAmbiguousRefusal(err) {
|
|
|
66501
66578
|
return err instanceof Error && errorCauseChain(err).some((link) => link instanceof DynamicReferenceRegionAmbiguousError);
|
|
66502
66579
|
}
|
|
66503
66580
|
/**
|
|
66581
|
+
* A secrets map with its OWN identity whose entries ARE `target`'s (issue
|
|
66582
|
+
* #2531): every read and write goes to `target`; nothing is stored here.
|
|
66583
|
+
*
|
|
66584
|
+
* The outputs pass resolves each `Export.Name` through one of these rather
|
|
66585
|
+
* than through the pass map itself. The resolver records the uncollapsed
|
|
66586
|
+
* `expression -> plaintext` PAIR beside each entry, keyed by the map INSTANCE
|
|
66587
|
+
* (`recordResolvedPair`), and a second write of one expression with a
|
|
66588
|
+
* different plaintext marks that pair conflicting — so a name's resolution
|
|
66589
|
+
* recording straight into the pass map could conflict the pair a literal
|
|
66590
|
+
* Output embedding the same token needs. Pairs recorded through this view
|
|
66591
|
+
* hang off THIS instance and are discarded with it (a name never positions a
|
|
66592
|
+
* leaf); the entries are the pass map's, live, in both directions.
|
|
66593
|
+
*
|
|
66594
|
+
* Live sharing rather than a private map with a copy: the resolver records
|
|
66595
|
+
* as it goes and `Fn::Join` resolves its parts concurrently, so a part that
|
|
66596
|
+
* rejects while a secret part is still pending ends the name's block before
|
|
66597
|
+
* that part records — a copy at the end of the block misses the late entry,
|
|
66598
|
+
* and a private map SEEDED from the pass map is a snapshot that a LATER
|
|
66599
|
+
* name's resolution would not see the late entry through either. Every
|
|
66600
|
+
* reader in the block (the cross-region pin, the cross-stack pre-pass, the
|
|
66601
|
+
* resolver, the masking of their messages) needs exactly what the pass map
|
|
66602
|
+
* holds at the moment it reads, which is what a view gives. (The deploy
|
|
66603
|
+
* engine's sibling block keeps its end-of-block copy and so still has the
|
|
66604
|
+
* late-entry hole — issue #2563.)
|
|
66605
|
+
*
|
|
66606
|
+
* TWO RESIDUALS of discarding what the view recorded, both bounded to the
|
|
66607
|
+
* choice of EXPRESSION and neither reaching a plaintext:
|
|
66608
|
+
*
|
|
66609
|
+
* - The pairs. A value leaf re-using the token records its own pair at the
|
|
66610
|
+
* seam, which is what positions it — PROVIDED its own resolution succeeds.
|
|
66611
|
+
* A value whose read through the PRIMARY resolver fails after the name's
|
|
66612
|
+
* succeeded (that call sits in the value loop's best-effort catch) has no
|
|
66613
|
+
* pair of its own and no name-recorded one to fall back on, so its leaf
|
|
66614
|
+
* drops to the value scan (the #2485 shape, a sibling's spelling for a
|
|
66615
|
+
* same-plaintext pair); the entry the name recorded is still a needle, so
|
|
66616
|
+
* the plaintext is scrubbed either way — whole, at any length, when the
|
|
66617
|
+
* leaf IS the plaintext (`redactSecretsForState`'s whole-value arm is
|
|
66618
|
+
* unbounded and this pass hands it the raw pass map), and by the substring
|
|
66619
|
+
* arm at `MIN_NEEDLE_LENGTH` or longer when the plaintext is EMBEDDED in a
|
|
66620
|
+
* larger leaf; a shorter embedded one is the #2516 residual, which has
|
|
66621
|
+
* nothing to do with the name loop. A failed read through the cross-region PIN
|
|
66622
|
+
* is not a fallback at all: `unresolvableForeignScrubSecretError` refuses
|
|
66623
|
+
* the whole scrub (`SCRUB_CROSS_REGION_SECRET_UNRESOLVED`), name loop or
|
|
66624
|
+
* value loop.
|
|
66625
|
+
* - The cross-stack associations the pre-pass records
|
|
66626
|
+
* (`recordCrossStackExpression`), keyed by instance the same way. They are
|
|
66627
|
+
* recorded only for a plaintext ALREADY in the map (the resolver gates on
|
|
66628
|
+
* `recordedSecretValues.has(...)`), so losing one degrades the expression a
|
|
66629
|
+
* later leaf is positioned by, never whether the plaintext is found. No
|
|
66630
|
+
* case pins this arm on its own.
|
|
66631
|
+
*
|
|
66632
|
+
* Every member of `Map.prototype` that reads or writes entries is overridden,
|
|
66633
|
+
* and a unit fence (`scrub-shared-entries-secrets.test.ts`) asserts that set
|
|
66634
|
+
* against the runtime's `Map.prototype` so a future addition (the stage-3
|
|
66635
|
+
* `getOrInsert` / `getOrInsertComputed`) cannot land on the permanently empty
|
|
66636
|
+
* backing store unnoticed — a needle inserted there would never reach the
|
|
66637
|
+
* pass map. `structuredClone` reads the internal slots the same way and is
|
|
66638
|
+
* equally unreachable from this file; a clone of a view would be an empty
|
|
66639
|
+
* `Map`.
|
|
66640
|
+
*/
|
|
66641
|
+
var SharedEntriesSecrets = class extends Map {
|
|
66642
|
+
target;
|
|
66643
|
+
constructor(target) {
|
|
66644
|
+
super();
|
|
66645
|
+
this.target = target;
|
|
66646
|
+
}
|
|
66647
|
+
get size() {
|
|
66648
|
+
return this.target.size;
|
|
66649
|
+
}
|
|
66650
|
+
get(plaintext) {
|
|
66651
|
+
return this.target.get(plaintext);
|
|
66652
|
+
}
|
|
66653
|
+
has(plaintext) {
|
|
66654
|
+
return this.target.has(plaintext);
|
|
66655
|
+
}
|
|
66656
|
+
set(plaintext, expression) {
|
|
66657
|
+
this.target.set(plaintext, expression);
|
|
66658
|
+
return this;
|
|
66659
|
+
}
|
|
66660
|
+
delete(plaintext) {
|
|
66661
|
+
return this.target.delete(plaintext);
|
|
66662
|
+
}
|
|
66663
|
+
clear() {
|
|
66664
|
+
this.target.clear();
|
|
66665
|
+
}
|
|
66666
|
+
keys() {
|
|
66667
|
+
return this.target.keys();
|
|
66668
|
+
}
|
|
66669
|
+
values() {
|
|
66670
|
+
return this.target.values();
|
|
66671
|
+
}
|
|
66672
|
+
entries() {
|
|
66673
|
+
return this.target.entries();
|
|
66674
|
+
}
|
|
66675
|
+
forEach(callback, thisArg) {
|
|
66676
|
+
this.target.forEach((value, key) => callback.call(thisArg, value, key, this));
|
|
66677
|
+
}
|
|
66678
|
+
[Symbol.iterator]() {
|
|
66679
|
+
return this.target[Symbol.iterator]();
|
|
66680
|
+
}
|
|
66681
|
+
};
|
|
66682
|
+
/**
|
|
66504
66683
|
* Build the CROSS-STACK pre-pass for ONE stack's scrub (issue
|
|
66505
66684
|
* [#2133](https://github.com/go-to-k/cdkd/issues/2133)).
|
|
66506
66685
|
*
|
|
@@ -66898,21 +67077,28 @@ async function scrubStack(stack, region, stateBackend, lockManager, opts) {
|
|
|
66898
67077
|
const declaredExportName = output.Export?.Name;
|
|
66899
67078
|
let exportName = declaredExportName;
|
|
66900
67079
|
if (declaredExportName !== void 0 && typeof declaredExportName !== "string") {
|
|
67080
|
+
const nameSecrets = new SharedEntriesSecrets(outputSecrets);
|
|
67081
|
+
let nameFailed = false;
|
|
67082
|
+
let nameError;
|
|
66901
67083
|
const nameSource = await pinCrossRegionSecrets(declaredExportName, stack.stackName, {
|
|
66902
67084
|
stackRegion: region,
|
|
66903
67085
|
producerRegions,
|
|
66904
67086
|
resolvers,
|
|
66905
|
-
recordedSecretValues:
|
|
67087
|
+
recordedSecretValues: nameSecrets,
|
|
66906
67088
|
origin: `Export.Name of output '${name}'`
|
|
66907
67089
|
});
|
|
66908
|
-
const nameContext = resolverContext(
|
|
67090
|
+
const nameContext = resolverContext(nameSecrets);
|
|
66909
67091
|
await resolveCrossStackReads(nameSource, nameContext, `Export.Name of output '${name}'`, { canRefuse: !isOutputSuppressed(name, output, conditions, state.outputs ?? {}) });
|
|
66910
67092
|
try {
|
|
66911
67093
|
exportName = await resolver.resolve(nameSource, nameContext);
|
|
66912
67094
|
} catch (err) {
|
|
66913
67095
|
if (isRegionAmbiguousRefusal(err)) throw err;
|
|
67096
|
+
nameFailed = true;
|
|
67097
|
+
nameError = err;
|
|
67098
|
+
}
|
|
67099
|
+
if (nameFailed) {
|
|
66914
67100
|
outputsSourceUntrusted = true;
|
|
66915
|
-
logger.warn(`Export.Name of output ${name} could not be resolved during scrub (${maskSecretsInText(
|
|
67101
|
+
logger.warn(`Export.Name of output ${name} could not be resolved during scrub (${maskSecretsInText(nameError instanceof Error ? nameError.message : String(nameError), outputSecrets)}) — redacting this stack's outputs by value match instead of by template position, since state may be keyed under a name this run cannot reproduce.`);
|
|
66916
67102
|
}
|
|
66917
67103
|
}
|
|
66918
67104
|
if (declaredExportName !== void 0 && typeof exportName !== "string") outputsSourceUntrusted = true;
|
|
@@ -67962,9 +68148,10 @@ var DriftDetectedError = class DriftDetectedError extends CdkdError {
|
|
|
67962
68148
|
* `notCompared`. The roll-up also contains resources whose only problem is a
|
|
67963
68149
|
* surviving `{{resolve:...}}` token cdkd resolves for nobody — a look-alike
|
|
67964
68150
|
* spelling, since issue #2482 moved `ssm-secure` onto the `ssm` path — which
|
|
67965
|
-
* can never clear on a re-run, and which this change did not create. Exiting
|
|
67966
|
-
*
|
|
67967
|
-
*
|
|
68151
|
+
* can never clear on a re-run, and which this change did not create. Exiting
|
|
68152
|
+
* non-zero for them would break `cdkd drift` in CI forever over an unrelated
|
|
68153
|
+
* defect, so the exit reads `refused` and `readFailed` while the report covers
|
|
68154
|
+
* all three. See `NotComparedCause` and `outcomeExitSignal`.
|
|
67968
68155
|
*
|
|
67969
68156
|
* WHY A NON-ZERO EXIT, and why it is a PRESERVATION rather than a new signal.
|
|
67970
68157
|
* Pre-#2108 that population resolved the reference in the WRONG region, the
|
|
@@ -68075,7 +68262,8 @@ function notComparedOutcomes(report) {
|
|
|
68075
68262
|
* code covers only the CLEARABLE ones — `refused` (the population #2108 created)
|
|
68076
68263
|
* and `readFailed` (issues #2151 / #1945). Exiting non-zero on
|
|
68077
68264
|
* `unresolvedToken` would break `cdkd drift` in CI forever for every stack
|
|
68078
|
-
* holding such a spelling — permanent, unclearable, and unrelated. See
|
|
68265
|
+
* holding such a spelling — permanent, unclearable, and unrelated. See
|
|
68266
|
+
* {@link NotComparedCause}.
|
|
68079
68267
|
*
|
|
68080
68268
|
* A `drifted` outcome reports `drifted` even when its own comparison was
|
|
68081
68269
|
* refused: drift is the stronger, actionable signal and the caller ranks it
|
|
@@ -68211,10 +68399,10 @@ function uncomparedTally(reports) {
|
|
|
68211
68399
|
*
|
|
68212
68400
|
* - The TRIGGER is `anyIncomplete`, i.e. {@link outcomeExitSignal}'s
|
|
68213
68401
|
* `incomplete`. It is narrow on purpose: a stack whose ONLY uncompared
|
|
68214
|
-
* resource holds a `{{resolve:...}}` token cdkd resolves for nobody must
|
|
68215
|
-
* shouting on every run about a comparison no action of the
|
|
68216
|
-
* complete -- the same CI-forever hazard that cause is
|
|
68217
|
-
* code on.
|
|
68402
|
+
* resource holds a `{{resolve:...}}` token cdkd resolves for nobody must
|
|
68403
|
+
* not start shouting on every run about a comparison no action of the
|
|
68404
|
+
* user's can ever complete -- the same CI-forever hazard that cause is
|
|
68405
|
+
* kept out of the exit code on.
|
|
68218
68406
|
* - The COUNT and the LABELS, once the line is triggered, cover EVERY
|
|
68219
68407
|
* uncompared resource, each named by its own reason (see
|
|
68220
68408
|
* {@link uncomparedTally}). A resource that was not compared was not
|
|
@@ -70490,9 +70678,19 @@ function orderConsumersBeforeProducers(stackNames, consumerToProducers) {
|
|
|
70490
70678
|
*
|
|
70491
70679
|
* Purges the stack's deployment-event history (the post-mortem `deployments/`
|
|
70492
70680
|
* store cdkd keeps by default) ONLY after a clean, non-interrupted destroy, so
|
|
70493
|
-
* the state bucket
|
|
70494
|
-
* interrupted destroy — those events ARE the post-mortem
|
|
70495
|
-
* retry — and a no-op when `--purge-events` was not
|
|
70681
|
+
* a plain object listing of the state bucket comes back empty. Deliberately
|
|
70682
|
+
* skipped on a failed / interrupted destroy — those events ARE the post-mortem
|
|
70683
|
+
* the user wants on the retry — and a no-op when `--purge-events` was not
|
|
70684
|
+
* passed.
|
|
70685
|
+
*
|
|
70686
|
+
* "Empty" is bounded, and this used to say "the state bucket returns fully
|
|
70687
|
+
* empty" without the bound (issue
|
|
70688
|
+
* [#2624](https://github.com/go-to-k/cdkd/issues/2624)). The purge routes
|
|
70689
|
+
* through `DeploymentEventsReader.pruneRuns` -> `deleteRawObjects`, which sends
|
|
70690
|
+
* `DeleteObjects` with no `VersionId`: on the versioned state bucket that
|
|
70691
|
+
* writes DELETE MARKERS and leaves every earlier version of those keys readable
|
|
70692
|
+
* with a `VersionId`. The log line below states that rather than claiming a
|
|
70693
|
+
* removal that did not happen.
|
|
70496
70694
|
*
|
|
70497
70695
|
* MUST be called AFTER the run's `eventRecorder.finalize()` (which writes this
|
|
70498
70696
|
* run's own events + index): purging first would just be re-created by the
|
|
@@ -70509,7 +70707,7 @@ async function purgeEventsAfterDestroy(reader, stackName, region, opts, logger)
|
|
|
70509
70707
|
if (opts.purgeEvents !== true || opts.runResult !== "SUCCEEDED" || opts.interrupted) return null;
|
|
70510
70708
|
try {
|
|
70511
70709
|
const purge = await reader.pruneRuns(stackName, region, { all: true });
|
|
70512
|
-
if (purge.deletedRunIds.length > 0 || purge.indexDeleted) logger.info(` Purged deployment-event history for ${stackName} (${region}).`);
|
|
70710
|
+
if (purge.deletedRunIds.length > 0 || purge.indexDeleted) logger.info(` Purged deployment-event history for ${stackName} (${region}). Where the state bucket is versioned — which cdkd bootstrap enables — earlier versions of those keys survive and stay readable with GetObject and a VersionId.`);
|
|
70513
70711
|
return purge;
|
|
70514
70712
|
} catch (purgeError) {
|
|
70515
70713
|
logger.warn(` Failed to purge deployment-event history for ${stackName}: ${purgeError instanceof Error ? purgeError.message : String(purgeError)}`);
|
|
@@ -70793,7 +70991,7 @@ async function destroyCommand(stackArgs, options) {
|
|
|
70793
70991
|
* Create destroy command
|
|
70794
70992
|
*/
|
|
70795
70993
|
function createDestroyCommand() {
|
|
70796
|
-
const cmd = new Command("destroy").description("Destroy all resources in the stack").argument("[stacks...]", "Stack name(s) to destroy. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*').").option("--all", "Destroy all stacks", false).option("--purge-events", "After a clean destroy, also delete the stack's deployment-event history (issue #808 store) so the state bucket
|
|
70994
|
+
const cmd = new Command("destroy").description("Destroy all resources in the stack").argument("[stacks...]", "Stack name(s) to destroy. Accepts physical CloudFormation names (e.g. 'MyStage-Api') or CDK display paths (e.g. 'MyStage/Api'). Supports wildcards (e.g. 'MyStage/*').").option("--all", "Destroy all stacks", false).option("--purge-events", "After a clean destroy, also delete the stack's deployment-event history (issue #808 store) so an object listing of the state bucket comes back empty. Where the state bucket is versioned — which cdkd bootstrap enables — earlier versions of those keys survive and stay readable with GetObject and a VersionId. By default events survive destroy as post-mortem context. Skipped when the destroy fails or is interrupted (those events aid the retry). Equivalent for an already-destroyed stack: 'cdkd events prune <stack> --all'.", false).action(withErrorHandling(destroyCommand));
|
|
70797
70995
|
[
|
|
70798
70996
|
...commonOptions,
|
|
70799
70997
|
...appOptions,
|
|
@@ -76201,8 +76399,8 @@ function actionLabel(item, skipFinalSnapshot) {
|
|
|
76201
76399
|
case "orphan-retain": return ` - orphan ${op.logicalId} (${op.resourceType}) [DeletionPolicy Retain — left in AWS]`;
|
|
76202
76400
|
case "orphan-flag": return ` - orphan ${op.logicalId} (${op.resourceType}) [--orphan]`;
|
|
76203
76401
|
case "revert": return ` - revert ${op.logicalId} (${op.resourceType})${rep}`;
|
|
76204
|
-
case "reverse-replacement": return ` - reverse-replace ${op.logicalId} (${op.resourceType}) [re-create old resource, delete new]`;
|
|
76205
|
-
case "reverse-replacement-readopt": return ` - reverse-replace ${op.logicalId} (${op.resourceType}) [delete new, re-adopt retained old resource]`;
|
|
76402
|
+
case "reverse-replacement": return item.retainsNewResource ? ` - reverse-replace ${op.logicalId} (${op.resourceType}) [re-create old resource; new one RETAINED (UpdateReplacePolicy: Retain) and left untracked — REFUSED instead if the re-create collides with the name the retained resource still holds]` : ` - reverse-replace ${op.logicalId} (${op.resourceType}) [re-create old resource, delete new]`;
|
|
76403
|
+
case "reverse-replacement-readopt": return item.retainsNewResource ? ` - reverse-replace ${op.logicalId} (${op.resourceType}) [re-adopt retained old resource; new one RETAINED (UpdateReplacePolicy: Retain) and left untracked]` : ` - reverse-replace ${op.logicalId} (${op.resourceType}) [delete new, re-adopt retained old resource]`;
|
|
76206
76404
|
case "unrecoverable-delete": return ` - (cannot restore) ${op.logicalId} (${op.resourceType}) — was DELETED, unrecoverable`;
|
|
76207
76405
|
case "skip-mismatch": return ` - skip ${op.logicalId} (${op.resourceType}) — physical id changed, needs manual attention`;
|
|
76208
76406
|
case "skip-absent": return ` - skip ${op.logicalId} (${op.resourceType}) — no longer in state`;
|
|
@@ -76538,6 +76736,73 @@ function createRollbackCommand() {
|
|
|
76538
76736
|
//#endregion
|
|
76539
76737
|
//#region src/cli/commands/events.ts
|
|
76540
76738
|
/**
|
|
76739
|
+
* Issue [#2438](https://github.com/go-to-k/cdkd/issues/2438): every string
|
|
76740
|
+
* `cdkd events` renders on its HUMAN path comes back out of a durable store
|
|
76741
|
+
* (`deployments/{runId}.jsonl` and `deployments/index.json`), and several of
|
|
76742
|
+
* those fields are provider- or template-authored — a logical id, a resource
|
|
76743
|
+
* type, a `reason` that interpolates a user-chosen physical name, an AWS error
|
|
76744
|
+
* message. `JSON.stringify` escapes control bytes on the way IN, so the store
|
|
76745
|
+
* is well-formed, but the reader is a plain `JSON.parse` that restores the
|
|
76746
|
+
* original bytes verbatim. Without a sanitising pass here an ESC-bracket
|
|
76747
|
+
* sequence stored in any of them re-forges the terminal of the one command
|
|
76748
|
+
* whose entire purpose is to be BELIEVED after a run.
|
|
76749
|
+
*
|
|
76750
|
+
* Three helpers rather than one, because the field classes have different
|
|
76751
|
+
* correct answers:
|
|
76752
|
+
*
|
|
76753
|
+
* - {@link safeId} is for values with a KNOWN ASCII charset (a run id, a
|
|
76754
|
+
* logical id, a CFn resource type, a region, an event type, a version
|
|
76755
|
+
* string). `asciiOnly` is a positive allowlist and therefore has no residual
|
|
76756
|
+
* at all — the right fence when nothing legitimate is lost.
|
|
76757
|
+
* - {@link safeText} is for free PROSE (`reason`, `error.name`,
|
|
76758
|
+
* `error.message`). A legitimate AWS error message may carry non-ASCII, and
|
|
76759
|
+
* `asciiOnly` would mangle it — so prose gets the control-character denylist,
|
|
76760
|
+
* accepting `display-safe.ts`'s documented residual.
|
|
76761
|
+
*
|
|
76762
|
+
* The wrap goes around the VALUE, never around the colorized token: sanitising
|
|
76763
|
+
* `red(x)` would strip cdkd's OWN ANSI and leave the row uncolored.
|
|
76764
|
+
*/
|
|
76765
|
+
const safeId = (value) => displaySafe(value, { asciiOnly: true });
|
|
76766
|
+
/** Free-prose counterpart of {@link safeId}. See that helper's block comment. */
|
|
76767
|
+
const safeText = (value) => displaySafe(value);
|
|
76768
|
+
/**
|
|
76769
|
+
* Numeric counterpart of {@link safeId}, for the stored COUNTER fields.
|
|
76770
|
+
*
|
|
76771
|
+
* Their declared `number` type is the WRITER's guarantee; the value rendered
|
|
76772
|
+
* here came back through `JSON.parse` and can be any JSON scalar. Sanitising
|
|
76773
|
+
* would be the wrong tool — it would still render the forged text, merely
|
|
76774
|
+
* stripped — so a value that is not a number is not renderable as one and
|
|
76775
|
+
* becomes `?`. Mirrors the pre-existing `typeof e.durationMs === 'number'`
|
|
76776
|
+
* guard, which is why that one line needed no change.
|
|
76777
|
+
*/
|
|
76778
|
+
const safeCount = (value) => typeof value === "number" ? String(value) : "?";
|
|
76779
|
+
/**
|
|
76780
|
+
* Appended to every `cdkd events prune` line that reports a DELETE, because
|
|
76781
|
+
* "pruned" and "removed" would otherwise read as removal and are not (issue
|
|
76782
|
+
* [#2624](https://github.com/go-to-k/cdkd/issues/2624)).
|
|
76783
|
+
*
|
|
76784
|
+
* The pruner reaches S3 through `S3StateBackend.deleteRawObjects`, which sends
|
|
76785
|
+
* `DeleteObjects` with no `VersionId`. On the versioned state bucket
|
|
76786
|
+
* `cdkd bootstrap` creates, that writes a DELETE MARKER and leaves every
|
|
76787
|
+
* earlier version of the key readable through `GetObject` with a `VersionId`.
|
|
76788
|
+
* And the versions exist in QUANTITY here rather than as a theoretical
|
|
76789
|
+
* remainder: the store re-PUTs a run's whole JSONL body on every flush, so a
|
|
76790
|
+
* single run accumulates one noncurrent version per flush before it is ever
|
|
76791
|
+
* pruned. `docs/deployment-events.md` classes `deployments/*.jsonl` as
|
|
76792
|
+
* sensitive, which is what makes the distinction worth a line of output.
|
|
76793
|
+
*
|
|
76794
|
+
* Deliberately NOT appended to the "no runs matched" arm: that arm returns
|
|
76795
|
+
* before any `deleteRawObjects` call, so there is no delete to qualify.
|
|
76796
|
+
*
|
|
76797
|
+
* The converse does NOT hold, and the gap is tracked on issue #2624 rather than
|
|
76798
|
+
* closed here: `pruneRuns({ all: true })` reports `indexDeleted: true`
|
|
76799
|
+
* unconditionally (its `DeleteObjects` is idempotent, so it "succeeds" on an
|
|
76800
|
+
* empty prefix), so the arms this note IS appended to can fire for a stack that
|
|
76801
|
+
* had no history at all. Making that gate truthful is a behaviour change, not a
|
|
76802
|
+
* wording one.
|
|
76803
|
+
*/
|
|
76804
|
+
const NONCURRENT_VERSIONS_SURVIVE_NOTE = " Where the state bucket is versioned — which cdkd bootstrap enables — earlier versions of the deleted keys survive and stay readable with GetObject and a VersionId; prune does not purge them.";
|
|
76805
|
+
/**
|
|
76541
76806
|
* `cdkd events <stack>` — read back the structured deployment events
|
|
76542
76807
|
* (issue #808) cdkd persists per deploy / destroy run, the local
|
|
76543
76808
|
* equivalent of CloudFormation's `DescribeStackEvents`.
|
|
@@ -76580,7 +76845,7 @@ async function eventsCommand(stackName, options) {
|
|
|
76580
76845
|
const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
|
|
76581
76846
|
if (options.run) {
|
|
76582
76847
|
const events = await reader.readRunEvents(stackName, targetRegion, options.run);
|
|
76583
|
-
if (events === null) throw new CdkdError(`No deployment-event stream found for run '${options.run}' of stack '${stackName}' in region '${targetRegion}'.`, "EVENTS_RUN_NOT_FOUND");
|
|
76848
|
+
if (events === null) throw new CdkdError(`No deployment-event stream found for run '${safeId(options.run) || "<unrenderable>"}' of stack '${safeId(stackName) || "<unrenderable>"}' in region '${safeId(targetRegion) || "<unrenderable>"}'.`, "EVENTS_RUN_NOT_FOUND");
|
|
76584
76849
|
if (asJson) {
|
|
76585
76850
|
process.stdout.write(JSON.stringify(events, null, 2) + "\n");
|
|
76586
76851
|
return;
|
|
@@ -76611,16 +76876,22 @@ async function eventsCommand(stackName, options) {
|
|
|
76611
76876
|
async function resolveEventsRegion(reader, stackName, explicitRegion) {
|
|
76612
76877
|
if (explicitRegion) return explicitRegion;
|
|
76613
76878
|
const regions = await reader.listRegions(stackName);
|
|
76614
|
-
if (regions.length === 0) throw new CdkdError(`No deployment-event history found for stack '${stackName}'. Events are recorded by 'cdkd deploy' / 'cdkd destroy' (issue #808); a stack deployed by an older cdkd version has none.`, "EVENTS_NOT_FOUND");
|
|
76615
|
-
if (regions.length > 1) throw new CdkdError(`Stack '${stackName}' has deployment-event history in multiple regions: ${regions.join(", ")}. Re-run with '--stack-region <region>' to disambiguate.`, "EVENTS_REGION_AMBIGUOUS");
|
|
76879
|
+
if (regions.length === 0) throw new CdkdError(`No deployment-event history found for stack '${safeId(stackName) || "<unrenderable>"}'. Events are recorded by 'cdkd deploy' / 'cdkd destroy' (issue #808); a stack deployed by an older cdkd version has none.`, "EVENTS_NOT_FOUND");
|
|
76880
|
+
if (regions.length > 1) throw new CdkdError(`Stack '${safeId(stackName) || "<unrenderable>"}' has deployment-event history in multiple regions: ${regions.map((r) => safeId(r) || "<unrenderable>").join(", ")}. Re-run with '--stack-region <region>' to disambiguate.`, "EVENTS_REGION_AMBIGUOUS");
|
|
76616
76881
|
return regions[0];
|
|
76617
76882
|
}
|
|
76618
76883
|
/**
|
|
76619
|
-
* `cdkd events prune <stack>` —
|
|
76620
|
-
*
|
|
76621
|
-
* keeps event history as post-mortem context, so this is the
|
|
76622
|
-
* to purge it; the deploy/destroy writer also self-bounds to the
|
|
76623
|
-
* {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs automatically.
|
|
76884
|
+
* `cdkd events prune <stack>` — clear old per-run `{runId}.jsonl` event
|
|
76885
|
+
* streams out of the bucket's object LISTING (issue #885). `cdkd destroy`
|
|
76886
|
+
* deliberately keeps event history as post-mortem context, so this is the
|
|
76887
|
+
* explicit way to purge it; the deploy/destroy writer also self-bounds to the
|
|
76888
|
+
* last {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs automatically.
|
|
76889
|
+
*
|
|
76890
|
+
* It does NOT reclaim the storage, and the wording above said it did until
|
|
76891
|
+
* issue [#2624](https://github.com/go-to-k/cdkd/issues/2624): the delete
|
|
76892
|
+
* carries no `VersionId`, so on a versioned state bucket every earlier
|
|
76893
|
+
* version of a pruned key is still billed and still readable. See
|
|
76894
|
+
* {@link NONCURRENT_VERSIONS_SURVIVE_NOTE}.
|
|
76624
76895
|
*
|
|
76625
76896
|
* Retention selection:
|
|
76626
76897
|
* - `--all` purge every run + the index.
|
|
@@ -76657,10 +76928,12 @@ async function eventsPruneCommand(stackName, options) {
|
|
|
76657
76928
|
await stateBackend.verifyBucketExists();
|
|
76658
76929
|
const reader = new DeploymentEventsReader(stateBackend);
|
|
76659
76930
|
const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
|
|
76931
|
+
const safeStack = safeId(stackName) || "<unrenderable>";
|
|
76932
|
+
const safeRegion = safeId(targetRegion) || "<unrenderable>";
|
|
76660
76933
|
const totalRuns = (await reader.listRuns(stackName, targetRegion)).length;
|
|
76661
|
-
const scope = options.all ? `ALL ${totalRuns} run(s)` : options.keep !== void 0 && olderThanMs !== void 0 ? `runs beyond the newest ${options.keep} AND older than ${options.olderThan}` : options.keep !== void 0 ? `runs beyond the newest ${options.keep}` : olderThanMs !== void 0 ? `runs older than ${options.olderThan}` : `runs beyond the newest ${20}`;
|
|
76934
|
+
const scope = options.all ? `ALL ${totalRuns} run(s)` : options.keep !== void 0 && olderThanMs !== void 0 ? `runs beyond the newest ${options.keep} AND older than ${safeId(options.olderThan)}` : options.keep !== void 0 ? `runs beyond the newest ${options.keep}` : olderThanMs !== void 0 ? `runs older than ${safeId(options.olderThan)}` : `runs beyond the newest ${20}`;
|
|
76662
76935
|
if (options.yes !== true) {
|
|
76663
|
-
if (!await confirmPrompt$2(`Prune deployment-event history for ${cyan(
|
|
76936
|
+
if (!await confirmPrompt$2(`Prune deployment-event history for ${cyan(safeStack)} ${gray(`(${safeRegion})`)}: ${scope}?`)) {
|
|
76664
76937
|
logger.info(gray("Aborted; nothing was deleted."));
|
|
76665
76938
|
return;
|
|
76666
76939
|
}
|
|
@@ -76671,10 +76944,10 @@ async function eventsPruneCommand(stackName, options) {
|
|
|
76671
76944
|
...olderThanMs !== void 0 && { olderThanMs }
|
|
76672
76945
|
});
|
|
76673
76946
|
if (result.deletedRunIds.length === 0) {
|
|
76674
|
-
logger.info(gray(result.indexDeleted ? `Removed the empty deployment-event index for ${
|
|
76947
|
+
logger.info(gray(result.indexDeleted ? `Removed the empty deployment-event index for ${safeStack} (${safeRegion}); no run streams to delete. Where the state bucket is versioned — which cdkd bootstrap enables — earlier versions of the deleted keys survive and stay readable with GetObject and a VersionId; prune does not purge them.` : `No runs matched the prune criteria for ${safeStack} (${safeRegion}).`));
|
|
76675
76948
|
return;
|
|
76676
76949
|
}
|
|
76677
|
-
logger.info(`${green("Pruned")} ${result.deletedRunIds.length} deployment-event run(s) for ${cyan(
|
|
76950
|
+
logger.info(`${green("Pruned")} ${result.deletedRunIds.length} deployment-event run(s) for ${cyan(safeStack)} ${gray(`(${safeRegion})`)}; ${result.remainingRunIds.length} retained` + (result.indexDeleted ? gray(" (index removed)") : "") + "." + gray(NONCURRENT_VERSIONS_SURVIVE_NOTE));
|
|
76678
76951
|
} finally {
|
|
76679
76952
|
awsClients.destroy();
|
|
76680
76953
|
}
|
|
@@ -76690,19 +76963,29 @@ async function eventsPruneCommand(stackName, options) {
|
|
|
76690
76963
|
async function confirmPrompt$2(prompt) {
|
|
76691
76964
|
return confirmOrRefuse(prompt, { refusal: "The cdkd events prune confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes to confirm the prune, or run the command from a real terminal. Nothing has been deleted." });
|
|
76692
76965
|
}
|
|
76693
|
-
/**
|
|
76966
|
+
/**
|
|
76967
|
+
* Human-readable run listing (newest first).
|
|
76968
|
+
*
|
|
76969
|
+
* Issue #2438: every `run.*` field here comes back out of
|
|
76970
|
+
* `deployments/index.json` via a plain `JSON.parse`, so its declared type is a
|
|
76971
|
+
* claim about the writer, not about the bytes on disk — each one goes through
|
|
76972
|
+
* {@link safeId}. `result` is sanitised BEFORE the colour decision so the
|
|
76973
|
+
* colour matches the text the user actually sees.
|
|
76974
|
+
*/
|
|
76694
76975
|
function printRunList(stackName, region, runs) {
|
|
76695
76976
|
const logger = getLogger();
|
|
76696
|
-
|
|
76977
|
+
const safeStack = safeId(stackName) || "<unrenderable>";
|
|
76978
|
+
logger.info(`${bold("Deployment runs for")} ${cyan(safeStack)} ${gray(`(${safeId(region) || "<unrenderable>"})`)}`);
|
|
76697
76979
|
if (runs.length === 0) {
|
|
76698
76980
|
logger.info(gray(" (no runs recorded)"));
|
|
76699
76981
|
return;
|
|
76700
76982
|
}
|
|
76701
76983
|
for (const run of runs) {
|
|
76702
|
-
const
|
|
76703
|
-
|
|
76984
|
+
const result = safeId(run.result) || "<unrenderable>";
|
|
76985
|
+
const resultColored = result === "SUCCEEDED" ? green(result) : result === "UNKNOWN" || result === "<unrenderable>" ? gray(result) : red(result);
|
|
76986
|
+
logger.info(` ${cyan(safeId(run.runId) || "<unrenderable>")} ${safeId(run.command) || "<unrenderable>"} ${resultColored} ${gray(safeId(run.startedAt) || "?")} -> ${gray(safeId(run.finishedAt) || "?")} ${gray(`cdkd ${safeId(run.cdkdVersion) || "<unrenderable>"}`)} ${gray(`${safeCount(run.eventCount)} events`)}`);
|
|
76704
76987
|
}
|
|
76705
|
-
logger.info(gray(`\nUse 'cdkd events ${
|
|
76988
|
+
logger.info(gray(`\nUse 'cdkd events ${safeStack} --run <runId>' to read one run's events.`));
|
|
76706
76989
|
}
|
|
76707
76990
|
/** Human-readable single-run event stream (in recorded order). */
|
|
76708
76991
|
/**
|
|
@@ -76712,29 +76995,38 @@ function printRunList(stackName, region, runs) {
|
|
|
76712
76995
|
*/
|
|
76713
76996
|
function printRunEvents(stackName, region, runId, events) {
|
|
76714
76997
|
const logger = getLogger();
|
|
76715
|
-
logger.info(`${bold("Events for run")} ${cyan(runId)} ${gray(`(${stackName}, ${region})`)}`);
|
|
76998
|
+
logger.info(`${bold("Events for run")} ${cyan(safeId(runId) || "<unrenderable>")} ${gray(`(${safeId(stackName) || "<unrenderable>"}, ${safeId(region) || "<unrenderable>"})`)}`);
|
|
76716
76999
|
if (events.length === 0) {
|
|
76717
77000
|
logger.info(gray(" (no events)"));
|
|
76718
77001
|
return;
|
|
76719
77002
|
}
|
|
76720
77003
|
for (const e of events) {
|
|
76721
|
-
const parts = [gray(e.timestamp), colorizeEventType(e.eventType)];
|
|
76722
|
-
if (e.logicalId)
|
|
76723
|
-
|
|
76724
|
-
|
|
76725
|
-
|
|
76726
|
-
|
|
76727
|
-
if (e.
|
|
76728
|
-
if (e.
|
|
76729
|
-
if (e.
|
|
77004
|
+
const parts = [gray(safeId(e.timestamp) || "<unrenderable>"), colorizeEventType(e.eventType)];
|
|
77005
|
+
if (e.logicalId) {
|
|
77006
|
+
const logicalId = safeId(e.logicalId) || "<unrenderable>";
|
|
77007
|
+
const resourceType = e.resourceType ? ` (${safeId(e.resourceType) || "<unrenderable>"})` : "";
|
|
77008
|
+
parts.push(`${logicalId}${resourceType}`);
|
|
77009
|
+
}
|
|
77010
|
+
if (e.operation) parts.push(gray(safeId(e.operation) || "<unrenderable>"));
|
|
77011
|
+
if (e.guard) parts.push(gray(`guard=${safeId(e.guard) || "<unrenderable>"}`));
|
|
77012
|
+
if (e.provisionedBy) parts.push(gray(`[${safeId(e.provisionedBy) || "<unrenderable>"}]`));
|
|
77013
|
+
if (e.command) parts.push(gray(safeId(e.command) || "<unrenderable>"));
|
|
77014
|
+
if (e.region) parts.push(gray(safeId(e.region) || "<unrenderable>"));
|
|
77015
|
+
if (e.cdkdVersion) parts.push(gray(`cdkd ${safeId(e.cdkdVersion) || "<unrenderable>"}`));
|
|
77016
|
+
if (e.result) {
|
|
77017
|
+
const result = safeId(e.result) || "<unrenderable>";
|
|
77018
|
+
parts.push(result === "SUCCEEDED" ? green(result) : result === "<unrenderable>" ? gray(result) : red(result));
|
|
77019
|
+
}
|
|
76730
77020
|
if (typeof e.durationMs === "number") parts.push(gray(`${e.durationMs}ms`));
|
|
76731
|
-
if (e.counts) parts.push(gray(`+${e.counts.created}/~${e.counts.updated}/-${e.counts.deleted}` + (e.counts.failed ? ` !${e.counts.failed}` : "") + (e.counts.skipped ? ` ⚠${e.counts.skipped}` : "")));
|
|
77021
|
+
if (e.counts) parts.push(gray(`+${safeCount(e.counts.created)}/~${safeCount(e.counts.updated)}/-${safeCount(e.counts.deleted)}` + (e.counts.failed ? ` !${safeCount(e.counts.failed)}` : "") + (e.counts.skipped ? ` ⚠${safeCount(e.counts.skipped)}` : "")));
|
|
76732
77022
|
logger.info(` ${parts.join(" ")}`);
|
|
76733
|
-
if (e.reason) logger.info(` ${yellow(e.reason)}`);
|
|
77023
|
+
if (e.reason) logger.info(` ${yellow(safeText(e.reason) || "<unrenderable>")}`);
|
|
76734
77024
|
if (e.error) {
|
|
76735
|
-
const code = e.error.awsErrorCode ? ` (${e.error.awsErrorCode})` : "";
|
|
76736
|
-
const reqId = e.error.requestId ? gray(` requestId=${e.error.requestId}`) : "";
|
|
76737
|
-
|
|
77025
|
+
const code = e.error.awsErrorCode ? ` (${safeId(e.error.awsErrorCode) || "<unrenderable>"})` : "";
|
|
77026
|
+
const reqId = e.error.requestId ? gray(` requestId=${safeId(e.error.requestId) || "<unrenderable>"}`) : "";
|
|
77027
|
+
const name = safeText(e.error.name) || "<unrenderable>";
|
|
77028
|
+
const message = e.error.message ? `: ${safeText(e.error.message) || "<unrenderable>"}` : "";
|
|
77029
|
+
logger.info(` ${red(`${name}${code}${message}`)}${reqId}`);
|
|
76738
77030
|
}
|
|
76739
77031
|
}
|
|
76740
77032
|
}
|
|
@@ -76744,14 +77036,22 @@ function printRunEvents(stackName, region, runId, events) {
|
|
|
76744
77036
|
* Exported for unit-test coverage (matching `destroy-runner.ts`'s
|
|
76745
77037
|
* `PROTECTION_PROPERTY_BY_TYPE` convention) — it is pure, so testing it
|
|
76746
77038
|
* directly beats driving it through the renderer's log output.
|
|
77039
|
+
*
|
|
77040
|
+
* Issue #2438: the declared union is what the WRITER emits; the value handed
|
|
77041
|
+
* here was restored by `JSON.parse` from the stored stream and is therefore
|
|
77042
|
+
* arbitrary text. It is sanitised once, up front, and every arm below both
|
|
77043
|
+
* classifies AND colorizes the sanitised token — classifying the raw value
|
|
77044
|
+
* while printing the sanitised one would let the two disagree. The token is
|
|
77045
|
+
* a known-ASCII discriminator, so `safeId`'s positive allowlist loses nothing.
|
|
76747
77046
|
*/
|
|
76748
77047
|
function colorizeEventType(eventType) {
|
|
76749
|
-
|
|
76750
|
-
if (
|
|
76751
|
-
if (
|
|
76752
|
-
if (
|
|
76753
|
-
if (
|
|
76754
|
-
return
|
|
77048
|
+
const token = safeId(eventType) || "<unrenderable>";
|
|
77049
|
+
if (token.endsWith("FAILED")) return red(token);
|
|
77050
|
+
if (token.endsWith("SUCCEEDED") || token === "RUN_FINISHED") return green(token);
|
|
77051
|
+
if (token === "RESOURCE_SKIPPED") return yellow(token);
|
|
77052
|
+
if (token === "RESOURCE_GUARD_INDETERMINATE") return yellow(token);
|
|
77053
|
+
if (token.startsWith("ROLLBACK")) return yellow(token);
|
|
77054
|
+
return cyan(token);
|
|
76755
77055
|
}
|
|
76756
77056
|
/**
|
|
76757
77057
|
* Create the `cdkd events` command.
|
|
@@ -76780,11 +77080,11 @@ function createEventsCommand() {
|
|
|
76780
77080
|
* action therefore reads the merged view via `command.optsWithGlobals()`.
|
|
76781
77081
|
*/
|
|
76782
77082
|
function createEventsPruneCommand() {
|
|
76783
|
-
return new Command("prune").description("Delete old per-run deployment-event streams
|
|
77083
|
+
return new Command("prune").description("Delete old per-run deployment-event streams. On a versioned state bucket this clears the object listing; earlier versions of the deleted keys survive and are not purged.").argument("<stack>", "Stack name (physical CloudFormation name)").addOption(new Option("--keep <N>", "Retain only the newest N runs").argParser((v) => {
|
|
76784
77084
|
const n = parseInt(v, 10);
|
|
76785
|
-
if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid --keep value "${v}": expected a non-negative integer.`);
|
|
77085
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid --keep value "${safeId(v)}": expected a non-negative integer.`);
|
|
76786
77086
|
return n;
|
|
76787
|
-
})).option("--older-than <duration>", "Delete runs older than this duration (e.g. 24h, 90m)").option("--all", "Delete every recorded run and the index
|
|
77087
|
+
})).option("--older-than <duration>", "Delete runs older than this duration (e.g. 24h, 90m)").option("--all", "Delete every recorded run and the index", false).action(withErrorHandling((stack, _options, command) => eventsPruneCommand(stack, command.optsWithGlobals())));
|
|
76788
77088
|
}
|
|
76789
77089
|
|
|
76790
77090
|
//#endregion
|
|
@@ -82548,10 +82848,11 @@ async function pullEcrImage(imageUri, options) {
|
|
|
82548
82848
|
ecr.destroy();
|
|
82549
82849
|
}
|
|
82550
82850
|
logger.info(`Pulling ${canonicalUri}...`);
|
|
82851
|
+
const pullArgs = ["pull", canonicalUri];
|
|
82551
82852
|
try {
|
|
82552
|
-
await runDockerForeground(
|
|
82853
|
+
await runDockerForeground(pullArgs);
|
|
82553
82854
|
} catch (err) {
|
|
82554
|
-
throw new LocalInvokeBuildError$1(`docker pull ${canonicalUri} failed: ${err
|
|
82855
|
+
throw new LocalInvokeBuildError$1(`docker pull ${canonicalUri} failed: ${describeDockerFailure(err, pullArgs)}`);
|
|
82555
82856
|
}
|
|
82556
82857
|
return canonicalUri;
|
|
82557
82858
|
}
|
|
@@ -82600,17 +82901,17 @@ async function ecrLogin(client, accountId, region) {
|
|
|
82600
82901
|
const [username, password] = Buffer.from(authData.authorizationToken, "base64").toString().split(":");
|
|
82601
82902
|
if (!username || password === void 0) throw new LocalInvokeBuildError$1("ECR authorization token has unexpected shape (missing username/password)");
|
|
82602
82903
|
const endpoint = authData.proxyEndpoint || `https://${accountId}.dkr.ecr.${region}.${ecrUrlSuffix(region)}`;
|
|
82904
|
+
const loginArgs = [
|
|
82905
|
+
"login",
|
|
82906
|
+
"--username",
|
|
82907
|
+
username,
|
|
82908
|
+
"--password-stdin",
|
|
82909
|
+
endpoint
|
|
82910
|
+
];
|
|
82603
82911
|
try {
|
|
82604
|
-
await runDockerStreaming(
|
|
82605
|
-
"login",
|
|
82606
|
-
"--username",
|
|
82607
|
-
username,
|
|
82608
|
-
"--password-stdin",
|
|
82609
|
-
endpoint
|
|
82610
|
-
], { input: password });
|
|
82912
|
+
await runDockerStreaming(loginArgs, { input: password });
|
|
82611
82913
|
} catch (err) {
|
|
82612
|
-
|
|
82613
|
-
throw new LocalInvokeBuildError$1(`ECR login failed: ${formatDockerLoginError(e.stderr || e.message || String(err), endpoint)}`);
|
|
82914
|
+
throw new LocalInvokeBuildError$1(`ECR login failed: ${formatDockerLoginError(describeDockerFailure(err, loginArgs), endpoint)}`);
|
|
82614
82915
|
}
|
|
82615
82916
|
}
|
|
82616
82917
|
/**
|
|
@@ -85982,7 +86283,7 @@ async function prepareOneImage(task, container, options) {
|
|
|
85982
86283
|
const actualTag = await buildDockerImage(asset, cdkOutDir, {
|
|
85983
86284
|
tag,
|
|
85984
86285
|
...options.platformOverride !== void 0 && { platform: options.platformOverride },
|
|
85985
|
-
wrapError: (stderr) => new LocalInvokeBuildError$1(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? asset.source.executable
|
|
86286
|
+
wrapError: (stderr) => new LocalInvokeBuildError$1(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? (asset.source.executable ? redactDockerArgvValues(asset.source.executable).join(" ") : void 0)}): ${stderr}`)
|
|
85986
86287
|
});
|
|
85987
86288
|
if (actualTag !== tag) {
|
|
85988
86289
|
const tagArgs = [
|
|
@@ -88020,6 +88321,123 @@ function createLocalStartAgentCoreCommand$1() {
|
|
|
88020
88321
|
//#endregion
|
|
88021
88322
|
//#region src/cli/commands/local-start-alb.ts
|
|
88022
88323
|
/**
|
|
88324
|
+
* Every distinct Lambda logical id a resolved front-door plan forwards to,
|
|
88325
|
+
* sorted so the warning text is deterministic.
|
|
88326
|
+
*
|
|
88327
|
+
* Walks BOTH action slots — a listener's `defaultAction` and each of its
|
|
88328
|
+
* rules' `action` — because either one can carry a `TargetType: lambda`
|
|
88329
|
+
* target group and an ALB that reaches a Lambda only through a
|
|
88330
|
+
* `path-pattern` rule is the common shape (`/api/*` to a Lambda, everything
|
|
88331
|
+
* else to the ECS service). Mirrors cdk-local's own `collectAlbLambdaTargets`
|
|
88332
|
+
* walk, one layer later: this reads the QUALIFIED plan the strategy returns,
|
|
88333
|
+
* where a Lambda target already carries its resolved `lambda.logicalId`.
|
|
88334
|
+
*/
|
|
88335
|
+
function collectLambdaTargetLogicalIds(frontDoor) {
|
|
88336
|
+
if (!frontDoor) return [];
|
|
88337
|
+
const ids = /* @__PURE__ */ new Set();
|
|
88338
|
+
const fromAction = (action) => {
|
|
88339
|
+
if (action?.kind !== "forward") return;
|
|
88340
|
+
for (const target of action.targets) if (target.kind === "lambda") ids.add(target.lambda.logicalId);
|
|
88341
|
+
};
|
|
88342
|
+
for (const listener of frontDoor.listeners) {
|
|
88343
|
+
fromAction(listener.defaultAction);
|
|
88344
|
+
for (const rule of listener.rules) fromAction(rule.action);
|
|
88345
|
+
}
|
|
88346
|
+
return [...ids].sort();
|
|
88347
|
+
}
|
|
88348
|
+
/**
|
|
88349
|
+
* Wrap an ALB {@link EmulatorStrategy} so that, under `--from-state`, a plan
|
|
88350
|
+
* carrying a `TargetType: lambda` target group emits a WARNING naming the
|
|
88351
|
+
* affected Lambdas — because the flag does not reach their container
|
|
88352
|
+
* environment.
|
|
88353
|
+
*
|
|
88354
|
+
* ## Why a warning and not a refusal
|
|
88355
|
+
*
|
|
88356
|
+
* `cdkd local start-cloudfront` REFUSES cdkd's state flags outright
|
|
88357
|
+
* ([#2528](https://github.com/go-to-k/cdkd/issues/2528)) because NO consumer on
|
|
88358
|
+
* that command's path reads a host-registered state source. `start-alb` is
|
|
88359
|
+
* partial, not total, so the same remedy would delete a working capability:
|
|
88360
|
+
*
|
|
88361
|
+
* - **ECS service targets DO honor `--from-state`.** `bootOneTarget` /
|
|
88362
|
+
* `rollOneTarget` (`cdk-local@0.147.7`, `dist/local-studio-BBtUAVNy.js`
|
|
88363
|
+
* `:26474` / `:26422`) hand `createLocalStateProvider` the FULL options bag,
|
|
88364
|
+
* so cdkd's `fromState` factory is selected and the task containers'
|
|
88365
|
+
* images / env / secrets / volumes resolve against S3 state.
|
|
88366
|
+
* - **Lambda target groups do NOT.** `resolveAlbLambdaTargetEnv` (`:26614`)
|
|
88367
|
+
* rebuilds the bag it hands the shared `resolveLambdaContainerEnv` as a
|
|
88368
|
+
* SIX-KEY allow-list — `fromCfnStack` / `assumeRole` / `region` / `profile` /
|
|
88369
|
+
* `stackRegion` / `envVars` (`:26619-26626`) — dropping `fromState` /
|
|
88370
|
+
* `stateBucket` / `statePrefix`. It forwards `extraStateProviders` faithfully,
|
|
88371
|
+
* but the dispatcher activates an extra provider only when `options[key]` is
|
|
88372
|
+
* truthy (`:4894`), and the key it looks for is exactly the one the bag no
|
|
88373
|
+
* longer carries. So cdkd's factory is registered and never selected, and the
|
|
88374
|
+
* Lambda boots with its intrinsics dropped — one WARN per variable, none of
|
|
88375
|
+
* which says the flag was the problem.
|
|
88376
|
+
*
|
|
88377
|
+
* The fix is upstream (two arguments in `resolveAlbLambdaTargetEnv`); until it
|
|
88378
|
+
* lands, this makes the partiality LOUD at boot instead of leaving the user to
|
|
88379
|
+
* read "Environment variable X contains a CloudFormation intrinsic and was
|
|
88380
|
+
* dropped" as a state problem. `--from-cfn-stack` survives the allow-list and
|
|
88381
|
+
* therefore reaches both target kinds, which is what the message points at.
|
|
88382
|
+
*
|
|
88383
|
+
* ## Mechanics
|
|
88384
|
+
*
|
|
88385
|
+
* `runEcsServiceEmulator` logs every string in the `warnings` array
|
|
88386
|
+
* `strategy.resolveBoots` returns, so appending to it is the whole wiring — no
|
|
88387
|
+
* second synth, since the front-door plan is already resolved. Returns the
|
|
88388
|
+
* strategy UNCHANGED when `--from-state` is absent, so the non-state path
|
|
88389
|
+
* carries no wrapper at all.
|
|
88390
|
+
*
|
|
88391
|
+
* The warning REPEATS on every `--watch` reload, because `resolveBoots` is
|
|
88392
|
+
* re-run per reload (`:26264`) and its warnings re-logged. That matches how
|
|
88393
|
+
* cdk-local's own ALB resolution warnings behave on the same path, which is
|
|
88394
|
+
* the whole reason: repeating is what the surrounding output already does.
|
|
88395
|
+
* It is NOT because a reload could introduce a new Lambda target — the reload
|
|
88396
|
+
* discards `frontDoor` and `buildFrontDoor` runs only at boot (`:26110`), so
|
|
88397
|
+
* a Lambda added mid-`--watch` is never stood up at all. An earlier revision
|
|
88398
|
+
* of this comment argued the second, false thing.
|
|
88399
|
+
*/
|
|
88400
|
+
function warnUnresolvedLambdaTargetEnv(strategy, options) {
|
|
88401
|
+
if (options.fromState !== true) return strategy;
|
|
88402
|
+
return {
|
|
88403
|
+
...strategy,
|
|
88404
|
+
resolveBoots: (stacks, chosenTargets) => {
|
|
88405
|
+
const resolved = strategy.resolveBoots(stacks, chosenTargets);
|
|
88406
|
+
const lambdaIds = collectLambdaTargetLogicalIds(resolved.frontDoor);
|
|
88407
|
+
if (lambdaIds.length === 0) return resolved;
|
|
88408
|
+
const ecsNote = resolved.boots.length > 0 ? "The ECS service targets behind this ALB DO honor --from-state. " : "";
|
|
88409
|
+
return {
|
|
88410
|
+
...resolved,
|
|
88411
|
+
warnings: [...resolved.warnings, `--from-state does not reach the container environment of this ALB's Lambda target group(s): ${lambdaIds.join(", ")}. Their Environment.Variables keep any Ref / Fn::GetAtt / Fn::Sub / Fn::ImportValue intrinsics unresolved, and each is then dropped with its own warning. ` + ecsNote + "The only state source the Lambda path reads is --from-cfn-stack <name>, which REPLACES --from-state (the two are mutually exclusive) and reaches both target kinds on a CloudFormation-deployed stack; otherwise override the affected variables with --env-vars. Tracked as go-to-k/cdkd#2602 (upstream go-to-k/cdk-local#707)."]
|
|
88412
|
+
};
|
|
88413
|
+
}
|
|
88414
|
+
};
|
|
88415
|
+
}
|
|
88416
|
+
/**
|
|
88417
|
+
* The `EmulatorStrategy` `cdkd local start-alb` runs with: cdk-local's ALB
|
|
88418
|
+
* strategy, decorated by {@link warnUnresolvedLambdaTargetEnv}.
|
|
88419
|
+
*
|
|
88420
|
+
* Extracted from the command's action for ONE reason, and it is the finding
|
|
88421
|
+
* that produced it: with the composition written inline in the `.action(...)`
|
|
88422
|
+
* closure, deleting the decorator at its only wiring point left the entire
|
|
88423
|
+
* unit suite green (measured in review — 29/29). The decorator's own cases
|
|
88424
|
+
* exercise it directly, so they cannot see a call site that stopped calling
|
|
88425
|
+
* it. A named export gives that seam a subject a test can hold.
|
|
88426
|
+
*
|
|
88427
|
+
* The seam is fenced BEHAVIOURALLY, in
|
|
88428
|
+
* `tests/unit/cli/local-start-alb-wiring.test.ts`, which runs the real command
|
|
88429
|
+
* and drives whatever object the action hands the engine. A source-shape
|
|
88430
|
+
* assertion was tried first and defeated in review round 3 by hoisting
|
|
88431
|
+
* `albStrategy(options)` into a local one statement up: its positive half was
|
|
88432
|
+
* satisfied by the mutation's own COMMENT and its negative half could not
|
|
88433
|
+
* cross the `)` the hoist introduced. That case is deleted rather than
|
|
88434
|
+
* tightened — the behavioural one catches strictly more, and a fence a comment
|
|
88435
|
+
* can satisfy reads as coverage while providing none.
|
|
88436
|
+
*/
|
|
88437
|
+
function buildAlbEmulatorStrategy(options) {
|
|
88438
|
+
return warnUnresolvedLambdaTargetEnv(albStrategy(options), options);
|
|
88439
|
+
}
|
|
88440
|
+
/**
|
|
88023
88441
|
* `cdkl start-alb <Stack/Alb>` — Issue #86 v1. Names an
|
|
88024
88442
|
* `AWS::ElasticLoadBalancingV2::LoadBalancer`, discovers the ECS service(s)
|
|
88025
88443
|
* behind its HTTP `forward` listeners, boots their replicas, and stands up a
|
|
@@ -88027,8 +88445,8 @@ function createLocalStartAgentCoreCommand$1() {
|
|
|
88027
88445
|
* The symmetric ALB counterpart of `start-api`.
|
|
88028
88446
|
*/
|
|
88029
88447
|
function createLocalStartAlbCommand() {
|
|
88030
|
-
const cmd = new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP and HTTPS listeners — by default a cloud-HTTPS listener is served over plain HTTP locally (with X-Forwarded-Proto: https preserved). Pass --tls (or --tls-cert / --tls-key) to terminate TLS locally with a self-signed or user-supplied cert. All six ALB rule-condition fields are honored (path-pattern / host-header / http-header / http-request-method / query-string / source-ip); forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). authenticate-cognito / authenticate-oidc actions enforce a local Bearer-JWT check (or AWSELBAuthSessionCookie pass-through) against the same JWKS / OIDC discovery URL the deployed ALB would; use --bearer-token <jwt> to inject a default token or --no-verify-auth to disable the guard. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--from-state", "Read cdkd's S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes of the ECS services behind the ALB. Mutually exclusive with --from-cfn-stack.").default(false)).addOption(new Option("--state-bucket <bucket>", "S3 bucket for --from-state. Falls back to CDKD_STATE_BUCKET env or cdk.json context.cdkd.stateBucket.")).addOption(new Option("--state-prefix <prefix>", "S3 key prefix for --from-state state files.").default("cdkd")).action(withErrorHandling(async (targets, options) => {
|
|
88031
|
-
await runEcsServiceEmulator(targets, options,
|
|
88448
|
+
const cmd = new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP and HTTPS listeners — by default a cloud-HTTPS listener is served over plain HTTP locally (with X-Forwarded-Proto: https preserved). Pass --tls (or --tls-cert / --tls-key) to terminate TLS locally with a self-signed or user-supplied cert. All six ALB rule-condition fields are honored (path-pattern / host-header / http-header / http-request-method / query-string / source-ip); forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). authenticate-cognito / authenticate-oidc actions enforce a local Bearer-JWT check (or AWSELBAuthSessionCookie pass-through) against the same JWKS / OIDC discovery URL the deployed ALB would; use --bearer-token <jwt> to inject a default token or --no-verify-auth to disable the guard. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--from-state", "Read cdkd's S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::ImportValue / Fn::GetStackOutput intrinsics in container images, environment variables, secrets, role ARNs, and volumes of the ECS services behind the ALB. Does NOT reach a Lambda target group's container environment (upstream go-to-k/cdk-local#707; a boot warning names the affected functions) — use --from-cfn-stack for those. Mutually exclusive with --from-cfn-stack.").default(false)).addOption(new Option("--state-bucket <bucket>", "S3 bucket for --from-state. Falls back to CDKD_STATE_BUCKET env or cdk.json context.cdkd.stateBucket.")).addOption(new Option("--state-prefix <prefix>", "S3 key prefix for --from-state state files.").default("cdkd")).action(withErrorHandling(async (targets, options) => {
|
|
88449
|
+
await runEcsServiceEmulator(targets, options, buildAlbEmulatorStrategy(options), cdkdExtraStateProviders);
|
|
88032
88450
|
}));
|
|
88033
88451
|
addAlbSpecificOptions(cmd);
|
|
88034
88452
|
return adoptDeprecatedRegionFlag(addCommonEcsServiceOptions(cmd));
|
|
@@ -89099,4 +89517,4 @@ function buildProgram() {
|
|
|
89099
89517
|
|
|
89100
89518
|
//#endregion
|
|
89101
89519
|
export { buildProgram };
|
|
89102
|
-
//# sourceMappingURL=program-
|
|
89520
|
+
//# sourceMappingURL=program-D2Vykejc.js.map
|