- docker save/rmi:镜像名不再拼接 digest({{.Digest}} 对多数镜像输出
sha256:…,导致 alpine:3.20 被存成 alpine:3.20@sha256:… 而查不到),
digest 单独保存;同时修正自定义 --format 输出中 IMAGE ID 表头未被过滤
- diagnose:目标不存在时给出相似名称候选,无相似时列出当前存在的对象
- net.pids:无进程占用时向 stderr 提示且 stdout 保持为空,新增 --pid-file
- net.ports:默认输出跨平台一致的端口表格,--raw 保留原生命令输出
- disk.usage:路径可省略(默认当前目录),新增 --depth N 控制层级
217 lines
7.4 KiB
Go
217 lines
7.4 KiB
Go
package diagnose
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestResolveAutoDocker(t *testing.T) {
|
|
inspectJSON := `[{"Id":"abc","Name":"/web","State":{"Status":"running"}}]`
|
|
r := &fakeRunner{
|
|
available: map[string]bool{"docker": true, "kubectl": true},
|
|
respond: func(name string, args []string) (CommandResult, error) {
|
|
switch {
|
|
case len(args) > 0 && args[0] == "inspect":
|
|
return CommandResult{Stdout: []byte(inspectJSON)}, nil
|
|
case len(args) > 0 && args[0] == "logs":
|
|
return CommandResult{Stdout: []byte("ok\n")}, nil
|
|
case len(args) > 0 && args[0] == "events":
|
|
return CommandResult{}, nil
|
|
}
|
|
return CommandResult{}, fmt.Errorf("unexpected call %s %v", name, args)
|
|
},
|
|
}
|
|
|
|
s, out, _ := newTestService(r)
|
|
if err := s.Run(context.Background(), []string{"web"}); err != nil {
|
|
t.Fatalf("Run error: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "来源: Docker (docker)") {
|
|
t.Errorf("expected docker source:\n%s", out.String())
|
|
}
|
|
if r.called("kubectl") {
|
|
t.Error("kubectl should not be consulted when docker hits")
|
|
}
|
|
}
|
|
|
|
func TestResolveAutoKubernetes(t *testing.T) {
|
|
r := &fakeRunner{
|
|
available: map[string]bool{"docker": true, "kubectl": true},
|
|
respond: func(name string, args []string) (CommandResult, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case len(args) > 0 && args[0] == "inspect":
|
|
return CommandResult{Stdout: []byte("[]")}, nil
|
|
case strings.HasPrefix(joined, "config view"):
|
|
return CommandResult{}, nil
|
|
case strings.HasPrefix(joined, "get pod"):
|
|
return CommandResult{Stdout: []byte("pod/api-0\n")}, nil
|
|
case strings.HasPrefix(joined, "logs"):
|
|
return CommandResult{Stdout: []byte("log\n")}, nil
|
|
case strings.HasPrefix(joined, "get events"):
|
|
return CommandResult{}, nil
|
|
}
|
|
return CommandResult{}, fmt.Errorf("unexpected call %s", joined)
|
|
},
|
|
}
|
|
|
|
s, out, _ := newTestService(r)
|
|
// The fake returns name output for the first pod probe but diagnoseK8s
|
|
// needs JSON; swap the responder after resolve would be brittle, so return
|
|
// JSON for every get pod call instead.
|
|
r.respond = func(name string, args []string) (CommandResult, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case len(args) > 0 && args[0] == "inspect":
|
|
return CommandResult{Stdout: []byte("[]")}, nil
|
|
case strings.HasPrefix(joined, "config view"):
|
|
return CommandResult{}, nil
|
|
case strings.HasPrefix(joined, "get pod"):
|
|
return CommandResult{Stdout: []byte(crashLoopPodJSON)}, nil
|
|
case strings.HasPrefix(joined, "logs"):
|
|
return CommandResult{Stdout: []byte("log\n")}, nil
|
|
case strings.HasPrefix(joined, "get events"):
|
|
return CommandResult{}, nil
|
|
}
|
|
return CommandResult{}, fmt.Errorf("unexpected call %s", joined)
|
|
}
|
|
|
|
if err := s.Run(context.Background(), []string{"api-0"}); err != nil {
|
|
t.Fatalf("Run error: %v", err)
|
|
}
|
|
if !strings.Contains(out.String(), "来源: Kubernetes") {
|
|
t.Errorf("expected kubernetes source:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestResolveNotFound(t *testing.T) {
|
|
r := &fakeRunner{
|
|
available: map[string]bool{"docker": true, "kubectl": true},
|
|
respond: func(name string, args []string) (CommandResult, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case len(args) > 0 && args[0] == "inspect":
|
|
return CommandResult{Stdout: []byte("[]")}, nil
|
|
case strings.HasPrefix(joined, "config view"):
|
|
return CommandResult{}, nil
|
|
case strings.HasPrefix(joined, "get pod"):
|
|
return CommandResult{Stderr: []byte("not found")}, fmt.Errorf("exit status 1")
|
|
}
|
|
return CommandResult{}, nil
|
|
},
|
|
}
|
|
|
|
s, _, _ := newTestService(r)
|
|
err := s.Run(context.Background(), []string{"ghost"})
|
|
if err == nil || !strings.Contains(err.Error(), "未找到目标") {
|
|
t.Fatalf("err = %v, want not-found error", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveNamespaceSkipsDocker(t *testing.T) {
|
|
r := &fakeRunner{
|
|
available: map[string]bool{"docker": true, "kubectl": true},
|
|
respond: func(name string, args []string) (CommandResult, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case strings.HasPrefix(joined, "get pod"):
|
|
return CommandResult{Stdout: []byte(crashLoopPodJSON)}, nil
|
|
case strings.HasPrefix(joined, "logs"):
|
|
return CommandResult{}, nil
|
|
case strings.HasPrefix(joined, "get events"):
|
|
return CommandResult{}, nil
|
|
}
|
|
return CommandResult{}, fmt.Errorf("unexpected call %s", joined)
|
|
},
|
|
}
|
|
|
|
s, out, _ := newTestService(r)
|
|
if err := s.Run(context.Background(), []string{"api-0", "--namespace", "prod"}); err != nil {
|
|
t.Fatalf("Run error: %v", err)
|
|
}
|
|
if r.called("inspect") {
|
|
t.Error("namespace should skip the docker probe")
|
|
}
|
|
if !strings.Contains(out.String(), "namespace=prod") {
|
|
t.Errorf("namespace missing from output:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestResolveNotFoundSuggestsSimilarContainers(t *testing.T) {
|
|
r := &fakeRunner{
|
|
available: map[string]bool{"docker": true, "kubectl": true},
|
|
respond: func(name string, args []string) (CommandResult, error) {
|
|
joined := strings.Join(args, " ")
|
|
switch {
|
|
case len(args) > 0 && args[0] == "inspect":
|
|
return CommandResult{Stdout: []byte("[]")}, nil
|
|
case strings.HasPrefix(joined, "ps -a"):
|
|
return CommandResult{Stdout: []byte("web-1\nweb-2\ndb\nnginx-web\n")}, nil
|
|
case strings.HasPrefix(joined, "config view"):
|
|
return CommandResult{}, nil
|
|
case strings.HasPrefix(joined, "get pod"):
|
|
return CommandResult{Stderr: []byte("not found")}, fmt.Errorf("exit status 1")
|
|
}
|
|
return CommandResult{}, nil
|
|
},
|
|
}
|
|
|
|
s, _, _ := newTestService(r)
|
|
err := s.Run(context.Background(), []string{"web"})
|
|
if err == nil || !strings.Contains(err.Error(), "未找到目标") {
|
|
t.Fatalf("err = %v, want not-found error", err)
|
|
}
|
|
msg := err.Error()
|
|
for _, want := range []string{"你可能要找的是", "web-1", "web-2"} {
|
|
if !strings.Contains(msg, want) {
|
|
t.Errorf("error missing %q:\n%s", want, msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolveNotFoundWithoutCandidatesKeepsOriginalText(t *testing.T) {
|
|
r := &fakeRunner{
|
|
available: map[string]bool{"docker": true},
|
|
respond: func(name string, args []string) (CommandResult, error) {
|
|
switch {
|
|
case len(args) > 0 && args[0] == "inspect":
|
|
return CommandResult{Stdout: []byte("[]")}, nil
|
|
case strings.HasPrefix(strings.Join(args, " "), "ps -a"):
|
|
return CommandResult{Stdout: []byte("alpha\nbeta\n")}, nil
|
|
}
|
|
return CommandResult{}, nil
|
|
},
|
|
}
|
|
|
|
s, _, _ := newTestService(r)
|
|
err := s.Run(context.Background(), []string{"ghost"})
|
|
if err == nil || !strings.Contains(err.Error(), "未找到目标") {
|
|
t.Fatalf("err = %v, want not-found error", err)
|
|
}
|
|
if strings.Contains(err.Error(), "你可能要找的是") {
|
|
t.Errorf("unexpected candidate hint:\n%s", err.Error())
|
|
}
|
|
// Nothing is similar, so the existing objects are listed instead.
|
|
for _, want := range []string{"当前存在的对象", "alpha", "beta"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("error missing %q:\n%s", want, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMatchNamesRanking(t *testing.T) {
|
|
names := []string{"nginx-web", "web-1", "WEB-2", "api", "myweb"}
|
|
got := matchNames(names, "web")
|
|
want := []string{"web-1", "WEB-2", "nginx-web", "myweb"}
|
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("matchNames() = %v, want %v (prefix hits first, case-insensitive)", got, want)
|
|
}
|
|
|
|
many := []string{"web-1", "web-2", "web-3", "web-4", "web-5", "web-6", "web-7"}
|
|
if got := matchNames(many, "web"); len(got) != 5 {
|
|
t.Fatalf("matchNames returned %d names, want the 5 closest", len(got))
|
|
}
|
|
}
|