diff --git a/Makefile b/Makefile
index 36c29177ce..bf7861e6ac 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:
else
DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ")
endif
-VERSION ?= v0.32.0
+VERSION ?= v0.32.1
IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION}
diff --git a/change_logs/release_v0.32.1.md b/change_logs/release_v0.32.1.md
new file mode 100644
index 0000000000..6018dd45e2
--- /dev/null
+++ b/change_logs/release_v0.32.1.md
@@ -0,0 +1,51 @@
+
+
+# Release v0.32.1
+
+## 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!
+
+The aftermath ;(
+
+---
+
+## 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
+
+* [#2584](https://github.com/derailed/k9s/issues/2584) Transfer of file doesn't detect corruption
+* [#2579](https://github.com/derailed/k9s/issues/2579) Default sorting behavior changed to descending sort bug
+
+---
+
+## 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!!
+
+* [#2586](https://github.com/derailed/k9s/pull/2586) Properly initialize key actions in picker
+
+---
+
+ © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
\ No newline at end of file
diff --git a/internal/config/data/context.go b/internal/config/data/context.go
index 591f072f7b..48fbe7c3b0 100644
--- a/internal/config/data/context.go
+++ b/internal/config/data/context.go
@@ -10,9 +10,6 @@ import (
"k8s.io/client-go/tools/clientcmd/api"
)
-// DefaultPFAddress specifies the default PortForward host address.
-const DefaultPFAddress = "localhost"
-
// Context tracks K9s context configuration.
type Context struct {
ClusterName string `yaml:"cluster,omitempty"`
@@ -30,20 +27,18 @@ func NewContext() *Context {
return &Context{
Namespace: NewNamespace(),
View: NewView(),
- PortForwardAddress: DefaultPFAddress,
+ PortForwardAddress: defaultPFAddress(),
FeatureGates: NewFeatureGates(),
}
}
// NewContextFromConfig returns a config based on a kubecontext.
func NewContextFromConfig(cfg *api.Context) *Context {
- return &Context{
- Namespace: NewActiveNamespace(cfg.Namespace),
- ClusterName: cfg.Cluster,
- View: NewView(),
- PortForwardAddress: DefaultPFAddress,
- FeatureGates: NewFeatureGates(),
- }
+ ct := NewContext()
+ ct.Namespace, ct.ClusterName = NewActiveNamespace(cfg.Namespace), cfg.Cluster
+
+ return ct
+
}
// NewContextFromKubeConfig returns a new instance based on kubesettings or an error.
@@ -61,8 +56,8 @@ func (c *Context) merge(old *Context) {
return
}
c.Namespace.merge(old.Namespace)
-
}
+
func (c *Context) GetClusterName() string {
c.mx.RLock()
defer c.mx.RUnlock()
@@ -76,7 +71,7 @@ func (c *Context) Validate(conn client.Connection, ks KubeSettings) {
defer c.mx.Unlock()
if c.PortForwardAddress == "" {
- c.PortForwardAddress = DefaultPFAddress
+ c.PortForwardAddress = defaultPFAddress()
}
if cl, err := ks.CurrentClusterName(); err == nil {
c.ClusterName = cl
diff --git a/internal/config/data/helpers.go b/internal/config/data/helpers.go
index ae6cc6e9c6..5295972d65 100644
--- a/internal/config/data/helpers.go
+++ b/internal/config/data/helpers.go
@@ -11,6 +11,11 @@ import (
"regexp"
)
+const (
+ envPFAddress = "K9S_DEFAULT_PF_ADDRESS"
+ defaultPortFwdAddress = "localhost"
+)
+
var invalidPathCharsRX = regexp.MustCompile(`[:/]+`)
// SanitizeContextSubpath ensure cluster/context produces a valid path.
@@ -23,6 +28,14 @@ func SanitizeFileName(name string) string {
return invalidPathCharsRX.ReplaceAllString(name, "-")
}
+func defaultPFAddress() string {
+ if a := os.Getenv(envPFAddress); a != "" {
+ return a
+ }
+
+ return defaultPortFwdAddress
+}
+
// InList check if string is in a collection of strings.
func InList(ll []string, n string) bool {
for _, l := range ll {
diff --git a/internal/dao/registry.go b/internal/dao/registry.go
index 3aaf1bf5ac..fd19c6c4e9 100644
--- a/internal/dao/registry.go
+++ b/internal/dao/registry.go
@@ -101,7 +101,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("v1/pods"): &Pod{},
client.NewGVR("v1/nodes"): &Node{},
client.NewGVR("v1/namespaces"): &Namespace{},
- client.NewGVR("v1/configmap"): &ConfigMap{},
+ client.NewGVR("v1/configmaps"): &ConfigMap{},
client.NewGVR("v1/secrets"): &Secret{},
client.NewGVR("apps/v1/deployments"): &Deployment{},
client.NewGVR("apps/v1/daemonsets"): &DaemonSet{},
diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go
index 2b8cd096fe..10b0c3c654 100644
--- a/internal/model1/table_data.go
+++ b/internal/model1/table_data.go
@@ -376,6 +376,7 @@ func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) {
psc.Name = t.header[0].Name
}
}
+ psc.ASC = true
return psc, nil
}
diff --git a/internal/ui/dialog/transfer.go b/internal/ui/dialog/transfer.go
index 7db559c0d6..41c60e0e41 100644
--- a/internal/ui/dialog/transfer.go
+++ b/internal/ui/dialog/transfer.go
@@ -4,6 +4,7 @@
package dialog
import (
+ "strconv"
"strings"
"github.com/derailed/k9s/internal/config"
@@ -13,12 +14,19 @@ import (
const confirmKey = "confirm"
-type TransferFn func(from, to, co string, download, no_preserve bool) bool
+type TransferFn func(TransferArgs) bool
+
+type TransferArgs struct {
+ From, To, CO string
+ Download, NoPreserve bool
+ Retries int
+}
type TransferDialogOpts struct {
Containers []string
Pod string
Title, Message string
+ Retries int
Ack TransferFn
Cancel cancelFunc
}
@@ -38,44 +46,49 @@ func ShowUploads(styles config.Dialog, pages *ui.Pages, opts TransferDialogOpts)
modal := tview.NewModalForm("<"+opts.Title+">", f)
- from, to := opts.Pod, ""
+ args := TransferArgs{
+ From: opts.Pod,
+ Retries: opts.Retries,
+ }
var fromField, toField *tview.InputField
- download := true
- f.AddCheckbox("Download:", download, func(_ string, flag bool) {
+ args.Download = true
+ f.AddCheckbox("Download:", args.Download, func(_ string, flag bool) {
if flag {
modal.SetText(strings.Replace(opts.Message, "Upload", "Download", 1))
} else {
modal.SetText(strings.Replace(opts.Message, "Download", "Upload", 1))
}
- download = flag
- from, to = to, from
- fromField.SetText(from)
- toField.SetText(to)
+ args.Download = flag
+ args.From, args.To = args.To, args.From
+ fromField.SetText(args.From)
+ toField.SetText(args.To)
})
- f.AddInputField("From:", from, 40, nil, func(t string) {
- from = t
+ f.AddInputField("From:", args.From, 40, nil, func(v string) {
+ args.From = v
})
- f.AddInputField("To:", to, 40, nil, func(t string) {
- to = t
+ f.AddInputField("To:", args.To, 40, nil, func(v string) {
+ args.To = v
})
fromField, _ = f.GetFormItemByLabel("From:").(*tview.InputField)
toField, _ = f.GetFormItemByLabel("To:").(*tview.InputField)
- var no_preserve bool
- f.AddCheckbox("NoPreserve:", no_preserve, func(_ string, f bool) {
- no_preserve = f
+ f.AddCheckbox("NoPreserve:", args.NoPreserve, func(_ string, f bool) {
+ args.NoPreserve = f
})
- var co string
if len(opts.Containers) > 0 {
- co = opts.Containers[0]
+ args.CO = opts.Containers[0]
}
- f.AddInputField("Container:", co, 30, nil, func(t string) {
- co = t
+ f.AddInputField("Container:", args.CO, 30, nil, func(v string) {
+ args.CO = v
+ })
+ retries := strconv.Itoa(opts.Retries)
+ f.AddInputField("Retries:", retries, 30, nil, func(v string) {
+ retries = v
})
f.AddButton("OK", func() {
- if !opts.Ack(from, to, co, download, no_preserve) {
+ if !opts.Ack(args) {
return
}
dismissConfirm(pages)
diff --git a/internal/view/exec.go b/internal/view/exec.go
index 520731816b..e692e8b5d9 100644
--- a/internal/view/exec.go
+++ b/internal/view/exec.go
@@ -79,10 +79,13 @@ func runK(a *App, opts shellOpts) error {
}
opts.binary = bin
- suspended, errChan, _ := run(a, opts)
+ suspended, errChan, stChan := run(a, opts)
if !suspended {
return fmt.Errorf("unable to run command")
}
+ for v := range stChan {
+ log.Debug().Msgf(" - %s", v)
+ }
var errs error
for e := range errChan {
errs = errors.Join(errs, e)
@@ -474,7 +477,7 @@ func asResource(r config.Limits) v1.ResourceRequirements {
}
}
-func pipe(_ context.Context, opts shellOpts, statusChan chan<- string, w, e io.Writer, cmds ...*exec.Cmd) error {
+func pipe(_ context.Context, opts shellOpts, statusChan chan<- string, w, e *bytes.Buffer, cmds ...*exec.Cmd) error {
if len(cmds) == 0 {
return nil
}
@@ -487,6 +490,11 @@ func pipe(_ context.Context, opts shellOpts, statusChan chan<- string, w, e io.W
if err := cmd.Run(); err != nil {
log.Error().Err(err).Msgf("Command failed: %s", err)
} else {
+ for _, l := range strings.Split(w.String(), "\n") {
+ if l != "" {
+ statusChan <- fmt.Sprintf("[output] %s", l)
+ }
+ }
statusChan <- fmt.Sprintf("Command completed successfully: %q", render.Truncate(cmd.String(), 20))
log.Info().Msgf("Command completed successfully: %q", cmd.String())
}
diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go
index 8a79167ff0..27f0d29b0e 100644
--- a/internal/view/pf_dialog.go
+++ b/internal/view/pf_dialog.go
@@ -38,7 +38,6 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe
log.Error().Err(err).Msgf("No active context detected")
return
}
- address := ct.PortForwardAddress
pf, err := aa.PreferredPorts(ports)
if err != nil {
@@ -62,6 +61,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe
if loField.GetText() == "" {
loField.SetPlaceholder("Enter a local port")
}
+ address := ct.PortForwardAddress
f.AddInputField("Address:", address, fieldLen, nil, func(h string) {
address = h
})
diff --git a/internal/view/pod.go b/internal/view/pod.go
index cdd2a92428..fe223b33fe 100644
--- a/internal/view/pod.go
+++ b/internal/view/pod.go
@@ -30,13 +30,14 @@ import (
)
const (
- windowsOS = "windows"
- powerShell = "powershell"
- osSelector = "kubernetes.io/os"
- osBetaSelector = "beta." + osSelector
- trUpload = "Upload"
- trDownload = "Download"
- pfIndicator = "[orange::b]Ⓕ"
+ windowsOS = "windows"
+ powerShell = "powershell"
+ osSelector = "kubernetes.io/os"
+ osBetaSelector = "beta." + osSelector
+ trUpload = "Upload"
+ trDownload = "Download"
+ pfIndicator = "[orange::b]Ⓕ"
+ defaultTxRetries = 999
)
// Pod represents a pod viewer.
@@ -310,36 +311,36 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey {
}
ns, n := client.Namespaced(path)
- ack := func(from, to, co string, download, no_preserve bool) bool {
- local := to
- if !download {
- local = from
+ ack := func(args dialog.TransferArgs) bool {
+ local := args.To
+ if !args.Download {
+ local = args.From
}
- if _, err := os.Stat(local); !download && errors.Is(err, fs.ErrNotExist) {
+ if _, err := os.Stat(local); !args.Download && errors.Is(err, fs.ErrNotExist) {
p.App().Flash().Err(err)
return false
}
- args := make([]string, 0, 10)
- args = append(args, "cp")
- args = append(args, strings.TrimSpace(from))
- args = append(args, strings.TrimSpace(to))
- args = append(args, fmt.Sprintf("--no-preserve=%t", no_preserve))
- if co != "" {
- args = append(args, "-c="+co)
+ opts := make([]string, 0, 10)
+ opts = append(opts, "cp")
+ opts = append(opts, strings.TrimSpace(args.From))
+ opts = append(opts, strings.TrimSpace(args.To))
+ opts = append(opts, fmt.Sprintf("--no-preserve=%t", args.NoPreserve))
+ if args.CO != "" {
+ opts = append(opts, "-c="+args.CO)
}
- opts := shellOpts{
+ cliOpts := shellOpts{
background: true,
- args: args,
+ args: opts,
}
op := trUpload
- if download {
+ if args.Download {
op = trDownload
}
- fqn := path + ":" + co
- if err := runK(p.App(), opts); err != nil {
+ fqn := path + ":" + args.CO
+ if err := runK(p.App(), cliOpts); err != nil {
p.App().cowCmd(err.Error())
} else {
p.App().Flash().Infof("%s successful on %s!", op, fqn)
@@ -359,6 +360,7 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey {
Message: "Download Files",
Pod: fmt.Sprintf("%s/%s:", ns, n),
Ack: ack,
+ Retries: defaultTxRetries,
Cancel: func() {},
}
dialog.ShowUploads(p.App().Styles.Dialog(), p.App().Content.Pages, opts)
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 090c2e652d..5c2fdaa4ca 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,6 +1,6 @@
name: k9s
base: core20
-version: 'v0.32.0'
+version: 'v0.32.1'
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.