diff --git a/.golangci.yml b/.golangci.yml index fb6b49e8..33fcb57f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -79,7 +79,7 @@ linters-settings: # exclude: /path/to/file.txt funlen: - lines: 65 + lines: 75 statements: 40 govet: @@ -114,7 +114,7 @@ linters-settings: local-prefixes: github.com/org/project gocyclo: # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 15 + min-complexity: 20 gocognit: # minimal code complexity to report, 30 by default (but we recommend 10-20) min-complexity: 20 diff --git a/.goreleaser.yml b/.goreleaser.yml index f5511c2c..fd2046c0 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -13,12 +13,14 @@ builds: - darwin - windows goarch: - - 386 + # - 386 - amd64 - arm64 - arm goarm: - 7 + flags: + - -trimpath 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}} archives: @@ -30,7 +32,7 @@ archives: bit: Arm bitv6: Arm6 bitv7: Arm7 - 386: i386 + # 386: i386 amd64: x86_64 checksum: name_template: "checksums.txt" @@ -48,12 +50,12 @@ brews: - name: k9s github: owner: derailed - name: k9s-homebrew-tap + name: homebrew-k9s commit_author: name: derailed email: fernand@imhotep.io folder: Formula - homepage: https://k8sk9s.dev/ + homepage: https://k9scli.io/ description: Kubernetes CLI To Manage Your Clusters In Style! test: | system "k9s version" diff --git a/Dockerfile b/Dockerfile index 75fa1da8..fa8c4186 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build... -FROM golang:1.13.6-alpine3.11 AS build +FROM golang:1.14.1-alpine3.11 AS build WORKDIR /k9s COPY go.mod go.sum main.go Makefile ./ @@ -13,7 +13,7 @@ RUN apk --no-cache add make git gcc libc-dev curl && make build FROM alpine:3.10.0 COPY --from=build /k9s/execs/k9s /bin/k9s -ENV KUBE_LATEST_VERSION="v1.16.2" +ENV KUBE_LATEST_VERSION="v1.18.1" RUN apk add --update ca-certificates \ && apk add --update -t deps curl \ && curl -L https://storage.googleapis.com/kubernetes-release/release/${KUBE_LATEST_VERSION}/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl \ diff --git a/Makefile b/Makefile index 189c8a55..ab93a4ed 100644 --- a/Makefile +++ b/Makefile @@ -3,26 +3,26 @@ PACKAGE := github.com/derailed/$(NAME) GIT := $(shell git rev-parse --short HEAD) SOURCE_DATE_EPOCH ?= $(shell date +%s) DATE := $(shell date -u -d @${SOURCE_DATE_EPOCH} +%FT%T%Z) -VERSION ?= v0.17.6 +VERSION ?= v0.19.1 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} default: help -test: ## Run all tests +test: ## Run all tests @go clean --testcache && go test ./... -cover: ## Run test coverage suite +cover: ## Run test coverage suite @go test ./... --coverprofile=cov.out @go tool cover --html=cov.out -build: ## Builds the CLI +build: ## Builds the CLI @go build \ -ldflags "-w -s -X ${PACKAGE}/cmd.version=${VERSION} -X ${PACKAGE}/cmd.commit=${GIT} -X ${PACKAGE}/cmd.date=${DATE}" \ -a -tags netgo -o execs/${NAME} main.go -img: ## Build Docker Image +img: ## Build Docker Image @docker build --rm -t ${IMAGE} . help: diff --git a/README.md b/README.md index f4f4fcb1..b0177c21 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,79 @@ K9s is available on Linux, macOS and Windows platforms. export TERM=xterm-256color ``` +* In order to issue manifest edit commands make sure your EDITOR env is set. + + ```shell + export EDITOR=my_fav_editor_here! + ``` + +--- + +## The Command Line + +```shell +# List all available CLI options +k9s help +# To get info about K9s runtime (logs, configs, etc..) +k9s info +# To run K9s in a given namespace +k9s -n mycoolns +# Start K9s in an existing KubeConfig context +k9s --context coolCtx +# Start K9s in readonly mode - with all modification commands disabled +k9s --readonly +``` + +## Logs + +Given the nature of the ui k9s does produce logs to a specific location. To view the logs and turn on debug mode, use the following commands: + +```shell +k9s info +# Will produces something like this +# ____ __.________ +# | |/ _/ __ \______ +# | < \____ / ___/ +# | | \ / /\___ \ +# |____|__ \ /____//____ > +# \/ \/ +# +# Configuration: /Users/fernand/.k9s/config.yml +# Logs: /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-fernand.log +# Screen Dumps: /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-screens-fernand + +# To view k9s logs +tail -f /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-fernand.log + +# Start K9s in debug mode +k9s -l debug +``` + +## Key Bindings + +K9s uses aliases to navigate most K8s resources. + +| Action | Command | Comment | +|---------------------------------------------------------------|-----------------------|-------------------------------------------------------------| +| Show active keyboard mnemonics and help | `?` | | +| Show all available resource alias | `ctrl-a` | | +| To bail out of K9s | `:q`, `ctrl-c` | | +| View a Kubernetes resource using singular/plural or shortname | `:`po⏎ | accepts singular, plural, shortname or alias ie pod or pods | +| View a Kubernetes resource in a given namespace | `:`alias namespace⏎ | | +| Filter out a resource view given a filter | `/`filter⏎ | | +| Filter resource view by labels | `/`-l label-selector⏎ | | +| Fuzzy find a resource given a filter | `/`-f filter⏎ | | +| Bails out of view/command/filter mode | `` | | +| Key mapping to describe, view, edit, view logs,... | `d`,`v`, `e`, `l`,... | | +| To view and switch to another Kubernetes context | `:`ctx⏎ | | +| To view and switch to another Kubernetes context | `:`ctx context-name⏎ | | +| To view and switch to another Kubernetes namespace | `:`ns⏎ | | +| To view all saved resources | `:`screendump or sd⏎ | | +| To delete a resource (TAB and ENTER to confirm) | `ctrl-d` | | +| To kill a resource (no confirmation dialog!) | `ctrl-k` | | +| Launch pulses view | `:`pulses or pu⏎ | | +| Launch XRay view | `:`xray RESOURCE [NAMESPACE]⏎ | RESOURCE can be one of po, svc, dp, rs, sts, ds, NAMESPACE is optional | + --- ## Screenshots @@ -129,6 +202,7 @@ K9s is available on Linux, macOS and Windows platforms. ## Demo Videos/Recordings +* [K9s v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw) * [K9s v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be) * [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN) * [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM) @@ -139,46 +213,6 @@ K9s is available on Linux, macOS and Windows platforms. --- -## The Command Line - -```shell -# List all available CLI options -k9s help -# To get info about K9s runtime (logs, configs, etc..) -k9s info -# To run K9s in a given namespace -k9s -n mycoolns -# Start K9s in an existing KubeConfig context -k9s --context coolCtx -# Start K9s in readonly mode - with all modification commands disabled -k9s --readonly -``` - -## Key Bindings - -K9s uses aliases to navigate most K8s resources. - -| Command | Result | Example | -|-----------------------------|----------------------------------------------------|----------------------------| -| `:dp`, `:deploy` | View deployments | | -| `:no`, `:nodes` | View nodes | | -| `:svc`, `:service` | View services | | -| `:`alias`` | View a Kubernetes resource aliases | `:po` | -| `?` | Show keyboard shortcuts and help | | -| `Ctrl-a` | Show all available resource alias | select+`` to view | -| `/`filter`ENTER` | Filter out a resource view given a filter | `/bumblebeetuna` | -| `/`-l label-selector`ENTER` | Filter resource view by labels | `/-l app=fred` | -| `` | Bails out of view/command/filter mode | | -| `d`,`v`, `e`, `l`,... | Key mapping to describe, view, edit, view logs,... | `d` (describes a resource) | -| `:`ctx`` | To view and switch to another Kubernetes context | `:`+`ctx`+`` | -| `:`ns`` | To view and switch to another Kubernetes namespace | `:`+`ns`+`` | -| `:screendump`, `:sd` | To view all saved resources | | -| `Ctrl-d` | To delete a resource (TAB and ENTER to confirm) | | -| `Ctrl-k` | To kill a resource (no confirmation dialog!) | | -| `:q`, `Ctrl-c` | To bail out of K9s | | - ---- - ## K9s Configuration K9s keeps its configurations in a .k9s directory in your home directory `$HOME/.k9s/config.yml`. @@ -192,10 +226,14 @@ K9s uses aliases to navigate most K8s resources. refreshRate: 2 # Indicates whether modification commands like delete/kill/edit are disabled. Default is false readOnly: false - # Indicates log view maximum buffer size. Default 1k lines. - logBufferSize: 200 - # Indicates how many lines of logs to retrieve from the api-server. Default 200 lines. - logRequestSize: 200 + # Logs configuration + logger: + # Defines the number of lines to return. Default 100 + tail: 200 + # Defines the total number of log lines to allow in the view. Default 1000 + buffer: 500 + # Represents how far to go back in the log timeline in seconds. Default is 5min + sinceSeconds: 300 # Indicates the current kube context. Defaults to current context currentContext: minikube # Indicates the current kube cluster. Defaults to current context cluster @@ -275,17 +313,42 @@ Entering the command mode and typing a resource name or alias, could be cumberso ## Plugins -K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins. K9s looks at `$HOME/.k9s/plugin.yml` to locate all 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: + +* Shortcut option represents the key combination a user would type to activate the plugin +* Description will be printed next to the shortcut in the k9s menu +* Scopes defines a collection of resources names/shortnames for the views associated with the plugin. You can specify `all` to provide this shortcut for all views. +* Command represents adhoc commands the plugin runs upon activation +* Background specifies whether or not the command runs in the background +* Args specifies the various arguments that should apply to the command above + +K9s does provide additional environment variables for you to customize your plugins arguments. Currently, the available environment variables are as follows: + +* `$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 +* `$USER` the active user +* `$GROUPS` the active groups +* `$POD` while in a container view +* `$COL-` use a given column name for a viewed resource. Must be prefixed by `COL-`! + +### Example + +This defines a plugin for viewing logs on a selected pod using `ctrl-l` for shorcut. ```yaml # $HOME/.k9s/plugin.yml plugin: - # Defines a plugin to provide a `Ctrl-l` shortcut to tail the logs while in pod view. + # Defines a plugin to provide a `ctrl-l` shorcut to tail the logs while in pod view. fred: shortCut: Ctrl-L description: Pod logs scopes: - - po + - pods command: kubectl background: false args: @@ -298,28 +361,6 @@ plugin: - $CONTEXT ``` -This defines a plugin for viewing logs on a selected pod using `Ctrl-l` mnemonic while in the pods view. - -* Shortcut: the key a user must enter to activate the plugin. -* Command: the shell commands the plugin runs upon activation. -* Scopes: select the resources that can access the plugin command. Defines a collection of resource names/shortnames for which the plugin shortcut will be made available to the user. You can specify `all` to make a plugin available in all views. -* Background: boolean to indicate whether to run the command in the background or not. -* Description: a short description of the command that will be shown in the ui next to the action mnemonic. -* Args: a collection of arguments for the given command. - -K9s does provide additional environment variables for you to further customize your plugins. Currently, the available environment variables are as follows: - -* `$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 -* `$USER` the active user -* `$GROUPS` the active groups -* `$COL-` use a given column name for a viewed resource. Must be prefixed by `COL-`! - > NOTE: This is an experimental feature! Options and layout may change in future K9s releases as this feature solidifies. --- diff --git a/assets/beach.png b/assets/beach.png deleted file mode 100644 index 986e6dcb..00000000 Binary files a/assets/beach.png and /dev/null differ diff --git a/assets/k9s_popeye.png b/assets/k9s_popeye.png new file mode 100644 index 00000000..4b64ba9c Binary files /dev/null and b/assets/k9s_popeye.png differ diff --git a/assets/popeye/report.png b/assets/popeye/report.png new file mode 100644 index 00000000..0f30fa75 Binary files /dev/null and b/assets/popeye/report.png differ diff --git a/assets/popeye/sanitizers.png b/assets/popeye/sanitizers.png new file mode 100644 index 00000000..ff0175df Binary files /dev/null and b/assets/popeye/sanitizers.png differ diff --git a/assets/shirts/k9s_back.png b/assets/shirts/k9s_back.png new file mode 100644 index 00000000..125cfcfb Binary files /dev/null and b/assets/shirts/k9s_back.png differ diff --git a/assets/shirts/k9s_front.png b/assets/shirts/k9s_front.png new file mode 100644 index 00000000..eba51d2e Binary files /dev/null and b/assets/shirts/k9s_front.png differ diff --git a/change_logs/release_v0.18.0.md b/change_logs/release_v0.18.0.md new file mode 100644 index 00000000..652f0f10 --- /dev/null +++ b/change_logs/release_v0.18.0.md @@ -0,0 +1,69 @@ + + +# Release v0.18.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 consider sponsoring 👆us or 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) + +--- + +## GH Sponsors + +Big `ThankYou` to the following folks that I've decided to dig in and give back!! 👏🙏🎊 +Thank you for your gesture of kindness and for supporting K9s!! + +* [Bob Johnson](https://github.com/bbobjohnson) +* [Poundex](https://github.com/Poundex) +* [thllxb](https://github.com/thllxb) + +If you've contributed $25 or more please reach out to me on slack with your earth coordinates so I can send you your K9s swags! + + + + +NOTE: I am one not to pressure folks into giving. However, it does make me sad to see postings out there with clear indications that K9s is being used and yet zero mentions of the web site nor this repo. K9s marketing budget relies entirely on word of mouth and is not pimped out by big corps. So if you publish your work and leverage K9s, please give us a shoutout or at the very least reference this repo or website! + +--- + +## AutoSuggestions + +K9s command mode now provides for auto complete. Suggestions are pulled from available kubernetes resources and custom aliases. The command mode supports the following keyboard triggers: + +| Key | Description | +|---------------------|------------------------------------------| +| ⬆️ ⬇️ | Navigate up or down thru the suggestions | +| `Ctrl-w`, `Ctrl-u` | Clear out the command | +| `Tab`, `Ctrl-f`, ➡️ | Accept the suggestion | + +## Logs Revisited + +Breaking Change! This drop changes how logs are viewed and configured. The log view now support for pulling logs based on the log timeline current settings are: all, 1m, 5m, 15m and 1h. The following log configuration is in effect as of this drop: + +```yaml +# $HOME/.k9s/config.yml +k9s: + refreshRate: 2 + readOnly: false + # NOTE: New logger configuration! + logger: + tail: 200 # Tail the last 100 lines. Default 100 + buffer: 5000 # Max number of lines displayed in the view. Default 1000 + sinceSeconds: 900 # Displays the last x seconds from the logs timeline. Default 5m + ... +``` + +## Resolved Bugs/Features/PRs + +* [Issue #628](https://github.com/derailed/k9s/issues/628) +* [Issue #623](https://github.com/derailed/k9s/issues/623) +* [Issue #622](https://github.com/derailed/k9s/issues/622) +* [Issue #565](https://github.com/derailed/k9s/issues/565) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.18.1.md b/change_logs/release_v0.18.1.md new file mode 100644 index 00000000..e536f3f3 --- /dev/null +++ b/change_logs/release_v0.18.1.md @@ -0,0 +1,24 @@ + + +# Release v0.18.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 consider sponsoring 👆us or 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 #632](https://github.com/derailed/k9s/issues/632) +* [Issue #631](https://github.com/derailed/k9s/issues/631) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.19.0.md b/change_logs/release_v0.19.0.md new file mode 100644 index 00000000..7b4fcae1 --- /dev/null +++ b/change_logs/release_v0.19.0.md @@ -0,0 +1,73 @@ + + +# Release v0.19.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 consider sponsoring 👆us or 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) + +--- + +## A Word From Our Sponsors... + +It makes me always very happy to hear folks are digging this effort and using K9s daily! If you feel this way please tell us and consider joining our [sponsorship](https://github.com/sponsors/derailed) program. + +Big Thank You! to [hornbech](https://github.com/hornbech) for joining our sponsors! + +## K8s v1.18.0 Support + +As you might have heard, the good Kubernetes folks just dropped some big features in this new release. ATTA Girls/Boys!! We've (painfully) updated K9s to now link with the latest and greatest apis. Likely more work will need to take place here as I am still trying to catch up with the latest enhancements. This is great to see and excellent for all our Kubernetes friends! + +## Oh Biffs'em And Buffs'em Popeye! + + + +As you may know, I am the author of [Popeye](https://popeyecli.io) a Kubernetes cluster linter/sanitizer. Popeye scans your clusters live and reports potential issues, things like: referential integrity, misconfiguration, resource usage, etc... +In this drop, we've integrated K9s and Popeye to produce what I believe is a killer combo. Not only can you manage/observe your cluster resources in the wild, but you can now assert that your resources are indeed cool and potentially get ride of dead weights that might add up to your monthly cloud service bills. How cool is that? + +In order to run your sanitization and produce reports, you can enter a new command `:popeye`. Once your cluster sanitization is complete, you can use familiar keyboard shortcuts to sort columms and view the sanitization reports by pressing `enter` on a given resource linter. Popeye also supports a configuration file namely `spinach.yml`, this file provides for customizing what resources get scanned as well as setting different severity levels to your own company policies. Please read the Popeye docs on how to customize your reports. The spinach.yml file will be read from K9s home directory `$HOME/.k9s/MY_CLUSTER_CONTEXT_NAME_spinach.yml` + +NOTE! This is very much still experimental, so you may experience some disturbances in the force! And remember PRs are always open ;) + + + + +## Command History Support + +K9s now supports for command history. Entering command mode via `:` you can now up/down arrow to navigate thru your command history. Pression `tab` or `ctrl-e` or `->` will activate the selected command upon `enter`. + +## K9s Icons + +Some terminals often don't offer icon support. In this release there is a new option `noIcons` available to enable/disable K9s icons. By default this option is set `false`. You can now set your icon preference in the K9s config file as follows: + +```yaml +# $HOME/.k9s/config.yml +k9s: + refreshRate: 2 + headless: false + readOnly: false + noIcons: true # Enable/Disable K9s icons display. +``` + +## Videos! + +* [video v0.19.0](https://www.youtube.com/watch?v=kj-WverKZ24) +* [video v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw) + +## Resolved Bugs/Features/PRs + +* [Issue #647](https://github.com/derailed/k9s/issues/647) +* [Issue #645](https://github.com/derailed/k9s/issues/645) +* [Issue #640](https://github.com/derailed/k9s/issues/640) +* [Issue #639](https://github.com/derailed/k9s/issues/639) +* [Issue #635](https://github.com/derailed/k9s/issues/635) +* [Issue #634](https://github.com/derailed/k9s/issues/634) Thank you!! [David Němec](https://github.com/davidnemec) +* [Issue #626](https://github.com/derailed/k9s/issues/626) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.19.1.md b/change_logs/release_v0.19.1.md new file mode 100644 index 00000000..87245801 --- /dev/null +++ b/change_logs/release_v0.19.1.md @@ -0,0 +1,30 @@ + + +# Release v0.19.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 consider sponsoring 👆us or 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) + +--- + +## A Word From Our Sponsors... + +It makes me always very happy to hear folks are digging this effort and using K9s daily! If you feel this way please tell us and consider joining our [sponsorship](https://github.com/sponsors/derailed) program. + +Big Thank You! to [Azar](https://github.com/azarudeena) for joining our sponsors! + +Maintenance Release! + +## Resolved Bugs/Features/PRs + +* [Issue #649](https://github.com/derailed/k9s/issues/649) +* [PR #638](https://github.com/derailed/k9s/pull/638) Thank you! [Shang Yuanchun](https://github.com/ideal) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.19.2.md b/change_logs/release_v0.19.2.md new file mode 100644 index 00000000..0da7e168 --- /dev/null +++ b/change_logs/release_v0.19.2.md @@ -0,0 +1,36 @@ + + +# Release v0.19.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 consider sponsoring 👆us or 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) + +--- + +## A Word From Our Sponsors... + +It makes me always very happy to hear folks are digging this effort and using K9s daily! If you feel this way please tell us and consider joining our [sponsorship](https://github.com/sponsors/derailed) program. + +Big Thank You! to the following folks for joining our program: + +* [Nick Hobart](https://github.com/nwhobart) +* [Shopeonarope](https://github.com/shopeonarope) + +Maintenance Release! + +NOTE! During K9s update to support the latest version of Kubernetes (v1.18), K9s Helm charts support took one for the team ;( At this time Helm as yet to be released k8s v1.18 support. We will track for updates and enable this feature once HelmV3 releases it. + +## Resolved Bugs/Features/PRs + +* [Issue #665](https://github.com/derailed/k9s/issues/665) +* [Issue #662](https://github.com/derailed/k9s/issues/662) +* [PR #660](https://github.com/derailed/k9s/pull/660) Thank you! [Tomáš Pospíšek](https://github.com/tpo) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.19.3.md b/change_logs/release_v0.19.3.md new file mode 100644 index 00000000..dfd85d8f --- /dev/null +++ b/change_logs/release_v0.19.3.md @@ -0,0 +1,70 @@ + + +# Release v0.19.3 + +## 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, consider joining our [sponsorhip program](https://github.com/sponsors/derailed) and/or 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) + +--- + +## Look Who Is Back? + +Thanks to the good Helm folks, we're now back on par with the Helm charts support feature. As you may recall, when we've updated to K8s v1.18, the Helm feature took one for the team ;( as they had yet to upgrade to the latest k8s rev. So K9s Helm chart feature is back in this drop! On that note, we've added new aliases to allow you to view your currently installed Helm charts aka `helm` | `hm` | `chart` | `charts`. + +## Boh-Bye Windows 386! + +As of this drop, I've decided to axe Windows 386 support. Our good friend [Guy Barrette](https://github.com/guybarrette) reported K9s Windows-386 binary is triping his virus scanner. After double checking my installed shas/binaries/dependencies/etc... and performing vulnerabily scans on various win-i386 K9s binaries, I just could not figure out which dependencies are causing the exec to bomb on the scans?? + +Note: This does not necessary entails that there is a deliberate or malicious intent with the software, but likely a false positive thrown by the Windows virus scanner. This has been [reported](https://golang.org/doc/faq#virus) with other GO binaries on windows as well ;( + +That said, I've repeatdly scanned the K9s Windows-x64 and ended up with a clean bill of health on every single scans. So I've decided to drop the 386 windows support for the time being. If that causes you some grief, please land a hand as I am fresh out of ideas... + +## And Now For Something A Bit More... Controversial? + +There has been a lot of requests for K9s to support shelling directly into cluster nodes. I was resisting the temptation to support this useful feature as depending on your cluster hosting solution, this involved less than ideal solutions. My clusters are provisioned in a multitude of platforms ranging from bare metal to cloud vendor self/managed hosting. I wanted the same experience shelling into an GKE/AWS node as a local KiND cluster node. To this end, we've opted to experimentally support shelling into nodes using the following approach: + +1. While in the Node view, we are introducing a new `s` mnemonic to shell into nodes on your cluster. +2. K9s will spin up a `k9s-shell` pod in the `default` namespace with an official Busybox container running in `privileged` mode. This may require extra RBAC and PSPs (This will need Docs!) +3. Once shelled-in, you can poke around any of your nodes. +4. Upon exiting the node shell, K9s will automatically delete the `k9s-shell` pod for that node. + +This feature is `OPT-IN` only ie you will need to manually enable the feature gate to make this functionality available to K9s on a specific cluster as follows: + +```yaml +# $HOME/.k9s/config.yml +k9s: + ... + clusters: + fred: + namespace: + active: "default" + favorites: + - default + view: + active: po + featureGates: + nodeShell: true # Defaults to false! +``` + +Please let us know if you dig this feature? This very much experimental and we're open to your suggestions. Thank you! + +## New Sheriff In Town K9S_EDITOR + +As you may know K9s currently uses your `EDITOR` env var to launch an editor while editing a k8s resource or viewing a screen dump or a performance benchmark. So folks voiced they are using some editors that require different CLI args when editing k8s resources vs files on disk. In this drop, we're introducing a new env var `K9S_EDITOR` to provide an affordance to deal with these discrepancies. If you are using emacs/vi/nano no action should be required. K9s will now check for `K9S_EDITOR` existence to view K9s artifacts such as screen_dumps. K9s still honors `KUBE_EDITOR` or `EDITOR` for K8s resource edits. K9s will fallback to the `EDITOR` env var if `K9S_EDITOR` is not set. + +## Resolved Bugs/Features/PRs + +* [Issue #669](https://github.com/derailed/k9s/issues/669) +* [Issue #677](https://github.com/derailed/k9s/issues/677) +* [Issue #673](https://github.com/derailed/k9s/issues/673) +* [Issue #671](https://github.com/derailed/k9s/issues/671) +* [Issue #670](https://github.com/derailed/k9s/issues/670) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/cmd/root.go b/cmd/root.go index 8a1045a6..35f185a8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -74,7 +74,7 @@ func run(cmd *cobra.Command, args []string) { log.Error().Msg(string(debug.Stack())) printLogo(color.Red) fmt.Printf("%s", color.Colorize("Boom!! ", color.Red)) - fmt.Println(color.Colorize(fmt.Sprintf("%v.", err), color.White)) + fmt.Println(color.Colorize(fmt.Sprintf("%v.", err), color.LightGray)) } }() diff --git a/cmd/version.go b/cmd/version.go index 63a01156..22ce0bf8 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -41,7 +41,7 @@ func printVersion(short bool) { func printTuple(fmat, section, value string, outputColor color.Paint) { if outputColor != -1 { - fmt.Printf(fmat, color.Colorize(section+":", outputColor), color.Colorize(value, color.White)) + fmt.Printf(fmat, color.Colorize(section+":", outputColor), color.Colorize(value, color.LightGray)) return } fmt.Printf(fmat, section, value) diff --git a/go.mod b/go.mod index 64a6a316..e9961927 100644 --- a/go.mod +++ b/go.mod @@ -2,65 +2,37 @@ module github.com/derailed/k9s go 1.13 -replace ( - github.com/docker/docker => github.com/docker/docker v1.4.2-0.20181221150755-2cb26cfe9cbf - k8s.io/api => k8s.io/api v0.0.0-20190918155943-95b840bb6a1f - k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 - k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655 - k8s.io/apiserver => k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad - k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20190918162238-f783a3654da8 - k8s.io/client-go => k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90 - k8s.io/cloud-provider => k8s.io/cloud-provider v0.0.0-20190918163234-a9c1f33e9fb9 - k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.0.0-20190918163108-da9fdfce26bb - k8s.io/code-generator => k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269 - k8s.io/component-base => k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090 - k8s.io/cri-api => k8s.io/cri-api v0.0.0-20190828162817-608eb1dad4ac - k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.0.0-20190918163402-db86a8c7bb21 - k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.0.0-20190918161219-8c8f079fddc3 - k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.0.0-20190918162944-7a93a0ddadd8 - k8s.io/kube-proxy => k8s.io/kube-proxy v0.0.0-20190918162534-de037b596c1e - k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.0.0-20190918162820-3b5c1246eb18 - k8s.io/kubectl => k8s.io/kubectl v0.0.0-20190918164019-21692a0861df - k8s.io/kubelet => k8s.io/kubelet v0.0.0-20190918162654-250a1838aa2c - k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.0.0-20190918163543-cfa506e53441 - k8s.io/metrics => k8s.io/metrics v0.0.0-20190918162108-227c654b2546 - k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.0.0-20190918161442-d4c9c65c82af -) - require ( - github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/atotto/clipboard v0.1.2 - github.com/derailed/tview v0.3.7 + github.com/derailed/popeye v0.8.1 + github.com/derailed/tview v0.3.10 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/fatih/color v1.9.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/mattn/go-runewidth v0.0.8 + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mattn/go-runewidth v0.0.9 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/petergtz/pegomock v2.7.0+incompatible + github.com/rakyll/hey v0.1.3 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 + github.com/spf13/cobra v1.0.0 + github.com/stretchr/testify v1.5.1 golang.org/x/text v0.3.2 - gopkg.in/yaml.v2 v2.2.4 - helm.sh/helm/v3 v3.0.2 - k8s.io/api v0.0.0 - k8s.io/apimachinery v0.0.0 - k8s.io/cli-runtime v0.0.0 - k8s.io/client-go v0.0.0 + gopkg.in/yaml.v2 v2.2.8 + helm.sh/helm/v3 v3.2.0 + k8s.io/api v0.18.2 + k8s.io/apimachinery v0.18.2 + k8s.io/cli-runtime v0.18.2 + k8s.io/client-go v0.18.2 k8s.io/klog v1.0.0 - k8s.io/kubectl v0.0.0 - k8s.io/kubernetes v1.16.3 - k8s.io/metrics v0.0.0 - sigs.k8s.io/yaml v1.1.0 + k8s.io/kubectl v0.18.2 + k8s.io/metrics v0.18.2 + sigs.k8s.io/yaml v1.2.0 vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 ) diff --git a/go.sum b/go.sum index 9697d1f9..144800ea 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,57 @@ -bitbucket.org/bertimus9/systemstat v0.0.0-20180207000608-0eeff89b0690/go.mod h1:Ulb78X89vxKYgdL24HMTiXYHlyHEvruOj1ZPlqeNEZM= +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 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= -github.com/Azure/azure-sdk-for-go v32.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v16.2.1+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= -github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= +github.com/Azure/go-autorest v10.8.1+incompatible h1:u0jVQf+a6k6x8A+sT60l6EY9XZu+kHdnZVPAYqpVRo0= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.0.0+incompatible h1:r/ug62X9o8vikt53/nkAPmFmzfSrCCAplPH7wa+mK0U= +github.com/Azure/go-autorest v14.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest/adal v0.5.0 h1:q2gDruN08/guU9vAjuPWff0+QIrpH6ediguzdAzXAUU= +github.com/Azure/go-autorest/autorest v0.10.0 h1:mvdtztBqcL8se7MdrUweNieTNi4kfNG6GOJuurQJpuY= +github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/date v0.1.0 h1:YGrhWfrgtFs84+h0o46rJrlmsZtyZRg470CqAXTZaGM= +github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0= +github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= -github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= +github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/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/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= 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= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14= github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= +github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.0.2 h1:wz22D0CiSctrliXiI9ZO3HoNApweeRGftyDN+BQa3B8= github.com/Masterminds/sprig/v3 v3.0.2/go.mod h1:oesJ8kPONMONaZgtiHNzUShJbksypC5kWczhZAf6+aU= -github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -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/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Masterminds/sprig/v3 v3.1.0 h1:j7GpgZ7PdFqNsmncycTHsLmVPf5/3wJtlgW9TNDYD9Y= +github.com/Masterminds/sprig/v3 v3.1.0/go.mod h1:ONGMf7UfYGAbMXCZmQLy8x3lCDIPrEZE/rU8pmrbihA= +github.com/Masterminds/squirrel v1.2.0 h1:K1NhbTO21BWG47IVR0OnIZuE0LZcXAYqywrC3Ko53KI= +github.com/Masterminds/squirrel v1.2.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= +github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= @@ -50,70 +59,71 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 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/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= 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/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM= -github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/bazelbuild/bazel-gazelle v0.0.0-20181012220611-c728ce9f663e/go.mod h1:uHBSeeATKpVazAACZBDPL/Nk/UhQDDsJWDlqYJo8/Us= -github.com/bazelbuild/buildtools v0.0.0-20180226164855-80c7f0d45d7e/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.29.32 h1:o4I8Qc+h9ht8NXvTHeXZH3EmtSUZ/PC0bg9Wawr+aTA= +github.com/aws/aws-sdk-go v1.29.32/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= 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/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/bugsnag/bugsnag-go v1.5.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -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= -github.com/cespare/prettybench v0.0.0-20150116022406-03b8cfe5406c/go.mod h1:Xe6ZsFhtM8HrDku0pxJ3/Lr51rwykrzgFwpmTzleatY= -github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA= -github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= -github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho= -github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/cfssl v0.0.0-20180726162950-56268a613adf/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= -github.com/clusterhq/flocker-go v0.0.0-20160920122132-2b8b7259d313/go.mod h1:P1wt9Z3DP8O6W3rvwCt0REIlshg1InHImaLW0t3ObY0= -github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= -github.com/container-storage-interface/spec v1.1.0/go.mod h1:6URME8mwIBbpVyZV93Ce5St17xBiQJQY67NDsuohiy4= -github.com/containerd/console v0.0.0-20170925154832-84eeaae905fa/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/containerd v1.0.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190823190603-4a2f61c4f2b4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0 h1:xjvXQWABwS2uiv3TWgQt5Uth60Gu86LTGZXMJkjc7rY= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/coredns/corefile-migration v1.0.2/go.mod h1:OFwBp/Wc9dJt5cAZzHWMNhK1r5L0p0jDwIBc6j8NC8E= -github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2 h1:ForxmXkA6tPIvffbrDAcPUIB32QgXkt2XFj+F0UxetA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.15+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/rkt v1.30.0/go.mod h1:O634mlH6U7qk87poQifK6M2rsFNt+FyUTWNMnP1hF1U= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -121,47 +131,51 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/derailed/tview v0.3.7 h1:q0eYai9blR6wAWz/+lo2Knacl/Pnv9YSfI4aYme1aok= -github.com/derailed/tview v0.3.7/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII= +github.com/deislabs/oras v0.8.1 h1:If674KraJVpujYR00rzdi0QAmW4BxzMJPVAZJKuhQ0c= +github.com/deislabs/oras v0.8.1/go.mod h1:Mx0rMSbBNaNfY9hjpccEnxkOqJL6KGjtxNHPLC4G4As= +github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/derailed/popeye v0.8.1 h1:N69XH0NZTBkrNj8qvUzy6Z6bP7+jx0AwollETqvc3dc= +github.com/derailed/popeye v0.8.1/go.mod h1:OBHcJDa50VpE9QNyOU243bNOtHb29MyLlVHJolwlwas= +github.com/derailed/tview v0.3.10 h1:n+iQwYh9Ff9STdR5hBhp+rTJRlu59q2xP2pHvwQbYPw= +github.com/derailed/tview v0.3.10/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d h1:qdD+BtyCE1XXpDyhvn0yZVcZOLILdj9Cw4pKu0kQbPQ= -github.com/docker/cli v0.0.0-20190506213505-d88565df0c2d/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492 h1:FwssHbCDJD025h+BchanCwE1Q8fyMgqDr2mOQAWOLGw= +github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= 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 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= -github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= -github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce h1:KXS1Jg+ddGcWA8e1N7cupxaHHZhit5rB9tfDU+mfjyY= +github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA= -github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 h1:yWHOI+vFjEsAakUTSrtqc/SAHrhSkmn48pqjidZX3QA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/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 v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.11.1+incompatible h1:CjKsv3uWcCMvySPQYKxO8XX3f9zD4FeZRsW4G0B4ffE= -github.com/emicklei/go-restful v2.11.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/euank/go-kmsg-parser v2.0.0+incompatible/go.mod h1:MhmAMZ8V4CYH4ybgdRwPr2TU5ThnS43puaKEMpja1uw= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -169,25 +183,23 @@ 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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 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/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/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= github.com/gdamore/tcell v1.1.2/go.mod h1:h3kq4HO9l2On+V9ed8w8ewqQEmGCSSHOgQ+2h8uzurE= github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 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-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 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= @@ -197,6 +209,7 @@ github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70t github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= @@ -216,17 +229,20 @@ github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= -github.com/go-openapi/spec v0.19.4 h1:ixzUSnHTd6hCemgtAJgluaTSGYpLNpJY4mA2DIkdOAo= -github.com/go-openapi/spec v0.19.4/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= @@ -235,22 +251,30 @@ github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tF github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= -github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= 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/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -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= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 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-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/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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -264,97 +288,112 @@ github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA// github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/cadvisor v0.34.0/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48= -github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= -github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.3.1 h1:WeAefnSUHlBb0iJKwxFDZdbfGwkd7xRNuV+IpXMJhYk= -github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/googleapis/gnostic v0.1.0 h1:rVsPeBmXbYv4If/cumu1AzZPwV58q433hvONV1UEZoI= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 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/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/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/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gosuri/uitable v0.0.1 h1:M9sMNgSZPyAu1FJZJLpJ16ofL8q5ko2EDUkICsynvlY= -github.com/gosuri/uitable v0.0.1/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc h1:f8eY6cV/x1x+HLjOp4r72s/31/V2aTUtg5oKRRPf8/Q= -github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= -github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/heketi/heketi v9.0.0+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o= -github.com/heketi/rest v0.0.0-20180404230133-aa6a65207413/go.mod h1:BeS3M108VzVlmAue3lv2WcGuPAX94/KN63MUURzbYSI= -github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4= -github.com/heketi/utils v0.0.0-20170317161834-435bc5bdfa64/go.mod h1:RYlF4ghFZPPmk2TC5REt5OFwvfb6lzxFWrTWB+qs28s= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 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/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 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= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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/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= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= -github.com/lpabon/godbc v0.1.1/go.mod h1:Jo9QV0cf3U6jZABgiJ2skINAXb9j8m51r07g4KI92ZA= -github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04= -github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk= -github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao= -github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58= github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -363,25 +402,27 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 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/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= 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-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.5/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= -github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV59fDr4= -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/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/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 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= @@ -389,6 +430,7 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk 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= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -398,33 +440,37 @@ github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lN github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.0.0-20160930181131-4ee1cc9a8058/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 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/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= 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/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/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= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc2.0.20190611121236-6cc515888830 h1:yvQ/2Pupw60ON8TYEIGGTAI77yZsWYkiOeHFZWkwlCk= -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/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= 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= @@ -432,174 +478,205 @@ github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c/go.mod h1:u/KO+e 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/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 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/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/petergtz/pegomock v2.7.0+incompatible h1:42rJ5wIOBAg9OGdkLaPW9PlF/RtqDc5aGl6PcTCXl3o= +github.com/petergtz/pegomock v2.7.0+incompatible/go.mod h1:nuBLWZpVyv/fLo56qTwt/AUau7jgouO1h7bEvZCq82o= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/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= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/pquerna/ffjson v0.0.0-20180717144149-af8b230fcd20/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= -github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190129233650-316cf8ccfec5/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/quobyte/api v0.1.2/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H6VI= -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/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rakyll/hey v0.1.3 h1:OxaaU+3P16QGT4tccjFgZ51havtIZTgTshnUc62JPBE= +github.com/rakyll/hey v0.1.3/go.mod h1:nAOTOo+L52KB9SZq/M6J18kxjto4yVtXQDjU2HgjUPI= 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/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= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/robfig/cron v1.1.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 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/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3 h1:xkBtI5JktwbW/vf4vopBbhYsRFTGfQWHYXzC0/qYwxI= +github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3/go.mod h1:rtQlpHw+eR6UrqaS3kX1VYeaCxzCVdimDS7g5Ln4pPc= 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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= -github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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/storageos/go-api v0.0.0-20180912212459-343b3eff91fc/go.mod h1:ZrLn+e0ZuF3Y65PNF6dIwbJPZqfmtCXxFm9ckv0agOY= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 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= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/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= -github.com/thecodeteam/goscaleio v0.1.0/go.mod h1:68sdkZAsK8bvEwBlbQnlLS+xU+hvLYM/iQ8KXej1AwM= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -github.com/vishvananda/netlink v0.0.0-20171020171820-b2de5d10e38e/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netns v0.0.0-20171111001504-be1fbeda1936/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vmware/govmomi v0.20.1/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= 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/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY= -github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/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/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.6/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= 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= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4= go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= -go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152 h1:ZC1Xn5A1nlpSmQCIva4bZ3ob3lmhYIefc+GU+DLg1Ow= -golang.org/x/crypto v0.0.0-20191028145041-f83a4685e152/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/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-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -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= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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/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= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271 h1:N66aaryRB3Ax92gH0v3hp1QYZ3zWWCCUR/j8Ifh45Ss= -golang.org/x/net v0.0.0-20191028085509-fe3aa8a45271/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= @@ -608,167 +685,207 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181004145325-8469e314837c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/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= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/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= +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/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 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/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= 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= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 h1:UXl+Zk3jqqcbEVV7ace5lrt4YdA4tXiz3f/KbmD29Vo= -google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 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/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/square/go-jose.v1 v1.1.2/go.mod h1:QpYS+a4WhS+DTlyQIi6Ka7MS3SuR9a055rgXNEe6EiA= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 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= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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/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= +helm.sh/helm/v3 v3.1.2 h1:VpNzaNv2DX4aRnOCcV7v5Of+XT2SZrJ8iOQ25AGKOos= +helm.sh/helm/v3 v3.1.2/go.mod h1:WYsFJuMASa/4XUqLyv54s0U/f3mlAaRErGmyy4z921g= +helm.sh/helm/v3 v3.2.0-rc.1 h1:P5Aui2Q+P9eQYmRxdIgOKPatxEPd8yRUVFaOTdUvDYE= +helm.sh/helm/v3 v3.2.0-rc.1/go.mod h1:ZaXz/vzktgwjyGGFbUWtIQkscfE7WYoRGP2szqAFHR0= +helm.sh/helm/v3 v3.2.0 h1:V12EGAmr2DJ/fWrPo2fPdXWSIXvlXm51vGkQIXMeymE= +helm.sh/helm/v3 v3.2.0/go.mod h1:ZaXz/vzktgwjyGGFbUWtIQkscfE7WYoRGP2szqAFHR0= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.2/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -k8s.io/api v0.0.0-20190918155943-95b840bb6a1f h1:8FRUST8oUkEI45WYKyD8ed7Ad0Kg5v11zHyPkEVb2xo= -k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= -k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783 h1:V6ndwCPoao1yZ52agqOKaUAl7DYWVGiXjV7ePA2i610= -k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= -k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655 h1:CS1tBQz3HOXiseWZu6ZicKX361CZLT97UFnnPx0aqBw= -k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= -k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= -k8s.io/cli-runtime v0.0.0-20190918162238-f783a3654da8 h1:W3zT6wRwUKkEGnUu1OAAJFwcgETlCu1BLdNP/VCTFuM= -k8s.io/cli-runtime v0.0.0-20190918162238-f783a3654da8/go.mod h1:WRliO+M6Osz7/zdOF0RI42IsJgSYHUwbLgqAWJPneSs= -k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90 h1:mLmhKUm1X+pXu0zXMEzNsOF5E2kKFGe5o6BZBIIqA6A= -k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= -k8s.io/cloud-provider v0.0.0-20190918163234-a9c1f33e9fb9/go.mod h1:YfUBehfPUDgnhqAFcuXj8haXt/v86nhy8r4ZOuSvXhg= -k8s.io/cluster-bootstrap v0.0.0-20190918163108-da9fdfce26bb/go.mod h1:mQVbtFRxlw/BzBqBaQwIMzjDTST1KrGtzWaR4CGlsTU= -k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= -k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA= -k8s.io/cri-api v0.0.0-20190828162817-608eb1dad4ac/go.mod h1:BvtUaNBr0fEpzb11OfrQiJLsLPtqbmulpo1fPwcpP6Q= -k8s.io/csi-translation-lib v0.0.0-20190918163402-db86a8c7bb21/go.mod h1:Ja9f0K9MkTuUSyBgpjFt2am69TOjrmkQUN25WTF3CCM= +k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= +k8s.io/api v0.18.0 h1:lwYk8Vt7rsVTwjRU6pzEsa9YNhThbmbocQlKvNBB4EQ= +k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= +k8s.io/api v0.18.2 h1:wG5g5ZmSVgm5B+eHMIbI9EGATS2L8Z72rda19RIEgY8= +k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= +k8s.io/apiextensions-apiserver v0.17.2 h1:cP579D2hSZNuO/rZj9XFRzwJNYb41DbNANJb6Kolpss= +k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs= +k8s.io/apiextensions-apiserver v0.18.0 h1:HN4/P8vpGZFvB5SOMuPPH2Wt9Y/ryX+KRvIyAkchu1Q= +k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo= +k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.18.0 h1:fuPfYpk3cs1Okp/515pAf0dNhL66+8zk8RLbSX+EgAE= +k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA= +k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo= +k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw= +k8s.io/cli-runtime v0.17.2/go.mod h1:aa8t9ziyQdbkuizkNLAw3qe3srSyWh9zlSB7zTqRNPI= +k8s.io/cli-runtime v0.18.0 h1:jG8XpSqQ5TrV0N+EZ3PFz6+gqlCk71dkggWCCq9Mq34= +k8s.io/cli-runtime v0.18.0/go.mod h1:1eXfmBsIJosjn9LjEBUd2WVPoPAY9XGTqTFcPMIBsUQ= +k8s.io/cli-runtime v0.18.2 h1:JiTN5RgkFNTiMxHBRyrl6n26yKWAuNRlei1ZJALUmC8= +k8s.io/cli-runtime v0.18.2/go.mod h1:yfFR2sQQzDsV0VEKGZtrJwEy4hLZ2oj4ZIfodgxAHWQ= +k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= +k8s.io/client-go v0.18.0 h1:yqKw4cTUQraZK3fcVCMeSa+lqKwcjZ5wtcOIPnxQno4= +k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8= +k8s.io/client-go v0.18.2 h1:aLB0iaD4nmwh7arT2wIn+lMnAq7OswjaejkQ8p9bBYE= +k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= +k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/code-generator v0.18.0/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= +k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= +k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs= +k8s.io/component-base v0.18.0 h1:I+lP0fNfsEdTDpHaL61bCAqTZLoiWjEEP304Mo5ZQgE= +k8s.io/component-base v0.18.0/go.mod h1:u3BCg0z1uskkzrnAKFzulmYaEpZF7XC9Pf/uFyb1v2c= +k8s.io/component-base v0.18.2 h1:SJweNZAGcUvsypLGNPNGeJ9UgPZQ6+bW+gEHe8uyh/Y= +k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmDfUM= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/heapster v1.2.0-beta.1/go.mod h1:h1uhptVXMwC8xtZBYsPXKVi8fpdlYkTs6k949KozGrM= +k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/kube-aggregator v0.0.0-20190918161219-8c8f079fddc3/go.mod h1:NJisPUqwlg1A99RhO1BTnNtwC4pKUyXJ2f3Xc4PxKQg= -k8s.io/kube-controller-manager v0.0.0-20190918162944-7a93a0ddadd8/go.mod h1:+HrHoqJm0UqnlrBEKXGzs2701YN4+ozi76oG7iYvJ8s= -k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/kube-openapi v0.0.0-20190918143330-0270cf2f1c1d h1:Xpe6sK+RY4ZgCTyZ3y273UmFmURhjtoJiwOMbQsXitY= -k8s.io/kube-openapi v0.0.0-20190918143330-0270cf2f1c1d/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/kube-proxy v0.0.0-20190918162534-de037b596c1e/go.mod h1:/48p8Y6dkWJrll4tsceAoGKudGpRmtQu/u1zlG14NnI= -k8s.io/kube-scheduler v0.0.0-20190918162820-3b5c1246eb18/go.mod h1:k2dnGirIGylr51dpqxn2Zv6Yt47A+6NiynBIYfAU67I= -k8s.io/kubectl v0.0.0-20190918164019-21692a0861df h1:EwjdCG4HveZxJkI650+g4UoIuSvH7vODn55VmBjxIAo= -k8s.io/kubectl v0.0.0-20190918164019-21692a0861df/go.mod h1:AjffgL1ZYSrbpRJHER9vC+/INYwTSdmoZD0DXhMKzxQ= -k8s.io/kubelet v0.0.0-20190918162654-250a1838aa2c/go.mod h1:LGhpyzd/3AkWcFcQJ3yO1UxMnJ6urMkCYfCp4iVxhjs= -k8s.io/kubernetes v1.16.3 h1:Bk2cKOdTtuGeod3+ytBeXxqIVHbh7Pu+aq0c+YJLX7g= -k8s.io/kubernetes v1.16.3/go.mod h1:hJd0X6w7E/MiE7PcDp11XHhdgQBYc33vP+WtTJqG/AU= -k8s.io/legacy-cloud-providers v0.0.0-20190918163543-cfa506e53441/go.mod h1:Phw/j+7dcoTPXRkv9Nyi3RJuA6SVSoHlc7M5K1pHizM= -k8s.io/metrics v0.0.0-20190918162108-227c654b2546 h1:GmR5FKUvbcVV2TLAVFusUFWENjlIg7KLldAST5DqalY= -k8s.io/metrics v0.0.0-20190918162108-227c654b2546/go.mod h1:XUFuIsGbIqaUga6Ivs02cCzxNjY4RPRvYnW0KhmnpQY= -k8s.io/repo-infra v0.0.0-20181204233714-00fe14e3d1a3/go.mod h1:+G1xBfZDfVFsm1Tj/HNCvg4QqWx8rJ2Fxpqr1rqp/gQ= -k8s.io/sample-apiserver v0.0.0-20190918161442-d4c9c65c82af/go.mod h1:HP/BmiRyZTMIZ5RI2p4tCz/b2kre7URuKLQ7/KHqWAs= -k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20191010214722-8d271d903fe4 h1:Gi+/O1saihwDqnlmC8Vhv1M5Sp4+rbOmK9TbsLn8ZEA= -k8s.io/utils v0.0.0-20191010214722-8d271d903fe4/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c h1:/KUFqjjqAcY4Us6luF5RDNZ16KJtb49HfR3ZHB9qYXM= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/kubectl v0.17.2/go.mod h1:y4rfLV0n6aPmvbRCqZQjvOp3ezxsFgpqL+zF5jH/lxk= +k8s.io/kubectl v0.18.0 h1:hu52Ndq/d099YW+3sS3VARxFz61Wheiq8K9S7oa82Dk= +k8s.io/kubectl v0.18.0/go.mod h1:LOkWx9Z5DXMEg5KtOjHhRiC1fqJPLyCr3KtQgEolCkU= +k8s.io/kubectl v0.18.2 h1:9jnGSOC2DDVZmMUTMi0D1aed438mfQcgqa5TAzVjA1k= +k8s.io/kubectl v0.18.2/go.mod h1:OdgFa3AlsPKRpFFYE7ICTwulXOcMGXHTc+UKhHKvrb4= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/metrics v0.17.2/go.mod h1:3TkNHET4ROd+NfzNxkjoVfQ0Ob4iZnaHmSEA4vYpwLw= +k8s.io/metrics v0.18.0 h1:yTt/yuRVW1XfnBg8DcOGecW+rrR7VxrMUXYIiUqSELE= +k8s.io/metrics v0.18.0/go.mod h1:8aYTW18koXqjLVKL7Ds05RPMX9ipJZI3mywYvBOxXd4= +k8s.io/metrics v0.18.2 h1:v4J7WKu/Zo/htSH3w//UWJZT9/CpUThXWYyUbQ/F/jY= +k8s.io/metrics v0.18.2/go.mod h1:qga8E7QfYNR9Q89cSCAjinC9pTZ7yv1XSVGUB0vJypg= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= 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/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= vbom.ml/util v0.0.0-20160121211510-db5cfe13f5cc/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/internal/client/client.go b/internal/client/client.go index 9741e48c..69fff2e6 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,6 +1,7 @@ package client import ( + "context" "fmt" "path/filepath" "strings" @@ -27,22 +28,24 @@ const ( cacheMXKey = "metrics" cacheMXAPIKey = "metricsAPI" checkConnTimeout = 10 * time.Second + + // CallTimeout represents default api call timeout. + CallTimeout = 5 * time.Second ) var supportedMetricsAPIVersions = []string{"v1beta1"} // APIClient represents a Kubernetes api client. type APIClient struct { - checkClientSet *kubernetes.Clientset - client kubernetes.Interface - dClient dynamic.Interface - nsClient dynamic.NamespaceableResourceInterface - mxsClient *versioned.Clientset - cachedClient *disk.CachedDiscoveryClient - config *Config - mx sync.Mutex - cache *cache.LRUExpireCache - metricsAPI bool + client kubernetes.Interface + dClient dynamic.Interface + nsClient dynamic.NamespaceableResourceInterface + mxsClient *versioned.Clientset + cachedClient *disk.CachedDiscoveryClient + config *Config + mx sync.Mutex + cache *cache.LRUExpireCache + metricsAPI bool } // NewTestClient for testing ONLY!! @@ -86,6 +89,33 @@ func makeCacheKey(ns, gvr string, vv []string) string { return ns + ":" + gvr + "::" + strings.Join(vv, ",") } +// ActiveCluster returns the current cluster name. +func (a *APIClient) ActiveCluster() string { + c, err := a.config.CurrentClusterName() + if err != nil { + log.Error().Msgf("Unable to located active cluster") + return "" + } + return c +} + +// IsActiveNamespace returns true if namespaces matches. +func (a *APIClient) IsActiveNamespace(ns string) bool { + if a.ActiveNamespace() == AllNamespaces { + return true + } + return a.ActiveNamespace() == ns +} + +// ActiveNamespace returns the current namespace. +func (a *APIClient) ActiveNamespace() string { + ns, err := a.CurrentNamespaceName() + if err != nil { + return AllNamespaces + } + return ns +} + func (a *APIClient) clearCache() { for _, k := range a.cache.Keys() { a.cache.Remove(k) @@ -104,9 +134,12 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) } } dial, sar := a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr) + + ctx, cancel := context.WithTimeout(context.Background(), CallTimeout) + defer cancel() for _, v := range verbs { sar.Spec.ResourceAttributes.Verb = v - resp, err := dial.Create(sar) + resp, err := dial.Create(ctx, sar, metav1.CreateOptions{}) if err != nil { log.Warn().Err(err).Msgf(" Dial Failed!") a.cache.Add(key, false, cacheExpiry) @@ -135,7 +168,9 @@ func (a *APIClient) ServerVersion() (*version.Info, error) { // ValidNamespaces returns all available namespaces. func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { - nn, err := a.DialOrDie().CoreV1().Namespaces().List(metav1.ListOptions{}) + ctx, cancel := context.WithTimeout(context.Background(), CallTimeout) + defer cancel() + nn, err := a.DialOrDie().CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } @@ -143,31 +178,30 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { } // CheckConnectivity return true if api server is cool or false otherwise. -// 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 } + if !status { + a.clearCache() + } }() - if a.checkClientSet == nil { - cfg, err := a.config.flags.ToRESTConfig() - if err != nil { - return - } - cfg.Timeout = checkConnTimeout + cfg, err := a.config.flags.ToRESTConfig() + if err != nil { + return + } + cfg.Timeout = checkConnTimeout - if a.checkClientSet, err = kubernetes.NewForConfig(cfg); err != nil { - log.Error().Err(err).Msgf("Unable to connect to api server") - return - } + client, err := kubernetes.NewForConfig(cfg) + if err != nil { + log.Error().Err(err).Msgf("Unable to connect to api server") + return } - if _, err := a.checkClientSet.ServerVersion(); err == nil { + if _, err := client.ServerVersion(); err == nil { + a.reset() status = true } else { log.Error().Err(err).Msgf("K9s can't connect to cluster") @@ -198,8 +232,12 @@ func (a *APIClient) HasMetrics() bool { a.cache.Add(cacheMXKey, flag, cacheExpiry) return flag } - if _, err := dial.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{Limit: 1}); err == nil { + ctx, cancel := context.WithTimeout(context.Background(), CallTimeout) + defer cancel() + if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil { flag = true + } else { + log.Error().Err(err).Msgf("List metrics failed") } a.cache.Add(cacheMXKey, flag, cacheExpiry) @@ -214,8 +252,9 @@ func (a *APIClient) DialOrDie() kubernetes.Interface { var err error if a.client, err = kubernetes.NewForConfig(a.RestConfigOrDie()); err != nil { - log.Fatal().Err(err).Msgf("Unable to connect to api server") + log.Panic().Err(err).Msgf("Unable to connect to api server") } + return a.client } @@ -223,7 +262,7 @@ func (a *APIClient) DialOrDie() kubernetes.Interface { func (a *APIClient) RestConfigOrDie() *restclient.Config { cfg, err := a.config.RESTConfig() if err != nil { - log.Fatal().Err(err).Msgf("Unable to connect to api server") + log.Panic().Err(err).Msgf("Unable to connect to api server") } return cfg } @@ -303,6 +342,7 @@ func (a *APIClient) reset() { a.mx.Lock() defer a.mx.Unlock() + a.config.reset() a.cache = cache.NewLRUExpireCache(cacheSize) a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil a.cachedClient = nil @@ -324,6 +364,7 @@ func (a *APIClient) supportsMetricsResources() (supported bool) { apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups() if err != nil { + log.Debug().Msgf("Unable to access servergroups %#v", err) return } for _, grp := range apiGroups.Groups { diff --git a/internal/client/config.go b/internal/client/config.go index 0be32021..cdea393c 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -319,8 +319,6 @@ func (c *Config) ensureConfig() { if c.clientConfig != nil { return } - - log.Debug().Msg("Loading raw config from flags...") c.clientConfig = c.flags.ToRawKubeConfigLoader() } diff --git a/internal/client/metrics.go b/internal/client/metrics.go index 0d66f00a..78281bde 100644 --- a/internal/client/metrics.go +++ b/internal/client/metrics.go @@ -1,6 +1,7 @@ package client import ( + "context" "fmt" "math" "time" @@ -79,9 +80,9 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL tmem += mx.AllocatableMEM teph += mx.AllocatableEphemeral } - mx.PercCPU, mx.PercMEM, mx.PercEphemeral = ToPercentage(ccpu, tcpu), - ToPercentage(cmem, tmem), - ToPercentage(ceph, teph) + mx.PercCPU = ToPercentage(ccpu, tcpu) + mx.PercMEM = ToPercentage(cmem, tmem) + mx.PercEphemeral = ToPercentage(ceph, teph) return nil } @@ -129,7 +130,7 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM } // FetchNodesMetrics return all metrics for nodes. -func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) { +func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error) { const msg = "user is not authorized to list node metrics" mx := new(mv1beta1.NodeMetricsList) @@ -150,7 +151,7 @@ func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) { if err != nil { return mx, err } - mxList, err := client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{}) + mxList, err := client.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{}) if err != nil { return mx, err } @@ -160,7 +161,7 @@ func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) { } // FetchPodsMetrics return all metrics for pods in a given namespace. -func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) { +func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error) { mx := new(mv1beta1.PodMetricsList) const msg = "user is not authorized to list pods metrics" @@ -184,7 +185,7 @@ func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, e if err != nil { return mx, err } - mxList, err := client.MetricsV1beta1().PodMetricses(ns).List(metav1.ListOptions{}) + mxList, err := client.MetricsV1beta1().PodMetricses(ns).List(ctx, metav1.ListOptions{}) if err != nil { return mx, err } @@ -194,7 +195,7 @@ func (m *MetricsServer) FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, e } // FetchPodMetrics return all metrics for pods in a given namespace. -func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error) { +func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1beta1.PodMetrics, error) { var mx *mv1beta1.PodMetrics const msg = "user is not authorized to list pod metrics" @@ -218,7 +219,7 @@ func (m *MetricsServer) FetchPodMetrics(fqn string) (*mv1beta1.PodMetrics, error if err != nil { return mx, err } - mx, err = client.MetricsV1beta1().PodMetricses(ns).Get(n, metav1.GetOptions{}) + mx, err = client.MetricsV1beta1().PodMetricses(ns).Get(ctx, n, metav1.GetOptions{}) if err != nil { return mx, err } diff --git a/internal/client/types.go b/internal/client/types.go index 6a115db4..61f1ef56 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -101,6 +101,15 @@ type Connection interface { // CheckConnectivity checks if api server connection is happy or not. CheckConnectivity() bool + + // ActiveCluster returns the current cluster name. + ActiveCluster() string + + // ActiveNamespace returns the current namespace. + ActiveNamespace() string + + // IsActiveNamespace checks if given ns is active. + IsActiveNamespace(string) bool } // CurrentMetrics tracks current cpu/mem. diff --git a/internal/color/colorize.go b/internal/color/colorize.go index 80d8f20c..72be9de5 100644 --- a/internal/color/colorize.go +++ b/internal/color/colorize.go @@ -4,20 +4,23 @@ import ( "fmt" ) +// ColorFmt colorize a string with ansi colors. +const ColorFmt = "\x1b[%dm%s\x1b[0m" + // Paint describes a terminal color. type Paint int // Defines basic ANSI colors. const ( - Black Paint = iota + 30 - Red - Green - Yellow - Blue - Magenta - Cyan - White - DarkGray = 90 + Black Paint = iota + 30 // 30 + Red // 31 + Green // 32 + Yellow // 33 + Blue // 34 + Magenta // 35 + Cyan // 36 + LightGray // 37 + DarkGray = 90 Bold = 1 ) @@ -25,7 +28,7 @@ const ( // Colorize returns an ASCII colored string based on given color. func Colorize(s string, c Paint) string { if c == 0 { - c = White + return s } - return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s) + return fmt.Sprintf(ColorFmt, c, s) } diff --git a/internal/color/colorize_test.go b/internal/color/colorize_test.go index 8a616531..9a8f6da9 100644 --- a/internal/color/colorize_test.go +++ b/internal/color/colorize_test.go @@ -1,26 +1,27 @@ -package color +package color_test import ( "testing" + "github.com/derailed/k9s/internal/color" "github.com/stretchr/testify/assert" ) func TestColorize(t *testing.T) { uu := map[string]struct { s string - c Paint + c color.Paint e string }{ - "white": {"blee", White, "\x1b[37mblee\x1b[0m"}, - "black": {"blee", Black, "\x1b[30mblee\x1b[0m"}, - "default": {"blee", 0, "\x1b[37mblee\x1b[0m"}, + "white": {"blee", color.LightGray, "\x1b[37mblee\x1b[0m"}, + "black": {"blee", color.Black, "\x1b[30mblee\x1b[0m"}, + "default": {"blee", 0, "blee"}, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, Colorize(u.s, u.c)) + assert.Equal(t, u.e, color.Colorize(u.s, u.c)) }) } } diff --git a/internal/config/alias.go b/internal/config/alias.go index ceced855..929c67b5 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -31,6 +31,18 @@ func NewAliases() *Aliases { } } +// Keys returns all aliases keys. +func (a *Aliases) Keys() []string { + a.mx.RLock() + defer a.mx.RUnlock() + + ss := make([]string, 0, len(a.Alias)) + for k := range a.Alias { + ss = append(ss, k) + } + return ss +} + // ShortNames return all shortnames. func (a *Aliases) ShortNames() ShortNames { a.mx.RLock() @@ -107,6 +119,13 @@ func (a *Aliases) LoadFileAliases(path string) error { return nil } +func (a *Aliases) declare(key string, aliases ...string) { + a.Alias[key] = key + for _, alias := range aliases { + a.Alias[alias] = key + } +} + func (a *Aliases) loadDefaultAliases() { a.mx.Lock() defer a.mx.Unlock() @@ -120,49 +139,19 @@ func (a *Aliases) loadDefaultAliases() { 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 - a.Alias["pulses"] = pulses - } + a.declare("help", "h", "?") + a.declare("quit", "q", "Q") + a.declare("aliases", "alias", "a") + a.declare("popeye", "pop") + a.declare("helm", "charts", "chart", "hm") + a.declare("contexts", "context", "ctx") + a.declare("users", "user", "usr") + a.declare("groups", "group", "grp") + a.declare("portforwards", "portforward", "pf") + a.declare("benchmarks", "benchmark", "be") + a.declare("screendumps", "screendump", "sd") + a.declare("pulses", "pulse", "pu", "hz") + a.declare("xrays", "xray", "x") } // Save alias to disk. diff --git a/internal/config/cluster.go b/internal/config/cluster.go index d056e9b8..be734ae0 100644 --- a/internal/config/cluster.go +++ b/internal/config/cluster.go @@ -4,13 +4,18 @@ import "github.com/derailed/k9s/internal/client" // Cluster tracks K9s cluster configuration. type Cluster struct { - Namespace *Namespace `yaml:"namespace"` - View *View `yaml:"view"` + Namespace *Namespace `yaml:"namespace"` + View *View `yaml:"view"` + FeatureGates *FeatureGates `yaml:"featureGates"` } // NewCluster creates a new cluster configuration. func NewCluster() *Cluster { - return &Cluster{Namespace: NewNamespace(), View: NewView()} + return &Cluster{ + Namespace: NewNamespace(), + View: NewView(), + FeatureGates: NewFeatureGates(), + } } // Validate a cluster config. @@ -20,6 +25,10 @@ func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) { } c.Namespace.Validate(conn, ks) + if c.FeatureGates == nil { + c.FeatureGates = NewFeatureGates() + } + if c.View == nil { c.View = NewView() } diff --git a/internal/config/config.go b/internal/config/config.go index e06fef6c..620c1b0d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -199,6 +199,9 @@ func (c *Config) Load(path string) error { if cfg.K9s != nil { c.K9s = cfg.K9s } + if c.K9s.Logger == nil { + c.K9s.Logger = NewLogger() + } return nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a7b2cd09..ffab42c5 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -96,7 +96,8 @@ func TestConfigLoad(t *testing.T) { assert.Nil(t, cfg.Load("testdata/k9s.yml")) assert.Equal(t, 2, cfg.K9s.RefreshRate) - assert.Equal(t, 200, cfg.K9s.LogBufferSize) + assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) + assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount) assert.Equal(t, "minikube", cfg.K9s.CurrentContext) assert.Equal(t, "minikube", cfg.K9s.CurrentCluster) assert.NotNil(t, cfg.K9s.Clusters) @@ -206,8 +207,8 @@ func TestConfigSaveFile(t *testing.T) { assert.Nil(t, cfg.Load("testdata/k9s.yml")) cfg.K9s.RefreshRate = 100 cfg.K9s.ReadOnly = true - cfg.K9s.LogBufferSize = 500 - cfg.K9s.LogRequestSize = 100 + cfg.K9s.Logger.TailCount = 500 + cfg.K9s.Logger.BufferSize = 800 cfg.K9s.CurrentContext = "blee" cfg.K9s.CurrentCluster = "blee" cfg.Validate() @@ -262,11 +263,16 @@ var expectedConfig = `k9s: refreshRate: 100 headless: false readOnly: true - logBufferSize: 500 - logRequestSize: 100 + noIcons: false + logger: + tail: 500 + buffer: 800 + sinceSeconds: -1 + fullScreenLogs: false + textWrap: false + showTime: false currentContext: blee currentCluster: blee - fullScreenLogs: false clusters: blee: namespace: @@ -275,28 +281,30 @@ var expectedConfig = `k9s: - default view: active: po + featureGates: + nodeShell: false fred: namespace: active: default favorites: - default - - kube-public - istio-system - all - - kube-system view: active: po + featureGates: + nodeShell: false minikube: namespace: active: kube-system favorites: - default - - kube-public - istio-system - all - - kube-system view: active: ctx + featureGates: + nodeShell: false thresholds: cpu: critical: 90 @@ -310,11 +318,16 @@ var resetConfig = `k9s: refreshRate: 2 headless: false readOnly: false - logBufferSize: 200 - logRequestSize: 200 + noIcons: false + logger: + tail: 200 + buffer: 2000 + sinceSeconds: -1 + fullScreenLogs: false + textWrap: false + showTime: false currentContext: blee currentCluster: blee - fullScreenLogs: false clusters: blee: namespace: @@ -323,6 +336,8 @@ var resetConfig = `k9s: - default view: active: po + featureGates: + nodeShell: false thresholds: cpu: critical: 90 diff --git a/internal/config/feature.go b/internal/config/feature.go new file mode 100644 index 00000000..c9741fae --- /dev/null +++ b/internal/config/feature.go @@ -0,0 +1,11 @@ +package config + +// FeatureGates represents K9s opt-in features. +type FeatureGates struct { + NodeShell bool `yaml:"nodeShell"` +} + +// NewFeatureGate returns a new feature gate. +func NewFeatureGates() *FeatureGates { + return &FeatureGates{} +} diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 0e6cfdc4..f2a6cf32 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -2,23 +2,17 @@ package config import "github.com/derailed/k9s/internal/client" -const ( - defaultRefreshRate = 2 - defaultLogRequestSize = 200 - defaultLogBufferSize = 1000 - defaultReadOnly = false -) +const defaultRefreshRate = 2 // K9s tracks K9s configuration options. type K9s struct { RefreshRate int `yaml:"refreshRate"` Headless bool `yaml:"headless"` ReadOnly bool `yaml:"readOnly"` - LogBufferSize int `yaml:"logBufferSize"` - LogRequestSize int `yaml:"logRequestSize"` + NoIcons bool `yaml:"noIcons"` + Logger *Logger `yaml:"logger"` CurrentContext string `yaml:"currentContext"` CurrentCluster string `yaml:"currentCluster"` - FullScreenLogs bool `yaml:"fullScreenLogs"` Clusters map[string]*Cluster `yaml:"clusters,omitempty"` Thresholds Threshold `yaml:"thresholds"` manualRefreshRate int @@ -30,12 +24,10 @@ type K9s struct { // NewK9s create a new K9s configuration. func NewK9s() *K9s { return &K9s{ - RefreshRate: defaultRefreshRate, - ReadOnly: defaultReadOnly, - LogBufferSize: defaultLogBufferSize, - LogRequestSize: defaultLogRequestSize, - Clusters: make(map[string]*Cluster), - Thresholds: NewThreshold(), + RefreshRate: defaultRefreshRate, + Logger: NewLogger(), + Clusters: make(map[string]*Cluster), + Thresholds: NewThreshold(), } } @@ -106,22 +98,15 @@ func (k *K9s) validateDefaults() { if k.RefreshRate <= 0 { k.RefreshRate = defaultRefreshRate } - - if k.LogBufferSize <= 0 { - k.LogBufferSize = defaultLogBufferSize - } - - if k.LogRequestSize <= 0 { - k.LogRequestSize = defaultLogRequestSize - } } -func (k *K9s) checkClusters(ks KubeSettings) { +func (k *K9s) checkClusters(c client.Connection, ks KubeSettings) { cc, err := ks.ClusterNames() if err != nil { return } for key := range k.Clusters { + k.Clusters[key].Validate(c, ks) if InList(cc, key) { continue } @@ -138,8 +123,13 @@ func (k *K9s) Validate(c client.Connection, ks KubeSettings) { if k.Clusters == nil { k.Clusters = map[string]*Cluster{} } - k.checkClusters(ks) + k.checkClusters(c, ks) + if k.Logger == nil { + k.Logger = NewLogger() + } else { + k.Logger.Validate(c, ks) + } if k.Thresholds == nil { k.Thresholds = NewThreshold() } diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go index 4b061de6..b33b4055 100644 --- a/internal/config/k9s_test.go +++ b/internal/config/k9s_test.go @@ -22,8 +22,8 @@ func TestK9sValidate(t *testing.T) { c.Validate(mc, mk) assert.Equal(t, 2, c.RefreshRate) - assert.Equal(t, 1000, c.LogBufferSize) - assert.Equal(t, 200, c.LogRequestSize) + assert.Equal(t, int64(100), c.Logger.TailCount) + assert.Equal(t, 5000, c.Logger.BufferSize) assert.Equal(t, "ctx1", c.CurrentContext) assert.Equal(t, "c1", c.CurrentCluster) assert.Equal(t, 1, len(c.Clusters)) @@ -45,8 +45,8 @@ func TestK9sValidateBlank(t *testing.T) { c.Validate(mc, mk) assert.Equal(t, 2, c.RefreshRate) - assert.Equal(t, 1000, c.LogBufferSize) - assert.Equal(t, 200, c.LogRequestSize) + assert.Equal(t, int64(100), c.Logger.TailCount) + assert.Equal(t, 5000, c.Logger.BufferSize) assert.Equal(t, "ctx1", c.CurrentContext) assert.Equal(t, "c1", c.CurrentCluster) assert.Equal(t, 1, len(c.Clusters)) diff --git a/internal/config/logger.go b/internal/config/logger.go new file mode 100644 index 00000000..66f0d1bb --- /dev/null +++ b/internal/config/logger.go @@ -0,0 +1,49 @@ +package config + +import ( + "github.com/derailed/k9s/internal/client" +) + +const ( + // DefaultLoggerTailCount tracks default log tail size. + DefaultLoggerTailCount = 100 + // MaxLogThreshold sets the max value for log size. + MaxLogThreshold = 5000 + // DefaultSinceSeconds tracks default log age. + DefaultSinceSeconds = -1 // all logs +) + +// Logger tracks logger options +type Logger struct { + TailCount int64 `yaml:"tail"` + BufferSize int `yaml:"buffer"` + SinceSeconds int64 `yaml:"sinceSeconds"` + FullScreenLogs bool `yaml:"fullScreenLogs"` + TextWrap bool `yaml:"textWrap"` + ShowTime bool `yaml:"showTime"` +} + +// NewLogger returns a new instance. +func NewLogger() *Logger { + return &Logger{ + TailCount: DefaultLoggerTailCount, + BufferSize: MaxLogThreshold, + SinceSeconds: DefaultSinceSeconds, + } +} + +// Validate checks thresholds and make sure we're cool. If not use defaults. +func (l *Logger) Validate(_ client.Connection, _ KubeSettings) { + if l.TailCount <= 0 { + l.TailCount = DefaultLoggerTailCount + } + if l.TailCount > MaxLogThreshold { + l.TailCount = MaxLogThreshold + } + if l.BufferSize <= 0 || l.BufferSize > MaxLogThreshold { + l.BufferSize = MaxLogThreshold + } + if l.SinceSeconds == 0 { + l.SinceSeconds = DefaultSinceSeconds + } +} diff --git a/internal/config/logger_test.go b/internal/config/logger_test.go new file mode 100644 index 00000000..ff1d2ff1 --- /dev/null +++ b/internal/config/logger_test.go @@ -0,0 +1,24 @@ +package config_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestNewLogger(t *testing.T) { + l := config.NewLogger() + l.Validate(nil, nil) + + assert.Equal(t, int64(100), l.TailCount) + assert.Equal(t, 5000, l.BufferSize) +} + +func TestLoggerValidate(t *testing.T) { + var l config.Logger + l.Validate(nil, nil) + + assert.Equal(t, int64(100), l.TailCount) + assert.Equal(t, 5000, l.BufferSize) +} diff --git a/internal/config/mock_connection_test.go b/internal/config/mock_connection_test.go index d2fd8c48..3635f62a 100644 --- a/internal/config/mock_connection_test.go +++ b/internal/config/mock_connection_test.go @@ -142,6 +142,51 @@ func (mock *MockConnection) HasMetrics() bool { return ret0 } +func (mock *MockConnection) ActiveCluster() string { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockConnection().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveCluster", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) + var ret0 string + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + } + return ret0 +} + +func (mock *MockConnection) ActiveNamespace() string { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockConnection().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveNamespace", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) + var ret0 string + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(string) + } + } + return ret0 +} + +func (mock *MockConnection) IsActiveNamespace(s string) bool { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockConnection().") + } + params := []pegomock.Param{} + result := pegomock.GetGenericMockFrom(mock).Invoke("IsActiveNamespace", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) + var ret0 bool + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].(bool) + } + } + return ret0 +} + func (mock *MockConnection) IsNamespaced(_param0 string) bool { if mock == nil { panic("mock must not be nil. Use myMock := NewMockConnection().") diff --git a/internal/config/styles.go b/internal/config/styles.go index b9766071..b58eab2d 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -77,6 +77,13 @@ type ( // Log tracks Log styles. Log struct { + FgColor Color `yaml:"fgColor"` + BgColor Color `yaml:"bgColor"` + Indicator LogIndicator `yaml:"indicator"` + } + + // LogIndicator tracks log view indicator. + LogIndicator struct { FgColor Color `yaml:"fgColor"` BgColor Color `yaml:"bgColor"` } @@ -138,7 +145,6 @@ type ( BgColor Color `yaml:"bgColor"` CursorColor Color `yaml:"cursorColor"` GraphicColor Color `yaml:"graphicColor"` - ShowIcons bool `yaml:"showIcons"` } // Menu tracks menu styles. @@ -259,15 +265,21 @@ func newStatus() Status { } } -// NewLog returns a new log style. func newLog() Log { return Log{ - FgColor: "lightskyblue", + FgColor: "lightskyblue", + BgColor: "black", + Indicator: newLogIndicator(), + } +} + +func newLogIndicator() LogIndicator { + return LogIndicator{ + FgColor: "dodgerblue", BgColor: "black", } } -// NewYaml returns a new yaml style. func newYaml() Yaml { return Yaml{ KeyColor: "steelblue", @@ -276,7 +288,6 @@ func newYaml() Yaml { } } -// NewTitle returns a new title style. func newTitle() Title { return Title{ FgColor: "aqua", @@ -287,7 +298,6 @@ func newTitle() Title { } } -// NewInfo returns a new info style. func newInfo() Info { return Info{ SectionColor: "white", @@ -295,18 +305,15 @@ func newInfo() Info { } } -// NewXray returns a new xray style. func newXray() Xray { return Xray{ FgColor: "aqua", BgColor: "black", CursorColor: "whitesmoke", GraphicColor: "floralwhite", - ShowIcons: true, } } -// NewTable returns a new table style. func newTable() Table { return Table{ FgColor: "aqua", @@ -317,7 +324,6 @@ func newTable() Table { } } -// NewTableHeader returns a new table header style. func newTableHeader() TableHeader { return TableHeader{ FgColor: "white", @@ -326,7 +332,6 @@ func newTableHeader() TableHeader { } } -// NewCrumb returns a new crumbs style. func newCrumb() Crumb { return Crumb{ FgColor: "black", @@ -335,7 +340,6 @@ func newCrumb() Crumb { } } -// NewBorder returns a new border style. func newBorder() Border { return Border{ FgColor: "dodgerblue", @@ -343,7 +347,6 @@ func newBorder() Border { } } -// NewMenu returns a new menu style. func newMenu() Menu { return Menu{ FgColor: "white", @@ -464,6 +467,7 @@ func (s *Styles) Load(path string) error { func (s *Styles) Update() { tview.Styles.PrimitiveBackgroundColor = s.BgColor() tview.Styles.ContrastBackgroundColor = s.BgColor() + tview.Styles.MoreContrastBackgroundColor = s.BgColor() tview.Styles.PrimaryTextColor = s.FgColor() tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color() tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color() diff --git a/internal/config/testdata/k9s.yml b/internal/config/testdata/k9s.yml index 91f4ea26..7bf42504 100644 --- a/internal/config/testdata/k9s.yml +++ b/internal/config/testdata/k9s.yml @@ -1,7 +1,8 @@ k9s: refreshRate: 2 - logBufferSize: 200 - logRequestSize: 200 + logger: + tail: 200 + buffer: 2000 currentContext: minikube currentCluster: minikube clusters: diff --git a/internal/dao/container.go b/internal/dao/container.go index bedfd2e8..4e1bcb4d 100644 --- a/internal/dao/container.go +++ b/internal/dao/container.go @@ -2,7 +2,6 @@ package dao import ( "context" - "errors" "fmt" "github.com/derailed/k9s/internal" @@ -13,7 +12,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - restclient "k8s.io/client-go/rest" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -39,7 +37,7 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error err error ) if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { - if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(fqn); err != nil { + if pmx, err = client.DialMetrics(c.Client()).FetchPodMetrics(ctx, fqn); err != nil { log.Warn().Err(err).Msgf("No metrics found for pod %q", fqn) } } @@ -60,37 +58,12 @@ 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<- []byte, opts LogOptions) error { - fac, ok := ctx.Value(internal.KeyFactory).(Factory) - if !ok { - return errors.New("Expecting an informer") - } - o, err := fac.Get("v1/pods", opts.Path, true, labels.Everything()) - if err != nil { - return err - } +func (c *Container) TailLogs(ctx context.Context, logChan LogChan, opts LogOptions) error { + log.Debug().Msgf("CONTAINER-LOGS") + po := Pod{} + po.Init(c.Factory, client.NewGVR("v1/pods")) - var po v1.Pod - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil { - return err - } - - return tailLogs(ctx, c, logChan, opts) -} - -// Logs fetch container logs for a given pod and container. -func (c *Container) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) { - ns, _ := client.Namespaced(path) - auth, err := c.Client().CanI(ns, "v1/pods:log", client.GetAccess) - if err != nil { - return nil, err - } - if !auth { - return nil, fmt.Errorf("user is not authorized to view pod logs") - } - - ns, n := client.Namespaced(path) - return c.Client().DialOrDie().CoreV1().Pods(ns).GetLogs(n, opts), nil + return po.TailLogs(ctx, logChan, opts) } // ---------------------------------------------------------------------------- diff --git a/internal/dao/container_test.go b/internal/dao/container_test.go index a486017f..b10d7ee3 100644 --- a/internal/dao/container_test.go +++ b/internal/dao/container_test.go @@ -60,6 +60,9 @@ func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil } func (c *conn) CurrentNamespaceName() (string, error) { return "", nil } func (c *conn) CanI(ns, gvr string, verbs []string) (bool, error) { return true, nil } +func (c *conn) ActiveCluster() string { return "" } +func (c *conn) ActiveNamespace() string { return "" } +func (c *conn) IsActiveNamespace(string) bool { return false } type podFactory struct{} diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go index 7e7efd0b..4feebb30 100644 --- a/internal/dao/cronjob.go +++ b/internal/dao/cronjob.go @@ -1,6 +1,7 @@ package dao import ( + "context" "fmt" "github.com/derailed/k9s/internal/client" @@ -33,7 +34,9 @@ func (c *CronJob) Run(path string) error { } // BOZO!! Factory resource?? - cj, err := c.Client().DialOrDie().BatchV1beta1().CronJobs(ns).Get(n, metav1.GetOptions{}) + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() + cj, err := c.Client().DialOrDie().BatchV1beta1().CronJobs(ns).Get(ctx, n, metav1.GetOptions{}) if err != nil { return err } @@ -51,7 +54,7 @@ func (c *CronJob) Run(path string) error { }, Spec: cj.Spec.JobTemplate.Spec, } - _, err = c.Client().DialOrDie().BatchV1().Jobs(ns).Create(job) + _, err = c.Client().DialOrDie().BatchV1().Jobs(ns).Create(ctx, job, metav1.CreateOptions{}) return err } diff --git a/internal/dao/describe.go b/internal/dao/describe.go index 9c549e80..6d40d482 100644 --- a/internal/dao/describe.go +++ b/internal/dao/describe.go @@ -4,7 +4,6 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" "k8s.io/kubectl/pkg/describe" - "k8s.io/kubectl/pkg/describe/versioned" ) // Describe describes a resource. @@ -31,7 +30,7 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error) log.Error().Err(err).Msgf("Unable to find mapper for %s %s", gvr, n) return "", err } - d, err := versioned.Describer(c.Config().Flags(), mapping) + d, err := describe.Describer(c.Config().Flags(), mapping) if err != nil { log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping) return "", err diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 9410227b..c743888a 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -35,7 +35,7 @@ func (d *Deployment) IsHappy(dp appsv1.Deployment) bool { } // Scale a Deployment. -func (d *Deployment) Scale(path string, replicas int32) error { +func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error { ns, n := client.Namespaced(path) auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", []string{client.GetVerb, client.UpdateVerb}) if err != nil { @@ -45,18 +45,18 @@ func (d *Deployment) Scale(path string, replicas int32) error { return fmt.Errorf("user is not authorized to scale a deployment") } - scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(n, metav1.GetOptions{}) + scale, err := d.Client().DialOrDie().AppsV1().Deployments(ns).GetScale(ctx, n, metav1.GetOptions{}) if err != nil { return err } scale.Spec.Replicas = replicas - _, err = d.Client().DialOrDie().AppsV1().Deployments(ns).UpdateScale(n, scale) + _, err = d.Client().DialOrDie().AppsV1().Deployments(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{}) return err } // Restart a Deployment rollout. -func (d *Deployment) Restart(path string) error { +func (d *Deployment) Restart(ctx context.Context, path string) error { dp, err := d.Load(d.Factory, path) if err != nil { return err @@ -75,12 +75,18 @@ func (d *Deployment) Restart(path string) error { return err } - _, err = d.Client().DialOrDie().AppsV1().Deployments(dp.Namespace).Patch(dp.Name, types.StrategicMergePatchType, update) + _, err = d.Client().DialOrDie().AppsV1().Deployments(dp.Namespace).Patch( + ctx, + dp.Name, + types.StrategicMergePatchType, + update, + metav1.PatchOptions{}, + ) return err } // TailLogs tail logs for all pods represented by this Deployment. -func (d *Deployment) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { +func (d *Deployment) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error { dp, err := d.Load(d.Factory, opts.Path) if err != nil { return err diff --git a/internal/dao/ds.go b/internal/dao/ds.go index 22ca93bc..b4270501 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -38,7 +38,7 @@ func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool { } // Restart a DaemonSet rollout. -func (d *DaemonSet) Restart(path string) error { +func (d *DaemonSet) Restart(ctx context.Context, path string) error { ds, err := d.GetInstance(path) if err != nil { return err @@ -56,12 +56,18 @@ func (d *DaemonSet) Restart(path string) error { 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( + ctx, + ds.Name, + types.StrategicMergePatchType, + update, + metav1.PatchOptions{}, + ) return err } // TailLogs tail logs for all pods represented by this DaemonSet. -func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { +func (d *DaemonSet) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error { ds, err := d.GetInstance(opts.Path) if err != nil { return err @@ -74,7 +80,7 @@ func (d *DaemonSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptio return podLogs(ctx, c, ds.Spec.Selector.MatchLabels, opts) } -func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts LogOptions) error { +func podLogs(ctx context.Context, c LogChan, sel map[string]string, opts LogOptions) error { f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) if !ok { return errors.New("expecting a context factory") @@ -89,14 +95,11 @@ func podLogs(ctx context.Context, c chan<- []byte, sel map[string]string, opts L } ns, _ := client.Namespaced(opts.Path) - oo, err := f.List("v1/pods", ns, false, lsel) + oo, err := f.List("v1/pods", ns, true, lsel) if err != nil { return err } - - if len(oo) > 1 { - opts.MultiPods = true - } + opts.MultiPods = true po := Pod{} po.Init(f, client.NewGVR("v1/pods")) diff --git a/internal/dao/generic.go b/internal/dao/generic.go index 7963019e..dd5856f5 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -39,9 +39,9 @@ func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) err error ) if client.IsClusterScoped(ns) { - ll, err = g.dynClient().List(metav1.ListOptions{LabelSelector: labelSel}) + ll, err = g.dynClient().List(ctx, metav1.ListOptions{LabelSelector: labelSel}) } else { - ll, err = g.dynClient().Namespace(ns).List(metav1.ListOptions{LabelSelector: labelSel}) + ll, err = g.dynClient().Namespace(ns).List(ctx, metav1.ListOptions{LabelSelector: labelSel}) } if err != nil { return nil, err @@ -57,15 +57,15 @@ func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) // Get returns a given resource. func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) { + log.Debug().Msgf("GENERIC-GET %q", path) var opts metav1.GetOptions - ns, n := client.Namespaced(path) dial := g.dynClient() if client.IsClusterScoped(ns) { - return dial.Get(n, opts) + return dial.Get(ctx, n, opts) } - return dial.Namespace(ns).Get(n, opts) + return dial.Namespace(ns).Get(ctx, n, opts) } // Describe describes a resource. @@ -111,11 +111,14 @@ func (g *Generic) Delete(path string, cascade, force bool) error { PropagationPolicy: &p, GracePeriodSeconds: grace, } + // BOZO!! Move to caller! + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() if client.IsClusterScoped(ns) { - return g.dynClient().Delete(n, &opts) + return g.dynClient().Delete(ctx, n, opts) } - return g.dynClient().Namespace(ns).Delete(n, &opts) + return g.dynClient().Namespace(ns).Delete(ctx, n, opts) } func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface { diff --git a/internal/dao/chart.go b/internal/dao/helm.go similarity index 73% rename from internal/dao/chart.go rename to internal/dao/helm.go index a407bd0d..02fb990f 100644 --- a/internal/dao/chart.go +++ b/internal/dao/helm.go @@ -13,18 +13,18 @@ import ( ) var ( - _ Accessor = (*Chart)(nil) - _ Nuker = (*Chart)(nil) - _ Describer = (*Chart)(nil) + _ Accessor = (*Helm)(nil) + _ Nuker = (*Helm)(nil) + _ Describer = (*Helm)(nil) ) -// Chart represents a helm chart. -type Chart struct { +// Helm represents a helm chart. +type Helm struct { NonResource } // List returns a collection of resources. -func (c *Chart) List(ctx context.Context, ns string) ([]runtime.Object, error) { +func (c *Helm) List(ctx context.Context, ns string) ([]runtime.Object, error) { cfg, err := c.EnsureHelmConfig(ns) if err != nil { return nil, err @@ -37,14 +37,14 @@ func (c *Chart) List(ctx context.Context, ns string) ([]runtime.Object, error) { oo := make([]runtime.Object, 0, len(rr)) for _, r := range rr { - oo = append(oo, render.ChartRes{Release: r}) + oo = append(oo, render.HelmRes{Release: r}) } return oo, nil } // Get returns a resource. -func (c *Chart) Get(_ context.Context, path string) (runtime.Object, error) { +func (c *Helm) Get(_ context.Context, path string) (runtime.Object, error) { ns, n := client.Namespaced(path) cfg, err := c.EnsureHelmConfig(ns) if err != nil { @@ -55,11 +55,11 @@ func (c *Chart) Get(_ context.Context, path string) (runtime.Object, error) { return nil, err } - return render.ChartRes{Release: resp}, nil + return render.HelmRes{Release: resp}, nil } // Describe returns the chart notes. -func (c *Chart) Describe(path string) (string, error) { +func (c *Helm) Describe(path string) (string, error) { ns, n := client.Namespaced(path) cfg, err := c.EnsureHelmConfig(ns) if err != nil { @@ -74,7 +74,7 @@ func (c *Chart) Describe(path string) (string, error) { } // ToYAML returns the chart manifest. -func (c *Chart) ToYAML(path string) (string, error) { +func (c *Helm) ToYAML(path string) (string, error) { ns, n := client.Namespaced(path) cfg, err := c.EnsureHelmConfig(ns) if err != nil { @@ -88,8 +88,8 @@ func (c *Chart) ToYAML(path string) (string, error) { return resp.Manifest, nil } -// Delete uninstall a Chart. -func (c *Chart) Delete(path string, cascade, force bool) error { +// Delete uninstall a Helm. +func (c *Helm) Delete(path string, cascade, force bool) error { ns, n := client.Namespaced(path) cfg, err := c.EnsureHelmConfig(ns) if err != nil { @@ -109,7 +109,7 @@ func (c *Chart) Delete(path string, cascade, force bool) error { } // EnsureHelmConfig return a new configuration. -func (c *Chart) EnsureHelmConfig(ns string) (*action.Configuration, error) { +func (c *Helm) EnsureHelmConfig(ns string) (*action.Configuration, error) { cfg := new(action.Configuration) flags := c.Client().Config().Flags() if err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil { diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go index 718ad677..733df5c2 100644 --- a/internal/dao/helpers.go +++ b/internal/dao/helpers.go @@ -12,6 +12,14 @@ import ( "k8s.io/cli-runtime/pkg/printers" ) +// IsFuzzySelector checks if filter is fuzzy or not. +func IsFuzzySelector(s string) bool { + if s == "" { + return false + } + return fuzzyRx.MatchString(s) +} + func toPerc(v1, v2 float64) float64 { if v2 == 0 { return 0 diff --git a/internal/dao/job.go b/internal/dao/job.go index 5f002804..f243ed99 100644 --- a/internal/dao/job.go +++ b/internal/dao/job.go @@ -23,7 +23,7 @@ type Job struct { } // TailLogs tail logs for all pods represented by this Job. -func (j *Job) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { +func (j *Job) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error { o, err := j.Factory.Get(j.gvr.String(), opts.Path, true, labels.Everything()) if err != nil { return err diff --git a/internal/dao/log_item.go b/internal/dao/log_item.go new file mode 100644 index 00000000..cde710dd --- /dev/null +++ b/internal/dao/log_item.go @@ -0,0 +1,182 @@ +package dao + +import ( + "bytes" + "fmt" + "math/rand" + "regexp" + "strings" + "time" + + "github.com/derailed/tview" + "github.com/rs/zerolog/log" + "github.com/sahilm/fuzzy" +) + +// LogChan represents a channel for logs. +type LogChan chan *LogItem + +// LogItem represents a container log line. +type LogItem struct { + Pod, Container, Timestamp string + Bytes []byte +} + +// NewLogItem returns a new item. +func NewLogItem(b []byte) *LogItem { + space := []byte(" ") + var l LogItem + + cols := bytes.Split(b[:len(b)-1], space) + l.Timestamp = string(cols[0]) + l.Bytes = bytes.Join(cols[1:], space) + + return &l +} + +// NewLogItemFromString returns a new item. +func NewLogItemFromString(s string) *LogItem { + l := LogItem{Bytes: []byte(s)} + l.Timestamp = time.Now().String() + + return &l +} + +// ID returns pod and or container based id. +func (l *LogItem) ID() string { + if l.Pod != "" { + return l.Pod + } + return l.Container +} + +// Info returns pod and container information. +func (l *LogItem) Info() string { + return fmt.Sprintf("%q::%q", l.Pod, l.Container) +} + +// IsEmpty checks if the entry is empty. +func (l *LogItem) IsEmpty() bool { + return len(l.Bytes) == 0 +} + +const colorFmt = "\033[38;5;%dm%s\033[0m" + +// colorize me +func colorize(s string, c int) string { + return fmt.Sprintf(colorFmt, c, s) +} + +// Render returns a log line as string. +func (l *LogItem) Render(c int, showTime bool) []byte { + bb := make([]byte, 0, 30+len(l.Bytes)+len(l.Info())) + if showTime { + bb = append(bb, colorize(fmt.Sprintf("%-30s ", l.Timestamp), 106)...) + } + + if l.Pod != "" { + bb = append(bb, []byte(colorize(l.Pod, c))...) + bb = append(bb, ':') + } + if l.Container != "" { + bb = append(bb, []byte(colorize(l.Container, c))...) + bb = append(bb, ' ') + } + bb = append(bb, []byte(tview.Escape(string(l.Bytes)))...) + + return bb +} + +func colorFor(n string) int { + var sum int + for _, r := range n { + sum += int(r) + } + + c := sum % 256 + if c == 0 { + c = 207 + rand.Intn(10) + } + return c +} + +// ---------------------------------------------------------------------------- + +// LogItems represents a collection of log items. +type LogItems []*LogItem + +// Lines returns a collection of log lines. +func (l LogItems) Lines() []string { + ll := make([]string, len(l)) + for i, item := range l { + ll[i] = string(item.Render(0, false)) + } + + return ll +} + +// Render returns logs as a collection of strings. +func (l LogItems) Render(showTime bool, ll [][]byte) { + colors := map[string]int{} + for i, item := range l { + info := item.ID() + c, ok := colors[item.ID()] + if !ok { + c = colorFor(info) + colors[info] = c + } + ll[i] = item.Render(c, showTime) + } +} + +// DumpDebug for debuging +func (l LogItems) DumpDebug(m string) { + fmt.Println(m + strings.Repeat("-", 50)) + for i, line := range l { + fmt.Println(i, string(line.Bytes)) + } +} + +// Filter filters out log items based on given filter. +func (l LogItems) Filter(q string) ([]int, error) { + if q == "" { + return nil, nil + } + if IsFuzzySelector(q) { + return l.fuzzyFilter(strings.TrimSpace(q[2:])), nil + } + indexes, err := l.filterLogs(q) + if err != nil { + log.Error().Err(err).Msgf("Logs filter failed") + return nil, err + } + return indexes, nil +} + +var fuzzyRx = regexp.MustCompile(`\A\-f`) + +func (l LogItems) fuzzyFilter(q string) []int { + q = strings.TrimSpace(q) + matches := make([]int, 0, len(l)) + mm := fuzzy.Find(q, l.Lines()) + for _, m := range mm { + matches = append(matches, m.Index) + } + + return matches +} + +func (l LogItems) filterLogs(q string) ([]int, error) { + rx, err := regexp.Compile(`(?i)` + q) + if err != nil { + return nil, err + } + matches := make([]int, 0, len(l)) + for i, line := range l.Lines() { + if rx.MatchString(line) { + matches = append(matches, i) + } + } + + return matches, nil +} diff --git a/internal/dao/log_item_test.go b/internal/dao/log_item_test.go new file mode 100644 index 00000000..ba17b1d9 --- /dev/null +++ b/internal/dao/log_item_test.go @@ -0,0 +1,200 @@ +package dao_test + +import ( + "fmt" + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestLogItemsFilter(t *testing.T) { + uu := map[string]struct { + q string + opts dao.LogOptions + e []int + err error + }{ + "empty": { + opts: dao.LogOptions{}, + }, + "pod-name": { + q: "blee", + opts: dao.LogOptions{ + Path: "fred/blee", + Container: "c1", + }, + e: []int{0, 1, 2}, + }, + "container-name": { + q: "c1", + opts: dao.LogOptions{ + Path: "fred/blee", + Container: "c1", + }, + e: []int{0, 1, 2}, + }, + "message": { + q: "zorg", + opts: dao.LogOptions{ + Path: "fred/blee", + Container: "c1", + }, + e: []int{2}, + }, + "fuzzy": { + q: "-f zorg", + opts: dao.LogOptions{ + Path: "fred/blee", + Container: "c1", + }, + e: []int{2}, + }, + } + + for k := range uu { + u := uu[k] + ii := dao.LogItems{ + dao.NewLogItem([]byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3..."))), + dao.NewLogItemFromString("Bumble bee tuna"), + dao.NewLogItemFromString("Jean Batiste Emmanuel Zorg"), + } + t.Run(k, func(t *testing.T) { + _, n := client.Namespaced(u.opts.Path) + for _, i := range ii { + i.Pod, i.Container = n, u.opts.Container + } + res, err := ii.Filter(u.q) + assert.Equal(t, u.err, err) + if err == nil { + assert.Equal(t, u.e, res) + } + }) + } +} + +func TestLogItemsRender(t *testing.T) { + uu := map[string]struct { + opts dao.LogOptions + e string + }{ + "empty": { + opts: dao.LogOptions{}, + e: "Testing 1,2,3...", + }, + "container": { + opts: dao.LogOptions{ + Container: "fred", + }, + e: "\x1b[38;5;161mfred\x1b[0m Testing 1,2,3...", + }, + "pod": { + opts: dao.LogOptions{ + Path: "blee/fred", + Container: "blee", + }, + e: "\x1b[38;5;161mfred\x1b[0m:\x1b[38;5;161mblee\x1b[0m Testing 1,2,3...", + }, + "full": { + opts: dao.LogOptions{ + Path: "blee/fred", + Container: "blee", + ShowTimestamp: true, + }, + e: "\x1b[38;5;106m2018-12-14T10:36:43.326972-07:00 \x1b[0m\x1b[38;5;161mfred\x1b[0m:\x1b[38;5;161mblee\x1b[0m Testing 1,2,3...", + }, + } + + s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3...")) + ii := dao.LogItems{dao.NewLogItem(s)} + for k := range uu { + u := uu[k] + _, n := client.Namespaced(u.opts.Path) + ii[0].Pod, ii[0].Container = n, u.opts.Container + t.Run(k, func(t *testing.T) { + res := make([][]byte, 1) + ii.Render(u.opts.ShowTimestamp, res) + assert.Equal(t, u.e, string(res[0])) + }) + } +} + +func TestLogItemEmpty(t *testing.T) { + uu := map[string]struct { + s string + e bool + }{ + "empty": {s: "", e: true}, + "full": {s: "Testing 1,2,3..."}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + i := dao.NewLogItemFromString(u.s) + assert.Equal(t, u.e, i.IsEmpty()) + }) + } +} + +func TestLogItemRender(t *testing.T) { + uu := map[string]struct { + opts dao.LogOptions + e string + }{ + "empty": { + opts: dao.LogOptions{}, + e: "Testing 1,2,3...", + }, + "container": { + opts: dao.LogOptions{ + Container: "fred", + }, + e: "\x1b[38;5;0mfred\x1b[0m Testing 1,2,3...", + }, + "pod": { + opts: dao.LogOptions{ + Path: "blee/fred", + Container: "blee", + }, + e: "\x1b[38;5;0mfred\x1b[0m:\x1b[38;5;0mblee\x1b[0m Testing 1,2,3...", + }, + "full": { + opts: dao.LogOptions{ + Path: "blee/fred", + Container: "blee", + ShowTimestamp: true, + }, + e: "\x1b[38;5;106m2018-12-14T10:36:43.326972-07:00 \x1b[0m\x1b[38;5;0mfred\x1b[0m:\x1b[38;5;0mblee\x1b[0m Testing 1,2,3...", + }, + } + + s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3...")) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + i := dao.NewLogItem(s) + _, n := client.Namespaced(u.opts.Path) + i.Pod, i.Container = n, u.opts.Container + + assert.Equal(t, u.e, string(i.Render(0, u.opts.ShowTimestamp))) + }) + } +} + +func BenchmarkLogItemRender(b *testing.B) { + s := []byte(fmt.Sprintf("%s %s\n", "2018-12-14T10:36:43.326972-07:00", "Testing 1,2,3...")) + i := dao.NewLogItem(s) + i.Pod, i.Container = "fred", "blee" + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + i.Render(0, true) + } +} diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index c8a3a70e..23c77b73 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -1,10 +1,13 @@ package dao import ( + "fmt" "strings" + "time" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/color" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // LogOptions represent logger options. @@ -12,11 +15,18 @@ type LogOptions struct { Path string Container string Lines int64 - Color color.Paint Previous bool SingleContainer bool MultiPods bool ShowTimestamp bool + SinceTime string + SinceSeconds int64 + In, Out string +} + +// Info returns the option pod and container info. +func (o LogOptions) Info() string { + return fmt.Sprintf("%q::%q", o.Path, o.Container) } // HasContainer checks if a container is present. @@ -24,6 +34,33 @@ func (o LogOptions) HasContainer() bool { return o.Container != "" } +// ToPodLogOptions returns pod log options. +func (o LogOptions) ToPodLogOptions() *v1.PodLogOptions { + opts := v1.PodLogOptions{ + Follow: true, + Timestamps: true, + Container: o.Container, + Previous: o.Previous, + TailLines: &o.Lines, + } + + if o.SinceSeconds < 0 { + return &opts + } + if o.SinceSeconds != 0 { + opts.SinceSeconds = &o.SinceSeconds + return &opts + } + if o.SinceTime == "" { + return &opts + } + if t, err := time.Parse(time.RFC3339, o.SinceTime); err == nil { + opts.SinceTime = &metav1.Time{Time: t.Add(time.Second)} + } + + return &opts +} + // FixedSizeName returns a normalize fixed size pod name if possible. func (o LogOptions) FixedSizeName() string { _, n := client.Namespaced(o.Path) @@ -39,35 +76,19 @@ func (o LogOptions) FixedSizeName() string { return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1] } -func colorize(c color.Paint, txt string) string { - if c == 0 { - return "" - } - - return color.Colorize(txt, c) -} - // DecorateLog add a log header to display po/co information along with the log message. -func (o LogOptions) DecorateLog(bytes []byte) []byte { +func (o LogOptions) DecorateLog(bytes []byte) *LogItem { + item := NewLogItem(bytes) if len(bytes) == 0 { - return bytes + return item } - bytes = bytes[:len(bytes)-1] - _, n := client.Namespaced(o.Path) - - var prefix []byte if o.MultiPods { - prefix = []byte(colorize(o.Color, n+":"+o.Container+" ")) + _, pod := client.Namespaced(o.Path) + item.Pod, item.Container = pod, o.Container + } else { + item.Container = o.Container } - if !o.SingleContainer { - prefix = []byte(colorize(o.Color, o.Container+" ")) - } - - if len(prefix) == 0 { - return bytes - } - - return append(prefix, bytes...) + return item } diff --git a/internal/dao/node.go b/internal/dao/node.go index 5ea1cfae..bd1367a4 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -34,13 +34,15 @@ type Node struct { // ToggleCordon toggles cordon/uncordon a node. func (n *Node) ToggleCordon(path string, cordon bool) error { - o, err := n.Get(context.Background(), path) + log.Debug().Msgf("CORDON %q::%t -- %q", path, cordon, n.gvr.GVK()) + o, err := FetchNode(context.Background(), n.Factory, path) if err != nil { return err } h, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK()) if err != nil { + log.Debug().Msgf("BOOM %v", err) return err } @@ -50,7 +52,7 @@ func (n *Node) ToggleCordon(path string, cordon bool) error { } return fmt.Errorf("node is already uncordoned") } - err, patchErr := h.PatchOrReplace(n.Factory.Client().DialOrDie()) + err, patchErr := h.PatchOrReplace(n.Factory.Client().DialOrDie(), false) if patchErr != nil { return patchErr } @@ -97,8 +99,30 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error { } // Get returns a node resource. -func (n *Node) Get(_ context.Context, path string) (runtime.Object, error) { - return FetchNode(n.Factory, path) +func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) { + var ( + nmx *mv1beta1.NodeMetricsList + err error + ) + if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { + if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx); err != nil { + log.Warn().Err(err).Msgf("No node metrics") + } + } + + no, err := FetchNode(ctx, n.Factory, path) + if err != nil { + return nil, err + } + o, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&no) + if err != nil { + return nil, err + } + + return &render.NodeWithMetrics{ + Raw: &unstructured.Unstructured{Object: o}, + MX: nodeMetricsFor(MetaFQN(no.ObjectMeta), nmx), + }, nil } // List returns a collection of node resources. @@ -113,12 +137,12 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) { err error ) if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { - if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(); err != nil { + if nmx, err = client.DialMetrics(n.Client()).FetchNodesMetrics(ctx); err != nil { log.Warn().Err(err).Msgf("No node metrics") } } - nn, err := FetchNodes(n.Factory, labels) + nn, err := FetchNodes(ctx, n.Factory, labels) if err != nil { return nil, err } @@ -141,7 +165,7 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) { // Helpers... // FetchNode retrieves a node. -func FetchNode(f Factory, path string) (*v1.Node, error) { +func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) { auth, err := f.Client().CanI("", "v1/nodes", []string{"get"}) if err != nil { return nil, err @@ -150,11 +174,11 @@ func FetchNode(f Factory, path string) (*v1.Node, error) { return nil, fmt.Errorf("user is not authorized to list nodes") } - return f.Client().DialOrDie().CoreV1().Nodes().Get(path, metav1.GetOptions{}) + return f.Client().DialOrDie().CoreV1().Nodes().Get(ctx, path, metav1.GetOptions{}) } // FetchNodes retrieves all nodes. -func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) { +func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) { auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb}) if err != nil { return nil, err @@ -163,7 +187,7 @@ func FetchNodes(f Factory, labelsSel string) (*v1.NodeList, error) { return nil, fmt.Errorf("user is not authorized to list nodes") } - return f.Client().DialOrDie().CoreV1().Nodes().List(metav1.ListOptions{ + return f.Client().DialOrDie().CoreV1().Nodes().List(ctx, metav1.ListOptions{ LabelSelector: labelsSel, }) } diff --git a/internal/dao/pod.go b/internal/dao/pod.go index 114d7b7b..12b3e36d 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -9,7 +9,6 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/watch" "github.com/rs/zerolog/log" @@ -58,7 +57,7 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) { 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 { + if pmx, err = client.DialMetrics(p.Client()).FetchPodMetrics(ctx, path); err != nil { log.Debug().Err(err).Msgf("No pod metrics") } } @@ -85,7 +84,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { 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 { + if pmx, err = client.DialMetrics(p.Client()).FetchPodsMetrics(ctx, ns); err != nil { log.Debug().Err(err).Msgf("No pods metrics") } } @@ -171,14 +170,8 @@ func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) { } // TailLogs tails a given container logs -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<- []byte, opts LogOptions) error { +func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error { + log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container) fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory) if !ok { return errors.New("Expecting an informer") @@ -187,68 +180,79 @@ func (p *Pod) logs(ctx context.Context, c chan<- []byte, opts LogOptions) error if err != nil { return err } - var po v1.Pod if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil { return err } - opts.Color = asColor(po.Name) + + if opts.HasContainer() { + opts.SingleContainer = true + if err := tailLogs(ctx, p, c, opts); err != nil { + return err + } + return nil + } if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 { opts.SingleContainer = true } + var tailed bool for _, co := range po.Spec.InitContainers { + log.Debug().Msgf("Tailing INIT-CO %q", co.Name) opts.Container = co.Name if err := p.TailLogs(ctx, c, opts); err != nil { return err } + tailed = true } - rcos := loggableContainers(po.Status) for _, co := range po.Spec.Containers { - if in(rcos, co.Name) { - opts.Container = co.Name - if err := p.TailLogs(ctx, c, opts); err != nil { - log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name) - return err - } + log.Debug().Msgf("Tailing CO %q", co.Name) + opts.Container = co.Name + if err := tailLogs(ctx, p, c, opts); err != nil { + return err } + tailed = true + } + for _, co := range po.Spec.EphemeralContainers { + log.Debug().Msgf("Tailing EPH-CO %q", co.Name) + opts.Container = co.Name + if err := tailLogs(ctx, p, c, opts); err != nil { + return err + } + tailed = true + } + + if !tailed { + return fmt.Errorf("no loggable containers found for pod %s", opts.Path) } return nil } -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{ - Follow: true, - Timestamps: false, - Container: opts.Container, - Previous: opts.Previous, - TailLines: &opts.Lines, - } - req, err := logger.Logs(opts.Path, &o) +func tailLogs(ctx context.Context, logger Logger, c LogChan, opts LogOptions) error { + log.Debug().Msgf("Tailing logs for %q:%q", opts.Path, opts.Container) + req, err := logger.Logs(opts.Path, opts.ToPodLogOptions()) if err != nil { return err } - req.Context(ctx) // This call will block if nothing is in the stream!! - stream, err := req.Stream() + stream, err := req.Stream(ctx) if err != nil { c <- opts.DecorateLog([]byte(err.Error() + "\n")) - log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path) - return fmt.Errorf("Unable to obtain log stream for %s", opts.Path) + log.Error().Err(err).Msgf("Unable to obtain log stream failed for `%s", opts.Path) + return err } go readLogs(stream, c, opts) return nil } -func readLogs(stream io.ReadCloser, c chan<- []byte, opts LogOptions) { +func readLogs(stream io.ReadCloser, c LogChan, opts LogOptions) { defer func() { - log.Debug().Msgf(">>> Closing stream `%s", opts.Path) + log.Debug().Msgf(">>> Closing stream %s", opts.Info()) if err := stream.Close(); err != nil { - log.Error().Err(err).Msg("Cloing stream") + log.Error().Err(err).Msgf("Fail to close stream %s", opts.Info()) } }() @@ -256,13 +260,13 @@ func readLogs(stream io.ReadCloser, c chan<- []byte, opts LogOptions) { for { bytes, err := r.ReadBytes('\n') if err != nil { - log.Warn().Err(err).Msg("Read error") if err == io.EOF { - c <- opts.DecorateLog([]byte(" closed\n")) + log.Warn().Err(err).Msgf("Stream closed for %s", opts.Info()) + c <- opts.DecorateLog([]byte("log stream closed\n")) return } - log.Error().Err(err).Msgf("stream reader failed") - c <- opts.DecorateLog([]byte(" failed\n")) + log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info()) + c <- opts.DecorateLog([]byte("log stream failed\n")) return } c <- opts.DecorateLog(bytes) @@ -328,22 +332,6 @@ func extractFQN(o runtime.Object) string { return FQN(ns, n) } -func loggableContainers(s v1.PodStatus) []string { - var rcos []string - for _, c := range s.ContainerStatuses { - rcos = append(rcos, c.Name) - } - return rcos -} - -func asColor(n string) color.Paint { - var sum int - for _, r := range n { - sum += int(r) - } - return color.Paint(30 + 2 + sum%6) -} - // Check if string is in a string list. func in(ll []string, s string) bool { for _, l := range ll { diff --git a/internal/dao/popeye.go b/internal/dao/popeye.go new file mode 100644 index 00000000..371f1233 --- /dev/null +++ b/internal/dao/popeye.go @@ -0,0 +1,136 @@ +package dao + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + cfg "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/popeye/pkg" + "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/types" + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ Accessor = (*Popeye)(nil) + +// Popeye tracks cluster sanitization. +type Popeye struct { + NonResource +} + +// NewPopeye returns a new set of aliases. +func NewPopeye(f Factory) *Popeye { + a := Popeye{} + a.Init(f, client.NewGVR("popeye")) + + return &a +} + +type readWriteCloser struct { + *bytes.Buffer +} + +// Close close read stream. +func (readWriteCloser) Close() error { + return nil +} + +// List returns a collection of aliases. +func (p *Popeye) List(ctx context.Context, _ string) ([]runtime.Object, error) { + defer func(t time.Time) { + log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t)) + if err := recover(); err != nil { + log.Debug().Msgf("POPEYE DIED!") + } + }(time.Now()) + + flags := config.NewFlags() + js := "json" + flags.Output = &js + + if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" { + sections := []string{report} + flags.Sections = §ions + } + spinach := filepath.Join(cfg.K9sHome, "spinach.yml") + if c, err := p.Factory.Client().Config().CurrentContextName(); err == nil { + spinach = filepath.Join(cfg.K9sHome, fmt.Sprintf("%s_spinach.yml", c)) + } + if _, err := os.Stat(spinach); err == nil { + flags.Spinach = &spinach + } + + popeye, err := pkg.NewPopeye(flags, &log.Logger) + if err != nil { + return nil, err + } + popeye.SetFactory(newPopFactory(p.Factory)) + if err = popeye.Init(); err != nil { + return nil, err + } + + buff := readWriteCloser{Buffer: bytes.NewBufferString("")} + popeye.SetOutputTarget(buff) + if err = popeye.Sanitize(); err != nil { + log.Debug().Msgf("BOOM %#v", *flags.Sections) + return nil, err + } + + var b render.Builder + if err = json.Unmarshal(buff.Bytes(), &b); err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(b.Report.Sections)) + sort.Sort(b.Report.Sections) + for _, s := range b.Report.Sections { + s.Tally.Count = len(s.Outcome) + if s.Tally.Sum() > 0 { + oo = append(oo, s) + } + } + + return oo, nil +} + +// Get retrieves a resource. +func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) { + return nil, errors.New("NYI!!") +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type popFactory struct { + Factory +} + +var _ types.Factory = (*popFactory)(nil) + +func newPopFactory(f Factory) *popFactory { + return &popFactory{Factory: f} +} +func (p *popFactory) Client() types.Connection { + return &popConnection{Connection: p.Factory.Client()} +} + +type popConnection struct { + client.Connection +} + +var _ types.Connection = (*popConnection)(nil) + +func (c *popConnection) Config() types.Config { + return c.Connection.Config() +} diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index f91175df..dd4d0a83 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -126,7 +126,7 @@ func (p *PortForwarder) Start(path, co string, t client.PortTunnel) (*portforwar return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase) } - auth, err = p.Client().CanI(ns, "v1/pods:portforward", []string{client.UpdateVerb}) + auth, err = p.Client().CanI(ns, "v1/pods:portforward", []string{client.CreateVerb}) if err != nil { return nil, err } diff --git a/internal/dao/rbac.go b/internal/dao/rbac.go index 238abd32..6b5802b6 100644 --- a/internal/dao/rbac.go +++ b/internal/dao/rbac.go @@ -7,6 +7,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -119,6 +120,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { } func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) { + log.Debug().Msgf("LOAD-CR %q", path) o, err := r.Factory.Get(crGVR, path, true, labels.Everything()) if err != nil { return nil, err diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 2486f550..d0fd7feb 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -47,8 +47,10 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { client.NewGVR("apps/v1/statefulsets"): &StatefulSet{}, client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{}, client.NewGVR("batch/v1/jobs"): &Job{}, - client.NewGVR("charts"): &Chart{}, client.NewGVR("openfaas"): &OpenFaas{}, + client.NewGVR("popeye"): &Popeye{}, + client.NewGVR("sanitizer"): &Popeye{}, + client.NewGVR("helm"): &Helm{}, } r, ok := m[gvr] @@ -163,6 +165,20 @@ func loadK9s(m ResourceMetas) { Verbs: []string{}, Categories: []string{"k9s"}, } + m[client.NewGVR("popeye")] = metav1.APIResource{ + Name: "popeye", + Kind: "Popeye", + SingularName: "popeye", + Verbs: []string{}, + Categories: []string{"k9s"}, + } + m[client.NewGVR("sanitizer")] = metav1.APIResource{ + Name: "sanitizer", + Kind: "Sanitizer", + SingularName: "sanitizer", + Verbs: []string{}, + Categories: []string{"k9s"}, + } m[client.NewGVR("contexts")] = metav1.APIResource{ Name: "contexts", Kind: "Contexts", @@ -206,9 +222,9 @@ func loadK9s(m ResourceMetas) { } func loadHelm(m ResourceMetas) { - m[client.NewGVR("charts")] = metav1.APIResource{ - Name: "charts", - Kind: "Charts", + m[client.NewGVR("helm")] = metav1.APIResource{ + Name: "helm", + Kind: "Helm", Namespaced: true, Verbs: []string{"delete"}, Categories: []string{"helm"}, diff --git a/internal/dao/resource.go b/internal/dao/resource.go index cf459470..0f00cf8b 100644 --- a/internal/dao/resource.go +++ b/internal/dao/resource.go @@ -22,10 +22,12 @@ type Resource struct { // List returns a collection of resources. func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error) { - strLabel, ok := ctx.Value(internal.KeyLabels).(string) + strLabel, _ := ctx.Value(internal.KeyLabels).(string) lsel := labels.Everything() - if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil { - lsel = sel.AsSelector() + if strLabel != "" { + if sel, err := labels.Parse(strLabel); err == nil { + lsel = sel + } } return r.Factory.List(r.gvr.String(), ns, false, lsel) diff --git a/internal/dao/rs.go b/internal/dao/rs.go index 749e4803..0ab1c7c4 100644 --- a/internal/dao/rs.go +++ b/internal/dao/rs.go @@ -13,6 +13,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/polymorphichelpers" ) @@ -94,7 +95,7 @@ func (r *ReplicaSet) Rollback(fqn string) error { return err } - _, err = rb.Rollback(dp, map[string]string{}, version, false) + _, err = rb.Rollback(dp, map[string]string{}, version, cmdutil.DryRunNone) if err != nil { return err } diff --git a/internal/dao/sts.go b/internal/dao/sts.go index 8f599f3a..025866b0 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -35,7 +35,7 @@ func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool { } // Scale a StatefulSet. -func (s *StatefulSet) Scale(path string, replicas int32) error { +func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error { ns, n := client.Namespaced(path) auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", []string{client.GetVerb, client.UpdateVerb}) if err != nil { @@ -45,18 +45,18 @@ func (s *StatefulSet) Scale(path string, replicas int32) error { return fmt.Errorf("user is not authorized to scale statefulsets") } - scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(n, metav1.GetOptions{}) + scale, err := s.Client().DialOrDie().AppsV1().StatefulSets(ns).GetScale(ctx, n, metav1.GetOptions{}) if err != nil { return err } scale.Spec.Replicas = replicas - _, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(n, scale) + _, err = s.Client().DialOrDie().AppsV1().StatefulSets(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{}) return err } // Restart a StatefulSet rollout. -func (s *StatefulSet) Restart(path string) error { +func (s *StatefulSet) Restart(ctx context.Context, path string) error { sts, err := s.getStatefulSet(path) if err != nil { return err @@ -76,12 +76,18 @@ func (s *StatefulSet) Restart(path string) error { return err } - _, err = s.Client().DialOrDie().AppsV1().StatefulSets(sts.Namespace).Patch(sts.Name, types.StrategicMergePatchType, update) + _, err = s.Client().DialOrDie().AppsV1().StatefulSets(sts.Namespace).Patch( + ctx, + sts.Name, + types.StrategicMergePatchType, + update, + metav1.PatchOptions{}, + ) return err } // TailLogs tail logs for all pods represented by this StatefulSet. -func (s *StatefulSet) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { +func (s *StatefulSet) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error { sts, err := s.getStatefulSet(opts.Path) if err != nil { return errors.New("expecting StatefulSet resource") diff --git a/internal/dao/svc.go b/internal/dao/svc.go index f900119d..5b4b7dba 100644 --- a/internal/dao/svc.go +++ b/internal/dao/svc.go @@ -24,7 +24,7 @@ type Service struct { } // TailLogs tail logs for all pods represented by this Service. -func (s *Service) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error { +func (s *Service) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error { svc, err := s.GetInstance(opts.Path) if err != nil { return err diff --git a/internal/dao/table.go b/internal/dao/table.go index 290f2201..6b49a06f 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -21,8 +21,6 @@ type Table struct { // Get returns a given resource. func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { - ns, n := client.Namespaced(path) - a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) _, codec := t.codec() @@ -30,18 +28,17 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { if err != nil { return nil, err } - o, err := c.Get(). + ns, n := client.Namespaced(path) + req := c.Get(). SetHeader("Accept", a). - Namespace(ns). Name(n). Resource(t.gvr.R()). - VersionedParams(&metav1beta1.TableOptions{}, codec). - Do().Get() - if err != nil { - return nil, err + VersionedParams(&metav1beta1.TableOptions{}, codec) + if ns != client.ClusterScope { + req = req.Namespace(ns) } - return o, nil + return req.Do(ctx).Get() } // List all Resources in a given namespace. @@ -63,7 +60,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { Namespace(ns). Resource(t.gvr.R()). VersionedParams(&metav1.ListOptions{LabelSelector: labelSel}, codec). - Do().Get() + Do(ctx).Get() if err != nil { return nil, err } diff --git a/internal/dao/types.go b/internal/dao/types.go index 0a62f0f7..0362f19d 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -93,7 +93,7 @@ type NodeMaintainer interface { // Loggable represents resources with logs. type Loggable interface { // TaiLogs streams resource logs. - TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error + TailLogs(ctx context.Context, c LogChan, opts LogOptions) error } // Describer describes a resource. @@ -108,7 +108,7 @@ type Describer interface { // Scalable represents resources that can scale. type Scalable interface { // Scale scales a resource up or down. - Scale(path string, replicas int32) error + Scale(ctx context.Context, path string, replicas int32) error } // Controller represents a pod controller. @@ -132,7 +132,7 @@ type Switchable interface { // Restartable represents a restartable resource. type Restartable interface { // Restart performs a rollout restart. - Restart(path string) error + Restart(ctx context.Context, path string) error } // Runnable represents a runnable resource. diff --git a/internal/model/cluster.go b/internal/model/cluster.go index 59f7fb7b..b23122b7 100644 --- a/internal/model/cluster.go +++ b/internal/model/cluster.go @@ -1,6 +1,8 @@ package model import ( + "context" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" v1 "k8s.io/api/core/v1" @@ -20,8 +22,8 @@ type ( // MetricsService calls the metrics server for metrics info. MetricsService interface { HasMetrics() bool - FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) - FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error) + FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error) + FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error) } // Cluster represents a kubernetes resource. @@ -77,13 +79,13 @@ func (c *Cluster) UserName() string { } // Metrics gathers node level metrics and compute utilization percentages. -func (c *Cluster) Metrics(mx *client.ClusterMetrics) error { - nn, err := dao.FetchNodes(c.factory, "") +func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error { + nn, err := dao.FetchNodes(ctx, c.factory, "") if err != nil { return err } - nmx, err := c.mx.FetchNodesMetrics() + nmx, err := c.mx.FetchNodesMetrics(ctx) if err != nil { return err } diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 01198014..f654a2f3 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -1,6 +1,8 @@ package model import ( + "context" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" ) @@ -90,8 +92,10 @@ func (c *ClusterInfo) Refresh() { data.K9sVer = c.version data.K8sVer = c.cluster.Version() + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() var mx client.ClusterMetrics - if err := c.cluster.Metrics(&mx); err == nil { + if err := c.cluster.Metrics(ctx, &mx); err == nil { data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral } diff --git a/internal/model/cmd_buff.go b/internal/model/cmd_buff.go new file mode 100644 index 00000000..83960a3b --- /dev/null +++ b/internal/model/cmd_buff.go @@ -0,0 +1,168 @@ +package model + +const maxBuff = 10 + +const ( + // CommandBuffer represents a command buffer. + CommandBuffer BufferKind = 1 << iota + // FilterBuffer represents a filter buffer. + FilterBuffer +) + +// BufferKind indicates a buffer type +type BufferKind int8 + +// BuffWatcher represents a command buffer listener. +type BuffWatcher interface { + // Changed indicates the buffer was changed. + BufferChanged(s string) + + // Active indicates the buff activity changed. + BufferActive(state bool, kind BufferKind) +} + +// CmdBuff represents user command input. +type CmdBuff struct { + buff []rune + listeners []BuffWatcher + hotKey rune + kind BufferKind + active bool +} + +// NewCmdBuff returns a new command buffer. +func NewCmdBuff(key rune, kind BufferKind) *CmdBuff { + return &CmdBuff{ + hotKey: key, + kind: kind, + buff: make([]rune, 0, maxBuff), + listeners: []BuffWatcher{}, + } +} + +// CurrentSuggestion returns the current suggestion. +func (c *CmdBuff) CurrentSuggestion() (string, bool) { + return "", false +} + +// NextSuggestion returns the next suggestion. +func (c *CmdBuff) NextSuggestion() (string, bool) { + return "", false +} + +// PrevSuggestion returns the prev suggestion. +func (c *CmdBuff) PrevSuggestion() (string, bool) { + return "", false +} + +// ClearSuggestions clear out all suggestions. +func (c *CmdBuff) ClearSuggestions() {} + +// AutoSuggests returns true if model implements auto suggestions. +func (c *CmdBuff) AutoSuggests() bool { + return false +} + +// Suggestions returns suggestions. +func (c *CmdBuff) Suggestions() []string { + return nil +} + +// Notify notifies all listener of current suggestions. +func (c *CmdBuff) Notify() {} + +// InCmdMode checks if a command exists and the buffer is active. +func (c *CmdBuff) InCmdMode() bool { + return c.active || len(c.buff) > 0 +} + +// IsActive checks if command buffer is active. +func (c *CmdBuff) IsActive() bool { + return c.active +} + +// SetActive toggles cmd buffer active state. +func (c *CmdBuff) SetActive(b bool) { + c.active = b + c.fireActive(c.active) +} + +// GetText returns the current text. +func (c *CmdBuff) GetText() string { + return string(c.buff) +} + +// SetText initializes the buffer with a command. +func (c *CmdBuff) SetText(cmd string) { + c.buff = []rune(cmd) + c.fireBufferChanged() +} + +// Add adds a new charater to the buffer. +func (c *CmdBuff) Add(r rune) { + c.buff = append(c.buff, r) + c.fireBufferChanged() +} + +// Delete removes the last character from the buffer. +func (c *CmdBuff) Delete() { + if c.Empty() { + return + } + c.buff = c.buff[:len(c.buff)-1] + c.fireBufferChanged() +} + +// ClearText clears out command buffer. +func (c *CmdBuff) ClearText() { + c.buff = make([]rune, 0, maxBuff) + c.fireBufferChanged() +} + +// Reset clears out the command buffer and deactivates it. +func (c *CmdBuff) Reset() { + c.ClearText() + c.fireBufferChanged() + c.SetActive(false) +} + +// Empty returns true if no cmd, false otherwise. +func (c *CmdBuff) Empty() bool { + return len(c.buff) == 0 +} + +// ---------------------------------------------------------------------------- +// Event Listeners... + +// AddListener registers a cmd buffer listener. +func (c *CmdBuff) AddListener(w BuffWatcher) { + c.listeners = append(c.listeners, w) +} + +// RemoveListener unregisters a listener. +func (c *CmdBuff) RemoveListener(l BuffWatcher) { + victim := -1 + for i, lis := range c.listeners { + if l == lis { + victim = i + break + } + } + + if victim == -1 { + return + } + c.listeners = append(c.listeners[:victim], c.listeners[victim+1:]...) +} + +func (c *CmdBuff) fireBufferChanged() { + for _, l := range c.listeners { + l.BufferChanged(c.GetText()) + } +} + +func (c *CmdBuff) fireActive(b bool) { + for _, l := range c.listeners { + l.BufferActive(b, c.kind) + } +} diff --git a/internal/ui/cmd_buff_test.go b/internal/model/cmd_buff_test.go similarity index 72% rename from internal/ui/cmd_buff_test.go rename to internal/model/cmd_buff_test.go index 182f6109..34719773 100644 --- a/internal/ui/cmd_buff_test.go +++ b/internal/model/cmd_buff_test.go @@ -1,9 +1,9 @@ -package ui_test +package model_test import ( "testing" - "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/model" "github.com/stretchr/testify/assert" ) @@ -17,7 +17,7 @@ func (l *testListener) BufferChanged(s string) { l.text = s } -func (l *testListener) BufferActive(s bool, _ ui.BufferKind) { +func (l *testListener) BufferActive(s bool, _ model.BufferKind) { if s { l.act++ return @@ -26,7 +26,7 @@ func (l *testListener) BufferActive(s bool, _ ui.BufferKind) { } func TestCmdBuffActivate(t *testing.T) { - b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} + b, l := model.NewCmdBuff('>', model.CommandBuffer), testListener{} b.AddListener(&l) b.SetActive(true) @@ -36,7 +36,7 @@ func TestCmdBuffActivate(t *testing.T) { } func TestCmdBuffDeactivate(t *testing.T) { - b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} + b, l := model.NewCmdBuff('>', model.CommandBuffer), testListener{} b.AddListener(&l) b.SetActive(false) @@ -46,39 +46,39 @@ func TestCmdBuffDeactivate(t *testing.T) { } func TestCmdBuffChanged(t *testing.T) { - b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{} + b, l := model.NewCmdBuff('>', model.CommandBuffer), testListener{} b.AddListener(&l) b.Add('b') assert.Equal(t, 0, l.act) assert.Equal(t, 0, l.inact) assert.Equal(t, "b", l.text) - assert.Equal(t, "b", b.String()) + assert.Equal(t, "b", b.GetText()) b.Delete() assert.Equal(t, 0, l.act) assert.Equal(t, 0, l.inact) assert.Equal(t, "", l.text) - assert.Equal(t, "", b.String()) + assert.Equal(t, "", b.GetText()) b.Add('c') - b.Clear() + b.ClearText() assert.Equal(t, 0, l.act) assert.Equal(t, 0, l.inact) assert.Equal(t, "", l.text) - assert.Equal(t, "", b.String()) + assert.Equal(t, "", b.GetText()) b.Add('c') b.Reset() assert.Equal(t, 0, l.act) assert.Equal(t, 1, l.inact) assert.Equal(t, "", l.text) - assert.Equal(t, "", b.String()) + assert.Equal(t, "", b.GetText()) assert.True(t, b.Empty()) } func TestCmdBuffAdd(t *testing.T) { - b := ui.NewCmdBuff('>', ui.CommandBuff) + b := model.NewCmdBuff('>', model.CommandBuffer) uu := []struct { runes []rune @@ -93,13 +93,13 @@ func TestCmdBuffAdd(t *testing.T) { for _, r := range u.runes { b.Add(r) } - assert.Equal(t, u.cmd, b.String()) + assert.Equal(t, u.cmd, b.GetText()) b.Reset() } } func TestCmdBuffDel(t *testing.T) { - b := ui.NewCmdBuff('>', ui.CommandBuff) + b := model.NewCmdBuff('>', model.CommandBuffer) uu := []struct { runes []rune @@ -115,13 +115,13 @@ func TestCmdBuffDel(t *testing.T) { b.Add(r) } b.Delete() - assert.Equal(t, u.cmd, b.String()) + assert.Equal(t, u.cmd, b.GetText()) b.Reset() } } func TestCmdBuffEmpty(t *testing.T) { - b := ui.NewCmdBuff('>', ui.CommandBuff) + b := model.NewCmdBuff('>', model.CommandBuffer) uu := []struct { runes []rune diff --git a/internal/model/fish_buff.go b/internal/model/fish_buff.go new file mode 100644 index 00000000..4a9a7cd8 --- /dev/null +++ b/internal/model/fish_buff.go @@ -0,0 +1,141 @@ +package model + +import ( + "sort" +) + +// SuggestionListener listens for suggestions. +type SuggestionListener interface { + BuffWatcher + + // SuggestionChanged notifies suggestion changes. + SuggestionChanged(text, sugg string) +} + +// SuggestionFunc produces suggestions. +type SuggestionFunc func(text string) sort.StringSlice + +// FishBuff represents a suggestion buffer. +type FishBuff struct { + *CmdBuff + + suggestionFn SuggestionFunc + suggestion string + suggestions []string + suggestionIndex int +} + +// NewFishBuff returns a new command buffer. +func NewFishBuff(key rune, kind BufferKind) *FishBuff { + return &FishBuff{ + CmdBuff: NewCmdBuff(key, kind), + suggestionIndex: -1, + } +} + +// PrevSuggestion returns the prev suggestion. +func (f *FishBuff) PrevSuggestion() (string, bool) { + if f.suggestionIndex < 0 { + return "", false + } + f.suggestionIndex-- + if f.suggestionIndex < 0 { + f.suggestionIndex = len(f.suggestions) - 1 + } + return f.suggestions[f.suggestionIndex], true +} + +// NextSuggestion returns the next suggestion. +func (f *FishBuff) NextSuggestion() (string, bool) { + if f.suggestionIndex < 0 { + return "", false + } + + if f.suggestionIndex >= len(f.suggestions) { + f.suggestionIndex = 0 + } + s := f.suggestions[f.suggestionIndex] + f.suggestionIndex++ + + return s, true +} + +// ClearSuggestions clear out all suggestions. +func (f *FishBuff) ClearSuggestions() { + f.suggestion, f.suggestionIndex = "", -1 +} + +// CurrentSuggestion returns the current suggestion. +func (f *FishBuff) CurrentSuggestion() (string, bool) { + if f.suggestionIndex < 0 { + return "", false + } + + if f.suggestionIndex >= len(f.suggestions) { + return "", false + } + + return f.suggestions[f.suggestionIndex], true +} + +// AutoSuggests returns true if model implements auto suggestions. +func (f *FishBuff) AutoSuggests() bool { + return true +} + +// Suggestions returns suggestions. +func (f *FishBuff) Suggestions() []string { + if f.suggestionFn != nil { + return f.suggestionFn(string(f.buff)) + } + return nil +} + +// SetSuggestionFn sets up suggestions. +func (f *FishBuff) SetSuggestionFn(fn SuggestionFunc) { + f.suggestionFn = fn +} + +// Notify publish suggestions to all listeners. +func (f *FishBuff) Notify() { + if f.suggestionFn == nil { + return + } + cc := f.suggestionFn(string(f.buff)) + f.fireSuggestionChanged(cc) +} + +// Add adds a new charater to the buffer. +func (f *FishBuff) Add(r rune) { + f.CmdBuff.Add(r) + if f.suggestionFn == nil { + return + } + cc := f.suggestionFn(string(f.buff)) + f.fireSuggestionChanged(cc) +} + +// Delete removes the last character from the buffer. +func (f *FishBuff) Delete() { + f.CmdBuff.Delete() + if f.suggestionFn == nil { + return + } + cc := f.suggestionFn(string(f.buff)) + f.fireSuggestionChanged(cc) +} + +func (f *FishBuff) fireSuggestionChanged(ss []string) { + f.suggestions, f.suggestionIndex = ss, 0 + if len(ss) == 0 { + f.suggestionIndex, f.suggestion = -1, "" + return + } + f.suggestion = ss[f.suggestionIndex] + + for _, l := range f.listeners { + if listener, ok := l.(SuggestionListener); ok { + listener.SuggestionChanged(f.GetText(), f.suggestion) + } + } +} diff --git a/internal/model/history.go b/internal/model/history.go new file mode 100644 index 00000000..3561ff8f --- /dev/null +++ b/internal/model/history.go @@ -0,0 +1,62 @@ +package model + +import ( + "strings" +) + +// MaxHistory tracks max command history +const MaxHistory = 20 + +// History represents a command history. +type History struct { + commands []string + limit int +} + +// NewHistory returns a new instance. +func NewHistory(limit int) *History { + return &History{ + limit: limit, + } +} + +// List returns the current command history. +func (h *History) List() []string { + return h.commands +} + +// Push adds a new item. +func (h *History) Push(c string) { + if c == "" { + return + } + + c = strings.ToLower(c) + if i := h.indexOf(c); i != -1 { + return + } + if len(h.commands) < h.limit { + h.commands = append([]string{c}, h.commands...) + return + } + h.commands = append([]string{c}, h.commands[:len(h.commands)-1]...) +} + +// Clear clear out the stack using pops. +func (h *History) Clear() { + h.commands = nil +} + +// Empty returns true if no history. +func (h *History) Empty() bool { + return len(h.commands) == 0 +} + +func (h *History) indexOf(s string) int { + for i, c := range h.commands { + if c == s { + return i + } + } + return -1 +} diff --git a/internal/model/history_test.go b/internal/model/history_test.go new file mode 100644 index 00000000..d4fade17 --- /dev/null +++ b/internal/model/history_test.go @@ -0,0 +1,31 @@ +package model_test + +import ( + "fmt" + "testing" + + "github.com/derailed/k9s/internal/model" + "github.com/stretchr/testify/assert" +) + +func TestHistory(t *testing.T) { + h := model.NewHistory(3) + for i := 1; i < 5; i++ { + h.Push(fmt.Sprintf("cmd%d", i)) + } + + assert.Equal(t, []string{"cmd4", "cmd3", "cmd2"}, h.List()) + h.Clear() + assert.True(t, h.Empty()) +} + +func TestHistoryDups(t *testing.T) { + h := model.NewHistory(3) + for i := 1; i < 4; i++ { + h.Push(fmt.Sprintf("cmd%d", i)) + } + h.Push("cmd1") + h.Push("") + + assert.Equal(t, []string{"cmd3", "cmd2", "cmd1"}, h.List()) +} diff --git a/internal/model/log.go b/internal/model/log.go index 5bba7ad6..6eaa1880 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -3,24 +3,20 @@ package model import ( "context" "fmt" - "regexp" - "strings" "sync" "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" - "github.com/sahilm/fuzzy" ) -const logMaxBufferSize = 100 - // LogsListener represents a log model listener. type LogsListener interface { // LogChanged notifies the model changed. - LogChanged([]string) + LogChanged(dao.LogItems) // LogCleanred indicates logs are cleared. LogCleared() @@ -31,29 +27,52 @@ 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 - mx sync.RWMutex - filter string - lastSent int - showTimestamp bool - timeOut time.Duration + factory dao.Factory + lines dao.LogItems + listeners []LogsListener + gvr client.GVR + logOptions dao.LogOptions + cancelFn context.CancelFunc + mx sync.RWMutex + filter string + lastSent int + flushTimeout time.Duration } // NewLog returns a new model. -func NewLog(gvr client.GVR, opts dao.LogOptions, timeOut time.Duration) *Log { +func NewLog(gvr client.GVR, opts dao.LogOptions, flushTimeout time.Duration) *Log { return &Log{ - gvr: gvr, - logOptions: opts, - lines: nil, - timeOut: timeOut, + gvr: gvr, + logOptions: opts, + lines: nil, + flushTimeout: flushTimeout, } } +// LogOptions returns the current log options. +func (l *Log) LogOptions() dao.LogOptions { + return l.logOptions +} + +// SinceSeconds returns since seconds option. +func (l *Log) SinceSeconds() int64 { + l.mx.RLock() + defer l.mx.RUnlock() + return l.logOptions.SinceSeconds +} + +// SetLogOptions updates logger options. +func (l *Log) SetLogOptions(opts dao.LogOptions) { + l.logOptions = opts + l.Restart() +} + +// Configure sets logger configuration. +func (l *Log) Configure(opts *config.Logger) { + l.logOptions.Lines = int64(opts.BufferSize) + l.logOptions.SinceSeconds = opts.SinceSeconds +} + // GetPath returns resource path. func (l *Log) GetPath() string { return l.logOptions.Path } @@ -69,22 +88,25 @@ func (l *Log) Init(f dao.Factory) { func (l *Log) Clear() { l.mx.Lock() { - l.lines, l.lastSent = []string{}, 0 + l.lines, l.lastSent = dao.LogItems{}, 0 } l.mx.Unlock() l.fireLogCleared() } -// ShowTimestamp toggles timestamp on logs. -func (l *Log) ShowTimestamp(b bool) { - l.mx.RLock() - defer l.mx.RUnlock() - - l.showTimestamp = b +// Refresh refreshes the logs. +func (l *Log) Refresh() { l.fireLogCleared() l.fireLogChanged(l.lines) } +// Restart restarts the logger. +func (l *Log) Restart() { + l.Clear() + l.Stop() + l.Start() +} + // Start initialize log tailer. func (l *Log) Start() { if err := l.load(); err != nil { @@ -103,21 +125,21 @@ func (l *Log) Stop() { } // Set sets the log lines (for testing only!) -func (l *Log) Set(lines []string) { +func (l *Log) Set(items dao.LogItems) { l.mx.Lock() defer l.mx.Unlock() - - l.lines = lines - l.fireLogChanged(lines) + l.lines = items + l.fireLogCleared() + l.fireLogChanged(items) } // ClearFilter resets the log filter if any. func (l *Log) ClearFilter() { - log.Debug().Msgf("CLEARED!!") l.mx.RLock() defer l.mx.RUnlock() l.filter = "" + l.fireLogCleared() l.fireLogChanged(l.lines) } @@ -126,14 +148,9 @@ func (l *Log) Filter(q string) error { l.mx.RLock() defer l.mx.RUnlock() - log.Debug().Msgf("FILTER!") l.filter = q - filtered, err := applyFilter(l.filter, l.lines) - if err != nil { - return err - } l.fireLogCleared() - l.fireLogChanged(filtered) + l.fireLogBuffChanged(l.lines) return nil } @@ -143,7 +160,7 @@ func (l *Log) load() error { ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory) ctx, l.cancelFn = context.WithCancel(ctx) - c := make(chan []byte, 10) + c := make(dao.LogChan, 10) go l.updateLogs(ctx, c) accessor, err := dao.AccessorFor(l.factory, l.gvr) @@ -155,10 +172,10 @@ func (l *Log) load() error { return fmt.Errorf("Resource %s is not Loggable", l.gvr) } if err := logger.TailLogs(ctx, c, l.logOptions); err != nil { + log.Error().Err(err).Msgf("Tail logs failed") if l.cancelFn != nil { l.cancelFn() } - close(c) return err } @@ -166,14 +183,15 @@ func (l *Log) load() error { } // Append adds a log line. -func (l *Log) Append(line string) { - if line == "" { +func (l *Log) Append(line *dao.LogItem) { + if line == nil || line.IsEmpty() { return } l.mx.Lock() defer l.mx.Unlock() + l.logOptions.SinceTime = line.Timestamp if l.lines == nil { l.fireLogCleared() } @@ -200,30 +218,30 @@ func (l *Log) Notify(timedOut bool) { } } -func (l *Log) updateLogs(ctx context.Context, c <-chan []byte) { +func (l *Log) updateLogs(ctx context.Context, c dao.LogChan) { defer func() { log.Debug().Msgf("updateLogs view bailing out!") }() for { select { - case bytes, ok := <-c: + case item, ok := <-c: if !ok { log.Debug().Msgf("Closed channel detected. Bailing out...") - l.Append(string(bytes)) - l.Notify(false) + l.Append(item) + l.Notify(true) return } - l.Append(string(bytes)) + l.Append(item) var overflow bool l.mx.RLock() { - overflow = len(l.lines)-l.lastSent > logMaxBufferSize + overflow = int64(len(l.lines)-l.lastSent) > l.logOptions.Lines } l.mx.RUnlock() if overflow { l.Notify(true) } - case <-time.After(l.timeOut): + case <-time.After(l.flushTimeout): l.Notify(true) case <-ctx.Done(): return @@ -251,11 +269,11 @@ func (l *Log) RemoveListener(listener LogsListener) { } } -func applyFilter(q string, lines []string) ([]string, error) { +func applyFilter(q string, lines dao.LogItems) (dao.LogItems, error) { if q == "" { return lines, nil } - indexes, err := filter(q, lines) + indexes, err := lines.Filter(q) if err != nil { return nil, err } @@ -267,7 +285,7 @@ func applyFilter(q string, lines []string) ([]string, error) { if len(indexes) == 0 { return nil, nil } - filtered := make([]string, 0, len(indexes)) + filtered := make(dao.LogItems, 0, len(indexes)) for _, idx := range indexes { filtered = append(filtered, lines[idx]) } @@ -275,7 +293,7 @@ func applyFilter(q string, lines []string) ([]string, error) { return filtered, nil } -func (l *Log) fireLogBuffChanged(lines []string) { +func (l *Log) fireLogBuffChanged(lines dao.LogItems) { filtered, err := applyFilter(l.filter, lines) if err != nil { l.fireLogError(err) @@ -292,7 +310,7 @@ func (l *Log) fireLogError(err error) { } } -func (l *Log) fireLogChanged(lines []string) { +func (l *Log) fireLogChanged(lines dao.LogItems) { for _, lis := range l.listeners { lis.LogChanged(lines) } @@ -303,55 +321,3 @@ func (l *Log) fireLogCleared() { lis.LogCleared() } } - -// ---------------------------------------------------------------------------- -// Helpers... - -var fuzzyRx = regexp.MustCompile(`\A\-f`) - -func isFuzzySelector(s string) bool { - if s == "" { - return false - } - return fuzzyRx.MatchString(s) -} - -func filter(q string, lines []string) ([]int, error) { - if q == "" { - return nil, nil - } - if isFuzzySelector(q) { - return fuzzyFilter(strings.TrimSpace(q[2:]), lines), nil - } - indexes, err := filterLogs(q, lines) - if err != nil { - log.Error().Err(err).Msgf("Logs filter failed") - return nil, err - } - return indexes, nil -} - -func fuzzyFilter(q string, lines []string) []int { - matches := make([]int, 0, len(lines)) - mm := fuzzy.Find(q, lines) - for _, m := range mm { - matches = append(matches, m.Index) - } - - return matches -} - -func filterLogs(q string, lines []string) ([]int, error) { - rx, err := regexp.Compile(`(?i)` + q) - if err != nil { - return nil, err - } - matches := make([]int, 0, len(lines)) - for i, l := range lines { - if rx.MatchString(l) { - matches = append(matches, i) - } - } - - return matches, nil -} diff --git a/internal/model/log_test.go b/internal/model/log_test.go index d6ac27d7..9859a4f2 100644 --- a/internal/model/log_test.go +++ b/internal/model/log_test.go @@ -24,9 +24,9 @@ func TestLogFullBuffer(t *testing.T) { v := newTestView() m.AddListener(v) - data := make([]string, 0, 2*size) + data := make(dao.LogItems, 0, 2*size) for i := 0; i < 2*size; i++ { - data = append(data, "line"+strconv.Itoa(i)) + data = append(data, dao.NewLogItemFromString("line"+strconv.Itoa(i))) m.Append(data[i]) } m.Notify(true) @@ -47,8 +47,8 @@ func TestLogFilter(t *testing.T) { e: 2, }, "regexp": { - q: `\Apod-line-[1-3]{1}\z`, - e: 3, + q: `pod-line-[1-3]{1}`, + e: 4, }, "fuzzy": { q: `-f po-l1`, @@ -67,21 +67,21 @@ func TestLogFilter(t *testing.T) { m.AddListener(v) m.Filter(u.q) - var data []string + var data dao.LogItems for i := 0; i < size; i++ { - data = append(data, fmt.Sprintf("pod-line-%d", i+1)) + data = append(data, dao.NewLogItemFromString(fmt.Sprintf("pod-line-%d", i+1))) m.Append(data[i]) } m.Notify(true) - assert.Equal(t, 2, v.dataCalled) + assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, u.e, len(v.data)) m.ClearFilter() - assert.Equal(t, 3, v.dataCalled) - assert.Equal(t, 2, v.clearCalled) + assert.Equal(t, 2, v.dataCalled) + assert.Equal(t, 3, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, size, len(v.data)) }) @@ -96,7 +96,7 @@ func TestLogStartStop(t *testing.T) { m.AddListener(v) m.Start() - data := []string{"line1", "line2"} + data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")} for _, d := range data { m.Append(d) } @@ -118,7 +118,7 @@ func TestLogClear(t *testing.T) { v := newTestView() m.AddListener(v) - data := []string{"line1", "line2"} + data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")} for _, d := range data { m.Append(d) } @@ -138,11 +138,11 @@ func TestLogBasic(t *testing.T) { v := newTestView() m.AddListener(v) - data := []string{"line1", "line2"} + data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")} m.Set(data) assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, 0, v.clearCalled) + assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) assert.Equal(t, data, v.data) } @@ -153,21 +153,25 @@ func TestLogAppend(t *testing.T) { v := newTestView() m.AddListener(v) - m.Set([]string{"blah blah"}) - assert.Equal(t, []string{"blah blah"}, v.data) + items := dao.LogItems{dao.NewLogItemFromString("blah blah")} + m.Set(items) + assert.Equal(t, items, v.data) - data := []string{"line1", "line2"} + data := dao.LogItems{ + dao.NewLogItemFromString("line1"), + dao.NewLogItemFromString("line2"), + } for _, d := range data { m.Append(d) } assert.Equal(t, 1, v.dataCalled) - assert.Equal(t, []string{"blah blah"}, v.data) + assert.Equal(t, items, v.data) m.Notify(true) assert.Equal(t, 2, v.dataCalled) - assert.Equal(t, 0, v.clearCalled) + assert.Equal(t, 1, v.clearCalled) assert.Equal(t, 0, v.errCalled) - assert.Equal(t, append([]string{"blah blah"}, data...), v.data) + assert.Equal(t, append(items, data...), v.data) } func TestLogTimedout(t *testing.T) { @@ -178,15 +182,20 @@ func TestLogTimedout(t *testing.T) { m.AddListener(v) m.Filter("line1") - data := []string{"line1", "line2", "line3", "line4"} + data := dao.LogItems{ + dao.NewLogItemFromString("line1"), + dao.NewLogItemFromString("line2"), + dao.NewLogItemFromString("line3"), + dao.NewLogItemFromString("line4"), + } for _, d := range data { m.Append(d) } m.Notify(true) - assert.Equal(t, 2, v.dataCalled) + assert.Equal(t, 1, v.dataCalled) assert.Equal(t, 2, v.clearCalled) assert.Equal(t, 0, v.errCalled) - assert.Equal(t, []string{"line1"}, v.data) + assert.Equal(t, dao.LogItems{data[0]}, v.data) } // ---------------------------------------------------------------------------- @@ -203,7 +212,7 @@ func makeLogOpts(count int) dao.LogOptions { // ---------------------------------------------------------------------------- type testView struct { - data []string + data dao.LogItems dataCalled int clearCalled int errCalled int @@ -213,13 +222,13 @@ func newTestView() *testView { return &testView{} } -func (t *testView) LogChanged(d []string) { +func (t *testView) LogChanged(d dao.LogItems) { t.data = d t.dataCalled++ } func (t *testView) LogCleared() { t.clearCalled++ - t.data = []string{} + t.data = dao.LogItems{} } func (t *testView) LogFailed(err error) { fmt.Println("LogErr", err) diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go index d7ddf87a..ec7dcf8a 100644 --- a/internal/model/pulse_health.go +++ b/internal/model/pulse_health.go @@ -51,7 +51,7 @@ func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, er hh = append(hh, c) } - mm, err := h.checkMetrics() + mm, err := h.checkMetrics(ctx) if err != nil { return hh, nil } @@ -62,15 +62,15 @@ func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, er return hh, nil } -func (h *PulseHealth) checkMetrics() (health.Checks, error) { +func (h *PulseHealth) checkMetrics(ctx context.Context) (health.Checks, error) { dial := client.DialMetrics(h.factory.Client()) - nn, err := dao.FetchNodes(h.factory, "") + nn, err := dao.FetchNodes(ctx, h.factory, "") if err != nil { return nil, err } - nmx, err := dial.FetchNodesMetrics() + nmx, err := dial.FetchNodesMetrics(ctx) if err != nil { log.Error().Err(err).Msgf("Fetching metrics") return nil, err diff --git a/internal/model/registry.go b/internal/model/registry.go index 0dde91f1..80b39f3b 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -10,9 +10,9 @@ import ( // BOZO!! Break up deps and merge into single registrar var Registry = map[string]ResourceMeta{ // Custom... - "charts": { - DAO: &dao.Chart{}, - Renderer: &render.Chart{}, + "helm": { + DAO: &dao.Helm{}, + Renderer: &render.Helm{}, }, "pulses": { DAO: &dao.Pulse{}, @@ -62,6 +62,14 @@ var Registry = map[string]ResourceMeta{ DAO: &dao.Alias{}, Renderer: &render.Alias{}, }, + "popeye": { + DAO: &dao.Popeye{}, + Renderer: &render.Popeye{}, + }, + "sanitizer": { + DAO: &dao.Popeye{}, + TreeRenderer: &xray.Section{}, + }, // Core... "v1/endpoints": { diff --git a/internal/model/table.go b/internal/model/table.go index d6f72ad5..c0d1b087 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -179,7 +179,7 @@ func (t *Table) Peek() render.TableData { } func (t *Table) updater(ctx context.Context) { - defer log.Debug().Msgf("Model canceled -- %q", t.gvr) + defer log.Debug().Msgf("TABLE-MODEL canceled -- %q", t.gvr) rate := initRefreshRate for { @@ -290,7 +290,6 @@ func (t *Table) getMeta(ctx context.Context) (ResourceMeta, error) { func (t *Table) resourceMeta() ResourceMeta { meta, ok := Registry[t.gvr.String()] if !ok { - log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr) meta = ResourceMeta{ DAO: &dao.Table{}, Renderer: &render.Generic{}, diff --git a/internal/model/text.go b/internal/model/text.go index 1da28560..442b7567 100644 --- a/internal/model/text.go +++ b/internal/model/text.go @@ -4,6 +4,7 @@ import ( "regexp" "strings" + "github.com/derailed/k9s/internal/dao" "github.com/sahilm/fuzzy" ) @@ -94,7 +95,7 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if isFuzzySelector(q) { + if dao.IsFuzzySelector(q) { return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines) } return t.rxFilter(q, lines) diff --git a/internal/model/tree.go b/internal/model/tree.go index f28f4b0b..7a1723b7 100644 --- a/internal/model/tree.go +++ b/internal/model/tree.go @@ -219,6 +219,7 @@ func (t *Tree) reconcile(ctx context.Context) error { } } else { if err := treeHydrate(ctx, ns, oo, meta.TreeRenderer); err != nil { + return err } } @@ -238,7 +239,6 @@ func (t *Tree) reconcile(ctx context.Context) error { func (t *Tree) resourceMeta() ResourceMeta { meta, ok := Registry[t.gvr.String()] if !ok { - log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr) meta = ResourceMeta{ DAO: &dao.Table{}, Renderer: &render.Generic{}, diff --git a/internal/perf/benchmark.go b/internal/perf/benchmark.go index ea4a1249..204e5c9f 100644 --- a/internal/perf/benchmark.go +++ b/internal/perf/benchmark.go @@ -117,7 +117,6 @@ func (b *Benchmark) Run(cluster string, done func()) { // this call will block until the benchmark is complete or timesout. b.worker.Run() b.worker.Stop() - log.Debug().Msgf("YO!! %t %s", b.canceled, buff) if len(buff.Bytes()) > 0 { if err := b.save(cluster, buff); err != nil { log.Error().Err(err).Msg("Saving Benchmark") diff --git a/internal/render/delta.go b/internal/render/delta.go index 201e3e82..fd2b3fd7 100644 --- a/internal/render/delta.go +++ b/internal/render/delta.go @@ -24,6 +24,23 @@ func NewDeltaRow(o, n Row, excludeLast bool) DeltaRow { return deltas } +// Labelize returns a new deltaRow based on labels. +func (d DeltaRow) Labelize(cols []int, labelCol int) DeltaRow { + if len(d) == 0 { + return d + } + _, vals := sortLabels(labelize(d[labelCol])) + out := make(DeltaRow, 0, len(cols)+len(vals)) + for _, i := range cols { + out = append(out, d[i]) + } + for _, v := range vals { + out = append(out, v) + } + + return out +} + // Diff returns true if deltas differ or false otherwise. func (d DeltaRow) Diff(r DeltaRow, ageCol int) bool { if len(d) != len(r) { @@ -77,9 +94,7 @@ func (d DeltaRow) IsBlank() bool { // Clone returns a delta copy. func (d DeltaRow) Clone() DeltaRow { res := make(DeltaRow, len(d)) - for i, f := range d { - res[i] = f - } + copy(res, d) return res } diff --git a/internal/render/delta_test.go b/internal/render/delta_test.go index fba6ecf1..31e38da4 100644 --- a/internal/render/delta_test.go +++ b/internal/render/delta_test.go @@ -7,6 +7,33 @@ import ( "github.com/stretchr/testify/assert" ) +func TestDeltaLabelize(t *testing.T) { + uu := map[string]struct { + o render.Row + n render.Row + e render.DeltaRow + }{ + "same": { + o: render.Row{ + Fields: render.Fields{"a", "b", "blee=fred,doh=zorg"}, + }, + n: render.Row{ + Fields: render.Fields{"a", "b", "blee=fred1,doh=zorg"}, + }, + e: render.DeltaRow{"", "", "fred", "zorg"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + d := render.NewDeltaRow(u.o, u.n, false) + d = d.Labelize([]int{0, 1}, 2) + assert.Equal(t, u.e, d) + }) + } +} + func TestDeltaCustomize(t *testing.T) { uu := map[string]struct { r1, r2 render.Row diff --git a/internal/render/generic.go b/internal/render/generic.go index 9d8b9cdf..eeb35ee2 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -70,7 +70,6 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { if !ok { return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0]) } - r.ID = client.FQN(nns, n) r.Fields = make(Fields, 0, len(g.Header(ns))) r.Fields = append(r.Fields, nns) diff --git a/internal/render/header.go b/internal/render/header.go index 9a3dffa8..231ca2b5 100644 --- a/internal/render/header.go +++ b/internal/render/header.go @@ -39,6 +39,20 @@ func (h Header) Clone() Header { return header } +// Labelize returns a new Header based on labels. +func (h Header) Labelize(cols []int, labelCol int, rr RowEvents) Header { + header := make(Header, 0, len(cols)+1) + for _, c := range cols { + header = append(header, h[c]) + } + cc := rr.ExtractHeaderLabels(labelCol) + for _, c := range cc { + header = append(header, HeaderColumn{Name: c}) + } + + return header +} + // MapIndices returns a collection of mapped column indices based of the requested columns. func (h Header) MapIndices(cols []string, wide bool) []int { ii := make([]int, 0, len(cols)) diff --git a/internal/render/chart.go b/internal/render/helm.go similarity index 76% rename from internal/render/chart.go rename to internal/render/helm.go index bc1c4d90..c41e6f03 100644 --- a/internal/render/chart.go +++ b/internal/render/helm.go @@ -12,11 +12,11 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) -// Chart renders a helm chart to screen. -type Chart struct{} +// Helm renders a helm chart to screen. +type Helm struct{} // ColorerFunc colors a resource row. -func (Chart) ColorerFunc() ColorerFunc { +func (Helm) ColorerFunc() ColorerFunc { return func(ns string, h Header, re RowEvent) tcell.Color { if !Happy(ns, h, re.Row) { return ErrColor @@ -27,7 +27,7 @@ func (Chart) ColorerFunc() ColorerFunc { } // Header returns a header row. -func (Chart) Header(_ string) Header { +func (Helm) Header(_ string) Header { return Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, @@ -41,10 +41,10 @@ func (Chart) Header(_ string) Header { } // Render renders a chart to screen. -func (c Chart) Render(o interface{}, ns string, r *Row) error { - h, ok := o.(ChartRes) +func (c Helm) Render(o interface{}, ns string, r *Row) error { + h, ok := o.(HelmRes) if !ok { - return fmt.Errorf("expected ChartRes, but got %T", o) + return fmt.Errorf("expected HelmRes, but got %T", o) } r.ID = client.FQN(h.Release.Namespace, h.Release.Name) @@ -62,7 +62,7 @@ func (c Chart) Render(o interface{}, ns string, r *Row) error { return nil } -func (c Chart) diagnose(s string) error { +func (c Helm) diagnose(s string) error { if s != "deployed" { return fmt.Errorf("chart is in an invalid state") } @@ -73,17 +73,17 @@ func (c Chart) diagnose(s string) error { // ---------------------------------------------------------------------------- // Helpers... -// ChartRes represents an helm chart resource. -type ChartRes struct { +// HelmRes represents an helm chart resource. +type HelmRes struct { Release *release.Release } // GetObjectKind returns a schema object. -func (ChartRes) GetObjectKind() schema.ObjectKind { +func (HelmRes) GetObjectKind() schema.ObjectKind { return nil } // DeepCopyObject returns a container copy. -func (h ChartRes) DeepCopyObject() runtime.Object { +func (h HelmRes) DeepCopyObject() runtime.Object { return h } diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 2b693b72..bbb0721c 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -1,6 +1,7 @@ package render import ( + "regexp" "sort" "strconv" "strings" @@ -15,6 +16,35 @@ import ( "k8s.io/apimachinery/pkg/util/duration" ) +var durationRx = regexp.MustCompile(`\A(\d*d)*?(\d*h)*?(\d*m)*?(\d*s)*?\z`) + +func durationToSeconds(duration string) string { + tokens := durationRx.FindAllStringSubmatch(duration, -1) + if len(tokens) == 0 { + return duration + } + if len(tokens[0]) < 5 { + return duration + } + + d, h, m, s := tokens[0][1], tokens[0][2], tokens[0][3], tokens[0][4] + var n int + if v, err := strconv.Atoi(strings.Replace(d, "d", "", 1)); err == nil { + n += v * 24 * 60 * 60 + } + if v, err := strconv.Atoi(strings.Replace(h, "h", "", 1)); err == nil { + n += v * 60 * 60 + } + if v, err := strconv.Atoi(strings.Replace(m, "m", "", 1)); err == nil { + n += v * 60 + } + if v, err := strconv.Atoi(strings.Replace(s, "s", "", 1)); err == nil { + n += v + } + + return strconv.Itoa(n) +} + // AsThousands prints a number with thousand separator. func AsThousands(n int64) string { p := message.NewPrinter(language.English) @@ -267,3 +297,30 @@ func Pad(s string, width int) string { return s + strings.Repeat(" ", width-len(s)) } + +// Converts labels string to map +func labelize(labels string) map[string]string { + ll := strings.Split(labels, ",") + data := make(map[string]string, len(ll)) + + for _, l := range ll { + tokens := strings.Split(l, "=") + if len(tokens) == 2 { + data[tokens[0]] = tokens[1] + } + } + + return data +} + +func sortLabels(m map[string]string) (keys, vals []string) { + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + vals = append(vals, m[k]) + } + + return +} diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index fb911c65..9427f29e 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -9,6 +9,70 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func TestSortLabels(t *testing.T) { + uu := map[string]struct { + labels string + e [][]string + }{ + "simple": { + labels: "a=b,c=d", + e: [][]string{ + {"a", "c"}, + {"b", "d"}, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + hh, vv := sortLabels(labelize(u.labels)) + assert.Equal(t, u.e[0], hh) + assert.Equal(t, u.e[1], vv) + }) + } +} + +func TestLabelize(t *testing.T) { + uu := map[string]struct { + labels string + e map[string]string + }{ + "simple": { + labels: "a=b,c=d", + e: map[string]string{"a": "b", "c": "d"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, labelize(u.labels)) + }) + } +} + +func TestDurationToNumber(t *testing.T) { + uu := map[string]struct { + s, e string + }{ + "seconds": {s: "22s", e: "22"}, + "minutes": {s: "22m", e: "1320"}, + "hours": {s: "12h", e: "43200"}, + "days": {s: "3d", e: "259200"}, + "day_hour": {s: "3d9h", e: "291600"}, + "day_hour_minute": {s: "2d22h3m", e: "252180"}, + "day_hour_minute_seconds": {s: "2d22h3m50s", e: "252230"}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, durationToSeconds(u.s)) + }) + } +} + func TestToAge(t *testing.T) { uu := map[string]struct { t time.Time diff --git a/internal/render/pdb.go b/internal/render/pdb.go index b45d1d5c..399c2bc8 100644 --- a/internal/render/pdb.go +++ b/internal/render/pdb.go @@ -55,7 +55,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error { pdb.Name, numbToStr(pdb.Spec.MinAvailable), numbToStr(pdb.Spec.MaxUnavailable), - strconv.Itoa(int(pdb.Status.PodDisruptionsAllowed)), + strconv.Itoa(int(pdb.Status.DisruptionsAllowed)), strconv.Itoa(int(pdb.Status.CurrentHealthy)), strconv.Itoa(int(pdb.Status.DesiredHealthy)), strconv.Itoa(int(pdb.Status.ExpectedPods)), diff --git a/internal/render/pod.go b/internal/render/pod.go index 809f5d5e..028761e5 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -13,7 +13,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/kubernetes/pkg/util/node" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -84,8 +83,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error { } var po v1.Pod - err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po) - if err != nil { + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po); err != nil { return err } @@ -262,7 +260,7 @@ func (*Pod) Statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { func (p *Pod) Phase(po *v1.Pod) string { status := string(po.Status.Phase) if po.Status.Reason != "" { - if po.DeletionTimestamp != nil && po.Status.Reason == node.NodeUnreachablePodReason { + if po.DeletionTimestamp != nil && po.Status.Reason == "NodeLost" { return "Unknown" } status = po.Status.Reason diff --git a/internal/render/popeye.go b/internal/render/popeye.go new file mode 100644 index 00000000..0330617b --- /dev/null +++ b/internal/render/popeye.go @@ -0,0 +1,198 @@ +package render + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/derailed/popeye/pkg/config" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Popeye renders a sanitizer to screen. +type Popeye struct{} + +// ColorerFunc colors a resource row. +func (Popeye) ColorerFunc() ColorerFunc { + return func(ns string, h Header, re RowEvent) tcell.Color { + c := DefaultColorer(ns, h, re) + + warnCol := h.IndexOf("WARNING", true) + status, _ := strconv.Atoi(strings.TrimSpace(re.Row.Fields[warnCol])) + if status > 0 { + c = tcell.ColorOrange + } + errCol := h.IndexOf("ERROR", true) + status, _ = strconv.Atoi(strings.TrimSpace(re.Row.Fields[errCol])) + if status > 0 { + c = ErrColor + } + return c + } +} + +// Header returns a header row. +func (Popeye) Header(ns string) Header { + return Header{ + HeaderColumn{Name: "RESOURCE"}, + HeaderColumn{Name: "SCORE%", Align: tview.AlignRight}, + HeaderColumn{Name: "SCANNED", Align: tview.AlignRight}, + HeaderColumn{Name: "OK", Align: tview.AlignRight}, + HeaderColumn{Name: "INFO", Align: tview.AlignRight}, + HeaderColumn{Name: "WARNING", Align: tview.AlignRight}, + HeaderColumn{Name: "ERROR", Align: tview.AlignRight}, + } +} + +// Render renders a K8s resource to screen. +func (Popeye) Render(o interface{}, ns string, r *Row) error { + s, ok := o.(Section) + if !ok { + return fmt.Errorf("expected Section, but got %T", o) + } + + r.ID = s.Title + r.Fields = append(r.Fields, + s.Title, + strconv.Itoa(s.Tally.Score()), + strconv.Itoa(s.Tally.OK+s.Tally.Info+s.Tally.Warning+s.Tally.Error), + strconv.Itoa(s.Tally.OK), + strconv.Itoa(s.Tally.Info), + strconv.Itoa(s.Tally.Warning), + strconv.Itoa(s.Tally.Error), + ) + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type ( + // Builder represents a popeye report. + Builder struct { + Report Report `json:"popeye" yaml:"popeye"` + } + + // Report represents the output of a sanitization pass. + Report struct { + Score int `json:"score" yaml:"score"` + Grade string `json:"grade" yaml:"grade"` + Sections Sections `json:"sanitizers,omitempty" yaml:"sanitizers,omitempty"` + } + + // Sections represents a collection of sections. + Sections []Section + + // Section represents a sanitizer pass + Section struct { + Title string `json:"sanitizer" yaml:"sanitizer"` + GVR string `yaml:"gvr" json:"gvr"` + Tally *Tally `json:"tally" yaml:"tally"` + Outcome Outcome `json:"issues,omitempty" yaml:"issues,omitempty"` + } + + // Outcome represents a classification of reports outcome. + Outcome map[string]Issues + + // Issues represents a collection of issues. + Issues []Issue + + // Issue represents a sanitization issue. + Issue struct { + Group string `yaml:"group" json:"group"` + GVR string `yaml:"gvr" json:"gvr"` + Level config.Level `yaml:"level" json:"level"` + Message string `yaml:"message" json:"message"` + } + + // Tally tracks a section scores. + Tally struct { + OK, Info, Warning, Error int + Count int + } +) + +// Sum sums up tally counts. +func (t *Tally) Sum() int { + return t.OK + t.Info + t.Warning + t.Error +} + +// Score returns the overall sections score in percent. +func (t *Tally) Score() int { + oks := t.OK + t.Info + return toPerc(float64(oks), float64(oks+t.Warning+t.Error)) +} + +func toPerc(v1, v2 float64) int { + if v2 == 0 { + return 0 + } + return int(math.Floor((v1 / v2) * 100)) +} + +// Len returns a section length. +func (s Sections) Len() int { + return len(s) +} + +// Swap swaps values. +func (s Sections) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less compares section scores. +func (s Sections) Less(i, j int) bool { + t1, t2 := s[i].Tally, s[j].Tally + return t1.Score() < t2.Score() +} + +// GetObjectKind returns a schema object. +func (Section) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (s Section) DeepCopyObject() runtime.Object { + return s +} + +// MaxSeverity gather the max severity in a collection of issues. +func (s Section) MaxSeverity() config.Level { + max := config.OkLevel + for _, issues := range s.Outcome { + m := issues.MaxSeverity() + if m > max { + max = m + } + } + + return max +} + +// MaxSeverity gather the max severity in a collection of issues. +func (i Issues) MaxSeverity() config.Level { + max := config.OkLevel + for _, is := range i { + if is.Level > max { + max = is.Level + } + } + + return max +} + +// CountSeverity counts severity level instances +func (i Issues) CountSeverity(l config.Level) int { + var count int + for _, is := range i { + if is.Level == l { + count++ + } + } + + return count +} diff --git a/internal/render/row.go b/internal/render/row.go index 6ae8a805..dcafd20e 100644 --- a/internal/render/row.go +++ b/internal/render/row.go @@ -3,6 +3,7 @@ package render import ( "reflect" "sort" + "strconv" "time" "vbom.ml/util/sortorder" @@ -56,6 +57,20 @@ func NewRow(size int) Row { return Row{Fields: make([]string, size)} } +// Labelize returns a new row based on labels. +func (r Row) Labelize(cols []int, labelCol int, labels []string) Row { + out := NewRow(len(cols) + len(labels)) + for _, col := range cols { + out.Fields = append(out.Fields, r.Fields[col]) + } + m := labelize(r.Fields[labelCol]) + for _, label := range labels { + out.Fields = append(out.Fields, m[label]) + } + + return out +} + // Customize returns a row subset based on given col indices. func (r Row) Customize(cols []int) Row { out := NewRow(len(cols)) @@ -160,36 +175,21 @@ func (s RowSorter) Less(i, j int) bool { // ---------------------------------------------------------------------------- // Helpers... -// Less return true if c1 < c2. -func Less(asc bool, c1, c2 string) bool { - if o, ok := isDurationSort(asc, c1, c2); ok { - return o +func toAgeDuration(dur string) string { + d, err := time.ParseDuration(dur) + if err != nil { + return durationToSeconds(dur) } + return strconv.Itoa(int(d.Seconds())) +} + +// Less return true if c1 < c2. +func Less(asc bool, c1, c2 string) bool { + c1, c2 = toAgeDuration(c1), toAgeDuration(c2) b := sortorder.NaturalLess(c1, c2) if asc { return b } return !b } - -func isDurationSort(asc bool, s1, s2 string) (bool, bool) { - d1, ok1 := isDuration(s1) - d2, ok2 := isDuration(s2) - if !ok1 || !ok2 { - return false, false - } - - if asc { - return d1 <= d2, true - } - return d1 >= d2, true -} - -func isDuration(s string) (time.Duration, bool) { - d, err := time.ParseDuration(s) - if err != nil { - return d, false - } - return d, true -} diff --git a/internal/render/row_event.go b/internal/render/row_event.go index c0230724..3bfe04f0 100644 --- a/internal/render/row_event.go +++ b/internal/render/row_event.go @@ -3,10 +3,8 @@ package render import ( "fmt" "sort" - "time" "github.com/rs/zerolog/log" - "k8s.io/apimachinery/pkg/util/duration" ) const ( @@ -44,8 +42,8 @@ func NewRowEvent(kind ResEvent, row Row) RowEvent { } } -// NewDeltaRowEvent returns a new row event with deltas. -func NewDeltaRowEvent(row Row, delta DeltaRow) RowEvent { +// NewRowEventWithDeltas returns a new row event with deltas. +func NewRowEventWithDeltas(row Row, delta DeltaRow) RowEvent { return RowEvent{ Kind: EventUpdate, Row: row, @@ -77,6 +75,20 @@ func (r RowEvent) Customize(cols []int) RowEvent { } } +func (r RowEvent) ExtractHeaderLabels(labelCol int) []string { + hh, _ := sortLabels(labelize(r.Row.Fields[labelCol])) + return hh +} + +// Labelize returns a new row event based on labels. +func (r RowEvent) Labelize(cols []int, labelCol int, labels []string) RowEvent { + return RowEvent{ + Kind: r.Kind, + Deltas: r.Deltas.Labelize(cols, labelCol), + Row: r.Row.Labelize(cols, labelCol, labels), + } +} + // Diff returns true if the row changed. func (r RowEvent) Diff(re RowEvent, ageCol int) bool { if r.Kind != re.Kind { @@ -93,6 +105,24 @@ func (r RowEvent) Diff(re RowEvent, ageCol int) bool { // RowEvents a collection of row events. type RowEvents []RowEvent +func (r RowEvents) ExtractHeaderLabels(labelCol int) []string { + ll := make([]string, 0, 10) + for _, re := range r { + ll = append(ll, re.ExtractHeaderLabels(labelCol)...) + } + + return ll +} + +func (r RowEvents) Labelize(cols []int, labelCol int, labels []string) RowEvents { + out := make(RowEvents, 0, len(r)) + for _, re := range r { + out = append(out, re.Labelize(cols, labelCol, labels)) + } + + return out +} + // Customize returns custom row events based on columns layout. func (r RowEvents) Customize(cols []int) RowEvents { ee := make(RowEvents, 0, len(cols)) @@ -164,42 +194,32 @@ func (r RowEvents) FindIndex(id string) (int, bool) { // Sort rows based on column index and order. func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) { + if sortCol == -1 { + return + } + t := RowEventSorter{NS: ns, Events: r, Index: sortCol, Asc: asc} sort.Sort(t) - gg, kk := map[string][]string{}, make(StringSet, 0, len(r)) + iids, fields := map[string][]string{}, make(StringSet, 0, len(r)) for _, re := range r { - g := re.Row.Fields[sortCol] + field := re.Row.Fields[sortCol] if ageCol { - g = toAgeDuration(g) - } - kk = kk.Add(g) - if ss, ok := gg[g]; ok { - gg[g] = append(ss, re.Row.ID) - } else { - gg[g] = []string{re.Row.ID} + field = toAgeDuration(field) } + fields = fields.Add(field) + iids[field] = append(iids[field], re.Row.ID) } ids := make([]string, 0, len(r)) - for _, k := range kk { - sort.StringSlice(gg[k]).Sort() - ids = append(ids, gg[k]...) + for _, field := range fields { + sort.StringSlice(iids[field]).Sort() + ids = append(ids, iids[field]...) } s := IdSorter{Ids: ids, Events: r} sort.Sort(s) } -// Helpers... - -func toAgeDuration(dur string) string { - d, err := time.ParseDuration(dur) - if err != nil { - return dur - } - return duration.HumanDuration(d) -} - // ---------------------------------------------------------------------------- // RowEventSorter sorts row events by a given colon. diff --git a/internal/render/row_event_test.go b/internal/render/row_event_test.go index 8c8aa219..e2c21434 100644 --- a/internal/render/row_event_test.go +++ b/internal/render/row_event_test.go @@ -409,11 +409,41 @@ func TestRowEventsDelete(t *testing.T) { func TestRowEventsSort(t *testing.T) { uu := map[string]struct { - re render.RowEvents - col int - asc bool - e render.RowEvents + re render.RowEvents + col int + age, asc bool + e render.RowEvents }{ + "age_time": { + re: render.RowEvents{ + {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "1h10m10.5s"}}}, + {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "10.5s"}}}, + {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3h20m5.2s"}}}, + }, + col: 2, + asc: true, + age: true, + e: render.RowEvents{ + {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "10.5s"}}}, + {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "1h10m10.5s"}}}, + {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3h20m5.2s"}}}, + }, + }, + "age_duration": { + re: render.RowEvents{ + {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "32d"}}}, + {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "1m10s"}}}, + {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3h20m5s"}}}, + }, + col: 2, + asc: true, + age: true, + e: render.RowEvents{ + {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "1m10s"}}}, + {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3h20m5s"}}}, + {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "32d"}}}, + }, + }, "col0": { re: render.RowEvents{ {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, @@ -453,7 +483,7 @@ func TestRowEventsSort(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - u.re.Sort("", u.col, false, u.asc) + u.re.Sort("", u.col, u.age, u.asc) assert.Equal(t, u.e, u.re) }) } diff --git a/internal/render/row_test.go b/internal/render/row_test.go index 627110bf..c0caf2ef 100644 --- a/internal/render/row_test.go +++ b/internal/render/row_test.go @@ -65,6 +65,38 @@ func TestFieldClone(t *testing.T) { assert.NotEqual(t, fmt.Sprintf("%p", f), fmt.Sprintf("%p", f1)) } +func TestRowlabelize(t *testing.T) { + uu := map[string]struct { + row render.Row + cols []int + e render.Row + }{ + "empty": { + row: render.Row{}, + cols: []int{0, 1, 2}, + e: render.Row{ID: "", Fields: render.Fields{"", "", ""}}, + }, + "no-cols-no-data": { + row: render.Row{}, + cols: []int{}, + e: render.Row{ID: "", Fields: render.Fields{}}, + }, + "no-cols-data": { + row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}}, + cols: []int{}, + e: render.Row{ID: "fred", Fields: render.Fields{}}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + row := u.row.Customize(u.cols) + assert.Equal(t, u.e, row) + }) + } +} + func TestRowCustomize(t *testing.T) { uu := map[string]struct { row render.Row diff --git a/internal/render/svc.go b/internal/render/svc.go index 5f274e58..62d7b43b 100644 --- a/internal/render/svc.go +++ b/internal/render/svc.go @@ -29,7 +29,7 @@ func (Service) Header(ns string) Header { HeaderColumn{Name: "CLUSTER-IP"}, HeaderColumn{Name: "EXTERNAL-IP"}, HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "PORTS", Wide: true}, + HeaderColumn{Name: "PORTS", Wide: false}, HeaderColumn{Name: "LABELS", Wide: true}, HeaderColumn{Name: "VALID", Wide: true}, HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator}, diff --git a/internal/render/table_data.go b/internal/render/table_data.go index 0657c70f..b42d537a 100644 --- a/internal/render/table_data.go +++ b/internal/render/table_data.go @@ -1,5 +1,7 @@ package render +import "github.com/derailed/k9s/internal/client" + // TableData tracks a K8s resource for tabular display. type TableData struct { Header Header @@ -12,6 +14,22 @@ func NewTableData() *TableData { return &TableData{} } +// Labelize prints out specific label columns +func (t *TableData) Labelize(labels []string) TableData { + labelCol := t.Header.IndexOf("LABELS", true) + cols := []int{0, 1} + if client.IsNamespaced(t.Namespace) { + cols = cols[1:] + } + data := TableData{ + Namespace: t.Namespace, + Header: t.Header.Labelize(cols, labelCol, t.RowEvents), + } + data.RowEvents = t.RowEvents.Labelize(cols, labelCol, labels) + + return data +} + // Customize returns a new model with customized column layout. func (t *TableData) Customize(cols []string, wide bool) TableData { res := TableData{ @@ -61,7 +79,7 @@ func (t *TableData) Update(rows Rows) { t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta t.RowEvents[index].Row = row } else { - t.RowEvents[index] = NewDeltaRowEvent(row, delta) + t.RowEvents[index] = NewRowEventWithDeltas(row, delta) } continue } diff --git a/internal/ui/app.go b/internal/ui/app.go index ee66562a..4435ed96 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -14,28 +14,29 @@ type App struct { *tview.Application Configurator - Main *Pages - flash *model.Flash - actions KeyActions - views map[string]tview.Primitive - cmdBuff *CmdBuff + Main *Pages + flash *model.Flash + actions KeyActions + views map[string]tview.Primitive + cmdModel *model.FishBuff } // NewApp returns a new app. -func NewApp(context string) *App { +func NewApp(cfg *config.Config, context string) *App { a := App{ - Application: tview.NewApplication(), - actions: make(KeyActions), - Main: NewPages(), - flash: model.NewFlash(model.DefaultFlashDelay), - cmdBuff: NewCmdBuff(':', CommandBuff), + Application: tview.NewApplication(), + actions: make(KeyActions), + Configurator: Configurator{Config: cfg}, + Main: NewPages(), + flash: model.NewFlash(model.DefaultFlashDelay), + cmdModel: model.NewFishBuff(':', model.CommandBuffer), } a.ReloadStyles(context) a.views = map[string]tview.Primitive{ "menu": NewMenu(a.Styles), "logo": NewLogo(a.Styles), - "cmd": NewCommand(a.Styles), + "prompt": NewPrompt(a.Config.K9s.NoIcons, a.Styles), "crumbs": NewCrumbs(a.Styles), } @@ -45,9 +46,9 @@ func NewApp(context string) *App { // Init initializes the application. func (a *App) Init() { a.bindKeys() - a.cmdBuff.AddListener(a.Cmd()) + a.Prompt().SetModel(a.cmdModel) + a.cmdModel.AddListener(a) a.Styles.AddListener(a) - a.CmdBuff().AddListener(a) a.SetRoot(a.Main, true) } @@ -56,20 +57,24 @@ func (a *App) Init() { func (a *App) BufferChanged(s string) {} // BufferActive indicates the buff activity changed. -func (a *App) BufferActive(state bool, _ BufferKind) { +func (a *App) BufferActive(state bool, kind model.BufferKind) { flex, ok := a.Main.GetPrimitive("main").(*tview.Flex) if !ok { return } - if state && flex.ItemAt(1) != a.Cmd() { - flex.AddItemAtIndex(1, a.Cmd(), 3, 1, false) - } else if !state && flex.ItemAt(1) == a.Cmd() { + if state && flex.ItemAt(1) != a.Prompt() { + flex.AddItemAtIndex(1, a.Prompt(), 3, 1, false) + } else if !state && flex.ItemAt(1) == a.Prompt() { flex.RemoveItemAtIndex(1) + a.SetFocus(flex) } a.Draw() } +// SuggestionChanged notifies of update to command suggestions. +func (a *App) SuggestionChanged(ss []string) {} + // StylesChanged notifies the skin changed. func (a *App) StylesChanged(s *config.Styles) { a.Main.SetBackgroundColor(s.BgColor()) @@ -97,14 +102,11 @@ func (a *App) Conn() client.Connection { func (a *App) bindKeys() { a.actions = KeyActions{ - KeyColon: NewKeyAction("Cmd", a.activateCmd, false), - tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false), - tcell.KeyCtrlC: NewKeyAction("Quit", a.quitCmd, false), - tcell.KeyEscape: NewKeyAction("Escape", a.escapeCmd, false), - tcell.KeyBackspace2: NewKeyAction("Erase", a.eraseCmd, false), - tcell.KeyBackspace: NewKeyAction("Erase", a.eraseCmd, false), - tcell.KeyDelete: NewKeyAction("Erase", a.eraseCmd, false), - tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false), + KeyColon: NewKeyAction("Cmd", a.activateCmd, false), + tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false), + tcell.KeyCtrlC: NewKeyAction("Quit", a.quitCmd, false), + tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false), + tcell.KeyCtrlQ: NewSharedKeyAction("Clear Filter", a.clearCmd, false), } } @@ -113,29 +115,36 @@ func (a *App) BailOut() { a.Stop() } +// ResetPrompt reset the prompt model and marks buffer as active. +func (a *App) ResetPrompt(m PromptModel) { + a.Prompt().SetModel(m) + a.SetFocus(a.Prompt()) + m.SetActive(true) +} + // ResetCmd clear out user command. func (a *App) ResetCmd() { - a.cmdBuff.Reset() + a.cmdModel.Reset() } // ActivateCmd toggle command mode. func (a *App) ActivateCmd(b bool) { - a.cmdBuff.SetActive(b) + a.cmdModel.SetActive(b) } // GetCmd retrieves user command. func (a *App) GetCmd() string { - return a.cmdBuff.String() + return a.cmdModel.GetText() } // CmdBuff returns a cmd buffer. -func (a *App) CmdBuff() *CmdBuff { - return a.cmdBuff +func (a *App) CmdBuff() *model.FishBuff { + return a.cmdModel } // HasCmd check if cmd buffer is active and has a command. func (a *App) HasCmd() bool { - return a.cmdBuff.IsActive() && !a.cmdBuff.Empty() + return a.cmdModel.IsActive() && !a.cmdModel.Empty() } func (a *App) quitCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -149,7 +158,7 @@ func (a *App) quitCmd(evt *tcell.EventKey) *tcell.EventKey { // InCmdMode check if command mode is active. func (a *App) InCmdMode() bool { - return a.Cmd().InCmdMode() + return a.Prompt().InCmdMode() } // HasAction checks if key matches a registered binding. @@ -179,7 +188,7 @@ func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey { if !a.CmdBuff().IsActive() { return evt } - a.CmdBuff().Clear() + a.CmdBuff().ClearText() return nil } @@ -188,36 +197,19 @@ func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey { if a.InCmdMode() { return evt } - a.cmdBuff.SetActive(true) - a.cmdBuff.Clear() + a.ResetPrompt(a.cmdModel) + a.cmdModel.ClearText() return nil } -// EraseCmd removes the last char from a command. -func (a *App) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.cmdBuff.IsActive() { - a.cmdBuff.Delete() - return nil - } - return evt -} - -// EscapeCmd dismiss cmd mode. -func (a *App) escapeCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.cmdBuff.IsActive() { - a.cmdBuff.Reset() - } - return evt -} - // RedrawCmd forces a redraw. func (a *App) redrawCmd(evt *tcell.EventKey) *tcell.EventKey { a.Draw() return evt } -// View Accessora... +// View Accessors... // Crumbs return app crumba. func (a *App) Crumbs() *Crumbs { @@ -229,9 +221,9 @@ func (a *App) Logo() *Logo { return a.views["logo"].(*Logo) } -// Cmd returns app cmd. -func (a *App) Cmd() *Command { - return a.views["cmd"].(*Command) +// Prompt returns command prompt. +func (a *App) Prompt() *Prompt { + return a.views["prompt"].(*Prompt) } // Menu returns app menu. @@ -249,6 +241,9 @@ func (a *App) Flash() *model.Flash { // AsKey converts rune to keyboard key., func AsKey(evt *tcell.EventKey) tcell.Key { + if evt.Key() != tcell.KeyRune { + return evt.Key() + } key := tcell.Key(evt.Rune()) if evt.Modifiers() == tcell.ModAlt { key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers())) diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index 2fdde772..cbf6cbb8 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -3,63 +3,64 @@ package ui_test import ( "testing" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestAppGetCmd(t *testing.T) { - a := ui.NewApp("") + a := ui.NewApp(config.NewConfig(nil), "") a.Init() - a.CmdBuff().Set("blee") + a.CmdBuff().SetText("blee") assert.Equal(t, "blee", a.GetCmd()) } func TestAppInCmdMode(t *testing.T) { - a := ui.NewApp("") + a := ui.NewApp(config.NewConfig(nil), "") a.Init() - a.CmdBuff().Set("blee") + a.CmdBuff().SetText("blee") assert.False(t, a.InCmdMode()) - a.CmdBuff().SetActive(true) - assert.True(t, a.InCmdMode()) + a.CmdBuff().SetActive(false) + assert.False(t, a.InCmdMode()) } func TestAppResetCmd(t *testing.T) { - a := ui.NewApp("") + a := ui.NewApp(config.NewConfig(nil), "") a.Init() - a.CmdBuff().Set("blee") + a.CmdBuff().SetText("blee") a.ResetCmd() - assert.Equal(t, "", a.CmdBuff().String()) + assert.Equal(t, "", a.CmdBuff().GetText()) } func TestAppHasCmd(t *testing.T) { - a := ui.NewApp("") + a := ui.NewApp(config.NewConfig(nil), "") a.Init() a.ActivateCmd(true) assert.False(t, a.HasCmd()) - a.CmdBuff().Set("blee") + a.CmdBuff().SetText("blee") assert.True(t, a.InCmdMode()) } func TestAppGetActions(t *testing.T) { - a := ui.NewApp("") + a := ui.NewApp(config.NewConfig(nil), "") a.Init() a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}}) - assert.Equal(t, 9, len(a.GetActions())) + assert.Equal(t, 6, len(a.GetActions())) } func TestAppViews(t *testing.T) { - a := ui.NewApp("") + a := ui.NewApp(config.NewConfig(nil), "") a.Init() - vv := []string{"crumbs", "logo", "cmd", "menu"} + vv := []string{"crumbs", "logo", "prompt", "menu"} for i := range vv { v := vv[i] t.Run(v, func(t *testing.T) { @@ -69,6 +70,6 @@ func TestAppViews(t *testing.T) { assert.NotNil(t, a.Crumbs()) assert.NotNil(t, a.Logo()) - assert.NotNil(t, a.Cmd()) + assert.NotNil(t, a.Prompt()) assert.NotNil(t, a.Menu()) } diff --git a/internal/ui/cmd_buff.go b/internal/ui/cmd_buff.go deleted file mode 100644 index db714eef..00000000 --- a/internal/ui/cmd_buff.go +++ /dev/null @@ -1,150 +0,0 @@ -package ui - -const maxBuff = 10 - -const ( - // CommandBuff indicates a command buffer. - CommandBuff BufferKind = 1 << iota - // FilterBuff indicates a search buffer. - FilterBuff -) - -type ( - // BufferKind indicates a buffer type - BufferKind int8 - - // BuffWatcher represents a command buffer listener. - BuffWatcher interface { - // Changed indicates the buffer was changed. - BufferChanged(s string) - - // Active indicates the buff activity changed. - BufferActive(state bool, kind BufferKind) - } - - // CmdBuff represents user command input. - CmdBuff struct { - buff []rune - listeners []BuffWatcher - hotKey rune - kind BufferKind - sticky bool - active bool - } -) - -// NewCmdBuff returns a new command buffer. -func NewCmdBuff(key rune, kind BufferKind) *CmdBuff { - return &CmdBuff{ - hotKey: key, - kind: kind, - buff: make([]rune, 0, maxBuff), - listeners: []BuffWatcher{}, - } -} - -// IsSticky checks if the cmd is going to perist or not. -func (c *CmdBuff) IsSticky() bool { - return c.sticky -} - -// SetSticky returns cmd stickness. -func (c *CmdBuff) SetSticky(b bool) { - c.sticky = b -} - -// InCmdMode checks if a command exists and the buffer is active. -func (c *CmdBuff) InCmdMode() bool { - return c.active || len(c.buff) > 0 -} - -// IsActive checks if command buffer is active. -func (c *CmdBuff) IsActive() bool { - return c.active -} - -// SetActive toggles cmd buffer active state. -func (c *CmdBuff) SetActive(b bool) { - c.active = b - c.fireActive(c.active) -} - -// String turns rune to string (Stringer protocol) -func (c *CmdBuff) String() string { - return string(c.buff) -} - -// Set initializes the buffer with a command. -func (c *CmdBuff) Set(cmd string) { - c.buff = []rune(cmd) - c.fireChanged() -} - -// Add adds a new charater to the buffer. -func (c *CmdBuff) Add(r rune) { - c.buff = append(c.buff, r) - c.fireChanged() -} - -// Delete removes the last character from the buffer. -func (c *CmdBuff) Delete() { - if c.Empty() { - return - } - c.buff = c.buff[:len(c.buff)-1] - c.fireChanged() -} - -// Clear clears out command buffer. -func (c *CmdBuff) Clear() { - c.buff = make([]rune, 0, maxBuff) - c.fireChanged() -} - -// Reset clears out the command buffer and deactives it. -func (c *CmdBuff) Reset() { - c.Clear() - c.fireChanged() - c.SetActive(false) -} - -// Empty returns true is no cmd, false otherwise. -func (c *CmdBuff) Empty() bool { - return len(c.buff) == 0 -} - -// ---------------------------------------------------------------------------- -// Event Listeners... - -// AddListener registers a cmd buffer listener. -func (c *CmdBuff) AddListener(w ...BuffWatcher) { - c.listeners = append(c.listeners, w...) -} - -// RemoveListener unregisters a listener. -func (c *CmdBuff) RemoveListener(l BuffWatcher) { - victim := -1 - for i, lis := range c.listeners { - if l == lis { - victim = i - break - } - } - - if victim == -1 { - return - } - c.listeners = append(c.listeners[:victim], c.listeners[victim+1:]...) -} - -func (c *CmdBuff) fireChanged() { - for _, l := range c.listeners { - l.BufferChanged(c.String()) - } -} - -func (c *CmdBuff) fireActive(b bool) { - for _, l := range c.listeners { - l.BufferActive(b, c.kind) - } -} diff --git a/internal/ui/command.go b/internal/ui/command.go deleted file mode 100644 index c08fc121..00000000 --- a/internal/ui/command.go +++ /dev/null @@ -1,107 +0,0 @@ -package ui - -import ( - "fmt" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/tview" - "github.com/gdamore/tcell" -) - -const defaultPrompt = "%c> %s" - -// Command captures users free from command input. -type Command struct { - *tview.TextView - - activated bool - icon rune - text string - styles *config.Styles -} - -// NewCommand returns a new command view. -func NewCommand(styles *config.Styles) *Command { - c := Command{styles: styles, TextView: tview.NewTextView()} - c.SetWordWrap(true) - c.SetWrap(true) - c.SetDynamicColors(true) - c.SetBorder(true) - c.SetBorderPadding(0, 0, 1, 1) - c.SetBackgroundColor(styles.BgColor()) - c.SetTextColor(styles.FgColor()) - styles.AddListener(&c) - - return &c -} - -// StylesChanged notifies skin changed. -func (c *Command) StylesChanged(s *config.Styles) { - c.styles = s - c.SetBackgroundColor(s.BgColor()) - c.SetTextColor(s.FgColor()) -} - -// InCmdMode returns true if command is active, false otherwise. -func (c *Command) InCmdMode() bool { - return c.activated -} - -func (c *Command) activate() { - c.write(c.text) -} - -func (c *Command) update(s string) { - if c.text == s { - return - } - c.text = s - c.Clear() - c.write(c.text) -} - -func (c *Command) write(s string) { - fmt.Fprintf(c, defaultPrompt, c.icon, s) -} - -// ---------------------------------------------------------------------------- -// Event Listener protocol... - -// BufferChanged indicates the buffer was changed. -func (c *Command) BufferChanged(s string) { - c.update(s) -} - -// BufferActive indicates the buff activity changed. -func (c *Command) BufferActive(f bool, k BufferKind) { - if c.activated = f; f { - c.SetBorder(true) - c.SetTextColor(c.styles.FgColor()) - c.SetBorderColor(colorFor(k)) - c.icon = iconFor(k) - // c.reset() - c.activate() - } else { - c.SetBorder(false) - c.SetBackgroundColor(c.styles.BgColor()) - c.Clear() - } -} - -func colorFor(k BufferKind) tcell.Color { - switch k { - case CommandBuff: - return tcell.ColorAqua - default: - return tcell.ColorSeaGreen - } -} - -func iconFor(k BufferKind) rune { - switch k { - case CommandBuff: - return '🐶' - default: - return '🐩' - } -} diff --git a/internal/ui/command_test.go b/internal/ui/command_test.go deleted file mode 100644 index bfe82f74..00000000 --- a/internal/ui/command_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package ui_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/ui" - "github.com/stretchr/testify/assert" -) - -func TestCmdNew(t *testing.T) { - v := ui.NewCommand(config.NewStyles()) - - buff := ui.NewCmdBuff(':', ui.CommandBuff) - buff.AddListener(v) - buff.Set("blee") - - assert.Equal(t, "\x00> blee\n", v.GetText(false)) -} - -func TestCmdUpdate(t *testing.T) { - v := ui.NewCommand(config.NewStyles()) - - buff := ui.NewCmdBuff(':', ui.CommandBuff) - buff.AddListener(v) - - buff.Set("blee") - buff.Add('!') - - assert.Equal(t, "\x00> blee!\n", v.GetText(false)) - assert.False(t, v.InCmdMode()) -} - -func TestCmdMode(t *testing.T) { - v := ui.NewCommand(config.NewStyles()) - - buff := ui.NewCmdBuff(':', ui.CommandBuff) - buff.AddListener(v) - - for _, f := range []bool{false, true} { - buff.SetActive(f) - assert.Equal(t, f, v.InCmdMode()) - } -} diff --git a/internal/ui/config.go b/internal/ui/config.go index 17640570..2e426c43 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -74,7 +74,7 @@ func (c *Configurator) RefreshCustomViews() { } if err := c.CustomView.Load(config.K9sViewConfigFile); err != nil { - log.Debug().Msgf("No view custom configuration file found -- %s", config.K9sViewConfigFile) + log.Error().Err(err).Msgf("Custom view load failed %s", config.K9sViewConfigFile) return } } diff --git a/internal/ui/flash.go b/internal/ui/flash.go index 5bcafcd4..82abf53b 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -71,7 +71,7 @@ func (f *Flash) SetMessage(m model.LevelMessage) { return } f.SetTextColor(flashColor(m.Level)) - f.SetText(flashEmoji(m.Level) + " " + m.Text) + f.SetText(f.flashEmoji(m.Level) + " " + m.Text) } if f.testMode { @@ -81,7 +81,10 @@ func (f *Flash) SetMessage(m model.LevelMessage) { } } -func flashEmoji(l model.FlashLevel) string { +func (f *Flash) flashEmoji(l model.FlashLevel) string { + if f.app.Config.K9s.NoIcons { + return "" + } switch l { case model.FlashWarn: return emoDoh @@ -92,6 +95,8 @@ func flashEmoji(l model.FlashLevel) string { } } +// Helpers... + func flashColor(l model.FlashLevel) tcell.Color { switch l { case model.FlashWarn: diff --git a/internal/ui/flash_test.go b/internal/ui/flash_test.go index ee7b2ad9..76acec05 100644 --- a/internal/ui/flash_test.go +++ b/internal/ui/flash_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" @@ -21,7 +22,7 @@ func TestFlash(t *testing.T) { "err": {l: model.FlashErr, i: "hello", e: "😡 hello\n"}, } - a := ui.NewApp("test") + a := ui.NewApp(config.NewConfig(nil), "test") f := ui.NewFlash(a) f.SetTestMode(true) ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go index 60ec1d3e..c144689a 100644 --- a/internal/ui/indicator.go +++ b/internal/ui/indicator.go @@ -133,9 +133,5 @@ func (s *StatusIndicator) setText(msg string) { // AsPercDelta represents a percentage with a delta indicator. func AsPercDelta(ov, nv int) string { prev, cur := render.IntToStr(ov), render.IntToStr(nv) - if cur == "0" { - return render.NAValue - } - return cur + "%" + Deltas(prev, cur) } diff --git a/internal/ui/indicator_test.go b/internal/ui/indicator_test.go index 2e3c5a36..7a3a0922 100644 --- a/internal/ui/indicator_test.go +++ b/internal/ui/indicator_test.go @@ -9,7 +9,7 @@ import ( ) func TestIndicatorReset(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) i.SetPermanent("Blee") i.Info("duh") i.Reset() @@ -18,21 +18,21 @@ func TestIndicatorReset(t *testing.T) { } func TestIndicatorInfo(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) i.Info("Blee") assert.Equal(t, "[lawngreen::b] \n", i.GetText(false)) } func TestIndicatorWarn(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) i.Warn("Blee") assert.Equal(t, "[mediumvioletred::b] \n", i.GetText(false)) } func TestIndicatorErr(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) i.Err("Blee") assert.Equal(t, "[orangered::b] \n", i.GetText(false)) diff --git a/internal/ui/key.go b/internal/ui/key.go index d726f739..feeecd31 100644 --- a/internal/ui/key.go +++ b/internal/ui/key.go @@ -19,7 +19,7 @@ func initKeys() { // Defines numeric keys for container actions const ( - Key0 int32 = iota + 48 + Key0 tcell.Key = iota + 48 Key1 Key2 Key3 @@ -33,16 +33,16 @@ const ( // Defines numeric keys for container actions const ( - KeyShift0 int32 = 41 - KeyShift1 int32 = 33 - KeyShift2 int32 = 64 - KeyShift3 int32 = 35 - KeyShift4 int32 = 36 - KeyShift5 int32 = 37 - KeyShift6 int32 = 94 - KeyShift7 int32 = 38 - KeyShift8 int32 = 42 - KeyShift9 int32 = 40 + KeyShift0 tcell.Key = 41 + KeyShift1 tcell.Key = 33 + KeyShift2 tcell.Key = 64 + KeyShift3 tcell.Key = 35 + KeyShift4 tcell.Key = 36 + KeyShift5 tcell.Key = 37 + KeyShift6 tcell.Key = 94 + KeyShift7 tcell.Key = 38 + KeyShift8 tcell.Key = 42 + KeyShift9 tcell.Key = 40 ) // Defines char keystrokes @@ -110,7 +110,7 @@ const ( ) // NumKeys tracks number keys. -var NumKeys = map[int]int32{ +var NumKeys = map[int]tcell.Key{ 0: Key0, 1: Key1, 2: Key2, @@ -124,16 +124,16 @@ var NumKeys = map[int]int32{ } func initNumbKeys() { - tcell.KeyNames[tcell.Key(Key0)] = "0" - tcell.KeyNames[tcell.Key(Key1)] = "1" - tcell.KeyNames[tcell.Key(Key2)] = "2" - tcell.KeyNames[tcell.Key(Key3)] = "3" - tcell.KeyNames[tcell.Key(Key4)] = "4" - tcell.KeyNames[tcell.Key(Key5)] = "5" - tcell.KeyNames[tcell.Key(Key6)] = "6" - tcell.KeyNames[tcell.Key(Key7)] = "7" - tcell.KeyNames[tcell.Key(Key8)] = "8" - tcell.KeyNames[tcell.Key(Key9)] = "9" + tcell.KeyNames[Key0] = "0" + tcell.KeyNames[Key1] = "1" + tcell.KeyNames[Key2] = "2" + tcell.KeyNames[Key3] = "3" + tcell.KeyNames[Key4] = "4" + tcell.KeyNames[Key5] = "5" + tcell.KeyNames[Key6] = "6" + tcell.KeyNames[Key7] = "7" + tcell.KeyNames[Key8] = "8" + tcell.KeyNames[Key9] = "9" } func initStdKeys() { diff --git a/internal/ui/menu_test.go b/internal/ui/menu_test.go index 51511448..6f9eb799 100644 --- a/internal/ui/menu_test.go +++ b/internal/ui/menu_test.go @@ -6,7 +6,6 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" - "github.com/gdamore/tcell" "github.com/stretchr/testify/assert" ) @@ -30,10 +29,10 @@ func TestActionHints(t *testing.T) { }{ "a": { aa: ui.KeyActions{ - ui.KeyB: ui.NewKeyAction("bleeB", nil, true), - ui.KeyA: ui.NewKeyAction("bleeA", nil, true), - tcell.Key(ui.Key0): ui.NewKeyAction("zero", nil, true), - tcell.Key(ui.Key1): ui.NewKeyAction("one", nil, false), + ui.KeyB: ui.NewKeyAction("bleeB", nil, true), + ui.KeyA: ui.NewKeyAction("bleeA", nil, true), + ui.Key0: ui.NewKeyAction("zero", nil, true), + ui.Key1: ui.NewKeyAction("one", nil, false), }, e: model.MenuHints{ {Mnemonic: "0", Description: "zero", Visible: true}, diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go new file mode 100644 index 00000000..cc924fb3 --- /dev/null +++ b/internal/ui/prompt.go @@ -0,0 +1,257 @@ +package ui + +import ( + "fmt" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/tview" + "github.com/gdamore/tcell" +) + +const ( + defaultPrompt = "%c> [::b]%s" + defaultSpacer = 4 +) + +var _ PromptModel = (*model.CmdBuff)(nil) +var _ Suggester = (*model.CmdBuff)(nil) +var _ PromptModel = (*model.FishBuff)(nil) +var _ Suggester = (*model.FishBuff)(nil) + +// Suggester provides suggestions. +type Suggester interface { + // CurrentSuggestion returns the current suggestion. + CurrentSuggestion() (string, bool) + + // NextSuggestion returns the next suggestion. + NextSuggestion() (string, bool) + + // PrevSuggestion returns the prev suggestion. + PrevSuggestion() (string, bool) + + // ClearSuggestions clear out all suggestions. + ClearSuggestions() +} + +// PromptModel represents a prompt buffer +type PromptModel interface { + // SetText sets the model text. + SetText(string) + + // GetText returns the current text. + GetText() string + + // ClearText clears out model text. + ClearText() + + // Notify notifies all listener of current suggestions. + Notify() + + // AddListener registers a command listener. + AddListener(model.BuffWatcher) + + // RemoveListener removes a listener. + RemoveListener(model.BuffWatcher) + + // IsActive returns true if prompt is active. + IsActive() bool + + // SetActive sets whether the prompt is active or not. + SetActive(bool) + + // Add adds a new char to the prompt. + Add(rune) + + // Delete deletes the last prompt character. + Delete() +} + +// Prompt captures users free from command input. +type Prompt struct { + *tview.TextView + + noIcons bool + icon rune + styles *config.Styles + model PromptModel + spacer int +} + +// NewPrompt returns a new command view. +func NewPrompt(noIcons bool, styles *config.Styles) *Prompt { + p := Prompt{ + styles: styles, + noIcons: noIcons, + TextView: tview.NewTextView(), + spacer: defaultSpacer, + } + if noIcons { + p.spacer-- + } + p.SetWordWrap(true) + p.SetWrap(true) + p.SetDynamicColors(true) + p.SetBorder(true) + p.SetBorderPadding(0, 0, 1, 1) + p.SetBackgroundColor(styles.BgColor()) + p.SetTextColor(styles.FgColor()) + styles.AddListener(&p) + p.SetInputCapture(p.keyboard) + + return &p +} + +// SendKey sends an keyboard event (testing only!). +func (p *Prompt) SendKey(evt *tcell.EventKey) { + p.keyboard(evt) +} + +// SendStrokes (testing only!) +func (p *Prompt) SendStrokes(s string) { + for _, r := range s { + p.keyboard(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone)) + } +} + +// SetModel sets the prompt buffer model. +func (p *Prompt) SetModel(m PromptModel) { + if p.model != nil { + p.model.RemoveListener(p) + } + p.model = m + p.model.AddListener(p) +} + +func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey { + m, ok := p.model.(Suggester) + if !ok { + return evt + } + + switch evt.Key() { + case tcell.KeyBackspace2, tcell.KeyBackspace, tcell.KeyDelete: + p.model.Delete() + case tcell.KeyRune: + p.model.Add(evt.Rune()) + case tcell.KeyEscape: + p.model.ClearText() + p.model.SetActive(false) + case tcell.KeyEnter, tcell.KeyCtrlE: + p.model.SetText(p.model.GetText()) + p.model.SetActive(false) + case tcell.KeyCtrlW, tcell.KeyCtrlU: + p.model.ClearText() + case tcell.KeyDown: + if next, ok := m.NextSuggestion(); ok { + p.suggest(p.model.GetText(), next) + } + case tcell.KeyUp: + if prev, ok := m.PrevSuggestion(); ok { + p.suggest(p.model.GetText(), prev) + } + case tcell.KeyTab, tcell.KeyRight, tcell.KeyCtrlF: + if curr, ok := m.CurrentSuggestion(); ok { + p.model.SetText(p.model.GetText() + curr) + m.ClearSuggestions() + } + } + return evt +} + +// StylesChanged notifies skin changed. +func (p *Prompt) StylesChanged(s *config.Styles) { + p.styles = s + p.SetBackgroundColor(s.BgColor()) + p.SetTextColor(s.FgColor()) +} + +// InCmdMode returns true if command is active, false otherwise. +func (p *Prompt) InCmdMode() bool { + if p.model == nil { + return false + } + return p.model.IsActive() +} + +func (p *Prompt) activate() { + p.SetCursorIndex(len(p.model.GetText())) + p.write(p.model.GetText(), "") + p.model.Notify() +} + +func (p *Prompt) update(s string) { + p.Clear() + p.write(s, "") +} + +func (p *Prompt) suggest(text, suggestion string) { + p.Clear() + p.write(text, suggestion) +} + +func (p *Prompt) write(text, suggest string) { + p.SetCursorIndex(p.spacer + len(text)) + txt := text + if suggest != "" { + txt += "[gray::-]" + suggest + } + fmt.Fprintf(p, defaultPrompt, p.icon, txt) +} + +// ---------------------------------------------------------------------------- +// Event Listener protocol... + +// BufferChanged indicates the buffer was changed. +func (p *Prompt) BufferChanged(s string) { + p.update(s) +} + +// SuggestionChanged notifies the suggestion changed. +func (p *Prompt) SuggestionChanged(text, sugg string) { + p.Clear() + p.write(text, sugg) +} + +// BufferActive indicates the buff activity changed. +func (p *Prompt) BufferActive(activate bool, kind model.BufferKind) { + if activate { + p.ShowCursor(true) + p.SetBorder(true) + p.SetTextColor(p.styles.FgColor()) + p.SetBorderColor(colorFor(kind)) + p.icon = p.iconFor(kind) + p.activate() + return + } + + p.ShowCursor(false) + p.SetBorder(false) + p.SetBackgroundColor(p.styles.BgColor()) + p.Clear() +} + +func (p *Prompt) iconFor(k model.BufferKind) rune { + if p.noIcons { + return ' ' + } + + switch k { + case model.CommandBuffer: + return '🐶' + default: + return '🐩' + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func colorFor(k model.BufferKind) tcell.Color { + switch k { + case model.CommandBuffer: + return tcell.ColorAqua + default: + return tcell.ColorSeaGreen + } +} diff --git a/internal/ui/prompt_test.go b/internal/ui/prompt_test.go new file mode 100644 index 00000000..49bc71b8 --- /dev/null +++ b/internal/ui/prompt_test.go @@ -0,0 +1,45 @@ +package ui_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/stretchr/testify/assert" +) + +func TestCmdNew(t *testing.T) { + v := ui.NewPrompt(true, config.NewStyles()) + model := model.NewFishBuff(':', model.CommandBuffer) + v.SetModel(model) + model.AddListener(v) + model.SetText("blee") + + assert.Equal(t, "\x00> [::b]blee\n", v.GetText(false)) +} + +func TestCmdUpdate(t *testing.T) { + model := model.NewFishBuff(':', model.CommandBuffer) + v := ui.NewPrompt(true, config.NewStyles()) + v.SetModel(model) + + model.AddListener(v) + model.SetText("blee") + model.Add('!') + + assert.Equal(t, "\x00> [::b]blee!\n", v.GetText(false)) + assert.False(t, v.InCmdMode()) +} + +func TestCmdMode(t *testing.T) { + model := model.NewFishBuff(':', model.CommandBuffer) + v := ui.NewPrompt(true, config.NewStyles()) + v.SetModel(model) + model.AddListener(v) + + for _, f := range []bool{false, true} { + model.SetActive(f) + assert.Equal(t, f, v.InCmdMode()) + } +} diff --git a/internal/ui/table.go b/internal/ui/table.go index eae9122f..648e5d7b 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -34,7 +34,8 @@ type Table struct { actions KeyActions gvr client.GVR Path string - cmdBuff *CmdBuff + Extras string + cmdBuff *model.FishBuff styles *config.Styles viewSetting *config.ViewSetting sortCol SortColumn @@ -56,7 +57,7 @@ func NewTable(gvr client.GVR) *Table { }, gvr: gvr, actions: make(KeyActions), - cmdBuff: NewCmdBuff('/', FilterBuff), + cmdBuff: model.NewFishBuff('/', model.FilterBuffer), sortCol: SortColumn{asc: true}, } } @@ -147,6 +148,14 @@ func (t *Table) FilterInput(r rune) bool { return true } +// Filter filters out table data. +func (t *Table) Filter(q string) { + t.ClearSelection() + t.doUpdate(t.filtered(t.GetModel().Peek())) + t.UpdateTitle() + t.SelectFirstRow() +} + // Hints returns the view hints. func (t *Table) Hints() model.MenuHints { return t.actions.Hints() @@ -203,7 +212,7 @@ func (t *Table) doUpdate(data render.TableData) { } custData := data.Customize(cols, t.wide) - if (t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1) && len(custData.Header) > 0 { + if (t.sortCol.name == "" || custData.Header.IndexOf(t.sortCol.name, false) == -1) && len(custData.Header) > 0 && t.sortCol.name != "NONE" { t.sortCol.name = custData.Header[0].Name } @@ -225,7 +234,12 @@ func (t *Table) doUpdate(data render.TableData) { c.SetTextColor(fg) col++ } - custData.RowEvents.Sort(custData.Namespace, custData.Header.IndexOf(t.sortCol.name, false), t.sortCol.name == "AGE", t.sortCol.asc) + custData.RowEvents.Sort( + custData.Namespace, + custData.Header.IndexOf(t.sortCol.name, false), + t.sortCol.name == "AGE", + t.sortCol.asc, + ) pads := make(MaxyPad, len(custData.Header)) ComputeMaxColumns(pads, t.sortCol.name, custData.Header, custData.RowEvents) @@ -322,8 +336,13 @@ func (t *Table) Refresh() { } // GetSelectedRow returns the entire selected row. -func (t *Table) GetSelectedRow() render.Row { - return t.model.Peek().RowEvents[t.GetSelectedRowIndex()-1].Row +func (t *Table) GetSelectedRow(path string) (render.Row, bool) { + data := t.model.Peek() + i, ok := data.RowEvents.FindIndex(path) + if !ok { + return render.Row{}, ok + } + return data.RowEvents[i].Row, true } // NameColIndex returns the index of the resource name column. @@ -352,26 +371,26 @@ func (t *Table) filtered(data render.TableData) render.TableData { if t.toast { filtered = filterToast(data) } - if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.String()) { + if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.GetText()) { return filtered } - q := t.cmdBuff.String() + q := t.cmdBuff.GetText() if IsFuzzySelector(q) { - return fuzzyFilter(q[2:], t.NameColIndex(), filtered) + return fuzzyFilter(q[2:], filtered) } - filtered, err := rxFilter(t.cmdBuff.String(), filtered) + filtered, err := rxFilter(t.cmdBuff.GetText(), filtered) if err != nil { log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp") - t.cmdBuff.Clear() + t.cmdBuff.ClearText() } return filtered } -// SearchBuff returns the associated command buffer. -func (t *Table) SearchBuff() *CmdBuff { +// CmdBuff returns the associated command buffer. +func (t *Table) CmdBuff() *model.FishBuff { return t.cmdBuff } @@ -409,7 +428,9 @@ func (t *Table) styleTitle() string { ns = path } } - + if t.Extras != "" { + ns = t.Extras + } var title string if ns == client.ClusterScope { title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame()) @@ -417,7 +438,7 @@ func (t *Table) styleTitle() string { title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, rc), t.styles.Frame()) } - buff := t.cmdBuff.String() + buff := t.cmdBuff.GetText() if buff == "" { return title } diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 65431aa6..2d3f02b2 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -149,8 +149,8 @@ func rxFilter(q string, data render.TableData) (render.TableData, error) { Namespace: data.Namespace, } for _, re := range data.RowEvents { - f := strings.Join(re.Row.Fields, " ") - if rx.MatchString(f) { + fields := strings.Join(re.Row.Fields, " ") + if rx.MatchString(fields) { filtered.RowEvents = append(filtered.RowEvents, re) } } @@ -158,10 +158,11 @@ func rxFilter(q string, data render.TableData) (render.TableData, error) { return filtered, nil } -func fuzzyFilter(q string, index int, data render.TableData) render.TableData { +func fuzzyFilter(q string, data render.TableData) render.TableData { + q = strings.TrimSpace(q) var ss []string for _, re := range data.RowEvents { - ss = append(ss, re.Row.Fields[index]) + ss = append(ss, re.Row.ID) } filtered := render.TableData{ diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 24b71180..5527ec24 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -41,8 +41,10 @@ func TestTableSelection(t *testing.T) { v.Update(m.Peek()) v.SelectRow(1, true) + r, ok := v.GetSelectedRow("r1") + assert.True(t, ok) assert.Equal(t, "r1", v.GetSelectedItem()) - assert.Equal(t, render.Row{ID: "r1", Fields: render.Fields{"blee", "duh", "fred"}}, v.GetSelectedRow()) + assert.Equal(t, render.Row{ID: "r1", Fields: render.Fields{"blee", "duh", "fred"}}, r) assert.Equal(t, "blee", v.GetSelectedCell(0)) assert.Equal(t, 1, v.GetSelectedRowIndex()) assert.Equal(t, []string{"r1"}, v.GetSelectedItems()) diff --git a/internal/ui/tree.go b/internal/ui/tree.go index 1e0971b0..eb0e3e33 100644 --- a/internal/ui/tree.go +++ b/internal/ui/tree.go @@ -17,7 +17,7 @@ type Tree struct { actions KeyActions selectedItem string - cmdBuff *CmdBuff + cmdBuff *model.CmdBuff expandNodes bool Count int keyListener KeyListenerFunc @@ -29,7 +29,7 @@ func NewTree() *Tree { TreeView: tview.NewTreeView(), expandNodes: true, actions: make(KeyActions), - cmdBuff: NewCmdBuff('/', FilterBuff), + cmdBuff: model.NewCmdBuff('/', model.FilterBuffer), } } @@ -62,7 +62,7 @@ func (t *Tree) ExpandNodes() bool { } // CmdBuff returns the filter command. -func (t *Tree) CmdBuff() *CmdBuff { +func (t *Tree) CmdBuff() *model.CmdBuff { return t.cmdBuff } @@ -95,20 +95,7 @@ func (t *Tree) BindKeys() { } func (t *Tree) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyRune { - if t.cmdBuff.IsActive() { - t.cmdBuff.Add(evt.Rune()) - t.ClearSelection() - if t.keyListener != nil { - t.keyListener() - } - return nil - } - key = mapKey(evt) - } - - if a, ok := t.actions[key]; ok { + if a, ok := t.actions[AsKey(evt)]; ok { return a.Action(evt) } @@ -135,14 +122,3 @@ func (t *Tree) ClearSelection() { t.selectedItem = "" t.SetCurrentNode(nil) } - -// ---------------------------------------------------------------------------- -// Helpers... - -func mapKey(evt *tcell.EventKey) tcell.Key { - key := tcell.Key(evt.Rune()) - if evt.Modifiers() == tcell.ModAlt { - key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers())) - } - return key -} diff --git a/internal/view/alias.go b/internal/view/alias.go index 4acac09a..23e1c53a 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -47,6 +47,7 @@ func (a *Alias) aliasContext(ctx context.Context) context.Context { func (a *Alias) bindKeys(aa ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL) aa.Add(ui.KeyActions{ tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true), ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd("RESOURCE", true), false), @@ -66,7 +67,7 @@ func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - if a.GetTable().SearchBuff().IsActive() { + if a.GetTable().CmdBuff().IsActive() { return a.GetTable().activateCmd(evt) } return evt diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index d2641c25..f4dfccce 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -23,20 +23,19 @@ func TestAliasNew(t *testing.T) { assert.Nil(t, v.Init(makeContext())) assert.Equal(t, "Aliases", v.Name()) - assert.Equal(t, 6, len(v.Hints())) + assert.Equal(t, 5, len(v.Hints())) } func TestAliasSearch(t *testing.T) { v := view.NewAlias(client.NewGVR("aliases")) assert.Nil(t, v.Init(makeContext())) v.GetTable().SetModel(&testModel{}) - v.GetTable().SearchBuff().SetActive(true) - v.GetTable().SearchBuff().Set("dump") - - v.GetTable().SendKey(tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone)) + v.GetTable().Refresh() + v.App().Prompt().SetModel(v.GetTable().CmdBuff()) + v.App().Prompt().SendStrokes("blee") assert.Equal(t, 3, v.GetTable().GetColumnCount()) - assert.Equal(t, 1, v.GetTable().GetRowCount()) + assert.Equal(t, 2, v.GetTable().GetRowCount()) } func TestAliasGoto(t *testing.T) { @@ -45,11 +44,11 @@ func TestAliasGoto(t *testing.T) { v.GetTable().Select(0, 0) b := buffL{} - v.GetTable().SearchBuff().SetActive(true) - v.GetTable().SearchBuff().AddListener(&b) + v.GetTable().CmdBuff().SetActive(true) + v.GetTable().CmdBuff().AddListener(&b) v.GetTable().SendKey(tcell.NewEventKey(tcell.KeyEnter, 256, tcell.ModNone)) - assert.True(t, v.GetTable().SearchBuff().IsActive()) + assert.True(t, v.GetTable().CmdBuff().IsActive()) } // ---------------------------------------------------------------------------- @@ -63,7 +62,7 @@ type buffL struct { func (b *buffL) BufferChanged(s string) { b.changed++ } -func (b *buffL) BufferActive(state bool, kind ui.BufferKind) { +func (b *buffL) BufferActive(state bool, kind model.BufferKind) { b.active++ } @@ -97,7 +96,13 @@ func (k ks) NamespaceNames(nn []v1.Namespace) []string { type testModel struct{} -var _ ui.Tabular = &testModel{} +var _ ui.Tabular = (*testModel)(nil) +var _ ui.Suggester = (*testModel)(nil) + +func (t *testModel) CurrentSuggestion() (string, bool) { return "", false } +func (t *testModel) NextSuggestion() (string, bool) { return "", false } +func (t *testModel) PrevSuggestion() (string, bool) { return "", false } +func (t *testModel) ClearSuggestions() {} func (t *testModel) SetInstance(string) {} func (t *testModel) Empty() bool { return false } diff --git a/internal/view/app.go b/internal/view/app.go index 137cf95c..4059fb6f 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -4,7 +4,12 @@ import ( "context" "errors" "fmt" + "os" + "os/signal" + "sort" + "strings" "sync/atomic" + "syscall" "time" "github.com/derailed/k9s/internal" @@ -24,7 +29,7 @@ var ExitStatus = "" const ( splashDelay = 1 * time.Second clusterRefresh = 5 * time.Second - maxConRetry = 10 + maxConRetry = 15 clusterInfoWidth = 50 clusterInfoPad = 15 ) @@ -33,23 +38,26 @@ const ( type App struct { *ui.App - Content *PageStack - command *Command - factory *watch.Factory - version string - showHeader bool - cancelFn context.CancelFunc - conRetry int32 - clusterModel *model.ClusterInfo + Content *PageStack + command *Command + factory *watch.Factory + version string + showHeader bool + cancelFn context.CancelFunc + conRetry int32 + clusterModel *model.ClusterInfo + cmdHistory *model.History + filterHistory *model.History } // NewApp returns a K9s app instance. func NewApp(cfg *config.Config) *App { a := App{ - App: ui.NewApp(cfg.K9s.CurrentContext), - Content: NewPageStack(), + App: ui.NewApp(cfg, cfg.K9s.CurrentContext), + cmdHistory: model.NewHistory(model.MaxHistory), + filterHistory: model.NewHistory(model.MaxHistory), + Content: NewPageStack(), } - a.Config = cfg a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles) a.Views()["clusterInfo"] = NewClusterInfo(&a) @@ -91,41 +99,71 @@ func (a *App) Init(version string, rate int) error { a.clusterModel.AddListener(a.clusterInfo()) a.clusterModel.AddListener(a.statusIndicator()) a.clusterModel.Refresh() + a.clusterInfo().Init() a.command = NewCommand(a) if err := a.command.Init(); err != nil { return err } - - a.clusterInfo().Init() + a.CmdBuff().SetSuggestionFn(a.suggestCommand()) + a.CmdBuff().AddListener(a) flash := ui.NewFlash(a.App) go flash.Watch(ctx, a.Flash().Channel()) main := tview.NewFlex().SetDirection(tview.FlexRow) main.AddItem(a.statusIndicator(), 1, 1, false) - main.AddItem(flash, 1, 1, false) main.AddItem(a.Content, 0, 10, true) main.AddItem(a.Crumbs(), 1, 1, false) + main.AddItem(flash, 1, 1, false) a.Main.AddPage("main", main, true, false) a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true) a.toggleHeader(!a.Config.K9s.GetHeadless()) + a.initSignals() + return nil } -func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyRune { - if a.CmdBuff().IsActive() && evt.Modifiers() == tcell.ModNone { - a.CmdBuff().Add(evt.Rune()) +func (a *App) initSignals() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGABRT, syscall.SIGINT, syscall.SIGHUP, syscall.SIGQUIT) + + go func(sig chan os.Signal) { + <-sig + a.BailOut() + }(sig) +} + +func (a *App) suggestCommand() model.SuggestionFunc { + return func(s string) (entries sort.StringSlice) { + if s == "" { + if a.cmdHistory.Empty() { + return + } + return a.cmdHistory.List() + } + + s = strings.ToLower(s) + for _, k := range a.command.alias.Aliases.Keys() { + if k == s { + continue + } + if strings.HasPrefix(k, s) { + entries = append(entries, strings.Replace(k, s, "", 1)) + } + } + if len(entries) == 0 { return nil } - key = ui.AsKey(evt) + entries.Sort() + return } +} - if k, ok := a.HasAction(key); ok && !a.Content.IsTopDialog() { +func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { + if k, ok := a.HasAction(ui.AsKey(evt)); ok && !a.Content.IsTopDialog() { return k.Action(evt) } @@ -311,6 +349,13 @@ func (a *App) initFactory(ns string) { // BailOut exists the application. func (a *App) BailOut() { + defer func() { + if err := recover(); err != nil { + log.Error().Msgf("Bailing out %v", err) + } + }() + + nukeK9sShell(a.Conn()) a.factory.Terminate() a.App.BailOut() } @@ -400,7 +445,7 @@ func (a *App) PrevCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *App) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.Cmd().InCmdMode() { + if a.Prompt().InCmdMode() { return evt } @@ -426,10 +471,11 @@ func (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { - if _, ok := a.Content.GetPrimitive("main").(*Help); ok { + if a.CmdBuff().InCmdMode() { return evt } - if a.Content.Top() != nil && a.Content.Top().Name() == helpTitle { + + if a.Content.Top() != nil && a.Content.Top().Name() == "help" { a.Content.Pop() return nil } @@ -442,7 +488,7 @@ func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { } func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { - if _, ok := a.Content.GetPrimitive("main").(*Alias); ok { + if a.CmdBuff().InCmdMode() { return evt } @@ -458,10 +504,6 @@ func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (a *App) viewResource(gvr, path string, clearStack bool) error { - return a.command.run(gvr, path, clearStack) -} - func (a *App) gotoResource(cmd, path string, clearStack bool) error { return a.command.run(cmd, path, clearStack) } diff --git a/internal/view/app_test.go b/internal/view/app_test.go index b1f3476f..571cbabb 100644 --- a/internal/view/app_test.go +++ b/internal/view/app_test.go @@ -12,5 +12,5 @@ func TestAppNew(t *testing.T) { a := view.NewApp(config.NewConfig(ks{})) a.Init("blee", 10) - assert.Equal(t, 12, len(a.GetActions())) + assert.Equal(t, 9, len(a.GetActions())) } diff --git a/internal/view/browser.go b/internal/view/browser.go index 4d65fa94..507b96bd 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -4,13 +4,16 @@ import ( "context" "errors" "fmt" + "sort" "strconv" + "strings" "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -73,9 +76,36 @@ func (b *Browser) Init(ctx context.Context) error { b.GetModel().AddListener(b) b.GetModel().SetRefreshRate(time.Duration(b.App().Config.K9s.GetRefreshRate()) * time.Second) + b.CmdBuff().SetSuggestionFn(b.suggestFilter()) + return nil } +func (b *Browser) suggestFilter() model.SuggestionFunc { + return func(s string) (entries sort.StringSlice) { + if s == "" { + if b.App().filterHistory.Empty() { + return + } + return b.App().filterHistory.List() + } + + s = strings.ToLower(s) + for _, h := range b.App().filterHistory.List() { + if h == s { + continue + } + if strings.HasPrefix(h, s) { + entries = append(entries, strings.Replace(h, s, "", 1)) + } + } + if len(entries) == 0 { + return nil + } + return + } +} + func (b *Browser) bindKeys() { b.Actions().Add(ui.KeyActions{ tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", b.resetCmd, false), @@ -92,9 +122,35 @@ func (b *Browser) SetInstance(path string) { func (b *Browser) Start() { b.Stop() b.Table.Start() + b.CmdBuff().AddListener(b) b.GetModel().Watch(b.prepareContext()) } +// Stop terminates browser updates. +func (b *Browser) Stop() { + b.CmdBuff().RemoveListener(b) + b.Table.Stop() + if b.cancelFn != nil { + b.cancelFn() + b.cancelFn = nil + } +} + +// BufferChanged indicates the buffer was changed. +func (b *Browser) BufferChanged(s string) {} + +// BufferActive indicates the buff activity changed. +func (b *Browser) BufferActive(state bool, k model.BufferKind) { + if b.cancelFn != nil { + b.cancelFn() + } + b.GetModel().Watch(b.prepareContext()) + + if !state && b.GetRowCount() > 1 { + b.App().filterHistory.Push(b.CmdBuff().GetText()) + } +} + func (b *Browser) prepareContext() context.Context { ctx := b.defaultContext() ctx, b.cancelFn = context.WithCancel(ctx) @@ -108,16 +164,6 @@ func (b *Browser) prepareContext() context.Context { return ctx } -// Stop terminates browser updates. -func (b *Browser) Stop() { - if b.cancelFn == nil { - return - } - b.Table.Stop() - b.cancelFn() - b.cancelFn = nil -} - func (b *Browser) refresh() { b.Start() } @@ -183,32 +229,28 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { } func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !b.SearchBuff().InCmdMode() { - b.SearchBuff().Reset() + if !b.CmdBuff().InCmdMode() { + b.CmdBuff().Reset() return b.App().PrevCmd(evt) } - cmd := b.SearchBuff().String() - b.SearchBuff().Reset() - - if ui.IsLabelSelector(cmd) { + if ui.IsLabelSelector(b.CmdBuff().GetText()) { + b.CmdBuff().Reset() b.Start() - } else { - b.Refresh() } + b.CmdBuff().Reset() + b.Refresh() return nil } func (b *Browser) filterCmd(evt *tcell.EventKey) *tcell.EventKey { - if !b.SearchBuff().IsActive() { + if !b.CmdBuff().IsActive() { return evt } - b.SearchBuff().SetActive(false) - - cmd := b.SearchBuff().String() - if ui.IsLabelSelector(cmd) { + b.CmdBuff().SetActive(false) + if ui.IsLabelSelector(b.CmdBuff().GetText()) { b.Start() return nil } @@ -355,8 +397,8 @@ func (b *Browser) defaultContext() context.Context { ctx = context.WithValue(ctx, internal.KeyPath, b.Path) ctx = context.WithValue(ctx, internal.KeyLabels, "") - if ui.IsLabelSelector(b.SearchBuff().String()) { - ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.SearchBuff().String())) + if ui.IsLabelSelector(b.CmdBuff().GetText()) { + ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText())) } ctx = context.WithValue(ctx, internal.KeyFields, "") ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace())) @@ -403,14 +445,14 @@ func (b *Browser) namespaceActions(aa ui.KeyActions) { return } b.namespaces = make(map[int]string, config.MaxFavoritesNS) - aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true) + aa[ui.Key0] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true) b.namespaces[0] = client.NamespaceAll index := 1 for _, ns := range b.app.Config.FavNamespaces() { if ns == client.NamespaceAll { continue } - aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true) + aa[ui.NumKeys[index]] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true) b.namespaces[index] = ns index++ } diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index b7a9f4e9..b686209f 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -2,6 +2,7 @@ package view import ( "fmt" + "os" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" @@ -84,14 +85,13 @@ func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) { // ClusterInfoChanged notifies the cluster meta was changed. func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { - // BOZO!! c.app.QueueUpdate(func() { c.Clear() c.layout() row := c.setCell(0, curr.Context) row = c.setCell(row, curr.Cluster) row = c.setCell(row, curr.User) - row = c.setCell(row, curr.K9sVer) + row = c.setCell(row, fmt.Sprintf("%s [%d]", curr.K9sVer, os.Getpid())) row = c.setCell(row, curr.K8sVer) if c.app.Conn().HasMetrics() { row = c.setCell(row, ui.AsPercDelta(prev.Cpu, curr.Cpu)) diff --git a/internal/view/command.go b/internal/view/command.go index eab353c5..4858d71b 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -85,10 +85,10 @@ func (c *Command) xrayCmd(cmd string) error { } gvr, ok := c.alias.AsGVR(tokens[1]) if !ok { - return fmt.Errorf("Huh? `%s` Command not found", cmd) + return fmt.Errorf("Huh? `%s` command not found", cmd) } if !allowedXRay(gvr) { - return fmt.Errorf("Huh? `%s` Command not found", cmd) + return fmt.Errorf("Huh? `%s` command not found", cmd) } x := NewXray(gvr) @@ -106,17 +106,34 @@ func (c *Command) xrayCmd(cmd string) error { return c.exec(cmd, "xrays", x, true) } +func (c *Command) checkAccess(gvr string) error { + m, err := dao.MetaAccess.MetaFor(client.NewGVR(gvr)) + if err != nil { + return err + } + ns := client.CleanseNamespace(c.app.Config.ActiveNamespace()) + if dao.IsK8sMeta(m) && c.app.ConOK() { + if _, e := c.app.factory.CanForResource(ns, gvr, client.MonitorAccess); e != nil { + return e + } + } + return nil +} + // Exec the Command by showing associated display. func (c *Command) run(cmd, path string, clearStack bool) error { if c.specialCmd(cmd) { return nil } - cmds := strings.Split(cmd, " ") gvr, v, err := c.viewMetaFor(cmds[0]) if err != nil { return err } + if err := c.checkAccess(gvr); err != nil { + return err + } + switch cmds[0] { case "ctx", "context", "contexts": if len(cmds) == 2 { @@ -141,8 +158,7 @@ func (c *Command) run(cmd, path string, clearStack bool) error { } func (c *Command) defaultCmd() error { - err := c.run(c.app.Config.ActiveView(), "", true) - if err != nil { + if err := c.run(c.app.Config.ActiveView(), "", true); err != nil { log.Error().Err(err).Msgf("Saved command failed. Loading default view") return c.run("pod", "", true) } @@ -185,7 +201,7 @@ func (c *Command) specialCmd(cmd string) bool { func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) { gvr, ok := c.alias.AsGVR(cmd) if !ok { - return "", nil, fmt.Errorf("Huh? `%s` Command not found", cmd) + return "", nil, fmt.Errorf("Huh? `%s` command not found", cmd) } v, ok := customViewers[gvr] @@ -225,5 +241,10 @@ func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) e c.app.Content.Stack.Clear() } - return c.app.inject(comp) + if err := c.app.inject(comp); err != nil { + return err + } + c.app.cmdHistory.Push(cmd) + + return nil } diff --git a/internal/view/container.go b/internal/view/container.go index f18e791b..2f2993cb 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -62,12 +62,12 @@ func (c *Container) bindKeys(aa ui.KeyActions) { } func (c *Container) k9sEnv() Env { - env := defaultEnv( - c.App().Conn().Config(), - c.GetTable().GetSelectedItem(), - c.GetTable().GetModel().Peek().Header, - c.GetTable().GetSelectedRow(), - ) + path := c.GetTable().GetSelectedItem() + row, ok := c.GetTable().GetSelectedRow(path) + if !ok { + log.Error().Msgf("unable to locate seleted row for %q", path) + } + env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header, row) env["NAMESPACE"], env["POD"] = client.Namespaced(c.GetTable().Path) return env @@ -116,7 +116,6 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - log.Debug().Msgf("CONTAINER-SEL %q", path) if _, ok := c.App().factory.ForwarderFor(fwFQN(c.GetTable().Path, path)); ok { c.App().Flash().Err(fmt.Errorf("A PortForward already exist on container %s", c.GetTable().Path)) return nil @@ -126,7 +125,6 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { if !ok { return nil } - log.Debug().Msgf("CONTAINER-PORTS %#v", ports) ShowPortForwards(c, c.GetTable().Path, ports, startFwdCB) return nil diff --git a/internal/view/details.go b/internal/view/details.go index 708c455a..15726044 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -23,7 +23,7 @@ type Details struct { actions ui.KeyActions app *App title, subject string - cmdBuff *ui.CmdBuff + cmdBuff *model.CmdBuff model *model.Text currentRegion, maxRegions int searchable bool @@ -37,7 +37,7 @@ func NewDetails(app *App, title, subject string, searchable bool) *Details { title: title, subject: subject, actions: make(ui.KeyActions), - cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), + cmdBuff: model.NewCmdBuff('/', model.FilterBuffer), model: model.NewText(), searchable: searchable, } @@ -55,6 +55,7 @@ func (d *Details) Init(_ context.Context) error { d.SetHighlightColor(tcell.ColorOrange) d.SetTitleColor(tcell.ColorAqua) d.SetInputCapture(d.keyboard) + d.SetBorderPadding(0, 0, 1, 1) d.SetChangedFunc(func() { d.app.Draw() }) @@ -63,7 +64,7 @@ func (d *Details) Init(_ context.Context) error { d.app.Styles.AddListener(d) d.StylesChanged(d.app.Styles) - d.cmdBuff.AddListener(d.app.Cmd()) + d.app.Prompt().SetModel(d.cmdBuff) d.cmdBuff.AddListener(d) d.bindKeys() @@ -100,26 +101,26 @@ func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) { } // BufferChanged indicates the buffer was changed. -func (d *Details) BufferChanged(s string) {} +func (d *Details) BufferChanged(s string) { + d.model.Filter(s) + d.updateTitle() +} // BufferActive indicates the buff activity changed. -func (d *Details) BufferActive(state bool, k ui.BufferKind) { +func (d *Details) BufferActive(state bool, k model.BufferKind) { d.app.BufferActive(state, k) } func (d *Details) bindKeys() { d.actions.Set(ui.KeyActions{ - tcell.KeyEnter: ui.NewSharedKeyAction("Filter", d.filterCmd, false), - tcell.KeyEscape: ui.NewKeyAction("Back", d.resetCmd, false), - tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), - ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), - ui.KeyN: ui.NewKeyAction("Next Match", d.nextCmd, true), - ui.KeyShiftN: ui.NewKeyAction("Prev Match", d.prevCmd, true), - ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", d.activateCmd, false), - tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", d.clearCmd, false), - tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), - tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), - tcell.KeyDelete: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), + tcell.KeyEnter: ui.NewSharedKeyAction("Filter", d.filterCmd, false), + tcell.KeyEscape: ui.NewKeyAction("Back", d.resetCmd, false), + tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), + ui.KeyC: ui.NewKeyAction("Copy", d.cpCmd, true), + ui.KeyN: ui.NewKeyAction("Next Match", d.nextCmd, true), + ui.KeyShiftN: ui.NewKeyAction("Prev Match", d.prevCmd, true), + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", d.activateCmd, false), + tcell.KeyDelete: ui.NewSharedKeyAction("Erase", d.eraseCmd, false), }) if !d.searchable { @@ -128,48 +129,24 @@ func (d *Details) bindKeys() { } func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyUp || key == tcell.KeyDown { - return evt - } - - if key == tcell.KeyRune { - if d.filterInput(evt.Rune()) { - return nil - } - key = ui.AsKey(evt) - } - - if a, ok := d.actions[key]; ok { + if a, ok := d.actions[ui.AsKey(evt)]; ok { return a.Action(evt) } return evt } -func (d *Details) filterInput(r rune) bool { - if !d.cmdBuff.IsActive() { - return false - } - d.cmdBuff.Add(r) - d.updateTitle() - - return true -} - // StylesChanged notifies the skin changed. func (d *Details) StylesChanged(s *config.Styles) { d.SetBackgroundColor(d.app.Styles.BgColor()) d.SetTextColor(d.app.Styles.FgColor()) d.SetBorderFocusColor(d.app.Styles.Frame().Border.FocusColor.Color()) - d.TextChanged(d.model.Peek()) } // Update updates the view content. func (d *Details) Update(buff string) *Details { d.model.SetText(buff) - return d } @@ -237,7 +214,7 @@ func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey { } func (d *Details) filterCmd(evt *tcell.EventKey) *tcell.EventKey { - d.model.Filter(d.cmdBuff.String()) + d.model.Filter(d.cmdBuff.GetText()) d.cmdBuff.SetActive(false) d.updateTitle() @@ -248,16 +225,7 @@ func (d *Details) activateCmd(evt *tcell.EventKey) *tcell.EventKey { if d.app.InCmdMode() { return evt } - d.cmdBuff.SetActive(true) - - return nil -} - -func (d *Details) clearCmd(*tcell.EventKey) *tcell.EventKey { - if !d.app.InCmdMode() { - return nil - } - d.cmdBuff.Clear() + d.app.ResetPrompt(d.cmdBuff) return nil } @@ -277,7 +245,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey { return d.app.PrevCmd(evt) } - if d.cmdBuff.String() != "" { + if d.cmdBuff.GetText() != "" { d.model.ClearFilter() } d.cmdBuff.SetActive(false) @@ -293,6 +261,7 @@ func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { } else { d.app.Flash().Infof("Log %s saved successfully!", path) } + return nil } @@ -301,6 +270,7 @@ func (d *Details) cpCmd(evt *tcell.EventKey) *tcell.EventKey { if err := clipboard.WriteAll(d.GetText(true)); err != nil { d.app.Flash().Err(err) } + return nil } @@ -310,17 +280,15 @@ func (d *Details) updateTitle() { } fmat := fmt.Sprintf(detailsTitleFmt, d.title, d.subject) - buff := d.cmdBuff.String() + buff := d.cmdBuff.GetText() if buff == "" { d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) return } - search := d.cmdBuff.String() if d.maxRegions != 0 { - search += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions) + buff += fmt.Sprintf("[%d:%d]", d.currentRegion+1, d.maxRegions) } - fmat += fmt.Sprintf(ui.SearchFmt, search) - + fmat += fmt.Sprintf(ui.SearchFmt, buff) d.SetTitle(ui.SkinTitle(fmat, d.app.Styles.Frame())) } diff --git a/internal/view/exec.go b/internal/view/exec.go index 2250b4aa..226833d2 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -9,8 +9,17 @@ import ( "os/signal" "strings" "syscall" + "time" + "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" ) const ( @@ -28,7 +37,7 @@ type shellOpts struct { func runK(a *App, opts shellOpts) bool { bin, err := exec.LookPath("kubectl") if err != nil { - log.Error().Msgf("Unable to find kubectl command in path %v", err) + log.Error().Err(err).Msgf("kubectl command is not in your path") return false } var args []string @@ -43,9 +52,8 @@ func runK(a *App, opts shellOpts) bool { args = append(args, "--kubeconfig", *cfg) } if len(args) > 0 { - opts.args = append(opts.args, args...) + opts.args = append(args, opts.args...) } - opts.binary, opts.background = bin, false return run(a, opts) @@ -63,10 +71,13 @@ func run(a *App, opts shellOpts) bool { } func edit(a *App, opts shellOpts) bool { - bin, err := exec.LookPath(os.Getenv("EDITOR")) + bin, err := exec.LookPath(os.Getenv("K9S_EDITOR")) if err != nil { - log.Error().Msgf("Unable to find editor command in path %v", err) - return false + bin, err = exec.LookPath(os.Getenv("EDITOR")) + if err != nil { + log.Error().Err(err).Msgf("K9S_EDITOR|EDITOR not set") + return false + } } opts.binary, opts.background = bin, false @@ -92,7 +103,6 @@ func execute(opts shellOpts) error { }() log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " ")) - cmd := exec.Command(opts.binary, opts.args...) var err error @@ -115,3 +125,114 @@ func execute(opts shellOpts) error { func clearScreen() { fmt.Print("\033[H\033[2J") } + +const ( + k9sShell = "k9s-shell" + k9sShellNS = "default" + k9sShellRetryCount = 10 + k9sShellRetryDelay = 500 * time.Millisecond +) + +func ssh(a *App, node string) error { + nukeK9sShell(a.Conn()) + defer nukeK9sShell(a.Conn()) + if err := launchShellPod(a, node); err != nil { + return err + } + shellIn(a, client.FQN(k9sShellNS, k9sShell), k9sShell) + + return nil +} + +func nukeK9sShell(c client.Connection) { + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := c.DialOrDie().CoreV1().Pods(k9sShellNS).Delete(ctx, k9sShell, metav1.DeleteOptions{}) + if kerrors.IsNotFound(err) { + return + } + if err != nil { + log.Error().Err(err).Msgf("Fail to delete pod %s", k9sShell) + } +} + +func launchShellPod(a *App, node string) error { + spec := k9sShellPod(node) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + dial := a.Conn().DialOrDie().CoreV1().Pods(k9sShellNS) + if _, err := dial.Create(ctx, &spec, metav1.CreateOptions{}); err != nil { + return err + } + + for i := 0; i < k9sShellRetryCount; i++ { + o, err := a.factory.Get("v1/pods", client.FQN(k9sShellNS, k9sShell), true, labels.Everything()) + if err != nil { + time.Sleep(k9sShellRetryDelay) + continue + } + var pod v1.Pod + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil { + return err + } + if pod.Status.Phase == v1.PodRunning { + return nil + } + time.Sleep(k9sShellRetryDelay) + } + + return fmt.Errorf("Unable to launch shell pod on node %s", node) +} + +func k9sShellPod(node string) v1.Pod { + var grace int64 + var priv bool = true + + return v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: k9sShell, + Namespace: k9sShellNS, + }, + Spec: v1.PodSpec{ + NodeName: node, + RestartPolicy: v1.RestartPolicyNever, + HostPID: true, + HostNetwork: true, + TerminationGracePeriodSeconds: &grace, + Volumes: []v1.Volume{ + { + Name: "root-vol", + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: "/", + }, + }, + }, + }, + Containers: []v1.Container{ + { + Name: k9sShell, + Image: "busybox:1.31", + VolumeMounts: []v1.VolumeMount{ + { + Name: "root-vol", + MountPath: "/host", + ReadOnly: true, + }, + }, + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("200m"), + v1.ResourceMemory: resource.MustParse("100Mi"), + }, + }, + Stdin: true, + SecurityContext: &v1.SecurityContext{ + Privileged: &priv, + }, + }, + }, + }, + } +} diff --git a/internal/view/charts.go b/internal/view/helm.go similarity index 59% rename from internal/view/charts.go rename to internal/view/helm.go index c0ea52fc..1d366b14 100644 --- a/internal/view/charts.go +++ b/internal/view/helm.go @@ -7,20 +7,19 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" - "github.com/rs/zerolog/log" ) -// Chart represents a helm chart view. -type Chart struct { +// Helm represents a helm chart view. +type Helm struct { ResourceViewer } -// NewChart returns a new alias view. -func NewChart(gvr client.GVR) ResourceViewer { - c := Chart{ +// NewHelm returns a new alias view. +func NewHelm(gvr client.GVR) ResourceViewer { + c := Helm{ ResourceViewer: NewBrowser(gvr), } - c.GetTable().SetColorerFn(render.Chart{}.ColorerFunc()) + c.GetTable().SetColorerFn(render.Helm{}.ColorerFunc()) c.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) c.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) c.SetBindKeysFn(c.bindKeys) @@ -29,25 +28,15 @@ func NewChart(gvr client.GVR) ResourceViewer { return &c } -func (c *Chart) chartContext(ctx context.Context) context.Context { +func (c *Helm) chartContext(ctx context.Context) context.Context { return ctx } -func (c *Chart) bindKeys(aa ui.KeyActions) { +func (c *Helm) bindKeys(aa ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Add(ui.KeyActions{ - ui.KeyB: ui.NewKeyAction("Blee", c.bleeCmd, true), ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(nameCol, true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false), ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(ageCol, true), false), }) } - -func (c *Chart) bleeCmd(evt *tcell.EventKey) *tcell.EventKey { - path := c.GetTable().GetSelectedItem() - if path == "" { - return nil - } - log.Debug().Msgf("BLEE CMD %q", path) - return nil -} diff --git a/internal/view/help.go b/internal/view/help.go index 3f935a7d..09886b42 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -58,9 +58,9 @@ func (h *Help) Init(ctx context.Context) error { func (h *Help) bindKeys() { h.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS) h.Actions().Set(ui.KeyActions{ - tcell.KeyEsc: ui.NewKeyAction("Back", h.app.PrevCmd, false), - ui.KeyHelp: ui.NewKeyAction("Back", h.app.PrevCmd, false), - tcell.KeyEnter: ui.NewKeyAction("Back", h.app.PrevCmd, false), + tcell.KeyEscape: ui.NewKeyAction("Back", h.app.PrevCmd, false), + ui.KeyHelp: ui.NewKeyAction("Back", h.app.PrevCmd, false), + tcell.KeyEnter: ui.NewKeyAction("Back", h.app.PrevCmd, false), }) } diff --git a/internal/view/helpers.go b/internal/view/helpers.go index a948126b..e084f856 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -50,9 +50,10 @@ func k8sEnv(c *client.Config) Env { func defaultEnv(c *client.Config, path string, header render.Header, row render.Row) Env { env := k8sEnv(c) + log.Debug().Msgf("PATH %q::%q", path, row.Fields[1]) env["NAMESPACE"], env["NAME"] = client.Namespaced(path) - for i := range header { - env["COL-"+header[i].Name] = row.Fields[i] + for _, col := range header.Columns(true) { + env["COL-"+col] = row.Fields[header.IndexOf(col, true)] } return env @@ -105,7 +106,7 @@ func podCtx(app *App, path, labelSel, fieldSel string) ContextFunc { ns, _ := client.Namespaced(path) mx := client.NewMetricsServer(app.factory.Client()) - nmx, err := mx.FetchPodsMetrics(ns) + nmx, err := mx.FetchPodsMetrics(ctx, ns) if err != nil { log.Debug().Err(err).Msgf("No pods metrics") } diff --git a/internal/view/log.go b/internal/view/log.go index 1f001748..9242a3fa 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -1,6 +1,7 @@ package view import ( + "bytes" "context" "fmt" "io" @@ -10,6 +11,7 @@ import ( "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" @@ -20,14 +22,11 @@ import ( ) const ( - logTitle = "logs" - logMessage = "[:orange:b]Waiting for logs...[::]" - logCoFmt = " Logs([fg:bg:]%s:[hilite:bg:b]%s[-:bg:-]) " - logFmt = " Logs([fg:bg:]%s) " - - // BOZO!! Canned! Need config tail line counts! - tailLineCount = 50 - defaultTimeout = 200 * time.Millisecond + logTitle = "logs" + logMessage = "Waiting for logs..." + logFmt = " Logs([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " + logCoFmt = " Logs([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] " + flushTimeout = 100 * time.Millisecond ) // Log represents a generic log viewer. @@ -38,7 +37,6 @@ type Log struct { logs *Details indicator *LogIndicator ansiWriter io.Writer - cmdBuff *ui.CmdBuff model *model.Log } @@ -47,21 +45,25 @@ var _ model.Component = (*Log)(nil) // NewLog returns a new viewer. func NewLog(gvr client.GVR, path, co string, prev bool) *Log { l := Log{ - Flex: tview.NewFlex(), - cmdBuff: ui.NewCmdBuff('/', ui.FilterBuff), - model: model.NewLog(gvr, buildLogOpts(path, co, prev, true, tailLineCount), defaultTimeout), + Flex: tview.NewFlex(), + model: model.NewLog( + gvr, + buildLogOpts(path, co, prev, true, config.DefaultLoggerTailCount), + flushTimeout, + ), } return &l } -// Init initialiazes the viewer. +// Init initializes the viewer. func (l *Log) Init(ctx context.Context) (err error) { if l.app, err = extractApp(ctx); err != nil { return err } + l.model.Configure(l.app.Config.K9s.Logger) + l.SetBorder(true) - l.SetBorderPadding(0, 0, 1, 1) l.SetDirection(tview.FlexRow) l.indicator = NewLogIndicator(l.app.Config, l.app.Styles) @@ -72,14 +74,15 @@ func (l *Log) Init(ctx context.Context) (err error) { if err = l.logs.Init(ctx); err != nil { return err } + l.logs.SetBorderPadding(0, 0, 1, 1) l.logs.SetText(logMessage) - l.logs.SetWrap(false) - l.logs.SetMaxBuffer(l.app.Config.K9s.LogBufferSize) + l.logs.SetWrap(l.app.Config.K9s.Logger.TextWrap) + l.logs.SetMaxBuffer(l.app.Config.K9s.Logger.BufferSize) + l.logs.cmdBuff.AddListener(l) l.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String()) l.AddItem(l.logs, 0, 1, true) l.bindKeys() - l.logs.SetInputCapture(l.keyboard) l.StylesChanged(l.app.Styles) l.app.Styles.AddListener(l) @@ -89,9 +92,6 @@ func (l *Log) Init(ctx context.Context) (err error) { l.model.AddListener(l) l.updateTitle() - l.cmdBuff.AddListener(l.app.Cmd()) - l.cmdBuff.AddListener(l) - return nil } @@ -107,21 +107,30 @@ func (l *Log) LogCleared() { func (l *Log) LogFailed(err error) { l.app.QueueUpdateDraw(func() { l.app.Flash().Err(err) + if l.logs.GetText(true) == logMessage { + l.logs.Clear() + } + fmt.Fprintln(l.ansiWriter, tview.Escape(color.Colorize(err.Error(), color.Red))) }) } // LogChanged updates the logs. -func (l *Log) LogChanged(lines []string) { +func (l *Log) LogChanged(lines dao.LogItems) { l.app.QueueUpdateDraw(func() { l.Flush(lines) }) } // BufferChanged indicates the buffer was changed. -func (l *Log) BufferChanged(s string) {} +func (l *Log) BufferChanged(s string) { + if err := l.model.Filter(l.logs.cmdBuff.GetText()); err != nil { + l.app.Flash().Err(err) + } + l.updateTitle() +} // BufferActive indicates the buff activity changed. -func (l *Log) BufferActive(state bool, k ui.BufferKind) { +func (l *Log) BufferActive(state bool, k model.BufferKind) { l.app.BufferActive(state, k) } @@ -150,7 +159,6 @@ func (l *Log) ExtraHints() map[string]string { // Start runs the component. func (l *Log) Start() { l.model.Start() - l.app.SetFocus(l) } // Stop terminates the component. @@ -158,8 +166,8 @@ func (l *Log) Stop() { l.model.Stop() l.model.RemoveListener(l) l.app.Styles.RemoveListener(l) - l.cmdBuff.RemoveListener(l) - l.cmdBuff.RemoveListener(l.app.Cmd()) + l.logs.cmdBuff.RemoveListener(l) + l.logs.cmdBuff.RemoveListener(l.app.Prompt()) } // Name returns the component name. @@ -167,43 +175,32 @@ func (l *Log) Name() string { return logTitle } func (l *Log) bindKeys() { l.logs.Actions().Set(ui.KeyActions{ - tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), - tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, true), - ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), - ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.ToggleAutoScrollCmd, true), - ui.KeyF: ui.NewKeyAction("FullScreen", l.fullScreenCmd, true), - ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.textWrapCmd, true), - tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), - ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", l.activateCmd, false), - tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", l.resetCmd, false), - tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), - tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), - tcell.KeyDelete: ui.NewSharedKeyAction("Erase", l.eraseCmd, false), + ui.Key0: ui.NewKeyAction("all", l.sinceCmd(-1), true), + ui.Key1: ui.NewKeyAction("1m", l.sinceCmd(60), true), + ui.Key2: ui.NewKeyAction("5m", l.sinceCmd(5*60), true), + ui.Key3: ui.NewKeyAction("15m", l.sinceCmd(15*60), true), + ui.Key4: ui.NewKeyAction("30m", l.sinceCmd(30*60), true), + ui.Key5: ui.NewKeyAction("1h", l.sinceCmd(60*60), true), + tcell.KeyEnter: ui.NewSharedKeyAction("Filter", l.filterCmd, false), + ui.KeyC: ui.NewKeyAction("Clear", l.clearCmd, true), + ui.KeyS: ui.NewKeyAction("Toggle AutoScroll", l.toggleAutoScrollCmd, true), + ui.KeyF: ui.NewKeyAction("Toggle FullScreen", l.toggleFullScreenCmd, true), + ui.KeyT: ui.NewKeyAction("Toggle Timestamp", l.toggleTimestampCmd, true), + ui.KeyW: ui.NewKeyAction("Toggle Wrap", l.toggleTextWrapCmd, true), + tcell.KeyCtrlS: ui.NewKeyAction("Save", l.SaveCmd, true), }) } -func (l *Log) keyboard(evt *tcell.EventKey) *tcell.EventKey { - key := evt.Key() - if key == tcell.KeyUp || key == tcell.KeyDown { - return evt - } - if key == tcell.KeyRune { - if l.cmdBuff.IsActive() { - l.cmdBuff.Add(evt.Rune()) - if err := l.model.Filter(l.cmdBuff.String()); err != nil { - l.app.Flash().Err(err) - } - l.updateTitle() - return nil - } - key = extractKey(evt) - } +// SendStrokes (testing only!) +func (l *Log) SendStrokes(s string) { + l.app.Prompt().SendStrokes(s) +} - if a, ok := l.logs.Actions()[key]; ok { - return a.Action(evt) +// SendKeys (testing only!) +func (l *Log) SendKeys(kk ...tcell.Key) { + for _, k := range kk { + l.logs.keyboard(tcell.NewEventKey(k, ' ', tcell.ModNone)) } - - return evt } // Indicator returns the scroll mode viewer. @@ -212,19 +209,26 @@ func (l *Log) Indicator() *LogIndicator { } func (l *Log) updateTitle() { - var fmat string + sinceSeconds, since := l.model.SinceSeconds(), "all" + if sinceSeconds > 0 && sinceSeconds < 60*60 { + since = fmt.Sprintf("%dm", sinceSeconds/60) + } + if sinceSeconds >= 60*60 { + since = fmt.Sprintf("%dh", sinceSeconds/(60*60)) + } + var title string path, co := l.model.GetPath(), l.model.GetContainer() if co == "" { - fmat = ui.SkinTitle(fmt.Sprintf(logFmt, path), l.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(logFmt, path, since), l.app.Styles.Frame()) } else { - fmat = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co), l.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), l.app.Styles.Frame()) } - buff := l.cmdBuff.String() + buff := l.logs.cmdBuff.GetText() if buff != "" { - fmat += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame()) + title += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), l.app.Styles.Frame()) } - l.SetTitle(fmat) + l.SetTitle(title) } // Logs returns the log viewer. @@ -232,66 +236,41 @@ func (l *Log) Logs() *Details { return l.logs } -func (l *Log) write(lines string) { - fmt.Fprintln(l.ansiWriter, tview.Escape(lines)) -} - // Flush write logs to viewer. -func (l *Log) Flush(lines []string) { - l.write(strings.Join(lines, "\n")) - l.indicator.Refresh() +func (l *Log) Flush(lines dao.LogItems) { + defer func(t time.Time) { + log.Debug().Msgf("FLUSH %d--%v", len(lines), time.Since(t)) + }(time.Now()) + + showTime := l.Indicator().showTime + ll := make([][]byte, len(lines)) + lines.Render(showTime, ll) + fmt.Fprintln(l.ansiWriter, string(bytes.Join(ll, []byte("\n")))) l.logs.ScrollToEnd() + l.indicator.Refresh() } // ---------------------------------------------------------------------------- // Actions()... -func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { - if !l.cmdBuff.IsActive() { - return evt - } - l.cmdBuff.SetActive(false) - if err := l.model.Filter(l.cmdBuff.String()); err != nil { - l.app.Flash().Err(err) - } - l.updateTitle() - - return nil -} - -func (l *Log) activateCmd(evt *tcell.EventKey) *tcell.EventKey { - if l.app.InCmdMode() { - return evt - } - l.cmdBuff.SetActive(true) - - return nil -} - -func (l *Log) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { - if !l.cmdBuff.IsActive() { +func (l *Log) sinceCmd(a int) func(evt *tcell.EventKey) *tcell.EventKey { + return func(evt *tcell.EventKey) *tcell.EventKey { + opts := l.model.LogOptions() + opts.SinceSeconds = int64(a) + l.model.SetLogOptions(opts) + l.updateTitle() return nil } - l.cmdBuff.Delete() - if err := l.model.Filter(l.cmdBuff.String()); err != nil { - l.app.Flash().Err(err) - } - l.updateTitle() - - return nil } -func (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey { - if !l.cmdBuff.InCmdMode() { - l.cmdBuff.Reset() - return l.app.PrevCmd(evt) +func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { + if !l.logs.cmdBuff.IsActive() { + return evt } - - if l.cmdBuff.String() != "" { - l.model.ClearFilter() + l.logs.cmdBuff.SetActive(false) + if err := l.model.Filter(l.logs.cmdBuff.GetText()); err != nil { + l.app.Flash().Err(err) } - l.cmdBuff.SetActive(false) - l.cmdBuff.Reset() l.updateTitle() return nil @@ -345,14 +324,33 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey { return nil } -func (l *Log) textWrapCmd(*tcell.EventKey) *tcell.EventKey { +func (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey { + if l.app.InCmdMode() { + return evt + } + + l.indicator.ToggleTimestamp() + l.model.Refresh() + + return nil +} + +func (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey { + if l.app.InCmdMode() { + return evt + } + l.indicator.ToggleTextWrap() l.logs.SetWrap(l.indicator.textWrap) return nil } // ToggleAutoScrollCmd toggles autoscroll status. -func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { +func (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { + if l.app.InCmdMode() { + return evt + } + l.indicator.ToggleAutoScroll() if l.indicator.AutoScroll() { l.model.Start() @@ -362,34 +360,23 @@ func (l *Log) ToggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (l *Log) fullScreenCmd(*tcell.EventKey) *tcell.EventKey { +func (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey { + if l.app.InCmdMode() { + return evt + } l.indicator.ToggleFullScreen() l.goFullScreen() return nil } func (l *Log) goFullScreen() { - sidePadding := 1 - if l.indicator.FullScreen() { - sidePadding = 0 - } l.SetFullScreen(l.indicator.FullScreen()) l.Box.SetBorder(!l.indicator.FullScreen()) - l.Flex.SetBorderPadding(0, 0, sidePadding, sidePadding) } // ---------------------------------------------------------------------------- // Helpers... -// AsKey converts rune to keyboard key., -func extractKey(evt *tcell.EventKey) tcell.Key { - key := tcell.Key(evt.Rune()) - if evt.Modifiers() == tcell.ModAlt { - key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers())) - } - return key -} - func buildLogOpts(path, co string, prevLogs, showTime bool, tailLineCount int) dao.LogOptions { return dao.LogOptions{ Path: path, diff --git a/internal/view/log_dialog.go b/internal/view/log_dialog.go new file mode 100644 index 00000000..19bda1dd --- /dev/null +++ b/internal/view/log_dialog.go @@ -0,0 +1,80 @@ +package view + +import ( + "fmt" + "strconv" + "time" + + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" +) + +const logKey = "logs" + +// LogCB represents a log callback function. +type LogCB func(path string, opts dao.LogOptions) + +// ShowLogs pops a port forwarding configuration dialog. +func ShowLogs(a *App, path string, applyFn LogCB) { + styles := a.Styles + + f := tview.NewForm() + f.SetItemPadding(0) + f.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(styles.BgColor()). + SetButtonTextColor(styles.FgColor()). + SetLabelColor(styles.K9s.Info.FgColor.Color()). + SetFieldTextColor(styles.K9s.Info.SectionColor.Color()) + + secs, start, in, out, container := "5", time.Now().String(), "", "", "" + f.AddInputField("Container:", container, 0, nil, func(v string) { + container = v + }) + f.AddInputField("Since Seconds:", secs, 0, nil, func(v string) { + secs = v + }) + f.AddInputField("Since Time:", start, 0, nil, func(v string) { + start = v + }) + f.AddInputField("Filter In:", in, 0, nil, func(v string) { + in = v + }) + f.AddInputField("Filter Out:", out, 0, nil, func(v string) { + out = v + }) + + pages := a.Content.Pages + + f.AddButton("Apply", func() { + s, _ := strconv.Atoi(secs) + opts := dao.LogOptions{ + SinceTime: start, + SinceSeconds: int64(s), + In: in, + Out: out, + } + applyFn(path, opts) + }) + f.AddButton("Dismiss", func() { + DismissLogs(a, pages) + }) + + modal := tview.NewModalForm(fmt.Sprintf("", path), f) + modal.SetDoneFunc(func(_ int, b string) { + DismissLogs(a, pages) + }) + + pages.AddPage(logKey, modal, false, true) + pages.ShowPage(logKey) + a.SetFocus(pages.GetPrimitive(logKey)) +} + +// DismissLogs dismiss the dialog. +func DismissLogs(a *App, p *ui.Pages) { + p.RemovePage(logKey) + a.SetFocus(p.CurrentPage().Item) +} + +// ---------------------------------------------------------------------------- +// Helpers... diff --git a/internal/view/log_indicator.go b/internal/view/log_indicator.go index 989303c5..5696a0b4 100644 --- a/internal/view/log_indicator.go +++ b/internal/view/log_indicator.go @@ -25,15 +25,24 @@ func NewLogIndicator(cfg *config.Config, styles *config.Styles) *LogIndicator { styles: styles, TextView: tview.NewTextView(), scrollStatus: 1, - fullScreen: cfg.K9s.FullScreenLogs, + fullScreen: cfg.K9s.Logger.FullScreenLogs, + textWrap: cfg.K9s.Logger.TextWrap, + showTime: cfg.K9s.Logger.ShowTime, } - l.SetBackgroundColor(styles.Views().Log.BgColor.Color()) - l.SetTextAlign(tview.AlignRight) + l.StylesChanged(styles) + styles.AddListener(&l) + l.SetTextAlign(tview.AlignCenter) l.SetDynamicColors(true) return &l } +// StylesChanged notifies listener the skin changed. +func (l *LogIndicator) StylesChanged(styles *config.Styles) { + l.SetBackgroundColor(styles.K9s.Views.Log.Indicator.BgColor.Color()) + l.SetTextColor(styles.K9s.Views.Log.Indicator.FgColor.Color()) +} + // AutoScroll reports the current scrolling status. func (l *LogIndicator) AutoScroll() bool { return atomic.LoadInt32(&l.scrollStatus) == 1 @@ -86,8 +95,7 @@ func (l *LogIndicator) Refresh() { l.Clear() l.update("Autoscroll: " + l.onOff(l.AutoScroll())) l.update("FullScreen: " + l.onOff(l.fullScreen)) - // BOZO!! log timestamp - // l.update("Timestamp: " + l.onOff(l.showTime)) + l.update("Timestamps: " + l.onOff(l.showTime)) l.update("Wrap: " + l.onOff(l.textWrap)) } @@ -99,6 +107,5 @@ func (l *LogIndicator) onOff(b bool) string { } func (l *LogIndicator) update(status string) { - fg, bg := l.styles.Frame().Crumb.FgColor, l.styles.Frame().Crumb.ActiveColor - fmt.Fprintf(l, "[%s:%s:b] %-15s ", fg, bg, status) + fmt.Fprintf(l, "[::b]%-20s", status) } diff --git a/internal/view/log_indicator_test.go b/internal/view/log_indicator_test.go index 2a76ed74..6aeb19e2 100644 --- a/internal/view/log_indicator_test.go +++ b/internal/view/log_indicator_test.go @@ -13,5 +13,5 @@ func TestLogIndicatorRefresh(t *testing.T) { v := view.NewLogIndicator(config.NewConfig(nil), defaults) v.Refresh() - assert.Equal(t, "[black:orange:b] Autoscroll: On [black:orange:b] FullScreen: Off [black:orange:b] Wrap: Off \n", v.GetText(false)) + assert.Equal(t, "[::b]Autoscroll: On [::b]FullScreen: Off [::b]Timestamps: Off [::b]Wrap: Off \n", v.GetText(false)) } diff --git a/internal/view/log_int_test.go b/internal/view/log_int_test.go new file mode 100644 index 00000000..b355f104 --- /dev/null +++ b/internal/view/log_int_test.go @@ -0,0 +1,110 @@ +package view + +import ( + "fmt" + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/ui" + "github.com/stretchr/testify/assert" +) + +func TestLogAutoScroll(t *testing.T) { + v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false) + v.Init(makeContext()) + v.GetModel().Set(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")}) + v.GetModel().Notify(true) + + assert.Equal(t, 13, len(v.Hints())) + + v.toggleAutoScrollCmd(nil) + assert.Equal(t, "Autoscroll: Off FullScreen: Off Timestamps: Off Wrap: Off ", v.Indicator().GetText(true)) +} + +func TestLogViewNav(t *testing.T) { + v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false) + v.Init(makeContext()) + + var buff dao.LogItems + for i := 0; i < 100; i++ { + buff = append(buff, dao.NewLogItemFromString(fmt.Sprintf("line-%d\n", i))) + } + v.GetModel().Set(buff) + v.toggleAutoScrollCmd(nil) + + r, _ := v.Logs().GetScrollOffset() + assert.Equal(t, 0, r) +} + +func TestLogViewClear(t *testing.T) { + v := NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false) + v.Init(makeContext()) + + v.toggleAutoScrollCmd(nil) + v.Logs().SetText("blee\nblah") + v.Logs().Clear() + + assert.Equal(t, "", v.Logs().GetText(true)) +} + +func TestLogTimestamp(t *testing.T) { + l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false) + l.Init(makeContext()) + buff := dao.LogItems{ + &dao.LogItem{ + Pod: "fred/blee", + Container: "c1", + Timestamp: "ttt", + Bytes: []byte("Testing 1, 2, 3"), + }, + } + var list logList + l.GetModel().AddListener(&list) + l.GetModel().Set(buff) + l.SendKeys(ui.KeyT) + l.Logs().Clear() + l.Flush(buff) + + assert.Equal(t, fmt.Sprintf("%-30s %s", "ttt", "fred/blee:c1 Testing 1, 2, 3\n"), l.Logs().GetText(true)) + assert.Equal(t, 2, list.change) + assert.Equal(t, 2, list.clear) + assert.Equal(t, 0, list.fail) +} + +func TestLogFilter(t *testing.T) { + l := NewLog(client.NewGVR("test"), "fred/blee", "c1", false) + l.Init(makeContext()) + buff := dao.LogItems{ + dao.NewLogItemFromString("duh"), + dao.NewLogItemFromString("zorg"), + } + var list logList + l.GetModel().AddListener(&list) + l.GetModel().Set(buff) + l.SendKeys(ui.KeySlash) + l.SendStrokes("zorg") + + assert.Equal(t, "zorg", list.lines) + assert.Equal(t, 5, list.change) + assert.Equal(t, 5, list.clear) + assert.Equal(t, 0, list.fail) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type logList struct { + change, clear, fail int + lines string +} + +func (l *logList) LogChanged(ii dao.LogItems) { + l.change++ + l.lines = "" + for _, i := range ii { + l.lines += string(i.Render(0, false)) + } +} +func (l *logList) LogCleared() { l.clear++ } +func (l *logList) LogFailed(error) { l.fail++ } diff --git a/internal/view/log_test.go b/internal/view/log_test.go index bb13df3b..049636a3 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -9,6 +9,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/view" "github.com/derailed/tview" "github.com/stretchr/testify/assert" @@ -28,24 +29,12 @@ func TestLogAnsi(t *testing.T) { assert.Equal(t, s+"\n", v.GetText(false)) } -func TestLogAutoScroll(t *testing.T) { - v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false) - v.Init(makeContext()) - v.GetModel().Set([]string{"blee", "bozo"}) - v.GetModel().Notify(true) - - assert.Equal(t, 6, len(v.Hints())) - - v.ToggleAutoScrollCmd(nil) - assert.Equal(t, " Autoscroll: Off FullScreen: Off Wrap: Off ", v.Indicator().GetText(true)) -} - func TestLogViewSave(t *testing.T) { v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false) v.Init(makeContext()) app := makeApp() - v.Flush([]string{"blee", "bozo"}) + v.Flush(dao.LogItems{dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo")}) config.K9sDumpDir = "/tmp" dir := filepath.Join(config.K9sDumpDir, app.Config.K9s.CurrentCluster) c1, _ := ioutil.ReadDir(dir) @@ -54,31 +43,6 @@ func TestLogViewSave(t *testing.T) { assert.Equal(t, len(c2), len(c1)+1) } -func TestLogViewNav(t *testing.T) { - v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false) - v.Init(makeContext()) - - var buff []string - for i := 0; i < 100; i++ { - buff = append(buff, fmt.Sprintf("line-%d\n", i)) - } - v.GetModel().Set(buff) - v.ToggleAutoScrollCmd(nil) - - r, _ := v.Logs().GetScrollOffset() - assert.Equal(t, 0, r) -} - -func TestLogViewClear(t *testing.T) { - v := view.NewLog(client.NewGVR("v1/pods"), "fred/p1", "blee", false) - v.Init(makeContext()) - - v.ToggleAutoScrollCmd(nil) - v.Logs().SetText("blee\nblah") - v.Logs().Clear() - assert.Equal(t, "", v.Logs().GetText(true)) -} - // ---------------------------------------------------------------------------- // Helpers... diff --git a/internal/view/node.go b/internal/view/node.go index af9fb4fc..d4b23f7c 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -2,6 +2,7 @@ package view import ( "bytes" + "context" "fmt" "strings" "time" @@ -11,6 +12,7 @@ import ( "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -42,6 +44,13 @@ func (n *Node) bindKeys(aa ui.KeyActions) { ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", n.GetTable().SortColCmd("%CPU", false), false), ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", n.GetTable().SortColCmd("%MEM", false), false), }) + + cl := n.App().Config.K9s.CurrentCluster + if n.App().Config.K9s.Clusters[cl].FeatureGates.NodeShell { + aa.Add(ui.KeyActions{ + ui.KeyS: ui.NewKeyAction("Shell", n.sshCmd, true), + }) + } } func (n *Node) showPods(app *App, _ ui.Tabular, _, path string) { @@ -126,15 +135,32 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve } } +func (n *Node) sshCmd(evt *tcell.EventKey) *tcell.EventKey { + path := n.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + _, node := client.Namespaced(path) + if err := ssh(n.App(), node); err != nil { + log.Error().Err(err).Msgf("SSH Failed") + } + + return nil +} + func (n *Node) yamlCmd(evt *tcell.EventKey) *tcell.EventKey { path := n.GetTable().GetSelectedItem() if path == "" { return evt } + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() + sel := n.GetTable().GetSelectedItem() gvr := n.GVR().GVR() - o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(sel, metav1.GetOptions{}) + o, err := n.App().factory.Client().DynDialOrDie().Resource(gvr).Get(ctx, sel, metav1.GetOptions{}) if err != nil { n.App().Flash().Errf("Unable to get resource %q -- %s", n.GVR(), err) return nil diff --git a/internal/view/pf.go b/internal/view/pf.go index 8c2f6deb..cc89788a 100644 --- a/internal/view/pf.go +++ b/internal/view/pf.go @@ -116,8 +116,8 @@ func (p *PortForward) runBenchmark() { } func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { - if !p.GetTable().SearchBuff().Empty() { - p.GetTable().SearchBuff().Reset() + if !p.GetTable().CmdBuff().Empty() { + p.GetTable().CmdBuff().Reset() return nil } diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index 7f1c44b9..718570b0 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -11,11 +11,11 @@ import ( const portForwardKey = "portforward" -// PortForwardFunc represents a port-forward callback function. -type PortForwardFunc func(v ResourceViewer, path, co string, mapper client.PortTunnel) +// PortForwardCB represents a port-forward callback function. +type PortForwardCB func(v ResourceViewer, path, co string, mapper client.PortTunnel) // ShowPortForwards pops a port forwarding configuration dialog. -func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardFunc) { +func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardCB) { styles := v.App().Styles f := tview.NewForm() diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index ac110d56..5f9ae132 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -123,7 +123,7 @@ func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) { go runForward(v, pf, fwd) } -func showFwdDialog(v ResourceViewer, path string, cb PortForwardFunc) error { +func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { mm, err := fetchPodPorts(v.App().factory, path) if err != nil { return nil diff --git a/internal/view/pod.go b/internal/view/pod.go index 2383adb9..1219d1d0 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -184,7 +184,7 @@ func resumeShellIn(a *App, c model.Component, path, co string) { } func shellIn(a *App, path, co string) { - args := computeShellArgs(path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) + args := computeShellArgs(path, co, a.Conn().Config().Flags().KubeConfig) c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) if !runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}) { @@ -226,24 +226,25 @@ func resumeAttachIn(a *App, c model.Component, path, co string) { } func attachIn(a *App, path, co string) { - args := buildShellArgs("attach", path, co, a.Config.K9s.CurrentContext, a.Conn().Config().Flags().KubeConfig) + args := buildShellArgs("attach", path, co, a.Conn().Config().Flags().KubeConfig) c := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold) if !runK(a, shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}) { a.Flash().Err(errors.New("Attach exec failed")) } } -func computeShellArgs(path, co, context string, kcfg *string) []string { - args := buildShellArgs("exec", path, co, context, kcfg) +func computeShellArgs(path, co string, kcfg *string) []string { + args := buildShellArgs("exec", path, co, kcfg) return append(args, "--", "sh", "-c", shellCheck) } -func buildShellArgs(cmd, path, co, context string, kcfg *string) []string { +func buildShellArgs(cmd, path, co string, kcfg *string) []string { args := make([]string, 0, 15) args = append(args, cmd, "-it") - args = append(args, "--context", context) ns, po := client.Namespaced(path) - args = append(args, "-n", ns) + if ns != client.AllNamespaces { + args = append(args, "-n", ns) + } args = append(args, po) if kcfg != nil && *kcfg != "" { args = append(args, "--kubeconfig", *kcfg) diff --git a/internal/view/pod_int_test.go b/internal/view/pod_int_test.go index 2351bade..53de43ec 100644 --- a/internal/view/pod_int_test.go +++ b/internal/view/pod_int_test.go @@ -10,44 +10,40 @@ import ( func TestComputeShellArgs(t *testing.T) { config, empty := "coolConfig", "" uu := map[string]struct { - path, co, context string - cfg *string - e string + path, co string + cfg *string + e string }{ "config": { "fred/blee", "c1", - "ctx1", &config, - "exec -it --context ctx1 -n fred blee --kubeconfig coolConfig -c c1 -- sh -c " + shellCheck, + "exec -it -n fred blee --kubeconfig coolConfig -c c1 -- sh -c " + shellCheck, }, "noconfig": { "fred/blee", "c1", - "ctx1", nil, - "exec -it --context ctx1 -n fred blee -c c1 -- sh -c " + shellCheck, + "exec -it -n fred blee -c c1 -- sh -c " + shellCheck, }, "emptyConfig": { "fred/blee", "c1", - "ctx1", &empty, - "exec -it --context ctx1 -n fred blee -c c1 -- sh -c " + shellCheck, + "exec -it -n fred blee -c c1 -- sh -c " + shellCheck, }, "singleContainer": { "fred/blee", "", - "ctx1", &empty, - "exec -it --context ctx1 -n fred blee -- sh -c " + shellCheck, + "exec -it -n fred blee -- sh -c " + shellCheck, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - args := computeShellArgs(u.path, u.co, u.context, u.cfg) + args := computeShellArgs(u.path, u.co, u.cfg) assert.Equal(t, u.e, strings.Join(args, " ")) }) diff --git a/internal/view/popeye.go b/internal/view/popeye.go new file mode 100644 index 00000000..e2999073 --- /dev/null +++ b/internal/view/popeye.go @@ -0,0 +1,114 @@ +package view + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/gdamore/tcell" +) + +// Popeye represents a sanitizer view. +type Popeye struct { + ResourceViewer +} + +// NewPopeye returns a new view. +func NewPopeye(gvr client.GVR) ResourceViewer { + p := Popeye{ + ResourceViewer: NewBrowser(gvr), + } + p.GetTable().SetColorerFn(render.Popeye{}.ColorerFunc()) + p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) + p.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) + p.GetTable().SetSortCol("SCORE%", true) + p.GetTable().SetDecorateFn(p.decorateRows) + p.SetBindKeysFn(p.bindKeys) + + return &p +} + +// Init initializes the view. +func (p *Popeye) Init(ctx context.Context) error { + if err := p.ResourceViewer.Init(ctx); err != nil { + return err + } + p.GetTable().GetModel().SetNamespace("*") + p.GetTable().GetModel().SetRefreshRate(5 * time.Second) + + return nil +} + +func (p *Popeye) decorateRows(data render.TableData) render.TableData { + var sum int + for _, re := range data.RowEvents { + n, err := strconv.Atoi(re.Row.Fields[1]) + if err != nil { + continue + } + sum += n + } + score := sum / len(data.RowEvents) + p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, grade(score)) + return data +} + +func (p *Popeye) bindKeys(aa ui.KeyActions) { + aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Add(ui.KeyActions{ + tcell.KeyEnter: ui.NewKeyAction("Goto", p.describeCmd, true), + ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false), + ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false), + ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false), + ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false), + ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false), + ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false), + }) +} + +func (p *Popeye) describeCmd(evt *tcell.EventKey) *tcell.EventKey { + path := p.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + v := NewSanitizer(client.NewGVR("sanitizer")) + v.SetContextFn(sanitizerCtx(path)) + + if err := p.App().inject(v); err != nil { + p.App().Flash().Err(err) + } + + return nil +} + +func sanitizerCtx(path string) ContextFunc { + return func(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeyPath, path) + return ctx + } +} + +// Helpers... + +func grade(score int) string { + switch { + case score >= 90: + return "A" + case score >= 80: + return "B" + case score >= 70: + return "C" + case score >= 60: + return "D" + case score >= 50: + return "E" + default: + return "F" + } +} diff --git a/internal/view/registrar.go b/internal/view/registrar.go index 9adf1eb4..b2979686 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -21,8 +21,8 @@ func loadCustomViewers() MetaViewers { } func helmViewers(vv MetaViewers) { - vv[client.NewGVR("charts")] = MetaViewer{ - viewerFn: NewChart, + vv[client.NewGVR("helm")] = MetaViewer{ + viewerFn: NewHelm, } } @@ -72,6 +72,13 @@ func miscViewers(vv MetaViewers) { vv[client.NewGVR("pulses")] = MetaViewer{ viewerFn: NewPulse, } + vv[client.NewGVR("popeye")] = MetaViewer{ + viewerFn: NewPopeye, + } + vv[client.NewGVR("sanitizer")] = MetaViewer{ + viewerFn: NewSanitizer, + } + } func appsViewers(vv MetaViewers) { diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index e2b4b001..10f36e1c 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -1,9 +1,11 @@ package view import ( + "context" "errors" "fmt" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -43,8 +45,10 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { msg = fmt.Sprintf("Restart %d deployments?", len(paths)) } dialog.ShowConfirm(r.App().Content.Pages, "Confirm Restart", msg, func() { + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() for _, path := range paths { - if err := r.restartRollout(path); err != nil { + if err := r.restartRollout(ctx, path); err != nil { r.App().Flash().Err(err) } else { r.App().Flash().Infof("Rollout restart in progress for `%s...", path) @@ -55,7 +59,7 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (r *RestartExtender) restartRollout(path string) error { +func (r *RestartExtender) restartRollout(ctx context.Context, path string) error { res, err := dao.AccessorFor(r.App().factory, r.GVR()) if err != nil { return nil @@ -65,5 +69,5 @@ func (r *RestartExtender) restartRollout(path string) error { return errors.New("resource is not restartable") } - return s.Restart(path) + return s.Restart(ctx, path) } diff --git a/internal/view/sanitizer.go b/internal/view/sanitizer.go new file mode 100644 index 00000000..58f2a07e --- /dev/null +++ b/internal/view/sanitizer.go @@ -0,0 +1,416 @@ +package view + +import ( + "context" + "fmt" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/xray" + "github.com/derailed/tview" + "github.com/gdamore/tcell" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ ResourceViewer = (*Sanitizer)(nil) + +// Sanitizer represents an sanitizer tree view. +type Sanitizer struct { + *ui.Tree + + app *App + gvr client.GVR + meta metav1.APIResource + model *model.Tree + cancelFn context.CancelFunc + envFn EnvFunc + contextFn ContextFunc +} + +// NewSanitizer returns a new view. +func NewSanitizer(gvr client.GVR) ResourceViewer { + return &Sanitizer{ + gvr: gvr, + Tree: ui.NewTree(), + model: model.NewTree(gvr), + } +} + +// Init initializes the view +func (s *Sanitizer) Init(ctx context.Context) error { + s.envFn = s.k9sEnv + + if err := s.Tree.Init(ctx); err != nil { + return err + } + s.SetKeyListenerFn(s.keyEntered) + + var err error + s.meta, err = dao.MetaAccess.MetaFor(s.gvr) + if err != nil { + return err + } + + if s.app, err = extractApp(ctx); err != nil { + return err + } + + s.bindKeys() + s.SetBackgroundColor(s.app.Styles.Xray().BgColor.Color()) + s.SetBorderColor(s.app.Styles.Xray().FgColor.Color()) + s.SetBorderFocusColor(s.app.Styles.Frame().Border.FocusColor.Color()) + s.SetGraphicsColor(s.app.Styles.Xray().GraphicColor.Color()) + s.SetTitle(strings.Title(s.gvr.R())) + + s.model.SetNamespace(client.CleanseNamespace(s.app.Config.ActiveNamespace())) + s.model.AddListener(s) + + s.SetChangedFunc(func(n *tview.TreeNode) { + spec, ok := n.GetReference().(xray.NodeSpec) + if !ok { + log.Error().Msgf("No ref found on node %s", n.GetText()) + return + } + s.SetSelectedItem(spec.AsPath()) + s.refreshActions() + }) + s.refreshActions() + + return nil +} + +// ExtraHints returns additional hints. +func (s *Sanitizer) ExtraHints() map[string]string { + if s.app.Config.K9s.NoIcons { + return nil + } + return xray.EmojiInfo() +} + +// SetInstance sets specific resource instance. +func (s *Sanitizer) SetInstance(string) {} + +func (s *Sanitizer) bindKeys() { + s.Actions().Add(ui.KeyActions{ + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", s.activateCmd, false), + tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", s.resetCmd, false), + tcell.KeyEnter: ui.NewKeyAction("Goto", s.gotoCmd, true), + }) +} + +func (s *Sanitizer) keyEntered() { + s.ClearSelection() + s.update(s.filter(s.model.Peek())) +} + +func (s *Sanitizer) refreshActions() {} + +// GetSelectedPath returns the current selection as string. +func (s *Sanitizer) GetSelectedPath() string { + spec := s.selectedSpec() + if spec == nil { + return "" + } + return spec.Path() +} + +func (s *Sanitizer) selectedSpec() *xray.NodeSpec { + node := s.GetCurrentNode() + if node == nil { + return nil + } + + ref, ok := node.GetReference().(xray.NodeSpec) + if !ok { + log.Error().Msgf("Expecting a NodeSpec!") + return nil + } + + return &ref +} + +// EnvFn returns an plugin env function if available. +func (s *Sanitizer) EnvFn() EnvFunc { + return s.envFn +} + +func (s *Sanitizer) k9sEnv() Env { + env := k8sEnv(s.app.Conn().Config()) + + spec := s.selectedSpec() + if spec == nil { + return env + } + + env["FILTER"] = s.CmdBuff().GetText() + if env["FILTER"] == "" { + ns, n := client.Namespaced(spec.Path()) + env["NAMESPACE"], env["FILTER"] = ns, n + } + + switch spec.GVR() { + case "containers": + _, co := client.Namespaced(spec.Path()) + env["CONTAINER"] = co + ns, n := client.Namespaced(*spec.ParentPath()) + env["NAMESPACE"], env["POD"], env["NAME"] = ns, n, co + default: + ns, n := client.Namespaced(spec.Path()) + env["NAMESPACE"], env["NAME"] = ns, n + } + + return env +} + +// Aliases returns all available aliases. +func (s *Sanitizer) Aliases() []string { + return append(s.meta.ShortNames, s.meta.SingularName, s.meta.Name) +} + +func (s *Sanitizer) activateCmd(evt *tcell.EventKey) *tcell.EventKey { + if s.app.InCmdMode() { + return evt + } + s.app.ResetPrompt(s.CmdBuff()) + + return nil +} + +func (s *Sanitizer) resetCmd(evt *tcell.EventKey) *tcell.EventKey { + if !s.CmdBuff().InCmdMode() { + s.CmdBuff().Reset() + return s.app.PrevCmd(evt) + } + s.CmdBuff().Reset() + s.model.ClearFilter() + s.Start() + + return nil +} + +func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { + if s.CmdBuff().IsActive() { + if ui.IsLabelSelector(s.CmdBuff().GetText()) { + s.Start() + } + s.CmdBuff().SetActive(false) + s.GetRoot().ExpandAll() + return nil + } + + spec := s.selectedSpec() + if spec == nil { + return nil + } + if len(spec.GVRs) <= 2 { + return nil + } + path := strings.Replace(spec.Path(), "::", "/", 1) + if strings.Contains(path, "[") { + return nil + } + if len(strings.Split(path, "/")) == 1 && spec.GVR() != "node" { + path = "-/" + path + } + if err := s.app.gotoResource(client.NewGVR(spec.GVR()).R(), path, false); err != nil { + log.Debug().Err(err) + } + + return nil +} + +func (s *Sanitizer) filter(root *xray.TreeNode) *xray.TreeNode { + q := s.CmdBuff().GetText() + if s.CmdBuff().Empty() || ui.IsLabelSelector(q) { + return root + } + + s.UpdateTitle() + if ui.IsFuzzySelector(q) { + return root.Filter(q, fuzzyFilter) + } + + return root.Filter(q, rxFilter) +} + +// TreeNodeSelected callback for node selection. +func (s *Sanitizer) TreeNodeSelected() { + s.app.QueueUpdateDraw(func() { + n := s.GetCurrentNode() + if n != nil { + n.SetColor(s.app.Styles.Xray().CursorColor.Color()) + } + }) +} + +// TreeLoadFailed notifies the load failed. +func (s *Sanitizer) TreeLoadFailed(err error) { + s.app.Flash().Err(err) +} + +func (s *Sanitizer) update(node *xray.TreeNode) { + root := makeTreeNode(node, s.ExpandNodes(), s.app.Config.K9s.NoIcons, s.app.Styles) + if node == nil { + s.app.QueueUpdateDraw(func() { + s.SetRoot(root) + }) + return + } + + for _, c := range node.Children { + s.hydrate(root, c) + } + if s.GetSelectedItem() == "" { + s.SetSelectedItem(node.Spec().Path()) + } + + s.app.QueueUpdateDraw(func() { + s.SetRoot(root) + root.Walk(func(node, parent *tview.TreeNode) bool { + spec, ok := node.GetReference().(xray.NodeSpec) + if !ok { + log.Error().Msgf("Expecting a NodeSpec but got %T", node.GetReference()) + return false + } + // BOZO!! Figure this out expand/collapse but the root + if parent != nil { + node.SetExpanded(s.ExpandNodes()) + } else { + node.SetExpanded(true) + } + + if spec.AsPath() == s.GetSelectedItem() { + node.SetExpanded(true).SetSelectable(true) + s.SetCurrentNode(node) + } + return true + }) + }) +} + +// TreeChanged notifies the model data changed. +func (s *Sanitizer) TreeChanged(node *xray.TreeNode) { + s.Count = node.Count(s.gvr.String()) + s.update(s.filter(node)) + s.UpdateTitle() +} + +func (s *Sanitizer) hydrate(parent *tview.TreeNode, n *xray.TreeNode) { + node := makeTreeNode(n, s.ExpandNodes(), s.app.Config.K9s.NoIcons, s.app.Styles) + for _, c := range n.Children { + s.hydrate(node, c) + } + parent.AddChild(node) +} + +// SetEnvFn sets the custom environment function. +func (s *Sanitizer) SetEnvFn(EnvFunc) {} + +// Refresh updates the view +func (s *Sanitizer) Refresh() {} + +// BufferChanged indicates the buffer was changed. +func (s *Sanitizer) BufferChanged(q string) { + s.update(s.filter(s.model.Peek())) +} + +// BufferActive indicates the buff activity changed. +func (s *Sanitizer) BufferActive(state bool, k model.BufferKind) { + s.app.BufferActive(state, k) +} + +func (s *Sanitizer) defaultContext() context.Context { + ctx := context.WithValue(context.Background(), internal.KeyFactory, s.app.factory) + ctx = context.WithValue(ctx, internal.KeyFields, "") + if s.CmdBuff().Empty() { + ctx = context.WithValue(ctx, internal.KeyLabels, "") + } else { + ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(s.CmdBuff().GetText())) + } + + return ctx +} + +// Start initializes resource watch loop. +func (s *Sanitizer) Start() { + s.Stop() + s.CmdBuff().AddListener(s) + + ctx := s.defaultContext() + ctx, s.cancelFn = context.WithCancel(ctx) + if s.contextFn != nil { + ctx = s.contextFn(ctx) + } + s.model.Refresh(ctx) + s.UpdateTitle() +} + +// Stop terminates watch loop. +func (s *Sanitizer) Stop() { + if s.cancelFn == nil { + return + } + s.cancelFn() + s.cancelFn = nil + s.CmdBuff().RemoveListener(s) +} + +// SetBindKeysFn sets up extra key bindings. +func (s *Sanitizer) SetBindKeysFn(BindKeysFunc) {} + +// SetContextFn sets custom context. +func (s *Sanitizer) SetContextFn(f ContextFunc) { + s.contextFn = f +} + +// Name returns the component name. +func (s *Sanitizer) Name() string { return "report" } + +// GetTable returns the underlying table. +func (s *Sanitizer) GetTable() *Table { return nil } + +// GVR returns a resource descriptor. +func (s *Sanitizer) GVR() client.GVR { return s.gvr } + +// App returns the current app handle. +func (s *Sanitizer) App() *App { + return s.app +} + +// UpdateTitle updates the view title. +func (s *Sanitizer) UpdateTitle() { + t := s.styleTitle() + s.app.QueueUpdateDraw(func() { + s.SetTitle(t) + }) +} + +func (s *Sanitizer) styleTitle() string { + base := strings.Title(s.gvr.R()) + ns := s.model.GetNamespace() + if client.IsAllNamespaces(ns) { + ns = client.NamespaceAll + } + + var title string + if ns == client.ClusterScope { + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, s.Count), s.app.Styles.Frame()) + } else { + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, s.Count), s.app.Styles.Frame()) + } + + buff := s.CmdBuff().GetText() + if buff == "" { + return title + } + if ui.IsLabelSelector(buff) { + buff = ui.TrimLabelSelector(buff) + } + + return title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), s.app.Styles.Frame()) +} diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index c6fe3cc3..1127b3f4 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -1,10 +1,12 @@ package view import ( + "context" "fmt" "strconv" "strings" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" @@ -73,7 +75,9 @@ func (s *ScaleExtender) makeScaleForm(sel string) *tview.Form { s.App().Flash().Err(err) return } - if err := s.scale(sel, count); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() + if err := s.scale(ctx, sel, count); err != nil { log.Error().Err(err).Msgf("DP %s scaling failed", sel) s.App().Flash().Err(err) } else { @@ -104,7 +108,7 @@ func (s *ScaleExtender) makeStyledForm() *tview.Form { return f } -func (s *ScaleExtender) scale(path string, replicas int) error { +func (s *ScaleExtender) scale(ctx context.Context, path string, replicas int) error { res, err := dao.AccessorFor(s.App().factory, s.GVR()) if err != nil { return nil @@ -114,5 +118,5 @@ func (s *ScaleExtender) scale(path string, replicas int) error { return fmt.Errorf("expecting a scalable resource for %q", s.GVR()) } - return scaler.Scale(path, int32(replicas)) + return scaler.Scale(ctx, path, int32(replicas)) } diff --git a/internal/view/secret.go b/internal/view/secret.go index b008c97a..b76948fd 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -62,7 +62,7 @@ func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(s.App(), "Secret Decoder", path, false).Update(string(raw)) + details := NewDetails(s.App(), "Secret Decoder", path, true).Update(string(raw)) if err := s.App().inject(details); err != nil { s.App().Flash().Err(err) } diff --git a/internal/view/table.go b/internal/view/table.go index e02512ef..15baf047 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -7,6 +7,7 @@ import ( "github.com/atotto/clipboard" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/gdamore/tcell" "github.com/rs/zerolog/log" @@ -46,13 +47,14 @@ func (t *Table) Init(ctx context.Context) (err error) { t.SetInputCapture(t.keyboard) t.bindKeys() t.GetModel().SetRefreshRate(time.Duration(t.app.Config.K9s.GetRefreshRate()) * time.Second) + t.CmdBuff().AddListener(t) return nil } // SendKey sends an keyboard event (testing only!). func (t *Table) SendKey(evt *tcell.EventKey) { - t.keyboard(evt) + t.app.Prompt().SendKey(evt) } func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { @@ -61,14 +63,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { return evt } - if key == tcell.KeyRune { - if t.FilterInput(evt.Rune()) { - return nil - } - key = ui.AsKey(evt) - } - - if a, ok := t.Actions()[key]; ok && !t.app.Content.IsTopDialog() { + if a, ok := t.Actions()[ui.AsKey(evt)]; ok && !t.app.Content.IsTopDialog() { return a.Action(evt) } @@ -91,8 +86,12 @@ func (t *Table) EnvFn() EnvFunc { func (t *Table) defaultEnv() Env { path := t.GetSelectedItem() - env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header, t.GetSelectedRow()) - env["FILTER"] = t.SearchBuff().String() + row, ok := t.GetSelectedRow(path) + if !ok { + log.Error().Msgf("unable to locate selected row for %q", path) + } + env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header, row) + env["FILTER"] = t.CmdBuff().GetText() if env["FILTER"] == "" { env["NAMESPACE"], env["FILTER"] = client.Namespaced(path) } @@ -108,15 +107,13 @@ func (t *Table) App() *App { // Start runs the component. func (t *Table) Start() { t.Stop() - t.SearchBuff().AddListener(t.app.Cmd()) - t.SearchBuff().AddListener(t) + t.CmdBuff().AddListener(t) t.Styles().AddListener(t.Table) } // Stop terminates the component. func (t *Table) Stop() { - t.SearchBuff().RemoveListener(t.app.Cmd()) - t.SearchBuff().RemoveListener(t) + t.CmdBuff().RemoveListener(t) t.Styles().RemoveListener(t.Table) } @@ -129,11 +126,16 @@ func (t *Table) SetEnterFn(f EnterFunc) { func (t *Table) SetExtraActionsFn(BoostActionsFunc) {} // BufferChanged indicates the buffer was changed. -func (t *Table) BufferChanged(s string) {} +func (t *Table) BufferChanged(s string) { + t.Filter(s) +} // BufferActive indicates the buff activity changed. -func (t *Table) BufferActive(state bool, k ui.BufferKind) { +func (t *Table) BufferActive(state bool, k model.BufferKind) { t.app.BufferActive(state, k) + if !state { + t.app.SetFocus(t) + } } func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -148,30 +150,24 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { func (t *Table) bindKeys() { t.Actions().Add(ui.KeyActions{ - ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false), - tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Marks Clear", t.clearMarksCmd, false), - tcell.KeyCtrlS: ui.NewSharedKeyAction("Save", t.saveCmd, false), - ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", t.activateCmd, false), - tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", t.clearCmd, false), - tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), - tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), - tcell.KeyDelete: ui.NewSharedKeyAction("Erase", t.eraseCmd, false), - tcell.KeyCtrlZ: ui.NewKeyAction("Toggle Faults", t.toggleFaultCmd, false), - tcell.KeyCtrlW: ui.NewKeyAction("Show Wide", t.toggleWideCmd, false), - ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(nameCol, true), false), - ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(ageCol, true), false), + ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false), + tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Marks Clear", t.clearMarksCmd, false), + tcell.KeyCtrlS: ui.NewSharedKeyAction("Save", t.saveCmd, false), + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", t.activateCmd, false), + tcell.KeyCtrlZ: ui.NewKeyAction("Toggle Faults", t.toggleFaultCmd, false), + tcell.KeyCtrlW: ui.NewKeyAction("Toggle Wide", t.toggleWideCmd, false), + ui.KeyShiftN: ui.NewKeyAction("Sort Name", t.SortColCmd(nameCol, true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Age", t.SortColCmd(ageCol, true), false), }) } func (t *Table) toggleFaultCmd(evt *tcell.EventKey) *tcell.EventKey { t.ToggleToast() - return nil } func (t *Table) toggleWideCmd(evt *tcell.EventKey) *tcell.EventKey { t.ToggleWide() - return nil } @@ -212,28 +208,11 @@ func (t *Table) clearMarksCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (t *Table) clearCmd(evt *tcell.EventKey) *tcell.EventKey { - if !t.SearchBuff().IsActive() { - return evt - } - t.SearchBuff().Clear() - - return nil -} - -func (t *Table) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { - if t.SearchBuff().IsActive() { - t.SearchBuff().Delete() - } - - return nil -} - func (t *Table) activateCmd(evt *tcell.EventKey) *tcell.EventKey { if t.app.InCmdMode() { return evt } - t.SearchBuff().SetActive(true) + t.App().ResetPrompt(t.CmdBuff()) return nil } diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index 285a1110..737c5b88 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -65,9 +65,10 @@ func TestTableViewFilter(t *testing.T) { v := NewTable(client.NewGVR("test")) v.Init(makeContext()) v.SetModel(&testTableModel{}) - v.SearchBuff().SetActive(true) - v.SearchBuff().Set("blee") v.Refresh() + v.CmdBuff().SetActive(true) + v.CmdBuff().SetText("blee") + assert.Equal(t, 2, v.GetRowCount()) } diff --git a/internal/view/xray.go b/internal/view/xray.go index d2c621a1..91993fa7 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -94,7 +94,7 @@ func (x *Xray) Init(ctx context.Context) error { // ExtraHints returns additional hints. func (x *Xray) ExtraHints() map[string]string { - if !x.app.Styles.Xray().ShowIcons { + if x.app.Config.K9s.NoIcons { return nil } return xray.EmojiInfo() @@ -105,13 +105,9 @@ func (x *Xray) SetInstance(string) {} func (x *Xray) bindKeys() { x.Actions().Add(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Goto", x.gotoCmd, true), - ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", x.activateCmd, false), - tcell.KeyBackspace2: ui.NewSharedKeyAction("Erase", x.eraseCmd, false), - tcell.KeyBackspace: ui.NewSharedKeyAction("Erase", x.eraseCmd, false), - tcell.KeyDelete: ui.NewSharedKeyAction("Erase", x.eraseCmd, false), - tcell.KeyCtrlU: ui.NewSharedKeyAction("Clear Filter", x.clearCmd, false), - tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", x.resetCmd, false), + ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", x.activateCmd, false), + tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", x.resetCmd, false), + tcell.KeyEnter: ui.NewKeyAction("Goto", x.gotoCmd, true), }) } @@ -213,7 +209,7 @@ func (x *Xray) k9sEnv() Env { return env } - env["FILTER"] = x.CmdBuff().String() + env["FILTER"] = x.CmdBuff().GetText() if env["FILTER"] == "" { ns, n := client.Namespaced(spec.Path()) env["NAMESPACE"], env["FILTER"] = ns, n @@ -417,27 +413,7 @@ func (x *Xray) activateCmd(evt *tcell.EventKey) *tcell.EventKey { if x.app.InCmdMode() { return evt } - x.CmdBuff().SetActive(true) - - return nil -} - -func (x *Xray) clearCmd(evt *tcell.EventKey) *tcell.EventKey { - if !x.CmdBuff().IsActive() { - return evt - } - x.CmdBuff().Clear() - x.model.ClearFilter() - x.Start() - - return nil -} - -func (x *Xray) eraseCmd(evt *tcell.EventKey) *tcell.EventKey { - if x.CmdBuff().IsActive() { - x.CmdBuff().Delete() - } - x.UpdateTitle() + x.app.ResetPrompt(x.CmdBuff()) return nil } @@ -456,7 +432,7 @@ func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if x.CmdBuff().IsActive() { - if ui.IsLabelSelector(x.CmdBuff().String()) { + if ui.IsLabelSelector(x.CmdBuff().GetText()) { x.Start() } x.CmdBuff().SetActive(false) @@ -469,11 +445,10 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if spec == nil { return nil } - log.Debug().Msgf("SELECTED REF %#v", spec) if len(strings.Split(spec.Path(), "/")) == 1 { return nil } - if err := x.app.viewResource(client.NewGVR(spec.GVR()).R(), spec.Path(), false); err != nil { + if err := x.app.gotoResource(client.NewGVR(spec.GVR()).R(), spec.Path(), false); err != nil { x.app.Flash().Err(err) } @@ -481,7 +456,7 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { } func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode { - q := x.CmdBuff().String() + q := x.CmdBuff().GetText() if x.CmdBuff().Empty() || ui.IsLabelSelector(q) { return root } @@ -510,7 +485,7 @@ func (x *Xray) TreeLoadFailed(err error) { } func (x *Xray) update(node *xray.TreeNode) { - root := makeTreeNode(node, x.ExpandNodes(), x.app.Styles) + root := makeTreeNode(node, x.ExpandNodes(), x.app.Config.K9s.NoIcons, x.app.Styles) if node == nil { x.app.QueueUpdateDraw(func() { x.SetRoot(root) @@ -541,7 +516,6 @@ func (x *Xray) update(node *xray.TreeNode) { } if spec.AsPath() == x.GetSelectedItem() { - log.Debug().Msgf("SEL %q--%q", spec.Path(), x.GetSelectedItem()) node.SetExpanded(true).SetSelectable(true) x.SetCurrentNode(node) } @@ -558,7 +532,7 @@ func (x *Xray) TreeChanged(node *xray.TreeNode) { } func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) { - node := makeTreeNode(n, x.ExpandNodes(), x.app.Styles) + node := makeTreeNode(n, x.ExpandNodes(), x.app.Config.K9s.NoIcons, x.app.Styles) for _, c := range n.Children { x.hydrate(node, c) } @@ -569,14 +543,15 @@ func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) { func (x *Xray) SetEnvFn(EnvFunc) {} // Refresh updates the view -func (x *Xray) Refresh() { -} +func (x *Xray) Refresh() {} // BufferChanged indicates the buffer was changed. -func (x *Xray) BufferChanged(s string) {} +func (x *Xray) BufferChanged(s string) { + x.update(x.filter(x.model.Peek())) +} // BufferActive indicates the buff activity changed. -func (x *Xray) BufferActive(state bool, k ui.BufferKind) { +func (x *Xray) BufferActive(state bool, k model.BufferKind) { x.app.BufferActive(state, k) } @@ -586,7 +561,7 @@ func (x *Xray) defaultContext() context.Context { if x.CmdBuff().Empty() { ctx = context.WithValue(ctx, internal.KeyLabels, "") } else { - ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(x.CmdBuff().String())) + ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(x.CmdBuff().GetText())) } return ctx @@ -595,8 +570,6 @@ func (x *Xray) defaultContext() context.Context { // Start initializes resource watch loop. func (x *Xray) Start() { x.Stop() - - x.CmdBuff().AddListener(x.app.Cmd()) x.CmdBuff().AddListener(x) ctx := x.defaultContext() @@ -612,8 +585,6 @@ func (x *Xray) Stop() { } x.cancelFn() x.cancelFn = nil - - x.CmdBuff().RemoveListener(x.app.Cmd()) x.CmdBuff().RemoveListener(x) } @@ -652,17 +623,17 @@ func (x *Xray) styleTitle() string { ns = client.NamespaceAll } - buff := x.CmdBuff().String() var title string if ns == client.ClusterScope { title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, x.Count), x.app.Styles.Frame()) } else { title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, x.Count), x.app.Styles.Frame()) } + + buff := x.CmdBuff().GetText() if buff == "" { return title } - if ui.IsLabelSelector(buff) { buff = ui.TrimLabelSelector(buff) } @@ -716,10 +687,10 @@ func rxFilter(q, path string) bool { return false } -func makeTreeNode(node *xray.TreeNode, expanded bool, styles *config.Styles) *tview.TreeNode { +func makeTreeNode(node *xray.TreeNode, expanded bool, showIcons bool, styles *config.Styles) *tview.TreeNode { n := tview.NewTreeNode("No data...") if node != nil { - n.SetText(node.Title(styles.Xray())) + n.SetText(node.Title(showIcons)) n.SetReference(node.Spec()) } n.SetSelectable(true) diff --git a/internal/view/yaml.go b/internal/view/yaml.go index 356568dc..90ff4258 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -25,7 +25,6 @@ const ( ) func colorizeYAML(style config.Yaml, raw string) string { - // lines := strings.Split(raw, "\n") lines := strings.Split(tview.Escape(raw), "\n") fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor.String(), 1) diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 32d86ae6..23e33298 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -15,7 +15,7 @@ import ( const ( defaultResync = 10 * time.Minute - defaultWaitTime = 500 * time.Millisecond + defaultWaitTime = 250 * time.Millisecond ) // Factory tracks various resource informers. diff --git a/internal/xray/container.go b/internal/xray/container.go index c4ba5ead..98b1d5f7 100644 --- a/internal/xray/container.go +++ b/internal/xray/container.go @@ -35,9 +35,7 @@ func (c *Container) Render(ctx context.Context, ns string, o interface{}) error } pns, _ := client.Namespaced(parent.ID) c.envRefs(f, root, pns, co.Container) - // if !root.IsLeaf() { parent.Add(root) - // } return nil } diff --git a/internal/xray/section.go b/internal/xray/section.go new file mode 100644 index 00000000..edf2ba2d --- /dev/null +++ b/internal/xray/section.go @@ -0,0 +1,75 @@ +package xray + +import ( + "context" + "fmt" + "strings" + + "github.com/derailed/k9s/internal/render" + "github.com/derailed/popeye/pkg/config" +) + +// Section represents an xray renderer. +type Section struct{} + +// Render renders an xray node. +func (s *Section) Render(ctx context.Context, ns string, o interface{}) error { + section, ok := o.(render.Section) + if !ok { + return fmt.Errorf("Expected Section, but got %T", o) + } + root := NewTreeNode(section.GVR, section.Title) + parent, ok := ctx.Value(KeyParent).(*TreeNode) + if !ok { + return fmt.Errorf("Expecting a TreeNode but got %T", ctx.Value(KeyParent)) + } + s.outcomeRefs(root, section) + parent.Add(root) + + return nil +} + +func (*Section) outcomeRefs(parent *TreeNode, section render.Section) { + for k, issues := range section.Outcome { + p := NewTreeNode(section.GVR, cleanse(k)) + parent.Add(p) + for _, issue := range issues { + msg := colorize(cleanse(issue.Message), issue.Level) + c := NewTreeNode(fmt.Sprintf("issue_%d", issue.Level), msg) + if issue.Group == "__root__" { + p.Add(c) + continue + } + if pa := p.Find(issue.GVR, issue.Group); pa != nil { + pa.Add(c) + continue + } + pa := NewTreeNode(issue.GVR, issue.Group) + pa.Add(c) + p.Add(pa) + } + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func colorize(s string, l config.Level) string { + c := "green" + switch l { + case config.ErrorLevel: + c = "red" + case config.WarnLevel: + c = "orange" + case config.InfoLevel: + c = "blue" + } + return fmt.Sprintf("[%s::]%s", c, s) +} + +func cleanse(s string) string { + s = strings.Replace(s, "[", "(", -1) + s = strings.Replace(s, "]", ")", -1) + s = strings.Replace(s, "/", "::", -1) + return s +} diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index 5d2d8577..21f0bed4 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" "vbom.ml/util/sortorder" @@ -336,8 +335,8 @@ func (t *TreeNode) Find(gvr, id string) *TreeNode { } // Title computes the node title. -func (t *TreeNode) Title(styles config.Xray) string { - return t.computeTitle(styles) +func (t *TreeNode) Title(noIcons bool) string { + return t.computeTitle(noIcons) } // ---------------------------------------------------------------------------- @@ -384,8 +383,8 @@ func category(gvr string) string { return meta.SingularName } -func (t TreeNode) computeTitle(styles config.Xray) string { - if styles.ShowIcons { +func (t TreeNode) computeTitle(noIcons bool) string { + if !noIcons { return t.toEmojiTitle() } @@ -469,36 +468,92 @@ func (t TreeNode) toEmojiTitle() (title string) { } func toEmoji(gvr string) string { + if e := v1Emoji(gvr); e != "" { + return e + } + if e := appsEmoji(gvr); e != "" { + return e + } + if e := issueEmoji(gvr); e != "" { + return e + } switch gvr { + case "autoscaling/v1/horizontalpodautoscalers": + return "♎️" + case "rbac.authorization.k8s.io/v1/clusterrolebindings", "rbac.authorization.k8s.io/v1/clusterroles": + return "👩‍" + case "rbac.authorization.k8s.io/v1/rolebindings", "rbac.authorization.k8s.io/v1/roles": + return "👨🏻‍" + case "networking.k8s.io/v1/networkpolicies": + return "📕" + case "policy/v1beta1/poddisruptionbudgets": + return "🏷 " + case "policy/v1beta1/podsecuritypolicies": + return "👮‍♂️" case "containers": return "🐳" - case "v1/namespaces", "namespaces": - return "🗂" - case "v1/pods", "pods": - return "🚛" - case "v1/services", "services": - return "💁‍♀️" - case "v1/serviceaccounts", "serviceaccounts": - return "💳" - case "v1/persistentvolumes", "persistentvolumes": - return "📚" - case "v1/persistentvolumeclaims", "persistentvolumeclaims": - return "🎟" - case "v1/secrets", "secrets": - return "🔒" - case "v1/configmaps", "configmaps": - return "🗺" - case "apps/v1/deployments", "deployments": - return "🪂" - case "apps/v1/statefulsets", "statefulsets": - return "🎎" - case "apps/v1/daemonsets", "daemonsets": - return "😈" + case "report": + return "🧼" default: return "📎" } } +func issueEmoji(gvr string) string { + switch gvr { + case "issue_0": + return "👍" + case "issue_1": + return "🔊" + case "issue_2": + return "☣️ " + case "issue_3": + return "🧨" + default: + return "" + } +} + +func v1Emoji(gvr string) string { + switch gvr { + case "v1/namespaces": + return "🗂 " + case "v1/nodes": + return "🖥 " + case "v1/pods": + return "🚛" + case "v1/services": + return "💁‍♀️" + case "v1/serviceaccounts": + return "💳" + case "v1/persistentvolumes": + return "📚" + case "v1/persistentvolumeclaims": + return "🎟 " + case "v1/secrets": + return "🔒" + case "v1/configmaps": + return "🗺 " + default: + return "" + } +} + +func appsEmoji(gvr string) string { + switch gvr { + case "apps/v1/deployments": + return "🪂" + case "apps/v1/statefulsets": + return "🎎" + case "apps/v1/daemonsets": + return "😈" + case "apps/v1/replicasets": + return "👯‍♂️" + default: + return "" + } +} + // EmojiInfo returns emoji help. func EmojiInfo() map[string]string { GVRs := []string{ diff --git a/plugins/README.md b/plugins/README.md index 0ecc4cca..af9331bc 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -2,8 +2,9 @@ K9s plugins extend the tool to provide additonal functionality via actions to further help you observe or administer your Kubernetes clusters. -| Plugin-Name | Description | Available on Views | Shortcut | Kubectl plugin | -|-----------------|--------------------------------|--------------------|----------|----------------------------| -| log_stern.yml | View resource logs using stern | pods | Ctrl-l | | -| log_jq.yml | View resource logs using jq | pods | Ctrl-j | kubetcl-plugins/kubectl-jq | -| job_suspend.yml | Suspends a running cronjob | cronjobs | Ctrl-s | | +| Plugin-Name | Description | Available on Views | Shortcut | Kubectl plugin, external dependencies | +|-----------------|--------------------------------|--------------------|----------|-------------------------------------------| +| log_stern.yml | View resource logs using stern | pods | Ctrl-l | | +| log_jq.yml | View resource logs using jq | pods | Ctrl-j | kubetcl-plugins/kubectl-jq | +| job_suspend.yml | Suspends a running cronjob | cronjobs | Ctrl-s | | +| dive.yml | Dive image layers | containers | d | [Dive](https://github.com/wagoodman/dive) | diff --git a/plugins/dive.yml b/plugins/dive.yml new file mode 100644 index 00000000..8824d132 --- /dev/null +++ b/plugins/dive.yml @@ -0,0 +1,10 @@ +plugin: + dive: + shortCut: d + description: "Dive image" + scopes: + - containers + command: dive + background: false + args: + - $COL-IMAGE diff --git a/skins/black_and_wtf.yml b/skins/black_and_wtf.yml index 07009922..514d7f5e 100644 --- a/skins/black_and_wtf.yml +++ b/skins/black_and_wtf.yml @@ -68,6 +68,9 @@ k9s: logs: fgColor: *ghost bgColor: *bg + indicator: + fgColor: *ghost + bgColor: *bg charts: bgColor: default defaultDialColors: diff --git a/skins/dracula.yml b/skins/dracula.yml index 4ba08cd8..a48ae42a 100644 --- a/skins/dracula.yml +++ b/skins/dracula.yml @@ -90,3 +90,6 @@ k9s: logs: fgColor: *foreground bgColor: *background + indicator: + fgColor: *foreground + bgColor: *purple diff --git a/skins/in_the_navy.yml b/skins/in_the_navy.yml index ed101190..64b882f7 100644 --- a/skins/in_the_navy.yml +++ b/skins/in_the_navy.yml @@ -84,3 +84,6 @@ k9s: logs: fgColor: *dark bgColor: *bg + indicator: + fgColor: *dark + bgColor: *bg diff --git a/skins/kiss.yml b/skins/kiss.yml new file mode 100644 index 00000000..da7d4f31 --- /dev/null +++ b/skins/kiss.yml @@ -0,0 +1,51 @@ +# K9s Kiss Skin Contributed by [@beejeebus](justin.p.randell@gmail.com) +k9s: + body: + fgColor: default + bgColor: default + logoColor: default + info: + fgColor: default + sectionColor: default + frame: + border: + fgColor: default + focusColor: default + menu: + fgColor: default + keyColor: default + numKeyColor: default + crumbs: + fgColor: default + bgColor: default + activeColor: default + status: + newColor: default + modifyColor: default + addColor: default + errorColor: default + highlightcolor: default + killColor: default + completedColor: default + title: + fgColor: default + bgColor: default + highlightColor: default + counterColor: default + filterColor: default + views: + table: + fgColor: default + bgColor: default + cursorColor: default + header: + fgColor: default + bgColor: default + sorterColor: default + yaml: + keyColor: default + colonColor: default + valueColor: default + logs: + fgColor: default + bgColor: default diff --git a/skins/snazzy.yml b/skins/snazzy.yml index 11cf5240..abf11ad7 100644 --- a/skins/snazzy.yml +++ b/skins/snazzy.yml @@ -64,3 +64,6 @@ k9s: logs: fgColor: white bgColor: "#282a36" + indicator: + fgColor: white + bgColor: "#282a36" diff --git a/skins/stock.yml b/skins/stock.yml index 33c9538c..1cb4505b 100644 --- a/skins/stock.yml +++ b/skins/stock.yml @@ -63,3 +63,6 @@ k9s: logs: fgColor: white bgColor: black + indicator: + fgColor: dodgerblue + bgColor: black