derailed 2020-05-01 12:01:21 -06:00
parent f0b56964ea
commit 56a520c2d1
18 changed files with 154 additions and 50 deletions

View File

@ -45,7 +45,7 @@ type APIClient struct {
config *Config
mx sync.Mutex
cache *cache.LRUExpireCache
metricsAPI bool
connOK bool
}
// NewTestClient for testing ONLY!!
@ -63,7 +63,7 @@ func InitConnectionOrDie(config *Config) *APIClient {
config: config,
cache: cache.NewLRUExpireCache(cacheSize),
}
a.metricsAPI = a.supportsMetricsResources()
_ = a.supportsMetricsResources()
return &a
}
@ -168,12 +168,21 @@ func (a *APIClient) ServerVersion() (*version.Info, error) {
// ValidNamespaces returns all available namespaces.
func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) {
if nn, ok := a.cache.Get("validNamespaces"); ok {
if nss, ok := nn.([]v1.Namespace); ok {
return nss, nil
}
}
log.Debug().Msgf(">>>>> Loading all namespaces")
ctx, cancel := context.WithTimeout(context.Background(), CallTimeout)
defer cancel()
nn, err := a.DialOrDie().CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
a.cache.Add("validNamespaces", nn.Items, cacheExpiry)
return nn.Items, nil
}
@ -186,6 +195,7 @@ func (a *APIClient) CheckConnectivity() (status bool) {
if !status {
a.clearCache()
}
a.connOK = status
}()
cfg, err := a.config.flags.ToRESTConfig()
@ -201,7 +211,10 @@ func (a *APIClient) CheckConnectivity() (status bool) {
}
if _, err := client.ServerVersion(); err == nil {
a.reset()
if !a.connOK {
log.Debug().Msgf("RESETING CON!!")
a.reset()
}
status = true
} else {
log.Error().Err(err).Msgf("K9s can't connect to cluster")
@ -332,7 +345,7 @@ func (a *APIClient) SwitchContext(ctx string) error {
}
a.clearCache()
a.reset()
a.metricsAPI = a.supportsMetricsResources()
_ = a.supportsMetricsResources()
ResetMetrics()
return nil

View File

@ -1,6 +1,8 @@
package config
import "github.com/derailed/k9s/internal/client"
import (
"github.com/derailed/k9s/internal/client"
)
// Cluster tracks K9s cluster configuration.
type Cluster struct {
@ -23,7 +25,6 @@ func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) {
if c.Namespace == nil {
c.Namespace = NewNamespace()
}
c.Namespace.Validate(conn, ks)
if c.FeatureGates == nil {
c.FeatureGates = NewFeatureGates()

View File

@ -132,13 +132,22 @@ func (c *Config) ActiveNamespace() string {
return "default"
}
func (c *Config) ValidateFavorites() {
cl := c.K9s.ActiveCluster()
if cl == nil {
cl = NewCluster()
}
cl.Validate(c.client, c.settings)
cl.Namespace.Validate(c.client, c.settings)
}
// FavNamespaces returns fav namespaces in the current cluster.
func (c *Config) FavNamespaces() []string {
cl := c.K9s.ActiveCluster()
if cl != nil {
return c.K9s.ActiveCluster().Namespace.Favorites
if cl == nil {
return nil
}
return []string{}
return c.K9s.ActiveCluster().Namespace.Favorites
}
// SetActiveNamespace set the active namespace in the current cluster.

View File

@ -288,8 +288,10 @@ var expectedConfig = `k9s:
active: default
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: po
featureGates:
@ -299,8 +301,10 @@ var expectedConfig = `k9s:
active: kube-system
favorites:
- default
- kube-public
- istio-system
- all
- kube-system
view:
active: ctx
featureGates:

View File

@ -100,7 +100,7 @@ func (k *K9s) validateDefaults() {
}
}
func (k *K9s) checkClusters(c client.Connection, ks KubeSettings) {
func (k *K9s) validateClusters(c client.Connection, ks KubeSettings) {
cc, err := ks.ClusterNames()
if err != nil {
return
@ -123,7 +123,7 @@ func (k *K9s) Validate(c client.Connection, ks KubeSettings) {
if k.Clusters == nil {
k.Clusters = map[string]*Cluster{}
}
k.checkClusters(c, ks)
k.validateClusters(c, ks)
if k.Logger == nil {
k.Logger = NewLogger()

View File

@ -59,7 +59,6 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error
// TailLogs tails a given container logs
func (c *Container) TailLogs(ctx context.Context, logChan LogChan, opts LogOptions) error {
log.Debug().Msgf("CONTAINER-LOGS")
po := Pod{}
po.Init(c.Factory, client.NewGVR("v1/pods"))

View File

@ -19,6 +19,7 @@ type LogChan chan *LogItem
// LogItem represents a container log line.
type LogItem struct {
Pod, Container, Timestamp string
SingleContainer bool
Bytes []byte
}
@ -78,7 +79,7 @@ func (l *LogItem) Render(c int, showTime bool) []byte {
bb = append(bb, []byte(colorize(l.Pod, c))...)
bb = append(bb, ':')
}
if l.Container != "" {
if !l.SingleContainer && l.Container != "" {
bb = append(bb, []byte(colorize(l.Container, c))...)
bb = append(bb, ' ')
}

View File

@ -82,7 +82,10 @@ func (o LogOptions) DecorateLog(bytes []byte) *LogItem {
if len(bytes) == 0 {
return item
}
item.SingleContainer = o.SingleContainer
if item.SingleContainer {
item.Container = o.Container
}
if o.MultiPods {
_, pod := client.Namespaced(o.Path)
item.Pod, item.Container = pod, o.Container

View File

@ -187,10 +187,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error {
if opts.HasContainer() {
opts.SingleContainer = true
if err := tailLogs(ctx, p, c, opts); err != nil {
return err
}
return nil
return tailLogs(ctx, p, c, opts)
}
if len(po.Spec.InitContainers)+len(po.Spec.Containers) == 1 {
opts.SingleContainer = true

View File

@ -74,10 +74,14 @@ func (l *Log) Configure(opts *config.Logger) {
}
// GetPath returns resource path.
func (l *Log) GetPath() string { return l.logOptions.Path }
func (l *Log) GetPath() string {
return l.logOptions.Path
}
// GetContainer returns the resource container if any or "" otherwise.
func (l *Log) GetContainer() string { return l.logOptions.Container }
func (l *Log) GetContainer() string {
return l.logOptions.Container
}
// Init initializes the model.
func (l *Log) Init(f dao.Factory) {

View File

@ -132,7 +132,7 @@ func (a *App) initSignals() {
go func(sig chan os.Signal) {
<-sig
a.BailOut()
nukeK9sShell(a.Conn())
}(sig)
}
@ -296,17 +296,35 @@ func (a *App) refreshCluster() {
a.clusterModel.Refresh()
}
func (a *App) switchNS(ns string) bool {
func (a *App) switchNS(ns string) error {
if ns == client.ClusterScope {
ns = client.AllNamespaces
}
if !a.isValidNS(ns) {
return fmt.Errorf("Invalid namespace %q", ns)
}
if err := a.Config.SetActiveNamespace(ns); err != nil {
log.Error().Err(err).Msg("Config Set NS failed!")
return false
return fmt.Errorf("Unable to save active namespace in config")
}
a.factory.SetActiveNS(ns)
return true
return nil
}
func (a *App) isValidNS(ns string) bool {
if ns == client.AllNamespaces || ns == client.NamespaceAll {
return true
}
nn, err := a.Conn().ValidNamespaces()
if err != nil {
return false
}
for _, n := range nn {
if n.Name == ns {
return true
}
}
return false
}
func (a *App) switchCtx(name string, loadPods bool) error {
@ -323,10 +341,11 @@ func (a *App) switchCtx(name string, loadPods bool) error {
if err := a.command.Reset(true); err != nil {
return err
}
a.Config.Reset()
if err := a.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")
}
a.Config.Reset()
a.Flash().Infof("Switching context to %s", name)
a.ReloadStyles(name)
v := a.Config.ActiveView()

View File

@ -120,6 +120,11 @@ func (b *Browser) SetInstance(path string) {
// Start initializes browser updates.
func (b *Browser) Start() {
b.app.Config.ValidateFavorites()
if err := b.app.Config.Save(); err != nil {
log.Error().Err(err).Msgf("Config Save")
}
b.Stop()
b.Table.Start()
b.CmdBuff().AddListener(b)
@ -359,7 +364,10 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
b.app.switchNS(ns)
if err := b.app.switchNS(ns); err != nil {
b.App().Flash().Err(err)
return nil
}
b.setNamespace(ns)
b.app.Flash().Infof("Viewing namespace `%s`...", ns)
b.refresh()

View File

@ -147,8 +147,8 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
if len(cmds) == 2 {
ns = cmds[1]
}
if !c.app.switchNS(ns) {
return fmt.Errorf("namespace switch failed for ns %q", ns)
if err := c.app.switchNS(ns); err != nil {
return err
}
if !c.alias.Check(cmds[0]) {
return fmt.Errorf("Huh? `%s` Command not found", cmd)
@ -159,7 +159,7 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
func (c *Command) defaultCmd() error {
if err := c.run(c.app.Config.ActiveView(), "", true); err != nil {
log.Error().Err(err).Msgf("Saved command failed. Loading default view")
log.Error().Err(err).Msgf("Saved command load failed. Loading default view")
return c.run("pod", "", true)
}
return nil
@ -228,7 +228,22 @@ func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer {
return view
}
func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) error {
func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) (err error) {
defer func() {
if e := recover(); e != nil {
c.app.Content.Dump()
log.Debug().Msgf("History %v", c.app.cmdHistory.List())
hh := c.app.cmdHistory.List()
if len(hh) == 0 {
_ = c.run("pod", "", true)
} else {
_ = c.run(hh[0], "", true)
}
err = fmt.Errorf("Invalid command %q", cmd)
}
}()
if comp == nil {
return fmt.Errorf("No component found for %s", gvr)
}
@ -246,5 +261,5 @@ func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) e
}
c.app.cmdHistory.Push(cmd)
return nil
return
}

View File

@ -84,7 +84,10 @@ func showPodsWithLabels(app *App, path string, sel map[string]string) {
}
func showPods(app *App, path, labelSel, fieldSel string) {
app.switchNS(client.AllNamespaces)
if err := app.switchNS(client.AllNamespaces); err != nil {
app.Flash().Err(err)
return
}
v := NewPod(client.NewGVR("v1/pods"))
v.SetContextFn(podCtx(app, path, labelSel, fieldSel))

View File

@ -59,13 +59,16 @@ func (n *Namespace) useNsCmd(evt *tcell.EventKey) *tcell.EventKey {
func (n *Namespace) useNamespace(fqn string) {
_, ns := client.Namespaced(fqn)
log.Debug().Msgf("SWITCHING NS %q", ns)
n.App().switchNS(ns)
if err := n.App().switchNS(ns); err != nil {
n.App().Flash().Err(err)
return
}
if err := n.App().Config.SetActiveNamespace(ns); err != nil {
n.App().Flash().Err(err)
} else {
n.App().Flash().Infof("Namespace %s is now active!", ns)
return
}
n.App().Flash().Infof("Namespace %s is now active!", ns)
if err := n.App().Config.Save(); err != nil {
log.Error().Err(err).Msg("Config file save failed!")
}

View File

@ -2,6 +2,7 @@ package view
import (
"fmt"
"regexp"
"strings"
"github.com/derailed/k9s/internal/client"
@ -72,15 +73,17 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) {
// Helpers...
func extractPort(p string) string {
tokens := strings.Split(p, ":")
switch {
case len(tokens) < 2:
return tokens[0]
case len(tokens) == 2:
return strings.Replace(tokens[1], "UDP", "", 1)
default:
return tokens[1]
rx := regexp.MustCompile(`\A(\w+)/?(\w+)?:?(\d+)?(UDP)?\z`)
mm := rx.FindStringSubmatch(p)
if len(mm) != 5 {
return p
}
for i := 3; i > 0; i-- {
if mm[i] != "" {
return mm[i]
}
}
return p
}
func extractContainer(p string) string {

View File

@ -10,6 +10,9 @@ func TestExtractPort(t *testing.T) {
uu := map[string]struct {
port, e string
}{
"empty": {
"", "",
},
"full": {
"co/fred:8000", "8000",
},
@ -22,6 +25,9 @@ func TestExtractPort(t *testing.T) {
"protocol": {
"dns:53UDP", "53",
},
"unamed": {
"dns/53", "53",
},
}
for k := range uu {

View File

@ -27,11 +27,10 @@ type Pod struct {
// NewPod returns a new viewer.
func NewPod(gvr client.GVR) ResourceViewer {
p := Pod{
ResourceViewer: NewPortForwardExtender(
NewLogsExtender(NewBrowser(gvr), nil),
),
}
p := Pod{}
p.ResourceViewer = NewPortForwardExtender(
NewLogsExtender(NewBrowser(gvr), p.selectedContainer),
)
p.SetBindKeysFn(p.bindKeys)
p.GetTable().SetEnterFn(p.showContainers)
p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc())
@ -67,6 +66,23 @@ func (p *Pod) bindKeys(aa ui.KeyActions) {
})
}
func (p *Pod) selectedContainer() string {
path := p.GetTable().GetSelectedItem()
if path == "" {
return ""
}
cc, err := fetchContainers(p.App().factory, path, true)
if err != nil {
log.Error().Err(err).Msgf("Fetch containers")
return ""
}
if len(cc) == 1 {
return cc[0]
}
return ""
}
func (p *Pod) showContainers(app *App, model ui.Tabular, gvr, path string) {
co := NewContainer(client.NewGVR("containers"))
co.SetContextFn(p.coContext)