k9s/internal/view/command.go

362 lines
7.9 KiB
Go

// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of K9s
package view
import (
"errors"
"fmt"
"log/slog"
"regexp"
"runtime/debug"
"sync"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/slogs"
"github.com/derailed/k9s/internal/view/cmd"
"k8s.io/apimachinery/pkg/util/sets"
)
const (
podCmd = "v1/pods"
ctxCmd = "ctx"
)
var (
customViewers MetaViewers
contextRX = regexp.MustCompile(`\s+@([\w-]+)`)
)
// Command represents a user command.
type Command struct {
app *App
alias *dao.Alias
mx sync.Mutex
}
// NewCommand returns a new command.
func NewCommand(app *App) *Command {
return &Command{
app: app,
}
}
// AliasesFor gather all known aliases for a given resource.
func (c *Command) AliasesFor(gvr *client.GVR) sets.Set[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
}
customViewers = loadCustomViewers()
return nil
}
// Reset resets Command and reload aliases.
func (c *Command) Reset(path string, nuke bool) error {
c.mx.Lock()
defer c.mx.Unlock()
if nuke {
c.alias.Clear()
}
if _, err := c.alias.Ensure(path); err != nil {
return err
}
return nil
}
var allowedCmds = sets.New[*client.GVR](
client.PodGVR,
client.SvcGVR,
client.DpGVR,
client.DsGVR,
client.StsGVR,
client.RsGVR,
)
func allowedXRay(gvr *client.GVR) bool {
return allowedCmds.Has(gvr)
}
func (c *Command) contextCmd(p *cmd.Interpreter, pushCmd bool) error {
ct, ok := p.ContextArg()
if !ok {
return fmt.Errorf("invalid command use `context xxx`")
}
if ct != "" {
return useContext(c.app, ct)
}
gvr, v, err := c.viewMetaFor(p)
if err != nil {
return err
}
return c.exec(p, gvr, c.componentFor(gvr, ct, v), true, pushCmd)
}
func (*Command) namespaceCmd(p *cmd.Interpreter) bool {
ns, ok := p.NSArg()
if !ok {
return false
}
if ns != "" {
_ = p.Reset("pod " + ns)
}
return false
}
func (c *Command) aliasCmd(p *cmd.Interpreter, pushCmd bool) error {
filter, _ := p.FilterArg()
v := NewAlias(client.AliGVR)
v.SetFilter(filter)
return c.exec(p, client.AliGVR, v, false, pushCmd)
}
func (c *Command) xrayCmd(p *cmd.Interpreter, pushCmd bool) error {
arg, cns, ok := p.XrayArgs()
if !ok {
return errors.New("invalid command. use `xray xxx`")
}
gvr, _, ok := c.alias.AsGVR(arg)
if !ok {
return fmt.Errorf("invalid resource name: %q", arg)
}
if !allowedXRay(gvr) {
return fmt.Errorf("unsupported resource %q", arg)
}
ns := c.app.Config.ActiveNamespace()
if cns != "" {
ns = cns
}
if err := c.app.Config.SetActiveNamespace(client.CleanseNamespace(ns)); err != nil {
return err
}
if err := c.app.switchNS(ns); err != nil {
return err
}
return c.exec(p, client.NewGVR("xrays"), NewXray(gvr), true, pushCmd)
}
// Run execs the command by showing associated display.
func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack, pushCmd bool) error {
if c.specialCmd(p, pushCmd) {
return nil
}
gvr, v, err := c.viewMetaFor(p)
if err != nil {
return err
}
if context, ok := p.HasContext(); ok {
if context != c.app.Config.ActiveContextName() {
if err := c.app.Config.Save(true); err != nil {
slog.Error("Config save failed during command exec", slogs.Error, err)
} else {
slog.Debug("Successfully saved config", slogs.Context, context)
}
}
res, err := dao.AccessorFor(c.app.factory, client.CtGVR)
if err != nil {
return err
}
switcher, ok := res.(dao.Switchable)
if !ok {
return errors.New("expecting a switchable resource")
}
if err := switcher.Switch(context); err != nil {
slog.Error("Unable to switch context", slogs.Error, err)
return err
}
if err := c.app.switchContext(p, false); err != nil {
return err
}
}
ns := c.app.Config.ActiveNamespace()
if cns, ok := p.NSArg(); ok {
ns = cns
}
if err := c.app.switchNS(ns); err != nil {
return err
}
co := c.componentFor(gvr, fqn, v)
co.SetFilter("")
co.SetLabelFilter(nil)
if f, ok := p.FilterArg(); ok {
co.SetFilter(f)
}
if f, ok := p.FuzzyArg(); ok {
co.SetFilter("-f " + f)
}
if ll, ok := p.LabelsArg(); ok {
co.SetLabelFilter(ll)
}
return c.exec(p, gvr, co, clearStack, pushCmd)
}
func (c *Command) defaultCmd(isRoot bool) error {
if c.app.Conn() == nil || !c.app.Conn().ConnectionOK() {
return c.run(cmd.NewInterpreter("context"), "", true, true)
}
defCmd := podCmd
if isRoot {
defCmd = ctxCmd
}
p := cmd.NewInterpreter(c.app.Config.ActiveView())
if p.IsBlank() {
return c.run(p.Reset(defCmd), "", true, true)
}
if err := c.run(p, "", true, true); err != nil {
p = p.Reset(defCmd)
slog.Error("Command exec failed. Using default command",
slogs.Command, p.GetLine(),
slogs.Error, err,
)
return c.run(p, "", true, true)
}
return nil
}
func (c *Command) specialCmd(p *cmd.Interpreter, pushCmd bool) bool {
switch {
case p.IsCowCmd():
if msg, ok := p.CowArg(); !ok {
c.app.Flash().Errf("Invalid command. Use `cow xxx`")
} else {
c.app.cowCmd(msg)
}
case p.IsBailCmd():
c.app.BailOut(0)
case p.IsHelpCmd():
_ = c.app.helpCmd(nil)
case p.IsAliasCmd():
if err := c.aliasCmd(p, pushCmd); err != nil {
c.app.Flash().Err(err)
}
case p.IsXrayCmd():
if err := c.xrayCmd(p, pushCmd); err != nil {
c.app.Flash().Err(err)
}
case p.IsRBACCmd():
if cat, sub, ok := p.RBACArgs(); !ok {
c.app.Flash().Errf("Invalid command. Use `can [u|g|s]:xxx`")
} else if err := c.app.inject(NewPolicy(c.app, cat, sub), true); err != nil {
c.app.Flash().Err(err)
}
case p.IsContextCmd():
if err := c.contextCmd(p, pushCmd); err != nil {
c.app.Flash().Err(err)
}
case p.IsNamespaceCmd():
return c.namespaceCmd(p)
case p.IsDirCmd():
if a, ok := p.DirArg(); !ok {
c.app.Flash().Errf("Invalid command. Use `dir xxx`")
} else if err := c.app.dirCmd(a, pushCmd); err != nil {
c.app.Flash().Err(err)
}
default:
return false
}
return true
}
func (c *Command) viewMetaFor(p *cmd.Interpreter) (*client.GVR, *MetaViewer, error) {
gvr, exp, ok := c.alias.AsGVR(p.Cmd())
if !ok {
return client.NoGVR, nil, fmt.Errorf("`%s` command not found", p.Cmd())
}
if exp != "" {
p.Amend(cmd.NewInterpreter(gvr.String() + " " + exp))
}
v := MetaViewer{
viewerFn: func(gvr *client.GVR) ResourceViewer {
return NewScaleExtender(NewOwnerExtender(NewBrowser(gvr)))
},
}
if mv, ok := customViewers[gvr]; ok {
v = mv
}
return gvr, &v, nil
}
func (*Command) componentFor(gvr *client.GVR, fqn string, v *MetaViewer) ResourceViewer {
var view ResourceViewer
if v.viewerFn != nil {
view = v.viewerFn(gvr)
} else {
view = NewBrowser(gvr)
}
view.SetInstance(fqn)
if v.enterFn != nil {
view.GetTable().SetEnterFn(v.enterFn)
}
return view
}
func (c *Command) exec(p *cmd.Interpreter, gvr *client.GVR, comp model.Component, clearStack, pushCmd bool) (err error) {
defer func() {
if e := recover(); e != nil {
slog.Error("Failure detected during command exec", slogs.Error, e)
c.app.Content.Dump()
slog.Debug("Dumping history buffer", slogs.CmdHist, c.app.cmdHistory.List())
slog.Error("Dumping stack", slogs.Stack, string(debug.Stack()))
ci := cmd.NewInterpreter(podCmd)
cmds := c.app.cmdHistory.List()
currentCommand := cmds[c.app.cmdHistory.CurrentIndex()]
if currentCommand != podCmd {
ci = ci.Reset(currentCommand)
}
err = c.run(ci, "", true, true)
}
}()
if comp == nil {
return fmt.Errorf("no component found for %s", gvr)
}
comp.SetCommand(p)
c.app.Flash().Infof("Viewing %s...", gvr)
if clearStack {
cmd := contextRX.ReplaceAllString(p.GetLine(), "")
c.app.Config.SetActiveView(cmd)
}
if err := c.app.inject(comp, clearStack); err != nil {
return err
}
if pushCmd {
c.app.cmdHistory.Push(p.GetLine())
}
slog.Debug("History", slogs.Stack, c.app.cmdHistory.List())
return
}