diff --git a/.gitignore b/.gitignore index 1bd09ab9..b9dc0ef1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ .vscode .idea -k9s.log .envrc cov.out execs @@ -10,11 +9,10 @@ dist notes vendor go.mod1 -popeye1.go gen.sh -cluster_info_test.go *.test *.log *~ pod1.go .project +faas diff --git a/.goreleaser.yml b/.goreleaser.yml index ffb43d7c..2643bb05 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -15,23 +15,23 @@ builds: goarch: - 386 - amd64 - - arm - arm64 goarm: - 6 - 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 +43,7 @@ changelog: - "^docs:" - "^test:" -# Homebrew +# Homebrews brews: - name: k9s github: 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} diff --git a/README.md b/README.md index 2d8e10a6..9b88228d 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,16 @@ 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 + ``` + +* Via [Scoop](https://scoop.sh) for Windows + + ```shell + scoop install k9s ``` * Building from source @@ -97,6 +101,7 @@ K9s is available on Linux, OSX 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) @@ -141,16 +146,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 @@ -185,9 +190,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 @@ -200,9 +205,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-2 + 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 @@ -213,7 +253,7 @@ plugin: description: Pod logs scopes: - po - command: /usr/local/bin/kubectl + command: kubectl background: false args: - logs @@ -233,6 +273,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 @@ -240,13 +282,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. @@ -268,11 +310,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 @@ -291,15 +333,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 @@ -308,41 +350,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. @@ -450,18 +457,18 @@ 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! ```yaml -# InTheNavy Skin... +# Skin InTheNavy... k9s: # General K9s styles body: fgColor: dodgerblue - bgColor: #ffffff - logoColor: #0000ff + bgColor: '#ffffff' + logoColor: '#0000ff' # ClusterInfoView styles. info: fgColor: lightskyblue @@ -484,7 +491,7 @@ k9s: activeColor: skyblue # Resource status and update styles status: - newColor: #00ff00 + newColor: '#00ff00' modifyColor: powderblue addColor: lightskyblue errorColor: indianred @@ -585,8 +592,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/assets/k9s_fez.png b/assets/k9s_fez.png new file mode 100644 index 00000000..078a33df Binary files /dev/null and b/assets/k9s_fez.png differ diff --git a/change_logs/release_v0.14.0.md b/change_logs/release_v0.14.0.md new file mode 100644 index 00000000..20112695 --- /dev/null +++ b/change_logs/release_v0.14.0.md @@ -0,0 +1,67 @@ + + +# Release v0.14.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) + +--- + +## 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 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!! + +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) + +## 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...😿 + +## Resolved Bugs/Features/PRs + +* [Issue #536](https://github.com/derailed/k9s/issues/536) +* [Issue #526](https://github.com/derailed/k9s/issues/526) +* [Issue #464](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) 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) diff --git a/change_logs/release_v0.15.0.md b/change_logs/release_v0.15.0.md new file mode 100644 index 00000000..c40fa60b --- /dev/null +++ b/change_logs/release_v0.15.0.md @@ -0,0 +1,58 @@ + + +# 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) + +--- + + + +## 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. + +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 #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) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) 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/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) diff --git a/go.mod b/go.mod index 898850ec..d6e03b95 100644 --- a/go.mod +++ b/go.mod @@ -28,20 +28,30 @@ 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 github.com/atotto/clipboard v0.1.2 - github.com/derailed/tview v0.3.3 + 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 + 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 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 + github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c + 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 + 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 b37542d1..c28d347a 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= @@ -27,8 +29,11 @@ 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= 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= @@ -43,6 +48,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,11 +62,17 @@ 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/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= 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= @@ -78,13 +90,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,9 +151,12 @@ 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/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= @@ -145,7 +165,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,12 +178,15 @@ 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= 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= @@ -185,10 +207,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= @@ -204,6 +228,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= @@ -267,6 +295,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 +303,12 @@ 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= -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,15 +344,16 @@ 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 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= @@ -358,9 +390,13 @@ 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/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= +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= @@ -368,6 +404,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 +444,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,11 +458,13 @@ 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= 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= @@ -439,6 +480,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= @@ -448,10 +490,13 @@ 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= 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= @@ -468,6 +513,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= @@ -476,6 +527,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= @@ -527,6 +579,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= @@ -554,6 +608,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= @@ -563,6 +621,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= @@ -580,18 +639,23 @@ 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= 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= @@ -612,8 +676,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= @@ -621,6 +689,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= @@ -677,6 +747,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= @@ -686,8 +757,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 +787,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 +820,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 +835,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 +898,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/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/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/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/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/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 a224f17c..f3e0a149 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 } @@ -104,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 77df32fd..fd6e4d97 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()) }) } } @@ -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/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/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.go b/internal/config/styles.go index f72c8233..97fe0dbd 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -292,6 +292,11 @@ func NewStyles() *Styles { } } +// DefaultSkin loads the default skin +func (s *Styles) DefaultSkin() { + s.K9s = newStyle() +} + // FgColor returns the foreground color. func (s *Styles) FgColor() tcell.Color { return AsColor(s.Body().FgColor) @@ -385,10 +390,14 @@ 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. 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/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/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/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/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/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/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 43ab784a..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<- string, opts LogOptions) error { - o, err := d.Factory.Get(d.gvr.String(), opts.Path, true, labels.Everything()) +func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { + 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 c957766b..89a0877d 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -9,7 +9,6 @@ import ( "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" @@ -25,6 +24,7 @@ var ( _ Nuker = (*DaemonSet)(nil) _ Loggable = (*DaemonSet)(nil) _ Restartable = (*DaemonSet)(nil) + _ Controller = (*DaemonSet)(nil) ) // DaemonSet represents a K8s daemonset. @@ -34,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 } @@ -51,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<- string, opts LogOptions) error { - o, err := d.Factory.Get(d.gvr.String(), opts.Path, true, labels.Everything()) +func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { + 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) @@ -79,7 +69,7 @@ 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 { f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) if !ok { return errors.New("expecting a context factory") @@ -94,7 +84,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 } @@ -111,7 +101,6 @@ func podLogs(ctx context.Context, c chan<- string, 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 @@ -120,6 +109,32 @@ func podLogs(ctx context.Context, c chan<- string, 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/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/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/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/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/dao/ofaas.go b/internal/dao/ofaas.go new file mode 100644 index 00000000..123e702c --- /dev/null +++ b/internal/dao/ofaas.go @@ -0,0 +1,219 @@ +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 +} + +// Get returns a function by name. +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 +} + +// Delete removes a function. +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) +} + +// 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 { + 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/pod.go b/internal/dao/pod.go index 1258af29..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,15 +142,36 @@ 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<- 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 +210,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 +222,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 +237,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 +250,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 +258,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/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 0b31cf19..f91175df 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -25,7 +25,7 @@ const localhost = "localhost" // PortForwarder tracks a port forward stream. type PortForwarder struct { - client.Connection + Factory genericclioptions.IOStreams stopChan, readyChan chan struct{} @@ -37,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{}), } } @@ -67,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. @@ -87,19 +92,33 @@ 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, 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 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 } if !auth { return nil, fmt.Errorf("user is not authorized to get pods") } - 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 } @@ -107,7 +126,7 @@ func (p *PortForwarder) Start(path, co, address string, ports []string) (*portfo 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 } @@ -115,7 +134,7 @@ func (p *PortForwarder) Start(path, co, address string, ports []string) (*portfo 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() @@ -131,11 +150,11 @@ 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) { - cfg, err := p.Config().RESTConfig() + cfg, err := p.Client().Config().RESTConfig() if err != nil { return nil, err } 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/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 3fe02c52..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<- string, 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) +func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { + 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<- string, 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 25b82e0f..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. @@ -22,20 +24,62 @@ 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 { - o, err := s.Factory.Get(s.gvr.String(), opts.Path, true, labels.Everything()) +func (s *Service) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { + 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/table.go b/internal/dao/table.go index 3abdf94e..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,7 +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) { - log.Debug().Msgf("TABLE-LIST %q:%q", ns, t.gvr) + 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() @@ -58,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/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 c57ccb70..ccddbb8e 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. @@ -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/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/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/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/model/table.go b/internal/model/table.go index 327d5586..e54decbf 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -75,10 +75,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 +139,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 @@ -205,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) { @@ -259,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 != "" { @@ -298,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/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/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/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/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/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/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/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/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, ",") } 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/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/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/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 { 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/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/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/config.go b/internal/ui/config.go index cc4f69e3..857179db 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -20,20 +20,20 @@ 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 } -// 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 } @@ -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() @@ -103,6 +97,9 @@ func (c *Configurator) RefreshStyles(context string) { func (c *Configurator) updateStyles(f string) { c.skinFile = f + if !c.HasSkin() { + c.Styles.DefaultSkin() + } c.Styles.Update() render.StdColor = config.AsColor(c.Styles.Frame().Status.NewColor) diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index 0c409721..2eb9006f 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -17,24 +17,12 @@ 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("") - assert.True(t, cfg.HasSkins()) + assert.True(t, cfg.HasSkin()) 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_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_forward_test.go b/internal/ui/dialog/port_forward_test.go deleted file mode 100644 index 1c2461e1..00000000 --- a/internal/ui/dialog/port_forward_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package dialog - -import ( - "testing" - - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tview" - "github.com/stretchr/testify/assert" -) - -func TestPortForwardDialog(t *testing.T) { - p := ui.NewPages() - - okFunc := func(address, lport, cport string) { - } - ShowPortForward(p, "8080", okFunc) - - d := p.GetPrimitive(portForwardKey).(*tview.ModalForm) - assert.NotNil(t, d) - - DismissPortForward(p) - assert.Nil(t, p.GetPrimitive(portForwardKey)) -} - -func TestStripPort(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, stripPort(u.port)) - }) - } -} 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 7789833e..e5f2ec9c 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -27,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 diff --git a/internal/ui/menu.go b/internal/ui/menu.go index 55184944..958cf5d9 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -15,8 +15,8 @@ import ( ) const ( - menuIndexFmt = " [key:bg:b]<%d> [fg:bg:d]%s " - maxRows = 7 + menuIndexFmt = " [key:-:b]<%d> [fg:-:d]%s " + maxRows = 6 ) var menuRX = regexp.MustCompile(`\d`) @@ -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.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/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/ui/tree.go b/internal/ui/tree.go index 61363d14..1e0971b0 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,8 @@ func (t *Tree) ExtraHints() map[string]string { return nil } -func (t *Tree) bindKeys() { +// BindKeys binds default mnemonics. +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 c15ed77c..f0ec5529 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -118,18 +118,22 @@ 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 } } - if run(true, r.App(), bin, bg, aa...) { + if run(r.App(), shellOpts{clear: true, binary: bin, background: bg, args: aa}) { r.App().Flash().Info("Plugin command launched successfully!") } else { r.App().Flash().Info("Plugin command failed!") diff --git a/internal/view/app.go b/internal/view/app.go index 43efb7f8..903917a1 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" @@ -18,13 +18,13 @@ import ( "github.com/rs/zerolog/log" ) -// ExitStatus indicates UI exit conditions. +// // ExitStatus indicates UI exit conditions. 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. @@ -51,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) @@ -61,9 +59,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. @@ -117,7 +113,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), @@ -147,7 +143,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 @@ -172,6 +167,7 @@ func (a *App) buildHeader() tview.Primitive { func (a *App) Halt() { if a.cancelFn != nil { a.cancelFn() + a.cancelFn = nil } } @@ -198,32 +194,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 } @@ -271,11 +266,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 @@ -315,19 +310,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/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())) } diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index 681eddee..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)).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/browser.go b/internal/view/browser.go index b83f9bfb..80c65281 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 @@ -147,7 +145,6 @@ func (b *Browser) TableDataChanged(data render.TableData) { b.app.QueueUpdateDraw(func() { b.refreshActions() b.Update(data) - b.App().ClearStatus(false) }) } @@ -175,7 +172,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) } @@ -292,7 +289,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(b.app, shellOpts{clear: true, args: append(args, n)}) { b.app.Flash().Err(errors.New("Edit exec failed")) } } @@ -310,6 +307,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 } @@ -370,7 +370,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/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/internal/view/container.go b/internal/view/container.go index 385b8a1e..073f22fb 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 ( @@ -117,6 +114,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 @@ -126,8 +124,8 @@ 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) + ShowPortForwards(c, c.GetTable().Path, ports, startFwdCB) return nil } @@ -146,58 +144,13 @@ 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()) - ports := []string{lport + ":" + cport} - fw, err := pf.Start(c.GetTable().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) -} - -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) - }) - - pf.SetActive(true) - if err := f.ForwardPorts(); err != nil { - c.App().Flash().Err(err) - return - } - c.App().QueueUpdateDraw(func() { - c.App().factory.DeleteForwarder(pf.FQN()) - pf.SetActive(false) - }) + return pp, 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/details.go b/internal/view/details.go index c1e52f67..66d64624 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,125 @@ 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 } +// 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 + + 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 +204,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 +310,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/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/exec.go b/internal/view/exec.go index dc5e39e2..ff905ca7 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -13,39 +13,53 @@ import ( "github.com/rs/zerolog/log" ) -func runK(clear bool, app *App, args ...string) bool { +const ( + shellCheck = `command -v bash >/dev/null && exec bash || exec sh` + bannerFmt = "<> Pod: %s | Container: %s \n" +) + +type shellOpts struct { + clear, background bool + binary string + banner string + args []string +} + +func runK(app *App, opts shellOpts) bool { bin, err := exec.LookPath("kubectl") if err != nil { log.Error().Msgf("Unable to find kubectl command in path %v", err) return false } + opts.binary, opts.background = bin, false - return run(clear, app, bin, false, args...) + return run(app, opts) } -func run(clear bool, app *App, bin string, bg bool, args ...string) bool { +func run(app *App, opts shellOpts) bool { app.Halt() defer app.Resume() return app.Suspend(func() { - if err := execute(clear, bin, bg, args...); err != nil { + if err := execute(opts); err != nil { app.Flash().Errf("Command exited: %v", err) } }) } -func edit(clear bool, app *App, args ...string) bool { +func edit(app *App, opts shellOpts) bool { bin, err := exec.LookPath(os.Getenv("EDITOR")) if err != nil { log.Error().Msgf("Unable to find editor command in path %v", err) return false } + opts.binary, opts.background = bin, false - return run(clear, app, bin, false, args...) + return run(app, opts) } -func execute(clear bool, bin string, bg bool, args ...string) error { - if clear { +func execute(opts shellOpts) error { + if opts.clear { clearScreen() } ctx, cancel := context.WithCancel(context.Background()) @@ -62,18 +76,20 @@ func execute(clear bool, bin string, bg bool, args ...string) error { cancel() }() - log.Debug().Msgf("Running command > %s %s", bin, strings.Join(args, " ")) + log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " ")) - cmd := exec.Command(bin, args...) + cmd := exec.Command(opts.binary, opts.args...) var err error - if bg { + if opts.background { err = cmd.Start() } else { cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + + _, _ = cmd.Stdout.Write([]byte(opts.banner)) 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/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/view/helpers.go b/internal/view/helpers.go index bafeacfb..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 @@ -67,7 +72,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..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, "", "") + 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) @@ -173,7 +175,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/ofaas.go b/internal/view/ofaas.go new file mode 100644 index 00000000..00c93a2c --- /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.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), + }) +} + +func (o *OpenFaas) showPods(a *App, _ ui.Tabular, _, path string) { + labels := o.GetTable().GetSelectedCell(o.GetTable().NameColIndex() + 3) + 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/pf_dialog.go b/internal/view/pf_dialog.go new file mode 100644 index 00000000..6b7e3b15 --- /dev/null +++ b/internal/view/pf_dialog.go @@ -0,0 +1,102 @@ +package view + +import ( + "fmt" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" +) + +const portForwardKey = "portforward" + +// PortForwardFunc represents a port-forward callback function. +type PortForwardFunc func(v ResourceViewer, path, co string, mapper client.PortTunnel) + +// ShowPortForwards pops a port forwarding configuration dialog. +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(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) { + p1, p2 = sel, extractPort(sel) + }) + + dropD, ok := f.GetFormItem(0).(*tview.DropDown) + if ok { + dropD.SetFieldBackgroundColor(styles.BgColor()) + list := dropD.GetList() + 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 + }) + f.AddInputField("Address:", address, 20, nil, func(h string) { + address = h + }) + + pages := v.App().Content.Pages + + f.AddButton("OK", func() { + tunnel := client.PortTunnel{ + Address: address, + LocalPort: p2, + ContainerPort: extractPort(p1), + } + okFn(v, path, extractContainer(p1), tunnel) + }) + f.AddButton("Cancel", func() { + DismissPortForwards(pages) + }) + + modal := tview.NewModalForm(fmt.Sprintf("", path), f) + modal.SetDoneFunc(func(_ int, b string) { + DismissPortForwards(pages) + }) + + pages.AddPage(portForwardKey, modal, false, false) + pages.ShowPage(portForwardKey) +} + +// DismissPortForwards dismiss the port forward dialog. +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/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 81a88dd7..7d5e3e8b 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -4,22 +4,22 @@ import ( "context" "errors" "fmt" + "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" + "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" "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 @@ -27,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()) @@ -64,7 +68,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 { @@ -109,36 +112,23 @@ 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, "") - 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 + return nil } func (p *Pod) attachCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -171,7 +161,7 @@ func (p *Pod) attachCmd(evt *tcell.EventKey) *tcell.EventKey { p.App().Flash().Err(err) } - return evt + return nil } func (p *Pod) shellIn(path, co string) { @@ -189,34 +179,44 @@ func (p *Pod) attachIn(path string) { // ---------------------------------------------------------------------------- // Helpers... -func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, error) { - o, err := f.Get("v1/pods", path, true, labels.Everything()) - if err != nil { - return nil, err +func containerShellin(a *App, comp model.Component, path, co string) error { + if co != "" { + resumeShellIn(a, comp, path, co) + return nil } - var pod v1.Pod - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) + cc, err := fetchContainers(a.factory, path, false) if err != nil { - return nil, err + 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 } - nn := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers)) - for _, c := range pod.Spec.Containers { - nn = append(nn, c.Name) - } - if includeInit { - for _, c := range pod.Spec.InitContainers { - nn = append(nn, c.Name) - } - } - return nn, nil + 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) - log.Debug().Msgf("Shell args %v", args) - if !runK(true, a, 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")) } } @@ -258,3 +258,29 @@ func computeAttachArgs(path, context string, kcfg *string) []string { } return args } + +func fetchContainers(f *watch.Factory, path string, includeInit bool) ([]string, error) { + o, err := f.Get("v1/pods", path, true, 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 + } + + nn := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers)) + for _, c := range pod.Spec.Containers { + nn = append(nn, c.Name) + } + if !includeInit { + return nn, nil + } + for _, c := range pod.Spec.InitContainers { + nn = append(nn, c.Name) + } + + return nn, nil +} diff --git a/internal/view/port_forward.go b/internal/view/port_forward.go index 551e2558..3901ef75 100644 --- a/internal/view/port_forward.go +++ b/internal/view/port_forward.go @@ -7,7 +7,6 @@ import ( "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" @@ -42,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) { @@ -71,34 +70,32 @@ 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 } + cfg := dao.BenchConfigFor(p.App().BenchFile, path) + cfg.Name = path r, _ := p.GetTable().GetSelection() - cfg := defaultConfig() - if b, ok := p.App().Bench.Benchmarks.Containers[sel]; ok { - cfg = b - } - cfg.Name = sel - 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() { @@ -127,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 @@ -146,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/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/screen_dump.go b/internal/view/screen_dump.go index 41b82088..41799d65 100644 --- a/internal/view/screen_dump.go +++ b/internal/view/screen_dump.go @@ -45,7 +45,7 @@ func (s *ScreenDump) edit(app *App, model ui.Tabular, gvr, path string) { s.Stop() defer s.Start() - if !edit(true, app, path) { + if !edit(app, shellOpts{clear: true, args: []string{path}}) { app.Flash().Err(errors.New("Failed to launch editor")) } } 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/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 5cd7257b..51711e63 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -8,14 +8,11 @@ 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/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. @@ -28,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) @@ -45,20 +44,17 @@ func (s *Service) bindKeys(aa ui.KeyActions) { }) } -func (s *Service) showPods(app *App, _ ui.Tabular, gvr, path string) { - o, err := app.factory.Get(gvr, path, true, labels.Everything()) +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 { - app.Flash().Err(err) - return - } - var svc v1.Service - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) - if err != nil { - app.Flash().Err(err) + a.Flash().Err(err) return } - showPodsWithLabels(app, path, svc.Spec.Selector) + showPodsWithLabels(a, path, svc.Spec.Selector) } func (s *Service) checkSvc(row int) error { @@ -86,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!!") @@ -105,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 @@ -119,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) @@ -170,6 +161,9 @@ func (s *Service) benchDone() { }) } +// ---------------------------------------------------------------------------- +// Helpers... + 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/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/view/xray.go b/internal/view/xray.go index f6e7a634..e944aac6 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,23 +159,30 @@ func (x *Xray) refreshActions() { aa[ui.KeyD] = ui.NewKeyAction("Describe", x.describeCmd, true) } - if ref.GVR == "containers" { + 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) + 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.KeyA] = ui.NewKeyAction("Attach", x.attachCmd, 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 { @@ -194,6 +205,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) @@ -201,62 +240,61 @@ 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) attachCmd(evt *tcell.EventKey) *tcell.EventKey { ref := x.selectedSpec() if ref == nil { @@ -284,43 +322,42 @@ func (x *Xray) attachIn(path string) { } 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).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 @@ -328,12 +365,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 } @@ -348,31 +385,31 @@ 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) } } 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 != "" { args = append(args, "--kubeconfig", *cfg) } - if !runK(true, x.app, append(args, n)...) { + if !runK(x.app, shellOpts{args: append(args, n)}) { x.app.Flash().Err(errors.New("Edit exec failed")) } } @@ -435,14 +472,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) } @@ -491,13 +529,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 @@ -509,7 +547,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) } @@ -638,9 +677,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") @@ -652,11 +691,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() {}) @@ -688,15 +727,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/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", diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 8b1d9849..32d86ae6 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{}) { @@ -214,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 } @@ -225,11 +232,17 @@ 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 } // 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 aa53cc1b..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 { - // Start initializes a port forward. - Start(path, co, address string, ports []string) (*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. 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..5d2d8577 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -52,8 +52,53 @@ type TreeRef string // NodeSpec represents a node resource specification. type NodeSpec struct { - GVR, Path, Status string - Parent *NodeSpec + GVRs, Paths, Statuses []string +} + +// ParentGVR returns the parent GVR. +func (s NodeSpec) ParentGVR() *string { + if len(s.GVRs) > 1 { + return &s.GVRs[1] + } + return nil +} + +// ParentPath returns the parent path. +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 +190,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 +223,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 +300,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 +501,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 +516,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 } 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 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