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 {
// 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)

View File

@ -5,7 +5,6 @@ package view
import (
"context"
"errors"
"fmt"
"log/slog"
"maps"
@ -106,11 +105,11 @@ 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()
// 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)
@ -121,6 +120,7 @@ func (a *App) Init(version string, _ int) error {
a.clusterModel.Refresh()
a.clusterInfo().Init()
}
}
a.command = NewCommand(a)
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) {
// 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)
}
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,8 +511,11 @@ 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)
if a.clusterModel != nil {
a.clusterModel.Reset(a.factory)
}
}
return nil
}

View File

@ -47,16 +47,21 @@ 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 {
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()
return nil
@ -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())