1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
| import (
"context"
"strings"
"github.com/pkg/errors"
"github.com/docker/docker/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/strslice"
"github.com/zeromake/docker-debug/pkg/stream"
"github.com/zeromake/docker-debug/pkg/tty"
)
const (
legacyDefaultDomain = "index.docker.io"
defaultDomain = "docker.io"
fficialRepoName = "library"
)
type Cli struct {
client client.APIClient
in *stream.InStream
out *stream.OutStream
err io.Writer
}
// splitDockerDomain 分割镜像名和domain
func splitDockerDomain(name string) (domain, remainder string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
domain, remainder = defaultDomain, name
} else {
domain, remainder = name[:i], name[i+1:]
}
if domain == legacyDefaultDomain {
domain = defaultDomain
}
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
// 处理docker hub 镜像名映射
remainder = officialRepoName + "/" + remainder
}
return
}
// ImagePull 拉取镜像
func (c *Cli) ImagePull(imageName string) error {
domain, remainder := splitDockerDomain(imageName);
body, err := c.client.ImagePull(
context.Background(),
domain + '/' + remainder,
types.ImagePullOptions{},
)
if err != nil {
return errors.WithStack(err)
}
defer body.Close()
// docker 包里自带处理 pull 的 http 输出到 tty。
return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.out, nil)
}
// CreateContainer 创建一个自定义镜像的新容器并把目标容器的各种资源挂载到新容器上
func (c *Cli) CreateContainer(targetContainerID string) error {
ctx := context.Background()
info, err := cli.client.ContainerInspect(ctx, targetContainerID)
if err != nil {
return errors.WithStack(err)
}
mountDir, ok := info.GraphDriver.Data["MergedDir"]
mounts = []mount.Mount{}
targetMountDir = '/mnt/container';
// 通过 Inspect 查找出 MergedDir 位置并挂载到新容器的 /mnt/container
if ok {
mounts = append(mounts, mount.Mount{
Type: "bind",
Source: mountDir,
Target: targetMountDir,
})
}
// 继承目标容器的挂载目录
for _, i := range info.Mounts {
var mountType = i.Type
if i.Type == "volume" {
// 虚拟目录在 docker 处理后也是一个在宿主机上的普通目录改为 bind
mountType = "bind"
}
mounts = append(mounts, mount.Mount{
Type: mountType,
Source: i.Source,
Target: targetMountDir + i.Destination,
ReadOnly: !i.RW,
})
}
targetName := fmt.Sprintf("container:%s", targetContainerID)
containerConfig := &container.Config{
// 直接使用 sh 命令作为统一的容器后台进程
Entrypoint: strslice.StrSlice([]string{"/usr/bin/env", "sh"}),
// 默认镜像,真实项目中应该来自配置
Image: 'nicolaka/netshoot:latest',
Tty: true,
OpenStdin: true,
StdinOnce: true,
}
hostConfig := &container.HostConfig{
// network 共享
NetworkMode: container.NetworkMode(targetName),
// 用户共享
UsernsMode: container.UsernsMode(targetName),
// ipc 共享
IpcMode: container.IpcMode(targetName),
// pid 共享
PidMode: container.PidMode(targetName),
// 文件共享
Mounts: mounts,
}
}
// ExecCreate 创建exec
func ExecCreate(containerID string) error {
ctx := context.Background()
execConfig := types.ExecConfig{
// User: options.user,
// Privileged: options.privileged,
// DetachKeys: options.detachKeys,
// 是否分配一个 tty vim 之类的 cli 交互类工具需要
Tty: true,
// 是否附加各种标准流
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
// exec 需要执行的命令也就是目标命令
Cmd: [
'bash',
'-l',
],
}
resp, err := cli.client.ContainerExecCreate(ctx, container, opt)
return resp, errors.WithStack(err)
}
func (cli *DebugCli) ExecStart(execID string) error {
ctx := context.Background()
execConfig := types.ExecStartCheck{
Tty: true,
}
response, err := cli.client.ContainerExecAttach(ctx, execID, execConfig)
if err != nil {
return errors.WithStack(err)
}
// 把 docker cli 包的 tty 移植了可以直接处理 HijackedIO。
streamer := tty.HijackedIOStreamer{
Streams: cli,
InputStream: cli.in,
OutputStream: cli.out,
ErrorStream: cli.err,
Resp: response,
TTY: true,
}
return streamer.Stream(ctx)
}
|