diff --git a/Makefile b/Makefile
index 7dc2db8d..14a039da 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.30.5
+VERSION ?= v0.30.6
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}
diff --git a/change_logs/release_v0.30.6.md b/change_logs/release_v0.30.6.md
new file mode 100644
index 00000000..51050bd4
--- /dev/null
+++ b/change_logs/release_v0.30.6.md
@@ -0,0 +1,43 @@
+
+
+# Release v0.30.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! 🎄
+
+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
+
+* [#2401](https://github.com/derailed/k9s/issues/2401) Context completion broken with mixed case context names
+* [#2400](https://github.com/derailed/k9s/issues/2400) Panic on start if dns lookup fails
+* [#2387](https://github.com/derailed/k9s/issues/2387) Invalid namespace xxx - with feelings??
+
+---
+
+
© 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
diff --git a/cmd/root.go b/cmd/root.go
index 079ec37e..4f1f4847 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -93,7 +93,7 @@ func run(cmd *cobra.Command, args []string) error {
cfg, err := loadConfiguration()
if err != nil {
- return err
+ log.Error().Err(err).Msgf("load configuration failed")
}
app := view.NewApp(cfg)
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
@@ -120,12 +120,11 @@ func loadConfiguration() (*config.Config, error) {
k9sCfg.K9s.Override(k9sFlags)
if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {
log.Error().Err(err).Msgf("refine failed")
- return nil, err
}
conn, err := client.InitConnection(k8sCfg)
+ k9sCfg.SetConnection(conn)
if err != nil {
- log.Error().Err(err).Msgf("failed to connect to context %q", k9sCfg.K9s.ActiveContextName())
- return nil, err
+ return k9sCfg, err
}
// Try to access server version if that fail. Connectivity issue?
if !conn.CheckConnectivity() {
@@ -134,7 +133,6 @@ func loadConfiguration() (*config.Config, error) {
if !conn.ConnectionOK() {
return nil, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName())
}
- k9sCfg.SetConnection(conn)
log.Info().Msg("✅ Kubernetes connectivity")
if err := k9sCfg.Save(); err != nil {
diff --git a/internal/client/client.go b/internal/client/client.go
index 66bcf3fa..0cc6cf79 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -30,6 +30,7 @@ const (
cacheExpiry = 5 * time.Minute
cacheMXAPIKey = "metricsAPI"
serverVersion = "serverVersion"
+ cacheNSKey = "validNamespaces"
)
var supportedMetricsAPIVersions = []string{"v1beta1"}
@@ -213,29 +214,28 @@ func (a *APIClient) ServerVersion() (*version.Info, error) {
}
func (a *APIClient) IsValidNamespace(ns string) bool {
- if IsAllNamespace(ns) {
+ if IsClusterWide(ns) || ns == NotNamespaced {
return true
}
ok, err := a.CanI(ClusterScope, "v1/namespaces", []string{ListVerb})
- if !ok || err != nil {
- cool, err := a.isValidNamespace(ns)
- if err != nil {
- log.Error().Err(err).Msgf("unable to assert valid namespace")
- }
- return cool
+ if ok && err == nil {
+ nn, _ := a.ValidNamespaceNames()
+ _, ok = nn[ns]
+ return ok
}
- nn, err := a.ValidNamespaceNames()
- if err != nil {
- return false
- }
- _, ok = nn[ns]
- return ok
+ ok, err = a.isValidNamespace(ns)
+ if ok && err == nil {
+ return ok
+ }
+ log.Warn().Err(err).Msgf("namespace validation failed for: %q", ns)
+
+ return false
}
func (a *APIClient) cachedNamespaceNames() NamespaceNames {
- cns, ok := a.cache.Get("validNamespaces")
+ cns, ok := a.cache.Get(cacheNSKey)
if !ok {
return make(NamespaceNames)
}
@@ -244,6 +244,10 @@ func (a *APIClient) cachedNamespaceNames() NamespaceNames {
}
func (a *APIClient) isValidNamespace(n string) (bool, error) {
+ if IsClusterWide(n) || n == NotNamespaced {
+ return true, nil
+ }
+
if a == nil {
return false, errors.New("invalid client")
}
@@ -264,7 +268,7 @@ func (a *APIClient) isValidNamespace(n string) (bool, error) {
return false, err
}
cnss[n] = struct{}{}
- a.cache.Add("validNamespaces", cnss, cacheExpiry)
+ a.cache.Add(cacheNSKey, cnss, cacheExpiry)
return true, nil
}
@@ -275,7 +279,7 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
return nil, fmt.Errorf("validNamespaces: no available client found")
}
- if nn, ok := a.cache.Get("validNamespaces"); ok {
+ if nn, ok := a.cache.Get(cacheNSKey); ok {
if nss, ok := nn.(NamespaceNames); ok {
return nss, nil
}
@@ -294,7 +298,7 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
for _, n := range nn.Items {
nns[n.Name] = struct{}{}
}
- a.cache.Add("validNamespaces", nns, cacheExpiry)
+ a.cache.Add(cacheNSKey, nns, cacheExpiry)
return nns, nil
}
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
index e777b298..e5c17ddd 100644
--- a/internal/client/client_test.go
+++ b/internal/client/client_test.go
@@ -8,8 +8,145 @@ import (
"time"
"github.com/stretchr/testify/assert"
+ authorizationv1 "k8s.io/api/authorization/v1"
)
+func TestMakeSAR(t *testing.T) {
+ uu := map[string]struct {
+ ns string
+ gvr GVR
+ sar *authorizationv1.SelfSubjectAccessReview
+ }{
+ "all-pods": {
+ ns: NamespaceAll,
+ gvr: NewGVR("v1/pods"),
+ sar: &authorizationv1.SelfSubjectAccessReview{
+ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
+ ResourceAttributes: &authorizationv1.ResourceAttributes{
+ Namespace: NamespaceAll,
+ Version: "v1",
+ Resource: "pods",
+ },
+ },
+ },
+ },
+ "ns-pods": {
+ ns: "fred",
+ gvr: NewGVR("v1/pods"),
+ sar: &authorizationv1.SelfSubjectAccessReview{
+ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
+ ResourceAttributes: &authorizationv1.ResourceAttributes{
+ Namespace: "fred",
+ Version: "v1",
+ Resource: "pods",
+ },
+ },
+ },
+ },
+ "clusterscope-ns": {
+ ns: ClusterScope,
+ gvr: NewGVR("v1/namespaces"),
+ sar: &authorizationv1.SelfSubjectAccessReview{
+ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
+ ResourceAttributes: &authorizationv1.ResourceAttributes{
+ Version: "v1",
+ Resource: "namespaces",
+ },
+ },
+ },
+ },
+ "subres-pods": {
+ ns: "fred",
+ gvr: NewGVR("v1/pods:logs"),
+ sar: &authorizationv1.SelfSubjectAccessReview{
+ Spec: authorizationv1.SelfSubjectAccessReviewSpec{
+ ResourceAttributes: &authorizationv1.ResourceAttributes{
+ Namespace: "fred",
+ Version: "v1",
+ Resource: "pods",
+ Subresource: "logs",
+ },
+ },
+ },
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr.String()))
+ })
+ }
+}
+
+func TestIsValidNamespace(t *testing.T) {
+ c := NewTestAPIClient()
+
+ uu := map[string]struct {
+ ns string
+ cache NamespaceNames
+ ok bool
+ }{
+ "all-ns": {
+ ns: NamespaceAll,
+ cache: NamespaceNames{
+ DefaultNamespace: {},
+ },
+ ok: true,
+ },
+ "blank-ns": {
+ ns: BlankNamespace,
+ cache: NamespaceNames{
+ DefaultNamespace: {},
+ },
+ ok: true,
+ },
+ "cluster-ns": {
+ ns: ClusterScope,
+ cache: NamespaceNames{
+ DefaultNamespace: {},
+ },
+ ok: true,
+ },
+ "no-ns": {
+ ns: NotNamespaced,
+ cache: NamespaceNames{
+ DefaultNamespace: {},
+ },
+ ok: true,
+ },
+ "default-ns": {
+ ns: DefaultNamespace,
+ cache: NamespaceNames{
+ DefaultNamespace: {},
+ },
+ ok: true,
+ },
+ "valid-ns": {
+ ns: "fred",
+ cache: NamespaceNames{
+ "fred": {},
+ },
+ ok: true,
+ },
+ "invalid-ns": {
+ ns: "fred",
+ cache: NamespaceNames{
+ DefaultNamespace: {},
+ },
+ },
+ }
+
+ expiry := 1 * time.Millisecond
+ for k := range uu {
+ u := uu[k]
+ c.cache.Add("validNamespaces", u.cache, expiry)
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.ok, c.IsValidNamespace(u.ns))
+ })
+ }
+}
+
func TestCheckCacheBool(t *testing.T) {
c := NewTestAPIClient()
diff --git a/internal/client/config_test.go b/internal/client/config_test.go
index 046e5dd6..c92593af 100644
--- a/internal/client/config_test.go
+++ b/internal/client/config_test.go
@@ -7,6 +7,7 @@ import (
"errors"
"os"
"testing"
+ "time"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog"
@@ -18,6 +19,31 @@ func init() {
zerolog.SetGlobalLevel(zerolog.FatalLevel)
}
+func TestCallTimeout(t *testing.T) {
+ uu := map[string]struct {
+ t string
+ e time.Duration
+ }{
+ "custom": {
+ t: "1m",
+ e: 1 * time.Minute,
+ },
+ "default": {
+ e: 15 * time.Second,
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ flags := genericclioptions.NewConfigFlags(false)
+ flags.Timeout = &u.t
+ cfg := client.NewConfig(flags)
+ assert.Equal(t, u.e, cfg.CallTimeout())
+ })
+ }
+}
+
func TestConfigCurrentContext(t *testing.T) {
var kubeConfig = "./testdata/config"
diff --git a/internal/client/helper_test.go b/internal/client/helper_test.go
index 74e4988d..6103e5ea 100644
--- a/internal/client/helper_test.go
+++ b/internal/client/helper_test.go
@@ -8,8 +8,193 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/stretchr/testify/assert"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
+func TestMetaFQN(t *testing.T) {
+ uu := map[string]struct {
+ meta metav1.ObjectMeta
+ e string
+ }{
+ "empty": {
+ e: "-/",
+ },
+ "full": {
+ meta: metav1.ObjectMeta{Name: "blee", Namespace: "ns1"},
+ e: "ns1/blee",
+ },
+ "no-ns": {
+ meta: metav1.ObjectMeta{Name: "blee"},
+ e: "-/blee",
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, client.MetaFQN(u.meta))
+ })
+ }
+}
+
+func TestCoFQN(t *testing.T) {
+ uu := map[string]struct {
+ meta metav1.ObjectMeta
+ co string
+ e string
+ }{
+ "empty": {
+ e: "-/:",
+ },
+ "full": {
+ meta: metav1.ObjectMeta{Name: "blee", Namespace: "ns1"},
+ co: "fred",
+ e: "ns1/blee:fred",
+ },
+ "no-co": {
+ meta: metav1.ObjectMeta{Name: "blee", Namespace: "ns1"},
+ e: "ns1/blee:",
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, client.CoFQN(u.meta, u.co))
+ })
+ }
+}
+
+func TestIsClusterScoped(t *testing.T) {
+ uu := map[string]struct {
+ ns string
+ e bool
+ }{
+ "empty": {},
+ "all": {
+ ns: client.NamespaceAll,
+ },
+ "none": {
+ ns: client.BlankNamespace,
+ },
+ "custom": {
+ ns: "fred",
+ },
+ "scoped": {
+ ns: "-",
+ e: true,
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, client.IsClusterScoped(u.ns))
+ })
+ }
+}
+
+func TestIsNamespaced(t *testing.T) {
+ uu := map[string]struct {
+ ns string
+ e bool
+ }{
+ "empty": {},
+ "all": {
+ ns: client.NamespaceAll,
+ },
+ "none": {
+ ns: client.BlankNamespace,
+ },
+ "custom": {
+ ns: "fred",
+ e: true,
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, client.IsNamespaced(u.ns))
+ })
+ }
+}
+
+func TestIsAllNamespaces(t *testing.T) {
+ uu := map[string]struct {
+ ns string
+ e bool
+ }{
+ "empty": {
+ e: true,
+ },
+ "all": {
+ ns: client.NamespaceAll,
+ e: true,
+ },
+ "none": {
+ ns: client.BlankNamespace,
+ e: true,
+ },
+ "custom": {
+ ns: "fred",
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, client.IsAllNamespaces(u.ns))
+ })
+ }
+}
+
+func TestIsAllNamespace(t *testing.T) {
+ uu := map[string]struct {
+ ns string
+ e bool
+ }{
+ "empty": {},
+ "all": {
+ ns: client.NamespaceAll,
+ e: true,
+ },
+ "custom": {
+ ns: "fred",
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, client.IsAllNamespace(u.ns))
+ })
+ }
+}
+
+func TestCleanseNamespace(t *testing.T) {
+ uu := map[string]struct {
+ ns, e string
+ }{
+ "empty": {},
+ "all": {
+ ns: client.NamespaceAll,
+ e: client.BlankNamespace,
+ },
+ "custom": {
+ ns: "fred",
+ e: "fred",
+ },
+ }
+
+ for k := range uu {
+ u := uu[k]
+ t.Run(k, func(t *testing.T) {
+ assert.Equal(t, u.e, client.CleanseNamespace(u.ns))
+ })
+ }
+}
+
func TestNamespaced(t *testing.T) {
uu := []struct {
p, ns, n string
diff --git a/internal/config/data/ns.go b/internal/config/data/ns.go
index e9e9379c..2800a45d 100644
--- a/internal/config/data/ns.go
+++ b/internal/config/data/ns.go
@@ -40,18 +40,11 @@ func NewActiveNamespace(n string) *Namespace {
// Validate validates a namespace is setup correctly.
func (n *Namespace) Validate(c client.Connection, ks KubeSettings) {
- if n.Active == client.BlankNamespace || c == nil {
- n.Active = client.DefaultNamespace
- }
- if c == nil {
+ if c == nil || !c.IsValidNamespace(n.Active) {
return
}
- if !n.isAllNamespaces() && !c.IsValidNamespace(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 {
- if ns != client.NamespaceAll && !c.IsValidNamespace(ns) {
+ if !c.IsValidNamespace(ns) {
log.Debug().Msgf("[Namespace] Invalid favorite found '%s' - %t", ns, n.isAllNamespaces())
n.rmFavNS(ns)
}
diff --git a/internal/view/app.go b/internal/view/app.go
index ab9431e7..e6bfd1a1 100644
--- a/internal/view/app.go
+++ b/internal/view/app.go
@@ -184,9 +184,9 @@ func (a *App) suggestCommand() model.SuggestionFunc {
return a.cmdHistory.List()
}
- s = strings.ToLower(s)
+ ls := strings.ToLower(s)
for _, k := range a.command.alias.Aliases.Keys() {
- if suggest, ok := cmd.ShouldAddSuggest(s, k); ok {
+ if suggest, ok := cmd.ShouldAddSuggest(ls, k); ok {
entries = append(entries, suggest)
}
}
@@ -195,12 +195,10 @@ func (a *App) suggestCommand() model.SuggestionFunc {
if err != nil {
log.Error().Err(err).Msg("failed to list namespaces")
}
-
entries = append(entries, cmd.SuggestSubCommand(s, namespaceNames, contextNames)...)
if len(entries) == 0 {
return nil
}
-
entries.Sort()
return
}
@@ -214,11 +212,11 @@ func (a *App) contextNames() ([]string, error) {
if err != nil {
return nil, err
}
-
contextNames := make([]string, 0, len(contexts))
for ctxName := range contexts {
contextNames = append(contextNames, ctxName)
}
+
return contextNames, nil
}
diff --git a/internal/view/cmd/helpers.go b/internal/view/cmd/helpers.go
index 58b8335f..6ccbcfb7 100644
--- a/internal/view/cmd/helpers.go
+++ b/internal/view/cmd/helpers.go
@@ -94,6 +94,7 @@ func SuggestSubCommand(command string, namespaces client.NamespaceNames, context
}
func completeNS(s string, nn client.NamespaceNames) []string {
+ s = strings.ToLower(s)
var suggests []string
if suggest, ok := ShouldAddSuggest(s, client.NamespaceAll); ok {
suggests = append(suggests, suggest)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index c83d7863..9c915a8b 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core20
-version: 'v0.30.5'
+version: 'v0.30.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.