- Docker:新增只读、生命周期、清理、Compose 共 32 条命令,Linux 自动适配 podman - 危险命令 clearall/prunevolumes 未带 --yes 时仅提示并以退出码 2 结束 - 需要容器/镜像名的命令做必填参数校验,-- 之后参数计入位置参数 - diagnose:自动识别 Docker/Podman 容器或 K8s Pod,输出状态、根因、依据、建议并附日志与事件 - diagnose 中 -n 表示 namespace,其余命令 -n 仍为 --dry-run - 新增内置动作桥接与平台变体 helper,补充单元/集成/CLI 测试与 README
373 lines
11 KiB
Go
373 lines
11 KiB
Go
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 ""
|
||
}
|