diff --git a/Makefile b/Makefile
index d464c796f5..2382ab3354 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.31.8
+VERSION ?= v0.31.9
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}
diff --git a/change_logs/release_v0.31.9.md b/change_logs/release_v0.31.9.md
new file mode 100644
index 0000000000..25273f8813
--- /dev/null
+++ b/change_logs/release_v0.31.9.md
@@ -0,0 +1,98 @@
+
+
+# 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
+
+---
+
+ © 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 981e5db9f6..85946451ed 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -84,7 +84,7 @@ func (a *APIClient) ConnectionOK() bool {
return a.connOK
}
-func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
+func makeSAR(ns, gvr, name string) *authorizationv1.SelfSubjectAccessReview {
if ns == ClusterScope {
ns = BlankNamespace
}
@@ -98,13 +98,14 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
Version: res.Version,
Resource: res.Resource,
Subresource: spec.SubResource(),
+ Name: name,
},
},
}
}
-func makeCacheKey(ns, gvr string, vv []string) string {
- return ns + ":" + gvr + "::" + strings.Join(vv, ",")
+func makeCacheKey(ns, gvr, n string, vv []string) string {
+ return ns + ":" + gvr + ":" + n + "::" + strings.Join(vv, ",")
}
// ActiveContext returns the current context name.
@@ -142,14 +143,14 @@ func (a *APIClient) clearCache() {
}
// 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() {
return false, errors.New("ACCESS -- No API server connection")
}
if IsClusterWide(ns) {
ns = BlankNamespace
}
- key := makeCacheKey(ns, gvr, verbs)
+ key := makeCacheKey(ns, gvr, name, verbs)
if v, ok := a.cache.Get(key); ok {
if auth, ok = v.(bool); ok {
return auth, nil
@@ -160,7 +161,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error)
if err != nil {
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())
defer cancel()
@@ -215,7 +216,7 @@ func (a *APIClient) IsValidNamespace(ns string) bool {
return true
}
- ok, err := a.CanI(ClusterScope, "v1/namespaces", []string{ListVerb})
+ ok, err := a.CanI(ClusterScope, "v1/namespaces", "", []string{ListVerb})
if ok && err == nil {
nn, _ := a.ValidNamespaceNames()
_, ok = nn[ns]
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
index e5c17ddd71..e60634880e 100644
--- a/internal/client/client_test.go
+++ b/internal/client/client_test.go
@@ -74,7 +74,7 @@ func TestMakeSAR(t *testing.T) {
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()))
+ assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr.String(), ""))
})
}
}
diff --git a/internal/client/metrics.go b/internal/client/metrics.go
index c31ba6c5b0..13485c71a4 100644
--- a/internal/client/metrics.go
+++ b/internal/client/metrics.go
@@ -93,7 +93,7 @@ func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
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 {
return err
}
diff --git a/internal/client/types.go b/internal/client/types.go
index c6bcf760a1..8dad38a06a 100644
--- a/internal/client/types.go
+++ b/internal/client/types.go
@@ -80,7 +80,7 @@ type PodsMetricsMap map[string]*mv1beta1.PodMetrics
// Authorizer checks what a user can or cannot do to a resource.
type Authorizer interface {
// 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.
diff --git a/internal/config/alias.go b/internal/config/alias.go
index 21cf26ba18..35cd8e788c 100644
--- a/internal/config/alias.go
+++ b/internal/config/alias.go
@@ -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.
func (a *Aliases) Keys() []string {
a.mx.RLock()
diff --git a/internal/config/mock/test_helpers.go b/internal/config/mock/test_helpers.go
index a20db334c5..c5a3b69a75 100644
--- a/internal/config/mock/test_helpers.go
+++ b/internal/config/mock/test_helpers.go
@@ -115,7 +115,7 @@ func NewMockConnectionWithContext(ct string) mockConnection {
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
}
func (m mockConnection) Config() *client.Config {
diff --git a/internal/dao/alias.go b/internal/dao/alias.go
index f07b86c655..bf4bf308d5 100644
--- a/internal/dao/alias.go
+++ b/internal/dao/alias.go
@@ -37,6 +37,10 @@ func NewAlias(f Factory) *Alias {
return &a
}
+func (a *Alias) AliasesFor(s string) []string {
+ return a.Aliases.AliasesFor(s)
+}
+
// Check verifies an alias is defined for this command.
func (a *Alias) Check(cmd string) (string, bool) {
return a.Aliases.Get(cmd)
diff --git a/internal/dao/container_test.go b/internal/dao/container_test.go
index 38a24dd815..f53c82959d 100644
--- a/internal/dao/container_test.go
+++ b/internal/dao/container_test.go
@@ -62,14 +62,14 @@ func (c *conn) ValidNamespaces() ([]v1.Namespace, error) { return n
func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) {
return "", false, nil
}
-func (c *conn) ServerVersion() (*version.Info, error) { return nil, 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) ActiveContext() string { return "" }
-func (c *conn) ActiveNamespace() string { return "" }
-func (c *conn) IsValidNamespace(string) bool { return true }
-func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
-func (c *conn) IsActiveNamespace(string) bool { return false }
+func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil }
+func (c *conn) CurrentNamespaceName() (string, error) { return "", 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) ActiveNamespace() string { return "" }
+func (c *conn) IsValidNamespace(string) bool { return true }
+func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
+func (c *conn) IsActiveNamespace(string) bool { return false }
type podFactory struct{}
diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go
index 6feee0ca11..b02b95759e 100644
--- a/internal/dao/cronjob.go
+++ b/internal/dao/cronjob.go
@@ -47,8 +47,8 @@ func (c *CronJob) ListImages(ctx context.Context, fqn string) ([]string, error)
// Run a CronJob.
func (c *CronJob) Run(path string) error {
- ns, _ := client.Namespaced(path)
- auth, err := c.Client().CanI(ns, jobGVR, []string{client.GetVerb, client.CreateVerb})
+ ns, n := client.Namespaced(path)
+ auth, err := c.Client().CanI(ns, jobGVR, n, []string{client.GetVerb, client.CreateVerb})
if err != nil {
return err
}
@@ -144,7 +144,7 @@ func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) {
// ToggleSuspend toggles suspend/resume on a CronJob.
func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
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 {
return err
}
diff --git a/internal/dao/dp.go b/internal/dao/dp.go
index 4ab1a4badb..4f88d4a02a 100644
--- a/internal/dao/dp.go
+++ b/internal/dao/dp.go
@@ -57,7 +57,7 @@ func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
// Scale a Deployment.
func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {
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 {
return err
}
@@ -91,7 +91,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
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 {
return err
}
@@ -266,7 +266,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", []string{client.PatchVerb})
+ auth, err := d.Client().CanI(ns, "apps/v1/deployments", n, []string{client.PatchVerb})
if err != nil {
return err
}
diff --git a/internal/dao/ds.go b/internal/dao/ds.go
index 1488960762..0e2d9430a0 100644
--- a/internal/dao/ds.go
+++ b/internal/dao/ds.go
@@ -68,7 +68,7 @@ func (d *DaemonSet) Restart(ctx context.Context, path string) error {
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 {
return err
}
@@ -285,7 +285,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", []string{client.PatchVerb})
+ auth, err := d.Client().CanI(ns, "apps/v1/daemonset", n, []string{client.PatchVerb})
if err != nil {
return err
}
diff --git a/internal/dao/generic.go b/internal/dao/generic.go
index cf579cf7e8..e0aff2b649 100644
--- a/internal/dao/generic.go
+++ b/internal/dao/generic.go
@@ -106,7 +106,7 @@ func (g *Generic) ToYAML(path string, showManaged bool) (string, error) {
// Delete deletes a resource.
func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
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 {
return err
}
diff --git a/internal/dao/node.go b/internal/dao/node.go
index da5c8ddfba..1ee29ed143 100644
--- a/internal/dao/node.go
+++ b/internal/dao/node.go
@@ -106,7 +106,7 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
dd, errs := h.GetPodsForDeletion(path)
if len(errs) != 0 {
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
}
}
@@ -247,7 +247,8 @@ func (n *Node) ensureCordoned(path string) (bool, error) {
// FetchNode retrieves a node.
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 {
return nil, err
}
@@ -271,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", "", []string{client.ListVerb})
if err != nil {
return nil, err
}
diff --git a/internal/dao/pod.go b/internal/dao/pod.go
index f0bd20ad06..e9c6e09f46 100644
--- a/internal/dao/pod.go
+++ b/internal/dao/pod.go
@@ -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.
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
- ns, _ := client.Namespaced(path)
- auth, err := p.Client().CanI(ns, "v1/pods:log", []string{client.GetVerb})
+ ns, n := client.Namespaced(path)
+ auth, err := p.Client().CanI(ns, "v1/pods:log", n, []string{client.GetVerb})
if err != nil {
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")
}
- ns, n := client.Namespaced(path)
dial, err := p.Client().DialLogs()
if err != nil {
return nil, err
@@ -457,7 +456,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", []string{client.PatchVerb})
+ auth, err := p.Client().CanI(ns, "v1/pod", n, []string{client.PatchVerb})
if err != nil {
return err
}
diff --git a/internal/dao/popeye.go b/internal/dao/popeye.go
index e940f1b561..ec1aa801d3 100644
--- a/internal/dao/popeye.go
+++ b/internal/dao/popeye.go
@@ -3,140 +3,141 @@
package dao
-import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "sort"
- "time"
-
- "github.com/derailed/k9s/internal"
- "github.com/derailed/k9s/internal/client"
- cfg "github.com/derailed/k9s/internal/config"
- "github.com/derailed/k9s/internal/render"
- "github.com/derailed/popeye/pkg"
- "github.com/derailed/popeye/pkg/config"
- "github.com/derailed/popeye/types"
- "github.com/rs/zerolog/log"
- "k8s.io/apimachinery/pkg/runtime"
-)
-
-var _ Accessor = (*Popeye)(nil)
-
-// Popeye tracks cluster sanitization.
-type Popeye struct {
- NonResource
-}
-
-// NewPopeye returns a new set of aliases.
-func NewPopeye(f Factory) *Popeye {
- a := Popeye{}
- a.Init(f, client.NewGVR("popeye"))
-
- return &a
-}
-
-type readWriteCloser struct {
- *bytes.Buffer
-}
-
-// Close close read stream.
-func (readWriteCloser) Close() error {
- return nil
-}
-
-// List returns a collection of aliases.
-func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) {
- defer func(t time.Time) {
- log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t))
- if err := recover(); err != nil {
- log.Debug().Msgf("POPEYE DIED!")
- }
- }(time.Now())
-
- flags, js := config.NewFlags(), "json"
- flags.Output = &js
- flags.ActiveNamespace = &ns
-
- if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
- ns, n := client.Namespaced(report)
- sections := []string{n}
- flags.Sections = §ions
- flags.ActiveNamespace = &ns
- }
- spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml")
- if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil {
- spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c))
- }
- if _, err := os.Stat(spinach); err == nil {
- flags.Spinach = &spinach
- }
-
- popeye, err := pkg.NewPopeye(flags, &log.Logger)
- if err != nil {
- return nil, err
- }
- popeye.SetFactory(newPopeyeFactory(p.Factory))
- if err = popeye.Init(); err != nil {
- return nil, err
- }
-
- buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
- popeye.SetOutputTarget(buff)
- if _, _, err = popeye.Sanitize(); err != nil {
- log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections)
- return nil, err
- }
-
- var b render.Builder
- if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
- return nil, err
- }
-
- oo := make([]runtime.Object, 0, len(b.Report.Sections))
- sort.Sort(b.Report.Sections)
- for _, s := range b.Report.Sections {
- s.Tally.Count = len(s.Outcome)
- if s.Tally.Sum() > 0 {
- oo = append(oo, s)
- }
- }
-
- return oo, nil
-}
-
-// Get retrieves a resource.
-func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
- return nil, errors.New("NYI!!")
-}
-
-// ----------------------------------------------------------------------------
-// Helpers...
-
-type popFactory struct {
- Factory
-}
-
-var _ types.Factory = (*popFactory)(nil)
-
-func newPopeyeFactory(f Factory) *popFactory {
- return &popFactory{Factory: f}
-}
-
-func (p *popFactory) Client() types.Connection {
- return &popeyeConnection{Connection: p.Factory.Client()}
-}
-
-type popeyeConnection struct {
- client.Connection
-}
-
-var _ types.Connection = (*popeyeConnection)(nil)
-
-func (c *popeyeConnection) Config() types.Config {
- return c.Connection.Config()
-}
+// !!BOZO!!
+// import (
+// "bytes"
+// "context"
+// "encoding/json"
+// "errors"
+// "fmt"
+// "os"
+// "path/filepath"
+// "sort"
+// "time"
+
+// "github.com/derailed/k9s/internal"
+// "github.com/derailed/k9s/internal/client"
+// cfg "github.com/derailed/k9s/internal/config"
+// "github.com/derailed/k9s/internal/render"
+// "github.com/derailed/popeye/pkg"
+// "github.com/derailed/popeye/pkg/config"
+// "github.com/derailed/popeye/types"
+// "github.com/rs/zerolog/log"
+// "k8s.io/apimachinery/pkg/runtime"
+// )
+
+// var _ Accessor = (*Popeye)(nil)
+
+// // Popeye tracks cluster sanitization.
+// type Popeye struct {
+// NonResource
+// }
+
+// // NewPopeye returns a new set of aliases.
+// func NewPopeye(f Factory) *Popeye {
+// a := Popeye{}
+// a.Init(f, client.NewGVR("popeye"))
+
+// return &a
+// }
+
+// type readWriteCloser struct {
+// *bytes.Buffer
+// }
+
+// // Close close read stream.
+// func (readWriteCloser) Close() error {
+// return nil
+// }
+
+// // List returns a collection of aliases.
+// func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) {
+// defer func(t time.Time) {
+// log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t))
+// if err := recover(); err != nil {
+// log.Debug().Msgf("POPEYE DIED!")
+// }
+// }(time.Now())
+
+// flags, js := config.NewFlags(), "json"
+// flags.Output = &js
+// flags.ActiveNamespace = &ns
+
+// if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
+// ns, n := client.Namespaced(report)
+// sections := []string{n}
+// flags.Sections = §ions
+// flags.ActiveNamespace = &ns
+// }
+// spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml")
+// if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil {
+// spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c))
+// }
+// if _, err := os.Stat(spinach); err == nil {
+// flags.Spinach = &spinach
+// }
+
+// popeye, err := pkg.NewPopeye(flags, &log.Logger)
+// if err != nil {
+// return nil, err
+// }
+// popeye.SetFactory(newPopeyeFactory(p.Factory))
+// if err = popeye.Init(); err != nil {
+// return nil, err
+// }
+
+// buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
+// popeye.SetOutputTarget(buff)
+// if _, _, err = popeye.Sanitize(); err != nil {
+// log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections)
+// return nil, err
+// }
+
+// var b render.Builder
+// if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
+// return nil, err
+// }
+
+// oo := make([]runtime.Object, 0, len(b.Report.Sections))
+// sort.Sort(b.Report.Sections)
+// for _, s := range b.Report.Sections {
+// s.Tally.Count = len(s.Outcome)
+// if s.Tally.Sum() > 0 {
+// oo = append(oo, s)
+// }
+// }
+
+// return oo, nil
+// }
+
+// // Get retrieves a resource.
+// func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
+// return nil, errors.New("NYI!!")
+// }
+
+// // ----------------------------------------------------------------------------
+// // Helpers...
+
+// type popFactory struct {
+// Factory
+// }
+
+// var _ types.Factory = (*popFactory)(nil)
+
+// func newPopeyeFactory(f Factory) *popFactory {
+// return &popFactory{Factory: f}
+// }
+
+// func (p *popFactory) Client() types.Connection {
+// return &popeyeConnection{Connection: p.Factory.Client()}
+// }
+
+// type popeyeConnection struct {
+// client.Connection
+// }
+
+// var _ types.Connection = (*popeyeConnection)(nil)
+
+// func (c *popeyeConnection) Config() types.Config {
+// return c.Connection.Config()
+// }
diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go
index 97043ff138..345ad3bb13 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", []string{client.GetVerb})
+ auth, err := p.Client().CanI(ns, "v1/pods", n, []string{client.GetVerb})
if err != nil {
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)
}
- 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 {
return nil, err
}
diff --git a/internal/dao/registry.go b/internal/dao/registry.go
index ff11836dbc..4060d4e9b0 100644
--- a/internal/dao/registry.go
+++ b/internal/dao/registry.go
@@ -107,10 +107,11 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
client.NewGVR("batch/v1/jobs"): &Job{},
client.NewGVR("v1/namespaces"): &Namespace{},
- client.NewGVR("popeye"): &Popeye{},
- client.NewGVR("helm"): &HelmChart{},
- client.NewGVR("helm-history"): &HelmHistory{},
- client.NewGVR("dir"): &Dir{},
+ // !!BOZO!!
+ //client.NewGVR("popeye"): &Popeye{},
+ client.NewGVR("helm"): &HelmChart{},
+ client.NewGVR("helm-history"): &HelmHistory{},
+ client.NewGVR("dir"): &Dir{},
}
r, ok := m[gvr]
diff --git a/internal/dao/sts.go b/internal/dao/sts.go
index 219b613c39..f562adef7c 100644
--- a/internal/dao/sts.go
+++ b/internal/dao/sts.go
@@ -58,7 +58,7 @@ func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
// Scale a StatefulSet.
func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error {
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 {
return err
}
@@ -87,7 +87,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
return err
}
- ns, _ := client.Namespaced(path)
+ ns, n := client.Namespaced(path)
pp, err := podsFromSelector(s.Factory, ns, sts.Spec.Selector.MatchLabels)
if err != nil {
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))
}
- 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 {
return err
}
@@ -296,7 +296,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", []string{client.PatchVerb})
+ auth, err := s.Client().CanI(ns, "apps/v1/statefulset", n, []string{client.PatchVerb})
if err != nil {
return err
}
diff --git a/internal/dao/workload.go b/internal/dao/workload.go
index 72027605ce..dfe4aa05b4 100644
--- a/internal/dao/workload.go
+++ b/internal/dao/workload.go
@@ -48,7 +48,7 @@ type Workload struct {
func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR)
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 {
return err
}
diff --git a/internal/model/registry.go b/internal/model/registry.go
index b4936c380f..55db15f832 100644
--- a/internal/model/registry.go
+++ b/internal/model/registry.go
@@ -82,14 +82,15 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Alias{},
Renderer: &render.Alias{},
},
- "popeye": {
- DAO: &dao.Popeye{},
- Renderer: &render.Popeye{},
- },
- "sanitizer": {
- DAO: &dao.Popeye{},
- TreeRenderer: &xray.Section{},
- },
+ // !!BOZO!!
+ //"popeye": {
+ // DAO: &dao.Popeye{},
+ // Renderer: &render.Popeye{},
+ //},
+ //"sanitizer": {
+ // DAO: &dao.Popeye{},
+ // TreeRenderer: &xray.Section{},
+ //},
// Core...
"v1/endpoints": {
diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go
index d9fb97a19d..ed4645a424 100644
--- a/internal/ui/indicator.go
+++ b/internal/ui/indicator.go
@@ -74,8 +74,8 @@ func (s *StatusIndicator) ClusterInfoChanged(prev, cur model.ClusterMeta) {
s.SetPermanent(fmt.Sprintf(
statusIndicatorFmt,
cur.K9sVer,
+ cur.Context,
cur.Cluster,
- cur.User,
cur.K8sVer,
AsPercDelta(prev.Cpu, cur.Cpu),
AsPercDelta(prev.Cpu, cur.Mem),
diff --git a/internal/view/actions.go b/internal/view/actions.go
index 2a1f8b1cb4..6709b4451b 100644
--- a/internal/view/actions.go
+++ b/internal/view/actions.go
@@ -75,12 +75,12 @@ func hotKeyActions(r Runner, aa ui.KeyActions) error {
errs = errors.Join(errs, err)
continue
}
- _, ok := aa[key]
- if ok && !hk.Override {
- errs = errors.Join(errs, fmt.Errorf("duplicated hotkeys found for %q in %q", hk.ShortCut, k))
- continue
- } else if ok && hk.Override == true {
- log.Info().Msgf("Action %q has been overrided by hotkey in %q", hk.ShortCut, k)
+ if _, ok := aa[key]; ok {
+ if !hk.Override {
+ errs = errors.Join(errs, fmt.Errorf("duplicate hotkey found for %q in %q", hk.ShortCut, k))
+ continue
+ }
+ log.Info().Msgf("Action %q has been overridden by hotkey in %q", hk.ShortCut, k)
}
command, err := r.EnvFn()().Substitute(hk.Command)
@@ -127,18 +127,18 @@ func pluginActions(r Runner, aa ui.KeyActions) error {
continue
}
key, err := asKey(plugin.ShortCut)
-
if err != nil {
errs = errors.Join(errs, err)
continue
}
- _, ok := aa[key]
- if ok && !plugin.Override {
- errs = errors.Join(errs, fmt.Errorf("duplicated plugin key found for %q in %q", plugin.ShortCut, k))
- continue
- } else if ok && plugin.Override == true {
- log.Info().Msgf("Action %q has been overrided by plugin in %q", plugin.ShortCut, k)
+ if _, ok := aa[key]; ok {
+ if !plugin.Override {
+ errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", plugin.ShortCut, k))
+ continue
+ }
+ log.Info().Msgf("Action %q has been overridden by plugin in %q", plugin.ShortCut, k)
}
+
aa[key] = ui.NewKeyActionWithOpts(
plugin.Description,
pluginAction(r, plugin),
diff --git a/internal/view/app.go b/internal/view/app.go
index cdc9dd2f21..1cfb2dcdd7 100644
--- a/internal/view/app.go
+++ b/internal/view/app.go
@@ -485,6 +485,8 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
}
if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("config save failed!")
+ } else {
+ log.Debug().Msgf("Saved context config for: %q", name)
}
a.initFactory(ns)
if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil {
diff --git a/internal/view/browser.go b/internal/view/browser.go
index e688f54c5e..932fbbb6cd 100644
--- a/internal/view/browser.go
+++ b/internal/view/browser.go
@@ -398,7 +398,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(), []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)
}
@@ -423,7 +423,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
}
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 err == nil {
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() {
b.ShowDeleted()
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 {
b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0])
}
diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go
index e6a24dea69..98512a9ac0 100644
--- a/internal/view/cluster_info.go
+++ b/internal/view/cluster_info.go
@@ -53,7 +53,7 @@ func (c *ClusterInfo) StylesChanged(s *config.Styles) {
func (c *ClusterInfo) hasMetrics() bool {
mx := c.app.Conn().HasMetrics()
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 {
log.Warn().Err(err).Msgf("No nodes metrics access")
}
diff --git a/internal/view/command.go b/internal/view/command.go
index 3373fce0b6..9a8d335551 100644
--- a/internal/view/command.go
+++ b/internal/view/command.go
@@ -39,14 +39,7 @@ func NewCommand(app *App) *Command {
// AliasesFor gather all known aliases for a given resource.
func (c *Command) AliasesFor(s string) []string {
- aa := make([]string, 0, 10)
- for k, v := range c.alias.Alias {
- if v == s {
- aa = append(aa, k)
- }
- }
-
- return aa
+ return c.alias.AliasesFor(s)
}
// 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 != 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"))
if err != nil {
return err
diff --git a/internal/view/drain_dialog.go b/internal/view/drain_dialog.go
index 722c89cedf..b576a18517 100644
--- a/internal/view/drain_dialog.go
+++ b/internal/view/drain_dialog.go
@@ -4,6 +4,7 @@
package view
import (
+ "fmt"
"strconv"
"time"
@@ -15,10 +16,10 @@ import (
const drainKey = "drain"
// 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.
-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
f := tview.NewForm()
@@ -63,10 +64,17 @@ func ShowDrain(view ResourceViewer, path string, opts dao.DrainOptions, okFn Dra
})
f.AddButton("OK", func() {
DismissDrain(view, pages)
- okFn(view, path, opts)
+ okFn(view, sels, opts)
})
modal := tview.NewModalForm("", f)
+ path := "Drain "
+ if len(sels) == 1 {
+ path += sels[0]
+ } else {
+ path += fmt.Sprintf("(%d) nodes", len(sels))
+ }
+ path += "?"
modal.SetText(path)
modal.SetDoneFunc(func(_ int, b string) {
DismissDrain(view, pages)
diff --git a/internal/view/exec.go b/internal/view/exec.go
index e0af76b92a..520731816b 100644
--- a/internal/view/exec.go
+++ b/internal/view/exec.go
@@ -123,12 +123,11 @@ func edit(a *App, opts shellOpts) bool {
)
for _, e := range editorEnvVars {
env := os.Getenv(e)
- if env != "" {
+ if env == "" {
continue
}
- bin, err = exec.LookPath(env)
- if err != nil {
- continue
+ if bin, err = exec.LookPath(env); err == nil {
+ break
}
}
if bin == "" {
diff --git a/internal/view/node.go b/internal/view/node.go
index e39d4091ad..1be326f12e 100644
--- a/internal/view/node.go
+++ b/internal/view/node.go
@@ -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 {
- path := n.GetTable().GetSelectedItem()
- if path == "" {
+ sels := n.GetTable().GetSelectedItems()
+ if len(sels) == 0 {
return evt
}
@@ -105,12 +105,12 @@ func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {
GracePeriodSeconds: -1,
Timeout: 5 * time.Second,
}
- ShowDrain(n, path, opts, drainNode)
+ ShowDrain(n, sels, opts, drainNode)
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())
if err != nil {
v.App().Flash().Err(err)
@@ -125,14 +125,14 @@ func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) {
v.Stop()
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 {
v.App().Flash().Err(err)
}
-
- if err := m.Drain(path, opts, d.GetWriter()); err != nil {
- v.App().Flash().Err(err)
- return
+ for _, sel := range sels {
+ if err := m.Drain(sel, opts, d.GetWriter()); err != nil {
+ v.App().Flash().Err(err)
+ }
}
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 {
return func(evt *tcell.EventKey) *tcell.EventKey {
- path := n.GetTable().GetSelectedItem()
- if path == "" {
+ sels := n.GetTable().GetSelectedItems()
+ if len(sels) == 0 {
return evt
}
@@ -151,7 +151,11 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve
} else {
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() {
res, err := dao.AccessorFor(n.App().factory, n.GVR())
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()))
return
}
- if err := m.ToggleCordon(path, cordon); err != nil {
- n.App().Flash().Err(err)
+ for _, s := range sels {
+ if err := m.ToggleCordon(s, cordon); err != nil {
+ n.App().Flash().Err(err)
+ }
}
n.Refresh()
}, func() {})
diff --git a/internal/watch/factory.go b/internal/watch/factory.go
index d21726f121..6c896afa6d 100644
--- a/internal/watch/factory.go
+++ b/internal/watch/factory.go
@@ -191,7 +191,7 @@ func (f *Factory) isClusterWide() bool {
// CanForResource return an informer is user has access.
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 {
return nil, err
}
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 918ed46a11..a8b389e7ec 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core20
-version: 'v0.31.8'
+version: 'v0.31.9'
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.