commit
c3c77b6e50
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
10
Makefile
10
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:
|
||||
|
|
|
|||
179
README.md
179
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 | `<esc>` | |
|
||||
| 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`<ENTER>` | View a Kubernetes resource aliases | `:po<ENTER>` |
|
||||
| `?` | Show keyboard shortcuts and help | |
|
||||
| `Ctrl-a` | Show all available resource alias | select+`<ENTER>` 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` |
|
||||
| `<Esc>` | Bails out of view/command/filter mode | |
|
||||
| `d`,`v`, `e`, `l`,... | Key mapping to describe, view, edit, view logs,... | `d` (describes a resource) |
|
||||
| `:`ctx`<ENTER>` | To view and switch to another Kubernetes context | `:`+`ctx`+`<ENTER>` |
|
||||
| `:`ns`<ENTER>` | To view and switch to another Kubernetes namespace | `:`+`ns`+`<ENTER>` |
|
||||
| `:screendump`, `:sd` | To view all saved resources | |
|
||||
| `Ctrl-d` | To delete a resource (TAB and ENTER to confirm) | |
|
||||
| `Ctrl-k` | To 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-<RESOURCE_COLUMN_NAME>` 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-<RESOURCE_COLUMN_NAME>` 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.
|
||||
|
||||
---
|
||||
|
|
|
|||
BIN
assets/beach.png
BIN
assets/beach.png
Binary file not shown.
|
Before Width: | Height: | Size: 11 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 749 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 653 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 414 KiB |
|
|
@ -0,0 +1,69 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# 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!
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/shirts/k9s_front.png" align="center" width="auto" height="100"/>
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/shirts/k9s_back.png" align="center" width="auto" height="100"/>
|
||||
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# 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!
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_popeye.png" align="center" width="400" height="auto"/>
|
||||
|
||||
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 ;)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/popeye/sanitizers.png" align="center" width="400" height="auto"/>
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/popeye/report.png" align="center" width="400" height="auto"/>
|
||||
|
||||
## 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)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
|
||||
|
||||
# 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)
|
||||
|
||||
---
|
||||
|
||||
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
|
@ -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))
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
64
go.mod
64
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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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().")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
k9s:
|
||||
refreshRate: 2
|
||||
logBufferSize: 200
|
||||
logRequestSize: 200
|
||||
logger:
|
||||
tail: 200
|
||||
buffer: 2000
|
||||
currentContext: minikube
|
||||
currentCluster: minikube
|
||||
clusters:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("<STREAM> 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("<STREAM> 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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 '🐩'
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue