From 381ca5ee9ed6e42da501612659e0500e92aab02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cm=C3=BCt=20=C3=96zalp?= <54961032+uozalp@users.noreply.github.com> Date: Sat, 13 Dec 2025 19:48:57 +0100 Subject: [PATCH] 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 --- internal/view/actions.go | 5 ++++ internal/view/app.go | 59 +++++++++++++++++++++++++++++----------- internal/view/command.go | 23 +++++++++++++--- 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/internal/view/actions.go b/internal/view/actions.go index 0c58ee45..33f39305 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -113,6 +113,11 @@ func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler { } 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) { if a.Opts.Plugin { aa.Delete(k) diff --git a/internal/view/app.go b/internal/view/app.go index c02ecb1c..79cc6c2e 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -5,7 +5,6 @@ package view import ( "context" - "errors" "fmt" "log/slog" "maps" @@ -106,20 +105,21 @@ func (a *App) Init(version string, _ int) error { a.App.Init() a.SetInputCapture(a.keyboard) a.bindKeys() - if a.Conn() == nil { - return errors.New("no client connection detected") - } - ns := a.Config.ActiveNamespace() - a.factory = watch.NewFactory(a.Conn()) - a.initFactory(ns) + // 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.initFactory(ns) - a.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s) - a.clusterModel.AddListener(a.clusterInfo()) - a.clusterModel.AddListener(a.statusIndicator()) - if a.Conn().ConnectionOK() { - a.clusterModel.Refresh() - a.clusterInfo().Init() + a.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s) + a.clusterModel.AddListener(a.clusterInfo()) + a.clusterModel.AddListener(a.statusIndicator()) + if a.Conn().ConnectionOK() { + a.clusterModel.Refresh() + a.clusterInfo().Init() + } } a.command = NewCommand(a) @@ -223,6 +223,10 @@ func (a *App) suggestCommand() model.SuggestionFunc { } 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() if err != nil { return nil, err @@ -303,7 +307,7 @@ func (a *App) buildHeader() tview.Primitive { } clWidth := clusterInfoWidth - if a.Conn().ConnectionOK() { + if a.Conn() != nil && a.Conn().ConnectionOK() { n, err := a.Conn().Config().CurrentClusterName() if err == nil { size := len(n) + clusterInfoPad @@ -351,6 +355,11 @@ func (a *App) Resume() { } 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 { slog.Error("Cluster updater failed!", slogs.Error, err) return @@ -379,6 +388,10 @@ func (a *App) clusterUpdater(ctx context.Context) { } func (a *App) refreshCluster(context.Context) error { + if a.Conn() == nil || a.factory == nil || a.clusterModel == nil { + return nil + } + c := a.Content.Top() if ok := a.Conn().CheckConnectivity(); ok { 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 { slog.Error("Fail to save config to disk", slogs.Subsys, "config", slogs.Error, err) } - a.initFactory(ns) + + 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) + } + if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil { return err } @@ -487,7 +511,10 @@ func (a *App) switchContext(ci *cmd.Interpreter, force bool) error { a.Flash().Infof("Switching context to %q::%q", contextName, ns) a.ReloadStyles() a.gotoResource(a.Config.ActiveView(), "", true, true) - a.clusterModel.Reset(a.factory) + + if a.clusterModel != nil { + a.clusterModel.Reset(a.factory) + } } return nil diff --git a/internal/view/command.go b/internal/view/command.go index f315ee58..b6ec6851 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -47,15 +47,20 @@ func NewCommand(app *App) *Command { // AliasesFor gather all known aliases for a given resource. func (c *Command) AliasesFor(gvr *client.GVR) sets.Set[string] { + if c.alias == nil { + return sets.New[string]() + } return c.alias.AliasesFor(gvr) } // Init initializes the command. func (c *Command) Init(path string) error { - c.alias = dao.NewAlias(c.app.factory) - if _, err := c.alias.Ensure(path); err != nil { - slog.Error("Ensure aliases failed", slogs.Error, err) - return err + if c.app.factory != nil { + c.alias = dao.NewAlias(c.app.factory) + if _, err := c.alias.Ensure(path); err != nil { + slog.Error("Ensure aliases failed", slogs.Error, err) + return err + } } customViewers = loadCustomViewers() @@ -67,6 +72,10 @@ func (c *Command) Reset(path string, nuke bool) error { c.mx.Lock() defer c.mx.Unlock() + if c.alias == nil { + return nil + } + if nuke { c.alias.Clear() } @@ -139,6 +148,9 @@ func (c *Command) xrayCmd(p *cmd.Interpreter, pushCmd bool) error { if !ok { 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)) if !ok { 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) { + if c.alias == nil { + return client.NoGVR, nil, nil, fmt.Errorf("no connection available") + } gvr, ok := c.alias.Resolve(p) if !ok { return client.NoGVR, nil, nil, fmt.Errorf("`%s` command not found", p.Cmd())