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 notesmine
parent
207d05615a
commit
f2f4077b59
2
Makefile
2
Makefile
|
|
@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
|
||||||
else
|
else
|
||||||
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
endif
|
endif
|
||||||
VERSION ?= v0.31.8
|
VERSION ?= v0.31.9
|
||||||
IMG_NAME := derailed/k9s
|
IMG_NAME := derailed/k9s
|
||||||
IMAGE := ${IMG_NAME}:${VERSION}
|
IMAGE := ${IMG_NAME}:${VERSION}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -84,7 +84,7 @@ func (a *APIClient) ConnectionOK() bool {
|
||||||
return a.connOK
|
return a.connOK
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
|
func makeSAR(ns, gvr, name string) *authorizationv1.SelfSubjectAccessReview {
|
||||||
if ns == ClusterScope {
|
if ns == ClusterScope {
|
||||||
ns = BlankNamespace
|
ns = BlankNamespace
|
||||||
}
|
}
|
||||||
|
|
@ -98,13 +98,14 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview {
|
||||||
Version: res.Version,
|
Version: res.Version,
|
||||||
Resource: res.Resource,
|
Resource: res.Resource,
|
||||||
Subresource: spec.SubResource(),
|
Subresource: spec.SubResource(),
|
||||||
|
Name: name,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeCacheKey(ns, gvr string, vv []string) string {
|
func makeCacheKey(ns, gvr, n string, vv []string) string {
|
||||||
return ns + ":" + gvr + "::" + strings.Join(vv, ",")
|
return ns + ":" + gvr + ":" + n + "::" + strings.Join(vv, ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActiveContext returns the current context name.
|
// ActiveContext returns the current context name.
|
||||||
|
|
@ -142,14 +143,14 @@ func (a *APIClient) clearCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanI checks if user has access to a certain resource.
|
// CanI checks if user has access to a certain resource.
|
||||||
func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) {
|
func (a *APIClient) CanI(ns, gvr, name string, verbs []string) (auth bool, err error) {
|
||||||
if !a.getConnOK() {
|
if !a.getConnOK() {
|
||||||
return false, errors.New("ACCESS -- No API server connection")
|
return false, errors.New("ACCESS -- No API server connection")
|
||||||
}
|
}
|
||||||
if IsClusterWide(ns) {
|
if IsClusterWide(ns) {
|
||||||
ns = BlankNamespace
|
ns = BlankNamespace
|
||||||
}
|
}
|
||||||
key := makeCacheKey(ns, gvr, verbs)
|
key := makeCacheKey(ns, gvr, name, verbs)
|
||||||
if v, ok := a.cache.Get(key); ok {
|
if v, ok := a.cache.Get(key); ok {
|
||||||
if auth, ok = v.(bool); ok {
|
if auth, ok = v.(bool); ok {
|
||||||
return auth, nil
|
return auth, nil
|
||||||
|
|
@ -160,7 +161,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
client, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr)
|
client, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr, name)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())
|
ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
@ -215,7 +216,7 @@ func (a *APIClient) IsValidNamespace(ns string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := a.CanI(ClusterScope, "v1/namespaces", []string{ListVerb})
|
ok, err := a.CanI(ClusterScope, "v1/namespaces", "", []string{ListVerb})
|
||||||
if ok && err == nil {
|
if ok && err == nil {
|
||||||
nn, _ := a.ValidNamespaceNames()
|
nn, _ := a.ValidNamespaceNames()
|
||||||
_, ok = nn[ns]
|
_, ok = nn[ns]
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ func TestMakeSAR(t *testing.T) {
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
u := uu[k]
|
u := uu[k]
|
||||||
t.Run(k, func(t *testing.T) {
|
t.Run(k, func(t *testing.T) {
|
||||||
assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr.String()))
|
assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr.String(), ""))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ func (m *MetricsServer) checkAccess(ns, gvr, msg string) error {
|
||||||
return errors.New("no metrics-server detected on cluster")
|
return errors.New("no metrics-server detected on cluster")
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := m.CanI(ns, gvr, ListAccess)
|
auth, err := m.CanI(ns, gvr, "", ListAccess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ type PodsMetricsMap map[string]*mv1beta1.PodMetrics
|
||||||
// Authorizer checks what a user can or cannot do to a resource.
|
// Authorizer checks what a user can or cannot do to a resource.
|
||||||
type Authorizer interface {
|
type Authorizer interface {
|
||||||
// CanI returns true if the user can use these actions for a given resource.
|
// CanI returns true if the user can use these actions for a given resource.
|
||||||
CanI(ns, gvr string, verbs []string) (bool, error)
|
CanI(ns, gvr, n string, verbs []string) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection represents a Kubernetes apiserver connection.
|
// Connection represents a Kubernetes apiserver connection.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,20 @@ func NewAliases() *Aliases {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Aliases) AliasesFor(s string) []string {
|
||||||
|
aa := make([]string, 0, 10)
|
||||||
|
|
||||||
|
a.mx.RLock()
|
||||||
|
defer a.mx.RUnlock()
|
||||||
|
for k, v := range a.Alias {
|
||||||
|
if v == s {
|
||||||
|
aa = append(aa, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aa
|
||||||
|
}
|
||||||
|
|
||||||
// Keys returns all aliases keys.
|
// Keys returns all aliases keys.
|
||||||
func (a *Aliases) Keys() []string {
|
func (a *Aliases) Keys() []string {
|
||||||
a.mx.RLock()
|
a.mx.RLock()
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ func NewMockConnectionWithContext(ct string) mockConnection {
|
||||||
return mockConnection{ct: ct}
|
return mockConnection{ct: ct}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mockConnection) CanI(ns, gvr string, verbs []string) (bool, error) {
|
func (m mockConnection) CanI(ns, gvr, n string, verbs []string) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
func (m mockConnection) Config() *client.Config {
|
func (m mockConnection) Config() *client.Config {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ func NewAlias(f Factory) *Alias {
|
||||||
return &a
|
return &a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Alias) AliasesFor(s string) []string {
|
||||||
|
return a.Aliases.AliasesFor(s)
|
||||||
|
}
|
||||||
|
|
||||||
// Check verifies an alias is defined for this command.
|
// Check verifies an alias is defined for this command.
|
||||||
func (a *Alias) Check(cmd string) (string, bool) {
|
func (a *Alias) Check(cmd string) (string, bool) {
|
||||||
return a.Aliases.Get(cmd)
|
return a.Aliases.Get(cmd)
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,14 @@ func (c *conn) ValidNamespaces() ([]v1.Namespace, error) { return n
|
||||||
func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) {
|
func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) {
|
||||||
return "", false, nil
|
return "", false, nil
|
||||||
}
|
}
|
||||||
func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil }
|
func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil }
|
||||||
func (c *conn) CurrentNamespaceName() (string, error) { return "", nil }
|
func (c *conn) CurrentNamespaceName() (string, error) { return "", nil }
|
||||||
func (c *conn) CanI(ns, gvr string, verbs []string) (bool, error) { return true, nil }
|
func (c *conn) CanI(ns, gvr, n string, verbs []string) (bool, error) { return true, nil }
|
||||||
func (c *conn) ActiveContext() string { return "" }
|
func (c *conn) ActiveContext() string { return "" }
|
||||||
func (c *conn) ActiveNamespace() string { return "" }
|
func (c *conn) ActiveNamespace() string { return "" }
|
||||||
func (c *conn) IsValidNamespace(string) bool { return true }
|
func (c *conn) IsValidNamespace(string) bool { return true }
|
||||||
func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
|
func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil }
|
||||||
func (c *conn) IsActiveNamespace(string) bool { return false }
|
func (c *conn) IsActiveNamespace(string) bool { return false }
|
||||||
|
|
||||||
type podFactory struct{}
|
type podFactory struct{}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ func (c *CronJob) ListImages(ctx context.Context, fqn string) ([]string, error)
|
||||||
|
|
||||||
// Run a CronJob.
|
// Run a CronJob.
|
||||||
func (c *CronJob) Run(path string) error {
|
func (c *CronJob) Run(path string) error {
|
||||||
ns, _ := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := c.Client().CanI(ns, jobGVR, []string{client.GetVerb, client.CreateVerb})
|
auth, err := c.Client().CanI(ns, jobGVR, n, []string{client.GetVerb, client.CreateVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -144,7 +144,7 @@ func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) {
|
||||||
// ToggleSuspend toggles suspend/resume on a CronJob.
|
// ToggleSuspend toggles suspend/resume on a CronJob.
|
||||||
func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
|
func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := c.Client().CanI(ns, c.GVR(), []string{client.GetVerb, client.UpdateVerb})
|
auth, err := c.Client().CanI(ns, c.GVR(), n, []string{client.GetVerb, client.UpdateVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ func (d *Deployment) IsHappy(dp appsv1.Deployment) bool {
|
||||||
// Scale a Deployment.
|
// Scale a Deployment.
|
||||||
func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {
|
func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", []string{client.GetVerb, client.UpdateVerb})
|
auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", n, []string{client.GetVerb, client.UpdateVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +91,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", []string{client.PatchVerb})
|
auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", dp.Name, []string{client.PatchVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -266,7 +266,7 @@ func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||||
// SetImages sets container images.
|
// SetImages sets container images.
|
||||||
func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb})
|
auth, err := d.Client().CanI(ns, "apps/v1/deployments", n, []string{client.PatchVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ func (d *DaemonSet) Restart(ctx context.Context, path string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", []string{client.PatchVerb})
|
auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", ds.Name, []string{client.PatchVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -285,7 +285,7 @@ func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||||
// SetImages sets container images.
|
// SetImages sets container images.
|
||||||
func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := d.Client().CanI(ns, "apps/v1/daemonset", []string{client.PatchVerb})
|
auth, err := d.Client().CanI(ns, "apps/v1/daemonset", n, []string{client.PatchVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ func (g *Generic) ToYAML(path string, showManaged bool) (string, error) {
|
||||||
// Delete deletes a resource.
|
// Delete deletes a resource.
|
||||||
func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
|
func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := g.Client().CanI(ns, g.gvrStr(), []string{client.DeleteVerb})
|
auth, err := g.Client().CanI(ns, g.gvrStr(), n, []string{client.DeleteVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {
|
||||||
dd, errs := h.GetPodsForDeletion(path)
|
dd, errs := h.GetPodsForDeletion(path)
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
for _, e := range errs {
|
for _, e := range errs {
|
||||||
if _, err := h.ErrOut.Write([]byte(e.Error() + "\n")); err != nil {
|
if _, err := h.ErrOut.Write([]byte(fmt.Sprintf("[%s] %s\n", path, e.Error()))); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,7 +247,8 @@ func (n *Node) ensureCordoned(path string) (bool, error) {
|
||||||
|
|
||||||
// FetchNode retrieves a node.
|
// FetchNode retrieves a node.
|
||||||
func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
|
func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
|
||||||
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", []string{"get"})
|
_, n := client.Namespaced(path)
|
||||||
|
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", n, []string{"get"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +272,7 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) {
|
||||||
|
|
||||||
// FetchNodes retrieves all nodes.
|
// FetchNodes retrieves all nodes.
|
||||||
func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) {
|
func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) {
|
||||||
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", []string{client.ListVerb})
|
auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", "", []string{client.ListVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
|
|
||||||
// Logs fetch container logs for a given pod and container.
|
// Logs fetch container logs for a given pod and container.
|
||||||
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
|
func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {
|
||||||
ns, _ := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := p.Client().CanI(ns, "v1/pods:log", []string{client.GetVerb})
|
auth, err := p.Client().CanI(ns, "v1/pods:log", n, []string{client.GetVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +141,6 @@ func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, er
|
||||||
return nil, fmt.Errorf("user is not authorized to view pod logs")
|
return nil, fmt.Errorf("user is not authorized to view pod logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
ns, n := client.Namespaced(path)
|
|
||||||
dial, err := p.Client().DialLogs()
|
dial, err := p.Client().DialLogs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -457,7 +456,7 @@ func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||||
// SetImages sets container images.
|
// SetImages sets container images.
|
||||||
func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := p.Client().CanI(ns, "v1/pod", []string{client.PatchVerb})
|
auth, err := p.Client().CanI(ns, "v1/pod", n, []string{client.PatchVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,140 +3,141 @@
|
||||||
|
|
||||||
package dao
|
package dao
|
||||||
|
|
||||||
import (
|
// !!BOZO!!
|
||||||
"bytes"
|
// import (
|
||||||
"context"
|
// "bytes"
|
||||||
"encoding/json"
|
// "context"
|
||||||
"errors"
|
// "encoding/json"
|
||||||
"fmt"
|
// "errors"
|
||||||
"os"
|
// "fmt"
|
||||||
"path/filepath"
|
// "os"
|
||||||
"sort"
|
// "path/filepath"
|
||||||
"time"
|
// "sort"
|
||||||
|
// "time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
// "github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
// "github.com/derailed/k9s/internal/client"
|
||||||
cfg "github.com/derailed/k9s/internal/config"
|
// cfg "github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/render"
|
// "github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/popeye/pkg"
|
// "github.com/derailed/popeye/pkg"
|
||||||
"github.com/derailed/popeye/pkg/config"
|
// "github.com/derailed/popeye/pkg/config"
|
||||||
"github.com/derailed/popeye/types"
|
// "github.com/derailed/popeye/types"
|
||||||
"github.com/rs/zerolog/log"
|
// "github.com/rs/zerolog/log"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
// "k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
// )
|
||||||
|
|
||||||
var _ Accessor = (*Popeye)(nil)
|
// var _ Accessor = (*Popeye)(nil)
|
||||||
|
|
||||||
// Popeye tracks cluster sanitization.
|
// // Popeye tracks cluster sanitization.
|
||||||
type Popeye struct {
|
// type Popeye struct {
|
||||||
NonResource
|
// NonResource
|
||||||
}
|
// }
|
||||||
|
|
||||||
// NewPopeye returns a new set of aliases.
|
// // NewPopeye returns a new set of aliases.
|
||||||
func NewPopeye(f Factory) *Popeye {
|
// func NewPopeye(f Factory) *Popeye {
|
||||||
a := Popeye{}
|
// a := Popeye{}
|
||||||
a.Init(f, client.NewGVR("popeye"))
|
// a.Init(f, client.NewGVR("popeye"))
|
||||||
|
|
||||||
return &a
|
// return &a
|
||||||
}
|
// }
|
||||||
|
|
||||||
type readWriteCloser struct {
|
// type readWriteCloser struct {
|
||||||
*bytes.Buffer
|
// *bytes.Buffer
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Close close read stream.
|
// // Close close read stream.
|
||||||
func (readWriteCloser) Close() error {
|
// func (readWriteCloser) Close() error {
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// List returns a collection of aliases.
|
// // List returns a collection of aliases.
|
||||||
func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
// func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) {
|
||||||
defer func(t time.Time) {
|
// defer func(t time.Time) {
|
||||||
log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t))
|
// log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t))
|
||||||
if err := recover(); err != nil {
|
// if err := recover(); err != nil {
|
||||||
log.Debug().Msgf("POPEYE DIED!")
|
// log.Debug().Msgf("POPEYE DIED!")
|
||||||
}
|
// }
|
||||||
}(time.Now())
|
// }(time.Now())
|
||||||
|
|
||||||
flags, js := config.NewFlags(), "json"
|
// flags, js := config.NewFlags(), "json"
|
||||||
flags.Output = &js
|
// flags.Output = &js
|
||||||
flags.ActiveNamespace = &ns
|
// flags.ActiveNamespace = &ns
|
||||||
|
|
||||||
if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
|
// if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
|
||||||
ns, n := client.Namespaced(report)
|
// ns, n := client.Namespaced(report)
|
||||||
sections := []string{n}
|
// sections := []string{n}
|
||||||
flags.Sections = §ions
|
// flags.Sections = §ions
|
||||||
flags.ActiveNamespace = &ns
|
// flags.ActiveNamespace = &ns
|
||||||
}
|
// }
|
||||||
spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml")
|
// spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml")
|
||||||
if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil {
|
// if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil {
|
||||||
spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c))
|
// spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c))
|
||||||
}
|
// }
|
||||||
if _, err := os.Stat(spinach); err == nil {
|
// if _, err := os.Stat(spinach); err == nil {
|
||||||
flags.Spinach = &spinach
|
// flags.Spinach = &spinach
|
||||||
}
|
// }
|
||||||
|
|
||||||
popeye, err := pkg.NewPopeye(flags, &log.Logger)
|
// popeye, err := pkg.NewPopeye(flags, &log.Logger)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
popeye.SetFactory(newPopeyeFactory(p.Factory))
|
// popeye.SetFactory(newPopeyeFactory(p.Factory))
|
||||||
if err = popeye.Init(); err != nil {
|
// if err = popeye.Init(); err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
|
// buff := readWriteCloser{Buffer: bytes.NewBufferString("")}
|
||||||
popeye.SetOutputTarget(buff)
|
// popeye.SetOutputTarget(buff)
|
||||||
if _, _, err = popeye.Sanitize(); err != nil {
|
// if _, _, err = popeye.Sanitize(); err != nil {
|
||||||
log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections)
|
// log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections)
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
var b render.Builder
|
// var b render.Builder
|
||||||
if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
|
// if err = json.Unmarshal(buff.Bytes(), &b); err != nil {
|
||||||
return nil, err
|
// return nil, err
|
||||||
}
|
// }
|
||||||
|
|
||||||
oo := make([]runtime.Object, 0, len(b.Report.Sections))
|
// oo := make([]runtime.Object, 0, len(b.Report.Sections))
|
||||||
sort.Sort(b.Report.Sections)
|
// sort.Sort(b.Report.Sections)
|
||||||
for _, s := range b.Report.Sections {
|
// for _, s := range b.Report.Sections {
|
||||||
s.Tally.Count = len(s.Outcome)
|
// s.Tally.Count = len(s.Outcome)
|
||||||
if s.Tally.Sum() > 0 {
|
// if s.Tally.Sum() > 0 {
|
||||||
oo = append(oo, s)
|
// oo = append(oo, s)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return oo, nil
|
// return oo, nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Get retrieves a resource.
|
// // Get retrieves a resource.
|
||||||
func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
|
// func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) {
|
||||||
return nil, errors.New("NYI!!")
|
// return nil, errors.New("NYI!!")
|
||||||
}
|
// }
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// // ----------------------------------------------------------------------------
|
||||||
// Helpers...
|
// // Helpers...
|
||||||
|
|
||||||
type popFactory struct {
|
// type popFactory struct {
|
||||||
Factory
|
// Factory
|
||||||
}
|
// }
|
||||||
|
|
||||||
var _ types.Factory = (*popFactory)(nil)
|
// var _ types.Factory = (*popFactory)(nil)
|
||||||
|
|
||||||
func newPopeyeFactory(f Factory) *popFactory {
|
// func newPopeyeFactory(f Factory) *popFactory {
|
||||||
return &popFactory{Factory: f}
|
// return &popFactory{Factory: f}
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (p *popFactory) Client() types.Connection {
|
// func (p *popFactory) Client() types.Connection {
|
||||||
return &popeyeConnection{Connection: p.Factory.Client()}
|
// return &popeyeConnection{Connection: p.Factory.Client()}
|
||||||
}
|
// }
|
||||||
|
|
||||||
type popeyeConnection struct {
|
// type popeyeConnection struct {
|
||||||
client.Connection
|
// client.Connection
|
||||||
}
|
// }
|
||||||
|
|
||||||
var _ types.Connection = (*popeyeConnection)(nil)
|
// var _ types.Connection = (*popeyeConnection)(nil)
|
||||||
|
|
||||||
func (c *popeyeConnection) Config() types.Config {
|
// func (c *popeyeConnection) Config() types.Config {
|
||||||
return c.Connection.Config()
|
// return c.Connection.Config()
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por
|
||||||
p.path, p.tunnel, p.age = path, tt, time.Now()
|
p.path, p.tunnel, p.age = path, tt, time.Now()
|
||||||
|
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := p.Client().CanI(ns, "v1/pods", []string{client.GetVerb})
|
auth, err := p.Client().CanI(ns, "v1/pods", n, []string{client.GetVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -136,7 +136,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por
|
||||||
return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase)
|
return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase)
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err = p.Client().CanI(ns, "v1/pods:portforward", []string{client.CreateVerb})
|
auth, err = p.Client().CanI(ns, "v1/pods:portforward", "", []string{client.CreateVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,10 +107,11 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
|
||||||
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
|
client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{},
|
||||||
client.NewGVR("batch/v1/jobs"): &Job{},
|
client.NewGVR("batch/v1/jobs"): &Job{},
|
||||||
client.NewGVR("v1/namespaces"): &Namespace{},
|
client.NewGVR("v1/namespaces"): &Namespace{},
|
||||||
client.NewGVR("popeye"): &Popeye{},
|
// !!BOZO!!
|
||||||
client.NewGVR("helm"): &HelmChart{},
|
//client.NewGVR("popeye"): &Popeye{},
|
||||||
client.NewGVR("helm-history"): &HelmHistory{},
|
client.NewGVR("helm"): &HelmChart{},
|
||||||
client.NewGVR("dir"): &Dir{},
|
client.NewGVR("helm-history"): &HelmHistory{},
|
||||||
|
client.NewGVR("dir"): &Dir{},
|
||||||
}
|
}
|
||||||
|
|
||||||
r, ok := m[gvr]
|
r, ok := m[gvr]
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool {
|
||||||
// Scale a StatefulSet.
|
// Scale a StatefulSet.
|
||||||
func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error {
|
func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", []string{client.GetVerb, client.UpdateVerb})
|
auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", n, []string{client.GetVerb, client.UpdateVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ns, _ := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
pp, err := podsFromSelector(s.Factory, ns, sts.Spec.Selector.MatchLabels)
|
pp, err := podsFromSelector(s.Factory, ns, sts.Spec.Selector.MatchLabels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -96,7 +96,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error {
|
||||||
s.Forwarders().Kill(client.FQN(p.Namespace, p.Name))
|
s.Forwarders().Kill(client.FQN(p.Namespace, p.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", []string{client.PatchVerb})
|
auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", n, []string{client.PatchVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +296,7 @@ func (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) {
|
||||||
// SetImages sets container images.
|
// SetImages sets container images.
|
||||||
func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := s.Client().CanI(ns, "apps/v1/statefulset", []string{client.PatchVerb})
|
auth, err := s.Client().CanI(ns, "apps/v1/statefulset", n, []string{client.PatchVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ type Workload struct {
|
||||||
func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
|
func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {
|
||||||
gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR)
|
gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR)
|
||||||
ns, n := client.Namespaced(path)
|
ns, n := client.Namespaced(path)
|
||||||
auth, err := w.Client().CanI(ns, gvr.String(), []string{client.DeleteVerb})
|
auth, err := w.Client().CanI(ns, gvr.String(), n, []string{client.DeleteVerb})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,14 +82,15 @@ var Registry = map[string]ResourceMeta{
|
||||||
DAO: &dao.Alias{},
|
DAO: &dao.Alias{},
|
||||||
Renderer: &render.Alias{},
|
Renderer: &render.Alias{},
|
||||||
},
|
},
|
||||||
"popeye": {
|
// !!BOZO!!
|
||||||
DAO: &dao.Popeye{},
|
//"popeye": {
|
||||||
Renderer: &render.Popeye{},
|
// DAO: &dao.Popeye{},
|
||||||
},
|
// Renderer: &render.Popeye{},
|
||||||
"sanitizer": {
|
//},
|
||||||
DAO: &dao.Popeye{},
|
//"sanitizer": {
|
||||||
TreeRenderer: &xray.Section{},
|
// DAO: &dao.Popeye{},
|
||||||
},
|
// TreeRenderer: &xray.Section{},
|
||||||
|
//},
|
||||||
|
|
||||||
// Core...
|
// Core...
|
||||||
"v1/endpoints": {
|
"v1/endpoints": {
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,8 @@ func (s *StatusIndicator) ClusterInfoChanged(prev, cur model.ClusterMeta) {
|
||||||
s.SetPermanent(fmt.Sprintf(
|
s.SetPermanent(fmt.Sprintf(
|
||||||
statusIndicatorFmt,
|
statusIndicatorFmt,
|
||||||
cur.K9sVer,
|
cur.K9sVer,
|
||||||
|
cur.Context,
|
||||||
cur.Cluster,
|
cur.Cluster,
|
||||||
cur.User,
|
|
||||||
cur.K8sVer,
|
cur.K8sVer,
|
||||||
AsPercDelta(prev.Cpu, cur.Cpu),
|
AsPercDelta(prev.Cpu, cur.Cpu),
|
||||||
AsPercDelta(prev.Cpu, cur.Mem),
|
AsPercDelta(prev.Cpu, cur.Mem),
|
||||||
|
|
|
||||||
|
|
@ -75,12 +75,12 @@ func hotKeyActions(r Runner, aa ui.KeyActions) error {
|
||||||
errs = errors.Join(errs, err)
|
errs = errors.Join(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, ok := aa[key]
|
if _, ok := aa[key]; ok {
|
||||||
if ok && !hk.Override {
|
if !hk.Override {
|
||||||
errs = errors.Join(errs, fmt.Errorf("duplicated hotkeys found for %q in %q", hk.ShortCut, k))
|
errs = errors.Join(errs, fmt.Errorf("duplicate hotkey found for %q in %q", hk.ShortCut, k))
|
||||||
continue
|
continue
|
||||||
} else if ok && hk.Override == true {
|
}
|
||||||
log.Info().Msgf("Action %q has been overrided by hotkey in %q", hk.ShortCut, k)
|
log.Info().Msgf("Action %q has been overridden by hotkey in %q", hk.ShortCut, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
command, err := r.EnvFn()().Substitute(hk.Command)
|
command, err := r.EnvFn()().Substitute(hk.Command)
|
||||||
|
|
@ -127,18 +127,18 @@ func pluginActions(r Runner, aa ui.KeyActions) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key, err := asKey(plugin.ShortCut)
|
key, err := asKey(plugin.ShortCut)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = errors.Join(errs, err)
|
errs = errors.Join(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, ok := aa[key]
|
if _, ok := aa[key]; ok {
|
||||||
if ok && !plugin.Override {
|
if !plugin.Override {
|
||||||
errs = errors.Join(errs, fmt.Errorf("duplicated plugin key found for %q in %q", plugin.ShortCut, k))
|
errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", plugin.ShortCut, k))
|
||||||
continue
|
continue
|
||||||
} else if ok && plugin.Override == true {
|
}
|
||||||
log.Info().Msgf("Action %q has been overrided by plugin in %q", plugin.ShortCut, k)
|
log.Info().Msgf("Action %q has been overridden by plugin in %q", plugin.ShortCut, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
aa[key] = ui.NewKeyActionWithOpts(
|
aa[key] = ui.NewKeyActionWithOpts(
|
||||||
plugin.Description,
|
plugin.Description,
|
||||||
pluginAction(r, plugin),
|
pluginAction(r, plugin),
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,8 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
|
||||||
}
|
}
|
||||||
if err := a.Config.Save(); err != nil {
|
if err := a.Config.Save(); err != nil {
|
||||||
log.Error().Err(err).Msg("config save failed!")
|
log.Error().Err(err).Msg("config save failed!")
|
||||||
|
} else {
|
||||||
|
log.Debug().Msgf("Saved context config for: %q", name)
|
||||||
}
|
}
|
||||||
a.initFactory(ns)
|
a.initFactory(ns)
|
||||||
if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil {
|
if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -398,7 +398,7 @@ func editRes(app *App, gvr client.GVR, path string) error {
|
||||||
if gvr.String() == "v1/namespaces" {
|
if gvr.String() == "v1/namespaces" {
|
||||||
ns = n
|
ns = n
|
||||||
}
|
}
|
||||||
if ok, err := app.Conn().CanI(ns, gvr.String(), []string{"patch"}); !ok || err != nil {
|
if ok, err := app.Conn().CanI(ns, gvr.String(), n, []string{"patch"}); !ok || err != nil {
|
||||||
return fmt.Errorf("current user can't edit resource %s", gvr)
|
return fmt.Errorf("current user can't edit resource %s", gvr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -423,7 +423,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
ns := b.namespaces[i]
|
ns := b.namespaces[i]
|
||||||
|
|
||||||
auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), client.ListAccess)
|
auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), "", client.ListAccess)
|
||||||
if !auth {
|
if !auth {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = fmt.Errorf("current user can't access namespace %s", ns)
|
err = fmt.Errorf("current user can't access namespace %s", ns)
|
||||||
|
|
@ -556,7 +556,7 @@ func (b *Browser) simpleDelete(selections []string, msg string) {
|
||||||
dialog.ShowConfirm(b.app.Styles.Dialog(), b.app.Content.Pages, "Confirm Delete", msg, func() {
|
dialog.ShowConfirm(b.app.Styles.Dialog(), b.app.Content.Pages, "Confirm Delete", msg, func() {
|
||||||
b.ShowDeleted()
|
b.ShowDeleted()
|
||||||
if len(selections) > 1 {
|
if len(selections) > 1 {
|
||||||
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR())
|
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR().R())
|
||||||
} else {
|
} else {
|
||||||
b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0])
|
b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ func (c *ClusterInfo) StylesChanged(s *config.Styles) {
|
||||||
func (c *ClusterInfo) hasMetrics() bool {
|
func (c *ClusterInfo) hasMetrics() bool {
|
||||||
mx := c.app.Conn().HasMetrics()
|
mx := c.app.Conn().HasMetrics()
|
||||||
if mx {
|
if mx {
|
||||||
auth, err := c.app.Conn().CanI("", "metrics.k8s.io/v1beta1/nodes", client.ListAccess)
|
auth, err := c.app.Conn().CanI("", "metrics.k8s.io/v1beta1/nodes", "", client.ListAccess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).Msgf("No nodes metrics access")
|
log.Warn().Err(err).Msgf("No nodes metrics access")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,7 @@ func NewCommand(app *App) *Command {
|
||||||
|
|
||||||
// AliasesFor gather all known aliases for a given resource.
|
// AliasesFor gather all known aliases for a given resource.
|
||||||
func (c *Command) AliasesFor(s string) []string {
|
func (c *Command) AliasesFor(s string) []string {
|
||||||
aa := make([]string, 0, 10)
|
return c.alias.AliasesFor(s)
|
||||||
for k, v := range c.alias.Alias {
|
|
||||||
if v == s {
|
|
||||||
aa = append(aa, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return aa
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the command.
|
// Init initializes the command.
|
||||||
|
|
@ -163,6 +156,13 @@ func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if context, ok := p.HasContext(); ok {
|
if context, ok := p.HasContext(); ok {
|
||||||
|
if context != c.app.Config.ActiveContextName() {
|
||||||
|
if err := c.app.Config.Save(); err != nil {
|
||||||
|
log.Error().Err(err).Msg("config save failed!")
|
||||||
|
} else {
|
||||||
|
log.Debug().Msgf("Saved context config for: %q", context)
|
||||||
|
}
|
||||||
|
}
|
||||||
res, err := dao.AccessorFor(c.app.factory, client.NewGVR("contexts"))
|
res, err := dao.AccessorFor(c.app.factory, client.NewGVR("contexts"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -15,10 +16,10 @@ import (
|
||||||
const drainKey = "drain"
|
const drainKey = "drain"
|
||||||
|
|
||||||
// DrainFunc represents a drain callback function.
|
// DrainFunc represents a drain callback function.
|
||||||
type DrainFunc func(v ResourceViewer, path string, opts dao.DrainOptions)
|
type DrainFunc func(v ResourceViewer, sels []string, opts dao.DrainOptions)
|
||||||
|
|
||||||
// ShowDrain pops a node drain dialog.
|
// ShowDrain pops a node drain dialog.
|
||||||
func ShowDrain(view ResourceViewer, path string, opts dao.DrainOptions, okFn DrainFunc) {
|
func ShowDrain(view ResourceViewer, sels []string, opts dao.DrainOptions, okFn DrainFunc) {
|
||||||
styles := view.App().Styles
|
styles := view.App().Styles
|
||||||
|
|
||||||
f := tview.NewForm()
|
f := tview.NewForm()
|
||||||
|
|
@ -63,10 +64,17 @@ func ShowDrain(view ResourceViewer, path string, opts dao.DrainOptions, okFn Dra
|
||||||
})
|
})
|
||||||
f.AddButton("OK", func() {
|
f.AddButton("OK", func() {
|
||||||
DismissDrain(view, pages)
|
DismissDrain(view, pages)
|
||||||
okFn(view, path, opts)
|
okFn(view, sels, opts)
|
||||||
})
|
})
|
||||||
|
|
||||||
modal := tview.NewModalForm("<Drain>", f)
|
modal := tview.NewModalForm("<Drain>", f)
|
||||||
|
path := "Drain "
|
||||||
|
if len(sels) == 1 {
|
||||||
|
path += sels[0]
|
||||||
|
} else {
|
||||||
|
path += fmt.Sprintf("(%d) nodes", len(sels))
|
||||||
|
}
|
||||||
|
path += "?"
|
||||||
modal.SetText(path)
|
modal.SetText(path)
|
||||||
modal.SetDoneFunc(func(_ int, b string) {
|
modal.SetDoneFunc(func(_ int, b string) {
|
||||||
DismissDrain(view, pages)
|
DismissDrain(view, pages)
|
||||||
|
|
|
||||||
|
|
@ -123,12 +123,11 @@ func edit(a *App, opts shellOpts) bool {
|
||||||
)
|
)
|
||||||
for _, e := range editorEnvVars {
|
for _, e := range editorEnvVars {
|
||||||
env := os.Getenv(e)
|
env := os.Getenv(e)
|
||||||
if env != "" {
|
if env == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bin, err = exec.LookPath(env)
|
if bin, err = exec.LookPath(env); err == nil {
|
||||||
if err != nil {
|
break
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bin == "" {
|
if bin == "" {
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,8 @@ func (n *Node) showPods(a *App, _ ui.Tabular, _ client.GVR, path string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
path := n.GetTable().GetSelectedItem()
|
sels := n.GetTable().GetSelectedItems()
|
||||||
if path == "" {
|
if len(sels) == 0 {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,12 +105,12 @@ func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
GracePeriodSeconds: -1,
|
GracePeriodSeconds: -1,
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
}
|
}
|
||||||
ShowDrain(n, path, opts, drainNode)
|
ShowDrain(n, sels, opts, drainNode)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) {
|
func drainNode(v ResourceViewer, sels []string, opts dao.DrainOptions) {
|
||||||
res, err := dao.AccessorFor(v.App().factory, v.GVR())
|
res, err := dao.AccessorFor(v.App().factory, v.GVR())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.App().Flash().Err(err)
|
v.App().Flash().Err(err)
|
||||||
|
|
@ -125,14 +125,14 @@ func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) {
|
||||||
v.Stop()
|
v.Stop()
|
||||||
defer v.Start()
|
defer v.Start()
|
||||||
{
|
{
|
||||||
d := NewDetails(v.App(), "Drain Progress", path, contentYAML, true)
|
d := NewDetails(v.App(), "Drain Progress", "nodes", contentYAML, true)
|
||||||
if err := v.App().inject(d, false); err != nil {
|
if err := v.App().inject(d, false); err != nil {
|
||||||
v.App().Flash().Err(err)
|
v.App().Flash().Err(err)
|
||||||
}
|
}
|
||||||
|
for _, sel := range sels {
|
||||||
if err := m.Drain(path, opts, d.GetWriter()); err != nil {
|
if err := m.Drain(sel, opts, d.GetWriter()); err != nil {
|
||||||
v.App().Flash().Err(err)
|
v.App().Flash().Err(err)
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
v.Refresh()
|
v.Refresh()
|
||||||
}
|
}
|
||||||
|
|
@ -140,8 +140,8 @@ func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) {
|
||||||
|
|
||||||
func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
return func(evt *tcell.EventKey) *tcell.EventKey {
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
path := n.GetTable().GetSelectedItem()
|
sels := n.GetTable().GetSelectedItems()
|
||||||
if path == "" {
|
if len(sels) == 0 {
|
||||||
return evt
|
return evt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +151,11 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve
|
||||||
} else {
|
} else {
|
||||||
title, msg = title+"Uncordon", "Uncordon "
|
title, msg = title+"Uncordon", "Uncordon "
|
||||||
}
|
}
|
||||||
msg += path + "?"
|
if len(sels) == 1 {
|
||||||
|
msg += sels[0] + "?"
|
||||||
|
} else {
|
||||||
|
msg += fmt.Sprintf("(%d) marked %s?", len(sels), n.GVR().R())
|
||||||
|
}
|
||||||
dialog.ShowConfirm(n.App().Styles.Dialog(), n.App().Content.Pages, title, msg, func() {
|
dialog.ShowConfirm(n.App().Styles.Dialog(), n.App().Content.Pages, title, msg, func() {
|
||||||
res, err := dao.AccessorFor(n.App().factory, n.GVR())
|
res, err := dao.AccessorFor(n.App().factory, n.GVR())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -163,8 +167,10 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve
|
||||||
n.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", n.GVR()))
|
n.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", n.GVR()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := m.ToggleCordon(path, cordon); err != nil {
|
for _, s := range sels {
|
||||||
n.App().Flash().Err(err)
|
if err := m.ToggleCordon(s, cordon); err != nil {
|
||||||
|
n.App().Flash().Err(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
n.Refresh()
|
n.Refresh()
|
||||||
}, func() {})
|
}, func() {})
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ func (f *Factory) isClusterWide() bool {
|
||||||
|
|
||||||
// CanForResource return an informer is user has access.
|
// CanForResource return an informer is user has access.
|
||||||
func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
|
func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
|
||||||
auth, err := f.Client().CanI(ns, gvr, verbs)
|
auth, err := f.Client().CanI(ns, gvr, "", verbs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: k9s
|
name: k9s
|
||||||
base: core20
|
base: core20
|
||||||
version: 'v0.31.8'
|
version: 'v0.31.9'
|
||||||
summary: K9s is a CLI to view and manage your Kubernetes clusters.
|
summary: K9s is a CLI to view and manage your Kubernetes clusters.
|
||||||
description: |
|
description: |
|
||||||
K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.
|
K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue