520 lines
13 KiB
Go
520 lines
13 KiB
Go
package view
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
rt "runtime"
|
|
"strconv"
|
|
|
|
"github.com/atotto/clipboard"
|
|
"github.com/derailed/k9s/internal"
|
|
"github.com/derailed/k9s/internal/client"
|
|
"github.com/derailed/k9s/internal/config"
|
|
"github.com/derailed/k9s/internal/dao"
|
|
"github.com/derailed/k9s/internal/render"
|
|
"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
|
|
|
|
type BindKeysFunc func(ui.KeyActions)
|
|
|
|
// Browser represents a generic resource browser.
|
|
type Browser struct {
|
|
*Table
|
|
|
|
namespaces map[int]string
|
|
gvr client.GVR
|
|
envFn EnvFunc
|
|
meta metav1.APIResource
|
|
accessor dao.Accessor
|
|
contextFn ContextFunc
|
|
bindKeysFn BindKeysFunc
|
|
cancelFn context.CancelFunc
|
|
}
|
|
|
|
// NewBrowser returns a new browser.
|
|
func NewBrowser(gvr client.GVR) ResourceViewer {
|
|
return &Browser{
|
|
Table: NewTable(string(gvr)),
|
|
gvr: gvr,
|
|
}
|
|
}
|
|
|
|
// Init watches all running pods in given namespace
|
|
func (b *Browser) Init(ctx context.Context) error {
|
|
var err error
|
|
b.meta, err = dao.MetaFor(b.gvr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = b.Table.Init(ctx); err != nil {
|
|
return err
|
|
}
|
|
if !dao.IsK9sMeta(b.meta) {
|
|
if _, err := b.app.factory.CanForResource(b.app.Config.ActiveNamespace(), b.GVR()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if b.bindKeysFn != nil {
|
|
b.bindKeysFn(b.Actions())
|
|
}
|
|
b.BaseTitle = b.meta.Kind
|
|
b.SetTitle(" [orange:i:]LOADING... ")
|
|
b.accessor, err = dao.AccessorFor(b.app.factory, b.gvr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Debug().Msgf("ACCESSOR FOR %s -- %#v", b.gvr, b.accessor)
|
|
|
|
b.envFn = b.defaultK9sEnv
|
|
b.setNamespace(b.App().Config.ActiveNamespace())
|
|
row, _ := b.GetSelection()
|
|
if row == 0 && b.GetRowCount() > 0 {
|
|
b.Select(1, 0)
|
|
}
|
|
b.GetModel().AddListener(b)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Start initializes browser updates.
|
|
func (b *Browser) Start() {
|
|
b.Stop()
|
|
log.Debug().Msgf("GOROUTINE %d", rt.NumGoroutine())
|
|
log.Debug().Msgf("BROWSER START %s", b.gvr)
|
|
|
|
b.Table.Start()
|
|
ctx := b.defaultContext()
|
|
ctx, b.cancelFn = context.WithCancel(ctx)
|
|
if b.contextFn != nil {
|
|
ctx = b.contextFn(ctx)
|
|
}
|
|
if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" {
|
|
b.Path = path
|
|
}
|
|
|
|
b.GetModel().Start(ctx)
|
|
}
|
|
|
|
// Stop terminates browser updates.
|
|
func (b *Browser) Stop() {
|
|
if b.cancelFn == nil {
|
|
return
|
|
}
|
|
b.Table.Stop()
|
|
log.Debug().Msgf("BROWSER <STOP> %q", b.gvr)
|
|
b.cancelFn()
|
|
b.cancelFn = nil
|
|
}
|
|
|
|
func (b *Browser) refresh() {
|
|
b.Start()
|
|
}
|
|
|
|
// Name returns the component name.
|
|
func (b *Browser) Name() string { return b.meta.Kind }
|
|
|
|
// SetContextFn populates a custom context.
|
|
func (b *Browser) SetContextFn(f ContextFunc) { b.contextFn = f }
|
|
|
|
// SetBindKeysFn adds additional key bindings.
|
|
func (b *Browser) SetBindKeysFn(f BindKeysFunc) { b.bindKeysFn = f }
|
|
|
|
// SetEnvFn sets a function to pull viewer env vars for plugins.
|
|
func (b *Browser) SetEnvFn(f EnvFunc) { b.envFn = f }
|
|
|
|
// GVR returns a resource descriptor.
|
|
func (b *Browser) GVR() string { return string(b.gvr) }
|
|
|
|
// GetTable returns the underlying table.
|
|
func (b *Browser) GetTable() *Table { return b.Table }
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Actions()...
|
|
|
|
func (b *Browser) cpCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
path := b.GetSelectedItem()
|
|
if path == "" {
|
|
return evt
|
|
}
|
|
|
|
_, n := client.Namespaced(path)
|
|
log.Debug().Msgf("Copied selection to clipboard %q", n)
|
|
b.app.Flash().Info("Current selection copied to clipboard...")
|
|
if err := clipboard.WriteAll(n); err != nil {
|
|
b.app.Flash().Err(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
path := b.GetSelectedItem()
|
|
if b.filterCmd(evt) == nil || path == "" {
|
|
return nil
|
|
}
|
|
|
|
f := b.describeResource
|
|
if b.enterFn != nil {
|
|
f = b.enterFn
|
|
}
|
|
f(b.app, b.GetModel().GetNamespace(), string(b.gvr), path)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Browser) refreshCmd(*tcell.EventKey) *tcell.EventKey {
|
|
b.app.Flash().Info("Refreshing...")
|
|
b.refresh()
|
|
return nil
|
|
}
|
|
|
|
func (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
selections := b.GetSelectedItems()
|
|
if len(selections) == 0 {
|
|
return evt
|
|
}
|
|
log.Debug().Msgf("DEL SELECTIONS %#v", selections)
|
|
|
|
b.Stop()
|
|
defer b.Start()
|
|
{
|
|
msg := fmt.Sprintf("Delete %s %s?", b.gvr, selections[0])
|
|
if len(selections) > 1 {
|
|
msg = fmt.Sprintf("Delete %d marked %s?", len(selections), b.gvr)
|
|
}
|
|
|
|
cancelFn := func() {}
|
|
if dao.IsK9sMeta(b.meta) {
|
|
dialog.ShowConfirm(b.app.Content.Pages, "Confirm Delete", msg, func() {
|
|
b.ShowDeleted()
|
|
if len(selections) > 1 {
|
|
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.gvr)
|
|
} else {
|
|
b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0])
|
|
}
|
|
for _, sel := range selections {
|
|
if err := b.accessor.(dao.Nuker).Delete(sel, true, true); err != nil {
|
|
b.app.Flash().Errf("Delete failed with `%s", err)
|
|
} else {
|
|
b.GetTable().DeleteMark(sel)
|
|
}
|
|
}
|
|
b.refresh()
|
|
b.SelectRow(1, true)
|
|
}, cancelFn)
|
|
return nil
|
|
}
|
|
|
|
dialog.ShowDelete(b.app.Content.Pages, msg, func(cascade, force bool) {
|
|
b.ShowDeleted()
|
|
if len(selections) > 1 {
|
|
b.app.Flash().Infof("Delete %d marked %s", len(selections), b.gvr)
|
|
} else {
|
|
b.app.Flash().Infof("Delete resource %s %s", b.gvr, selections[0])
|
|
}
|
|
for _, sel := range selections {
|
|
if err := b.accessor.(dao.Nuker).Delete(sel, cascade, force); err != nil {
|
|
b.app.Flash().Errf("Delete failed with `%s", err)
|
|
} else {
|
|
b.app.factory.DeleteForwarder(sel)
|
|
b.GetTable().DeleteMark(sel)
|
|
}
|
|
}
|
|
b.refresh()
|
|
b.SelectRow(1, true)
|
|
}, cancelFn)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
path := b.GetSelectedItem()
|
|
if path == "" {
|
|
return evt
|
|
}
|
|
b.describeResource(b.app, b.GetModel().GetNamespace(), string(b.gvr), path)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Browser) describeResource(app *App, _, _, sel string) {
|
|
ns, n := client.Namespaced(sel)
|
|
yaml, err := dao.Describe(b.app.Conn(), b.gvr, ns, n)
|
|
if err != nil {
|
|
b.app.Flash().Errf("Describe command failed: %s", err)
|
|
return
|
|
}
|
|
|
|
details := NewDetails("Describe")
|
|
details.SetSubject(sel)
|
|
details.SetTextColor(b.app.Styles.FgColor())
|
|
details.SetText(colorizeYAML(b.app.Styles.Views().Yaml, yaml))
|
|
details.ScrollToBeginning()
|
|
if err := b.app.inject(details); err != nil {
|
|
b.app.Flash().Err(err)
|
|
}
|
|
}
|
|
|
|
func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
path := b.GetSelectedItem()
|
|
if path == "" {
|
|
return evt
|
|
}
|
|
|
|
log.Debug().Msgf("------ NAMESPACES %q vs %q", path, b.GetModel().GetNamespace())
|
|
o, err := b.app.factory.Get(string(b.gvr), path, labels.Everything())
|
|
if err != nil {
|
|
b.app.Flash().Errf("Unable to get resource %q -- %s", b.gvr, err)
|
|
return nil
|
|
}
|
|
|
|
raw, err := toYAML(o)
|
|
if err != nil {
|
|
b.app.Flash().Errf("Unable to marshal resource %s", err)
|
|
return nil
|
|
}
|
|
|
|
details := NewDetails("YAML")
|
|
details.SetSubject(path)
|
|
details.SetTextColor(b.app.Styles.FgColor())
|
|
details.SetText(colorizeYAML(b.app.Styles.Views().Yaml, raw))
|
|
details.ScrollToBeginning()
|
|
if err := b.app.inject(details); err != nil {
|
|
b.App().Flash().Err(err)
|
|
}
|
|
|
|
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 (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
path := b.GetSelectedItem()
|
|
if path == "" {
|
|
return evt
|
|
}
|
|
|
|
b.Stop()
|
|
defer b.Start()
|
|
{
|
|
ns, n := client.Namespaced(path)
|
|
args := make([]string, 0, 10)
|
|
args = append(args, "edit")
|
|
args = append(args, b.meta.Kind)
|
|
args = append(args, "-n", ns)
|
|
args = append(args, "--context", b.app.Config.K9s.CurrentContext)
|
|
if cfg := b.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
|
|
args = append(args, "--kubeconfig", *cfg)
|
|
}
|
|
if !runK(true, b.app, append(args, n)...) {
|
|
b.app.Flash().Err(errors.New("Edit exec failed"))
|
|
}
|
|
}
|
|
|
|
return evt
|
|
}
|
|
|
|
func (b *Browser) setNamespace(ns string) {
|
|
if !b.meta.Namespaced {
|
|
b.GetModel().SetNamespace(render.ClusterScope)
|
|
return
|
|
}
|
|
if b.GetModel().InNamespace(ns) {
|
|
return
|
|
}
|
|
|
|
if ns == render.NamespaceAll {
|
|
ns = render.AllNamespaces
|
|
}
|
|
log.Debug().Msgf("!!!!!! SETTING NS %q", ns)
|
|
b.GetModel().SetNamespace(ns)
|
|
}
|
|
|
|
func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {
|
|
i, _ := strconv.Atoi(string(evt.Rune()))
|
|
ns := b.namespaces[i]
|
|
if ns == "" {
|
|
ns = render.NamespaceAll
|
|
}
|
|
|
|
b.app.switchNS(ns)
|
|
b.setNamespace(ns)
|
|
b.app.Flash().Infof("Viewing namespace `%s`...", ns)
|
|
b.refresh()
|
|
b.UpdateTitle()
|
|
b.SelectRow(1, true)
|
|
b.app.CmdBuff().Reset()
|
|
if err := b.app.Config.SetActiveNamespace(b.GetModel().GetNamespace()); err != nil {
|
|
log.Error().Err(err).Msg("Config save NS failed!")
|
|
}
|
|
if err := b.app.Config.Save(); err != nil {
|
|
log.Error().Err(err).Msg("Config save failed!")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// TableLoadChanged notifies view something went south.
|
|
func (b *Browser) TableLoadFailed(err error) {
|
|
b.app.QueueUpdateDraw(func() {
|
|
b.app.Flash().Err(err)
|
|
})
|
|
}
|
|
|
|
// TableDataChanged notifies view new data is available.
|
|
func (b *Browser) TableDataChanged(data render.TableData) {
|
|
b.Update(data)
|
|
b.app.QueueUpdateDraw(func() {
|
|
b.refreshActions()
|
|
})
|
|
}
|
|
|
|
func (b *Browser) defaultContext() context.Context {
|
|
ctx := context.Background()
|
|
|
|
ctx = context.WithValue(ctx, internal.KeyFactory, b.app.factory)
|
|
ctx = context.WithValue(ctx, internal.KeyGVR, string(b.gvr))
|
|
ctx = context.WithValue(ctx, internal.KeyPath, b.Path)
|
|
ctx = context.WithValue(ctx, internal.KeyLabels, "")
|
|
ctx = context.WithValue(ctx, internal.KeyFields, "")
|
|
ctx = context.WithValue(ctx, internal.KeyNamespace, b.App().Config.ActiveNamespace())
|
|
|
|
return ctx
|
|
}
|
|
|
|
func (b *Browser) namespaceActions(aa ui.KeyActions) {
|
|
if b.app.Conn() == nil || !b.meta.Namespaced || b.GetTable().Path != "" {
|
|
log.Warn().Msgf("NOT NAMESPACE RES %q -- %t -- %q", b.gvr, b.meta.Namespaced, b.GetTable().Path)
|
|
return
|
|
}
|
|
b.namespaces = make(map[int]string, config.MaxFavoritesNS)
|
|
aa[tcell.Key(ui.NumKeys[0])] = ui.NewKeyAction(render.NamespaceAll, b.switchNamespaceCmd, true)
|
|
b.namespaces[0] = render.NamespaceAll
|
|
index := 1
|
|
for _, n := range b.app.Config.FavNamespaces() {
|
|
if n == render.NamespaceAll {
|
|
continue
|
|
}
|
|
aa[tcell.Key(ui.NumKeys[index])] = ui.NewKeyAction(n, b.switchNamespaceCmd, true)
|
|
b.namespaces[index] = n
|
|
index++
|
|
}
|
|
}
|
|
|
|
func (b *Browser) refreshActions() {
|
|
aa := ui.KeyActions{
|
|
ui.KeyC: ui.NewKeyAction("Copy", b.cpCmd, false),
|
|
tcell.KeyEnter: ui.NewKeyAction("View", b.enterCmd, false),
|
|
tcell.KeyCtrlR: ui.NewKeyAction("Refresh", b.refreshCmd, false),
|
|
}
|
|
b.namespaceActions(aa)
|
|
|
|
if client.Can(b.meta.Verbs, "edit") {
|
|
aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true)
|
|
}
|
|
if client.Can(b.meta.Verbs, "delete") {
|
|
aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true)
|
|
}
|
|
if client.Can(b.meta.Verbs, "view") {
|
|
aa[ui.KeyY] = ui.NewKeyAction("YAML", b.viewCmd, true)
|
|
}
|
|
if client.Can(b.meta.Verbs, "describe") {
|
|
aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true)
|
|
}
|
|
b.customActions(aa)
|
|
b.Actions().Add(aa)
|
|
|
|
if b.bindKeysFn != nil {
|
|
b.bindKeysFn(b.Actions())
|
|
}
|
|
b.app.Menu().HydrateMenu(b.Hints())
|
|
}
|
|
|
|
func (b *Browser) 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, b.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,
|
|
b.execCmd(plugin.Command, plugin.Background, plugin.Args...),
|
|
true)
|
|
}
|
|
}
|
|
|
|
func (b *Browser) execCmd(bin string, bg bool, args ...string) ui.ActionHandler {
|
|
return func(evt *tcell.EventKey) *tcell.EventKey {
|
|
path := b.GetSelectedItem()
|
|
if path == "" {
|
|
return evt
|
|
}
|
|
|
|
var (
|
|
env = b.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, b.app, bin, bg, aa...) {
|
|
b.app.Flash().Info("Custom CMD launched!")
|
|
} else {
|
|
b.app.Flash().Info("Custom CMD failed!")
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (b *Browser) defaultK9sEnv() K9sEnv {
|
|
return defaultK9sEnv(b.app, b.GetSelectedItem(), b.GetSelectedRow())
|
|
}
|