├── .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 |
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 |
--------------------------------------------------------------------------------