add pvc usage check + bugz #642 #754 #753 #743 #728 #718

mine
derailed 2020-06-04 18:20:41 -06:00
parent c0786c2016
commit e2f1626764
39 changed files with 168 additions and 62 deletions

View File

@ -215,30 +215,32 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
}
// CheckConnectivity return true if api server is cool or false otherwise.
func (a *APIClient) CheckConnectivity() (ok bool) {
func (a *APIClient) CheckConnectivity() bool {
a.mx.Lock()
defer a.mx.Unlock()
defer func() {
if err := recover(); err != nil {
ok = false
a.connOK = false
}
if !ok {
if !a.connOK {
a.clearCache()
}
a.connOK = ok
}()
// Need to reload to pickup any kubeconfig changes.
cfg, err := NewConfig(a.config.flags).RESTConfig()
if err != nil {
return false
log.Error().Err(err).Msgf("restConfig load failed")
a.connOK = false
return a.connOK
}
cfg.Timeout = checkConnTimeout
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
log.Error().Err(err).Msgf("Unable to connect to api server")
return
a.connOK = false
return a.connOK
}
// Check connection
@ -247,12 +249,13 @@ func (a *APIClient) CheckConnectivity() (ok bool) {
log.Debug().Msgf("RESETING CON!!")
a.reset()
}
ok = true
a.connOK = true
} else {
log.Error().Err(err).Msgf("K9s can't connect to cluster")
a.connOK = false
}
return
return a.connOK
}
// Config return a kubernetes configuration.
@ -385,6 +388,9 @@ func (a *APIClient) MXDial() (*versioned.Clientset, error) {
// SwitchContext handles kubeconfig context switches.
func (a *APIClient) SwitchContext(name string) error {
a.mx.Lock()
defer a.mx.Unlock()
log.Debug().Msgf("Switching context %q", name)
currentCtx, err := a.config.CurrentContextName()
if err != nil {
@ -411,9 +417,6 @@ func (a *APIClient) SwitchContext(name string) error {
}
func (a *APIClient) reset() {
a.mx.Lock()
defer a.mx.Unlock()
a.config.reset()
a.cache = cache.NewLRUExpireCache(cacheSize)
a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil

View File

@ -10,7 +10,7 @@ import (
)
// K9sAlias manages K9s aliases.
var K9sAlias = filepath.Join(K9sHome, "alias.yml")
var K9sAlias = filepath.Join(K9sHome(), "alias.yml")
// Alias tracks shortname to GVR mappings.
type Alias map[string]string

View File

@ -14,11 +14,14 @@ import (
"k8s.io/cli-runtime/pkg/genericclioptions"
)
// K9sConfig represents K9s configuration dir env var.
const K9sConfig = "K9SCONFIG"
var (
// K9sHome represent K9s home directory.
K9sHome = filepath.Join(mustK9sHome(), ".k9s")
// DefaultK9sHome represent K9s home directory.
DefaultK9sHome = filepath.Join(mustK9sHome(), ".k9s")
// K9sConfigFile represents K9s config file location.
K9sConfigFile = filepath.Join(K9sHome, "config.yml")
K9sConfigFile = filepath.Join(K9sHome(), "config.yml")
// K9sLogs represents K9s log.
K9sLogs = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser()))
// K9sDumpDir represents a directory where K9s screen dumps will be persisted.
@ -53,6 +56,15 @@ type (
}
)
// K9sHome returns k9s configs home directory.
func K9sHome() string {
if env := os.Getenv(K9sConfig); env != "" {
return env
}
return DefaultK9sHome
}
// NewConfig creates a new default config.
func NewConfig(ks KubeSettings) *Config {
return &Config{K9s: NewK9s(), settings: ks}

View File

@ -40,7 +40,7 @@ func InNSList(nn []interface{}, ns string) bool {
func mustK9sHome() string {
usr, err := user.Current()
if err != nil {
log.Fatal().Err(err).Msg("Die on retriving user home")
log.Fatal().Err(err).Msg("Die on retrieving user home")
}
return usr.HomeDir
}
@ -49,7 +49,7 @@ func mustK9sHome() string {
func MustK9sUser() string {
usr, err := user.Current()
if err != nil {
log.Fatal().Err(err).Msg("Die on retriving user info")
log.Fatal().Err(err).Msg("Die on retrieving user info")
}
return usr.Username
}

View File

@ -8,7 +8,7 @@ import (
)
// K9sHotKeys manages K9s hotKeys.
var K9sHotKeys = filepath.Join(K9sHome, "hotkey.yml")
var K9sHotKeys = filepath.Join(K9sHome(), "hotkey.yml")
// HotKeys represents a collection of plugins.
type HotKeys struct {

View File

@ -8,7 +8,7 @@ import (
)
// K9sPlugins manages K9s plugins.
var K9sPlugins = filepath.Join(K9sHome, "plugin.yml")
var K9sPlugins = filepath.Join(K9sHome(), "plugin.yml")
// Plugins represents a collection of plugins.
type Plugins struct {

View File

@ -10,7 +10,7 @@ import (
)
// K9sStylesFile represents K9s skins file location.
var K9sStylesFile = filepath.Join(K9sHome, "skin.yml")
var K9sStylesFile = filepath.Join(K9sHome(), "skin.yml")
// StyleListener represents a skin's listener.
type StyleListener interface {

View File

@ -8,7 +8,7 @@ import (
)
// K9sViewConfigFile represents the location for the views configuration.
var K9sViewConfigFile = filepath.Join(K9sHome, "views.yml")
var K9sViewConfigFile = filepath.Join(K9sHome(), "views.yml")
// ViewConfigListener represents a view config listener.
type ViewConfigListener interface {

View File

@ -65,7 +65,7 @@ func ScanForRefs(ctx context.Context, f Factory) (Refs, error) {
}
wait, ok := ctx.Value(internal.KeyWait).(bool)
if !ok {
return nil, errors.New("expecting context Wait")
log.Error().Msgf("expecting Context Wait Key")
}
ss := scanners()
@ -105,7 +105,7 @@ func ScanForRefs(ctx context.Context, f Factory) (Refs, error) {
// ScanForSARefs scans cluster resources for serviceaccount refs.
func ScanForSARefs(ctx context.Context, f Factory) (Refs, error) {
defer func(t time.Time) {
log.Debug().Msgf("Cluster Scan %v", time.Since(t))
log.Debug().Msgf("SA Cluster Scan %v", time.Since(t))
}(time.Now())
fqn, ok := ctx.Value(internal.KeyPath).(string)

View File

@ -197,12 +197,29 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs
GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name),
})
case "v1/persistentvolumeclaims":
if !hasPVC(&dp.Spec.Template.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: d.GVR(),
FQN: client.FQN(dp.Namespace, dp.Name),
})
}
}
return refs, nil
}
func hasPVC(spec *v1.PodSpec, name string) bool {
for _, v := range spec.Volumes {
if v.PersistentVolumeClaim != nil && v.PersistentVolumeClaim.ClaimName == name {
return true
}
}
return false
}
func hasConfigMap(spec *v1.PodSpec, name string) bool {
for _, c := range spec.InitContainers {
if containerHasConfigMap(c, name) {

View File

@ -211,6 +211,14 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs,
GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name),
})
case "v1/persistentvolumeclaims":
if !hasPVC(&ds.Spec.Template.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: d.GVR(),
FQN: client.FQN(ds.Namespace, ds.Name),
})
}
}

View File

@ -301,6 +301,14 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error
GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name),
})
case "v1/persistentvolumeclaims":
if !hasPVC(&pod.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: p.GVR(),
FQN: client.FQN(pod.Namespace, pod.Name),
})
}
}

View File

@ -55,17 +55,16 @@ func (p *Popeye) List(ctx context.Context, _ string) ([]runtime.Object, error) {
}
}(time.Now())
flags := config.NewFlags()
js := "json"
flags, js := config.NewFlags(), "json"
flags.Output = &js
if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" {
sections := []string{report}
flags.Sections = &sections
}
spinach := filepath.Join(cfg.K9sHome, "spinach.yml")
spinach := filepath.Join(cfg.K9sHome(), "spinach.yml")
if c, err := p.Factory.Client().Config().CurrentContextName(); err == nil {
spinach = filepath.Join(cfg.K9sHome, fmt.Sprintf("%s_spinach.yml", c))
spinach = filepath.Join(cfg.K9sHome(), fmt.Sprintf("%s_spinach.yml", c))
}
if _, err := os.Stat(spinach); err == nil {
flags.Spinach = &spinach

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/rs/zerolog/log"
@ -196,6 +197,23 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref
GVR: s.GVR(),
FQN: client.FQN(sts.Namespace, sts.Name),
})
case "v1/persistentvolumeclaims":
for _, v := range sts.Spec.VolumeClaimTemplates {
if !strings.HasPrefix(n, v.Name) {
continue
}
refs = append(refs, Ref{
GVR: s.GVR(),
FQN: client.FQN(sts.Namespace, sts.Name),
})
}
if !hasPVC(&sts.Spec.Template.Spec, n) {
continue
}
refs = append(refs, Ref{
GVR: s.GVR(),
FQN: client.FQN(sts.Namespace, sts.Name),
})
}
}

View File

@ -117,14 +117,14 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error
// BenchConfig location of the benchmarks configuration file.
func BenchConfig(context string) string {
return filepath.Join(config.K9sHome, config.K9sBench+"-"+context+".yml")
return filepath.Join(config.K9sHome(), config.K9sBench+"-"+context+".yml")
}
// RefreshStyles load for skin configuration changes.
func (c *Configurator) RefreshStyles(context string) {
c.BenchFile = BenchConfig(context)
clusterSkins := filepath.Join(config.K9sHome, fmt.Sprintf("%s_skin.yml", context))
clusterSkins := filepath.Join(config.K9sHome(), fmt.Sprintf("%s_skin.yml", context))
if c.Styles == nil {
c.Styles = config.NewStyles()
} else {

View File

@ -1,6 +1,7 @@
package ui_test
import (
"os"
"path/filepath"
"testing"
@ -12,7 +13,7 @@ import (
)
func TestBenchConfig(t *testing.T) {
config.K9sHome = "/tmp/blee"
os.Setenv(config.K9sConfig, "/tmp/blee")
assert.Equal(t, "/tmp/blee/bench-fred.yml", ui.BenchConfig("fred"))
}

View File

@ -23,7 +23,7 @@ func TestAliasNew(t *testing.T) {
assert.Nil(t, v.Init(makeContext()))
assert.Equal(t, "Aliases", v.Name())
assert.Equal(t, 5, len(v.Hints()))
assert.Equal(t, 6, len(v.Hints()))
}
func TestAliasSearch(t *testing.T) {

View File

@ -281,6 +281,8 @@ func (a *App) refreshCluster() {
if c != nil {
c.Start()
}
} else {
a.ClearStatus(true)
}
} else {
atomic.AddInt32(&a.conRetry, 1)
@ -363,6 +365,11 @@ func (a *App) switchCtx(name string, loadPods bool) error {
if err := a.command.Reset(true); err != nil {
return err
}
v := a.Config.ActiveView()
if v == "" || isContextCmd(v) || loadPods {
v = "pod"
a.Config.SetActiveView(v)
}
if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")
}
@ -370,10 +377,6 @@ func (a *App) switchCtx(name string, loadPods bool) error {
a.Flash().Infof("Switching context to %s", name)
a.ReloadStyles(name)
v := a.Config.ActiveView()
if v == "" || v == "ctx" || v == "context" {
v = "pod"
}
if err := a.gotoResource(v, "", true); loadPods && err != nil {
a.Flash().Err(err)
}

View File

@ -13,5 +13,5 @@ func TestConfigMapNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "ConfigMaps", s.Name())
assert.Equal(t, 5, len(s.Hints()))
assert.Equal(t, 6, len(s.Hints()))
}

View File

@ -151,17 +151,21 @@ func (c *Command) defaultCmd() error {
tokens := strings.Split(view, " ")
cmd := view
ns, err := c.app.Conn().Config().CurrentNamespaceName()
if err == nil {
if err == nil && !isContextCmd(tokens[0]) {
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("Default run command failed")
return c.run("meow", err.Error(), true)
}
return nil
}
func isContextCmd(c string) bool {
return c == "ctx" || c == "context"
}
func (c *Command) specialCmd(cmd, path string) bool {
cmds := strings.Split(cmd, " ")
switch cmds[0] {

View File

@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) {
assert.Nil(t, c.Init(makeCtx()))
assert.Equal(t, "Containers", c.Name())
assert.Equal(t, 16, len(c.Hints()))
assert.Equal(t, 17, len(c.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestContext(t *testing.T) {
assert.Nil(t, ctx.Init(makeCtx()))
assert.Equal(t, "Contexts", ctx.Name())
assert.Equal(t, 3, len(ctx.Hints()))
assert.Equal(t, 4, len(ctx.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestDeploy(t *testing.T) {
assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "Deployments", v.Name())
assert.Equal(t, 11, len(v.Hints()))
assert.Equal(t, 12, len(v.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestDaemonSet(t *testing.T) {
assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "DaemonSets", v.Name())
assert.Equal(t, 12, len(v.Hints()))
assert.Equal(t, 13, len(v.Hints()))
}

View File

@ -58,7 +58,7 @@ func (h *Help) Init(ctx context.Context) error {
func (h *Help) bindKeys() {
h.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS)
h.Actions().Set(ui.KeyActions{
tcell.KeyEscape: ui.NewKeyAction("Back", h.app.PrevCmd, false),
tcell.KeyEscape: ui.NewKeyAction("Back", h.app.PrevCmd, true),
ui.KeyHelp: ui.NewKeyAction("Back", h.app.PrevCmd, false),
tcell.KeyEnter: ui.NewKeyAction("Back", h.app.PrevCmd, false),
})

View File

@ -21,7 +21,7 @@ func TestHelp(t *testing.T) {
v := view.NewHelp()
assert.Nil(t, v.Init(ctx))
assert.Equal(t, 22, v.GetRowCount())
assert.Equal(t, 23, v.GetRowCount())
assert.Equal(t, 8, v.GetColumnCount())
assert.Equal(t, "<a>", strings.TrimSpace(v.GetCell(1, 0).Text))
assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text))

View File

@ -20,6 +20,7 @@ func NewJob(gvr client.GVR) ResourceViewer {
j := Job{ResourceViewer: NewLogsExtender(NewBrowser(gvr), nil)}
j.GetTable().SetEnterFn(j.showPods)
j.GetTable().SetColorerFn(render.Job{}.ColorerFunc())
j.GetTable().SetSortCol("AGE", true)
return &j
}

View File

@ -13,5 +13,5 @@ func TestNSCleanser(t *testing.T) {
assert.Nil(t, ns.Init(makeCtx()))
assert.Equal(t, "Namespaces", ns.Name())
assert.Equal(t, 5, len(ns.Hints()))
assert.Equal(t, 6, len(ns.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestPortForwardNew(t *testing.T) {
assert.Nil(t, pf.Init(makeCtx()))
assert.Equal(t, "PortForwards", pf.Name())
assert.Equal(t, 9, len(pf.Hints()))
assert.Equal(t, 10, len(pf.Hints()))
}

View File

@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) {
assert.Nil(t, po.Init(makeCtx()))
assert.Equal(t, "Pods", po.Name())
assert.Equal(t, 21, len(po.Hints()))
assert.Equal(t, 22, len(po.Hints()))
}
// Helpers...

View File

@ -4,6 +4,7 @@ import (
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
)
// PersistentVolumeClaim represents a PVC custom viewer.
@ -13,20 +14,25 @@ type PersistentVolumeClaim struct {
// NewPersistentVolumeClaim returns a new viewer.
func NewPersistentVolumeClaim(gvr client.GVR) ResourceViewer {
d := PersistentVolumeClaim{
v := PersistentVolumeClaim{
ResourceViewer: NewBrowser(gvr),
}
d.SetBindKeysFn(d.bindKeys)
d.GetTable().SetColorerFn(render.PersistentVolumeClaim{}.ColorerFunc())
v.SetBindKeysFn(v.bindKeys)
v.GetTable().SetColorerFn(render.PersistentVolumeClaim{}.ColorerFunc())
return &d
return &v
}
func (d *PersistentVolumeClaim) bindKeys(aa ui.KeyActions) {
func (p *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),
ui.KeyU: ui.NewKeyAction("UsedBy", p.refCmd, true),
ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd("STATUS", true), false),
ui.KeyShiftV: ui.NewKeyAction("Sort Volume", p.GetTable().SortColCmd("VOLUME", true), false),
ui.KeyShiftO: ui.NewKeyAction("Sort StorageClass", p.GetTable().SortColCmd("STORAGECLASS", true), false),
ui.KeyShiftC: ui.NewKeyAction("Sort Capacity", p.GetTable().SortColCmd("CAPACITY", true), false),
})
}
func (p *PersistentVolumeClaim) refCmd(evt *tcell.EventKey) *tcell.EventKey {
return scanRefs(evt, p.App(), p.GetTable(), "v1/persistentvolumeclaims")
}

17
internal/view/pvc_test.go Normal file
View File

@ -0,0 +1,17 @@
package view_test
import (
"testing"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/view"
"github.com/stretchr/testify/assert"
)
func TestPVCNew(t *testing.T) {
v := view.NewPersistentVolumeClaim(client.NewGVR("v1/persistentvolumeclaims"))
assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "PersistentVolumeClaims", v.Name())
assert.Equal(t, 10, len(v.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestRbacNew(t *testing.T) {
assert.Nil(t, v.Init(makeCtx()))
assert.Equal(t, "Rbac", v.Name())
assert.Equal(t, 4, len(v.Hints()))
assert.Equal(t, 5, len(v.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestReferenceNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "References", s.Name())
assert.Equal(t, 3, len(s.Hints()))
assert.Equal(t, 4, len(s.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestScreenDumpNew(t *testing.T) {
assert.Nil(t, po.Init(makeCtx()))
assert.Equal(t, "ScreenDumps", po.Name())
assert.Equal(t, 4, len(po.Hints()))
assert.Equal(t, 5, len(po.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestSecretNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "Secrets", s.Name())
assert.Equal(t, 6, len(s.Hints()))
assert.Equal(t, 7, len(s.Hints()))
}

View File

@ -13,5 +13,5 @@ func TestStatefulSetNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "StatefulSets", s.Name())
assert.Equal(t, 10, len(s.Hints()))
assert.Equal(t, 11, len(s.Hints()))
}

View File

@ -141,6 +141,14 @@ func init() {
Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"},
})
dao.MetaAccess.RegisterMeta("v1/persistentvolumeclaims", metav1.APIResource{
Name: "persistentvolumeclaims",
SingularName: "persistentvolumeclaim",
Namespaced: true,
Kind: "PersistentVolumeClaims",
Verbs: []string{"get", "list", "watch", "delete"},
Categories: []string{"k9s"},
})
}
func TestServiceNew(t *testing.T) {
@ -148,5 +156,5 @@ func TestServiceNew(t *testing.T) {
assert.Nil(t, s.Init(makeCtx()))
assert.Equal(t, "Services", s.Name())
assert.Equal(t, 9, len(s.Hints()))
assert.Equal(t, 10, len(s.Hints()))
}

View File

@ -150,6 +150,7 @@ func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey {
func (t *Table) bindKeys() {
t.Actions().Add(ui.KeyActions{
ui.KeyHelp: ui.NewKeyAction("Help", t.App().helpCmd, true),
ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false),
tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Mark Range", t.markSpanCmd, false),
tcell.KeyCtrlBackslash: ui.NewSharedKeyAction("Marks Clear", t.clearMarksCmd, false),