diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f49a4e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 0fa60ef..a362f59 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,52 @@ # Kubectl-debug -> Pod debugging made easy +![license](https://img.shields.io/hexpm/l/plug.svg) +[![travis](https://travis-ci.org/aylei/kubectl-debug.svg?branch=master)](https://travis-ci.org/aylei/kubectl-debug) +[![Go Report Card](https://goreportcard.com/badge/github.com/aylei/kubectl-debug)](https://goreportcard.com/report/github.com/aylei/kubectl-debug) +[![docker](https://img.shields.io/docker/pulls/aylei/debug-agent.svg)](https://hub.docker.com/r/aylei/debug-agent) -`kubectl-debug` is an out-of-tree solution for [troubleshooting running pods](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/troubleshoot-running-pods.md), which allows you to run new containers in running pod for debugging purpose. `kubectl-debug` is pretty simple and capable for all versions* of k8s. +[中文文档](./docs/zh-cn.md) -`*`: I've tested `kubectl-debug` with kubectl version v1.13.1 and kubernetes version v1.9.1. I don't have an environment to test more versions but I suppose that `kubectl-debug` is compatible with all versions of kubernetes and kubectl 1.12.0 or higher. Please [file an issue] if you find `kubectl-debug` do not work. +`kubectl-debug` is an out-of-tree solution for [troubleshooting running pods](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/troubleshoot-running-pods.md), which allows you to run a new container in running pods for debugging purpose. The new container will join the `pid`, `network`, `user` and `ipc` namespaces of the target container, so you can use arbitrary trouble-shooting tools without pre-install them in your production container image. + +> Compatibility: I've tested `kubectl-debug` with kubectl v1.13.1 and kubernetes v1.9.1. I don't have an environment to test more versions but I suppose that `kubectl-debug` is compatible with all versions of kubernetes and kubectl 1.12.0+. Please [file an issue](https://github.com/aylei/kubectl-debug/issues/new) if you find `kubectl-debug` do not work. # Quick Start -WIP +Install the debug agent DaemonSet in your cluster, which is responsible to run the "new container": +```bash +kubectl apply -f https://raw.githubusercontent.com/aylei/kubectl-debug/master/scripts/agent_daemonset.yml +``` + +Install the kubectl debug plugin: +```bash +curl +``` + +Try it out! +```bash +kubectl debug POD_NAME +# learn more with +kubectl debug -h +``` -# TODO +# Default image and entrypoint -- [ ] DaemonSet YAML and helm chart for agent +`kubectl-debug` use [nicolaka/netshoot](https://github.com/nicolaka/netshoot) as the default image to run debug container, and use `bash` as default entrypoint. -nice to have: +You can override the default image and entrypoint with cli flag, or even better, with config file `~/.kube/debug-config`: -- [ ] bash completion -- [ ] `kubectl debug list`: list debug containers, we might need this because the debug container is not discovered by kubernetes. -- [ ] security: security is import, but not a consideration in current stage +```yaml +agent_port: 10027 +image: nicolaka/netshoot:latest +command: +- '/bin/bash' +- '-l' +``` -# Design +PS: `kubectl-debug` will always override the entrypoint of the container, which is by design to avoid users running an unwanted service by mistake(of course you can always do this explicitly). + +# Details `kubectl-debug` consists of 2 components: @@ -39,8 +65,6 @@ When user run `kubectl debug target-pod -c /bin/bash`: 8. Jobs done, user close the SPDY connection. 9. The node agent close the SPDY connection, then wait the `debug contaienr` exit and do the cleanup. -# Reference - -[sample-cli-plugin](https://github.com/kubernetes/sample-cli-plugin) +# Contribute -[docker api](https://godoc.org/github.com/docker/docker/client) +Feel free to open issues and pull requests. Any feedback will be highly appreciated! \ No newline at end of file diff --git a/docs/zh-cn.md b/docs/zh-cn.md new file mode 100644 index 0000000..d346036 --- /dev/null +++ b/docs/zh-cn.md @@ -0,0 +1,49 @@ +# Kubectl debug + +`kubectl-debug` 是一个简单的 kubectl 插件, 能够帮助你便捷地进行 Kubernetes 上的 Pod 排障诊断. 背后做的事情很简单: 在运行中的 Pod 上额外起一个新容器, 并将新容器加入到目标容器的 `pid`, `network`, `user` 以及 `ipc` namespace 中, 这时我们就可以在新容器中直接用 `netstat`, `tcpdump` 这些熟悉的工具来解决问题了, 而旧容器可以保持最小化, 不需要预装任何额外的排障工具. + +# Quick Start + +`kubectl-debug` 包含两部分, 一部分是用户侧的 kubectl 插件, 另一部分是部署在所有 k8s 节点上的 agent(用于启动"新容器", 同时也作为 SPDY 连接的中继). 因此首先要部署 agent. + +推荐以 DaemonSet 的形式部署: +```bash +kubectl apply -f https://raw.githubusercontent.com/aylei/kubectl-debug/master/scripts/agent_daemonset.yml +``` + +接下来, 安装 kubectl 插件: + + +装完之后就可以试试看了: +```bash +kubectl debug POD_NAME +# learn more with +kubectl debug -h +``` + +# 默认镜像和 Entrypoint + +`kubectl-debug` 使用 [nicolaka/netshoot](https://github.com/nicolaka/netshoot) 作为默认镜像. 默认镜像和指令都可以通过命令行参数进行覆盖. 考虑到每次都指定有点麻烦, 也可以通过文件配置的形式进行覆盖, 编辑 `~/.kube/debug-config` 文件: + +```bash +agent_port: 10027 +image: nicolaka/netshoot:latest +command: +- '/bin/bash' +- '-l' +``` + +> `kubectl-debug` 会将容器的 entrypoint 直接覆盖掉, 这是为了避免在 debug 时不小心启动非 shell 进程. + +# 实现细节 + +主要参照了 `kubectl exec` 的实现, 但 `exec` 要复杂很多, `debug` 的链路还是很简单的: + +1. 根据指令找到 Pod 的 HostIP, 以及目标容器的 ContainerID +2. 对 HostIP 上的 agent 发起 http 请求, 请求会携带上 image, command 这些参数 +3. agent 返回一个协议升级响应, 在 client 与 agent 之间建立 SPDY 连接 +4. agent 确认目标容器是否正常运行, 若否, 返回一个错误 +5. agent 启动一个新容器, 加入到目标容器的 `pid`, `network`, `ipc` 以及 `user` namespace 中 +6. 容器启动完成后, agent 将 server 侧 SPDY 的 stdin, stdout, tty 绑定到新容器的 stdin, stdout 和 tty 上 +7. 一切就绪, 可以开始 debug 了 +8. debug 完毕, 关闭连接, agent 做一些清理操作, 关闭并移除容器 \ No newline at end of file diff --git a/pkg/agent/runtime.go b/pkg/agent/runtime.go index 6a5d424..bf6a17d 100644 --- a/pkg/agent/runtime.go +++ b/pkg/agent/runtime.go @@ -181,7 +181,7 @@ func (m *DebugAttacher) PullImage(image string, stdout io.WriteCloser) error { } defer out.Close() // write pull progress to user - term.DisplayDockerJsonStream(out, stdout) + term.DisplayJSONMessagesStream(out, stdout, 1, true, nil) return nil } diff --git a/pkg/plugin/cmd.go b/pkg/plugin/cmd.go index 1edc4d6..542932b 100644 --- a/pkg/plugin/cmd.go +++ b/pkg/plugin/cmd.go @@ -14,7 +14,9 @@ import ( coreclient "k8s.io/client-go/kubernetes/typed/core/v1" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" + "log" "net/url" + "os/user" ) const ( @@ -30,9 +32,18 @@ const ( # override entrypoint of debug container kubectl debug POD_NAME --image aylei/debug-jvm /bin/bash + + # override the debug config file + kubectl debug POD_NAME --debug-config ./debug-config.yml +` + longDesc = ` +Run a container in a running pod, this container will join the namespaces of an existing container of the pod. + +You may set default configuration such as image and command in the config file, which locates in "~/.kube/debug-config" by default. ` - defaultImage = "aylei/troubleshoot:latest" - defaultAgentPort = 10027 + defaultImage = "nicolaka/netshoot:latest" + defaultAgentPort = 10027 + defaultConfigLocation = "/.kube/debug-config" ) // DebugOptions specify how to run debug container in a running pod @@ -48,6 +59,7 @@ type DebugOptions struct { ContainerName string Command []string AgentPort int + ConfigLocation string Flags *genericclioptions.ConfigFlags PodClient coreclient.PodsGetter @@ -68,7 +80,8 @@ func NewDebugCmd(streams genericclioptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "debug POD [-c CONTAINER] -- COMMAND [args...]", DisableFlagsInUseLine: true, - Short: "Run a container in running pod", + Short: "Run a container in a running pod", + Long: longDesc, Example: example, Run: func(c *cobra.Command, args []string) { argsLenAtDash := c.ArgsLenAtDash() @@ -85,12 +98,14 @@ func NewDebugCmd(streams genericclioptions.IOStreams) *cobra.Command { } //cmd.Flags().BoolVarP(&opts.RetainContainer, "retain", "r", defaultRetain, // fmt.Sprintf("Retain container after debug session closed, default to %s", defaultRetain)) - cmd.Flags().StringVar(&opts.Image, "image", defaultImage, + cmd.Flags().StringVar(&opts.Image, "image", "", fmt.Sprintf("Container Image to run the debug container, default to %s", defaultImage)) cmd.Flags().StringVarP(&opts.ContainerName, "container", "c", "", "Target container to debug, default to the first container in pod") - cmd.Flags().IntVarP(&opts.AgentPort, "port", "p", defaultAgentPort, + cmd.Flags().IntVarP(&opts.AgentPort, "port", "p", 0, fmt.Sprintf("Agent port for debug cli to connect, default to %d", defaultAgentPort)) + cmd.Flags().StringVar(&opts.ConfigLocation, "debug-config", "", + fmt.Sprintf("Debug config file, default to ~%s", defaultConfigLocation)) opts.Flags.AddFlags(cmd.Flags()) return cmd @@ -102,12 +117,6 @@ func (o *DebugOptions) Complete(cmd *cobra.Command, args []string, argsLenAtDash if len(args) == 0 { return fmt.Errorf("error pod not specified") } - o.PodName = args[0] - o.Command = args[1:] - if len(o.Command) < 1 { - // default command is guaranteed in default container - o.Command = []string{"bash"} - } var err error configLoader := o.Flags.ToRawKubeConfigLoader() @@ -116,6 +125,46 @@ func (o *DebugOptions) Complete(cmd *cobra.Command, args []string, argsLenAtDash return err } + o.PodName = args[0] + + // read defaults from config file + configFile := o.ConfigLocation + if len(o.ConfigLocation) < 1 { + usr, err := user.Current() + if err == nil { + configFile = usr.HomeDir + defaultConfigLocation + } + } + config, err := LoadFile(configFile) + if err != nil { + log.Println("error loading file ", err) + config = &Config{} + } + + // combine defaults, config file and user parameters + o.Command = args[1:] + if len(o.Command) < 1 { + if len(config.Command) > 0 { + o.Command = config.Command + } else { + o.Command = []string{"bash"} + } + } + if len(o.Image) < 1 { + if len(config.Image) > 0 { + o.Image = config.Image + } else { + o.Image = defaultImage + } + } + if o.AgentPort < 1 { + if config.AgentPort > 0 { + o.AgentPort = config.AgentPort + } else { + o.AgentPort = defaultAgentPort + } + } + o.Config, err = configLoader.ClientConfig() if err != nil { return err @@ -174,8 +223,6 @@ func (o *DebugOptions) Run() error { o.ErrOut = nil } - hostIP = "localhost" - containerId = "docker://2a69a97f73720793a14a5e4bef3480a43c7c656310a100ef6260820bd872bb08" fn := func() error { // TODO: refactor as kubernetes api style, reuse rbac mechanism of kubernetes diff --git a/pkg/plugin/config.go b/pkg/plugin/config.go new file mode 100644 index 0000000..756cd00 --- /dev/null +++ b/pkg/plugin/config.go @@ -0,0 +1,30 @@ +package plugin + +import ( + "gopkg.in/yaml.v2" + "io/ioutil" +) + +type Config struct { + AgentPort int `yaml:"agent_port,omitempty"` + Image string `yaml:"image,omitempty"` + Command []string `yaml:"command,omitempty"` +} + +func Load(s string) (*Config, error) { + cfg := &Config{} + + err := yaml.UnmarshalStrict([]byte(s), cfg) + if err != nil { + return nil, err + } + return cfg, nil +} + +func LoadFile(filename string) (*Config, error) { + c, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return Load(string(c)) +} diff --git a/pkg/util/jsonstream.go b/pkg/util/jsonstream.go index 364cac2..a9c53c3 100644 --- a/pkg/util/jsonstream.go +++ b/pkg/util/jsonstream.go @@ -1,49 +1,254 @@ +// copied from docker/docker/pkg/jsonmessage/jsonmesage.go, fix the row align package term import ( "encoding/json" "fmt" "io" + "os" + "strings" + "time" + + gotty "github.com/Nvveen/Gotty" + "github.com/docker/docker/pkg/term" + units "github.com/docker/go-units" ) +// RFC3339NanoFixed is time.RFC3339Nano with nanoseconds padded using zeros to +// ensure the formatted time isalways the same number of characters. +const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" + +// JSONError wraps a concrete Code and Message, `Code` is +// is an integer error code, `Message` is the error message. type JSONError struct { Code int `json:"code,omitempty"` Message string `json:"message,omitempty"` } +func (e *JSONError) Error() string { + return e.Message +} + +// JSONProgress describes a Progress. terminalFd is the fd of the current terminal, +// Start is the initial value for the operation. Current is the current status and +// value of the progress made towards Total. Total is the end value describing when +// we made 100% progress for an operation. +type JSONProgress struct { + terminalFd uintptr + Current int64 `json:"current,omitempty"` + Total int64 `json:"total,omitempty"` + Start int64 `json:"start,omitempty"` + // If true, don't show xB/yB + HideCounts bool `json:"hidecounts,omitempty"` + Units string `json:"units,omitempty"` +} + +func (p *JSONProgress) String() string { + var ( + width = 200 + pbBox string + numbersBox string + timeLeftBox string + ) + + ws, err := term.GetWinsize(p.terminalFd) + if err == nil { + width = int(ws.Width) + } + + if p.Current <= 0 && p.Total <= 0 { + return "" + } + if p.Total <= 0 { + switch p.Units { + case "": + current := units.HumanSize(float64(p.Current)) + return fmt.Sprintf("%8v", current) + default: + return fmt.Sprintf("%d %s", p.Current, p.Units) + } + } + + percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 + if percentage > 50 { + percentage = 50 + } + if width > 110 { + // this number can't be negative gh#7136 + numSpaces := 0 + if 50-percentage > 0 { + numSpaces = 50 - percentage + } + pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)) + } + + switch { + case p.HideCounts: + case p.Units == "": // no units, use bytes + current := units.HumanSize(float64(p.Current)) + total := units.HumanSize(float64(p.Total)) + + numbersBox = fmt.Sprintf("%8v/%v", current, total) + + if p.Current > p.Total { + // remove total display if the reported current is wonky. + numbersBox = fmt.Sprintf("%8v", current) + } + default: + numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units) + + if p.Current > p.Total { + // remove total display if the reported current is wonky. + numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units) + } + } + + if p.Current > 0 && p.Start > 0 && percentage < 50 { + fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0)) + perEntry := fromStart / time.Duration(p.Current) + left := time.Duration(p.Total-p.Current) * perEntry + left = (left / time.Second) * time.Second + + if width > 50 { + timeLeftBox = " " + left.String() + } + } + return pbBox + numbersBox + timeLeftBox +} + +// JSONMessage defines a message struct. It describes +// the created time, where it from, status, ID of the +// message. It's used for docker events. type JSONMessage struct { - Status string `json:"status,omitempty"` - ID string `json:"id,omitempty"` - From string `json:"from,omitempty"` - Time int64 `json:"time,omitempty"` - TimeNano int64 `json:"timeNano,omitempty"` - ProgressMessage string `json:"progress,omitempty"` - Error *JSONError `json:"errorDetail,omitempty"` + Stream string `json:"stream,omitempty"` + Status string `json:"status,omitempty"` + Progress *JSONProgress `json:"progressDetail,omitempty"` + ProgressMessage string `json:"progress,omitempty"` //deprecated + ID string `json:"id,omitempty"` + From string `json:"from,omitempty"` + Time int64 `json:"time,omitempty"` + TimeNano int64 `json:"timeNano,omitempty"` + Error *JSONError `json:"errorDetail,omitempty"` + ErrorMessage string `json:"error,omitempty"` //deprecated + // Aux contains out-of-band data, such as digests for push signing and image id after building. + Aux *json.RawMessage `json:"aux,omitempty"` +} + +/* Satisfied by gotty.TermInfo as well as noTermInfo from below */ +type termInfo interface { + Parse(attr string, params ...interface{}) (string, error) +} + +type noTermInfo struct{} // canary used when no terminfo. + +func (ti *noTermInfo) Parse(attr string, params ...interface{}) (string, error) { + return "", fmt.Errorf("noTermInfo") +} + +func clearLine(out io.Writer, ti termInfo) { + // el2 (clear whole line) is not exposed by terminfo. + + // First clear line from beginning to cursor + if attr, err := ti.Parse("el1"); err == nil { + fmt.Fprintf(out, "%s", attr) + } else { + fmt.Fprintf(out, "\x1b[1K") + } + // Then clear line from cursor to end + if attr, err := ti.Parse("el"); err == nil { + fmt.Fprintf(out, "%s", attr) + } else { + fmt.Fprintf(out, "\x1b[K") + } +} + +func cursorUp(out io.Writer, ti termInfo, l int) { + if l == 0 { // Should never be the case, but be tolerant + return + } + if attr, err := ti.Parse("cuu", l); err == nil { + fmt.Fprintf(out, "%s", attr) + } else { + fmt.Fprintf(out, "\x1b[%dA", l) + } } -func (jm *JSONMessage) Display(out io.Writer) error { +func cursorDown(out io.Writer, ti termInfo, l int) { + if l == 0 { // Should never be the case, but be tolerant + return + } + if attr, err := ti.Parse("cud", l); err == nil { + fmt.Fprintf(out, "%s", attr) + } else { + fmt.Fprintf(out, "\x1b[%dB", l) + } +} +// Display displays the JSONMessage to `out`. `termInfo` is non-nil if `out` +// is a terminal. If this is the case, it will erase the entire current line +// when displaying the progressbar. +func (jm *JSONMessage) Display(out io.Writer, termInfo termInfo) error { if jm.Error != nil { - fmt.Fprintf(out, "error pulling image, %s\n\r", jm.Error.Message) - return nil + if jm.Error.Code == 401 { + return fmt.Errorf("authentication is required") + } + return jm.Error } - // do not display progress bar - if len(jm.ProgressMessage) > 0 { + endl := "\r" + if termInfo != nil && jm.Stream == "" && jm.Progress != nil { + clearLine(out, termInfo) + fmt.Fprintf(out, endl) + } else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal return nil } - fmt.Fprintf(out, "\t%s %s \n\r", jm.Status, jm.ID) - + if jm.TimeNano != 0 { + fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(RFC3339NanoFixed)) + } else if jm.Time != 0 { + fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(RFC3339NanoFixed)) + } + if jm.ID != "" { + fmt.Fprintf(out, "%s: ", jm.ID) + } + if jm.From != "" { + fmt.Fprintf(out, "(from %s) ", jm.From) + } + if jm.Progress != nil && termInfo != nil { + fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl) + } else if jm.ProgressMessage != "" { //deprecated + fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl) + } else if jm.Stream != "" { + fmt.Fprintf(out, "%s%s", jm.Stream, endl) + } else { + fmt.Fprintf(out, "%s\n%s", jm.Status, endl) + } return nil } -// DisplayJsonStream parse the json input from `in` and pipe the necessary message to `out` -func DisplayDockerJsonStream(in io.Reader, out io.Writer) error { - +// DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal` +// describes if `out` is a terminal. If this is the case, it will print `\n` at the end of +// each line and move the cursor while displaying. +func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error { var ( dec = json.NewDecoder(in) + ids = make(map[string]int) ) + var termInfo termInfo + + if isTerminal { + term := os.Getenv("TERM") + if term == "" { + term = "vt102" + } + + var err error + if termInfo, err = gotty.OpenTermInfo(term); err != nil { + termInfo = &noTermInfo{} + } + } + for { + diff := 0 var jm JSONMessage if err := dec.Decode(&jm); err != nil { if err == io.EOF { @@ -51,11 +256,62 @@ func DisplayDockerJsonStream(in io.Reader, out io.Writer) error { } return err } - err := jm.Display(out) + + if jm.Aux != nil { + if auxCallback != nil { + auxCallback(jm.Aux) + } + continue + } + + if jm.Progress != nil { + jm.Progress.terminalFd = terminalFd + } + if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") { + line, ok := ids[jm.ID] + if !ok { + // NOTE: This approach of using len(id) to + // figure out the number of lines of history + // only works as long as we clear the history + // when we output something that's not + // accounted for in the map, such as a line + // with no ID. + line = len(ids) + ids[jm.ID] = line + if termInfo != nil { + fmt.Fprintf(out, "\n") + } + } + diff = len(ids) - line + if termInfo != nil { + cursorUp(out, termInfo, diff) + } + } else { + // When outputting something that isn't progress + // output, clear the history of previous lines. We + // don't want progress entries from some previous + // operation to be updated (for example, pull -a + // with multiple tags). + ids = make(map[string]int) + } + err := jm.Display(out, termInfo) + if jm.ID != "" && termInfo != nil { + cursorDown(out, termInfo, diff) + } if err != nil { return err } } - return nil } + +type stream interface { + io.Writer + FD() uintptr + IsTerminal() bool +} + +// DisplayJSONMessagesToStream prints json messages to the output stream +func DisplayJSONMessagesToStream(in io.Reader, stream stream, auxCallback func(*json.RawMessage)) error { + return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback) +} diff --git a/scripts/agent_daemonset.yml b/scripts/agent_daemonset.yml index 55a47aa..0281171 100644 --- a/scripts/agent_daemonset.yml +++ b/scripts/agent_daemonset.yml @@ -14,7 +14,7 @@ spec: app: debug-agent spec: containers: - - image: aylei/debug-agent:latest + - image: registry.qunhequnhe.com/monitor/debug-agent:0.0.5 imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 @@ -43,4 +43,4 @@ spec: updateStrategy: rollingUpdate: maxUnavailable: 5 - type: RollingUpdate \ No newline at end of file + type: RollingUpdate