diff --git a/Makefile b/Makefile
index e9e7f73a7b..d64fd545fc 100644
--- a/Makefile
+++ b/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.31.5
+VERSION ?= v0.31.6
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}
diff --git a/change_logs/release_v0.31.6.md b/change_logs/release_v0.31.6.md
new file mode 100644
index 0000000000..890e6aca5a
--- /dev/null
+++ b/change_logs/release_v0.31.6.md
@@ -0,0 +1,75 @@
+
+
+# Release v0.31.6
+
+## 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)
+
+## Maintenance Release!
+
+😱 More aftermath... 😱
+
+Thank you all for pitching in and helping flesh out issues!!
+
+Please make sure to add gory details to issues ie relevant configs, debug logs, etc...
+
+Comments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;(
+Everyone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant.
+
+---
+
+## NOTE
+
+In this drop, we've made k9s a bit more resilient (hopefully!) to configuration issues and in most cases k9s will come up but may exhibit `limp mode` behaviors.
+Please double check your k9s logs if things don't work as expected and file an issue with the `gory` details!
+
+☢️ This drop may cause `some disturbance in the farce!` ☢️
+
+Please proceed with caution with this one as we did our best to attempt to address potential context config file corruption by eliminating race conditions.
+It's late and I am operating on minimal sleep so I may have hosed some behaviors 🫣
+If you experience k9s locking up or misbehaving, as per the above👆 you know what to do now and as customary
+we will do our best to address them quickly to get you back up and running!
+
+Thank you for your support, kindness and patience!
+
+---
+
+## 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
+
+* [#2476](https://github.com/derailed/k9s/issues/2476) Pod's are not displayed for the selected namespace. Hopefully!
+* [#2471](https://github.com/derailed/k9s/issues/2471) Shell autocomplete functions do not work correctly
+
+---
+
+## 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!!
+
+* [#2480](https://github.com/derailed/k9s/pull/2480) Adding system arch to nodes view
+* [#2477](https://github.com/derailed/k9s/pull/2477) Shell autocomplete for k8s flags
+
+---
+
+
© 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
\ No newline at end of file
diff --git a/cmd/root.go b/cmd/root.go
index ea4d15cc6c..db029ab92b 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -47,9 +47,9 @@ var (
out = colorable.NewColorableStdout()
)
-type FlagError struct{ err error }
+type flagError struct{ err error }
-func (e *FlagError) Error() string { return e.err.Error() }
+func (e flagError) Error() string { return e.err.Error() }
func init() {
if err := config.InitLogLoc(); err != nil {
@@ -57,7 +57,7 @@ func init() {
}
rootCmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error {
- return &FlagError{err: err}
+ return flagError{err: err}
})
rootCmd.AddCommand(versionCmd(), infoCmd())
@@ -68,8 +68,7 @@ func init() {
// Execute root command.
func Execute() {
if err := rootCmd.Execute(); err != nil {
- var flagError *FlagError
- if !errors.As(err, &flagError) {
+ if !errors.As(err, &flagError{}) {
panic(err)
}
}
@@ -128,35 +127,36 @@ func loadConfiguration() (*config.Config, error) {
k8sCfg := client.NewConfig(k8sFlags)
k9sCfg := config.NewConfig(k8sCfg)
+ var errs error
conn, err := client.InitConnection(k8sCfg)
k9sCfg.SetConnection(conn)
if err != nil {
- return k9sCfg, err
+ errs = errors.Join(errs, err)
}
if err := k9sCfg.Load(config.AppConfigFile); err != nil {
- return k9sCfg, err
+ errs = errors.Join(errs, err)
}
k9sCfg.K9s.Override(k9sFlags)
if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {
log.Error().Err(err).Msgf("config refine failed")
- return k9sCfg, err
+ errs = errors.Join(errs, err)
}
// Try to access server version if that fail. Connectivity issue?
if !conn.CheckConnectivity() {
- return k9sCfg, fmt.Errorf("cannot connect to context: %s", k9sCfg.K9s.ActiveContextName())
+ errs = errors.Join(errs, fmt.Errorf("cannot connect to context: %s", k9sCfg.K9s.ActiveContextName()))
}
if !conn.ConnectionOK() {
- return k9sCfg, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName())
+ errs = errors.Join(errs, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName()))
}
log.Info().Msg("✅ Kubernetes connectivity")
if err := k9sCfg.Save(); err != nil {
log.Error().Err(err).Msg("Config save")
- return k9sCfg, err
+ errs = errors.Join(errs, err)
}
- return k9sCfg, nil
+ return k9sCfg, errs
}
func parseLevel(level string) zerolog.Level {
@@ -351,50 +351,58 @@ func initCertFlags() {
)
}
+type (
+ k8sPickerFn[T any] func(cfg *api.Config) map[string]T
+ completeFn func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)
+)
+
func initK8sFlagCompletion() {
- _ = rootCmd.RegisterFlagCompletionFunc("context", k8sFlagCompletionFunc(func(cfg *api.Config) map[string]*api.Context {
+ conn := client.NewConfig(k8sFlags)
+ cfg, err := conn.RawConfig()
+ if err != nil {
+ log.Error().Err(err).Msgf("k8s config getter failed")
+ }
+
+ _ = rootCmd.RegisterFlagCompletionFunc("context", k8sFlagCompletion(&cfg, func(cfg *api.Config) map[string]*api.Context {
return cfg.Contexts
}))
- _ = rootCmd.RegisterFlagCompletionFunc("cluster", k8sFlagCompletionFunc(func(cfg *api.Config) map[string]*api.Cluster {
+ _ = rootCmd.RegisterFlagCompletionFunc("cluster", k8sFlagCompletion(&cfg, func(cfg *api.Config) map[string]*api.Cluster {
return cfg.Clusters
}))
- _ = rootCmd.RegisterFlagCompletionFunc("user", k8sFlagCompletionFunc(func(cfg *api.Config) map[string]*api.AuthInfo {
+ _ = rootCmd.RegisterFlagCompletionFunc("user", k8sFlagCompletion(&cfg, func(cfg *api.Config) map[string]*api.AuthInfo {
return cfg.AuthInfos
}))
- _ = rootCmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- conn, err := client.InitConnection(client.NewConfig(k8sFlags))
- if err != nil {
- return nil, cobra.ShellCompDirectiveError
+ _ = rootCmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, s string) ([]string, cobra.ShellCompDirective) {
+ if c, err := client.InitConnection(conn); err == nil {
+ if nss, err := c.ValidNamespaceNames(); err == nil {
+ return filterFlagCompletions(nss, s)
+ }
}
- nss, err := conn.ValidNamespaceNames()
- if err != nil {
- return nil, cobra.ShellCompDirectiveError
- }
-
- return filterFlagCompletions(nss, toComplete)
+ return nil, cobra.ShellCompDirectiveError
})
}
-func k8sFlagCompletionFunc[T any](picker func(cfg *api.Config) map[string]T) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
+func k8sFlagCompletion[T any](cfg *api.Config, picker k8sPickerFn[T]) completeFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- k8sCfg, err := client.NewConfig(k8sFlags).RawConfig()
- if err != nil {
+ if cfg == nil {
return nil, cobra.ShellCompDirectiveError
}
- return filterFlagCompletions(picker(&k8sCfg), toComplete)
+
+ return filterFlagCompletions(picker(cfg), toComplete)
}
}
-func filterFlagCompletions[T any](m map[string]T, toComplete string) ([]string, cobra.ShellCompDirective) {
- var completions []string
+func filterFlagCompletions[T any](m map[string]T, s string) ([]string, cobra.ShellCompDirective) {
+ cc := make([]string, 0, len(m))
for name := range m {
- if strings.HasPrefix(name, toComplete) {
- completions = append(completions, name)
+ if strings.HasPrefix(name, s) {
+ cc = append(cc, name)
}
}
- return completions, cobra.ShellCompDirectiveNoFileComp
+
+ return cc, cobra.ShellCompDirectiveNoFileComp
}
diff --git a/internal/client/client.go b/internal/client/client.go
index 76dace3118..981e5db9f6 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -46,7 +46,7 @@ type APIClient struct {
mxsClient *versioned.Clientset
cachedClient *disk.CachedDiscoveryClient
config *Config
- mx sync.Mutex
+ mx sync.RWMutex
cache *cache.LRUExpireCache
connOK bool
}
@@ -143,10 +143,7 @@ func (a *APIClient) clearCache() {
// CanI checks if user has access to a certain resource.
func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) {
- a.mx.Lock()
- defer a.mx.Unlock()
-
- if !a.connOK {
+ if !a.getConnOK() {
return false, errors.New("ACCESS -- No API server connection")
}
if IsClusterWide(ns) {
@@ -305,14 +302,11 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
// CheckConnectivity return true if api server is cool or false otherwise.
func (a *APIClient) CheckConnectivity() bool {
- a.mx.Lock()
- defer a.mx.Unlock()
-
defer func() {
if err := recover(); err != nil {
- a.connOK = false
+ a.setConnOK(false)
}
- if !a.connOK {
+ if !a.getConnOK() {
a.clearCache()
}
}()
@@ -328,21 +322,21 @@ func (a *APIClient) CheckConnectivity() bool {
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
log.Error().Err(err).Msgf("Unable to connect to api server")
- a.connOK = false
- return a.connOK
+ a.setConnOK(false)
+ return a.getConnOK()
}
// Check connection
if _, err := client.ServerVersion(); err == nil {
- if !a.connOK {
+ if !a.getConnOK() {
a.reset()
}
} else {
log.Error().Err(err).Msgf("can't connect to cluster")
- a.connOK = false
+ a.setConnOK(false)
}
- return a.connOK
+ return a.getConnOK()
}
// Config return a kubernetes configuration.
@@ -355,13 +349,97 @@ func (a *APIClient) HasMetrics() bool {
return a.supportsMetricsResources() == nil
}
+func (a *APIClient) getMxsClient() *versioned.Clientset {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ return a.mxsClient
+}
+
+func (a *APIClient) setMxsClient(c *versioned.Clientset) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ a.mxsClient = c
+}
+
+func (a *APIClient) getCachedClient() *disk.CachedDiscoveryClient {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ return a.cachedClient
+}
+
+func (a *APIClient) setCachedClient(c *disk.CachedDiscoveryClient) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ a.cachedClient = c
+}
+
+func (a *APIClient) getDClient() dynamic.Interface {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ return a.dClient
+}
+
+func (a *APIClient) setDClient(c dynamic.Interface) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ a.dClient = c
+}
+
+func (a *APIClient) getConnOK() bool {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ return a.connOK
+}
+
+func (a *APIClient) setConnOK(b bool) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ a.connOK = b
+}
+
+func (a *APIClient) setLogClient(k kubernetes.Interface) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ a.logClient = k
+}
+
+func (a *APIClient) getLogClient() kubernetes.Interface {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ return a.logClient
+}
+
+func (a *APIClient) setClient(k kubernetes.Interface) {
+ a.mx.Lock()
+ defer a.mx.Unlock()
+
+ a.client = k
+}
+
+func (a *APIClient) getClient() kubernetes.Interface {
+ a.mx.RLock()
+ defer a.mx.RUnlock()
+
+ return a.client
+}
+
// DialLogs returns a handle to api server for logs.
func (a *APIClient) DialLogs() (kubernetes.Interface, error) {
- if !a.connOK {
- return nil, errors.New("no connection to dial")
+ if !a.getConnOK() {
+ return nil, errors.New("dialLogs - no connection to dial")
}
- if a.logClient != nil {
- return a.logClient, nil
+ if clt := a.getLogClient(); clt != nil {
+ return clt, nil
}
cfg, err := a.RestConfig()
@@ -369,31 +447,35 @@ func (a *APIClient) DialLogs() (kubernetes.Interface, error) {
return nil, err
}
cfg.Timeout = 0
- if a.logClient, err = kubernetes.NewForConfig(cfg); err != nil {
+ c, err := kubernetes.NewForConfig(cfg)
+ if err != nil {
return nil, err
}
+ a.setLogClient(c)
- return a.logClient, nil
+ return a.getLogClient(), nil
}
// Dial returns a handle to api server or die.
func (a *APIClient) Dial() (kubernetes.Interface, error) {
- if !a.connOK {
+ if !a.getConnOK() {
return nil, errors.New("no connection to dial")
}
- if a.client != nil {
- return a.client, nil
+ if c := a.getClient(); c != nil {
+ return c, nil
}
cfg, err := a.RestConfig()
if err != nil {
return nil, err
}
- if a.client, err = kubernetes.NewForConfig(cfg); err != nil {
+ if c, err := kubernetes.NewForConfig(cfg); err != nil {
return nil, err
+ } else {
+ a.setClient(c)
}
- return a.client, nil
+ return a.getClient(), nil
}
// RestConfig returns a rest api client.
@@ -403,15 +485,12 @@ func (a *APIClient) RestConfig() (*restclient.Config, error) {
// CachedDiscovery returns a cached discovery client.
func (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
- a.mx.Lock()
- defer a.mx.Unlock()
-
- if !a.connOK {
+ if !a.getConnOK() {
return nil, errors.New("no connection to cached dial")
}
- if a.cachedClient != nil {
- return a.cachedClient, nil
+ if c := a.getCachedClient(); c != nil {
+ return c, nil
}
cfg, err := a.RestConfig()
@@ -422,37 +501,38 @@ func (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {
httpCacheDir := filepath.Join(mustHomeDir(), ".kube", "http-cache")
discCacheDir := filepath.Join(mustHomeDir(), ".kube", "cache", "discovery", toHostDir(cfg.Host))
- a.cachedClient, err = disk.NewCachedDiscoveryClientForConfig(cfg, discCacheDir, httpCacheDir, cacheExpiry)
+ c, err := disk.NewCachedDiscoveryClientForConfig(cfg, discCacheDir, httpCacheDir, cacheExpiry)
if err != nil {
return nil, err
}
- return a.cachedClient, nil
+ a.setCachedClient(c)
+
+ return a.getCachedClient(), nil
}
// DynDial returns a handle to a dynamic interface.
func (a *APIClient) DynDial() (dynamic.Interface, error) {
- if a.dClient != nil {
- return a.dClient, nil
+ if c := a.getDClient(); c != nil {
+ return c, nil
}
cfg, err := a.RestConfig()
if err != nil {
return nil, err
}
- if a.dClient, err = dynamic.NewForConfig(cfg); err != nil {
- log.Panic().Err(err)
+ c, err := dynamic.NewForConfig(cfg)
+ if err != nil {
+ return nil, err
}
+ a.setDClient(c)
- return a.dClient, nil
+ return a.getDClient(), nil
}
// MXDial returns a handle to the metrics server.
func (a *APIClient) MXDial() (*versioned.Clientset, error) {
- a.mx.Lock()
- defer a.mx.Unlock()
-
- if a.mxsClient != nil {
- return a.mxsClient, nil
+ if c := a.getMxsClient(); c != nil {
+ return c, nil
}
cfg, err := a.RestConfig()
@@ -460,11 +540,13 @@ func (a *APIClient) MXDial() (*versioned.Clientset, error) {
return nil, err
}
- if a.mxsClient, err = versioned.NewForConfig(cfg); err != nil {
- log.Error().Err(err)
+ c, err := versioned.NewForConfig(cfg)
+ if err != nil {
+ return nil, err
}
+ a.setMxsClient(c)
- return a.mxsClient, err
+ return a.getMxsClient(), err
}
// SwitchContext handles kubeconfig context switches.
@@ -473,12 +555,8 @@ func (a *APIClient) SwitchContext(name string) error {
if err := a.config.SwitchContext(name); err != nil {
return err
}
- a.mx.Lock()
- {
- a.reset()
- ResetMetrics()
- }
- a.mx.Unlock()
+ a.reset()
+ ResetMetrics()
if !a.CheckConnectivity() {
return fmt.Errorf("unable to connect to context %q", name)
@@ -490,9 +568,14 @@ func (a *APIClient) SwitchContext(name string) error {
func (a *APIClient) reset() {
a.config.reset()
a.cache = cache.NewLRUExpireCache(cacheSize)
- a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil
- a.cachedClient, a.logClient = nil, nil
- a.connOK = true
+ a.nsClient = nil
+
+ a.setDClient(nil)
+ a.setMxsClient(nil)
+ a.setCachedClient(nil)
+ a.setClient(nil)
+ a.setLogClient(nil)
+ a.setConnOK(true)
}
func (a *APIClient) checkCacheBool(key string) (state bool, ok bool) {
diff --git a/internal/config/config.go b/internal/config/config.go
index a3fa3bef28..0390bf5e6a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -4,6 +4,7 @@
package config
import (
+ "errors"
"fmt"
"os"
@@ -58,7 +59,7 @@ func (c *Config) ContextPluginsPath() string {
return ""
}
- return AppContextPluginsFile(ct.ClusterName, c.K9s.activeContextName)
+ return AppContextPluginsFile(ct.GetClusterName(), c.K9s.activeContextName)
}
// Refine the configuration based on cli args.
@@ -218,17 +219,18 @@ func (c *Config) Load(path string) error {
if err != nil {
return err
}
+ var errs error
if err := data.JSONValidator.Validate(json.K9sSchema, bb); err != nil {
- return fmt.Errorf("k9s config file %q load failed:\n%w", path, err)
+ errs = errors.Join(errs, fmt.Errorf("k9s config file %q load failed:\n%w", path, err))
}
var cfg Config
if err := yaml.Unmarshal(bb, &cfg); err != nil {
- return fmt.Errorf("main config yaml load failed: %w", err)
+ errs = errors.Join(errs, fmt.Errorf("main config.yaml load failed: %w", err))
}
c.Merge(&cfg)
- return nil
+ return errs
}
// Save configuration to disk.
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 509bd5af5f..10c66cb670 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -583,90 +583,3 @@ func TestSetup(t *testing.T) {
fmt.Println("Boom!", m, i)
})
}
-
-// ----------------------------------------------------------------------------
-// Test Data...
-
-// 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/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
-// `
diff --git a/internal/config/data/config.go b/internal/config/data/config.go
index 6b821bb103..f005571ad2 100644
--- a/internal/config/data/config.go
+++ b/internal/config/data/config.go
@@ -29,8 +29,6 @@ func NewConfig(ct *api.Context) *Config {
// Validate ensures config is in norms.
func (c *Config) Validate(conn client.Connection, ks KubeSettings) {
- c.mx.Lock()
- defer c.mx.Unlock()
if c.Context == nil {
c.Context = NewContext()
diff --git a/internal/config/data/ns.go b/internal/config/data/ns.go
index ba8aacae27..1926289460 100644
--- a/internal/config/data/ns.go
+++ b/internal/config/data/ns.go
@@ -4,6 +4,8 @@
package data
import (
+ "sync"
+
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
)
@@ -18,6 +20,7 @@ type Namespace struct {
Active string `yaml:"active"`
LockFavorites bool `yaml:"lockFavorites"`
Favorites []string `yaml:"favorites"`
+ mx sync.RWMutex
}
// NewNamespace create a new namespace configuration.
@@ -37,6 +40,9 @@ func NewActiveNamespace(n string) *Namespace {
// Validate validates a namespace is setup correctly.
func (n *Namespace) Validate(c client.Connection) {
+ n.mx.RLock()
+ defer n.mx.RUnlock()
+
if c == nil || !c.IsValidNamespace(n.Active) {
return
}
@@ -50,14 +56,18 @@ func (n *Namespace) Validate(c client.Connection) {
// SetActive set the active namespace.
func (n *Namespace) SetActive(ns string, ks KubeSettings) error {
- if ns == client.BlankNamespace {
- ns = client.NamespaceAll
- }
if n == nil {
n = NewActiveNamespace(ns)
- } else {
- n.Active = ns
}
+
+ n.mx.Lock()
+ defer n.mx.Unlock()
+
+ if ns == client.BlankNamespace {
+ ns = client.NamespaceAll
+ }
+ n.Active = ns
+
if ns != "" && !n.LockFavorites {
n.addFavNS(ns)
}
diff --git a/internal/config/k9s.go b/internal/config/k9s.go
index 21453ce412..f4eb69fe8d 100644
--- a/internal/config/k9s.go
+++ b/internal/config/k9s.go
@@ -68,17 +68,17 @@ func (k *K9s) resetConnection(conn client.Connection) {
// Save saves the k9s config to disk.
func (k *K9s) Save() error {
- if k.activeConfig == nil {
+ if k.getActiveConfig() == nil {
log.Warn().Msgf("Save failed. no active config detected")
return nil
}
path := filepath.Join(
AppContextsDir,
- data.SanitizeContextSubpath(k.activeConfig.Context.ClusterName, k.activeContextName),
+ data.SanitizeContextSubpath(k.activeConfig.Context.GetClusterName(), k.getActiveContextName()),
data.MainConfigFile,
)
- return k.activeConfig.Save(path)
+ return k.getActiveConfig().Save(path)
}
// Merge merges k9s configs.
@@ -124,19 +124,20 @@ func (k *K9s) ContextScreenDumpDir() string {
}
func (k *K9s) contextPath() string {
- if k.activeConfig == nil {
+ if k.getActiveConfig() == nil {
return "na"
}
return data.SanitizeContextSubpath(
- k.activeConfig.Context.ClusterName,
+ k.getActiveConfig().Context.GetClusterName(),
k.ActiveContextName(),
)
}
// Reset resets configuration and context.
func (k *K9s) Reset() {
- k.activeConfig, k.activeContextName = nil, ""
+ k.setActiveConfig(nil)
+ k.setActiveContextName("")
}
// ActiveContextNamespace fetch the context active ns.
@@ -151,23 +152,16 @@ func (k *K9s) ActiveContextNamespace() (string, error) {
// ActiveContextName returns the active context name.
func (k *K9s) ActiveContextName() string {
- k.mx.RLock()
- defer k.mx.RUnlock()
-
- return k.activeContextName
+ return k.getActiveContextName()
}
// ActiveContext returns the currently active context.
func (k *K9s) ActiveContext() (*data.Context, error) {
- var ac *data.Config
- k.mx.RLock()
- ac = k.activeConfig
- k.mx.RUnlock()
- if ac != nil && ac.Context != nil {
- return ac.Context, nil
+ if cfg := k.getActiveConfig(); cfg != nil && cfg.Context != nil {
+ return cfg.Context, nil
}
- ct, err := k.ActivateContext(k.activeContextName)
+ ct, err := k.ActivateContext(k.getActiveContextName())
if err != nil {
return nil, err
}
@@ -175,46 +169,75 @@ func (k *K9s) ActiveContext() (*data.Context, error) {
return ct, nil
}
+func (k *K9s) setActiveConfig(c *data.Config) {
+ k.mx.Lock()
+ defer k.mx.Unlock()
+
+ k.activeConfig = c
+}
+
+func (k *K9s) getActiveConfig() *data.Config {
+ k.mx.RLock()
+ defer k.mx.RUnlock()
+
+ return k.activeConfig
+}
+
+func (k *K9s) setActiveContextName(n string) {
+ k.mx.Lock()
+ defer k.mx.Unlock()
+
+ k.activeContextName = n
+}
+
+func (k *K9s) getActiveContextName() string {
+ k.mx.RLock()
+ defer k.mx.RUnlock()
+
+ return k.activeContextName
+}
+
// ActivateContext initializes the active context if not present.
func (k *K9s) ActivateContext(n string) (*data.Context, error) {
- k.activeContextName = n
+ k.setActiveContextName(n)
ct, err := k.ks.GetContext(n)
if err != nil {
return nil, err
}
- k.activeConfig, err = k.dir.Load(n, ct)
+
+ cfg, err := k.dir.Load(n, ct)
if err != nil {
return nil, err
}
+ k.setActiveConfig(cfg)
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
+ k.getActiveConfig().Context.Namespace.Active = ns
} else if k.activeConfig.Context.Namespace.Active == "" {
- k.activeConfig.Context.Namespace.Active = client.DefaultNamespace
+ k.getActiveConfig().Context.Namespace.Active = client.DefaultNamespace
}
- if k.activeConfig.Context == nil {
+ if k.getActiveConfig().Context == nil {
return nil, fmt.Errorf("context activation failed for: %s", n)
}
- return k.activeConfig.Context, nil
+ return k.getActiveConfig().Context, nil
}
// 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)
+ ct, err := k.ks.GetContext(k.getActiveContextName())
if err != nil {
return err
}
- k.activeConfig, err = k.dir.Load(k.activeContextName, ct)
+
+ cfg, err := k.dir.Load(k.getActiveContextName(), ct)
if err != nil {
return err
}
- k.activeConfig.Validate(k.conn, k.ks)
+ k.setActiveConfig(cfg)
+ k.getActiveConfig().Validate(k.conn, k.ks)
return nil
}
@@ -277,12 +300,9 @@ func (k *K9s) GetRefreshRate() int {
// IsReadOnly returns the readonly setting.
func (k *K9s) IsReadOnly() bool {
- 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 cfg := k.getActiveConfig(); cfg != nil && cfg.Context.ReadOnly != nil {
+ ro = *cfg.Context.ReadOnly
}
if k.manualReadOnly != nil {
ro = true
@@ -300,7 +320,7 @@ func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) {
k.MaxConnRetry = defaultMaxConnRetry
}
- if k.activeConfig == nil {
+ if k.getActiveConfig() == nil {
if n, err := ks.CurrentContextName(); err == nil {
_, _ = k.ActivateContext(n)
}
@@ -309,7 +329,7 @@ func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) {
k.Logger = k.Logger.Validate()
k.Thresholds = k.Thresholds.Validate()
- if k.activeConfig != nil {
- k.activeConfig.Validate(c, ks)
+ if cfg := k.getActiveConfig(); cfg != nil {
+ cfg.Validate(c, ks)
}
}
diff --git a/internal/dao/container.go b/internal/dao/container.go
index f90d74c908..2074607f26 100644
--- a/internal/dao/container.go
+++ b/internal/dao/container.go
@@ -94,7 +94,7 @@ func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus {
}
func (c *Container) fetchPod(fqn string) (*v1.Pod, error) {
- o, err := c.GetFactory().Get("v1/pods", fqn, true, labels.Everything())
+ o, err := c.getFactory().Get("v1/pods", fqn, true, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/context.go b/internal/dao/context.go
index 222dbc6c32..671ed34f02 100644
--- a/internal/dao/context.go
+++ b/internal/dao/context.go
@@ -23,7 +23,7 @@ type Context struct {
}
func (c *Context) config() *client.Config {
- return c.GetFactory().Client().Config()
+ return c.getFactory().Client().Config()
}
// Get a Context.
@@ -60,5 +60,5 @@ func (c *Context) MustCurrentContextName() string {
// Switch to another context.
func (c *Context) Switch(ctx string) error {
- return c.GetFactory().Client().SwitchContext(ctx)
+ return c.getFactory().Client().SwitchContext(ctx)
}
diff --git a/internal/dao/crd.go b/internal/dao/crd.go
index fcf7f53b65..16f06076d7 100644
--- a/internal/dao/crd.go
+++ b/internal/dao/crd.go
@@ -44,5 +44,5 @@ func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtim
}
const gvr = "apiextensions.k8s.io/v1/customresourcedefinitions"
- return c.GetFactory().List(gvr, "-", false, labelSel)
+ return c.getFactory().List(gvr, "-", false, labelSel)
}
diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go
index 99ae897e2e..6feee0ca11 100644
--- a/internal/dao/cronjob.go
+++ b/internal/dao/cronjob.go
@@ -56,7 +56,7 @@ func (c *CronJob) Run(path string) error {
return fmt.Errorf("user is not authorized to run jobs")
}
- o, err := c.GetFactory().Get(c.GVR(), path, true, labels.Everything())
+ o, err := c.getFactory().Get(c.GVR(), path, true, labels.Everything())
if err != nil {
return err
}
@@ -102,7 +102,7 @@ func (c *CronJob) Run(path string) error {
// ScanSA scans for serviceaccount refs.
func (c *CronJob) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := c.GetFactory().List(c.GVR(), ns, wait, labels.Everything())
+ oo, err := c.getFactory().List(c.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
@@ -127,7 +127,7 @@ func (c *CronJob) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, erro
// GetInstance fetch a matching cronjob.
func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) {
- o, err := c.GetFactory().Get(c.GVR(), fqn, true, labels.Everything())
+ o, err := c.getFactory().Get(c.GVR(), fqn, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -175,7 +175,7 @@ func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
// Scan scans for cluster resource refs.
func (c *CronJob) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := c.GetFactory().List(c.GVR(), ns, wait, labels.Everything())
+ oo, err := c.getFactory().List(c.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/dp.go b/internal/dao/dp.go
index 2eb5e56bbf..4ab1a4badb 100644
--- a/internal/dao/dp.go
+++ b/internal/dao/dp.go
@@ -81,7 +81,7 @@ func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) err
// Restart a Deployment rollout.
func (d *Deployment) Restart(ctx context.Context, path string) error {
- o, err := d.GetFactory().Get("apps/v1/deployments", path, true, labels.Everything())
+ o, err := d.getFactory().Get("apps/v1/deployments", path, true, labels.Everything())
if err != nil {
return err
}
@@ -170,7 +170,7 @@ func (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) {
// ScanSA scans for serviceaccount refs.
func (d *Deployment) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := d.GetFactory().List(d.GVR(), ns, wait, labels.Everything())
+ oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
@@ -196,7 +196,7 @@ func (d *Deployment) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, e
// Scan scans for resource references.
func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := d.GetFactory().List(d.GVR(), ns, wait, labels.Everything())
+ oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/ds.go b/internal/dao/ds.go
index 0850a8ab3e..1488960762 100644
--- a/internal/dao/ds.go
+++ b/internal/dao/ds.go
@@ -58,7 +58,7 @@ func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool {
// Restart a DaemonSet rollout.
func (d *DaemonSet) Restart(ctx context.Context, path string) error {
- o, err := d.GetFactory().Get("apps/v1/daemonsets", path, true, labels.Everything())
+ o, err := d.getFactory().Get("apps/v1/daemonsets", path, true, labels.Everything())
if err != nil {
return err
}
@@ -173,7 +173,7 @@ func (d *DaemonSet) Pod(fqn string) (string, error) {
// GetInstance returns a daemonset instance.
func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) {
- o, err := d.GetFactory().Get(d.gvr.String(), fqn, true, labels.Everything())
+ o, err := d.getFactory().Get(d.gvrStr(), fqn, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -190,7 +190,7 @@ func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) {
// ScanSA scans for serviceaccount refs.
func (d *DaemonSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := d.GetFactory().List(d.GVR(), ns, wait, labels.Everything())
+ oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
@@ -216,7 +216,7 @@ func (d *DaemonSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, er
// Scan scans for cluster refs.
func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := d.GetFactory().List(d.GVR(), ns, wait, labels.Everything())
+ oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/generic.go b/internal/dao/generic.go
index 489c3f835c..cf579cf7e8 100644
--- a/internal/dao/generic.go
+++ b/internal/dao/generic.go
@@ -106,7 +106,7 @@ func (g *Generic) ToYAML(path string, showManaged bool) (string, error) {
// Delete deletes a resource.
func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
ns, n := client.Namespaced(path)
- auth, err := g.Client().CanI(ns, g.gvr.String(), []string{client.DeleteVerb})
+ auth, err := g.Client().CanI(ns, g.gvrStr(), []string{client.DeleteVerb})
if err != nil {
return err
}
diff --git a/internal/dao/job.go b/internal/dao/job.go
index 1444929a34..286cde59be 100644
--- a/internal/dao/job.go
+++ b/internal/dao/job.go
@@ -73,7 +73,7 @@ func (j *Job) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// TailLogs tail logs for all pods represented by this Job.
func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {
- o, err := j.GetFactory().Get(j.gvr.String(), opts.Path, true, labels.Everything())
+ o, err := j.getFactory().Get(j.gvrStr(), opts.Path, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -92,7 +92,7 @@ func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
}
func (j *Job) GetInstance(fqn string) (*batchv1.Job, error) {
- o, err := j.GetFactory().Get(j.gvr.String(), fqn, true, labels.Everything())
+ o, err := j.getFactory().Get(j.gvrStr(), fqn, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -109,7 +109,7 @@ func (j *Job) GetInstance(fqn string) (*batchv1.Job, error) {
// ScanSA scans for serviceaccount refs.
func (j *Job) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := j.GetFactory().List(j.GVR(), ns, wait, labels.Everything())
+ oo, err := j.getFactory().List(j.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
@@ -135,7 +135,7 @@ func (j *Job) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
// Scan scans for resource references.
func (j *Job) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := j.GetFactory().List(j.GVR(), ns, wait, labels.Everything())
+ oo, err := j.getFactory().List(j.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/node.go b/internal/dao/node.go
index db0c8fffef..da5c8ddfba 100644
--- a/internal/dao/node.go
+++ b/internal/dao/node.go
@@ -56,7 +56,7 @@ func (n *Node) ToggleCordon(path string, cordon bool) error {
}
return fmt.Errorf("node is already uncordoned")
}
- dial, err := n.GetFactory().Client().Dial()
+ dial, err := n.getFactory().Client().Dial()
if err != nil {
return err
}
@@ -98,7 +98,7 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
}
}
- dial, err := n.GetFactory().Client().Dial()
+ dial, err := n.getFactory().Client().Dial()
if err != nil {
return err
}
@@ -189,7 +189,7 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// CountPods counts the pods scheduled on a given node.
func (n *Node) CountPods(nodeName string) (int, error) {
var count int
- oo, err := n.GetFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything())
+ oo, err := n.getFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything())
if err != nil {
return 0, err
}
@@ -213,7 +213,7 @@ func (n *Node) CountPods(nodeName string) (int, error) {
// GetPods returns all pods running on given node.
func (n *Node) GetPods(nodeName string) ([]*v1.Pod, error) {
- oo, err := n.GetFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything())
+ oo, err := n.getFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/non_resource.go b/internal/dao/non_resource.go
index c32728d3da..9840532ec4 100644
--- a/internal/dao/non_resource.go
+++ b/internal/dao/non_resource.go
@@ -29,7 +29,14 @@ func (n *NonResource) Init(f Factory, gvr client.GVR) {
n.mx.Unlock()
}
-func (n *NonResource) GetFactory() Factory {
+func (n *NonResource) gvrStr() string {
+ n.mx.RLock()
+ defer n.mx.RUnlock()
+
+ return n.gvr.String()
+}
+
+func (n *NonResource) getFactory() Factory {
n.mx.RLock()
defer n.mx.RUnlock()
@@ -41,7 +48,7 @@ func (n *NonResource) GVR() string {
n.mx.RLock()
defer n.mx.RUnlock()
- return n.gvr.String()
+ return n.gvrStr()
}
// Get returns the given resource.
diff --git a/internal/dao/pod.go b/internal/dao/pod.go
index 850454499c..1f34b60cef 100644
--- a/internal/dao/pod.go
+++ b/internal/dao/pod.go
@@ -177,7 +177,7 @@ func (p *Pod) Pod(fqn string) (string, error) {
// GetInstance returns a pod instance.
func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {
- o, err := p.GetFactory().Get(p.gvr.String(), fqn, true, labels.Everything())
+ o, err := p.getFactory().Get(p.gvrStr(), fqn, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -197,7 +197,7 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
if !ok {
return nil, errors.New("no factory in context")
}
- o, err := fac.Get(p.gvr.String(), opts.Path, true, labels.Everything())
+ o, err := fac.Get(p.gvrStr(), opts.Path, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -240,7 +240,7 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
// ScanSA scans for ServiceAccount refs.
func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := p.GetFactory().List(p.GVR(), ns, wait, labels.Everything())
+ oo, err := p.getFactory().List(p.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
@@ -270,7 +270,7 @@ func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
// Scan scans for cluster resource refs.
func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := p.GetFactory().List(p.GVR(), ns, wait, labels.Everything())
+ oo, err := p.getFactory().List(p.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/popeye.go b/internal/dao/popeye.go
index 0ec61fd0f8..e940f1b561 100644
--- a/internal/dao/popeye.go
+++ b/internal/dao/popeye.go
@@ -69,7 +69,7 @@ func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error)
flags.ActiveNamespace = &ns
}
spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml")
- if c, err := p.GetFactory().Client().Config().CurrentContextName(); err == nil {
+ if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil {
spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c))
}
if _, err := os.Stat(spinach); err == nil {
diff --git a/internal/dao/port_forward.go b/internal/dao/port_forward.go
index 4efa7509ef..3a0b6280fe 100644
--- a/internal/dao/port_forward.go
+++ b/internal/dao/port_forward.go
@@ -30,7 +30,7 @@ type PortForward struct {
// Delete deletes a portforward.
func (p *PortForward) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error {
- p.GetFactory().DeleteForwarder(path)
+ p.getFactory().DeleteForwarder(path)
return nil
}
@@ -48,7 +48,7 @@ func (p *PortForward) List(ctx context.Context, _ string) ([]runtime.Object, err
log.Debug().Msgf("No custom benchmark config file found: %q", benchFile)
}
- ff, cc := p.GetFactory().Forwarders(), config.Benchmarks.Containers
+ ff, cc := p.getFactory().Forwarders(), config.Benchmarks.Containers
oo := make([]runtime.Object, 0, len(ff))
for k, f := range ff {
if !strings.HasPrefix(k, path) {
diff --git a/internal/dao/rbac.go b/internal/dao/rbac.go
index 68dcaddb64..7e688aee4d 100644
--- a/internal/dao/rbac.go
+++ b/internal/dao/rbac.go
@@ -60,7 +60,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) {
}
func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
- o, err := r.GetFactory().Get(crbGVR, path, true, labels.Everything())
+ o, err := r.getFactory().Get(crbGVR, path, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -71,7 +71,7 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
return nil, err
}
- crbo, err := r.GetFactory().Get(crGVR, client.FQN("-", crb.RoleRef.Name), true, labels.Everything())
+ crbo, err := r.getFactory().Get(crGVR, client.FQN("-", crb.RoleRef.Name), true, labels.Everything())
if err != nil {
return nil, err
}
@@ -85,7 +85,7 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {
}
func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
- o, err := r.GetFactory().Get(rbGVR, path, true, labels.Everything())
+ o, err := r.getFactory().Get(rbGVR, path, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -96,7 +96,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
}
if rb.RoleRef.Kind == "ClusterRole" {
- o, e := r.GetFactory().Get(crGVR, client.FQN("-", rb.RoleRef.Name), true, labels.Everything())
+ o, e := r.getFactory().Get(crGVR, client.FQN("-", rb.RoleRef.Name), true, labels.Everything())
if e != nil {
return nil, e
}
@@ -108,7 +108,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
return asRuntimeObjects(parseRules(client.ClusterScope, "-", cr.Rules)), nil
}
- ro, err := r.GetFactory().Get(rGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), true, labels.Everything())
+ ro, err := r.getFactory().Get(rGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), true, labels.Everything())
if err != nil {
return nil, err
}
@@ -123,7 +123,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {
func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) {
log.Debug().Msgf("LOAD-CR %q", path)
- o, err := r.GetFactory().Get(crGVR, path, true, labels.Everything())
+ o, err := r.getFactory().Get(crGVR, path, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -138,7 +138,7 @@ func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) {
}
func (r *Rbac) loadRole(path string) ([]runtime.Object, error) {
- o, err := r.GetFactory().Get(rGVR, path, true, labels.Everything())
+ o, err := r.getFactory().Get(rGVR, path, true, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/rbac_policy.go b/internal/dao/rbac_policy.go
index e74f5b7718..7c901c227a 100644
--- a/internal/dao/rbac_policy.go
+++ b/internal/dao/rbac_policy.go
@@ -195,7 +195,7 @@ func isSameSubject(kind, ns, name string, subject *rbacv1.Subject) bool {
func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) {
const gvr = "rbac.authorization.k8s.io/v1/clusterroles"
- oo, err := p.GetFactory().List(gvr, client.ClusterScope, false, labels.Everything())
+ oo, err := p.getFactory().List(gvr, client.ClusterScope, false, labels.Everything())
if err != nil {
return nil, err
}
@@ -215,7 +215,7 @@ func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) {
func (p *Policy) fetchRoles() ([]rbacv1.Role, error) {
const gvr = "rbac.authorization.k8s.io/v1/roles"
- oo, err := p.GetFactory().List(gvr, client.BlankNamespace, false, labels.Everything())
+ oo, err := p.getFactory().List(gvr, client.BlankNamespace, false, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/resource.go b/internal/dao/resource.go
index 5cf85d71d3..d704c4b5df 100644
--- a/internal/dao/resource.go
+++ b/internal/dao/resource.go
@@ -33,12 +33,12 @@ func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error
}
}
- return r.GetFactory().List(r.gvr.String(), ns, false, lsel)
+ return r.getFactory().List(r.gvrStr(), ns, false, lsel)
}
// Get returns a resource instance if found, else an error.
func (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) {
- return r.GetFactory().Get(r.gvr.String(), path, true, labels.Everything())
+ return r.getFactory().Get(r.gvrStr(), path, true, labels.Everything())
}
// ToYAML returns a resource yaml.
diff --git a/internal/dao/sts.go b/internal/dao/sts.go
index 3d3dbb4123..219b613c39 100644
--- a/internal/dao/sts.go
+++ b/internal/dao/sts.go
@@ -174,7 +174,7 @@ func (s *StatefulSet) Pod(fqn string) (string, error) {
}
func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) {
- o, err := s.GetFactory().Get(s.gvr.String(), fqn, true, labels.Everything())
+ o, err := s.getFactory().Get(s.gvrStr(), fqn, true, labels.Everything())
if err != nil {
return nil, err
}
@@ -191,7 +191,7 @@ func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) {
// ScanSA scans for serviceaccount refs.
func (s *StatefulSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := s.GetFactory().List(s.GVR(), ns, wait, labels.Everything())
+ oo, err := s.getFactory().List(s.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
@@ -217,7 +217,7 @@ func (s *StatefulSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs,
// Scan scans for cluster resource refs.
func (s *StatefulSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) {
ns, n := client.Namespaced(fqn)
- oo, err := s.GetFactory().List(s.GVR(), ns, wait, labels.Everything())
+ oo, err := s.getFactory().List(s.GVR(), ns, wait, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/dao/svc.go b/internal/dao/svc.go
index 382a4373c2..1e8fcfe877 100644
--- a/internal/dao/svc.go
+++ b/internal/dao/svc.go
@@ -51,7 +51,7 @@ func (s *Service) Pod(fqn string) (string, error) {
// GetInstance returns a service instance.
func (s *Service) GetInstance(fqn string) (*v1.Service, error) {
- o, err := s.GetFactory().Get(s.gvr.String(), fqn, true, labels.Everything())
+ o, err := s.getFactory().Get(s.gvrStr(), fqn, true, labels.Everything())
if err != nil {
return nil, err
}
diff --git a/internal/model/stack.go b/internal/model/stack.go
index 19f291a304..b660a1ab1b 100644
--- a/internal/model/stack.go
+++ b/internal/model/stack.go
@@ -166,6 +166,8 @@ func (s *Stack) Top() Component {
return nil
}
+ s.mx.RLock()
+ defer s.mx.RUnlock()
return s.components[len(s.components)-1]
}
diff --git a/internal/model/table.go b/internal/model/table.go
index c6e84c9fcf..6259382799 100644
--- a/internal/model/table.go
+++ b/internal/model/table.go
@@ -76,6 +76,9 @@ func (t *Table) SetInstance(path string) {
// AddListener adds a new model listener.
func (t *Table) AddListener(l TableListener) {
+ t.mx.Lock()
+ defer t.mx.Unlock()
+
t.listeners = append(t.listeners, l)
}
@@ -91,8 +94,8 @@ func (t *Table) RemoveListener(l TableListener) {
if victim >= 0 {
t.mx.Lock()
- defer t.mx.Unlock()
t.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...)
+ t.mx.Unlock()
}
}
@@ -289,16 +292,23 @@ func (t *Table) reconcile(ctx context.Context) error {
}
func (t *Table) fireTableChanged(data *render.TableData) {
+ var ll []TableListener
t.mx.RLock()
- defer t.mx.RUnlock()
+ ll = t.listeners
+ t.mx.RUnlock()
- for _, l := range t.listeners {
+ for _, l := range ll {
l.TableDataChanged(data)
}
}
func (t *Table) fireTableLoadFailed(err error) {
- for _, l := range t.listeners {
+ var ll []TableListener
+ t.mx.RLock()
+ ll = t.listeners
+ t.mx.RUnlock()
+
+ for _, l := range ll {
l.TableLoadFailed(err)
}
}
diff --git a/internal/ui/config.go b/internal/ui/config.go
index 27c5cf82ef..fb2191fff9 100644
--- a/internal/ui/config.go
+++ b/internal/ui/config.go
@@ -20,6 +20,7 @@ import (
// Synchronizer manages ui event queue.
type synchronizer interface {
Flash() *model.Flash
+ Logo() *Logo
UpdateClusterInfo()
QueueUpdateDraw(func())
QueueUpdate(func())
@@ -101,7 +102,7 @@ func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) erro
for {
select {
case evt := <-w.Events:
- if evt.Name == c.skinFile && evt.Op != fsnotify.Chmod {
+ if evt.Op != fsnotify.Chmod && filepath.Base(evt.Name) == filepath.Base(c.skinFile) {
log.Debug().Msgf("Skin changed: %s", c.skinFile)
s.QueueUpdateDraw(func() {
c.RefreshStyles(s)
@@ -141,11 +142,13 @@ func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error
if err := c.Config.Load(evt.Name); err != nil {
log.Error().Err(err).Msgf("k9s config reload failed")
s.Flash().Warn("k9s config reload failed. Check k9s logs!")
+ s.Logo().Warn("K9s config reload failed!")
}
} else {
if err := c.Config.K9s.Reload(); err != nil {
log.Error().Err(err).Msgf("k9s context config reload failed")
s.Flash().Warn("Context config reload failed. Check k9s logs!")
+ s.Logo().Warn("Context config reload failed!")
}
}
s.QueueUpdateDraw(func() {
@@ -252,10 +255,11 @@ func (c *Configurator) loadSkinFile(s synchronizer) {
if err := c.Styles.Load(skinFile); err != nil {
if errors.Is(err, os.ErrNotExist) {
s.Flash().Warnf("Skin file %q not found in skins dir: %s", filepath.Base(skinFile), config.AppSkinsDir)
+ c.updateStyles("")
} else {
s.Flash().Errf("Failed to parse skin file -- %s: %s.", filepath.Base(skinFile), err)
+ c.updateStyles(skinFile)
}
- c.updateStyles("")
} else {
s.Flash().Infof("Skin file loaded: %q", skinFile)
c.updateStyles(skinFile)
diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go
index 7117de9cc2..a38d26a1bd 100644
--- a/internal/ui/config_test.go
+++ b/internal/ui/config_test.go
@@ -72,6 +72,7 @@ func newMockSynchronizer() synchronizer {
func (s synchronizer) Flash() *model.Flash {
return model.NewFlash(100 * time.Millisecond)
}
+func (s synchronizer) Logo() *ui.Logo { return nil }
func (s synchronizer) UpdateClusterInfo() {}
func (s synchronizer) QueueUpdateDraw(func()) {}
func (s synchronizer) QueueUpdate(func()) {}
diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go
index 2e0b524bce..d9fb97a19d 100644
--- a/internal/ui/indicator.go
+++ b/internal/ui/indicator.go
@@ -56,8 +56,8 @@ func (s *StatusIndicator) ClusterInfoUpdated(data model.ClusterMeta) {
s.SetPermanent(fmt.Sprintf(
statusIndicatorFmt,
data.K9sVer,
+ data.Context,
data.Cluster,
- data.User,
data.K8sVer,
render.PrintPerc(data.Cpu),
render.PrintPerc(data.Mem),
diff --git a/internal/ui/logo.go b/internal/ui/logo.go
index eb8f8501ad..34724f46ab 100644
--- a/internal/ui/logo.go
+++ b/internal/ui/logo.go
@@ -52,7 +52,10 @@ func (l *Logo) Status() *tview.TextView {
// StylesChanged notifies the skin changed.
func (l *Logo) StylesChanged(s *config.Styles) {
l.styles = s
- l.Reset()
+ l.SetBackgroundColor(l.styles.BgColor())
+ l.status.SetBackgroundColor(l.styles.BgColor())
+ l.logo.SetBackgroundColor(l.styles.BgColor())
+ l.refreshLogo(l.styles.Body().LogoColor)
}
// IsBenchmarking checks if benchmarking is active or not.
@@ -64,10 +67,7 @@ func (l *Logo) IsBenchmarking() bool {
// Reset clears out the logo view and resets colors.
func (l *Logo) Reset() {
l.status.Clear()
- l.SetBackgroundColor(l.styles.BgColor())
- l.status.SetBackgroundColor(l.styles.BgColor())
- l.logo.SetBackgroundColor(l.styles.BgColor())
- l.refreshLogo(l.styles.Body().LogoColor)
+ l.StylesChanged(l.styles)
}
// Err displays a log error state.
diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go
index bf468c555c..45613a4830 100644
--- a/internal/ui/prompt.go
+++ b/internal/ui/prompt.go
@@ -5,6 +5,7 @@ package ui
import (
"fmt"
+ "sync"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
@@ -83,6 +84,7 @@ type Prompt struct {
styles *config.Styles
model PromptModel
spacer int
+ mx sync.RWMutex
}
// NewPrompt returns a new command view.
@@ -206,17 +208,29 @@ func (p *Prompt) activate() {
p.model.Notify(false)
}
-func (p *Prompt) update(text, suggestion string) {
- p.Clear()
- p.write(text, suggestion)
+func (p *Prompt) Clear() {
+ p.mx.Lock()
+ defer p.mx.Unlock()
+
+ p.TextView.Clear()
+}
+
+func (p *Prompt) Draw(sc tcell.Screen) {
+ p.mx.RLock()
+ defer p.mx.RUnlock()
+
+ p.TextView.Draw(sc)
}
-func (p *Prompt) suggest(text, suggestion string) {
+func (p *Prompt) update(text, suggestion string) {
p.Clear()
p.write(text, suggestion)
}
func (p *Prompt) write(text, suggest string) {
+ p.mx.Lock()
+ defer p.mx.Unlock()
+
p.SetCursorIndex(p.spacer + len(text))
txt := text
if suggest != "" {
@@ -240,7 +254,7 @@ func (p *Prompt) BufferChanged(text, suggestion string) {
// SuggestionChanged notifies the suggestion changed.
func (p *Prompt) SuggestionChanged(text, suggestion string) {
- p.suggest(text, suggestion)
+ p.update(text, suggestion)
}
// BufferActive indicates the buff activity changed.
diff --git a/internal/view/ns.go b/internal/view/ns.go
index ada763e93a..a9b9764f04 100644
--- a/internal/view/ns.go
+++ b/internal/view/ns.go
@@ -58,6 +58,9 @@ func (n *Namespace) useNsCmd(evt *tcell.EventKey) *tcell.EventKey {
func (n *Namespace) useNamespace(fqn string) {
_, ns := client.Namespaced(fqn)
+ if client.CleanseNamespace(n.App().Config.ActiveNamespace()) == ns {
+ return
+ }
if err := n.App().switchNS(ns); err != nil {
n.App().Flash().Err(err)
return
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index e9caf609e9..b0ffecdffa 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core20
-version: 'v0.31.5'
+version: 'v0.31.6'
summary: K9s is a CLI to view and manage your Kubernetes clusters.
description: |
K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.