From e68e8bf145d7c1fb78ccdc632a85917bb771dc6e Mon Sep 17 00:00:00 2001 From: Paul Varache Date: Fri, 10 Jan 2020 14:31:35 +0000 Subject: [PATCH 01/39] Add instructions for scoop --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 08190740..46ed1caa 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,12 @@ K9s is available on Linux, OSX and Windows platforms. yay -S k9s-bin ``` +* Via [Scoop](https://scoop.sh) for Windows + + ```shell + scoop install k9s + ``` + * Building from source K9s was built using go 1.13 or above. In order to build K9 from source you must: 1. Clone the repo From dec03ceb912cf6a46d4da9282b03d94c63861e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20F=2E=20R=C3=B8dseth?= Date: Sun, 26 Jan 2020 12:59:56 +0100 Subject: [PATCH 02/39] Update documentation --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dfec5388..ce8a8dff 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ Wanna discuss K9s features with your fellow `K9sers` or simply show your support ## Installation -K9s is available on Linux, OSX and Windows platforms. +K9s is available on Linux, macOS and Windows platforms. * Binaries for Linux, Windows and Mac are available as tarballs in the [release](https://github.com/derailed/k9s/releases) page. -* Via Homebrew or LinuxBrew for OSX and Linux +* Via Homebrew or LinuxBrew for macOS and Linux ```shell brew install derailed/k9s/k9s @@ -47,12 +47,10 @@ K9s is available on Linux, OSX and Windows platforms. sudo port install k9s ``` -* Archlinux (AUR) - - K9s is available in the Arch User Repository under the name [k9s-bin](https://aur.archlinux.org/packages/k9s-bin/), you can install it with your favorite AUR helper like so: +* On Arch Linux ```shell - yay -S k9s-bin + pacman -S k9s ``` * Building from source From e07c968432d627cdfbea8ec94cda19e9b5f1da59 Mon Sep 17 00:00:00 2001 From: derailed Date: Fri, 7 Feb 2020 07:06:57 -0800 Subject: [PATCH 03/39] fix #529 --- internal/config/alias.go | 2 +- internal/render/np.go | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/config/alias.go b/internal/config/alias.go index 25f76bbb..ff8075da 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -147,7 +147,7 @@ func (a *Aliases) LoadAliases(path string) error { return nil } - var aa *Aliases + var aa Aliases if err := yaml.Unmarshal(f, &aa); err != nil { return err } diff --git a/internal/render/np.go b/internal/render/np.go index e1c1b881..bdedf401 100644 --- a/internal/render/np.go +++ b/internal/render/np.go @@ -111,7 +111,14 @@ func egress(ee []v1beta1.NetworkPolicyEgressRule) (string, string, string) { func portsToStr(pp []v1beta1.NetworkPolicyPort) string { ports := make([]string, 0, len(pp)) for _, p := range pp { - ports = append(ports, string(*p.Protocol)+":"+p.Port.String()) + proto, port := NAValue, NAValue + if p.Protocol != nil { + proto = string(*p.Protocol) + } + if p.Port != nil { + port = p.Port.String() + } + ports = append(ports, proto+":"+port) } return strings.Join(ports, ",") } From 5b707c656efd374ccffd7e7888d845c16d52f511 Mon Sep 17 00:00:00 2001 From: derailed Date: Fri, 7 Feb 2020 07:21:21 -0800 Subject: [PATCH 04/39] fix #528 --- internal/view/browser.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/view/browser.go b/internal/view/browser.go index b83f9bfb..89cb8126 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -310,6 +310,9 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { auth, err := b.App().factory.Client().CanI(ns, b.GVR(), client.MonitorAccess) if !auth { + if err == nil { + err = fmt.Errorf("current user can't access namespace %s", ns) + } b.App().Flash().Err(err) return nil } From 7d56789a200b965d68fac28497555abae5b75973 Mon Sep 17 00:00:00 2001 From: Joscha Alisch Date: Fri, 7 Feb 2020 20:22:55 +0100 Subject: [PATCH 05/39] Allow setting info message on command execution --- internal/view/actions.go | 2 +- internal/view/browser.go | 2 +- internal/view/exec.go | 14 ++++++++------ internal/view/pod.go | 3 ++- internal/view/xray.go | 2 +- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/internal/view/actions.go b/internal/view/actions.go index c15ed77c..4d91f2ee 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -129,7 +129,7 @@ func execCmd(r Runner, bin string, bg bool, args ...string) ui.ActionHandler { return nil } } - if run(true, r.App(), bin, bg, aa...) { + if run(true, r.App(), bin, bg, "", aa...) { r.App().Flash().Info("Plugin command launched successfully!") } else { r.App().Flash().Info("Plugin command failed!") diff --git a/internal/view/browser.go b/internal/view/browser.go index b83f9bfb..5999840b 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -292,7 +292,7 @@ func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey { if cfg := b.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } - if !runK(true, b.app, append(args, n)...) { + if !runK(true, b.app, "", append(args, n)...) { b.app.Flash().Err(errors.New("Edit exec failed")) } } diff --git a/internal/view/exec.go b/internal/view/exec.go index dc5e39e2..f9407d1e 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -13,22 +13,22 @@ import ( "github.com/rs/zerolog/log" ) -func runK(clear bool, app *App, args ...string) bool { +func runK(clear bool, app *App, info string, args ...string) bool { bin, err := exec.LookPath("kubectl") if err != nil { log.Error().Msgf("Unable to find kubectl command in path %v", err) return false } - return run(clear, app, bin, false, args...) + return run(clear, app, bin, false, info, args...) } -func run(clear bool, app *App, bin string, bg bool, args ...string) bool { +func run(clear bool, app *App, bin string, bg bool, info string, args ...string) bool { app.Halt() defer app.Resume() return app.Suspend(func() { - if err := execute(clear, bin, bg, args...); err != nil { + if err := execute(clear, bin, bg, info, args...); err != nil { app.Flash().Errf("Command exited: %v", err) } }) @@ -41,10 +41,10 @@ func edit(clear bool, app *App, args ...string) bool { return false } - return run(clear, app, bin, false, args...) + return run(clear, app, bin, false, "", args...) } -func execute(clear bool, bin string, bg bool, args ...string) error { +func execute(clear bool, bin string, bg bool, info string, args ...string) error { if clear { clearScreen() } @@ -71,6 +71,8 @@ func execute(clear bool, bin string, bg bool, args ...string) error { err = cmd.Start() } else { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + + _, _ = cmd.Stdout.Write([]byte(info)) err = cmd.Run() } log.Debug().Msgf("Command returned error?? %v", err) diff --git a/internal/view/pod.go b/internal/view/pod.go index cc03de98..73e508f4 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -176,7 +176,8 @@ func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, func shellIn(a *App, path, co string) { args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) log.Debug().Msgf("Shell args %v", args) - if !runK(true, a, args...) { + + if !runK(true, a, "", args...) { a.Flash().Err(errors.New("Shell exec failed")) } } diff --git a/internal/view/xray.go b/internal/view/xray.go index d16ce4bc..3e809d95 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -345,7 +345,7 @@ func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey { if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } - if !runK(true, x.app, append(args, n)...) { + if !runK(true, x.app, "", append(args, n)...) { x.app.Flash().Err(errors.New("Edit exec failed")) } } From 7d0febf4b0b74604bd168095b4c472481e2e55a6 Mon Sep 17 00:00:00 2001 From: Joscha Alisch Date: Fri, 7 Feb 2020 20:24:28 +0100 Subject: [PATCH 06/39] Print pod on shell access --- internal/view/pod.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/view/pod.go b/internal/view/pod.go index 73e508f4..7a2bdd2f 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -177,7 +177,7 @@ func shellIn(a *App, path, co string) { args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) log.Debug().Msgf("Shell args %v", args) - if !runK(true, a, "", args...) { + if !runK(true, a, fmt.Sprintf("Pod: %s\n\n", path, co), args...) { a.Flash().Err(errors.New("Shell exec failed")) } } From fc2de70dea0ab87240836b66beb2d9daface1bde Mon Sep 17 00:00:00 2001 From: Joscha Alisch Date: Fri, 7 Feb 2020 20:25:24 +0100 Subject: [PATCH 07/39] Print container name on shell access as well --- internal/view/pod.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/view/pod.go b/internal/view/pod.go index 7a2bdd2f..92a6a5f4 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -177,7 +177,7 @@ func shellIn(a *App, path, co string) { args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) log.Debug().Msgf("Shell args %v", args) - if !runK(true, a, fmt.Sprintf("Pod: %s\n\n", path, co), args...) { + if !runK(true, a, fmt.Sprintf("Pod: %s | Container: %s\n\n", path, co), args...) { a.Flash().Err(errors.New("Shell exec failed")) } } From 4b3ed1674908eeec2bfae61cf345c0f94bf8556e Mon Sep 17 00:00:00 2001 From: Joscha Alisch Date: Fri, 7 Feb 2020 20:25:55 +0100 Subject: [PATCH 08/39] Print container name even if there is only one --- internal/view/pod.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/view/pod.go b/internal/view/pod.go index 92a6a5f4..ef1d98a0 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -125,7 +125,7 @@ func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } if len(cc) == 1 { - p.shellIn(sel, "") + p.shellIn(sel, cc[0]) return nil } picker := NewPicker() From 0e68e92e9891f106d8b45ac9dac99c5a60c7114f Mon Sep 17 00:00:00 2001 From: Joscha Alisch Date: Fri, 7 Feb 2020 20:26:37 +0100 Subject: [PATCH 09/39] Colour the output --- go.mod | 1 + go.sum | 36 +++++++++++++++++++++++++++--------- internal/view/pod.go | 4 +++- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 898850ec..5af8ab15 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/derailed/tview v0.3.3 github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect + github.com/fatih/color v1.6.0 github.com/fsnotify/fsnotify v1.4.7 github.com/gdamore/tcell v1.3.0 github.com/ghodss/yaml v1.0.0 diff --git a/go.sum b/go.sum index b37542d1..16167445 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,7 @@ github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcy github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc= github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/hcsshim v0.0.0-20190417211021-672e52e9209d/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -56,6 +57,7 @@ github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdko github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Rican7/retry v0.1.0/go.mod h1:FgOROf8P5bebcC1DS0PdOQiqGUridaZvikzUmkFW6gg= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -78,13 +80,18 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/blang/semver v3.5.0+incompatible h1:CGxCgetQ64DKk7rdZ++Vfnb1+ogGNnB17OJKJXD2Cfs= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bugsnag/bugsnag-go v1.5.0 h1:tP8hiPv1pGGW3LA6LKy5lW6WG+y9J2xWUdPd3WC452k= github.com/bugsnag/bugsnag-go v1.5.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA= github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/caddyserver/caddy v1.0.3/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -134,7 +141,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/deislabs/oras v0.7.0 h1:RnDoFd3tQYODMiUqxgQ8JxlrlWL0/VMKIKRD01MmNYk= github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2RzUTKDM= -github.com/deislabs/oras v0.8.0 h1:WZqPI25DlEmth2VE/pIcnEh6msL2yHrzS5lV5gwaCsQ= github.com/derailed/tview v0.3.3 h1:tipPwxcDhx0zRBZuc8VKIrNgWL40FL5JeF/30XVieUE= github.com/derailed/tview v0.3.3/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -145,7 +151,6 @@ github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d/go.mod h1:JLrzqnKDaYBop github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf h1:+Hdbkr8QbGSQ4dY50mmgZEGtzjhv0we2Ws2XCz3c0Q8= github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.1 h1:Dq4iIfcM7cNtddhLVWe9h4QDjsi4OER3Z8voPu/I52g= @@ -159,6 +164,7 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libnetwork v0.0.0-20180830151422-a9cd636e3789/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= @@ -185,10 +191,12 @@ github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwC github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= @@ -267,6 +275,7 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -274,10 +283,10 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09Vjb github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -313,14 +322,13 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= -github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -368,6 +376,7 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -407,7 +416,9 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.5 h1:jrGtp51JOKTWgvLFzfG6OtZOJcK2sEnzc/U+zw7TtbA= @@ -419,6 +430,7 @@ github.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= github.com/miekg/dns v0.0.0-20181005163659-0d29b283ac0f/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mindprince/gonvml v0.0.0-20171110221305-fee913ce8fb2/go.mod h1:2eu9pRWp8mo84xCg6KswZ+USQHjwgRhNp06sozOdsTY= github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= @@ -452,6 +464,7 @@ github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -476,6 +489,7 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/petergtz/pegomock v2.6.0+incompatible h1:gD9YvI42LylIA/il2Cy8lMfg+CncNFMqexYepyEWGaQ= github.com/petergtz/pegomock v2.6.0+incompatible/go.mod h1:nuBLWZpVyv/fLo56qTwt/AUau7jgouO1h7bEvZCq82o= +github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5 h1:rZQtoozkfsiNs36c7Tdv/gyGNzD1X1XWKO8rptVNZuM= github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -580,13 +594,17 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xenolf/lego v0.0.0-20160613233155-a9d8cec0e656/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY= +github.com/xenolf/lego v0.3.2-0.20160613233155-a9d8cec0e656 h1:BTvU+npm3/yjuBd53EvgiFLl5+YLikf2WvHsjRQ4KrY= github.com/xenolf/lego v0.3.2-0.20160613233155-a9d8cec0e656/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY= github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940 h1:p7OofyZ509h8DmPLh8Hn+EIIZm/xYhdZHJ9GnXHdr6U= github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.6 h1:qMJQYPNdtJ7UNYHjX38KXZtltKTqimMuoQjNnSVIuJg= github.com/yvasiyarov/gorelic v0.0.6/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -686,8 +704,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20170824195420-5d2fd3ccab98/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -718,6 +734,7 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= @@ -750,6 +767,7 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/square/go-jose.v1 v1.1.2 h1:/5jmADZB+RiKtZGr4HxsEFOEfbfsjTKsVnqpThUpE30= gopkg.in/square/go-jose.v1 v1.1.2/go.mod h1:QpYS+a4WhS+DTlyQIi6Ka7MS3SuR9a055rgXNEe6EiA= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= @@ -764,7 +782,6 @@ gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81 gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= -helm.sh/helm v2.16.1+incompatible h1:np11uYeEtlYcFIFRya8Xs5ZweV1z6MvaWQqJAW+1SZQ= helm.sh/helm/v3 v3.0.2 h1:BggvLisIMrAc+Is5oAHVrlVxgwOOrMN8nddfQbm5gKo= helm.sh/helm/v3 v3.0.2/go.mod h1:KBxE6XWO57XSNA1PA9CvVLYRY0zWqYQTad84bNXp1lw= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -828,6 +845,7 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +rsc.io/letsencrypt v0.0.1 h1:DV0d09Ne9E7UUa9ZqWktZ9L2VmybgTgfq7xlfFR/bbU= rsc.io/letsencrypt v0.0.1/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= diff --git a/internal/view/pod.go b/internal/view/pod.go index ef1d98a0..a442f140 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -10,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/watch" + "github.com/fatih/color" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" @@ -177,7 +178,8 @@ func shellIn(a *App, path, co string) { args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) log.Debug().Msgf("Shell args %v", args) - if !runK(true, a, fmt.Sprintf("Pod: %s | Container: %s\n\n", path, co), args...) { + c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) + if !runK(true, a, c.Sprintf("Pod: %s | Container: %s\n\n", path, co), args...) { a.Flash().Err(errors.New("Shell exec failed")) } } From e7031ed20a4d12238ece5dea68e67ce1b0fd762c Mon Sep 17 00:00:00 2001 From: Joscha Alisch Date: Fri, 7 Feb 2020 20:27:10 +0100 Subject: [PATCH 10/39] Make output a bit nicer --- internal/view/pod.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/view/pod.go b/internal/view/pod.go index a442f140..cd0c6b60 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -179,7 +179,7 @@ func shellIn(a *App, path, co string) { log.Debug().Msgf("Shell args %v", args) c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) - if !runK(true, a, c.Sprintf("Pod: %s | Container: %s\n\n", path, co), args...) { + if !runK(true, a, c.Sprintf(" Pod: %s | Container: %s \n\n", path, co), args...) { a.Flash().Err(errors.New("Shell exec failed")) } } From 67c0702bf2abe183c8f8dfd9467e4b9871d8d445 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 14:28:22 -0700 Subject: [PATCH 11/39] add searchable yaml/describe. Fix #464, #526 --- internal/config/alias.go | 2 +- internal/model/text.go | 120 ++++++++++++++++++ internal/model/text_test.go | 90 ++++++++++++++ internal/ui/app.go | 4 +- internal/ui/table.go | 6 +- internal/view/benchmark.go | 2 +- internal/view/browser.go | 2 +- internal/view/details.go | 238 +++++++++++++++++++++++++++++++----- internal/view/exec.go | 4 +- internal/view/helpers.go | 2 +- internal/view/log.go | 4 +- internal/view/node.go | 2 +- internal/view/pod.go | 3 +- internal/view/secret.go | 2 +- internal/view/xray.go | 4 +- internal/view/yaml.go | 14 ++- internal/view/yaml_test.go | 14 ++- 17 files changed, 453 insertions(+), 60 deletions(-) create mode 100644 internal/model/text.go create mode 100644 internal/model/text_test.go diff --git a/internal/config/alias.go b/internal/config/alias.go index ff8075da..622f94e3 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -143,7 +143,7 @@ func (a *Aliases) Define(gvr string, aliases ...string) { func (a *Aliases) LoadAliases(path string) error { f, err := ioutil.ReadFile(path) if err != nil { - log.Warn().Err(err).Msgf("No custom aliases found") + log.Debug().Err(err).Msgf("No custom aliases found") return nil } diff --git a/internal/model/text.go b/internal/model/text.go new file mode 100644 index 00000000..1da28560 --- /dev/null +++ b/internal/model/text.go @@ -0,0 +1,120 @@ +package model + +import ( + "regexp" + "strings" + + "github.com/sahilm/fuzzy" +) + +// TextListener represents a text model listener. +type TextListener interface { + // TextChanged notifies the model changed. + TextChanged([]string) + + // TextFiltered notifies when the filter changed. + TextFiltered([]string, fuzzy.Matches) +} + +// Text represents a text model. +type Text struct { + lines []string + listeners []TextListener + query string +} + +// NewText returns a new model. +func NewText() *Text { + return &Text{} +} + +// Peek returns the current model state. +func (t *Text) Peek() []string { + return t.lines +} + +// ClearFilter clear out filter. +func (t *Text) ClearFilter() { + t.query = "" + t.filterChanged(t.lines) +} + +// Filter filters out the text. +func (t *Text) Filter(q string) { + t.query = q + t.filterChanged(t.lines) +} + +// SetText sets the current model content. +func (t *Text) SetText(buff string) { + t.lines = strings.Split(buff, "\n") + t.fireTextChanged(t.lines) +} + +// AddListener adds a new model listener. +func (t *Text) AddListener(listener TextListener) { + t.listeners = append(t.listeners, listener) +} + +// RemoveListener delete a listener from the list. +func (t *Text) RemoveListener(listener TextListener) { + victim := -1 + for i, lis := range t.listeners { + if lis == listener { + victim = i + break + } + } + + if victim >= 0 { + t.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...) + } +} + +func (t *Text) filterChanged(lines []string) { + t.fireTextFiltered(lines, t.filter(t.query, lines)) +} + +func (t *Text) fireTextChanged(lines []string) { + for _, lis := range t.listeners { + lis.TextChanged(lines) + } +} + +func (t *Text) fireTextFiltered(lines []string, matches fuzzy.Matches) { + for _, lis := range t.listeners { + lis.TextFiltered(lines, matches) + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func (t *Text) filter(q string, lines []string) fuzzy.Matches { + if q == "" { + return nil + } + if isFuzzySelector(q) { + return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + } + return t.rxFilter(q, lines) +} + +func (*Text) fuzzyFilter(q string, lines []string) fuzzy.Matches { + return fuzzy.Find(q, lines) +} + +func (*Text) rxFilter(q string, lines []string) fuzzy.Matches { + rx, err := regexp.Compile(`(?i)` + q) + if err != nil { + return nil + } + matches := make(fuzzy.Matches, 0, len(lines)) + for i, l := range lines { + if loc := rx.FindStringIndex(l); len(loc) == 2 { + matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc}) + } + } + + return matches +} diff --git a/internal/model/text_test.go b/internal/model/text_test.go new file mode 100644 index 00000000..d8c2f949 --- /dev/null +++ b/internal/model/text_test.go @@ -0,0 +1,90 @@ +package model_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/sahilm/fuzzy" + "github.com/stretchr/testify/assert" +) + +func TestNewText(t *testing.T) { + m := model.NewText() + + lis := textLis{} + m.AddListener(&lis) + + m.SetText("Hello World\nBumbleBeeTuna") + + assert.Equal(t, 1, lis.changed) + assert.Equal(t, 2, lis.lines) + assert.Equal(t, 0, lis.filtered) + assert.Equal(t, 0, lis.matches) +} + +func TestTextFilterRXMatch(t *testing.T) { + m := model.NewText() + + lis := textLis{} + m.AddListener(&lis) + + m.SetText("Hello World\nBumbleBeeTuna") + m.Filter("world") + + assert.Equal(t, 1, lis.changed) + assert.Equal(t, 2, lis.lines) + assert.Equal(t, 1, lis.filtered) + assert.Equal(t, 1, lis.matches) + assert.Equal(t, 6, lis.index) +} + +func TestTextFilterFuzzyMatch(t *testing.T) { + m := model.NewText() + + lis := textLis{} + m.AddListener(&lis) + + m.SetText("Hello World\nBumbleBeeTuna") + m.Filter("-f world") + + assert.Equal(t, 1, lis.changed) + assert.Equal(t, 2, lis.lines) + assert.Equal(t, 1, lis.filtered) + assert.Equal(t, 1, lis.matches) + assert.Equal(t, 6, lis.index) +} + +func TestTextFilterNoMatch(t *testing.T) { + m := model.NewText() + + lis := textLis{} + m.AddListener(&lis) + + m.SetText("Hello World\nBumbleBeeTuna") + m.Filter("blee") + + assert.Equal(t, 1, lis.changed) + assert.Equal(t, 2, lis.lines) + assert.Equal(t, 1, lis.filtered) + assert.Equal(t, 0, lis.matches) + assert.Equal(t, 0, lis.index) +} + +// Helpers... + +type textLis struct { + changed, filtered, matches, lines, index int +} + +func (l *textLis) TextChanged(ll []string) { + l.lines = len(ll) + l.changed++ +} + +func (l *textLis) TextFiltered(ll []string, mm fuzzy.Matches) { + l.matches = len(mm) + l.filtered++ + if len(mm) > 0 && len(mm[0].MatchedIndexes) > 0 { + l.index = mm[0].MatchedIndexes[0] + } +} diff --git a/internal/ui/app.go b/internal/ui/app.go index e200f282..54f2a833 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -175,7 +175,7 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { a.cmdBuff.Add(evt.Rune()) return nil } - key = asKey(evt) + key = AsKey(evt) } if a, ok := a.actions[key]; ok { @@ -258,7 +258,7 @@ func (a *App) Menu() *Menu { // Helpers... // AsKey converts rune to keyboard key., -func asKey(evt *tcell.EventKey) tcell.Key { +func AsKey(evt *tcell.EventKey) tcell.Key { key := tcell.Key(evt.Rune()) if evt.Modifiers() == tcell.ModAlt { key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers())) diff --git a/internal/ui/table.go b/internal/ui/table.go index 2c091cf2..a9c8c9e5 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -121,7 +121,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { if t.filterInput(evt.Rune()) { return nil } - key = asKey(evt) + key = AsKey(evt) } if a, ok := t.actions[key]; ok { @@ -370,17 +370,17 @@ func (t *Table) styleTitle() string { } } - buff := t.cmdBuff.String() var title string if ns == client.ClusterScope { title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame()) } else { title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, rc), t.styles.Frame()) } + + buff := t.cmdBuff.String() if buff == "" { return title } - if IsLabelSelector(buff) { buff = TrimLabelSelector(buff) } diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index 681eddee..7fdc78f2 100644 --- a/internal/view/benchmark.go +++ b/internal/view/benchmark.go @@ -46,7 +46,7 @@ func (b *Benchmark) viewBench(app *App, model ui.Tabular, gvr, path string) { return } - details := NewDetails(b.App(), "Benchmark", fileToSubject(path)).Update(data) + details := NewDetails(b.App(), "Benchmark", fileToSubject(path), false).Update(data) if err := app.inject(details); err != nil { app.Flash().Err(err) } diff --git a/internal/view/browser.go b/internal/view/browser.go index 89cb8126..e416df6f 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -175,7 +175,7 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(b.app, "YAML", path).Update(raw) + details := NewDetails(b.app, "YAML", path, true).Update(raw) if err := b.App().inject(details); err != nil { b.App().Flash().Err(err) } diff --git a/internal/view/details.go b/internal/view/details.go index c1e52f67..76bf5916 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -3,6 +3,7 @@ package view import ( "context" "fmt" + "strings" "github.com/atotto/clipboard" "github.com/derailed/k9s/internal/config" @@ -10,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/gdamore/tcell" + "github.com/sahilm/fuzzy" ) const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " @@ -18,20 +20,26 @@ const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " type Details struct { *tview.TextView - actions ui.KeyActions - app *App - title, subject string - buff string + actions ui.KeyActions + app *App + title, subject string + cmdBuff *ui.CmdBuff + model *model.Text + currentRegion, maxRegions int + searchable bool } // NewDetails returns a details viewer. -func NewDetails(app *App, title, subject string) *Details { +func NewDetails(app *App, title, subject string, searchable bool) *Details { d := Details{ - TextView: tview.NewTextView(), - app: app, - title: title, - subject: subject, - actions: make(ui.KeyActions), + TextView: tview.NewTextView(), + app: app, + title: title, + subject: subject, + actions: make(ui.KeyActions), + cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), + model: model.NewText(), + searchable: searchable, } return &d @@ -42,37 +50,123 @@ func (d *Details) Init(_ context.Context) error { if d.title != "" { d.SetBorder(true) } - d.SetScrollable(true) - d.SetWrap(true) + d.SetScrollable(true).SetWrap(true).SetRegions(true) d.SetDynamicColors(true) d.SetHighlightColor(tcell.ColorOrange) d.SetTitleColor(tcell.ColorAqua) d.SetInputCapture(d.keyboard) - d.bindKeys() d.SetChangedFunc(func() { d.app.Draw() }) d.updateTitle() + d.app.Styles.AddListener(d) d.StylesChanged(d.app.Styles) + d.cmdBuff.AddListener(d.app.Cmd()) + d.cmdBuff.AddListener(d) + + d.bindKeys() + d.SetInputCapture(d.keyboard) + d.model.AddListener(d) + return nil } +func (d *Details) TextChanged(lines []string) { + d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, "\n"))) + d.ScrollToBeginning() +} + +func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) { + d.currentRegion, d.maxRegions = 0, 0 + + ll := make([]string, len(lines)) + copy(ll, lines) + for _, m := range matches { + loc, line := m.MatchedIndexes, ll[m.Index] + ll[m.Index] = line[:loc[0]] + fmt.Sprintf(`<<<"search_%d">>>`, d.maxRegions) + line[loc[0]:loc[1]] + `<<<"">>>` + line[loc[1]:] + d.maxRegions++ + } + + d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(ll, "\n"))) + d.Highlight() + if d.maxRegions > 0 { + d.Highlight("search_0") + d.ScrollToHighlight() + } +} + +// BufferChanged indicates the buffer was changed. +func (d *Details) BufferChanged(s string) {} + +// BufferActive indicates the buff activity changed. +func (d *Details) BufferActive(state bool, k ui.BufferKind) { + d.app.BufferActive(state, k) +} + +func (d *Details) bindKeys() { + d.actions.Set(ui.KeyActions{ + tcell.KeyEnter: ui.NewSharedKeyAction("Filter", d.filterCmd, false), + tcell.KeyEscape: ui.NewKeyAction("Back", d.resetCmd, false), + tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), + ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), + ui.KeyN: ui.NewKeyAction("Next Match", d.nextCmd, true), + ui.KeyShiftN: ui.NewKeyAction("Prev Match", d.prevCmd, true), + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", d.activateCmd, false), + tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", d.clearCmd, false), + tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), + tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), + tcell.KeyDelete: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), + }) + + if !d.searchable { + d.actions.Delete(ui.KeyN, ui.KeyShiftN) + } +} + +func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { + key := evt.Key() + if key == tcell.KeyUp || key == tcell.KeyDown { + return evt + } + + if key == tcell.KeyRune { + if d.filterInput(evt.Rune()) { + return nil + } + key = ui.AsKey(evt) + } + + if a, ok := d.actions[key]; ok { + return a.Action(evt) + } + + return evt +} + +func (d *Details) filterInput(r rune) bool { + if !d.cmdBuff.IsActive() { + return false + } + d.cmdBuff.Add(r) + d.updateTitle() + + return true +} + // StylesChanged notifies the skin changed. func (d *Details) StylesChanged(s *config.Styles) { d.SetBackgroundColor(d.app.Styles.BgColor()) d.SetTextColor(d.app.Styles.FgColor()) d.SetBorderFocusColor(config.AsColor(d.app.Styles.Frame().Border.FocusColor)) - d.Update(d.buff) + d.TextChanged(d.model.Peek()) } // Update updates the view content. func (d *Details) Update(buff string) *Details { - d.buff = buff - d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, buff)) - d.ScrollToBeginning() + d.model.SetText(buff) return d } @@ -108,24 +202,89 @@ func (d *Details) ExtraHints() map[string]string { return nil } -func (d *Details) bindKeys() { - d.actions.Set(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Back", d.app.PrevCmd, false), - tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), - ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), - }) +func (d *Details) nextCmd(evt *tcell.EventKey) *tcell.EventKey { + if d.cmdBuff.Empty() { + return evt + } + + d.currentRegion++ + if d.currentRegion >= d.maxRegions { + d.currentRegion = 0 + } + d.Highlight(fmt.Sprintf("search_%d", d.currentRegion)) + d.ScrollToHighlight() + d.updateTitle() + + return nil } -func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyRune { - key = tcell.Key(evt.Rune()) - } - if a, ok := d.actions[key]; ok { - return a.Action(evt) +func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey { + if d.cmdBuff.Empty() { + return evt } - return evt + d.currentRegion-- + if d.currentRegion < 0 { + d.currentRegion = d.maxRegions - 1 + } + d.Highlight(fmt.Sprintf("search_%d", d.currentRegion)) + d.ScrollToHighlight() + d.updateTitle() + + return nil +} + +func (d *Details) filterCmd(evt *tcell.EventKey) *tcell.EventKey { + d.model.Filter(d.cmdBuff.String()) + d.cmdBuff.SetActive(false) + d.updateTitle() + + return nil +} + +func (d *Details) activateCmd(evt *tcell.EventKey) *tcell.EventKey { + if d.app.InCmdMode() { + return evt + } + d.app.Flash().Info("Filter mode activated.") + d.cmdBuff.SetActive(true) + + return nil +} + +func (d *Details) clearCmd(*tcell.EventKey) *tcell.EventKey { + if !d.app.InCmdMode() { + return nil + } + d.cmdBuff.Clear() + + return nil +} + +func (d *Details) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { + if !d.cmdBuff.IsActive() { + return nil + } + d.cmdBuff.Delete() + + return nil +} + +func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey { + if !d.cmdBuff.InCmdMode() { + d.cmdBuff.Reset() + return d.app.PrevCmd(evt) + } + + if d.cmdBuff.String() != "" { + d.model.ClearFilter() + } + d.app.Flash().Info("Clearing filter...") + d.cmdBuff.SetActive(false) + d.cmdBuff.Reset() + d.updateTitle() + + return nil } func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -149,6 +308,19 @@ func (d *Details) updateTitle() { if d.title == "" { return } - title := ui.SkinTitle(fmt.Sprintf(detailsTitleFmt, d.title, d.subject), d.app.Styles.Frame()) - d.SetTitle(title) + fmat := fmt.Sprintf(detailsTitleFmt, d.title, d.subject) + + buff := d.cmdBuff.String() + if buff == "" { + d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) + return + } + + search := d.cmdBuff.String() + if d.maxRegions != 0 { + search += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions) + } + fmat += fmt.Sprintf(ui.SearchFmt, search) + + d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) } diff --git a/internal/view/exec.go b/internal/view/exec.go index dc5e39e2..8516e34b 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -13,6 +13,8 @@ import ( "github.com/rs/zerolog/log" ) +const shellCheck = `command -v bash >/dev/null && exec bash || exec sh` + func runK(clear bool, app *App, args ...string) bool { bin, err := exec.LookPath("kubectl") if err != nil { @@ -73,7 +75,7 @@ func execute(clear bool, bin string, bg bool, args ...string) error { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr err = cmd.Run() } - log.Debug().Msgf("Command returned error?? %v", err) + select { case <-ctx.Done(): return errors.New("canceled by operator") diff --git a/internal/view/helpers.go b/internal/view/helpers.go index bafeacfb..58d05ffe 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -67,7 +67,7 @@ func describeResource(app *App, model ui.Tabular, gvr, path string) { return } - details := NewDetails(app, "Describe", path).Update(yaml) + details := NewDetails(app, "Describe", path, true).Update(yaml) if err := app.inject(details); err != nil { app.Flash().Err(err) } diff --git a/internal/view/log.go b/internal/view/log.go index 3865590e..5e7ede41 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -67,7 +67,7 @@ func (l *Log) Init(ctx context.Context) (err error) { l.indicator = NewLogIndicator(l.app.Config, l.app.Styles) l.AddItem(l.indicator, 1, 1, false) - l.logs = NewDetails(l.app, "", "") + l.logs = NewDetails(l.app, "", "", false) if err = l.logs.Init(ctx); err != nil { return err } @@ -173,7 +173,7 @@ func (l *Log) bindKeys() { ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false), - tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.clearCmd, false), + tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.resetCmd, false), tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), diff --git a/internal/view/node.go b/internal/view/node.go index 4d69e677..dafbad67 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -59,7 +59,7 @@ func (n *Node) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(n.App(), "YAML", sel).Update(raw) + details := NewDetails(n.App(), "YAML", sel, true).Update(raw) if err := n.App().inject(details); err != nil { n.App().Flash().Err(err) } diff --git a/internal/view/pod.go b/internal/view/pod.go index cc03de98..2463a808 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" @@ -18,8 +19,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -const shellCheck = "command -v bash >/dev/null && exec bash || exec sh" - // Pod represents a pod viewer. type Pod struct { ResourceViewer diff --git a/internal/view/secret.go b/internal/view/secret.go index a8ea94f0..82eca65c 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -62,7 +62,7 @@ func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(s.App(), "Secret Decoder", path).Update(string(raw)) + details := NewDetails(s.App(), "Secret Decoder", path, false).Update(string(raw)) if err := s.App().inject(details); err != nil { s.App().Flash().Err(err) } diff --git a/internal/view/xray.go b/internal/view/xray.go index d16ce4bc..9613c2d5 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -269,7 +269,7 @@ func (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(x.app, "YAML", ref.Path).Update(raw) + details := NewDetails(x.app, "YAML", ref.Path, true).Update(raw) if err := x.app.inject(details); err != nil { x.app.Flash().Err(err) } @@ -321,7 +321,7 @@ func (x *Xray) describe(gvr, path string) { return } - details := NewDetails(x.app, "Describe", path).Update(yaml) + details := NewDetails(x.app, "Describe", path, true).Update(yaml) if err := x.app.inject(details); err != nil { x.app.Flash().Err(err) } diff --git a/internal/view/yaml.go b/internal/view/yaml.go index a25580b9..9ab389e9 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -8,9 +8,8 @@ import ( "strings" "time" - "github.com/derailed/tview" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/tview" "github.com/rs/zerolog/log" ) @@ -26,6 +25,7 @@ const ( ) func colorizeYAML(style config.Yaml, raw string) string { + // lines := strings.Split(raw, "\n") lines := strings.Split(tview.Escape(raw), "\n") fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor, 1) @@ -41,22 +41,26 @@ func colorizeYAML(style config.Yaml, raw string) string { for _, l := range lines { res := keyValRX.FindStringSubmatch(l) if len(res) == 4 { - buff = append(buff, fmt.Sprintf(fullFmt, res[1], res[2], res[3])) + buff = append(buff, enableRegion(fmt.Sprintf(fullFmt, res[1], res[2], res[3]))) continue } res = keyRX.FindStringSubmatch(l) if len(res) == 3 { - buff = append(buff, fmt.Sprintf(keyFmt, res[1], res[2])) + buff = append(buff, enableRegion(fmt.Sprintf(keyFmt, res[1], res[2]))) continue } - buff = append(buff, fmt.Sprintf(valFmt, l)) + buff = append(buff, enableRegion(fmt.Sprintf(valFmt, l))) } return strings.Join(buff, "\n") } +func enableRegion(str string) string { + return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]") +} + func saveYAML(cluster, name, data string) (string, error) { dir := filepath.Join(config.K9sDumpDir, cluster) if err := ensureDir(dir); err != nil { diff --git a/internal/view/yaml_test.go b/internal/view/yaml_test.go index 41451559..5e7ed5e9 100644 --- a/internal/view/yaml_test.go +++ b/internal/view/yaml_test.go @@ -13,15 +13,21 @@ func TestYaml(t *testing.T) { }{ { `api: fred - version: v1`, + version: v1`, `[steelblue::b]api[white::-]: [papayawhip::]fred - [steelblue::b]version[white::-]: [papayawhip::]v1`, + [steelblue::b]version[white::-]: [papayawhip::]v1`, + }, + { + `api: <<<"search_0">>>fred<<<"">>> + version: v1`, + `[steelblue::b]api[white::-]: [papayawhip::]["search_0"]fred[""] + [steelblue::b]version[white::-]: [papayawhip::]v1`, }, { `api: - version: v1`, + version: v1`, `[steelblue::b]api[white::-]: - [steelblue::b]version[white::-]: [papayawhip::]v1`, + [steelblue::b]version[white::-]: [papayawhip::]v1`, }, { " fred:blee", From 1a6ff3bfa38cdf650824f27c9fda49fe0110a020 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 15:36:46 -0700 Subject: [PATCH 12/39] rows age sort bust. Fix #536 --- internal/render/row_event.go | 30 ++++++++++++++++++++++++++---- internal/render/row_event_test.go | 20 ++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/internal/render/row_event.go b/internal/render/row_event.go index aeb3d0e5..5b607c69 100644 --- a/internal/render/row_event.go +++ b/internal/render/row_event.go @@ -4,8 +4,10 @@ import ( "fmt" "reflect" "sort" + "time" "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/util/duration" ) const ( @@ -139,19 +141,31 @@ func (rr RowEvents) FindIndex(id string) (int, bool) { return 0, false } +func (rr RowEvents) isAgeCol(col int) bool { + var age bool + if len(rr) == 0 { + return age + } + return col == len(rr[0].Row.Fields)-1 +} + // Sort rows based on column index and order. func (rr RowEvents) Sort(ns string, col int, asc bool) { t := RowEventSorter{NS: ns, Events: rr, Index: col, Asc: asc} sort.Sort(t) + ageCol := rr.isAgeCol(col) gg, kk := map[string][]string{}, make(StringSet, 0, len(rr)) - for _, e := range rr { - g := e.Row.Fields[col] + for _, r := range rr { + g := r.Row.Fields[col] + if ageCol { + g = toAgeDuration(g) + } kk = kk.Add(g) if ss, ok := gg[g]; ok { - gg[g] = append(ss, e.Row.ID) + gg[g] = append(ss, r.Row.ID) } else { - gg[g] = []string{e.Row.ID} + gg[g] = []string{r.Row.ID} } } @@ -164,6 +178,14 @@ func (rr RowEvents) Sort(ns string, col int, asc bool) { sort.Sort(s) } +func toAgeDuration(dur string) string { + d, err := time.ParseDuration(dur) + if err != nil { + return "n/a" + } + return duration.HumanDuration(d) +} + // ---------------------------------------------------------------------------- // RowEventSorter sorts row events by a given colon. diff --git a/internal/render/row_event_test.go b/internal/render/row_event_test.go index 496eca65..347940ac 100644 --- a/internal/render/row_event_test.go +++ b/internal/render/row_event_test.go @@ -81,6 +81,26 @@ func TestSort(t *testing.T) { {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, }, }, + "id_preserve": { + re: render.RowEvents{ + {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}}, + {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}}, + {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}}, + {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}}, + {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}}, + {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, + }, + col: 1, + asc: true, + e: render.RowEvents{ + {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}}, + {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}}, + {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}}, + {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}}, + {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}}, + {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, + }, + }, } for k := range uu { From 47d8bfb6e8aedfa0722d3904204769003e46d54b Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 16:01:16 -0700 Subject: [PATCH 13/39] add rel notes --- change_logs/release_v0.13.9.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 change_logs/release_v0.13.9.md diff --git a/change_logs/release_v0.13.9.md b/change_logs/release_v0.13.9.md new file mode 100644 index 00000000..789faeea --- /dev/null +++ b/change_logs/release_v0.13.9.md @@ -0,0 +1,31 @@ + + +# Release v0.13.9 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +Maintenance Release! + +## Resolved Bugs/Features/PRs + +* [Issue #536](https://github.com/derailed/k9s/issues/536) +* [Issue #526](https://github.com/derailed/k9s/issues/526) +* [Issue #564](https://github.com/derailed/k9s/issues/464) + +* [PR #532](https://github.com/derailed/k9s/pull/532) Thank you!! [Joscha Alisch](https://github.com/joscha-alisch) +* [PR #525](https://github.com/derailed/k9s/pull/525) Big Thanks!! [darklore](https://github.com/darklore) +* [PR #524](https://github.com/derailed/k9s/pull/524) Thank you (Again)!! [Joscha Alisch](https://github.com/joscha-alisch) +* [PR #514](https://github.com/derailed/k9s/pull/514) ATTA Boy!! [Alexander F. Rødseth](https://github.com/xyproto) +* [PR #483](https://github.com/derailed/k9s/pull/483) Thank you!! [Paul Varache](https://github.com/paulvarache) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) From 6670600756a4affe09517ba31be9358e751e9b4d Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 17:31:44 -0700 Subject: [PATCH 14/39] update rel notes --- change_logs/release_v0.13.9.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/change_logs/release_v0.13.9.md b/change_logs/release_v0.13.9.md index 098b3cd6..86a83f37 100644 --- a/change_logs/release_v0.13.9.md +++ b/change_logs/release_v0.13.9.md @@ -12,7 +12,39 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv --- -Maintenance Release! +## Happy Birthday K9s!! + +🎉🥳🎊 Doh! Almost missed it... 🎉🥳🎊 + + Yes sir, it's been a year (already...) since K9s was first launched 🎉. I can't tell you what a year this has been 🙀. Difficult? sure. However, you guys are making this project a total gas, by your candor, kindness and for giving back via your creative issues, prs, sponsorships, slack channel help to name a few... I do think, you've all been all too quiet tho 🐭... So if K9s helps make your K8s life bett'a on a day to day basis, please reach out for your shoe-phones and dial up [@kitesurfer](https://twitter.com/kitesurfer) or write an article/blog and /cc the K8s/CNCF royalties. Lastly I am so humbled by this... but we're closing on 5k stars/136k downloads in this repo, so please invite 28 of your closest friends soon... + +Major Thanks to all of you for you patience and for making this project a reality to all our K8s friends! You're all redefining awesomeness!! + +Also I'd like to take this opportunity to recognize and thank a few folks that have willingly volunteered their own time to track down issues and help improve K9s for all of us!! + +* [Gustavo Silva Paiva](https://github.com/paivagustavo) +* [Joscha Alisch](https://github.com/joscha-alisch) +* [Michael Christina](https://github.com/mcristina422) +* [Bruno Meneguello](https://github.com/bkmeneguello) +* [Tuomo Syvänperä](https://github.com/syvanpera) +* [Oskar F](https://github.com/fridokus) +* [Bruno Ohms](https://github.com/brunohms) +* [IgorRamalho](https://github.com/IgorRamalho) +* [Benjamin](https://github.com/binarycoded) +* [Norbert Csibra](https://github.com/ncsibra) +* [Andrew Roth](https://github.com/RothAndrew) +* [Sgandon](https://github.com/sgandon) +* [Chris Werner Rau](https://github.com/cwrau) +* [Eldad Assis](https://github.com/eldada) +* [Tobias](https://github.com/mycrEEpy) +* [Helge Sychla](https://github.com/hsychla) +* [Markusi75](https://github.com/Makusi75) +* [Swe-Covis](https://github.com/swe-covis) +* [Evgeniy Shubin](https://github.com/com30n) + +## And On Another Note... + +More bugz...😿 ## Resolved Bugs/Features/PRs From adb1475474a8e7bf79a62d0da754228cf8eaf865 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 17:42:37 -0700 Subject: [PATCH 15/39] update rel config --- .goreleaser.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index ffb43d7c..71ceb931 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -22,16 +22,17 @@ builds: - 7 ldflags: - -s -w -X github.com/derailed/k9s/cmd.version={{.Version}} -X github.com/derailed/k9s/cmd.commit={{.Commit}} -X github.com/derailed/k9s/cmd.date={{.Date}} -archive: - replacements: - darwin: Darwin - linux: Linux - windows: Windows - bit: Arm - bitv6: Arm6 - bitv7: Arm7 - 386: i386 - amd64: x86_64 +archives: + - name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" + replacements: + darwin: Darwin + linux: Linux + windows: Windows + bit: Arm + bitv6: Arm6 + bitv7: Arm7 + 386: i386 + amd64: x86_64 checksum: name_template: "checksums.txt" snapshot: @@ -43,7 +44,7 @@ changelog: - "^docs:" - "^test:" -# Homebrew +# Homebrews brews: - name: k9s github: From 659117a7e4e0069adbf20dd73e50aee0769682a9 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 17:43:33 -0700 Subject: [PATCH 16/39] update rel notes --- change_logs/release_v0.13.9.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/change_logs/release_v0.13.9.md b/change_logs/release_v0.13.9.md index 86a83f37..42fe34c2 100644 --- a/change_logs/release_v0.13.9.md +++ b/change_logs/release_v0.13.9.md @@ -16,7 +16,7 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv 🎉🥳🎊 Doh! Almost missed it... 🎉🥳🎊 - Yes sir, it's been a year (already...) since K9s was first launched 🎉. I can't tell you what a year this has been 🙀. Difficult? sure. However, you guys are making this project a total gas, by your candor, kindness and for giving back via your creative issues, prs, sponsorships, slack channel help to name a few... I do think, you've all been all too quiet tho 🐭... So if K9s helps make your K8s life bett'a on a day to day basis, please reach out for your shoe-phones and dial up [@kitesurfer](https://twitter.com/kitesurfer) or write an article/blog and /cc the K8s/CNCF royalties. Lastly I am so humbled by this... but we're closing on 5k stars/136k downloads in this repo, so please invite 28 of your closest friends soon... + Yes sir, it's been a year (already...) since K9s was first launched 🎉. I can't tell you what a year this has been 🙀. Difficult? sure. However, you guys are making this project a total gas, by your candor, kindness and for giving back via your creative issues, prs, sponsorships, slack channel help to name a few... I do think, you've all been all too quiet tho 🐭... So if K9s helps make your K8s life bett'a on a day to day basis, please reach out for your shoe-phones and dial up [@kitesurfer](https://twitter.com/kitesurfer) or write an article/blog and share it! Lastly I am so humbled by this... but we're closing on 5k stars/136k downloads in this repo, so please invite 28 of your closest friends soon... Major Thanks to all of you for you patience and for making this project a reality to all our K8s friends! You're all redefining awesomeness!! From e09d61f10d0ab49338b30401a2ba71f3b5c1ac70 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 17:51:26 -0700 Subject: [PATCH 17/39] update rel notes --- change_logs/{release_v0.13.9.md => release_v0.14.0.md} | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) rename change_logs/{release_v0.13.9.md => release_v0.14.0.md} (89%) diff --git a/change_logs/release_v0.13.9.md b/change_logs/release_v0.14.0.md similarity index 89% rename from change_logs/release_v0.13.9.md rename to change_logs/release_v0.14.0.md index 42fe34c2..20112695 100644 --- a/change_logs/release_v0.13.9.md +++ b/change_logs/release_v0.14.0.md @@ -1,6 +1,6 @@ -# Release v0.13.9 +# Release v0.14.0 ## Notes @@ -42,6 +42,10 @@ Also I'd like to take this opportunity to recognize and thank a few folks that h * [Swe-Covis](https://github.com/swe-covis) * [Evgeniy Shubin](https://github.com/com30n) +## Search Enabled For Describe/YAML views + +In this drop we made the Describe/YAML views searchable. So you no longer need to plow thru your resource configurations and get directly to the just of it by using the search command ie `/elvis` + `enter`. You can use the familiar keys `n` and `N` to nav back and forth to the next occurrence in a circular buffer fashion once you've reached the BOF/EOF. It's the little things in life... + ## And On Another Note... More bugz...😿 From 54582b9274046d19ca425fc803372f6e1dfc9ac3 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 18:01:59 -0700 Subject: [PATCH 18/39] update build target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index afbae2da..4685f5b8 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ NAME := k9s PACKAGE := github.com/derailed/$(NAME) GIT := $(shell git rev-parse --short HEAD) DATE := $(shell date +%FT%T%Z) -VERSION := v0.12.0 +VERSION ?= v0.14.0 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} From bf0fabcb9e2e742eeaa8c3c07489c4b69c1f2567 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 18:04:51 -0700 Subject: [PATCH 19/39] udpate rel targets --- .goreleaser.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 71ceb931..2643bb05 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -15,7 +15,6 @@ builds: goarch: - 386 - amd64 - - arm - arm64 goarm: - 6 From 8299129baf56c705bed35d294411400d954e358b Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 10 Feb 2020 19:27:22 -0700 Subject: [PATCH 20/39] cleaning up --- internal/view/details.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/view/details.go b/internal/view/details.go index 76bf5916..66d64624 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -73,11 +73,13 @@ func (d *Details) Init(_ context.Context) error { return nil } +// TextChanged notifies the model changed. func (d *Details) TextChanged(lines []string) { d.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, "\n"))) d.ScrollToBeginning() } +// TextFiltered notifies when the filter changed. func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) { d.currentRegion, d.maxRegions = 0, 0 From aa58d1063aa9e325afabacbe561c3368e939a80d Mon Sep 17 00:00:00 2001 From: derailed Date: Tue, 11 Feb 2020 15:26:49 -0700 Subject: [PATCH 21/39] fix pod xray menu + layout --- go.mod | 1 + go.sum | 26 +++ internal/client/client.go | 38 ++-- internal/client/config.go | 4 + internal/client/types.go | 2 +- internal/config/mock_connection_test.go | 6 +- internal/config/styles.go | 3 + internal/dao/container_test.go | 2 +- internal/dao/context.go | 3 +- internal/dao/node.go | 2 - internal/model/mock_clustermeta_test.go | 6 +- internal/model/table.go | 14 +- internal/ui/logo.go | 4 + internal/ui/tree.go | 4 +- internal/view/actions.go | 8 +- internal/view/app.go | 36 ++-- internal/view/browser.go | 3 - internal/view/context.go | 1 + internal/view/exec.go | 2 +- internal/view/helpers.go | 25 ++- internal/view/pod.go | 117 +++++++----- internal/view/xray.go | 179 ++++++++++-------- internal/xray/container.go | 6 +- internal/xray/container_test.go | 2 +- internal/xray/pod.go | 9 +- internal/xray/pod_test.go | 34 ++-- .../{test_assets => testdata}/cilium.json | 0 .../xray/{test_assets => testdata}/dp.json | 0 .../xray/{test_assets => testdata}/ds.json | 0 .../xray/{test_assets => testdata}/init.json | 0 .../xray/{test_assets => testdata}/ns.json | 0 .../xray/{test_assets => testdata}/po.json | 0 .../xray/{test_assets => testdata}/rs.json | 0 .../xray/{test_assets => testdata}/sa.json | 0 .../xray/{test_assets => testdata}/sts.json | 0 .../xray/{test_assets => testdata}/svc.json | 0 internal/xray/tree_node.go | 90 ++++++--- internal/xray/tree_node_test.go | 116 ++++++------ 38 files changed, 443 insertions(+), 300 deletions(-) rename internal/xray/{test_assets => testdata}/cilium.json (100%) rename internal/xray/{test_assets => testdata}/dp.json (100%) rename internal/xray/{test_assets => testdata}/ds.json (100%) rename internal/xray/{test_assets => testdata}/init.json (100%) rename internal/xray/{test_assets => testdata}/ns.json (100%) rename internal/xray/{test_assets => testdata}/po.json (100%) rename internal/xray/{test_assets => testdata}/rs.json (100%) rename internal/xray/{test_assets => testdata}/sa.json (100%) rename internal/xray/{test_assets => testdata}/sts.json (100%) rename internal/xray/{test_assets => testdata}/svc.json (100%) diff --git a/go.mod b/go.mod index 5af8ab15..b8356134 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ replace ( ) require ( + fyne.io/fyne v1.2.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/atotto/clipboard v0.1.2 github.com/derailed/tview v0.3.3 diff --git a/go.sum b/go.sum index 16167445..824aab0a 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +fyne.io/fyne v1.2.2 h1:mf7EseASp3CAC5vLWVPLnsoKxvp/ARdu3Seh0HvAQak= +fyne.io/fyne v1.2.2/go.mod h1:Ab+3DIB/FVteW0y4DXfmZv4N3JdnCBh2lHkINI02BOU= github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= @@ -29,6 +31,7 @@ github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFD github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= +github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= @@ -59,6 +62,7 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/Rican7/retry v0.1.0/go.mod h1:FgOROf8P5bebcC1DS0PdOQiqGUridaZvikzUmkFW6gg= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -212,6 +216,10 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7a github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M= github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= +github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f h1:7MsFMbSn8Lcw0blK4+NEOf8DuHoOBDhJsHz04yh13pM= +github.com/go-gl/glfw v0.0.0-20181213070059-819e8ce5125f/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -283,6 +291,8 @@ github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09Vjb github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -366,9 +376,11 @@ github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josephspurrier/goversioninfo v0.0.0-20190124120936-8611f5a5ff3f/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= @@ -460,6 +472,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= @@ -568,6 +582,10 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/srwiley/oksvg v0.0.0-20190829233741-58e08c8fe40e h1:LJUrNHytcMXWKxnULIHPe5SCb1jDpO9o672VB1x2EuQ= +github.com/srwiley/oksvg v0.0.0-20190829233741-58e08c8fe40e/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= +github.com/srwiley/rasterx v0.0.0-20181219215540-696f7edb7a7e h1:FFotfUvew9Eg02LYRl8YybAnm0HCwjjfY5JlOI1oB00= +github.com/srwiley/rasterx v0.0.0-20181219215540-696f7edb7a7e/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= github.com/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -577,6 +595,7 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/syndtr/gocapability v0.0.0-20160928074757-e7cb7fa329f4/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -630,8 +649,12 @@ golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152 h1:ZC1Xn5A1nlpSmQCIva4bZ3 golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495 h1:I6A9Ag9FpEKOjcKrRNjQkPHawoXIhKyTGfvvjFAiiAk= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -639,6 +662,8 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -695,6 +720,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934 h1:u/E0NqCIWRDAo9WCFo6Ko49njPFDLSd3z+X1HgWDMpE= golang.org/x/sys v0.0.0-20191028164358-195ce5e7f934/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/client/client.go b/internal/client/client.go index 7c287284..11a1132e 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -77,6 +77,12 @@ func makeCacheKey(ns, gvr string, vv []string) string { return ns + ":" + gvr + "::" + strings.Join(vv, ",") } +func (a *APIClient) clearCache() { + for _, k := range a.cache.Keys() { + a.cache.Remove(k) + } +} + // CanI checks if user has access to a certain resource. func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) { if IsClusterWide(ns) { @@ -131,6 +137,9 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { // BOZO!! No super sure about this approach either?? func (a *APIClient) CheckConnectivity() (status bool) { defer func() { + if !status { + a.clearCache() + } if err := recover(); err != nil { status = false } @@ -149,10 +158,10 @@ func (a *APIClient) CheckConnectivity() (status bool) { } } - if _, err := a.checkClientSet.ServerVersion(); err != nil { - log.Error().Err(err).Msgf("K9s can't connect to cluster") - } else { + if _, err := a.checkClientSet.ServerVersion(); err == nil { status = true + } else { + log.Error().Err(err).Msgf("K9s can't connect to cluster") } return @@ -258,21 +267,24 @@ func (a *APIClient) MXDial() (*versioned.Clientset, error) { return a.mxsClient, err } -// SwitchContextOrDie handles kubeconfig context switches. -func (a *APIClient) SwitchContextOrDie(ctx string) { +// SwitchContext handles kubeconfig context switches. +func (a *APIClient) SwitchContext(ctx string) error { currentCtx, err := a.config.CurrentContextName() if err != nil { - log.Fatal().Err(err).Msg("Fetching current context") + return err + } + if currentCtx == ctx { + return nil } - if currentCtx != ctx { - a.cachedClient = nil - a.reset() - if err := a.config.SwitchContext(ctx); err != nil { - log.Fatal().Err(err).Msg("Switching context") - } - _ = a.supportsMxServer() + if err := a.config.SwitchContext(ctx); err != nil { + return err } + a.clearCache() + a.reset() + _ = a.supportsMxServer() + + return nil } func (a *APIClient) reset() { diff --git a/internal/client/config.go b/internal/client/config.go index bcfec129..58f9d74d 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -48,6 +48,10 @@ func (c *Config) SwitchContext(name string) error { return err } + if _, err := c.GetContext(name); err != nil { + return fmt.Errorf("context %s does not exist", name) + } + if currentCtx != name { c.reset() c.flags.Context, c.currentContext = &name, name diff --git a/internal/client/types.go b/internal/client/types.go index 91e1f247..48fe4146 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -64,7 +64,7 @@ type Connection interface { Config() *Config DialOrDie() kubernetes.Interface - SwitchContextOrDie(ctx string) + SwitchContext(ctx string) error CachedDiscoveryOrDie() *disk.CachedDiscoveryClient RestConfigOrDie() *restclient.Config MXDial() (*versioned.Clientset, error) diff --git a/internal/config/mock_connection_test.go b/internal/config/mock_connection_test.go index 57a8104b..d2fd8c48 100644 --- a/internal/config/mock_connection_test.go +++ b/internal/config/mock_connection_test.go @@ -267,12 +267,14 @@ func (mock *MockConnection) SupportsResource(_param0 string) bool { return ret0 } -func (mock *MockConnection) SwitchContextOrDie(_param0 string) { +func (mock *MockConnection) SwitchContext(_param0 string) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockConnection().") } params := []pegomock.Param{_param0} - pegomock.GetGenericMockFrom(mock).Invoke("SwitchContextOrDie", params, []reflect.Type{}) + pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + + return nil } func (mock *MockConnection) ValidNamespaces() ([]v1.Namespace, error) { diff --git a/internal/config/styles.go b/internal/config/styles.go index f72c8233..ef7596bd 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -389,6 +389,9 @@ func (s *Styles) Update() { // AsColor checks color index, if match return color otherwise pink it is. func AsColor(c string) tcell.Color { + if c == "default" { + return tcell.ColorDefault + } if color, ok := tcell.ColorNames[c]; ok { return color } diff --git a/internal/dao/container_test.go b/internal/dao/container_test.go index 2b61a9d1..a486017f 100644 --- a/internal/dao/container_test.go +++ b/internal/dao/container_test.go @@ -44,7 +44,7 @@ func makeConn() *conn { func (c *conn) Config() *client.Config { return nil } func (c *conn) DialOrDie() kubernetes.Interface { return nil } -func (c *conn) SwitchContextOrDie(ctx string) {} +func (c *conn) SwitchContext(ctx string) error { return nil } func (c *conn) CachedDiscoveryOrDie() *disk.CachedDiscoveryClient { return nil } func (c *conn) RestConfigOrDie() *restclient.Config { return nil } func (c *conn) MXDial() (*versioned.Clientset, error) { return nil, nil } diff --git a/internal/dao/context.go b/internal/dao/context.go index 1741dbd1..da13e7b2 100644 --- a/internal/dao/context.go +++ b/internal/dao/context.go @@ -58,8 +58,7 @@ func (c *Context) MustCurrentContextName() string { // Switch to another context. func (c *Context) Switch(ctx string) error { - c.Factory.Client().SwitchContextOrDie(ctx) - return nil + return c.Factory.Client().SwitchContext(ctx) } // KubeUpdate modifies kubeconfig default context. diff --git a/internal/dao/node.go b/internal/dao/node.go index c821fdc1..ec58eb6d 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -29,8 +29,6 @@ type Node struct { // List returns a collection of node resources. func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) { - log.Debug().Msgf("NODE-LIST %q:%q", ns, n.gvr) - labels, ok := ctx.Value(internal.KeyLabels).(string) if !ok { log.Warn().Msgf("No label selector found in context") diff --git a/internal/model/mock_clustermeta_test.go b/internal/model/mock_clustermeta_test.go index cf5426c2..a47a8434 100644 --- a/internal/model/mock_clustermeta_test.go +++ b/internal/model/mock_clustermeta_test.go @@ -332,12 +332,14 @@ func (mock *MockClusterMeta) SupportsResource(_param0 string) bool { return ret0 } -func (mock *MockClusterMeta) SwitchContextOrDie(_param0 string) { +func (mock *MockClusterMeta) SwitchContext(_param0 string) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClusterMeta().") } params := []pegomock.Param{_param0} - pegomock.GetGenericMockFrom(mock).Invoke("SwitchContextOrDie", params, []reflect.Type{}) + pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + + return nil } func (mock *MockClusterMeta) UserName() (string, error) { diff --git a/internal/model/table.go b/internal/model/table.go index 327d5586..2fbb6376 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -2,6 +2,7 @@ package model import ( "context" + "errors" "fmt" "sync" "sync/atomic" @@ -75,10 +76,15 @@ func (t *Table) RemoveListener(l TableListener) { // Watch initiates model updates. func (t *Table) Watch(ctx context.Context) { - t.Refresh(ctx) + t.refresh(ctx) go t.updater(ctx) } +// Refresh updates the table content. +func (t *Table) Refresh(ctx context.Context) { + t.refresh(ctx) +} + // Get returns a resource instance if found, else an error. func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { meta, err := t.getMeta(ctx) @@ -134,11 +140,6 @@ func (t *Table) ToYAML(ctx context.Context, path string) (string, error) { return desc.ToYAML(path) } -// Refresh update the model now. -func (t *Table) Refresh(ctx context.Context) { - t.refresh(ctx) -} - // GetNamespace returns the model namespace. func (t *Table) GetNamespace() string { return t.namespace @@ -185,6 +186,7 @@ func (t *Table) updater(ctx context.Context) { for { select { case <-ctx.Done(): + t.fireTableLoadFailed(errors.New("operation canceled")) return case <-time.After(rate): rate = t.refreshRate diff --git a/internal/ui/logo.go b/internal/ui/logo.go index 7789833e..0935de22 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -3,6 +3,8 @@ package ui import ( "fmt" + "github.com/gdamore/tcell" + "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" ) @@ -94,6 +96,7 @@ func (l *Logo) refreshLogo(c string) { func logo() *tview.TextView { v := tview.NewTextView() + v.SetBackgroundColor(tcell.ColorDefault) v.SetWordWrap(false) v.SetWrap(false) v.SetTextAlign(tview.AlignLeft) @@ -104,6 +107,7 @@ func logo() *tview.TextView { func status() *tview.TextView { v := tview.NewTextView() + v.SetBackgroundColor(tcell.ColorDefault) v.SetWordWrap(false) v.SetWrap(false) v.SetTextAlign(tview.AlignCenter) diff --git a/internal/ui/tree.go b/internal/ui/tree.go index 61363d14..8179418c 100644 --- a/internal/ui/tree.go +++ b/internal/ui/tree.go @@ -35,7 +35,7 @@ func NewTree() *Tree { // Init initializes the view func (t *Tree) Init(ctx context.Context) error { - t.bindKeys() + t.BindKeys() t.SetBorder(true) t.SetBorderAttributes(tcell.AttrBold) t.SetBorderPadding(0, 0, 1, 1) @@ -86,7 +86,7 @@ func (t *Tree) ExtraHints() map[string]string { return nil } -func (t *Tree) bindKeys() { +func (t *Tree) BindKeys() { t.Actions().Add(KeyActions{ KeySpace: NewKeyAction("Expand/Collapse", t.noopCmd, true), KeyX: NewKeyAction("Expand/Collapse All", t.toggleCollapseCmd, true), diff --git a/internal/view/actions.go b/internal/view/actions.go index 38ccec30..f0ec5529 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -118,12 +118,16 @@ func execCmd(r Runner, bin string, bg bool, args ...string) ui.ActionHandler { ns, _ := client.Namespaced(path) var ( - env = r.EnvFn()() aa = make([]string, len(args)) err error ) + + if r.EnvFn() == nil { + return nil + } + for i, a := range args { - aa[i], err = env.envFor(ns, a) + aa[i], err = r.EnvFn()().envFor(ns, a) if err != nil { log.Error().Err(err).Msg("Plugin Args match failed") return nil diff --git a/internal/view/app.go b/internal/view/app.go index 43efb7f8..aded6e6f 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "sync" + "sync/atomic" "time" "github.com/derailed/k9s/internal" @@ -24,7 +24,7 @@ var ExitStatus = "" const ( splashDelay = 1 * time.Second clusterRefresh = 5 * time.Second - maxConRetry = 5 + maxConRetry = 10 clusterInfoWidth = 50 clusterInfoPad = 15 ) @@ -39,9 +39,8 @@ type App struct { version string showHeader bool cancelFn context.CancelFunc - conRetry int + conRetry int32 clusterModel *model.ClusterInfo - mx sync.Mutex } // NewApp returns a K9s app instance. @@ -61,9 +60,7 @@ func NewApp(cfg *config.Config) *App { // ConOK checks the connection is cool, returns false otherwise. func (a *App) ConOK() bool { - a.mx.Lock() - defer a.mx.Unlock() - return a.conRetry == 0 + return atomic.LoadInt32(&a.conRetry) == 0 } // Init initializes the application. @@ -198,32 +195,31 @@ func (a *App) clusterUpdater(ctx context.Context) { } func (a *App) refreshCluster() { - a.mx.Lock() - defer a.mx.Unlock() - c := a.Content.Top() if ok := a.Conn().CheckConnectivity(); ok { - if a.conRetry > 0 { + if atomic.LoadInt32(&a.conRetry) > 0 { + atomic.StoreInt32(&a.conRetry, 0) + a.Status(ui.FlashInfo, "K8s connectivity OK") if c != nil { c.Start() } - a.Status(ui.FlashInfo, "K8s connectivity OK") } - a.conRetry = 0 } else { - a.conRetry++ - log.Warn().Msgf("Conn check failed (%d/%d)", a.conRetry, maxConRetry) + atomic.AddInt32(&a.conRetry, 1) if c != nil { c.Stop() } - a.Status(ui.FlashWarn, fmt.Sprintf("Dial K8s failed (%d)", a.conRetry)) - + count := atomic.LoadInt32(&a.conRetry) + log.Warn().Msgf("Conn check failed (%d/%d)", count, maxConRetry) + a.Status(ui.FlashWarn, fmt.Sprintf("Dial K8s failed (%d)", count)) } - if a.conRetry >= maxConRetry { - ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", a.conRetry) + + count := atomic.LoadInt32(&a.conRetry) + if count >= maxConRetry { + ExitStatus = fmt.Sprintf("Lost K8s connection (%d). Bailing out!", count) a.BailOut() } - if a.conRetry > 0 { + if count > 0 { return } diff --git a/internal/view/browser.go b/internal/view/browser.go index 4d265775..8dc6d351 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -93,7 +93,6 @@ func (b *Browser) SetInstance(path string) { func (b *Browser) Start() { b.Stop() - log.Debug().Msgf("BROWSER started!") b.Table.Start() ctx := b.defaultContext() ctx, b.cancelFn = context.WithCancel(ctx) @@ -111,7 +110,6 @@ func (b *Browser) Stop() { if b.cancelFn == nil { return } - log.Debug().Msgf("BROWSER Stopped!") b.Table.Stop() b.cancelFn() b.cancelFn = nil @@ -373,7 +371,6 @@ func (b *Browser) refreshActions() { if b.app.ConOK() { b.namespaceActions(aa) - if !b.app.Config.K9s.GetReadOnly() { if client.Can(b.meta.Verbs, "edit") { aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true) diff --git a/internal/view/context.go b/internal/view/context.go index 54ffb613..fce01e41 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -55,6 +55,7 @@ func useContext(app *App, name string) error { return errors.New("Expecting a switchable resource") } if err := switcher.Switch(name); err != nil { + log.Error().Err(err).Msgf("Context switch failed") return err } if err := app.switchCtx(name, false); err != nil { diff --git a/internal/view/exec.go b/internal/view/exec.go index f85d010c..ff905ca7 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -76,7 +76,7 @@ func execute(opts shellOpts) error { cancel() }() - log.Debug().Msgf("Running command > %s %s", opts.binary, strings.Join(opts.args, " ")) + log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " ")) cmd := exec.Command(opts.binary, opts.args...) diff --git a/internal/view/helpers.go b/internal/view/helpers.go index 58d05ffe..a48e5057 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -16,39 +16,44 @@ import ( "github.com/rs/zerolog/log" ) -func defaultK9sEnv(app *App, sel string, row render.Row) K9sEnv { - ns, n := client.Namespaced(sel) - ctx, err := app.Conn().Config().CurrentContextName() +func generalEnv(a *App) K9sEnv { + ctx, err := a.Conn().Config().CurrentContextName() if err != nil { ctx = render.NAValue } - cluster, err := app.Conn().Config().CurrentClusterName() + cluster, err := a.Conn().Config().CurrentClusterName() if err != nil { cluster = render.NAValue } - user, err := app.Conn().Config().CurrentUserName() + user, err := a.Conn().Config().CurrentUserName() if err != nil { user = render.NAValue } - groups, err := app.Conn().Config().CurrentGroupNames() + groups, err := a.Conn().Config().CurrentGroupNames() if err != nil { groups = []string{render.NAValue} } + var cfg string - kcfg := app.Conn().Config().Flags().KubeConfig + kcfg := a.Conn().Config().Flags().KubeConfig if kcfg != nil && *kcfg != "" { cfg = *kcfg } - env := K9sEnv{ - "NAMESPACE": ns, - "NAME": n, + return K9sEnv{ "CONTEXT": ctx, "CLUSTER": cluster, "USER": user, "GROUPS": strings.Join(groups, ","), "KUBECONFIG": cfg, } +} + +func defaultK9sEnv(a *App, sel string, row render.Row) K9sEnv { + ns, n := client.Namespaced(sel) + + env := generalEnv(a) + env["NAMESPACE"], env["NAME"] = ns, n for i, r := range row.Fields { env["COL"+strconv.Itoa(i)] = r diff --git a/internal/view/pod.go b/internal/view/pod.go index 436fa883..11f50ec0 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -8,6 +8,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/watch" @@ -108,47 +109,86 @@ func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey { } func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey { - sel := p.GetTable().GetSelectedItem() - if sel == "" { + path := p.GetTable().GetSelectedItem() + if path == "" { return evt } row := p.GetTable().GetSelectedRowIndex() status := ui.TrimCell(p.GetTable().SelectTable, row, p.GetTable().NameColIndex()+2) if status != render.Running { - p.App().Flash().Errf("%s is not in a running state", sel) + p.App().Flash().Errf("%s is not in a running state", path) return nil } - cc, err := fetchContainers(p.App().factory, sel, false) - if err != nil { - p.App().Flash().Errf("Unable to retrieve containers %s", err) - return evt - } - if len(cc) == 1 { - p.shellIn(sel, cc[0]) - return nil - } - picker := NewPicker() - picker.populate(cc) - picker.SetSelectedFunc(func(i int, t, d string, r rune) { - p.shellIn(sel, t) - }) - if err := p.App().inject(picker); err != nil { + + if err := containerShellin(p.App(), p, path, ""); err != nil { p.App().Flash().Err(err) } - return evt -} - -func (p *Pod) shellIn(path, co string) { - p.Stop() - shellIn(p.App(), path, co) - p.Start() + return nil } // ---------------------------------------------------------------------------- // Helpers... +func containerShellin(a *App, comp model.Component, path, co string) error { + if co != "" { + resumeShellIn(a, comp, path, co) + return nil + } + + cc, err := fetchContainers(a.factory, path, false) + if err != nil { + return err + } + if len(cc) == 1 { + resumeShellIn(a, comp, path, cc[0]) + return nil + } + picker := NewPicker() + picker.populate(cc) + picker.SetSelectedFunc(func(_ int, co, _ string, _ rune) { + resumeShellIn(a, comp, path, co) + }) + if err := a.inject(picker); err != nil { + return err + } + + return nil +} + +func resumeShellIn(a *App, c model.Component, path, co string) { + c.Stop() + defer c.Start() + shellIn(a, path, co) +} + +func shellIn(a *App, path, co string) { + args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) + + c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) + if !runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}) { + a.Flash().Err(errors.New("Shell exec failed")) + } +} + +func computeShellArgs(path, co, context string, kcfg *string) []string { + args := make([]string, 0, 15) + args = append(args, "exec", "-it") + args = append(args, "--context", context) + ns, po := client.Namespaced(path) + args = append(args, "-n", ns) + args = append(args, po) + if kcfg != nil && *kcfg != "" { + args = append(args, "--kubeconfig", *kcfg) + } + if co != "" { + args = append(args, "-c", co) + } + + return append(args, "--", "sh", "-c", shellCheck) +} + func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, error) { o, err := f.Get("v1/pods", path, true, labels.Everything()) if err != nil { @@ -172,30 +212,3 @@ func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, } return nn, nil } - -func shellIn(a *App, path, co string) { - args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) - log.Debug().Msgf("Shell args %v", args) - - c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) - if !runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}) { - a.Flash().Err(errors.New("Shell exec failed")) - } -} - -func computeShellArgs(path, co, context string, kcfg *string) []string { - args := make([]string, 0, 15) - args = append(args, "exec", "-it") - args = append(args, "--context", context) - ns, po := client.Namespaced(path) - args = append(args, "-n", ns) - args = append(args, po) - if kcfg != nil && *kcfg != "" { - args = append(args, "--kubeconfig", *kcfg) - } - if co != "" { - args = append(args, "-c", co) - } - - return append(args, "--", "sh", "-c", shellCheck) -} diff --git a/internal/view/xray.go b/internal/view/xray.go index c4847839..8d0841e9 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -50,6 +50,8 @@ func NewXray(gvr client.GVR) ResourceViewer { // Init initializes the view func (x *Xray) Init(ctx context.Context) error { + x.envFn = x.k9sEnv + if err := x.Tree.Init(ctx); err != nil { return err } @@ -77,12 +79,12 @@ func (x *Xray) Init(ctx context.Context) error { x.model.AddListener(x) x.SetChangedFunc(func(n *tview.TreeNode) { - ref, ok := n.GetReference().(xray.NodeSpec) + spec, ok := n.GetReference().(xray.NodeSpec) if !ok { log.Error().Msgf("No ref found on node %s", n.GetText()) return } - x.SetSelectedItem(ref.Path) + x.SetSelectedItem(spec.AsPath()) x.refreshActions() }) x.refreshActions() @@ -131,16 +133,18 @@ func (x *Xray) refreshActions() { x.Actions().Clear() x.bindKeys() + x.Tree.BindKeys() - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return } + gvr := spec.GVR() var err error - x.meta, err = dao.MetaAccess.MetaFor(client.NewGVR(ref.GVR)) + x.meta, err = dao.MetaAccess.MetaFor(client.NewGVR(gvr)) if err != nil { - log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err) + log.Warn().Msgf("NO meta for %q -- %s", gvr, err) return } @@ -155,22 +159,27 @@ func (x *Xray) refreshActions() { aa[ui.KeyD] = ui.NewKeyAction("Describe", x.describeCmd, true) } - if ref.GVR == "containers" { + switch gvr { + case "containers": + x.Actions().Delete(tcell.KeyEnter) + aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) + aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) + aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) + case "v1/pods": aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) aa[ui.KeyShiftL] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) } - x.Actions().Add(aa) } // GetSelectedPath returns the current selection as string. func (x *Xray) GetSelectedPath() string { - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return "" } - return ref.Path + return spec.Path() } func (x *Xray) selectedSpec() *xray.NodeSpec { @@ -193,6 +202,34 @@ func (x *Xray) EnvFn() EnvFunc { return x.envFn } +func (x *Xray) k9sEnv() K9sEnv { + env := generalEnv(x.app) + + spec := x.selectedSpec() + if spec == nil { + return env + } + + env["FILTER"] = x.CmdBuff().String() + if env["FILTER"] == "" { + ns, n := client.Namespaced(spec.Path()) + env["NAMESPACE"], env["FILTER"] = ns, n + } + + switch spec.GVR() { + case "containers": + _, co := client.Namespaced(spec.Path()) + env["CONTAINER"] = co + ns, n := client.Namespaced(*spec.ParentPath()) + env["NAMESPACE"], env["POD"], env["NAME"] = ns, n, co + default: + ns, n := client.Namespaced(spec.Path()) + env["NAMESPACE"], env["NAME"] = ns, n + } + + return env +} + // Aliases returns all available aliases. func (x *Xray) Aliases() []string { return append(x.meta.ShortNames, x.meta.SingularName, x.meta.Name) @@ -200,100 +237,98 @@ func (x *Xray) Aliases() []string { func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return nil } - if ref.Parent != nil { - x.showLogs(ref.Parent, ref, prev) - } else { - log.Error().Msgf("No parent found for container %q", ref.Path) - } + x.showLogs(spec, prev) return nil } } -func (x *Xray) showLogs(pod, co *xray.NodeSpec, prev bool) { +func (x *Xray) showLogs(spec *xray.NodeSpec, prev bool) { // Need to load and wait for pods - ns, _ := client.Namespaced(pod.Path) + path, co := spec.Path(), "" + if spec.GVR() == "containers" { + _, coName := client.Namespaced(spec.Path()) + path, co = *spec.ParentPath(), coName + } + + ns, _ := client.Namespaced(path) _, err := x.app.factory.CanForResource(ns, "v1/pods", client.MonitorAccess) if err != nil { x.app.Flash().Err(err) return } - if err := x.app.inject(NewLog(client.NewGVR(co.GVR), pod.Path, co.Path, prev)); err != nil { + if err := x.app.inject(NewLog(client.NewGVR("v1/pods"), path, co, prev)); err != nil { x.app.Flash().Err(err) } } func (x *Xray) shellCmd(evt *tcell.EventKey) *tcell.EventKey { - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return nil } - if ref.Status != "" { - x.app.Flash().Errf("%s is not in a running state", ref.Path) + if spec.Status() != "ok" { + x.app.Flash().Errf("%s is not in a running state", spec.Path()) return nil } - if ref.Parent != nil { - _, co := client.Namespaced(ref.Path) - x.shellIn(ref.Parent.Path, co) - } else { - log.Error().Msgf("No parent found on container node %q", ref.Path) + path, co := spec.Path(), "" + if spec.GVR() == "containers" { + _, co = client.Namespaced(spec.Path()) + path = *spec.ParentPath() + } + + if err := containerShellin(x.app, x, path, co); err != nil { + x.app.Flash().Err(err) } return nil } -func (x *Xray) shellIn(path, co string) { - x.Stop() - shellIn(x.app, path, co) - x.Start() -} - func (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey { - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return evt } ctx := x.defaultContext() - raw, err := x.model.ToYAML(ctx, ref.GVR, ref.Path) + raw, err := x.model.ToYAML(ctx, spec.GVR(), spec.Path()) if err != nil { - x.App().Flash().Errf("unable to get resource %q -- %s", ref.GVR, err) + x.App().Flash().Errf("unable to get resource %q -- %s", spec.GVR(), err) return nil } - details := NewDetails(x.app, "YAML", ref.Path, true).Update(raw) + details := NewDetails(x.app, "YAML", spec.Path(), true).Update(raw) if err := x.app.inject(details); err != nil { x.app.Flash().Err(err) } return nil - } func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return evt } x.Stop() defer x.Start() { - gvr := client.NewGVR(ref.GVR) + gvr := client.NewGVR(spec.GVR()) meta, err := dao.MetaAccess.MetaFor(gvr) if err != nil { - log.Warn().Msgf("NO meta for %q -- %s", ref.GVR, err) + log.Warn().Msgf("NO meta for %q -- %s", spec.GVR(), err) return nil } - x.resourceDelete(gvr, ref, fmt.Sprintf("Delete %s %s?", meta.SingularName, ref.Path)) + x.resourceDelete(gvr, spec, fmt.Sprintf("Delete %s %s?", meta.SingularName, spec.Path())) } return nil @@ -301,12 +336,12 @@ func (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { } func (x *Xray) describeCmd(evt *tcell.EventKey) *tcell.EventKey { - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return evt } - x.describe(ref.GVR, ref.Path) + x.describe(spec.GVR(), spec.Path()) return nil } @@ -328,18 +363,18 @@ func (x *Xray) describe(gvr, path string) { } func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey { - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return evt } x.Stop() defer x.Start() { - ns, n := client.Namespaced(ref.Path) + ns, n := client.Namespaced(spec.Path()) args := make([]string, 0, 10) args = append(args, "edit") - args = append(args, client.NewGVR(ref.GVR).R()) + args = append(args, client.NewGVR(spec.GVR()).R()) args = append(args, "-n", ns) args = append(args, "--context", x.app.Config.K9s.CurrentContext) if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { @@ -408,14 +443,15 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - ref := x.selectedSpec() - if ref == nil { + spec := x.selectedSpec() + if spec == nil { return nil } - if len(strings.Split(ref.Path, "/")) == 1 { + log.Debug().Msgf("SELECTED REF %#v", spec) + if len(strings.Split(spec.Path(), "/")) == 1 { return nil } - if err := x.app.viewResource(client.NewGVR(ref.GVR).R(), ref.Path, false); err != nil { + if err := x.app.viewResource(client.NewGVR(spec.GVR()).R(), spec.Path(), false); err != nil { x.app.Flash().Err(err) } @@ -464,13 +500,13 @@ func (x *Xray) update(node *xray.TreeNode) { x.hydrate(root, c) } if x.GetSelectedItem() == "" { - x.SetSelectedItem(node.ID) + x.SetSelectedItem(node.Spec().Path()) } x.app.QueueUpdateDraw(func() { x.SetRoot(root) root.Walk(func(node, parent *tview.TreeNode) bool { - ref, ok := node.GetReference().(xray.NodeSpec) + spec, ok := node.GetReference().(xray.NodeSpec) if !ok { log.Error().Msgf("Expecting a NodeSpec but got %T", node.GetReference()) return false @@ -482,7 +518,8 @@ func (x *Xray) update(node *xray.TreeNode) { node.SetExpanded(true) } - if ref.Path == x.GetSelectedItem() { + if spec.AsPath() == x.GetSelectedItem() { + log.Debug().Msgf("SEL %q--%q", spec.Path(), x.GetSelectedItem()) node.SetExpanded(true).SetSelectable(true) x.SetCurrentNode(node) } @@ -611,9 +648,9 @@ func (x *Xray) styleTitle() string { return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), x.app.Styles.Frame()) } -func (x *Xray) resourceDelete(gvr client.GVR, ref *xray.NodeSpec, msg string) { +func (x *Xray) resourceDelete(gvr client.GVR, spec *xray.NodeSpec, msg string) { dialog.ShowDelete(x.app.Content.Pages, msg, func(cascade, force bool) { - x.app.Flash().Infof("Delete resource %s %s", ref.GVR, ref.Path) + x.app.Flash().Infof("Delete resource %s %s", spec.GVR(), spec.Path()) accessor, err := dao.AccessorFor(x.app.factory, gvr) if err != nil { log.Error().Err(err).Msgf("No accessor") @@ -625,11 +662,11 @@ func (x *Xray) resourceDelete(gvr client.GVR, ref *xray.NodeSpec, msg string) { x.app.Flash().Errf("Invalid nuker %T", accessor) return } - if err := nuker.Delete(ref.Path, true, true); err != nil { + if err := nuker.Delete(spec.Path(), true, true); err != nil { x.app.Flash().Errf("Delete failed with `%s", err) } else { - x.app.Flash().Infof("%s `%s deleted successfully", x.GVR(), ref.Path) - x.app.factory.DeleteForwarder(ref.Path) + x.app.Flash().Infof("%s `%s deleted successfully", x.GVR(), spec.Path()) + x.app.factory.DeleteForwarder(spec.Path()) } x.Refresh() }, func() {}) @@ -661,15 +698,7 @@ func makeTreeNode(node *xray.TreeNode, expanded bool, styles *config.Styles) *tv n := tview.NewTreeNode("No data...") if node != nil { n.SetText(node.Title(styles.Xray())) - spec := xray.NodeSpec{} - if p := node.Parent; p != nil { - spec.GVR, spec.Path = p.GVR, p.ID - } - n.SetReference(xray.NodeSpec{ - GVR: node.GVR, - Path: node.ID, - Parent: &spec, - }) + n.SetReference(node.Spec()) } n.SetSelectable(true) n.SetExpanded(expanded) diff --git a/internal/xray/container.go b/internal/xray/container.go index ae1a8eb5..c4ba5ead 100644 --- a/internal/xray/container.go +++ b/internal/xray/container.go @@ -35,9 +35,9 @@ func (c *Container) Render(ctx context.Context, ns string, o interface{}) error } pns, _ := client.Namespaced(parent.ID) c.envRefs(f, root, pns, co.Container) - if !root.IsLeaf() { - parent.Add(root) - } + // if !root.IsLeaf() { + parent.Add(root) + // } return nil } diff --git a/internal/xray/container_test.go b/internal/xray/container_test.go index 31aca2a3..bf18164c 100644 --- a/internal/xray/container_test.go +++ b/internal/xray/container_test.go @@ -235,7 +235,7 @@ func makeDoubleCMKeysContainer(n string, optional bool) *v1.Container { } func load(t *testing.T, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n)) + raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured diff --git a/internal/xray/pod.go b/internal/xray/pod.go index 9a9e0cf6..434253a0 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -40,7 +40,6 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { if !ok { return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent)) } - parent.Add(node) if err := p.containerRefs(ctx, node, po.Namespace, po.Spec); err != nil { return err @@ -50,6 +49,14 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { return err } + gvr, nsID := "v1/namespaces", client.FQN(client.ClusterScope, po.Namespace) + nsn := parent.Find(gvr, nsID) + if nsn == nil { + nsn = NewTreeNode(gvr, nsID) + parent.Add(nsn) + } + nsn.Add(node) + return p.validate(node, po) } diff --git a/internal/xray/pod_test.go b/internal/xray/pod_test.go index ffa1fbc8..7d144304 100644 --- a/internal/xray/pod_test.go +++ b/internal/xray/pod_test.go @@ -12,27 +12,27 @@ import ( func TestPodRender(t *testing.T) { uu := map[string]struct { - file string - level1, level2 int - status string + file string + count, children int + status string }{ "plain": { - file: "po", - level1: 1, - level2: 3, - status: xray.OkStatus, + file: "po", + children: 1, + count: 7, + status: xray.OkStatus, }, "withInit": { - file: "init", - level1: 1, - level2: 2, - status: xray.OkStatus, + file: "init", + children: 1, + count: 7, + status: xray.OkStatus, }, "cilium": { - file: "cilium", - level1: 1, - level2: 3, - status: xray.OkStatus, + file: "cilium", + children: 1, + count: 8, + status: xray.OkStatus, }, } @@ -46,8 +46,8 @@ func TestPodRender(t *testing.T) { ctx = context.WithValue(ctx, internal.KeyFactory, makeFactory()) assert.Nil(t, re.Render(ctx, "", &render.PodWithMetrics{Raw: o})) - assert.Equal(t, u.level1, root.CountChildren()) - assert.Equal(t, u.level2, root.Children[0].CountChildren()) + assert.Equal(t, u.children, root.CountChildren()) + assert.Equal(t, u.count, root.Count("")) }) } } diff --git a/internal/xray/test_assets/cilium.json b/internal/xray/testdata/cilium.json similarity index 100% rename from internal/xray/test_assets/cilium.json rename to internal/xray/testdata/cilium.json diff --git a/internal/xray/test_assets/dp.json b/internal/xray/testdata/dp.json similarity index 100% rename from internal/xray/test_assets/dp.json rename to internal/xray/testdata/dp.json diff --git a/internal/xray/test_assets/ds.json b/internal/xray/testdata/ds.json similarity index 100% rename from internal/xray/test_assets/ds.json rename to internal/xray/testdata/ds.json diff --git a/internal/xray/test_assets/init.json b/internal/xray/testdata/init.json similarity index 100% rename from internal/xray/test_assets/init.json rename to internal/xray/testdata/init.json diff --git a/internal/xray/test_assets/ns.json b/internal/xray/testdata/ns.json similarity index 100% rename from internal/xray/test_assets/ns.json rename to internal/xray/testdata/ns.json diff --git a/internal/xray/test_assets/po.json b/internal/xray/testdata/po.json similarity index 100% rename from internal/xray/test_assets/po.json rename to internal/xray/testdata/po.json diff --git a/internal/xray/test_assets/rs.json b/internal/xray/testdata/rs.json similarity index 100% rename from internal/xray/test_assets/rs.json rename to internal/xray/testdata/rs.json diff --git a/internal/xray/test_assets/sa.json b/internal/xray/testdata/sa.json similarity index 100% rename from internal/xray/test_assets/sa.json rename to internal/xray/testdata/sa.json diff --git a/internal/xray/test_assets/sts.json b/internal/xray/testdata/sts.json similarity index 100% rename from internal/xray/test_assets/sts.json rename to internal/xray/testdata/sts.json diff --git a/internal/xray/test_assets/svc.json b/internal/xray/testdata/svc.json similarity index 100% rename from internal/xray/test_assets/svc.json rename to internal/xray/testdata/svc.json diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index bab59321..8825e408 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -52,8 +52,51 @@ type TreeRef string // NodeSpec represents a node resource specification. type NodeSpec struct { - GVR, Path, Status string - Parent *NodeSpec + GVRs, Paths, Statuses []string +} + +func (s NodeSpec) ParentGVR() *string { + if len(s.GVRs) > 1 { + return &s.GVRs[1] + } + return nil +} + +func (s NodeSpec) ParentPath() *string { + if len(s.Paths) > 1 { + return &s.Paths[1] + } + return nil +} + +// GVR returns the current GVR. +func (s NodeSpec) GVR() string { + return s.GVRs[0] +} + +// Path returns the current path. +func (s NodeSpec) Path() string { + return s.Paths[0] +} + +// Status returns the current status. +func (s NodeSpec) Status() string { + return s.Statuses[0] +} + +// AsPath returns path hierarchy as string. +func (s NodeSpec) AsPath() string { + return strings.Join(s.Paths, PathSeparator) +} + +// AsGVR returns a gvr hierarchy as string. +func (s NodeSpec) AsGVR() string { + return strings.Join(s.GVRs, PathSeparator) +} + +// AsStatus returns a status hierarchy as string. +func (s NodeSpec) AsStatus() string { + return strings.Join(s.Statuses, PathSeparator) } // ---------------------------------------------------------------------------- @@ -145,19 +188,17 @@ func (t *TreeNode) Sort() { // Spec returns this node specification. func (t *TreeNode) Spec() NodeSpec { - parent := t - var gvr, path, status []string - for parent != nil { - gvr = append(gvr, parent.GVR) - path = append(path, parent.ID) - status = append(status, parent.Extras[StatusKey]) - parent = parent.Parent + var GVRs, Paths, Statuses []string + for parent := t; parent != nil; parent = parent.Parent { + GVRs = append(GVRs, parent.GVR) + Paths = append(Paths, parent.ID) + Statuses = append(Statuses, parent.Extras[StatusKey]) } return NodeSpec{ - GVR: strings.Join(gvr, PathSeparator), - Path: strings.Join(path, PathSeparator), - Status: strings.Join(status, PathSeparator), + GVRs: GVRs, + Paths: Paths, + Statuses: Statuses, } } @@ -180,21 +221,18 @@ func (t *TreeNode) Blank() bool { } // Hydrate hydrates a full tree bases on a collection of specifications. -func Hydrate(refs []NodeSpec) *TreeNode { +func Hydrate(specs []NodeSpec) *TreeNode { root := NewTreeNode("", "") nav := root - for _, ref := range refs { - gvrs := strings.Split(ref.GVR, PathSeparator) - paths := strings.Split(ref.Path, PathSeparator) - statuses := strings.Split(ref.Status, PathSeparator) - for i := len(paths) - 1; i >= 0; i-- { + for _, spec := range specs { + for i := len(spec.Paths) - 1; i >= 0; i-- { if nav.Blank() { - nav.GVR, nav.ID, nav.Extras[StatusKey] = gvrs[i], paths[i], statuses[i] + nav.GVR, nav.ID, nav.Extras[StatusKey] = spec.GVRs[i], spec.Paths[i], spec.Statuses[i] continue } - c := NewTreeNode(gvrs[i], paths[i]) - c.Extras[StatusKey] = statuses[i] - if n := nav.Find(gvrs[i], paths[i]); n == nil { + c := NewTreeNode(spec.GVRs[i], spec.Paths[i]) + c.Extras[StatusKey] = spec.Statuses[i] + if n := nav.Find(spec.GVRs[i], spec.Paths[i]); n == nil { nav.Add(c) nav = c } else { @@ -260,7 +298,7 @@ func (t *TreeNode) Filter(q string, filter func(q, path string) bool) *TreeNode specs := t.Flatten() matches := make([]NodeSpec, 0, len(specs)) for _, s := range specs { - if filter(q, s.Path+s.Status) { + if filter(q, s.AsPath()+s.AsStatus()) { matches = append(matches, s) } } @@ -461,7 +499,7 @@ func toEmoji(gvr string) string { // EmojiInfo returns emoji help. func EmojiInfo() map[string]string { - gvrs := []string{ + GVRs := []string{ "containers", "v1/namespaces", "v1/pods", @@ -476,8 +514,8 @@ func EmojiInfo() map[string]string { "apps/v1/daemonsets", } - m := make(map[string]string, len(gvrs)) - for _, g := range gvrs { + m := make(map[string]string, len(GVRs)) + for _, g := range GVRs { m[client.NewGVR(g).R()] = toEmoji(g) } diff --git a/internal/xray/tree_node_test.go b/internal/xray/tree_node_test.go index 652bc7fd..fbce2e10 100644 --- a/internal/xray/tree_node_test.go +++ b/internal/xray/tree_node_test.go @@ -86,8 +86,8 @@ func TestTreeNodeFilter(t *testing.T) { } func TestTreeNodeHydrate(t *testing.T) { - threeOK := strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator) - fiveOK := strings.Join([]string{"ok", "ok", "ok", "ok", "ok"}, xray.PathSeparator) + threeOK := []string{"ok", "ok", "ok"} + fiveOK := append(threeOK, "ok", "ok") uu := map[string]struct { spec []xray.NodeSpec @@ -96,14 +96,14 @@ func TestTreeNodeHydrate(t *testing.T) { "flat_simple": { spec: []xray.NodeSpec{ { - GVR: "containers::v1/pods", - Path: "c1::default/p1", - Status: threeOK, + GVRs: []string{"containers", "v1/pods"}, + Paths: []string{"c1", "default/p1"}, + Statuses: threeOK, }, { - GVR: "containers::v1/pods", - Path: "c2::default/p1", - Status: threeOK, + GVRs: []string{"containers", "v1/pods"}, + Paths: []string{"c2", "default/p1"}, + Statuses: threeOK, }, }, e: root1(), @@ -111,14 +111,14 @@ func TestTreeNodeHydrate(t *testing.T) { "flat_complex": { spec: []xray.NodeSpec{ { - GVR: "v1/secrets::containers::v1/pods", - Path: "s1::c1::default/p1", - Status: threeOK, + GVRs: []string{"v1/secrets", "containers", "v1/pods"}, + Paths: []string{"s1", "c1", "default/p1"}, + Statuses: threeOK, }, { - GVR: "v1/secrets::containers::v1/pods", - Path: "s2::c2::default/p1", - Status: threeOK, + GVRs: []string{"v1/secrets", "containers", "v1/pods"}, + Paths: []string{"s2", "c2", "default/p1"}, + Statuses: threeOK, }, }, e: root2(), @@ -126,49 +126,49 @@ func TestTreeNodeHydrate(t *testing.T) { "complex1": { spec: []xray.NodeSpec{ { - GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "default/default-token-rr22g::default/nginx-6b866d578b-c6tcn::default/nginx::-/default::deployments", - Status: fiveOK, + GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"default/default-token-rr22g", "default/nginx-6b866d578b-c6tcn", "default/nginx", "-/default", "deployments"}, + Statuses: fiveOK, }, { - GVR: "v1/configmaps::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "kube-system/coredns::kube-system/coredns-6955765f44-89q2p::kube-system/coredns::-/kube-system::deployments", - Status: fiveOK, + GVRs: []string{"v1/configmaps", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"kube-system/coredns", "kube-system/coredns-6955765f44-89q2p", "kube-system/coredns", "-/kube-system", "deployments"}, + Statuses: fiveOK, }, { - GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "kube-system/coredns-token-5cq9j::kube-system/coredns-6955765f44-89q2p::kube-system/coredns::-/kube-system::deployments", - Status: fiveOK, + GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"kube-system/coredns-token-5cq9j", "kube-system/coredns-6955765f44-89q2p", "kube-system/coredns", "-/kube-system", "deployments"}, + Statuses: fiveOK, }, { - GVR: "v1/configmaps::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "kube-system/coredns::kube-system/coredns-6955765f44-r9j9t::kube-system/coredns::-/kube-system::deployments", - Status: fiveOK, + GVRs: []string{"v1/configmaps", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"kube-system/coredns", "kube-system/coredns-6955765f44-r9j9t", "kube-system/coredns", "-/kube-system", "deployments"}, + Statuses: fiveOK, }, { - GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "kube-system/coredns-token-5cq9j::kube-system/coredns-6955765f44-r9j9t::kube-system/coredns::-/kube-system::deployments", - Status: fiveOK, + GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"kube-system/coredns-token-5cq9j", "kube-system/coredns-6955765f44-r9j9t", "kube-system/coredns", "-/kube-system", "deployments"}, + Statuses: fiveOK, }, { - GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "kube-system/default-token-thzt8::kube-system/metrics-server-6754dbc9df-88bk4::kube-system/metrics-server::-/kube-system::deployments", - Status: fiveOK, + GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"kube-system/default-token-thzt8", "kube-system/metrics-server-6754dbc9df-88bk4", "kube-system/metrics-server", "-/kube-system", "deployments"}, + Statuses: fiveOK, }, { - GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "kube-system/nginx-ingress-token-kff5q::kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55::kube-system/nginx-ingress-controller::-/kube-system::deployments", - Status: fiveOK, + GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"kube-system/nginx-ingress-token-kff5q", "kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55", "kube-system/nginx-ingress-controller", "-/kube-system", "deployments"}, + Statuses: fiveOK, }, { - GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4::kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56::kubernetes-dashboard/dashboard-metrics-scraper::-/kubernetes-dashboard::deployments", - Status: fiveOK, + GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4", "kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56", "kubernetes-dashboard/dashboard-metrics-scraper", "-/kubernetes-dashboard", "deployments"}, + Statuses: fiveOK, }, { - GVR: "v1/secrets::v1/pods::apps/v1/deployments::v1/namespaces::apps/v1/deployments", - Path: "kubernetes-dashboard/kubernetes-dashboard-token-d6rt4::kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d::kubernetes-dashboard/kubernetes-dashboard::-/kubernetes-dashboard::deployments", - Status: fiveOK, + GVRs: []string{"v1/secrets", "v1/pods", "apps/v1/deployments", "v1/namespaces", "apps/v1/deployments"}, + Paths: []string{"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4", "kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d", "kubernetes-dashboard/kubernetes-dashboard", "-/kubernetes-dashboard", "deployments"}, + Statuses: fiveOK, }, }, e: root3(), @@ -193,14 +193,14 @@ func TestTreeNodeFlatten(t *testing.T) { root: root1(), e: []xray.NodeSpec{ { - GVR: "containers::v1/pods", - Path: "c1::default/p1", - Status: strings.Join([]string{"ok", "ok"}, xray.PathSeparator), + GVRs: []string{"containers", "v1/pods"}, + Paths: []string{"c1", "default/p1"}, + Statuses: []string{"ok", "ok"}, }, { - GVR: "containers::v1/pods", - Path: "c2::default/p1", - Status: strings.Join([]string{"ok", "ok"}, xray.PathSeparator), + GVRs: []string{"containers", "v1/pods"}, + Paths: []string{"c2", "default/p1"}, + Statuses: []string{"ok", "ok"}, }, }, }, @@ -208,14 +208,14 @@ func TestTreeNodeFlatten(t *testing.T) { root: root2(), e: []xray.NodeSpec{ { - GVR: "v1/secrets::containers::v1/pods", - Path: "s1::c1::default/p1", - Status: strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator), + GVRs: []string{"v1/secrets", "containers", "v1/pods"}, + Paths: []string{"s1", "c1", "default/p1"}, + Statuses: []string{"ok", "ok", "ok"}, }, { - GVR: "v1/secrets::containers::v1/pods", - Path: "s2::c2::default/p1", - Status: strings.Join([]string{"ok", "ok", "ok"}, xray.PathSeparator), + GVRs: []string{"v1/secrets", "containers", "v1/pods"}, + Paths: []string{"s2", "c2", "default/p1"}, + Statuses: []string{"ok", "ok", "ok"}, }, }, }, @@ -323,18 +323,18 @@ func diff1() *xray.TreeNode { } func root2() *xray.TreeNode { - n := xray.NewTreeNode("v1/pods", "default/p1") c1 := xray.NewTreeNode("containers", "c1") - c2 := xray.NewTreeNode("containers", "c2") - n.Add(c1) - n.Add(c2) - s1 := xray.NewTreeNode("v1/secrets", "s1") c1.Add(s1) + c2 := xray.NewTreeNode("containers", "c2") s2 := xray.NewTreeNode("v1/secrets", "s2") c2.Add(s2) + n := xray.NewTreeNode("v1/pods", "default/p1") + n.Add(c1) + n.Add(c2) + return n } From 7000b93d4e64fadd10ee2c04c1fa5498a4d1a7a6 Mon Sep 17 00:00:00 2001 From: derailed Date: Tue, 11 Feb 2020 23:44:49 -0700 Subject: [PATCH 22/39] enhanced xray pod view and add support for transparent background --- README.md | 8 +-- go.mod | 2 +- go.sum | 2 + internal/config/styles.go | 6 ++ internal/render/helpers.go | 2 +- internal/render/helpers_test.go | 2 +- internal/ui/config.go | 3 + internal/ui/menu.go | 4 +- internal/ui/menu_test.go | 6 +- internal/ui/splash.go | 4 +- internal/ui/table_helper.go | 11 +++- internal/view/app.go | 2 +- internal/view/cluster_info.go | 12 +++- skins/black_and_wtf.yml | 92 ++++++++++++++++------------ skins/dracula.yml | 3 + skins/in_the_navy.yml | 104 +++++++++++++++++++------------- 16 files changed, 161 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 038f0ccb..114ff127 100644 --- a/README.md +++ b/README.md @@ -454,7 +454,7 @@ You can style K9s based on your own sense of look and style. Skins are YAML file You can also change K9s skins based on the cluster you are connecting too. In this case, you can specify the skin file name as `$HOME/.k9s/mycluster_skin.yml` Below is a sample skin file, more skins are available in the skins directory in this repo, just simply copy any of these in your user's home dir as `skin.yml`. -Colors can be defined by name or uing an hex representation. +Colors can be defined by name or uing an hex representation. Of recent, we've added a color named `default` to indicate a transparent background color to preserve your terminal background color settings if so desired. > NOTE: This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly! @@ -464,8 +464,8 @@ k9s: # General K9s styles body: fgColor: dodgerblue - bgColor: #ffffff - logoColor: #0000ff + bgColor: '#ffffff' + logoColor: '#0000ff' # ClusterInfoView styles. info: fgColor: lightskyblue @@ -488,7 +488,7 @@ k9s: activeColor: skyblue # Resource status and update styles status: - newColor: #00ff00 + newColor: '#00ff00' modifyColor: powderblue addColor: lightskyblue errorColor: indianred diff --git a/go.mod b/go.mod index b8356134..a42b7e03 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( fyne.io/fyne v1.2.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/atotto/clipboard v0.1.2 - github.com/derailed/tview v0.3.3 + github.com/derailed/tview v0.3.4 github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/fatih/color v1.6.0 diff --git a/go.sum b/go.sum index 824aab0a..847d5a31 100644 --- a/go.sum +++ b/go.sum @@ -147,6 +147,8 @@ github.com/deislabs/oras v0.7.0 h1:RnDoFd3tQYODMiUqxgQ8JxlrlWL0/VMKIKRD01MmNYk= github.com/deislabs/oras v0.7.0/go.mod h1:sqMKPG3tMyIX9xwXUBRLhZ24o+uT4y6jgBD2RzUTKDM= github.com/derailed/tview v0.3.3 h1:tipPwxcDhx0zRBZuc8VKIrNgWL40FL5JeF/30XVieUE= github.com/derailed/tview v0.3.3/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= +github.com/derailed/tview v0.3.4 h1:PnF64fLqm48LEjC/XwOS7JufDgFuuPYx85YVt5t3rwE= +github.com/derailed/tview v0.3.4/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= diff --git a/internal/config/styles.go b/internal/config/styles.go index ef7596bd..854ad73b 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -292,6 +292,11 @@ func NewStyles() *Styles { } } +// LoadDefaults loads the default skin +func (s *Styles) DefaultSkins() { + s.K9s = newStyle() +} + // FgColor returns the foreground color. func (s *Styles) FgColor() tcell.Color { return AsColor(s.Body().FgColor) @@ -385,6 +390,7 @@ func (s *Styles) Update() { tview.Styles.PrimaryTextColor = s.FgColor() tview.Styles.BorderColor = AsColor(s.K9s.Frame.Border.FgColor) tview.Styles.FocusColor = AsColor(s.K9s.Frame.Border.FocusColor) + s.fireStylesChanged() } // AsColor checks color index, if match return color otherwise pink it is. diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 33996772..a598065d 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -151,7 +151,7 @@ func Truncate(str string, width int) string { func mapToStr(m map[string]string) (s string) { if len(m) == 0 { - return MissingValue + return "" } kk := make([]string, 0, len(m)) diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index b45edd36..ed366a46 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -305,7 +305,7 @@ func TestMapToStr(t *testing.T) { e string }{ {map[string]string{"blee": "duh", "aa": "bb"}, "aa=bb,blee=duh"}, - {map[string]string{}, MissingValue}, + {map[string]string{}, ""}, } for _, u := range uu { assert.Equal(t, u.e, mapToStr(u.i)) diff --git a/internal/ui/config.go b/internal/ui/config.go index cc4f69e3..96080c3d 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -103,6 +103,9 @@ func (c *Configurator) RefreshStyles(context string) { func (c *Configurator) updateStyles(f string) { c.skinFile = f + if !c.HasSkins() { + c.Styles.DefaultSkins() + } c.Styles.Update() render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor) diff --git a/internal/ui/menu.go b/internal/ui/menu.go index 55184944..ef44f60a 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -15,7 +15,7 @@ import ( ) const ( - menuIndexFmt = " [key:bg:b]<%d> [fg:bg:d]%s " + menuIndexFmt = " [key:-:b]<%d> [fg:-:d]%s " maxRows = 7 ) @@ -195,7 +195,7 @@ func formatNSMenu(i int, name string, styles config.Frame) string { } func formatPlainMenu(h model.MenuHint, size int, styles config.Frame) string { - menuFmt := " [key:bg:b]%-" + strconv.Itoa(size+2) + "s [fg:bg:d]%s " + menuFmt := " [key:-:b]%-" + strconv.Itoa(size+2) + "s [fg:-:d]%s " fmat := strings.Replace(menuFmt, "[key", "["+styles.Menu.KeyColor, 1) fmat = strings.Replace(fmat, "[fg", "["+styles.Menu.FgColor, 1) fmat = strings.Replace(fmat, ":bg:", ":"+styles.Title.BgColor+":", -1) diff --git a/internal/ui/menu_test.go b/internal/ui/menu_test.go index ead592ca..51511448 100644 --- a/internal/ui/menu_test.go +++ b/internal/ui/menu_test.go @@ -18,9 +18,9 @@ func TestNewMenu(t *testing.T) { {Mnemonic: "0", Description: "zero", Visible: true}, }) - assert.Equal(t, " [fuchsia:black:b]<0> [white:black:d]zero ", v.GetCell(0, 0).Text) - assert.Equal(t, " [dodgerblue:black:b] [white:black:d]bleeA ", v.GetCell(0, 1).Text) - assert.Equal(t, " [dodgerblue:black:b] [white:black:d]bleeB ", v.GetCell(1, 1).Text) + assert.Equal(t, " [fuchsia:-:b]<0> [white:-:d]zero ", v.GetCell(0, 0).Text) + assert.Equal(t, " [dodgerblue:-:b] [white:-:d]bleeA ", v.GetCell(0, 1).Text) + assert.Equal(t, " [dodgerblue:-:b] [white:-:d]bleeB ", v.GetCell(1, 1).Text) } func TestActionHints(t *testing.T) { diff --git a/internal/ui/splash.go b/internal/ui/splash.go index 3c43d318..ba44f209 100644 --- a/internal/ui/splash.go +++ b/internal/ui/splash.go @@ -6,7 +6,6 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" - "github.com/gdamore/tcell" ) // LogoSmall K9s small log. @@ -37,16 +36,15 @@ type Splash struct { // NewSplash instantiates a new splash screen with product and company info. func NewSplash(styles *config.Styles, version string) *Splash { s := Splash{Flex: tview.NewFlex()} + s.SetBackgroundColor(styles.BgColor()) logo := tview.NewTextView() logo.SetDynamicColors(true) - logo.SetBackgroundColor(tcell.ColorDefault) logo.SetTextAlign(tview.AlignCenter) s.layoutLogo(logo, styles) vers := tview.NewTextView() vers.SetDynamicColors(true) - vers.SetBackgroundColor(tcell.ColorDefault) vers.SetTextAlign(tview.AlignCenter) s.layoutRev(vers, version, styles) diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index a8f271a1..c3d5b0ef 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -14,6 +14,9 @@ import ( ) const ( + // DefaultColorName indicator to keep term colors. + DefaultColorName = "default" + // SearchFmt represents a filter view title. SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> " @@ -81,12 +84,16 @@ func TrimLabelSelector(s string) string { // SkinTitle decorates a title. func SkinTitle(fmat string, style config.Frame) string { - fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor+":"+style.Title.BgColor, -1) + bgColor := style.Title.BgColor + if bgColor == "default" { + bgColor = "-" + } + fmat = strings.Replace(fmat, "[fg:bg", "["+style.Title.FgColor+":"+bgColor, -1) fmat = strings.Replace(fmat, "[hilite", "["+style.Title.HighlightColor, 1) fmat = strings.Replace(fmat, "[key", "["+style.Menu.NumKeyColor, 1) fmat = strings.Replace(fmat, "[filter", "["+style.Title.FilterColor, 1) fmat = strings.Replace(fmat, "[count", "["+style.Title.CounterColor, 1) - fmat = strings.Replace(fmat, ":bg:", ":"+style.Title.BgColor+":", -1) + fmat = strings.Replace(fmat, ":bg:", ":"+bgColor+":", -1) return fmat } diff --git a/internal/view/app.go b/internal/view/app.go index aded6e6f..bfff02c9 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -267,11 +267,11 @@ func (a *App) switchCtx(name string, loadPods bool) error { log.Error().Err(err).Msg("Config save failed!") } a.Flash().Infof("Switching context to %s", name) + a.ReloadStyles(name) if err := a.gotoResource("pods", true); loadPods && err != nil { a.Flash().Err(err) } a.clusterModel.Reset(a.factory) - a.ReloadStyles(name) } return nil diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index c69a94a2..dc87cfa5 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -32,6 +32,7 @@ func NewClusterInfo(app *App) *ClusterInfo { func (c *ClusterInfo) Init() { c.app.Styles.AddListener(c) c.layout() + c.StylesChanged(c.app.Styles) } // StylesChanged notifies skin changed. @@ -51,9 +52,14 @@ func (c *ClusterInfo) layout() { func (c *ClusterInfo) sectionCell(t string) *tview.TableCell { cell := tview.NewTableCell(t + ":") cell.SetAlign(tview.AlignLeft) - var s tcell.Style - cell.SetStyle(s.Bold(true).Foreground(config.AsColor(c.styles.K9s.Info.SectionColor))) - cell.SetBackgroundColor(c.app.Styles.BgColor()) + // var style tcell.Style + // style.Bold(true). + // Background(tcell.ColorGreen). + // Foreground(config.AsColor(c.styles.K9s.Info.SectionColor)) + // cell.SetStyle(style) + // cell.SetBackgroundColor(c.app.Styles.BgColor()) + // cell.SetBackgroundColor(tcell.ColorDefault) + cell.SetBackgroundColor(tcell.ColorGreen) return cell } diff --git a/skins/black_and_wtf.yml b/skins/black_and_wtf.yml index a31da501..73fe07d8 100644 --- a/skins/black_and_wtf.yml +++ b/skins/black_and_wtf.yml @@ -1,56 +1,70 @@ +# Styles... +fg: &fg "white" +bg: &bg "black" +mark: &mark "darkgoldenrod" +active: &active "dimgray" +text: &text "navajowhite" +white: &white "whitesmoke" +ghost: &ghost "ghostwhite" +dslate: &dslate "darkslategray" +err: &err "pink" +slate: &slate "slategray" +gray: &gray "gray" + +# Skin k9s: body: - fgColor: white - bgColor: black - logoColor: white + fgColor: *fg + bgColor: *bg + logoColor: *fg info: - fgColor: navajowhite - sectionColor: white + fgColor: *text + sectionColor: *fg frame: border: - fgColor: white - focusColor: white + fgColor: *fg + focusColor: *fg menu: - fgColor: white - keyColor: white - numKeyColor: navajowhite + fgColor: *fg + keyColor: *fg + numKeyColor: *text crumbs: - fgColor: black - bgColor: navajowhite - activeColor: whitesmoke + fgColor: *fg + bgColor: *bg + activeColor: *active status: - newColor: ghostwhite - modifyColor: navajowhite - addColor: darkslategray - errorColor: whitesmoke - highlightcolor: dimgray - killColor: slategray - completedColor: gray + newColor: *white + modifyColor: *text + addColor: *ghost + errorColor: *err + highlightcolor: *dslate + killColor: *slate + completedColor: *gray title: - fgColor: ghostwhite - highlightColor: navajowhite - counterColor: navajowhite - filterColor: slategray + fgColor: *fg + highlightColor: *active + counterColor: *text + filterColor: *slate table: - fgColor: white - bgColor: black - cursorColor: white - markColor: darkgoldenrod + fgColor: *fg + bgColor: *bg + cursorColor: *fg + markColor: *mark header: - fgColor: darkgray - bgColor: black - sorterColor: white + fgColor: *dslate + bgColor: *bg + sorterColor: *fg xray: - fgColor: white - bgColor: black - cursorColor: whitesmoke + fgColor: *fg + bgColor: *bg + cursorColor: *ghost graphicColor: gray showIcons: false views: yaml: - keyColor: ghostwhite - colorColor: slategray - valueColor: navajowhite + keyColor: *ghost + colorColor: *slate + valueColor: *text logs: - fgColor: ghostwhite - bgColor: black + fgColor: *ghost + bgColor: *bg diff --git a/skins/dracula.yml b/skins/dracula.yml index 8905eb6b..801b2250 100644 --- a/skins/dracula.yml +++ b/skins/dracula.yml @@ -1,3 +1,4 @@ +# Styles... foreground: &foreground "#f8f8f2" background: &background "#282a36" current_line: ¤t_line "#44475a" @@ -10,6 +11,8 @@ pink: &pink "#ff79c6" purple: &purple "#bd93f9" red: &red "#ff5555" yellow: &yellow "#f1fa8c" + +# Skin... k9s: # General K9s styles body: diff --git a/skins/in_the_navy.yml b/skins/in_the_navy.yml index 382a3f95..3a85f1bb 100644 --- a/skins/in_the_navy.yml +++ b/skins/in_the_navy.yml @@ -1,58 +1,78 @@ +# Styles... + +fg: &fg "dodgerblue" +bg: &bg "white" +blue: &blue "blue" +sky: &sky "lightskyblue" +steel: &steel "steelblue" +dark: &dark "darkblue" +alice: &alice "aliceblue" +corn: &corn "cornflowerblue" +err: &err "indianred" +royal: &royal "royalblue" +slate: &slate "slategray" +gray: &gray "gray" +cadet: &cadet "cadetblue" +powder: &powder "powderblue" +aqua: &aqua "aqua" +mslate: &mslate "mediumslateblue" + +# Skin... k9s: body: - fgColor: dodgerblue - bgColor: white - logoColor: blue + fgColor: *fg + bgColor: *bg + logoColor: *blue info: - fgColor: lightskyblue - sectionColor: steelblue + fgColor: *sky + sectionColor: *steel frame: border: - fgColor: dodgerblue - bgColor: darkblue - focusColor: aliceblue + fgColor: *fg + bgColor: *dark + focusColor: *alice menu: - fgColor: darkblue - keyColor: cornflowerblue - numKeyColor: cadetblue + fgColor: *dark + keyColor: *corn + numKeyColor: *cadet crumbs: - fgColor: white - bgColor: steelblue - activeColor: skyblue + fgColor: *bg + bgColor: *steel + activeColor: *sky status: - newColor: blue - modifyColor: powderblue - addColor: lightskyblue - errorColor: indianred - highlightcolor: royalblue - killColor: slategray - completedColor: gray + newColor: *blue + modifyColor: *powder + addColor: *sky + errorColor: *err + highlightcolor: *royal + killColor: *slate + completedColor: *gray title: - fgColor: aqua - bgColor: darkblue - highlightColor: skyblue - counterColor: slateblue - filterColor: slategray + fgColor: *cadet + bgColor: *bg + highlightColor: *sky + counterColor: *slate + filterColor: *slate table: - fgColor: blue - bgColor: darkblue - cursorColor: aqua - markColor: mediumslateblue + fgColor: *fg + bgColor: *bg + cursorColor: *aqua + markColor: *mslate header: - fgColor: white - bgColor: darkblue - sorterColor: orange + fgColor: *fg + bgColor: *bg + sorterColor: *cadet xray: - fgColor: blue - bgColor: darkblue - cursorColor: aqua - graphicColor: mediumslateblue + fgColor: *blue + bgColor: *dark + cursorColor: *aqua + graphicColor: *mslate showIcons: false views: yaml: - keyColor: steelblue - colorColor: blue - valueColor: royalblue + keyColor: *steel + colorColor: *blue + valueColor: *royal logs: - fgColor: white - bgColor: darkblue + fgColor: *dark + bgColor: *bg From a491f7c77ef622c0fe4f39488f426013ea6a1f8f Mon Sep 17 00:00:00 2001 From: derailed Date: Wed, 12 Feb 2020 08:33:56 -0700 Subject: [PATCH 23/39] fix race conditions --- internal/dao/table.go | 1 - internal/model/table.go | 8 ++------ internal/render/row.go | 5 ++--- internal/render/table_data.go | 10 +++++----- internal/ui/flash.go | 3 +-- internal/ui/logo.go | 5 +---- internal/view/app.go | 21 ++++++++++++--------- internal/view/xray.go | 2 ++ internal/watch/factory.go | 4 ++++ 9 files changed, 29 insertions(+), 30 deletions(-) diff --git a/internal/dao/table.go b/internal/dao/table.go index 3abdf94e..944a1ef9 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -46,7 +46,6 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { // List all Resources in a given namespace. func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { - log.Debug().Msgf("TABLE-LIST %q:%q", ns, t.gvr) a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) _, codec := t.codec() diff --git a/internal/model/table.go b/internal/model/table.go index 2fbb6376..e54decbf 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -2,7 +2,6 @@ package model import ( "context" - "errors" "fmt" "sync" "sync/atomic" @@ -186,7 +185,6 @@ func (t *Table) updater(ctx context.Context) { for { select { case <-ctx.Done(): - t.fireTableLoadFailed(errors.New("operation canceled")) return case <-time.After(rate): rate = t.refreshRate @@ -207,7 +205,7 @@ func (t *Table) refresh(ctx context.Context) { t.fireTableLoadFailed(err) return } - t.fireTableChanged() + t.fireTableChanged(t.Peek()) } func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) { @@ -261,7 +259,6 @@ func (t *Table) reconcile(ctx context.Context) error { t.mx.Lock() defer t.mx.Unlock() - // if labelSelector in place might as well clear the model data. sel, ok := ctx.Value(internal.KeyLabels).(string) if ok && sel != "" { @@ -300,8 +297,7 @@ func (t *Table) resourceMeta() ResourceMeta { return meta } -func (t *Table) fireTableChanged() { - data := t.Peek() +func (t *Table) fireTableChanged(data render.TableData) { for _, l := range t.listeners { l.TableDataChanged(data) } diff --git a/internal/render/row.go b/internal/render/row.go index f3e4171e..4076b866 100644 --- a/internal/render/row.go +++ b/internal/render/row.go @@ -13,9 +13,8 @@ type Fields []string // Clone returns a copy of the fields. func (f Fields) Clone() Fields { cp := make(Fields, len(f)) - for i, v := range f { - cp[i] = v - } + copy(cp, f) + return cp } diff --git a/internal/render/table_data.go b/internal/render/table_data.go index d907277a..4a133a53 100644 --- a/internal/render/table_data.go +++ b/internal/render/table_data.go @@ -19,11 +19,11 @@ func (t *TableData) Clear() { // Clone returns a copy of the table func (t *TableData) Clone() TableData { - return cloneTable(*t) -} - -func cloneTable(t TableData) TableData { - return t + return TableData{ + Header: t.Header.Clone(), + RowEvents: t.RowEvents.Clone(), + Namespace: t.Namespace, + } } // SetHeader sets table header. diff --git a/internal/ui/flash.go b/internal/ui/flash.go index 855bba3c..956cb893 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -134,8 +134,7 @@ func (f *Flash) SetMessage(level FlashLevel, msg ...string) { } var ctx context.Context - ctx, f.cancel = context.WithCancel(context.TODO()) - ctx, f.cancel = context.WithTimeout(ctx, flashDelay) + ctx, f.cancel = context.WithTimeout(context.Background(), flashDelay) go f.refresh(ctx) } diff --git a/internal/ui/logo.go b/internal/ui/logo.go index 0935de22..e5f2ec9c 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -3,8 +3,6 @@ package ui import ( "fmt" - "github.com/gdamore/tcell" - "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" ) @@ -29,6 +27,7 @@ func NewLogo(styles *config.Styles) *Logo { l.AddItem(l.logo, 0, 6, false) l.AddItem(l.status, 0, 1, false) l.refreshLogo(styles.Body().LogoColor) + l.SetBackgroundColor(styles.BgColor()) styles.AddListener(&l) return &l @@ -96,7 +95,6 @@ func (l *Logo) refreshLogo(c string) { func logo() *tview.TextView { v := tview.NewTextView() - v.SetBackgroundColor(tcell.ColorDefault) v.SetWordWrap(false) v.SetWrap(false) v.SetTextAlign(tview.AlignLeft) @@ -107,7 +105,6 @@ func logo() *tview.TextView { func status() *tview.TextView { v := tview.NewTextView() - v.SetBackgroundColor(tcell.ColorDefault) v.SetWordWrap(false) v.SetWrap(false) v.SetTextAlign(tview.AlignCenter) diff --git a/internal/view/app.go b/internal/view/app.go index bfff02c9..96c96e0b 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -169,6 +169,7 @@ func (a *App) buildHeader() tview.Primitive { func (a *App) Halt() { if a.cancelFn != nil { a.cancelFn() + a.cancelFn = nil } } @@ -311,19 +312,21 @@ func (a *App) Run() error { // Status reports a new app status for display. func (a *App) Status(l ui.FlashLevel, msg string) { - a.Flash().SetMessage(l, msg) - a.setIndicator(l, msg) - a.setLogo(l, msg) - a.Draw() + a.QueueUpdateDraw(func() { + a.Flash().SetMessage(l, msg) + a.setIndicator(l, msg) + a.setLogo(l, msg) + }) } // ClearStatus reset logo back to normal. func (a *App) ClearStatus(flash bool) { - a.Logo().Reset() - if flash { - a.Flash().Clear() - } - a.Draw() + a.QueueUpdateDraw(func() { + a.Logo().Reset() + if flash { + a.Flash().Clear() + } + }) } func (a *App) setLogo(l ui.FlashLevel, msg string) { diff --git a/internal/view/xray.go b/internal/view/xray.go index 8d0841e9..2858233a 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -160,6 +160,8 @@ func (x *Xray) refreshActions() { } switch gvr { + case "v1/namespaces": + x.Actions().Delete(tcell.KeyEnter) case "containers": x.Actions().Delete(tcell.KeyEnter) aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 8b1d9849..4a56f178 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -109,10 +109,14 @@ func (f *Factory) waitForCacheSync(ns string) { if f.isClusterWide() { ns = client.AllNamespaces } + + f.mx.RLock() + defer f.mx.RUnlock() fac, ok := f.factories[ns] if !ok { return } + // Hang for a sec for the cache to refresh if still not done bail out! c := make(chan struct{}) go func(c chan struct{}) { From 757479e281db4449f83ac50b702ddbad6c88e3ca Mon Sep 17 00:00:00 2001 From: derailed Date: Wed, 12 Feb 2020 09:07:20 -0700 Subject: [PATCH 24/39] add rel notes --- change_logs/release_v0.14.1.md | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 change_logs/release_v0.14.1.md diff --git a/change_logs/release_v0.14.1.md b/change_logs/release_v0.14.1.md new file mode 100644 index 00000000..584de21f --- /dev/null +++ b/change_logs/release_v0.14.1.md @@ -0,0 +1,41 @@ + + +# Release v0.14.1 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## Term Color Part Duh! + +Some folks had reported issues with skins and wanting to preserve their terminal background colors while in K9s. In this drop, we're introducing a new skin setting called `default` that should enable the skin to keep the original terminal background color. Here is a sample skin snippet that should achieve just that: + +```yaml +# .k9s/pale_rider.yml + +# Styles... +fg: &fg "#ff00ff" +bg: &bg "default" # default keeps your current terminal window background color. + +# Skin... +k9s: + body: + fgColor: *fg + bgColor: "default" +#... +``` + +## Resolved Bugs/Features/PRs + +* [Issue #539](https://github.com/derailed/k9s/issues/539) +* [Issue #538](https://github.com/derailed/k9s/issues/538) Fingers crossed! + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) From e43260ed054af4512c95425bdf7b8cb526bc5583 Mon Sep 17 00:00:00 2001 From: derailed Date: Wed, 12 Feb 2020 09:32:32 -0700 Subject: [PATCH 25/39] cleaning up --- internal/config/styles.go | 4 ++-- internal/ui/config.go | 10 +++++----- internal/ui/config_test.go | 2 +- internal/ui/tree.go | 1 + internal/xray/tree_node.go | 2 ++ 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/internal/config/styles.go b/internal/config/styles.go index 854ad73b..97fe0dbd 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -292,8 +292,8 @@ func NewStyles() *Styles { } } -// LoadDefaults loads the default skin -func (s *Styles) DefaultSkins() { +// DefaultSkin loads the default skin +func (s *Styles) DefaultSkin() { s.K9s = newStyle() } diff --git a/internal/ui/config.go b/internal/ui/config.go index 96080c3d..71771745 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -26,14 +26,14 @@ type Configurator struct { Bench *config.Bench } -// HasSkins returns true if a skin file was located. -func (c *Configurator) HasSkins() bool { +// HasSkin returns true if a skin file was located. +func (c *Configurator) HasSkin() bool { return c.skinFile != "" } // StylesUpdater watches for skin file changes. func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error { - if !c.HasSkins() { + if !c.HasSkin() { return nil } @@ -103,8 +103,8 @@ func (c *Configurator) RefreshStyles(context string) { func (c *Configurator) updateStyles(f string) { c.skinFile = f - if !c.HasSkins() { - c.Styles.DefaultSkins() + if !c.HasSkin() { + c.Styles.DefaultSkin() } c.Styles.Update() diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index 0c409721..6436057e 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -22,7 +22,7 @@ func TestConfiguratorRefreshStyle(t *testing.T) { cfg := ui.Configurator{} cfg.RefreshStyles("") - assert.True(t, cfg.HasSkins()) + assert.True(t, cfg.HasSkin()) assert.Equal(t, tcell.ColorGhostWhite, render.StdColor) assert.Equal(t, tcell.ColorWhiteSmoke, render.ErrColor) } diff --git a/internal/ui/tree.go b/internal/ui/tree.go index 8179418c..1e0971b0 100644 --- a/internal/ui/tree.go +++ b/internal/ui/tree.go @@ -86,6 +86,7 @@ func (t *Tree) ExtraHints() map[string]string { return nil } +// BindKeys binds default mnemonics. func (t *Tree) BindKeys() { t.Actions().Add(KeyActions{ KeySpace: NewKeyAction("Expand/Collapse", t.noopCmd, true), diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index 8825e408..5d2d8577 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -55,6 +55,7 @@ type NodeSpec struct { GVRs, Paths, Statuses []string } +// ParentGVR returns the parent GVR. func (s NodeSpec) ParentGVR() *string { if len(s.GVRs) > 1 { return &s.GVRs[1] @@ -62,6 +63,7 @@ func (s NodeSpec) ParentGVR() *string { return nil } +// ParentPath returns the parent path. func (s NodeSpec) ParentPath() *string { if len(s.Paths) > 1 { return &s.Paths[1] From 7ad07b791d15e2224ad1dcbf096fc7c8d04f76b7 Mon Sep 17 00:00:00 2001 From: derailed Date: Wed, 12 Feb 2020 16:43:22 -0700 Subject: [PATCH 26/39] fix #542, #541 --- .gitignore | 5 +- README.md | 108 ++++++++++++++-------------- go.mod | 5 ++ go.sum | 14 ++++ internal/dao/container.go | 2 +- internal/dao/crd.go | 6 +- internal/dao/dp.go | 2 +- internal/dao/ds.go | 11 ++- internal/dao/generic.go | 2 +- internal/dao/job.go | 2 +- internal/dao/log_options.go | 21 ++++-- internal/dao/pod.go | 34 +++++---- internal/dao/sts.go | 2 +- internal/dao/svc.go | 2 +- internal/dao/table.go | 9 ++- internal/dao/types.go | 2 +- internal/model/cluster_info_test.go | 46 ++++++++++++ internal/model/log.go | 50 ++++++------- internal/model/log_test.go | 20 +++--- internal/render/job.go | 2 - internal/view/app.go | 3 +- internal/view/log.go | 6 +- 22 files changed, 217 insertions(+), 137 deletions(-) create mode 100644 internal/model/cluster_info_test.go diff --git a/.gitignore b/.gitignore index b8428f68..cd5193e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .vscode .idea -k9s.log .envrc cov.out execs @@ -10,10 +9,8 @@ dist notes vendor go.mod1 -popeye1.go gen.sh -cluster_info_test.go *.test *.log *~ -pod1.go \ No newline at end of file +faas diff --git a/README.md b/README.md index 114ff127..6a83473e 100644 --- a/README.md +++ b/README.md @@ -145,16 +145,16 @@ K9s uses aliases to navigate most K8s resources. --- -## K9s config file ($HOME/.k9s/config.yml) +## K9s Configuration - K9s keeps its configurations in a .k9s directory in your home directory. + K9s keeps its configurations in a .k9s directory in your home directory `$HOME/.k9s/config.yml`. > NOTE: This is still in flux and will change while in pre-release stage! ```yaml # config.yml k9s: - # Indicates api-server poll intervals. + # Represents ui poll intervals. refreshRate: 2 # Indicates whether modification commands like delete/kill/edit are disabled. Default is false readOnly: false @@ -189,9 +189,9 @@ K9s uses aliases to navigate most K8s resources. --- -## Aliases +## Command Aliases -In K9s, you can define your own command aliases (shortnames) to access your resources. In your `$HOME/.k9s` define a file called `alias.yml`. A K9s alias defines pairs of alias:gvr. A gvr represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file: +In K9s, you can define your very own command aliases (shortnames) to access your resources. In your `$HOME/.k9s` define a file called `alias.yml`. A K9s alias defines pairs of alias:gvr. A gvr (Group/Version/Resource) represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file: ```yaml # $HOME/.k9s/alias.yml @@ -204,9 +204,44 @@ Using this alias file, you can now type pp/crb to list pods or clusterrolebindin --- +## HotKey Support + +Entering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources. We're introducing hotkeys that allows a user to define their own hotkeys to activate their favorite resource views. In order to enable hotkeys please follow these steps: + +1. Create a file named `$HOME/.k9s/hotkey.yml` +2. Add the following to your `hotkey.yml`. You can use resource name/short name to specify a command ie same as typing it while in command mode. + + ```yaml + # $HOME/.k9s/hotkey.yml + hotKey: + # Hitting Shift-0 navigates to your pod view + shift-0: + shortCut: Shift-0 + description: Viewing pods + command: pods + # Hitting Shift-1 navigates to your deployments + shift-1: + shortCut: Shift-1 + description: View deployments + command: dp + # Hitting Shift-2 navigates to your xray deployments + shift-2: + shortCut: Shift-4 + description: Xray Deployments + command: xray deploy + ``` + + Not feeling so hot? Your custom hotkeys will be listed in the help view `?`. Also your hotkey file will be automatically reloaded so you can readily use your hotkeys as you define them. + + You can choose any keyboard shotcuts that make sense to you, provided they are not part of the standard K9s shortcuts list. + +> NOTE: This feature/configuration might change in future releases! + +--- + ## Plugins -K9s allows you to define your own cluster commands via plugins. K9s will look at `$HOME/.k9s/plugin.yml` to locate available plugins. A plugin is defined as follows: +K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins. K9s will look at `$HOME/.k9s/plugin.yml` to locate all available plugins. A plugin is defined as follows: ```yaml # $HOME/.k9s/plugin.yml @@ -217,7 +252,7 @@ plugin: description: Pod logs scopes: - po - command: /usr/local/bin/kubectl + command: kubectl background: false args: - logs @@ -237,6 +272,8 @@ K9s does provide additional environment variables for you to customize your plug * `$NAMESPACE` -- the selected resource namespace * `$NAME` -- the selected resource name +* `$CONTAINER` -- the current container if applicable +* `$FILTER` -- the current filter if any * `$KUBECONFIG` -- the KubeConfig location. * `$CLUSTER` the active cluster name * `$CONTEXT` the active context name @@ -244,13 +281,13 @@ K9s does provide additional environment variables for you to customize your plug * `$GROUPS` the active groups * `$COLX` the column at index X for the viewed resource -NOTE: This is an experimental feature! Options and layout may change in future K9s releases as this feature solidifies. +> NOTE: This is an experimental feature! Options and layout may change in future K9s releases as this feature solidifies. --- -## Benchmarking +## Benchmark Your Applications -K9s integrates [Hey](https://github.com/rakyll/hey) from the brilliant and super talented [Jaana Dogan](https://github.com/rakyll) of Google fame. Hey is a CLI tool to benchmark HTTP endpoints similar to AB bench. This preliminary feature currently supports benchmarking port-forwards and services (Read the paint on this is way fresh!). +K9s integrates [Hey](https://github.com/rakyll/hey) from the brilliant and super talented [Jaana Dogan](https://github.com/rakyll). `Hey` is a CLI tool to benchmark HTTP endpoints similar to AB bench. This preliminary feature currently supports benchmarking port-forwards and services (Read the paint on this is way fresh!). To setup a port-forward, you will need to navigate to the PodView, select a pod and a container that exposes a given port. Using `SHIFT-F` a dialog comes up to allow you to specify a local port to forward. Once acknowledged, you can navigate to the PortForward view (alias `pf`) listing out your active port-forwards. Selecting a port-forward and using `CTRL-B` will run a benchmark on that HTTP endpoint. To view the results of your benchmark runs, go to the Benchmarks view (alias `be`). You should now be able to select a benchmark and view the run stats details by pressing ``. NOTE: Port-forwards only last for the duration of the K9s session and will be terminated upon exit. @@ -272,11 +309,11 @@ benchmarks: defaults: # One concurrent connection concurrency: 1 - # 500 requests will be sent to an endpoint + # Number of requests that will be sent to an endpoint requests: 500 containers: # Containers section allows you to configure your http container's endpoints and benchmarking settings. - # NOTE: the container ID syntax uses namespace/pod_name:container_name + # NOTE: the container ID syntax uses namespace/pod-name:container-name default/nginx:nginx: # Benchmark a container named nginx using POST HTTP verb using http://localhost:port/bozo URL and headers. concurrency: 1 @@ -295,15 +332,15 @@ benchmarks: # Similary you can Benchmark an HTTP service exposed either via nodeport, loadbalancer types. # Service ID is ns/svc-name default/nginx: - # Hit the service with 5 concurrent sessions + # Set the concurrency level concurrency: 5 - # Issues a total of 500 requests + # Number of requests to be sent requests: 500 http: method: GET # This setting will depend on whether service is nodeport or loadbalancer. Nodeport may require vendor port tuneling setting. # Set this to a node if nodeport or LB if applicable. IP or dns name. - host: 10.11.13.14 + host: A.B.C.D path: /bumblebeetuna auth: user: jean-baptiste-emmanuel @@ -312,41 +349,6 @@ benchmarks: --- -## HotKeys - -Entering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources. We're introducing hotkeys that allows a user to define their own hotkeys to activate their favorite resource views. In order to enable hotkeys please follow these steps: - -1. In your .k9s home directory create a file named `hotkey.yml` -2. Add the following to your `hotkey.yml`. You can use short names or resource name to specify a command ie same as typing it in command mode. - - ```yaml - hotKey: - shift-0: - shortCut: Shift-0 - description: View pods - command: pods - shift-1: - shortCut: Shift-1 - description: View deployments - command: dp - shift-2: - shortCut: Shift-2 - description: View services - command: service - shift-3: - shortCut: Shift-3 - description: View statefulsets - command: sts - ``` - - Not feeling so hot? Your custom hotkeys list will be listed in the help view.``. Also your hotkey file will be automatically reloaded so you can readily use your hotkeys as you define them. - - You can choose any keyboard shotcuts that make sense to you, provided they are not part of the standard K9s shortcuts list. - -NOTE: This feature/configuration might change in future releases! - ---- - ## K9s RBAC FU On RBAC enabled clusters, you would need to give your users/groups capabilities so that they can use K9s to explore their Kubernetes cluster. K9s needs minimally read privileges at both the cluster and namespace level to display resources and metrics. @@ -459,7 +461,7 @@ Colors can be defined by name or uing an hex representation. Of recent, we've ad > NOTE: This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly! ```yaml -# InTheNavy Skin... +# Skin InTheNavy... k9s: # General K9s styles body: @@ -589,8 +591,8 @@ to make this project a reality! ## Meet The Core Team! * [Fernand Galiana](https://github.com/derailed) - * fernand@imhotep.io - * [@kitesurfer](https://twitter.com/kitesurfer?lang=en) + * fernand@imhotep.io + * [@kitesurfer](https://twitter.com/kitesurfer?lang=en) --- diff --git a/go.mod b/go.mod index a42b7e03..f440f8d9 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/atotto/clipboard v0.1.2 github.com/derailed/tview v0.3.4 + github.com/drone/envsubst v1.0.2 // indirect github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/fatih/color v1.6.0 @@ -41,9 +42,13 @@ require ( github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.5 + github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec // indirect + github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c + github.com/openfaas/faas-provider v0.15.0 // indirect github.com/petergtz/pegomock v2.6.0+incompatible github.com/rakyll/hey v0.1.2 github.com/rs/zerolog v1.17.2 + github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sahilm/fuzzy v0.1.0 github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum index 847d5a31..38a835e0 100644 --- a/go.sum +++ b/go.sum @@ -177,6 +177,8 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c h1:ZfSZ3P3BedhKGUhzj7BQlPSU4OvT6tfOKe3DVHzOA7s= github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/drone/envsubst v1.0.2 h1:dpYLMAspQHW0a8dZpLRKe9jCNvIGZPhCPrycZzIHdqo= +github.com/drone/envsubst v1.0.2/go.mod h1:bkZbnc/2vh1M12Ecn7EYScpI4YGYU0etwLJICOWi8Z0= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f h1:8GDPb0tCY8LQ+OJ3dbHb5sA6YZWXFORQYZx5sdsTlMs= @@ -339,9 +341,11 @@ github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1a github.com/gophercloud/gophercloud v0.1.0 h1:P/nh25+rzXouhytV2pUHBb65fnds26Ghl8/391+sT5o= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.4.0 h1:XulKRWSQK5uChr4pEgSE4Tc/OcmnU9GJuSwdog/tZsA= github.com/gorilla/handlers v1.4.0/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -450,6 +454,7 @@ github.com/mindprince/gonvml v0.0.0-20171110221305-fee913ce8fb2/go.mod h1:2eu9pR github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= @@ -497,6 +502,12 @@ github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830 h1:yvQ/2 github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runtime-spec v1.0.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.2.2/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs= +github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec h1:S6wtb5ie7KeMcuEaESj0RoSmpyGfvOSuunmKEdX7wg8= +github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec/go.mod h1:E0m2rLup0Vvxg53BKxGgaYAGcZa3Xl+vvL7vSi5yQ14= +github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c h1:9RGaDpUySgRscx5oiagwUtm9vBZti/4+QYq2GM4FegE= +github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c/go.mod h1:u/KO+e43wkagC0lqM1eaqNEWEBdg08Q1ugP/idj39MM= +github.com/openfaas/faas-provider v0.15.0 h1:3x5ma90FL7AqP4NOD6f03AY24y3xBeVF6xGLUx6Xrlc= +github.com/openfaas/faas-provider v0.15.0/go.mod h1:8Fagi2UeMfL+gZAqZWSMQg86i+w1+hBOKtwKRP5sLFI= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -557,6 +568,8 @@ github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvf github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -631,6 +644,7 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569 h1:nSQar3Y0E3VQF/VdZ8PTAilaXpER+d7ypdABCrpwMdg= go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df h1:shvkWr0NAZkg4nPuE3XrKP0VuBPijjk3TfX6Y6acFNg= go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15 h1:Z2sc4+v0JHV6Mn4kX1f2a5nruNjmV+Th32sugE8zwz8= diff --git a/internal/dao/container.go b/internal/dao/container.go index 60a8ac3c..a655f505 100644 --- a/internal/dao/container.go +++ b/internal/dao/container.go @@ -62,7 +62,7 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error } // TailLogs tails a given container logs -func (c *Container) TailLogs(ctx context.Context, logChan chan<- string, opts LogOptions) error { +func (c *Container) TailLogs(ctx context.Context, logChan chan<- []byte, opts LogOptions) error { fac, ok := ctx.Value(internal.KeyFactory).(Factory) if !ok { return errors.New("Expecting an informer") diff --git a/internal/dao/crd.go b/internal/dao/crd.go index 18ff56f6..c329560f 100644 --- a/internal/dao/crd.go +++ b/internal/dao/crd.go @@ -21,11 +21,11 @@ type CustomResourceDefinition struct { // List returns a collection of nodes. func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtime.Object, error) { strLabel, ok := ctx.Value(internal.KeyLabels).(string) - lsel := labels.Everything() + labelSel := labels.Everything() if sel, e := labels.ConvertSelectorToLabelsMap(strLabel); ok && e == nil { - lsel = sel.AsSelector() + labelSel = sel.AsSelector() } const gvr = "apiextensions.k8s.io/v1beta1/customresourcedefinitions" - return c.Factory.List(gvr, "-", true, lsel) + return c.Factory.List(gvr, "-", true, labelSel) } diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 43ab784a..e61b07f6 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -79,7 +79,7 @@ func (d *Deployment) Restart(path string) error { } // TailLogs tail logs for all pods represented by this Deployment. -func (d *Deployment) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { +func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { o, err := d.Factory.Get(d.gvr.String(), opts.Path, true, labels.Everything()) if err != nil { return err diff --git a/internal/dao/ds.go b/internal/dao/ds.go index c957766b..d42b159e 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" @@ -61,7 +62,7 @@ func (d *DaemonSet) Restart(path string) error { } // TailLogs tail logs for all pods represented by this DaemonSet. -func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { +func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { o, err := d.Factory.Get(d.gvr.String(), opts.Path, true, labels.Everything()) if err != nil { return err @@ -79,7 +80,11 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptio return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) } -func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts LogOptions) error { +func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts LogOptions) error { + defer func(t time.Time) { + log.Debug().Msgf("POD LOGS %v", time.Since(t)) + }(time.Now()) + f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) if !ok { return errors.New("expecting a context factory") @@ -94,7 +99,7 @@ func podLogs(ctx context.Context, c chan<- string, sel map[string]string, opts L } ns, _ := client.Namespaced(opts.Path) - oo, err := f.List("v1/pods", ns, true, lsel) + oo, err := f.List("v1/pods", ns, false, lsel) if err != nil { return err } diff --git a/internal/dao/generic.go b/internal/dao/generic.go index 4d60762d..7963019e 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -28,7 +28,7 @@ type Generic struct { func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) { labelSel, ok := ctx.Value(internal.KeyLabels).(string) if !ok { - log.Warn().Msgf("No label selector found in context. Listing all resources") + log.Debug().Msgf("No label selector found in context. Listing all resources") } if client.IsAllNamespace(ns) { ns = client.AllNamespaces diff --git a/internal/dao/job.go b/internal/dao/job.go index ff96a6d4..5f002804 100644 --- a/internal/dao/job.go +++ b/internal/dao/job.go @@ -23,7 +23,7 @@ type Job struct { } // TailLogs tail logs for all pods represented by this Job. -func (j *Job) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { +func (j *Job) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { o, err := j.Factory.Get(j.gvr.String(), opts.Path, true, labels.Everything()) if err != nil { return err diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index ce31c575..67e5e38c 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -47,19 +47,26 @@ func colorize(c color.Paint, txt string) string { } // DecorateLog add a log header to display po/co information along with the log message. -func (o LogOptions) DecorateLog(msg string) string { - _, n := client.Namespaced(o.Path) - if msg == "" { - return msg +func (o LogOptions) DecorateLog(bytes []byte) []byte { + if len(bytes) == 0 { + return bytes } + bytes = bytes[:len(bytes)-1] + _, n := client.Namespaced(o.Path) + + var prefix []byte if o.MultiPods { - return colorize(o.Color, n+":"+o.Container+" ") + msg + prefix = []byte(colorize(o.Color, n+":"+o.Container+" ")) } if !o.SingleContainer { - return colorize(o.Color, o.Container+" ") + msg + prefix = []byte(colorize(o.Color, o.Container+" ")) } - return msg + if len(prefix) == 0 { + return bytes + } + + return append(prefix, bytes...) } diff --git a/internal/dao/pod.go b/internal/dao/pod.go index 1258af29..faf39cb5 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -148,14 +148,14 @@ func (p *Pod) Containers(path string, includeInit bool) ([]string, error) { } // TailLogs tails a given container logs -func (p *Pod) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { +func (p *Pod) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { if !opts.HasContainer() { return p.logs(ctx, c, opts) } return tailLogs(ctx, p, c, opts) } -func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error { +func (p *Pod) logs(ctx context.Context, c chan<- []byte, opts LogOptions) error { fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) if !ok { return errors.New("Expecting an informer") @@ -194,7 +194,7 @@ func (p *Pod) logs(ctx context.Context, c chan<- string, opts LogOptions) error return nil } -func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptions) error { +func tailLogs(ctx context.Context, logger Logger, c chan<- []byte, opts LogOptions) error { log.Debug().Msgf("Tailing logs for %q -- %q", opts.Path, opts.Container) o := v1.PodLogOptions{ Container: opts.Container, @@ -206,11 +206,13 @@ func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptio if err != nil { return err } - ctxt, cancelFunc := context.WithCancel(ctx) - req.Context(ctxt) + + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + req.Context(ctx) var blocked int32 = 1 - go logsTimeout(cancelFunc, &blocked) + go logsTimeout(cancel, &blocked) // This call will block if nothing is in the stream!! stream, err := req.Stream() @@ -219,7 +221,7 @@ func tailLogs(ctx context.Context, logger Logger, c chan<- string, opts LogOptio log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path) return fmt.Errorf("Unable to obtain log stream for %s", opts.Path) } - go readLogs(ctx, stream, c, opts) + go readLogs(stream, c, opts) return nil } @@ -232,7 +234,7 @@ func logsTimeout(cancel context.CancelFunc, blocked *int32) { } } -func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts LogOptions) { +func readLogs(stream io.ReadCloser, c chan<- []byte, opts LogOptions) { defer func() { log.Debug().Msgf(">>> Closing stream `%s", opts.Path) if err := stream.Close(); err != nil { @@ -240,16 +242,18 @@ func readLogs(ctx context.Context, stream io.ReadCloser, c chan<- string, opts L } }() - scanner := bufio.NewScanner(stream) - for scanner.Scan() { - select { - case <-ctx.Done(): + r := bufio.NewReader(stream) + for { + bytes, err := r.ReadBytes('\n') + if err != nil { + log.Warn().Err(err).Msg("Read error") + if err != io.EOF { + log.Error().Err(err).Msgf("stream reader failed") + } return - default: - c <- opts.DecorateLog(scanner.Text()) } + c <- opts.DecorateLog(bytes) } - log.Error().Msgf("SCAN_ERR %#v", scanner.Err()) } // ---------------------------------------------------------------------------- diff --git a/internal/dao/sts.go b/internal/dao/sts.go index 3fe02c52..0946ff24 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -80,7 +80,7 @@ func (s *StatefulSet) Restart(path string) error { } // TailLogs tail logs for all pods represented by this StatefulSet. -func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { +func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { o, err := s.Factory.Get(s.gvr.String(), opts.Path, true, labels.Everything()) if err != nil { return err diff --git a/internal/dao/svc.go b/internal/dao/svc.go index 25b82e0f..9fbc3787 100644 --- a/internal/dao/svc.go +++ b/internal/dao/svc.go @@ -22,7 +22,7 @@ type Service struct { } // TailLogs tail logs for all pods represented by this Service. -func (s *Service) TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error { +func (s *Service) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { o, err := s.Factory.Get(s.gvr.String(), opts.Path, true, labels.Everything()) if err != nil { return err diff --git a/internal/dao/table.go b/internal/dao/table.go index 944a1ef9..290f2201 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -22,7 +23,6 @@ type Table struct { func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { ns, n := client.Namespaced(path) - log.Debug().Msgf("TABLE-GET %q:%q", ns, t.gvr) a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) _, codec := t.codec() @@ -46,6 +46,11 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { // List all Resources in a given namespace. func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { + labelSel, ok := ctx.Value(internal.KeyLabels).(string) + if !ok { + log.Debug().Msgf("No label selector found in context. Listing all resources") + } + a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) _, codec := t.codec() @@ -57,7 +62,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { SetHeader("Accept", a). Namespace(ns). Resource(t.gvr.R()). - VersionedParams(&metav1beta1.TableOptions{}, codec). + VersionedParams(&metav1.ListOptions{LabelSelector: labelSel}, codec). Do().Get() if err != nil { return nil, err diff --git a/internal/dao/types.go b/internal/dao/types.go index c57ccb70..b93d7f76 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -73,7 +73,7 @@ type Accessor interface { // Loggable represents resources with logs. type Loggable interface { // TaiLogs streams resource logs. - TailLogs(ctx context.Context, c chan<- string, opts LogOptions) error + TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error } // Describer describes a resource. diff --git a/internal/model/cluster_info_test.go b/internal/model/cluster_info_test.go new file mode 100644 index 00000000..7fabb932 --- /dev/null +++ b/internal/model/cluster_info_test.go @@ -0,0 +1,46 @@ +package model_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/stretchr/testify/assert" +) + +func TestClusterMetaDelta(t *testing.T) { + uu := map[string]struct { + o, n model.ClusterMeta + e bool + }{ + "empty": { + o: model.NewClusterMeta(), + n: model.NewClusterMeta(), + }, + "same": { + o: makeClusterMeta("fred"), + n: makeClusterMeta("fred"), + }, + "diff": { + o: makeClusterMeta("fred"), + n: makeClusterMeta("freddie"), + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.o.Deltas(u.n)) + }) + } +} + +// Helpers... + +func makeClusterMeta(cluster string) model.ClusterMeta { + m := model.NewClusterMeta() + m.Cluster = cluster + m.Cpu, m.Mem = 10, 20 + + return m +} diff --git a/internal/model/log.go b/internal/model/log.go index 896f00be..07cbc4ad 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -31,25 +31,23 @@ type LogsListener interface { // Log represents a resource logger. type Log struct { - factory dao.Factory - lines []string - listeners []LogsListener - gvr client.GVR - logOptions dao.LogOptions - cancelFn context.CancelFunc - initialized bool - mx sync.RWMutex - filter string - lastSent int + factory dao.Factory + lines []string + listeners []LogsListener + gvr client.GVR + logOptions dao.LogOptions + cancelFn context.CancelFunc + mx sync.RWMutex + filter string + lastSent int } // NewLog returns a new model. -func NewLog(gvr client.GVR, msg string, opts dao.LogOptions, timeOut time.Duration) *Log { +func NewLog(gvr client.GVR, opts dao.LogOptions, timeOut time.Duration) *Log { return &Log{ - gvr: gvr, - logOptions: opts, - initialized: true, - lines: []string{msg}, + gvr: gvr, + logOptions: opts, + lines: nil, } } @@ -84,12 +82,11 @@ func (l *Log) Start() { // Stop terminates log tailing. func (l *Log) Stop() { - if l.cancelFn == nil { - return + defer log.Debug().Msgf("<<<< Logger STOPPED!") + if l.cancelFn != nil { + l.cancelFn() + l.cancelFn = nil } - log.Debug().Msgf("<<<< Logger STOP!") - l.cancelFn() - l.cancelFn = nil } // Set sets the log lines (for testing only!) @@ -131,7 +128,7 @@ func (l *Log) load() error { ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory) ctx, l.cancelFn = context.WithCancel(ctx) - c := make(chan string, 10) + c := make(chan []byte, 10) go l.updateLogs(ctx, c) accessor, err := dao.AccessorFor(l.factory, l.gvr) @@ -163,8 +160,7 @@ func (l *Log) Append(line string) { l.mx.Lock() defer l.mx.Unlock() - if l.initialized { - l.lines, l.initialized, l.lastSent = []string{}, false, 0 + if l.lines == nil { l.fireLogCleared() } @@ -190,20 +186,20 @@ func (l *Log) Notify(timedOut bool) { } } -func (l *Log) updateLogs(ctx context.Context, c <-chan string) { +func (l *Log) updateLogs(ctx context.Context, c <-chan []byte) { defer func() { log.Debug().Msgf("updateLogs view bailing out!") }() for { select { - case line, ok := <-c: + case bytes, ok := <-c: if !ok { log.Debug().Msgf("Closed channel detected. Bailing out...") - l.Append(line) + l.Append(string(bytes)) l.Notify(false) return } - l.Append(line) + l.Append(string(bytes)) var overflow bool l.mx.RLock() { diff --git a/internal/model/log_test.go b/internal/model/log_test.go index 6b23ea46..d6ac27d7 100644 --- a/internal/model/log_test.go +++ b/internal/model/log_test.go @@ -18,7 +18,7 @@ import ( func TestLogFullBuffer(t *testing.T) { size := 4 - m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(size), 10*time.Millisecond) + m := model.NewLog(client.NewGVR("fred"), makeLogOpts(size), 10*time.Millisecond) m.Init(makeFactory()) v := newTestView() @@ -60,7 +60,7 @@ func TestLogFilter(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(size), 10*time.Millisecond) + m := model.NewLog(client.NewGVR("fred"), makeLogOpts(size), 10*time.Millisecond) m.Init(makeFactory()) v := newTestView() @@ -89,7 +89,7 @@ func TestLogFilter(t *testing.T) { } func TestLogStartStop(t *testing.T) { - m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(4), 10*time.Millisecond) + m := model.NewLog(client.NewGVR("fred"), makeLogOpts(4), 10*time.Millisecond) m.Init(makeFactory()) v := newTestView() @@ -110,7 +110,7 @@ func TestLogStartStop(t *testing.T) { } func TestLogClear(t *testing.T) { - m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(4), 10*time.Millisecond) + m := model.NewLog(client.NewGVR("fred"), makeLogOpts(4), 10*time.Millisecond) m.Init(makeFactory()) assert.Equal(t, "fred", m.GetPath()) assert.Equal(t, "blee", m.GetContainer()) @@ -132,7 +132,7 @@ func TestLogClear(t *testing.T) { } func TestLogBasic(t *testing.T) { - m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(2), 10*time.Millisecond) + m := model.NewLog(client.NewGVR("fred"), makeLogOpts(2), 10*time.Millisecond) m.Init(makeFactory()) v := newTestView() @@ -148,7 +148,7 @@ func TestLogBasic(t *testing.T) { } func TestLogAppend(t *testing.T) { - m := model.NewLog(client.NewGVR("fred"), "blah blah", makeLogOpts(4), 5*time.Millisecond) + m := model.NewLog(client.NewGVR("fred"), makeLogOpts(4), 5*time.Millisecond) m.Init(makeFactory()) v := newTestView() @@ -161,17 +161,17 @@ func TestLogAppend(t *testing.T) { m.Append(d) } assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, []string{}, v.data) + assert.Equal(t, []string{"blah blah"}, v.data) m.Notify(true) assert.Equal(t, 2, v.dataCalled) - assert.Equal(t, 1, v.clearCalled) + assert.Equal(t, 0, v.clearCalled) assert.Equal(t, 0, v.errCalled) - assert.Equal(t, data, v.data) + assert.Equal(t, append([]string{"blah blah"}, data...), v.data) } func TestLogTimedout(t *testing.T) { - m := model.NewLog(client.NewGVR("fred"), "Blee", makeLogOpts(4), 10*time.Millisecond) + m := model.NewLog(client.NewGVR("fred"), makeLogOpts(4), 10*time.Millisecond) m.Init(makeFactory()) v := newTestView() diff --git a/internal/render/job.go b/internal/render/job.go index 62048990..661c7c88 100644 --- a/internal/render/job.go +++ b/internal/render/job.go @@ -7,7 +7,6 @@ import ( "time" "github.com/derailed/k9s/internal/client" - "github.com/rs/zerolog/log" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -42,7 +41,6 @@ func (Job) Header(ns string) HeaderRow { // Render renders a K8s resource to screen. func (j Job) Render(o interface{}, ns string, r *Row) error { - log.Debug().Msgf("JOB RENDER %q", ns) raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("Expected Job, but got %T", o) diff --git a/internal/view/app.go b/internal/view/app.go index 96c96e0b..d41814cb 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -134,7 +134,7 @@ func (a *App) toggleHeader(flag bool) { } if a.showHeader { flex.RemoveItemAtIndex(0) - flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false) + flex.AddItemAtIndex(0, a.buildHeader(), 8, 1, false) } else { flex.RemoveItemAtIndex(0) flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false) @@ -144,7 +144,6 @@ func (a *App) toggleHeader(flag bool) { func (a *App) buildHeader() tview.Primitive { header := tview.NewFlex() header.SetBackgroundColor(a.Styles.BgColor()) - header.SetBorderPadding(0, 0, 1, 1) header.SetDirection(tview.FlexColumn) if !a.showHeader { return header diff --git a/internal/view/log.go b/internal/view/log.go index 5e7ede41..7e0e715b 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -21,7 +21,7 @@ import ( const ( logTitle = "logs" - logMessage = "Waiting for logs..." + logMessage = "[:orange:b]Waiting for logs...[::]" logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) " logFmt = " Logs([fg:bg:]%s) " @@ -49,7 +49,7 @@ func NewLog(gvr client.GVR, path, co string, prev bool) *Log { l := Log{ Flex: tview.NewFlex(), cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), - model: model.NewLog(gvr, logMessage, buildLogOpts(path, co, prev, tailLineCount), defaultTimeout), + model: model.NewLog(gvr, buildLogOpts(path, co, prev, tailLineCount), defaultTimeout), } return &l @@ -66,11 +66,13 @@ func (l *Log) Init(ctx context.Context) (err error) { l.indicator = NewLogIndicator(l.app.Config, l.app.Styles) l.AddItem(l.indicator, 1, 1, false) + l.indicator.Refresh() l.logs = NewDetails(l.app, "", "", false) if err = l.logs.Init(ctx); err != nil { return err } + l.logs.SetText(logMessage) l.logs.SetWrap(false) l.logs.SetMaxBuffer(l.app.Config.K9s.LogBufferSize) From 94e96b39a51a9ab50b07d6a08c8f51bde996aa59 Mon Sep 17 00:00:00 2001 From: Philipp Eckel Date: Thu, 13 Feb 2020 19:46:39 +0100 Subject: [PATCH 27/39] Update README.md: fix example hotkey.yml --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a83473e..88700e05 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ Entering the command mode and typing a resource name or alias, could be cumberso command: dp # Hitting Shift-2 navigates to your xray deployments shift-2: - shortCut: Shift-4 + shortCut: Shift-2 description: Xray Deployments command: xray deploy ``` From f1ef8d216ccdc9e30fe4373fc1b66c89c2378188 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 13 Feb 2020 12:31:30 -0700 Subject: [PATCH 28/39] enable port-forwards on pods and services --- go.mod | 8 +- go.sum | 5 + internal/dao/chart.go | 1 - internal/dao/helpers.go | 1 + internal/dao/ofaas.go | 216 ++++++++++++++++++++++++++++ internal/dao/registry.go | 17 ++- internal/model/registry.go | 4 + internal/render/chart.go | 2 +- internal/render/ofaas.go | 102 +++++++++++++ internal/render/ofaas_test.go | 34 +++++ internal/ui/dialog/port_forwards.go | 62 ++++++++ internal/view/app.go | 2 +- internal/view/container.go | 26 ++-- internal/view/help_test.go | 2 +- internal/view/ofaas.go | 46 ++++++ internal/view/pod.go | 80 ++++++++++- internal/view/pod_test.go | 2 +- internal/view/registrar.go | 3 + internal/view/svc.go | 100 ++++++++++++- internal/view/svc_test.go | 2 +- internal/watch/factory.go | 3 + plugins/README.md | 4 + plugins/job_suspend.yml | 19 +++ plugins/kubectl/kubectl-jq | 3 + plugins/log_jq.yml | 14 ++ plugins/log_stern.yml | 17 +++ 26 files changed, 745 insertions(+), 30 deletions(-) create mode 100644 internal/dao/ofaas.go create mode 100644 internal/render/ofaas.go create mode 100644 internal/render/ofaas_test.go create mode 100644 internal/ui/dialog/port_forwards.go create mode 100644 internal/view/ofaas.go create mode 100644 plugins/README.md create mode 100644 plugins/job_suspend.yml create mode 100755 plugins/kubectl/kubectl-jq create mode 100644 plugins/log_jq.yml create mode 100644 plugins/log_stern.yml diff --git a/go.mod b/go.mod index f440f8d9..599c7759 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/derailed/k9s go 1.13 +replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview + replace ( github.com/docker/docker => github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f @@ -30,6 +32,8 @@ replace ( require ( fyne.io/fyne v1.2.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e // indirect + github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de // indirect github.com/atotto/clipboard v0.1.2 github.com/derailed/tview v0.3.4 github.com/drone/envsubst v1.0.2 // indirect @@ -42,9 +46,9 @@ require ( github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-runewidth v0.0.5 - github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec // indirect + github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c - github.com/openfaas/faas-provider v0.15.0 // indirect + github.com/openfaas/faas-provider v0.15.0 github.com/petergtz/pegomock v2.6.0+incompatible github.com/rakyll/hey v0.1.2 github.com/rs/zerolog v1.17.2 diff --git a/go.sum b/go.sum index 38a835e0..07ae4013 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,10 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e h1:0cv4CUENL7e67/ZlNrvExWqa6oKH/9iv0KQn0/+hYaY= +github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e/go.mod h1:zfRbgnPVxXCSpiKrg1CE72hNUWInqxExiaz2D9ppTts= +github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de h1:jiPEvtW8VT0KwJxRyjW2VAAvlssjj9SfecsQ3Vgv5tk= +github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de/go.mod h1:uAbpy8G7sjNB4qYdY6ymf5OIQ+TLDPApBYiR0Vc3lhk= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= @@ -470,6 +474,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mohae/deepcopy v0.0.0-20170603005431-491d3605edfb/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs= diff --git a/internal/dao/chart.go b/internal/dao/chart.go index e69aa050..a407bd0d 100644 --- a/internal/dao/chart.go +++ b/internal/dao/chart.go @@ -90,7 +90,6 @@ func (c *Chart) ToYAML(path string) (string, error) { // Delete uninstall a Chart. func (c *Chart) Delete(path string, cascade, force bool) error { - log.Debug().Msgf("CHART DELETE %q", path) ns, n := client.Namespaced(path) cfg, err := c.EnsureHelmConfig(ns) if err != nil { diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go index e893e3b2..718ad677 100644 --- a/internal/dao/helpers.go +++ b/internal/dao/helpers.go @@ -29,6 +29,7 @@ func ToYAML(o runtime.Object) (string, error) { if o == nil { return "", errors.New("no object to yamlize") } + var ( buff bytes.Buffer p printers.YAMLPrinter diff --git a/internal/dao/ofaas.go b/internal/dao/ofaas.go new file mode 100644 index 00000000..7feb5402 --- /dev/null +++ b/internal/dao/ofaas.go @@ -0,0 +1,216 @@ +package dao + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path" + "strings" + "time" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/openfaas/faas-cli/proxy" + "github.com/openfaas/faas/gateway/requests" + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" +) + +const ( + oFaasGatewayEnv = "OPENFAAS_GATEWAY" + oFaasJWTTokenEnv = "OPENFAAS_JWT_TOKEN" + oFaasTLSInsecure = "OPENFAAS_TLS_INSECURE" +) + +var ( + _ Accessor = (*OpenFaas)(nil) + _ Nuker = (*OpenFaas)(nil) + _ Describer = (*OpenFaas)(nil) +) + +// OpenFaas represents a faas gateway connection. +type OpenFaas struct { + NonResource +} + +// IsOpenFaasEnabled returns true if a gateway url is set in the environment. +func IsOpenFaasEnabled() bool { + return os.Getenv(oFaasGatewayEnv) != "" +} + +func getOpenFAASFlags() (string, string, bool) { + gw, token := os.Getenv(oFaasGatewayEnv), os.Getenv(oFaasJWTTokenEnv) + tlsInsecure := false + if os.Getenv(oFaasTLSInsecure) == "true" { + tlsInsecure = true + } + + return gw, token, tlsInsecure +} + +// List returns a collection of functions +func (f *OpenFaas) Get(ctx context.Context, path string) (runtime.Object, error) { + ns, n := client.Namespaced(path) + + oo, err := f.List(ctx, ns) + if err != nil { + return nil, err + } + + var found runtime.Object + for _, o := range oo { + r, ok := o.(render.OpenFaasRes) + if !ok { + continue + } + if r.Function.Name == n { + found = o + break + } + } + + if found == nil { + return nil, fmt.Errorf("unable to locate function %q", path) + } + + return found, nil +} + +// List returns a collection of functions +func (f *OpenFaas) List(_ context.Context, ns string) ([]runtime.Object, error) { + if !IsOpenFaasEnabled() { + return nil, errors.New("OpenFAAS is not enabled on this cluster") + } + + gw, token, tls := getOpenFAASFlags() + ff, err := proxy.ListFunctionsToken(gw, tls, token, ns) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(ff)) + for _, f := range ff { + oo = append(oo, render.OpenFaasRes{Function: f}) + } + + return oo, nil +} + +func (f *OpenFaas) Delete(path string, _, _ bool) error { + gw, token, tls := getOpenFAASFlags() + ns, n := client.Namespaced(path) + + // BOZO!! openfaas spews to stdout. Not good for us... + return deleteFunctionToken(gw, n, tls, token, ns) +} + +func (f *OpenFaas) ToYAML(path string) (string, error) { + return f.Describe(path) +} + +func (f *OpenFaas) Describe(path string) (string, error) { + o, err := f.Get(context.Background(), path) + if err != nil { + return "", err + } + + fn, ok := o.(render.OpenFaasRes) + if !ok { + return "", fmt.Errorf("expecting OpenFaasRes but got %T", o) + } + + raw, err := json.Marshal(fn) + if err != nil { + return "", err + } + + bytes, err := yaml.JSONToYAML(raw) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +// BOZO!! Meow! openfaas fn prints to stdout have to dup ;( +func deleteFunctionToken(gateway string, functionName string, tlsInsecure bool, token string, namespace string) error { + defaultCommandTimeout := 60 * time.Second + + gateway = strings.TrimRight(gateway, "/") + delReq := requests.DeleteFunctionRequest{FunctionName: functionName} + reqBytes, _ := json.Marshal(&delReq) + reader := bytes.NewReader(reqBytes) + + c := proxy.MakeHTTPClient(&defaultCommandTimeout, tlsInsecure) + + deleteEndpoint, err := createSystemEndpoint(gateway, namespace) + if err != nil { + return err + } + + req, err := http.NewRequest("DELETE", deleteEndpoint, reader) + if err != nil { + fmt.Println(err) + return err + } + req.Header.Set("Content-Type", "application/json") + + if len(token) > 0 { + proxy.SetToken(req, token) + } else { + proxy.SetAuth(req, gateway) + } + + delRes, delErr := c.Do(req) + + if delErr != nil { + fmt.Printf("Error removing existing function: %s, gateway=%s, functionName=%s\n", delErr.Error(), gateway, functionName) + return delErr + } + + if delRes.Body != nil { + defer func() { + if err := delRes.Body.Close(); err != nil { + log.Error().Err(err).Msgf("closing delete-gtw body") + } + }() + } + + switch delRes.StatusCode { + case http.StatusOK, http.StatusCreated, http.StatusAccepted: + return nil + case http.StatusNotFound: + return fmt.Errorf("no function named %s found", functionName) + case http.StatusUnauthorized: + return fmt.Errorf("unauthorized access, run \"faas-cli login\" to setup authentication for this server") + default: + bytesOut, err := ioutil.ReadAll(delRes.Body) + if err != nil { + return err + } + return fmt.Errorf("server returned unexpected status code %d %s", delRes.StatusCode, string(bytesOut)) + } +} + +func createSystemEndpoint(gateway, namespace string) (string, error) { + const systemPath = "/system/functions" + + gatewayURL, err := url.Parse(gateway) + if err != nil { + return "", fmt.Errorf("invalid gateway URL: %s", err.Error()) + } + gatewayURL.Path = path.Join(gatewayURL.Path, systemPath) + if len(namespace) > 0 { + q := gatewayURL.Query() + q.Set("namespace", namespace) + gatewayURL.RawQuery = q.Encode() + } + return gatewayURL.String(), nil +} diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 56799b5b..1436efd7 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -47,6 +47,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{}, client.NewGVR("batch/v1/jobs"): &Job{}, client.NewGVR("charts"): &Chart{}, + client.NewGVR("openfaas"): &OpenFaas{}, } r, ok := m[gvr] @@ -96,7 +97,7 @@ func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) { // IsK8sMeta checks for non resource meta. func IsK8sMeta(m metav1.APIResource) bool { for _, c := range m.Categories { - if c == "k9s" || c == "helm" { + if c == "k9s" || c == "helm" || c == "faas" { return false } } @@ -135,6 +136,9 @@ func loadNonResource(m ResourceMetas) { loadK9s(m) loadRBAC(m) loadHelm(m) + if IsOpenFaasEnabled() { + loadOpenFaas(m) + } } func loadK9s(m ResourceMetas) { @@ -203,6 +207,17 @@ func loadHelm(m ResourceMetas) { } } +func loadOpenFaas(m ResourceMetas) { + m[client.NewGVR("openfaas")] = metav1.APIResource{ + Name: "openfaas", + Kind: "OpenFaaS", + ShortNames: []string{"ofaas", "ofa"}, + Namespaced: true, + Verbs: []string{"delete"}, + Categories: []string{"faas"}, + } +} + func loadRBAC(m ResourceMetas) { m[client.NewGVR("rbac")] = metav1.APIResource{ Name: "rbacs", diff --git a/internal/model/registry.go b/internal/model/registry.go index 717f8bec..997ddfe2 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -14,6 +14,10 @@ var Registry = map[string]ResourceMeta{ DAO: &dao.Chart{}, Renderer: &render.Chart{}, }, + "openfaas": { + DAO: &dao.OpenFaas{}, + Renderer: &render.OpenFaas{}, + }, "containers": { DAO: &dao.Container{}, Renderer: &render.Container{}, diff --git a/internal/render/chart.go b/internal/render/chart.go index cb3aa0c7..f1ab5892 100644 --- a/internal/render/chart.go +++ b/internal/render/chart.go @@ -66,7 +66,7 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error { // ---------------------------------------------------------------------------- // Helpers... -// ChartRes represents an alias resource. +// ChartRes represents an helm chart resource. type ChartRes struct { Release *release.Release } diff --git a/internal/render/ofaas.go b/internal/render/ofaas.go new file mode 100644 index 00000000..c51dd4c7 --- /dev/null +++ b/internal/render/ofaas.go @@ -0,0 +1,102 @@ +package render + +import ( + "fmt" + "strconv" + "time" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + ofaas "github.com/openfaas/faas-provider/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + fnStatusReady = "Ready" + fnStatusNotReady = "Not Ready" +) + +// OpenFaas renders an openfaas function to screen. +type OpenFaas struct{} + +// ColorerFunc colors a resource row. +func (OpenFaas) ColorerFunc() ColorerFunc { + return func(ns string, re RowEvent) tcell.Color { + return tcell.ColorPaleTurquoise + } +} + +// Header returns a header row. +func (OpenFaas) Header(ns string) HeaderRow { + var h HeaderRow + if client.IsAllNamespaces(ns) { + h = append(h, Header{Name: "NAMESPACE"}) + } + + return append(h, + Header{Name: "NAME"}, + Header{Name: "STATUS"}, + Header{Name: "IMAGE"}, + Header{Name: "LABELS"}, + Header{Name: "INVOCATIONS", Align: tview.AlignRight}, + Header{Name: "REPLICAS", Align: tview.AlignRight}, + Header{Name: "AVAILABLE", Align: tview.AlignRight}, + Header{Name: "AGE", Decorator: AgeDecorator}, + ) +} + +// Render renders a chart to screen. +func (f OpenFaas) Render(o interface{}, ns string, r *Row) error { + fn, ok := o.(OpenFaasRes) + if !ok { + return fmt.Errorf("expected OpenFaasRes, but got %T", o) + } + + var labels string + if fn.Function.Labels != nil { + labels = mapToStr(*fn.Function.Labels) + } + var status = fnStatusReady + if fn.Function.AvailableReplicas == 0 { + status = fnStatusNotReady + } + + r.ID = client.FQN(fn.Function.Namespace, fn.Function.Name) + r.Fields = make(Fields, 0, len(f.Header(ns))) + if client.IsAllNamespaces(ns) { + r.Fields = append(r.Fields, fn.Function.Namespace) + } + r.Fields = append(r.Fields, + fn.Function.Name, + status, + fn.Function.Image, + labels, + strconv.Itoa(int(fn.Function.InvocationCount)), + strconv.Itoa(int(fn.Function.Replicas)), + strconv.Itoa(int(fn.Function.AvailableReplicas)), + toAge(metav1.Time{Time: time.Now()}), + ) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// OpenFaasRes represents an openfaas function resource. +type OpenFaasRes struct { + Function ofaas.FunctionStatus `json:"function"` +} + +// GetObjectKind returns a schema object. +func (OpenFaasRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (h OpenFaasRes) DeepCopyObject() runtime.Object { + return h +} diff --git a/internal/render/ofaas_test.go b/internal/render/ofaas_test.go new file mode 100644 index 00000000..3deda698 --- /dev/null +++ b/internal/render/ofaas_test.go @@ -0,0 +1,34 @@ +package render_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/render" + ofaas "github.com/openfaas/faas-provider/types" + "github.com/stretchr/testify/assert" +) + +func TestOpenFaasRender(t *testing.T) { + c := render.OpenFaas{} + r := render.NewRow(9) + c.Render(makeFn("blee"), "", &r) + + assert.Equal(t, "default/blee", r.ID) + assert.Equal(t, render.Fields{"default", "blee", "Ready", "nginx:0", "fred=blee", "10", "1", "1"}, r.Fields[:8]) +} + +// Helpers... + +func makeFn(n string) render.OpenFaasRes { + return render.OpenFaasRes{ + Function: ofaas.FunctionStatus{ + Name: n, + Namespace: "default", + Image: "nginx:0", + InvocationCount: 10, + Replicas: 1, + AvailableReplicas: 1, + Labels: &map[string]string{"fred": "blee"}, + }, + } +} diff --git a/internal/ui/dialog/port_forwards.go b/internal/ui/dialog/port_forwards.go new file mode 100644 index 00000000..279e4524 --- /dev/null +++ b/internal/ui/dialog/port_forwards.go @@ -0,0 +1,62 @@ +package dialog + +import ( + "fmt" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" +) + +type PortForwardFunc func(path, address, lport, cport string) + +// ShowPortForwards pops a port forwarding configuration dialog. +func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string, okFn PortForwardFunc) { + f := tview.NewForm() + f.SetItemPadding(0) + f.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(s.BgColor()). + SetButtonTextColor(s.FgColor()). + SetLabelColor(config.AsColor(s.K9s.Info.FgColor)). + SetFieldTextColor(config.AsColor(s.K9s.Info.SectionColor)) + + p1, p2, address := ports[0], ports[0], "localhost" + f.AddDropDown("Container Ports", ports, 0, func(sel string, _ int) { + p1, p2 = sel, stripPort(sel) + }) + + dropD, ok := f.GetFormItem(0).(*tview.DropDown) + if ok { + dropD.SetFieldBackgroundColor(s.BgColor()) + list := dropD.GetList() + list.SetMainTextColor(s.FgColor()) + list.SetSelectedTextColor(s.FgColor()) + list.SetSelectedBackgroundColor(config.AsColor(s.Table().CursorColor)) + list.SetBackgroundColor(s.BgColor() + 100) + } + f.AddInputField("Local Port:", p2, 20, nil, func(p string) { + p2 = p + }) + f.AddInputField("Address:", address, 20, nil, func(h string) { + address = h + }) + + f.AddButton("OK", func() { + okFn(path, address, stripPort(p2), stripPort(p1)) + }) + f.AddButton("Cancel", func() { + DismissPortForward(p) + }) + + modal := tview.NewModalForm(fmt.Sprintf("", path), f) + modal.SetDoneFunc(func(_ int, b string) { + DismissPortForward(p) + }) + p.AddPage(portForwardKey, modal, false, false) + p.ShowPage(portForwardKey) +} + +// DismissPortForward dismiss the port forward dialog. +func DismissPortForwards(p *ui.Pages) { + p.RemovePage(portForwardKey) +} diff --git a/internal/view/app.go b/internal/view/app.go index d41814cb..d36f5745 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -134,7 +134,7 @@ func (a *App) toggleHeader(flag bool) { } if a.showHeader { flex.RemoveItemAtIndex(0) - flex.AddItemAtIndex(0, a.buildHeader(), 8, 1, false) + flex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false) } else { flex.RemoveItemAtIndex(0) flex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false) diff --git a/internal/view/container.go b/internal/view/container.go index 9b25a00f..724801eb 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -159,31 +159,35 @@ func (c *Container) preparePort(pp []string) string { func (c *Container) portForward(address, lport, cport string) { co := c.GetTable().GetSelectedCell(0) pf := dao.NewPortForwarder(c.App().Conn()) + path := c.GetTable().GetSelectedItem() ports := []string{lport + ":" + cport} - fw, err := pf.Start(c.GetTable().Path, co, address, ports) + fw, err := pf.Start(path, co, address, ports) if err != nil { c.App().Flash().Err(err) return } - log.Debug().Msgf(">>> Starting port forward %q %v", c.GetTable().Path, ports) - go c.runForward(pf, fw) + log.Debug().Msgf(">>> Starting port forward %q %v", path, ports) + go runForward(c.App(), pf, fw) } -func (c *Container) runForward(pf *dao.PortForwarder, f *portforward.PortForwarder) { - c.App().QueueUpdateDraw(func() { - c.App().factory.AddForwarder(pf) - c.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) - dialog.DismissPortForward(c.App().Content.Pages) +// ---------------------------------------------------------------------------- +// Helpers... + +func runForward(a *App, pf *dao.PortForwarder, f *portforward.PortForwarder) { + a.QueueUpdateDraw(func() { + a.factory.AddForwarder(pf) + a.Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) + dialog.DismissPortForward(a.Content.Pages) }) pf.SetActive(true) if err := f.ForwardPorts(); err != nil { - c.App().Flash().Err(err) + a.Flash().Err(err) return } - c.App().QueueUpdateDraw(func() { - c.App().factory.DeleteForwarder(pf.FQN()) + a.QueueUpdateDraw(func() { + a.factory.DeleteForwarder(pf.FQN()) pf.SetActive(false) }) } diff --git a/internal/view/help_test.go b/internal/view/help_test.go index 18c5ccd3..443633c3 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -21,7 +21,7 @@ func TestHelp(t *testing.T) { v := view.NewHelp() assert.Nil(t, v.Init(ctx)) - assert.Equal(t, 19, v.GetRowCount()) + assert.Equal(t, 20, v.GetRowCount()) assert.Equal(t, 8, v.GetColumnCount()) assert.Equal(t, "", strings.TrimSpace(v.GetCell(1, 0).Text)) assert.Equal(t, "Kill", strings.TrimSpace(v.GetCell(1, 1).Text)) diff --git a/internal/view/ofaas.go b/internal/view/ofaas.go new file mode 100644 index 00000000..fda3bd62 --- /dev/null +++ b/internal/view/ofaas.go @@ -0,0 +1,46 @@ +package view + +import ( + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" +) + +// OpenFaas represents an OpenFaaS viewer. +type OpenFaas struct { + ResourceViewer +} + +// NewOpenFaas returns a new viewer. +func NewOpenFaas(gvr client.GVR) ResourceViewer { + o := OpenFaas{ResourceViewer: NewBrowser(gvr)} + o.SetBindKeysFn(o.bindKeys) + o.GetTable().SetEnterFn(o.showPods) + o.GetTable().SetColorerFn(render.OpenFaas{}.ColorerFunc()) + + return &o +} + +func (o *OpenFaas) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyShiftS: ui.NewKeyAction("Sort Status", o.GetTable().SortColCmd(2, true), false), + ui.KeyShiftT: ui.NewKeyAction("Sort Invocations", o.GetTable().SortColCmd(5, false), false), + ui.KeyShiftC: ui.NewKeyAction("Sort Replicas", o.GetTable().SortColCmd(6, false), false), + ui.KeyShiftM: ui.NewKeyAction("Sort Available", o.GetTable().SortColCmd(7, false), false), + }) +} + +func (o *OpenFaas) showPods(a *App, _ ui.Tabular, _, path string) { + labels := o.GetTable().GetSelectedCell(4) + sels := make(map[string]string) + + tokens := strings.Split(labels, ",") + for _, t := range tokens { + s := strings.Split(t, "=") + sels[s[0]] = s[1] + } + + showPodsWithLabels(a, path, sels) +} diff --git a/internal/view/pod.go b/internal/view/pod.go index 11f50ec0..0a79b7c7 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -11,6 +11,7 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/k9s/internal/watch" "github.com/fatih/color" "github.com/gdamore/tcell" @@ -49,6 +50,7 @@ func (p *Pod) bindKeys(aa ui.KeyActions) { } aa.Add(ui.KeyActions{ + ui.KeyShiftF: ui.NewKeyAction("Port-Forward", p.portFwdCmd, true), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(1, true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(2, true), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd(3, false), false), @@ -64,7 +66,6 @@ func (p *Pod) bindKeys(aa ui.KeyActions) { } func (p *Pod) showContainers(app *App, model ui.Tabular, gvr, path string) { - log.Debug().Msgf("SHOW CONTAINERS %q -- %q -- %q", gvr, model.GetNamespace(), path) co := NewContainer(client.NewGVR("containers")) co.SetContextFn(p.coContext) if err := app.inject(co); err != nil { @@ -128,6 +129,51 @@ func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } +func (p *Pod) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { + path := p.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + pp, err := fetchPodPorts(p.App().factory, path) + if err != nil { + p.App().Flash().Err(err) + return nil + } + ports := make([]string, 0, len(pp)) + for _, p := range pp { + if p.Protocol == v1.ProtocolTCP { + port := fmt.Sprintf("%s:%d", p.Name, p.ContainerPort) + if p.Name == "" { + port = fmt.Sprintf("%d", p.ContainerPort) + } + ports = append(ports, port) + } + } + + if len(ports) == 0 { + p.App().Flash().Err(fmt.Errorf("no tcp ports found on %s", path)) + return nil + } + + dialog.ShowPortForwards(p.App().Content.Pages, p.App().Styles, path, ports, p.portForward) + + return nil +} + +func (p *Pod) portForward(path, address, lport, cport string) { + pf := dao.NewPortForwarder(p.App().Conn()) + ports := []string{lport + ":" + cport} + fw, err := pf.Start(path, "", address, ports) + if err != nil { + p.App().Flash().Err(err) + return + } + + log.Debug().Msgf(">>> Starting port forward %q %v", path, ports) + go runForward(p.App(), pf, fw) +} + // ---------------------------------------------------------------------------- // Helpers... @@ -160,6 +206,7 @@ func containerShellin(a *App, comp model.Component, path, co string) error { func resumeShellIn(a *App, c model.Component, path, co string) { c.Stop() defer c.Start() + shellIn(a, path, co) } @@ -205,10 +252,33 @@ func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, for _, c := range pod.Spec.Containers { nn = append(nn, c.Name) } - if includeInit { - for _, c := range pod.Spec.InitContainers { - nn = append(nn, c.Name) - } + if !includeInit { + return nn, nil } + for _, c := range pod.Spec.InitContainers { + nn = append(nn, c.Name) + } + return nn, nil } + +func fetchPodPorts(f *watch.Factory, path string) ([]v1.ContainerPort, error) { + log.Debug().Msgf("Fetching ports on pod %q", path) + o, err := f.Get("v1/pods", path, false, labels.Everything()) + if err != nil { + return nil, err + } + + var pod v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + if err != nil { + return nil, err + } + + pp := make([]v1.ContainerPort, 0, len(pod.Spec.Containers)) + for _, c := range pod.Spec.Containers { + pp = append(pp, c.Ports...) + } + + return pp, nil +} diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index 00878bdb..8588155c 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) { assert.Nil(t, po.Init(makeCtx())) assert.Equal(t, "Pods", po.Name()) - assert.Equal(t, 18, len(po.Hints())) + assert.Equal(t, 19, len(po.Hints())) } // Helpers... diff --git a/internal/view/registrar.go b/internal/view/registrar.go index 40d228a9..9631348d 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -51,6 +51,9 @@ func miscViewers(vv MetaViewers) { vv[client.NewGVR("contexts")] = MetaViewer{ viewerFn: NewContext, } + vv[client.NewGVR("openfaas")] = MetaViewer{ + viewerFn: NewOpenFaas, + } vv[client.NewGVR("containers")] = MetaViewer{ viewerFn: NewContainer, } diff --git a/internal/view/svc.go b/internal/view/svc.go index 5cd7257b..0a5aff0b 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -8,8 +8,10 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/perf" "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" @@ -40,19 +42,92 @@ func NewService(gvr client.GVR) ResourceViewer { func (s *Service) bindKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ + ui.KeyShiftF: ui.NewKeyAction("Port-Forward", s.portFwdCmd, true), tcell.KeyCtrlB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true), ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd(1, true), false), }) } -func (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) { - o, err := app.factory.Get(gvr, path, true, labels.Everything()) +func podFromSelector(f dao.Factory, ns string, sel map[string]string) (string, error) { + log.Debug().Msgf("Looking for pods %q:%v -- %v", ns, sel, labels.Set(sel).AsSelector()) + oo, err := f.List("v1/pods", ns, true, labels.Set(sel).AsSelector()) if err != nil { - app.Flash().Err(err) + return "", err + } + + if len(oo) == 0 { + return "", fmt.Errorf("no matching pods for %v", sel) + } + + var pod v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(oo[0].(*unstructured.Unstructured).Object, &pod) + if err != nil { + return "", err + } + + return client.FQN(pod.Namespace, pod.Name), nil +} + +func (s *Service) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { + path := s.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + svc, err := fetchService(s.App().factory, s.GVR(), path) + if err != nil { + s.App().Flash().Err(err) + return nil + } + + ns, _ := client.Namespaced(path) + pod, err := podFromSelector(s.App().factory, ns, svc.Spec.Selector) + if err != nil { + s.App().Flash().Err(err) + return nil + } + + pp, err := fetchPodPorts(s.App().factory, pod) + if err != nil { + s.App().Flash().Err(err) + return nil + } + ports := make([]string, 0, len(pp)) + for _, p := range pp { + if p.Protocol == v1.ProtocolTCP { + port := fmt.Sprintf("%s:%d", p.Name, p.ContainerPort) + if p.Name == "" { + port = fmt.Sprintf("%d", p.ContainerPort) + } + ports = append(ports, port) + } + } + + if len(ports) == 0 { + s.App().Flash().Err(fmt.Errorf("no tcp ports found on %s", path)) + return nil + } + + dialog.ShowPortForwards(s.App().Content.Pages, s.App().Styles, pod, ports, s.portForward) + + return nil +} + +func (s *Service) portForward(path, address, lport, cport string) { + pf := dao.NewPortForwarder(s.App().Conn()) + ports := []string{lport + ":" + cport} + fw, err := pf.Start(path, "", address, ports) + if err != nil { + s.App().Flash().Err(err) return } - var svc v1.Service - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) + + log.Debug().Msgf(">>> Starting port forward %q %v", path, ports) + go runForward(s.App(), pf, fw) +} + +func (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) { + svc, err := fetchService(app.factory, gvr, path) if err != nil { app.Flash().Err(err) return @@ -170,6 +245,21 @@ func (s *Service) benchDone() { }) } +// ---------------------------------------------------------------------------- +// Helpers... + +func fetchService(f dao.Factory, gvr, path string) (*v1.Service, error) { + o, err := f.Get(gvr, path, true, labels.Everything()) + if err != nil { + return nil, err + } + + var svc v1.Service + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) + + return &svc, err +} + func benchTimedOut(app *App) { <-time.After(2 * time.Second) app.QueueUpdate(func() { diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go index b09e3dfc..a9a5bd1c 100644 --- a/internal/view/svc_test.go +++ b/internal/view/svc_test.go @@ -132,5 +132,5 @@ func TestServiceNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "Services", s.Name()) - assert.Equal(t, 7, len(s.Hints())) + assert.Equal(t, 8, len(s.Hints())) } diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 4a56f178..3fe01958 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -229,6 +229,9 @@ func (f *Factory) DeleteForwarder(path string) { // Forwarders returns all portforwards. func (f *Factory) Forwarders() Forwarders { + f.mx.RLock() + defer f.mx.RUnlock() + return f.forwarders } diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 00000000..e23d86d7 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,4 @@ +# K9s community plugins + +These plugins provide for extending the K9s cli to provide for more cluster management fu. + diff --git a/plugins/job_suspend.yml b/plugins/job_suspend.yml new file mode 100644 index 00000000..3d4b4ae7 --- /dev/null +++ b/plugins/job_suspend.yml @@ -0,0 +1,19 @@ +plugin: + # Suspends/Resumes a cronjob + suspendCronsToggle: + shortCut: Ctrl-S + scopes: + - cj + description: Suspend toggle + command: kubectl + background: true + args: + - patch + - cronjobs + - $NAME + - ns + - $NAMESPACE + - --context + - $CONTEXT + - -p + - '{"spec" : {"suspend" : $COL3 }}' diff --git a/plugins/kubectl/kubectl-jq b/plugins/kubectl/kubectl-jq new file mode 100755 index 00000000..5229a1d5 --- /dev/null +++ b/plugins/kubectl/kubectl-jq @@ -0,0 +1,3 @@ +#!/bin/bash + +/usr/local/bin/kubectl logs -f $1 -n $2 --context $3 | jq -r '.message' \ No newline at end of file diff --git a/plugins/log_jq.yml b/plugins/log_jq.yml new file mode 100644 index 00000000..4e6d6d3c --- /dev/null +++ b/plugins/log_jq.yml @@ -0,0 +1,14 @@ +plugin: + # Sends logs over to jq for processing. This leverages kubectl plugin kubectl-jq. + jqlogs: + shortCut: Ctrl-J + description: "Logs (jq)" + scopes: + - po + command: kubectl + background: false + args: + - jq + - $NAME + - $NAMESPACE + - $CONTEXT diff --git a/plugins/log_stern.yml b/plugins/log_stern.yml new file mode 100644 index 00000000..83dbe6f1 --- /dev/null +++ b/plugins/log_stern.yml @@ -0,0 +1,17 @@ +plugin: + # Leverage stern (https://github.com/wercker/stern) to output logs. + stern: + shortCut: Ctrl-L + description: "Logs " + scopes: + - pods + command: stern + background: false + args: + - --tail + - 50 + - $FILTER + - -n + - $NAMESPACE + - --context + - $CONTEXT From bda9f4e6a44323443bd7e8ef4080948598654d41 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 13 Feb 2020 14:00:43 -0700 Subject: [PATCH 29/39] update rel notes --- change_logs/release_v0.15.0.md | 55 ++++++++++++++++++++++++++++++++++ internal/view/ofaas.go | 6 ++-- 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 change_logs/release_v0.15.0.md diff --git a/change_logs/release_v0.15.0.md b/change_logs/release_v0.15.0.md new file mode 100644 index 00000000..f680d8a8 --- /dev/null +++ b/change_logs/release_v0.15.0.md @@ -0,0 +1,55 @@ + + +# Release v0.15.0 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## Does This FaaS Look Familiar? + +The awesome and ever so smart and creative [Alex Ellis](https://github.com/alexellis) of [OpenFaas Fame](https://www.openfaas.com) fame, had pinged me when I had launched K9s to add support for OpenFaas functions. It's been a long time coming indeed, but we now have a very (VERY!) primitive integration with this very cool framework. + +The current approach is to enable a few environment variables to tell K9s that you have an OpenFaas cluster available namely: + +```shell +OPENFAAS_GATEWAY=http://YOUR_CLUSTER_IP:31112 +OPENFAAS_TLS_INSECURE=false +OPENFAAS_JWT_TOKEN=YOUR_TOKEN +``` + +These will tell K9s that an OpenFaas gateway is available and exposed on a given nodeport. + +Next you can navigate to your OpenFaas function view by entering command mode `:openfaas` or using aliases `:ofaas` or `ofa` + +If functions are present in the given namespace they will be displayed here just like any other K8s resources. + +The following operations are currently supported: + +* Describe and YAML to view function definitions (Note: currently yields same results!) +* Enter to view all pods instances associated with the selected function +* Delete a function +* Editing, shelling, logs, etc... are all supported by navigating to the underlying pods + +Keep in mind, the paint is way fresh here and this feature could be a complete dud, but figure will give it a rinse on this drop and Alex can pipe in and helps us ironing this out. + +> NOTE! It's been a while since I've played with OpenFaas so if some of you are more versed in this space by all means please do land a hand so we can make this feature more awesome! + +## Moving Forward! + +A few folks had mentioned the eagerness to port-forward directly from a pod or a service. Well now you can! Port Forwarding is now available on both the pod view and services view. Note! at the end of the day, you are still port-fowarding to a container! So the port-forward dialog is a bit different for these views as there might be several container ports available now when looking at this from a pod perspective. So the first field in the dialog is a combo-box that allows one to pick their desired ports. The rest of the dialog works the same as the container port-forward dialog. + +## Resolved Bugs/Features/PRs + +* [Issue #541](https://github.com/derailed/k9s/issues/541) +* [Issue #227](https://github.com/derailed/k9s/issues/227) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/view/ofaas.go b/internal/view/ofaas.go index fda3bd62..fa5799c4 100644 --- a/internal/view/ofaas.go +++ b/internal/view/ofaas.go @@ -26,9 +26,9 @@ func NewOpenFaas(gvr client.GVR) ResourceViewer { func (o *OpenFaas) bindKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ ui.KeyShiftS: ui.NewKeyAction("Sort Status", o.GetTable().SortColCmd(2, true), false), - ui.KeyShiftT: ui.NewKeyAction("Sort Invocations", o.GetTable().SortColCmd(5, false), false), - ui.KeyShiftC: ui.NewKeyAction("Sort Replicas", o.GetTable().SortColCmd(6, false), false), - ui.KeyShiftM: ui.NewKeyAction("Sort Available", o.GetTable().SortColCmd(7, false), false), + ui.KeyShiftI: ui.NewKeyAction("Sort Invocations", o.GetTable().SortColCmd(4, false), false), + ui.KeyShiftR: ui.NewKeyAction("Sort Replicas", o.GetTable().SortColCmd(5, false), false), + ui.KeyShiftV: ui.NewKeyAction("Sort Available", o.GetTable().SortColCmd(6, false), false), }) } From 580d8ea36d12267addee3cf20d0bd663c34ae389 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 13 Feb 2020 17:36:05 -0700 Subject: [PATCH 30/39] oops hosed the build... --- change_logs/release_v0.15.0.md | 1 + go.mod | 4 +--- go.sum | 2 ++ internal/view/app.go | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/change_logs/release_v0.15.0.md b/change_logs/release_v0.15.0.md index f680d8a8..57c823b3 100644 --- a/change_logs/release_v0.15.0.md +++ b/change_logs/release_v0.15.0.md @@ -47,6 +47,7 @@ A few folks had mentioned the eagerness to port-forward directly from a pod or a ## Resolved Bugs/Features/PRs +* [Issue #546](https://github.com/derailed/k9s/issues/546) BREAKING NEWS! Use `t` vs `ctrl-h` now to toggle the K9s header * [Issue #541](https://github.com/derailed/k9s/issues/541) * [Issue #227](https://github.com/derailed/k9s/issues/227) diff --git a/go.mod b/go.mod index 599c7759..1fb59774 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/derailed/k9s go 1.13 -replace github.com/derailed/tview => /Users/fernand/go_wk/derailed/src/github.com/derailed/tview - replace ( github.com/docker/docker => github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f @@ -35,7 +33,7 @@ require ( github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e // indirect github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de // indirect github.com/atotto/clipboard v0.1.2 - github.com/derailed/tview v0.3.4 + github.com/derailed/tview v0.3.5 github.com/drone/envsubst v1.0.2 // indirect github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect diff --git a/go.sum b/go.sum index 07ae4013..34dddd58 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,8 @@ github.com/derailed/tview v0.3.3 h1:tipPwxcDhx0zRBZuc8VKIrNgWL40FL5JeF/30XVieUE= github.com/derailed/tview v0.3.3/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= github.com/derailed/tview v0.3.4 h1:PnF64fLqm48LEjC/XwOS7JufDgFuuPYx85YVt5t3rwE= github.com/derailed/tview v0.3.4/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= +github.com/derailed/tview v0.3.5 h1:1vKqcJIiZtLAs5moX9c38+BbBSYhPgFq0ZndnVNVNFc= +github.com/derailed/tview v0.3.5/go.mod h1:yApPszFU62FoaGkf7swy2nIdV/h7Nid3dhMSVy6+OFI= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= diff --git a/internal/view/app.go b/internal/view/app.go index d36f5745..c0ac13b8 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -114,7 +114,7 @@ func (a *App) Init(version string, rate int) error { func (a *App) bindKeys() { a.AddActions(ui.KeyActions{ - tcell.KeyCtrlH: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false), + ui.KeyT: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false), ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false), tcell.KeyCtrlA: ui.NewSharedKeyAction("Aliases", a.aliasCmd, false), tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false), From 69bf1ee1518019cca05f99f2829e0228fc07a246 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 13 Feb 2020 17:41:00 -0700 Subject: [PATCH 31/39] add openfaas support + bugz. Just in time for OpenFezThurday ;) --- assets/k9s_fez.png | Bin 0 -> 231866 bytes change_logs/release_v0.15.0.md | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 assets/k9s_fez.png diff --git a/assets/k9s_fez.png b/assets/k9s_fez.png new file mode 100644 index 0000000000000000000000000000000000000000..078a33dfacddeb241ddd55febee6d93116595a8d GIT binary patch literal 231866 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 F{{h{BHY@-D literal 0 HcmV?d00001 diff --git a/change_logs/release_v0.15.0.md b/change_logs/release_v0.15.0.md index 57c823b3..c40fa60b 100644 --- a/change_logs/release_v0.15.0.md +++ b/change_logs/release_v0.15.0.md @@ -12,7 +12,9 @@ On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_inv --- -## Does This FaaS Look Familiar? + + +## Seen This Fez Before? The awesome and ever so smart and creative [Alex Ellis](https://github.com/alexellis) of [OpenFaas Fame](https://www.openfaas.com) fame, had pinged me when I had launched K9s to add support for OpenFaas functions. It's been a long time coming indeed, but we now have a very (VERY!) primitive integration with this very cool framework. From cd4e4a72ab873c0b679ee8c6ccb9098826cebd4b Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 13 Feb 2020 17:49:26 -0700 Subject: [PATCH 32/39] and missed one... --- internal/view/app_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/view/app_test.go b/internal/view/app_test.go index 4ff96086..b1f3476f 100644 --- a/internal/view/app_test.go +++ b/internal/view/app_test.go @@ -12,5 +12,5 @@ func TestAppNew(t *testing.T) { a := view.NewApp(config.NewConfig(ks{})) a.Init("blee", 10) - assert.Equal(t, 11, len(a.GetActions())) + assert.Equal(t, 12, len(a.GetActions())) } From 642303bb724257d80818cb45c7e8ca1c60cbcdf4 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 13 Feb 2020 17:56:13 -0700 Subject: [PATCH 33/39] cleaning up --- internal/dao/ofaas.go | 7 +++++-- internal/ui/dialog/port_forwards.go | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/dao/ofaas.go b/internal/dao/ofaas.go index 7feb5402..123e702c 100644 --- a/internal/dao/ofaas.go +++ b/internal/dao/ofaas.go @@ -55,7 +55,7 @@ func getOpenFAASFlags() (string, string, bool) { return gw, token, tlsInsecure } -// List returns a collection of functions +// Get returns a function by name. func (f *OpenFaas) Get(ctx context.Context, path string) (runtime.Object, error) { ns, n := client.Namespaced(path) @@ -83,7 +83,7 @@ func (f *OpenFaas) Get(ctx context.Context, path string) (runtime.Object, error) return found, nil } -// List returns a collection of functions +// List returns a collection of functions. func (f *OpenFaas) List(_ context.Context, ns string) ([]runtime.Object, error) { if !IsOpenFaasEnabled() { return nil, errors.New("OpenFAAS is not enabled on this cluster") @@ -103,6 +103,7 @@ func (f *OpenFaas) List(_ context.Context, ns string) ([]runtime.Object, error) return oo, nil } +// Delete removes a function. func (f *OpenFaas) Delete(path string, _, _ bool) error { gw, token, tls := getOpenFAASFlags() ns, n := client.Namespaced(path) @@ -111,10 +112,12 @@ func (f *OpenFaas) Delete(path string, _, _ bool) error { return deleteFunctionToken(gw, n, tls, token, ns) } +// ToYAML dumps a function to yaml. func (f *OpenFaas) ToYAML(path string) (string, error) { return f.Describe(path) } +// Describe describes a function. func (f *OpenFaas) Describe(path string) (string, error) { o, err := f.Get(context.Background(), path) if err != nil { diff --git a/internal/ui/dialog/port_forwards.go b/internal/ui/dialog/port_forwards.go index 279e4524..45871e3b 100644 --- a/internal/ui/dialog/port_forwards.go +++ b/internal/ui/dialog/port_forwards.go @@ -8,6 +8,7 @@ import ( "github.com/derailed/tview" ) +// PortForwardFunc represents a port-forward callback function. type PortForwardFunc func(path, address, lport, cport string) // ShowPortForwards pops a port forwarding configuration dialog. @@ -56,7 +57,7 @@ func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string p.ShowPage(portForwardKey) } -// DismissPortForward dismiss the port forward dialog. +// DismissPortForwards dismiss the port forward dialog. func DismissPortForwards(p *ui.Pages) { p.RemovePage(portForwardKey) } From a3f19e060d7e68179112e6d988f0e14ef4afe11b Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 13 Feb 2020 19:01:28 -0700 Subject: [PATCH 34/39] and... first openfaas support bug. Meow! --- change_logs/release_v0.15.1.md | 56 ++++++++++++++++++++++++++++++++++ internal/view/ofaas.go | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 change_logs/release_v0.15.1.md diff --git a/change_logs/release_v0.15.1.md b/change_logs/release_v0.15.1.md new file mode 100644 index 00000000..193d9beb --- /dev/null +++ b/change_logs/release_v0.15.1.md @@ -0,0 +1,56 @@ + + +# Release v0.15.1 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + + + +## OpenFeZ Reloaded? + +🙀With feelings and one less bugZ! + +The awesome and ever so smart and creative [Alex Ellis](https://github.com/alexellis) of [OpenFaas Fame](https://www.openfaas.com) fame, had pinged me when I had launched K9s to add support for OpenFaas functions. It's been a long time coming indeed, but we now have a very (VERY!) primitive integration with this very cool framework. + +The current approach is to enable a few environment variables to tell K9s that you have an OpenFaas cluster available namely: + +```shell +OPENFAAS_GATEWAY=http://YOUR_CLUSTER_IP:31112 +OPENFAAS_TLS_INSECURE=false +OPENFAAS_JWT_TOKEN=YOUR_TOKEN +``` + +These will tell K9s that an OpenFaas gateway is available and exposed on a given nodeport. + +Next you can navigate to your OpenFaas function view by entering command mode `:openfaas` or using aliases `:ofaas` or `ofa` + +If functions are present in the given namespace they will be displayed here just like any other K8s resources. + +The following operations are currently supported: + +* Describe and YAML to view function definitions (Note: currently yields same results!) +* Enter to view all pods instances associated with the selected function +* Delete a function +* Editing, shelling, logs, etc... are all supported by navigating to the underlying pods + +Keep in mind, the paint is way fresh here and this feature could be a complete dud, but figure will give it a rinse on this drop and Alex can pipe in and helps us ironing this out. + +> NOTE! It's been a while since I've played with OpenFaas so if some of you are more versed in this space by all means please do land a hand so we can make this feature more awesome! + +## Moving Forward! + +A few folks had mentioned the eagerness to port-forward directly from a pod or a service. Well now you can! Port Forwarding is now available on both the pod view and services view. Note! at the end of the day, you are still port-fowarding to a container! So the port-forward dialog is a bit different for these views as there might be several container ports available now when looking at this from a pod perspective. So the first field in the dialog is a combo-box that allows one to pick their desired ports. The rest of the dialog works the same as the container port-forward dialog. + +## Resolved Bugs/Features/PRs + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/view/ofaas.go b/internal/view/ofaas.go index fa5799c4..00c93a2c 100644 --- a/internal/view/ofaas.go +++ b/internal/view/ofaas.go @@ -33,7 +33,7 @@ func (o *OpenFaas) bindKeys(aa ui.KeyActions) { } func (o *OpenFaas) showPods(a *App, _ ui.Tabular, _, path string) { - labels := o.GetTable().GetSelectedCell(4) + labels := o.GetTable().GetSelectedCell(o.GetTable().NameColIndex() + 3) sels := make(map[string]string) tokens := strings.Split(labels, ",") From 1ef375e78150f2bf7254c3f05435e79a9d8035f2 Mon Sep 17 00:00:00 2001 From: derailed Date: Thu, 13 Feb 2020 19:54:15 -0700 Subject: [PATCH 35/39] update docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 88700e05..9b88228d 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ K9s is available on Linux, macOS and Windows platforms. ## Demo Video +* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM) * [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s) * [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I) * [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8) From 2822ac702ed0a213f8541ca537e436de0aa13b13 Mon Sep 17 00:00:00 2001 From: derailed Date: Fri, 14 Feb 2020 00:57:57 -0700 Subject: [PATCH 36/39] rework portforward + benchmarks --- internal/config/bench.go | 5 +- internal/config/bench_test.go | 2 +- internal/dao/port_forwarder.go | 14 +++- internal/perf/benchmark.go | 2 + internal/ui/dialog/port_forward.go | 65 ------------------- internal/ui/dialog/port_forwards.go | 45 +++++++++++-- ..._forward_test.go => port_forwards_test.go} | 15 +++-- internal/view/benchmark.go | 2 +- internal/view/container.go | 55 ++++++---------- internal/view/pod.go | 60 +++++++++-------- internal/view/port_forward.go | 30 +++++++-- internal/view/svc.go | 29 ++------- internal/watch/forwarders.go | 6 +- 13 files changed, 150 insertions(+), 180 deletions(-) delete mode 100644 internal/ui/dialog/port_forward.go rename internal/ui/dialog/{port_forward_test.go => port_forwards_test.go} (60%) diff --git a/internal/config/bench.go b/internal/config/bench.go index a224f17c..86502c96 100644 --- a/internal/config/bench.go +++ b/internal/config/bench.go @@ -49,11 +49,11 @@ type ( // BenchConfig represents a service benchmark. BenchConfig struct { + Name string C int `yaml:"concurrency"` N int `yaml:"requests"` Auth Auth `yaml:"auth"` HTTP HTTP `yaml:"http"` - Name string } ) @@ -73,7 +73,8 @@ func newBenchmark() Benchmark { } } -func (b Benchmark) empty() bool { +// Empty checks if the benchmark is set +func (b Benchmark) Empty() bool { return b.C == 0 && b.N == 0 } diff --git a/internal/config/bench_test.go b/internal/config/bench_test.go index 77df32fd..36157277 100644 --- a/internal/config/bench_test.go +++ b/internal/config/bench_test.go @@ -19,7 +19,7 @@ func TestBenchEmpty(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.b.empty()) + assert.Equal(t, u.e, u.b.Empty()) }) } } diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index 0b31cf19..38d1d8f5 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -23,6 +23,11 @@ import ( const localhost = "localhost" +// Tunnel represents a host tunnel port mapper. +type Tunnel struct { + Address, LocalPort, ContainerPort string +} + // PortForwarder tracks a port forward stream. type PortForwarder struct { client.Connection @@ -88,8 +93,9 @@ func (p *PortForwarder) FQN() string { } // Start initiates a port forward session for a given pod and ports. -func (p *PortForwarder) Start(path, co, address string, ports []string) (*portforward.PortForwarder, error) { - p.path, p.container, p.ports, p.age = path, co, ports, time.Now() +func (p *PortForwarder) Start(path, co string, t Tunnel) (*portforward.PortForwarder, error) { + fwds := []string{t.LocalPort + ":" + t.ContainerPort} + p.path, p.container, p.ports, p.age = path, co, fwds, time.Now() ns, n := client.Namespaced(path) auth, err := p.CanI(ns, "v1/pods", []string{client.GetVerb}) @@ -99,6 +105,8 @@ func (p *PortForwarder) Start(path, co, address string, ports []string) (*portfo if !auth { return nil, fmt.Errorf("user is not authorized to get pods") } + + // BOZO!! Use the factory! pod, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{}) if err != nil { return nil, err @@ -131,7 +139,7 @@ func (p *PortForwarder) Start(path, co, address string, ports []string) (*portfo Name(n). SubResource("portforward") - return p.forwardPorts("POST", req.URL(), address, ports) + return p.forwardPorts("POST", req.URL(), t.Address, fwds) } func (p *PortForwarder) forwardPorts(method string, url *url.URL, address string, ports []string) (*portforward.PortForwarder, error) { diff --git a/internal/perf/benchmark.go b/internal/perf/benchmark.go index 53dfff8c..cbb669b8 100644 --- a/internal/perf/benchmark.go +++ b/internal/perf/benchmark.go @@ -64,6 +64,8 @@ func (b *Benchmark) init(base, version string) error { } req.Header.Set("User-Agent", ua) + log.Debug().Msgf("Benching %d:%d", b.config.N, b.config.C) + b.worker = &requester.Work{ Request: req, RequestBody: []byte(b.config.HTTP.Body), diff --git a/internal/ui/dialog/port_forward.go b/internal/ui/dialog/port_forward.go deleted file mode 100644 index 52a6cb7d..00000000 --- a/internal/ui/dialog/port_forward.go +++ /dev/null @@ -1,65 +0,0 @@ -package dialog - -import ( - "strings" - - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/gdamore/tcell" -) - -const portForwardKey = "portforward" - -// ShowPortForward pops a port forwarding configuration dialog. -func ShowPortForward(p *ui.Pages, port string, okFn func(address, lport, cport string)) { - f := tview.NewForm() - f.SetItemPadding(0) - f.SetButtonsAlign(tview.AlignCenter). - SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor). - SetButtonTextColor(tview.Styles.PrimaryTextColor). - SetLabelColor(tcell.ColorAqua). - SetFieldTextColor(tcell.ColorOrange) - - p1, p2, address := port, port, "localhost" - f.AddInputField("Pod Port:", p1, 20, nil, func(p string) { - p1 = p - }) - f.AddInputField("Local Port:", p2, 20, nil, func(p string) { - p2 = p - }) - f.AddInputField("Address:", address, 20, nil, func(h string) { - address = h - }) - - f.AddButton("OK", func() { - okFn(address, stripPort(p2), stripPort(p1)) - }) - f.AddButton("Cancel", func() { - DismissPortForward(p) - }) - - modal := tview.NewModalForm("", f) - modal.SetDoneFunc(func(_ int, b string) { - DismissPortForward(p) - }) - p.AddPage(portForwardKey, modal, false, false) - p.ShowPage(portForwardKey) -} - -// DismissPortForward dismiss the port forward dialog. -func DismissPortForward(p *ui.Pages) { - p.RemovePage(portForwardKey) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -// StripPort removes the named port id if present. -func stripPort(p string) string { - tokens := strings.Split(p, ":") - if len(tokens) == 2 { - return strings.Replace(tokens[1], "╱UDP", "", 1) - } - - return p -} diff --git a/internal/ui/dialog/port_forwards.go b/internal/ui/dialog/port_forwards.go index 45871e3b..fbfc6ded 100644 --- a/internal/ui/dialog/port_forwards.go +++ b/internal/ui/dialog/port_forwards.go @@ -2,14 +2,19 @@ package dialog import ( "fmt" + "strings" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" ) +const portForwardKey = "portforward" + // PortForwardFunc represents a port-forward callback function. -type PortForwardFunc func(path, address, lport, cport string) +type PortForwardFunc func(path, co string, mapper dao.Tunnel) // ShowPortForwards pops a port forwarding configuration dialog. func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string, okFn PortForwardFunc) { @@ -23,7 +28,7 @@ func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string p1, p2, address := ports[0], ports[0], "localhost" f.AddDropDown("Container Ports", ports, 0, func(sel string, _ int) { - p1, p2 = sel, stripPort(sel) + p1, p2 = sel, extractPort(sel) }) dropD, ok := f.GetFormItem(0).(*tview.DropDown) @@ -43,15 +48,20 @@ func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string }) f.AddButton("OK", func() { - okFn(path, address, stripPort(p2), stripPort(p1)) + tunnel := dao.Tunnel{ + Address: address, + LocalPort: p2, + ContainerPort: extractPort(p1), + } + okFn(path, extractContainer(p1), tunnel) }) f.AddButton("Cancel", func() { - DismissPortForward(p) + DismissPortForwards(p) }) modal := tview.NewModalForm(fmt.Sprintf("", path), f) modal.SetDoneFunc(func(_ int, b string) { - DismissPortForward(p) + DismissPortForwards(p) }) p.AddPage(portForwardKey, modal, false, false) p.ShowPage(portForwardKey) @@ -61,3 +71,28 @@ func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string func DismissPortForwards(p *ui.Pages) { p.RemovePage(portForwardKey) } + +// ---------------------------------------------------------------------------- +// Helpers... + +func extractPort(p string) string { + tokens := strings.Split(p, ":") + switch { + case len(tokens) < 2: + return tokens[0] + case len(tokens) == 2: + return strings.Replace(tokens[1], "╱UDP", "", 1) + default: + return tokens[1] + } +} + +func extractContainer(p string) string { + tokens := strings.Split(p, ":") + if len(tokens) != 2 { + return "n/a" + } + + co, _ := client.Namespaced(tokens[0]) + return co +} diff --git a/internal/ui/dialog/port_forward_test.go b/internal/ui/dialog/port_forwards_test.go similarity index 60% rename from internal/ui/dialog/port_forward_test.go rename to internal/ui/dialog/port_forwards_test.go index 1c2461e1..6ecb876f 100644 --- a/internal/ui/dialog/port_forward_test.go +++ b/internal/ui/dialog/port_forwards_test.go @@ -3,26 +3,27 @@ package dialog import ( "testing" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/stretchr/testify/assert" ) -func TestPortForwardDialog(t *testing.T) { +func TestPortForwards(t *testing.T) { p := ui.NewPages() - okFunc := func(address, lport, cport string) { - } - ShowPortForward(p, "8080", okFunc) + cbFunc := func(path, co string, t dao.Tunnel) {} + ShowPortForwards(p, config.NewStyles(), "fred", []string{"8080"}, cbFunc) d := p.GetPrimitive(portForwardKey).(*tview.ModalForm) assert.NotNil(t, d) - DismissPortForward(p) + DismissPortForwards(p) assert.Nil(t, p.GetPrimitive(portForwardKey)) } -func TestStripPort(t *testing.T) { +func TestExtractPort(t *testing.T) { uu := map[string]struct { port, e string }{ @@ -40,7 +41,7 @@ func TestStripPort(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, stripPort(u.port)) + assert.Equal(t, u.e, extractPort(u.port)) }) } } diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index 7fdc78f2..059c3703 100644 --- a/internal/view/benchmark.go +++ b/internal/view/benchmark.go @@ -46,7 +46,7 @@ func (b *Benchmark) viewBench(app *App, model ui.Tabular, gvr, path string) { return } - details := NewDetails(b.App(), "Benchmark", fileToSubject(path), false).Update(data) + details := NewDetails(b.App(), "Results", fileToSubject(path), false).Update(data) if err := app.inject(details); err != nil { app.Flash().Err(err) } diff --git a/internal/view/container.go b/internal/view/container.go index 724801eb..941b2889 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -103,6 +103,7 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } + log.Debug().Msgf("CONTAINER-SEL %q", path) if _, ok := c.App().factory.ForwarderFor(fwFQN(c.GetTable().Path, path)); ok { c.App().Flash().Err(fmt.Errorf("A PortForward already exist on container %s", c.GetTable().Path)) return nil @@ -112,12 +113,24 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { if !ok { return nil } - - dialog.ShowPortForward(c.App().Content.Pages, c.preparePort(ports), c.portForward) + log.Debug().Msgf("CONTAINER-PORTS %#v", ports) + dialog.ShowPortForwards(c.App().Content.Pages, c.App().Styles, c.GetTable().Path, ports, c.portForward) return nil } +func (c *Container) portForward(path, co string, t dao.Tunnel) { + pf := dao.NewPortForwarder(c.App().Conn()) + fw, err := pf.Start(path, co, t) + if err != nil { + c.App().Flash().Err(err) + return + } + + log.Debug().Msgf(">>> Starting port forward %q %#v", path, t) + go runForward(c.App(), pf, fw) +} + func (c *Container) isForwardable(path string) ([]string, bool) { state := c.GetTable().GetSelectedCell(3) if state != "Running" { @@ -132,43 +145,15 @@ func (c *Container) isForwardable(path string) ([]string, bool) { return nil, false } - return ports, true -} - -func (c *Container) preparePort(pp []string) string { - var port string - for _, p := range pp { + pp := make([]string, 0, len(ports)) + for _, p := range ports { if !isTCPPort(p) { continue } - port = strings.TrimSpace(p) - tokens := strings.Split(port, ":") - if len(tokens) == 2 { - port = tokens[1] - } - break - } - if port == "" { - c.App().Flash().Warn("No valid TCP port found on this container. User will specify...") - return "MY_TCP_PORT!" + pp = append(pp, path+"/"+p) } - return port -} - -func (c *Container) portForward(address, lport, cport string) { - co := c.GetTable().GetSelectedCell(0) - pf := dao.NewPortForwarder(c.App().Conn()) - path := c.GetTable().GetSelectedItem() - ports := []string{lport + ":" + cport} - fw, err := pf.Start(path, co, address, ports) - if err != nil { - c.App().Flash().Err(err) - return - } - - log.Debug().Msgf(">>> Starting port forward %q %v", path, ports) - go runForward(c.App(), pf, fw) + return pp, true } // ---------------------------------------------------------------------------- @@ -178,7 +163,7 @@ func runForward(a *App, pf *dao.PortForwarder, f *portforward.PortForwarder) { a.QueueUpdateDraw(func() { a.factory.AddForwarder(pf) a.Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) - dialog.DismissPortForward(a.Content.Pages) + dialog.DismissPortForwards(a.Content.Pages) }) pf.SetActive(true) diff --git a/internal/view/pod.go b/internal/view/pod.go index 0a79b7c7..413c18d5 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strconv" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" @@ -135,48 +136,51 @@ func (p *Pod) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - pp, err := fetchPodPorts(p.App().factory, path) - if err != nil { + if err := showFwdDialog(p.App(), path, p.portForward); err != nil { p.App().Flash().Err(err) - return nil } - ports := make([]string, 0, len(pp)) - for _, p := range pp { - if p.Protocol == v1.ProtocolTCP { - port := fmt.Sprintf("%s:%d", p.Name, p.ContainerPort) - if p.Name == "" { - port = fmt.Sprintf("%d", p.ContainerPort) - } - ports = append(ports, port) - } - } - - if len(ports) == 0 { - p.App().Flash().Err(fmt.Errorf("no tcp ports found on %s", path)) - return nil - } - - dialog.ShowPortForwards(p.App().Content.Pages, p.App().Styles, path, ports, p.portForward) return nil } -func (p *Pod) portForward(path, address, lport, cport string) { +func (p *Pod) portForward(path, co string, t dao.Tunnel) { pf := dao.NewPortForwarder(p.App().Conn()) - ports := []string{lport + ":" + cport} - fw, err := pf.Start(path, "", address, ports) + fw, err := pf.Start(path, co, t) if err != nil { p.App().Flash().Err(err) return } - log.Debug().Msgf(">>> Starting port forward %q %v", path, ports) + log.Debug().Msgf(">>> Starting port forward %q:%s %v", path, co, t) go runForward(p.App(), pf, fw) } // ---------------------------------------------------------------------------- // Helpers... +func showFwdDialog(a *App, path string, cb dialog.PortForwardFunc) error { + mm, err := fetchPodPorts(a.factory, path) + if err != nil { + return nil + } + ports := make([]string, 0, len(mm)) + for co, pp := range mm { + for _, p := range pp { + if p.Protocol != v1.ProtocolTCP { + continue + } + ports = append(ports, client.FQN(co, p.Name)+":"+strconv.Itoa(int(p.ContainerPort))) + } + } + + if len(ports) == 0 { + return fmt.Errorf("no tcp ports found on %s", path) + } + dialog.ShowPortForwards(a.Content.Pages, a.Styles, path, ports, cb) + + return nil +} + func containerShellin(a *App, comp model.Component, path, co string) error { if co != "" { resumeShellIn(a, comp, path, co) @@ -262,7 +266,7 @@ func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, return nn, nil } -func fetchPodPorts(f *watch.Factory, path string) ([]v1.ContainerPort, error) { +func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, error) { log.Debug().Msgf("Fetching ports on pod %q", path) o, err := f.Get("v1/pods", path, false, labels.Everything()) if err != nil { @@ -275,9 +279,9 @@ func fetchPodPorts(f *watch.Factory, path string) ([]v1.ContainerPort, error) { return nil, err } - pp := make([]v1.ContainerPort, 0, len(pod.Spec.Containers)) - for _, c := range pod.Spec.Containers { - pp = append(pp, c.Ports...) + pp := make(map[string][]v1.ContainerPort) + for _, co := range pod.Spec.Containers { + pp[co.Name] = co.Ports } return pp, nil diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go index 551e2558..b31e7c84 100644 --- a/internal/view/port_forward.go +++ b/internal/view/port_forward.go @@ -3,6 +3,8 @@ package view import ( "context" "fmt" + "regexp" + "strings" "time" "github.com/derailed/k9s/internal" @@ -63,6 +65,8 @@ func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } +var podNameRX = regexp.MustCompile(`\A(.+)\-(\w{10})\-(\w{5})\z`) + func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { if p.bench != nil { p.App().Status(ui.FlashErr, "Benchmark Canceled!") @@ -71,18 +75,34 @@ func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - sel := p.GetTable().GetSelectedItem() - if sel == "" { + path := p.GetTable().GetSelectedItem() + if path == "" { return nil } + tokens := strings.Split(path, ":") + ns, po := client.Namespaced(tokens[0]) + sections := podNameRX.FindStringSubmatch(po) + log.Debug().Msgf("SECTIONS %q::%q--%#v", ns, po, sections) + if len(sections) >= 1 { + po = sections[1] + } + key := client.FQN(ns, po) + ":" + tokens[1] - r, _ := p.GetTable().GetSelection() cfg := defaultConfig() - if b, ok := p.App().Bench.Benchmarks.Containers[sel]; ok { + if defaults := p.App().Bench.Benchmarks.Defaults; !defaults.Empty() { + cfg.C, cfg.N = defaults.C, defaults.N + } + + log.Debug().Msgf("CUST-CFG %q -- %#v", path, key) + if b, ok := p.App().Bench.Benchmarks.Containers[key]; ok { + log.Debug().Msgf("FOUND CUST BENCH_CFG!") cfg = b } - cfg.Name = sel + cfg.Name = path + log.Debug().Msgf("BenchCFG %q::%#v", path, cfg) + + r, _ := p.GetTable().GetSelection() base := ui.TrimCell(p.GetTable().SelectTable, r, 4) var err error if p.bench, err = perf.NewBenchmark(base, p.App().version, cfg); err != nil { diff --git a/internal/view/svc.go b/internal/view/svc.go index 0a5aff0b..b8270991 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -11,7 +11,6 @@ import ( "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/perf" "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/ui/dialog" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" @@ -87,42 +86,22 @@ func (s *Service) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - pp, err := fetchPodPorts(s.App().factory, pod) - if err != nil { + if err := showFwdDialog(s.App(), pod, s.portForward); err != nil { s.App().Flash().Err(err) - return nil } - ports := make([]string, 0, len(pp)) - for _, p := range pp { - if p.Protocol == v1.ProtocolTCP { - port := fmt.Sprintf("%s:%d", p.Name, p.ContainerPort) - if p.Name == "" { - port = fmt.Sprintf("%d", p.ContainerPort) - } - ports = append(ports, port) - } - } - - if len(ports) == 0 { - s.App().Flash().Err(fmt.Errorf("no tcp ports found on %s", path)) - return nil - } - - dialog.ShowPortForwards(s.App().Content.Pages, s.App().Styles, pod, ports, s.portForward) return nil } -func (s *Service) portForward(path, address, lport, cport string) { +func (s *Service) portForward(path, co string, t dao.Tunnel) { pf := dao.NewPortForwarder(s.App().Conn()) - ports := []string{lport + ":" + cport} - fw, err := pf.Start(path, "", address, ports) + fw, err := pf.Start(path, co, t) if err != nil { s.App().Flash().Err(err) return } - log.Debug().Msgf(">>> Starting port forward %q %v", path, ports) + log.Debug().Msgf(">>> Starting port forward %q %#v", path, t) go runForward(s.App(), pf, fw) } diff --git a/internal/watch/forwarders.go b/internal/watch/forwarders.go index aa53cc1b..60464f32 100644 --- a/internal/watch/forwarders.go +++ b/internal/watch/forwarders.go @@ -4,13 +4,13 @@ import ( "strings" "github.com/rs/zerolog/log" - "k8s.io/client-go/tools/portforward" ) // Forwarder represents a port forwarder. type Forwarder interface { - // Start initializes a port forward. - Start(path, co, address string, ports []string) (*portforward.PortForwarder, error) + // BOZO!! + // // Start initializes a port forward. + // Start(path, co, string, t dao.Tunnel) (*portforward.PortForwarder, error) // Stop terminates a port forward. Stop() From 7a977f31c77bdc4628eb348ade8c20084544c937 Mon Sep 17 00:00:00 2001 From: derailed Date: Fri, 14 Feb 2020 14:28:52 -0700 Subject: [PATCH 37/39] refactor pf and benchmarks --- go.mod | 1 + go.sum | 4 + internal/client/config_test.go | 30 ++-- internal/client/{assets => testdata}/config | 0 internal/client/{assets => testdata}/config.1 | 0 internal/client/tunnel.go | 11 ++ internal/config/alias_test.go | 2 +- internal/config/bench.go | 12 ++ internal/config/bench_test.go | 14 +- internal/config/config_test.go | 26 +-- internal/config/hotkey_test.go | 2 +- internal/config/k9s_test.go | 2 +- internal/config/plugin_test.go | 2 +- internal/config/styles_test.go | 8 +- .../{test_assets => testdata}/alias.yml | 0 .../b_containers.yml | 0 .../b_containers_1.yml | 0 .../{test_assets => testdata}/b_good.yml | 0 .../{test_assets => testdata}/b_toast.yml | 0 .../{test_assets => testdata}/bench-fred.yml | 0 .../black_and_wtf.yml | 0 .../{test_assets => testdata}/empty_skin.yml | 0 .../{test_assets => testdata}/hot_key.yml | 0 .../config/{test_assets => testdata}/k8s.yml | 0 .../config/{test_assets => testdata}/k9s.yml | 0 .../config/{test_assets => testdata}/k9s1.yml | 0 .../{test_assets => testdata}/k9s_old.yml | 0 .../{test_assets => testdata}/k9s_toast.yml | 0 .../kubeconfig-test.yml | 0 .../{test_assets => testdata}/plugin.yml | 0 .../skin_boarked.yml | 0 internal/dao/benchmark_test.go | 4 +- internal/dao/cruiser_test.go | 2 +- internal/dao/dp.go | 46 ++++-- internal/dao/ds.go | 52 +++--- internal/dao/pod.go | 36 ++-- internal/dao/port_forward.go | 50 ++++-- internal/dao/port_forward_test.go | 33 ++++ internal/dao/port_forwarder.go | 49 +++--- internal/dao/registry_test.go | 2 +- internal/dao/sts.go | 45 +++-- internal/dao/svc.go | 62 ++++++- .../default_fred_1577308050814961000.txt | 0 internal/dao/testdata/benchspec.yml | 19 +++ internal/dao/{assets => testdata}/config | 0 internal/dao/{assets => testdata}/config.1 | 0 .../dao/{test_assets => testdata}/crb.json | 0 internal/dao/{assets => testdata}/dr.json | 0 .../dao/{test_assets => testdata}/n1.json | 0 .../dao/{test_assets => testdata}/p1.json | 0 internal/dao/types.go | 6 + internal/model/table_int_test.go | 12 +- internal/model/table_test.go | 2 +- .../model/{test_assets => testdata}/p1.json | 0 internal/render/benchmark_int_test.go | 8 +- internal/render/render_test.go | 2 +- internal/render/{assets => testdata}/b1.txt | 0 internal/render/{assets => testdata}/b2.txt | 0 internal/render/{assets => testdata}/b3.txt | 0 internal/render/{assets => testdata}/b4.txt | 0 internal/render/{assets => testdata}/cj.json | 0 internal/render/{assets => testdata}/cm.json | 0 internal/render/{assets => testdata}/cr.json | 0 internal/render/{assets => testdata}/crb.json | 0 internal/render/{assets => testdata}/crd.json | 0 internal/render/{assets => testdata}/dp.json | 0 internal/render/{assets => testdata}/ds.json | 0 internal/render/{assets => testdata}/ep.json | 0 internal/render/{assets => testdata}/ev.json | 0 internal/render/{assets => testdata}/hpa.json | 0 internal/render/{assets => testdata}/ing.json | 0 internal/render/{assets => testdata}/job.json | 0 internal/render/{assets => testdata}/no.json | 0 internal/render/{assets => testdata}/np.json | 0 internal/render/{assets => testdata}/ns.json | 0 internal/render/{assets => testdata}/pdb.json | 0 internal/render/{assets => testdata}/po.json | 0 .../render/{assets => testdata}/po_init.json | 0 internal/render/{assets => testdata}/pv.json | 0 internal/render/{assets => testdata}/pvc.json | 0 internal/render/{assets => testdata}/rb.json | 0 internal/render/{assets => testdata}/ro.json | 0 internal/render/{assets => testdata}/rs.json | 0 internal/render/{assets => testdata}/sa.json | 0 internal/render/{assets => testdata}/sc.json | 0 internal/render/{assets => testdata}/sec.json | 0 internal/render/{assets => testdata}/sts.json | 0 internal/render/{assets => testdata}/svc.json | 0 internal/ui/config.go | 22 +-- internal/ui/config_test.go | 14 +- internal/ui/dialog/port_forwards_test.go | 47 ------ internal/ui/menu.go | 2 +- internal/view/app.go | 3 +- internal/view/browser.go | 1 - internal/view/container.go | 38 +---- internal/view/dp.go | 28 ++-- internal/view/dp_test.go | 3 +- internal/view/ds.go | 21 +-- internal/view/ds_test.go | 2 +- internal/view/help.go | 4 +- .../port_forwards.go => view/pf_dialog.go} | 42 ++--- internal/view/pf_dialog_test.go | 30 ++++ internal/view/pf_extender.go | 155 ++++++++++++++++++ internal/view/pod.go | 79 +-------- internal/view/port_forward.go | 47 +----- internal/view/sts.go | 8 +- internal/view/sts_test.go | 2 +- internal/view/svc.go | 107 ++---------- internal/view/types.go | 2 + internal/watch/factory.go | 6 + internal/watch/forwarders.go | 16 +- 111 files changed, 683 insertions(+), 540 deletions(-) rename internal/client/{assets => testdata}/config (100%) rename internal/client/{assets => testdata}/config.1 (100%) create mode 100644 internal/client/tunnel.go rename internal/config/{test_assets => testdata}/alias.yml (100%) rename internal/config/{test_assets => testdata}/b_containers.yml (100%) rename internal/config/{test_assets => testdata}/b_containers_1.yml (100%) rename internal/config/{test_assets => testdata}/b_good.yml (100%) rename internal/config/{test_assets => testdata}/b_toast.yml (100%) rename internal/config/{test_assets => testdata}/bench-fred.yml (100%) rename internal/config/{test_assets => testdata}/black_and_wtf.yml (100%) rename internal/config/{test_assets => testdata}/empty_skin.yml (100%) rename internal/config/{test_assets => testdata}/hot_key.yml (100%) rename internal/config/{test_assets => testdata}/k8s.yml (100%) rename internal/config/{test_assets => testdata}/k9s.yml (100%) rename internal/config/{test_assets => testdata}/k9s1.yml (100%) rename internal/config/{test_assets => testdata}/k9s_old.yml (100%) rename internal/config/{test_assets => testdata}/k9s_toast.yml (100%) rename internal/config/{test_assets => testdata}/kubeconfig-test.yml (100%) rename internal/config/{test_assets => testdata}/plugin.yml (100%) rename internal/config/{test_assets => testdata}/skin_boarked.yml (100%) create mode 100644 internal/dao/port_forward_test.go rename internal/dao/{test_assets => testdata}/bench/default_fred_1577308050814961000.txt (100%) create mode 100644 internal/dao/testdata/benchspec.yml rename internal/dao/{assets => testdata}/config (100%) rename internal/dao/{assets => testdata}/config.1 (100%) rename internal/dao/{test_assets => testdata}/crb.json (100%) rename internal/dao/{assets => testdata}/dr.json (100%) rename internal/dao/{test_assets => testdata}/n1.json (100%) rename internal/dao/{test_assets => testdata}/p1.json (100%) rename internal/model/{test_assets => testdata}/p1.json (100%) rename internal/render/{assets => testdata}/b1.txt (100%) rename internal/render/{assets => testdata}/b2.txt (100%) rename internal/render/{assets => testdata}/b3.txt (100%) rename internal/render/{assets => testdata}/b4.txt (100%) rename internal/render/{assets => testdata}/cj.json (100%) rename internal/render/{assets => testdata}/cm.json (100%) rename internal/render/{assets => testdata}/cr.json (100%) rename internal/render/{assets => testdata}/crb.json (100%) rename internal/render/{assets => testdata}/crd.json (100%) rename internal/render/{assets => testdata}/dp.json (100%) rename internal/render/{assets => testdata}/ds.json (100%) rename internal/render/{assets => testdata}/ep.json (100%) rename internal/render/{assets => testdata}/ev.json (100%) rename internal/render/{assets => testdata}/hpa.json (100%) rename internal/render/{assets => testdata}/ing.json (100%) rename internal/render/{assets => testdata}/job.json (100%) rename internal/render/{assets => testdata}/no.json (100%) rename internal/render/{assets => testdata}/np.json (100%) rename internal/render/{assets => testdata}/ns.json (100%) rename internal/render/{assets => testdata}/pdb.json (100%) rename internal/render/{assets => testdata}/po.json (100%) rename internal/render/{assets => testdata}/po_init.json (100%) rename internal/render/{assets => testdata}/pv.json (100%) rename internal/render/{assets => testdata}/pvc.json (100%) rename internal/render/{assets => testdata}/rb.json (100%) rename internal/render/{assets => testdata}/ro.json (100%) rename internal/render/{assets => testdata}/rs.json (100%) rename internal/render/{assets => testdata}/sa.json (100%) rename internal/render/{assets => testdata}/sc.json (100%) rename internal/render/{assets => testdata}/sec.json (100%) rename internal/render/{assets => testdata}/sts.json (100%) rename internal/render/{assets => testdata}/svc.json (100%) delete mode 100644 internal/ui/dialog/port_forwards_test.go rename internal/{ui/dialog/port_forwards.go => view/pf_dialog.go} (64%) create mode 100644 internal/view/pf_dialog_test.go create mode 100644 internal/view/pf_extender.go diff --git a/go.mod b/go.mod index 1fb59774..d6e03b95 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ replace ( require ( fyne.io/fyne v1.2.2 // indirect + github.com/GeertJohan/gomatrix v0.0.0-20190924221747-74328b69a02f // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/alexellis/go-execute v0.0.0-20200124154445-8697e4e28c5e // indirect github.com/alexellis/hmac v0.0.0-20180624211220-5c52ab81c0de // indirect diff --git a/go.sum b/go.sum index 34dddd58..c28d347a 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/GeertJohan/gomatrix v0.0.0-20190924221747-74328b69a02f h1:O4XncXE6+qNjZIvermf2/Z4esEl8K1zFVPbl3l14mjM= +github.com/GeertJohan/gomatrix v0.0.0-20190924221747-74328b69a02f/go.mod h1:HqtsgfzGADJzbZ+MbYAJ+PJnxIxBwBvYjyqd2wWw0j0= github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= @@ -389,6 +391,8 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= diff --git a/internal/client/config_test.go b/internal/client/config_test.go index 19a95703..46137574 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -18,7 +18,7 @@ func init() { } func TestConfigCurrentContext(t *testing.T) { - name, kubeConfig := "blee", "./assets/config" + name, kubeConfig := "blee", "./testdata/config" uu := []struct { flags *genericclioptions.ConfigFlags context string @@ -36,7 +36,7 @@ func TestConfigCurrentContext(t *testing.T) { } func TestConfigCurrentCluster(t *testing.T) { - name, kubeConfig := "blee", "./assets/config" + name, kubeConfig := "blee", "./testdata/config" uu := []struct { flags *genericclioptions.ConfigFlags cluster string @@ -54,7 +54,7 @@ func TestConfigCurrentCluster(t *testing.T) { } func TestConfigCurrentUser(t *testing.T) { - name, kubeConfig := "blee", "./assets/config" + name, kubeConfig := "blee", "./testdata/config" uu := []struct { flags *genericclioptions.ConfigFlags user string @@ -72,7 +72,7 @@ func TestConfigCurrentUser(t *testing.T) { } func TestConfigCurrentNamespace(t *testing.T) { - name, kubeConfig := "blee", "./assets/config" + name, kubeConfig := "blee", "./testdata/config" uu := []struct { flags *genericclioptions.ConfigFlags namespace string @@ -91,7 +91,7 @@ func TestConfigCurrentNamespace(t *testing.T) { } func TestConfigGetContext(t *testing.T) { - kubeConfig := "./assets/config" + kubeConfig := "./testdata/config" uu := []struct { flags *genericclioptions.ConfigFlags cluster string @@ -114,7 +114,7 @@ func TestConfigGetContext(t *testing.T) { } func TestConfigSwitchContext(t *testing.T) { - cluster, kubeConfig := "duh", "./assets/config" + cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, ClusterName: &cluster, @@ -129,7 +129,7 @@ func TestConfigSwitchContext(t *testing.T) { } func TestConfigClusterNameFromContext(t *testing.T) { - cluster, kubeConfig := "duh", "./assets/config" + cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, ClusterName: &cluster, @@ -142,7 +142,7 @@ func TestConfigClusterNameFromContext(t *testing.T) { } func TestConfigAccess(t *testing.T) { - cluster, kubeConfig := "duh", "./assets/config" + cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, ClusterName: &cluster, @@ -155,7 +155,7 @@ func TestConfigAccess(t *testing.T) { } func TestConfigContexts(t *testing.T) { - cluster, kubeConfig := "duh", "./assets/config" + cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, ClusterName: &cluster, @@ -168,7 +168,7 @@ func TestConfigContexts(t *testing.T) { } func TestConfigContextNames(t *testing.T) { - cluster, kubeConfig := "duh", "./assets/config" + cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, ClusterName: &cluster, @@ -181,7 +181,7 @@ func TestConfigContextNames(t *testing.T) { } func TestConfigClusterNames(t *testing.T) { - cluster, kubeConfig := "duh", "./assets/config" + cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, ClusterName: &cluster, @@ -194,7 +194,7 @@ func TestConfigClusterNames(t *testing.T) { } func TestConfigDelContext(t *testing.T) { - cluster, kubeConfig := "duh", "./assets/config.1" + cluster, kubeConfig := "duh", "./testdata/config.1" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, ClusterName: &cluster, @@ -209,7 +209,7 @@ func TestConfigDelContext(t *testing.T) { } func TestConfigRestConfig(t *testing.T) { - kubeConfig := "./assets/config" + kubeConfig := "./testdata/config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, } @@ -221,7 +221,7 @@ func TestConfigRestConfig(t *testing.T) { } func TestConfigBadConfig(t *testing.T) { - kubeConfig := "./assets/bork_config" + kubeConfig := "./testdata/bork_config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, } @@ -232,7 +232,7 @@ func TestConfigBadConfig(t *testing.T) { } func TestNamespaceNames(t *testing.T) { - kubeConfig := "./assets/config" + kubeConfig := "./testdata/config" flags := genericclioptions.ConfigFlags{ KubeConfig: &kubeConfig, diff --git a/internal/client/assets/config b/internal/client/testdata/config similarity index 100% rename from internal/client/assets/config rename to internal/client/testdata/config diff --git a/internal/client/assets/config.1 b/internal/client/testdata/config.1 similarity index 100% rename from internal/client/assets/config.1 rename to internal/client/testdata/config.1 diff --git a/internal/client/tunnel.go b/internal/client/tunnel.go new file mode 100644 index 00000000..2eba5a37 --- /dev/null +++ b/internal/client/tunnel.go @@ -0,0 +1,11 @@ +package client + +// PortTunnel represents a host tunnel port mapper. +type PortTunnel struct { + Address, LocalPort, ContainerPort string +} + +// PortMap returns a port mapping. +func (t PortTunnel) PortMap() string { + return t.LocalPort + ":" + t.ContainerPort +} diff --git a/internal/config/alias_test.go b/internal/config/alias_test.go index 80416d70..49e3c445 100644 --- a/internal/config/alias_test.go +++ b/internal/config/alias_test.go @@ -72,7 +72,7 @@ func TestAliasDefine(t *testing.T) { func TestAliasesLoad(t *testing.T) { a := config.NewAliases() - assert.Nil(t, a.LoadAliases("test_assets/alias.yml")) + assert.Nil(t, a.LoadAliases("testdata/alias.yml")) assert.Equal(t, 2, len(a.Alias)) } diff --git a/internal/config/bench.go b/internal/config/bench.go index 86502c96..f3e0a149 100644 --- a/internal/config/bench.go +++ b/internal/config/bench.go @@ -105,3 +105,15 @@ func (s *Bench) load(path string) error { return yaml.Unmarshal(f, &s) } + +// DefaultBenchSpec returns a default bench spec. +func DefaultBenchSpec() BenchConfig { + return BenchConfig{ + C: DefaultC, + N: DefaultN, + HTTP: HTTP{ + Method: DefaultMethod, + Path: "/", + }, + } +} diff --git a/internal/config/bench_test.go b/internal/config/bench_test.go index 36157277..fd6e4d97 100644 --- a/internal/config/bench_test.go +++ b/internal/config/bench_test.go @@ -32,14 +32,14 @@ func TestBenchLoad(t *testing.T) { coCount int }{ "goodConfig": { - "test_assets/b_good.yml", + "testdata/b_good.yml", 2, 1000, 2, 0, }, "malformed": { - "test_assets/b_toast.yml", + "testdata/b_toast.yml", 1, 200, 0, @@ -100,7 +100,7 @@ func TestBenchServiceLoad(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - b, err := NewBench("test_assets/b_good.yml") + b, err := NewBench("testdata/b_good.yml") assert.Nil(t, err) assert.Equal(t, 2, len(b.Benchmarks.Services)) @@ -119,16 +119,16 @@ func TestBenchServiceLoad(t *testing.T) { } func TestBenchReLoad(t *testing.T) { - b, err := NewBench("test_assets/b_containers.yml") + b, err := NewBench("testdata/b_containers.yml") assert.Nil(t, err) assert.Equal(t, 2, b.Benchmarks.Defaults.C) - assert.Nil(t, b.Reload("test_assets/b_containers_1.yml")) + assert.Nil(t, b.Reload("testdata/b_containers_1.yml")) assert.Equal(t, 20, b.Benchmarks.Defaults.C) } func TestBenchLoadToast(t *testing.T) { - _, err := NewBench("test_assets/toast.yml") + _, err := NewBench("testdata/toast.yml") assert.NotNil(t, err) } @@ -171,7 +171,7 @@ func TestBenchContainerLoad(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - b, err := NewBench("test_assets/b_containers.yml") + b, err := NewBench("testdata/b_containers.yml") assert.Nil(t, err) assert.Equal(t, 2, len(b.Benchmarks.Services)) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 52a6c570..0f940013 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -18,7 +18,7 @@ func init() { } func TestConfigRefine(t *testing.T) { - cfgFile, ctx, cluster, ns := "test_assets/kubeconfig-test.yml", "test", "c1", "ns1" + cfgFile, ctx, cluster, ns := "testdata/kubeconfig-test.yml", "test", "c1", "ns1" uu := map[string]struct { flags *genericclioptions.ConfigFlags issue bool @@ -85,7 +85,7 @@ func TestConfigValidate(t *testing.T) { cfg := config.NewConfig(mk) cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) cfg.Validate() // mc.VerifyWasCalledOnce().ValidNamespaces() } @@ -93,7 +93,7 @@ func TestConfigValidate(t *testing.T) { func TestConfigLoad(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) assert.Equal(t, 2, cfg.K9s.RefreshRate) assert.Equal(t, 200, cfg.K9s.LogBufferSize) @@ -119,7 +119,7 @@ func TestConfigCurrentCluster(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) assert.NotNil(t, cfg.CurrentCluster()) assert.Equal(t, "kube-system", cfg.CurrentCluster().Namespace.Active) assert.Equal(t, "ctx", cfg.CurrentCluster().View.Active) @@ -129,7 +129,7 @@ func TestConfigActiveNamespace(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) assert.Equal(t, "kube-system", cfg.ActiveNamespace()) } @@ -142,7 +142,7 @@ func TestConfigSetActiveNamespace(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) assert.Nil(t, cfg.SetActiveNamespace("default")) assert.Equal(t, "default", cfg.ActiveNamespace()) } @@ -151,7 +151,7 @@ func TestConfigActiveView(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) assert.Equal(t, "ctx", cfg.ActiveView()) } @@ -164,7 +164,7 @@ func TestConfigSetActiveView(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) cfg.SetActiveView("po") assert.Equal(t, "po", cfg.ActiveView()) } @@ -173,7 +173,7 @@ func TestConfigFavNamespaces(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) expectedNS := []string{"default", "kube-public", "istio-system", "all", "kube-system"} assert.Equal(t, expectedNS, cfg.FavNamespaces()) } @@ -181,13 +181,13 @@ func TestConfigFavNamespaces(t *testing.T) { func TestConfigLoadOldCfg(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s_old.yml")) + assert.Nil(t, cfg.Load("testdata/k9s_old.yml")) } func TestConfigLoadCrap(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.NotNil(t, cfg.Load("test_assets/k9s_not_there.yml")) + assert.NotNil(t, cfg.Load("testdata/k9s_not_there.yml")) } func TestConfigSaveFile(t *testing.T) { @@ -203,7 +203,7 @@ func TestConfigSaveFile(t *testing.T) { cfg := config.NewConfig(mk) cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) cfg.K9s.RefreshRate = 100 cfg.K9s.ReadOnly = true cfg.K9s.LogBufferSize = 500 @@ -233,7 +233,7 @@ func TestConfigReset(t *testing.T) { cfg := config.NewConfig(mk) cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) cfg.Reset() cfg.Validate() diff --git a/internal/config/hotkey_test.go b/internal/config/hotkey_test.go index 38f8242b..96515b24 100644 --- a/internal/config/hotkey_test.go +++ b/internal/config/hotkey_test.go @@ -9,7 +9,7 @@ import ( func TestHotKeyLoad(t *testing.T) { h := config.NewHotKeys() - assert.Nil(t, h.LoadHotKeys("test_assets/hot_key.yml")) + assert.Nil(t, h.LoadHotKeys("testdata/hot_key.yml")) assert.Equal(t, 1, len(h.HotKey)) diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go index 65f45a27..4b061de6 100644 --- a/internal/config/k9s_test.go +++ b/internal/config/k9s_test.go @@ -72,7 +72,7 @@ func TestK9sActiveClusterBlank(t *testing.T) { func TestK9sActiveCluster(t *testing.T) { mk := NewMockKubeSettings() cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("test_assets/k9s.yml")) + assert.Nil(t, cfg.Load("testdata/k9s.yml")) cl := cfg.K9s.ActiveCluster() assert.NotNil(t, cl) diff --git a/internal/config/plugin_test.go b/internal/config/plugin_test.go index 7a81412b..43f8e278 100644 --- a/internal/config/plugin_test.go +++ b/internal/config/plugin_test.go @@ -9,7 +9,7 @@ import ( func TestPluginLoad(t *testing.T) { p := config.NewPlugins() - assert.Nil(t, p.LoadPlugins("test_assets/plugin.yml")) + assert.Nil(t, p.LoadPlugins("testdata/plugin.yml")) assert.Equal(t, 1, len(p.Plugin)) k, ok := p.Plugin["blah"] diff --git a/internal/config/styles_test.go b/internal/config/styles_test.go index ee9fe85a..bdb0d7ee 100644 --- a/internal/config/styles_test.go +++ b/internal/config/styles_test.go @@ -27,7 +27,7 @@ func TestAsColor(t *testing.T) { func TestSkinNone(t *testing.T) { s := config.NewStyles() - assert.Nil(t, s.Load("test_assets/empty_skin.yml")) + assert.Nil(t, s.Load("testdata/empty_skin.yml")) s.Update() assert.Equal(t, "cadetblue", s.Body().FgColor) @@ -40,7 +40,7 @@ func TestSkinNone(t *testing.T) { func TestSkin(t *testing.T) { s := config.NewStyles() - assert.Nil(t, s.Load("test_assets/black_and_wtf.yml")) + assert.Nil(t, s.Load("testdata/black_and_wtf.yml")) s.Update() assert.Equal(t, "white", s.Body().FgColor) @@ -53,10 +53,10 @@ func TestSkin(t *testing.T) { func TestSkinNotExits(t *testing.T) { s := config.NewStyles() - assert.NotNil(t, s.Load("test_assets/blee.yml")) + assert.NotNil(t, s.Load("testdata/blee.yml")) } func TestSkinBoarked(t *testing.T) { s := config.NewStyles() - assert.NotNil(t, s.Load("test_assets/skin_boarked.yml")) + assert.NotNil(t, s.Load("testdata/skin_boarked.yml")) } diff --git a/internal/config/test_assets/alias.yml b/internal/config/testdata/alias.yml similarity index 100% rename from internal/config/test_assets/alias.yml rename to internal/config/testdata/alias.yml diff --git a/internal/config/test_assets/b_containers.yml b/internal/config/testdata/b_containers.yml similarity index 100% rename from internal/config/test_assets/b_containers.yml rename to internal/config/testdata/b_containers.yml diff --git a/internal/config/test_assets/b_containers_1.yml b/internal/config/testdata/b_containers_1.yml similarity index 100% rename from internal/config/test_assets/b_containers_1.yml rename to internal/config/testdata/b_containers_1.yml diff --git a/internal/config/test_assets/b_good.yml b/internal/config/testdata/b_good.yml similarity index 100% rename from internal/config/test_assets/b_good.yml rename to internal/config/testdata/b_good.yml diff --git a/internal/config/test_assets/b_toast.yml b/internal/config/testdata/b_toast.yml similarity index 100% rename from internal/config/test_assets/b_toast.yml rename to internal/config/testdata/b_toast.yml diff --git a/internal/config/test_assets/bench-fred.yml b/internal/config/testdata/bench-fred.yml similarity index 100% rename from internal/config/test_assets/bench-fred.yml rename to internal/config/testdata/bench-fred.yml diff --git a/internal/config/test_assets/black_and_wtf.yml b/internal/config/testdata/black_and_wtf.yml similarity index 100% rename from internal/config/test_assets/black_and_wtf.yml rename to internal/config/testdata/black_and_wtf.yml diff --git a/internal/config/test_assets/empty_skin.yml b/internal/config/testdata/empty_skin.yml similarity index 100% rename from internal/config/test_assets/empty_skin.yml rename to internal/config/testdata/empty_skin.yml diff --git a/internal/config/test_assets/hot_key.yml b/internal/config/testdata/hot_key.yml similarity index 100% rename from internal/config/test_assets/hot_key.yml rename to internal/config/testdata/hot_key.yml diff --git a/internal/config/test_assets/k8s.yml b/internal/config/testdata/k8s.yml similarity index 100% rename from internal/config/test_assets/k8s.yml rename to internal/config/testdata/k8s.yml diff --git a/internal/config/test_assets/k9s.yml b/internal/config/testdata/k9s.yml similarity index 100% rename from internal/config/test_assets/k9s.yml rename to internal/config/testdata/k9s.yml diff --git a/internal/config/test_assets/k9s1.yml b/internal/config/testdata/k9s1.yml similarity index 100% rename from internal/config/test_assets/k9s1.yml rename to internal/config/testdata/k9s1.yml diff --git a/internal/config/test_assets/k9s_old.yml b/internal/config/testdata/k9s_old.yml similarity index 100% rename from internal/config/test_assets/k9s_old.yml rename to internal/config/testdata/k9s_old.yml diff --git a/internal/config/test_assets/k9s_toast.yml b/internal/config/testdata/k9s_toast.yml similarity index 100% rename from internal/config/test_assets/k9s_toast.yml rename to internal/config/testdata/k9s_toast.yml diff --git a/internal/config/test_assets/kubeconfig-test.yml b/internal/config/testdata/kubeconfig-test.yml similarity index 100% rename from internal/config/test_assets/kubeconfig-test.yml rename to internal/config/testdata/kubeconfig-test.yml diff --git a/internal/config/test_assets/plugin.yml b/internal/config/testdata/plugin.yml similarity index 100% rename from internal/config/test_assets/plugin.yml rename to internal/config/testdata/plugin.yml diff --git a/internal/config/test_assets/skin_boarked.yml b/internal/config/testdata/skin_boarked.yml similarity index 100% rename from internal/config/test_assets/skin_boarked.yml rename to internal/config/testdata/skin_boarked.yml diff --git a/internal/dao/benchmark_test.go b/internal/dao/benchmark_test.go index 767abef1..b3e52f21 100644 --- a/internal/dao/benchmark_test.go +++ b/internal/dao/benchmark_test.go @@ -15,10 +15,10 @@ func TestBenchmarkList(t *testing.T) { a := dao.Benchmark{} a.Init(makeFactory(), client.NewGVR("benchmarks")) - ctx := context.WithValue(context.Background(), internal.KeyDir, "test_assets/bench") + ctx := context.WithValue(context.Background(), internal.KeyDir, "testdata/bench") oo, err := a.List(ctx, "-") assert.Nil(t, err) assert.Equal(t, 1, len(oo)) - assert.Equal(t, "test_assets/bench/default_fred_1577308050814961000.txt", oo[0].(render.BenchInfo).Path) + assert.Equal(t, "testdata/bench/default_fred_1577308050814961000.txt", oo[0].(render.BenchInfo).Path) } diff --git a/internal/dao/cruiser_test.go b/internal/dao/cruiser_test.go index 33981345..8574d8a4 100644 --- a/internal/dao/cruiser_test.go +++ b/internal/dao/cruiser_test.go @@ -29,7 +29,7 @@ func TestCruiserSlice(t *testing.T) { // Helpers... func loadJSON(t assert.TestingT, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n)) + raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured diff --git a/internal/dao/dp.go b/internal/dao/dp.go index e61b07f6..6f937b25 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -21,6 +21,7 @@ var ( _ Loggable = (*Deployment)(nil) _ Restartable = (*Deployment)(nil) _ Scalable = (*Deployment)(nil) + _ Controller = (*Deployment)(nil) ) // Deployment represents a deployment K8s resource. @@ -51,12 +52,7 @@ func (d *Deployment) Scale(path string, replicas int32) error { // Restart a Deployment rollout. func (d *Deployment) Restart(path string) error { - o, err := d.Factory.Get(d.gvr.String(), path, true, labels.Everything()) - if err != nil { - return err - } - var ds appsv1.Deployment - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + dp, err := d.GetInstance(path) if err != nil { return err } @@ -69,30 +65,50 @@ func (d *Deployment) Restart(path string) error { if !auth { return fmt.Errorf("user is not authorized to restart a deployment") } - update, err := polymorphichelpers.ObjectRestarterFn(&ds) + update, err := polymorphichelpers.ObjectRestarterFn(dp) if err != nil { return err } - _, err = d.Client().DialOrDie().AppsV1().Deployments(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update) + _, err = d.Client().DialOrDie().AppsV1().Deployments(dp.Namespace).Patch(dp.Name, types.StrategicMergePatchType, update) return err } // TailLogs tail logs for all pods represented by this Deployment. func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { - o, err := d.Factory.Get(d.gvr.String(), opts.Path, true, labels.Everything()) + dp, err := d.GetInstance(opts.Path) if err != nil { return err } - - var dp appsv1.Deployment - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) - if err != nil { - return errors.New("expecting Deployment resource") - } if dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 { return fmt.Errorf("No valid selector found on Deployment %s", opts.Path) } return podLogs(ctx, c, dp.Spec.Selector.MatchLabels, opts) } + +// Pod returns a pod victim by name. +func (d *Deployment) Pod(fqn string) (string, error) { + dp, err := d.GetInstance(fqn) + if err != nil { + return "", err + } + + return podFromSelector(d.Factory, dp.Namespace, dp.Spec.Selector.MatchLabels) +} + +// GetInstance returns a deployment instance. +func (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) { + o, err := d.Factory.Get(d.gvr.String(), fqn, false, labels.Everything()) + if err != nil { + return nil, err + } + + var dp appsv1.Deployment + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) + if err != nil { + return nil, errors.New("expecting Deployment resource") + } + + return &dp, nil +} diff --git a/internal/dao/ds.go b/internal/dao/ds.go index d42b159e..89a0877d 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -5,12 +5,10 @@ import ( "errors" "fmt" "strings" - "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/watch" - "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,6 +24,7 @@ var ( _ Nuker = (*DaemonSet)(nil) _ Loggable = (*DaemonSet)(nil) _ Restartable = (*DaemonSet)(nil) + _ Controller = (*DaemonSet)(nil) ) // DaemonSet represents a K8s daemonset. @@ -35,12 +34,7 @@ type DaemonSet struct { // Restart a DaemonSet rollout. func (d *DaemonSet) Restart(path string) error { - o, err := d.Factory.Get(d.gvr.String(), path, true, labels.Everything()) - if err != nil { - return err - } - var ds appsv1.DaemonSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + ds, err := d.GetInstance(path) if err != nil { return err } @@ -52,26 +46,21 @@ func (d *DaemonSet) Restart(path string) error { if !auth { return fmt.Errorf("user is not authorized to restart a daemonset") } - update, err := polymorphichelpers.ObjectRestarterFn(&ds) + update, err := polymorphichelpers.ObjectRestarterFn(ds) if err != nil { return err } - _, err = d.Client().DialOrDie().AppsV1().DaemonSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update) + _, err = d.Client().DialOrDie().AppsV1().DaemonSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update) return err } // TailLogs tail logs for all pods represented by this DaemonSet. func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { - o, err := d.Factory.Get(d.gvr.String(), opts.Path, true, labels.Everything()) + ds, err := d.GetInstance(opts.Path) if err != nil { return err } - var ds appsv1.DaemonSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) - if err != nil { - return errors.New("expecting daemonset resource") - } if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 { return fmt.Errorf("no valid selector found on daemonset %q", opts.Path) @@ -81,10 +70,6 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptio } func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts LogOptions) error { - defer func(t time.Time) { - log.Debug().Msgf("POD LOGS %v", time.Since(t)) - }(time.Now()) - f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) if !ok { return errors.New("expecting a context factory") @@ -116,7 +101,6 @@ func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts L if err != nil { return err } - log.Debug().Msgf("TAILING logs on pod %q", pod.Name) opts.Path = client.FQN(pod.Namespace, pod.Name) if err := po.TailLogs(ctx, c, opts); err != nil { return err @@ -125,6 +109,32 @@ func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts L return nil } +// Pod returns a pod victim by name. +func (d *DaemonSet) Pod(fqn string) (string, error) { + ds, err := d.GetInstance(fqn) + if err != nil { + return "", err + } + + return podFromSelector(d.Factory, ds.Namespace, ds.Spec.Selector.MatchLabels) +} + +// GetInstance returns a daemonset instance. +func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) { + o, err := d.Factory.Get(d.gvr.String(), fqn, false, labels.Everything()) + if err != nil { + return nil, err + } + + var ds appsv1.DaemonSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + if err != nil { + return nil, errors.New("expecting DaemonSet resource") + } + + return &ds, nil +} + // ---------------------------------------------------------------------------- // Helpers... diff --git a/internal/dao/pod.go b/internal/dao/pod.go index faf39cb5..1fddf20b 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -27,9 +27,10 @@ import ( const defaultTimeout = 1 * time.Second var ( - _ Accessor = (*Pod)(nil) - _ Nuker = (*Pod)(nil) - _ Loggable = (*Pod)(nil) + _ Accessor = (*Pod)(nil) + _ Nuker = (*Pod)(nil) + _ Loggable = (*Pod)(nil) + _ Controller = (*Pod)(nil) ) // Pod represents a pod resource. @@ -122,13 +123,7 @@ func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, er // Containers returns all container names on pod func (p *Pod) Containers(path string, includeInit bool) ([]string, error) { - o, err := p.Factory.Get(p.gvr.String(), path, true, labels.Everything()) - if err != nil { - return nil, err - } - - var pod v1.Pod - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + pod, err := p.GetInstance(path) if err != nil { return nil, err } @@ -147,6 +142,27 @@ func (p *Pod) Containers(path string, includeInit bool) ([]string, error) { return cc, nil } +// Pod returns a pod victim by name. +func (p *Pod) Pod(fqn string) (string, error) { + return fqn, nil +} + +// GetInstance returns a pod instance. +func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) { + o, err := p.Factory.Get(p.gvr.String(), fqn, false, labels.Everything()) + if err != nil { + return nil, err + } + + var pod v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + if err != nil { + return nil, err + } + + return &pod, nil +} + // TailLogs tails a given container logs func (p *Pod) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { if !opts.HasContainer() { diff --git a/internal/dao/port_forward.go b/internal/dao/port_forward.go index 02852886..e88f7969 100644 --- a/internal/dao/port_forward.go +++ b/internal/dao/port_forward.go @@ -3,12 +3,14 @@ package dao import ( "context" "fmt" + "regexp" "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/runtime" ) @@ -40,21 +42,26 @@ func (p *PortForward) Delete(path string, cascade, force bool) error { // List returns a collection of screen dumps. func (p *PortForward) List(ctx context.Context, _ string) ([]runtime.Object, error) { - config, ok := ctx.Value(internal.KeyBenchCfg).(*config.Bench) + benchFile, ok := ctx.Value(internal.KeyBenchCfg).(string) if !ok { - return nil, fmt.Errorf("no benchconfig found in context") + return nil, fmt.Errorf("no bench file found in context") + } + + config, err := config.NewBench(benchFile) + if err != nil { + log.Debug().Msgf("No custom benchmark config file found") } cc := config.Benchmarks.Containers oo := make([]runtime.Object, 0, len(p.Factory.Forwarders())) - for _, f := range p.Factory.Forwarders() { + for k, f := range p.Factory.Forwarders() { cfg := render.BenchCfg{ C: config.Benchmarks.Defaults.C, N: config.Benchmarks.Defaults.N, } - if config, ok := cc[containerID(f.Path(), f.Container())]; ok { - cfg.C, cfg.N = config.C, config.N - cfg.Host, cfg.Path = config.HTTP.Host, config.HTTP.Path + if cust, ok := cc[PodToKey(k)]; ok { + cfg.C, cfg.N = cust.C, cust.N + cfg.Host, cfg.Path = cust.HTTP.Host, cust.HTTP.Path } oo = append(oo, render.ForwardRes{ Forwarder: f, @@ -68,10 +75,31 @@ func (p *PortForward) List(ctx context.Context, _ string) ([]runtime.Object, err // ---------------------------------------------------------------------------- // Helpers... -// ContainerID computes container ID based on ns/po/co. -func containerID(path, co string) string { - ns, n := client.Namespaced(path) - po := strings.Split(n, "-")[0] +var podNameRX = regexp.MustCompile(`\A(.+)\-(\w{10})\-(\w{5})\z`) - return ns + "/" + po + ":" + co +// PodToKey converts a pod path to a generic bench config key +func PodToKey(path string) string { + tokens := strings.Split(path, ":") + ns, po := client.Namespaced(tokens[0]) + sections := podNameRX.FindStringSubmatch(po) + if len(sections) >= 1 { + po = sections[1] + } + return client.FQN(ns, po) + ":" + tokens[1] +} + +// BenchConfigFor returns a custom bench spec if defined otherwise returns the default one. +func BenchConfigFor(benchFile, path string) config.BenchConfig { + def := config.DefaultBenchSpec() + cust, err := config.NewBench(benchFile) + if err != nil { + log.Debug().Msgf("No custom benchmark config file found") + return def + } + if b, ok := cust.Benchmarks.Containers[PodToKey(path)]; ok { + return b + } + + def.C, def.N = cust.Benchmarks.Defaults.C, cust.Benchmarks.Defaults.N + return def } diff --git a/internal/dao/port_forward_test.go b/internal/dao/port_forward_test.go new file mode 100644 index 00000000..691eee7f --- /dev/null +++ b/internal/dao/port_forward_test.go @@ -0,0 +1,33 @@ +package dao_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" + "github.com/stretchr/testify/assert" +) + +func TestBenchForConfig(t *testing.T) { + uu := map[string]struct { + file, key string + spec config.BenchConfig + }{ + "no_file": {file: "", key: "", spec: config.DefaultBenchSpec()}, + "spec": {file: "testdata/benchspec.yml", key: "default/nginx-123-456:nginx", spec: config.BenchConfig{ + C: 2, + N: 3000, + HTTP: config.HTTP{ + Method: "GET", + Path: "/", + }, + }}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.NotNil(t, u.spec, dao.BenchConfigFor(u.file, u.key)) + }) + } +} diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index 38d1d8f5..f91175df 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -23,14 +23,9 @@ import ( const localhost = "localhost" -// Tunnel represents a host tunnel port mapper. -type Tunnel struct { - Address, LocalPort, ContainerPort string -} - // PortForwarder tracks a port forward stream. type PortForwarder struct { - client.Connection + Factory genericclioptions.IOStreams stopChan, readyChan chan struct{} @@ -42,11 +37,11 @@ type PortForwarder struct { } // NewPortForwarder returns a new port forward streamer. -func NewPortForwarder(c client.Connection) *PortForwarder { +func NewPortForwarder(f Factory) *PortForwarder { return &PortForwarder{ - Connection: c, - stopChan: make(chan struct{}), - readyChan: make(chan struct{}), + Factory: f, + stopChan: make(chan struct{}), + readyChan: make(chan struct{}), } } @@ -72,7 +67,12 @@ func (p *PortForwarder) Ports() []string { // Path returns the pod resource path. func (p *PortForwarder) Path() string { - return p.path + ":" + p.container + return PortForwardID(p.path, p.container) +} + +// PortForwardID computes port-forward identifier. +func PortForwardID(path, co string) string { + return path + ":" + co } // Container returns the targetes container. @@ -92,13 +92,23 @@ func (p *PortForwarder) FQN() string { return p.path + ":" + p.container } +// HasPortMapping checks if port mapping is defined for this fwd. +func (p *PortForwarder) HasPortMapping(m string) bool { + for _, mapping := range p.ports { + if mapping == m { + return true + } + } + return false +} + // Start initiates a port forward session for a given pod and ports. -func (p *PortForwarder) Start(path, co string, t Tunnel) (*portforward.PortForwarder, error) { - fwds := []string{t.LocalPort + ":" + t.ContainerPort} +func (p *PortForwarder) Start(path, co string, t client.PortTunnel) (*portforward.PortForwarder, error) { + fwds := []string{t.PortMap()} p.path, p.container, p.ports, p.age = path, co, fwds, time.Now() ns, n := client.Namespaced(path) - auth, err := p.CanI(ns, "v1/pods", []string{client.GetVerb}) + auth, err := p.Client().CanI(ns, "v1/pods", []string{client.GetVerb}) if err != nil { return nil, err } @@ -106,8 +116,9 @@ func (p *PortForwarder) Start(path, co string, t Tunnel) (*portforward.PortForwa return nil, fmt.Errorf("user is not authorized to get pods") } - // BOZO!! Use the factory! - pod, err := p.DialOrDie().CoreV1().Pods(ns).Get(n, metav1.GetOptions{}) + var res Pod + res.Init(p, client.NewGVR("v1/pods")) + pod, err := res.GetInstance(path) if err != nil { return nil, err } @@ -115,7 +126,7 @@ func (p *PortForwarder) Start(path, co string, t Tunnel) (*portforward.PortForwa return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase) } - auth, err = p.CanI(ns, "v1/pods:portforward", []string{client.UpdateVerb}) + auth, err = p.Client().CanI(ns, "v1/pods:portforward", []string{client.UpdateVerb}) if err != nil { return nil, err } @@ -123,7 +134,7 @@ func (p *PortForwarder) Start(path, co string, t Tunnel) (*portforward.PortForwa return nil, fmt.Errorf("user is not authorized to update portforward") } - rcfg := p.RestConfigOrDie() + rcfg := p.Client().RestConfigOrDie() rcfg.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"} rcfg.APIPath = "/api" codec, _ := codec() @@ -143,7 +154,7 @@ func (p *PortForwarder) Start(path, co string, t Tunnel) (*portforward.PortForwa } func (p *PortForwarder) forwardPorts(method string, url *url.URL, address string, ports []string) (*portforward.PortForwarder, error) { - cfg, err := p.Config().RESTConfig() + cfg, err := p.Client().Config().RESTConfig() if err != nil { return nil, err } diff --git a/internal/dao/registry_test.go b/internal/dao/registry_test.go index 241869b8..28ff4ef8 100644 --- a/internal/dao/registry_test.go +++ b/internal/dao/registry_test.go @@ -89,7 +89,7 @@ func TestExtractString(t *testing.T) { // Helpers... func load(t *testing.T, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("assets/%s.json", n)) + raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured diff --git a/internal/dao/sts.go b/internal/dao/sts.go index 0946ff24..e00d849b 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -21,6 +21,7 @@ var ( _ Loggable = (*StatefulSet)(nil) _ Restartable = (*StatefulSet)(nil) _ Scalable = (*StatefulSet)(nil) + _ Controller = (*StatefulSet)(nil) ) // StatefulSet represents a K8s sts. @@ -51,12 +52,7 @@ func (s *StatefulSet) Scale(path string, replicas int32) error { // Restart a StatefulSet rollout. func (s *StatefulSet) Restart(path string) error { - o, err := s.Factory.Get(s.gvr.String(), path, true, labels.Everything()) - if err != nil { - return err - } - var ds appsv1.StatefulSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + sts, err := s.getStatefulSet(path) if err != nil { return err } @@ -70,24 +66,18 @@ func (s *StatefulSet) Restart(path string) error { return fmt.Errorf("user is not authorized to update statefulsets") } - update, err := polymorphichelpers.ObjectRestarterFn(&ds) + update, err := polymorphichelpers.ObjectRestarterFn(sts) if err != nil { return err } - _, err = s.Client().DialOrDie().AppsV1().StatefulSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, update) + _, err = s.Client().DialOrDie().AppsV1().StatefulSets(sts.Namespace).Patch(sts.Name, types.StrategicMergePatchType, update) return err } // TailLogs tail logs for all pods represented by this StatefulSet. func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { - o, err := s.Factory.Get(s.gvr.String(), opts.Path, true, labels.Everything()) - if err != nil { - return err - } - - var sts appsv1.StatefulSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts) + sts, err := s.getStatefulSet(opts.Path) if err != nil { return errors.New("expecting StatefulSet resource") } @@ -97,3 +87,28 @@ func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOpt return podLogs(ctx, c, sts.Spec.Selector.MatchLabels, opts) } + +// Pod returns a pod victim by name. +func (s *StatefulSet) Pod(fqn string) (string, error) { + sts, err := s.getStatefulSet(fqn) + if err != nil { + return "", err + } + + return podFromSelector(s.Factory, sts.Namespace, sts.Spec.Selector.MatchLabels) +} + +func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) { + o, err := s.Factory.Get(s.gvr.String(), fqn, false, labels.Everything()) + if err != nil { + return nil, err + } + + var sts appsv1.StatefulSet + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts) + if err != nil { + return nil, errors.New("expecting Service resource") + } + + return &sts, nil +} diff --git a/internal/dao/svc.go b/internal/dao/svc.go index 9fbc3787..2369870b 100644 --- a/internal/dao/svc.go +++ b/internal/dao/svc.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/derailed/k9s/internal/client" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -12,8 +13,9 @@ import ( ) var ( - _ Accessor = (*Service)(nil) - _ Loggable = (*Service)(nil) + _ Accessor = (*Service)(nil) + _ Loggable = (*Service)(nil) + _ Controller = (*Service)(nil) ) // Service represents a k8s service. @@ -23,19 +25,61 @@ type Service struct { // TailLogs tail logs for all pods represented by this Service. func (s *Service) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { - o, err := s.Factory.Get(s.gvr.String(), opts.Path, true, labels.Everything()) + svc, err := s.GetInstance(opts.Path) if err != nil { return err } - var svc v1.Service - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) - if err != nil { - return errors.New("expecting Service resource") - } - if svc.Spec.Selector == nil || len(svc.Spec.Selector) == 0 { return fmt.Errorf("no valid selector found on Service %s", opts.Path) } return podLogs(ctx, c, svc.Spec.Selector, opts) } + +// Pod returns a pod victim by name. +func (s *Service) Pod(fqn string) (string, error) { + svc, err := s.GetInstance(fqn) + if err != nil { + return "", err + } + + return podFromSelector(s.Factory, svc.Namespace, svc.Spec.Selector) +} + +// GetInstance returns a service instance. +func (s *Service) GetInstance(fqn string) (*v1.Service, error) { + o, err := s.Factory.Get(s.gvr.String(), fqn, false, labels.Everything()) + if err != nil { + return nil, err + } + + var svc v1.Service + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) + if err != nil { + return nil, errors.New("expecting Service resource") + } + + return &svc, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func podFromSelector(f Factory, ns string, sel map[string]string) (string, error) { + oo, err := f.List("v1/pods", ns, true, labels.Set(sel).AsSelector()) + if err != nil { + return "", err + } + + if len(oo) == 0 { + return "", fmt.Errorf("no matching pods for %v", sel) + } + + var pod v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(oo[0].(*unstructured.Unstructured).Object, &pod) + if err != nil { + return "", err + } + + return client.FQN(pod.Namespace, pod.Name), nil +} diff --git a/internal/dao/test_assets/bench/default_fred_1577308050814961000.txt b/internal/dao/testdata/bench/default_fred_1577308050814961000.txt similarity index 100% rename from internal/dao/test_assets/bench/default_fred_1577308050814961000.txt rename to internal/dao/testdata/bench/default_fred_1577308050814961000.txt diff --git a/internal/dao/testdata/benchspec.yml b/internal/dao/testdata/benchspec.yml new file mode 100644 index 00000000..e308b288 --- /dev/null +++ b/internal/dao/testdata/benchspec.yml @@ -0,0 +1,19 @@ +benchmarks: + defaults: + concurrency: 2 + requests: 500 + containers: + default/nginx:nginx: + concurrency: 2 + requests: 3000 + http: + method: GET + path: / + services: + default/nginx: + concurrency: 1 + requests: 666 + http: + method: GET + host: 192.168.64.1 + path: / diff --git a/internal/dao/assets/config b/internal/dao/testdata/config similarity index 100% rename from internal/dao/assets/config rename to internal/dao/testdata/config diff --git a/internal/dao/assets/config.1 b/internal/dao/testdata/config.1 similarity index 100% rename from internal/dao/assets/config.1 rename to internal/dao/testdata/config.1 diff --git a/internal/dao/test_assets/crb.json b/internal/dao/testdata/crb.json similarity index 100% rename from internal/dao/test_assets/crb.json rename to internal/dao/testdata/crb.json diff --git a/internal/dao/assets/dr.json b/internal/dao/testdata/dr.json similarity index 100% rename from internal/dao/assets/dr.json rename to internal/dao/testdata/dr.json diff --git a/internal/dao/test_assets/n1.json b/internal/dao/testdata/n1.json similarity index 100% rename from internal/dao/test_assets/n1.json rename to internal/dao/testdata/n1.json diff --git a/internal/dao/test_assets/p1.json b/internal/dao/testdata/p1.json similarity index 100% rename from internal/dao/test_assets/p1.json rename to internal/dao/testdata/p1.json diff --git a/internal/dao/types.go b/internal/dao/types.go index b93d7f76..ccddbb8e 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -91,6 +91,12 @@ type Scalable interface { Scale(path string, replicas int32) error } +// Controller represents a pod controller. +type Controller interface { + // Pod returns a pod instance matching the selector. + Pod(path string) (string, error) +} + // Nuker represents a resource deleter. type Nuker interface { // Delete removes a resource from the api server. diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index 3ba5405d..123da35c 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -69,12 +69,6 @@ func TestTableMeta(t *testing.T) { accessor dao.Accessor renderer Renderer }{ - // BOZO!! - // "full": { - // gvr: "v1/pods", - // accessor: &pd, - // renderer: &render.Pod{}, - // }, "generic": { gvr: "containers", accessor: &dao.Container{}, @@ -144,7 +138,7 @@ func TestTableGenericHydrate(t *testing.T) { // Helpers... func mustLoad(n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n)) + raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) if err != nil { panic(err) } @@ -156,7 +150,7 @@ func mustLoad(n string) *unstructured.Unstructured { } func load(t *testing.T, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n)) + raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured err = json.Unmarshal(raw, &o) @@ -165,7 +159,7 @@ func load(t *testing.T, n string) *unstructured.Unstructured { } func raw(t *testing.T, n string) []byte { - raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n)) + raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) return raw } diff --git a/internal/model/table_test.go b/internal/model/table_test.go index b72878ca..0ae6da2d 100644 --- a/internal/model/table_test.go +++ b/internal/model/table_test.go @@ -116,7 +116,7 @@ func makeTableFactory() tableFactory { } func mustLoad(n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("test_assets/%s.json", n)) + raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) if err != nil { panic(err) } diff --git a/internal/model/test_assets/p1.json b/internal/model/testdata/p1.json similarity index 100% rename from internal/model/test_assets/p1.json rename to internal/model/testdata/p1.json diff --git a/internal/render/benchmark_int_test.go b/internal/render/benchmark_int_test.go index 7489b914..6c7c5384 100644 --- a/internal/render/benchmark_int_test.go +++ b/internal/render/benchmark_int_test.go @@ -18,19 +18,19 @@ func TestAugmentRow(t *testing.T) { e Fields }{ "cool": { - "assets/b1.txt", + "testdata/b1.txt", Fields{"pass", "3.3544", "29.8116", "100", "0"}, }, "2XX": { - "assets/b4.txt", + "testdata/b4.txt", Fields{"pass", "3.3544", "29.8116", "160", "0"}, }, "4XX/5XX": { - "assets/b2.txt", + "testdata/b2.txt", Fields{"pass", "3.3544", "29.8116", "100", "12"}, }, "toast": { - "assets/b3.txt", + "testdata/b3.txt", Fields{"fail", "2.3688", "35.4606", "0", "0"}, }, } diff --git a/internal/render/render_test.go b/internal/render/render_test.go index dd758af1..f526ed70 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -12,7 +12,7 @@ import ( // Helpers... func load(t assert.TestingT, n string) *unstructured.Unstructured { - raw, err := ioutil.ReadFile(fmt.Sprintf("assets/%s.json", n)) + raw, err := ioutil.ReadFile(fmt.Sprintf("testdata/%s.json", n)) assert.Nil(t, err) var o unstructured.Unstructured diff --git a/internal/render/assets/b1.txt b/internal/render/testdata/b1.txt similarity index 100% rename from internal/render/assets/b1.txt rename to internal/render/testdata/b1.txt diff --git a/internal/render/assets/b2.txt b/internal/render/testdata/b2.txt similarity index 100% rename from internal/render/assets/b2.txt rename to internal/render/testdata/b2.txt diff --git a/internal/render/assets/b3.txt b/internal/render/testdata/b3.txt similarity index 100% rename from internal/render/assets/b3.txt rename to internal/render/testdata/b3.txt diff --git a/internal/render/assets/b4.txt b/internal/render/testdata/b4.txt similarity index 100% rename from internal/render/assets/b4.txt rename to internal/render/testdata/b4.txt diff --git a/internal/render/assets/cj.json b/internal/render/testdata/cj.json similarity index 100% rename from internal/render/assets/cj.json rename to internal/render/testdata/cj.json diff --git a/internal/render/assets/cm.json b/internal/render/testdata/cm.json similarity index 100% rename from internal/render/assets/cm.json rename to internal/render/testdata/cm.json diff --git a/internal/render/assets/cr.json b/internal/render/testdata/cr.json similarity index 100% rename from internal/render/assets/cr.json rename to internal/render/testdata/cr.json diff --git a/internal/render/assets/crb.json b/internal/render/testdata/crb.json similarity index 100% rename from internal/render/assets/crb.json rename to internal/render/testdata/crb.json diff --git a/internal/render/assets/crd.json b/internal/render/testdata/crd.json similarity index 100% rename from internal/render/assets/crd.json rename to internal/render/testdata/crd.json diff --git a/internal/render/assets/dp.json b/internal/render/testdata/dp.json similarity index 100% rename from internal/render/assets/dp.json rename to internal/render/testdata/dp.json diff --git a/internal/render/assets/ds.json b/internal/render/testdata/ds.json similarity index 100% rename from internal/render/assets/ds.json rename to internal/render/testdata/ds.json diff --git a/internal/render/assets/ep.json b/internal/render/testdata/ep.json similarity index 100% rename from internal/render/assets/ep.json rename to internal/render/testdata/ep.json diff --git a/internal/render/assets/ev.json b/internal/render/testdata/ev.json similarity index 100% rename from internal/render/assets/ev.json rename to internal/render/testdata/ev.json diff --git a/internal/render/assets/hpa.json b/internal/render/testdata/hpa.json similarity index 100% rename from internal/render/assets/hpa.json rename to internal/render/testdata/hpa.json diff --git a/internal/render/assets/ing.json b/internal/render/testdata/ing.json similarity index 100% rename from internal/render/assets/ing.json rename to internal/render/testdata/ing.json diff --git a/internal/render/assets/job.json b/internal/render/testdata/job.json similarity index 100% rename from internal/render/assets/job.json rename to internal/render/testdata/job.json diff --git a/internal/render/assets/no.json b/internal/render/testdata/no.json similarity index 100% rename from internal/render/assets/no.json rename to internal/render/testdata/no.json diff --git a/internal/render/assets/np.json b/internal/render/testdata/np.json similarity index 100% rename from internal/render/assets/np.json rename to internal/render/testdata/np.json diff --git a/internal/render/assets/ns.json b/internal/render/testdata/ns.json similarity index 100% rename from internal/render/assets/ns.json rename to internal/render/testdata/ns.json diff --git a/internal/render/assets/pdb.json b/internal/render/testdata/pdb.json similarity index 100% rename from internal/render/assets/pdb.json rename to internal/render/testdata/pdb.json diff --git a/internal/render/assets/po.json b/internal/render/testdata/po.json similarity index 100% rename from internal/render/assets/po.json rename to internal/render/testdata/po.json diff --git a/internal/render/assets/po_init.json b/internal/render/testdata/po_init.json similarity index 100% rename from internal/render/assets/po_init.json rename to internal/render/testdata/po_init.json diff --git a/internal/render/assets/pv.json b/internal/render/testdata/pv.json similarity index 100% rename from internal/render/assets/pv.json rename to internal/render/testdata/pv.json diff --git a/internal/render/assets/pvc.json b/internal/render/testdata/pvc.json similarity index 100% rename from internal/render/assets/pvc.json rename to internal/render/testdata/pvc.json diff --git a/internal/render/assets/rb.json b/internal/render/testdata/rb.json similarity index 100% rename from internal/render/assets/rb.json rename to internal/render/testdata/rb.json diff --git a/internal/render/assets/ro.json b/internal/render/testdata/ro.json similarity index 100% rename from internal/render/assets/ro.json rename to internal/render/testdata/ro.json diff --git a/internal/render/assets/rs.json b/internal/render/testdata/rs.json similarity index 100% rename from internal/render/assets/rs.json rename to internal/render/testdata/rs.json diff --git a/internal/render/assets/sa.json b/internal/render/testdata/sa.json similarity index 100% rename from internal/render/assets/sa.json rename to internal/render/testdata/sa.json diff --git a/internal/render/assets/sc.json b/internal/render/testdata/sc.json similarity index 100% rename from internal/render/assets/sc.json rename to internal/render/testdata/sc.json diff --git a/internal/render/assets/sec.json b/internal/render/testdata/sec.json similarity index 100% rename from internal/render/assets/sec.json rename to internal/render/testdata/sec.json diff --git a/internal/render/assets/sts.json b/internal/render/testdata/sts.json similarity index 100% rename from internal/render/assets/sts.json rename to internal/render/testdata/sts.json diff --git a/internal/render/assets/svc.json b/internal/render/testdata/svc.json similarity index 100% rename from internal/render/assets/svc.json rename to internal/render/testdata/svc.json diff --git a/internal/ui/config.go b/internal/ui/config.go index 71771745..857179db 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -20,10 +20,10 @@ type synchronizer interface { // Configurator represents an application configurationa. type Configurator struct { - skinFile string - Config *config.Config - Styles *config.Styles - Bench *config.Bench + skinFile string + Config *config.Config + Styles *config.Styles + BenchFile string } // HasSkin returns true if a skin file was located. @@ -67,21 +67,15 @@ func (c *Configurator) StylesUpdater(ctx context.Context, s synchronizer) error return w.Add(c.skinFile) } -// InitBench load benchmark configuration if any. -func (c *Configurator) InitBench(cluster string) { - var err error - if c.Bench, err = config.NewBench(BenchConfig(cluster)); err != nil { - log.Info().Msg("No benchmark config file found, using defaults.") - } -} - // BenchConfig location of the benchmarks configuration file. -func BenchConfig(cluster string) string { - return filepath.Join(config.K9sHome, config.K9sBench+"-"+cluster+".yml") +func BenchConfig(context string) string { + return filepath.Join(config.K9sHome, config.K9sBench+"-"+context+".yml") } // RefreshStyles load for skin configuration changes. func (c *Configurator) RefreshStyles(context string) { + c.BenchFile = BenchConfig(context) + clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", context)) if c.Styles == nil { c.Styles = config.NewStyles() diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index 6436057e..2eb9006f 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -17,7 +17,7 @@ func TestBenchConfig(t *testing.T) { } func TestConfiguratorRefreshStyle(t *testing.T) { - config.K9sStylesFile = filepath.Join("..", "config", "test_assets", "black_and_wtf.yml") + config.K9sStylesFile = filepath.Join("..", "config", "testdata", "black_and_wtf.yml") cfg := ui.Configurator{} cfg.RefreshStyles("") @@ -26,15 +26,3 @@ func TestConfiguratorRefreshStyle(t *testing.T) { assert.Equal(t, tcell.ColorGhostWhite, render.StdColor) assert.Equal(t, tcell.ColorWhiteSmoke, render.ErrColor) } - -func TestInitBench(t *testing.T) { - config.K9sHome = filepath.Join("..", "config", "test_assets") - - cfg := ui.Configurator{} - cfg.InitBench("fred") - - assert.NotNil(t, cfg.Bench) - assert.Equal(t, 2, cfg.Bench.Benchmarks.Defaults.C) - assert.Equal(t, 1000, cfg.Bench.Benchmarks.Defaults.N) - assert.Equal(t, 2, len(cfg.Bench.Benchmarks.Services)) -} diff --git a/internal/ui/dialog/port_forwards_test.go b/internal/ui/dialog/port_forwards_test.go deleted file mode 100644 index 6ecb876f..00000000 --- a/internal/ui/dialog/port_forwards_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package dialog - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/dao" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/stretchr/testify/assert" -) - -func TestPortForwards(t *testing.T) { - p := ui.NewPages() - - cbFunc := func(path, co string, t dao.Tunnel) {} - ShowPortForwards(p, config.NewStyles(), "fred", []string{"8080"}, cbFunc) - - d := p.GetPrimitive(portForwardKey).(*tview.ModalForm) - assert.NotNil(t, d) - - DismissPortForwards(p) - assert.Nil(t, p.GetPrimitive(portForwardKey)) -} - -func TestExtractPort(t *testing.T) { - uu := map[string]struct { - port, e string - }{ - "full": { - "fred:8000", "8000", - }, - "port": { - "8000", "8000", - }, - "protocol": { - "dns:53╱UDP", "53", - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, extractPort(u.port)) - }) - } -} diff --git a/internal/ui/menu.go b/internal/ui/menu.go index ef44f60a..958cf5d9 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -16,7 +16,7 @@ import ( const ( menuIndexFmt = " [key:-:b]<%d> [fg:-:d]%s " - maxRows = 7 + maxRows = 6 ) var menuRX = regexp.MustCompile(`\d`) diff --git a/internal/view/app.go b/internal/view/app.go index c0ac13b8..903917a1 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -18,7 +18,7 @@ import ( "github.com/rs/zerolog/log" ) -// ExitStatus indicates UI exit conditions. +// // ExitStatus indicates UI exit conditions. var ExitStatus = "" const ( @@ -50,7 +50,6 @@ func NewApp(cfg *config.Config) *App { Content: NewPageStack(), } a.Config = cfg - a.InitBench(cfg.K9s.CurrentCluster) a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles) a.Views()["clusterInfo"] = NewClusterInfo(&a) diff --git a/internal/view/browser.go b/internal/view/browser.go index 8dc6d351..80c65281 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -145,7 +145,6 @@ func (b *Browser) TableDataChanged(data render.TableData) { b.app.QueueUpdateDraw(func() { b.refreshActions() b.Update(data) - b.App().ClearStatus(false) }) } diff --git a/internal/view/container.go b/internal/view/container.go index 941b2889..e128ccac 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -6,13 +6,10 @@ import ( "strings" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/ui/dialog" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" - "k8s.io/client-go/tools/portforward" ) const ( @@ -114,23 +111,11 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } log.Debug().Msgf("CONTAINER-PORTS %#v", ports) - dialog.ShowPortForwards(c.App().Content.Pages, c.App().Styles, c.GetTable().Path, ports, c.portForward) + ShowPortForwards(c, c.GetTable().Path, ports, startFwdCB) return nil } -func (c *Container) portForward(path, co string, t dao.Tunnel) { - pf := dao.NewPortForwarder(c.App().Conn()) - fw, err := pf.Start(path, co, t) - if err != nil { - c.App().Flash().Err(err) - return - } - - log.Debug().Msgf(">>> Starting port forward %q %#v", path, t) - go runForward(c.App(), pf, fw) -} - func (c *Container) isForwardable(path string) ([]string, bool) { state := c.GetTable().GetSelectedCell(3) if state != "Running" { @@ -155,24 +140,3 @@ func (c *Container) isForwardable(path string) ([]string, bool) { return pp, true } - -// ---------------------------------------------------------------------------- -// Helpers... - -func runForward(a *App, pf *dao.PortForwarder, f *portforward.PortForwarder) { - a.QueueUpdateDraw(func() { - a.factory.AddForwarder(pf) - a.Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) - dialog.DismissPortForwards(a.Content.Pages) - }) - - pf.SetActive(true) - if err := f.ForwardPorts(); err != nil { - a.Flash().Err(err) - return - } - a.QueueUpdateDraw(func() { - a.factory.DeleteForwarder(pf.FQN()) - pf.SetActive(false) - }) -} diff --git a/internal/view/dp.go b/internal/view/dp.go index aa9bb793..d88464a8 100644 --- a/internal/view/dp.go +++ b/internal/view/dp.go @@ -2,13 +2,10 @@ package view import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" - appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" ) const scaleDialogKey = "scale" @@ -21,8 +18,15 @@ type Deploy struct { // NewDeploy returns a new deployment view. func NewDeploy(gvr client.GVR) ResourceViewer { d := Deploy{ - ResourceViewer: NewRestartExtender( - NewScaleExtender(NewLogsExtender(NewBrowser(gvr), nil)), + ResourceViewer: NewPortForwardExtender( + NewRestartExtender( + NewScaleExtender( + NewLogsExtender( + NewBrowser(gvr), + nil, + ), + ), + ), ), } d.SetBindKeysFn(d.bindKeys) @@ -41,21 +45,19 @@ func (d *Deploy) bindKeys(aa ui.KeyActions) { } func (d *Deploy) showPods(app *App, model ui.Tabular, gvr, path string) { - o, err := app.factory.Get(d.GVR(), path, true, labels.Everything()) + var res dao.Deployment + res.Init(app.factory, client.NewGVR(d.GVR())) + + dp, err := res.GetInstance(path) if err != nil { app.Flash().Err(err) return } - var dp appsv1.Deployment - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) - if err != nil { - app.Flash().Err(err) - } - showPodsFromSelector(app, path, dp.Spec.Selector) } +// ---------------------------------------------------------------------------- // Helpers... func showPodsFromSelector(app *App, path string, sel *metav1.LabelSelector) { diff --git a/internal/view/dp_test.go b/internal/view/dp_test.go index 6adde7f7..18e94546 100644 --- a/internal/view/dp_test.go +++ b/internal/view/dp_test.go @@ -13,6 +13,5 @@ func TestDeploy(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "Deployments", v.Name()) - assert.Equal(t, 10, len(v.Hints())) - + assert.Equal(t, 11, len(v.Hints())) } diff --git a/internal/view/ds.go b/internal/view/ds.go index c18028f2..77c24414 100644 --- a/internal/view/ds.go +++ b/internal/view/ds.go @@ -2,12 +2,9 @@ package view import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" - appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" ) // DaemonSet represents a daemon set custom viewer. @@ -18,8 +15,10 @@ type DaemonSet struct { // NewDaemonSet returns a new viewer. func NewDaemonSet(gvr client.GVR) ResourceViewer { d := DaemonSet{ - ResourceViewer: NewRestartExtender( - NewLogsExtender(NewBrowser(gvr), nil), + ResourceViewer: NewPortForwardExtender( + NewRestartExtender( + NewLogsExtender(NewBrowser(gvr), nil), + ), ), } d.SetBindKeysFn(d.bindKeys) @@ -40,14 +39,10 @@ func (d *DaemonSet) bindKeys(aa ui.KeyActions) { } func (d *DaemonSet) showPods(app *App, model ui.Tabular, _, path string) { - o, err := app.factory.Get(d.GVR(), path, true, labels.Everything()) - if err != nil { - d.App().Flash().Err(err) - return - } + var res dao.DaemonSet + res.Init(app.factory, client.NewGVR(d.GVR())) - var ds appsv1.DaemonSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) + ds, err := res.GetInstance(path) if err != nil { d.App().Flash().Err(err) } diff --git a/internal/view/ds_test.go b/internal/view/ds_test.go index 762ad2b1..34cfe258 100644 --- a/internal/view/ds_test.go +++ b/internal/view/ds_test.go @@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "DaemonSets", v.Name()) - assert.Equal(t, 11, len(v.Hints())) + assert.Equal(t, 12, len(v.Hints())) } diff --git a/internal/view/help.go b/internal/view/help.go index ca257204..0758d4ca 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -235,11 +235,11 @@ func (h *Help) showGeneral() model.MenuHints { Description: "Clear command", }, { - Mnemonic: "Ctrl-h", + Mnemonic: "t", Description: "Toggle Header", }, { - Mnemonic: ":q", + Mnemonic: "q", Description: "Quit", }, { diff --git a/internal/ui/dialog/port_forwards.go b/internal/view/pf_dialog.go similarity index 64% rename from internal/ui/dialog/port_forwards.go rename to internal/view/pf_dialog.go index fbfc6ded..6b7e3b15 100644 --- a/internal/ui/dialog/port_forwards.go +++ b/internal/view/pf_dialog.go @@ -1,4 +1,4 @@ -package dialog +package view import ( "fmt" @@ -6,7 +6,6 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" ) @@ -14,17 +13,19 @@ import ( const portForwardKey = "portforward" // PortForwardFunc represents a port-forward callback function. -type PortForwardFunc func(path, co string, mapper dao.Tunnel) +type PortForwardFunc func(v ResourceViewer, path, co string, mapper client.PortTunnel) // ShowPortForwards pops a port forwarding configuration dialog. -func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string, okFn PortForwardFunc) { +func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardFunc) { + styles := v.App().Styles + f := tview.NewForm() f.SetItemPadding(0) f.SetButtonsAlign(tview.AlignCenter). - SetButtonBackgroundColor(s.BgColor()). - SetButtonTextColor(s.FgColor()). - SetLabelColor(config.AsColor(s.K9s.Info.FgColor)). - SetFieldTextColor(config.AsColor(s.K9s.Info.SectionColor)) + SetButtonBackgroundColor(styles.BgColor()). + SetButtonTextColor(styles.FgColor()). + SetLabelColor(config.AsColor(styles.K9s.Info.FgColor)). + SetFieldTextColor(config.AsColor(styles.K9s.Info.SectionColor)) p1, p2, address := ports[0], ports[0], "localhost" f.AddDropDown("Container Ports", ports, 0, func(sel string, _ int) { @@ -33,12 +34,12 @@ func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string dropD, ok := f.GetFormItem(0).(*tview.DropDown) if ok { - dropD.SetFieldBackgroundColor(s.BgColor()) + dropD.SetFieldBackgroundColor(styles.BgColor()) list := dropD.GetList() - list.SetMainTextColor(s.FgColor()) - list.SetSelectedTextColor(s.FgColor()) - list.SetSelectedBackgroundColor(config.AsColor(s.Table().CursorColor)) - list.SetBackgroundColor(s.BgColor() + 100) + list.SetMainTextColor(styles.FgColor()) + list.SetSelectedTextColor(styles.FgColor()) + list.SetSelectedBackgroundColor(config.AsColor(styles.Table().CursorColor)) + list.SetBackgroundColor(styles.BgColor() + 100) } f.AddInputField("Local Port:", p2, 20, nil, func(p string) { p2 = p @@ -47,24 +48,27 @@ func ShowPortForwards(p *ui.Pages, s *config.Styles, path string, ports []string address = h }) + pages := v.App().Content.Pages + f.AddButton("OK", func() { - tunnel := dao.Tunnel{ + tunnel := client.PortTunnel{ Address: address, LocalPort: p2, ContainerPort: extractPort(p1), } - okFn(path, extractContainer(p1), tunnel) + okFn(v, path, extractContainer(p1), tunnel) }) f.AddButton("Cancel", func() { - DismissPortForwards(p) + DismissPortForwards(pages) }) modal := tview.NewModalForm(fmt.Sprintf("", path), f) modal.SetDoneFunc(func(_ int, b string) { - DismissPortForwards(p) + DismissPortForwards(pages) }) - p.AddPage(portForwardKey, modal, false, false) - p.ShowPage(portForwardKey) + + pages.AddPage(portForwardKey, modal, false, false) + pages.ShowPage(portForwardKey) } // DismissPortForwards dismiss the port forward dialog. diff --git a/internal/view/pf_dialog_test.go b/internal/view/pf_dialog_test.go new file mode 100644 index 00000000..f4154494 --- /dev/null +++ b/internal/view/pf_dialog_test.go @@ -0,0 +1,30 @@ +package view + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractPort(t *testing.T) { + uu := map[string]struct { + port, e string + }{ + "full": { + "fred:8000", "8000", + }, + "port": { + "8000", "8000", + }, + "protocol": { + "dns:53╱UDP", "53", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, extractPort(u.port)) + }) + } +} diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go new file mode 100644 index 00000000..1393d37a --- /dev/null +++ b/internal/view/pf_extender.go @@ -0,0 +1,155 @@ +package view + +import ( + "errors" + "fmt" + "strconv" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/watch" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/portforward" +) + +// PortForwardExtender adds port-forward extensions. +type PortForwardExtender struct { + ResourceViewer +} + +// NewPortForwardExtender returns a new extender. +func NewPortForwardExtender(r ResourceViewer) ResourceViewer { + s := PortForwardExtender{ResourceViewer: r} + s.bindKeys(s.Actions()) + + return &s +} + +func (p *PortForwardExtender) bindKeys(aa ui.KeyActions) { + aa.Add(ui.KeyActions{ + ui.KeyShiftF: ui.NewKeyAction("Port-Forward", p.portFwdCmd, true), + }) +} + +func (p *PortForwardExtender) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { + path := p.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + pod, err := p.fetchPodName(path) + if err != nil { + p.App().Flash().Err(err) + return nil + } + if err := showFwdDialog(p, pod, startFwdCB); err != nil { + p.App().Flash().Err(err) + } + + return nil +} + +func (p *PortForwardExtender) fetchPodName(path string) (string, error) { + res, err := dao.AccessorFor(p.App().factory, client.NewGVR(p.GVR())) + if err != nil { + return "", nil + } + ctrl, ok := res.(dao.Controller) + if !ok { + return "", fmt.Errorf("expecting a controller resource for %q", p.GVR()) + } + + return ctrl.Pod(path) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForwarder) { + v.App().factory.AddForwarder(pf) + + v.App().QueueUpdateDraw(func() { + v.App().Flash().Infof("PortForward activated %s:%s", pf.Path(), pf.Ports()[0]) + DismissPortForwards(v.App().Content.Pages) + }) + + pf.SetActive(true) + if err := f.ForwardPorts(); err != nil { + v.App().Flash().Err(err) + return + } + + v.App().QueueUpdateDraw(func() { + v.App().factory.DeleteForwarder(pf.FQN()) + pf.SetActive(false) + }) +} + +func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) { + log.Debug().Msgf("CURRENT-FWD %#v", v.App().factory.Forwarders()) + + if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, co)); ok { + v.App().Flash().Err(errors.New("A port-forward is already active on this pod")) + return + } + + pf := dao.NewPortForwarder(v.App().factory) + fwd, err := pf.Start(path, co, t) + if err != nil { + v.App().Flash().Err(err) + return + } + + log.Debug().Msgf(">>> Starting port forward %q %#v", path, t) + go runForward(v, pf, fwd) +} + +func showFwdDialog(v ResourceViewer, path string, cb PortForwardFunc) error { + mm, err := fetchPodPorts(v.App().factory, path) + if err != nil { + return nil + } + ports := make([]string, 0, len(mm)) + for co, pp := range mm { + for _, p := range pp { + if p.Protocol != v1.ProtocolTCP { + continue + } + ports = append(ports, client.FQN(co, p.Name)+":"+strconv.Itoa(int(p.ContainerPort))) + } + } + + if len(ports) == 0 { + return fmt.Errorf("no tcp ports found on %s", path) + } + ShowPortForwards(v, path, ports, cb) + + return nil +} + +func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, error) { + log.Debug().Msgf("Fetching ports on pod %q", path) + o, err := f.Get("v1/pods", path, false, labels.Everything()) + if err != nil { + return nil, err + } + + var pod v1.Pod + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + if err != nil { + return nil, err + } + + pp := make(map[string][]v1.ContainerPort) + for _, co := range pod.Spec.Containers { + pp[co.Name] = co.Ports + } + + return pp, nil +} diff --git a/internal/view/pod.go b/internal/view/pod.go index 413c18d5..dd90fcaf 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strconv" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" @@ -12,11 +11,9 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" - "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/k9s/internal/watch" "github.com/fatih/color" "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -30,7 +27,11 @@ type Pod struct { // NewPod returns a new viewer. func NewPod(gvr client.GVR) ResourceViewer { - p := Pod{ResourceViewer: NewLogsExtender(NewBrowser(gvr), nil)} + p := Pod{ + ResourceViewer: NewPortForwardExtender( + NewLogsExtender(NewBrowser(gvr), nil), + ), + } p.SetBindKeysFn(p.bindKeys) p.GetTable().SetEnterFn(p.showContainers) p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc()) @@ -51,7 +52,6 @@ func (p *Pod) bindKeys(aa ui.KeyActions) { } aa.Add(ui.KeyActions{ - ui.KeyShiftF: ui.NewKeyAction("Port-Forward", p.portFwdCmd, true), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(1, true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(2, true), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd(3, false), false), @@ -130,57 +130,9 @@ func (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (p *Pod) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { - path := p.GetTable().GetSelectedItem() - if path == "" { - return evt - } - - if err := showFwdDialog(p.App(), path, p.portForward); err != nil { - p.App().Flash().Err(err) - } - - return nil -} - -func (p *Pod) portForward(path, co string, t dao.Tunnel) { - pf := dao.NewPortForwarder(p.App().Conn()) - fw, err := pf.Start(path, co, t) - if err != nil { - p.App().Flash().Err(err) - return - } - - log.Debug().Msgf(">>> Starting port forward %q:%s %v", path, co, t) - go runForward(p.App(), pf, fw) -} - // ---------------------------------------------------------------------------- // Helpers... -func showFwdDialog(a *App, path string, cb dialog.PortForwardFunc) error { - mm, err := fetchPodPorts(a.factory, path) - if err != nil { - return nil - } - ports := make([]string, 0, len(mm)) - for co, pp := range mm { - for _, p := range pp { - if p.Protocol != v1.ProtocolTCP { - continue - } - ports = append(ports, client.FQN(co, p.Name)+":"+strconv.Itoa(int(p.ContainerPort))) - } - } - - if len(ports) == 0 { - return fmt.Errorf("no tcp ports found on %s", path) - } - dialog.ShowPortForwards(a.Content.Pages, a.Styles, path, ports, cb) - - return nil -} - func containerShellin(a *App, comp model.Component, path, co string) error { if co != "" { resumeShellIn(a, comp, path, co) @@ -265,24 +217,3 @@ func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, return nn, nil } - -func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, error) { - log.Debug().Msgf("Fetching ports on pod %q", path) - o, err := f.Get("v1/pods", path, false, labels.Everything()) - if err != nil { - return nil, err - } - - var pod v1.Pod - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) - if err != nil { - return nil, err - } - - pp := make(map[string][]v1.ContainerPort) - for _, co := range pod.Spec.Containers { - pp[co.Name] = co.Ports - } - - return pp, nil -} diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go index b31e7c84..3901ef75 100644 --- a/internal/view/port_forward.go +++ b/internal/view/port_forward.go @@ -3,13 +3,10 @@ package view import ( "context" "fmt" - "regexp" - "strings" "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/perf" "github.com/derailed/k9s/internal/render" @@ -44,7 +41,7 @@ func NewPortForward(gvr client.GVR) ResourceViewer { } func (p *PortForward) portForwardContext(ctx context.Context) context.Context { - return context.WithValue(ctx, internal.KeyBenchCfg, p.App().Bench) + return context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) } func (p *PortForward) bindKeys(aa ui.KeyActions) { @@ -65,8 +62,6 @@ func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -var podNameRX = regexp.MustCompile(`\A(.+)\-(\w{10})\-(\w{5})\z`) - func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { if p.bench != nil { p.App().Status(ui.FlashErr, "Benchmark Canceled!") @@ -79,46 +74,28 @@ func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { if path == "" { return nil } - tokens := strings.Split(path, ":") - ns, po := client.Namespaced(tokens[0]) - sections := podNameRX.FindStringSubmatch(po) - log.Debug().Msgf("SECTIONS %q::%q--%#v", ns, po, sections) - if len(sections) >= 1 { - po = sections[1] - } - key := client.FQN(ns, po) + ":" + tokens[1] - - cfg := defaultConfig() - if defaults := p.App().Bench.Benchmarks.Defaults; !defaults.Empty() { - cfg.C, cfg.N = defaults.C, defaults.N - } - - log.Debug().Msgf("CUST-CFG %q -- %#v", path, key) - if b, ok := p.App().Bench.Benchmarks.Containers[key]; ok { - log.Debug().Msgf("FOUND CUST BENCH_CFG!") - cfg = b - } + cfg := dao.BenchConfigFor(p.App().BenchFile, path) cfg.Name = path - log.Debug().Msgf("BenchCFG %q::%#v", path, cfg) - r, _ := p.GetTable().GetSelection() base := ui.TrimCell(p.GetTable().SelectTable, r, 4) var err error - if p.bench, err = perf.NewBenchmark(base, p.App().version, cfg); err != nil { + p.bench, err = perf.NewBenchmark(base, p.App().version, cfg) + if err != nil { p.App().Flash().Errf("Bench failed %v", err) p.App().ClearStatus(false) return nil } p.App().Status(ui.FlashWarn, "Benchmark in progress...") - log.Debug().Msg("Bench starting...") go p.runBenchmark() return nil } func (p *PortForward) runBenchmark() { + log.Debug().Msg("Bench starting...") + p.bench.Run(p.App().Config.K9s.CurrentCluster, func() { log.Debug().Msg("Bench Completed!") p.App().QueueUpdate(func() { @@ -147,7 +124,6 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { if path == "" { return nil } - log.Debug().Msgf("PF DELETE %q", path) showModal(p.App().Content.Pages, fmt.Sprintf("Delete PortForward `%s?", path), func() { var pf dao.PortForward @@ -166,17 +142,6 @@ func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { // ---------------------------------------------------------------------------- // Helpers... -func defaultConfig() config.BenchConfig { - return config.BenchConfig{ - C: config.DefaultC, - N: config.DefaultN, - HTTP: config.HTTP{ - Method: config.DefaultMethod, - Path: "/", - }, - } -} - func showModal(p *ui.Pages, msg string, ok func()) { m := tview.NewModal(). AddButtons([]string{"Cancel", "OK"}). diff --git a/internal/view/sts.go b/internal/view/sts.go index 01930512..23cf7d97 100644 --- a/internal/view/sts.go +++ b/internal/view/sts.go @@ -18,9 +18,11 @@ type StatefulSet struct { // NewStatefulSet returns a new viewer. func NewStatefulSet(gvr client.GVR) ResourceViewer { s := StatefulSet{ - ResourceViewer: NewRestartExtender( - NewScaleExtender( - NewLogsExtender(NewBrowser(gvr), nil), + ResourceViewer: NewPortForwardExtender( + NewRestartExtender( + NewScaleExtender( + NewLogsExtender(NewBrowser(gvr), nil), + ), ), ), } diff --git a/internal/view/sts_test.go b/internal/view/sts_test.go index bf32d2d7..57deed3f 100644 --- a/internal/view/sts_test.go +++ b/internal/view/sts_test.go @@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "StatefulSets", s.Name()) - assert.Equal(t, 8, len(s.Hints())) + assert.Equal(t, 9, len(s.Hints())) } diff --git a/internal/view/svc.go b/internal/view/svc.go index b8270991..51711e63 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -13,10 +13,6 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" ) // Service represents a service viewer. @@ -29,7 +25,9 @@ type Service struct { // NewService returns a new viewer. func NewService(gvr client.GVR) ResourceViewer { s := Service{ - ResourceViewer: NewLogsExtender(NewBrowser(gvr), nil), + ResourceViewer: NewPortForwardExtender( + NewLogsExtender(NewBrowser(gvr), nil), + ), } s.SetBindKeysFn(s.bindKeys) s.GetTable().SetEnterFn(s.showPods) @@ -41,78 +39,22 @@ func NewService(gvr client.GVR) ResourceViewer { func (s *Service) bindKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ - ui.KeyShiftF: ui.NewKeyAction("Port-Forward", s.portFwdCmd, true), tcell.KeyCtrlB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true), ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd(1, true), false), }) } -func podFromSelector(f dao.Factory, ns string, sel map[string]string) (string, error) { - log.Debug().Msgf("Looking for pods %q:%v -- %v", ns, sel, labels.Set(sel).AsSelector()) - oo, err := f.List("v1/pods", ns, true, labels.Set(sel).AsSelector()) +func (s *Service) showPods(a *App, _ ui.Tabular, gvr, path string) { + var res dao.Service + res.Init(a.factory, client.NewGVR(s.GVR())) + + svc, err := res.GetInstance(path) if err != nil { - return "", err - } - - if len(oo) == 0 { - return "", fmt.Errorf("no matching pods for %v", sel) - } - - var pod v1.Pod - err = runtime.DefaultUnstructuredConverter.FromUnstructured(oo[0].(*unstructured.Unstructured).Object, &pod) - if err != nil { - return "", err - } - - return client.FQN(pod.Namespace, pod.Name), nil -} - -func (s *Service) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { - path := s.GetTable().GetSelectedItem() - if path == "" { - return evt - } - - svc, err := fetchService(s.App().factory, s.GVR(), path) - if err != nil { - s.App().Flash().Err(err) - return nil - } - - ns, _ := client.Namespaced(path) - pod, err := podFromSelector(s.App().factory, ns, svc.Spec.Selector) - if err != nil { - s.App().Flash().Err(err) - return nil - } - - if err := showFwdDialog(s.App(), pod, s.portForward); err != nil { - s.App().Flash().Err(err) - } - - return nil -} - -func (s *Service) portForward(path, co string, t dao.Tunnel) { - pf := dao.NewPortForwarder(s.App().Conn()) - fw, err := pf.Start(path, co, t) - if err != nil { - s.App().Flash().Err(err) + a.Flash().Err(err) return } - log.Debug().Msgf(">>> Starting port forward %q %#v", path, t) - go runForward(s.App(), pf, fw) -} - -func (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) { - svc, err := fetchService(app.factory, gvr, path) - if err != nil { - app.Flash().Err(err) - return - } - - showPodsWithLabels(app, path, svc.Spec.Selector) + showPodsWithLabels(a, path, svc.Spec.Selector) } func (s *Service) checkSvc(row int) error { @@ -140,11 +82,6 @@ func (s *Service) getExternalPort(row int) (string, error) { return tokens[1], nil } -func (s *Service) reloadBenchCfg() error { - path := ui.BenchConfig(s.App().Config.K9s.CurrentCluster) - return s.App().Bench.Reload(path) -} - func (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { if s.bench != nil { log.Debug().Msg(">>> Benchmark canceled!!") @@ -159,12 +96,12 @@ func (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - if err := s.reloadBenchCfg(); err != nil { - s.App().Flash().Err(err) - return nil + cust, err := config.NewBench(s.App().BenchFile) + if err != nil { + log.Debug().Msgf("No custom benchmark config file found") } - cfg, ok := s.App().Bench.Benchmarks.Services[sel] + cfg, ok := cust.Benchmarks.Services[sel] if !ok { s.App().Flash().Errf("No bench config found for service %s", sel) return nil @@ -173,8 +110,8 @@ func (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { log.Debug().Msgf("Benchmark config %#v", cfg) row := s.GetTable().GetSelectedRowIndex() - if err := s.checkSvc(row); err != nil { - s.App().Flash().Err(err) + if e := s.checkSvc(row); e != nil { + s.App().Flash().Err(e) return nil } port, err := s.getExternalPort(row) @@ -227,18 +164,6 @@ func (s *Service) benchDone() { // ---------------------------------------------------------------------------- // Helpers... -func fetchService(f dao.Factory, gvr, path string) (*v1.Service, error) { - o, err := f.Get(gvr, path, true, labels.Everything()) - if err != nil { - return nil, err - } - - var svc v1.Service - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) - - return &svc, err -} - func benchTimedOut(app *App) { <-time.After(2 * time.Second) app.QueueUpdate(func() { diff --git a/internal/view/types.go b/internal/view/types.go index e55f9490..8b8020c1 100644 --- a/internal/view/types.go +++ b/internal/view/types.go @@ -77,6 +77,8 @@ type ResourceViewer interface { // SetBindKeys provision additional key bindings. SetBindKeysFn(BindKeysFunc) + + // SetInstance sets a parent FQN SetInstance(string) } diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 3fe01958..32d86ae6 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -218,6 +218,9 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory { // AddForwarder registers a new portforward for a given container. func (f *Factory) AddForwarder(pf Forwarder) { + f.mx.Lock() + defer f.mx.Unlock() + f.forwarders[pf.Path()] = pf } @@ -237,6 +240,9 @@ func (f *Factory) Forwarders() Forwarders { // ForwarderFor returns a portforward for a given container or nil if none exists. func (f *Factory) ForwarderFor(path string) (Forwarder, bool) { + f.mx.RLock() + defer f.mx.RUnlock() + fwd, ok := f.forwarders[path] return fwd, ok } diff --git a/internal/watch/forwarders.go b/internal/watch/forwarders.go index 60464f32..4ec6dd33 100644 --- a/internal/watch/forwarders.go +++ b/internal/watch/forwarders.go @@ -3,14 +3,15 @@ package watch import ( "strings" + "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" + "k8s.io/client-go/tools/portforward" ) // Forwarder represents a port forwarder. type Forwarder interface { - // BOZO!! - // // Start initializes a port forward. - // Start(path, co, string, t dao.Tunnel) (*portforward.PortForwarder, error) + // Start starts a port-forward. + Start(path, co string, t client.PortTunnel) (*portforward.PortForwarder, error) // Stop terminates a port forward. Stop() @@ -24,11 +25,20 @@ type Forwarder interface { // Ports returns container exposed ports. Ports() []string + // FQN returns the full port-forward name. + FQN() string + // Active returns forwarder current state. Active() bool + // SetActive sets port-forward state. + SetActive(bool) + // Age returns forwarder age. Age() string + + // HasPortMapping returns true if port mapping exists. + HasPortMapping(string) bool } // Forwarders tracks active port forwards. From fc115c5476ba66852a918612c6e681a395f44c97 Mon Sep 17 00:00:00 2001 From: Anthony Cruz Date: Fri, 14 Feb 2020 13:43:17 -0800 Subject: [PATCH 38/39] Update help mnemonic for Toggle Header This change complements what had already been started for #546. --- internal/view/help.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/view/help.go b/internal/view/help.go index ca257204..4c33128b 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -235,7 +235,7 @@ func (h *Help) showGeneral() model.MenuHints { Description: "Clear command", }, { - Mnemonic: "Ctrl-h", + Mnemonic: "t", Description: "Toggle Header", }, { From 889e5131cf9558056690b375f1702fb1a03479b4 Mon Sep 17 00:00:00 2001 From: derailed Date: Fri, 14 Feb 2020 16:43:41 -0700 Subject: [PATCH 39/39] add rel notes --- change_logs/release_v0.15.2.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 change_logs/release_v0.15.2.md diff --git a/change_logs/release_v0.15.2.md b/change_logs/release_v0.15.2.md new file mode 100644 index 00000000..6c29b88b --- /dev/null +++ b/change_logs/release_v0.15.2.md @@ -0,0 +1,23 @@ + + +# Release v0.15.2 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated! + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## Mo PortForwards... + +While putting together the [OpenFeeZ video](https://youtu.be/7Fx4XQ2ftpM), I've noticed a few issues with port-forwards and benchmarks. While I was doing surgery on that carp, figured why not go pull a full monty on port-forwards and enable for other controller like resources such as deployments, statefulsets and daemonsets. So now you can set up port-forwards on any of these using `shift-f`. This exhibits the same mechanics as service based port-forwards ie pick a container port from pods matching the controller selector. + +## Resolved Bugs/Features/PRs + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)