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
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}

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()
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 {

View File

@ -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
}
nn, err := a.ValidNamespaceNames()
if err != nil {
return false
}
if ok && err == nil {
nn, _ := a.ValidNamespaceNames()
_, 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
}

View File

@ -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()

View File

@ -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"

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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.