@go-to-k/cdkd 0.284.10 → 0.284.11
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-ChVCjyK2.js → asg-provider-C9OlqHee.js} +2 -2
- package/dist/{asg-provider-ChVCjyK2.js.map → asg-provider-C9OlqHee.js.map} +1 -1
- package/dist/cli.js +3 -3
- package/dist/{deploy-engine-DGuaeWaV.js → deploy-engine-K5kHzsB-.js} +491 -40
- package/dist/{deploy-engine-DGuaeWaV.js.map → deploy-engine-K5kHzsB-.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -16053,7 +16053,7 @@ var CloudControlProvider = class {
|
|
|
16053
16053
|
if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
|
|
16054
16054
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
16055
16055
|
this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
|
|
16056
|
-
const { ASGProvider } = await import("./asg-provider-
|
|
16056
|
+
const { ASGProvider } = await import("./asg-provider-C9OlqHee.js").then((n) => n.n);
|
|
16057
16057
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
16058
16058
|
}
|
|
16059
16059
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -16741,6 +16741,33 @@ const CR_NO_PROPERTIES_SKIP_REASON = "no properties in state — Delete handler
|
|
|
16741
16741
|
*/
|
|
16742
16742
|
const CR_NO_SERVICE_TOKEN_SKIP_REASON = "no ServiceToken in state — Delete handler not invoked";
|
|
16743
16743
|
/**
|
|
16744
|
+
* Third sibling of the two above, for the arm where cdkd HAD everything it
|
|
16745
|
+
* needed and the Delete request still could not be completed — a permanent
|
|
16746
|
+
* `lambda:InvokeFunction` denial, an exhausted readiness waiter, a response
|
|
16747
|
+
* that never arrived.
|
|
16748
|
+
*
|
|
16749
|
+
* That arm used to swallow the error and return `undefined`, which
|
|
16750
|
+
* `deleteSkipReason` reads as DELETED: `cdkd destroy` printed `✓ … deleted`,
|
|
16751
|
+
* dropped the state record and exited 0 over a handler that never received a
|
|
16752
|
+
* `Delete` — silently orphaning everything that handler manages. It is the
|
|
16753
|
+
* same silent-orphan class issue
|
|
16754
|
+
* [#1752](https://github.com/go-to-k/cdkd/issues/1752) removed from the two
|
|
16755
|
+
* arms above, reached through the catch rather than through a guard.
|
|
16756
|
+
*
|
|
16757
|
+
* **Fixed wording, no interpolation.** The underlying AWS message goes out on
|
|
16758
|
+
* the `logger.warn` beside it and NOT into the reason, because a `reason` is
|
|
16759
|
+
* rendered into the `Error` the deploy-side replacement sites throw, whose
|
|
16760
|
+
* catch classifies an already-deleted resource by SUBSTRING — an AWS message
|
|
16761
|
+
* carrying `does not exist` / `not found` would make a skip read as "already
|
|
16762
|
+
* gone" and drop the record again, one layer further out. Same rule the
|
|
16763
|
+
* `sns-subscription` abort follows.
|
|
16764
|
+
*
|
|
16765
|
+
* The premise is "the resource was NOT destroyed", not "no AWS call was
|
|
16766
|
+
* issued": the handler may have run and failed, or run and had its response
|
|
16767
|
+
* lost. Both leave the resource unproven, which is what a skip asserts.
|
|
16768
|
+
*/
|
|
16769
|
+
const CR_DELETE_INVOKE_FAILED_SKIP_REASON = "Delete request to the handler did not complete — resource unproven";
|
|
16770
|
+
/**
|
|
16744
16771
|
* The deploy-side caveat both skip warnings in this file carry (issue
|
|
16745
16772
|
* [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
|
|
16746
16773
|
*
|
|
@@ -16795,12 +16822,115 @@ function decodeInvokeLogTail(logResult) {
|
|
|
16795
16822
|
}
|
|
16796
16823
|
}
|
|
16797
16824
|
/**
|
|
16825
|
+
* Recover the backing function's own STATUS fields from an
|
|
16826
|
+
* `@smithy/util-waiter` failure message, or `undefined` when they are not
|
|
16827
|
+
* there (issue #2033).
|
|
16828
|
+
*
|
|
16829
|
+
* The message is `JSON.stringify(result)` and `result.reason` is, for both
|
|
16830
|
+
* Lambda readiness waiters, the ENTIRE `GetFunction` response. Only these
|
|
16831
|
+
* AWS-authored status fields are lifted out of it — never the whole payload,
|
|
16832
|
+
* which carries `Configuration.Environment.Variables` into a durable store.
|
|
16833
|
+
*
|
|
16834
|
+
* Best-effort by construction: a non-JSON message, a different waiter shape, or
|
|
16835
|
+
* a payload with no `Configuration` all yield `undefined`, and the caller falls
|
|
16836
|
+
* back to a fixed sentence.
|
|
16837
|
+
*/
|
|
16838
|
+
function extractWaiterFunctionStatus(message) {
|
|
16839
|
+
let parsed;
|
|
16840
|
+
try {
|
|
16841
|
+
parsed = JSON.parse(message);
|
|
16842
|
+
} catch {
|
|
16843
|
+
return;
|
|
16844
|
+
}
|
|
16845
|
+
const config = (parsed?.reason)?.Configuration;
|
|
16846
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) return void 0;
|
|
16847
|
+
const fields = config;
|
|
16848
|
+
const parts = [];
|
|
16849
|
+
for (const key of [
|
|
16850
|
+
"State",
|
|
16851
|
+
"StateReasonCode",
|
|
16852
|
+
"StateReason",
|
|
16853
|
+
"LastUpdateStatus",
|
|
16854
|
+
"LastUpdateStatusReasonCode",
|
|
16855
|
+
"LastUpdateStatusReason"
|
|
16856
|
+
]) {
|
|
16857
|
+
const value = fields[key];
|
|
16858
|
+
if (typeof value === "string" && value !== "") parts.push(`${key}=${value}`);
|
|
16859
|
+
}
|
|
16860
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
16861
|
+
}
|
|
16862
|
+
/**
|
|
16863
|
+
* Render a Lambda readiness-waiter failure for a message that is persisted
|
|
16864
|
+
* (issue #2033) — see `waitForBackingLambdaReady` for the whole argument.
|
|
16865
|
+
*
|
|
16866
|
+
* TIMEOUT / ABORT keep the waiter's own message: its `observedResponses` keys
|
|
16867
|
+
* are status lines `@smithy/util-waiter` builds itself (`403: <AWS message>`),
|
|
16868
|
+
* which is exactly the diagnostic a stalled waiter needs and carries no
|
|
16869
|
+
* response body. Every other state serialized the full `GetFunction` response,
|
|
16870
|
+
* so that arm reports the error NAME plus the function's own status fields.
|
|
16871
|
+
*/
|
|
16872
|
+
function describeWaiterFailure(error) {
|
|
16873
|
+
const name = error instanceof Error ? error.name : "Error";
|
|
16874
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16875
|
+
if (name === "TimeoutError" || name === "AbortError") return message;
|
|
16876
|
+
return `${name} (${extractWaiterFunctionStatus(message) ?? "no function status reported"}). The waiter's raw payload is withheld because it embeds the whole GetFunction response, environment variables included; run \`aws lambda get-function\` for the detail.`;
|
|
16877
|
+
}
|
|
16878
|
+
/**
|
|
16798
16879
|
* IAM-authorization-propagation signals in a custom resource FAILED reason that
|
|
16799
16880
|
* indicate the backing Lambda's freshly-attached execution-role policy has not
|
|
16800
16881
|
* yet taken effect for its assumed-role session (so a recycle + retry will
|
|
16801
16882
|
* succeed once IAM settles). Lowercase substrings. Intentionally narrow — these
|
|
16802
16883
|
* are the IAM-permission-not-yet-effective phrases only, NOT generic transient
|
|
16803
16884
|
* errors (throttling / timeouts), which must not trigger a CR re-invoke.
|
|
16885
|
+
*
|
|
16886
|
+
* **This set is deliberately NARROWER than
|
|
16887
|
+
* `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` (`src/deployment/retryable-errors.ts`)
|
|
16888
|
+
* and stays that way** (issue
|
|
16889
|
+
* [#2033](https://github.com/go-to-k/cdkd/issues/2033), which asked whether the
|
|
16890
|
+
* narrowing was still intended or a list that had stopped tracking its
|
|
16891
|
+
* counterpart).
|
|
16892
|
+
*
|
|
16893
|
+
* "Narrower", not "a subset" — an earlier revision of this comment said SUBSET
|
|
16894
|
+
* and that was FALSE of the list beside it. Three of these six entries appear
|
|
16895
|
+
* in no form in the shared list (`no identity-based policy allows`, both
|
|
16896
|
+
* `not in the state functionActive` spellings), and a fourth appears there only
|
|
16897
|
+
* ANCHORED: the shared list carries `Firehose is unable to assume role` /
|
|
16898
|
+
* `is unable to assume provided role` / `is unable to assume the role` and
|
|
16899
|
+
* deliberately refuses the bare `is unable to assume` this list uses, so that a
|
|
16900
|
+
* permanent `... is unable to assume role X because of an explicit deny` cannot
|
|
16901
|
+
* burn a retry budget. The bare spelling is right HERE — the text is the
|
|
16902
|
+
* handler's own reason about the race cdkd created — and wrong for AWS-authored
|
|
16903
|
+
* text about a call cdkd made, which is why
|
|
16904
|
+
* {@link CR_THROWN_AUTHZ_EXTRA_SIGNALS} does not re-export it.
|
|
16905
|
+
*
|
|
16906
|
+
* The narrowing is intended, because the two lists are consumed under different
|
|
16907
|
+
* COSTS and classify text with different AUTHORS:
|
|
16908
|
+
*
|
|
16909
|
+
* - This set is matched against the HANDLER's own FAILED `Reason` (and, since
|
|
16910
|
+
* #1674, against arbitrary handler stdout in the log tail). A match here buys
|
|
16911
|
+
* a re-INVOKE, which re-runs the user's `Create` — a non-idempotent handler
|
|
16912
|
+
* repeats partial work, and a Provider-framework `onEvent` can create a
|
|
16913
|
+
* SECOND physical resource and orphan the first (the accepted cost stated on
|
|
16914
|
+
* {@link CR_TRANSIENT_AUTHZ_LOG_SIGNALS}). So the phrases must name the race
|
|
16915
|
+
* cdkd ITSELF created — the backing function's freshly-attached execution
|
|
16916
|
+
* role — and nothing else. Most of the superset's entries describe a
|
|
16917
|
+
* DOWNSTREAM call the handler made (`Invalid principal in policy`,
|
|
16918
|
+
* `Cannot access stream`, `KMS key is invalid for CreateGrant`,
|
|
16919
|
+
* `Invalid InstanceProfile`, …); a re-invoke is not the remedy for those, so
|
|
16920
|
+
* each one would buy a recycle plus an identical re-failure. The three the
|
|
16921
|
+
* issue named specifically — `role defined for the function`, `trust policy`,
|
|
16922
|
+
* `Invalid principal in policy` — are exactly that shape when they arrive in
|
|
16923
|
+
* a handler-authored reason, and the first two are ALREADY covered here in
|
|
16924
|
+
* the spelling that matters (`cannot be assumed` / `is unable to assume` are
|
|
16925
|
+
* what Lambda emits for an unassumable execution role).
|
|
16926
|
+
* - The shared list is matched against text AWS wrote about a call CDKD made.
|
|
16927
|
+
* It is used, in full, by
|
|
16928
|
+
* {@link CustomResourceProvider.isTransientAuthzThrow} for a THROWN error
|
|
16929
|
+
* from one of the provider's OWN SDK calls — see that method for why the
|
|
16930
|
+
* wider list is correct there and costs nothing extra.
|
|
16931
|
+
*
|
|
16932
|
+
* So the answer to "should these converge" is no; what was genuinely missing was
|
|
16933
|
+
* the second consumer, not a wider first one.
|
|
16804
16934
|
*/
|
|
16805
16935
|
const CR_TRANSIENT_AUTHZ_SIGNALS = [
|
|
16806
16936
|
"not authorized to perform",
|
|
@@ -16811,6 +16941,27 @@ const CR_TRANSIENT_AUTHZ_SIGNALS = [
|
|
|
16811
16941
|
"is unable to assume"
|
|
16812
16942
|
];
|
|
16813
16943
|
/**
|
|
16944
|
+
* The CR-specific spellings {@link CustomResourceProvider.isTransientAuthzThrow}
|
|
16945
|
+
* adds ON TOP of `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` — i.e. exactly the
|
|
16946
|
+
* phrases the shared list does not carry in any form (issue #2033).
|
|
16947
|
+
*
|
|
16948
|
+
* Deliberately NOT `CR_TRANSIENT_AUTHZ_SIGNALS` itself, which is what the first
|
|
16949
|
+
* cut of the fix used. Three of that list's entries are already covered by the
|
|
16950
|
+
* shared list (`not authorized to perform` / `cannot be assumed`, plus the three
|
|
16951
|
+
* ANCHORED `unable to assume` spellings), so re-uniting the whole thing bought
|
|
16952
|
+
* nothing except the bare, UN-anchored `is unable to assume` — which the shared
|
|
16953
|
+
* list refuses on purpose so a permanent explicit-deny cannot spend a 47.75s
|
|
16954
|
+
* budget before failing. This list is the difference, and only the difference.
|
|
16955
|
+
*
|
|
16956
|
+
* Lower-cased substrings, matched against a lower-cased message (the shared list
|
|
16957
|
+
* is mixed-case and matched verbatim by `isIamPropagationError`).
|
|
16958
|
+
*
|
|
16959
|
+
* `is not in the state functionactive` from the sibling list is omitted as a
|
|
16960
|
+
* pure superstring of the entry below it: any message matching it matches this
|
|
16961
|
+
* one too.
|
|
16962
|
+
*/
|
|
16963
|
+
const CR_THROWN_AUTHZ_EXTRA_SIGNALS = ["no identity-based policy allows", "not in the state functionactive"];
|
|
16964
|
+
/**
|
|
16814
16965
|
* The same IAM-authorization-propagation signals, matched against the backing
|
|
16815
16966
|
* function's INVOCATION LOG TAIL rather than the FAILED reason (issue #1674).
|
|
16816
16967
|
*
|
|
@@ -16871,6 +17022,37 @@ const CR_TRANSIENT_AUTHZ_LOG_SIGNALS = [
|
|
|
16871
17022
|
* at 4 KB regardless, so this only trims the extreme case.
|
|
16872
17023
|
*/
|
|
16873
17024
|
const CR_LOG_TAIL_WARN_MAX_CHARS = 2e3;
|
|
17025
|
+
/** Default for `CDKD_CR_AUTHZ_MAX_RETRIES` — see `transientAuthzMaxRetries`. */
|
|
17026
|
+
const CR_AUTHZ_MAX_RETRIES_DEFAULT = 2;
|
|
17027
|
+
/**
|
|
17028
|
+
* Hard ceiling for `CDKD_CR_AUTHZ_MAX_RETRIES`.
|
|
17029
|
+
*
|
|
17030
|
+
* The knob's units are RE-INVOCATIONS OF THE USER'S HANDLER, each one also
|
|
17031
|
+
* paying a `recycleBackingFunctionExecEnv` (an `UpdateFunctionConfiguration`
|
|
17032
|
+
* plus a 120s waiter). Ten of those is already far past the point where an
|
|
17033
|
+
* IAM-propagation race would have settled, so anything above it is a typo or a
|
|
17034
|
+
* misunderstanding rather than a preference — and left unclamped a `1e9`
|
|
17035
|
+
* passes the finite / `>= 0` gate and re-invokes until the deploy engine's
|
|
17036
|
+
* per-resource deadline fires an hour later.
|
|
17037
|
+
*/
|
|
17038
|
+
const CR_AUTHZ_MAX_RETRIES_CEILING = 10;
|
|
17039
|
+
/**
|
|
17040
|
+
* Bound for the `.cause` walks in this file, matching the depth
|
|
17041
|
+
* `isMarkedNonRetryable` / `isThrottlingError` use in
|
|
17042
|
+
* `src/deployment/retryable-errors.ts`. Bounded rather than unbounded so a
|
|
17043
|
+
* cyclic chain cannot hang the classifier.
|
|
17044
|
+
*/
|
|
17045
|
+
const CR_ERROR_CAUSE_MAX_DEPTH = 5;
|
|
17046
|
+
/**
|
|
17047
|
+
* Sleep seam for this provider's hand-rolled waits (the pre-delivery retry
|
|
17048
|
+
* backoff and the S3 response poll).
|
|
17049
|
+
*
|
|
17050
|
+
* Mutable module state ONLY so tests can run a 47.75s retry schedule without
|
|
17051
|
+
* spending 47.75s; production never reassigns it. Mirrors the
|
|
17052
|
+
* `deleteTableRetryDelays.sleep` seam the DynamoDB providers use, and the
|
|
17053
|
+
* `sleep` option `withRetry` already exposes for the same reason.
|
|
17054
|
+
*/
|
|
17055
|
+
const customResourceRetryDelays = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
|
|
16874
17056
|
/**
|
|
16875
17057
|
* Lines Lambda emits for EVERY invocation regardless of what the handler logged.
|
|
16876
17058
|
* A tail consisting only of these carries no diagnostic value, and it is the
|
|
@@ -17010,6 +17192,24 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17010
17192
|
* exponential backoff for async patterns (CDK Provider framework with
|
|
17011
17193
|
* isCompleteHandler), so an outer retry adds nothing but the multi-
|
|
17012
17194
|
* key bug.
|
|
17195
|
+
*
|
|
17196
|
+
* **Opting out of the outer loop is a promise to retry HERE, and that
|
|
17197
|
+
* promise is kept PER CALL — not per attempt.** Issue
|
|
17198
|
+
* [#2033](https://github.com/go-to-k/cdkd/issues/2033) found the claim
|
|
17199
|
+
* backed for exactly one error SHAPE: the internal loop keyed only on the
|
|
17200
|
+
* handler's RETURNED `cfnResponse.Status === 'FAILED'`, and its body had no
|
|
17201
|
+
* `try` / `catch` at any point, so a THROWN error from any AWS SDK call the
|
|
17202
|
+
* provider itself makes left `create()` directly and was single-shot — while
|
|
17203
|
+
* every other resource type got 26 retries over 47.75s for the identical
|
|
17204
|
+
* wording. The per-call decision, in one place:
|
|
17205
|
+
*
|
|
17206
|
+
* | call | retried on a throw? | why |
|
|
17207
|
+
* |---|---|---|
|
|
17208
|
+
* | S3 `PutObject` (response-key placeholder) | YES, own `withRetry` (the standard dense propagation schedule, 47.75s) — and its exhausted throw is `markNonRetryable`d so the loop below cannot spend a SECOND budget on it | idempotent PUT of an empty object at a key cdkd just minted; touches no response-URL lifecycle, so a replay is free |
|
|
17209
|
+
* | Lambda `Invoke` / SNS `Publish` | YES, but only PRE-DELIVERY, on {@link CustomResourceProvider.preDeliveryAuthzMaxRetries} — its OWN budget, the same dense 47.75s schedule every other resource type gets | a replay re-delivers the request, which is the hazard this flag exists for, so the PRE-delivery fence is what makes the budget affordable: nothing has been delivered, so a replay re-invokes NOTHING — see {@link CustomResourceProvider.isTransientAuthzThrow} |
|
|
17210
|
+
* | `waitUntilFunctionActiveV2` / `waitUntilFunctionUpdatedV2` | ALREADY, by the SDK waiter — and their wrapped failure is `markNonRetryable`d, so the loop below does not replay it either | measured against `@aws-sdk/client-lambda`: the generated `checkState` catches EVERY exception and returns `RETRY`, so a mid-propagation 403 on `lambda:GetFunction` is polled out to `maxWaitTime` (600s). Wrapping them again would only stack a second budget on top — and `@smithy/util-waiter` serializes its `observedResponses` into the TIMEOUT message, whose keys read `403: User: … is not authorized to perform: lambda:GetFunction …`, so a classifier reading that message would have replayed a PERMANENT denial for 3 x 600s |
|
|
17211
|
+
* | `GetFunction` (delete-path backing-Lambda probe) | NO, deliberately | it already fails OPEN — anything but a definitive `ResourceNotFoundException` falls through to the normal invoke path, whose waiters cover the same propagation window one call later. Retrying would only delay that fall-through by up to 47.75s on a genuine permission denial |
|
|
17212
|
+
* | anything AFTER delivery (`pollS3Response`, the `FunctionError` throw, `cleanupResponseObject`) | NO, deliberately | the handler is running and will PUT to the URL of THIS attempt; a replay strands it at a key nobody polls, which is precisely the bug `disableOuterRetry` prevents |
|
|
17013
17213
|
*/
|
|
17014
17214
|
disableOuterRetry = true;
|
|
17015
17215
|
/** Max time to wait for synchronous S3 response after Lambda invocation (30 seconds) */
|
|
@@ -17023,8 +17223,13 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17023
17223
|
/** Max poll interval for async polling with exponential backoff (30 seconds) */
|
|
17024
17224
|
MAX_POLL_INTERVAL_MS = 3e4;
|
|
17025
17225
|
/**
|
|
17026
|
-
* How many extra times to
|
|
17027
|
-
* FAILED with a *transient IAM-authorization* reason
|
|
17226
|
+
* How many extra times to RE-INVOKE a custom resource whose handler returned
|
|
17227
|
+
* FAILED with a *transient IAM-authorization* reason.
|
|
17228
|
+
*
|
|
17229
|
+
* TWO error shapes, TWO budgets, deliberately (issue #2033) — this one and
|
|
17230
|
+
* {@link CustomResourceProvider.preDeliveryAuthzMaxRetries}. This budget
|
|
17231
|
+
* governs the shape where the handler ALREADY RAN: it returned FAILED with a
|
|
17232
|
+
* transient-authz reason (e.g. the CDK Provider
|
|
17028
17233
|
* framework's `lambda:GetFunction` / "not in the state functionActive" 403
|
|
17029
17234
|
* when the framework role's freshly-attached inline policy has not yet
|
|
17030
17235
|
* propagated to the assumed-role session). cdkd's fast SDK path invokes the
|
|
@@ -17036,16 +17241,67 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17036
17241
|
* `disableOuterRetry` to avoid stranding a pre-signed response URL — so we
|
|
17037
17242
|
* retry HERE instead, deriving a fresh response URL + RequestId per attempt
|
|
17038
17243
|
* and recycling the backing function's execution environment between tries).
|
|
17039
|
-
*
|
|
17244
|
+
*
|
|
17245
|
+
* **It is SMALL (2) because every retry it authorises RE-RUNS THE USER'S
|
|
17246
|
+
* HANDLER**, and that is the whole reason the second shape does not share it:
|
|
17247
|
+
* a pre-delivery throw invokes the handler ZERO times, so the argument that
|
|
17248
|
+
* bounds this number does not apply there at all. Sharing it was measured
|
|
17249
|
+
* wrong — 2 retries on the dense schedule is 250ms + 500ms of coverage, i.e.
|
|
17250
|
+
* 0.75s against an IAM-propagation window this repo has measured at 7-12s, so
|
|
17251
|
+
* issue #2033's own scenario still failed with the fix in place.
|
|
17252
|
+
*
|
|
17253
|
+
* Override via `CDKD_CR_AUTHZ_MAX_RETRIES` (clamped to
|
|
17254
|
+
* {@link CR_AUTHZ_MAX_RETRIES_CEILING}). `0` disables the RE-INVOKE only —
|
|
17040
17255
|
* the issue-#1674 log-tail scan and the reason annotation it produces still
|
|
17041
17256
|
* run, because they describe the failure rather than react to it.
|
|
17257
|
+
*
|
|
17258
|
+
* It does NOT disable either of the two retries that cannot reach the
|
|
17259
|
+
* handler: the response-placeholder `PutObject` retry in
|
|
17260
|
+
* `generateResponseURL`, and the pre-delivery arm above. Neither is what this
|
|
17261
|
+
* knob exists to bound — it bounds how many times a user's handler may be
|
|
17262
|
+
* re-run — and a user turning off re-invokes should not thereby lose the
|
|
17263
|
+
* propagation coverage every other resource type gets for free.
|
|
17042
17264
|
*/
|
|
17043
17265
|
transientAuthzMaxRetries = (() => {
|
|
17044
17266
|
const raw = process.env["CDKD_CR_AUTHZ_MAX_RETRIES"];
|
|
17045
|
-
if (raw === void 0 || raw === "") return
|
|
17267
|
+
if (raw === void 0 || raw === "") return CR_AUTHZ_MAX_RETRIES_DEFAULT;
|
|
17046
17268
|
const n = Number(raw);
|
|
17047
|
-
|
|
17269
|
+
if (!Number.isFinite(n) || n < 0) return CR_AUTHZ_MAX_RETRIES_DEFAULT;
|
|
17270
|
+
const clamped = Math.min(Math.floor(n), CR_AUTHZ_MAX_RETRIES_CEILING);
|
|
17271
|
+
if (clamped !== n) this.logger.warn(`CDKD_CR_AUTHZ_MAX_RETRIES=${raw} is out of range; using ${clamped} (whole numbers, at most ${CR_AUTHZ_MAX_RETRIES_CEILING} — each retry re-invokes the custom resource handler).`);
|
|
17272
|
+
return clamped;
|
|
17048
17273
|
})();
|
|
17274
|
+
/**
|
|
17275
|
+
* Budget for the PRE-DELIVERY thrown arm — a transient IAM-authorization
|
|
17276
|
+
* error thrown by the `Invoke` / `Publish` itself BEFORE the request reached
|
|
17277
|
+
* the handler (issue #2033). The reported shape is an `AccessDeniedException`
|
|
17278
|
+
* on `lambda:InvokeFunction` while the DEPLOYING principal's own
|
|
17279
|
+
* freshly-attached policy is still propagating.
|
|
17280
|
+
*
|
|
17281
|
+
* It is {@link IAM_PROPAGATION_MAX_RETRIES} — the same 26 retries over 47.75s
|
|
17282
|
+
* that `withRetry` gives every other resource type for the identical wording,
|
|
17283
|
+
* on the same dense schedule ({@link IAM_PROPAGATION_INITIAL_DELAY_MS}
|
|
17284
|
+
* doubling to {@link IAM_PROPAGATION_MAX_DELAY_MS}). The measured window this
|
|
17285
|
+
* has to cover is 7-12s; the FAILED-response budget's 0.75s does not.
|
|
17286
|
+
*
|
|
17287
|
+
* **Why it can afford that while its sibling cannot**: it fires only when
|
|
17288
|
+
* `delivered === false`, so the handler has been invoked ZERO times and a
|
|
17289
|
+
* replay re-runs NOTHING — no partial work repeated, no second physical
|
|
17290
|
+
* resource from a Provider-framework `onEvent`, no stranded response URL. The
|
|
17291
|
+
* only cost of a retry here is one `PutObject` + one presign, and the
|
|
17292
|
+
* abandoned placeholder is swept before the next attempt. So the constraint
|
|
17293
|
+
* that keeps `transientAuthzMaxRetries` at 2 is simply absent, and matching
|
|
17294
|
+
* every other resource type is the correct answer instead.
|
|
17295
|
+
*
|
|
17296
|
+
* A thrown retry also skips the exec-env recycle: the denial is on CDKD's own
|
|
17297
|
+
* principal, not on the backing function's role, so there is no warm
|
|
17298
|
+
* container holding stale credentials to invalidate.
|
|
17299
|
+
*
|
|
17300
|
+
* Deliberately NOT overridable by an env var. It bounds no handler
|
|
17301
|
+
* invocation, so there is nothing for a user to trade off — the same reason
|
|
17302
|
+
* the placeholder `PutObject`'s `withRetry` takes no knob either.
|
|
17303
|
+
*/
|
|
17304
|
+
preDeliveryAuthzMaxRetries = 26;
|
|
17049
17305
|
constructor(config) {
|
|
17050
17306
|
const awsClients = getAwsClients();
|
|
17051
17307
|
this.lambdaClient = awsClients.lambda;
|
|
@@ -17252,7 +17508,11 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17252
17508
|
if (cfnResponse.Status === "FAILED") this.logger.warn(`Custom resource delete handler returned FAILED for ${logicalId}: ${cfnResponse.Reason || "Unknown reason"}`);
|
|
17253
17509
|
else this.logger.debug(`Successfully deleted custom resource ${logicalId}`);
|
|
17254
17510
|
} catch (error) {
|
|
17255
|
-
this.logger.warn(`Failed to delete custom resource ${logicalId}, but continuing: ${error instanceof Error ? error.message : String(error)}`);
|
|
17511
|
+
this.logger.warn(`Failed to delete custom resource ${logicalId}, but continuing: ${error instanceof Error ? error.message : String(error)}. The Delete handler did not complete, so anything this custom resource manages may still be LIVE — cdkd is KEEPING the state record so a re-run can retry it. ${DEPLOY_SKIP_CAVEAT}`);
|
|
17512
|
+
return {
|
|
17513
|
+
outcome: "skipped",
|
|
17514
|
+
reason: CR_DELETE_INVOKE_FAILED_SKIP_REASON
|
|
17515
|
+
};
|
|
17256
17516
|
}
|
|
17257
17517
|
}
|
|
17258
17518
|
/**
|
|
@@ -17309,31 +17569,71 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17309
17569
|
* `prepareInvocation()`) and recycling the backing function's execution
|
|
17310
17570
|
* environment between tries so its next cold start re-assumes the role.
|
|
17311
17571
|
*
|
|
17572
|
+
* **The loop covers TWO error shapes** (issue #2033). The original one is the
|
|
17573
|
+
* handler's RETURNED `Status: 'FAILED'`. The second is an error THROWN by one
|
|
17574
|
+
* of the provider's OWN SDK calls before the request reached the handler —
|
|
17575
|
+
* which used to leave `create()` directly, because the loop body had no
|
|
17576
|
+
* `try` / `catch` at any point, making every such call single-shot while every
|
|
17577
|
+
* other resource type got the outer `withRetry`'s 26 attempts for the identical
|
|
17578
|
+
* wording. The `delivered` flag below is what keeps the second arm honest: it
|
|
17579
|
+
* flips the moment `Invoke` / `Publish` RETURNS, and a throw after that point
|
|
17580
|
+
* is rethrown untouched no matter how transient it reads, because the handler
|
|
17581
|
+
* is running and will PUT to THIS attempt's response URL. See
|
|
17582
|
+
* {@link CustomResourceProvider.disableOuterRetry} for the whole per-call
|
|
17583
|
+
* table, and {@link CustomResourceProvider.isTransientAuthzThrow} for why a
|
|
17584
|
+
* PRE-delivery throw is safe to replay at all.
|
|
17585
|
+
*
|
|
17312
17586
|
* `buildRequest` is called once per attempt with the fresh invocation so the
|
|
17313
17587
|
* CFn request body always carries the matching ResponseURL / RequestId.
|
|
17314
17588
|
* Returns the final response; the caller decides what a terminal FAILED means
|
|
17315
17589
|
* (create/update throw, delete warns-and-continues).
|
|
17316
17590
|
*/
|
|
17317
17591
|
async invokeCustomResourceWithRetry(serviceToken, logicalId, operation, buildRequest) {
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17592
|
+
const watch = this.startInterruptWatch(logicalId);
|
|
17593
|
+
try {
|
|
17594
|
+
let preDeliveryRetries = 0;
|
|
17595
|
+
let failedResponseRetries = 0;
|
|
17596
|
+
for (let attempt = 0;; attempt++) {
|
|
17597
|
+
let delivered = false;
|
|
17598
|
+
let cfnResponse;
|
|
17599
|
+
let logResult;
|
|
17600
|
+
let invocation;
|
|
17601
|
+
try {
|
|
17602
|
+
invocation = await this.prepareInvocation(logicalId, watch);
|
|
17603
|
+
const request = buildRequest(invocation);
|
|
17604
|
+
this.logger.debug(`Sending custom resource ${operation.toLowerCase()} request: ${serviceToken}`);
|
|
17605
|
+
const sent = await this.sendRequest(serviceToken, request, invocation.responseKey, logicalId, operation, () => {
|
|
17606
|
+
delivered = true;
|
|
17607
|
+
});
|
|
17608
|
+
cfnResponse = sent.response;
|
|
17609
|
+
logResult = sent.logResult;
|
|
17610
|
+
} catch (error) {
|
|
17611
|
+
if (delivered || preDeliveryRetries >= this.preDeliveryAuthzMaxRetries || !this.isTransientAuthzThrow(error)) throw error;
|
|
17612
|
+
const delayMs = Math.min(250 * Math.pow(2, preDeliveryRetries), IAM_PROPAGATION_MAX_DELAY_MS);
|
|
17613
|
+
this.logger.warn(`Custom resource ${operation} for ${logicalId} hit a transient IAM-authorization error before the request was delivered (attempt ${attempt + 1}/${this.preDeliveryAuthzMaxRetries + 1}): ${this.truncateReason(error instanceof Error ? error.message : String(error))}. Retrying in ${delayMs / 1e3}s with a fresh response URL and RequestId.`);
|
|
17614
|
+
if (invocation !== void 0) await this.cleanupResponseObject(invocation.responseKey);
|
|
17615
|
+
preDeliveryRetries += 1;
|
|
17616
|
+
await this.sleepInterruptibly(delayMs, watch);
|
|
17617
|
+
continue;
|
|
17618
|
+
}
|
|
17619
|
+
const reasonIsAuthz = cfnResponse.Status === "FAILED" && this.isTransientAuthzFailure(cfnResponse.Reason);
|
|
17620
|
+
const logTail = cfnResponse.Status === "FAILED" && !reasonIsAuthz ? decodeInvokeLogTail(logResult) : void 0;
|
|
17621
|
+
const logAuthzMatch = logTail === void 0 ? void 0 : this.findTransientAuthzLogLine(logTail);
|
|
17622
|
+
if (cfnResponse.Status === "FAILED" && failedResponseRetries < this.transientAuthzMaxRetries && (reasonIsAuthz || logAuthzMatch !== void 0)) {
|
|
17623
|
+
failedResponseRetries += 1;
|
|
17624
|
+
this.logger.warn(`Custom resource ${operation} for ${logicalId} returned a transient IAM-authorization FAILED (attempt ${attempt + 1}/${this.transientAuthzMaxRetries + 1}): ${this.truncateReason(cfnResponse.Reason)}. ` + (logAuthzMatch === void 0 ? "" : `The handler's reason carried no authorization wording; the denial was found in the backing function's log: ${this.truncateReason(logAuthzMatch.line)}. `) + `Recycling the backing function's execution environment and retrying so its next cold start picks up the propagated policy.`);
|
|
17625
|
+
await this.recycleBackingFunctionExecEnv(serviceToken, logicalId);
|
|
17626
|
+
continue;
|
|
17627
|
+
}
|
|
17628
|
+
if (cfnResponse.Status === "FAILED" && logAuthzMatch !== void 0) return {
|
|
17629
|
+
...cfnResponse,
|
|
17630
|
+
Reason: `${cfnResponse.Reason ?? "Unknown reason"} [cdkd: the reason carried no authorization wording, but the backing function's invocation log matched the IAM-authorization signal "${logAuthzMatch.signal}" — see the cdkd warning for the log line, or the function's CloudWatch log group]`
|
|
17631
|
+
};
|
|
17632
|
+
if (logTail !== void 0 && hasHandlerLogOutput(logTail)) this.logger.warn(`Custom resource ${operation} for ${logicalId} failed and cdkd could not classify the reason. Log tail from the DISPATCH invocation (for the CDK Provider framework's async pattern the failure may have occurred in a later execution, whose log this is not):\n` + this.truncateReason(logTail, CR_LOG_TAIL_WARN_MAX_CHARS));
|
|
17633
|
+
return cfnResponse;
|
|
17330
17634
|
}
|
|
17331
|
-
|
|
17332
|
-
|
|
17333
|
-
Reason: `${cfnResponse.Reason ?? "Unknown reason"} [cdkd: the reason carried no authorization wording, but the backing function's invocation log matched the IAM-authorization signal "${logAuthzMatch.signal}" — see the cdkd warning for the log line, or the function's CloudWatch log group]`
|
|
17334
|
-
};
|
|
17335
|
-
if (logTail !== void 0 && hasHandlerLogOutput(logTail)) this.logger.warn(`Custom resource ${operation} for ${logicalId} failed and cdkd could not classify the reason. Log tail from the DISPATCH invocation (for the CDK Provider framework's async pattern the failure may have occurred in a later execution, whose log this is not):\n` + this.truncateReason(logTail, CR_LOG_TAIL_WARN_MAX_CHARS));
|
|
17336
|
-
return cfnResponse;
|
|
17635
|
+
} finally {
|
|
17636
|
+
watch.dispose();
|
|
17337
17637
|
}
|
|
17338
17638
|
}
|
|
17339
17639
|
/**
|
|
@@ -17355,6 +17655,63 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17355
17655
|
return CR_TRANSIENT_AUTHZ_SIGNALS.some((p) => lower.includes(p));
|
|
17356
17656
|
}
|
|
17357
17657
|
/**
|
|
17658
|
+
* Classify an error THROWN by one of the provider's own SDK calls as a
|
|
17659
|
+
* transient IAM-authorization race worth replaying (issue #2033).
|
|
17660
|
+
*
|
|
17661
|
+
* The list is cdkd's shared `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` — the
|
|
17662
|
+
* same one every other resource type is classified with, via `withRetry` —
|
|
17663
|
+
* plus {@link CR_THROWN_AUTHZ_EXTRA_SIGNALS}, the two CR-specific spellings
|
|
17664
|
+
* the shared list does not carry in any form. That is deliberately WIDER than
|
|
17665
|
+
* the FAILED-reason classifier, and the asymmetry is the whole point: this
|
|
17666
|
+
* text was written by AWS about a call CDKD made, whereas a FAILED reason was
|
|
17667
|
+
* written by the user's handler about a call IT made. See
|
|
17668
|
+
* {@link CR_TRANSIENT_AUTHZ_SIGNALS} for the full argument, including why the
|
|
17669
|
+
* bare `is unable to assume` spelling stays on that side of the line.
|
|
17670
|
+
*
|
|
17671
|
+
* **Two REFUSALS come before any pattern**, and each closes a way for a
|
|
17672
|
+
* single call to be given two retry budgets:
|
|
17673
|
+
*
|
|
17674
|
+
* - `isMarkedNonRetryable` — cdkd's own declaration that this raising cannot
|
|
17675
|
+
* succeed on a replay. Both of the provider's already-retried calls stamp
|
|
17676
|
+
* it: the placeholder `PutObject` after its `withRetry` is exhausted, and
|
|
17677
|
+
* `waitForBackingLambdaReady` after the SDK waiter has polled for its full
|
|
17678
|
+
* 600s. The marker rather than the wording is what makes the second one
|
|
17679
|
+
* sound: `@smithy/util-waiter` serializes `observedResponses` into its
|
|
17680
|
+
* TIMEOUT message and those keys read
|
|
17681
|
+
* `403: User: … is not authorized to perform: lambda:GetFunction …`, so a
|
|
17682
|
+
* PERMANENT denial matched every pattern here and a message-shaped fence
|
|
17683
|
+
* would be one AWS wording change from failing open. It also refuses a
|
|
17684
|
+
* cdkd-authored REFUSAL whose text happens to carry an authz phrase.
|
|
17685
|
+
* - the `.cause` chain is WALKED (bounded), matching `isThrottlingError` /
|
|
17686
|
+
* `isMarkedNonRetryable`. cdkd wraps SDK errors routinely, and issue #2040
|
|
17687
|
+
* documents the drop-`cause` class in this very directory; a top-level-only
|
|
17688
|
+
* read would silently un-retry a wrapped propagation denial.
|
|
17689
|
+
*
|
|
17690
|
+
* **Every pattern in that union is an AUTHORIZATION or REQUEST-VALIDATION
|
|
17691
|
+
* rejection, and that is what makes replaying an `Invoke` safe here.** Such a
|
|
17692
|
+
* rejection is decided at the API front door, before any execution environment
|
|
17693
|
+
* is engaged, so the handler provably did not run and a replay cannot
|
|
17694
|
+
* re-deliver work — the hazard `disableOuterRetry` exists for. It is also why
|
|
17695
|
+
* this classifier deliberately does NOT reach for the broader
|
|
17696
|
+
* `isRetryableTransientError`: a throttle, an HTTP 5xx or a socket timeout can
|
|
17697
|
+
* each arrive AFTER the request was accepted, so replaying one could invoke a
|
|
17698
|
+
* non-idempotent handler twice. Those classes stay single-shot on this path by
|
|
17699
|
+
* design, and the caller's `delivered` flag is the second, independent fence.
|
|
17700
|
+
*/
|
|
17701
|
+
isTransientAuthzThrow(error) {
|
|
17702
|
+
if (isMarkedNonRetryable(error)) return false;
|
|
17703
|
+
let current = error;
|
|
17704
|
+
for (let depth = 0; depth < CR_ERROR_CAUSE_MAX_DEPTH && current != null; depth++) {
|
|
17705
|
+
const message = current instanceof Error ? current.message : typeof current === "string" ? current : "";
|
|
17706
|
+
if (message !== "") {
|
|
17707
|
+
const lower = message.toLowerCase();
|
|
17708
|
+
if (isIamPropagationError(message) || CR_THROWN_AUTHZ_EXTRA_SIGNALS.some((p) => lower.includes(p))) return true;
|
|
17709
|
+
}
|
|
17710
|
+
current = current.cause;
|
|
17711
|
+
}
|
|
17712
|
+
return false;
|
|
17713
|
+
}
|
|
17714
|
+
/**
|
|
17358
17715
|
* Find the IAM-authorization denial inside a backing function's invocation
|
|
17359
17716
|
* log tail, for the case the FAILED reason itself carries none (issue #1674).
|
|
17360
17717
|
*
|
|
@@ -17440,15 +17797,25 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17440
17797
|
* erased from the FAILED reason (issue #1674). Returned UNDECODED so the
|
|
17441
17798
|
* happy path pays nothing — only a FAILED whose reason missed decodes it.
|
|
17442
17799
|
* Absent on the SNS path: there is no Lambda invoke to attach a log to.
|
|
17800
|
+
*
|
|
17801
|
+
* `onDelivered` is invoked exactly once, the moment the `Invoke` / `Publish`
|
|
17802
|
+
* RETURNS — i.e. at the point after which a replay would re-deliver the
|
|
17803
|
+
* request to the handler and strand this attempt's pre-signed response URL.
|
|
17804
|
+
* The caller's retry-on-throw arm is fenced on it (issue #2033). Deliberately
|
|
17805
|
+
* called AFTER the send rather than before: a rejection at the API front door
|
|
17806
|
+
* (the IAM-propagation class this arm exists for) means the handler never ran,
|
|
17807
|
+
* and treating that as delivered would leave the reported failure single-shot.
|
|
17443
17808
|
*/
|
|
17444
|
-
async sendRequest(serviceToken, request, responseKey, logicalId, operation) {
|
|
17809
|
+
async sendRequest(serviceToken, request, responseKey, logicalId, operation, onDelivered) {
|
|
17445
17810
|
if (this.isSnsServiceToken(serviceToken)) {
|
|
17446
17811
|
this.logger.debug(`ServiceToken is SNS topic, publishing to: ${serviceToken}`);
|
|
17447
17812
|
await this.publishToSns(serviceToken, request);
|
|
17813
|
+
onDelivered();
|
|
17448
17814
|
return { response: await this.pollS3Response(responseKey, logicalId, operation) };
|
|
17449
17815
|
}
|
|
17450
17816
|
await this.waitForBackingLambdaReady(serviceToken, logicalId);
|
|
17451
17817
|
const invokeResponse = await this.invokeLambda(serviceToken, request);
|
|
17818
|
+
onDelivered();
|
|
17452
17819
|
return {
|
|
17453
17820
|
response: await this.getCustomResourceResponse(invokeResponse, responseKey, logicalId, operation),
|
|
17454
17821
|
...invokeResponse.LogResult === void 0 ? {} : { logResult: invokeResponse.LogResult }
|
|
@@ -17481,6 +17848,37 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17481
17848
|
* deploy engine's per-resource `--resource-timeout` (default 30 min)
|
|
17482
17849
|
* still bounds the outer Custom Resource provisioning attempt, so
|
|
17483
17850
|
* this waiter cap is layered defense, not the only timeout.
|
|
17851
|
+
*
|
|
17852
|
+
* **The wrapped failure is `markNonRetryable`d, and its message is NOT the
|
|
17853
|
+
* waiter's** (issue #2033). Both halves are about `@smithy/util-waiter`'s
|
|
17854
|
+
* `checkExceptions`, which serializes its whole result into the `Error`
|
|
17855
|
+
* message:
|
|
17856
|
+
*
|
|
17857
|
+
* - On TIMEOUT that includes `observedResponses`, whose keys read
|
|
17858
|
+
* `403: User: … is not authorized to perform: lambda:GetFunction on
|
|
17859
|
+
* resource: …`. The waiter's generated `checkState` catches EVERY
|
|
17860
|
+
* exception and returns `RETRY`, so reaching here means a 600s budget was
|
|
17861
|
+
* already spent — yet the message matched
|
|
17862
|
+
* {@link CustomResourceProvider.isTransientAuthzThrow} exactly, so a
|
|
17863
|
+
* PERMANENT `lambda:GetFunction` denial was replayed for 3 x 600s instead
|
|
17864
|
+
* of 10 minutes, blowing any `--resource-timeout
|
|
17865
|
+
* AWS::CloudFormation::CustomResource=15m`. The marker is a property of
|
|
17866
|
+
* the error object, so unlike a wording test it cannot be defeated by AWS
|
|
17867
|
+
* rephrasing the denial.
|
|
17868
|
+
* - On the non-TIMEOUT arm (`State: Failed`, i.e. an ENI / VPC failure) the
|
|
17869
|
+
* serialized result carries `reason` and `final`, which for this waiter
|
|
17870
|
+
* are the ENTIRE `GetFunction` response — `Configuration.Environment.
|
|
17871
|
+
* Variables` included. That message reached `ProvisioningError.message`
|
|
17872
|
+
* and `extractDeploymentEventError` persisted it to
|
|
17873
|
+
* `deployments/{runId}.jsonl`, a durable store that outlives
|
|
17874
|
+
* `cdkd destroy` and is contractually "error + metadata only, never
|
|
17875
|
+
* resource properties, because they may contain secrets"
|
|
17876
|
+
* (`docs/deployment-events.md`). So this arm interpolates the error NAME
|
|
17877
|
+
* plus a fixed sentence, and recovers only the function's own
|
|
17878
|
+
* `State` / `StateReason` / `StateReasonCode` — AWS-authored status
|
|
17879
|
+
* fields — from the serialized payload. The TIMEOUT / ABORT arms keep
|
|
17880
|
+
* their message: `observedResponses` keys are status lines built by
|
|
17881
|
+
* `createMessageFromResponse`, never a response body.
|
|
17484
17882
|
*/
|
|
17485
17883
|
async waitForBackingLambdaReady(serviceToken, logicalId) {
|
|
17486
17884
|
try {
|
|
@@ -17497,7 +17895,7 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17497
17895
|
maxDelay: 5
|
|
17498
17896
|
}, { FunctionName: serviceToken });
|
|
17499
17897
|
} catch (error) {
|
|
17500
|
-
throw new Error(`Lambda backing custom resource ${logicalId} (${serviceToken}) did not reach a ready state for Invoke: ${error
|
|
17898
|
+
throw markNonRetryable(new Error(`Lambda backing custom resource ${logicalId} (${serviceToken}) did not reach a ready state for Invoke: ${describeWaiterFailure(error)}`, { cause: error }));
|
|
17501
17899
|
}
|
|
17502
17900
|
}
|
|
17503
17901
|
/**
|
|
@@ -17593,28 +17991,38 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17593
17991
|
* Centralising this in one helper makes that invariant impossible to
|
|
17594
17992
|
* violate at the call sites.
|
|
17595
17993
|
*/
|
|
17596
|
-
async prepareInvocation() {
|
|
17994
|
+
async prepareInvocation(logicalId, watch) {
|
|
17597
17995
|
const requestId = `cdkd-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
17598
17996
|
const responseKey = this.getResponseKey(requestId);
|
|
17599
17997
|
return {
|
|
17600
17998
|
requestId,
|
|
17601
17999
|
responseKey,
|
|
17602
|
-
responseURL: await this.generateResponseURL(responseKey)
|
|
18000
|
+
responseURL: await this.generateResponseURL(responseKey, logicalId, watch)
|
|
17603
18001
|
};
|
|
17604
18002
|
}
|
|
17605
18003
|
/**
|
|
17606
18004
|
* Generate a pre-signed S3 PUT URL for Lambda to send its response
|
|
17607
18005
|
*/
|
|
17608
|
-
async generateResponseURL(responseKey) {
|
|
18006
|
+
async generateResponseURL(responseKey, logicalId, watch) {
|
|
17609
18007
|
if (!this.responseBucket) return "https://localhost/cfn-response-not-configured";
|
|
17610
18008
|
await this.ensureResponseClient();
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
18009
|
+
const bucket = this.responseBucket;
|
|
18010
|
+
try {
|
|
18011
|
+
await withRetry(() => this.s3Client.send(new PutObjectCommand({
|
|
18012
|
+
Bucket: bucket,
|
|
18013
|
+
Key: responseKey,
|
|
18014
|
+
Body: "",
|
|
18015
|
+
ContentLength: 0,
|
|
18016
|
+
ContentType: "application/json"
|
|
18017
|
+
})), `${logicalId} (custom-resource response placeholder)`, {
|
|
18018
|
+
logger: this.logger,
|
|
18019
|
+
isInterrupted: watch.isInterrupted,
|
|
18020
|
+
onInterrupted: watch.onInterrupted,
|
|
18021
|
+
sleep: customResourceRetryDelays.sleep
|
|
18022
|
+
});
|
|
18023
|
+
} catch (error) {
|
|
18024
|
+
throw markNonRetryable(error instanceof Error ? error : new Error(String(error)));
|
|
18025
|
+
}
|
|
17618
18026
|
const command = new PutObjectCommand({
|
|
17619
18027
|
Bucket: this.responseBucket,
|
|
17620
18028
|
Key: responseKey
|
|
@@ -17725,7 +18133,50 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17725
18133
|
return result;
|
|
17726
18134
|
}
|
|
17727
18135
|
sleep(ms) {
|
|
17728
|
-
return
|
|
18136
|
+
return customResourceRetryDelays.sleep(ms);
|
|
18137
|
+
}
|
|
18138
|
+
/**
|
|
18139
|
+
* Install a SIGINT watch for one custom-resource invocation (issue #2033).
|
|
18140
|
+
*
|
|
18141
|
+
* `docs/provider-development.md` requires a new `withRetry` to thread
|
|
18142
|
+
* `isInterrupted` / `onInterrupted`, and a hand-rolled backoff to be
|
|
18143
|
+
* interruptible for the same reason: without it Ctrl-C is dead for the whole
|
|
18144
|
+
* 47.75s schedule. The provider already had the shape — `pollS3Response`
|
|
18145
|
+
* installs its own handler — so this is that pattern, lifted so the two new
|
|
18146
|
+
* wait sites share one flag and one disposal.
|
|
18147
|
+
*
|
|
18148
|
+
* The caller MUST `dispose()` in a `finally`; a leaked listener would
|
|
18149
|
+
* accumulate one per resource across a deploy.
|
|
18150
|
+
*/
|
|
18151
|
+
startInterruptWatch(logicalId) {
|
|
18152
|
+
let interrupted = false;
|
|
18153
|
+
const handler = () => {
|
|
18154
|
+
interrupted = true;
|
|
18155
|
+
};
|
|
18156
|
+
process.on("SIGINT", handler);
|
|
18157
|
+
return {
|
|
18158
|
+
isInterrupted: () => interrupted,
|
|
18159
|
+
onInterrupted: () => /* @__PURE__ */ new Error(`Custom resource ${logicalId} interrupted by user`),
|
|
18160
|
+
dispose: () => {
|
|
18161
|
+
process.removeListener("SIGINT", handler);
|
|
18162
|
+
}
|
|
18163
|
+
};
|
|
18164
|
+
}
|
|
18165
|
+
/**
|
|
18166
|
+
* `sleep` that checks the interrupt watch at most a second apart, mirroring
|
|
18167
|
+
* `withRetry`'s own once-per-second probe. Throws the watch's error when the
|
|
18168
|
+
* user has hit Ctrl-C, so the retry loop unwinds instead of sitting out the
|
|
18169
|
+
* remaining backoff.
|
|
18170
|
+
*/
|
|
18171
|
+
async sleepInterruptibly(ms, watch) {
|
|
18172
|
+
let remaining = ms;
|
|
18173
|
+
while (remaining > 0) {
|
|
18174
|
+
if (watch.isInterrupted()) throw watch.onInterrupted();
|
|
18175
|
+
const chunk = Math.min(1e3, remaining);
|
|
18176
|
+
await this.sleep(chunk);
|
|
18177
|
+
remaining -= chunk;
|
|
18178
|
+
}
|
|
18179
|
+
if (watch.isInterrupted()) throw watch.onInterrupted();
|
|
17729
18180
|
}
|
|
17730
18181
|
/**
|
|
17731
18182
|
* Adopt an existing custom resource into cdkd state.
|
|
@@ -23631,7 +24082,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
23631
24082
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
23632
24083
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
23633
24084
|
function getCdkdVersion() {
|
|
23634
|
-
return "0.284.
|
|
24085
|
+
return "0.284.11";
|
|
23635
24086
|
}
|
|
23636
24087
|
/**
|
|
23637
24088
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -26100,4 +26551,4 @@ var DeployEngine = class {
|
|
|
26100
26551
|
|
|
26101
26552
|
//#endregion
|
|
26102
26553
|
export { IntrinsicFunctionResolver as $, StackHasActiveImportsError as $n, validateContainerRepoName as $t, formatResourceLine as A, AssemblyReader as An, describeTypeWithThrottleRetry as At, isExportAliasCollision as B, ConfigError as Bn, WorkGraph as Bt, refusesFinalSnapshot as C, MIGRATE_TMP_PREFIX as Cn, s3BucketDomainName as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, PARTITION_TABLE as Dn, applyRoleArnIfSet as Dt, extractDeploymentEventError as E, expectedOwnerParam as En, s3BucketWebsiteUrl as Et, red as F, getAwsClients as Fn, S3StateBackend as Ft, clearOnUpdateRemoval as G, LocalStartServiceError as Gn, escapeRegExp$1 as Gt, stateKeySecretExposure as H, DeployCancelledError as Hn, createAssetRedirectResolver as Ht, yellow as I, resetAwsClients as In, rebuildClientForBucketRegion as It, findSilentDropProperties as J, NestedStackChildDirectDestroyError as Jn, BOOTSTRAP_MARKER_PREFIX as Jt, ProviderRegistry as K, LockError as Kn, stripControlChars as Kt, collectDeclaredOutputNames as L, setAwsClients as Ln, shouldRetainResource as Lt, cyan as M, clearBucketRegionCache as Mn, DagBuilder as Mt, gray as N, resolveBucketRegion as Nn, TemplateParser as Nt, isStatefulRecreateTargetSync as O, canonicalizeRegion as On, DiffCalculator as Ot, green as P, AwsClients as Pn, LockManager as Pt, isTerminationProtectionPropagationError as Q, ResourceUpdateNotSupportedError as Qn, validateAssetBucketName as Qt, collectPublishedOutputNames as R, AssetError as Rn, AssetPublisher as Rt, isFinalSnapshotError as S, CFN_TEMPLATE_URL_LIMIT as Sn, s3BucketArn as St, makeCanonicalizePropertiesFn as T, uploadCfnTemplate as Tn, s3BucketRegionalDomainName as Tt, IAMRoleProvider as U, LocalInvokeBuildError as Un, loadPublishableAssetManifest as Ut, secretBearingStateKeyWarning as V, DependencyError as Vn, buildAssetRedirectMap as Vt, collectInlinePolicyNamesManagedBySiblings as W, LocalMigrateError as Wn, rewriteTemplateAssetReferences as Wt, slowCcOperationTimeoutMs as X, ProvisioningError as Xn, getBootstrapMarkerKey as Xt, CloudControlProvider as Y, PartialFailureError as Yn, ensureAssetStorage as Yt, disableInstanceApiTermination as Z, ResourceTimeoutError as Zn, parseBootstrapMarker as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, resolveStateBucketWithDefaultAndSource as _n, TEMPLATE_SOURCED_RULES as _t, DeploymentEventsStore as a, runDockerStreaming as an, normalizeAwsError as ar, resolveExplicitPhysicalId as at, ccRoutedFinalSnapshotError as b, warnDeprecatedNoPrefixCliFlag as bn, redactSecretsForState as bt, replayFailedOperations as c, Synthesizer as cn, isRetryableTransientError as cr, configBooleanRefusal as ct, updatePartialReason as d, getLegacyStateBucketName as dn, __exportAll as dr, replayWarn as dt, buildDenyExternalAccessPolicy as en, StackTerminationProtectionError as er, cfnRefValueFromPhysicalId as et, UNSPECIFIED_SKIP_REASON as f, resolveApp as fn, requireConfigArray as ft, computeImplicitDeleteEdges as g, resolveStateBucketWithDefault as gn, STATE_SOURCED_READBACK_RULES as gt, IMPLICIT_DELETE_DEPENDENCIES as h, resolveSkipPrefix as hn, STATE_SOURCED_CROSS_GENERATION_RULES as ht, DeploymentEventsReader as i, runDockerForeground as in, isCdkdError as ir, normalizeAwsTagsToCfn as it, bold as j, processStackMessages as jn, withRetry as jt, renderStatefulReason as k, derivePartitionAndUrlSuffix as kn, INTRINSIC_KEYS as kt, replayRollback as l, synthesisStatusMessage as ln, isThrottlingError as lr, configStringRefusal as lt, withResourceDeadline as m, resolveCaptureObservedState as mn, requireConfigString as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatDockerLoginError as nn, SynthesisError as nr, refStateLookupFromResource as nt, planFailedOps as o, AssetManifestLoader as on, withErrorHandling as or, assertRegionMatch as ot, deleteSkipReason as p, resolveAutoAssetStorage as pn, requireConfigObject as pt, findActionableSilentDrops as q, MissingCdkCliError as qn, AssetModeResolver as qt, DeployEngine as r, getDockerCmd as rn, formatError as rr, WAFv2WebACLProvider as rt, planRollback as s, getDockerImageBySourceHash as sn, isMarkedNonRetryable as sr, coerceCfnBoolean as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDockerImage as tn, StateError as tr, getAccountInfo as tt, updatePartialMessage as u, getDefaultStateBucketName as un, markNonRetryable as ur, readConfigString as ut, PRE_DELETE_SNAPSHOT_TYPES as v, resolveUseCdkBootstrapAssets as vn, createSecretMasker as vt, unsupportedFinalSnapshotError as w, findLargeInlineResources as wn, s3BucketDualStackDomainName as wt, createPreDeleteFinalSnapshot as x, CFN_TEMPLATE_BODY_LIMIT as xn, scrubResourceRecord as xt, buildFinalSnapshotIdentifier as y, stateBucketExistenceConfirmed as yn, maskSecretsInText as yt, exportAliasCollisionScrubWarning as z, CdkdError as zn, stringifyValue as zt };
|
|
26103
|
-
//# sourceMappingURL=deploy-engine-
|
|
26554
|
+
//# sourceMappingURL=deploy-engine-K5kHzsB-.js.map
|