K9s/release v0.30.6 (#2403)

* fix #2401

* fix #2400

* fix #2387

* rel notes
mine
Fernand Galiana 2023-12-28 14:16:59 -07:00 committed by GitHub
parent cbe13e8c63
commit 6a43167f1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 423 additions and 38 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.5 VERSION ?= v0.30.6
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,43 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png" align="center" width="800" height="auto"/>
# 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??
---
<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

@ -93,7 +93,7 @@ func run(cmd *cobra.Command, args []string) error {
cfg, err := loadConfiguration() cfg, err := loadConfiguration()
if err != nil { if err != nil {
return err log.Error().Err(err).Msgf("load configuration failed")
} }
app := view.NewApp(cfg) app := view.NewApp(cfg)
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil { if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
@ -120,12 +120,11 @@ func loadConfiguration() (*config.Config, error) {
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()) return k9sCfg, err
return nil, err
} }
// Try to access server version if that fail. Connectivity issue? // Try to access server version if that fail. Connectivity issue?
if !conn.CheckConnectivity() { if !conn.CheckConnectivity() {
@ -134,7 +133,6 @@ func loadConfiguration() (*config.Config, error) {
if !conn.ConnectionOK() { if !conn.ConnectionOK() {
return nil, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName()) 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 {

View File

@ -30,6 +30,7 @@ const (
cacheExpiry = 5 * time.Minute cacheExpiry = 5 * time.Minute
cacheMXAPIKey = "metricsAPI" cacheMXAPIKey = "metricsAPI"
serverVersion = "serverVersion" serverVersion = "serverVersion"
cacheNSKey = "validNamespaces"
) )
var supportedMetricsAPIVersions = []string{"v1beta1"} var supportedMetricsAPIVersions = []string{"v1beta1"}
@ -213,29 +214,28 @@ func (a *APIClient) ServerVersion() (*version.Info, error) {
} }
func (a *APIClient) IsValidNamespace(ns string) bool { func (a *APIClient) IsValidNamespace(ns string) bool {
if IsAllNamespace(ns) { if IsClusterWide(ns) || ns == NotNamespaced {
return true return true
} }
ok, err := a.CanI(ClusterScope, "v1/namespaces", []string{ListVerb}) ok, err := a.CanI(ClusterScope, "v1/namespaces", []string{ListVerb})
if !ok || err != nil { if ok && err == nil {
cool, err := a.isValidNamespace(ns) nn, _ := a.ValidNamespaceNames()
if err != nil {
log.Error().Err(err).Msgf("unable to assert valid namespace")
}
return cool
}
nn, err := a.ValidNamespaceNames()
if err != nil {
return false
}
_, ok = nn[ns] _, ok = nn[ns]
return ok 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 { func (a *APIClient) cachedNamespaceNames() NamespaceNames {
cns, ok := a.cache.Get("validNamespaces") cns, ok := a.cache.Get(cacheNSKey)
if !ok { if !ok {
return make(NamespaceNames) return make(NamespaceNames)
} }
@ -244,6 +244,10 @@ func (a *APIClient) cachedNamespaceNames() NamespaceNames {
} }
func (a *APIClient) isValidNamespace(n string) (bool, error) { func (a *APIClient) isValidNamespace(n string) (bool, error) {
if IsClusterWide(n) || n == NotNamespaced {
return true, nil
}
if a == nil { if a == nil {
return false, errors.New("invalid client") return false, errors.New("invalid client")
} }
@ -264,7 +268,7 @@ func (a *APIClient) isValidNamespace(n string) (bool, error) {
return false, err return false, err
} }
cnss[n] = struct{}{} cnss[n] = struct{}{}
a.cache.Add("validNamespaces", cnss, cacheExpiry) a.cache.Add(cacheNSKey, cnss, cacheExpiry)
return true, nil return true, nil
} }
@ -275,7 +279,7 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
return nil, fmt.Errorf("validNamespaces: no available client found") 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 { if nss, ok := nn.(NamespaceNames); ok {
return nss, nil return nss, nil
} }
@ -294,7 +298,7 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
for _, n := range nn.Items { for _, n := range nn.Items {
nns[n.Name] = struct{}{} nns[n.Name] = struct{}{}
} }
a.cache.Add("validNamespaces", nns, cacheExpiry) a.cache.Add(cacheNSKey, nns, cacheExpiry)
return nns, nil return nns, nil
} }

View File

@ -8,8 +8,145 @@ import (
"time" "time"
"github.com/stretchr/testify/assert" "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) { func TestCheckCacheBool(t *testing.T) {
c := NewTestAPIClient() c := NewTestAPIClient()

View File

@ -7,6 +7,7 @@ import (
"errors" "errors"
"os" "os"
"testing" "testing"
"time"
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -18,6 +19,31 @@ func init() {
zerolog.SetGlobalLevel(zerolog.FatalLevel) 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) { func TestConfigCurrentContext(t *testing.T) {
var kubeConfig = "./testdata/config" var kubeConfig = "./testdata/config"

View File

@ -8,8 +8,193 @@ import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/stretchr/testify/assert" "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) { func TestNamespaced(t *testing.T) {
uu := []struct { uu := []struct {
p, ns, n string p, ns, n string

View File

@ -40,18 +40,11 @@ func NewActiveNamespace(n string) *Namespace {
// Validate validates 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 n.Active == client.BlankNamespace || c == nil { if c == nil || !c.IsValidNamespace(n.Active) {
n.Active = client.DefaultNamespace
}
if c == nil {
return 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 { 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()) log.Debug().Msgf("[Namespace] Invalid favorite found '%s' - %t", ns, n.isAllNamespaces())
n.rmFavNS(ns) n.rmFavNS(ns)
} }

View File

@ -184,9 +184,9 @@ func (a *App) suggestCommand() model.SuggestionFunc {
return a.cmdHistory.List() return a.cmdHistory.List()
} }
s = strings.ToLower(s) ls := strings.ToLower(s)
for _, k := range a.command.alias.Aliases.Keys() { 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) entries = append(entries, suggest)
} }
} }
@ -195,12 +195,10 @@ func (a *App) suggestCommand() model.SuggestionFunc {
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to list namespaces") log.Error().Err(err).Msg("failed to list namespaces")
} }
entries = append(entries, cmd.SuggestSubCommand(s, namespaceNames, contextNames)...) entries = append(entries, cmd.SuggestSubCommand(s, namespaceNames, contextNames)...)
if len(entries) == 0 { if len(entries) == 0 {
return nil return nil
} }
entries.Sort() entries.Sort()
return return
} }
@ -214,11 +212,11 @@ func (a *App) contextNames() ([]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
contextNames := make([]string, 0, len(contexts)) contextNames := make([]string, 0, len(contexts))
for ctxName := range contexts { for ctxName := range contexts {
contextNames = append(contextNames, ctxName) contextNames = append(contextNames, ctxName)
} }
return contextNames, nil return contextNames, nil
} }

View File

@ -94,6 +94,7 @@ func SuggestSubCommand(command string, namespaces client.NamespaceNames, context
} }
func completeNS(s string, nn client.NamespaceNames) []string { func completeNS(s string, nn client.NamespaceNames) []string {
s = strings.ToLower(s)
var suggests []string var suggests []string
if suggest, ok := ShouldAddSuggest(s, client.NamespaceAll); ok { if suggest, ok := ShouldAddSuggest(s, client.NamespaceAll); ok {
suggests = append(suggests, suggest) suggests = append(suggests, suggest)

View File

@ -1,6 +1,6 @@
name: k9s name: k9s
base: core20 base: core20
version: 'v0.30.5' version: 'v0.30.6'
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.