parent
f0b56964ea
commit
56a520c2d1
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ' ')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:53╱UDP", "53",
|
||||
},
|
||||
"unamed": {
|
||||
"dns/53", "53",
|
||||
},
|
||||
}
|
||||
|
||||
for k := range uu {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue