Merge branch 'master' into dont-allow-same-port-twice

mine
Fernand Galiana 2020-02-20 13:48:24 -07:00 committed by GitHub
commit 22ff38ef0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
299 changed files with 6462 additions and 1841 deletions

6
.gitignore vendored
View File

@ -1,6 +1,5 @@
.vscode
.idea
k9s.log
.envrc
cov.out
execs
@ -10,10 +9,9 @@ dist
notes
vendor
go.mod1
popeye1.go
gen.sh
cluster_info_test.go
*.test
*.log
*~
pod1.go
faas
demos

View File

@ -15,23 +15,23 @@ builds:
goarch:
- 386
- amd64
- arm
- arm64
- arm
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:

View File

@ -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}

158
README.md
View File

@ -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
@ -95,8 +99,10 @@ K9s is available on Linux, OSX and Windows platforms.
---
## Demo Video
## Demo Videos/Recordings
* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN)
* [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)
@ -136,21 +142,21 @@ K9s uses aliases to navigate most K8s resources.
| `:`ns`<ENTER>` | To view and switch to another Kubernetes namespace | `:`+`ns`+`<ENTER>` |
| `:screendump`, `:sd` | To view all saved resources | |
| `Ctrl-d` | To delete a resource (TAB and ENTER to confirm) | |
| `Ctrl-k` | To delete a resource (no confirmation dialog) | |
| `Ctrl-k` | To kill a resource (no confirmation dialog!) | |
| `:q`, `Ctrl-c` | To bail out of K9s | |
---
## 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 +191,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 +206,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 +254,7 @@ plugin:
description: Pod logs
scopes:
- po
command: /usr/local/bin/kubectl
command: kubectl
background: false
args:
- logs
@ -233,6 +274,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 +283,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 `<ENTER>`. NOTE: Port-forwards only last for the duration of the K9s session and will be terminated upon exit.
@ -268,11 +311,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 +334,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 +351,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 +458,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 +492,7 @@ k9s:
activeColor: skyblue
# Resource status and update styles
status:
newColor: #00ff00
newColor: '#00ff00'
modifyColor: powderblue
addColor: lightskyblue
errorColor: indianred
@ -498,17 +506,17 @@ k9s:
highlightColor: skyblue
counterColor: slateblue
filterColor: slategray
# TableView attributes.
table:
fgColor: blue
bgColor: darkblue
cursorColor: aqua
# Header row styles.
header:
fgColor: white
bgColor: darkblue
sorterColor: orange
views:
# TableView attributes.
table:
fgColor: blue
bgColor: darkblue
cursorColor: aqua
# Header row styles.
header:
fgColor: white
bgColor: darkblue
sorterColor: orange
# YAML info styles.
yaml:
keyColor: steelblue
@ -585,8 +593,8 @@ to make this project a reality!
## Meet The Core Team!
* [Fernand Galiana](https://github.com/derailed)
* <img src="assets/mail.png" width="16" height="auto"/> fernand@imhotep.io
* <img src="assets/twitter.png" width="16" height="auto"/> [@kitesurfer](https://twitter.com/kitesurfer?lang=en)
* <img src="assets/mail.png" width="16" height="auto"/> fernand@imhotep.io
* <img src="assets/twitter.png" width="16" height="auto"/> [@kitesurfer](https://twitter.com/kitesurfer?lang=en)
---

BIN
assets/k9s_doc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
assets/k9s_fez.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
assets/k9s_health.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 KiB

View File

@ -0,0 +1,67 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# 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)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -0,0 +1,41 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# 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!
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -0,0 +1,58 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# 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)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_fez.png" align="center" width="400" height="auto"/>
## 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)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -0,0 +1,56 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# 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)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_fez.png" align="center" width="400" height="auto"/>
## 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
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -0,0 +1,23 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# 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
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -0,0 +1,185 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.16.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)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_doc.png" align="center"/>
This is one of these drops that may make you wonder if you'll go from zero to hero or likely the reverse?? Will see how this goes... Please proceed with caution on this one as there could very well be much distrubances in the force...
Lots of code churns so could have totally hose some stuff, but like my GranPappy used to say `can't cook without making a mess!`
## Going Wide?
In this drop, we've enabled a new shortcut namely `wide` as `Ctrl-w`. On table views, you will be able to see more information about the resources such as labels or others depending on the viewed resource. This mnemonic works as a toggle so you can `narrow` the view by hitting it again.
## Zoom, Zoom, Zoom!
While viewing some resources that may contain errors, sorting on columns may not achieve the results you're seeking ie `show me all resources in an error state`. We've added a new option to achieve just that aka `zoom errors` as `ctrl-z`. This works as a toggle and will unveil resources that are need of some TLC on your part ;)
## Does Your Cluster Have A Pulse 💓?
In this drop, we're introducing a brand new view aka `K9s Pulses` 💓. This is a summary view listing the most sailient resources in your clusters and their current states. This view tracks two main metrics ie Ok and Toast on a 5sec beat. This view affords cluster activity and failure rates. BTW this is the zero to hero deal 🙀 Hopefully you'll dig it as this was much work to put together and I personally think it's the `ducks nuts`... If you like, please give me some luving on social or via GH sponsors as batteries are running low...
To active, enter command mode by typing in `:pulse` aliases are `pu`, `pulses` or `hz`
To navigate thru the various pulses, you can use `tab`/`backtab` or use the menu index (just like namespaces selectors). Once on a pulse view, you can press `enter` to see the associated resource table view. Pressing `esc` will nav you back.
As I've may have mentioned before, my front-end/UX FU is weak, so I've also added a way for you to skin the charts via skins yaml to your own liking. Please see the skin section below for an example on how to skin the pulses dials. BONUS you should be able to skin K9s live! How cool is that 😻?
NOTE: Pulses are very much experimental and could totally bomb on your clusters! So please thread carefully and please do report (kindly!) back.
## BReaking Bad!
In this drop I've broken a few things (that I know off...), here is the list as I can recall...
1. Toggle header aka `my red headed step child`. Key moved (again!) now `Ctrl-e`
2. Skin yaml layout CHANGED! Moved table and xray sections under views and added charts section.
## Skins Updates!
The skin file format CHANGE! If you are running skins with K9s, please make sure to update your skin file. If not K9s could bomb coming up!
NOTE: I don't think I'll get around to update all the contributed skins in this repo `skins` dir. If you're looking for a way to help out and are UI inclined, please take a peek and make them cool!
```yaml
# my_cluster_skin.yml
# Styles...
foreground: &foreground "#f8f8f2"
background: &background "#282a36"
current_line: &current_line "#44475a"
selection: &selection "#44475a"
comment: &comment "#6272a4"
cyan: &cyan "#8be9fd"
green: &green "#50fa7b"
orange: &orange "#ffb86c"
pink: &pink "#ff79c6"
purple: &purple "#bd93f9"
red: &red "#ff5555"
yellow: &yellow "#f1fa8c"
# Skin...
k9s:
# General K9s styles
body:
fgColor: *foreground
bgColor: *background
logoColor: *purple
# ClusterInfoView styles.
info:
fgColor: *pink
sectionColor: *foreground
frame:
# Borders styles.
border:
fgColor: *selection
focusColor: *current_line
menu:
fgColor: *foreground
keyColor: *pink
# Used for favorite namespaces
numKeyColor: *purple
# CrumbView attributes for history navigation.
crumbs:
fgColor: *foreground
bgColor: *current_line
activeColor: *current_line
# Resource status and update styles
status:
newColor: *cyan
modifyColor: *purple
addColor: *green
errorColor: *red
highlightcolor: *orange
killColor: *comment
completedColor: *comment
# Border title styles.
title:
fgColor: *foreground
bgColor: *current_line
highlightColor: *orange
counterColor: *purple
filterColor: *pink
views:
charts:
bgColor: *background
dialBgColor: "#0A2239"
chartBgColor: "#0A2239"
defaultDialColors:
- "#1E3888"
- "#820101"
defaultChartColors:
- "#1E3888"
- "#820101"
resourceColors:
batch/v1/jobs:
- "#5D737E"
- "#820101"
v1/persistentvolumes:
- "#3E554A"
- "#820101"
cpu:
- "#6EA4BF"
- "#820101"
mem:
- "#17505B"
- "#820101"
v1/events:
- "#073B3A"
- "#820101"
v1/pods:
- "#487FFF"
- "#820101"
# TableView attributes.
table:
fgColor: *foreground
bgColor: *background
cursorColor: *current_line
# Header row styles.
header:
fgColor: *foreground
bgColor: *background
sorterColor: *cyan
# Xray view attributes.
xray:
fgColor: *foreground
bgColor: *background
cursorColor: *current_line
graphicColor: *purple
showIcons: true
# YAML info styles.
yaml:
keyColor: *pink
colonColor: *purple
valueColor: *foreground
# Logs styles.
logs:
fgColor: *foreground
bgColor: *background
```
## Resolved Bugs/Features/PRs
- [Issue #557](https://github.com/derailed/k9s/issues/557)
- [Issue #555](https://github.com/derailed/k9s/issues/555)
- [Issue #554](https://github.com/derailed/k9s/issues/554)
- [Issue #553](https://github.com/derailed/k9s/issues/553)
- [Issue #552](https://github.com/derailed/k9s/issues/552)
- [Issue #551](https://github.com/derailed/k9s/issues/551)
- [Issue #549](https://github.com/derailed/k9s/issues/549) A start with pulses...
- [Issue #540](https://github.com/derailed/k9s/issues/540)
- [Issue #421](https://github.com/derailed/k9s/issues/421)
- [Issue #351](https://github.com/derailed/k9s/issues/351) Solved by Pulses?
- [Issue #25](https://github.com/derailed/k9s/issues/25) Pulses? Oldie but goodie!
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -0,0 +1,23 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.16.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)
---
Maintenance Release!
## Resolved Bugs/Features/PRs
- [Issue #561](https://github.com/derailed/k9s/issues/561)
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

16
go.mod
View File

@ -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.6
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/rivo/tview v0.0.0-20191018115645-bacbf5155bc1
github.com/rs/zerolog v1.18.0
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

95
go.sum
View File

@ -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,14 @@ 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/derailed/tview v0.3.6 h1:9PyX6Nu1vs9mCVfvV2q2fwT/dZta0dBGr4ZPjCF1KnU=
github.com/derailed/tview v0.3.6/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII=
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 +167,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 +180,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 +209,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 +230,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 +297,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 +305,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 +346,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 +392,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 +406,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 +446,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 +460,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,8 +482,10 @@ 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/mum4k/termdash v0.10.0/go.mod h1:l3tO+lJi9LZqXRq7cu7h5/8rDIK3AzelSuq2v/KncxI=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d h1:7PxY7LVfSZm7PEeBTyK1rj1gABdCO2mbri6GKO1cMDs=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
@ -448,10 +493,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 +516,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 +530,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=
@ -513,6 +568,7 @@ github.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H
github.com/rakyll/hey v0.1.2 h1:XlGaKcBdmXJaPImiTnE+TGLDUWQ2toYuHCwdrylLjmg=
github.com/rakyll/hey v0.1.2/go.mod h1:S5M+++KwbmxA7w68S92B5NdWiCB+cIhITaMUkq9W608=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1 h1:s9Lw4phBWkuQJUd+msaBMxP3utLvrFaBQV9jNgG55r0=
github.com/rivo/tview v0.0.0-20191018115645-bacbf5155bc1/go.mod h1:+rKjP5+h9HMwWRpAfhIkkQ9KE3m3Nz5rwn7YtUpwgqk=
github.com/rivo/uniseg v0.0.0-20190513083848-b9f5b9457d44/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
@ -523,10 +579,14 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/rs/zerolog v1.18.0 h1:CbAm3kP2Tptby1i9sYy2MGRg0uxIN9cyDb59Ys7W8z8=
github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto=
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 +614,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 +627,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 +645,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 +682,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 +695,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 +753,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 +763,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 +793,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 +826,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 +841,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 +904,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=

View File

@ -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() {

View File

@ -3,6 +3,7 @@ package client
import (
"errors"
"fmt"
"strings"
"sync"
"github.com/rs/zerolog/log"
@ -48,6 +49,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
@ -173,13 +178,31 @@ func (c *Config) ClusterNames() ([]string, error) {
// CurrentGroupNames retrieves the active group names.
func (c *Config) CurrentGroupNames() ([]string, error) {
if c.flags.ImpersonateGroup != nil && len(*c.flags.ImpersonateGroup) != 0 {
if areSet(c.flags.ImpersonateGroup) {
return *c.flags.ImpersonateGroup, nil
}
return []string{}, errors.New("unable to locate current group")
}
// ImpersonateGroups retrieves the active groupsif set on the CLI.
func (c *Config) ImpersonateGroups() (string, error) {
if areSet(c.flags.ImpersonateGroup) {
return strings.Join(*c.flags.ImpersonateGroup, ","), nil
}
return "", errors.New("no groups set")
}
// ImpersonateUser retrieves the active user name if set on the CLI.
func (c *Config) ImpersonateUser() (string, error) {
if isSet(c.flags.Impersonate) {
return *c.flags.Impersonate, nil
}
return "", errors.New("no user set")
}
// CurrentUserName retrieves the active user name.
func (c *Config) CurrentUserName() (string, error) {
if isSet(c.flags.Impersonate) {
@ -307,3 +330,7 @@ func (c *Config) ensureConfig() {
func isSet(s *string) bool {
return s != nil && len(*s) != 0
}
func areSet(s *[]string) bool {
return s != nil && len(*s) != 0
}

View File

@ -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,

View File

@ -3,20 +3,49 @@ package client
import (
"fmt"
"math"
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/cache"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
const (
mxCacheSize = 100
mxCacheExpiry = 1 * time.Minute
)
// MetricsDial tracks global metric server handle.
var MetricsDial *MetricsServer
// DialMetrics dials the metrics server.
func DialMetrics(c Connection) *MetricsServer {
if MetricsDial == nil {
MetricsDial = NewMetricsServer(c)
}
return MetricsDial
}
// ResetMetrics resets the metric server handle.
func ResetMetrics() {
MetricsDial = nil
}
// MetricsServer serves cluster metrics for nodes and pods.
type MetricsServer struct {
Connection
cache *cache.LRUExpireCache
}
// NewMetricsServer return a metric server instance.
func NewMetricsServer(c Connection) *MetricsServer {
return &MetricsServer{Connection: c}
return &MetricsServer{
Connection: c,
cache: cache.NewLRUExpireCache(mxCacheSize),
}
}
// NodesMetrics retrieves metrics for a given set of nodes.
@ -28,15 +57,15 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
for _, no := range nodes.Items {
mmx[no.Name] = NodeMetrics{
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
AvailMEM: toMB(no.Status.Allocatable.Memory().Value()),
AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()),
TotalCPU: no.Status.Capacity.Cpu().MilliValue(),
TotalMEM: toMB(no.Status.Capacity.Memory().Value()),
TotalMEM: ToMB(no.Status.Capacity.Memory().Value()),
}
}
for _, c := range metrics.Items {
if mx, ok := mmx[c.Name]; ok {
mx.CurrentCPU = c.Usage.Cpu().MilliValue()
mx.CurrentMEM = toMB(c.Usage.Memory().Value())
mx.CurrentMEM = ToMB(c.Usage.Memory().Value())
mmx[c.Name] = mx
}
}
@ -51,13 +80,13 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
for _, no := range nos.Items {
nodeMetrics[no.Name] = NodeMetrics{
AvailCPU: no.Status.Allocatable.Cpu().MilliValue(),
AvailMEM: toMB(no.Status.Allocatable.Memory().Value()),
AvailMEM: ToMB(no.Status.Allocatable.Memory().Value()),
}
}
for _, mx := range nmx.Items {
if m, ok := nodeMetrics[mx.Name]; ok {
m.CurrentCPU = mx.Usage.Cpu().MilliValue()
m.CurrentMEM = toMB(mx.Usage.Memory().Value())
m.CurrentMEM = ToMB(mx.Usage.Memory().Value())
nodeMetrics[mx.Name] = m
}
}
@ -74,86 +103,121 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
return nil
}
// FetchNodesMetrics return all metrics for pods in a given namespace.
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
var mx mv1beta1.NodeMetricsList
func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
if !m.HasMetrics() {
return &mx, fmt.Errorf("No metrics-server detected on cluster")
return fmt.Errorf("No metrics-server detected on cluster")
}
auth, err := m.CanI("", "metrics.k8s.io/v1beta1/nodes", ListAccess)
auth, err := m.CanI(ns, gvr, ListAccess)
if err != nil {
return &mx, err
return err
}
if !auth {
return &mx, fmt.Errorf("user is not authorized to list node metrics")
return fmt.Errorf(msg)
}
return nil
}
// FetchNodesMetrics return all metrics for nodes.
func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
const msg = "user is not authorized to list node metrics"
mx := new(mv1beta1.NodeMetricsList)
if err := m.checkAccess("", "metrics.k8s.io/v1beta1/nodes", msg); err != nil {
return mx, err
}
const key = "nodes"
if entry, ok := m.cache.Get(key); ok && entry != nil {
mxList, ok := entry.(*mv1beta1.NodeMetricsList)
if !ok {
return nil, fmt.Errorf("expected nodemetricslist but got %T", entry)
}
return mxList, nil
}
client, err := m.MXDial()
if err != nil {
return &mx, err
return mx, err
}
return client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
mxList, err := client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
if err != nil {
return mx, err
}
m.cache.Add(key, mxList, mxCacheExpiry)
return mxList, nil
}
// FetchPodsMetrics return all metrics for pods in a given namespace.
func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) {
var mx mv1beta1.PodMetricsList
if m.Connection == nil {
return &mx, fmt.Errorf("no client connection")
}
mx := new(mv1beta1.PodMetricsList)
const msg = "user is not authorized to list pods metrics"
if !m.HasMetrics() {
return &mx, fmt.Errorf("No metrics-server detected on cluster")
}
if ns == NamespaceAll {
ns = AllNamespaces
}
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", ListAccess)
if err != nil {
return &mx, err
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
return mx, err
}
if !auth {
return &mx, fmt.Errorf("user is not authorized to list pods metrics")
key := FQN(ns, "pods")
if entry, ok := m.cache.Get(key); ok {
mxList, ok := entry.(*mv1beta1.PodMetricsList)
if !ok {
return mx, fmt.Errorf("expected podmetricslist but got %T", entry)
}
return mxList, nil
}
client, err := m.MXDial()
if err != nil {
return &mx, err
return mx, err
}
mxList, err := client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
if err != nil {
return mx, err
}
m.cache.Add(key, mxList, mxCacheExpiry)
return client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{})
return mxList, err
}
// FetchPodMetrics return all metrics for pods in a given namespace.
func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) {
var mx mv1beta1.PodMetrics
if m.Connection == nil {
return &mx, fmt.Errorf("no client connection")
}
if !m.HasMetrics() {
return &mx, fmt.Errorf("No metrics-server detected on cluster")
}
var mx *mv1beta1.PodMetrics
const msg = "user is not authorized to list pod metrics"
ns, n := Namespaced(fqn)
if ns == NamespaceAll {
ns = AllNamespaces
}
auth, err := m.CanI(ns, "metrics.k8s.io/v1beta1/pods", GetAccess)
if err != nil {
return &mx, err
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
return mx, err
}
if !auth {
return &mx, fmt.Errorf("user is not authorized to list pod metrics")
var key = FQN(ns, "pods")
if entry, ok := m.cache.Get(key); ok {
if list, ok := entry.(*mv1beta1.PodMetricsList); ok && list != nil {
for _, m := range list.Items {
if FQN(m.Namespace, m.Name) == fqn {
return &m, nil
}
}
}
}
client, err := m.MXDial()
if err != nil {
return &mx, err
return mx, err
}
mx, err = client.MetricsV1beta1().PodMetricses(ns).Get(n, metav1.GetOptions{})
if err != nil {
return mx, err
}
m.cache.Add(key, mx, mxCacheExpiry)
return client.MetricsV1beta1().PodMetricses(ns).Get(n, metav1.GetOptions{})
return mx, nil
}
// PodsMetrics retrieves metrics for all pods in a given namespace.
@ -167,7 +231,7 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
var mx PodMetrics
for _, c := range p.Containers {
mx.CurrentCPU += c.Usage.Cpu().MilliValue()
mx.CurrentMEM += toMB(c.Usage.Memory().Value())
mx.CurrentMEM += ToMB(c.Usage.Memory().Value())
}
mmx[p.Namespace+"/"+p.Name] = mx
}
@ -178,8 +242,8 @@ func (m *MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetri
const megaByte = 1024 * 1024
// toMB converts bytes to megabytes.
func toMB(v int64) float64 {
// ToMB converts bytes to megabytes.
func ToMB(v int64) float64 {
return float64(v) / megaByte
}

11
internal/client/tunnel.go Normal file
View File

@ -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
}

View File

@ -22,6 +22,9 @@ const (
// ClusterScope designates a resource is not namespaced.
ClusterScope = "-"
// NotNamespaced designates a non resource namespace.
NotNamespaced = "*"
)
const (
@ -64,7 +67,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)

View File

@ -2,6 +2,7 @@ package config
import (
"io/ioutil"
"os"
"path/filepath"
"sync"
@ -87,6 +88,13 @@ func (a *Aliases) loadDefaults() {
// Load K9s aliases.
func (a *Aliases) Load() error {
a.loadDefaults()
_, err := os.Stat(K9sAlias)
if os.IsNotExist(err) {
log.Debug().Err(err).Msgf("No custom aliases found")
return nil
}
return a.LoadAliases(K9sAlias)
}
@ -139,11 +147,17 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
}
}
// LoadAliases loads alias from a given file.
func (a *Aliases) LoadAliases(path string) error {
// Load K9s aliases.
func (a *Aliases) Load() error {
a.loadDefaultAliases()
return a.LoadFileAliases(K9sAlias)
}
// LoadFileAliases loads alias from a given file.
func (a *Aliases) LoadFileAliases(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
}
@ -161,6 +175,63 @@ func (a *Aliases) LoadAliases(path string) error {
return nil
}
func (a *Aliases) loadDefaultAliases() {
a.mx.Lock()
defer a.mx.Unlock()
a.Alias["dp"] = "apps/v1/deployments"
a.Alias["sec"] = "v1/secrets"
a.Alias["jo"] = "batch/v1/jobs"
a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
const contexts = "contexts"
{
a.Alias["ctx"] = contexts
a.Alias[contexts] = contexts
a.Alias["context"] = contexts
}
const users = "users"
{
a.Alias["usr"] = users
a.Alias[users] = users
a.Alias["user"] = users
}
const groups = "groups"
{
a.Alias["grp"] = groups
a.Alias["group"] = groups
a.Alias[groups] = groups
}
const portFwds = "portforwards"
{
a.Alias["pf"] = portFwds
a.Alias[portFwds] = portFwds
a.Alias["portforward"] = portFwds
}
const benchmarks = "benchmarks"
{
a.Alias["be"] = benchmarks
a.Alias["benchmark"] = benchmarks
a.Alias[benchmarks] = benchmarks
}
const dumps = "screendumps"
{
a.Alias["sd"] = dumps
a.Alias["screendump"] = dumps
a.Alias[dumps] = dumps
}
const pulses = "pulses"
{
a.Alias["hz"] = pulses
a.Alias["pu"] = pulses
a.Alias["pulse"] = pulses
}
}
// Save alias to disk.
func (a *Aliases) Save() error {
log.Debug().Msg("[Config] Saving Aliases...")

View File

@ -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.LoadFileAliases("testdata/alias.yml"))
assert.Equal(t, 2, len(a.Alias))
}
@ -82,6 +82,6 @@ func TestAliasesSave(t *testing.T) {
a.Alias["blee"] = "duh"
assert.Nil(t, a.SaveAliases("/tmp/a.yml"))
assert.Nil(t, a.LoadAliases("/tmp/a.yml"))
assert.Nil(t, a.LoadFileAliases("/tmp/a.yml"))
assert.Equal(t, 2, len(a.Alias))
}

View File

@ -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: "/",
},
}
}

View File

@ -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))

View File

@ -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()

View File

@ -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))

View File

@ -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)

View File

@ -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) {

View File

@ -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"]

View File

@ -21,17 +21,31 @@ type StyleListener interface {
}
type (
// Color represents a color.
Color string
// Colors tracks multiple colors.
Colors []Color
// Styles tracks K9s styling options.
Styles struct {
K9s Style `yaml:"k9s"`
listeners []StyleListener
}
// Style tracks K9s styles.
Style struct {
Body Body `yaml:"body"`
Frame Frame `yaml:"frame"`
Info Info `yaml:"info"`
Views Views `yaml:"views"`
}
// Body tracks body styles.
Body struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
LogoColor string `yaml:"logoColor"`
FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"`
LogoColor Color `yaml:"logoColor"`
}
// Frame tracks frame styles.
@ -45,120 +59,171 @@ type (
// Views tracks individual view styles.
Views struct {
Yaml Yaml `yaml:"yaml"`
Log Log `yaml:"logs"`
Table Table `yaml:"table"`
Xray Xray `yaml:"xray"`
Charts Charts `yaml:"charts"`
Yaml Yaml `yaml:"yaml"`
Log Log `yaml:"logs"`
}
// Status tracks resource status styles.
Status struct {
NewColor string `yaml:"newColor"`
ModifyColor string `yaml:"modifyColor"`
AddColor string `yaml:"addColor"`
ErrorColor string `yaml:"errorColor"`
HighlightColor string `yaml:"highlightColor"`
KillColor string `yaml:"killColor"`
CompletedColor string `yaml:"completedColor"`
NewColor Color `yaml:"newColor"`
ModifyColor Color `yaml:"modifyColor"`
AddColor Color `yaml:"addColor"`
ErrorColor Color `yaml:"errorColor"`
HighlightColor Color `yaml:"highlightColor"`
KillColor Color `yaml:"killColor"`
CompletedColor Color `yaml:"completedColor"`
}
// Log tracks Log styles.
Log struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"`
}
// Yaml tracks yaml styles.
Yaml struct {
KeyColor string `yaml:"keyColor"`
ValueColor string `yaml:"valueColor"`
ColonColor string `yaml:"colonColor"`
KeyColor Color `yaml:"keyColor"`
ValueColor Color `yaml:"valueColor"`
ColonColor Color `yaml:"colonColor"`
}
// Title tracks title styles.
Title struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
HighlightColor string `yaml:"highlightColor"`
CounterColor string `yaml:"counterColor"`
FilterColor string `yaml:"filterColor"`
FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"`
HighlightColor Color `yaml:"highlightColor"`
CounterColor Color `yaml:"counterColor"`
FilterColor Color `yaml:"filterColor"`
}
// Info tracks info styles.
Info struct {
SectionColor string `yaml:"sectionColor"`
FgColor string `yaml:"fgColor"`
SectionColor Color `yaml:"sectionColor"`
FgColor Color `yaml:"fgColor"`
}
// Border tracks border styles.
Border struct {
FgColor string `yaml:"fgColor"`
FocusColor string `yaml:"focusColor"`
FgColor Color `yaml:"fgColor"`
FocusColor Color `yaml:"focusColor"`
}
// Crumb tracks crumbs styles.
Crumb struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
ActiveColor string `yaml:"activeColor"`
FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"`
ActiveColor Color `yaml:"activeColor"`
}
// Table tracks table styles.
Table struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
CursorColor string `yaml:"cursorColor"`
MarkColor string `yaml:"markColor"`
FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"`
CursorColor Color `yaml:"cursorColor"`
MarkColor Color `yaml:"markColor"`
Header TableHeader `yaml:"header"`
}
// TableHeader tracks table header styles.
TableHeader struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
SorterColor string `yaml:"sorterColor"`
FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"`
SorterColor Color `yaml:"sorterColor"`
}
// Xray tracks xray styles.
Xray struct {
FgColor string `yaml:"fgColor"`
BgColor string `yaml:"bgColor"`
CursorColor string `yaml:"cursorColor"`
GraphicColor string `yaml:"graphicColor"`
ShowIcons bool `yaml:"showIcons"`
FgColor Color `yaml:"fgColor"`
BgColor Color `yaml:"bgColor"`
CursorColor Color `yaml:"cursorColor"`
GraphicColor Color `yaml:"graphicColor"`
ShowIcons bool `yaml:"showIcons"`
}
// Menu tracks menu styles.
Menu struct {
FgColor string `yaml:"fgColor"`
KeyColor string `yaml:"keyColor"`
NumKeyColor string `yaml:"numKeyColor"`
FgColor Color `yaml:"fgColor"`
KeyColor Color `yaml:"keyColor"`
NumKeyColor Color `yaml:"numKeyColor"`
}
// Style tracks K9s styles.
Style struct {
Body Body `yaml:"body"`
Frame Frame `yaml:"frame"`
Info Info `yaml:"info"`
Table Table `yaml:"table"`
Xray Xray `yaml:"xray"`
Views Views `yaml:"views"`
// Charts tracks charts styles.
Charts struct {
BgColor Color `yaml:"bgColor"`
DialBgColor Color `yaml:"dialBgColor"`
ChartBgColor Color `yaml:"chartBgColor"`
DefaultDialColors Colors `yaml:"defaultDialColors"`
DefaultChartColors Colors `yaml:"defaultChartColors"`
ResourceColors map[string]Colors `yaml:"resourceColors"`
}
)
const (
// DefaultColor represents a default color.
DefaultColor Color = "default"
// TransparentColor represents the terminal bg color.
TransparentColor Color = "-"
)
// NewColor returns a new color.
func NewColor(c string) Color {
return Color(c)
}
// String returns color as string.
func (c Color) String() string {
return string(c)
}
// Color returns a view color.
func (c Color) Color() tcell.Color {
if c == DefaultColor {
return tcell.ColorDefault
}
if color, ok := tcell.ColorNames[c.String()]; ok {
return color
}
return tcell.GetColor(c.String())
}
// Colors converts series string colors to colors.
func (c Colors) Colors() []tcell.Color {
cc := make([]tcell.Color, 0, len(c))
for _, color := range c {
cc = append(cc, color.Color())
}
return cc
}
func newStyle() Style {
return Style{
Body: newBody(),
Frame: newFrame(),
Info: newInfo(),
Table: newTable(),
Views: newViews(),
Xray: newXray(),
}
}
func newCharts() Charts {
return Charts{
BgColor: "default",
DialBgColor: "default",
ChartBgColor: "default",
DefaultDialColors: Colors{Color("palegreen"), Color("orangered")},
DefaultChartColors: Colors{Color("palegreen"), Color("orangered")},
}
}
func newViews() Views {
return Views{
Yaml: newYaml(),
Log: newLog(),
Table: newTable(),
Xray: newXray(),
Charts: newCharts(),
Yaml: newYaml(),
Log: newLog(),
}
}
@ -188,7 +253,7 @@ func newStatus() Status {
ErrorColor: "orangered",
HighlightColor: "aqua",
KillColor: "mediumpurple",
CompletedColor: "gray",
CompletedColor: "lightgray",
}
}
@ -292,14 +357,24 @@ func NewStyles() *Styles {
}
}
// Reset resets styles.
func (s *Styles) Reset() {
s.K9s = newStyle()
}
// 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)
return s.Body().FgColor.Color()
}
// BgColor returns the background color.
func (s *Styles) BgColor() tcell.Color {
return AsColor(s.Body().BgColor)
return s.Body().BgColor.Color()
}
// AddListener registers a new listener.
@ -348,14 +423,19 @@ func (s *Styles) Title() Title {
return s.Frame().Title
}
// Charts returns charts styles.
func (s *Styles) Charts() Charts {
return s.K9s.Views.Charts
}
// Table returns table styles.
func (s *Styles) Table() Table {
return s.K9s.Table
return s.K9s.Views.Table
}
// Xray returns xray styles.
func (s *Styles) Xray() Xray {
return s.K9s.Xray
return s.K9s.Views.Xray
}
// Views returns views styles.
@ -383,15 +463,7 @@ func (s *Styles) Update() {
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
tview.Styles.ContrastBackgroundColor = s.BgColor()
tview.Styles.PrimaryTextColor = s.FgColor()
tview.Styles.BorderColor = AsColor(s.K9s.Frame.Border.FgColor)
tview.Styles.FocusColor = AsColor(s.K9s.Frame.Border.FocusColor)
}
// AsColor checks color index, if match return color otherwise pink it is.
func AsColor(c string) tcell.Color {
if color, ok := tcell.ColorNames[c]; ok {
return color
}
return tcell.GetColor(c)
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()
s.fireStylesChanged()
}

View File

@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestAsColor(t *testing.T) {
func TestColor(t *testing.T) {
uu := map[string]tcell.Color{
"blah": tcell.ColorDefault,
"blue": tcell.ColorBlue,
@ -20,19 +20,19 @@ func TestAsColor(t *testing.T) {
for k := range uu {
c, u := k, uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u, config.AsColor(c))
assert.Equal(t, u, config.NewColor(c).Color())
})
}
}
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)
assert.Equal(t, "black", s.Body().BgColor)
assert.Equal(t, "black", s.Table().BgColor)
assert.Equal(t, "cadetblue", s.Body().FgColor.String())
assert.Equal(t, "black", s.Body().BgColor.String())
assert.Equal(t, "black", s.Table().BgColor.String())
assert.Equal(t, tcell.ColorCadetBlue, s.FgColor())
assert.Equal(t, tcell.ColorBlack, s.BgColor())
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
@ -40,12 +40,12 @@ 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)
assert.Equal(t, "black", s.Body().BgColor)
assert.Equal(t, "black", s.Table().BgColor)
assert.Equal(t, "white", s.Body().FgColor.String())
assert.Equal(t, "black", s.Body().BgColor.String())
assert.Equal(t, "black", s.Table().BgColor.String())
assert.Equal(t, tcell.ColorWhite, s.FgColor())
assert.Equal(t, tcell.ColorBlack, s.BgColor())
assert.Equal(t, tcell.ColorBlack, tview.Styles.PrimitiveBackgroundColor)
@ -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"))
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -33,23 +33,21 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
if !ok {
return nil, fmt.Errorf("no context path for %q", c.gvr)
}
var (
pmx *mv1beta1.PodMetrics
err error
)
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(fqn); err != nil {
log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn)
}
}
po, err := c.fetchPod(fqn)
if err != nil {
return nil, err
}
var pmx *mv1beta1.PodMetrics
if c.Client().HasMetrics() {
mx := client.NewMetricsServer(c.Client())
if c.Client() != nil {
var err error
pmx, err = mx.FetchPodMetrics(fqn)
if err != nil {
log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn)
}
}
}
res := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers))
for _, co := range po.Spec.InitContainers {
res = append(res, makeContainerRes(co, po, pmx, true))
@ -62,7 +60,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")

View File

@ -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 }

View File

@ -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.

View File

@ -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)
}

View File

@ -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

View File

@ -21,6 +21,7 @@ var (
_ Loggable = (*Deployment)(nil)
_ Restartable = (*Deployment)(nil)
_ Scalable = (*Deployment)(nil)
_ Controller = (*Deployment)(nil)
)
// Deployment represents a deployment K8s resource.
@ -28,6 +29,11 @@ type Deployment struct {
Resource
}
// IsHappy check for happy deployments.
func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
return dp.Status.Replicas == dp.Status.AvailableReplicas
}
// Scale a Deployment.
func (d *Deployment) Scale(path string, replicas int32) error {
ns, n := client.Namespaced(path)
@ -51,12 +57,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 +70,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
}

View File

@ -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.
@ -32,14 +32,14 @@ type DaemonSet struct {
Resource
}
// IsHappy check for happy deployments.
func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool {
return ds.Status.DesiredNumberScheduled == ds.Status.CurrentNumberScheduled
}
// 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 +51,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 +74,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 +89,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 +106,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 +114,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...

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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...)
}

View File

@ -29,17 +29,19 @@ 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")
}
mx := client.NewMetricsServer(n.Client())
nmx, err := mx.FetchNodesMetrics()
if err != nil {
log.Warn().Err(err).Msgf("No node metrics")
var (
nmx *mv1beta1.NodeMetricsList
err error
)
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(); err != nil {
log.Warn().Err(err).Msgf("No node metrics")
}
}
nn, err := FetchNodes(n.Factory, labels)

219
internal/dao/ofaas.go Normal file
View File

@ -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
}

View File

@ -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.
@ -37,6 +38,16 @@ type Pod struct {
Resource
}
// IsHappy check for happy deployments.
func (p *Pod) IsHappy(po v1.Pod) bool {
for _, c := range po.Status.Conditions {
if c.Status == v1.ConditionFalse {
return false
}
}
return true
}
// Get returns a resource instance if found, else an error.
func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
o, err := p.Resource.Get(ctx, path)
@ -49,11 +60,11 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {
return nil, fmt.Errorf("expecting *unstructured.Unstructured but got `%T", o)
}
// No Deal!
mx := client.NewMetricsServer(p.Client())
pmx, err := mx.FetchPodMetrics(path)
if err != nil {
log.Warn().Err(err).Msgf("No pods metrics")
var pmx *mv1beta1.PodMetrics
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(path); err != nil {
log.Warn().Err(err).Msgf("No pod metrics")
}
}
return &render.PodWithMetrics{Raw: u, MX: pmx}, nil
@ -76,10 +87,11 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
return oo, err
}
mx := client.NewMetricsServer(p.Client())
pmx, err := mx.FetchPodsMetrics(ns)
if err != nil {
log.Warn().Err(err).Msgf("No pods metrics")
var pmx *mv1beta1.PodMetricsList
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ns); err != nil {
log.Warn().Err(err).Msgf("No pods metrics")
}
}
var res []runtime.Object
@ -122,18 +134,12 @@ 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())
pod, err := p.GetInstance(path)
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
}
cc := []string{}
cc := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
for _, c := range pod.Spec.Containers {
cc = append(cc, c.Name)
}
@ -147,15 +153,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 +221,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 +233,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 +248,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 +261,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 +269,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())
}
// ----------------------------------------------------------------------------

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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
}

18
internal/dao/pulse.go Normal file
View File

@ -0,0 +1,18 @@
package dao
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
)
// Pulse tracks pulses.
type Pulse struct {
NonResource
}
// List lists out pulses.
func (h *Pulse) List(ctx context.Context, ns string) ([]runtime.Object, error) {
return nil, fmt.Errorf("NYI")
}

View File

@ -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,9 +136,19 @@ func loadNonResource(m ResourceMetas) {
loadK9s(m)
loadRBAC(m)
loadHelm(m)
if IsOpenFaasEnabled() {
loadOpenFaas(m)
}
}
func loadK9s(m ResourceMetas) {
m[client.NewGVR("pulses")] = metav1.APIResource{
Name: "pulses",
Kind: "Pulse",
SingularName: "pulses",
ShortNames: []string{"hz", "pu"},
Categories: []string{"k9s"},
}
m[client.NewGVR("xrays")] = metav1.APIResource{
Name: "xray",
Kind: "XRays",
@ -203,6 +214,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",

View File

@ -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

23
internal/dao/rs.go Normal file
View File

@ -0,0 +1,23 @@
package dao
import (
appsv1 "k8s.io/api/apps/v1"
)
// ReplicaSet represents a replicaset K8s resource.
type ReplicaSet struct {
Resource
}
// IsHappy check for happy deployments.
func (d *ReplicaSet) IsHappy(rs appsv1.ReplicaSet) bool {
if rs.Status.Replicas == 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
return false
}
if rs.Status.Replicas != 0 && rs.Status.Replicas != rs.Status.ReadyReplicas {
return false
}
return true
}

View File

@ -21,6 +21,7 @@ var (
_ Loggable = (*StatefulSet)(nil)
_ Restartable = (*StatefulSet)(nil)
_ Scalable = (*StatefulSet)(nil)
_ Controller = (*StatefulSet)(nil)
)
// StatefulSet represents a K8s sts.
@ -28,6 +29,11 @@ type StatefulSet struct {
Resource
}
// IsHappy check for happy sts.
func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
return sts.Status.Replicas == sts.Status.ReadyReplicas
}
// Scale a StatefulSet.
func (s *StatefulSet) Scale(path string, replicas int32) error {
ns, n := client.Namespaced(path)
@ -51,12 +57,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 +71,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 +92,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
}

View File

@ -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
}

View File

@ -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

19
internal/dao/testdata/benchspec.yml vendored Normal file
View File

@ -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: /

View File

@ -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.

54
internal/health/check.go Normal file
View File

@ -0,0 +1,54 @@
package health
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Check tracks resource health.
type Check struct {
Counts
GVR string
}
// Checks represents a collection of health checks.
type Checks []*Check
// NewCheck returns a new health check.
func NewCheck(gvr string) *Check {
return &Check{
GVR: gvr,
Counts: make(Counts),
}
}
// Set sets a health metric.
func (c *Check) Set(l Level, v int) {
c.Counts[l] = v
}
// Inc increments a health metric.
func (c *Check) Inc(l Level) {
c.Counts[l]++
}
// Total stores a metric total.
func (c *Check) Total(n int) {
c.Counts[Corpus] = n
}
// Tally retrieves a given health metric.
func (c *Check) Tally(l Level) int {
return c.Counts[l]
}
// GetObjectKind returns a schema object.
func (Check) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (c Check) DeepCopyObject() runtime.Object {
return c
}

View File

@ -0,0 +1,26 @@
package health_test
import (
"testing"
"github.com/derailed/k9s/internal/health"
"github.com/stretchr/testify/assert"
)
func TestCheck(t *testing.T) {
var cc health.Checks
c := health.NewCheck("test")
n := 0
for i := 0; i < 10; i++ {
c.Inc(health.OK)
cc = append(cc, c)
n++
}
c.Total(n)
assert.Equal(t, 10, len(cc))
assert.Equal(t, 10, c.Tally(health.Corpus))
assert.Equal(t, 10, c.Tally(health.OK))
assert.Equal(t, 0, c.Tally(health.Toast))
}

44
internal/health/types.go Normal file
View File

@ -0,0 +1,44 @@
package health
// Level tracks health count categories.
type Level int
const (
// Unknown represents no health level.
Unknown Level = 1 << iota
// Corpus tracks total health.
Corpus
// OK tracks healhy.
OK
// Warn tracks health warnings.
Warn
// Toast tracks unhealties.
Toast
)
// Message represents a health message.
type Message struct {
Level Level
Message string
GVR string
FQN string
}
// Messages tracks a collection of messages.
type Messages []Message
// Counts tracks health counts by category.
type Counts map[Level]int
// Vital tracks a resource vitals.
type Vital struct {
Resource string
Total, OK, Toast int
}
// Vitals tracks a collection of resource health.
type Vitals []Vital

View File

@ -25,4 +25,6 @@ const (
KeyApp ContextKey = "app"
KeyStyles ContextKey = "styles"
KeyMetrics ContextKey = "metrics"
KeyToast ContextKey = "toast"
KeyWithMetrics ContextKey = "withMetrics"
)

View File

@ -35,7 +35,7 @@ type (
func NewCluster(f dao.Factory) *Cluster {
return &Cluster{
factory: f,
mx: client.NewMetricsServer(f.Client()),
mx: client.DialMetrics(f.Client()),
}
}

View File

@ -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
}

156
internal/model/flash.go Normal file
View File

@ -0,0 +1,156 @@
package model
import (
"context"
"fmt"
"time"
"github.com/rs/zerolog/log"
)
const (
// DefaultFlashDelay sets the flash clear delay.
DefaultFlashDelay = 3 * time.Second
// FlashInfo represents an info message.
FlashInfo FlashLevel = iota
// FlashWarn represents an warning message.
FlashWarn
// FlashErr represents an error message.
FlashErr
)
// LevelMessage tracks an message and severity.
type LevelMessage struct {
Level FlashLevel
Text string
}
func newClearMessage() LevelMessage {
return LevelMessage{}
}
// IsClear returns true if message is empty.
func (l LevelMessage) IsClear() bool {
return l.Text == ""
}
// FlashLevel represents flash message severity.
type FlashLevel int
// FlashChan represents a flash event channel.
type FlashChan chan LevelMessage
// FlashListener represents a text model listener.
type FlashListener interface {
// FlashChanged notifies the model changed.
FlashChanged(FlashLevel, string)
// FlashCleared notifies when the filter changed.
FlashCleared()
}
// Flash represents a flash message model.
type Flash struct {
msg LevelMessage
cancel context.CancelFunc
delay time.Duration
msgChan chan LevelMessage
}
// NewFlash returns a new instance.
func NewFlash(dur time.Duration) *Flash {
return &Flash{
delay: dur,
msgChan: make(FlashChan, 3),
}
}
// Channel returns the flash channel.
func (f *Flash) Channel() FlashChan {
return f.msgChan
}
// Info displays an info flash message.
func (f *Flash) Info(msg string) {
f.SetMessage(FlashInfo, msg)
}
// Infof displays a formatted info flash message.
func (f *Flash) Infof(fmat string, args ...interface{}) {
f.Info(fmt.Sprintf(fmat, args...))
}
// Warn displays a warning flash message.
func (f *Flash) Warn(msg string) {
log.Warn().Msg(msg)
f.SetMessage(FlashWarn, msg)
}
// Warnf displays a formatted warning flash message.
func (f *Flash) Warnf(fmat string, args ...interface{}) {
f.Warn(fmt.Sprintf(fmat, args...))
}
// Err displays an error flash message.
func (f *Flash) Err(err error) {
log.Error().Msg(err.Error())
f.SetMessage(FlashErr, err.Error())
}
// Errf displays a formatted error flash message.
func (f *Flash) Errf(fmat string, args ...interface{}) {
var err error
for _, a := range args {
switch e := a.(type) {
case error:
err = e
}
}
log.Error().Err(err).Msgf(fmat, args...)
f.SetMessage(FlashErr, fmt.Sprintf(fmat, args...))
}
// Clear clears the flash message.
func (f *Flash) Clear() {
f.fireCleared()
}
// SetMessage sets the flash level message.
func (f *Flash) SetMessage(level FlashLevel, msg string) {
if f.cancel != nil {
f.cancel()
f.cancel = nil
}
f.setLevelMessage(LevelMessage{Level: level, Text: msg})
f.fireFlashChanged()
var ctx context.Context
ctx, f.cancel = context.WithCancel(context.Background())
go f.refresh(ctx)
}
func (f *Flash) refresh(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-time.After(f.delay):
f.fireCleared()
return
}
}
}
func (f *Flash) setLevelMessage(msg LevelMessage) {
f.msg = msg
}
func (f *Flash) fireFlashChanged() {
f.msgChan <- f.msg
}
func (f *Flash) fireCleared() {
f.msgChan <- newClearMessage()
}

View File

@ -0,0 +1,101 @@
package model_test
import (
"errors"
"fmt"
"sync"
"testing"
"time"
"github.com/derailed/k9s/internal/model"
"github.com/stretchr/testify/assert"
)
func TestFlash(t *testing.T) {
const delay = 1 * time.Millisecond
uu := map[string]struct {
level model.FlashLevel
e string
}{
"info": {level: model.FlashInfo, e: "blee"},
"warn": {level: model.FlashWarn, e: "blee"},
"err": {level: model.FlashErr, e: "blee"},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
f := model.NewFlash(delay)
v := newFlash()
go v.listen(f.Channel())
switch u.level {
case model.FlashInfo:
f.Info(u.e)
case model.FlashWarn:
f.Warn(u.e)
case model.FlashErr:
f.Err(errors.New(u.e))
}
time.Sleep(5 * delay)
s, _, l, m := v.getMetrics()
assert.Equal(t, 1, s)
assert.Equal(t, u.level, l)
assert.Equal(t, u.e, m)
})
}
}
func TestFlashBurst(t *testing.T) {
const delay = 1 * time.Millisecond
f := model.NewFlash(delay)
v := newFlash()
go v.listen(f.Channel())
count := 5
for i := 1; i <= count; i++ {
f.Info(fmt.Sprintf("test-%d", i))
}
time.Sleep(2 * delay)
s, _, l, m := v.getMetrics()
assert.Equal(t, count, s)
assert.Equal(t, model.FlashInfo, l)
assert.Equal(t, fmt.Sprintf("test-%d", count), m)
}
type flash struct {
set, clear int
level model.FlashLevel
msg string
mx sync.RWMutex
}
func newFlash() *flash {
return &flash{}
}
func (f *flash) getMetrics() (int, int, model.FlashLevel, string) {
f.mx.RLock()
defer f.mx.RUnlock()
return f.set, f.clear, f.level, f.msg
}
func (f *flash) listen(c model.FlashChan) {
for m := range c {
f.mx.Lock()
{
if m.IsClear() {
f.clear++
} else {
f.set++
f.level, f.msg = m.Level, m.Text
}
}
f.mx.Unlock()
}
}

View File

@ -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()
{

View File

@ -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()

View File

@ -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) {

160
internal/model/pulse.go Normal file
View File

@ -0,0 +1,160 @@
package model
import (
"context"
"fmt"
"sync/atomic"
"time"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/health"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
)
const defaultRefreshRate = 5 * time.Second
// PulseListener represents a health model listener.
type PulseListener interface {
// PulseChanged notifies the model data changed.
PulseChanged(*health.Check)
// TreeFailed notifies the health check failed.
PulseFailed(error)
}
// Pulse tracks multiple resources health.
type Pulse struct {
gvr string
namespace string
inUpdate int32
listeners []PulseListener
refreshRate time.Duration
health *PulseHealth
data health.Checks
}
// NewPulse returns a new pulse.
func NewPulse(gvr string) *Pulse {
return &Pulse{
gvr: gvr,
refreshRate: defaultRefreshRate,
}
}
// Watch monitors pulses.
func (p *Pulse) Watch(ctx context.Context) {
p.Refresh(ctx)
go p.updater(ctx)
}
func (p *Pulse) updater(ctx context.Context) {
defer log.Debug().Msgf("Pulse canceled -- %q", p.gvr)
rate := initRefreshRate
for {
select {
case <-ctx.Done():
return
case <-time.After(rate):
rate = p.refreshRate
p.refresh(ctx)
}
}
}
// Refresh update the model now.
func (p *Pulse) Refresh(ctx context.Context) {
for _, d := range p.data {
p.firePulseChanged(d)
}
p.refresh(ctx)
}
func (p *Pulse) refresh(ctx context.Context) {
if !atomic.CompareAndSwapInt32(&p.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...")
return
}
defer atomic.StoreInt32(&p.inUpdate, 0)
if err := p.reconcile(ctx); err != nil {
log.Error().Err(err).Msg("Reconcile failed")
p.firePulseFailed(err)
return
}
}
func (p *Pulse) list(ctx context.Context) ([]runtime.Object, error) {
f, ok := ctx.Value(internal.KeyFactory).(dao.Factory)
if !ok {
return nil, fmt.Errorf("expected Factory in context but got %T", ctx.Value(internal.KeyFactory))
}
if p.health == nil {
p.health = NewPulseHealth(f)
}
ctx = context.WithValue(ctx, internal.KeyFields, "")
ctx = context.WithValue(ctx, internal.KeyWithMetrics, false)
return p.health.List(ctx, p.namespace)
}
func (p *Pulse) reconcile(ctx context.Context) error {
oo, err := p.list(ctx)
if err != nil {
return err
}
p.data = health.Checks{}
for _, o := range oo {
c, ok := o.(*health.Check)
if !ok {
return fmt.Errorf("Expecting health check but got %T", o)
}
p.data = append(p.data, c)
p.firePulseChanged(c)
}
return nil
}
// GetNamespace returns the model namespace.
func (p *Pulse) GetNamespace() string {
return p.namespace
}
// SetNamespace sets up model namespace.
func (p *Pulse) SetNamespace(ns string) {
p.namespace = ns
}
// AddListener adds a listener.
func (p *Pulse) AddListener(l PulseListener) {
p.listeners = append(p.listeners, l)
}
// RemoveListener delete a listener.
func (p *Pulse) RemoveListener(l PulseListener) {
victim := -1
for i, lis := range p.listeners {
if lis == l {
victim = i
break
}
}
if victim >= 0 {
p.listeners = append(p.listeners[:victim], p.listeners[victim+1:]...)
}
}
func (p *Pulse) firePulseChanged(check *health.Check) {
for _, l := range p.listeners {
l.PulseChanged(check)
}
}
func (p *Pulse) firePulseFailed(err error) {
for _, l := range p.listeners {
l.PulseFailed(err)
}
}

View File

@ -0,0 +1,117 @@
package model
import (
"context"
"fmt"
"math"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/health"
"github.com/derailed/k9s/internal/render"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime"
)
// PulseHealth tracks resources health.
type PulseHealth struct {
factory dao.Factory
}
// NewPulseHealth returns a new instance.
func NewPulseHealth(f dao.Factory) *PulseHealth {
return &PulseHealth{
factory: f,
}
}
// List returns a canned collection of resources health.
func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, error) {
defer func(t time.Time) {
log.Debug().Msgf("PulseHealthCheck %v", time.Since(t))
}(time.Now())
gvrs := []string{
"v1/pods",
"v1/events",
"apps/v1/replicasets",
"apps/v1/deployments",
"apps/v1/statefulsets",
"apps/v1/daemonsets",
"batch/v1/jobs",
"v1/persistentvolumes",
}
hh := make([]runtime.Object, 0, 10)
for _, gvr := range gvrs {
c, err := h.check(ctx, ns, gvr)
if err != nil {
return nil, err
}
hh = append(hh, c)
}
mm, err := h.checkMetrics()
if err != nil {
return hh, nil
}
for _, m := range mm {
hh = append(hh, m)
}
return hh, nil
}
func (h *PulseHealth) checkMetrics() (health.Checks, error) {
dial := client.DialMetrics(h.factory.Client())
nmx, err := dial.FetchNodesMetrics()
if err != nil {
log.Error().Err(err).Msgf("Fetching metrics")
return nil, err
}
var cpu, mem float64
for _, mx := range nmx.Items {
cpu += float64(mx.Usage.Cpu().MilliValue())
mem += client.ToMB(mx.Usage.Memory().Value())
}
c1 := health.NewCheck("cpu")
c1.Set(health.OK, int(math.Round(cpu)))
c2 := health.NewCheck("mem")
c2.Set(health.OK, int(math.Round(mem)))
return health.Checks{c1, c2}, nil
}
func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, error) {
meta, ok := Registry[gvr]
if !ok {
return nil, fmt.Errorf("No meta for %q", gvr)
}
if meta.DAO == nil {
meta.DAO = &dao.Resource{}
}
meta.DAO.Init(h.factory, client.NewGVR(gvr))
oo, err := meta.DAO.List(ctx, ns)
if err != nil {
return nil, err
}
c := health.NewCheck(gvr)
c.Total(len(oo))
rr, re := make(render.Rows, len(oo)), meta.Renderer
for i, o := range oo {
if err := re.Render(o, ns, &rr[i]); err != nil {
return nil, err
}
if !render.Happy(ns, rr[i]) {
c.Inc(health.Toast)
} else {
c.Inc(health.OK)
}
}
return c, nil
}

Some files were not shown because too many files have changed in this diff Show More