diff --git a/Makefile b/Makefile
index bf7861e6..9a0824fa 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.32.1
+VERSION ?= v0.32.2
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}
diff --git a/change_logs/release_v0.32.2.md b/change_logs/release_v0.32.2.md
new file mode 100644
index 00000000..96affe59
--- /dev/null
+++ b/change_logs/release_v0.32.2.md
@@ -0,0 +1,43 @@
+
+
+# Release v0.32.2
+
+## 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!
+
+Mo aftermath ;(
+
+---
+
+## Videos Are In The Can!
+
+Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
+
+* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)
+* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
+* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
+
+---
+
+## Resolved Issues
+
+* [#2582](https://github.com/derailed/k9s/issues/2582) Slowness due to client-side throttling in v0.32.0 (Maybe??)
+* [#2593](https://github.com/derailed/k9s/issues/2593) Popeye not working in 0.32.X
+
+---
+
+
© 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
\ No newline at end of file
diff --git a/internal/client/client.go b/internal/client/client.go
index e12f9005..aaac43e8 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -246,7 +246,7 @@ func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {
}
}
- ok, err := a.CanI(ClusterScope, "v1/namespaces", "", []string{ListVerb})
+ ok, err := a.CanI(ClusterScope, "v1/namespaces", "", ListAccess)
if !ok || err != nil {
return nil, fmt.Errorf("user not authorized to list all namespaces")
}
@@ -524,12 +524,25 @@ func (a *APIClient) MXDial() (*versioned.Clientset, error) {
return a.getMxsClient(), err
}
+func (a *APIClient) invalidateCache() error {
+ dial, err := a.CachedDiscovery()
+ if err != nil {
+ return err
+ }
+ dial.Invalidate()
+
+ return nil
+}
+
// SwitchContext handles kubeconfig context switches.
func (a *APIClient) SwitchContext(name string) error {
log.Debug().Msgf("Switching context %q", name)
if err := a.config.SwitchContext(name); err != nil {
return err
}
+ if err := a.invalidateCache(); err != nil {
+ return err
+ }
a.reset()
ResetMetrics()
diff --git a/internal/client/types.go b/internal/client/types.go
index 8dad38a0..b5a7d3cf 100644
--- a/internal/client/types.go
+++ b/internal/client/types.go
@@ -55,6 +55,9 @@ const (
)
var (
+ // PatchAccess patch a resource.
+ PatchAccess = []string{PatchVerb}
+
// GetAccess reads a resource.
GetAccess = []string{GetVerb}
diff --git a/internal/config/alias.go b/internal/config/alias.go
index be53f402..426d41d7 100644
--- a/internal/config/alias.go
+++ b/internal/config/alias.go
@@ -177,7 +177,8 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("help", "h", "?")
a.declare("quit", "q", "q!", "qa", "Q")
a.declare("aliases", "alias", "a")
- a.declare("popeye", "pop")
+ // !!BOZO!!
+ // a.declare("popeye", "pop")
a.declare("helm", "charts", "chart", "hm")
a.declare("dir", "d")
a.declare("contexts", "context", "ctx")
diff --git a/internal/config/alias_test.go b/internal/config/alias_test.go
index d8551bfc..c67f4f58 100644
--- a/internal/config/alias_test.go
+++ b/internal/config/alias_test.go
@@ -6,6 +6,7 @@ package config_test
import (
"fmt"
"os"
+ "path"
"slices"
"testing"
@@ -109,8 +110,8 @@ func TestAliasesLoad(t *testing.T) {
config.AppConfigDir = "testdata/aliases"
a := config.NewAliases()
- assert.Nil(t, a.Load("testdata/aliases/plain.yaml"))
- assert.Equal(t, 56, len(a.Alias))
+ assert.Nil(t, a.Load(path.Join(config.AppConfigDir, "plain.yaml")))
+ assert.Equal(t, 54, len(a.Alias))
}
func TestAliasesSave(t *testing.T) {
@@ -123,7 +124,7 @@ func TestAliasesSave(t *testing.T) {
assert.Equal(t, c, len(a.Alias))
assert.Nil(t, a.Save())
- assert.Nil(t, a.LoadFile("/tmp/test-aliases/aliases.yaml"))
+ assert.Nil(t, a.LoadFile(config.AppAliasesFile))
assert.Equal(t, c, len(a.Alias))
}
diff --git a/internal/config/testdata/aliases/plain.yaml b/internal/config/testdata/aliases/plain.yaml
index 185113e7..4291a3c7 100644
--- a/internal/config/testdata/aliases/plain.yaml
+++ b/internal/config/testdata/aliases/plain.yaml
@@ -1,3 +1,3 @@
aliases:
- dp: "apps.v1.deployments"
- pe: ".v1.pods"
+ dp: "apps/v1/deployments"
+ pe: "v1/pods"
diff --git a/internal/dao/dp.go b/internal/dao/dp.go
index f3273399..5df34321 100644
--- a/internal/dao/dp.go
+++ b/internal/dao/dp.go
@@ -86,7 +86,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
return err
}
- auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", dp.Name, []string{client.PatchVerb})
+ auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", dp.Name, client.PatchAccess)
if err != nil {
return err
}
@@ -261,7 +261,7 @@ func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images.
func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path)
- auth, err := d.Client().CanI(ns, "apps/v1/deployments", n, []string{client.PatchVerb})
+ auth, err := d.Client().CanI(ns, "apps/v1/deployments", n, client.PatchAccess)
if err != nil {
return err
}
diff --git a/internal/dao/ds.go b/internal/dao/ds.go
index b0bf8c0b..3bbd7827 100644
--- a/internal/dao/ds.go
+++ b/internal/dao/ds.go
@@ -63,7 +63,7 @@ func (d *DaemonSet) Restart(ctx context.Context, path string) error {
return err
}
- auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", ds.Name, []string{client.PatchVerb})
+ auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", ds.Name, client.PatchAccess)
if err != nil {
return err
}
@@ -280,7 +280,7 @@ func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images.
func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path)
- auth, err := d.Client().CanI(ns, "apps/v1/daemonset", n, []string{client.PatchVerb})
+ auth, err := d.Client().CanI(ns, "apps/v1/daemonset", n, client.PatchAccess)
if err != nil {
return err
}
diff --git a/internal/dao/node.go b/internal/dao/node.go
index 1ee29ed1..e55f40c5 100644
--- a/internal/dao/node.go
+++ b/internal/dao/node.go
@@ -248,7 +248,7 @@ func (n *Node) ensureCordoned(path string) (bool, error) {
// FetchNode retrieves a node.
func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
_, n := client.Namespaced(path)
- auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", n, []string{"get"})
+ auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", n, client.GetAccess)
if err != nil {
return nil, err
}
@@ -272,7 +272,7 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
// FetchNodes retrieves all nodes.
func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) {
- auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", "", []string{client.ListVerb})
+ auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", "", client.ListAccess)
if err != nil {
return nil, err
}
diff --git a/internal/dao/pod.go b/internal/dao/pod.go
index 4b03d66e..d6585449 100644
--- a/internal/dao/pod.go
+++ b/internal/dao/pod.go
@@ -121,7 +121,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
// Logs fetch container logs for a given pod and container.
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
ns, n := client.Namespaced(path)
- auth, err := p.Client().CanI(ns, "v1/pods:log", n, []string{client.GetVerb})
+ auth, err := p.Client().CanI(ns, "v1/pods:log", n, client.GetAccess)
if err != nil {
return nil, err
}
@@ -426,7 +426,7 @@ func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images.
func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path)
- auth, err := p.Client().CanI(ns, "v1/pod", n, []string{client.PatchVerb})
+ auth, err := p.Client().CanI(ns, "v1/pod", n, client.PatchAccess)
if err != nil {
return err
}
diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go
index 345ad3bb..454af942 100644
--- a/internal/dao/port_forwarder.go
+++ b/internal/dao/port_forwarder.go
@@ -117,7 +117,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por
p.path, p.tunnel, p.age = path, tt, time.Now()
ns, n := client.Namespaced(path)
- auth, err := p.Client().CanI(ns, "v1/pods", n, []string{client.GetVerb})
+ auth, err := p.Client().CanI(ns, "v1/pods", n, client.GetAccess)
if err != nil {
return nil, err
}
diff --git a/internal/dao/registry.go b/internal/dao/registry.go
index fd19c6c4..e19ad586 100644
--- a/internal/dao/registry.go
+++ b/internal/dao/registry.go
@@ -21,50 +21,24 @@ const (
crdCat = "crd"
k9sCat = "k9s"
helmCat = "helm"
+ crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
)
// MetaAccess tracks resources metadata.
var MetaAccess = NewMeta()
-var stdGroups = []string{
- "admissionregistration.k8s.io/v1",
- "admissionregistration.k8s.io/v1beta1",
- "apiextensions.k8s.io/v1",
- "apiextensions.k8s.io/v1beta1",
- "apiregistration.k8s.io/v1",
- "apiregistration.k8s.io/v1beta1",
- "apps/v1",
- "authentication.k8s.io/v1",
- "authentication.k8s.io/v1beta1",
- "authorization.k8s.io/v1",
- "authorization.k8s.io/v1beta1",
- "autoscaling/v1",
- "autoscaling/v2beta1",
- "autoscaling/v2beta2",
- "batch/v1",
- "batch/v1beta1",
- "certificates.k8s.io/v1",
- "certificates.k8s.io/v1beta1",
- "coordination.k8s.io/v1",
- "coordination.k8s.io/v1beta1",
- "discovery.k8s.io/v1beta1",
- "dynatrace.com/v1alpha1",
- "events.k8s.io/v1",
- "extensions/v1beta1",
- "flowcontrol.apiserver.k8s.io/v1beta1",
- "metrics.k8s.io/v1beta1",
- "networking.k8s.io/v1",
- "networking.k8s.io/v1beta1",
- "node.k8s.io/v1",
- "node.k8s.io/v1beta1",
- "policy/v1beta1",
- "rbac.authorization.k8s.io/v1",
- "rbac.authorization.k8s.io/v1beta1",
- "scheduling.k8s.io/v1",
- "scheduling.k8s.io/v1beta1",
- "storage.k8s.io/v1",
- "storage.k8s.io/v1beta1",
- "v1",
+var stdGroups = map[string]struct{}{
+ "apps/v1": {},
+ "autoscaling/v1": {},
+ "autoscaling/v2": {},
+ "autoscaling/v2beta1": {},
+ "autoscaling/v2beta2": {},
+ "batch/v1": {},
+ "batch/v1beta1": {},
+ "extensions/v1beta1": {},
+ "policy/v1beta1": {},
+ "policy/v1": {},
+ "v1": {},
}
func (m ResourceMetas) clear() {
@@ -372,7 +346,6 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if err != nil {
return err
}
- dial.Invalidate()
rr, err := dial.ServerPreferredResources()
if err != nil {
log.Debug().Err(err).Msgf("Failed to load preferred resources")
@@ -387,7 +360,7 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if res.SingularName == "" {
res.SingularName = strings.ToLower(res.Kind)
}
- if !isStandardGroup(res.Group) {
+ if !isStandardGroup(r.GroupVersion) {
res.Categories = append(res.Categories, crdCat)
}
m[gvr] = res
@@ -397,14 +370,12 @@ func loadPreferred(f Factory, m ResourceMetas) error {
return nil
}
-func isStandardGroup(r string) bool {
- for _, res := range stdGroups {
- if strings.Index(res, r) == 0 {
- return true
- }
+func isStandardGroup(gv string) bool {
+ if _, ok := stdGroups[gv]; ok {
+ return true
}
- return false
+ return strings.Contains(gv, "k8s.io")
}
var deprecatedGVRs = map[client.GVR]struct{}{
@@ -420,7 +391,6 @@ func loadCRDs(f Factory, m ResourceMetas) {
if f.Client() == nil || !f.Client().ConnectionOK() {
return
}
- const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything())
if err != nil {
log.Warn().Err(err).Msgf("Fail CRDs load")
diff --git a/internal/dao/sts.go b/internal/dao/sts.go
index 01376409..9c111d52 100644
--- a/internal/dao/sts.go
+++ b/internal/dao/sts.go
@@ -91,7 +91,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
s.Forwarders().Kill(client.FQN(p.Namespace, p.Name))
}
- auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", n, []string{client.PatchVerb})
+ auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", n, client.PatchAccess)
if err != nil {
return err
}
@@ -291,7 +291,7 @@ func (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images.
func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path)
- auth, err := s.Client().CanI(ns, "apps/v1/statefulset", n, []string{client.PatchVerb})
+ auth, err := s.Client().CanI(ns, "apps/v1/statefulset", n, client.PatchAccess)
if err != nil {
return err
}
diff --git a/internal/model/table.go b/internal/model/table.go
index ce848724..be1157b5 100644
--- a/internal/model/table.go
+++ b/internal/model/table.go
@@ -204,6 +204,10 @@ func (t *Table) updater(ctx context.Context) {
}
func (t *Table) refresh(ctx context.Context) error {
+ defer func(ti time.Time) {
+ log.Trace().Msgf("Refresh [%s](%d) %s ", t.gvr, t.data.RowCount(), time.Since(ti))
+ }(time.Now())
+
if !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...")
return nil
diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go
index 10b0c3c6..13ef48ce 100644
--- a/internal/model1/table_data.go
+++ b/internal/model1/table_data.go
@@ -244,7 +244,6 @@ func (t *TableData) Reset(ns string) {
func (t *TableData) Reconcile(ctx context.Context, r Renderer, oo []runtime.Object) error {
var rows Rows
-
if len(oo) > 0 {
if r.IsGeneric() {
table, ok := oo[0].(*metav1.Table)
@@ -399,6 +398,7 @@ func (t *TableData) Clone() *TableData {
header: t.header.Clone(),
rowEvents: t.rowEvents.Clone(),
namespace: t.namespace,
+ gvr: t.gvr,
}
}
diff --git a/internal/view/browser.go b/internal/view/browser.go
index b6045a6d..7f0876d1 100644
--- a/internal/view/browser.go
+++ b/internal/view/browser.go
@@ -442,7 +442,7 @@ func editRes(app *App, gvr client.GVR, path string) error {
if gvr.String() == "v1/namespaces" {
ns = n
}
- if ok, err := app.Conn().CanI(ns, gvr.String(), n, []string{"patch"}); !ok || err != nil {
+ if ok, err := app.Conn().CanI(ns, gvr.String(), n, client.PatchAccess); !ok || err != nil {
return fmt.Errorf("current user can't edit resource %s", gvr)
}
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 5c2fdaa4..de4bd294 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core20
-version: 'v0.32.1'
+version: 'v0.32.2'
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.