├── .gitignore ├── pkg ├── constants │ └── constants.go ├── app │ └── app.go ├── gui │ ├── state.go │ ├── tabs.go │ ├── cluster.go │ ├── node.go │ ├── gui.go │ ├── info.go │ ├── layout.go │ ├── view_helpers.go │ ├── keybindings.go │ ├── namespace.go │ └── resource.go ├── utils │ └── utils.go └── client │ └── kube.go ├── .github └── workflows │ └── release.yml ├── main.go ├── go.mod ├── LICENSE ├── .goreleaser.yml ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lazykubernetes -------------------------------------------------------------------------------- /pkg/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | type Constants struct { 4 | NotEnoughSpace string 5 | } 6 | 7 | func GetConstants() *Constants{ 8 | return &Constants{ 9 | NotEnoughSpace: "Not enough space to render panels", 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/yolossn/lazykubernetes/pkg/client" 5 | "github.com/yolossn/lazykubernetes/pkg/gui" 6 | ) 7 | 8 | type App struct { 9 | Gui *gui.Gui 10 | } 11 | 12 | func (app *App) Run() error { 13 | return app.Gui.Run() 14 | } 15 | 16 | func NewApp(k8sClient *client.K8s) (*App, error) { 17 | 18 | gui, err := gui.NewGui(k8sClient) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return &App{Gui: gui}, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/gui/state.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | type namespaceState struct { 4 | SelectedLine int 5 | } 6 | 7 | type resourceState struct { 8 | SelectedLine int 9 | TabIndex int 10 | } 11 | 12 | type infoState struct { 13 | SelectedLine int 14 | TabIndex int 15 | } 16 | 17 | type panelStates struct { 18 | Namespace *namespaceState 19 | Resource *resourceState 20 | Info *infoState 21 | } 22 | 23 | func NewPanelStates() *panelStates { 24 | ns := &namespaceState{} 25 | rs := &resourceState{} 26 | is := &infoState{} 27 | return &panelStates{Namespace: ns, Resource: rs, Info: is} 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release with goreleaser 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - v*.*.* 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Unshallow Fetch 14 | run: git fetch --prune --unshallow 15 | - uses: actions/setup-go@v2 16 | with: 17 | go-version: '^1.13.0' 18 | - name: Release via goreleaser 19 | uses: goreleaser/goreleaser-action@master 20 | with: 21 | args: release --rm-dist 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.MYSECRET }} -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/jesseduffield/gocui" 8 | "github.com/yolossn/lazykubernetes/pkg/app" 9 | "github.com/yolossn/lazykubernetes/pkg/client" 10 | ) 11 | 12 | func main() { 13 | // Setup k8sClient 14 | k8sClient, err := client.Newk8s() 15 | if err != nil { 16 | log.Fatal("Couldn't connect to the k8s cluster") 17 | } 18 | 19 | ui, err := app.NewApp(k8sClient) 20 | if err != nil { 21 | log.Fatal("Something went wrong") 22 | } 23 | 24 | err = ui.Run() 25 | if err != nil { 26 | if err == gocui.ErrQuit { 27 | os.Exit(0) 28 | } 29 | log.Fatal("Something went wrong") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yolossn/lazykubernetes 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2 7 | github.com/go-errors/errors v1.1.1 // indirect 8 | github.com/imdario/mergo v0.3.8 // indirect 9 | github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5 10 | github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect 11 | github.com/mattn/go-runewidth v0.0.9 // indirect 12 | github.com/olekukonko/tablewriter v0.0.4 13 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect 14 | k8s.io/api v0.19.0 15 | k8s.io/apimachinery v0.19.0 16 | k8s.io/client-go v0.19.0 17 | sigs.k8s.io/yaml v1.2.0 18 | 19 | ) 20 | -------------------------------------------------------------------------------- /pkg/gui/tabs.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | func getResourceTabs() []string { 4 | return []string{"pod", "job", "deploy", "service", "secret", "configMap"} 5 | } 6 | 7 | func getClusterInfoTabs() []string { 8 | return []string{"info-dump"} 9 | } 10 | 11 | func getNamespaceInfoTabs() []string { 12 | return []string{"description"} 13 | } 14 | 15 | func getPodInfoTabs() []string { 16 | return []string{"description", "logs"} 17 | } 18 | 19 | func getJobInfoTabs() []string { 20 | return []string{"description", "logs", "cron"} 21 | } 22 | 23 | func getDeployInfoTabs() []string { 24 | return []string{"description"} 25 | } 26 | 27 | func getServiceInfoTabs() []string { 28 | return []string{"description"} 29 | } 30 | 31 | func getSecretInfoTabs() []string { 32 | return []string{"description"} 33 | } 34 | 35 | func getConfigMapInfoTabs() []string { 36 | return []string{"description"} 37 | } 38 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/common-nighthawk/go-figure" 7 | "github.com/olekukonko/tablewriter" 8 | ) 9 | 10 | func RenderTable(view io.Writer, data [][]string, header []string) { 11 | // Use table writer to render the data into view 12 | // https://github.com/olekukonko/tablewriter#example-10---set-nowhitespace-and-tablepadding-option 13 | 14 | table := tablewriter.NewWriter(view) 15 | table.SetHeader(header) 16 | table.SetAutoWrapText(false) 17 | table.SetAutoFormatHeaders(true) 18 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 19 | table.SetAlignment(tablewriter.ALIGN_LEFT) 20 | table.SetCenterSeparator("") 21 | table.SetColumnSeparator("") 22 | table.SetRowSeparator("") 23 | table.SetHeaderLine(false) 24 | table.SetBorder(false) 25 | table.SetTablePadding(" ") 26 | table.SetNoWhiteSpace(true) 27 | table.AppendBulk(data) 28 | table.Render() 29 | } 30 | 31 | func GetLazykubernetesArt() string { 32 | myFigure := figure.NewFigure("lazykubernetes", "stop", true) 33 | return myFigure.String() 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 S Santhosh Nagaraj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | # - go generate ./... 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | archives: 13 | - 14 | name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 15 | replacements: 16 | darwin: Darwin 17 | linux: Linux 18 | windows: Windows 19 | amd64: x86_64 20 | format: binary 21 | - 22 | id: homebrew 23 | name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 24 | replacements: 25 | darwin: Darwin 26 | linux: Linux 27 | windows: Windows 28 | amd64: x86_64 29 | format: zip 30 | checksum: 31 | name_template: 'checksums.txt' 32 | snapshot: 33 | name_template: "{{ .Tag }}-next" 34 | changelog: 35 | sort: asc 36 | filters: 37 | exclude: 38 | - '^docs:' 39 | - '^test:' 40 | 41 | brews: 42 | - 43 | name: lazykubernetes 44 | tap: 45 | owner: yolossn 46 | name: homebrew-tap 47 | url_template: "https://github.com/yolossn/lazykubernetes/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 48 | homepage: https://github.com/yolossn/lazykubernetes 49 | test: | 50 | system "#{bin}/lazykubernetes --version" -------------------------------------------------------------------------------- /pkg/gui/cluster.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jesseduffield/gocui" 7 | "github.com/yolossn/lazykubernetes/pkg/utils" 8 | ) 9 | 10 | func (gui *Gui) getClusterInfoView() *gocui.View { 11 | v, _ := gui.g.View("cluster-info") 12 | return v 13 | } 14 | 15 | func (gui *Gui) highlightClusterInfoView() error { 16 | var cview *gocui.View 17 | for { 18 | cview = gui.getClusterInfoView() 19 | if cview != nil { 20 | break 21 | } 22 | } 23 | return gui.onClusterInfoClick(gui.g, cview) 24 | } 25 | 26 | func (gui *Gui) onClusterInfoClick(g *gocui.Gui, v *gocui.View) error { 27 | 28 | if _, err := g.SetCurrentView(v.Name()); err != nil { 29 | return err 30 | } 31 | 32 | infoView := gui.getInfoView() 33 | infoView.Clear() 34 | out := utils.GetLazykubernetesArt() 35 | fmt.Fprintln(infoView, out) 36 | return nil 37 | } 38 | 39 | func (gui *Gui) reRenderClusterInfo() error { 40 | 41 | clusterView := gui.getClusterInfoView() 42 | if clusterView == nil { 43 | return nil 44 | } 45 | 46 | info, err := gui.k8sClient.GetServerInfo() 47 | if err != nil { 48 | clusterView.Clear() 49 | fmt.Fprintf(clusterView, "Health: %s", "🔴") 50 | return nil 51 | } 52 | 53 | clusterView.Clear() 54 | fmt.Fprintf(clusterView, "Version: %s.%s\nplatform: %s\nHealth: %s", info.Major, info.Minor, info.Platform, "🟢") 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/gui/node.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jesseduffield/gocui" 7 | "github.com/yolossn/lazykubernetes/pkg/utils" 8 | duration "k8s.io/apimachinery/pkg/util/duration" 9 | ) 10 | 11 | func (gui *Gui) getNodeInfoView() *gocui.View { 12 | v, _ := gui.g.View("node") 13 | return v 14 | } 15 | 16 | func (gui *Gui) WatchNodes() error { 17 | // Init fetch data 18 | _ = gui.updateNodeData() 19 | // TODO:Handle error 20 | // Wait for namespace events and update data 21 | eventInterface, _ := gui.k8sClient.WatchNodes() 22 | for { 23 | _ = <-eventInterface.ResultChan() 24 | _ = gui.updateNodeData() 25 | } 26 | return nil 27 | } 28 | 29 | func (gui *Gui) updateNodeData() error { 30 | gui.data.nodeMux.Lock() 31 | defer gui.data.nodeMux.Unlock() 32 | nodes, err := gui.k8sClient.ListNode() 33 | if err != nil { 34 | return err 35 | } 36 | gui.data.NodeData = nodes 37 | return nil 38 | } 39 | 40 | func (gui *Gui) getNodeCount() int { 41 | return len(gui.data.NodeData) + 1 42 | } 43 | 44 | func (gui *Gui) reRenderNodeInfo() error { 45 | 46 | nodeView := gui.getNodeInfoView() 47 | if nodeView == nil { 48 | return nil 49 | } 50 | 51 | if gui.getNodeCount() == 0 { 52 | return nil 53 | } 54 | 55 | gui.data.nodeMux.RLock() 56 | defer gui.data.nodeMux.RUnlock() 57 | nodes := gui.data.NodeData 58 | 59 | gui.g.Update(func(*gocui.Gui) error { 60 | nodeView.Clear() 61 | 62 | data := make([][]string, cap(nodes)) 63 | 64 | for x := 0; x < cap(nodes); x++ { 65 | data[x] = make([]string, 4) 66 | } 67 | 68 | headers := []string{"NAME", "STATUS", "VERSION", "AGE"} 69 | for i, n := range nodes { 70 | data[i][0] = n.Name 71 | data[i][1] = n.Status 72 | data[i][2] = n.Version 73 | data[i][3] = duration.HumanDuration(time.Since(n.CreatedAt)) 74 | } 75 | 76 | utils.RenderTable(nodeView, data, headers) 77 | return nil 78 | }) 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

lazykubernetes

2 |

An hazzle-free CUI tool to monitor your kubernetes cluster.

3 |

4 | 5 |

6 |

7 | Supported by FOSS United 8 |

9 | 10 | 11 | ## Why ? 12 | 13 | Memorising commands and checking updates constantly is an issue which anyone who works with kubernetes faces. Inspired by tools like lazydocker, we wanted to make monitoring and working with kubernetes an hazzle-free experience. 14 | 15 | 16 | ## In Action 17 | 18 |

19 | 20 |

21 | 22 | 23 | 24 | ## Installation. 25 | 26 | ### homebrew 27 | > brew install yolossn/tap/lazykubernetes 28 | 29 | ### binary 30 | Find the latest release [here](https://github.com/yolossn/lazykubernetes/releases) 31 | 32 | ## How to use ? 33 | 34 | Just type lazykubernetes in your terminal 35 | 36 | >lazykubernetes 37 | 38 | ## Room for development 39 | 40 | This tool was rapidly prototyped during two days [FossHack](https://fossunited.org/hackathon) and is in alpha version. We are committed to update the tool for better experience. 41 | Have feedback or facing any issues ? 42 | 43 | Please create an [issue](https://github.com/yolossn/lazykubernetes/issues) 44 | 45 | ## Contributors 46 | - [Nityananda](https://github.com/nityanandagohain) 47 | - [Gnanesh](https://github.com/GnaneshKunal) 48 | 49 | ## Credits 50 | - Thanks to [Jesse Duffield](https://github.com/jesseduffield) for his awesome work on lazydocker. 51 | - Logo credit [gopherize.me](gopherize.me) 52 | 53 | ## License 54 | 55 | [MIT License](https://github.com/yolossn/lazykubernetes/blob/master/LICENSE) 56 | -------------------------------------------------------------------------------- /pkg/gui/gui.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/jesseduffield/gocui" 8 | "github.com/yolossn/lazykubernetes/pkg/client" 9 | ) 10 | 11 | var OverlappingEdges = false 12 | 13 | type data struct { 14 | NamespaceData []client.NamespaceInfo 15 | nsMux sync.RWMutex 16 | PodData []client.PodInfo 17 | JobData []client.JobInfo 18 | DeploymentData []client.DeploymentInfo 19 | ServiceData []client.ServiceInfo 20 | SecretData []client.SecretInfo 21 | ConfigMapData []client.ConfigMapInfo 22 | NodeData []client.NodeInfo 23 | rsMux sync.RWMutex 24 | nodeMux sync.RWMutex 25 | } 26 | 27 | type Gui struct { 28 | g *gocui.Gui 29 | k8sClient *client.K8s 30 | data *data 31 | panelStates *panelStates 32 | } 33 | 34 | func NewGui(k8sClient *client.K8s) (*Gui, error) { 35 | 36 | // NewData 37 | data := &data{} 38 | panelStates := NewPanelStates() 39 | return &Gui{k8sClient: k8sClient, data: data, panelStates: panelStates}, nil 40 | } 41 | 42 | func (gui *Gui) Run() error { 43 | g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges) 44 | if err != nil { 45 | return err 46 | } 47 | defer g.Close() 48 | 49 | gui.g = g 50 | 51 | // shows border select 52 | g.Highlight = true 53 | 54 | // Set ColorScheme 55 | g.SelFgColor = gocui.ColorGreen 56 | g.BgColor = gocui.ColorBlack 57 | g.FgColor = gocui.ColorDefault 58 | 59 | // Allow mouse events 60 | g.Mouse = true 61 | // Set Manager 62 | g.SetManager(gocui.ManagerFunc(gui.layout)) 63 | 64 | // Set Keybindings 65 | err = gui.SetKeybindings(g) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | // Update Namespace data 71 | go gui.WatchNamespace() 72 | go gui.WatchPods() 73 | go gui.WatchNodes() 74 | 75 | // reRender 76 | go gui.goEvery(time.Second, gui.reRenderNamespace) 77 | go gui.goEvery(time.Second, gui.reRenderResource) 78 | go gui.goEvery(time.Second, gui.reRenderClusterInfo) 79 | go gui.goEvery(time.Second, gui.reRenderNodeInfo) 80 | 81 | // highlight cluster view on start 82 | go gui.highlightClusterInfoView() 83 | 84 | err = g.MainLoop() 85 | return err 86 | } 87 | -------------------------------------------------------------------------------- /pkg/gui/info.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/jesseduffield/gocui" 7 | ) 8 | 9 | func (gui *Gui) getInfoView() *gocui.View { 10 | v, _ := gui.g.View("info") 11 | return v 12 | } 13 | 14 | func (gui *Gui) onInfoClick(g *gocui.Gui, v *gocui.View) error { 15 | if _, err := g.SetCurrentView(v.Name()); err != nil { 16 | return err 17 | } 18 | return nil 19 | } 20 | 21 | func (gui *Gui) onInfoTabClick(tabIndex int) error { 22 | 23 | infoView := gui.getInfoView() 24 | infoView.TabIndex = tabIndex 25 | gui.panelStates.Info.TabIndex = tabIndex 26 | 27 | return nil 28 | } 29 | 30 | // The following scroll functions are modified version of code from lazydocker 31 | // https://github.com/jesseduffield/lazydocker/blob/fa6460b8ab3486b7e84c3a7d4c64fbd8e3f4be21/pkg/gui/main_panel.go 32 | func (gui *Gui) scrollLeftInfo(g *gocui.Gui, v *gocui.View) error { 33 | infoView := gui.getInfoView() 34 | ox, oy := infoView.Origin() 35 | newOx := int(math.Max(0, float64(ox-20))) 36 | 37 | return infoView.SetOrigin(newOx, oy) 38 | } 39 | 40 | func (gui *Gui) scrollRightInfo(g *gocui.Gui, v *gocui.View) error { 41 | infoView := gui.getInfoView() 42 | ox, oy := infoView.Origin() 43 | 44 | content := infoView.ViewBufferLines() 45 | var largestNumberOfCharacters int 46 | for _, txt := range content { 47 | if len(txt) > largestNumberOfCharacters { 48 | largestNumberOfCharacters = len(txt) 49 | } 50 | } 51 | 52 | sizeX, _ := infoView.Size() 53 | if ox+sizeX >= largestNumberOfCharacters { 54 | return nil 55 | } 56 | 57 | return infoView.SetOrigin(ox+20, oy) 58 | } 59 | 60 | func (gui *Gui) scrollUpInfo(g *gocui.Gui, v *gocui.View) error { 61 | mainView := gui.getInfoView() 62 | mainView.Autoscroll = false 63 | ox, oy := mainView.Origin() 64 | newOy := int(math.Max(0, float64(oy-20))) 65 | return mainView.SetOrigin(ox, newOy) 66 | } 67 | 68 | func (gui *Gui) scrollDownInfo(g *gocui.Gui, v *gocui.View) error { 69 | mainView := gui.getInfoView() 70 | mainView.Autoscroll = false 71 | ox, oy := mainView.Origin() 72 | 73 | reservedLines := 0 74 | _, sizeY := mainView.Size() 75 | reservedLines = sizeY 76 | 77 | totalLines := mainView.ViewLinesHeight() 78 | if oy+reservedLines >= totalLines { 79 | return nil 80 | } 81 | 82 | return mainView.SetOrigin(ox, oy+20) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/gui/layout.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "github.com/jesseduffield/gocui" 5 | 6 | "github.com/yolossn/lazykubernetes/pkg/constants" 7 | ) 8 | 9 | func (gui *Gui) layout(g *gocui.Gui) error { 10 | 11 | termWidth, termHeight := g.Size() 12 | 13 | // minimum size 14 | minimumHeight := 9 15 | minimumWidth := 10 16 | 17 | if termHeight < minimumHeight || termWidth < minimumWidth { 18 | v, err := g.SetView("limit", 0, 0, termWidth-1, termHeight-1, 0) 19 | if err != nil { 20 | if err.Error() != "unknown view" { 21 | return err 22 | } 23 | v.Title = constants.GetConstants().NotEnoughSpace 24 | v.Wrap = true 25 | _, _ = g.SetViewOnTop("limit") 26 | } 27 | return nil 28 | } 29 | 30 | _, _ = g.SetViewOnBottom("limit") 31 | g.DeleteView("limit") 32 | 33 | unitHeight := termHeight / 10 34 | 35 | leftColumnWidth := termWidth / 3 36 | 37 | if clusterInfoView, err := g.SetView("cluster-info", 0, 0, leftColumnWidth, unitHeight, gocui.BOTTOM|gocui.RIGHT); err != nil { 38 | if err.Error() != "unknown view" { 39 | return err 40 | } 41 | clusterInfoView.Title = "cluster-info" 42 | clusterInfoView.Highlight = true 43 | } 44 | 45 | namespaceViewHeight := termHeight - unitHeight - 8 46 | namespaceView, err := g.SetViewBeneath("namespace", "cluster-info", namespaceViewHeight) 47 | if err != nil { 48 | if err.Error() != "unknown view" { 49 | return err 50 | } 51 | namespaceView.Title = "namespace" 52 | namespaceView.Highlight = true 53 | } 54 | 55 | nodeViewHeight := termHeight - unitHeight - namespaceViewHeight - 1 56 | nodeView, err := g.SetViewBeneath("node", "namespace", nodeViewHeight) 57 | if err != nil { 58 | if err.Error() != "unknown view" { 59 | return err 60 | } 61 | nodeView.Title = "node" 62 | nodeView.Highlight = true 63 | } 64 | 65 | resourceViewHeight := unitHeight * 2 66 | if resourceView, err := g.SetView("resource", leftColumnWidth+1, 0, termWidth-1, resourceViewHeight, 0); err != nil { 67 | if err.Error() != "unknown view" { 68 | return err 69 | } 70 | resourceView.Tabs = getResourceTabs() 71 | resourceView.Highlight = true 72 | } 73 | 74 | infoViewHeight := termHeight - unitHeight*2 - 1 75 | _, err = g.SetViewBeneath("info", "resource", infoViewHeight) 76 | if err != nil { 77 | if err.Error() != "unknown view" { 78 | return err 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/gui/view_helpers.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jesseduffield/gocui" 7 | ) 8 | 9 | // This function is a modified version of 10 | // https://github.com/jesseduffield/lazydocker/blob/fa6460b8ab3486b7e84c3a7d4c64fbd8e3f4be21/pkg/gui/gui.go#L227 11 | func (gui *Gui) goEvery(interval time.Duration, function func() error) { 12 | _ = function() // time.Tick doesn't run immediately so we'll do that here // TODO: maybe change 13 | go func() { 14 | ticker := time.NewTicker(interval) 15 | defer ticker.Stop() 16 | for range ticker.C { 17 | _ = function() 18 | } 19 | }() 20 | } 21 | 22 | // This function is a modified version of 23 | // https://github.com/jesseduffield/lazydocker/blob/a14e6400cbbd7e2aa9ef22166b085b0678f9ca3a/pkg/gui/view_helpers.go#L361 24 | func (gui *Gui) FindSelectedLine(v *gocui.View, itemCount int) int { 25 | _, cy := v.Cursor() 26 | _, oy := v.Origin() 27 | 28 | selectedLine := cy - oy 29 | 30 | if selectedLine < 0 { 31 | return 0 32 | } 33 | 34 | if selectedLine > itemCount-1 { 35 | return itemCount - 1 36 | } 37 | return selectedLine 38 | } 39 | 40 | // This function was copied from 41 | // https://github.com/jesseduffield/lazydocker/blob/a14e6400cbbd7e2aa9ef22166b085b0678f9ca3a/pkg/gui/view_helpers.go#L319 42 | func (gui *Gui) changeSelectedLine(line *int, total int, up bool) { 43 | if up { 44 | if *line == -1 || *line == 0 { 45 | return 46 | } 47 | 48 | *line -= 1 49 | } else { 50 | if *line == -1 || *line == total-1 { 51 | return 52 | } 53 | 54 | *line += 1 55 | } 56 | } 57 | 58 | // This function was copied from 59 | // https://github.com/jesseduffield/lazydocker/blob/a14e6400cbbd7e2aa9ef22166b085b0678f9ca3a/pkg/gui/view_helpers.go#L168 60 | func (gui *Gui) focusPoint(selectedX int, selectedY int, lineCount int, v *gocui.View) error { 61 | if selectedY < 0 || selectedY > lineCount { 62 | return nil 63 | } 64 | ox, oy := v.Origin() 65 | originalOy := oy 66 | cx, cy := v.Cursor() 67 | originalCy := cy 68 | _, height := v.Size() 69 | 70 | ly := Max(height-1, 0) 71 | 72 | windowStart := oy 73 | windowEnd := oy + ly 74 | 75 | if selectedY < windowStart { 76 | oy = Max(oy-(windowStart-selectedY), 0) 77 | } else if selectedY > windowEnd { 78 | oy += (selectedY - windowEnd) 79 | } 80 | 81 | if windowEnd > lineCount-1 { 82 | shiftAmount := (windowEnd - (lineCount - 1)) 83 | oy = Max(oy-shiftAmount, 0) 84 | } 85 | 86 | if originalOy != oy { 87 | _ = v.SetOrigin(ox, oy) 88 | } 89 | 90 | cy = selectedY - oy 91 | if originalCy != cy { 92 | _ = v.SetCursor(cx, selectedY-oy) 93 | } 94 | return nil 95 | } 96 | 97 | func Max(x, y int) int { 98 | if x > y { 99 | return x 100 | } 101 | return y 102 | } 103 | -------------------------------------------------------------------------------- /pkg/gui/keybindings.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import "github.com/jesseduffield/gocui" 4 | 5 | func (gui *Gui) SetKeybindings(g *gocui.Gui) error { 6 | 7 | // Exit Keybinding Ctrl+C 8 | if err := g.SetKeybinding("", nil, gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { 9 | return err 10 | } 11 | 12 | // MouseClick 13 | if err := g.SetKeybinding("cluster-info", nil, gocui.MouseLeft, gocui.ModNone, gui.onClusterInfoClick); err != nil { 14 | return err 15 | } 16 | 17 | if err := g.SetKeybinding("namespace", nil, gocui.MouseLeft, gocui.ModNone, gui.onNamespaceClick); err != nil { 18 | return err 19 | } 20 | 21 | if err := g.SetKeybinding("resource", nil, gocui.MouseLeft, gocui.ModNone, gui.onResourceClick); err != nil { 22 | return err 23 | } 24 | 25 | if err := g.SetKeybinding("info", nil, gocui.MouseLeft, gocui.ModNone, gui.onInfoClick); err != nil { 26 | return err 27 | } 28 | 29 | // Arrow keys 30 | if err := g.SetKeybinding("namespace", nil, gocui.KeyArrowDown, gocui.ModNone, gui.handleNSKeyUp); err != nil { 31 | return err 32 | } 33 | 34 | if err := g.SetKeybinding("namespace", nil, gocui.KeyArrowUp, gocui.ModNone, gui.handleNSKeyDown); err != nil { 35 | return err 36 | } 37 | 38 | if err := g.SetKeybinding("namespace", nil, gocui.MouseWheelDown, gocui.ModNone, gui.handleNSKeyUp); err != nil { 39 | return err 40 | } 41 | 42 | if err := g.SetKeybinding("namespace", nil, gocui.MouseWheelUp, gocui.ModNone, gui.handleNSKeyDown); err != nil { 43 | return err 44 | } 45 | 46 | if err := g.SetKeybinding("resource", nil, gocui.KeyArrowDown, gocui.ModNone, gui.handleResourceKeyUp); err != nil { 47 | return err 48 | } 49 | 50 | if err := g.SetKeybinding("resource", nil, gocui.KeyArrowUp, gocui.ModNone, gui.handleResourceKeyDown); err != nil { 51 | return err 52 | } 53 | 54 | // Info Scroll 55 | if err := g.SetKeybinding("info", nil, gocui.KeyArrowLeft, gocui.ModNone, gui.scrollLeftInfo); err != nil { 56 | return err 57 | } 58 | if err := g.SetKeybinding("info", nil, gocui.KeyArrowRight, gocui.ModNone, gui.scrollRightInfo); err != nil { 59 | return err 60 | } 61 | 62 | if err := g.SetKeybinding("info", nil, gocui.KeyArrowUp, gocui.ModNone, gui.scrollUpInfo); err != nil { 63 | return err 64 | } 65 | if err := g.SetKeybinding("info", nil, gocui.KeyArrowDown, gocui.ModNone, gui.scrollDownInfo); err != nil { 66 | return err 67 | } 68 | 69 | if err := g.SetKeybinding("info", nil, gocui.MouseWheelUp, gocui.ModNone, gui.scrollUpInfo); err != nil { 70 | return err 71 | } 72 | if err := g.SetKeybinding("info", nil, gocui.MouseWheelDown, gocui.ModNone, gui.scrollDownInfo); err != nil { 73 | return err 74 | } 75 | 76 | // Tab click 77 | if err := g.SetTabClickBinding("resource", gui.onResourceTabClick); err != nil { 78 | return err 79 | } 80 | 81 | if err := g.SetTabClickBinding("info", gui.onInfoTabClick); err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func quit(g *gocui.Gui, v *gocui.View) error { 89 | return gocui.ErrQuit 90 | } 91 | -------------------------------------------------------------------------------- /pkg/gui/namespace.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/yolossn/lazykubernetes/pkg/utils" 8 | 9 | "github.com/jesseduffield/gocui" 10 | duration "k8s.io/apimachinery/pkg/util/duration" 11 | "sigs.k8s.io/yaml" 12 | ) 13 | 14 | func (gui *Gui) getNamespaceView() *gocui.View { 15 | v, _ := gui.g.View("namespace") 16 | return v 17 | } 18 | 19 | func (gui *Gui) onNamespaceClick(g *gocui.Gui, v *gocui.View) error { 20 | 21 | if _, err := g.SetCurrentView(v.Name()); err != nil { 22 | return err 23 | } 24 | 25 | infoView := gui.getInfoView() 26 | infoView.Tabs = getNamespaceInfoTabs() 27 | 28 | // Find selectedLine 29 | gui.panelStates.Namespace.SelectedLine = gui.FindSelectedLine(v, gui.getNSCount()) 30 | fmt.Fprintln(infoView, gui.panelStates.Namespace.SelectedLine) 31 | err := gui.handleNSSelect(v) 32 | if err != nil { 33 | return err 34 | } 35 | return gui.reRenderResource() 36 | } 37 | 38 | func (gui *Gui) handleNSSelect(v *gocui.View) error { 39 | infoView := gui.getInfoView() 40 | ns := gui.getCurrentNS() 41 | 42 | err := gui.focusPoint(0, gui.panelStates.Namespace.SelectedLine, gui.getNSCount(), v) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | if ns == "" { 48 | infoView.Clear() 49 | art := utils.GetLazykubernetesArt() 50 | fmt.Fprintln(infoView, art) 51 | return gui.reRenderResource() 52 | } 53 | data, err := gui.k8sClient.GetNamespace(ns) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | infoView.Clear() 59 | output, err := yaml.Marshal(data) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | fmt.Fprintln(infoView, string(output)) 65 | return gui.reRenderResource() 66 | } 67 | 68 | func (gui *Gui) getCurrentNS() string { 69 | if gui.getNSCount() >= 0 { 70 | if gui.panelStates.Namespace.SelectedLine == 0 { 71 | return "" 72 | } 73 | if gui.panelStates.Namespace.SelectedLine >= gui.getNSCount() { 74 | gui.panelStates.Namespace.SelectedLine = gui.getNSCount() - 1 75 | } 76 | return gui.data.NamespaceData[gui.panelStates.Namespace.SelectedLine-1].Name 77 | } 78 | return "" 79 | } 80 | 81 | func (gui *Gui) getNSCount() int { 82 | return len(gui.data.NamespaceData) + 1 83 | } 84 | 85 | func (gui *Gui) WatchNamespace() error { 86 | // Init fetch data 87 | _ = gui.updateNSData() 88 | // TODO:Handle error 89 | // Wait for namespace events and update data 90 | eventInterface, _ := gui.k8sClient.WatchNamespace() 91 | for { 92 | _ = <-eventInterface.ResultChan() 93 | _ = gui.updateNSData() 94 | } 95 | return nil 96 | } 97 | 98 | func (gui *Gui) updateNSData() error { 99 | gui.data.nsMux.Lock() 100 | defer gui.data.nsMux.Unlock() 101 | ns, err := gui.k8sClient.ListNamespace() 102 | if err != nil { 103 | return err 104 | } 105 | gui.data.NamespaceData = ns 106 | return nil 107 | } 108 | 109 | func (gui *Gui) reRenderNamespace() error { 110 | nsView := gui.getNamespaceView() 111 | if nsView == nil { 112 | return nil 113 | } 114 | 115 | if gui.getNSCount() == 0 { 116 | return nil 117 | } 118 | 119 | gui.data.nsMux.RLock() 120 | defer gui.data.nsMux.RUnlock() 121 | ns := gui.data.NamespaceData 122 | 123 | gui.g.Update(func(*gocui.Gui) error { 124 | nsView.Clear() 125 | 126 | // make data for namespace tablewriter 127 | data := make([][]string, cap(ns)) 128 | 129 | for x := 0; x < cap(ns); x++ { 130 | data[x] = make([]string, 3) 131 | } 132 | headers := []string{"NAME", "STATUS", "AGE"} 133 | for i, n := range ns { 134 | data[i][0] = n.Name 135 | data[i][1] = n.Status 136 | data[i][2] = duration.HumanDuration(time.Since(n.CreatedAt)) 137 | } 138 | 139 | utils.RenderTable(nsView, data, headers) 140 | 141 | return nil 142 | }) 143 | 144 | return nil 145 | } 146 | 147 | func (gui *Gui) handleNSKeyUp(g *gocui.Gui, v *gocui.View) error { 148 | gui.changeSelectedLine(&gui.panelStates.Namespace.SelectedLine, gui.getNSCount(), false) 149 | return gui.handleNSSelect(v) 150 | } 151 | 152 | func (gui *Gui) handleNSKeyDown(g *gocui.Gui, v *gocui.View) error { 153 | gui.changeSelectedLine(&gui.panelStates.Namespace.SelectedLine, gui.getNSCount(), true) 154 | return gui.handleNSSelect(v) 155 | } 156 | -------------------------------------------------------------------------------- /pkg/client/kube.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | "time" 7 | 8 | v1Apps "k8s.io/api/apps/v1" 9 | v1Batch "k8s.io/api/batch/v1" 10 | v1Core "k8s.io/api/core/v1" 11 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/version" 13 | "k8s.io/apimachinery/pkg/watch" 14 | "k8s.io/client-go/kubernetes" 15 | restclient "k8s.io/client-go/rest" 16 | "k8s.io/client-go/tools/clientcmd" 17 | "k8s.io/client-go/util/homedir" 18 | ) 19 | 20 | type K8s struct { 21 | client *kubernetes.Clientset 22 | } 23 | 24 | func Newk8s() (*K8s, error) { 25 | var kubeconfig string 26 | if home := homedir.HomeDir(); home != "" { 27 | kubeconfig = filepath.Join(home, ".kube", "config") 28 | } 29 | // } else { 30 | // kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") 31 | // } 32 | 33 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) 34 | if err != nil { 35 | return nil, err 36 | } 37 | clientset, err := kubernetes.NewForConfig(config) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return &K8s{clientset}, nil 43 | } 44 | 45 | func (k *K8s) GetServerInfo() (*version.Info, error) { 46 | return k.client.DiscoveryClient.ServerVersion() 47 | } 48 | 49 | type NamespaceInfo struct { 50 | Name string 51 | Status string 52 | CreatedAt time.Time 53 | } 54 | 55 | func (k *K8s) ListNamespace() ([]NamespaceInfo, error) { 56 | ctx := context.TODO() 57 | opts := v1.ListOptions{} 58 | list, _ := k.client.CoreV1().Namespaces().List(ctx, opts) 59 | ns := []NamespaceInfo{} 60 | for _, item := range list.Items { 61 | n := NamespaceInfo{ 62 | Name: item.ObjectMeta.Name, 63 | Status: string(item.Status.Phase), 64 | CreatedAt: item.ObjectMeta.CreationTimestamp.Time, 65 | } 66 | ns = append(ns, n) 67 | } 68 | return ns, nil 69 | } 70 | 71 | // TODO: Verify timeout and handle it 72 | func (k *K8s) WatchNamespace() (watch.Interface, error) { 73 | ctx := context.TODO() 74 | opts := v1.ListOptions{} 75 | wi, err := k.client.CoreV1().Namespaces().Watch(ctx, opts) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return wi, nil 80 | } 81 | 82 | func (k *K8s) GetNamespace(ns string) (*v1Core.Namespace, error) { 83 | ctx := context.TODO() 84 | opts := v1.GetOptions{} 85 | return k.client.CoreV1().Namespaces().Get(ctx, ns, opts) 86 | } 87 | 88 | func (k *K8s) WatchPods(namespace string) (watch.Interface, error) { 89 | ctx := context.TODO() 90 | opts := v1.ListOptions{} 91 | 92 | wi, err := k.client.CoreV1().Pods(namespace).Watch(ctx, opts) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return wi, nil 97 | } 98 | 99 | type PodInfo struct { 100 | Name string 101 | Namespace string 102 | Status string 103 | ReadyContainers int32 104 | TotalContainers int32 105 | Restarts int32 106 | CreatedAt time.Time 107 | } 108 | 109 | func (k *K8s) ListPods(namespace string) ([]PodInfo, error) { 110 | 111 | ctx := context.TODO() 112 | opts := v1.ListOptions{} 113 | pods, _ := k.client.CoreV1().Pods(namespace).List(ctx, opts) 114 | podList := []PodInfo{} 115 | for _, pod := range pods.Items { 116 | restarts := int32(0) 117 | 118 | ready := int32(0) 119 | totalContianers := int32(len(pod.Status.ContainerStatuses)) 120 | for _, container := range pod.Status.ContainerStatuses { 121 | if container.RestartCount > restarts { 122 | restarts = container.RestartCount 123 | } 124 | if container.State.Running != nil { 125 | ready++ 126 | } 127 | } 128 | 129 | p := PodInfo{ 130 | Name: pod.Name, 131 | Namespace: pod.Namespace, 132 | Status: string(pod.Status.Phase), 133 | Restarts: restarts, 134 | ReadyContainers: ready, 135 | TotalContainers: totalContianers, 136 | CreatedAt: pod.ObjectMeta.CreationTimestamp.Time, 137 | } 138 | podList = append(podList, p) 139 | } 140 | return podList, nil 141 | } 142 | 143 | func (k *K8s) DescribePod(ns string, podname string) (*v1Core.Pod, error) { 144 | ctx := context.TODO() 145 | opts := v1.GetOptions{} 146 | 147 | out, err := k.client.CoreV1().Pods(ns).Get(ctx, podname, opts) 148 | if err != nil { 149 | return nil, err 150 | } 151 | return out, nil 152 | } 153 | 154 | func (k *K8s) DescribeJob(ns string, jobname string) (*v1Batch.Job, error) { 155 | ctx := context.TODO() 156 | opts := v1.GetOptions{} 157 | out, err := k.client.BatchV1().Jobs(ns).Get(ctx, jobname, opts) 158 | if err != nil { 159 | return nil, err 160 | } 161 | return out, nil 162 | } 163 | 164 | func (k *K8s) DescribeService(ns string, servicename string) (*v1Core.Service, error) { 165 | ctx := context.TODO() 166 | opts := v1.GetOptions{} 167 | out, err := k.client.CoreV1().Services(ns).Get(ctx, servicename, opts) 168 | if err != nil { 169 | return nil, err 170 | } 171 | return out, nil 172 | } 173 | 174 | func (k *K8s) DescribeDeployment(ns string, deploymentname string) (*v1Apps.Deployment, error) { 175 | ctx := context.TODO() 176 | opts := v1.GetOptions{} 177 | out, err := k.client.AppsV1().Deployments(ns).Get(ctx, deploymentname, opts) 178 | if err != nil { 179 | return nil, err 180 | } 181 | return out, nil 182 | } 183 | 184 | func (k *K8s) DescribeSecret(ns string, secretName string) (*v1Core.Secret, error) { 185 | ctx := context.TODO() 186 | opts := v1.GetOptions{} 187 | out, err := k.client.CoreV1().Secrets(ns).Get(ctx, secretName, opts) 188 | if err != nil { 189 | return nil, err 190 | } 191 | return out, nil 192 | } 193 | 194 | func (k *K8s) DescribeConfigMap(ns string, configMapName string) (*v1Core.ConfigMap, error) { 195 | ctx := context.TODO() 196 | opts := v1.GetOptions{} 197 | out, err := k.client.CoreV1().ConfigMaps(ns).Get(ctx, configMapName, opts) 198 | if err != nil { 199 | return nil, err 200 | } 201 | return out, nil 202 | } 203 | 204 | 205 | func (k *K8s) StreamPodLogs(ns string, podname string) *restclient.Request { 206 | opts := &v1Core.PodLogOptions{Follow: true} 207 | request := k.client.CoreV1().Pods(ns).GetLogs(podname, opts) 208 | return request 209 | } 210 | 211 | type JobInfo struct { 212 | Name string 213 | Namespace string 214 | Active int32 215 | Succeeded int32 216 | Failed int32 217 | Age string 218 | CompletedAt time.Time 219 | CreatedAt time.Time 220 | } 221 | 222 | func (k *K8s) ListJobs(namespace string) ([]JobInfo, error) { 223 | ctx := context.TODO() 224 | opts := v1.ListOptions{} 225 | jobs, _ := k.client.BatchV1().Jobs(namespace).List(ctx, opts) 226 | jobList := []JobInfo{} 227 | for _, job := range jobs.Items { 228 | 229 | p := JobInfo{ 230 | Name: job.Name, 231 | Namespace: job.Namespace, 232 | Active: job.Status.Active, 233 | Succeeded: job.Status.Succeeded, 234 | Failed: job.Status.Failed, 235 | CompletedAt: job.Status.CompletionTime.Time, 236 | CreatedAt: job.ObjectMeta.CreationTimestamp.Time, 237 | } 238 | jobList = append(jobList, p) 239 | } 240 | return jobList, nil 241 | } 242 | 243 | type DeploymentInfo struct { 244 | Name string 245 | Namespace string 246 | Available int32 247 | ReadyReplicas int32 248 | Replicas int32 249 | UpdatedReplicas int32 250 | CreatedAt time.Time 251 | } 252 | 253 | func (k *K8s) ListDeployments(namespace string) ([]DeploymentInfo, error) { 254 | ctx := context.TODO() 255 | opts := v1.ListOptions{} 256 | deployments, _ := k.client.AppsV1().Deployments(namespace).List(ctx, opts) 257 | deploymentList := []DeploymentInfo{} 258 | for _, deployment := range deployments.Items { 259 | d := DeploymentInfo{ 260 | Name: deployment.Name, 261 | Namespace: deployment.Namespace, 262 | Available: deployment.Status.AvailableReplicas, 263 | ReadyReplicas: deployment.Status.ReadyReplicas, 264 | Replicas: deployment.Status.Replicas, 265 | UpdatedReplicas: deployment.Status.UpdatedReplicas, 266 | CreatedAt: deployment.ObjectMeta.CreationTimestamp.Time, 267 | } 268 | deploymentList = append(deploymentList, d) 269 | } 270 | return deploymentList, nil 271 | } 272 | 273 | type ServiceInfoPort struct { 274 | Name string 275 | Protocol string 276 | Port int32 277 | TargetPort int32 278 | } 279 | 280 | type ServiceInfo struct { 281 | Name string 282 | Namespace string 283 | Type string 284 | ClusterIP string 285 | ExternalIP string 286 | Ports []ServiceInfoPort 287 | CreatedAt time.Time 288 | } 289 | 290 | func (k *K8s) ListServices(namespace string) ([]ServiceInfo, error) { 291 | ctx := context.TODO() 292 | opts := v1.ListOptions{} 293 | services, _ := k.client.CoreV1().Services(namespace).List(ctx, opts) 294 | serviceList := []ServiceInfo{} 295 | for _, service := range services.Items { 296 | serviceInfoPorts := []ServiceInfoPort{} 297 | for _, sPort := range service.Spec.Ports { 298 | serviceInfoPort := ServiceInfoPort{ 299 | Name: sPort.Name, 300 | Protocol: string(sPort.Protocol), 301 | Port: int32(sPort.Port), 302 | TargetPort: sPort.TargetPort.IntVal, 303 | } 304 | serviceInfoPorts = append(serviceInfoPorts, serviceInfoPort) 305 | } 306 | s := ServiceInfo{ 307 | Name: service.ObjectMeta.Name, 308 | Namespace: service.ObjectMeta.Namespace, 309 | Type: string(service.Spec.Type), 310 | ClusterIP: service.Spec.ClusterIP, 311 | ExternalIP: "", // TODO: Implement external ip 312 | Ports: serviceInfoPorts, 313 | CreatedAt: service.ObjectMeta.CreationTimestamp.Time, 314 | } 315 | serviceList = append(serviceList, s) 316 | } 317 | return serviceList, nil 318 | } 319 | 320 | type StatefulsetInfo struct { 321 | Name string 322 | Namespace string 323 | CurrentReplicas int32 324 | ReadyReplicas int32 325 | Replicas int32 326 | UpdatedReplicas int32 327 | CreatedAt time.Time 328 | } 329 | 330 | func (k *K8s) ListStatefulsets(namespace string) ([]StatefulsetInfo, error) { 331 | ctx := context.TODO() 332 | opts := v1.ListOptions{} 333 | statefulsets, _ := k.client.AppsV1().StatefulSets(namespace).List(ctx, opts) 334 | statefulsetList := []StatefulsetInfo{} 335 | // https: //kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#statefulsetstatus-v1-apps 336 | for _, statefulset := range statefulsets.Items { 337 | s := StatefulsetInfo{ 338 | Name: statefulset.Name, 339 | Namespace: statefulset.Namespace, 340 | CurrentReplicas: statefulset.Status.CurrentReplicas, 341 | ReadyReplicas: statefulset.Status.ReadyReplicas, 342 | Replicas: statefulset.Status.Replicas, 343 | UpdatedReplicas: statefulset.Status.UpdatedReplicas, 344 | CreatedAt: statefulset.ObjectMeta.CreationTimestamp.Time, 345 | } 346 | statefulsetList = append(statefulsetList, s) 347 | } 348 | return statefulsetList, nil 349 | } 350 | 351 | type SecretInfo struct { 352 | Name string 353 | Namespace string 354 | Type string 355 | Data int32 356 | CreatedAt time.Time 357 | } 358 | 359 | func (k *K8s) ListSecrets(namespace string) ([]SecretInfo, error) { 360 | ctx := context.TODO() 361 | opts := v1.ListOptions{} 362 | secrets, _ := k.client.CoreV1().Secrets(namespace).List(ctx, opts) 363 | secretList := []SecretInfo{} 364 | // https: //kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#secret-v1-core 365 | for _, secret := range secrets.Items { 366 | s := SecretInfo{ 367 | Name: secret.Name, 368 | Namespace: secret.Namespace, 369 | Type: string(secret.Type), 370 | Data: int32(len(secret.Data)), 371 | CreatedAt: secret.ObjectMeta.CreationTimestamp.Time, 372 | } 373 | secretList = append(secretList, s) 374 | } 375 | return secretList, nil 376 | } 377 | 378 | type ConfigMapInfo struct { 379 | Name string 380 | Namespace string 381 | Data int32 382 | CreatedAt time.Time 383 | } 384 | 385 | func (k *K8s) ListConfigMap(namespace string) ([]ConfigMapInfo, error) { 386 | ctx := context.TODO() 387 | opts := v1.ListOptions{} 388 | configmaps, _ := k.client.CoreV1().ConfigMaps(namespace).List(ctx, opts) 389 | configmapList := []ConfigMapInfo{} 390 | // https: //kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#configmap-v1-core 391 | for _, configmap := range configmaps.Items { 392 | s := ConfigMapInfo{ 393 | Name: configmap.Name, 394 | Namespace: configmap.Namespace, 395 | Data: int32(len(configmap.Data)), 396 | CreatedAt: configmap.ObjectMeta.CreationTimestamp.Time, 397 | } 398 | configmapList = append(configmapList, s) 399 | } 400 | return configmapList, nil 401 | } 402 | 403 | type NodeInfo struct { 404 | Name string 405 | Status string 406 | Version string 407 | CreatedAt time.Time 408 | } 409 | 410 | func (k *K8s) ListNode() ([]NodeInfo, error) { 411 | ctx := context.TODO() 412 | opts := v1.ListOptions{} 413 | nodes, _ := k.client.CoreV1().Nodes().List(ctx, opts) 414 | nodeList := []NodeInfo{} 415 | // https: //kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#node-v1-core 416 | for _, node := range nodes.Items { 417 | var status string 418 | if len(node.Status.Conditions) > 0 { 419 | status = string(node.Status.Conditions[len(node.Status.Conditions)-1].Type) 420 | } 421 | 422 | n := NodeInfo{ 423 | Name: node.Name, 424 | Status: status, 425 | Version: node.Status.NodeInfo.KubeletVersion, 426 | CreatedAt: node.ObjectMeta.CreationTimestamp.Time, 427 | } 428 | nodeList = append(nodeList, n) 429 | } 430 | return nodeList, nil 431 | } 432 | 433 | func (k *K8s) WatchNodes() (watch.Interface, error) { 434 | ctx := context.TODO() 435 | opts := v1.ListOptions{} 436 | wi, err := k.client.CoreV1().Nodes().Watch(ctx, opts) 437 | if err != nil { 438 | return nil, err 439 | } 440 | return wi, nil 441 | } 442 | -------------------------------------------------------------------------------- /pkg/gui/resource.go: -------------------------------------------------------------------------------- 1 | package gui 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | "github.com/yolossn/lazykubernetes/pkg/utils" 10 | duration "k8s.io/apimachinery/pkg/util/duration" 11 | 12 | "github.com/jesseduffield/gocui" 13 | "sigs.k8s.io/yaml" 14 | ) 15 | 16 | func (gui *Gui) getResourceView() *gocui.View { 17 | v, _ := gui.g.View("resource") 18 | return v 19 | } 20 | 21 | func (gui *Gui) onResourceClick(g *gocui.Gui, v *gocui.View) error { 22 | if _, err := g.SetCurrentView(v.Name()); err != nil { 23 | return err 24 | } 25 | 26 | infoView := gui.getInfoView() 27 | // render pod 28 | switch getResourceTabs()[gui.panelStates.Resource.TabIndex] { 29 | case "pod": 30 | infoView.Tabs = getPodInfoTabs() 31 | gui.panelStates.Resource.SelectedLine = gui.FindSelectedLine(v, len(gui.data.PodData)) 32 | return gui.handlePodSelect(v) 33 | case "job": 34 | infoView.Tabs = getJobInfoTabs() 35 | gui.panelStates.Resource.SelectedLine = gui.FindSelectedLine(v, len(gui.data.JobData)) 36 | return gui.handleJobSelect(v) 37 | case "deploy": 38 | infoView.Tabs = getDeployInfoTabs() 39 | gui.panelStates.Resource.SelectedLine = gui.FindSelectedLine(v, len(gui.data.DeploymentData)) 40 | return gui.handleDeploymentSelect(v) 41 | case "service": 42 | infoView.Tabs = getServiceInfoTabs() 43 | gui.panelStates.Resource.SelectedLine = gui.FindSelectedLine(v, len(gui.data.ServiceData)) 44 | return gui.handleServiceSelect(v) 45 | case "configMap": 46 | infoView.Tabs = getConfigMapInfoTabs() 47 | gui.panelStates.Resource.SelectedLine = gui.FindSelectedLine(v, len(gui.data.ConfigMapData)) 48 | return gui.handleConfigMapSelect(v) 49 | case "secret": 50 | infoView.Tabs = getSecretInfoTabs() 51 | gui.panelStates.Resource.SelectedLine = gui.FindSelectedLine(v, len(gui.data.SecretData)) 52 | return gui.handleSecretSelect(v) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func (gui *Gui) handleJobSelect(v *gocui.View) error { 59 | // Find Selected Job 60 | jobSelected := gui.panelStates.Resource.SelectedLine 61 | 62 | if jobSelected < 0 || jobSelected > len(gui.data.JobData) { 63 | return nil 64 | } 65 | 66 | job := gui.data.JobData[jobSelected] 67 | 68 | infoView := gui.getInfoView() 69 | 70 | err := gui.focusPoint(0, gui.panelStates.Resource.SelectedLine, len(gui.data.JobData), v) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | // Find the tab in info panel 76 | switch getJobInfoTabs()[gui.panelStates.Info.TabIndex] { 77 | case "description": 78 | infoView.Clear() 79 | data, err := gui.k8sClient.DescribeJob(job.Namespace, job.Name) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | output, err := yaml.Marshal(data) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | fmt.Fprintln(infoView, string(output)) 90 | } 91 | return nil 92 | } 93 | 94 | func (gui *Gui) handleDeploymentSelect(v *gocui.View) error { 95 | // Find Selected Deployment 96 | deploymentSelected := gui.panelStates.Resource.SelectedLine 97 | 98 | if deploymentSelected < 0 || deploymentSelected > len(gui.data.DeploymentData) { 99 | return nil 100 | } 101 | 102 | deployment := gui.data.DeploymentData[deploymentSelected] 103 | 104 | infoView := gui.getInfoView() 105 | 106 | err := gui.focusPoint(0, gui.panelStates.Resource.SelectedLine, len(gui.data.DeploymentData), v) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | // Find the tab in info panel 112 | switch getDeployInfoTabs()[gui.panelStates.Info.TabIndex] { 113 | case "description": 114 | infoView.Clear() 115 | data, err := gui.k8sClient.DescribeDeployment(deployment.Namespace, deployment.Name) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | output, err := yaml.Marshal(data) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | fmt.Fprintln(infoView, string(output)) 126 | } 127 | return nil 128 | } 129 | 130 | func (gui *Gui) handleConfigMapSelect(v *gocui.View) error { 131 | // Find Selected ConfigMap 132 | configMapSelected := gui.panelStates.Resource.SelectedLine 133 | 134 | if configMapSelected < 0 || configMapSelected > len(gui.data.ConfigMapData) { 135 | return nil 136 | } 137 | 138 | configMap := gui.data.ConfigMapData[configMapSelected] 139 | 140 | infoView := gui.getInfoView() 141 | 142 | err := gui.focusPoint(0, gui.panelStates.Resource.SelectedLine, len(gui.data.ConfigMapData), v) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | // Find the tab in info panel 148 | switch getConfigMapInfoTabs()[gui.panelStates.Info.TabIndex] { 149 | case "description": 150 | infoView.Clear() 151 | data, err := gui.k8sClient.DescribeConfigMap(configMap.Namespace, configMap.Name) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | output, err := yaml.Marshal(data) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | fmt.Fprintln(infoView, string(output)) 162 | } 163 | return nil 164 | } 165 | 166 | func (gui *Gui) handleSecretSelect(v *gocui.View) error { 167 | // Find Selected Secret 168 | secretSelected := gui.panelStates.Resource.SelectedLine 169 | 170 | if secretSelected < 0 || secretSelected > len(gui.data.SecretData) { 171 | return nil 172 | } 173 | 174 | secret := gui.data.SecretData[secretSelected] 175 | 176 | infoView := gui.getInfoView() 177 | 178 | err := gui.focusPoint(0, gui.panelStates.Resource.SelectedLine, len(gui.data.SecretData), v) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | // Find the tab in info panel 184 | switch getSecretInfoTabs()[gui.panelStates.Info.TabIndex] { 185 | case "description": 186 | infoView.Clear() 187 | data, err := gui.k8sClient.DescribeSecret(secret.Namespace, secret.Name) 188 | if err != nil { 189 | return err 190 | } 191 | 192 | output, err := yaml.Marshal(data) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | fmt.Fprintln(infoView, string(output)) 198 | } 199 | return nil 200 | } 201 | 202 | func (gui *Gui) handleServiceSelect(v *gocui.View) error { 203 | // Find Selected Service 204 | serviceSelected := gui.panelStates.Resource.SelectedLine 205 | 206 | if serviceSelected < 0 || serviceSelected > len(gui.data.ServiceData) { 207 | return nil 208 | } 209 | 210 | service := gui.data.ServiceData[serviceSelected] 211 | 212 | infoView := gui.getInfoView() 213 | 214 | err := gui.focusPoint(0, gui.panelStates.Resource.SelectedLine, len(gui.data.ServiceData), v) 215 | if err != nil { 216 | return err 217 | } 218 | 219 | // Find the tab in info panel 220 | switch getServiceInfoTabs()[gui.panelStates.Info.TabIndex] { 221 | case "description": 222 | infoView.Clear() 223 | data, err := gui.k8sClient.DescribeService(service.Namespace, service.Name) 224 | if err != nil { 225 | return err 226 | } 227 | 228 | output, err := yaml.Marshal(data) 229 | if err != nil { 230 | return err 231 | } 232 | 233 | fmt.Fprintln(infoView, string(output)) 234 | } 235 | return nil 236 | } 237 | 238 | func (gui *Gui) handlePodSelect(v *gocui.View) error { 239 | 240 | // Find Selected Pod 241 | podSelected := gui.panelStates.Resource.SelectedLine 242 | if podSelected < 0 || podSelected > len(gui.data.PodData) { 243 | return nil 244 | } 245 | 246 | pod := gui.data.PodData[podSelected] 247 | 248 | infoView := gui.getInfoView() 249 | 250 | err := gui.focusPoint(0, gui.panelStates.Resource.SelectedLine, len(gui.data.PodData), v) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | // Find the tab in info panel 256 | switch getPodInfoTabs()[gui.panelStates.Info.TabIndex] { 257 | case "logs": 258 | infoView.Clear() 259 | gui.g.Update(func(*gocui.Gui) error { 260 | ctx := context.TODO() 261 | req := gui.k8sClient.StreamPodLogs(pod.Namespace, pod.Name) 262 | readCloser, err := req.Stream(ctx) 263 | if err != nil { 264 | fmt.Println(err) 265 | } 266 | infoView.Clear() 267 | infoView.Autoscroll = true 268 | go func() { 269 | for { 270 | io.Copy(infoView, readCloser) 271 | } 272 | }() 273 | return nil 274 | }) 275 | case "description": 276 | infoView.Clear() 277 | data, err := gui.k8sClient.DescribePod(pod.Namespace, pod.Name) 278 | if err != nil { 279 | return err 280 | } 281 | 282 | output, err := yaml.Marshal(data) 283 | if err != nil { 284 | return err 285 | } 286 | 287 | fmt.Fprintln(infoView, string(output)) 288 | } 289 | return nil 290 | } 291 | 292 | func (gui *Gui) onResourceTabClick(tabIndex int) error { 293 | 294 | resourceView := gui.getResourceView() 295 | resourceView.TabIndex = tabIndex 296 | 297 | gui.panelStates.Resource.TabIndex = tabIndex 298 | infoView := gui.getInfoView() 299 | switch gui.getCurrentResourceTab() { 300 | case "pod": 301 | infoView.Tabs = getPodInfoTabs() 302 | case "job": 303 | infoView.Tabs = getJobInfoTabs() 304 | case "deploy": 305 | infoView.Tabs = getDeployInfoTabs() 306 | case "service": 307 | infoView.Tabs = getServiceInfoTabs() 308 | case "secret": 309 | infoView.Tabs = getSecretInfoTabs() 310 | case "configMap": 311 | infoView.Tabs = getConfigMapInfoTabs() 312 | } 313 | 314 | return gui.reRenderResource() 315 | } 316 | 317 | func (gui *Gui) reRenderResource() error { 318 | rsView := gui.getResourceView() 319 | if rsView == nil { 320 | return nil 321 | } 322 | 323 | ns := gui.getCurrentNS() 324 | switch getResourceTabs()[gui.panelStates.Resource.TabIndex] { 325 | case "pod": 326 | gui.setPods(ns) 327 | return gui.renderPods() 328 | case "job": 329 | gui.setJobs(ns) 330 | return gui.renderJobs() 331 | case "deploy": 332 | gui.setDeployments(ns) 333 | return gui.renderDeployments() 334 | case "service": 335 | gui.setServices(ns) 336 | return gui.renderServices() 337 | case "secret": 338 | gui.setSecrets(ns) 339 | return gui.renderSecrets() 340 | case "configMap": 341 | gui.setConfigMaps(ns) 342 | return gui.renderConfigMaps() 343 | } 344 | 345 | return nil 346 | } 347 | 348 | func (gui *Gui) getCurrentResourceTab() string { 349 | return getResourceTabs()[gui.panelStates.Resource.TabIndex] 350 | } 351 | 352 | func (gui *Gui) setServices(namespace string) { 353 | gui.data.rsMux.Lock() 354 | defer gui.data.rsMux.Unlock() 355 | 356 | services, err := gui.k8sClient.ListServices(namespace) 357 | if err != nil { 358 | 359 | } 360 | gui.data.ServiceData = services 361 | } 362 | 363 | func (gui *Gui) setPods(namespace string) { 364 | gui.data.rsMux.Lock() 365 | defer gui.data.rsMux.Unlock() 366 | 367 | pods, err := gui.k8sClient.ListPods(namespace) 368 | if err != nil { 369 | return 370 | } 371 | gui.data.PodData = pods 372 | } 373 | 374 | func (gui *Gui) setJobs(namespace string) { 375 | gui.data.rsMux.Lock() 376 | defer gui.data.rsMux.Unlock() 377 | 378 | jobs, err := gui.k8sClient.ListJobs(namespace) 379 | if err != nil { 380 | return 381 | } 382 | gui.data.JobData = jobs 383 | } 384 | 385 | func (gui *Gui) setDeployments(namespace string) { 386 | gui.data.rsMux.Lock() 387 | defer gui.data.rsMux.Unlock() 388 | 389 | deployments, err := gui.k8sClient.ListDeployments(namespace) 390 | if err != nil { 391 | 392 | } 393 | gui.data.DeploymentData = deployments 394 | } 395 | 396 | func (gui *Gui) setConfigMaps(namespace string) { 397 | gui.data.rsMux.Lock() 398 | defer gui.data.rsMux.Unlock() 399 | 400 | configmaps, err := gui.k8sClient.ListConfigMap(namespace) 401 | if err != nil { 402 | return 403 | } 404 | gui.data.ConfigMapData = configmaps 405 | } 406 | 407 | func (gui *Gui) setSecrets(namespace string) { 408 | gui.data.rsMux.Lock() 409 | defer gui.data.rsMux.Unlock() 410 | 411 | secrets, err := gui.k8sClient.ListSecrets(namespace) 412 | if err != nil { 413 | return 414 | } 415 | gui.data.SecretData = secrets 416 | } 417 | 418 | func (gui *Gui) renderServices() error { 419 | rsView := gui.getResourceView() 420 | if rsView == nil { 421 | return nil 422 | } 423 | 424 | gui.data.rsMux.RLock() 425 | defer gui.data.rsMux.RUnlock() 426 | 427 | rsView.Clear() 428 | services := gui.data.ServiceData 429 | data := make([][]string, cap(services)) 430 | for i := 0; i < cap(services); i++ { 431 | data[i] = make([]string, 6) 432 | } 433 | // headers := []string{"NAME", "TYPE", "CLUSTER-IP", "EXTERNAL-IP", "PORT(s)", "AGE"} 434 | headers := []string{"NAME", "TYPE", "CLUSTER-IP", "PORT(s)", "AGE"} 435 | 436 | for i, service := range services { 437 | data[i][0] = service.Name 438 | data[i][1] = service.Type 439 | data[i][2] = service.ClusterIP 440 | // data[i][3] = service.ExternalIP 441 | s := "" 442 | for _, portInfo := range service.Ports { 443 | port_string := fmt.Sprintf("%v/%s", portInfo.Port, portInfo.Protocol) 444 | s = fmt.Sprintf("%s %s", s, port_string) 445 | } 446 | data[i][3] = fmt.Sprintf("%v", s) 447 | data[i][4] = duration.HumanDuration(time.Since(service.CreatedAt)) 448 | } 449 | 450 | utils.RenderTable(rsView, data, headers) 451 | 452 | return nil 453 | } 454 | 455 | func (gui *Gui) renderSecrets() error { 456 | rsView := gui.getResourceView() 457 | if rsView == nil { 458 | return nil 459 | } 460 | 461 | gui.data.rsMux.RLock() 462 | defer gui.data.rsMux.RUnlock() 463 | 464 | rsView.Clear() 465 | secrets := gui.data.SecretData 466 | data := make([][]string, cap(secrets)) 467 | 468 | for i := 0; i < cap(secrets); i++ { 469 | data[i] = make([]string, 4) 470 | } 471 | headers := []string{"NAME", "TYPE", "DATA", "AGE"} 472 | 473 | for i, secret := range secrets { 474 | data[i][0] = secret.Name 475 | data[i][1] = secret.Type 476 | data[i][2] = fmt.Sprintf("%v", secret.Data) 477 | data[i][3] = duration.HumanDuration(time.Since(secret.CreatedAt)) 478 | } 479 | 480 | utils.RenderTable(rsView, data, headers) 481 | 482 | return nil 483 | } 484 | 485 | func (gui *Gui) renderConfigMaps() error { 486 | rsView := gui.getResourceView() 487 | if rsView == nil { 488 | return nil 489 | } 490 | 491 | gui.data.rsMux.RLock() 492 | defer gui.data.rsMux.RUnlock() 493 | 494 | rsView.Clear() 495 | configmaps := gui.data.ConfigMapData 496 | data := make([][]string, cap(configmaps)) 497 | 498 | for i := 0; i < cap(configmaps); i++ { 499 | data[i] = make([]string, 3) 500 | } 501 | headers := []string{"NAME", "DATA", "AGE"} 502 | 503 | for i, configmap := range configmaps { 504 | data[i][0] = configmap.Name 505 | data[i][1] = fmt.Sprintf("%v", configmap.Data) 506 | data[i][2] = duration.HumanDuration(time.Since(configmap.CreatedAt)) 507 | } 508 | 509 | utils.RenderTable(rsView, data, headers) 510 | 511 | return nil 512 | } 513 | 514 | func (gui *Gui) renderDeployments() error { 515 | rsView := gui.getResourceView() 516 | if rsView == nil { 517 | return nil 518 | } 519 | 520 | gui.data.rsMux.RLock() 521 | defer gui.data.rsMux.RUnlock() 522 | 523 | rsView.Clear() 524 | deployments := gui.data.DeploymentData 525 | data := make([][]string, cap(deployments)) 526 | 527 | for i := 0; i < cap(deployments); i++ { 528 | data[i] = make([]string, 5) 529 | } 530 | headers := []string{"NAME", "READY", "UP-TO-DATE", "AVAILABLE", "AGE"} 531 | for i, deployment := range deployments { 532 | data[i][0] = deployment.Name 533 | data[i][1] = fmt.Sprintf("%v/%v", deployment.ReadyReplicas, deployment.Replicas) 534 | data[i][2] = fmt.Sprintf("%v", deployment.UpdatedReplicas) 535 | data[i][3] = fmt.Sprintf("%v", deployment.Available) 536 | data[i][4] = duration.HumanDuration(time.Since(deployment.CreatedAt)) 537 | } 538 | 539 | utils.RenderTable(rsView, data, headers) 540 | 541 | return nil 542 | } 543 | 544 | func (gui *Gui) renderJobs() error { 545 | rsView := gui.getResourceView() 546 | if rsView == nil { 547 | return nil 548 | } 549 | 550 | gui.data.rsMux.RLock() 551 | defer gui.data.rsMux.RUnlock() 552 | 553 | rsView.Clear() 554 | jobs := gui.data.JobData 555 | 556 | data := make([][]string, cap(jobs)) 557 | 558 | for x := 0; x < cap(jobs); x++ { 559 | data[x] = make([]string, 4) 560 | } 561 | headers := []string{"NAME", "COMPLETIONS", "DURATION", "AGE"} 562 | for i, job := range jobs { 563 | data[i][0] = job.Name 564 | data[i][1] = fmt.Sprintf("%v/%v", job.Succeeded, job.Succeeded+job.Failed) 565 | data[i][2] = duration.HumanDuration(job.CompletedAt.Sub(job.CreatedAt)) 566 | data[i][3] = duration.HumanDuration(time.Since(job.CreatedAt)) 567 | } 568 | 569 | utils.RenderTable(rsView, data, headers) 570 | 571 | return nil 572 | } 573 | 574 | func (gui *Gui) renderPods() error { 575 | 576 | rsView := gui.getResourceView() 577 | if rsView == nil { 578 | return nil 579 | } 580 | 581 | gui.data.rsMux.RLock() 582 | defer gui.data.rsMux.RUnlock() 583 | 584 | rsView.Clear() 585 | pods := gui.data.PodData 586 | 587 | data := make([][]string, cap(pods)) 588 | 589 | for x := 0; x < cap(pods); x++ { 590 | data[x] = make([]string, 5) 591 | } 592 | headers := []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE"} 593 | for i, pod := range pods { 594 | data[i][0] = pod.Name 595 | data[i][1] = fmt.Sprintf("%v/%v", pod.ReadyContainers, pod.TotalContainers) 596 | data[i][2] = pod.Status 597 | data[i][3] = fmt.Sprintf("%v", pod.Restarts) 598 | data[i][4] = duration.HumanDuration(time.Since(pod.CreatedAt)) 599 | } 600 | 601 | utils.RenderTable(rsView, data, headers) 602 | 603 | return nil 604 | } 605 | 606 | func (gui *Gui) WatchPods() error { 607 | _ = gui.reRenderResource() 608 | // TODO: Handle error 609 | event, _ := gui.k8sClient.WatchPods("") 610 | for { 611 | _ = <-event.ResultChan() 612 | if gui.getCurrentResourceTab() != "pod" { 613 | continue 614 | } 615 | _ = gui.reRenderNamespace() 616 | } 617 | } 618 | 619 | func (gui *Gui) handleResourceKeyUp(g *gocui.Gui, v *gocui.View) error { 620 | switch gui.getCurrentResourceTab() { 621 | case "pod": 622 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.PodData), false) 623 | return gui.handlePodSelect(v) 624 | case "job": 625 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.JobData), false) 626 | return gui.handleJobSelect(v) 627 | case "deploy": 628 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.DeploymentData), true) 629 | return gui.handleDeploymentSelect(v) 630 | case "service": 631 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.ServiceData), false) 632 | return gui.handleServiceSelect(v) 633 | case "secret": 634 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.SecretData), false) 635 | return gui.handleSecretSelect(v) 636 | case "configMap": 637 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.ConfigMapData), false) 638 | return gui.handleConfigMapSelect(v) 639 | } 640 | return nil 641 | } 642 | 643 | func (gui *Gui) handleResourceKeyDown(g *gocui.Gui, v *gocui.View) error { 644 | switch gui.getCurrentResourceTab() { 645 | case "pod": 646 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.PodData), true) 647 | return gui.handlePodSelect(v) 648 | case "job": 649 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.JobData), true) 650 | return gui.handleJobSelect(v) 651 | case "deploy": 652 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.DeploymentData), true) 653 | return gui.handleDeploymentSelect(v) 654 | case "service": 655 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.ServiceData), false) 656 | return gui.handleServiceSelect(v) 657 | case "secret": 658 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.SecretData), false) 659 | return gui.handleSecretSelect(v) 660 | case "configMap": 661 | gui.changeSelectedLine(&gui.panelStates.Resource.SelectedLine, len(gui.data.ConfigMapData), false) 662 | return gui.handleConfigMapSelect(v) 663 | } 664 | return nil 665 | } 666 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= 9 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 10 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= 15 | github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= 16 | github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= 17 | github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= 18 | github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= 19 | github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= 20 | github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 21 | github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= 22 | github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= 23 | github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= 24 | github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= 25 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 26 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 27 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 28 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 29 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 30 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 31 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 32 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 33 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 34 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 35 | github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2 h1:tjT4Jp4gxECvsJcYpAMtW2I3YqzBTPuB67OejxXs86s= 36 | github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 37 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 39 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 41 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 42 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 43 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 44 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 45 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 46 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 47 | github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 48 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 49 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 50 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 51 | github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg= 52 | github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= 53 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 54 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 55 | github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= 56 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 57 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 58 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 59 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 60 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 61 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= 62 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 63 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 64 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 65 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 66 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 67 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 68 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 69 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 70 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 71 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 72 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 73 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 74 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 75 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 76 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 77 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 78 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 79 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 80 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 81 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 82 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 83 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 84 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 85 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 86 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 87 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 88 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 89 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= 90 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 91 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 92 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 93 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 94 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 95 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 96 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 97 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 98 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 99 | github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= 100 | github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= 101 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 102 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 103 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 104 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 105 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 106 | github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= 107 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 108 | github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= 109 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 110 | github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5 h1:tE0w3tuL/bj1o5VMhjjE0ep6i7Fva+RYjKcMFcniJEY= 111 | github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw= 112 | github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E= 113 | github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4= 114 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 115 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= 116 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 117 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 118 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 119 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 120 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 121 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 122 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 123 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 124 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 125 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 126 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 127 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 128 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 129 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 130 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 131 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 132 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 133 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 134 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 135 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 136 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 137 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 138 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 139 | github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= 140 | github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 141 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 142 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 143 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 144 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 145 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 146 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 147 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 148 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 149 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 150 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 151 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 152 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 153 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 154 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 155 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 156 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 157 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 158 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 159 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 160 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 161 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 162 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 163 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 164 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 165 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 166 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 167 | golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 168 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 169 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 170 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 171 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 172 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 173 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 174 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 175 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 176 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 177 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 178 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 179 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 180 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 181 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 182 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 183 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 184 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 185 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 186 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 187 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 188 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 189 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 190 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 191 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 192 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 193 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 194 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 195 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 196 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 197 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 198 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 199 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 200 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 201 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 202 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= 203 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 204 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= 205 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 206 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 207 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 208 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= 209 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 210 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 211 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 212 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 213 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 214 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 215 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 216 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 217 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 218 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 219 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 226 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 227 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 228 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= 230 | golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 232 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 233 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 234 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 235 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 236 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 237 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 238 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 239 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= 240 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 241 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 242 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 243 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 244 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 245 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 246 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 247 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 248 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 249 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 250 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 251 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 252 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 253 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 254 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 255 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 256 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 257 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 258 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 259 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 260 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 261 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 262 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 263 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 264 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 265 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 266 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 267 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 268 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 269 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 270 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 271 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 272 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 273 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= 274 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 275 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 276 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 277 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 278 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 279 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 280 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 281 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 282 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 283 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 284 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 285 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 286 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 287 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 288 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 289 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 290 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 291 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 292 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 293 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 294 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 295 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 296 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 297 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 298 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 299 | google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= 300 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 301 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 302 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 303 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 304 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 305 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 306 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 307 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 308 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 309 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 310 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 311 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 312 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 313 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 314 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 315 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 316 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 317 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 318 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 319 | k8s.io/api v0.19.0 h1:XyrFIJqTYZJ2DU7FBE/bSPz7b1HvbVBuBf07oeo6eTc= 320 | k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= 321 | k8s.io/apimachinery v0.19.0 h1:gjKnAda/HZp5k4xQYjL0K/Yb66IvNqjthCb03QlKpaQ= 322 | k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= 323 | k8s.io/client-go v0.19.0 h1:1+0E0zfWFIWeyRhQYWzimJOyAk2UT7TiARaLNwJCf7k= 324 | k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= 325 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 326 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 327 | k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= 328 | k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= 329 | k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= 330 | k8s.io/utils v0.0.0-20200729134348-d5654de09c73 h1:uJmqzgNWG7XyClnU/mLPBWwfKKF1K8Hf8whTseBgJcg= 331 | k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 332 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 333 | sigs.k8s.io/structured-merge-diff/v4 v4.0.1 h1:YXTMot5Qz/X1iBRJhAt+vI+HVttY0WkSqqhKxQ0xVbA= 334 | sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= 335 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 336 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= 337 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 338 | --------------------------------------------------------------------------------