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.