Merge branch '5_14_20'

mine
derailed 2020-05-15 16:33:30 -06:00
commit 9f1b099e29
20 changed files with 287 additions and 91 deletions

View File

@ -15,7 +15,7 @@ FROM alpine:3.10.0
COPY --from=build /k9s/execs/k9s /bin/k9s COPY --from=build /k9s/execs/k9s /bin/k9s
ENV KUBE_LATEST_VERSION="v1.18.1" ENV KUBE_LATEST_VERSION="v1.18.1"
RUN apk add --update ca-certificates \ RUN apk add --update ca-certificates \
&& apk add --update -t deps curl \ && apk add --update -t deps curl vim \
&& curl -L https://storage.googleapis.com/kubernetes-release/release/${KUBE_LATEST_VERSION}/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl \ && curl -L https://storage.googleapis.com/kubernetes-release/release/${KUBE_LATEST_VERSION}/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl \
&& chmod +x /usr/local/bin/kubectl \ && chmod +x /usr/local/bin/kubectl \
&& apk del --purge deps \ && apk del --purge deps \

View File

@ -0,0 +1,72 @@
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png" align="right" width="200" height="auto"/>
# Release v0.19.5
## 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, consider joining our [sponsorhip 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)
---
## A Word From Out Sponsors...
First off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!
* [Tommy Dejbjerg Pedersen](https://github.com/tpedersen123)
* [Matt Welke](https://github.com/mattwelke)
## Disruption In The Force
During this drop, I've gotten totally slammed by other forces ;( I've had so many disruptions that affected my `quasi` normal flow hence this drop might be a bit wonky ;( So please proceed with caution!!
As always please help me flush/report issues and I'll address them promptly! Thank you so much for your understanding and patience!! 🙏👨‍❤️‍👨😍
## Improved Node Shell Usability
In this drop we've changed the configuration of the node shell action that let's you shell into nodes. Big thanks to [Patrick Decat](https://github.com/pdecat) for helping us flesh out this beta feature! We've added configuration to not only customize the image but also the resources and namespace on how to run these K9s pods on your clusters. The new configuration is set at the cluster scope level.
Here is an example of the new pod shell config options:
```yaml
# $HOME/.k9s/config.yml
k9s:
clusters:
blee:
featureGates:
# You must enable the nodeShell feature gate to enable shelling into nodes
nodeShell: true
# NEW! You can now tune the pod specification: currently image, namespace and resources
shellPod:
image: cool_kid_admin:42
namespace: blee
limits:
cpu: 100m
memory: 100Mi
```
## Resolved Bugs/Features/PRs
* [Issue #714](https://github.com/derailed/k9s/issues/714)
* [Issue #713](https://github.com/derailed/k9s/issues/713)
* [Issue #708](https://github.com/derailed/k9s/issues/708)
* [Issue #707](https://github.com/derailed/k9s/issues/707)
* [Issue #705](https://github.com/derailed/k9s/issues/705)
* [Issue #704](https://github.com/derailed/k9s/issues/704)
* [Issue #702](https://github.com/derailed/k9s/issues/702)
* [Issue #700](https://github.com/derailed/k9s/issues/700) Fingers and toes crossed ;)
* [Issue #694](https://github.com/derailed/k9s/issues/694)
* [Issue #663](https://github.com/derailed/k9s/issues/663) Partially - should be better launching in a given namespace ie k9s -n fred??
* [Issue #702](https://github.com/derailed/k9s/issues/702)
* [PR #709](https://github.com/derailed/k9s/pull/709) All credits goes to [Namco](https://github.com/namco1992)!!
* [PR #706](https://github.com/derailed/k9s/pull/706) Big Thanks to [M. Tarık Yurt](https://github.com/mtyurt)!
* [PR #704](https://github.com/derailed/k9s/pull/704) Atta Boy!! [psvo](https://github.com/psvo)
* [PR #696](https://github.com/derailed/k9s/pull/696) Thank you! Credits to [Christian Köhn](https://github.com/ckoehn)
* [PR #691](https://github.com/derailed/k9s/pull/691) Mega Thanks To [Pavel Tumik](https://github.com/sagor999)!
---
<img src="https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png" width="32" height="auto"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)

View File

@ -105,7 +105,6 @@ func loadConfiguration() *config.Config {
log.Warn().Msg("Unable to locate K9s config. Generating new configuration...") log.Warn().Msg("Unable to locate K9s config. Generating new configuration...")
} }
log.Debug().Msgf("DEMO MODE %#v", demoMode)
if demoMode != nil { if demoMode != nil {
k9sCfg.SetDemoMode(*demoMode) k9sCfg.SetDemoMode(*demoMode)
} }

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.13
require ( require (
github.com/atotto/clipboard v0.1.2 github.com/atotto/clipboard v0.1.2
github.com/derailed/popeye v0.8.1 github.com/derailed/popeye v0.8.2
github.com/derailed/tview v0.3.10 github.com/derailed/tview v0.3.10
github.com/drone/envsubst v1.0.2 // indirect github.com/drone/envsubst v1.0.2 // indirect
github.com/fatih/color v1.9.0 github.com/fatih/color v1.9.0

2
go.sum
View File

@ -137,6 +137,8 @@ github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xb
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/derailed/popeye v0.8.1 h1:N69XH0NZTBkrNj8qvUzy6Z6bP7+jx0AwollETqvc3dc= github.com/derailed/popeye v0.8.1 h1:N69XH0NZTBkrNj8qvUzy6Z6bP7+jx0AwollETqvc3dc=
github.com/derailed/popeye v0.8.1/go.mod h1:OBHcJDa50VpE9QNyOU243bNOtHb29MyLlVHJolwlwas= github.com/derailed/popeye v0.8.1/go.mod h1:OBHcJDa50VpE9QNyOU243bNOtHb29MyLlVHJolwlwas=
github.com/derailed/popeye v0.8.2 h1:O4JcIC3MwJS9pNKb8ZHLUJKoLPZW0gfcMqFoSYYdbAs=
github.com/derailed/popeye v0.8.2/go.mod h1:i4ge2tKHDKXgUq3NzFlIhVIBNHS0zFDMJWXsC2bVe2A=
github.com/derailed/tview v0.3.10 h1:n+iQwYh9Ff9STdR5hBhp+rTJRlu59q2xP2pHvwQbYPw= github.com/derailed/tview v0.3.10 h1:n+iQwYh9Ff9STdR5hBhp+rTJRlu59q2xP2pHvwQbYPw=
github.com/derailed/tview v0.3.10/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII= github.com/derailed/tview v0.3.10/go.mod h1:GJ3k/TIzEE+sj1L09/usk6HrkjsdadSsb03eHgPbcII=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=

View File

@ -9,6 +9,7 @@ type Cluster struct {
Namespace *Namespace `yaml:"namespace"` Namespace *Namespace `yaml:"namespace"`
View *View `yaml:"view"` View *View `yaml:"view"`
FeatureGates *FeatureGates `yaml:"featureGates"` FeatureGates *FeatureGates `yaml:"featureGates"`
ShellPod *ShellPod `yaml:"shellPod"`
} }
// NewCluster creates a new cluster configuration. // NewCluster creates a new cluster configuration.
@ -17,6 +18,7 @@ func NewCluster() *Cluster {
Namespace: NewNamespace(), Namespace: NewNamespace(),
View: NewView(), View: NewView(),
FeatureGates: NewFeatureGates(), FeatureGates: NewFeatureGates(),
ShellPod: NewShellPod(),
} }
} }
@ -34,4 +36,9 @@ func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) {
c.View = NewView() c.View = NewView()
} }
c.View.Validate() c.View.Validate()
if c.ShellPod == nil {
c.ShellPod = NewShellPod()
}
c.ShellPod.Validate(conn, ks)
} }

View File

@ -84,13 +84,13 @@ func (c *Config) Refine(flags *genericclioptions.ConfigFlags) error {
if c.K9s.CurrentContext == "" { if c.K9s.CurrentContext == "" {
return errors.New("Invalid kubeconfig context detected") return errors.New("Invalid kubeconfig context detected")
} }
ctx, ok := cfg.Contexts[c.K9s.CurrentContext] context, ok := cfg.Contexts[c.K9s.CurrentContext]
if !ok { if !ok {
return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext) return fmt.Errorf("The specified context %q does not exists in kubeconfig", c.K9s.CurrentContext)
} }
c.K9s.CurrentCluster = ctx.Cluster c.K9s.CurrentCluster = context.Cluster
if len(ctx.Namespace) != 0 { if len(context.Namespace) != 0 {
if err := c.SetActiveNamespace(ctx.Namespace); err != nil { if err := c.SetActiveNamespace(context.Namespace); err != nil {
return err return err
} }
} }
@ -124,13 +124,6 @@ func (c *Config) CurrentCluster() *Cluster {
// ActiveNamespace returns the active namespace in the current cluster. // ActiveNamespace returns the active namespace in the current cluster.
func (c *Config) ActiveNamespace() string { func (c *Config) ActiveNamespace() string {
if c.client != nil {
ns := c.client.ActiveNamespace()
if client.IsNamespaced(ns) {
return ns
}
}
if cl := c.CurrentCluster(); cl != nil { if cl := c.CurrentCluster(); cl != nil {
if cl.Namespace != nil { if cl.Namespace != nil {
return cl.Namespace.Active return cl.Namespace.Active
@ -162,9 +155,9 @@ func (c *Config) SetActiveNamespace(ns string) error {
if c.K9s.ActiveCluster() != nil { if c.K9s.ActiveCluster() != nil {
return c.K9s.ActiveCluster().Namespace.SetActive(ns, c.settings) return c.K9s.ActiveCluster().Namespace.SetActive(ns, c.settings)
} }
err := errors.New("no active cluster. unable to set active namespace") err := errors.New("no active cluster. unable to set active namespace")
log.Error().Err(err).Msg("SetActiveNamespace") log.Error().Err(err).Msg("SetActiveNamespace")
return err return err
} }

View File

@ -261,7 +261,6 @@ func TestSetup(t *testing.T) {
var expectedConfig = `k9s: var expectedConfig = `k9s:
refreshRate: 100 refreshRate: 100
dockerShellImage: busybox:1.31
headless: false headless: false
readOnly: true readOnly: true
noIcons: false noIcons: false
@ -284,6 +283,12 @@ var expectedConfig = `k9s:
active: po active: po
featureGates: featureGates:
nodeShell: false nodeShell: false
shellPod:
image: busybox:1.31
namespace: default
limits:
cpu: 100m
memory: 100Mi
fred: fred:
namespace: namespace:
active: default active: default
@ -297,6 +302,12 @@ var expectedConfig = `k9s:
active: po active: po
featureGates: featureGates:
nodeShell: false nodeShell: false
shellPod:
image: busybox:1.31
namespace: default
limits:
cpu: 100m
memory: 100Mi
minikube: minikube:
namespace: namespace:
active: kube-system active: kube-system
@ -310,6 +321,12 @@ var expectedConfig = `k9s:
active: ctx active: ctx
featureGates: featureGates:
nodeShell: false nodeShell: false
shellPod:
image: busybox:1.31
namespace: default
limits:
cpu: 100m
memory: 100Mi
thresholds: thresholds:
cpu: cpu:
critical: 90 critical: 90
@ -321,7 +338,6 @@ var expectedConfig = `k9s:
var resetConfig = `k9s: var resetConfig = `k9s:
refreshRate: 2 refreshRate: 2
dockerShellImage: busybox:1.31
headless: false headless: false
readOnly: false readOnly: false
noIcons: false noIcons: false
@ -344,6 +360,12 @@ var resetConfig = `k9s:
active: po active: po
featureGates: featureGates:
nodeShell: false nodeShell: false
shellPod:
image: busybox:1.31
namespace: default
limits:
cpu: 100m
memory: 100Mi
thresholds: thresholds:
cpu: cpu:
critical: 90 critical: 90

View File

@ -2,16 +2,11 @@ package config
import "github.com/derailed/k9s/internal/client" import "github.com/derailed/k9s/internal/client"
const ( const defaultRefreshRate = 2
defaultRefreshRate = 2
// DefaultDockerShellImage specifies the docker image and tag for shelling into nodes.
DefaultDockerShellImage = "busybox:1.31"
)
// K9s tracks K9s configuration options. // K9s tracks K9s configuration options.
type K9s struct { type K9s struct {
RefreshRate int `yaml:"refreshRate"` RefreshRate int `yaml:"refreshRate"`
DockerShellImage string `yaml:"dockerShellImage"`
Headless bool `yaml:"headless"` Headless bool `yaml:"headless"`
ReadOnly bool `yaml:"readOnly"` ReadOnly bool `yaml:"readOnly"`
NoIcons bool `yaml:"noIcons"` NoIcons bool `yaml:"noIcons"`
@ -30,7 +25,6 @@ type K9s struct {
func NewK9s() *K9s { func NewK9s() *K9s {
return &K9s{ return &K9s{
RefreshRate: defaultRefreshRate, RefreshRate: defaultRefreshRate,
DockerShellImage: DefaultDockerShellImage,
Logger: NewLogger(), Logger: NewLogger(),
Clusters: make(map[string]*Cluster), Clusters: make(map[string]*Cluster),
Thresholds: NewThreshold(), Thresholds: NewThreshold(),
@ -104,9 +98,6 @@ func (k *K9s) validateDefaults() {
if k.RefreshRate <= 0 { if k.RefreshRate <= 0 {
k.RefreshRate = defaultRefreshRate k.RefreshRate = defaultRefreshRate
} }
if k.DockerShellImage == "" {
k.DockerShellImage = DefaultDockerShellImage
}
} }
func (k *K9s) validateClusters(c client.Connection, ks KubeSettings) { func (k *K9s) validateClusters(c client.Connection, ks KubeSettings) {

View File

@ -0,0 +1,44 @@
package config
import (
"github.com/derailed/k9s/internal/client"
v1 "k8s.io/api/core/v1"
)
const defaultDockerShellImage = "busybox:1.31"
// Limits represents resource limits.
type Limits map[v1.ResourceName]string
// ShellPod represents k9s shell configuration.
type ShellPod struct {
Image string `json:"Image"`
Namespace string `json:"namespace"`
Limits Limits `json:"resources,omitempty"`
}
// NewShellPod returns a new instance.
func NewShellPod() *ShellPod {
return &ShellPod{
Image: defaultDockerShellImage,
Namespace: "default",
Limits: defaultLimits(),
}
}
// Validate validates the configuration.
func (s *ShellPod) Validate(client.Connection, KubeSettings) {
if s.Image == "" {
s.Image = defaultDockerShellImage
}
if len(s.Limits) == 0 {
s.Limits = defaultLimits()
}
}
func defaultLimits() Limits {
return Limits{
v1.ResourceCPU: "100m",
v1.ResourceMemory: "100Mi",
}
}

View File

@ -88,8 +88,13 @@ func (a *App) Init(version string, rate int) error {
return errors.New("No client connection detected") return errors.New("No client connection detected")
} }
ns, err := a.Conn().Config().CurrentNamespaceName() ns, err := a.Conn().Config().CurrentNamespaceName()
log.Debug().Msgf("CURRENT-NS %q -- %v", ns, err)
if err != nil { if err != nil {
log.Info().Msg("No namespace specified using all namespaces") log.Info().Msg("No namespace specified using cluster default namespace")
} else {
if err := a.Config.SetActiveNamespace(ns); err != nil {
log.Error().Err(err).Msgf("Fail to set active namespace to %q", ns)
}
} }
a.factory = watch.NewFactory(a.Conn()) a.factory = watch.NewFactory(a.Conn())
@ -111,6 +116,13 @@ func (a *App) Init(version string, rate int) error {
a.CmdBuff().SetSuggestionFn(a.suggestCommand()) a.CmdBuff().SetSuggestionFn(a.suggestCommand())
a.CmdBuff().AddListener(a) a.CmdBuff().AddListener(a)
a.layout(ctx, version)
a.initSignals()
return nil
}
func (a *App) layout(ctx context.Context, version string) {
flash := ui.NewFlash(a.App) flash := ui.NewFlash(a.App)
go flash.Watch(ctx, a.Flash().Channel()) go flash.Watch(ctx, a.Flash().Channel())
@ -123,10 +135,6 @@ func (a *App) Init(version string, rate int) error {
a.Main.AddPage("main", main, true, false) a.Main.AddPage("main", main, true, false)
a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true) a.Main.AddPage("splash", ui.NewSplash(a.Styles, version), true, true)
a.toggleHeader(!a.Config.K9s.GetHeadless()) a.toggleHeader(!a.Config.K9s.GetHeadless())
a.initSignals()
return nil
} }
func (a *App) initSignals() { func (a *App) initSignals() {
@ -134,8 +142,12 @@ func (a *App) initSignals() {
signal.Notify(sig, syscall.SIGABRT, syscall.SIGINT, syscall.SIGHUP, syscall.SIGQUIT) signal.Notify(sig, syscall.SIGABRT, syscall.SIGINT, syscall.SIGHUP, syscall.SIGQUIT)
go func(sig chan os.Signal) { go func(sig chan os.Signal) {
<-sig signal := <-sig
nukeK9sShell(a.Conn()) if signal == syscall.SIGHUP {
a.BailOut()
return
}
nukeK9sShell(a)
}(sig) }(sig)
} }
@ -375,7 +387,7 @@ func (a *App) BailOut() {
} }
}() }()
nukeK9sShell(a.Conn()) nukeK9sShell(a)
a.factory.Terminate() a.factory.Terminate()
a.App.BailOut() a.App.BailOut()
} }

View File

@ -158,7 +158,18 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
} }
func (c *Command) defaultCmd() error { func (c *Command) defaultCmd() error {
if err := c.run(c.app.Config.ActiveView(), "", true); err != nil { view := c.app.Config.ActiveView()
if view == "" {
return c.run("pod", "", true)
}
tokens := strings.Split(view, " ")
cmd := view
ns, err := c.app.Conn().Config().CurrentNamespaceName()
if err == nil {
cmd = tokens[0] + " " + ns
}
if err := c.run(cmd, "", true); err != nil {
log.Error().Err(err).Msgf("Saved command load failed. Loading default view") log.Error().Err(err).Msgf("Saved command load failed. Loading default view")
return c.run("pod", "", true) return c.run("pod", "", true)
} }

View File

@ -52,13 +52,8 @@ func (c *Container) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{ aa.Add(ui.KeyActions{
ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true),
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", c.GetTable().SortColCmd(cpuCol, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", c.GetTable().SortColCmd(memCol, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", c.GetTable().SortColCmd("%CPU/R", false), false),
ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", c.GetTable().SortColCmd("%MEM/R", false), false),
tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", c.GetTable().SortColCmd("%CPU/L", false), false),
tcell.KeyCtrlQ: ui.NewKeyAction("Sort %MEM (LIM)", c.GetTable().SortColCmd("%MEM/L", false), false),
}) })
aa.Add(resourceSorters(c.GetTable()))
} }
func (c *Container) k9sEnv() Env { func (c *Container) k9sEnv() Env {

View File

@ -129,27 +129,33 @@ func clearScreen() {
const ( const (
k9sShell = "k9s-shell" k9sShell = "k9s-shell"
k9sShellNS = "default"
k9sShellRetryCount = 10 k9sShellRetryCount = 10
k9sShellRetryDelay = 500 * time.Millisecond k9sShellRetryDelay = 500 * time.Millisecond
) )
func ssh(a *App, node string) error { func ssh(a *App, node string) error {
nukeK9sShell(a.Conn()) nukeK9sShell(a)
defer nukeK9sShell(a.Conn()) defer nukeK9sShell(a)
if err := launchShellPod(a, node); err != nil { if err := launchShellPod(a, node); err != nil {
return err return err
} }
shellIn(a, client.FQN(k9sShellNS, k9sShellPodName()), k9sShell) ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
shellIn(a, client.FQN(ns, k9sShellPodName()), k9sShell)
return nil return nil
} }
func nukeK9sShell(c client.Connection) { func nukeK9sShell(a *App) {
cl := a.Config.K9s.CurrentCluster
if !a.Config.K9s.Clusters[cl].FeatureGates.NodeShell {
return
}
ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel() defer cancel()
err := c.DialOrDie().CoreV1().Pods(k9sShellNS).Delete(ctx, k9sShellPodName(), metav1.DeleteOptions{}) err := a.Conn().DialOrDie().CoreV1().Pods(ns).Delete(ctx, k9sShellPodName(), metav1.DeleteOptions{})
if kerrors.IsNotFound(err) { if kerrors.IsNotFound(err) {
return return
} }
@ -159,20 +165,17 @@ func nukeK9sShell(c client.Connection) {
} }
func launchShellPod(a *App, node string) error { func launchShellPod(a *App, node string) error {
img := a.Config.K9s.DockerShellImage ns := a.Config.K9s.ActiveCluster().ShellPod.Namespace
if img == "" { spec := k9sShellPod(node, a.Config.K9s.ActiveCluster().ShellPod)
img = config.DefaultDockerShellImage
}
spec := k9sShellPod(node, img)
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel() defer cancel()
dial := a.Conn().DialOrDie().CoreV1().Pods(k9sShellNS) dial := a.Conn().DialOrDie().CoreV1().Pods(ns)
if _, err := dial.Create(ctx, &spec, metav1.CreateOptions{}); err != nil { if _, err := dial.Create(ctx, &spec, metav1.CreateOptions{}); err != nil {
return err return err
} }
for i := 0; i < k9sShellRetryCount; i++ { for i := 0; i < k9sShellRetryCount; i++ {
o, err := a.factory.Get("v1/pods", client.FQN(k9sShellNS, k9sShellPodName()), true, labels.Everything()) o, err := a.factory.Get("v1/pods", client.FQN(ns, k9sShellPodName()), true, labels.Everything())
if err != nil { if err != nil {
time.Sleep(k9sShellRetryDelay) time.Sleep(k9sShellRetryDelay)
continue continue
@ -194,14 +197,14 @@ func k9sShellPodName() string {
return fmt.Sprintf("%s-%d", k9sShell, os.Getpid()) return fmt.Sprintf("%s-%d", k9sShell, os.Getpid())
} }
func k9sShellPod(node, image string) v1.Pod { func k9sShellPod(node string, cfg *config.ShellPod) v1.Pod {
var grace int64 var grace int64
var priv bool = true var priv bool = true
return v1.Pod{ return v1.Pod{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: k9sShellPodName(), Name: k9sShellPodName(),
Namespace: k9sShellNS, Namespace: cfg.Namespace,
}, },
Spec: v1.PodSpec{ Spec: v1.PodSpec{
NodeName: node, NodeName: node,
@ -222,7 +225,7 @@ func k9sShellPod(node, image string) v1.Pod {
Containers: []v1.Container{ Containers: []v1.Container{
{ {
Name: k9sShell, Name: k9sShell,
Image: image, Image: cfg.Image,
VolumeMounts: []v1.VolumeMount{ VolumeMounts: []v1.VolumeMount{
{ {
Name: "root-vol", Name: "root-vol",
@ -230,12 +233,7 @@ func k9sShellPod(node, image string) v1.Pod {
ReadOnly: true, ReadOnly: true,
}, },
}, },
Resources: v1.ResourceRequirements{ Resources: asResource(cfg.Limits),
Limits: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("200m"),
v1.ResourceMemory: resource.MustParse("100Mi"),
},
},
Stdin: true, Stdin: true,
SecurityContext: &v1.SecurityContext{ SecurityContext: &v1.SecurityContext{
Privileged: &priv, Privileged: &priv,
@ -245,3 +243,12 @@ func k9sShellPod(node, image string) v1.Pod {
}, },
} }
} }
func asResource(r config.Limits) v1.ResourceRequirements {
return v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceCPU: resource.MustParse(r[v1.ResourceCPU]),
v1.ResourceMemory: resource.MustParse(r[v1.ResourceMemory]),
},
}
}

View File

@ -55,15 +55,10 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(readyCol, true), false), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(readyCol, true), false),
ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd("RESTARTS", false), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd("RESTARTS", false), false),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(statusCol, true), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(statusCol, true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", p.GetTable().SortColCmd(cpuCol, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", p.GetTable().SortColCmd(memCol, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", p.GetTable().SortColCmd("%CPU", false), false),
ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", p.GetTable().SortColCmd("%MEM", false), false),
tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", p.GetTable().SortColCmd("%CPU/L", false), false),
tcell.KeyCtrlQ: ui.NewKeyAction("Sort %MEM (LIM)", p.GetTable().SortColCmd("%MEM/L", false), false),
ui.KeyShiftI: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd("IP", true), false), ui.KeyShiftI: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd("IP", true), false),
ui.KeyShiftO: ui.NewKeyAction("Sort Node", p.GetTable().SortColCmd("NODE", true), false), ui.KeyShiftO: ui.NewKeyAction("Sort Node", p.GetTable().SortColCmd("NODE", true), false),
}) })
aa.Add(resourceSorters(p.GetTable()))
} }
func (p *Pod) selectedContainer() string { func (p *Pod) selectedContainer() string {
@ -318,3 +313,14 @@ func podIsRunning(f dao.Factory, path string) bool {
log.Debug().Msgf("Phase %#v", re.Phase(po)) log.Debug().Msgf("Phase %#v", re.Phase(po))
return re.Phase(po) == render.Running return re.Phase(po) == render.Running
} }
func resourceSorters(t *Table) ui.KeyActions {
return ui.KeyActions{
ui.KeyShiftC: ui.NewKeyAction("Sort CPU", t.SortColCmd(cpuCol, false), false),
ui.KeyShiftM: ui.NewKeyAction("Sort MEM", t.SortColCmd(memCol, false), false),
ui.KeyShiftX: ui.NewKeyAction("Sort %CPU (REQ)", t.SortColCmd("%CPU/R", false), false),
ui.KeyShiftZ: ui.NewKeyAction("Sort %MEM (REQ)", t.SortColCmd("%MEM/R", false), false),
tcell.KeyCtrlX: ui.NewKeyAction("Sort %CPU (LIM)", t.SortColCmd("%CPU/L", false), false),
tcell.KeyCtrlQ: ui.NewKeyAction("Sort %MEM (LIM)", t.SortColCmd("%MEM/L", false), false),
}
}

View File

@ -53,8 +53,13 @@ func (p *Popeye) decorateRows(data render.TableData) render.TableData {
} }
sum += n sum += n
} }
score := sum / len(data.RowEvents) score, letter := 0, render.NAValue
p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, grade(score)) if len(data.RowEvents) > 0 {
score = sum / len(data.RowEvents)
letter = grade(score)
}
p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, letter)
return data return data
} }

32
internal/view/pvc.go Normal file
View File

@ -0,0 +1,32 @@
package view
import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
)
// PersistentVolumeClaim represents a PVC custom viewer.
type PersistentVolumeClaim struct {
ResourceViewer
}
// NewPersistentVolumeClaim returns a new viewer.
func NewPersistentVolumeClaim(gvr client.GVR) ResourceViewer {
d := PersistentVolumeClaim{
ResourceViewer: NewBrowser(gvr),
}
d.SetBindKeysFn(d.bindKeys)
d.GetTable().SetColorerFn(render.PersistentVolumeClaim{}.ColorerFunc())
return &d
}
func (d *PersistentVolumeClaim) bindKeys(aa ui.KeyActions) {
aa.Add(ui.KeyActions{
ui.KeyShiftS: ui.NewKeyAction("Sort Status", d.GetTable().SortColCmd("STATUS", true), false),
ui.KeyShiftV: ui.NewKeyAction("Sort Volume", d.GetTable().SortColCmd("VOLUME", true), false),
ui.KeyShiftO: ui.NewKeyAction("Sort StorageClass", d.GetTable().SortColCmd("STORAGECLASS", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Capacity", d.GetTable().SortColCmd("CAPACITY", true), false),
})
}

View File

@ -45,6 +45,9 @@ func coreViewers(vv MetaViewers) {
vv[client.NewGVR("v1/secrets")] = MetaViewer{ vv[client.NewGVR("v1/secrets")] = MetaViewer{
viewerFn: NewSecret, viewerFn: NewSecret,
} }
vv[client.NewGVR("v1/persistentvolumeclaims")] = MetaViewer{
viewerFn: NewPersistentVolumeClaim,
}
} }
func miscViewers(vv MetaViewers) { func miscViewers(vv MetaViewers) {

View File

@ -163,13 +163,6 @@ func (f *Factory) isClusterWide() bool {
// CanForResource return an informer is user has access. // CanForResource return an informer is user has access.
func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) {
// If user can access resource cluster wide, prefer cluster wide factory.
if !client.IsClusterWide(ns) {
auth, err := f.Client().CanI(client.AllNamespaces, gvr, verbs)
if auth && err == nil {
return f.ForResource(client.AllNamespaces, gvr), nil
}
}
auth, err := f.Client().CanI(ns, gvr, verbs) auth, err := f.Client().CanI(ns, gvr, verbs)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -3,6 +3,8 @@ package main
import ( import (
"os" "os"
_ "net/http/pprof"
"github.com/derailed/k9s/cmd" "github.com/derailed/k9s/cmd"
"github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/config"
"github.com/rs/zerolog" "github.com/rs/zerolog"