K9s/release v0.31.9 (#2543)

* [Bug] fix #2535

* [Bug] fix #2532

* [Bug] fix #2536

* [Bug] fix #2533

* [Bug] fix #2538

* [Maint] cleaning up

* Release notes
mine
Fernand Galiana 2024-02-15 17:43:29 -07:00 committed by GitHub
parent 207d05615a
commit f2f4077b59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 352 additions and 217 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.31.8 VERSION ?= v0.31.9
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,98 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.31.9
## 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!
```text
S .-'-.
o __| F `\
S `-,-`--._ `\
[] .->' X `|-'
`=/ (__/_ /
\_, ` _)
`----; |
```
⛔️ WE HAVE A PIPER DOWN! I REPEAT PIPER IS DOWN!! ⛔️
Popeye is undergoing heavy surgery at the moment so I had to break the bridge.
If you dig Popeye please run the binary separately for the time being.
I'll post another message here once the spinach formula upgrade is successful!
Also please make sure to add the gory details to issues ie relevant configs, debug logs, etc...
Comments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;(
Everyone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant.
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.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)
---
## ♫ Sounds Behind The Release ♭
Ushered or Taylored out?
* [Rough God Goes Riding - Van Morrison](https://www.youtube.com/watch?v=-kGrwRlJxcM)
* [Walk On - John Hiatt](https://www.youtube.com/watch?v=YVdMyeTQCkw)
* [On The Beach - Neil Young](https://www.youtube.com/watch?v=KBVde75e4sU)
---
## A Word From Our Sponsors...
To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!
* [Francis Lalonde](https://github.com/f-lalonde)
* [e-conomic a/s](https://github.com/e-conomic)
> Sponsorship cancellations since the last release: **2!** 🥹
---
## Resolved Issues
* [#2540](https://github.com/derailed/k9s/issues/2540) Option --write not functional
* [#2538](https://github.com/derailed/k9s/issues/2538) Opening screen dumps (sd) in K9s results in Failed to launch editor error message
* [#2536](https://github.com/derailed/k9s/issues/2536) Recent namespaces are lost when changing context
* [#2535](https://github.com/derailed/k9s/issues/2535) Namespaced configmap edit fails for user with RoleBinding to a role that allows it
* [#2532](https://github.com/derailed/k9s/issues/2532) Sporadic crashes (Maybe??)
---
## Contributed PRs
Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!
* [#2541](https://github.com/derailed/k9s/pull/2541) Add Rose Pine moon and dawn variants to skins
* [#2531](https://github.com/derailed/k9s/pull/2531) fix the --write flag
* [#2516](https://github.com/derailed/k9s/pull/2516) Added defaultsToFullScreen flag for Live/Details view,logs
---
<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

@ -84,7 +84,7 @@ func (a *APIClient) ConnectionOK() bool {
return a.connOK return a.connOK
} }
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { func makeSAR(ns, gvr, name string) *authorizationv1.SelfSubjectAccessReview {
if ns == ClusterScope { if ns == ClusterScope {
ns = BlankNamespace ns = BlankNamespace
} }
@ -98,13 +98,14 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
Version: res.Version, Version: res.Version,
Resource: res.Resource, Resource: res.Resource,
Subresource: spec.SubResource(), Subresource: spec.SubResource(),
Name: name,
}, },
}, },
} }
} }
func makeCacheKey(ns, gvr string, vv []string) string { func makeCacheKey(ns, gvr, n string, vv []string) string {
return ns + ":" + gvr + "::" + strings.Join(vv, ",") return ns + ":" + gvr + ":" + n + "::" + strings.Join(vv, ",")
} }
// ActiveContext returns the current context name. // ActiveContext returns the current context name.
@ -142,14 +143,14 @@ func (a *APIClient) clearCache() {
} }
// CanI checks if user has access to a certain resource. // CanI checks if user has access to a certain resource.
func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) { func (a *APIClient) CanI(ns, gvr, name string, verbs []string) (auth bool, err error) {
if !a.getConnOK() { if !a.getConnOK() {
return false, errors.New("ACCESS -- No API server connection") return false, errors.New("ACCESS -- No API server connection")
} }
if IsClusterWide(ns) { if IsClusterWide(ns) {
ns = BlankNamespace ns = BlankNamespace
} }
key := makeCacheKey(ns, gvr, verbs) key := makeCacheKey(ns, gvr, name, verbs)
if v, ok := a.cache.Get(key); ok { if v, ok := a.cache.Get(key); ok {
if auth, ok = v.(bool); ok { if auth, ok = v.(bool); ok {
return auth, nil return auth, nil
@ -160,7 +161,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error)
if err != nil { if err != nil {
return false, err return false, err
} }
client, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr) client, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr, name)
ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout()) ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())
defer cancel() defer cancel()
@ -215,7 +216,7 @@ func (a *APIClient) IsValidNamespace(ns string) bool {
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 {
nn, _ := a.ValidNamespaceNames() nn, _ := a.ValidNamespaceNames()
_, ok = nn[ns] _, ok = nn[ns]

View File

@ -74,7 +74,7 @@ func TestMakeSAR(t *testing.T) {
for k := range uu { for k := range uu {
u := uu[k] u := uu[k]
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr.String())) assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr.String(), ""))
}) })
} }
} }

View File

@ -93,7 +93,7 @@ func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
return errors.New("no metrics-server detected on cluster") return errors.New("no metrics-server detected on cluster")
} }
auth, err := m.CanI(ns, gvr, ListAccess) auth, err := m.CanI(ns, gvr, "", ListAccess)
if err != nil { if err != nil {
return err return err
} }

View File

@ -80,7 +80,7 @@ type PodsMetricsMap map[string]*mv1beta1.PodMetrics
// Authorizer checks what a user can or cannot do to a resource. // Authorizer checks what a user can or cannot do to a resource.
type Authorizer interface { type Authorizer interface {
// CanI returns true if the user can use these actions for a given resource. // CanI returns true if the user can use these actions for a given resource.
CanI(ns, gvr string, verbs []string) (bool, error) CanI(ns, gvr, n string, verbs []string) (bool, error)
} }
// Connection represents a Kubernetes apiserver connection. // Connection represents a Kubernetes apiserver connection.

View File

@ -33,6 +33,20 @@ func NewAliases() *Aliases {
} }
} }
func (a *Aliases) AliasesFor(s string) []string {
aa := make([]string, 0, 10)
a.mx.RLock()
defer a.mx.RUnlock()
for k, v := range a.Alias {
if v == s {
aa = append(aa, k)
}
}
return aa
}
// Keys returns all aliases keys. // Keys returns all aliases keys.
func (a *Aliases) Keys() []string { func (a *Aliases) Keys() []string {
a.mx.RLock() a.mx.RLock()

View File

@ -115,7 +115,7 @@ func NewMockConnectionWithContext(ct string) mockConnection {
return mockConnection{ct: ct} return mockConnection{ct: ct}
} }
func (m mockConnection) CanI(ns, gvr string, verbs []string) (bool, error) { func (m mockConnection) CanI(ns, gvr, n string, verbs []string) (bool, error) {
return true, nil return true, nil
} }
func (m mockConnection) Config() *client.Config { func (m mockConnection) Config() *client.Config {

View File

@ -37,6 +37,10 @@ func NewAlias(f Factory) *Alias {
return &a return &a
} }
func (a *Alias) AliasesFor(s string) []string {
return a.Aliases.AliasesFor(s)
}
// Check verifies an alias is defined for this command. // Check verifies an alias is defined for this command.
func (a *Alias) Check(cmd string) (string, bool) { func (a *Alias) Check(cmd string) (string, bool) {
return a.Aliases.Get(cmd) return a.Aliases.Get(cmd)

View File

@ -62,14 +62,14 @@ func (c *conn) ValidNamespaces() ([]v1.Namespace, error) { return n
func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) { func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) {
return "", false, nil return "", false, nil
} }
func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil } func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil }
func (c *conn) CurrentNamespaceName() (string, error) { return "", nil } func (c *conn) CurrentNamespaceName() (string, error) { return "", nil }
func (c *conn) CanI(ns, gvr string, verbs []string) (bool, error) { return true, nil } func (c *conn) CanI(ns, gvr, n string, verbs []string) (bool, error) { return true, nil }
func (c *conn) ActiveContext() string { return "" } func (c *conn) ActiveContext() string { return "" }
func (c *conn) ActiveNamespace() string { return "" } func (c *conn) ActiveNamespace() string { return "" }
func (c *conn) IsValidNamespace(string) bool { return true } func (c *conn) IsValidNamespace(string) bool { return true }
func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil } func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
func (c *conn) IsActiveNamespace(string) bool { return false } func (c *conn) IsActiveNamespace(string) bool { return false }
type podFactory struct{} type podFactory struct{}

View File

@ -47,8 +47,8 @@ func (c *CronJob) ListImages(ctx context.Context, fqn string) ([]string, error)
// Run a CronJob. // Run a CronJob.
func (c *CronJob) Run(path string) error { func (c *CronJob) Run(path string) error {
ns, _ := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := c.Client().CanI(ns, jobGVR, []string{client.GetVerb, client.CreateVerb}) auth, err := c.Client().CanI(ns, jobGVR, n, []string{client.GetVerb, client.CreateVerb})
if err != nil { if err != nil {
return err return err
} }
@ -144,7 +144,7 @@ func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) {
// ToggleSuspend toggles suspend/resume on a CronJob. // ToggleSuspend toggles suspend/resume on a CronJob.
func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error { func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := c.Client().CanI(ns, c.GVR(), []string{client.GetVerb, client.UpdateVerb}) auth, err := c.Client().CanI(ns, c.GVR(), n, []string{client.GetVerb, client.UpdateVerb})
if err != nil { if err != nil {
return err return err
} }

View File

@ -57,7 +57,7 @@ func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
// Scale a Deployment. // Scale a Deployment.
func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error { func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", []string{client.GetVerb, client.UpdateVerb}) auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", n, []string{client.GetVerb, client.UpdateVerb})
if err != nil { if err != nil {
return err return err
} }
@ -91,7 +91,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", []string{client.PatchVerb}) auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", dp.Name, []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }
@ -266,7 +266,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", []string{client.PatchVerb}) auth, err := d.Client().CanI(ns, "apps/v1/deployments", n, []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }

View File

@ -68,7 +68,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", []string{client.PatchVerb}) auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", ds.Name, []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }
@ -285,7 +285,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", []string{client.PatchVerb}) auth, err := d.Client().CanI(ns, "apps/v1/daemonset", n, []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }

View File

@ -106,7 +106,7 @@ func (g *Generic) ToYAML(path string, showManaged bool) (string, error) {
// Delete deletes a resource. // Delete deletes a resource.
func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error { func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := g.Client().CanI(ns, g.gvrStr(), []string{client.DeleteVerb}) auth, err := g.Client().CanI(ns, g.gvrStr(), n, []string{client.DeleteVerb})
if err != nil { if err != nil {
return err return err
} }

View File

@ -106,7 +106,7 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
dd, errs := h.GetPodsForDeletion(path) dd, errs := h.GetPodsForDeletion(path)
if len(errs) != 0 { if len(errs) != 0 {
for _, e := range errs { for _, e := range errs {
if _, err := h.ErrOut.Write([]byte(e.Error() + "\n")); err != nil { if _, err := h.ErrOut.Write([]byte(fmt.Sprintf("[%s] %s\n", path, e.Error()))); err != nil {
return err return err
} }
} }
@ -247,7 +247,8 @@ 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) {
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", []string{"get"}) _, n := client.Namespaced(path)
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", n, []string{"get"})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -271,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", "", []string{client.ListVerb})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -132,8 +132,8 @@ 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, _ := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := p.Client().CanI(ns, "v1/pods:log", []string{client.GetVerb}) auth, err := p.Client().CanI(ns, "v1/pods:log", n, []string{client.GetVerb})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -141,7 +141,6 @@ func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, er
return nil, fmt.Errorf("user is not authorized to view pod logs") return nil, fmt.Errorf("user is not authorized to view pod logs")
} }
ns, n := client.Namespaced(path)
dial, err := p.Client().DialLogs() dial, err := p.Client().DialLogs()
if err != nil { if err != nil {
return nil, err return nil, err
@ -457,7 +456,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", []string{client.PatchVerb}) auth, err := p.Client().CanI(ns, "v1/pod", n, []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }

View File

@ -3,140 +3,141 @@
package dao package dao
import ( // !!BOZO!!
"bytes" // import (
"context" // "bytes"
"encoding/json" // "context"
"errors" // "encoding/json"
"fmt" // "errors"
"os" // "fmt"
"path/filepath" // "os"
"sort" // "path/filepath"
"time" // "sort"
// "time"
"github.com/derailed/k9s/internal" // "github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client" // "github.com/derailed/k9s/internal/client"
cfg "github.com/derailed/k9s/internal/config" // cfg "github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/render" // "github.com/derailed/k9s/internal/render"
"github.com/derailed/popeye/pkg" // "github.com/derailed/popeye/pkg"
"github.com/derailed/popeye/pkg/config" // "github.com/derailed/popeye/pkg/config"
"github.com/derailed/popeye/types" // "github.com/derailed/popeye/types"
"github.com/rs/zerolog/log" // "github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime" // "k8s.io/apimachinery/pkg/runtime"
) // )
var _ Accessor = (*Popeye)(nil) // var _ Accessor = (*Popeye)(nil)
// Popeye tracks cluster sanitization. // // Popeye tracks cluster sanitization.
type Popeye struct { // type Popeye struct {
NonResource // NonResource
} // }
// NewPopeye returns a new set of aliases. // // NewPopeye returns a new set of aliases.
func NewPopeye(f Factory) *Popeye { // func NewPopeye(f Factory) *Popeye {
a := Popeye{} // a := Popeye{}
a.Init(f, client.NewGVR("popeye")) // a.Init(f, client.NewGVR("popeye"))
return &a // return &a
} // }
type readWriteCloser struct { // type readWriteCloser struct {
*bytes.Buffer // *bytes.Buffer
} // }
// Close close read stream. // // Close close read stream.
func (readWriteCloser) Close() error { // func (readWriteCloser) Close() error {
return nil // return nil
} // }
// List returns a collection of aliases. // // List returns a collection of aliases.
func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) { // func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) {
defer func(t time.Time) { // defer func(t time.Time) {
log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t)) // log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t))
if err := recover(); err != nil { // if err := recover(); err != nil {
log.Debug().Msgf("POPEYE DIED!") // log.Debug().Msgf("POPEYE DIED!")
} // }
}(time.Now()) // }(time.Now())
flags, js := config.NewFlags(), "json" // flags, js := config.NewFlags(), "json"
flags.Output = &js // flags.Output = &js
flags.ActiveNamespace = &ns // flags.ActiveNamespace = &ns
if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" { // if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
ns, n := client.Namespaced(report) // ns, n := client.Namespaced(report)
sections := []string{n} // sections := []string{n}
flags.Sections = &sections // flags.Sections = &sections
flags.ActiveNamespace = &ns // flags.ActiveNamespace = &ns
} // }
spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml") // spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml")
if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil { // if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil {
spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c)) // spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c))
} // }
if _, err := os.Stat(spinach); err == nil { // if _, err := os.Stat(spinach); err == nil {
flags.Spinach = &spinach // flags.Spinach = &spinach
} // }
popeye, err := pkg.NewPopeye(flags, &log.Logger) // popeye, err := pkg.NewPopeye(flags, &log.Logger)
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
popeye.SetFactory(newPopeyeFactory(p.Factory)) // popeye.SetFactory(newPopeyeFactory(p.Factory))
if err = popeye.Init(); err != nil { // if err = popeye.Init(); err != nil {
return nil, err // return nil, err
} // }
buff := readWriteCloser{Buffer: bytes.NewBufferString("")} // buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
popeye.SetOutputTarget(buff) // popeye.SetOutputTarget(buff)
if _, _, err = popeye.Sanitize(); err != nil { // if _, _, err = popeye.Sanitize(); err != nil {
log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections) // log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections)
return nil, err // return nil, err
} // }
var b render.Builder // var b render.Builder
if err = json.Unmarshal(buff.Bytes(), &b); err != nil { // if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
return nil, err // return nil, err
} // }
oo := make([]runtime.Object, 0, len(b.Report.Sections)) // oo := make([]runtime.Object, 0, len(b.Report.Sections))
sort.Sort(b.Report.Sections) // sort.Sort(b.Report.Sections)
for _, s := range b.Report.Sections { // for _, s := range b.Report.Sections {
s.Tally.Count = len(s.Outcome) // s.Tally.Count = len(s.Outcome)
if s.Tally.Sum() > 0 { // if s.Tally.Sum() > 0 {
oo = append(oo, s) // oo = append(oo, s)
} // }
} // }
return oo, nil // return oo, nil
} // }
// Get retrieves a resource. // // Get retrieves a resource.
func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) { // func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!") // return nil, errors.New("NYI!!")
} // }
// ---------------------------------------------------------------------------- // // ----------------------------------------------------------------------------
// Helpers... // // Helpers...
type popFactory struct { // type popFactory struct {
Factory // Factory
} // }
var _ types.Factory = (*popFactory)(nil) // var _ types.Factory = (*popFactory)(nil)
func newPopeyeFactory(f Factory) *popFactory { // func newPopeyeFactory(f Factory) *popFactory {
return &popFactory{Factory: f} // return &popFactory{Factory: f}
} // }
func (p *popFactory) Client() types.Connection { // func (p *popFactory) Client() types.Connection {
return &popeyeConnection{Connection: p.Factory.Client()} // return &popeyeConnection{Connection: p.Factory.Client()}
} // }
type popeyeConnection struct { // type popeyeConnection struct {
client.Connection // client.Connection
} // }
var _ types.Connection = (*popeyeConnection)(nil) // var _ types.Connection = (*popeyeConnection)(nil)
func (c *popeyeConnection) Config() types.Config { // func (c *popeyeConnection) Config() types.Config {
return c.Connection.Config() // return c.Connection.Config()
} // }

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", []string{client.GetVerb}) auth, err := p.Client().CanI(ns, "v1/pods", n, []string{client.GetVerb})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -136,7 +136,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por
return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase) return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase)
} }
auth, err = p.Client().CanI(ns, "v1/pods:portforward", []string{client.CreateVerb}) auth, err = p.Client().CanI(ns, "v1/pods:portforward", "", []string{client.CreateVerb})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -107,10 +107,11 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{}, client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
client.NewGVR("batch/v1/jobs"): &Job{}, client.NewGVR("batch/v1/jobs"): &Job{},
client.NewGVR("v1/namespaces"): &Namespace{}, client.NewGVR("v1/namespaces"): &Namespace{},
client.NewGVR("popeye"): &Popeye{}, // !!BOZO!!
client.NewGVR("helm"): &HelmChart{}, //client.NewGVR("popeye"): &Popeye{},
client.NewGVR("helm-history"): &HelmHistory{}, client.NewGVR("helm"): &HelmChart{},
client.NewGVR("dir"): &Dir{}, client.NewGVR("helm-history"): &HelmHistory{},
client.NewGVR("dir"): &Dir{},
} }
r, ok := m[gvr] r, ok := m[gvr]

View File

@ -58,7 +58,7 @@ func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
// Scale a StatefulSet. // Scale a StatefulSet.
func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error { func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error {
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", []string{client.GetVerb, client.UpdateVerb}) auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", n, []string{client.GetVerb, client.UpdateVerb})
if err != nil { if err != nil {
return err return err
} }
@ -87,7 +87,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
return err return err
} }
ns, _ := client.Namespaced(path) ns, n := client.Namespaced(path)
pp, err := podsFromSelector(s.Factory, ns, sts.Spec.Selector.MatchLabels) pp, err := podsFromSelector(s.Factory, ns, sts.Spec.Selector.MatchLabels)
if err != nil { if err != nil {
return err return err
@ -96,7 +96,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", []string{client.PatchVerb}) auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", n, []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }
@ -296,7 +296,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", []string{client.PatchVerb}) auth, err := s.Client().CanI(ns, "apps/v1/statefulset", n, []string{client.PatchVerb})
if err != nil { if err != nil {
return err return err
} }

View File

@ -48,7 +48,7 @@ type Workload struct {
func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error { func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR) gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR)
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
auth, err := w.Client().CanI(ns, gvr.String(), []string{client.DeleteVerb}) auth, err := w.Client().CanI(ns, gvr.String(), n, []string{client.DeleteVerb})
if err != nil { if err != nil {
return err return err
} }

View File

@ -82,14 +82,15 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Alias{}, DAO: &dao.Alias{},
Renderer: &render.Alias{}, Renderer: &render.Alias{},
}, },
"popeye": { // !!BOZO!!
DAO: &dao.Popeye{}, //"popeye": {
Renderer: &render.Popeye{}, // DAO: &dao.Popeye{},
}, // Renderer: &render.Popeye{},
"sanitizer": { //},
DAO: &dao.Popeye{}, //"sanitizer": {
TreeRenderer: &xray.Section{}, // DAO: &dao.Popeye{},
}, // TreeRenderer: &xray.Section{},
//},
// Core... // Core...
"v1/endpoints": { "v1/endpoints": {

View File

@ -74,8 +74,8 @@ func (s *StatusIndicator) ClusterInfoChanged(prev, cur model.ClusterMeta) {
s.SetPermanent(fmt.Sprintf( s.SetPermanent(fmt.Sprintf(
statusIndicatorFmt, statusIndicatorFmt,
cur.K9sVer, cur.K9sVer,
cur.Context,
cur.Cluster, cur.Cluster,
cur.User,
cur.K8sVer, cur.K8sVer,
AsPercDelta(prev.Cpu, cur.Cpu), AsPercDelta(prev.Cpu, cur.Cpu),
AsPercDelta(prev.Cpu, cur.Mem), AsPercDelta(prev.Cpu, cur.Mem),

View File

@ -75,12 +75,12 @@ func hotKeyActions(r Runner, aa ui.KeyActions) error {
errs = errors.Join(errs, err) errs = errors.Join(errs, err)
continue continue
} }
_, ok := aa[key] if _, ok := aa[key]; ok {
if ok && !hk.Override { if !hk.Override {
errs = errors.Join(errs, fmt.Errorf("duplicated hotkeys found for %q in %q", hk.ShortCut, k)) errs = errors.Join(errs, fmt.Errorf("duplicate hotkey found for %q in %q", hk.ShortCut, k))
continue continue
} else if ok && hk.Override == true { }
log.Info().Msgf("Action %q has been overrided by hotkey in %q", hk.ShortCut, k) log.Info().Msgf("Action %q has been overridden by hotkey in %q", hk.ShortCut, k)
} }
command, err := r.EnvFn()().Substitute(hk.Command) command, err := r.EnvFn()().Substitute(hk.Command)
@ -127,18 +127,18 @@ func pluginActions(r Runner, aa ui.KeyActions) error {
continue continue
} }
key, err := asKey(plugin.ShortCut) key, err := asKey(plugin.ShortCut)
if err != nil { if err != nil {
errs = errors.Join(errs, err) errs = errors.Join(errs, err)
continue continue
} }
_, ok := aa[key] if _, ok := aa[key]; ok {
if ok && !plugin.Override { if !plugin.Override {
errs = errors.Join(errs, fmt.Errorf("duplicated plugin key found for %q in %q", plugin.ShortCut, k)) errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", plugin.ShortCut, k))
continue continue
} else if ok && plugin.Override == true { }
log.Info().Msgf("Action %q has been overrided by plugin in %q", plugin.ShortCut, k) log.Info().Msgf("Action %q has been overridden by plugin in %q", plugin.ShortCut, k)
} }
aa[key] = ui.NewKeyActionWithOpts( aa[key] = ui.NewKeyActionWithOpts(
plugin.Description, plugin.Description,
pluginAction(r, plugin), pluginAction(r, plugin),

View File

@ -485,6 +485,8 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
} }
if err := a.Config.Save(); err != nil { if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("config save failed!") log.Error().Err(err).Msg("config save failed!")
} else {
log.Debug().Msgf("Saved context config for: %q", name)
} }
a.initFactory(ns) a.initFactory(ns)
if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil { if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil {

View File

@ -398,7 +398,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(), []string{"patch"}); !ok || err != nil { if ok, err := app.Conn().CanI(ns, gvr.String(), n, []string{"patch"}); !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)
} }
@ -423,7 +423,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
ns := b.namespaces[i] ns := b.namespaces[i]
auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), client.ListAccess) auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), "", client.ListAccess)
if !auth { if !auth {
if err == nil { if err == nil {
err = fmt.Errorf("current user can't access namespace %s", ns) err = fmt.Errorf("current user can't access namespace %s", ns)
@ -556,7 +556,7 @@ func (b *Browser) simpleDelete(selections []string, msg string) {
dialog.ShowConfirm(b.app.Styles.Dialog(), b.app.Content.Pages, "Confirm Delete", msg, func() { dialog.ShowConfirm(b.app.Styles.Dialog(), b.app.Content.Pages, "Confirm Delete", msg, func() {
b.ShowDeleted() b.ShowDeleted()
if len(selections) > 1 { if len(selections) > 1 {
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR()) b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR().R())
} else { } else {
b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0]) b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0])
} }

View File

@ -53,7 +53,7 @@ func (c *ClusterInfo) StylesChanged(s *config.Styles) {
func (c *ClusterInfo) hasMetrics() bool { func (c *ClusterInfo) hasMetrics() bool {
mx := c.app.Conn().HasMetrics() mx := c.app.Conn().HasMetrics()
if mx { if mx {
auth, err := c.app.Conn().CanI("", "metrics.k8s.io/v1beta1/nodes", client.ListAccess) auth, err := c.app.Conn().CanI("", "metrics.k8s.io/v1beta1/nodes", "", client.ListAccess)
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("No nodes metrics access") log.Warn().Err(err).Msgf("No nodes metrics access")
} }

View File

@ -39,14 +39,7 @@ func NewCommand(app *App) *Command {
// AliasesFor gather all known aliases for a given resource. // AliasesFor gather all known aliases for a given resource.
func (c *Command) AliasesFor(s string) []string { func (c *Command) AliasesFor(s string) []string {
aa := make([]string, 0, 10) return c.alias.AliasesFor(s)
for k, v := range c.alias.Alias {
if v == s {
aa = append(aa, k)
}
}
return aa
} }
// Init initializes the command. // Init initializes the command.
@ -163,6 +156,13 @@ func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool) error {
} }
if context, ok := p.HasContext(); ok { if context, ok := p.HasContext(); ok {
if context != c.app.Config.ActiveContextName() {
if err := c.app.Config.Save(); err != nil {
log.Error().Err(err).Msg("config save failed!")
} else {
log.Debug().Msgf("Saved context config for: %q", context)
}
}
res, err := dao.AccessorFor(c.app.factory, client.NewGVR("contexts")) res, err := dao.AccessorFor(c.app.factory, client.NewGVR("contexts"))
if err != nil { if err != nil {
return err return err

View File

@ -4,6 +4,7 @@
package view package view
import ( import (
"fmt"
"strconv" "strconv"
"time" "time"
@ -15,10 +16,10 @@ import (
const drainKey = "drain" const drainKey = "drain"
// DrainFunc represents a drain callback function. // DrainFunc represents a drain callback function.
type DrainFunc func(v ResourceViewer, path string, opts dao.DrainOptions) type DrainFunc func(v ResourceViewer, sels []string, opts dao.DrainOptions)
// ShowDrain pops a node drain dialog. // ShowDrain pops a node drain dialog.
func ShowDrain(view ResourceViewer, path string, opts dao.DrainOptions, okFn DrainFunc) { func ShowDrain(view ResourceViewer, sels []string, opts dao.DrainOptions, okFn DrainFunc) {
styles := view.App().Styles styles := view.App().Styles
f := tview.NewForm() f := tview.NewForm()
@ -63,10 +64,17 @@ func ShowDrain(view ResourceViewer, path string, opts dao.DrainOptions, okFn Dra
}) })
f.AddButton("OK", func() { f.AddButton("OK", func() {
DismissDrain(view, pages) DismissDrain(view, pages)
okFn(view, path, opts) okFn(view, sels, opts)
}) })
modal := tview.NewModalForm("<Drain>", f) modal := tview.NewModalForm("<Drain>", f)
path := "Drain "
if len(sels) == 1 {
path += sels[0]
} else {
path += fmt.Sprintf("(%d) nodes", len(sels))
}
path += "?"
modal.SetText(path) modal.SetText(path)
modal.SetDoneFunc(func(_ int, b string) { modal.SetDoneFunc(func(_ int, b string) {
DismissDrain(view, pages) DismissDrain(view, pages)

View File

@ -123,12 +123,11 @@ func edit(a *App, opts shellOpts) bool {
) )
for _, e := range editorEnvVars { for _, e := range editorEnvVars {
env := os.Getenv(e) env := os.Getenv(e)
if env != "" { if env == "" {
continue continue
} }
bin, err = exec.LookPath(env) if bin, err = exec.LookPath(env); err == nil {
if err != nil { break
continue
} }
} }
if bin == "" { if bin == "" {

View File

@ -96,8 +96,8 @@ func (n *Node) showPods(a *App, _ ui.Tabular, _ client.GVR, path string) {
} }
func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey { func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {
path := n.GetTable().GetSelectedItem() sels := n.GetTable().GetSelectedItems()
if path == "" { if len(sels) == 0 {
return evt return evt
} }
@ -105,12 +105,12 @@ func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {
GracePeriodSeconds: -1, GracePeriodSeconds: -1,
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
} }
ShowDrain(n, path, opts, drainNode) ShowDrain(n, sels, opts, drainNode)
return nil return nil
} }
func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) { func drainNode(v ResourceViewer, sels []string, opts dao.DrainOptions) {
res, err := dao.AccessorFor(v.App().factory, v.GVR()) res, err := dao.AccessorFor(v.App().factory, v.GVR())
if err != nil { if err != nil {
v.App().Flash().Err(err) v.App().Flash().Err(err)
@ -125,14 +125,14 @@ func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) {
v.Stop() v.Stop()
defer v.Start() defer v.Start()
{ {
d := NewDetails(v.App(), "Drain Progress", path, contentYAML, true) d := NewDetails(v.App(), "Drain Progress", "nodes", contentYAML, true)
if err := v.App().inject(d, false); err != nil { if err := v.App().inject(d, false); err != nil {
v.App().Flash().Err(err) v.App().Flash().Err(err)
} }
for _, sel := range sels {
if err := m.Drain(path, opts, d.GetWriter()); err != nil { if err := m.Drain(sel, opts, d.GetWriter()); err != nil {
v.App().Flash().Err(err) v.App().Flash().Err(err)
return }
} }
v.Refresh() v.Refresh()
} }
@ -140,8 +140,8 @@ func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) {
func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.EventKey { func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.EventKey {
return func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey {
path := n.GetTable().GetSelectedItem() sels := n.GetTable().GetSelectedItems()
if path == "" { if len(sels) == 0 {
return evt return evt
} }
@ -151,7 +151,11 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve
} else { } else {
title, msg = title+"Uncordon", "Uncordon " title, msg = title+"Uncordon", "Uncordon "
} }
msg += path + "?" if len(sels) == 1 {
msg += sels[0] + "?"
} else {
msg += fmt.Sprintf("(%d) marked %s?", len(sels), n.GVR().R())
}
dialog.ShowConfirm(n.App().Styles.Dialog(), n.App().Content.Pages, title, msg, func() { dialog.ShowConfirm(n.App().Styles.Dialog(), n.App().Content.Pages, title, msg, func() {
res, err := dao.AccessorFor(n.App().factory, n.GVR()) res, err := dao.AccessorFor(n.App().factory, n.GVR())
if err != nil { if err != nil {
@ -163,8 +167,10 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve
n.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", n.GVR())) n.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", n.GVR()))
return return
} }
if err := m.ToggleCordon(path, cordon); err != nil { for _, s := range sels {
n.App().Flash().Err(err) if err := m.ToggleCordon(s, cordon); err != nil {
n.App().Flash().Err(err)
}
} }
n.Refresh() n.Refresh()
}, func() {}) }, func() {})

View File

@ -191,7 +191,7 @@ func (f *Factory) isClusterWide() bool {
// CanForResource return an informer is user has access. // CanForResource return an informer is user has access.
func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
auth, err := f.Client().CanI(ns, gvr, verbs) auth, err := f.Client().CanI(ns, gvr, "", verbs)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

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