From f2f4077b592dcbb4162cbfe07bd99546d47d9955 Mon Sep 17 00:00:00 2001 From: Fernand Galiana Date: Thu, 15 Feb 2024 17:43:29 -0700 Subject: [PATCH] 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 --- Makefile | 2 +- change_logs/release_v0.31.9.md | 98 ++++++++++ internal/client/client.go | 15 +- internal/client/client_test.go | 2 +- internal/client/metrics.go | 2 +- internal/client/types.go | 2 +- internal/config/alias.go | 14 ++ internal/config/mock/test_helpers.go | 2 +- internal/dao/alias.go | 4 + internal/dao/container_test.go | 16 +- internal/dao/cronjob.go | 6 +- internal/dao/dp.go | 6 +- internal/dao/ds.go | 4 +- internal/dao/generic.go | 2 +- internal/dao/node.go | 7 +- internal/dao/pod.go | 7 +- internal/dao/popeye.go | 275 ++++++++++++++------------- internal/dao/port_forwarder.go | 4 +- internal/dao/registry.go | 9 +- internal/dao/sts.go | 8 +- internal/dao/workload.go | 2 +- internal/model/registry.go | 17 +- internal/ui/indicator.go | 2 +- internal/view/actions.go | 26 +-- internal/view/app.go | 2 + internal/view/browser.go | 6 +- internal/view/cluster_info.go | 2 +- internal/view/command.go | 16 +- internal/view/drain_dialog.go | 14 +- internal/view/exec.go | 7 +- internal/view/node.go | 34 ++-- internal/watch/factory.go | 2 +- snap/snapcraft.yaml | 2 +- 33 files changed, 376 insertions(+), 241 deletions(-) create mode 100644 change_logs/release_v0.31.9.md 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.