From 7ad07b791d15e2224ad1dcbf096fc7c8d04f76b7 Mon Sep 17 00:00:00 2001 From: derailed Date: Wed, 12 Feb 2020 16:43:22 -0700 Subject: [PATCH] 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)