K9s/rel v0.30.0 (#2361)

* [Maint] Refactor VS col handling

* [Bug] Add helm hist <enter> values cmd

* [Bug] Add context specific skins within a given cluster config.

* [Maint] Image scan controls

* [Bug] Fix fwd+bench timestamp

* [Refact] all-ns const

* [Maint] update tabledefs from metav1beta1 to metav1

* [Feat] Introduce workload view

* [Maint] Add convenience to map out ns names

- Refactor allnamespaces

* [Cleanup] axe pegomock

* [Refact] Use gvr type vs string

* [Feat] Add blacklist scans exclusions

* [Feat] setLabels for stored commands

* [Maint] Rename api-group column

* [Refact] gvr type refactor

* [Maint] Cleaning up

* [Bug] Add ability to skin based on context

- Handles cluster spanning *contexts

* [Maint] Cleaning up

* [Feat] Cmd interpreter

* [Maint] Clean up + bug fixes

* [Feat] Changed k9s config loader

> NOTE: !!Breaking change!!

- Make k9s config readonly
- Move writable artifacts to XDG data dir
- Move transient artifacts to XDG state dir
- Add per context skin option
- Add per context readonly option
- Consistent pluralization file names to yaml section

* [Docs] Update release and README docs

* [Maint] Rebase + cleanup

* [Maint] Normalize config extensions all yml -> yaml

* [Maint] Cleaning up + fixes
mine
Fernand Galiana 2023-12-23 14:29:55 -07:00 committed by GitHub
parent 7897fb0eef
commit dcec53e061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
303 changed files with 5719 additions and 5268 deletions

3
.gitignore vendored
View File

@ -4,7 +4,7 @@
.envrc
cov.out
execs
k9s
/k9s
/k8s
dist
notes
@ -23,3 +23,4 @@ demos
/code
kind
*.snap
/stresser

View File

@ -1,8 +1,10 @@
project_name: k9s
before:
hooks:
- go mod download
- go generate ./...
release:
prerelease: false

View File

@ -13,7 +13,7 @@ RUN apk --no-cache add --update make libx11-dev git gcc libc-dev curl && make bu
# Build the final Docker image
FROM alpine:3.19.0
ARG KUBECTL_VERSION="v1.27.3"
ARG KUBECTL_VERSION="v1.29.0"
COPY --from=build /k9s/execs/k9s /bin/k9s
RUN apk add --update ca-certificates \

View File

@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
else
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif
VERSION ?= v0.29.0
VERSION ?= v0.30.0
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}

420
README.md
View File

@ -11,7 +11,10 @@ for changes and offers subsequent commands to interact with your observed resour
## Note...
As you may know k9s is not pimped out by a big corporation with deep pockets. It is a complex OSS project that demands a lot of my time to maintain and support. K9s will always remain OSS and therefore free! That said if you feel, k9s makes your day to day Kubernetes journey a tad brighter, please consider sponsoring us or purchase a [K9sAlpha license](https://k9salpha.io). Your donations will go a long way in keeping our servers lights on and beers in our fridge!
K9s is not pimped out by a big corporation with deep pockets.
It is a complex OSS project that demands a lot of my time to maintain and support.
K9s will always remain OSS and therefore free! That said, if you feel k9s makes your day to day Kubernetes journey a tad brighter, saves you time and makes you more productive, please consider [sponsoring us!](https://github.com/sponsors/derailed)
Your donations will go a long way in keeping our servers lights on and beers in our fridge!
**Thank you!**
@ -28,6 +31,35 @@ As you may know k9s is not pimped out by a big corporation with deep pockets. It
---
## Screenshots
1. Pods
<img src="assets/screen_po.png"/>
2. Logs
<img src="assets/screen_logs.png"/>
3. Deployments
<img src="assets/screen_dp.png"/>
---
## Demo Videos/Recordings
* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
* [K9s v0.29.0](https://youtu.be/oiU3wmoAkBo)
* [K9s v0.21.3](https://youtu.be/wG8KCwDAhnw)
* [K9s v0.19.X](https://youtu.be/kj-WverKZ24)
* [K9s v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw)
* [K9s v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be)
* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN)
* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM)
* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s)
* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I)
* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8)
* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU)
---
## Documentation
Please refer to our [K9s documentation](https://k9scli.io) site for installation, usage, customization and tips.
@ -42,8 +74,7 @@ Wanna discuss K9s features with your fellow `K9sers` or simply show your support
## Installation
K9s is available on Linux, macOS and Windows platforms.
* Binaries for Linux, Windows and Mac are available as tarballs in the [release](https://github.com/derailed/k9s/releases) page.
Binaries for Linux, Windows and Mac are available as tarballs in the [release page](https://github.com/derailed/k9s/releases).
* Via [Homebrew](https://brew.sh/) for macOS or Linux
@ -56,6 +87,7 @@ K9s is available on Linux, macOS and Windows platforms.
```shell
sudo port install k9s
```
* Via [snap](https://snapcraft.io/k9s) for Linux
```shell
@ -81,6 +113,7 @@ K9s is available on Linux, macOS and Windows platforms.
```
* Via [Winget](https://github.com/microsoft/winget-cli) for Windows
```shell
winget install k9s
```
@ -132,7 +165,8 @@ K9s is available on Linux, macOS and Windows platforms.
## Building From Source
K9s is currently using go v1.14 or above. In order to build K9s from source you must:
K9s is currently using GO v1.21.X or above.
In order to build K9s from source you must:
1. Clone the repo
2. Build and run the executable
@ -164,7 +198,7 @@ K9s is available on Linux, macOS and Windows platforms.
You can build your own Docker image of k9s from the [Dockerfile](Dockerfile) with the following:
```shell
docker build -t k9s-docker:0.1 .
docker build -t k9s-docker:v0.0.1 .
```
You can get the latest stable `kubectl` version and pass it to the `docker build` command with the `--build-arg` option.
@ -200,7 +234,7 @@ K9s is available on Linux, macOS and Windows platforms.
export K9S_EDITOR=my_fav_editor
```
* K9s prefers recent kubernetes versions ie 1.16+
* K9s prefers recent kubernetes versions ie 1.28+
---
@ -208,54 +242,79 @@ K9s is available on Linux, macOS and Windows platforms.
| k9s | k8s client |
| ------------------ | ---------- |
| >= v0.27.0 | 0.26.1 |
| v0.26.7 - v0.26.6 | 0.25.3 |
| v0.26.5 - v0.26.4 | 0.25.1 |
| v0.26.3 - v0.26.1 | 0.24.3 |
| v0.26.0 - v0.25.19 | 0.24.2 |
| v0.25.18 - v0.25.3 | 0.22.3 |
| v0.25.2 - v0.25.0 | 0.22.0 |
| <= v0.24 | 0.21.3 |
| >= v0.27.0 | 1.26.1 |
| v0.26.7 - v0.26.6 | 1.25.3 |
| v0.26.5 - v0.26.4 | 1.25.1 |
| v0.26.3 - v0.26.1 | 1.24.3 |
| v0.26.0 - v0.25.19 | 1.24.2 |
| v0.25.18 - v0.25.3 | 1.22.3 |
| v0.25.2 - v0.25.0 | 1.22.0 |
| <= v0.24 | 1.21.3 |
---
## The Command Line
```shell
# List all available CLI options
k9s help
# List current version
k9s version
# To get info about K9s runtime (logs, configs, etc..)
k9s info
# List all available CLI options
k9s help
# 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 cluster modification commands disabled
k9s --readonly
```
## Logs
## Logs And Debug 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:
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
# Find out where the logs are stored
k9s info
# Will produces something like this
# ____ __.________
# | |/ _/ __ \______
# | < \____ / ___/
# | | \ / /\___ \
# |____|__ \ /____//____ >
# \/ \/
#
# Configuration: ~/Library/Preferences/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
```text
____ __.________
| |/ _/ __ \______
| < \____ / ___/
| | \ / /\___ \
|____|__ \ /____//____ >
\/ \/
# Start K9s in debug mode
Version: vX.Y.Z
Config: /Users/fernand/.config/k9s/config.yaml
Logs: /Users/fernand/.local/state/k9s/k9s.log
Dumps dir: /Users/fernand/.local/state/k9s/screen-dumps
Benchmarks dir: /Users/fernand/.local/state/k9s/benchmarks
Skins dir: /Users/fernand/.local/share/k9s/skins
Contexts dir: /Users/fernand/.local/share/k9s/clusters
Custom views file: /Users/fernand/.local/share/k9s/views.yaml
Plugins file: /Users/fernand/.local/share/k9s/plugins.yaml
Hotkeys file: /Users/fernand/.local/share/k9s/hotkeys.yaml
Alias file: /Users/fernand/.local/share/k9s/aliases.yaml
```
### View K9s logs
```shell
tail -f /Users/fernand/.local/data/k9s/k9s.log
```
### Start K9s in debug mode
```shell
k9s -l debug
```
@ -263,57 +322,31 @@ k9s -l debug
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 short-name | `:`po⏎ | accepts singular, plural, short-name 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⏎ | Regex2 supported ie `fred|blee` to filter resources named fred or blee |
| Inverse regex filter | `/`! filter⏎ | Keep everything that *doesn't* match. |
| 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 (Pod view) | `:`ctx⏎ | |
| To view and switch directly to another Kubernetes context (Last used view) | `:`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, equivalent to kubectl delete --now) | `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 |
| Launch Popeye view | `:`popeye or pop⏎ | See [popeye](#popeye) |
---
## Screenshots
1. Pods
<img src="assets/screen_po.png"/>
1. Logs
<img src="assets/screen_logs.png"/>
1. Deployments
<img src="assets/screen_dp.png"/>
---
---
## Demo Videos/Recordings
* [k9s Kubernetes UI - A Terminal-Based Vim-Like Kubernetes Dashboard](https://youtu.be/boaW9odvRCc)
* [K9s v0.21.3](https://youtu.be/wG8KCwDAhnw)
* [K9s v0.19.X](https://youtu.be/kj-WverKZ24)
* [K9s v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw)
* [K9s v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be)
* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN)
* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM)
* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s)
* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I)
* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8)
* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU)
| 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 short-name | `:`pod⏎ | accepts singular, plural, short-name or alias ie pod or pods |
| View a Kubernetes resource in a given namespace | `:`pod ns-x⏎ | |
| View filtered pods (New v0.30.0!) | `:`pod /fred⏎ | View all pods filtered by fred |
| View labeled pods (New v0.30.0!) | `:`pod app=fred,env=dev⏎ | View all pods with labels matching app=fred and env=dev |
| View pods in a given context (New v0.30.0!) | `:`pod @ctx1⏎ | View all pods in context ctx1. Switches out your current k9s context! |
| Filter out a resource view given a filter | `/`filter⏎ | Regex2 supported ie `fred|blee` to filter resources named fred or blee |
| Inverse regex filter | `/`! filter⏎ | Keep everything that *doesn't* match. |
| 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 (Pod view) | `:`ctx⏎ | |
| To view and switch directly to another Kubernetes context (Last used view) | `:`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, equivalent to kubectl delete --now) | `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 |
| Launch Popeye view | `:`popeye or pop⏎ | See [popeye](#popeye) |
---
@ -327,13 +360,13 @@ K9s uses aliases to navigate most K8s resources.
> NOTE: This is still in flux and will change while in pre-release stage!
> NOTE! Thanks to [Mr Alexandru Placenta](https://github.com/placintaalexandru) the config files can now use either `.yml` or `.yaml` mimes.
```yaml
# $XDG_CONFIG_HOME/k9s/config.yml
# $XDG_CONFIG_HOME/k9s/config.yaml
k9s:
# Enable periodic refresh of resource browser windows. Default false
liveViewAutoRefresh: false
# The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info)
screenDumpDir: /tmp/dumps
# Represents ui poll intervals. Default 2secs
refreshRate: 2
# Number of retries once the connection to the api-server is lost. Default 15.
@ -368,12 +401,6 @@ K9s uses aliases to navigate most K8s resources.
textWrap: false
# Toggles log line timestamp info. Default false
showTime: false
# Indicates the current kube context. Defaults to current context
currentContext: minikube
# Indicates the current kube cluster. Defaults to current context cluster
currentCluster: minikube
# KeepMissingClusters will keep clusters in the config if they are missing from the current kubeconfig file. Default false
KeepMissingClusters: false
# Provide shell pod customization when nodeShell feature gate is enabled!
shellPod:
# The shell pod image to use.
@ -386,41 +413,13 @@ K9s uses aliases to navigate most K8s resources.
memory: 100Mi
# Enable TTY
tty: true
# Persists per cluster preferences for favorite namespaces and view.
clusters:
coolio:
namespace:
active: coolio
# With this set, the favorites list won't be updated as you switch namespaces
lockFavorites: false
favorites:
- cassandra
- default
view:
active: po
featureGates:
# Toggles NodeShell support. Allow K9s to shell into nodes if needed. Default false.
nodeShell: true
# The IP Address to use when launching a port-forward.
portForwardAddress: 1.2.3.4
kind:
namespace:
active: all
favorites:
- all
- kube-system
- default
view:
active: dp
# The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info)
screenDumpDir: /tmp
```
---
## <a id="popeye"></a>Popeye Configuration
K9s has integration with [Popeye](https://popeyecli.io/), which is a Kubernetes cluster sanitizer. Popeye itself uses a configuration called `spinach.yml`, but when integrating with K9s the cluster-specific file should be name `$XDG_CONFIG_HOME/k9s/<context>_spinach.yml`. This allows you to have a different spinach config per cluster.
K9s has integration with [Popeye](https://popeyecli.io/), which is a Kubernetes cluster sanitizer. Popeye itself uses a configuration called `spinach.yml`, but when integrating with K9s the cluster-specific file should be name `$XDG_CONFIG_HOME/share/k9s/clusters/clusterX/contextY/spinach.yml`. This allows you to have a different spinach config per cluster.
---
@ -429,7 +428,7 @@ K9s has integration with [Popeye](https://popeyecli.io/), which is a Kubernetes
By enabling the nodeShell feature gate on a given cluster, K9s allows you to shell into your cluster nodes. Once enabled, you will have a new `s` for `shell` menu option while in node view. K9s will launch a pod on the selected node using a special k9s_shell pod. Furthermore, you can refine your shell pod by using a custom docker image preloaded with the shell tools you love. By default k9s uses a BusyBox image, but you can configure it as follows:
```yaml
# $XDG_CONFIG_HOME/k9s/config.yml
# $XDG_CONFIG_HOME/k9s/config.yaml
k9s:
# You can also further tune the shell pod specification
shellPod:
@ -438,41 +437,63 @@ k9s:
limits:
cpu: 100m
memory: 100Mi
clusters:
# Configures node shell on cluster blee
blee:
featureGates:
# You must enable the nodeShell feature gate to enable shelling into nodes
nodeShell: true
```
Then in your cluster configuration file...
```yaml
# $XDG_DATA_HOME/k9s/clusters/cluster-1/context-1
k9s:
cluster: cluster-1
readOnly: false
namespace:
active: default
lockFavorites: false
favorites:
- kube-system
- default
view:
active: po
featureGates:
nodeShell: true # => Enable this feature gate to make nodeShell available on this cluster
portForwardAddress: localhost
```
---
## Command Aliases
In K9s, you can define your very own command aliases (shortnames) to access your resources. In your `$HOME/.config/k9s` define a file called `alias.yml`. A K9s alias defines pairs of alias:gvr. A gvr (Group/Version/Resource) represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file:
In K9s, you can define your very own command aliases (shortnames) to access your resources. In your `$HOME/.config/k9s` define a file called `aliases.yaml`.
A K9s alias defines pairs of alias:gvr. A gvr (Group/Version/Resource) represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file:
```yaml
# $XDG_CONFIG_HOME/k9s/alias.yml
alias:
# $XDG_DATA_HOME/k9s/aliases.yaml
aliases:
pp: v1/pods
crb: rbac.authorization.k8s.io/v1/clusterrolebindings
# As of v0.30.0 you can also refer to another command alias...
fred: pod fred app=blee # => view pods in namespace fred with labels matching app=blee
```
Using this alias file, you can now type pp/crb to list pods or ClusterRoleBindings respectively.
Using this aliases file, you can now type `:pp` or `:crb` or `:fred` to activate their respective commands.
---
## HotKey Support
Entering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources. We're introducing hotkeys that allows a user to define their own hotkeys to activate their favorite resource views. In order to enable hotkeys please follow these steps:
Entering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources.
We're introducing hotkeys that allow users to define their own key combination to activate their favorite resource views.
1. Create a file named `$XDG_CONFIG_HOME/k9s/hotkey.yml`
2. Add the following to your `hotkey.yml`. You can use resource name/short name to specify a command ie same as typing it while in command mode.
Additionally, you can define context specific hotkeys by add a context level configuration file in `$XDG_DATA_HOME/k9s/clusters/clusterX/contextY/hotkeys.yaml`
In order to surface hotkeys globally please follow these steps:
1. Create a file named `$XDG_CONFIG_HOME/k9s/hotkeys.yaml`
2. Add the following to your `hotkeys.yaml`. You can use resource name/short name to specify a command ie same as typing it while in command mode.
```yaml
# $XDG_CONFIG_HOME/k9s/hotkey.yml
hotKey:
# $XDG_CONFIG_HOME/k9s/hotkeys.yaml
hotKeys:
# Hitting Shift-0 navigates to your pod view
shift-0:
shortCut: Shift-0
@ -490,7 +511,8 @@ Entering the command mode and typing a resource name or alias, could be cumberso
command: xray deploy
```
Not feeling so hot? Your custom hotkeys will be listed in the help view `?`. Also your hotkey file will be automatically reloaded so you can readily use your hotkeys as you define them.
Not feeling so hot? Your custom hotkeys will be listed in the help view `?`.
Also your hotkeys file will be automatically reloaded so you can readily use your hotkeys as you define them.
You can choose any keyboard shortcuts that make sense to you, provided they are not part of the standard K9s shortcuts list.
@ -502,9 +524,9 @@ Entering the command mode and typing a resource name or alias, could be cumberso
As of v0.25.0, you can leverage the `FastForwards` feature to tell K9s how to default port-forwards. In situations where you are dealing with multiple containers or containers exposing multiple ports, it can be cumbersome to specify the desired port-forward from the dialog as in most cases, you already know which container/port tuple you desire. For these use cases, you can now annotate your manifests with the following annotations:
- `k9scli.io/auto-port-forwards`
@ `k9scli.io/auto-port-forwards`
activates one or more port-forwards directly bypassing the port-forward dialog all together.
- `k9scli.io/port-forwards`
@ `k9scli.io/port-forwards`
pre-selects one or more port-forwards when launching the port-forward dialog.
The annotation value takes on the shape `container-name::[local-port:]container-port`
@ -553,14 +575,14 @@ The annotation value must specify a container to forward to as well as a local p
[SneakCast v0.17.0 on The Beach! - Yup! sound is sucking but what a setting!](https://youtu.be/7S33CNLAofk)
You can change which columns shows up for a given resource via custom views. To surface this feature, you will need to create a new configuration file, namely `$XDG_CONFIG_HOME/k9s/views.yml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live!
You can change which columns shows up for a given resource via custom views. To surface this feature, you will need to create a new configuration file, namely `$XDG_CONFIG_HOME/k9s/views.yaml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live!
> NOTE: This is experimental and will most likely change as we iron this out!
Here is a sample views configuration that customize a pods and services views.
```yaml
# $XDG_CONFIG_HOME/k9s/views.yml
# $XDG_CONFIG_HOME/k9s/views.yaml
k9s:
views:
v1/pods:
@ -585,7 +607,9 @@ k9s:
## Plugins
K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins. K9s will look at `$XDG_CONFIG_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 `$XDG_CONFIG_HOME/k9s/plugins.yaml` 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
* Confirm option (when enabled) lets you see the command that is going to be executed and gives you an option to confirm or prevent execution
@ -614,13 +638,13 @@ K9s does provide additional environment variables for you to customize your plug
Curly braces can be used to embed an environment variable inside another string, or if the column name contains special characters. (e.g. `${NAME}-example` or `${COL-%CPU/L}`)
### Example
### Plugin Example
This defines a plugin for viewing logs on a selected pod using `ctrl-l` for shortcut.
This defines a plugin for viewing logs on a selected pod using `ctrl-l` as shortcut.
```yaml
# $XDG_CONFIG_HOME/k9s/plugin.yml
plugin:
# $XDG_DATA_HOME/k9s/plugins.yaml
plugins:
# Defines a plugin to provide a `ctrl-l` shortcut to tail the logs while in pod view.
fred:
shortCut: Ctrl-L
@ -657,12 +681,14 @@ Initially, the benchmarks will run with the following defaults:
* HTTP Verb: GET
* Path: /
The PortForward view is backed by a new K9s config file namely: `$XDG_CONFIG_HOME/k9s/bench-<k8s_context>.yml` (note: extension is `yml` and not `yaml`). Each cluster you connect to will have its own bench config file, containing the name of the K8s context for the cluster. Changes to this file should automatically update the PortForward view to indicate how you want to run your benchmarks.
The PortForward view is backed by a new K9s config file namely: `$XDG_DATA_HOME/k9s/clusters/clusterX/contextY/benchmarks.yaml`. Each cluster you connect to will have its own bench config file, containing the name of the K8s context for the cluster. Changes to this file should automatically update the PortForward view to indicate how you want to run your benchmarks.
Here is a sample benchmarks.yml configuration. Please keep in mind this file will likely change in subsequent releases!
Benchmarks result reports are stored in `$XDG_STATE_HOME/k9s/clusters/clusterX/contextY`
Here is a sample benchmarks.yaml configuration. Please keep in mind this file will likely change in subsequent releases!
```yaml
# This file resides in $XDG_CONFIG_HOME/k9s/bench-mycontext.yml
# This file resides in $XDG_DATA_HOME/k9s/clusters/clusterX/contextY/benchmarks.yaml
benchmarks:
# Indicates the default concurrency and number of requests setting if a container or service rule does not match.
defaults:
@ -810,36 +836,90 @@ Example: Dracula Skin ;)
<img src="assets/skins/dracula.png" alt="Dracula Skin">
You can style K9s based on your own sense of look and style. Skins are YAML files, that enable a user to change the K9s presentation layer. K9s default skin is loaded from `$XDG_CONFIG_HOME/k9s/skin.yml`. If a skin file is detected then the skin will be loaded if not the current stock skin remains in effect.
You can style K9s based on your own sense of look and style. Skins are YAML files, that enable a user to change the K9s presentation layer. See this repo `skins` directory for examples.
You can skin k9s by default by specifying a UI.skin attribute. You can also change K9s skins based on the context you are connecting too.
In this case, you can specify a skin field on your cluster config aka `skin: dracula` (just the name of the skin file without the extension!) and copy this repo
`skins/dracula.yaml` to `$XDG_CONFIG_HOME/k9s/skins/` directory.
You can also change K9s skins based on the cluster you are connecting too. In this case, you can specify a skin field on your cluster config aka `skin: dracula` (just the name of the skin!) and copy this repo skins/dracula.yml to `$XDG_CONFIG_HOME/k9s/skins` directory.
Below is a sample skin file, more skins are available in the skins directory in this repo, just simply copy any of these in your k9s home dir as `skin.yml`.
In the case where your cluster spans several contexts, you can add a skin context configuration to your context configuration.
This is a collection of {context_name, skin} tuples (please see example below!)
Colors can be defined by name or using a hex representation. Of recent, we've added a color named `default` to indicate a transparent background color to preserve your terminal background color settings if so desired.
> NOTE: This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly!
> NOTE: Please see [K9s Skins](https://k9scli.io/topics/skins/) for a list of available colors.
To skin a specific context and provided the file `in_the_navy.yaml` is present in your skins directory.
```yaml
# Make cluster fred display in_the_navy skin when loaded...
# $XDG_DATA_HOME/k9s/clusters/clusterX/contextY/config.yaml
k9s:
...
clusters:
fred:
# Override the default skin and use this skin for this cluster.
# NOTE: Just the skin file name to extension!
skin: in_the_navy # -> Look for a skin file in ~/.config/k9s/skins/in_the_navy.yml
namespace:
...
view:
active: pod
featureGates:
nodeShell: false
portForwardAddress: localhost
cluster: clusterX
skin: in_the_navy
readOnly: false
namespace:
active: default
lockFavorites: false
favorites:
- kube-system
- default
view:
active: po
featureGates:
nodeShell: false
portForwardAddress: localhost
```
You can also specify a default skin for all contexts in the root k9s config file as so:
```yaml
k9s:
liveViewAutoRefresh: false
screenDumpDir: /tmp/dumps
refreshRate: 2
maxConnRetry: 5
readOnly: false
noExitOnCtrlC: false
ui:
enableMouse: false
headless: false
logoless: false
crumbsless: false
noIcons: false
# By default all contexts wil use the dracula skin unless explicitly overridden in the context config file.
skin: dracula # => assumes the file skins/dracular.yaml is present in the $XDG_DATA_HOME/k9s/skins directory
skipLatestRevCheck: false
disablePodCounting: false
shellPod:
image: busybox
namespace: default
limits:
cpu: 100m
memory: 100Mi
imageScans:
enable: false
blackList:
namespaces: []
labels: {}
logger:
tail: 100
buffer: 5000
sinceSeconds: -1
fullScreenLogs: false
textWrap: false
showTime: false
thresholds:
cpu:
critical: 90
warn: 70
memory:
critical: 90
warn: 70
```
```yaml
# in_the_navy.yml: Skin InTheNavy...
# $XDG_DATA_HOME/k9s/skins/in_the_navy.yaml
# Skin InTheNavy!
k9s:
# General K9s styles
body:
@ -935,7 +1015,7 @@ that you want, please file an issue and if so inclined submit a PR!
K9s will most likely blow up if...
1. You're running older versions of Kubernetes. K9s works best on Kubernetes latest.
1. You're running older versions of Kubernetes. K9s works best on later Kubernetes versions.
2. You don't have enough RBAC fu to manage your cluster.
---
@ -966,4 +1046,4 @@ We always enjoy hearing from folks who benefit from our work!
---
<img src="assets/imhotep_logo.png" width="32" height="auto" alt="Imhotep"/> &nbsp;© 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
<img src="assets/imhotep_logo.png" width="32" height="auto" alt="Imhotep"/> &nbsp;© 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -0,0 +1,313 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png" align="center" width="800" height="auto"/>
# Release v0.30.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 are, as ever, very much noted and appreciated!
Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!
As you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,
please consider joining our [sponsorship 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)
---
## ♫ Sounds Behind The Release ♭
Going back to the classics...
* [Home For Christmas - Fats Domino](https://www.youtube.com/watch?v=ykAVdPz8o1Q)
* [Our Love - Al Jarreau](https://www.youtube.com/watch?v=9ztMe6GIwi8)
* [Body And Soul - Louis Armstrong](https://www.youtube.com/watch?v=2Gnz69TbqHQ)
* [On The Dunes - Donald Fagen](https://www.youtube.com/watch?v=QoVT3XcMVvk)
* [Ciao - Lucio Dalla](https://www.youtube.com/watch?v=qcqXcmKu_I4)
* [Basin Street Blues - Louis Prima](https://www.youtube.com/watch?v=IijXXXpUefM&list=RDIijXXXpUefM&start_radio=1)
---
## A Word From Our Sponsors...
To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!
* [Bojan](https://github.com/rbojan)
> Sponsorship cancellations since the last release: **5!** 🥹
---
## 🎄 Feature Release! 🎄
🎅 Merry Christmas to all and Best wishes for the new year!!🧑‍🎄
---
### Videos Are In The Can!
Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
---
### Breaking Bad!
> ☢️ !!Prior to installing v0.30.0!! Please be sure to backup your k9s configs directories or move them somewhere safe!!
> ☢️ Please watch the v0.30.0 Sneak peek series (links below) for detailed information.
>
> ☢️ Most K9s configuration files have either split or changed location or names on this drop!!
> We recommend moving your current k9s config dirs to another location and start k9s from scratch and let it create and initialize the various configs
> to their new spec and location. You can then use your existing setup and patch with the new layout/spec.
> As of v0.30.0 all config files now use the `*.yaml` extension. We did our best to update all the docs to match the new version.
> If you find doc issues either file an issue or better yet submit a PR!
Some of you might say: `You're on the roll their bud! Two breaking changes drops in a row!!`
Per the wise words of my beloved Grand mama! `One can't cook a decent meal without creating a mess!`
Not to mention we're still at v0.x.y so `Open season on breaking changes` is very much in full effect.
Tho I have tested this drop quite a bit, there is a strong chance that I've broken some stuff.
The key here is to walk the fine line of improving k9s code base and features set with minimal impact to you.
As you know by now, I am committed to ease the pain and resolve issues quickly to get you all back up and running.
From the scope changes in this release, I would caution that this drop will likely break you!
If so, worry not! We will fix the duds so we are `Happy as a Hippo` once again.
There was a few issues with the way K9s persists it's configuration and various artifacts. So we rewrote it!
First and foremost all k9s related YAML resources, will now use the standard ".yaml" extension.
I think we've bloated the code checking for both extensions with no real actionable value!
As it stands the main K9s configuration `config.yml` will now be static. These settings are now readonly! All the dynamic configurations that K9s manages now live in a new directory aka `clusters`. The clusters directory manages your k8s cluster/context configurations. So things like active view, namespace, favorites, etc... now live in this directory. K9s configurations are still managed using either xdg `XDG_CONFIG_HOME` or you can set `K9S_CONFIG_DIR` to specify a your preferred k9s configs location. Also all config files will now use the ".yaml" extension vs ".yml"!!
So the main k9s configuration (static) now looks like this:
```yaml
# $XDG_CONFIG_HOME/k9s/config.yaml
# File will be autogenerated will all the default fixins if not found in the config specification.
k9s:
liveViewAutoRefresh: false
refreshRate: 2
maxConnRetry: 5
readOnly: false
noExitOnCtrlC: false
ui: # NOTE! New level!!
enableMouse: false
headless: false
logoless: false
crumbsless: false
noIcons: false
skipLatestRevCheck: false
disablePodCounting: false
# ShellPod configuration applies to all your clusters
shellPod:
image: busybox:1.35.0
namespace: default
limits:
cpu: 100m
memory: 100Mi
# ImageScan config changed from v0.29.0!
imageScans:
enable: false
# Now figures exclusions ie blacklist namespaces or specific workload labels
blackList:
# Exclude the following namespaces for image vulscans!
namespaces:
- kube-system
- fred
# Exclude the following labels from image vulscans!
labels:
k8s-app:
- kindnet
- bozo
env:
- dev
logger:
tail: 100
buffer: 5000
sinceSeconds: -1
fullScreenLogs: false
textWrap: false
showTime: false
thresholds:
cpu:
critical: 90
warn: 70
memory:
critical: 90
warn: 70
```
Next context specific configurations that are managed by you and k9s live in the XDG data directory
i.e `$XDG_DATA_HOME/k9s/clusters` or `$K9S_CONFIG_DIR/clusters` if the env var is set.
```text
$XDG_DATA_HOME/k9s
// Clusters tracks visited kubeconfig cluster/contexts
├── clusters
│ ├── fred
│ │ └── bozo
│ │ └── config.yaml
│ ├── bozorg
│ │ ├── kind-bozo-1
│ │ │ └── config.yaml
│ │ ├── kind-bozo-2
│ │ │ └── config.yaml
│ │ └── kind-bozo-3
│ │ └── config.yaml
│ └── bumblebeetuna
│ └── blee
│ └── config.yaml
└── skins
├── black_and_wtf.yaml
├── dracula.yaml
├── in_the_navy.yml
├── ...
```
Now looking at a given context configuration i.e cluster-1/context-1/config.yaml
```yaml
# $XDG_DATA_HOME/k9s/clusters/bumblebeetuna/blee/config.yaml
k9s:
cluster: bumblebeetuna
readOnly: false # [New!] you can now single out a given context and make it readonly. Woof!
skin: in_the_navy # [NEW!] you can also skin individual contexts. Woof Woof!
namespace:
active: all
lockFavorites: false
favorites:
- all
- kube-system
- default
view:
active: dp
featureGates:
nodeShell: false
portForwardAddress: localhost
```
Transient artifacts ie k9s logs, screen-dumps, benchmarks etc now live in the state config dir.
```text
$XDG_STATE_HOME/k9s
├── k9s.log # K9s log files
└── screen-dumps
└── bumblebeetuna # Screen dumps location for context blee
└── blee
└── deployments-kube-system-1703018199222861000.csv
```
If you get stuck or if my instructions are just `clear as mud`... `k9s info` is always your friend!!
I feel this is an improvement (tho I might be unanimous on this!) especially for folks dealing with multi-clusters or swapping out there kubeconfigs...
> NOTE! Paint is still fresh on this deal. Proceed with caution and please help us flush this feature out!
---
# Got Prompt?
In this drop, we've also gave the k9s command prompt aka `:xxx` some love.
You have the ability to specify filter directly in the prompt.
So for example, you can now run something like `:po /fred` to run pod view with a filter to just show pods containing `fred`. Likewise `:po k8s-app=fred,env=blee` to filter by labels.
And now for the`Krampus` special... you can see pods in a different context all together via `:pod @ctx-2`.
Finally you can combo and send the `whole enchilada` via `:po k8s-app=fred /blee ns-1 @ctx-x`
Did I mention with completion where applicable? Yes Please!!
Compliments of [Jayson Wang](https://github.com/wjiec). Be sure to thank him!!
Put these frequent flyers command in an alias and now you can nav your clusters with `even more style`!
---
# All Is Love?
🎵 `On The twentieth day of Christmas my true love gave to me... Ten worklords a-leaping??...` 🎵
This is a feature reported by many of you and its (finally!) here. As of this drop, we intro the `workload` view aka `wk` which is similar to `kubetcl get all`. I was reluctant to intro it given the potential hazards on larger clusters but figured why not? YOLO. I think using it in combo with the prompt updates it could pack a serious punch to observe workload related artifacts.
---
# The Black List...
As it seems customary with all k9s new features, folks want to turn them off ;(
The `Vulscan` feature did not get out unscaped ;(
As it was rightfully so pointed out, you may want to opted out scans for images that you do not control.
Tho I think it might be a good idea to run wide open once in a while to see if your cluster has any holes??
For this reason, we've opted to intro a blacklist section under the image scan configuration to exclude certain images from the scans.
Here is a sample configuration:
```yaml
k9s:
liveViewAutoRefresh: false
refreshRate: 2
ui:
enableMouse: false
headless: false
logoless: false
crumbsless: false
noIcons: false
imageScans:
enable: true
blackList:
# Skip scans on these namespaces
namespaces:
- ns-1
- ns-2
# Skip scans for pods matching these labels
labels:
- app:
- fred
- blee
- duh
- env:
- dev
```
This is a bit of a blur now, but I think that it! We hope you guys will dig this drop or at least the concepts as likely this is going to be `Open Season` on bugs ;(
🎵 `On The second day of Christmas my true love gave to me... Eleven buggers bugging??...` 🎵
Lastly looks like the sponsorship stream is down to an alarming trickle so if you dig this project and find it useful be sure `to give til it hurts!`
---
🎅 Best wishes to you and yours for good health and happiness this holiday season!! 🎉
AndJoy!
Fernand
---
## Resolved Issues
* [#2346](https://github.com/derailed/k9s/issues/2346) k9s should not write state to config.yaml
* [#2335](https://github.com/derailed/k9s/issues/2335) Restore 0.28 column order on pod view bug
* [#2331](https://github.com/derailed/k9s/issues/2331) Set a shortcut key to run Vuln Scanning on a resource. Don't scan every resource at every startup.
* [#2283](https://github.com/derailed/k9s/issues/2283) Adding auto complete in search bar
---
## Contributed PRs
Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!
* [#2357](https://github.com/derailed/k9s/pull/2357) Added ln check for snap
* [#2350](https://github.com/derailed/k9s/pull/2350) Add symlink into snap
* [#2348](https://github.com/derailed/k9s/pull/2348) Fix(misc plugins): split up multiline commands, use less -K everywhere
* [#2343](https://github.com/derailed/k9s/pull/2343) Passing on the correct suggestion parameters
* [#2341](https://github.com/derailed/k9s/pull/2340) Adding value, yaml and describe views to helm-history
* [#2340](https://github.com/derailed/k9s/pull/2340) Add pkgx to installation section
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -5,7 +5,6 @@ package cmd
import (
"fmt"
"os"
"github.com/derailed/k9s/internal/color"
@ -19,21 +18,31 @@ import (
func infoCmd() *cobra.Command {
return &cobra.Command{
Use: "info",
Short: "Print configuration info",
Long: "Print configuration information",
Run: func(cmd *cobra.Command, args []string) {
printInfo()
},
Short: "List K9s configurations info",
RunE: printInfo,
}
}
func printInfo() {
const fmat = "%-25s %s\n"
func printInfo(cmd *cobra.Command, args []string) error {
if err := config.InitLocs(); err != nil {
return err
}
const fmat = "%-27s %s\n"
printLogo(color.Cyan)
printTuple(fmat, "Configuration", config.K9sConfigFile, color.Cyan)
printTuple(fmat, "Logs", config.DefaultLogFile, color.Cyan)
printTuple(fmat, "Screen Dumps", getScreenDumpDirForInfo(), color.Cyan)
printTuple(fmat, "Version", version, color.Cyan)
printTuple(fmat, "Config", config.AppConfigFile, color.Cyan)
printTuple(fmat, "Custom Views", config.AppViewsFile, color.Cyan)
printTuple(fmat, "Plugins", config.AppPluginsFile, color.Cyan)
printTuple(fmat, "Hotkeys", config.AppHotKeysFile, color.Cyan)
printTuple(fmat, "Aliases", config.AppAliasesFile, color.Cyan)
printTuple(fmat, "Skins", config.AppSkinsDir, color.Cyan)
printTuple(fmat, "Context Configs", config.AppContextsDir, color.Cyan)
printTuple(fmat, "Logs", config.AppLogFile, color.Cyan)
printTuple(fmat, "Benchmarks", config.AppBenchmarksDir, color.Cyan)
printTuple(fmat, "ScreenDumps", getScreenDumpDirForInfo(), color.Cyan)
return nil
}
func printLogo(c color.Paint) {
@ -45,23 +54,20 @@ func printLogo(c color.Paint) {
// getScreenDumpDirForInfo get default screen dump config dir or from config.K9sConfigFile configuration.
func getScreenDumpDirForInfo() string {
if config.K9sConfigFile == "" {
return config.K9sDefaultScreenDumpDir
if config.AppConfigFile == "" {
return config.AppDumpsDir
}
f, err := os.ReadFile(config.K9sConfigFile)
f, err := os.ReadFile(config.AppConfigFile)
if err != nil {
log.Error().Err(err).Msgf("Reads k9s config file %v", err)
return config.K9sDefaultScreenDumpDir
return config.AppDumpsDir
}
var cfg config.Config
if err := yaml.Unmarshal(f, &cfg); err != nil {
log.Error().Err(err).Msgf("Unmarshal k9s config %v", err)
return config.K9sDefaultScreenDumpDir
}
if cfg.K9s == nil {
cfg.K9s = config.NewK9s()
return config.AppDumpsDir
}
return cfg.K9s.GetScreenDumpDir()

View File

@ -16,32 +16,31 @@ func Test_getScreenDumpDirForInfo(t *testing.T) {
expectedScreenDumpDir string
}{
"withK9sConfigFile": {
k9sConfigFile: "testdata/k9s.yml",
k9sConfigFile: "testdata/k9s.yaml",
expectedScreenDumpDir: "/tmp",
},
"withEmptyK9sConfigFile": {
k9sConfigFile: "",
expectedScreenDumpDir: config.K9sDefaultScreenDumpDir,
expectedScreenDumpDir: config.AppDumpsDir,
},
"withInvalidK9sConfigFilePath": {
k9sConfigFile: "invalid",
expectedScreenDumpDir: config.K9sDefaultScreenDumpDir,
expectedScreenDumpDir: config.AppDumpsDir,
},
"withScreenDumpDirEmptyInK9sConfigFile": {
k9sConfigFile: "testdata/k9s1.yml",
expectedScreenDumpDir: config.K9sDefaultScreenDumpDir,
k9sConfigFile: "testdata/k9s1.yaml",
expectedScreenDumpDir: config.AppDumpsDir,
},
}
for k := range tests {
u := tests[k]
t.Run(k, func(t *testing.T) {
initK9sConfigFile := config.K9sConfigFile
config.K9sConfigFile = u.k9sConfigFile
initK9sConfigFile := config.AppConfigFile
config.AppConfigFile = u.k9sConfigFile
assert.Equal(t, u.expectedScreenDumpDir, getScreenDumpDirForInfo())
config.K9sConfigFile = initK9sConfigFile
config.AppConfigFile = initK9sConfigFile
})
}
}

View File

@ -8,6 +8,8 @@ import (
"os"
"runtime/debug"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/config"
@ -20,12 +22,12 @@ import (
)
const (
appName = "k9s"
appName = config.AppName
shortAppDesc = "A graphical CLI for your Kubernetes cluster management."
longAppDesc = "K9s is a CLI to view and manage your Kubernetes clusters."
)
var _ config.KubeSettings = (*client.Config)(nil)
var _ data.KubeSettings = (*client.Config)(nil)
var (
version, commit, date = "dev", "dev", client.NA
@ -43,6 +45,10 @@ var (
)
func init() {
if err := config.InitLogLoc(); err != nil {
fmt.Printf("Fail to init k9s logs location %s\n", err)
}
rootCmd.AddCommand(versionCmd(), infoCmd())
initK9sFlags()
initK8sFlags()
@ -51,18 +57,21 @@ func init() {
// Execute root command.
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Panic().Err(err)
panic(err)
}
}
func run(cmd *cobra.Command, args []string) error {
if err := config.EnsureDirPath(*k9sFlags.LogFile, config.DefaultDirMod); err != nil {
if err := config.InitLocs(); err != nil {
return err
}
mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY
file, err := os.OpenFile(*k9sFlags.LogFile, mod, config.DefaultFileMod)
file, err := os.OpenFile(
*k9sFlags.LogFile,
os.O_CREATE|os.O_APPEND|os.O_WRONLY,
data.DefaultFileMod,
)
if err != nil {
return err
return fmt.Errorf("Log file %q init failed: %w", *k9sFlags.LogFile, err)
}
defer func() {
if file != nil {
@ -80,8 +89,8 @@ func run(cmd *cobra.Command, args []string) error {
}()
log.Logger = log.Output(zerolog.ConsoleWriter{Out: file})
zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel))
app := view.NewApp(loadConfiguration())
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
return err
@ -99,38 +108,24 @@ func run(cmd *cobra.Command, args []string) error {
func loadConfiguration() *config.Config {
log.Info().Msg("🐶 K9s starting up...")
// Load K9s config file...
k8sCfg := client.NewConfig(k8sFlags)
k9sCfg := config.NewConfig(k8sCfg)
if err := k9sCfg.Load(config.K9sConfigFile); err != nil {
if err := k9sCfg.Load(config.AppConfigFile); err != nil {
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
k9sCfg.K9s.Generate(k9sFlags)
}
if *k9sFlags.RefreshRate != config.DefaultRefreshRate {
k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate)
}
k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless)
k9sCfg.K9s.OverrideLogoless(*k9sFlags.Logoless)
k9sCfg.K9s.OverrideCrumbsless(*k9sFlags.Crumbsless)
k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly)
k9sCfg.K9s.OverrideWrite(*k9sFlags.Write)
k9sCfg.K9s.OverrideCommand(*k9sFlags.Command)
k9sCfg.K9s.OverrideScreenDumpDir(*k9sFlags.ScreenDumpDir)
if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {
log.Error().Err(err).Msgf("refine failed")
}
conn, err := client.InitConnection(k8sCfg)
k9sCfg.SetConnection(conn)
if err != nil {
log.Error().Err(err).Msgf("failed to connect to cluster %q", k9sCfg.K9s.CurrentContext)
log.Error().Err(err).Msgf("failed to connect to context %q", k9sCfg.K9s.ActiveContextName())
return k9sCfg
}
// Try to access server version if that fail. Connectivity issue?
if !k9sCfg.GetConnection().CheckConnectivity() {
log.Panic().Msgf("Cannot connect to cluster %s", k9sCfg.K9s.CurrentCluster)
log.Panic().Msgf("Cannot connect to context %s", k9sCfg.K9s.ActiveContextName())
}
if !k9sCfg.GetConnection().ConnectionOK() {
panic("No connectivity")
@ -177,7 +172,7 @@ func initK9sFlags() {
rootCmd.Flags().StringVarP(
k9sFlags.LogFile,
"logFile", "",
config.DefaultLogFile,
config.AppLogFile,
"Specify the log file",
)
rootCmd.Flags().BoolVar(

34
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/anchore/grype v0.73.4
github.com/atotto/clipboard v0.1.4
github.com/cenkalti/backoff/v4 v4.2.1
github.com/derailed/popeye v0.11.1
github.com/derailed/popeye v0.11.2
github.com/derailed/tcell/v2 v2.3.1-rc.3
github.com/derailed/tview v0.8.2
github.com/fatih/color v1.16.0
@ -27,15 +27,15 @@ require (
github.com/stretchr/testify v1.8.4
golang.org/x/text v0.14.0
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.13.3
k8s.io/api v0.28.4
k8s.io/apiextensions-apiserver v0.28.4
k8s.io/apimachinery v0.28.4
k8s.io/cli-runtime v0.28.4
k8s.io/client-go v0.28.4
helm.sh/helm/v3 v3.13.2
k8s.io/api v0.29.0
k8s.io/apiextensions-apiserver v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/cli-runtime v0.29.0
k8s.io/client-go v0.29.0
k8s.io/klog/v2 v2.110.1
k8s.io/kubectl v0.28.4
k8s.io/metrics v0.28.4
k8s.io/kubectl v0.29.0
k8s.io/metrics v0.29.0
sigs.k8s.io/yaml v1.4.0
)
@ -109,7 +109,7 @@ require (
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
@ -153,6 +153,7 @@ require (
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect
@ -216,9 +217,10 @@ require (
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/gomega v1.27.10 // indirect
github.com/onsi/gomega v1.29.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
@ -310,10 +312,10 @@ require (
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/gorm v1.25.5 // indirect
k8s.io/apiserver v0.28.4 // indirect
k8s.io/component-base v0.28.4 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
k8s.io/apiserver v0.29.0 // indirect
k8s.io/component-base v0.29.0 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
modernc.org/libc v1.29.0 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect
@ -322,5 +324,5 @@ require (
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

76
go.sum
View File

@ -384,8 +384,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M=
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=
github.com/derailed/popeye v0.11.1 h1:bjt5mXkcXY696ipuJqwY1sa5s3i431L9BlkQc6EuaqE=
github.com/derailed/popeye v0.11.1/go.mod h1:NkvjHH1F94tE7Ui17PlYiagQcFt7yXUV2hIhPzSK+0w=
github.com/derailed/popeye v0.11.2 h1:8MKMjYBJdYNktTKeh98TeT127jZY6CFAsurrENoTZCY=
github.com/derailed/popeye v0.11.2/go.mod h1:HygqX7A8BwidorJjJUnWDZ5AvbxHIU7uRwXgOtn9GwY=
github.com/derailed/tcell/v2 v2.3.1-rc.3 h1:9s1fmyRcSPRlwr/C9tcpJKCujbrtmPpST6dcMUD2piY=
github.com/derailed/tcell/v2 v2.3.1-rc.3/go.mod h1:nf68BEL8fjmXQHJT3xZjoZFs2uXOzyJcNAQqGUEMrFY=
github.com/derailed/tview v0.8.2 h1:8b+QwVECV1lZ6VV7Vf1tergpJxJ+ReA/JhIBYyUVSFI=
@ -423,8 +423,8 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ=
github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -670,6 +670,8 @@ github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
@ -918,6 +920,8 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@ -926,10 +930,10 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
@ -1204,8 +1208,8 @@ go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93V
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
@ -1834,8 +1838,8 @@ gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
helm.sh/helm/v3 v3.13.3 h1:0zPEdGqHcubehJHP9emCtzRmu8oYsJFRrlVF3TFj8xY=
helm.sh/helm/v3 v3.13.3/go.mod h1:3OKO33yI3p4YEXtTITN2+4oScsHeQe71KuzhlZ+aPfg=
helm.sh/helm/v3 v3.13.2 h1:IcO9NgmmpetJODLZhR3f3q+6zzyXVKlRizKFwbi7K8w=
helm.sh/helm/v3 v3.13.2/go.mod h1:GIHDwZggaTGbedevTlrQ6DB++LBN6yuQdeGj0HNaDx0=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -1843,30 +1847,30 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY=
k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0=
k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU=
k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM=
k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8=
k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg=
k8s.io/apiserver v0.28.4 h1:BJXlaQbAU/RXYX2lRz+E1oPe3G3TKlozMMCZWu5GMgg=
k8s.io/apiserver v0.28.4/go.mod h1:Idq71oXugKZoVGUUL2wgBCTHbUR+FYTWa4rq9j4n23w=
k8s.io/cli-runtime v0.28.4 h1:IW3aqSNFXiGDllJF4KVYM90YX4cXPGxuCxCVqCD8X+Q=
k8s.io/cli-runtime v0.28.4/go.mod h1:MLGRB7LWTIYyYR3d/DOgtUC8ihsAPA3P8K8FDNIqJ0k=
k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY=
k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4=
k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo=
k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU=
k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A=
k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA=
k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0=
k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc=
k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o=
k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis=
k8s.io/apiserver v0.29.0 h1:Y1xEMjJkP+BIi0GSEv1BBrf1jLU9UPfAnnGGbbDdp7o=
k8s.io/apiserver v0.29.0/go.mod h1:31n78PsRKPmfpee7/l9NYEv67u6hOL6AfcE761HapDM=
k8s.io/cli-runtime v0.29.0 h1:q2kC3cex4rOBLfPOnMSzV2BIrrQlx97gxHJs21KxKS4=
k8s.io/cli-runtime v0.29.0/go.mod h1:VKudXp3X7wR45L+nER85YUzOQIru28HQpXr0mTdeCrk=
k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8=
k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38=
k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s=
k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ=
k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c=
k8s.io/metrics v0.28.4 h1:u36fom9+6c8jX2sk8z58H0hFaIUfrPWbXIxN7GT2blk=
k8s.io/metrics v0.28.4/go.mod h1:bBqAJxH20c7wAsTQxDXOlVqxGMdce49d7WNr1WeaLac=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI=
k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs=
k8s.io/metrics v0.29.0 h1:a6dWcNM+EEowMzMZ8trka6wZtSRIfEA/9oLjuhBksGc=
k8s.io/metrics v0.29.0/go.mod h1:UCuTT4dC/x/x6ODSk87IWIZQnuAfcwxOjb1gjWJdjMA=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
@ -1886,8 +1890,8 @@ sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKU
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY=
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U=
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@ -14,7 +14,6 @@ import (
"github.com/rs/zerolog/log"
authorizationv1 "k8s.io/api/authorization/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/cache"
"k8s.io/apimachinery/pkg/version"
@ -35,8 +34,8 @@ const (
var supportedMetricsAPIVersions = []string{"v1beta1"}
// Namespaces tracks a collection of namespace names.
type Namespaces map[string]struct{}
// NamespaceNames tracks a collection of namespace names.
type NamespaceNames map[string]struct{}
// APIClient represents a Kubernetes api client.
type APIClient struct {
@ -86,7 +85,7 @@ func (a *APIClient) ConnectionOK() bool {
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
if ns == ClusterScope {
ns = AllNamespaces
ns = BlankNamespace
}
spec := NewGVR(gvr)
res := spec.GVR()
@ -107,9 +106,9 @@ func makeCacheKey(ns, gvr string, vv []string) string {
return ns + ":" + gvr + "::" + strings.Join(vv, ",")
}
// ActiveCluster returns the current cluster name.
func (a *APIClient) ActiveCluster() string {
c, err := a.config.CurrentClusterName()
// ActiveContext returns the current context name.
func (a *APIClient) ActiveContext() string {
c, err := a.config.CurrentContextName()
if err != nil {
log.Error().Msgf("Unable to located active cluster")
return ""
@ -119,9 +118,10 @@ func (a *APIClient) ActiveCluster() string {
// IsActiveNamespace returns true if namespaces matches.
func (a *APIClient) IsActiveNamespace(ns string) bool {
if a.ActiveNamespace() == AllNamespaces {
if a.ActiveNamespace() == BlankNamespace {
return true
}
return a.ActiveNamespace() == ns
}
@ -131,7 +131,7 @@ func (a *APIClient) ActiveNamespace() string {
return ns
}
return AllNamespaces
return BlankNamespace
}
func (a *APIClient) clearCache() {
@ -149,7 +149,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error)
return false, errors.New("ACCESS -- No API server connection")
}
if IsClusterWide(ns) {
ns = AllNamespaces
ns = BlankNamespace
}
key := makeCacheKey(ns, gvr, verbs)
if v, ok := a.cache.Get(key); ok {
@ -212,14 +212,27 @@ func (a *APIClient) ServerVersion() (*version.Info, error) {
return info, nil
}
// ValidNamespaces returns all available namespaces.
func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
func (a *APIClient) IsValidNamespace(ns string) bool {
if IsAllNamespace(ns) {
return true
}
nn, err := a.ValidNamespaceNames()
if err != nil {
return false
}
_, ok := nn[ns]
return ok
}
// ValidNamespaceNames returns all available namespaces.
func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
if a == nil {
return nil, fmt.Errorf("validNamespaces: no available client found")
}
if nn, ok := a.cache.Get("validNamespaces"); ok {
if nss, ok := nn.([]v1.Namespace); ok {
if nss, ok := nn.(NamespaceNames); ok {
return nss, nil
}
}
@ -233,9 +246,13 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
if err != nil {
return nil, err
}
a.cache.Add("validNamespaces", nn.Items, cacheExpiry)
nns := make(NamespaceNames, len(nn.Items))
for _, n := range nn.Items {
nns[n.Name] = struct{}{}
}
a.cache.Add("validNamespaces", nns, cacheExpiry)
return nn.Items, nil
return nns, nil
}
// CheckConnectivity return true if api server is cool or false otherwise.

View File

@ -10,15 +10,14 @@ import (
"sync"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
restclient "k8s.io/client-go/rest"
clientcmd "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/tools/clientcmd/api"
)
const (
defaultCallTimeoutDuration time.Duration = 10 * time.Second
defaultCallTimeoutDuration time.Duration = 15 * time.Second
// UsePersistentConfig caches client config to avoid reloads.
UsePersistentConfig = true
@ -60,7 +59,7 @@ func (c *Config) Flags() *genericclioptions.ConfigFlags {
return c.flags
}
func (c *Config) RawConfig() (clientcmdapi.Config, error) {
func (c *Config) RawConfig() (api.Config, error) {
return c.clientConfig().RawConfig()
}
@ -72,11 +71,14 @@ func (c *Config) reset() {}
// SwitchContext changes the kubeconfig context to a new cluster.
func (c *Config) SwitchContext(name string) error {
if _, err := c.GetContext(name); err != nil {
ct, err := c.GetContext(name)
if err != nil {
return fmt.Errorf("context %q does not exist", name)
}
// !!BOZO!! Do you need to reset the flags?
flags := genericclioptions.NewConfigFlags(UsePersistentConfig)
flags.Context = &name
flags.Context, flags.ClusterName = &name, &ct.Cluster
flags.Namespace = c.flags.Namespace
flags.Timeout = c.flags.Timeout
flags.KubeConfig = c.flags.KubeConfig
c.flags = flags
@ -84,6 +86,22 @@ func (c *Config) SwitchContext(name string) error {
return nil
}
// CurrentClusterName returns the currently active cluster name.
func (c *Config) CurrentClusterName() (string, error) {
if isSet(c.flags.ClusterName) {
return *c.flags.ClusterName, nil
}
cfg, err := c.RawConfig()
if err != nil {
return "", err
}
ct := cfg.Contexts[cfg.CurrentContext]
return ct.Cluster, nil
}
// CurrentContextName returns the currently active config context.
func (c *Config) CurrentContextName() (string, error) {
if isSet(c.flags.Context) {
@ -110,8 +128,17 @@ func (c *Config) CurrentContextNamespace() (string, error) {
return context.Namespace, nil
}
// CurrentContext returns the current context configuration.
func (c *Config) CurrentContext() (*api.Context, error) {
n, err := c.CurrentContextName()
if err != nil {
return nil, err
}
return c.GetContext(n)
}
// GetContext fetch a given context or error if it does not exists.
func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) {
func (c *Config) GetContext(n string) (*api.Context, error) {
cfg, err := c.RawConfig()
if err != nil {
return nil, err
@ -124,7 +151,7 @@ func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) {
}
// Contexts fetch all available contexts.
func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) {
func (c *Config) Contexts() (map[string]*api.Context, error) {
cfg, err := c.RawConfig()
if err != nil {
return nil, err
@ -180,63 +207,14 @@ func (c *Config) RenameContext(old string, new string) error {
}
// ContextNames fetch all available contexts.
func (c *Config) ContextNames() ([]string, error) {
func (c *Config) ContextNames() (map[string]struct{}, error) {
cfg, err := c.RawConfig()
if err != nil {
return nil, err
}
cc := make([]string, 0, len(cfg.Contexts))
cc := make(map[string]struct{}, len(cfg.Contexts))
for n := range cfg.Contexts {
cc = append(cc, n)
}
return cc, nil
}
// ClusterNameFromContext returns the cluster associated with the given context.
func (c *Config) ClusterNameFromContext(context string) (string, error) {
cfg, err := c.RawConfig()
if err != nil {
return "", err
}
if ctx, ok := cfg.Contexts[context]; ok {
return ctx.Cluster, nil
}
return "", fmt.Errorf("unable to locate cluster from context %s", context)
}
// CurrentClusterName returns the active cluster name.
func (c *Config) CurrentClusterName() (string, error) {
if isSet(c.flags.ClusterName) {
return *c.flags.ClusterName, nil
}
cfg, err := c.RawConfig()
if err != nil {
return "", err
}
context, err := c.CurrentContextName()
if err != nil {
context = cfg.CurrentContext
}
if ctx, ok := cfg.Contexts[context]; ok {
return ctx.Cluster, nil
}
return "", errors.New("unable to locate current cluster")
}
// ClusterNames fetch all kubeconfig defined clusters.
func (c *Config) ClusterNames() (map[string]struct{}, error) {
cfg, err := c.RawConfig()
if err != nil {
return nil, err
}
cc := make(map[string]struct{}, len(cfg.Clusters))
for name := range cfg.Clusters {
cc[name] = struct{}{}
cc[n] = struct{}{}
}
return cc, nil
@ -297,16 +275,17 @@ func (c *Config) CurrentUserName() (string, error) {
// CurrentNamespaceName retrieves the active namespace.
func (c *Config) CurrentNamespaceName() (string, error) {
ns, _, err := c.clientConfig().Namespace()
if ns == "default" {
ns, err = c.CurrentContextNamespace()
if ns == "" && err == nil {
return "", errors.New("No namespace specified in context")
}
ns, overridden, err := c.clientConfig().Namespace()
if err != nil {
return BlankNamespace, err
}
// Checks if ns is passed is in args.
if overridden {
return ns, nil
}
return ns, err
// Return ns set in context if any??
return c.CurrentContextNamespace()
}
// ConfigAccess return the current kubeconfig api server access configuration.
@ -320,16 +299,6 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) {
// ----------------------------------------------------------------------------
// Helpers...
// NamespaceNames fetch all available namespaces on current cluster.
func NamespaceNames(nns []v1.Namespace) []string {
nn := make([]string, 0, len(nns))
for _, ns := range nns {
nn = append(nn, ns.Name)
}
return nn
}
func isSet(s *string) bool {
return s != nil && len(*s) != 0
}

View File

@ -5,13 +5,12 @@ package client_test
import (
"errors"
"os"
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
@ -55,15 +54,15 @@ func TestConfigCurrentCluster(t *testing.T) {
name, kubeConfig := "blee", "./testdata/config"
uu := map[string]struct {
flags *genericclioptions.ConfigFlags
cluster string
context string
}{
"default": {
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},
cluster: "fred",
context: "fred",
},
"custom": {
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name},
cluster: "blee",
flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &name},
context: "blee",
},
}
@ -71,9 +70,9 @@ func TestConfigCurrentCluster(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
cfg := client.NewConfig(u.flags)
ctx, err := cfg.CurrentClusterName()
ct, err := cfg.CurrentContextName()
assert.Nil(t, err)
assert.Equal(t, u.cluster, ctx)
assert.Equal(t, u.context, ct)
})
}
}
@ -173,8 +172,8 @@ func TestConfigGetContext(t *testing.T) {
func TestConfigSwitchContext(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
ClusterName: &cluster,
KubeConfig: &kubeConfig,
Context: &cluster,
}
cfg := client.NewConfig(&flags)
@ -185,24 +184,11 @@ func TestConfigSwitchContext(t *testing.T) {
assert.Equal(t, "blee", ctx)
}
func TestConfigClusterNameFromContext(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
ClusterName: &cluster,
}
cfg := client.NewConfig(&flags)
cl, err := cfg.ClusterNameFromContext("blee")
assert.Nil(t, err)
assert.Equal(t, "blee", cl)
}
func TestConfigAccess(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config"
context, kubeConfig := "duh", "./testdata/config"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
ClusterName: &cluster,
KubeConfig: &kubeConfig,
Context: &context,
}
cfg := client.NewConfig(&flags)
@ -211,11 +197,24 @@ func TestConfigAccess(t *testing.T) {
assert.True(t, len(acc.GetDefaultFilename()) > 0)
}
func TestConfigContexts(t *testing.T) {
func TestConfigContextNames(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
ClusterName: &cluster,
KubeConfig: &kubeConfig,
Context: &cluster,
}
cfg := client.NewConfig(&flags)
cc, err := cfg.ContextNames()
assert.Nil(t, err)
assert.Equal(t, 3, len(cc))
}
func TestConfigContexts(t *testing.T) {
context, kubeConfig := "duh", "./testdata/config"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
Context: &context,
}
cfg := client.NewConfig(&flags)
@ -224,46 +223,24 @@ func TestConfigContexts(t *testing.T) {
assert.Equal(t, 3, len(cc))
}
func TestConfigContextNames(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
ClusterName: &cluster,
}
cfg := client.NewConfig(&flags)
cc, err := cfg.ContextNames()
assert.Nil(t, err)
assert.Equal(t, 3, len(cc))
}
func TestConfigClusterNames(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
ClusterName: &cluster,
}
cfg := client.NewConfig(&flags)
cc, err := cfg.ClusterNames()
assert.Nil(t, err)
assert.Equal(t, 3, len(cc))
}
func TestConfigDelContext(t *testing.T) {
cluster, kubeConfig := "duh", "./testdata/config.1"
assert.NoError(t, cp("./testdata/config.2", "./testdata/config.1"))
context, kubeConfig := "duh", "./testdata/config.1"
flags := genericclioptions.ConfigFlags{
KubeConfig: &kubeConfig,
ClusterName: &cluster,
KubeConfig: &kubeConfig,
Context: &context,
}
cfg := client.NewConfig(&flags)
err := cfg.DelContext("fred")
assert.Nil(t, err)
assert.NoError(t, err)
cc, err := cfg.ContextNames()
assert.Nil(t, err)
assert.NoError(t, err)
assert.Equal(t, 1, len(cc))
assert.Equal(t, "blee", cc[0])
_, ok := cc["blee"]
assert.True(t, ok)
}
func TestConfigRestConfig(t *testing.T) {
@ -289,13 +266,13 @@ func TestConfigBadConfig(t *testing.T) {
assert.NotNil(t, err)
}
func TestNamespaceNames(t *testing.T) {
nn := []v1.Namespace{
{ObjectMeta: metav1.ObjectMeta{Name: "ns1"}},
{ObjectMeta: metav1.ObjectMeta{Name: "ns2"}},
// Helpers...
func cp(src string, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
nns := client.NamespaceNames(nn)
assert.Equal(t, 2, len(nns))
assert.Equal(t, []string{"ns1", "ns2"}, nns)
return os.WriteFile(dst, data, 0600)
}

View File

@ -14,6 +14,8 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
var NoGVR = GVR{}
// GVR represents a kubernetes resource schema as a string.
// Format is group/version/resources:subresource.
type GVR struct {

View File

@ -49,7 +49,7 @@ func TestGVRCan(t *testing.T) {
}
}
func TestAsGVR(t *testing.T) {
func TestGVR(t *testing.T) {
uu := map[string]struct {
gvr string
e schema.GroupVersionResource

View File

@ -17,13 +17,13 @@ var toFileName = regexp.MustCompile(`[^(\w/\.)]`)
// IsClusterWide returns true if ns designates cluster scope, false otherwise.
func IsClusterWide(ns string) bool {
return ns == NamespaceAll || ns == AllNamespaces || ns == ClusterScope
return ns == NamespaceAll || ns == BlankNamespace || ns == ClusterScope
}
// CleanseNamespace ensures all ns maps to blank.
func CleanseNamespace(ns string) string {
if IsAllNamespace(ns) {
return AllNamespaces
return BlankNamespace
}
return ns
@ -36,7 +36,7 @@ func IsAllNamespace(ns string) bool {
// IsAllNamespaces returns true if all namespaces, false otherwise.
func IsAllNamespaces(ns string) bool {
return ns == NamespaceAll || ns == AllNamespaces
return ns == NamespaceAll || ns == BlankNamespace
}
// IsNamespaced returns true if a specific ns is given.

View File

@ -20,6 +20,8 @@ import (
const (
mxCacheSize = 100
mxCacheExpiry = 1 * time.Minute
podMXGVR = "metrics.k8s.io/v1beta1/pods"
nodeMXGVR = "metrics.k8s.io/v1beta1/nodes"
)
// MetricsDial tracks global metric server handle.
@ -149,7 +151,7 @@ func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMe
const msg = "user is not authorized to list node metrics"
mx := new(mv1beta1.NodeMetricsList)
if err := m.checkAccess(ClusterScope, "metrics.k8s.io/v1beta1/nodes", msg); err != nil {
if err := m.checkAccess(ClusterScope, nodeMXGVR, msg); err != nil {
return mx, err
}
@ -180,7 +182,7 @@ func (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1bet
const msg = "user is not authorized to list node metrics"
mx := new(mv1beta1.NodeMetrics)
if err := m.checkAccess(ClusterScope, "metrics.k8s.io/v1beta1/nodes", msg); err != nil {
if err := m.checkAccess(ClusterScope, nodeMXGVR, msg); err != nil {
return mx, err
}
@ -218,9 +220,9 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be
const msg = "user is not authorized to list pods metrics"
if ns == NamespaceAll {
ns = AllNamespaces
ns = BlankNamespace
}
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
if err := m.checkAccess(ns, podMXGVR, msg); err != nil {
return mx, err
}
@ -269,9 +271,9 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be
ns, _ := Namespaced(fqn)
if ns == NamespaceAll {
ns = AllNamespaces
ns = BlankNamespace
}
if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil {
if err := m.checkAccess(ns, podMXGVR, msg); err != nil {
return mx, err
}

23
internal/client/testdata/config.2 vendored Normal file
View File

@ -0,0 +1,23 @@
apiVersion: v1
clusters:
- cluster:
insecure-skip-tls-verify: true
server: https://localhost:3001
name: blee
- cluster:
insecure-skip-tls-verify: true
server: https://localhost:3002
name: fred
contexts:
- context:
cluster: blee
user: blee
name: blee
- context:
cluster: fred
user: fred
name: fred
current-context: blee
kind: Config
preferences: {}
users: null

View File

@ -4,7 +4,6 @@
package client
import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/discovery/cached/disk"
"k8s.io/client-go/dynamic"
@ -21,8 +20,8 @@ const (
// NamespaceAll designates the fictional all namespace.
NamespaceAll = "all"
// AllNamespaces designates all namespaces.
AllNamespaces = ""
// BlankNamespace designates no namespace.
BlankNamespace = ""
// DefaultNamespace designates the default namespace.
DefaultNamespace = "default"
@ -118,8 +117,11 @@ type Connection interface {
// HasMetrics checks if metrics server is available.
HasMetrics() bool
// ValidNamespaces returns all available namespaces.
ValidNamespaces() ([]v1.Namespace, error)
// ValidNamespaces returns all available namespace names.
ValidNamespaceNames() (NamespaceNames, error)
// IsValidNamespace checks if given namespace is known.
IsValidNamespace(string) bool
// ServerVersion returns current server version.
ServerVersion() (*version.Info, error)
@ -127,8 +129,8 @@ type Connection interface {
// CheckConnectivity checks if api server connection is happy or not.
CheckConnectivity() bool
// ActiveCluster returns the current cluster name.
ActiveCluster() string
// ActiveContext returns the current context name.
ActiveContext() string
// ActiveNamespace returns the current namespace.
ActiveNamespace() string

View File

@ -5,16 +5,13 @@ package config
import (
"os"
"path/filepath"
"sync"
"github.com/derailed/k9s/internal/config/data"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
)
// K9sAlias manages K9s aliases.
var K9sAlias = YamlExtension(filepath.Join(K9sHome(), "alias.yml"))
// Alias tracks shortname to GVR mappings.
type Alias map[string]string
@ -23,7 +20,7 @@ type ShortNames map[string][]string
// Aliases represents a collection of aliases.
type Aliases struct {
Alias Alias `yaml:"alias"`
Alias Alias `yaml:"aliases"`
mx sync.RWMutex
}
@ -101,13 +98,28 @@ func (a *Aliases) Define(gvr string, aliases ...string) {
}
// Load K9s aliases.
func (a *Aliases) Load() error {
func (a *Aliases) Load(path string) error {
a.loadDefaultAliases()
return a.LoadFileAliases(K9sAlias)
f, err := EnsureAliasesCfgFile()
if err != nil {
log.Error().Err(err).Msgf("Unable to gen config aliases")
}
// load global alias file
if err := a.LoadFile(f); err != nil {
return err
}
// load context specific aliases if any
return a.LoadFile(path)
}
// LoadFileAliases loads alias from a given file.
func (a *Aliases) LoadFileAliases(path string) error {
// LoadFile loads alias from a given file.
func (a *Aliases) LoadFile(path string) error {
if path == "" {
return nil
}
f, err := os.ReadFile(path)
if err == nil {
var aa Aliases
@ -136,15 +148,6 @@ func (a *Aliases) loadDefaultAliases() {
a.mx.Lock()
defer a.mx.Unlock()
a.Alias["dp"] = "apps/v1/deployments"
a.Alias["sec"] = "v1/secrets"
a.Alias["jo"] = "batch/v1/jobs"
a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles"
a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings"
a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles"
a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings"
a.Alias["np"] = "networking.k8s.io/v1/networkpolicies"
a.declare("help", "h", "?")
a.declare("quit", "q", "q!", "qa", "Q")
a.declare("aliases", "alias", "a")
@ -155,21 +158,22 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("users", "user", "usr")
a.declare("groups", "group", "grp")
a.declare("portforwards", "portforward", "pf")
a.declare("benchmarks", "bench", "benchmark", "be")
a.declare("benchmarks", "benchmark", "bench")
a.declare("screendumps", "screendump", "sd")
a.declare("pulses", "pulse", "pu", "hz")
a.declare("xrays", "xray", "x")
a.declare("workloads", "workload", "wk")
}
// Save alias to disk.
func (a *Aliases) Save() error {
log.Debug().Msg("[Config] Saving Aliases...")
return a.SaveAliases(K9sAlias)
return a.SaveAliases(AppAliasesFile)
}
// SaveAliases saves aliases to a given file.
func (a *Aliases) SaveAliases(path string) error {
if err := EnsureDirPath(path, DefaultDirMod); err != nil {
if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil {
return err
}
cfg, err := yaml.Marshal(a)

View File

@ -75,7 +75,7 @@ func TestAliasDefine(t *testing.T) {
func TestAliasesLoad(t *testing.T) {
a := config.NewAliases()
assert.Nil(t, a.LoadFileAliases("testdata/alias.yml"))
assert.Nil(t, a.LoadFile("testdata/alias.yaml"))
assert.Equal(t, 2, len(a.Alias))
}
@ -84,7 +84,7 @@ func TestAliasesSave(t *testing.T) {
a.Alias["test"] = "fred"
a.Alias["blee"] = "duh"
assert.Nil(t, a.SaveAliases("/tmp/a.yml"))
assert.Nil(t, a.LoadFileAliases("/tmp/a.yml"))
assert.Nil(t, a.SaveAliases("/tmp/a.yaml"))
assert.Nil(t, a.LoadFile("/tmp/a.yaml"))
assert.Equal(t, 2, len(a.Alias))
}

View File

@ -67,6 +67,18 @@ const (
DefaultMethod = "GET"
)
// DefaultBenchSpec returns a default bench spec.
func DefaultBenchSpec() BenchConfig {
return BenchConfig{
C: DefaultC,
N: DefaultN,
HTTP: HTTP{
Method: DefaultMethod,
Path: "/",
},
}
}
func newBenchmark() Benchmark {
return Benchmark{
C: DefaultC,
@ -106,15 +118,3 @@ func (s *Bench) load(path string) error {
return yaml.Unmarshal(f, &s)
}
// DefaultBenchSpec returns a default bench spec.
func DefaultBenchSpec() BenchConfig {
return BenchConfig{
C: DefaultC,
N: DefaultN,
HTTP: HTTP{
Method: DefaultMethod,
Path: "/",
},
}
}

View File

@ -35,14 +35,14 @@ func TestBenchLoad(t *testing.T) {
coCount int
}{
"goodConfig": {
"testdata/b_good.yml",
"testdata/b_good.yaml",
2,
1000,
2,
0,
},
"malformed": {
"testdata/b_toast.yml",
"testdata/b_toast.yaml",
1,
200,
0,
@ -103,7 +103,7 @@ func TestBenchServiceLoad(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
b, err := NewBench("testdata/b_good.yml")
b, err := NewBench("testdata/b_good.yaml")
assert.Nil(t, err)
assert.Equal(t, 2, len(b.Benchmarks.Services))
@ -122,16 +122,16 @@ func TestBenchServiceLoad(t *testing.T) {
}
func TestBenchReLoad(t *testing.T) {
b, err := NewBench("testdata/b_containers.yml")
b, err := NewBench("testdata/b_containers.yaml")
assert.Nil(t, err)
assert.Equal(t, 2, b.Benchmarks.Defaults.C)
assert.Nil(t, b.Reload("testdata/b_containers_1.yml"))
assert.Nil(t, b.Reload("testdata/b_containers_1.yaml"))
assert.Equal(t, 20, b.Benchmarks.Defaults.C)
}
func TestBenchLoadToast(t *testing.T) {
_, err := NewBench("testdata/toast.yml")
_, err := NewBench("testdata/toast.yaml")
assert.NotNil(t, err)
}
@ -174,7 +174,7 @@ func TestBenchContainerLoad(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
b, err := NewBench("testdata/b_containers.yml")
b, err := NewBench("testdata/b_containers.yaml")
assert.Nil(t, err)
assert.Equal(t, 2, len(b.Benchmarks.Services))

View File

@ -1,51 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config
import "github.com/derailed/k9s/internal/client"
// DefaultPFAddress specifies the default PortForward host address.
const DefaultPFAddress = "localhost"
// Cluster tracks K9s cluster configuration.
type Cluster struct {
Namespace *Namespace `yaml:"namespace"`
View *View `yaml:"view"`
Skin string `yaml:"skin,omitempty"`
FeatureGates *FeatureGates `yaml:"featureGates"`
PortForwardAddress string `yaml:"portForwardAddress"`
}
// NewCluster creates a new cluster configuration.
func NewCluster() *Cluster {
return &Cluster{
Namespace: NewNamespace(),
View: NewView(),
PortForwardAddress: DefaultPFAddress,
FeatureGates: NewFeatureGates(),
}
}
// Validate a cluster config.
func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) {
if c.PortForwardAddress == "" {
c.PortForwardAddress = DefaultPFAddress
}
if c.Namespace == nil {
c.Namespace = NewNamespace()
}
if c.Namespace.Active == client.AllNamespaces {
c.Namespace.Active = client.NamespaceAll
}
if c.FeatureGates == nil {
c.FeatureGates = NewFeatureGates()
}
if c.View == nil {
c.View = NewView()
}
c.View.Validate()
}

View File

@ -1,61 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestClusterValidate(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings()
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"})
c := config.NewCluster()
c.Validate(mc, mk)
assert.Equal(t, "po", c.View.Active)
assert.Equal(t, "default", c.Namespace.Active)
assert.Equal(t, 1, len(c.Namespace.Favorites))
assert.Equal(t, []string{"default"}, c.Namespace.Favorites)
}
func TestClusterValidateEmpty(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings()
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"})
var c config.Cluster
c.Validate(mc, mk)
assert.Equal(t, "po", c.View.Active)
assert.Equal(t, "default", c.Namespace.Active)
assert.Equal(t, 1, len(c.Namespace.Favorites))
assert.Equal(t, []string{"default"}, c.Namespace.Favorites)
}
func namespaces() []v1.Namespace {
return []v1.Namespace{
{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "fred",
},
},
}
}

View File

@ -4,7 +4,6 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
@ -12,56 +11,26 @@ import (
"github.com/adrg/xdg"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
// K9sConfig represents K9s configuration dir env var.
const K9sConfig = "K9SCONFIG"
var (
// K9sConfigFile represents K9s config file location.
K9sConfigFile = filepath.Join(K9sHome(), "config.yml")
// K9sSkinDir represent K9s skin dir
K9sSkinDir = filepath.Join(K9sHome(), "skins")
// K9sDefaultScreenDumpDir represents a default directory where K9s screen dumps will be persisted.
K9sDefaultScreenDumpDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-screens-%s", MustK9sUser()))
)
type (
// KubeSettings exposes kubeconfig context information.
KubeSettings interface {
// CurrentContextName returns the name of the current context.
CurrentContextName() (string, error)
// CurrentClusterName returns the name of the current cluster.
CurrentClusterName() (string, error)
// CurrentNamespace returns the name of the current namespace.
CurrentNamespaceName() (string, error)
// ClusterNames() returns all available cluster names.
ClusterNames() (map[string]struct{}, error)
}
// Config tracks K9s configuration options.
Config struct {
K9s *K9s `yaml:"k9s"`
client client.Connection
settings KubeSettings
}
)
// Config tracks K9s configuration options.
type Config struct {
K9s *K9s `yaml:"k9s"`
conn client.Connection
settings data.KubeSettings
}
// K9sHome returns k9s configs home directory.
func K9sHome() string {
if env := os.Getenv(K9sConfig); env != "" {
if env := os.Getenv(K9sConfigDir); env != "" {
return env
}
xdgK9sHome, err := xdg.ConfigFile("k9s")
xdgK9sHome, err := xdg.ConfigFile(AppName)
if err != nil {
log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s")
}
@ -70,35 +39,50 @@ func K9sHome() string {
}
// NewConfig creates a new default config.
func NewConfig(ks KubeSettings) *Config {
return &Config{K9s: NewK9s(), settings: ks}
func NewConfig(ks data.KubeSettings) *Config {
return &Config{
settings: ks,
K9s: NewK9s(nil, ks),
}
}
// ContextAliasesPath returns a context specific aliases file spec.
func (c *Config) ContextAliasesPath() string {
ct, err := c.K9s.ActiveContext()
if err != nil {
return ""
}
return AppContextAliasesFile(ct.ClusterName, c.K9s.activeContextName)
}
// ContextPluginsPath returns a context specific plugins file spec.
func (c *Config) ContextPluginsPath() string {
ct, err := c.K9s.ActiveContext()
if err != nil {
return ""
}
return AppContextPluginsFile(ct.ClusterName, c.K9s.activeContextName)
}
// Refine the configuration based on cli args.
func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error {
if isSet(flags.Context) {
c.K9s.CurrentContext = *flags.Context
if _, err := c.K9s.ActivateContext(*flags.Context); err != nil {
return err
}
} else {
context, err := cfg.CurrentContextName()
n, err := cfg.CurrentContextName()
if err != nil {
return err
}
_, err = c.K9s.ActivateContext(n)
if err != nil {
return err
}
c.K9s.CurrentContext = context
}
log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext)
if c.K9s.CurrentContext == "" {
return errors.New("Invalid kubeconfig context detected")
}
cc, err := cfg.Contexts()
if err != nil {
return err
}
context, ok := cc[c.K9s.CurrentContext]
if !ok {
return fmt.Errorf("the specified context %q does not exists in kubeconfig", c.K9s.CurrentContext)
}
c.K9s.CurrentCluster = context.Cluster
c.K9s.ActivateCluster(context.Namespace)
log.Debug().Msgf("Active Context %q", c.K9s.ActiveContextName())
var ns = client.DefaultNamespace
switch {
@ -107,96 +91,87 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
case isSet(flags.Namespace):
ns = *flags.Namespace
default:
if nss := context.Namespace; nss != "" {
ns = nss
} else if nss == "" {
ns = c.K9s.ActiveCluster().Namespace.Active
nss, err := c.K9s.ActiveContextNamespace()
if err != nil {
return err
}
ns = nss
}
if err := c.SetActiveNamespace(ns); err != nil {
return err
}
flags.Namespace = &ns
if isSet(flags.ClusterName) {
c.K9s.CurrentCluster = *flags.ClusterName
}
return EnsureDirPath(c.K9s.GetScreenDumpDir(), DefaultDirMod)
return data.EnsureDirPath(c.K9s.GetScreenDumpDir(), data.DefaultDirMod)
}
// Reset the context to the new current context/cluster.
// if it does not exist.
// Reset resets the context to the new current context/cluster.
func (c *Config) Reset() {
c.K9s.CurrentContext, c.K9s.CurrentCluster = "", ""
c.K9s.Reset()
}
// CurrentCluster fetch the configuration activeCluster.
func (c *Config) CurrentCluster() *Cluster {
if c, ok := c.K9s.Clusters[c.K9s.CurrentCluster]; ok {
return c
func (c *Config) SetCurrentContext(n string) (*data.Context, error) {
ct, err := c.K9s.ActivateContext(n)
if err != nil {
return nil, fmt.Errorf("set current context %q failed: %w", n, err)
}
return nil
return ct, nil
}
// ActiveNamespace returns the active namespace in the current cluster.
// CurrentContext fetch the configuration active context.
func (c *Config) CurrentContext() (*data.Context, error) {
return c.K9s.ActiveContext()
}
// ActiveNamespace returns the active namespace in the current context.
// If none found return the empty ns.
func (c *Config) ActiveNamespace() string {
if c.K9s.Clusters == nil {
log.Warn().Msgf("No context detected returning default namespace")
return "default"
}
cl := c.CurrentCluster()
if cl != nil && cl.Namespace != nil {
return cl.Namespace.Active
}
if cl == nil {
cl = NewCluster()
c.K9s.Clusters[c.K9s.CurrentCluster] = cl
}
if ns, err := c.settings.CurrentNamespaceName(); err == nil && ns != "" {
if cl.Namespace == nil {
cl.Namespace = NewNamespace()
}
cl.Namespace.Active = ns
return ns
ns, err := c.K9s.ActiveContextNamespace()
if err != nil {
log.Error().Err(err).Msgf("Unable to assert active namespace. Using default")
ns = client.DefaultNamespace
}
return "default"
return ns
}
// ValidateFavorites ensure favorite ns are legit.
func (c *Config) ValidateFavorites() {
cl := c.K9s.ActiveCluster()
cl.Validate(c.client, c.settings)
cl.Namespace.Validate(c.client, c.settings)
ct, err := c.K9s.ActiveContext()
if err == nil {
ct.Validate(c.conn, c.settings)
ct.Namespace.Validate(c.conn, c.settings)
}
}
// FavNamespaces returns fav namespaces in the current cluster.
// FavNamespaces returns fav namespaces in the current context.
func (c *Config) FavNamespaces() []string {
cl := c.K9s.ActiveCluster()
ct, err := c.K9s.ActiveContext()
if err != nil {
return nil
}
return cl.Namespace.Favorites
return ct.Namespace.Favorites
}
// SetActiveNamespace set the active namespace in the current cluster.
// SetActiveNamespace set the active namespace in the current context.
func (c *Config) SetActiveNamespace(ns string) error {
if cl := c.K9s.ActiveCluster(); cl != nil {
return cl.Namespace.SetActive(ns, c.settings)
ct, err := c.K9s.ActiveContext()
if err != nil {
return err
}
err := errors.New("no active cluster. unable to set active namespace")
log.Error().Err(err).Msg("SetActiveNamespace")
return err
return ct.Namespace.SetActive(ns, c.settings)
}
// ActiveView returns the active view in the current cluster.
// ActiveView returns the active view in the current context.
func (c *Config) ActiveView() string {
cl := c.K9s.ActiveCluster()
if cl == nil {
return defaultView
ct, err := c.K9s.ActiveContext()
if err != nil {
return data.DefaultView
}
cmd := cl.View.Active
cmd := ct.View.Active
if c.K9s.manualCommand != nil && *c.K9s.manualCommand != "" {
cmd = *c.K9s.manualCommand
// We reset the manualCommand property because
@ -208,37 +183,41 @@ func (c *Config) ActiveView() string {
return cmd
}
// SetActiveView set the currently cluster active view.
// SetActiveView sets current context active view.
func (c *Config) SetActiveView(view string) {
if cl := c.K9s.ActiveCluster(); cl != nil {
cl.View.Active = view
if ct, err := c.K9s.ActiveContext(); err == nil {
ct.View.Active = view
}
}
// GetConnection return an api server connection.
func (c *Config) GetConnection() client.Connection {
return c.client
return c.conn
}
// SetConnection set an api server connection.
func (c *Config) SetConnection(conn client.Connection) {
c.client = conn
c.conn, c.K9s.conn = conn, conn
c.Validate()
}
// Load K9s configuration from file.
func (c *Config) ActiveContextName() string {
return c.K9s.activeContextName
}
// Load loads K9s configuration from file.
func (c *Config) Load(path string) error {
f, err := os.ReadFile(path)
if err != nil {
return err
}
c.K9s = NewK9s()
var cfg Config
if err := yaml.Unmarshal(f, &cfg); err != nil {
return err
}
if cfg.K9s != nil {
c.K9s = cfg.K9s
c.K9s.Refine(cfg.K9s)
}
if c.K9s.Logger == nil {
c.K9s.Logger = NewLogger()
@ -249,13 +228,15 @@ func (c *Config) Load(path string) error {
// Save configuration to disk.
func (c *Config) Save() error {
c.Validate()
return c.SaveFile(K9sConfigFile)
if err := c.K9s.Save(); err != nil {
return err
}
return c.SaveFile(AppConfigFile)
}
// SaveFile K9s configuration to disk.
func (c *Config) SaveFile(path string) error {
if err := EnsureDirPath(path, DefaultDirMod); err != nil {
if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil {
return err
}
cfg, err := yaml.Marshal(c)
@ -268,14 +249,14 @@ func (c *Config) SaveFile(path string) error {
// Validate the configuration.
func (c *Config) Validate() {
c.K9s.Validate(c.client, c.settings)
c.K9s.Validate(c.conn, c.settings)
}
// Dump debug...
func (c *Config) Dump(msg string) {
log.Debug().Msgf("Current Cluster: %s\n", c.K9s.CurrentCluster)
for k, cl := range c.K9s.Clusters {
log.Debug().Msgf("K9s cluster: %s -- %+v\n", k, cl.Namespace)
ct, err := c.K9s.ActiveContext()
if err != nil {
log.Debug().Msgf("Current Contexts: %s\n", ct.ClusterName)
}
}

View File

@ -10,7 +10,7 @@ import (
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/mock"
m "github.com/petergtz/pegomock"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
@ -22,19 +22,16 @@ func init() {
}
func TestConfigRefine(t *testing.T) {
cfgFile, ctx, cluster, ns := "testdata/kubeconfig-test.yml", "test2", "cluster2", "ns2"
var (
cfgFile = "testdata/kubeconfig-test.yaml"
ctx, cluster, ns = "ct-1-1", "cl-1", "ns-1"
)
uu := map[string]struct {
flags *genericclioptions.ConfigFlags
issue bool
context, cluster, namespace string
}{
"plain": {
flags: &genericclioptions.ConfigFlags{KubeConfig: &cfgFile},
issue: false,
context: "test1",
cluster: "cluster1",
namespace: "ns1",
},
"overrideNS": {
flags: &genericclioptions.ConfigFlags{
KubeConfig: &cfgFile,
@ -61,18 +58,14 @@ func TestConfigRefine(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := newMockSettings(u.flags)
cfg := config.NewConfig(mk)
cfg := mock.NewMockConfig()
err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))
if u.issue {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
assert.Equal(t, u.context, cfg.K9s.CurrentContext)
assert.Equal(t, u.cluster, cfg.K9s.CurrentCluster)
assert.Equal(t, u.context, cfg.K9s.ActiveContextName())
assert.Equal(t, u.namespace, cfg.ActiveNamespace())
}
})
@ -80,167 +73,60 @@ func TestConfigRefine(t *testing.T) {
}
func TestConfigValidate(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
cfg := mock.NewMockConfig()
cfg.SetConnection(mock.NewMockConnection())
mk := NewMockKubeSettings()
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"})
cfg := config.NewConfig(mk)
cfg.SetConnection(mc)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
cfg.Validate()
}
func TestConfigLoad(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
assert.Equal(t, 2, cfg.K9s.RefreshRate)
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount)
assert.Equal(t, "minikube", cfg.K9s.CurrentContext)
assert.Equal(t, "minikube", cfg.K9s.CurrentCluster)
assert.NotNil(t, cfg.K9s.Clusters)
assert.Equal(t, 2, len(cfg.K9s.Clusters))
nn := []string{
"default",
"kube-public",
"istio-system",
"all",
"kube-system",
}
assert.Equal(t, "kube-system", cfg.K9s.Clusters["minikube"].Namespace.Active)
assert.Equal(t, nn, cfg.K9s.Clusters["minikube"].Namespace.Favorites)
assert.Equal(t, "ctx", cfg.K9s.Clusters["minikube"].View.Active)
}
func TestConfigCurrentCluster(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
assert.NotNil(t, cfg.CurrentCluster())
assert.Equal(t, "kube-system", cfg.CurrentCluster().Namespace.Active)
assert.Equal(t, "ctx", cfg.CurrentCluster().View.Active)
}
func TestConfigActiveNamespace(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
assert.Equal(t, "kube-system", cfg.ActiveNamespace())
}
func TestConfigActiveNamespaceBlank(t *testing.T) {
cfg := config.Config{K9s: new(config.K9s)}
assert.Equal(t, "default", cfg.ActiveNamespace())
}
func TestConfigSetActiveNamespace(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
assert.Nil(t, cfg.SetActiveNamespace("default"))
assert.Equal(t, "default", cfg.ActiveNamespace())
}
func TestConfigActiveView(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
assert.Equal(t, "ctx", cfg.ActiveView())
}
func TestConfigActiveViewBlank(t *testing.T) {
cfg := config.Config{K9s: new(config.K9s)}
assert.Equal(t, "po", cfg.ActiveView())
}
func TestConfigSetActiveView(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cfg.SetActiveView("po")
assert.Equal(t, "po", cfg.ActiveView())
}
func TestConfigFavNamespaces(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
expectedNS := []string{"default", "kube-public", "istio-system", "all", "kube-system"}
assert.Equal(t, expectedNS, cfg.FavNamespaces())
}
func TestConfigLoadOldCfg(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s_old.yml"))
cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/k9s_old.yaml"))
}
func TestConfigLoadCrap(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.NotNil(t, cfg.Load("testdata/k9s_not_there.yml"))
cfg := mock.NewMockConfig()
assert.NotNil(t, cfg.Load("testdata/k9s_not_there.yaml"))
}
func TestConfigSaveFile(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
cfg := mock.NewMockConfig()
mk := NewMockKubeSettings()
m.When(mk.CurrentContextName()).ThenReturn("minikube", nil)
m.When(mk.CurrentClusterName()).ThenReturn("minikube", nil)
m.When(mk.CurrentNamespaceName()).ThenReturn("default", nil)
m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"minikube": {}, "fred": {}, "blee": {}}, nil)
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"})
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
cfg := config.NewConfig(mk)
cfg.SetConnection(mc)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cfg.K9s.RefreshRate = 100
cfg.K9s.ReadOnly = true
cfg.K9s.Logger.TailCount = 500
cfg.K9s.Logger.BufferSize = 800
cfg.K9s.CurrentContext = "blee"
cfg.K9s.CurrentCluster = "blee"
cfg.Validate()
path := filepath.Join("/tmp", "k9s.yml")
path := filepath.Join("/tmp", "k9s.yaml")
err := cfg.SaveFile(path)
assert.Nil(t, err)
raw, err := os.ReadFile(path)
assert.Nil(t, err)
assert.Equal(t, expectedConfig, string(raw))
}
func TestConfigReset(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings()
m.When(mk.CurrentContextName()).ThenReturn("blee", nil)
m.When(mk.CurrentClusterName()).ThenReturn("blee", nil)
m.When(mk.CurrentNamespaceName()).ThenReturn("default", nil)
m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"blee": {}}, nil)
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"})
cfg := config.NewConfig(mk)
cfg.SetConnection(mc)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
cfg.Reset()
cfg.Validate()
path := filepath.Join("/tmp", "k9s.yml")
path := filepath.Join("/tmp", "k9s.yaml")
err := cfg.SaveFile(path)
assert.Nil(t, err)
@ -258,46 +144,35 @@ func TestSetup(t *testing.T) {
})
}
type mockSettings struct {
flags *genericclioptions.ConfigFlags
}
var _ config.KubeSettings = (*mockSettings)(nil)
func newMockSettings(flags *genericclioptions.ConfigFlags) *mockSettings {
return &mockSettings{flags: flags}
}
func (m *mockSettings) CurrentContextName() (string, error) {
return *m.flags.Context, nil
}
func (m *mockSettings) CurrentClusterName() (string, error) { return "", nil }
func (m *mockSettings) CurrentNamespaceName() (string, error) {
return *m.flags.Namespace, nil
}
func (m *mockSettings) ClusterNames() (map[string]struct{}, error) { return nil, nil }
// ----------------------------------------------------------------------------
// Test Data...
var expectedConfig = `k9s:
liveViewAutoRefresh: true
screenDumpDir: /tmp
refreshRate: 100
maxConnRetry: 5
enableMouse: false
enableImageScan: false
headless: false
logoless: false
crumbsless: false
readOnly: true
noExitOnCtrlC: false
noIcons: false
ui:
enableMouse: false
headless: false
logoless: false
crumbsless: false
noIcons: false
skipLatestRevCheck: false
disablePodCounting: false
shellPod:
image: busybox:1.35.0
namespace: default
limits:
cpu: 100m
memory: 100Mi
skipLatestRevCheck: false
imageScans:
enable: false
blackList:
namespaces: []
labels: {}
logger:
tail: 500
buffer: 800
@ -305,51 +180,6 @@ var expectedConfig = `k9s:
fullScreenLogs: false
textWrap: false
showTime: false
currentContext: blee
currentCluster: blee
keepMissingClusters: false
clusters:
blee:
namespace:
active: default
lockFavorites: false
favorites:
- default
view:
active: po
featureGates:
nodeShell: false
portForwardAddress: localhost
fred:
namespace:
active: default
lockFavorites: false
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: po
featureGates:
nodeShell: false
portForwardAddress: localhost
minikube:
namespace:
active: kube-system
lockFavorites: false
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: ctx
featureGates:
nodeShell: false
portForwardAddress: localhost
thresholds:
cpu:
critical: 90
@ -357,29 +187,34 @@ var expectedConfig = `k9s:
memory:
critical: 90
warn: 70
screenDumpDir: /tmp
disablePodCounting: false
`
var resetConfig = `k9s:
liveViewAutoRefresh: true
screenDumpDir: /tmp
refreshRate: 2
maxConnRetry: 5
enableMouse: false
enableImageScan: false
headless: false
logoless: false
crumbsless: false
readOnly: false
noExitOnCtrlC: false
noIcons: false
ui:
enableMouse: false
headless: false
logoless: false
crumbsless: false
noIcons: false
skipLatestRevCheck: false
disablePodCounting: false
shellPod:
image: busybox:1.35.0
namespace: default
limits:
cpu: 100m
memory: 100Mi
skipLatestRevCheck: false
imageScans:
enable: false
blackList:
namespaces: []
labels: {}
logger:
tail: 200
buffer: 2000
@ -387,21 +222,6 @@ var resetConfig = `k9s:
fullScreenLogs: false
textWrap: false
showTime: false
currentContext: blee
currentCluster: blee
keepMissingClusters: false
clusters:
blee:
namespace:
active: default
lockFavorites: false
favorites:
- default
view:
active: po
featureGates:
nodeShell: false
portForwardAddress: localhost
thresholds:
cpu:
critical: 90
@ -409,6 +229,4 @@ var resetConfig = `k9s:
memory:
critical: 90
warn: 70
screenDumpDir: /tmp
disablePodCounting: false
`

View File

@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data
import (
"fmt"
"io"
"os"
"github.com/derailed/k9s/internal/client"
"gopkg.in/yaml.v2"
"k8s.io/client-go/tools/clientcmd/api"
)
// Config tracks a context configuration.
type Config struct {
Context *Context `yaml:"k9s"`
}
func NewConfig(ct *api.Context) *Config {
return &Config{
Context: NewContextFromConfig(ct),
}
}
func (c *Config) Validate(conn client.Connection, ks KubeSettings) {
c.Context.Validate(conn, ks)
}
func (c *Config) Dump(w io.Writer) {
bb, _ := yaml.Marshal(&c)
fmt.Fprintf(w, "%s\n", string(bb))
}
func (c *Config) Save(path string) error {
if err := EnsureDirPath(path, DefaultDirMod); err != nil {
return err
}
cfg, err := yaml.Marshal(c)
if err != nil {
return err
}
return os.WriteFile(path, cfg, DefaultFileMod)
}

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data
import (
"github.com/derailed/k9s/internal/client"
"k8s.io/client-go/tools/clientcmd/api"
)
// DefaultPFAddress specifies the default PortForward host address.
const DefaultPFAddress = "localhost"
// Context tracks K9s context configuration.
type Context struct {
ClusterName string `yaml:"cluster,omitempty"`
ReadOnly bool `yaml:"readOnly"`
Skin string `yaml:"skin,omitempty"`
Namespace *Namespace `yaml:"namespace"`
View *View `yaml:"view"`
FeatureGates FeatureGates `yaml:"featureGates"`
PortForwardAddress string `yaml:"portForwardAddress"`
}
// NewContext creates a new cluster configuration.
func NewContext() *Context {
return &Context{
Namespace: NewNamespace(),
View: NewView(),
PortForwardAddress: DefaultPFAddress,
FeatureGates: NewFeatureGates(),
}
}
func NewContextFromConfig(cfg *api.Context) *Context {
return &Context{
Namespace: NewActiveNamespace(cfg.Namespace),
ClusterName: cfg.Cluster,
View: NewView(),
PortForwardAddress: DefaultPFAddress,
FeatureGates: NewFeatureGates(),
}
}
// Validate a context config.
func (c *Context) Validate(conn client.Connection, ks KubeSettings) {
if c.PortForwardAddress == "" {
c.PortForwardAddress = DefaultPFAddress
}
if cl, err := ks.CurrentClusterName(); err != nil {
c.ClusterName = cl
}
if c.Namespace == nil {
c.Namespace = NewNamespace()
}
if c.Namespace.Active == client.BlankNamespace {
c.Namespace.Active = client.DefaultNamespace
}
c.Namespace.Validate(conn, ks)
if c.View == nil {
c.View = NewView()
}
c.View.Validate()
}

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data_test
import (
"testing"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/mock"
"github.com/stretchr/testify/assert"
)
func TestClusterValidate(t *testing.T) {
c := data.NewContext()
c.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1")))
assert.Equal(t, "po", c.View.Active)
assert.Equal(t, "default", c.Namespace.Active)
assert.Equal(t, 1, len(c.Namespace.Favorites))
assert.Equal(t, []string{"default"}, c.Namespace.Favorites)
}
func TestClusterValidateEmpty(t *testing.T) {
c := data.NewContext()
c.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1")))
assert.Equal(t, "po", c.View.Active)
assert.Equal(t, "default", c.Namespace.Active)
assert.Equal(t, 1, len(c.Namespace.Favorites))
assert.Equal(t, []string{"default"}, c.Namespace.Favorites)
}

View File

@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data
import (
"errors"
"os"
"path/filepath"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
"k8s.io/client-go/tools/clientcmd/api"
)
type Dir struct {
root string
conn client.Connection
ks KubeSettings
}
func NewDir(root string, conn client.Connection, ks KubeSettings) *Dir {
return &Dir{
root: root,
ks: ks,
conn: conn,
}
}
func (d Dir) Load(n string, ct *api.Context) (*Config, error) {
if ct == nil {
return nil, errors.New("api.Context must not be nil")
}
var (
path = filepath.Join(d.root, ct.Cluster, n, MainConfigFile)
cfg *Config
err error
)
if f, e := os.Stat(path); os.IsNotExist(e) || f.Size() == 0 {
log.Debug().Msgf("Context config not found! Generating... %q", path)
cfg, err = d.genConfig(path, ct)
} else {
log.Debug().Msgf("Found existing context config: %q", path)
cfg, err = d.loadConfig(path)
}
return cfg, err
}
func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) {
cfg := NewConfig(ct)
cfg.Validate(d.conn, d.ks)
if err := cfg.Save(path); err != nil {
return nil, err
}
return cfg, nil
}
func (d *Dir) loadConfig(path string) (*Config, error) {
bb, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(bb, &cfg); err != nil {
return nil, err
}
cfg.Validate(d.conn, d.ks)
return &cfg, nil
}

View File

@ -0,0 +1,104 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data_test
import (
"os"
"strings"
"testing"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/mock"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
func init() {
zerolog.SetGlobalLevel(zerolog.FatalLevel)
}
func TestDirLoad(t *testing.T) {
uu := map[string]struct {
dir string
flags *genericclioptions.ConfigFlags
err error
cfg *data.Config
}{
"happy-cl-1-ct-1": {
dir: "testdata/data/k9s",
flags: makeFlags("cl-1", "ct-1-1"),
cfg: mustLoadConfig("testdata/configs/ct-1-1.yaml"),
},
"happy-cl-1-ct2": {
dir: "testdata/data/k9s",
flags: makeFlags("cl-1", "ct-1-2"),
cfg: mustLoadConfig("testdata/configs/ct-1-2.yaml"),
},
"happy-cl-2": {
dir: "testdata/data/k9s",
flags: makeFlags("cl-2", "ct-2-1"),
cfg: mustLoadConfig("testdata/configs/ct-2-1.yaml"),
},
"toast": {
dir: "/tmp/data/k9s",
flags: makeFlags("cl-test", "ct-test-1"),
cfg: mustLoadConfig("testdata/configs/def_ct.yaml"),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.NotNil(t, u.cfg, "test config must not be nil")
if u.cfg == nil {
return
}
ks := mock.NewMockKubeSettings(u.flags)
if strings.Index(u.dir, "/tmp") == 0 {
assert.NoError(t, mock.EnsureDir(u.dir))
}
d := data.NewDir(u.dir, mock.NewMockConnection(), ks)
ct, err := ks.CurrentContext()
assert.NoError(t, err)
if err != nil {
return
}
cfg, err := d.Load(*u.flags.Context, ct)
assert.Equal(t, u.err, err)
if u.err == nil {
assert.Equal(t, u.cfg, cfg)
}
})
}
}
// Helpers...
func makeFlags(cl, ct string) *genericclioptions.ConfigFlags {
return &genericclioptions.ConfigFlags{
ClusterName: &cl,
Context: &ct,
}
}
func mustLoadConfig(cfg string) *data.Config {
bb, err := os.ReadFile(cfg)
if err != nil {
return nil
}
var ct data.Config
if err = yaml.Unmarshal(bb, &ct); err != nil {
return nil
}
return &ct
}

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data
// FeatureGates represents K9s opt-in features.
type FeatureGates struct {
NodeShell bool `yaml:"nodeShell"`
}
// NewFeatureGates returns a new feature gate.
func NewFeatureGates() FeatureGates {
return FeatureGates{}
}

View File

@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data
import (
"os"
"path/filepath"
)
// InList check if string is in a collection of strings.
func InList(ll []string, n string) bool {
for _, l := range ll {
if l == n {
return true
}
}
return false
}
// EnsureDirPath ensures a directory exist from the given path.
func EnsureDirPath(path string, mod os.FileMode) error {
return EnsureFullPath(filepath.Dir(path), mod)
}
// EnsureFullPath ensures a directory exist from the given path.
func EnsureFullPath(path string, mod os.FileMode) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
if err = os.MkdirAll(path, mod); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data_test
import (
"os"
"path/filepath"
"testing"
"github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert"
)
func TestHelperInList(t *testing.T) {
uu := []struct {
item string
list []string
expected bool
}{
{"a", []string{}, false},
{"", []string{}, false},
{"", []string{""}, true},
{"a", []string{"a", "b", "c", "d"}, true},
{"z", []string{"a", "b", "c", "d"}, false},
}
for _, u := range uu {
assert.Equal(t, u.expected, data.InList(u.list, u.item))
}
}
func TestEnsureDirPathNone(t *testing.T) {
var mod os.FileMode = 0744
dir := filepath.Join("/tmp", "fred")
os.Remove(dir)
path := filepath.Join(dir, "duh.yaml")
assert.NoError(t, data.EnsureDirPath(path, mod))
p, err := os.Stat(dir)
assert.NoError(t, err)
assert.Equal(t, "drwxr--r--", p.Mode().String())
}
func TestEnsureDirPathNoOpt(t *testing.T) {
var mod os.FileMode = 0744
dir := filepath.Join("/tmp", "k9s-test")
os.Remove(dir)
assert.NoError(t, os.Mkdir(dir, mod))
path := filepath.Join(dir, "duh.yaml")
assert.NoError(t, data.EnsureDirPath(path, mod))
p, err := os.Stat(dir)
assert.NoError(t, err)
assert.Equal(t, "drwxr--r--", p.Mode().String())
}

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config
package data
import (
"github.com/derailed/k9s/internal/client"
@ -11,8 +11,6 @@ import (
const (
// MaxFavoritesNS number # favorite namespaces to keep in the configuration.
MaxFavoritesNS = 9
defaultNS = "default"
allNS = "all"
)
// Namespace tracks active and favorites namespaces.
@ -25,27 +23,33 @@ type Namespace struct {
// NewNamespace create a new namespace configuration.
func NewNamespace() *Namespace {
return &Namespace{
Active: defaultNS,
Favorites: []string{defaultNS},
Active: client.DefaultNamespace,
Favorites: []string{client.DefaultNamespace},
}
}
func NewActiveNamespace(n string) *Namespace {
return &Namespace{
Active: n,
Favorites: []string{client.DefaultNamespace},
}
}
// Validate a namespace is setup correctly.
func (n *Namespace) Validate(c client.Connection, ks KubeSettings) {
if c == nil {
n = NewActiveNamespace(client.DefaultNamespace)
}
if c == nil {
log.Debug().Msgf("No connection found. Skipping ns validation")
return
}
nns, err := c.ValidNamespaces()
if err != nil {
return
}
nn := client.NamespaceNames(nns)
if !n.isAllNamespaces() && !InList(nn, n.Active) {
if !n.isAllNamespaces() && !c.IsValidNamespace(n.Active) {
log.Error().Msgf("[Config] Validation error active namespace %q does not exists", n.Active)
}
for _, ns := range n.Favorites {
if ns != allNS && !InList(nn, ns) {
if ns != client.NamespaceAll && !c.IsValidNamespace(ns) {
log.Debug().Msgf("[Config] Invalid favorite found '%s' - %t", ns, n.isAllNamespaces())
n.rmFavNS(ns)
}
@ -54,8 +58,8 @@ func (n *Namespace) Validate(c client.Connection, ks KubeSettings) {
// SetActive set the active namespace.
func (n *Namespace) SetActive(ns string, ks KubeSettings) error {
if ns == client.NotNamespaced {
ns = client.AllNamespaces
if ns == client.BlankNamespace {
ns = client.NamespaceAll
}
n.Active = ns
if ns != "" && !n.LockFavorites {
@ -66,7 +70,7 @@ func (n *Namespace) SetActive(ns string, ks KubeSettings) error {
}
func (n *Namespace) isAllNamespaces() bool {
return n.Active == allNS || n.Active == ""
return n.Active == client.NamespaceAll || n.Active == ""
}
func (n *Namespace) addFavNS(ns string) {

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data_test
import (
"testing"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/mock"
"github.com/stretchr/testify/assert"
)
func TestNSValidate(t *testing.T) {
ns := data.NewNamespace()
ns.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1")))
assert.Equal(t, "default", ns.Active)
assert.Equal(t, []string{"default"}, ns.Favorites)
}
func TestNSValidateMissing(t *testing.T) {
ns := data.NewNamespace()
ns.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1")))
assert.Equal(t, "default", ns.Active)
assert.Equal(t, []string{"default"}, ns.Favorites)
}
func TestNSValidateNoNS(t *testing.T) {
ns := data.NewNamespace()
ns.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1")))
assert.Equal(t, "default", ns.Active)
assert.Equal(t, []string{"default"}, ns.Favorites)
}
func TestNSSetActive(t *testing.T) {
allNS := []string{"ns4", "ns3", "ns2", "ns1", "all", "default"}
uu := []struct {
ns string
fav []string
}{
{"all", []string{"all", "default"}},
{"ns1", []string{"ns1", "all", "default"}},
{"ns2", []string{"ns2", "ns1", "all", "default"}},
{"ns3", []string{"ns3", "ns2", "ns1", "all", "default"}},
{"ns4", allNS},
}
mk := mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))
ns := data.NewNamespace()
for _, u := range uu {
err := ns.SetActive(u.ns, mk)
assert.Nil(t, err)
assert.Equal(t, u.ns, ns.Active)
assert.Equal(t, u.fav, ns.Favorites)
}
}
func TestNSValidateRmFavs(t *testing.T) {
ns := data.NewNamespace()
ns.Favorites = []string{"default", "fred"}
ns.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1")))
assert.Equal(t, []string{"default", "fred"}, ns.Favorites)
}

View File

@ -0,0 +1,16 @@
k9s:
cluster: cl-1
skin: skin-1
readOnly: false
namespace:
active: ns-1
lockFavorites: true
favorites:
- default
- ns-1
- ns-2
view:
active: dp
featureGates:
nodeShell: true
portForwardAddress: localhost

View File

@ -0,0 +1,14 @@
k9s:
cluster: cl-1
skin: in_the_navy
readOnly: true
namespace:
active: default
lockFavorites: false
favorites:
- default
view:
active: po
featureGates:
nodeShell: false
portForwardAddress: localhost

View File

@ -0,0 +1,15 @@
k9s:
cluster: cl-2
skin: skin-2
readOnly: true
namespace:
active: ns-2
lockFavorites: true
favorites:
- ns-1
- ns-2
view:
active: svc
featureGates:
nodeShell: true
portForwardAddress: fred

View File

@ -0,0 +1,12 @@
k9s:
cluster: cl-test
namespace:
active: default
lockFavorites: false
favorites:
- default
view:
active: po
featureGates:
nodeShell: false
portForwardAddress: localhost

View File

@ -0,0 +1,16 @@
k9s:
cluster: cl-1
skin: skin-1
readOnly: false
namespace:
active: ns-1
lockFavorites: true
favorites:
- default
- ns-1
- ns-2
view:
active: dp
featureGates:
nodeShell: true
portForwardAddress: localhost

View File

@ -0,0 +1,14 @@
k9s:
cluster: cl-1
skin: in_the_navy
readOnly: true
namespace:
active: default
lockFavorites: false
favorites:
- default
view:
active: po
featureGates:
nodeShell: false
portForwardAddress: localhost

View File

@ -0,0 +1,15 @@
k9s:
cluster: cl-2
skin: skin-2
readOnly: true
namespace:
active: ns-2
lockFavorites: true
favorites:
- ns-1
- ns-2
view:
active: svc
featureGates:
nodeShell: true
portForwardAddress: fred

View File

@ -0,0 +1,42 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package data
import (
"os"
"k8s.io/client-go/tools/clientcmd/api"
)
const (
// DefaultDirMod default unix perms for k9s directory.
DefaultDirMod os.FileMode = 0744
// DefaultFileMod default unix perms for k9s files.
DefaultFileMod os.FileMode = 0600
// MainConfigFile track main configuration file..
MainConfigFile = "config.yaml"
)
// KubeSettings exposes kubeconfig context information.
type KubeSettings interface {
// CurrentContextName returns the name of the current context.
CurrentContextName() (string, error)
// CurrentClusterName returns the name of the current cluster.
CurrentClusterName() (string, error)
// CurrentNamespace returns the name of the current namespace.
CurrentNamespaceName() (string, error)
// ContextNames() returns all available context names.
ContextNames() (map[string]struct{}, error)
// CurrentContext returns the current context configuration.
CurrentContext() (*api.Context, error)
// GetContext returns a given context configuration or err if not found.
GetContext(string) (*api.Context, error)
}

View File

@ -1,9 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config
package data
const defaultView = "po"
const DefaultView = "po"
// View tracks view configuration options.
type View struct {
@ -12,12 +12,12 @@ type View struct {
// NewView creates a new view configuration.
func NewView() *View {
return &View{Active: defaultView}
return &View{Active: DefaultView}
}
// Validate a view configuration.
func (v *View) Validate() {
if len(v.Active) == 0 {
v.Active = defaultView
v.Active = DefaultView
}
}

View File

@ -1,17 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config_test
package data_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert"
)
func TestViewValidate(t *testing.T) {
v := config.NewView()
v := data.NewView()
v.Validate()
assert.Equal(t, "po", v.Active)
@ -22,7 +22,7 @@ func TestViewValidate(t *testing.T) {
}
func TestViewValidateBlank(t *testing.T) {
var v config.View
var v data.View
v.Validate()
assert.Equal(t, "po", v.Active)
}

View File

@ -3,12 +3,12 @@
package config
// FeatureGates represents K9s opt-in features.
type FeatureGates struct {
NodeShell bool `yaml:"nodeShell"`
}
// // FeatureGates represents K9s opt-in features.
// type FeatureGates struct {
// NodeShell bool `yaml:"nodeShell"`
// }
// NewFeatureGates returns a new feature gate.
func NewFeatureGates() *FeatureGates {
return &FeatureGates{}
}
// // NewFeatureGates returns a new feature gate.
// func NewFeatureGates() *FeatureGates {
// return &FeatureGates{}
// }

297
internal/config/files.go Normal file
View File

@ -0,0 +1,297 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config
import (
_ "embed"
"os"
"os/user"
"path/filepath"
"regexp"
"github.com/derailed/k9s/internal/config/data"
"github.com/adrg/xdg"
"github.com/rs/zerolog/log"
)
const (
// K9sConfigDir represents k9s configuration dir env var.
K9sConfigDir = "K9S_CONFIG_DIR"
// AppName tracks k9s app name.
AppName = "k9s"
K9sLogsFile = "k9s.log"
)
var (
//go:embed templates/benchmarks.yaml
// benchmarkTpl tracks benchmark default config template
benchmarkTpl []byte
//go:embed templates/aliases.yaml
// aliasesTpl tracks aliases default config template
aliasesTpl []byte
//go:embed templates/hotkeys.yaml
// hotkeysTpl tracks hotkeys default config template
hotkeysTpl []byte
//go:embed templates/stock-skin.yaml
// stockSkinTpl tracks stock skin template
stockSkinTpl []byte
)
var (
// AppConfigDir tracks main k9s config home directory.
AppConfigDir string
// AppSkinsDir tracks skins data directory.
AppSkinsDir string
// AppBenchmarksDir tracks benchmarks results directory.
AppBenchmarksDir string
// AppDumpsDir tracks screen dumps data directory.
AppDumpsDir string
// AppContextsDir tracks contexts data directory.
AppContextsDir string
// AppConfigFile tracks k9s config file.
AppConfigFile string
// AppLogFile tracks k9s logs file.
AppLogFile string
// AppViewsFile tracks custom views config file.
AppViewsFile string
// AppAliasesFile tracks aliases config file.
AppAliasesFile string
// AppPluginsFile tracks plugins config file.
AppPluginsFile string
// AppHotKeysFile tracks hotkeys config file.
AppHotKeysFile string
)
// InitLogsLoc initializes K9s logs location.
func InitLogLoc() error {
if hasK9sConfigEnv() {
tmpDir, err := userTmpDir()
if err != nil {
return err
}
AppLogFile = filepath.Join(tmpDir, K9sLogsFile)
return nil
}
var err error
AppLogFile, err = xdg.StateFile(filepath.Join(AppName, K9sLogsFile))
return err
}
// InitLocs initializes k9s artifacts locations.
func InitLocs() error {
if hasK9sConfigEnv() {
return initK9sEnvLocs()
}
return initXDGLocs()
}
func initK9sEnvLocs() error {
AppConfigDir = os.Getenv(K9sConfigDir)
if err := data.EnsureFullPath(AppConfigDir, data.DefaultDirMod); err != nil {
return err
}
AppDumpsDir = filepath.Join(AppConfigDir, "screen-dumps")
if err := data.EnsureFullPath(AppDumpsDir, data.DefaultDirMod); err != nil {
log.Warn().Err(err).Msgf("Unable to create screen-dumps dir: %s", AppDumpsDir)
}
AppBenchmarksDir = filepath.Join(AppConfigDir, "benchmarks")
if err := data.EnsureFullPath(AppBenchmarksDir, data.DefaultDirMod); err != nil {
log.Warn().Err(err).Msgf("Unable to create benchmarks dir: %s", AppBenchmarksDir)
}
AppSkinsDir = filepath.Join(AppConfigDir, "skins")
if err := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); err != nil {
log.Warn().Err(err).Msgf("Unable to create skins dir: %s", AppSkinsDir)
}
AppContextsDir = filepath.Join(AppConfigDir, "clusters")
if err := data.EnsureFullPath(AppContextsDir, data.DefaultDirMod); err != nil {
log.Warn().Err(err).Msgf("Unable to create clusters dir: %s", AppContextsDir)
}
AppConfigFile = filepath.Join(AppConfigDir, data.MainConfigFile)
AppHotKeysFile = filepath.Join(AppConfigDir, "hotkeys.yaml")
AppAliasesFile = filepath.Join(AppConfigDir, "aliases.yaml")
AppPluginsFile = filepath.Join(AppConfigDir, "plugins.yaml")
AppViewsFile = filepath.Join(AppConfigDir, "views.yaml")
return nil
}
func initXDGLocs() error {
var err error
AppConfigDir, err = xdg.ConfigFile(AppName)
if err != nil {
return err
}
AppConfigFile, err = xdg.ConfigFile(filepath.Join(AppName, data.MainConfigFile))
if err != nil {
return err
}
AppHotKeysFile = filepath.Join(AppConfigDir, "hotkeys.yaml")
AppAliasesFile = filepath.Join(AppConfigDir, "aliases.yaml")
AppPluginsFile = filepath.Join(AppConfigDir, "plugins.yaml")
AppViewsFile = filepath.Join(AppConfigDir, "views.yaml")
AppSkinsDir = filepath.Join(AppConfigDir, "skins")
if err := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); err != nil {
log.Warn().Err(err).Msgf("No skins dir detected")
}
AppDumpsDir, err = xdg.StateFile(filepath.Join(AppName, "screen-dumps"))
if err != nil {
return err
}
AppBenchmarksDir, err = xdg.StateFile(filepath.Join(AppName, "benchmarks"))
if err != nil {
log.Warn().Err(err).Msgf("No benchmarks dir detected")
}
dataDir, err := xdg.DataFile(AppName)
if err != nil {
return err
}
AppContextsDir = filepath.Join(dataDir, "clusters")
if err := data.EnsureFullPath(AppContextsDir, data.DefaultDirMod); err != nil {
log.Warn().Err(err).Msgf("No context dir detected")
}
return nil
}
var invalidPathCharsRX = regexp.MustCompile(`[:/]+`)
// SanitizeFileName ensure file spec is valid.
func SanitizeFileName(name string) string {
return invalidPathCharsRX.ReplaceAllString(name, "-")
}
// AppContextDir generates a valid context config dir.
func AppContextDir(cluster, context string) string {
return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context))
}
// AppContextAliasesFile generates a valid context specific aliases file path.
func AppContextAliasesFile(cluster, context string) string {
return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "aliases.yaml")
}
// AppContextPluginsFile generates a valid context specific plugins file path.
func AppContextPluginsFile(cluster, context string) string {
return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "plugins.yaml")
}
// AppContextHotkeysFile generates a valid context specific hotkeys file path.
func AppContextHotkeysFile(cluster, context string) string {
return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "hotkeys.yaml")
}
// AppContextConfig generates a valid context config file path.
func AppContextConfig(cluster, context string) string {
return filepath.Join(AppContextDir(cluster, context), data.MainConfigFile)
}
// DumpsDir generates a valid context dump directory.
func DumpsDir(cluster, context string) (string, error) {
dir := filepath.Join(AppDumpsDir, sanContextSubpath(cluster, context))
return dir, data.EnsureDirPath(dir, data.DefaultDirMod)
}
// EnsureBenchmarksDir generates a valid benchmark results directory.
func EnsureBenchmarksDir(cluster, context string) (string, error) {
dir := filepath.Join(AppBenchmarksDir, sanContextSubpath(cluster, context))
return dir, data.EnsureDirPath(dir, data.DefaultDirMod)
}
// EnsureBenchmarksCfgFile generates a valid benchmark file.
func EnsureBenchmarksCfgFile(cluster, context string) (string, error) {
f := filepath.Join(AppContextDir(cluster, context), "benchmarks.yaml")
if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {
return "", err
}
if _, err := os.Stat(f); os.IsNotExist(err) {
return f, os.WriteFile(f, benchmarkTpl, data.DefaultFileMod)
}
return f, nil
}
// EnsureAliasesCfgFile generates a valid aliases file.
func EnsureAliasesCfgFile() (string, error) {
f := filepath.Join(AppConfigDir, "aliases.yaml")
if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {
return "", err
}
if _, err := os.Stat(f); os.IsNotExist(err) {
return f, os.WriteFile(f, aliasesTpl, data.DefaultFileMod)
}
return f, nil
}
// EnsureHotkeysCfgFile generates a valid hotkeys file.
func EnsureHotkeysCfgFile() (string, error) {
f := filepath.Join(AppConfigDir, "hotkeys.yaml")
if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {
return "", err
}
if _, err := os.Stat(f); os.IsNotExist(err) {
return f, os.WriteFile(f, hotkeysTpl, data.DefaultFileMod)
}
return f, nil
}
// SkinFileFromName generate skin file path from spec.
func SkinFileFromName(n string) string {
return filepath.Join(AppSkinsDir, n+".yaml")
}
// Helpers...
func sanContextSubpath(cluster, context string) string {
return filepath.Join(SanitizeFileName(cluster), SanitizeFileName(context))
}
func hasK9sConfigEnv() bool {
return os.Getenv(K9sConfigDir) != ""
}
func userTmpDir() (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
dir := filepath.Join(os.TempDir(), AppName, u.Username)
if err := data.EnsureFullPath(dir, data.DefaultDirMod); err != nil {
return "", err
}
return dir, nil
}

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config_test
import (
"os"
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert"
)
func TestEnsureBenchmarkCfg(t *testing.T) {
os.Setenv(config.K9sConfigDir, "/tmp/test-config")
assert.NoError(t, config.InitLocs())
defer assert.NoError(t, os.RemoveAll(config.K9sConfigDir))
assert.NoError(t, data.EnsureFullPath("/tmp/test-config/clusters/cl-1/ct-2", data.DefaultDirMod))
assert.NoError(t, os.WriteFile("/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", []byte{}, data.DefaultFileMod))
uu := map[string]struct {
cluster, context string
f, e string
}{
"not-exist": {
cluster: "cl-1",
context: "ct-1",
f: "/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml",
e: "benchmarks:\n defaults:\n concurrency: 2\n requests: 200",
},
"exist": {
cluster: "cl-1",
context: "ct-2",
f: "/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
f, err := config.EnsureBenchmarksCfgFile(u.cluster, u.context)
assert.NoError(t, err)
assert.Equal(t, u.f, f)
bb, err := os.ReadFile(f)
assert.NoError(t, err)
assert.Equal(t, u.e, string(bb))
})
}
}

View File

@ -3,12 +3,6 @@
package config
import (
"fmt"
"os"
"path/filepath"
)
const (
// DefaultRefreshRate represents the refresh interval.
DefaultRefreshRate = 2 // secs
@ -21,7 +15,7 @@ const (
)
// DefaultLogFile represents the default K9s log file.
var DefaultLogFile = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser()))
// var DefaultLogFile = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser()))
// Flags represents K9s configuration flags.
type Flags struct {
@ -43,7 +37,7 @@ func NewFlags() *Flags {
return &Flags{
RefreshRate: intPtr(DefaultRefreshRate),
LogLevel: strPtr(DefaultLogLevel),
LogFile: strPtr(DefaultLogFile),
LogFile: strPtr(AppLogFile),
Headless: boolPtr(false),
Logoless: boolPtr(false),
Command: strPtr(DefaultCommand),
@ -51,7 +45,7 @@ func NewFlags() *Flags {
ReadOnly: boolPtr(false),
Write: boolPtr(false),
Crumbsless: boolPtr(false),
ScreenDumpDir: strPtr(K9sDefaultScreenDumpDir),
ScreenDumpDir: strPtr(AppDumpsDir),
}
}

View File

@ -4,39 +4,18 @@
package config
import (
"os"
"os/user"
"path/filepath"
"regexp"
"github.com/derailed/k9s/internal/config/data"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
)
const (
// DefaultDirMod default unix perms for k9s directory.
DefaultDirMod os.FileMode = 0755
// DefaultFileMod default unix perms for k9s files.
DefaultFileMod os.FileMode = 0600
)
var invalidPathCharsRX = regexp.MustCompile(`[:/]+`)
// SanitizeFilename sanitizes the dump filename.
func SanitizeFilename(name string) string {
return invalidPathCharsRX.ReplaceAllString(name, "-")
}
// InList check if string is in a collection of strings.
func InList(ll []string, n string) bool {
for _, l := range ll {
if l == n {
return true
}
}
return false
}
// InNSList check if ns is in an ns collection.
func InNSList(nn []interface{}, ns string) bool {
ss := make([]string, len(nn))
@ -45,7 +24,7 @@ func InNSList(nn []interface{}, ns string) bool {
ss[i] = nsp.Name
}
}
return InList(ss, ns)
return data.InList(ss, ns)
}
// MustK9sUser establishes current user identity or fail.
@ -57,22 +36,6 @@ func MustK9sUser() string {
return usr.Username
}
// EnsureDirPath ensures a directory exist from the given path.
func EnsureDirPath(path string, mod os.FileMode) error {
return EnsureFullPath(filepath.Dir(path), mod)
}
// EnsureFullPath ensures a directory exist from the given path.
func EnsureFullPath(path string, mod os.FileMode) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
if err = os.MkdirAll(path, mod); err != nil {
return err
}
}
return nil
}
// IsBoolSet checks if a bool prt is set.
func IsBoolSet(b *bool) bool {
return b != nil && *b

View File

@ -4,8 +4,6 @@
package config_test
import (
"os"
"path/filepath"
"testing"
"github.com/derailed/k9s/internal/config"
@ -14,24 +12,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestHelperInList(t *testing.T) {
uu := []struct {
item string
list []string
expected bool
}{
{"a", []string{}, false},
{"", []string{}, false},
{"", []string{""}, true},
{"a", []string{"a", "b", "c", "d"}, true},
{"z", []string{"a", "b", "c", "d"}, false},
}
for _, u := range uu {
assert.Equal(t, u.expected, config.InList(u.list, u.item))
}
}
func TestHelperInNSList(t *testing.T) {
uu := []struct {
item string
@ -58,30 +38,3 @@ func TestHelperInNSList(t *testing.T) {
assert.Equal(t, u.expected, config.InNSList(u.list, u.item))
}
}
func TestEnsureDirPathNone(t *testing.T) {
var mod os.FileMode = 0744
dir := filepath.Join("/tmp", "fred")
os.Remove(dir)
path := filepath.Join(dir, "duh.yml")
assert.NoError(t, config.EnsureDirPath(path, mod))
p, err := os.Stat(dir)
assert.NoError(t, err)
assert.Equal(t, "drwxr--r--", p.Mode().String())
}
func TestEnsureDirPathNoOpt(t *testing.T) {
var mod os.FileMode = 0744
dir := filepath.Join("/tmp", "blee")
os.Remove(dir)
assert.NoError(t, os.Mkdir(dir, mod))
path := filepath.Join(dir, "duh.yml")
assert.NoError(t, config.EnsureDirPath(path, mod))
p, err := os.Stat(dir)
assert.NoError(t, err)
assert.Equal(t, "drwxr--r--", p.Mode().String())
}

View File

@ -5,17 +5,13 @@ package config
import (
"os"
"path/filepath"
"gopkg.in/yaml.v2"
)
// K9sHotKeys manages K9s hotKeys.
var K9sHotKeys = YamlExtension(filepath.Join(K9sHome(), "hotkey.yml"))
// HotKeys represents a collection of plugins.
type HotKeys struct {
HotKey map[string]HotKey `yaml:"hotKey"`
HotKey map[string]HotKey `yaml:"hotKeys"`
}
// HotKey describes a K9s hotkey.
@ -34,7 +30,7 @@ func NewHotKeys() HotKeys {
// Load K9s plugins.
func (h HotKeys) Load() error {
return h.LoadHotKeys(K9sHotKeys)
return h.LoadHotKeys(AppHotKeysFile)
}
// LoadHotKeys loads plugins from a given file.

View File

@ -12,7 +12,7 @@ import (
func TestHotKeyLoad(t *testing.T) {
h := config.NewHotKeys()
assert.Nil(t, h.LoadHotKeys("testdata/hot_key.yml"))
assert.Nil(t, h.LoadHotKeys("testdata/hotkeys.yaml"))
assert.Equal(t, 1, len(h.HotKey))

View File

@ -4,37 +4,28 @@
package config
import (
"github.com/derailed/k9s/internal/client"
)
"errors"
"path/filepath"
const (
defaultRefreshRate = 2
defaultMaxConnRetry = 5
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
)
// K9s tracks K9s configuration options.
type K9s struct {
LiveViewAutoRefresh bool `yaml:"liveViewAutoRefresh"`
RefreshRate int `yaml:"refreshRate"`
MaxConnRetry int `yaml:"maxConnRetry"`
EnableMouse bool `yaml:"enableMouse"`
EnableImageScan bool `yaml:"enableImageScan"`
Headless bool `yaml:"headless"`
Logoless bool `yaml:"logoless"`
Crumbsless bool `yaml:"crumbsless"`
ReadOnly bool `yaml:"readOnly"`
NoExitOnCtrlC bool `yaml:"noExitOnCtrlC"`
NoIcons bool `yaml:"noIcons"`
ShellPod *ShellPod `yaml:"shellPod"`
SkipLatestRevCheck bool `yaml:"skipLatestRevCheck"`
Logger *Logger `yaml:"logger"`
CurrentContext string `yaml:"currentContext"`
CurrentCluster string `yaml:"currentCluster"`
KeepMissingClusters bool `yaml:"keepMissingClusters"`
Clusters map[string]*Cluster `yaml:"clusters,omitempty"`
Thresholds Threshold `yaml:"thresholds"`
ScreenDumpDir string `yaml:"screenDumpDir"`
DisablePodCounting bool `yaml:"disablePodCounting"`
LiveViewAutoRefresh bool `yaml:"liveViewAutoRefresh"`
ScreenDumpDir string `yaml:"screenDumpDir,omitempty"`
RefreshRate int `yaml:"refreshRate"`
MaxConnRetry int `yaml:"maxConnRetry"`
ReadOnly bool `yaml:"readOnly"`
NoExitOnCtrlC bool `yaml:"noExitOnCtrlC"`
UI UI `yaml:"ui"`
SkipLatestRevCheck bool `yaml:"skipLatestRevCheck"`
DisablePodCounting bool `yaml:"disablePodCounting"`
ShellPod *ShellPod `yaml:"shellPod"`
ImageScans *ImageScans `yaml:"imageScans"`
Logger *Logger `yaml:"logger"`
Thresholds Threshold `yaml:"thresholds"`
manualRefreshRate int
manualHeadless *bool
manualLogoless *bool
@ -42,36 +33,151 @@ type K9s struct {
manualReadOnly *bool
manualCommand *string
manualScreenDumpDir *string
dir *data.Dir
activeContextName string
activeConfig *data.Config
conn client.Connection
ks data.KubeSettings
}
// NewK9s create a new K9s configuration.
func NewK9s() *K9s {
func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s {
return &K9s{
RefreshRate: defaultRefreshRate,
MaxConnRetry: defaultMaxConnRetry,
ScreenDumpDir: AppDumpsDir,
Logger: NewLogger(),
Clusters: make(map[string]*Cluster),
Thresholds: NewThreshold(),
ScreenDumpDir: K9sDefaultScreenDumpDir,
ShellPod: NewShellPod(),
ImageScans: NewImageScans(),
dir: data.NewDir(AppContextsDir, conn, ks),
conn: conn,
ks: ks,
}
}
func (k *K9s) CurrentContextDir() string {
return SanitizeFilename(k.CurrentContext)
func (k *K9s) Save() error {
if k.activeConfig != nil {
path := filepath.Join(
AppContextsDir,
k.activeConfig.Context.ClusterName,
k.activeContextName,
data.MainConfigFile,
)
return k.activeConfig.Save(path)
}
return nil
}
// ActivateCluster initializes the active cluster is not present.
func (k *K9s) ActivateCluster(ns string) {
if k.Clusters == nil {
k.Clusters = map[string]*Cluster{}
func (k *K9s) Refine(k1 *K9s) {
k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh
k.ScreenDumpDir = k1.ScreenDumpDir
k.RefreshRate = k1.RefreshRate
k.MaxConnRetry = k1.MaxConnRetry
k.ReadOnly = k1.ReadOnly
k.NoExitOnCtrlC = k1.NoExitOnCtrlC
k.UI = k1.UI
k.SkipLatestRevCheck = k1.SkipLatestRevCheck
k.DisablePodCounting = k1.DisablePodCounting
k.ShellPod = k1.ShellPod
k.ImageScans = k1.ImageScans
k.Logger = k1.Logger
k.Thresholds = k1.Thresholds
}
func (k *K9s) Generate(k9sFlags *Flags) {
if *k9sFlags.RefreshRate != DefaultRefreshRate {
k.OverrideRefreshRate(*k9sFlags.RefreshRate)
}
if _, ok := k.Clusters[k.CurrentCluster]; ok {
return
k.OverrideHeadless(*k9sFlags.Headless)
k.OverrideLogoless(*k9sFlags.Logoless)
k.OverrideCrumbsless(*k9sFlags.Crumbsless)
k.OverrideReadOnly(*k9sFlags.ReadOnly)
k.OverrideWrite(*k9sFlags.Write)
k.OverrideCommand(*k9sFlags.Command)
k.OverrideScreenDumpDir(*k9sFlags.ScreenDumpDir)
}
// OverrideScreenDumpDir set the screen dump dir manually.
func (k *K9s) OverrideScreenDumpDir(dir string) {
k.manualScreenDumpDir = &dir
}
func (k *K9s) GetScreenDumpDir() string {
screenDumpDir := k.ScreenDumpDir
if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" {
screenDumpDir = *k.manualScreenDumpDir
}
cl := NewCluster()
cl.Namespace.Active = ns
k.Clusters[k.CurrentCluster] = cl
if screenDumpDir == "" {
screenDumpDir = AppDumpsDir
}
return screenDumpDir
}
func (k *K9s) Reset() {
k.activeConfig, k.activeContextName = nil, ""
}
func (k *K9s) ActiveContextDir() string {
if k.activeConfig == nil {
return "na"
}
return filepath.Join(
SanitizeFileName(k.activeConfig.Context.ClusterName),
SanitizeFileName(k.ActiveContextName()),
)
}
func (k *K9s) ActiveContextNamespace() (string, error) {
if k.activeConfig != nil {
return k.activeConfig.Context.Namespace.Active, nil
}
return "", errors.New("context config is not set")
}
func (k *K9s) ActiveContextName() string {
return k.activeContextName
}
// ActiveContext returns the currently active context.
func (k *K9s) ActiveContext() (*data.Context, error) {
if k.activeConfig != nil {
return k.activeConfig.Context, nil
}
ct, err := k.ActivateContext(k.activeContextName)
if err != nil {
return nil, err
}
return ct, nil
}
// ActivateContext initializes the active context is not present.
func (k *K9s) ActivateContext(n string) (*data.Context, error) {
k.activeContextName = n
ct, err := k.ks.GetContext(k.activeContextName)
if err != nil {
return nil, err
}
cfg, err := k.dir.Load(n, ct)
if err != nil {
return nil, err
}
k.activeConfig = cfg
// If the context specifies a default namespace, use it!
if k.conn != nil {
if ns := k.conn.ActiveNamespace(); ns != client.BlankNamespace {
k.activeConfig.Context.Namespace.Active = ns
}
}
return cfg.Context, nil
}
// OverrideRefreshRate set the refresh rate manually.
@ -114,14 +220,9 @@ func (k *K9s) OverrideCommand(cmd string) {
k.manualCommand = &cmd
}
// OverrideScreenDumpDir set the screen dump dir manually.
func (k *K9s) OverrideScreenDumpDir(dir string) {
k.manualScreenDumpDir = &dir
}
// IsHeadless returns headless setting.
func (k *K9s) IsHeadless() bool {
h := k.Headless
h := k.UI.Headless
if k.manualHeadless != nil && *k.manualHeadless {
h = *k.manualHeadless
}
@ -131,7 +232,7 @@ func (k *K9s) IsHeadless() bool {
// IsLogoless returns logoless setting.
func (k *K9s) IsLogoless() bool {
h := k.Logoless
h := k.UI.Logoless
if k.manualLogoless != nil && *k.manualLogoless {
h = *k.manualLogoless
}
@ -141,7 +242,7 @@ func (k *K9s) IsLogoless() bool {
// IsCrumbsless returns crumbsless setting.
func (k *K9s) IsCrumbsless() bool {
h := k.Crumbsless
h := k.UI.Crumbsless
if k.manualCrumbsless != nil && *k.manualCrumbsless {
h = *k.manualCrumbsless
}
@ -165,36 +266,13 @@ func (k *K9s) IsReadOnly() bool {
if k.manualReadOnly != nil {
readOnly = *k.manualReadOnly
}
if k.activeConfig != nil && k.activeConfig.Context.ReadOnly {
readOnly = true
}
return readOnly
}
// ActiveCluster returns the currently active cluster.
func (k *K9s) ActiveCluster() *Cluster {
if k.Clusters == nil {
k.Clusters = map[string]*Cluster{}
}
if c, ok := k.Clusters[k.CurrentCluster]; ok {
return c
}
k.Clusters[k.CurrentCluster] = NewCluster()
return k.Clusters[k.CurrentCluster]
}
func (k *K9s) GetScreenDumpDir() string {
screenDumpDir := k.ScreenDumpDir
if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" {
screenDumpDir = *k.manualScreenDumpDir
}
if screenDumpDir == "" {
return K9sDefaultScreenDumpDir
}
return screenDumpDir
}
func (k *K9s) validateDefaults() {
if k.RefreshRate <= 0 {
k.RefreshRate = defaultRefreshRate
@ -202,44 +280,19 @@ func (k *K9s) validateDefaults() {
if k.MaxConnRetry <= 0 {
k.MaxConnRetry = defaultMaxConnRetry
}
if k.ScreenDumpDir == "" {
k.ScreenDumpDir = K9sDefaultScreenDumpDir
}
}
func (k *K9s) validateClusters(c client.Connection, ks KubeSettings) {
cc, err := ks.ClusterNames()
if err != nil {
return
}
for key, cluster := range k.Clusters {
cluster.Validate(c, ks)
// if the cluster is defined in the $KUBECONFIG file, keep it in the k9s config file
if _, ok := cc[key]; ok {
continue
}
// if we asked to keep the clusters in the config file
if k.KeepMissingClusters {
continue
}
// else remove it from the k9s config file
if k.CurrentCluster == key {
k.CurrentCluster = ""
}
delete(k.Clusters, key)
}
}
// Validate the current configuration.
func (k *K9s) Validate(c client.Connection, ks KubeSettings) {
func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) {
k.validateDefaults()
if k.Clusters == nil {
k.Clusters = map[string]*Cluster{}
if k.activeConfig == nil {
if n, err := ks.CurrentContextName(); err == nil {
_, _ = k.ActivateContext(n)
}
}
if k.ImageScans == nil {
k.ImageScans = NewImageScans()
}
k.validateClusters(c, ks)
if k.ShellPod == nil {
k.ShellPod = NewShellPod()
}
@ -254,18 +307,4 @@ func (k *K9s) Validate(c client.Connection, ks KubeSettings) {
k.Thresholds = NewThreshold()
}
k.Thresholds.Validate(c, ks)
if context, err := ks.CurrentContextName(); err == nil && len(k.CurrentContext) == 0 {
k.CurrentContext = context
k.CurrentCluster = ""
}
if cl, err := ks.CurrentClusterName(); err == nil && len(k.CurrentCluster) == 0 {
k.CurrentCluster = cl
}
if _, ok := k.Clusters[k.CurrentCluster]; !ok {
k.Clusters[k.CurrentCluster] = NewCluster()
}
k.Clusters[k.CurrentCluster].Validate(c, ks)
}

View File

@ -7,165 +7,37 @@ import (
"testing"
"github.com/derailed/k9s/internal/config"
m "github.com/petergtz/pegomock"
"github.com/derailed/k9s/internal/config/mock"
"github.com/stretchr/testify/assert"
)
func TestIsReadOnly(t *testing.T) {
uu := map[string]struct {
config string
read, write bool
readOnly bool
}{
"writable": {
config: "k9s.yml",
},
"writable_read_override": {
config: "k9s.yml",
read: true,
readOnly: true,
},
"writable_write_override": {
config: "k9s.yml",
write: true,
},
"readonly": {
config: "k9s_readonly.yml",
readOnly: true,
},
"readonly_read_override": {
config: "k9s_readonly.yml",
read: true,
readOnly: true,
},
"readonly_write_override": {
config: "k9s_readonly.yml",
write: true,
},
"readonly_both_override": {
config: "k9s_readonly.yml",
read: true,
write: true,
},
}
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Nil(t, cfg.Load("testdata/"+u.config))
cfg.K9s.OverrideReadOnly(u.read)
cfg.K9s.OverrideWrite(u.write)
assert.Equal(t, u.readOnly, cfg.K9s.IsReadOnly())
})
}
}
func TestK9sValidate(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings()
m.When(mk.CurrentContextName()).ThenReturn("ctx1", nil)
m.When(mk.CurrentClusterName()).ThenReturn("c1", nil)
m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"c1": {}, "c2": {}}, nil)
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"})
c := config.NewK9s()
c.Validate(mc, mk)
assert.Equal(t, 2, c.RefreshRate)
assert.Equal(t, int64(100), c.Logger.TailCount)
assert.Equal(t, 5000, c.Logger.BufferSize)
assert.Equal(t, "ctx1", c.CurrentContext)
assert.Equal(t, "c1", c.CurrentCluster)
assert.Equal(t, 1, len(c.Clusters))
assert.Equal(t, config.K9sDefaultScreenDumpDir, c.GetScreenDumpDir())
_, ok := c.Clusters[c.CurrentCluster]
assert.True(t, ok)
}
func TestK9sValidateBlank(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings()
m.When(mk.CurrentContextName()).ThenReturn("ctx1", nil)
m.When(mk.CurrentClusterName()).ThenReturn("c1", nil)
m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"c1": {}, "c2": {}}, nil)
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"})
var c config.K9s
c.Validate(mc, mk)
assert.Equal(t, 2, c.RefreshRate)
assert.Equal(t, int64(100), c.Logger.TailCount)
assert.Equal(t, 5000, c.Logger.BufferSize)
assert.Equal(t, "ctx1", c.CurrentContext)
assert.Equal(t, "c1", c.CurrentCluster)
assert.Equal(t, 1, len(c.Clusters))
_, ok := c.Clusters[c.CurrentCluster]
assert.True(t, ok)
}
func TestK9sActiveClusterZero(t *testing.T) {
c := config.NewK9s()
c.CurrentCluster = "fred"
cl := c.ActiveCluster()
assert.NotNil(t, cl)
assert.Equal(t, "default", cl.Namespace.Active)
assert.Equal(t, 1, len(cl.Namespace.Favorites))
}
func TestK9sActiveClusterBlank(t *testing.T) {
var c config.K9s
cl := c.ActiveCluster()
assert.Equal(t, config.NewCluster(), cl)
}
func TestK9sActiveCluster(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cl := cfg.K9s.ActiveCluster()
assert.NotNil(t, cl)
assert.Equal(t, "kube-system", cl.Namespace.Active)
assert.Equal(t, 5, len(cl.Namespace.Favorites))
}
func TestGetScreenDumpDir(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir())
}
func TestGetScreenDumpDirOverride(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cfg.K9s.OverrideScreenDumpDir("/override")
cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
cfg.K9s.OverrideScreenDumpDir("/override")
assert.Equal(t, "/override", cfg.K9s.GetScreenDumpDir())
}
func TestGetScreenDumpDirOverrideEmpty(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s.yml"))
cfg.K9s.OverrideScreenDumpDir("")
cfg := mock.NewMockConfig()
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
cfg.K9s.OverrideScreenDumpDir("")
assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir())
}
func TestGetScreenDumpDirEmpty(t *testing.T) {
mk := NewMockKubeSettings()
cfg := config.NewConfig(mk)
assert.Nil(t, cfg.Load("testdata/k9s1.yml"))
cfg.K9s.OverrideScreenDumpDir("")
cfg := mock.NewMockConfig()
assert.Equal(t, config.K9sDefaultScreenDumpDir, cfg.K9s.GetScreenDumpDir())
assert.Nil(t, cfg.Load("testdata/k9s1.yaml"))
cfg.K9s.OverrideScreenDumpDir("")
assert.Equal(t, config.AppDumpsDir, cfg.K9s.GetScreenDumpDir())
}

View File

@ -5,6 +5,7 @@ package config
import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
)
const (
@ -38,7 +39,7 @@ func NewLogger() *Logger {
}
// Validate checks thresholds and make sure we're cool. If not use defaults.
func (l *Logger) Validate(_ client.Connection, _ KubeSettings) {
func (l *Logger) Validate(_ client.Connection, _ data.KubeSettings) {
if l.TailCount <= 0 {
l.TailCount = DefaultLoggerTailCount
}

View File

@ -0,0 +1,161 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package mock
import (
"fmt"
"os"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
version "k8s.io/apimachinery/pkg/version"
"k8s.io/cli-runtime/pkg/genericclioptions"
disk "k8s.io/client-go/discovery/cached/disk"
dynamic "k8s.io/client-go/dynamic"
kubernetes "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd/api"
versioned "k8s.io/metrics/pkg/client/clientset/versioned"
)
func EnsureDir(d string) error {
if _, err := os.Stat(d); os.IsNotExist(err) {
return os.MkdirAll(d, 0700)
}
if err := os.RemoveAll(d); err != nil {
return err
}
return os.MkdirAll(d, 0700)
}
func NewMockConfig() *config.Config {
config.AppContextsDir = "/tmp/test"
cl, ct := "cl-1", "ct-1"
flags := genericclioptions.ConfigFlags{
ClusterName: &cl,
Context: &ct,
}
cfg := config.NewConfig(
NewMockKubeSettings(&flags),
)
return cfg
}
type mockKubeSettings struct {
flags *genericclioptions.ConfigFlags
cts map[string]*api.Context
}
func NewMockKubeSettings(f *genericclioptions.ConfigFlags) mockKubeSettings {
_, idx, _ := strings.Cut(*f.ClusterName, "-")
ctId := "ct-" + idx
return mockKubeSettings{
flags: f,
cts: map[string]*api.Context{
ctId + "-1": {
Cluster: *f.ClusterName,
Namespace: "",
},
ctId + "-2": {
Cluster: *f.ClusterName,
Namespace: "ns-1",
},
ctId + "-3": {
Cluster: *f.ClusterName,
Namespace: client.DefaultNamespace,
},
},
}
}
func (m mockKubeSettings) CurrentContextName() (string, error) {
return *m.flags.Context, nil
}
func (m mockKubeSettings) CurrentClusterName() (string, error) {
return *m.flags.ClusterName, nil
}
func (m mockKubeSettings) CurrentNamespaceName() (string, error) {
return "default", nil
}
func (m mockKubeSettings) GetContext(s string) (*api.Context, error) {
ct, ok := m.cts[s]
if !ok {
return nil, fmt.Errorf("no context found for: %q", s)
}
return ct, nil
}
func (m mockKubeSettings) CurrentContext() (*api.Context, error) {
return m.GetContext(*m.flags.Context)
}
func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) {
mm := make(map[string]struct{}, len(m.cts))
for k := range m.cts {
mm[k] = struct{}{}
}
return mm, nil
}
type mockConnection struct{}
func NewMockConnection() mockConnection {
return mockConnection{}
}
func (m mockConnection) CanI(ns, gvr string, verbs []string) (bool, error) {
return true, nil
}
func (m mockConnection) Config() *client.Config {
return nil
}
func (m mockConnection) ConnectionOK() bool {
return false
}
func (m mockConnection) Dial() (kubernetes.Interface, error) {
return nil, nil
}
func (m mockConnection) DialLogs() (kubernetes.Interface, error) {
return nil, nil
}
func (m mockConnection) SwitchContext(ctx string) error {
return nil
}
func (m mockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
return nil, nil
}
func (m mockConnection) RestConfig() (*restclient.Config, error) {
return nil, nil
}
func (m mockConnection) MXDial() (*versioned.Clientset, error) {
return nil, nil
}
func (m mockConnection) DynDial() (dynamic.Interface, error) {
return nil, nil
}
func (m mockConnection) HasMetrics() bool {
return false
}
func (m mockConnection) ValidNamespaceNames() (client.NamespaceNames, error) {
return nil, nil
}
func (m mockConnection) IsValidNamespace(string) bool {
return true
}
func (m mockConnection) ServerVersion() (*version.Info, error) {
return nil, nil
}
func (m mockConnection) CheckConnectivity() bool {
return false
}
func (m mockConnection) ActiveContext() string {
return ""
}
func (m mockConnection) ActiveNamespace() string {
return ""
}
func (m mockConnection) IsActiveNamespace(string) bool {
return false
}

View File

@ -1,674 +0,0 @@
// Code generated by pegomock. DO NOT EDIT.
// Source: github.com/derailed/k9s/internal/client (interfaces: Connection)
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config_test
import (
client "github.com/derailed/k9s/internal/client"
pegomock "github.com/petergtz/pegomock"
v1 "k8s.io/api/core/v1"
version "k8s.io/apimachinery/pkg/version"
disk "k8s.io/client-go/discovery/cached/disk"
dynamic "k8s.io/client-go/dynamic"
kubernetes "k8s.io/client-go/kubernetes"
rest "k8s.io/client-go/rest"
versioned "k8s.io/metrics/pkg/client/clientset/versioned"
"reflect"
"time"
)
type MockConnection struct {
fail func(message string, callerSkip ...int)
}
func NewMockConnection(options ...pegomock.Option) *MockConnection {
mock := &MockConnection{}
for _, option := range options {
option.Apply(mock)
}
return mock
}
func (mock *MockConnection) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockConnection) FailHandler() pegomock.FailHandler { return mock.fail }
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) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 *disk.CachedDiscoveryClient
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(*disk.CachedDiscoveryClient)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{_param0, _param1, _param2}
result := pegomock.GetGenericMockFrom(mock).Invoke("CanI", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 bool
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(bool)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) CheckConnectivity() bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("CheckConnectivity", 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) Config() *client.Config {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("Config", params, []reflect.Type{reflect.TypeOf((**client.Config)(nil)).Elem()})
var ret0 *client.Config
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(*client.Config)
}
}
return ret0
}
func (mock *MockConnection) ConnectionOK() bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("ConnectionOK", 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) Dial() (kubernetes.Interface, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("Dial", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 kubernetes.Interface
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(kubernetes.Interface)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) DialLogs() (kubernetes.Interface, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("DialLogs", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 kubernetes.Interface
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(kubernetes.Interface)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) DynDial() (dynamic.Interface, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("DynDial", params, []reflect.Type{reflect.TypeOf((*dynamic.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 dynamic.Interface
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(dynamic.Interface)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) HasMetrics() bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", 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) IsActiveNamespace(_param0 string) bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{_param0}
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) MXDial() (*versioned.Clientset, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("MXDial", params, []reflect.Type{reflect.TypeOf((**versioned.Clientset)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 *versioned.Clientset
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(*versioned.Clientset)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) RestConfig() (*rest.Config, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("RestConfig", params, []reflect.Type{reflect.TypeOf((**rest.Config)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 *rest.Config
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(*rest.Config)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) ServerVersion() (*version.Info, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("ServerVersion", params, []reflect.Type{reflect.TypeOf((**version.Info)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 *version.Info
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(*version.Info)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) SwitchContext(_param0 string) error {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{_param0}
result := pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var ret0 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(error)
}
}
return ret0
}
func (mock *MockConnection) ValidNamespaces() ([]v1.Namespace, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("ValidNamespaces", params, []reflect.Type{reflect.TypeOf((*[]v1.Namespace)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 []v1.Namespace
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].([]v1.Namespace)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockConnection) VerifyWasCalledOnce() *VerifierMockConnection {
return &VerifierMockConnection{
mock: mock,
invocationCountMatcher: pegomock.Times(1),
}
}
func (mock *MockConnection) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockConnection {
return &VerifierMockConnection{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
}
}
func (mock *MockConnection) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockConnection {
return &VerifierMockConnection{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
inOrderContext: inOrderContext,
}
}
func (mock *MockConnection) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockConnection {
return &VerifierMockConnection{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
timeout: timeout,
}
}
type VerifierMockConnection struct {
mock *MockConnection
invocationCountMatcher pegomock.Matcher
inOrderContext *pegomock.InOrderContext
timeout time.Duration
}
func (verifier *VerifierMockConnection) ActiveCluster() *MockConnection_ActiveCluster_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveCluster", params, verifier.timeout)
return &MockConnection_ActiveCluster_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_ActiveCluster_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_ActiveCluster_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_ActiveCluster_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) ActiveNamespace() *MockConnection_ActiveNamespace_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveNamespace", params, verifier.timeout)
return &MockConnection_ActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_ActiveNamespace_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_ActiveNamespace_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_ActiveNamespace_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) CachedDiscovery() *MockConnection_CachedDiscovery_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CachedDiscovery", params, verifier.timeout)
return &MockConnection_CachedDiscovery_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_CachedDiscovery_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_CachedDiscovery_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) CanI(_param0 string, _param1 string, _param2 []string) *MockConnection_CanI_OngoingVerification {
params := []pegomock.Param{_param0, _param1, _param2}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanI", params, verifier.timeout)
return &MockConnection_CanI_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_CanI_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_CanI_OngoingVerification) GetCapturedArguments() (string, string, []string) {
_param0, _param1, _param2 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1]
}
func (c *MockConnection_CanI_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]string, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(string)
}
_param1 = make([]string, len(c.methodInvocations))
for u, param := range params[1] {
_param1[u] = param.(string)
}
_param2 = make([][]string, len(c.methodInvocations))
for u, param := range params[2] {
_param2[u] = param.([]string)
}
}
return
}
func (verifier *VerifierMockConnection) CheckConnectivity() *MockConnection_CheckConnectivity_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckConnectivity", params, verifier.timeout)
return &MockConnection_CheckConnectivity_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_CheckConnectivity_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_CheckConnectivity_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_CheckConnectivity_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) Config() *MockConnection_Config_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout)
return &MockConnection_Config_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_Config_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_Config_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_Config_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) ConnectionOK() *MockConnection_ConnectionOK_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ConnectionOK", params, verifier.timeout)
return &MockConnection_ConnectionOK_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_ConnectionOK_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_ConnectionOK_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_ConnectionOK_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) Dial() *MockConnection_Dial_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Dial", params, verifier.timeout)
return &MockConnection_Dial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_Dial_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_Dial_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_Dial_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) DynDial() *MockConnection_DynDial_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDial", params, verifier.timeout)
return &MockConnection_DynDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_DynDial_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_DynDial_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_DynDial_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) HasMetrics() *MockConnection_HasMetrics_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout)
return &MockConnection_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_HasMetrics_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_HasMetrics_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_HasMetrics_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) IsActiveNamespace(_param0 string) *MockConnection_IsActiveNamespace_OngoingVerification {
params := []pegomock.Param{_param0}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsActiveNamespace", params, verifier.timeout)
return &MockConnection_IsActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_IsActiveNamespace_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetCapturedArguments() string {
_param0 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1]
}
func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]string, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(string)
}
}
return
}
func (verifier *VerifierMockConnection) MXDial() *MockConnection_MXDial_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MXDial", params, verifier.timeout)
return &MockConnection_MXDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_MXDial_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_MXDial_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_MXDial_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) RestConfig() *MockConnection_RestConfig_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfig", params, verifier.timeout)
return &MockConnection_RestConfig_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_RestConfig_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_RestConfig_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_RestConfig_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) ServerVersion() *MockConnection_ServerVersion_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ServerVersion", params, verifier.timeout)
return &MockConnection_ServerVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_ServerVersion_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_ServerVersion_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_ServerVersion_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockConnection) SwitchContext(_param0 string) *MockConnection_SwitchContext_OngoingVerification {
params := []pegomock.Param{_param0}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContext", params, verifier.timeout)
return &MockConnection_SwitchContext_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_SwitchContext_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_SwitchContext_OngoingVerification) GetCapturedArguments() string {
_param0 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1]
}
func (c *MockConnection_SwitchContext_OngoingVerification) GetAllCapturedArguments() (_param0 []string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]string, len(c.methodInvocations))
for u, param := range params[0] {
_param0[u] = param.(string)
}
}
return
}
func (verifier *VerifierMockConnection) ValidNamespaces() *MockConnection_ValidNamespaces_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidNamespaces", params, verifier.timeout)
return &MockConnection_ValidNamespaces_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockConnection_ValidNamespaces_OngoingVerification struct {
mock *MockConnection
methodInvocations []pegomock.MethodInvocation
}
func (c *MockConnection_ValidNamespaces_OngoingVerification) GetCapturedArguments() {
}
func (c *MockConnection_ValidNamespaces_OngoingVerification) GetAllCapturedArguments() {
}

View File

@ -1,252 +0,0 @@
// Code generated by pegomock. DO NOT EDIT.
// Source: github.com/derailed/k9s/internal/config (interfaces: KubeSettings)
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config_test
import (
pegomock "github.com/petergtz/pegomock"
v1 "k8s.io/api/core/v1"
"reflect"
"time"
)
type MockKubeSettings struct {
fail func(message string, callerSkip ...int)
}
func NewMockKubeSettings(options ...pegomock.Option) *MockKubeSettings {
mock := &MockKubeSettings{}
for _, option := range options {
option.Apply(mock)
}
return mock
}
func (mock *MockKubeSettings) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockKubeSettings) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockKubeSettings) ClusterNames() (map[string]struct{}, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockKubeSettings().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("ClusterNames", params, []reflect.Type{reflect.TypeOf((*map[string]struct{})(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 map[string]struct{}
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(map[string]struct{})
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockKubeSettings) CurrentClusterName() (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockKubeSettings().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentClusterName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockKubeSettings) CurrentContextName() (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockKubeSettings().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentContextName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockKubeSettings) CurrentNamespaceName() (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockKubeSettings().")
}
params := []pegomock.Param{}
result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentNamespaceName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var ret0 string
var ret1 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(string)
}
if result[1] != nil {
ret1 = result[1].(error)
}
}
return ret0, ret1
}
func (mock *MockKubeSettings) NamespaceNames(_param0 []v1.Namespace) []string {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockKubeSettings().")
}
params := []pegomock.Param{_param0}
result := pegomock.GetGenericMockFrom(mock).Invoke("NamespaceNames", 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 *MockKubeSettings) VerifyWasCalledOnce() *VerifierMockKubeSettings {
return &VerifierMockKubeSettings{
mock: mock,
invocationCountMatcher: pegomock.Times(1),
}
}
func (mock *MockKubeSettings) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockKubeSettings {
return &VerifierMockKubeSettings{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
}
}
func (mock *MockKubeSettings) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockKubeSettings {
return &VerifierMockKubeSettings{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
inOrderContext: inOrderContext,
}
}
func (mock *MockKubeSettings) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockKubeSettings {
return &VerifierMockKubeSettings{
mock: mock,
invocationCountMatcher: invocationCountMatcher,
timeout: timeout,
}
}
type VerifierMockKubeSettings struct {
mock *MockKubeSettings
invocationCountMatcher pegomock.Matcher
inOrderContext *pegomock.InOrderContext
timeout time.Duration
}
func (verifier *VerifierMockKubeSettings) ClusterNames() *MockKubeSettings_ClusterNames_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterNames", params, verifier.timeout)
return &MockKubeSettings_ClusterNames_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockKubeSettings_ClusterNames_OngoingVerification struct {
mock *MockKubeSettings
methodInvocations []pegomock.MethodInvocation
}
func (c *MockKubeSettings_ClusterNames_OngoingVerification) GetCapturedArguments() {
}
func (c *MockKubeSettings_ClusterNames_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockKubeSettings) CurrentClusterName() *MockKubeSettings_CurrentClusterName_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentClusterName", params, verifier.timeout)
return &MockKubeSettings_CurrentClusterName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockKubeSettings_CurrentClusterName_OngoingVerification struct {
mock *MockKubeSettings
methodInvocations []pegomock.MethodInvocation
}
func (c *MockKubeSettings_CurrentClusterName_OngoingVerification) GetCapturedArguments() {
}
func (c *MockKubeSettings_CurrentClusterName_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockKubeSettings) CurrentContextName() *MockKubeSettings_CurrentContextName_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentContextName", params, verifier.timeout)
return &MockKubeSettings_CurrentContextName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockKubeSettings_CurrentContextName_OngoingVerification struct {
mock *MockKubeSettings
methodInvocations []pegomock.MethodInvocation
}
func (c *MockKubeSettings_CurrentContextName_OngoingVerification) GetCapturedArguments() {
}
func (c *MockKubeSettings_CurrentContextName_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockKubeSettings) CurrentNamespaceName() *MockKubeSettings_CurrentNamespaceName_OngoingVerification {
params := []pegomock.Param{}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentNamespaceName", params, verifier.timeout)
return &MockKubeSettings_CurrentNamespaceName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockKubeSettings_CurrentNamespaceName_OngoingVerification struct {
mock *MockKubeSettings
methodInvocations []pegomock.MethodInvocation
}
func (c *MockKubeSettings_CurrentNamespaceName_OngoingVerification) GetCapturedArguments() {
}
func (c *MockKubeSettings_CurrentNamespaceName_OngoingVerification) GetAllCapturedArguments() {
}
func (verifier *VerifierMockKubeSettings) NamespaceNames(_param0 []v1.Namespace) *MockKubeSettings_NamespaceNames_OngoingVerification {
params := []pegomock.Param{_param0}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NamespaceNames", params, verifier.timeout)
return &MockKubeSettings_NamespaceNames_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
type MockKubeSettings_NamespaceNames_OngoingVerification struct {
mock *MockKubeSettings
methodInvocations []pegomock.MethodInvocation
}
func (c *MockKubeSettings_NamespaceNames_OngoingVerification) GetCapturedArguments() []v1.Namespace {
_param0 := c.GetAllCapturedArguments()
return _param0[len(_param0)-1]
}
func (c *MockKubeSettings_NamespaceNames_OngoingVerification) GetAllCapturedArguments() (_param0 [][]v1.Namespace) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([][]v1.Namespace, len(params[0]))
for u, param := range params[0] {
_param0[u] = param.([]v1.Namespace)
}
}
return
}

View File

@ -1,91 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config_test
import (
"fmt"
"testing"
"github.com/derailed/k9s/internal/config"
m "github.com/petergtz/pegomock"
"github.com/stretchr/testify/assert"
)
func TestNSValidate(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings()
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"})
ns := config.NewNamespace()
ns.Validate(mc, mk)
mk.VerifyWasCalledOnce()
assert.Equal(t, "default", ns.Active)
assert.Equal(t, []string{"default"}, ns.Favorites)
}
func TestNSValidateMissing(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings()
ns := config.NewNamespace()
ns.Validate(mc, mk)
assert.Equal(t, "default", ns.Active)
assert.Equal(t, []string{"default"}, ns.Favorites)
}
func TestNSValidateNoNS(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), fmt.Errorf("Crap!"))
mk := NewMockKubeSettings()
m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2"})
ns := config.NewNamespace()
ns.Validate(mc, mk)
mk.VerifyWasCalledOnce()
assert.Equal(t, "default", ns.Active)
assert.Equal(t, []string{"default"}, ns.Favorites)
}
func TestNSSetActive(t *testing.T) {
allNS := []string{"ns4", "ns3", "ns2", "ns1", "all", "default"}
uu := []struct {
ns string
fav []string
}{
{"all", []string{"all", "default"}},
{"ns1", []string{"ns1", "all", "default"}},
{"ns2", []string{"ns2", "ns1", "all", "default"}},
{"ns3", []string{"ns3", "ns2", "ns1", "all", "default"}},
{"ns4", allNS},
}
mk := NewMockKubeSettings()
m.When(mk.NamespaceNames(namespaces())).ThenReturn(allNS)
ns := config.NewNamespace()
for _, u := range uu {
err := ns.SetActive(u.ns, mk)
assert.Nil(t, err)
assert.Equal(t, u.ns, ns.Active)
assert.Equal(t, u.fav, ns.Favorites)
}
}
func TestNSValidateRmFavs(t *testing.T) {
mc := NewMockConnection()
m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil)
mk := NewMockKubeSettings()
ns := config.NewNamespace()
ns.Favorites = []string{"default", "fred", "blee"}
ns.Validate(mc, mk)
assert.Equal(t, []string{"default", "fred"}, ns.Favorites)
}

View File

@ -4,6 +4,7 @@
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
@ -13,13 +14,11 @@ import (
"gopkg.in/yaml.v2"
)
// K9sPluginsFilePath manages K9s plugins.
var K9sPluginsFilePath = YamlExtension(filepath.Join(K9sHome(), "plugin.yml"))
var K9sPluginDirectory = filepath.Join("k9s", "plugins")
const k9sPluginsDir = "k9s/plugins"
// Plugins represents a collection of plugins.
type Plugins struct {
Plugin map[string]Plugin `yaml:"plugin"`
Plugins map[string]Plugin `yaml:"plugins"`
}
// Plugin describes a K9s plugin.
@ -41,22 +40,58 @@ func (p Plugin) String() string {
// NewPlugins returns a new plugin.
func NewPlugins() Plugins {
return Plugins{
Plugin: make(map[string]Plugin),
Plugins: make(map[string]Plugin),
}
}
// Load K9s plugins.
func (p Plugins) Load() error {
pluginDirs := make([]string, 0, len(xdg.DataDirs))
for _, dataDir := range xdg.DataDirs {
pluginDirs = append(pluginDirs, filepath.Join(dataDir, K9sPluginDirectory))
func (p Plugins) Load(path string) error {
var errs error
if err := p.load(AppPluginsFile); err != nil {
errs = errors.Join(errs, err)
}
if err := p.load(path); err != nil {
errs = errors.Join(errs, err)
}
return p.LoadPlugins(K9sPluginsFilePath, pluginDirs)
for _, dataDir := range xdg.DataDirs {
if err := p.loadPluginDir(filepath.Join(dataDir, k9sPluginsDir)); err != nil {
errs = errors.Join(errs, err)
}
}
return errs
}
// LoadPlugins loads plugins from a given file and a set of plugin directories.
func (p Plugins) LoadPlugins(path string, pluginDirs []string) error {
func (p Plugins) loadPluginDir(dir string) error {
pluginFiles, err := os.ReadDir(dir)
if err != nil {
return nil
}
var errs error
for _, file := range pluginFiles {
if file.IsDir() || !isYamlFile(file.Name()) {
continue
}
pluginFile, err := os.ReadFile(filepath.Join(dir, file.Name()))
if err != nil {
errs = errors.Join(errs, err)
}
var plugin Plugin
if err = yaml.Unmarshal(pluginFile, &plugin); err != nil {
return err
}
p.Plugins[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin
}
return errs
}
func (p *Plugins) load(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil
}
f, err := os.ReadFile(path)
if err != nil {
return err
@ -66,29 +101,8 @@ func (p Plugins) LoadPlugins(path string, pluginDirs []string) error {
if err := yaml.Unmarshal(f, &pp); err != nil {
return err
}
for k, v := range pp.Plugin {
p.Plugin[k] = v
}
for _, pluginDir := range pluginDirs {
pluginFiles, err := os.ReadDir(pluginDir)
if err != nil {
continue
}
for _, file := range pluginFiles {
if file.IsDir() || !isYamlFile(file.Name()) {
continue
}
pluginFile, err := os.ReadFile(filepath.Join(pluginDir, file.Name()))
if err != nil {
return err
}
var plugin Plugin
if err = yaml.Unmarshal(pluginFile, &plugin); err != nil {
return err
}
p.Plugin[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin
}
for k, v := range pp.Plugins {
p.Plugins[k] = v
}
return nil

View File

@ -1,16 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config_test
package config
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
)
var pluginYmlTestData = config.Plugin{
var pluginYmlTestData = Plugin{
Scopes: []string{"po", "dp"},
Args: []string{"-n", "$NAMESPACE", "-boolean"},
ShortCut: "shift-s",
@ -20,7 +19,7 @@ var pluginYmlTestData = config.Plugin{
Background: false,
}
var test1YmlTestData = config.Plugin{
var test1YmlTestData = Plugin{
Scopes: []string{"po", "dp"},
Args: []string{"-n", "$NAMESPACE", "-boolean"},
ShortCut: "shift-s",
@ -30,7 +29,7 @@ var test1YmlTestData = config.Plugin{
Background: false,
}
var test2YmlTestData = config.Plugin{
var test2YmlTestData = Plugin{
Scopes: []string{"svc", "ing"},
Args: []string{"-n", "$NAMESPACE", "-oyaml"},
ShortCut: "shift-r",
@ -41,29 +40,31 @@ var test2YmlTestData = config.Plugin{
}
func TestSinglePluginFileLoad(t *testing.T) {
p := config.NewPlugins()
assert.Nil(t, p.LoadPlugins("testdata/plugin.yml", []string{"/random/dir/not/exist"}))
p := NewPlugins()
assert.Nil(t, p.load("testdata/plugins.yaml"))
assert.Nil(t, p.loadPluginDir("/random/dir/not/exist"))
assert.Equal(t, 1, len(p.Plugin))
k, ok := p.Plugin["blah"]
assert.Equal(t, 1, len(p.Plugins))
k, ok := p.Plugins["blah"]
assert.True(t, ok)
assert.ObjectsAreEqual(pluginYmlTestData, k)
}
func TestMultiplePluginFilesLoad(t *testing.T) {
p := config.NewPlugins()
assert.Nil(t, p.LoadPlugins("testdata/plugin.yml", []string{"testdata/plugins"}))
p := NewPlugins()
assert.Nil(t, p.load("testdata/plugins.yaml"))
assert.Nil(t, p.loadPluginDir("testdata/plugins"))
testPlugins := map[string]config.Plugin{
testPlugins := map[string]Plugin{
"blah": pluginYmlTestData,
"test1": test1YmlTestData,
"test2": test2YmlTestData,
}
assert.Equal(t, len(testPlugins), len(p.Plugin))
assert.Equal(t, len(testPlugins), len(p.Plugins))
for name, expectedPlugin := range testPlugins {
k, ok := p.Plugin[name]
k, ok := p.Plugins[name]
assert.True(t, ok)
assert.ObjectsAreEqual(expectedPlugin, k)
}

71
internal/config/scans.go Normal file
View File

@ -0,0 +1,71 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config
// Labels tracks a collection of labels.
type Labels map[string][]string
func (l Labels) exclude(k, val string) bool {
vv, ok := l[k]
if !ok {
return false
}
for _, v := range vv {
if v == val {
return true
}
}
return false
}
// Blacklist tracks vul scan exclusions.
type BlackList struct {
Namespaces []string `yaml:"namespaces"`
Labels Labels `yaml:"labels"`
}
func newBlackList() BlackList {
return BlackList{
Labels: make(Labels),
}
}
func (b BlackList) exclude(ns string, ll map[string]string) bool {
for _, nss := range b.Namespaces {
if nss == ns {
return true
}
}
for k, v := range ll {
if b.Labels.exclude(k, v) {
return true
}
}
return false
}
// ImageScans tracks vul scans options.
type ImageScans struct {
Enable bool `yaml:"enable"`
BlackList BlackList `yaml:"blackList"`
}
// NewImageScans returns a new instance.
func NewImageScans() *ImageScans {
return &ImageScans{
BlackList: newBlackList(),
}
}
// ShouldExclude checks if scan should be excluder given ns/labels
func (i *ImageScans) ShouldExclude(ns string, ll map[string]string) bool {
if !i.Enable {
return false
}
return i.BlackList.exclude(ns, ll)
}

View File

@ -5,6 +5,7 @@ package config
import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
v1 "k8s.io/api/core/v1"
)
@ -36,7 +37,7 @@ func NewShellPod() *ShellPod {
}
// Validate validates the configuration.
func (s *ShellPod) Validate(client.Connection, KubeSettings) {
func (s *ShellPod) Validate(client.Connection, data.KubeSettings) {
if s.Image == "" {
s.Image = defaultDockerShellImage
}

View File

@ -5,16 +5,12 @@ package config
import (
"os"
"path/filepath"
"github.com/derailed/tcell/v2"
"github.com/derailed/tview"
"gopkg.in/yaml.v2"
)
// K9sStylesFile represents K9s skins file location.
var K9sStylesFile = YamlExtension(filepath.Join(K9sHome(), "skin.yml"))
// StyleListener represents a skin's listener.
type StyleListener interface {
// StylesChanged notifies listener the skin changed.
@ -434,6 +430,11 @@ func newMenu() Menu {
// NewStyles creates a new default config.
func NewStyles() *Styles {
var s Styles
if err := yaml.Unmarshal(stockSkinTpl, &s); err == nil {
return &s
}
return &Styles{
K9s: newStyle(),
}
@ -446,7 +447,6 @@ func (s *Styles) Reset() {
// DefaultSkin loads the default skin.
func (s *Styles) DefaultSkin() {
s.K9s = newStyle()
}
// FgColor returns the foreground color.
@ -545,7 +545,6 @@ func (s *Styles) Load(path string) error {
if err := yaml.Unmarshal(f, s); err != nil {
return err
}
// s.fireStylesChanged()
return nil
}

View File

@ -30,7 +30,7 @@ func TestColor(t *testing.T) {
func TestSkinNone(t *testing.T) {
s := config.NewStyles()
assert.Nil(t, s.Load("testdata/empty_skin.yml"))
assert.Nil(t, s.Load("testdata/empty_skin.yaml"))
s.Update()
assert.Equal(t, "#5f9ea0", s.Body().FgColor.String())
@ -43,7 +43,7 @@ func TestSkinNone(t *testing.T) {
func TestSkin(t *testing.T) {
s := config.NewStyles()
assert.Nil(t, s.Load("testdata/black_and_wtf.yml"))
assert.Nil(t, s.Load("testdata/black_and_wtf.yaml"))
s.Update()
assert.Equal(t, "#ffffff", s.Body().FgColor.String())
@ -56,10 +56,10 @@ func TestSkin(t *testing.T) {
func TestSkinNotExits(t *testing.T) {
s := config.NewStyles()
assert.NotNil(t, s.Load("testdata/blee.yml"))
assert.NotNil(t, s.Load("testdata/blee.yaml"))
}
func TestSkinBoarked(t *testing.T) {
s := config.NewStyles()
assert.NotNil(t, s.Load("testdata/skin_boarked.yml"))
assert.NotNil(t, s.Load("testdata/skin_boarked.yaml"))
}

View File

@ -0,0 +1,9 @@
aliases:
dp: apps/v1/deployments
sec: v1/secrets
jo: batch/v1/jobs
cr: rbac.authorization.k8s.io/v1/clusterroles
crb: rbac.authorization.k8s.io/v1/clusterrolebindings
ro: rbac.authorization.k8s.io/v1/roles
rb: rbac.authorization.k8s.io/v1/rolebindings
np: networking.k8s.io/v1/networkpolicies

View File

@ -0,0 +1,4 @@
benchmarks:
defaults:
concurrency: 2
requests: 200

View File

@ -0,0 +1,6 @@
hotKeys:
# Examples...
# shift-0:
# shortCut: Shift-0
# description: View Workloads
# command: wk k8s-app=cilium

View File

@ -0,0 +1,97 @@
# -----------------------------------------------------------------------------
# Stock skin
# -----------------------------------------------------------------------------
# Skin...
k9s:
body:
fgColor: cadetblue
bgColor: black
logoColor: orange
logoColorMsg: white
logoColorInfo: green
logoColorWarn: mediumvioletred
logoColorError: red
prompt:
fgColor: cadetblue
bgColor: black
suggestColor: dodgerblue
border:
default: seagreen
command: aqua
info:
fgColor: orange
sectionColor: white
dialog:
fgColor: dodgerblue
bgColor: black
buttonFgColor: black
buttonBgColor: dodgerblue
buttonFocusFgColor: white
buttonFocusBgColor: fuchsia
labelFgColor: fuchsia
fieldFgColor: dodgerblue
frame:
border:
fgColor: dodgerblue
focusColor: aqua
menu:
fgColor: white
keyColor: dodgerblue
numKeyColor: fuchsia
crumbs:
fgColor: black
bgColor: aqua
activeColor: orange
status:
newColor: lightskyblue
modifyColor: greenyellow
addColor: white
errorColor: orangered
pendingColor: darkorange
highlightColor: aqua
killColor: mediumpurple
completedColor: gray
title:
fgColor: aqua
highlightColor: fuchsia
counterColor: papayawhip
filterColor: steelblue
views:
# Charts skins...
charts:
bgColor: black
defaultDialColors:
- linegreen
- orangered
defaultChartColors:
- linegreen
- orangered
table:
fgColor: aqua
bgColor: black
cursorFgColor: white
cursorBgColor: black
markColor: darkgoldenrod
header:
fgColor: lightGray
bgColor: black
sorterColor: orange
xray:
fgColor: blue
bgColor: black
cursorColor: aqua
graphicColor: darkgoldenrod
showIcons: false
yaml:
keyColor: steelblue
colonColor: white
valueColor: papayawhip
logs:
fgColor: white
bgColor: black
indicator:
fgColor: dodgerblue
bgColor: black
toggleOnColor: limegreen
toggleOffColor: steelblue

View File

@ -1,3 +1,3 @@
alias:
aliases:
dp: "apps.v1.deployments"
pe: ".v1.pods"

View File

@ -1,4 +1,4 @@
hotKey:
hotKeys:
pods:
shortCut: shift-0
description: Launch pod view

View File

@ -6,9 +6,9 @@ k9s:
tail: 200
buffer: 2000
currentContext: minikube
currentCluster: minikube
clusters:
contexts:
minikube:
cluster: minikube
namespace:
active: kube-system
favorites:
@ -20,6 +20,7 @@ k9s:
view:
active: ctx
fred:
cluster: fred
namespace:
active: default
favorites:

View File

@ -5,8 +5,7 @@ k9s:
tail: 200
buffer: 2000
currentContext: minikube
currentCluster: minikube
clusters:
contexts:
minikube:
namespace:
active: kube-system

View File

@ -2,8 +2,7 @@ k9s:
refreshRate: 2
logBufferSize: 200
currentContext: minikube
currentCluster: minikube
clusters:
contexts:
minikube:
namespace:
active: kube-system

View File

@ -4,19 +4,19 @@ clusters:
- cluster:
certificate-authority: /Users/test/ca.crt
server: https://1.2.3.4:8443
name: testCluster
name: cl-1
contexts:
- context:
cluster: cluster1
cluster: cl-1
user: user1
namespace: ns1
name: test1
namespace: ns-1
name: ct-1
- context:
cluster: cluster2
cluster: cl-1
user: user2
namespace: ns2
name: test2
current-context: test1
namespace: ns-2
name: ct-2
current-context: ct-1
preferences: {}
users:
- name: user1

View File

@ -1,4 +1,4 @@
plugin:
plugins:
blah:
shortCut: shift-s
confirm: true

View File

@ -5,6 +5,7 @@ package config
import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config/data"
)
const (
@ -65,7 +66,7 @@ func NewThreshold() Threshold {
}
// Validate a namespace is setup correctly.
func (t Threshold) Validate(c client.Connection, ks KubeSettings) {
func (t Threshold) Validate(c client.Connection, ks data.KubeSettings) {
for _, k := range []string{"cpu", "memory"} {
v, ok := t[k]
if !ok {

31
internal/config/types.go Normal file
View File

@ -0,0 +1,31 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package config
const (
defaultRefreshRate = 2
defaultMaxConnRetry = 5
)
// UI tracks ui specific configs.
type UI struct {
// EnableMouse toggles mouse support.
EnableMouse bool `yaml:"enableMouse"`
// Headless toggles top header display.
Headless bool `yaml:"headless"`
// LogoLess toggles k9s logo.
Logoless bool `yaml:"logoless"`
// Crumbsless toggles nav crumb display.
Crumbsless bool `yaml:"crumbsless"`
// NoIcons toggles icons display.
NoIcons bool `yaml:"noIcons"`
// Skin reference the general k9s skin name.
// Can be overridden per context.
Skin string `yaml:"skin,omitempty"`
}

View File

@ -5,17 +5,13 @@ package config
import (
"os"
"path/filepath"
"gopkg.in/yaml.v2"
)
// K9sViewConfigFile represents the location for the views configuration.
var K9sViewConfigFile = YamlExtension(filepath.Join(K9sHome(), "views.yml"))
// ViewConfigListener represents a view config listener.
type ViewConfigListener interface {
// ConfigChanged notifies listener the view configuration changed.
// ViewSettingsChanged notifies listener the view configuration changed.
ViewSettingsChanged(ViewSetting)
}
@ -90,6 +86,8 @@ func (v *CustomView) fireConfigChanged() {
for gvr, list := range v.listeners {
if v, ok := v.K9s.Views[gvr]; ok {
list.ViewSettingsChanged(v)
} else {
list.ViewSettingsChanged(ViewSetting{})
}
}
}

View File

@ -13,7 +13,7 @@ import (
func TestViewSettingsLoad(t *testing.T) {
cfg := config.NewCustomView()
assert.Nil(t, cfg.Load("testdata/view_settings.yml"))
assert.Nil(t, cfg.Load("testdata/view_settings.yaml"))
assert.Equal(t, 1, len(cfg.K9s.Views))
assert.Equal(t, 4, len(cfg.K9s.Views["v1/pods"].Columns))
}

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