commit
c3c77b6e50
|
|
@ -79,7 +79,7 @@ linters-settings:
|
||||||
# exclude: /path/to/file.txt
|
# exclude: /path/to/file.txt
|
||||||
|
|
||||||
funlen:
|
funlen:
|
||||||
lines: 65
|
lines: 75
|
||||||
statements: 40
|
statements: 40
|
||||||
|
|
||||||
govet:
|
govet:
|
||||||
|
|
@ -114,7 +114,7 @@ linters-settings:
|
||||||
local-prefixes: github.com/org/project
|
local-prefixes: github.com/org/project
|
||||||
gocyclo:
|
gocyclo:
|
||||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||||
min-complexity: 15
|
min-complexity: 20
|
||||||
gocognit:
|
gocognit:
|
||||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||||
min-complexity: 20
|
min-complexity: 20
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ builds:
|
||||||
- darwin
|
- darwin
|
||||||
- windows
|
- windows
|
||||||
goarch:
|
goarch:
|
||||||
- 386
|
# - 386
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- arm
|
- arm
|
||||||
goarm:
|
goarm:
|
||||||
- 7
|
- 7
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
ldflags:
|
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}}
|
- -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:
|
archives:
|
||||||
|
|
@ -30,7 +32,7 @@ archives:
|
||||||
bit: Arm
|
bit: Arm
|
||||||
bitv6: Arm6
|
bitv6: Arm6
|
||||||
bitv7: Arm7
|
bitv7: Arm7
|
||||||
386: i386
|
# 386: i386
|
||||||
amd64: x86_64
|
amd64: x86_64
|
||||||
checksum:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
|
|
@ -48,12 +50,12 @@ brews:
|
||||||
- name: k9s
|
- name: k9s
|
||||||
github:
|
github:
|
||||||
owner: derailed
|
owner: derailed
|
||||||
name: k9s-homebrew-tap
|
name: homebrew-k9s
|
||||||
commit_author:
|
commit_author:
|
||||||
name: derailed
|
name: derailed
|
||||||
email: fernand@imhotep.io
|
email: fernand@imhotep.io
|
||||||
folder: Formula
|
folder: Formula
|
||||||
homepage: https://k8sk9s.dev/
|
homepage: https://k9scli.io/
|
||||||
description: Kubernetes CLI To Manage Your Clusters In Style!
|
description: Kubernetes CLI To Manage Your Clusters In Style!
|
||||||
test: |
|
test: |
|
||||||
system "k9s version"
|
system "k9s version"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Build...
|
# Build...
|
||||||
FROM golang:1.13.6-alpine3.11 AS build
|
FROM golang:1.14.1-alpine3.11 AS build
|
||||||
|
|
||||||
WORKDIR /k9s
|
WORKDIR /k9s
|
||||||
COPY go.mod go.sum main.go Makefile ./
|
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
|
FROM alpine:3.10.0
|
||||||
|
|
||||||
COPY --from=build /k9s/execs/k9s /bin/k9s
|
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 \
|
RUN apk add --update ca-certificates \
|
||||||
&& apk add --update -t deps curl \
|
&& 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 \
|
&& 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)
|
GIT := $(shell git rev-parse --short HEAD)
|
||||||
SOURCE_DATE_EPOCH ?= $(shell date +%s)
|
SOURCE_DATE_EPOCH ?= $(shell date +%s)
|
||||||
DATE := $(shell date -u -d @${SOURCE_DATE_EPOCH} +%FT%T%Z)
|
DATE := $(shell date -u -d @${SOURCE_DATE_EPOCH} +%FT%T%Z)
|
||||||
VERSION ?= v0.17.6
|
VERSION ?= v0.19.1
|
||||||
IMG_NAME := derailed/k9s
|
IMG_NAME := derailed/k9s
|
||||||
IMAGE := ${IMG_NAME}:${VERSION}
|
IMAGE := ${IMG_NAME}:${VERSION}
|
||||||
|
|
||||||
default: help
|
default: help
|
||||||
|
|
||||||
test: ## Run all tests
|
test: ## Run all tests
|
||||||
@go clean --testcache && go test ./...
|
@go clean --testcache && go test ./...
|
||||||
|
|
||||||
|
|
||||||
cover: ## Run test coverage suite
|
cover: ## Run test coverage suite
|
||||||
@go test ./... --coverprofile=cov.out
|
@go test ./... --coverprofile=cov.out
|
||||||
@go tool cover --html=cov.out
|
@go tool cover --html=cov.out
|
||||||
|
|
||||||
build: ## Builds the CLI
|
build: ## Builds the CLI
|
||||||
@go build \
|
@go build \
|
||||||
-ldflags "-w -s -X ${PACKAGE}/cmd.version=${VERSION} -X ${PACKAGE}/cmd.commit=${GIT} -X ${PACKAGE}/cmd.date=${DATE}" \
|
-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
|
-a -tags netgo -o execs/${NAME} main.go
|
||||||
|
|
||||||
img: ## Build Docker Image
|
img: ## Build Docker Image
|
||||||
@docker build --rm -t ${IMAGE} .
|
@docker build --rm -t ${IMAGE} .
|
||||||
|
|
||||||
help:
|
help:
|
||||||
|
|
|
||||||
179
README.md
179
README.md
|
|
@ -90,6 +90,79 @@ K9s is available on Linux, macOS and Windows platforms.
|
||||||
export TERM=xterm-256color
|
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
|
## Screenshots
|
||||||
|
|
@ -129,6 +202,7 @@ K9s is available on Linux, macOS and Windows platforms.
|
||||||
|
|
||||||
## Demo Videos/Recordings
|
## 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 v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be)
|
||||||
* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN)
|
* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN)
|
||||||
* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM)
|
* [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 Configuration
|
||||||
|
|
||||||
K9s keeps its configurations in a .k9s directory in your home directory `$HOME/.k9s/config.yml`.
|
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
|
refreshRate: 2
|
||||||
# Indicates whether modification commands like delete/kill/edit are disabled. Default is false
|
# Indicates whether modification commands like delete/kill/edit are disabled. Default is false
|
||||||
readOnly: false
|
readOnly: false
|
||||||
# Indicates log view maximum buffer size. Default 1k lines.
|
# Logs configuration
|
||||||
logBufferSize: 200
|
logger:
|
||||||
# Indicates how many lines of logs to retrieve from the api-server. Default 200 lines.
|
# Defines the number of lines to return. Default 100
|
||||||
logRequestSize: 200
|
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
|
# Indicates the current kube context. Defaults to current context
|
||||||
currentContext: minikube
|
currentContext: minikube
|
||||||
# Indicates the current kube cluster. Defaults to current context cluster
|
# 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
|
## 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
|
```yaml
|
||||||
# $HOME/.k9s/plugin.yml
|
# $HOME/.k9s/plugin.yml
|
||||||
plugin:
|
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:
|
fred:
|
||||||
shortCut: Ctrl-L
|
shortCut: Ctrl-L
|
||||||
description: Pod logs
|
description: Pod logs
|
||||||
scopes:
|
scopes:
|
||||||
- po
|
- pods
|
||||||
command: kubectl
|
command: kubectl
|
||||||
background: false
|
background: false
|
||||||
args:
|
args:
|
||||||
|
|
@ -298,28 +361,6 @@ plugin:
|
||||||
- $CONTEXT
|
- $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.
|
> 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()))
|
log.Error().Msg(string(debug.Stack()))
|
||||||
printLogo(color.Red)
|
printLogo(color.Red)
|
||||||
fmt.Printf("%s", color.Colorize("Boom!! ", 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) {
|
func printTuple(fmat, section, value string, outputColor color.Paint) {
|
||||||
if outputColor != -1 {
|
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
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf(fmat, section, value)
|
fmt.Printf(fmat, section, value)
|
||||||
|
|
|
||||||
64
go.mod
64
go.mod
|
|
@ -2,65 +2,37 @@ module github.com/derailed/k9s
|
||||||
|
|
||||||
go 1.13
|
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 (
|
require (
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
|
||||||
github.com/atotto/clipboard v0.1.2
|
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/drone/envsubst v1.0.2 // indirect
|
||||||
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
|
github.com/fatih/color v1.9.0
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f // indirect
|
|
||||||
github.com/fatih/color v1.6.0
|
|
||||||
github.com/fsnotify/fsnotify v1.4.7
|
github.com/fsnotify/fsnotify v1.4.7
|
||||||
github.com/gdamore/tcell v1.3.0
|
github.com/gdamore/tcell v1.3.0
|
||||||
github.com/ghodss/yaml v1.0.0
|
github.com/ghodss/yaml v1.0.0
|
||||||
github.com/gregjones/httpcache v0.0.0-20190212212710-3befbb6ad0cc // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.8
|
github.com/mattn/go-runewidth v0.0.9
|
||||||
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
|
||||||
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
|
||||||
github.com/openfaas/faas-provider v0.15.0
|
github.com/openfaas/faas-provider v0.15.0
|
||||||
github.com/petergtz/pegomock v2.6.0+incompatible
|
github.com/petergtz/pegomock v2.7.0+incompatible
|
||||||
github.com/rakyll/hey v0.1.2
|
github.com/rakyll/hey v0.1.3
|
||||||
github.com/rs/zerolog v1.18.0
|
github.com/rs/zerolog v1.18.0
|
||||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||||
github.com/sahilm/fuzzy v0.1.0
|
github.com/sahilm/fuzzy v0.1.0
|
||||||
github.com/spf13/cobra v0.0.5
|
github.com/spf13/cobra v1.0.0
|
||||||
github.com/stretchr/testify v1.4.0
|
github.com/stretchr/testify v1.5.1
|
||||||
golang.org/x/text v0.3.2
|
golang.org/x/text v0.3.2
|
||||||
gopkg.in/yaml.v2 v2.2.4
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
helm.sh/helm/v3 v3.0.2
|
helm.sh/helm/v3 v3.2.0
|
||||||
k8s.io/api v0.0.0
|
k8s.io/api v0.18.2
|
||||||
k8s.io/apimachinery v0.0.0
|
k8s.io/apimachinery v0.18.2
|
||||||
k8s.io/cli-runtime v0.0.0
|
k8s.io/cli-runtime v0.18.2
|
||||||
k8s.io/client-go v0.0.0
|
k8s.io/client-go v0.18.2
|
||||||
k8s.io/klog v1.0.0
|
k8s.io/klog v1.0.0
|
||||||
k8s.io/kubectl v0.0.0
|
k8s.io/kubectl v0.18.2
|
||||||
k8s.io/kubernetes v1.16.3
|
k8s.io/metrics v0.18.2
|
||||||
k8s.io/metrics v0.0.0
|
sigs.k8s.io/yaml v1.2.0
|
||||||
sigs.k8s.io/yaml v1.1.0
|
|
||||||
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787
|
vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -27,22 +28,24 @@ const (
|
||||||
cacheMXKey = "metrics"
|
cacheMXKey = "metrics"
|
||||||
cacheMXAPIKey = "metricsAPI"
|
cacheMXAPIKey = "metricsAPI"
|
||||||
checkConnTimeout = 10 * time.Second
|
checkConnTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
// CallTimeout represents default api call timeout.
|
||||||
|
CallTimeout = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var supportedMetricsAPIVersions = []string{"v1beta1"}
|
var supportedMetricsAPIVersions = []string{"v1beta1"}
|
||||||
|
|
||||||
// APIClient represents a Kubernetes api client.
|
// APIClient represents a Kubernetes api client.
|
||||||
type APIClient struct {
|
type APIClient struct {
|
||||||
checkClientSet *kubernetes.Clientset
|
client kubernetes.Interface
|
||||||
client kubernetes.Interface
|
dClient dynamic.Interface
|
||||||
dClient dynamic.Interface
|
nsClient dynamic.NamespaceableResourceInterface
|
||||||
nsClient dynamic.NamespaceableResourceInterface
|
mxsClient *versioned.Clientset
|
||||||
mxsClient *versioned.Clientset
|
cachedClient *disk.CachedDiscoveryClient
|
||||||
cachedClient *disk.CachedDiscoveryClient
|
config *Config
|
||||||
config *Config
|
mx sync.Mutex
|
||||||
mx sync.Mutex
|
cache *cache.LRUExpireCache
|
||||||
cache *cache.LRUExpireCache
|
metricsAPI bool
|
||||||
metricsAPI bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestClient for testing ONLY!!
|
// NewTestClient for testing ONLY!!
|
||||||
|
|
@ -86,6 +89,33 @@ func makeCacheKey(ns, gvr string, vv []string) string {
|
||||||
return ns + ":" + gvr + "::" + strings.Join(vv, ",")
|
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() {
|
func (a *APIClient) clearCache() {
|
||||||
for _, k := range a.cache.Keys() {
|
for _, k := range a.cache.Keys() {
|
||||||
a.cache.Remove(k)
|
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)
|
dial, sar := a.DialOrDie().AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
|
||||||
|
defer cancel()
|
||||||
for _, v := range verbs {
|
for _, v := range verbs {
|
||||||
sar.Spec.ResourceAttributes.Verb = v
|
sar.Spec.ResourceAttributes.Verb = v
|
||||||
resp, err := dial.Create(sar)
|
resp, err := dial.Create(ctx, sar, metav1.CreateOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msgf(" Dial Failed!")
|
log.Warn().Err(err).Msgf(" Dial Failed!")
|
||||||
a.cache.Add(key, false, cacheExpiry)
|
a.cache.Add(key, false, cacheExpiry)
|
||||||
|
|
@ -135,7 +168,9 @@ func (a *APIClient) ServerVersion() (*version.Info, error) {
|
||||||
|
|
||||||
// ValidNamespaces returns all available namespaces.
|
// ValidNamespaces returns all available namespaces.
|
||||||
func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// 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) {
|
func (a *APIClient) CheckConnectivity() (status bool) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if !status {
|
|
||||||
a.clearCache()
|
|
||||||
}
|
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
status = false
|
status = false
|
||||||
}
|
}
|
||||||
|
if !status {
|
||||||
|
a.clearCache()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if a.checkClientSet == nil {
|
cfg, err := a.config.flags.ToRESTConfig()
|
||||||
cfg, err := a.config.flags.ToRESTConfig()
|
if err != nil {
|
||||||
if err != nil {
|
return
|
||||||
return
|
}
|
||||||
}
|
cfg.Timeout = checkConnTimeout
|
||||||
cfg.Timeout = checkConnTimeout
|
|
||||||
|
|
||||||
if a.checkClientSet, err = kubernetes.NewForConfig(cfg); err != nil {
|
client, err := kubernetes.NewForConfig(cfg)
|
||||||
log.Error().Err(err).Msgf("Unable to connect to api server")
|
if err != nil {
|
||||||
return
|
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
|
status = true
|
||||||
} else {
|
} else {
|
||||||
log.Error().Err(err).Msgf("K9s can't connect to cluster")
|
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)
|
a.cache.Add(cacheMXKey, flag, cacheExpiry)
|
||||||
return flag
|
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
|
flag = true
|
||||||
|
} else {
|
||||||
|
log.Error().Err(err).Msgf("List metrics failed")
|
||||||
}
|
}
|
||||||
a.cache.Add(cacheMXKey, flag, cacheExpiry)
|
a.cache.Add(cacheMXKey, flag, cacheExpiry)
|
||||||
|
|
||||||
|
|
@ -214,8 +252,9 @@ func (a *APIClient) DialOrDie() kubernetes.Interface {
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if a.client, err = kubernetes.NewForConfig(a.RestConfigOrDie()); err != nil {
|
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
|
return a.client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +262,7 @@ func (a *APIClient) DialOrDie() kubernetes.Interface {
|
||||||
func (a *APIClient) RestConfigOrDie() *restclient.Config {
|
func (a *APIClient) RestConfigOrDie() *restclient.Config {
|
||||||
cfg, err := a.config.RESTConfig()
|
cfg, err := a.config.RESTConfig()
|
||||||
if err != nil {
|
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
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
@ -303,6 +342,7 @@ func (a *APIClient) reset() {
|
||||||
a.mx.Lock()
|
a.mx.Lock()
|
||||||
defer a.mx.Unlock()
|
defer a.mx.Unlock()
|
||||||
|
|
||||||
|
a.config.reset()
|
||||||
a.cache = cache.NewLRUExpireCache(cacheSize)
|
a.cache = cache.NewLRUExpireCache(cacheSize)
|
||||||
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
|
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
|
||||||
a.cachedClient = nil
|
a.cachedClient = nil
|
||||||
|
|
@ -324,6 +364,7 @@ func (a *APIClient) supportsMetricsResources() (supported bool) {
|
||||||
|
|
||||||
apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups()
|
apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Debug().Msgf("Unable to access servergroups %#v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, grp := range apiGroups.Groups {
|
for _, grp := range apiGroups.Groups {
|
||||||
|
|
|
||||||
|
|
@ -319,8 +319,6 @@ func (c *Config) ensureConfig() {
|
||||||
if c.clientConfig != nil {
|
if c.clientConfig != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msg("Loading raw config from flags...")
|
|
||||||
c.clientConfig = c.flags.ToRawKubeConfigLoader()
|
c.clientConfig = c.flags.ToRawKubeConfigLoader()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -79,9 +80,9 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
||||||
tmem += mx.AllocatableMEM
|
tmem += mx.AllocatableMEM
|
||||||
teph += mx.AllocatableEphemeral
|
teph += mx.AllocatableEphemeral
|
||||||
}
|
}
|
||||||
mx.PercCPU, mx.PercMEM, mx.PercEphemeral = ToPercentage(ccpu, tcpu),
|
mx.PercCPU = ToPercentage(ccpu, tcpu)
|
||||||
ToPercentage(cmem, tmem),
|
mx.PercMEM = ToPercentage(cmem, tmem)
|
||||||
ToPercentage(ceph, teph)
|
mx.PercEphemeral = ToPercentage(ceph, teph)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +130,7 @@ func (m *MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeM
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchNodesMetrics return all metrics for nodes.
|
// 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"
|
const msg = "user is not authorized to list node metrics"
|
||||||
|
|
||||||
mx := new(mv1beta1.NodeMetricsList)
|
mx := new(mv1beta1.NodeMetricsList)
|
||||||
|
|
@ -150,7 +151,7 @@ func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mx, err
|
return mx, err
|
||||||
}
|
}
|
||||||
mxList, err := client.MetricsV1beta1().NodeMetricses().List(metav1.ListOptions{})
|
mxList, err := client.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mx, err
|
return mx, err
|
||||||
}
|
}
|
||||||
|
|
@ -160,7 +161,7 @@ func (m *MetricsServer) FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchPodsMetrics return all metrics for pods in a given namespace.
|
// 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)
|
mx := new(mv1beta1.PodMetricsList)
|
||||||
const msg = "user is not authorized to list pods metrics"
|
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 {
|
if err != nil {
|
||||||
return mx, err
|
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 {
|
if err != nil {
|
||||||
return mx, err
|
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.
|
// 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
|
var mx *mv1beta1.PodMetrics
|
||||||
const msg = "user is not authorized to list pod metrics"
|
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 {
|
if err != nil {
|
||||||
return mx, err
|
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 {
|
if err != nil {
|
||||||
return mx, err
|
return mx, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,15 @@ type Connection interface {
|
||||||
|
|
||||||
// CheckConnectivity checks if api server connection is happy or not.
|
// CheckConnectivity checks if api server connection is happy or not.
|
||||||
CheckConnectivity() bool
|
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.
|
// CurrentMetrics tracks current cpu/mem.
|
||||||
|
|
|
||||||
|
|
@ -4,20 +4,23 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ColorFmt colorize a string with ansi colors.
|
||||||
|
const ColorFmt = "\x1b[%dm%s\x1b[0m"
|
||||||
|
|
||||||
// Paint describes a terminal color.
|
// Paint describes a terminal color.
|
||||||
type Paint int
|
type Paint int
|
||||||
|
|
||||||
// Defines basic ANSI colors.
|
// Defines basic ANSI colors.
|
||||||
const (
|
const (
|
||||||
Black Paint = iota + 30
|
Black Paint = iota + 30 // 30
|
||||||
Red
|
Red // 31
|
||||||
Green
|
Green // 32
|
||||||
Yellow
|
Yellow // 33
|
||||||
Blue
|
Blue // 34
|
||||||
Magenta
|
Magenta // 35
|
||||||
Cyan
|
Cyan // 36
|
||||||
White
|
LightGray // 37
|
||||||
DarkGray = 90
|
DarkGray = 90
|
||||||
|
|
||||||
Bold = 1
|
Bold = 1
|
||||||
)
|
)
|
||||||
|
|
@ -25,7 +28,7 @@ const (
|
||||||
// Colorize returns an ASCII colored string based on given color.
|
// Colorize returns an ASCII colored string based on given color.
|
||||||
func Colorize(s string, c Paint) string {
|
func Colorize(s string, c Paint) string {
|
||||||
if c == 0 {
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/color"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestColorize(t *testing.T) {
|
func TestColorize(t *testing.T) {
|
||||||
uu := map[string]struct {
|
uu := map[string]struct {
|
||||||
s string
|
s string
|
||||||
c Paint
|
c color.Paint
|
||||||
e string
|
e string
|
||||||
}{
|
}{
|
||||||
"white": {"blee", White, "\x1b[37mblee\x1b[0m"},
|
"white": {"blee", color.LightGray, "\x1b[37mblee\x1b[0m"},
|
||||||
"black": {"blee", Black, "\x1b[30mblee\x1b[0m"},
|
"black": {"blee", color.Black, "\x1b[30mblee\x1b[0m"},
|
||||||
"default": {"blee", 0, "\x1b[37mblee\x1b[0m"},
|
"default": {"blee", 0, "blee"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
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.
|
// ShortNames return all shortnames.
|
||||||
func (a *Aliases) ShortNames() ShortNames {
|
func (a *Aliases) ShortNames() ShortNames {
|
||||||
a.mx.RLock()
|
a.mx.RLock()
|
||||||
|
|
@ -107,6 +119,13 @@ func (a *Aliases) LoadFileAliases(path string) error {
|
||||||
return nil
|
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() {
|
func (a *Aliases) loadDefaultAliases() {
|
||||||
a.mx.Lock()
|
a.mx.Lock()
|
||||||
defer a.mx.Unlock()
|
defer a.mx.Unlock()
|
||||||
|
|
@ -120,49 +139,19 @@ func (a *Aliases) loadDefaultAliases() {
|
||||||
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
|
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
|
||||||
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
|
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
|
||||||
|
|
||||||
const contexts = "contexts"
|
a.declare("help", "h", "?")
|
||||||
{
|
a.declare("quit", "q", "Q")
|
||||||
a.Alias["ctx"] = contexts
|
a.declare("aliases", "alias", "a")
|
||||||
a.Alias[contexts] = contexts
|
a.declare("popeye", "pop")
|
||||||
a.Alias["context"] = contexts
|
a.declare("helm", "charts", "chart", "hm")
|
||||||
}
|
a.declare("contexts", "context", "ctx")
|
||||||
const users = "users"
|
a.declare("users", "user", "usr")
|
||||||
{
|
a.declare("groups", "group", "grp")
|
||||||
a.Alias["usr"] = users
|
a.declare("portforwards", "portforward", "pf")
|
||||||
a.Alias[users] = users
|
a.declare("benchmarks", "benchmark", "be")
|
||||||
a.Alias["user"] = users
|
a.declare("screendumps", "screendump", "sd")
|
||||||
}
|
a.declare("pulses", "pulse", "pu", "hz")
|
||||||
const groups = "groups"
|
a.declare("xrays", "xray", "x")
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save alias to disk.
|
// Save alias to disk.
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,18 @@ import "github.com/derailed/k9s/internal/client"
|
||||||
|
|
||||||
// Cluster tracks K9s cluster configuration.
|
// Cluster tracks K9s cluster configuration.
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
Namespace *Namespace `yaml:"namespace"`
|
Namespace *Namespace `yaml:"namespace"`
|
||||||
View *View `yaml:"view"`
|
View *View `yaml:"view"`
|
||||||
|
FeatureGates *FeatureGates `yaml:"featureGates"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCluster creates a new cluster configuration.
|
// NewCluster creates a new cluster configuration.
|
||||||
func NewCluster() *Cluster {
|
func NewCluster() *Cluster {
|
||||||
return &Cluster{Namespace: NewNamespace(), View: NewView()}
|
return &Cluster{
|
||||||
|
Namespace: NewNamespace(),
|
||||||
|
View: NewView(),
|
||||||
|
FeatureGates: NewFeatureGates(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate a cluster config.
|
// Validate a cluster config.
|
||||||
|
|
@ -20,6 +25,10 @@ func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) {
|
||||||
}
|
}
|
||||||
c.Namespace.Validate(conn, ks)
|
c.Namespace.Validate(conn, ks)
|
||||||
|
|
||||||
|
if c.FeatureGates == nil {
|
||||||
|
c.FeatureGates = NewFeatureGates()
|
||||||
|
}
|
||||||
|
|
||||||
if c.View == nil {
|
if c.View == nil {
|
||||||
c.View = NewView()
|
c.View = NewView()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,9 @@ func (c *Config) Load(path string) error {
|
||||||
if cfg.K9s != nil {
|
if cfg.K9s != nil {
|
||||||
c.K9s = cfg.K9s
|
c.K9s = cfg.K9s
|
||||||
}
|
}
|
||||||
|
if c.K9s.Logger == nil {
|
||||||
|
c.K9s.Logger = NewLogger()
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,8 @@ func TestConfigLoad(t *testing.T) {
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
|
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
|
||||||
|
|
||||||
assert.Equal(t, 2, cfg.K9s.RefreshRate)
|
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.CurrentContext)
|
||||||
assert.Equal(t, "minikube", cfg.K9s.CurrentCluster)
|
assert.Equal(t, "minikube", cfg.K9s.CurrentCluster)
|
||||||
assert.NotNil(t, cfg.K9s.Clusters)
|
assert.NotNil(t, cfg.K9s.Clusters)
|
||||||
|
|
@ -206,8 +207,8 @@ func TestConfigSaveFile(t *testing.T) {
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
|
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
|
||||||
cfg.K9s.RefreshRate = 100
|
cfg.K9s.RefreshRate = 100
|
||||||
cfg.K9s.ReadOnly = true
|
cfg.K9s.ReadOnly = true
|
||||||
cfg.K9s.LogBufferSize = 500
|
cfg.K9s.Logger.TailCount = 500
|
||||||
cfg.K9s.LogRequestSize = 100
|
cfg.K9s.Logger.BufferSize = 800
|
||||||
cfg.K9s.CurrentContext = "blee"
|
cfg.K9s.CurrentContext = "blee"
|
||||||
cfg.K9s.CurrentCluster = "blee"
|
cfg.K9s.CurrentCluster = "blee"
|
||||||
cfg.Validate()
|
cfg.Validate()
|
||||||
|
|
@ -262,11 +263,16 @@ var expectedConfig = `k9s:
|
||||||
refreshRate: 100
|
refreshRate: 100
|
||||||
headless: false
|
headless: false
|
||||||
readOnly: true
|
readOnly: true
|
||||||
logBufferSize: 500
|
noIcons: false
|
||||||
logRequestSize: 100
|
logger:
|
||||||
|
tail: 500
|
||||||
|
buffer: 800
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreenLogs: false
|
||||||
|
textWrap: false
|
||||||
|
showTime: false
|
||||||
currentContext: blee
|
currentContext: blee
|
||||||
currentCluster: blee
|
currentCluster: blee
|
||||||
fullScreenLogs: false
|
|
||||||
clusters:
|
clusters:
|
||||||
blee:
|
blee:
|
||||||
namespace:
|
namespace:
|
||||||
|
|
@ -275,28 +281,30 @@ var expectedConfig = `k9s:
|
||||||
- default
|
- default
|
||||||
view:
|
view:
|
||||||
active: po
|
active: po
|
||||||
|
featureGates:
|
||||||
|
nodeShell: false
|
||||||
fred:
|
fred:
|
||||||
namespace:
|
namespace:
|
||||||
active: default
|
active: default
|
||||||
favorites:
|
favorites:
|
||||||
- default
|
- default
|
||||||
- kube-public
|
|
||||||
- istio-system
|
- istio-system
|
||||||
- all
|
- all
|
||||||
- kube-system
|
|
||||||
view:
|
view:
|
||||||
active: po
|
active: po
|
||||||
|
featureGates:
|
||||||
|
nodeShell: false
|
||||||
minikube:
|
minikube:
|
||||||
namespace:
|
namespace:
|
||||||
active: kube-system
|
active: kube-system
|
||||||
favorites:
|
favorites:
|
||||||
- default
|
- default
|
||||||
- kube-public
|
|
||||||
- istio-system
|
- istio-system
|
||||||
- all
|
- all
|
||||||
- kube-system
|
|
||||||
view:
|
view:
|
||||||
active: ctx
|
active: ctx
|
||||||
|
featureGates:
|
||||||
|
nodeShell: false
|
||||||
thresholds:
|
thresholds:
|
||||||
cpu:
|
cpu:
|
||||||
critical: 90
|
critical: 90
|
||||||
|
|
@ -310,11 +318,16 @@ var resetConfig = `k9s:
|
||||||
refreshRate: 2
|
refreshRate: 2
|
||||||
headless: false
|
headless: false
|
||||||
readOnly: false
|
readOnly: false
|
||||||
logBufferSize: 200
|
noIcons: false
|
||||||
logRequestSize: 200
|
logger:
|
||||||
|
tail: 200
|
||||||
|
buffer: 2000
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreenLogs: false
|
||||||
|
textWrap: false
|
||||||
|
showTime: false
|
||||||
currentContext: blee
|
currentContext: blee
|
||||||
currentCluster: blee
|
currentCluster: blee
|
||||||
fullScreenLogs: false
|
|
||||||
clusters:
|
clusters:
|
||||||
blee:
|
blee:
|
||||||
namespace:
|
namespace:
|
||||||
|
|
@ -323,6 +336,8 @@ var resetConfig = `k9s:
|
||||||
- default
|
- default
|
||||||
view:
|
view:
|
||||||
active: po
|
active: po
|
||||||
|
featureGates:
|
||||||
|
nodeShell: false
|
||||||
thresholds:
|
thresholds:
|
||||||
cpu:
|
cpu:
|
||||||
critical: 90
|
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"
|
import "github.com/derailed/k9s/internal/client"
|
||||||
|
|
||||||
const (
|
const defaultRefreshRate = 2
|
||||||
defaultRefreshRate = 2
|
|
||||||
defaultLogRequestSize = 200
|
|
||||||
defaultLogBufferSize = 1000
|
|
||||||
defaultReadOnly = false
|
|
||||||
)
|
|
||||||
|
|
||||||
// K9s tracks K9s configuration options.
|
// K9s tracks K9s configuration options.
|
||||||
type K9s struct {
|
type K9s struct {
|
||||||
RefreshRate int `yaml:"refreshRate"`
|
RefreshRate int `yaml:"refreshRate"`
|
||||||
Headless bool `yaml:"headless"`
|
Headless bool `yaml:"headless"`
|
||||||
ReadOnly bool `yaml:"readOnly"`
|
ReadOnly bool `yaml:"readOnly"`
|
||||||
LogBufferSize int `yaml:"logBufferSize"`
|
NoIcons bool `yaml:"noIcons"`
|
||||||
LogRequestSize int `yaml:"logRequestSize"`
|
Logger *Logger `yaml:"logger"`
|
||||||
CurrentContext string `yaml:"currentContext"`
|
CurrentContext string `yaml:"currentContext"`
|
||||||
CurrentCluster string `yaml:"currentCluster"`
|
CurrentCluster string `yaml:"currentCluster"`
|
||||||
FullScreenLogs bool `yaml:"fullScreenLogs"`
|
|
||||||
Clusters map[string]*Cluster `yaml:"clusters,omitempty"`
|
Clusters map[string]*Cluster `yaml:"clusters,omitempty"`
|
||||||
Thresholds Threshold `yaml:"thresholds"`
|
Thresholds Threshold `yaml:"thresholds"`
|
||||||
manualRefreshRate int
|
manualRefreshRate int
|
||||||
|
|
@ -30,12 +24,10 @@ type K9s struct {
|
||||||
// NewK9s create a new K9s configuration.
|
// NewK9s create a new K9s configuration.
|
||||||
func NewK9s() *K9s {
|
func NewK9s() *K9s {
|
||||||
return &K9s{
|
return &K9s{
|
||||||
RefreshRate: defaultRefreshRate,
|
RefreshRate: defaultRefreshRate,
|
||||||
ReadOnly: defaultReadOnly,
|
Logger: NewLogger(),
|
||||||
LogBufferSize: defaultLogBufferSize,
|
Clusters: make(map[string]*Cluster),
|
||||||
LogRequestSize: defaultLogRequestSize,
|
Thresholds: NewThreshold(),
|
||||||
Clusters: make(map[string]*Cluster),
|
|
||||||
Thresholds: NewThreshold(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,22 +98,15 @@ func (k *K9s) validateDefaults() {
|
||||||
if k.RefreshRate <= 0 {
|
if k.RefreshRate <= 0 {
|
||||||
k.RefreshRate = defaultRefreshRate
|
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()
|
cc, err := ks.ClusterNames()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for key := range k.Clusters {
|
for key := range k.Clusters {
|
||||||
|
k.Clusters[key].Validate(c, ks)
|
||||||
if InList(cc, key) {
|
if InList(cc, key) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -138,8 +123,13 @@ func (k *K9s) Validate(c client.Connection, ks KubeSettings) {
|
||||||
if k.Clusters == nil {
|
if k.Clusters == nil {
|
||||||
k.Clusters = map[string]*Cluster{}
|
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 {
|
if k.Thresholds == nil {
|
||||||
k.Thresholds = NewThreshold()
|
k.Thresholds = NewThreshold()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ func TestK9sValidate(t *testing.T) {
|
||||||
c.Validate(mc, mk)
|
c.Validate(mc, mk)
|
||||||
|
|
||||||
assert.Equal(t, 2, c.RefreshRate)
|
assert.Equal(t, 2, c.RefreshRate)
|
||||||
assert.Equal(t, 1000, c.LogBufferSize)
|
assert.Equal(t, int64(100), c.Logger.TailCount)
|
||||||
assert.Equal(t, 200, c.LogRequestSize)
|
assert.Equal(t, 5000, c.Logger.BufferSize)
|
||||||
assert.Equal(t, "ctx1", c.CurrentContext)
|
assert.Equal(t, "ctx1", c.CurrentContext)
|
||||||
assert.Equal(t, "c1", c.CurrentCluster)
|
assert.Equal(t, "c1", c.CurrentCluster)
|
||||||
assert.Equal(t, 1, len(c.Clusters))
|
assert.Equal(t, 1, len(c.Clusters))
|
||||||
|
|
@ -45,8 +45,8 @@ func TestK9sValidateBlank(t *testing.T) {
|
||||||
c.Validate(mc, mk)
|
c.Validate(mc, mk)
|
||||||
|
|
||||||
assert.Equal(t, 2, c.RefreshRate)
|
assert.Equal(t, 2, c.RefreshRate)
|
||||||
assert.Equal(t, 1000, c.LogBufferSize)
|
assert.Equal(t, int64(100), c.Logger.TailCount)
|
||||||
assert.Equal(t, 200, c.LogRequestSize)
|
assert.Equal(t, 5000, c.Logger.BufferSize)
|
||||||
assert.Equal(t, "ctx1", c.CurrentContext)
|
assert.Equal(t, "ctx1", c.CurrentContext)
|
||||||
assert.Equal(t, "c1", c.CurrentCluster)
|
assert.Equal(t, "c1", c.CurrentCluster)
|
||||||
assert.Equal(t, 1, len(c.Clusters))
|
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
|
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 {
|
func (mock *MockConnection) IsNamespaced(_param0 string) bool {
|
||||||
if mock == nil {
|
if mock == nil {
|
||||||
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
panic("mock must not be nil. Use myMock := NewMockConnection().")
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,13 @@ type (
|
||||||
|
|
||||||
// Log tracks Log styles.
|
// Log tracks Log styles.
|
||||||
Log struct {
|
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"`
|
FgColor Color `yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +145,6 @@ type (
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `yaml:"bgColor"`
|
||||||
CursorColor Color `yaml:"cursorColor"`
|
CursorColor Color `yaml:"cursorColor"`
|
||||||
GraphicColor Color `yaml:"graphicColor"`
|
GraphicColor Color `yaml:"graphicColor"`
|
||||||
ShowIcons bool `yaml:"showIcons"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu tracks menu styles.
|
// Menu tracks menu styles.
|
||||||
|
|
@ -259,15 +265,21 @@ func newStatus() Status {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLog returns a new log style.
|
|
||||||
func newLog() Log {
|
func newLog() Log {
|
||||||
return Log{
|
return Log{
|
||||||
FgColor: "lightskyblue",
|
FgColor: "lightskyblue",
|
||||||
|
BgColor: "black",
|
||||||
|
Indicator: newLogIndicator(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogIndicator() LogIndicator {
|
||||||
|
return LogIndicator{
|
||||||
|
FgColor: "dodgerblue",
|
||||||
BgColor: "black",
|
BgColor: "black",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewYaml returns a new yaml style.
|
|
||||||
func newYaml() Yaml {
|
func newYaml() Yaml {
|
||||||
return Yaml{
|
return Yaml{
|
||||||
KeyColor: "steelblue",
|
KeyColor: "steelblue",
|
||||||
|
|
@ -276,7 +288,6 @@ func newYaml() Yaml {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTitle returns a new title style.
|
|
||||||
func newTitle() Title {
|
func newTitle() Title {
|
||||||
return Title{
|
return Title{
|
||||||
FgColor: "aqua",
|
FgColor: "aqua",
|
||||||
|
|
@ -287,7 +298,6 @@ func newTitle() Title {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInfo returns a new info style.
|
|
||||||
func newInfo() Info {
|
func newInfo() Info {
|
||||||
return Info{
|
return Info{
|
||||||
SectionColor: "white",
|
SectionColor: "white",
|
||||||
|
|
@ -295,18 +305,15 @@ func newInfo() Info {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewXray returns a new xray style.
|
|
||||||
func newXray() Xray {
|
func newXray() Xray {
|
||||||
return Xray{
|
return Xray{
|
||||||
FgColor: "aqua",
|
FgColor: "aqua",
|
||||||
BgColor: "black",
|
BgColor: "black",
|
||||||
CursorColor: "whitesmoke",
|
CursorColor: "whitesmoke",
|
||||||
GraphicColor: "floralwhite",
|
GraphicColor: "floralwhite",
|
||||||
ShowIcons: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTable returns a new table style.
|
|
||||||
func newTable() Table {
|
func newTable() Table {
|
||||||
return Table{
|
return Table{
|
||||||
FgColor: "aqua",
|
FgColor: "aqua",
|
||||||
|
|
@ -317,7 +324,6 @@ func newTable() Table {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTableHeader returns a new table header style.
|
|
||||||
func newTableHeader() TableHeader {
|
func newTableHeader() TableHeader {
|
||||||
return TableHeader{
|
return TableHeader{
|
||||||
FgColor: "white",
|
FgColor: "white",
|
||||||
|
|
@ -326,7 +332,6 @@ func newTableHeader() TableHeader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCrumb returns a new crumbs style.
|
|
||||||
func newCrumb() Crumb {
|
func newCrumb() Crumb {
|
||||||
return Crumb{
|
return Crumb{
|
||||||
FgColor: "black",
|
FgColor: "black",
|
||||||
|
|
@ -335,7 +340,6 @@ func newCrumb() Crumb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBorder returns a new border style.
|
|
||||||
func newBorder() Border {
|
func newBorder() Border {
|
||||||
return Border{
|
return Border{
|
||||||
FgColor: "dodgerblue",
|
FgColor: "dodgerblue",
|
||||||
|
|
@ -343,7 +347,6 @@ func newBorder() Border {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMenu returns a new menu style.
|
|
||||||
func newMenu() Menu {
|
func newMenu() Menu {
|
||||||
return Menu{
|
return Menu{
|
||||||
FgColor: "white",
|
FgColor: "white",
|
||||||
|
|
@ -464,6 +467,7 @@ func (s *Styles) Load(path string) error {
|
||||||
func (s *Styles) Update() {
|
func (s *Styles) Update() {
|
||||||
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
tview.Styles.PrimitiveBackgroundColor = s.BgColor()
|
||||||
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
tview.Styles.ContrastBackgroundColor = s.BgColor()
|
||||||
|
tview.Styles.MoreContrastBackgroundColor = s.BgColor()
|
||||||
tview.Styles.PrimaryTextColor = s.FgColor()
|
tview.Styles.PrimaryTextColor = s.FgColor()
|
||||||
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
|
tview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()
|
||||||
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()
|
tview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
k9s:
|
k9s:
|
||||||
refreshRate: 2
|
refreshRate: 2
|
||||||
logBufferSize: 200
|
logger:
|
||||||
logRequestSize: 200
|
tail: 200
|
||||||
|
buffer: 2000
|
||||||
currentContext: minikube
|
currentContext: minikube
|
||||||
currentCluster: minikube
|
currentCluster: minikube
|
||||||
clusters:
|
clusters:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
|
|
@ -13,7 +12,6 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
restclient "k8s.io/client-go/rest"
|
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
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
|
err error
|
||||||
)
|
)
|
||||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
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)
|
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
|
// TailLogs tails a given container logs
|
||||||
func (c *Container) TailLogs(ctx context.Context, logChan chan<- []byte, opts LogOptions) error {
|
func (c *Container) TailLogs(ctx context.Context, logChan LogChan, opts LogOptions) error {
|
||||||
fac, ok := ctx.Value(internal.KeyFactory).(Factory)
|
log.Debug().Msgf("CONTAINER-LOGS")
|
||||||
if !ok {
|
po := Pod{}
|
||||||
return errors.New("Expecting an informer")
|
po.Init(c.Factory, client.NewGVR("v1/pods"))
|
||||||
}
|
|
||||||
o, err := fac.Get("v1/pods", opts.Path, true, labels.Everything())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var po v1.Pod
|
return po.TailLogs(ctx, logChan, opts)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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) ServerVersion() (*version.Info, error) { return nil, nil }
|
||||||
func (c *conn) CurrentNamespaceName() (string, error) { return "", 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) 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{}
|
type podFactory struct{}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
|
@ -33,7 +34,9 @@ func (c *CronJob) Run(path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BOZO!! Factory resource??
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +54,7 @@ func (c *CronJob) Run(path string) error {
|
||||||
},
|
},
|
||||||
Spec: cj.Spec.JobTemplate.Spec,
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/kubectl/pkg/describe"
|
"k8s.io/kubectl/pkg/describe"
|
||||||
"k8s.io/kubectl/pkg/describe/versioned"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Describe describes a resource.
|
// 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)
|
log.Error().Err(err).Msgf("Unable to find mapper for %s %s", gvr, n)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
d, err := versioned.Describer(c.Config().Flags(), mapping)
|
d, err := describe.Describer(c.Config().Flags(), mapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)
|
log.Error().Err(err).Msgf("Unable to find describer for %#v", mapping)
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale a Deployment.
|
// 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)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", []string{client.GetVerb, client.UpdateVerb})
|
auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", []string{client.GetVerb, client.UpdateVerb})
|
||||||
if err != nil {
|
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")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
scale.Spec.Replicas = replicas
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart a Deployment rollout.
|
// 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)
|
dp, err := d.Load(d.Factory, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -75,12 +75,18 @@ func (d *Deployment) Restart(path string) error {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TailLogs tail logs for all pods represented by this Deployment.
|
// 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)
|
dp, err := d.Load(d.Factory, opts.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart a DaemonSet rollout.
|
// 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)
|
ds, err := d.GetInstance(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -56,12 +56,18 @@ func (d *DaemonSet) Restart(path string) error {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TailLogs tail logs for all pods represented by this DaemonSet.
|
// 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)
|
ds, err := d.GetInstance(opts.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
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)
|
f, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("expecting a context factory")
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
opts.MultiPods = true
|
||||||
if len(oo) > 1 {
|
|
||||||
opts.MultiPods = true
|
|
||||||
}
|
|
||||||
|
|
||||||
po := Pod{}
|
po := Pod{}
|
||||||
po.Init(f, client.NewGVR("v1/pods"))
|
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
|
err error
|
||||||
)
|
)
|
||||||
if client.IsClusterScoped(ns) {
|
if client.IsClusterScoped(ns) {
|
||||||
ll, err = g.dynClient().List(metav1.ListOptions{LabelSelector: labelSel})
|
ll, err = g.dynClient().List(ctx, metav1.ListOptions{LabelSelector: labelSel})
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -57,15 +57,15 @@ func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error)
|
||||||
|
|
||||||
// Get returns a given resource.
|
// Get returns a given resource.
|
||||||
func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) {
|
func (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||||
|
log.Debug().Msgf("GENERIC-GET %q", path)
|
||||||
var opts metav1.GetOptions
|
var opts metav1.GetOptions
|
||||||
|
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
dial := g.dynClient()
|
dial := g.dynClient()
|
||||||
if client.IsClusterScoped(ns) {
|
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.
|
// Describe describes a resource.
|
||||||
|
|
@ -111,11 +111,14 @@ func (g *Generic) Delete(path string, cascade, force bool) error {
|
||||||
PropagationPolicy: &p,
|
PropagationPolicy: &p,
|
||||||
GracePeriodSeconds: grace,
|
GracePeriodSeconds: grace,
|
||||||
}
|
}
|
||||||
|
// BOZO!! Move to caller!
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
|
||||||
|
defer cancel()
|
||||||
if client.IsClusterScoped(ns) {
|
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 {
|
func (g *Generic) dynClient() dynamic.NamespaceableResourceInterface {
|
||||||
|
|
|
||||||
|
|
@ -13,18 +13,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ Accessor = (*Chart)(nil)
|
_ Accessor = (*Helm)(nil)
|
||||||
_ Nuker = (*Chart)(nil)
|
_ Nuker = (*Helm)(nil)
|
||||||
_ Describer = (*Chart)(nil)
|
_ Describer = (*Helm)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chart represents a helm chart.
|
// Helm represents a helm chart.
|
||||||
type Chart struct {
|
type Helm struct {
|
||||||
NonResource
|
NonResource
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns a collection of resources.
|
// 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)
|
cfg, err := c.EnsureHelmConfig(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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))
|
oo := make([]runtime.Object, 0, len(rr))
|
||||||
for _, r := range rr {
|
for _, r := range rr {
|
||||||
oo = append(oo, render.ChartRes{Release: r})
|
oo = append(oo, render.HelmRes{Release: r})
|
||||||
}
|
}
|
||||||
|
|
||||||
return oo, nil
|
return oo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns a resource.
|
// 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)
|
ns, n := client.Namespaced(path)
|
||||||
cfg, err := c.EnsureHelmConfig(ns)
|
cfg, err := c.EnsureHelmConfig(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -55,11 +55,11 @@ func (c *Chart) Get(_ context.Context, path string) (runtime.Object, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.ChartRes{Release: resp}, nil
|
return render.HelmRes{Release: resp}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Describe returns the chart notes.
|
// 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)
|
ns, n := client.Namespaced(path)
|
||||||
cfg, err := c.EnsureHelmConfig(ns)
|
cfg, err := c.EnsureHelmConfig(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -74,7 +74,7 @@ func (c *Chart) Describe(path string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToYAML returns the chart manifest.
|
// 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)
|
ns, n := client.Namespaced(path)
|
||||||
cfg, err := c.EnsureHelmConfig(ns)
|
cfg, err := c.EnsureHelmConfig(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -88,8 +88,8 @@ func (c *Chart) ToYAML(path string) (string, error) {
|
||||||
return resp.Manifest, nil
|
return resp.Manifest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete uninstall a Chart.
|
// Delete uninstall a Helm.
|
||||||
func (c *Chart) Delete(path string, cascade, force bool) error {
|
func (c *Helm) Delete(path string, cascade, force bool) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
cfg, err := c.EnsureHelmConfig(ns)
|
cfg, err := c.EnsureHelmConfig(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -109,7 +109,7 @@ func (c *Chart) Delete(path string, cascade, force bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureHelmConfig return a new configuration.
|
// 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)
|
cfg := new(action.Configuration)
|
||||||
flags := c.Client().Config().Flags()
|
flags := c.Client().Config().Flags()
|
||||||
if err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil {
|
if err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger); err != nil {
|
||||||
|
|
@ -12,6 +12,14 @@ import (
|
||||||
"k8s.io/cli-runtime/pkg/printers"
|
"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 {
|
func toPerc(v1, v2 float64) float64 {
|
||||||
if v2 == 0 {
|
if v2 == 0 {
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ type Job struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TailLogs tail logs for all pods represented by this Job.
|
// 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())
|
o, err := j.Factory.Get(j.gvr.String(), opts.Path, true, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
package dao
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"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.
|
// LogOptions represent logger options.
|
||||||
|
|
@ -12,11 +15,18 @@ type LogOptions struct {
|
||||||
Path string
|
Path string
|
||||||
Container string
|
Container string
|
||||||
Lines int64
|
Lines int64
|
||||||
Color color.Paint
|
|
||||||
Previous bool
|
Previous bool
|
||||||
SingleContainer bool
|
SingleContainer bool
|
||||||
MultiPods bool
|
MultiPods bool
|
||||||
ShowTimestamp 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.
|
// HasContainer checks if a container is present.
|
||||||
|
|
@ -24,6 +34,33 @@ func (o LogOptions) HasContainer() bool {
|
||||||
return o.Container != ""
|
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.
|
// FixedSizeName returns a normalize fixed size pod name if possible.
|
||||||
func (o LogOptions) FixedSizeName() string {
|
func (o LogOptions) FixedSizeName() string {
|
||||||
_, n := client.Namespaced(o.Path)
|
_, n := client.Namespaced(o.Path)
|
||||||
|
|
@ -39,35 +76,19 @@ func (o LogOptions) FixedSizeName() string {
|
||||||
return Truncate(strings.Join(s, "-"), 15) + "-" + tokens[len(tokens)-1]
|
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.
|
// 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 {
|
if len(bytes) == 0 {
|
||||||
return bytes
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes = bytes[:len(bytes)-1]
|
|
||||||
_, n := client.Namespaced(o.Path)
|
|
||||||
|
|
||||||
var prefix []byte
|
|
||||||
if o.MultiPods {
|
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 {
|
return item
|
||||||
prefix = []byte(colorize(o.Color, o.Container+" "))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(prefix) == 0 {
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(prefix, bytes...)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,15 @@ type Node struct {
|
||||||
|
|
||||||
// ToggleCordon toggles cordon/uncordon a node.
|
// ToggleCordon toggles cordon/uncordon a node.
|
||||||
func (n *Node) ToggleCordon(path string, cordon bool) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK())
|
h, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Debug().Msgf("BOOM %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +52,7 @@ func (n *Node) ToggleCordon(path string, cordon bool) error {
|
||||||
}
|
}
|
||||||
return fmt.Errorf("node is already uncordoned")
|
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 {
|
if patchErr != nil {
|
||||||
return patchErr
|
return patchErr
|
||||||
}
|
}
|
||||||
|
|
@ -97,8 +99,30 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns a node resource.
|
// Get returns a node resource.
|
||||||
func (n *Node) Get(_ context.Context, path string) (runtime.Object, error) {
|
func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||||
return FetchNode(n.Factory, path)
|
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.
|
// 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
|
err error
|
||||||
)
|
)
|
||||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
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")
|
log.Warn().Err(err).Msgf("No node metrics")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nn, err := FetchNodes(n.Factory, labels)
|
nn, err := FetchNodes(ctx, n.Factory, labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +165,7 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
// FetchNode retrieves a node.
|
// 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"})
|
auth, err := f.Client().CanI("", "v1/nodes", []string{"get"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 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.
|
// 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})
|
auth, err := f.Client().CanI("", "v1/nodes", []string{client.ListVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 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,
|
LabelSelector: labelsSel,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/color"
|
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/watch"
|
"github.com/derailed/k9s/internal/watch"
|
||||||
"github.com/rs/zerolog/log"
|
"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
|
var pmx *mv1beta1.PodMetrics
|
||||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
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")
|
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
|
var pmx *mv1beta1.PodMetricsList
|
||||||
if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {
|
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")
|
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
|
// TailLogs tails a given container logs
|
||||||
func (p *Pod) TailLogs(ctx context.Context, c chan<- []byte, opts LogOptions) error {
|
func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
|
||||||
if !opts.HasContainer() {
|
log.Debug().Msgf("TAIL-LOGS for %q:%q", opts.Path, opts.Container)
|
||||||
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 {
|
|
||||||
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
fac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("Expecting an informer")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var po v1.Pod
|
var po v1.Pod
|
||||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
|
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {
|
||||||
return err
|
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 {
|
if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 {
|
||||||
opts.SingleContainer = true
|
opts.SingleContainer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tailed bool
|
||||||
for _, co := range po.Spec.InitContainers {
|
for _, co := range po.Spec.InitContainers {
|
||||||
|
log.Debug().Msgf("Tailing INIT-CO %q", co.Name)
|
||||||
opts.Container = co.Name
|
opts.Container = co.Name
|
||||||
if err := p.TailLogs(ctx, c, opts); err != nil {
|
if err := p.TailLogs(ctx, c, opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
tailed = true
|
||||||
}
|
}
|
||||||
rcos := loggableContainers(po.Status)
|
|
||||||
for _, co := range po.Spec.Containers {
|
for _, co := range po.Spec.Containers {
|
||||||
if in(rcos, co.Name) {
|
log.Debug().Msgf("Tailing CO %q", co.Name)
|
||||||
opts.Container = co.Name
|
opts.Container = co.Name
|
||||||
if err := p.TailLogs(ctx, c, opts); err != nil {
|
if err := tailLogs(ctx, p, c, opts); err != nil {
|
||||||
log.Error().Err(err).Msgf("Getting logs for %s failed", co.Name)
|
return err
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tailLogs(ctx context.Context, logger Logger, c chan<- []byte, opts LogOptions) error {
|
func tailLogs(ctx context.Context, logger Logger, c LogChan, opts LogOptions) error {
|
||||||
log.Debug().Msgf("Tailing logs for %q -- %q", opts.Path, opts.Container)
|
log.Debug().Msgf("Tailing logs for %q:%q", opts.Path, opts.Container)
|
||||||
o := v1.PodLogOptions{
|
req, err := logger.Logs(opts.Path, opts.ToPodLogOptions())
|
||||||
Follow: true,
|
|
||||||
Timestamps: false,
|
|
||||||
Container: opts.Container,
|
|
||||||
Previous: opts.Previous,
|
|
||||||
TailLines: &opts.Lines,
|
|
||||||
}
|
|
||||||
req, err := logger.Logs(opts.Path, &o)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Context(ctx)
|
|
||||||
|
|
||||||
// This call will block if nothing is in the stream!!
|
// This call will block if nothing is in the stream!!
|
||||||
stream, err := req.Stream()
|
stream, err := req.Stream(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c <- opts.DecorateLog([]byte(err.Error() + "\n"))
|
c <- opts.DecorateLog([]byte(err.Error() + "\n"))
|
||||||
log.Error().Err(err).Msgf("Log stream failed for `%s", opts.Path)
|
log.Error().Err(err).Msgf("Unable to obtain log stream failed for `%s", opts.Path)
|
||||||
return fmt.Errorf("Unable to obtain log stream for %s", opts.Path)
|
return err
|
||||||
}
|
}
|
||||||
go readLogs(stream, c, opts)
|
go readLogs(stream, c, opts)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readLogs(stream io.ReadCloser, c chan<- []byte, opts LogOptions) {
|
func readLogs(stream io.ReadCloser, c LogChan, opts LogOptions) {
|
||||||
defer func() {
|
defer func() {
|
||||||
log.Debug().Msgf(">>> Closing stream `%s", opts.Path)
|
log.Debug().Msgf(">>> Closing stream %s", opts.Info())
|
||||||
if err := stream.Close(); err != nil {
|
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 {
|
for {
|
||||||
bytes, err := r.ReadBytes('\n')
|
bytes, err := r.ReadBytes('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msg("Read error")
|
|
||||||
if err == io.EOF {
|
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
|
return
|
||||||
}
|
}
|
||||||
log.Error().Err(err).Msgf("stream reader failed")
|
log.Warn().Err(err).Msgf("Stream READ error %s", opts.Info())
|
||||||
c <- opts.DecorateLog([]byte("<STREAM> failed\n"))
|
c <- opts.DecorateLog([]byte("log stream failed\n"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c <- opts.DecorateLog(bytes)
|
c <- opts.DecorateLog(bytes)
|
||||||
|
|
@ -328,22 +332,6 @@ func extractFQN(o runtime.Object) string {
|
||||||
return FQN(ns, n)
|
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.
|
// Check if string is in a string list.
|
||||||
func in(ll []string, s string) bool {
|
func in(ll []string, s string) bool {
|
||||||
for _, l := range ll {
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"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) {
|
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())
|
o, err := r.Factory.Get(crGVR, path, true, labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,10 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
|
||||||
client.NewGVR("apps/v1/statefulsets"): &StatefulSet{},
|
client.NewGVR("apps/v1/statefulsets"): &StatefulSet{},
|
||||||
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
|
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
|
||||||
client.NewGVR("batch/v1/jobs"): &Job{},
|
client.NewGVR("batch/v1/jobs"): &Job{},
|
||||||
client.NewGVR("charts"): &Chart{},
|
|
||||||
client.NewGVR("openfaas"): &OpenFaas{},
|
client.NewGVR("openfaas"): &OpenFaas{},
|
||||||
|
client.NewGVR("popeye"): &Popeye{},
|
||||||
|
client.NewGVR("sanitizer"): &Popeye{},
|
||||||
|
client.NewGVR("helm"): &Helm{},
|
||||||
}
|
}
|
||||||
|
|
||||||
r, ok := m[gvr]
|
r, ok := m[gvr]
|
||||||
|
|
@ -163,6 +165,20 @@ func loadK9s(m ResourceMetas) {
|
||||||
Verbs: []string{},
|
Verbs: []string{},
|
||||||
Categories: []string{"k9s"},
|
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{
|
m[client.NewGVR("contexts")] = metav1.APIResource{
|
||||||
Name: "contexts",
|
Name: "contexts",
|
||||||
Kind: "Contexts",
|
Kind: "Contexts",
|
||||||
|
|
@ -206,9 +222,9 @@ func loadK9s(m ResourceMetas) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadHelm(m ResourceMetas) {
|
func loadHelm(m ResourceMetas) {
|
||||||
m[client.NewGVR("charts")] = metav1.APIResource{
|
m[client.NewGVR("helm")] = metav1.APIResource{
|
||||||
Name: "charts",
|
Name: "helm",
|
||||||
Kind: "Charts",
|
Kind: "Helm",
|
||||||
Namespaced: true,
|
Namespaced: true,
|
||||||
Verbs: []string{"delete"},
|
Verbs: []string{"delete"},
|
||||||
Categories: []string{"helm"},
|
Categories: []string{"helm"},
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,12 @@ type Resource struct {
|
||||||
|
|
||||||
// List returns a collection of resources.
|
// List returns a collection of resources.
|
||||||
func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
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()
|
lsel := labels.Everything()
|
||||||
if sel, err := labels.ConvertSelectorToLabelsMap(strLabel); ok && err == nil {
|
if strLabel != "" {
|
||||||
lsel = sel.AsSelector()
|
if sel, err := labels.Parse(strLabel); err == nil {
|
||||||
|
lsel = sel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.Factory.List(r.gvr.String(), ns, false, lsel)
|
return r.Factory.List(r.gvr.String(), ns, false, lsel)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
"k8s.io/kubectl/pkg/polymorphichelpers"
|
"k8s.io/kubectl/pkg/polymorphichelpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -94,7 +95,7 @@ func (r *ReplicaSet) Rollback(fqn string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = rb.Rollback(dp, map[string]string{}, version, false)
|
_, err = rb.Rollback(dp, map[string]string{}, version, cmdutil.DryRunNone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale a StatefulSet.
|
// 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)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", []string{client.GetVerb, client.UpdateVerb})
|
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", []string{client.GetVerb, client.UpdateVerb})
|
||||||
if err != nil {
|
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")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
scale.Spec.Replicas = replicas
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart a StatefulSet rollout.
|
// 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)
|
sts, err := s.getStatefulSet(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -76,12 +76,18 @@ func (s *StatefulSet) Restart(path string) error {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TailLogs tail logs for all pods represented by this StatefulSet.
|
// 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)
|
sts, err := s.getStatefulSet(opts.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("expecting StatefulSet resource")
|
return errors.New("expecting StatefulSet resource")
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ type Service struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TailLogs tail logs for all pods represented by this Service.
|
// 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)
|
svc, err := s.GetInstance(opts.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ type Table struct {
|
||||||
|
|
||||||
// Get returns a given resource.
|
// Get returns a given resource.
|
||||||
func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
|
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)
|
a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)
|
||||||
_, codec := t.codec()
|
_, codec := t.codec()
|
||||||
|
|
||||||
|
|
@ -30,18 +28,17 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
o, err := c.Get().
|
ns, n := client.Namespaced(path)
|
||||||
|
req := c.Get().
|
||||||
SetHeader("Accept", a).
|
SetHeader("Accept", a).
|
||||||
Namespace(ns).
|
|
||||||
Name(n).
|
Name(n).
|
||||||
Resource(t.gvr.R()).
|
Resource(t.gvr.R()).
|
||||||
VersionedParams(&metav1beta1.TableOptions{}, codec).
|
VersionedParams(&metav1beta1.TableOptions{}, codec)
|
||||||
Do().Get()
|
if ns != client.ClusterScope {
|
||||||
if err != nil {
|
req = req.Namespace(ns)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return o, nil
|
return req.Do(ctx).Get()
|
||||||
}
|
}
|
||||||
|
|
||||||
// List all Resources in a given namespace.
|
// 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).
|
Namespace(ns).
|
||||||
Resource(t.gvr.R()).
|
Resource(t.gvr.R()).
|
||||||
VersionedParams(&metav1.ListOptions{LabelSelector: labelSel}, codec).
|
VersionedParams(&metav1.ListOptions{LabelSelector: labelSel}, codec).
|
||||||
Do().Get()
|
Do(ctx).Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ type NodeMaintainer interface {
|
||||||
// Loggable represents resources with logs.
|
// Loggable represents resources with logs.
|
||||||
type Loggable interface {
|
type Loggable interface {
|
||||||
// TaiLogs streams resource logs.
|
// 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.
|
// Describer describes a resource.
|
||||||
|
|
@ -108,7 +108,7 @@ type Describer interface {
|
||||||
// Scalable represents resources that can scale.
|
// Scalable represents resources that can scale.
|
||||||
type Scalable interface {
|
type Scalable interface {
|
||||||
// Scale scales a resource up or down.
|
// 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.
|
// Controller represents a pod controller.
|
||||||
|
|
@ -132,7 +132,7 @@ type Switchable interface {
|
||||||
// Restartable represents a restartable resource.
|
// Restartable represents a restartable resource.
|
||||||
type Restartable interface {
|
type Restartable interface {
|
||||||
// Restart performs a rollout restart.
|
// Restart performs a rollout restart.
|
||||||
Restart(path string) error
|
Restart(ctx context.Context, path string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runnable represents a runnable resource.
|
// Runnable represents a runnable resource.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -20,8 +22,8 @@ type (
|
||||||
// MetricsService calls the metrics server for metrics info.
|
// MetricsService calls the metrics server for metrics info.
|
||||||
MetricsService interface {
|
MetricsService interface {
|
||||||
HasMetrics() bool
|
HasMetrics() bool
|
||||||
FetchNodesMetrics() (*mv1beta1.NodeMetricsList, error)
|
FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error)
|
||||||
FetchPodsMetrics(ns string) (*mv1beta1.PodMetricsList, error)
|
FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cluster represents a kubernetes resource.
|
// Cluster represents a kubernetes resource.
|
||||||
|
|
@ -77,13 +79,13 @@ func (c *Cluster) UserName() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics gathers node level metrics and compute utilization percentages.
|
// Metrics gathers node level metrics and compute utilization percentages.
|
||||||
func (c *Cluster) Metrics(mx *client.ClusterMetrics) error {
|
func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error {
|
||||||
nn, err := dao.FetchNodes(c.factory, "")
|
nn, err := dao.FetchNodes(ctx, c.factory, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nmx, err := c.mx.FetchNodesMetrics()
|
nmx, err := c.mx.FetchNodesMetrics(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
)
|
)
|
||||||
|
|
@ -90,8 +92,10 @@ func (c *ClusterInfo) Refresh() {
|
||||||
data.K9sVer = c.version
|
data.K9sVer = c.version
|
||||||
data.K8sVer = c.cluster.Version()
|
data.K8sVer = c.cluster.Version()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout)
|
||||||
|
defer cancel()
|
||||||
var mx client.ClusterMetrics
|
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
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/model"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ func (l *testListener) BufferChanged(s string) {
|
||||||
l.text = s
|
l.text = s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *testListener) BufferActive(s bool, _ ui.BufferKind) {
|
func (l *testListener) BufferActive(s bool, _ model.BufferKind) {
|
||||||
if s {
|
if s {
|
||||||
l.act++
|
l.act++
|
||||||
return
|
return
|
||||||
|
|
@ -26,7 +26,7 @@ func (l *testListener) BufferActive(s bool, _ ui.BufferKind) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdBuffActivate(t *testing.T) {
|
func TestCmdBuffActivate(t *testing.T) {
|
||||||
b, l := ui.NewCmdBuff('>', ui.CommandBuff), testListener{}
|
b, l := model.NewCmdBuff('>', model.CommandBuffer), testListener{}
|
||||||
b.AddListener(&l)
|
b.AddListener(&l)
|
||||||
|
|
||||||
b.SetActive(true)
|
b.SetActive(true)
|
||||||
|
|
@ -36,7 +36,7 @@ func TestCmdBuffActivate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdBuffDeactivate(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.AddListener(&l)
|
||||||
|
|
||||||
b.SetActive(false)
|
b.SetActive(false)
|
||||||
|
|
@ -46,39 +46,39 @@ func TestCmdBuffDeactivate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdBuffChanged(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.AddListener(&l)
|
||||||
|
|
||||||
b.Add('b')
|
b.Add('b')
|
||||||
assert.Equal(t, 0, l.act)
|
assert.Equal(t, 0, l.act)
|
||||||
assert.Equal(t, 0, l.inact)
|
assert.Equal(t, 0, l.inact)
|
||||||
assert.Equal(t, "b", l.text)
|
assert.Equal(t, "b", l.text)
|
||||||
assert.Equal(t, "b", b.String())
|
assert.Equal(t, "b", b.GetText())
|
||||||
|
|
||||||
b.Delete()
|
b.Delete()
|
||||||
assert.Equal(t, 0, l.act)
|
assert.Equal(t, 0, l.act)
|
||||||
assert.Equal(t, 0, l.inact)
|
assert.Equal(t, 0, l.inact)
|
||||||
assert.Equal(t, "", l.text)
|
assert.Equal(t, "", l.text)
|
||||||
assert.Equal(t, "", b.String())
|
assert.Equal(t, "", b.GetText())
|
||||||
|
|
||||||
b.Add('c')
|
b.Add('c')
|
||||||
b.Clear()
|
b.ClearText()
|
||||||
assert.Equal(t, 0, l.act)
|
assert.Equal(t, 0, l.act)
|
||||||
assert.Equal(t, 0, l.inact)
|
assert.Equal(t, 0, l.inact)
|
||||||
assert.Equal(t, "", l.text)
|
assert.Equal(t, "", l.text)
|
||||||
assert.Equal(t, "", b.String())
|
assert.Equal(t, "", b.GetText())
|
||||||
|
|
||||||
b.Add('c')
|
b.Add('c')
|
||||||
b.Reset()
|
b.Reset()
|
||||||
assert.Equal(t, 0, l.act)
|
assert.Equal(t, 0, l.act)
|
||||||
assert.Equal(t, 1, l.inact)
|
assert.Equal(t, 1, l.inact)
|
||||||
assert.Equal(t, "", l.text)
|
assert.Equal(t, "", l.text)
|
||||||
assert.Equal(t, "", b.String())
|
assert.Equal(t, "", b.GetText())
|
||||||
assert.True(t, b.Empty())
|
assert.True(t, b.Empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdBuffAdd(t *testing.T) {
|
func TestCmdBuffAdd(t *testing.T) {
|
||||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
b := model.NewCmdBuff('>', model.CommandBuffer)
|
||||||
|
|
||||||
uu := []struct {
|
uu := []struct {
|
||||||
runes []rune
|
runes []rune
|
||||||
|
|
@ -93,13 +93,13 @@ func TestCmdBuffAdd(t *testing.T) {
|
||||||
for _, r := range u.runes {
|
for _, r := range u.runes {
|
||||||
b.Add(r)
|
b.Add(r)
|
||||||
}
|
}
|
||||||
assert.Equal(t, u.cmd, b.String())
|
assert.Equal(t, u.cmd, b.GetText())
|
||||||
b.Reset()
|
b.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdBuffDel(t *testing.T) {
|
func TestCmdBuffDel(t *testing.T) {
|
||||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
b := model.NewCmdBuff('>', model.CommandBuffer)
|
||||||
|
|
||||||
uu := []struct {
|
uu := []struct {
|
||||||
runes []rune
|
runes []rune
|
||||||
|
|
@ -115,13 +115,13 @@ func TestCmdBuffDel(t *testing.T) {
|
||||||
b.Add(r)
|
b.Add(r)
|
||||||
}
|
}
|
||||||
b.Delete()
|
b.Delete()
|
||||||
assert.Equal(t, u.cmd, b.String())
|
assert.Equal(t, u.cmd, b.GetText())
|
||||||
b.Reset()
|
b.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCmdBuffEmpty(t *testing.T) {
|
func TestCmdBuffEmpty(t *testing.T) {
|
||||||
b := ui.NewCmdBuff('>', ui.CommandBuff)
|
b := model.NewCmdBuff('>', model.CommandBuffer)
|
||||||
|
|
||||||
uu := []struct {
|
uu := []struct {
|
||||||
runes []rune
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/sahilm/fuzzy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const logMaxBufferSize = 100
|
|
||||||
|
|
||||||
// LogsListener represents a log model listener.
|
// LogsListener represents a log model listener.
|
||||||
type LogsListener interface {
|
type LogsListener interface {
|
||||||
// LogChanged notifies the model changed.
|
// LogChanged notifies the model changed.
|
||||||
LogChanged([]string)
|
LogChanged(dao.LogItems)
|
||||||
|
|
||||||
// LogCleanred indicates logs are cleared.
|
// LogCleanred indicates logs are cleared.
|
||||||
LogCleared()
|
LogCleared()
|
||||||
|
|
@ -31,29 +27,52 @@ type LogsListener interface {
|
||||||
|
|
||||||
// Log represents a resource logger.
|
// Log represents a resource logger.
|
||||||
type Log struct {
|
type Log struct {
|
||||||
factory dao.Factory
|
factory dao.Factory
|
||||||
lines []string
|
lines dao.LogItems
|
||||||
listeners []LogsListener
|
listeners []LogsListener
|
||||||
gvr client.GVR
|
gvr client.GVR
|
||||||
logOptions dao.LogOptions
|
logOptions dao.LogOptions
|
||||||
cancelFn context.CancelFunc
|
cancelFn context.CancelFunc
|
||||||
mx sync.RWMutex
|
mx sync.RWMutex
|
||||||
filter string
|
filter string
|
||||||
lastSent int
|
lastSent int
|
||||||
showTimestamp bool
|
flushTimeout time.Duration
|
||||||
timeOut time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLog returns a new model.
|
// 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{
|
return &Log{
|
||||||
gvr: gvr,
|
gvr: gvr,
|
||||||
logOptions: opts,
|
logOptions: opts,
|
||||||
lines: nil,
|
lines: nil,
|
||||||
timeOut: timeOut,
|
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.
|
// GetPath returns resource path.
|
||||||
func (l *Log) GetPath() string { return l.logOptions.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() {
|
func (l *Log) Clear() {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
{
|
{
|
||||||
l.lines, l.lastSent = []string{}, 0
|
l.lines, l.lastSent = dao.LogItems{}, 0
|
||||||
}
|
}
|
||||||
l.mx.Unlock()
|
l.mx.Unlock()
|
||||||
l.fireLogCleared()
|
l.fireLogCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShowTimestamp toggles timestamp on logs.
|
// Refresh refreshes the logs.
|
||||||
func (l *Log) ShowTimestamp(b bool) {
|
func (l *Log) Refresh() {
|
||||||
l.mx.RLock()
|
|
||||||
defer l.mx.RUnlock()
|
|
||||||
|
|
||||||
l.showTimestamp = b
|
|
||||||
l.fireLogCleared()
|
l.fireLogCleared()
|
||||||
l.fireLogChanged(l.lines)
|
l.fireLogChanged(l.lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart restarts the logger.
|
||||||
|
func (l *Log) Restart() {
|
||||||
|
l.Clear()
|
||||||
|
l.Stop()
|
||||||
|
l.Start()
|
||||||
|
}
|
||||||
|
|
||||||
// Start initialize log tailer.
|
// Start initialize log tailer.
|
||||||
func (l *Log) Start() {
|
func (l *Log) Start() {
|
||||||
if err := l.load(); err != nil {
|
if err := l.load(); err != nil {
|
||||||
|
|
@ -103,21 +125,21 @@ func (l *Log) Stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets the log lines (for testing only!)
|
// Set sets the log lines (for testing only!)
|
||||||
func (l *Log) Set(lines []string) {
|
func (l *Log) Set(items dao.LogItems) {
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
|
l.lines = items
|
||||||
l.lines = lines
|
l.fireLogCleared()
|
||||||
l.fireLogChanged(lines)
|
l.fireLogChanged(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearFilter resets the log filter if any.
|
// ClearFilter resets the log filter if any.
|
||||||
func (l *Log) ClearFilter() {
|
func (l *Log) ClearFilter() {
|
||||||
log.Debug().Msgf("CLEARED!!")
|
|
||||||
l.mx.RLock()
|
l.mx.RLock()
|
||||||
defer l.mx.RUnlock()
|
defer l.mx.RUnlock()
|
||||||
|
|
||||||
l.filter = ""
|
l.filter = ""
|
||||||
|
l.fireLogCleared()
|
||||||
l.fireLogChanged(l.lines)
|
l.fireLogChanged(l.lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,14 +148,9 @@ func (l *Log) Filter(q string) error {
|
||||||
l.mx.RLock()
|
l.mx.RLock()
|
||||||
defer l.mx.RUnlock()
|
defer l.mx.RUnlock()
|
||||||
|
|
||||||
log.Debug().Msgf("FILTER!")
|
|
||||||
l.filter = q
|
l.filter = q
|
||||||
filtered, err := applyFilter(l.filter, l.lines)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
l.fireLogCleared()
|
l.fireLogCleared()
|
||||||
l.fireLogChanged(filtered)
|
l.fireLogBuffChanged(l.lines)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +160,7 @@ func (l *Log) load() error {
|
||||||
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
|
ctx = context.WithValue(context.Background(), internal.KeyFactory, l.factory)
|
||||||
ctx, l.cancelFn = context.WithCancel(ctx)
|
ctx, l.cancelFn = context.WithCancel(ctx)
|
||||||
|
|
||||||
c := make(chan []byte, 10)
|
c := make(dao.LogChan, 10)
|
||||||
go l.updateLogs(ctx, c)
|
go l.updateLogs(ctx, c)
|
||||||
|
|
||||||
accessor, err := dao.AccessorFor(l.factory, l.gvr)
|
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)
|
return fmt.Errorf("Resource %s is not Loggable", l.gvr)
|
||||||
}
|
}
|
||||||
if err := logger.TailLogs(ctx, c, l.logOptions); err != nil {
|
if err := logger.TailLogs(ctx, c, l.logOptions); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Tail logs failed")
|
||||||
if l.cancelFn != nil {
|
if l.cancelFn != nil {
|
||||||
l.cancelFn()
|
l.cancelFn()
|
||||||
}
|
}
|
||||||
close(c)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,14 +183,15 @@ func (l *Log) load() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append adds a log line.
|
// Append adds a log line.
|
||||||
func (l *Log) Append(line string) {
|
func (l *Log) Append(line *dao.LogItem) {
|
||||||
if line == "" {
|
if line == nil || line.IsEmpty() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.mx.Lock()
|
l.mx.Lock()
|
||||||
defer l.mx.Unlock()
|
defer l.mx.Unlock()
|
||||||
|
|
||||||
|
l.logOptions.SinceTime = line.Timestamp
|
||||||
if l.lines == nil {
|
if l.lines == nil {
|
||||||
l.fireLogCleared()
|
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() {
|
defer func() {
|
||||||
log.Debug().Msgf("updateLogs view bailing out!")
|
log.Debug().Msgf("updateLogs view bailing out!")
|
||||||
}()
|
}()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case bytes, ok := <-c:
|
case item, ok := <-c:
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Msgf("Closed channel detected. Bailing out...")
|
log.Debug().Msgf("Closed channel detected. Bailing out...")
|
||||||
l.Append(string(bytes))
|
l.Append(item)
|
||||||
l.Notify(false)
|
l.Notify(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Append(string(bytes))
|
l.Append(item)
|
||||||
var overflow bool
|
var overflow bool
|
||||||
l.mx.RLock()
|
l.mx.RLock()
|
||||||
{
|
{
|
||||||
overflow = len(l.lines)-l.lastSent > logMaxBufferSize
|
overflow = int64(len(l.lines)-l.lastSent) > l.logOptions.Lines
|
||||||
}
|
}
|
||||||
l.mx.RUnlock()
|
l.mx.RUnlock()
|
||||||
if overflow {
|
if overflow {
|
||||||
l.Notify(true)
|
l.Notify(true)
|
||||||
}
|
}
|
||||||
case <-time.After(l.timeOut):
|
case <-time.After(l.flushTimeout):
|
||||||
l.Notify(true)
|
l.Notify(true)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
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 == "" {
|
if q == "" {
|
||||||
return lines, nil
|
return lines, nil
|
||||||
}
|
}
|
||||||
indexes, err := filter(q, lines)
|
indexes, err := lines.Filter(q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +285,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
|
||||||
if len(indexes) == 0 {
|
if len(indexes) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
filtered := make([]string, 0, len(indexes))
|
filtered := make(dao.LogItems, 0, len(indexes))
|
||||||
for _, idx := range indexes {
|
for _, idx := range indexes {
|
||||||
filtered = append(filtered, lines[idx])
|
filtered = append(filtered, lines[idx])
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +293,7 @@ func applyFilter(q string, lines []string) ([]string, error) {
|
||||||
return filtered, nil
|
return filtered, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Log) fireLogBuffChanged(lines []string) {
|
func (l *Log) fireLogBuffChanged(lines dao.LogItems) {
|
||||||
filtered, err := applyFilter(l.filter, lines)
|
filtered, err := applyFilter(l.filter, lines)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.fireLogError(err)
|
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 {
|
for _, lis := range l.listeners {
|
||||||
lis.LogChanged(lines)
|
lis.LogChanged(lines)
|
||||||
}
|
}
|
||||||
|
|
@ -303,55 +321,3 @@ func (l *Log) fireLogCleared() {
|
||||||
lis.LogCleared()
|
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()
|
v := newTestView()
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
|
|
||||||
data := make([]string, 0, 2*size)
|
data := make(dao.LogItems, 0, 2*size)
|
||||||
for i := 0; i < 2*size; i++ {
|
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.Append(data[i])
|
||||||
}
|
}
|
||||||
m.Notify(true)
|
m.Notify(true)
|
||||||
|
|
@ -47,8 +47,8 @@ func TestLogFilter(t *testing.T) {
|
||||||
e: 2,
|
e: 2,
|
||||||
},
|
},
|
||||||
"regexp": {
|
"regexp": {
|
||||||
q: `\Apod-line-[1-3]{1}\z`,
|
q: `pod-line-[1-3]{1}`,
|
||||||
e: 3,
|
e: 4,
|
||||||
},
|
},
|
||||||
"fuzzy": {
|
"fuzzy": {
|
||||||
q: `-f po-l1`,
|
q: `-f po-l1`,
|
||||||
|
|
@ -67,21 +67,21 @@ func TestLogFilter(t *testing.T) {
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
|
|
||||||
m.Filter(u.q)
|
m.Filter(u.q)
|
||||||
var data []string
|
var data dao.LogItems
|
||||||
for i := 0; i < size; i++ {
|
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.Append(data[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Notify(true)
|
m.Notify(true)
|
||||||
assert.Equal(t, 2, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, 2, v.clearCalled)
|
assert.Equal(t, 2, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
assert.Equal(t, 0, v.errCalled)
|
||||||
assert.Equal(t, u.e, len(v.data))
|
assert.Equal(t, u.e, len(v.data))
|
||||||
|
|
||||||
m.ClearFilter()
|
m.ClearFilter()
|
||||||
assert.Equal(t, 3, v.dataCalled)
|
assert.Equal(t, 2, v.dataCalled)
|
||||||
assert.Equal(t, 2, v.clearCalled)
|
assert.Equal(t, 3, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
assert.Equal(t, 0, v.errCalled)
|
||||||
assert.Equal(t, size, len(v.data))
|
assert.Equal(t, size, len(v.data))
|
||||||
})
|
})
|
||||||
|
|
@ -96,7 +96,7 @@ func TestLogStartStop(t *testing.T) {
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
|
|
||||||
m.Start()
|
m.Start()
|
||||||
data := []string{"line1", "line2"}
|
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
m.Append(d)
|
m.Append(d)
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +118,7 @@ func TestLogClear(t *testing.T) {
|
||||||
v := newTestView()
|
v := newTestView()
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
|
|
||||||
data := []string{"line1", "line2"}
|
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
m.Append(d)
|
m.Append(d)
|
||||||
}
|
}
|
||||||
|
|
@ -138,11 +138,11 @@ func TestLogBasic(t *testing.T) {
|
||||||
v := newTestView()
|
v := newTestView()
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
|
|
||||||
data := []string{"line1", "line2"}
|
data := dao.LogItems{dao.NewLogItemFromString("line1"), dao.NewLogItemFromString("line2")}
|
||||||
m.Set(data)
|
m.Set(data)
|
||||||
|
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
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, 0, v.errCalled)
|
||||||
assert.Equal(t, data, v.data)
|
assert.Equal(t, data, v.data)
|
||||||
}
|
}
|
||||||
|
|
@ -153,21 +153,25 @@ func TestLogAppend(t *testing.T) {
|
||||||
|
|
||||||
v := newTestView()
|
v := newTestView()
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
m.Set([]string{"blah blah"})
|
items := dao.LogItems{dao.NewLogItemFromString("blah blah")}
|
||||||
assert.Equal(t, []string{"blah blah"}, v.data)
|
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 {
|
for _, d := range data {
|
||||||
m.Append(d)
|
m.Append(d)
|
||||||
}
|
}
|
||||||
assert.Equal(t, 1, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, []string{"blah blah"}, v.data)
|
assert.Equal(t, items, v.data)
|
||||||
|
|
||||||
m.Notify(true)
|
m.Notify(true)
|
||||||
assert.Equal(t, 2, v.dataCalled)
|
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, 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) {
|
func TestLogTimedout(t *testing.T) {
|
||||||
|
|
@ -178,15 +182,20 @@ func TestLogTimedout(t *testing.T) {
|
||||||
m.AddListener(v)
|
m.AddListener(v)
|
||||||
|
|
||||||
m.Filter("line1")
|
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 {
|
for _, d := range data {
|
||||||
m.Append(d)
|
m.Append(d)
|
||||||
}
|
}
|
||||||
m.Notify(true)
|
m.Notify(true)
|
||||||
assert.Equal(t, 2, v.dataCalled)
|
assert.Equal(t, 1, v.dataCalled)
|
||||||
assert.Equal(t, 2, v.clearCalled)
|
assert.Equal(t, 2, v.clearCalled)
|
||||||
assert.Equal(t, 0, v.errCalled)
|
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 {
|
type testView struct {
|
||||||
data []string
|
data dao.LogItems
|
||||||
dataCalled int
|
dataCalled int
|
||||||
clearCalled int
|
clearCalled int
|
||||||
errCalled int
|
errCalled int
|
||||||
|
|
@ -213,13 +222,13 @@ func newTestView() *testView {
|
||||||
return &testView{}
|
return &testView{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testView) LogChanged(d []string) {
|
func (t *testView) LogChanged(d dao.LogItems) {
|
||||||
t.data = d
|
t.data = d
|
||||||
t.dataCalled++
|
t.dataCalled++
|
||||||
}
|
}
|
||||||
func (t *testView) LogCleared() {
|
func (t *testView) LogCleared() {
|
||||||
t.clearCalled++
|
t.clearCalled++
|
||||||
t.data = []string{}
|
t.data = dao.LogItems{}
|
||||||
}
|
}
|
||||||
func (t *testView) LogFailed(err error) {
|
func (t *testView) LogFailed(err error) {
|
||||||
fmt.Println("LogErr", err)
|
fmt.Println("LogErr", err)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, er
|
||||||
hh = append(hh, c)
|
hh = append(hh, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
mm, err := h.checkMetrics()
|
mm, err := h.checkMetrics(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return hh, nil
|
return hh, nil
|
||||||
}
|
}
|
||||||
|
|
@ -62,15 +62,15 @@ func (h *PulseHealth) List(ctx context.Context, ns string) ([]runtime.Object, er
|
||||||
return hh, nil
|
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())
|
dial := client.DialMetrics(h.factory.Client())
|
||||||
|
|
||||||
nn, err := dao.FetchNodes(h.factory, "")
|
nn, err := dao.FetchNodes(ctx, h.factory, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nmx, err := dial.FetchNodesMetrics()
|
nmx, err := dial.FetchNodesMetrics(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("Fetching metrics")
|
log.Error().Err(err).Msgf("Fetching metrics")
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ import (
|
||||||
// BOZO!! Break up deps and merge into single registrar
|
// BOZO!! Break up deps and merge into single registrar
|
||||||
var Registry = map[string]ResourceMeta{
|
var Registry = map[string]ResourceMeta{
|
||||||
// Custom...
|
// Custom...
|
||||||
"charts": {
|
"helm": {
|
||||||
DAO: &dao.Chart{},
|
DAO: &dao.Helm{},
|
||||||
Renderer: &render.Chart{},
|
Renderer: &render.Helm{},
|
||||||
},
|
},
|
||||||
"pulses": {
|
"pulses": {
|
||||||
DAO: &dao.Pulse{},
|
DAO: &dao.Pulse{},
|
||||||
|
|
@ -62,6 +62,14 @@ var Registry = map[string]ResourceMeta{
|
||||||
DAO: &dao.Alias{},
|
DAO: &dao.Alias{},
|
||||||
Renderer: &render.Alias{},
|
Renderer: &render.Alias{},
|
||||||
},
|
},
|
||||||
|
"popeye": {
|
||||||
|
DAO: &dao.Popeye{},
|
||||||
|
Renderer: &render.Popeye{},
|
||||||
|
},
|
||||||
|
"sanitizer": {
|
||||||
|
DAO: &dao.Popeye{},
|
||||||
|
TreeRenderer: &xray.Section{},
|
||||||
|
},
|
||||||
|
|
||||||
// Core...
|
// Core...
|
||||||
"v1/endpoints": {
|
"v1/endpoints": {
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ func (t *Table) Peek() render.TableData {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) updater(ctx context.Context) {
|
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
|
rate := initRefreshRate
|
||||||
for {
|
for {
|
||||||
|
|
@ -290,7 +290,6 @@ func (t *Table) getMeta(ctx context.Context) (ResourceMeta, error) {
|
||||||
func (t *Table) resourceMeta() ResourceMeta {
|
func (t *Table) resourceMeta() ResourceMeta {
|
||||||
meta, ok := Registry[t.gvr.String()]
|
meta, ok := Registry[t.gvr.String()]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
|
|
||||||
meta = ResourceMeta{
|
meta = ResourceMeta{
|
||||||
DAO: &dao.Table{},
|
DAO: &dao.Table{},
|
||||||
Renderer: &render.Generic{},
|
Renderer: &render.Generic{},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/sahilm/fuzzy"
|
"github.com/sahilm/fuzzy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -94,7 +95,7 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches {
|
||||||
if q == "" {
|
if q == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if isFuzzySelector(q) {
|
if dao.IsFuzzySelector(q) {
|
||||||
return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
|
return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines)
|
||||||
}
|
}
|
||||||
return t.rxFilter(q, lines)
|
return t.rxFilter(q, lines)
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@ func (t *Tree) reconcile(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := treeHydrate(ctx, ns, oo, meta.TreeRenderer); err != nil {
|
if err := treeHydrate(ctx, ns, oo, meta.TreeRenderer); err != nil {
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +239,6 @@ func (t *Tree) reconcile(ctx context.Context) error {
|
||||||
func (t *Tree) resourceMeta() ResourceMeta {
|
func (t *Tree) resourceMeta() ResourceMeta {
|
||||||
meta, ok := Registry[t.gvr.String()]
|
meta, ok := Registry[t.gvr.String()]
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Msgf("Resource %s not found in registry. Going generic!", t.gvr)
|
|
||||||
meta = ResourceMeta{
|
meta = ResourceMeta{
|
||||||
DAO: &dao.Table{},
|
DAO: &dao.Table{},
|
||||||
Renderer: &render.Generic{},
|
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.
|
// this call will block until the benchmark is complete or timesout.
|
||||||
b.worker.Run()
|
b.worker.Run()
|
||||||
b.worker.Stop()
|
b.worker.Stop()
|
||||||
log.Debug().Msgf("YO!! %t %s", b.canceled, buff)
|
|
||||||
if len(buff.Bytes()) > 0 {
|
if len(buff.Bytes()) > 0 {
|
||||||
if err := b.save(cluster, buff); err != nil {
|
if err := b.save(cluster, buff); err != nil {
|
||||||
log.Error().Err(err).Msg("Saving Benchmark")
|
log.Error().Err(err).Msg("Saving Benchmark")
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,23 @@ func NewDeltaRow(o, n Row, excludeLast bool) DeltaRow {
|
||||||
return deltas
|
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.
|
// Diff returns true if deltas differ or false otherwise.
|
||||||
func (d DeltaRow) Diff(r DeltaRow, ageCol int) bool {
|
func (d DeltaRow) Diff(r DeltaRow, ageCol int) bool {
|
||||||
if len(d) != len(r) {
|
if len(d) != len(r) {
|
||||||
|
|
@ -77,9 +94,7 @@ func (d DeltaRow) IsBlank() bool {
|
||||||
// Clone returns a delta copy.
|
// Clone returns a delta copy.
|
||||||
func (d DeltaRow) Clone() DeltaRow {
|
func (d DeltaRow) Clone() DeltaRow {
|
||||||
res := make(DeltaRow, len(d))
|
res := make(DeltaRow, len(d))
|
||||||
for i, f := range d {
|
copy(res, d)
|
||||||
res[i] = f
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,33 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestDeltaCustomize(t *testing.T) {
|
||||||
uu := map[string]struct {
|
uu := map[string]struct {
|
||||||
r1, r2 render.Row
|
r1, r2 render.Row
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
|
return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ID = client.FQN(nns, n)
|
r.ID = client.FQN(nns, n)
|
||||||
r.Fields = make(Fields, 0, len(g.Header(ns)))
|
r.Fields = make(Fields, 0, len(g.Header(ns)))
|
||||||
r.Fields = append(r.Fields, nns)
|
r.Fields = append(r.Fields, nns)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,20 @@ func (h Header) Clone() Header {
|
||||||
return 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.
|
// MapIndices returns a collection of mapped column indices based of the requested columns.
|
||||||
func (h Header) MapIndices(cols []string, wide bool) []int {
|
func (h Header) MapIndices(cols []string, wide bool) []int {
|
||||||
ii := make([]int, 0, len(cols))
|
ii := make([]int, 0, len(cols))
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chart renders a helm chart to screen.
|
// Helm renders a helm chart to screen.
|
||||||
type Chart struct{}
|
type Helm struct{}
|
||||||
|
|
||||||
// ColorerFunc colors a resource row.
|
// ColorerFunc colors a resource row.
|
||||||
func (Chart) ColorerFunc() ColorerFunc {
|
func (Helm) ColorerFunc() ColorerFunc {
|
||||||
return func(ns string, h Header, re RowEvent) tcell.Color {
|
return func(ns string, h Header, re RowEvent) tcell.Color {
|
||||||
if !Happy(ns, h, re.Row) {
|
if !Happy(ns, h, re.Row) {
|
||||||
return ErrColor
|
return ErrColor
|
||||||
|
|
@ -27,7 +27,7 @@ func (Chart) ColorerFunc() ColorerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header returns a header row.
|
// Header returns a header row.
|
||||||
func (Chart) Header(_ string) Header {
|
func (Helm) Header(_ string) Header {
|
||||||
return Header{
|
return Header{
|
||||||
HeaderColumn{Name: "NAMESPACE"},
|
HeaderColumn{Name: "NAMESPACE"},
|
||||||
HeaderColumn{Name: "NAME"},
|
HeaderColumn{Name: "NAME"},
|
||||||
|
|
@ -41,10 +41,10 @@ func (Chart) Header(_ string) Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders a chart to screen.
|
// Render renders a chart to screen.
|
||||||
func (c Chart) Render(o interface{}, ns string, r *Row) error {
|
func (c Helm) Render(o interface{}, ns string, r *Row) error {
|
||||||
h, ok := o.(ChartRes)
|
h, ok := o.(HelmRes)
|
||||||
if !ok {
|
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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) diagnose(s string) error {
|
func (c Helm) diagnose(s string) error {
|
||||||
if s != "deployed" {
|
if s != "deployed" {
|
||||||
return fmt.Errorf("chart is in an invalid state")
|
return fmt.Errorf("chart is in an invalid state")
|
||||||
}
|
}
|
||||||
|
|
@ -73,17 +73,17 @@ func (c Chart) diagnose(s string) error {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
// ChartRes represents an helm chart resource.
|
// HelmRes represents an helm chart resource.
|
||||||
type ChartRes struct {
|
type HelmRes struct {
|
||||||
Release *release.Release
|
Release *release.Release
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectKind returns a schema object.
|
// GetObjectKind returns a schema object.
|
||||||
func (ChartRes) GetObjectKind() schema.ObjectKind {
|
func (HelmRes) GetObjectKind() schema.ObjectKind {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopyObject returns a container copy.
|
// DeepCopyObject returns a container copy.
|
||||||
func (h ChartRes) DeepCopyObject() runtime.Object {
|
func (h HelmRes) DeepCopyObject() runtime.Object {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -15,6 +16,35 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/util/duration"
|
"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.
|
// AsThousands prints a number with thousand separator.
|
||||||
func AsThousands(n int64) string {
|
func AsThousands(n int64) string {
|
||||||
p := message.NewPrinter(language.English)
|
p := message.NewPrinter(language.English)
|
||||||
|
|
@ -267,3 +297,30 @@ func Pad(s string, width int) string {
|
||||||
|
|
||||||
return s + strings.Repeat(" ", width-len(s))
|
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"
|
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) {
|
func TestToAge(t *testing.T) {
|
||||||
uu := map[string]struct {
|
uu := map[string]struct {
|
||||||
t time.Time
|
t time.Time
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error {
|
||||||
pdb.Name,
|
pdb.Name,
|
||||||
numbToStr(pdb.Spec.MinAvailable),
|
numbToStr(pdb.Spec.MinAvailable),
|
||||||
numbToStr(pdb.Spec.MaxUnavailable),
|
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.CurrentHealthy)),
|
||||||
strconv.Itoa(int(pdb.Status.DesiredHealthy)),
|
strconv.Itoa(int(pdb.Status.DesiredHealthy)),
|
||||||
strconv.Itoa(int(pdb.Status.ExpectedPods)),
|
strconv.Itoa(int(pdb.Status.ExpectedPods)),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/kubernetes/pkg/util/node"
|
|
||||||
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
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
|
var po v1.Pod
|
||||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po)
|
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,7 +260,7 @@ func (*Pod) Statuses(ss []v1.ContainerStatus) (cr, ct, rc int) {
|
||||||
func (p *Pod) Phase(po *v1.Pod) string {
|
func (p *Pod) Phase(po *v1.Pod) string {
|
||||||
status := string(po.Status.Phase)
|
status := string(po.Status.Phase)
|
||||||
if po.Status.Reason != "" {
|
if po.Status.Reason != "" {
|
||||||
if po.DeletionTimestamp != nil && po.Status.Reason == node.NodeUnreachablePodReason {
|
if po.DeletionTimestamp != nil && po.Status.Reason == "NodeLost" {
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
status = po.Status.Reason
|
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 (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"vbom.ml/util/sortorder"
|
"vbom.ml/util/sortorder"
|
||||||
|
|
@ -56,6 +57,20 @@ func NewRow(size int) Row {
|
||||||
return Row{Fields: make([]string, size)}
|
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.
|
// Customize returns a row subset based on given col indices.
|
||||||
func (r Row) Customize(cols []int) Row {
|
func (r Row) Customize(cols []int) Row {
|
||||||
out := NewRow(len(cols))
|
out := NewRow(len(cols))
|
||||||
|
|
@ -160,36 +175,21 @@ func (s RowSorter) Less(i, j int) bool {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
// Less return true if c1 < c2.
|
func toAgeDuration(dur string) string {
|
||||||
func Less(asc bool, c1, c2 string) bool {
|
d, err := time.ParseDuration(dur)
|
||||||
if o, ok := isDurationSort(asc, c1, c2); ok {
|
if err != nil {
|
||||||
return o
|
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)
|
b := sortorder.NaturalLess(c1, c2)
|
||||||
if asc {
|
if asc {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"k8s.io/apimachinery/pkg/util/duration"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -44,8 +42,8 @@ func NewRowEvent(kind ResEvent, row Row) RowEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDeltaRowEvent returns a new row event with deltas.
|
// NewRowEventWithDeltas returns a new row event with deltas.
|
||||||
func NewDeltaRowEvent(row Row, delta DeltaRow) RowEvent {
|
func NewRowEventWithDeltas(row Row, delta DeltaRow) RowEvent {
|
||||||
return RowEvent{
|
return RowEvent{
|
||||||
Kind: EventUpdate,
|
Kind: EventUpdate,
|
||||||
Row: row,
|
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.
|
// Diff returns true if the row changed.
|
||||||
func (r RowEvent) Diff(re RowEvent, ageCol int) bool {
|
func (r RowEvent) Diff(re RowEvent, ageCol int) bool {
|
||||||
if r.Kind != re.Kind {
|
if r.Kind != re.Kind {
|
||||||
|
|
@ -93,6 +105,24 @@ func (r RowEvent) Diff(re RowEvent, ageCol int) bool {
|
||||||
// RowEvents a collection of row events.
|
// RowEvents a collection of row events.
|
||||||
type RowEvents []RowEvent
|
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.
|
// Customize returns custom row events based on columns layout.
|
||||||
func (r RowEvents) Customize(cols []int) RowEvents {
|
func (r RowEvents) Customize(cols []int) RowEvents {
|
||||||
ee := make(RowEvents, 0, len(cols))
|
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.
|
// Sort rows based on column index and order.
|
||||||
func (r RowEvents) Sort(ns string, sortCol int, ageCol bool, asc bool) {
|
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}
|
t := RowEventSorter{NS: ns, Events: r, Index: sortCol, Asc: asc}
|
||||||
sort.Sort(t)
|
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 {
|
for _, re := range r {
|
||||||
g := re.Row.Fields[sortCol]
|
field := re.Row.Fields[sortCol]
|
||||||
if ageCol {
|
if ageCol {
|
||||||
g = toAgeDuration(g)
|
field = toAgeDuration(field)
|
||||||
}
|
|
||||||
kk = kk.Add(g)
|
|
||||||
if ss, ok := gg[g]; ok {
|
|
||||||
gg[g] = append(ss, re.Row.ID)
|
|
||||||
} else {
|
|
||||||
gg[g] = []string{re.Row.ID}
|
|
||||||
}
|
}
|
||||||
|
fields = fields.Add(field)
|
||||||
|
iids[field] = append(iids[field], re.Row.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
ids := make([]string, 0, len(r))
|
ids := make([]string, 0, len(r))
|
||||||
for _, k := range kk {
|
for _, field := range fields {
|
||||||
sort.StringSlice(gg[k]).Sort()
|
sort.StringSlice(iids[field]).Sort()
|
||||||
ids = append(ids, gg[k]...)
|
ids = append(ids, iids[field]...)
|
||||||
}
|
}
|
||||||
s := IdSorter{Ids: ids, Events: r}
|
s := IdSorter{Ids: ids, Events: r}
|
||||||
sort.Sort(s)
|
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.
|
// RowEventSorter sorts row events by a given colon.
|
||||||
|
|
|
||||||
|
|
@ -409,11 +409,41 @@ func TestRowEventsDelete(t *testing.T) {
|
||||||
|
|
||||||
func TestRowEventsSort(t *testing.T) {
|
func TestRowEventsSort(t *testing.T) {
|
||||||
uu := map[string]struct {
|
uu := map[string]struct {
|
||||||
re render.RowEvents
|
re render.RowEvents
|
||||||
col int
|
col int
|
||||||
asc bool
|
age, asc bool
|
||||||
e render.RowEvents
|
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": {
|
"col0": {
|
||||||
re: render.RowEvents{
|
re: render.RowEvents{
|
||||||
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
{Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}},
|
||||||
|
|
@ -453,7 +483,7 @@ func TestRowEventsSort(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
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)
|
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))
|
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) {
|
func TestRowCustomize(t *testing.T) {
|
||||||
uu := map[string]struct {
|
uu := map[string]struct {
|
||||||
row render.Row
|
row render.Row
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func (Service) Header(ns string) Header {
|
||||||
HeaderColumn{Name: "CLUSTER-IP"},
|
HeaderColumn{Name: "CLUSTER-IP"},
|
||||||
HeaderColumn{Name: "EXTERNAL-IP"},
|
HeaderColumn{Name: "EXTERNAL-IP"},
|
||||||
HeaderColumn{Name: "SELECTOR", Wide: true},
|
HeaderColumn{Name: "SELECTOR", Wide: true},
|
||||||
HeaderColumn{Name: "PORTS", Wide: true},
|
HeaderColumn{Name: "PORTS", Wide: false},
|
||||||
HeaderColumn{Name: "LABELS", Wide: true},
|
HeaderColumn{Name: "LABELS", Wide: true},
|
||||||
HeaderColumn{Name: "VALID", Wide: true},
|
HeaderColumn{Name: "VALID", Wide: true},
|
||||||
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
HeaderColumn{Name: "AGE", Time: true, Decorator: AgeDecorator},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package render
|
package render
|
||||||
|
|
||||||
|
import "github.com/derailed/k9s/internal/client"
|
||||||
|
|
||||||
// TableData tracks a K8s resource for tabular display.
|
// TableData tracks a K8s resource for tabular display.
|
||||||
type TableData struct {
|
type TableData struct {
|
||||||
Header Header
|
Header Header
|
||||||
|
|
@ -12,6 +14,22 @@ func NewTableData() *TableData {
|
||||||
return &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.
|
// Customize returns a new model with customized column layout.
|
||||||
func (t *TableData) Customize(cols []string, wide bool) TableData {
|
func (t *TableData) Customize(cols []string, wide bool) TableData {
|
||||||
res := 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].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta
|
||||||
t.RowEvents[index].Row = row
|
t.RowEvents[index].Row = row
|
||||||
} else {
|
} else {
|
||||||
t.RowEvents[index] = NewDeltaRowEvent(row, delta)
|
t.RowEvents[index] = NewRowEventWithDeltas(row, delta)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,28 +14,29 @@ type App struct {
|
||||||
*tview.Application
|
*tview.Application
|
||||||
Configurator
|
Configurator
|
||||||
|
|
||||||
Main *Pages
|
Main *Pages
|
||||||
flash *model.Flash
|
flash *model.Flash
|
||||||
actions KeyActions
|
actions KeyActions
|
||||||
views map[string]tview.Primitive
|
views map[string]tview.Primitive
|
||||||
cmdBuff *CmdBuff
|
cmdModel *model.FishBuff
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp returns a new app.
|
// NewApp returns a new app.
|
||||||
func NewApp(context string) *App {
|
func NewApp(cfg *config.Config, context string) *App {
|
||||||
a := App{
|
a := App{
|
||||||
Application: tview.NewApplication(),
|
Application: tview.NewApplication(),
|
||||||
actions: make(KeyActions),
|
actions: make(KeyActions),
|
||||||
Main: NewPages(),
|
Configurator: Configurator{Config: cfg},
|
||||||
flash: model.NewFlash(model.DefaultFlashDelay),
|
Main: NewPages(),
|
||||||
cmdBuff: NewCmdBuff(':', CommandBuff),
|
flash: model.NewFlash(model.DefaultFlashDelay),
|
||||||
|
cmdModel: model.NewFishBuff(':', model.CommandBuffer),
|
||||||
}
|
}
|
||||||
a.ReloadStyles(context)
|
a.ReloadStyles(context)
|
||||||
|
|
||||||
a.views = map[string]tview.Primitive{
|
a.views = map[string]tview.Primitive{
|
||||||
"menu": NewMenu(a.Styles),
|
"menu": NewMenu(a.Styles),
|
||||||
"logo": NewLogo(a.Styles),
|
"logo": NewLogo(a.Styles),
|
||||||
"cmd": NewCommand(a.Styles),
|
"prompt": NewPrompt(a.Config.K9s.NoIcons, a.Styles),
|
||||||
"crumbs": NewCrumbs(a.Styles),
|
"crumbs": NewCrumbs(a.Styles),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,9 +46,9 @@ func NewApp(context string) *App {
|
||||||
// Init initializes the application.
|
// Init initializes the application.
|
||||||
func (a *App) Init() {
|
func (a *App) Init() {
|
||||||
a.bindKeys()
|
a.bindKeys()
|
||||||
a.cmdBuff.AddListener(a.Cmd())
|
a.Prompt().SetModel(a.cmdModel)
|
||||||
|
a.cmdModel.AddListener(a)
|
||||||
a.Styles.AddListener(a)
|
a.Styles.AddListener(a)
|
||||||
a.CmdBuff().AddListener(a)
|
|
||||||
|
|
||||||
a.SetRoot(a.Main, true)
|
a.SetRoot(a.Main, true)
|
||||||
}
|
}
|
||||||
|
|
@ -56,20 +57,24 @@ func (a *App) Init() {
|
||||||
func (a *App) BufferChanged(s string) {}
|
func (a *App) BufferChanged(s string) {}
|
||||||
|
|
||||||
// BufferActive indicates the buff activity changed.
|
// 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)
|
flex, ok := a.Main.GetPrimitive("main").(*tview.Flex)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if state && flex.ItemAt(1) != a.Cmd() {
|
if state && flex.ItemAt(1) != a.Prompt() {
|
||||||
flex.AddItemAtIndex(1, a.Cmd(), 3, 1, false)
|
flex.AddItemAtIndex(1, a.Prompt(), 3, 1, false)
|
||||||
} else if !state && flex.ItemAt(1) == a.Cmd() {
|
} else if !state && flex.ItemAt(1) == a.Prompt() {
|
||||||
flex.RemoveItemAtIndex(1)
|
flex.RemoveItemAtIndex(1)
|
||||||
|
a.SetFocus(flex)
|
||||||
}
|
}
|
||||||
a.Draw()
|
a.Draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SuggestionChanged notifies of update to command suggestions.
|
||||||
|
func (a *App) SuggestionChanged(ss []string) {}
|
||||||
|
|
||||||
// StylesChanged notifies the skin changed.
|
// StylesChanged notifies the skin changed.
|
||||||
func (a *App) StylesChanged(s *config.Styles) {
|
func (a *App) StylesChanged(s *config.Styles) {
|
||||||
a.Main.SetBackgroundColor(s.BgColor())
|
a.Main.SetBackgroundColor(s.BgColor())
|
||||||
|
|
@ -97,14 +102,11 @@ func (a *App) Conn() client.Connection {
|
||||||
|
|
||||||
func (a *App) bindKeys() {
|
func (a *App) bindKeys() {
|
||||||
a.actions = KeyActions{
|
a.actions = KeyActions{
|
||||||
KeyColon: NewKeyAction("Cmd", a.activateCmd, false),
|
KeyColon: NewKeyAction("Cmd", a.activateCmd, false),
|
||||||
tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false),
|
tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false),
|
||||||
tcell.KeyCtrlC: NewKeyAction("Quit", a.quitCmd, false),
|
tcell.KeyCtrlC: NewKeyAction("Quit", a.quitCmd, false),
|
||||||
tcell.KeyEscape: NewKeyAction("Escape", a.escapeCmd, false),
|
tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
|
||||||
tcell.KeyBackspace2: NewKeyAction("Erase", a.eraseCmd, false),
|
tcell.KeyCtrlQ: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
|
||||||
tcell.KeyBackspace: NewKeyAction("Erase", a.eraseCmd, false),
|
|
||||||
tcell.KeyDelete: NewKeyAction("Erase", a.eraseCmd, false),
|
|
||||||
tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,29 +115,36 @@ func (a *App) BailOut() {
|
||||||
a.Stop()
|
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.
|
// ResetCmd clear out user command.
|
||||||
func (a *App) ResetCmd() {
|
func (a *App) ResetCmd() {
|
||||||
a.cmdBuff.Reset()
|
a.cmdModel.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivateCmd toggle command mode.
|
// ActivateCmd toggle command mode.
|
||||||
func (a *App) ActivateCmd(b bool) {
|
func (a *App) ActivateCmd(b bool) {
|
||||||
a.cmdBuff.SetActive(b)
|
a.cmdModel.SetActive(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCmd retrieves user command.
|
// GetCmd retrieves user command.
|
||||||
func (a *App) GetCmd() string {
|
func (a *App) GetCmd() string {
|
||||||
return a.cmdBuff.String()
|
return a.cmdModel.GetText()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CmdBuff returns a cmd buffer.
|
// CmdBuff returns a cmd buffer.
|
||||||
func (a *App) CmdBuff() *CmdBuff {
|
func (a *App) CmdBuff() *model.FishBuff {
|
||||||
return a.cmdBuff
|
return a.cmdModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasCmd check if cmd buffer is active and has a command.
|
// HasCmd check if cmd buffer is active and has a command.
|
||||||
func (a *App) HasCmd() bool {
|
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 {
|
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.
|
// InCmdMode check if command mode is active.
|
||||||
func (a *App) InCmdMode() bool {
|
func (a *App) InCmdMode() bool {
|
||||||
return a.Cmd().InCmdMode()
|
return a.Prompt().InCmdMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasAction checks if key matches a registered binding.
|
// 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() {
|
if !a.CmdBuff().IsActive() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
a.CmdBuff().Clear()
|
a.CmdBuff().ClearText()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -188,36 +197,19 @@ func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if a.InCmdMode() {
|
if a.InCmdMode() {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
a.cmdBuff.SetActive(true)
|
a.ResetPrompt(a.cmdModel)
|
||||||
a.cmdBuff.Clear()
|
a.cmdModel.ClearText()
|
||||||
|
|
||||||
return nil
|
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.
|
// RedrawCmd forces a redraw.
|
||||||
func (a *App) redrawCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (a *App) redrawCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
a.Draw()
|
a.Draw()
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
// View Accessora...
|
// View Accessors...
|
||||||
|
|
||||||
// Crumbs return app crumba.
|
// Crumbs return app crumba.
|
||||||
func (a *App) Crumbs() *Crumbs {
|
func (a *App) Crumbs() *Crumbs {
|
||||||
|
|
@ -229,9 +221,9 @@ func (a *App) Logo() *Logo {
|
||||||
return a.views["logo"].(*Logo)
|
return a.views["logo"].(*Logo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cmd returns app cmd.
|
// Prompt returns command prompt.
|
||||||
func (a *App) Cmd() *Command {
|
func (a *App) Prompt() *Prompt {
|
||||||
return a.views["cmd"].(*Command)
|
return a.views["prompt"].(*Prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu returns app menu.
|
// Menu returns app menu.
|
||||||
|
|
@ -249,6 +241,9 @@ func (a *App) Flash() *model.Flash {
|
||||||
|
|
||||||
// AsKey converts rune to keyboard key.,
|
// AsKey converts rune to keyboard key.,
|
||||||
func AsKey(evt *tcell.EventKey) tcell.Key {
|
func AsKey(evt *tcell.EventKey) tcell.Key {
|
||||||
|
if evt.Key() != tcell.KeyRune {
|
||||||
|
return evt.Key()
|
||||||
|
}
|
||||||
key := tcell.Key(evt.Rune())
|
key := tcell.Key(evt.Rune())
|
||||||
if evt.Modifiers() == tcell.ModAlt {
|
if evt.Modifiers() == tcell.ModAlt {
|
||||||
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
|
key = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))
|
||||||
|
|
|
||||||
|
|
@ -3,63 +3,64 @@ package ui_test
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAppGetCmd(t *testing.T) {
|
func TestAppGetCmd(t *testing.T) {
|
||||||
a := ui.NewApp("")
|
a := ui.NewApp(config.NewConfig(nil), "")
|
||||||
a.Init()
|
a.Init()
|
||||||
a.CmdBuff().Set("blee")
|
a.CmdBuff().SetText("blee")
|
||||||
|
|
||||||
assert.Equal(t, "blee", a.GetCmd())
|
assert.Equal(t, "blee", a.GetCmd())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppInCmdMode(t *testing.T) {
|
func TestAppInCmdMode(t *testing.T) {
|
||||||
a := ui.NewApp("")
|
a := ui.NewApp(config.NewConfig(nil), "")
|
||||||
a.Init()
|
a.Init()
|
||||||
a.CmdBuff().Set("blee")
|
a.CmdBuff().SetText("blee")
|
||||||
assert.False(t, a.InCmdMode())
|
assert.False(t, a.InCmdMode())
|
||||||
|
|
||||||
a.CmdBuff().SetActive(true)
|
a.CmdBuff().SetActive(false)
|
||||||
assert.True(t, a.InCmdMode())
|
assert.False(t, a.InCmdMode())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppResetCmd(t *testing.T) {
|
func TestAppResetCmd(t *testing.T) {
|
||||||
a := ui.NewApp("")
|
a := ui.NewApp(config.NewConfig(nil), "")
|
||||||
a.Init()
|
a.Init()
|
||||||
a.CmdBuff().Set("blee")
|
a.CmdBuff().SetText("blee")
|
||||||
|
|
||||||
a.ResetCmd()
|
a.ResetCmd()
|
||||||
|
|
||||||
assert.Equal(t, "", a.CmdBuff().String())
|
assert.Equal(t, "", a.CmdBuff().GetText())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppHasCmd(t *testing.T) {
|
func TestAppHasCmd(t *testing.T) {
|
||||||
a := ui.NewApp("")
|
a := ui.NewApp(config.NewConfig(nil), "")
|
||||||
a.Init()
|
a.Init()
|
||||||
|
|
||||||
a.ActivateCmd(true)
|
a.ActivateCmd(true)
|
||||||
assert.False(t, a.HasCmd())
|
assert.False(t, a.HasCmd())
|
||||||
|
|
||||||
a.CmdBuff().Set("blee")
|
a.CmdBuff().SetText("blee")
|
||||||
assert.True(t, a.InCmdMode())
|
assert.True(t, a.InCmdMode())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppGetActions(t *testing.T) {
|
func TestAppGetActions(t *testing.T) {
|
||||||
a := ui.NewApp("")
|
a := ui.NewApp(config.NewConfig(nil), "")
|
||||||
a.Init()
|
a.Init()
|
||||||
|
|
||||||
a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}})
|
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) {
|
func TestAppViews(t *testing.T) {
|
||||||
a := ui.NewApp("")
|
a := ui.NewApp(config.NewConfig(nil), "")
|
||||||
a.Init()
|
a.Init()
|
||||||
|
|
||||||
vv := []string{"crumbs", "logo", "cmd", "menu"}
|
vv := []string{"crumbs", "logo", "prompt", "menu"}
|
||||||
for i := range vv {
|
for i := range vv {
|
||||||
v := vv[i]
|
v := vv[i]
|
||||||
t.Run(v, func(t *testing.T) {
|
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.Crumbs())
|
||||||
assert.NotNil(t, a.Logo())
|
assert.NotNil(t, a.Logo())
|
||||||
assert.NotNil(t, a.Cmd())
|
assert.NotNil(t, a.Prompt())
|
||||||
assert.NotNil(t, a.Menu())
|
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