k9s/internal/view/generic.go

513 lines
12 KiB
Go

package view
import (
"bytes"
"context"
"errors"
"fmt"
"strconv"
"time"
"github.com/atotto/clipboard"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/dao"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/resource"
"github.com/derailed/k9s/internal/ui"
"github.com/derailed/k9s/internal/ui/dialog"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/printers"
)
// ContextFunc enhances a given context.
type ContextFunc func(context.Context) context.Context
// Generic represents a generic resource vieweg.
type Generic struct {
*Table
namespaces map[int]string
path string
gvr dao.GVR
envFn EnvFunc
meta metav1.APIResource
accessor dao.Accessor
contextFn ContextFunc
}
// NewGeneric returns a new vieweg.
func NewGeneric(gvr dao.GVR) *Generic {
return &Generic{
Table: NewTable(string(gvr)),
gvr: gvr,
}
}
// Init watches all running pods in given namespace
func (g *Generic) Init(ctx context.Context) error {
log.Debug().Msgf(">>> GENERIC VIEW INIT %s", g.gvr)
var err error
g.meta, err = dao.MetaFor(g.gvr)
if err != nil {
return err
}
if err := g.Table.Init(ctx); err != nil {
return err
}
g.Table.BaseTitle = g.meta.Kind
g.accessor, err = dao.AccessorFor(g.app.factory, g.gvr)
if err != nil {
return err
}
g.envFn = g.defaultK9sEnv
g.Table.setFilterFn(g.filterGeneric)
g.setNamespace(g.App().Config.ActiveNamespace())
g.refresh()
row, _ := g.GetSelection()
if row == 0 && g.GetRowCount() > 0 {
g.Select(1, 0)
}
return nil
}
// Start initializes updates.
func (g *Generic) Start() {
g.Stop()
log.Debug().Msgf(">>>>>>> START %s", g.gvr)
g.Table.Start()
var ctx context.Context
ctx, g.cancelFn = context.WithCancel(context.Background())
go g.update(ctx)
}
func (g *Generic) Refresh() {
g.app.QueueUpdateDraw(func() {
g.refresh()
})
}
// Name returns the component name.
func (g *Generic) Name() string {
return g.meta.Kind
}
func (g *Generic) SetContextFn(f ContextFunc) {
g.contextFn = f
}
// List returns a resource List.
func (g *Generic) List() resource.List { return nil }
// SetEnvFn sets a function to pull viewer env vars for plugins.
func (g *Generic) SetEnvFn(f EnvFunc) { g.envFn = f }
// SetPath set parents selector.
func (g *Generic) SetPath(p string) { g.Path = p }
// GVR returns a resource descriptor.
func (g *Generic) GVR() string { return string(g.gvr) }
func (g *Generic) GetTable() *Table {
return g.Table
}
func (g *Generic) filterGeneric(sel string) {
panic("NYI")
// g.list.SetLabelSelector(sel)
g.refresh()
}
func (g *Generic) update(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Debug().Msgf("%s updater canceled!", g.gvr)
return
case <-time.After(time.Duration(g.app.Config.K9s.GetRefreshRate()) * time.Second):
g.app.QueueUpdateDraw(func() {
g.refresh()
})
}
}
}
// ----------------------------------------------------------------------------
// Actions()...
func (g *Generic) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
if !g.RowSelected() {
return evt
}
_, n := namespaced(g.GetSelectedItem())
log.Debug().Msgf("Copied selection to clipboard %q", n)
g.app.Flash().Info("Current selection copied to clipboard...")
if err := clipboard.WriteAll(n); err != nil {
g.app.Flash().Err(err)
}
return nil
}
func (g *Generic) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("RES ENTER CMD...")
// If in command mode run filter otherwise enter function.
if g.filterCmd(evt) == nil || !g.RowSelected() {
return nil
}
f := g.defaultEnter
if g.enterFn != nil {
log.Debug().Msgf("Found custom enter")
f = g.enterFn
}
f(g.app, g.Data.Namespace, string(g.gvr), g.GetSelectedItem())
return nil
}
func (g *Generic) refreshCmd(*tcell.EventKey) *tcell.EventKey {
g.app.Flash().Info("Refreshing...")
g.refresh()
return nil
}
func (g *Generic) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
selections := g.GetSelectedItems()
if len(selections) == 0 {
return evt
}
log.Debug().Msgf("DEL SELECTIONS %#v", selections)
var msg string
if len(selections) > 1 {
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), g.gvr)
} else {
msg = fmt.Sprintf("Delete %s %s?", g.gvr, selections[0])
}
cancelFn := func() {}
if in(g.meta.Categories, "K9s") {
dialog.ShowConfirm(g.app.Content.Pages, "Confirm Delete", msg, func() {
g.ShowDeleted()
if len(selections) > 1 {
g.app.Flash().Infof("Delete %d marked %s", len(selections), g.gvr)
} else {
g.app.Flash().Infof("Delete resource %s %s", g.gvr, selections[0])
}
for _, sel := range selections {
ns, n := namespaced(sel)
if err := g.accessor.(dao.Nuker).Delete(ns, n, true, true); err != nil {
g.app.Flash().Errf("Delete failed with %s", err)
} else {
g.GetTable().DeleteMark(sel)
}
}
g.refresh()
g.SelectRow(1, true)
}, cancelFn)
return nil
}
dialog.ShowDelete(g.app.Content.Pages, msg, func(cascade, force bool) {
g.ShowDeleted()
if len(selections) > 1 {
g.app.Flash().Infof("Delete %d marked %s", len(selections), g.gvr)
} else {
g.app.Flash().Infof("Delete resource %s %s", g.gvr, selections[0])
}
for _, sel := range selections {
ns, n := namespaced(sel)
if err := g.accessor.(dao.Nuker).Delete(ns, n, cascade, force); err != nil {
g.app.Flash().Errf("Delete failed with %s", err)
} else {
g.app.forwarders.Kill(sel)
g.GetTable().DeleteMark(sel)
}
}
g.refresh()
g.SelectRow(1, true)
}, func() {})
return nil
}
func (g *Generic) defaultEnter(app *App, ns, _, sel string) {
log.Debug().Msgf("--------- Resource %q Verbs %v", sel, g.meta.Verbs)
ns, n := namespaced(sel)
yaml, err := dao.Describe(g.app.Conn(), g.gvr, ns, n)
if err != nil {
g.app.Flash().Errf("Describe command failed: %s", err)
return
}
details := NewDetails("Describe")
details.SetSubject(sel)
details.SetTextColor(g.app.Styles.FgColor())
details.SetText(colorizeYAML(g.app.Styles.Views().Yaml, yaml))
details.ScrollToBeginning()
g.app.inject(details)
}
func (g *Generic) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("DESCRIBE %t -- %#v", g.RowSelected(), g.GetSelectedItems())
if !g.RowSelected() {
return evt
}
g.defaultEnter(g.app, g.Data.Namespace, string(g.gvr), g.GetSelectedItem())
return nil
}
func (g *Generic) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
if !g.RowSelected() {
return evt
}
sel := g.GetSelectedItem()
ns, n := resource.Namespaced(sel)
if ns == "" {
ns = g.Data.Namespace
}
log.Debug().Msgf("------ NAMESPACES %q vs %q", ns, g.Data.Namespace)
o, err := g.app.factory.Get(ns, string(g.gvr), n, labels.Everything())
if err != nil {
g.app.Flash().Errf("Unable to get resource %s", err)
return nil
}
raw, err := toYAML(o)
if err != nil {
g.app.Flash().Errf("Unable to marshal resource %s", err)
return nil
}
details := NewDetails("YAML")
details.SetSubject(sel)
details.SetTextColor(g.app.Styles.FgColor())
details.SetText(colorizeYAML(g.app.Styles.Views().Yaml, raw))
details.ScrollToBeginning()
g.app.inject(details)
return nil
}
func toYAML(o runtime.Object) (string, error) {
var (
buff bytes.Buffer
p printers.YAMLPrinter
)
err := p.PrintObj(o, &buff)
if err != nil {
log.Error().Msgf("Marshal Error %v", err)
return "", err
}
return buff.String(), nil
}
func (g *Generic) editCmd(evt *tcell.EventKey) *tcell.EventKey {
if !g.RowSelected() {
return evt
}
g.Stop()
defer g.Start()
{
ns, po := namespaced(g.GetSelectedItem())
args := make([]string, 0, 10)
args = append(args, "edit")
args = append(args, g.meta.Kind)
args = append(args, "-n", ns)
args = append(args, "--context", g.app.Config.K9s.CurrentContext)
if cfg := g.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
args = append(args, "--kubeconfig", *cfg)
}
if !runK(true, g.app, append(args, po)...) {
g.app.Flash().Err(errors.New("Edit exec failed"))
}
}
return evt
}
func (g *Generic) setNamespace(ns string) {
if !g.meta.Namespaced {
g.Data.Namespace = render.ClusterWide
return
}
if g.Data.Namespace == ns {
return
}
if ns == render.NamespaceAll {
ns = render.AllNamespaces
}
log.Debug().Msgf("!!!!!! SETTING NS %q", ns)
g.Data.Namespace = ns
g.Data.RowEvents = g.Data.RowEvents.Clear()
}
func (g *Generic) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
i, _ := strconv.Atoi(string(evt.Rune()))
ns := g.namespaces[i]
if ns == "" {
ns = render.NamespaceAll
}
g.app.switchNS(ns)
g.setNamespace(ns)
g.app.Flash().Infof("Viewing namespace `%s`...", ns)
g.refresh()
g.UpdateTitle()
g.SelectRow(1, true)
g.app.CmdBuff().Reset()
if err := g.app.Config.SetActiveNamespace(g.Data.Namespace); err != nil {
log.Error().Err(err).Msg("Config save NS failed!")
}
if err := g.app.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")
}
return nil
}
func (g *Generic) refresh() {
if g.app.Conn() == nil {
log.Error().Msg("No api connection")
return
}
log.Debug().Msgf("REFRESHING (%q) in ns %q", g.gvr, g.Data.Namespace)
ctx := g.defaultContext()
if g.contextFn != nil {
ctx = g.contextFn(ctx)
}
data, err := dao.Reconcile(ctx, g.Table.Data, g.gvr)
if err != nil {
g.app.Flash().Err(err)
}
g.refreshActions()
g.Update(data)
}
func (g *Generic) defaultContext() context.Context {
ctx := context.WithValue(context.Background(), internal.KeyFactory, g.app.factory)
ctx = context.WithValue(ctx, internal.KeySelection, g.Path)
ctx = context.WithValue(ctx, internal.KeyLabels, "")
ctx = context.WithValue(ctx, internal.KeyFields, "")
return ctx
}
func (g *Generic) namespaceActions(aa ui.KeyActions) {
if g.app.Conn() == nil || !g.meta.Namespaced {
return
}
g.namespaces = make(map[int]string, config.MaxFavoritesNS)
aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(resource.AllNamespace, g.switchNamespaceCmd, true)
g.namespaces[0] = resource.AllNamespace
index := 1
for _, n := range g.app.Config.FavNamespaces() {
if n == resource.AllNamespace {
continue
}
aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(n, g.switchNamespaceCmd, true)
g.namespaces[index] = n
index++
}
}
func (g *Generic) refreshActions() {
aa := ui.KeyActions{
ui.KeyC: ui.NewKeyAction("Copy", g.cpCmd, false),
tcell.KeyEnter: ui.NewKeyAction("View", g.enterCmd, false),
tcell.KeyCtrlR: ui.NewKeyAction("Refresh", g.refreshCmd, false),
}
g.namespaceActions(aa)
if dao.Can(g.meta.Verbs, "edit") {
aa[ui.KeyE] = ui.NewKeyAction("Edit", g.editCmd, true)
}
if dao.Can(g.meta.Verbs, "delete") {
aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", g.deleteCmd, true)
}
if dao.Can(g.meta.Verbs, "view") {
aa[ui.KeyY] = ui.NewKeyAction("YAML", g.viewCmd, true)
}
if dao.Can(g.meta.Verbs, "describe") {
aa[ui.KeyD] = ui.NewKeyAction("Describe", g.describeCmd, true)
}
g.customActions(aa)
g.Actions().Set(aa)
}
func (g *Generic) customActions(aa ui.KeyActions) {
pp := config.NewPlugins()
if err := pp.Load(); err != nil {
log.Warn().Msgf("No plugin configuration found")
return
}
for k, plugin := range pp.Plugin {
if !in(plugin.Scopes, g.meta.Name) {
continue
}
key, err := asKey(plugin.ShortCut)
if err != nil {
log.Error().Err(err).Msg("Unable to map shortcut to a key")
continue
}
_, ok := aa[key]
if ok {
log.Error().Err(fmt.Errorf("Doh! you are trying to overide an existing command `%s", k)).Msg("Invalid shortcut")
continue
}
aa[key] = ui.NewKeyAction(
plugin.Description,
g.execCmd(plugin.Command, plugin.Background, plugin.Args...),
true)
}
}
func (g *Generic) execCmd(bin string, bg bool, args ...string) ui.ActionHandler {
return func(evt *tcell.EventKey) *tcell.EventKey {
if !g.RowSelected() {
return evt
}
var (
env = g.envFn()
aa = make([]string, len(args))
err error
)
for i, a := range args {
aa[i], err = env.envFor(a)
if err != nil {
log.Error().Err(err).Msg("Args match failed")
return nil
}
}
if run(true, g.app, bin, bg, aa...) {
g.app.Flash().Info("Custom CMD launched!")
} else {
g.app.Flash().Info("Custom CMD failed!")
}
return nil
}
}
func (g *Generic) defaultK9sEnv() K9sEnv {
return defaultK9sEnv(g.app, g.GetSelectedItem(), g.GetRow())
}