parent
463be69a9c
commit
26d1585699
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.30.2
|
VERSION ?= v0.30.3
|
||||||
IMG_NAME := derailed/k9s
|
IMG_NAME := derailed/k9s
|
||||||
IMAGE := ${IMG_NAME}:${VERSION}
|
IMAGE := ${IMG_NAME}:${VERSION}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png" align="center" width="800" height="auto"/>
|
||||||
|
|
||||||
|
# Release v0.30.3
|
||||||
|
|
||||||
|
## 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! 🎄
|
||||||
|
|
||||||
|
🎵 `On The twelfth day of Christmas my true love gave to me... More Bugs!!` 🎵
|
||||||
|
|
||||||
|
Thank you all for pitching in and help flesh out issues!!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Videos Are In The Can!
|
||||||
|
|
||||||
|
Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...
|
||||||
|
|
||||||
|
* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)
|
||||||
|
* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolved Issues
|
||||||
|
|
||||||
|
* [#2379](https://github.com/derailed/k9s/issues/2379) Filtering with equal sign (=) does not work in 0.30.X
|
||||||
|
* [#2378](https://github.com/derailed/k9s/issues/2378) Logs directory not created in the k9s config/home dir 0.30.1
|
||||||
|
* [#2377](https://github.com/derailed/k9s/issues/2377) Opening AWS EKS contexts create two directories per cluster 0.30.1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
@ -34,9 +34,14 @@ func (d Dir) Load(n string, ct *api.Context) (*Config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
path = filepath.Join(d.root, ct.Cluster, n, MainConfigFile)
|
path = filepath.Join(
|
||||||
cfg *Config
|
d.root,
|
||||||
err error
|
SanitizeFileName(ct.Cluster),
|
||||||
|
SanitizeFileName(n),
|
||||||
|
MainConfigFile,
|
||||||
|
)
|
||||||
|
cfg *Config
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
if f, e := os.Stat(path); os.IsNotExist(e) || f.Size() == 0 {
|
if f, e := os.Stat(path); os.IsNotExist(e) || f.Size() == 0 {
|
||||||
log.Debug().Msgf("Context config not found! Generating... %q", path)
|
log.Debug().Msgf("Context config not found! Generating... %q", path)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,12 @@ func TestDirLoad(t *testing.T) {
|
||||||
flags: makeFlags("cl-test", "ct-test-1"),
|
flags: makeFlags("cl-test", "ct-test-1"),
|
||||||
cfg: mustLoadConfig("testdata/configs/def_ct.yaml"),
|
cfg: mustLoadConfig("testdata/configs/def_ct.yaml"),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"non-sanitized-path": {
|
||||||
|
dir: "/tmp/data/k9s",
|
||||||
|
flags: makeFlags("arn:aws:eks:eu-central-1:xxx:cluster/fred-blee", "fred-blee"),
|
||||||
|
cfg: mustLoadConfig("testdata/configs/aws_ct.yaml"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,21 @@ package data
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var invalidPathCharsRX = regexp.MustCompile(`[:/]+`)
|
||||||
|
|
||||||
|
// SanitizeContextSubpath ensure cluster/context produces a valid path.
|
||||||
|
func SanitizeContextSubpath(cluster, context string) string {
|
||||||
|
return filepath.Join(SanitizeFileName(cluster), SanitizeFileName(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SanitizeFileName ensure file spec is valid.
|
||||||
|
func SanitizeFileName(name string) string {
|
||||||
|
return invalidPathCharsRX.ReplaceAllString(name, "-")
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
k9s:
|
||||||
|
cluster: arn:aws:eks:eu-central-1:xxx:cluster/fred-blee
|
||||||
|
namespace:
|
||||||
|
active: default
|
||||||
|
lockFavorites: false
|
||||||
|
favorites:
|
||||||
|
- default
|
||||||
|
view:
|
||||||
|
active: po
|
||||||
|
featureGates:
|
||||||
|
nodeShell: false
|
||||||
|
portForwardAddress: localhost
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config/data"
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
|
|
||||||
|
|
@ -81,19 +80,13 @@ var (
|
||||||
|
|
||||||
// InitLogsLoc initializes K9s logs location.
|
// InitLogsLoc initializes K9s logs location.
|
||||||
func InitLogLoc() error {
|
func InitLogLoc() error {
|
||||||
if hasK9sConfigEnv() {
|
tmpDir, err := userTmpDir()
|
||||||
tmpDir, err := userTmpDir()
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
AppLogFile = filepath.Join(tmpDir, K9sLogsFile)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
AppLogFile = filepath.Join(tmpDir, K9sLogsFile)
|
||||||
|
|
||||||
var err error
|
return nil
|
||||||
AppLogFile, err = xdg.StateFile(filepath.Join(AppName, K9sLogsFile))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitLocs initializes k9s artifacts locations.
|
// InitLocs initializes k9s artifacts locations.
|
||||||
|
|
@ -182,31 +175,24 @@ func initXDGLocs() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var invalidPathCharsRX = regexp.MustCompile(`[:/]+`)
|
|
||||||
|
|
||||||
// SanitizeFileName ensure file spec is valid.
|
|
||||||
func SanitizeFileName(name string) string {
|
|
||||||
return invalidPathCharsRX.ReplaceAllString(name, "-")
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppContextDir generates a valid context config dir.
|
// AppContextDir generates a valid context config dir.
|
||||||
func AppContextDir(cluster, context string) string {
|
func AppContextDir(cluster, context string) string {
|
||||||
return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context))
|
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppContextAliasesFile generates a valid context specific aliases file path.
|
// AppContextAliasesFile generates a valid context specific aliases file path.
|
||||||
func AppContextAliasesFile(cluster, context string) string {
|
func AppContextAliasesFile(cluster, context string) string {
|
||||||
return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "aliases.yaml")
|
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "aliases.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppContextPluginsFile generates a valid context specific plugins file path.
|
// AppContextPluginsFile generates a valid context specific plugins file path.
|
||||||
func AppContextPluginsFile(cluster, context string) string {
|
func AppContextPluginsFile(cluster, context string) string {
|
||||||
return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "plugins.yaml")
|
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "plugins.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppContextHotkeysFile generates a valid context specific hotkeys file path.
|
// AppContextHotkeysFile generates a valid context specific hotkeys file path.
|
||||||
func AppContextHotkeysFile(cluster, context string) string {
|
func AppContextHotkeysFile(cluster, context string) string {
|
||||||
return filepath.Join(AppContextsDir, sanContextSubpath(cluster, context), "hotkeys.yaml")
|
return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "hotkeys.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppContextConfig generates a valid context config file path.
|
// AppContextConfig generates a valid context config file path.
|
||||||
|
|
@ -216,14 +202,14 @@ func AppContextConfig(cluster, context string) string {
|
||||||
|
|
||||||
// DumpsDir generates a valid context dump directory.
|
// DumpsDir generates a valid context dump directory.
|
||||||
func DumpsDir(cluster, context string) (string, error) {
|
func DumpsDir(cluster, context string) (string, error) {
|
||||||
dir := filepath.Join(AppDumpsDir, sanContextSubpath(cluster, context))
|
dir := filepath.Join(AppDumpsDir, data.SanitizeContextSubpath(cluster, context))
|
||||||
|
|
||||||
return dir, data.EnsureDirPath(dir, data.DefaultDirMod)
|
return dir, data.EnsureDirPath(dir, data.DefaultDirMod)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureBenchmarksDir generates a valid benchmark results directory.
|
// EnsureBenchmarksDir generates a valid benchmark results directory.
|
||||||
func EnsureBenchmarksDir(cluster, context string) (string, error) {
|
func EnsureBenchmarksDir(cluster, context string) (string, error) {
|
||||||
dir := filepath.Join(AppBenchmarksDir, sanContextSubpath(cluster, context))
|
dir := filepath.Join(AppBenchmarksDir, data.SanitizeContextSubpath(cluster, context))
|
||||||
|
|
||||||
return dir, data.EnsureDirPath(dir, data.DefaultDirMod)
|
return dir, data.EnsureDirPath(dir, data.DefaultDirMod)
|
||||||
}
|
}
|
||||||
|
|
@ -274,10 +260,6 @@ func SkinFileFromName(n string) string {
|
||||||
|
|
||||||
// Helpers...
|
// Helpers...
|
||||||
|
|
||||||
func sanContextSubpath(cluster, context string) string {
|
|
||||||
return filepath.Join(SanitizeFileName(cluster), SanitizeFileName(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasK9sConfigEnv() bool {
|
func hasK9sConfigEnv() bool {
|
||||||
return os.Getenv(K9sConfigDir) != ""
|
return os.Getenv(K9sConfigDir) != ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,6 @@ import (
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SanitizeFilename sanitizes the dump filename.
|
|
||||||
func SanitizeFilename(name string) string {
|
|
||||||
return invalidPathCharsRX.ReplaceAllString(name, "-")
|
|
||||||
}
|
|
||||||
|
|
||||||
// InNSList check if ns is in an ns collection.
|
// InNSList check if ns is in an ns collection.
|
||||||
func InNSList(nn []interface{}, ns string) bool {
|
func InNSList(nn []interface{}, ns string) bool {
|
||||||
ss := make([]string, len(nn))
|
ss := make([]string, len(nn))
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save saves the k9s config to dis.
|
||||||
func (k *K9s) Save() error {
|
func (k *K9s) Save() error {
|
||||||
if k.activeConfig != nil {
|
if k.activeConfig != nil {
|
||||||
path := filepath.Join(
|
path := filepath.Join(
|
||||||
|
|
@ -70,6 +71,7 @@ func (k *K9s) Save() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refine merges k9s configs.
|
||||||
func (k *K9s) Refine(k1 *K9s) {
|
func (k *K9s) Refine(k1 *K9s) {
|
||||||
k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh
|
k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh
|
||||||
k.ScreenDumpDir = k1.ScreenDumpDir
|
k.ScreenDumpDir = k1.ScreenDumpDir
|
||||||
|
|
@ -86,6 +88,7 @@ func (k *K9s) Refine(k1 *K9s) {
|
||||||
k.Thresholds = k1.Thresholds
|
k.Thresholds = k1.Thresholds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override overrides k9s config from cli args.
|
||||||
func (k *K9s) Override(k9sFlags *Flags) {
|
func (k *K9s) Override(k9sFlags *Flags) {
|
||||||
if *k9sFlags.RefreshRate != DefaultRefreshRate {
|
if *k9sFlags.RefreshRate != DefaultRefreshRate {
|
||||||
k.OverrideRefreshRate(*k9sFlags.RefreshRate)
|
k.OverrideRefreshRate(*k9sFlags.RefreshRate)
|
||||||
|
|
@ -105,6 +108,7 @@ func (k *K9s) OverrideScreenDumpDir(dir string) {
|
||||||
k.manualScreenDumpDir = &dir
|
k.manualScreenDumpDir = &dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetScreenDumpDir fetch screen dumps dir.
|
||||||
func (k *K9s) GetScreenDumpDir() string {
|
func (k *K9s) GetScreenDumpDir() string {
|
||||||
screenDumpDir := k.ScreenDumpDir
|
screenDumpDir := k.ScreenDumpDir
|
||||||
if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" {
|
if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" {
|
||||||
|
|
@ -117,21 +121,29 @@ func (k *K9s) GetScreenDumpDir() string {
|
||||||
return screenDumpDir
|
return screenDumpDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset resets configuration and context.
|
||||||
func (k *K9s) Reset() {
|
func (k *K9s) Reset() {
|
||||||
k.activeConfig, k.activeContextName = nil, ""
|
k.activeConfig, k.activeContextName = nil, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActiveScreenDumpsDir fetch context specific screen dumps dir.
|
||||||
|
func (k *K9s) ActiveScreenDumpsDir() string {
|
||||||
|
return filepath.Join(k.GetScreenDumpDir(), k.ActiveContextDir())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveContextDir fetch current cluster/context path.
|
||||||
func (k *K9s) ActiveContextDir() string {
|
func (k *K9s) ActiveContextDir() string {
|
||||||
if k.activeConfig == nil {
|
if k.activeConfig == nil {
|
||||||
return "na"
|
return "na"
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(
|
return data.SanitizeContextSubpath(
|
||||||
SanitizeFileName(k.activeConfig.Context.ClusterName),
|
k.activeConfig.Context.ClusterName,
|
||||||
SanitizeFileName(k.ActiveContextName()),
|
k.ActiveContextName(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActiveContextNamespace fetch the context active ns.
|
||||||
func (k *K9s) ActiveContextNamespace() (string, error) {
|
func (k *K9s) ActiveContextNamespace() (string, error) {
|
||||||
if k.activeConfig != nil {
|
if k.activeConfig != nil {
|
||||||
return k.activeConfig.Context.Namespace.Active, nil
|
return k.activeConfig.Context.Namespace.Active, nil
|
||||||
|
|
@ -140,6 +152,7 @@ func (k *K9s) ActiveContextNamespace() (string, error) {
|
||||||
return "", errors.New("context config is not set")
|
return "", errors.New("context config is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActiveContextName returns the active context name.
|
||||||
func (k *K9s) ActiveContextName() string {
|
func (k *K9s) ActiveContextName() string {
|
||||||
return k.activeContextName
|
return k.activeContextName
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ func NewMockKubeSettings(f *genericclioptions.ConfigFlags) mockKubeSettings {
|
||||||
Cluster: *f.ClusterName,
|
Cluster: *f.ClusterName,
|
||||||
Namespace: client.DefaultNamespace,
|
Namespace: client.DefaultNamespace,
|
||||||
},
|
},
|
||||||
|
"fred-blee": {
|
||||||
|
Cluster: "arn:aws:eks:eu-central-1:xxx:cluster/fred-blee",
|
||||||
|
Namespace: client.DefaultNamespace,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,11 @@ func (p Pod) Render(o interface{}, ns string, row *Row) error {
|
||||||
cs := po.Status.ContainerStatuses
|
cs := po.Status.ContainerStatuses
|
||||||
cr, _, rc := p.Statuses(cs)
|
cr, _, rc := p.Statuses(cs)
|
||||||
|
|
||||||
c, r := p.gatherPodMX(&po, pwm.MX)
|
var ccmx []mv1beta1.ContainerMetrics
|
||||||
|
if pwm.MX != nil {
|
||||||
|
ccmx = pwm.MX.Containers
|
||||||
|
}
|
||||||
|
c, r := gatherCoMX(po.Spec.Containers, ccmx)
|
||||||
phase := p.Phase(&po)
|
phase := p.Phase(&po)
|
||||||
row.ID = client.MetaFQN(po.ObjectMeta)
|
row.ID = client.MetaFQN(po.ObjectMeta)
|
||||||
row.Fields = Fields{
|
row.Fields = Fields{
|
||||||
|
|
@ -232,22 +236,20 @@ func (p *PodWithMetrics) DeepCopyObject() runtime.Object {
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, r metric) {
|
func gatherCoMX(cc []v1.Container, ccmx []mv1beta1.ContainerMetrics) (c, r metric) {
|
||||||
rcpu, rmem := podRequests(pod.Spec.Containers)
|
rcpu, rmem := cosRequests(cc)
|
||||||
r.cpu, r.mem = rcpu.MilliValue(), rmem.Value()
|
r.cpu, r.mem = rcpu.MilliValue(), rmem.Value()
|
||||||
|
|
||||||
lcpu, lmem := podLimits(pod.Spec.Containers)
|
lcpu, lmem := cosLimits(cc)
|
||||||
r.lcpu, r.lmem = lcpu.MilliValue(), lmem.Value()
|
r.lcpu, r.lmem = lcpu.MilliValue(), lmem.Value()
|
||||||
|
|
||||||
if mx != nil {
|
ccpu, cmem := currentRes(ccmx)
|
||||||
ccpu, cmem := currentRes(mx)
|
c.cpu, c.mem = ccpu.MilliValue(), cmem.Value()
|
||||||
c.cpu, c.mem = ccpu.MilliValue(), cmem.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func podLimits(cc []v1.Container) (resource.Quantity, resource.Quantity) {
|
func cosLimits(cc []v1.Container) (resource.Quantity, resource.Quantity) {
|
||||||
cpu, mem := new(resource.Quantity), new(resource.Quantity)
|
cpu, mem := new(resource.Quantity), new(resource.Quantity)
|
||||||
for _, c := range cc {
|
for _, c := range cc {
|
||||||
limits := c.Resources.Limits
|
limits := c.Resources.Limits
|
||||||
|
|
@ -264,7 +266,7 @@ func podLimits(cc []v1.Container) (resource.Quantity, resource.Quantity) {
|
||||||
return *cpu, *mem
|
return *cpu, *mem
|
||||||
}
|
}
|
||||||
|
|
||||||
func podRequests(cc []v1.Container) (resource.Quantity, resource.Quantity) {
|
func cosRequests(cc []v1.Container) (resource.Quantity, resource.Quantity) {
|
||||||
cpu, mem := new(resource.Quantity), new(resource.Quantity)
|
cpu, mem := new(resource.Quantity), new(resource.Quantity)
|
||||||
for _, c := range cc {
|
for _, c := range cc {
|
||||||
co := c
|
co := c
|
||||||
|
|
@ -280,12 +282,12 @@ func podRequests(cc []v1.Container) (resource.Quantity, resource.Quantity) {
|
||||||
return *cpu, *mem
|
return *cpu, *mem
|
||||||
}
|
}
|
||||||
|
|
||||||
func currentRes(mx *mv1beta1.PodMetrics) (resource.Quantity, resource.Quantity) {
|
func currentRes(ccmx []mv1beta1.ContainerMetrics) (resource.Quantity, resource.Quantity) {
|
||||||
cpu, mem := new(resource.Quantity), new(resource.Quantity)
|
cpu, mem := new(resource.Quantity), new(resource.Quantity)
|
||||||
if mx == nil {
|
if ccmx == nil {
|
||||||
return *cpu, *mem
|
return *cpu, *mem
|
||||||
}
|
}
|
||||||
for _, co := range mx.Containers {
|
for _, co := range ccmx {
|
||||||
c, m := co.Usage.Cpu(), co.Usage.Memory()
|
c, m := co.Usage.Cpu(), co.Usage.Memory()
|
||||||
cpu.Add(*c)
|
cpu.Add(*c)
|
||||||
mem.Add(*m)
|
mem.Add(*m)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// Copyright Authors of K9s
|
||||||
|
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
res "k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_gatherPodMx(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
cc []v1.Container
|
||||||
|
mx []mv1beta1.ContainerMetrics
|
||||||
|
c, r metric
|
||||||
|
perc string
|
||||||
|
}{
|
||||||
|
"single": {
|
||||||
|
cc: []v1.Container{
|
||||||
|
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
|
||||||
|
},
|
||||||
|
mx: []mv1beta1.ContainerMetrics{
|
||||||
|
makeCoMX("c1", "1m", "22Mi"),
|
||||||
|
},
|
||||||
|
c: metric{
|
||||||
|
cpu: 1,
|
||||||
|
mem: 22 * client.MegaByte,
|
||||||
|
},
|
||||||
|
r: metric{
|
||||||
|
cpu: 10,
|
||||||
|
mem: 1 * client.MegaByte,
|
||||||
|
lcpu: 20,
|
||||||
|
lmem: 2 * client.MegaByte,
|
||||||
|
},
|
||||||
|
perc: "10",
|
||||||
|
},
|
||||||
|
"multi": {
|
||||||
|
cc: []v1.Container{
|
||||||
|
makeContainer("c1", false, "11m", "22Mi", "111m", "44Mi"),
|
||||||
|
makeContainer("c2", false, "93m", "1402Mi", "0m", "2804Mi"),
|
||||||
|
makeContainer("c3", false, "11m", "34Mi", "0m", "69Mi"),
|
||||||
|
},
|
||||||
|
r: metric{
|
||||||
|
cpu: 11 + 93 + 11,
|
||||||
|
mem: (22 + 1402 + 34) * client.MegaByte,
|
||||||
|
lcpu: 111 + 0 + 0,
|
||||||
|
lmem: (44 + 2804 + 69) * client.MegaByte,
|
||||||
|
},
|
||||||
|
mx: []mv1beta1.ContainerMetrics{
|
||||||
|
makeCoMX("c1", "1m", "22Mi"),
|
||||||
|
makeCoMX("c2", "51m", "1275Mi"),
|
||||||
|
makeCoMX("c3", "1m", "27Mi"),
|
||||||
|
},
|
||||||
|
c: metric{
|
||||||
|
cpu: 1 + 51 + 1,
|
||||||
|
mem: (22 + 1275 + 27) * client.MegaByte,
|
||||||
|
},
|
||||||
|
perc: "46",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c, r := gatherCoMX(u.cc, u.mx)
|
||||||
|
assert.Equal(t, u.c.cpu, c.cpu)
|
||||||
|
assert.Equal(t, u.c.mem, c.mem)
|
||||||
|
assert.Equal(t, u.c.lcpu, c.lcpu)
|
||||||
|
assert.Equal(t, u.c.lmem, c.lmem)
|
||||||
|
|
||||||
|
assert.Equal(t, u.r.cpu, r.cpu)
|
||||||
|
assert.Equal(t, u.r.mem, r.mem)
|
||||||
|
assert.Equal(t, u.r.lcpu, r.lcpu)
|
||||||
|
assert.Equal(t, u.r.lmem, r.lmem)
|
||||||
|
|
||||||
|
assert.Equal(t, u.perc, client.ToPercentageStr(c.cpu, r.cpu))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_podLimits(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
cc []v1.Container
|
||||||
|
l v1.ResourceList
|
||||||
|
}{
|
||||||
|
"plain": {
|
||||||
|
cc: []v1.Container{
|
||||||
|
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
|
||||||
|
},
|
||||||
|
l: makeRes("20m", "2Mi"),
|
||||||
|
},
|
||||||
|
"multi-co": {
|
||||||
|
cc: []v1.Container{
|
||||||
|
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
|
||||||
|
makeContainer("c2", false, "10m", "1Mi", "40m", "4Mi"),
|
||||||
|
},
|
||||||
|
l: makeRes("60m", "6Mi"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c, m := cosLimits(u.cc)
|
||||||
|
assert.True(t, c.Equal(*u.l.Cpu()))
|
||||||
|
assert.True(t, m.Equal(*u.l.Memory()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_podRequests(t *testing.T) {
|
||||||
|
uu := map[string]struct {
|
||||||
|
cc []v1.Container
|
||||||
|
l v1.ResourceList
|
||||||
|
}{
|
||||||
|
"plain": {
|
||||||
|
cc: []v1.Container{
|
||||||
|
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
|
||||||
|
},
|
||||||
|
l: makeRes("10m", "1Mi"),
|
||||||
|
},
|
||||||
|
"multi-co": {
|
||||||
|
cc: []v1.Container{
|
||||||
|
makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"),
|
||||||
|
makeContainer("c2", false, "10m", "1Mi", "40m", "4Mi"),
|
||||||
|
},
|
||||||
|
l: makeRes("20m", "2Mi"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range uu {
|
||||||
|
u := uu[k]
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
c, m := cosRequests(u.cc)
|
||||||
|
assert.True(t, c.Equal(*u.l.Cpu()))
|
||||||
|
assert.True(t, m.Equal(*u.l.Memory()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers...
|
||||||
|
|
||||||
|
func makeContainer(n string, init bool, rc, rm, lc, lm string) v1.Container {
|
||||||
|
var res v1.ResourceRequirements
|
||||||
|
if init {
|
||||||
|
res = v1.ResourceRequirements{}
|
||||||
|
} else {
|
||||||
|
res = v1.ResourceRequirements{
|
||||||
|
Requests: makeRes(rc, rm),
|
||||||
|
Limits: makeRes(lc, lm),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v1.Container{Name: n, Resources: res}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeRes(c, m string) v1.ResourceList {
|
||||||
|
cpu, _ := res.ParseQuantity(c)
|
||||||
|
mem, _ := res.ParseQuantity(m)
|
||||||
|
|
||||||
|
return v1.ResourceList{
|
||||||
|
v1.ResourceCPU: cpu,
|
||||||
|
v1.ResourceMemory: mem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoMX(n string, c, m string) mv1beta1.ContainerMetrics {
|
||||||
|
return mv1beta1.ContainerMetrics{
|
||||||
|
Name: n,
|
||||||
|
Usage: makeRes(c, m),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -281,123 +281,3 @@ func makeRes(c, m string) v1.ResourceList {
|
||||||
v1.ResourceMemory: mem,
|
v1.ResourceMemory: mem,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiVersion: v1
|
|
||||||
// kind: Pod
|
|
||||||
// metadata:
|
|
||||||
// creationTimestamp: "2023-11-11T17:01:40Z"
|
|
||||||
// finalizers:
|
|
||||||
// - batch.kubernetes.io/job-tracking
|
|
||||||
// generateName: hello-28328646-
|
|
||||||
// labels:
|
|
||||||
// batch.kubernetes.io/controller-uid: 35cf5552-7180-48c1-b7b2-8b6e630a7860
|
|
||||||
// batch.kubernetes.io/job-name: hello-28328646
|
|
||||||
// controller-uid: 35cf5552-7180-48c1-b7b2-8b6e630a7860
|
|
||||||
// job-name: hello-28328646
|
|
||||||
// name: hello-28328646-h9fnh
|
|
||||||
// namespace: fred
|
|
||||||
// ownerReferences:
|
|
||||||
// - apiVersion: batch/v1
|
|
||||||
// blockOwnerDeletion: true
|
|
||||||
// controller: true
|
|
||||||
// kind: Job
|
|
||||||
// name: hello-28328646
|
|
||||||
// uid: 35cf5552-7180-48c1-b7b2-8b6e630a7860
|
|
||||||
// resourceVersion: "381637"
|
|
||||||
// uid: ea77c360-6375-459b-8b30-2ac0c59404cd
|
|
||||||
// spec:
|
|
||||||
// containers:
|
|
||||||
// - args:
|
|
||||||
// - /bin/bash
|
|
||||||
// - -c
|
|
||||||
// - for i in {1..5}; do echo "hello";sleep 1; done
|
|
||||||
// image: blang/busybox-bash
|
|
||||||
// imagePullPolicy: Always
|
|
||||||
// name: c1
|
|
||||||
// resources: {}
|
|
||||||
// terminationMessagePath: /dev/termination-log
|
|
||||||
// terminationMessagePolicy: File
|
|
||||||
// volumeMounts:
|
|
||||||
// - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
|
|
||||||
// name: kube-api-access-7sztm
|
|
||||||
// readOnly: true
|
|
||||||
// dnsPolicy: ClusterFirst
|
|
||||||
// enableServiceLinks: true
|
|
||||||
// nodeName: kind-worker
|
|
||||||
// preemptionPolicy: PreemptLowerPriority
|
|
||||||
// priority: 0
|
|
||||||
// restartPolicy: OnFailure
|
|
||||||
// schedulerName: default-scheduler
|
|
||||||
// securityContext: {}
|
|
||||||
// serviceAccount: default
|
|
||||||
// serviceAccountName: default
|
|
||||||
// terminationGracePeriodSeconds: 30
|
|
||||||
// tolerations:
|
|
||||||
// - effect: NoExecute
|
|
||||||
// key: node.kubernetes.io/not-ready
|
|
||||||
// operator: Exists
|
|
||||||
// tolerationSeconds: 300
|
|
||||||
// - effect: NoExecute
|
|
||||||
// key: node.kubernetes.io/unreachable
|
|
||||||
// operator: Exists
|
|
||||||
// tolerationSeconds: 300
|
|
||||||
// volumes:
|
|
||||||
// - name: kube-api-access-7sztm
|
|
||||||
// projected:
|
|
||||||
// defaultMode: 420
|
|
||||||
// sources:
|
|
||||||
// - serviceAccountToken:
|
|
||||||
// expirationSeconds: 3607
|
|
||||||
// path: token
|
|
||||||
// - configMap:
|
|
||||||
// items:
|
|
||||||
// - key: ca.crt
|
|
||||||
// path: ca.crt
|
|
||||||
// name: kube-root-ca.crt
|
|
||||||
// - downwardAPI:
|
|
||||||
// items:
|
|
||||||
// - fieldRef:
|
|
||||||
// apiVersion: v1
|
|
||||||
// fieldPath: metadata.namespace
|
|
||||||
// path: namespace
|
|
||||||
// status:
|
|
||||||
// conditions:
|
|
||||||
// - lastProbeTime: null
|
|
||||||
// lastTransitionTime: "2023-11-11T17:01:40Z"
|
|
||||||
// status: "True"
|
|
||||||
// type: Initialized
|
|
||||||
// - lastProbeTime: null
|
|
||||||
// lastTransitionTime: "2023-11-11T17:01:40Z"
|
|
||||||
// message: 'containers with unready status: [c1[]'
|
|
||||||
// reason: ContainersNotReady
|
|
||||||
// status: "False"
|
|
||||||
// type: Ready
|
|
||||||
// - lastProbeTime: null
|
|
||||||
// lastTransitionTime: "2023-11-11T17:01:40Z"
|
|
||||||
// message: 'containers with unready status: [c1[]'
|
|
||||||
// reason: ContainersNotReady
|
|
||||||
// status: "False"
|
|
||||||
// type: ContainersReady
|
|
||||||
// - lastProbeTime: null
|
|
||||||
// lastTransitionTime: "2023-11-11T17:01:40Z"
|
|
||||||
// status: "True"
|
|
||||||
// type: PodScheduled
|
|
||||||
// containerStatuses:
|
|
||||||
// - image: blang/busybox-bash
|
|
||||||
// imageID: ""
|
|
||||||
// lastState: {}
|
|
||||||
// name: c1
|
|
||||||
// ready: false
|
|
||||||
// restartCount: 0
|
|
||||||
// started: false
|
|
||||||
// state:
|
|
||||||
// waiting:
|
|
||||||
// message: Back-off pulling image "blang/busybox-bash"
|
|
||||||
// reason: ImagePullBackOff
|
|
||||||
// hostIP: 172.18.0.3
|
|
||||||
// phase: Pending
|
|
||||||
// podIP: 10.244.1.59
|
|
||||||
// podIPs:
|
|
||||||
// - ip: 10.244.1.59
|
|
||||||
// qosClass: BestEffort
|
|
||||||
// startTime: "2023-11-11T17:01:40Z"
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
|
"github.com/derailed/k9s/internal/view/cmd"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/sahilm/fuzzy"
|
"github.com/sahilm/fuzzy"
|
||||||
)
|
)
|
||||||
|
|
@ -41,11 +42,9 @@ const (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// LabelRx identifies a label query.
|
// LabelRx identifies a label query.
|
||||||
LabelRx = regexp.MustCompile(`\A\-l`)
|
LabelRx = regexp.MustCompile(`\A\-l`)
|
||||||
|
|
||||||
inverseRx = regexp.MustCompile(`\A\!`)
|
inverseRx = regexp.MustCompile(`\A\!`)
|
||||||
|
fuzzyRx = regexp.MustCompile(`\A\-f`)
|
||||||
fuzzyRx = regexp.MustCompile(`\A\-f`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustExtractStyles(ctx context.Context) *config.Styles {
|
func mustExtractStyles(ctx context.Context) *config.Styles {
|
||||||
|
|
@ -71,12 +70,11 @@ func IsLabelSelector(s string) bool {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if LabelRx.MatchString(s) {
|
if LabelRx.MatchString(s) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return !strings.Contains(s, " ") && strings.Contains(s, "=")
|
return !strings.Contains(s, " ") && cmd.ToLabels(s) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFuzzySelector checks if query is fuzzy.
|
// IsFuzzySelector checks if query is fuzzy.
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,13 @@ func TestIsLabelSelector(t *testing.T) {
|
||||||
s string
|
s string
|
||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
"empty": {s: ""},
|
"empty": {s: ""},
|
||||||
"cool": {s: "-l app=fred,env=blee", ok: true},
|
"cool": {s: "-l app=fred,env=blee", ok: true},
|
||||||
"no-flag": {s: "app=fred,env=blee", ok: true},
|
"no-flag": {s: "app=fred,env=blee", ok: true},
|
||||||
"no-space": {s: "-lapp=fred,env=blee", ok: true},
|
"no-space": {s: "-lapp=fred,env=blee", ok: true},
|
||||||
"wrong-flag": {s: "-f app=fred,env=blee"},
|
"wrong-flag": {s: "-f app=fred,env=blee"},
|
||||||
|
"missing-key": {s: "=fred"},
|
||||||
|
"missing-val": {s: "fred="},
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range uu {
|
for k := range uu {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/derailed/tcell/v2"
|
"github.com/derailed/tcell/v2"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -74,8 +75,8 @@ func benchDir(cfg *config.Config) string {
|
||||||
}
|
}
|
||||||
return filepath.Join(
|
return filepath.Join(
|
||||||
config.AppBenchmarksDir,
|
config.AppBenchmarksDir,
|
||||||
config.SanitizeFileName(ct.ClusterName),
|
data.SanitizeFileName(ct.ClusterName),
|
||||||
config.SanitizeFilename(cfg.K9s.ActiveContextName()),
|
data.SanitizeFileName(cfg.K9s.ActiveContextName()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToLabels(s string) map[string]string {
|
func ToLabels(s string) map[string]string {
|
||||||
|
|
@ -74,7 +73,6 @@ func SuggestSubCommand(command string, namespaces client.NamespaceNames, context
|
||||||
if n, ok := p.HasContext(); ok {
|
if n, ok := p.HasContext(); ok {
|
||||||
suggests = completeCtx(n, contexts)
|
suggests = completeCtx(n, contexts)
|
||||||
}
|
}
|
||||||
log.Debug().Msgf("!!SUGG CTX!! %#v", suggests)
|
|
||||||
if len(suggests) > 0 {
|
if len(suggests) > 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +82,6 @@ func SuggestSubCommand(command string, namespaces client.NamespaceNames, context
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
suggests = completeNS(ns, namespaces)
|
suggests = completeNS(ns, namespaces)
|
||||||
log.Debug().Msgf("!!SUGG NS!! %#v", suggests)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if n, ok := p.HasContext(); ok {
|
if n, ok := p.HasContext(); ok {
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if path, err := saveYAML(d.app.Config.K9s.GetScreenDumpDir(), d.app.Config.K9s.ActiveContextDir(), d.title, d.text.GetText(true)); err != nil {
|
if path, err := saveYAML(d.app.Config.K9s.ActiveScreenDumpsDir(), d.title, d.text.GetText(true)); err != nil {
|
||||||
d.app.Flash().Err(err)
|
d.app.Flash().Err(err)
|
||||||
} else {
|
} else {
|
||||||
d.app.Flash().Infof("Log %s saved successfully!", path)
|
d.app.Flash().Infof("Log %s saved successfully!", path)
|
||||||
|
|
|
||||||
|
|
@ -357,7 +357,7 @@ func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
func (v *LiveView) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (v *LiveView) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
name := fmt.Sprintf("%s--%s", strings.Replace(v.model.GetPath(), "/", "-", 1), strings.ToLower(v.title))
|
name := fmt.Sprintf("%s--%s", strings.Replace(v.model.GetPath(), "/", "-", 1), strings.ToLower(v.title))
|
||||||
if _, err := saveYAML(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.ActiveContextDir(), name, sanitizeEsc(v.text.GetText(true))); err != nil {
|
if _, err := saveYAML(v.app.Config.K9s.ActiveScreenDumpsDir(), name, sanitizeEsc(v.text.GetText(true))); err != nil {
|
||||||
v.app.Flash().Err(err)
|
v.app.Flash().Err(err)
|
||||||
} else {
|
} else {
|
||||||
v.app.Flash().Infof("File %q saved successfully!", name)
|
v.app.Flash().Infof("File %q saved successfully!", name)
|
||||||
|
|
|
||||||
|
|
@ -406,7 +406,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
// SaveCmd dumps the logs to file.
|
// SaveCmd dumps the logs to file.
|
||||||
func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey {
|
func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey {
|
||||||
path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.ActiveContextDir(), l.model.GetPath(), l.logs.GetText(true))
|
path, err := saveData(l.app.Config.K9s.ActiveScreenDumpsDir(), l.model.GetPath(), l.logs.GetText(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.app.Flash().Err(err)
|
l.app.Flash().Err(err)
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -420,8 +420,7 @@ func ensureDir(dir string) error {
|
||||||
return os.MkdirAll(dir, 0744)
|
return os.MkdirAll(dir, 0744)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveData(screenDumpDir, context, fqn, data string) (string, error) {
|
func saveData(dir, fqn, data string) (string, error) {
|
||||||
dir := filepath.Join(screenDumpDir, context)
|
|
||||||
if err := ensureDir(dir); err != nil {
|
if err := ensureDir(dir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
|
@ -113,7 +112,7 @@ func TestLogViewSave(t *testing.T) {
|
||||||
dd := "/tmp/test-dumps/na"
|
dd := "/tmp/test-dumps/na"
|
||||||
assert.NoError(t, ensureDumpDir(dd))
|
assert.NoError(t, ensureDumpDir(dd))
|
||||||
app.Config.K9s.ScreenDumpDir = "/tmp/test-dumps"
|
app.Config.K9s.ScreenDumpDir = "/tmp/test-dumps"
|
||||||
dir := filepath.Join(app.Config.K9s.GetScreenDumpDir(), app.Config.K9s.ActiveContextDir())
|
dir := app.Config.K9s.ActiveScreenDumpsDir()
|
||||||
c1, err := os.ReadDir(dir)
|
c1, err := os.ReadDir(dir)
|
||||||
assert.NoError(t, err, fmt.Sprintf("Dir: %q", dir))
|
assert.NoError(t, err, fmt.Sprintf("Dir: %q", dir))
|
||||||
v.SaveCmd(nil)
|
v.SaveCmd(nil)
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ func (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (l *Logger) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if path, err := saveYAML(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.ActiveContextDir(), l.title, l.GetText(true)); err != nil {
|
if path, err := saveYAML(l.app.Config.K9s.ActiveScreenDumpsDir(), l.title, l.GetText(true)); err != nil {
|
||||||
l.app.Flash().Err(err)
|
l.app.Flash().Err(err)
|
||||||
} else {
|
} else {
|
||||||
l.app.Flash().Infof("Log %s saved successfully!", path)
|
l.app.Flash().Infof("Log %s saved successfully!", path)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal"
|
"github.com/derailed/k9s/internal"
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
|
|
@ -36,7 +35,7 @@ func NewScreenDump(gvr client.GVR) ResourceViewer {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ScreenDump) dirContext(ctx context.Context) context.Context {
|
func (s *ScreenDump) dirContext(ctx context.Context) context.Context {
|
||||||
dir := filepath.Join(s.App().Config.K9s.GetScreenDumpDir(), s.App().Config.K9s.ActiveContextDir())
|
dir := s.App().Config.K9s.ActiveScreenDumpsDir()
|
||||||
if err := data.EnsureFullPath(dir, data.DefaultDirMod); err != nil {
|
if err := data.EnsureFullPath(dir, data.DefaultDirMod); err != nil {
|
||||||
s.App().Flash().Err(err)
|
s.App().Flash().Err(err)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ func (t *Table) BufferActive(state bool, k model.BufferKind) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
|
||||||
if path, err := saveTable(t.app.Config.K9s.GetScreenDumpDir(), t.app.Config.K9s.ActiveContextDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil {
|
if path, err := saveTable(t.app.Config.K9s.ActiveScreenDumpsDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil {
|
||||||
t.app.Flash().Err(err)
|
t.app.Flash().Err(err)
|
||||||
} else {
|
} else {
|
||||||
t.app.Flash().Infof("File %s saved successfully!", path)
|
t.app.Flash().Infof("File %s saved successfully!", path)
|
||||||
|
|
|
||||||
|
|
@ -12,21 +12,21 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/client"
|
"github.com/derailed/k9s/internal/client"
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
"github.com/derailed/k9s/internal/render"
|
"github.com/derailed/k9s/internal/render"
|
||||||
"github.com/derailed/k9s/internal/ui"
|
"github.com/derailed/k9s/internal/ui"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func computeFilename(screenDumpDir, context, ns, title, path string) (string, error) {
|
func computeFilename(dumpPath, ns, title, path string) (string, error) {
|
||||||
now := time.Now().UnixNano()
|
now := time.Now().UnixNano()
|
||||||
|
|
||||||
dir := filepath.Join(screenDumpDir, context)
|
dir := filepath.Join(dumpPath)
|
||||||
if err := ensureDir(dir); err != nil {
|
if err := ensureDir(dir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
name := title + "-" + config.SanitizeFilename(path)
|
name := title + "-" + data.SanitizeFileName(path)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
name = title
|
name = title
|
||||||
}
|
}
|
||||||
|
|
@ -41,13 +41,13 @@ func computeFilename(screenDumpDir, context, ns, title, path string) (string, er
|
||||||
return strings.ToLower(filepath.Join(dir, fName)), nil
|
return strings.ToLower(filepath.Join(dir, fName)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveTable(screenDumpDir, context, title, path string, data *render.TableData) (string, error) {
|
func saveTable(dir, title, path string, data *render.TableData) (string, error) {
|
||||||
ns := data.Namespace
|
ns := data.Namespace
|
||||||
if client.IsClusterWide(ns) {
|
if client.IsClusterWide(ns) {
|
||||||
ns = client.NamespaceAll
|
ns = client.NamespaceAll
|
||||||
}
|
}
|
||||||
|
|
||||||
fPath, err := computeFilename(screenDumpDir, context, ns, title, path)
|
fPath, err := computeFilename(dir, ns, title, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ package view
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -30,7 +29,7 @@ func TestTableSave(t *testing.T) {
|
||||||
v.SetTitle("k9s-test")
|
v.SetTitle("k9s-test")
|
||||||
|
|
||||||
assert.NoError(t, ensureDumpDir("/tmp/test-dumps"))
|
assert.NoError(t, ensureDumpDir("/tmp/test-dumps"))
|
||||||
dir := filepath.Join(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.ActiveContextDir())
|
dir := v.app.Config.K9s.ActiveScreenDumpsDir()
|
||||||
c1, _ := os.ReadDir(dir)
|
c1, _ := os.ReadDir(dir)
|
||||||
v.saveCmd(nil)
|
v.saveCmd(nil)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/derailed/k9s/internal/config"
|
"github.com/derailed/k9s/internal/config"
|
||||||
|
"github.com/derailed/k9s/internal/config/data"
|
||||||
"github.com/derailed/tview"
|
"github.com/derailed/tview"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
@ -62,18 +63,17 @@ func enableRegion(str string) string {
|
||||||
return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]")
|
return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]")
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveYAML(screenDumpDir, context, name, data string) (string, error) {
|
func saveYAML(dir, name, raw string) (string, error) {
|
||||||
dir := filepath.Join(screenDumpDir, config.SanitizeFilename(context))
|
|
||||||
if err := ensureDir(dir); err != nil {
|
if err := ensureDir(dir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
fName := fmt.Sprintf("%s--%d.yaml", config.SanitizeFilename(name), time.Now().Unix())
|
fName := fmt.Sprintf("%s--%d.yaml", data.SanitizeFileName(name), time.Now().Unix())
|
||||||
path := filepath.Join(dir, fName)
|
fpath := filepath.Join(dir, fName)
|
||||||
mod := os.O_CREATE | os.O_WRONLY
|
mod := os.O_CREATE | os.O_WRONLY
|
||||||
file, err := os.OpenFile(path, mod, 0600)
|
file, err := os.OpenFile(fpath, mod, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msgf("YAML create %s", path)
|
log.Error().Err(err).Msgf("YAML create %s", fpath)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
@ -81,9 +81,9 @@ func saveYAML(screenDumpDir, context, name, data string) (string, error) {
|
||||||
log.Error().Err(err).Msg("Closing yaml file")
|
log.Error().Err(err).Msg("Closing yaml file")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if _, err := file.Write([]byte(data)); err != nil {
|
if _, err := file.Write([]byte(raw)); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return path, nil
|
return fpath, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
name: k9s
|
name: k9s
|
||||||
base: core20
|
base: core20
|
||||||
version: 'v0.30.2'
|
version: 'v0.30.3'
|
||||||
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