@go-to-k/cdkd 0.280.38 → 0.280.39

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.
@@ -12872,7 +12872,7 @@ var CloudControlProvider = class {
12872
12872
  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);
12873
12873
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
12874
12874
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
12875
- const { ASGProvider } = await import("./asg-provider-CVra7Nuk.js").then((n) => n.n);
12875
+ const { ASGProvider } = await import("./asg-provider-DPYPr-Op.js").then((n) => n.n);
12876
12876
  await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
12877
12877
  return;
12878
12878
  }
@@ -13522,6 +13522,25 @@ function parseLambdaPayload(payloadBytes) {
13522
13522
  return parsed;
13523
13523
  }
13524
13524
  /**
13525
+ * Decode the base64 `LogResult` a `LogType: 'Tail'` invoke returns into the
13526
+ * backing function's log tail (issue #1674).
13527
+ *
13528
+ * Best-effort by design: this only ever feeds diagnostics and the retry
13529
+ * classifier, so nothing here may fail a deploy that is otherwise fine. Node's
13530
+ * base64 decoder is LENIENT — it drops invalid characters rather than throwing —
13531
+ * so a malformed value decodes to garbage, which simply matches no signal; the
13532
+ * `catch` is belt-and-braces for that contract changing, not a live path.
13533
+ */
13534
+ function decodeInvokeLogTail(logResult) {
13535
+ if (!logResult) return void 0;
13536
+ try {
13537
+ const decoded = Buffer.from(logResult, "base64").toString("utf8");
13538
+ return decoded.length > 0 ? decoded : void 0;
13539
+ } catch {
13540
+ return;
13541
+ }
13542
+ }
13543
+ /**
13525
13544
  * IAM-authorization-propagation signals in a custom resource FAILED reason that
13526
13545
  * indicate the backing Lambda's freshly-attached execution-role policy has not
13527
13546
  * yet taken effect for its assumed-role session (so a recycle + retry will
@@ -13538,6 +13557,67 @@ const CR_TRANSIENT_AUTHZ_SIGNALS = [
13538
13557
  "is unable to assume"
13539
13558
  ];
13540
13559
  /**
13560
+ * The same IAM-authorization-propagation signals, matched against the backing
13561
+ * function's INVOCATION LOG TAIL rather than the FAILED reason (issue #1674).
13562
+ *
13563
+ * Why a second set is needed at all: the reason string is written by the
13564
+ * handler, and a handler that wraps an SDK / CLI failure in its own message —
13565
+ * normal handler hygiene — erases every authz phrase before cdkd ever sees it.
13566
+ * CDK's `BucketDeployment` is the widely-used instance: `aws_command()` lets
13567
+ * `subprocess.check_call` raise, and `str(CalledProcessError)` is only
13568
+ * `Command '[...]' returned non-zero exit status 1.`, so the 403 on the asset
13569
+ * object reaches cdkd with no authz wording at all and the retry above never
13570
+ * fires — on a resource GUARANTEED to race, since CDK generates the handler
13571
+ * role, its inline policy and the custom resource in the same stack.
13572
+ *
13573
+ * The 403 does survive in the function's own log, which a `LogType: 'Tail'`
13574
+ * invoke returns inline (no CloudWatch Logs dependency, no extra API call, no
13575
+ * additional IAM permission — see `invokeLambda`). So this set adds the
13576
+ * CLI / SDK-level spellings of the SAME denial that the handler swallowed:
13577
+ * botocore's `An error occurred (403) when calling the HeadObject operation`
13578
+ * and the `AccessDenied` / `AccessDeniedException` error codes.
13579
+ *
13580
+ * Deliberately still NARROW, for the reason `isTransientAuthzFailure`
13581
+ * documents: a bare `403` / `forbidden` would match a handler legitimately
13582
+ * logging an unrelated downstream denial, and generic transient errors
13583
+ * (throttling / timeouts) must not trigger a CR re-invoke at all.
13584
+ *
13585
+ * ACCEPTED COST, stated because a log surface is noisier than a
13586
+ * handler-authored reason: a handler that merely LOGS a genuine, permanent
13587
+ * `AccessDenied` — one it caught, or one no propagation will fix — now buys
13588
+ * `transientAuthzMaxRetries` extra invokes plus a
13589
+ * `recycleBackingFunctionExecEnv` each. Bounded (2 by default; set
13590
+ * `CDKD_CR_AUTHZ_MAX_RETRIES=0` to disable the RETRY — the log-tail scan and the
13591
+ * reason annotation below still run, since they only describe the failure), and
13592
+ * the original failure still surfaces afterwards, so the failure is DELAYED,
13593
+ * never masked.
13594
+ *
13595
+ * The cost is NOT purely wasted time, and saying only "delayed, never masked"
13596
+ * would undersell it: a re-invoke re-runs the handler's `Create`, so a handler
13597
+ * that is not idempotent and had already done partial work repeats that work —
13598
+ * and under the CDK Provider framework the re-invoked `onEvent` can create a
13599
+ * SECOND physical resource, orphaning the first. That exposure is not new (the
13600
+ * pre-existing reason-string match has always been able to retry), but the log
13601
+ * signal widens what reaches it. The trade is still deliberate: the alternative
13602
+ * is the pre-#1674 behavior, where the propagation race this whole mechanism
13603
+ * exists for fails the deploy on its first attempt, every time.
13604
+ */
13605
+ const CR_TRANSIENT_AUTHZ_LOG_SIGNALS = [
13606
+ ...CR_TRANSIENT_AUTHZ_SIGNALS,
13607
+ "an error occurred (403)",
13608
+ "accessdenied",
13609
+ "access denied"
13610
+ ];
13611
+ /**
13612
+ * Cap for the backing function's log tail when it is surfaced in an EPHEMERAL
13613
+ * warning (the `FunctionError` arm). Deliberately far larger than
13614
+ * `truncateReason`'s 200-char default — a crashed handler's real cause is often
13615
+ * several lines above the last one (a Python traceback) — but not unbounded:
13616
+ * this lands in CI logs and terminal scrollback. Lambda caps the tail at 4 KB
13617
+ * regardless, so this only trims the extreme case.
13618
+ */
13619
+ const CR_LOG_TAIL_WARN_MAX_CHARS = 2e3;
13620
+ /**
13541
13621
  * Custom Resource Provider
13542
13622
  *
13543
13623
  * Implements Lambda-backed custom resources by invoking the Lambda function
@@ -13634,7 +13714,9 @@ var CustomResourceProvider = class CustomResourceProvider {
13634
13714
  * `disableOuterRetry` to avoid stranding a pre-signed response URL — so we
13635
13715
  * retry HERE instead, deriving a fresh response URL + RequestId per attempt
13636
13716
  * and recycling the backing function's execution environment between tries).
13637
- * Override via `CDKD_CR_AUTHZ_MAX_RETRIES` (0 disables).
13717
+ * Override via `CDKD_CR_AUTHZ_MAX_RETRIES`. `0` disables the RETRY only —
13718
+ * the issue-#1674 log-tail scan and the reason annotation it produces still
13719
+ * run, because they describe the failure rather than react to it.
13638
13720
  */
13639
13721
  transientAuthzMaxRetries = (() => {
13640
13722
  const raw = process.env["CDKD_CR_AUTHZ_MAX_RETRIES"];
@@ -13900,12 +13982,18 @@ var CustomResourceProvider = class CustomResourceProvider {
13900
13982
  const invocation = await this.prepareInvocation();
13901
13983
  const request = buildRequest(invocation);
13902
13984
  this.logger.debug(`Sending custom resource ${operation.toLowerCase()} request: ${serviceToken}`);
13903
- const cfnResponse = await this.sendRequest(serviceToken, request, invocation.responseKey, logicalId, operation);
13904
- if (cfnResponse.Status === "FAILED" && attempt < this.transientAuthzMaxRetries && this.isTransientAuthzFailure(cfnResponse.Reason)) {
13905
- this.logger.warn(`Custom resource ${operation} for ${logicalId} returned a transient IAM-authorization FAILED (attempt ${attempt + 1}/${this.transientAuthzMaxRetries + 1}): ${this.truncateReason(cfnResponse.Reason)}. Recycling the backing function's execution environment and retrying so its next cold start picks up the propagated policy.`);
13985
+ const { response: cfnResponse, logResult } = await this.sendRequest(serviceToken, request, invocation.responseKey, logicalId, operation);
13986
+ const reasonIsAuthz = cfnResponse.Status === "FAILED" && this.isTransientAuthzFailure(cfnResponse.Reason);
13987
+ const logAuthzMatch = cfnResponse.Status === "FAILED" && !reasonIsAuthz ? this.findTransientAuthzLogLine(decodeInvokeLogTail(logResult)) : void 0;
13988
+ if (cfnResponse.Status === "FAILED" && attempt < this.transientAuthzMaxRetries && (reasonIsAuthz || logAuthzMatch !== void 0)) {
13989
+ 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.`);
13906
13990
  await this.recycleBackingFunctionExecEnv(serviceToken, logicalId);
13907
13991
  continue;
13908
13992
  }
13993
+ if (cfnResponse.Status === "FAILED" && logAuthzMatch !== void 0) return {
13994
+ ...cfnResponse,
13995
+ 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]`
13996
+ };
13909
13997
  return cfnResponse;
13910
13998
  }
13911
13999
  }
@@ -13927,6 +14015,45 @@ var CustomResourceProvider = class CustomResourceProvider {
13927
14015
  const lower = reason.toLowerCase();
13928
14016
  return CR_TRANSIENT_AUTHZ_SIGNALS.some((p) => lower.includes(p));
13929
14017
  }
14018
+ /**
14019
+ * Find the IAM-authorization denial inside a backing function's invocation
14020
+ * log tail, for the case the FAILED reason itself carries none (issue #1674).
14021
+ *
14022
+ * Returns the FIRST matching log LINE rather than a boolean, so the caller can
14023
+ * put the denial the handler swallowed into its message — today the user has
14024
+ * to open CloudWatch to discover that `returned non-zero exit status 1` was a
14025
+ * 403 on the asset object. The line is returned verbatim (the caller
14026
+ * truncates); only the MATCH is case-insensitive.
14027
+ *
14028
+ * Absent for an SNS-backed custom resource — no Lambda invoke, so no tail.
14029
+ *
14030
+ * **Known bound.** The tail always belongs to the DISPATCH invoke. For a
14031
+ * handler that does its work inline and PUTs the cfn-response itself (CDK's
14032
+ * `BucketDeployment` — the case this exists for), that IS where the failure
14033
+ * happened. For the CDK Provider framework's genuinely async pattern it is
14034
+ * NOT: the failure happens later, in a Step-Functions-driven handler, so a
14035
+ * stray denial logged by the `onEvent` wrapper can buy a bounded extra retry
14036
+ * with a message pointing at the wrong execution. That path is otherwise
14037
+ * covered by the reason string, which the framework populates from the
14038
+ * underlying error, so the cost is redundancy rather than a wrong answer.
14039
+ *
14040
+ * Suppressing the tail for the async pattern is NOT the fix, and the reason
14041
+ * is worth recording: `isAsyncPattern` in `getCustomResourceResponse` means
14042
+ * only "the invoke returned no direct payload", which is equally true of
14043
+ * `BucketDeployment` — its Python handler returns `None`. Gating on it would
14044
+ * switch off exactly the case this feature exists for.
14045
+ */
14046
+ findTransientAuthzLogLine(logTail) {
14047
+ if (!logTail) return void 0;
14048
+ for (const line of logTail.split("\n")) {
14049
+ const lower = line.toLowerCase();
14050
+ const signal = CR_TRANSIENT_AUTHZ_LOG_SIGNALS.find((p) => lower.includes(p));
14051
+ if (signal !== void 0) return {
14052
+ line: line.trim(),
14053
+ signal
14054
+ };
14055
+ }
14056
+ }
13930
14057
  /** Truncate a CR FAILED reason for log readability. */
13931
14058
  truncateReason(reason, max = 200) {
13932
14059
  const r = reason ?? "Unknown reason";
@@ -13968,16 +14095,25 @@ var CustomResourceProvider = class CustomResourceProvider {
13968
14095
  * Send custom resource request via the appropriate service (Lambda or SNS)
13969
14096
  * For Lambda: invokes synchronously and returns the response
13970
14097
  * For SNS: publishes to topic and polls S3 for response
14098
+ *
14099
+ * Also returns the backing function's raw invocation `LogResult` when there
14100
+ * is one, so the caller can recover an authorization denial the handler
14101
+ * erased from the FAILED reason (issue #1674). Returned UNDECODED so the
14102
+ * happy path pays nothing — only a FAILED whose reason missed decodes it.
14103
+ * Absent on the SNS path: there is no Lambda invoke to attach a log to.
13971
14104
  */
13972
14105
  async sendRequest(serviceToken, request, responseKey, logicalId, operation) {
13973
14106
  if (this.isSnsServiceToken(serviceToken)) {
13974
14107
  this.logger.debug(`ServiceToken is SNS topic, publishing to: ${serviceToken}`);
13975
14108
  await this.publishToSns(serviceToken, request);
13976
- return await this.pollS3Response(responseKey, logicalId, operation);
14109
+ return { response: await this.pollS3Response(responseKey, logicalId, operation) };
13977
14110
  }
13978
14111
  await this.waitForBackingLambdaReady(serviceToken, logicalId);
13979
- const response = await this.invokeLambda(serviceToken, request);
13980
- return await this.getCustomResourceResponse(response, responseKey, logicalId, operation);
14112
+ const invokeResponse = await this.invokeLambda(serviceToken, request);
14113
+ return {
14114
+ response: await this.getCustomResourceResponse(invokeResponse, responseKey, logicalId, operation),
14115
+ ...invokeResponse.LogResult === void 0 ? {} : { logResult: invokeResponse.LogResult }
14116
+ };
13981
14117
  }
13982
14118
  /**
13983
14119
  * Block until the backing Lambda function for a Custom Resource is in a
@@ -14036,11 +14172,21 @@ var CustomResourceProvider = class CustomResourceProvider {
14036
14172
  }
14037
14173
  /**
14038
14174
  * Invoke Lambda function synchronously
14175
+ *
14176
+ * `LogType: 'Tail'` makes Lambda return the last 4 KB of THIS invocation's
14177
+ * log, base64-encoded, in `LogResult` (issue #1674). It is only valid with
14178
+ * `RequestResponse`, which is what this path always uses. This is deliberately
14179
+ * preferred over reading the function's CloudWatch log group after the fact:
14180
+ * it needs no CloudWatch Logs client, no `logs:GetLogEvents` on cdkd's own
14181
+ * credentials, and no extra API call — the tail rides the invoke response
14182
+ * cdkd already waits for, and is scoped to the exact invocation that failed
14183
+ * rather than to whatever happens to be latest in the log stream.
14039
14184
  */
14040
14185
  async invokeLambda(serviceToken, request) {
14041
14186
  return await this.lambdaClient.send(new InvokeCommand({
14042
14187
  FunctionName: serviceToken,
14043
14188
  InvocationType: "RequestResponse",
14189
+ LogType: "Tail",
14044
14190
  Payload: Buffer.from(JSON.stringify(request))
14045
14191
  }));
14046
14192
  }
@@ -14055,6 +14201,8 @@ var CustomResourceProvider = class CustomResourceProvider {
14055
14201
  async getCustomResourceResponse(lambdaResponse, responseKey, logicalId, operation) {
14056
14202
  if (lambdaResponse.FunctionError) {
14057
14203
  const errorPayload = lambdaResponse.Payload ? Buffer.from(lambdaResponse.Payload).toString() : "Unknown";
14204
+ const logTail = decodeInvokeLogTail(lambdaResponse.LogResult);
14205
+ if (logTail !== void 0) this.logger.warn(`Backing function log tail for ${logicalId} (${operation}):\n` + this.truncateReason(logTail, CR_LOG_TAIL_WARN_MAX_CHARS));
14058
14206
  throw new Error(`Lambda function error (${lambdaResponse.FunctionError}): ${errorPayload}`);
14059
14207
  }
14060
14208
  let hasDirectPayload = false;
@@ -19541,7 +19689,7 @@ const FLUSH_INTERVAL_MS = 2e3;
19541
19689
  const FLUSH_EVENT_THRESHOLD = 50;
19542
19690
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
19543
19691
  function getCdkdVersion() {
19544
- return "0.280.38";
19692
+ return "0.280.39";
19545
19693
  }
19546
19694
  /**
19547
19695
  * Generate a time-sortable unique run id, e.g.
@@ -21707,4 +21855,4 @@ var DeployEngine = class {
21707
21855
 
21708
21856
  //#endregion
21709
21857
  export { requireConfigObject as $, AssemblyReader as $t, green as A, __exportAll as An, runDockerForeground as At, disableInstanceApiTermination as B, resolveCaptureObservedState as Bt, MULTI_REGION_RECREATE_BLOCKED_TYPES as C, StackTerminationProtectionError as Cn, getBootstrapMarkerKey as Ct, bold as D, isCdkdError as Dn, buildDockerImage as Dt, formatResourceLine as E, formatError as En, validateContainerRepoName as Et, clearOnUpdateRemoval as F, synthesisStatusMessage as Ft, WAFv2WebACLProvider as G, stateBucketExistenceConfirmed as Gt, IntrinsicFunctionResolver as H, resolveStateBucketWithDefault as Ht, ProviderRegistry as I, getDefaultStateBucketName as It, assertRegionMatch as J, CFN_TEMPLATE_URL_LIMIT as Jt, normalizeAwsTagsToCfn as K, warnDeprecatedNoPrefixCliFlag as Kt, findActionableSilentDrops as L, getLegacyStateBucketName as Lt, yellow as M, AssetManifestLoader as Mt, IAMRoleProvider as N, getDockerImageBySourceHash as Nt, cyan as O, normalizeAwsError as On, formatDockerLoginError as Ot, collectInlinePolicyNamesManagedBySiblings as P, Synthesizer as Pt, requireConfigArray as Q, expectedOwnerParam as Qt, CloudControlProvider as R, resolveApp as Rt, extractDeploymentEventError as S, StackHasActiveImportsError as Sn, ensureAssetStorage as St, renderStatefulReason as T, SynthesisError as Tn, validateAssetBucketName as Tt, cfnRefValueFromPhysicalId as U, resolveStateBucketWithDefaultAndSource as Ut, isTerminationProtectionPropagationError as V, resolveSkipPrefix as Vt, refStateLookupFromResource as W, resolveUseCdkBootstrapAssets as Wt, readConfigString as X, findLargeInlineResources as Xt, configStringRefusal as Y, MIGRATE_TMP_PREFIX as Yt, replayWarn as Z, uploadCfnTemplate as Zt, createPreDeleteFinalSnapshot as _, NestedStackChildDirectDestroyError as _n, createAssetRedirectResolver as _t, DeploymentEventsStore as a, resetAwsClients as an, isRetryableTransientError as at, unsupportedFinalSnapshotError as b, ResourceTimeoutError as bn, AssetModeResolver as bt, replayFailedOperations as c, CdkdError as cn, TemplateParser as ct, IMPLICIT_DELETE_DEPENDENCIES as d, DeployCancelledError as dn, rebuildClientForBucketRegion as dt, processStackMessages as en, requireConfigString as et, computeImplicitDeleteEdges as f, LocalInvokeBuildError as fn, shouldRetainResource as ft, ccRoutedFinalSnapshotError as g, MissingCdkCliError as gn, buildAssetRedirectMap as gt, buildFinalSnapshotIdentifier as h, LockError as hn, WorkGraph as ht, DeploymentEventsReader as i, getAwsClients as in, withRetry as it, red as j, runDockerStreaming as jt, gray as k, withErrorHandling as kn, getDockerCmd as kt, replayRollback as l, ConfigError as ln, LockManager as lt, PRE_DELETE_SNAPSHOT_TYPES as m, LocalStartServiceError as mn, stringifyValue as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, resolveBucketRegion as nn, DiffCalculator as nt, planFailedOps as o, setAwsClients as on, isThrottlingError as ot, ATOMIC_FINAL_SNAPSHOT_TYPES as p, LocalMigrateError as pn, AssetPublisher as pt, resolveExplicitPhysicalId as q, CFN_TEMPLATE_BODY_LIMIT as qt, DeployEngine as r, AwsClients as rn, describeTypeWithThrottleRetry as rt, planRollback as s, AssetError as sn, DagBuilder as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, clearBucketRegionCache as tn, applyRoleArnIfSet as tt, withResourceDeadline as u, DependencyError as un, S3StateBackend as ut, isFinalSnapshotError as v, PartialFailureError as vn, loadPublishableAssetManifest as vt, isStatefulRecreateTargetSync as w, StateError as wn, parseBootstrapMarker as wt, makeCanonicalizePropertiesFn as x, ResourceUpdateNotSupportedError as xn, BOOTSTRAP_MARKER_PREFIX as xt, refusesFinalSnapshot as y, ProvisioningError as yn, rewriteTemplateAssetReferences as yt, slowCcOperationTimeoutMs as z, resolveAutoAssetStorage as zt };
21710
- //# sourceMappingURL=deploy-engine-B3a5MhON.js.map
21858
+ //# sourceMappingURL=deploy-engine-BXVYshtP.js.map