From 6a43167f1a30db50ed2acd4e3ffcfad25f657679 Mon Sep 17 00:00:00 2001 From: Fernand Galiana Date: Thu, 28 Dec 2023 14:16:59 -0700 Subject: [PATCH] K9s/release v0.30.6 (#2403) * fix #2401 * fix #2400 * fix #2387 * rel notes --- Makefile | 2 +- change_logs/release_v0.30.6.md | 43 ++++++++ cmd/root.go | 8 +- internal/client/client.go | 38 ++++--- internal/client/client_test.go | 137 ++++++++++++++++++++++++ internal/client/config_test.go | 26 +++++ internal/client/helper_test.go | 185 +++++++++++++++++++++++++++++++++ internal/config/data/ns.go | 11 +- internal/view/app.go | 8 +- internal/view/cmd/helpers.go | 1 + snap/snapcraft.yaml | 2 +- 11 files changed, 423 insertions(+), 38 deletions(-) create mode 100644 change_logs/release_v0.30.6.md 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.