@go-to-k/cdkd 0.286.2 → 0.286.4
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-CSEPihOt.js → asg-provider-dBSdt3nB.js} +146 -4
- package/dist/asg-provider-dBSdt3nB.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-BoSlW08T.js → deploy-engine-DylsbAdu.js} +2004 -164
- package/dist/deploy-engine-DylsbAdu.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{program-BuZzDR4s.js → program-dEYAo80R.js} +655 -460
- package/dist/{program-BuZzDR4s.js.map → program-dEYAo80R.js.map} +1 -1
- package/dist/{version-Crg_SdRh.js → version-CKOnsgg1.js} +2 -2
- package/dist/{version-Crg_SdRh.js.map → version-CKOnsgg1.js.map} +1 -1
- package/package.json +2 -1
- package/dist/asg-provider-CSEPihOt.js.map +0 -1
- package/dist/deploy-engine-BoSlW08T.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as findSilentDropProperties, $n as escapeRegExp, $t as createSecretMasker, A as WARM_THROUGHPUT_MEMBERS, An as AssetModeResolver, Ar as resolveBucketRegion, At as readConfigString, B as yellow, Bn as buildDenyExternalAccessPolicy, Br as DeployCancelledError, Bt as s3BucketRegionalDomainName, C as unsupportedFinalSnapshotError, Cn as AssetPublisher, Cr as expectedOwnerParam, Ct as WAFv2WebACLProvider, D as isStatefulRecreateTargetForReplace, Dn as createAssetRedirectResolver, Dr as AssemblyReader, Dt as coerceCfnBoolean, E as MULTI_REGION_RECREATE_BLOCKED_TYPES, En as buildAssetRedirectMap, Er as derivePartitionAndUrlSuffix, Et as assertRegionMatch, F as bold, Fn as isCrossRegionRedirect, Ft as classifyReplaySecretRegion, G as secretBearingStateKeyWarning, Gn as describeDockerFailure, Gt as describeTypeWithThrottleRetry, H as collectPublishedOutputNames, Hn as buildDockerImage, Hr as IntrinsicResolutionRefusalError, Ht as applyRoleArnIfSet, I as cyan, In as parseBootstrapMarker, Ir as CdkdError, It as producerRegionsFromState, J as IAMRoleProvider, Jn as getDockerCmd, Jr as ProvisioningError, Jt as TemplateParser, K as stateKeySecretExposure, Kn as dockerSpawnEnvWithSensitive, Kr as NestedStackChildDirectDestroyError, Kt as withRetry, L as gray, Ln as readBootstrapMarkerBody, Lt as s3BucketArn, M as isWarmThroughputDecrease, Mn as assertAssetBucketRegion, Mr as getAwsClients, Mt as requireConfigArray, N as toFiniteNumber, Nn as ensureAssetStorage, Nr as resetAwsClients, Nt as requireConfigObject, O as isStatefulRecreateTargetSync, On as loadPublishableAssetManifest, Or as processStackMessages, Ot as configBooleanRefusal, P as formatResourceLine, Pn as getBootstrapMarkerKey, Pr as setAwsClients, Pt as requireConfigString, Q as findActionableSilentDrops, Qn as runDockerStreaming, Qr as StackTerminationProtectionError, Qt as carriesSecretMask, R as green, Rn as validateAssetBucketName, Rr as CrossAccountSecretRefusalError, Rt as s3BucketDomainName, S as refusesFinalSnapshot, Sn as shouldRetainResource, Sr as displaySafe, St as refStateLookupFromResource, T as extractDeploymentEventError, Tn as WorkGraph, Tr as canonicalizeRegion, Tt as resolveExplicitPhysicalId, U as exportAliasCollisionScrubWarning, Un as describeDockerCapturedOutput, Ur as LocalInvokeBuildError$1, Ut as DiffCalculator, V as collectDeclaredOutputNames, Vn as describeAwsFailure, Vr as DynamicReferenceRegionAmbiguousError, Vt as s3BucketWebsiteUrl, W as isExportAliasCollision, Wn as describeDockerExecFailure, Wr as LocalStartServiceError, Wt as INTRINSIC_KEYS, X as clearOnUpdateRemoval, Xn as redactDockerArgvValues, Xr as ResourceUpdateNotSupportedError, Xt as STATE_SOURCED_READBACK_RULES, Y as collectInlinePolicyNamesManagedBySiblings, Yn as partitionSensitiveEnv, Yr as ResourceTimeoutError, Yt as STATE_SOURCED_CROSS_GENERATION_RULES, Z as ProviderRegistry, Zn as runDockerForeground, Zr as StackHasActiveImportsError, Zt as TEMPLATE_SOURCED_RULES, _ as PRE_DELETE_SNAPSHOT_TYPES, _n as CUSTOM_RESOURCE_RESPONSE_PREFIX, _r as CFN_TEMPLATE_URL_LIMIT, _t as cfnRefValueFromPhysicalId, a as DeploymentEventsStore, ai as isMarkedNonRetryable, an as recordMaskOnlyValue, ar as getDefaultStateBucketName, at as interruptWatchListenerCount, b as createPreDeleteFinalSnapshot, bn as importableOutputKeys, br as uploadCfnTemplate, bt as isUnboundTemplateParameter, c as replayFailedOperations, ci as isTransientServerError, cn as scrubResourceRecord, cr as resolveAutoAssetStorage, ct as CloudControlProvider, d as updatePartialReason, di as retryClassificationText, dn as rebuildClientForBucketRegion, dr as resolveStateBucketWithDefault, dt as deleteIndeterminateGuards, en as dynamicReferenceTokens, er as stripControlChars, et as createMaskedRetryLogger, f as withResourceDeadline, fn as UNRENDERABLE, fr as resolveStateBucketWithDefaultAndSource, ft as deleteSkipReason, g as ATOMIC_FINAL_SNAPSHOT_TYPES, gn as shellQuote, gr as CFN_TEMPLATE_BODY_LIMIT, gt as carriesDynamicReference, h as computeImplicitDeleteEdges, hn as forceQuitRecoveryClause, hr as warnDeprecatedNoPrefixCliFlag, ht as IntrinsicFunctionResolver, i as DeploymentEventsReader, ii as withErrorHandling, in as maskSecretsInText, ir as synthesisStatusMessage, it as endCommandInterruptScope, j as coerceWarmThroughput$1, jn as BOOTSTRAP_MARKER_PREFIX, jr as AwsClients, jt as replayWarn, k as renderStatefulReason, kn as rewriteTemplateAssetReferences, kt as configStringRefusal, l as replayRollback, li as markNonRetryable, ln as LockManager, lr as resolveCaptureObservedState, lt as slowCcOperationTimeoutMs, m as IMPLICIT_DELETE_DEPENDENCIES, mn as buildLockContentionMessage, mr as stateBucketExistenceConfirmed, mt as isTerminationProtectionPropagationError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as isSingleDynamicReferenceToken, nr as getDockerImageBySourceHash, nt as maskerOrIdentity, o as planFailedOps, oi as isRetryableTransientError, on as recoverMaskedOutput, or as getLegacyStateBucketName, ot as isInterruptedWaitError, p as maskingRetryLogger, pn as buildForceUnlockCommand, pr as resolveUseCdkBootstrapAssets, pt as disableInstanceApiTermination, q as getCurrentResourceSecrets, qn as formatDockerLoginError, qr as PartialFailureError, qt as DagBuilder, r as DeployEngine, ri as normalizeAwsError, rn as maskSecretsInError, rr as Synthesizer, rt as beginCommandInterruptScope, s as planRollback, si as isThrottlingError, sn as redactSecretsForState, sr as resolveApp, st as startInterruptWatch, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as errorCauseChain, tr as AssetManifestLoader, tt as maskDeep, u as updatePartialMessage, ui as markRedactedCause, un as S3StateBackend, ur as resolveSkipPrefix, ut as UNSPECIFIED_SKIP_REASON, v as buildFinalSnapshotIdentifier, vn as DEFAULT_STATE_PREFIX, vr as MIGRATE_TMP_PREFIX, vt as coerceParameterTypedValue, w as makeCanonicalizePropertiesFn, wn as stringifyValue, wr as PARTITION_TABLE, wt as normalizeAwsTagsToCfn, x as isFinalSnapshotError, xn as importableOutputs, xr as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, xt as parameterTypeMayLoseSecretIdentity, y as ccRoutedFinalSnapshotError, yn as exportNamesCarriedFrom, yr as findLargeInlineResources, yt as getAccountInfo, z as red, zn as validateContainerRepoName, zt as s3BucketDualStackDomainName } from "./deploy-engine-DylsbAdu.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-CKOnsgg1.js";
|
|
5
|
+
import { r as protectedReplacementAdvice, t as ASGProvider } from "./asg-provider-dBSdt3nB.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";
|
|
@@ -36,6 +36,7 @@ import { AddTagsCommand, CreateListenerCommand, CreateLoadBalancerCommand, Creat
|
|
|
36
36
|
import { CreateAliasCommand, CreateKeyCommand, DeleteAliasCommand, DescribeKeyCommand, DisableKeyCommand, DisableKeyRotationCommand, EnableKeyCommand, EnableKeyRotationCommand, GetKeyPolicyCommand, GetKeyRotationStatusCommand, KMSClient, ListAliasesCommand, ListResourceTagsCommand, NotFoundException as NotFoundException$2, PutKeyPolicyCommand, ScheduleKeyDeletionCommand, TagResourceCommand as TagResourceCommand$11, UntagResourceCommand as UntagResourceCommand$11, UpdateAliasCommand, UpdateKeyDescriptionCommand } from "@aws-sdk/client-kms";
|
|
37
37
|
import { join as join$1 } from "path";
|
|
38
38
|
import { BatchDeleteImageCommand, CreateRepositoryCommand, DeleteLifecyclePolicyCommand, DeleteRepositoryCommand, DeleteRepositoryPolicyCommand, DescribeImagesCommand as DescribeImagesCommand$1, DescribeRepositoriesCommand, ECRClient, GetAuthorizationTokenCommand, GetLifecyclePolicyCommand, LifecyclePolicyNotFoundException, ListTagsForResourceCommand as ListTagsForResourceCommand$9, PutImageScanningConfigurationCommand, PutImageTagMutabilityCommand, PutLifecyclePolicyCommand, RepositoryNotEmptyException, RepositoryNotFoundException, SetRepositoryPolicyCommand, TagResourceCommand as TagResourceCommand$12, UntagResourceCommand as UntagResourceCommand$12 } from "@aws-sdk/client-ecr";
|
|
39
|
+
import { isDeepStrictEqual, promisify } from "node:util";
|
|
39
40
|
import graphlib from "graphlib";
|
|
40
41
|
import { AddTagsToResourceCommand as AddTagsToResourceCommand$2, CreateDBClusterCommand, CreateDBInstanceCommand, CreateDBProxyCommand, CreateDBProxyEndpointCommand, CreateDBSubnetGroupCommand, DBProxyEndpointNotFoundFault, DBProxyNotFoundFault, DBProxyTargetGroupNotFoundFault, DBProxyTargetNotFoundFault, DeleteDBClusterCommand, DeleteDBInstanceCommand, DeleteDBProxyCommand, DeleteDBProxyEndpointCommand, DeleteDBSubnetGroupCommand, DeregisterDBProxyTargetsCommand, DescribeDBClustersCommand, DescribeDBInstancesCommand, DescribeDBProxiesCommand, DescribeDBProxyEndpointsCommand, DescribeDBProxyTargetGroupsCommand, DescribeDBProxyTargetsCommand, DescribeDBSubnetGroupsCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$10, ModifyDBClusterCommand, ModifyDBInstanceCommand, ModifyDBProxyCommand, ModifyDBProxyEndpointCommand, ModifyDBProxyTargetGroupCommand, ModifyDBSubnetGroupCommand, RDSClient, RegisterDBProxyTargetsCommand, RemoveTagsFromResourceCommand as RemoveTagsFromResourceCommand$2 } from "@aws-sdk/client-rds";
|
|
41
42
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
@@ -45,7 +46,6 @@ import { writeFileSync as writeFileSync$1 } from "fs";
|
|
|
45
46
|
import { Document, Pair, Scalar, YAMLMap, YAMLSeq, isScalar, parse, stringify, visit } from "yaml";
|
|
46
47
|
import * as zlib from "node:zlib";
|
|
47
48
|
import { ApplicationAutoScalingClient, DeleteScalingPolicyCommand, DeregisterScalableTargetCommand, DescribeScalableTargetsCommand, DescribeScalingPoliciesCommand, PutScalingPolicyCommand, RegisterScalableTargetCommand } from "@aws-sdk/client-application-auto-scaling";
|
|
48
|
-
import { isDeepStrictEqual, promisify } from "node:util";
|
|
49
49
|
import { ApiGatewayV2Client, CreateApiCommand, CreateAuthorizerCommand as CreateAuthorizerCommand$1, CreateIntegrationCommand, CreateRouteCommand as CreateRouteCommand$1, CreateStageCommand as CreateStageCommand$1, DeleteAccessLogSettingsCommand, DeleteApiCommand, DeleteAuthorizerCommand as DeleteAuthorizerCommand$1, DeleteCorsConfigurationCommand, DeleteIntegrationCommand, DeleteRouteCommand as DeleteRouteCommand$1, DeleteRouteRequestParameterCommand, DeleteRouteSettingsCommand, DeleteStageCommand as DeleteStageCommand$1, GetApiCommand, GetAuthorizerCommand as GetAuthorizerCommand$1, GetIntegrationCommand, GetRouteCommand, GetStageCommand as GetStageCommand$1, NotFoundException as NotFoundException$3, UpdateApiCommand, UpdateAuthorizerCommand as UpdateAuthorizerCommand$1, UpdateIntegrationCommand, UpdateRouteCommand, UpdateStageCommand as UpdateStageCommand$1 } from "@aws-sdk/client-apigatewayv2";
|
|
50
50
|
import { CreateStateMachineCommand, DeleteStateMachineCommand, DescribeStateMachineCommand, ListTagsForResourceCommand as ListTagsForResourceCommand$11, SFNClient, StateMachineDoesNotExist, TagResourceCommand as TagResourceCommand$13, UntagResourceCommand as UntagResourceCommand$13, UpdateStateMachineCommand } from "@aws-sdk/client-sfn";
|
|
51
51
|
import { CreateClusterCommand, CreateServiceCommand, DeleteClusterCommand, DeleteServiceCommand, DeregisterTaskDefinitionCommand, DescribeClustersCommand, DescribeServicesCommand, DescribeTaskDefinitionCommand, ECSClient, PutClusterCapacityProvidersCommand, RegisterTaskDefinitionCommand, TagResourceCommand as TagResourceCommand$14, UntagResourceCommand as UntagResourceCommand$14, UpdateClusterCommand, UpdateServiceCommand, waitUntilServicesStable } from "@aws-sdk/client-ecs";
|
|
@@ -2579,6 +2579,31 @@ function isPromptAbortError(error) {
|
|
|
2579
2579
|
* intent** — does the user want SDK + silent drop, or CC migration?
|
|
2580
2580
|
* Fail fast and let the user pick one strategy per resource.
|
|
2581
2581
|
*/
|
|
2582
|
+
const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
|
|
2583
|
+
/**
|
|
2584
|
+
* The CFn type of a nested stack's row in its PARENT's template. It decides
|
|
2585
|
+
* both the refusal (`blockedNestedStackTargets`) and the evidence the
|
|
2586
|
+
* unknown-id note renders (`nestedStackLogicalIds`) — so within this module it
|
|
2587
|
+
* is spelled once, or the note could name ids the validator does not refuse.
|
|
2588
|
+
*
|
|
2589
|
+
* The two are NOT the same set, and deliberately so. The refusal fires when
|
|
2590
|
+
* EITHER the state record or the template says nested, because the two types
|
|
2591
|
+
* select different halves of the destructive path: the TEMPLATE's type picks
|
|
2592
|
+
* the provider that deletes the old resource (a Type change emits an UPDATE
|
|
2593
|
+
* carrying `resourceType: desiredResource.Type`, which is what
|
|
2594
|
+
* `oldDeleteProvider` resolves from), while the STATE's type is what
|
|
2595
|
+
* `NestedStackProvider` owns and derives `<parent>~<logicalId>` from. The note
|
|
2596
|
+
* keys on the TEMPLATE alone, because it is telling the user what their current
|
|
2597
|
+
* template contains. Which type governs was argued from plausibility twice and
|
|
2598
|
+
* settled by reading `analyzer/diff-calculator.ts` and `deploy-engine.ts`'s
|
|
2599
|
+
* `oldDeleteProvider`; do not re-derive it from either name. Other modules spell it
|
|
2600
|
+
* for themselves rather than sharing one export: the only EXPORTED copy lives
|
|
2601
|
+
* in `src/cli/commands/retire-cfn-stack.ts`, and importing a CLI command module
|
|
2602
|
+
* from the deployment layer would invert the dependency direction. (No count of
|
|
2603
|
+
* the other spellings is given here on purpose — an unfenced number in a
|
|
2604
|
+
* comment is a number that goes stale.)
|
|
2605
|
+
*/
|
|
2606
|
+
const NESTED_STACK_RESOURCE_TYPE$1 = "AWS::CloudFormation::Stack";
|
|
2582
2607
|
/**
|
|
2583
2608
|
* Plan-time validation of the user's recreate-via-cc-api list.
|
|
2584
2609
|
*
|
|
@@ -2592,7 +2617,6 @@ function isPromptAbortError(error) {
|
|
|
2592
2617
|
* Input order is preserved; duplicate logical ids in the user's input
|
|
2593
2618
|
* are deduplicated.
|
|
2594
2619
|
*/
|
|
2595
|
-
const EMPTY_ALLOW_SET$1 = /* @__PURE__ */ new Set();
|
|
2596
2620
|
function validateRecreateTargets(input) {
|
|
2597
2621
|
const seenCcApi = new Set(input.recreateViaCcApi);
|
|
2598
2622
|
const seenSdk = new Set(input.recreateViaSdkProvider ?? []);
|
|
@@ -2605,6 +2629,7 @@ function validateRecreateTargets(input) {
|
|
|
2605
2629
|
const ambiguousIntentSdk = [];
|
|
2606
2630
|
const blockedStatefulTargets = [];
|
|
2607
2631
|
const blockedMultiRegionTargets = [];
|
|
2632
|
+
const blockedNestedStackTargets = [];
|
|
2608
2633
|
const blockedAlreadySdk = [];
|
|
2609
2634
|
const blockedAlreadyCcApi = [];
|
|
2610
2635
|
const blockedNoSdkProvider = [];
|
|
@@ -2635,11 +2660,12 @@ function validateRecreateTargets(input) {
|
|
|
2635
2660
|
logicalId,
|
|
2636
2661
|
resourceType,
|
|
2637
2662
|
physicalId: recordedResource.physicalId,
|
|
2638
|
-
statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties),
|
|
2663
|
+
statefulReason: isStatefulRecreateTargetSync(resourceType, recordedResource.properties, recordedResource.observedProperties),
|
|
2639
2664
|
direction
|
|
2640
2665
|
};
|
|
2641
2666
|
targets.push(target);
|
|
2642
2667
|
if (MULTI_REGION_RECREATE_BLOCKED_TYPES.has(resourceType)) blockedMultiRegionTargets.push(target);
|
|
2668
|
+
if (resourceType === NESTED_STACK_RESOURCE_TYPE$1 || templateResource.Type === NESTED_STACK_RESOURCE_TYPE$1) blockedNestedStackTargets.push(target);
|
|
2643
2669
|
if (direction === "to-cc-api") {
|
|
2644
2670
|
const actionableDrops = findActionableSilentDrops(resourceType, templateResource.Properties, EMPTY_ALLOW_SET$1);
|
|
2645
2671
|
for (const { property } of actionableDrops) {
|
|
@@ -2671,10 +2697,12 @@ function validateRecreateTargets(input) {
|
|
|
2671
2697
|
ambiguousIntentSdk,
|
|
2672
2698
|
blockedStatefulTargets,
|
|
2673
2699
|
blockedMultiRegionTargets,
|
|
2700
|
+
blockedNestedStackTargets,
|
|
2674
2701
|
blockedAlreadySdk,
|
|
2675
2702
|
blockedAlreadyCcApi,
|
|
2676
2703
|
blockedNoSdkProvider,
|
|
2677
|
-
conflictingDirections
|
|
2704
|
+
conflictingDirections,
|
|
2705
|
+
nestedStackLogicalIds: Object.entries(input.template.Resources ?? {}).filter(([, resource]) => resource?.Type === NESTED_STACK_RESOURCE_TYPE$1).map(([id]) => id)
|
|
2678
2706
|
};
|
|
2679
2707
|
}
|
|
2680
2708
|
/**
|
|
@@ -2691,6 +2719,8 @@ function renderRecreateTargetsErrors(validation) {
|
|
|
2691
2719
|
lines.push(`${FLAG_UMBRELLA} named ${validation.unknownLogicalIds.length} logical id(s) not present in the synth template:`);
|
|
2692
2720
|
for (const id of validation.unknownLogicalIds) lines.push(` - ${id}`);
|
|
2693
2721
|
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.");
|
|
2722
|
+
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.");
|
|
2723
|
+
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
2724
|
}
|
|
2695
2725
|
if (validation.missingFromState.length > 0) {
|
|
2696
2726
|
if (lines.length > 0) lines.push("");
|
|
@@ -2709,6 +2739,16 @@ function renderRecreateTargetsErrors(validation) {
|
|
|
2709
2739
|
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
2740
|
for (const blocked of validation.blockedStatefulTargets) lines.push(` - ${blocked.logicalId} (${blocked.resourceType}) — ${renderStatefulReason(blocked.statefulReason)}`);
|
|
2711
2741
|
}
|
|
2742
|
+
if (validation.blockedNestedStackTargets.length > 0) {
|
|
2743
|
+
if (lines.length > 0) lines.push("");
|
|
2744
|
+
lines.push(`${FLAG_UMBRELLA} refuses to operate on ${validation.blockedNestedStackTargets.length} nested-stack resource(s):`);
|
|
2745
|
+
for (const blocked of validation.blockedNestedStackTargets) {
|
|
2746
|
+
const templateType = validation.nestedStackLogicalIds.includes(blocked.logicalId) ? NESTED_STACK_RESOURCE_TYPE$1 : void 0;
|
|
2747
|
+
const shown = templateType && templateType !== blocked.resourceType ? `${blocked.resourceType} in state, ${templateType} in the template` : blocked.resourceType;
|
|
2748
|
+
lines.push(` - ${blocked.logicalId} (${shown})`);
|
|
2749
|
+
}
|
|
2750
|
+
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.");
|
|
2751
|
+
}
|
|
2712
2752
|
if (validation.blockedMultiRegionTargets.length > 0) {
|
|
2713
2753
|
if (lines.length > 0) lines.push("");
|
|
2714
2754
|
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 +3112,7 @@ async function promptRecreateConfirm(input) {
|
|
|
3072
3112
|
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
3113
|
else logger.warn(`--recreate-via-sdk-provider will destroy + recreate ${toSdkCount} resource(s) via SDK Provider on stack ${input.stackName}:`);
|
|
3074
3114
|
for (const t of input.targets) {
|
|
3075
|
-
const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0) : null;
|
|
3115
|
+
const rederivedReason = t.statefulReason === null && input.forceStatefulRecreation ? isStatefulRecreateTargetForReplace(t.resourceType, void 0, void 0) : null;
|
|
3076
3116
|
const rederived = rederivedReason !== null;
|
|
3077
3117
|
const reason = t.statefulReason ?? rederivedReason;
|
|
3078
3118
|
const stateful = reason !== null;
|
|
@@ -15996,209 +16036,6 @@ function isRecord$1(value) {
|
|
|
15996
16036
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15997
16037
|
}
|
|
15998
16038
|
|
|
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
16039
|
//#endregion
|
|
16203
16040
|
//#region src/provisioning/dynamodb-index-busy-delete.ts
|
|
16204
16041
|
/**
|
|
@@ -19969,9 +19806,27 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
19969
19806
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
19970
19807
|
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
19971
19808
|
debug(`Updating DynamoDB GlobalTable ${logicalId}: ${maskSecrets(physicalId)}`);
|
|
19972
|
-
|
|
19973
|
-
|
|
19974
|
-
|
|
19809
|
+
const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
|
|
19810
|
+
const replaceRemedy = async () => {
|
|
19811
|
+
let guardRegion = "";
|
|
19812
|
+
try {
|
|
19813
|
+
guardRegion = await this.dynamoDBClient.config.region() ?? "";
|
|
19814
|
+
} catch {
|
|
19815
|
+
guardRegion = "";
|
|
19816
|
+
}
|
|
19817
|
+
return extractLocalDeletionProtection(previousProperties, guardRegion) === true ? protectedReplacementAdvice({
|
|
19818
|
+
evidence: "cdkd's recorded properties for this table carry DeletionProtectionEnabled: true for the deploy region",
|
|
19819
|
+
replaceFlags,
|
|
19820
|
+
disable: {
|
|
19821
|
+
before: "aws dynamodb update-table --table-name",
|
|
19822
|
+
identifier: physicalId,
|
|
19823
|
+
after: "--no-deletion-protection-enabled"
|
|
19824
|
+
}
|
|
19825
|
+
}) : `replacement required (deploy with ${replaceFlags}, or destroy + redeploy)`;
|
|
19826
|
+
};
|
|
19827
|
+
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);
|
|
19828
|
+
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);
|
|
19829
|
+
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
19830
|
const oldAttrs = previousProperties["AttributeDefinitions"] ?? [];
|
|
19976
19831
|
const newAttrs = properties["AttributeDefinitions"] ?? [];
|
|
19977
19832
|
const desiredKeyAttrNames = collectDesiredKeyAttributeNames(properties);
|
|
@@ -19984,7 +19839,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
19984
19839
|
const describeResp = await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: physicalId }));
|
|
19985
19840
|
const tableArn = describeResp.Table?.TableArn;
|
|
19986
19841
|
const extractLocalTags = (props) => {
|
|
19987
|
-
return (props
|
|
19842
|
+
return localReplicaEntry(props, currentRegion)?.["Tags"];
|
|
19988
19843
|
};
|
|
19989
19844
|
if (tableArn) await this.applyTagDiff(tableArn, extractLocalTags(previousProperties), extractLocalTags(properties));
|
|
19990
19845
|
const liveWarmByIndexName = /* @__PURE__ */ new Map();
|
|
@@ -21034,7 +20889,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
21034
20889
|
return entry;
|
|
21035
20890
|
}));
|
|
21036
20891
|
result["Replicas"] = replicas;
|
|
21037
|
-
const
|
|
20892
|
+
const localReplica = replicas.find((r) => r["Region"] === currentRegion);
|
|
21038
20893
|
if (table.GlobalSecondaryIndexes && table.GlobalSecondaryIndexes.length > 0) {
|
|
21039
20894
|
const cfnIndexes = [];
|
|
21040
20895
|
const replicaIndexEntries = [];
|
|
@@ -21065,11 +20920,11 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
21065
20920
|
if (Object.keys(replicaEntry).length > 1) replicaIndexEntries.push(replicaEntry);
|
|
21066
20921
|
}
|
|
21067
20922
|
result["GlobalSecondaryIndexes"] = cfnIndexes;
|
|
21068
|
-
if (
|
|
20923
|
+
if (localReplica && replicaIndexEntries.length > 0) localReplica["GlobalSecondaryIndexes"] = replicaIndexEntries;
|
|
21069
20924
|
}
|
|
21070
20925
|
if (table.OnDemandThroughput?.MaxWriteRequestUnits !== void 0) result["WriteOnDemandThroughputSettings"] = { MaxWriteRequestUnits: table.OnDemandThroughput.MaxWriteRequestUnits };
|
|
21071
20926
|
else result["WriteOnDemandThroughputSettings"] = {};
|
|
21072
|
-
if (
|
|
20927
|
+
if (localReplica && table.OnDemandThroughput?.MaxReadRequestUnits !== void 0) localReplica["ReadOnDemandThroughputSettings"] = { MaxReadRequestUnits: table.OnDemandThroughput.MaxReadRequestUnits };
|
|
21073
20928
|
if (billingMode === "PROVISIONED") {
|
|
21074
20929
|
const autoScaling = await this.readAutoScalingSettings(tableNameForSubs, "dynamodb:table:WriteCapacityUnits");
|
|
21075
20930
|
if (autoScaling) result["WriteProvisionedThroughputSettings"] = { WriteCapacityAutoScalingSettings: autoScaling };
|
|
@@ -21638,10 +21493,29 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
21638
21493
|
}
|
|
21639
21494
|
};
|
|
21640
21495
|
/**
|
|
21641
|
-
*
|
|
21642
|
-
*
|
|
21643
|
-
*
|
|
21496
|
+
* The `Replicas[]` entry for `region`, or `undefined`.
|
|
21497
|
+
*
|
|
21498
|
+
* ONE predicate, because two readers of the same field had drifted: issue
|
|
21499
|
+
* [#2610]'s review round guarded `extractLocalDeletionProtection` against a
|
|
21500
|
+
* non-array `Replicas` and left `update()`'s inline `extractLocalTags` on the
|
|
21501
|
+
* old `(props['Replicas'] ?? []) as Array<...>` -- which is the one that
|
|
21502
|
+
* actually throws FIRST on the apply path, ~400 lines earlier in the same
|
|
21503
|
+
* method, so guarding only the other produced no visible change and no test
|
|
21504
|
+
* went red. A shared predicate is what stops the next reader inheriting the
|
|
21505
|
+
* unguarded spelling; `derivePerCallProvisionedThroughput` documents the same
|
|
21506
|
+
* hazard for the same field and guards independently.
|
|
21507
|
+
*
|
|
21508
|
+
* `Array.isArray`, not `?? []`: a non-array `Replicas` -- an unresolved
|
|
21509
|
+
* intrinsic, or a hand-edited state record -- makes `.find` throw a bare
|
|
21510
|
+
* `TypeError`, and since [#2610] one caller sits on the REFUSAL path, where a
|
|
21511
|
+
* throw REPLACES three informative `ProvisioningError`s with a stack trace.
|
|
21512
|
+
* The `r?.` guards a null ENTRY inside an otherwise-valid array.
|
|
21644
21513
|
*/
|
|
21514
|
+
function localReplicaEntry(props, region) {
|
|
21515
|
+
const raw = props["Replicas"];
|
|
21516
|
+
if (!Array.isArray(raw)) return void 0;
|
|
21517
|
+
return raw.find((r) => r?.["Region"] === region);
|
|
21518
|
+
}
|
|
21645
21519
|
/**
|
|
21646
21520
|
* Extract the local replica's `DeletionProtectionEnabled` from a CFn
|
|
21647
21521
|
* properties shape. The `AWS::DynamoDB::GlobalTable` CFn schema places
|
|
@@ -21657,7 +21531,7 @@ var DynamoDBGlobalTableProvider = class {
|
|
|
21657
21531
|
* the SDK call.
|
|
21658
21532
|
*/
|
|
21659
21533
|
function extractLocalDeletionProtection(props, region) {
|
|
21660
|
-
const perReplica = (props
|
|
21534
|
+
const perReplica = localReplicaEntry(props, region)?.["DeletionProtectionEnabled"];
|
|
21661
21535
|
if (typeof perReplica === "boolean") return perReplica;
|
|
21662
21536
|
const topLevel = props["DeletionProtectionEnabled"];
|
|
21663
21537
|
return typeof topLevel === "boolean" ? topLevel : void 0;
|
|
@@ -22916,6 +22790,11 @@ function collectTableOnDemandCeilings(properties, region, diagnostics, maskSecre
|
|
|
22916
22790
|
}
|
|
22917
22791
|
};
|
|
22918
22792
|
}
|
|
22793
|
+
/**
|
|
22794
|
+
* Diff CFn `Replicas[]` arrays. Keyed by `Region`. Returns adds, removes,
|
|
22795
|
+
* and modifies (entries whose other keys — KMSMasterKeyId,
|
|
22796
|
+
* GlobalSecondaryIndexes, TableClassOverride — differ from the old shape).
|
|
22797
|
+
*/
|
|
22919
22798
|
function diffReplicas(oldReplicas, newReplicas) {
|
|
22920
22799
|
const oldByRegion = /* @__PURE__ */ new Map();
|
|
22921
22800
|
const newByRegion = /* @__PURE__ */ new Map();
|
|
@@ -22984,6 +22863,75 @@ function deepEqual$1(a, b) {
|
|
|
22984
22863
|
//#endregion
|
|
22985
22864
|
//#region src/provisioning/providers/logs-loggroup-provider.ts
|
|
22986
22865
|
/**
|
|
22866
|
+
* Refuse a `RetentionInDays` the send path cannot USE (issue [#2521]).
|
|
22867
|
+
*
|
|
22868
|
+
* **The scope of this refusal is a TEST, not a paragraph.** Every review round
|
|
22869
|
+
* on issue [#2521] found a hole in a PROSE statement of it — a coercible
|
|
22870
|
+
* negative that fell through to `DeleteRetentionPolicy`, an unusable value
|
|
22871
|
+
* silenced by the wrong change gate, wrong cells in the hand-written table
|
|
22872
|
+
* that replaced those sentences, and then the one non-finite number
|
|
22873
|
+
* JavaScript makes FALSY. No count is given here: the count itself drifted
|
|
22874
|
+
* between this comment and the changelog, which is the same failure one level
|
|
22875
|
+
* up. The table now lives in
|
|
22876
|
+
* `tests/unit/provisioning/logs-loggroup-provider-retention-coercion.test.ts`
|
|
22877
|
+
* as `CREATE_MATRIX` / `UPDATE_MATRIX`: one row per (value, previous) pair,
|
|
22878
|
+
* each asserting which AWS call goes out, each carrying what the pre-#2521
|
|
22879
|
+
* code did. Read a change against those rows and add one for any pair they do
|
|
22880
|
+
* not cover; do not restate the rule here, because a fourth restatement is
|
|
22881
|
+
* the thing that keeps being wrong.
|
|
22882
|
+
*
|
|
22883
|
+
* THREE refusal CASES across TWO `throw` statements — the non-finite guard
|
|
22884
|
+
* clause at the top of the body has its own, and the other two share the
|
|
22885
|
+
* second, differing only in the `detail` clause. Only the WHY of each:
|
|
22886
|
+
*
|
|
22887
|
+
* - **a non-finite NUMBER** (`NaN`, `Infinity`, `-Infinity`). It sits ahead
|
|
22888
|
+
* of the falsy return because `NaN` is FALSY: without it that value exits
|
|
22889
|
+
* early, fails the `> 0` send test and reaches `DeleteRetentionPolicy`,
|
|
22890
|
+
* while its two siblings — truthy — were already refused. Leaving one
|
|
22891
|
+
* member of a family out is the inconsistency the negative arm below
|
|
22892
|
+
* introduced, so the arm names the family.
|
|
22893
|
+
* - **not a number at all** (`' '`, `'abc'`, `[]`, `{}`, `true`). The
|
|
22894
|
+
* pre-coercion code tested `if (properties['RetentionInDays'])` and handed
|
|
22895
|
+
* the raw value to `PutRetentionPolicyCommand`, so CloudWatch Logs rejected
|
|
22896
|
+
* it. Refusing moves that rejection one layer earlier and names the
|
|
22897
|
+
* property.
|
|
22898
|
+
* - **a NEGATIVE number** (`-1`, `'-1'`). It coerces, so a refusal gated on
|
|
22899
|
+
* coercibility alone let it through; it then failed the `> 0` send test and
|
|
22900
|
+
* fell into the UPDATE path's `else`, which issues
|
|
22901
|
+
* `DeleteRetentionPolicy` — a template typo SILENTLY REMOVING a live
|
|
22902
|
+
* retention where the old code sent `-1` and failed loudly.
|
|
22903
|
+
*
|
|
22904
|
+
* The FALSY values pass through, as they did before — `0`, `''`, `false`,
|
|
22905
|
+
* `null` and absent; `NaN` is the one exception, refused above because it is a
|
|
22906
|
+
* malformed number rather than a spelling of "no retention".
|
|
22907
|
+
*
|
|
22908
|
+
* `'0'` is TRUTHY, so it reaches this function and passes too: zero is
|
|
22909
|
+
* what {@link LogsLogGroupProvider.readCurrentState} records for a group with
|
|
22910
|
+
* no retention policy, and CloudFormation coerces `'0'` to `0`, so routing the
|
|
22911
|
+
* string to the same arm as the number is the consistency the coercion is for.
|
|
22912
|
+
* On the UPDATE path against a live positive retention that is a real change —
|
|
22913
|
+
* it DELETES where the old code sent and was rejected — and `UPDATE_MATRIX`
|
|
22914
|
+
* pins it.
|
|
22915
|
+
*
|
|
22916
|
+
* The message names the value's TYPE and never the value. A provider's
|
|
22917
|
+
* `properties` bag arrives with dynamic references already RESOLVED, so a
|
|
22918
|
+
* `{{resolve:secretsmanager:...}}` scalar is PLAINTEXT by the time it reaches
|
|
22919
|
+
* here, and this function has no masker to thread — `create()` takes no
|
|
22920
|
+
* `CreateContext` and `applyUpdate` no `UpdateContext`
|
|
22921
|
+
* (`.claude/rules/provider-masking.md`). Interpolating it as
|
|
22922
|
+
* `${JSON.stringify(raw)}` was the first draft and
|
|
22923
|
+
* `vp run audit:provider-secret-mask:check` refused it — but read that as one
|
|
22924
|
+
* SPELLING being fenced, not the rule: the critic's population is
|
|
22925
|
+
* interpolated `JSON.stringify` calls, so a bare `${raw}` here would leave it
|
|
22926
|
+
* green. Do not put the value back without threading the capability first.
|
|
22927
|
+
*/
|
|
22928
|
+
function assertUsableRetention(raw, coerced, logicalId, resourceType) {
|
|
22929
|
+
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);
|
|
22930
|
+
if (!raw) return;
|
|
22931
|
+
if (coerced !== void 0 && coerced >= 0) return;
|
|
22932
|
+
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);
|
|
22933
|
+
}
|
|
22934
|
+
/**
|
|
22987
22935
|
* AWS CloudWatch Logs LogGroup Provider
|
|
22988
22936
|
*
|
|
22989
22937
|
* Implements resource provisioning for AWS::Logs::LogGroup using the CloudWatch Logs SDK.
|
|
@@ -23064,8 +23012,10 @@ var LogsLogGroupProvider = class {
|
|
|
23064
23012
|
else throw createError;
|
|
23065
23013
|
}
|
|
23066
23014
|
try {
|
|
23067
|
-
const
|
|
23068
|
-
|
|
23015
|
+
const rawRetention = properties["RetentionInDays"];
|
|
23016
|
+
const retentionInDays = toFiniteNumber(rawRetention);
|
|
23017
|
+
assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
|
|
23018
|
+
if (retentionInDays !== void 0 && retentionInDays > 0) await this.logsClient.send(new PutRetentionPolicyCommand({
|
|
23069
23019
|
logGroupName,
|
|
23070
23020
|
retentionInDays
|
|
23071
23021
|
}));
|
|
@@ -23135,14 +23085,14 @@ var LogsLogGroupProvider = class {
|
|
|
23135
23085
|
*/
|
|
23136
23086
|
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
23137
23087
|
try {
|
|
23138
|
-
return await this.applyUpdate(logicalId, physicalId, properties, previousProperties);
|
|
23088
|
+
return await this.applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties);
|
|
23139
23089
|
} catch (error) {
|
|
23140
23090
|
if (error instanceof CdkdError) throw error;
|
|
23141
23091
|
const cause = error instanceof Error ? error : void 0;
|
|
23142
23092
|
throw new ProvisioningError(`Failed to update log group ${logicalId}: ${error instanceof Error ? error.message : String(error)}`, resourceType, logicalId, physicalId, cause);
|
|
23143
23093
|
}
|
|
23144
23094
|
}
|
|
23145
|
-
async applyUpdate(logicalId, physicalId, properties, previousProperties) {
|
|
23095
|
+
async applyUpdate(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
23146
23096
|
this.logger.debug(`Updating log group ${logicalId}: ${physicalId}`);
|
|
23147
23097
|
const normalizeClass = (v) => typeof v === "string" && v.length > 0 ? v : "STANDARD";
|
|
23148
23098
|
const prevClass = normalizeClass(previousProperties["LogGroupClass"]);
|
|
@@ -23151,6 +23101,13 @@ var LogsLogGroupProvider = class {
|
|
|
23151
23101
|
const replaceFlags = "--replace --force-stateful-recreation";
|
|
23152
23102
|
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
23103
|
}
|
|
23104
|
+
const rawRetention = properties["RetentionInDays"];
|
|
23105
|
+
const rawPreviousRetention = previousProperties["RetentionInDays"];
|
|
23106
|
+
const renderedRetention = JSON.stringify(rawRetention);
|
|
23107
|
+
const sameRawRetention = Object.is(rawRetention, rawPreviousRetention) || renderedRetention !== void 0 && renderedRetention !== "null" && renderedRetention === JSON.stringify(rawPreviousRetention);
|
|
23108
|
+
const retentionInDays = toFiniteNumber(rawRetention);
|
|
23109
|
+
const oldRetentionInDays = toFiniteNumber(rawPreviousRetention);
|
|
23110
|
+
if (!sameRawRetention) assertUsableRetention(rawRetention, retentionInDays, logicalId, resourceType);
|
|
23154
23111
|
const newKmsKeyId = properties["KmsKeyId"];
|
|
23155
23112
|
if (newKmsKeyId !== previousProperties["KmsKeyId"]) {
|
|
23156
23113
|
if (newKmsKeyId) await this.logsClient.send(new AssociateKmsKeyCommand({
|
|
@@ -23160,8 +23117,7 @@ var LogsLogGroupProvider = class {
|
|
|
23160
23117
|
else await this.logsClient.send(new DisassociateKmsKeyCommand({ logGroupName: physicalId }));
|
|
23161
23118
|
this.logger.debug(`Updated KMS key association for log group ${physicalId}`);
|
|
23162
23119
|
}
|
|
23163
|
-
|
|
23164
|
-
if (retentionInDays !== previousProperties["RetentionInDays"]) if (retentionInDays) await this.logsClient.send(new PutRetentionPolicyCommand({
|
|
23120
|
+
if (retentionInDays !== oldRetentionInDays || !sameRawRetention && Boolean(rawPreviousRetention) && oldRetentionInDays === void 0) if (retentionInDays !== void 0 && retentionInDays > 0) await this.logsClient.send(new PutRetentionPolicyCommand({
|
|
23165
23121
|
logGroupName: physicalId,
|
|
23166
23122
|
retentionInDays
|
|
23167
23123
|
}));
|
|
@@ -37258,7 +37214,15 @@ var ELBv2Provider = class {
|
|
|
37258
37214
|
for (const [k, v] of Object.entries(p)) if (!handledKeys.has(k)) out[k] = v;
|
|
37259
37215
|
return out;
|
|
37260
37216
|
};
|
|
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
|
|
37217
|
+
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({
|
|
37218
|
+
evidence: "cdkd's recorded properties for this load balancer carry LoadBalancerAttributes deletion_protection.enabled=true",
|
|
37219
|
+
replaceFlags: "cdkd deploy --replace",
|
|
37220
|
+
disable: {
|
|
37221
|
+
before: "aws elbv2 modify-load-balancer-attributes --load-balancer-arn",
|
|
37222
|
+
identifier: physicalId,
|
|
37223
|
+
after: "--attributes Key=deletion_protection.enabled,Value=false"
|
|
37224
|
+
}
|
|
37225
|
+
}) : "For Name / Type / Scheme re-deploy with cdkd deploy --replace, or destroy + redeploy the stack."));
|
|
37262
37226
|
const submittedAttrs = this.diffAttributes(this.normalizeAttributes(properties["LoadBalancerAttributes"]), this.normalizeAttributes(previousProperties["LoadBalancerAttributes"]), this.attributeRemovalResolver(LOAD_BALANCER_ATTRIBUTE_DEFAULTS));
|
|
37263
37227
|
if (submittedAttrs.length > 0) {
|
|
37264
37228
|
await this.getClient().send(new ModifyLoadBalancerAttributesCommand({
|
|
@@ -43897,7 +43861,7 @@ var CognitoUserPoolProvider = class {
|
|
|
43897
43861
|
if (properties["SmsVerificationMessage"] !== void 0) updateParams.SmsVerificationMessage = properties["SmsVerificationMessage"];
|
|
43898
43862
|
if (properties["UserPoolTier"]) updateParams.UserPoolTier = properties["UserPoolTier"];
|
|
43899
43863
|
await this.getClient().send(new UpdateUserPoolCommand(updateParams));
|
|
43900
|
-
await this.reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, properties["Schema"], previousProperties["Schema"]);
|
|
43864
|
+
await this.reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, properties["Schema"], previousProperties["Schema"], (properties["DeletionProtection"] ? properties["DeletionProtection"] : previousProperties["DeletionProtection"]) === "ACTIVE");
|
|
43901
43865
|
await this.applyMfaConfig(physicalId, properties, mfaConfiguration, {
|
|
43902
43866
|
...liveMfa?.value !== void 0 ? { liveMfaConfiguration: liveMfa.value } : {},
|
|
43903
43867
|
...liveMfa?.failed === true ? { liveReadFailed: true } : {},
|
|
@@ -43939,7 +43903,7 @@ var CognitoUserPoolProvider = class {
|
|
|
43939
43903
|
* attribute, requires replacement -> ResourceUpdateNotSupportedError.
|
|
43940
43904
|
* A byte-identical Schema is a no-op (no AddCustomAttributes call).
|
|
43941
43905
|
*/
|
|
43942
|
-
async reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, newSchema, oldSchema) {
|
|
43906
|
+
async reconcileSchemaCustomAttributes(logicalId, physicalId, resourceType, newSchema, oldSchema, deletionProtected = false) {
|
|
43943
43907
|
const newAttrs = newSchema ?? [];
|
|
43944
43908
|
const oldAttrs = oldSchema ?? [];
|
|
43945
43909
|
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 +43928,20 @@ var CognitoUserPoolProvider = class {
|
|
|
43964
43928
|
...modified.map((n) => `modified attribute '${n}'`),
|
|
43965
43929
|
...addedStandard.map((n) => `added standard attribute '${n}'`)
|
|
43966
43930
|
];
|
|
43967
|
-
if (immutableChanges.length > 0)
|
|
43931
|
+
if (immutableChanges.length > 0) {
|
|
43932
|
+
const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
|
|
43933
|
+
const remedy = deletionProtected ? protectedReplacementAdvice({
|
|
43934
|
+
evidence: "cdkd's properties for this user pool carry DeletionProtection: ACTIVE",
|
|
43935
|
+
replaceFlags,
|
|
43936
|
+
disable: {
|
|
43937
|
+
before: "aws cognito-idp update-user-pool --user-pool-id",
|
|
43938
|
+
identifier: physicalId,
|
|
43939
|
+
after: "--deletion-protection INACTIVE",
|
|
43940
|
+
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."
|
|
43941
|
+
}
|
|
43942
|
+
}) : `AWS::Cognito::UserPool is a stateful type, so re-run with ${replaceFlags} to recreate it (this deletes all users in the pool).`;
|
|
43943
|
+
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}`);
|
|
43944
|
+
}
|
|
43968
43945
|
const addedCustom = added.filter((a) => a.Name !== void 0 && !STANDARD_USER_POOL_ATTRIBUTES.has(a.Name));
|
|
43969
43946
|
if (addedCustom.length === 0) return;
|
|
43970
43947
|
this.logger.debug(`Adding ${addedCustom.length} custom attribute(s) to ${physicalId}: ` + addedCustom.map((a) => a.Name).join(", "));
|
|
@@ -52048,7 +52025,7 @@ var SchedulerScheduleProvider = class {
|
|
|
52048
52025
|
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
52049
52026
|
const groupName = this.groupNameOf(properties);
|
|
52050
52027
|
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
|
|
52028
|
+
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
52029
|
this.logger.debug(`Updating Schedule ${logicalId}: ${physicalId}${groupName ? ` (group: ${groupName})` : ""}`);
|
|
52053
52030
|
try {
|
|
52054
52031
|
const input = {
|
|
@@ -52070,7 +52047,11 @@ var SchedulerScheduleProvider = class {
|
|
|
52070
52047
|
}
|
|
52071
52048
|
async delete(logicalId, physicalId, resourceType, properties, context) {
|
|
52072
52049
|
const groupName = this.groupNameOf(properties);
|
|
52073
|
-
if (properties === void 0)
|
|
52050
|
+
if (properties === void 0) {
|
|
52051
|
+
const safeId = displaySafe(physicalId, { asciiOnly: true });
|
|
52052
|
+
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.";
|
|
52053
|
+
this.logger.warn(`State record for Schedule ${logicalId} carries no properties — deleting '${safeId || "<unrenderable>"}' from the default group. ${manualHint}`);
|
|
52054
|
+
}
|
|
52074
52055
|
this.logger.debug(`Deleting Schedule ${logicalId}: ${physicalId}${groupName ? ` (group: ${groupName})` : ""}`);
|
|
52075
52056
|
try {
|
|
52076
52057
|
await this.getClient().send(new DeleteScheduleCommand({
|
|
@@ -52254,7 +52235,7 @@ var EFSProvider = class {
|
|
|
52254
52235
|
]) {
|
|
52255
52236
|
const next = properties[key];
|
|
52256
52237
|
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.`);
|
|
52238
|
+
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
52239
|
}
|
|
52259
52240
|
const newThroughputMode = properties["ThroughputMode"];
|
|
52260
52241
|
const newProvisioned = properties["ProvisionedThroughputInMibps"];
|
|
@@ -53375,7 +53356,7 @@ var FSxFileSystemProvider = class {
|
|
|
53375
53356
|
for (const key of TOP_LEVEL_IMMUTABLE_PROPS) {
|
|
53376
53357
|
const next = properties[key];
|
|
53377
53358
|
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.`);
|
|
53359
|
+
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
53360
|
}
|
|
53380
53361
|
const variantConfigKey = this.detectVariantConfigKey(properties, previousProperties);
|
|
53381
53362
|
const { diff: variantDiff, hasMutableDiff: variantHasMutableDiff } = variantConfigKey ? this.computeVariantConfigDiff(resourceType, logicalId, variantConfigKey, properties[variantConfigKey] ?? {}, previousProperties[variantConfigKey] ?? {}) : {
|
|
@@ -53456,7 +53437,7 @@ var FSxFileSystemProvider = class {
|
|
|
53456
53437
|
const nextVal = next[key];
|
|
53457
53438
|
const prevVal = prev[key];
|
|
53458
53439
|
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.`);
|
|
53440
|
+
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
53441
|
const ctx = {
|
|
53461
53442
|
resourceType,
|
|
53462
53443
|
logicalId,
|
|
@@ -54417,6 +54398,16 @@ var EMRClusterProvider = class {
|
|
|
54417
54398
|
const changed = (key) => !jsonEqual$2(properties[key], previousProperties[key]);
|
|
54418
54399
|
const nextInstances = properties["Instances"] ?? {};
|
|
54419
54400
|
const prevInstances = previousProperties["Instances"] ?? {};
|
|
54401
|
+
const replaceFlags = "cdkd deploy --replace --force-stateful-recreation";
|
|
54402
|
+
const replaceRemedy = () => toBoolean(prevInstances["TerminationProtected"]) === true ? protectedReplacementAdvice({
|
|
54403
|
+
evidence: "cdkd's recorded properties for this cluster carry Instances.TerminationProtected: true",
|
|
54404
|
+
replaceFlags,
|
|
54405
|
+
disable: {
|
|
54406
|
+
before: "aws emr modify-cluster-attributes --cluster-id",
|
|
54407
|
+
identifier: physicalId,
|
|
54408
|
+
after: "--no-termination-protected"
|
|
54409
|
+
}
|
|
54410
|
+
}) : `Re-deploy with ${replaceFlags}, or destroy + redeploy the stack.`;
|
|
54420
54411
|
let terminationProtectedChanged = false;
|
|
54421
54412
|
if (changed("Instances")) {
|
|
54422
54413
|
const instanceKeys = /* @__PURE__ */ new Set([...Object.keys(nextInstances), ...Object.keys(prevInstances)]);
|
|
@@ -54426,7 +54417,7 @@ var EMRClusterProvider = class {
|
|
|
54426
54417
|
terminationProtectedChanged = true;
|
|
54427
54418
|
continue;
|
|
54428
54419
|
}
|
|
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.
|
|
54420
|
+
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
54421
|
}
|
|
54431
54422
|
}
|
|
54432
54423
|
for (const key of Object.keys({
|
|
@@ -54435,7 +54426,7 @@ var EMRClusterProvider = class {
|
|
|
54435
54426
|
})) {
|
|
54436
54427
|
if (key === "Instances") continue;
|
|
54437
54428
|
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.
|
|
54429
|
+
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
54430
|
}
|
|
54440
54431
|
const visibleChanged = changed("VisibleToAllUsers");
|
|
54441
54432
|
const stepConcurrencyChanged = changed("StepConcurrencyLevel");
|
|
@@ -58447,7 +58438,7 @@ var DLMLifecyclePolicyProvider = class {
|
|
|
58447
58438
|
}
|
|
58448
58439
|
}
|
|
58449
58440
|
async update(logicalId, physicalId, resourceType, properties, previousProperties) {
|
|
58450
|
-
if (JSON.stringify(properties["DefaultPolicy"]) !== JSON.stringify(previousProperties["DefaultPolicy"])) throw new ResourceUpdateNotSupportedError(resourceType, logicalId,
|
|
58441
|
+
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
58442
|
this.logger.debug(`Updating DLM Lifecycle Policy ${logicalId} (${physicalId})`);
|
|
58452
58443
|
try {
|
|
58453
58444
|
await this.getClient().send(new UpdateLifecyclePolicyCommand({
|
|
@@ -58647,7 +58638,7 @@ var S3VectorsProvider = class {
|
|
|
58647
58638
|
* is surfaced as `ResourceUpdateNotSupportedError`.
|
|
58648
58639
|
*/
|
|
58649
58640
|
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)`);
|
|
58641
|
+
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
58642
|
const oldTags = this.cfnTagsToRecord(previousProperties["Tags"]);
|
|
58652
58643
|
const newTags = this.cfnTagsToRecord(properties["Tags"]);
|
|
58653
58644
|
const toSet = {};
|
|
@@ -62686,8 +62677,7 @@ async function deployCommand(stacks, options) {
|
|
|
62686
62677
|
skipPrefix,
|
|
62687
62678
|
yes: options.yes
|
|
62688
62679
|
});
|
|
62689
|
-
let
|
|
62690
|
-
let recreateViaSdkProviderTargets;
|
|
62680
|
+
let recreateTargets;
|
|
62691
62681
|
if (options.recreateViaCcApi?.length || options.recreateViaSdkProvider?.length) {
|
|
62692
62682
|
const stateForRecreateCheck = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
|
|
62693
62683
|
const validation = await probeAndRevalidateStateful({
|
|
@@ -62716,9 +62706,12 @@ async function deployCommand(stacks, options) {
|
|
|
62716
62706
|
});
|
|
62717
62707
|
const errorBlock = renderRecreateTargetsErrors(validation);
|
|
62718
62708
|
if (errorBlock) throw new CdkdError(errorBlock, "RECREATE_TARGETS_INVALID");
|
|
62719
|
-
|
|
62720
|
-
|
|
62721
|
-
|
|
62709
|
+
recreateTargets = {
|
|
62710
|
+
stackName: stackInfo.stackName,
|
|
62711
|
+
viaCcApi: new Set(validation.targets.filter((t) => t.direction === "to-cc-api").map((t) => t.logicalId)),
|
|
62712
|
+
viaSdkProvider: new Set(validation.targets.filter((t) => t.direction === "to-sdk").map((t) => t.logicalId))
|
|
62713
|
+
};
|
|
62714
|
+
if (recreateTargets.viaCcApi.size > 0 || recreateTargets.viaSdkProvider.size > 0) {
|
|
62722
62715
|
const downstreamConsumers = await findDownstreamConsumers({
|
|
62723
62716
|
producerStack: stackInfo.stackName,
|
|
62724
62717
|
producerRegion: stackRegion,
|
|
@@ -62743,8 +62736,7 @@ async function deployCommand(stacks, options) {
|
|
|
62743
62736
|
...assetRedirect && { assetRedirect },
|
|
62744
62737
|
...eventRecorder && { eventRecorder },
|
|
62745
62738
|
...migrationGate && { onCurrentStateLoaded: migrationGate },
|
|
62746
|
-
...
|
|
62747
|
-
...recreateViaSdkProviderTargets && recreateViaSdkProviderTargets.size > 0 && { recreateViaSdkProviderTargets },
|
|
62739
|
+
...recreateTargets && (recreateTargets.viaCcApi.size > 0 || recreateTargets.viaSdkProvider.size > 0) && { recreateTargets },
|
|
62748
62740
|
...options.replace && { replace: true },
|
|
62749
62741
|
...options.forceStatefulRecreation && { forceStatefulRecreation: true },
|
|
62750
62742
|
...options.skipFinalSnapshot && { skipFinalSnapshot: true },
|
|
@@ -65301,7 +65293,7 @@ async function scrubCommand(stacks, options) {
|
|
|
65301
65293
|
if (options.fail) throw new ScrubNeededError();
|
|
65302
65294
|
return;
|
|
65303
65295
|
}
|
|
65304
|
-
if (totalStacksScrubbed > 0) logger.info(`\nDone: scrubbed ${totalStacksScrubbed} stack(s). The
|
|
65296
|
+
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
65297
|
else logger.info(`\nNothing could be rewritten.${keyNote}${unverifiableNote}${failureNote} ROTATE any exposed secret.`);
|
|
65306
65298
|
if (failures.length > 0) throw scrubStacksFailedError(failures);
|
|
65307
65299
|
if (options.fail && (totalStacksWithUnscrubbableKeys > 0 || totalStacksWithUnverifiableReads > 0)) throw new ScrubNeededError();
|
|
@@ -67962,9 +67954,10 @@ var DriftDetectedError = class DriftDetectedError extends CdkdError {
|
|
|
67962
67954
|
* `notCompared`. The roll-up also contains resources whose only problem is a
|
|
67963
67955
|
* surviving `{{resolve:...}}` token cdkd resolves for nobody — a look-alike
|
|
67964
67956
|
* 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
|
-
*
|
|
67957
|
+
* can never clear on a re-run, and which this change did not create. Exiting
|
|
67958
|
+
* non-zero for them would break `cdkd drift` in CI forever over an unrelated
|
|
67959
|
+
* defect, so the exit reads `refused` and `readFailed` while the report covers
|
|
67960
|
+
* all three. See `NotComparedCause` and `outcomeExitSignal`.
|
|
67968
67961
|
*
|
|
67969
67962
|
* WHY A NON-ZERO EXIT, and why it is a PRESERVATION rather than a new signal.
|
|
67970
67963
|
* Pre-#2108 that population resolved the reference in the WRONG region, the
|
|
@@ -68075,7 +68068,8 @@ function notComparedOutcomes(report) {
|
|
|
68075
68068
|
* code covers only the CLEARABLE ones — `refused` (the population #2108 created)
|
|
68076
68069
|
* and `readFailed` (issues #2151 / #1945). Exiting non-zero on
|
|
68077
68070
|
* `unresolvedToken` would break `cdkd drift` in CI forever for every stack
|
|
68078
|
-
* holding such a spelling — permanent, unclearable, and unrelated. See
|
|
68071
|
+
* holding such a spelling — permanent, unclearable, and unrelated. See
|
|
68072
|
+
* {@link NotComparedCause}.
|
|
68079
68073
|
*
|
|
68080
68074
|
* A `drifted` outcome reports `drifted` even when its own comparison was
|
|
68081
68075
|
* refused: drift is the stronger, actionable signal and the caller ranks it
|
|
@@ -68211,10 +68205,10 @@ function uncomparedTally(reports) {
|
|
|
68211
68205
|
*
|
|
68212
68206
|
* - The TRIGGER is `anyIncomplete`, i.e. {@link outcomeExitSignal}'s
|
|
68213
68207
|
* `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.
|
|
68208
|
+
* resource holds a `{{resolve:...}}` token cdkd resolves for nobody must
|
|
68209
|
+
* not start shouting on every run about a comparison no action of the
|
|
68210
|
+
* user's can ever complete -- the same CI-forever hazard that cause is
|
|
68211
|
+
* kept out of the exit code on.
|
|
68218
68212
|
* - The COUNT and the LABELS, once the line is triggered, cover EVERY
|
|
68219
68213
|
* uncompared resource, each named by its own reason (see
|
|
68220
68214
|
* {@link uncomparedTally}). A resource that was not compared was not
|
|
@@ -70490,9 +70484,19 @@ function orderConsumersBeforeProducers(stackNames, consumerToProducers) {
|
|
|
70490
70484
|
*
|
|
70491
70485
|
* Purges the stack's deployment-event history (the post-mortem `deployments/`
|
|
70492
70486
|
* 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
|
|
70487
|
+
* a plain object listing of the state bucket comes back empty. Deliberately
|
|
70488
|
+
* skipped on a failed / interrupted destroy — those events ARE the post-mortem
|
|
70489
|
+
* the user wants on the retry — and a no-op when `--purge-events` was not
|
|
70490
|
+
* passed.
|
|
70491
|
+
*
|
|
70492
|
+
* "Empty" is bounded, and this used to say "the state bucket returns fully
|
|
70493
|
+
* empty" without the bound (issue
|
|
70494
|
+
* [#2624](https://github.com/go-to-k/cdkd/issues/2624)). The purge routes
|
|
70495
|
+
* through `DeploymentEventsReader.pruneRuns` -> `deleteRawObjects`, which sends
|
|
70496
|
+
* `DeleteObjects` with no `VersionId`: on the versioned state bucket that
|
|
70497
|
+
* writes DELETE MARKERS and leaves every earlier version of those keys readable
|
|
70498
|
+
* with a `VersionId`. The log line below states that rather than claiming a
|
|
70499
|
+
* removal that did not happen.
|
|
70496
70500
|
*
|
|
70497
70501
|
* MUST be called AFTER the run's `eventRecorder.finalize()` (which writes this
|
|
70498
70502
|
* run's own events + index): purging first would just be re-created by the
|
|
@@ -70509,7 +70513,7 @@ async function purgeEventsAfterDestroy(reader, stackName, region, opts, logger)
|
|
|
70509
70513
|
if (opts.purgeEvents !== true || opts.runResult !== "SUCCEEDED" || opts.interrupted) return null;
|
|
70510
70514
|
try {
|
|
70511
70515
|
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}).`);
|
|
70516
|
+
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
70517
|
return purge;
|
|
70514
70518
|
} catch (purgeError) {
|
|
70515
70519
|
logger.warn(` Failed to purge deployment-event history for ${stackName}: ${purgeError instanceof Error ? purgeError.message : String(purgeError)}`);
|
|
@@ -70793,7 +70797,7 @@ async function destroyCommand(stackArgs, options) {
|
|
|
70793
70797
|
* Create destroy command
|
|
70794
70798
|
*/
|
|
70795
70799
|
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
|
|
70800
|
+
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
70801
|
[
|
|
70798
70802
|
...commonOptions,
|
|
70799
70803
|
...appOptions,
|
|
@@ -76201,8 +76205,8 @@ function actionLabel(item, skipFinalSnapshot) {
|
|
|
76201
76205
|
case "orphan-retain": return ` - orphan ${op.logicalId} (${op.resourceType}) [DeletionPolicy Retain — left in AWS]`;
|
|
76202
76206
|
case "orphan-flag": return ` - orphan ${op.logicalId} (${op.resourceType}) [--orphan]`;
|
|
76203
76207
|
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]`;
|
|
76208
|
+
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]`;
|
|
76209
|
+
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
76210
|
case "unrecoverable-delete": return ` - (cannot restore) ${op.logicalId} (${op.resourceType}) — was DELETED, unrecoverable`;
|
|
76207
76211
|
case "skip-mismatch": return ` - skip ${op.logicalId} (${op.resourceType}) — physical id changed, needs manual attention`;
|
|
76208
76212
|
case "skip-absent": return ` - skip ${op.logicalId} (${op.resourceType}) — no longer in state`;
|
|
@@ -76538,6 +76542,73 @@ function createRollbackCommand() {
|
|
|
76538
76542
|
//#endregion
|
|
76539
76543
|
//#region src/cli/commands/events.ts
|
|
76540
76544
|
/**
|
|
76545
|
+
* Issue [#2438](https://github.com/go-to-k/cdkd/issues/2438): every string
|
|
76546
|
+
* `cdkd events` renders on its HUMAN path comes back out of a durable store
|
|
76547
|
+
* (`deployments/{runId}.jsonl` and `deployments/index.json`), and several of
|
|
76548
|
+
* those fields are provider- or template-authored — a logical id, a resource
|
|
76549
|
+
* type, a `reason` that interpolates a user-chosen physical name, an AWS error
|
|
76550
|
+
* message. `JSON.stringify` escapes control bytes on the way IN, so the store
|
|
76551
|
+
* is well-formed, but the reader is a plain `JSON.parse` that restores the
|
|
76552
|
+
* original bytes verbatim. Without a sanitising pass here an ESC-bracket
|
|
76553
|
+
* sequence stored in any of them re-forges the terminal of the one command
|
|
76554
|
+
* whose entire purpose is to be BELIEVED after a run.
|
|
76555
|
+
*
|
|
76556
|
+
* Three helpers rather than one, because the field classes have different
|
|
76557
|
+
* correct answers:
|
|
76558
|
+
*
|
|
76559
|
+
* - {@link safeId} is for values with a KNOWN ASCII charset (a run id, a
|
|
76560
|
+
* logical id, a CFn resource type, a region, an event type, a version
|
|
76561
|
+
* string). `asciiOnly` is a positive allowlist and therefore has no residual
|
|
76562
|
+
* at all — the right fence when nothing legitimate is lost.
|
|
76563
|
+
* - {@link safeText} is for free PROSE (`reason`, `error.name`,
|
|
76564
|
+
* `error.message`). A legitimate AWS error message may carry non-ASCII, and
|
|
76565
|
+
* `asciiOnly` would mangle it — so prose gets the control-character denylist,
|
|
76566
|
+
* accepting `display-safe.ts`'s documented residual.
|
|
76567
|
+
*
|
|
76568
|
+
* The wrap goes around the VALUE, never around the colorized token: sanitising
|
|
76569
|
+
* `red(x)` would strip cdkd's OWN ANSI and leave the row uncolored.
|
|
76570
|
+
*/
|
|
76571
|
+
const safeId = (value) => displaySafe(value, { asciiOnly: true });
|
|
76572
|
+
/** Free-prose counterpart of {@link safeId}. See that helper's block comment. */
|
|
76573
|
+
const safeText = (value) => displaySafe(value);
|
|
76574
|
+
/**
|
|
76575
|
+
* Numeric counterpart of {@link safeId}, for the stored COUNTER fields.
|
|
76576
|
+
*
|
|
76577
|
+
* Their declared `number` type is the WRITER's guarantee; the value rendered
|
|
76578
|
+
* here came back through `JSON.parse` and can be any JSON scalar. Sanitising
|
|
76579
|
+
* would be the wrong tool — it would still render the forged text, merely
|
|
76580
|
+
* stripped — so a value that is not a number is not renderable as one and
|
|
76581
|
+
* becomes `?`. Mirrors the pre-existing `typeof e.durationMs === 'number'`
|
|
76582
|
+
* guard, which is why that one line needed no change.
|
|
76583
|
+
*/
|
|
76584
|
+
const safeCount = (value) => typeof value === "number" ? String(value) : "?";
|
|
76585
|
+
/**
|
|
76586
|
+
* Appended to every `cdkd events prune` line that reports a DELETE, because
|
|
76587
|
+
* "pruned" and "removed" would otherwise read as removal and are not (issue
|
|
76588
|
+
* [#2624](https://github.com/go-to-k/cdkd/issues/2624)).
|
|
76589
|
+
*
|
|
76590
|
+
* The pruner reaches S3 through `S3StateBackend.deleteRawObjects`, which sends
|
|
76591
|
+
* `DeleteObjects` with no `VersionId`. On the versioned state bucket
|
|
76592
|
+
* `cdkd bootstrap` creates, that writes a DELETE MARKER and leaves every
|
|
76593
|
+
* earlier version of the key readable through `GetObject` with a `VersionId`.
|
|
76594
|
+
* And the versions exist in QUANTITY here rather than as a theoretical
|
|
76595
|
+
* remainder: the store re-PUTs a run's whole JSONL body on every flush, so a
|
|
76596
|
+
* single run accumulates one noncurrent version per flush before it is ever
|
|
76597
|
+
* pruned. `docs/deployment-events.md` classes `deployments/*.jsonl` as
|
|
76598
|
+
* sensitive, which is what makes the distinction worth a line of output.
|
|
76599
|
+
*
|
|
76600
|
+
* Deliberately NOT appended to the "no runs matched" arm: that arm returns
|
|
76601
|
+
* before any `deleteRawObjects` call, so there is no delete to qualify.
|
|
76602
|
+
*
|
|
76603
|
+
* The converse does NOT hold, and the gap is tracked on issue #2624 rather than
|
|
76604
|
+
* closed here: `pruneRuns({ all: true })` reports `indexDeleted: true`
|
|
76605
|
+
* unconditionally (its `DeleteObjects` is idempotent, so it "succeeds" on an
|
|
76606
|
+
* empty prefix), so the arms this note IS appended to can fire for a stack that
|
|
76607
|
+
* had no history at all. Making that gate truthful is a behaviour change, not a
|
|
76608
|
+
* wording one.
|
|
76609
|
+
*/
|
|
76610
|
+
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.";
|
|
76611
|
+
/**
|
|
76541
76612
|
* `cdkd events <stack>` — read back the structured deployment events
|
|
76542
76613
|
* (issue #808) cdkd persists per deploy / destroy run, the local
|
|
76543
76614
|
* equivalent of CloudFormation's `DescribeStackEvents`.
|
|
@@ -76580,7 +76651,7 @@ async function eventsCommand(stackName, options) {
|
|
|
76580
76651
|
const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
|
|
76581
76652
|
if (options.run) {
|
|
76582
76653
|
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");
|
|
76654
|
+
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
76655
|
if (asJson) {
|
|
76585
76656
|
process.stdout.write(JSON.stringify(events, null, 2) + "\n");
|
|
76586
76657
|
return;
|
|
@@ -76611,16 +76682,22 @@ async function eventsCommand(stackName, options) {
|
|
|
76611
76682
|
async function resolveEventsRegion(reader, stackName, explicitRegion) {
|
|
76612
76683
|
if (explicitRegion) return explicitRegion;
|
|
76613
76684
|
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");
|
|
76685
|
+
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");
|
|
76686
|
+
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
76687
|
return regions[0];
|
|
76617
76688
|
}
|
|
76618
76689
|
/**
|
|
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.
|
|
76690
|
+
* `cdkd events prune <stack>` — clear old per-run `{runId}.jsonl` event
|
|
76691
|
+
* streams out of the bucket's object LISTING (issue #885). `cdkd destroy`
|
|
76692
|
+
* deliberately keeps event history as post-mortem context, so this is the
|
|
76693
|
+
* explicit way to purge it; the deploy/destroy writer also self-bounds to the
|
|
76694
|
+
* last {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs automatically.
|
|
76695
|
+
*
|
|
76696
|
+
* It does NOT reclaim the storage, and the wording above said it did until
|
|
76697
|
+
* issue [#2624](https://github.com/go-to-k/cdkd/issues/2624): the delete
|
|
76698
|
+
* carries no `VersionId`, so on a versioned state bucket every earlier
|
|
76699
|
+
* version of a pruned key is still billed and still readable. See
|
|
76700
|
+
* {@link NONCURRENT_VERSIONS_SURVIVE_NOTE}.
|
|
76624
76701
|
*
|
|
76625
76702
|
* Retention selection:
|
|
76626
76703
|
* - `--all` purge every run + the index.
|
|
@@ -76657,10 +76734,12 @@ async function eventsPruneCommand(stackName, options) {
|
|
|
76657
76734
|
await stateBackend.verifyBucketExists();
|
|
76658
76735
|
const reader = new DeploymentEventsReader(stateBackend);
|
|
76659
76736
|
const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
|
|
76737
|
+
const safeStack = safeId(stackName) || "<unrenderable>";
|
|
76738
|
+
const safeRegion = safeId(targetRegion) || "<unrenderable>";
|
|
76660
76739
|
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}`;
|
|
76740
|
+
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
76741
|
if (options.yes !== true) {
|
|
76663
|
-
if (!await confirmPrompt$2(`Prune deployment-event history for ${cyan(
|
|
76742
|
+
if (!await confirmPrompt$2(`Prune deployment-event history for ${cyan(safeStack)} ${gray(`(${safeRegion})`)}: ${scope}?`)) {
|
|
76664
76743
|
logger.info(gray("Aborted; nothing was deleted."));
|
|
76665
76744
|
return;
|
|
76666
76745
|
}
|
|
@@ -76671,10 +76750,10 @@ async function eventsPruneCommand(stackName, options) {
|
|
|
76671
76750
|
...olderThanMs !== void 0 && { olderThanMs }
|
|
76672
76751
|
});
|
|
76673
76752
|
if (result.deletedRunIds.length === 0) {
|
|
76674
|
-
logger.info(gray(result.indexDeleted ? `Removed the empty deployment-event index for ${
|
|
76753
|
+
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
76754
|
return;
|
|
76676
76755
|
}
|
|
76677
|
-
logger.info(`${green("Pruned")} ${result.deletedRunIds.length} deployment-event run(s) for ${cyan(
|
|
76756
|
+
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
76757
|
} finally {
|
|
76679
76758
|
awsClients.destroy();
|
|
76680
76759
|
}
|
|
@@ -76690,19 +76769,29 @@ async function eventsPruneCommand(stackName, options) {
|
|
|
76690
76769
|
async function confirmPrompt$2(prompt) {
|
|
76691
76770
|
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
76771
|
}
|
|
76693
|
-
/**
|
|
76772
|
+
/**
|
|
76773
|
+
* Human-readable run listing (newest first).
|
|
76774
|
+
*
|
|
76775
|
+
* Issue #2438: every `run.*` field here comes back out of
|
|
76776
|
+
* `deployments/index.json` via a plain `JSON.parse`, so its declared type is a
|
|
76777
|
+
* claim about the writer, not about the bytes on disk — each one goes through
|
|
76778
|
+
* {@link safeId}. `result` is sanitised BEFORE the colour decision so the
|
|
76779
|
+
* colour matches the text the user actually sees.
|
|
76780
|
+
*/
|
|
76694
76781
|
function printRunList(stackName, region, runs) {
|
|
76695
76782
|
const logger = getLogger();
|
|
76696
|
-
|
|
76783
|
+
const safeStack = safeId(stackName) || "<unrenderable>";
|
|
76784
|
+
logger.info(`${bold("Deployment runs for")} ${cyan(safeStack)} ${gray(`(${safeId(region) || "<unrenderable>"})`)}`);
|
|
76697
76785
|
if (runs.length === 0) {
|
|
76698
76786
|
logger.info(gray(" (no runs recorded)"));
|
|
76699
76787
|
return;
|
|
76700
76788
|
}
|
|
76701
76789
|
for (const run of runs) {
|
|
76702
|
-
const
|
|
76703
|
-
|
|
76790
|
+
const result = safeId(run.result) || "<unrenderable>";
|
|
76791
|
+
const resultColored = result === "SUCCEEDED" ? green(result) : result === "UNKNOWN" || result === "<unrenderable>" ? gray(result) : red(result);
|
|
76792
|
+
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
76793
|
}
|
|
76705
|
-
logger.info(gray(`\nUse 'cdkd events ${
|
|
76794
|
+
logger.info(gray(`\nUse 'cdkd events ${safeStack} --run <runId>' to read one run's events.`));
|
|
76706
76795
|
}
|
|
76707
76796
|
/** Human-readable single-run event stream (in recorded order). */
|
|
76708
76797
|
/**
|
|
@@ -76712,29 +76801,38 @@ function printRunList(stackName, region, runs) {
|
|
|
76712
76801
|
*/
|
|
76713
76802
|
function printRunEvents(stackName, region, runId, events) {
|
|
76714
76803
|
const logger = getLogger();
|
|
76715
|
-
logger.info(`${bold("Events for run")} ${cyan(runId)} ${gray(`(${stackName}, ${region})`)}`);
|
|
76804
|
+
logger.info(`${bold("Events for run")} ${cyan(safeId(runId) || "<unrenderable>")} ${gray(`(${safeId(stackName) || "<unrenderable>"}, ${safeId(region) || "<unrenderable>"})`)}`);
|
|
76716
76805
|
if (events.length === 0) {
|
|
76717
76806
|
logger.info(gray(" (no events)"));
|
|
76718
76807
|
return;
|
|
76719
76808
|
}
|
|
76720
76809
|
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.
|
|
76810
|
+
const parts = [gray(safeId(e.timestamp) || "<unrenderable>"), colorizeEventType(e.eventType)];
|
|
76811
|
+
if (e.logicalId) {
|
|
76812
|
+
const logicalId = safeId(e.logicalId) || "<unrenderable>";
|
|
76813
|
+
const resourceType = e.resourceType ? ` (${safeId(e.resourceType) || "<unrenderable>"})` : "";
|
|
76814
|
+
parts.push(`${logicalId}${resourceType}`);
|
|
76815
|
+
}
|
|
76816
|
+
if (e.operation) parts.push(gray(safeId(e.operation) || "<unrenderable>"));
|
|
76817
|
+
if (e.guard) parts.push(gray(`guard=${safeId(e.guard) || "<unrenderable>"}`));
|
|
76818
|
+
if (e.provisionedBy) parts.push(gray(`[${safeId(e.provisionedBy) || "<unrenderable>"}]`));
|
|
76819
|
+
if (e.command) parts.push(gray(safeId(e.command) || "<unrenderable>"));
|
|
76820
|
+
if (e.region) parts.push(gray(safeId(e.region) || "<unrenderable>"));
|
|
76821
|
+
if (e.cdkdVersion) parts.push(gray(`cdkd ${safeId(e.cdkdVersion) || "<unrenderable>"}`));
|
|
76822
|
+
if (e.result) {
|
|
76823
|
+
const result = safeId(e.result) || "<unrenderable>";
|
|
76824
|
+
parts.push(result === "SUCCEEDED" ? green(result) : result === "<unrenderable>" ? gray(result) : red(result));
|
|
76825
|
+
}
|
|
76730
76826
|
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}` : "")));
|
|
76827
|
+
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
76828
|
logger.info(` ${parts.join(" ")}`);
|
|
76733
|
-
if (e.reason) logger.info(` ${yellow(e.reason)}`);
|
|
76829
|
+
if (e.reason) logger.info(` ${yellow(safeText(e.reason) || "<unrenderable>")}`);
|
|
76734
76830
|
if (e.error) {
|
|
76735
|
-
const code = e.error.awsErrorCode ? ` (${e.error.awsErrorCode})` : "";
|
|
76736
|
-
const reqId = e.error.requestId ? gray(` requestId=${e.error.requestId}`) : "";
|
|
76737
|
-
|
|
76831
|
+
const code = e.error.awsErrorCode ? ` (${safeId(e.error.awsErrorCode) || "<unrenderable>"})` : "";
|
|
76832
|
+
const reqId = e.error.requestId ? gray(` requestId=${safeId(e.error.requestId) || "<unrenderable>"}`) : "";
|
|
76833
|
+
const name = safeText(e.error.name) || "<unrenderable>";
|
|
76834
|
+
const message = e.error.message ? `: ${safeText(e.error.message) || "<unrenderable>"}` : "";
|
|
76835
|
+
logger.info(` ${red(`${name}${code}${message}`)}${reqId}`);
|
|
76738
76836
|
}
|
|
76739
76837
|
}
|
|
76740
76838
|
}
|
|
@@ -76744,14 +76842,22 @@ function printRunEvents(stackName, region, runId, events) {
|
|
|
76744
76842
|
* Exported for unit-test coverage (matching `destroy-runner.ts`'s
|
|
76745
76843
|
* `PROTECTION_PROPERTY_BY_TYPE` convention) — it is pure, so testing it
|
|
76746
76844
|
* directly beats driving it through the renderer's log output.
|
|
76845
|
+
*
|
|
76846
|
+
* Issue #2438: the declared union is what the WRITER emits; the value handed
|
|
76847
|
+
* here was restored by `JSON.parse` from the stored stream and is therefore
|
|
76848
|
+
* arbitrary text. It is sanitised once, up front, and every arm below both
|
|
76849
|
+
* classifies AND colorizes the sanitised token — classifying the raw value
|
|
76850
|
+
* while printing the sanitised one would let the two disagree. The token is
|
|
76851
|
+
* a known-ASCII discriminator, so `safeId`'s positive allowlist loses nothing.
|
|
76747
76852
|
*/
|
|
76748
76853
|
function colorizeEventType(eventType) {
|
|
76749
|
-
|
|
76750
|
-
if (
|
|
76751
|
-
if (
|
|
76752
|
-
if (
|
|
76753
|
-
if (
|
|
76754
|
-
return
|
|
76854
|
+
const token = safeId(eventType) || "<unrenderable>";
|
|
76855
|
+
if (token.endsWith("FAILED")) return red(token);
|
|
76856
|
+
if (token.endsWith("SUCCEEDED") || token === "RUN_FINISHED") return green(token);
|
|
76857
|
+
if (token === "RESOURCE_SKIPPED") return yellow(token);
|
|
76858
|
+
if (token === "RESOURCE_GUARD_INDETERMINATE") return yellow(token);
|
|
76859
|
+
if (token.startsWith("ROLLBACK")) return yellow(token);
|
|
76860
|
+
return cyan(token);
|
|
76755
76861
|
}
|
|
76756
76862
|
/**
|
|
76757
76863
|
* Create the `cdkd events` command.
|
|
@@ -76780,11 +76886,11 @@ function createEventsCommand() {
|
|
|
76780
76886
|
* action therefore reads the merged view via `command.optsWithGlobals()`.
|
|
76781
76887
|
*/
|
|
76782
76888
|
function createEventsPruneCommand() {
|
|
76783
|
-
return new Command("prune").description("Delete old per-run deployment-event streams
|
|
76889
|
+
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
76890
|
const n = parseInt(v, 10);
|
|
76785
|
-
if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid --keep value "${v}": expected a non-negative integer.`);
|
|
76891
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`Invalid --keep value "${safeId(v)}": expected a non-negative integer.`);
|
|
76786
76892
|
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
|
|
76893
|
+
})).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
76894
|
}
|
|
76789
76895
|
|
|
76790
76896
|
//#endregion
|
|
@@ -82182,7 +82288,7 @@ async function pullImage(image, skipPull) {
|
|
|
82182
82288
|
try {
|
|
82183
82289
|
await runDockerForeground(["pull", image]);
|
|
82184
82290
|
} catch (err) {
|
|
82185
|
-
throw new DockerRunnerError(`docker pull ${image} failed: ${err
|
|
82291
|
+
throw new DockerRunnerError(`docker pull ${image} failed: ${describeDockerFailure(err, ["pull", image])}`);
|
|
82186
82292
|
}
|
|
82187
82293
|
return;
|
|
82188
82294
|
}
|
|
@@ -82191,8 +82297,8 @@ async function pullImage(image, skipPull) {
|
|
|
82191
82297
|
await runDockerStreaming(["pull", image], { streamLive: false });
|
|
82192
82298
|
} catch (err) {
|
|
82193
82299
|
const e = err;
|
|
82194
|
-
if (e.exitCode === void 0 || e.exitCode === null) throw new DockerRunnerError(`docker pull ${image} failed: ${
|
|
82195
|
-
const detail =
|
|
82300
|
+
if (e.exitCode === void 0 || e.exitCode === null) throw new DockerRunnerError(`docker pull ${image} failed: ${describeDockerFailure(err, ["pull", image])}`);
|
|
82301
|
+
const detail = describeDockerCapturedOutput(err, ["pull", image], "(no output)");
|
|
82196
82302
|
throw new DockerRunnerError(`docker pull ${image} exited with code ${e.exitCode}: ${detail}`);
|
|
82197
82303
|
}
|
|
82198
82304
|
}
|
|
@@ -82238,7 +82344,7 @@ async function runDetached(opts) {
|
|
|
82238
82344
|
}
|
|
82239
82345
|
args.push(opts.image, ...entryPointTail, ...opts.cmd);
|
|
82240
82346
|
const logger = getLogger().child("docker");
|
|
82241
|
-
logger.debug(`${getDockerCmd()} ${
|
|
82347
|
+
logger.debug(`${getDockerCmd()} ${redactDockerArgvValues(args).join(" ")}`);
|
|
82242
82348
|
if (collisions.length > 0) logger.warn(`Env var(s) ${collisions.map((k) => JSON.stringify(k)).join(", ")} share a name with a docker-client environment variable or have a malformed name (empty, or containing '=' / NUL), and were NOT passed to the container at all (a colliding name would hijack the docker client; a malformed name cannot form a valid environment variable). Rename the env var if the container needs it.`);
|
|
82243
82349
|
try {
|
|
82244
82350
|
const { stdout } = await execFileAsync$3(getDockerCmd(), args, {
|
|
@@ -82247,8 +82353,7 @@ async function runDetached(opts) {
|
|
|
82247
82353
|
});
|
|
82248
82354
|
return stdout.trim();
|
|
82249
82355
|
} catch (error) {
|
|
82250
|
-
|
|
82251
|
-
throw new DockerRunnerError(`docker run failed: ${err.stderr?.trim() || err.message || String(error)}`);
|
|
82356
|
+
throw new DockerRunnerError(`docker run failed: ${describeDockerFailure(error, args)}`);
|
|
82252
82357
|
}
|
|
82253
82358
|
}
|
|
82254
82359
|
/**
|
|
@@ -82280,16 +82385,16 @@ function streamLogs(containerId) {
|
|
|
82280
82385
|
async function removeContainer(containerId) {
|
|
82281
82386
|
if (!containerId) return;
|
|
82282
82387
|
const logger = getLogger().child("docker");
|
|
82388
|
+
const args = [
|
|
82389
|
+
"rm",
|
|
82390
|
+
"-f",
|
|
82391
|
+
containerId
|
|
82392
|
+
];
|
|
82283
82393
|
try {
|
|
82284
|
-
await execFileAsync$3(getDockerCmd(),
|
|
82285
|
-
"rm",
|
|
82286
|
-
"-f",
|
|
82287
|
-
containerId
|
|
82288
|
-
]);
|
|
82394
|
+
await execFileAsync$3(getDockerCmd(), args);
|
|
82289
82395
|
logger.debug(`Removed container ${containerId}`);
|
|
82290
82396
|
} catch (error) {
|
|
82291
|
-
|
|
82292
|
-
logger.debug(`docker rm -f ${containerId} failed: ${err.stderr || err.message || String(error)}`);
|
|
82397
|
+
logger.debug(`docker rm -f ${containerId} failed: ${describeDockerFailure(error, args)}`);
|
|
82293
82398
|
}
|
|
82294
82399
|
}
|
|
82295
82400
|
/**
|
|
@@ -82299,16 +82404,16 @@ async function removeContainer(containerId) {
|
|
|
82299
82404
|
*/
|
|
82300
82405
|
async function ensureDockerAvailable() {
|
|
82301
82406
|
const cmd = getDockerCmd();
|
|
82407
|
+
const args = [
|
|
82408
|
+
"version",
|
|
82409
|
+
"--format",
|
|
82410
|
+
"{{.Server.Version}}"
|
|
82411
|
+
];
|
|
82302
82412
|
try {
|
|
82303
|
-
await execFileAsync$3(cmd,
|
|
82304
|
-
"version",
|
|
82305
|
-
"--format",
|
|
82306
|
-
"{{.Server.Version}}"
|
|
82307
|
-
]);
|
|
82413
|
+
await execFileAsync$3(cmd, args);
|
|
82308
82414
|
} catch (error) {
|
|
82309
|
-
|
|
82310
|
-
|
|
82311
|
-
throw new DockerRunnerError(`${cmd} daemon is not reachable: ${err.stderr?.trim() || err.message || String(error)}. Start Docker Desktop / the docker daemon and retry.`);
|
|
82415
|
+
if (error.code === "ENOENT") throw new DockerRunnerError(`${cmd} is not installed or not on PATH. cdkd local invoke needs Docker (or a compatible CLI specified via CDK_DOCKER) — install it and retry.`);
|
|
82416
|
+
throw new DockerRunnerError(`${cmd} daemon is not reachable: ${describeDockerFailure(error, args)}. Start Docker Desktop / the docker daemon and retry.`);
|
|
82312
82417
|
}
|
|
82313
82418
|
}
|
|
82314
82419
|
/**
|
|
@@ -82339,25 +82444,12 @@ function pickFreePort() {
|
|
|
82339
82444
|
});
|
|
82340
82445
|
}
|
|
82341
82446
|
/**
|
|
82342
|
-
* AWS credential keys whose values must NOT be written to the debug log.
|
|
82343
|
-
* `forwardAwsEnv` / `assumeLambdaExecutionRole` (in `local-invoke.ts`)
|
|
82344
|
-
* push these via `-e <KEY>=<value>` flags into `runDetached`'s args
|
|
82345
|
-
* array, and `cdkd local invoke --verbose` would otherwise leak them
|
|
82346
|
-
* into stdout / log files. Only the matching `-e <KEY>=<value>` pair is
|
|
82347
|
-
* redacted; non-credential `-e KEY=val` entries pass through unchanged.
|
|
82348
|
-
*/
|
|
82349
|
-
const REDACTED_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
82350
|
-
"AWS_ACCESS_KEY_ID",
|
|
82351
|
-
"AWS_SECRET_ACCESS_KEY",
|
|
82352
|
-
"AWS_SESSION_TOKEN"
|
|
82353
|
-
]);
|
|
82354
|
-
/**
|
|
82355
82447
|
* Built-in sensitive env keys whose VALUES are always routed through
|
|
82356
82448
|
* docker's value-from-process-env form (`-e KEY` rather than
|
|
82357
|
-
* `-e KEY=value`) by {@link runDetached}.
|
|
82358
|
-
*
|
|
82359
|
-
*
|
|
82360
|
-
*
|
|
82449
|
+
* `-e KEY=value`) by {@link runDetached}. Stronger than any log-time
|
|
82450
|
+
* masking: the value is kept off the docker `run` argv entirely (and
|
|
82451
|
+
* therefore off `ps` / `/proc/<pid>/cmdline` as well as every rendering of
|
|
82452
|
+
* the argv) instead of only being hidden at display time. A caller
|
|
82361
82453
|
* may extend this set via the `sensitiveEnvKeys` option (used by the
|
|
82362
82454
|
* AgentCore local-invoke path to keep decrypted `--from-cfn-stack`
|
|
82363
82455
|
* SecureString SSM values off the argv).
|
|
@@ -82367,32 +82459,6 @@ const SENSITIVE_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
|
82367
82459
|
"AWS_SECRET_ACCESS_KEY",
|
|
82368
82460
|
"AWS_SESSION_TOKEN"
|
|
82369
82461
|
]);
|
|
82370
|
-
/**
|
|
82371
|
-
* Returns a copy of `args` with any `-e <KEY>=<value>` pair whose KEY is
|
|
82372
|
-
* in {@link REDACTED_ENV_KEYS} replaced with `-e <KEY>=***`. The actual
|
|
82373
|
-
* `args` passed to `spawn` are never mutated — this is for log output
|
|
82374
|
-
* only.
|
|
82375
|
-
*/
|
|
82376
|
-
function redactAwsCredentialsInArgs(args) {
|
|
82377
|
-
const out = [];
|
|
82378
|
-
for (let i = 0; i < args.length; i++) {
|
|
82379
|
-
const cur = args[i];
|
|
82380
|
-
const next = args[i + 1];
|
|
82381
|
-
if (cur === "-e" && typeof next === "string") {
|
|
82382
|
-
const eqIdx = next.indexOf("=");
|
|
82383
|
-
if (eqIdx > 0) {
|
|
82384
|
-
const key = next.substring(0, eqIdx);
|
|
82385
|
-
if (REDACTED_ENV_KEYS.has(key)) {
|
|
82386
|
-
out.push("-e", `${key}=***`);
|
|
82387
|
-
i++;
|
|
82388
|
-
continue;
|
|
82389
|
-
}
|
|
82390
|
-
}
|
|
82391
|
-
}
|
|
82392
|
-
out.push(cur);
|
|
82393
|
-
}
|
|
82394
|
-
return out;
|
|
82395
|
-
}
|
|
82396
82462
|
|
|
82397
82463
|
//#endregion
|
|
82398
82464
|
//#region src/local/docker-image-builder.ts
|
|
@@ -82588,10 +82654,11 @@ async function pullEcrImage(imageUri, options) {
|
|
|
82588
82654
|
ecr.destroy();
|
|
82589
82655
|
}
|
|
82590
82656
|
logger.info(`Pulling ${canonicalUri}...`);
|
|
82657
|
+
const pullArgs = ["pull", canonicalUri];
|
|
82591
82658
|
try {
|
|
82592
|
-
await runDockerForeground(
|
|
82659
|
+
await runDockerForeground(pullArgs);
|
|
82593
82660
|
} catch (err) {
|
|
82594
|
-
throw new LocalInvokeBuildError$1(`docker pull ${canonicalUri} failed: ${err
|
|
82661
|
+
throw new LocalInvokeBuildError$1(`docker pull ${canonicalUri} failed: ${describeDockerFailure(err, pullArgs)}`);
|
|
82595
82662
|
}
|
|
82596
82663
|
return canonicalUri;
|
|
82597
82664
|
}
|
|
@@ -82640,17 +82707,17 @@ async function ecrLogin(client, accountId, region) {
|
|
|
82640
82707
|
const [username, password] = Buffer.from(authData.authorizationToken, "base64").toString().split(":");
|
|
82641
82708
|
if (!username || password === void 0) throw new LocalInvokeBuildError$1("ECR authorization token has unexpected shape (missing username/password)");
|
|
82642
82709
|
const endpoint = authData.proxyEndpoint || `https://${accountId}.dkr.ecr.${region}.${ecrUrlSuffix(region)}`;
|
|
82710
|
+
const loginArgs = [
|
|
82711
|
+
"login",
|
|
82712
|
+
"--username",
|
|
82713
|
+
username,
|
|
82714
|
+
"--password-stdin",
|
|
82715
|
+
endpoint
|
|
82716
|
+
];
|
|
82643
82717
|
try {
|
|
82644
|
-
await runDockerStreaming(
|
|
82645
|
-
"login",
|
|
82646
|
-
"--username",
|
|
82647
|
-
username,
|
|
82648
|
-
"--password-stdin",
|
|
82649
|
-
endpoint
|
|
82650
|
-
], { input: password });
|
|
82718
|
+
await runDockerStreaming(loginArgs, { input: password });
|
|
82651
82719
|
} catch (err) {
|
|
82652
|
-
|
|
82653
|
-
throw new LocalInvokeBuildError$1(`ECR login failed: ${formatDockerLoginError(e.stderr || e.message || String(err), endpoint)}`);
|
|
82720
|
+
throw new LocalInvokeBuildError$1(`ECR login failed: ${formatDockerLoginError(describeDockerFailure(err, loginArgs), endpoint)}`);
|
|
82654
82721
|
}
|
|
82655
82722
|
}
|
|
82656
82723
|
/**
|
|
@@ -85373,19 +85440,19 @@ async function createNetworkAndSidecar(args) {
|
|
|
85373
85440
|
const { networkName, cidr, sidecarIp, credentials, cluster, skipPull } = args;
|
|
85374
85441
|
await pullImage(METADATA_ENDPOINT_IMAGE, skipPull);
|
|
85375
85442
|
logger.info(`Creating docker network ${networkName} (subnet ${cidr})...`);
|
|
85443
|
+
const createArgs = [
|
|
85444
|
+
"network",
|
|
85445
|
+
"create",
|
|
85446
|
+
"--driver",
|
|
85447
|
+
"bridge",
|
|
85448
|
+
"--subnet",
|
|
85449
|
+
cidr,
|
|
85450
|
+
networkName
|
|
85451
|
+
];
|
|
85376
85452
|
try {
|
|
85377
|
-
await execFileAsync$2(getDockerCmd(),
|
|
85378
|
-
"network",
|
|
85379
|
-
"create",
|
|
85380
|
-
"--driver",
|
|
85381
|
-
"bridge",
|
|
85382
|
-
"--subnet",
|
|
85383
|
-
cidr,
|
|
85384
|
-
networkName
|
|
85385
|
-
]);
|
|
85453
|
+
await execFileAsync$2(getDockerCmd(), createArgs);
|
|
85386
85454
|
} catch (err) {
|
|
85387
|
-
|
|
85388
|
-
throw new DockerRunnerError(`docker network create failed: ${e.stderr?.trim() || e.message || String(err)}. Hint: another cdkd run may already own subnet ${cidr}; wait for it to finish, or remove the leftover network with \`docker network ls\` + \`docker network rm\`. \`cdkd local start-service\` shares one network across every service in the run; bare \`cdkd local run-task\` uses a per-task network so only one run can be active at a time.`);
|
|
85455
|
+
throw new DockerRunnerError(`docker network create failed: ${describeDockerFailure(err, createArgs)}. Hint: another cdkd run may already own subnet ${cidr}; wait for it to finish, or remove the leftover network with \`docker network ls\` + \`docker network rm\`. \`cdkd local start-service\` shares one network across every service in the run; bare \`cdkd local run-task\` uses a per-task network so only one run can be active at a time.`);
|
|
85389
85456
|
}
|
|
85390
85457
|
const sidecarArgs = [
|
|
85391
85458
|
"run",
|
|
@@ -85418,8 +85485,7 @@ async function createNetworkAndSidecar(args) {
|
|
|
85418
85485
|
return stdout.trim();
|
|
85419
85486
|
} catch (err) {
|
|
85420
85487
|
await destroyNetworkOnly(networkName);
|
|
85421
|
-
|
|
85422
|
-
throw new DockerRunnerError(`Failed to start metadata-endpoints sidecar: ${e.stderr?.trim() || e.message || String(err)}`);
|
|
85488
|
+
throw new DockerRunnerError(`Failed to start metadata-endpoints sidecar: ${describeDockerFailure(err, sidecarArgs)}`);
|
|
85423
85489
|
}
|
|
85424
85490
|
}
|
|
85425
85491
|
/**
|
|
@@ -85481,16 +85547,16 @@ async function destroyTaskNetwork(net) {
|
|
|
85481
85547
|
async function destroyNetworkOnly(networkName) {
|
|
85482
85548
|
if (!networkName) return;
|
|
85483
85549
|
const logger = getLogger().child("ecs-network");
|
|
85550
|
+
const rmArgs = [
|
|
85551
|
+
"network",
|
|
85552
|
+
"rm",
|
|
85553
|
+
networkName
|
|
85554
|
+
];
|
|
85484
85555
|
try {
|
|
85485
|
-
await execFileAsync$2(getDockerCmd(),
|
|
85486
|
-
"network",
|
|
85487
|
-
"rm",
|
|
85488
|
-
networkName
|
|
85489
|
-
]);
|
|
85556
|
+
await execFileAsync$2(getDockerCmd(), rmArgs);
|
|
85490
85557
|
logger.debug(`Removed docker network ${networkName}`);
|
|
85491
85558
|
} catch (err) {
|
|
85492
|
-
|
|
85493
|
-
logger.debug(`docker network rm ${networkName} failed: ${e.stderr || e.message || String(err)}`);
|
|
85559
|
+
logger.debug(`docker network rm ${networkName} failed: ${describeDockerFailure(err, rmArgs)}`);
|
|
85494
85560
|
}
|
|
85495
85561
|
}
|
|
85496
85562
|
|
|
@@ -85666,30 +85732,38 @@ async function cleanupEcsRun(state, options) {
|
|
|
85666
85732
|
state.logStoppers = [];
|
|
85667
85733
|
if (!options.keepRunning) {
|
|
85668
85734
|
for (const c of state.startedContainers) {
|
|
85735
|
+
const stopArgs = dockerStopArgs(c.id, CLEANUP_STOP_GRACE_SECONDS);
|
|
85669
85736
|
try {
|
|
85670
|
-
await stopContainer(c.id,
|
|
85737
|
+
await stopContainer(c.id, CLEANUP_STOP_GRACE_SECONDS);
|
|
85671
85738
|
} catch (err) {
|
|
85672
|
-
logger.debug(`docker stop ${c.id} failed: ${err
|
|
85739
|
+
logger.debug(`docker stop ${c.id} failed: ${describeDockerFailure(err, stopArgs)}`);
|
|
85673
85740
|
}
|
|
85674
85741
|
try {
|
|
85675
85742
|
await removeContainer(c.id);
|
|
85676
85743
|
} catch (err) {
|
|
85677
|
-
logger.debug(`docker rm -f ${c.id} failed: ${err
|
|
85744
|
+
logger.debug(`docker rm -f ${c.id} failed: ${describeDockerFailure(err, [
|
|
85745
|
+
"rm",
|
|
85746
|
+
"-f",
|
|
85747
|
+
c.id
|
|
85748
|
+
])}`);
|
|
85678
85749
|
}
|
|
85679
85750
|
}
|
|
85680
85751
|
state.startedContainers = [];
|
|
85681
85752
|
}
|
|
85682
85753
|
if (state.network && !state.network.ownedByCaller) await destroyTaskNetwork(state.network);
|
|
85683
85754
|
state.network = void 0;
|
|
85684
|
-
for (const v of state.dockerVolumeNames)
|
|
85685
|
-
|
|
85755
|
+
for (const v of state.dockerVolumeNames) {
|
|
85756
|
+
const rmArgs = [
|
|
85686
85757
|
"volume",
|
|
85687
85758
|
"rm",
|
|
85688
85759
|
v
|
|
85689
|
-
]
|
|
85690
|
-
|
|
85691
|
-
|
|
85692
|
-
|
|
85760
|
+
];
|
|
85761
|
+
try {
|
|
85762
|
+
await execFileAsync$1(getDockerCmd(), rmArgs);
|
|
85763
|
+
logger.debug(`Removed docker volume ${v}`);
|
|
85764
|
+
} catch (err) {
|
|
85765
|
+
logger.debug(`docker volume rm ${v} failed: ${describeDockerFailure(err, rmArgs)}`);
|
|
85766
|
+
}
|
|
85693
85767
|
}
|
|
85694
85768
|
state.dockerVolumeNames = [];
|
|
85695
85769
|
}
|
|
@@ -85783,8 +85857,7 @@ async function runEcsTask(task, options, state) {
|
|
|
85783
85857
|
});
|
|
85784
85858
|
id = stdout.trim();
|
|
85785
85859
|
} catch (err) {
|
|
85786
|
-
|
|
85787
|
-
throw new DockerRunnerError(`docker run failed for container '${container.name}': ${e.stderr?.trim() || e.message || String(err)}`);
|
|
85860
|
+
throw new DockerRunnerError(`docker run failed for container '${container.name}': ${describeDockerFailure(err, args)}`);
|
|
85788
85861
|
}
|
|
85789
85862
|
state.startedContainers.push({
|
|
85790
85863
|
name: container.name,
|
|
@@ -85887,14 +85960,15 @@ async function waitForContainerHealthy(containerId, displayName) {
|
|
|
85887
85960
|
const logger = getLogger().child("ecs-runner");
|
|
85888
85961
|
const deadline = Date.now() + 300 * 1e3;
|
|
85889
85962
|
let lastStatus = "";
|
|
85963
|
+
const inspectArgs = [
|
|
85964
|
+
"inspect",
|
|
85965
|
+
"--format",
|
|
85966
|
+
"{{.State.Health.Status}}",
|
|
85967
|
+
containerId
|
|
85968
|
+
];
|
|
85890
85969
|
while (Date.now() < deadline) {
|
|
85891
85970
|
try {
|
|
85892
|
-
const { stdout } = await execFileAsync$1(getDockerCmd(),
|
|
85893
|
-
"inspect",
|
|
85894
|
-
"--format",
|
|
85895
|
-
"{{.State.Health.Status}}",
|
|
85896
|
-
containerId
|
|
85897
|
-
]);
|
|
85971
|
+
const { stdout } = await execFileAsync$1(getDockerCmd(), inspectArgs);
|
|
85898
85972
|
const status = stdout.trim();
|
|
85899
85973
|
if (status !== lastStatus) {
|
|
85900
85974
|
logger.debug(`Container '${displayName}' health status: ${status}`);
|
|
@@ -85904,30 +85978,36 @@ async function waitForContainerHealthy(containerId, displayName) {
|
|
|
85904
85978
|
if (status === "unhealthy") throw new EcsTaskRunnerError(`Container '${displayName}' health status is 'unhealthy'; aborting before dependents start.`);
|
|
85905
85979
|
} catch (err) {
|
|
85906
85980
|
if (err instanceof EcsTaskRunnerError) throw err;
|
|
85907
|
-
logger.debug(`docker inspect on '${displayName}' failed: ${err
|
|
85981
|
+
logger.debug(`docker inspect on '${displayName}' failed: ${describeDockerFailure(err, inspectArgs)}`);
|
|
85908
85982
|
}
|
|
85909
85983
|
await sleep(1e3);
|
|
85910
85984
|
}
|
|
85911
85985
|
throw new EcsTaskRunnerError(`Container '${displayName}' did not become healthy within 5 minutes.`);
|
|
85912
85986
|
}
|
|
85913
85987
|
async function waitForContainerExit(containerId) {
|
|
85988
|
+
const waitArgs = ["wait", containerId];
|
|
85914
85989
|
try {
|
|
85915
|
-
const { stdout } = await execFileAsync$1(getDockerCmd(),
|
|
85990
|
+
const { stdout } = await execFileAsync$1(getDockerCmd(), waitArgs, { maxBuffer: 1024 * 1024 });
|
|
85916
85991
|
const code = Number.parseInt(stdout.trim(), 10);
|
|
85917
85992
|
return Number.isFinite(code) ? code : 1;
|
|
85918
85993
|
} catch (err) {
|
|
85919
|
-
|
|
85920
|
-
throw new DockerRunnerError(`docker wait failed: ${e.stderr?.trim() || e.message || String(err)}`);
|
|
85994
|
+
throw new DockerRunnerError(`docker wait failed: ${describeDockerFailure(err, waitArgs)}`);
|
|
85921
85995
|
}
|
|
85922
85996
|
}
|
|
85997
|
+
/** Seconds `cleanupEcsRun` gives a container to stop before `docker rm -f`. */
|
|
85998
|
+
const CLEANUP_STOP_GRACE_SECONDS = 10;
|
|
85999
|
+
/** The exact `docker stop` argv, so callers redact with the argv actually spawned. */
|
|
86000
|
+
function dockerStopArgs(containerId, graceSeconds) {
|
|
86001
|
+
return [
|
|
86002
|
+
"stop",
|
|
86003
|
+
"-t",
|
|
86004
|
+
String(graceSeconds),
|
|
86005
|
+
containerId
|
|
86006
|
+
];
|
|
86007
|
+
}
|
|
85923
86008
|
async function stopContainer(containerId, graceSeconds) {
|
|
85924
86009
|
try {
|
|
85925
|
-
await execFileAsync$1(getDockerCmd(),
|
|
85926
|
-
"stop",
|
|
85927
|
-
"-t",
|
|
85928
|
-
String(graceSeconds),
|
|
85929
|
-
containerId
|
|
85930
|
-
]);
|
|
86010
|
+
await execFileAsync$1(getDockerCmd(), dockerStopArgs(containerId, graceSeconds));
|
|
85931
86011
|
} catch {}
|
|
85932
86012
|
}
|
|
85933
86013
|
function sleep(ms) {
|
|
@@ -86009,17 +86089,19 @@ async function prepareOneImage(task, container, options) {
|
|
|
86009
86089
|
const actualTag = await buildDockerImage(asset, cdkOutDir, {
|
|
86010
86090
|
tag,
|
|
86011
86091
|
...options.platformOverride !== void 0 && { platform: options.platformOverride },
|
|
86012
|
-
wrapError: (stderr) => new LocalInvokeBuildError$1(`docker build failed for ECS container '${container.name}' (${asset.source.directory ?? asset.source.executable
|
|
86092
|
+
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}`)
|
|
86013
86093
|
});
|
|
86014
|
-
if (actualTag !== tag)
|
|
86015
|
-
|
|
86094
|
+
if (actualTag !== tag) {
|
|
86095
|
+
const tagArgs = [
|
|
86016
86096
|
"tag",
|
|
86017
86097
|
actualTag,
|
|
86018
86098
|
tag
|
|
86019
|
-
]
|
|
86020
|
-
|
|
86021
|
-
|
|
86022
|
-
|
|
86099
|
+
];
|
|
86100
|
+
try {
|
|
86101
|
+
await runDockerStreaming(tagArgs);
|
|
86102
|
+
} catch (err) {
|
|
86103
|
+
throw new LocalInvokeBuildError$1(`docker tag failed re-tagging '${actualTag}' → '${tag}' for ECS container '${container.name}': ${describeDockerFailure(err, tagArgs)}`);
|
|
86104
|
+
}
|
|
86023
86105
|
}
|
|
86024
86106
|
return tag;
|
|
86025
86107
|
}
|
|
@@ -86051,8 +86133,7 @@ async function realizeDockerVolumes(volumes, state) {
|
|
|
86051
86133
|
state.dockerVolumeNames.push(dockerVolumeName);
|
|
86052
86134
|
logger.debug(`Created docker volume ${dockerVolumeName} for task volume '${v.name}'`);
|
|
86053
86135
|
} catch (err) {
|
|
86054
|
-
|
|
86055
|
-
throw new DockerRunnerError(`docker volume create failed for '${v.name}': ${e.stderr?.trim() || e.message || String(err)}`);
|
|
86136
|
+
throw new DockerRunnerError(`docker volume create failed for '${v.name}': ${describeDockerFailure(err, args)}`);
|
|
86056
86137
|
}
|
|
86057
86138
|
out.set(v.name, {
|
|
86058
86139
|
...v,
|
|
@@ -86778,40 +86859,37 @@ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
|
|
|
86778
86859
|
const logger = getLogger();
|
|
86779
86860
|
const dockerCmd = getDockerCmd();
|
|
86780
86861
|
let workdir;
|
|
86862
|
+
const inspectArgs = [
|
|
86863
|
+
"inspect",
|
|
86864
|
+
"--format",
|
|
86865
|
+
"{{.Config.WorkingDir}}",
|
|
86866
|
+
containerId
|
|
86867
|
+
];
|
|
86781
86868
|
try {
|
|
86782
|
-
const { stdout } = await execFileAsync(dockerCmd,
|
|
86783
|
-
"inspect",
|
|
86784
|
-
"--format",
|
|
86785
|
-
"{{.Config.WorkingDir}}",
|
|
86786
|
-
containerId
|
|
86787
|
-
]);
|
|
86869
|
+
const { stdout } = await execFileAsync(dockerCmd, inspectArgs);
|
|
86788
86870
|
workdir = stdout.trim() || "/";
|
|
86789
86871
|
} catch (err) {
|
|
86790
|
-
throw new CdkdError(`softReloadAgentContainer: docker inspect of container '${containerId}' failed: ${
|
|
86872
|
+
throw new CdkdError(`softReloadAgentContainer: docker inspect of container '${containerId}' failed: ${describeDockerExecFailure(err, inspectArgs)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_INSPECT_FAILED");
|
|
86791
86873
|
}
|
|
86792
86874
|
const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
|
|
86793
86875
|
logger.info(`Soft-reload: docker cp ${newAssetSourceDir} -> ${containerId}:${workdirDest}; restart.`);
|
|
86876
|
+
const cpArgs = [
|
|
86877
|
+
"cp",
|
|
86878
|
+
`${newAssetSourceDir}/.`,
|
|
86879
|
+
`${containerId}:${workdirDest}`
|
|
86880
|
+
];
|
|
86794
86881
|
try {
|
|
86795
|
-
await execFileAsync(dockerCmd,
|
|
86796
|
-
"cp",
|
|
86797
|
-
`${newAssetSourceDir}/.`,
|
|
86798
|
-
`${containerId}:${workdirDest}`
|
|
86799
|
-
], { maxBuffer: 64 * 1024 * 1024 });
|
|
86882
|
+
await execFileAsync(dockerCmd, cpArgs, { maxBuffer: 64 * 1024 * 1024 });
|
|
86800
86883
|
} catch (err) {
|
|
86801
|
-
throw new CdkdError(`softReloadAgentContainer: docker cp into '${containerId}:${workdir}' failed: ${
|
|
86884
|
+
throw new CdkdError(`softReloadAgentContainer: docker cp into '${containerId}:${workdir}' failed: ${describeDockerExecFailure(err, cpArgs)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_CP_FAILED");
|
|
86802
86885
|
}
|
|
86886
|
+
const restartArgs = ["restart", containerId];
|
|
86803
86887
|
try {
|
|
86804
|
-
await execFileAsync(dockerCmd,
|
|
86888
|
+
await execFileAsync(dockerCmd, restartArgs);
|
|
86805
86889
|
} catch (err) {
|
|
86806
|
-
throw new CdkdError(`softReloadAgentContainer: docker restart of '${containerId}' failed: ${
|
|
86890
|
+
throw new CdkdError(`softReloadAgentContainer: docker restart of '${containerId}' failed: ${describeDockerExecFailure(err, restartArgs)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_RESTART_FAILED");
|
|
86807
86891
|
}
|
|
86808
86892
|
}
|
|
86809
|
-
function describeExecError(err) {
|
|
86810
|
-
if (!(err instanceof Error)) return String(err);
|
|
86811
|
-
const stderr = err.stderr;
|
|
86812
|
-
const stderrText = typeof stderr === "string" ? stderr.trim() : stderr instanceof Buffer ? stderr.toString("utf8").trim() : "";
|
|
86813
|
-
return stderrText ? `${err.message}\n${stderrText}` : err.message;
|
|
86814
|
-
}
|
|
86815
86893
|
/**
|
|
86816
86894
|
* Build the per-firing classifier context for the agent container. Mirrors the
|
|
86817
86895
|
* ECS service emulator's `loadAssetContextForTarget` shape: returns `undefined`
|
|
@@ -88049,6 +88127,123 @@ function createLocalStartAgentCoreCommand$1() {
|
|
|
88049
88127
|
//#endregion
|
|
88050
88128
|
//#region src/cli/commands/local-start-alb.ts
|
|
88051
88129
|
/**
|
|
88130
|
+
* Every distinct Lambda logical id a resolved front-door plan forwards to,
|
|
88131
|
+
* sorted so the warning text is deterministic.
|
|
88132
|
+
*
|
|
88133
|
+
* Walks BOTH action slots — a listener's `defaultAction` and each of its
|
|
88134
|
+
* rules' `action` — because either one can carry a `TargetType: lambda`
|
|
88135
|
+
* target group and an ALB that reaches a Lambda only through a
|
|
88136
|
+
* `path-pattern` rule is the common shape (`/api/*` to a Lambda, everything
|
|
88137
|
+
* else to the ECS service). Mirrors cdk-local's own `collectAlbLambdaTargets`
|
|
88138
|
+
* walk, one layer later: this reads the QUALIFIED plan the strategy returns,
|
|
88139
|
+
* where a Lambda target already carries its resolved `lambda.logicalId`.
|
|
88140
|
+
*/
|
|
88141
|
+
function collectLambdaTargetLogicalIds(frontDoor) {
|
|
88142
|
+
if (!frontDoor) return [];
|
|
88143
|
+
const ids = /* @__PURE__ */ new Set();
|
|
88144
|
+
const fromAction = (action) => {
|
|
88145
|
+
if (action?.kind !== "forward") return;
|
|
88146
|
+
for (const target of action.targets) if (target.kind === "lambda") ids.add(target.lambda.logicalId);
|
|
88147
|
+
};
|
|
88148
|
+
for (const listener of frontDoor.listeners) {
|
|
88149
|
+
fromAction(listener.defaultAction);
|
|
88150
|
+
for (const rule of listener.rules) fromAction(rule.action);
|
|
88151
|
+
}
|
|
88152
|
+
return [...ids].sort();
|
|
88153
|
+
}
|
|
88154
|
+
/**
|
|
88155
|
+
* Wrap an ALB {@link EmulatorStrategy} so that, under `--from-state`, a plan
|
|
88156
|
+
* carrying a `TargetType: lambda` target group emits a WARNING naming the
|
|
88157
|
+
* affected Lambdas — because the flag does not reach their container
|
|
88158
|
+
* environment.
|
|
88159
|
+
*
|
|
88160
|
+
* ## Why a warning and not a refusal
|
|
88161
|
+
*
|
|
88162
|
+
* `cdkd local start-cloudfront` REFUSES cdkd's state flags outright
|
|
88163
|
+
* ([#2528](https://github.com/go-to-k/cdkd/issues/2528)) because NO consumer on
|
|
88164
|
+
* that command's path reads a host-registered state source. `start-alb` is
|
|
88165
|
+
* partial, not total, so the same remedy would delete a working capability:
|
|
88166
|
+
*
|
|
88167
|
+
* - **ECS service targets DO honor `--from-state`.** `bootOneTarget` /
|
|
88168
|
+
* `rollOneTarget` (`cdk-local@0.147.7`, `dist/local-studio-BBtUAVNy.js`
|
|
88169
|
+
* `:26474` / `:26422`) hand `createLocalStateProvider` the FULL options bag,
|
|
88170
|
+
* so cdkd's `fromState` factory is selected and the task containers'
|
|
88171
|
+
* images / env / secrets / volumes resolve against S3 state.
|
|
88172
|
+
* - **Lambda target groups do NOT.** `resolveAlbLambdaTargetEnv` (`:26614`)
|
|
88173
|
+
* rebuilds the bag it hands the shared `resolveLambdaContainerEnv` as a
|
|
88174
|
+
* SIX-KEY allow-list — `fromCfnStack` / `assumeRole` / `region` / `profile` /
|
|
88175
|
+
* `stackRegion` / `envVars` (`:26619-26626`) — dropping `fromState` /
|
|
88176
|
+
* `stateBucket` / `statePrefix`. It forwards `extraStateProviders` faithfully,
|
|
88177
|
+
* but the dispatcher activates an extra provider only when `options[key]` is
|
|
88178
|
+
* truthy (`:4894`), and the key it looks for is exactly the one the bag no
|
|
88179
|
+
* longer carries. So cdkd's factory is registered and never selected, and the
|
|
88180
|
+
* Lambda boots with its intrinsics dropped — one WARN per variable, none of
|
|
88181
|
+
* which says the flag was the problem.
|
|
88182
|
+
*
|
|
88183
|
+
* The fix is upstream (two arguments in `resolveAlbLambdaTargetEnv`); until it
|
|
88184
|
+
* lands, this makes the partiality LOUD at boot instead of leaving the user to
|
|
88185
|
+
* read "Environment variable X contains a CloudFormation intrinsic and was
|
|
88186
|
+
* dropped" as a state problem. `--from-cfn-stack` survives the allow-list and
|
|
88187
|
+
* therefore reaches both target kinds, which is what the message points at.
|
|
88188
|
+
*
|
|
88189
|
+
* ## Mechanics
|
|
88190
|
+
*
|
|
88191
|
+
* `runEcsServiceEmulator` logs every string in the `warnings` array
|
|
88192
|
+
* `strategy.resolveBoots` returns, so appending to it is the whole wiring — no
|
|
88193
|
+
* second synth, since the front-door plan is already resolved. Returns the
|
|
88194
|
+
* strategy UNCHANGED when `--from-state` is absent, so the non-state path
|
|
88195
|
+
* carries no wrapper at all.
|
|
88196
|
+
*
|
|
88197
|
+
* The warning REPEATS on every `--watch` reload, because `resolveBoots` is
|
|
88198
|
+
* re-run per reload (`:26264`) and its warnings re-logged. That matches how
|
|
88199
|
+
* cdk-local's own ALB resolution warnings behave on the same path, which is
|
|
88200
|
+
* the whole reason: repeating is what the surrounding output already does.
|
|
88201
|
+
* It is NOT because a reload could introduce a new Lambda target — the reload
|
|
88202
|
+
* discards `frontDoor` and `buildFrontDoor` runs only at boot (`:26110`), so
|
|
88203
|
+
* a Lambda added mid-`--watch` is never stood up at all. An earlier revision
|
|
88204
|
+
* of this comment argued the second, false thing.
|
|
88205
|
+
*/
|
|
88206
|
+
function warnUnresolvedLambdaTargetEnv(strategy, options) {
|
|
88207
|
+
if (options.fromState !== true) return strategy;
|
|
88208
|
+
return {
|
|
88209
|
+
...strategy,
|
|
88210
|
+
resolveBoots: (stacks, chosenTargets) => {
|
|
88211
|
+
const resolved = strategy.resolveBoots(stacks, chosenTargets);
|
|
88212
|
+
const lambdaIds = collectLambdaTargetLogicalIds(resolved.frontDoor);
|
|
88213
|
+
if (lambdaIds.length === 0) return resolved;
|
|
88214
|
+
const ecsNote = resolved.boots.length > 0 ? "The ECS service targets behind this ALB DO honor --from-state. " : "";
|
|
88215
|
+
return {
|
|
88216
|
+
...resolved,
|
|
88217
|
+
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)."]
|
|
88218
|
+
};
|
|
88219
|
+
}
|
|
88220
|
+
};
|
|
88221
|
+
}
|
|
88222
|
+
/**
|
|
88223
|
+
* The `EmulatorStrategy` `cdkd local start-alb` runs with: cdk-local's ALB
|
|
88224
|
+
* strategy, decorated by {@link warnUnresolvedLambdaTargetEnv}.
|
|
88225
|
+
*
|
|
88226
|
+
* Extracted from the command's action for ONE reason, and it is the finding
|
|
88227
|
+
* that produced it: with the composition written inline in the `.action(...)`
|
|
88228
|
+
* closure, deleting the decorator at its only wiring point left the entire
|
|
88229
|
+
* unit suite green (measured in review — 29/29). The decorator's own cases
|
|
88230
|
+
* exercise it directly, so they cannot see a call site that stopped calling
|
|
88231
|
+
* it. A named export gives that seam a subject a test can hold.
|
|
88232
|
+
*
|
|
88233
|
+
* The seam is fenced BEHAVIOURALLY, in
|
|
88234
|
+
* `tests/unit/cli/local-start-alb-wiring.test.ts`, which runs the real command
|
|
88235
|
+
* and drives whatever object the action hands the engine. A source-shape
|
|
88236
|
+
* assertion was tried first and defeated in review round 3 by hoisting
|
|
88237
|
+
* `albStrategy(options)` into a local one statement up: its positive half was
|
|
88238
|
+
* satisfied by the mutation's own COMMENT and its negative half could not
|
|
88239
|
+
* cross the `)` the hoist introduced. That case is deleted rather than
|
|
88240
|
+
* tightened — the behavioural one catches strictly more, and a fence a comment
|
|
88241
|
+
* can satisfy reads as coverage while providing none.
|
|
88242
|
+
*/
|
|
88243
|
+
function buildAlbEmulatorStrategy(options) {
|
|
88244
|
+
return warnUnresolvedLambdaTargetEnv(albStrategy(options), options);
|
|
88245
|
+
}
|
|
88246
|
+
/**
|
|
88052
88247
|
* `cdkl start-alb <Stack/Alb>` — Issue #86 v1. Names an
|
|
88053
88248
|
* `AWS::ElasticLoadBalancingV2::LoadBalancer`, discovers the ECS service(s)
|
|
88054
88249
|
* behind its HTTP `forward` listeners, boots their replicas, and stands up a
|
|
@@ -88056,8 +88251,8 @@ function createLocalStartAgentCoreCommand$1() {
|
|
|
88056
88251
|
* The symmetric ALB counterpart of `start-api`.
|
|
88057
88252
|
*/
|
|
88058
88253
|
function createLocalStartAlbCommand() {
|
|
88059
|
-
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) => {
|
|
88060
|
-
await runEcsServiceEmulator(targets, options,
|
|
88254
|
+
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) => {
|
|
88255
|
+
await runEcsServiceEmulator(targets, options, buildAlbEmulatorStrategy(options), cdkdExtraStateProviders);
|
|
88061
88256
|
}));
|
|
88062
88257
|
addAlbSpecificOptions(cmd);
|
|
88063
88258
|
return adoptDeprecatedRegionFlag(addCommonEcsServiceOptions(cmd));
|
|
@@ -89128,4 +89323,4 @@ function buildProgram() {
|
|
|
89128
89323
|
|
|
89129
89324
|
//#endregion
|
|
89130
89325
|
export { buildProgram };
|
|
89131
|
-
//# sourceMappingURL=program-
|
|
89326
|
+
//# sourceMappingURL=program-dEYAo80R.js.map
|