K9s release v0.32.2 (#2598)

* [Maint] cleaning up

* [Bug] Fix #2593

* [Bug] Fix #2582

* Release v0.32.2
mine
Fernand Galiana 2024-03-05 17:11:31 -07:00 committed by GitHub
parent 69cd0cd707
commit ecd33ff48d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 105 additions and 70 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.32.1 VERSION ?= v0.32.2
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.png" align="center" width="800" height="auto"/>
# 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
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -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 { if !ok || err != nil {
return nil, fmt.Errorf("user not authorized to list all namespaces") 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 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. // SwitchContext handles kubeconfig context switches.
func (a *APIClient) SwitchContext(name string) error { func (a *APIClient) SwitchContext(name string) error {
log.Debug().Msgf("Switching context %q", name) log.Debug().Msgf("Switching context %q", name)
if err := a.config.SwitchContext(name); err != nil { if err := a.config.SwitchContext(name); err != nil {
return err return err
} }
if err := a.invalidateCache(); err != nil {
return err
}
a.reset() a.reset()
ResetMetrics() ResetMetrics()

View File

@ -55,6 +55,9 @@ const (
) )
var ( var (
// PatchAccess patch a resource.
PatchAccess = []string{PatchVerb}
// GetAccess reads a resource. // GetAccess reads a resource.
GetAccess = []string{GetVerb} GetAccess = []string{GetVerb}

View File

@ -177,7 +177,8 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("help", "h", "?") a.declare("help", "h", "?")
a.declare("quit", "q", "q!", "qa", "Q") a.declare("quit", "q", "q!", "qa", "Q")
a.declare("aliases", "alias", "a") a.declare("aliases", "alias", "a")
a.declare("popeye", "pop") // !!BOZO!!
// a.declare("popeye", "pop")
a.declare("helm", "charts", "chart", "hm") a.declare("helm", "charts", "chart", "hm")
a.declare("dir", "d") a.declare("dir", "d")
a.declare("contexts", "context", "ctx") a.declare("contexts", "context", "ctx")

View File

@ -6,6 +6,7 @@ package config_test
import ( import (
"fmt" "fmt"
"os" "os"
"path"
"slices" "slices"
"testing" "testing"
@ -109,8 +110,8 @@ func TestAliasesLoad(t *testing.T) {
config.AppConfigDir = "testdata/aliases" config.AppConfigDir = "testdata/aliases"
a := config.NewAliases() a := config.NewAliases()
assert.Nil(t, a.Load("testdata/aliases/plain.yaml")) assert.Nil(t, a.Load(path.Join(config.AppConfigDir, "plain.yaml")))
assert.Equal(t, 56, len(a.Alias)) assert.Equal(t, 54, len(a.Alias))
} }
func TestAliasesSave(t *testing.T) { func TestAliasesSave(t *testing.T) {
@ -123,7 +124,7 @@ func TestAliasesSave(t *testing.T) {
assert.Equal(t, c, len(a.Alias)) assert.Equal(t, c, len(a.Alias))
assert.Nil(t, a.Save()) 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)) assert.Equal(t, c, len(a.Alias))
} }

View File

@ -1,3 +1,3 @@
aliases: aliases:
dp: "apps.v1.deployments" dp: "apps/v1/deployments"
pe: ".v1.pods" pe: "v1/pods"

View File

@ -86,7 +86,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
return err 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 { if err != nil {
return err return err
} }
@ -261,7 +261,7 @@ func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images. // SetImages sets container images.
func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path) 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 { if err != nil {
return err return err
} }

View File

@ -63,7 +63,7 @@ func (d *DaemonSet) Restart(ctx context.Context, path string) error {
return err 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 { if err != nil {
return err return err
} }
@ -280,7 +280,7 @@ func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images. // SetImages sets container images.
func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path) 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 { if err != nil {
return err return err
} }

View File

@ -248,7 +248,7 @@ func (n *Node) ensureCordoned(path string) (bool, error) {
// FetchNode retrieves a node. // FetchNode retrieves a node.
func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) { func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
_, n := client.Namespaced(path) _, 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 { if err != nil {
return nil, err return nil, err
} }
@ -272,7 +272,7 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
// FetchNodes retrieves all nodes. // FetchNodes retrieves all nodes.
func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -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. // Logs fetch container logs for a given pod and container.
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) { func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
ns, n := client.Namespaced(path) 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 { if err != nil {
return nil, err return nil, err
} }
@ -426,7 +426,7 @@ func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images. // SetImages sets container images.
func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path) 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 { if err != nil {
return err return err
} }

View File

@ -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() p.path, p.tunnel, p.age = path, tt, time.Now()
ns, n := client.Namespaced(path) 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -21,50 +21,24 @@ const (
crdCat = "crd" crdCat = "crd"
k9sCat = "k9s" k9sCat = "k9s"
helmCat = "helm" helmCat = "helm"
crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
) )
// MetaAccess tracks resources metadata. // MetaAccess tracks resources metadata.
var MetaAccess = NewMeta() var MetaAccess = NewMeta()
var stdGroups = []string{ var stdGroups = map[string]struct{}{
"admissionregistration.k8s.io/v1", "apps/v1": {},
"admissionregistration.k8s.io/v1beta1", "autoscaling/v1": {},
"apiextensions.k8s.io/v1", "autoscaling/v2": {},
"apiextensions.k8s.io/v1beta1", "autoscaling/v2beta1": {},
"apiregistration.k8s.io/v1", "autoscaling/v2beta2": {},
"apiregistration.k8s.io/v1beta1", "batch/v1": {},
"apps/v1", "batch/v1beta1": {},
"authentication.k8s.io/v1", "extensions/v1beta1": {},
"authentication.k8s.io/v1beta1", "policy/v1beta1": {},
"authorization.k8s.io/v1", "policy/v1": {},
"authorization.k8s.io/v1beta1", "v1": {},
"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",
} }
func (m ResourceMetas) clear() { func (m ResourceMetas) clear() {
@ -372,7 +346,6 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if err != nil { if err != nil {
return err return err
} }
dial.Invalidate()
rr, err := dial.ServerPreferredResources() rr, err := dial.ServerPreferredResources()
if err != nil { if err != nil {
log.Debug().Err(err).Msgf("Failed to load preferred resources") log.Debug().Err(err).Msgf("Failed to load preferred resources")
@ -387,7 +360,7 @@ func loadPreferred(f Factory, m ResourceMetas) error {
if res.SingularName == "" { if res.SingularName == "" {
res.SingularName = strings.ToLower(res.Kind) res.SingularName = strings.ToLower(res.Kind)
} }
if !isStandardGroup(res.Group) { if !isStandardGroup(r.GroupVersion) {
res.Categories = append(res.Categories, crdCat) res.Categories = append(res.Categories, crdCat)
} }
m[gvr] = res m[gvr] = res
@ -397,14 +370,12 @@ func loadPreferred(f Factory, m ResourceMetas) error {
return nil return nil
} }
func isStandardGroup(r string) bool { func isStandardGroup(gv string) bool {
for _, res := range stdGroups { if _, ok := stdGroups[gv]; ok {
if strings.Index(res, r) == 0 { return true
return true
}
} }
return false return strings.Contains(gv, "k8s.io")
} }
var deprecatedGVRs = map[client.GVR]struct{}{ var deprecatedGVRs = map[client.GVR]struct{}{
@ -420,7 +391,6 @@ func loadCRDs(f Factory, m ResourceMetas) {
if f.Client() == nil || !f.Client().ConnectionOK() { if f.Client() == nil || !f.Client().ConnectionOK() {
return return
} }
const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions"
oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything()) oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything())
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("Fail CRDs load") log.Warn().Err(err).Msgf("Fail CRDs load")

View File

@ -91,7 +91,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
s.Forwarders().Kill(client.FQN(p.Namespace, p.Name)) 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 { if err != nil {
return err return err
} }
@ -291,7 +291,7 @@ func (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) {
// SetImages sets container images. // SetImages sets container images.
func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
ns, n := client.Namespaced(path) 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 { if err != nil {
return err return err
} }

View File

@ -204,6 +204,10 @@ func (t *Table) updater(ctx context.Context) {
} }
func (t *Table) refresh(ctx context.Context) error { 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) { if !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) {
log.Debug().Msgf("Dropping update...") log.Debug().Msgf("Dropping update...")
return nil return nil

View File

@ -244,7 +244,6 @@ func (t *TableData) Reset(ns string) {
func (t *TableData) Reconcile(ctx context.Context, r Renderer, oo []runtime.Object) error { func (t *TableData) Reconcile(ctx context.Context, r Renderer, oo []runtime.Object) error {
var rows Rows var rows Rows
if len(oo) > 0 { if len(oo) > 0 {
if r.IsGeneric() { if r.IsGeneric() {
table, ok := oo[0].(*metav1.Table) table, ok := oo[0].(*metav1.Table)
@ -399,6 +398,7 @@ func (t *TableData) Clone() *TableData {
header: t.header.Clone(), header: t.header.Clone(),
rowEvents: t.rowEvents.Clone(), rowEvents: t.rowEvents.Clone(),
namespace: t.namespace, namespace: t.namespace,
gvr: t.gvr,
} }
} }

View File

@ -442,7 +442,7 @@ func editRes(app *App, gvr client.GVR, path string) error {
if gvr.String() == "v1/namespaces" { if gvr.String() == "v1/namespaces" {
ns = n 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) return fmt.Errorf("current user can't edit resource %s", gvr)
} }

View File

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