├── .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 | [![Build Status](https://github.com/zeromake/docker-debug/actions/workflows/release.yml/badge.svg)](https://github.com/zeromake/docker-debug/actions/workflows/release.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/zeromake/docker-debug)](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 | [![asciicast](https://asciinema.org/a/235025.svg)](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 | [![Build Status](https://github.com/zeromake/docker-debug/actions/workflows/release.yml/badge.svg)](https://github.com/zeromake/docker-debug/actions/workflows/release.yml) 4 | [![Go Report Card](https://goreportcard.com/badge/zeromake/docker-debug)](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 | [![asciicast](https://asciinema.org/a/235025.svg)](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 | --------------------------------------------------------------------------------