├── .chglog
├── CHANGELOG.tpl.md
└── config.yml
├── .github
├── FUNDING.yml
└── workflows
│ └── release.yml
├── .gitignore
├── .reviewdog.yml
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README-zh-Hans.md
├── README.md
├── cmd
└── docker-debug
│ └── main.go
├── demo.cast
├── go.mod
├── go.sum
├── internal
├── command
│ ├── cli.go
│ ├── config.go
│ ├── info.go
│ ├── init.go
│ ├── required.go
│ ├── root.go
│ └── use.go
└── config
│ ├── config.go
│ ├── migration.go
│ ├── version-0.2.1.go
│ ├── version-0.7.10.go
│ ├── version-0.7.2.go
│ └── version-0.7.6.go
├── pkg
├── opts
│ ├── hosts.go
│ ├── hosts_unix.go
│ └── hosts_windows.go
├── stream
│ ├── in.go
│ ├── out.go
│ └── stream.go
└── tty
│ ├── hijack.go
│ └── tty.go
├── scripts
├── binary.sh
├── upx.sh
├── variables.darwin-m1.env
├── variables.darwin.env
├── variables.env
├── variables.linux.env
└── variables.windows.env
├── shell
└── docker-debug.sh
└── version
└── version.go
/.chglog/CHANGELOG.tpl.md:
--------------------------------------------------------------------------------
1 | {{ if .Versions -}}
2 |
3 | ## [Unreleased]
4 |
5 | {{ if .Unreleased.CommitGroups -}}
6 | {{ range .Unreleased.CommitGroups -}}
7 | ### {{ .Title }}
8 | {{ range .Commits -}}
9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
10 | {{ end }}
11 | {{ end -}}
12 | {{ end -}}
13 | {{ end -}}
14 |
15 | {{ range .Versions }}
16 |
17 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
18 | {{ range .CommitGroups -}}
19 | ### {{ .Title }}
20 | {{ range .Commits -}}
21 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
22 | {{ end }}
23 | {{ end -}}
24 |
25 | {{- if .RevertCommits -}}
26 | ### Reverts
27 | {{ range .RevertCommits -}}
28 | - {{ .Revert.Header }}
29 | {{ end }}
30 | {{ end -}}
31 |
32 | {{- if .NoteGroups -}}
33 | {{ range .NoteGroups -}}
34 | ### {{ .Title }}
35 | {{ range .Notes }}
36 | {{ .Body }}
37 | {{ end }}
38 | {{ end -}}
39 | {{ end -}}
40 | {{ end -}}
41 |
42 | {{- if .Versions }}
43 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD
44 | {{ range .Versions -}}
45 | {{ if .Tag.Previous -}}
46 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}
47 | {{ end -}}
48 | {{ end -}}
49 | {{ end -}}
--------------------------------------------------------------------------------
/.chglog/config.yml:
--------------------------------------------------------------------------------
1 | style: github
2 | template: CHANGELOG.tpl.md
3 | info:
4 | title: CHANGELOG
5 | repository_url: https://github.com/zeromake/docker-debug
6 | options:
7 | commits:
8 | # filters:
9 | # Type:
10 | # - feat
11 | # - fix
12 | # - perf
13 | # - refactor
14 | commit_groups:
15 | # title_maps:
16 | # feat: Features
17 | # fix: Bug Fixes
18 | # perf: Performance Improvements
19 | # refactor: Code Refactoring
20 | header:
21 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
22 | pattern_maps:
23 | - Type
24 | - Scope
25 | - Subject
26 | notes:
27 | keywords:
28 | - BREAKING CHANGE
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: docker-debug
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | custom: # Replace with a single custom sponsorship URL
9 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: set up go
13 | uses: actions/setup-go@v5
14 | with:
15 | go-version: 1.23
16 | # - name: reviewdog up
17 | # uses: reviewdog/action-setup@v1
18 | # - name: reviewdog
19 | # run: reviewdog -conf=.reviewdog.yml -reporter=local
20 | - name: build
21 | env:
22 | RELEASE_VERSION: ${{ github.ref_name }}
23 | run: ./scripts/binary.sh darwin && ./scripts/binary.sh darwin-m1 && ./scripts/binary.sh windows && ./scripts/binary.sh linux && sha256sum ./dist/*
24 | - name: update
25 | uses: softprops/action-gh-release@v1
26 | if: startsWith(github.ref, 'refs/tags/')
27 | with:
28 | files: |
29 | dist/docker-debug-darwin-amd64
30 | dist/docker-debug-darwin-arm64
31 | dist/docker-debug-linux-amd64
32 | dist/docker-debug-windows-amd64.exe
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | *.exe~
3 | *.orig
4 | .*.swp
5 | .DS_Store
6 | /.vscode/
7 | /dist/
8 | /vendor/
9 | /.idea/
--------------------------------------------------------------------------------
/.reviewdog.yml:
--------------------------------------------------------------------------------
1 | runner:
2 | golint:
3 | cmd: golint ./...
4 | errorformat:
5 | - "%f:%l:%c: %m"
6 | govet:
7 | cmd: go vet -all ./...
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.x
5 |
6 | env:
7 | global:
8 | - REVIEWDOG_VERSION="0.10.0"
9 | - UPX_VERSION="3.96"
10 | - GO111MODULE=on
11 |
12 | install:
13 | - mkdir -p ~/bin/ && export export PATH="~/bin/:$PATH"
14 | - curl -fSL https://github.com/reviewdog/reviewdog/releases/download/v${REVIEWDOG_VERSION}/reviewdog_${REVIEWDOG_VERSION}_Linux_x86_64.tar.gz -o ~/reviewdog.tar.gz && tar -xf ~/reviewdog.tar.gz -C ~ && mv ~/reviewdog ~/bin
15 | - curl -fSL https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-amd64_linux.tar.xz -o ~/upx.tar.xz && tar -xf ~/upx.tar.xz -C ~ && mv ~/upx-${UPX_VERSION}-amd64_linux/upx ~/bin
16 |
17 | script:
18 | - go mod tidy
19 | - go test ./...
20 | - reviewdog -conf=.reviewdog.yml -reporter=github-pr-check
21 | - ./scripts/binary.sh darwin && ./scripts/binary.sh windows && ./scripts/binary.sh linux
22 | - file ./dist/* && ./dist/docker-debug-linux-amd64 info
23 | - ls ./dist | xargs -I {} upx ./dist/{} -o ./dist/{}-upx
24 | - mv ./dist/docker-debug-windows-amd64.exe-upx ./dist/docker-debug-windows-amd64-upx.exe
25 | - file ./dist/* && ./dist/docker-debug-linux-amd64-upx info
26 | - openssl dgst -sha256 ./dist/*
27 |
28 | deploy:
29 | provider: releases
30 | token: ${GITHUB_TOKEN}
31 | file_glob: true
32 | file: dist/*
33 | cleanup: false
34 | skip_cleanup: true
35 | on:
36 | tags: true
37 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## [0.7.4] - 2022-01-20
3 | ### Docs
4 | - docker client version
5 |
6 | ### Feat
7 | - add -v $c/path
8 |
9 | ### Fix
10 | - container exit but not stop,check external stop
11 | - update docker term dep
12 |
13 |
14 |
15 | ## [0.7.3] - 2020-05-08
16 | ### Fix
17 | - config version migration
18 |
19 |
20 |
21 | ## [0.7.2] - 2020-05-08
22 | ### Feat
23 | - config add client version
24 |
25 | ### Fix
26 | - ci script error
27 | - ci script error
28 | - ci script error
29 | - upx reviewdog up version
30 |
31 |
32 |
33 | ## [0.7.0] - 2020-04-21
34 | ### Docs
35 | - update readme
36 |
37 | ### Feat
38 | - update mod dep
39 |
40 | ### Fix
41 | - update ci config
42 | - update ci config
43 |
44 |
45 |
46 | ## [0.6.3] - 2019-12-15
47 | ### Fix
48 | - check container is running
49 | - update change log
50 |
51 |
52 |
53 | ## [0.6.2] - 2019-06-20
54 | ### Doc
55 | - update changelog
56 |
57 | ### Docs
58 | - update readme
59 | - update readme version
60 |
61 | ### Feat
62 | - add FUNDING.yml
63 |
64 | ### Fix
65 | - ipc mode is not default
66 |
67 |
68 |
69 | ## [0.6.1] - 2019-04-28
70 | ### Docs
71 | - update changelog
72 |
73 | ### Fix
74 | - create container change id
75 | - del find container handle
76 |
77 |
78 |
79 | ## [0.6.0] - 2019-04-21
80 | ### Docs
81 | - update readme
82 |
83 | ### Feat
84 | - support -v mount filesystem
85 |
86 |
87 |
88 | ## [0.5.2] - 2019-04-09
89 | ### Docs
90 | - update version 0.5.2
91 | - update change log
92 | - readme add icon
93 | - add license
94 | - update changelog
95 |
96 | ### Feat
97 | - travis add deploy
98 | - go mod get docker pkg on latest
99 |
100 | ### Fix
101 | - ci config
102 | - ci add sha256
103 | - upx -o name change
104 | - upx out
105 | - del upx
106 | - tar -C
107 | - update upx pkg = 3.95
108 | - update apt pkg
109 | - change travis ci
110 | - update docker deb pkg latest
111 | - code style fmt
112 | - go mod env
113 |
114 |
115 |
116 | ## [v0.5.1] - 2019-04-03
117 | ### Docs
118 | - update readme download lastest
119 | - up changelog
120 |
121 | ### Fix
122 | - content error
123 |
124 |
125 |
126 | ## [v0.5.0] - 2019-04-01
127 | ### Docs
128 | - update readme
129 | - update readme changelog
130 |
131 | ### Feat
132 | - default config from env
133 |
134 | ### Fix
135 | - image split domain
136 |
137 | ### Test
138 | - travis add script test
139 | - add travis ci
140 | - add reviewdog ci
141 |
142 |
143 |
144 | ## [v0.4.0] - 2019-03-29
145 | ### Docs
146 | - update changelog
147 |
148 | ### Feat
149 | - add command ,
150 |
151 | ### Fix
152 | - docker config string format and write flag lost
153 |
154 |
155 |
156 | ## [v0.3.0] - 2019-03-28
157 | ### Docs
158 | - update readme download url on v0.3.0
159 | - update download url and changelog up
160 |
161 | ### Feat
162 | - mount volume filesystem
163 |
164 |
165 |
166 | ## [v0.2.2] - 2019-03-28
167 | ### Docs
168 | - update todo list
169 | - update v0.2.1
170 |
171 | ### Feat
172 | - add brew pkg install on readme
173 |
174 | ### Fix
175 | - mount dir del suffix
176 | - readme download url version set lastest
177 |
178 |
179 |
180 | ## [v0.2.1] - 2019-03-27
181 | ### Docs
182 | - update readme download url version
183 | - update changelog v0.2.0
184 |
185 | ### Feat
186 | - update asciinema demo
187 |
188 | ### Fix
189 | - add version semver migration
190 | - add readme todo
191 |
192 |
193 |
194 | ## [v0.2.0] - 2019-03-20
195 | ### Docs
196 | - update readme
197 | - init changelog
198 |
199 | ### Feat
200 | - command add more
201 | - add support windows7
202 | - add readme zh-hans
203 |
204 |
205 |
206 | ## v0.1.0 - 2019-03-19
207 | ### Feat
208 | - update README.md
209 | - init CHANGELOG.md file
210 | - add git-chglog
211 | - init cmd run docker-debug
212 | - add root cmd
213 | - project applying google layout
214 | - github.com/docker/docker -> github.com/zeromake/moby
215 | - add cmd pkg
216 | - add version and makefile
217 | - add docker/pkg dep
218 | - add win7 tls config
219 | - add docker inspect info.mount, MergeDir
220 | - add config
221 | - update win dep
222 | - main write a run example
223 | - init docker-debug
224 |
225 | ### Fix
226 | - file mode
227 | - info time
228 | - exit container is force
229 | - tty = true
230 | - ignore add more
231 |
232 | [0.7.4]: https://github.com/zeromake/docker-debug/compare/0.7.3...0.7.4
233 | [0.7.3]: https://github.com/zeromake/docker-debug/compare/0.7.2...0.7.3
234 | [0.7.2]: https://github.com/zeromake/docker-debug/compare/0.7.0...0.7.2
235 | [0.7.0]: https://github.com/zeromake/docker-debug/compare/0.6.3...0.7.0
236 | [0.6.3]: https://github.com/zeromake/docker-debug/compare/0.6.2...0.6.3
237 | [0.6.2]: https://github.com/zeromake/docker-debug/compare/0.6.1...0.6.2
238 | [0.6.1]: https://github.com/zeromake/docker-debug/compare/0.6.0...0.6.1
239 | [0.6.0]: https://github.com/zeromake/docker-debug/compare/0.5.2...0.6.0
240 | [0.5.2]: https://github.com/zeromake/docker-debug/compare/v0.5.1...0.5.2
241 | [v0.5.1]: https://github.com/zeromake/docker-debug/compare/v0.5.0...v0.5.1
242 | [v0.5.0]: https://github.com/zeromake/docker-debug/compare/v0.4.0...v0.5.0
243 | [v0.4.0]: https://github.com/zeromake/docker-debug/compare/v0.3.0...v0.4.0
244 | [v0.3.0]: https://github.com/zeromake/docker-debug/compare/v0.2.2...v0.3.0
245 | [v0.2.2]: https://github.com/zeromake/docker-debug/compare/v0.2.1...v0.2.2
246 | [v0.2.1]: https://github.com/zeromake/docker-debug/compare/v0.2.0...v0.2.1
247 | [v0.2.0]: https://github.com/zeromake/docker-debug/compare/v0.1.0...v0.2.0
248 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 zeromake
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: binary
2 |
3 | .PHONY: binary
4 | binary: ## build executable for Linux
5 | @echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
6 | ./scripts/binary.sh
7 |
8 | .PHONY: upx
9 | upx:
10 | ./scripts/upx.sh
11 |
12 | .PHONY: clean
13 | clean:
14 | rm -rf ./dist/*
15 |
16 | .PHONY: binary-upx
17 | binary-upx:
18 | ./scripts/binary.sh
19 | ./scripts/upx.sh
20 |
--------------------------------------------------------------------------------
/README-zh-Hans.md:
--------------------------------------------------------------------------------
1 | # Docker-debug
2 |
3 | [](https://github.com/zeromake/docker-debug/actions/workflows/release.yml)
4 | [](https://goreportcard.com/report/zeromake/docker-debug)
5 |
6 | [English](README.md) ∙ [简体中文](README-zh-Hans.md)
7 |
8 | ## Overview
9 |
10 | `docker-debug` 是一个运行中的 `docker` 容器故障排查方案,
11 | 在运行中的 `docker` 上额外启动一个容器,并将目标容器的 `pid`, `network`, `uses`, `filesystem` 和 `ipc` 命名空间注入到新的容器里,
12 | 因此,您可以使用任意故障排除工具,而无需在生产容器镜像中预先安装额外的工具环境。
13 |
14 | ## Demo
15 | [](https://asciinema.org/a/235025)
16 | ## Quick Start
17 |
18 | 安装 `docker-debug` 命令行工具
19 |
20 | **mac brew**
21 |
22 | ```shell
23 | brew install zeromake/docker-debug/docker-debug
24 | ```
25 |
26 | **下载二进制文件**
27 |
28 |
29 |
30 | 使用 bash 或 zsh
31 |
32 |
33 | ``` bash
34 | # get latest tag
35 | VERSION=`curl -w '%{url_effective}' -I -L -s -S https://github.com/zeromake/docker-debug/releases/latest -o /dev/null | awk -F/ '{print $NF}'`
36 |
37 | # MacOS Intel
38 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/${VERSION}/docker-debug-darwin-amd64
39 |
40 | # MacOS M1
41 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/${VERSION}/docker-debug-darwin-arm64
42 |
43 | # Linux
44 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/${VERSION}/docker-debug-linux-amd64
45 |
46 | chmod +x ./docker-debug
47 | sudo mv docker-debug /usr/local/bin/
48 |
49 | # Windows
50 | curl -Lo docker-debug.exe https://github.com/zeromake/docker-debug/releases/download/${VERSION}/docker-debug-windows-amd64.exe
51 | ```
52 |
53 |
54 |
55 |
56 |
57 | 使用 fish
58 |
59 |
60 | ``` fish
61 | # get latest tag
62 | set VERSION (curl -w '%{url_effective}' -I -L -s -S https://github.com/zeromake/docker-debug/releases/latest -o /dev/null | awk -F/ '{print $NF}')
63 |
64 | # MacOS Intel
65 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/$VERSION/docker-debug-darwin-amd64
66 |
67 | # MacOS M1
68 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/$VERSION/docker-debug-darwin-arm64
69 |
70 | # Linux
71 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/$VERSION/docker-debug-linux-amd64
72 |
73 | chmod +x ./docker-debug
74 | sudo mv docker-debug /usr/local/bin/
75 |
76 | # Windows
77 | curl -Lo docker-debug.exe https://github.com/zeromake/docker-debug/releases/download/$VERSION/docker-debug-windows-amd64.exe
78 | ```
79 |
80 |
81 |
82 | 或者到 [release page](https://github.com/zeromake/docker-debug/releases/lastest) 下载最新可执行文件并添加到 PATH。
83 |
84 | **我们来试试吧!**
85 | ``` shell
86 | # docker-debug [OPTIONS] CONTAINER COMMAND [ARG...] [flags]
87 | docker-debug CONTAINER COMMAND
88 |
89 | # More flags
90 | docker-debug --help
91 |
92 | # info
93 | docker-debug info
94 | ```
95 |
96 | ## Build from source
97 | Clone this repo and:
98 | ``` shell
99 | go build -o docker-debug ./cmd/docker-debug
100 | mv docker-debug /usr/local/bin
101 | ```
102 |
103 | ## 默认镜像
104 | docker-debug 使用 `nicolaka/netshoot` 作为默认镜像来运行额外容器。
105 | 你可以通过命令行 `flag(--image)` 覆盖默认镜像,或者直接修改配置文件 `~/.docker-debug/config.toml` 中的 `image`。
106 | ``` toml
107 | # 配置文件版本号
108 | version = "0.7.4"
109 | # 目标容器文件系统挂载点
110 | mount_dir = "/mnt/container"
111 | # 默认镜像
112 | image = "nicolaka/netshoot:latest"
113 | # 大多数docker操作的超时,默认为 10s。
114 | timeout = 10000000000
115 | # 默认使用哪个配置来连接docker
116 | config_default = "default"
117 |
118 | # docker 连接配置
119 | [config]
120 | # docker 默认连接配置
121 | [config.default]
122 | # docker 客户端版本指定默认 1.40
123 | version = "1.40"
124 | host = "unix:///var/run/docker.sock"
125 | # 是否为 tls
126 | tls = false
127 | # 证书目录
128 | cert_dir = ""
129 | # 证书密码
130 | cert_password = ""
131 | ```
132 |
133 | ## 详细
134 | 1. 在 `docker` 中查找镜像,没有调用 `docker` 拉取镜像。
135 | 2. 查找目标容器, 没找到返回报错。
136 | 3. 通过自定义镜像创建一个容器并挂载 `ipc`, `pid`, `network`, `etc`, `filesystem`。
137 | 4. 在新容器中创建并运行 `docker exec`。
138 | 5. 在新容器中进行调试。
139 | 6. 等待调试容器退出运行,把调试用的额外容器清理掉。
140 |
141 | ## Reference & Thank
142 | 1. [kubectl-debug](https://github.com/aylei/kubectl-debug): `docker-debug` 想法来自这个 kubectl 调试工具。
143 | 2. [Docker核心技术与实现原理](https://draveness.me/docker): `docker-debug` 的文件系统挂载原理来自这个博文。
144 | 3. [docker-engine-api-doc](https://docs.docker.com/engine/api/latest): docker engine api 文档。
145 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Docker-debug
2 |
3 | [](https://github.com/zeromake/docker-debug/actions/workflows/release.yml)
4 | [](https://goreportcard.com/report/zeromake/docker-debug)
5 |
6 | [English](README.md) ∙ [简体中文](README-zh-Hans.md)
7 |
8 | ## Overview
9 |
10 | `docker-debug` is an troubleshooting running docker container,
11 | which allows you to run a new container in running docker for debugging purpose.
12 | The new container will join the `pid`, `network`, `user`, `filesystem` and `ipc` namespaces of the target container,
13 | so you can use arbitrary trouble-shooting tools without pre-installing them in your production container image.
14 |
15 | ## Demo
16 | [](https://asciinema.org/a/235025)
17 | ## Quick Start
18 |
19 | Install the `docker-debug` cli
20 |
21 | **mac brew**
22 | ```shell
23 | brew install zeromake/docker-debug/docker-debug
24 | ```
25 |
26 | **download binary file**
27 |
28 |
29 |
30 | use bash or zsh
31 |
32 |
33 | ``` bash
34 | # get latest tag
35 | VERSION=`curl -w '%{url_effective}' -I -L -s -S https://github.com/zeromake/docker-debug/releases/latest -o /dev/null | awk -F/ '{print $NF}'`
36 |
37 | # MacOS Intel
38 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/${VERSION}/docker-debug-darwin-amd64
39 |
40 | # MacOS M1
41 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/${VERSION}/docker-debug-darwin-arm64
42 |
43 | # Linux
44 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/${VERSION}/docker-debug-linux-amd64
45 |
46 | chmod +x ./docker-debug
47 | sudo mv docker-debug /usr/local/bin/
48 |
49 | # Windows
50 | curl -Lo docker-debug.exe https://github.com/zeromake/docker-debug/releases/download/${VERSION}/docker-debug-windows-amd64.exe
51 | ```
52 |
53 |
54 |
55 |
56 |
57 | use fish
58 |
59 |
60 | ``` fish
61 | # get latest tag
62 | set VERSION (curl -w '%{url_effective}' -I -L -s -S https://github.com/zeromake/docker-debug/releases/latest -o /dev/null | awk -F/ '{print $NF}')
63 |
64 | # MacOS Intel
65 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/$VERSION/docker-debug-darwin-amd64
66 |
67 | # MacOS M1
68 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/$VERSION/docker-debug-darwin-arm64
69 |
70 | # Linux
71 | curl -Lo docker-debug https://github.com/zeromake/docker-debug/releases/download/$VERSION/docker-debug-linux-amd64
72 |
73 | chmod +x ./docker-debug
74 | sudo mv docker-debug /usr/local/bin/
75 |
76 | # Windows
77 | curl -Lo docker-debug.exe https://github.com/zeromake/docker-debug/releases/download/$VERSION/docker-debug-windows-amd64.exe
78 | ```
79 |
80 |
81 |
82 | download the latest binary from the [release page](https://github.com/zeromake/docker-debug/releases/lastest) and add it to your PATH.
83 |
84 | **Try it out!**
85 | ``` shell
86 | # docker-debug [OPTIONS] CONTAINER COMMAND [ARG...] [flags]
87 | docker-debug CONTAINER COMMAND
88 |
89 | # More flags
90 | docker-debug --help
91 |
92 | # info
93 | docker-debug info
94 | ```
95 |
96 | ## Build from source
97 | Clone this repo and:
98 | ``` shell
99 | go build -o docker-debug ./cmd/docker-debug
100 | mv docker-debug /usr/local/bin
101 | ```
102 |
103 | ## Default image
104 | docker-debug uses nicolaka/netshoot as the default image to run debug container.
105 | You can override the default image with cli flag, or even better, with config file ~/.docker-debug/config.toml
106 | ``` toml
107 | version = "0.7.5"
108 | image = "nicolaka/netshoot:latest"
109 | mount_dir = "/mnt/container"
110 | timeout = 10000000000
111 | config_default = "default"
112 |
113 | [config]
114 | [config.default]
115 | version = "1.40"
116 | host = "unix:///var/run/docker.sock"
117 | tls = false
118 | cert_dir = ""
119 | cert_password = ""
120 | ```
121 |
122 | ## Todo
123 | - [x] support windows7(Docker Toolbox)
124 | - [ ] support windows10
125 | - [ ] refactoring code
126 | - [ ] add testing
127 | - [x] add changelog
128 | - [x] add README_CN.md
129 | - [x] add brew package
130 | - [x] docker-debug version manage config file
131 | - [x] cli command set mount target container filesystem
132 | - [x] mount volume filesystem
133 | - [x] docker connection config on cli command
134 | - [x] `-v` cli args support
135 | - [ ] docker-debug signal handle smooth exit
136 | - [ ] cli command document on readme
137 | - [ ] config file document on readme
138 | - [ ] add http api and web shell
139 |
140 | ## Details
141 | 1. find image docker is has, not has pull the image.
142 | 2. find container name is has, not has return error.
143 | 3. from customize image runs a new container in the container's namespaces (ipc, pid, network, etc, filesystem) with the STDIN stay open.
144 | 4. create and run a exec on new container.
145 | 5. Debug in the debug container.
146 | 6. then waits for the debug container to exit and do the cleanup.
147 |
148 | ## Reference & Thank
149 | 1. [kubectl-debug](https://github.com/aylei/kubectl-debug): `docker-debug` inspiration is from to this a kubectl debug tool.
150 | 2. [Docker核心技术与实现原理](https://draveness.me/docker): `docker-debug` filesystem is from the blog.
151 | 3. [docker-engine-api-doc](https://docs.docker.com/engine/api/latest): docker engine api document.
152 |
153 | ## Contributors
154 |
155 | ### Code Contributors
156 |
157 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
158 |
159 |
160 | ### Financial Contributors
161 |
162 | Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/docker-debug/contribute)]
163 |
164 | #### Individuals
165 |
166 |
167 |
168 | #### Organizations
169 |
170 | Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/docker-debug/contribute)]
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/cmd/docker-debug/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/zeromake/docker-debug/internal/command"
5 | )
6 |
7 | func main() {
8 | command.Execute()
9 | }
10 |
--------------------------------------------------------------------------------
/demo.cast:
--------------------------------------------------------------------------------
1 | {"version": 2, "width": 185, "height": 18, "timestamp": 1553070182, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
2 | [0.023817, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
3 | [0.026669, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[JmacbookdeMacBook-Pro% \u001b[K"]
4 | [0.026832, "o", "\u001b[?2004h"]
5 | [0.821847, "o", "d"]
6 | [0.981775, "o", "\bdo"]
7 | [1.149486, "o", "c"]
8 | [1.317568, "o", "k"]
9 | [1.525321, "o", "e"]
10 | [1.613531, "o", "r"]
11 | [2.036519, "o", " "]
12 | [2.541444, "o", "p"]
13 | [2.725049, "o", "s"]
14 | [3.15695, "o", "\u001b[?2004l\r\r\n"]
15 | [3.203952, "o", "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS "]
16 | [3.204186, "o", " NAMES\r\n76b124cc7103 zero-reader_web \"python3 ./main.py -…\" 40 minutes ago Up 40 minutes 0.0.0.0:8000->8000/tcp zero-reader_web_1\r\n"]
17 | [3.206478, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
18 | [3.206615, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[JmacbookdeMacBook-Pro% \u001b[K\u001b[?2004h"]
19 | [5.461394, "o", "d"]
20 | [5.605236, "o", "\bdo"]
21 | [5.773247, "o", "c"]
22 | [5.877235, "o", "k"]
23 | [6.013154, "o", "e"]
24 | [6.085214, "o", "r"]
25 | [6.893367, "o", "-"]
26 | [7.493331, "o", "d"]
27 | [7.709265, "o", "e"]
28 | [8.013284, "o", "b"]
29 | [8.63705, "o", "u"]
30 | [8.877187, "o", "g"]
31 | [9.588232, "o", " "]
32 | [10.165369, "o", "zero"]
33 | [10.16563, "o", "-reader_web_1"]
34 | [11.349231, "o", " "]
35 | [11.988081, "o", "b"]
36 | [12.124914, "o", "a"]
37 | [12.308979, "o", "s"]
38 | [12.477195, "o", "h"]
39 | [12.771876, "o", " "]
40 | [13.164831, "o", "-"]
41 | [13.453043, "o", "l"]
42 | [14.284661, "o", "\u001b[?2004l\r\r\n"]
43 | [14.891759, "o", " dP dP dP \r\n 88 88 88 \r\n88d888b. .d8888b. d8888P .d8888b. 88d888b. .d8888b. .d8888b. d8888P \r\n88' `88 88ooood8 88 Y8ooooo. 88' `88 88' `88 88' `88 88 \r\n88 88 88. ... 88 88 88 88 88. .88 88. .88 88 \r\ndP dP `88888P' dP `88888P' dP dP `88888P' `88888P' dP \r\n \r\nWelcome to Netshoot! (github.com/nicolaka/netshoot) "]
44 | [14.898365, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/ \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[1] 🐳 → \u001b[00m"]
45 | [14.927227, "o", "\r\u001b[K \u001b[0;34m[1] 🐳 → \u001b[00m"]
46 | [16.862739, "o", "h"]
47 | [17.118452, "o", "o"]
48 | [17.454096, "o", "s"]
49 | [17.638273, "o", "t"]
50 | [18.197976, "o", "n"]
51 | [18.318368, "o", "a"]
52 | [18.445908, "o", "m"]
53 | [18.574166, "o", "e"]
54 | [19.104281, "o", "\r\n"]
55 | [19.105132, "o", "76b124cc7103\r\n"]
56 | [19.106105, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/ \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[2] 🐳 → \u001b[00m"]
57 | [21.38212, "o", "p"]
58 | [21.726367, "o", "s"]
59 | [22.070376, "o", " "]
60 | [22.72628, "o", "-"]
61 | [23.238488, "o", "e"]
62 | [23.502125, "o", "f"]
63 | [23.768591, "o", "\r\nPID USER TIME COMMAND\r\n 1 root 0:00 python3 ./main.py -w 1\r\n 7 root 0:00 python3 ./main.py -w 1\r\n 8 root 0:00 python3 ./main.py -w 1\r\n 9 root 0:00 python3 ./main.py -w 1\r\n 10 root 0:00 python3 ./main.py -w 1\r\n 11 root 0:00 python3 ./main.py -w 1\r\n 12 root 0:00 python3 ./main.py -w 1\r\n 13 root 0:00 python3 ./main.py -w 1\r\n 14 root 0:00 python3 ./main.py -w 1\r\n 98 root 0:00 sh\r\n 103 root 0:00 bash -l\r\n 117 root 0:00 ps -ef\r\n"]
64 | [23.769935, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/ \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[3] 🐳 → \u001b[00m"]
65 | [29.638047, "o", "t"]
66 | [29.757566, "o", "o"]
67 | [30.037555, "o", "p"]
68 | [31.422682, "o", "\r\n"]
69 | [31.528944, "o", "\u001b[H\u001b[JMem: 1099408K used, 947340K free, 540K shrd, 88632K buff, 524268K cached\r\nCPU: 0% usr 2% sys 0% nic 97% idle 0% io 0% irq 0% sirq\r\nLoad average: 0.01 0.31 0.36 1/574 120\r\n\u001b[7m PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND\u001b[m\r\n 7 1 root S 62768 3% 2 0% python3 ./main.py -w 1\r\n 14 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 11 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 12 1 root S 51228 2% 1 0% python3 ./main.py -w 1\r\n 9 1 root S 51228 2% 2 0% python3 ./main.py -w 1\r\n 8 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 10 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 13 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 1 0 root S 39704 2% 1 0% python3 ./main.py -w 1\r\n 103 0 root S 2332 0% 3 0% bash -l\r\n 98 0 root S 1592 0% 3 0% sh\r\n 120 103 root R 1524 0% "]
70 | [31.529251, "o", " 1 0% top\r"]
71 | [32.716462, "o", "\u001b[H\u001b[JMem: 1099392K used, 947356K free, 540K shrd, 88632K buff, 524268K cached\r\nCPU: 0% usr 2% sys 0% nic 97% idle 0% io 0% irq 0% sirq\r\nLoad average: 0.01 0.31 0.36 2/574 120\r\n\u001b[7m PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND\u001b[m\r\n 14 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 11 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 12 1 root S 51228 2% 1 0% python3 ./main.py -w 1\r\n 9 1 root S 51228 2% 2 0% python3 ./main.py -w 1\r\n 8 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 10 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 13 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 1 0 root S 39704 2% 1 0% python3 ./main.py -w 1\r\n 103 0 root S 2332 0% 3 0% bash -l\r\n 98 0 root S 1592 0% 3 0% sh\r\n 120 103 root R 1524 0% 1 0% top\r"]
72 | [32.965325, "o", "\u001b[H\u001b[JMem: 1099392K used, 947356K free, 540K shrd, 88632K buff, 524268K cached\r\nCPU: 0% usr 2% sys 0% nic 97% idle 0% io 0% irq 0% sirq\r\nLoad average: 0.01 0.31 0.36 3/574 120\r\n\u001b[7m PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND\u001b[m\r\n 11 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 12 1 root S 51228 2% 1 0% python3 ./main.py -w 1\r\n 9 1 root S 51228 2% 2 0% python3 ./main.py -w 1\r\n 8 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 10 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 13 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 1 0 root S 39704 2% 1 0% python3 ./main.py -w 1\r\n 103 0 root S 2332 0% 3 0% bash -l\r\n 98 0 root S 1592 0% 3 0% sh\r\n 120 103 root R 1524 0% 1 0% top\r"]
73 | [33.629812, "o", "\u001b[H\u001b[JMem: 1099392K used, 947356K free, 540K shrd, 88632K buff, 524268K cached\r\nCPU: 0% usr 2% sys 0% nic 97% idle 0% io 0% irq 0% sirq\r\nLoad average: 0.01 0.31 0.36 4/574 120\r\n\u001b[7m PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND\u001b[m\r\n 14 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 11 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 12 1 root S 51228 2% 1 0% python3 ./main.py -w 1\r\n 9 1 root S 51228 2% 2 0% python3 ./main.py -w 1\r\n 8 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 10 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 13 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 1 0 root S 39704 2% 1 0% python3 ./main.py -w 1\r\n 103 0 root S 2332 0% 3 0% bash -l\r\n 98 0 root S 1592 0% 3 0% sh\r\n 120 103 root R 1524 0% 1 0% top\r"]
74 | [33.821257, "o", "\u001b[H\u001b[JMem: 1099392K used, 947356K free, 540K shrd, 88632K buff, 524268K cached\r\nCPU: 0% usr 2% sys 0% nic 97% idle 0% io 0% irq 0% sirq\r\nLoad average: 0.01 0.31 0.36 2/574 120\r\n\u001b[7m PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND\u001b[m\r\n 7 1 root S 62768 3% 2 0% python3 ./main.py -w 1\r\n 14 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 11 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 12 1 root S 51228 2% 1 0% python3 ./main.py -w 1\r\n 9 1 root S 51228 2% 2 0% python3 ./main.py -w 1\r\n 8 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 10 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 13 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 1 0 root S 39704 2% 1 0% python3 ./main.py -w 1\r\n 103 0 root S 2332 0% 3 0% bash -l\r\n 98 0 root S 1592 0% 3 0% sh\r\n 120 103 root R 1524 0% "]
75 | [33.821583, "o", " 1 0% top\r"]
76 | [34.020832, "o", "\u001b[H\u001b[JMem: 1099392K used, 947356K free, 540K shrd, 88632K buff, 524268K cached\r\nCPU: 0% usr 2% sys 0% nic 97% idle 0% io 0% irq 0% sirq\r\nLoad average: 0.01 0.31 0.36 2/574 120\r\n\u001b[7m PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND\u001b[m\r\n 7 1 root S 62768 3% 2 0% python3 ./main.py -w 1\r\n 14 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 11 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 12 1 root S 51228 2% 1 0% python3 ./main.py -w 1\r\n 9 1 root S 51228 2% 2 0% python3 ./main.py -w 1\r\n 8 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 10 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 13 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 1 0 root S 39704 2% 1 0% python3 ./main.py -w 1\r\n 103 0 root S 2332 0% 3 0% bash -l\r\n 98 0 root S 1592 0% 3 0% sh\r\n 120 103 root R 1524 0% "]
77 | [34.021081, "o", " 1 0% top\r"]
78 | [34.220714, "o", "\u001b[H\u001b[JMem: 1099392K used, 947356K free, 540K shrd, 88632K buff, 524268K cached\r\nCPU: 0% usr 2% sys 0% nic 97% idle 0% io 0% irq 0% sirq\r\nLoad average: 0.01 0.31 0.36 3/574 120\r\n\u001b[7m PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND\u001b[m\r\n 7 1 root S 62768 3% 2 0% python3 ./main.py -w 1\r\n 14 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 11 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 12 1 root S 51228 2% 1 0% python3 ./main.py -w 1\r\n 9 1 root S 51228 2% 2 0% python3 ./main.py -w 1\r\n 8 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 10 1 root S 51228 2% 0 0% python3 ./main.py -w 1\r\n 13 1 root S 51228 2% 3 0% python3 ./main.py -w 1\r\n 1 0 root S 39704 2% 1 0% python3 ./main.py -w 1\r\n 103 0 root S 2332 0% 3 0% bash -l\r\n 98 0 root S 1592 0% 3 0% sh\r\n 120 103 root R 1524 0% "]
79 | [34.220987, "o", " 1 0% top\r"]
80 | [34.909761, "o", "\r\n"]
81 | [34.911199, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/ \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[4] 🐳 → \u001b[00m"]
82 | [38.853014, "o", "n"]
83 | [39.004673, "o", "e"]
84 | [39.372559, "o", "t"]
85 | [39.750002, "o", "s"]
86 | [39.909625, "o", "t"]
87 | [40.05396, "o", "a"]
88 | [40.285805, "o", "t"]
89 | [41.117314, "o", " "]
90 | [41.397597, "o", "-"]
91 | [41.941796, "o", "n"]
92 | [42.221781, "o", "p"]
93 | [42.428784, "o", "l"]
94 | [42.605298, "o", "t"]
95 | [42.92677, "o", "\r\n"]
96 | [42.928025, "o", "Active Internet connections (only servers)\r\nProto Recv-Q Send-Q Local Address Foreign Address State PID/Program name \r\n"]
97 | [42.92843, "o", "tcp 0 0 127.0.0.11:38143 0.0.0.0:* LISTEN -\r\ntcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 1/python3\r\n"]
98 | [42.930022, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/ \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[5] 🐳 → \u001b[00m"]
99 | [45.117352, "o", "c"]
100 | [45.221368, "o", "d"]
101 | [45.405684, "o", " "]
102 | [46.012228, "o", "/"]
103 | [46.35764, "o", "m"]
104 | [46.853975, "o", "n"]
105 | [47.27003, "o", "t"]
106 | [47.893594, "o", "/"]
107 | [48.197342, "o", "c"]
108 | [48.517461, "o", "ontainer/"]
109 | [49.553208, "o", "\r\n"]
110 | [49.554821, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/mnt/container \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[6] 🐳 → \u001b[00m"]
111 | [50.349379, "o", "l"]
112 | [50.453424, "o", "s"]
113 | [50.677258, "o", "\r\n"]
114 | [50.678566, "o", "\u001b[1;34mbin\u001b[m \u001b[1;34mdata\u001b[m \u001b[1;34mdev\u001b[m \u001b[1;34metc\u001b[m \u001b[1;34mhome\u001b[m \u001b[1;34mlib\u001b[m \u001b[1;34mmedia\u001b[m \u001b[1;34mmnt\u001b[m \u001b[1;34mopt\u001b[m \u001b[1;34mproc\u001b[m \u001b[1;34mroot\u001b[m \u001b[1;34mrun\u001b[m \u001b[1;34msbin\u001b[m \u001b[1;34msrv\u001b[m \u001b[1;34msys\u001b[m \u001b[1;34mtmp\u001b[m \u001b[1;34musr\u001b[m \u001b[1;34mvar\u001b[m\r\n"]
115 | [50.680391, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/mnt/container \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[7] 🐳 → \u001b[00m"]
116 | [53.300745, "o", "c"]
117 | [53.404927, "o", "d"]
118 | [53.549217, "o", " "]
119 | [56.212738, "o", "d"]
120 | [56.331986, "o", "a"]
121 | [56.652636, "o", "ta/"]
122 | [57.172764, "o", "\r\n"]
123 | [57.174654, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/mnt/container/data \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[8] 🐳 → \u001b[00m"]
124 | [57.771787, "o", "l"]
125 | [57.892737, "o", "s"]
126 | [58.109358, "o", "\r\n"]
127 | [58.110744, "o", "\u001b[1;34mconfig\u001b[m \u001b[1;34mdist\u001b[m \u001b[1;34mform\u001b[m \u001b[1;34mlibrarys\u001b[m \u001b[0;0mmain.py\u001b[m \u001b[0;0mrequirements.txt\u001b[m \u001b[1;34mweb_app\u001b[m\r\n"]
128 | [58.112359, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/mnt/container/data \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[9] 🐳 → \u001b[00m"]
129 | [59.836884, "o", "c"]
130 | [59.899932, "o", "d"]
131 | [60.06095, "o", " "]
132 | [60.388717, "o", "l"]
133 | [60.692446, "o", "i"]
134 | [60.934265, "o", "brarys/"]
135 | [61.450084, "o", "\r\n"]
136 | [61.453691, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/mnt/container/data/librarys \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[10] 🐳 → \u001b[00m"]
137 | [61.900685, "o", "l"]
138 | [62.06051, "o", "s"]
139 | [62.220942, "o", "\r\n"]
140 | [62.232373, "o", "\u001b[1;34mGpZuZBRPsp2u_uZI6jeDXtXqj-2ADcIEcqf9eHhEKNQ=\u001b[m \u001b[1;34mVfZyzbmmK1tJx2fpwurpWI4DcKX2kkDzIfUfEyTy1Iw=\u001b[m \u001b[1;34mqdNIPgFiB1BRvx0jZW8bwmHXZ6ynjj7moTBR6PITbLo=\u001b[m\r\n\u001b[1;34mJRBS50zZitlVj6684twm8aogNfbi12h3f0ACRoA0QgM=\u001b[m \u001b[0;0mdb.json\u001b[m\r\n"]
141 | [62.233232, "o", "\r\r\n\u001b[0;31mroot \u001b[0;35m@ \u001b[0;32m/mnt/container/data/librarys \u001b[00m\u001b[1;32m\r\r\n \u001b[0;34m[11] 🐳 → \u001b[00m"]
142 | [63.844617, "o", "e"]
143 | [64.100896, "o", "x"]
144 | [64.316857, "o", "i"]
145 | [64.45184, "o", "t"]
146 | [64.903205, "o", "\r\nlogout\r\n"]
147 | [65.148382, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
148 | [65.148655, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[JmacbookdeMacBook-Pro% \u001b[K\u001b[?2004h"]
149 | [65.777914, "o", "docker-debug zero-reader_web_1 bash -l"]
150 | [66.345743, "o", "\b"]
151 | [66.846799, "o", "\b"]
152 | [66.930895, "o", "\b"]
153 | [67.014764, "o", "\b"]
154 | [67.098903, "o", "\b"]
155 | [67.182081, "o", "\b"]
156 | [67.266618, "o", "\b"]
157 | [67.35048, "o", "\b"]
158 | [67.434864, "o", "\b"]
159 | [67.518566, "o", "\b"]
160 | [67.60164, "o", "\b"]
161 | [67.685161, "o", "\b"]
162 | [67.768265, "o", "\b"]
163 | [67.851607, "o", "\b"]
164 | [67.935393, "o", "\b"]
165 | [68.019654, "o", "\b"]
166 | [68.10329, "o", "\b"]
167 | [68.187835, "o", "\b"]
168 | [68.272177, "o", "\b"]
169 | [68.356412, "o", "\b"]
170 | [68.440671, "o", "\b"]
171 | [68.525015, "o", "\b"]
172 | [68.609373, "o", "\b"]
173 | [68.693589, "o", "\b"]
174 | [68.777993, "o", "\b"]
175 | [68.861942, "o", "\b"]
176 | [69.3384, "o", "\u001b[1C zero-reader_web_1 bash -l\u001b[26D"]
177 | [69.570466, "o", "- zero-reader_web_1 bash -l\u001b[26D"]
178 | [69.722786, "o", "- zero-reader_web_1 bash -l\u001b[26D"]
179 | [70.218633, "o", "i zero-reader_web_1 bash -l\u001b[26D"]
180 | [70.498645, "o", "m zero-reader_web_1 bash -l\u001b[26D"]
181 | [70.658604, "o", "a zero-reader_web_1 bash -l\u001b[26D"]
182 | [70.811037, "o", "g zero-reader_web_1 bash -l\u001b[26D"]
183 | [70.93841, "o", "e zero-reader_web_1 bash -l\u001b[26D"]
184 | [71.454562, "o", "\u001b[1C zero-reader_web_1 bash -l\u001b[26D"]
185 | [75.2589, "o", "f zero-reader_web_1 bash -l\u001b[26Dr zero-reader_web_1 bash -l\u001b[26D"]
186 | [75.25912, "o", "a zero-reader_web_1 bash -l\u001b[26Dp zero-reader_web_1 bash -l\u001b[26Ds zero-reader_web_1 bash -l\u001b[26Do zero-reader_web_1 bash -l\u001b[26Df zero-reader_web_1 bash -l\u001b[26Dt zero-reader_web_1 bash -l\u001b[26D/ zero-reader_web_1 bash -l\u001b[26Dh zero-reader_web_1 bash -l\u001b[26Dt zero-reader_web_1 bash -l\u001b[26Do zero-reader_web_1 bash -l\u001b[26Dp zero-reader_web_1 bash -l\u001b[26D"]
187 | [76.282136, "o", "\u001b[?2004l\r\r\n"]
188 | [76.933943, "o", "OCI runtime exec failed: exec failed: container_linux.go:344: starting container process caused \"exec: \\\"bash\\\": executable file not found in $PATH\": unknown\r\n"]
189 | [77.128538, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
190 | [77.128734, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[JmacbookdeMacBook-Pro% \u001b[K\u001b[?2004h"]
191 | [78.345297, "o", "docker-debug --image frapsoft/htop zero-reader_web_1 bash -l"]
192 | [78.993857, "o", "\b \b"]
193 | [79.145799, "o", "\b \b"]
194 | [79.297821, "o", "\b"]
195 | [79.457923, "o", "\b \b"]
196 | [79.60981, "o", "\b \b"]
197 | [79.769985, "o", "\b \b"]
198 | [79.97793, "o", "\b \b"]
199 | [80.602244, "o", "h"]
200 | [81.074171, "o", "t"]
201 | [81.290109, "o", "o"]
202 | [81.490016, "o", "p"]
203 | [82.137867, "o", "\u001b[?2004l\r\r\n"]
204 | [82.703812, "o", "\u001b[?1049h\u001b[1;24r\u001b(B\u001b[m\u001b[4l\u001b[?7h\u001b[?1h\u001b=\u001b[?25l\u001b[39;49m\u001b[?1000h"]
205 | [82.785499, "o", "\u001b[39;49m\u001b(B\u001b[m\u001b[H\u001b[2J\u001b[2d \u001b[36m1 \u001b[39m\u001b(B\u001b[0;1m[\u001b[30m\u001b[26X\u001b[2;33H0.0%\u001b[39m]\u001b(B\u001b[m \u001b[36mTasks: \u001b(B\u001b[0;1m\u001b[36m11\u001b(B\u001b[0m\u001b[36m, \u001b(B\u001b[0;1m\u001b[32m83\u001b(B\u001b[0m\u001b[32m thr\u001b[36m; \u001b(B\u001b[0;1m\u001b[32m1\u001b(B\u001b[0m\u001b[36m running\u001b[3;3H2 \u001b[39m\u001b(B\u001b[0;1m[\u001b[30m\u001b[26X\u001b[3;33H0.0%\u001b[39m]\u001b(B\u001b[m \u001b[36mLoad average: \u001b[39m\u001b(B\u001b[0;1m0.12 \u001b[36m0.29 \u001b(B\u001b[0m\u001b[36m0.35 \u001b[4;3H3 \u001b[39m\u001b(B\u001b[0;1m[\u001b[30m\u001b[26X\u001b[4;33H0.0%\u001b[39m]\u001b(B\u001b[m \u001b[36mUptime: \u001b(B\u001b[0;1m\u001b[36m05:46:22\u001b[5;3H\u001b(B\u001b[0m\u001b[36m4 \u001b[39m\u001b(B\u001b[0;1m[\u001b[30m\u001b[26X\u001b[5;33H0.0%\u001b[39m]\u001b[6;3H\u001b(B\u001b[0m\u001b[36mMem\u001b[39m\u001b(B\u001b[0;1m[\u001b(B\u001b[0m\u001b[32m|||||||\u001b[34m||\u001b[33m|||||||||\u001b(B\u001b[0;1m\u001b[30m 427M/1.95G\u001b[39m]\u001b[7;3H\u001b(B\u001b[0m\u001b[36mSwp\u001b[39m\u001b(B\u001b[0;1m[\u001b(B\u001b[0m\u001b[31m|\u001b(B\u001b[0;1m\u001b[30m\u001b[18X\u001b[7;26H5.55M/1024M\u001b[39m]\r\u001b[9d\u001b(B\u001b[0m\u001b[30m\u001b[42m PID USER PRI NI VIRT RES SHR S \u001b[30m\u001b[46mCPU% \u001b[30m\u001b[42mMEM% TIME+ Command \r\u001b[10d\u001b[30m\u001b[46m 1 root 20 0 39704 36424 7024 S 0.0 1.8 0:00.60 python3 ./main.py\u001b[11;4H\u001b[39;49m\u001b(B\u001b[m16 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[3"]
206 | [82.785697, "o", "9m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py\u001b[12;4H\u001b[39m\u001b(B\u001b[m17 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py\u001b[13;4H\u001b[39m\u001b(B\u001b[m18 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py\u001b[14;4H\u001b[39m\u001b(B\u001b[m19 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py\u001b[15;4H\u001b[39m\u001b(B\u001b[m21 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py\u001b[16;4H\u001b[39m\u001b(B\u001b[m22 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py\u001b[17;4H\u001b[39m\u001b(B\u001b[m23 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m"]
207 | [82.785824, "o", "992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py\u001b[18;4H\u001b[39m\u001b(B\u001b[m24 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py\u001b[H\u001b[39m\u001b(B\u001b[m"]
208 | [82.787996, "o", "\u001b[2;7H\u001b[2;33H\u001b(B\u001b[0;1m\u001b[30m\u001b[52X\u001b[2;85H0.0%\u001b[39m]\u001b(B\u001b[m \u001b[36mTasks: \u001b(B\u001b[0;1m\u001b[36m11\u001b(B\u001b[0m\u001b[36m, \u001b(B\u001b[0;1m\u001b[32m83\u001b(B\u001b[0m\u001b[32m thr\u001b[36m; \u001b(B\u001b[0;1m\u001b[32m1\u001b(B\u001b[0m\u001b[36m running\u001b[3;7H\u001b[3;33H\u001b(B\u001b[0;1m\u001b[30m\u001b[52X\u001b[3;85H0.0%\u001b[39m]\u001b(B\u001b[m \u001b[36mLoad average: \u001b[39m\u001b(B\u001b[0;1m0.12 \u001b[36m0.29 \u001b(B\u001b[0m\u001b[36m0.35 \u001b[4;7H\u001b[4;33H\u001b(B\u001b[0;1m\u001b[30m\u001b[52X\u001b[4;85H0.0%\u001b[39m]\u001b(B\u001b[m \u001b[36mUptime: \u001b(B\u001b[0;1m\u001b[36m05:46:22\u001b[5;7H\u001b[5;33H\u001b[30m\u001b[52X\u001b[5;85H0.0%\u001b[39m]\u001b[6;14H\u001b(B\u001b[0m\u001b[32m|||||||||||\u001b[34m||||\u001b[33m||||||||||||||||||||||||\u001b(B\u001b[0;1m\u001b[30m\u001b[26X\u001b[6;79H427M/1.95G\u001b[39m]\u001b[7;8H\u001b[7;26H\u001b[30m\u001b[52X\u001b[7;78H5.55M/1024M\u001b[39m]\u001b[9;81H\u001b(B\u001b[0m\u001b[30m\u001b[42m\u001b[K\u001b[10d\u001b[30m\u001b[46m -w 1\u001b[K\u001b[11;81H\u001b[39;49m\u001b[32m -w 1 \u001b[12;81H -w 1 \u001b[13;81H -w 1 \u001b[14;81H -w 1 \u001b[15;81H -w 1 \u001b[16;81H -w 1 \u001b[17;81H -w 1 \r\u001b[18d\u001b[39m\u001b(B\u001b[mF1\u001b[30m\u001b[46mHelp \u001b[39;49m\u001b(B\u001b[mF2\u001b[30m\u001b[46mSetup \u001b[39;49m\u001b(B\u001b[mF3\u001b[30m\u001b[46mSearch\u001b[39;49m\u001b(B\u001b[mF4\u001b[30m\u001b[46mFilter\u001b[39;49m\u001b(B\u001b[mF5\u001b[30m\u001b[46mTree \u001b[39;49m\u001b(B\u001b[mF6\u001b[30m\u001b[46mSortBy\u001b[39;49m\u001b(B\u001b[mF7\u001b[30m\u001b[46mNice -\u001b[39;49m\u001b(B\u001b[mF8\u001b[30m\u001b[46mNice +\u001b[39;49m\u001b(B\u001b[mF9\u001b"]
209 | [82.788764, "o", "[30m\u001b[46mKill \u001b[39;49m\u001b(B\u001b[mF10\u001b[30m\u001b[46mQuit\u001b[K\u001b[H\u001b[39;49m\u001b(B\u001b[m"]
210 | [84.178954, "o", "\u001b[4;108H\u001b(B\u001b[0;1m\u001b[36m4\r\u001b[10d\u001b[39m\u001b(B\u001b[m 1 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m39\u001b[39m\u001b(B\u001b[m704 \u001b[36m36\u001b[39m\u001b(B\u001b[m424 \u001b[36m 7\u001b[39m\u001b(B\u001b[m024 S 0.0 1.8 0:00.60 python3 ./main.py -w 1\u001b[K\r\u001b[11d\u001b[30m\u001b[46m 16 root 20 0 62768 34992 4152 S 0.0 1.7 0:00.00 python3 ./main.py -w 1\u001b[K\u001b[H\u001b[39;49m\u001b(B\u001b[m"]
211 | [84.454931, "o", "\u001b[3;7H\u001b[32m|\u001b[3;87H\u001b(B\u001b[0;1m\u001b[30m6\r\u001b[11d\u001b[39m\u001b(B\u001b[m 16 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py -w 1 \u001b[39m\u001b(B\u001b[m\u001b[K\r\u001b[12d\u001b[30m\u001b[46m 17 root 20 0 62768 34992 4152 S 0.0 1.7 0:00.00 python3 ./main.py -w 1\u001b[K\u001b[H\u001b[39;49m\u001b(B\u001b[m"]
212 | [84.658438, "o", "\u001b[12d 17 \u001b(B\u001b[0;1m\u001b[30mroot \u001b[39m\u001b(B\u001b[m 20 0 \u001b[36m62\u001b[39m\u001b(B\u001b[m768 \u001b[36m34\u001b[39m\u001b(B\u001b[m992 \u001b[36m 4\u001b[39m\u001b(B\u001b[m152 S 0.0 1.7 0:00.00 \u001b[32mpython3 ./main.py -w 1 \u001b[39m\u001b(B\u001b[m\u001b[K\r\u001b[13d\u001b[30m\u001b[46m 18 root 20 0 62768 34992 4152 S 0.0 1.7 0:00.00 python3 ./main.py -w 1\u001b[K\u001b[H\u001b[39;49m\u001b(B\u001b[m"]
213 | [85.30817, "o", "\u001b[18d\u001b[J\u001b[?12l\u001b[?25h\u001b[?1000l\u001b[18;1H\u001b[?1049l\r\u001b[?1l\u001b>"]
214 | [85.524622, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
215 | [85.524819, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[JmacbookdeMacBook-Pro% \u001b[K\u001b[?2004h"]
216 | [86.274225, "o", "e"]
217 | [86.513819, "o", "\bex"]
218 | [86.722033, "o", "i"]
219 | [86.882104, "o", "t"]
220 | [87.37715, "o", "\u001b[?2004l\r\r\n"]
221 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/zeromake/docker-debug
2 |
3 | go 1.22.0
4 | toolchain go1.24.1
5 |
6 | require (
7 | github.com/BurntSushi/toml v1.4.0
8 | github.com/blang/semver v3.5.1+incompatible
9 | github.com/docker/docker v27.4.1+incompatible
10 | github.com/moby/term v0.5.0
11 | github.com/pkg/errors v0.9.1
12 | github.com/sirupsen/logrus v1.9.3
13 | github.com/spf13/cobra v1.8.1
14 | )
15 |
16 | require (
17 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
18 | github.com/Microsoft/go-winio v0.6.2 // indirect
19 | github.com/containerd/log v0.1.0 // indirect
20 | github.com/distribution/reference v0.6.0 // indirect
21 | github.com/docker/go-connections v0.5.0 // indirect
22 | github.com/docker/go-units v0.5.0 // indirect
23 | github.com/felixge/httpsnoop v1.0.4 // indirect
24 | github.com/go-logr/logr v1.4.2 // indirect
25 | github.com/go-logr/stdr v1.2.2 // indirect
26 | github.com/gogo/protobuf v1.3.2 // indirect
27 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
28 | github.com/moby/docker-image-spec v1.3.1 // indirect
29 | github.com/morikuni/aec v1.0.0 // indirect
30 | github.com/opencontainers/go-digest v1.0.0 // indirect
31 | github.com/opencontainers/image-spec v1.1.0 // indirect
32 | github.com/spf13/pflag v1.0.5 // indirect
33 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect
34 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
35 | go.opentelemetry.io/otel v1.33.0 // indirect
36 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
37 | go.opentelemetry.io/otel/metric v1.33.0 // indirect
38 | go.opentelemetry.io/otel/sdk v1.28.0 // indirect
39 | go.opentelemetry.io/otel/trace v1.33.0 // indirect
40 | golang.org/x/net v0.38.0 // indirect
41 | golang.org/x/sys v0.31.0 // indirect
42 | golang.org/x/time v0.8.0 // indirect
43 | gotest.tools/v3 v3.5.1 // indirect
44 | )
45 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
2 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
3 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
4 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
7 | github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
8 | github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
9 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
10 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
11 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
12 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
13 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
14 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
15 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
20 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
21 | github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
22 | github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
23 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
24 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
25 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
26 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
27 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
28 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
29 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
30 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
31 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
32 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
33 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
34 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
35 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
36 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
37 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
38 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
39 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
40 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
41 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
42 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
43 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
44 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
45 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
46 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
47 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
48 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
49 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
50 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
51 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
52 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
53 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
54 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
55 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
56 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
57 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
58 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
59 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
60 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
61 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
62 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
63 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
64 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
65 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
66 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
68 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
69 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
70 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
71 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
72 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
73 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
74 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
75 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
76 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
77 | go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
78 | go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
79 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
80 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
81 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
82 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
83 | go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
84 | go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
85 | go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
86 | go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
87 | go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
88 | go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
89 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
90 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
91 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
92 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
93 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
94 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
95 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
96 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
97 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
98 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
99 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
100 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
101 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
102 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
103 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
104 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
105 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
106 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
107 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
108 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
109 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
110 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
111 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
112 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
113 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
114 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
115 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
116 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
117 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
118 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
119 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
120 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
121 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
122 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
123 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
124 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
125 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
126 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
127 | google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
128 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
129 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
130 | google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
131 | google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
132 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
133 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
134 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
135 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
136 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
137 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
138 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
139 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
140 |
--------------------------------------------------------------------------------
/internal/command/cli.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "path"
8 | "strings"
9 | "time"
10 |
11 | "github.com/docker/docker/api/types"
12 | "github.com/docker/docker/api/types/container"
13 | "github.com/docker/docker/api/types/events"
14 | "github.com/docker/docker/api/types/filters"
15 | dockerImage "github.com/docker/docker/api/types/image"
16 | "github.com/docker/docker/api/types/mount"
17 | "github.com/docker/docker/api/types/strslice"
18 | "github.com/docker/docker/client"
19 | "github.com/docker/docker/pkg/jsonmessage"
20 | "github.com/moby/term"
21 | "github.com/pkg/errors"
22 | "github.com/sirupsen/logrus"
23 |
24 | "github.com/zeromake/docker-debug/internal/config"
25 | "github.com/zeromake/docker-debug/pkg/opts"
26 | "github.com/zeromake/docker-debug/pkg/stream"
27 | "github.com/zeromake/docker-debug/pkg/tty"
28 | )
29 |
30 | const (
31 | caKey = "ca.pem"
32 | certKey = "cert.pem"
33 | keyKey = "key.pem"
34 |
35 | legacyDefaultDomain = "index.docker.io"
36 | defaultDomain = "docker.io"
37 | officialRepoName = "library"
38 | )
39 |
40 | // DebugCliOption cli option
41 | type DebugCliOption func(cli *DebugCli) error
42 |
43 | // Cli interface
44 | type Cli interface {
45 | Client() client.APIClient
46 | Out() *stream.OutStream
47 | Err() io.Writer
48 | In() *stream.InStream
49 | SetIn(in *stream.InStream)
50 | PullImage(image string) error
51 | FindImage(image string) error
52 | Config() *config.Config
53 | }
54 |
55 | // DebugCli cli struct
56 | type DebugCli struct {
57 | in *stream.InStream
58 | out *stream.OutStream
59 | err io.Writer
60 | client client.APIClient
61 | config *config.Config
62 | ctx context.Context
63 | }
64 |
65 | // NewDebugCli new DebugCli
66 | func NewDebugCli(ctx context.Context, ops ...DebugCliOption) (*DebugCli, error) {
67 | cli := &DebugCli{ctx: ctx}
68 | if err := cli.Apply(ops...); err != nil {
69 | return nil, err
70 | }
71 | if cli.out == nil || cli.in == nil || cli.err == nil {
72 | stdin, stdout, stderr := term.StdStreams()
73 | if cli.in == nil {
74 | cli.in = stream.NewInStream(stdin)
75 | }
76 | if cli.out == nil {
77 | cli.out = stream.NewOutStream(stdout)
78 | }
79 | if cli.err == nil {
80 | cli.err = stderr
81 | }
82 | }
83 | return cli, nil
84 | }
85 |
86 | // Apply all the operation on the cli
87 | func (cli *DebugCli) Apply(ops ...DebugCliOption) error {
88 | for _, op := range ops {
89 | if err := op(cli); err != nil {
90 | return err
91 | }
92 | }
93 | return nil
94 | }
95 |
96 | // WithConfig set config
97 | func WithConfig(config *config.Config) DebugCliOption {
98 | return func(cli *DebugCli) error {
99 | cli.config = config
100 | return nil
101 | }
102 | }
103 |
104 | // WithClientConfig set docker config
105 | func WithClientConfig(dockerConfig *config.DockerConfig) DebugCliOption {
106 | return func(cli *DebugCli) error {
107 | if cli.client != nil {
108 | err := cli.client.Close()
109 | if err != nil {
110 | return errors.WithStack(err)
111 | }
112 | }
113 | var (
114 | host string
115 | err error
116 | )
117 | host, err = opts.ValidateHost(dockerConfig.Host)
118 | if err != nil {
119 | return err
120 | }
121 | clientOpts := []client.Opt{
122 | client.WithHost(host),
123 | client.WithVersion(dockerConfig.Version),
124 | }
125 | if dockerConfig.TLS {
126 | clientOpts = append(clientOpts, client.WithTLSClientConfig(
127 | fmt.Sprintf("%s%s%s", dockerConfig.CertDir, config.PathSeparator, caKey),
128 | fmt.Sprintf("%s%s%s", dockerConfig.CertDir, config.PathSeparator, certKey),
129 | fmt.Sprintf("%s%s%s", dockerConfig.CertDir, config.PathSeparator, keyKey),
130 | ))
131 | }
132 | dockerClient, err := client.NewClientWithOpts(clientOpts...)
133 | if err != nil {
134 | return errors.WithStack(err)
135 | }
136 | cli.client = dockerClient
137 | return nil
138 | }
139 | }
140 |
141 | // UserAgent returns the user agent string used for making API requests
142 |
143 | // Close cli close
144 | func (cli *DebugCli) Close() error {
145 | if cli.client != nil {
146 | return errors.WithStack(cli.client.Close())
147 | }
148 | return nil
149 | }
150 |
151 | // Client returns the APIClient
152 | func (cli *DebugCli) Client() client.APIClient {
153 | return cli.client
154 | }
155 |
156 | // Out returns the writer used for stdout
157 | func (cli *DebugCli) Out() *stream.OutStream {
158 | return cli.out
159 | }
160 |
161 | // Err returns the writer used for stderr
162 | func (cli *DebugCli) Err() io.Writer {
163 | return cli.err
164 | }
165 |
166 | // SetIn sets the reader used for stdin
167 | func (cli *DebugCli) SetIn(in *stream.InStream) {
168 | cli.in = in
169 | }
170 |
171 | // In returns the reader used for stdin
172 | func (cli *DebugCli) In() *stream.InStream {
173 | return cli.in
174 | }
175 |
176 | // Config config
177 | func (cli *DebugCli) Config() *config.Config {
178 | return cli.config
179 | }
180 |
181 | // splitDockerDomain splits a repository name to domain and remotename string.
182 | // If no valid domain is found, the default domain is used. Repository name
183 | // needs to be already validated before.
184 | func splitDockerDomain(name string) (domain, remainder string) {
185 | i := strings.IndexRune(name, '/')
186 | if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
187 | domain, remainder = defaultDomain, name
188 | } else {
189 | domain, remainder = name[:i], name[i+1:]
190 | }
191 | if domain == legacyDefaultDomain {
192 | domain = defaultDomain
193 | }
194 | if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
195 | remainder = officialRepoName + "/" + remainder
196 | }
197 | return
198 | }
199 |
200 | // PullImage pull docker image
201 | func (cli *DebugCli) PullImage(image string) error {
202 | domain, remainder := splitDockerDomain(image)
203 | imageName := path.Join(domain, remainder)
204 |
205 | ctx, cancel := context.WithCancel(cli.ctx)
206 | defer cancel()
207 | responseBody, err := cli.client.ImagePull(ctx, imageName, dockerImage.PullOptions{})
208 | if err != nil {
209 | return errors.WithStack(err)
210 | }
211 | defer func() {
212 | err = responseBody.Close()
213 | if err != nil {
214 | logrus.Debugf("%+v", err)
215 | }
216 | }()
217 | return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.out, nil)
218 | }
219 |
220 | // FindImage find image
221 | func (cli *DebugCli) FindImage(image string) ([]dockerImage.Summary, error) {
222 | args := filters.NewArgs()
223 | args.Add("reference", image)
224 | ctx, cancel := cli.withContent(cli.config.Timeout)
225 | defer cancel()
226 | return cli.client.ImageList(ctx, dockerImage.ListOptions{
227 | Filters: args,
228 | })
229 | }
230 |
231 | // Ping ping docker
232 | func (cli *DebugCli) Ping() (types.Ping, error) {
233 | ctx, cancel := cli.withContent(cli.config.Timeout)
234 | defer cancel()
235 | return cli.client.Ping(ctx)
236 | }
237 |
238 | func (cli *DebugCli) withContent(timeout time.Duration) (context.Context, context.CancelFunc) {
239 | return context.WithTimeout(cli.ctx, timeout)
240 | }
241 |
242 | func containerMode(name string) string {
243 | return fmt.Sprintf("container:%s", name)
244 | }
245 |
246 | // CreateContainer create new container and attach target container resource
247 | func (cli *DebugCli) CreateContainer(attachContainer string, options execOptions) (string, error) {
248 | var mounts []mount.Mount
249 | ctx, cancel := cli.withContent(cli.config.Timeout)
250 | info, err := cli.client.ContainerInspect(ctx, attachContainer)
251 | cancel()
252 | if err != nil {
253 | return "", errors.WithStack(err)
254 | }
255 | if !info.State.Running {
256 | return "", errors.Errorf("container: `%s` is not running", attachContainer)
257 | }
258 | attachContainer = info.ID
259 | mergedDir, ok := info.GraphDriver.Data["MergedDir"]
260 | if !ok || mergedDir == "" {
261 | return "", fmt.Errorf("container: `%s` not found merged dir", attachContainer)
262 | }
263 | if cli.config.MountDir != "" {
264 | mounts = append(mounts, mount.Mount{
265 | Type: "bind",
266 | Source: mergedDir,
267 | Target: cli.config.MountDir,
268 | })
269 | for _, i := range info.Mounts {
270 | var mountType = i.Type
271 | if i.Type == "volume" {
272 | mountType = "bind"
273 | }
274 | mounts = append(mounts, mount.Mount{
275 | Type: mountType,
276 | Source: i.Source,
277 | Target: cli.config.MountDir + i.Destination,
278 | ReadOnly: !i.RW,
279 | })
280 | }
281 | }
282 | if options.volumes != nil {
283 | // -v bind mount
284 | if mounts == nil {
285 | mounts = []mount.Mount{}
286 | }
287 | for _, m := range options.volumes {
288 | mountArgs := strings.Split(m, ":")
289 | mountLen := len(mountArgs)
290 | if mountLen > 0 && mountLen <= 3 {
291 | if strings.HasPrefix(mountArgs[0], "$c/") {
292 | mountArgs[0] = path.Join(mergedDir, mountArgs[0][11:])
293 | }
294 | mountDefault := mount.Mount{
295 | Type: "bind",
296 | ReadOnly: false,
297 | }
298 | switch mountLen {
299 | case 1:
300 | mountDefault.Source = mountArgs[0]
301 | mountDefault.Target = mountArgs[0]
302 | case 2:
303 | if mountArgs[1] == "rw" || mountArgs[1] == "ro" {
304 | mountDefault.ReadOnly = mountArgs[1] != "rw"
305 | mountDefault.Source = mountArgs[0]
306 | mountDefault.Target = mountArgs[0]
307 | } else {
308 | mountDefault.Source = mountArgs[0]
309 | mountDefault.Target = mountArgs[1]
310 | }
311 | case 3:
312 | mountDefault.Source = mountArgs[0]
313 | mountDefault.Target = mountArgs[1]
314 | mountDefault.ReadOnly = mountArgs[2] != "rw"
315 | }
316 | mounts = append(mounts, mountDefault)
317 | }
318 | }
319 | }
320 | targetName := containerMode(attachContainer)
321 |
322 | conf := &container.Config{
323 | Entrypoint: strslice.StrSlice([]string{"/usr/bin/env", "sh"}),
324 | Image: cli.config.Image,
325 | Tty: true,
326 | OpenStdin: true,
327 | StdinOnce: true,
328 | StopSignal: "SIGKILL",
329 | }
330 | hostConfig := &container.HostConfig{
331 | NetworkMode: container.NetworkMode(targetName),
332 | UsernsMode: container.UsernsMode(":" + attachContainer),
333 | PidMode: container.PidMode(targetName),
334 | Mounts: mounts,
335 | SecurityOpt: options.securityOpts,
336 | CapAdd: options.capAdds,
337 | AutoRemove: true,
338 | Privileged: options.privileged,
339 | }
340 |
341 | // default is not use ipc
342 | if options.ipc {
343 | hostConfig.IpcMode = container.IpcMode(targetName)
344 | }
345 | ctx, cancel = cli.withContent(cli.config.Timeout)
346 | body, err := cli.client.ContainerCreate(
347 | ctx,
348 | conf,
349 | hostConfig,
350 | nil,
351 | nil,
352 | "",
353 | )
354 | cancel()
355 | if err != nil {
356 | return "", errors.WithStack(err)
357 | }
358 | ctx, cancel = cli.withContent(cli.config.Timeout)
359 | err = cli.client.ContainerStart(
360 | ctx,
361 | body.ID,
362 | container.StartOptions{},
363 | )
364 | cancel()
365 | return body.ID, errors.WithStack(err)
366 | }
367 |
368 | // ContainerClean stop and remove container
369 | func (cli *DebugCli) ContainerClean(ctx context.Context, id string) error {
370 | ctx, cancel := context.WithTimeout(ctx, time.Second*3)
371 | defer cancel()
372 | var timeout int = 5
373 | return errors.WithStack(cli.client.ContainerStop(
374 | ctx,
375 | id,
376 | container.StopOptions{Timeout: &timeout},
377 | ))
378 | }
379 |
380 | // ExecCreate exec create
381 | func (cli *DebugCli) ExecCreate(options execOptions, containerStr string) (types.IDResponse, error) {
382 | var workDir = options.workDir
383 | if workDir == "" && cli.config.MountDir != "" {
384 | workDir = path.Join(cli.config.MountDir, options.targetDir)
385 | }
386 | h, w := cli.out.GetTtySize()
387 | opt := container.ExecOptions{
388 | User: options.user,
389 | Privileged: options.privileged,
390 | DetachKeys: options.detachKeys,
391 | Tty: true,
392 | AttachStderr: true,
393 | AttachStdin: true,
394 | AttachStdout: true,
395 | WorkingDir: workDir,
396 | Cmd: options.command,
397 | ConsoleSize: &[2]uint{h, w},
398 | }
399 | ctx, cancel := cli.withContent(cli.config.Timeout)
400 | defer cancel()
401 | resp, err := cli.client.ContainerExecCreate(ctx, containerStr, opt)
402 | return resp, errors.WithStack(err)
403 | }
404 |
405 | // ExecStart exec start
406 | func (cli *DebugCli) ExecStart(options execOptions, execID string) error {
407 | h, w := cli.out.GetTtySize()
408 | execConfig := container.ExecStartOptions{
409 | Tty: true,
410 | ConsoleSize: &[2]uint{h, w},
411 | }
412 |
413 | ctx, cancel := cli.withContent(cli.config.Timeout)
414 | defer cancel()
415 | response, err := cli.client.ContainerExecAttach(ctx, execID, execConfig)
416 | if err != nil {
417 | return errors.WithStack(err)
418 | }
419 | defer response.Close()
420 | errCh := make(chan error, 1)
421 | go func() {
422 | defer close(errCh)
423 | streamer := tty.HijackedIOStreamer{
424 | Streams: cli,
425 | InputStream: cli.in,
426 | OutputStream: cli.out,
427 | ErrorStream: cli.err,
428 | Resp: response,
429 | TTY: true,
430 | DetachKeys: options.detachKeys,
431 | }
432 | errCh <- streamer.Stream(cli.ctx)
433 | }()
434 | if err := tty.MonitorTtySize(cli.ctx, cli.client, cli.out, execID, true); err != nil {
435 | _, _ = fmt.Fprintln(cli.err, "Error monitoring TTY size:", err)
436 | }
437 | if err := <-errCh; err != nil {
438 | logrus.Debugf("Error hijack: %s", err)
439 | return err
440 | }
441 | return getExecExitStatus(cli.ctx, cli.client, execID)
442 | }
443 |
444 | // WatchContainer watch container
445 | func (cli *DebugCli) WatchContainer(ctx context.Context, containerID string) error {
446 | subCtx, cancel := context.WithCancel(ctx)
447 | defer cancel()
448 |
449 | filterArgs := filters.NewArgs()
450 | filterArgs.Add("container", containerID)
451 | messages, errs := cli.client.Events(subCtx, events.ListOptions{
452 | Filters: filterArgs,
453 | })
454 |
455 | for {
456 | select {
457 | case event := <-messages:
458 | if event.Type == events.ContainerEventType {
459 | switch event.Action {
460 | case events.ActionDestroy, events.ActionDie, events.ActionKill, events.ActionStop:
461 | return nil
462 | }
463 | }
464 | case err := <-errs:
465 | return err
466 | }
467 | }
468 | }
469 |
470 | func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient, execID string) error {
471 | resp, err := apiClient.ContainerExecInspect(ctx, execID)
472 | if err != nil {
473 | // If we can't connect, then the daemon probably died.
474 | if !client.IsErrConnectionFailed(err) {
475 | return err
476 | }
477 | return errors.Errorf("ExitStatus %d", -1)
478 | }
479 | status := resp.ExitCode
480 | if status != 0 {
481 | return errors.Errorf("ExitStatus %d", status)
482 | }
483 | return nil
484 | }
485 |
486 | // FindContainer find container
487 | func (cli *DebugCli) FindContainer(name string) (string, error) {
488 | return name, nil
489 | }
490 |
--------------------------------------------------------------------------------
/internal/command/config.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "github.com/pkg/errors"
6 | "github.com/spf13/cobra"
7 | "github.com/zeromake/docker-debug/internal/config"
8 | )
9 |
10 | func init() {
11 | cfg := &config.DockerConfig{}
12 | name := ""
13 | cmd := &cobra.Command{
14 | Use: "config",
15 | Short: "docker conn config cli",
16 | Args: RequiresMinArgs(0),
17 | RunE: func(cmd *cobra.Command, args []string) error {
18 | conf, err := config.LoadConfig()
19 | if err != nil {
20 | return err
21 | }
22 | if cfg.Host == "" {
23 | c, ok := conf.DockerConfig[name]
24 | if ok {
25 | fmt.Printf("config `%s`:\n%+v\n", name, c)
26 | return nil
27 | }
28 | return errors.Errorf("not find %s config", name)
29 | }
30 | conf.DockerConfig[name] = cfg
31 | return conf.Save()
32 | },
33 | }
34 | flags := cmd.Flags()
35 | flags.SetInterspersed(false)
36 | flags.StringVarP(&name, "name", "n", "default", "docker config name")
37 | flags.BoolVarP(&cfg.TLS, "tls", "t", false, "docker conn is tls")
38 | flags.StringVarP(&cfg.CertDir, "cert-dir", "c", "", "docker tls cert dir")
39 | flags.StringVarP(&cfg.Host, "host", "H", "", "docker host")
40 | flags.StringVarP(&cfg.CertPassword, "password", "p", "", "docker tls password")
41 | rootCmd.AddCommand(cmd)
42 | }
43 |
--------------------------------------------------------------------------------
/internal/command/info.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "fmt"
5 | "github.com/spf13/cobra"
6 | "github.com/zeromake/docker-debug/version"
7 | )
8 |
9 | func init() {
10 | cmd := &cobra.Command{
11 | Use: "info",
12 | Short: "docker and client info",
13 | Args: RequiresMinArgs(0),
14 | RunE: func(cmd *cobra.Command, args []string) error {
15 | fmt.Printf("Version:\t%s\n", version.Version)
16 | fmt.Printf("Platform:\t%s\n", version.PlatformName)
17 | fmt.Printf("Commit:\t\t%s\n", version.GitCommit)
18 | fmt.Printf("Time:\t\t%s\n", version.BuildTime)
19 | return nil
20 | },
21 | }
22 | rootCmd.AddCommand(cmd)
23 | }
24 |
--------------------------------------------------------------------------------
/internal/command/init.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | "github.com/zeromake/docker-debug/internal/config"
6 | )
7 |
8 | func init() {
9 | cmd := &cobra.Command{
10 | Use: "init",
11 | Short: "docker-debug init config",
12 | Args: RequiresMinArgs(0),
13 | RunE: func(cmd *cobra.Command, args []string) error {
14 | _, err := config.InitConfig()
15 | return err
16 | },
17 | }
18 | rootCmd.AddCommand(cmd)
19 | }
20 |
--------------------------------------------------------------------------------
/internal/command/required.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | // RequiresMinArgs returns an error if there is not at least min args
9 | func RequiresMinArgs(min int) cobra.PositionalArgs {
10 | return func(cmd *cobra.Command, args []string) error {
11 | if len(args) >= min {
12 | return nil
13 | }
14 | return errors.Errorf(
15 | "%q requires at least %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
16 | cmd.CommandPath(),
17 | min,
18 | pluralize("argument", min),
19 | cmd.CommandPath(),
20 | cmd.UseLine(),
21 | cmd.Short,
22 | )
23 | }
24 | }
25 | func pluralize(word string, number int) string {
26 | if number == 1 {
27 | return word
28 | }
29 | return word + "s"
30 | }
31 |
--------------------------------------------------------------------------------
/internal/command/root.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/pkg/errors"
7 | "github.com/sirupsen/logrus"
8 | "github.com/spf13/cobra"
9 |
10 | "github.com/zeromake/docker-debug/internal/config"
11 | )
12 |
13 | var rootCmd = newExecCommand()
14 |
15 | type execOptions struct {
16 | host string
17 | image string
18 | detachKeys string
19 | user string
20 | privileged bool
21 | workDir string
22 | targetDir string
23 | container string
24 | certDir string
25 | command []string
26 | name string
27 | volumes []string
28 | ipc bool
29 | securityOpts []string
30 | capAdds []string
31 | }
32 |
33 | func newExecOptions() execOptions {
34 | return execOptions{}
35 | }
36 |
37 | func newExecCommand() *cobra.Command {
38 | options := newExecOptions()
39 |
40 | cmd := &cobra.Command{
41 | Use: "docker-debug [OPTIONS] CONTAINER COMMAND [ARG...]",
42 | Short: "Run a command in a running container",
43 | Args: RequiresMinArgs(2),
44 | RunE: func(cmd *cobra.Command, args []string) error {
45 | options.container = args[0]
46 | options.command = args[1:]
47 | return runExec(options)
48 | },
49 | }
50 |
51 | flags := cmd.Flags()
52 | flags.SetInterspersed(false)
53 |
54 | flags.StringArrayVarP(&options.volumes, "volume", "v", nil, "Attach a filesystem mount to the container")
55 | flags.StringVarP(&options.image, "image", "i", "", "use this image")
56 | flags.StringVarP(&options.name, "name", "n", "", "docker config name")
57 | flags.StringVarP(&options.host, "host", "H", "", "connection host's docker (format: tcp://192.168.99.100:2376)")
58 | flags.StringVarP(&options.certDir, "cert-dir", "c", "", "cert dir use tls")
59 | flags.StringVarP(&options.detachKeys, "detach-keys", "d", "", "Override the key sequence for detaching a container")
60 | flags.StringVarP(&options.user, "user", "u", "", "Username or UID (format: [:])")
61 | flags.BoolVarP(&options.privileged, "privileged", "p", false, "Give extended privileges to the command")
62 | flags.StringVarP(&options.workDir, "work-dir", "w", "", "Working directory inside the container")
63 | _ = flags.SetAnnotation("work-dir", "version", []string{"1.35"})
64 | flags.StringVarP(&options.targetDir, "target-dir", "t", "", "Working directory inside the container")
65 | flags.StringArrayVarP(&options.securityOpts, "security-opts", "s", nil, "Add security options to the Docker container")
66 | flags.StringArrayVarP(&options.capAdds, "cap-adds", "C", nil, "Add Linux capabilities to the Docker container")
67 | flags.BoolVar(&options.ipc, "ipc", false, "share target container ipc")
68 | return cmd
69 | }
70 |
71 | func buildCli(ctx context.Context, options execOptions) (*DebugCli, error) {
72 | conf, err := config.LoadConfig()
73 | if err != nil {
74 | return nil, err
75 | }
76 | opts := []DebugCliOption{
77 | WithConfig(conf),
78 | }
79 | if options.image != "" {
80 | conf.Image = options.image
81 | }
82 | if conf.Image == "" {
83 | return nil, errors.New("not set image")
84 | }
85 | if options.host != "" {
86 | dockerConfig := &config.DockerConfig{
87 | Host: options.host,
88 | }
89 | if options.certDir != "" {
90 | dockerConfig.TLS = true
91 | dockerConfig.CertDir = options.certDir
92 | }
93 | opts = append(opts, WithClientConfig(dockerConfig))
94 | } else {
95 | name := conf.DockerConfigDefault
96 | if options.name != "" {
97 | name = options.name
98 | }
99 | opt, ok := conf.DockerConfig[name]
100 | if !ok {
101 | return nil, errors.Errorf("not find %s docker config", name)
102 | }
103 | opts = append(opts, WithClientConfig(opt))
104 | }
105 |
106 | return NewDebugCli(ctx, opts...)
107 | }
108 |
109 | func runExec(options execOptions) error {
110 | var ctx, cancel = context.WithCancel(context.Background())
111 | defer cancel()
112 | logrus.SetLevel(logrus.ErrorLevel)
113 |
114 | cli, err := buildCli(ctx, options)
115 | if err != nil {
116 | return err
117 | }
118 | defer cli.Close()
119 |
120 | conf := cli.Config()
121 | // find image
122 | images, err := cli.FindImage(conf.Image)
123 | if err != nil {
124 | return err
125 | }
126 | if len(images) == 0 {
127 | // pull image
128 | err = cli.PullImage(conf.Image)
129 | if err != nil {
130 | return err
131 | }
132 | }
133 |
134 | containerID, err := cli.CreateContainer(options.container, options)
135 | if err != nil {
136 | return err
137 | }
138 | defer cli.ContainerClean(ctx, containerID)
139 |
140 | resp, err := cli.ExecCreate(options, containerID)
141 | if err != nil {
142 | return err
143 | }
144 |
145 | errCh := make(chan error, 1)
146 | defer close(errCh)
147 |
148 | go func() {
149 | errCh <- cli.ExecStart(options, resp.ID)
150 | }()
151 | go func() {
152 | errCh <- cli.WatchContainer(ctx, options.container)
153 | }()
154 |
155 | return <-errCh
156 | }
157 |
158 | // Execute main func
159 | func Execute() {
160 | if err := rootCmd.Execute(); err != nil {
161 | logrus.Debugf("%+v", err)
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/internal/command/use.go:
--------------------------------------------------------------------------------
1 | package command
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | "github.com/spf13/cobra"
6 | "github.com/zeromake/docker-debug/internal/config"
7 | )
8 |
9 | func init() {
10 | cmd := &cobra.Command{
11 | Use: "use",
12 | Short: "docker set default config",
13 | Args: RequiresMinArgs(1),
14 | RunE: func(cmd *cobra.Command, args []string) error {
15 | conf, err := config.LoadConfig()
16 | if err != nil {
17 | return err
18 | }
19 | name := args[0]
20 | _, ok := conf.DockerConfig[name]
21 | if !ok {
22 | return errors.Errorf("not find %s config", name)
23 | }
24 | conf.DockerConfigDefault = name
25 | return conf.Save()
26 | },
27 | }
28 | rootCmd.AddCommand(cmd)
29 | }
30 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | "github.com/BurntSushi/toml"
10 | "github.com/docker/docker/api"
11 | "github.com/pkg/errors"
12 | "github.com/zeromake/docker-debug/pkg/opts"
13 | "github.com/zeromake/docker-debug/version"
14 | )
15 |
16 | var configDir = ".docker-debug"
17 |
18 | var configName = "config.toml"
19 |
20 | // PathSeparator path separator
21 | var PathSeparator = string(os.PathSeparator)
22 |
23 | // File 默认配置文件
24 | var File = fmt.Sprintf(
25 | "~%s%s%s%s",
26 | PathSeparator,
27 | configDir,
28 | PathSeparator,
29 | configName,
30 | )
31 |
32 | func init() {
33 | var (
34 | home string
35 | err error
36 | )
37 | home, err = os.UserHomeDir()
38 | if err != nil {
39 | return
40 | }
41 | //HOME = home
42 | configDir = fmt.Sprintf("%s%s%s", home, PathSeparator, configDir)
43 | File = fmt.Sprintf("%s%s%s", configDir, PathSeparator, configName)
44 | }
45 |
46 | // DockerConfig docker 配置
47 | type DockerConfig struct {
48 | Version string `toml:"version"`
49 | Host string `toml:"host"`
50 | TLS bool `toml:"tls"`
51 | CertDir string `toml:"cert_dir"`
52 | CertPassword string `toml:"cert_password"`
53 | }
54 |
55 | func (c DockerConfig) String() string {
56 | s, _ := json.MarshalIndent(&c, "", " ")
57 | return string(s)
58 | }
59 |
60 | // Config 配置
61 | type Config struct {
62 | Version string `toml:"version"`
63 | MountDir string `toml:"mount_dir"`
64 | Image string `toml:"image"`
65 | Timeout time.Duration `toml:"timeout"`
66 | DockerConfigDefault string `toml:"config_default"`
67 | DockerConfig map[string]*DockerConfig `toml:"config"`
68 | ReadTimeout time.Duration `toml:"read_timeout"`
69 | }
70 |
71 | // Save to default file
72 | func (c *Config) Save() error {
73 | file, err := os.OpenFile(File, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
74 | if err != nil {
75 | return errors.WithStack(err)
76 | }
77 | encoder := toml.NewEncoder(file)
78 | defer func() {
79 | _ = file.Close()
80 | }()
81 | return encoder.Encode(c)
82 | }
83 |
84 | // Load reload default file
85 | func (c *Config) Load() error {
86 | _, err := toml.DecodeFile(File, c)
87 | return errors.WithStack(err)
88 | }
89 |
90 | // PathExists path is has
91 | func PathExists(path string) bool {
92 | _, err := os.Stat(path)
93 | if err == nil {
94 | return true
95 | }
96 | if os.IsNotExist(err) {
97 | return false
98 | }
99 | return false
100 | }
101 |
102 | // LoadConfig load default file(not has init file)
103 | func LoadConfig() (*Config, error) {
104 | if !PathExists(File) {
105 | return InitConfig()
106 | }
107 | config := &Config{}
108 | _, err := toml.DecodeFile(File, config)
109 | if err != nil {
110 | return nil, errors.WithStack(err)
111 | }
112 | err = MigrationConfig(config)
113 | return config, err
114 | }
115 |
116 | // InitConfig init create file
117 | func InitConfig() (*Config, error) {
118 | host := os.Getenv("DOCKER_HOST")
119 | tlsVerify := os.Getenv("DOCKER_TLS_VERIFY") == "1"
120 | host, err := opts.ParseHost(tlsVerify, host)
121 | if err != nil {
122 | return nil, errors.WithStack(err)
123 | }
124 | if !PathExists(configDir) {
125 | err = os.Mkdir(configDir, 0755)
126 | if err != nil {
127 | return nil, errors.WithStack(err)
128 | }
129 | }
130 | dc := DockerConfig{
131 | Host: host,
132 | Version: api.DefaultVersion,
133 | }
134 | certPath := os.Getenv("DOCKER_CERT_PATH")
135 | if tlsVerify && certPath != "" {
136 | dc.TLS = true
137 | dc.CertDir = certPath
138 | }
139 | config := &Config{
140 | Version: version.Version,
141 | Image: "nicolaka/netshoot:latest",
142 | Timeout: time.Second * 10,
143 | MountDir: "/mnt/container",
144 | DockerConfigDefault: "default",
145 | DockerConfig: map[string]*DockerConfig{
146 | "default": &dc,
147 | },
148 | ReadTimeout: time.Second * 3,
149 | }
150 | file, err := os.OpenFile(File, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
151 | if err != nil {
152 | return nil, errors.WithStack(err)
153 | }
154 | encoder := toml.NewEncoder(file)
155 | defer file.Close()
156 | return config, encoder.Encode(config)
157 | }
158 |
--------------------------------------------------------------------------------
/internal/config/migration.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/blang/semver"
5 | "github.com/pkg/errors"
6 | "github.com/zeromake/docker-debug/version"
7 | "sort"
8 | "strings"
9 | )
10 |
11 | type migration struct {
12 | Up func(*Config) error
13 | Version semver.Version
14 | }
15 |
16 | var migrationArr []*migration
17 |
18 | // MigrationConfig migration config version
19 | func MigrationConfig(conf *Config) error {
20 | ver1 := version.Version
21 | var flag bool
22 | if strings.HasPrefix(ver1, "v") {
23 | ver1 = ver1[1:]
24 | }
25 | v1, err := semver.Parse(ver1)
26 | if err != nil {
27 | return nil
28 | }
29 | ver2 := conf.Version
30 | if strings.HasPrefix(ver2, "v") {
31 | ver2 = ver2[1:]
32 | }
33 | v2, err := semver.Parse(ver2)
34 | if err != nil {
35 | return errors.WithStack(err)
36 | }
37 | if strings.HasSuffix(conf.MountDir, "/") {
38 | flag = true
39 | l := len(conf.MountDir)
40 | conf.MountDir = conf.MountDir[:l-1]
41 | }
42 | if v2.LT(v1) {
43 | sort.Slice(migrationArr, func(i, j int) bool {
44 | return migrationArr[i].Version.LT(migrationArr[j].Version)
45 | })
46 | for _, m := range migrationArr {
47 | if v2.LT(m.Version) {
48 | err = m.Up(conf)
49 | if err != nil {
50 | return err
51 | }
52 | }
53 | }
54 | conf.Version = ver1
55 | return conf.Save()
56 | }
57 | if flag {
58 | return conf.Save()
59 | }
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/internal/config/version-0.2.1.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/blang/semver"
5 | )
6 |
7 | // Up000201 update version 0.2.1
8 | func Up000201(conf *Config) error {
9 | if conf.MountDir == "" {
10 | conf.MountDir = "/mnt/container"
11 | }
12 | return nil
13 | }
14 |
15 | func init() {
16 | v, err := semver.Parse("0.2.1")
17 | if err != nil {
18 | return
19 | }
20 | migrationArr = append(migrationArr, &migration{
21 | Up: Up000201,
22 | Version: v,
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/internal/config/version-0.7.10.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/blang/semver"
5 | "github.com/docker/docker/api"
6 | )
7 |
8 | // Up000710 update version 0.7.10
9 | func Up000710(conf *Config) error {
10 | for _, c := range conf.DockerConfig {
11 | // 强制切换为 1.40
12 | if c.Version == "" || c.Version == api.DefaultVersion {
13 | c.Version = "1.40"
14 | }
15 | }
16 | return nil
17 | }
18 |
19 | func init() {
20 | v, err := semver.Parse("0.7.10")
21 | if err != nil {
22 | return
23 | }
24 | migrationArr = append(migrationArr, &migration{
25 | Up: Up000710,
26 | Version: v,
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/internal/config/version-0.7.2.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/blang/semver"
5 | "github.com/docker/docker/api"
6 | )
7 |
8 | // Up000702 update version 0.7.2
9 | func Up000702(conf *Config) error {
10 | for _, c := range conf.DockerConfig {
11 | if c.Version == "" {
12 | c.Version = api.DefaultVersion
13 | }
14 | }
15 | return nil
16 | }
17 |
18 | func init() {
19 | v, err := semver.Parse("0.7.2")
20 | if err != nil {
21 | return
22 | }
23 | migrationArr = append(migrationArr, &migration{
24 | Up: Up000702,
25 | Version: v,
26 | })
27 | }
28 |
--------------------------------------------------------------------------------
/internal/config/version-0.7.6.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/blang/semver"
5 | "time"
6 | )
7 |
8 | // Up000706 update version 0.7.6
9 | func Up000706(conf *Config) error {
10 | if conf.ReadTimeout == 0 {
11 | conf.ReadTimeout = time.Second * 3
12 | }
13 | return nil
14 | }
15 |
16 | func init() {
17 | v, err := semver.Parse("0.7.6")
18 | if err != nil {
19 | return
20 | }
21 | migrationArr = append(migrationArr, &migration{
22 | Up: Up000706,
23 | Version: v,
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/opts/hosts.go:
--------------------------------------------------------------------------------
1 | package opts
2 |
3 | import (
4 | "fmt"
5 | "github.com/pkg/errors"
6 | "net"
7 | "net/url"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | var (
13 | // DefaultHTTPPort Default HTTP Port used if only the protocol is provided to -H flag e.g. dockerd -H tcp://
14 | // These are the IANA registered port numbers for use with Docker
15 | // see http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=docker
16 | DefaultHTTPPort = 2375 // Default HTTP Port
17 | // DefaultTLSHTTPPort Default HTTP Port used when TLS enabled
18 | DefaultTLSHTTPPort = 2376 // Default TLS encrypted HTTP Port
19 | // DefaultUnixSocket Path for the unix socket.
20 | // Docker daemon by default always listens on the default unix socket
21 | DefaultUnixSocket = "/var/run/docker.sock"
22 | // DefaultTCPHost constant defines the default host string used by docker on Windows
23 | DefaultTCPHost = fmt.Sprintf("tcp://%s:%d", DefaultHTTPHost, DefaultHTTPPort)
24 | // DefaultTLSHost constant defines the default host string used by docker for TLS sockets
25 | DefaultTLSHost = fmt.Sprintf("tcp://%s:%d", DefaultHTTPHost, DefaultTLSHTTPPort)
26 | // DefaultNamedPipe defines the default named pipe used by docker on Windows
27 | DefaultNamedPipe = `//./pipe/docker_engine`
28 | )
29 |
30 | // ValidateHost validates that the specified string is a valid host and returns it.
31 | func ValidateHost(val string) (string, error) {
32 | host := strings.TrimSpace(val)
33 | // The empty string means default and is not handled by parseDockerDaemonHost
34 | if host != "" {
35 | _, err := parseDockerDaemonHost(host)
36 | if err != nil {
37 | return val, err
38 | }
39 | }
40 | // Note: unlike most flag validators, we don't return the mutated value here
41 | // we need to know what the user entered later (using ParseHost) to adjust for TLS
42 | return val, nil
43 | }
44 |
45 | // ParseHost and set defaults for a Daemon host string
46 | func ParseHost(defaultToTLS bool, val string) (string, error) {
47 | host := strings.TrimSpace(val)
48 | if host == "" {
49 | if defaultToTLS {
50 | host = DefaultTLSHost
51 | } else {
52 | host = DefaultHost
53 | }
54 | } else {
55 | var err error
56 | host, err = parseDockerDaemonHost(host)
57 | if err != nil {
58 | return val, err
59 | }
60 | }
61 | return host, nil
62 | }
63 |
64 | // parseDockerDaemonHost parses the specified address and returns an address that will be used as the host.
65 | // Depending of the address specified, this may return one of the global Default* strings defined in hosts.go.
66 | func parseDockerDaemonHost(addr string) (string, error) {
67 | addrParts := strings.SplitN(addr, "://", 2)
68 | if len(addrParts) == 1 && addrParts[0] != "" {
69 | addrParts = []string{"tcp", addrParts[0]}
70 | }
71 |
72 | switch addrParts[0] {
73 | case "tcp":
74 | return ParseTCPAddr(addrParts[1], DefaultTCPHost)
75 | case "unix":
76 | return parseSimpleProtoAddr("unix", addrParts[1], DefaultUnixSocket)
77 | case "npipe":
78 | return parseSimpleProtoAddr("npipe", addrParts[1], DefaultNamedPipe)
79 | case "fd":
80 | return addr, nil
81 | case "ssh":
82 | return addr, nil
83 | default:
84 | return "", errors.Errorf("Invalid bind address format: %s", addr)
85 | }
86 | }
87 |
88 | // parseSimpleProtoAddr parses and validates that the specified address is a valid
89 | // socket address for simple protocols like unix and npipe. It returns a formatted
90 | // socket address, either using the address parsed from addr, or the contents of
91 | // defaultAddr if addr is a blank string.
92 | func parseSimpleProtoAddr(proto, addr, defaultAddr string) (string, error) {
93 | addr = strings.TrimPrefix(addr, proto+"://")
94 | if strings.Contains(addr, "://") {
95 | return "", errors.Errorf("Invalid proto, expected %s: %s", proto, addr)
96 | }
97 | if addr == "" {
98 | addr = defaultAddr
99 | }
100 | return fmt.Sprintf("%s://%s", proto, addr), nil
101 | }
102 |
103 | // ParseTCPAddr parses and validates that the specified address is a valid TCP
104 | // address. It returns a formatted TCP address, either using the address parsed
105 | // from tryAddr, or the contents of defaultAddr if tryAddr is a blank string.
106 | // tryAddr is expected to have already been Trim()'d
107 | // defaultAddr must be in the full `tcp://host:port` form
108 | func ParseTCPAddr(tryAddr string, defaultAddr string) (string, error) {
109 | if tryAddr == "" || tryAddr == "tcp://" {
110 | return defaultAddr, nil
111 | }
112 | addr := strings.TrimPrefix(tryAddr, "tcp://")
113 | if strings.Contains(addr, "://") || addr == "" {
114 | return "", errors.Errorf("Invalid proto, expected tcp: %s", tryAddr)
115 | }
116 |
117 | defaultAddr = strings.TrimPrefix(defaultAddr, "tcp://")
118 | defaultHost, defaultPort, err := net.SplitHostPort(defaultAddr)
119 | if err != nil {
120 | return "", errors.WithStack(err)
121 | }
122 | // url.Parse fails for trailing colon on IPv6 brackets on Go 1.5, but
123 | // not 1.4. See https://github.com/golang/go/issues/12200 and
124 | // https://github.com/golang/go/issues/6530.
125 | if strings.HasSuffix(addr, "]:") {
126 | addr += defaultPort
127 | }
128 |
129 | u, err := url.Parse("tcp://" + addr)
130 | if err != nil {
131 | return "", errors.WithStack(err)
132 | }
133 | host, port, err := net.SplitHostPort(u.Host)
134 | if err != nil {
135 | // try port addition once
136 | host, port, err = net.SplitHostPort(net.JoinHostPort(u.Host, defaultPort))
137 | }
138 | if err != nil {
139 | return "", fmt.Errorf("Invalid bind address format: %s", tryAddr)
140 | }
141 |
142 | if host == "" {
143 | host = defaultHost
144 | }
145 | if port == "" {
146 | port = defaultPort
147 | }
148 | p, err := strconv.Atoi(port)
149 | if err != nil && p == 0 {
150 | return "", errors.Errorf("Invalid bind address format: %s", tryAddr)
151 | }
152 |
153 | return fmt.Sprintf("tcp://%s%s", net.JoinHostPort(host, port), u.Path), nil
154 | }
155 |
156 | // ValidateExtraHost validates that the specified string is a valid extrahost and returns it.
157 | // ExtraHost is in the form of name:ip where the ip has to be a valid ip (IPv4 or IPv6).
158 | // func ValidateExtraHost(val string) (string, error) {
159 | // // allow for IPv6 addresses in extra hosts by only splitting on first ":"
160 | // arr := strings.SplitN(val, ":", 2)
161 | // if len(arr) != 2 || len(arr[0]) == 0 {
162 | // return "", fmt.Errorf("bad format for add-host: %q", val)
163 | // }
164 | // if _, err := ValidateIPAddress(arr[1]); err != nil {
165 | // return "", fmt.Errorf("invalid IP address in add-host: %q", arr[1])
166 | // }
167 | // return val, nil
168 | // }
169 |
--------------------------------------------------------------------------------
/pkg/opts/hosts_unix.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package opts
4 |
5 | import (
6 | "fmt"
7 | )
8 |
9 | // DefaultHost constant defines the default host string used by docker on other hosts than Windows
10 | var DefaultHost = fmt.Sprintf("unix://%s", DefaultUnixSocket)
11 |
12 | // DefaultHTTPHost http host
13 | const DefaultHTTPHost = "localhost"
14 |
--------------------------------------------------------------------------------
/pkg/opts/hosts_windows.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package opts
4 |
5 | // DefaultHost constant defines the default host string used by docker on Windows
6 | var DefaultHost = "npipe://" + DefaultNamedPipe
7 | var DefaultHTTPHost = "127.0.0.1"
8 |
--------------------------------------------------------------------------------
/pkg/stream/in.go:
--------------------------------------------------------------------------------
1 | package stream
2 |
3 | import (
4 | "io"
5 | "os"
6 | "runtime"
7 |
8 | "github.com/moby/term"
9 | "github.com/pkg/errors"
10 | )
11 |
12 | // InStream is an input stream used by the DockerCli to read user input
13 | type InStream struct {
14 | CommonStream
15 | in io.ReadCloser
16 | }
17 |
18 | func (i *InStream) Read(p []byte) (int, error) {
19 | return i.in.Read(p)
20 | }
21 |
22 | // Close implements the Closer interface
23 | func (i *InStream) Close() error {
24 | return i.in.Close()
25 | }
26 |
27 | // SetRawTerminal sets raw mode on the input terminal
28 | func (i *InStream) SetRawTerminal() (err error) {
29 | if os.Getenv("NORAW") != "" || !i.CommonStream.isTerminal {
30 | return nil
31 | }
32 | i.CommonStream.state, err = term.SetRawTerminal(i.CommonStream.fd)
33 | return err
34 | }
35 |
36 | // CheckTty checks if we are trying to attach to a container tty
37 | // from a non-tty client input stream, and if so, returns an error.
38 | func (i *InStream) CheckTty(attachStdin, ttyMode bool) error {
39 | // In order to attach to a container tty, input stream for the client must
40 | // be a tty itself: redirecting or piping the client standard input is
41 | // incompatible with `docker run -t`, `docker exec -t` or `docker attach`.
42 | if ttyMode && attachStdin && !i.isTerminal {
43 | eText := "the input device is not a TTY"
44 | if runtime.GOOS == "windows" {
45 | return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'")
46 | }
47 | return errors.New(eText)
48 | }
49 | return nil
50 | }
51 |
52 | // NewInStream returns a new InStream object from a ReadCloser
53 | func NewInStream(in io.ReadCloser) *InStream {
54 | fd, isTerminal := term.GetFdInfo(in)
55 | return &InStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, in: in}
56 | }
57 |
--------------------------------------------------------------------------------
/pkg/stream/out.go:
--------------------------------------------------------------------------------
1 | package stream
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 |
8 | "github.com/moby/term"
9 | "github.com/pkg/errors"
10 | )
11 |
12 | // OutStream is an output stream used by the DockerCli to write normal program
13 | // output.
14 | type OutStream struct {
15 | CommonStream
16 | out io.Writer
17 | }
18 |
19 | func (o *OutStream) Write(p []byte) (int, error) {
20 | return o.out.Write(p)
21 | }
22 |
23 | // SetRawTerminal sets raw mode on the input terminal
24 | func (o *OutStream) SetRawTerminal() (err error) {
25 | if os.Getenv("NORAW") != "" || !o.CommonStream.isTerminal {
26 | return nil
27 | }
28 | o.CommonStream.state, err = term.SetRawTerminalOutput(o.CommonStream.fd)
29 | return err
30 | }
31 |
32 | // GetTtySize returns the height and width in characters of the tty
33 | func (o *OutStream) GetTtySize() (uint, uint) {
34 | if !o.isTerminal {
35 | return 0, 0
36 | }
37 | ws, err := term.GetWinsize(o.fd)
38 | if err != nil {
39 | fmt.Println(errors.Errorf("Error getting size: %s", err))
40 | // logrus.Debugf("Error getting size: %s", err)
41 | if ws == nil {
42 | return 0, 0
43 | }
44 | }
45 | return uint(ws.Height), uint(ws.Width)
46 | }
47 |
48 | // NewOutStream returns a new OutStream object from a Writer
49 | func NewOutStream(out io.Writer) *OutStream {
50 | fd, isTerminal := term.GetFdInfo(out)
51 | return &OutStream{
52 | CommonStream: CommonStream{
53 | fd: fd,
54 | isTerminal: isTerminal,
55 | },
56 | out: out,
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/stream/stream.go:
--------------------------------------------------------------------------------
1 | package stream
2 |
3 | import "github.com/moby/term"
4 |
5 | // Streams interface
6 | type Streams interface {
7 | Out() *OutStream
8 | In() *InStream
9 | }
10 |
11 | // CommonStream is an input stream used by the DockerCli to read user input
12 | type CommonStream struct {
13 | fd uintptr
14 | isTerminal bool
15 | state *term.State
16 | }
17 |
18 | // FD returns the file descriptor number for this stream
19 | func (s *CommonStream) FD() uintptr {
20 | return s.fd
21 | }
22 |
23 | // IsTerminal returns true if this stream is connected to a terminal
24 | func (s *CommonStream) IsTerminal() bool {
25 | return s.isTerminal
26 | }
27 |
28 | // RestoreTerminal restores normal mode to the terminal
29 | func (s *CommonStream) RestoreTerminal() error {
30 | if s.state != nil {
31 | return term.RestoreTerminal(s.fd, s.state)
32 | }
33 | return nil
34 | }
35 |
36 | // SetIsTerminal sets the boolean used for isTerminal
37 | func (s *CommonStream) SetIsTerminal(isTerminal bool) {
38 | s.isTerminal = isTerminal
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/tty/hijack.go:
--------------------------------------------------------------------------------
1 | package tty
2 |
3 | import (
4 | "context"
5 | "github.com/docker/docker/api/types"
6 | "github.com/docker/docker/pkg/ioutils"
7 | "github.com/docker/docker/pkg/stdcopy"
8 | "github.com/moby/term"
9 | "github.com/pkg/errors"
10 | "github.com/sirupsen/logrus"
11 | "github.com/zeromake/docker-debug/pkg/stream"
12 | "io"
13 | "runtime"
14 | "sync"
15 | )
16 |
17 | // The default escape key sequence: ctrl-p, ctrl-q
18 | // TODO: This could be moved to `pkg/term`.
19 | var defaultEscapeKeys = []byte{16, 17}
20 |
21 | // HijackedIOStreamer handles copying input to and output from streams to the
22 | // connection.
23 | type HijackedIOStreamer struct {
24 | Streams stream.Streams
25 | InputStream io.ReadCloser
26 | OutputStream io.Writer
27 | ErrorStream io.Writer
28 |
29 | Resp types.HijackedResponse
30 |
31 | TTY bool
32 | DetachKeys string
33 | }
34 |
35 | // Stream handles setting up the IO and then begins streaming stdin/stdout
36 | // to/from the hijacked connection, blocking until it is either done reading
37 | // output, the user inputs the detach key sequence when in TTY mode, or when
38 | // the given context is cancelled.
39 | func (h *HijackedIOStreamer) Stream(ctx context.Context) error {
40 | restoreInput, err := h.setupInput()
41 | if err != nil {
42 | return errors.Errorf("unable to setup input stream: %s", err)
43 | }
44 |
45 | defer restoreInput()
46 | defer h.Resp.Close()
47 | outputDone := h.beginOutputStream(restoreInput)
48 | inputDone, detached := h.beginInputStream(restoreInput)
49 |
50 | select {
51 | case err = <-outputDone:
52 | return errors.WithStack(err)
53 | case <-inputDone:
54 | // Input stream has closed.
55 | if h.OutputStream != nil || h.ErrorStream != nil {
56 | // Wait for output to complete streaming.
57 | select {
58 | case err = <-outputDone:
59 | return errors.WithStack(err)
60 | case <-ctx.Done():
61 | return errors.WithStack(ctx.Err())
62 | }
63 | }
64 | return nil
65 | case err = <-detached:
66 | // Got a detach key sequence.
67 | return errors.WithStack(err)
68 | case <-ctx.Done():
69 | return errors.WithStack(ctx.Err())
70 | }
71 | }
72 |
73 | func (h *HijackedIOStreamer) setupInput() (restore func(), err error) {
74 | if h.InputStream == nil || !h.TTY {
75 | // No need to setup input TTY.
76 | // The restore func is a nop.
77 | return func() {}, nil
78 | }
79 |
80 | if err := setRawTerminal(h.Streams); err != nil {
81 | return nil, errors.Errorf("unable to set IO streams as raw terminal: %s", err)
82 | }
83 |
84 | // Use sync.Once so we may call restore multiple times but ensure we
85 | // only restore the terminal once.
86 | var restoreOnce sync.Once
87 | restore = func() {
88 | restoreOnce.Do(func() {
89 | _ = restoreTerminal(h.Streams, h.InputStream)
90 | })
91 | }
92 |
93 | // Wrap the input to detect detach escape sequence.
94 | // Use default escape keys if an invalid sequence is given.
95 | escapeKeys := defaultEscapeKeys
96 | if h.DetachKeys != "" {
97 | customEscapeKeys, err := term.ToBytes(h.DetachKeys)
98 | if err != nil {
99 | logrus.Warnf("invalid detach escape keys, using default: %s", err)
100 | } else {
101 | escapeKeys = customEscapeKeys
102 | }
103 | }
104 |
105 | h.InputStream = ioutils.NewReadCloserWrapper(
106 | term.NewEscapeProxy(h.InputStream, escapeKeys),
107 | h.InputStream.Close,
108 | )
109 |
110 | return restore, nil
111 | }
112 |
113 | func (h *HijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
114 | if h.OutputStream == nil && h.ErrorStream == nil {
115 | // There is no need to copy output.
116 | return nil
117 | }
118 |
119 | outputDone := make(chan error)
120 | go func() {
121 | var err error
122 |
123 | // When TTY is ON, use regular copy
124 | if h.OutputStream != nil && h.TTY {
125 | _, err = io.Copy(h.OutputStream, h.Resp.Reader)
126 | restoreInput()
127 | } else {
128 | _, err = stdcopy.StdCopy(h.OutputStream, h.ErrorStream, h.Resp.Reader)
129 | }
130 |
131 | logrus.Debug("[hijack] End of stdout")
132 |
133 | if err != nil {
134 | logrus.Debugf("Error receiveStdout: %s", err)
135 | }
136 |
137 | outputDone <- errors.WithStack(err)
138 | }()
139 |
140 | return outputDone
141 | }
142 |
143 | func (h *HijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan struct{}, detachedC <-chan error) {
144 | inputDone := make(chan struct{})
145 | detached := make(chan error)
146 |
147 | go func() {
148 | if h.InputStream != nil {
149 | _, err := io.Copy(h.Resp.Conn, h.InputStream)
150 | restoreInput()
151 |
152 | logrus.Debug("[hijack] End of stdin")
153 |
154 | if _, ok := err.(term.EscapeError); ok {
155 | detached <- errors.WithStack(err)
156 | return
157 | }
158 |
159 | if err != nil {
160 | logrus.Debugf("Error sendStdin: %s", err)
161 | }
162 | }
163 |
164 | if err := h.Resp.CloseWrite(); err != nil {
165 | logrus.Debugf("Couldn't send EOF: %s", err)
166 | }
167 |
168 | close(inputDone)
169 | }()
170 |
171 | return inputDone, detached
172 | }
173 |
174 | func setRawTerminal(streams stream.Streams) error {
175 | if err := streams.In().SetRawTerminal(); err != nil {
176 | return errors.WithStack(err)
177 | }
178 | return streams.Out().SetRawTerminal()
179 | }
180 |
181 | func restoreTerminal(streams stream.Streams, in io.Closer) error {
182 | _ = streams.In().RestoreTerminal()
183 | _ = streams.Out().RestoreTerminal()
184 | if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
185 | return in.Close()
186 | }
187 | return nil
188 | }
189 |
--------------------------------------------------------------------------------
/pkg/tty/tty.go:
--------------------------------------------------------------------------------
1 | package tty
2 |
3 | import (
4 | "context"
5 | "os"
6 | goSignal "os/signal"
7 | "runtime"
8 | "syscall"
9 | "time"
10 |
11 | "github.com/sirupsen/logrus"
12 |
13 | "github.com/docker/docker/api/types/container"
14 | "github.com/docker/docker/client"
15 | "github.com/zeromake/docker-debug/pkg/stream"
16 | )
17 |
18 | // ResizeTtyTo re sizes tty to specific height and width
19 | func ResizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width uint, isExec bool) {
20 | if height == 0 && width == 0 {
21 | return
22 | }
23 |
24 | options := container.ResizeOptions{
25 | Height: height,
26 | Width: width,
27 | }
28 |
29 | var err error
30 | if isExec {
31 | err = client.ContainerExecResize(ctx, id, options)
32 | } else {
33 | err = client.ContainerResize(ctx, id, options)
34 | }
35 |
36 | if err != nil {
37 | logrus.Debugf("Error resize: %s", err)
38 | }
39 | }
40 |
41 | // MonitorTtySize updates the container tty size when the terminal tty changes size
42 | func MonitorTtySize(ctx context.Context, client client.ContainerAPIClient, out *stream.OutStream, id string, isExec bool) error {
43 | resizeTty := func() {
44 | height, width := out.GetTtySize()
45 | ResizeTtyTo(ctx, client, id, height, width, isExec)
46 | }
47 |
48 | resizeTty()
49 |
50 | if runtime.GOOS == "windows" {
51 | go func() {
52 | prevH, prevW := out.GetTtySize()
53 | for {
54 | time.Sleep(time.Millisecond * 250)
55 | h, w := out.GetTtySize()
56 |
57 | if prevW != w || prevH != h {
58 | resizeTty()
59 | }
60 | prevH = h
61 | prevW = w
62 | }
63 | }()
64 | } else {
65 | sigChan := make(chan os.Signal, 1)
66 | goSignal.Notify(sigChan, syscall.Signal(0x1c))
67 | go func() {
68 | for range sigChan {
69 | resizeTty()
70 | }
71 | }()
72 | }
73 | return nil
74 | }
75 |
--------------------------------------------------------------------------------
/scripts/binary.sh:
--------------------------------------------------------------------------------
1 | #!/bin/env bash
2 | #
3 | # Build a static binary for the host OS/ARCH
4 | #
5 |
6 | OS=$1
7 | source ./scripts/variables.env
8 | if [[ -n $OS ]]; then
9 | source ./scripts/variables.${OS}.env
10 | fi
11 | echo "Building statically linked $VERSION $TARGET"
12 | export CGO_ENABLED=0
13 | go build -o "${TARGET}" --ldflags "${LDFLAGS}" "${SOURCE}"
14 |
15 | # ln -sf "$(basename "${TARGET}")" dist/docker-debug
16 |
--------------------------------------------------------------------------------
/scripts/upx.sh:
--------------------------------------------------------------------------------
1 | #!/bin/env bash
2 | source ./scripts/variables.env
3 |
4 | upx -q ${TARGET} -o ${TARGET}-upx
5 |
6 | ln -sf "$(basename "${TARGET}-upx")" dist/docker-debug
7 |
--------------------------------------------------------------------------------
/scripts/variables.darwin-m1.env:
--------------------------------------------------------------------------------
1 | export GOOS=darwin
2 | export GOARCH=arm64
3 |
4 | export TARGET="dist/docker-debug-$GOOS-$GOARCH"
5 |
--------------------------------------------------------------------------------
/scripts/variables.darwin.env:
--------------------------------------------------------------------------------
1 | export GOOS=darwin
2 | export GOARCH=amd64
3 |
4 | export TARGET="dist/docker-debug-$GOOS-$GOARCH"
5 |
--------------------------------------------------------------------------------
/scripts/variables.env:
--------------------------------------------------------------------------------
1 | PLATFORM=${PLATFORM:-"TravisLinux"}
2 | V=${V:-$RELEASE_VERSION}
3 | V=${V:-"unknown-version"}
4 | GITCOMMIT=${GITCOMMIT:-$(git rev-parse --short HEAD 2> /dev/null || true)}
5 | BUILDTIME=${BUILDTIME:-$(date +'%Y-%m-%d %H:%M:%S %z')}
6 | VERSION=`echo $V | sed 's/^v//g'`
7 |
8 | PLATFORM_LDFLAGS=
9 | if test -n "${PLATFORM}"; then
10 | PLATFORM_LDFLAGS="-X \"github.com/zeromake/docker-debug/version.PlatformName=${PLATFORM}\""
11 | fi
12 |
13 | export LDFLAGS="\
14 | -s \
15 | -w \
16 | ${PLATFORM_LDFLAGS} \
17 | -X \"github.com/zeromake/docker-debug/version.GitCommit=${GITCOMMIT}\" \
18 | -X \"github.com/zeromake/docker-debug/version.BuildTime=${BUILDTIME}\" \
19 | -X \"github.com/zeromake/docker-debug/version.Version=${VERSION}\" \
20 | ${LDFLAGS:-} \
21 | "
22 |
23 | GOOS="${GOOS:-$(go env GOHOSTOS)}"
24 | GOARCH="${GOARCH:-$(go env GOHOSTARCH)}"
25 | export TARGET="dist/docker-debug-$GOOS-$GOARCH"
26 | export SOURCE="github.com/zeromake/docker-debug/cmd/docker-debug"
27 |
--------------------------------------------------------------------------------
/scripts/variables.linux.env:
--------------------------------------------------------------------------------
1 | export GOOS=linux
2 | export GOARCH=amd64
3 |
4 | export TARGET="dist/docker-debug-$GOOS-$GOARCH"
5 |
--------------------------------------------------------------------------------
/scripts/variables.windows.env:
--------------------------------------------------------------------------------
1 | export GOOS=windows
2 | export GOARCH=amd64
3 |
4 | export TARGET="dist/docker-debug-$GOOS-$GOARCH.exe"
5 |
--------------------------------------------------------------------------------
/shell/docker-debug.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | inspectInfo=$(docker inspect "$1")
6 | containerId=$(echo "$inspectInfo" | grep "Id" | cut -d'"' -f 4)
7 | mergedDir=$(echo "$inspectInfo" | grep "MergedDir" | cut -d'"' -f 4)
8 | targetName="container:$containerId"
9 | name=$(docker run --network $targetName --pid $targetName --stop-signal SIGKILL -v $mergedDir:/mnt/container --rm -it -d nicolaka/netshoot:latest /usr/bin/env sh)
10 | args="${@:2}"
11 | docker exec -it "$name" $args
12 | docker stop "$name" > /dev/null &
13 |
--------------------------------------------------------------------------------
/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | // Default build-time variable.
4 | // These values are overridden via ldflags
5 | var (
6 | PlatformName = ""
7 | Version = "unknown-version"
8 | GitCommit = "unknown-commit"
9 | BuildTime = "unknown-buildtime"
10 | )
11 |
--------------------------------------------------------------------------------