├── .gitignore ├── .github ├── pull_request_template.md └── workflows │ └── release.yml ├── ktop.go ├── .goreleaser.yml ├── pkg ├── ui │ ├── text_field.go │ ├── graph.go │ └── table.go ├── resource │ ├── summarized.go │ ├── summarized_viewer.go │ ├── node_viewer.go │ ├── node.go │ ├── resource.go │ └── resource_viewer.go ├── util │ └── util.go ├── kube │ └── clients.go └── ktop │ └── ktop.go ├── LICENCE ├── README.md ├── go.mod ├── cmd └── ktop.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Overview 2 | -------------------------------------------------------------------------------- /ktop.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ynqa/ktop/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - binary: ktop 3 | goos: 4 | - windows 5 | - darwin 6 | - linux 7 | goarch: 8 | - amd64 9 | 10 | archives: 11 | - format: tar.gz 12 | format_overrides: 13 | - goos: windows 14 | format: zip 15 | 16 | brews: 17 | - tap: 18 | owner: ynqa 19 | name: homebrew-tap-archived 20 | homepage: https://github.com/ynqa/ktop/ 21 | description: "A visualized monitoring dashboard for Kubernetes" 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | - name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@v4 23 | with: 24 | distribution: goreleaser 25 | version: latest 26 | args: release --clean 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 29 | -------------------------------------------------------------------------------- /pkg/ui/text_field.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "image" 5 | 6 | . "github.com/gizak/termui/v3" 7 | ) 8 | 9 | type TextField struct { 10 | *Block 11 | 12 | Text string 13 | TextStyle Style 14 | } 15 | 16 | func NewTextField() *TextField { 17 | return &TextField{ 18 | Block: NewBlock(), 19 | } 20 | } 21 | 22 | func (self *TextField) Draw(buf *Buffer) { 23 | cells := ParseStyles(self.Text, self.TextStyle) 24 | rows := SplitCells(cells, '\n') 25 | 26 | for y, row := range rows { 27 | if y+self.Inner.Min.Y >= self.Inner.Max.Y { 28 | break 29 | } 30 | row = TrimCells(row, self.Inner.Dx()) 31 | for _, cx := range BuildCellWithXArray(row) { 32 | x, cell := cx.X, cx.Cell 33 | buf.SetCell(cell, image.Pt(x, y).Add(self.Inner.Min)) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019, Makoto Ito. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /pkg/resource/summarized.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | 6 | . "github.com/ynqa/ktop/pkg/util" 7 | ) 8 | 9 | type SummarizedResource struct { 10 | podName string 11 | nodeName string 12 | usage corev1.ResourceList 13 | } 14 | 15 | func NewSummarizedResource(p corev1.Pod, sumUsage corev1.ResourceList) *SummarizedResource { 16 | return &SummarizedResource{ 17 | podName: p.Name, 18 | nodeName: p.Spec.NodeName, 19 | usage: sumUsage, 20 | } 21 | } 22 | 23 | func (s *SummarizedResource) GetNodeName() string { 24 | return s.nodeName 25 | } 26 | 27 | func (s *SummarizedResource) GetPodName() string { 28 | return s.podName 29 | } 30 | 31 | func (s *SummarizedResource) GetCpuUsage() (float64, string) { 32 | return GetResourceValue(s.usage, corev1.ResourceCPU), 33 | GetResourceValueString(s.usage, corev1.ResourceCPU) 34 | } 35 | 36 | func (s *SummarizedResource) GetMemoryUsage() (float64, string) { 37 | return GetResourceValue(s.usage, corev1.ResourceMemory), 38 | GetResourceValueString(s.usage, corev1.ResourceMemory) 39 | } 40 | 41 | // header: "POD", "%CPU", "%MEM" 42 | func (s *SummarizedResource) toRow() []string { 43 | return []string{ 44 | s.podName, 45 | GetResourceValueString(s.usage, corev1.ResourceCPU), 46 | GetResourceValueString(s.usage, corev1.ResourceMemory), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/resource/summarized_viewer.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "image" 5 | "sort" 6 | 7 | . "github.com/ynqa/ktop/pkg/util" 8 | ) 9 | 10 | var ( 11 | summarizedTitle = "⎈ Pod ⎈" 12 | summarizedHeader = []string{ 13 | "POD", "CPU(U)", "Memory(U)", 14 | } 15 | summarizedWidthFn = func(rect image.Rectangle, maxLen int) []int { 16 | nameWidth := IntMax(50, IntMin(rect.Dx()-20, maxLen+indentSize)) 17 | return []int{nameWidth, 10, 10} 18 | } 19 | ) 20 | 21 | func AsSummarizedTableViewer(resources []*SummarizedResource, sortType SortType) ResourceTableViewer { 22 | switch sortType { 23 | case ByName: 24 | return sortByNameForSummarized(resources) 25 | default: 26 | return sortByNameForSummarized(resources) 27 | } 28 | } 29 | 30 | type sortByNameForSummarized []*SummarizedResource 31 | 32 | func (s sortByNameForSummarized) GetTableShape(rect image.Rectangle) (string, []string, []int, [][]string) { 33 | rows := make([][]string, len(s)) 34 | var maxLen int 35 | for i, v := range s { 36 | rows[i] = v.toRow() 37 | maxLen = IntMax(maxLen, len(rows[i][0])) 38 | } 39 | title, header, widths := 40 | summarizedTitle, summarizedHeader, summarizedWidthFn(rect, maxLen) 41 | 42 | if len(s) == 0 { 43 | header = emptyHeader 44 | widths = emptyWidthFn(rect) 45 | rows = emptyRows 46 | } 47 | return title, header, widths, rows 48 | } 49 | 50 | func (s sortByNameForSummarized) SortRows() { 51 | sort.Slice(s, func(i, j int) bool { 52 | return s[i].podName < s[j].podName 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /pkg/resource/node_viewer.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "image" 5 | "sort" 6 | 7 | . "github.com/ynqa/ktop/pkg/util" 8 | ) 9 | 10 | var ( 11 | nodeTitle = "⎈ Node ⎈" 12 | nodeHeader = []string{ 13 | "NODE", 14 | "CPU(A)", "CPU(U)", "%CPU", 15 | "Memory(A)", "Memory(U)", "%Memory", 16 | } 17 | nodeWidthFn = func(rect image.Rectangle, maxLen int) []int { 18 | nameWidth := IntMax(50, IntMin(rect.Dx()-20, maxLen+indentSize)) 19 | return []int{nameWidth, 10, 10, 10, 10, 10, 10} 20 | } 21 | ) 22 | 23 | func AsNodeTableViewer(resources []*NodeResource, sortType SortType) ResourceTableViewer { 24 | switch sortType { 25 | case ByName: 26 | return sortByNameForNode(resources) 27 | default: 28 | return sortByNameForNode(resources) 29 | } 30 | } 31 | 32 | type sortByNameForNode []*NodeResource 33 | 34 | func (s sortByNameForNode) GetTableShape(rect image.Rectangle) (string, []string, []int, [][]string) { 35 | rows := make([][]string, len(s)) 36 | var maxLen int 37 | for i, v := range s { 38 | rows[i] = v.toRow() 39 | maxLen = IntMax(maxLen, len(rows[i][0])) 40 | } 41 | title, header, widths := 42 | nodeTitle, nodeHeader, nodeWidthFn(rect, maxLen) 43 | 44 | if len(s) == 0 { 45 | header = emptyHeader 46 | widths = emptyWidthFn(rect) 47 | rows = emptyRows 48 | } 49 | return title, header, widths, rows 50 | } 51 | 52 | func (s sortByNameForNode) GetRows() [][]string { 53 | if len(s) == 0 { 54 | return emptyRows 55 | } 56 | rows := make([][]string, len(s)) 57 | for i, v := range s { 58 | rows[i] = v.toRow() 59 | } 60 | return rows 61 | } 62 | 63 | func (s sortByNameForNode) SortRows() { 64 | sort.Slice(s, func(i, j int) bool { 65 | return s[i].nodeName < s[j].nodeName 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /pkg/resource/node.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "k8s.io/metrics/pkg/apis/metrics" 6 | 7 | . "github.com/ynqa/ktop/pkg/util" 8 | ) 9 | 10 | type NodeResource struct { 11 | nodeName string 12 | capacity corev1.ResourceList 13 | allocatable corev1.ResourceList 14 | usage corev1.ResourceList 15 | } 16 | 17 | func NewNodeResource(n corev1.Node, nm metrics.NodeMetrics) *NodeResource { 18 | return &NodeResource{ 19 | nodeName: nm.Name, 20 | capacity: n.Status.Capacity, 21 | allocatable: n.Status.Allocatable, 22 | usage: nm.Usage, 23 | } 24 | } 25 | 26 | func (r *NodeResource) GetNodeName() string { 27 | return r.nodeName 28 | } 29 | 30 | func (r *NodeResource) GetCpuUsagePercentage() (float64, string) { 31 | return GetResourcePercentage(*r.usage.Cpu(), *r.allocatable.Cpu()), 32 | GetResourcePercentageString(*r.usage.Cpu(), *r.allocatable.Cpu()) 33 | } 34 | 35 | func (r *NodeResource) GetMemoryUsagePercentage() (float64, string) { 36 | return GetResourcePercentage(*r.usage.Memory(), *r.allocatable.Memory()), 37 | GetResourcePercentageString(*r.usage.Memory(), *r.allocatable.Memory()) 38 | } 39 | 40 | // header: "NODE", "CPU(C)", "CPU(A)", "CPU(U)", "%CPU", "Memory(C)", "Memory(A)", "Memory(U)", "%Memory", 41 | func (r *NodeResource) toRow() []string { 42 | return []string{ 43 | r.nodeName, 44 | GetResourceValueString(r.allocatable, corev1.ResourceCPU), 45 | GetResourceValueString(r.usage, corev1.ResourceCPU), 46 | GetResourcePercentageString(*r.usage.Cpu(), *r.allocatable.Cpu()), 47 | GetResourceValueString(r.allocatable, corev1.ResourceMemory), 48 | GetResourceValueString(r.usage, corev1.ResourceMemory), 49 | GetResourcePercentageString(*r.usage.Memory(), *r.allocatable.Memory()), 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | "k8s.io/metrics/pkg/apis/metrics" 6 | 7 | . "github.com/ynqa/ktop/pkg/util" 8 | ) 9 | 10 | type Resource struct { 11 | nodeName string 12 | podName string 13 | containerName string 14 | usage corev1.ResourceList 15 | limits corev1.ResourceList 16 | requests corev1.ResourceList 17 | } 18 | 19 | func NewResource(p corev1.Pod, c corev1.Container, cm metrics.ContainerMetrics) *Resource { 20 | return &Resource{ 21 | nodeName: p.Spec.NodeName, 22 | podName: p.Name, 23 | containerName: c.Name, 24 | usage: cm.Usage, 25 | limits: c.Resources.Limits, 26 | requests: c.Resources.Requests, 27 | } 28 | } 29 | 30 | func (r *Resource) GetNodeName() string { 31 | return r.nodeName 32 | } 33 | 34 | func (r *Resource) GetContainerName() string { 35 | return r.containerName 36 | } 37 | 38 | func (r *Resource) GetCpuLimits() (float64, string, bool) { 39 | _, ok := r.limits[corev1.ResourceCPU] 40 | str := GetResourceValueString(r.limits, corev1.ResourceCPU) 41 | return GetResourceValue(r.limits, corev1.ResourceCPU), str, ok 42 | } 43 | 44 | func (r *Resource) GetCpuUsage() (float64, string) { 45 | return GetResourceValue(r.usage, corev1.ResourceCPU), 46 | GetResourceValueString(r.usage, corev1.ResourceCPU) 47 | } 48 | 49 | func (r *Resource) GetMemoryLimits() (float64, string, bool) { 50 | _, ok := r.limits[corev1.ResourceMemory] 51 | str := GetResourceValueString(r.limits, corev1.ResourceMemory) 52 | return GetResourceValue(r.limits, corev1.ResourceMemory), str, ok 53 | } 54 | 55 | func (r *Resource) GetMemoryUsage() (float64, string) { 56 | return GetResourceValue(r.usage, corev1.ResourceMemory), 57 | GetResourceValueString(r.usage, corev1.ResourceMemory) 58 | } 59 | 60 | // header: "POD", "CONTAINER", "CPU(U)", "CPU(L)", "CPU(R)", "Mem(U)", "Mem(L)", "Mem(R)" 61 | func (r *Resource) toRow() []string { 62 | return []string{ 63 | r.podName, 64 | r.containerName, 65 | GetResourceValueString(r.usage, corev1.ResourceCPU), 66 | GetResourceValueString(r.limits, corev1.ResourceCPU), 67 | GetResourceValueString(r.requests, corev1.ResourceCPU), 68 | GetResourceValueString(r.usage, corev1.ResourceMemory), 69 | GetResourceValueString(r.limits, corev1.ResourceMemory), 70 | GetResourceValueString(r.requests, corev1.ResourceMemory), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ktop 2 | 3 | A visualized monitoring dashboard for Kubernetes. 4 | 5 | `kubectl` also has `top` subcommand, but it is not able to: 6 | 7 | - Watch usages regularly for Pod/Node 8 | - Compare the usage of Pod resources with Node or limits/requests 9 | 10 | `ktop` resolves these problems and has a more graphical dashboard. 11 | 12 | ## Installation 13 | 14 | For MacOS: 15 | 16 | ```bash 17 | $ brew tap ynqa/tap-archived 18 | $ brew install ktop 19 | ``` 20 | 21 | From source codes: 22 | 23 | ```bash 24 | $ go get -u github.com/ynqa/ktop 25 | ``` 26 | 27 | ## Usage 28 | ``` 29 | Kubernetes monitoring dashboard on terminal 30 | 31 | Usage: 32 | ktop [flags] 33 | 34 | Flags: 35 | --as string Username to impersonate for the operation 36 | --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. 37 | --cache-dir string Default HTTP cache directory (default "/Users/ynqa/.kube/http-cache") 38 | --certificate-authority string Path to a cert file for the certificate authority 39 | --client-certificate string Path to a client certificate file for TLS 40 | --client-key string Path to a client key file for TLS 41 | --cluster string The name of the kubeconfig cluster to use 42 | -C, --container-query string container query (default ".*") 43 | --context string The name of the kubeconfig context to use 44 | -h, --help help for ktop 45 | --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure 46 | -i, --interval duration set interval (default 1s) 47 | --kubeconfig string Path to the kubeconfig file to use for CLI requests. 48 | -n, --namespace string If present, the namespace scope for this CLI request 49 | -N, --node-query string node query (default ".*") 50 | -P, --pod-query string pod query (default ".*") 51 | --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") 52 | -s, --server string The address and port of the Kubernetes API server 53 | --token string Bearer token for authentication to the API server 54 | --user string The name of the kubeconfig user to use 55 | ``` 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ynqa/ktop 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gizak/termui/v3 v3.1.0 7 | github.com/pkg/errors v0.8.1 8 | github.com/spf13/cobra v0.0.3 9 | k8s.io/api v0.0.0-20190222213804-5cb15d344471 10 | k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 11 | k8s.io/cli-runtime v0.0.0-20190228180923-a9e421a79326 12 | k8s.io/client-go v0.0.0-20190228174230-b40b2a5939e4 13 | k8s.io/kubernetes v1.13.4 14 | k8s.io/metrics v0.0.0-20190228180609-34472d076c30 15 | ) 16 | 17 | require ( 18 | cloud.google.com/go v0.37.0 // indirect 19 | contrib.go.opencensus.io/exporter/ocagent v0.2.0 // indirect 20 | github.com/Azure/go-autorest v11.5.2+incompatible // indirect 21 | github.com/census-instrumentation/opencensus-proto v0.1.0 // indirect 22 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 23 | github.com/evanphx/json-patch v4.1.0+incompatible // indirect 24 | github.com/gogo/protobuf v1.2.1 // indirect 25 | github.com/golang/protobuf v1.3.1 // indirect 26 | github.com/google/btree v1.0.0 // indirect 27 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect 28 | github.com/googleapis/gnostic v0.2.0 // indirect 29 | github.com/gophercloud/gophercloud v0.0.0-20190315020455-954aa14363ce // indirect 30 | github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect 31 | github.com/hashicorp/golang-lru v0.5.0 // indirect 32 | github.com/imdario/mergo v0.3.7 // indirect 33 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 34 | github.com/json-iterator/go v1.1.6 // indirect 35 | github.com/mattn/go-runewidth v0.0.4 // indirect 36 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 39 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect 40 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 41 | github.com/spf13/pflag v1.0.3 // indirect 42 | go.opencensus.io v0.19.1 // indirect 43 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect 44 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977 // indirect 45 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 // indirect 46 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 // indirect 47 | golang.org/x/sys v0.6.0 // indirect 48 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect 49 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect 50 | google.golang.org/api v0.2.0 // indirect 51 | google.golang.org/appengine v1.4.0 // indirect 52 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 // indirect 53 | google.golang.org/grpc v1.19.0 // indirect 54 | gopkg.in/inf.v0 v0.9.1 // indirect 55 | gopkg.in/yaml.v2 v2.2.2 // indirect 56 | k8s.io/klog v0.2.0 // indirect 57 | sigs.k8s.io/yaml v1.1.0 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /pkg/ui/graph.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "image" 5 | 6 | . "github.com/gizak/termui/v3" 7 | ) 8 | 9 | type Graph struct { 10 | *Block 11 | // plot data 12 | Data []float64 13 | UpperLimit float64 14 | DrawUpperLimit bool 15 | 16 | // label 17 | LabelHeader string 18 | LabelData string 19 | LabelUpperLimit string 20 | 21 | // color 22 | DataColor Color 23 | LimitColor Color 24 | LabelNameColor Color 25 | } 26 | 27 | func NewGraph() *Graph { 28 | return &Graph{ 29 | Block: NewBlock(), 30 | Data: make([]float64, 0), 31 | } 32 | } 33 | 34 | func (self *Graph) Reset() { 35 | self.Data = make([]float64, 0) 36 | self.UpperLimit = 0 37 | self.LabelHeader = "" 38 | self.LabelData = "" 39 | self.LabelUpperLimit = "" 40 | } 41 | 42 | func (self *Graph) calcHeight(val float64) int { 43 | return int((val / self.UpperLimit) * float64(self.Inner.Dy()-5)) 44 | } 45 | 46 | func (self *Graph) Draw(buf *Buffer) { 47 | self.Block.Draw(buf) 48 | 49 | // describe graph 50 | if len(self.Data) != 0 { 51 | canvas := NewCanvas() 52 | canvas.Rectangle = self.Inner 53 | // draw upper limit 54 | if self.DrawUpperLimit { 55 | limitHeight := self.calcHeight(self.UpperLimit) 56 | canvas.SetLine( 57 | image.Pt( 58 | self.Inner.Min.X*2, 59 | (self.Inner.Max.Y-limitHeight-1)*4, 60 | ), 61 | image.Pt( 62 | self.Inner.Max.X*2, 63 | (self.Inner.Max.Y-limitHeight-1)*4, 64 | ), 65 | self.LimitColor, 66 | ) 67 | } 68 | 69 | // use latest data 70 | data := self.Data 71 | if len(self.Data) > self.Inner.Dx() { 72 | data = data[len(self.Data)-1-self.Inner.Dx() : len(self.Data)-1] 73 | } 74 | previousHeight := self.calcHeight(data[len(data)-1]) 75 | for i := len(data) - 1; i >= 0; i-- { 76 | height := self.calcHeight(data[i]) 77 | // draw data 78 | canvas.SetLine( 79 | image.Pt( 80 | (self.Inner.Min.X+i)*2, 81 | (self.Inner.Max.Y-previousHeight-1)*4, 82 | ), 83 | image.Pt( 84 | (self.Inner.Min.X+i+1)*2, 85 | (self.Inner.Max.Y-height-1)*4, 86 | ), 87 | self.DataColor, 88 | ) 89 | previousHeight = height 90 | } 91 | canvas.Draw(buf) 92 | } 93 | 94 | // describe labels 95 | stage := 1 96 | if self.Inner.Dy() >= 3 { 97 | if self.LabelHeader != "" { 98 | buf.SetString( 99 | self.LabelHeader, 100 | NewStyle(self.LabelNameColor, ColorClear, ModifierBold), 101 | image.Pt(self.Inner.Min.X+1, self.Inner.Min.Y+stage), 102 | ) 103 | stage++ 104 | } 105 | if self.LabelUpperLimit != "" { 106 | buf.SetString( 107 | self.LabelUpperLimit, 108 | NewStyle(self.LimitColor), 109 | image.Pt(self.Inner.Min.X+2, self.Inner.Min.Y+stage), 110 | ) 111 | stage++ 112 | } 113 | if self.LabelData != "" { 114 | buf.SetString( 115 | self.LabelData, 116 | NewStyle(self.DataColor), 117 | image.Pt(self.Inner.Min.X+2, self.Inner.Min.Y+stage), 118 | ) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /pkg/ui/table.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "image" 5 | "strings" 6 | 7 | . "github.com/gizak/termui/v3" 8 | ) 9 | 10 | type Table struct { 11 | *Block 12 | 13 | Header []string 14 | ColumnWidths []int 15 | Rows [][]string 16 | Cursor bool 17 | CursorColor Color 18 | topRow int 19 | 20 | SelectedRow int 21 | } 22 | 23 | func NewTable() *Table { 24 | return &Table{ 25 | Block: NewBlock(), 26 | Cursor: true, 27 | topRow: 0, 28 | SelectedRow: 0, 29 | } 30 | } 31 | 32 | func (self *Table) Reset(title string, header []string, width []int) { 33 | self.Title = title 34 | self.Header = header 35 | self.ColumnWidths = width 36 | self.Rows = [][]string{} 37 | self.topRow = 0 38 | self.SelectedRow = 0 39 | } 40 | 41 | func (self *Table) Draw(buf *Buffer) { 42 | self.Block.Draw(buf) 43 | 44 | if self.Inner.Dy() > 2 { 45 | // store positions for each column 46 | columnPositions := []int{} 47 | var cur int 48 | for _, w := range self.ColumnWidths { 49 | columnPositions = append(columnPositions, cur) 50 | cur += w 51 | } 52 | 53 | // describe a header 54 | for i, h := range self.Header { 55 | buf.SetString( 56 | h, 57 | NewStyle(Theme.Default.Fg, ColorClear, ModifierBold), 58 | image.Pt(self.Inner.Min.X+columnPositions[i], self.Inner.Min.Y), 59 | ) 60 | } 61 | 62 | if self.SelectedRow < self.topRow { 63 | self.topRow = self.SelectedRow 64 | } else if self.SelectedRow > self.cursorBottom() { 65 | self.topRow = self.cursorBottom() 66 | } 67 | 68 | // describe rows 69 | for idx := self.topRow; idx >= 0 && idx < len(self.Rows) && idx < self.bottom(); idx++ { 70 | row := self.Rows[idx] 71 | // move y+1 for a header 72 | y := self.Inner.Min.Y + 1 + idx - self.topRow 73 | style := NewStyle(Theme.Default.Fg) 74 | if self.Cursor { 75 | if idx == self.SelectedRow { 76 | style.Fg = self.CursorColor 77 | style.Modifier = ModifierReverse 78 | buf.SetString( 79 | strings.Repeat(" ", self.Inner.Dx()), 80 | style, 81 | image.Pt(self.Inner.Min.X, y), 82 | ) 83 | self.SelectedRow = idx 84 | } 85 | } 86 | for i, width := range self.ColumnWidths { 87 | r := TrimString(row[i], width) 88 | buf.SetString( 89 | r, 90 | style, 91 | image.Pt(self.Inner.Min.X+columnPositions[i], y), 92 | ) 93 | } 94 | } 95 | } 96 | } 97 | 98 | func (self *Table) cursorBottom() int { 99 | return self.topRow + self.Inner.Dy() - 2 100 | } 101 | 102 | func (self *Table) bottom() int { 103 | return self.topRow + self.Inner.Dy() - 1 104 | } 105 | 106 | func (self *Table) scroll(i int) { 107 | self.SelectedRow += i 108 | maxRow := len(self.Rows) - 1 109 | if self.SelectedRow < 0 { 110 | self.SelectedRow = 0 111 | } else if self.SelectedRow > maxRow { 112 | self.SelectedRow = maxRow 113 | } 114 | } 115 | 116 | func (self *Table) ScrollUp() { 117 | self.scroll(-1) 118 | } 119 | 120 | func (self *Table) ScrollDown() { 121 | self.scroll(1) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/api/resource" 9 | "k8s.io/metrics/pkg/apis/metrics" 10 | ) 11 | 12 | func FilterNodeMetrics(query *regexp.Regexp, nodes []metrics.NodeMetrics) []metrics.NodeMetrics { 13 | var filtered []metrics.NodeMetrics 14 | for _, node := range nodes { 15 | if query.MatchString(node.Name) { 16 | filtered = append(filtered, node) 17 | } 18 | } 19 | return filtered 20 | } 21 | 22 | func FilterPodMetrics(query *regexp.Regexp, pods []metrics.PodMetrics) []metrics.PodMetrics { 23 | var filtered []metrics.PodMetrics 24 | for _, pod := range pods { 25 | if query.MatchString(pod.Name) { 26 | filtered = append(filtered, pod) 27 | } 28 | } 29 | return filtered 30 | } 31 | 32 | func FilterContainerMetrics(query *regexp.Regexp, containers []metrics.ContainerMetrics) []metrics.ContainerMetrics { 33 | var filtered []metrics.ContainerMetrics 34 | for _, container := range containers { 35 | if query.MatchString(container.Name) { 36 | filtered = append(filtered, container) 37 | } 38 | } 39 | return filtered 40 | } 41 | 42 | func FindNode(name string, nodes []corev1.Node) *corev1.Node { 43 | for _, node := range nodes { 44 | if name == node.Name { 45 | return &node 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func FindPod(name string, pods []corev1.Pod) *corev1.Pod { 52 | for _, pod := range pods { 53 | if name == pod.Name { 54 | return &pod 55 | } 56 | } 57 | return nil 58 | } 59 | 60 | func FindContainer(name string, containers []corev1.Container) *corev1.Container { 61 | for _, container := range containers { 62 | if name == container.Name { 63 | return &container 64 | } 65 | } 66 | return nil 67 | } 68 | 69 | func GetResourceValue(lst corev1.ResourceList, typ corev1.ResourceName) float64 { 70 | val, ok := lst[typ] 71 | switch { 72 | case typ == corev1.ResourceCPU && ok: 73 | return float64(val.MilliValue()) 74 | case typ == corev1.ResourceMemory && ok: 75 | return float64(val.Value() / (1024 * 1024)) 76 | } 77 | return 0 78 | } 79 | 80 | func GetResourceValueString(lst corev1.ResourceList, typ corev1.ResourceName) string { 81 | val, ok := lst[typ] 82 | switch { 83 | case typ == corev1.ResourceCPU && ok: 84 | return fmt.Sprintf("%vm", val.MilliValue()) 85 | case typ == corev1.ResourceMemory && ok: 86 | return fmt.Sprintf("%vMi", val.Value()/(1024*1024)) 87 | default: 88 | return "-" 89 | } 90 | } 91 | 92 | func GetResourcePercentage(usage, available resource.Quantity) float64 { 93 | return float64(usage.MilliValue()) / float64(available.MilliValue()) * 100 94 | } 95 | 96 | func GetResourcePercentageString(usage, available resource.Quantity) string { 97 | return fmt.Sprintf("%v%%", int(float64(usage.MilliValue())/float64(available.MilliValue())*100)) 98 | } 99 | 100 | func IntMax(x, y int) int { 101 | if x > y { 102 | return x 103 | } 104 | return y 105 | } 106 | 107 | func IntMin(x, y int) int { 108 | if x < y { 109 | return x 110 | } 111 | return y 112 | } 113 | -------------------------------------------------------------------------------- /pkg/resource/resource_viewer.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "container/ring" 5 | "image" 6 | "sort" 7 | 8 | . "github.com/ynqa/ktop/pkg/util" 9 | ) 10 | 11 | type SortType int 12 | 13 | const ( 14 | ByName SortType = iota 15 | ) 16 | 17 | type ResourceTableViewer interface { 18 | GetTableShape(rect image.Rectangle) (string, []string, []int, [][]string) 19 | SortRows() 20 | } 21 | 22 | var ( 23 | SummarizedType = "Summarized" 24 | AllType = "All" 25 | NodeType = "Node" 26 | 27 | allTitle = "⎈ Pod/Container ⎈" 28 | allHeader = []string{ 29 | "POD", "CONTAINER", 30 | "CPU(U)", "CPU(L)", "CPU(R)", 31 | "Memory(U)", "Memory(L)", "Memory(R)", 32 | } 33 | indentSize = 4 34 | allWidthFn = func(rect image.Rectangle, maxLen0, maxLen1 int) []int { 35 | podWidth := IntMax(40, IntMin(rect.Dx()-60, maxLen0+indentSize)) 36 | containerWidth := IntMax(30, IntMin(rect.Dx()-60, maxLen1+indentSize)) 37 | return []int{podWidth, containerWidth, 10, 10, 10, 10, 10, 10} 38 | } 39 | 40 | emptyHeader = []string{ 41 | "Message", 42 | } 43 | emptyWidthFn = func(rect image.Rectangle) []int { 44 | return []int{rect.Dx() - 1} 45 | } 46 | emptyRows = [][]string{[]string{"No data points"}} 47 | ) 48 | 49 | func TableTypeCircle() *ring.Ring { 50 | types := []string{SummarizedType, AllType, NodeType} 51 | circle := ring.New(len(types)) 52 | for _, typ := range types { 53 | circle.Value = typ 54 | circle = circle.Next() 55 | } 56 | return circle 57 | } 58 | 59 | func ResetTableShapeFrom(typ string, rect image.Rectangle) (string, []string, []int) { 60 | switch typ { 61 | case SummarizedType: 62 | return summarizedTitle, summarizedHeader, summarizedWidthFn(rect, 0) 63 | case AllType: 64 | return allTitle, allHeader, allWidthFn(rect, 0, 0) 65 | case NodeType: 66 | return nodeTitle, nodeHeader, nodeWidthFn(rect, 0) 67 | default: 68 | return summarizedTitle, summarizedHeader, summarizedWidthFn(rect, 0) 69 | } 70 | } 71 | 72 | func AsAllTableViewer(resources []*Resource, sortType SortType) ResourceTableViewer { 73 | switch sortType { 74 | case ByName: 75 | return sortByName(resources) 76 | default: 77 | return sortByName(resources) 78 | } 79 | } 80 | 81 | type sortByName []*Resource 82 | 83 | func (s sortByName) GetTableShape(rect image.Rectangle) (string, []string, []int, [][]string) { 84 | rows := make([][]string, len(s)) 85 | var maxLen0, maxLen1 int 86 | for i, v := range s { 87 | rows[i] = v.toRow() 88 | maxLen0 = IntMax(maxLen0, len(rows[i][0])) 89 | maxLen1 = IntMax(maxLen1, len(rows[i][1])) 90 | } 91 | title, header, widths := 92 | allTitle, allHeader, allWidthFn(rect, maxLen0, maxLen1) 93 | 94 | if len(s) == 0 { 95 | header = emptyHeader 96 | widths = emptyWidthFn(rect) 97 | rows = emptyRows 98 | } 99 | return title, header, widths, rows 100 | } 101 | 102 | func (s sortByName) SortRows() { 103 | sort.Slice(s, func(i, j int) bool { 104 | if s[i].podName < s[j].podName { 105 | return true 106 | } 107 | if s[i].podName > s[j].podName { 108 | return false 109 | } 110 | return s[i].containerName < s[j].containerName 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /cmd/ktop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "regexp" 7 | "sync" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/gizak/termui/v3" 12 | "github.com/spf13/cobra" 13 | 14 | "k8s.io/cli-runtime/pkg/genericclioptions" 15 | _ "k8s.io/client-go/plugin/pkg/client/auth" 16 | 17 | "github.com/ynqa/ktop/pkg/ktop" 18 | "github.com/ynqa/ktop/pkg/kube" 19 | "github.com/ynqa/ktop/pkg/ui" 20 | ) 21 | 22 | const ( 23 | logoStr = ` 24 | __ __ ______ ______ ______ 25 | /\ \/ / /\__ _\ /\ __ \ /\ == \ 26 | \ \ _"-. \/_/\ \/ \ \ \/\ \ \ \ _-/ 27 | \ \_\ \_\ \ \_\ \ \_____\ \ \_\ 28 | \/_/\/_/ \/_/ \/_____/ \/_/ 29 | ` 30 | hintStr = ` 31 | , Quit 32 | Up 33 | Down 34 | , Switch Table Mode 35 | ` 36 | ) 37 | 38 | type ktopCmd struct { 39 | k8sFlags *genericclioptions.ConfigFlags 40 | interval time.Duration 41 | nodeQuery string 42 | podQuery string 43 | containerQuery string 44 | renderMutex sync.RWMutex 45 | } 46 | 47 | func newKtopCmd() *cobra.Command { 48 | ktop := ktopCmd{} 49 | cmd := &cobra.Command{ 50 | Use: "ktop", 51 | Short: "Kubernetes monitoring dashboard on terminal", 52 | RunE: ktop.run, 53 | } 54 | cmd.Flags().DurationVarP( 55 | &ktop.interval, 56 | "interval", 57 | "i", 58 | 1*time.Second, 59 | "set interval", 60 | ) 61 | cmd.Flags().StringVarP( 62 | &ktop.nodeQuery, 63 | "node-query", 64 | "N", 65 | ".*", 66 | "node query", 67 | ) 68 | cmd.Flags().StringVarP( 69 | &ktop.podQuery, 70 | "pod-query", 71 | "P", 72 | ".*", 73 | "pod query", 74 | ) 75 | cmd.Flags().StringVarP( 76 | &ktop.containerQuery, 77 | "container-query", 78 | "C", 79 | ".*", 80 | "container query", 81 | ) 82 | ktop.k8sFlags = genericclioptions.NewConfigFlags() 83 | ktop.k8sFlags.AddFlags(cmd.Flags()) 84 | if *ktop.k8sFlags.Namespace == "" { 85 | *ktop.k8sFlags.Namespace = "default" 86 | } 87 | return cmd 88 | } 89 | 90 | func (k *ktopCmd) render(items ...termui.Drawable) { 91 | k.renderMutex.Lock() 92 | defer k.renderMutex.Unlock() 93 | termui.Render(items...) 94 | } 95 | 96 | func (k *ktopCmd) run(cmd *cobra.Command, args []string) error { 97 | if err := termui.Init(); err != nil { 98 | return err 99 | } 100 | defer termui.Close() 101 | 102 | kubeclients, err := kube.NewKubeClients(k.k8sFlags) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | // define queries 108 | podQuery, err := regexp.Compile(k.podQuery) 109 | if err != nil { 110 | return err 111 | } 112 | containerQuery, err := regexp.Compile(k.containerQuery) 113 | if err != nil { 114 | return err 115 | } 116 | nodeQuery, err := regexp.Compile(k.nodeQuery) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | monitor := ktop.NewMonitor(kubeclients, podQuery, containerQuery, nodeQuery) 122 | logo := ui.NewTextField() 123 | logo.Text = logoStr 124 | logo.TextStyle = termui.NewStyle(termui.ColorWhite, termui.ColorClear, termui.ModifierBold) 125 | hint := ui.NewTextField() 126 | hint.Text = hintStr 127 | hint.TextStyle = termui.NewStyle(termui.Color(244), termui.ColorClear) 128 | 129 | grid := termui.NewGrid() 130 | grid.Set( 131 | termui.NewRow(1./6, 132 | termui.NewCol(1./2, logo), 133 | termui.NewCol(1./2, hint), 134 | ), 135 | termui.NewRow(6./12, monitor.GetPodTable()), 136 | termui.NewRow(4./12, 137 | termui.NewCol(1./2, monitor.GetCPUGraph()), 138 | termui.NewCol(1./2, monitor.GetMemGraph()), 139 | ), 140 | ) 141 | termWidth, termHeight := termui.TerminalDimensions() 142 | grid.SetRect(0, 0, termWidth, termHeight) 143 | 144 | events := termui.PollEvents() 145 | tick := time.NewTicker(k.interval) 146 | sigCh := make(chan os.Signal, 1) 147 | signal.Notify(sigCh, syscall.SIGTERM, os.Interrupt) 148 | 149 | for { 150 | select { 151 | case <-sigCh: 152 | return nil 153 | case <-tick.C: 154 | if err := monitor.Update(); err != nil { 155 | return err 156 | } 157 | case e := <-events: 158 | switch e.ID { 159 | case "": 160 | monitor.ScrollDown() 161 | case "": 162 | monitor.ScrollUp() 163 | case "": 164 | monitor.Rotate() 165 | case "": 166 | monitor.ReverseRotate() 167 | case "q", "": 168 | return nil 169 | case "": 170 | termWidth, termHeight := termui.TerminalDimensions() 171 | grid.SetRect(0, 0, termWidth, termHeight) 172 | } 173 | } 174 | k.render(grid) 175 | } 176 | } 177 | 178 | func Execute() { 179 | rootCmd := newKtopCmd() 180 | if err := rootCmd.Execute(); err != nil { 181 | os.Exit(1) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /pkg/kube/clients.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/labels" 9 | "k8s.io/cli-runtime/pkg/genericclioptions" 10 | "k8s.io/client-go/kubernetes" 11 | corev1client "k8s.io/client-go/kubernetes/typed/core/v1" 12 | "k8s.io/client-go/rest" 13 | "k8s.io/kubernetes/pkg/kubectl/metricsutil" 14 | "k8s.io/metrics/pkg/apis/metrics" 15 | metricsv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" 16 | "k8s.io/metrics/pkg/client/clientset/versioned" 17 | ) 18 | 19 | type KubeClients struct { 20 | Flags *genericclioptions.ConfigFlags 21 | clientset *kubernetes.Clientset 22 | metricsClient metricsClient 23 | } 24 | 25 | func NewKubeClients(flags *genericclioptions.ConfigFlags) (*KubeClients, error) { 26 | config, err := flags.ToRESTConfig() 27 | if err != nil { 28 | return nil, err 29 | } 30 | clientset, err := kubernetes.NewForConfig(config) 31 | if err != nil { 32 | return nil, err 33 | } 34 | var metricsClient metricsClient 35 | mergedErr := errors.New("Failed to create metrics client") 36 | metricsClient, err = newMetricsServerClient(config) 37 | if err != nil { 38 | mergedErr = errors.Wrap(mergedErr, err.Error()) 39 | metricsClient, err = newHeapsterClient(clientset.CoreV1()) 40 | if err != nil { 41 | mergedErr = errors.Wrap(mergedErr, err.Error()) 42 | return nil, mergedErr 43 | } 44 | } 45 | return &KubeClients{ 46 | Flags: flags, 47 | clientset: clientset, 48 | metricsClient: metricsClient, 49 | }, nil 50 | } 51 | 52 | func (k *KubeClients) GetPodList(namespace string, labelSelector labels.Selector) (*corev1.PodList, error) { 53 | return k.clientset.CoreV1().Pods(namespace).List(metav1.ListOptions{LabelSelector: labelSelector.String()}) 54 | } 55 | 56 | func (k *KubeClients) GetPodMetricsList(namespace string, labelSelector labels.Selector) (*metrics.PodMetricsList, error) { 57 | return k.metricsClient.getPodMetricsList(namespace, labelSelector) 58 | } 59 | 60 | func (k *KubeClients) GetNodeList(labelSelector labels.Selector) (*corev1.NodeList, error) { 61 | return k.clientset.CoreV1().Nodes().List(metav1.ListOptions{LabelSelector: labelSelector.String()}) 62 | } 63 | 64 | func (k *KubeClients) GetNodeMetricsList(labelSelector labels.Selector) (*metrics.NodeMetricsList, error) { 65 | return k.metricsClient.getNodeMetricsList(labelSelector) 66 | } 67 | 68 | type metricsClient interface { 69 | getPodMetricsList(namespace string, labelSelector labels.Selector) (*metrics.PodMetricsList, error) 70 | getNodeMetricsList(labelSelector labels.Selector) (*metrics.NodeMetricsList, error) 71 | } 72 | 73 | type metricsServerClient struct { 74 | *versioned.Clientset 75 | } 76 | 77 | func newMetricsServerClient(config *rest.Config) (*metricsServerClient, error) { 78 | clientset, err := versioned.NewForConfig(config) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return &metricsServerClient{ 83 | Clientset: clientset, 84 | }, nil 85 | } 86 | 87 | func (c *metricsServerClient) getPodMetricsList(namespace string, labelSelector labels.Selector) (*metrics.PodMetricsList, error) { 88 | list, err := c.MetricsV1beta1().PodMetricses(namespace).List(metav1.ListOptions{LabelSelector: labelSelector.String()}) 89 | if err != nil { 90 | return nil, err 91 | } 92 | old := &metrics.PodMetricsList{} 93 | if err := metricsv1beta1.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(list, old, nil); err != nil { 94 | return nil, err 95 | } 96 | return old, nil 97 | } 98 | 99 | func (c *metricsServerClient) getNodeMetricsList(labelSelector labels.Selector) (*metrics.NodeMetricsList, error) { 100 | list, err := c.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{LabelSelector: labelSelector.String()}) 101 | if err != nil { 102 | return nil, err 103 | } 104 | old := &metrics.NodeMetricsList{} 105 | if err := metricsv1beta1.Convert_v1beta1_NodeMetricsList_To_metrics_NodeMetricsList(list, old, nil); err != nil { 106 | return nil, err 107 | } 108 | return old, nil 109 | } 110 | 111 | type heapsterClient struct { 112 | *metricsutil.HeapsterMetricsClient 113 | } 114 | 115 | func newHeapsterClient(svcClient corev1client.ServicesGetter) (*heapsterClient, error) { 116 | heapster := metricsutil.NewHeapsterMetricsClient( 117 | svcClient, 118 | metricsutil.DefaultHeapsterNamespace, 119 | metricsutil.DefaultHeapsterScheme, 120 | metricsutil.DefaultHeapsterService, 121 | metricsutil.DefaultHeapsterPort, 122 | ) 123 | return &heapsterClient{ 124 | HeapsterMetricsClient: heapster, 125 | }, nil 126 | } 127 | 128 | func (c *heapsterClient) getPodMetricsList(namespace string, labelSelector labels.Selector) (*metrics.PodMetricsList, error) { 129 | return c.GetPodMetrics(namespace, "", false, labelSelector) 130 | } 131 | 132 | func (c *heapsterClient) getNodeMetricsList(labelSelector labels.Selector) (*metrics.NodeMetricsList, error) { 133 | return c.GetNodeMetrics("", labelSelector.String()) 134 | } 135 | -------------------------------------------------------------------------------- /pkg/ktop/ktop.go: -------------------------------------------------------------------------------- 1 | package ktop 2 | 3 | import ( 4 | "container/ring" 5 | "fmt" 6 | "regexp" 7 | "sync" 8 | 9 | "github.com/gizak/termui/v3" 10 | "github.com/pkg/errors" 11 | 12 | corev1 "k8s.io/api/core/v1" 13 | kr "k8s.io/apimachinery/pkg/api/resource" 14 | "k8s.io/apimachinery/pkg/labels" 15 | 16 | "github.com/ynqa/ktop/pkg/kube" 17 | "github.com/ynqa/ktop/pkg/resource" 18 | "github.com/ynqa/ktop/pkg/ui" 19 | . "github.com/ynqa/ktop/pkg/util" 20 | ) 21 | 22 | var ( 23 | // style 24 | titleStyle = termui.NewStyle(termui.ColorWhite, termui.ColorClear, termui.ModifierBold) 25 | ) 26 | 27 | const ( 28 | // label names 29 | containerLimitLabel = "ContainerLimits" 30 | nodeAllocatableLabel = "NodeAllocatable" 31 | 32 | // colors 33 | borderColor = termui.ColorBlue 34 | selectedTableColor = termui.ColorYellow 35 | graphLabelNameColor = termui.ColorWhite 36 | graphLimitColor = termui.ColorWhite 37 | graphDataColor = termui.ColorGreen 38 | ) 39 | 40 | type Monitor struct { 41 | *kube.KubeClients 42 | 43 | table *ui.Table 44 | tableTypeCircle *ring.Ring 45 | 46 | cpuGraph *ui.Graph 47 | memGraph *ui.Graph 48 | 49 | podQuery *regexp.Regexp 50 | containerQuery *regexp.Regexp 51 | nodeQuery *regexp.Regexp 52 | } 53 | 54 | func NewMonitor(kubeclients *kube.KubeClients, podQuery, containerQuery, nodeQuery *regexp.Regexp) *Monitor { 55 | monitor := &Monitor{ 56 | KubeClients: kubeclients, 57 | tableTypeCircle: resource.TableTypeCircle(), 58 | podQuery: podQuery, 59 | containerQuery: containerQuery, 60 | nodeQuery: nodeQuery, 61 | } 62 | 63 | // table for resources 64 | table := ui.NewTable() 65 | table.TitleStyle = titleStyle 66 | table.Cursor = true 67 | table.BorderStyle = termui.NewStyle(borderColor) 68 | table.CursorColor = selectedTableColor 69 | 70 | // graph for cpu 71 | cpu := ui.NewGraph() 72 | cpu.Title = "⎈ CPU Usage ⎈" 73 | cpu.TitleStyle = titleStyle 74 | cpu.BorderStyle = termui.NewStyle(borderColor) 75 | cpu.LabelNameColor = graphLabelNameColor 76 | cpu.DataColor = graphDataColor 77 | cpu.LimitColor = graphLimitColor 78 | 79 | // graph for memory 80 | mem := ui.NewGraph() 81 | mem.Title = "⎈ Memory Usage ⎈" 82 | mem.TitleStyle = titleStyle 83 | mem.BorderStyle = termui.NewStyle(borderColor) 84 | mem.LabelNameColor = graphLabelNameColor 85 | mem.DataColor = graphDataColor 86 | mem.LimitColor = graphLimitColor 87 | 88 | monitor.table = table 89 | monitor.cpuGraph = cpu 90 | monitor.memGraph = mem 91 | return monitor 92 | } 93 | 94 | func (m *Monitor) resetGraph() { 95 | m.cpuGraph.Reset() 96 | m.memGraph.Reset() 97 | } 98 | 99 | func (m *Monitor) resetTable() { 100 | m.table.Reset(resource.ResetTableShapeFrom( 101 | m.tableTypeCircle.Value.(string), 102 | m.table.Inner, 103 | )) 104 | } 105 | 106 | func (m *Monitor) ScrollDown() { 107 | m.scrollDown() 108 | m.resetGraph() 109 | } 110 | 111 | func (m *Monitor) scrollDown() { 112 | m.table.ScrollDown() 113 | } 114 | 115 | func (m *Monitor) ScrollUp() { 116 | m.scrollUp() 117 | m.resetGraph() 118 | } 119 | 120 | func (m *Monitor) scrollUp() { 121 | m.table.ScrollUp() 122 | } 123 | 124 | func (m *Monitor) Rotate() { 125 | m.rotate(1) 126 | m.resetGraph() 127 | m.resetTable() 128 | } 129 | 130 | func (m *Monitor) ReverseRotate() { 131 | m.rotate(-1) 132 | m.resetGraph() 133 | m.resetTable() 134 | } 135 | 136 | func (m *Monitor) rotate(i int) { 137 | m.tableTypeCircle = m.tableTypeCircle.Move(i) 138 | } 139 | 140 | func (m *Monitor) GetCPUGraph() *ui.Graph { 141 | return m.cpuGraph 142 | } 143 | 144 | func (m *Monitor) GetMemGraph() *ui.Graph { 145 | return m.memGraph 146 | } 147 | 148 | func (m *Monitor) GetPodTable() *ui.Table { 149 | return m.table 150 | } 151 | 152 | func (m *Monitor) Update() error { 153 | nodeList, err := m.GetNodeList(labels.Everything()) 154 | if err != nil { 155 | return nil 156 | } 157 | 158 | var wg sync.WaitGroup 159 | errCh := make(chan error, 2) 160 | resourcesCh := make(chan []*resource.Resource, 1) 161 | summarizedResourcesCh := make(chan []*resource.SummarizedResource, 1) 162 | nodeResourcesCh := make(chan []*resource.NodeResource, 1) 163 | 164 | wg.Add(1) 165 | go func() { 166 | defer wg.Done() 167 | resources, summarizedResources, err := m.fetchPodResources() 168 | if err != nil { 169 | errCh <- err 170 | return 171 | } 172 | resourcesCh <- resources 173 | summarizedResourcesCh <- summarizedResources 174 | }() 175 | wg.Add(1) 176 | go func() { 177 | defer wg.Done() 178 | nodeResources, err := m.fetchNodeResources(nodeList) 179 | if err != nil { 180 | errCh <- err 181 | return 182 | } 183 | nodeResourcesCh <- nodeResources 184 | }() 185 | 186 | go func() { 187 | wg.Wait() 188 | close(errCh) 189 | close(resourcesCh) 190 | close(summarizedResourcesCh) 191 | close(nodeResourcesCh) 192 | }() 193 | 194 | var mergedError error 195 | for err := range errCh { 196 | if mergedError == nil { 197 | mergedError = errors.New(err.Error()) 198 | } 199 | mergedError = errors.Wrap(mergedError, err.Error()) 200 | } 201 | if mergedError != nil { 202 | return mergedError 203 | } 204 | 205 | resources, ok := <-resourcesCh 206 | if !ok { 207 | return errors.New("Failed to get resources") 208 | } 209 | 210 | summarizedResources, ok := <-summarizedResourcesCh 211 | if !ok { 212 | return errors.New("Failed to get summarized resources") 213 | } 214 | 215 | nodeResources, ok := <-nodeResourcesCh 216 | if !ok { 217 | return errors.New("Failed to get node resources") 218 | } 219 | 220 | // temporary 221 | defer func() { 222 | if p := recover(); p != nil { 223 | m.table.SelectedRow = 0 224 | } 225 | }() 226 | 227 | switch m.tableTypeCircle.Value.(string) { 228 | case resource.SummarizedType: 229 | summarizedViewer := resource.AsSummarizedTableViewer(summarizedResources, resource.ByName) 230 | summarizedViewer.SortRows() 231 | m.updatePodTable(summarizedViewer) 232 | if len(summarizedResources) > 0 { 233 | current := summarizedResources[m.table.SelectedRow] 234 | if err := m.updateSummarizedGraph(nodeList, current); err != nil { 235 | return err 236 | } 237 | } 238 | case resource.AllType: 239 | viewer := resource.AsAllTableViewer(resources, resource.ByName) 240 | viewer.SortRows() 241 | m.updatePodTable(viewer) 242 | if len(resources) > 0 { 243 | current := resources[m.table.SelectedRow] 244 | if err := m.updateAllGraph(nodeList, current); err != nil { 245 | return err 246 | } 247 | } 248 | case resource.NodeType: 249 | nodeViewer := resource.AsNodeTableViewer(nodeResources, resource.ByName) 250 | nodeViewer.SortRows() 251 | m.updatePodTable(nodeViewer) 252 | if len(nodeResources) > 0 { 253 | current := nodeResources[m.table.SelectedRow] 254 | if err := m.updateNodeGraph(current); err != nil { 255 | return err 256 | } 257 | } 258 | default: 259 | } 260 | 261 | return nil 262 | } 263 | 264 | func (m *Monitor) fetchPodResources() ([]*resource.Resource, []*resource.SummarizedResource, error) { 265 | podMetricsList, err := m.GetPodMetricsList(*m.Flags.Namespace, labels.Everything()) 266 | if err != nil { 267 | return nil, nil, err 268 | } 269 | podList, err := m.GetPodList(*m.Flags.Namespace, labels.Everything()) 270 | if err != nil { 271 | return nil, nil, err 272 | } 273 | 274 | // collect resource list 275 | resources := make([]*resource.Resource, 0) 276 | summarizedResources := make([]*resource.SummarizedResource, 0) 277 | // filtered 278 | for _, podMetrics := range FilterPodMetrics(m.podQuery, podMetricsList.Items) { 279 | podName := podMetrics.Name 280 | pod := FindPod(podName, podList.Items) 281 | if pod == nil { 282 | continue 283 | } 284 | var cpu, mem kr.Quantity 285 | // filtered 286 | for _, containerMetrics := range FilterContainerMetrics(m.containerQuery, podMetrics.Containers) { 287 | container := FindContainer(containerMetrics.Name, pod.Spec.Containers) 288 | if container == nil { 289 | continue 290 | } 291 | containerResource := resource.NewResource(*pod, *container, containerMetrics) 292 | resources = append(resources, containerResource) 293 | cpu.Add(*containerMetrics.Usage.Cpu()) 294 | mem.Add(*containerMetrics.Usage.Memory()) 295 | } 296 | summarizedResource := resource.NewSummarizedResource(*pod, 297 | corev1.ResourceList{ 298 | corev1.ResourceCPU: cpu, 299 | corev1.ResourceMemory: mem, 300 | }) 301 | summarizedResources = append(summarizedResources, summarizedResource) 302 | } 303 | return resources, summarizedResources, nil 304 | } 305 | 306 | func (m *Monitor) fetchNodeResources(nodeList *corev1.NodeList) ([]*resource.NodeResource, error) { 307 | nodeMetricsList, err := m.GetNodeMetricsList(labels.Everything()) 308 | if err != nil { 309 | return nil, err 310 | } 311 | resources := make([]*resource.NodeResource, 0) 312 | // filtered 313 | for _, nodeMetrics := range FilterNodeMetrics(m.nodeQuery, nodeMetricsList.Items) { 314 | node := FindNode(nodeMetrics.Name, nodeList.Items) 315 | resources = append(resources, resource.NewNodeResource(*node, nodeMetrics)) 316 | } 317 | return resources, nil 318 | } 319 | 320 | func (m *Monitor) updatePodTable(resources resource.ResourceTableViewer) { 321 | m.table.Title, m.table.Header, m.table.ColumnWidths, m.table.Rows = resources.GetTableShape(m.table.Inner) 322 | } 323 | 324 | func (m *Monitor) updateSummarizedGraph(nodeList *corev1.NodeList, summarized *resource.SummarizedResource) error { 325 | cpuUsage, cpuUsageStr := summarized.GetCpuUsage() 326 | memUsage, memUsageStr := summarized.GetMemoryUsage() 327 | node := FindNode(summarized.GetNodeName(), nodeList.Items) 328 | limitCpu := GetResourceValue(node.Status.Allocatable, corev1.ResourceCPU) 329 | limitCpuStr := GetResourceValueString(node.Status.Allocatable, corev1.ResourceCPU) 330 | limitMemory := GetResourceValue(node.Status.Allocatable, corev1.ResourceMemory) 331 | limitMemoryStr := GetResourceValueString(node.Status.Allocatable, corev1.ResourceMemory) 332 | 333 | m.cpuGraph.LabelHeader = fmt.Sprintf("Name: %v", summarized.GetPodName()) 334 | m.cpuGraph.Data = append(m.cpuGraph.Data, cpuUsage) 335 | m.cpuGraph.LabelData = fmt.Sprintf("Usage: %v", cpuUsageStr) 336 | m.cpuGraph.UpperLimit = limitCpu 337 | m.cpuGraph.DrawUpperLimit = false 338 | m.cpuGraph.LabelUpperLimit = fmt.Sprintf("%v: %v", nodeAllocatableLabel, limitCpuStr) 339 | 340 | m.memGraph.LabelHeader = fmt.Sprintf("Name: %v", summarized.GetPodName()) 341 | m.memGraph.Data = append(m.memGraph.Data, memUsage) 342 | m.memGraph.LabelData = fmt.Sprintf("Usage: %v", memUsageStr) 343 | m.memGraph.UpperLimit = limitMemory 344 | m.memGraph.DrawUpperLimit = false 345 | m.memGraph.LabelUpperLimit = fmt.Sprintf("%v: %v", nodeAllocatableLabel, limitMemoryStr) 346 | return nil 347 | } 348 | 349 | func (m *Monitor) updateAllGraph(nodeList *corev1.NodeList, all *resource.Resource) error { 350 | cpuUsage, cpuUsageStr := all.GetCpuUsage() 351 | memUsage, memUsageStr := all.GetMemoryUsage() 352 | 353 | limitCpuLabel := containerLimitLabel 354 | limitCpu, limitCpuStr, cok := all.GetCpuLimits() 355 | limitMemoryLabel := containerLimitLabel 356 | limitMemory, limitMemoryStr, mok := all.GetMemoryLimits() 357 | 358 | var node *corev1.Node 359 | if !cok || !mok { 360 | node = FindNode(all.GetNodeName(), nodeList.Items) 361 | } 362 | if !cok { 363 | limitCpuLabel = nodeAllocatableLabel 364 | limitCpu = GetResourceValue(node.Status.Allocatable, corev1.ResourceCPU) 365 | limitCpuStr = GetResourceValueString(node.Status.Allocatable, corev1.ResourceCPU) 366 | } 367 | if !mok { 368 | limitMemoryLabel = nodeAllocatableLabel 369 | limitMemory = GetResourceValue(node.Status.Allocatable, corev1.ResourceMemory) 370 | limitMemoryStr = GetResourceValueString(node.Status.Allocatable, corev1.ResourceMemory) 371 | } 372 | 373 | m.cpuGraph.LabelHeader = fmt.Sprintf("Name: %v", all.GetContainerName()) 374 | m.cpuGraph.Data = append(m.cpuGraph.Data, cpuUsage) 375 | m.cpuGraph.LabelData = fmt.Sprintf("Usage: %v", cpuUsageStr) 376 | m.cpuGraph.UpperLimit = limitCpu 377 | m.cpuGraph.DrawUpperLimit = false 378 | m.cpuGraph.LabelUpperLimit = fmt.Sprintf("%v: %v", limitCpuLabel, limitCpuStr) 379 | 380 | m.memGraph.LabelHeader = fmt.Sprintf("Name: %v", all.GetContainerName()) 381 | m.memGraph.Data = append(m.memGraph.Data, memUsage) 382 | m.memGraph.LabelData = fmt.Sprintf("Usage: %v", memUsageStr) 383 | m.memGraph.UpperLimit = limitMemory 384 | m.memGraph.DrawUpperLimit = false 385 | m.memGraph.LabelUpperLimit = fmt.Sprintf("%v: %v", limitMemoryLabel, limitMemoryStr) 386 | return nil 387 | } 388 | 389 | func (m *Monitor) updateNodeGraph(node *resource.NodeResource) error { 390 | cpuUsage, cpuUsageStr := node.GetCpuUsagePercentage() 391 | memUsage, memUsageStr := node.GetMemoryUsagePercentage() 392 | 393 | m.cpuGraph.LabelHeader = fmt.Sprintf("Name: %v", node.GetNodeName()) 394 | m.cpuGraph.Data = append(m.cpuGraph.Data, cpuUsage) 395 | m.cpuGraph.UpperLimit = 100. 396 | m.cpuGraph.DrawUpperLimit = false 397 | m.cpuGraph.LabelData = fmt.Sprintf("%%Usage: %v", cpuUsageStr) 398 | 399 | m.memGraph.LabelHeader = fmt.Sprintf("Name: %v", node.GetNodeName()) 400 | m.memGraph.Data = append(m.memGraph.Data, memUsage) 401 | m.memGraph.UpperLimit = 100. 402 | m.memGraph.DrawUpperLimit = false 403 | m.memGraph.LabelData = fmt.Sprintf("%%Usage: %v", memUsageStr) 404 | return nil 405 | } 406 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.37.0 h1:69FNAINiZfsEuwH3fKq8QrAAnHz+2m4XL4kVYi5BX0Q= 5 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 6 | contrib.go.opencensus.io/exporter/ocagent v0.2.0 h1:Q/jXnVbliDYozuWJni9452xsSUuo+y8yrioxRgofBhE= 7 | contrib.go.opencensus.io/exporter/ocagent v0.2.0/go.mod h1:0fnkYHF+ORKj7HWzOExKkUHeFX79gXSKUQbpnAM+wzo= 8 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 9 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 10 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 11 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 12 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 13 | git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 14 | github.com/Azure/go-autorest v11.5.2+incompatible h1:NTIEargbhAGNWuT7QEXJ2fqLMFvatupHIscb9FYwVOg= 15 | github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 16 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 17 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 18 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 19 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 20 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 21 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 22 | github.com/census-instrumentation/opencensus-proto v0.0.2-0.20180913191712-f303ae3f8d6a/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 23 | github.com/census-instrumentation/opencensus-proto v0.1.0 h1:VwZ9smxzX8u14/125wHIX7ARV+YhR+L4JADswwxWK0Y= 24 | github.com/census-instrumentation/opencensus-proto v0.1.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 25 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 26 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 27 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 28 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 30 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 31 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 32 | github.com/evanphx/json-patch v4.1.0+incompatible h1:K1MDoo4AZ4wU0GIU/fPmtZg7VpzLjCxu+UwBD1FvwOc= 33 | github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 34 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 35 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 36 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 37 | github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= 38 | github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= 39 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 40 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 41 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 42 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 43 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 44 | github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= 45 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 46 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 47 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 48 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 49 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 50 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 51 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 52 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 53 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 54 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 55 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 56 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 57 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 58 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 59 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 60 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 61 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= 62 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 63 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 64 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 65 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 66 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 67 | github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= 68 | github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 69 | github.com/gophercloud/gophercloud v0.0.0-20190315020455-954aa14363ce h1:voTDkqZ+HmvFeiyUwg5wfaT3co4BsJICJFtQGm3jszA= 70 | github.com/gophercloud/gophercloud v0.0.0-20190315020455-954aa14363ce/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= 71 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 72 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 73 | github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= 74 | github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 75 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 76 | github.com/grpc-ecosystem/grpc-gateway v1.6.2/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 77 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 78 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 79 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 80 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 81 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 82 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 83 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 84 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 85 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 86 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 87 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 88 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 89 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 90 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 91 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 92 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 93 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 94 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 95 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 96 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 97 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 98 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 99 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 100 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 101 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 102 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 103 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 104 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 105 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 106 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 107 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 108 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 109 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 110 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 111 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 112 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 113 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= 114 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= 115 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 116 | github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 117 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 118 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 119 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 120 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 121 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 122 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 123 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 124 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 125 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 126 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 127 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 128 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 129 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 130 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 131 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 132 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 133 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 134 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 135 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 136 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 137 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 138 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 139 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 140 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 141 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 142 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 143 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 144 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 145 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 146 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 147 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 148 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 149 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 150 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 151 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 152 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 153 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 154 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 155 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 156 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 157 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 158 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 159 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 160 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 161 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 162 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 163 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 164 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 165 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 166 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 167 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 168 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 169 | go.opencensus.io v0.17.0/go.mod h1:mp1VrMQxhlqqDpKvH4UcQUa4YwlzNmymAjPrDdfxNpI= 170 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 171 | go.opencensus.io v0.19.1 h1:gPYKQ/GAQYR2ksU+qXNmq3CrOZWT1kkryvW6O0v1acY= 172 | go.opencensus.io v0.19.1/go.mod h1:gug0GbSHa8Pafr0d2urOSgoXHZ6x/RUlaiT0d9pqb4A= 173 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 174 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 175 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 176 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 177 | golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 178 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 179 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= 180 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 181 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 182 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 183 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 184 | golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 185 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 186 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 187 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 188 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 189 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 190 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 191 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 192 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 193 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 194 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 195 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 196 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 197 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977 h1:actzWV6iWn3GLqN8dZjzsB+CLt+gaV2+wsxroxiQI8I= 198 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 199 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 200 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 201 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 202 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= 203 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 204 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 205 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 206 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 207 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 208 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= 209 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 210 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 211 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 212 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 213 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 214 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 215 | golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 216 | golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 217 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 218 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f h1:yCrMx/EeIue0+Qca57bWZS7VX6ymEoypmhWyPhz0NHM= 219 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 221 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 222 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 223 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= 224 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 225 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 226 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 227 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 228 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 229 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 230 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 231 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 232 | golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 233 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 234 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 235 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 236 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 237 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 238 | google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 239 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 240 | google.golang.org/api v0.2.0 h1:B5VXkdjt7K2Gm6fGBC9C9a1OAKJDT95cTqwet+2zib0= 241 | google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU= 242 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 243 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 244 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 245 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 246 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 247 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 248 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 249 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 250 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 251 | google.golang.org/genproto v0.0.0-20181219182458-5a97ab628bfb/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 252 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 253 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= 254 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 255 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 256 | google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 257 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 258 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 259 | google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= 260 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 261 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 262 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 263 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 264 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 265 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 266 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 267 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 268 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 269 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 270 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 271 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 272 | honnef.co/go/tools v0.0.0-20180920025451-e3ad64cb4ed3/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 273 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 274 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 275 | k8s.io/api v0.0.0-20190222213804-5cb15d344471 h1:MzQGt8qWQCR+39kbYRd0uQqsvSidpYqJLFeWiJ9l4OE= 276 | k8s.io/api v0.0.0-20190222213804-5cb15d344471/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= 277 | k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 h1:UYfHH+KEF88OTg+GojQUwFTNxbxwmoktLwutUzR0GPg= 278 | k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 279 | k8s.io/cli-runtime v0.0.0-20190228180923-a9e421a79326 h1:uMY/EHQt0VHYgRMPWwPl/vQ0K0fPlt5zxTEGAdt8pDg= 280 | k8s.io/cli-runtime v0.0.0-20190228180923-a9e421a79326/go.mod h1:qWnH3/b8sp/l7EvlDh7ulDU3UWA4P4N1NFbEEP791tM= 281 | k8s.io/client-go v0.0.0-20190228174230-b40b2a5939e4 h1:aE8wOCKuoRs2aU0OP/Rz8SXiAB0FTTku3VtGhhrkSmc= 282 | k8s.io/client-go v0.0.0-20190228174230-b40b2a5939e4/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 283 | k8s.io/klog v0.2.0 h1:0ElL0OHzF3N+OhoJTL0uca20SxtYt4X4+bzHeqrB83c= 284 | k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 285 | k8s.io/kubernetes v1.13.4 h1:gQqFv/pH8hlbznLXQUsi8s5zqYnv0slmUDl/yVA0EWc= 286 | k8s.io/kubernetes v1.13.4/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= 287 | k8s.io/metrics v0.0.0-20190228180609-34472d076c30 h1:JxQs0/r8IPtVI7WL0BzC6ci1RfO9/CK9YZJtL/qXUvk= 288 | k8s.io/metrics v0.0.0-20190228180609-34472d076c30/go.mod h1:a25VAbm3QT3xiVl1jtoF1ueAKQM149UdZ+L93ePfV3M= 289 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 290 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 291 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 292 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 293 | --------------------------------------------------------------------------------