@go-to-k/cdkd 0.290.27 → 0.290.29
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-Dn1f0SJi.js → asg-provider-Dwnb3CES.js} +3 -3
- package/dist/{asg-provider-Dn1f0SJi.js.map → asg-provider-Dwnb3CES.js.map} +1 -1
- package/dist/cli.js +2 -2
- package/dist/{deploy-engine-CA70_Rji.js → deploy-engine-Bcwb9_mO.js} +118 -12
- package/dist/deploy-engine-Bcwb9_mO.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{logger-CkHVgzLx.js → logger-6LP1UiFA.js} +18 -2
- package/dist/logger-6LP1UiFA.js.map +1 -0
- package/dist/{program-dYPgGoOv.js → program-PCCyP9-O.js} +585 -123
- package/dist/{program-dYPgGoOv.js.map → program-PCCyP9-O.js.map} +1 -1
- package/dist/{version-CWpPiHCn.js → version-snXN__f1.js} +2 -2
- package/dist/{version-CWpPiHCn.js.map → version-snXN__f1.js.map} +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-CA70_Rji.js.map +0 -1
- package/dist/logger-CkHVgzLx.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $ as secretBearing, $n as recordMaskOnlyValue, $r as
|
|
2
|
-
import { C as
|
|
1
|
+
import { $ as secretBearing, $n as recordMaskOnlyValue, $r as AssetManifestLoader, $t as s3BucketArn, A as extractDeploymentEventError, Ai as getAwsClients, An as refuseMalformedState, Ar as BOOTSTRAP_MARKER_PREFIX, At as isImpossibleEmptyStoredAttribute, B as toCfnInteger, Bi as IntrinsicResolutionRefusalError, Bn as STATE_SOURCED_BASELINE_RULES, Br as buildDockerImage, Bt as coerceCfnBoolean, C as buildFinalSnapshotIdentifier, Ci as canonicalizeRegion, Cn as producerRecordKey, Cr as stringifyValue, Ct as isTerminationProtectionPropagationError, D as refusesFinalSnapshot, Dn as refuseMalformedOutputsForDestroy, Dr as loadPublishableAssetManifest, Dt as cfnRefValueFromPhysicalId, E as isFinalSnapshotError, Ei as processStackMessages, En as refuseMalformedOutputs, Er as createAssetRedirectResolver, Et as carriesFetchableDynamicReference, F as isStatefulRecreateTargetSync, Fn as findSilentDropProperties, Fr as parseBootstrapMarker, Ft as isSettledInstanceState, G as gray, Gi as NestedStackChildDirectDestroyError, Gn as createSecretMasker, Gr as formatDockerLoginError, Gt as readConfigString, H as formatResourceLine, Hi as LocalStartServiceError, Hn as STATE_SOURCED_READBACK_RULES, Hr as describeDockerExecFailure, Ht as configBooleanRefusal, I as renderStatefulReason, Ii as CrossAccountSecretRefusalError, In as describeTypeWithThrottleRetry, Ir as readBootstrapMarkerBody, It as WAFv2WebACLProvider, J as yellow, Ji as ResourceTimeoutError, Jn as identityKeyFor, Jr as redactDockerArgvValues, Jt as requireConfigObject, K as green, Ki as PartialFailureError, Kn as dynamicReferenceTokens, Kr as getDockerCmd, Kt as replayWarn, L as WARM_THROUGHPUT_MEMBERS, Ln as withRetry, Lr as validateAssetBucketName, Lt as normalizeAwsTagsToCfn, M as planOrphanAdoption, Mi as setAwsClients, Mn as repairMalformedResourcePropertiesForReadOnly, Mr as ensureAssetStorage, Mt as isUnboundTemplateParameter, N as MULTI_REGION_RECREATE_BLOCKED_TYPES, Nn as repairMalformedResourcesForReadOnly, Nr as getBootstrapMarkerKey, Nt as parameterTypeMayLoseSecretIdentity, O as unsupportedFinalSnapshotError, Oi as resolveBucketRegion, On as refuseMalformedResourcePropertiesForOrphan, Or as rewriteTemplateAssetReferences, Ot as coerceParameterTypedValue, P as isStatefulRecreateTargetForReplace, Pi as CdkdError, Pn as findActionableSilentDrops, Pr as isCrossRegionRedirect, Pt as refStateLookupFromResource, Q as isExportAliasCollision, Qn as maskSecretsInText, Qr as stripControlChars, Qt as withSharedDrainBudget, R as coerceWarmThroughput$1, Ri as DeployCancelledError, Rn as DagBuilder, Rr as validateContainerRepoName, Rt as resolveExplicitPhysicalId, S as PRE_DELETE_SNAPSHOT_TYPES, Si as PARTITION_TABLE, Sn as malformedStateRefusalMessage, Sr as AssetPublisher, St as disableInstanceApiTermination, T as createPreDeleteFinalSnapshot, Ti as AssemblyReader, Tn as refuseMalformedNestedChildOutputs, Tr as buildAssetRedirectMap, Tt as carriesDynamicReference, U as bold, Un as TEMPLATE_SOURCED_RULES, Ur as describeDockerFailure, Ut as configIntegerRefusal, V as toFiniteNumber, Vi as LocalInvokeBuildError$1, Vn as STATE_SOURCED_CROSS_GENERATION_RULES, Vr as describeDockerCapturedOutput, Vt as coerceCfnInteger, W as cyan, Wi as MalformedProducerRecordRefusalError, Wn as carriesSecretMask, Wr as dockerSpawnEnvWithSensitive, Wt as configStringRefusal, X as collectPublishedOutputNames, Xi as StackHasActiveImportsError, Xn as markSameGenerationBag, Xr as runDockerStreaming, Xt as classifyReplaySecretRegion, Y as collectDeclaredOutputNames, Yi as ResourceUpdateNotSupportedError, Yn as isSingleDynamicReferenceToken, Yr as runDockerForeground, Yt as requireConfigString, Z as exportAliasCollisionScrubWarning, Zi as StackTerminationProtectionError, Zn as maskSecretsInError, Zr as escapeRegExp, Zt as producerRegionsFromState, _ as isSecretBearingReferenceString, _i as findLargeInlineResources, _n as malformedOutputsRefusalMessage, _r as importableOutputs, _t as isWaitAbandonedError, a as DeploymentEventsStore, aa as isRetryableTransientError, ai as resolveApp, an as AWS_CREDENTIAL_ENV_KEYS, ar as rebuildClientForBucketRegion, at as clearOnUpdateRemoval, b as bindingSkippedOutputs, bi as decodeListingKey, bn as malformedResourcePropertiesWarning, br as orphansCarriedFrom, bt as deleteIndeterminateGuards, c as replayFailedOperations, ca as markNonRetryable, ci as resolveSkipPrefix, cn as envCredentialsAreAssumedRole, cr as buildLockContentionMessage, ct as createMaskedRetryLogger, d as updatePartialReason, da as describeAwsFailure, di as resolveUseCdkBootstrapAssets, dn as STATE_RESOURCES_MALFORMED, dr as shellQuote, dt as beginCommandInterruptScope, ei as getDockerImageBySourceHash, en as s3BucketDomainName, er as recoverMaskedOutput, et as secretBearingStateKeyWarning, f as withResourceDeadline, fa as safeStringify, fi as stateBucketExistenceConfirmed, fn as hasReadableOutputs, fr as CUSTOM_RESOURCE_RESPONSE_PREFIX, ft as endCommandInterruptScope, g as bagHoldsSecretExpression, gi as MIGRATE_TMP_PREFIX, gn as malformedLocalOutputsWarning, gr as importableOutputKeys, gt as CloudControlProvider, h as computeImplicitDeleteEdges, hi as CFN_TEMPLATE_URL_LIMIT, hn as malformedExportSourceWarning, hr as hasReadableExportSet, ht as startInterruptWatch, i as DeploymentEventsReader, ia as isMarkedNonRetryable, ii as getLegacyStateBucketName, in as applyRoleArnIfSet, ir as S3StateBackend, it as collectInlinePolicyNamesManagedBySiblings, j as makeSiblingClaimReader, ji as resetAwsClients, jn as repairMalformedOutputsForReadOnly, jr as assertAssetBucketRegion, jt as isNamelessDynamicReferenceError, k as makeCanonicalizePropertiesFn, ki as AwsClients, kn as refuseMalformedResourcesForDestroy, kr as AssetModeResolver, kt as getAccountInfo, l as replayRollback, la as markRedactedCause, li as resolveStateBucketWithDefault, ln as DiffCalculator, lr as forceQuitRecoveryClause, lt as maskDeep, m as IMPLICIT_DELETE_DEPENDENCIES, mi as CFN_TEMPLATE_BODY_LIMIT, mn as malformedExportNamesWarning, mr as exportNamesCarriedFrom, mt as isInterruptedWaitError, n as DEFAULT_RESOURCE_WARN_AFTER_MS, na as normalizeAwsError, ni as synthesisStatusMessage, nn as s3BucketRegionalDomainName, nr as scrubResourceRecord, nt as getCurrentResourceSecrets, o as planFailedOps, oa as isThrottlingError, oi as resolveAutoAssetStorage, on as applyCallerIdentityCredentials, or as UNREPRODUCIBLE_LOCK_CLAUSE, ot as ProviderRegistry, p as maskingRetryLogger, pi as warnDeprecatedNoPrefixCliFlag, pn as hasReadableResources, pr as DEFAULT_STATE_PREFIX, pt as interruptWatchListenerCount, q as red, qi as ProvisioningError, qn as errorCauseChain, qr as partitionSensitiveEnv, qt as requireConfigArray, r as DeployEngine, ra as withErrorHandling, ri as getDefaultStateBucketName, rn as s3BucketWebsiteUrl, rr as LockManager, rt as IAMRoleProvider, s as planRollback, sa as isTransientServerError, si as resolveCaptureObservedState, sn as callerEnvCredentials, sr as buildForceUnlockCommand, st as wouldReturnToSdkProvider, t as DEFAULT_RESOURCE_TIMEOUT_MS, ti as Synthesizer, tn as s3BucketDualStackDomainName, tr as redactSecretsForState, tt as secretSafeKeyDisplay, u as updatePartialMessage, ua as retryClassificationText, ui as resolveStateBucketWithDefaultAndSource, un as INTRINSIC_KEYS, ur as formatLockExpiry, ut as maskerOrIdentity, v as keptWholeReasonText, vi as uploadCfnTemplate, vn as malformedOutputsWarning, vr as isReadableBag, vt as slowCcOperationTimeoutMs, w as ccRoutedFinalSnapshotError, wi as derivePartitionAndUrlSuffix, wn as refuseDivergentRecordRegionForDestroy, wr as WorkGraph, wt as IntrinsicFunctionResolver, x as ATOMIC_FINAL_SNAPSHOT_TYPES, xi as expectedOwnerParam, xn as malformedResourcesWarning, xr as shouldRetainResource, xt as deleteSkipReason, y as mergeNoChangeOutputs, yi as CUSTOM_RESOURCE_RESPONSE_OBJECT_DESCRIPTION, yn as malformedRenderedContainersWarning, yr as orphansAfterRollback, yt as UNSPECIFIED_SKIP_REASON, z as isWarmThroughputDecrease, zi as DynamicReferenceRegionAmbiguousError, zn as TemplateParser, zr as buildDenyExternalAccessPolicy, zt as assertRegionMatch } from "./deploy-engine-Bcwb9_mO.js";
|
|
2
|
+
import { C as displayIdent, S as UNRENDERABLE, T as truncateCodePoints, _ as withSkipPrefix, b as SECRET_REF_MAX_CODE_POINTS, c as PATTERN_B_NAME_OPTIONS, f as explicitNamePropertyFor, h as getCurrentStackName, i as reserveStdoutForPayload, l as PATTERN_B_NAME_PROPERTIES, m as generateResourceNameWithFallback, n as getLogger, o as runStackBuffered, p as generateResourceName, s as getLiveRenderer, u as PATTERN_B_RESOURCE_TYPES, v as withStackName, w as displaySafe, x as STACK_REF_MAX_CODE_POINTS } from "./logger-6LP1UiFA.js";
|
|
3
3
|
import { n as awsClientDefaults } from "./aws-client-defaults-BoIxyCir.js";
|
|
4
|
-
import { t as getCdkdVersion } from "./version-
|
|
5
|
-
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-
|
|
4
|
+
import { t as getCdkdVersion } from "./version-snXN__f1.js";
|
|
5
|
+
import { a as renderDisableCommand, i as protectedReplacementAdvice, r as UNNAMEABLE_ID_CLAUSE, t as ASGProvider } from "./asg-provider-Dwnb3CES.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";
|
|
@@ -17948,6 +17948,158 @@ function liveCeilingAlreadyMatches(live, requested) {
|
|
|
17948
17948
|
return compared > 0;
|
|
17949
17949
|
}
|
|
17950
17950
|
/**
|
|
17951
|
+
* The sentinel DynamoDB accepts to CLEAR an on-demand request-unit maximum
|
|
17952
|
+
* (issue [#3373](https://github.com/go-to-k/cdkd/issues/3373)).
|
|
17953
|
+
*
|
|
17954
|
+
* LIVE-VERIFIED at BOTH positions this file sends from, which is what makes it
|
|
17955
|
+
* implementable at both at once rather than at one and not the other:
|
|
17956
|
+
*
|
|
17957
|
+
* - `UpdateTable.OnDemandThroughput` — AWS's own SDK model doc for the block,
|
|
17958
|
+
* and the measurement recorded on issue
|
|
17959
|
+
* [#1434](https://github.com/go-to-k/cdkd/issues/1434) (us-east-1 +
|
|
17960
|
+
* us-west-2, 2026-08-09), which probed the SINGLE-member shape this file
|
|
17961
|
+
* also sends: `{MaxWriteRequestUnits: -1}` against a live `{100, 100}` was
|
|
17962
|
+
* accepted and read back as `{MaxReadRequestUnits: 100}`;
|
|
17963
|
+
* - `UpdateTable.GlobalSecondaryIndexUpdates[].Update.OnDemandThroughput` —
|
|
17964
|
+
* the position issue #3373's body called UNMEASURED. It is not, and the
|
|
17965
|
+
* evidence is in TWO places, named separately because a reviewer checked
|
|
17966
|
+
* and one of them is not where an earlier draft of this said it was:
|
|
17967
|
+
* issue [#1423](https://github.com/go-to-k/cdkd/issues/1423)'s comment
|
|
17968
|
+
* thread carries the `{-1, -1}` transcript (us-east-1, 2026-08-09,
|
|
17969
|
+
* throwaway table) — ACCEPTED, with `DescribeTable` then reporting the
|
|
17970
|
+
* block ABSENT — and PR
|
|
17971
|
+
* [#1433](https://github.com/go-to-k/cdkd/pull/1433)'s body carries the
|
|
17972
|
+
* MIXED `keep 50 / reset -1` probe at the same position, re-run precisely
|
|
17973
|
+
* because only `{-1, -1}` had been verified. `dynamodb-globaltable-provider.ts`
|
|
17974
|
+
* has shipped on both since. #3373's dup-check dismissed those issues
|
|
17975
|
+
* because `AWS::DynamoDB::GlobalTable` spells its per-GSI ceilings
|
|
17976
|
+
* `Write` / `ReadOnDemandThroughputSettings` — true of the TEMPLATE and
|
|
17977
|
+
* irrelevant to the WIRE, where both providers send the same
|
|
17978
|
+
* `UpdateGlobalSecondaryIndexAction.OnDemandThroughput`.
|
|
17979
|
+
*
|
|
17980
|
+
* The read-back is ABSENCE, never `-1`, at both positions — so nothing on the
|
|
17981
|
+
* drift / `readCurrentState` side may expect this value.
|
|
17982
|
+
*/
|
|
17983
|
+
const ON_DEMAND_LIMIT_RESET$1 = -1;
|
|
17984
|
+
/**
|
|
17985
|
+
* Which `OnDemandThroughput` members this update must explicitly REMOVE, as a
|
|
17986
|
+
* list of member names (issue #3373).
|
|
17987
|
+
*
|
|
17988
|
+
* An absent member KEEPS whatever maximum the table already carries, so a
|
|
17989
|
+
* template edit that drops a ceiling used to deploy GREEN, be recorded as
|
|
17990
|
+
* applied, and leave the live maximum in force forever — the #1160
|
|
17991
|
+
* absent-field-reset class, one level down inside a nested block.
|
|
17992
|
+
*
|
|
17993
|
+
* PER MEMBER, never per BLOCK: read and write maxima are independent template
|
|
17994
|
+
* values, so a whole-block branch would drop the single-member removal, which
|
|
17995
|
+
* is the likelier user edit (the `AWS::DynamoDB::GlobalTable` twin paid for
|
|
17996
|
+
* that once already).
|
|
17997
|
+
*
|
|
17998
|
+
* FOUR conditions, all necessary, each closing a different way this goes
|
|
17999
|
+
* wrong:
|
|
18000
|
+
*
|
|
18001
|
+
* 1. **the PREVIOUS side declared it AND it survived narrowing.**
|
|
18002
|
+
* `previousProperties` is a cdkd STATE record and the record is RAW — it
|
|
18003
|
+
* holds the spelling the TEMPLATE declared, including a member
|
|
18004
|
+
* {@link narrowOnDemandCeilings} REFUSED and cdkd therefore never sent. A
|
|
18005
|
+
* raw presence test would read such a member as "previously applied".
|
|
18006
|
+
* 2. **the DESIRED side does not declare it AT ALL, raw.** This is issue
|
|
18007
|
+
* [#1440](https://github.com/go-to-k/cdkd/issues/1440) exactly: a member
|
|
18008
|
+
* the grammar rejected is the template TRYING to SET a ceiling, and taking
|
|
18009
|
+
* the reset branch for it would silently CLEAR the ceiling being set. The
|
|
18010
|
+
* narrowed desired side cannot tell those apart; the raw one can.
|
|
18011
|
+
* 3. **AWS actually HOLDS the member right now.** It proves there is a maximum
|
|
18012
|
+
* to remove, and it makes the whole rule fail CLOSED when cdkd holds no
|
|
18013
|
+
* live snapshot — the correct direction for a send that DESTROYS a live
|
|
18014
|
+
* value.
|
|
18015
|
+
* 4. **this update leaves the table on PAY_PER_REQUEST** (`ceilingsSendable`).
|
|
18016
|
+
* Condition 3 does NOT subsume this, and believing it did was a BLOCKER on
|
|
18017
|
+
* PR go-to-k/cdkd#3401 (spec review, M0). The live snapshot is the ONE
|
|
18018
|
+
* `DescribeTable` at the top of `update()` and is never refreshed, while
|
|
18019
|
+
* both removal arms run AFTER the BillingMode flip — so on a template that
|
|
18020
|
+
* flips a live on-demand table to PROVISIONED **and** drops the ceiling,
|
|
18021
|
+
* the pre-flight refusals both decline (neither side DECLARES a ceiling),
|
|
18022
|
+
* the flip lands, and the stale snapshot then says the member is live: cdkd
|
|
18023
|
+
* would send `-1` to a now-PROVISIONED table, AWS would reject it, and a
|
|
18024
|
+
* previously-green no-op becomes a HALF-APPLIED deploy — the exact class
|
|
18025
|
+
* the #3392 pre-flight exists to prevent. `dynamodb-globaltable-provider.ts`
|
|
18026
|
+
* carries the same guard at both of its positions
|
|
18027
|
+
* (`onDemandCeilingLive` / `!billingFlipped`), named in PR
|
|
18028
|
+
* go-to-k/cdkd#1433's body as "skipped on a billing flip".
|
|
18029
|
+
*
|
|
18030
|
+
* What the skip COSTS was asked and MEASURED, not assumed (the
|
|
18031
|
+
* go-to-k/cdkd#3401 code review raised it; issue
|
|
18032
|
+
* [#1441](https://github.com/go-to-k/cdkd/issues/1441) had already
|
|
18033
|
+
* settled it). The worry is a round trip: deploy A sets a ceiling on a
|
|
18034
|
+
* PAY_PER_REQUEST table, deploy B flips to PROVISIONED and drops it (the
|
|
18035
|
+
* reset suppressed here), deploy C flips back declaring nothing — and
|
|
18036
|
+
* since deploy B's template is now the previous side, condition 1 can
|
|
18037
|
+
* never fire again, so a RETAINED ceiling would be unclearable. #1441's
|
|
18038
|
+
* probe (us-east-1, 2026-08-10, transcript on the issue) ran exactly that
|
|
18039
|
+
* sequence at the TABLE level: `DescribeTable` reports the block ABSENT
|
|
18040
|
+
* after the flip, and it does NOT come back on the flip home. Nothing is
|
|
18041
|
+
* stranded. The PER-INDEX round trip is not separately measured — issue
|
|
18042
|
+
* [#3403](https://github.com/go-to-k/cdkd/issues/3403) — but it fails in
|
|
18043
|
+
* the SAFE direction (a lingering maximum, never a destroyed one).
|
|
18044
|
+
*
|
|
18045
|
+
* Every member-presence test here is `Object.hasOwn`, not `in` (the
|
|
18046
|
+
* go-to-k/cdkd#3401 security review). `in` walks the prototype chain, and these
|
|
18047
|
+
* three tests gate a DESTRUCTIVE send off a cdkd STATE record — the own-key
|
|
18048
|
+
* convention `drift.ts`'s `hasOwnKey` / `ownValue` already follow for
|
|
18049
|
+
* state-derived bags (issue go-to-k/cdkd#2899). Not demonstrably reachable
|
|
18050
|
+
* today; the point is that the guard on a destroying path should not depend on
|
|
18051
|
+
* that staying true.
|
|
18052
|
+
*
|
|
18053
|
+
* `declared` present but NOT a plain block (a string, an array, an unresolved
|
|
18054
|
+
* intrinsic) yields NO removals: that value is forwarded VERBATIM for AWS to
|
|
18055
|
+
* reject by shape, and merging a sentinel into it is not expressible.
|
|
18056
|
+
*
|
|
18057
|
+
* And a declared block cdkd cannot READ AT ALL yields none either — a plain
|
|
18058
|
+
* object with no member surviving {@link narrowOnDemandCeilings}, i.e. only
|
|
18059
|
+
* rejected spellings or only names the grammar does not know (`{Ref: 'Unset'}`,
|
|
18060
|
+
* a mis-nested value). That is condition 2 one level up: such a block is not a
|
|
18061
|
+
* statement that the sibling member was REMOVED, it is a template cdkd could
|
|
18062
|
+
* not read, and clearing a live maximum on the strength of it is the same
|
|
18063
|
+
* destruction #1440 describes. An ABSENT desired side is the opposite and is
|
|
18064
|
+
* the headline case — nothing declared means nothing kept.
|
|
18065
|
+
*/
|
|
18066
|
+
function onDemandCeilingRemovals(declared, previous, live, ceilingsSendable) {
|
|
18067
|
+
if (!ceilingsSendable) return [];
|
|
18068
|
+
if (live === void 0) return [];
|
|
18069
|
+
if (declared !== void 0 && !isPlainCapacityBlock(declared)) return [];
|
|
18070
|
+
const previousBlock = narrowOnDemandCeilings(previous).block;
|
|
18071
|
+
if (!isPlainCapacityBlock(previousBlock)) return [];
|
|
18072
|
+
const declaredBlock = isPlainCapacityBlock(declared) ? declared : void 0;
|
|
18073
|
+
if (declaredBlock !== void 0) {
|
|
18074
|
+
const declaredSurvivors = narrowOnDemandCeilings(declaredBlock).block;
|
|
18075
|
+
if (!isPlainCapacityBlock(declaredSurvivors) || !ON_DEMAND_CEILING_MEMBERS.some((member) => Object.hasOwn(declaredSurvivors, member))) return [];
|
|
18076
|
+
}
|
|
18077
|
+
const liveBag = live;
|
|
18078
|
+
const removals = [];
|
|
18079
|
+
for (const member of ON_DEMAND_CEILING_MEMBERS) {
|
|
18080
|
+
if (!Object.hasOwn(previousBlock, member)) continue;
|
|
18081
|
+
if (declaredBlock !== void 0 && Object.hasOwn(declaredBlock, member)) continue;
|
|
18082
|
+
if (toFiniteNumber(liveBag[member]) === void 0) continue;
|
|
18083
|
+
removals.push(member);
|
|
18084
|
+
}
|
|
18085
|
+
return removals;
|
|
18086
|
+
}
|
|
18087
|
+
/**
|
|
18088
|
+
* Merge the {@link onDemandCeilingRemovals} sentinels into a block bound for
|
|
18089
|
+
* the wire (issue #3373).
|
|
18090
|
+
*
|
|
18091
|
+
* A COPY, never a mutation of `block`: {@link narrowOnDemandCeilings} returns
|
|
18092
|
+
* its input BY IDENTITY when nothing needed rewriting, so mutating here would
|
|
18093
|
+
* write `-1` into the caller's own `properties` bag — which the engine then
|
|
18094
|
+
* records as state.
|
|
18095
|
+
*/
|
|
18096
|
+
function withOnDemandCeilingRemovals(block, removals) {
|
|
18097
|
+
if (removals.length === 0) return block;
|
|
18098
|
+
const merged = { ...isPlainCapacityBlock(block) ? block : {} };
|
|
18099
|
+
for (const member of removals) merged[member] = ON_DEMAND_LIMIT_RESET$1;
|
|
18100
|
+
return merged;
|
|
18101
|
+
}
|
|
18102
|
+
/**
|
|
17951
18103
|
* The capacity both TABLE-level forwarders substitute for a member the
|
|
17952
18104
|
* template does not declare at all — `create()`'s `CreateTable` and the
|
|
17953
18105
|
* BillingMode flip's `UpdateTable`. It is the pre-issue-#3147 `?? 5`, kept
|
|
@@ -18712,7 +18864,17 @@ var DynamoDBTableProvider = class {
|
|
|
18712
18864
|
const billingOrThroughputChanged = billingMode !== prevBillingMode || JSON.stringify(properties["ProvisionedThroughput"]) !== JSON.stringify(previousProperties["ProvisionedThroughput"]);
|
|
18713
18865
|
if (billingOrThroughputChanged && billingMode === "PROVISIONED" && (prevBillingMode === "PAY_PER_REQUEST" || liveBillingMode === "PAY_PER_REQUEST") && properties["OnDemandThroughput"] !== void 0) {
|
|
18714
18866
|
const absentNote = properties["BillingMode"] === void 0 ? " (BillingMode is absent from the template, which CloudFormation resets to PROVISIONED)" : "";
|
|
18715
|
-
throw new ProvisioningError(`AWS::DynamoDB::Table ${logicalId}: the template flips BillingMode to PROVISIONED${absentNote} while still declaring OnDemandThroughput, which AWS accepts only on a PAY_PER_REQUEST table.
|
|
18867
|
+
throw new ProvisioningError(`AWS::DynamoDB::Table ${logicalId}: the template flips BillingMode to PROVISIONED${absentNote} while still declaring OnDemandThroughput, which AWS accepts only on a PAY_PER_REQUEST table. No BillingMode flip and no index change were applied. Remove OnDemandThroughput, or keep BillingMode: PAY_PER_REQUEST.`, resourceType, logicalId);
|
|
18868
|
+
}
|
|
18869
|
+
const effectiveBillingMode = billingOrThroughputChanged ? billingMode : liveBillingMode;
|
|
18870
|
+
if (effectiveBillingMode === "PROVISIONED") {
|
|
18871
|
+
const ceilingIndexNames = (Array.isArray(properties["GlobalSecondaryIndexes"]) ? properties["GlobalSecondaryIndexes"] : []).filter((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry)).filter((entry) => Boolean(entry["OnDemandThroughput"])).map((entry) => typeof entry["IndexName"] === "string" ? maskSecrets(entry["IndexName"]) : "<unnamed>");
|
|
18872
|
+
if (ceilingIndexNames.length > 0) {
|
|
18873
|
+
const modeNote = billingOrThroughputChanged ? "" : " (the table is already PROVISIONED in AWS and this update sends no BillingMode change)";
|
|
18874
|
+
const message = `AWS::DynamoDB::Table ${logicalId}: GlobalSecondaryIndexes ${ceilingIndexNames.join(", ")} declare OnDemandThroughput while the table's billing mode is PROVISIONED${modeNote}, which AWS accepts only on a PAY_PER_REQUEST table. Remove the per-index OnDemandThroughput, or set BillingMode: PAY_PER_REQUEST.`;
|
|
18875
|
+
if (context?.replayingState === true || context?.desiredFromAwsReadback === true) warn(`${message} Downgraded to a warning because this update's desired properties did not come from the template, so there is no template-side remedy; the per-index ceiling will be sent and AWS will reject it.`);
|
|
18876
|
+
else throw new ProvisioningError(`${message} No BillingMode flip and no index change were applied.`, resourceType, logicalId, physicalId);
|
|
18877
|
+
}
|
|
18716
18878
|
}
|
|
18717
18879
|
const liveIndexes = table?.GlobalSecondaryIndexes ?? [];
|
|
18718
18880
|
const desiredIndexByName = /* @__PURE__ */ new Map();
|
|
@@ -18796,10 +18958,13 @@ var DynamoDBTableProvider = class {
|
|
|
18796
18958
|
this.logger.debug(`Updated BillingMode/ProvisionedThroughput/TableClass on DynamoDB table ${physicalId}`);
|
|
18797
18959
|
}
|
|
18798
18960
|
if (JSON.stringify(properties["OnDemandThroughput"]) !== JSON.stringify(previousProperties["OnDemandThroughput"])) {
|
|
18799
|
-
|
|
18961
|
+
const ceilingRemovals = onDemandCeilingRemovals(properties["OnDemandThroughput"], previousProperties["OnDemandThroughput"], table?.OnDemandThroughput, effectiveBillingMode === "PAY_PER_REQUEST");
|
|
18962
|
+
if (properties["OnDemandThroughput"] || ceilingRemovals.length > 0) {
|
|
18963
|
+
const declaredForSend = properties["OnDemandThroughput"] ? this.coerceOnDemandCeilingsForSend(`AWS::DynamoDB::Table ${logicalId}`, properties["OnDemandThroughput"], maskSecrets) : {};
|
|
18964
|
+
if (ceilingRemovals.length > 0) debug(`AWS::DynamoDB::Table ${logicalId}: removing the OnDemandThroughput ${ceilingRemovals.join(" / ")} the template no longer declares (sending ${ON_DEMAND_LIMIT_RESET$1}, DynamoDB's reset sentinel)`);
|
|
18800
18965
|
await this.dynamoDBClient.send(new UpdateTableCommand({
|
|
18801
18966
|
TableName: physicalId,
|
|
18802
|
-
OnDemandThroughput:
|
|
18967
|
+
OnDemandThroughput: withOnDemandCeilingRemovals(declaredForSend, ceilingRemovals)
|
|
18803
18968
|
}));
|
|
18804
18969
|
await this.waitForTableActiveAfterUpdate(physicalId);
|
|
18805
18970
|
this.logger.debug(`Updated OnDemandThroughput on DynamoDB table ${physicalId}`);
|
|
@@ -18868,7 +19033,7 @@ var DynamoDBTableProvider = class {
|
|
|
18868
19033
|
this.logger.debug(`Disabled DynamoDB Stream on table ${physicalId}`);
|
|
18869
19034
|
}
|
|
18870
19035
|
}
|
|
18871
|
-
if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip, currentLiveIndexNames, currentLiveIndexByName, liveCapacityComparable, maskSecrets);
|
|
19036
|
+
if (JSON.stringify(properties["GlobalSecondaryIndexes"]) !== JSON.stringify(previousProperties["GlobalSecondaryIndexes"])) await this.applyGsiUpdates(physicalId, resourceType, logicalId, previousProperties["GlobalSecondaryIndexes"], properties["GlobalSecondaryIndexes"], properties["AttributeDefinitions"], gsiHandledByBillingFlip, currentLiveIndexNames, currentLiveIndexByName, liveCapacityComparable, effectiveBillingMode === "PAY_PER_REQUEST", maskSecrets);
|
|
18872
19037
|
if (JSON.stringify(properties["PointInTimeRecoverySpecification"]) !== JSON.stringify(previousProperties["PointInTimeRecoverySpecification"])) await this.applyPointInTimeRecovery(physicalId, properties["PointInTimeRecoverySpecification"], previousProperties["PointInTimeRecoverySpecification"], maskSecrets);
|
|
18873
19038
|
if (JSON.stringify(properties["TimeToLiveSpecification"]) !== JSON.stringify(previousProperties["TimeToLiveSpecification"])) {
|
|
18874
19039
|
this.assertNoActiveTtlAttributeNameChange(logicalId, resourceType, physicalId, properties["TimeToLiveSpecification"], previousProperties["TimeToLiveSpecification"]);
|
|
@@ -19062,7 +19227,7 @@ var DynamoDBTableProvider = class {
|
|
|
19062
19227
|
* change", so a dropped block must be turned into an explicit
|
|
19063
19228
|
* `PointInTimeRecoveryEnabled: false`).
|
|
19064
19229
|
*/
|
|
19065
|
-
async applyPointInTimeRecovery(tableName, spec, previousSpec, maskSecrets
|
|
19230
|
+
async applyPointInTimeRecovery(tableName, spec, previousSpec, maskSecrets) {
|
|
19066
19231
|
let enabled;
|
|
19067
19232
|
let recoveryPeriodInDays;
|
|
19068
19233
|
if (spec !== void 0 && spec !== null) {
|
|
@@ -19237,7 +19402,7 @@ var DynamoDBTableProvider = class {
|
|
|
19237
19402
|
* pre-existing WarmThroughput "no clean remap" stance; precision changes flow
|
|
19238
19403
|
* through on the create / first-enable path.
|
|
19239
19404
|
*/
|
|
19240
|
-
async applyKinesisStreamingDestination(tableName, spec, previousSpec, maskSecrets
|
|
19405
|
+
async applyKinesisStreamingDestination(tableName, spec, previousSpec, maskSecrets) {
|
|
19241
19406
|
const newArn = this.extractKinesisStreamArn(spec);
|
|
19242
19407
|
const prevArn = this.extractKinesisStreamArn(previousSpec);
|
|
19243
19408
|
if (newArn === prevArn) {
|
|
@@ -19427,7 +19592,7 @@ var DynamoDBTableProvider = class {
|
|
|
19427
19592
|
* a remove-then-add pair, so this method throws on such a change rather than
|
|
19428
19593
|
* silently dropping it.
|
|
19429
19594
|
*/
|
|
19430
|
-
async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions, handledByBillingFlip
|
|
19595
|
+
async applyGsiUpdates(physicalId, resourceType, logicalId, previousGsis, desiredGsis, desiredAttributeDefinitions, handledByBillingFlip, liveIndexNames, liveIndexByName, liveCapacityComparable, onDemandCeilingsSendable, maskSecrets) {
|
|
19431
19596
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
19432
19597
|
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
19433
19598
|
const prev = previousGsis ?? [];
|
|
@@ -19459,7 +19624,7 @@ var DynamoDBTableProvider = class {
|
|
|
19459
19624
|
adopted.WarmThroughput = adoptedWarm;
|
|
19460
19625
|
adoptedHasMember = true;
|
|
19461
19626
|
}
|
|
19462
|
-
const adoptedCeiling = this.indexCeilingForSend(name, physicalId, gsi, void 0, live, maskSecrets);
|
|
19627
|
+
const adoptedCeiling = this.indexCeilingForSend(name, physicalId, gsi, void 0, live, onDemandCeilingsSendable, maskSecrets);
|
|
19463
19628
|
if (adoptedCeiling) {
|
|
19464
19629
|
adopted.OnDemandThroughput = adoptedCeiling;
|
|
19465
19630
|
adoptedHasMember = true;
|
|
@@ -19495,7 +19660,7 @@ var DynamoDBTableProvider = class {
|
|
|
19495
19660
|
update.WarmThroughput = warm;
|
|
19496
19661
|
updateHasMember = true;
|
|
19497
19662
|
}
|
|
19498
|
-
const ceiling = this.indexCeilingForSend(name, physicalId, gsi, before, liveIndexByName?.get(name), maskSecrets);
|
|
19663
|
+
const ceiling = this.indexCeilingForSend(name, physicalId, gsi, before, liveIndexByName?.get(name), onDemandCeilingsSendable, maskSecrets);
|
|
19499
19664
|
if (ceiling) {
|
|
19500
19665
|
update.OnDemandThroughput = ceiling;
|
|
19501
19666
|
updateHasMember = true;
|
|
@@ -19515,7 +19680,7 @@ var DynamoDBTableProvider = class {
|
|
|
19515
19680
|
* two messages are disjoint by construction: this one fires only when
|
|
19516
19681
|
* something DID resolve.
|
|
19517
19682
|
*/
|
|
19518
|
-
coerceWarmThroughputForSend(scope, value, maskSecrets
|
|
19683
|
+
coerceWarmThroughputForSend(scope, value, maskSecrets) {
|
|
19519
19684
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
19520
19685
|
const coerced = coerceWarmThroughput(value);
|
|
19521
19686
|
if (coerced === void 0) return void 0;
|
|
@@ -19618,24 +19783,23 @@ var DynamoDBTableProvider = class {
|
|
|
19618
19783
|
* deploy succeed, so the engine records the un-sent ceiling as applied and
|
|
19619
19784
|
* the deploy that FIXES the billing mode then compares equal and never
|
|
19620
19785
|
* sends it — issue #3287's own defect, reintroduced by its own guard. The
|
|
19621
|
-
* file's table-level twin states that reasoning at its own refusal.
|
|
19622
|
-
*
|
|
19623
|
-
*
|
|
19624
|
-
*
|
|
19625
|
-
*
|
|
19626
|
-
*
|
|
19627
|
-
*
|
|
19628
|
-
*
|
|
19629
|
-
*
|
|
19630
|
-
*
|
|
19631
|
-
*
|
|
19632
|
-
*
|
|
19633
|
-
*
|
|
19634
|
-
*
|
|
19635
|
-
*
|
|
19636
|
-
*
|
|
19637
|
-
*
|
|
19638
|
-
* before any call instead of an AWS rejection after one.
|
|
19786
|
+
* file's table-level twin states that reasoning at its own refusal.
|
|
19787
|
+
*
|
|
19788
|
+
* **The refusal it pointed at has SHIPPED** (issue
|
|
19789
|
+
* [#3392](https://github.com/go-to-k/cdkd/issues/3392)): `update()` walks
|
|
19790
|
+
* `GlobalSecondaryIndexes[]` before any call and throws when the EFFECTIVE
|
|
19791
|
+
* billing mode is PROVISIONED, which covers all THREE per-index send sites
|
|
19792
|
+
* at once (this arm, the same-name `Update` arm's caller, and the `Create`
|
|
19793
|
+
* action, which has carried the same defect since #3265) because none of
|
|
19794
|
+
* them is reached. So this arm's job is unchanged and stays deliberately
|
|
19795
|
+
* mode-blind: a gate here would be a SECOND spelling of one rule, and the
|
|
19796
|
+
* mid-update spelling is the one that was measured wrong.
|
|
19797
|
+
*
|
|
19798
|
+
* On the REPLAY path the pre-flight downgrades to a warning and this arm
|
|
19799
|
+
* still runs, so the remaining behaviour is what #3287's round 3 recorded:
|
|
19800
|
+
* the member goes out, AWS rejects the `UpdateTable`, and the co-resident
|
|
19801
|
+
* capacity / warm edits fail with it. Loud and recording nothing beats
|
|
19802
|
+
* silent and lost.
|
|
19639
19803
|
* - **The change detector compares the NARROWED blocks WHEN NOTHING WAS
|
|
19640
19804
|
* DROPPED, and the RAW ones otherwise** — the one place it differs from its
|
|
19641
19805
|
* `ProvisionedThroughput` sibling two arms up. `'200'` and `200` are the
|
|
@@ -19681,21 +19845,39 @@ var DynamoDBTableProvider = class {
|
|
|
19681
19845
|
* rejects by name (issue #3392) rather than a silently dropped change.
|
|
19682
19846
|
* There is no `{0, 0}` analogue of the #1571 capacity trap.
|
|
19683
19847
|
*
|
|
19684
|
-
*
|
|
19685
|
-
*
|
|
19686
|
-
*
|
|
19687
|
-
*
|
|
19688
|
-
*
|
|
19689
|
-
*
|
|
19690
|
-
*
|
|
19691
|
-
*
|
|
19692
|
-
*
|
|
19693
|
-
*
|
|
19694
|
-
*
|
|
19695
|
-
*
|
|
19696
|
-
|
|
19697
|
-
|
|
19698
|
-
|
|
19848
|
+
* It DOES remove a ceiling, since issue
|
|
19849
|
+
* [#3373](https://github.com/go-to-k/cdkd/issues/3373) — and the removal is
|
|
19850
|
+
* the one thing here that runs AHEAD of the truthiness gate, because the
|
|
19851
|
+
* whole point is a desired side that declares nothing. An absent member KEEPS
|
|
19852
|
+
* whatever maximum the index already carries, so before #3373 a template edit
|
|
19853
|
+
* dropping a ceiling deployed GREEN, was recorded as applied, and left the
|
|
19854
|
+
* live maximum in force forever.
|
|
19855
|
+
*
|
|
19856
|
+
* {@link onDemandCeilingRemovals} owns every condition and
|
|
19857
|
+
* {@link ON_DEMAND_LIMIT_RESET} carries the measurement for THIS action's
|
|
19858
|
+
* position (issue #1423's probe transcript, not an extrapolation from the
|
|
19859
|
+
* table-level field's docs). Two properties of that rule matter most here:
|
|
19860
|
+
* the previous side is read through {@link narrowOnDemandCeilings} so a
|
|
19861
|
+
* member cdkd REFUSED cannot read as "previously applied"; the reset fires
|
|
19862
|
+
* only for a member AWS is OBSERVED to hold, which makes it fail CLOSED with
|
|
19863
|
+
* no live snapshot; and it is suppressed outright when this update FLIPS the
|
|
19864
|
+
* table to PROVISIONED (`onDemandCeilingsSendable`). Do not read the
|
|
19865
|
+
* observed-live condition as covering the flip — it does not, and believing
|
|
19866
|
+
* it did was a merge blocker: the live snapshot is taken before the flip and
|
|
19867
|
+
* never refreshed.
|
|
19868
|
+
*
|
|
19869
|
+
* The TABLE-level arm in `update()` takes the same rule in the same change,
|
|
19870
|
+
* because implementing it at ONE of the two positions would make the same
|
|
19871
|
+
* property behave differently at the table and at an index of the same table.
|
|
19872
|
+
*/
|
|
19873
|
+
indexCeilingForSend(name, physicalId, gsi, before, live, onDemandCeilingsSendable, maskSecrets) {
|
|
19874
|
+
const removals = onDemandCeilingRemovals(gsi.OnDemandThroughput, before?.OnDemandThroughput, live?.OnDemandThroughput, onDemandCeilingsSendable);
|
|
19875
|
+
if (!gsi.OnDemandThroughput) {
|
|
19876
|
+
if (removals.length === 0) return void 0;
|
|
19877
|
+
const removalScope = this.indexScopeAt(name, physicalId, maskSecrets);
|
|
19878
|
+
this.logger.debug(maskSecrets(`${removalScope}: removing the OnDemandThroughput ${removals.join(" / ")} the template no longer declares (sending ${ON_DEMAND_LIMIT_RESET$1}, DynamoDB's reset sentinel)`));
|
|
19879
|
+
return withOnDemandCeilingRemovals({}, removals);
|
|
19880
|
+
}
|
|
19699
19881
|
if (before !== void 0) {
|
|
19700
19882
|
const desired = narrowOnDemandCeilings(gsi.OnDemandThroughput);
|
|
19701
19883
|
const previous = narrowOnDemandCeilings(before.OnDemandThroughput);
|
|
@@ -19705,8 +19887,10 @@ var DynamoDBTableProvider = class {
|
|
|
19705
19887
|
const scope = this.indexScopeAt(name, physicalId, maskSecrets);
|
|
19706
19888
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
19707
19889
|
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
19708
|
-
const
|
|
19709
|
-
if (!isPlainCapacityBlock(
|
|
19890
|
+
const coerced = this.coerceOnDemandCeilingsForSend(scope, gsi.OnDemandThroughput, maskSecrets);
|
|
19891
|
+
if (!isPlainCapacityBlock(coerced)) return coerced;
|
|
19892
|
+
if (removals.length > 0) debug(`${scope}: removing the OnDemandThroughput ${removals.join(" / ")} the template no longer declares (sending ${ON_DEMAND_LIMIT_RESET$1}, DynamoDB's reset sentinel)`);
|
|
19893
|
+
const send = withOnDemandCeilingRemovals(coerced, removals);
|
|
19710
19894
|
if (!ON_DEMAND_CEILING_MEMBERS.some((member) => member in send)) {
|
|
19711
19895
|
if (narrowOnDemandCeilings(gsi.OnDemandThroughput).dropped.length > 0) debug(`${scope}: every declared OnDemandThroughput member was unusable, so no ceiling op was sent`);
|
|
19712
19896
|
else warn(`${scope}: declares an OnDemandThroughput with no member DynamoDB accepts (${ON_DEMAND_CEILING_MEMBERS.join(" / ")}), so no ceiling op was sent. Check the member names.`);
|
|
@@ -19751,7 +19935,7 @@ var DynamoDBTableProvider = class {
|
|
|
19751
19935
|
* value, which is pre-existing behaviour and costs one spread per declared
|
|
19752
19936
|
* block.
|
|
19753
19937
|
*/
|
|
19754
|
-
coerceIndexThroughputForCreate(logicalId, entry, maskSecrets
|
|
19938
|
+
coerceIndexThroughputForCreate(logicalId, entry, maskSecrets) {
|
|
19755
19939
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
|
|
19756
19940
|
const bag = entry;
|
|
19757
19941
|
const declaredWarm = bag["WarmThroughput"];
|
|
@@ -19837,7 +20021,7 @@ var DynamoDBTableProvider = class {
|
|
|
19837
20021
|
* same fail-open reason — a misspelled `ReadCapacityUnit` reaches AWS and is
|
|
19838
20022
|
* named there rather than vanishing here.
|
|
19839
20023
|
*/
|
|
19840
|
-
coerceIndexCapacityBlock(scope, declared, maskSecrets
|
|
20024
|
+
coerceIndexCapacityBlock(scope, declared, maskSecrets) {
|
|
19841
20025
|
if (!isPlainCapacityBlock(declared)) return void 0;
|
|
19842
20026
|
const block = declared;
|
|
19843
20027
|
let rewritten;
|
|
@@ -20045,7 +20229,7 @@ var DynamoDBTableProvider = class {
|
|
|
20045
20229
|
* table that does not exist. {@link tableCapacityForSend}'s doc carries why
|
|
20046
20230
|
* this type differs.
|
|
20047
20231
|
*/
|
|
20048
|
-
warnUnusableProvisionedCapacity(scope, member, raw, maskSecrets
|
|
20232
|
+
warnUnusableProvisionedCapacity(scope, member, raw, maskSecrets) {
|
|
20049
20233
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
20050
20234
|
warn(`${scope}: ProvisionedThroughput.${member} ${JSON.stringify(maskLeafValue$1(raw, maskSecrets))} is not an Integer CloudFormation accepts (an optional sign and decimal digits only — no surrounding whitespace, hex, exponent or decimal point), so it was NOT sent and NO capacity was substituted for it. DynamoDB requires both members wherever a PROVISIONED ProvisionedThroughput block is declared — the table itself, and every GlobalSecondaryIndexes[] entry of a PROVISIONED table — and will reject the request naming this one; CloudFormation refuses the same template at properties validation. Check for an unresolved intrinsic, or spell the value as a decimal integer.`);
|
|
20051
20235
|
}
|
|
@@ -20061,7 +20245,7 @@ var DynamoDBTableProvider = class {
|
|
|
20061
20245
|
* Silent for an ABSENT or FALSY value — see {@link isRefusedWarmThroughput}
|
|
20062
20246
|
* for why those two are not this warning's business.
|
|
20063
20247
|
*/
|
|
20064
|
-
warnRefusedWarmThroughput(scope, value, maskSecrets
|
|
20248
|
+
warnRefusedWarmThroughput(scope, value, maskSecrets) {
|
|
20065
20249
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
20066
20250
|
if (!isRefusedWarmThroughput(value)) return;
|
|
20067
20251
|
warn(`${scope}: the declared WarmThroughput ${JSON.stringify(maskLeafValue$1(value, maskSecrets))} carries no usable ReadUnitsPerSecond or WriteUnitsPerSecond, so it was NOT sent — DynamoDB accepts only those two members and rejects a request without either. Check for a misspelled member name or an unresolved intrinsic. Nothing else on this resource was affected.`);
|
|
@@ -20119,7 +20303,7 @@ var DynamoDBTableProvider = class {
|
|
|
20119
20303
|
* `DescribeTable` reports for every index anyway, so there is no divergence
|
|
20120
20304
|
* to hide.
|
|
20121
20305
|
*/
|
|
20122
|
-
skipZeroCapacityIndexUpdate(indexName, physicalId, requested, maskSecrets
|
|
20306
|
+
skipZeroCapacityIndexUpdate(indexName, physicalId, requested, maskSecrets) {
|
|
20123
20307
|
if (requested === void 0) return false;
|
|
20124
20308
|
if (toFiniteNumber(requested.ReadCapacityUnits) !== 0 || toFiniteNumber(requested.WriteCapacityUnits) !== 0) return false;
|
|
20125
20309
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
@@ -20152,7 +20336,7 @@ var DynamoDBTableProvider = class {
|
|
|
20152
20336
|
* lower than current WarmThroughput, decreasing WarmThroughput is not
|
|
20153
20337
|
* supported`.
|
|
20154
20338
|
*/
|
|
20155
|
-
warmThroughputOpFor(indexName, physicalId, desired, previous, live, maskSecrets
|
|
20339
|
+
warmThroughputOpFor(indexName, physicalId, desired, previous, live, maskSecrets) {
|
|
20156
20340
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
20157
20341
|
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
20158
20342
|
const scope = this.indexScopeAt(indexName, physicalId, maskSecrets);
|
|
@@ -20185,7 +20369,7 @@ var DynamoDBTableProvider = class {
|
|
|
20185
20369
|
* of it — the wait is what makes the next op (there, the BillingMode flip)
|
|
20186
20370
|
* legal.
|
|
20187
20371
|
*/
|
|
20188
|
-
async runGsiOps(physicalId, ops, desiredAttributeDefinitions, maskSecrets
|
|
20372
|
+
async runGsiOps(physicalId, ops, desiredAttributeDefinitions, maskSecrets) {
|
|
20189
20373
|
const debug = (message) => this.logger.debug(maskSecrets(message));
|
|
20190
20374
|
for (const op of ops) {
|
|
20191
20375
|
const input = {
|
|
@@ -20244,7 +20428,7 @@ var DynamoDBTableProvider = class {
|
|
|
20244
20428
|
* resource clean. The message therefore says the warning may be the only
|
|
20245
20429
|
* signal, and names both numbers so the template edit needs no second look.
|
|
20246
20430
|
*/
|
|
20247
|
-
skipWarmThroughputDecrease(logicalId, physicalId, desired, live, maskSecrets
|
|
20431
|
+
skipWarmThroughputDecrease(logicalId, physicalId, desired, live, maskSecrets) {
|
|
20248
20432
|
const warn = (message) => this.logger.warn(maskSecrets(message));
|
|
20249
20433
|
if (!isWarmThroughputDecrease(desired, live)) return false;
|
|
20250
20434
|
warn(`AWS::DynamoDB::Table ${logicalId}: the requested WarmThroughput ${JSON.stringify(maskDeep(desired, maskSecrets))} is lower than the ${JSON.stringify(maskDeep({
|
|
@@ -49460,10 +49644,14 @@ var AppSyncProvider = class {
|
|
|
49460
49644
|
}
|
|
49461
49645
|
const explicit = resolveExplicitPhysicalId(input, null);
|
|
49462
49646
|
if (explicit) try {
|
|
49463
|
-
await this.getClient().send(new GetGraphqlApiCommand({ apiId: explicit }));
|
|
49647
|
+
const response = await this.getClient().send(new GetGraphqlApiCommand({ apiId: explicit }));
|
|
49464
49648
|
return {
|
|
49465
49649
|
physicalId: explicit,
|
|
49466
|
-
attributes: {
|
|
49650
|
+
attributes: definedAttributes({
|
|
49651
|
+
ApiId: explicit,
|
|
49652
|
+
Arn: response.graphqlApi?.arn,
|
|
49653
|
+
GraphQLUrl: response.graphqlApi?.uris?.["GRAPHQL"]
|
|
49654
|
+
})
|
|
49467
49655
|
};
|
|
49468
49656
|
} catch (err) {
|
|
49469
49657
|
if (err instanceof NotFoundException$4) return null;
|
|
@@ -75683,6 +75871,43 @@ const COMPOSITE_ID_SPLITTERS = {
|
|
|
75683
75871
|
},
|
|
75684
75872
|
propertiesOverlay: { FunctionName: functionName }
|
|
75685
75873
|
};
|
|
75874
|
+
},
|
|
75875
|
+
"AWS::AppSync::ApiKey": (physicalId, properties) => {
|
|
75876
|
+
const trimmed = physicalId.trim();
|
|
75877
|
+
if (!trimmed) throw new Error("empty physical id for AWS::AppSync::ApiKey (expected 'apiId|apiKeyId', the key ARN, or a bare apiKeyId)");
|
|
75878
|
+
const arnMatch = /^arn:[^:]+:appsync:[^:]*:[^:]*:apis\/([^/|]+)\/apikey\/([^/|]+)$/.exec(trimmed);
|
|
75879
|
+
if (arnMatch) {
|
|
75880
|
+
const [, apiId, apiKeyId] = arnMatch;
|
|
75881
|
+
return {
|
|
75882
|
+
resourceIdentifier: {
|
|
75883
|
+
ApiId: apiId,
|
|
75884
|
+
ApiKeyId: apiKeyId
|
|
75885
|
+
},
|
|
75886
|
+
propertiesOverlay: { ApiId: apiId }
|
|
75887
|
+
};
|
|
75888
|
+
}
|
|
75889
|
+
if (trimmed.startsWith("arn:")) throw new Error(`'${physicalId}' looks like an ARN but is not an AppSync API key ARN (expected arn:<partition>:appsync:<region>:<account>:apis/<apiId>/apikey/<apiKeyId>)`);
|
|
75890
|
+
const parts = trimmed.split("|");
|
|
75891
|
+
if (parts.length > 2) throw new Error(`expected 'apiId|apiKeyId', the key ARN, or a bare apiKeyId, got ${parts.length} parts: '${physicalId}'`);
|
|
75892
|
+
if (parts.length === 2) {
|
|
75893
|
+
const [apiId, apiKeyId] = parts;
|
|
75894
|
+
if (!apiId.trim() || !apiKeyId.trim()) throw new Error(`empty part in 'apiId|apiKeyId': '${physicalId}'`);
|
|
75895
|
+
return {
|
|
75896
|
+
resourceIdentifier: {
|
|
75897
|
+
ApiId: apiId,
|
|
75898
|
+
ApiKeyId: apiKeyId
|
|
75899
|
+
},
|
|
75900
|
+
propertiesOverlay: { ApiId: apiId }
|
|
75901
|
+
};
|
|
75902
|
+
}
|
|
75903
|
+
const apiId = readStringProperty(properties, "ApiId", "AWS::AppSync::ApiKey");
|
|
75904
|
+
return {
|
|
75905
|
+
resourceIdentifier: {
|
|
75906
|
+
ApiId: apiId,
|
|
75907
|
+
ApiKeyId: trimmed
|
|
75908
|
+
},
|
|
75909
|
+
propertiesOverlay: { ApiId: apiId }
|
|
75910
|
+
};
|
|
75686
75911
|
}
|
|
75687
75912
|
};
|
|
75688
75913
|
/**
|
|
@@ -76000,6 +76225,24 @@ function createIdentifierBackfillDeps(ec2Client) {
|
|
|
76000
76225
|
* | `AWS::AppSync::Resolver` | `ResolverArn` | `attributes.ResolverArn` |
|
|
76001
76226
|
* | `AWS::S3Tables::Table` | `TableARN` | `attributes.TableARN` |
|
|
76002
76227
|
* | `AWS::EC2::SecurityGroupIngress` | `Id` | `attributes.Id` (issue #1761) |
|
|
76228
|
+
* | `AWS::AppSync::GraphQLApi` | `Arn` (2026-09-18) | `attributes.Arn` (issue #3414) |
|
|
76229
|
+
*
|
|
76230
|
+
* The GraphQLApi row is the one whose identifier AWS RE-DECLARED under cdkd:
|
|
76231
|
+
* the 2026-09-17 public bundle flipped it from `ApiId` to `Arn` (issue #3327
|
|
76232
|
+
* recorded the two published sources disagreeing at the time), and by
|
|
76233
|
+
* 2026-09-18 `describe-type` in us-east-1 answers `["/properties/Arn"]` with a
|
|
76234
|
+
* full handler set. cdkd's physicalId is the bare `apiId`, which is NOT the ARN
|
|
76235
|
+
* and cannot be turned into one without account + partition, so the entry
|
|
76236
|
+
* reads the `Arn` attribute `AppSyncProvider.create` records. Because the
|
|
76237
|
+
* flip is recent and the two sources disagreed, the entry ALSO declares
|
|
76238
|
+
* `physicalIdIsIdentifierFor: ['ApiId']`: a schema that reports the OLD
|
|
76239
|
+
* single field is not a schema change that needs a code edit — for a record
|
|
76240
|
+
* cdkd deployed, the physicalId already IS that value — so `resolveCompositeId`
|
|
76241
|
+
* takes the plain single-key path for it instead of refusing by name. The
|
|
76242
|
+
* bypass excludes an ARN-shaped physicalId (a `--migrate-from-cloudformation`
|
|
76243
|
+
* record stores CloudFormation's `PhysicalResourceId`, the ARN), which under
|
|
76244
|
+
* a pre-flip registry keeps the loud cross-check refusal rather than shipping
|
|
76245
|
+
* an ARN as `ApiId`.
|
|
76003
76246
|
*
|
|
76004
76247
|
* The SG-ingress row is measured the same way as the rest, and separately
|
|
76005
76248
|
* against a real CloudFormation stack because the three strings that name one
|
|
@@ -76013,11 +76256,21 @@ function createIdentifierBackfillDeps(ec2Client) {
|
|
|
76013
76256
|
* `sgr-02345615af6d2db0d`, with `Ref` and `Fn::GetAtt .Id` both returning that
|
|
76014
76257
|
* same value.
|
|
76015
76258
|
*
|
|
76016
|
-
* All
|
|
76017
|
-
* IMPORT — unlike `AWS::Glue::Table`, the type issue #1659's title names,
|
|
76018
|
-
* CFn rejects outright (see {@link typeIsImportUnsupported}).
|
|
76259
|
+
* All five declare a `read` handler. For four of them CFn genuinely does accept
|
|
76260
|
+
* the IMPORT — unlike `AWS::Glue::Table`, the type issue #1659's title names,
|
|
76261
|
+
* which CFn rejects outright (see {@link typeIsImportUnsupported}). The fifth,
|
|
76262
|
+
* `AWS::AppSync::GraphQLApi`, is measured REFUSED despite its read handler
|
|
76263
|
+
* (see {@link CFN_IMPORT_REFUSED_DESPITE_REGISTRY}), so its row below is
|
|
76264
|
+
* reachable only under `--skip-import-support-preflight`.
|
|
76019
76265
|
*/
|
|
76020
76266
|
const COMPOSITE_PHYSICAL_ID_IDENTIFIERS = {
|
|
76267
|
+
"AWS::AppSync::GraphQLApi": recordedArnIdentifier({
|
|
76268
|
+
resourceType: "AWS::AppSync::GraphQLApi",
|
|
76269
|
+
field: "Arn",
|
|
76270
|
+
physicalIdShape: "<apiId>",
|
|
76271
|
+
arnServiceSegment: "appsync",
|
|
76272
|
+
physicalIdIsIdentifierFor: ["ApiId"]
|
|
76273
|
+
}),
|
|
76021
76274
|
"AWS::AppSync::DataSource": recordedArnIdentifier({
|
|
76022
76275
|
resourceType: "AWS::AppSync::DataSource",
|
|
76023
76276
|
field: "DataSourceArn",
|
|
@@ -76433,7 +76686,7 @@ function maskedIdentifierAttributeReason(field, logicalId) {
|
|
|
76433
76686
|
* and the remedy is the one that doc already gives.
|
|
76434
76687
|
*/
|
|
76435
76688
|
function recordedArnIdentifier(options) {
|
|
76436
|
-
const { resourceType, field, physicalIdShape, arnServiceSegment } = options;
|
|
76689
|
+
const { resourceType, field, physicalIdShape, arnServiceSegment, physicalIdIsIdentifierFor } = options;
|
|
76437
76690
|
/**
|
|
76438
76691
|
* Is this value usable AS the type's CFn identifier?
|
|
76439
76692
|
*
|
|
@@ -76450,12 +76703,14 @@ function recordedArnIdentifier(options) {
|
|
|
76450
76703
|
};
|
|
76451
76704
|
return {
|
|
76452
76705
|
field,
|
|
76706
|
+
...physicalIdIsIdentifierFor && { physicalIdIsIdentifierFor },
|
|
76453
76707
|
resolve: ({ logicalId, physicalId, attributes }) => {
|
|
76454
76708
|
const recorded = attributes[field];
|
|
76455
76709
|
if (typeof recorded === "string" && isUsableIdentifier(recorded)) return recorded.trim();
|
|
76456
76710
|
if (isUsableIdentifier(physicalId)) return physicalId.trim();
|
|
76457
76711
|
const recordedNote = typeof recorded === "string" && recorded.trim() ? `attributes.${field} is recorded as '${recorded.trim()}', which is not a ${arnServiceSegment} ARN` : `attributes.${field} is missing or empty`;
|
|
76458
|
-
|
|
76712
|
+
const shapeWord = physicalIdShape.includes("|") ? "the composite" : "the bare";
|
|
76713
|
+
throw new Error(`cdkd's physical id for ${resourceType} is ${shapeWord} '${physicalIdShape}', but CloudFormation IMPORT identifies the resource by its ${field}, which cdkd state does not record usably for '${logicalId}' (${recordedNote}). Re-deploy the stack once so cdkd records ${field}, then re-run cdkd export.`);
|
|
76459
76714
|
}
|
|
76460
76715
|
};
|
|
76461
76716
|
}
|
|
@@ -77333,6 +77588,15 @@ async function buildImportPlan(state, template, cfnClient, parentStackName, opti
|
|
|
77333
77588
|
}
|
|
77334
77589
|
let resolved;
|
|
77335
77590
|
try {
|
|
77591
|
+
const measuredRefusal = cfnRefusesImportDespiteRegistry(resourceType);
|
|
77592
|
+
if (!options.skipImportSupportPreflight && measuredRefusal !== void 0) {
|
|
77593
|
+
blocked.push({
|
|
77594
|
+
logicalId,
|
|
77595
|
+
resourceType,
|
|
77596
|
+
reason: `AWS CloudFormation does not support ${resourceType} in IMPORT changesets, ${measuredRefusal}. Its registry schema passes cdkd's read-handler pre-flight, so without this list CreateChangeSet would reject the whole export with "ResourceTypes [${resourceType}] are not supported for Import" after the stack was locked. Remove the resource from the stack before exporting (it stays in AWS and can be re-declared in CloudFormation afterwards), or destroy it first and let CloudFormation create it fresh. If AWS has since added IMPORT support for this type, re-run with --skip-import-support-preflight and please open a cdkd issue.`
|
|
77597
|
+
});
|
|
77598
|
+
continue;
|
|
77599
|
+
}
|
|
77336
77600
|
const schemaInfo = await cachedTypeSchemaInfo(resourceType, cfnClient, identifierCache);
|
|
77337
77601
|
if (!options.skipImportSupportPreflight && typeIsImportUnsupported(schemaInfo)) {
|
|
77338
77602
|
blocked.push({
|
|
@@ -77438,7 +77702,8 @@ function groupBlockedReasons(blocked) {
|
|
|
77438
77702
|
* IMPORT pre-flight and this resolution.
|
|
77439
77703
|
*/
|
|
77440
77704
|
async function resolveResourceIdentifier(resourceType, logicalId, physicalId, properties, attributes, entry, backfillDeps) {
|
|
77441
|
-
const
|
|
77705
|
+
const registered = Object.hasOwn(COMPOSITE_PHYSICAL_ID_IDENTIFIERS, resourceType) ? COMPOSITE_PHYSICAL_ID_IDENTIFIERS[resourceType] : void 0;
|
|
77706
|
+
const compositeIdentifier = registered && entry.fields.length === 1 && registered.physicalIdIsIdentifierFor?.includes(entry.fields[0]) && !physicalId.trim().startsWith("arn:") ? void 0 : registered;
|
|
77442
77707
|
if (compositeIdentifier) {
|
|
77443
77708
|
if (entry.fields.length !== 1 || entry.fields[0] !== compositeIdentifier.field) throw new Error(`${resourceType} is registered in COMPOSITE_PHYSICAL_ID_IDENTIFIERS as identified by '${compositeIdentifier.field}', but CloudFormation now reports primaryIdentifier [${entry.fields.join(", ")}]. The registry schema changed under cdkd — update the entry in src/cli/commands/export.ts (or move the type to COMPOSITE_ID_SPLITTERS if its identifier is now composite too)`);
|
|
77444
77709
|
const value = await resolveIdentifierValue(compositeIdentifier, {
|
|
@@ -77492,6 +77757,14 @@ async function cachedTypeSchemaInfo(resourceType, cfnClient, cache) {
|
|
|
77492
77757
|
* legacy types) and a `handlers` block whose keys are only create / update /
|
|
77493
77758
|
* delete (`AWS::EC2::NetworkAclEntry`).
|
|
77494
77759
|
*
|
|
77760
|
+
* That list is a MEASUREMENT, not a contract, and AWS moves types off it:
|
|
77761
|
+
* `AWS::AppSync::ApiKey` was re-published `FULLY_MUTABLE` with a full handler
|
|
77762
|
+
* set by 2026-09-18 (issue #3414), so it now passes this pre-flight and is
|
|
77763
|
+
* resolved by its `COMPOSITE_ID_SPLITTERS` entry instead. The refusal reads
|
|
77764
|
+
* the LIVE response, so such a move needs no edit here — only the resolution
|
|
77765
|
+
* that the refusal used to shadow. `AWS::AppSync::GraphQLSchema` is still on
|
|
77766
|
+
* the list as of that date (no `handlers` block, `NON_PROVISIONABLE`).
|
|
77767
|
+
*
|
|
77495
77768
|
* **Why the verdict needs TWO agreeing fields.** Reading "the `handlers` key is
|
|
77496
77769
|
* missing" as a refusal on its own makes any partial or unusual response block
|
|
77497
77770
|
* an export that works today — the one regression this pre-flight could cause,
|
|
@@ -77511,6 +77784,35 @@ function typeIsImportUnsupported(entry) {
|
|
|
77511
77784
|
return entry.importSupport === "unsupported";
|
|
77512
77785
|
}
|
|
77513
77786
|
/**
|
|
77787
|
+
* Types CloudFormation REFUSES in an IMPORT changeset although their registry
|
|
77788
|
+
* schema passes {@link typeIsImportUnsupported} — a `read` handler declared,
|
|
77789
|
+
* `ProvisioningType: FULLY_MUTABLE` — so the registry heuristic lets them
|
|
77790
|
+
* through and `CreateChangeSet` answers `ResourceTypes [<T>] are not
|
|
77791
|
+
* supported for Import` after the lock, the prompt and the template
|
|
77792
|
+
* preprocessing have already happened. The heuristic is NECESSARY (a type
|
|
77793
|
+
* with no read handler is never importable) but the 2026-09-18 measurement
|
|
77794
|
+
* showed it is not SUFFICIENT: AWS's supported-for-import list is a separate
|
|
77795
|
+
* fact the registry does not carry.
|
|
77796
|
+
*
|
|
77797
|
+
* Each entry is a dated measurement, and the map is the `blocked` message's
|
|
77798
|
+
* evidence. `--skip-import-support-preflight` bypasses this set together with
|
|
77799
|
+
* the heuristic, for the same reason it exists there: AWS may add support
|
|
77800
|
+
* without cdkd noticing, and the changeset is then the authority.
|
|
77801
|
+
*
|
|
77802
|
+
* | Type | measured |
|
|
77803
|
+
* |---|---|
|
|
77804
|
+
* | `AWS::AppSync::GraphQLApi` | us-east-1, 2026-09-18, the `export` integ fixture: registry `[read, create, update, delete, list]` / `FULLY_MUTABLE` / identifier `Arn`; `CreateChangeSet --change-set-type IMPORT` carrying the API (resolved to its ARN) rejected with `ResourceTypes [AWS::AppSync::GraphQLApi] are not supported for Import`. `AWS::AppSync::ApiKey` is NOT here: a standalone IMPORT changeset carrying one key with `{ApiId, ApiKeyId}` reached `CREATE_COMPLETE` the same day. (issue #3414) |
|
|
77805
|
+
*/
|
|
77806
|
+
const CFN_IMPORT_REFUSED_DESPITE_REGISTRY = /* @__PURE__ */ new Map([["AWS::AppSync::GraphQLApi", "measured in us-east-1 on 2026-09-18: the registry schema declares a read handler and FULLY_MUTABLE, yet CreateChangeSet --change-set-type IMPORT rejected the type"]]);
|
|
77807
|
+
/**
|
|
77808
|
+
* The dated measurement behind a {@link CFN_IMPORT_REFUSED_DESPITE_REGISTRY}
|
|
77809
|
+
* entry, or `undefined` when CloudFormation has not been measured refusing the
|
|
77810
|
+
* type. Exported for unit tests.
|
|
77811
|
+
*/
|
|
77812
|
+
function cfnRefusesImportDespiteRegistry(resourceType) {
|
|
77813
|
+
return CFN_IMPORT_REFUSED_DESPITE_REGISTRY.get(resourceType);
|
|
77814
|
+
}
|
|
77815
|
+
/**
|
|
77514
77816
|
* Fetch the primary identifier field names for a resource type, with a
|
|
77515
77817
|
* hardcoded single-key fallback when DescribeType is unavailable.
|
|
77516
77818
|
*/
|
|
@@ -79043,7 +79345,7 @@ async function confirmPrompt$3(prompt) {
|
|
|
79043
79345
|
return confirmOrRefuse(prompt, { refusal: "The cdkd export confirmation prompt cannot run in a non-interactive environment. Pass -y / --yes to confirm, or run the command from a real terminal." });
|
|
79044
79346
|
}
|
|
79045
79347
|
function createExportCommand() {
|
|
79046
|
-
const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.", parseStackRegion).option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--skip-import-support-preflight", "Skip the pre-flight that refuses resource types whose CloudFormation registry schema says CFn cannot IMPORT them (no read handler AND NON_PROVISIONABLE), and let CreateChangeSet answer instead.
|
|
79348
|
+
const cmd = new Command("export").description("Hand a cdkd-managed stack over to CloudFormation via CFn IMPORT (changeset). AWS resources are unchanged; cdkd state for the stack is deleted on success. Mirror of `cdkd import` (AWS → cdkd) in the reverse direction (cdkd → CFn). Accepts JSON and YAML templates (YAML via a CFn-aware codec that preserves !Ref / !GetAtt / !Sub shorthand). Aborts if any resource is not CFn-importable.").argument("[stack]", "Stack name to export (auto-detected for single-stack apps)").option("--cfn-stack-name <name>", "Name of the destination CloudFormation stack for the root stack. Defaults to the cdkd stack name (or the cdkd2cfnStackName mapping when the name contains '~'). For per-nested-child overrides, use --cfn-child-stack-name.").option("--cfn-child-stack-name <pair...>", "Per-nested-child CFn stack-name override. Repeatable. Format: '<cdkdName>=<cfnName>' (e.g. --cfn-child-stack-name 'MyApp~Database=my-app-db'). The cdkd stack name for a nested child is '<parent>~<childLogicalId>' (v6 state-key form). Without an override the CFn name is derived by replacing '~' with '-'. Only consulted when the exported stack tree contains nested children; ignored for flat stacks.").option("--template <path>", "Path to a pre-rendered CloudFormation template (JSON or YAML — format auto-detected). Skips synth.").option("--stack-region <region>", "Region of the cdkd state record to operate on. Required when the same stack name has state in multiple regions.", parseStackRegion).option("--dry-run", "Print the import plan without creating a changeset.", false).option("--accept-transient-context", "Allow CLI -c key=value overrides at export time even though they are not persisted to cdk.json / cdk.context.json (default: refuse). When set, the user is responsible for passing the same -c flags to every future cdk deploy.", false).option("--include-non-importable", "Run a 2-phase migration when the stack contains non-importable resources (Custom::*). Phase 1 imports the importable resources; phase 2 CFn-CREATEs the non-importable ones, which re-invokes each Custom Resource's onCreate handler. Make sure onCreate is idempotent before enabling.", false).option("--parameter <key=value...>", "CFn template Parameter override, repeatable. Required when the synthesized template has Parameters without Default values; otherwise overrides the template's default value. Format: --parameter Key=Value.").option("--strict-cross-stack", "Refuse to export when sibling cdkd stacks in the same CDK app reference the exporting stack via Fn::GetStackOutput. Without the flag, cdkd warns and proceeds — the consumers keep resolving via the CloudFormation fallback (weak reference) unless they deploy with --no-cfn-fallback.", false).option("--skip-import-support-preflight", "Skip the pre-flight that refuses resource types whose CloudFormation registry schema says CFn cannot IMPORT them (no read handler AND NON_PROVISIONABLE), AND cdkd's short list of types measured refused by CreateChangeSet despite their registry schema (AWS::AppSync::GraphQLApi), and let CreateChangeSet answer instead. Neither check is AWS's supported-for-import list, so this is the escape hatch for a type AWS has since made importable. Expect the changeset to be rejected if the check was right.", false).option("--no-recreate-import-unsupported", "Block instead of auto-handling resource types AWS does NOT support in IMPORT changesets (AWS::ApiGatewayV2::Stage and AWS::IAM::Policy). Default behavior: cdkd skips these from phase 1, deletes the AWS-side resource between phases, and lets CFn re-CREATE in phase 2 (brief unavailability window). With this flag, the export aborts with a clear error instead.").action(withErrorHandling(exportCommand));
|
|
79047
79349
|
[
|
|
79048
79350
|
...commonOptions,
|
|
79049
79351
|
...appOptions,
|
|
@@ -80176,11 +80478,12 @@ async function stateOrphanCommand(stackArgs, options) {
|
|
|
80176
80478
|
if (!options.force) {
|
|
80177
80479
|
for (const target of targets) if (await setup.lockManager.isLocked(stackName, target.region)) {
|
|
80178
80480
|
const where = target.region ? displaySafe(target.region, { asciiOnly: true }) || "<unrenderable>" : "legacy";
|
|
80179
|
-
|
|
80481
|
+
const recoveryCommand = buildForceUnlockCommand(stackName, target.region, {
|
|
80180
80482
|
profile: options.profile,
|
|
80181
80483
|
stateBucket: setup.bucket,
|
|
80182
80484
|
statePrefix: options.statePrefix
|
|
80183
|
-
})
|
|
80485
|
+
});
|
|
80486
|
+
throw new Error(`Stack '${displaySafe(stackName, { asciiOnly: true })}' (${where}) is locked. ` + (recoveryCommand ? `Run: ${recoveryCommand} first, or pass --force to remove anyway.` : `${UNREPRODUCIBLE_LOCK_CLAUSE} Or pass --force to remove anyway.`));
|
|
80184
80487
|
}
|
|
80185
80488
|
}
|
|
80186
80489
|
if (!options.yes && !options.force) {
|
|
@@ -88249,11 +88552,11 @@ async function pullEcrImage(imageUri, options) {
|
|
|
88249
88552
|
const cached = ASSUMED_ROLE_CACHE.get(cacheKey);
|
|
88250
88553
|
if (cached && isCredentialFresh(cached)) {
|
|
88251
88554
|
assumed = cached;
|
|
88252
|
-
logger.debug(`Reusing cached AssumeRole credentials for ${options.ecrRoleArn}`);
|
|
88555
|
+
logger.debug(`Reusing cached AssumeRole credentials for ${displayIdent(options.ecrRoleArn, { maxCodePoints: 612 })}`);
|
|
88253
88556
|
} else {
|
|
88254
88557
|
assumed = await assumeRoleForEcr(options.ecrRoleArn, callerRegion, logger);
|
|
88255
88558
|
ASSUMED_ROLE_CACHE.set(cacheKey, assumed);
|
|
88256
|
-
logger.info(`Assumed role ${options.ecrRoleArn} for ECR pull (account=${parsed.accountId}, region=${parsed.region})`);
|
|
88559
|
+
logger.info(`Assumed role ${displayIdent(options.ecrRoleArn, { maxCodePoints: 612 })} for ECR pull (account=${parsed.accountId}, region=${parsed.region})`);
|
|
88257
88560
|
}
|
|
88258
88561
|
} else if (crossAccount) logger.info(`Cross-account ECR pull: image account ${parsed.accountId} != caller ${callerAccount}. Using the caller's credentials; pass --ecr-role-arn <arn> if AWS rejects with AccessDenied.`);
|
|
88259
88562
|
if (crossRegion) logger.info(`Cross-region ECR pull: image region ${parsed.region} != caller ${callerRegion ?? "(unset)"}. Authenticating against the image region directly.`);
|
|
@@ -88284,7 +88587,7 @@ async function pullEcrImage(imageUri, options) {
|
|
|
88284
88587
|
* mirrors the convention used by `src/utils/role-arn.ts`.
|
|
88285
88588
|
*/
|
|
88286
88589
|
async function assumeRoleForEcr(roleArn, callerRegion, logger) {
|
|
88287
|
-
logger.debug(`Assuming role ${roleArn} for ECR pull...`);
|
|
88590
|
+
logger.debug(`Assuming role ${displayIdent(roleArn, { maxCodePoints: 612 })} for ECR pull...`);
|
|
88288
88591
|
const sts = new STSClient({
|
|
88289
88592
|
...awsClientDefaults(),
|
|
88290
88593
|
...callerRegion && { region: callerRegion }
|
|
@@ -88295,7 +88598,7 @@ async function assumeRoleForEcr(roleArn, callerRegion, logger) {
|
|
|
88295
88598
|
RoleSessionName: `cdkd-local-ecr-${Date.now()}`,
|
|
88296
88599
|
DurationSeconds: 3600
|
|
88297
88600
|
}))).Credentials;
|
|
88298
|
-
if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalInvokeBuildError$1(`AssumeRole(${roleArn}) returned no usable credentials. Verify the role's trust policy allows your identity to assume it.`);
|
|
88601
|
+
if (!creds || !creds.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new LocalInvokeBuildError$1(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials. Verify the role's trust policy allows your identity to assume it.`);
|
|
88299
88602
|
return {
|
|
88300
88603
|
accessKeyId: creds.AccessKeyId,
|
|
88301
88604
|
secretAccessKey: creds.SecretAccessKey,
|
|
@@ -88305,7 +88608,7 @@ async function assumeRoleForEcr(roleArn, callerRegion, logger) {
|
|
|
88305
88608
|
} catch (err) {
|
|
88306
88609
|
if (err instanceof LocalInvokeBuildError$1) throw err;
|
|
88307
88610
|
const reason = err instanceof Error ? err.message : String(err);
|
|
88308
|
-
throw new LocalInvokeBuildError$1(`Failed to assume role ${roleArn} for ECR pull: ${reason}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
|
|
88611
|
+
throw new LocalInvokeBuildError$1(`Failed to assume role ${displayIdent(roleArn, { maxCodePoints: 612 })} for ECR pull: ${displaySafe(reason)}. Verify the role exists and its trust policy permits the caller's identity to assume it.`);
|
|
88309
88612
|
} finally {
|
|
88310
88613
|
sts.destroy();
|
|
88311
88614
|
}
|
|
@@ -88623,7 +88926,7 @@ const CONTAINER_AWS_CREDENTIALS_PATH = "/cdkd-aws/credentials";
|
|
|
88623
88926
|
*/
|
|
88624
88927
|
async function writeProfileCredentialsFile(profileName, creds) {
|
|
88625
88928
|
if (profileName === "") throw new Error("writeProfileCredentialsFile: profile name must not be empty.");
|
|
88626
|
-
if (/[\r\n[\]]/.test(profileName)) throw new Error(`writeProfileCredentialsFile: profile name
|
|
88929
|
+
if (/[\r\n[\]]/.test(profileName)) throw new Error(`writeProfileCredentialsFile: profile name ${displayIdent(profileName)} contains a forbidden character (any of CR, LF, '[', ']' would corrupt the INI file or the docker -e env var).`);
|
|
88627
88930
|
const dir = await mkdtemp(path.join(tmpdir(), "cdkd-profile-creds-"));
|
|
88628
88931
|
const hostPath = path.join(dir, "credentials");
|
|
88629
88932
|
const lines = [
|
|
@@ -88632,7 +88935,15 @@ async function writeProfileCredentialsFile(profileName, creds) {
|
|
|
88632
88935
|
`aws_secret_access_key = ${creds.secretAccessKey}`
|
|
88633
88936
|
];
|
|
88634
88937
|
if (creds.sessionToken) lines.push(`aws_session_token = ${creds.sessionToken}`);
|
|
88635
|
-
|
|
88938
|
+
try {
|
|
88939
|
+
await writeFile(hostPath, lines.join("\n") + "\n", { mode: 384 });
|
|
88940
|
+
} catch (err) {
|
|
88941
|
+
await rm(dir, {
|
|
88942
|
+
recursive: true,
|
|
88943
|
+
force: true
|
|
88944
|
+
}).catch(() => {});
|
|
88945
|
+
throw err;
|
|
88946
|
+
}
|
|
88636
88947
|
return {
|
|
88637
88948
|
hostPath,
|
|
88638
88949
|
containerPath: CONTAINER_AWS_CREDENTIALS_PATH,
|
|
@@ -90645,7 +90956,11 @@ async function resolveProfileCredentials(profile) {
|
|
|
90645
90956
|
try {
|
|
90646
90957
|
const credsProvider = sts.config.credentials;
|
|
90647
90958
|
const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
|
|
90648
|
-
if (!creds || !creds.accessKeyId || !creds.secretAccessKey)
|
|
90959
|
+
if (!creds || !creds.accessKeyId || !creds.secretAccessKey) {
|
|
90960
|
+
const shownProfile = displayIdent(profile);
|
|
90961
|
+
const ssoHint = isPasteableIdent(profile) ? "run `aws sso login --profile " + profile + "`" : "run `aws sso login` for that profile -- cdkd is not writing the command out, because it cannot vouch that the name as rendered is the one your shell would act on";
|
|
90962
|
+
throw new Error(`--profile ${shownProfile}: credential provider chain resolved without usable credentials. For SSO profiles, ${ssoHint}. For regular profiles, check \`~/.aws/credentials\` / \`~/.aws/config\`.`);
|
|
90963
|
+
}
|
|
90649
90964
|
return {
|
|
90650
90965
|
accessKeyId: creds.accessKeyId,
|
|
90651
90966
|
secretAccessKey: creds.secretAccessKey,
|
|
@@ -90672,7 +90987,7 @@ async function assumeLambdaExecutionRole$1(roleArn, region) {
|
|
|
90672
90987
|
RoleSessionName: `cdkd-local-start-api-${Date.now()}`,
|
|
90673
90988
|
DurationSeconds: 3600
|
|
90674
90989
|
}))).Credentials;
|
|
90675
|
-
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
|
|
90990
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials.`);
|
|
90676
90991
|
return {
|
|
90677
90992
|
accessKeyId: creds.AccessKeyId,
|
|
90678
90993
|
secretAccessKey: creds.SecretAccessKey,
|
|
@@ -91234,7 +91549,7 @@ async function resolveEcsSecrets(entries, options = {}) {
|
|
|
91234
91549
|
try {
|
|
91235
91550
|
return await Promise.all(entries.map(async (entry) => {
|
|
91236
91551
|
const value = await resolveOne(entry, secretsClient, ssmClient);
|
|
91237
|
-
logger.debug(`Resolved secret ${entry.containerName}.${entry.name} (${entry.valueFrom})`);
|
|
91552
|
+
logger.debug(`Resolved secret ${displayIdent(entry.containerName)}.${displayIdent(entry.name)} (${displayIdent(entry.valueFrom, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })})`);
|
|
91238
91553
|
return {
|
|
91239
91554
|
...entry,
|
|
91240
91555
|
value
|
|
@@ -91251,7 +91566,7 @@ async function resolveOne(entry, secretsClient, ssmClient) {
|
|
|
91251
91566
|
switch (shape.kind) {
|
|
91252
91567
|
case "secrets-manager": return resolveSecretsManager(entry, shape, secretsClient);
|
|
91253
91568
|
case "ssm": return resolveSsm(entry, shape, ssmClient);
|
|
91254
|
-
case "unknown": throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' references an unsupported ValueFrom shape
|
|
91569
|
+
case "unknown": throw new EcsSecretsResolutionError(`Container '${displayIdent(entry.containerName)}' secret '${displayIdent(entry.name)}' references an unsupported ValueFrom shape ${displayIdent(arn, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}. Expected Secrets Manager ARN (optionally with :<json-key>::) or SSM Parameter ARN.`);
|
|
91255
91570
|
}
|
|
91256
91571
|
}
|
|
91257
91572
|
/**
|
|
@@ -91284,20 +91599,20 @@ async function resolveSecretsManager(entry, shape, client) {
|
|
|
91284
91599
|
try {
|
|
91285
91600
|
secretString = (await client.send(new GetSecretValueCommand({ SecretId: shape.baseArn }))).SecretString;
|
|
91286
91601
|
} catch (err) {
|
|
91287
|
-
throw new EcsSecretsResolutionError(`Failed to resolve Secrets Manager secret for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}): ${err instanceof Error ? err.message : String(err)}`);
|
|
91602
|
+
throw new EcsSecretsResolutionError(`Failed to resolve Secrets Manager secret for container '${displayIdent(entry.containerName)}' / env '${displayIdent(entry.name)}' (${displayIdent(shape.baseArn, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}): ${displaySafe(err instanceof Error ? err.message : String(err))}`);
|
|
91288
91603
|
}
|
|
91289
|
-
if (secretString === void 0) throw new EcsSecretsResolutionError(`Secrets Manager returned no SecretString for container '${entry.containerName}' / env '${entry.name}' (${shape.baseArn}). Binary secrets are not supported.`);
|
|
91604
|
+
if (secretString === void 0) throw new EcsSecretsResolutionError(`Secrets Manager returned no SecretString for container '${displayIdent(entry.containerName)}' / env '${displayIdent(entry.name)}' (${displayIdent(shape.baseArn, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}). Binary secrets are not supported.`);
|
|
91290
91605
|
if (shape.jsonKey === void 0) return secretString;
|
|
91291
91606
|
let parsed;
|
|
91292
91607
|
try {
|
|
91293
91608
|
parsed = JSON.parse(secretString);
|
|
91294
91609
|
} catch (err) {
|
|
91295
91610
|
const kind = err instanceof Error ? err.name : "unknown";
|
|
91296
|
-
throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but the secret value is not valid JSON (${kind}). The parser detail is withheld because it would echo the secret plaintext.`);
|
|
91611
|
+
throw new EcsSecretsResolutionError(`Container '${displayIdent(entry.containerName)}' secret '${displayIdent(entry.name)}' specified json-key '${displaySafe(shape.jsonKey)}' but the secret value is not valid JSON (${displayIdent(kind)}). The parser detail is withheld because it would echo the secret plaintext.`);
|
|
91297
91612
|
}
|
|
91298
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but the secret root is not a JSON object.`);
|
|
91613
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new EcsSecretsResolutionError(`Container '${displayIdent(entry.containerName)}' secret '${displayIdent(entry.name)}' specified json-key '${displaySafe(shape.jsonKey)}' but the secret root is not a JSON object.`);
|
|
91299
91614
|
const value = parsed[shape.jsonKey];
|
|
91300
|
-
if (value === void 0) throw new EcsSecretsResolutionError(`Container '${entry.containerName}' secret '${entry.name}' specified json-key '${shape.jsonKey}' but no such key exists in the secret JSON.`);
|
|
91615
|
+
if (value === void 0) throw new EcsSecretsResolutionError(`Container '${displayIdent(entry.containerName)}' secret '${displayIdent(entry.name)}' specified json-key '${displaySafe(shape.jsonKey)}' but no such key exists in the secret JSON.`);
|
|
91301
91616
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
91302
91617
|
}
|
|
91303
91618
|
async function resolveSsm(entry, shape, client) {
|
|
@@ -91306,11 +91621,11 @@ async function resolveSsm(entry, shape, client) {
|
|
|
91306
91621
|
Name: shape.name,
|
|
91307
91622
|
WithDecryption: true
|
|
91308
91623
|
}))).Parameter?.Value;
|
|
91309
|
-
if (value === void 0) throw new EcsSecretsResolutionError(`SSM parameter '${shape.name}' returned no Value for container '${entry.containerName}' / env '${entry.name}'.`);
|
|
91624
|
+
if (value === void 0) throw new EcsSecretsResolutionError(`SSM parameter '${displayIdent(shape.name, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}' returned no Value for container '${displayIdent(entry.containerName)}' / env '${displayIdent(entry.name)}'.`);
|
|
91310
91625
|
return value;
|
|
91311
91626
|
} catch (err) {
|
|
91312
91627
|
if (err instanceof EcsSecretsResolutionError) throw err;
|
|
91313
|
-
throw new EcsSecretsResolutionError(`Failed to resolve SSM parameter for container '${entry.containerName}' / env '${entry.name}' (${shape.name}): ${err instanceof Error ? err.message : String(err)}`);
|
|
91628
|
+
throw new EcsSecretsResolutionError(`Failed to resolve SSM parameter for container '${displayIdent(entry.containerName)}' / env '${displayIdent(entry.name)}' (${displayIdent(shape.name, { maxCodePoints: SECRET_REF_MAX_CODE_POINTS })}): ${displaySafe(err instanceof Error ? err.message : String(err))}`);
|
|
91314
91629
|
}
|
|
91315
91630
|
}
|
|
91316
91631
|
|
|
@@ -91914,8 +92229,9 @@ async function localRunTaskCommand(target, options) {
|
|
|
91914
92229
|
const state = createEcsRunState();
|
|
91915
92230
|
let sigintHandler;
|
|
91916
92231
|
let sigintCount = 0;
|
|
92232
|
+
let detachedSuccessfully = false;
|
|
91917
92233
|
let stateProvider;
|
|
91918
|
-
let
|
|
92234
|
+
let channels;
|
|
91919
92235
|
let cleanupPromise;
|
|
91920
92236
|
const cleanup = async () => {
|
|
91921
92237
|
if (!cleanupPromise) cleanupPromise = (async () => {
|
|
@@ -91924,10 +92240,10 @@ async function localRunTaskCommand(target, options) {
|
|
|
91924
92240
|
} catch (err) {
|
|
91925
92241
|
getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
91926
92242
|
}
|
|
91927
|
-
if (profileCredsFile) try {
|
|
91928
|
-
await profileCredsFile.dispose();
|
|
92243
|
+
if (channels?.profileCredsFile) try {
|
|
92244
|
+
await channels.profileCredsFile.dispose();
|
|
91929
92245
|
} catch (err) {
|
|
91930
|
-
getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
92246
|
+
getLogger().debug(`Failed to remove profile credentials tmpdir ${channels.profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
91931
92247
|
}
|
|
91932
92248
|
})();
|
|
91933
92249
|
await cleanupPromise;
|
|
@@ -91993,33 +92309,20 @@ async function localRunTaskCommand(target, options) {
|
|
|
91993
92309
|
resolvedRoleArn = options.assumeTaskRole;
|
|
91994
92310
|
assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
|
|
91995
92311
|
}
|
|
91996
|
-
|
|
91997
|
-
if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
|
|
92312
|
+
channels = await resolveTaskCredentialChannels(options, assumedCredentials);
|
|
91998
92313
|
const envOverrides = readEnvOverridesFile$2(options.envVars);
|
|
91999
|
-
const runOpts = {
|
|
92000
|
-
|
|
92001
|
-
|
|
92002
|
-
|
|
92003
|
-
keepRunning: options.keepRunning,
|
|
92004
|
-
detach: options.detach
|
|
92005
|
-
};
|
|
92006
|
-
if (envOverrides) runOpts.envOverrides = envOverrides;
|
|
92007
|
-
if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
|
|
92008
|
-
if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
|
|
92009
|
-
if (options.platform) runOpts.platformOverride = options.platform;
|
|
92010
|
-
if (options.region) runOpts.region = options.region;
|
|
92011
|
-
if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
|
|
92012
|
-
if (profileCredsFile) runOpts.profileCredentialsFile = {
|
|
92013
|
-
hostPath: profileCredsFile.hostPath,
|
|
92014
|
-
containerPath: profileCredsFile.containerPath,
|
|
92015
|
-
profileName: profileCredsFile.profileName
|
|
92016
|
-
};
|
|
92314
|
+
const runOpts = buildRunEcsTaskOptions(options, channels, {
|
|
92315
|
+
...envOverrides !== void 0 && { envOverrides },
|
|
92316
|
+
...resolvedRoleArn !== void 0 && { resolvedRoleArn }
|
|
92317
|
+
});
|
|
92017
92318
|
const hostGatewayExtraHosts = await resolveHostGatewayExtraHosts();
|
|
92018
92319
|
if (hostGatewayExtraHosts.length > 0) runOpts.hostGatewayExtraHosts = hostGatewayExtraHosts;
|
|
92019
92320
|
const result = await runEcsTask(task, runOpts, state);
|
|
92020
92321
|
if (options.detach) {
|
|
92021
92322
|
logger.info("Task containers started in detached mode; cdkd is exiting.");
|
|
92022
92323
|
logger.info(`Use 'docker ps -a --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
|
|
92324
|
+
if (channels.profileCredsFile) logger.info(`The AWS credentials file mounted into the containers is NOT removed in detached mode: delete ${displayIdent(channels.profileCredsFile.hostPath)} once you tear them down.`);
|
|
92325
|
+
detachedSuccessfully = true;
|
|
92023
92326
|
sigintCount = 99;
|
|
92024
92327
|
return;
|
|
92025
92328
|
}
|
|
@@ -92028,7 +92331,7 @@ async function localRunTaskCommand(target, options) {
|
|
|
92028
92331
|
} finally {
|
|
92029
92332
|
if (sigintHandler) process.off("SIGINT", sigintHandler);
|
|
92030
92333
|
if (stateProvider) stateProvider.dispose();
|
|
92031
|
-
if (!
|
|
92334
|
+
if (!detachedSuccessfully) await cleanup();
|
|
92032
92335
|
}
|
|
92033
92336
|
}
|
|
92034
92337
|
/**
|
|
@@ -92047,7 +92350,7 @@ async function resolvePlaceholderAccount(arn, region) {
|
|
|
92047
92350
|
});
|
|
92048
92351
|
try {
|
|
92049
92352
|
const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
|
|
92050
|
-
if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN
|
|
92353
|
+
if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN ${displayIdent(arn, { maxCodePoints: 612 })}. Pass the ARN explicitly: --assume-task-role <arn>`);
|
|
92051
92354
|
return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
|
|
92052
92355
|
} finally {
|
|
92053
92356
|
sts.destroy();
|
|
@@ -92069,7 +92372,7 @@ async function assumeTaskRole(roleArn, region) {
|
|
|
92069
92372
|
RoleSessionName: `cdkd-local-run-task-${Date.now()}`,
|
|
92070
92373
|
DurationSeconds: 3600
|
|
92071
92374
|
}))).Credentials;
|
|
92072
|
-
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
|
|
92375
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials.`);
|
|
92073
92376
|
return {
|
|
92074
92377
|
accessKeyId: creds.AccessKeyId,
|
|
92075
92378
|
secretAccessKey: creds.SecretAccessKey,
|
|
@@ -92256,6 +92559,106 @@ async function resolveSidecarCredentials(options, assumedCredentials) {
|
|
|
92256
92559
|
if (assumedCredentials) return assumedCredentials;
|
|
92257
92560
|
if (options.profile) return resolveProfileCredentials(options.profile);
|
|
92258
92561
|
}
|
|
92562
|
+
/**
|
|
92563
|
+
* Resolve BOTH credential channels a locally-run ECS task receives, and decide
|
|
92564
|
+
* whether the second one exists at all.
|
|
92565
|
+
*
|
|
92566
|
+
* 1. `sidecarCredentials` — what the AWS-published
|
|
92567
|
+
* `amazon-ecs-local-container-endpoints` sidecar serves at
|
|
92568
|
+
* `169.254.170.2/role/<role>`; {@link resolveSidecarCredentials} owns the
|
|
92569
|
+
* precedence (issue #658).
|
|
92570
|
+
* 2. `profileCredsFile` — the ECS analogue of PR #670 (Lambda-container
|
|
92571
|
+
* fix-back finding #1). When `--profile <p>` is set AND `--assume-task-role`
|
|
92572
|
+
* did NOT produce credentials for this task, a host-side AWS shared
|
|
92573
|
+
* credentials file is written under `[<options.profile>]` and bind-mounted
|
|
92574
|
+
* read-only into every user container, so handler code calling
|
|
92575
|
+
* `fromIni({ profile: '<options.profile>' })` resolves to the same creds the
|
|
92576
|
+
* sidecar serves. Without it the SDK looks for `[<options.profile>]` in
|
|
92577
|
+
* `~/.aws/credentials` INSIDE the container and fails.
|
|
92578
|
+
*
|
|
92579
|
+
* Gating on `!assumedCredentials` preserves the documented precedence
|
|
92580
|
+
* (assume-task-role > profile-file > sidecar): when `--assume-task-role`
|
|
92581
|
+
* won, the sidecar's `/role/<arn>` endpoint already serves the assumed creds
|
|
92582
|
+
* and the file's env vars must NOT override them.
|
|
92583
|
+
*
|
|
92584
|
+
* ONE helper rather than two, and that is the substance of issue
|
|
92585
|
+
* go-to-k/cdkd#3378 rather than a tidying choice. Both of these lived inline in
|
|
92586
|
+
* `localRunTaskCommand`'s body, which meant the gate's annotation below was
|
|
92587
|
+
* fenced only for EXISTING (`local-surface-env-identity.test.ts` checks that an
|
|
92588
|
+
* annotation is there and carries a reason — it cannot check that the reason is
|
|
92589
|
+
* TRUE). And the reason is not a property of the `if` at all: it is a property
|
|
92590
|
+
* of the COMPOSITION, that `sidecarCredentials` on the `!assumedCredentials`
|
|
92591
|
+
* path can only have come from {@link resolveSidecarCredentials}'s `--profile`
|
|
92592
|
+
* arm. Extracting the three-line gate ALONE would have re-tested the same three
|
|
92593
|
+
* booleans and proved nothing; a test over this helper writes a real file and
|
|
92594
|
+
* can read the bytes back. `resolveSidecarCredentials`'s own doc already set
|
|
92595
|
+
* the precedent for the seam over a command-level harness — that harness would
|
|
92596
|
+
* have to mock the whole Synth + Docker + AWS pipeline, and a mock is exactly
|
|
92597
|
+
* what cannot answer "which identity's bytes landed in the file".
|
|
92598
|
+
*
|
|
92599
|
+
* cdkd-local-env-identity: the `!assumedCredentials` gate means
|
|
92600
|
+
* `resolveSidecarCredentials` reached its `--profile` arm, so this is
|
|
92601
|
+
* `resolveProfileCredentials` through
|
|
92602
|
+
* `awsClientDefaults({ ignoreAssumedRole: true })` — the caller's own chain,
|
|
92603
|
+
* never the `--role-arn` role — and never an `--assume-task-role` STS result,
|
|
92604
|
+
* which wins earlier and is served by the metadata sidecar instead.
|
|
92605
|
+
*/
|
|
92606
|
+
async function resolveTaskCredentialChannels(options, assumedCredentials) {
|
|
92607
|
+
const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
|
|
92608
|
+
if (options.profile && sidecarCredentials && !assumedCredentials) return {
|
|
92609
|
+
sidecarCredentials,
|
|
92610
|
+
profileCredsFile: await writeProfileCredentialsFile(options.profile, sidecarCredentials)
|
|
92611
|
+
};
|
|
92612
|
+
return {
|
|
92613
|
+
sidecarCredentials,
|
|
92614
|
+
profileCredsFile: void 0
|
|
92615
|
+
};
|
|
92616
|
+
}
|
|
92617
|
+
/**
|
|
92618
|
+
* Build the `RunEcsTaskOptions` bag from the options and the resolved credential
|
|
92619
|
+
* channels.
|
|
92620
|
+
*
|
|
92621
|
+
* Extracted for the WIRING, which is the half
|
|
92622
|
+
* {@link resolveTaskCredentialChannels} could not cover (go-to-k/cdkd#3390 round
|
|
92623
|
+
* 2, on go-to-k/cdkd#3394). That helper decides WHETHER a credentials file
|
|
92624
|
+
* exists and whose bytes are in it; this one decides whether the answer reaches
|
|
92625
|
+
* the runner at all. Between them sat a single assignment, and mutating it to
|
|
92626
|
+
* `undefined` left 114 tests green across five files: no file is bind-mounted,
|
|
92627
|
+
* so every handler calling `fromIni({ profile })` inside the task container
|
|
92628
|
+
* fails -- the exact defect PR go-to-k/cdkd#670 shipped this path to fix. "A
|
|
92629
|
+
* probed callee says nothing about its WIRING" is the rule, and this is the
|
|
92630
|
+
* seam that lets a unit test say something about it.
|
|
92631
|
+
*
|
|
92632
|
+
* PURE, and it takes the whole `channels` object rather than a pre-destructured
|
|
92633
|
+
* file so the test subject is the same value the resolver returns -- a helper
|
|
92634
|
+
* taking `profileCredsFile` alone would move the untested assignment rather
|
|
92635
|
+
* than remove it.
|
|
92636
|
+
*
|
|
92637
|
+
* The file is COPIED field by field rather than passed through: `dispose` stays
|
|
92638
|
+
* with the command, which owns the lifetime, and `RunEcsTaskOptions` deliberately
|
|
92639
|
+
* declares only the three fields the runner mounts with.
|
|
92640
|
+
*/
|
|
92641
|
+
function buildRunEcsTaskOptions(options, channels, extra = {}) {
|
|
92642
|
+
const runOpts = {
|
|
92643
|
+
cluster: options.cluster,
|
|
92644
|
+
containerHost: options.containerHost,
|
|
92645
|
+
skipPull: options.pull === false,
|
|
92646
|
+
keepRunning: options.keepRunning,
|
|
92647
|
+
detach: options.detach
|
|
92648
|
+
};
|
|
92649
|
+
if (extra.envOverrides) runOpts.envOverrides = extra.envOverrides;
|
|
92650
|
+
if (channels.sidecarCredentials) runOpts.taskCredentials = channels.sidecarCredentials;
|
|
92651
|
+
if (extra.resolvedRoleArn) runOpts.taskRoleArn = extra.resolvedRoleArn;
|
|
92652
|
+
if (options.platform) runOpts.platformOverride = options.platform;
|
|
92653
|
+
if (options.region) runOpts.region = options.region;
|
|
92654
|
+
if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
|
|
92655
|
+
if (channels.profileCredsFile) runOpts.profileCredentialsFile = {
|
|
92656
|
+
hostPath: channels.profileCredsFile.hostPath,
|
|
92657
|
+
containerPath: channels.profileCredsFile.containerPath,
|
|
92658
|
+
profileName: channels.profileCredsFile.profileName
|
|
92659
|
+
};
|
|
92660
|
+
return runOpts;
|
|
92661
|
+
}
|
|
92259
92662
|
function createLocalRunTaskCommand() {
|
|
92260
92663
|
const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix.").argument("<target>", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default("cdkd-local")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries (#455). Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. A same-account pull in ANY region does not need this flag: the ECR client is built for the image URI's own region, so crossing a region costs nothing extra (issue #2536).")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-state", "Read cdkd S3 state for the target stack and substitute Fn::Sub / Fn::GetAtt references to same-stack AWS::ECR::Repository resources with the deployed URI. Off by default — only the AWS pseudo-parameter tier (${AWS::AccountId} / ${AWS::Region}) is resolved without this flag.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-state when the same stack name has state in multiple regions, and with --from-cfn-stack as the CFn client region (cdkd does not have a separate --cfn-stack-region flag).").argParser(parseStackRegion)).action(withErrorHandling(localRunTaskCommand));
|
|
92261
92664
|
[
|
|
@@ -93126,7 +93529,13 @@ async function resolveHostCredentialsForSigV4(options, resolved, loaded, region)
|
|
|
93126
93529
|
if (assumeRoleArn) try {
|
|
93127
93530
|
return await assumeAgentCoreExecutionRole(assumeRoleArn, region);
|
|
93128
93531
|
} catch (err) {
|
|
93129
|
-
logger.warn(
|
|
93532
|
+
logger.warn(assumeRoleFallbackWarning({
|
|
93533
|
+
assumeRoleArn,
|
|
93534
|
+
purpose: "--sigv4 signing",
|
|
93535
|
+
error: err,
|
|
93536
|
+
profile: options.profile,
|
|
93537
|
+
fallbackWithoutProfile: "shell credentials"
|
|
93538
|
+
}));
|
|
93130
93539
|
}
|
|
93131
93540
|
if (options.profile) {
|
|
93132
93541
|
const creds = await resolveProfileCredentials(options.profile);
|
|
@@ -93241,7 +93650,13 @@ async function resolveAgentCoreCodeImageFromS3(resolved, code, s3Source, options
|
|
|
93241
93650
|
if (assumeRoleArn) try {
|
|
93242
93651
|
credentials = await assumeAgentCoreExecutionRole(assumeRoleArn, region);
|
|
93243
93652
|
} catch (err) {
|
|
93244
|
-
logger.warn(
|
|
93653
|
+
logger.warn(assumeRoleFallbackWarning({
|
|
93654
|
+
assumeRoleArn,
|
|
93655
|
+
purpose: "the fromS3 bundle download",
|
|
93656
|
+
error: err,
|
|
93657
|
+
profile: options.profile,
|
|
93658
|
+
fallbackWithoutProfile: "the default credentials"
|
|
93659
|
+
}));
|
|
93245
93660
|
}
|
|
93246
93661
|
const bundle = await downloadAndExtractS3Bundle(location, {
|
|
93247
93662
|
...region !== void 0 && { region },
|
|
@@ -93439,7 +93854,7 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
|
|
|
93439
93854
|
if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
|
|
93440
93855
|
assumeSucceeded = true;
|
|
93441
93856
|
} catch (err) {
|
|
93442
|
-
logger.warn(`--assume-role: STS AssumeRole(${args.assumeRoleArn}) failed: ${err instanceof Error ? err.message : String(err)}. Falling back to the developer's shell credentials.`);
|
|
93857
|
+
logger.warn(`--assume-role: STS AssumeRole(${displayIdent(args.assumeRoleArn, { maxCodePoints: 612 })}) failed: ${displaySafe(err instanceof Error ? err.message : String(err))}. Falling back to the developer's shell credentials.`);
|
|
93443
93858
|
}
|
|
93444
93859
|
}
|
|
93445
93860
|
if (!assumeSucceeded) {
|
|
@@ -93452,6 +93867,53 @@ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
|
|
|
93452
93867
|
}
|
|
93453
93868
|
}
|
|
93454
93869
|
/**
|
|
93870
|
+
* The `--assume-role: STS AssumeRole(...) failed` warning, for the two sites
|
|
93871
|
+
* that emit the FALLBACK form of it — the `--sigv4` signing path and the fromS3 bundle download.
|
|
93872
|
+
*
|
|
93873
|
+
* ONE builder rather than two near-identical template literals, and that is
|
|
93874
|
+
* go-to-k/cdkd#3390's test review rather than tidiness. The two were twins, and
|
|
93875
|
+
* a twin is where a sanitizing rule goes half-applied: dropping `displayIdent`
|
|
93876
|
+
* from the fromS3 copy alone reddened only the source-shape fence, because the
|
|
93877
|
+
* `--sigv4` copy was the one with a behavioural case. As one PURE function both
|
|
93878
|
+
* sites inherit whatever this is tested to do, and there is one subject to test
|
|
93879
|
+
* rather than a second one somebody has to remember exists.
|
|
93880
|
+
*
|
|
93881
|
+
* TWO untrusted values, and both take `displayIdent` (issue
|
|
93882
|
+
* [#3377](https://github.com/go-to-k/cdkd/issues/3377) for the first, this PR's
|
|
93883
|
+
* security review for the second):
|
|
93884
|
+
*
|
|
93885
|
+
* - `profile` is user-supplied argv, and `ConsoleLogger.formatMessage`
|
|
93886
|
+
* sanitizes only a call's EXTRA ARGS, never the message itself — so the
|
|
93887
|
+
* message text is the last place it can be made safe. A MESSAGE, not a
|
|
93888
|
+
* command to paste, so `displayIdent` and not `isPasteableIdent`;
|
|
93889
|
+
* `resolveProfileCredentials` in `local-start-api.ts` is the site that needs
|
|
93890
|
+
* both spellings and shows the difference.
|
|
93891
|
+
* - `assumeRoleArn` is the WIDER boundary of the two, which is why it is here
|
|
93892
|
+
* at all: nothing validates it before `assumeAgentCoreExecutionRole` (whose
|
|
93893
|
+
* own throw lands in the very `catch` that calls this), and
|
|
93894
|
+
* `resolveAssumeRoleArn` can source it from a DEPLOYED STATE RECORD rather
|
|
93895
|
+
* than from argv. Every character of a legitimate ARN is in `PLAIN_IDENT`, so
|
|
93896
|
+
* it renders bare exactly as before. cdk-local's twin of this line flattens
|
|
93897
|
+
* the same value for the same reason; cdkd had nothing.
|
|
93898
|
+
*
|
|
93899
|
+
* `error` takes `displaySafe` — the DENYLIST, since an SDK error message is
|
|
93900
|
+
* free-form text rather than an identifier, which is the line
|
|
93901
|
+
* `display-safe.ts`'s header draws. The first cut stringified it and sanitized
|
|
93902
|
+
* NOTHING, and go-to-k/cdkd#3390's round-2 security review showed that was the
|
|
93903
|
+
* hole the other two guards leave open: STS answers an unparseable RoleArn with
|
|
93904
|
+
* a `ValidationError` that ECHOES THE SUBMITTED VALUE VERBATIM, so the
|
|
93905
|
+
* attacker-controlled ARN comes back through the error string and lands on the
|
|
93906
|
+
* same line the `displayIdent` two expressions to its left was added to
|
|
93907
|
+
* protect. A C1 byte, `U+0085`, `U+2028` and the bidi overrides all survive an
|
|
93908
|
+
* XML 1.0 response body. `formatError` sanitizes a `cause` for exactly this
|
|
93909
|
+
* reason and cannot help here, because this is a logger MESSAGE and
|
|
93910
|
+
* `ConsoleLogger.formatMessage` sanitizes only a call's EXTRA ARGS.
|
|
93911
|
+
*/
|
|
93912
|
+
function assumeRoleFallbackWarning(args) {
|
|
93913
|
+
const fallback = args.profile ? `--profile ${displayIdent(args.profile)}` : args.fallbackWithoutProfile;
|
|
93914
|
+
return `--assume-role: STS AssumeRole(${displayIdent(args.assumeRoleArn, { maxCodePoints: 612 })}) failed for ${args.purpose}: ${displaySafe(args.error instanceof Error ? args.error.message : String(args.error))}. Falling back to ${fallback}.`;
|
|
93915
|
+
}
|
|
93916
|
+
/**
|
|
93455
93917
|
* Resolve the role ARN to assume, honoring the three `--assume-role` forms.
|
|
93456
93918
|
* Bare `--assume-role` uses the runtime's literal `RoleArn`; when that is an
|
|
93457
93919
|
* intrinsic (the common L2 case — `Fn::GetAtt` to an auto-created role) it
|
|
@@ -93465,7 +93927,7 @@ function resolveAssumeRoleArn(options, resolved, loaded) {
|
|
|
93465
93927
|
if (loaded) {
|
|
93466
93928
|
const fromState = resolveExecutionRoleArnFromState(loaded, resolved.logicalId, "RoleArn");
|
|
93467
93929
|
if (fromState) {
|
|
93468
|
-
getLogger().debug(`--assume-role: resolved RoleArn from state: ${fromState}`);
|
|
93930
|
+
getLogger().debug(`--assume-role: resolved RoleArn from state: ${displayIdent(fromState)}`);
|
|
93469
93931
|
return fromState;
|
|
93470
93932
|
}
|
|
93471
93933
|
}
|
|
@@ -93610,7 +94072,7 @@ async function assumeAgentCoreExecutionRole(roleArn, region) {
|
|
|
93610
94072
|
RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-invoke-agentcore-${Date.now()}`,
|
|
93611
94073
|
DurationSeconds: 3600
|
|
93612
94074
|
}))).Credentials;
|
|
93613
|
-
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new CdkdError(`AssumeRole(${roleArn}) returned no usable credentials.`, "LOCAL_INVOKE_AGENTCORE_ASSUMEROLE_NO_CREDS");
|
|
94075
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new CdkdError(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials.`, "LOCAL_INVOKE_AGENTCORE_ASSUMEROLE_NO_CREDS");
|
|
93614
94076
|
return {
|
|
93615
94077
|
accessKeyId: creds.AccessKeyId,
|
|
93616
94078
|
secretAccessKey: creds.SecretAccessKey,
|
|
@@ -94223,7 +94685,7 @@ async function localInvokeCommand(target, options) {
|
|
|
94223
94685
|
const arn = resolveExecutionRoleArnFromState(stateForRoleHint, lambda.logicalId);
|
|
94224
94686
|
if (arn) {
|
|
94225
94687
|
resolvedAssumeRoleArn = arn;
|
|
94226
|
-
logger.info(`--assume-role: auto-resolved execution role from cdkd state: ${arn}`);
|
|
94688
|
+
logger.info(`--assume-role: auto-resolved execution role from cdkd state: ${displayIdent(arn, { maxCodePoints: 612 })}`);
|
|
94227
94689
|
} else logger.warn(`--assume-role: could not resolve the execution role ARN from cdkd state for '${lambda.logicalId}'. Pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.`);
|
|
94228
94690
|
}
|
|
94229
94691
|
} else maybeSuggestAssumeRole(options, stateForRoleHint, lambda.logicalId);
|
|
@@ -94691,7 +95153,7 @@ async function applyLambdaCredentialEnv(dockerEnv, args) {
|
|
|
94691
95153
|
assumeSucceeded = true;
|
|
94692
95154
|
} catch (err) {
|
|
94693
95155
|
const reason = err instanceof Error ? err.message : String(err);
|
|
94694
|
-
logger.warn(`--assume-role: STS AssumeRole(${args.assumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
|
|
95156
|
+
logger.warn(`--assume-role: STS AssumeRole(${displayIdent(args.assumeRoleArn, { maxCodePoints: 612 })}) failed: ${displaySafe(reason)}. Falling back to the developer's shell credentials.`);
|
|
94695
95157
|
}
|
|
94696
95158
|
}
|
|
94697
95159
|
if (!assumeSucceeded) {
|
|
@@ -94729,7 +95191,7 @@ async function assumeLambdaExecutionRole(roleArn, region) {
|
|
|
94729
95191
|
RoleSessionName: `cdkd-local-invoke-${Date.now()}`,
|
|
94730
95192
|
DurationSeconds: 3600
|
|
94731
95193
|
}))).Credentials;
|
|
94732
|
-
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
|
|
95194
|
+
if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${displayIdent(roleArn, { maxCodePoints: 612 })}) returned no usable credentials.`);
|
|
94733
95195
|
return {
|
|
94734
95196
|
accessKeyId: creds.AccessKeyId,
|
|
94735
95197
|
secretAccessKey: creds.SecretAccessKey,
|
|
@@ -94850,7 +95312,7 @@ function materializeInlineCode(handler, source, fileExtension) {
|
|
|
94850
95312
|
function suggestAssumeRoleFromState(state, logicalId) {
|
|
94851
95313
|
const logger = getLogger();
|
|
94852
95314
|
const roleArn = resolveExecutionRoleArnFromState(state, logicalId);
|
|
94853
|
-
if (roleArn) logger.info(`Hint: the deployed function uses execution role ${roleArn}. Re-run with --assume-role to invoke under the deployed function's narrow permissions.`);
|
|
95315
|
+
if (roleArn) logger.info(`Hint: the deployed function uses execution role ${displayIdent(roleArn, { maxCodePoints: 612 })}. Re-run with --assume-role to invoke under the deployed function's narrow permissions.`);
|
|
94854
95316
|
}
|
|
94855
95317
|
/**
|
|
94856
95318
|
* Emit the "re-run with `--assume-role`" hint, if it applies.
|
|
@@ -95010,4 +95472,4 @@ function buildProgram() {
|
|
|
95010
95472
|
|
|
95011
95473
|
//#endregion
|
|
95012
95474
|
export { buildProgram };
|
|
95013
|
-
//# sourceMappingURL=program-
|
|
95475
|
+
//# sourceMappingURL=program-PCCyP9-O.js.map
|