K9s/release v0.32.1 (#2591)

* [Bug] Fix #2579

* [Bug] Fix #2584

* [Exp] make pf address configurable via K9S_DEFAULT_PF_ADDRESS

* v0.32.1 release
mine
Fernand Galiana 2024-03-04 17:57:20 -07:00 committed by GitHub
parent 6c6fc22393
commit 69cd0cd707
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 146 additions and 63 deletions

View File

@ -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.32.0 VERSION ?= v0.32.1
IMG_NAME := derailed/k9s IMG_NAME := derailed/k9s
IMAGE := ${IMG_NAME}:${VERSION} IMAGE := ${IMG_NAME}:${VERSION}

View File

@ -0,0 +1,51 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# 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
---
<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

@ -10,9 +10,6 @@ import (
"k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/tools/clientcmd/api"
) )
// DefaultPFAddress specifies the default PortForward host address.
const DefaultPFAddress = "localhost"
// Context tracks K9s context configuration. // Context tracks K9s context configuration.
type Context struct { type Context struct {
ClusterName string `yaml:"cluster,omitempty"` ClusterName string `yaml:"cluster,omitempty"`
@ -30,20 +27,18 @@ func NewContext() *Context {
return &Context{ return &Context{
Namespace: NewNamespace(), Namespace: NewNamespace(),
View: NewView(), View: NewView(),
PortForwardAddress: DefaultPFAddress, PortForwardAddress: defaultPFAddress(),
FeatureGates: NewFeatureGates(), FeatureGates: NewFeatureGates(),
} }
} }
// NewContextFromConfig returns a config based on a kubecontext. // NewContextFromConfig returns a config based on a kubecontext.
func NewContextFromConfig(cfg *api.Context) *Context { func NewContextFromConfig(cfg *api.Context) *Context {
return &Context{ ct := NewContext()
Namespace: NewActiveNamespace(cfg.Namespace), ct.Namespace, ct.ClusterName = NewActiveNamespace(cfg.Namespace), cfg.Cluster
ClusterName: cfg.Cluster,
View: NewView(), return ct
PortForwardAddress: DefaultPFAddress,
FeatureGates: NewFeatureGates(),
}
} }
// NewContextFromKubeConfig returns a new instance based on kubesettings or an error. // NewContextFromKubeConfig returns a new instance based on kubesettings or an error.
@ -61,8 +56,8 @@ func (c *Context) merge(old *Context) {
return return
} }
c.Namespace.merge(old.Namespace) c.Namespace.merge(old.Namespace)
} }
func (c *Context) GetClusterName() string { func (c *Context) GetClusterName() string {
c.mx.RLock() c.mx.RLock()
defer c.mx.RUnlock() defer c.mx.RUnlock()
@ -76,7 +71,7 @@ func (c *Context) Validate(conn client.Connection, ks KubeSettings) {
defer c.mx.Unlock() defer c.mx.Unlock()
if c.PortForwardAddress == "" { if c.PortForwardAddress == "" {
c.PortForwardAddress = DefaultPFAddress c.PortForwardAddress = defaultPFAddress()
} }
if cl, err := ks.CurrentClusterName(); err == nil { if cl, err := ks.CurrentClusterName(); err == nil {
c.ClusterName = cl c.ClusterName = cl

View File

@ -11,6 +11,11 @@ import (
"regexp" "regexp"
) )
const (
envPFAddress = "K9S_DEFAULT_PF_ADDRESS"
defaultPortFwdAddress = "localhost"
)
var invalidPathCharsRX = regexp.MustCompile(`[:/]+`) var invalidPathCharsRX = regexp.MustCompile(`[:/]+`)
// SanitizeContextSubpath ensure cluster/context produces a valid path. // SanitizeContextSubpath ensure cluster/context produces a valid path.
@ -23,6 +28,14 @@ func SanitizeFileName(name string) string {
return invalidPathCharsRX.ReplaceAllString(name, "-") 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. // InList check if string is in a collection of strings.
func InList(ll []string, n string) bool { func InList(ll []string, n string) bool {
for _, l := range ll { for _, l := range ll {

View File

@ -101,7 +101,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("v1/pods"): &Pod{}, client.NewGVR("v1/pods"): &Pod{},
client.NewGVR("v1/nodes"): &Node{}, client.NewGVR("v1/nodes"): &Node{},
client.NewGVR("v1/namespaces"): &Namespace{}, client.NewGVR("v1/namespaces"): &Namespace{},
client.NewGVR("v1/configmap"): &ConfigMap{}, client.NewGVR("v1/configmaps"): &ConfigMap{},
client.NewGVR("v1/secrets"): &Secret{}, client.NewGVR("v1/secrets"): &Secret{},
client.NewGVR("apps/v1/deployments"): &Deployment{}, client.NewGVR("apps/v1/deployments"): &Deployment{},
client.NewGVR("apps/v1/daemonsets"): &DaemonSet{}, client.NewGVR("apps/v1/daemonsets"): &DaemonSet{},

View File

@ -376,6 +376,7 @@ func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) {
psc.Name = t.header[0].Name psc.Name = t.header[0].Name
} }
} }
psc.ASC = true
return psc, nil return psc, nil
} }

View File

@ -4,6 +4,7 @@
package dialog package dialog
import ( import (
"strconv"
"strings" "strings"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
@ -13,12 +14,19 @@ import (
const confirmKey = "confirm" 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 { type TransferDialogOpts struct {
Containers []string Containers []string
Pod string Pod string
Title, Message string Title, Message string
Retries int
Ack TransferFn Ack TransferFn
Cancel cancelFunc Cancel cancelFunc
} }
@ -38,44 +46,49 @@ func ShowUploads(styles config.Dialog, pages *ui.Pages, opts TransferDialogOpts)
modal := tview.NewModalForm("<"+opts.Title+">", f) modal := tview.NewModalForm("<"+opts.Title+">", f)
from, to := opts.Pod, "" args := TransferArgs{
From: opts.Pod,
Retries: opts.Retries,
}
var fromField, toField *tview.InputField var fromField, toField *tview.InputField
download := true args.Download = true
f.AddCheckbox("Download:", download, func(_ string, flag bool) { f.AddCheckbox("Download:", args.Download, func(_ string, flag bool) {
if flag { if flag {
modal.SetText(strings.Replace(opts.Message, "Upload", "Download", 1)) modal.SetText(strings.Replace(opts.Message, "Upload", "Download", 1))
} else { } else {
modal.SetText(strings.Replace(opts.Message, "Download", "Upload", 1)) modal.SetText(strings.Replace(opts.Message, "Download", "Upload", 1))
} }
download = flag args.Download = flag
from, to = to, from args.From, args.To = args.To, args.From
fromField.SetText(from) fromField.SetText(args.From)
toField.SetText(to) toField.SetText(args.To)
}) })
f.AddInputField("From:", from, 40, nil, func(t string) { f.AddInputField("From:", args.From, 40, nil, func(v string) {
from = t args.From = v
}) })
f.AddInputField("To:", to, 40, nil, func(t string) { f.AddInputField("To:", args.To, 40, nil, func(v string) {
to = t args.To = v
}) })
fromField, _ = f.GetFormItemByLabel("From:").(*tview.InputField) fromField, _ = f.GetFormItemByLabel("From:").(*tview.InputField)
toField, _ = f.GetFormItemByLabel("To:").(*tview.InputField) toField, _ = f.GetFormItemByLabel("To:").(*tview.InputField)
var no_preserve bool f.AddCheckbox("NoPreserve:", args.NoPreserve, func(_ string, f bool) {
f.AddCheckbox("NoPreserve:", no_preserve, func(_ string, f bool) { args.NoPreserve = f
no_preserve = f
}) })
var co string
if len(opts.Containers) > 0 { if len(opts.Containers) > 0 {
co = opts.Containers[0] args.CO = opts.Containers[0]
} }
f.AddInputField("Container:", co, 30, nil, func(t string) { f.AddInputField("Container:", args.CO, 30, nil, func(v string) {
co = t args.CO = v
})
retries := strconv.Itoa(opts.Retries)
f.AddInputField("Retries:", retries, 30, nil, func(v string) {
retries = v
}) })
f.AddButton("OK", func() { f.AddButton("OK", func() {
if !opts.Ack(from, to, co, download, no_preserve) { if !opts.Ack(args) {
return return
} }
dismissConfirm(pages) dismissConfirm(pages)

View File

@ -79,10 +79,13 @@ func runK(a *App, opts shellOpts) error {
} }
opts.binary = bin opts.binary = bin
suspended, errChan, _ := run(a, opts) suspended, errChan, stChan := run(a, opts)
if !suspended { if !suspended {
return fmt.Errorf("unable to run command") return fmt.Errorf("unable to run command")
} }
for v := range stChan {
log.Debug().Msgf(" - %s", v)
}
var errs error var errs error
for e := range errChan { for e := range errChan {
errs = errors.Join(errs, e) 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 { if len(cmds) == 0 {
return nil 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 { if err := cmd.Run(); err != nil {
log.Error().Err(err).Msgf("Command failed: %s", err) log.Error().Err(err).Msgf("Command failed: %s", err)
} else { } 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)) statusChan <- fmt.Sprintf("Command completed successfully: %q", render.Truncate(cmd.String(), 20))
log.Info().Msgf("Command completed successfully: %q", cmd.String()) log.Info().Msgf("Command completed successfully: %q", cmd.String())
} }

View File

@ -38,7 +38,6 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe
log.Error().Err(err).Msgf("No active context detected") log.Error().Err(err).Msgf("No active context detected")
return return
} }
address := ct.PortForwardAddress
pf, err := aa.PreferredPorts(ports) pf, err := aa.PreferredPorts(ports)
if err != nil { if err != nil {
@ -62,6 +61,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe
if loField.GetText() == "" { if loField.GetText() == "" {
loField.SetPlaceholder("Enter a local port") loField.SetPlaceholder("Enter a local port")
} }
address := ct.PortForwardAddress
f.AddInputField("Address:", address, fieldLen, nil, func(h string) { f.AddInputField("Address:", address, fieldLen, nil, func(h string) {
address = h address = h
}) })

View File

@ -37,6 +37,7 @@ const (
trUpload = "Upload" trUpload = "Upload"
trDownload = "Download" trDownload = "Download"
pfIndicator = "[orange::b]Ⓕ" pfIndicator = "[orange::b]Ⓕ"
defaultTxRetries = 999
) )
// Pod represents a pod viewer. // Pod represents a pod viewer.
@ -310,36 +311,36 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey {
} }
ns, n := client.Namespaced(path) ns, n := client.Namespaced(path)
ack := func(from, to, co string, download, no_preserve bool) bool { ack := func(args dialog.TransferArgs) bool {
local := to local := args.To
if !download { if !args.Download {
local = from 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) p.App().Flash().Err(err)
return false return false
} }
args := make([]string, 0, 10) opts := make([]string, 0, 10)
args = append(args, "cp") opts = append(opts, "cp")
args = append(args, strings.TrimSpace(from)) opts = append(opts, strings.TrimSpace(args.From))
args = append(args, strings.TrimSpace(to)) opts = append(opts, strings.TrimSpace(args.To))
args = append(args, fmt.Sprintf("--no-preserve=%t", no_preserve)) opts = append(opts, fmt.Sprintf("--no-preserve=%t", args.NoPreserve))
if co != "" { if args.CO != "" {
args = append(args, "-c="+co) opts = append(opts, "-c="+args.CO)
} }
opts := shellOpts{ cliOpts := shellOpts{
background: true, background: true,
args: args, args: opts,
} }
op := trUpload op := trUpload
if download { if args.Download {
op = trDownload op = trDownload
} }
fqn := path + ":" + co fqn := path + ":" + args.CO
if err := runK(p.App(), opts); err != nil { if err := runK(p.App(), cliOpts); err != nil {
p.App().cowCmd(err.Error()) p.App().cowCmd(err.Error())
} else { } else {
p.App().Flash().Infof("%s successful on %s!", op, fqn) 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", Message: "Download Files",
Pod: fmt.Sprintf("%s/%s:", ns, n), Pod: fmt.Sprintf("%s/%s:", ns, n),
Ack: ack, Ack: ack,
Retries: defaultTxRetries,
Cancel: func() {}, Cancel: func() {},
} }
dialog.ShowUploads(p.App().Styles.Dialog(), p.App().Content.Pages, opts) dialog.ShowUploads(p.App().Styles.Dialog(), p.App().Content.Pages, opts)

View File

@ -1,6 +1,6 @@
name: k9s name: k9s
base: core20 base: core20
version: 'v0.32.0' version: 'v0.32.1'
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.