Allow k9s to start without a valid Kubernetes context (#3704)

* allow initialization without a valid connection

* fix: handle nil factory and alias in command and context methods
mine
Ümüt Özalp 2025-12-13 19:48:57 +01:00 committed by GitHub
parent f27846ce23
commit 381ca5ee9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 20 deletions

View File

@ -113,6 +113,11 @@ func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler {
} }
func pluginActions(r Runner, aa *ui.KeyActions) error { func pluginActions(r Runner, aa *ui.KeyActions) error {
// Skip plugin loading if no valid connection
if r.App().Conn() == nil || !r.App().Conn().ConnectionOK() {
return nil
}
aa.Range(func(k tcell.Key, a ui.KeyAction) { aa.Range(func(k tcell.Key, a ui.KeyAction) {
if a.Opts.Plugin { if a.Opts.Plugin {
aa.Delete(k) aa.Delete(k)

View File

@ -5,7 +5,6 @@ package view
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"maps" "maps"
@ -106,11 +105,11 @@ func (a *App) Init(version string, _ int) error {
a.App.Init() a.App.Init()
a.SetInputCapture(a.keyboard) a.SetInputCapture(a.keyboard)
a.bindKeys() a.bindKeys()
if a.Conn() == nil {
return errors.New("no client connection detected")
}
ns := a.Config.ActiveNamespace()
// Allow initialization even without a valid connection
// We'll fall back to context view in defaultCmd
if a.Conn() != nil {
ns := a.Config.ActiveNamespace()
a.factory = watch.NewFactory(a.Conn()) a.factory = watch.NewFactory(a.Conn())
a.initFactory(ns) a.initFactory(ns)
@ -121,6 +120,7 @@ func (a *App) Init(version string, _ int) error {
a.clusterModel.Refresh() a.clusterModel.Refresh()
a.clusterInfo().Init() a.clusterInfo().Init()
} }
}
a.command = NewCommand(a) a.command = NewCommand(a)
if err := a.command.Init(a.Config.ContextAliasesPath()); err != nil { if err := a.command.Init(a.Config.ContextAliasesPath()); err != nil {
@ -223,6 +223,10 @@ func (a *App) suggestCommand() model.SuggestionFunc {
} }
func (a *App) contextNames() ([]string, error) { func (a *App) contextNames() ([]string, error) {
// Return empty list if no factory
if a.factory == nil {
return []string{}, nil
}
contexts, err := a.factory.Client().Config().Contexts() contexts, err := a.factory.Client().Config().Contexts()
if err != nil { if err != nil {
return nil, err return nil, err
@ -303,7 +307,7 @@ func (a *App) buildHeader() tview.Primitive {
} }
clWidth := clusterInfoWidth clWidth := clusterInfoWidth
if a.Conn().ConnectionOK() { if a.Conn() != nil && a.Conn().ConnectionOK() {
n, err := a.Conn().Config().CurrentClusterName() n, err := a.Conn().Config().CurrentClusterName()
if err == nil { if err == nil {
size := len(n) + clusterInfoPad size := len(n) + clusterInfoPad
@ -351,6 +355,11 @@ func (a *App) Resume() {
} }
func (a *App) clusterUpdater(ctx context.Context) { func (a *App) clusterUpdater(ctx context.Context) {
if a.Conn() == nil || !a.Conn().ConnectionOK() || a.factory == nil || a.clusterModel == nil {
slog.Debug("Skipping cluster updater - no valid connection")
return
}
if err := a.refreshCluster(ctx); err != nil { if err := a.refreshCluster(ctx); err != nil {
slog.Error("Cluster updater failed!", slogs.Error, err) slog.Error("Cluster updater failed!", slogs.Error, err)
return return
@ -379,6 +388,10 @@ func (a *App) clusterUpdater(ctx context.Context) {
} }
func (a *App) refreshCluster(context.Context) error { func (a *App) refreshCluster(context.Context) error {
if a.Conn() == nil || a.factory == nil || a.clusterModel == nil {
return nil
}
c := a.Content.Top() c := a.Content.Top()
if ok := a.Conn().CheckConnectivity(); ok { if ok := a.Conn().CheckConnectivity(); ok {
if atomic.LoadInt32(&a.conRetry) > 0 { if atomic.LoadInt32(&a.conRetry) > 0 {
@ -474,7 +487,18 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
if err := a.Config.Save(true); err != nil { if err := a.Config.Save(true); err != nil {
slog.Error("Fail to save config to disk", slogs.Subsys, "config", slogs.Error, err) slog.Error("Fail to save config to disk", slogs.Subsys, "config", slogs.Error, err)
} }
if a.factory == nil && a.Conn() != nil {
a.factory = watch.NewFactory(a.Conn())
a.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s)
a.clusterModel.AddListener(a.clusterInfo())
a.clusterModel.AddListener(a.statusIndicator())
}
if a.factory != nil {
a.initFactory(ns) a.initFactory(ns)
}
if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil { if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil {
return err return err
} }
@ -487,8 +511,11 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error {
a.Flash().Infof("Switching context to %q::%q", contextName, ns) a.Flash().Infof("Switching context to %q::%q", contextName, ns)
a.ReloadStyles() a.ReloadStyles()
a.gotoResource(a.Config.ActiveView(), "", true, true) a.gotoResource(a.Config.ActiveView(), "", true, true)
if a.clusterModel != nil {
a.clusterModel.Reset(a.factory) a.clusterModel.Reset(a.factory)
} }
}
return nil return nil
} }

View File

@ -47,16 +47,21 @@ func NewCommand(app *App) *Command {
// AliasesFor gather all known aliases for a given resource. // AliasesFor gather all known aliases for a given resource.
func (c *Command) AliasesFor(gvr *client.GVR) sets.Set[string] { func (c *Command) AliasesFor(gvr *client.GVR) sets.Set[string] {
if c.alias == nil {
return sets.New[string]()
}
return c.alias.AliasesFor(gvr) return c.alias.AliasesFor(gvr)
} }
// Init initializes the command. // Init initializes the command.
func (c *Command) Init(path string) error { func (c *Command) Init(path string) error {
if c.app.factory != nil {
c.alias = dao.NewAlias(c.app.factory) c.alias = dao.NewAlias(c.app.factory)
if _, err := c.alias.Ensure(path); err != nil { if _, err := c.alias.Ensure(path); err != nil {
slog.Error("Ensure aliases failed", slogs.Error, err) slog.Error("Ensure aliases failed", slogs.Error, err)
return err return err
} }
}
customViewers = loadCustomViewers() customViewers = loadCustomViewers()
return nil return nil
@ -67,6 +72,10 @@ func (c *Command) Reset(path string, nuke bool) error {
c.mx.Lock() c.mx.Lock()
defer c.mx.Unlock() defer c.mx.Unlock()
if c.alias == nil {
return nil
}
if nuke { if nuke {
c.alias.Clear() c.alias.Clear()
} }
@ -139,6 +148,9 @@ func (c *Command) xrayCmd(p *cmd.Interpreter, pushCmd bool) error {
if !ok { if !ok {
return errors.New("invalid command. use `xray xxx`") return errors.New("invalid command. use `xray xxx`")
} }
if c.alias == nil {
return fmt.Errorf("no connection available")
}
gvr, ok := c.alias.Resolve(cmd.NewInterpreter(arg)) gvr, ok := c.alias.Resolve(cmd.NewInterpreter(arg))
if !ok { if !ok {
return fmt.Errorf("invalid resource name: %q", arg) return fmt.Errorf("invalid resource name: %q", arg)
@ -301,6 +313,9 @@ func (c *Command) specialCmd(p *cmd.Interpreter, pushCmd bool) bool {
} }
func (c *Command) viewMetaFor(p *cmd.Interpreter) (*client.GVR, *MetaViewer, *cmd.Interpreter, error) { func (c *Command) viewMetaFor(p *cmd.Interpreter) (*client.GVR, *MetaViewer, *cmd.Interpreter, error) {
if c.alias == nil {
return client.NoGVR, nil, nil, fmt.Errorf("no connection available")
}
gvr, ok := c.alias.Resolve(p) gvr, ok := c.alias.Resolve(p)
if !ok { if !ok {
return client.NoGVR, nil, nil, fmt.Errorf("`%s` command not found", p.Cmd()) return client.NoGVR, nil, nil, fmt.Errorf("`%s` command not found", p.Cmd())