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
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}

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
}
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]

View File

@ -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(), ""))
})
}
}

View File

@ -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
}

View File

@ -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.

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.
func (a *Aliases) Keys() []string {
a.mx.RLock()

View File

@ -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 {

View File

@ -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)

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) {
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{}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

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.
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
}

View File

@ -3,140 +3,141 @@
package dao
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"time"
// !!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"
)
// "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)
// var _ Accessor = (*Popeye)(nil)
// Popeye tracks cluster sanitization.
type Popeye struct {
NonResource
}
// // 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"))
// // NewPopeye returns a new set of aliases.
// func NewPopeye(f Factory) *Popeye {
// a := Popeye{}
// a.Init(f, client.NewGVR("popeye"))
return &a
}
// return &a
// }
type readWriteCloser struct {
*bytes.Buffer
}
// type readWriteCloser struct {
// *bytes.Buffer
// }
// Close close read stream.
func (readWriteCloser) Close() error {
return nil
}
// // 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())
// // 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
// 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 = &sections
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
}
// if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
// ns, n := client.Namespaced(report)
// sections := []string{n}
// flags.Sections = &sections
// 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
}
// 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
}
// 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
}
// 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)
}
}
// 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
}
// return oo, nil
// }
// Get retrieves a resource.
func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!")
}
// // Get retrieves a resource.
// func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
// return nil, errors.New("NYI!!")
// }
// ----------------------------------------------------------------------------
// Helpers...
// // ----------------------------------------------------------------------------
// // Helpers...
type popFactory struct {
Factory
}
// type popFactory struct {
// Factory
// }
var _ types.Factory = (*popFactory)(nil)
// var _ types.Factory = (*popFactory)(nil)
func newPopeyeFactory(f Factory) *popFactory {
return &popFactory{Factory: f}
}
// func newPopeyeFactory(f Factory) *popFactory {
// return &popFactory{Factory: f}
// }
func (p *popFactory) Client() types.Connection {
return &popeyeConnection{Connection: p.Factory.Client()}
}
// func (p *popFactory) Client() types.Connection {
// return &popeyeConnection{Connection: p.Factory.Client()}
// }
type popeyeConnection struct {
client.Connection
}
// type popeyeConnection struct {
// client.Connection
// }
var _ types.Connection = (*popeyeConnection)(nil)
// var _ types.Connection = (*popeyeConnection)(nil)
func (c *popeyeConnection) Config() types.Config {
return c.Connection.Config()
}
// func (c *popeyeConnection) Config() types.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()
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
}

View File

@ -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]

View File

@ -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
}

View File

@ -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
}

View File

@ -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": {

View File

@ -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),

View File

@ -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),

View File

@ -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 {

View File

@ -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])
}

View File

@ -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")
}

View File

@ -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

View File

@ -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("<Drain>", 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)

View File

@ -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 == "" {

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 {
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() {})

View File

@ -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
}

View File

@ -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.