├── .dockerignore ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── debug-agent │ └── main.go └── kubectl-debug │ └── main.go ├── go.mod ├── go.sum ├── pkg ├── debug-agent │ ├── config.go │ ├── lxcfs.go │ ├── resize.go │ ├── runtime.go │ └── server.go ├── kubectl-debug │ ├── cmd.go │ └── config.go ├── nsenter │ └── nsenter.go └── util │ ├── jsonstream.go │ ├── resize.go │ ├── resizeevents.go │ ├── resizeevents_windows.go │ └── term.go ├── scripts ├── docker_push.sh └── start.sh ├── version.sh └── version └── version.go /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !debug-agent 3 | !./scripts/start.sh 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .uptodate 2 | .pkg 3 | .cache 4 | *.output 5 | .swp 6 | vendor/ 7 | /debug-agent 8 | /kubectl-debug 9 | dist/ 10 | .vscode/ -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: kubectl-debug 2 | before: 3 | hooks: 4 | # you may remove this if you don't use vgo 5 | - go mod download 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | - GO111MODULE=on 10 | binary: kubectl-debug 11 | main: ./cmd/kubectl-debug/main.go 12 | goos: 13 | - freebsd 14 | - windows 15 | - linux 16 | - darwin 17 | goarch: 18 | - amd64 19 | - 386 20 | ignore: 21 | - goos: darwin 22 | goarch: 386 23 | ldflags: 24 | - -s -w -X 'github.com/jamestgrant/kubectl-debug/version.gitVersion={{.Version}}' 25 | checksum: 26 | name_template: 'checksums.txt' 27 | snapshot: 28 | name_template: "{{ .Tag }}-next" 29 | changelog: 30 | sort: asc 31 | filters: 32 | exclude: 33 | - '^docs:' 34 | - '^test:' 35 | brew: 36 | github: 37 | owner: jamestgrant 38 | name: homebrew-tap 39 | commit_author: 40 | name: jamestgrant 41 | email: jamesrgrant@mediakind.com 42 | install: | 43 | bin.install "kubectl-debug" 44 | homepage: "https://www.github.com/jamestgrant/kubectl-debug" 45 | description: "Debug a troublesome container using a dubug container which contains all your favorite troubleshooting tools pre-installed and runs in the same cgroup/ipc/network namespace as your troublesome container" 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.15.0 as build 2 | 3 | RUN apk add lxcfs containerd 4 | 5 | FROM alpine:3.15.0 6 | 7 | COPY --from=build /usr/bin/lxcfs /usr/bin/lxcfs 8 | COPY --from=build /usr/lib/*fuse* /usr/lib/ 9 | COPY --from=build /usr/bin/ctr /usr/bin/ctr 10 | 11 | COPY ./scripts/start.sh / 12 | RUN chmod 755 /start.sh 13 | COPY ./debug-agent /bin/debug-agent 14 | 15 | EXPOSE 10027 16 | 17 | CMD ["/start.sh"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build kubectl-debug-binary debug-agent-binary debug-agent-docker-image check 2 | 3 | LDFLAGS = $(shell ./version.sh) 4 | GOENV := GO15VENDOREXPERIMENT="1" GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=amd64 5 | GO := $(GOENV) go 6 | 7 | default: build 8 | 9 | build: kubectl-debug-binary debug-agent-docker-image 10 | 11 | kubectl-debug-binary: 12 | GO111MODULE=on CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o kubectl-debug cmd/kubectl-debug/main.go 13 | 14 | debug-agent-docker-image: debug-agent-binary 15 | docker build . -t jamesgrantmediakind/debug-agent:latest 16 | 17 | debug-agent-binary: 18 | $(GO) build -ldflags '$(LDFLAGS)' -o debug-agent cmd/debug-agent/main.go 19 | 20 | check: 21 | find . -iname '*.go' -type f | grep -v /vendor/ | xargs gofmt -l 22 | GO111MODULE=on go test -v -race ./... 23 | $(GO) vet ./... 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubectl-debug 2 | 3 | ![license](https://img.shields.io/hexpm/l/plug.svg) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/jamesTGrant/kubectl-debug)](https://goreportcard.com/report/github.com/jamesTGrant/kubectl-debug) 5 | [![docker](https://img.shields.io/docker/pulls/jamesgrantmediakind/debug-agent.svg)](https://hub.docker.com/r/jamesgrantmediakind/debug-agent) 6 | 7 | - [Overview](#overview) 8 | - [Quick start](#quick-start) 9 | - [Download the binary](#download-the-binary) 10 | - [Usage instructions](#usage-instructions) 11 | - [Build from source](#build-from-source) 12 | - [Under the hood](#under-the-hood) 13 | - [Configuration options and overrides](#configuration-options-and-overrides) 14 | - [Authorization / required privileges](#authorization-required-privileges) 15 | - [(Optional) Create a Secret for use with Private Docker Registries](#create-a-secret-for-use-with-private-docker-registries) 16 | - [Roadmap](#roadmap) 17 | - [Contribute](#contribute) 18 | - [Acknowledgement](#acknowledgement) 19 | 20 | 21 | # Overview 22 | 23 | This project is a fork of this fine project: https://github.com/aylei/kubectl-debug which is no longer maintained (hence this fork). The credit for this project belongs with [aylei](https://github.com/aylei). Aylei and I have chatted and we are happy that this project will live on and get maintained here. 24 | 25 | `kubectl-debug` is an 'out-of-tree' solution for connecting to and troubleshooting an existing, running, 'target' container in an existing pod in a Kubernetes cluster. 26 | The target container may have a shell and busybox utils and hence provide some debug capability or it may be very minimal and not even provide a shell - which makes any real-time troubleshooting/debugging very difficult. kubectl-debug is designed to overcome that difficulty. 27 | 28 | There's a short video on YouTube: https://www.youtube.com/watch?v=jJHCxCqPn1g 29 | 30 | How does it work? 31 |
32 |
    33 |
  1. User invokes kubectl-debug like this: kubectl-debug --namespace NAMESPACE POD_NAME -c TARGET_CONTAINER_NAME
  2. 34 |
  3. kubectl-debug communicates with the cluster using the same interface as kubectl and instructs kubernetes to request the launch of a new 'debug-agent' container on the same node as the 'target' container
  4. 35 |
  5. debug-agent process within the debug-agent pod connects directly to containerd (or dockerd if applicable) on the host which is running the 'target' container and requests the launch of a new 'debug' container in the same pid, network, user and ipc namespaces as the target container
  6. 36 |
  7. In summary: 'kubectl-debug' causes the launch of the 'debug-agent' container, 'debug-agent' the causes the launch of the 'debug' pod/container
  8. 37 |
  9. 'debug-agent' pod redirects the terminal output of the 'debug' container to the 'kubectl-debug' executable and so you can interact directly with the shell running in the 'debug' container. You can now use of the troubleshooting tools available in the debug container (BASH, cURL, tcpdump, etc) without the need to have these utilities in the target container image.
  10. 38 |
39 |
40 | 41 | `kubectl-debug` is not related to `kubectl debug` 42 | 43 | `kubectl-debug` has been largely replaced by kubernetes [ephemeral containers](https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers). 44 | Ephemeral containers feature is in beta (enabled by default) from kubernetes 1.23 45 | Ephemeral containers feature is in alpha from kubernetes 1.16 to 1.22 46 | In Kuberenetes, by default, you are required to explicitly enable alpha features (alpha features are not enabled by default). If you are using Azure AKS (and perhaps others) you are not able, nor permitted, to configure kubernetes feature flags and so you will need a solution like the one provided by this github project. 47 | 48 | # Quick start 49 | 50 | ## Download the binary 51 | (I'm testing Linux only): 52 | ```bash 53 | export RELEASE_VERSION=1.0.0 54 | # linux x86_64 55 | curl -Lo kubectl-debug https://github.com/JamesTGrant/kubectl-debug/releases/download/v${RELEASE_VERSION}/kubectl-debug 56 | 57 | # make the binary executable 58 | chmod +x kubectl-debug 59 | 60 | # run the binary pointing at whatever cluster kubectl points at 61 | ./kubectl-debug --namespace NAMESPACE TARGET_POD_NAME -c TARGET_CONTAINER_NAME 62 | ``` 63 | ## Build from source 64 | 65 | Clone this repo and: 66 | ```bash 67 | # to use this kubectl-debug utility, you only need to take the resultant kubectl-debug binary 68 | # file which is created by: 69 | make kubectl-debug-binary 70 | 71 | # to 'install' the kubectl-debug binary, make it executable and either call it directy, put 72 | # it in your PATH, or move it to a location which is already in your PATH: 73 | 74 | chmod +x kubectl-debug 75 | mv kubectl-debug /usr/local/bin 76 | 77 | 78 | 79 | ##################### 80 | # Extra options 81 | ###################### 82 | 83 | # build 'debug-agent' binary only - you wont need this. This is the binary/executable that 84 | # the 'debug-agent container' contains. 85 | # The dockerfile of the debug-agent container refers to this binary. 86 | make debug-agent-binary 87 | 88 | # build 'debug-agent' binary, and the 'debug-agent docker image' 89 | # a docker image `jamesgrantmediakind/debug-agent:latest` will be created locally 90 | make debug-agent-docker-image 91 | 92 | # make everything; kubectl-debug-binary, debug-agent-binary, and 'debug-agent-docker-image' 93 | make 94 | 95 | ``` 96 | 97 | ## Usage instructions 98 | 99 | ```bash 100 | # kubectl 1.12.0 or higher 101 | 102 | # print the help 103 | kubectl-debug -h 104 | 105 | # start the debug container in the same namespace, and cgroup etc as container 'TARGET_CONTAINER_NAME' in 106 | # pod 'POD_NAME' in namespace 'NAMESPACE' 107 | kubectl-debug --namespace NAMESPACE POD_NAME -c TARGET_CONTAINER_NAME 108 | 109 | # in case of your pod stuck in `CrashLoopBackoff` state and cannot be connected to, 110 | # you can fork a new pod and diagnose the problem in the forked pod 111 | kubectl-debug --namespace NAMESPACE POD_NAME -c CONTAINER_NAME --fork 112 | 113 | # In 'fork' mode, if you want the copied pod to retain the labels of the original pod, you can use 114 | # the --fork-pod-retain-labels parameter (comma separated, no spaces). If not set (default), this parameter 115 | # is empty and so any labels of the original pod are not retained, and the labels of the copied pods are empty. 116 | # Example of fork mode: 117 | kubectl-debug --namespace NAMESPACE POD_NAME -c CONTAINER_NAME --fork --fork-pod-retain-labels=,, 118 | 119 | # in order to interact with the debug-agent pod on a node which doesn't have a public IP or direct access 120 | # (firewall and other reasons) to access, port-forward mode is enabled by default. If you don't want 121 | # port-forward mode, you can use --port-forward false to turn off it. I don't know why you'd want to do 122 | # this, but you can if you want. 123 | kubectl-debug --port-forward=false --namespace NAMESPACE POD_NAME -c CONTAINER_NAME 124 | 125 | # you can choose a different debug container image. By default, nicolaka/netshoot:latest will be 126 | # used but you can specify anything you like 127 | kubectl-debug --namespace NAMESPACE POD_NAME -c CONTAINER_NAME --image nicolaka/netshoot:latest 128 | 129 | # you can set the debug-agent pod's resource limits/requests, for example: 130 | # default is not set 131 | kubectl-debug --namespace NAMESPACE POD_NAME -c CONTAINER_NAME --agent-pod-cpu-requests=250m --agent-pod-cpu-limits=500m --agent-pod-memory-requests=200Mi --agent-pod-memory-limits=500Mi 132 | 133 | # use primary docker registry, set registry kubernetes secret to pull image 134 | # the default registry-secret-name is kubectl-debug-registry-secret, the default namespace is default 135 | # please set the secret data source as {Username: , Password: } 136 | kubectl-debug --namespace NAMESPACE POD_NAME --image nicolaka/netshoot:latest --registry-secret-name --registry-secret-namespace 137 | 138 | # in addition to passing cli arguments, you can use a config file if you would like to 139 | # non-default values for various things. 140 | kubectl-debug --configfile /PATH/FILENAME --namespace NAMESPACE POD_NAME -c TARGET_CONTAINER_NAME 141 | 142 | ``` 143 | ## Debugging examples 144 | 145 | This guide shows a few typical example of debugging a target container. 146 | 147 | ### Basic 148 | 149 | When you run `kubectl-debug` it causes a 'debug container' to be created on the same node, and which runs in the same `pid`, `network`, `ipc` and `user` namespace, as the target container. 150 | By default, `kubectl-debug` uses [`nicolaka/netshoot`](https://github.com/nicolaka/netshoot) as container image for the 'debug container'. 151 | The netshoot [project documentation](https://github.com/nicolaka/netshoot/blob/master/README.md) provides excellent guides and examples for using various tools to troubleshoot your target container. 152 | 153 | Here are a few examples to show `netshoot` working with `kubectl-debug`: 154 | 155 | Connect to a running container 'demo-container' in pod 'demo-pod' in the default namespace: 156 | 157 | ```shell 158 | ➜ ~ kubectl-debug --namespace default target-pod -c target-container 159 | 160 | Agent Pod info: [Name:debug-agent-pod-da46a000-8429-11e9-a40c-8c8590147766, Namespace:default, Image:jamesgrantmediakind/debug-agent:latest, HostPort:10027, ContainerPort:10027] 161 | Waiting for pod debug-agent-pod-da46a000-8429-11e9-a40c-8c8590147766 to run... 162 | pod target-pod pod IP: 10.233.111.78, agentPodIP 172.16.4.160 163 | wait for forward port to debug agent ready... 164 | Forwarding from 127.0.0.1:10027 -> 10027 165 | Forwarding from [::1]:10027 -> 10027 166 | Handling connection for 10027 167 | pulling image nicolaka/netshoot:latest... 168 | latest: Pulling from nicolaka/netshoot 169 | Digest: sha256:5b1f5d66c4fa48a931ff54f2f34e5771eff2bc5e615fef441d5858e30e9bb921 170 | Status: Image is up to date for nicolaka/netshoot:latest 171 | starting debug container... 172 | container created, open tty... 173 | 174 | [1] 🐳 → hostname 175 | target-container 176 | ``` 177 | 178 | 179 | Navigating the filesystem of the target container: 180 | 181 | The root filesystem of target container is located in `/proc/{pid}/root/`, and the `pid` is typically '1'. 182 | You can `chroot` to the root filesystem of target container to navigate the target container filesystem or 183 | `cd /proc/1/root` works just as well (assuming PID '1' is the correct PID). 184 | 185 | ```shell 186 | root @ / 187 | [2] 🐳 → chroot /proc/1/root 188 | 189 | root @ / 190 | [3] 🐳 → cd /proc/1/root 191 | 192 | root @ / 193 | [#] 🐳 → ls 194 | bin entrypoint.sh home lib64 mnt root sbin sys tmp var 195 | dev etc lib media proc run srv usr 196 | (you can navigate the target containers filesystem and view/edit files) 197 | 198 | root @ / 199 | [#] 🐳 → ./entrypoint.sh 200 | (you can attempt to run the target containers entrypoint.sh script and perhaps see what errors are produced) 201 | ``` 202 | 203 | 204 | Using **iftop** to inspect network traffic: 205 | ```shell 206 | root @ / 207 | [4] 🐳 → iftop -i eth0 208 | interface: eth0 209 | IP address is: 10.233.111.78 210 | MAC address is: 86:c3:ae:9d:46:2b 211 | (CLI graph omitted) 212 | ``` 213 | 214 | 215 | Using **drill** to diagnose DNS: 216 | ```shell 217 | root @ / 218 | [5] 🐳 → drill -V 5 demo-service 219 | ;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 0 220 | ;; flags: rd ; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0 221 | ;; QUESTION SECTION: 222 | ;; demo-service. IN A 223 | 224 | ;; ANSWER SECTION: 225 | 226 | ;; AUTHORITY SECTION: 227 | 228 | ;; ADDITIONAL SECTION: 229 | 230 | ;; Query time: 0 msec 231 | ;; WHEN: Sat Jun 1 05:05:39 2019 232 | ;; MSG SIZE rcvd: 0 233 | ;; ->>HEADER<<- opcode: QUERY, rcode: NXDOMAIN, id: 62711 234 | ;; flags: qr rd ra ; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0 235 | ;; QUESTION SECTION: 236 | ;; demo-service. IN A 237 | 238 | ;; ANSWER SECTION: 239 | 240 | ;; AUTHORITY SECTION: 241 | . 30 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2019053101 1800 900 604800 86400 242 | 243 | ;; ADDITIONAL SECTION: 244 | 245 | ;; Query time: 58 msec 246 | ;; SERVER: 10.233.0.10 247 | ;; WHEN: Sat Jun 1 05:05:39 2019 248 | ;; MSG SIZE rcvd: 121 249 | ``` 250 | 251 | ### `proc` filesystem and FUSE 252 | 253 | It is common to use tools like `top`, `free` to inspect system metrics like CPU usage and memory. Using these commands will display the metrics from the host system by default. Because they read the metrics from the `proc` filesystem (`/proc/*`), which is mounted from the host system. This can be extremely useful (you can still inspect the pod/container metrics of as part of the host metrics) You may find [this blog post](https://fabiokung.com/2014/03/13/memory-inside-linux-containers/) useful. 254 | 255 | ## Debug Pod in "CrashLoopBackoff" 256 | 257 | Troubleshooting kubernetes containers in the `CrashLoopBackoff` state can be tricky. Using kubectl-debug 'normally' probably wont help you as the debug container processed will be terminated reaped once the target container (process with pid 1) exits. To tackle with this, `kubectl-debug` provides the `--fork` flag, which borrows the idea from the `oc debug` command: copy the currently crashing pod and (hopefully) the issue will reproduce in the forked Pod with the added ability to debug via the debug container. 258 | 259 | Under the hood, `kubectl debug --fork` will copy the entire Pod spec and: 260 | 261 | * strip all the labels, so that no traffic will be routed from service to this pod (see[Readme.md](/README.md) for instructions on duplicating the labels); 262 | * modify the entry-point of target container in order to hold the pid namespace and avoid the Pod crash again; 263 | 264 | Here's an example: 265 | 266 | ```shell 267 | ➜ ~ kubectl-debug demo-pod -c demo-container --fork 268 | Agent Pod info: [Name:debug-agent-pod-dea9e7c8-8439-11e9-883a-8c8590147766, Namespace:default, Image:jamesgrantmediakind/debug-agent:latest, HostPort:10027, ContainerPort:10027] 269 | Waiting for pod debug-agent-pod-dea9e7c8-8439-11e9-883a-8c8590147766 to run... 270 | Waiting for pod demo-pod-e23c1b68-8439-11e9-883a-8c8590147766-debug to run... 271 | pod demo-pod PodIP 10.233.111.90, agentPodIP 172.16.4.160 272 | wait for forward port to debug agent ready... 273 | Forwarding from 127.0.0.1:10027 -> 10027 274 | Forwarding from [::1]:10027 -> 10027 275 | Handling connection for 10027 276 | pulling image nicolaka/netshoot:latest... 277 | latest: Pulling from nicolaka/netshoot 278 | Digest: sha256:5b1f5d66c4fa48a931ff54f2f34e5771eff2bc5e615fef441d5858e30e9bb921 279 | Status: Image is up to date for nicolaka/netshoot:latest 280 | starting debug container... 281 | container created, open tty... 282 | 283 | [1] 🐳 → ps -ef 284 | PID USER TIME COMMAND 285 | 1 root 0:00 sh -c -- while true; do sleep 30; done; 286 | 6 root 0:00 sleep 30 287 | 7 root 0:00 /bin/bash -l 288 | 15 root 0:00 ps -ef 289 | ``` 290 | 291 | 292 | ## Debug init container 293 | 294 | Just like debugging the ordinary container, we can debug the init-container of a pod. You must specify the container name of init-container: 295 | 296 | ```shell 297 | ➜ ~ kubectl-debug demo-pod -c init-container 298 | ``` 299 | 300 | 301 | # Under the hood 302 | 303 | `kubectl-debug` consists of 3 components: 304 | 305 | * the 'kubectl-debug' executable serves the `kubectl-debug` command and interfaces with the kube-api-server 306 | * the 'debug-agent' pod is a temporary pod that is started in the cluster by kubectl-debug. The 'debug-agent' container is responsible for starting and manipulating the 'debug container'. The 'debug-agent' will also act as a websockets relay for remote tty to join the output of the 'debug container' to the terminal from which the kubectl-debug command was issued 307 | * the 'debug container' which is the container that provides the debugging utilities and the shell in which the human user performs their debugging activity. `kubectl-debug` doesn't provide this - it's an 'off-the-shelf container image (nicolaka/netshoot:latest by default), it is invoked and configured by 'debug-agent'. 308 | 309 | The following occurs when the user runs the command: `kubectl-debug --namespace -c ` 310 | 311 | 1. 'kubectl-debug' gets the target-pod info from kube-api-server and extracts the `host` information (the target-pod within the namespace ) 312 | 2. 'kubectl-debug' sends a 'debug-agent' pod specification to kube-api-server with a node-selector matching the `host`. By default the container image is `docker.io/jamesgrantmediakind/debug-agent:latest` 313 | 3. kube-api-server requests the creation of 'debug-agent' pod. 'debug-agent' pod is created in the default namespace (doesn't have to be the same namespace as the target pod) 314 | 4. 'kubectl-debug' sends an HTTP request to the 'debug-agent' pod running on the `host` which includes a protocol upgrade from HTTP to SPDY 315 | 5. debug-agent' checks if the target container is actively running, if not, write an error to client 316 | 6. 'debug-agent' interfaces with containerd (or dockerd if applicable) on the host to request the creation of the 'debug-container'. `debug container` with `tty` and `stdin` opened, the 'debug-agent' configures the `debug container`'s `pid`, `network`, `ipc` and `user` namespace to be that of the target container 317 | 7. 'debug-agent' pipes the connection into the `debug container` using `attach` 318 | 8. Human performs debugging/troubleshooting on the target container from 'within' in the debug container with access to the target container process/network/ipc namespaces and root filesystem 319 | 9. debugging complete, user exits the debug-container shell which closes the SPDY connection 320 | 10. 'debug-agent' closes the SPDY connection, then waits for the `debug container` to exit and do the cleanup 321 | 11. 'debug-agent' pod is deleted 322 | 323 | 324 | # Configuration options and overrides 325 | 326 | The `debug-agent` uses [nicolaka/netshoot](https://github.com/nicolaka/netshoot) as the default image to run debug container, and uses `bash` as default entrypoint. You can override the default image and entrypoint, as well as a number of other useful things, by passing the config file to the kubectl-debug command like this: 327 | ```bash 328 | kubectl-debug --configfile CONFIGFILE --namespace NAMESPACE POD_NAME -c TARGET_CONTAINER_NAME 329 | ``` 330 | Example configfile: 331 | ```yaml 332 | # debug agent listening port (outside container) 333 | # default: 10027 334 | agentPort: 10027 335 | 336 | # namespace of debug-agent pod (does'nt need to be in the same namespace as the target container) 337 | # default: 'default' 338 | agentPodNamespace: default 339 | 340 | # prefix of debug-agent pod 341 | # default: 'debug-agent-pod' 342 | agentPodNamePrefix: debug-agent-pod 343 | 344 | # image of debug-agent pod 345 | # default: jamesgrantmediakind/debug-agent:latest 346 | agentImage: jamesgrantmediakind/debug-agent:latest 347 | 348 | # auditing can be enabled by setting 'audit' to 'true' 349 | # default: false 350 | audit: false 351 | 352 | # whether using port-forward when connecting debug-agent 353 | # default: true 354 | portForward: true 355 | 356 | # the 'debug container' image 357 | # default: nicolaka/netshoot:latest 358 | # for most reliable result, use the full path - for example: docker.io/library/busybox:latest will 359 | # work but busybox:latest may not (depending on the cluster) 360 | image: nicolaka/netshoot:latest 361 | 362 | # start command of the debug container 363 | # `kubectl-debug` always specifies this explicitly and you can not override this without making code changes to `kubectl-debug`) this is by design. 364 | # default ['bash'] 365 | command: 366 | - '/bin/bash' 367 | - '-l' 368 | 369 | # private docker registry auth kubernetes secret 370 | # default registrySecretName: kubectl-debug-registry-secret 371 | # default registrySecretNamespace: default 372 | registrySecretName: my-debug-secret 373 | registrySecretNamespace: debug 374 | 375 | # you can set the agent pod's resource limits/requests: 376 | # default is not set 377 | agentCpuRequests: "" 378 | agentCpuLimits: "" 379 | agentMemoryRequests: "" 380 | agentMemoryLimits: "" 381 | 382 | # in fork mode, if you want the copied pod retains the labels of the original pod, you can change this params 383 | # format is []string 384 | # If not set, this parameter is empty by default (Means that any labels of the original pod are not retained, and the labels of the copied pods are empty.) 385 | forkPodRetainLabels: [] 386 | 387 | # You can disable SSL certificate check when communicating with image registry by 388 | # setting registrySkipTLSVerify to true. 389 | registrySkipTLSVerify: false 390 | 391 | # You can set the debug logging output level with the verbosity setting. There are two levels of verbosity, 0 and any positive integer (ie; 'verbosity : 1' will produce the same debug output as 'verbosity : 5') 392 | verbosity : 0 393 | ``` 394 | 395 | # Authorization / required privileges 396 | 397 | Put simply - if you can successfully issue the command `kubectl exec` to a container in your cluster then `kubectl-debug` will work for you! 398 | Detail: `kubectl-debug` reuses the privilege of the `pod/exec` sub-resource to do authorization, which means that it has the same privilege requirements as the `kubectl exec` command. 399 | 400 | The processes in the debug-agent container run as `root` and the debug-agent container `securityContext` is configured with `privileged: true` Some clusters such as OpenShift may not, by default, allow either of these practices. 401 | 402 | # Auditing / Security 403 | 404 | Some teams may want to limit what debug image users are allowed to use and to have an audit record for each command they run in the debug container. 405 | 406 | You can add ```KCTLDBG_RESTRICT_IMAGE_TO``` to the config file to restrict the debug-agent to use a specific container image. For example putting the following in the config file will force the agent to always use the image ```docker.io/nicolaka/netshoot:latest``` regardless of what the user specifies on the kubectl-debug command line. This may be helpful for restrictive environments that mandate the use of ```KCTLDBG_RESTRICT_IMAGE_TO``` 407 | ``` 408 | KCTLDBG_RESTRICT_IMAGE_TO: docker.io/nicolaka/netshoot:latest 409 | ``` 410 | If ```KCTLDBG_RESTRICT_IMAGE_TO``` is set and as a result agent is using an image that is different than what the user requested then the agent will log to standard out a message that announces what is happening. The message will include the URI's of both images. 411 | 412 | There are 3 settings related to auditing. 413 |
414 |
audit
415 |
Boolean value that indicates whether auditing should be enabled or not. Default value is false
416 |
audit_fifo
417 |
Template of path to a FIFO that will be used to exchange audit information from the debug container to the agent. The default value is /var/data/kubectl-debug-audit-fifo/KCTLDBG-CONTAINER-ID. If auditing is enabled then the agent will : 418 |
    419 |
  1. Prior to creating the debug container, create a fifo based on the value of audit_fifo. The agent will replace KCTLDBG-CONTAINER-ID with the id of the debug container it is creating.
  2. 420 |
  3. Create a thread that reads lines of text from the FIFO and then writes log messages to standard out, where the log messages look similar to example below
    421 | 422 | 2020/05/22 17:59:58 runtime.go:717: audit - user: USERNAME/885cbd0506868985a6fc491bb59a2d3c debugee: 48107cbdacf4b478cbf1e2e34dbea6ebb48a2942c5f3d1effbacf0a216eac94f exec: 265 execve("/bin/tar", ["tar", "--help"], 0x55a8d0dfa6c0 /* 7 vars */) = 0 423 |
    424 | Where USERNAME is the kubernetes user as determined by the client that launched the debug container and debuggee is the container id of the container being debugged. 425 |
  4. 426 |
  5. Bind mount the fifo it creates to the debugger container.
  6. 427 |
428 |
429 |
audit_shim 430 |
String array that will be placed before the command that will be run in the debug container. The default value is {"/usr/bin/strace", "-o", "KCTLDBG-FIFO", "-f", "-e", "trace=/exec"}. The agent will replace KCTLDBG-FIFO with the fifo path ( see above ) If auditing is enabled then agent will use the concatenation of the array specified by audit_shim and the original command array it was going to use.
431 |
432 | 433 | # (Optional) Create a Secret for Use with Private Docker Registries 434 | 435 | You can use a new or existing [Kubernetes `dockerconfigjson` secret](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/#registry-secret-existing-credentials). For example: 436 | 437 | ```bash 438 | # Be sure to run "docker login" beforehand. 439 | kubectl create secret generic kubectl-debug-registry-secret \ 440 | --from-file=.dockerconfigjson= \ 441 | --type=kubernetes.io/dockerconfigjson 442 | ``` 443 | 444 | Alternatively, you can create a secret with the key `authStr` and a JSON payload containing a `Username` and `Password`. For example: 445 | 446 | ```bash 447 | echo -n '{"Username": "calmkart", "Password": "calmkart"}' > ./authStr 448 | kubectl create secret generic kubectl-debug-registry-secret --from-file=./authStr 449 | ``` 450 | 451 | Refer to [the official Kubernetes documentation on Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) for more ways to create them. 452 | 453 | 454 | # Roadmap 455 | 456 | Jan '22 - plan to add support for k3s enviroments 457 | March '22 - actually add support for k3s enviroments and auto LXCFS detection handling 458 | 459 | `kubectl-debug` has been replaced by kubernetes [ephemeral containers](https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers). 460 | Ephemeral containers feature is in beta from kubernetes 1.23 461 | Ephemeral containers feature is in alpha from kubernetes 1.16 to 1.22 462 | 463 | In Kuberenetes, by default, you are required to explicitly enable alpha features (alpha features are not enabled by default). If you are using Azure AKS (and perhaps others) you are not able, nor permitted, to configure kubernetes feature flags and so you will need a solution like the one provided by this github project. 464 | 465 | 466 | # Contribute 467 | 468 | Feel free to open issues and pull requests. Any feedback is much appreciated! 469 | 470 | # Acknowledgement 471 | 472 | This project is a fork of [this project](https://github.com/aylei/kubectl-debug) which is no longer maintained (hence this fork). 473 | This project would not be here without the effort of [aylei contributors](https://github.com/aylei/kubectl-debug/graphs/contributors) and [this fork](https://github.com/JamesTGrant/kubectl-debug/graphs/contributors) 474 | -------------------------------------------------------------------------------- /cmd/debug-agent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/jamestgrant/kubectl-debug/pkg/debug-agent" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | log.SetFlags(log.LstdFlags | log.Lshortfile) 11 | var configFile string 12 | flag.StringVar(&configFile, "config.file", "", "Config file location.") 13 | flag.Parse() 14 | 15 | config, err := debugagent.LoadFile(configFile) 16 | if err != nil { 17 | log.Fatalf("error reading config %v", err) 18 | } 19 | 20 | server, err := debugagent.NewServer(config) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | if err := server.Run(); err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | log.Println("server stopped, see you next time!") 30 | } 31 | -------------------------------------------------------------------------------- /cmd/kubectl-debug/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jamestgrant/kubectl-debug/pkg/kubectl-debug" 5 | "github.com/spf13/pflag" 6 | "k8s.io/cli-runtime/pkg/genericclioptions" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | flags := pflag.NewFlagSet("kubectldebug", pflag.ExitOnError) 12 | pflag.CommandLine = flags 13 | 14 | // bypass to DebugCmd 15 | cmd := kubectldebug.NewDebugCmd(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}) 16 | if err := cmd.Execute(); err != nil { 17 | os.Exit(1) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jamestgrant/kubectl-debug 2 | 3 | go 1.12 4 | 5 | require ( 6 | cloud.google.com/go v0.38.0 7 | contrib.go.opencensus.io/exporter/ocagent v0.6.0 8 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 9 | github.com/Azure/go-autorest v11.2.8+incompatible 10 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e 11 | github.com/Microsoft/go-winio v0.4.11 12 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 13 | github.com/PuerkitoBio/purell v1.1.1 14 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 15 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 16 | github.com/census-instrumentation/opencensus-proto v0.2.1 17 | github.com/containerd/cgroups v0.0.0-20200407151229-7fc7a507c04c // indirect 18 | github.com/containerd/console v1.0.0 // indirect 19 | github.com/containerd/containerd v1.3.3 20 | github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb // indirect 21 | github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b // indirect 22 | github.com/containerd/ttrpc v1.0.0 // indirect 23 | github.com/containerd/typeurl v1.0.0 24 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 25 | github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible // 2020-04-15 d : https://github.com/containerd/containerd/issues/3031 26 | github.com/docker/docker v0.0.0-20171023200535-7848b8beb9d3 27 | github.com/docker/go-connections v0.4.0 28 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect 29 | github.com/docker/go-units v0.4.0 30 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c 31 | github.com/evanphx/json-patch v4.1.0+incompatible 32 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d 33 | github.com/go-openapi/jsonpointer v0.19.0 34 | github.com/go-openapi/jsonreference v0.19.0 35 | github.com/go-openapi/spec v0.19.0 36 | github.com/go-openapi/swag v0.19.0 37 | github.com/gogo/googleapis v1.3.2 // indirect 38 | github.com/gogo/protobuf v1.3.1 39 | github.com/golang/protobuf v1.3.2 40 | github.com/google/btree v1.0.0 41 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf 42 | github.com/google/uuid v1.1.1 43 | github.com/googleapis/gnostic v0.2.0 44 | github.com/gophercloud/gophercloud v0.0.0-20181221023737-94924357ebf6 45 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f 46 | github.com/imdario/mergo v0.3.6 47 | github.com/inconshreveable/mousetrap v1.0.0 48 | github.com/json-iterator/go v1.1.5 49 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 50 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe 51 | github.com/matttproud/golang_protobuf_extensions v1.0.1 52 | github.com/mitchellh/go-wordwrap v1.0.0 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd 54 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 55 | github.com/opencontainers/go-digest v1.0.0-rc1 56 | github.com/opencontainers/image-spec v1.0.1 57 | github.com/opencontainers/runc v0.1.1 // indirect 58 | github.com/opencontainers/runtime-spec v1.0.2 59 | github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 60 | github.com/petar/GoLLRB v0.0.0-20130427215148-53be0d36a84c 61 | github.com/peterbourgon/diskv v2.0.1+incompatible 62 | github.com/pkg/errors v0.9.1 63 | github.com/prometheus/client_golang v0.9.2 64 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 65 | github.com/prometheus/common v0.0.0-20181218105931-67670fe90761 66 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a 67 | github.com/rs/xid v1.3.0 68 | github.com/russross/blackfriday v0.0.0-20151117072312-300106c228d5 69 | github.com/shurcooL/sanitized_anchor_name v1.0.0 70 | github.com/sirupsen/logrus v1.4.2 71 | github.com/spf13/cobra v0.0.3 72 | github.com/spf13/pflag v1.0.3 73 | github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 // indirect 74 | github.com/urfave/cli v1.22.4 // indirect 75 | go.opencensus.io v0.22.0 76 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 77 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 78 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 79 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 80 | golang.org/x/sys v0.0.0-20200120151820-655fe14d7479 81 | golang.org/x/text v0.3.2 82 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c 83 | google.golang.org/api v0.7.0 84 | google.golang.org/appengine v1.5.0 85 | google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 86 | google.golang.org/grpc v1.22.0 87 | gopkg.in/inf.v0 v0.9.1 88 | gopkg.in/yaml.v2 v2.2.2 89 | k8s.io/api v0.0.0-20181130031204-d04500c8c3dd 90 | k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 91 | k8s.io/apiserver v0.0.0-20181220070914-ce7b605bead3 92 | k8s.io/cli-runtime v0.0.0-20190228180923-a9e421a79326 93 | k8s.io/client-go v0.0.0-20190228174230-b40b2a5939e4 94 | k8s.io/klog v0.1.0 95 | k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580 96 | k8s.io/kubernetes v1.13.1 97 | k8s.io/utils v0.0.0-20181115163542-0d26856f57b3 98 | sigs.k8s.io/yaml v1.1.0 99 | ) 100 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= 2 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 4 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 5 | cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= 6 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 7 | contrib.go.opencensus.io/exporter/ocagent v0.2.0 h1:Q/jXnVbliDYozuWJni9452xsSUuo+y8yrioxRgofBhE= 8 | contrib.go.opencensus.io/exporter/ocagent v0.2.0/go.mod h1:0fnkYHF+ORKj7HWzOExKkUHeFX79gXSKUQbpnAM+wzo= 9 | contrib.go.opencensus.io/exporter/ocagent v0.6.0 h1:Z1n6UAyr0QwM284yUuh5Zd8JlvxUGAhFZcgMJkMPrGM= 10 | contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeSEBLPA9YJp5bjrofdU3pIXs= 11 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 12 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= 13 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 14 | github.com/Azure/go-autorest v11.2.8+incompatible h1:Q2feRPMlcfVcqz3pF87PJzkm5lZrL+x6BDtzhODzNJM= 15 | github.com/Azure/go-autorest v11.2.8+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 16 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 17 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= 18 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= 19 | github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= 20 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= 21 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= 22 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 23 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 24 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 25 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 26 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 27 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= 28 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 29 | github.com/census-instrumentation/opencensus-proto v0.0.2-0.20180913191712-f303ae3f8d6a/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 30 | github.com/census-instrumentation/opencensus-proto v0.1.0 h1:VwZ9smxzX8u14/125wHIX7ARV+YhR+L4JADswwxWK0Y= 31 | github.com/census-instrumentation/opencensus-proto v0.1.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 32 | github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= 33 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 34 | github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= 35 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 36 | github.com/containerd/cgroups v0.0.0-20200407151229-7fc7a507c04c h1:BRbZO594sFDSfkqApcikeNRjePj+rJNoh4waZgefcEE= 37 | github.com/containerd/cgroups v0.0.0-20200407151229-7fc7a507c04c/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= 38 | github.com/containerd/console v1.0.0 h1:fU3UuQapBs+zLJu82NhR11Rif1ny2zfMMAyPJzSN5tQ= 39 | github.com/containerd/console v1.0.0/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= 40 | github.com/containerd/containerd v1.3.3 h1:LoIzb5y9x5l8VKAlyrbusNPXqBY0+kviRloxFUMFwKc= 41 | github.com/containerd/containerd v1.3.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 42 | github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI= 43 | github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb h1:nXPkFq8X1a9ycY3GYQpFNxHh3j2JgY7zDZfq2EXMIzk= 44 | github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= 45 | github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b h1:qUtCegLdOUVfVJOw+KDg6eJyE1TGvLlkGEd1091kSSQ= 46 | github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= 47 | github.com/containerd/ttrpc v1.0.0 h1:NY8Zk2i7TpkLxrkOASo+KTFq9iNCEmMH2/ZG9OuOw6k= 48 | github.com/containerd/ttrpc v1.0.0/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= 49 | github.com/containerd/typeurl v1.0.0 h1:7LMH7LfEmpWeCkGcIputvd4P0Rnd0LrIv1Jk2s5oobs= 50 | github.com/containerd/typeurl v1.0.0/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= 51 | github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28= 52 | github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= 53 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 54 | github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= 55 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 56 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 58 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 59 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 60 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 61 | github.com/docker/distribution v2.7.0+incompatible h1:neUDAlf3wX6Ml4HdqTrbcOHXtfRN0TFIwt6YFL7N9RU= 62 | github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 63 | github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible h1:dvc1KSkIYTVjZgHf/CTC2diTYC8PzhaA5sFISRfNVrE= 64 | github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 65 | github.com/docker/docker v0.0.0-20171023200535-7848b8beb9d3 h1:EVdfrzOhgGwYz/l0c52j97khuNSZTk4J0q3HHri/aUc= 66 | github.com/docker/docker v0.0.0-20171023200535-7848b8beb9d3/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 67 | github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= 68 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 69 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 70 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= 71 | github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= 72 | github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= 73 | github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 74 | github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= 75 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 76 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= 77 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 78 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 79 | github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc= 80 | github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 81 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= 82 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= 83 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 84 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 85 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 86 | github.com/go-openapi/jsonpointer v0.19.0 h1:FTUMcX77w5rQkClIzDtTxvn6Bsa894CcrzNj2MMfeg8= 87 | github.com/go-openapi/jsonpointer v0.19.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 88 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 89 | github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk= 90 | github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 91 | github.com/go-openapi/spec v0.19.0 h1:A4SZ6IWh3lnjH0rG0Z5lkxazMGBECtrZcbyYQi+64k4= 92 | github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 93 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 94 | github.com/go-openapi/swag v0.19.0 h1:Kg7Wl7LkTPlmc393QZQ/5rQadPhi7pBVEMZxyTi0Ii8= 95 | github.com/go-openapi/swag v0.19.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 96 | github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= 97 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 98 | github.com/gogo/googleapis v1.3.2 h1:kX1es4djPJrsDhY7aZKJy7aZasdcB5oSOEphMjSB53c= 99 | github.com/gogo/googleapis v1.3.2/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= 100 | github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= 101 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 102 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 103 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 104 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 105 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 106 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 107 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 108 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 109 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 110 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 111 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 112 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 113 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 114 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 115 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 116 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 117 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 118 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= 119 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 120 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 121 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 122 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 123 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 124 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 125 | github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= 126 | github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 127 | github.com/gophercloud/gophercloud v0.0.0-20181221023737-94924357ebf6 h1:4/heZnxONZh0s2O5wg6R0nPBs2ikXQ8nHKGuSBNWqx8= 128 | github.com/gophercloud/gophercloud v0.0.0-20181221023737-94924357ebf6/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= 129 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f h1:ShTPMJQes6tubcjzGMODIVG5hlrCeImaBnZzKF2N8SM= 130 | github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 131 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 132 | github.com/grpc-ecosystem/grpc-gateway v1.9.4 h1:5xLhQjsk4zqPf9EHCrja2qFZMx+yBqkO3XgJ14bNnU0= 133 | github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 134 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 135 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= 136 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 137 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 138 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 139 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 140 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 141 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 142 | github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= 143 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 144 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 145 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 146 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 147 | github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 148 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 149 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 150 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 151 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 152 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 153 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 154 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 155 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 156 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe h1:W/GaMY0y69G4cFlmsC6B9sbuo2fP8OFP1ABjt4kPz+w= 157 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 158 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 159 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 160 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 161 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 162 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 163 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 164 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 165 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 166 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 167 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 168 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 169 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= 170 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 171 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= 172 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 173 | github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= 174 | github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 175 | github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0= 176 | github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 177 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 178 | github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 h1:zNBQb37RGLmJybyMcs983HfUfpkw9OTFD9tbBfAViHE= 179 | github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= 180 | github.com/petar/GoLLRB v0.0.0-20130427215148-53be0d36a84c/go.mod h1:HUpKUBZnpzkdx0kD/+Yfuft+uD3zHGtXF/XJB14TUr4= 181 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 182 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 183 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 184 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 185 | github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 186 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 187 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 188 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 189 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 190 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 191 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 192 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 193 | github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= 194 | github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= 195 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= 196 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 197 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 198 | github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 199 | github.com/prometheus/common v0.0.0-20181218105931-67670fe90761 h1:z6tvbDJ5OLJ48FFmnksv04a78maSTRBUIhkdHYV5Y98= 200 | github.com/prometheus/common v0.0.0-20181218105931-67670fe90761/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 201 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 202 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= 203 | github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 204 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 205 | github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= 206 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 207 | github.com/russross/blackfriday v0.0.0-20151117072312-300106c228d5 h1:+6eORf9Bt4C3Wjt91epyu6wvLW+P6+AEODb6uKgO+4g= 208 | github.com/russross/blackfriday v0.0.0-20151117072312-300106c228d5/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 209 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 210 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 211 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 212 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 213 | github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 214 | github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg= 215 | github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= 216 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 217 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 218 | github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 219 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 220 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 221 | github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 222 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 223 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 224 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 225 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 226 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 227 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 228 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 229 | github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8= 230 | github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 231 | github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 232 | github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= 233 | github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 234 | go.opencensus.io v0.17.0/go.mod h1:mp1VrMQxhlqqDpKvH4UcQUa4YwlzNmymAjPrDdfxNpI= 235 | go.opencensus.io v0.18.0 h1:Mk5rgZcggtbvtAun5aJzAtjKKN/t0R3jJPlWILlv938= 236 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 237 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 238 | go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= 239 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 240 | golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 241 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 242 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= 243 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 244 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 245 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 246 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 247 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 248 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 249 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 250 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 251 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 252 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 253 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 254 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 255 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 256 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 257 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 258 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 259 | golang.org/x/net v0.0.0-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4= 260 | golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 261 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 262 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 263 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 264 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 265 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 266 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 267 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= 268 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 269 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 270 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE= 271 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 272 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 273 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 274 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 275 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 276 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 277 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 278 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 279 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 280 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 281 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 282 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 283 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 284 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 285 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 286 | golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM= 287 | golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 288 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 289 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= 293 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= 297 | golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20200120151820-655fe14d7479 h1:LhLiKguPgZL+Tglay4GhVtfF0kb8cvOJ0dHTCBO8YNI= 299 | golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 301 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 302 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 303 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 304 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 305 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= 306 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 307 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 308 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 309 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 310 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 311 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 312 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 313 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 314 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 315 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 316 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 317 | google.golang.org/api v0.0.0-20181221000618-65a46cafb132 h1:SLcC5l+3o5vwvXAbdm936WwLkHteUZpo1RULZD7YvQ4= 318 | google.golang.org/api v0.0.0-20181221000618-65a46cafb132/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 319 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 320 | google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= 321 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 322 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 323 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 324 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 325 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 326 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 327 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 328 | google.golang.org/genproto v0.0.0-20181221175505-bd9b4fb69e2f h1:eT3B0O2ghdSPzjAOznr3oOLyN1HFeYUncYl7FRwg4VI= 329 | google.golang.org/genproto v0.0.0-20181221175505-bd9b4fb69e2f/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 330 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 331 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 332 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 333 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 334 | google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610 h1:Ygq9/SRJX9+dU0WCIICM8RkWvDw03lvB77hrhJnpxfU= 335 | google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 336 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 337 | google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 338 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 339 | google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= 340 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 341 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 342 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 343 | google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= 344 | google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 345 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 346 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 347 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 348 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 349 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 350 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 351 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 352 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 353 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 354 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 355 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 356 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 357 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 358 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 359 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 360 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 361 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 362 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 363 | k8s.io/api v0.0.0-20181130031204-d04500c8c3dd h1:5aHsneN62ehs/tdtS9tWZlhVk68V7yms/Qw7nsGmvCA= 364 | k8s.io/api v0.0.0-20181130031204-d04500c8c3dd/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= 365 | k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg= 366 | k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 367 | k8s.io/apimachinery v0.18.1 h1:hKPYcQRPLQoG2e7fKkVl0YCvm9TBefXTfGILa9vjVVk= 368 | k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA= 369 | k8s.io/apiserver v0.0.0-20181220070914-ce7b605bead3 h1:GIl6hq5bRA5OSLauW8qCPfibG3m+FKtdEGvX36n8/m8= 370 | k8s.io/apiserver v0.0.0-20181220070914-ce7b605bead3/go.mod h1:6bqaTSOSJavUIXUtfaR9Os9JtTCm8ZqH2SUl2S60C4w= 371 | k8s.io/cli-runtime v0.0.0-20190228180923-a9e421a79326 h1:uMY/EHQt0VHYgRMPWwPl/vQ0K0fPlt5zxTEGAdt8pDg= 372 | k8s.io/cli-runtime v0.0.0-20190228180923-a9e421a79326/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM= 373 | k8s.io/cli-runtime v0.18.2 h1:JiTN5RgkFNTiMxHBRyrl6n26yKWAuNRlei1ZJALUmC8= 374 | k8s.io/cli-runtime v0.18.3 h1:8IBtaTYmXiXipKdx2FAKotvnQMjcF0kSLvX4szY340c= 375 | k8s.io/client-go v0.0.0-20190228174230-b40b2a5939e4 h1:aE8wOCKuoRs2aU0OP/Rz8SXiAB0FTTku3VtGhhrkSmc= 376 | k8s.io/client-go v0.0.0-20190228174230-b40b2a5939e4/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 377 | k8s.io/client-go v1.5.1 h1:XaX/lo2/u3/pmFau8HN+sB5C/b4dc4Dmm2eXjBH4p1E= 378 | k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= 379 | k8s.io/klog v0.1.0 h1:I5HMfc/DtuVaGR1KPwUrTc476K8NCqNBldC7H4dYEzk= 380 | k8s.io/klog v0.1.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 381 | k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580 h1:fq0ZXW/BAIFZH+dazlups6JTVdwzRo5d9riFA103yuQ= 382 | k8s.io/kube-openapi v0.0.0-20190320154901-5e45bb682580/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= 383 | k8s.io/kubernetes v1.13.1 h1:IwCCcPOZwY9rKcQyBJYXAE4Wgma4oOW5NYR3HXKFfZ8= 384 | k8s.io/kubernetes v1.13.1/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= 385 | k8s.io/kubernetes v1.18.1 h1:qW6zgSY96X/NO+ueIIfsFSbHl/e0Txio62XcA4cxpIk= 386 | k8s.io/kubernetes v1.18.2 h1:37sJPq6p+gx5hEHQSwCWXIiXDc9AajzV1A5UrswnDq0= 387 | k8s.io/utils v0.0.0-20181115163542-0d26856f57b3 h1:S3/Kq185JnolOEemhmDXXd23l2t4bX5hPQPQPADlF1E= 388 | k8s.io/utils v0.0.0-20181115163542-0d26856f57b3/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= 389 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 390 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 391 | -------------------------------------------------------------------------------- /pkg/debug-agent/config.go: -------------------------------------------------------------------------------- 1 | package debugagent 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v2" 6 | "io/ioutil" 7 | "time" 8 | ) 9 | 10 | var ( 11 | DefaultConfig = Config{ 12 | DockerEndpoint: "unix:///var/run/docker.sock", 13 | ContainerdEndpoint: "/run/containerd/containerd.sock", 14 | RuntimeTimeout: 30 * time.Second, 15 | StreamIdleTimeout: 10 * time.Minute, 16 | StreamCreationTimeout: 15 * time.Second, 17 | ListenAddress: "0.0.0.0:10027", 18 | AuditFifo: "/var/data/kubectl-debug-audit-fifo/KCTLDBG-CONTAINER-ID", 19 | AuditShim: []string{"/usr/bin/strace", "-o", "KCTLDBG-FIFO", "-f", "-e", "trace=/exec"}, 20 | } 21 | ) 22 | 23 | type Config struct { 24 | DockerEndpoint string `yaml:"docker_endpoint,omitempty"` 25 | ContainerdEndpoint string `yaml:"containerd_endpoint,omitempty"` 26 | RuntimeTimeout time.Duration `yaml:"runtime_timeout,omitempty"` 27 | StreamIdleTimeout time.Duration `yaml:"stream_idle_timeout,omitempty"` 28 | StreamCreationTimeout time.Duration `yaml:"stream_creation_timeout,omitempty"` 29 | ListenAddress string `yaml:"listen_address,omitempty"` 30 | Verbosity int `yaml:"verbosity,omitempty"` 31 | Audit bool `yaml:"audit,omitempty"` 32 | AuditFifo string `yaml:"audit_fifo,omitempty"` 33 | AuditShim []string `yaml:"audit_shim,omitempty"` 34 | } 35 | 36 | func Load(s string) (*Config, error) { 37 | cfg := &Config{} 38 | // If the entire config body is empty the UnmarshalYAML method is 39 | // never called. We thus have to set the DefaultConfig at the entry 40 | // point as well. 41 | *cfg = DefaultConfig 42 | 43 | err := yaml.UnmarshalStrict([]byte(s), cfg) 44 | fmt.Printf("Config after reading from file %v\r\n", cfg) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return cfg, nil 49 | } 50 | 51 | func LoadFile(filename string) (*Config, error) { 52 | if len(filename) < 1 { 53 | fmt.Println("No config file provided. Using default values.\r\n") 54 | return &DefaultConfig, nil 55 | } 56 | fmt.Printf("Reading config file %v.\r\n", filename) 57 | c, err := ioutil.ReadFile(filename) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return Load(string(c)) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/debug-agent/lxcfs.go: -------------------------------------------------------------------------------- 1 | package debugagent 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | ) 10 | 11 | // List of LXC filesystem files 12 | const ( 13 | MemFile string = "/proc/meminfo" 14 | CpuFile string = "/proc/cpuinfo" 15 | UpTimeFile string = "/proc/uptime" 16 | SwapsFile string = "/proc/swaps" 17 | StatFile string = "/proc/stat" 18 | DiskStatsFile string = "/proc/diskstats" 19 | LoadavgFile string = "/proc/loadavg" 20 | ) 21 | 22 | var ( 23 | // IsLxcfsEnabled means whether to enable lxcfs 24 | LxcfsEnabled bool 25 | 26 | // LxcfsRootDir 27 | LxcfsRootDir = "/var/lib/lxc" 28 | 29 | // LxcfsHomeDir means /var/lib/lxc/lxcfs 30 | LxcfsHomeDir = "/var/lib/lxc/lxcfs" 31 | 32 | // LxcfsFiles is a list of LXC files 33 | LxcfsProcFiles = []string{MemFile, CpuFile, UpTimeFile, SwapsFile, StatFile, DiskStatsFile, LoadavgFile} 34 | ) 35 | 36 | // CheckLxcfsMount check if the the mount point of lxcfs exists 37 | func CheckLxcfsMount() error { 38 | isMount := false 39 | f, err := os.Open("/proc/1/mountinfo") 40 | if err != nil { 41 | return fmt.Errorf("Check lxcfs mounts failed: %v", err) 42 | } 43 | fr := bufio.NewReader(f) 44 | for { 45 | line, err := fr.ReadBytes('\n') 46 | if err != nil { 47 | if err == io.EOF { 48 | break 49 | } 50 | return fmt.Errorf("Check lxcfs mounts failed: %v", err) 51 | } 52 | 53 | if bytes.Contains(line, []byte(LxcfsHomeDir)) { 54 | isMount = true 55 | break 56 | } 57 | } 58 | if !isMount { 59 | return fmt.Errorf("%s is not a mount point, please run \" lxcfs %s \" before debug", LxcfsHomeDir, LxcfsHomeDir) 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/debug-agent/resize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package debugagent 18 | 19 | import ( 20 | "k8s.io/apimachinery/pkg/util/runtime" 21 | "k8s.io/client-go/tools/remotecommand" 22 | ) 23 | 24 | // handleResizing spawns a goroutine that processes the resize channel, calling resizeFunc for each 25 | // remotecommand.TerminalSize received from the channel. The resize channel must be closed elsewhere to stop the 26 | // goroutine. 27 | func HandleResizing(resize <-chan remotecommand.TerminalSize, resizeFunc func(size remotecommand.TerminalSize)) { 28 | go func() { 29 | defer runtime.HandleCrash() 30 | 31 | for size := range resize { 32 | if size.Height < 1 || size.Width < 1 { 33 | continue 34 | } 35 | resizeFunc(size) 36 | } 37 | }() 38 | } 39 | -------------------------------------------------------------------------------- /pkg/debug-agent/runtime.go: -------------------------------------------------------------------------------- 1 | package debugagent 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "net" 15 | "net/http" 16 | "net/url" 17 | "os" 18 | "path" 19 | "strings" 20 | "sync" 21 | "syscall" 22 | "text/tabwriter" 23 | "time" 24 | 25 | containerd "github.com/containerd/containerd" 26 | "github.com/containerd/containerd/cio" 27 | "github.com/containerd/containerd/content" 28 | "github.com/containerd/containerd/errdefs" 29 | glog "github.com/containerd/containerd/log" 30 | "github.com/containerd/containerd/namespaces" 31 | "github.com/containerd/containerd/oci" 32 | "github.com/containerd/containerd/pkg/progress" 33 | "github.com/containerd/containerd/remotes" 34 | "github.com/containerd/containerd/remotes/docker" 35 | "github.com/containerd/typeurl" 36 | "github.com/docker/docker/api/types" 37 | "github.com/docker/docker/api/types/container" 38 | "github.com/docker/docker/api/types/strslice" 39 | dockerclient "github.com/docker/docker/client" 40 | "github.com/docker/docker/pkg/stdcopy" 41 | "github.com/google/uuid" 42 | "github.com/jamestgrant/kubectl-debug/pkg/nsenter" 43 | term "github.com/jamestgrant/kubectl-debug/pkg/util" 44 | "github.com/opencontainers/go-digest" 45 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 46 | "github.com/opencontainers/runtime-spec/specs-go" 47 | kubetype "k8s.io/apimachinery/pkg/types" 48 | "k8s.io/client-go/tools/remotecommand" 49 | kubeletremote "k8s.io/kubernetes/pkg/kubelet/server/remotecommand" 50 | ) 51 | 52 | type ContainerRuntimeScheme string 53 | 54 | const ( 55 | DockerScheme ContainerRuntimeScheme = "docker" 56 | ContainerdScheme ContainerRuntimeScheme = "containerd" 57 | KubectlDebugNS string = "kctldbg" 58 | K8NS string = "k8s.io" 59 | ) 60 | 61 | type ContainerInfo struct { 62 | Pid int64 63 | MountDestinations []string 64 | } 65 | 66 | type RunConfig struct { 67 | context context.Context 68 | timeout time.Duration 69 | idOfContainerToDebug string 70 | image string 71 | command []string 72 | stdin io.Reader 73 | stdout io.WriteCloser 74 | stderr io.WriteCloser 75 | tty bool 76 | resize <-chan remotecommand.TerminalSize 77 | clientHostName string 78 | clientUserName string 79 | verbosity int 80 | audit bool 81 | auditFifo string 82 | auditShim []string 83 | } 84 | 85 | func (c *RunConfig) getContextWithTimeout() (context.Context, context.CancelFunc) { 86 | return context.WithTimeout(c.context, c.timeout) 87 | } 88 | 89 | type ContainerRuntime interface { 90 | PullImage(ctx context.Context, image string, 91 | skipTLS bool, authStr string, 92 | cfg RunConfig) error 93 | ContainerInfo(ctx context.Context, cfg RunConfig) (ContainerInfo, error) 94 | RunDebugContainer(cfg RunConfig) error 95 | } 96 | 97 | type DockerContainerRuntime struct { 98 | client *dockerclient.Client 99 | } 100 | 101 | var DockerContainerRuntimeImplementsContainerRuntime ContainerRuntime = (*DockerContainerRuntime)(nil) 102 | 103 | func (c *DockerContainerRuntime) PullImage(ctx context.Context, 104 | image string, skipTLS bool, authStr string, 105 | cfg RunConfig) error { 106 | // Verify that we have a valid Docker AuthConfig and try to make one out of a 107 | // username:password-style authStr if not. 108 | if authStr != "" { 109 | var authConfig types.AuthConfig 110 | authBytes := []byte(authStr) 111 | if err := json.Unmarshal(authBytes, &authConfig); err != nil { 112 | crds := strings.Split(authStr, ":") 113 | if len(crds) == 2 { 114 | authConfig = types.AuthConfig{ 115 | Username: crds[0], 116 | Password: crds[1], 117 | } 118 | authBytes, _ = json.Marshal(authConfig) 119 | } else { 120 | return fmt.Errorf("Failed to parse authStr '%s': %v", authStr, err) 121 | } 122 | } 123 | authStr = base64.URLEncoding.EncodeToString(authBytes) 124 | } 125 | out, err := c.client.ImagePull(ctx, image, types.ImagePullOptions{RegistryAuth: authStr}) 126 | if err != nil { 127 | return err 128 | } 129 | defer out.Close() 130 | // write pull progress to user 131 | if cfg.verbosity > 0 { 132 | term.DisplayJSONMessagesStream(out, cfg.stdout, 1, true, nil) 133 | } 134 | return nil 135 | } 136 | 137 | func (c *DockerContainerRuntime) ContainerInfo(ctx context.Context, cfg RunConfig) (ContainerInfo, error) { 138 | var ret ContainerInfo 139 | cntnr, err := c.client.ContainerInspect(ctx, cfg.idOfContainerToDebug) 140 | if err != nil { 141 | return ContainerInfo{}, err 142 | } 143 | ret.Pid = int64(cntnr.State.Pid) 144 | for _, mount := range cntnr.Mounts { 145 | ret.MountDestinations = append(ret.MountDestinations, mount.Destination) 146 | } 147 | return ret, nil 148 | } 149 | 150 | func (c *DockerContainerRuntime) RunDebugContainer(cfg RunConfig) error { 151 | 152 | createdBody, err := c.CreateContainer(cfg) 153 | if err != nil { 154 | return err 155 | } 156 | if err := c.StartContainer(cfg, createdBody.ID); err != nil { 157 | return err 158 | } 159 | 160 | defer c.CleanContainer(cfg, createdBody.ID) 161 | 162 | cfg.stdout.Write([]byte("container created, open tty...\n\r")) 163 | 164 | // from now on, should pipe stdin to the container and no long read stdin 165 | // close(m.stopListenEOF) 166 | 167 | return c.AttachToContainer(cfg, createdBody.ID) 168 | } 169 | 170 | func (c *DockerContainerRuntime) CreateContainer(cfg RunConfig) (*container.ContainerCreateCreatedBody, error) { 171 | 172 | config := &container.Config{ 173 | Entrypoint: strslice.StrSlice(cfg.command), 174 | Image: cfg.image, 175 | Tty: true, 176 | OpenStdin: true, 177 | StdinOnce: true, 178 | } 179 | hostConfig := &container.HostConfig{ 180 | NetworkMode: container.NetworkMode(c.containerMode(cfg.idOfContainerToDebug)), 181 | UsernsMode: container.UsernsMode(c.containerMode(cfg.idOfContainerToDebug)), 182 | IpcMode: container.IpcMode(c.containerMode(cfg.idOfContainerToDebug)), 183 | PidMode: container.PidMode(c.containerMode(cfg.idOfContainerToDebug)), 184 | CapAdd: strslice.StrSlice([]string{"SYS_PTRACE", "SYS_ADMIN"}), 185 | } 186 | ctx, cancel := cfg.getContextWithTimeout() 187 | defer cancel() 188 | body, err := c.client.ContainerCreate(ctx, config, hostConfig, nil, "") 189 | if err != nil { 190 | return nil, err 191 | } 192 | return &body, nil 193 | } 194 | 195 | func (c *DockerContainerRuntime) containerMode(idOfCntnrToDbg string) string { 196 | return fmt.Sprintf("container:%s", idOfCntnrToDbg) 197 | } 198 | 199 | // Run a new container, this container will join the network, 200 | // mount, and pid namespace of the given container 201 | func (c *DockerContainerRuntime) StartContainer(cfg RunConfig, id string) error { 202 | ctx, cancel := cfg.getContextWithTimeout() 203 | defer cancel() 204 | err := c.client.ContainerStart(ctx, id, types.ContainerStartOptions{}) 205 | if err != nil { 206 | return err 207 | } 208 | return nil 209 | } 210 | 211 | func (c *DockerContainerRuntime) CleanContainer(cfg RunConfig, id string) { 212 | // cleanup procedure should use background context 213 | ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout) 214 | defer cancel() 215 | // wait the container gracefully exit 216 | statusCh, errCh := c.client.ContainerWait(ctx, id, container.WaitConditionNotRunning) 217 | var rmErr error 218 | select { 219 | case err := <-errCh: 220 | if err != nil { 221 | log.Println("error waiting container exit, kill with --force") 222 | // timeout or error occurs, try force remove anywawy 223 | rmErr = c.RmContainer(cfg, id, true) 224 | } 225 | case <-statusCh: 226 | rmErr = c.RmContainer(cfg, id, false) 227 | } 228 | if rmErr != nil { 229 | log.Printf("error remove container: %s \n", id) 230 | } else if cfg.verbosity > 0 { 231 | log.Printf("Debug session end, debug container %s removed", id) 232 | } 233 | } 234 | 235 | func (c *DockerContainerRuntime) RmContainer(cfg RunConfig, id string, force bool) error { 236 | // cleanup procedure should use background context 237 | ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout) 238 | defer cancel() 239 | return c.client.ContainerRemove(ctx, id, 240 | types.ContainerRemoveOptions{ 241 | Force: true, 242 | }) 243 | } 244 | 245 | // AttachToContainer do `docker attach`. Blocks until container I/O complete 246 | func (c *DockerContainerRuntime) AttachToContainer(cfg RunConfig, container string) error { 247 | HandleResizing(cfg.resize, func(size remotecommand.TerminalSize) { 248 | c.resizeContainerTTY(cfg, container, uint(size.Height), uint(size.Width)) 249 | }) 250 | 251 | opts := types.ContainerAttachOptions{ 252 | Stream: true, 253 | Stdin: cfg.stdin != nil, 254 | Stdout: cfg.stdout != nil, 255 | Stderr: cfg.stderr != nil, 256 | } 257 | ctx, cancel := cfg.getContextWithTimeout() 258 | defer cancel() 259 | resp, err := c.client.ContainerAttach(ctx, container, opts) 260 | if err != nil { 261 | return err 262 | } 263 | defer resp.Close() 264 | 265 | return c.holdHijackedConnection(cfg, resp) 266 | } 267 | 268 | func (c *DockerContainerRuntime) resizeContainerTTY(cfg RunConfig, id string, height, width uint) error { 269 | ctx, cancel := cfg.getContextWithTimeout() 270 | defer cancel() 271 | return c.client.ContainerResize(ctx, id, types.ResizeOptions{ 272 | Height: height, 273 | Width: width, 274 | }) 275 | } 276 | 277 | // holdHijackedConnection hold the HijackedResponse, redirect the inputStream to the connection, and redirect the response 278 | // stream to stdout and stderr. NOTE: If needed, we could also add context in this function. 279 | func (c *DockerContainerRuntime) holdHijackedConnection(cfg RunConfig, resp types.HijackedResponse) error { 280 | receiveStdout := make(chan error) 281 | if cfg.stdout != nil || cfg.stderr != nil { 282 | go func() { 283 | receiveStdout <- c.redirectResponseToOutputStream(cfg, resp.Reader) 284 | }() 285 | } 286 | 287 | stdinDone := make(chan struct{}) 288 | go func() { 289 | if cfg.stdin != nil { 290 | io.Copy(resp.Conn, cfg.stdin) 291 | } 292 | resp.CloseWrite() 293 | close(stdinDone) 294 | }() 295 | 296 | select { 297 | case err := <-receiveStdout: 298 | return err 299 | case <-stdinDone: 300 | if cfg.stdout != nil || cfg.stderr != nil { 301 | return <-receiveStdout 302 | } 303 | } 304 | return nil 305 | } 306 | 307 | func (c *DockerContainerRuntime) redirectResponseToOutputStream(cfg RunConfig, resp io.Reader) error { 308 | var stdout io.Writer = cfg.stdout 309 | if stdout == nil { 310 | stdout = ioutil.Discard 311 | } 312 | var stderr io.Writer = cfg.stderr 313 | if stderr == nil { 314 | stderr = ioutil.Discard 315 | } 316 | var err error 317 | if cfg.tty { 318 | _, err = io.Copy(stdout, resp) 319 | } else { 320 | _, err = stdcopy.StdCopy(stdout, stderr, resp) 321 | } 322 | return err 323 | } 324 | 325 | type ContainerdContainerRuntime struct { 326 | client *containerd.Client 327 | image containerd.Image 328 | } 329 | 330 | var ContainerdContainerRuntimeImplementsContainerRuntime ContainerRuntime = (*ContainerdContainerRuntime)(nil) 331 | 332 | var PushTracker = docker.NewInMemoryTracker() 333 | 334 | type jobs struct { 335 | name string 336 | added map[digest.Digest]struct{} 337 | descs []ocispec.Descriptor 338 | mu sync.Mutex 339 | resolved bool 340 | } 341 | 342 | func (j *jobs) isResolved() bool { 343 | j.mu.Lock() 344 | defer j.mu.Unlock() 345 | return j.resolved 346 | } 347 | 348 | func (j *jobs) jobs() []ocispec.Descriptor { 349 | j.mu.Lock() 350 | defer j.mu.Unlock() 351 | 352 | var descs []ocispec.Descriptor 353 | return append(descs, j.descs...) 354 | } 355 | 356 | func newJobs(name string) *jobs { 357 | return &jobs{ 358 | name: name, 359 | added: map[digest.Digest]struct{}{}, 360 | } 361 | } 362 | 363 | type StatusInfo struct { 364 | Ref string 365 | Status string 366 | Offset int64 367 | Total int64 368 | StartedAt time.Time 369 | UpdatedAt time.Time 370 | } 371 | 372 | func Display(w io.Writer, statuses []StatusInfo, start time.Time) { 373 | var total int64 374 | for _, status := range statuses { 375 | total += status.Offset 376 | switch status.Status { 377 | case "downloading", "uploading": 378 | var bar progress.Bar 379 | if status.Total > 0.0 { 380 | bar = progress.Bar(float64(status.Offset) / float64(status.Total)) 381 | } 382 | fmt.Fprintf(w, "%s:\t%s\t%40r\t%8.8s/%s\t\r\n", 383 | status.Ref, 384 | status.Status, 385 | bar, 386 | progress.Bytes(status.Offset), progress.Bytes(status.Total)) 387 | case "resolving", "waiting": 388 | bar := progress.Bar(0.0) 389 | fmt.Fprintf(w, "%s:\t%s\t%40r\t\r\n", 390 | status.Ref, 391 | status.Status, 392 | bar) 393 | default: 394 | bar := progress.Bar(1.0) 395 | fmt.Fprintf(w, "%s:\t%s\t%40r\t\r\n", 396 | status.Ref, 397 | status.Status, 398 | bar) 399 | } 400 | } 401 | 402 | fmt.Fprintf(w, "elapsed: %-4.1fs\ttotal: %7.6v\t(%v)\t\r\n", 403 | time.Since(start).Seconds(), 404 | // TODO(stevvooe): These calculations are actually way off. 405 | // Need to account for previously downloaded data. These 406 | // will basically be right for a download the first time 407 | // but will be skewed if restarting, as it includes the 408 | // data into the start time before. 409 | progress.Bytes(total), 410 | progress.NewBytesPerSecond(total, time.Since(start))) 411 | } 412 | 413 | func showProgress(ctx context.Context, ongoing *jobs, cs content.Store, out io.Writer) { 414 | var ( 415 | ticker = time.NewTicker(100 * time.Millisecond) 416 | fw = progress.NewWriter(out) 417 | start = time.Now() 418 | statuses = map[string]StatusInfo{} 419 | done bool 420 | ) 421 | defer ticker.Stop() 422 | 423 | outer: 424 | for { 425 | select { 426 | case <-ticker.C: 427 | fw.Flush() 428 | 429 | tw := tabwriter.NewWriter(fw, 1, 8, 1, ' ', 0) 430 | 431 | resolved := "resolved" 432 | if !ongoing.isResolved() { 433 | resolved = "resolving" 434 | } 435 | statuses[ongoing.name] = StatusInfo{ 436 | Ref: ongoing.name, 437 | Status: resolved, 438 | } 439 | keys := []string{ongoing.name} 440 | 441 | activeSeen := map[string]struct{}{} 442 | if !done { 443 | active, err := cs.ListStatuses(ctx, "") 444 | if err != nil { 445 | glog.G(ctx).WithError(err).Error("active check failed") 446 | continue 447 | } 448 | // update status of active entries! 449 | for _, active := range active { 450 | statuses[active.Ref] = StatusInfo{ 451 | Ref: active.Ref, 452 | Status: "downloading", 453 | Offset: active.Offset, 454 | Total: active.Total, 455 | StartedAt: active.StartedAt, 456 | UpdatedAt: active.UpdatedAt, 457 | } 458 | activeSeen[active.Ref] = struct{}{} 459 | } 460 | } 461 | 462 | // now, update the items in jobs that are not in active 463 | for _, j := range ongoing.jobs() { 464 | key := remotes.MakeRefKey(ctx, j) 465 | keys = append(keys, key) 466 | if _, ok := activeSeen[key]; ok { 467 | continue 468 | } 469 | 470 | status, ok := statuses[key] 471 | if !done && (!ok || status.Status == "downloading") { 472 | info, err := cs.Info(ctx, j.Digest) 473 | if err != nil { 474 | if !errdefs.IsNotFound(err) { 475 | glog.G(ctx).WithError(err).Errorf("failed to get content info") 476 | continue outer 477 | } else { 478 | statuses[key] = StatusInfo{ 479 | Ref: key, 480 | Status: "waiting", 481 | } 482 | } 483 | } else if info.CreatedAt.After(start) { 484 | statuses[key] = StatusInfo{ 485 | Ref: key, 486 | Status: "done", 487 | Offset: info.Size, 488 | Total: info.Size, 489 | UpdatedAt: info.CreatedAt, 490 | } 491 | } else { 492 | statuses[key] = StatusInfo{ 493 | Ref: key, 494 | Status: "exists", 495 | } 496 | } 497 | } else if done { 498 | if ok { 499 | if status.Status != "done" && status.Status != "exists" { 500 | status.Status = "done" 501 | statuses[key] = status 502 | } 503 | } else { 504 | statuses[key] = StatusInfo{ 505 | Ref: key, 506 | Status: "done", 507 | } 508 | } 509 | } 510 | } 511 | 512 | var ordered []StatusInfo 513 | for _, key := range keys { 514 | ordered = append(ordered, statuses[key]) 515 | } 516 | 517 | Display(tw, ordered, start) 518 | tw.Flush() 519 | 520 | if done { 521 | fw.Flush() 522 | return 523 | } 524 | case <-ctx.Done(): 525 | done = true // allow ui to update once more 526 | } 527 | } 528 | } 529 | 530 | func (c *ContainerdContainerRuntime) PullImage( 531 | ctx context.Context, image string, skipTLS bool, 532 | authStr string, 533 | cfg RunConfig) error { 534 | 535 | authStr, err := url.QueryUnescape(authStr) 536 | if err != nil { 537 | log.Printf("Failed to decode authStr: %v\r\n", err) 538 | return err 539 | } 540 | ctx = namespaces.WithNamespace(ctx, KubectlDebugNS) 541 | 542 | ongoing := newJobs(image) 543 | pctx, stopProgress := context.WithCancel(ctx) 544 | if cfg.verbosity > 0 { 545 | progress := make(chan struct{}) 546 | go func() { 547 | if cfg.stdout != nil { 548 | // no progress bar, because it hides some debug logs 549 | showProgress(pctx, ongoing, c.client.ContentStore(), cfg.stdout) 550 | } 551 | close(progress) 552 | }() 553 | } 554 | 555 | rslvrOpts := docker.ResolverOptions{ 556 | Tracker: PushTracker, 557 | } 558 | 559 | rmtOpts := []containerd.RemoteOpt{ 560 | containerd.WithPullUnpack, 561 | } 562 | 563 | crds := strings.Split(authStr, ":") 564 | if cfg.verbosity > 0 { 565 | log.Printf("User name for pull : %v\r\n", crds[0]) 566 | } 567 | var useCrds = len(crds) == 2 568 | if useCrds || skipTLS { 569 | tr := &http.Transport{ 570 | Proxy: http.ProxyFromEnvironment, 571 | DialContext: (&net.Dialer{ 572 | Timeout: 30 * time.Second, 573 | KeepAlive: 30 * time.Second, 574 | DualStack: true, 575 | }).DialContext, 576 | MaxIdleConns: 10, 577 | IdleConnTimeout: 30 * time.Second, 578 | TLSHandshakeTimeout: 10 * time.Second, 579 | TLSClientConfig: &tls.Config{ 580 | InsecureSkipVerify: skipTLS, 581 | }, 582 | ExpectContinueTimeout: 5 * time.Second, 583 | } 584 | 585 | rslvrOpts.Client = &http.Client{ 586 | Transport: tr, 587 | } 588 | 589 | if useCrds { 590 | if cfg.verbosity > 0 { 591 | log.Println("Setting credentials call back") 592 | } 593 | crdsClbck := func(host string) (string, string, error) { 594 | if cfg.verbosity > 0 { 595 | log.Printf("crdsClbck returning username: %v\r\n", crds[0]) 596 | } 597 | return crds[0], crds[1], nil 598 | } 599 | 600 | authOpts := []docker.AuthorizerOpt{ 601 | docker.WithAuthClient(rslvrOpts.Client), docker.WithAuthCreds(crdsClbck), 602 | } 603 | 604 | rslvrOpts.Authorizer = docker.NewDockerAuthorizer(authOpts...) 605 | } 606 | rmtOpts = append(rmtOpts, containerd.WithResolver(docker.NewResolver(rslvrOpts))) 607 | } 608 | 609 | c.image, err = c.client.Pull(ctx, image, rmtOpts...) 610 | stopProgress() 611 | 612 | if err != nil { 613 | log.Printf("Failed to download image: %v\r\n", err) 614 | return err 615 | } 616 | return err 617 | } 618 | 619 | func (c *ContainerdContainerRuntime) ContainerInfo( 620 | ctx context.Context, cfg RunConfig) (ContainerInfo, error) { 621 | var ret ContainerInfo 622 | ctx = namespaces.WithNamespace(ctx, K8NS) 623 | cntnr, err := c.client.LoadContainer(ctx, cfg.idOfContainerToDebug) 624 | if err != nil { 625 | log.Printf("Failed to access target container %s : %v\r\n", 626 | cfg.idOfContainerToDebug, err) 627 | 628 | return ContainerInfo{}, err 629 | } 630 | tsk, err := cntnr.Task(ctx, nil) 631 | if err != nil { 632 | log.Printf("Failed to get task of target container %s : %v\r\n", 633 | cfg.idOfContainerToDebug, err) 634 | 635 | return ContainerInfo{}, err 636 | } 637 | pids, err := tsk.Pids(ctx) 638 | if err != nil { 639 | log.Printf("Failed to get pids of target container %s : %v\r\n", 640 | cfg.idOfContainerToDebug, err) 641 | 642 | return ContainerInfo{}, err 643 | } 644 | 645 | info, err := cntnr.Info(ctx, containerd.WithoutRefreshedMetadata) 646 | if err != nil { 647 | log.Printf("Failed to load target container info %s : %v\r\n", 648 | cfg.idOfContainerToDebug, err) 649 | 650 | return ContainerInfo{}, err 651 | } 652 | 653 | if cfg.verbosity > 0 { 654 | log.Printf("Pids from target container: %+v\r\n", pids) 655 | } 656 | ret.Pid = int64(pids[0].Pid) 657 | if info.Spec != nil && info.Spec.Value != nil { 658 | v, err := typeurl.UnmarshalAny(info.Spec) 659 | if err != nil { 660 | log.Printf("Error unmarshalling spec for container %s : %v\r\n", 661 | cfg.idOfContainerToDebug, err) 662 | } 663 | for _, mnt := range v.(*specs.Spec).Mounts { 664 | ret.MountDestinations = append( 665 | ret.MountDestinations, mnt.Destination) 666 | fmt.Printf("%+v\r\n", mnt) 667 | } 668 | } 669 | return ret, nil 670 | } 671 | 672 | const ( 673 | // netNSFormat is the format of network namespace of a process. 674 | netNSFormat = "/proc/%v/ns/net" 675 | // ipcNSFormat is the format of ipc namespace of a process. 676 | ipcNSFormat = "/proc/%v/ns/ipc" 677 | // utsNSFormat is the format of uts namespace of a process. 678 | userNSFormat = "/proc/%v/ns/user" 679 | // pidNSFormat is the format of pid namespace of a process. 680 | pidNSFormat = "/proc/%v/ns/pid" 681 | ) 682 | 683 | func GetNetworkNamespace(pid int64) string { 684 | return fmt.Sprintf(netNSFormat, pid) 685 | } 686 | func GetIPCNamespace(pid int64) string { 687 | return fmt.Sprintf(ipcNSFormat, pid) 688 | } 689 | func GetUserNamespace(pid int64) string { 690 | return fmt.Sprintf(userNSFormat, pid) 691 | } 692 | func GetPIDNamespace(pid int64) string { 693 | return fmt.Sprintf(pidNSFormat, pid) 694 | } 695 | 696 | func (c *ContainerdContainerRuntime) RunDebugContainer(cfg RunConfig) error { 697 | defer c.client.Close() 698 | 699 | uuid := uuid.New().String() 700 | fifoNm := "" 701 | if cfg.audit { 702 | fifoDir, _ := path.Split(cfg.auditFifo) 703 | err := os.MkdirAll(fifoDir, 0777) 704 | if err != nil { 705 | fmt.Printf("Failed to create directory for audit fifos, %v : %v\r\n", fifoDir, err) 706 | return err 707 | } 708 | fifoNm = strings.ReplaceAll(cfg.auditFifo, "KCTLDBG-CONTAINER-ID", uuid) 709 | if cfg.verbosity > 0 { 710 | log.Printf("Creating fifo %v for receiving audit data.\r\n", fifoNm) 711 | } 712 | err = syscall.Mkfifo(fifoNm, 0600) 713 | if err != nil { 714 | fmt.Printf("Failed to create audit fifo %v : %v\r\n", fifoNm, err) 715 | return err 716 | } else { 717 | defer os.Remove(fifoNm) 718 | } 719 | 720 | go func() { 721 | log.Println("Audit read thread started.") 722 | fl, rdErr := os.Open(fifoNm) 723 | if rdErr != nil { 724 | log.Printf("Audit read thread aborting. Failed to open fifo : %v\r\n", rdErr) 725 | return 726 | } 727 | log.Println("Audit read thread started.") 728 | defer fl.Close() 729 | rdr := bufio.NewReader(fl) 730 | var ln []byte 731 | for { 732 | ln, _, rdErr = rdr.ReadLine() 733 | if rdErr != nil { 734 | break 735 | } 736 | log.Printf("audit - user: %v debugee: %v exec: %v\r\n", cfg.clientUserName, 737 | cfg.idOfContainerToDebug, string(ln)) 738 | } 739 | if rdErr != nil { 740 | if rdErr == io.EOF { 741 | log.Printf("EOF reached while reading from %v. Audit read thread exiting.\r\n", fifoNm) 742 | } else { 743 | log.Printf("Error %v while reading from %v. Audit read thread exiting.\r\n", rdErr, fifoNm) 744 | } 745 | } 746 | }() 747 | } 748 | // If audit, create thread for reading from fifo, defer clean up of thread 749 | ctx := namespaces.WithNamespace(cfg.context, KubectlDebugNS) 750 | 751 | var spcOpts []oci.SpecOpts 752 | spcOpts = append(spcOpts, oci.WithImageConfig(c.image)) 753 | spcOpts = append(spcOpts, oci.WithPrivileged) 754 | // if audit, build command vector array using shim + cfg.command 755 | // Make sure to replace KCTLDBG-FIFO with the actual fifo path ( Or maybe that is done before we get this far ) 756 | if cfg.audit { 757 | cmd := append([]string(nil), cfg.auditShim...) 758 | for i, s := range cmd { 759 | cmd[i] = strings.ReplaceAll(s, "KCTLDBG-FIFO", fifoNm) 760 | } 761 | cmd = append(cmd, cfg.command...) 762 | spcOpts = append(spcOpts, oci.WithProcessArgs(cmd...)) 763 | } else { 764 | spcOpts = append(spcOpts, oci.WithProcessArgs(cfg.command...)) 765 | } 766 | spcOpts = append(spcOpts, oci.WithTTY) 767 | // If fifo, make sure fifo is bind mounted 768 | trgtInf, err := c.ContainerInfo(ctx, cfg) 769 | if err != nil { 770 | log.Printf("Failed to get a pid from target container %s : %v\r\n", 771 | cfg.idOfContainerToDebug, err) 772 | return err 773 | } 774 | spcOpts = append(spcOpts, oci.WithLinuxNamespace(specs.LinuxNamespace{ 775 | Type: specs.NetworkNamespace, 776 | Path: GetNetworkNamespace(trgtInf.Pid), 777 | })) 778 | 779 | if fifoNm != "" { 780 | kbctlDbgMnt := specs.Mount{ 781 | Destination: fifoNm, 782 | Source: fifoNm, 783 | Type: "bind", 784 | Options: []string{"bind", "rw"}, 785 | } 786 | spcOpts = append(spcOpts, oci.WithMounts([]specs.Mount{kbctlDbgMnt})) 787 | } 788 | // 2020-04-21 d : 789 | // Tried setting the user namespace without success. 790 | // - If I just use WithLinuxNamespace and don't use WithUserNamespace 791 | // then I get the error "User namespaces enabled, but no uid mappings found.: unknown" 792 | // - If I then add WithUserNamespace I instead get the error 793 | // "getting the final child's pid from pipe caused \"EOF\"": unknown" 794 | // 795 | // I examined one of our environments, checked available kubernetes settings it seems 796 | // really all containers are sharing the host user namespace. I then stumbled on this 797 | // https://kubernetes.io/blog/2018/07/18/11-ways-not-to-get-hacked/ 798 | // article which claims that Kubernetes doesn't provide a way to set up separate user 799 | // namespaces for containers. 800 | // Consequently am just going to comment this out for now. 801 | // spcOpts = append(spcOpts, oci.WithLinuxNamespace(specs.LinuxNamespace{ 802 | // Type: specs.UserNamespace, 803 | // Path: GetUserNamespace(trgtInf.Pid), 804 | // })) 805 | // spcOpts = append(spcOpts, oci.WithUserNamespace(0, 0, 1024)) 806 | spcOpts = append(spcOpts, oci.WithLinuxNamespace(specs.LinuxNamespace{ 807 | Type: specs.IPCNamespace, 808 | Path: GetIPCNamespace(trgtInf.Pid), 809 | })) 810 | spcOpts = append(spcOpts, oci.WithLinuxNamespace(specs.LinuxNamespace{ 811 | Type: specs.PIDNamespace, 812 | Path: GetPIDNamespace(trgtInf.Pid), 813 | })) 814 | cntnr, err := c.client.NewContainer( 815 | ctx, 816 | // Was using "dbg-[idOfContainerToDebug]" but this meant that you couldn't use multiple debug containers for the same debugee 817 | // e.g. You couldn't have 1 running tcpdump and another one generating traffic. 818 | uuid, 819 | containerd.WithImage(c.image), 820 | containerd.WithNewSnapshot("netshoot-snapshot-"+uuid, c.image), // Had hoped this would fix 2020/04/17 17:04:31 runtime.go:672: Failed to create container for debugging 3d4059893a086fc7c59991fde9835ac7e35b754cd017a300292af9c721a4e6b9 : rootfs absolute path is required but it did not 821 | containerd.WithNewSpec(spcOpts...), 822 | ) 823 | 824 | if cntnr != nil { 825 | // Label the container so we have some idea of who created it and why 826 | lbls := make(map[string]string) 827 | lbls["ClientHostName"] = cfg.clientHostName 828 | lbls["ClientUserName"] = cfg.clientUserName 829 | lbls["IdOfDebuggee"] = cfg.idOfContainerToDebug 830 | cntnr.SetLabels(ctx, lbls) 831 | 832 | defer func() { 833 | cdctx, ccancel := context.WithTimeout(context.Background(), cfg.timeout) 834 | defer ccancel() 835 | cdctx = namespaces.WithNamespace(cdctx, KubectlDebugNS) 836 | cderr := cntnr.Delete(cdctx, containerd.WithSnapshotCleanup) 837 | if cderr != nil { 838 | log.Printf("Failed to delete container for debugging %s : %v\r\n", 839 | cfg.idOfContainerToDebug, cderr) 840 | } 841 | }() 842 | } 843 | 844 | if err != nil { 845 | log.Printf("Failed to create container for debugging %s : %v\r\n", 846 | cfg.idOfContainerToDebug, err) 847 | return err 848 | } 849 | 850 | var stdIo cio.Opt 851 | if cfg.stderr == nil { 852 | // 2020-04-21 d : Otherwise create fails with 853 | // E0421 14:16:36.797876 24356 attach.go:54] error attaching to container: failed to start io pipe copy: unable to copy pipes: containerd-shim: opening file "" failed: open : no such file or directory: unknown 854 | stdIo = cio.WithStreams(cfg.stdin, cfg.stdout, cfg.stdout) 855 | } else { 856 | stdIo = cio.WithStreams(cfg.stdin, cfg.stdout, cfg.stderr) 857 | } 858 | 859 | tsk, err := cntnr.NewTask(ctx, 860 | cio.NewCreator( 861 | stdIo, 862 | cio.WithTerminal, 863 | )) 864 | 865 | if tsk != nil { 866 | defer func() { 867 | tdctx, tcancel := context.WithTimeout(context.Background(), cfg.timeout) 868 | defer tcancel() 869 | tdctx = namespaces.WithNamespace(tdctx, KubectlDebugNS) 870 | _, tderr := tsk.Delete(tdctx, containerd.WithProcessKill) 871 | if tderr != nil { 872 | log.Printf("Failed to delete task for debugging %s : %v\r\n", 873 | cfg.idOfContainerToDebug, tderr) 874 | } 875 | }() 876 | } 877 | 878 | if err != nil { 879 | log.Printf("Failed to create task for debugging %s : %v\r\n", 880 | cfg.idOfContainerToDebug, err) 881 | return err 882 | } 883 | 884 | exitStatusC, err := tsk.Wait(ctx) 885 | if err != nil { 886 | log.Printf("Failed to get exit channel for task for debugging %s : %v\r\n", 887 | cfg.idOfContainerToDebug, err) 888 | return err 889 | } 890 | 891 | HandleResizing(cfg.resize, func(size remotecommand.TerminalSize) { 892 | c.resizeContainerTTY(ctx, cfg.idOfContainerToDebug, tsk, size.Height, 893 | size.Width) 894 | }) 895 | 896 | if err := tsk.Start(ctx); err != nil { 897 | return err 898 | } 899 | 900 | status := <-exitStatusC 901 | _, _, err = status.Result() 902 | if err != nil { 903 | log.Printf("Failed to get exit status for task for debugging %s : %v\r\n", 904 | cfg.idOfContainerToDebug, err) 905 | return err 906 | } 907 | 908 | return nil 909 | } 910 | 911 | func (c *ContainerdContainerRuntime) resizeContainerTTY(ctx context.Context, 912 | trgtId string, tsk containerd.Task, height, width uint16) error { 913 | err := tsk.Resize(ctx, uint32(width), uint32(height)) 914 | if err != nil { 915 | log.Printf("Failed to resize debugger task %+v for debuggee %+v : %+v\r\n", 916 | tsk.Pid(), trgtId, err) 917 | } 918 | return nil 919 | } 920 | 921 | // DebugAttacher implements Attacher 922 | // we use this struct in order to inject debug info (image, command) in the debug procedure 923 | type DebugAttacher struct { 924 | containerRuntime ContainerRuntime 925 | image string 926 | authStr string 927 | registrySkipTLS bool 928 | lxcfsEnabled bool 929 | command []string 930 | timeout time.Duration 931 | idOfContainerToDebug string 932 | verbosity int 933 | clientHostName string 934 | clientUserName string 935 | 936 | // control the preparing of debug container 937 | stopListenEOF chan struct{} 938 | context context.Context 939 | cancel context.CancelFunc 940 | 941 | // audit options 942 | audit bool 943 | auditFifo string 944 | auditShim []string 945 | } 946 | 947 | var DebugAttacherImplementsAttacher kubeletremote.Attacher = (*DebugAttacher)(nil) 948 | 949 | // Implement kubeletremote.Attacher 950 | func (a *DebugAttacher) AttachContainer(name string, uid kubetype.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error { 951 | if a.verbosity > 0 { 952 | log.Println("Enter") 953 | 954 | if resize == nil { 955 | log.Println("Resize channel is nil") 956 | } 957 | } 958 | 959 | return a.DebugContainer(RunConfig{ 960 | context: a.context, 961 | timeout: a.timeout, 962 | idOfContainerToDebug: a.idOfContainerToDebug, 963 | image: a.image, 964 | command: a.command, 965 | stdin: in, 966 | stdout: out, 967 | stderr: err, 968 | tty: tty, 969 | resize: resize, 970 | clientHostName: a.clientHostName, 971 | clientUserName: a.clientUserName, 972 | verbosity: a.verbosity, 973 | audit: a.audit, 974 | auditFifo: a.auditFifo, 975 | auditShim: a.auditShim, 976 | }) 977 | } 978 | 979 | // DebugContainer executes the main debug flow 980 | func (m *DebugAttacher) DebugContainer(cfg RunConfig) error { 981 | 982 | if m.verbosity > 0 { 983 | log.Printf("Accept new debug request:\n\t target container: %s \n\t image: %s \n\t command: %v \n\r", m.idOfContainerToDebug, m.image, m.command) 984 | } 985 | 986 | // the following steps may takes much time, 987 | // so we listen to EOF from stdin 988 | // which helps user to terminate the procedure proactively 989 | 990 | // FIXME: the following logic will 'eat' a character 991 | //var buf bytes.Buffer 992 | //tee := io.TeeReader(stdin, &buf) 993 | //go func() { 994 | // p := make([]byte, 4) 995 | // OUTER: 996 | // for { 997 | // select { 998 | // case <- m.stopListenEOF: 999 | // break OUTER 1000 | // default: 1001 | // n, err := tee.Read(p) 1002 | // // 4 -> EOT 1003 | // if (n > 0 && binary.LittleEndian.Uint32(p) == 4) || err == io.EOF { 1004 | // log.Println("receive ctrl-d or EOF when preparing debug container, cancel session") 1005 | // m.cancel() 1006 | // break OUTER 1007 | // } 1008 | // } 1009 | // } 1010 | //} () 1011 | // step 0: set container procfs to lxcfs 1012 | if cfg.verbosity > 0 { 1013 | cfg.stdout.Write([]byte(fmt.Sprintf("set container procfs lxcfs: %t .. \n\r", m.lxcfsEnabled))) 1014 | } 1015 | if m.lxcfsEnabled { 1016 | if err := CheckLxcfsMount(); err != nil { 1017 | return err 1018 | } 1019 | 1020 | if err := m.SetContainerLxcfs(cfg); err != nil { 1021 | return err 1022 | } 1023 | } 1024 | 1025 | // step 1: pull image 1026 | if cfg.verbosity > 0 { 1027 | cfg.stdout.Write([]byte(fmt.Sprintf("pulling image %s, skip TLS %v... \n\r", m.image, m.registrySkipTLS))) 1028 | } 1029 | err := m.containerRuntime.PullImage(m.context, m.image, m.registrySkipTLS, m.authStr, cfg) 1030 | if err != nil { 1031 | cfg.stdout.Write([]byte(fmt.Sprintf("pulling image %s, \n\r", m.image))) 1032 | return err 1033 | } 1034 | 1035 | // step 2: run debug container (join the namespaces of target container) 1036 | if cfg.verbosity > 0 { 1037 | cfg.stdout.Write([]byte("starting debug container...\n\r")) 1038 | } 1039 | return m.containerRuntime.RunDebugContainer(cfg) 1040 | } 1041 | 1042 | func (m *DebugAttacher) SetContainerLxcfs(cfg RunConfig) error { 1043 | ctx, cancel := cfg.getContextWithTimeout() 1044 | defer cancel() 1045 | cntnrInf, err := m.containerRuntime.ContainerInfo(ctx, cfg) 1046 | if err != nil { 1047 | return err 1048 | } 1049 | for _, mntDst := range cntnrInf.MountDestinations { 1050 | if mntDst == LxcfsRootDir { 1051 | if cfg.verbosity > 0 { 1052 | log.Printf("remount lxcfs when the rootdir of lxcfs of target container has been mounted. \n\t ") 1053 | } 1054 | for _, procfile := range LxcfsProcFiles { 1055 | nsenter := &nsenter.MountNSEnter{ 1056 | Target: cntnrInf.Pid, 1057 | MountLxcfs: true, 1058 | } 1059 | _, stderr, err := nsenter.Execute("--", "mount", "-B", LxcfsHomeDir+procfile, procfile) 1060 | if err != nil { 1061 | log.Printf("bind mount lxcfs files failed. \n\t reason: %s", stderr) 1062 | return err 1063 | } 1064 | } 1065 | } 1066 | } 1067 | 1068 | return nil 1069 | } 1070 | 1071 | // RuntimeManager is responsible for docker operation 1072 | type RuntimeManager struct { 1073 | dockerClient *dockerclient.Client 1074 | containerdClient *containerd.Client 1075 | timeout time.Duration 1076 | verbosity int 1077 | idOfContainerToDebug string 1078 | containerScheme ContainerRuntimeScheme 1079 | clientHostName string 1080 | clientUserName string 1081 | audit bool 1082 | auditFifo string 1083 | auditShim []string 1084 | } 1085 | 1086 | func NewRuntimeManager(srvCfg Config, containerUri string, verbosity int, 1087 | hstNm, usrNm string) (*RuntimeManager, error) { 1088 | if len(containerUri) < 1 { 1089 | return nil, errors.New("target container id must be provided") 1090 | } 1091 | containerUriParts := strings.SplitN(containerUri, "://", 2) 1092 | if len(containerUriParts) != 2 { 1093 | msg := fmt.Sprintf("target container id must have form scheme:id but was %v", containerUri) 1094 | if verbosity > 0 { 1095 | log.Println(msg) 1096 | } 1097 | return nil, errors.New(msg) 1098 | } 1099 | containerScheme := ContainerRuntimeScheme(containerUriParts[0]) 1100 | idOfContainerToDebug := containerUriParts[1] 1101 | 1102 | var dockerClient *dockerclient.Client 1103 | var containerdClient *containerd.Client 1104 | switch containerScheme { 1105 | case DockerScheme: 1106 | { 1107 | var err error 1108 | dockerClient, err = dockerclient.NewClient(srvCfg.DockerEndpoint, "", nil, nil) 1109 | if err != nil { 1110 | return nil, err 1111 | } 1112 | } 1113 | case ContainerdScheme: 1114 | { 1115 | var err error 1116 | var clntOpts []containerd.ClientOpt 1117 | if os.Getenv("KCTLDBG_CONTAINERDV1_SHIM") != "" { 1118 | if verbosity > 0 { 1119 | log.Println("Using containerd v1 runtime") 1120 | } 1121 | clntOpts = append(clntOpts, 1122 | containerd.WithDefaultRuntime("io.containerd.runc.v1")) 1123 | } 1124 | containerdClient, err = containerd.New(srvCfg.ContainerdEndpoint, 1125 | clntOpts...) 1126 | if err != nil { 1127 | return nil, err 1128 | } 1129 | } 1130 | default: 1131 | { 1132 | msg := "only docker and containerd container runtimes are suppored right now" 1133 | log.Println(msg) 1134 | return nil, errors.New(msg) 1135 | } 1136 | } 1137 | 1138 | return &RuntimeManager{ 1139 | dockerClient: dockerClient, 1140 | containerdClient: containerdClient, 1141 | timeout: srvCfg.RuntimeTimeout, 1142 | verbosity: verbosity, 1143 | idOfContainerToDebug: idOfContainerToDebug, 1144 | containerScheme: containerScheme, 1145 | clientHostName: hstNm, 1146 | clientUserName: usrNm, 1147 | audit: srvCfg.Audit, 1148 | auditFifo: srvCfg.AuditFifo, 1149 | auditShim: srvCfg.AuditShim, 1150 | }, nil 1151 | } 1152 | 1153 | // GetAttacher returns an implementation of Attacher 1154 | func (m *RuntimeManager) GetAttacher(image, authStr string, 1155 | lxcfsEnabled, registrySkipTLS bool, 1156 | command []string, context context.Context, 1157 | cancel context.CancelFunc) kubeletremote.Attacher { 1158 | var containerRuntime ContainerRuntime 1159 | if m.dockerClient != nil { 1160 | containerRuntime = &DockerContainerRuntime{ 1161 | client: m.dockerClient, 1162 | } 1163 | } else { 1164 | containerRuntime = &ContainerdContainerRuntime{ 1165 | client: m.containerdClient, 1166 | } 1167 | } 1168 | return &DebugAttacher{ 1169 | containerRuntime: containerRuntime, 1170 | image: image, 1171 | authStr: authStr, 1172 | lxcfsEnabled: lxcfsEnabled, 1173 | registrySkipTLS: registrySkipTLS, 1174 | command: command, 1175 | context: context, 1176 | idOfContainerToDebug: m.idOfContainerToDebug, 1177 | verbosity: m.verbosity, 1178 | timeout: m.timeout, 1179 | cancel: cancel, 1180 | stopListenEOF: make(chan struct{}), 1181 | clientHostName: m.clientHostName, 1182 | clientUserName: m.clientUserName, 1183 | audit: m.audit, 1184 | auditFifo: m.auditFifo, 1185 | auditShim: m.auditShim, 1186 | } 1187 | } 1188 | -------------------------------------------------------------------------------- /pkg/debug-agent/server.go: -------------------------------------------------------------------------------- 1 | package debugagent 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | remoteapi "k8s.io/apimachinery/pkg/util/remotecommand" 16 | kubeletremote "k8s.io/kubernetes/pkg/kubelet/server/remotecommand" 17 | ) 18 | 19 | type Server struct { 20 | config *Config 21 | } 22 | 23 | func NewServer(config *Config) (*Server, error) { 24 | return &Server{config: config}, nil 25 | } 26 | 27 | func (s *Server) Run() error { 28 | 29 | stop := make(chan os.Signal, 1) 30 | signal.Notify(stop, os.Interrupt) 31 | 32 | mux := http.NewServeMux() 33 | mux.HandleFunc("/api/v1/debug", s.ServeDebug) 34 | mux.HandleFunc("/healthz", s.Healthz) 35 | server := &http.Server{Addr: s.config.ListenAddress, Handler: mux} 36 | 37 | go func() { 38 | log.Printf("Listening on %s \n", s.config.ListenAddress) 39 | 40 | if err := server.ListenAndServe(); err != nil { 41 | log.Fatal(err) 42 | } 43 | }() 44 | <-stop 45 | 46 | log.Println("shutting down server...") 47 | 48 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 49 | defer cancel() 50 | server.Shutdown(ctx) 51 | 52 | return nil 53 | } 54 | 55 | func maxInt(lhs, rhs int) int { 56 | if lhs >= rhs { 57 | return lhs 58 | } 59 | return rhs 60 | } 61 | 62 | // ServeDebug serves the debug request. 63 | // first, it will upgrade the connection to SPDY. 64 | // then, server will try to create the debug container, and sent creating progress to user via SPDY. 65 | // after the debug container running, server attach to the debug container and pipe the streams to user. 66 | // once connection closed, server killed the debug container and release related resources 67 | // if any error occurs above, an error status were written to the user's stderr. 68 | func (s *Server) ServeDebug(w http.ResponseWriter, req *http.Request) { 69 | 70 | log.Println("received debug request") 71 | containerUri := req.FormValue("container") 72 | 73 | sverbosity := req.FormValue("verbosity") 74 | if sverbosity == "" { 75 | sverbosity = "0" 76 | } 77 | iverbosity, _ := strconv.Atoi(sverbosity) 78 | 79 | imageFromPlugin := req.FormValue("image") 80 | imageFromEnv := os.Getenv("KCTLDBG_RESTRICT_IMAGE_TO") 81 | var image string 82 | if len(imageFromEnv) > 0 { 83 | image = imageFromEnv 84 | if imageFromPlugin != imageFromEnv && iverbosity > 0 { 85 | log.Printf("Using image %v, specified by env var KCTLDBG_RESTRICT_IMAGE_TO on agent, instead of image %v specified by client.\r\n", 86 | imageFromEnv, imageFromPlugin) 87 | } 88 | } else { 89 | image = imageFromPlugin 90 | } 91 | if len(image) < 1 { 92 | http.Error(w, "image must be provided", 400) 93 | return 94 | } 95 | command := req.FormValue("command") 96 | var commandSlice []string 97 | err := json.Unmarshal([]byte(command), &commandSlice) 98 | if err != nil || len(commandSlice) < 1 { 99 | http.Error(w, "cannot parse command", 400) 100 | return 101 | } 102 | authStr := req.FormValue("authStr") 103 | streamOpts := &kubeletremote.Options{ 104 | Stdin: true, 105 | Stdout: true, 106 | Stderr: false, 107 | TTY: true, 108 | } 109 | lxcfsEnabled := req.FormValue("lxcfsEnabled") 110 | if lxcfsEnabled == "" || lxcfsEnabled == "false" { 111 | LxcfsEnabled = false 112 | } else if lxcfsEnabled == "true" { 113 | LxcfsEnabled = true 114 | } 115 | var registrySkipTLS bool 116 | registrySkipTLSParam := req.FormValue("registrySkipTLS") 117 | if registrySkipTLSParam == "" || registrySkipTLSParam == "false" { 118 | registrySkipTLS = false 119 | } else if registrySkipTLSParam == "true" { 120 | registrySkipTLS = true 121 | } 122 | 123 | context, cancel := context.WithCancel(req.Context()) 124 | defer cancel() 125 | 126 | runtime, err := NewRuntimeManager(*s.config, containerUri, 127 | maxInt(iverbosity, s.config.Verbosity), 128 | req.FormValue("hostname"), 129 | req.FormValue("username")) 130 | if err != nil { 131 | msg := fmt.Sprintf("Failed to construct RuntimeManager. Error: %s", err.Error()) 132 | log.Println(msg) 133 | // 2020-04-15 d : 134 | // The client will be in SPDY roundtripper when we return this. This passes the response to 135 | // statusCodecs.UniversalDecoder().Decode. Decode will see any ":" as indication that the 136 | // response bytes are an object to be deserialized and consequently our message to the client 137 | // will be lost. 138 | http.Error(w, strings.ReplaceAll(msg, ":", "-"), 400) 139 | return 140 | } 141 | 142 | // replace Attacher implementation to hook the ServeAttach procedure 143 | if s.config.Verbosity > 0 { 144 | log.Println("Invoking kubeletremote.ServeAttach") 145 | } 146 | 147 | kubeletremote.ServeAttach( 148 | w, 149 | req, 150 | runtime.GetAttacher(image, authStr, LxcfsEnabled, registrySkipTLS, 151 | commandSlice, context, cancel), 152 | "", 153 | "", 154 | "", 155 | streamOpts, 156 | s.config.StreamIdleTimeout, 157 | s.config.StreamCreationTimeout, 158 | remoteapi.SupportedStreamingProtocols) 159 | if s.config.Verbosity > 0 { 160 | log.Println("kubeletremote.ServeAttach returned") 161 | } 162 | } 163 | 164 | func (s *Server) Healthz(w http.ResponseWriter, req *http.Request) { 165 | w.Write([]byte("I am OK")) 166 | } 167 | -------------------------------------------------------------------------------- /pkg/kubectl-debug/cmd.go: -------------------------------------------------------------------------------- 1 | package kubectldebug 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "path/filepath" 14 | "strconv" 15 | "sync" 16 | "time" 17 | 18 | dockerterm "github.com/docker/docker/pkg/term" 19 | "github.com/rs/xid" 20 | "github.com/spf13/cobra" 21 | authorizationv1 "k8s.io/api/authorization/v1" 22 | corev1 "k8s.io/api/core/v1" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | "k8s.io/apimachinery/pkg/api/resource" 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/labels" 27 | "k8s.io/apimachinery/pkg/util/intstr" 28 | "k8s.io/apimachinery/pkg/util/uuid" 29 | "k8s.io/cli-runtime/pkg/genericclioptions" 30 | "k8s.io/client-go/kubernetes" 31 | coreclient "k8s.io/client-go/kubernetes/typed/core/v1" 32 | restclient "k8s.io/client-go/rest" 33 | cmdapi "k8s.io/client-go/tools/clientcmd/api" 34 | "k8s.io/client-go/tools/portforward" 35 | "k8s.io/client-go/tools/remotecommand" 36 | "k8s.io/client-go/tools/watch" 37 | "k8s.io/client-go/transport/spdy" 38 | "k8s.io/kubernetes/pkg/client/conditions" 39 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 40 | "k8s.io/kubernetes/pkg/util/interrupt" 41 | 42 | term "github.com/jamestgrant/kubectl-debug/pkg/util" 43 | "github.com/jamestgrant/kubectl-debug/version" 44 | ) 45 | 46 | const ( 47 | example = ` 48 | # print the help 49 | kubectl-debug -h 50 | 51 | # start the debug container in the same namespace, and cgroup etc as container 'CONTAINER_NAME' 52 | # in pod 'POD_NAME' in namespace 'NAMESPACE' 53 | kubectl-debug --namespace NAMESPACE POD_NAME -c TARGET_CONTAINER_NAME 54 | 55 | # in case of your pod stuck in CrashLoopBackoff state and cannot be connected to, 56 | # you can fork a new pod and diagnose the problem in the forked pod 57 | kubectl-debug --namespace NAMESPACE POD_NAME -c CONTAINER_NAME --fork 58 | 59 | # In 'fork' mode, if you want the copied pod to retain the labels of the original pod, you can 60 | # use the --fork-pod-retain-labels parameter (comma separated, no spaces). If not set (default), 61 | # this parameter is empty and so any labels of the original pod are not retained, and the labels 62 | # of the copied pods are empty. 63 | # Example of fork mode: 64 | kubectl-debug --namespace NAMESPACE POD_NAME -c CONTAINER_NAME --fork --fork-pod-retain-labels=,, 65 | 66 | # in order to interact with the debug-agent pod on a node which doesn't have a public IP or direct 67 | # access (firewall and other reasons) to access, port-forward mode is enabled by default. if you don't 68 | # want port-forward mode, you can use --port-forward false to turn off it. I don't know why you'd want 69 | # to do this, but you can if you want. 70 | kubectl-debug --port-forward=false --namespace NAMESPACE POD_NAME -c CONTAINER_NAME 71 | 72 | # you can choose a different debug container image. By default, nicolaka/netshoot:latest will be used 73 | # but you can specify anything you like 74 | kubectl-debug --namespace NAMESPACE POD_NAME -c CONTAINER_NAME --image nicolaka/netshoot:latest 75 | 76 | # you can set the debug-agent pod's resource limits/requests, for example: 77 | # default is not set 78 | kubectl-debug --namespace NAMESPACE POD_NAME -c CONTAINER_NAME --agent-pod-cpu-requests=250m --agent-pod-cpu-limits=500m --agent-pod-memory-requests=200Mi --agent-pod-memory-limits=500Mi 79 | 80 | # use primary docker registry, set registry kubernetes secret to pull image 81 | # the default registry-secret-name is kubectl-debug-registry-secret, the default namespace is default 82 | # please set the secret data source as {Username: , Password: } 83 | kubectl-debug --namespace NAMESPACE POD_NAME --image nicolaka/netshoot:latest --registry-secret-name --registry-secret-namespace 84 | ` 85 | longDesc = ` 86 | kubectl-debug is an 'out-of-tree' solution for connecting to and troubleshooting an existing, 87 | running, 'target' container in an existing pod in a Kubernetes cluster. 88 | The target container may have a shell and busybox utils and hence provide some debug capability or it 89 | may be very minimal and not even provide a shell - which makes any real-time troubleshooting/debugging 90 | very difficult. kubectl-debug is designed to overcome that difficulty. 91 | ` 92 | usageError = "run like this: kubectl-debug --namespace NAMESPACE POD_NAME -c TARGET_CONTAINER_NAME" 93 | defaultDebugContainerImage = "docker.io/nicolaka/netshoot:latest" 94 | 95 | defaultDebugAgentPort = 10027 96 | defaultDebugAgentConfigFileLocation = "/tmp/debugAgentConfigFile" 97 | defaultDebugAgentImage = "jamesgrantmediakind/debug-agent:latest" 98 | defaultDebugAgentImagePullPolicy = string(corev1.PullIfNotPresent) 99 | defaultDebugAgentImagePullSecretName = "" 100 | defaultDebugAgentPodNamePrefix = "debug-agent-pod" 101 | defaultDebugAgentPodNamespace = "default" 102 | defaultDebugAgentPodCpuRequests = "" 103 | defaultDebugAgentPodCpuLimits = "" 104 | defaultDebugAgentPodMemoryRequests = "" 105 | defaultDebugAgentPodMemoryLimits = "" 106 | defaultDebugAgentDaemonSetName = "debug-agent" 107 | 108 | defaultRegistrySecretName = "kubectl-debug-registry-secret" 109 | defaultRegistrySecretNamespace = "default" 110 | defaultRegistrySkipTLSVerify = false 111 | defaultPortForward = true 112 | defaultCreateDebugAgentPod = true 113 | defaultLxcfsEnable = true 114 | defaultVerbosity = 0 115 | ) 116 | 117 | // DebugOptions specify how to run debug container in a running pod 118 | type DebugOptions struct { 119 | 120 | // target pod select options 121 | Namespace string 122 | PodName string 123 | Fork bool 124 | ForkPodRetainLabels []string 125 | 126 | // Debug-container options 127 | Image string 128 | RegistrySecretName string 129 | RegistrySecretNamespace string 130 | RegistrySkipTLSVerify bool 131 | IsLxcfsEnabled bool 132 | ContainerName string 133 | Command []string 134 | AppName string 135 | ConfigLocation string 136 | 137 | // Debug-agent options 138 | CreateDebugAgentPod bool 139 | AgentImage string 140 | AgentPort int 141 | AgentImagePullPolicy string 142 | AgentImagePullSecretName string 143 | 144 | // agentPodName = agentPodNamePrefix + nodeName 145 | AgentPodName string 146 | AgentPodNamespace string 147 | AgentPodNode string 148 | AgentPodResource agentPodResources 149 | 150 | Flags *genericclioptions.ConfigFlags 151 | CoreClient coreclient.CoreV1Interface 152 | KubeCli *kubernetes.Clientset 153 | Args []string 154 | Config *restclient.Config 155 | 156 | // use for port-forward 157 | RESTClient *restclient.RESTClient 158 | PortForwarder portForwarder 159 | Ports []string 160 | StopChannel chan struct{} 161 | ReadyChannel chan struct{} 162 | 163 | PortForward bool 164 | DebugAgentDaemonSet string 165 | DebugAgentNamespace string 166 | 167 | genericclioptions.IOStreams 168 | 169 | wait sync.WaitGroup 170 | 171 | Verbosity int 172 | Logger *log.Logger 173 | UserName string 174 | } 175 | 176 | type agentPodResources struct { 177 | CpuRequests string 178 | CpuLimits string 179 | MemoryRequests string 180 | MemoryLimits string 181 | } 182 | 183 | // NewDebugOptions new debug options 184 | func NewDebugOptions(streams genericclioptions.IOStreams) *DebugOptions { 185 | return &DebugOptions{ 186 | Flags: genericclioptions.NewConfigFlags(), 187 | IOStreams: streams, 188 | PortForwarder: &defaultPortForwarder{ 189 | IOStreams: streams, 190 | }, 191 | Logger: log.New(streams.Out, "kubectl-debug ", (log.LstdFlags | log.Lshortfile)), 192 | } 193 | } 194 | 195 | // NewDebugCmd returns a cobra command wrapping DebugOptions 196 | func NewDebugCmd(streams genericclioptions.IOStreams) *cobra.Command { 197 | opts := NewDebugOptions(streams) 198 | 199 | cmd := &cobra.Command{ 200 | Use: "kubectl-debug --namespace NAMESPACE POD_NAME -c TARGET_CONTAINER_NAME", 201 | DisableFlagsInUseLine: true, 202 | Short: "Launch a debug container, attached to a target container in a running pod", 203 | Long: longDesc, 204 | Example: example, 205 | Version: version.Version(), 206 | Run: func(c *cobra.Command, args []string) { 207 | argsLenAtDash := c.ArgsLenAtDash() 208 | cmdutil.CheckErr(opts.Complete(c, args, argsLenAtDash)) 209 | cmdutil.CheckErr(opts.Validate()) 210 | cmdutil.CheckErr(opts.Run()) 211 | }, 212 | } 213 | 214 | cmd.Flags().StringVar(&opts.Image, "image", "", 215 | fmt.Sprintf("the debug container image, default: %s", defaultDebugContainerImage)) 216 | 217 | cmd.Flags().StringVar(&opts.RegistrySecretName, "registry-secret-name", "", 218 | fmt.Sprintf("private registry secret name, default: %s", defaultRegistrySecretName)) 219 | 220 | cmd.Flags().StringVar(&opts.RegistrySecretNamespace, "registry-secret-namespace", "", 221 | fmt.Sprintf("private registry secret namespace, default: %s", defaultRegistrySecretNamespace)) 222 | 223 | cmd.Flags().BoolVar(&opts.RegistrySkipTLSVerify, "registry-skip-tls-verify", false, 224 | fmt.Sprintf("if true, the registry's certificate will not be checked for validity. This will make your HTTPS connections insecure, default: %s", defaultRegistrySkipTLSVerify)) 225 | 226 | cmd.Flags().StringSliceVar(&opts.ForkPodRetainLabels, "fork-pod-retain-labels", []string{}, 227 | "list of pod labels to retain when in fork mode, default: not set") 228 | 229 | cmd.Flags().StringVarP(&opts.ContainerName, "container", "c", "", 230 | "target container to debug, defaults to the first container in target pod spec") 231 | 232 | cmd.Flags().IntVarP(&opts.AgentPort, "port", "p", 0, 233 | fmt.Sprintf("debug-agent port to which kubectl-debug will connect, default: %d", defaultDebugAgentPort)) 234 | 235 | cmd.Flags().StringVar(&opts.ConfigLocation, "configfile", "", 236 | fmt.Sprintf("debug-agent config file (including path), if no config file is present at the specified location then default values are used. Default: %s", filepath.FromSlash(defaultDebugAgentConfigFileLocation))) 237 | 238 | cmd.Flags().BoolVar(&opts.Fork, "fork", false, 239 | "fork a new pod for debugging (useful if the pod status is CrashLoopBackoff)") 240 | 241 | cmd.Flags().BoolVar(&opts.PortForward, "port-forward", true, 242 | fmt.Sprintf("use port-forward to connect from kubectl-debug to debug-agent pod, default: %t", defaultPortForward)) 243 | 244 | // it may be that someone has already deployed a daemonset containing with the debug-agent pod and so we can use that (create-debug-agent-pod must be 'false' for this param to be used) 245 | cmd.Flags().StringVar(&opts.DebugAgentDaemonSet, "daemonset-name", opts.DebugAgentDaemonSet, 246 | fmt.Sprintf("debug agent daemonset name when using port-forward, default: %s", defaultDebugAgentDaemonSetName)) 247 | 248 | cmd.Flags().StringVar(&opts.DebugAgentNamespace, "debug-agent-namespace", opts.DebugAgentNamespace, 249 | fmt.Sprintf("namespace in which to create the debug-agent pod, default: %s", defaultDebugAgentPodNamespace)) 250 | 251 | // flags used for daemonsetless, aka createDebugAgentPod mode. 252 | cmd.Flags().BoolVarP(&opts.CreateDebugAgentPod, "create-debug-agent-pod", "a", true, 253 | fmt.Sprintf("debug-agent pod will be automatically created on the target host, default: %t", defaultCreateDebugAgentPod)) 254 | 255 | cmd.Flags().StringVar(&opts.AgentImage, "debug-agent-image", "", 256 | fmt.Sprintf("the image of the debug-agent container, default: %s", defaultDebugAgentImage)) 257 | 258 | cmd.Flags().StringVar(&opts.AgentImagePullPolicy, "agent-pull-policy", "", 259 | fmt.Sprintf("the debug-agent container image pull policy, default: %s", defaultDebugAgentImagePullPolicy)) 260 | 261 | cmd.Flags().StringVar(&opts.AgentImagePullSecretName, "agent-pull-secret-name", "", 262 | fmt.Sprintf("the debug-agent container image pull secret name, default to empty")) 263 | 264 | cmd.Flags().StringVar(&opts.AgentPodName, "agent-pod-name-prefix", "", 265 | fmt.Sprintf("debug-agent pod name prefix , default to %s", defaultDebugAgentPodNamePrefix)) 266 | 267 | cmd.Flags().StringVar(&opts.AgentPodNamespace, "agent-pod-namespace", "", 268 | fmt.Sprintf("agent pod namespace, default: %s", defaultDebugAgentPodNamespace)) 269 | 270 | cmd.Flags().StringVar(&opts.AgentPodResource.CpuRequests, "agent-pod-cpu-requests", "", 271 | fmt.Sprintf("agent pod cpu requests, default is not set")) 272 | 273 | cmd.Flags().StringVar(&opts.AgentPodResource.MemoryRequests, "agent-pod-memory-requests", "", 274 | fmt.Sprintf("agent pod memory requests, default is not set")) 275 | 276 | cmd.Flags().StringVar(&opts.AgentPodResource.CpuLimits, "agent-pod-cpu-limits", "", 277 | fmt.Sprintf("agent pod cpu limits, default is not set")) 278 | 279 | cmd.Flags().StringVar(&opts.AgentPodResource.MemoryLimits, "agent-pod-memory-limits", "", 280 | fmt.Sprintf("agent pod memory limits, default is not set")) 281 | 282 | cmd.Flags().BoolVarP(&opts.IsLxcfsEnabled, "enable-lxcfs", "", true, 283 | fmt.Sprintf("Enable Lxcfs, the target container can use its proc files, default: %t", defaultLxcfsEnable)) 284 | 285 | cmd.Flags().IntVarP(&opts.Verbosity, "verbosity", "v", 0, 286 | fmt.Sprintf("Set logging verbosity, default: %d", defaultVerbosity)) 287 | 288 | opts.Flags.AddFlags(cmd.Flags()) 289 | 290 | return cmd 291 | } 292 | 293 | // Complete populate default values from KUBECONFIG file 294 | func (o *DebugOptions) Complete(cmd *cobra.Command, args []string, argsLenAtDash int) error { 295 | o.Args = args 296 | if len(args) == 0 { 297 | return cmdutil.UsageErrorf(cmd, usageError) 298 | } 299 | 300 | var err error 301 | configLoader := o.Flags.ToRawKubeConfigLoader() 302 | o.Namespace, _, err = configLoader.Namespace() 303 | if err != nil { 304 | return err 305 | } 306 | 307 | matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(o.Flags) 308 | f := cmdutil.NewFactory(matchVersionKubeConfigFlags) 309 | o.RESTClient, err = f.RESTClient() 310 | if err != nil { 311 | return err 312 | } 313 | 314 | o.PodName = args[0] 315 | 316 | // read values from config file 317 | configFile := o.ConfigLocation 318 | if len(o.ConfigLocation) < 1 { 319 | if err == nil { 320 | configFile = filepath.FromSlash(defaultDebugAgentConfigFileLocation) 321 | } 322 | } 323 | config, err := LoadFile(configFile) 324 | if err != nil { 325 | if !os.IsNotExist(err) { 326 | // TODO: support verbosity level 327 | fmt.Fprintf(o.ErrOut, "error parsing configuration file: %v", err) 328 | } 329 | config = &Config{} 330 | } 331 | 332 | // combine hardcoded default values, configfile specified values and user cli specified values 333 | o.Command = args[1:] 334 | if len(o.Command) < 1 { 335 | if len(config.Command) > 0 { 336 | o.Command = config.Command 337 | } else { 338 | o.Command = []string{"bash"} 339 | } 340 | } 341 | 342 | if len(o.Image) < 1 { 343 | if len(config.Image) > 0 { 344 | o.Image = config.Image 345 | } else { 346 | o.Image = defaultDebugContainerImage 347 | } 348 | } 349 | 350 | if len(o.RegistrySecretName) < 1 { 351 | if len(config.RegistrySecretName) > 0 { 352 | o.RegistrySecretName = config.RegistrySecretName 353 | } else { 354 | o.RegistrySecretName = defaultRegistrySecretName 355 | } 356 | } 357 | 358 | if len(o.RegistrySecretNamespace) < 1 { 359 | if len(config.RegistrySecretNamespace) > 0 { 360 | o.RegistrySecretNamespace = config.RegistrySecretNamespace 361 | } else { 362 | o.RegistrySecretNamespace = defaultRegistrySecretNamespace 363 | } 364 | } 365 | 366 | if !o.RegistrySkipTLSVerify { 367 | if config.RegistrySkipTLSVerify { 368 | o.RegistrySkipTLSVerify = config.RegistrySkipTLSVerify 369 | } else { 370 | o.RegistrySkipTLSVerify = defaultRegistrySkipTLSVerify 371 | } 372 | } 373 | 374 | if len(o.ForkPodRetainLabels) < 1 { 375 | if len(config.ForkPodRetainLabels) > 0 { 376 | o.ForkPodRetainLabels = config.ForkPodRetainLabels 377 | } 378 | } 379 | 380 | if o.AgentPort < 1 { 381 | if config.AgentPort > 0 { 382 | o.AgentPort = config.AgentPort 383 | } else { 384 | o.AgentPort = defaultDebugAgentPort 385 | } 386 | } 387 | 388 | if len(o.DebugAgentNamespace) < 1 { 389 | if len(config.DebugAgentNamespace) > 0 { 390 | o.DebugAgentNamespace = config.DebugAgentNamespace 391 | } else { 392 | o.DebugAgentNamespace = defaultDebugAgentPodNamespace 393 | } 394 | } 395 | 396 | if len(o.DebugAgentDaemonSet) < 1 { 397 | if len(config.DebugAgentDaemonSet) > 0 { 398 | o.DebugAgentDaemonSet = config.DebugAgentDaemonSet 399 | } else { 400 | o.DebugAgentDaemonSet = defaultDebugAgentDaemonSetName 401 | } 402 | } 403 | 404 | if len(o.AgentPodName) < 1 { 405 | if len(config.AgentPodNamePrefix) > 0 { 406 | o.AgentPodName = config.AgentPodNamePrefix 407 | } else { 408 | o.AgentPodName = defaultDebugAgentPodNamePrefix 409 | } 410 | } 411 | 412 | if len(o.AgentImage) < 1 { 413 | if len(config.AgentImage) > 0 { 414 | o.AgentImage = config.AgentImage 415 | } else { 416 | o.AgentImage = defaultDebugAgentImage 417 | } 418 | } 419 | 420 | if len(o.AgentImagePullPolicy) < 1 { 421 | if len(config.AgentImagePullPolicy) > 0 { 422 | o.AgentImagePullPolicy = config.AgentImagePullPolicy 423 | } else { 424 | o.AgentImagePullPolicy = defaultDebugAgentImagePullPolicy 425 | } 426 | } 427 | 428 | if len(o.AgentImagePullSecretName) < 1 { 429 | if len(config.AgentImagePullSecretName) > 0 { 430 | o.AgentImagePullSecretName = config.AgentImagePullSecretName 431 | } else { 432 | o.AgentImagePullSecretName = defaultDebugAgentImagePullSecretName 433 | } 434 | } 435 | 436 | if len(o.AgentPodNamespace) < 1 { 437 | if len(config.AgentPodNamespace) > 0 { 438 | o.AgentPodNamespace = config.AgentPodNamespace 439 | } else { 440 | o.AgentPodNamespace = defaultDebugAgentPodNamespace 441 | } 442 | } 443 | 444 | if len(o.AgentPodResource.CpuRequests) < 1 { 445 | if len(config.AgentPodCpuRequests) > 0 { 446 | o.AgentPodResource.CpuRequests = config.AgentPodCpuRequests 447 | } else { 448 | o.AgentPodResource.CpuRequests = defaultDebugAgentPodCpuRequests 449 | } 450 | } 451 | 452 | if len(o.AgentPodResource.MemoryRequests) < 1 { 453 | if len(config.AgentPodMemoryRequests) > 0 { 454 | o.AgentPodResource.MemoryRequests = config.AgentPodMemoryRequests 455 | } else { 456 | o.AgentPodResource.MemoryRequests = defaultDebugAgentPodMemoryRequests 457 | } 458 | } 459 | 460 | if len(o.AgentPodResource.CpuLimits) < 1 { 461 | if len(config.AgentPodCpuLimits) > 0 { 462 | o.AgentPodResource.CpuLimits = config.AgentPodCpuLimits 463 | } else { 464 | o.AgentPodResource.CpuLimits = defaultDebugAgentPodCpuLimits 465 | } 466 | } 467 | 468 | if len(o.AgentPodResource.MemoryLimits) < 1 { 469 | if len(config.AgentPodMemoryLimits) > 0 { 470 | o.AgentPodResource.MemoryLimits = config.AgentPodMemoryLimits 471 | } else { 472 | o.AgentPodResource.MemoryLimits = defaultDebugAgentPodMemoryLimits 473 | } 474 | } 475 | 476 | if o.Verbosity < 1 { 477 | if config.Verbosity > 0 { 478 | o.Verbosity = config.Verbosity 479 | } else { 480 | o.Verbosity = defaultVerbosity 481 | } 482 | } 483 | 484 | if !o.IsLxcfsEnabled { 485 | if config.IsLxcfsEnabled { 486 | o.IsLxcfsEnabled = config.IsLxcfsEnabled 487 | } else { 488 | o.IsLxcfsEnabled = defaultLxcfsEnable 489 | } 490 | } 491 | 492 | if !o.CreateDebugAgentPod { 493 | if config.CreateDebugAgentPod { 494 | o.CreateDebugAgentPod = config.CreateDebugAgentPod 495 | } else { 496 | o.CreateDebugAgentPod = defaultCreateDebugAgentPod 497 | } 498 | } 499 | 500 | if !o.PortForward { 501 | if config.PortForward { 502 | o.PortForward = config.PortForward 503 | } else { 504 | o.PortForward = defaultPortForward 505 | } 506 | } 507 | 508 | o.Ports = []string{strconv.Itoa(o.AgentPort)} 509 | o.Config, err = configLoader.ClientConfig() 510 | if err != nil { 511 | return err 512 | } 513 | 514 | o.UserName = "unidentified user" 515 | // cli help for the flags referenced below can be viewed by running 516 | // kubectl options 517 | if o.Flags.Username != nil && len(*o.Flags.Username) > 0 { 518 | // --username : "Username for basic authentication to the API server" 519 | o.UserName = *o.Flags.Username 520 | log.Printf("User name '%v' received from switch --username\r\n", o.UserName) 521 | } else if o.Flags.AuthInfoName != nil && len(*o.Flags.AuthInfoName) > 0 { 522 | // --user : "The name of the kubeconfig user to use" 523 | o.UserName = *o.Flags.AuthInfoName 524 | log.Printf("User name '%v' received from switch --user\r\n", o.UserName) 525 | } else { 526 | rwCfg, err := configLoader.RawConfig() 527 | if err != nil { 528 | log.Printf("Failed to load configuration : %v\r\n", err) 529 | return err 530 | } 531 | var cfgCtxt *cmdapi.Context 532 | if o.Flags.Context != nil && len(*o.Flags.Context) > 0 { 533 | // --context : "The name of the kubeconfig context to use" 534 | cfgCtxt = rwCfg.Contexts[*o.Flags.Context] 535 | log.Printf("Getting user name from kubectl context '%v' received from switch --context\r\n", *o.Flags.Context) 536 | } else { 537 | cfgCtxt = rwCfg.Contexts[rwCfg.CurrentContext] 538 | log.Printf("Getting user name from default kubectl context '%v'\r\n", rwCfg.CurrentContext) 539 | } 540 | o.UserName = cfgCtxt.AuthInfo 541 | log.Printf("User name '%v' received from kubectl context\r\n", o.UserName) 542 | } 543 | 544 | clientset, err := kubernetes.NewForConfig(o.Config) 545 | if err != nil { 546 | return err 547 | } 548 | o.KubeCli = clientset 549 | o.CoreClient = clientset.CoreV1() 550 | o.StopChannel = make(chan struct{}, 1) 551 | o.ReadyChannel = make(chan struct{}) 552 | return nil 553 | } 554 | 555 | // Validate validate 556 | func (o *DebugOptions) Validate() error { 557 | if len(o.PodName) == 0 { 558 | return fmt.Errorf("target pod name must be specified") 559 | } 560 | if len(o.Command) == 0 { 561 | return fmt.Errorf("you must specify at least one command for the container") 562 | } 563 | return nil 564 | } 565 | 566 | // TODO: refactor Run() spaghetti code 567 | // Run run 568 | func (o *DebugOptions) Run() error { 569 | pod, err := o.CoreClient.Pods(o.Namespace).Get(o.PodName, v1.GetOptions{}) 570 | if err != nil { 571 | o.Logger.Printf("error with pod spec") 572 | return err 573 | } 574 | 575 | containerName := o.ContainerName 576 | if len(containerName) == 0 { 577 | if len(pod.Spec.Containers) > 1 { 578 | usageString := fmt.Sprintf("No container name specified, choosing container: %s.", pod.Spec.Containers[0].Name) 579 | fmt.Fprintf(o.ErrOut, "%s\n\r", usageString) 580 | } 581 | containerName = pod.Spec.Containers[0].Name 582 | } 583 | err = o.auth(pod) 584 | if err != nil { 585 | return err 586 | } 587 | // Launch debug launching pod in createDebugAgentPod mode. 588 | var agentPod *corev1.Pod 589 | if o.CreateDebugAgentPod { 590 | o.AgentPodNode = pod.Spec.NodeName 591 | o.AgentPodName = fmt.Sprintf("%s-%s", o.AgentPodName, uuid.NewUUID()) 592 | agentPod = o.getAgentPod() 593 | agentPod, err = o.launchPod(agentPod) 594 | if err != nil { 595 | fmt.Fprintf(o.Out, "the debug-agent pod is not running, you should check the reason, delete any failed debug-agent pod(s) and retry.\r\n") 596 | return err 597 | } 598 | } 599 | 600 | // in fork mode, we launch an new pod as a copy of target pod 601 | // and hack the entry point of the target container with sleep command 602 | // which keeps the container running. 603 | if o.Fork { 604 | // build the fork pod labels 605 | fmt.Fprintf(o.Out, "Forked mode selected\n") 606 | podLabels := o.buildForkPodLabels(pod) 607 | // copy pod and run 608 | pod = copyAndStripPod(pod, containerName, podLabels) 609 | pod, err = o.launchPod(pod) 610 | if err != nil { 611 | fmt.Fprintf(o.Out, "the ForkedPod is not running, you should check the reason and delete the failed ForkedPod and retry\r\n") 612 | o.deleteAgent(agentPod) 613 | return err 614 | } 615 | } 616 | 617 | if pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { 618 | o.deleteAgent(agentPod) 619 | return fmt.Errorf("cannot debug in a completed pod; current pod phase is %s", pod.Status.Phase) 620 | } 621 | 622 | containerID, err := o.getContainerIDByName(pod, containerName) 623 | if err != nil { 624 | fmt.Fprintf(o.Out, "an error occured, pod is %s, container name is: %s . Will clean up and exit.\r\n", pod, containerName) 625 | o.deleteAgent(agentPod) 626 | return err 627 | } 628 | 629 | t := o.setupTTY() 630 | var sizeQueue remotecommand.TerminalSizeQueue 631 | if t.Raw { 632 | // this call spawns a goroutine to monitor/update the terminal size 633 | sizeQueue = t.MonitorSize(t.GetSize()) 634 | // unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is 635 | // true 636 | // o.ErrOut = nil 637 | } 638 | 639 | if o.PortForward { 640 | var agent *corev1.Pod 641 | if !o.CreateDebugAgentPod { 642 | // See if there is a debug-agent pod running as a daemonset 643 | o.Logger.Printf("See if there is a debug-agent pod running in a daemonset. daemonset '%v' from namespace %v\r\n", o.DebugAgentDaemonSet, o.DebugAgentNamespace) 644 | 645 | daemonSet, err := o.KubeCli.AppsV1().DaemonSets(o.DebugAgentNamespace).Get(o.DebugAgentDaemonSet, v1.GetOptions{}) 646 | if err != nil { 647 | return err 648 | } 649 | labelSet := labels.Set(daemonSet.Spec.Selector.MatchLabels) 650 | agents, err := o.CoreClient.Pods(o.DebugAgentNamespace).List(v1.ListOptions{ 651 | LabelSelector: labelSet.String(), 652 | }) 653 | if err != nil { 654 | return err 655 | } 656 | for i := range agents.Items { 657 | if agents.Items[i].Spec.NodeName == pod.Spec.NodeName { 658 | agent = &agents.Items[i] 659 | break 660 | } 661 | } 662 | } else { 663 | agent = agentPod 664 | } 665 | 666 | if agent == nil { 667 | return fmt.Errorf("there is no debug-agent pod running on the same node as your target pod %s\r\n", o.PodName) 668 | } 669 | if o.Verbosity > 0 { 670 | fmt.Fprintf(o.Out, "target pod: %s target pod IP: %s, debug-agent pod IP: %s\r\n", o.PodName, pod.Status.PodIP, agent.Status.HostIP) 671 | } 672 | err = o.runPortForward(agent) 673 | if err != nil { 674 | fmt.Fprintf(o.Out, "an error has occured, will delete debug-agent pod and exit\r\n") 675 | o.deleteAgent(agentPod) 676 | return err 677 | } 678 | // client can't access the node ip in the k8s cluster sometimes, 679 | // than we use forward ports to connect the specified pod and that will listen 680 | // on specified ports in localhost, the ports can not access until receive the 681 | // ready signal 682 | if o.Verbosity > 0 { 683 | fmt.Fprintln(o.Out, "using port-forwarding. Waiting for port-forward connection with debug-agent...\r\n") 684 | } 685 | <-o.ReadyChannel 686 | } 687 | 688 | fn := func() error { 689 | // TODO: refactor as kubernetes api style, reuse rbac mechanism of kubernetes 690 | var targetHost string 691 | if o.PortForward { 692 | targetHost = "localhost" 693 | } else { 694 | targetHost = pod.Status.HostIP 695 | } 696 | uri, err := url.Parse(fmt.Sprintf("http://%s:%d", targetHost, o.AgentPort)) 697 | if err != nil { 698 | o.Logger.Printf("error parsing url http://%s:%d", targetHost, o.AgentPort) 699 | return err 700 | } 701 | uri.Path = fmt.Sprintf("/api/v1/debug") 702 | params := url.Values{} 703 | params.Add("image", o.Image) 704 | params.Add("container", containerID) 705 | params.Add("verbosity", fmt.Sprintf("%v", o.Verbosity)) 706 | hstNm, _ := os.Hostname() 707 | params.Add("hostname", hstNm) 708 | params.Add("username", o.UserName) 709 | if o.IsLxcfsEnabled { 710 | params.Add("lxcfsEnabled", "true") 711 | } else { 712 | params.Add("lxcfsEnabled", "false") 713 | } 714 | if o.RegistrySkipTLSVerify { 715 | params.Add("registrySkipTLS", "true") 716 | } else { 717 | params.Add("registrySkipTLS", "false") 718 | } 719 | var authStr string 720 | registrySecret, err := o.CoreClient.Secrets(o.RegistrySecretNamespace).Get(o.RegistrySecretName, v1.GetOptions{}) 721 | if err != nil { 722 | if errors.IsNotFound(err) { 723 | if o.Verbosity > 0 { 724 | o.Logger.Printf("Secret: %v not found in namespace: %v\r\n", o.RegistrySecretName, o.RegistrySecretNamespace) 725 | } 726 | authStr = "" 727 | } else { 728 | return err 729 | } 730 | } else { 731 | if o.Verbosity > 1 { 732 | o.Logger.Printf("Found secret: %v:%v\r\n", o.RegistrySecretNamespace, o.RegistrySecretName) 733 | } 734 | authStr, _ = o.extractSecret(registrySecret.Data) 735 | } 736 | params.Add("authStr", authStr) 737 | commandBytes, err := json.Marshal(o.Command) 738 | if err != nil { 739 | return err 740 | } 741 | params.Add("command", string(commandBytes)) 742 | uri.RawQuery = params.Encode() 743 | return o.remoteExecute("POST", uri, o.Config, o.In, o.Out, o.ErrOut, t.Raw, sizeQueue) 744 | } 745 | 746 | // ensure debug pod is deleted 747 | withCleanUp := func() error { 748 | return interrupt.Chain(nil, func() { 749 | if o.Fork { 750 | fmt.Fprintf(o.Out, "deleting forked pod: %s \n\r", pod.Name) 751 | err := o.CoreClient.Pods(pod.Namespace).Delete(pod.Name, v1.NewDeleteOptions(0)) 752 | if err != nil { 753 | // we may leak pod here, but we have nothing to do except notify the user 754 | fmt.Fprintf(o.ErrOut, "failed to delete forked pod: %s Namespace: %s, you may have to manually delete the pod.\n\r", pod.Name, pod.Namespace) 755 | } 756 | } 757 | 758 | if o.PortForward { 759 | // close the port-forward 760 | if o.StopChannel != nil { 761 | close(o.StopChannel) 762 | } 763 | } 764 | // delete agent pod 765 | if o.CreateDebugAgentPod && agentPod != nil { 766 | fmt.Fprintf(o.Out, "\n\rdeleting debug-agent pod\n\r") 767 | o.deleteAgent(agentPod) 768 | } 769 | }).Run(fn) 770 | } 771 | 772 | if err := t.Safe(withCleanUp); err != nil { 773 | fmt.Fprintf(o.Out, "an error occured executing remote command(s), %v\r\n", err) 774 | return err 775 | } 776 | o.wait.Wait() 777 | return nil 778 | } 779 | 780 | func (o *DebugOptions) extractSecret(scrtDta map[string][]byte) (string, error) { 781 | var ret []byte 782 | ret = scrtDta["authStr"] 783 | if len(ret) == 0 { 784 | // In IKS ( IBM Kubernetes ) the secret is stored in a json blob with the key '.dockerconfigjson' 785 | // The json has the form 786 | // {"auths":{"":{"username":"iamapikey","password":"","email":"iamapikey","auth":""}}} 787 | // Where would be one of the public domain names values here 788 | // https://cloud.ibm.com/docs/Registry?topic=registry-registry_overview#registry_regions_local 789 | // e.g. us.icr.io 790 | ret = scrtDta[".dockerconfigjson"] 791 | if len(ret) == 0 { 792 | return "", nil 793 | } else if o.Verbosity > 0 { 794 | o.Logger.Printf("Found secret with key .dockerconfigjson\r\n") 795 | } 796 | 797 | var dta map[string]interface{} 798 | if err := json.Unmarshal(ret, &dta); err != nil { 799 | o.Logger.Printf("Failed to parse .dockerconfigjson value: %v\r\n", err) 800 | return "", err 801 | } else { 802 | dta = dta["auths"].(map[string]interface{}) 803 | // Under auths there will be a value stored with the region key. e.g. "us.icr.io" 804 | for _, v := range dta { 805 | dta = v.(map[string]interface{}) 806 | break 807 | } 808 | sret := dta["auth"].(string) 809 | ret, err = base64.StdEncoding.DecodeString(sret) 810 | if err != nil { 811 | o.Logger.Printf("Failed to base 64 decode auth value : %v\r\n", err) 812 | return "", err 813 | } 814 | } 815 | } else if o.Verbosity > 0 { 816 | o.Logger.Println("Found secret with key authStr") 817 | } 818 | return string(ret), nil 819 | } 820 | 821 | func (o *DebugOptions) getContainerIDByName(pod *corev1.Pod, containerName string) (string, error) { 822 | for _, containerStatus := range pod.Status.ContainerStatuses { 823 | if containerStatus.Name != containerName { 824 | continue 825 | } 826 | // #52 if a pod is running but not ready(because of readiness probe), we can connect 827 | if containerStatus.State.Running == nil { 828 | return "", fmt.Errorf("container [%s] not running", containerName) 829 | } 830 | if o.Verbosity > 0 { 831 | o.Logger.Printf("Getting id from containerStatus %+v\r\n", containerStatus) 832 | } 833 | return containerStatus.ContainerID, nil 834 | } 835 | 836 | // #14 otherwise we should search for running init containers 837 | for _, initContainerStatus := range pod.Status.InitContainerStatuses { 838 | if initContainerStatus.Name != containerName { 839 | continue 840 | } 841 | if initContainerStatus.State.Running == nil { 842 | return "", fmt.Errorf("init container [%s] is not running", containerName) 843 | } 844 | if o.Verbosity > 0 { 845 | o.Logger.Printf("Getting id from initContainerStatus %+v\r\n", initContainerStatus) 846 | } 847 | return initContainerStatus.ContainerID, nil 848 | } 849 | 850 | return "", fmt.Errorf("cannot find specified container %s", containerName) 851 | } 852 | 853 | func (o *DebugOptions) remoteExecute( 854 | method string, 855 | url *url.URL, 856 | config *restclient.Config, 857 | stdin io.Reader, 858 | stdout, stderr io.Writer, 859 | tty bool, 860 | terminalSizeQueue remotecommand.TerminalSizeQueue) error { 861 | 862 | if o.Verbosity > 0 { 863 | o.Logger.Printf("Creating SPDY executor %+v %+v %+v\r\n", config, method, url) 864 | } 865 | exec, err := remotecommand.NewSPDYExecutor(config, method, url) 866 | if err != nil { 867 | o.Logger.Printf("Error creating SPDY executor.\r\n") 868 | return err 869 | } 870 | if o.Verbosity > 0 { 871 | o.Logger.Printf("Creating exec Stream\r\n") 872 | } 873 | return exec.Stream(remotecommand.StreamOptions{ 874 | Stdin: stdin, 875 | Stdout: stdout, 876 | Stderr: stderr, 877 | Tty: tty, 878 | TerminalSizeQueue: terminalSizeQueue, 879 | }) 880 | } 881 | 882 | func (o *DebugOptions) setupTTY() term.TTY { 883 | t := term.TTY{ 884 | Out: o.Out, 885 | } 886 | t.In = o.In 887 | t.Raw = true 888 | if !t.IsTerminalIn() { 889 | if o.ErrOut != nil { 890 | fmt.Fprintln(o.ErrOut, "Unable to use a TTY - input is not a terminal or the right kind of file\r\n") 891 | } 892 | return t 893 | } 894 | stdin, stdout, _ := dockerterm.StdStreams() 895 | o.In = stdin 896 | t.In = stdin 897 | if o.Out != nil { 898 | o.Out = stdout 899 | t.Out = stdout 900 | } 901 | return t 902 | } 903 | 904 | func (o *DebugOptions) buildForkPodLabels(pod *corev1.Pod) map[string]string { 905 | podLabels := map[string]string{} 906 | for _, label := range o.ForkPodRetainLabels { 907 | for k, v := range pod.ObjectMeta.Labels { 908 | if label == k { 909 | podLabels[k] = v 910 | } 911 | } 912 | } 913 | return podLabels 914 | } 915 | 916 | // copyAndStripPod copy the given pod template, strip the probes and labels, 917 | // and replace the entry point 918 | func copyAndStripPod(pod *corev1.Pod, targetContainer string, podLabels map[string]string) *corev1.Pod { 919 | copied := &corev1.Pod{ 920 | ObjectMeta: *pod.ObjectMeta.DeepCopy(), 921 | Spec: *pod.Spec.DeepCopy(), 922 | } 923 | // Using original pod name + xid + debug ad copied pod name. To ensure a 924 | // valid pod name we truncate original pod name to keep the total chars <64 925 | copied.Name = fmt.Sprintf("%.34s-%s-debug", pod.Name, xid.New().String()) 926 | copied.Labels = podLabels 927 | copied.Spec.RestartPolicy = corev1.RestartPolicyNever 928 | for i, c := range copied.Spec.Containers { 929 | copied.Spec.Containers[i].LivenessProbe = nil 930 | copied.Spec.Containers[i].ReadinessProbe = nil 931 | if c.Name == targetContainer { 932 | // Hack, infinite sleep command to keep the container running 933 | copied.Spec.Containers[i].Command = []string{"sh", "-c", "--"} 934 | copied.Spec.Containers[i].Args = []string{"while true; do sleep 30; done;"} 935 | } 936 | } 937 | copied.ResourceVersion = "" 938 | copied.UID = "" 939 | copied.SelfLink = "" 940 | copied.CreationTimestamp = v1.Time{} 941 | copied.OwnerReferences = []v1.OwnerReference{} 942 | 943 | return copied 944 | } 945 | 946 | // launchPod launch given pod until it's running 947 | func (o *DebugOptions) launchPod(pod *corev1.Pod) (*corev1.Pod, error) { 948 | pod, err := o.CoreClient.Pods(pod.Namespace).Create(pod) 949 | if err != nil { 950 | o.Logger.Printf("error with launch pod") 951 | return pod, err 952 | } 953 | 954 | watcher, err := o.CoreClient.Pods(pod.Namespace).Watch(v1.SingleObject(pod.ObjectMeta)) 955 | if err != nil { 956 | o.Logger.Printf("error with watching pod") 957 | return nil, err 958 | } 959 | // FIXME: hard code -> config 960 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 961 | defer cancel() 962 | fmt.Fprintf(o.Out, "Waiting for pod %s to run...\n", pod.Name) 963 | event, err := watch.UntilWithoutRetry(ctx, watcher, conditions.PodRunning) 964 | if err != nil { 965 | fmt.Fprintf(o.ErrOut, "Error occurred while waiting for pod to run: %v\r\n", err) 966 | return nil, err 967 | } 968 | pod = event.Object.(*corev1.Pod) 969 | return pod, nil 970 | } 971 | 972 | // getAgentPod construct debug-agent pod template 973 | func (o *DebugOptions) getAgentPod() *corev1.Pod { 974 | prop := corev1.MountPropagationBidirectional 975 | directoryCreate := corev1.HostPathDirectoryOrCreate 976 | priveleged := true 977 | agentPod := &corev1.Pod{ 978 | TypeMeta: v1.TypeMeta{ 979 | Kind: "Pod", 980 | APIVersion: "v1", 981 | }, 982 | ObjectMeta: v1.ObjectMeta{ 983 | Name: o.AgentPodName, 984 | Namespace: o.AgentPodNamespace, 985 | }, 986 | Spec: corev1.PodSpec{ 987 | HostPID: true, 988 | NodeName: o.AgentPodNode, 989 | ImagePullSecrets: []corev1.LocalObjectReference{ 990 | { 991 | Name: o.AgentImagePullSecretName, 992 | }, 993 | }, 994 | Containers: []corev1.Container{ 995 | { 996 | Name: "debug-agent", 997 | Image: o.AgentImage, 998 | ImagePullPolicy: corev1.PullPolicy(o.AgentImagePullPolicy), 999 | LivenessProbe: &corev1.Probe{ 1000 | Handler: corev1.Handler{ 1001 | HTTPGet: &corev1.HTTPGetAction{ 1002 | Path: "/healthz", 1003 | Port: intstr.FromInt(10027), 1004 | }, 1005 | }, 1006 | InitialDelaySeconds: 10, 1007 | PeriodSeconds: 10, 1008 | SuccessThreshold: 1, 1009 | TimeoutSeconds: 1, 1010 | FailureThreshold: 3, 1011 | }, 1012 | SecurityContext: &corev1.SecurityContext{ 1013 | Privileged: &priveleged, 1014 | }, 1015 | Resources: o.buildAgentResourceRequirements(), 1016 | VolumeMounts: []corev1.VolumeMount{ 1017 | { 1018 | Name: "docker", 1019 | MountPath: "/var/run/docker.sock", 1020 | }, 1021 | { 1022 | Name: "cgroup", 1023 | MountPath: "/sys/fs/cgroup", 1024 | }, 1025 | // containerd client needs to access /var/data, /run/containerd, /var/lib/containerd and /run/runc 1026 | { 1027 | Name: "vardata", 1028 | MountPath: "/var/data", 1029 | }, 1030 | { 1031 | Name: "varlibcontainerd", 1032 | MountPath: "/var/lib/containerd", 1033 | }, 1034 | { 1035 | Name: "runcontainerd", 1036 | MountPath: "/run/containerd", 1037 | }, 1038 | { 1039 | Name: "runrunc", 1040 | MountPath: "/run/runc", 1041 | }, 1042 | { 1043 | Name: "lxcfs", 1044 | MountPath: "/var/lib/lxc", 1045 | MountPropagation: &prop, 1046 | }, 1047 | }, 1048 | Ports: []corev1.ContainerPort{ 1049 | { 1050 | Name: "http", 1051 | HostPort: int32(o.AgentPort), 1052 | ContainerPort: 10027, 1053 | }, 1054 | }, 1055 | }, 1056 | }, 1057 | Volumes: []corev1.Volume{ 1058 | { 1059 | Name: "docker", 1060 | VolumeSource: corev1.VolumeSource{ 1061 | HostPath: &corev1.HostPathVolumeSource{ 1062 | Path: "/var/run/docker.sock", 1063 | }, 1064 | }, 1065 | }, 1066 | { 1067 | Name: "cgroup", 1068 | VolumeSource: corev1.VolumeSource{ 1069 | HostPath: &corev1.HostPathVolumeSource{ 1070 | Path: "/sys/fs/cgroup", 1071 | }, 1072 | }, 1073 | }, 1074 | { 1075 | Name: "lxcfs", 1076 | VolumeSource: corev1.VolumeSource{ 1077 | HostPath: &corev1.HostPathVolumeSource{ 1078 | Path: "/var/lib/lxc", 1079 | Type: &directoryCreate, 1080 | }, 1081 | }, 1082 | }, 1083 | { 1084 | Name: "vardata", 1085 | VolumeSource: corev1.VolumeSource{ 1086 | HostPath: &corev1.HostPathVolumeSource{ 1087 | Path: "/var/data", 1088 | }, 1089 | }, 1090 | }, 1091 | { 1092 | Name: "runcontainerd", 1093 | VolumeSource: corev1.VolumeSource{ 1094 | HostPath: &corev1.HostPathVolumeSource{ 1095 | Path: "/run/containerd", 1096 | }, 1097 | }, 1098 | }, 1099 | { 1100 | Name: "varlibcontainerd", 1101 | VolumeSource: corev1.VolumeSource{ 1102 | HostPath: &corev1.HostPathVolumeSource{ 1103 | Path: "/var/lib/containerd", 1104 | }, 1105 | }, 1106 | }, 1107 | { 1108 | Name: "runrunc", 1109 | VolumeSource: corev1.VolumeSource{ 1110 | HostPath: &corev1.HostPathVolumeSource{ 1111 | Path: "/run/runc", 1112 | }, 1113 | }, 1114 | }, 1115 | }, 1116 | RestartPolicy: corev1.RestartPolicyNever, 1117 | }, 1118 | } 1119 | fmt.Fprintf(o.Out, "Agent Pod info: [Name:%s, Namespace:%s, Image:%s, HostPort:%d, ContainerPort:%d]\n", agentPod.ObjectMeta.Name, agentPod.ObjectMeta.Namespace, agentPod.Spec.Containers[0].Image, agentPod.Spec.Containers[0].Ports[0].HostPort, agentPod.Spec.Containers[0].Ports[0].ContainerPort) 1120 | return agentPod 1121 | } 1122 | 1123 | func (o *DebugOptions) runPortForward(pod *corev1.Pod) error { 1124 | if pod.Status.Phase != corev1.PodRunning { 1125 | return fmt.Errorf("unable to forward port because pod is not running. Current status=%v\r\n", pod.Status.Phase) 1126 | } 1127 | o.wait.Add(1) 1128 | go func() { 1129 | defer o.wait.Done() 1130 | req := o.RESTClient.Post(). 1131 | Resource("pods"). 1132 | Namespace(pod.Namespace). 1133 | Name(pod.Name). 1134 | SubResource("portforward") 1135 | err := o.PortForwarder.ForwardPorts("POST", req.URL(), o) 1136 | if err != nil { 1137 | log.Printf("PortForwarded failed with %+v\r\n", err) 1138 | log.Printf("Sending ready signal just in case the failure reason is that the port is already forwarded.\r\n") 1139 | o.ReadyChannel <- struct{}{} 1140 | } 1141 | if o.Verbosity > 0 { 1142 | fmt.Fprintln(o.Out, "end port-forward...") 1143 | } 1144 | }() 1145 | return nil 1146 | } 1147 | 1148 | type portForwarder interface { 1149 | ForwardPorts(method string, url *url.URL, opts *DebugOptions) error 1150 | } 1151 | 1152 | type defaultPortForwarder struct { 1153 | genericclioptions.IOStreams 1154 | } 1155 | 1156 | // ForwardPorts forward ports 1157 | func (f *defaultPortForwarder) ForwardPorts(method string, url *url.URL, opts *DebugOptions) error { 1158 | transport, upgrader, err := spdy.RoundTripperFor(opts.Config) 1159 | if err != nil { 1160 | opts.Logger.Printf("error with setting up spdy forwarder") 1161 | return err 1162 | } 1163 | dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) 1164 | fw, err := portforward.New(dialer, opts.Ports, opts.StopChannel, opts.ReadyChannel, f.Out, f.ErrOut) 1165 | if err != nil { 1166 | opts.Logger.Printf("error with NewDialer") 1167 | return err 1168 | } 1169 | return fw.ForwardPorts() 1170 | } 1171 | 1172 | // auth checks if current user has permission to create pods/exec subresource. 1173 | func (o *DebugOptions) auth(pod *corev1.Pod) error { 1174 | sarClient := o.KubeCli.AuthorizationV1() 1175 | sar := &authorizationv1.SelfSubjectAccessReview{ 1176 | Spec: authorizationv1.SelfSubjectAccessReviewSpec{ 1177 | ResourceAttributes: &authorizationv1.ResourceAttributes{ 1178 | Namespace: pod.Namespace, 1179 | Verb: "create", 1180 | Group: "", 1181 | Resource: "pods", 1182 | Subresource: "exec", 1183 | Name: "", 1184 | }, 1185 | }, 1186 | } 1187 | response, err := sarClient.SelfSubjectAccessReviews().Create(sar) 1188 | if err != nil { 1189 | fmt.Fprintf(o.ErrOut, "Failed to create SelfSubjectAccessReview: %v \r\n", err) 1190 | return err 1191 | } 1192 | if !response.Status.Allowed { 1193 | denyReason := fmt.Sprintf("Current user has no permission to create pods/exec subresource in namespace:%s. Detail:", pod.Namespace) 1194 | if len(response.Status.Reason) > 0 { 1195 | denyReason = fmt.Sprintf("%s %v, ", denyReason, response.Status.Reason) 1196 | } 1197 | if len(response.Status.EvaluationError) > 0 { 1198 | denyReason = fmt.Sprintf("%s %v", denyReason, response.Status.EvaluationError) 1199 | } 1200 | return fmt.Errorf(denyReason) 1201 | } 1202 | return nil 1203 | } 1204 | 1205 | // delete the agent pod 1206 | func (o *DebugOptions) deleteAgent(agentPod *corev1.Pod) { 1207 | // only if createDebugAgentPod=true should we manage the debug-agent pod 1208 | if !o.CreateDebugAgentPod { 1209 | return 1210 | } 1211 | err := o.CoreClient.Pods(agentPod.Namespace).Delete(agentPod.Name, v1.NewDeleteOptions(0)) 1212 | if err != nil { 1213 | fmt.Fprintf(o.ErrOut, "failed to delete agent pod[Name:%s, Namespace: %s], consider manual deletion.\r\nerror msg: %v", agentPod.Name, agentPod.Namespace, err) 1214 | } 1215 | } 1216 | 1217 | // build the debug-agent pod Resource Requirements 1218 | func (o *DebugOptions) buildAgentResourceRequirements() corev1.ResourceRequirements { 1219 | return getResourceRequirements(getResourceList(o.AgentPodResource.CpuRequests, o.AgentPodResource.MemoryRequests), getResourceList(o.AgentPodResource.CpuLimits, o.AgentPodResource.MemoryLimits)) 1220 | } 1221 | 1222 | func getResourceList(cpu, memory string) corev1.ResourceList { 1223 | // catch error in resource.MustParse 1224 | defer func() { 1225 | if err := recover(); err != nil { 1226 | fmt.Printf("Parse Resource list error: %v\n", err) 1227 | } 1228 | }() 1229 | res := corev1.ResourceList{} 1230 | if cpu != "" { 1231 | res[corev1.ResourceCPU] = resource.MustParse(cpu) 1232 | } 1233 | if memory != "" { 1234 | res[corev1.ResourceMemory] = resource.MustParse(memory) 1235 | } 1236 | return res 1237 | } 1238 | 1239 | func getResourceRequirements(requests, limits corev1.ResourceList) corev1.ResourceRequirements { 1240 | res := corev1.ResourceRequirements{} 1241 | res.Requests = requests 1242 | res.Limits = limits 1243 | return res 1244 | } 1245 | -------------------------------------------------------------------------------- /pkg/kubectl-debug/config.go: -------------------------------------------------------------------------------- 1 | package kubectldebug 2 | 3 | import ( 4 | "gopkg.in/yaml.v2" 5 | "io/ioutil" 6 | ) 7 | 8 | type Config struct { 9 | AgentPort int `yaml:"agentPort,omitempty"` 10 | Image string `yaml:"image,omitempty"` 11 | RegistrySecretName string `yaml:"registrySecretName,omitempty"` 12 | RegistrySecretNamespace string `yaml:"registrySecretNamespace,omitempty"` 13 | RegistrySkipTLSVerify bool `yaml:"registrySkipTLSVerify,omitempty"` 14 | ForkPodRetainLabels []string `yaml:"forkPodRetainLabels,omitempty"` 15 | DebugAgentDaemonSet string `yaml:"debugAgentDaemonset,omitempty"` 16 | DebugAgentNamespace string `yaml:"debugAgentNamespace,omitempty"` 17 | Command []string `yaml:"command,omitempty"` 18 | PortForward bool `yaml:"portForward,omitempty"` 19 | CreateDebugAgentPod bool `yaml:"createDebugAgentPod,omitempty"` 20 | AgentPodNamePrefix string `yaml:"agentPodNamePrefix,omitempty"` 21 | AgentPodNamespace string `yaml:"agentPodNamespace,omitempty"` 22 | AgentImage string `yaml:"agentImage,omitempty"` 23 | AgentImagePullPolicy string `yaml:"agentImagePullPolicy,omitempty"` 24 | AgentImagePullSecretName string `yaml:"agentImagePullSecretName,omitempty"` 25 | AgentPodCpuRequests string `yaml:"agentCpuRequests,omitempty"` 26 | AgentPodMemoryRequests string `yaml:"agentMemoryRequests,omitempty"` 27 | AgentPodCpuLimits string `yaml:"agentCpuLimits,omitempty"` 28 | AgentPodMemoryLimits string `yaml:"agentMemoryLimits,omitempty"` 29 | IsLxcfsEnabled bool `yaml:"isLxcfsEnabled,omitempty"` 30 | Verbosity int `yaml:"verbosity,omitempty"` 31 | } 32 | 33 | func Load(s string) (*Config, error) { 34 | cfg := &Config{} 35 | err := yaml.Unmarshal([]byte(s), cfg) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return cfg, nil 40 | } 41 | 42 | func LoadFile(filename string) (*Config, error) { 43 | c, err := ioutil.ReadFile(filename) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return Load(string(c)) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/nsenter/nsenter.go: -------------------------------------------------------------------------------- 1 | package nsenter 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os/exec" 8 | "strconv" 9 | ) 10 | 11 | // MountNSEnter is the client used to enter the mount namespace 12 | type MountNSEnter struct { 13 | Target int64 // target PID (required) 14 | MountLxcfs bool // enter mount namespace or not 15 | MountFile string // Mount namespace location, default to /proc/PID/ns/mnt 16 | } 17 | 18 | // Execute runs the given command with a default background context 19 | func (cli *MountNSEnter) Execute(command string, args ...string) (stdout, stderr string, err error) { 20 | return cli.ExecuteContext(context.Background(), command, args...) 21 | } 22 | 23 | // ExecuteContext the given command using the specific nsenter config 24 | func (cli *MountNSEnter) ExecuteContext(ctx context.Context, command string, args ...string) (string, string, error) { 25 | cmd, err := cli.setCommand(ctx) 26 | if err != nil { 27 | return "", "", fmt.Errorf("Error when set command: %v", err) 28 | } 29 | 30 | var stdout, stderr bytes.Buffer 31 | cmd.Stdout = &stdout 32 | cmd.Stderr = &stderr 33 | cmd.Args = append(cmd.Args, command) 34 | cmd.Args = append(cmd.Args, args...) 35 | 36 | err = cmd.Run() 37 | if err != nil { 38 | return stdout.String(), stderr.String(), fmt.Errorf("Error while executing command: %v", err) 39 | } 40 | 41 | return stdout.String(), stderr.String(), nil 42 | } 43 | 44 | func (cli *MountNSEnter) setCommand(ctx context.Context) (*exec.Cmd, error) { 45 | if cli.Target == 0 { 46 | return nil, fmt.Errorf("Target must be specified") 47 | } 48 | var args []string 49 | args = append(args, "--target", strconv.FormatInt(cli.Target, 10)) 50 | 51 | if cli.MountLxcfs { 52 | if cli.MountFile != "" { 53 | args = append(args, fmt.Sprintf("--mount=%s", cli.MountFile)) 54 | } else { 55 | args = append(args, "--mount") 56 | } 57 | } 58 | 59 | cmd := exec.CommandContext(ctx, "/usr/bin/nsenter", args...) 60 | return cmd, nil 61 | } 62 | -------------------------------------------------------------------------------- /pkg/util/jsonstream.go: -------------------------------------------------------------------------------- 1 | // copied from docker/docker/pkg/jsonmessage/jsonmesage.go, fix the row align 2 | package term 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | gotty "github.com/Nvveen/Gotty" 13 | "github.com/docker/docker/pkg/term" 14 | units "github.com/docker/go-units" 15 | ) 16 | 17 | // RFC3339NanoFixed is time.RFC3339Nano with nanoseconds padded using zeros to 18 | // ensure the formatted time isalways the same number of characters. 19 | const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" 20 | 21 | // JSONError wraps a concrete Code and Message, `Code` is 22 | // is an integer error code, `Message` is the error message. 23 | type JSONError struct { 24 | Code int `json:"code,omitempty"` 25 | Message string `json:"message,omitempty"` 26 | } 27 | 28 | func (e *JSONError) Error() string { 29 | return e.Message 30 | } 31 | 32 | // JSONProgress describes a Progress. terminalFd is the fd of the current terminal, 33 | // Start is the initial value for the operation. Current is the current status and 34 | // value of the progress made towards Total. Total is the end value describing when 35 | // we made 100% progress for an operation. 36 | type JSONProgress struct { 37 | terminalFd uintptr 38 | Current int64 `json:"current,omitempty"` 39 | Total int64 `json:"total,omitempty"` 40 | Start int64 `json:"start,omitempty"` 41 | // If true, don't show xB/yB 42 | HideCounts bool `json:"hidecounts,omitempty"` 43 | Units string `json:"units,omitempty"` 44 | } 45 | 46 | func (p *JSONProgress) String() string { 47 | var ( 48 | width = 200 49 | pbBox string 50 | numbersBox string 51 | timeLeftBox string 52 | ) 53 | 54 | ws, err := term.GetWinsize(p.terminalFd) 55 | if err == nil { 56 | width = int(ws.Width) 57 | } 58 | 59 | if p.Current <= 0 && p.Total <= 0 { 60 | return "" 61 | } 62 | if p.Total <= 0 { 63 | switch p.Units { 64 | case "": 65 | current := units.HumanSize(float64(p.Current)) 66 | return fmt.Sprintf("%8v", current) 67 | default: 68 | return fmt.Sprintf("%d %s", p.Current, p.Units) 69 | } 70 | } 71 | 72 | percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 73 | if percentage > 50 { 74 | percentage = 50 75 | } 76 | if width > 110 { 77 | // this number can't be negative gh#7136 78 | numSpaces := 0 79 | if 50-percentage > 0 { 80 | numSpaces = 50 - percentage 81 | } 82 | pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)) 83 | } 84 | 85 | switch { 86 | case p.HideCounts: 87 | case p.Units == "": // no units, use bytes 88 | current := units.HumanSize(float64(p.Current)) 89 | total := units.HumanSize(float64(p.Total)) 90 | 91 | numbersBox = fmt.Sprintf("%8v/%v", current, total) 92 | 93 | if p.Current > p.Total { 94 | // remove total display if the reported current is wonky. 95 | numbersBox = fmt.Sprintf("%8v", current) 96 | } 97 | default: 98 | numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units) 99 | 100 | if p.Current > p.Total { 101 | // remove total display if the reported current is wonky. 102 | numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units) 103 | } 104 | } 105 | 106 | if p.Current > 0 && p.Start > 0 && percentage < 50 { 107 | fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0)) 108 | perEntry := fromStart / time.Duration(p.Current) 109 | left := time.Duration(p.Total-p.Current) * perEntry 110 | left = (left / time.Second) * time.Second 111 | 112 | if width > 50 { 113 | timeLeftBox = " " + left.String() 114 | } 115 | } 116 | return pbBox + numbersBox + timeLeftBox 117 | } 118 | 119 | // JSONMessage defines a message struct. It describes 120 | // the created time, where it from, status, ID of the 121 | // message. It's used for docker events. 122 | type JSONMessage struct { 123 | Stream string `json:"stream,omitempty"` 124 | Status string `json:"status,omitempty"` 125 | Progress *JSONProgress `json:"progressDetail,omitempty"` 126 | ProgressMessage string `json:"progress,omitempty"` //deprecated 127 | ID string `json:"id,omitempty"` 128 | From string `json:"from,omitempty"` 129 | Time int64 `json:"time,omitempty"` 130 | TimeNano int64 `json:"timeNano,omitempty"` 131 | Error *JSONError `json:"errorDetail,omitempty"` 132 | ErrorMessage string `json:"error,omitempty"` //deprecated 133 | // Aux contains out-of-band data, such as digests for push signing and image id after building. 134 | Aux *json.RawMessage `json:"aux,omitempty"` 135 | } 136 | 137 | /* Satisfied by gotty.TermInfo as well as noTermInfo from below */ 138 | type termInfo interface { 139 | Parse(attr string, params ...interface{}) (string, error) 140 | } 141 | 142 | type noTermInfo struct{} // canary used when no terminfo. 143 | 144 | func (ti *noTermInfo) Parse(attr string, params ...interface{}) (string, error) { 145 | return "", fmt.Errorf("noTermInfo") 146 | } 147 | 148 | func clearLine(out io.Writer, ti termInfo) { 149 | // el2 (clear whole line) is not exposed by terminfo. 150 | 151 | // First clear line from beginning to cursor 152 | if attr, err := ti.Parse("el1"); err == nil { 153 | fmt.Fprintf(out, "%s", attr) 154 | } else { 155 | fmt.Fprintf(out, "\x1b[1K") 156 | } 157 | // Then clear line from cursor to end 158 | if attr, err := ti.Parse("el"); err == nil { 159 | fmt.Fprintf(out, "%s", attr) 160 | } else { 161 | fmt.Fprintf(out, "\x1b[K") 162 | } 163 | } 164 | 165 | func cursorUp(out io.Writer, ti termInfo, l int) { 166 | if l == 0 { // Should never be the case, but be tolerant 167 | return 168 | } 169 | if attr, err := ti.Parse("cuu", l); err == nil { 170 | fmt.Fprintf(out, "%s", attr) 171 | } else { 172 | fmt.Fprintf(out, "\x1b[%dA", l) 173 | } 174 | } 175 | 176 | func cursorDown(out io.Writer, ti termInfo, l int) { 177 | if l == 0 { // Should never be the case, but be tolerant 178 | return 179 | } 180 | if attr, err := ti.Parse("cud", l); err == nil { 181 | fmt.Fprintf(out, "%s", attr) 182 | } else { 183 | fmt.Fprintf(out, "\x1b[%dB", l) 184 | } 185 | } 186 | 187 | // Display displays the JSONMessage to `out`. `termInfo` is non-nil if `out` 188 | // is a terminal. If this is the case, it will erase the entire current line 189 | // when displaying the progressbar. 190 | func (jm *JSONMessage) Display(out io.Writer, termInfo termInfo) error { 191 | if jm.Error != nil { 192 | if jm.Error.Code == 401 { 193 | return fmt.Errorf("authentication is required") 194 | } 195 | return jm.Error 196 | } 197 | endl := "\r" 198 | if termInfo != nil && jm.Stream == "" && jm.Progress != nil { 199 | clearLine(out, termInfo) 200 | fmt.Fprint(out, endl) 201 | } else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal 202 | return nil 203 | } 204 | if jm.TimeNano != 0 { 205 | fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(RFC3339NanoFixed)) 206 | } else if jm.Time != 0 { 207 | fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(RFC3339NanoFixed)) 208 | } 209 | if jm.ID != "" { 210 | fmt.Fprintf(out, "%s: ", jm.ID) 211 | } 212 | if jm.From != "" { 213 | fmt.Fprintf(out, "(from %s) ", jm.From) 214 | } 215 | if jm.Progress != nil && termInfo != nil { 216 | fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl) 217 | } else if jm.ProgressMessage != "" { //deprecated 218 | fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl) 219 | } else if jm.Stream != "" { 220 | fmt.Fprintf(out, "%s%s", jm.Stream, endl) 221 | } else { 222 | fmt.Fprintf(out, "%s\n%s", jm.Status, endl) 223 | } 224 | return nil 225 | } 226 | 227 | // DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal` 228 | // describes if `out` is a terminal. If this is the case, it will print `\n` at the end of 229 | // each line and move the cursor while displaying. 230 | func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error { 231 | var ( 232 | dec = json.NewDecoder(in) 233 | ids = make(map[string]int) 234 | ) 235 | 236 | var termInfo termInfo 237 | 238 | if isTerminal { 239 | term := os.Getenv("TERM") 240 | if term == "" { 241 | term = "vt102" 242 | } 243 | 244 | var err error 245 | if termInfo, err = gotty.OpenTermInfo(term); err != nil { 246 | termInfo = &noTermInfo{} 247 | } 248 | } 249 | 250 | for { 251 | diff := 0 252 | var jm JSONMessage 253 | if err := dec.Decode(&jm); err != nil { 254 | if err == io.EOF { 255 | break 256 | } 257 | return err 258 | } 259 | 260 | if jm.Aux != nil { 261 | if auxCallback != nil { 262 | auxCallback(jm.Aux) 263 | } 264 | continue 265 | } 266 | 267 | if jm.Progress != nil { 268 | jm.Progress.terminalFd = terminalFd 269 | } 270 | if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") { 271 | line, ok := ids[jm.ID] 272 | if !ok { 273 | // NOTE: This approach of using len(id) to 274 | // figure out the number of lines of history 275 | // only works as long as we clear the history 276 | // when we output something that's not 277 | // accounted for in the map, such as a line 278 | // with no ID. 279 | line = len(ids) 280 | ids[jm.ID] = line 281 | if termInfo != nil { 282 | fmt.Fprintf(out, "\n") 283 | } 284 | } 285 | diff = len(ids) - line 286 | if termInfo != nil { 287 | cursorUp(out, termInfo, diff) 288 | } 289 | } else { 290 | // When outputting something that isn't progress 291 | // output, clear the history of previous lines. We 292 | // don't want progress entries from some previous 293 | // operation to be updated (for example, pull -a 294 | // with multiple tags). 295 | ids = make(map[string]int) 296 | } 297 | err := jm.Display(out, termInfo) 298 | if jm.ID != "" && termInfo != nil { 299 | cursorDown(out, termInfo, diff) 300 | } 301 | if err != nil { 302 | return err 303 | } 304 | } 305 | return nil 306 | } 307 | 308 | type stream interface { 309 | io.Writer 310 | FD() uintptr 311 | IsTerminal() bool 312 | } 313 | 314 | // DisplayJSONMessagesToStream prints json messages to the output stream 315 | func DisplayJSONMessagesToStream(in io.Reader, stream stream, auxCallback func(*json.RawMessage)) error { 316 | return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback) 317 | } 318 | -------------------------------------------------------------------------------- /pkg/util/resize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // copied from github.com/kubernetes/kubernetes/pkg/kubectl/cmd/util/resize.go 18 | package term 19 | 20 | import ( 21 | "fmt" 22 | 23 | "github.com/docker/docker/pkg/term" 24 | "k8s.io/apimachinery/pkg/util/runtime" 25 | "k8s.io/client-go/tools/remotecommand" 26 | ) 27 | 28 | // GetSize returns the current size of the user's terminal. If it isn't a terminal, 29 | // nil is returned. 30 | func (t TTY) GetSize() *remotecommand.TerminalSize { 31 | outFd, isTerminal := term.GetFdInfo(t.Out) 32 | if !isTerminal { 33 | return nil 34 | } 35 | return GetSize(outFd) 36 | } 37 | 38 | // GetSize returns the current size of the terminal associated with fd. 39 | func GetSize(fd uintptr) *remotecommand.TerminalSize { 40 | winsize, err := term.GetWinsize(fd) 41 | if err != nil { 42 | runtime.HandleError(fmt.Errorf("unable to get terminal size: %v", err)) 43 | return nil 44 | } 45 | 46 | return &remotecommand.TerminalSize{Width: winsize.Width, Height: winsize.Height} 47 | } 48 | 49 | // MonitorSize monitors the terminal's size. It returns a TerminalSizeQueue primed with 50 | // initialSizes, or nil if there's no TTY present. 51 | func (t *TTY) MonitorSize(initialSizes ...*remotecommand.TerminalSize) remotecommand.TerminalSizeQueue { 52 | outFd, isTerminal := term.GetFdInfo(t.Out) 53 | if !isTerminal { 54 | return nil 55 | } 56 | 57 | t.sizeQueue = &sizeQueue{ 58 | t: *t, 59 | // make it buffered so we can send the initial terminal sizes without blocking, prior to starting 60 | // the streaming below 61 | resizeChan: make(chan remotecommand.TerminalSize, len(initialSizes)), 62 | stopResizing: make(chan struct{}), 63 | } 64 | 65 | t.sizeQueue.monitorSize(outFd, initialSizes...) 66 | 67 | return t.sizeQueue 68 | } 69 | 70 | // sizeQueue implements remotecommand.TerminalSizeQueue 71 | type sizeQueue struct { 72 | t TTY 73 | // resizeChan receives a Size each time the user's terminal is resized. 74 | resizeChan chan remotecommand.TerminalSize 75 | stopResizing chan struct{} 76 | } 77 | 78 | // make sure sizeQueue implements the resize.TerminalSizeQueue interface 79 | var _ remotecommand.TerminalSizeQueue = &sizeQueue{} 80 | 81 | // monitorSize primes resizeChan with initialSizes and then monitors for resize events. With each 82 | // new event, it sends the current terminal size to resizeChan. 83 | func (s *sizeQueue) monitorSize(outFd uintptr, initialSizes ...*remotecommand.TerminalSize) { 84 | // send the initial sizes 85 | for i := range initialSizes { 86 | if initialSizes[i] != nil { 87 | s.resizeChan <- *initialSizes[i] 88 | } 89 | } 90 | 91 | resizeEvents := make(chan remotecommand.TerminalSize, 1) 92 | 93 | monitorResizeEvents(outFd, resizeEvents, s.stopResizing) 94 | 95 | // listen for resize events in the background 96 | go func() { 97 | defer runtime.HandleCrash() 98 | 99 | for { 100 | select { 101 | case size, ok := <-resizeEvents: 102 | if !ok { 103 | return 104 | } 105 | 106 | select { 107 | // try to send the size to resizeChan, but don't block 108 | case s.resizeChan <- size: 109 | // send successful 110 | default: 111 | // unable to send / no-op 112 | } 113 | case <-s.stopResizing: 114 | return 115 | } 116 | } 117 | }() 118 | } 119 | 120 | // Next returns the new terminal size after the terminal has been resized. It returns nil when 121 | // monitoring has been stopped. 122 | func (s *sizeQueue) Next() *remotecommand.TerminalSize { 123 | size, ok := <-s.resizeChan 124 | if !ok { 125 | return nil 126 | } 127 | return &size 128 | } 129 | 130 | // stop stops the background goroutine that is monitoring for terminal resizes. 131 | func (s *sizeQueue) stop() { 132 | close(s.stopResizing) 133 | } 134 | -------------------------------------------------------------------------------- /pkg/util/resizeevents.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | /* 5 | Copyright 2016 The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | package term 21 | 22 | import ( 23 | "os" 24 | "os/signal" 25 | 26 | "golang.org/x/sys/unix" 27 | "k8s.io/apimachinery/pkg/util/runtime" 28 | "k8s.io/client-go/tools/remotecommand" 29 | ) 30 | 31 | // monitorResizeEvents spawns a goroutine that waits for SIGWINCH signals (these indicate the 32 | // terminal has resized). After receiving a SIGWINCH, this gets the terminal size and tries to send 33 | // it to the resizeEvents channel. The goroutine stops when the stop channel is closed. 34 | func monitorResizeEvents(fd uintptr, resizeEvents chan<- remotecommand.TerminalSize, stop chan struct{}) { 35 | go func() { 36 | defer runtime.HandleCrash() 37 | 38 | winch := make(chan os.Signal, 1) 39 | signal.Notify(winch, unix.SIGWINCH) 40 | defer signal.Stop(winch) 41 | 42 | for { 43 | select { 44 | case <-winch: 45 | size := GetSize(fd) 46 | if size == nil { 47 | return 48 | } 49 | 50 | // try to send size 51 | select { 52 | case resizeEvents <- *size: 53 | // success 54 | default: 55 | // not sent 56 | } 57 | case <-stop: 58 | return 59 | } 60 | } 61 | }() 62 | } 63 | -------------------------------------------------------------------------------- /pkg/util/resizeevents_windows.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package term 18 | 19 | import ( 20 | "time" 21 | 22 | "k8s.io/apimachinery/pkg/util/runtime" 23 | "k8s.io/client-go/tools/remotecommand" 24 | ) 25 | 26 | // monitorResizeEvents spawns a goroutine that periodically gets the terminal size and tries to send 27 | // it to the resizeEvents channel if the size has changed. The goroutine stops when the stop channel 28 | // is closed. 29 | func monitorResizeEvents(fd uintptr, resizeEvents chan<- remotecommand.TerminalSize, stop chan struct{}) { 30 | go func() { 31 | defer runtime.HandleCrash() 32 | 33 | size := GetSize(fd) 34 | if size == nil { 35 | return 36 | } 37 | lastSize := *size 38 | 39 | for { 40 | // see if we need to stop running 41 | select { 42 | case <-stop: 43 | return 44 | default: 45 | } 46 | 47 | size := GetSize(fd) 48 | if size == nil { 49 | return 50 | } 51 | 52 | if size.Height != lastSize.Height || size.Width != lastSize.Width { 53 | lastSize.Height = size.Height 54 | lastSize.Width = size.Width 55 | resizeEvents <- *size 56 | } 57 | 58 | // sleep to avoid hot looping 59 | time.Sleep(250 * time.Millisecond) 60 | } 61 | }() 62 | } 63 | -------------------------------------------------------------------------------- /pkg/util/term.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // copied from github.com/kubernetes/kubernetes/pkg/kubectl/cmd/util/term.go 18 | package term 19 | 20 | import ( 21 | "io" 22 | "os" 23 | 24 | "github.com/docker/docker/pkg/term" 25 | 26 | "k8s.io/kubernetes/pkg/util/interrupt" 27 | ) 28 | 29 | // SafeFunc is a function to be invoked by TTY. 30 | type SafeFunc func() error 31 | 32 | // TTY helps invoke a function and preserve the state of the terminal, even if the process is 33 | // terminated during execution. It also provides support for terminal resizing for remote command 34 | // execution/attachment. 35 | type TTY struct { 36 | // In is a reader representing stdin. It is a required field. 37 | In io.Reader 38 | // Out is a writer representing stdout. It must be set to support terminal resizing. It is an 39 | // optional field. 40 | Out io.Writer 41 | // Raw is true if the terminal should be set raw. 42 | Raw bool 43 | // TryDev indicates the TTY should try to open /dev/tty if the provided input 44 | // is not a file descriptor. 45 | TryDev bool 46 | // Parent is an optional interrupt handler provided to this function - if provided 47 | // it will be invoked after the terminal state is restored. If it is not provided, 48 | // a signal received during the TTY will result in os.Exit(0) being invoked. 49 | Parent *interrupt.Handler 50 | 51 | // sizeQueue is set after a call to MonitorSize() and is used to monitor SIGWINCH signals when the 52 | // user's terminal resizes. 53 | sizeQueue *sizeQueue 54 | } 55 | 56 | // IsTerminalIn returns true if t.In is a terminal. Does not check /dev/tty 57 | // even if TryDev is set. 58 | func (t TTY) IsTerminalIn() bool { 59 | return IsTerminal(t.In) 60 | } 61 | 62 | // IsTerminalOut returns true if t.Out is a terminal. Does not check /dev/tty 63 | // even if TryDev is set. 64 | func (t TTY) IsTerminalOut() bool { 65 | return IsTerminal(t.Out) 66 | } 67 | 68 | // IsTerminal returns whether the passed object is a terminal or not 69 | func IsTerminal(i interface{}) bool { 70 | _, terminal := term.GetFdInfo(i) 71 | return terminal 72 | } 73 | 74 | // Safe invokes the provided function and will attempt to ensure that when the 75 | // function returns (or a termination signal is sent) that the terminal state 76 | // is reset to the condition it was in prior to the function being invoked. If 77 | // t.Raw is true the terminal will be put into raw mode prior to calling the function. 78 | // If the input file descriptor is not a TTY and TryDev is true, the /dev/tty file 79 | // will be opened (if available). 80 | func (t TTY) Safe(fn SafeFunc) error { 81 | inFd, isTerminal := term.GetFdInfo(t.In) 82 | 83 | if !isTerminal && t.TryDev { 84 | if f, err := os.Open("/dev/tty"); err == nil { 85 | defer f.Close() 86 | inFd = f.Fd() 87 | isTerminal = term.IsTerminal(inFd) 88 | } 89 | } 90 | if !isTerminal { 91 | return fn() 92 | } 93 | 94 | var state *term.State 95 | var err error 96 | if t.Raw { 97 | state, err = term.MakeRaw(inFd) 98 | } else { 99 | state, err = term.SaveState(inFd) 100 | } 101 | if err != nil { 102 | return err 103 | } 104 | return interrupt.Chain(t.Parent, func() { 105 | if t.sizeQueue != nil { 106 | t.sizeQueue.stop() 107 | } 108 | 109 | term.RestoreTerminal(inFd, state) 110 | }).Run(fn) 111 | } 112 | -------------------------------------------------------------------------------- /scripts/docker_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 3 | docker push jamesgrantmediakind/debug-agent:latest -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | 4 | # Cleanup 5 | /usr/bin/nsenter -m/proc/1/ns/mnt -- fusermount -u /var/lib/lxc/lxcfs 2> /dev/null || true 6 | /usr/bin/nsenter -m/proc/1/ns/mnt -- [ -L /etc/mtab ] || \ 7 | sed -i "/^lxcfs \/var\/lib\/lxc\/lxcfs fuse.lxcfs/d" /etc/mtab 8 | 9 | # Prepare 10 | /usr/bin/nsenter -m/proc/1/ns/mnt -- mkdir -p /var/lib/lxc/lxcfs 11 | 12 | # Mount 13 | LXCFS_USR=/usr/bin/lxcfs 14 | LXCFS=/usr/local/bin/lxcfs 15 | /usr/bin/nsenter -m/proc/1/ns/mnt -- [ -f $LXCFS_USR ] && LXCFS=$LXCFS_USR 16 | exec /usr/bin/nsenter -m/proc/1/ns/mnt -- $LXCFS -p "/run/lxcfs-$$.pid" /var/lib/lxc/lxcfs/ & 17 | 18 | if grep -q io.containerd.runtime.v1.linux /proc/$PPID/cmdline 19 | then 20 | export KCTLDBG_CONTAINERDV1_SHIM=io.containerd.runc.v1 21 | fi 22 | 23 | /bin/debug-agent "$@" 24 | -------------------------------------------------------------------------------- /version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | if [[ -n ${GIT_COMMIT-} ]] || GIT_COMMIT=$(git rev-parse "HEAD^{commit}" 2>/dev/null); then 5 | if [[ -z ${GIT_TREE_STATE-} ]]; then 6 | # Check if the tree is dirty. default to dirty 7 | if git_status=$(git status --porcelain 2>/dev/null) && [[ -z ${git_status} ]]; then 8 | GIT_TREE_STATE="clean" 9 | else 10 | GIT_TREE_STATE="dirty" 11 | fi 12 | fi 13 | 14 | # Use git describe to find the version based on tags. 15 | if [[ -n ${GIT_VERSION-} ]] || GIT_VERSION=$(git describe --tags --abbrev=14 "${GIT_COMMIT}^{commit}" 2>/dev/null); then 16 | # This translates the "git describe" to an actual semver.org 17 | # compatible semantic version that looks something like this: 18 | # v1.0.0-beta.0.10+4c183422345d8f 19 | # 20 | # downstream consumers are expecting it there. 21 | DASHES_IN_VERSION=$(echo "${GIT_VERSION}" | sed "s/[^-]//g") 22 | if [[ "${DASHES_IN_VERSION}" == "---" ]] ; then 23 | # We have distance to subversion (v1.1.0-subversion-1-gCommitHash) 24 | GIT_VERSION=$(echo "${GIT_VERSION}" | sed "s/-\([0-9]\{1,\}\)-g\([0-9a-f]\{14\}\)$/.\1\+\2/") 25 | elif [[ "${DASHES_IN_VERSION}" == "--" ]] ; then 26 | # We have distance to base tag (v1.1.0-1-gCommitHash) 27 | GIT_VERSION=$(echo "${GIT_VERSION}" | sed "s/-g\([0-9a-f]\{14\}\)$/+\1/") 28 | fi 29 | if [[ "${GIT_TREE_STATE}" == "dirty" ]]; then 30 | # git describe --dirty only considers changes to existing files, but 31 | # that is problematic since new untracked .go files affect the build, 32 | # so use our idea of "dirty" from git status instead. 33 | GIT_VERSION+="-dirty" 34 | fi 35 | 36 | 37 | # If GIT_VERSION is not a valid Semantic Version, then refuse to build. 38 | if ! [[ "${GIT_VERSION}" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then 39 | echo "GIT_VERSION should be a valid Semantic Version. Current value: ${GIT_VERSION}" 40 | echo "Please see more details here: https://semver.org" 41 | exit 1 42 | fi 43 | fi 44 | fi 45 | 46 | echo "-X 'github.com/jamestgrant/kubectl-debug/version.gitVersion=${GIT_VERSION}'" 47 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | gitVersion = "v1.0.0-master+$Format:%h$" 5 | ) 6 | 7 | func Version() string { 8 | return gitVersion 9 | } 10 | --------------------------------------------------------------------------------