@go-to-k/cdkd 0.284.10 → 0.284.12
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-CkIFzX7p.js} +2 -2
- package/dist/{asg-provider-ChVCjyK2.js.map → asg-provider-CkIFzX7p.js.map} +1 -1
- package/dist/cli.js +4 -7
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-DGuaeWaV.js → deploy-engine-nwoIJsLn.js} +816 -72
- package/dist/deploy-engine-nwoIJsLn.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-DGuaeWaV.js.map +0 -1
|
@@ -11095,6 +11095,159 @@ function maskSecretsInText(text, secrets) {
|
|
|
11095
11095
|
return text.replace(regex, "***");
|
|
11096
11096
|
}
|
|
11097
11097
|
/**
|
|
11098
|
+
* How deep {@link maskSecretsInError} follows a `cause` chain. A CYCLE is
|
|
11099
|
+
* already handled by the visited-set, so this bounds only a pathologically long
|
|
11100
|
+
* chain; the same bounded-walk shape `extractDeploymentEventError` (depth 10)
|
|
11101
|
+
* and the retry classifiers (depth 5) use. A link BEYOND the cap keeps its
|
|
11102
|
+
* original, UNMASKED message, and the last cloned link points at it.
|
|
11103
|
+
*/
|
|
11104
|
+
const ERROR_CAUSE_MASK_MAX_DEPTH = 20;
|
|
11105
|
+
/**
|
|
11106
|
+
* The `cause` chain of `root`, root first, stopping at the first non-`Error`
|
|
11107
|
+
* link, at a link already visited (so a cycle terminates instead of hanging),
|
|
11108
|
+
* or at {@link ERROR_CAUSE_MASK_MAX_DEPTH}.
|
|
11109
|
+
*/
|
|
11110
|
+
function errorCauseChain(root) {
|
|
11111
|
+
const chain = [];
|
|
11112
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11113
|
+
let current = root;
|
|
11114
|
+
while (current instanceof Error && !seen.has(current) && chain.length < ERROR_CAUSE_MASK_MAX_DEPTH) {
|
|
11115
|
+
seen.add(current);
|
|
11116
|
+
chain.push(current);
|
|
11117
|
+
current = current.cause;
|
|
11118
|
+
}
|
|
11119
|
+
return chain;
|
|
11120
|
+
}
|
|
11121
|
+
/**
|
|
11122
|
+
* Return `error` with {@link maskSecretsInText} applied to the `message` AND the
|
|
11123
|
+
* `stack` of every link in its `cause` chain, with everything else about each
|
|
11124
|
+
* link preserved (issue [#2038](https://github.com/go-to-k/cdkd/issues/2038)
|
|
11125
|
+
* review).
|
|
11126
|
+
*
|
|
11127
|
+
* Two bounds on that "every link", both stated here because the PUBLIC contract
|
|
11128
|
+
* is what a caller reads and neither is visible from the call site:
|
|
11129
|
+
* - The walk stops at {@link ERROR_CAUSE_MASK_MAX_DEPTH}. A chain longer than
|
|
11130
|
+
* that keeps its remaining links' ORIGINAL, UNMASKED messages, and the last
|
|
11131
|
+
* cloned link points straight at them.
|
|
11132
|
+
* - A `cause` that is not an `Error` (a string, a plain object) is carried
|
|
11133
|
+
* through VERBATIM and is never masked — the walk has nothing to clone. No
|
|
11134
|
+
* cdkd or AWS SDK site constructs one today, so this is a documented residual
|
|
11135
|
+
* rather than a reachable leak, but a caller attaching arbitrary data as a
|
|
11136
|
+
* `cause` must mask it itself. `src/cli/index.ts`'s `console.error` renders
|
|
11137
|
+
* such a cause via `util.inspect`, so it WOULD reach the terminal.
|
|
11138
|
+
*
|
|
11139
|
+
* **Why an error and not just its text.** `formatError` (`src/utils/error-handler.ts`)
|
|
11140
|
+
* renders a `CdkdError`'s CAUSE as `Caused by: <cause.message>`, and `handleError`
|
|
11141
|
+
* logs that at `error` level for any failure that escapes a command — so a raw
|
|
11142
|
+
* provider error attached as a `ProvisioningError`'s cause reaches the terminal
|
|
11143
|
+
* verbatim, at DEFAULT verbosity, even when every log site that INTERPOLATED
|
|
11144
|
+
* the message masked it. Masking the string at each log site cannot close that:
|
|
11145
|
+
* the sink reads the error OBJECT.
|
|
11146
|
+
*
|
|
11147
|
+
* `formatError` is not the only such sink, and the second one is what makes the
|
|
11148
|
+
* CHAIN argument below concrete rather than hypothetical: `src/cli/index.ts`'s
|
|
11149
|
+
* top-level `main().catch(...)` does `console.error('Fatal error:', error)`,
|
|
11150
|
+
* which renders the whole object through `util.inspect` — every `[cause]` link
|
|
11151
|
+
* AND every link's `stack`. Measured: an outer `Error('top')` wrapping
|
|
11152
|
+
* `Error("Value 'hunter2' failed")` prints as
|
|
11153
|
+
* `Error: top ... { [cause]: Error: Value 'hunter2' failed ... }`. So a
|
|
11154
|
+
* multi-level sink exists TODAY, and it is why `stack` is masked below rather
|
|
11155
|
+
* than merely preserved.
|
|
11156
|
+
*
|
|
11157
|
+
* **Why the whole CHAIN and not just the top link.** Masking only `error.message`
|
|
11158
|
+
* looks sufficient because `formatError` renders one level — and it is not, in
|
|
11159
|
+
* two ways that a top-level-only fix gets exactly backwards. A provider that
|
|
11160
|
+
* wraps an AWS failure in a generic sentence (`new Error('the call failed',
|
|
11161
|
+
* { cause: awsError })`) leaves the plaintext ONE link down, where the
|
|
11162
|
+
* identity-return below then reports "nothing to mask" and hands back an object
|
|
11163
|
+
* still carrying it — the function's own contract says the returned error is
|
|
11164
|
+
* safe to render, and every later reader believes it. And `formatError`
|
|
11165
|
+
* rendering a single level is an implementation detail: one edit there (walking
|
|
11166
|
+
* the chain is the obvious improvement) re-opens the hole with nothing failing.
|
|
11167
|
+
* So the invariant is about the OBJECT, not about today's renderer.
|
|
11168
|
+
*
|
|
11169
|
+
* **Why a clone rather than assigning to `error.message`.** The argument is an
|
|
11170
|
+
* error cdkd did not create — usually the AWS SDK's — and mutating a caller's
|
|
11171
|
+
* object is visible to every other holder of it, including a retry loop that
|
|
11172
|
+
* may still classify it. Each link's clone copies the prototype and EVERY own
|
|
11173
|
+
* property descriptor, symbols included, so the three things that read a cause
|
|
11174
|
+
* chain keep working: `isMarkedNonRetryable` (a non-enumerable `Symbol.for`
|
|
11175
|
+
* marker), `extractDeploymentEventError` / `isThrottlingError` /
|
|
11176
|
+
* `isTransientServerError` (`$metadata`, `Code`, `name`), and the chain itself.
|
|
11177
|
+
* `Object.assign` would have dropped the marker, which is why the descriptors
|
|
11178
|
+
* form is used.
|
|
11179
|
+
*
|
|
11180
|
+
* `message`, `cause` and `stack` are the three descriptors deliberately NOT
|
|
11181
|
+
* copied through: each is re-defined per link, and copying a NON-CONFIGURABLE
|
|
11182
|
+
* original would make that re-definition throw. `cause` is rewired to the CLONE
|
|
11183
|
+
* of whatever it pointed at, in a second pass over the already-built clone map —
|
|
11184
|
+
* which is what makes a cyclic chain terminate rather than recurse. A `cause`
|
|
11185
|
+
* that is not an `Error` (a string, a plain object) keeps its original
|
|
11186
|
+
* descriptor verbatim.
|
|
11187
|
+
*
|
|
11188
|
+
* **Why `stack` is re-defined as DATA rather than copied.** V8 installs `stack`
|
|
11189
|
+
* as an own ACCESSOR whose getter reads a slot the engine attaches to an error
|
|
11190
|
+
* IT created, so copying that descriptor onto an `Object.create` clone yields a
|
|
11191
|
+
* getter with nothing behind it and `clone.stack` reads `undefined` (measured).
|
|
11192
|
+
* That is not a leak today — the clone is only ever reached as a `cause`, and
|
|
11193
|
+
* `handleError` prints the TOP-level error's stack — but this function is
|
|
11194
|
+
* exported and generic, so a future top-level caller would get back an error
|
|
11195
|
+
* with no trace at all. The clone therefore carries a masked COPY of the
|
|
11196
|
+
* original's stack text, which both preserves the trace and closes the sink the
|
|
11197
|
+
* copy would otherwise open: a stack's first line embeds the message, so an
|
|
11198
|
+
* unmasked stack re-exposes exactly the plaintext the `message` mask removed —
|
|
11199
|
+
* and `util.inspect` prints it. An original with no readable string `stack`
|
|
11200
|
+
* (not an engine-created error) simply gets no own `stack`, as before.
|
|
11201
|
+
*
|
|
11202
|
+
* Returns the ORIGINAL object by identity when NOTHING ANYWHERE IN THE CHAIN
|
|
11203
|
+
* changed, so a non-secret failure keeps referential equality and the common
|
|
11204
|
+
* path allocates nothing.
|
|
11205
|
+
*/
|
|
11206
|
+
function maskSecretsInError(error, secrets) {
|
|
11207
|
+
if (secrets.size === 0 || !(error instanceof Error)) return error;
|
|
11208
|
+
const chain = errorCauseChain(error);
|
|
11209
|
+
const maskedMessages = chain.map((link) => maskSecretsInText(link.message, secrets));
|
|
11210
|
+
if (maskedMessages.every((masked, i) => masked === chain[i].message)) return error;
|
|
11211
|
+
const clones = /* @__PURE__ */ new Map();
|
|
11212
|
+
for (const [i, original] of chain.entries()) {
|
|
11213
|
+
const descriptors = {};
|
|
11214
|
+
for (const key of Reflect.ownKeys(original)) {
|
|
11215
|
+
if (key === "message" || key === "cause" || key === "stack") continue;
|
|
11216
|
+
const descriptor = Object.getOwnPropertyDescriptor(original, key);
|
|
11217
|
+
if (descriptor) descriptors[key] = descriptor;
|
|
11218
|
+
}
|
|
11219
|
+
const clone = Object.create(Object.getPrototypeOf(original), descriptors);
|
|
11220
|
+
Object.defineProperty(clone, "message", {
|
|
11221
|
+
value: maskedMessages[i],
|
|
11222
|
+
writable: true,
|
|
11223
|
+
enumerable: false,
|
|
11224
|
+
configurable: true
|
|
11225
|
+
});
|
|
11226
|
+
const originalStack = original.stack;
|
|
11227
|
+
if (typeof originalStack === "string") Object.defineProperty(clone, "stack", {
|
|
11228
|
+
value: maskSecretsInText(originalStack, secrets),
|
|
11229
|
+
writable: true,
|
|
11230
|
+
enumerable: false,
|
|
11231
|
+
configurable: true
|
|
11232
|
+
});
|
|
11233
|
+
clones.set(original, clone);
|
|
11234
|
+
}
|
|
11235
|
+
for (const original of chain) {
|
|
11236
|
+
const causeDescriptor = Object.getOwnPropertyDescriptor(original, "cause");
|
|
11237
|
+
if (!causeDescriptor) continue;
|
|
11238
|
+
const clone = clones.get(original);
|
|
11239
|
+
const causeValue = original.cause;
|
|
11240
|
+
const replacement = causeValue instanceof Error ? clones.get(causeValue) : void 0;
|
|
11241
|
+
Object.defineProperty(clone, "cause", replacement ? {
|
|
11242
|
+
value: replacement,
|
|
11243
|
+
writable: true,
|
|
11244
|
+
enumerable: causeDescriptor.enumerable === true,
|
|
11245
|
+
configurable: true
|
|
11246
|
+
} : causeDescriptor);
|
|
11247
|
+
}
|
|
11248
|
+
return clones.get(error);
|
|
11249
|
+
}
|
|
11250
|
+
/**
|
|
11098
11251
|
* Bind a {@link RecordedSecretValues} bag into a {@link SecretMasker} for a
|
|
11099
11252
|
* caller to hand to a provider.
|
|
11100
11253
|
*
|
|
@@ -16053,7 +16206,7 @@ var CloudControlProvider = class {
|
|
|
16053
16206
|
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
16207
|
if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
|
|
16055
16208
|
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-
|
|
16209
|
+
const { ASGProvider } = await import("./asg-provider-CkIFzX7p.js").then((n) => n.n);
|
|
16057
16210
|
return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
|
|
16058
16211
|
}
|
|
16059
16212
|
const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
|
|
@@ -16741,6 +16894,33 @@ const CR_NO_PROPERTIES_SKIP_REASON = "no properties in state — Delete handler
|
|
|
16741
16894
|
*/
|
|
16742
16895
|
const CR_NO_SERVICE_TOKEN_SKIP_REASON = "no ServiceToken in state — Delete handler not invoked";
|
|
16743
16896
|
/**
|
|
16897
|
+
* Third sibling of the two above, for the arm where cdkd HAD everything it
|
|
16898
|
+
* needed and the Delete request still could not be completed — a permanent
|
|
16899
|
+
* `lambda:InvokeFunction` denial, an exhausted readiness waiter, a response
|
|
16900
|
+
* that never arrived.
|
|
16901
|
+
*
|
|
16902
|
+
* That arm used to swallow the error and return `undefined`, which
|
|
16903
|
+
* `deleteSkipReason` reads as DELETED: `cdkd destroy` printed `✓ … deleted`,
|
|
16904
|
+
* dropped the state record and exited 0 over a handler that never received a
|
|
16905
|
+
* `Delete` — silently orphaning everything that handler manages. It is the
|
|
16906
|
+
* same silent-orphan class issue
|
|
16907
|
+
* [#1752](https://github.com/go-to-k/cdkd/issues/1752) removed from the two
|
|
16908
|
+
* arms above, reached through the catch rather than through a guard.
|
|
16909
|
+
*
|
|
16910
|
+
* **Fixed wording, no interpolation.** The underlying AWS message goes out on
|
|
16911
|
+
* the `logger.warn` beside it and NOT into the reason, because a `reason` is
|
|
16912
|
+
* rendered into the `Error` the deploy-side replacement sites throw, whose
|
|
16913
|
+
* catch classifies an already-deleted resource by SUBSTRING — an AWS message
|
|
16914
|
+
* carrying `does not exist` / `not found` would make a skip read as "already
|
|
16915
|
+
* gone" and drop the record again, one layer further out. Same rule the
|
|
16916
|
+
* `sns-subscription` abort follows.
|
|
16917
|
+
*
|
|
16918
|
+
* The premise is "the resource was NOT destroyed", not "no AWS call was
|
|
16919
|
+
* issued": the handler may have run and failed, or run and had its response
|
|
16920
|
+
* lost. Both leave the resource unproven, which is what a skip asserts.
|
|
16921
|
+
*/
|
|
16922
|
+
const CR_DELETE_INVOKE_FAILED_SKIP_REASON = "Delete request to the handler did not complete — resource unproven";
|
|
16923
|
+
/**
|
|
16744
16924
|
* The deploy-side caveat both skip warnings in this file carry (issue
|
|
16745
16925
|
* [#1762](https://github.com/go-to-k/cdkd/issues/1762)).
|
|
16746
16926
|
*
|
|
@@ -16795,12 +16975,115 @@ function decodeInvokeLogTail(logResult) {
|
|
|
16795
16975
|
}
|
|
16796
16976
|
}
|
|
16797
16977
|
/**
|
|
16978
|
+
* Recover the backing function's own STATUS fields from an
|
|
16979
|
+
* `@smithy/util-waiter` failure message, or `undefined` when they are not
|
|
16980
|
+
* there (issue #2033).
|
|
16981
|
+
*
|
|
16982
|
+
* The message is `JSON.stringify(result)` and `result.reason` is, for both
|
|
16983
|
+
* Lambda readiness waiters, the ENTIRE `GetFunction` response. Only these
|
|
16984
|
+
* AWS-authored status fields are lifted out of it — never the whole payload,
|
|
16985
|
+
* which carries `Configuration.Environment.Variables` into a durable store.
|
|
16986
|
+
*
|
|
16987
|
+
* Best-effort by construction: a non-JSON message, a different waiter shape, or
|
|
16988
|
+
* a payload with no `Configuration` all yield `undefined`, and the caller falls
|
|
16989
|
+
* back to a fixed sentence.
|
|
16990
|
+
*/
|
|
16991
|
+
function extractWaiterFunctionStatus(message) {
|
|
16992
|
+
let parsed;
|
|
16993
|
+
try {
|
|
16994
|
+
parsed = JSON.parse(message);
|
|
16995
|
+
} catch {
|
|
16996
|
+
return;
|
|
16997
|
+
}
|
|
16998
|
+
const config = (parsed?.reason)?.Configuration;
|
|
16999
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) return void 0;
|
|
17000
|
+
const fields = config;
|
|
17001
|
+
const parts = [];
|
|
17002
|
+
for (const key of [
|
|
17003
|
+
"State",
|
|
17004
|
+
"StateReasonCode",
|
|
17005
|
+
"StateReason",
|
|
17006
|
+
"LastUpdateStatus",
|
|
17007
|
+
"LastUpdateStatusReasonCode",
|
|
17008
|
+
"LastUpdateStatusReason"
|
|
17009
|
+
]) {
|
|
17010
|
+
const value = fields[key];
|
|
17011
|
+
if (typeof value === "string" && value !== "") parts.push(`${key}=${value}`);
|
|
17012
|
+
}
|
|
17013
|
+
return parts.length > 0 ? parts.join(", ") : void 0;
|
|
17014
|
+
}
|
|
17015
|
+
/**
|
|
17016
|
+
* Render a Lambda readiness-waiter failure for a message that is persisted
|
|
17017
|
+
* (issue #2033) — see `waitForBackingLambdaReady` for the whole argument.
|
|
17018
|
+
*
|
|
17019
|
+
* TIMEOUT / ABORT keep the waiter's own message: its `observedResponses` keys
|
|
17020
|
+
* are status lines `@smithy/util-waiter` builds itself (`403: <AWS message>`),
|
|
17021
|
+
* which is exactly the diagnostic a stalled waiter needs and carries no
|
|
17022
|
+
* response body. Every other state serialized the full `GetFunction` response,
|
|
17023
|
+
* so that arm reports the error NAME plus the function's own status fields.
|
|
17024
|
+
*/
|
|
17025
|
+
function describeWaiterFailure(error) {
|
|
17026
|
+
const name = error instanceof Error ? error.name : "Error";
|
|
17027
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17028
|
+
if (name === "TimeoutError" || name === "AbortError") return message;
|
|
17029
|
+
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.`;
|
|
17030
|
+
}
|
|
17031
|
+
/**
|
|
16798
17032
|
* IAM-authorization-propagation signals in a custom resource FAILED reason that
|
|
16799
17033
|
* indicate the backing Lambda's freshly-attached execution-role policy has not
|
|
16800
17034
|
* yet taken effect for its assumed-role session (so a recycle + retry will
|
|
16801
17035
|
* succeed once IAM settles). Lowercase substrings. Intentionally narrow — these
|
|
16802
17036
|
* are the IAM-permission-not-yet-effective phrases only, NOT generic transient
|
|
16803
17037
|
* errors (throttling / timeouts), which must not trigger a CR re-invoke.
|
|
17038
|
+
*
|
|
17039
|
+
* **This set is deliberately NARROWER than
|
|
17040
|
+
* `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` (`src/deployment/retryable-errors.ts`)
|
|
17041
|
+
* and stays that way** (issue
|
|
17042
|
+
* [#2033](https://github.com/go-to-k/cdkd/issues/2033), which asked whether the
|
|
17043
|
+
* narrowing was still intended or a list that had stopped tracking its
|
|
17044
|
+
* counterpart).
|
|
17045
|
+
*
|
|
17046
|
+
* "Narrower", not "a subset" — an earlier revision of this comment said SUBSET
|
|
17047
|
+
* and that was FALSE of the list beside it. Three of these six entries appear
|
|
17048
|
+
* in no form in the shared list (`no identity-based policy allows`, both
|
|
17049
|
+
* `not in the state functionActive` spellings), and a fourth appears there only
|
|
17050
|
+
* ANCHORED: the shared list carries `Firehose is unable to assume role` /
|
|
17051
|
+
* `is unable to assume provided role` / `is unable to assume the role` and
|
|
17052
|
+
* deliberately refuses the bare `is unable to assume` this list uses, so that a
|
|
17053
|
+
* permanent `... is unable to assume role X because of an explicit deny` cannot
|
|
17054
|
+
* burn a retry budget. The bare spelling is right HERE — the text is the
|
|
17055
|
+
* handler's own reason about the race cdkd created — and wrong for AWS-authored
|
|
17056
|
+
* text about a call cdkd made, which is why
|
|
17057
|
+
* {@link CR_THROWN_AUTHZ_EXTRA_SIGNALS} does not re-export it.
|
|
17058
|
+
*
|
|
17059
|
+
* The narrowing is intended, because the two lists are consumed under different
|
|
17060
|
+
* COSTS and classify text with different AUTHORS:
|
|
17061
|
+
*
|
|
17062
|
+
* - This set is matched against the HANDLER's own FAILED `Reason` (and, since
|
|
17063
|
+
* #1674, against arbitrary handler stdout in the log tail). A match here buys
|
|
17064
|
+
* a re-INVOKE, which re-runs the user's `Create` — a non-idempotent handler
|
|
17065
|
+
* repeats partial work, and a Provider-framework `onEvent` can create a
|
|
17066
|
+
* SECOND physical resource and orphan the first (the accepted cost stated on
|
|
17067
|
+
* {@link CR_TRANSIENT_AUTHZ_LOG_SIGNALS}). So the phrases must name the race
|
|
17068
|
+
* cdkd ITSELF created — the backing function's freshly-attached execution
|
|
17069
|
+
* role — and nothing else. Most of the superset's entries describe a
|
|
17070
|
+
* DOWNSTREAM call the handler made (`Invalid principal in policy`,
|
|
17071
|
+
* `Cannot access stream`, `KMS key is invalid for CreateGrant`,
|
|
17072
|
+
* `Invalid InstanceProfile`, …); a re-invoke is not the remedy for those, so
|
|
17073
|
+
* each one would buy a recycle plus an identical re-failure. The three the
|
|
17074
|
+
* issue named specifically — `role defined for the function`, `trust policy`,
|
|
17075
|
+
* `Invalid principal in policy` — are exactly that shape when they arrive in
|
|
17076
|
+
* a handler-authored reason, and the first two are ALREADY covered here in
|
|
17077
|
+
* the spelling that matters (`cannot be assumed` / `is unable to assume` are
|
|
17078
|
+
* what Lambda emits for an unassumable execution role).
|
|
17079
|
+
* - The shared list is matched against text AWS wrote about a call CDKD made.
|
|
17080
|
+
* It is used, in full, by
|
|
17081
|
+
* {@link CustomResourceProvider.isTransientAuthzThrow} for a THROWN error
|
|
17082
|
+
* from one of the provider's OWN SDK calls — see that method for why the
|
|
17083
|
+
* wider list is correct there and costs nothing extra.
|
|
17084
|
+
*
|
|
17085
|
+
* So the answer to "should these converge" is no; what was genuinely missing was
|
|
17086
|
+
* the second consumer, not a wider first one.
|
|
16804
17087
|
*/
|
|
16805
17088
|
const CR_TRANSIENT_AUTHZ_SIGNALS = [
|
|
16806
17089
|
"not authorized to perform",
|
|
@@ -16811,6 +17094,27 @@ const CR_TRANSIENT_AUTHZ_SIGNALS = [
|
|
|
16811
17094
|
"is unable to assume"
|
|
16812
17095
|
];
|
|
16813
17096
|
/**
|
|
17097
|
+
* The CR-specific spellings {@link CustomResourceProvider.isTransientAuthzThrow}
|
|
17098
|
+
* adds ON TOP of `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` — i.e. exactly the
|
|
17099
|
+
* phrases the shared list does not carry in any form (issue #2033).
|
|
17100
|
+
*
|
|
17101
|
+
* Deliberately NOT `CR_TRANSIENT_AUTHZ_SIGNALS` itself, which is what the first
|
|
17102
|
+
* cut of the fix used. Three of that list's entries are already covered by the
|
|
17103
|
+
* shared list (`not authorized to perform` / `cannot be assumed`, plus the three
|
|
17104
|
+
* ANCHORED `unable to assume` spellings), so re-uniting the whole thing bought
|
|
17105
|
+
* nothing except the bare, UN-anchored `is unable to assume` — which the shared
|
|
17106
|
+
* list refuses on purpose so a permanent explicit-deny cannot spend a 47.75s
|
|
17107
|
+
* budget before failing. This list is the difference, and only the difference.
|
|
17108
|
+
*
|
|
17109
|
+
* Lower-cased substrings, matched against a lower-cased message (the shared list
|
|
17110
|
+
* is mixed-case and matched verbatim by `isIamPropagationError`).
|
|
17111
|
+
*
|
|
17112
|
+
* `is not in the state functionactive` from the sibling list is omitted as a
|
|
17113
|
+
* pure superstring of the entry below it: any message matching it matches this
|
|
17114
|
+
* one too.
|
|
17115
|
+
*/
|
|
17116
|
+
const CR_THROWN_AUTHZ_EXTRA_SIGNALS = ["no identity-based policy allows", "not in the state functionactive"];
|
|
17117
|
+
/**
|
|
16814
17118
|
* The same IAM-authorization-propagation signals, matched against the backing
|
|
16815
17119
|
* function's INVOCATION LOG TAIL rather than the FAILED reason (issue #1674).
|
|
16816
17120
|
*
|
|
@@ -16871,6 +17175,37 @@ const CR_TRANSIENT_AUTHZ_LOG_SIGNALS = [
|
|
|
16871
17175
|
* at 4 KB regardless, so this only trims the extreme case.
|
|
16872
17176
|
*/
|
|
16873
17177
|
const CR_LOG_TAIL_WARN_MAX_CHARS = 2e3;
|
|
17178
|
+
/** Default for `CDKD_CR_AUTHZ_MAX_RETRIES` — see `transientAuthzMaxRetries`. */
|
|
17179
|
+
const CR_AUTHZ_MAX_RETRIES_DEFAULT = 2;
|
|
17180
|
+
/**
|
|
17181
|
+
* Hard ceiling for `CDKD_CR_AUTHZ_MAX_RETRIES`.
|
|
17182
|
+
*
|
|
17183
|
+
* The knob's units are RE-INVOCATIONS OF THE USER'S HANDLER, each one also
|
|
17184
|
+
* paying a `recycleBackingFunctionExecEnv` (an `UpdateFunctionConfiguration`
|
|
17185
|
+
* plus a 120s waiter). Ten of those is already far past the point where an
|
|
17186
|
+
* IAM-propagation race would have settled, so anything above it is a typo or a
|
|
17187
|
+
* misunderstanding rather than a preference — and left unclamped a `1e9`
|
|
17188
|
+
* passes the finite / `>= 0` gate and re-invokes until the deploy engine's
|
|
17189
|
+
* per-resource deadline fires an hour later.
|
|
17190
|
+
*/
|
|
17191
|
+
const CR_AUTHZ_MAX_RETRIES_CEILING = 10;
|
|
17192
|
+
/**
|
|
17193
|
+
* Bound for the `.cause` walks in this file, matching the depth
|
|
17194
|
+
* `isMarkedNonRetryable` / `isThrottlingError` use in
|
|
17195
|
+
* `src/deployment/retryable-errors.ts`. Bounded rather than unbounded so a
|
|
17196
|
+
* cyclic chain cannot hang the classifier.
|
|
17197
|
+
*/
|
|
17198
|
+
const CR_ERROR_CAUSE_MAX_DEPTH = 5;
|
|
17199
|
+
/**
|
|
17200
|
+
* Sleep seam for this provider's hand-rolled waits (the pre-delivery retry
|
|
17201
|
+
* backoff and the S3 response poll).
|
|
17202
|
+
*
|
|
17203
|
+
* Mutable module state ONLY so tests can run a 47.75s retry schedule without
|
|
17204
|
+
* spending 47.75s; production never reassigns it. Mirrors the
|
|
17205
|
+
* `deleteTableRetryDelays.sleep` seam the DynamoDB providers use, and the
|
|
17206
|
+
* `sleep` option `withRetry` already exposes for the same reason.
|
|
17207
|
+
*/
|
|
17208
|
+
const customResourceRetryDelays = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
|
|
16874
17209
|
/**
|
|
16875
17210
|
* Lines Lambda emits for EVERY invocation regardless of what the handler logged.
|
|
16876
17211
|
* A tail consisting only of these carries no diagnostic value, and it is the
|
|
@@ -17010,6 +17345,24 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17010
17345
|
* exponential backoff for async patterns (CDK Provider framework with
|
|
17011
17346
|
* isCompleteHandler), so an outer retry adds nothing but the multi-
|
|
17012
17347
|
* key bug.
|
|
17348
|
+
*
|
|
17349
|
+
* **Opting out of the outer loop is a promise to retry HERE, and that
|
|
17350
|
+
* promise is kept PER CALL — not per attempt.** Issue
|
|
17351
|
+
* [#2033](https://github.com/go-to-k/cdkd/issues/2033) found the claim
|
|
17352
|
+
* backed for exactly one error SHAPE: the internal loop keyed only on the
|
|
17353
|
+
* handler's RETURNED `cfnResponse.Status === 'FAILED'`, and its body had no
|
|
17354
|
+
* `try` / `catch` at any point, so a THROWN error from any AWS SDK call the
|
|
17355
|
+
* provider itself makes left `create()` directly and was single-shot — while
|
|
17356
|
+
* every other resource type got 26 retries over 47.75s for the identical
|
|
17357
|
+
* wording. The per-call decision, in one place:
|
|
17358
|
+
*
|
|
17359
|
+
* | call | retried on a throw? | why |
|
|
17360
|
+
* |---|---|---|
|
|
17361
|
+
* | 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 |
|
|
17362
|
+
* | 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} |
|
|
17363
|
+
* | `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 |
|
|
17364
|
+
* | `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 |
|
|
17365
|
+
* | 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
17366
|
*/
|
|
17014
17367
|
disableOuterRetry = true;
|
|
17015
17368
|
/** Max time to wait for synchronous S3 response after Lambda invocation (30 seconds) */
|
|
@@ -17023,8 +17376,13 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17023
17376
|
/** Max poll interval for async polling with exponential backoff (30 seconds) */
|
|
17024
17377
|
MAX_POLL_INTERVAL_MS = 3e4;
|
|
17025
17378
|
/**
|
|
17026
|
-
* How many extra times to
|
|
17027
|
-
* FAILED with a *transient IAM-authorization* reason
|
|
17379
|
+
* How many extra times to RE-INVOKE a custom resource whose handler returned
|
|
17380
|
+
* FAILED with a *transient IAM-authorization* reason.
|
|
17381
|
+
*
|
|
17382
|
+
* TWO error shapes, TWO budgets, deliberately (issue #2033) — this one and
|
|
17383
|
+
* {@link CustomResourceProvider.preDeliveryAuthzMaxRetries}. This budget
|
|
17384
|
+
* governs the shape where the handler ALREADY RAN: it returned FAILED with a
|
|
17385
|
+
* transient-authz reason (e.g. the CDK Provider
|
|
17028
17386
|
* framework's `lambda:GetFunction` / "not in the state functionActive" 403
|
|
17029
17387
|
* when the framework role's freshly-attached inline policy has not yet
|
|
17030
17388
|
* propagated to the assumed-role session). cdkd's fast SDK path invokes the
|
|
@@ -17036,16 +17394,67 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17036
17394
|
* `disableOuterRetry` to avoid stranding a pre-signed response URL — so we
|
|
17037
17395
|
* retry HERE instead, deriving a fresh response URL + RequestId per attempt
|
|
17038
17396
|
* and recycling the backing function's execution environment between tries).
|
|
17039
|
-
*
|
|
17397
|
+
*
|
|
17398
|
+
* **It is SMALL (2) because every retry it authorises RE-RUNS THE USER'S
|
|
17399
|
+
* HANDLER**, and that is the whole reason the second shape does not share it:
|
|
17400
|
+
* a pre-delivery throw invokes the handler ZERO times, so the argument that
|
|
17401
|
+
* bounds this number does not apply there at all. Sharing it was measured
|
|
17402
|
+
* wrong — 2 retries on the dense schedule is 250ms + 500ms of coverage, i.e.
|
|
17403
|
+
* 0.75s against an IAM-propagation window this repo has measured at 7-12s, so
|
|
17404
|
+
* issue #2033's own scenario still failed with the fix in place.
|
|
17405
|
+
*
|
|
17406
|
+
* Override via `CDKD_CR_AUTHZ_MAX_RETRIES` (clamped to
|
|
17407
|
+
* {@link CR_AUTHZ_MAX_RETRIES_CEILING}). `0` disables the RE-INVOKE only —
|
|
17040
17408
|
* the issue-#1674 log-tail scan and the reason annotation it produces still
|
|
17041
17409
|
* run, because they describe the failure rather than react to it.
|
|
17410
|
+
*
|
|
17411
|
+
* It does NOT disable either of the two retries that cannot reach the
|
|
17412
|
+
* handler: the response-placeholder `PutObject` retry in
|
|
17413
|
+
* `generateResponseURL`, and the pre-delivery arm above. Neither is what this
|
|
17414
|
+
* knob exists to bound — it bounds how many times a user's handler may be
|
|
17415
|
+
* re-run — and a user turning off re-invokes should not thereby lose the
|
|
17416
|
+
* propagation coverage every other resource type gets for free.
|
|
17042
17417
|
*/
|
|
17043
17418
|
transientAuthzMaxRetries = (() => {
|
|
17044
17419
|
const raw = process.env["CDKD_CR_AUTHZ_MAX_RETRIES"];
|
|
17045
|
-
if (raw === void 0 || raw === "") return
|
|
17420
|
+
if (raw === void 0 || raw === "") return CR_AUTHZ_MAX_RETRIES_DEFAULT;
|
|
17046
17421
|
const n = Number(raw);
|
|
17047
|
-
|
|
17422
|
+
if (!Number.isFinite(n) || n < 0) return CR_AUTHZ_MAX_RETRIES_DEFAULT;
|
|
17423
|
+
const clamped = Math.min(Math.floor(n), CR_AUTHZ_MAX_RETRIES_CEILING);
|
|
17424
|
+
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).`);
|
|
17425
|
+
return clamped;
|
|
17048
17426
|
})();
|
|
17427
|
+
/**
|
|
17428
|
+
* Budget for the PRE-DELIVERY thrown arm — a transient IAM-authorization
|
|
17429
|
+
* error thrown by the `Invoke` / `Publish` itself BEFORE the request reached
|
|
17430
|
+
* the handler (issue #2033). The reported shape is an `AccessDeniedException`
|
|
17431
|
+
* on `lambda:InvokeFunction` while the DEPLOYING principal's own
|
|
17432
|
+
* freshly-attached policy is still propagating.
|
|
17433
|
+
*
|
|
17434
|
+
* It is {@link IAM_PROPAGATION_MAX_RETRIES} — the same 26 retries over 47.75s
|
|
17435
|
+
* that `withRetry` gives every other resource type for the identical wording,
|
|
17436
|
+
* on the same dense schedule ({@link IAM_PROPAGATION_INITIAL_DELAY_MS}
|
|
17437
|
+
* doubling to {@link IAM_PROPAGATION_MAX_DELAY_MS}). The measured window this
|
|
17438
|
+
* has to cover is 7-12s; the FAILED-response budget's 0.75s does not.
|
|
17439
|
+
*
|
|
17440
|
+
* **Why it can afford that while its sibling cannot**: it fires only when
|
|
17441
|
+
* `delivered === false`, so the handler has been invoked ZERO times and a
|
|
17442
|
+
* replay re-runs NOTHING — no partial work repeated, no second physical
|
|
17443
|
+
* resource from a Provider-framework `onEvent`, no stranded response URL. The
|
|
17444
|
+
* only cost of a retry here is one `PutObject` + one presign, and the
|
|
17445
|
+
* abandoned placeholder is swept before the next attempt. So the constraint
|
|
17446
|
+
* that keeps `transientAuthzMaxRetries` at 2 is simply absent, and matching
|
|
17447
|
+
* every other resource type is the correct answer instead.
|
|
17448
|
+
*
|
|
17449
|
+
* A thrown retry also skips the exec-env recycle: the denial is on CDKD's own
|
|
17450
|
+
* principal, not on the backing function's role, so there is no warm
|
|
17451
|
+
* container holding stale credentials to invalidate.
|
|
17452
|
+
*
|
|
17453
|
+
* Deliberately NOT overridable by an env var. It bounds no handler
|
|
17454
|
+
* invocation, so there is nothing for a user to trade off — the same reason
|
|
17455
|
+
* the placeholder `PutObject`'s `withRetry` takes no knob either.
|
|
17456
|
+
*/
|
|
17457
|
+
preDeliveryAuthzMaxRetries = 26;
|
|
17049
17458
|
constructor(config) {
|
|
17050
17459
|
const awsClients = getAwsClients();
|
|
17051
17460
|
this.lambdaClient = awsClients.lambda;
|
|
@@ -17252,7 +17661,11 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17252
17661
|
if (cfnResponse.Status === "FAILED") this.logger.warn(`Custom resource delete handler returned FAILED for ${logicalId}: ${cfnResponse.Reason || "Unknown reason"}`);
|
|
17253
17662
|
else this.logger.debug(`Successfully deleted custom resource ${logicalId}`);
|
|
17254
17663
|
} catch (error) {
|
|
17255
|
-
this.logger.warn(`Failed to delete custom resource ${logicalId}, but continuing: ${error instanceof Error ? error.message : String(error)}`);
|
|
17664
|
+
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}`);
|
|
17665
|
+
return {
|
|
17666
|
+
outcome: "skipped",
|
|
17667
|
+
reason: CR_DELETE_INVOKE_FAILED_SKIP_REASON
|
|
17668
|
+
};
|
|
17256
17669
|
}
|
|
17257
17670
|
}
|
|
17258
17671
|
/**
|
|
@@ -17309,31 +17722,71 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17309
17722
|
* `prepareInvocation()`) and recycling the backing function's execution
|
|
17310
17723
|
* environment between tries so its next cold start re-assumes the role.
|
|
17311
17724
|
*
|
|
17725
|
+
* **The loop covers TWO error shapes** (issue #2033). The original one is the
|
|
17726
|
+
* handler's RETURNED `Status: 'FAILED'`. The second is an error THROWN by one
|
|
17727
|
+
* of the provider's OWN SDK calls before the request reached the handler —
|
|
17728
|
+
* which used to leave `create()` directly, because the loop body had no
|
|
17729
|
+
* `try` / `catch` at any point, making every such call single-shot while every
|
|
17730
|
+
* other resource type got the outer `withRetry`'s 26 attempts for the identical
|
|
17731
|
+
* wording. The `delivered` flag below is what keeps the second arm honest: it
|
|
17732
|
+
* flips the moment `Invoke` / `Publish` RETURNS, and a throw after that point
|
|
17733
|
+
* is rethrown untouched no matter how transient it reads, because the handler
|
|
17734
|
+
* is running and will PUT to THIS attempt's response URL. See
|
|
17735
|
+
* {@link CustomResourceProvider.disableOuterRetry} for the whole per-call
|
|
17736
|
+
* table, and {@link CustomResourceProvider.isTransientAuthzThrow} for why a
|
|
17737
|
+
* PRE-delivery throw is safe to replay at all.
|
|
17738
|
+
*
|
|
17312
17739
|
* `buildRequest` is called once per attempt with the fresh invocation so the
|
|
17313
17740
|
* CFn request body always carries the matching ResponseURL / RequestId.
|
|
17314
17741
|
* Returns the final response; the caller decides what a terminal FAILED means
|
|
17315
17742
|
* (create/update throw, delete warns-and-continues).
|
|
17316
17743
|
*/
|
|
17317
17744
|
async invokeCustomResourceWithRetry(serviceToken, logicalId, operation, buildRequest) {
|
|
17318
|
-
|
|
17319
|
-
|
|
17320
|
-
|
|
17321
|
-
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17745
|
+
const watch = this.startInterruptWatch(logicalId);
|
|
17746
|
+
try {
|
|
17747
|
+
let preDeliveryRetries = 0;
|
|
17748
|
+
let failedResponseRetries = 0;
|
|
17749
|
+
for (let attempt = 0;; attempt++) {
|
|
17750
|
+
let delivered = false;
|
|
17751
|
+
let cfnResponse;
|
|
17752
|
+
let logResult;
|
|
17753
|
+
let invocation;
|
|
17754
|
+
try {
|
|
17755
|
+
invocation = await this.prepareInvocation(logicalId, watch);
|
|
17756
|
+
const request = buildRequest(invocation);
|
|
17757
|
+
this.logger.debug(`Sending custom resource ${operation.toLowerCase()} request: ${serviceToken}`);
|
|
17758
|
+
const sent = await this.sendRequest(serviceToken, request, invocation.responseKey, logicalId, operation, () => {
|
|
17759
|
+
delivered = true;
|
|
17760
|
+
});
|
|
17761
|
+
cfnResponse = sent.response;
|
|
17762
|
+
logResult = sent.logResult;
|
|
17763
|
+
} catch (error) {
|
|
17764
|
+
if (delivered || preDeliveryRetries >= this.preDeliveryAuthzMaxRetries || !this.isTransientAuthzThrow(error)) throw error;
|
|
17765
|
+
const delayMs = Math.min(250 * Math.pow(2, preDeliveryRetries), IAM_PROPAGATION_MAX_DELAY_MS);
|
|
17766
|
+
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.`);
|
|
17767
|
+
if (invocation !== void 0) await this.cleanupResponseObject(invocation.responseKey);
|
|
17768
|
+
preDeliveryRetries += 1;
|
|
17769
|
+
await this.sleepInterruptibly(delayMs, watch);
|
|
17770
|
+
continue;
|
|
17771
|
+
}
|
|
17772
|
+
const reasonIsAuthz = cfnResponse.Status === "FAILED" && this.isTransientAuthzFailure(cfnResponse.Reason);
|
|
17773
|
+
const logTail = cfnResponse.Status === "FAILED" && !reasonIsAuthz ? decodeInvokeLogTail(logResult) : void 0;
|
|
17774
|
+
const logAuthzMatch = logTail === void 0 ? void 0 : this.findTransientAuthzLogLine(logTail);
|
|
17775
|
+
if (cfnResponse.Status === "FAILED" && failedResponseRetries < this.transientAuthzMaxRetries && (reasonIsAuthz || logAuthzMatch !== void 0)) {
|
|
17776
|
+
failedResponseRetries += 1;
|
|
17777
|
+
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.`);
|
|
17778
|
+
await this.recycleBackingFunctionExecEnv(serviceToken, logicalId);
|
|
17779
|
+
continue;
|
|
17780
|
+
}
|
|
17781
|
+
if (cfnResponse.Status === "FAILED" && logAuthzMatch !== void 0) return {
|
|
17782
|
+
...cfnResponse,
|
|
17783
|
+
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]`
|
|
17784
|
+
};
|
|
17785
|
+
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));
|
|
17786
|
+
return cfnResponse;
|
|
17330
17787
|
}
|
|
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;
|
|
17788
|
+
} finally {
|
|
17789
|
+
watch.dispose();
|
|
17337
17790
|
}
|
|
17338
17791
|
}
|
|
17339
17792
|
/**
|
|
@@ -17355,6 +17808,63 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17355
17808
|
return CR_TRANSIENT_AUTHZ_SIGNALS.some((p) => lower.includes(p));
|
|
17356
17809
|
}
|
|
17357
17810
|
/**
|
|
17811
|
+
* Classify an error THROWN by one of the provider's own SDK calls as a
|
|
17812
|
+
* transient IAM-authorization race worth replaying (issue #2033).
|
|
17813
|
+
*
|
|
17814
|
+
* The list is cdkd's shared `IAM_PROPAGATION_ERROR_MESSAGE_PATTERNS` — the
|
|
17815
|
+
* same one every other resource type is classified with, via `withRetry` —
|
|
17816
|
+
* plus {@link CR_THROWN_AUTHZ_EXTRA_SIGNALS}, the two CR-specific spellings
|
|
17817
|
+
* the shared list does not carry in any form. That is deliberately WIDER than
|
|
17818
|
+
* the FAILED-reason classifier, and the asymmetry is the whole point: this
|
|
17819
|
+
* text was written by AWS about a call CDKD made, whereas a FAILED reason was
|
|
17820
|
+
* written by the user's handler about a call IT made. See
|
|
17821
|
+
* {@link CR_TRANSIENT_AUTHZ_SIGNALS} for the full argument, including why the
|
|
17822
|
+
* bare `is unable to assume` spelling stays on that side of the line.
|
|
17823
|
+
*
|
|
17824
|
+
* **Two REFUSALS come before any pattern**, and each closes a way for a
|
|
17825
|
+
* single call to be given two retry budgets:
|
|
17826
|
+
*
|
|
17827
|
+
* - `isMarkedNonRetryable` — cdkd's own declaration that this raising cannot
|
|
17828
|
+
* succeed on a replay. Both of the provider's already-retried calls stamp
|
|
17829
|
+
* it: the placeholder `PutObject` after its `withRetry` is exhausted, and
|
|
17830
|
+
* `waitForBackingLambdaReady` after the SDK waiter has polled for its full
|
|
17831
|
+
* 600s. The marker rather than the wording is what makes the second one
|
|
17832
|
+
* sound: `@smithy/util-waiter` serializes `observedResponses` into its
|
|
17833
|
+
* TIMEOUT message and those keys read
|
|
17834
|
+
* `403: User: … is not authorized to perform: lambda:GetFunction …`, so a
|
|
17835
|
+
* PERMANENT denial matched every pattern here and a message-shaped fence
|
|
17836
|
+
* would be one AWS wording change from failing open. It also refuses a
|
|
17837
|
+
* cdkd-authored REFUSAL whose text happens to carry an authz phrase.
|
|
17838
|
+
* - the `.cause` chain is WALKED (bounded), matching `isThrottlingError` /
|
|
17839
|
+
* `isMarkedNonRetryable`. cdkd wraps SDK errors routinely, and issue #2040
|
|
17840
|
+
* documents the drop-`cause` class in this very directory; a top-level-only
|
|
17841
|
+
* read would silently un-retry a wrapped propagation denial.
|
|
17842
|
+
*
|
|
17843
|
+
* **Every pattern in that union is an AUTHORIZATION or REQUEST-VALIDATION
|
|
17844
|
+
* rejection, and that is what makes replaying an `Invoke` safe here.** Such a
|
|
17845
|
+
* rejection is decided at the API front door, before any execution environment
|
|
17846
|
+
* is engaged, so the handler provably did not run and a replay cannot
|
|
17847
|
+
* re-deliver work — the hazard `disableOuterRetry` exists for. It is also why
|
|
17848
|
+
* this classifier deliberately does NOT reach for the broader
|
|
17849
|
+
* `isRetryableTransientError`: a throttle, an HTTP 5xx or a socket timeout can
|
|
17850
|
+
* each arrive AFTER the request was accepted, so replaying one could invoke a
|
|
17851
|
+
* non-idempotent handler twice. Those classes stay single-shot on this path by
|
|
17852
|
+
* design, and the caller's `delivered` flag is the second, independent fence.
|
|
17853
|
+
*/
|
|
17854
|
+
isTransientAuthzThrow(error) {
|
|
17855
|
+
if (isMarkedNonRetryable(error)) return false;
|
|
17856
|
+
let current = error;
|
|
17857
|
+
for (let depth = 0; depth < CR_ERROR_CAUSE_MAX_DEPTH && current != null; depth++) {
|
|
17858
|
+
const message = current instanceof Error ? current.message : typeof current === "string" ? current : "";
|
|
17859
|
+
if (message !== "") {
|
|
17860
|
+
const lower = message.toLowerCase();
|
|
17861
|
+
if (isIamPropagationError(message) || CR_THROWN_AUTHZ_EXTRA_SIGNALS.some((p) => lower.includes(p))) return true;
|
|
17862
|
+
}
|
|
17863
|
+
current = current.cause;
|
|
17864
|
+
}
|
|
17865
|
+
return false;
|
|
17866
|
+
}
|
|
17867
|
+
/**
|
|
17358
17868
|
* Find the IAM-authorization denial inside a backing function's invocation
|
|
17359
17869
|
* log tail, for the case the FAILED reason itself carries none (issue #1674).
|
|
17360
17870
|
*
|
|
@@ -17440,15 +17950,25 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17440
17950
|
* erased from the FAILED reason (issue #1674). Returned UNDECODED so the
|
|
17441
17951
|
* happy path pays nothing — only a FAILED whose reason missed decodes it.
|
|
17442
17952
|
* Absent on the SNS path: there is no Lambda invoke to attach a log to.
|
|
17953
|
+
*
|
|
17954
|
+
* `onDelivered` is invoked exactly once, the moment the `Invoke` / `Publish`
|
|
17955
|
+
* RETURNS — i.e. at the point after which a replay would re-deliver the
|
|
17956
|
+
* request to the handler and strand this attempt's pre-signed response URL.
|
|
17957
|
+
* The caller's retry-on-throw arm is fenced on it (issue #2033). Deliberately
|
|
17958
|
+
* called AFTER the send rather than before: a rejection at the API front door
|
|
17959
|
+
* (the IAM-propagation class this arm exists for) means the handler never ran,
|
|
17960
|
+
* and treating that as delivered would leave the reported failure single-shot.
|
|
17443
17961
|
*/
|
|
17444
|
-
async sendRequest(serviceToken, request, responseKey, logicalId, operation) {
|
|
17962
|
+
async sendRequest(serviceToken, request, responseKey, logicalId, operation, onDelivered) {
|
|
17445
17963
|
if (this.isSnsServiceToken(serviceToken)) {
|
|
17446
17964
|
this.logger.debug(`ServiceToken is SNS topic, publishing to: ${serviceToken}`);
|
|
17447
17965
|
await this.publishToSns(serviceToken, request);
|
|
17966
|
+
onDelivered();
|
|
17448
17967
|
return { response: await this.pollS3Response(responseKey, logicalId, operation) };
|
|
17449
17968
|
}
|
|
17450
17969
|
await this.waitForBackingLambdaReady(serviceToken, logicalId);
|
|
17451
17970
|
const invokeResponse = await this.invokeLambda(serviceToken, request);
|
|
17971
|
+
onDelivered();
|
|
17452
17972
|
return {
|
|
17453
17973
|
response: await this.getCustomResourceResponse(invokeResponse, responseKey, logicalId, operation),
|
|
17454
17974
|
...invokeResponse.LogResult === void 0 ? {} : { logResult: invokeResponse.LogResult }
|
|
@@ -17481,6 +18001,37 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17481
18001
|
* deploy engine's per-resource `--resource-timeout` (default 30 min)
|
|
17482
18002
|
* still bounds the outer Custom Resource provisioning attempt, so
|
|
17483
18003
|
* this waiter cap is layered defense, not the only timeout.
|
|
18004
|
+
*
|
|
18005
|
+
* **The wrapped failure is `markNonRetryable`d, and its message is NOT the
|
|
18006
|
+
* waiter's** (issue #2033). Both halves are about `@smithy/util-waiter`'s
|
|
18007
|
+
* `checkExceptions`, which serializes its whole result into the `Error`
|
|
18008
|
+
* message:
|
|
18009
|
+
*
|
|
18010
|
+
* - On TIMEOUT that includes `observedResponses`, whose keys read
|
|
18011
|
+
* `403: User: … is not authorized to perform: lambda:GetFunction on
|
|
18012
|
+
* resource: …`. The waiter's generated `checkState` catches EVERY
|
|
18013
|
+
* exception and returns `RETRY`, so reaching here means a 600s budget was
|
|
18014
|
+
* already spent — yet the message matched
|
|
18015
|
+
* {@link CustomResourceProvider.isTransientAuthzThrow} exactly, so a
|
|
18016
|
+
* PERMANENT `lambda:GetFunction` denial was replayed for 3 x 600s instead
|
|
18017
|
+
* of 10 minutes, blowing any `--resource-timeout
|
|
18018
|
+
* AWS::CloudFormation::CustomResource=15m`. The marker is a property of
|
|
18019
|
+
* the error object, so unlike a wording test it cannot be defeated by AWS
|
|
18020
|
+
* rephrasing the denial.
|
|
18021
|
+
* - On the non-TIMEOUT arm (`State: Failed`, i.e. an ENI / VPC failure) the
|
|
18022
|
+
* serialized result carries `reason` and `final`, which for this waiter
|
|
18023
|
+
* are the ENTIRE `GetFunction` response — `Configuration.Environment.
|
|
18024
|
+
* Variables` included. That message reached `ProvisioningError.message`
|
|
18025
|
+
* and `extractDeploymentEventError` persisted it to
|
|
18026
|
+
* `deployments/{runId}.jsonl`, a durable store that outlives
|
|
18027
|
+
* `cdkd destroy` and is contractually "error + metadata only, never
|
|
18028
|
+
* resource properties, because they may contain secrets"
|
|
18029
|
+
* (`docs/deployment-events.md`). So this arm interpolates the error NAME
|
|
18030
|
+
* plus a fixed sentence, and recovers only the function's own
|
|
18031
|
+
* `State` / `StateReason` / `StateReasonCode` — AWS-authored status
|
|
18032
|
+
* fields — from the serialized payload. The TIMEOUT / ABORT arms keep
|
|
18033
|
+
* their message: `observedResponses` keys are status lines built by
|
|
18034
|
+
* `createMessageFromResponse`, never a response body.
|
|
17484
18035
|
*/
|
|
17485
18036
|
async waitForBackingLambdaReady(serviceToken, logicalId) {
|
|
17486
18037
|
try {
|
|
@@ -17497,7 +18048,7 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17497
18048
|
maxDelay: 5
|
|
17498
18049
|
}, { FunctionName: serviceToken });
|
|
17499
18050
|
} catch (error) {
|
|
17500
|
-
throw new Error(`Lambda backing custom resource ${logicalId} (${serviceToken}) did not reach a ready state for Invoke: ${error
|
|
18051
|
+
throw markNonRetryable(new Error(`Lambda backing custom resource ${logicalId} (${serviceToken}) did not reach a ready state for Invoke: ${describeWaiterFailure(error)}`, { cause: error }));
|
|
17501
18052
|
}
|
|
17502
18053
|
}
|
|
17503
18054
|
/**
|
|
@@ -17593,28 +18144,38 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17593
18144
|
* Centralising this in one helper makes that invariant impossible to
|
|
17594
18145
|
* violate at the call sites.
|
|
17595
18146
|
*/
|
|
17596
|
-
async prepareInvocation() {
|
|
18147
|
+
async prepareInvocation(logicalId, watch) {
|
|
17597
18148
|
const requestId = `cdkd-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
17598
18149
|
const responseKey = this.getResponseKey(requestId);
|
|
17599
18150
|
return {
|
|
17600
18151
|
requestId,
|
|
17601
18152
|
responseKey,
|
|
17602
|
-
responseURL: await this.generateResponseURL(responseKey)
|
|
18153
|
+
responseURL: await this.generateResponseURL(responseKey, logicalId, watch)
|
|
17603
18154
|
};
|
|
17604
18155
|
}
|
|
17605
18156
|
/**
|
|
17606
18157
|
* Generate a pre-signed S3 PUT URL for Lambda to send its response
|
|
17607
18158
|
*/
|
|
17608
|
-
async generateResponseURL(responseKey) {
|
|
18159
|
+
async generateResponseURL(responseKey, logicalId, watch) {
|
|
17609
18160
|
if (!this.responseBucket) return "https://localhost/cfn-response-not-configured";
|
|
17610
18161
|
await this.ensureResponseClient();
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
18162
|
+
const bucket = this.responseBucket;
|
|
18163
|
+
try {
|
|
18164
|
+
await withRetry(() => this.s3Client.send(new PutObjectCommand({
|
|
18165
|
+
Bucket: bucket,
|
|
18166
|
+
Key: responseKey,
|
|
18167
|
+
Body: "",
|
|
18168
|
+
ContentLength: 0,
|
|
18169
|
+
ContentType: "application/json"
|
|
18170
|
+
})), `${logicalId} (custom-resource response placeholder)`, {
|
|
18171
|
+
logger: this.logger,
|
|
18172
|
+
isInterrupted: watch.isInterrupted,
|
|
18173
|
+
onInterrupted: watch.onInterrupted,
|
|
18174
|
+
sleep: customResourceRetryDelays.sleep
|
|
18175
|
+
});
|
|
18176
|
+
} catch (error) {
|
|
18177
|
+
throw markNonRetryable(error instanceof Error ? error : new Error(String(error)));
|
|
18178
|
+
}
|
|
17618
18179
|
const command = new PutObjectCommand({
|
|
17619
18180
|
Bucket: this.responseBucket,
|
|
17620
18181
|
Key: responseKey
|
|
@@ -17725,7 +18286,50 @@ var CustomResourceProvider = class CustomResourceProvider {
|
|
|
17725
18286
|
return result;
|
|
17726
18287
|
}
|
|
17727
18288
|
sleep(ms) {
|
|
17728
|
-
return
|
|
18289
|
+
return customResourceRetryDelays.sleep(ms);
|
|
18290
|
+
}
|
|
18291
|
+
/**
|
|
18292
|
+
* Install a SIGINT watch for one custom-resource invocation (issue #2033).
|
|
18293
|
+
*
|
|
18294
|
+
* `docs/provider-development.md` requires a new `withRetry` to thread
|
|
18295
|
+
* `isInterrupted` / `onInterrupted`, and a hand-rolled backoff to be
|
|
18296
|
+
* interruptible for the same reason: without it Ctrl-C is dead for the whole
|
|
18297
|
+
* 47.75s schedule. The provider already had the shape — `pollS3Response`
|
|
18298
|
+
* installs its own handler — so this is that pattern, lifted so the two new
|
|
18299
|
+
* wait sites share one flag and one disposal.
|
|
18300
|
+
*
|
|
18301
|
+
* The caller MUST `dispose()` in a `finally`; a leaked listener would
|
|
18302
|
+
* accumulate one per resource across a deploy.
|
|
18303
|
+
*/
|
|
18304
|
+
startInterruptWatch(logicalId) {
|
|
18305
|
+
let interrupted = false;
|
|
18306
|
+
const handler = () => {
|
|
18307
|
+
interrupted = true;
|
|
18308
|
+
};
|
|
18309
|
+
process.on("SIGINT", handler);
|
|
18310
|
+
return {
|
|
18311
|
+
isInterrupted: () => interrupted,
|
|
18312
|
+
onInterrupted: () => /* @__PURE__ */ new Error(`Custom resource ${logicalId} interrupted by user`),
|
|
18313
|
+
dispose: () => {
|
|
18314
|
+
process.removeListener("SIGINT", handler);
|
|
18315
|
+
}
|
|
18316
|
+
};
|
|
18317
|
+
}
|
|
18318
|
+
/**
|
|
18319
|
+
* `sleep` that checks the interrupt watch at most a second apart, mirroring
|
|
18320
|
+
* `withRetry`'s own once-per-second probe. Throws the watch's error when the
|
|
18321
|
+
* user has hit Ctrl-C, so the retry loop unwinds instead of sitting out the
|
|
18322
|
+
* remaining backoff.
|
|
18323
|
+
*/
|
|
18324
|
+
async sleepInterruptibly(ms, watch) {
|
|
18325
|
+
let remaining = ms;
|
|
18326
|
+
while (remaining > 0) {
|
|
18327
|
+
if (watch.isInterrupted()) throw watch.onInterrupted();
|
|
18328
|
+
const chunk = Math.min(1e3, remaining);
|
|
18329
|
+
await this.sleep(chunk);
|
|
18330
|
+
remaining -= chunk;
|
|
18331
|
+
}
|
|
18332
|
+
if (watch.isInterrupted()) throw watch.onInterrupted();
|
|
17729
18333
|
}
|
|
17730
18334
|
/**
|
|
17731
18335
|
* Adopt an existing custom resource into cdkd state.
|
|
@@ -22426,6 +23030,22 @@ function computeImplicitDeleteEdges(resources) {
|
|
|
22426
23030
|
return edges;
|
|
22427
23031
|
}
|
|
22428
23032
|
|
|
23033
|
+
//#endregion
|
|
23034
|
+
//#region src/deployment/masking-retry-logger.ts
|
|
23035
|
+
/**
|
|
23036
|
+
* Bind `logger` to `secrets`, masking every line the retry loop emits.
|
|
23037
|
+
*
|
|
23038
|
+
* No-op when the caller resolved no secret ({@link maskSecretsInText} returns
|
|
23039
|
+
* the text unchanged for an empty bag), so a non-secret resource's output is
|
|
23040
|
+
* byte-identical to threading the raw logger.
|
|
23041
|
+
*/
|
|
23042
|
+
function maskingRetryLogger(logger, secrets) {
|
|
23043
|
+
return {
|
|
23044
|
+
debug: (msg) => logger.debug(maskSecretsInText(msg, secrets)),
|
|
23045
|
+
warn: (msg) => logger.warn(maskSecretsInText(msg, secrets))
|
|
23046
|
+
};
|
|
23047
|
+
}
|
|
23048
|
+
|
|
22429
23049
|
//#endregion
|
|
22430
23050
|
//#region src/deployment/resource-deadline.ts
|
|
22431
23051
|
/**
|
|
@@ -22705,6 +23325,38 @@ function replayingStateCreateContext(secrets) {
|
|
|
22705
23325
|
};
|
|
22706
23326
|
}
|
|
22707
23327
|
/**
|
|
23328
|
+
* The {@link DeploymentEventError} a failed replay records, with this op's
|
|
23329
|
+
* re-resolved secrets masked out of its message (issue
|
|
23330
|
+
* [#2031](https://github.com/go-to-k/cdkd/issues/2031) acceptance item 2).
|
|
23331
|
+
*
|
|
23332
|
+
* `extractDeploymentEventError` copies `err.message` VERBATIM, and the events
|
|
23333
|
+
* store is a DURABLE sink — `deployments/{runId}.jsonl` in S3 outlives the
|
|
23334
|
+
* terminal the `logger.warn` beside it scrolls past, and `cdkd events` replays
|
|
23335
|
+
* it later. The standalone `cdkd rollback` command wires
|
|
23336
|
+
* `recordEvent: (e) => eventRecorder.record(e)` (`src/cli/commands/rollback.ts`)
|
|
23337
|
+
* with NO masking of its own, so without this the plaintext the terminal line
|
|
23338
|
+
* masks is persisted one statement later.
|
|
23339
|
+
*
|
|
23340
|
+
* The in-process caller (`DeployEngine.rollbackExecutorContext`) routes through
|
|
23341
|
+
* `maskSecretsInEvent`, but that masks with the DEPLOY's `perResourceSecrets`
|
|
23342
|
+
* for the resource — a different bag from the one this replay re-resolved from
|
|
23343
|
+
* the JOURNAL, which can name a different secret version or a reference the
|
|
23344
|
+
* deploy never resolved. Masking here is what makes both callers equal, and
|
|
23345
|
+
* double-masking is a no-op (the mask is not a key of either bag).
|
|
23346
|
+
*
|
|
23347
|
+
* `name` / `awsErrorCode` / `requestId` are deliberately left alone: they are
|
|
23348
|
+
* AWS-authored identifiers, not message text, and #2038 traced all three as
|
|
23349
|
+
* non-sensitive.
|
|
23350
|
+
*/
|
|
23351
|
+
function maskedRollbackEventError(error, secrets) {
|
|
23352
|
+
const extracted = extractDeploymentEventError(error);
|
|
23353
|
+
if (secrets.size === 0) return extracted;
|
|
23354
|
+
return {
|
|
23355
|
+
...extracted,
|
|
23356
|
+
message: maskSecretsInText(extracted.message, secrets)
|
|
23357
|
+
};
|
|
23358
|
+
}
|
|
23359
|
+
/**
|
|
22708
23360
|
* Which provisioning layer a delete must be judged against: the CURRENT
|
|
22709
23361
|
* state record wins (it is what state says AWS holds right now), with the
|
|
22710
23362
|
* journaled op's routing as the legacy-state fallback. Shared by both
|
|
@@ -23063,10 +23715,10 @@ function redactRollbackRecord(record, secrets, journaledProps) {
|
|
|
23063
23715
|
properties: redactSecretsForState(record.properties, secrets, journaledProps, STATE_DERIVED_RULES)
|
|
23064
23716
|
}, secrets);
|
|
23065
23717
|
}
|
|
23066
|
-
async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted) {
|
|
23718
|
+
async function updateWithRollbackRetry(provider, args, logicalId, logger, isInterrupted, secrets) {
|
|
23067
23719
|
if (provider.disableOuterRetry) return await provider.update(...args);
|
|
23068
23720
|
return await withRetry(() => provider.update(...args), logicalId, {
|
|
23069
|
-
logger,
|
|
23721
|
+
logger: maskingRetryLogger(logger, secrets),
|
|
23070
23722
|
...isInterrupted && {
|
|
23071
23723
|
isInterrupted,
|
|
23072
23724
|
onInterrupted: () => /* @__PURE__ */ new Error("Rollback interrupted while retrying a resource update")
|
|
@@ -23121,6 +23773,21 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23121
23773
|
const action = classifyRollbackOp(op, stateResources, orphanLogicalIds);
|
|
23122
23774
|
const { logger } = ctx;
|
|
23123
23775
|
/**
|
|
23776
|
+
* This op's `plaintext -> {{resolve:...}}expression` bag, filled by
|
|
23777
|
+
* {@link resolveReplayProps} on whichever arm runs (issues
|
|
23778
|
+
* [#2038](https://github.com/go-to-k/cdkd/issues/2038) /
|
|
23779
|
+
* [#2031](https://github.com/go-to-k/cdkd/issues/2031)).
|
|
23780
|
+
*
|
|
23781
|
+
* HOISTED above the `try` rather than declared per arm, which is what the
|
|
23782
|
+
* two arms used to do: the shared catch below logs the thrown AWS message and
|
|
23783
|
+
* persists it to the events store, and it cannot see a binding scoped to the
|
|
23784
|
+
* arm that threw. Exactly one arm runs per call, so a single per-op bag is
|
|
23785
|
+
* equivalent to the per-arm ones for every existing reader (`secrets.size`
|
|
23786
|
+
* stays 0 on the arms that resolve nothing, so `redactRollbackRecord` and the
|
|
23787
|
+
* maskers keep their identity behavior).
|
|
23788
|
+
*/
|
|
23789
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
23790
|
+
/**
|
|
23124
23791
|
* The route a CREATE-rollback arm resolved for this op (issue #1366) —
|
|
23125
23792
|
* hoisted so the shared catch's ROLLBACK_RESOURCE_FAILED reports the route
|
|
23126
23793
|
* the delete was going to take, which is the one a refusal is about. Stays
|
|
@@ -23244,7 +23911,6 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23244
23911
|
case "reverse-replacement": {
|
|
23245
23912
|
const current = stateResources[op.logicalId];
|
|
23246
23913
|
const prev = op.previousState;
|
|
23247
|
-
const secrets = /* @__PURE__ */ new Map();
|
|
23248
23914
|
const resolvedPrevProps = await resolveReplayProps(prev.properties, resolver, secrets) ?? {};
|
|
23249
23915
|
logger.info(` Rollback: Reversing replacement of ${op.logicalId} (${op.resourceType}) — re-creating the old resource and deleting the new one`);
|
|
23250
23916
|
if (STATEFUL_TYPES.has(op.resourceType)) logger.warn(` ⚠ ${op.logicalId} (${op.resourceType}) is a stateful type — the old physical resource's data was destroyed by the replacement and CANNOT be recovered; the re-created resource starts empty.`);
|
|
@@ -23261,7 +23927,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23261
23927
|
try {
|
|
23262
23928
|
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, replayingStateCreateContext(secrets)), op.logicalId, {
|
|
23263
23929
|
...RECREATE_RETRY_SCHEDULE,
|
|
23264
|
-
logger,
|
|
23930
|
+
logger: maskingRetryLogger(logger, secrets),
|
|
23265
23931
|
...isInterrupted && {
|
|
23266
23932
|
isInterrupted,
|
|
23267
23933
|
onInterrupted: () => /* @__PURE__ */ new Error("Rollback interrupted while waiting out the name cooldown")
|
|
@@ -23284,7 +23950,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23284
23950
|
try {
|
|
23285
23951
|
createResult = await withRetry(() => createProvider.create(op.logicalId, op.resourceType, { ...resolvedPrevProps }, replayingStateCreateContext(secrets)), op.logicalId, {
|
|
23286
23952
|
...RECREATE_RETRY_SCHEDULE,
|
|
23287
|
-
logger,
|
|
23953
|
+
logger: maskingRetryLogger(logger, secrets),
|
|
23288
23954
|
...isInterrupted && {
|
|
23289
23955
|
isInterrupted,
|
|
23290
23956
|
onInterrupted: () => /* @__PURE__ */ new Error("Rollback interrupted while waiting for the old name to release")
|
|
@@ -23292,7 +23958,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23292
23958
|
isRetryable: isRecreateRetryableError
|
|
23293
23959
|
});
|
|
23294
23960
|
} catch (recreateError) {
|
|
23295
|
-
throw new Error(`Failed to re-create the old ${op.logicalId} after the new resource (${current.physicalId}) was already deleted: ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. The resource is now absent — fix forward with 'cdkd deploy'
|
|
23961
|
+
throw new Error(maskSecretsInText(`Failed to re-create the old ${op.logicalId} after the new resource (${current.physicalId}) was already deleted: ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. The resource is now absent — fix forward with 'cdkd deploy'.`, secrets));
|
|
23296
23962
|
}
|
|
23297
23963
|
}
|
|
23298
23964
|
const adoptedLiveNewResource = !deletedNewFirst && createResult.physicalId === current.physicalId;
|
|
@@ -23315,7 +23981,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23315
23981
|
...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
|
|
23316
23982
|
}), op.logicalId, current.physicalId, "while deleting the new resource after re-creating the old one");
|
|
23317
23983
|
} catch (deleteError) {
|
|
23318
|
-
logger.warn(` Rollback: old ${op.logicalId} re-created, but deleting the new resource (${current.physicalId}) failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}. Delete it manually — it is no longer tracked in state
|
|
23984
|
+
logger.warn(maskSecretsInText(` Rollback: old ${op.logicalId} re-created, but deleting the new resource (${current.physicalId}) failed: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}. Delete it manually — it is no longer tracked in state.`, secrets));
|
|
23319
23985
|
result.warnings++;
|
|
23320
23986
|
}
|
|
23321
23987
|
logger.info(adoptedLiveNewResource ? ` Rollback: ${op.logicalId} adopted the live resource (${createResult.physicalId}) — replacement NOT fully reversed (name-idempotent Create API)` : ` Rollback: ${op.logicalId} replacement reversed (old resource re-created as ${createResult.physicalId})`);
|
|
@@ -23347,7 +24013,6 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23347
24013
|
resourceType: op.resourceType,
|
|
23348
24014
|
provisionedBy: op.provisionedBy
|
|
23349
24015
|
});
|
|
23350
|
-
const secrets = /* @__PURE__ */ new Map();
|
|
23351
24016
|
const desiredProps = await resolveReplayProps(previousState.properties, resolver, secrets);
|
|
23352
24017
|
const currentProps = await resolveReplayProps(current.properties, resolver, secrets);
|
|
23353
24018
|
const revertResult = await updateWithRollbackRetry(provider, [
|
|
@@ -23357,10 +24022,10 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23357
24022
|
desiredProps ?? {},
|
|
23358
24023
|
currentProps ?? {},
|
|
23359
24024
|
{ maskSecrets: createSecretMasker(secrets) }
|
|
23360
|
-
], op.logicalId, logger, isInterrupted);
|
|
24025
|
+
], op.logicalId, logger, isInterrupted, secrets);
|
|
23361
24026
|
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(previousState, revertResult), secrets, previousState.properties);
|
|
23362
24027
|
const rollbackPartial = updatePartialReason(revertResult);
|
|
23363
|
-
if (rollbackPartial !== void 0) logger.warn(` Rollback: ${op.logicalId} restored, ${updatePartialMessage(rollbackPartial)}
|
|
24028
|
+
if (rollbackPartial !== void 0) logger.warn(maskSecretsInText(` Rollback: ${op.logicalId} restored, ${updatePartialMessage(rollbackPartial)}`, secrets));
|
|
23364
24029
|
else logger.info(` Rollback: ${op.logicalId} restored successfully`);
|
|
23365
24030
|
await afterOp?.(op.logicalId);
|
|
23366
24031
|
ctx.recordEvent?.({
|
|
@@ -23370,13 +24035,13 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23370
24035
|
logicalId: op.logicalId,
|
|
23371
24036
|
resourceType: op.resourceType,
|
|
23372
24037
|
...op.provisionedBy && { provisionedBy: op.provisionedBy },
|
|
23373
|
-
...rollbackPartial !== void 0 && { reason: rollbackPartial }
|
|
24038
|
+
...rollbackPartial !== void 0 && { reason: maskSecretsInText(rollbackPartial, secrets) }
|
|
23374
24039
|
});
|
|
23375
24040
|
return;
|
|
23376
24041
|
}
|
|
23377
24042
|
}
|
|
23378
24043
|
} catch (rollbackError) {
|
|
23379
|
-
logger.warn(` Rollback failed for ${op.logicalId} (${op.changeType}): ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}
|
|
24044
|
+
logger.warn(maskSecretsInText(` Rollback failed for ${op.logicalId} (${op.changeType}): ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, secrets));
|
|
23380
24045
|
logger.warn(" Continuing with remaining rollback operations...");
|
|
23381
24046
|
result.failures++;
|
|
23382
24047
|
const failedRoute = createRollbackRoute ?? op.provisionedBy;
|
|
@@ -23387,7 +24052,7 @@ async function replaySingle(op, stateResources, stackName, ctx, resolver, orphan
|
|
|
23387
24052
|
logicalId: op.logicalId,
|
|
23388
24053
|
resourceType: op.resourceType,
|
|
23389
24054
|
...failedRoute && { provisionedBy: failedRoute },
|
|
23390
|
-
error:
|
|
24055
|
+
error: maskedRollbackEventError(rollbackError, secrets)
|
|
23391
24056
|
});
|
|
23392
24057
|
}
|
|
23393
24058
|
}
|
|
@@ -23424,6 +24089,15 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
23424
24089
|
}
|
|
23425
24090
|
const op = failedOps[i];
|
|
23426
24091
|
const action = classifyFailedOp(op, stateResources);
|
|
24092
|
+
/**
|
|
24093
|
+
* This op's re-resolved secret bag — the twin of `replaySingle`'s, and
|
|
24094
|
+
* hoisted above this iteration's `try` for the same reason (issues #2038 /
|
|
24095
|
+
* #2031): the shared catch below logs the thrown AWS message and persists it
|
|
24096
|
+
* to the events store, and could not see a binding scoped to the arm that
|
|
24097
|
+
* threw. Re-created per ITERATION, so one op's secrets can never mask
|
|
24098
|
+
* another's text.
|
|
24099
|
+
*/
|
|
24100
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
23427
24101
|
let createRollbackRoute;
|
|
23428
24102
|
try {
|
|
23429
24103
|
switch (action) {
|
|
@@ -23491,7 +24165,6 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
23491
24165
|
resourceType: op.resourceType,
|
|
23492
24166
|
provisionedBy: op.provisionedBy ?? current.provisionedBy
|
|
23493
24167
|
});
|
|
23494
|
-
const secrets = /* @__PURE__ */ new Map();
|
|
23495
24168
|
const desiredProps = await resolveReplayProps(prev.properties, resolver, secrets);
|
|
23496
24169
|
const attemptedProps = await resolveReplayProps(op.attemptedProperties ?? current.properties, resolver, secrets);
|
|
23497
24170
|
const revertFailedResult = await updateWithRollbackRetry(provider, [
|
|
@@ -23501,10 +24174,10 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
23501
24174
|
desiredProps ?? {},
|
|
23502
24175
|
attemptedProps ?? {},
|
|
23503
24176
|
{ maskSecrets: createSecretMasker(secrets) }
|
|
23504
|
-
], op.logicalId, logger, options.isInterrupted);
|
|
24177
|
+
], op.logicalId, logger, options.isInterrupted, secrets);
|
|
23505
24178
|
stateResources[op.logicalId] = redactRollbackRecord(recordAfterRollbackUpdate(prev, revertFailedResult), secrets, prev.properties);
|
|
23506
24179
|
const revertFailedPartial = updatePartialReason(revertFailedResult);
|
|
23507
|
-
if (revertFailedPartial !== void 0) logger.warn(` Rollback: ${op.logicalId} reverted, ${updatePartialMessage(revertFailedPartial)}
|
|
24180
|
+
if (revertFailedPartial !== void 0) logger.warn(maskSecretsInText(` Rollback: ${op.logicalId} reverted, ${updatePartialMessage(revertFailedPartial)}`, secrets));
|
|
23508
24181
|
else logger.info(` Rollback: ${op.logicalId} reverted successfully`);
|
|
23509
24182
|
await options.afterOp?.(op.logicalId);
|
|
23510
24183
|
ctx.recordEvent?.({
|
|
@@ -23514,13 +24187,13 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
23514
24187
|
logicalId: op.logicalId,
|
|
23515
24188
|
resourceType: op.resourceType,
|
|
23516
24189
|
...op.provisionedBy && { provisionedBy: op.provisionedBy },
|
|
23517
|
-
...revertFailedPartial !== void 0 && { reason: revertFailedPartial }
|
|
24190
|
+
...revertFailedPartial !== void 0 && { reason: maskSecretsInText(revertFailedPartial, secrets) }
|
|
23518
24191
|
});
|
|
23519
24192
|
break;
|
|
23520
24193
|
}
|
|
23521
24194
|
}
|
|
23522
24195
|
} catch (revertError) {
|
|
23523
|
-
logger.warn(` Rollback failed for failed-op ${op.logicalId} (${op.changeType}): ${revertError instanceof Error ? revertError.message : String(revertError)}
|
|
24196
|
+
logger.warn(maskSecretsInText(` Rollback failed for failed-op ${op.logicalId} (${op.changeType}): ${revertError instanceof Error ? revertError.message : String(revertError)}`, secrets));
|
|
23524
24197
|
result.failures++;
|
|
23525
24198
|
pending.add(op);
|
|
23526
24199
|
const failedRoute = createRollbackRoute ?? op.provisionedBy;
|
|
@@ -23531,7 +24204,7 @@ async function replayFailedOperations(failedOps, stateResources, stackName, ctx,
|
|
|
23531
24204
|
logicalId: op.logicalId,
|
|
23532
24205
|
resourceType: op.resourceType,
|
|
23533
24206
|
...failedRoute && { provisionedBy: failedRoute },
|
|
23534
|
-
error:
|
|
24207
|
+
error: maskedRollbackEventError(revertError, secrets)
|
|
23535
24208
|
});
|
|
23536
24209
|
}
|
|
23537
24210
|
}
|
|
@@ -23631,7 +24304,7 @@ const FLUSH_INTERVAL_MS = 2e3;
|
|
|
23631
24304
|
const FLUSH_EVENT_THRESHOLD = 50;
|
|
23632
24305
|
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
23633
24306
|
function getCdkdVersion() {
|
|
23634
|
-
return "0.284.
|
|
24307
|
+
return "0.284.12";
|
|
23635
24308
|
}
|
|
23636
24309
|
/**
|
|
23637
24310
|
* Generate a time-sortable unique run id, e.g.
|
|
@@ -24096,6 +24769,14 @@ var DeploymentEventsReader = class {
|
|
|
24096
24769
|
//#endregion
|
|
24097
24770
|
//#region src/deployment/deploy-engine.ts
|
|
24098
24771
|
/**
|
|
24772
|
+
* The bag a resource with no recorded secret masks against (issue #2038).
|
|
24773
|
+
* Shared so the "no entry" path allocates nothing and — more usefully — so
|
|
24774
|
+
* every masking site takes the SAME branch: `maskSecretsInText` /
|
|
24775
|
+
* `maskSecretsInError` both return their input unchanged for an empty bag, so
|
|
24776
|
+
* an absent entry and an empty one cannot behave differently. Never written to.
|
|
24777
|
+
*/
|
|
24778
|
+
const EMPTY_SECRETS = /* @__PURE__ */ new Map();
|
|
24779
|
+
/**
|
|
24099
24780
|
* Default per-resource warn threshold: warn the user when a single
|
|
24100
24781
|
* resource has been in flight for 5 minutes. Most CC API resources
|
|
24101
24782
|
* complete in under a minute; 5m is the agreed elbow.
|
|
@@ -25299,7 +25980,7 @@ var DeployEngine = class {
|
|
|
25299
25980
|
} catch (error) {
|
|
25300
25981
|
renderer.removeTask(logicalId);
|
|
25301
25982
|
const message = error instanceof Error ? error.message : String(error);
|
|
25302
|
-
this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
|
|
25983
|
+
this.logger.error(this.maskForResource(logicalId, `Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`));
|
|
25303
25984
|
this.recordEvent({
|
|
25304
25985
|
eventType: "RESOURCE_FAILED",
|
|
25305
25986
|
stackName,
|
|
@@ -25310,7 +25991,7 @@ var DeployEngine = class {
|
|
|
25310
25991
|
durationMs: Date.now() - resourceStartedAt,
|
|
25311
25992
|
error: extractDeploymentEventError(error)
|
|
25312
25993
|
});
|
|
25313
|
-
throw new ProvisioningError(`Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`, resourceType, logicalId, stateResources[logicalId]?.physicalId, error instanceof Error ? error : void 0);
|
|
25994
|
+
throw new ProvisioningError(`Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`, resourceType, logicalId, stateResources[logicalId]?.physicalId, error instanceof Error ? maskSecretsInError(error, this.perResourceSecrets.get(logicalId) ?? EMPTY_SECRETS) : void 0);
|
|
25314
25995
|
} finally {
|
|
25315
25996
|
renderer.removeTask(logicalId);
|
|
25316
25997
|
}
|
|
@@ -25399,7 +26080,8 @@ var DeployEngine = class {
|
|
|
25399
26080
|
* #960 follow-up) and the name-idempotent same-id guard (issue #1238) so
|
|
25400
26081
|
* the two --replace escape hatches cannot drift apart.
|
|
25401
26082
|
*/
|
|
25402
|
-
async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps,
|
|
26083
|
+
async replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, secrets, updateReplacePolicy) {
|
|
26084
|
+
const createContext = { maskSecrets: createSecretMasker(secrets) };
|
|
25403
26085
|
const finalSnapshotIdentifier = await this.prepareFinalSnapshotForDelete(logicalId, resourceType, currentResource, updateReplacePolicy);
|
|
25404
26086
|
let deleteResult;
|
|
25405
26087
|
try {
|
|
@@ -25409,7 +26091,7 @@ var DeployEngine = class {
|
|
|
25409
26091
|
...finalSnapshotIdentifier !== void 0 && { finalSnapshotIdentifier }
|
|
25410
26092
|
});
|
|
25411
26093
|
} catch (deleteError) {
|
|
25412
|
-
throw new Error(`Failed to delete old resource ${logicalId} (${currentResource.physicalId}) during the --replace delete-first fallback: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}
|
|
26094
|
+
throw new Error(maskSecretsInText(`Failed to delete old resource ${logicalId} (${currentResource.physicalId}) during the --replace delete-first fallback: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`, secrets));
|
|
25413
26095
|
}
|
|
25414
26096
|
const replaceSkipReason = deleteSkipReason(deleteResult);
|
|
25415
26097
|
if (replaceSkipReason !== void 0) throw new Error(deleteSkippedMessage(logicalId, currentResource.physicalId, replaceSkipReason, "during the --replace delete-first fallback"));
|
|
@@ -25420,13 +26102,13 @@ var DeployEngine = class {
|
|
|
25420
26102
|
maxRetries: 8,
|
|
25421
26103
|
initialDelayMs: 2e3,
|
|
25422
26104
|
maxDelayMs: 1e4,
|
|
25423
|
-
logger: this.
|
|
26105
|
+
logger: this.maskingRetryLoggerFor(secrets),
|
|
25424
26106
|
isInterrupted: () => this.interrupted,
|
|
25425
26107
|
onInterrupted: () => new InterruptedError(this.interruptCause ?? "user"),
|
|
25426
26108
|
isRetryable: isRecreateRetryableError
|
|
25427
26109
|
});
|
|
25428
26110
|
} catch (recreateError) {
|
|
25429
|
-
throw new Error(`Failed to re-create ${logicalId} after the --replace delete-first fallback already deleted the old resource (${currentResource.physicalId}): ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. Re-run the deploy to create it fresh
|
|
26111
|
+
throw new Error(maskSecretsInText(`Failed to re-create ${logicalId} after the --replace delete-first fallback already deleted the old resource (${currentResource.physicalId}): ${recreateError instanceof Error ? recreateError.message : String(recreateError)}. Re-run the deploy to create it fresh.`, secrets));
|
|
25430
26112
|
}
|
|
25431
26113
|
}
|
|
25432
26114
|
/**
|
|
@@ -25448,10 +26130,10 @@ var DeployEngine = class {
|
|
|
25448
26130
|
...parameterValues && { parameters: parameterValues },
|
|
25449
26131
|
...conditions && { conditions }
|
|
25450
26132
|
}, stackName);
|
|
26133
|
+
if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
|
|
25451
26134
|
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
25452
26135
|
this.perResourceTemplateProps.set(logicalId, desiredProps);
|
|
25453
26136
|
const createSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
|
|
25454
|
-
if (context.recordedSecretValues) this.perResourceSecrets.set(logicalId, context.recordedSecretValues);
|
|
25455
26137
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
25456
26138
|
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
25457
26139
|
const createDecision = this.providerRegistry.getProviderFor({
|
|
@@ -25492,9 +26174,9 @@ var DeployEngine = class {
|
|
|
25492
26174
|
...parameterValues && { parameters: parameterValues },
|
|
25493
26175
|
...conditions && { conditions }
|
|
25494
26176
|
}, stackName);
|
|
25495
|
-
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
25496
26177
|
const updateSecrets = context.recordedSecretValues ?? /* @__PURE__ */ new Map();
|
|
25497
26178
|
this.perResourceSecrets.set(logicalId, updateSecrets);
|
|
26179
|
+
const resolvedProps = await this.resolver.resolve(desiredProps, context);
|
|
25498
26180
|
this.perResourceTemplateProps.set(logicalId, desiredProps);
|
|
25499
26181
|
this.auditResolvedAssetReferences(logicalId, resourceType, resolvedProps);
|
|
25500
26182
|
this.attemptedResolvedProps.set(logicalId, resolvedProps);
|
|
@@ -25575,7 +26257,7 @@ var DeployEngine = class {
|
|
|
25575
26257
|
maxRetries: 8,
|
|
25576
26258
|
initialDelayMs: 2e3,
|
|
25577
26259
|
maxDelayMs: 1e4,
|
|
25578
|
-
logger: this.
|
|
26260
|
+
logger: this.maskingRetryLoggerFor(updateSecrets),
|
|
25579
26261
|
isInterrupted: () => this.interrupted,
|
|
25580
26262
|
onInterrupted: () => new InterruptedError(this.interruptCause ?? "user"),
|
|
25581
26263
|
isRetryable: isRecreateRetryableError
|
|
@@ -25594,7 +26276,7 @@ var DeployEngine = class {
|
|
|
25594
26276
|
if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but the create-first attempt collided with the existing resource: ${createMsg}. ${nameOrigin.descriptor}, so the CloudFormation-style safe replacement order (create the new resource before deleting the old) cannot reuse the occupied name — CloudFormation refuses this shape with "cannot update a stack when a custom-named resource requires replacing". ${nameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated).`, "NAMED_REPLACEMENT_COLLISION");
|
|
25595
26277
|
this.logger.info(` Create-first collided with the existing resource's name and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
|
|
25596
26278
|
deletedOldFirst = true;
|
|
25597
|
-
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps,
|
|
26279
|
+
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
|
|
25598
26280
|
}
|
|
25599
26281
|
if (!deletedOldFirst && createResult.physicalId === currentResource.physicalId) {
|
|
25600
26282
|
const idempotentNameOrigin = this.replacementNameOrigin(logicalId, currentResource.physicalId);
|
|
@@ -25602,7 +26284,7 @@ var DeployEngine = class {
|
|
|
25602
26284
|
if (this.options.replace !== true) throw new CdkdError(`${logicalId} (${resourceType}) requires replacement, but its Create API is name-idempotent: the create-first attempt returned the EXISTING resource (${currentResource.physicalId}) instead of creating a new one, so deleting the "old" resource would silently destroy the resource the deploy just reported as created. ${idempotentNameOrigin.descriptor}; ${idempotentNameOrigin.remedy}, or re-run with \`cdkd deploy --replace\` to delete the old resource FIRST and recreate it under the same name (the resource is briefly unavailable while it is recreated). Note: this branch is also reached when the old resource was deleted out-of-band and the physical id is name-derived — there the create was a genuine fresh create; \`--replace\` converges that case too.`, "NAMED_REPLACEMENT_IDEMPOTENT_CREATE");
|
|
25603
26285
|
this.logger.info(` Create-first returned the existing resource (name-idempotent Create API) and --replace is set — deleting old ${logicalId} (${currentResource.physicalId}) first...`);
|
|
25604
26286
|
deletedOldFirst = true;
|
|
25605
|
-
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps,
|
|
26287
|
+
createResult = await this.replaceDeleteFirstAndRecreate(logicalId, resourceType, currentResource, oldDeleteProvider, replaceProvider, replaceProps, updateSecrets, updateReplacePolicy);
|
|
25606
26288
|
}
|
|
25607
26289
|
if (deletedOldFirst) {} else if (updateReplacePolicy === "Retain") this.logger.info(` Retaining old ${logicalId} (${currentResource.physicalId}) - UpdateReplacePolicy: Retain`);
|
|
25608
26290
|
else {
|
|
@@ -25993,12 +26675,74 @@ var DeployEngine = class {
|
|
|
25993
26675
|
return withRetry(operation, logicalId, {
|
|
25994
26676
|
...maxRetries !== void 0 && { maxRetries },
|
|
25995
26677
|
...initialDelayMs !== void 0 && { initialDelayMs },
|
|
25996
|
-
logger: this.
|
|
26678
|
+
logger: this.maskingRetryLogger(logicalId),
|
|
25997
26679
|
isInterrupted: () => this.interrupted,
|
|
25998
26680
|
onInterrupted: () => new InterruptedError(this.interruptCause ?? "user")
|
|
25999
26681
|
});
|
|
26000
26682
|
}
|
|
26001
26683
|
/**
|
|
26684
|
+
* Mask one line of engine-authored text with a resource's OWN recorded
|
|
26685
|
+
* secrets (issue [#2038](https://github.com/go-to-k/cdkd/issues/2038)).
|
|
26686
|
+
*
|
|
26687
|
+
* Per-resource, never session-wide — see the `perResourceSecrets` field doc
|
|
26688
|
+
* for why one resource's secret must not rewrite another's literal. A
|
|
26689
|
+
* `logicalId` with no entry (or an empty bag) forwards verbatim, so every
|
|
26690
|
+
* non-secret resource is byte-identical to before.
|
|
26691
|
+
*/
|
|
26692
|
+
maskForResource(logicalId, text) {
|
|
26693
|
+
return maskSecretsInText(text, this.perResourceSecrets.get(logicalId) ?? EMPTY_SECRETS);
|
|
26694
|
+
}
|
|
26695
|
+
/**
|
|
26696
|
+
* The LAZY `RetryLogger` the engine's generic `withRetry` wrapper threads
|
|
26697
|
+
* (issue [#2038](https://github.com/go-to-k/cdkd/issues/2038) acceptance
|
|
26698
|
+
* item 1) — the same masking shape `drift.ts` and `rollback-executor.ts`
|
|
26699
|
+
* install, bound to the resource's OWN recorded secrets.
|
|
26700
|
+
*
|
|
26701
|
+
* `retry.ts` interpolates the AWS message verbatim into both the per-attempt
|
|
26702
|
+
* `debug` line and the give-up `warn` summary, and the bag this engine hands
|
|
26703
|
+
* a provider is RESOLVED — `perResourceSecrets` is populated immediately after
|
|
26704
|
+
* `resolver.resolve` and BEFORE the create / update call, so a secret is in
|
|
26705
|
+
* scope at every retried provider call. An AWS validation error routinely
|
|
26706
|
+
* quotes the offending value back, so the give-up summary could print it at
|
|
26707
|
+
* DEFAULT verbosity: the same hole #2038 found on the rollback path, one
|
|
26708
|
+
* caller over.
|
|
26709
|
+
*
|
|
26710
|
+
* The bag is resolved PER LINE rather than captured, because `withRetry`
|
|
26711
|
+
* (the private wrapper below) is reached from call sites that hold no bag —
|
|
26712
|
+
* DELETE, the observed-capture drain, the Outputs pass — and a
|
|
26713
|
+
* `logicalId`-keyed read is the only thing available there. The two
|
|
26714
|
+
* `--replace` sites do hold their caller's bag and use
|
|
26715
|
+
* {@link maskingRetryLoggerFor} instead; see the note on
|
|
26716
|
+
* {@link replaceDeleteFirstAndRecreate}'s `secrets` parameter for why binding
|
|
26717
|
+
* the bag beats looking it up whenever the bag is in scope.
|
|
26718
|
+
*
|
|
26719
|
+
* Do NOT restate that the engine "already masked its own error text" — an
|
|
26720
|
+
* earlier revision of this comment did, and it was FALSE: `provisionResource`
|
|
26721
|
+
* logged the raw AWS message at `error` level (a HIGHER level than this
|
|
26722
|
+
* summary) until #2038's review round. Only the EVENT store was masked.
|
|
26723
|
+
*/
|
|
26724
|
+
maskingRetryLogger(logicalId) {
|
|
26725
|
+
return {
|
|
26726
|
+
debug: (msg) => this.logger.debug(this.maskForResource(logicalId, msg)),
|
|
26727
|
+
warn: (msg) => this.logger.warn(this.maskForResource(logicalId, msg))
|
|
26728
|
+
};
|
|
26729
|
+
}
|
|
26730
|
+
/**
|
|
26731
|
+
* The EAGER `RetryLogger`, bound to the bag the caller actually resolved
|
|
26732
|
+
* with (issue [#2038](https://github.com/go-to-k/cdkd/issues/2038)).
|
|
26733
|
+
*
|
|
26734
|
+
* Preferred over {@link maskingRetryLogger} wherever the resolution pass's
|
|
26735
|
+
* own `RecordedSecretValues` is in scope, for the reason
|
|
26736
|
+
* {@link replaceDeleteFirstAndRecreate}'s parameter list already states about
|
|
26737
|
+
* the masker it threads: a map looked up by logical id is a DIFFERENT thing
|
|
26738
|
+
* from the bag this call resolved with, and one file must not argue both
|
|
26739
|
+
* sides. It delegates to the shared `masking-retry-logger.ts` so the deploy
|
|
26740
|
+
* engine, `rollback-executor.ts` and `drift.ts` cannot drift apart.
|
|
26741
|
+
*/
|
|
26742
|
+
maskingRetryLoggerFor(secrets) {
|
|
26743
|
+
return maskingRetryLogger(this.logger, secrets);
|
|
26744
|
+
}
|
|
26745
|
+
/**
|
|
26002
26746
|
* What a failed Output resolution does, shared by both passes of
|
|
26003
26747
|
* {@link resolveOutputs} so they cannot drift — the alias pass reports the
|
|
26004
26748
|
* SAME failure for a name it could not resolve as the value pass does for a
|
|
@@ -26099,5 +26843,5 @@ var DeployEngine = class {
|
|
|
26099
26843
|
};
|
|
26100
26844
|
|
|
26101
26845
|
//#endregion
|
|
26102
|
-
export {
|
|
26103
|
-
//# sourceMappingURL=deploy-engine-
|
|
26846
|
+
export { isTerminationProtectionPropagationError as $, ResourceUpdateNotSupportedError as $n, validateAssetBucketName as $t, renderStatefulReason as A, derivePartitionAndUrlSuffix as An, INTRINSIC_KEYS as At, exportAliasCollisionScrubWarning as B, CdkdError as Bn, stringifyValue as Bt, isFinalSnapshotError as C, CFN_TEMPLATE_URL_LIMIT as Cn, s3BucketArn as Ct, extractDeploymentEventError as D, expectedOwnerParam as Dn, s3BucketWebsiteUrl as Dt, makeCanonicalizePropertiesFn as E, uploadCfnTemplate as En, s3BucketRegionalDomainName as Et, green as F, AwsClients as Fn, LockManager as Ft, collectInlinePolicyNamesManagedBySiblings as G, LocalMigrateError as Gn, rewriteTemplateAssetReferences as Gt, secretBearingStateKeyWarning as H, DependencyError as Hn, buildAssetRedirectMap as Ht, red as I, getAwsClients as In, S3StateBackend as It, findActionableSilentDrops as J, MissingCdkCliError as Jn, AssetModeResolver as Jt, clearOnUpdateRemoval as K, LocalStartServiceError as Kn, escapeRegExp$1 as Kt, yellow as L, resetAwsClients as Ln, rebuildClientForBucketRegion as Lt, bold as M, processStackMessages as Mn, withRetry as Mt, cyan as N, clearBucketRegionCache as Nn, DagBuilder as Nt, MULTI_REGION_RECREATE_BLOCKED_TYPES as O, PARTITION_TABLE as On, applyRoleArnIfSet as Ot, gray as P, resolveBucketRegion as Pn, TemplateParser as Pt, disableInstanceApiTermination as Q, ResourceTimeoutError as Qn, parseBootstrapMarker as Qt, collectDeclaredOutputNames as R, setAwsClients as Rn, shouldRetainResource as Rt, createPreDeleteFinalSnapshot as S, CFN_TEMPLATE_BODY_LIMIT as Sn, scrubResourceRecord as St, unsupportedFinalSnapshotError as T, findLargeInlineResources as Tn, s3BucketDualStackDomainName as Tt, stateKeySecretExposure as U, DeployCancelledError as Un, createAssetRedirectResolver as Ut, isExportAliasCollision as V, ConfigError as Vn, WorkGraph as Vt, IAMRoleProvider as W, LocalInvokeBuildError as Wn, loadPublishableAssetManifest as Wt, CloudControlProvider as X, PartialFailureError as Xn, ensureAssetStorage as Xt, findSilentDropProperties as Y, NestedStackChildDirectDestroyError as Yn, BOOTSTRAP_MARKER_PREFIX as Yt, slowCcOperationTimeoutMs as Z, ProvisioningError as Zn, getBootstrapMarkerKey as Zt, computeImplicitDeleteEdges as _, resolveStateBucketWithDefault as _n, STATE_SOURCED_READBACK_RULES as _t, DeploymentEventsStore as a, runDockerForeground as an, isCdkdError as ar, normalizeAwsTagsToCfn as at, buildFinalSnapshotIdentifier as b, stateBucketExistenceConfirmed as bn, maskSecretsInText as bt, replayFailedOperations as c, getDockerImageBySourceHash as cn, isMarkedNonRetryable as cr, coerceCfnBoolean as ct, updatePartialReason as d, getDefaultStateBucketName as dn, markNonRetryable as dr, readConfigString as dt, validateContainerRepoName as en, StackHasActiveImportsError as er, IntrinsicFunctionResolver as et, UNSPECIFIED_SKIP_REASON as f, getLegacyStateBucketName as fn, __exportAll as fr, replayWarn as ft, IMPLICIT_DELETE_DEPENDENCIES as g, resolveSkipPrefix as gn, STATE_SOURCED_CROSS_GENERATION_RULES as gt, maskingRetryLogger as h, resolveCaptureObservedState as hn, requireConfigString as ht, DeploymentEventsReader as i, getDockerCmd as in, formatError as ir, WAFv2WebACLProvider as it, formatResourceLine as j, AssemblyReader as jn, describeTypeWithThrottleRetry as jt, isStatefulRecreateTargetSync as k, canonicalizeRegion as kn, DiffCalculator as kt, replayRollback as l, Synthesizer as ln, isRetryableTransientError as lr, configBooleanRefusal as lt, withResourceDeadline as m, resolveAutoAssetStorage as mn, requireConfigObject as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, buildDockerImage as nn, StateError as nr, getAccountInfo as nt, planFailedOps as o, runDockerStreaming as on, normalizeAwsError as or, resolveExplicitPhysicalId as ot, deleteSkipReason as p, resolveApp as pn, requireConfigArray as pt, ProviderRegistry as q, LockError as qn, stripControlChars as qt, DeployEngine as r, formatDockerLoginError as rn, SynthesisError as rr, refStateLookupFromResource as rt, planRollback as s, AssetManifestLoader as sn, withErrorHandling as sr, assertRegionMatch as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDenyExternalAccessPolicy as tn, StackTerminationProtectionError as tr, cfnRefValueFromPhysicalId as tt, updatePartialMessage as u, synthesisStatusMessage as un, isThrottlingError as ur, configStringRefusal as ut, ATOMIC_FINAL_SNAPSHOT_TYPES as v, resolveStateBucketWithDefaultAndSource as vn, TEMPLATE_SOURCED_RULES as vt, refusesFinalSnapshot as w, MIGRATE_TMP_PREFIX as wn, s3BucketDomainName as wt, ccRoutedFinalSnapshotError as x, warnDeprecatedNoPrefixCliFlag as xn, redactSecretsForState as xt, PRE_DELETE_SNAPSHOT_TYPES as y, resolveUseCdkBootstrapAssets as yn, createSecretMasker as yt, collectPublishedOutputNames as z, AssetError as zn, AssetPublisher as zt };
|
|
26847
|
+
//# sourceMappingURL=deploy-engine-nwoIJsLn.js.map
|