K9s/release v0.32.4 (#2637)

* [Bug] fix #2605

* [Bug] fix #2604

* [Bug] fix #2592

* [Bug] fix #2608

* [Bug] Fix #2612

* Rel v0.32.4
mine
Fernand Galiana 2024-03-20 13:00:34 -06:00 committed by derailed
parent c31a48fbee
commit d3027c8f29
26 changed files with 214 additions and 142 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.32.3
VERSION ?= v0.32.4
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,65 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.32.4
## 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!
---
## ♫ Sounds Behind The Release ♭
Thinking of all you at KubeCon Paris!!
May I suggest a nice glass of `cold Merlote` or other fine grape juices from my country?
* [Le Gorille - George Brassens](https://www.youtube.com/watch?v=KVfwvk_yVyA)
* [Les Funerailles D'antan (Love this guy!) - George Brassens](https://www.youtube.com/watch?v=bwb5k4k2EMc)
* [Poinconneur Des Lilas - Serge Gainsbourg](https://www.youtube.com/watch?v=eWkWCFzkOvU)
* [Mon Legionaire (Yup! same guy??) - Serge Gainsbourg](https://www.youtube.com/watch?v=gl8gopryqWI)
* [Les Cornichons - Nino Ferrer](https://www.youtube.com/watch?v=N7JSW4NhM8I)
* [Paris s'eveille - Jacques Dutronc](https://www.youtube.com/watch?v=3WcCg6rm3uM)
---
## 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)
---
## Resolved Issues
* [#2608](https://github.com/derailed/k9s/issues/2608) Make the sanitize feature easier to use
* [#2605](https://github.com/derailed/k9s/issues/2605) Built-in shortcuts being overridden by plugins result in excessive logging
* [#2604](https://github.com/derailed/k9s/issues/2604) Ability to mark a plugin as Dangerous/destructive
* [#2592](https://github.com/derailed/k9s/issues/2592) "list access denied" when switching contexts within k9s since 0.32.0
---
## 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!!
* [#2621](https://github.com/derailed/k9s/pull/2621) Fix snap build
---
<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

@ -217,10 +217,10 @@ func (a *APIClient) ServerVersion() (*version.Info, error) {
return info, nil
}
func (a *APIClient) IsValidNamespace(n string) bool {
ok, err := a.isValidNamespace(n)
func (a *APIClient) IsValidNamespace(ns string) bool {
ok, err := a.isValidNamespace(ns)
if err != nil {
log.Warn().Err(err).Msgf("namespace validation failed for: %q", n)
log.Warn().Err(err).Msgf("namespace validation failed for: %q", ns)
}
return ok

View File

@ -4,6 +4,7 @@
package data
import (
"os"
"sync"
"github.com/derailed/k9s/internal/client"
@ -70,6 +71,9 @@ func (c *Context) Validate(conn client.Connection, ks KubeSettings) {
c.mx.Lock()
defer c.mx.Unlock()
if a := os.Getenv(envPFAddress); a != "" {
c.PortForwardAddress = a
}
if c.PortForwardAddress == "" {
c.PortForwardAddress = defaultPFAddress()
}

View File

@ -32,6 +32,7 @@ func NewActiveNamespace(n string) *Namespace {
if n == client.BlankNamespace {
n = client.DefaultNamespace
}
return &Namespace{
Active: n,
Favorites: []string{client.DefaultNamespace},

View File

@ -14,6 +14,7 @@
"override": { "type": "boolean" },
"description": { "type": "string" },
"confirm": { "type": "boolean" },
"dangerous": { "type": "boolean" },
"scopes": {
"type": "array",
"items": { "type": "string" }

View File

@ -36,6 +36,7 @@ type Plugin struct {
Command string `yaml:"command"`
Confirm bool `yaml:"confirm"`
Background bool `yaml:"background"`
Dangerous bool `yaml:"dangerous"`
}
func (p Plugin) String() string {

View File

@ -207,19 +207,19 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)
return append(outs, tailLogs(ctx, p, opts)), nil
}
for _, co := range po.Spec.InitContainers {
o := opts.Clone()
o.Container = co.Name
outs = append(outs, tailLogs(ctx, p, o))
cfg := opts.Clone()
cfg.Container = co.Name
outs = append(outs, tailLogs(ctx, p, cfg))
}
for _, co := range po.Spec.Containers {
o := opts.Clone()
o.Container = co.Name
outs = append(outs, tailLogs(ctx, p, o))
cfg := opts.Clone()
cfg.Container = co.Name
outs = append(outs, tailLogs(ctx, p, cfg))
}
for _, co := range po.Spec.EphemeralContainers {
o := opts.Clone()
o.Container = co.Name
outs = append(outs, tailLogs(ctx, p, o))
cfg := opts.Clone()
cfg.Container = co.Name
outs = append(outs, tailLogs(ctx, p, cfg))
}
return outs, nil

View File

@ -59,16 +59,20 @@ func (s StatefulSet) Render(o interface{}, ns string, r *model1.Row) error {
podContainerNames(sts.Spec.Template.Spec, true),
podImageNames(sts.Spec.Template.Spec, true),
mapToStr(sts.Labels),
AsStatus(s.diagnose(sts.Status.Replicas, sts.Status.ReadyReplicas)),
AsStatus(s.diagnose(sts.Spec.Replicas, sts.Status.Replicas, sts.Status.ReadyReplicas)),
ToAge(sts.GetCreationTimestamp()),
}
return nil
}
func (StatefulSet) diagnose(d, r int32) error {
func (StatefulSet) diagnose(w *int32, d, r int32) error {
if d != r {
return fmt.Errorf("desiring %d replicas got %d available", d, r)
return fmt.Errorf("desired %d replicas got %d available", d, r)
}
if w != nil && *w != r {
return fmt.Errorf("want %d replicas got %d available", *w, r)
}
return nil
}

View File

@ -80,7 +80,7 @@ func hotKeyActions(r Runner, aa *ui.KeyActions) error {
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)
log.Debug().Msgf("Action %q has been overridden by hotkey in %q", hk.ShortCut, k)
}
command, err := r.EnvFn()().Substitute(hk.Command)
@ -110,7 +110,6 @@ func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler {
}
func pluginActions(r Runner, aa *ui.KeyActions) error {
pp := config.NewPlugins()
aa.Range(func(k tcell.Key, a ui.KeyAction) {
if a.Opts.Plugin {
aa.Delete(k)
@ -121,12 +120,16 @@ func pluginActions(r Runner, aa *ui.KeyActions) error {
if err != nil {
return err
}
pp := config.NewPlugins()
if err := pp.Load(path); err != nil {
return err
}
var errs error
aliases := r.Aliases()
var (
errs error
aliases = r.Aliases()
ro = r.App().Config.K9s.IsReadOnly()
)
for k, plugin := range pp.Plugins {
if !inScope(plugin.Scopes, aliases) {
continue
@ -141,15 +144,19 @@ func pluginActions(r Runner, aa *ui.KeyActions) error {
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)
log.Debug().Msgf("Action %q has been overridden by plugin in %q", plugin.ShortCut, k)
}
if plugin.Dangerous && ro {
continue
}
aa.Add(key, ui.NewKeyActionWithOpts(
plugin.Description,
pluginAction(r, plugin),
ui.ActionOpts{
Visible: true,
Plugin: true,
Visible: true,
Plugin: true,
Dangerous: plugin.Dangerous,
},
))
}

View File

@ -111,10 +111,6 @@ func (a *App) Init(version string, rate int) error {
ns := a.Config.ActiveNamespace()
a.factory = watch.NewFactory(a.Conn())
ok, err := a.isValidNS(ns)
if !ok && err == nil {
return fmt.Errorf("app-init - invalid namespace: %q", ns)
}
a.initFactory(ns)
a.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s)
@ -438,18 +434,6 @@ func (a *App) switchNS(ns string) error {
return a.factory.SetActiveNS(ns)
}
func (a *App) isValidNS(ns string) (bool, error) {
if ns == client.BlankNamespace || ns == client.NamespaceAll {
return true, nil
}
if !a.Conn().IsValidNamespace(ns) {
return false, fmt.Errorf("invalid namespace: %q", ns)
}
return true, nil
}
func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
name, ok := ci.HasContext()
if !ok || a.Config.ActiveContextName() == name {
@ -477,12 +461,13 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
}
ns := a.Config.ActiveNamespace()
if !a.Conn().IsValidNamespace(ns) {
a.Flash().Errf("Unable to validate namespace %q. Using %q namespace", ns, client.DefaultNamespace)
ns = client.DefaultNamespace
log.Warn().Msgf("Unable to validate namespace: %q. Using %q as active namespace", ns, ns)
if err := a.Config.SetActiveNamespace(ns); err != nil {
return err
}
}
a.Flash().Errf("Using %q namespace", ns)
if err := a.Config.Save(true); err != nil {
log.Error().Err(err).Msg("config save failed!")
} else {

View File

@ -168,6 +168,7 @@ func (d *Details) StylesChanged(s *config.Styles) {
// Update updates the view content.
func (d *Details) Update(buff string) *Details {
d.model.SetText(buff)
return d
}

View File

@ -53,49 +53,16 @@ func (d *Deploy) logOptions(prev bool) (*dao.LogOptions, error) {
if path == "" {
return nil, errors.New("you must provide a selection")
}
sts, err := d.dp(path)
dp, err := d.getInstance(path)
if err != nil {
return nil, err
}
cc := sts.Spec.Template.Spec.Containers
var (
co, dco string
allCos bool
)
if c, ok := dao.GetDefaultContainer(sts.Spec.Template.ObjectMeta, sts.Spec.Template.Spec); ok {
co, dco = c, c
} else if len(cc) == 1 {
co = cc[0].Name
} else {
dco, allCos = cc[0].Name, true
}
cfg := d.App().Config.K9s.Logger
opts := dao.LogOptions{
Path: path,
Container: co,
Lines: int64(cfg.TailCount),
SinceSeconds: cfg.SinceSeconds,
SingleContainer: len(cc) == 1,
AllContainers: allCos,
ShowTimestamp: cfg.ShowTime,
Previous: prev,
}
if co == "" {
opts.AllContainers = true
}
opts.DefaultContainer = dco
return &opts, nil
return podLogOptions(d.App(), path, prev, dp.ObjectMeta, dp.Spec.Template.Spec), nil
}
func (d *Deploy) showPods(app *App, model ui.Tabular, gvr client.GVR, fqn string) {
var ddp dao.Deployment
ddp.Init(d.App().factory, d.GVR())
dp, err := ddp.GetInstance(fqn)
dp, err := d.getInstance(fqn)
if err != nil {
app.Flash().Err(err)
return
@ -104,7 +71,7 @@ func (d *Deploy) showPods(app *App, model ui.Tabular, gvr client.GVR, fqn string
showPodsFromSelector(app, fqn, dp.Spec.Selector)
}
func (d *Deploy) dp(fqn string) (*appsv1.Deployment, error) {
func (d *Deploy) getInstance(fqn string) (*appsv1.Deployment, error) {
var dp dao.Deployment
dp.Init(d.App().factory, d.GVR())

View File

@ -4,9 +4,12 @@
package view
import (
"errors"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
appsv1 "k8s.io/api/apps/v1"
)
// DaemonSet represents a daemon set custom viewer.
@ -16,17 +19,16 @@ type DaemonSet struct {
// NewDaemonSet returns a new viewer.
func NewDaemonSet(gvr client.GVR) ResourceViewer {
d := DaemonSet{
ResourceViewer: NewPortForwardExtender(
NewVulnerabilityExtender(
NewRestartExtender(
NewImageExtender(
NewLogsExtender(NewBrowser(gvr), nil),
),
var d DaemonSet
d.ResourceViewer = NewPortForwardExtender(
NewVulnerabilityExtender(
NewRestartExtender(
NewImageExtender(
NewLogsExtender(NewBrowser(gvr), d.logOptions),
),
),
),
}
)
d.AddBindKeysFn(d.bindKeys)
d.GetTable().SetEnterFn(d.showPods)
@ -55,3 +57,23 @@ func (d *DaemonSet) showPods(app *App, model ui.Tabular, _ client.GVR, path stri
showPodsFromSelector(app, path, ds.Spec.Selector)
}
func (d *DaemonSet) logOptions(prev bool) (*dao.LogOptions, error) {
path := d.GetTable().GetSelectedItem()
if path == "" {
return nil, errors.New("you must provide a selection")
}
ds, err := d.getInstance(path)
if err != nil {
return nil, err
}
return podLogOptions(d.App(), path, prev, ds.ObjectMeta, ds.Spec.Template.Spec), nil
}
func (d *DaemonSet) getInstance(fqn string) (*appsv1.DaemonSet, error) {
var ds dao.DaemonSet
ds.Init(d.App().factory, client.NewGVR("apps/v1/daemonsets"))
return ds.GetInstance(fqn)
}

View File

@ -4,7 +4,10 @@
package view
import (
"errors"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
batchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -19,7 +22,11 @@ type Job struct {
// NewJob returns a new viewer.
func NewJob(gvr client.GVR) ResourceViewer {
j := Job{ResourceViewer: NewVulnerabilityExtender(NewLogsExtender(NewBrowser(gvr), nil))}
var j Job
j.ResourceViewer = NewVulnerabilityExtender(
NewLogsExtender(NewBrowser(gvr), j.logOptions),
)
j.GetTable().SetEnterFn(j.showPods)
j.GetTable().SetSortCol("AGE", true)
@ -42,3 +49,23 @@ func (*Job) showPods(app *App, model ui.Tabular, gvr client.GVR, path string) {
showPodsFromSelector(app, path, job.Spec.Selector)
}
func (j *Job) logOptions(prev bool) (*dao.LogOptions, error) {
path := j.GetTable().GetSelectedItem()
if path == "" {
return nil, errors.New("you must provide a selection")
}
job, err := j.getInstance(path)
if err != nil {
return nil, err
}
return podLogOptions(j.App(), path, prev, job.ObjectMeta, job.Spec.Template.Spec), nil
}
func (j *Job) getInstance(fqn string) (*batchv1.Job, error) {
var job dao.Job
job.Init(j.App().factory, client.NewGVR("batch/v1/jobs"))
return job.GetInstance(fqn)
}

View File

@ -8,6 +8,8 @@ import (
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/tcell/v2"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// LogsExtender adds log actions to a given viewer.
@ -91,3 +93,27 @@ func (l *LogsExtender) buildLogOpts(path, co string, prevLogs bool) *dao.LogOpti
return &opts
}
func podLogOptions(app *App, fqn string, prev bool, m metav1.ObjectMeta, spec v1.PodSpec) *dao.LogOptions {
var (
cc = fetchContainers(m, spec, true)
cfg = app.Config.K9s.Logger
opts = dao.LogOptions{
Path: fqn,
Lines: int64(cfg.TailCount),
SinceSeconds: cfg.SinceSeconds,
SingleContainer: len(cc) == 1,
ShowTimestamp: cfg.ShowTime,
Previous: prev,
}
)
if c, ok := dao.GetDefaultContainer(m, spec); ok {
opts.Container, opts.DefaultContainer = c, c
} else if len(cc) == 1 {
opts.Container = cc[0]
} else {
opts.AllContainers = true
}
return &opts
}

View File

@ -38,6 +38,7 @@ const (
trDownload = "Download"
pfIndicator = "[orange::b]Ⓕ"
defaultTxRetries = 999
magicPrompt = "Yes Please!"
)
// Pod represents a pod viewer.
@ -146,24 +147,7 @@ func (p *Pod) logOptions(prev bool) (*dao.LogOptions, error) {
return nil, err
}
cc, cfg := fetchContainers(pod.ObjectMeta, pod.Spec, true), p.App().Config.K9s.Logger
opts := dao.LogOptions{
Path: path,
Lines: int64(cfg.TailCount),
SinceSeconds: cfg.SinceSeconds,
SingleContainer: len(cc) == 1,
ShowTimestamp: cfg.ShowTime,
Previous: prev,
}
if c, ok := dao.GetDefaultContainer(pod.ObjectMeta, pod.Spec); ok {
opts.Container, opts.DefaultContainer = c, c
} else if len(cc) == 1 {
opts.Container = cc[0]
} else {
opts.AllContainers = true
}
return &opts, nil
return podLogOptions(p.App(), path, prev, pod.ObjectMeta, pod.Spec), nil
}
func (p *Pod) showContainers(app *App, _ ui.Tabular, _ client.GVR, _ string) {
@ -287,9 +271,8 @@ func (p *Pod) sanitizeCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
ack := "sanitize me pods!"
msg := fmt.Sprintf("Sanitize deletes all pods in completed/error state\nPlease enter [orange::b]%s[-::-] to proceed.", ack)
dialog.ShowConfirmAck(p.App().App, p.App().Content.Pages, ack, true, "Sanitize", msg, func() {
msg := fmt.Sprintf("Sanitize deletes all pods in completed/error state\nPlease enter [orange::b]%s[-::-] to proceed.", magicPrompt)
dialog.ShowConfirmAck(p.App().App, p.App().Content.Pages, magicPrompt, true, "Sanitize", msg, func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*p.App().Conn().Config().CallTimeout())
defer cancel()
total, err := s.Sanitize(ctx, p.GetTable().GetModel().GetNamespace())

View File

@ -42,42 +42,12 @@ func (s *StatefulSet) logOptions(prev bool) (*dao.LogOptions, error) {
if path == "" {
return nil, errors.New("you must provide a selection")
}
sts, err := s.getInstance(path)
if err != nil {
return nil, err
}
cc := sts.Spec.Template.Spec.Containers
var (
co, dco string
allCos bool
)
if c, ok := dao.GetDefaultContainer(sts.Spec.Template.ObjectMeta, sts.Spec.Template.Spec); ok {
co, dco = c, c
} else if len(cc) == 1 {
co = cc[0].Name
} else {
dco, allCos = cc[0].Name, true
}
cfg := s.App().Config.K9s.Logger
opts := dao.LogOptions{
Path: path,
Container: co,
Lines: int64(cfg.TailCount),
SingleContainer: len(cc) == 1,
SinceSeconds: cfg.SinceSeconds,
AllContainers: allCos,
ShowTimestamp: cfg.ShowTime,
Previous: prev,
}
if co == "" {
opts.AllContainers = true
}
opts.DefaultContainer = dco
return &opts, nil
return podLogOptions(s.App(), path, prev, sts.ObjectMeta, sts.Spec.Template.Spec), nil
}
func (s *StatefulSet) bindKeys(aa *ui.KeyActions) {
@ -96,5 +66,6 @@ func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _ client.GVR, path string
func (s *StatefulSet) getInstance(path string) (*appsv1.StatefulSet, error) {
var sts dao.StatefulSet
return sts.GetInstance(s.App().factory, path)
}

View File

@ -4,6 +4,7 @@ plugins:
debug:
shortCut: Shift-D
description: Add debug container
dangerous: true
scopes:
- containers
command: bash

View File

@ -4,6 +4,7 @@ plugins:
helm-purge:
shortCut: Ctrl-P
description: Helm Purge
dangerous: true
scopes:
- po
command: kubectl

View File

@ -3,6 +3,7 @@ plugins:
toggleCronjob:
shortCut: Ctrl-S
confirm: true
dangerous: true
scopes:
- cj
description: Toggle to suspend or resume a running cronjob

View File

@ -3,6 +3,7 @@ plugins:
k3d-root-shell:
shortCut: Shift-S
confirm: false
dangerous: true
description: "Root Shell"
scopes:
- containers

View File

@ -17,6 +17,7 @@ plugins:
description: Live Migrate moves VM to another compute node
# Enable confirmation dialog
confirm: true
dangerous: true
# Collections of views that support this shortcut. (You can use `all`)
scopes:
- virtualmachineinstance

View File

@ -11,6 +11,7 @@ plugins:
remove_finalizers:
shortCut: Ctrl-F
confirm: true
dangerous: true
scopes:
- all
description: |

View File

@ -3,6 +3,7 @@ plugins:
rm-ns:
shortCut: n
confirm: true
dangerous: true
description: Remove NS Finalizers
scopes:
- namespace

View File

@ -1,6 +1,6 @@
name: k9s
base: core22
version: 'v0.32.3'
version: 'v0.32.4'
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.