diff --git a/change_logs/release_v0.13.4.md b/change_logs/release_v0.13.4.md
new file mode 100644
index 00000000..17ab97d0
--- /dev/null
+++ b/change_logs/release_v0.13.4.md
@@ -0,0 +1,44 @@
+
+
+# Release v0.13.4
+
+## 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 is as ever very much noticed and appreciated!
+
+Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
+
+---
+
+Maintenance Release!
+
+## GH Sponsors
+
+A Big Thank You to the following folks that I've decided to dig in and give back!! πππ
+Thank you for your gesture of kindness and for supporting K9s!! (not to mention for replainishing my liquids during oh-dark-thirty hours πΊπΉπΈ)
+
+* [w11d](https://github.com/w11d)
+* [vglen](https://github.com/vglen)
+
+## CPU/MEM Metrics
+
+A small change here based on [Benjamin](https://github.com/binarycoded) excellent PR! We've added 2 new columms for pod/container views to indicate percentages of resources request/limits if set on the containers. The columms have been renamed to represent the resources requests/limits as follows:
+
+| Name | Description | Sort Keys |
+|--------|--------------------------------|-----------|
+| %CPU/R | Percentage of requested cpu | shift-x |
+| %MEM/R | Percentage of requested memory | shift-z |
+| %MEM/L | Percentage of limited cpu | ctrl-x |
+| %MEM/L | Percentage of limited memory | ctrl-z |
+
+---
+
+## Resolved Bugs/Features
+
+* [Issue #507](https://github.com/derailed/k9s/issues/507) ??May be??
+* [PR #489](https://github.com/derailed/k9s/issues/489) ATTA Boy! [Benjamin](https://github.com/binarycoded)
+* [PR #491](https://github.com/derailed/k9s/issues/491) Big Thanks! [Bjoern](https://github.com/bjoernmichaelsen)
+
+---
+
+
Β© 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)
diff --git a/cmd/root.go b/cmd/root.go
index 6a273530..b97fe285 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -22,6 +22,8 @@ const (
longAppDesc = "K9s is a CLI to view and manage your Kubernetes clusters."
)
+var _ config.KubeSettings = (*client.Config)(nil)
+
var (
version, commit, date = "dev", "dev", "n/a"
k9sFlags *config.Flags
@@ -33,7 +35,6 @@ var (
Long: longAppDesc,
Run: run,
}
- _ config.KubeSettings = &client.Config{}
)
func init() {
@@ -84,7 +85,12 @@ func run(cmd *cobra.Command, args []string) {
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
panic(err)
}
- app.Run()
+ if err := app.Run(); err != nil {
+ panic(err)
+ }
+ if view.ExitStatus != "" {
+ panic(view.ExitStatus)
+ }
}
}
@@ -121,8 +127,8 @@ func loadConfiguration() *config.Config {
k9sCfg.SetConnection(client.InitConnectionOrDie(k8sCfg))
// Try to access server version if that fail. Connectivity issue?
- if _, err := k9sCfg.GetConnection().ServerVersion(); err != nil {
- log.Panic().Msgf("K9s can't connect to cluster -- %s", err)
+ if !k9sCfg.GetConnection().CheckConnectivity() {
+ log.Panic().Msgf("K9s can't connect to cluster")
}
log.Info().Msg("β
Kubernetes connectivity")
if err := k9sCfg.Save(); err != nil {
diff --git a/internal/client/client.go b/internal/client/client.go
index 0dc37b78..9a757a5c 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -29,12 +29,6 @@ const (
var supportedMetricsAPIVersions = []string{"v1beta1"}
-// Authorizer checks what a user can or cannot do to a resource.
-type Authorizer interface {
- // CanI returns true if the user can use these actions for a given resource.
- CanI(ns, gvr string, verbs []string) (bool, error)
-}
-
// APIClient represents a Kubernetes api client.
type APIClient struct {
client kubernetes.Interface
@@ -131,33 +125,26 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
return nn.Items, nil
}
-// IsNamespaced check on server if given resource is namespaced
-func (a *APIClient) IsNamespaced(res string) bool {
- list, _ := a.CachedDiscoveryOrDie().ServerPreferredResources()
- for _, l := range list {
- for _, r := range l.APIResources {
- if r.Name == res {
- return r.Namespaced
- }
+// CheckConnectivity return true if api server is cool or false otherwise.
+// BOZO!! No super sure about this approach either??
+func (a *APIClient) CheckConnectivity() (status bool) {
+ defer func() {
+ if err := recover(); err != nil {
+ status = false
}
- }
- return false
-}
+ }()
-// SupportsResource checks for resource supported version against the server.
-func (a *APIClient) SupportsResource(group string) bool {
- list, err := a.CachedDiscoveryOrDie().ServerPreferredResources()
- if err != nil {
- log.Error().Err(err).Msg("Unable to dial api server")
- return false
+ client, ok := a.DialOrDie().(*kubernetes.Clientset)
+ if !ok {
+ return status
}
- for _, l := range list {
- log.Debug().Msgf(">>> Group %s", l.GroupVersion)
- if l.GroupVersion == group {
- return true
- }
+ if _, err := client.ServerVersion(); err != nil {
+ log.Error().Err(err).Msgf("K9s can't connect to cluster")
+ } else {
+ status = true
}
- return false
+
+ return status
}
// Config return a kubernetes configuration.
@@ -317,19 +304,3 @@ func checkMetricsVersion(grp metav1.APIGroup) bool {
return false
}
-
-// SupportsRes checks latest supported version.
-func (a *APIClient) SupportsRes(group string, versions []string) (string, bool, error) {
- apiGroups, err := a.CachedDiscoveryOrDie().ServerGroups()
- if err != nil {
- return "", false, err
- }
- for _, grp := range apiGroups.Groups {
- if grp.Name != group {
- continue
- }
- return grp.Versions[len(grp.Versions)-1].Version, true, nil
- }
-
- return "", false, nil
-}
diff --git a/internal/client/config.go b/internal/client/config.go
index 84af8ffd..364e6236 100644
--- a/internal/client/config.go
+++ b/internal/client/config.go
@@ -8,7 +8,6 @@ import (
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
- "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
clientcmd "k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
@@ -32,27 +31,6 @@ func NewConfig(f *genericclioptions.ConfigFlags) *Config {
}
}
-// CheckConnectivity return true if api server is cool or false otherwise.
-// BOZO!! No super sure about this approach either??
-func (c *Config) CheckConnectivity() bool {
- cfg, err := c.RESTConfig()
- if err != nil {
- log.Error().Err(err).Msgf("K9s can't connect to cluster (config)")
- return false
- }
- client, err := kubernetes.NewForConfig(cfg)
- if err != nil {
- log.Error().Err(err).Msgf("K9s can't connect to cluster (client)")
- return false
- }
- if _, err := client.ServerVersion(); err != nil {
- log.Error().Err(err).Msgf("K9s can't connect to cluster (serverVersion)")
- return false
- }
-
- return true
-}
-
// Flags returns configuration flags.
func (c *Config) Flags() *genericclioptions.ConfigFlags {
return c.flags
diff --git a/internal/client/gvr.go b/internal/client/gvr.go
index bd2720b1..fe1898e2 100644
--- a/internal/client/gvr.go
+++ b/internal/client/gvr.go
@@ -141,10 +141,10 @@ func (g GVRs) Less(i, j int) bool {
// Can determines the available actions for a given resource.
func Can(verbs []string, v string) bool {
if verbs == nil {
- return false
+ return true
}
if len(verbs) == 0 {
- return true
+ return false
}
for _, verb := range verbs {
candidates, err := mapVerb(v)
diff --git a/internal/client/types.go b/internal/client/types.go
index 79ad0be3..91e1f247 100644
--- a/internal/client/types.go
+++ b/internal/client/types.go
@@ -52,8 +52,13 @@ var (
ReadAllAccess = []string{GetVerb, ListVerb, WatchVerb}
)
+// Authorizer checks what a user can or cannot do to a resource.
+type Authorizer interface {
+ // CanI returns true if the user can use these actions for a given resource.
+ CanI(ns, gvr string, verbs []string) (bool, error)
+}
+
// Connection represents a Kubenetes apiserver connection.
-// BOZO!! Refactor!
type Connection interface {
Authorizer
@@ -65,12 +70,9 @@ type Connection interface {
MXDial() (*versioned.Clientset, error)
DynDialOrDie() dynamic.Interface
HasMetrics() bool
- IsNamespaced(n string) bool
- SupportsResource(group string) bool
ValidNamespaces() ([]v1.Namespace, error)
- SupportsRes(grp string, versions []string) (string, bool, error)
ServerVersion() (*version.Info, error)
- CurrentNamespaceName() (string, error)
+ CheckConnectivity() bool
}
// CurrentMetrics tracks current cpu/mem.
diff --git a/internal/config/mock_connection_test.go b/internal/config/mock_connection_test.go
index ac9b37c6..57a8104b 100644
--- a/internal/config/mock_connection_test.go
+++ b/internal/config/mock_connection_test.go
@@ -82,23 +82,19 @@ func (mock *MockConnection) Config() *client.Config {
return ret0
}
-func (mock *MockConnection) CurrentNamespaceName() (string, error) {
+func (mock *MockConnection) CheckConnectivity() bool {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockConnection().")
}
params := []pegomock.Param{}
- result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentNamespaceName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
- var ret0 string
- var ret1 error
+ result := pegomock.GetGenericMockFrom(mock).Invoke("CheckConnectivity", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()})
+ var ret0 bool
if len(result) != 0 {
if result[0] != nil {
- ret0 = result[0].(string)
- }
- if result[1] != nil {
- ret1 = result[1].(error)
+ ret0 = result[0].(bool)
}
}
- return ret0, ret1
+ return ret0
}
func (mock *MockConnection) DialOrDie() kubernetes.Interface {
diff --git a/internal/dao/alias.go b/internal/dao/alias.go
index 300fd2fe..29189e3b 100644
--- a/internal/dao/alias.go
+++ b/internal/dao/alias.go
@@ -36,7 +36,13 @@ func (a *Alias) Clear() {
}
}
-// List returns a collection of screen dumps.
+// Check verifies an alias is defined for this command.
+func (a *Alias) Check(cmd string) bool {
+ _, ok := a.Aliases.Get(cmd)
+ return ok
+}
+
+// List returns a collection of aliases.
// BOZO!! Already have aliases here. Refact!!
func (a *Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) {
a, ok := ctx.Value(internal.KeyAliases).(*Alias)
@@ -73,8 +79,7 @@ func (a *Alias) AsGVR(cmd string) (client.GVR, bool) {
// Get fetch a resource.
func (a *Alias) Get(_ context.Context, _ string) (runtime.Object, error) {
- // BOZO!! NYI
- panic("NYI!")
+ return nil, errors.New("NYI!!")
}
// Ensure makes sure alias are loaded.
diff --git a/internal/dao/container_test.go b/internal/dao/container_test.go
index e8f7c356..2b61a9d1 100644
--- a/internal/dao/container_test.go
+++ b/internal/dao/container_test.go
@@ -50,6 +50,7 @@ func (c *conn) RestConfigOrDie() *restclient.Config { return nil }
func (c *conn) MXDial() (*versioned.Clientset, error) { return nil, nil }
func (c *conn) DynDialOrDie() dynamic.Interface { return nil }
func (c *conn) HasMetrics() bool { return false }
+func (c *conn) CheckConnectivity() bool { return false }
func (c *conn) IsNamespaced(n string) bool { return false }
func (c *conn) SupportsResource(group string) bool { return false }
func (c *conn) ValidNamespaces() ([]v1.Namespace, error) { return nil, nil }
diff --git a/internal/dao/registry.go b/internal/dao/registry.go
index 52a84ed3..138b0b2a 100644
--- a/internal/dao/registry.go
+++ b/internal/dao/registry.go
@@ -161,6 +161,7 @@ func loadK9s(m ResourceMetas) {
Name: "containers",
Kind: "Containers",
SingularName: "container",
+ Verbs: []string{},
Categories: []string{"k9s"},
}
}
diff --git a/internal/render/container.go b/internal/render/container.go
index 77ea3b71..0e74e04f 100644
--- a/internal/render/container.go
+++ b/internal/render/container.go
@@ -75,10 +75,10 @@ func (Container) Header(ns string) HeaderRow {
Header{Name: "PROBES(L:R)"},
Header{Name: "CPU", Align: tview.AlignRight},
Header{Name: "MEM", Align: tview.AlignRight},
- Header{Name: "%CPU", Align: tview.AlignRight},
- Header{Name: "%MEM", Align: tview.AlignRight},
- Header{Name: "%MAX-CPU", Align: tview.AlignRight},
- Header{Name: "%MAX-MEM", Align: tview.AlignRight},
+ Header{Name: "%CPU/R", Align: tview.AlignRight},
+ Header{Name: "%MEM/R", Align: tview.AlignRight},
+ Header{Name: "%CPU/L", Align: tview.AlignRight},
+ Header{Name: "%MEM/L", Align: tview.AlignRight},
Header{Name: "PORTS"},
Header{Name: "AGE", Decorator: AgeDecorator},
}
diff --git a/internal/render/pod.go b/internal/render/pod.go
index 24495cec..ded5b483 100644
--- a/internal/render/pod.go
+++ b/internal/render/pod.go
@@ -78,10 +78,10 @@ func (Pod) Header(ns string) HeaderRow {
Header{Name: "RS", Align: tview.AlignRight},
Header{Name: "CPU", Align: tview.AlignRight},
Header{Name: "MEM", Align: tview.AlignRight},
- Header{Name: "%CPU", Align: tview.AlignRight},
- Header{Name: "%MEM", Align: tview.AlignRight},
- Header{Name: "%MAX_CPU", Align: tview.AlignRight},
- Header{Name: "%MAX_MEM", Align: tview.AlignRight},
+ Header{Name: "%CPU/R", Align: tview.AlignRight},
+ Header{Name: "%MEM/R", Align: tview.AlignRight},
+ Header{Name: "%CPU/L", Align: tview.AlignRight},
+ Header{Name: "%MEM/L", Align: tview.AlignRight},
Header{Name: "IP"},
Header{Name: "NODE"},
Header{Name: "QOS"},
diff --git a/internal/view/app.go b/internal/view/app.go
index 1d48a7fb..5700aa5a 100644
--- a/internal/view/app.go
+++ b/internal/view/app.go
@@ -18,6 +18,9 @@ import (
"github.com/rs/zerolog/log"
)
+// ExitStatus indicates UI exit conditions.
+var ExitStatus = ""
+
const (
splashTime = 1
clusterRefresh = 5 * time.Second
@@ -185,10 +188,14 @@ func (a *App) clusterUpdater(ctx context.Context) {
// BOZO!! Refact to use model/view strategy.
func (a *App) refreshClusterInfo() {
- if !a.Conn().Config().CheckConnectivity() {
- log.Error().Msgf("Something is wrong with the connection. Bailing out!")
+ if !a.Conn().CheckConnectivity() {
+ ExitStatus = "Lost K8s connection. Bailing out!"
a.BailOut()
}
+ // Reload alias
+ if err := a.command.Reset(); err != nil {
+ log.Error().Err(err).Msgf("Command reset failed")
+ }
a.QueueUpdateDraw(func() {
if !a.showHeader {
a.refreshIndicator()
@@ -288,7 +295,7 @@ func (a *App) BailOut() {
}
// Run starts the application loop
-func (a *App) Run() {
+func (a *App) Run() error {
a.Resume()
go func() {
@@ -299,11 +306,13 @@ func (a *App) Run() {
}()
if err := a.command.defaultCmd(); err != nil {
- panic(err)
+ return err
}
if err := a.Application.Run(); err != nil {
- panic(err)
+ return err
}
+
+ return nil
}
// Status reports a new app status for display.
diff --git a/internal/view/command.go b/internal/view/command.go
index 8eae512a..954d0b5e 100644
--- a/internal/view/command.go
+++ b/internal/view/command.go
@@ -43,6 +43,16 @@ func (c *Command) Init() error {
return nil
}
+// Reset resets Command and reload aliases.
+func (c *Command) Reset() error {
+ c.alias.Clear()
+ if _, err := c.alias.Ensure(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
func allowedXRay(gvr client.GVR) bool {
gg := []string{
"v1/pods",
@@ -52,7 +62,6 @@ func allowedXRay(gvr client.GVR) bool {
"apps/v1/statefulsets",
"apps/v1/replicasets",
}
-
for _, g := range gg {
if g == gvr.String() {
return true
@@ -117,23 +126,18 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
if !c.app.switchNS(ns) {
return fmt.Errorf("namespace switch failed for ns %q", ns)
}
-
+ if !c.alias.Check(cmds[0]) {
+ return fmt.Errorf("Huh? `%s` Command not found", cmd)
+ }
return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack)
}
}
-// Reset resets Command and reload aliases.
-func (c *Command) Reset() error {
- c.alias.Clear()
- if _, err := c.alias.Ensure(); err != nil {
- return err
- }
-
- return nil
-}
-
func (c *Command) defaultCmd() error {
- return c.run(c.app.Config.ActiveView(), "", true)
+ if err := c.run(c.app.Config.ActiveView(), "", true); err != nil {
+ log.Error().Err(err).Msgf("Saved command failed. Loading default view")
+ }
+ return c.run("pod", "", true)
}
func (c *Command) specialCmd(cmd string) bool {
@@ -204,7 +208,7 @@ func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer {
func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) error {
if comp == nil {
- return fmt.Errorf("No component given for %s", gvr)
+ return fmt.Errorf("No component found for %s", gvr)
}
c.app.Flash().Infof("Viewing %s...", client.NewGVR(gvr).R())
c.app.Config.SetActiveView(cmd)
diff --git a/internal/view/container.go b/internal/view/container.go
index 7b24fc81..0cabd372 100644
--- a/internal/view/container.go
+++ b/internal/view/container.go
@@ -40,12 +40,14 @@ func (c *Container) Name() string { return containerTitle }
func (c *Container) bindKeys(aa ui.KeyActions) {
aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace)
aa.Add(ui.KeyActions{
- ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true),
- ui.KeyS: ui.NewKeyAction("Shell", c.shellCmd, true),
- ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(6, false), false),
- ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(7, false), false),
- ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", c.GetTable().SortColCmd(8, false), false),
- ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", c.GetTable().SortColCmd(9, false), false),
+ ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true),
+ ui.KeyS: ui.NewKeyAction("Shell", c.shellCmd, true),
+ ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(6, false), false),
+ ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(7, false), false),
+ ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", c.GetTable().SortColCmd(8, false), false),
+ ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", c.GetTable().SortColCmd(9, false), false),
+ tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", c.GetTable().SortColCmd(8, false), false),
+ tcell.KeyCtrlZ: ui.NewKeyAction("Sort %MEM (LIM)", c.GetTable().SortColCmd(9, false), false),
})
}
diff --git a/internal/view/container_test.go b/internal/view/container_test.go
index 80f8de8b..7e8497fa 100644
--- a/internal/view/container_test.go
+++ b/internal/view/container_test.go
@@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) {
assert.Nil(t, c.Init(makeCtx()))
assert.Equal(t, "Containers", c.Name())
- assert.Equal(t, 11, len(c.Hints()))
+ assert.Equal(t, 13, len(c.Hints()))
}
diff --git a/internal/view/pod.go b/internal/view/pod.go
index d11b2120..a7100ac3 100644
--- a/internal/view/pod.go
+++ b/internal/view/pod.go
@@ -46,10 +46,10 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd(3, false), false),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", p.GetTable().SortColCmd(4, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", p.GetTable().SortColCmd(5, false), false),
- ui.KeyShiftX: ui.NewKeyAction("Sort CPU%", p.GetTable().SortColCmd(6, false), false),
- ui.KeyShiftZ: ui.NewKeyAction("Sort MEM%", p.GetTable().SortColCmd(7, false), false),
- tcell.KeyCtrlX: ui.NewKeyAction("Sort CPU% LIMITS", p.GetTable().SortColCmd(8, false), false),
- tcell.KeyCtrlZ: ui.NewKeyAction("Sort MEM% LIMITS", p.GetTable().SortColCmd(9, false), false),
+ ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", p.GetTable().SortColCmd(6, false), false),
+ ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", p.GetTable().SortColCmd(7, false), false),
+ tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", p.GetTable().SortColCmd(8, false), false),
+ tcell.KeyCtrlZ: ui.NewKeyAction("Sort %MEM (LIM)", p.GetTable().SortColCmd(9, false), false),
ui.KeyShiftI: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd(10, true), false),
ui.KeyShiftO: ui.NewKeyAction("Sort Node", p.GetTable().SortColCmd(11, true), false),
})