K9s/release v0.30.5 (#2397)

* [Bug] Fix #2393

* [Maint] Cleaning up

* rel docs
mine
Fernand Galiana 2023-12-27 17:40:44 -07:00 committed by GitHub
parent f5f3278e17
commit 9e337a6be0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 225 additions and 110 deletions

View File

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

View File

@ -0,0 +1,52 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png" align="center" width="800" height="auto"/>
# Release v0.30.5
## 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! 🎄
Thank you all for pitching in and helping flesh out issues!!
---
## Videos Are In The Can!
Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
---
## Resolved Issues
* [#2394](https://github.com/derailed/k9s/issues/2394) Allow setting custom log dir
* [#2393](https://github.com/derailed/k9s/issues/2393) When switching contexts k9s does not switching to cluster's pod/namespaces/other k8s kinds view
* [#2387](https://github.com/derailed/k9s/issues/2387) Invalid namespace xxx - with feelings!
---
## 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!!
* [#2396](https://github.com/derailed/k9s/pull/2396) feat: allow to customize logs dir through environment variable
* [#2395](https://github.com/derailed/k9s/pull/2395) fix: create user tmp directory before the app one
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -91,7 +91,11 @@ func run(cmd *cobra.Command, args []string) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) log.Logger = log.Output(zerolog.ConsoleWriter{Out: file})
zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel)) zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel))
app := view.NewApp(loadConfiguration()) cfg, err := loadConfiguration()
if err != nil {
return err
}
app := view.NewApp(cfg)
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil { if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
return err return err
} }
@ -105,7 +109,7 @@ func run(cmd *cobra.Command, args []string) error {
return nil return nil
} }
func loadConfiguration() *config.Config { func loadConfiguration() (*config.Config, error) {
log.Info().Msg("🐶 K9s starting up...") log.Info().Msg("🐶 K9s starting up...")
k8sCfg := client.NewConfig(k8sFlags) k8sCfg := client.NewConfig(k8sFlags)
@ -116,26 +120,29 @@ func loadConfiguration() *config.Config {
k9sCfg.K9s.Override(k9sFlags) k9sCfg.K9s.Override(k9sFlags)
if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil { if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {
log.Error().Err(err).Msgf("refine failed") log.Error().Err(err).Msgf("refine failed")
return nil, err
} }
conn, err := client.InitConnection(k8sCfg) conn, err := client.InitConnection(k8sCfg)
k9sCfg.SetConnection(conn)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("failed to connect to context %q", k9sCfg.K9s.ActiveContextName()) log.Error().Err(err).Msgf("failed to connect to context %q", k9sCfg.K9s.ActiveContextName())
return k9sCfg return nil, err
} }
// Try to access server version if that fail. Connectivity issue? // Try to access server version if that fail. Connectivity issue?
if !k9sCfg.GetConnection().CheckConnectivity() { if !conn.CheckConnectivity() {
log.Panic().Msgf("Cannot connect to context %s", k9sCfg.K9s.ActiveContextName()) return nil, fmt.Errorf("cannot connect to context: %s", k9sCfg.K9s.ActiveContextName())
} }
if !k9sCfg.GetConnection().ConnectionOK() { if !conn.ConnectionOK() {
panic("No connectivity") return nil, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName())
} }
k9sCfg.SetConnection(conn)
log.Info().Msg("✅ Kubernetes connectivity") log.Info().Msg("✅ Kubernetes connectivity")
if err := k9sCfg.Save(); err != nil { if err := k9sCfg.Save(); err != nil {
log.Error().Err(err).Msg("Config save") log.Error().Err(err).Msg("Config save")
return nil, err
} }
return k9sCfg return k9sCfg, nil
} }
func parseLevel(level string) zerolog.Level { func parseLevel(level string) zerolog.Level {

View File

@ -26,8 +26,8 @@ type Config struct {
// K9sHome returns k9s configs home directory. // K9sHome returns k9s configs home directory.
func K9sHome() string { func K9sHome() string {
if env := os.Getenv(K9sConfigDir); env != "" { if isEnvSet(K9sEnvConfigDir) {
return env return os.Getenv(K9sEnvConfigDir)
} }
xdgK9sHome, err := xdg.ConfigFile(AppName) xdgK9sHome, err := xdg.ConfigFile(AppName)
@ -84,7 +84,7 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
} }
log.Debug().Msgf("Active Context %q", c.K9s.ActiveContextName()) log.Debug().Msgf("Active Context %q", c.K9s.ActiveContextName())
var ns = client.DefaultNamespace var ns string
switch { switch {
case k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces): case k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces):
ns = client.NamespaceAll ns = client.NamespaceAll
@ -97,10 +97,12 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, c
} }
ns = nss ns = nss
} }
if ns == "" {
ns = client.DefaultNamespace
}
if err := c.SetActiveNamespace(ns); err != nil { if err := c.SetActiveNamespace(ns); err != nil {
return err return err
} }
flags.Namespace = &ns
return data.EnsureDirPath(c.K9s.GetScreenDumpDir(), data.DefaultDirMod) return data.EnsureDirPath(c.K9s.GetScreenDumpDir(), data.DefaultDirMod)
} }
@ -139,10 +141,10 @@ func (c *Config) ActiveNamespace() string {
// ValidateFavorites ensure favorite ns are legit. // ValidateFavorites ensure favorite ns are legit.
func (c *Config) ValidateFavorites() { func (c *Config) ValidateFavorites() {
ct, err := c.K9s.ActiveContext() ct, err := c.K9s.ActiveContext()
if err == nil { if err != nil {
ct.Validate(c.conn, c.settings) return
ct.Namespace.Validate(c.conn, c.settings)
} }
ct.Validate(c.conn, c.settings)
} }
// FavNamespaces returns fav namespaces in the current context. // FavNamespaces returns fav namespaces in the current context.
@ -197,8 +199,10 @@ func (c *Config) GetConnection() client.Connection {
// SetConnection set an api server connection. // SetConnection set an api server connection.
func (c *Config) SetConnection(conn client.Connection) { func (c *Config) SetConnection(conn client.Connection) {
c.conn, c.K9s.conn = conn, conn c.conn = conn
c.Validate() if conn != nil {
c.K9s.resetConnection(conn)
}
} }
func (c *Config) ActiveContextName() string { func (c *Config) ActiveContextName() string {

View File

@ -47,7 +47,6 @@ func (c *Context) Validate(conn client.Connection, ks KubeSettings) {
if c.PortForwardAddress == "" { if c.PortForwardAddress == "" {
c.PortForwardAddress = DefaultPFAddress c.PortForwardAddress = DefaultPFAddress
} }
if cl, err := ks.CurrentClusterName(); err != nil { if cl, err := ks.CurrentClusterName(); err != nil {
c.ClusterName = cl c.ClusterName = cl
} }
@ -55,9 +54,6 @@ func (c *Context) Validate(conn client.Connection, ks KubeSettings) {
if c.Namespace == nil { if c.Namespace == nil {
c.Namespace = NewNamespace() c.Namespace = NewNamespace()
} }
if c.Namespace.Active == client.BlankNamespace {
c.Namespace.Active = client.DefaultNamespace
}
c.Namespace.Validate(conn, ks) c.Namespace.Validate(conn, ks)
if c.View == nil { if c.View == nil {

View File

@ -8,31 +8,28 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/tools/clientcmd/api"
) )
// Dir tracks context configurations.
type Dir struct { type Dir struct {
root string root string
conn client.Connection
ks KubeSettings
} }
func NewDir(root string, conn client.Connection, ks KubeSettings) *Dir { // NewDir returns a new instance.
func NewDir(root string) *Dir {
return &Dir{ return &Dir{
root: root, root: root,
ks: ks,
conn: conn,
} }
} }
func (d Dir) Load(n string, ct *api.Context) (*Config, error) { // Load loads context configuration.
func (d *Dir) Load(n string, ct *api.Context) (*Config, error) {
if ct == nil { if ct == nil {
return nil, errors.New("api.Context must not be nil") return nil, errors.New("api.Context must not be nil")
} }
var ( var (
path = filepath.Join(d.root, SanitizeContextSubpath(ct.Cluster, n), MainConfigFile) path = filepath.Join(d.root, SanitizeContextSubpath(ct.Cluster, n), MainConfigFile)
cfg *Config cfg *Config
@ -51,7 +48,6 @@ func (d Dir) Load(n string, ct *api.Context) (*Config, error) {
func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) { func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) {
cfg := NewConfig(ct) cfg := NewConfig(ct)
cfg.Validate(d.conn, d.ks)
if err := cfg.Save(path); err != nil { if err := cfg.Save(path); err != nil {
return nil, err return nil, err
} }
@ -68,7 +64,6 @@ func (d *Dir) loadConfig(path string) (*Config, error) {
if err := yaml.Unmarshal(bb, &cfg); err != nil { if err := yaml.Unmarshal(bb, &cfg); err != nil {
return nil, err return nil, err
} }
cfg.Validate(d.conn, d.ks)
return &cfg, nil return &cfg, nil
} }

View File

@ -71,7 +71,7 @@ func TestDirLoad(t *testing.T) {
assert.NoError(t, mock.EnsureDir(u.dir)) assert.NoError(t, mock.EnsureDir(u.dir))
} }
d := data.NewDir(u.dir, mock.NewMockConnection(), ks) d := data.NewDir(u.dir)
ct, err := ks.CurrentContext() ct, err := ks.CurrentContext()
assert.NoError(t, err) assert.NoError(t, err)
if err != nil { if err != nil {

View File

@ -29,28 +29,30 @@ func NewNamespace() *Namespace {
} }
func NewActiveNamespace(n string) *Namespace { func NewActiveNamespace(n string) *Namespace {
if n == client.BlankNamespace {
n = client.DefaultNamespace
}
return &Namespace{ return &Namespace{
Active: n, Active: n,
Favorites: []string{client.DefaultNamespace}, Favorites: []string{client.DefaultNamespace},
} }
} }
// Validate a namespace is setup correctly. // Validate validates a namespace is setup correctly.
func (n *Namespace) Validate(c client.Connection, ks KubeSettings) { func (n *Namespace) Validate(c client.Connection, ks KubeSettings) {
if c == nil { if n.Active == client.BlankNamespace || c == nil {
n = NewActiveNamespace(client.DefaultNamespace) n.Active = client.DefaultNamespace
} }
if c == nil { if c == nil {
log.Debug().Msgf("No connection found. Skipping ns validation")
return return
} }
if !n.isAllNamespaces() && !c.IsValidNamespace(n.Active) { if !n.isAllNamespaces() && !c.IsValidNamespace(n.Active) {
log.Error().Msgf("[Config] Validation error active namespace %q does not exists", n.Active) log.Error().Msgf("[Config] Validation failed active namespace %q does not exists. Resetting to default ns", n.Active)
n.Active = client.DefaultNamespace
} }
for _, ns := range n.Favorites { for _, ns := range n.Favorites {
if ns != client.NamespaceAll && !c.IsValidNamespace(ns) { if ns != client.NamespaceAll && !c.IsValidNamespace(ns) {
log.Debug().Msgf("[Config] Invalid favorite found '%s' - %t", ns, n.isAllNamespaces()) log.Debug().Msgf("[Namespace] Invalid favorite found '%s' - %t", ns, n.isAllNamespaces())
n.rmFavNS(ns) n.rmFavNS(ns)
} }
} }

View File

@ -6,7 +6,6 @@ package config
import ( import (
_ "embed" _ "embed"
"os" "os"
"os/user"
"path/filepath" "path/filepath"
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
@ -16,11 +15,11 @@ import (
) )
const ( const (
// K9sConfigDir represents k9s configuration dir env var. // K9sEnvConfigDir represents k9s configuration dir env var.
K9sConfigDir = "K9S_CONFIG_DIR" K9sEnvConfigDir = "K9S_CONFIG_DIR"
// K9sLogsDir represents k9s logs dir env var. // K9sEnvLogsDir represents k9s logs dir env var.
K9sLogsDir = "K9S_LOGS_DIR" K9sEnvLogsDir = "K9S_LOGS_DIR"
// AppName tracks k9s app name. // AppName tracks k9s app name.
AppName = "k9s" AppName = "k9s"
@ -84,14 +83,21 @@ var (
// InitLogsLoc initializes K9s logs location. // InitLogsLoc initializes K9s logs location.
func InitLogLoc() error { func InitLogLoc() error {
var appLogDir string var appLogDir string
if envDir := os.Getenv(K9sLogsDir); envDir != "" { switch {
appLogDir = envDir case isEnvSet(K9sEnvLogsDir):
} else { appLogDir = os.Getenv(K9sEnvLogsDir)
tmpDir, err := userTmpDir() case isEnvSet(K9sEnvConfigDir):
tmpDir, err := UserTmpDir()
if err != nil { if err != nil {
return err return err
} }
appLogDir = tmpDir appLogDir = tmpDir
default:
var err error
appLogDir, err = xdg.StateFile(AppName)
if err != nil {
return err
}
} }
if err := data.EnsureFullPath(appLogDir, data.DefaultDirMod); err != nil { if err := data.EnsureFullPath(appLogDir, data.DefaultDirMod); err != nil {
return err return err
@ -103,7 +109,7 @@ func InitLogLoc() error {
// InitLocs initializes k9s artifacts locations. // InitLocs initializes k9s artifacts locations.
func InitLocs() error { func InitLocs() error {
if hasK9sConfigEnv() { if isEnvSet(K9sEnvConfigDir) {
return initK9sEnvLocs() return initK9sEnvLocs()
} }
@ -111,7 +117,7 @@ func InitLocs() error {
} }
func initK9sEnvLocs() error { func initK9sEnvLocs() error {
AppConfigDir = os.Getenv(K9sConfigDir) AppConfigDir = os.Getenv(K9sEnvConfigDir)
if err := data.EnsureFullPath(AppConfigDir, data.DefaultDirMod); err != nil { if err := data.EnsureFullPath(AppConfigDir, data.DefaultDirMod); err != nil {
return err return err
} }
@ -269,20 +275,3 @@ func EnsureHotkeysCfgFile() (string, error) {
func SkinFileFromName(n string) string { func SkinFileFromName(n string) string {
return filepath.Join(AppSkinsDir, n+".yaml") return filepath.Join(AppSkinsDir, n+".yaml")
} }
// Helpers...
func hasK9sConfigEnv() bool {
return os.Getenv(K9sConfigDir) != ""
}
func userTmpDir() (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
dir := filepath.Join(os.TempDir(), u.Username, AppName)
return dir, nil
}

View File

@ -5,17 +5,63 @@ package config_test
import ( import (
"os" "os"
"path/filepath"
"testing" "testing"
"github.com/adrg/xdg"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestInitLogLoc(t *testing.T) {
tmp, err := config.UserTmpDir()
assert.NoError(t, err)
uu := map[string]struct {
dir string
e string
}{
"log-env": {
dir: "/tmp/test/k9s/logs",
e: "/tmp/test/k9s/logs/k9s.log",
},
"xdg-env": {
dir: "/tmp/test/xdg-state",
e: "/tmp/test/xdg-state/k9s/k9s.log",
},
"cfg-env": {
dir: "/tmp/test/k9s-test",
e: filepath.Join(tmp, "k9s.log"),
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
os.Unsetenv(config.K9sEnvLogsDir)
os.Unsetenv("XDG_STATE_HOME")
os.Unsetenv(config.K9sEnvConfigDir)
switch k {
case "log-env":
os.Setenv(config.K9sEnvLogsDir, u.dir)
case "xdg-env":
os.Setenv("XDG_STATE_HOME", u.dir)
xdg.Reload()
case "cfg-env":
os.Setenv(config.K9sEnvConfigDir, u.dir)
}
err := config.InitLogLoc()
assert.NoError(t, err)
assert.Equal(t, u.e, config.AppLogFile)
assert.NoError(t, os.RemoveAll(config.AppLogFile))
})
}
}
func TestEnsureBenchmarkCfg(t *testing.T) { func TestEnsureBenchmarkCfg(t *testing.T) {
os.Setenv(config.K9sConfigDir, "/tmp/test-config") os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")
assert.NoError(t, config.InitLocs()) assert.NoError(t, config.InitLocs())
defer assert.NoError(t, os.RemoveAll(config.K9sConfigDir)) defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir))
assert.NoError(t, data.EnsureFullPath("/tmp/test-config/clusters/cl-1/ct-2", data.DefaultDirMod)) assert.NoError(t, data.EnsureFullPath("/tmp/test-config/clusters/cl-1/ct-2", data.DefaultDirMod))
assert.NoError(t, os.WriteFile("/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", []byte{}, data.DefaultFileMod)) assert.NoError(t, os.WriteFile("/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", []byte{}, data.DefaultFileMod))

View File

@ -14,9 +14,6 @@ const (
DefaultCommand = "" DefaultCommand = ""
) )
// DefaultLogFile represents the default K9s log file.
// var DefaultLogFile = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser()))
// Flags represents K9s configuration flags. // Flags represents K9s configuration flags.
type Flags struct { type Flags struct {
RefreshRate *int RefreshRate *int

View File

@ -4,13 +4,32 @@
package config package config
import ( import (
"os"
"os/user" "os/user"
"path/filepath"
"github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/config/data"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
) )
// isEnvSet checks if env var is set.
func isEnvSet(env string) bool {
return os.Getenv(env) != ""
}
// UserTmpDir returns the temp dir with the current user name.
func UserTmpDir() (string, error) {
u, err := user.Current()
if err != nil {
return "", err
}
dir := filepath.Join(os.TempDir(), u.Username, AppName)
return dir, nil
}
// InNSList check if ns is in an ns collection. // InNSList check if ns is in an ns collection.
func InNSList(nn []interface{}, ns string) bool { func InNSList(nn []interface{}, ns string) bool {
ss := make([]string, len(nn)) ss := make([]string, len(nn))

View File

@ -50,12 +50,16 @@ func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s {
Thresholds: NewThreshold(), Thresholds: NewThreshold(),
ShellPod: NewShellPod(), ShellPod: NewShellPod(),
ImageScans: NewImageScans(), ImageScans: NewImageScans(),
dir: data.NewDir(AppContextsDir, conn, ks), dir: data.NewDir(AppContextsDir),
conn: conn, conn: conn,
ks: ks, ks: ks,
} }
} }
func (k *K9s) resetConnection(conn client.Connection) {
k.conn = conn
}
// Save saves the k9s config to dis. // Save saves the k9s config to dis.
func (k *K9s) Save() error { func (k *K9s) Save() error {
if k.activeConfig != nil { if k.activeConfig != nil {
@ -177,19 +181,20 @@ func (k *K9s) ActivateContext(n string) (*data.Context, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg, err := k.dir.Load(n, ct) k.activeConfig, err = k.dir.Load(n, ct)
if err != nil { if err != nil {
return nil, err return nil, err
} }
k.activeConfig = cfg
// If the context specifies a default namespace, use it! // If the context specifies a default namespace, use it!
if k.conn != nil { if k.conn != nil {
if ns := k.conn.ActiveNamespace(); ns != client.BlankNamespace { if ns := k.conn.ActiveNamespace(); ns != client.BlankNamespace {
k.activeConfig.Context.Namespace.Active = ns k.activeConfig.Context.Namespace.Active = ns
} else {
k.activeConfig.Context.Namespace.Active = client.DefaultNamespace
} }
} }
return cfg.Context, nil return k.activeConfig.Context, nil
} }
// OverrideRefreshRate set the refresh rate manually. // OverrideRefreshRate set the refresh rate manually.
@ -319,4 +324,8 @@ func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) {
k.Thresholds = NewThreshold() k.Thresholds = NewThreshold()
} }
k.Thresholds.Validate(c, ks) k.Thresholds.Validate(c, ks)
if k.activeConfig != nil {
k.activeConfig.Validate(c, ks)
}
} }

View File

@ -116,10 +116,6 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error
} }
}() }()
log.Debug().Msgf("SkinWatcher watching %q", config.K9sHome())
if err := w.Add(config.K9sHome()); err != nil {
return err
}
log.Debug().Msgf("SkinWatcher watching %q", config.AppSkinsDir) log.Debug().Msgf("SkinWatcher watching %q", config.AppSkinsDir)
return w.Add(config.AppSkinsDir) return w.Add(config.AppSkinsDir)
} }

View File

@ -19,9 +19,9 @@ import (
) )
func TestBenchConfig(t *testing.T) { func TestBenchConfig(t *testing.T) {
os.Setenv(config.K9sConfigDir, "/tmp/test-config") os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")
assert.NoError(t, config.InitLocs()) assert.NoError(t, config.InitLocs())
defer assert.NoError(t, os.RemoveAll(config.K9sConfigDir)) defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir))
bc, error := config.EnsureBenchmarksCfgFile("cl-1", "ct-1") bc, error := config.EnsureBenchmarksCfgFile("cl-1", "ct-1")
assert.NoError(t, error) assert.NoError(t, error)
@ -29,9 +29,9 @@ func TestBenchConfig(t *testing.T) {
} }
func TestSkinnedContext(t *testing.T) { func TestSkinnedContext(t *testing.T) {
os.Setenv(config.K9sConfigDir, "/tmp/test-config") os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config")
assert.NoError(t, config.InitLocs()) assert.NoError(t, config.InitLocs())
defer assert.NoError(t, os.RemoveAll(config.K9sConfigDir)) defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir))
sf := filepath.Join("..", "config", "testdata", "black_and_wtf.yaml") sf := filepath.Join("..", "config", "testdata", "black_and_wtf.yaml")
raw, err := os.ReadFile(sf) raw, err := os.ReadFile(sf)

View File

@ -407,6 +407,10 @@ func (a *App) refreshCluster(context.Context) error {
} }
func (a *App) switchNS(ns string) error { func (a *App) switchNS(ns string) error {
if a.Config.ActiveNamespace() == ns {
return nil
}
if ns == client.ClusterScope { if ns == client.ClusterScope {
ns = client.BlankNamespace ns = client.BlankNamespace
} }
@ -433,7 +437,7 @@ func (a *App) isValidNS(ns string) (bool, error) {
} }
if !a.Conn().IsValidNamespace(ns) { if !a.Conn().IsValidNamespace(ns) {
return false, fmt.Errorf("isvalidns - invalid namespace: %q", ns) return false, fmt.Errorf("invalid namespace: %q", ns)
} }
return true, nil return true, nil
@ -445,16 +449,9 @@ func (a *App) switchContext(ci *cmd.Interpreter) error {
return nil return nil
} }
log.Debug().Msgf("--> Switching Context %q--%q", name, a.Config.ActiveView())
a.Halt() a.Halt()
defer a.Resume() defer a.Resume()
{ {
p := cmd.NewInterpreter(a.Config.ActiveView())
if p.IsContextCmd() {
a.Config.SetActiveView("pod")
}
p.ResetContextArg()
a.Config.Reset() a.Config.Reset()
ct, err := a.Config.K9s.ActivateContext(name) ct, err := a.Config.K9s.ActivateContext(name)
if err != nil { if err != nil {
@ -466,14 +463,26 @@ func (a *App) switchContext(ci *cmd.Interpreter) error {
if cns, ok := ci.NSArg(); ok { if cns, ok := ci.NSArg(); ok {
ct.Namespace.Active = cns ct.Namespace.Active = cns
} }
p := cmd.NewInterpreter(a.Config.ActiveView())
p.ResetContextArg()
if p.IsContextCmd() {
a.Config.SetActiveView("pod")
}
ns := a.Config.ActiveNamespace()
if !a.Conn().IsValidNamespace(ns) {
ns = client.DefaultNamespace
if err := a.Config.SetActiveNamespace(ns); err != nil {
return err
}
}
if err := a.Config.Save(); err != nil { if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("config save failed!") log.Error().Err(err).Msg("config save failed!")
} }
ns := a.Config.ActiveNamespace()
a.initFactory(ns) a.initFactory(ns)
a.Flash().Infof("Switching context to %s", name) log.Debug().Msgf("--> Switching Context %q -- %q -- %q", name, ns, a.Config.ActiveView())
a.Flash().Infof("Switching context to %q::%q", name, ns)
a.ReloadStyles(name) a.ReloadStyles(name)
a.gotoResource(a.Config.ActiveView(), "", true) a.gotoResource(a.Config.ActiveView(), "", true)
a.clusterModel.Reset(a.factory) a.clusterModel.Reset(a.factory)

View File

@ -133,7 +133,6 @@ func (b *Browser) SetInstance(path string) {
// Start initializes browser updates. // Start initializes browser updates.
func (b *Browser) Start() { func (b *Browser) Start() {
b.app.Config.ValidateFavorites()
ns := b.app.Config.ActiveNamespace() ns := b.app.Config.ActiveNamespace()
if n := b.GetModel().GetNamespace(); !client.IsClusterScoped(n) { if n := b.GetModel().GetNamespace(); !client.IsClusterScoped(n) {
ns = n ns = n

View File

@ -131,10 +131,6 @@ func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool) error {
if c.specialCmd(p) { if c.specialCmd(p) {
return nil return nil
} }
// if _, ok := c.alias.Check(p.Cmd()); !ok {
// return fmt.Errorf("command not found %q", p.Cmd())
// }
gvr, v, err := c.viewMetaFor(p) gvr, v, err := c.viewMetaFor(p)
if err != nil { if err != nil {
return err return err

View File

@ -16,6 +16,7 @@ import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/color"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui"
@ -420,19 +421,17 @@ func ensureDir(dir string) error {
return os.MkdirAll(dir, 0744) return os.MkdirAll(dir, 0744)
} }
func saveData(dir, fqn, data string) (string, error) { func saveData(dir, fqn, logs string) (string, error) {
if err := ensureDir(dir); err != nil { if err := ensureDir(dir); err != nil {
return "", err return "", err
} }
now := time.Now().UnixNano() f := fmt.Sprintf("%s-%d.log", fqn, time.Now().UnixNano())
fName := fmt.Sprintf("%s-%d.log", strings.Replace(fqn, "/", "-", 1), now) path := filepath.Join(dir, data.SanitizeFileName(f))
path := filepath.Join(dir, fName)
mod := os.O_CREATE | os.O_WRONLY mod := os.O_CREATE | os.O_WRONLY
file, err := os.OpenFile(path, mod, 0600) file, err := os.OpenFile(path, mod, 0600)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("LogFile create %s", path) log.Error().Err(err).Msgf("Log file save failed: %q", path)
return "", nil return "", nil
} }
defer func() { defer func() {
@ -440,7 +439,7 @@ func saveData(dir, fqn, data string) (string, error) {
log.Error().Err(err).Msg("Closing Log file") log.Error().Err(err).Msg("Closing Log file")
} }
}() }()
if _, err := file.Write([]byte(data)); err != nil { if _, err := file.WriteString(logs); err != nil {
return "", err return "", err
} }

View File

@ -1,6 +1,6 @@
name: k9s name: k9s
base: core20 base: core20
version: 'v0.30.4' version: 'v0.30.5'
summary: K9s is a CLI to view and manage your Kubernetes clusters. summary: K9s is a CLI to view and manage your Kubernetes clusters.
description: | 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. 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.