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
|
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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
"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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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{},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
windowsOS = "windows"
|
windowsOS = "windows"
|
||||||
powerShell = "powershell"
|
powerShell = "powershell"
|
||||||
osSelector = "kubernetes.io/os"
|
osSelector = "kubernetes.io/os"
|
||||||
osBetaSelector = "beta." + osSelector
|
osBetaSelector = "beta." + osSelector
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue