K9s/release v0.31.0 (#2440)
* cleaning up * [Bug] Fix #2425 - no context skin issue * [Bug] Fix #2428 * [Feat] schema validation * v0.31.0 Release notesmine
parent
ffd8d51a8b
commit
6cc4374e83
|
|
@ -25,6 +25,9 @@ Steps to reproduce the behavior:
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
|
**Historical Documents**
|
||||||
|
When applicable please include any supporting artifacts: k9s debug logs, configurations, resource manifests, ...
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
|
||||||
else
|
else
|
||||||
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
endif
|
endif
|
||||||
VERSION ?= v0.30.8
|
VERSION ?= v0.31.0
|
||||||
IMG_NAME := derailed/k9s
|
IMG_NAME := derailed/k9s
|
||||||
IMAGE := ${IMG_NAME}:${VERSION}
|
IMAGE := ${IMG_NAME}:${VERSION}
|
||||||
|
|
||||||
|
|
|
||||||
48
README.md
48
README.md
|
|
@ -225,13 +225,11 @@ Binaries for Linux, Windows and Mac are available as tarballs in the [release pa
|
||||||
export TERM=xterm-256color
|
export TERM=xterm-256color
|
||||||
```
|
```
|
||||||
|
|
||||||
* In order to issue manifest edit commands make sure your EDITOR env is set.
|
* In order to issue resource edit commands make sure your EDITOR and KUBE_EDITOR env vars are set.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Kubectl edit command will use this env var.
|
# Kubectl edit command will use this env var.
|
||||||
export EDITOR=my_fav_editor
|
export KUBE_EDITOR=my_fav_editor
|
||||||
# Should your editor deal with streamed vs on disk files differently, also set...
|
|
||||||
export K9S_EDITOR=my_fav_editor
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* K9s prefers recent kubernetes versions ie 1.28+
|
* K9s prefers recent kubernetes versions ie 1.28+
|
||||||
|
|
@ -607,24 +605,23 @@ Here is a sample views configuration that customize a pods and services views.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# $XDG_CONFIG_HOME/k9s/views.yaml
|
# $XDG_CONFIG_HOME/k9s/views.yaml
|
||||||
k9s:
|
views:
|
||||||
views:
|
v1/pods:
|
||||||
v1/pods:
|
columns:
|
||||||
columns:
|
- AGE
|
||||||
- AGE
|
- NAMESPACE
|
||||||
- NAMESPACE
|
- NAME
|
||||||
- NAME
|
- IP
|
||||||
- IP
|
- NODE
|
||||||
- NODE
|
- STATUS
|
||||||
- STATUS
|
- READY
|
||||||
- READY
|
v1/services:
|
||||||
v1/services:
|
columns:
|
||||||
columns:
|
- AGE
|
||||||
- AGE
|
- NAMESPACE
|
||||||
- NAMESPACE
|
- NAME
|
||||||
- NAME
|
- TYPE
|
||||||
- TYPE
|
- CLUSTER-IP
|
||||||
- CLUSTER-IP
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -897,6 +894,7 @@ k9s:
|
||||||
You can also specify a default skin for all contexts in the root k9s config file as so:
|
You can also specify a default skin for all contexts in the root k9s config file as so:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
# $XDG_CONFIG_HOME/k9s/config.yaml
|
||||||
k9s:
|
k9s:
|
||||||
liveViewAutoRefresh: false
|
liveViewAutoRefresh: false
|
||||||
screenDumpDir: /tmp/dumps
|
screenDumpDir: /tmp/dumps
|
||||||
|
|
@ -910,6 +908,8 @@ k9s:
|
||||||
logoless: false
|
logoless: false
|
||||||
crumbsless: false
|
crumbsless: false
|
||||||
noIcons: false
|
noIcons: false
|
||||||
|
# Toggles reactive UI. This option provide for watching on disk artifacts changes and update the UI live Defaults to false.
|
||||||
|
reactive: false
|
||||||
# By default all contexts wil use the dracula skin unless explicitly overridden in the context config file.
|
# By default all contexts wil use the dracula skin unless explicitly overridden in the context config file.
|
||||||
skin: dracula # => assumes the file skins/dracula.yaml is present in the $XDG_DATA_HOME/k9s/skins directory
|
skin: dracula # => assumes the file skins/dracula.yaml is present in the $XDG_DATA_HOME/k9s/skins directory
|
||||||
skipLatestRevCheck: false
|
skipLatestRevCheck: false
|
||||||
|
|
@ -929,7 +929,7 @@ k9s:
|
||||||
tail: 100
|
tail: 100
|
||||||
buffer: 5000
|
buffer: 5000
|
||||||
sinceSeconds: -1
|
sinceSeconds: -1
|
||||||
fullScreenLogs: false
|
fullScreen: false
|
||||||
textWrap: false
|
textWrap: false
|
||||||
showTime: false
|
showTime: false
|
||||||
thresholds:
|
thresholds:
|
||||||
|
|
@ -942,7 +942,7 @@ k9s:
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# $XDG_DATA_HOME/k9s/skins/in_the_navy.yaml
|
# $XDG_DATA_HOME/k9s/skins/in_the_navy.yaml
|
||||||
# Skin InTheNavy!
|
# Skin InTheNavy!
|
||||||
k9s:
|
k9s:
|
||||||
# General K9s styles
|
# General K9s styles
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
|
||||||
|
|
||||||
|
# Release v0.31.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 ♭
|
||||||
|
|
||||||
|
* [Border Crossing - Eek A Mouse](https://www.youtube.com/watch?v=KaAC9dBPcOM)
|
||||||
|
* [The Weight - The Band](https://www.youtube.com/watch?v=FFqb1I-hiHE)
|
||||||
|
* [Wonderin' - Neil Young](https://www.youtube.com/watch?v=h0PlwVPbM5k)
|
||||||
|
* [When Your Lover Has Gone - Louis Armstrong](https://www.youtube.com/watch?v=1tdfIj0fvlA)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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!!
|
||||||
|
|
||||||
|
* [Jacky Nguyen](https://github.com/nktpro)
|
||||||
|
* [Eckl, Máté](https://github.com/ecklm)
|
||||||
|
* [Jörgen](https://github.com/wthrbtn)
|
||||||
|
* [kmath313](https://github.com/kmath313)
|
||||||
|
* [a-thomas-22](https://github.com/a-thomas-22)
|
||||||
|
* [wpbeckwith](https://github.com/wpbeckwith)
|
||||||
|
* [Dima Altukhov](https://github.com/alt-dima)
|
||||||
|
* [Shoshin Nikita](https://github.com/ShoshinNikita)
|
||||||
|
* [Tu Hoang](https://github.com/rebyn)
|
||||||
|
* [Andreas Frangopoulos](https://github.com/qubeio)
|
||||||
|
|
||||||
|
> Sponsorship cancellations since the last release: **7!** 🥹
|
||||||
|
|
||||||
|
## Feature Release!
|
||||||
|
|
||||||
|
😳 Found a few issues in the neutrino drive...
|
||||||
|
This is another fairly heavy drop so bracing for impact 😱
|
||||||
|
Be sure to dial in the v0.31.0 SneakPeek video below for the gory details!
|
||||||
|
|
||||||
|
😵 Hopefully we've move the needle in the right direction on this drop... 🤞
|
||||||
|
|
||||||
|
Thank you all for your kindness, feedback and assistance in flushing out issues!!
|
||||||
|
|
||||||
|
### Hold My Hand...
|
||||||
|
|
||||||
|
In this drop, we've added schema validation to ensure various configs are setup as expected.
|
||||||
|
K9s will now run validation checks on the following configurations:
|
||||||
|
|
||||||
|
1. K9s main configuration (config.yaml)
|
||||||
|
2. Context specific configs (clusterX/contextY/config.yaml)
|
||||||
|
3. Skins
|
||||||
|
4. Aliases
|
||||||
|
5. HotKeys
|
||||||
|
6. Plugins
|
||||||
|
7. Views
|
||||||
|
|
||||||
|
K9s behavior changed in this release if the main configuration does not match schema expectations.
|
||||||
|
In the past, the configuration will be validated, updated and saved should validation checks failed. Now the app will stop and report validation issues.
|
||||||
|
|
||||||
|
The schemas are set to be a bit loose for the time being. Once we/ve vetted they are cool, we could publish them out (with additional TLC!) so k9s users can leverage them in their favorite editors.
|
||||||
|
|
||||||
|
In the meantime, you'll need to keep k9s logs handy, to check for validation errors. The validation messages can be somewhat cryptic at times and so please be sure to include your debug logs and config settings when reporting issues which might be plenty ;(.
|
||||||
|
|
||||||
|
### Breaking Bad!
|
||||||
|
|
||||||
|
Configuration changes:
|
||||||
|
|
||||||
|
1. DRY fullScreenLogs -> fullScreens (k9s root config.yaml)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# $XDG_CONFIG_HOME/k9s/config.yaml
|
||||||
|
k9s:
|
||||||
|
liveViewAutoRefresh: false
|
||||||
|
logger:
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreen: false # => Was fullScreenLogs
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Views Configuration.
|
||||||
|
To match other configurations the root is now `views:` vs `k9s: views:`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# $XDG_CONFIG_HOME/k9s/views.yaml
|
||||||
|
views: # => Was k9s:\n views:
|
||||||
|
v1/pods:
|
||||||
|
columns:
|
||||||
|
- AGE
|
||||||
|
- NAMESPACE
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Serenity Now!
|
||||||
|
|
||||||
|
You can now opt in/out of the `reactive ui` feature. This feature enable users to make change to some configurations and see changes reflected live in the ui. This feature is now disabled by default and one must opt-in to enable via `k9s.UI.reactive`
|
||||||
|
Reactive UI provides for monitoring various config files on disk and update the UI when changes to those files occur. This is handy while tuning skins, plugins, aliases, hotkeys and benchmarks parameters.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# $XDG_CONFIG_HOME/k9s/config.yaml
|
||||||
|
k9s:
|
||||||
|
liveViewAutoRefresh: false
|
||||||
|
UI:
|
||||||
|
...
|
||||||
|
reactive: true # => enable/disable reactive UI
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Videos Are In The Can!
|
||||||
|
|
||||||
|
Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
|
||||||
|
|
||||||
|
* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)
|
||||||
|
* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
|
||||||
|
* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolved Issues
|
||||||
|
|
||||||
|
* [#2434](https://github.com/derailed/k9s/issues/2434) readOnly: true in config.yaml doesnt get overriden by readOnly: false in cluster config
|
||||||
|
* [#2430](https://github.com/derailed/k9s/issues/2430) Referencing a namespace with the name of an alias inside an alias causes infinite loop
|
||||||
|
* [#2428](https://github.com/derailed/k9s/issues/2428) Boom!! runtime error: invalid memory address or nil pointer dereference - v0.30.8
|
||||||
|
* [#2421](https://github.com/derailed/k9s/issues/2421) k9s/config.yaml configuration file is overwritten on launch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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!!
|
||||||
|
|
||||||
|
* [#2433](https://github.com/derailed/k9s/pull/2433) switch contexts only when needed
|
||||||
|
* [#2429](https://github.com/derailed/k9s/pull/2429) Reference correct configuration ENV var in README
|
||||||
|
* [#2426](https://github.com/derailed/k9s/pull/2426) Update carvel plugin kick to shift K
|
||||||
|
* [#2420](https://github.com/derailed/k9s/pull/2420) supports referencing envs in hotkeys
|
||||||
|
* [#2419](https://github.com/derailed/k9s/pull/2419) fix typo
|
||||||
|
|
||||||
|
<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)
|
||||||
|
|
@ -69,6 +69,9 @@ func getScreenDumpDirForInfo() string {
|
||||||
log.Error().Err(err).Msgf("Unmarshal k9s config %v", err)
|
log.Error().Err(err).Msgf("Unmarshal k9s config %v", err)
|
||||||
return config.AppDumpsDir
|
return config.AppDumpsDir
|
||||||
}
|
}
|
||||||
|
if cfg.K9s == nil {
|
||||||
|
return config.AppDumpsDir
|
||||||
|
}
|
||||||
|
|
||||||
return cfg.K9s.GetScreenDumpDir()
|
return cfg.K9s.AppScreenDumpDir()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ func run(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
cfg, err := loadConfiguration()
|
cfg, err := loadConfiguration()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("load configuration failed")
|
return err
|
||||||
}
|
}
|
||||||
app := view.NewApp(cfg)
|
app := view.NewApp(cfg)
|
||||||
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
|
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
|
||||||
|
|
@ -115,11 +115,12 @@ func loadConfiguration() (*config.Config, error) {
|
||||||
k8sCfg := client.NewConfig(k8sFlags)
|
k8sCfg := client.NewConfig(k8sFlags)
|
||||||
k9sCfg := config.NewConfig(k8sCfg)
|
k9sCfg := config.NewConfig(k8sCfg)
|
||||||
if err := k9sCfg.Load(config.AppConfigFile); err != nil {
|
if err := k9sCfg.Load(config.AppConfigFile); err != nil {
|
||||||
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
|
return nil, err
|
||||||
}
|
}
|
||||||
k9sCfg.K9s.Override(k9sFlags)
|
k9sCfg.K9s.Override(k9sFlags)
|
||||||
if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {
|
if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {
|
||||||
log.Error().Err(err).Msgf("refine failed")
|
log.Error().Err(err).Msgf("config refine failed")
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
conn, err := client.InitConnection(k8sCfg)
|
conn, err := client.InitConnection(k8sCfg)
|
||||||
k9sCfg.SetConnection(conn)
|
k9sCfg.SetConnection(conn)
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -25,8 +25,10 @@ require (
|
||||||
github.com/sahilm/fuzzy v0.1.0
|
github.com/sahilm/fuzzy v0.1.0
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0
|
||||||
golang.org/x/text v0.14.0
|
golang.org/x/text v0.14.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
helm.sh/helm/v3 v3.13.3
|
helm.sh/helm/v3 v3.13.3
|
||||||
k8s.io/api v0.29.0
|
k8s.io/api v0.29.0
|
||||||
k8s.io/apiextensions-apiserver v0.29.0
|
k8s.io/apiextensions-apiserver v0.29.0
|
||||||
|
|
@ -276,7 +278,6 @@ require (
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
github.com/xlab/treeprint v1.2.0 // indirect
|
github.com/xlab/treeprint v1.2.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
|
|
@ -310,7 +311,6 @@ require (
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gorm.io/gorm v1.25.5 // indirect
|
gorm.io/gorm v1.25.5 // indirect
|
||||||
k8s.io/apiserver v0.29.0 // indirect
|
k8s.io/apiserver v0.29.0 // indirect
|
||||||
k8s.io/component-base v0.29.0 // indirect
|
k8s.io/component-base v0.29.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -517,12 +517,12 @@ func (a *APIClient) supportsMetricsResources() error {
|
||||||
a.cache.Add(cacheMXAPIKey, supported, cacheExpiry)
|
a.cache.Add(cacheMXAPIKey, supported, cacheExpiry)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
dial, err := a.CachedDiscovery()
|
dial, err := a.Dial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msgf("Unable to dial discovery API")
|
log.Warn().Err(err).Msgf("Unable to dial discovery API")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
apiGroups, err := dial.ServerGroups()
|
apiGroups, err := dial.Discovery().ServerGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL
|
||||||
|
|
||||||
func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
|
func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
|
||||||
if !m.HasMetrics() {
|
if !m.HasMetrics() {
|
||||||
return errors.New("No metrics-server detected on cluster")
|
return errors.New("no metrics-server detected on cluster")
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := m.CanI(ns, gvr, ListAccess)
|
auth, err := m.CanI(ns, gvr, ListAccess)
|
||||||
|
|
@ -193,7 +193,7 @@ func (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1bet
|
||||||
|
|
||||||
mx, ok := mmx[n]
|
mx, ok := mmx[n]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Unable to retrieve node metrics for %q", n)
|
return nil, fmt.Errorf("unable to retrieve node metrics for %q", n)
|
||||||
}
|
}
|
||||||
return mx, nil
|
return mx, nil
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +283,7 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be
|
||||||
}
|
}
|
||||||
pmx, ok := mmx[fqn]
|
pmx, ok := mmx[fqn]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Unable to locate pod metrics for pod %q", fqn)
|
return nil, fmt.Errorf("unable to locate pod metrics for pod %q", fqn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pmx, nil
|
return pmx, nil
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ type Connection interface {
|
||||||
// HasMetrics checks if metrics server is available.
|
// HasMetrics checks if metrics server is available.
|
||||||
HasMetrics() bool
|
HasMetrics() bool
|
||||||
|
|
||||||
// ValidNamespaces returns all available namespace names.
|
// ValidNamespaceNames returns all available namespace names.
|
||||||
ValidNamespaceNames() (NamespaceNames, error)
|
ValidNamespaceNames() (NamespaceNames, error)
|
||||||
|
|
||||||
// IsValidNamespace checks if given namespace is known.
|
// IsValidNamespace checks if given namespace is known.
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config/data"
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
@ -120,18 +122,26 @@ func (a *Aliases) LoadFile(path string) error {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
f, err := os.ReadFile(path)
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
if err == nil {
|
return nil
|
||||||
var aa Aliases
|
}
|
||||||
if err := yaml.Unmarshal(f, &aa); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
a.mx.Lock()
|
bb, err := os.ReadFile(path)
|
||||||
defer a.mx.Unlock()
|
if err != nil {
|
||||||
for k, v := range aa.Alias {
|
return err
|
||||||
a.Alias[k] = v
|
}
|
||||||
}
|
if err := data.JSONValidator.Validate(json.AliasesSchema, bb); err != nil {
|
||||||
|
return fmt.Errorf("validation failed for %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var aa Aliases
|
||||||
|
if err := yaml.Unmarshal(bb, &aa); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.mx.Lock()
|
||||||
|
defer a.mx.Unlock()
|
||||||
|
for k, v := range aa.Alias {
|
||||||
|
a.Alias[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,59 @@
|
||||||
package config_test
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestAliasClear(t *testing.T) {
|
||||||
|
a := testAliases()
|
||||||
|
a.Clear()
|
||||||
|
|
||||||
|
assert.Equal(t, 0, len(a.Keys()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAliasKeys(t *testing.T) {
|
||||||
|
a := testAliases()
|
||||||
|
kk := a.Keys()
|
||||||
|
slices.Sort(kk)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"a1", "a11", "a2", "a3"}, kk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAliasShortNames(t *testing.T) {
|
||||||
|
a := testAliases()
|
||||||
|
ess := config.ShortNames{
|
||||||
|
"gvr1": []string{"a1", "a11"},
|
||||||
|
"gvr2": []string{"a2"},
|
||||||
|
"gvr3": []string{"a3"},
|
||||||
|
}
|
||||||
|
ss := a.ShortNames()
|
||||||
|
assert.Equal(t, len(ess), len(ss))
|
||||||
|
for k, v := range ss {
|
||||||
|
v1, ok := ess[k]
|
||||||
|
assert.True(t, ok, fmt.Sprintf("missing: %q", k))
|
||||||
|
slices.Sort(v)
|
||||||
|
assert.Equal(t, v1, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAliasDefine(t *testing.T) {
|
func TestAliasDefine(t *testing.T) {
|
||||||
type aliasDef struct {
|
type aliasDef struct {
|
||||||
cmd string
|
cmd string
|
||||||
aliases []string
|
aliases []string
|
||||||
}
|
}
|
||||||
|
|
||||||
uu := []struct {
|
uu := map[string]struct {
|
||||||
name string
|
|
||||||
aliases []aliasDef
|
aliases []aliasDef
|
||||||
registeredCommands map[string]string
|
registeredCommands map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
"simple": {
|
||||||
name: "simple aliases",
|
|
||||||
aliases: []aliasDef{
|
aliases: []aliasDef{
|
||||||
{
|
{
|
||||||
cmd: "one",
|
cmd: "one",
|
||||||
|
|
@ -34,8 +68,7 @@ func TestAliasDefine(t *testing.T) {
|
||||||
"duh": "one",
|
"duh": "one",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
"duplicates": {
|
||||||
name: "duplicated aliases",
|
|
||||||
aliases: []aliasDef{
|
aliases: []aliasDef{
|
||||||
{
|
{
|
||||||
cmd: "one",
|
cmd: "one",
|
||||||
|
|
@ -54,9 +87,9 @@ func TestAliasDefine(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range uu {
|
for k := range uu {
|
||||||
u := uu[i]
|
u := uu[k]
|
||||||
t.Run(u.name, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
configAlias := config.NewAliases()
|
configAlias := config.NewAliases()
|
||||||
for _, aliases := range u.aliases {
|
for _, aliases := range u.aliases {
|
||||||
for _, a := range aliases.aliases {
|
for _, a := range aliases.aliases {
|
||||||
|
|
@ -73,18 +106,35 @@ func TestAliasDefine(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAliasesLoad(t *testing.T) {
|
func TestAliasesLoad(t *testing.T) {
|
||||||
|
config.AppConfigDir = "testdata/aliases"
|
||||||
a := config.NewAliases()
|
a := config.NewAliases()
|
||||||
|
|
||||||
assert.Nil(t, a.LoadFile("testdata/alias.yaml"))
|
assert.Nil(t, a.Load("testdata/aliases/plain.yaml"))
|
||||||
assert.Equal(t, 2, len(a.Alias))
|
assert.Equal(t, 56, len(a.Alias))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAliasesSave(t *testing.T) {
|
func TestAliasesSave(t *testing.T) {
|
||||||
a := config.NewAliases()
|
assert.NoError(t, data.EnsureFullPath("/tmp/test-aliases", data.DefaultDirMod))
|
||||||
a.Alias["test"] = "fred"
|
defer assert.NoError(t, os.RemoveAll("/tmp/test-aliases"))
|
||||||
a.Alias["blee"] = "duh"
|
|
||||||
|
|
||||||
assert.Nil(t, a.SaveAliases("/tmp/a.yaml"))
|
config.AppAliasesFile = "/tmp/test-aliases/aliases.yaml"
|
||||||
assert.Nil(t, a.LoadFile("/tmp/a.yaml"))
|
a := testAliases()
|
||||||
assert.Equal(t, 2, len(a.Alias))
|
c := len(a.Alias)
|
||||||
|
|
||||||
|
assert.Equal(t, c, len(a.Alias))
|
||||||
|
assert.Nil(t, a.Save())
|
||||||
|
assert.Nil(t, a.LoadFile("/tmp/test-aliases/aliases.yaml"))
|
||||||
|
assert.Equal(t, c, len(a.Alias))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func testAliases() *config.Aliases {
|
||||||
|
a := config.NewAliases()
|
||||||
|
a.Alias["a1"] = "gvr1"
|
||||||
|
a.Alias["a11"] = "gvr1"
|
||||||
|
a.Alias["a2"] = "gvr2"
|
||||||
|
a.Alias["a3"] = "gvr3"
|
||||||
|
|
||||||
|
return a
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,14 @@ func TestBenchLoad(t *testing.T) {
|
||||||
coCount int
|
coCount int
|
||||||
}{
|
}{
|
||||||
"goodConfig": {
|
"goodConfig": {
|
||||||
"testdata/b_good.yaml",
|
"testdata/benchmarks/b_good.yaml",
|
||||||
2,
|
2,
|
||||||
1000,
|
1000,
|
||||||
2,
|
2,
|
||||||
0,
|
0,
|
||||||
},
|
},
|
||||||
"malformed": {
|
"malformed": {
|
||||||
"testdata/b_toast.yaml",
|
"testdata/benchmarks/b_toast.yaml",
|
||||||
1,
|
1,
|
||||||
200,
|
200,
|
||||||
0,
|
0,
|
||||||
|
|
@ -103,7 +103,7 @@ func TestBenchServiceLoad(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
b, err := NewBench("testdata/b_good.yaml")
|
b, err := NewBench("testdata/benchmarks/b_good.yaml")
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 2, len(b.Benchmarks.Services))
|
assert.Equal(t, 2, len(b.Benchmarks.Services))
|
||||||
|
|
@ -122,11 +122,11 @@ func TestBenchServiceLoad(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBenchReLoad(t *testing.T) {
|
func TestBenchReLoad(t *testing.T) {
|
||||||
b, err := NewBench("testdata/b_containers.yaml")
|
b, err := NewBench("testdata/benchmarks/b_containers.yaml")
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 2, b.Benchmarks.Defaults.C)
|
assert.Equal(t, 2, b.Benchmarks.Defaults.C)
|
||||||
assert.Nil(t, b.Reload("testdata/b_containers_1.yaml"))
|
assert.NoError(t, b.Reload("testdata/benchmarks/b_containers_1.yaml"))
|
||||||
assert.Equal(t, 20, b.Benchmarks.Defaults.C)
|
assert.Equal(t, 20, b.Benchmarks.Defaults.C)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,7 +174,7 @@ func TestBenchContainerLoad(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
b, err := NewBench("testdata/b_containers.yaml")
|
b, err := NewBench("testdata/benchmarks/b_containers.yaml")
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 2, len(b.Benchmarks.Services))
|
assert.Equal(t, 2, len(b.Benchmarks.Services))
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,22 @@ const (
|
||||||
TransparentColor Color = "-"
|
TransparentColor Color = "-"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Colors tracks multiple colors.
|
||||||
|
type Colors []Color
|
||||||
|
|
||||||
|
// Colors converts series string colors to colors.
|
||||||
|
func (c Colors) Colors() []tcell.Color {
|
||||||
|
cc := make([]tcell.Color, 0, len(c))
|
||||||
|
for _, color := range c {
|
||||||
|
cc = append(cc, color.Color())
|
||||||
|
}
|
||||||
|
|
||||||
|
return cc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color represents a color.
|
||||||
|
type Color string
|
||||||
|
|
||||||
// NewColor returns a new color.
|
// NewColor returns a new color.
|
||||||
func NewColor(c string) Color {
|
func NewColor(c string) Color {
|
||||||
return Color(c)
|
return Color(c)
|
||||||
|
|
@ -50,12 +66,3 @@ func (c Color) Color() tcell.Color {
|
||||||
|
|
||||||
return tcell.GetColor(string(c)).TrueColor()
|
return tcell.GetColor(string(c)).TrueColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colors converts series string colors to colors.
|
|
||||||
func (c Colors) Colors() []tcell.Color {
|
|
||||||
cc := make([]tcell.Color, 0, len(c))
|
|
||||||
for _, color := range c {
|
|
||||||
cc = append(cc, color.Color())
|
|
||||||
}
|
|
||||||
return cc
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/tcell/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestColors(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
cc []string
|
||||||
|
ee []tcell.Color
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
ee: []tcell.Color{},
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
cc: []string{"default"},
|
||||||
|
ee: []tcell.Color{tcell.ColorDefault},
|
||||||
|
},
|
||||||
|
"multi": {
|
||||||
|
cc: []string{
|
||||||
|
"default",
|
||||||
|
"transparent",
|
||||||
|
"blue",
|
||||||
|
"green",
|
||||||
|
},
|
||||||
|
ee: []tcell.Color{
|
||||||
|
tcell.ColorDefault,
|
||||||
|
tcell.ColorDefault,
|
||||||
|
tcell.ColorBlue.TrueColor(),
|
||||||
|
tcell.ColorGreen.TrueColor(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
cc := make(config.Colors, 0, len(u.cc))
|
||||||
|
for _, c := range u.cc {
|
||||||
|
cc = append(cc, config.NewColor(c))
|
||||||
|
}
|
||||||
|
assert.Equal(t, u.ee, cc.Colors())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorString(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
c string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
e: "-",
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
c: "default",
|
||||||
|
e: "-",
|
||||||
|
},
|
||||||
|
"transparent": {
|
||||||
|
c: "-",
|
||||||
|
e: "-",
|
||||||
|
},
|
||||||
|
"blue": {
|
||||||
|
c: "blue",
|
||||||
|
e: "#0000ff",
|
||||||
|
},
|
||||||
|
"lightgray": {
|
||||||
|
c: "lightgray",
|
||||||
|
e: "#d3d3d3",
|
||||||
|
},
|
||||||
|
"hex": {
|
||||||
|
c: "#00ff00",
|
||||||
|
e: "#00ff00",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := config.NewColor(u.c)
|
||||||
|
assert.Equal(t, u.e, c.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorToColor(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
c string
|
||||||
|
e tcell.Color
|
||||||
|
}{
|
||||||
|
"default": {
|
||||||
|
c: "default",
|
||||||
|
e: tcell.ColorDefault,
|
||||||
|
},
|
||||||
|
"transparent": {
|
||||||
|
c: "-",
|
||||||
|
e: tcell.ColorDefault,
|
||||||
|
},
|
||||||
|
"aqua": {
|
||||||
|
c: "aqua",
|
||||||
|
e: tcell.ColorAqua.TrueColor(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := config.NewColor(u.c)
|
||||||
|
assert.Equal(t, u.e, c.Color())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,10 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config/data"
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
|
@ -19,25 +17,11 @@ import (
|
||||||
|
|
||||||
// Config tracks K9s configuration options.
|
// Config tracks K9s configuration options.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
K9s *K9s `yaml:"k9s"`
|
K9s *K9s `yaml:"k9s" json:"k9s"`
|
||||||
conn client.Connection
|
conn client.Connection
|
||||||
settings data.KubeSettings
|
settings data.KubeSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// K9sHome returns k9s configs home directory.
|
|
||||||
func K9sHome() string {
|
|
||||||
if isEnvSet(K9sEnvConfigDir) {
|
|
||||||
return os.Getenv(K9sEnvConfigDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
xdgK9sHome, err := xdg.ConfigFile(AppName)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s")
|
|
||||||
}
|
|
||||||
|
|
||||||
return xdgK9sHome
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConfig creates a new default config.
|
// NewConfig creates a new default config.
|
||||||
func NewConfig(ks data.KubeSettings) *Config {
|
func NewConfig(ks data.KubeSettings) *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
|
|
@ -46,6 +30,16 @@ func NewConfig(ks data.KubeSettings) *Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContextHotKeysPath returns a context specific hotkeys file spec.
|
||||||
|
func (c *Config) ContextHotkeysPath() string {
|
||||||
|
ct, err := c.K9s.ActiveContext()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppContextHotkeysFile(ct.ClusterName, c.K9s.activeContextName)
|
||||||
|
}
|
||||||
|
|
||||||
// ContextAliasesPath returns a context specific aliases file spec.
|
// ContextAliasesPath returns a context specific aliases file spec.
|
||||||
func (c *Config) ContextAliasesPath() string {
|
func (c *Config) ContextAliasesPath() string {
|
||||||
ct, err := c.K9s.ActiveContext()
|
ct, err := c.K9s.ActiveContext()
|
||||||
|
|
@ -53,13 +47,14 @@ func (c *Config) ContextAliasesPath() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppContextAliasesFile(ct.ClusterName, c.K9s.activeContextName)
|
return AppContextAliasesFile(ct.GetClusterName(), c.K9s.activeContextName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextPluginsPath returns a context specific plugins file spec.
|
// ContextPluginsPath returns a context specific plugins file spec.
|
||||||
func (c *Config) ContextPluginsPath() string {
|
func (c *Config) ContextPluginsPath() string {
|
||||||
ct, err := c.K9s.ActiveContext()
|
ct, err := c.K9s.ActiveContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("active context load failed")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +63,10 @@ func (c *Config) ContextPluginsPath() string {
|
||||||
|
|
||||||
// Refine the configuration based on cli args.
|
// Refine the configuration based on cli args.
|
||||||
func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error {
|
func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error {
|
||||||
if isSet(flags.Context) {
|
if flags == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if isStringSet(flags.Context) {
|
||||||
if _, err := c.K9s.ActivateContext(*flags.Context); err != nil {
|
if _, err := c.K9s.ActivateContext(*flags.Context); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +86,7 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
|
||||||
switch {
|
switch {
|
||||||
case k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces):
|
case k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces):
|
||||||
ns = client.NamespaceAll
|
ns = client.NamespaceAll
|
||||||
case isSet(flags.Namespace):
|
case isStringSet(flags.Namespace):
|
||||||
ns = *flags.Namespace
|
ns = *flags.Namespace
|
||||||
default:
|
default:
|
||||||
nss, err := c.K9s.ActiveContextNamespace()
|
nss, err := c.K9s.ActiveContextNamespace()
|
||||||
|
|
@ -104,7 +102,7 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.EnsureDirPath(c.K9s.GetScreenDumpDir(), data.DefaultDirMod)
|
return data.EnsureDirPath(c.K9s.AppScreenDumpDir(), data.DefaultDirMod)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset resets the context to the new current context/cluster.
|
// Reset resets the context to the new current context/cluster.
|
||||||
|
|
@ -115,7 +113,7 @@ func (c *Config) Reset() {
|
||||||
func (c *Config) SetCurrentContext(n string) (*data.Context, error) {
|
func (c *Config) SetCurrentContext(n string) (*data.Context, error) {
|
||||||
ct, err := c.K9s.ActivateContext(n)
|
ct, err := c.K9s.ActivateContext(n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("set current context %q failed: %w", n, err)
|
return nil, fmt.Errorf("set current context failed. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ct, nil
|
return ct, nil
|
||||||
|
|
@ -138,21 +136,13 @@ func (c *Config) ActiveNamespace() string {
|
||||||
return ns
|
return ns
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateFavorites ensure favorite ns are legit.
|
|
||||||
func (c *Config) ValidateFavorites() {
|
|
||||||
ct, err := c.K9s.ActiveContext()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ct.Validate(c.conn, c.settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FavNamespaces returns fav namespaces in the current context.
|
// FavNamespaces returns fav namespaces in the current context.
|
||||||
func (c *Config) FavNamespaces() []string {
|
func (c *Config) FavNamespaces() []string {
|
||||||
ct, err := c.K9s.ActiveContext()
|
ct, err := c.K9s.ActiveContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
ct.Validate(c.conn, c.settings)
|
||||||
|
|
||||||
return ct.Namespace.Favorites
|
return ct.Namespace.Favorites
|
||||||
}
|
}
|
||||||
|
|
@ -209,23 +199,26 @@ func (c *Config) ActiveContextName() string {
|
||||||
return c.K9s.activeContextName
|
return c.K9s.activeContextName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) Merge(c1 *Config) {
|
||||||
|
c.K9s.Merge(c1.K9s)
|
||||||
|
}
|
||||||
|
|
||||||
// Load loads K9s configuration from file.
|
// Load loads K9s configuration from file.
|
||||||
func (c *Config) Load(path string) error {
|
func (c *Config) Load(path string) error {
|
||||||
f, err := os.ReadFile(path)
|
bb, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := data.JSONValidator.Validate(json.K9sSchema, bb); err != nil {
|
||||||
|
return fmt.Errorf("k9s config file %q load failed:\n%w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
var cfg Config
|
var cfg Config
|
||||||
if err := yaml.Unmarshal(f, &cfg); err != nil {
|
if err := yaml.Unmarshal(bb, &cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if cfg.K9s != nil {
|
c.Merge(&cfg)
|
||||||
c.K9s.Refine(cfg.K9s)
|
|
||||||
}
|
|
||||||
if c.K9s.Logger == nil {
|
|
||||||
c.K9s.Logger = NewLogger()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,7 +228,11 @@ func (c *Config) Save() error {
|
||||||
if err := c.K9s.Save(); err != nil {
|
if err := c.K9s.Save(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.SaveFile(AppConfigFile)
|
if _, err := os.Stat(AppConfigFile); os.IsNotExist(err) {
|
||||||
|
return c.SaveFile(AppConfigFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveFile K9s configuration to disk.
|
// SaveFile K9s configuration to disk.
|
||||||
|
|
@ -248,48 +245,26 @@ func (c *Config) SaveFile(path string) error {
|
||||||
log.Error().Msgf("[Config] Unable to save K9s config file: %v", err)
|
log.Error().Msgf("[Config] Unable to save K9s config file: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(path, cfg, 0644)
|
return os.WriteFile(path, cfg, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the configuration.
|
// Validate the configuration.
|
||||||
func (c *Config) Validate() {
|
func (c *Config) Validate() {
|
||||||
|
if c.K9s == nil {
|
||||||
|
c.K9s = NewK9s(c.conn, c.settings)
|
||||||
|
}
|
||||||
|
|
||||||
c.K9s.Validate(c.conn, c.settings)
|
c.K9s.Validate(c.conn, c.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump debug...
|
// Dump for debug...
|
||||||
func (c *Config) Dump(msg string) {
|
func (c *Config) Dump(msg string) {
|
||||||
ct, err := c.K9s.ActiveContext()
|
ct, err := c.K9s.ActiveContext()
|
||||||
if err != nil {
|
if err == nil {
|
||||||
log.Debug().Msgf("Current Contexts: %s\n", ct.ClusterName)
|
bb, _ := yaml.Marshal(ct)
|
||||||
|
fmt.Printf("Dump: %q\n%s\n", msg, string(bb))
|
||||||
|
} else {
|
||||||
|
fmt.Println("BOOM!", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// YamlExtension tries to find the correct extension for a YAML file
|
|
||||||
func YamlExtension(path string) string {
|
|
||||||
if !isYamlFile(path) {
|
|
||||||
log.Error().Msgf("Config: File %s is not a yaml file", path)
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip any extension, if there is no extension the path will remain unchanged
|
|
||||||
path = strings.TrimSuffix(path, filepath.Ext(path))
|
|
||||||
result := path + ".yml"
|
|
||||||
|
|
||||||
if _, err := os.Stat(result); os.IsNotExist(err) {
|
|
||||||
return path + ".yaml"
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
// Helpers...
|
|
||||||
|
|
||||||
func isSet(s *string) bool {
|
|
||||||
return s != nil && len(*s) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func isYamlFile(file string) bool {
|
|
||||||
ext := filepath.Ext(file)
|
|
||||||
return ext == ".yml" || ext == ".yaml"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,16 @@
|
||||||
package config_test
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
"github.com/derailed/k9s/internal/config/mock"
|
"github.com/derailed/k9s/internal/config/mock"
|
||||||
m "github.com/petergtz/pegomock"
|
m "github.com/petergtz/pegomock"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
@ -21,37 +25,358 @@ func init() {
|
||||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigRefine(t *testing.T) {
|
func TestConfigSave(t *testing.T) {
|
||||||
|
config.AppConfigFile = "/tmp/k9s-test/k9s.yaml"
|
||||||
|
sd := "/tmp/k9s-test/screen-dumps"
|
||||||
|
cl, ct := "cl-1", "ct-1-1"
|
||||||
|
_ = os.RemoveAll(("/tmp/k9s-test"))
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
ct string
|
||||||
|
flags *genericclioptions.ConfigFlags
|
||||||
|
k9sFlags *config.Flags
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
ct: "ct-1-1",
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
ClusterName: &cl,
|
||||||
|
Context: &ct,
|
||||||
|
},
|
||||||
|
k9sFlags: &config.Flags{
|
||||||
|
ScreenDumpDir: &sd,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
xdg.Reload()
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := mock.NewMockConfig()
|
||||||
|
_, err := c.K9s.ActivateContext(u.ct)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if u.flags != nil {
|
||||||
|
c.K9s.Override(u.k9sFlags)
|
||||||
|
assert.NoError(t, c.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags)))
|
||||||
|
}
|
||||||
|
assert.NoError(t, c.Save())
|
||||||
|
bb, err := os.ReadFile(config.AppConfigFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
ee, err := os.ReadFile("testdata/configs/default.yaml")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, string(ee), string(bb))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetActiveView(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
cfgFile = "testdata/kubeconfig-test.yaml"
|
cfgFile = "testdata/kubes/test.yaml"
|
||||||
ctx, cluster, ns = "ct-1-1", "cl-1", "ns-1"
|
view = "dp"
|
||||||
)
|
)
|
||||||
|
|
||||||
uu := map[string]struct {
|
uu := map[string]struct {
|
||||||
flags *genericclioptions.ConfigFlags
|
ct string
|
||||||
issue bool
|
flags *genericclioptions.ConfigFlags
|
||||||
context, cluster, namespace string
|
k9sFlags *config.Flags
|
||||||
|
view string
|
||||||
|
e string
|
||||||
}{
|
}{
|
||||||
"overrideNS": {
|
"empty": {
|
||||||
flags: &genericclioptions.ConfigFlags{
|
view: data.DefaultView,
|
||||||
KubeConfig: &cfgFile,
|
e: data.DefaultView,
|
||||||
Context: &ctx,
|
|
||||||
ClusterName: &cluster,
|
|
||||||
Namespace: &ns,
|
|
||||||
},
|
|
||||||
issue: false,
|
|
||||||
context: ctx,
|
|
||||||
cluster: cluster,
|
|
||||||
namespace: ns,
|
|
||||||
},
|
},
|
||||||
"badContext": {
|
"not-exists": {
|
||||||
|
ct: "fred",
|
||||||
|
view: data.DefaultView,
|
||||||
|
e: data.DefaultView,
|
||||||
|
},
|
||||||
|
"happy": {
|
||||||
|
ct: "ct-1-1",
|
||||||
|
view: "xray",
|
||||||
|
e: "xray",
|
||||||
|
},
|
||||||
|
"cli-override": {
|
||||||
flags: &genericclioptions.ConfigFlags{
|
flags: &genericclioptions.ConfigFlags{
|
||||||
KubeConfig: &cfgFile,
|
KubeConfig: &cfgFile,
|
||||||
Context: &ns,
|
|
||||||
ClusterName: &cluster,
|
|
||||||
Namespace: &ns,
|
|
||||||
},
|
},
|
||||||
issue: true,
|
k9sFlags: &config.Flags{
|
||||||
|
Command: &view,
|
||||||
|
},
|
||||||
|
ct: "ct-1-1",
|
||||||
|
view: "xray",
|
||||||
|
e: "dp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := mock.NewMockConfig()
|
||||||
|
_, _ = c.K9s.ActivateContext(u.ct)
|
||||||
|
if u.flags != nil {
|
||||||
|
assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
|
||||||
|
c.K9s.Override(u.k9sFlags)
|
||||||
|
}
|
||||||
|
c.SetActiveView(u.view)
|
||||||
|
assert.Equal(t, u.e, c.ActiveView())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActiveContextName(t *testing.T) {
|
||||||
|
var (
|
||||||
|
cfgFile = "testdata/kubes/test.yaml"
|
||||||
|
ct2 = "ct-1-2"
|
||||||
|
)
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
flags *genericclioptions.ConfigFlags
|
||||||
|
k9sFlags *config.Flags
|
||||||
|
ct string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"empty": {},
|
||||||
|
"happy": {
|
||||||
|
ct: "ct-1-1",
|
||||||
|
e: "ct-1-1",
|
||||||
|
},
|
||||||
|
"cli-override": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
Context: &ct2,
|
||||||
|
},
|
||||||
|
k9sFlags: &config.Flags{},
|
||||||
|
ct: "ct-1-1",
|
||||||
|
e: "ct-1-2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := mock.NewMockConfig()
|
||||||
|
_, _ = c.K9s.ActivateContext(u.ct)
|
||||||
|
if u.flags != nil {
|
||||||
|
assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
|
||||||
|
c.K9s.Override(u.k9sFlags)
|
||||||
|
}
|
||||||
|
assert.Equal(t, u.e, c.ActiveContextName())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestActiveView(t *testing.T) {
|
||||||
|
var (
|
||||||
|
cfgFile = "testdata/kubes/test.yaml"
|
||||||
|
view = "dp"
|
||||||
|
)
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
ct string
|
||||||
|
flags *genericclioptions.ConfigFlags
|
||||||
|
k9sFlags *config.Flags
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
e: data.DefaultView,
|
||||||
|
},
|
||||||
|
"not-exists": {
|
||||||
|
ct: "fred",
|
||||||
|
e: data.DefaultView,
|
||||||
|
},
|
||||||
|
"happy": {
|
||||||
|
ct: "ct-1-1",
|
||||||
|
e: data.DefaultView,
|
||||||
|
},
|
||||||
|
"cli-override": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
},
|
||||||
|
k9sFlags: &config.Flags{
|
||||||
|
Command: &view,
|
||||||
|
},
|
||||||
|
e: "dp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := mock.NewMockConfig()
|
||||||
|
_, _ = c.K9s.ActivateContext(u.ct)
|
||||||
|
if u.flags != nil {
|
||||||
|
assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
|
||||||
|
c.K9s.Override(u.k9sFlags)
|
||||||
|
}
|
||||||
|
assert.Equal(t, u.e, c.ActiveView())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFavNamespaces(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
ct string
|
||||||
|
e []string
|
||||||
|
}{
|
||||||
|
"empty": {},
|
||||||
|
"not-exists": {
|
||||||
|
ct: "fred",
|
||||||
|
},
|
||||||
|
"happy": {
|
||||||
|
ct: "ct-1-1",
|
||||||
|
e: []string{client.DefaultNamespace},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := mock.NewMockConfig()
|
||||||
|
_, _ = c.K9s.ActivateContext(u.ct)
|
||||||
|
assert.Equal(t, u.e, c.FavNamespaces())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextAliasesPath(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
ct string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"empty": {},
|
||||||
|
"not-exists": {
|
||||||
|
ct: "fred",
|
||||||
|
},
|
||||||
|
"happy": {
|
||||||
|
ct: "ct-1-1",
|
||||||
|
e: "/tmp/test/cl-1/ct-1-1/aliases.yaml",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := mock.NewMockConfig()
|
||||||
|
_, _ = c.K9s.ActivateContext(u.ct)
|
||||||
|
assert.Equal(t, u.e, c.ContextAliasesPath())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextPluginsPath(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
ct string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"empty": {},
|
||||||
|
"happy": {
|
||||||
|
ct: "ct-1-1",
|
||||||
|
e: "/tmp/test/cl-1/ct-1-1/plugins.yaml",
|
||||||
|
},
|
||||||
|
"not-exists": {
|
||||||
|
ct: "fred",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c := mock.NewMockConfig()
|
||||||
|
_, _ = c.K9s.ActivateContext(u.ct)
|
||||||
|
assert.Equal(t, u.e, c.ContextPluginsPath())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigLoader(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
f string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
f: "testdata/configs/k9s.yaml",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
f: "testdata/configs/k9s_toast.yaml",
|
||||||
|
err: `k9s config file "testdata/configs/k9s_toast.yaml" load failed:
|
||||||
|
Additional property disablePodCounts is not allowed
|
||||||
|
Additional property shellPods is not allowed
|
||||||
|
Invalid type. Expected: boolean, given: string`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
cfg := config.NewConfig(nil)
|
||||||
|
if err := cfg.Load(u.f); err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSetCurrentContext(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
cl, ct string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
ct: "ct-1-2",
|
||||||
|
cl: "cl-1",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
ct: "fred",
|
||||||
|
cl: "cl-1",
|
||||||
|
err: `set current context failed. no context found for: "fred"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
cfg := mock.NewMockConfig()
|
||||||
|
ct, err := cfg.SetCurrentContext(u.ct)
|
||||||
|
if err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, u.cl, ct.ClusterName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigCurrentContext(t *testing.T) {
|
||||||
|
var (
|
||||||
|
cfgFile = "testdata/kubes/test.yaml"
|
||||||
|
ct2 = "ct-1-2"
|
||||||
|
)
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
flags *genericclioptions.ConfigFlags
|
||||||
|
err error
|
||||||
|
context string
|
||||||
|
cluster string
|
||||||
|
namespace string
|
||||||
|
}{
|
||||||
|
"override-context": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
Context: &ct2,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-2",
|
||||||
|
namespace: "ns-2",
|
||||||
|
},
|
||||||
|
"use-current-context": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-1",
|
||||||
|
namespace: client.DefaultNamespace,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,8 +386,128 @@ func TestConfigRefine(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cfg := mock.NewMockConfig()
|
||||||
|
|
||||||
err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))
|
err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))
|
||||||
if u.issue {
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, err)
|
ct, err := cfg.CurrentContext()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, u.cluster, ct.ClusterName)
|
||||||
|
assert.Equal(t, u.namespace, ct.Namespace.Active)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigRefine(t *testing.T) {
|
||||||
|
var (
|
||||||
|
cfgFile = "testdata/kubes/test.yaml"
|
||||||
|
cl1 = "cl-1"
|
||||||
|
ct2 = "ct-1-2"
|
||||||
|
ns1, ns2, nsx = "ns-1", "ns-2", "ns-x"
|
||||||
|
true = true
|
||||||
|
)
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
flags *genericclioptions.ConfigFlags
|
||||||
|
k9sFlags *config.Flags
|
||||||
|
err error
|
||||||
|
context string
|
||||||
|
cluster string
|
||||||
|
namespace string
|
||||||
|
}{
|
||||||
|
"no-override": {
|
||||||
|
namespace: "default",
|
||||||
|
},
|
||||||
|
"override-cluster": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
ClusterName: &cl1,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-1",
|
||||||
|
namespace: client.DefaultNamespace,
|
||||||
|
},
|
||||||
|
"override-cluster-context": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
ClusterName: &cl1,
|
||||||
|
Context: &ct2,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-2",
|
||||||
|
namespace: "ns-2",
|
||||||
|
},
|
||||||
|
"override-bad-cluster": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
ClusterName: &ns1,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-1",
|
||||||
|
namespace: client.DefaultNamespace,
|
||||||
|
},
|
||||||
|
"override-ns": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
Namespace: &ns2,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-1",
|
||||||
|
namespace: "ns-2",
|
||||||
|
},
|
||||||
|
"all-ns": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
Namespace: &ns2,
|
||||||
|
},
|
||||||
|
k9sFlags: &config.Flags{
|
||||||
|
AllNamespaces: &true,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-1",
|
||||||
|
namespace: client.NamespaceAll,
|
||||||
|
},
|
||||||
|
|
||||||
|
"override-bad-ns": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
Namespace: &nsx,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-1",
|
||||||
|
namespace: "ns-x",
|
||||||
|
},
|
||||||
|
"override-context": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
Context: &ct2,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-2",
|
||||||
|
namespace: "ns-2",
|
||||||
|
},
|
||||||
|
"override-bad-context": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
Context: &ns1,
|
||||||
|
},
|
||||||
|
err: errors.New(`no context found for: "ns-1"`),
|
||||||
|
},
|
||||||
|
"use-current-context": {
|
||||||
|
flags: &genericclioptions.ConfigFlags{
|
||||||
|
KubeConfig: &cfgFile,
|
||||||
|
},
|
||||||
|
cluster: "cl-1",
|
||||||
|
context: "ct-1-1",
|
||||||
|
namespace: client.DefaultNamespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
cfg := mock.NewMockConfig()
|
||||||
|
|
||||||
|
err := cfg.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags))
|
||||||
|
if err != nil {
|
||||||
|
assert.Equal(t, u.err, err)
|
||||||
} else {
|
} else {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, u.context, cfg.K9s.ActiveContextName())
|
assert.Equal(t, u.context, cfg.K9s.ActiveContextName())
|
||||||
|
|
@ -76,35 +521,29 @@ func TestConfigValidate(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cfg := mock.NewMockConfig()
|
||||||
cfg.SetConnection(mock.NewMockConnection())
|
cfg.SetConnection(mock.NewMockConnection())
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||||
cfg.Validate()
|
cfg.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigLoad(t *testing.T) {
|
func TestConfigLoad(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cfg := mock.NewMockConfig()
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||||
assert.Equal(t, 2, cfg.K9s.RefreshRate)
|
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, int64(200), cfg.K9s.Logger.TailCount)
|
||||||
}
|
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
|
||||||
|
|
||||||
func TestConfigLoadOldCfg(t *testing.T) {
|
|
||||||
cfg := mock.NewMockConfig()
|
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s_old.yaml"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigLoadCrap(t *testing.T) {
|
func TestConfigLoadCrap(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cfg := mock.NewMockConfig()
|
||||||
|
|
||||||
assert.NotNil(t, cfg.Load("testdata/k9s_not_there.yaml"))
|
assert.NotNil(t, cfg.Load("testdata/configs/k9s_not_there.yaml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigSaveFile(t *testing.T) {
|
func TestConfigSaveFile(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cfg := mock.NewMockConfig()
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||||
|
|
||||||
cfg.K9s.RefreshRate = 100
|
cfg.K9s.RefreshRate = 100
|
||||||
cfg.K9s.ReadOnly = true
|
cfg.K9s.ReadOnly = true
|
||||||
|
|
@ -113,26 +552,28 @@ func TestConfigSaveFile(t *testing.T) {
|
||||||
cfg.Validate()
|
cfg.Validate()
|
||||||
|
|
||||||
path := filepath.Join("/tmp", "k9s.yaml")
|
path := filepath.Join("/tmp", "k9s.yaml")
|
||||||
err := cfg.SaveFile(path)
|
assert.NoError(t, cfg.SaveFile(path))
|
||||||
assert.Nil(t, err)
|
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, expectedConfig, string(raw))
|
ee, err := os.ReadFile("testdata/configs/expected.yaml")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, string(ee), string(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigReset(t *testing.T) {
|
func TestConfigReset(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cfg := mock.NewMockConfig()
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||||
cfg.Reset()
|
cfg.Reset()
|
||||||
cfg.Validate()
|
cfg.Validate()
|
||||||
|
|
||||||
path := filepath.Join("/tmp", "k9s.yaml")
|
path := filepath.Join("/tmp", "k9s.yaml")
|
||||||
err := cfg.SaveFile(path)
|
assert.NoError(t, cfg.SaveFile(path))
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
raw, err := os.ReadFile(path)
|
bb, err := os.ReadFile(path)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, resetConfig, string(raw))
|
ee, err := os.ReadFile("testdata/configs/k9s.yaml")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, string(ee), string(bb))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
@ -147,86 +588,86 @@ func TestSetup(t *testing.T) {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Test Data...
|
// Test Data...
|
||||||
|
|
||||||
var expectedConfig = `k9s:
|
// var expectedConfig = `k9s:
|
||||||
liveViewAutoRefresh: true
|
// liveViewAutoRefresh: true
|
||||||
screenDumpDir: /tmp
|
// screenDumpDir: /tmp/screen-dumps
|
||||||
refreshRate: 100
|
// refreshRate: 100
|
||||||
maxConnRetry: 5
|
// maxConnRetry: 5
|
||||||
readOnly: true
|
// readOnly: true
|
||||||
noExitOnCtrlC: false
|
// noExitOnCtrlC: false
|
||||||
ui:
|
// ui:
|
||||||
enableMouse: false
|
// enableMouse: false
|
||||||
headless: false
|
// headless: false
|
||||||
logoless: false
|
// logoless: false
|
||||||
crumbsless: false
|
// crumbsless: false
|
||||||
noIcons: false
|
// noIcons: false
|
||||||
skipLatestRevCheck: false
|
// skipLatestRevCheck: false
|
||||||
disablePodCounting: false
|
// disablePodCounting: false
|
||||||
shellPod:
|
// shellPod:
|
||||||
image: busybox:1.35.0
|
// image: busybox:1.35.0
|
||||||
namespace: default
|
// namespace: default
|
||||||
limits:
|
// limits:
|
||||||
cpu: 100m
|
// cpu: 100m
|
||||||
memory: 100Mi
|
// memory: 100Mi
|
||||||
imageScans:
|
// imageScans:
|
||||||
enable: false
|
// enable: false
|
||||||
exclusions:
|
// exclusions:
|
||||||
namespaces: []
|
// namespaces: []
|
||||||
labels: {}
|
// labels: {}
|
||||||
logger:
|
// logger:
|
||||||
tail: 500
|
// tail: 500
|
||||||
buffer: 800
|
// buffer: 800
|
||||||
sinceSeconds: -1
|
// sinceSeconds: -1
|
||||||
fullScreenLogs: false
|
// fullScreen: false
|
||||||
textWrap: false
|
// textWrap: false
|
||||||
showTime: false
|
// showTime: false
|
||||||
thresholds:
|
// thresholds:
|
||||||
cpu:
|
// cpu:
|
||||||
critical: 90
|
// critical: 90
|
||||||
warn: 70
|
// warn: 70
|
||||||
memory:
|
// memory:
|
||||||
critical: 90
|
// critical: 90
|
||||||
warn: 70
|
// warn: 70
|
||||||
`
|
// `
|
||||||
|
|
||||||
var resetConfig = `k9s:
|
// var resetConfig = `k9s:
|
||||||
liveViewAutoRefresh: true
|
// liveViewAutoRefresh: true
|
||||||
screenDumpDir: /tmp
|
// screenDumpDir: /tmp/screen-dumps
|
||||||
refreshRate: 2
|
// refreshRate: 2
|
||||||
maxConnRetry: 5
|
// maxConnRetry: 5
|
||||||
readOnly: false
|
// readOnly: false
|
||||||
noExitOnCtrlC: false
|
// noExitOnCtrlC: false
|
||||||
ui:
|
// ui:
|
||||||
enableMouse: false
|
// enableMouse: false
|
||||||
headless: false
|
// headless: false
|
||||||
logoless: false
|
// logoless: false
|
||||||
crumbsless: false
|
// crumbsless: false
|
||||||
noIcons: false
|
// noIcons: false
|
||||||
skipLatestRevCheck: false
|
// skipLatestRevCheck: false
|
||||||
disablePodCounting: false
|
// disablePodCounting: false
|
||||||
shellPod:
|
// shellPod:
|
||||||
image: busybox:1.35.0
|
// image: busybox:1.35.0
|
||||||
namespace: default
|
// namespace: default
|
||||||
limits:
|
// limits:
|
||||||
cpu: 100m
|
// cpu: 100m
|
||||||
memory: 100Mi
|
// memory: 100Mi
|
||||||
imageScans:
|
// imageScans:
|
||||||
enable: false
|
// enable: false
|
||||||
exclusions:
|
// exclusions:
|
||||||
namespaces: []
|
// namespaces: []
|
||||||
labels: {}
|
// labels: {}
|
||||||
logger:
|
// logger:
|
||||||
tail: 200
|
// tail: 200
|
||||||
buffer: 2000
|
// buffer: 2000
|
||||||
sinceSeconds: -1
|
// sinceSeconds: -1
|
||||||
fullScreenLogs: false
|
// fullScreen: false
|
||||||
textWrap: false
|
// textWrap: false
|
||||||
showTime: false
|
// showTime: false
|
||||||
thresholds:
|
// thresholds:
|
||||||
cpu:
|
// cpu:
|
||||||
critical: 90
|
// critical: 90
|
||||||
warn: 70
|
// warn: 70
|
||||||
memory:
|
// memory:
|
||||||
critical: 90
|
// critical: 90
|
||||||
warn: 70
|
// warn: 70
|
||||||
`
|
// `
|
||||||
|
|
|
||||||
|
|
@ -18,27 +18,33 @@ type Config struct {
|
||||||
Context *Context `yaml:"k9s"`
|
Context *Context `yaml:"k9s"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewConfig returns a new config.
|
||||||
func NewConfig(ct *api.Context) *Config {
|
func NewConfig(ct *api.Context) *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Context: NewContextFromConfig(ct),
|
Context: NewContextFromConfig(ct),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate ensures config is in norms.
|
||||||
func (c *Config) Validate(conn client.Connection, ks KubeSettings) {
|
func (c *Config) Validate(conn client.Connection, ks KubeSettings) {
|
||||||
|
if c.Context == nil {
|
||||||
|
c.Context = NewContext()
|
||||||
|
}
|
||||||
c.Context.Validate(conn, ks)
|
c.Context.Validate(conn, ks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dump used for debugging.
|
||||||
func (c *Config) Dump(w io.Writer) {
|
func (c *Config) Dump(w io.Writer) {
|
||||||
bb, _ := yaml.Marshal(&c)
|
bb, _ := yaml.Marshal(&c)
|
||||||
|
|
||||||
fmt.Fprintf(w, "%s\n", string(bb))
|
fmt.Fprintf(w, "%s\n", string(bb))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save saves the config to disk.
|
||||||
func (c *Config) Save(path string) error {
|
func (c *Config) Save(path string) error {
|
||||||
if err := EnsureDirPath(path, DefaultDirMod); err != nil {
|
if err := EnsureDirPath(path, DefaultDirMod); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := yaml.Marshal(c)
|
cfg, err := yaml.Marshal(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
)
|
)
|
||||||
|
|
@ -14,12 +16,13 @@ const DefaultPFAddress = "localhost"
|
||||||
// Context tracks K9s context configuration.
|
// Context tracks K9s context configuration.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
ClusterName string `yaml:"cluster,omitempty"`
|
ClusterName string `yaml:"cluster,omitempty"`
|
||||||
ReadOnly bool `yaml:"readOnly"`
|
ReadOnly *bool `yaml:"readOnly,omitempty"`
|
||||||
Skin string `yaml:"skin,omitempty"`
|
Skin string `yaml:"skin,omitempty"`
|
||||||
Namespace *Namespace `yaml:"namespace"`
|
Namespace *Namespace `yaml:"namespace"`
|
||||||
View *View `yaml:"view"`
|
View *View `yaml:"view"`
|
||||||
FeatureGates FeatureGates `yaml:"featureGates"`
|
FeatureGates FeatureGates `yaml:"featureGates"`
|
||||||
PortForwardAddress string `yaml:"portForwardAddress"`
|
PortForwardAddress string `yaml:"portForwardAddress"`
|
||||||
|
mx sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContext creates a new cluster configuration.
|
// NewContext creates a new cluster configuration.
|
||||||
|
|
@ -32,6 +35,7 @@ func NewContext() *Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewContextFromConfig returns a config based on a kubecontext.
|
||||||
func NewContextFromConfig(cfg *api.Context) *Context {
|
func NewContextFromConfig(cfg *api.Context) *Context {
|
||||||
return &Context{
|
return &Context{
|
||||||
Namespace: NewActiveNamespace(cfg.Namespace),
|
Namespace: NewActiveNamespace(cfg.Namespace),
|
||||||
|
|
@ -42,12 +46,32 @@ func NewContextFromConfig(cfg *api.Context) *Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate a context config.
|
// NewContextFromKubeConfig returns a new instance based on kubesettings or an error.
|
||||||
|
func NewContextFromKubeConfig(ks KubeSettings) (*Context, error) {
|
||||||
|
ct, err := ks.CurrentContext()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewContextFromConfig(ct), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) GetClusterName() string {
|
||||||
|
c.mx.RLock()
|
||||||
|
defer c.mx.RUnlock()
|
||||||
|
|
||||||
|
return c.ClusterName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate ensures a context config is tip top.
|
||||||
func (c *Context) Validate(conn client.Connection, ks KubeSettings) {
|
func (c *Context) Validate(conn client.Connection, ks KubeSettings) {
|
||||||
|
c.mx.Lock()
|
||||||
|
defer c.mx.Unlock()
|
||||||
|
|
||||||
if c.PortForwardAddress == "" {
|
if c.PortForwardAddress == "" {
|
||||||
c.PortForwardAddress = DefaultPFAddress
|
c.PortForwardAddress = DefaultPFAddress
|
||||||
}
|
}
|
||||||
if cl, err := ks.CurrentClusterName(); err != nil {
|
if cl, err := ks.CurrentClusterName(); err == nil {
|
||||||
c.ClusterName = cl
|
c.ClusterName = cl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
|
@ -60,6 +62,10 @@ func (d *Dir) loadConfig(path string) (*Config, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := JSONValidator.Validate(json.ContextSchema, bb); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation failed for %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
var cfg Config
|
var cfg Config
|
||||||
if err := yaml.Unmarshal(bb, &cfg); err != nil {
|
if err := yaml.Unmarshal(bb, &cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ func TestEnsureDirPathNone(t *testing.T) {
|
||||||
func TestEnsureDirPathNoOpt(t *testing.T) {
|
func TestEnsureDirPathNoOpt(t *testing.T) {
|
||||||
var mod os.FileMode = 0744
|
var mod os.FileMode = 0744
|
||||||
dir := filepath.Join("/tmp", "k9s-test")
|
dir := filepath.Join("/tmp", "k9s-test")
|
||||||
os.Remove(dir)
|
assert.NoError(t, os.RemoveAll(dir))
|
||||||
assert.NoError(t, os.Mkdir(dir, mod))
|
assert.NoError(t, os.Mkdir(dir, mod))
|
||||||
|
|
||||||
path := filepath.Join(dir, "duh.yaml")
|
path := filepath.Join(dir, "duh.yaml")
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,13 @@ package data
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// JSONValidator validate yaml configurations.
|
||||||
|
var JSONValidator = json.NewValidator()
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DefaultDirMod default unix perms for k9s directory.
|
// DefaultDirMod default unix perms for k9s directory.
|
||||||
DefaultDirMod os.FileMode = 0744
|
DefaultDirMod os.FileMode = 0744
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ var (
|
||||||
AppHotKeysFile string
|
AppHotKeysFile string
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitLogsLoc initializes K9s logs location.
|
// InitLogLoc initializes K9s logs location.
|
||||||
func InitLogLoc() error {
|
func InitLogLoc() error {
|
||||||
var appLogDir string
|
var appLogDir string
|
||||||
switch {
|
switch {
|
||||||
|
|
@ -273,5 +273,9 @@ func EnsureHotkeysCfgFile() (string, error) {
|
||||||
|
|
||||||
// SkinFileFromName generate skin file path from spec.
|
// SkinFileFromName generate skin file path from spec.
|
||||||
func SkinFileFromName(n string) string {
|
func SkinFileFromName(n string) string {
|
||||||
|
if n == "" {
|
||||||
|
n = "stock"
|
||||||
|
}
|
||||||
|
|
||||||
return filepath.Join(AppSkinsDir, n+".yaml")
|
return filepath.Join(AppSkinsDir, n+".yaml")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_initXDGLocs(t *testing.T) {
|
||||||
|
tmp, err := UserTmpDir()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
os.Unsetenv("XDG_CONFIG_HOME")
|
||||||
|
os.Unsetenv("XDG_CACHE_HOME")
|
||||||
|
os.Unsetenv("XDG_STATE_HOME")
|
||||||
|
os.Unsetenv("XDG_DATA_HOME")
|
||||||
|
|
||||||
|
os.Setenv("XDG_CONFIG_HOME", filepath.Join(tmp, "k9s-xdg", "config"))
|
||||||
|
os.Setenv("XDG_CACHE_HOME", filepath.Join(tmp, "k9s-xdg", "cache"))
|
||||||
|
os.Setenv("XDG_STATE_HOME", filepath.Join(tmp, "k9s-xdg", "state"))
|
||||||
|
os.Setenv("XDG_DATA_HOME", filepath.Join(tmp, "k9s-xdg", "data"))
|
||||||
|
xdg.Reload()
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
configDir string
|
||||||
|
configFile string
|
||||||
|
benchmarksDir string
|
||||||
|
contextsDir string
|
||||||
|
contextHotkeysFile string
|
||||||
|
contextConfig string
|
||||||
|
dumpsDir string
|
||||||
|
benchDir string
|
||||||
|
hkFile string
|
||||||
|
}{
|
||||||
|
"check-env": {
|
||||||
|
configDir: filepath.Join(tmp, "k9s-xdg", "config", "k9s"),
|
||||||
|
configFile: filepath.Join(tmp, "k9s-xdg", "config", "k9s", data.MainConfigFile),
|
||||||
|
benchmarksDir: filepath.Join(tmp, "k9s-xdg", "state", "k9s", "benchmarks"),
|
||||||
|
contextsDir: filepath.Join(tmp, "k9s-xdg", "data", "k9s", "clusters"),
|
||||||
|
contextHotkeysFile: filepath.Join(tmp, "k9s-xdg", "data", "k9s", "clusters", "cl-1", "ct-1-1", "hotkeys.yaml"),
|
||||||
|
contextConfig: filepath.Join(tmp, "k9s-xdg", "data", "k9s", "clusters", "cl-1", "ct-1-1", data.MainConfigFile),
|
||||||
|
dumpsDir: filepath.Join(tmp, "k9s-xdg", "state", "k9s", "screen-dumps", "cl-1", "ct-1-1"),
|
||||||
|
benchDir: filepath.Join(tmp, "k9s-xdg", "state", "k9s", "benchmarks", "cl-1", "ct-1-1"),
|
||||||
|
hkFile: filepath.Join(tmp, "k9s-xdg", "config", "k9s", "hotkeys.yaml"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
assert.NoError(t, initXDGLocs())
|
||||||
|
assert.Equal(t, u.configDir, AppConfigDir)
|
||||||
|
assert.Equal(t, u.configFile, AppConfigFile)
|
||||||
|
assert.Equal(t, u.benchmarksDir, AppBenchmarksDir)
|
||||||
|
assert.Equal(t, u.contextsDir, AppContextsDir)
|
||||||
|
assert.Equal(t, u.contextHotkeysFile, AppContextHotkeysFile("cl-1", "ct-1-1"))
|
||||||
|
assert.Equal(t, u.contextConfig, AppContextConfig("cl-1", "ct-1-1"))
|
||||||
|
dir, err := DumpsDir("cl-1", "ct-1-1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, u.dumpsDir, dir)
|
||||||
|
bdir, err := EnsureBenchmarksDir("cl-1", "ct-1-1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, u.benchDir, bdir)
|
||||||
|
hk, err := EnsureHotkeysCfgFile()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, u.hkFile, hk)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,10 +58,11 @@ func TestInitLogLoc(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsureBenchmarkCfg(t *testing.T) {
|
func TestEnsureBenchmarkCfg(t *testing.T) {
|
||||||
os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")
|
os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")
|
||||||
assert.NoError(t, config.InitLocs())
|
assert.NoError(t, config.InitLocs())
|
||||||
defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir))
|
defer assert.NoError(t, os.RemoveAll("/tmp/test-config"))
|
||||||
|
|
||||||
assert.NoError(t, data.EnsureFullPath("/tmp/test-config/clusters/cl-1/ct-2", data.DefaultDirMod))
|
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))
|
assert.NoError(t, os.WriteFile("/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", []byte{}, data.DefaultFileMod))
|
||||||
|
|
@ -95,3 +96,28 @@ func TestEnsureBenchmarkCfg(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSkinFileFromName(t *testing.T) {
|
||||||
|
config.AppSkinsDir = "/tmp/k9s-test/skins"
|
||||||
|
defer assert.NoError(t, os.RemoveAll("/tmp/k9s-test/skins"))
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
n string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
e: "/tmp/k9s-test/skins/stock.yaml",
|
||||||
|
},
|
||||||
|
"happy": {
|
||||||
|
n: "fred-blee",
|
||||||
|
e: "/tmp/k9s-test/skins/fred-blee.yaml",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
assert.Equal(t, u.e, config.SkinFileFromName(u.n))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewFlags(t *testing.T) {
|
||||||
|
config.AppDumpsDir = "/tmp/k9s-test/screen-dumps"
|
||||||
|
config.AppLogFile = "/tmp/k9s-test/k9s.log"
|
||||||
|
|
||||||
|
f := config.NewFlags()
|
||||||
|
assert.Equal(t, 2, *f.RefreshRate)
|
||||||
|
assert.Equal(t, "info", *f.LogLevel)
|
||||||
|
assert.Equal(t, "/tmp/k9s-test/k9s.log", *f.LogFile)
|
||||||
|
assert.Equal(t, config.AppDumpsDir, *f.ScreenDumpDir)
|
||||||
|
assert.Empty(t, *f.Command)
|
||||||
|
assert.False(t, *f.Headless)
|
||||||
|
assert.False(t, *f.Logoless)
|
||||||
|
assert.False(t, *f.AllNamespaces)
|
||||||
|
assert.False(t, *f.ReadOnly)
|
||||||
|
assert.False(t, *f.Write)
|
||||||
|
assert.False(t, *f.Crumbsless)
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,19 @@ import (
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func isBoolSet(b *bool) bool {
|
||||||
|
return b != nil && *b
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStringSet(s *string) bool {
|
||||||
|
return s != nil && len(*s) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func isYamlFile(file string) bool {
|
||||||
|
ext := filepath.Ext(file)
|
||||||
|
return ext == ".yml" || ext == ".yaml"
|
||||||
|
}
|
||||||
|
|
||||||
// isEnvSet checks if env var is set.
|
// isEnvSet checks if env var is set.
|
||||||
func isEnvSet(env string) bool {
|
func isEnvSet(env string) bool {
|
||||||
return os.Getenv(env) != ""
|
return os.Getenv(env) != ""
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -30,19 +33,32 @@ func NewHotKeys() HotKeys {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load K9s plugins.
|
// Load K9s plugins.
|
||||||
func (h HotKeys) Load() error {
|
func (h HotKeys) Load(path string) error {
|
||||||
return h.LoadHotKeys(AppHotKeysFile)
|
if err := h.LoadHotKeys(AppHotKeysFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.LoadHotKeys(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadHotKeys loads plugins from a given file.
|
// LoadHotKeys loads plugins from a given file.
|
||||||
func (h HotKeys) LoadHotKeys(path string) error {
|
func (h HotKeys) LoadHotKeys(path string) error {
|
||||||
f, err := os.ReadFile(path)
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bb, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := data.JSONValidator.Validate(json.HotkeysSchema, bb); err != nil {
|
||||||
|
return fmt.Errorf("validation failed for %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
var hh HotKeys
|
var hh HotKeys
|
||||||
if err := yaml.Unmarshal(f, &hh); err != nil {
|
if err := yaml.Unmarshal(bb, &hh); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for k, v := range hh.HotKey {
|
for k, v := range hh.HotKey {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
func TestHotKeyLoad(t *testing.T) {
|
func TestHotKeyLoad(t *testing.T) {
|
||||||
h := config.NewHotKeys()
|
h := config.NewHotKeys()
|
||||||
assert.Nil(t, h.LoadHotKeys("testdata/hotkeys.yaml"))
|
assert.NoError(t, h.LoadHotKeys("testdata/hotkeys/hotkeys.yaml"))
|
||||||
|
|
||||||
assert.Equal(t, 1, len(h.HotKey))
|
assert.Equal(t, 1, len(h.HotKey))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "K9s aliases schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"aliases": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": { "type": "string" },
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["aliases"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "K9s context config schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"k9s": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"cluster": { "type": "string" },
|
||||||
|
"readOnly": {"type": "boolean"},
|
||||||
|
"skin": { "type": "string" },
|
||||||
|
"portForwardAddress": { "type": "string" },
|
||||||
|
"namespace": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"active": {"type": "string"},
|
||||||
|
"lockFavorites": {"type": "boolean"},
|
||||||
|
"favorites": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"active": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"featureGates": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"nodeShell": { "type": "boolean" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["k9s"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "K9s hotkeys schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"hotKeys": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"shortCut": {"type": "string"},
|
||||||
|
"description": {"type": "string"},
|
||||||
|
"command": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["hotKeys"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "K9s config schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"k9s": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"liveViewAutoRefresh": { "type": "boolean" },
|
||||||
|
"screenDumpDir": {"type": "string"},
|
||||||
|
"refreshRate": { "type": "integer" },
|
||||||
|
"maxConnRetry": { "type": "integer" },
|
||||||
|
"readOnly": { "type": "boolean" },
|
||||||
|
"noExitOnCtrlC": { "type": "boolean" },
|
||||||
|
"skipLatestRevCheck": { "type": "boolean" },
|
||||||
|
"disablePodCounting": { "type": "boolean" },
|
||||||
|
"ui": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"enableMouse": {"type": "boolean"},
|
||||||
|
"headless": {"type": "boolean"},
|
||||||
|
"logoless": {"type": "boolean"},
|
||||||
|
"crumbsless": {"type": "boolean"},
|
||||||
|
"noIcons": {"type": "boolean"},
|
||||||
|
"reactive": {"type": "boolean"},
|
||||||
|
"skin": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shellPod": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"image": { "type": "string" },
|
||||||
|
"namespace": { "type": "string" },
|
||||||
|
"limits": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cpu": { "type": "string" },
|
||||||
|
"memory": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["cpu", "memory"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["image", "namespace", "limits"]
|
||||||
|
},
|
||||||
|
"imageScans": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"enable": { "type": "boolean" },
|
||||||
|
"namespace": { "type": "string" },
|
||||||
|
"exclusions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"namespaces": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["enable"]
|
||||||
|
},
|
||||||
|
"logger": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"tail": {"type": "integer"},
|
||||||
|
"buffer": {"type": "integer"},
|
||||||
|
"sinceSeconds": {"type": "integer"},
|
||||||
|
"fullScreen": {"type": "boolean"},
|
||||||
|
"textWrap": {"type": "boolean"},
|
||||||
|
"showTime": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thresholds": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"cpu": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"critical": {"type": "integer"},
|
||||||
|
"warn": {"type": "integer"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"critical": {"type": "integer"},
|
||||||
|
"warn": {"type": "integer"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["k9s"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "K9s plugins schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"plugins": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"shortCut": { "type": "string" },
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"confirm": { "type": "boolean" },
|
||||||
|
"scopes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
},
|
||||||
|
"command": { "type": "string" },
|
||||||
|
"background": { "type": "boolean" },
|
||||||
|
"args": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": ["string", "number"] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["shortCut", "description", "scopes", "command"]
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["plugins"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "K9s skin schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"properties": {
|
||||||
|
"k9s": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"body": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"logoColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"suggestColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"sectionColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"help": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"keyColor": {"type": "string"},
|
||||||
|
"numKeyColor": {"type": "string"},
|
||||||
|
"sectionColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dialog": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"buttonFgColor": {"type": "string"},
|
||||||
|
"buttonBgColor": {"type": "string"},
|
||||||
|
"buttonFocusFgColor": {"type": "string"},
|
||||||
|
"buttonFocusBgColor": {"type": "string"},
|
||||||
|
"labelFgColor": {"type": "string"},
|
||||||
|
"fieldFgColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"border": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"menu": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"keyColor": {"type": "string"},
|
||||||
|
"numKeyColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"crumbs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"keyColor": {"type": "string"},
|
||||||
|
"activeColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"newColor": {"type": "string"},
|
||||||
|
"modifyColor": {"type": "string"},
|
||||||
|
"addColor:": {"type": "string"},
|
||||||
|
"errorColor": {"type": "string"},
|
||||||
|
"highlightColor": {"type": "string"},
|
||||||
|
"killColor": {"type": "string"},
|
||||||
|
"completedColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor":{"type": "string"},
|
||||||
|
"highlightColor": {"type": "string"},
|
||||||
|
"counterColor":{"type": "string"},
|
||||||
|
"filterColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"charts": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"defaultDialColors": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"}
|
||||||
|
},
|
||||||
|
"defaultChartColors": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"table": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"cursorFgColor": {"type": "string"},
|
||||||
|
"cursorBgColor": {"type": "string"},
|
||||||
|
"header": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xray": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"cursorFgColor": {"type": "string"},
|
||||||
|
"graphicColor": {"type": "string"},
|
||||||
|
"showIcons": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"yaml": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"keyColor": {"type": "string"},
|
||||||
|
"colonColor": {"type": "string"},
|
||||||
|
"valueColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"indicator": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"fgColor": {"type": "string"},
|
||||||
|
"bgColor": {"type": "string"},
|
||||||
|
"toggleOnColor": {"type": "string"},
|
||||||
|
"toggleOffColor": {"type": "string"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "K9s views schema",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"views": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"sortColumn": { "type": "string" },
|
||||||
|
"columns": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["columns"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["views"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
aliases:
|
||||||
|
blee: duh
|
||||||
|
fred: zorg
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
alias:
|
||||||
|
blee: duh
|
||||||
|
fred: zorg
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
k9s:
|
||||||
|
cluster: kind-dashb
|
||||||
|
readOnly: false
|
||||||
|
skin: nightfox
|
||||||
|
namespace:
|
||||||
|
active: default
|
||||||
|
lockFavorites: false
|
||||||
|
favorites:
|
||||||
|
- kube-system
|
||||||
|
- default
|
||||||
|
view:
|
||||||
|
active: pod
|
||||||
|
featureGates:
|
||||||
|
nodeShell: false
|
||||||
|
portForwardAddress: localhost
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
k9s:
|
||||||
|
cluster: kind-dashb
|
||||||
|
readOnly: false
|
||||||
|
skin: nightfox
|
||||||
|
namespaces:
|
||||||
|
active: default
|
||||||
|
lockFavorites: false
|
||||||
|
favorites:
|
||||||
|
- kube-system
|
||||||
|
- default
|
||||||
|
view:
|
||||||
|
active: pod
|
||||||
|
fred: blee
|
||||||
|
featureGates:
|
||||||
|
nodeShell: false
|
||||||
|
portForwardAddress: localhost
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
hotKey:
|
||||||
|
shift-0:
|
||||||
|
shortCut: Shift-0
|
||||||
|
description: Popeye
|
||||||
|
command: popeye
|
||||||
|
shift-1:
|
||||||
|
shortCut: Shift-1
|
||||||
|
description: View deployments
|
||||||
|
command: dp
|
||||||
|
shift-2:
|
||||||
|
shortCut: Shift-2
|
||||||
|
description: View services
|
||||||
|
command: service
|
||||||
|
shift-3:
|
||||||
|
shortCut: Shift-3
|
||||||
|
description: View statefulsets
|
||||||
|
command: sts
|
||||||
|
shift-4:
|
||||||
|
shortCut: Shift-4
|
||||||
|
description: Xray Deployments
|
||||||
|
command: xray dp
|
||||||
|
shift-5:
|
||||||
|
shortCut: Shift-5
|
||||||
|
description: Xray StatefulSets
|
||||||
|
command: xray sts
|
||||||
|
shift-6:
|
||||||
|
shortCut: Shift-6
|
||||||
|
description: Xray DaemonSets
|
||||||
|
command: xray ds
|
||||||
|
shift-7:
|
||||||
|
shortCut: Shift-7
|
||||||
|
description: Xray Services
|
||||||
|
command: xray svc
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
k9s:
|
||||||
|
liveViewAutoRefresh: false
|
||||||
|
screenDumpDir: /Users/fernand/.local/state/k9s/screen-dumps
|
||||||
|
refreshRate: 2
|
||||||
|
maxConnRetry: 5
|
||||||
|
readOnly: false
|
||||||
|
noExitOnCtrlC: 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
|
||||||
|
imageScans:
|
||||||
|
enable: false
|
||||||
|
exclusions:
|
||||||
|
namespaces: []
|
||||||
|
labels: {}
|
||||||
|
logger:
|
||||||
|
tail: 100
|
||||||
|
buffer: 5000
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreen: false
|
||||||
|
textWrap: false
|
||||||
|
showTime: false
|
||||||
|
thresholds:
|
||||||
|
cpu:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
memory:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
k9s:
|
||||||
|
liveViewAutoRefresh: false
|
||||||
|
screenDumpDir: /Users/fernand/.local/state/k9s/screen-dumps
|
||||||
|
refreshRate: 2
|
||||||
|
maxConnRetry: 5
|
||||||
|
readOnly: false
|
||||||
|
noExitOnCtrlC: false
|
||||||
|
skipLatestRevCheck: false
|
||||||
|
disablePodCounting: false
|
||||||
|
shellPods:
|
||||||
|
image: busybox:1.35.0
|
||||||
|
namespace: default
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 100Mi
|
||||||
|
imageScans:
|
||||||
|
enable: false
|
||||||
|
exclusions:
|
||||||
|
namespaces: []
|
||||||
|
labels: {}
|
||||||
|
logger:
|
||||||
|
tail: 100
|
||||||
|
buffer: 5000
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreen: false
|
||||||
|
textWrap: false
|
||||||
|
showTime: false
|
||||||
|
thresholds:
|
||||||
|
cpu:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
memory:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
plugins:
|
||||||
|
blee:
|
||||||
|
shortCut: g
|
||||||
|
confirm: false
|
||||||
|
description: blee
|
||||||
|
scopes:
|
||||||
|
- namespaces
|
||||||
|
command: sh
|
||||||
|
background: false
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- "blee bla"
|
||||||
|
duh:
|
||||||
|
shortCut: h
|
||||||
|
confirm: true
|
||||||
|
description: duh
|
||||||
|
scopes:
|
||||||
|
- all
|
||||||
|
command: sh
|
||||||
|
background: true
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- "duh fred"
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
plugins:
|
||||||
|
blee:
|
||||||
|
shortCuts: g
|
||||||
|
confirm: false
|
||||||
|
description: blee
|
||||||
|
scopes:
|
||||||
|
- namespaces
|
||||||
|
command: sh
|
||||||
|
background: false
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- "blee bla"
|
||||||
|
duh:
|
||||||
|
shortCut: h
|
||||||
|
confirm: true
|
||||||
|
description: duh
|
||||||
|
command: sh
|
||||||
|
background: true
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- "duh fred"
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# K9s Nightfox Theme
|
||||||
|
# Based on the Nightfox.nvim color scheme:
|
||||||
|
# https://github.com/EdenEast/nightfox.nvim
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Styles...
|
||||||
|
foreground: &foreground "#cdcecf"
|
||||||
|
background: &background "#192330"
|
||||||
|
current_line: ¤t_line "#2b3b51"
|
||||||
|
selection: &selection "#2b3b51"
|
||||||
|
comment: &comment "#738091"
|
||||||
|
cyan: &cyan "#63cdcf"
|
||||||
|
green: &green "#81b29a"
|
||||||
|
orange: &orange "#f4a261"
|
||||||
|
magenta: &magenta "#9d79d6"
|
||||||
|
blue: &blue "#719cd6"
|
||||||
|
red: &red "#c94f6d"
|
||||||
|
|
||||||
|
# Skin...
|
||||||
|
k9s:
|
||||||
|
body:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
logoColor: *blue
|
||||||
|
prompt:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
suggestColor: *orange
|
||||||
|
info:
|
||||||
|
fgColor: *magenta
|
||||||
|
sectionColor: *foreground
|
||||||
|
help:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
keyColor: *magenta
|
||||||
|
numKeyColor: *magenta
|
||||||
|
sectionColor: *foreground
|
||||||
|
dialog:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
buttonFgColor: *foreground
|
||||||
|
buttonBgColor: *magenta
|
||||||
|
buttonFocusFgColor: white
|
||||||
|
buttonFocusBgColor: *cyan
|
||||||
|
labelFgColor: *orange
|
||||||
|
fieldFgColor: *foreground
|
||||||
|
frame:
|
||||||
|
border:
|
||||||
|
fgColor: *selection
|
||||||
|
focusColor: *current_line
|
||||||
|
menu:
|
||||||
|
fgColor: *foreground
|
||||||
|
keyColor: *magenta
|
||||||
|
numKeyColor: *magenta
|
||||||
|
crumbs:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *current_line
|
||||||
|
activeColor: *current_line
|
||||||
|
status:
|
||||||
|
newColor: *cyan
|
||||||
|
modifyColor: *blue
|
||||||
|
addColor: *green
|
||||||
|
errorColor: *red
|
||||||
|
highlightColor: *orange
|
||||||
|
killColor: *comment
|
||||||
|
completedColor: *comment
|
||||||
|
title:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *current_line
|
||||||
|
highlightColor: *orange
|
||||||
|
counterColor: *blue
|
||||||
|
filterColor: *magenta
|
||||||
|
views:
|
||||||
|
charts:
|
||||||
|
bgColor: default
|
||||||
|
defaultDialColors:
|
||||||
|
- *blue
|
||||||
|
- *red
|
||||||
|
defaultChartColors:
|
||||||
|
- *blue
|
||||||
|
- *red
|
||||||
|
table:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
cursorFgColor: *selection
|
||||||
|
cursorBgColor: *current_line
|
||||||
|
header:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
sorterColor: *cyan
|
||||||
|
xray:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
cursorColor: *current_line
|
||||||
|
graphicColor: *blue
|
||||||
|
showIcons: false
|
||||||
|
yaml:
|
||||||
|
keyColor: *magenta
|
||||||
|
colonColor: *blue
|
||||||
|
valueColor: *foreground
|
||||||
|
logs:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
indicator:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *selection
|
||||||
|
toggleOnColor: *magenta
|
||||||
|
toggleOffColor: *blue
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# K9s Nightfox Theme
|
||||||
|
# Based on the Nightfox.nvim color scheme:
|
||||||
|
# https://github.com/EdenEast/nightfox.nvim
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Styles...
|
||||||
|
foreground: &foreground "#cdcecf"
|
||||||
|
background: &background "#192330"
|
||||||
|
current_line: ¤t_line "#2b3b51"
|
||||||
|
selection: &selection "#2b3b51"
|
||||||
|
comment: &comment "#738091"
|
||||||
|
cyan: &cyan "#63cdcf"
|
||||||
|
green: &green "#81b29a"
|
||||||
|
orange: &orange "#f4a261"
|
||||||
|
magenta: &magenta "#9d79d6"
|
||||||
|
blue: &blue "#719cd6"
|
||||||
|
red: &red "#c94f6d"
|
||||||
|
|
||||||
|
# Skin...
|
||||||
|
k9s:
|
||||||
|
bodys:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
logoColor: *blue
|
||||||
|
prompt:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
suggestColor: *orange
|
||||||
|
info:
|
||||||
|
fgColor: *magenta
|
||||||
|
sectionColor: *foreground
|
||||||
|
dialog:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
buttonFgColor: *foreground
|
||||||
|
buttonBgColor: *magenta
|
||||||
|
buttonFocusFgColor: white
|
||||||
|
buttonFocusBgColor: *cyan
|
||||||
|
labelFgColor: *orange
|
||||||
|
fieldFgColor: *foreground
|
||||||
|
frame:
|
||||||
|
border:
|
||||||
|
fgColor: *selection
|
||||||
|
focusColor: *current_line
|
||||||
|
menu:
|
||||||
|
fgColor: *foreground
|
||||||
|
keyColor: *magenta
|
||||||
|
numKeyColor: *magenta
|
||||||
|
crumbs:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *current_line
|
||||||
|
activeColor: *current_line
|
||||||
|
status:
|
||||||
|
newColor: *cyan
|
||||||
|
modifyColor: *blue
|
||||||
|
addColor: *green
|
||||||
|
errorColor: *red
|
||||||
|
highlightColor: *orange
|
||||||
|
killColor: *comment
|
||||||
|
completedColor: *comment
|
||||||
|
title:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *current_line
|
||||||
|
highlightColor: *orange
|
||||||
|
counterColor: *blue
|
||||||
|
filterColor: *magenta
|
||||||
|
views:
|
||||||
|
charts:
|
||||||
|
bgColor: default
|
||||||
|
defaultDialColors:
|
||||||
|
- *blue
|
||||||
|
- *red
|
||||||
|
defaultChartColors:
|
||||||
|
- *blue
|
||||||
|
- *red
|
||||||
|
table:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
cursorFgColor: *selection
|
||||||
|
cursorBgColor: *current_line
|
||||||
|
header:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
sorterColor: *cyan
|
||||||
|
xray:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
cursorColor: *current_line
|
||||||
|
graphicColor: *blue
|
||||||
|
showIcons: false
|
||||||
|
yaml:
|
||||||
|
keyColor: *magenta
|
||||||
|
colonColor: *blue
|
||||||
|
valueColor: *foreground
|
||||||
|
logs:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *background
|
||||||
|
indicator:
|
||||||
|
fgColor: *foreground
|
||||||
|
bgColor: *selection
|
||||||
|
toggleOnColor: *magenta
|
||||||
|
toggleOffColor: *blue
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
views:
|
||||||
|
v1/nodes:
|
||||||
|
columns:
|
||||||
|
- NAME
|
||||||
|
- IP
|
||||||
|
v1/endpoints:
|
||||||
|
sortColumn: AGE:asc
|
||||||
|
columns:
|
||||||
|
- NAME
|
||||||
|
- NAMESPACE
|
||||||
|
- ENDPOINTS
|
||||||
|
- AGE
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
views:
|
||||||
|
v1/nodes:
|
||||||
|
v1/endpoints:
|
||||||
|
sortCol: AGE:asc
|
||||||
|
cols:
|
||||||
|
- NAME
|
||||||
|
- NAMESPACE
|
||||||
|
- ENDPOINTS
|
||||||
|
- AGE
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/xeipuuv/gojsonschema"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PluginsSchema describes plugins schema.
|
||||||
|
PluginsSchema = "plugins.json"
|
||||||
|
|
||||||
|
// AliasesSchema describes aliases schema.
|
||||||
|
AliasesSchema = "aliases.json"
|
||||||
|
|
||||||
|
// ViewsSchema describes views schema.
|
||||||
|
ViewsSchema = "views.json"
|
||||||
|
|
||||||
|
// HotkeysSchema describes hotkeys schema.
|
||||||
|
HotkeysSchema = "hotkeys.json"
|
||||||
|
|
||||||
|
// K9sSchema describes k9s config schema.
|
||||||
|
K9sSchema = "k9s.json"
|
||||||
|
|
||||||
|
// ContextSchema describes context config schema.
|
||||||
|
ContextSchema = "context.json"
|
||||||
|
|
||||||
|
// SkinSchema describes skin config schema.
|
||||||
|
SkinSchema = "skin.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed schemas/plugins.json
|
||||||
|
pluginSchema string
|
||||||
|
|
||||||
|
//go:embed schemas/aliases.json
|
||||||
|
aliasSchema string
|
||||||
|
|
||||||
|
//go:embed schemas/views.json
|
||||||
|
viewsSchema string
|
||||||
|
|
||||||
|
//go:embed schemas/k9s.json
|
||||||
|
k9sSchema string
|
||||||
|
|
||||||
|
//go:embed schemas/context.json
|
||||||
|
contextSchema string
|
||||||
|
|
||||||
|
//go:embed schemas/hotkeys.json
|
||||||
|
hotkeysSchema string
|
||||||
|
|
||||||
|
//go:embed schemas/skin.json
|
||||||
|
skinSchema string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validator tracks schemas validation.
|
||||||
|
type Validator struct {
|
||||||
|
schemas map[string]gojsonschema.JSONLoader
|
||||||
|
loader *gojsonschema.SchemaLoader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValidator returns a new instance.
|
||||||
|
func NewValidator() *Validator {
|
||||||
|
v := Validator{
|
||||||
|
schemas: map[string]gojsonschema.JSONLoader{
|
||||||
|
K9sSchema: gojsonschema.NewStringLoader(k9sSchema),
|
||||||
|
ContextSchema: gojsonschema.NewStringLoader(contextSchema),
|
||||||
|
AliasesSchema: gojsonschema.NewStringLoader(aliasSchema),
|
||||||
|
ViewsSchema: gojsonschema.NewStringLoader(viewsSchema),
|
||||||
|
PluginsSchema: gojsonschema.NewStringLoader(pluginSchema),
|
||||||
|
HotkeysSchema: gojsonschema.NewStringLoader(hotkeysSchema),
|
||||||
|
SkinSchema: gojsonschema.NewStringLoader(skinSchema),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
v.register()
|
||||||
|
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the schemas.
|
||||||
|
func (v *Validator) register() {
|
||||||
|
v.loader = gojsonschema.NewSchemaLoader()
|
||||||
|
v.loader.Validate = true
|
||||||
|
for k, s := range v.schemas {
|
||||||
|
if err := v.loader.AddSchema(k, s); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("schema initialization failed: %q", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate runs document thru given schema validation.
|
||||||
|
func (v *Validator) Validate(k string, bb []byte) error {
|
||||||
|
var m interface{}
|
||||||
|
err := yaml.Unmarshal(bb, &m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s, ok := v.schemas[k]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no schema found for: %q", k)
|
||||||
|
}
|
||||||
|
result, err := gojsonschema.Validate(s, gojsonschema.NewGoLoader(m))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result.Valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(result.Errors(), func(a, b gojsonschema.ResultError) int {
|
||||||
|
return cmp.Compare(a.Description(), b.Description())
|
||||||
|
})
|
||||||
|
var errs error
|
||||||
|
for _, re := range result.Errors() {
|
||||||
|
errs = errors.Join(errs, errors.New(re.Description()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) ValidateObj(k string, o any) error {
|
||||||
|
s, ok := v.schemas[k]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("no schema found for: %q", k)
|
||||||
|
}
|
||||||
|
result, err := gojsonschema.Validate(s, gojsonschema.NewGoLoader(o))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result.Valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(result.Errors(), func(a, b gojsonschema.ResultError) int {
|
||||||
|
return cmp.Compare(a.Description(), b.Description())
|
||||||
|
})
|
||||||
|
var errs error
|
||||||
|
for _, re := range result.Errors() {
|
||||||
|
errs = errors.Join(errs, errors.New(re.Description()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package json_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidatePluginDir(t *testing.T) {
|
||||||
|
skinDir := "../../../plugins"
|
||||||
|
ee, err := os.ReadDir(skinDir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
p := json.NewValidator()
|
||||||
|
for _, e := range ee {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(e.Name())
|
||||||
|
if ext == ".md" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name()))
|
||||||
|
assert.True(t, !strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name()))
|
||||||
|
bb, err := os.ReadFile(filepath.Join(skinDir, e.Name()))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, p.Validate(json.PluginsSchema, bb), e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSkinDir(t *testing.T) {
|
||||||
|
skinDir := "../../../skins"
|
||||||
|
ee, err := os.ReadDir(skinDir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
p := json.NewValidator()
|
||||||
|
for _, e := range ee {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ext := filepath.Ext(e.Name())
|
||||||
|
assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name()))
|
||||||
|
assert.True(t, !strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name()))
|
||||||
|
bb, err := os.ReadFile(filepath.Join(skinDir, e.Name()))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, p.Validate(json.SkinSchema, bb), e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSkin(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
f string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
f: "testdata/skins/cool.yaml",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
f: "testdata/skins/toast.yaml",
|
||||||
|
err: `Additional property bodys is not allowed`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := json.NewValidator()
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
bb, err := os.ReadFile(u.f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if err := v.Validate(json.SkinSchema, bb); err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateK9s(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
f string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
f: "testdata/k9s/cool.yaml",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
f: "testdata/k9s/toast.yaml",
|
||||||
|
err: `Additional property shellPods is not allowed`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := json.NewValidator()
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
bb, err := os.ReadFile(u.f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if err := v.Validate(json.K9sSchema, bb); err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateContext(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
f string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
f: "testdata/context/cool.yaml",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
f: "testdata/context/toast.yaml",
|
||||||
|
err: `Additional property fred is not allowed
|
||||||
|
Additional property namespaces is not allowed`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := json.NewValidator()
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
bb, err := os.ReadFile(u.f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if err := v.Validate(json.ContextSchema, bb); err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatePlugins(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
f string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
f: "testdata/plugins/cool.yaml",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
f: "testdata/plugins/toast.yaml",
|
||||||
|
err: `Additional property shortCuts is not allowed
|
||||||
|
scopes is required
|
||||||
|
shortCut is required`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := json.NewValidator()
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
bb, err := os.ReadFile(u.f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if err := v.Validate(json.PluginsSchema, bb); err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateAliases(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
f string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
f: "testdata/aliases/cool.yaml",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
f: "testdata/aliases/toast.yaml",
|
||||||
|
err: `Additional property alias is not allowed
|
||||||
|
aliases is required`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := json.NewValidator()
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
bb, err := os.ReadFile(u.f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if err := v.Validate(json.AliasesSchema, bb); err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateViews(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
f string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
"happy": {
|
||||||
|
f: "testdata/views/cool.yaml",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
f: "testdata/views/toast.yaml",
|
||||||
|
err: `Additional property cols is not allowed
|
||||||
|
Additional property sortCol is not allowed
|
||||||
|
Invalid type. Expected: object, given: null
|
||||||
|
columns is required`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
v := json.NewValidator()
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
bb, err := os.ReadFile(u.f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if err := v.Validate(json.ViewsSchema, bb); err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config/data"
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
|
@ -13,19 +14,19 @@ import (
|
||||||
|
|
||||||
// K9s tracks K9s configuration options.
|
// K9s tracks K9s configuration options.
|
||||||
type K9s struct {
|
type K9s struct {
|
||||||
LiveViewAutoRefresh bool `yaml:"liveViewAutoRefresh"`
|
LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"`
|
||||||
ScreenDumpDir string `yaml:"screenDumpDir,omitempty"`
|
ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"`
|
||||||
RefreshRate int `yaml:"refreshRate"`
|
RefreshRate int `json:"refreshRate" yaml:"refreshRate"`
|
||||||
MaxConnRetry int `yaml:"maxConnRetry"`
|
MaxConnRetry int `json:"maxConnRetry" yaml:"maxConnRetry"`
|
||||||
ReadOnly bool `yaml:"readOnly"`
|
ReadOnly bool `json:"readOnly" yaml:"readOnly"`
|
||||||
NoExitOnCtrlC bool `yaml:"noExitOnCtrlC"`
|
NoExitOnCtrlC bool `json:"noExitOnCtrlC" yaml:"noExitOnCtrlC"`
|
||||||
UI UI `yaml:"ui"`
|
UI UI `json:"ui" yaml:"ui"`
|
||||||
SkipLatestRevCheck bool `yaml:"skipLatestRevCheck"`
|
SkipLatestRevCheck bool `json:"skipLatestRevCheck" yaml:"skipLatestRevCheck"`
|
||||||
DisablePodCounting bool `yaml:"disablePodCounting"`
|
DisablePodCounting bool `json:"disablePodCounting" yaml:"disablePodCounting"`
|
||||||
ShellPod *ShellPod `yaml:"shellPod"`
|
ShellPod ShellPod `json:"shellPod" yaml:"shellPod"`
|
||||||
ImageScans *ImageScans `yaml:"imageScans"`
|
ImageScans ImageScans `json:"imageScans" yaml:"imageScans"`
|
||||||
Logger *Logger `yaml:"logger"`
|
Logger Logger `json:"logger" yaml:"logger"`
|
||||||
Thresholds Threshold `yaml:"thresholds"`
|
Thresholds Threshold `json:"thresholds" yaml:"thresholds"`
|
||||||
manualRefreshRate int
|
manualRefreshRate int
|
||||||
manualHeadless *bool
|
manualHeadless *bool
|
||||||
manualLogoless *bool
|
manualLogoless *bool
|
||||||
|
|
@ -38,6 +39,7 @@ type K9s struct {
|
||||||
activeConfig *data.Config
|
activeConfig *data.Config
|
||||||
conn client.Connection
|
conn client.Connection
|
||||||
ks data.KubeSettings
|
ks data.KubeSettings
|
||||||
|
mx sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewK9s create a new K9s configuration.
|
// NewK9s create a new K9s configuration.
|
||||||
|
|
@ -57,25 +59,32 @@ func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *K9s) resetConnection(conn client.Connection) {
|
func (k *K9s) resetConnection(conn client.Connection) {
|
||||||
|
k.mx.Lock()
|
||||||
|
defer k.mx.Unlock()
|
||||||
|
|
||||||
k.conn = conn
|
k.conn = conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the k9s config to dis.
|
// Save saves the k9s config to dis.
|
||||||
func (k *K9s) Save() error {
|
func (k *K9s) Save() error {
|
||||||
if k.activeConfig != nil {
|
if k.activeConfig == nil {
|
||||||
path := filepath.Join(
|
return fmt.Errorf("save failed. no active config detected")
|
||||||
AppContextsDir,
|
|
||||||
data.SanitizeContextSubpath(k.activeConfig.Context.ClusterName, k.activeContextName),
|
|
||||||
data.MainConfigFile,
|
|
||||||
)
|
|
||||||
return k.activeConfig.Save(path)
|
|
||||||
}
|
}
|
||||||
|
path := filepath.Join(
|
||||||
|
AppContextsDir,
|
||||||
|
data.SanitizeContextSubpath(k.activeConfig.Context.ClusterName, k.activeContextName),
|
||||||
|
data.MainConfigFile,
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return k.activeConfig.Save(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refine merges k9s configs.
|
// Merge merges k9s configs.
|
||||||
func (k *K9s) Refine(k1 *K9s) {
|
func (k *K9s) Merge(k1 *K9s) {
|
||||||
|
if k1 == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh
|
k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh
|
||||||
k.ScreenDumpDir = k1.ScreenDumpDir
|
k.ScreenDumpDir = k1.ScreenDumpDir
|
||||||
k.RefreshRate = k1.RefreshRate
|
k.RefreshRate = k1.RefreshRate
|
||||||
|
|
@ -86,56 +95,31 @@ func (k *K9s) Refine(k1 *K9s) {
|
||||||
k.SkipLatestRevCheck = k1.SkipLatestRevCheck
|
k.SkipLatestRevCheck = k1.SkipLatestRevCheck
|
||||||
k.DisablePodCounting = k1.DisablePodCounting
|
k.DisablePodCounting = k1.DisablePodCounting
|
||||||
k.ShellPod = k1.ShellPod
|
k.ShellPod = k1.ShellPod
|
||||||
k.ImageScans = k1.ImageScans
|
|
||||||
k.Logger = k1.Logger
|
k.Logger = k1.Logger
|
||||||
|
k.ImageScans = k1.ImageScans
|
||||||
k.Thresholds = k1.Thresholds
|
k.Thresholds = k1.Thresholds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override overrides k9s config from cli args.
|
// AppScreenDumpDir fetch screen dumps dir.
|
||||||
func (k *K9s) Override(k9sFlags *Flags) {
|
func (k *K9s) AppScreenDumpDir() string {
|
||||||
if *k9sFlags.RefreshRate != DefaultRefreshRate {
|
d := k.ScreenDumpDir
|
||||||
k.OverrideRefreshRate(*k9sFlags.RefreshRate)
|
if isStringSet(k.manualScreenDumpDir) {
|
||||||
|
d = *k.manualScreenDumpDir
|
||||||
|
k.ScreenDumpDir = d
|
||||||
|
}
|
||||||
|
if d == "" {
|
||||||
|
d = AppDumpsDir
|
||||||
}
|
}
|
||||||
|
|
||||||
k.OverrideHeadless(*k9sFlags.Headless)
|
return d
|
||||||
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.
|
// ContextScreenDumpDir fetch context specific screen dumps dir.
|
||||||
func (k *K9s) OverrideScreenDumpDir(dir string) {
|
func (k *K9s) ContextScreenDumpDir() string {
|
||||||
k.manualScreenDumpDir = &dir
|
return filepath.Join(k.AppScreenDumpDir(), k.contextPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetScreenDumpDir fetch screen dumps dir.
|
func (k *K9s) contextPath() string {
|
||||||
func (k *K9s) GetScreenDumpDir() string {
|
|
||||||
screenDumpDir := k.ScreenDumpDir
|
|
||||||
if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" {
|
|
||||||
screenDumpDir = *k.manualScreenDumpDir
|
|
||||||
}
|
|
||||||
if screenDumpDir == "" {
|
|
||||||
screenDumpDir = AppDumpsDir
|
|
||||||
}
|
|
||||||
|
|
||||||
return screenDumpDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset resets configuration and context.
|
|
||||||
func (k *K9s) Reset() {
|
|
||||||
k.activeConfig, k.activeContextName = nil, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveScreenDumpsDir fetch context specific screen dumps dir.
|
|
||||||
func (k *K9s) ActiveScreenDumpsDir() string {
|
|
||||||
return filepath.Join(k.GetScreenDumpDir(), k.ActiveContextDir())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveContextDir fetch current cluster/context path.
|
|
||||||
func (k *K9s) ActiveContextDir() string {
|
|
||||||
if k.activeConfig == nil {
|
if k.activeConfig == nil {
|
||||||
return "na"
|
return "na"
|
||||||
}
|
}
|
||||||
|
|
@ -146,33 +130,39 @@ func (k *K9s) ActiveContextDir() string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset resets configuration and context.
|
||||||
|
func (k *K9s) Reset() {
|
||||||
|
k.activeConfig, k.activeContextName = nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
// ActiveContextNamespace fetch the context active ns.
|
// ActiveContextNamespace fetch the context active ns.
|
||||||
func (k *K9s) ActiveContextNamespace() (string, error) {
|
func (k *K9s) ActiveContextNamespace() (string, error) {
|
||||||
if k.activeConfig != nil {
|
act, err := k.ActiveContext()
|
||||||
return k.activeConfig.Context.Namespace.Active, nil
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errors.New("context config is not set")
|
return act.Namespace.Active, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActiveContextName returns the active context name.
|
// ActiveContextName returns the active context name.
|
||||||
func (k *K9s) ActiveContextName() string {
|
func (k *K9s) ActiveContextName() string {
|
||||||
|
k.mx.RLock()
|
||||||
|
defer k.mx.RUnlock()
|
||||||
|
|
||||||
return k.activeContextName
|
return k.activeContextName
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActiveContext returns the currently active context.
|
// ActiveContext returns the currently active context.
|
||||||
func (k *K9s) ActiveContext() (*data.Context, error) {
|
func (k *K9s) ActiveContext() (*data.Context, error) {
|
||||||
if k.activeConfig != nil {
|
var ac *data.Config
|
||||||
if k.activeConfig.Context == nil {
|
k.mx.RLock()
|
||||||
ct, err := k.ks.CurrentContext()
|
ac = k.activeConfig
|
||||||
if err != nil {
|
k.mx.RUnlock()
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
k.activeConfig.Context = data.NewContextFromConfig(ct)
|
|
||||||
}
|
|
||||||
return k.activeConfig.Context, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if ac != nil && ac.Context != nil {
|
||||||
|
return ac.Context, nil
|
||||||
|
}
|
||||||
ct, err := k.ActivateContext(k.activeContextName)
|
ct, err := k.ActivateContext(k.activeContextName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -181,7 +171,7 @@ func (k *K9s) ActiveContext() (*data.Context, error) {
|
||||||
return ct, nil
|
return ct, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivateContext initializes the active context is not present.
|
// ActivateContext initializes the active context if not present.
|
||||||
func (k *K9s) ActivateContext(n string) (*data.Context, error) {
|
func (k *K9s) ActivateContext(n string) (*data.Context, error) {
|
||||||
k.activeContextName = n
|
k.activeContextName = n
|
||||||
ct, err := k.ks.GetContext(n)
|
ct, err := k.ks.GetContext(n)
|
||||||
|
|
@ -192,161 +182,128 @@ func (k *K9s) ActivateContext(n string) (*data.Context, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// If the context specifies a default namespace, use it!
|
|
||||||
if k.conn != nil {
|
k.Validate(k.conn, k.ks)
|
||||||
k.Validate(k.conn, k.ks)
|
// If the context specifies a namespace, use it!
|
||||||
if ns := k.conn.ActiveNamespace(); ns != client.BlankNamespace {
|
if ns := ct.Namespace; ns != client.BlankNamespace {
|
||||||
k.activeConfig.Context.Namespace.Active = ns
|
k.activeConfig.Context.Namespace.Active = ns
|
||||||
} else {
|
} else {
|
||||||
k.activeConfig.Context.Namespace.Active = client.DefaultNamespace
|
k.activeConfig.Context.Namespace.Active = client.DefaultNamespace
|
||||||
}
|
}
|
||||||
|
if k.activeConfig.Context == nil {
|
||||||
|
return nil, fmt.Errorf("context activation failed for: %s", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
return k.activeConfig.Context, nil
|
return k.activeConfig.Context, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload reloads the active config from disk.
|
// Reload reloads the context config from disk.
|
||||||
func (k *K9s) Reload() error {
|
func (k *K9s) Reload() error {
|
||||||
|
k.mx.Lock()
|
||||||
|
defer k.mx.Unlock()
|
||||||
|
|
||||||
ct, err := k.ks.GetContext(k.activeContextName)
|
ct, err := k.ks.GetContext(k.activeContextName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
k.activeConfig, err = k.dir.Load(k.activeContextName, ct)
|
k.activeConfig, err = k.dir.Load(k.activeContextName, ct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
k.activeConfig.Validate(k.conn, k.ks)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OverrideRefreshRate set the refresh rate manually.
|
// Override overrides k9s config from cli args.
|
||||||
func (k *K9s) OverrideRefreshRate(r int) {
|
func (k *K9s) Override(k9sFlags *Flags) {
|
||||||
k.manualRefreshRate = r
|
if k9sFlags.RefreshRate != nil && *k9sFlags.RefreshRate != DefaultRefreshRate {
|
||||||
}
|
k.manualRefreshRate = *k9sFlags.RefreshRate
|
||||||
|
|
||||||
// OverrideHeadless toggle the header manually.
|
|
||||||
func (k *K9s) OverrideHeadless(b bool) {
|
|
||||||
k.manualHeadless = &b
|
|
||||||
}
|
|
||||||
|
|
||||||
// OverrideLogoless toggle the k9s logo manually.
|
|
||||||
func (k *K9s) OverrideLogoless(b bool) {
|
|
||||||
k.manualLogoless = &b
|
|
||||||
}
|
|
||||||
|
|
||||||
// OverrideCrumbsless tooh the crumbslessness manually.
|
|
||||||
func (k *K9s) OverrideCrumbsless(b bool) {
|
|
||||||
k.manualCrumbsless = &b
|
|
||||||
}
|
|
||||||
|
|
||||||
// OverrideReadOnly set the readonly mode manually.
|
|
||||||
func (k *K9s) OverrideReadOnly(b bool) {
|
|
||||||
if b {
|
|
||||||
k.manualReadOnly = &b
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// OverrideWrite set the write mode manually.
|
k.manualHeadless = k9sFlags.Headless
|
||||||
func (k *K9s) OverrideWrite(b bool) {
|
k.manualLogoless = k9sFlags.Logoless
|
||||||
if b {
|
k.manualCrumbsless = k9sFlags.Crumbsless
|
||||||
var flag bool
|
if k9sFlags.ReadOnly != nil && *k9sFlags.ReadOnly {
|
||||||
k.manualReadOnly = &flag
|
k.manualReadOnly = k9sFlags.ReadOnly
|
||||||
}
|
}
|
||||||
}
|
if k9sFlags.Write != nil && *k9sFlags.Write {
|
||||||
|
var false bool
|
||||||
// OverrideCommand set the command manually.
|
k.manualReadOnly = &false
|
||||||
func (k *K9s) OverrideCommand(cmd string) {
|
}
|
||||||
k.manualCommand = &cmd
|
k.manualCommand = k9sFlags.Command
|
||||||
|
k.manualScreenDumpDir = k9sFlags.ScreenDumpDir
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsHeadless returns headless setting.
|
// IsHeadless returns headless setting.
|
||||||
func (k *K9s) IsHeadless() bool {
|
func (k *K9s) IsHeadless() bool {
|
||||||
h := k.UI.Headless
|
if isBoolSet(k.manualHeadless) {
|
||||||
if k.manualHeadless != nil && *k.manualHeadless {
|
return true
|
||||||
h = *k.manualHeadless
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h
|
return k.UI.Headless
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLogoless returns logoless setting.
|
// IsLogoless returns logoless setting.
|
||||||
func (k *K9s) IsLogoless() bool {
|
func (k *K9s) IsLogoless() bool {
|
||||||
h := k.UI.Logoless
|
if isBoolSet(k.manualLogoless) {
|
||||||
if k.manualLogoless != nil && *k.manualLogoless {
|
return true
|
||||||
h = *k.manualLogoless
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h
|
return k.UI.Logoless
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsCrumbsless returns crumbsless setting.
|
// IsCrumbsless returns crumbsless setting.
|
||||||
func (k *K9s) IsCrumbsless() bool {
|
func (k *K9s) IsCrumbsless() bool {
|
||||||
h := k.UI.Crumbsless
|
if isBoolSet(k.manualCrumbsless) {
|
||||||
if k.manualCrumbsless != nil && *k.manualCrumbsless {
|
return true
|
||||||
h = *k.manualCrumbsless
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return h
|
return k.UI.Crumbsless
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRefreshRate returns the current refresh rate.
|
// GetRefreshRate returns the current refresh rate.
|
||||||
func (k *K9s) GetRefreshRate() int {
|
func (k *K9s) GetRefreshRate() int {
|
||||||
rate := k.RefreshRate
|
|
||||||
if k.manualRefreshRate != 0 {
|
if k.manualRefreshRate != 0 {
|
||||||
rate = k.manualRefreshRate
|
return k.manualRefreshRate
|
||||||
}
|
}
|
||||||
|
|
||||||
return rate
|
return k.RefreshRate
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsReadOnly returns the readonly setting.
|
// IsReadOnly returns the readonly setting.
|
||||||
func (k *K9s) IsReadOnly() bool {
|
func (k *K9s) IsReadOnly() bool {
|
||||||
readOnly := k.ReadOnly
|
k.mx.RLock()
|
||||||
if k.manualReadOnly != nil {
|
defer k.mx.RUnlock()
|
||||||
readOnly = *k.manualReadOnly
|
|
||||||
|
ro := k.ReadOnly
|
||||||
|
if k.activeConfig != nil && k.activeConfig.Context.ReadOnly != nil {
|
||||||
|
ro = *k.activeConfig.Context.ReadOnly
|
||||||
}
|
}
|
||||||
if k.activeConfig != nil && k.activeConfig.Context.ReadOnly {
|
if k.manualReadOnly != nil {
|
||||||
readOnly = true
|
ro = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return readOnly
|
return ro
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *K9s) validateDefaults() {
|
// Validate the current configuration.
|
||||||
|
func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) {
|
||||||
if k.RefreshRate <= 0 {
|
if k.RefreshRate <= 0 {
|
||||||
k.RefreshRate = defaultRefreshRate
|
k.RefreshRate = defaultRefreshRate
|
||||||
}
|
}
|
||||||
if k.MaxConnRetry <= 0 {
|
if k.MaxConnRetry <= 0 {
|
||||||
k.MaxConnRetry = defaultMaxConnRetry
|
k.MaxConnRetry = defaultMaxConnRetry
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the current configuration.
|
|
||||||
func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) {
|
|
||||||
k.validateDefaults()
|
|
||||||
if k.activeConfig == nil {
|
if k.activeConfig == nil {
|
||||||
if n, err := ks.CurrentContextName(); err == nil {
|
if n, err := ks.CurrentContextName(); err == nil {
|
||||||
_, _ = k.ActivateContext(n)
|
_, _ = k.ActivateContext(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if k.ImageScans == nil {
|
k.ShellPod = k.ShellPod.Validate()
|
||||||
k.ImageScans = NewImageScans()
|
k.Logger = k.Logger.Validate()
|
||||||
}
|
k.Thresholds = k.Thresholds.Validate()
|
||||||
if k.ShellPod == nil {
|
|
||||||
k.ShellPod = NewShellPod()
|
|
||||||
}
|
|
||||||
k.ShellPod.Validate()
|
|
||||||
|
|
||||||
if k.Logger == nil {
|
|
||||||
k.Logger = NewLogger()
|
|
||||||
} else {
|
|
||||||
k.Logger.Validate()
|
|
||||||
}
|
|
||||||
if k.Thresholds == nil {
|
|
||||||
k.Thresholds = NewThreshold()
|
|
||||||
}
|
|
||||||
k.Thresholds.Validate()
|
|
||||||
|
|
||||||
if k.activeConfig != nil {
|
if k.activeConfig != nil {
|
||||||
k.activeConfig.Validate(c, ks)
|
k.activeConfig.Validate(c, ks)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_k9sOverrides(t *testing.T) {
|
||||||
|
var (
|
||||||
|
true = true
|
||||||
|
cmd = "po"
|
||||||
|
dir = "/tmp/blee"
|
||||||
|
)
|
||||||
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
k *K9s
|
||||||
|
rate int
|
||||||
|
ro, hl, cl, ll bool
|
||||||
|
}{
|
||||||
|
"plain": {
|
||||||
|
k: &K9s{
|
||||||
|
LiveViewAutoRefresh: false,
|
||||||
|
ScreenDumpDir: "",
|
||||||
|
RefreshRate: 10,
|
||||||
|
MaxConnRetry: 0,
|
||||||
|
ReadOnly: false,
|
||||||
|
NoExitOnCtrlC: false,
|
||||||
|
UI: UI{},
|
||||||
|
SkipLatestRevCheck: false,
|
||||||
|
DisablePodCounting: false,
|
||||||
|
},
|
||||||
|
rate: 10,
|
||||||
|
},
|
||||||
|
"set": {
|
||||||
|
k: &K9s{
|
||||||
|
LiveViewAutoRefresh: false,
|
||||||
|
ScreenDumpDir: "",
|
||||||
|
RefreshRate: 10,
|
||||||
|
MaxConnRetry: 0,
|
||||||
|
ReadOnly: true,
|
||||||
|
NoExitOnCtrlC: false,
|
||||||
|
UI: UI{
|
||||||
|
Headless: true,
|
||||||
|
Logoless: true,
|
||||||
|
Crumbsless: true,
|
||||||
|
},
|
||||||
|
SkipLatestRevCheck: false,
|
||||||
|
DisablePodCounting: false,
|
||||||
|
},
|
||||||
|
rate: 10,
|
||||||
|
ro: true,
|
||||||
|
hl: true,
|
||||||
|
ll: true,
|
||||||
|
cl: true,
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
k: &K9s{
|
||||||
|
LiveViewAutoRefresh: false,
|
||||||
|
ScreenDumpDir: "",
|
||||||
|
RefreshRate: 10,
|
||||||
|
MaxConnRetry: 0,
|
||||||
|
ReadOnly: false,
|
||||||
|
NoExitOnCtrlC: false,
|
||||||
|
UI: UI{
|
||||||
|
Headless: false,
|
||||||
|
Logoless: false,
|
||||||
|
Crumbsless: false,
|
||||||
|
},
|
||||||
|
SkipLatestRevCheck: false,
|
||||||
|
DisablePodCounting: false,
|
||||||
|
manualRefreshRate: 100,
|
||||||
|
manualReadOnly: &true,
|
||||||
|
manualHeadless: &true,
|
||||||
|
manualLogoless: &true,
|
||||||
|
manualCrumbsless: &true,
|
||||||
|
manualCommand: &cmd,
|
||||||
|
manualScreenDumpDir: &dir,
|
||||||
|
},
|
||||||
|
rate: 100,
|
||||||
|
ro: true,
|
||||||
|
hl: true,
|
||||||
|
ll: true,
|
||||||
|
cl: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
assert.Equal(t, u.rate, u.k.GetRefreshRate())
|
||||||
|
assert.Equal(t, u.ro, u.k.IsReadOnly())
|
||||||
|
assert.Equal(t, u.cl, u.k.IsCrumbsless())
|
||||||
|
assert.Equal(t, u.hl, u.k.IsHeadless())
|
||||||
|
assert.Equal(t, u.ll, u.k.IsLogoless())
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_screenDumpDirOverride(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
dir string
|
||||||
|
e string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
e: "/tmp/k9s-test/screen-dumps",
|
||||||
|
},
|
||||||
|
"override": {
|
||||||
|
dir: "/tmp/k9s-test/sd",
|
||||||
|
e: "/tmp/k9s-test/sd",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
cfg := NewConfig(nil)
|
||||||
|
assert.NoError(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||||
|
|
||||||
|
cfg.K9s.manualScreenDumpDir = &u.dir
|
||||||
|
assert.Equal(t, u.e, cfg.K9s.AppScreenDumpDir())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,40 +4,145 @@
|
||||||
package config_test
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/config/mock"
|
"github.com/derailed/k9s/internal/config/mock"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetScreenDumpDir(t *testing.T) {
|
func TestK9sReload(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
config.AppConfigDir = "/tmp/k9s-test"
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
cl, ct := "cl-1", "ct-1-1"
|
||||||
assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir())
|
|
||||||
|
uu := map[string]struct {
|
||||||
|
k *config.K9s
|
||||||
|
cl, ct string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
"no-context": {
|
||||||
|
k: config.NewK9s(
|
||||||
|
mock.NewMockConnection(),
|
||||||
|
mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{
|
||||||
|
ClusterName: &cl,
|
||||||
|
Context: &ct,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
err: errors.New(`no context found for: ""`),
|
||||||
|
},
|
||||||
|
"set-context": {
|
||||||
|
k: config.NewK9s(
|
||||||
|
mock.NewMockConnection(),
|
||||||
|
mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{
|
||||||
|
ClusterName: &cl,
|
||||||
|
Context: &ct,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ct: "ct-1-1",
|
||||||
|
cl: "cl-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
_, _ = u.k.ActivateContext(u.ct)
|
||||||
|
assert.Equal(t, u.err, u.k.Reload())
|
||||||
|
ct, err := u.k.ActiveContext()
|
||||||
|
assert.Equal(t, u.err, err)
|
||||||
|
if err == nil {
|
||||||
|
assert.Equal(t, u.cl, ct.ClusterName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetScreenDumpDirOverride(t *testing.T) {
|
func TestK9sMerge(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cl, ct := "cl-1", "ct-1-1"
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
uu := map[string]struct {
|
||||||
cfg.K9s.OverrideScreenDumpDir("/override")
|
k1, k2 *config.K9s
|
||||||
assert.Equal(t, "/override", cfg.K9s.GetScreenDumpDir())
|
ek *config.K9s
|
||||||
|
}{
|
||||||
|
"no-opt": {
|
||||||
|
k1: config.NewK9s(
|
||||||
|
mock.NewMockConnection(),
|
||||||
|
mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{
|
||||||
|
ClusterName: &cl,
|
||||||
|
Context: &ct,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ek: config.NewK9s(
|
||||||
|
mock.NewMockConnection(),
|
||||||
|
mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{
|
||||||
|
ClusterName: &cl,
|
||||||
|
Context: &ct,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"override": {
|
||||||
|
k1: &config.K9s{
|
||||||
|
LiveViewAutoRefresh: false,
|
||||||
|
ScreenDumpDir: "",
|
||||||
|
RefreshRate: 0,
|
||||||
|
MaxConnRetry: 0,
|
||||||
|
ReadOnly: false,
|
||||||
|
NoExitOnCtrlC: false,
|
||||||
|
UI: config.UI{},
|
||||||
|
SkipLatestRevCheck: false,
|
||||||
|
DisablePodCounting: false,
|
||||||
|
ShellPod: config.ShellPod{},
|
||||||
|
ImageScans: config.ImageScans{},
|
||||||
|
Logger: config.Logger{},
|
||||||
|
Thresholds: nil,
|
||||||
|
},
|
||||||
|
k2: &config.K9s{
|
||||||
|
LiveViewAutoRefresh: true,
|
||||||
|
MaxConnRetry: 100,
|
||||||
|
ShellPod: config.NewShellPod(),
|
||||||
|
},
|
||||||
|
ek: &config.K9s{
|
||||||
|
LiveViewAutoRefresh: true,
|
||||||
|
ScreenDumpDir: "",
|
||||||
|
RefreshRate: 0,
|
||||||
|
MaxConnRetry: 100,
|
||||||
|
ReadOnly: false,
|
||||||
|
NoExitOnCtrlC: false,
|
||||||
|
UI: config.UI{},
|
||||||
|
SkipLatestRevCheck: false,
|
||||||
|
DisablePodCounting: false,
|
||||||
|
ShellPod: config.NewShellPod(),
|
||||||
|
ImageScans: config.ImageScans{},
|
||||||
|
Logger: config.Logger{},
|
||||||
|
Thresholds: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
u.k1.Merge(u.k2)
|
||||||
|
assert.Equal(t, u.ek, u.k1)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetScreenDumpDirOverrideEmpty(t *testing.T) {
|
func TestContextScreenDumpDir(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cfg := mock.NewMockConfig()
|
||||||
|
_, err := cfg.K9s.ActivateContext("ct-1-1")
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
assert.NoError(t, err)
|
||||||
cfg.K9s.OverrideScreenDumpDir("")
|
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||||
assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir())
|
assert.Equal(t, "/tmp/k9s-test/screen-dumps/cl-1/ct-1-1", cfg.K9s.ContextScreenDumpDir())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetScreenDumpDirEmpty(t *testing.T) {
|
func TestAppScreenDumpDir(t *testing.T) {
|
||||||
cfg := mock.NewMockConfig()
|
cfg := mock.NewMockConfig()
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/k9s1.yaml"))
|
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||||
cfg.K9s.OverrideScreenDumpDir("")
|
assert.Equal(t, "/tmp/k9s-test/screen-dumps", cfg.K9s.AppScreenDumpDir())
|
||||||
assert.Equal(t, config.AppDumpsDir, cfg.K9s.GetScreenDumpDir())
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,17 @@ const (
|
||||||
|
|
||||||
// Logger tracks logger options.
|
// Logger tracks logger options.
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
TailCount int64 `yaml:"tail"`
|
TailCount int64 `json:"tail" yaml:"tail"`
|
||||||
BufferSize int `yaml:"buffer"`
|
BufferSize int `json:"buffer" yaml:"buffer"`
|
||||||
SinceSeconds int64 `yaml:"sinceSeconds"`
|
SinceSeconds int64 `json:"sinceSeconds" yaml:"sinceSeconds"`
|
||||||
FullScreenLogs bool `yaml:"fullScreenLogs"`
|
FullScreen bool `json:"fullScreen" yaml:"fullScreen"`
|
||||||
TextWrap bool `yaml:"textWrap"`
|
TextWrap bool `json:"textWrap" yaml:"textWrap"`
|
||||||
ShowTime bool `yaml:"showTime"`
|
ShowTime bool `json:"showTime" yaml:"showTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogger returns a new instance.
|
// NewLogger returns a new instance.
|
||||||
func NewLogger() *Logger {
|
func NewLogger() Logger {
|
||||||
return &Logger{
|
return Logger{
|
||||||
TailCount: DefaultLoggerTailCount,
|
TailCount: DefaultLoggerTailCount,
|
||||||
BufferSize: MaxLogThreshold,
|
BufferSize: MaxLogThreshold,
|
||||||
SinceSeconds: DefaultSinceSeconds,
|
SinceSeconds: DefaultSinceSeconds,
|
||||||
|
|
@ -34,7 +34,7 @@ func NewLogger() *Logger {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks thresholds and make sure we're cool. If not use defaults.
|
// Validate checks thresholds and make sure we're cool. If not use defaults.
|
||||||
func (l *Logger) Validate() {
|
func (l Logger) Validate() Logger {
|
||||||
if l.TailCount <= 0 {
|
if l.TailCount <= 0 {
|
||||||
l.TailCount = DefaultLoggerTailCount
|
l.TailCount = DefaultLoggerTailCount
|
||||||
}
|
}
|
||||||
|
|
@ -47,4 +47,6 @@ func (l *Logger) Validate() {
|
||||||
if l.SinceSeconds == 0 {
|
if l.SinceSeconds == 0 {
|
||||||
l.SinceSeconds = DefaultSinceSeconds
|
l.SinceSeconds = DefaultSinceSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
|
|
||||||
func TestNewLogger(t *testing.T) {
|
func TestNewLogger(t *testing.T) {
|
||||||
l := config.NewLogger()
|
l := config.NewLogger()
|
||||||
l.Validate()
|
l = l.Validate()
|
||||||
|
|
||||||
assert.Equal(t, int64(100), l.TailCount)
|
assert.Equal(t, int64(100), l.TailCount)
|
||||||
assert.Equal(t, 5000, l.BufferSize)
|
assert.Equal(t, 5000, l.BufferSize)
|
||||||
|
|
@ -20,7 +20,7 @@ func TestNewLogger(t *testing.T) {
|
||||||
|
|
||||||
func TestLoggerValidate(t *testing.T) {
|
func TestLoggerValidate(t *testing.T) {
|
||||||
var l config.Logger
|
var l config.Logger
|
||||||
l.Validate()
|
l = l.Validate()
|
||||||
|
|
||||||
assert.Equal(t, int64(100), l.TailCount)
|
assert.Equal(t, int64(100), l.TailCount)
|
||||||
assert.Equal(t, 5000, l.BufferSize)
|
assert.Equal(t, 5000, l.BufferSize)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func EnsureDir(d string) error {
|
||||||
|
|
||||||
func NewMockConfig() *config.Config {
|
func NewMockConfig() *config.Config {
|
||||||
config.AppContextsDir = "/tmp/test"
|
config.AppContextsDir = "/tmp/test"
|
||||||
cl, ct := "cl-1", "ct-1"
|
cl, ct := "cl-1", "ct-1-1"
|
||||||
flags := genericclioptions.ConfigFlags{
|
flags := genericclioptions.ConfigFlags{
|
||||||
ClusterName: &cl,
|
ClusterName: &cl,
|
||||||
Context: &ct,
|
Context: &ct,
|
||||||
|
|
@ -63,7 +63,7 @@ func NewMockKubeSettings(f *genericclioptions.ConfigFlags) mockKubeSettings {
|
||||||
},
|
},
|
||||||
ctId + "-2": {
|
ctId + "-2": {
|
||||||
Cluster: *f.ClusterName,
|
Cluster: *f.ClusterName,
|
||||||
Namespace: "ns-1",
|
Namespace: "ns-2",
|
||||||
},
|
},
|
||||||
ctId + "-3": {
|
ctId + "-3": {
|
||||||
Cluster: *f.ClusterName,
|
Cluster: *f.ClusterName,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
|
|
||||||
"github.com/adrg/xdg"
|
"github.com/adrg/xdg"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
@ -47,6 +50,7 @@ func NewPlugins() Plugins {
|
||||||
// Load K9s plugins.
|
// Load K9s plugins.
|
||||||
func (p Plugins) Load(path string) error {
|
func (p Plugins) Load(path string) error {
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
if err := p.load(AppPluginsFile); err != nil {
|
if err := p.load(AppPluginsFile); err != nil {
|
||||||
errs = errors.Join(errs, err)
|
errs = errors.Join(errs, err)
|
||||||
}
|
}
|
||||||
|
|
@ -74,12 +78,12 @@ func (p Plugins) loadPluginDir(dir string) error {
|
||||||
if file.IsDir() || !isYamlFile(file.Name()) {
|
if file.IsDir() || !isYamlFile(file.Name()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pluginFile, err := os.ReadFile(filepath.Join(dir, file.Name()))
|
bb, err := os.ReadFile(filepath.Join(dir, file.Name()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = errors.Join(errs, err)
|
errs = errors.Join(errs, err)
|
||||||
}
|
}
|
||||||
var plugin Plugin
|
var plugin Plugin
|
||||||
if err = yaml.Unmarshal(pluginFile, &plugin); err != nil {
|
if err = yaml.Unmarshal(bb, &plugin); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.Plugins[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin
|
p.Plugins[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin
|
||||||
|
|
@ -92,13 +96,15 @@ func (p *Plugins) load(path string) error {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
f, err := os.ReadFile(path)
|
bb, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := data.JSONValidator.Validate(json.PluginsSchema, bb); err != nil {
|
||||||
|
return fmt.Errorf("validation failed for %q: %w", path, err)
|
||||||
|
}
|
||||||
var pp Plugins
|
var pp Plugins
|
||||||
if err := yaml.Unmarshal(f, &pp); err != nil {
|
if err := yaml.Unmarshal(bb, &pp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for k, v := range pp.Plugins {
|
for k, v := range pp.Plugins {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/adrg/xdg"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -39,10 +41,24 @@ var test2YmlTestData = Plugin{
|
||||||
Background: true,
|
Background: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPluginLoad(t *testing.T) {
|
||||||
|
AppPluginsFile = "/tmp/k9s-test/fred.yaml"
|
||||||
|
os.Setenv("XDG_DATA_HOME", "/tmp/k9s-test")
|
||||||
|
xdg.Reload()
|
||||||
|
|
||||||
|
p := NewPlugins()
|
||||||
|
assert.NoError(t, p.Load("testdata/plugins.yaml"))
|
||||||
|
|
||||||
|
assert.Equal(t, 1, len(p.Plugins))
|
||||||
|
k, ok := p.Plugins["blah"]
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.ObjectsAreEqual(pluginYmlTestData, k)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSinglePluginFileLoad(t *testing.T) {
|
func TestSinglePluginFileLoad(t *testing.T) {
|
||||||
p := NewPlugins()
|
p := NewPlugins()
|
||||||
assert.Nil(t, p.load("testdata/plugins.yaml"))
|
assert.NoError(t, p.load("testdata/plugins.yaml"))
|
||||||
assert.Nil(t, p.loadPluginDir("/random/dir/not/exist"))
|
assert.NoError(t, p.loadPluginDir("/random/dir/not/exist"))
|
||||||
|
|
||||||
assert.Equal(t, 1, len(p.Plugins))
|
assert.Equal(t, 1, len(p.Plugins))
|
||||||
k, ok := p.Plugins["blah"]
|
k, ok := p.Plugins["blah"]
|
||||||
|
|
@ -53,8 +69,8 @@ func TestSinglePluginFileLoad(t *testing.T) {
|
||||||
|
|
||||||
func TestMultiplePluginFilesLoad(t *testing.T) {
|
func TestMultiplePluginFilesLoad(t *testing.T) {
|
||||||
p := NewPlugins()
|
p := NewPlugins()
|
||||||
assert.Nil(t, p.load("testdata/plugins.yaml"))
|
assert.NoError(t, p.load("testdata/plugins.yaml"))
|
||||||
assert.Nil(t, p.loadPluginDir("testdata/plugins"))
|
assert.NoError(t, p.loadPluginDir("testdata/plugins"))
|
||||||
|
|
||||||
testPlugins := map[string]Plugin{
|
testPlugins := map[string]Plugin{
|
||||||
"blah": pluginYmlTestData,
|
"blah": pluginYmlTestData,
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ func (l Labels) exclude(k, val string) bool {
|
||||||
|
|
||||||
// ScanExcludes tracks vul scan exclusions.
|
// ScanExcludes tracks vul scan exclusions.
|
||||||
type ScanExcludes struct {
|
type ScanExcludes struct {
|
||||||
Namespaces []string `yaml:"namespaces"`
|
Namespaces []string `json:"namespaces" yaml:"namespaces"`
|
||||||
Labels Labels `yaml:"labels"`
|
Labels Labels `json:"labels" yaml:"labels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newScanExcludes() ScanExcludes {
|
func newScanExcludes() ScanExcludes {
|
||||||
|
|
@ -50,19 +50,19 @@ func (b ScanExcludes) exclude(ns string, ll map[string]string) bool {
|
||||||
|
|
||||||
// ImageScans tracks vul scans options.
|
// ImageScans tracks vul scans options.
|
||||||
type ImageScans struct {
|
type ImageScans struct {
|
||||||
Enable bool `yaml:"enable"`
|
Enable bool `json:"enable" yaml:"enable"`
|
||||||
Exclusions ScanExcludes `yaml:"exclusions"`
|
Exclusions ScanExcludes `json:"exclusions" yaml:"exclusions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewImageScans returns a new instance.
|
// NewImageScans returns a new instance.
|
||||||
func NewImageScans() *ImageScans {
|
func NewImageScans() ImageScans {
|
||||||
return &ImageScans{
|
return ImageScans{
|
||||||
Exclusions: newScanExcludes(),
|
Exclusions: newScanExcludes(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldExclude checks if scan should be excluder given ns/labels
|
// ShouldExclude checks if scan should be excluder given ns/labels
|
||||||
func (i *ImageScans) ShouldExclude(ns string, ll map[string]string) bool {
|
func (i ImageScans) ShouldExclude(ns string, ll map[string]string) bool {
|
||||||
if !i.Enable {
|
if !i.Enable {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScansShouldExclude(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
sc config.ImageScans
|
||||||
|
ns string
|
||||||
|
ll map[string]string
|
||||||
|
e bool
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
sc: config.NewImageScans(),
|
||||||
|
},
|
||||||
|
"exclude-ns": {
|
||||||
|
sc: config.ImageScans{
|
||||||
|
Enable: true,
|
||||||
|
Exclusions: config.ScanExcludes{
|
||||||
|
Namespaces: []string{"ns-1", "ns-2", "ns-3"},
|
||||||
|
Labels: config.Labels{
|
||||||
|
"app": []string{"fred", "blee"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ns: "ns-1",
|
||||||
|
ll: map[string]string{
|
||||||
|
"app": "freddy",
|
||||||
|
},
|
||||||
|
e: true,
|
||||||
|
},
|
||||||
|
"include-ns": {
|
||||||
|
sc: config.ImageScans{
|
||||||
|
Enable: true,
|
||||||
|
Exclusions: config.ScanExcludes{
|
||||||
|
Namespaces: []string{"ns-1", "ns-2", "ns-3"},
|
||||||
|
Labels: config.Labels{
|
||||||
|
"app": []string{"fred", "blee"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ns: "ns-4",
|
||||||
|
ll: map[string]string{
|
||||||
|
"app": "bozo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"exclude-labels": {
|
||||||
|
sc: config.ImageScans{
|
||||||
|
Enable: true,
|
||||||
|
Exclusions: config.ScanExcludes{
|
||||||
|
Namespaces: []string{"ns-1", "ns-2", "ns-3"},
|
||||||
|
Labels: config.Labels{
|
||||||
|
"app": []string{"fred", "blee"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ns: "ns-4",
|
||||||
|
ll: map[string]string{
|
||||||
|
"app": "fred",
|
||||||
|
},
|
||||||
|
e: true,
|
||||||
|
},
|
||||||
|
"include-labels": {
|
||||||
|
sc: config.ImageScans{
|
||||||
|
Enable: true,
|
||||||
|
Exclusions: config.ScanExcludes{
|
||||||
|
Namespaces: []string{"ns-1", "ns-2", "ns-3"},
|
||||||
|
Labels: config.Labels{
|
||||||
|
"app": []string{"fred", "blee"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ns: "ns-4",
|
||||||
|
ll: map[string]string{
|
||||||
|
"app": "freddy",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
assert.Equal(t, u.e, u.sc.ShouldExclude(u.ns, u.ll))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,20 +14,20 @@ type Limits map[v1.ResourceName]string
|
||||||
|
|
||||||
// ShellPod represents k9s shell configuration.
|
// ShellPod represents k9s shell configuration.
|
||||||
type ShellPod struct {
|
type ShellPod struct {
|
||||||
Image string `yaml:"image"`
|
Image string `json:"image" yaml:"image"`
|
||||||
Command []string `yaml:"command,omitempty"`
|
Command []string `json:"command,omitempty" yaml:"command,omitempty"`
|
||||||
Args []string `yaml:"args,omitempty"`
|
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||||
Namespace string `yaml:"namespace"`
|
Namespace string `json:"namespace" yaml:"namespace"`
|
||||||
Limits Limits `yaml:"limits,omitempty"`
|
Limits Limits `json:"limits,omitempty" yaml:"limits,omitempty"`
|
||||||
Labels map[string]string `yaml:"labels,omitempty"`
|
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
|
||||||
ImagePullSecrets []v1.LocalObjectReference `yaml:"imagePullSecrets,omitempty"`
|
ImagePullSecrets []v1.LocalObjectReference `json:"imagePullSecrets,omitempty" yaml:"imagePullSecrets,omitempty"`
|
||||||
ImagePullPolicy v1.PullPolicy `yaml:"imagePullPolicy,omitempty"`
|
ImagePullPolicy v1.PullPolicy `json:"imagePullPolicy,omitempty" yaml:"imagePullPolicy,omitempty"`
|
||||||
TTY bool `yaml:"tty,omitempty"`
|
TTY bool `json:"tty,omitempty" yaml:"tty,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShellPod returns a new instance.
|
// NewShellPod returns a new instance.
|
||||||
func NewShellPod() *ShellPod {
|
func NewShellPod() ShellPod {
|
||||||
return &ShellPod{
|
return ShellPod{
|
||||||
Image: defaultDockerShellImage,
|
Image: defaultDockerShellImage,
|
||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
Limits: defaultLimits(),
|
Limits: defaultLimits(),
|
||||||
|
|
@ -35,13 +35,15 @@ func NewShellPod() *ShellPod {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the configuration.
|
// Validate validates the configuration.
|
||||||
func (s *ShellPod) Validate() {
|
func (s ShellPod) Validate() ShellPod {
|
||||||
if s.Image == "" {
|
if s.Image == "" {
|
||||||
s.Image = defaultDockerShellImage
|
s.Image = defaultDockerShellImage
|
||||||
}
|
}
|
||||||
if len(s.Limits) == 0 {
|
if len(s.Limits) == 0 {
|
||||||
s.Limits = defaultLimits()
|
s.Limits = defaultLimits()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultLimits() Limits {
|
func defaultLimits() Limits {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,11 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
"github.com/derailed/tcell/v2"
|
"github.com/derailed/tcell/v2"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
|
@ -18,204 +21,198 @@ type StyleListener interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Color represents a color.
|
|
||||||
Color string
|
|
||||||
|
|
||||||
// Colors tracks multiple colors.
|
|
||||||
Colors []Color
|
|
||||||
|
|
||||||
// Styles tracks K9s styling options.
|
// Styles tracks K9s styling options.
|
||||||
Styles struct {
|
Styles struct {
|
||||||
K9s Style `yaml:"k9s"`
|
K9s Style `json:"k9s" yaml:"k9s"`
|
||||||
listeners []StyleListener
|
listeners []StyleListener
|
||||||
}
|
}
|
||||||
|
|
||||||
// Style tracks K9s styles.
|
// Style tracks K9s styles.
|
||||||
Style struct {
|
Style struct {
|
||||||
Body Body `yaml:"body"`
|
Body Body `json:"body" yaml:"body"`
|
||||||
Prompt Prompt `yaml:"prompt"`
|
Prompt Prompt `json:"prompt" yaml:"prompt"`
|
||||||
Help Help `yaml:"help"`
|
Help Help `json:"help" yaml:"help"`
|
||||||
Frame Frame `yaml:"frame"`
|
Frame Frame `json:"frame" yaml:"frame"`
|
||||||
Info Info `yaml:"info"`
|
Info Info `json:"info" yaml:"info"`
|
||||||
Views Views `yaml:"views"`
|
Views Views `json:"views" yaml:"views"`
|
||||||
Dialog Dialog `yaml:"dialog"`
|
Dialog Dialog `json:"dialog" yaml:"dialog"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt tracks command styles
|
// Prompt tracks command styles
|
||||||
Prompt struct {
|
Prompt struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
SuggestColor Color `yaml:"suggestColor"`
|
SuggestColor Color `json:"" yaml:"suggestColor"`
|
||||||
Border PromptBorder `yaml:"border"`
|
Border PromptBorder `json:"" yaml:"border"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PromptBorder tracks the color of the prompt depending on its kind (e.g., command or filter)
|
// PromptBorder tracks the color of the prompt depending on its kind (e.g., command or filter)
|
||||||
PromptBorder struct {
|
PromptBorder struct {
|
||||||
CommandColor Color `yaml:"command"`
|
CommandColor Color `json:"command" yaml:"command"`
|
||||||
DefaultColor Color `yaml:"default"`
|
DefaultColor Color `json:"default" yaml:"default"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help tracks help styles.
|
// Help tracks help styles.
|
||||||
Help struct {
|
Help struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
SectionColor Color `yaml:"sectionColor"`
|
SectionColor Color `json:"sectionColor" yaml:"sectionColor"`
|
||||||
KeyColor Color `yaml:"keyColor"`
|
KeyColor Color `json:"keyColor" yaml:"keyColor"`
|
||||||
NumKeyColor Color `yaml:"numKeyColor"`
|
NumKeyColor Color `json:"numKeyColor" yaml:"numKeyColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Body tracks body styles.
|
// Body tracks body styles.
|
||||||
Body struct {
|
Body struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
LogoColor Color `yaml:"logoColor"`
|
LogoColor Color `json:"logoColor" yaml:"logoColor"`
|
||||||
LogoColorMsg Color `yaml:"logoColorMsg"`
|
LogoColorMsg Color `json:"logoColorMsg" yaml:"logoColorMsg"`
|
||||||
LogoColorInfo Color `yaml:"logoColorInfo"`
|
LogoColorInfo Color `json:"logoColorInfo" yaml:"logoColorInfo"`
|
||||||
LogoColorWarn Color `yaml:"logoColorWarn"`
|
LogoColorWarn Color `json:"logoColorWarn" yaml:"logoColorWarn"`
|
||||||
LogoColorError Color `yaml:"logoColorError"`
|
LogoColorError Color `json:"logoColorError" yaml:"logoColorError"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialog tracks dialog styles.
|
// Dialog tracks dialog styles.
|
||||||
Dialog struct {
|
Dialog struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
ButtonFgColor Color `yaml:"buttonFgColor"`
|
ButtonFgColor Color `json:"buttonFgColor" yaml:"buttonFgColor"`
|
||||||
ButtonBgColor Color `yaml:"buttonBgColor"`
|
ButtonBgColor Color `json:"buttonBgColor" yaml:"buttonBgColor"`
|
||||||
ButtonFocusFgColor Color `yaml:"buttonFocusFgColor"`
|
ButtonFocusFgColor Color `json:"buttonFocusFgColor" yaml:"buttonFocusFgColor"`
|
||||||
ButtonFocusBgColor Color `yaml:"buttonFocusBgColor"`
|
ButtonFocusBgColor Color `json:"buttonFocusBgColor" yaml:"buttonFocusBgColor"`
|
||||||
LabelFgColor Color `yaml:"labelFgColor"`
|
LabelFgColor Color `json:"labelFgColor" yaml:"labelFgColor"`
|
||||||
FieldFgColor Color `yaml:"fieldFgColor"`
|
FieldFgColor Color `json:"fieldFgColor" yaml:"fieldFgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Frame tracks frame styles.
|
// Frame tracks frame styles.
|
||||||
Frame struct {
|
Frame struct {
|
||||||
Title Title `yaml:"title"`
|
Title Title `json:"title" yaml:"title"`
|
||||||
Border Border `yaml:"border"`
|
Border Border `json:"border" yaml:"border"`
|
||||||
Menu Menu `yaml:"menu"`
|
Menu Menu `json:"menu" yaml:"menu"`
|
||||||
Crumb Crumb `yaml:"crumbs"`
|
Crumb Crumb `json:"crumbs" yaml:"crumbs"`
|
||||||
Status Status `yaml:"status"`
|
Status Status `json:"status" yaml:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Views tracks individual view styles.
|
// Views tracks individual view styles.
|
||||||
Views struct {
|
Views struct {
|
||||||
Table Table `yaml:"table"`
|
Table Table `json:"table" yaml:"table"`
|
||||||
Xray Xray `yaml:"xray"`
|
Xray Xray `json:"xray" yaml:"xray"`
|
||||||
Charts Charts `yaml:"charts"`
|
Charts Charts `json:"charts" yaml:"charts"`
|
||||||
Yaml Yaml `yaml:"yaml"`
|
Yaml Yaml `json:"yaml" yaml:"yaml"`
|
||||||
Picker Picker `yaml:"picker"`
|
Picker Picker `json:"picker" yaml:"picker"`
|
||||||
Log Log `yaml:"logs"`
|
Log Log `json:"logs" yaml:"logs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status tracks resource status styles.
|
// Status tracks resource status styles.
|
||||||
Status struct {
|
Status struct {
|
||||||
NewColor Color `yaml:"newColor"`
|
NewColor Color `json:"newColor" yaml:"newColor"`
|
||||||
ModifyColor Color `yaml:"modifyColor"`
|
ModifyColor Color `json:"modifyColor" yaml:"modifyColor"`
|
||||||
AddColor Color `yaml:"addColor"`
|
AddColor Color `json:"addColor" yaml:"addColor"`
|
||||||
PendingColor Color `yaml:"pendingColor"`
|
PendingColor Color `json:"pendingColor" yaml:"pendingColor"`
|
||||||
ErrorColor Color `yaml:"errorColor"`
|
ErrorColor Color `json:"errorColor" yaml:"errorColor"`
|
||||||
HighlightColor Color `yaml:"highlightColor"`
|
HighlightColor Color `json:"highlightColor" yaml:"highlightColor"`
|
||||||
KillColor Color `yaml:"killColor"`
|
KillColor Color `json:"killColor" yaml:"killColor"`
|
||||||
CompletedColor Color `yaml:"completedColor"`
|
CompletedColor Color `json:"completedColor" yaml:"completedColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log tracks Log styles.
|
// Log tracks Log styles.
|
||||||
Log struct {
|
Log struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
Indicator LogIndicator `yaml:"indicator"`
|
Indicator LogIndicator `json:"indicator" yaml:"indicator"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Picker tracks color when selecting containers
|
// Picker tracks color when selecting containers
|
||||||
Picker struct {
|
Picker struct {
|
||||||
MainColor Color `yaml:"mainColor"`
|
MainColor Color `json:"mainColor" yaml:"mainColor"`
|
||||||
FocusColor Color `yaml:"focusColor"`
|
FocusColor Color `json:"focusColor" yaml:"focusColor"`
|
||||||
ShortcutColor Color `yaml:"shortcutColor"`
|
ShortcutColor Color `json:"shortcutColor" yaml:"shortcutColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogIndicator tracks log view indicator.
|
// LogIndicator tracks log view indicator.
|
||||||
LogIndicator struct {
|
LogIndicator struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
ToggleOnColor Color `yaml:"toggleOnColor"`
|
ToggleOnColor Color `json:"toggleOnColor" yaml:"toggleOnColor"`
|
||||||
ToggleOffColor Color `yaml:"toggleOffColor"`
|
ToggleOffColor Color `json:"toggleOffColor" yaml:"toggleOffColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yaml tracks yaml styles.
|
// Yaml tracks yaml styles.
|
||||||
Yaml struct {
|
Yaml struct {
|
||||||
KeyColor Color `yaml:"keyColor"`
|
KeyColor Color `json:"keyColor" yaml:"keyColor"`
|
||||||
ValueColor Color `yaml:"valueColor"`
|
ValueColor Color `json:"valueColor" yaml:"valueColor"`
|
||||||
ColonColor Color `yaml:"colonColor"`
|
ColonColor Color `json:"colonColor" yaml:"colonColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title tracks title styles.
|
// Title tracks title styles.
|
||||||
Title struct {
|
Title struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
HighlightColor Color `yaml:"highlightColor"`
|
HighlightColor Color `json:"highlightColor" yaml:"highlightColor"`
|
||||||
CounterColor Color `yaml:"counterColor"`
|
CounterColor Color `json:"counterColor" yaml:"counterColor"`
|
||||||
FilterColor Color `yaml:"filterColor"`
|
FilterColor Color `json:"filterColor" yaml:"filterColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info tracks info styles.
|
// Info tracks info styles.
|
||||||
Info struct {
|
Info struct {
|
||||||
SectionColor Color `yaml:"sectionColor"`
|
SectionColor Color `json:"sectionColor" yaml:"sectionColor"`
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border tracks border styles.
|
// Border tracks border styles.
|
||||||
Border struct {
|
Border struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
FocusColor Color `yaml:"focusColor"`
|
FocusColor Color `json:"focusColor" yaml:"focusColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crumb tracks crumbs styles.
|
// Crumb tracks crumbs styles.
|
||||||
Crumb struct {
|
Crumb struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
ActiveColor Color `yaml:"activeColor"`
|
ActiveColor Color `json:"activeColor" yaml:"activeColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table tracks table styles.
|
// Table tracks table styles.
|
||||||
Table struct {
|
Table struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
CursorFgColor Color `yaml:"cursorFgColor"`
|
CursorFgColor Color `json:"cursorFgColor" yaml:"cursorFgColor"`
|
||||||
CursorBgColor Color `yaml:"cursorBgColor"`
|
CursorBgColor Color `json:"cursorBgColor" yaml:"cursorBgColor"`
|
||||||
MarkColor Color `yaml:"markColor"`
|
MarkColor Color `json:"markColor" yaml:"markColor"`
|
||||||
Header TableHeader `yaml:"header"`
|
Header TableHeader `json:"header" yaml:"header"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableHeader tracks table header styles.
|
// TableHeader tracks table header styles.
|
||||||
TableHeader struct {
|
TableHeader struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
SorterColor Color `yaml:"sorterColor"`
|
SorterColor Color `json:"sorterColor" yaml:"sorterColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Xray tracks xray styles.
|
// Xray tracks xray styles.
|
||||||
Xray struct {
|
Xray struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
CursorColor Color `yaml:"cursorColor"`
|
CursorColor Color `json:"cursorColor" yaml:"cursorColor"`
|
||||||
CursorTextColor Color `yaml:"cursorTextColor"`
|
CursorTextColor Color `json:"cursorTextColor" yaml:"cursorTextColor"`
|
||||||
GraphicColor Color `yaml:"graphicColor"`
|
GraphicColor Color `json:"graphicColor" yaml:"graphicColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu tracks menu styles.
|
// Menu tracks menu styles.
|
||||||
Menu struct {
|
Menu struct {
|
||||||
FgColor Color `yaml:"fgColor"`
|
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||||
KeyColor Color `yaml:"keyColor"`
|
KeyColor Color `json:"keyColor" yaml:"keyColor"`
|
||||||
NumKeyColor Color `yaml:"numKeyColor"`
|
NumKeyColor Color `json:"numKeyColor" yaml:"numKeyColor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charts tracks charts styles.
|
// Charts tracks charts styles.
|
||||||
Charts struct {
|
Charts struct {
|
||||||
BgColor Color `yaml:"bgColor"`
|
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||||
DialBgColor Color `yaml:"dialBgColor"`
|
DialBgColor Color `json:"dialBgColor" yaml:"dialBgColor"`
|
||||||
ChartBgColor Color `yaml:"chartBgColor"`
|
ChartBgColor Color `json:"chartBgColor" yaml:"chartBgColor"`
|
||||||
DefaultDialColors Colors `yaml:"defaultDialColors"`
|
DefaultDialColors Colors `json:"defaultDialColors" yaml:"defaultDialColors"`
|
||||||
DefaultChartColors Colors `yaml:"defaultChartColors"`
|
DefaultChartColors Colors `json:"defaultChartColors" yaml:"defaultChartColors"`
|
||||||
ResourceColors map[string]Colors `yaml:"resourceColors"`
|
ResourceColors map[string]Colors `json:"resourceColors" yaml:"resourceColors"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -442,7 +439,9 @@ func NewStyles() *Styles {
|
||||||
|
|
||||||
// Reset resets styles.
|
// Reset resets styles.
|
||||||
func (s *Styles) Reset() {
|
func (s *Styles) Reset() {
|
||||||
s.K9s = newStyle()
|
if err := yaml.Unmarshal(stockSkinTpl, s); err != nil {
|
||||||
|
s.K9s = newStyle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FgColor returns the foreground color.
|
// FgColor returns the foreground color.
|
||||||
|
|
@ -533,11 +532,14 @@ func (s *Styles) Views() Views {
|
||||||
|
|
||||||
// Load K9s configuration from file.
|
// Load K9s configuration from file.
|
||||||
func (s *Styles) Load(path string) error {
|
func (s *Styles) Load(path string) error {
|
||||||
f, err := os.ReadFile(path)
|
bb, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := yaml.Unmarshal(f, s); err != nil {
|
if err := data.JSONValidator.Validate(json.SkinSchema, bb); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(bb, s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -561,3 +563,9 @@ func (s *Styles) Update() {
|
||||||
|
|
||||||
s.fireStylesChanged()
|
s.fireStylesChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dump for debug.
|
||||||
|
func (s *Styles) Dump() {
|
||||||
|
bb, _ := yaml.Marshal(s)
|
||||||
|
fmt.Println(string(bb))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newStyle(t *testing.T) {
|
||||||
|
s := newStyle()
|
||||||
|
|
||||||
|
assert.Equal(t, Color("black"), s.Body.BgColor)
|
||||||
|
assert.Equal(t, Color("cadetblue"), s.Body.FgColor)
|
||||||
|
assert.Equal(t, Color("lightskyblue"), s.Frame.Status.NewColor)
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,14 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNewStyle(t *testing.T) {
|
||||||
|
s := config.NewStyles()
|
||||||
|
|
||||||
|
assert.Equal(t, config.Color("black"), s.K9s.Body.BgColor)
|
||||||
|
assert.Equal(t, config.Color("cadetblue"), s.K9s.Body.FgColor)
|
||||||
|
assert.Equal(t, config.Color("lightskyblue"), s.K9s.Frame.Status.NewColor)
|
||||||
|
}
|
||||||
|
|
||||||
func TestColor(t *testing.T) {
|
func TestColor(t *testing.T) {
|
||||||
uu := map[string]tcell.Color{
|
uu := map[string]tcell.Color{
|
||||||
"blah": tcell.ColorDefault,
|
"blah": tcell.ColorDefault,
|
||||||
|
|
@ -28,22 +36,9 @@ func TestColor(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSkinNone(t *testing.T) {
|
func TestSkinHappy(t *testing.T) {
|
||||||
s := config.NewStyles()
|
s := config.NewStyles()
|
||||||
assert.Nil(t, s.Load("testdata/empty_skin.yaml"))
|
assert.Nil(t, s.Load("../../skins/black-and-wtf.yaml"))
|
||||||
s.Update()
|
|
||||||
|
|
||||||
assert.Equal(t, "#5f9ea0", s.Body().FgColor.String())
|
|
||||||
assert.Equal(t, "#000000", s.Body().BgColor.String())
|
|
||||||
assert.Equal(t, "#000000", s.Table().BgColor.String())
|
|
||||||
assert.Equal(t, tcell.ColorCadetBlue.TrueColor(), s.FgColor())
|
|
||||||
assert.Equal(t, tcell.ColorBlack.TrueColor(), s.BgColor())
|
|
||||||
assert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSkin(t *testing.T) {
|
|
||||||
s := config.NewStyles()
|
|
||||||
assert.Nil(t, s.Load("testdata/black_and_wtf.yaml"))
|
|
||||||
s.Update()
|
s.Update()
|
||||||
|
|
||||||
assert.Equal(t, "#ffffff", s.Body().FgColor.String())
|
assert.Equal(t, "#ffffff", s.Body().FgColor.String())
|
||||||
|
|
@ -54,12 +49,38 @@ func TestSkin(t *testing.T) {
|
||||||
assert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor)
|
assert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSkinNotExits(t *testing.T) {
|
func TestSkinLoad(t *testing.T) {
|
||||||
s := config.NewStyles()
|
uu := map[string]struct {
|
||||||
assert.NotNil(t, s.Load("testdata/blee.yaml"))
|
f string
|
||||||
}
|
err string
|
||||||
|
}{
|
||||||
|
"not-exist": {
|
||||||
|
f: "testdata/skins/blee.yaml",
|
||||||
|
err: "open testdata/skins/blee.yaml: no such file or directory",
|
||||||
|
},
|
||||||
|
"toast": {
|
||||||
|
f: "testdata/skins/boarked.yaml",
|
||||||
|
err: `Additional property bgColor is not allowed
|
||||||
|
Additional property fgColor is not allowed
|
||||||
|
Additional property logoColor is not allowed
|
||||||
|
Invalid type. Expected: object, given: array`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func TestSkinBoarked(t *testing.T) {
|
for k := range uu {
|
||||||
s := config.NewStyles()
|
u := uu[k]
|
||||||
assert.NotNil(t, s.Load("testdata/skin_boarked.yaml"))
|
t.Run(k, func(t *testing.T) {
|
||||||
|
s := config.NewStyles()
|
||||||
|
err := s.Load(u.f)
|
||||||
|
if err != nil {
|
||||||
|
assert.Equal(t, u.err, err.Error())
|
||||||
|
}
|
||||||
|
assert.Equal(t, "#5f9ea0", s.Body().FgColor.String())
|
||||||
|
assert.Equal(t, "#000000", s.Body().BgColor.String())
|
||||||
|
assert.Equal(t, "#000000", s.Table().BgColor.String())
|
||||||
|
assert.Equal(t, tcell.ColorCadetBlue.TrueColor(), s.FgColor())
|
||||||
|
assert.Equal(t, tcell.ColorBlack.TrueColor(), s.BgColor())
|
||||||
|
assert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
aliases:
|
aliases:
|
||||||
dp: apps/v1/deployments
|
dp: deployments
|
||||||
sec: v1/secrets
|
sec: v1/secrets
|
||||||
jo: batch/v1/jobs
|
jo: jobs
|
||||||
cr: rbac.authorization.k8s.io/v1/clusterroles
|
cr: clusterroles
|
||||||
crb: rbac.authorization.k8s.io/v1/clusterrolebindings
|
crb: clusterrolebindings
|
||||||
ro: rbac.authorization.k8s.io/v1/roles
|
ro: roles
|
||||||
rb: rbac.authorization.k8s.io/v1/rolebindings
|
rb: rolebindings
|
||||||
np: networking.k8s.io/v1/networkpolicies
|
np: networkpolicies
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
k9s:
|
||||||
|
liveViewAutoRefresh: false
|
||||||
|
screenDumpDir: /tmp/k9s-test/screen-dumps
|
||||||
|
refreshRate: 2
|
||||||
|
maxConnRetry: 5
|
||||||
|
readOnly: false
|
||||||
|
noExitOnCtrlC: false
|
||||||
|
ui:
|
||||||
|
enableMouse: false
|
||||||
|
headless: false
|
||||||
|
logoless: false
|
||||||
|
crumbsless: false
|
||||||
|
reactive: false
|
||||||
|
noIcons: false
|
||||||
|
skipLatestRevCheck: false
|
||||||
|
disablePodCounting: false
|
||||||
|
shellPod:
|
||||||
|
image: busybox:1.35.0
|
||||||
|
namespace: default
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 100Mi
|
||||||
|
imageScans:
|
||||||
|
enable: false
|
||||||
|
exclusions:
|
||||||
|
namespaces: []
|
||||||
|
labels: {}
|
||||||
|
logger:
|
||||||
|
tail: 100
|
||||||
|
buffer: 5000
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreen: false
|
||||||
|
textWrap: false
|
||||||
|
showTime: false
|
||||||
|
thresholds:
|
||||||
|
cpu:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
memory:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
k9s:
|
||||||
|
liveViewAutoRefresh: true
|
||||||
|
screenDumpDir: /tmp/k9s-test/screen-dumps
|
||||||
|
refreshRate: 100
|
||||||
|
maxConnRetry: 5
|
||||||
|
readOnly: true
|
||||||
|
noExitOnCtrlC: false
|
||||||
|
ui:
|
||||||
|
enableMouse: false
|
||||||
|
headless: false
|
||||||
|
logoless: false
|
||||||
|
crumbsless: false
|
||||||
|
reactive: false
|
||||||
|
noIcons: false
|
||||||
|
skipLatestRevCheck: false
|
||||||
|
disablePodCounting: false
|
||||||
|
shellPod:
|
||||||
|
image: busybox:1.35.0
|
||||||
|
namespace: default
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 100Mi
|
||||||
|
imageScans:
|
||||||
|
enable: false
|
||||||
|
exclusions:
|
||||||
|
namespaces: []
|
||||||
|
labels: {}
|
||||||
|
logger:
|
||||||
|
tail: 500
|
||||||
|
buffer: 800
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreen: false
|
||||||
|
textWrap: false
|
||||||
|
showTime: false
|
||||||
|
thresholds:
|
||||||
|
cpu:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
memory:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
k9s:
|
||||||
|
liveViewAutoRefresh: true
|
||||||
|
screenDumpDir: /tmp/k9s-test/screen-dumps
|
||||||
|
refreshRate: 2
|
||||||
|
maxConnRetry: 5
|
||||||
|
readOnly: false
|
||||||
|
noExitOnCtrlC: false
|
||||||
|
ui:
|
||||||
|
enableMouse: false
|
||||||
|
headless: false
|
||||||
|
logoless: false
|
||||||
|
crumbsless: false
|
||||||
|
reactive: false
|
||||||
|
noIcons: false
|
||||||
|
skipLatestRevCheck: false
|
||||||
|
disablePodCounting: false
|
||||||
|
shellPod:
|
||||||
|
image: busybox:1.35.0
|
||||||
|
namespace: default
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 100Mi
|
||||||
|
imageScans:
|
||||||
|
enable: false
|
||||||
|
exclusions:
|
||||||
|
namespaces: []
|
||||||
|
labels: {}
|
||||||
|
logger:
|
||||||
|
tail: 200
|
||||||
|
buffer: 2000
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreen: false
|
||||||
|
textWrap: false
|
||||||
|
showTime: false
|
||||||
|
thresholds:
|
||||||
|
cpu:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
memory:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
k9s:
|
||||||
|
liveViewAutoRefresh: true
|
||||||
|
screenDumpDir: /tmp/screen-dumps
|
||||||
|
refreshRate: 2
|
||||||
|
maxConnRetry: 5
|
||||||
|
readOnly: false
|
||||||
|
noExitOnCtrlC: false
|
||||||
|
ui:
|
||||||
|
enableMouse: false
|
||||||
|
headless: false
|
||||||
|
logoless: false
|
||||||
|
crumbsless: false
|
||||||
|
noIcons: false
|
||||||
|
skipLatestRevCheck: yes
|
||||||
|
disablePodCounts: false
|
||||||
|
shellPods:
|
||||||
|
image: busybox:1.35.0
|
||||||
|
namespace: default
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 100Mi
|
||||||
|
imageScans:
|
||||||
|
enable: false
|
||||||
|
exclusions:
|
||||||
|
namespaces: []
|
||||||
|
labels: {}
|
||||||
|
logger:
|
||||||
|
tail: 200
|
||||||
|
buffer: 2000
|
||||||
|
sinceSeconds: -1
|
||||||
|
fullScreen: false
|
||||||
|
textWrap: false
|
||||||
|
showTime: false
|
||||||
|
thresholds:
|
||||||
|
cpu:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
memory:
|
||||||
|
critical: 90
|
||||||
|
warn: 70
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
k9s:
|
|
||||||
liveViewAutoRefresh: true
|
|
||||||
refreshRate: 2
|
|
||||||
readOnly: false
|
|
||||||
logger:
|
|
||||||
tail: 200
|
|
||||||
buffer: 2000
|
|
||||||
currentContext: minikube
|
|
||||||
contexts:
|
|
||||||
minikube:
|
|
||||||
cluster: minikube
|
|
||||||
namespace:
|
|
||||||
active: kube-system
|
|
||||||
favorites:
|
|
||||||
- default
|
|
||||||
- kube-public
|
|
||||||
- istio-system
|
|
||||||
- all
|
|
||||||
- kube-system
|
|
||||||
view:
|
|
||||||
active: ctx
|
|
||||||
fred:
|
|
||||||
cluster: fred
|
|
||||||
namespace:
|
|
||||||
active: default
|
|
||||||
favorites:
|
|
||||||
- default
|
|
||||||
- kube-public
|
|
||||||
- istio-system
|
|
||||||
- all
|
|
||||||
- kube-system
|
|
||||||
view:
|
|
||||||
active: po
|
|
||||||
screenDumpDir: /tmp
|
|
||||||
disablePodCounting: false
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
k9s:
|
|
||||||
refreshRate: 10
|
|
||||||
namespace:
|
|
||||||
active: fred
|
|
||||||
favorites:
|
|
||||||
- blee
|
|
||||||
- duh
|
|
||||||
- crap
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
k9s:
|
|
||||||
refreshRate: 2
|
|
||||||
logBufferSize: 200
|
|
||||||
namespace:
|
|
||||||
active: kube-system
|
|
||||||
favorites:
|
|
||||||
- default
|
|
||||||
- kube-public
|
|
||||||
- istio-system
|
|
||||||
- all
|
|
||||||
- kube-system
|
|
||||||
view:
|
|
||||||
active: ctx
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
k9s:
|
|
||||||
refreshRate: 2
|
|
||||||
readOnly: true
|
|
||||||
logger:
|
|
||||||
tail: 200
|
|
||||||
buffer: 2000
|
|
||||||
currentContext: minikube
|
|
||||||
contexts:
|
|
||||||
minikube:
|
|
||||||
namespace:
|
|
||||||
active: kube-system
|
|
||||||
favorites:
|
|
||||||
- default
|
|
||||||
- kube-public
|
|
||||||
- istio-system
|
|
||||||
- all
|
|
||||||
- kube-system
|
|
||||||
view:
|
|
||||||
active: ctx
|
|
||||||
fred:
|
|
||||||
namespace:
|
|
||||||
active: default
|
|
||||||
favorites:
|
|
||||||
- default
|
|
||||||
- kube-public
|
|
||||||
- istio-system
|
|
||||||
- all
|
|
||||||
- kube-system
|
|
||||||
view:
|
|
||||||
active: po
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
k9s:
|
|
||||||
refreshRate: 2
|
|
||||||
logBufferSize: 200
|
|
||||||
currentContext: minikube
|
|
||||||
contexts:
|
|
||||||
minikube:
|
|
||||||
namespace:
|
|
||||||
active: kube-system
|
|
||||||
favorites:
|
|
||||||
- default
|
|
||||||
- kube-public
|
|
||||||
- istio-system
|
|
||||||
- all
|
|
||||||
- kube-system
|
|
||||||
view:
|
|
||||||
active: ctx
|
|
||||||
fred:
|
|
||||||
namespace:
|
|
||||||
active: default
|
|
||||||
favorites:
|
|
||||||
- default
|
|
||||||
- kube-public
|
|
||||||
- istio-system
|
|
||||||
- all
|
|
||||||
- kube-system
|
|
||||||
view:
|
|
||||||
active: po
|
|
||||||
|
|
@ -5,18 +5,27 @@ clusters:
|
||||||
certificate-authority: /Users/test/ca.crt
|
certificate-authority: /Users/test/ca.crt
|
||||||
server: https://1.2.3.4:8443
|
server: https://1.2.3.4:8443
|
||||||
name: cl-1
|
name: cl-1
|
||||||
|
- cluster:
|
||||||
|
certificate-authority: /Users/test/ca.crt
|
||||||
|
server: https://5.6.7.8:8443
|
||||||
|
name: cl-2
|
||||||
contexts:
|
contexts:
|
||||||
- context:
|
- context:
|
||||||
cluster: cl-1
|
cluster: cl-1
|
||||||
user: user1
|
user: user1
|
||||||
namespace: ns-1
|
namespace: ns-1
|
||||||
name: ct-1
|
name: ct-1-1
|
||||||
- context:
|
- context:
|
||||||
cluster: cl-1
|
cluster: cl-1
|
||||||
user: user2
|
user: user2
|
||||||
namespace: ns-2
|
namespace: ns-2
|
||||||
name: ct-2
|
name: ct-1-2
|
||||||
current-context: ct-1
|
- context:
|
||||||
|
cluster: cl-2
|
||||||
|
user: user2
|
||||||
|
namespace: ns-2
|
||||||
|
name: ct-2-1
|
||||||
|
current-context: ct-1-1
|
||||||
preferences: {}
|
preferences: {}
|
||||||
users:
|
users:
|
||||||
- name: user1
|
- name: user1
|
||||||
|
|
@ -31,11 +31,12 @@ k9s:
|
||||||
highlightColor: navajowhite
|
highlightColor: navajowhite
|
||||||
counterColor: navajowhite
|
counterColor: navajowhite
|
||||||
filterColor: slategray
|
filterColor: slategray
|
||||||
table:
|
views:
|
||||||
fgColor: white
|
table:
|
||||||
bgColor: black
|
fgColor: white
|
||||||
cursorColor: white
|
|
||||||
header:
|
|
||||||
fgColor: darkgray
|
|
||||||
bgColor: black
|
bgColor: black
|
||||||
sorterColor: white
|
cursorColor: white
|
||||||
|
header:
|
||||||
|
fgColor: darkgray
|
||||||
|
bgColor: black
|
||||||
|
sorterColor: white
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
k9s:
|
||||||
|
body:
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
k9s:
|
|
||||||
views:
|
|
||||||
v1/pods:
|
|
||||||
columns:
|
|
||||||
- NAMESPACE
|
|
||||||
- NAME
|
|
||||||
- AGE
|
|
||||||
- IP
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
views:
|
||||||
|
v1/pods:
|
||||||
|
columns:
|
||||||
|
- NAMESPACE
|
||||||
|
- NAME
|
||||||
|
- AGE
|
||||||
|
- IP
|
||||||
|
|
@ -61,7 +61,7 @@ func NewThreshold() Threshold {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate a namespace is setup correctly.
|
// Validate a namespace is setup correctly.
|
||||||
func (t Threshold) Validate() {
|
func (t Threshold) Validate() Threshold {
|
||||||
for _, k := range []string{"cpu", "memory"} {
|
for _, k := range []string{"cpu", "memory"} {
|
||||||
v, ok := t[k]
|
v, ok := t[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -70,6 +70,8 @@ func (t Threshold) Validate() {
|
||||||
v.Validate()
|
v.Validate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// LevelFor returns a defcon level for the current state.
|
// LevelFor returns a defcon level for the current state.
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,24 @@ const (
|
||||||
// UI tracks ui specific configs.
|
// UI tracks ui specific configs.
|
||||||
type UI struct {
|
type UI struct {
|
||||||
// EnableMouse toggles mouse support.
|
// EnableMouse toggles mouse support.
|
||||||
EnableMouse bool `yaml:"enableMouse"`
|
EnableMouse bool `json:"enableMouse" yaml:"enableMouse"`
|
||||||
|
|
||||||
// Headless toggles top header display.
|
// Headless toggles top header display.
|
||||||
Headless bool `yaml:"headless"`
|
Headless bool `json:"headless" yaml:"headless"`
|
||||||
|
|
||||||
// LogoLess toggles k9s logo.
|
// LogoLess toggles k9s logo.
|
||||||
Logoless bool `yaml:"logoless"`
|
Logoless bool `json:"logoless" yaml:"logoless"`
|
||||||
|
|
||||||
// Crumbsless toggles nav crumb display.
|
// Crumbsless toggles nav crumb display.
|
||||||
Crumbsless bool `yaml:"crumbsless"`
|
Crumbsless bool `json:"crumbsless" yaml:"crumbsless"`
|
||||||
|
|
||||||
|
// Reactive toggles reactive ui changes.
|
||||||
|
Reactive bool `json:"reactive" yaml:"reactive"`
|
||||||
|
|
||||||
// NoIcons toggles icons display.
|
// NoIcons toggles icons display.
|
||||||
NoIcons bool `yaml:"noIcons"`
|
NoIcons bool `json:"noIcons" yaml:"noIcons"`
|
||||||
|
|
||||||
// Skin reference the general k9s skin name.
|
// Skin reference the general k9s skin name.
|
||||||
// Can be overridden per context.
|
// Can be overridden per context.
|
||||||
Skin string `yaml:"skin,omitempty"`
|
Skin string `json:"skin" yaml:"skin,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,12 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
"github.com/derailed/k9s/internal/config/json"
|
||||||
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,51 +25,41 @@ type ViewSetting struct {
|
||||||
SortColumn string `yaml:"sortColumn"`
|
SortColumn string `yaml:"sortColumn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewSettings represent a collection of view configurations.
|
|
||||||
type ViewSettings struct {
|
|
||||||
Views map[string]ViewSetting `yaml:"views"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewViewSettings returns a new configuration.
|
|
||||||
func NewViewSettings() ViewSettings {
|
|
||||||
return ViewSettings{
|
|
||||||
Views: make(map[string]ViewSetting),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomView represents a collection of view customization.
|
// CustomView represents a collection of view customization.
|
||||||
type CustomView struct {
|
type CustomView struct {
|
||||||
K9s ViewSettings `yaml:"k9s"`
|
Views map[string]ViewSetting `yaml:"views"`
|
||||||
listeners map[string]ViewConfigListener
|
listeners map[string]ViewConfigListener
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCustomView returns a views configuration.
|
// NewCustomView returns a views configuration.
|
||||||
func NewCustomView() *CustomView {
|
func NewCustomView() *CustomView {
|
||||||
return &CustomView{
|
return &CustomView{
|
||||||
K9s: NewViewSettings(),
|
Views: make(map[string]ViewSetting),
|
||||||
listeners: make(map[string]ViewConfigListener),
|
listeners: make(map[string]ViewConfigListener),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset clears out configurations.
|
// Reset clears out configurations.
|
||||||
func (v *CustomView) Reset() {
|
func (v *CustomView) Reset() {
|
||||||
for k := range v.K9s.Views {
|
for k := range v.Views {
|
||||||
delete(v.K9s.Views, k)
|
delete(v.Views, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads view configurations.
|
// Load loads view configurations.
|
||||||
func (v *CustomView) Load(path string) error {
|
func (v *CustomView) Load(path string) error {
|
||||||
raw, err := os.ReadFile(path)
|
bb, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := data.JSONValidator.Validate(json.ViewsSchema, bb); err != nil {
|
||||||
|
return fmt.Errorf("validation failed for %q: %w", path, err)
|
||||||
|
}
|
||||||
var in CustomView
|
var in CustomView
|
||||||
if err := yaml.Unmarshal(raw, &in); err != nil {
|
if err := yaml.Unmarshal(bb, &in); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.K9s = in.K9s
|
v.Views = in.Views
|
||||||
v.fireConfigChanged()
|
v.fireConfigChanged()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -84,7 +78,7 @@ func (v *CustomView) RemoveListener(gvr string) {
|
||||||
|
|
||||||
func (v *CustomView) fireConfigChanged() {
|
func (v *CustomView) fireConfigChanged() {
|
||||||
for gvr, list := range v.listeners {
|
for gvr, list := range v.listeners {
|
||||||
if v, ok := v.K9s.Views[gvr]; ok {
|
if v, ok := v.Views[gvr]; ok {
|
||||||
list.ViewSettingsChanged(v)
|
list.ViewSettingsChanged(v)
|
||||||
} else {
|
} else {
|
||||||
list.ViewSettingsChanged(ViewSetting{})
|
list.ViewSettingsChanged(ViewSetting{})
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import (
|
||||||
func TestViewSettingsLoad(t *testing.T) {
|
func TestViewSettingsLoad(t *testing.T) {
|
||||||
cfg := config.NewCustomView()
|
cfg := config.NewCustomView()
|
||||||
|
|
||||||
assert.Nil(t, cfg.Load("testdata/view_settings.yaml"))
|
assert.Nil(t, cfg.Load("testdata/views/views.yaml"))
|
||||||
assert.Equal(t, 1, len(cfg.K9s.Views))
|
assert.Equal(t, 1, len(cfg.Views))
|
||||||
assert.Equal(t, 4, len(cfg.K9s.Views["v1/pods"].Columns))
|
assert.Equal(t, 4, len(cfg.Views["v1/pods"].Columns))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,9 @@ func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error)
|
||||||
flags.Sections = §ions
|
flags.Sections = §ions
|
||||||
flags.ActiveNamespace = &ns
|
flags.ActiveNamespace = &ns
|
||||||
}
|
}
|
||||||
spinach := cfg.YamlExtension(filepath.Join(cfg.K9sHome(), "spinach.yaml"))
|
spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml")
|
||||||
if c, err := p.GetFactory().Client().Config().CurrentContextName(); err == nil {
|
if c, err := p.GetFactory().Client().Config().CurrentContextName(); err == nil {
|
||||||
spinach = cfg.YamlExtension(filepath.Join(cfg.K9sHome(), fmt.Sprintf("%s_spinach.yaml", c)))
|
spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c))
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(spinach); err == nil {
|
if _, err := os.Stat(spinach); err == nil {
|
||||||
flags.Spinach = &spinach
|
flags.Spinach = &spinach
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,11 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/dao"
|
"github.com/derailed/k9s/internal/dao"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -72,24 +75,25 @@ func (c ClusterMeta) Deltas(n ClusterMeta) bool {
|
||||||
|
|
||||||
// ClusterInfo models cluster metadata.
|
// ClusterInfo models cluster metadata.
|
||||||
type ClusterInfo struct {
|
type ClusterInfo struct {
|
||||||
cluster *Cluster
|
cluster *Cluster
|
||||||
factory dao.Factory
|
factory dao.Factory
|
||||||
data ClusterMeta
|
data ClusterMeta
|
||||||
version string
|
version string
|
||||||
skipLatestRevCheck bool
|
cfg *config.K9s
|
||||||
listeners []ClusterInfoListener
|
listeners []ClusterInfoListener
|
||||||
cache *cache.LRUExpireCache
|
cache *cache.LRUExpireCache
|
||||||
|
mx sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClusterInfo returns a new instance.
|
// NewClusterInfo returns a new instance.
|
||||||
func NewClusterInfo(f dao.Factory, v string, skipLatestRevCheck bool) *ClusterInfo {
|
func NewClusterInfo(f dao.Factory, v string, cfg *config.K9s) *ClusterInfo {
|
||||||
c := ClusterInfo{
|
c := ClusterInfo{
|
||||||
factory: f,
|
factory: f,
|
||||||
cluster: NewCluster(f),
|
cluster: NewCluster(f),
|
||||||
data: NewClusterMeta(),
|
data: NewClusterMeta(),
|
||||||
version: v,
|
version: v,
|
||||||
skipLatestRevCheck: skipLatestRevCheck,
|
cfg: cfg,
|
||||||
cache: cache.NewLRUExpireCache(cacheSize),
|
cache: cache.NewLRUExpireCache(cacheSize),
|
||||||
}
|
}
|
||||||
|
|
||||||
return &c
|
return &c
|
||||||
|
|
@ -113,7 +117,16 @@ func (c *ClusterInfo) fetchK9sLatestRev() string {
|
||||||
|
|
||||||
// Reset resets context and reload.
|
// Reset resets context and reload.
|
||||||
func (c *ClusterInfo) Reset(f dao.Factory) {
|
func (c *ClusterInfo) Reset(f dao.Factory) {
|
||||||
c.cluster, c.data = NewCluster(f), NewClusterMeta()
|
if f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mx.Lock()
|
||||||
|
{
|
||||||
|
c.cluster, c.data = NewCluster(f), NewClusterMeta()
|
||||||
|
}
|
||||||
|
c.mx.Unlock()
|
||||||
|
|
||||||
c.Refresh()
|
c.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +151,7 @@ func (c *ClusterInfo) Refresh() {
|
||||||
v1 := NewSemVer(data.K9sVer)
|
v1 := NewSemVer(data.K9sVer)
|
||||||
|
|
||||||
var latestRev string
|
var latestRev string
|
||||||
if !c.skipLatestRevCheck {
|
if !c.cfg.SkipLatestRevCheck {
|
||||||
latestRev = c.fetchK9sLatestRev()
|
latestRev = c.fetchK9sLatestRev()
|
||||||
}
|
}
|
||||||
v2 := NewSemVer(latestRev)
|
v2 := NewSemVer(latestRev)
|
||||||
|
|
@ -153,7 +166,11 @@ func (c *ClusterInfo) Refresh() {
|
||||||
} else {
|
} else {
|
||||||
c.fireNoMetaChanged(data)
|
c.fireNoMetaChanged(data)
|
||||||
}
|
}
|
||||||
c.data = data
|
c.mx.Lock()
|
||||||
|
{
|
||||||
|
c.data = data
|
||||||
|
}
|
||||||
|
c.mx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddListener adds a new model listener.
|
// AddListener adds a new model listener.
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ func (l *Log) SetSinceSeconds(ctx context.Context, i int64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure sets logger configuration.
|
// Configure sets logger configuration.
|
||||||
func (l *Log) Configure(opts *config.Logger) {
|
func (l *Log) Configure(opts config.Logger) {
|
||||||
l.logOptions.Lines = int64(opts.TailCount)
|
l.logOptions.Lines = int64(opts.TailCount)
|
||||||
l.logOptions.SinceSeconds = opts.SinceSeconds
|
l.logOptions.SinceSeconds = opts.SinceSeconds
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,19 @@ type (
|
||||||
// ActionHandler handles a keyboard command.
|
// ActionHandler handles a keyboard command.
|
||||||
ActionHandler func(*tcell.EventKey) *tcell.EventKey
|
ActionHandler func(*tcell.EventKey) *tcell.EventKey
|
||||||
|
|
||||||
|
ActionOpts struct {
|
||||||
|
Visible bool
|
||||||
|
Shared bool
|
||||||
|
Plugin bool
|
||||||
|
HotKey bool
|
||||||
|
Dangerous bool
|
||||||
|
}
|
||||||
|
|
||||||
// KeyAction represents a keyboard action.
|
// KeyAction represents a keyboard action.
|
||||||
KeyAction struct {
|
KeyAction struct {
|
||||||
Description string
|
Description string
|
||||||
Action ActionHandler
|
Action ActionHandler
|
||||||
Visible bool
|
Opts ActionOpts
|
||||||
Shared bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyActions tracks mappings between keystrokes and actions.
|
// KeyActions tracks mappings between keystrokes and actions.
|
||||||
|
|
@ -28,13 +35,32 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewKeyAction returns a new keyboard action.
|
// NewKeyAction returns a new keyboard action.
|
||||||
func NewKeyAction(d string, a ActionHandler, display bool) KeyAction {
|
func NewKeyAction(d string, a ActionHandler, visible bool) KeyAction {
|
||||||
return KeyAction{Description: d, Action: a, Visible: display}
|
return NewKeyActionWithOpts(d, a, ActionOpts{
|
||||||
|
Visible: visible,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSharedKeyAction returns a new shared keyboard action.
|
// NewSharedKeyAction returns a new shared keyboard action.
|
||||||
func NewSharedKeyAction(d string, a ActionHandler, display bool) KeyAction {
|
func NewSharedKeyAction(d string, a ActionHandler, visible bool) KeyAction {
|
||||||
return KeyAction{Description: d, Action: a, Visible: display, Shared: true}
|
return NewKeyActionWithOpts(d, a, ActionOpts{
|
||||||
|
Visible: visible,
|
||||||
|
Shared: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyActionWithOpts returns a new keyboard action.
|
||||||
|
func NewKeyActionWithOpts(d string, a ActionHandler, opts ActionOpts) KeyAction {
|
||||||
|
return KeyAction{
|
||||||
|
Description: d,
|
||||||
|
Action: a,
|
||||||
|
Opts: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a KeyActions) Reset(aa KeyActions) {
|
||||||
|
a.Clear()
|
||||||
|
a.Add(aa)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add sets up keyboard action listener.
|
// Add sets up keyboard action listener.
|
||||||
|
|
@ -51,6 +77,15 @@ func (a KeyActions) Clear() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearDanger remove all dangerous actions.
|
||||||
|
func (a KeyActions) ClearDanger() {
|
||||||
|
for k, v := range a {
|
||||||
|
if v.Opts.Dangerous {
|
||||||
|
delete(a, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set replace actions with new ones.
|
// Set replace actions with new ones.
|
||||||
func (a KeyActions) Set(aa KeyActions) {
|
func (a KeyActions) Set(aa KeyActions) {
|
||||||
for k, v := range aa {
|
for k, v := range aa {
|
||||||
|
|
@ -69,7 +104,7 @@ func (a KeyActions) Delete(kk ...tcell.Key) {
|
||||||
func (a KeyActions) Hints() model.MenuHints {
|
func (a KeyActions) Hints() model.MenuHints {
|
||||||
kk := make([]int, 0, len(a))
|
kk := make([]int, 0, len(a))
|
||||||
for k := range a {
|
for k := range a {
|
||||||
if !a[k].Shared {
|
if !a[k].Opts.Shared {
|
||||||
kk = append(kk, int(k))
|
kk = append(kk, int(k))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +117,7 @@ func (a KeyActions) Hints() model.MenuHints {
|
||||||
model.MenuHint{
|
model.MenuHint{
|
||||||
Mnemonic: name,
|
Mnemonic: name,
|
||||||
Description: a[tcell.Key(k)].Description,
|
Description: a[tcell.Key(k)].Description,
|
||||||
Visible: a[tcell.Key(k)].Visible,
|
Visible: a[tcell.Key(k)].Opts.Visible,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,11 @@ func NewApp(cfg *config.Config, context string) *App {
|
||||||
a := App{
|
a := App{
|
||||||
Application: tview.NewApplication(),
|
Application: tview.NewApplication(),
|
||||||
actions: make(KeyActions),
|
actions: make(KeyActions),
|
||||||
Configurator: Configurator{Config: cfg},
|
Configurator: Configurator{Config: cfg, Styles: config.NewStyles()},
|
||||||
Main: NewPages(),
|
Main: NewPages(),
|
||||||
flash: model.NewFlash(model.DefaultFlashDelay),
|
flash: model.NewFlash(model.DefaultFlashDelay),
|
||||||
cmdBuff: model.NewFishBuff(':', model.CommandBuffer),
|
cmdBuff: model.NewFishBuff(':', model.CommandBuffer),
|
||||||
}
|
}
|
||||||
a.ReloadStyles()
|
|
||||||
|
|
||||||
a.views = map[string]tview.Primitive{
|
a.views = map[string]tview.Primitive{
|
||||||
"menu": NewMenu(a.Styles),
|
"menu": NewMenu(a.Styles),
|
||||||
|
|
@ -134,11 +133,6 @@ func (a *App) StylesChanged(s *config.Styles) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadStyles reloads skin file.
|
|
||||||
func (a *App) ReloadStyles() {
|
|
||||||
a.RefreshStyles()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conn returns an api server connection.
|
// Conn returns an api server connection.
|
||||||
func (a *App) Conn() client.Connection {
|
func (a *App) Conn() client.Connection {
|
||||||
return a.Config.GetConnection()
|
return a.Config.GetConnection()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/model"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
|
@ -17,6 +19,8 @@ import (
|
||||||
|
|
||||||
// Synchronizer manages ui event queue.
|
// Synchronizer manages ui event queue.
|
||||||
type synchronizer interface {
|
type synchronizer interface {
|
||||||
|
Flash() *model.Flash
|
||||||
|
UpdateClusterInfo()
|
||||||
QueueUpdateDraw(func())
|
QueueUpdateDraw(func())
|
||||||
QueueUpdate(func())
|
QueueUpdate(func())
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +50,7 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case evt := <-w.Events:
|
case evt := <-w.Events:
|
||||||
if evt.Name == config.AppViewsFile {
|
if evt.Name == config.AppViewsFile && evt.Op != fsnotify.Chmod {
|
||||||
s.QueueUpdateDraw(func() {
|
s.QueueUpdateDraw(func() {
|
||||||
if err := c.RefreshCustomViews(); err != nil {
|
if err := c.RefreshCustomViews(); err != nil {
|
||||||
log.Warn().Err(err).Msgf("Custom views refresh failed")
|
log.Warn().Err(err).Msgf("Custom views refresh failed")
|
||||||
|
|
@ -66,10 +70,11 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := c.RefreshCustomViews(); err != nil {
|
if err := w.Add(config.AppViewsFile); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return w.Add(config.AppViewsFile)
|
|
||||||
|
return c.RefreshCustomViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshCustomViews load view configuration changes.
|
// RefreshCustomViews load view configuration changes.
|
||||||
|
|
@ -85,15 +90,13 @@ func (c *Configurator) RefreshCustomViews() error {
|
||||||
|
|
||||||
// SkinsDirWatcher watches for skin directory file changes.
|
// SkinsDirWatcher watches for skin directory file changes.
|
||||||
func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error {
|
func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error {
|
||||||
if !c.HasSkin() {
|
if _, err := os.Stat(config.AppSkinsDir); os.IsNotExist(err) {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w, err := fsnotify.NewWatcher()
|
w, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
@ -101,7 +104,7 @@ func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) erro
|
||||||
if evt.Name == c.skinFile && evt.Op != fsnotify.Chmod {
|
if evt.Name == c.skinFile && evt.Op != fsnotify.Chmod {
|
||||||
log.Debug().Msgf("Skin changed: %s", c.skinFile)
|
log.Debug().Msgf("Skin changed: %s", c.skinFile)
|
||||||
s.QueueUpdateDraw(func() {
|
s.QueueUpdateDraw(func() {
|
||||||
c.RefreshStyles()
|
c.RefreshStyles(s)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case err := <-w.Errors:
|
case err := <-w.Errors:
|
||||||
|
|
@ -133,18 +136,20 @@ func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error
|
||||||
select {
|
select {
|
||||||
case evt := <-w.Events:
|
case evt := <-w.Events:
|
||||||
if evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) {
|
if evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) {
|
||||||
log.Debug().Msgf("ConfigWatcher file changed: %s -- %#v", evt.Name, evt.Op.String())
|
log.Debug().Msgf("ConfigWatcher file changed: %s", evt.Name)
|
||||||
if evt.Name == config.AppConfigFile {
|
if evt.Name == config.AppConfigFile {
|
||||||
if err := c.Config.Load(evt.Name); err != nil {
|
if err := c.Config.Load(evt.Name); err != nil {
|
||||||
log.Error().Err(err).Msgf("Config reload failed")
|
log.Error().Err(err).Msgf("k9s config reload failed")
|
||||||
|
s.Flash().Warn("k9s config reload failed. Check k9s logs!")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := c.Config.K9s.Reload(); err != nil {
|
if err := c.Config.K9s.Reload(); err != nil {
|
||||||
log.Error().Err(err).Msgf("Context config reload failed")
|
log.Error().Err(err).Msgf("k9s context config reload failed")
|
||||||
|
s.Flash().Warn("Context config reload failed. Check k9s logs!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.QueueUpdateDraw(func() {
|
s.QueueUpdateDraw(func() {
|
||||||
c.RefreshStyles()
|
c.RefreshStyles(s)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case err := <-w.Errors:
|
case err := <-w.Errors:
|
||||||
|
|
@ -181,11 +186,18 @@ func (c *Configurator) activeSkin() (string, bool) {
|
||||||
return skin, false
|
return skin, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if ct, err := c.Config.K9s.ActiveContext(); err == nil {
|
if ct, err := c.Config.K9s.ActiveContext(); err == nil && ct.Skin != "" {
|
||||||
skin = ct.Skin
|
if _, err := os.Stat(config.SkinFileFromName(ct.Skin)); !os.IsNotExist(err) {
|
||||||
|
skin = ct.Skin
|
||||||
|
log.Debug().Msgf("[Skin] Loading context skin (%q) from %q", skin, c.Config.K9s.ActiveContextName())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if skin == "" {
|
|
||||||
skin = c.Config.K9s.UI.Skin
|
if sk := c.Config.K9s.UI.Skin; skin == "" && sk != "" {
|
||||||
|
if _, err := os.Stat(config.SkinFileFromName(sk)); !os.IsNotExist(err) {
|
||||||
|
skin = sk
|
||||||
|
log.Debug().Msgf("[Skin] Loading global skin (%q)", skin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return skin, skin != ""
|
return skin, skin != ""
|
||||||
|
|
@ -208,46 +220,53 @@ func (c *Configurator) activeConfig() (cluster string, context string, ok bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RefreshStyles load for skin configuration changes.
|
// RefreshStyles load for skin configuration changes.
|
||||||
func (c *Configurator) RefreshStyles() {
|
func (c *Configurator) RefreshStyles(s synchronizer) {
|
||||||
|
s.UpdateClusterInfo()
|
||||||
if c.Styles == nil {
|
if c.Styles == nil {
|
||||||
c.Styles = config.NewStyles()
|
c.Styles = config.NewStyles()
|
||||||
}
|
}
|
||||||
|
defer c.loadSkinFile(s)
|
||||||
|
|
||||||
cl, ct, ok := c.activeConfig()
|
cl, ct, ok := c.activeConfig()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Msgf("No custom skin found. Using stock skin")
|
|
||||||
c.updateStyles("")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// !!BOZO!! Lame move out!
|
||||||
if bc, err := config.EnsureBenchmarksCfgFile(cl, ct); err != nil {
|
if bc, err := config.EnsureBenchmarksCfgFile(cl, ct); err != nil {
|
||||||
log.Warn().Err(err).Msgf("No benchmark config file found: %q@%q", cl, ct)
|
log.Warn().Err(err).Msgf("No benchmark config file found: %q@%q", cl, ct)
|
||||||
} else {
|
} else {
|
||||||
c.BenchFile = bc
|
c.BenchFile = bc
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Configurator) loadSkinFile(s synchronizer) {
|
||||||
skin, ok := c.activeSkin()
|
skin, ok := c.activeSkin()
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Msgf("No custom skin found. Using stock skin")
|
log.Debug().Msgf("No custom skin found. Using stock skin")
|
||||||
c.updateStyles("")
|
c.updateStyles("")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
skinFile := config.SkinFileFromName(skin)
|
skinFile := config.SkinFileFromName(skin)
|
||||||
|
log.Debug().Msgf("Loading skin file: %q", skinFile)
|
||||||
if err := c.Styles.Load(skinFile); err != nil {
|
if err := c.Styles.Load(skinFile); err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
log.Warn().Msgf("Skin file %q not found in skins dir: %s", skinFile, config.AppSkinsDir)
|
s.Flash().Warnf("Skin file %q not found in skins dir: %s", filepath.Base(skinFile), config.AppSkinsDir)
|
||||||
} else {
|
} else {
|
||||||
log.Error().Msgf("Failed to parse skin file -- %s: %s.", skinFile, err)
|
s.Flash().Errf("Failed to parse skin file -- %s: %s.", filepath.Base(skinFile), err)
|
||||||
}
|
}
|
||||||
c.updateStyles("")
|
c.updateStyles("")
|
||||||
} else {
|
} else {
|
||||||
log.Debug().Msgf("Loading skin file: %q", skinFile)
|
s.Flash().Infof("Skin file loaded: %q", skinFile)
|
||||||
c.updateStyles(skinFile)
|
c.updateStyles(skinFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Configurator) updateStyles(f string) {
|
func (c *Configurator) updateStyles(f string) {
|
||||||
c.skinFile = f
|
c.skinFile = f
|
||||||
|
if f == "" {
|
||||||
|
c.Styles.Reset()
|
||||||
|
}
|
||||||
c.Styles.Update()
|
c.Styles.Update()
|
||||||
|
|
||||||
render.ModColor = c.Styles.Frame().Status.ModifyColor.Color()
|
render.ModColor = c.Styles.Frame().Status.ModifyColor.Color()
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue