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 releasemine
parent
6c6fc22393
commit
69cd0cd707
2
Makefile
2
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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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{},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const (
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue