Rel v0.50.1 - HotFix ;( (#3263)

* v0.50.0 hotfix

* rel notes
mine
Fernand Galiana 2025-04-09 23:58:37 -06:00 committed by GitHub
parent e55083ba27
commit 142282b584
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 277 additions and 142 deletions

View File

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

View File

@ -0,0 +1,42 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png" align="center" width="800" height="auto"/>
# Release v0.51
## 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)
## 5-0, 5-0 HotFix!
It looks like we've broken a few things in the clean up process 😳
Apologizes for the `disruption in the farce`. Hopefully happier on v0.50.1...
Crossing fingers and toes!
☠️ Careful on this upgrade! 🏴‍☠️
We've gone thru lots of code revamp/refactor in the v0.50.0, so mileage may vary...
---
## Resolved Issues
* [#3262](https://github.com/derailed/k9s/issues/3262) Crash when no shellPod is defined in config file
* [#3261](https://github.com/derailed/k9s/issues/3261) aliases with namespace and/or labels produce an error
* [#3258](https://github.com/derailed/k9s/issues/3258) mac silicon 0.50.0 runtime error
* [#3257](https://github.com/derailed/k9s/issues/3257) pods are reported to run on nodes they are not running on
* [#3256](https://github.com/derailed/k9s/issues/3256) Pods view seems broken in 0.50.0
* [#3255](https://github.com/derailed/k9s/issues/3255) Custom view does not work randomly
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -71,6 +71,10 @@ func NewGVR(path string) *GVR {
return &gvr
}
func (g *GVR) IsCommand() bool {
return g != nil && strings.Contains(g.raw, " ")
}
func (g *GVR) IsK8sRes() bool {
return strings.Contains(g.raw, "/")
}

View File

@ -5,7 +5,6 @@ package config
import (
"errors"
"fmt"
"io/fs"
"log/slog"
"os"
@ -15,6 +14,7 @@ import (
"github.com/derailed/k9s/internal/config/data"
"github.com/derailed/k9s/internal/config/json"
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/view/cmd"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/util/sets"
)
@ -79,26 +79,35 @@ func (a *Aliases) Clear() {
}
}
func (a *Aliases) Resolve(command string) (*client.GVR, string, bool) {
agvr, ok := a.Get(command)
if !ok {
return nil, "", false
}
if agvr.IsCommand() {
p := cmd.NewInterpreter(agvr.String())
gvr, ok := a.Get(p.Cmd())
if !ok {
return nil, "", false
}
return gvr, p.Args(), true
}
return agvr, "", true
}
// Get retrieves an alias.
func (a *Aliases) Get(alias string) (*client.GVR, bool) {
a.mx.RLock()
defer a.mx.RUnlock()
gvr, ok := a.Alias[alias]
if ok && !gvr.IsK8sRes() {
if rgvr, found := a.Alias[gvr.String()]; found {
return rgvr, found
}
}
return gvr, ok
}
// Define declares a new alias.
func (a *Aliases) Define(gvr *client.GVR, aliases ...string) {
if gvr.String() == "deployment" {
fmt.Println("!!YO!!")
}
a.mx.Lock()
defer a.mx.Unlock()
for _, alias := range aliases {

View File

@ -53,7 +53,7 @@ func TestConfigSave(t *testing.T) {
xdg.Reload()
u := uu[k]
t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig()
c := mock.NewMockConfig(t)
_, err := c.K9s.ActivateContext(u.ct)
require.NoError(t, err)
if u.flags != nil {
@ -113,7 +113,7 @@ func TestSetActiveView(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig()
c := mock.NewMockConfig(t)
_, _ = c.K9s.ActivateContext(u.ct)
if u.flags != nil {
require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
@ -156,7 +156,7 @@ func TestActiveContextName(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig()
c := mock.NewMockConfig(t)
_, _ = c.K9s.ActivateContext(u.ct)
if u.flags != nil {
require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
@ -182,14 +182,22 @@ func TestActiveView(t *testing.T) {
"empty": {
e: data.DefaultView,
},
"not-exists": {
ct: "fred",
e: data.DefaultView,
},
"happy": {
ct: "ct-1-1",
e: data.DefaultView,
},
"happy1": {
ct: "ct-1-2",
e: data.DefaultView,
},
"cli-override": {
flags: &genericclioptions.ConfigFlags{
KubeConfig: &cfgFile,
@ -204,7 +212,7 @@ func TestActiveView(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig()
c := mock.NewMockConfig(t)
_, _ = c.K9s.ActivateContext(u.ct)
if u.flags != nil {
require.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))
@ -233,7 +241,7 @@ func TestFavNamespaces(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig()
c := mock.NewMockConfig(t)
_, _ = c.K9s.ActivateContext(u.ct)
assert.Equal(t, u.e, c.FavNamespaces())
})
@ -258,7 +266,7 @@ func TestContextAliasesPath(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig()
c := mock.NewMockConfig(t)
_, _ = c.K9s.ActivateContext(u.ct)
assert.Equal(t, u.e, c.ContextAliasesPath())
})
@ -286,7 +294,7 @@ func TestContextPluginsPath(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
c := mock.NewMockConfig()
c := mock.NewMockConfig(t)
_, _ = c.K9s.ActivateContext(u.ct)
s, err := c.ContextPluginsPath()
if err != nil {
@ -344,7 +352,7 @@ func TestConfigActivateContext(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
ct, err := cfg.ActivateContext(u.ct)
if err != nil {
assert.Equal(t, u.err, err.Error())
@ -391,7 +399,7 @@ func TestConfigCurrentContext(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))
require.NoError(t, err)
@ -511,7 +519,7 @@ func TestConfigRefine(t *testing.T) {
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
err := cfg.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags))
if err != nil {
@ -526,7 +534,7 @@ func TestConfigRefine(t *testing.T) {
}
func TestConfigValidate(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
cfg.SetConnection(mock.NewMockConnection())
require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
@ -534,7 +542,7 @@ func TestConfigValidate(t *testing.T) {
}
func TestConfigLoad(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
assert.Equal(t, 2, cfg.K9s.RefreshRate)
@ -543,13 +551,13 @@ func TestConfigLoad(t *testing.T) {
}
func TestConfigLoadCrap(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
assert.Error(t, cfg.Load("testdata/configs/k9s_not_there.yaml", true))
}
func TestConfigSaveFile(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
@ -570,7 +578,7 @@ func TestConfigSaveFile(t *testing.T) {
}
func TestConfigReset(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
cfg.Reset()
cfg.Validate("ct-1-1", "cl-1")

View File

@ -15,7 +15,7 @@ func TestClusterValidate(t *testing.T) {
c := data.NewContext()
c.Validate(mock.NewMockConnection(), "ct-1", "cl-1")
assert.Equal(t, "po", c.View.Active)
assert.Equal(t, data.DefaultView, c.View.Active)
assert.Equal(t, "default", c.Namespace.Active)
assert.Len(t, c.Namespace.Favorites, 1)
assert.Equal(t, []string{"default"}, c.Namespace.Favorites)
@ -25,7 +25,7 @@ func TestClusterValidateEmpty(t *testing.T) {
c := data.NewContext()
c.Validate(mock.NewMockConnection(), "ct-1", "cl-1")
assert.Equal(t, "po", c.View.Active)
assert.Equal(t, data.DefaultView, c.View.Active)
assert.Equal(t, "default", c.Namespace.Active)
assert.Len(t, c.Namespace.Favorites, 1)
assert.Equal(t, []string{"default"}, c.Namespace.Favorites)

View File

@ -386,7 +386,9 @@ func (k *K9s) Validate(c client.Connection, contextName, clusterName string) {
if k.getActiveConfig() == nil {
_, _ = k.ActivateContext(contextName)
}
if k.ShellPod != nil {
k.ShellPod.Validate()
}
k.Logger = k.Logger.Validate()
k.Thresholds = k.Thresholds.Validate()

View File

@ -133,7 +133,7 @@ func TestK9sMerge(t *testing.T) {
}
func TestContextScreenDumpDir(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
_, err := cfg.K9s.ActivateContext("ct-1-1")
require.NoError(t, err)
@ -142,7 +142,7 @@ func TestContextScreenDumpDir(t *testing.T) {
}
func TestAppScreenDumpDir(t *testing.T) {
cfg := mock.NewMockConfig()
cfg := mock.NewMockConfig(t)
require.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true))
assert.Equal(t, "/tmp/k9s-test/screen-dumps", cfg.K9s.AppScreenDumpDir())

View File

@ -11,9 +11,11 @@ import (
"net/url"
"os"
"strings"
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/require"
version "k8s.io/apimachinery/pkg/version"
"k8s.io/cli-runtime/pkg/genericclioptions"
disk "k8s.io/client-go/discovery/cached/disk"
@ -35,9 +37,11 @@ func EnsureDir(d string) error {
return os.MkdirAll(d, 0700)
}
func NewMockConfig() *config.Config {
if _, err := os.Stat("/tmp/test"); errors.Is(err, os.ErrExist) {
_ = os.RemoveAll("/tmp/test")
func NewMockConfig(t testing.TB) *config.Config {
if _, err := os.Stat("/tmp/test"); err == nil {
if e := os.RemoveAll("/tmp/test"); e != nil {
require.NoError(t, e)
}
}
config.AppContextsDir = "/tmp/test"
cl, ct := "cl-1", "ct-1-1"

View File

@ -63,14 +63,7 @@ func (*Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) {
// AsGVR returns a matching gvr if it exists.
func (a *Alias) AsGVR(alias string) (*client.GVR, string, bool) {
gvr, ok := a.Aliases.Get(alias)
if ok {
if pgvr := MetaAccess.Lookup(alias); pgvr != client.NoGVR {
return pgvr, "", ok
}
}
return gvr, "", ok
return a.Resolve(alias)
}
// Get fetch a resource.
@ -106,10 +99,9 @@ func (a *Alias) load(path string) error {
}
a.Define(gvr, gvr.AsResourceName())
// Allow single shot commands for k8s resources only!
if isStandardGroup(gvr.GVSub()) {
a.Define(gvr, meta.Name)
a.Define(gvr, meta.SingularName)
// Allow single shot commands for k8s resources only expect for metrics resource which override pods and nodes ;(!
if isStandardGroup(gvr.GVSub()) && gvr.G() != "metrics.k8s.io" {
a.Define(gvr, meta.Name, meta.SingularName)
}
if len(meta.ShortNames) > 0 {
a.Define(gvr, meta.ShortNames...)

View File

@ -20,39 +20,60 @@ func TestAsGVR(t *testing.T) {
a := dao.NewAlias(makeFactory())
a.Define(client.PodGVR, "po", "pod", "pods")
a.Define(client.WkGVR, client.WkGVR.String(), "workload", "wkl")
a.Define(client.NewGVR("pod default"), "pp")
a.Define(client.NewGVR("pod default @fred"), "ppc")
uu := map[string]struct {
cmd string
ok bool
gvr *client.GVR
exp string
}{
"ok": {
cmd: "pods",
ok: true,
gvr: client.PodGVR,
},
"ok-short": {
cmd: "po",
ok: true,
gvr: client.PodGVR,
},
"missing": {
cmd: "zorg",
},
"alias": {
cmd: "wkl",
ok: true,
gvr: client.WkGVR,
},
"ns-alias": {
cmd: "pp",
ok: true,
gvr: client.PodGVR,
exp: "default",
},
"full-alias": {
cmd: "ppc",
ok: true,
gvr: client.PodGVR,
exp: "default @fred",
},
}
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
gvr, _, ok := a.AsGVR(u.cmd)
gvr, exp, ok := a.AsGVR(u.cmd)
assert.Equal(t, u.ok, ok)
if u.ok {
assert.Equal(t, u.gvr, gvr)
assert.Equal(t, u.exp, exp)
}
})
}

View File

@ -7,7 +7,6 @@ import (
"fmt"
"strconv"
"strings"
"time"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/model1"
@ -19,7 +18,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/cache"
mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)
@ -76,22 +74,15 @@ var defaultPodHeader = model1.Header{
model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
}
const (
cacheSize = 5_000
expiration = 5 * time.Minute
)
// Pod renders a K8s Pod to screen.
type Pod struct {
*Base
cache *cache.LRUExpireCache
}
// NewPod returns a new instance.
func NewPod() *Pod {
return &Pod{
Base: new(Base),
cache: cache.NewLRUExpireCache(cacheSize),
}
}
@ -157,23 +148,10 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object["status"].(map[string]any), &st); err != nil {
return err
}
key := pwm.Raw.GetUID()
for _, o := range pwm.Raw.GetOwnerReferences() {
if o.Controller != nil && *o.Controller {
key = o.UID
break
}
}
spec := new(v1.PodSpec)
if cspec, ok := p.cache.Get(key); ok {
spec = cspec.(*v1.PodSpec)
} else {
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object["spec"].(map[string]any), spec); err != nil {
return err
}
p.cache.Add(key, spec, expiration)
}
dt := pwm.Raw.GetDeletionTimestamp()
_, _, irc, _ := p.Statuses(st.InitContainerStatuses)

View File

@ -12,7 +12,7 @@ import (
)
func TestAppGetCmd(t *testing.T) {
a := ui.NewApp(mock.NewMockConfig(), "")
a := ui.NewApp(mock.NewMockConfig(t), "")
a.Init()
a.CmdBuff().SetText("blee", "")
@ -20,7 +20,7 @@ func TestAppGetCmd(t *testing.T) {
}
func TestAppInCmdMode(t *testing.T) {
a := ui.NewApp(mock.NewMockConfig(), "")
a := ui.NewApp(mock.NewMockConfig(t), "")
a.Init()
a.CmdBuff().SetText("blee", "")
assert.False(t, a.InCmdMode())
@ -30,7 +30,7 @@ func TestAppInCmdMode(t *testing.T) {
}
func TestAppResetCmd(t *testing.T) {
a := ui.NewApp(mock.NewMockConfig(), "")
a := ui.NewApp(mock.NewMockConfig(t), "")
a.Init()
a.CmdBuff().SetText("blee", "")
@ -40,7 +40,7 @@ func TestAppResetCmd(t *testing.T) {
}
func TestAppHasCmd(t *testing.T) {
a := ui.NewApp(mock.NewMockConfig(), "")
a := ui.NewApp(mock.NewMockConfig(t), "")
a.Init()
a.ActivateCmd(true)
@ -51,7 +51,7 @@ func TestAppHasCmd(t *testing.T) {
}
func TestAppGetActions(t *testing.T) {
a := ui.NewApp(mock.NewMockConfig(), "")
a := ui.NewApp(mock.NewMockConfig(t), "")
a.Init()
a.GetActions().Add(ui.KeyZ, ui.KeyAction{Description: "zorg"})
@ -60,7 +60,7 @@ func TestAppGetActions(t *testing.T) {
}
func TestAppViews(t *testing.T) {
a := ui.NewApp(mock.NewMockConfig(), "")
a := ui.NewApp(mock.NewMockConfig(t), "")
a.Init()
vv := []string{"crumbs", "logo", "prompt", "menu"}

View File

@ -33,7 +33,7 @@ func TestSkinnedContext(t *testing.T) {
require.NoError(t, os.WriteFile(tf, raw, data.DefaultFileMod))
var cfg ui.Configurator
cfg.Config = mock.NewMockConfig()
cfg.Config = mock.NewMockConfig(t)
cl, ct := "cl-1", "ct-1"
flags := genericclioptions.ConfigFlags{
ClusterName: &cl,

View File

@ -25,7 +25,7 @@ func TestFlash(t *testing.T) {
"err": {l: model.FlashErr, i: "hello", e: "😡 hello\n"},
}
a := ui.NewApp(mock.NewMockConfig(), "test")
a := ui.NewApp(mock.NewMockConfig(t), "test")
f := ui.NewFlash(a)
f.SetTestMode(true)
ctx, cancel := context.WithCancel(context.Background())

View File

@ -13,7 +13,7 @@ import (
)
func TestIndicatorReset(t *testing.T) {
i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles())
i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(t), ""), config.NewStyles())
i.SetPermanent("Blee")
i.Info("duh")
i.Reset()
@ -22,21 +22,21 @@ func TestIndicatorReset(t *testing.T) {
}
func TestIndicatorInfo(t *testing.T) {
i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles())
i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(t), ""), config.NewStyles())
i.Info("Blee")
assert.Equal(t, "[lawngreen::b] <Blee> \n", i.GetText(false))
}
func TestIndicatorWarn(t *testing.T) {
i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles())
i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(t), ""), config.NewStyles())
i.Warn("Blee")
assert.Equal(t, "[mediumvioletred::b] <Blee> \n", i.GetText(false))
}
func TestIndicatorErr(t *testing.T) {
i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles())
i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(t), ""), config.NewStyles())
i.Err("Blee")
assert.Equal(t, "[orangered::b] <Blee> \n", i.GetText(false))

View File

@ -27,14 +27,14 @@ import (
func TestAliasNew(t *testing.T) {
v := view.NewAlias(client.AliGVR)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
assert.Equal(t, "Aliases", v.Name())
assert.Len(t, v.Hints(), 6)
}
func TestAliasSearch(t *testing.T) {
v := view.NewAlias(client.AliGVR)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
v.GetTable().SetModel(&mockModel{})
v.GetTable().Refresh()
v.App().Prompt().SetModel(v.GetTable().CmdBuff())
@ -46,7 +46,7 @@ func TestAliasSearch(t *testing.T) {
func TestAliasGoto(t *testing.T) {
v := view.NewAlias(client.AliGVR)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
v.GetTable().Select(0, 0)
b := buffL{}
@ -74,8 +74,8 @@ func (b *buffL) BufferActive(bool, model.BufferKind) {
b.active++
}
func makeContext() context.Context {
a := view.NewApp(mock.NewMockConfig())
func makeContext(t testing.TB) context.Context {
a := view.NewApp(mock.NewMockConfig(t))
ctx := context.WithValue(context.Background(), internal.KeyApp, a)
return context.WithValue(ctx, internal.KeyStyles, a.Styles)
}

View File

@ -466,7 +466,7 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
p := cmd.NewInterpreter(a.Config.ActiveView())
p.ResetContextArg()
if p.IsContextCmd() {
a.Config.SetActiveView("pod")
a.Config.SetActiveView(client.PodGVR.String())
}
ns := a.Config.ActiveNamespace()
if !a.Conn().IsValidNamespace(ns) {

View File

@ -12,7 +12,7 @@ import (
)
func TestAppNew(t *testing.T) {
a := view.NewApp(mock.NewMockConfig())
a := view.NewApp(mock.NewMockConfig(t))
_ = a.Init("blee", 10)
assert.Equal(t, 15, a.GetActions().Len())

View File

@ -15,7 +15,7 @@ import (
func TestConfigMapNew(t *testing.T) {
s := view.NewConfigMap(client.CmGVR)
require.NoError(t, s.Init(makeCtx()))
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "ConfigMaps", s.Name())
assert.Len(t, s.Hints(), 7)
}

View File

@ -48,6 +48,10 @@ func (c *Interpreter) Cmd() string {
return c.cmd
}
func (c *Interpreter) Args() string {
return strings.TrimSpace(strings.Replace(c.line, c.cmd, "", 1))
}
// IsBlank returns true if prompt is empty.
func (c *Interpreter) IsBlank() bool {
return c.line == ""

View File

@ -9,7 +9,6 @@ import (
"log/slog"
"regexp"
"runtime/debug"
"strings"
"sync"
"github.com/derailed/k9s/internal/client"
@ -21,7 +20,7 @@ import (
)
const (
podCmd = "pod"
podCmd = "v1/pods"
ctxCmd = "ctx"
)
@ -291,11 +290,7 @@ func (c *Command) viewMetaFor(p *cmd.Interpreter) (*client.GVR, *MetaViewer, err
return client.NoGVR, nil, fmt.Errorf("`%s` command not found", p.Cmd())
}
if exp != "" {
ff := strings.Fields(exp)
ff[0] = gvr.String()
ap := cmd.NewInterpreter(strings.Join(ff, " "))
gvr = client.NewGVR(ap.Cmd())
p.Amend(ap)
p.Amend(cmd.NewInterpreter(gvr.String() + " " + exp))
}
v := MetaViewer{

View File

@ -0,0 +1,71 @@
package view
import (
"errors"
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/view/cmd"
"github.com/stretchr/testify/assert"
)
func Test_viewMetaFor(t *testing.T) {
uu := map[string]struct {
cmd string
gvr *client.GVR
err error
}{
"empty": {
cmd: "",
gvr: client.PodGVR,
err: errors.New("`` command not found"),
},
"toast-cmd": {
cmd: "v1/pd",
gvr: client.PodGVR,
err: errors.New("`v1/pd` command not found"),
},
"gvr-cmd": {
cmd: "v1/pods",
gvr: client.PodGVR,
err: errors.New("blah"),
},
"alias-cmd": {
cmd: "po",
gvr: client.PodGVR,
err: errors.New("blee"),
},
"full-cmd": {
cmd: "pdl",
gvr: client.PodGVR,
err: errors.New("blee"),
},
}
c := &Command{
alias: &dao.Alias{
Aliases: config.NewAliases(),
},
}
c.alias.Define(client.PodGVR, "po", "pod", "pods", client.PodGVR.String())
c.alias.Define(client.NewGVR("pod default"), "pd")
c.alias.Define(client.NewGVR("pod default app=blee @fred"), "pdl")
for k, u := range uu {
t.Run(k, func(t *testing.T) {
p := cmd.NewInterpreter(u.cmd)
gvr, _, err := c.viewMetaFor(p)
if err != nil {
assert.Equal(t, u.err.Error(), err.Error())
} else {
assert.Equal(t, u.gvr, gvr)
}
})
}
}

View File

@ -15,7 +15,7 @@ import (
func TestContainerNew(t *testing.T) {
c := view.NewContainer(client.CoGVR)
require.NoError(t, c.Init(makeCtx()))
require.NoError(t, c.Init(makeCtx(t)))
assert.Equal(t, "Containers", c.Name())
assert.Len(t, c.Hints(), 19)
}

View File

@ -15,7 +15,7 @@ import (
func TestContext(t *testing.T) {
ctx := view.NewContext(client.CtGVR)
require.NoError(t, ctx.Init(makeCtx()))
require.NoError(t, ctx.Init(makeCtx(t)))
assert.Equal(t, "Contexts", ctx.Name())
assert.Len(t, ctx.Hints(), 5)
}

View File

@ -14,7 +14,7 @@ import (
func TestDir(t *testing.T) {
v := view.NewDir("/fred")
require.NoError(t, v.Init(makeCtx()))
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "Directory", v.Name())
assert.Len(t, v.Hints(), 7)
}

View File

@ -15,7 +15,7 @@ import (
func TestDeploy(t *testing.T) {
v := view.NewDeploy(client.DpGVR)
require.NoError(t, v.Init(makeCtx()))
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "Deployments", v.Name())
assert.Len(t, v.Hints(), 16)
}

View File

@ -15,7 +15,7 @@ import (
func TestDaemonSet(t *testing.T) {
v := view.NewDaemonSet(client.DsGVR)
require.NoError(t, v.Init(makeCtx()))
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "DaemonSets", v.Name())
assert.Len(t, v.Hints(), 17)
}

View File

@ -321,6 +321,11 @@ func launchNodeShell(v model.Igniter, a *App, node string) {
}
func launchPodShell(v model.Igniter, a *App) {
if a.Config.K9s.ShellPod == nil {
slog.Error("Shell pod not configured!")
return
}
defer func() {
if err := nukeK9sShell(a); err != nil {
a.Flash().Errf("Launching node shell failed: %s", err)

View File

@ -15,7 +15,7 @@ import (
)
func TestHelp(t *testing.T) {
ctx := makeCtx()
ctx := makeCtx(t)
app := ctx.Value(internal.KeyApp).(*view.App)
po := view.NewPod(client.PodGVR)

View File

@ -61,7 +61,7 @@ func TestParsePFAnn(t *testing.T) {
}
func TestExtractApp(t *testing.T) {
app := NewApp(mock.NewMockConfig())
app := NewApp(mock.NewMockConfig(t))
uu := map[string]struct {
app *App

View File

@ -20,7 +20,7 @@ apiVersion: v1
the secret name you want to quote to use tls.","title":"secretName","type":"string"}},"required":["http","class","classInSpec"],"type":"object"}
`
v := NewLiveView(NewApp(mock.NewMockConfig()), "fred", nil)
v := NewLiveView(NewApp(mock.NewMockConfig(t)), "fred", nil)
require.NoError(t, v.Init(context.Background()))
v.text.SetText(colorizeYAML(config.Yaml{}, s))

View File

@ -21,7 +21,7 @@ func TestLogAutoScroll(t *testing.T) {
SingleContainer: true,
}
v := NewLog(client.PodGVR, &opts)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
ii := dao.NewLogItems()
ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo"))
v.GetModel().Set(ii)
@ -39,7 +39,7 @@ func TestLogViewNav(t *testing.T) {
Container: "blee",
}
v := NewLog(client.PodGVR, &opts)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
buff := dao.NewLogItems()
for i := range 100 {
@ -58,7 +58,7 @@ func TestLogViewClear(t *testing.T) {
Container: "blee",
}
v := NewLog(client.PodGVR, &opts)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
v.toggleAutoScrollCmd(nil)
v.Logs().SetText("blee\nblah")
@ -73,7 +73,7 @@ func TestLogTimestamp(t *testing.T) {
Container: "c1",
}
l := NewLog(client.NewGVR("test"), &opts)
require.NoError(t, l.Init(makeContext()))
require.NoError(t, l.Init(makeContext(t)))
ii := dao.NewLogItems()
ii.Add(
&dao.LogItem{
@ -103,7 +103,7 @@ func TestLogFilter(t *testing.T) {
Container: "c1",
}
l := NewLog(client.NewGVR("test"), &opts)
require.NoError(t, l.Init(makeContext()))
require.NoError(t, l.Init(makeContext(t)))
buff := dao.NewLogItems()
buff.Add(
dao.NewLogItemFromString("duh"),

View File

@ -28,7 +28,7 @@ func TestLog(t *testing.T) {
Container: "blee",
}
v := view.NewLog(client.PodGVR, &opts)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
ii := dao.NewLogItems()
ii.Add(dao.NewLogItemFromString("blee\n"), dao.NewLogItemFromString("bozo\n"))
@ -45,7 +45,7 @@ func TestLogFlush(t *testing.T) {
Container: "blee",
}
v := view.NewLog(client.PodGVR, &opts)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
items := dao.NewLogItems()
items.Add(
@ -65,7 +65,7 @@ func BenchmarkLogFlush(b *testing.B) {
Container: "blee",
}
v := view.NewLog(client.PodGVR, &opts)
_ = v.Init(makeContext())
_ = v.Init(makeContext(b))
items := dao.NewLogItems()
items.Add(
@ -103,9 +103,9 @@ func TestLogViewSave(t *testing.T) {
Container: "blee",
}
v := view.NewLog(client.PodGVR, &opts)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
app := makeApp()
app := makeApp(t)
ii := dao.NewLogItems()
ii.Add(dao.NewLogItemFromString("blee"), dao.NewLogItemFromString("bozo"))
ll := make([][]byte, ii.Len())
@ -141,7 +141,7 @@ func TestAllContainerKeyBinding(t *testing.T) {
u := uu[k]
t.Run(k, func(t *testing.T) {
v := view.NewLog(client.PodGVR, u.opts)
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
_, got := v.Logs().Actions().Get(ui.KeyA)
assert.Equal(t, u.e, got)
})
@ -151,8 +151,8 @@ func TestAllContainerKeyBinding(t *testing.T) {
// ----------------------------------------------------------------------------
// Helpers...
func makeApp() *view.App {
return view.NewApp(mock.NewMockConfig())
func makeApp(t *testing.T) *view.App {
return view.NewApp(mock.NewMockConfig(t))
}
func ensureDumpDir(n string) error {

View File

@ -41,7 +41,7 @@ func (n *Namespace) bindKeys(aa *ui.KeyActions) {
func (n *Namespace) switchNs(app *App, _ ui.Tabular, _ *client.GVR, path string) {
n.useNamespace(path)
app.gotoResource("pods", "", false, true)
app.gotoResource(client.PodGVR.String(), "", false, true)
}
func (n *Namespace) useNsCmd(*tcell.EventKey) *tcell.EventKey {

View File

@ -15,7 +15,7 @@ import (
func TestNSCleanser(t *testing.T) {
ns := view.NewNamespace(client.NsGVR)
require.NoError(t, ns.Init(makeCtx()))
require.NoError(t, ns.Init(makeCtx(t)))
assert.Equal(t, "Namespaces", ns.Name())
assert.Len(t, ns.Hints(), 7)
}

View File

@ -15,7 +15,7 @@ import (
func TestPortForwardNew(t *testing.T) {
pf := view.NewPortForward(client.PfGVR)
require.NoError(t, pf.Init(makeCtx()))
require.NoError(t, pf.Init(makeCtx(t)))
assert.Equal(t, "PortForwards", pf.Name())
assert.Len(t, pf.Hints(), 10)
}

View File

@ -18,14 +18,14 @@ import (
func TestPodNew(t *testing.T) {
po := view.NewPod(client.PodGVR)
require.NoError(t, po.Init(makeCtx()))
require.NoError(t, po.Init(makeCtx(t)))
assert.Equal(t, "Pods", po.Name())
assert.Len(t, po.Hints(), 28)
}
// Helpers...
func makeCtx() context.Context {
cfg := mock.NewMockConfig()
func makeCtx(t testing.TB) context.Context {
cfg := mock.NewMockConfig(t)
return context.WithValue(context.Background(), internal.KeyApp, view.NewApp(cfg))
}

View File

@ -15,7 +15,7 @@ import (
func TestPriorityClassNew(t *testing.T) {
s := view.NewPriorityClass(client.PcGVR)
require.NoError(t, s.Init(makeCtx()))
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "PriorityClass", s.Name())
assert.Len(t, s.Hints(), 6)
}

View File

@ -15,7 +15,7 @@ import (
func TestPVCNew(t *testing.T) {
v := view.NewPersistentVolumeClaim(client.PvcGVR)
require.NoError(t, v.Init(makeCtx()))
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "PersistentVolumeClaims", v.Name())
assert.Len(t, v.Hints(), 11)
}

View File

@ -15,7 +15,7 @@ import (
func TestRbacNew(t *testing.T) {
v := view.NewRbac(client.RbacGVR)
require.NoError(t, v.Init(makeCtx()))
require.NoError(t, v.Init(makeCtx(t)))
assert.Equal(t, "Rbac", v.Name())
assert.Len(t, v.Hints(), 5)
}

View File

@ -56,7 +56,7 @@ func (r *Reference) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
path := r.GetTable().GetSelectedItem()
ns, _ := client.Namespaced(path)
gvr := ui.TrimCell(r.GetTable().SelectTable, row, 2)
r.App().gotoResource(client.NewGVR(gvr).R()+" "+ns, path, false, true)
r.App().gotoResource(client.NewGVR(gvr).String()+" "+ns, path, false, true)
return evt
}

View File

@ -15,7 +15,7 @@ import (
func TestReferenceNew(t *testing.T) {
s := view.NewReference(client.RefGVR)
require.NoError(t, s.Init(makeCtx()))
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "References", s.Name())
assert.Len(t, s.Hints(), 4)
}

View File

@ -15,7 +15,7 @@ import (
func TestScreenDumpNew(t *testing.T) {
po := view.NewScreenDump(client.SdGVR)
require.NoError(t, po.Init(makeCtx()))
require.NoError(t, po.Init(makeCtx(t)))
assert.Equal(t, "ScreenDumps", po.Name())
assert.Len(t, po.Hints(), 5)
}

View File

@ -15,7 +15,7 @@ import (
func TestSecretNew(t *testing.T) {
s := view.NewSecret(client.SecGVR)
require.NoError(t, s.Init(makeCtx()))
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "Secrets", s.Name())
assert.Len(t, s.Hints(), 8)
}

View File

@ -15,7 +15,7 @@ import (
func TestStatefulSetNew(t *testing.T) {
s := view.NewStatefulSet(client.StsGVR)
require.NoError(t, s.Init(makeCtx()))
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "StatefulSets", s.Name())
assert.Len(t, s.Hints(), 14)
}

View File

@ -172,7 +172,7 @@ func init() {
func TestServiceNew(t *testing.T) {
s := view.NewService(client.SvcGVR)
require.NoError(t, s.Init(makeCtx()))
require.NoError(t, s.Init(makeCtx(t)))
assert.Equal(t, "Services", s.Name())
assert.Len(t, s.Hints(), 12)
}

View File

@ -29,7 +29,7 @@ import (
func TestTableSave(t *testing.T) {
v := NewTable(client.NewGVR("test"))
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
v.SetTitle("k9s-test")
require.NoError(t, ensureDumpDir("/tmp/test-dumps"))
@ -43,7 +43,7 @@ func TestTableSave(t *testing.T) {
func TestTableNew(t *testing.T) {
v := NewTable(client.NewGVR("test"))
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
data := model1.NewTableDataWithRows(
client.NewGVR("test"),
@ -74,7 +74,7 @@ func TestTableNew(t *testing.T) {
func TestTableViewFilter(t *testing.T) {
v := NewTable(client.NewGVR("test"))
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
v.SetModel(&mockTableModel{})
v.Refresh()
@ -86,7 +86,7 @@ func TestTableViewFilter(t *testing.T) {
func TestTableViewSort(t *testing.T) {
v := NewTable(client.NewGVR("test"))
require.NoError(t, v.Init(makeContext()))
require.NoError(t, v.Init(makeContext(t)))
v.SetModel(new(mockTableModel))
uu := map[string]struct {
@ -199,8 +199,8 @@ func makeTableData() *model1.TableData {
)
}
func makeContext() context.Context {
a := NewApp(mock.NewMockConfig())
func makeContext(t *testing.T) context.Context {
a := NewApp(mock.NewMockConfig(t))
ctx := context.WithValue(context.Background(), internal.KeyApp, a)
return context.WithValue(ctx, internal.KeyStyles, a.Styles)
}

View File

@ -83,7 +83,7 @@ func (*Workload) showRes(app *App, _ ui.Tabular, _ *client.GVR, path string) {
app.Flash().Err(fmt.Errorf("unable to parse path: %q", path))
return
}
app.gotoResource(gvr.R(), fqn, false, true)
app.gotoResource(gvr.String(), fqn, false, true)
}
func (w *Workload) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {

View File

@ -1,6 +1,6 @@
name: k9s
base: core22
version: 'v0.50.0'
version: 'v0.50.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.