checkpoint

mine
derailed 2019-12-28 14:55:41 -07:00
parent 9d23488ff5
commit c885495ef4
17 changed files with 311 additions and 89 deletions

53
internal/config/hotkey.go Normal file
View File

@ -0,0 +1,53 @@
package config
import (
"io/ioutil"
"path/filepath"
"gopkg.in/yaml.v2"
)
// K9sHotKeys manages K9s hotKeys.
var K9sHotKeys = filepath.Join(K9sHome, "hotkey.yml")
// HotKeys represents a collection of plugins.
type HotKeys struct {
HotKey map[string]HotKey `yaml:"hotKey"`
}
// HotKey describes a K9s hotkey.
type HotKey struct {
ShortCut string `yaml:"shortCut"`
Description string `yaml:"description"`
Command string `yaml:"command"`
}
// NewHotKeys returns a new plugin.
func NewHotKeys() HotKeys {
return HotKeys{
HotKey: make(map[string]HotKey),
}
}
// Load K9s plugins.
func (h HotKeys) Load() error {
return h.LoadHotKeys(K9sHotKeys)
}
// LoadHotKeys loads plugins from a given file.
func (h HotKeys) LoadHotKeys(path string) error {
f, err := ioutil.ReadFile(path)
if err != nil {
return err
}
var hh HotKeys
if err := yaml.Unmarshal(f, &hh); err != nil {
return err
}
for k, v := range hh.HotKey {
h.HotKey[k] = v
}
return nil
}

View File

@ -0,0 +1,21 @@
package config_test
import (
"testing"
"github.com/derailed/k9s/internal/config"
"github.com/stretchr/testify/assert"
)
func TestHotKeyLoad(t *testing.T) {
h := config.NewHotKeys()
assert.Nil(t, h.LoadHotKeys("test_assets/hot_key.yml"))
assert.Equal(t, 1, len(h.HotKey))
k, ok := h.HotKey["pods"]
assert.True(t, ok)
assert.Equal(t, "shift-0", k.ShortCut)
assert.Equal(t, "Launch pod view", k.Description)
assert.Equal(t, "pods", k.Command)
}

View File

@ -12,4 +12,11 @@ func TestPluginLoad(t *testing.T) {
assert.Nil(t, p.LoadPlugins("test_assets/plugin.yml"))
assert.Equal(t, 1, len(p.Plugin))
k, ok := p.Plugin["blah"]
assert.True(t, ok)
assert.Equal(t, "shift-s", k.ShortCut)
assert.Equal(t, "blee", k.Description)
assert.Equal(t, []string{"po", "dp"}, k.Scopes)
assert.Equal(t, "duh", k.Command)
assert.Equal(t, []string{"-n", "$NAMESPACE", "-boolean"}, k.Args)
}

View File

@ -0,0 +1,5 @@
hotKey:
pods:
shortCut: shift-0
description: Launch pod view
command: pods

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/derailed/k9s/internal/client"
@ -87,7 +88,7 @@ func (p *PortForwarder) FQN() string {
}
// Start initiates a port forward session for a given pod and ports.
func (p *PortForwarder) Start(path, co string, ports []string) (*portforward.PortForwarder, error) {
func (p *PortForwarder) Start(path, co, address string, ports []string) (*portforward.PortForwarder, error) {
p.path, p.container, p.ports, p.age = path, co, ports, time.Now()
ns, n := client.Namespaced(path)
@ -115,10 +116,10 @@ func (p *PortForwarder) Start(path, co string, ports []string) (*portforward.Por
Name(n).
SubResource("portforward")
return p.forwardPorts("POST", req.URL(), ports)
return p.forwardPorts("POST", req.URL(), address, ports)
}
func (p *PortForwarder) forwardPorts(method string, url *url.URL, ports []string) (*portforward.PortForwarder, error) {
func (p *PortForwarder) forwardPorts(method string, url *url.URL, address string, ports []string) (*portforward.PortForwarder, error) {
cfg, err := p.Config().RESTConfig()
if err != nil {
return nil, err
@ -129,7 +130,10 @@ func (p *PortForwarder) forwardPorts(method string, url *url.URL, ports []string
}
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url)
addrs := []string{localhost}
if address == "" {
address = localhost
}
addrs := strings.Split(address, ",")
return portforward.NewOnAddresses(dialer, addrs, ports, p.stopChan, p.readyChan, p.Out, p.ErrOut)
}

View File

@ -11,7 +11,7 @@ import (
const portForwardKey = "portforward"
// ShowPortForward pops a port forwarding configuration dialog.
func ShowPortForward(p *ui.Pages, port string, okFn func(lport, cport string)) {
func ShowPortForward(p *ui.Pages, port string, okFn func(address, lport, cport string)) {
f := tview.NewForm()
f.SetItemPadding(0)
f.SetButtonsAlign(tview.AlignCenter).
@ -20,16 +20,19 @@ func ShowPortForward(p *ui.Pages, port string, okFn func(lport, cport string)) {
SetLabelColor(tcell.ColorAqua).
SetFieldTextColor(tcell.ColorOrange)
p1, p2 := port, port
f.AddInputField("Pod Port:", p1, 20, nil, func(port string) {
p1 = port
p1, p2, address := port, port, "localhost"
f.AddInputField("Pod Port:", p1, 20, nil, func(p string) {
p1 = p
})
f.AddInputField("Local Port:", p2, 20, nil, func(port string) {
p2 = port
f.AddInputField("Local Port:", p2, 20, nil, func(p string) {
p2 = p
})
f.AddInputField("Address:", address, 20, nil, func(h string) {
address = h
})
f.AddButton("OK", func() {
okFn(stripPort(p2), stripPort(p1))
okFn(address, stripPort(p2), stripPort(p1))
})
f.AddButton("Cancel", func() {
DismissPortForward(p)
@ -48,6 +51,7 @@ func DismissPortForward(p *ui.Pages) {
p.RemovePage(portForwardKey)
}
// ----------------------------------------------------------------------------
// Helpers...
// StripPort removes the named port id if present.

View File

@ -11,7 +11,7 @@ import (
func TestPortForwardDialog(t *testing.T) {
p := ui.NewPages()
okFunc := func(lport, cport string) {
okFunc := func(address, lport, cport string) {
}
ShowPortForward(p, "8080", okFunc)
@ -38,7 +38,7 @@ func TestStripPort(t *testing.T) {
}
for k := range uu {
u := uu[k]
u := uu[k]
t.Run(k, func(t *testing.T) {
assert.Equal(t, u.e, stripPort(u.port))
})

View File

@ -30,6 +30,20 @@ const (
Key9
)
// Defines numeric keys for container actions
const (
KeyShift0 int32 = 41
KeyShift1 int32 = 33
KeyShift2 int32 = 64
KeyShift3 int32 = 35
KeyShift4 int32 = 36
KeyShift5 int32 = 37
KeyShift6 int32 = 94
KeyShift7 int32 = 38
KeyShift8 int32 = 42
KeyShift9 int32 = 40
)
// Defines char keystrokes
const (
KeyA tcell.Key = iota + 97
@ -151,6 +165,17 @@ func initStdKeys() {
}
func initShiftKeys() {
tcell.KeyNames[tcell.Key(KeyShift0)] = "Shift-0"
tcell.KeyNames[tcell.Key(KeyShift1)] = "Shift-1"
tcell.KeyNames[tcell.Key(KeyShift2)] = "Shift-2"
tcell.KeyNames[tcell.Key(KeyShift3)] = "Shift-3"
tcell.KeyNames[tcell.Key(KeyShift4)] = "Shift-4"
tcell.KeyNames[tcell.Key(KeyShift5)] = "Shift-5"
tcell.KeyNames[tcell.Key(KeyShift6)] = "Shift-6"
tcell.KeyNames[tcell.Key(KeyShift7)] = "Shift-7"
tcell.KeyNames[tcell.Key(KeyShift8)] = "Shift-8"
tcell.KeyNames[tcell.Key(KeyShift9)] = "Shift-9"
tcell.KeyNames[tcell.Key(KeyShiftA)] = "Shift-A"
tcell.KeyNames[tcell.Key(KeyShiftB)] = "Shift-B"
tcell.KeyNames[tcell.Key(KeyShiftC)] = "Shift-C"

View File

@ -83,6 +83,7 @@ func (t *Table) SendKey(evt *tcell.EventKey) {
}
func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {
log.Debug().Msgf("KEY PRESS %#v", evt)
key := evt.Key()
if key == tcell.KeyUp || key == tcell.KeyDown {
return evt

139
internal/view/actions.go Normal file
View File

@ -0,0 +1,139 @@
package view
import (
"fmt"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/ui"
"github.com/gdamore/tcell"
"github.com/rs/zerolog/log"
)
type Runner interface {
App() *App
GetSelectedItem() string
Aliases() []string
EnvFn() EnvFunc
}
func hasAll(scopes []string) bool {
for _, s := range scopes {
if s == "all" {
return true
}
}
return false
}
func includes(aliases []string, s string) bool {
for _, a := range aliases {
if a == s {
return true
}
}
return false
}
func inScope(scopes, aliases []string) bool {
if hasAll(scopes) {
return true
}
for _, s := range scopes {
if includes(aliases, s) {
return true
}
}
return false
}
func hotKeyActions(r Runner, aa ui.KeyActions) {
hh := config.NewHotKeys()
if err := hh.Load(); err != nil {
log.Warn().Msgf("No HotKey configuration found")
return
}
for k, hk := range hh.HotKey {
key, err := asKey(hk.ShortCut)
if err != nil {
log.Error().Err(err).Msg("Unable to map hotkey 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(
hk.Description,
gotoCmd(r, hk.Command),
true)
}
}
func gotoCmd(r Runner, cmd string) ui.ActionHandler {
return func(evt *tcell.EventKey) *tcell.EventKey {
if err := r.App().gotoResource(cmd); err != nil {
r.App().Flash().Err(err)
}
return nil
}
}
func pluginActions(r Runner, 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 !inScope(plugin.Scopes, r.Aliases()) {
continue
}
key, err := asKey(plugin.ShortCut)
if err != nil {
log.Error().Err(err).Msg("Unable to map plugin 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,
execCmd(r, plugin.Command, plugin.Background, plugin.Args...),
true)
}
}
func execCmd(r Runner, bin string, bg bool, args ...string) ui.ActionHandler {
return func(evt *tcell.EventKey) *tcell.EventKey {
path := r.GetSelectedItem()
if path == "" {
return evt
}
var (
env = r.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, r.App(), bin, bg, aa...) {
r.App().Flash().Info("Custom CMD launched!")
} else {
r.App().Flash().Info("Custom CMD failed!")
}
return nil
}
}

View File

@ -447,7 +447,8 @@ func (b *Browser) refreshActions() {
if client.Can(b.meta.Verbs, "describe") {
aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true)
}
b.customActions(aa)
pluginActions(b, aa)
hotKeyActions(b, aa)
b.Actions().Add(aa)
if b.bindKeysFn != nil {
@ -456,61 +457,12 @@ func (b *Browser) refreshActions() {
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) Aliases() []string {
return append(b.meta.ShortNames, b.meta.SingularName, b.meta.Name)
}
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) EnvFn() EnvFunc {
return b.envFn
}
func (b *Browser) defaultK9sEnv() K9sEnv {

View File

@ -85,9 +85,6 @@ func (c *command) viewMetaFor(cmd string) (string, *MetaViewer, error) {
if !ok {
return "", nil, fmt.Errorf("Huh? `%s` command not found", cmd)
}
if _, err := c.app.factory.CanForResource(c.app.Config.ActiveNamespace(), gvr); err != nil {
return "", nil, err
}
v, ok := customViewers[client.GVR(gvr)]
if !ok {

View File

@ -64,7 +64,6 @@ func (c *Container) selectedContainer() string {
}
func (c *Container) viewLogs(app *App, ns, res, path string) {
log.Debug().Msgf(">>>>>>>> ViewLOgs %q -- %q -- %q", ns, res, path)
status := c.GetTable().GetSelectedCell(3)
if status != "Running" && status != "Completed" {
app.Flash().Err(errors.New("No logs available"))
@ -134,11 +133,11 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
func (c *Container) portForward(lport, cport string) {
func (c *Container) portForward(address, lport, cport string) {
co := c.GetTable().GetSelectedCell(0)
pf := dao.NewPortForwarder(c.App().Conn())
ports := []string{lport + ":" + cport}
fw, err := pf.Start(c.GetTable().Path, co, ports)
fw, err := pf.Start(c.GetTable().Path, co, address, ports)
if err != nil {
c.App().Flash().Err(err)
return

View File

@ -9,6 +9,7 @@ import (
"strings"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/config"
"github.com/derailed/k9s/internal/model"
"github.com/derailed/k9s/internal/render"
"github.com/derailed/k9s/internal/ui"
@ -104,6 +105,22 @@ func (v *Help) showNav() model.MenuHints {
}
}
func (v *Help) showHotKeys() (model.MenuHints, error) {
hh := config.NewHotKeys()
if err := hh.Load(); err != nil {
return nil, fmt.Errorf("no hotkey configuration found")
}
m := make(model.MenuHints, 0, len(hh.HotKey))
for _, hk := range hh.HotKey {
m = append(m, model.MenuHint{
Mnemonic: hk.ShortCut,
Description: hk.Description,
})
}
return m, nil
}
func (v *Help) showGeneral() model.MenuHints {
return model.MenuHints{
{
@ -160,10 +177,18 @@ func (v *Help) resetTitle() {
func (v *Help) build(hh model.MenuHints) {
v.Clear()
sort.Sort(hh)
v.addSection(0, "RESOURCE", hh)
v.addSection(4, "GENERAL", v.showGeneral())
v.addSection(6, "NAVIGATION", v.showNav())
v.addSection(8, "HELP", v.showHelp())
var col int
v.addSection(col, "RESOURCE", hh)
col += 2
v.addSection(col, "GENERAL", v.showGeneral())
col += 2
v.addSection(col, "NAVIGATION", v.showNav())
col += 2
if h, err := v.showHotKeys(); err == nil {
v.addSection(col, "HOTKEYS", h)
col += 2
}
v.addSection(col, "HELP", v.showHelp())
}
func (v *Help) addSection(c int, title string, hh model.MenuHints) {

View File

@ -58,17 +58,6 @@ func extractApp(ctx context.Context) (*App, error) {
return app, nil
}
// In check if a string belongs to a set.
func in(ss []string, s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
}
// AsKey maps a string representation of a key to a tcell key.
func asKey(key string) (tcell.Key, error) {
for k, v := range tcell.KeyNames {

View File

@ -201,6 +201,7 @@ func (f *Factory) ensureFactory(ns string) di.DynamicSharedInformerFactory {
}
func toGVR(gvr string) schema.GroupVersionResource {
log.Debug().Msgf(">>> Convert GVR %q", gvr)
tokens := strings.Split(gvr, "/")
if len(tokens) < 3 {
tokens = append([]string{""}, tokens...)

View File

@ -10,7 +10,7 @@ import (
// Forwarder represents a port forwarder.
type Forwarder interface {
// Start initializes a port forward.
Start(path, co string, ports []string) (*portforward.PortForwarder, error)
Start(path, co, address string, ports []string) (*portforward.PortForwarder, error)
// Stop terminates a port forward.
Stop()