Skip to content

feat: surface unrecoverable container errors during pod wait#15

Open
flysky22222 wants to merge 1 commit into
mainfrom
feature/surface-container-errors-v2
Open

feat: surface unrecoverable container errors during pod wait#15
flysky22222 wants to merge 1 commit into
mainfrom
feature/surface-container-errors-v2

Conversation

@flysky22222

Copy link
Copy Markdown

背景

K8s 模式下,当 CI job 引用了不存在的镜像、或没有权限拉取的镜像时,Pod 会卡在 Pending。此前 waitForPodPhases 只会一直 backoff 直到超时,最终只报一句笼统的 phase 错误,GitHub Actions 用户看不到真正的失败原因。

改动

  • 在轮询 Pod 状态时,检测 init 容器和普通容器的不可恢复 waiting 原因(ImagePullBackOffErrImagePullInvalidImageNameCreateContainerConfigErrorCreateContainerError)。
  • 命中后立即快速失败,并带上「容器名 + 原因 + K8s 原始 message」,让具体错误透传到 Actions 日志。
  • getPodPhase 重构为 readPod + parsePodPhase,以便复用 Pod 对象做容器检查。
  • 新增 describePodFailure 聚合 Pod phase、conditions、container statuses 和 Warning events 为诊断信息。
  • 新增 describePodWarningEvents 读取 K8s Warning 事件(可选 events 权限,缺失时优雅降级)。
  • 新增 getUnrecoverableWaitingReasons 支持通过环境变量扩展不可恢复原因列表。
  • waitForPodPhases 三条失败路径均附带完整诊断信息。
  • README 文档更新:可选 events 权限 + 新环境变量说明。

测试

  • 新增 19 个单元测试覆盖 parsePodPhasegetContainerErrorswaitForPodPhasesgetUnrecoverableWaitingReasonsdescribePodFailuredescribePodWarningEvents 及边界情况。
  • npm run build --prefix packages/k8s 通过
  • npm run test --prefix packages/k8s 中新增测试全部通过
  • prettier@2.6.2 格式检查通过

关联 Issue: https://github.com/opensourceways/backlog/issues/1102

When a CI job references a non-existent image or one it lacks permission to
pull, the pod stays in Pending and waitForPodPhases previously timed out with
only a generic phase-status message. GitHub Actions users had no indication
of the real cause.

Detect unrecoverable container waiting reasons (ImagePullBackOff, ErrImagePull,
InvalidImageName, CreateContainerConfigError, CreateContainerError) on both
init and regular containers, and fail fast with the container name, reason, and
Kubernetes message so the error is visible in the Actions log.

Refactor getPodPhase into readPod + parsePodPhase so the pod object can be
inspected for container errors, and add describePodFailure to aggregate pod
phase, conditions, container statuses and Warning events into a single
diagnostic string. describePodWarningEvents provides best-effort retrieval of
recent Warning events (requires optional events RBAC permission, degrades
gracefully when missing). getUnrecoverableWaitingReasons allows operators to
extend the built-in fast-fail whitelist via the
ACTIONS_RUNNER_K8S_UNRECOVERABLE_WAITING_REASONS env var.

All three failure paths in waitForPodPhases now attach full diagnostics:
1. Non-backoff phase (e.g. Failed) — includes pod details + events
2. Unrecoverable container error (e.g. ImagePullBackOff) — fail-fast
3. Timeout — includes pod details so the user can see WHY the pod never
   became ready

README: document the optional events permission and the new env var.

Tests: 19 new test cases covering parsePodPhase, getContainerErrors,
waitForPodPhases, getUnrecoverableWaitingReasons, describePodFailure,
describePodWarningEvents, and edge cases such as forbidden events API and
unreadable pods.
@opensourceways-bot

Copy link
Copy Markdown

Welcome To opensourceways Community

Hey @flysky22222 , thanks for your contribution to the community.

Bot Usage Manual

I'm the Bot here serving you. You can find the instructions on how to interact with me at Here . That means you can comment below every pull request or issue to trigger Bot Commands.

@opensourceways-bot

Copy link
Copy Markdown

CLA Signature Pass

flysky22222, thanks for your pull request. All authors of the commits have signed the CLA. 👍

@opensourceways-bot

Copy link
Copy Markdown

Linking Issue Notice

@flysky22222 , the pull request must be linked to at least one issue.
If an issue has already been linked, but the needs-issue label remains, you can remove the label by commenting /check-issue .

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces enhanced diagnostic logging and unrecoverable container error detection when waiting for Kubernetes pods to come online, including documentation updates in the README. The feedback focuses on optimizing the polling loop in waitForPodPhases by caching the unrecoverable waiting reasons environment variable and passing the already-fetched pod object to describePodFailure and getContainerErrors to avoid redundant API calls and parsing overhead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +396 to +398
export function getContainerErrors(pod: k8s.V1Pod): string[] {
const errors: string[] = []
const unrecoverableReasons = getUnrecoverableWaitingReasons()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling getUnrecoverableWaitingReasons() inside getContainerErrors parses the environment variable ACTIONS_RUNNER_K8S_UNRECOVERABLE_WAITING_REASONS on every single polling iteration. Since environment variables do not change during the execution of waitForPodPhases, we can optimize this by parsing the reasons once before the loop and passing them as an optional parameter to getContainerErrors.

export function getContainerErrors(
  pod: k8s.V1Pod,
  unrecoverableReasons = getUnrecoverableWaitingReasons()
): string[] {
  const errors: string[] = []

Comment on lines +416 to +424
export async function describePodFailure(podName: string): Promise<string> {
let pod: k8s.V1Pod
try {
pod = await readPod(podName)
} catch (err) {
return `Could not read pod ${podName} for diagnostics: ${
err instanceof Error ? err.message : String(err)
}`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The describePodFailure function always fetches the pod status from the Kubernetes API server using readPod(podName). However, in waitForPodPhases, we already have the fetched pod object. We can optimize this by allowing describePodFailure to accept an optional pod parameter, avoiding redundant API calls when the pod object is already available.

export async function describePodFailure(
  podName: string,
  providedPod?: k8s.V1Pod
): Promise<string> {
  let pod = providedPod
  if (!pod) {
    try {
      pod = await readPod(podName)
    } catch (err) {
      return `Could not read pod ${podName} for diagnostics: ${
        err instanceof Error ? err.message : String(err)
      }`
    }
  }

Comment on lines 534 to 569
): Promise<void> {
const backOffManager = new BackOffManager(maxTimeSeconds)
let phase: PodPhase = PodPhase.UNKNOWN
try {
while (true) {
phase = await getPodPhase(podName)
if (awaitingPhases.has(phase)) {
return
}
while (true) {
const pod = await readPod(podName)
phase = parsePodPhase(pod)
if (awaitingPhases.has(phase)) {
return
}

if (!backOffPhases.has(phase)) {
throw new Error(
`Pod ${podName} is unhealthy with phase status ${phase}`
)
}
if (!backOffPhases.has(phase)) {
const details = await describePodFailure(podName)
throw new Error(
`Pod ${podName} is unhealthy with phase status ${phase}\n${details}`
)
}

const containerErrors = getContainerErrors(pod)
if (containerErrors.length > 0) {
const details = await describePodFailure(podName)
throw new Error(
`Pod ${podName} has unrecoverable container errors: ${containerErrors.join(
'; '
)}\n${details}`
)
}

try {
await backOffManager.backOff()
} catch (error) {
const details = await describePodFailure(podName)
throw new Error(
`Pod ${podName} is unhealthy: timed out after ${maxTimeSeconds}s in phase ${phase}\n${details}`
)
}
} catch (error) {
throw new Error(`Pod ${podName} is unhealthy with phase status ${phase}`)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Optimize waitForPodPhases by fetching the unrecoverable waiting reasons once before entering the polling loop, and passing the already-fetched pod object to describePodFailure and getContainerErrors to avoid redundant environment variable parsing and Kubernetes API calls.

): Promise<void> {
  const backOffManager = new BackOffManager(maxTimeSeconds)
  let phase: PodPhase = PodPhase.UNKNOWN
  const unrecoverableReasons = getUnrecoverableWaitingReasons()
  while (true) {
    const pod = await readPod(podName)
    phase = parsePodPhase(pod)
    if (awaitingPhases.has(phase)) {
      return
    }

    if (!backOffPhases.has(phase)) {
      const details = await describePodFailure(podName, pod)
      throw new Error(
        `Pod ${podName} is unhealthy with phase status ${phase}\n${details}`
      )
    }

    const containerErrors = getContainerErrors(pod, unrecoverableReasons)
    if (containerErrors.length > 0) {
      const details = await describePodFailure(podName, pod)
      throw new Error(
        `Pod ${podName} has unrecoverable container errors: ${containerErrors.join(
          '; '
        )}\n${details}`
      )
    }

    try {
      await backOffManager.backOff()
    } catch (error) {
      const details = await describePodFailure(podName, pod)
      throw new Error(
        `Pod ${podName} is unhealthy: timed out after ${maxTimeSeconds}s in phase ${phase}\n${details}`
      )
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants