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 '....'
|
||||
4. See error
|
||||
|
||||
**Historical Documents**
|
||||
When applicable please include any supporting artifacts: k9s debug logs, configurations, resource manifests, ...
|
||||
|
||||
**Expected behavior**
|
||||
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
|
||||
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
||||
endif
|
||||
VERSION ?= v0.30.8
|
||||
VERSION ?= v0.31.0
|
||||
IMG_NAME := derailed/k9s
|
||||
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
|
||||
```
|
||||
|
||||
* 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
|
||||
# Kubectl edit command will use this env var.
|
||||
export EDITOR=my_fav_editor
|
||||
# Should your editor deal with streamed vs on disk files differently, also set...
|
||||
export K9S_EDITOR=my_fav_editor
|
||||
export KUBE_EDITOR=my_fav_editor
|
||||
```
|
||||
|
||||
* 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
|
||||
# $XDG_CONFIG_HOME/k9s/views.yaml
|
||||
k9s:
|
||||
views:
|
||||
v1/pods:
|
||||
columns:
|
||||
- AGE
|
||||
- NAMESPACE
|
||||
- NAME
|
||||
- IP
|
||||
- NODE
|
||||
- STATUS
|
||||
- READY
|
||||
v1/services:
|
||||
columns:
|
||||
- AGE
|
||||
- NAMESPACE
|
||||
- NAME
|
||||
- TYPE
|
||||
- CLUSTER-IP
|
||||
views:
|
||||
v1/pods:
|
||||
columns:
|
||||
- AGE
|
||||
- NAMESPACE
|
||||
- NAME
|
||||
- IP
|
||||
- NODE
|
||||
- STATUS
|
||||
- READY
|
||||
v1/services:
|
||||
columns:
|
||||
- AGE
|
||||
- NAMESPACE
|
||||
- NAME
|
||||
- TYPE
|
||||
- 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:
|
||||
|
||||
```yaml
|
||||
# $XDG_CONFIG_HOME/k9s/config.yaml
|
||||
k9s:
|
||||
liveViewAutoRefresh: false
|
||||
screenDumpDir: /tmp/dumps
|
||||
|
|
@ -910,6 +908,8 @@ k9s:
|
|||
logoless: false
|
||||
crumbsless: 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.
|
||||
skin: dracula # => assumes the file skins/dracula.yaml is present in the $XDG_DATA_HOME/k9s/skins directory
|
||||
skipLatestRevCheck: false
|
||||
|
|
@ -929,7 +929,7 @@ k9s:
|
|||
tail: 100
|
||||
buffer: 5000
|
||||
sinceSeconds: -1
|
||||
fullScreenLogs: false
|
||||
fullScreen: false
|
||||
textWrap: false
|
||||
showTime: false
|
||||
thresholds:
|
||||
|
|
@ -942,7 +942,7 @@ k9s:
|
|||
```
|
||||
|
||||
```yaml
|
||||
# $XDG_DATA_HOME/k9s/skins/in_the_navy.yaml
|
||||
# $XDG_DATA_HOME/k9s/skins/in_the_navy.yaml
|
||||
# Skin InTheNavy!
|
||||
k9s:
|
||||
# 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)
|
||||
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()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("load configuration failed")
|
||||
return err
|
||||
}
|
||||
app := view.NewApp(cfg)
|
||||
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
|
||||
|
|
@ -115,11 +115,12 @@ func loadConfiguration() (*config.Config, error) {
|
|||
k8sCfg := client.NewConfig(k8sFlags)
|
||||
k9sCfg := config.NewConfig(k8sCfg)
|
||||
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)
|
||||
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)
|
||||
k9sCfg.SetConnection(conn)
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -25,8 +25,10 @@ require (
|
|||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
helm.sh/helm/v3 v3.13.3
|
||||
k8s.io/api 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/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // 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/xlab/treeprint v1.2.0 // 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/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/gorm v1.25.5 // indirect
|
||||
k8s.io/apiserver v0.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)
|
||||
}()
|
||||
|
||||
dial, err := a.CachedDiscovery()
|
||||
dial, err := a.Dial()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msgf("Unable to dial discovery API")
|
||||
return err
|
||||
}
|
||||
apiGroups, err := dial.ServerGroups()
|
||||
apiGroups, err := dial.Discovery().ServerGroups()
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
|
|
@ -193,7 +193,7 @@ func (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1bet
|
|||
|
||||
mx, ok := mmx[n]
|
||||
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
|
||||
}
|
||||
|
|
@ -283,7 +283,7 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be
|
|||
}
|
||||
pmx, ok := mmx[fqn]
|
||||
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
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ type Connection interface {
|
|||
// HasMetrics checks if metrics server is available.
|
||||
HasMetrics() bool
|
||||
|
||||
// ValidNamespaces returns all available namespace names.
|
||||
// ValidNamespaceNames returns all available namespace names.
|
||||
ValidNamespaceNames() (NamespaceNames, error)
|
||||
|
||||
// IsValidNamespace checks if given namespace is known.
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/derailed/k9s/internal/config/data"
|
||||
"github.com/derailed/k9s/internal/config/json"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
|
@ -120,18 +122,26 @@ func (a *Aliases) LoadFile(path string) error {
|
|||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
f, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
var aa Aliases
|
||||
if err := yaml.Unmarshal(f, &aa); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.mx.Lock()
|
||||
defer a.mx.Unlock()
|
||||
for k, v := range aa.Alias {
|
||||
a.Alias[k] = v
|
||||
}
|
||||
bb, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,25 +4,59 @@
|
|||
package config_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/config/data"
|
||||
"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) {
|
||||
type aliasDef struct {
|
||||
cmd string
|
||||
aliases []string
|
||||
}
|
||||
|
||||
uu := []struct {
|
||||
name string
|
||||
uu := map[string]struct {
|
||||
aliases []aliasDef
|
||||
registeredCommands map[string]string
|
||||
}{
|
||||
{
|
||||
name: "simple aliases",
|
||||
"simple": {
|
||||
aliases: []aliasDef{
|
||||
{
|
||||
cmd: "one",
|
||||
|
|
@ -34,8 +68,7 @@ func TestAliasDefine(t *testing.T) {
|
|||
"duh": "one",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicated aliases",
|
||||
"duplicates": {
|
||||
aliases: []aliasDef{
|
||||
{
|
||||
cmd: "one",
|
||||
|
|
@ -54,9 +87,9 @@ func TestAliasDefine(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
for i := range uu {
|
||||
u := uu[i]
|
||||
t.Run(u.name, func(t *testing.T) {
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
configAlias := config.NewAliases()
|
||||
for _, aliases := range u.aliases {
|
||||
for _, a := range aliases.aliases {
|
||||
|
|
@ -73,18 +106,35 @@ func TestAliasDefine(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAliasesLoad(t *testing.T) {
|
||||
config.AppConfigDir = "testdata/aliases"
|
||||
a := config.NewAliases()
|
||||
|
||||
assert.Nil(t, a.LoadFile("testdata/alias.yaml"))
|
||||
assert.Equal(t, 2, len(a.Alias))
|
||||
assert.Nil(t, a.Load("testdata/aliases/plain.yaml"))
|
||||
assert.Equal(t, 56, len(a.Alias))
|
||||
}
|
||||
|
||||
func TestAliasesSave(t *testing.T) {
|
||||
a := config.NewAliases()
|
||||
a.Alias["test"] = "fred"
|
||||
a.Alias["blee"] = "duh"
|
||||
assert.NoError(t, data.EnsureFullPath("/tmp/test-aliases", data.DefaultDirMod))
|
||||
defer assert.NoError(t, os.RemoveAll("/tmp/test-aliases"))
|
||||
|
||||
assert.Nil(t, a.SaveAliases("/tmp/a.yaml"))
|
||||
assert.Nil(t, a.LoadFile("/tmp/a.yaml"))
|
||||
assert.Equal(t, 2, len(a.Alias))
|
||||
config.AppAliasesFile = "/tmp/test-aliases/aliases.yaml"
|
||||
a := testAliases()
|
||||
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
|
||||
}{
|
||||
"goodConfig": {
|
||||
"testdata/b_good.yaml",
|
||||
"testdata/benchmarks/b_good.yaml",
|
||||
2,
|
||||
1000,
|
||||
2,
|
||||
0,
|
||||
},
|
||||
"malformed": {
|
||||
"testdata/b_toast.yaml",
|
||||
"testdata/benchmarks/b_toast.yaml",
|
||||
1,
|
||||
200,
|
||||
0,
|
||||
|
|
@ -103,7 +103,7 @@ func TestBenchServiceLoad(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
b, err := NewBench("testdata/b_good.yaml")
|
||||
b, err := NewBench("testdata/benchmarks/b_good.yaml")
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(b.Benchmarks.Services))
|
||||
|
|
@ -122,11 +122,11 @@ func TestBenchServiceLoad(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.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)
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ func TestBenchContainerLoad(t *testing.T) {
|
|||
for k := range uu {
|
||||
u := uu[k]
|
||||
t.Run(k, func(t *testing.T) {
|
||||
b, err := NewBench("testdata/b_containers.yaml")
|
||||
b, err := NewBench("testdata/benchmarks/b_containers.yaml")
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(b.Benchmarks.Services))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,22 @@ const (
|
|||
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.
|
||||
func NewColor(c string) Color {
|
||||
return Color(c)
|
||||
|
|
@ -50,12 +66,3 @@ func (c Color) Color() tcell.Color {
|
|||
|
||||
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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config/data"
|
||||
"github.com/derailed/k9s/internal/config/json"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
|
|
@ -19,25 +17,11 @@ import (
|
|||
|
||||
// Config tracks K9s configuration options.
|
||||
type Config struct {
|
||||
K9s *K9s `yaml:"k9s"`
|
||||
K9s *K9s `yaml:"k9s" json:"k9s"`
|
||||
conn client.Connection
|
||||
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.
|
||||
func NewConfig(ks data.KubeSettings) *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.
|
||||
func (c *Config) ContextAliasesPath() string {
|
||||
ct, err := c.K9s.ActiveContext()
|
||||
|
|
@ -53,13 +47,14 @@ func (c *Config) ContextAliasesPath() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
return AppContextAliasesFile(ct.ClusterName, c.K9s.activeContextName)
|
||||
return AppContextAliasesFile(ct.GetClusterName(), c.K9s.activeContextName)
|
||||
}
|
||||
|
||||
// ContextPluginsPath returns a context specific plugins file spec.
|
||||
func (c *Config) ContextPluginsPath() string {
|
||||
ct, err := c.K9s.ActiveContext()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("active context load failed")
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +63,10 @@ func (c *Config) ContextPluginsPath() string {
|
|||
|
||||
// Refine the configuration based on cli args.
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
@ -88,7 +86,7 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
|
|||
switch {
|
||||
case k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces):
|
||||
ns = client.NamespaceAll
|
||||
case isSet(flags.Namespace):
|
||||
case isStringSet(flags.Namespace):
|
||||
ns = *flags.Namespace
|
||||
default:
|
||||
nss, err := c.K9s.ActiveContextNamespace()
|
||||
|
|
@ -104,7 +102,7 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
|
|||
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.
|
||||
|
|
@ -115,7 +113,7 @@ func (c *Config) Reset() {
|
|||
func (c *Config) SetCurrentContext(n string) (*data.Context, error) {
|
||||
ct, err := c.K9s.ActivateContext(n)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("set current context %q failed: %w", n, err)
|
||||
return nil, fmt.Errorf("set current context failed. %w", err)
|
||||
}
|
||||
|
||||
return ct, nil
|
||||
|
|
@ -138,21 +136,13 @@ func (c *Config) ActiveNamespace() string {
|
|||
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.
|
||||
func (c *Config) FavNamespaces() []string {
|
||||
ct, err := c.K9s.ActiveContext()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
ct.Validate(c.conn, c.settings)
|
||||
|
||||
return ct.Namespace.Favorites
|
||||
}
|
||||
|
|
@ -209,23 +199,26 @@ func (c *Config) ActiveContextName() string {
|
|||
return c.K9s.activeContextName
|
||||
}
|
||||
|
||||
func (c *Config) Merge(c1 *Config) {
|
||||
c.K9s.Merge(c1.K9s)
|
||||
}
|
||||
|
||||
// Load loads K9s configuration from file.
|
||||
func (c *Config) Load(path string) error {
|
||||
f, err := os.ReadFile(path)
|
||||
bb, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
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
|
||||
if err := yaml.Unmarshal(f, &cfg); err != nil {
|
||||
if err := yaml.Unmarshal(bb, &cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.K9s != nil {
|
||||
c.K9s.Refine(cfg.K9s)
|
||||
}
|
||||
if c.K9s.Logger == nil {
|
||||
c.K9s.Logger = NewLogger()
|
||||
}
|
||||
c.Merge(&cfg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +228,11 @@ func (c *Config) Save() error {
|
|||
if err := c.K9s.Save(); err != nil {
|
||||
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.
|
||||
|
|
@ -248,48 +245,26 @@ func (c *Config) SaveFile(path string) error {
|
|||
log.Error().Msgf("[Config] Unable to save K9s config file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, cfg, 0644)
|
||||
}
|
||||
|
||||
// Validate the configuration.
|
||||
func (c *Config) Validate() {
|
||||
if c.K9s == nil {
|
||||
c.K9s = NewK9s(c.conn, c.settings)
|
||||
}
|
||||
|
||||
c.K9s.Validate(c.conn, c.settings)
|
||||
}
|
||||
|
||||
// Dump debug...
|
||||
// Dump for debug...
|
||||
func (c *Config) Dump(msg string) {
|
||||
ct, err := c.K9s.ActiveContext()
|
||||
if err != nil {
|
||||
log.Debug().Msgf("Current Contexts: %s\n", ct.ClusterName)
|
||||
if err == nil {
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"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"
|
||||
m "github.com/petergtz/pegomock"
|
||||
"github.com/rs/zerolog"
|
||||
|
|
@ -21,37 +25,358 @@ func init() {
|
|||
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 (
|
||||
cfgFile = "testdata/kubeconfig-test.yaml"
|
||||
ctx, cluster, ns = "ct-1-1", "cl-1", "ns-1"
|
||||
cfgFile = "testdata/kubes/test.yaml"
|
||||
view = "dp"
|
||||
)
|
||||
|
||||
uu := map[string]struct {
|
||||
flags *genericclioptions.ConfigFlags
|
||||
issue bool
|
||||
context, cluster, namespace string
|
||||
ct string
|
||||
flags *genericclioptions.ConfigFlags
|
||||
k9sFlags *config.Flags
|
||||
view string
|
||||
e string
|
||||
}{
|
||||
"overrideNS": {
|
||||
flags: &genericclioptions.ConfigFlags{
|
||||
KubeConfig: &cfgFile,
|
||||
Context: &ctx,
|
||||
ClusterName: &cluster,
|
||||
Namespace: &ns,
|
||||
},
|
||||
issue: false,
|
||||
context: ctx,
|
||||
cluster: cluster,
|
||||
namespace: ns,
|
||||
"empty": {
|
||||
view: data.DefaultView,
|
||||
e: data.DefaultView,
|
||||
},
|
||||
"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{
|
||||
KubeConfig: &cfgFile,
|
||||
Context: &ns,
|
||||
ClusterName: &cluster,
|
||||
Namespace: &ns,
|
||||
KubeConfig: &cfgFile,
|
||||
},
|
||||
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()
|
||||
|
||||
err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))
|
||||
if u.issue {
|
||||
assert.NotNil(t, err)
|
||||
assert.NoError(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 {
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, u.context, cfg.K9s.ActiveContextName())
|
||||
|
|
@ -76,35 +521,29 @@ func TestConfigValidate(t *testing.T) {
|
|||
cfg := mock.NewMockConfig()
|
||||
cfg.SetConnection(mock.NewMockConnection())
|
||||
|
||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
||||
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||
cfg.Validate()
|
||||
}
|
||||
|
||||
func TestConfigLoad(t *testing.T) {
|
||||
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, 2000, cfg.K9s.Logger.BufferSize)
|
||||
assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount)
|
||||
}
|
||||
|
||||
func TestConfigLoadOldCfg(t *testing.T) {
|
||||
cfg := mock.NewMockConfig()
|
||||
|
||||
assert.Nil(t, cfg.Load("testdata/k9s_old.yaml"))
|
||||
assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)
|
||||
}
|
||||
|
||||
func TestConfigLoadCrap(t *testing.T) {
|
||||
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) {
|
||||
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.ReadOnly = true
|
||||
|
|
@ -113,26 +552,28 @@ func TestConfigSaveFile(t *testing.T) {
|
|||
cfg.Validate()
|
||||
|
||||
path := filepath.Join("/tmp", "k9s.yaml")
|
||||
err := cfg.SaveFile(path)
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, cfg.SaveFile(path))
|
||||
raw, err := os.ReadFile(path)
|
||||
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) {
|
||||
cfg := mock.NewMockConfig()
|
||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
||||
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||
cfg.Reset()
|
||||
cfg.Validate()
|
||||
|
||||
path := filepath.Join("/tmp", "k9s.yaml")
|
||||
err := cfg.SaveFile(path)
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, cfg.SaveFile(path))
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
bb, err := os.ReadFile(path)
|
||||
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...
|
||||
|
|
@ -147,86 +588,86 @@ func TestSetup(t *testing.T) {
|
|||
// ----------------------------------------------------------------------------
|
||||
// Test Data...
|
||||
|
||||
var expectedConfig = `k9s:
|
||||
liveViewAutoRefresh: true
|
||||
screenDumpDir: /tmp
|
||||
refreshRate: 100
|
||||
maxConnRetry: 5
|
||||
readOnly: true
|
||||
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: 500
|
||||
buffer: 800
|
||||
sinceSeconds: -1
|
||||
fullScreenLogs: false
|
||||
textWrap: false
|
||||
showTime: false
|
||||
thresholds:
|
||||
cpu:
|
||||
critical: 90
|
||||
warn: 70
|
||||
memory:
|
||||
critical: 90
|
||||
warn: 70
|
||||
`
|
||||
// var expectedConfig = `k9s:
|
||||
// liveViewAutoRefresh: true
|
||||
// screenDumpDir: /tmp/screen-dumps
|
||||
// refreshRate: 100
|
||||
// maxConnRetry: 5
|
||||
// readOnly: true
|
||||
// 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: 500
|
||||
// buffer: 800
|
||||
// sinceSeconds: -1
|
||||
// fullScreen: false
|
||||
// textWrap: false
|
||||
// showTime: false
|
||||
// thresholds:
|
||||
// cpu:
|
||||
// critical: 90
|
||||
// warn: 70
|
||||
// memory:
|
||||
// critical: 90
|
||||
// warn: 70
|
||||
// `
|
||||
|
||||
var resetConfig = `k9s:
|
||||
liveViewAutoRefresh: true
|
||||
screenDumpDir: /tmp
|
||||
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: 200
|
||||
buffer: 2000
|
||||
sinceSeconds: -1
|
||||
fullScreenLogs: false
|
||||
textWrap: false
|
||||
showTime: false
|
||||
thresholds:
|
||||
cpu:
|
||||
critical: 90
|
||||
warn: 70
|
||||
memory:
|
||||
critical: 90
|
||||
warn: 70
|
||||
`
|
||||
// var resetConfig = `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: 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
|
||||
// `
|
||||
|
|
|
|||
|
|
@ -18,27 +18,33 @@ type Config struct {
|
|||
Context *Context `yaml:"k9s"`
|
||||
}
|
||||
|
||||
// NewConfig returns a new config.
|
||||
func NewConfig(ct *api.Context) *Config {
|
||||
return &Config{
|
||||
Context: NewContextFromConfig(ct),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate ensures config is in norms.
|
||||
func (c *Config) Validate(conn client.Connection, ks KubeSettings) {
|
||||
if c.Context == nil {
|
||||
c.Context = NewContext()
|
||||
}
|
||||
c.Context.Validate(conn, ks)
|
||||
}
|
||||
|
||||
// Dump used for debugging.
|
||||
func (c *Config) Dump(w io.Writer) {
|
||||
bb, _ := yaml.Marshal(&c)
|
||||
|
||||
fmt.Fprintf(w, "%s\n", string(bb))
|
||||
}
|
||||
|
||||
// Save saves the config to disk.
|
||||
func (c *Config) Save(path string) error {
|
||||
if err := EnsureDirPath(path, DefaultDirMod); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
|
@ -14,12 +16,13 @@ const DefaultPFAddress = "localhost"
|
|||
// Context tracks K9s context configuration.
|
||||
type Context struct {
|
||||
ClusterName string `yaml:"cluster,omitempty"`
|
||||
ReadOnly bool `yaml:"readOnly"`
|
||||
ReadOnly *bool `yaml:"readOnly,omitempty"`
|
||||
Skin string `yaml:"skin,omitempty"`
|
||||
Namespace *Namespace `yaml:"namespace"`
|
||||
View *View `yaml:"view"`
|
||||
FeatureGates FeatureGates `yaml:"featureGates"`
|
||||
PortForwardAddress string `yaml:"portForwardAddress"`
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return &Context{
|
||||
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) {
|
||||
c.mx.Lock()
|
||||
defer c.mx.Unlock()
|
||||
|
||||
if c.PortForwardAddress == "" {
|
||||
c.PortForwardAddress = DefaultPFAddress
|
||||
}
|
||||
if cl, err := ks.CurrentClusterName(); err != nil {
|
||||
if cl, err := ks.CurrentClusterName(); err == nil {
|
||||
c.ClusterName = cl
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ package data
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/derailed/k9s/internal/config/json"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
|
|
@ -60,6 +62,10 @@ func (d *Dir) loadConfig(path string) (*Config, error) {
|
|||
if err != nil {
|
||||
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
|
||||
if err := yaml.Unmarshal(bb, &cfg); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ func TestEnsureDirPathNone(t *testing.T) {
|
|||
func TestEnsureDirPathNoOpt(t *testing.T) {
|
||||
var mod os.FileMode = 0744
|
||||
dir := filepath.Join("/tmp", "k9s-test")
|
||||
os.Remove(dir)
|
||||
assert.NoError(t, os.RemoveAll(dir))
|
||||
assert.NoError(t, os.Mkdir(dir, mod))
|
||||
|
||||
path := filepath.Join(dir, "duh.yaml")
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@ package data
|
|||
import (
|
||||
"os"
|
||||
|
||||
"github.com/derailed/k9s/internal/config/json"
|
||||
"k8s.io/client-go/tools/clientcmd/api"
|
||||
)
|
||||
|
||||
// JSONValidator validate yaml configurations.
|
||||
var JSONValidator = json.NewValidator()
|
||||
|
||||
const (
|
||||
// DefaultDirMod default unix perms for k9s directory.
|
||||
DefaultDirMod os.FileMode = 0744
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ var (
|
|||
AppHotKeysFile string
|
||||
)
|
||||
|
||||
// InitLogsLoc initializes K9s logs location.
|
||||
// InitLogLoc initializes K9s logs location.
|
||||
func InitLogLoc() error {
|
||||
var appLogDir string
|
||||
switch {
|
||||
|
|
@ -273,5 +273,9 @@ func EnsureHotkeysCfgFile() (string, error) {
|
|||
|
||||
// SkinFileFromName generate skin file path from spec.
|
||||
func SkinFileFromName(n string) string {
|
||||
if n == "" {
|
||||
n = "stock"
|
||||
}
|
||||
|
||||
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) {
|
||||
os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")
|
||||
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, 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"
|
||||
)
|
||||
|
||||
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.
|
||||
func isEnvSet(env string) bool {
|
||||
return os.Getenv(env) != ""
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/derailed/k9s/internal/config/data"
|
||||
"github.com/derailed/k9s/internal/config/json"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
|
@ -30,19 +33,32 @@ func NewHotKeys() HotKeys {
|
|||
}
|
||||
|
||||
// Load K9s plugins.
|
||||
func (h HotKeys) Load() error {
|
||||
return h.LoadHotKeys(AppHotKeysFile)
|
||||
func (h HotKeys) Load(path string) error {
|
||||
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.
|
||||
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 {
|
||||
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
|
||||
if err := yaml.Unmarshal(f, &hh); err != nil {
|
||||
if err := yaml.Unmarshal(bb, &hh); err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range hh.HotKey {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
func TestHotKeyLoad(t *testing.T) {
|
||||
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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/config/data"
|
||||
|
|
@ -13,19 +14,19 @@ import (
|
|||
|
||||
// K9s tracks K9s configuration options.
|
||||
type K9s struct {
|
||||
LiveViewAutoRefresh bool `yaml:"liveViewAutoRefresh"`
|
||||
ScreenDumpDir string `yaml:"screenDumpDir,omitempty"`
|
||||
RefreshRate int `yaml:"refreshRate"`
|
||||
MaxConnRetry int `yaml:"maxConnRetry"`
|
||||
ReadOnly bool `yaml:"readOnly"`
|
||||
NoExitOnCtrlC bool `yaml:"noExitOnCtrlC"`
|
||||
UI UI `yaml:"ui"`
|
||||
SkipLatestRevCheck bool `yaml:"skipLatestRevCheck"`
|
||||
DisablePodCounting bool `yaml:"disablePodCounting"`
|
||||
ShellPod *ShellPod `yaml:"shellPod"`
|
||||
ImageScans *ImageScans `yaml:"imageScans"`
|
||||
Logger *Logger `yaml:"logger"`
|
||||
Thresholds Threshold `yaml:"thresholds"`
|
||||
LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"`
|
||||
ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"`
|
||||
RefreshRate int `json:"refreshRate" yaml:"refreshRate"`
|
||||
MaxConnRetry int `json:"maxConnRetry" yaml:"maxConnRetry"`
|
||||
ReadOnly bool `json:"readOnly" yaml:"readOnly"`
|
||||
NoExitOnCtrlC bool `json:"noExitOnCtrlC" yaml:"noExitOnCtrlC"`
|
||||
UI UI `json:"ui" yaml:"ui"`
|
||||
SkipLatestRevCheck bool `json:"skipLatestRevCheck" yaml:"skipLatestRevCheck"`
|
||||
DisablePodCounting bool `json:"disablePodCounting" yaml:"disablePodCounting"`
|
||||
ShellPod ShellPod `json:"shellPod" yaml:"shellPod"`
|
||||
ImageScans ImageScans `json:"imageScans" yaml:"imageScans"`
|
||||
Logger Logger `json:"logger" yaml:"logger"`
|
||||
Thresholds Threshold `json:"thresholds" yaml:"thresholds"`
|
||||
manualRefreshRate int
|
||||
manualHeadless *bool
|
||||
manualLogoless *bool
|
||||
|
|
@ -38,6 +39,7 @@ type K9s struct {
|
|||
activeConfig *data.Config
|
||||
conn client.Connection
|
||||
ks data.KubeSettings
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
// 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) {
|
||||
k.mx.Lock()
|
||||
defer k.mx.Unlock()
|
||||
|
||||
k.conn = conn
|
||||
}
|
||||
|
||||
// Save saves the k9s config to dis.
|
||||
func (k *K9s) Save() error {
|
||||
if k.activeConfig != nil {
|
||||
path := filepath.Join(
|
||||
AppContextsDir,
|
||||
data.SanitizeContextSubpath(k.activeConfig.Context.ClusterName, k.activeContextName),
|
||||
data.MainConfigFile,
|
||||
)
|
||||
return k.activeConfig.Save(path)
|
||||
if k.activeConfig == nil {
|
||||
return fmt.Errorf("save failed. no active config detected")
|
||||
}
|
||||
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.
|
||||
func (k *K9s) Refine(k1 *K9s) {
|
||||
// Merge merges k9s configs.
|
||||
func (k *K9s) Merge(k1 *K9s) {
|
||||
if k1 == nil {
|
||||
return
|
||||
}
|
||||
|
||||
k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh
|
||||
k.ScreenDumpDir = k1.ScreenDumpDir
|
||||
k.RefreshRate = k1.RefreshRate
|
||||
|
|
@ -86,56 +95,31 @@ func (k *K9s) Refine(k1 *K9s) {
|
|||
k.SkipLatestRevCheck = k1.SkipLatestRevCheck
|
||||
k.DisablePodCounting = k1.DisablePodCounting
|
||||
k.ShellPod = k1.ShellPod
|
||||
k.ImageScans = k1.ImageScans
|
||||
k.Logger = k1.Logger
|
||||
k.ImageScans = k1.ImageScans
|
||||
k.Thresholds = k1.Thresholds
|
||||
}
|
||||
|
||||
// Override overrides k9s config from cli args.
|
||||
func (k *K9s) Override(k9sFlags *Flags) {
|
||||
if *k9sFlags.RefreshRate != DefaultRefreshRate {
|
||||
k.OverrideRefreshRate(*k9sFlags.RefreshRate)
|
||||
// AppScreenDumpDir fetch screen dumps dir.
|
||||
func (k *K9s) AppScreenDumpDir() string {
|
||||
d := k.ScreenDumpDir
|
||||
if isStringSet(k.manualScreenDumpDir) {
|
||||
d = *k.manualScreenDumpDir
|
||||
k.ScreenDumpDir = d
|
||||
}
|
||||
if d == "" {
|
||||
d = AppDumpsDir
|
||||
}
|
||||
|
||||
k.OverrideHeadless(*k9sFlags.Headless)
|
||||
k.OverrideLogoless(*k9sFlags.Logoless)
|
||||
k.OverrideCrumbsless(*k9sFlags.Crumbsless)
|
||||
k.OverrideReadOnly(*k9sFlags.ReadOnly)
|
||||
k.OverrideWrite(*k9sFlags.Write)
|
||||
k.OverrideCommand(*k9sFlags.Command)
|
||||
k.OverrideScreenDumpDir(*k9sFlags.ScreenDumpDir)
|
||||
return d
|
||||
}
|
||||
|
||||
// OverrideScreenDumpDir set the screen dump dir manually.
|
||||
func (k *K9s) OverrideScreenDumpDir(dir string) {
|
||||
k.manualScreenDumpDir = &dir
|
||||
// ContextScreenDumpDir fetch context specific screen dumps dir.
|
||||
func (k *K9s) ContextScreenDumpDir() string {
|
||||
return filepath.Join(k.AppScreenDumpDir(), k.contextPath())
|
||||
}
|
||||
|
||||
// GetScreenDumpDir fetch screen dumps dir.
|
||||
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 {
|
||||
func (k *K9s) contextPath() string {
|
||||
if k.activeConfig == nil {
|
||||
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.
|
||||
func (k *K9s) ActiveContextNamespace() (string, error) {
|
||||
if k.activeConfig != nil {
|
||||
return k.activeConfig.Context.Namespace.Active, nil
|
||||
act, err := k.ActiveContext()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", errors.New("context config is not set")
|
||||
return act.Namespace.Active, nil
|
||||
}
|
||||
|
||||
// ActiveContextName returns the active context name.
|
||||
func (k *K9s) ActiveContextName() string {
|
||||
k.mx.RLock()
|
||||
defer k.mx.RUnlock()
|
||||
|
||||
return k.activeContextName
|
||||
}
|
||||
|
||||
// ActiveContext returns the currently active context.
|
||||
func (k *K9s) ActiveContext() (*data.Context, error) {
|
||||
if k.activeConfig != nil {
|
||||
if k.activeConfig.Context == nil {
|
||||
ct, err := k.ks.CurrentContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k.activeConfig.Context = data.NewContextFromConfig(ct)
|
||||
}
|
||||
return k.activeConfig.Context, nil
|
||||
}
|
||||
var ac *data.Config
|
||||
k.mx.RLock()
|
||||
ac = k.activeConfig
|
||||
k.mx.RUnlock()
|
||||
|
||||
if ac != nil && ac.Context != nil {
|
||||
return ac.Context, nil
|
||||
}
|
||||
ct, err := k.ActivateContext(k.activeContextName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -181,7 +171,7 @@ func (k *K9s) ActiveContext() (*data.Context, error) {
|
|||
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) {
|
||||
k.activeContextName = n
|
||||
ct, err := k.ks.GetContext(n)
|
||||
|
|
@ -192,161 +182,128 @@ func (k *K9s) ActivateContext(n string) (*data.Context, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the context specifies a default namespace, use it!
|
||||
if k.conn != nil {
|
||||
k.Validate(k.conn, k.ks)
|
||||
if ns := k.conn.ActiveNamespace(); ns != client.BlankNamespace {
|
||||
k.activeConfig.Context.Namespace.Active = ns
|
||||
} else {
|
||||
k.activeConfig.Context.Namespace.Active = client.DefaultNamespace
|
||||
}
|
||||
|
||||
k.Validate(k.conn, k.ks)
|
||||
// If the context specifies a namespace, use it!
|
||||
if ns := ct.Namespace; ns != client.BlankNamespace {
|
||||
k.activeConfig.Context.Namespace.Active = ns
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// Reload reloads the active config from disk.
|
||||
// Reload reloads the context config from disk.
|
||||
func (k *K9s) Reload() error {
|
||||
k.mx.Lock()
|
||||
defer k.mx.Unlock()
|
||||
|
||||
ct, err := k.ks.GetContext(k.activeContextName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.activeConfig, err = k.dir.Load(k.activeContextName, ct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.activeConfig.Validate(k.conn, k.ks)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OverrideRefreshRate set the refresh rate manually.
|
||||
func (k *K9s) OverrideRefreshRate(r int) {
|
||||
k.manualRefreshRate = r
|
||||
}
|
||||
|
||||
// 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
|
||||
// Override overrides k9s config from cli args.
|
||||
func (k *K9s) Override(k9sFlags *Flags) {
|
||||
if k9sFlags.RefreshRate != nil && *k9sFlags.RefreshRate != DefaultRefreshRate {
|
||||
k.manualRefreshRate = *k9sFlags.RefreshRate
|
||||
}
|
||||
}
|
||||
|
||||
// OverrideWrite set the write mode manually.
|
||||
func (k *K9s) OverrideWrite(b bool) {
|
||||
if b {
|
||||
var flag bool
|
||||
k.manualReadOnly = &flag
|
||||
k.manualHeadless = k9sFlags.Headless
|
||||
k.manualLogoless = k9sFlags.Logoless
|
||||
k.manualCrumbsless = k9sFlags.Crumbsless
|
||||
if k9sFlags.ReadOnly != nil && *k9sFlags.ReadOnly {
|
||||
k.manualReadOnly = k9sFlags.ReadOnly
|
||||
}
|
||||
}
|
||||
|
||||
// OverrideCommand set the command manually.
|
||||
func (k *K9s) OverrideCommand(cmd string) {
|
||||
k.manualCommand = &cmd
|
||||
if k9sFlags.Write != nil && *k9sFlags.Write {
|
||||
var false bool
|
||||
k.manualReadOnly = &false
|
||||
}
|
||||
k.manualCommand = k9sFlags.Command
|
||||
k.manualScreenDumpDir = k9sFlags.ScreenDumpDir
|
||||
}
|
||||
|
||||
// IsHeadless returns headless setting.
|
||||
func (k *K9s) IsHeadless() bool {
|
||||
h := k.UI.Headless
|
||||
if k.manualHeadless != nil && *k.manualHeadless {
|
||||
h = *k.manualHeadless
|
||||
if isBoolSet(k.manualHeadless) {
|
||||
return true
|
||||
}
|
||||
|
||||
return h
|
||||
return k.UI.Headless
|
||||
}
|
||||
|
||||
// IsLogoless returns logoless setting.
|
||||
func (k *K9s) IsLogoless() bool {
|
||||
h := k.UI.Logoless
|
||||
if k.manualLogoless != nil && *k.manualLogoless {
|
||||
h = *k.manualLogoless
|
||||
if isBoolSet(k.manualLogoless) {
|
||||
return true
|
||||
}
|
||||
|
||||
return h
|
||||
return k.UI.Logoless
|
||||
}
|
||||
|
||||
// IsCrumbsless returns crumbsless setting.
|
||||
func (k *K9s) IsCrumbsless() bool {
|
||||
h := k.UI.Crumbsless
|
||||
if k.manualCrumbsless != nil && *k.manualCrumbsless {
|
||||
h = *k.manualCrumbsless
|
||||
if isBoolSet(k.manualCrumbsless) {
|
||||
return true
|
||||
}
|
||||
|
||||
return h
|
||||
return k.UI.Crumbsless
|
||||
}
|
||||
|
||||
// GetRefreshRate returns the current refresh rate.
|
||||
func (k *K9s) GetRefreshRate() int {
|
||||
rate := k.RefreshRate
|
||||
if k.manualRefreshRate != 0 {
|
||||
rate = k.manualRefreshRate
|
||||
return k.manualRefreshRate
|
||||
}
|
||||
|
||||
return rate
|
||||
return k.RefreshRate
|
||||
}
|
||||
|
||||
// IsReadOnly returns the readonly setting.
|
||||
func (k *K9s) IsReadOnly() bool {
|
||||
readOnly := k.ReadOnly
|
||||
if k.manualReadOnly != nil {
|
||||
readOnly = *k.manualReadOnly
|
||||
k.mx.RLock()
|
||||
defer k.mx.RUnlock()
|
||||
|
||||
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 {
|
||||
readOnly = true
|
||||
if k.manualReadOnly != nil {
|
||||
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 {
|
||||
k.RefreshRate = defaultRefreshRate
|
||||
}
|
||||
if k.MaxConnRetry <= 0 {
|
||||
k.MaxConnRetry = defaultMaxConnRetry
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the current configuration.
|
||||
func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) {
|
||||
k.validateDefaults()
|
||||
if k.activeConfig == nil {
|
||||
if n, err := ks.CurrentContextName(); err == nil {
|
||||
_, _ = k.ActivateContext(n)
|
||||
}
|
||||
}
|
||||
if k.ImageScans == nil {
|
||||
k.ImageScans = NewImageScans()
|
||||
}
|
||||
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()
|
||||
k.ShellPod = k.ShellPod.Validate()
|
||||
k.Logger = k.Logger.Validate()
|
||||
k.Thresholds = k.Thresholds.Validate()
|
||||
|
||||
if k.activeConfig != nil {
|
||||
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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/config/mock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
)
|
||||
|
||||
func TestGetScreenDumpDir(t *testing.T) {
|
||||
cfg := mock.NewMockConfig()
|
||||
func TestK9sReload(t *testing.T) {
|
||||
config.AppConfigDir = "/tmp/k9s-test"
|
||||
|
||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
||||
assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir())
|
||||
cl, ct := "cl-1", "ct-1-1"
|
||||
|
||||
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) {
|
||||
cfg := mock.NewMockConfig()
|
||||
func TestK9sMerge(t *testing.T) {
|
||||
cl, ct := "cl-1", "ct-1-1"
|
||||
|
||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
||||
cfg.K9s.OverrideScreenDumpDir("/override")
|
||||
assert.Equal(t, "/override", cfg.K9s.GetScreenDumpDir())
|
||||
uu := map[string]struct {
|
||||
k1, k2 *config.K9s
|
||||
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()
|
||||
_, err := cfg.K9s.ActivateContext("ct-1-1")
|
||||
|
||||
assert.Nil(t, cfg.Load("testdata/k9s.yaml"))
|
||||
cfg.K9s.OverrideScreenDumpDir("")
|
||||
assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||
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()
|
||||
|
||||
assert.Nil(t, cfg.Load("testdata/k9s1.yaml"))
|
||||
cfg.K9s.OverrideScreenDumpDir("")
|
||||
assert.Equal(t, config.AppDumpsDir, cfg.K9s.GetScreenDumpDir())
|
||||
assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml"))
|
||||
assert.Equal(t, "/tmp/k9s-test/screen-dumps", cfg.K9s.AppScreenDumpDir())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,17 +16,17 @@ const (
|
|||
|
||||
// Logger tracks logger options.
|
||||
type Logger struct {
|
||||
TailCount int64 `yaml:"tail"`
|
||||
BufferSize int `yaml:"buffer"`
|
||||
SinceSeconds int64 `yaml:"sinceSeconds"`
|
||||
FullScreenLogs bool `yaml:"fullScreenLogs"`
|
||||
TextWrap bool `yaml:"textWrap"`
|
||||
ShowTime bool `yaml:"showTime"`
|
||||
TailCount int64 `json:"tail" yaml:"tail"`
|
||||
BufferSize int `json:"buffer" yaml:"buffer"`
|
||||
SinceSeconds int64 `json:"sinceSeconds" yaml:"sinceSeconds"`
|
||||
FullScreen bool `json:"fullScreen" yaml:"fullScreen"`
|
||||
TextWrap bool `json:"textWrap" yaml:"textWrap"`
|
||||
ShowTime bool `json:"showTime" yaml:"showTime"`
|
||||
}
|
||||
|
||||
// NewLogger returns a new instance.
|
||||
func NewLogger() *Logger {
|
||||
return &Logger{
|
||||
func NewLogger() Logger {
|
||||
return Logger{
|
||||
TailCount: DefaultLoggerTailCount,
|
||||
BufferSize: MaxLogThreshold,
|
||||
SinceSeconds: DefaultSinceSeconds,
|
||||
|
|
@ -34,7 +34,7 @@ func NewLogger() *Logger {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
l.TailCount = DefaultLoggerTailCount
|
||||
}
|
||||
|
|
@ -47,4 +47,6 @@ func (l *Logger) Validate() {
|
|||
if l.SinceSeconds == 0 {
|
||||
l.SinceSeconds = DefaultSinceSeconds
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
func TestNewLogger(t *testing.T) {
|
||||
l := config.NewLogger()
|
||||
l.Validate()
|
||||
l = l.Validate()
|
||||
|
||||
assert.Equal(t, int64(100), l.TailCount)
|
||||
assert.Equal(t, 5000, l.BufferSize)
|
||||
|
|
@ -20,7 +20,7 @@ func TestNewLogger(t *testing.T) {
|
|||
|
||||
func TestLoggerValidate(t *testing.T) {
|
||||
var l config.Logger
|
||||
l.Validate()
|
||||
l = l.Validate()
|
||||
|
||||
assert.Equal(t, int64(100), l.TailCount)
|
||||
assert.Equal(t, 5000, l.BufferSize)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func EnsureDir(d string) error {
|
|||
|
||||
func NewMockConfig() *config.Config {
|
||||
config.AppContextsDir = "/tmp/test"
|
||||
cl, ct := "cl-1", "ct-1"
|
||||
cl, ct := "cl-1", "ct-1-1"
|
||||
flags := genericclioptions.ConfigFlags{
|
||||
ClusterName: &cl,
|
||||
Context: &ct,
|
||||
|
|
@ -63,7 +63,7 @@ func NewMockKubeSettings(f *genericclioptions.ConfigFlags) mockKubeSettings {
|
|||
},
|
||||
ctId + "-2": {
|
||||
Cluster: *f.ClusterName,
|
||||
Namespace: "ns-1",
|
||||
Namespace: "ns-2",
|
||||
},
|
||||
ctId + "-3": {
|
||||
Cluster: *f.ClusterName,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/derailed/k9s/internal/config/data"
|
||||
"github.com/derailed/k9s/internal/config/json"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
|
@ -47,6 +50,7 @@ func NewPlugins() Plugins {
|
|||
// Load K9s plugins.
|
||||
func (p Plugins) Load(path string) error {
|
||||
var errs error
|
||||
|
||||
if err := p.load(AppPluginsFile); err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
|
|
@ -74,12 +78,12 @@ func (p Plugins) loadPluginDir(dir string) error {
|
|||
if file.IsDir() || !isYamlFile(file.Name()) {
|
||||
continue
|
||||
}
|
||||
pluginFile, err := os.ReadFile(filepath.Join(dir, file.Name()))
|
||||
bb, err := os.ReadFile(filepath.Join(dir, file.Name()))
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
var plugin Plugin
|
||||
if err = yaml.Unmarshal(pluginFile, &plugin); err != nil {
|
||||
if err = yaml.Unmarshal(bb, &plugin); err != nil {
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
return nil
|
||||
}
|
||||
f, err := os.ReadFile(path)
|
||||
bb, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
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
|
||||
if err := yaml.Unmarshal(f, &pp); err != nil {
|
||||
if err := yaml.Unmarshal(bb, &pp); err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range pp.Plugins {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -39,10 +41,24 @@ var test2YmlTestData = Plugin{
|
|||
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) {
|
||||
p := NewPlugins()
|
||||
assert.Nil(t, p.load("testdata/plugins.yaml"))
|
||||
assert.Nil(t, p.loadPluginDir("/random/dir/not/exist"))
|
||||
assert.NoError(t, p.load("testdata/plugins.yaml"))
|
||||
assert.NoError(t, p.loadPluginDir("/random/dir/not/exist"))
|
||||
|
||||
assert.Equal(t, 1, len(p.Plugins))
|
||||
k, ok := p.Plugins["blah"]
|
||||
|
|
@ -53,8 +69,8 @@ func TestSinglePluginFileLoad(t *testing.T) {
|
|||
|
||||
func TestMultiplePluginFilesLoad(t *testing.T) {
|
||||
p := NewPlugins()
|
||||
assert.Nil(t, p.load("testdata/plugins.yaml"))
|
||||
assert.Nil(t, p.loadPluginDir("testdata/plugins"))
|
||||
assert.NoError(t, p.load("testdata/plugins.yaml"))
|
||||
assert.NoError(t, p.loadPluginDir("testdata/plugins"))
|
||||
|
||||
testPlugins := map[string]Plugin{
|
||||
"blah": pluginYmlTestData,
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ func (l Labels) exclude(k, val string) bool {
|
|||
|
||||
// ScanExcludes tracks vul scan exclusions.
|
||||
type ScanExcludes struct {
|
||||
Namespaces []string `yaml:"namespaces"`
|
||||
Labels Labels `yaml:"labels"`
|
||||
Namespaces []string `json:"namespaces" yaml:"namespaces"`
|
||||
Labels Labels `json:"labels" yaml:"labels"`
|
||||
}
|
||||
|
||||
func newScanExcludes() ScanExcludes {
|
||||
|
|
@ -50,19 +50,19 @@ func (b ScanExcludes) exclude(ns string, ll map[string]string) bool {
|
|||
|
||||
// ImageScans tracks vul scans options.
|
||||
type ImageScans struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Exclusions ScanExcludes `yaml:"exclusions"`
|
||||
Enable bool `json:"enable" yaml:"enable"`
|
||||
Exclusions ScanExcludes `json:"exclusions" yaml:"exclusions"`
|
||||
}
|
||||
|
||||
// NewImageScans returns a new instance.
|
||||
func NewImageScans() *ImageScans {
|
||||
return &ImageScans{
|
||||
func NewImageScans() ImageScans {
|
||||
return ImageScans{
|
||||
Exclusions: newScanExcludes(),
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
type ShellPod struct {
|
||||
Image string `yaml:"image"`
|
||||
Command []string `yaml:"command,omitempty"`
|
||||
Args []string `yaml:"args,omitempty"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
Limits Limits `yaml:"limits,omitempty"`
|
||||
Labels map[string]string `yaml:"labels,omitempty"`
|
||||
ImagePullSecrets []v1.LocalObjectReference `yaml:"imagePullSecrets,omitempty"`
|
||||
ImagePullPolicy v1.PullPolicy `yaml:"imagePullPolicy,omitempty"`
|
||||
TTY bool `yaml:"tty,omitempty"`
|
||||
Image string `json:"image" yaml:"image"`
|
||||
Command []string `json:"command,omitempty" yaml:"command,omitempty"`
|
||||
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
|
||||
Namespace string `json:"namespace" yaml:"namespace"`
|
||||
Limits Limits `json:"limits,omitempty" yaml:"limits,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
|
||||
ImagePullSecrets []v1.LocalObjectReference `json:"imagePullSecrets,omitempty" yaml:"imagePullSecrets,omitempty"`
|
||||
ImagePullPolicy v1.PullPolicy `json:"imagePullPolicy,omitempty" yaml:"imagePullPolicy,omitempty"`
|
||||
TTY bool `json:"tty,omitempty" yaml:"tty,omitempty"`
|
||||
}
|
||||
|
||||
// NewShellPod returns a new instance.
|
||||
func NewShellPod() *ShellPod {
|
||||
return &ShellPod{
|
||||
func NewShellPod() ShellPod {
|
||||
return ShellPod{
|
||||
Image: defaultDockerShellImage,
|
||||
Namespace: "default",
|
||||
Limits: defaultLimits(),
|
||||
|
|
@ -35,13 +35,15 @@ func NewShellPod() *ShellPod {
|
|||
}
|
||||
|
||||
// Validate validates the configuration.
|
||||
func (s *ShellPod) Validate() {
|
||||
func (s ShellPod) Validate() ShellPod {
|
||||
if s.Image == "" {
|
||||
s.Image = defaultDockerShellImage
|
||||
}
|
||||
if len(s.Limits) == 0 {
|
||||
s.Limits = defaultLimits()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func defaultLimits() Limits {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/derailed/k9s/internal/config/data"
|
||||
"github.com/derailed/k9s/internal/config/json"
|
||||
"github.com/derailed/tcell/v2"
|
||||
"github.com/derailed/tview"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
|
@ -18,204 +21,198 @@ type StyleListener interface {
|
|||
}
|
||||
|
||||
type (
|
||||
// Color represents a color.
|
||||
Color string
|
||||
|
||||
// Colors tracks multiple colors.
|
||||
Colors []Color
|
||||
|
||||
// Styles tracks K9s styling options.
|
||||
Styles struct {
|
||||
K9s Style `yaml:"k9s"`
|
||||
K9s Style `json:"k9s" yaml:"k9s"`
|
||||
listeners []StyleListener
|
||||
}
|
||||
|
||||
// Style tracks K9s styles.
|
||||
Style struct {
|
||||
Body Body `yaml:"body"`
|
||||
Prompt Prompt `yaml:"prompt"`
|
||||
Help Help `yaml:"help"`
|
||||
Frame Frame `yaml:"frame"`
|
||||
Info Info `yaml:"info"`
|
||||
Views Views `yaml:"views"`
|
||||
Dialog Dialog `yaml:"dialog"`
|
||||
Body Body `json:"body" yaml:"body"`
|
||||
Prompt Prompt `json:"prompt" yaml:"prompt"`
|
||||
Help Help `json:"help" yaml:"help"`
|
||||
Frame Frame `json:"frame" yaml:"frame"`
|
||||
Info Info `json:"info" yaml:"info"`
|
||||
Views Views `json:"views" yaml:"views"`
|
||||
Dialog Dialog `json:"dialog" yaml:"dialog"`
|
||||
}
|
||||
|
||||
// Prompt tracks command styles
|
||||
Prompt struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
SuggestColor Color `yaml:"suggestColor"`
|
||||
Border PromptBorder `yaml:"border"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
SuggestColor Color `json:"" yaml:"suggestColor"`
|
||||
Border PromptBorder `json:"" yaml:"border"`
|
||||
}
|
||||
|
||||
// PromptBorder tracks the color of the prompt depending on its kind (e.g., command or filter)
|
||||
PromptBorder struct {
|
||||
CommandColor Color `yaml:"command"`
|
||||
DefaultColor Color `yaml:"default"`
|
||||
CommandColor Color `json:"command" yaml:"command"`
|
||||
DefaultColor Color `json:"default" yaml:"default"`
|
||||
}
|
||||
|
||||
// Help tracks help styles.
|
||||
Help struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
SectionColor Color `yaml:"sectionColor"`
|
||||
KeyColor Color `yaml:"keyColor"`
|
||||
NumKeyColor Color `yaml:"numKeyColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
SectionColor Color `json:"sectionColor" yaml:"sectionColor"`
|
||||
KeyColor Color `json:"keyColor" yaml:"keyColor"`
|
||||
NumKeyColor Color `json:"numKeyColor" yaml:"numKeyColor"`
|
||||
}
|
||||
|
||||
// Body tracks body styles.
|
||||
Body struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
LogoColor Color `yaml:"logoColor"`
|
||||
LogoColorMsg Color `yaml:"logoColorMsg"`
|
||||
LogoColorInfo Color `yaml:"logoColorInfo"`
|
||||
LogoColorWarn Color `yaml:"logoColorWarn"`
|
||||
LogoColorError Color `yaml:"logoColorError"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
LogoColor Color `json:"logoColor" yaml:"logoColor"`
|
||||
LogoColorMsg Color `json:"logoColorMsg" yaml:"logoColorMsg"`
|
||||
LogoColorInfo Color `json:"logoColorInfo" yaml:"logoColorInfo"`
|
||||
LogoColorWarn Color `json:"logoColorWarn" yaml:"logoColorWarn"`
|
||||
LogoColorError Color `json:"logoColorError" yaml:"logoColorError"`
|
||||
}
|
||||
|
||||
// Dialog tracks dialog styles.
|
||||
Dialog struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
ButtonFgColor Color `yaml:"buttonFgColor"`
|
||||
ButtonBgColor Color `yaml:"buttonBgColor"`
|
||||
ButtonFocusFgColor Color `yaml:"buttonFocusFgColor"`
|
||||
ButtonFocusBgColor Color `yaml:"buttonFocusBgColor"`
|
||||
LabelFgColor Color `yaml:"labelFgColor"`
|
||||
FieldFgColor Color `yaml:"fieldFgColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
ButtonFgColor Color `json:"buttonFgColor" yaml:"buttonFgColor"`
|
||||
ButtonBgColor Color `json:"buttonBgColor" yaml:"buttonBgColor"`
|
||||
ButtonFocusFgColor Color `json:"buttonFocusFgColor" yaml:"buttonFocusFgColor"`
|
||||
ButtonFocusBgColor Color `json:"buttonFocusBgColor" yaml:"buttonFocusBgColor"`
|
||||
LabelFgColor Color `json:"labelFgColor" yaml:"labelFgColor"`
|
||||
FieldFgColor Color `json:"fieldFgColor" yaml:"fieldFgColor"`
|
||||
}
|
||||
|
||||
// Frame tracks frame styles.
|
||||
Frame struct {
|
||||
Title Title `yaml:"title"`
|
||||
Border Border `yaml:"border"`
|
||||
Menu Menu `yaml:"menu"`
|
||||
Crumb Crumb `yaml:"crumbs"`
|
||||
Status Status `yaml:"status"`
|
||||
Title Title `json:"title" yaml:"title"`
|
||||
Border Border `json:"border" yaml:"border"`
|
||||
Menu Menu `json:"menu" yaml:"menu"`
|
||||
Crumb Crumb `json:"crumbs" yaml:"crumbs"`
|
||||
Status Status `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// Views tracks individual view styles.
|
||||
Views struct {
|
||||
Table Table `yaml:"table"`
|
||||
Xray Xray `yaml:"xray"`
|
||||
Charts Charts `yaml:"charts"`
|
||||
Yaml Yaml `yaml:"yaml"`
|
||||
Picker Picker `yaml:"picker"`
|
||||
Log Log `yaml:"logs"`
|
||||
Table Table `json:"table" yaml:"table"`
|
||||
Xray Xray `json:"xray" yaml:"xray"`
|
||||
Charts Charts `json:"charts" yaml:"charts"`
|
||||
Yaml Yaml `json:"yaml" yaml:"yaml"`
|
||||
Picker Picker `json:"picker" yaml:"picker"`
|
||||
Log Log `json:"logs" yaml:"logs"`
|
||||
}
|
||||
|
||||
// Status tracks resource status styles.
|
||||
Status struct {
|
||||
NewColor Color `yaml:"newColor"`
|
||||
ModifyColor Color `yaml:"modifyColor"`
|
||||
AddColor Color `yaml:"addColor"`
|
||||
PendingColor Color `yaml:"pendingColor"`
|
||||
ErrorColor Color `yaml:"errorColor"`
|
||||
HighlightColor Color `yaml:"highlightColor"`
|
||||
KillColor Color `yaml:"killColor"`
|
||||
CompletedColor Color `yaml:"completedColor"`
|
||||
NewColor Color `json:"newColor" yaml:"newColor"`
|
||||
ModifyColor Color `json:"modifyColor" yaml:"modifyColor"`
|
||||
AddColor Color `json:"addColor" yaml:"addColor"`
|
||||
PendingColor Color `json:"pendingColor" yaml:"pendingColor"`
|
||||
ErrorColor Color `json:"errorColor" yaml:"errorColor"`
|
||||
HighlightColor Color `json:"highlightColor" yaml:"highlightColor"`
|
||||
KillColor Color `json:"killColor" yaml:"killColor"`
|
||||
CompletedColor Color `json:"completedColor" yaml:"completedColor"`
|
||||
}
|
||||
|
||||
// Log tracks Log styles.
|
||||
Log struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
Indicator LogIndicator `yaml:"indicator"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
Indicator LogIndicator `json:"indicator" yaml:"indicator"`
|
||||
}
|
||||
|
||||
// Picker tracks color when selecting containers
|
||||
Picker struct {
|
||||
MainColor Color `yaml:"mainColor"`
|
||||
FocusColor Color `yaml:"focusColor"`
|
||||
ShortcutColor Color `yaml:"shortcutColor"`
|
||||
MainColor Color `json:"mainColor" yaml:"mainColor"`
|
||||
FocusColor Color `json:"focusColor" yaml:"focusColor"`
|
||||
ShortcutColor Color `json:"shortcutColor" yaml:"shortcutColor"`
|
||||
}
|
||||
|
||||
// LogIndicator tracks log view indicator.
|
||||
LogIndicator struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
ToggleOnColor Color `yaml:"toggleOnColor"`
|
||||
ToggleOffColor Color `yaml:"toggleOffColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
ToggleOnColor Color `json:"toggleOnColor" yaml:"toggleOnColor"`
|
||||
ToggleOffColor Color `json:"toggleOffColor" yaml:"toggleOffColor"`
|
||||
}
|
||||
|
||||
// Yaml tracks yaml styles.
|
||||
Yaml struct {
|
||||
KeyColor Color `yaml:"keyColor"`
|
||||
ValueColor Color `yaml:"valueColor"`
|
||||
ColonColor Color `yaml:"colonColor"`
|
||||
KeyColor Color `json:"keyColor" yaml:"keyColor"`
|
||||
ValueColor Color `json:"valueColor" yaml:"valueColor"`
|
||||
ColonColor Color `json:"colonColor" yaml:"colonColor"`
|
||||
}
|
||||
|
||||
// Title tracks title styles.
|
||||
Title struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
HighlightColor Color `yaml:"highlightColor"`
|
||||
CounterColor Color `yaml:"counterColor"`
|
||||
FilterColor Color `yaml:"filterColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
HighlightColor Color `json:"highlightColor" yaml:"highlightColor"`
|
||||
CounterColor Color `json:"counterColor" yaml:"counterColor"`
|
||||
FilterColor Color `json:"filterColor" yaml:"filterColor"`
|
||||
}
|
||||
|
||||
// Info tracks info styles.
|
||||
Info struct {
|
||||
SectionColor Color `yaml:"sectionColor"`
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
SectionColor Color `json:"sectionColor" yaml:"sectionColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
}
|
||||
|
||||
// Border tracks border styles.
|
||||
Border struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
FocusColor Color `yaml:"focusColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
FocusColor Color `json:"focusColor" yaml:"focusColor"`
|
||||
}
|
||||
|
||||
// Crumb tracks crumbs styles.
|
||||
Crumb struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
ActiveColor Color `yaml:"activeColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
ActiveColor Color `json:"activeColor" yaml:"activeColor"`
|
||||
}
|
||||
|
||||
// Table tracks table styles.
|
||||
Table struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
CursorFgColor Color `yaml:"cursorFgColor"`
|
||||
CursorBgColor Color `yaml:"cursorBgColor"`
|
||||
MarkColor Color `yaml:"markColor"`
|
||||
Header TableHeader `yaml:"header"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
CursorFgColor Color `json:"cursorFgColor" yaml:"cursorFgColor"`
|
||||
CursorBgColor Color `json:"cursorBgColor" yaml:"cursorBgColor"`
|
||||
MarkColor Color `json:"markColor" yaml:"markColor"`
|
||||
Header TableHeader `json:"header" yaml:"header"`
|
||||
}
|
||||
|
||||
// TableHeader tracks table header styles.
|
||||
TableHeader struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
SorterColor Color `yaml:"sorterColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
SorterColor Color `json:"sorterColor" yaml:"sorterColor"`
|
||||
}
|
||||
|
||||
// Xray tracks xray styles.
|
||||
Xray struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
CursorColor Color `yaml:"cursorColor"`
|
||||
CursorTextColor Color `yaml:"cursorTextColor"`
|
||||
GraphicColor Color `yaml:"graphicColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
CursorColor Color `json:"cursorColor" yaml:"cursorColor"`
|
||||
CursorTextColor Color `json:"cursorTextColor" yaml:"cursorTextColor"`
|
||||
GraphicColor Color `json:"graphicColor" yaml:"graphicColor"`
|
||||
}
|
||||
|
||||
// Menu tracks menu styles.
|
||||
Menu struct {
|
||||
FgColor Color `yaml:"fgColor"`
|
||||
KeyColor Color `yaml:"keyColor"`
|
||||
NumKeyColor Color `yaml:"numKeyColor"`
|
||||
FgColor Color `json:"fgColor" yaml:"fgColor"`
|
||||
KeyColor Color `json:"keyColor" yaml:"keyColor"`
|
||||
NumKeyColor Color `json:"numKeyColor" yaml:"numKeyColor"`
|
||||
}
|
||||
|
||||
// Charts tracks charts styles.
|
||||
Charts struct {
|
||||
BgColor Color `yaml:"bgColor"`
|
||||
DialBgColor Color `yaml:"dialBgColor"`
|
||||
ChartBgColor Color `yaml:"chartBgColor"`
|
||||
DefaultDialColors Colors `yaml:"defaultDialColors"`
|
||||
DefaultChartColors Colors `yaml:"defaultChartColors"`
|
||||
ResourceColors map[string]Colors `yaml:"resourceColors"`
|
||||
BgColor Color `json:"bgColor" yaml:"bgColor"`
|
||||
DialBgColor Color `json:"dialBgColor" yaml:"dialBgColor"`
|
||||
ChartBgColor Color `json:"chartBgColor" yaml:"chartBgColor"`
|
||||
DefaultDialColors Colors `json:"defaultDialColors" yaml:"defaultDialColors"`
|
||||
DefaultChartColors Colors `json:"defaultChartColors" yaml:"defaultChartColors"`
|
||||
ResourceColors map[string]Colors `json:"resourceColors" yaml:"resourceColors"`
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -442,7 +439,9 @@ func NewStyles() *Styles {
|
|||
|
||||
// Reset resets styles.
|
||||
func (s *Styles) Reset() {
|
||||
s.K9s = newStyle()
|
||||
if err := yaml.Unmarshal(stockSkinTpl, s); err != nil {
|
||||
s.K9s = newStyle()
|
||||
}
|
||||
}
|
||||
|
||||
// FgColor returns the foreground color.
|
||||
|
|
@ -533,11 +532,14 @@ func (s *Styles) Views() Views {
|
|||
|
||||
// Load K9s configuration from file.
|
||||
func (s *Styles) Load(path string) error {
|
||||
f, err := os.ReadFile(path)
|
||||
bb, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -561,3 +563,9 @@ func (s *Styles) Update() {
|
|||
|
||||
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"
|
||||
)
|
||||
|
||||
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) {
|
||||
uu := map[string]tcell.Color{
|
||||
"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()
|
||||
assert.Nil(t, s.Load("testdata/empty_skin.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"))
|
||||
assert.Nil(t, s.Load("../../skins/black-and-wtf.yaml"))
|
||||
s.Update()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestSkinNotExits(t *testing.T) {
|
||||
s := config.NewStyles()
|
||||
assert.NotNil(t, s.Load("testdata/blee.yaml"))
|
||||
}
|
||||
func TestSkinLoad(t *testing.T) {
|
||||
uu := map[string]struct {
|
||||
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) {
|
||||
s := config.NewStyles()
|
||||
assert.NotNil(t, s.Load("testdata/skin_boarked.yaml"))
|
||||
for k := range uu {
|
||||
u := uu[k]
|
||||
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:
|
||||
dp: apps/v1/deployments
|
||||
dp: 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
|
||||
jo: jobs
|
||||
cr: clusterroles
|
||||
crb: clusterrolebindings
|
||||
ro: roles
|
||||
rb: rolebindings
|
||||
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
|
||||
server: https://1.2.3.4:8443
|
||||
name: cl-1
|
||||
- cluster:
|
||||
certificate-authority: /Users/test/ca.crt
|
||||
server: https://5.6.7.8:8443
|
||||
name: cl-2
|
||||
contexts:
|
||||
- context:
|
||||
cluster: cl-1
|
||||
user: user1
|
||||
namespace: ns-1
|
||||
name: ct-1
|
||||
name: ct-1-1
|
||||
- context:
|
||||
cluster: cl-1
|
||||
user: user2
|
||||
namespace: ns-2
|
||||
name: ct-2
|
||||
current-context: ct-1
|
||||
name: ct-1-2
|
||||
- context:
|
||||
cluster: cl-2
|
||||
user: user2
|
||||
namespace: ns-2
|
||||
name: ct-2-1
|
||||
current-context: ct-1-1
|
||||
preferences: {}
|
||||
users:
|
||||
- name: user1
|
||||
|
|
@ -31,11 +31,12 @@ k9s:
|
|||
highlightColor: navajowhite
|
||||
counterColor: navajowhite
|
||||
filterColor: slategray
|
||||
table:
|
||||
fgColor: white
|
||||
bgColor: black
|
||||
cursorColor: white
|
||||
header:
|
||||
fgColor: darkgray
|
||||
views:
|
||||
table:
|
||||
fgColor: white
|
||||
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.
|
||||
func (t Threshold) Validate() {
|
||||
func (t Threshold) Validate() Threshold {
|
||||
for _, k := range []string{"cpu", "memory"} {
|
||||
v, ok := t[k]
|
||||
if !ok {
|
||||
|
|
@ -70,6 +70,8 @@ func (t Threshold) Validate() {
|
|||
v.Validate()
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// LevelFor returns a defcon level for the current state.
|
||||
|
|
|
|||
|
|
@ -11,21 +11,24 @@ const (
|
|||
// UI tracks ui specific configs.
|
||||
type UI struct {
|
||||
// EnableMouse toggles mouse support.
|
||||
EnableMouse bool `yaml:"enableMouse"`
|
||||
EnableMouse bool `json:"enableMouse" yaml:"enableMouse"`
|
||||
|
||||
// Headless toggles top header display.
|
||||
Headless bool `yaml:"headless"`
|
||||
Headless bool `json:"headless" yaml:"headless"`
|
||||
|
||||
// LogoLess toggles k9s logo.
|
||||
Logoless bool `yaml:"logoless"`
|
||||
Logoless bool `json:"logoless" yaml:"logoless"`
|
||||
|
||||
// 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 bool `yaml:"noIcons"`
|
||||
NoIcons bool `json:"noIcons" yaml:"noIcons"`
|
||||
|
||||
// Skin reference the general k9s skin name.
|
||||
// Can be overridden per context.
|
||||
Skin string `yaml:"skin,omitempty"`
|
||||
Skin string `json:"skin" yaml:"skin,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/derailed/k9s/internal/config/data"
|
||||
"github.com/derailed/k9s/internal/config/json"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
|
@ -21,51 +25,41 @@ type ViewSetting struct {
|
|||
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.
|
||||
type CustomView struct {
|
||||
K9s ViewSettings `yaml:"k9s"`
|
||||
Views map[string]ViewSetting `yaml:"views"`
|
||||
listeners map[string]ViewConfigListener
|
||||
}
|
||||
|
||||
// NewCustomView returns a views configuration.
|
||||
func NewCustomView() *CustomView {
|
||||
return &CustomView{
|
||||
K9s: NewViewSettings(),
|
||||
Views: make(map[string]ViewSetting),
|
||||
listeners: make(map[string]ViewConfigListener),
|
||||
}
|
||||
}
|
||||
|
||||
// Reset clears out configurations.
|
||||
func (v *CustomView) Reset() {
|
||||
for k := range v.K9s.Views {
|
||||
delete(v.K9s.Views, k)
|
||||
for k := range v.Views {
|
||||
delete(v.Views, k)
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads view configurations.
|
||||
func (v *CustomView) Load(path string) error {
|
||||
raw, err := os.ReadFile(path)
|
||||
bb, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
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
|
||||
if err := yaml.Unmarshal(raw, &in); err != nil {
|
||||
if err := yaml.Unmarshal(bb, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
v.K9s = in.K9s
|
||||
v.Views = in.Views
|
||||
v.fireConfigChanged()
|
||||
|
||||
return nil
|
||||
|
|
@ -84,7 +78,7 @@ func (v *CustomView) RemoveListener(gvr string) {
|
|||
|
||||
func (v *CustomView) fireConfigChanged() {
|
||||
for gvr, list := range v.listeners {
|
||||
if v, ok := v.K9s.Views[gvr]; ok {
|
||||
if v, ok := v.Views[gvr]; ok {
|
||||
list.ViewSettingsChanged(v)
|
||||
} else {
|
||||
list.ViewSettingsChanged(ViewSetting{})
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import (
|
|||
func TestViewSettingsLoad(t *testing.T) {
|
||||
cfg := config.NewCustomView()
|
||||
|
||||
assert.Nil(t, cfg.Load("testdata/view_settings.yaml"))
|
||||
assert.Equal(t, 1, len(cfg.K9s.Views))
|
||||
assert.Equal(t, 4, len(cfg.K9s.Views["v1/pods"].Columns))
|
||||
assert.Nil(t, cfg.Load("testdata/views/views.yaml"))
|
||||
assert.Equal(t, 1, len(cfg.Views))
|
||||
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.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 {
|
||||
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 {
|
||||
flags.Spinach = &spinach
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
|
||||
"github.com/derailed/k9s/internal/client"
|
||||
"github.com/derailed/k9s/internal/dao"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
|
@ -72,24 +75,25 @@ func (c ClusterMeta) Deltas(n ClusterMeta) bool {
|
|||
|
||||
// ClusterInfo models cluster metadata.
|
||||
type ClusterInfo struct {
|
||||
cluster *Cluster
|
||||
factory dao.Factory
|
||||
data ClusterMeta
|
||||
version string
|
||||
skipLatestRevCheck bool
|
||||
listeners []ClusterInfoListener
|
||||
cache *cache.LRUExpireCache
|
||||
cluster *Cluster
|
||||
factory dao.Factory
|
||||
data ClusterMeta
|
||||
version string
|
||||
cfg *config.K9s
|
||||
listeners []ClusterInfoListener
|
||||
cache *cache.LRUExpireCache
|
||||
mx sync.RWMutex
|
||||
}
|
||||
|
||||
// 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{
|
||||
factory: f,
|
||||
cluster: NewCluster(f),
|
||||
data: NewClusterMeta(),
|
||||
version: v,
|
||||
skipLatestRevCheck: skipLatestRevCheck,
|
||||
cache: cache.NewLRUExpireCache(cacheSize),
|
||||
factory: f,
|
||||
cluster: NewCluster(f),
|
||||
data: NewClusterMeta(),
|
||||
version: v,
|
||||
cfg: cfg,
|
||||
cache: cache.NewLRUExpireCache(cacheSize),
|
||||
}
|
||||
|
||||
return &c
|
||||
|
|
@ -113,7 +117,16 @@ func (c *ClusterInfo) fetchK9sLatestRev() string {
|
|||
|
||||
// Reset resets context and reload.
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +151,7 @@ func (c *ClusterInfo) Refresh() {
|
|||
v1 := NewSemVer(data.K9sVer)
|
||||
|
||||
var latestRev string
|
||||
if !c.skipLatestRevCheck {
|
||||
if !c.cfg.SkipLatestRevCheck {
|
||||
latestRev = c.fetchK9sLatestRev()
|
||||
}
|
||||
v2 := NewSemVer(latestRev)
|
||||
|
|
@ -153,7 +166,11 @@ func (c *ClusterInfo) Refresh() {
|
|||
} else {
|
||||
c.fireNoMetaChanged(data)
|
||||
}
|
||||
c.data = data
|
||||
c.mx.Lock()
|
||||
{
|
||||
c.data = data
|
||||
}
|
||||
c.mx.Unlock()
|
||||
}
|
||||
|
||||
// AddListener adds a new model listener.
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ func (l *Log) SetSinceSeconds(ctx context.Context, i int64) {
|
|||
}
|
||||
|
||||
// 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.SinceSeconds = opts.SinceSeconds
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,19 @@ type (
|
|||
// ActionHandler handles a keyboard command.
|
||||
ActionHandler func(*tcell.EventKey) *tcell.EventKey
|
||||
|
||||
ActionOpts struct {
|
||||
Visible bool
|
||||
Shared bool
|
||||
Plugin bool
|
||||
HotKey bool
|
||||
Dangerous bool
|
||||
}
|
||||
|
||||
// KeyAction represents a keyboard action.
|
||||
KeyAction struct {
|
||||
Description string
|
||||
Action ActionHandler
|
||||
Visible bool
|
||||
Shared bool
|
||||
Opts ActionOpts
|
||||
}
|
||||
|
||||
// KeyActions tracks mappings between keystrokes and actions.
|
||||
|
|
@ -28,13 +35,32 @@ type (
|
|||
)
|
||||
|
||||
// NewKeyAction returns a new keyboard action.
|
||||
func NewKeyAction(d string, a ActionHandler, display bool) KeyAction {
|
||||
return KeyAction{Description: d, Action: a, Visible: display}
|
||||
func NewKeyAction(d string, a ActionHandler, visible bool) KeyAction {
|
||||
return NewKeyActionWithOpts(d, a, ActionOpts{
|
||||
Visible: visible,
|
||||
})
|
||||
}
|
||||
|
||||
// NewSharedKeyAction returns a new shared keyboard action.
|
||||
func NewSharedKeyAction(d string, a ActionHandler, display bool) KeyAction {
|
||||
return KeyAction{Description: d, Action: a, Visible: display, Shared: true}
|
||||
func NewSharedKeyAction(d string, a ActionHandler, visible bool) KeyAction {
|
||||
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.
|
||||
|
|
@ -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.
|
||||
func (a KeyActions) Set(aa KeyActions) {
|
||||
for k, v := range aa {
|
||||
|
|
@ -69,7 +104,7 @@ func (a KeyActions) Delete(kk ...tcell.Key) {
|
|||
func (a KeyActions) Hints() model.MenuHints {
|
||||
kk := make([]int, 0, len(a))
|
||||
for k := range a {
|
||||
if !a[k].Shared {
|
||||
if !a[k].Opts.Shared {
|
||||
kk = append(kk, int(k))
|
||||
}
|
||||
}
|
||||
|
|
@ -82,7 +117,7 @@ func (a KeyActions) Hints() model.MenuHints {
|
|||
model.MenuHint{
|
||||
Mnemonic: name,
|
||||
Description: a[tcell.Key(k)].Description,
|
||||
Visible: a[tcell.Key(k)].Visible,
|
||||
Visible: a[tcell.Key(k)].Opts.Visible,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -34,12 +34,11 @@ func NewApp(cfg *config.Config, context string) *App {
|
|||
a := App{
|
||||
Application: tview.NewApplication(),
|
||||
actions: make(KeyActions),
|
||||
Configurator: Configurator{Config: cfg},
|
||||
Configurator: Configurator{Config: cfg, Styles: config.NewStyles()},
|
||||
Main: NewPages(),
|
||||
flash: model.NewFlash(model.DefaultFlashDelay),
|
||||
cmdBuff: model.NewFishBuff(':', model.CommandBuffer),
|
||||
}
|
||||
a.ReloadStyles()
|
||||
|
||||
a.views = map[string]tview.Primitive{
|
||||
"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.
|
||||
func (a *App) Conn() client.Connection {
|
||||
return a.Config.GetConnection()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/derailed/k9s/internal/model"
|
||||
|
||||
"github.com/derailed/k9s/internal/config"
|
||||
"github.com/derailed/k9s/internal/render"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
|
|
@ -17,6 +19,8 @@ import (
|
|||
|
||||
// Synchronizer manages ui event queue.
|
||||
type synchronizer interface {
|
||||
Flash() *model.Flash
|
||||
UpdateClusterInfo()
|
||||
QueueUpdateDraw(func())
|
||||
QueueUpdate(func())
|
||||
}
|
||||
|
|
@ -46,7 +50,7 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e
|
|||
for {
|
||||
select {
|
||||
case evt := <-w.Events:
|
||||
if evt.Name == config.AppViewsFile {
|
||||
if evt.Name == config.AppViewsFile && evt.Op != fsnotify.Chmod {
|
||||
s.QueueUpdateDraw(func() {
|
||||
if err := c.RefreshCustomViews(); err != nil {
|
||||
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 w.Add(config.AppViewsFile)
|
||||
|
||||
return c.RefreshCustomViews()
|
||||
}
|
||||
|
||||
// RefreshCustomViews load view configuration changes.
|
||||
|
|
@ -85,15 +90,13 @@ func (c *Configurator) RefreshCustomViews() error {
|
|||
|
||||
// SkinsDirWatcher watches for skin directory file changes.
|
||||
func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error {
|
||||
if !c.HasSkin() {
|
||||
return nil
|
||||
if _, err := os.Stat(config.AppSkinsDir); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
|
|
@ -101,7 +104,7 @@ func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) erro
|
|||
if evt.Name == c.skinFile && evt.Op != fsnotify.Chmod {
|
||||
log.Debug().Msgf("Skin changed: %s", c.skinFile)
|
||||
s.QueueUpdateDraw(func() {
|
||||
c.RefreshStyles()
|
||||
c.RefreshStyles(s)
|
||||
})
|
||||
}
|
||||
case err := <-w.Errors:
|
||||
|
|
@ -133,18 +136,20 @@ func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error
|
|||
select {
|
||||
case evt := <-w.Events:
|
||||
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 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 {
|
||||
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() {
|
||||
c.RefreshStyles()
|
||||
c.RefreshStyles(s)
|
||||
})
|
||||
}
|
||||
case err := <-w.Errors:
|
||||
|
|
@ -181,11 +186,18 @@ func (c *Configurator) activeSkin() (string, bool) {
|
|||
return skin, false
|
||||
}
|
||||
|
||||
if ct, err := c.Config.K9s.ActiveContext(); err == nil {
|
||||
skin = ct.Skin
|
||||
if ct, err := c.Config.K9s.ActiveContext(); err == nil && 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 != ""
|
||||
|
|
@ -208,46 +220,53 @@ func (c *Configurator) activeConfig() (cluster string, context string, ok bool)
|
|||
}
|
||||
|
||||
// RefreshStyles load for skin configuration changes.
|
||||
func (c *Configurator) RefreshStyles() {
|
||||
func (c *Configurator) RefreshStyles(s synchronizer) {
|
||||
s.UpdateClusterInfo()
|
||||
if c.Styles == nil {
|
||||
c.Styles = config.NewStyles()
|
||||
}
|
||||
defer c.loadSkinFile(s)
|
||||
|
||||
cl, ct, ok := c.activeConfig()
|
||||
if !ok {
|
||||
log.Debug().Msgf("No custom skin found. Using stock skin")
|
||||
c.updateStyles("")
|
||||
return
|
||||
}
|
||||
|
||||
// !!BOZO!! Lame move out!
|
||||
if bc, err := config.EnsureBenchmarksCfgFile(cl, ct); err != nil {
|
||||
log.Warn().Err(err).Msgf("No benchmark config file found: %q@%q", cl, ct)
|
||||
} else {
|
||||
c.BenchFile = bc
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Configurator) loadSkinFile(s synchronizer) {
|
||||
skin, ok := c.activeSkin()
|
||||
if !ok {
|
||||
log.Debug().Msgf("No custom skin found. Using stock skin")
|
||||
c.updateStyles("")
|
||||
return
|
||||
}
|
||||
|
||||
skinFile := config.SkinFileFromName(skin)
|
||||
log.Debug().Msgf("Loading skin file: %q", skinFile)
|
||||
if err := c.Styles.Load(skinFile); err != nil {
|
||||
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 {
|
||||
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("")
|
||||
} else {
|
||||
log.Debug().Msgf("Loading skin file: %q", skinFile)
|
||||
s.Flash().Infof("Skin file loaded: %q", skinFile)
|
||||
c.updateStyles(skinFile)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Configurator) updateStyles(f string) {
|
||||
c.skinFile = f
|
||||
if f == "" {
|
||||
c.Styles.Reset()
|
||||
}
|
||||
c.Styles.Update()
|
||||
|
||||
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