Files
kenaito-kopsc/internal/diagnose/k8s.go
T
odboy e02f3010cf feat: 扩充 Docker 命令并新增 diagnose 状态诊断
- Docker:新增只读、生命周期、清理、Compose 共 32 条命令,Linux 自动适配 podman
- 危险命令 clearall/prunevolumes 未带 --yes 时仅提示并以退出码 2 结束
- 需要容器/镜像名的命令做必填参数校验,-- 之后参数计入位置参数
- diagnose:自动识别 Docker/Podman 容器或 K8s Pod,输出状态、根因、依据、建议并附日志与事件
- diagnose 中 -n 表示 namespace,其余命令 -n 仍为 --dry-run
- 新增内置动作桥接与平台变体 helper,补充单元/集成/CLI 测试与 README
2026-10-31 14:09:38 +08:00

373 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package diagnose
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
)
type pod struct {
Metadata struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
} `json:"metadata"`
Status struct {
Phase string `json:"phase"`
Reason string `json:"reason"`
Message string `json:"message"`
Conditions []podCondition `json:"conditions"`
ContainerStatuses []containerStatus `json:"containerStatuses"`
} `json:"status"`
}
type podCondition struct {
Type string `json:"type"`
Status string `json:"status"`
Reason string `json:"reason"`
Message string `json:"message"`
}
type containerStatus struct {
Name string `json:"name"`
Ready bool `json:"ready"`
RestartCount int `json:"restartCount"`
State podState `json:"state"`
LastState podState `json:"lastState"`
}
type podState struct {
Waiting *podWaiting `json:"waiting"`
Running *podRunning `json:"running"`
Terminated *podTerminated `json:"terminated"`
}
type podWaiting struct {
Reason string `json:"reason"`
Message string `json:"message"`
}
type podRunning struct {
StartedAt string `json:"startedAt"`
}
type podTerminated struct {
Reason string `json:"reason"`
ExitCode int `json:"exitCode"`
Message string `json:"message"`
StartedAt string `json:"startedAt"`
FinishedAt string `json:"finishedAt"`
}
func (s *Service) diagnoseK8s(ctx context.Context, src source, target string, opts Options) error {
if _, err := s.runner.LookPath("kubectl"); err != nil {
return fmt.Errorf("未找到 kubectl,请先安装并配置 kubeconfig")
}
namespace := src.namespace
if namespace == "" {
namespace, _ = s.kubeNamespace(ctx, src.context)
}
res, err := s.run(ctx, "kubectl", kubectlArgs(src.context, "get", "pod", target, "-n", namespace, "-o", "json")...)
if err != nil {
return err
}
var p pod
if err := json.Unmarshal(res.Stdout, &p); err != nil {
return fmt.Errorf("解析 kubectl get pod 输出失败: %w", err)
}
name := p.Metadata.Name
if name == "" {
name = target
}
if p.Metadata.Namespace != "" {
namespace = p.Metadata.Namespace
}
contextLabel := src.context
if contextLabel == "" {
contextLabel = "当前上下文"
}
ready, total, restarts := 0, len(p.Status.ContainerStatuses), 0
for _, cs := range p.Status.ContainerStatuses {
if cs.Ready {
ready++
}
restarts += cs.RestartCount
}
fmt.Fprintf(s.out, "目标: %s\n", name)
fmt.Fprintf(s.out, "来源: Kubernetes context=%s namespace=%s\n", contextLabel, namespace)
fmt.Fprintf(s.out, "状态: phase=%s ready=%d/%d restarts=%d\n", p.Status.Phase, ready, total, restarts)
s.renderFinding("", judgePod(p))
selected := p.Status.ContainerStatuses
if opts.Container != "" {
selected = nil
for _, cs := range p.Status.ContainerStatuses {
if cs.Name == opts.Container {
selected = append(selected, cs)
}
}
if len(selected) == 0 {
fmt.Fprintf(s.errOut, "警告: Pod %s 中没有名为 %q 的容器\n", name, opts.Container)
}
}
for _, cs := range selected {
fmt.Fprintf(s.out, "\n容器 %s:\n", cs.Name)
fmt.Fprintf(s.out, " 状态: %s ready=%t restarts=%d\n", containerState(cs), cs.Ready, cs.RestartCount)
s.renderFinding(" ", judgeContainer(cs))
if !opts.NoLogs {
s.writeContainerLogs(ctx, src, name, namespace, cs, opts)
}
}
if !opts.NoEvents {
eventRes, err := s.run(ctx, "kubectl", kubectlArgs(src.context,
"get", "events", "-n", namespace,
"--field-selector", "involvedObject.name="+name,
"--sort-by=.lastTimestamp")...)
if err != nil {
fmt.Fprintf(s.errOut, "警告: 获取事件失败: %v\n", err)
} else {
events := tailLines(string(eventRes.Stdout), 20)
s.writeSection(fmt.Sprintf("最近事件 (namespace=%s)", namespace), events)
if hint := eventHint(events); hint != "" {
fmt.Fprintf(s.out, "\n事件提示: %s\n", hint)
}
}
}
return nil
}
// judgePod reports Pod-level health; per-container verdicts follow separately.
func judgePod(p pod) Finding {
evidence := "status.phase=" + p.Status.Phase
if p.Status.Reason != "" {
evidence += ", reason=" + p.Status.Reason
}
switch p.Status.Phase {
case "Pending":
return Finding{
Severity: "warn",
Verdict: "Pod 处于 Pending:尚未成功调度或容器还未创建",
Evidence: evidence,
Advice: "查看下方事件:常见原因是资源不足、亲和性/污点不满足、PVC 未绑定或镜像拉取慢",
}
case "Failed":
return Finding{
Severity: "error",
Verdict: "Pod 已失败",
Evidence: evidence,
Advice: "结合下方容器状态、上一次日志与事件定位失败原因",
}
case "Succeeded":
return Finding{
Severity: "ok",
Verdict: "Pod 已正常完成",
Evidence: evidence,
}
case "Running":
return Finding{
Severity: "ok",
Verdict: "Pod 已调度并在运行(容器级状态见下)",
Evidence: evidence,
}
default:
return Finding{
Severity: "warn",
Verdict: "Pod 状态未识别:" + p.Status.Phase,
Evidence: evidence,
}
}
}
// judgeContainer maps containerStatus to a root cause.
func judgeContainer(cs containerStatus) Finding {
if cs.State.Waiting != nil {
w := cs.State.Waiting
evidence := "state.waiting.reason=" + w.Reason
if w.Message != "" {
evidence += ", message=" + truncate(w.Message, 200)
}
switch w.Reason {
case "CrashLoopBackOff":
if cs.LastState.Terminated != nil {
t := cs.LastState.Terminated
evidence += fmt.Sprintf(", lastState.reason=%s, lastState.exitCode=%d", t.Reason, t.ExitCode)
}
return Finding{
Severity: "error",
Verdict: "容器反复崩溃并重启(CrashLoopBackOff)",
Evidence: evidence,
Advice: "查看下方“上一次”日志与退出码;检查启动命令、依赖服务与资源限制",
}
case "ImagePullBackOff", "ErrImagePull":
return Finding{
Severity: "error",
Verdict: "镜像拉取失败(" + w.Reason + ")",
Evidence: evidence,
Advice: "检查镜像名与 tag、imagePullSecrets、仓库可达性与节点网络",
}
case "CreateContainerConfigError":
return Finding{
Severity: "error",
Verdict: "容器配置错误:ConfigMap/Secret 缺失或键不存在",
Evidence: evidence,
Advice: "核对该 Pod 引用的 ConfigMap/Secret 是否存在,键名是否正确",
}
case "CreateContainerError", "RunContainerError":
return Finding{
Severity: "error",
Verdict: "容器创建/启动失败(" + w.Reason + ")",
Evidence: evidence,
Advice: "检查容器运行时与事件中的具体错误(挂载、cgroup、权限)",
}
case "ContainerCreating":
return Finding{
Severity: "warn",
Verdict: "容器正在创建",
Evidence: evidence,
Advice: "通常会自动完成;若长时间不变,查看事件中的挂载/镜像错误",
}
case "PodInitializing":
return Finding{
Severity: "warn",
Verdict: "初始化容器尚未完成",
Evidence: evidence,
Advice: "查看 initContainer 的日志与状态",
}
default:
return Finding{
Severity: "warn",
Verdict: "容器处于等待状态:" + w.Reason,
Evidence: evidence,
}
}
}
if cs.State.Terminated != nil {
t := cs.State.Terminated
evidence := fmt.Sprintf("state.terminated.reason=%s, exitCode=%d", t.Reason, t.ExitCode)
if t.Message != "" {
evidence += ", message=" + truncate(t.Message, 200)
}
switch t.Reason {
case "OOMKilled":
return Finding{
Severity: "error",
Verdict: "容器因内存超限被 OOMKilled",
Evidence: evidence,
Advice: "提高 resources.limits.memory,或降低应用内存占用",
}
case "Completed":
return Finding{
Severity: "ok",
Verdict: "容器已正常结束(Completed)",
Evidence: evidence,
}
case "Error":
return Finding{
Severity: "error",
Verdict: "容器以非零状态退出(Error)",
Evidence: evidence,
Advice: "查看下方日志末尾定位应用错误",
}
default:
return Finding{
Severity: "warn",
Verdict: "容器已终止:" + t.Reason,
Evidence: evidence,
}
}
}
if cs.State.Running != nil {
evidence := "state.running"
if cs.LastState.Terminated != nil {
t := cs.LastState.Terminated
evidence += fmt.Sprintf(", lastState.reason=%s, lastState.exitCode=%d", t.Reason, t.ExitCode)
}
if !cs.Ready {
return Finding{
Severity: "warn",
Verdict: "容器运行中但未就绪(就绪探针未通过)",
Evidence: evidence + ", ready=false",
Advice: "检查 readinessProbe 配置、启动时间与依赖服务",
}
}
return Finding{
Severity: "ok",
Verdict: "容器运行中且已就绪",
Evidence: evidence + ", ready=true",
}
}
return Finding{
Severity: "warn",
Verdict: "容器状态未知(state 为空)",
Evidence: "state 为空",
Advice: "查看 kubectl describe pod 与事件",
}
}
func containerState(cs containerStatus) string {
switch {
case cs.State.Waiting != nil:
return "waiting(" + cs.State.Waiting.Reason + ")"
case cs.State.Terminated != nil:
return "terminated(" + cs.State.Terminated.Reason + ")"
case cs.State.Running != nil:
return "running"
default:
return "unknown"
}
}
func (s *Service) writeContainerLogs(ctx context.Context, src source, podName, namespace string, cs containerStatus, opts Options) {
base := []string{"logs", podName, "-n", namespace, "-c", cs.Name, "--tail", strconv.Itoa(opts.Lines)}
res, err := s.run(ctx, "kubectl", kubectlArgs(src.context, base...)...)
if err != nil {
fmt.Fprintf(s.errOut, "警告: 获取容器 %s 日志失败: %v\n", cs.Name, err)
} else {
s.writeSection(fmt.Sprintf("日志 (%s, 最近 %d 行)", cs.Name, opts.Lines), string(res.Stdout))
}
if cs.LastState.Terminated != nil {
previous := append(append([]string{}, base...), "--previous")
res, err := s.run(ctx, "kubectl", kubectlArgs(src.context, previous...)...)
if err != nil {
fmt.Fprintf(s.errOut, "警告: 获取容器 %s 上一次日志失败: %v\n", cs.Name, err)
} else {
s.writeSection(fmt.Sprintf("日志 (%s, 上一次)", cs.Name), string(res.Stdout))
}
}
}
// eventHint turns common event fragments into an actionable hint.
func eventHint(events string) string {
lower := strings.ToLower(events)
switch {
case strings.Contains(lower, "failedscheduling"), strings.Contains(lower, "insufficient"):
return "事件显示调度失败:检查节点资源、亲和性/反亲和性、污点与 PVC 绑定"
case strings.Contains(lower, "failedmount"), strings.Contains(lower, "failedattachvolume"):
return "事件显示存储挂载失败:检查 PVC/PV、StorageClass 与 ConfigMap/Secret 挂载"
case strings.Contains(lower, "unhealthy"):
return "事件显示探针失败:检查 liveness/readiness 探针路径、端口与启动时间"
case strings.Contains(lower, "backoff"):
return "事件显示退避重试:结合容器退出码与日志确认根因"
}
return ""
}