add dir view

mine
derailed 2020-07-02 17:58:25 -06:00
parent f1111174aa
commit 5893cffb0d
18 changed files with 443 additions and 13 deletions

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ pod1.go
faas
.settings/*
demos
/code

View File

@ -6,7 +6,7 @@
Thank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!
Also if you dig this tool, consider joining our [sponsorhip program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
If you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorhip program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)
On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)
@ -19,7 +19,18 @@ First off, I would like to send a `Big Thank You` to the following generous K9s
* [Remo Eichenberger](https://github.com/remoe)
* [Ken Ahrens](https://github.com/kenahrens)
Maintenance Release!
## Moving Forward!
In this drop, we've added a port-forward indicator to visually see if a port-forward is active on a pod/container. You can also navigate directly to the port-forward view using the new shortcut `f` available in
pod and container view.
## Manifest That!
Ever wanted to manipulate your Kubernetes manifests directly in K9s? `Yes Please!!`
We are introducing a new view namely `directory` aka `dir`. Using this command you can list/traverse a given directory structure containing your Kubernetes manifests using a new `:dir /fred` command.
From there you can view/edit your manifests and also deploy or delete these resources for your cluster directly from K9s. Just like `kubectl` you can apply/delete an entire directory or a single manifest.
How cool is that?
## Resolved Bugs/Features/PRs
@ -27,7 +38,8 @@ Maintenance Release!
* [Issue #774](https://github.com/derailed/k9s/issues/774)
* [Issue #761](https://github.com/derailed/k9s/issues/761)
* [Issue #759](https://github.com/derailed/k9s/issues/759)
* [Issue #756](https://github.com/derailed/k9s/issues/756)
* [Issue #758](https://github.com/derailed/k9s/issues/758)
* [PR #746](https://github.com/derailed/k9s/pull/746) Big Thanks to [Groselt](https://github.com/groselt)!
---

6
go.mod
View File

@ -5,6 +5,7 @@ go 1.14
require (
9fans.net/go v0.0.2
github.com/atotto/clipboard v0.1.2
github.com/coreos/etcd v3.3.10+incompatible
github.com/derailed/popeye v0.8.6
github.com/derailed/tview v0.3.10
github.com/drone/envsubst v1.0.2 // indirect
@ -13,7 +14,9 @@ require (
github.com/gdamore/tcell v1.3.0
github.com/ghodss/yaml v1.0.0
github.com/golang/protobuf v1.4.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/kylelemons/godebug v1.1.0
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
github.com/mattn/go-isatty v0.0.11
github.com/mattn/go-runewidth v0.0.9
github.com/openfaas/faas v0.0.0-20200207215241-6afae214e3ec
github.com/openfaas/faas-cli v0.0.0-20200124160744-30b7cec9634c
@ -30,6 +33,7 @@ require (
golang.org/x/text v0.3.2
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587 // indirect
google.golang.org/grpc v1.29.1 // indirect
gopkg.in/fsnotify.v1 v1.4.7
gopkg.in/yaml.v2 v2.2.8
helm.sh/helm/v3 v3.2.0
k8s.io/api v0.18.2

5
go.sum
View File

@ -114,15 +114,18 @@ github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@ -415,6 +418,8 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=

View File

@ -144,6 +144,7 @@ func (a *Aliases) loadDefaultAliases() {
a.declare("aliases", "alias", "a")
a.declare("popeye", "pop")
a.declare("helm", "charts", "chart", "hm")
a.declare("dir", "d")
a.declare("contexts", "context", "ctx")
a.declare("users", "user", "usr")
a.declare("groups", "group", "grp")

62
internal/dao/dir.go Normal file
View File

@ -0,0 +1,62 @@
package dao
import (
"context"
"errors"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"github.com/derailed/k9s/internal/render"
"k8s.io/apimachinery/pkg/runtime"
)
var _ Accessor = (*Dir)(nil)
// Dir tracks standard and custom command aliases.
type Dir struct {
NonResource
}
// NewDir returns a new set of aliases.
func NewDir(f Factory) *Dir {
var a Dir
a.Init(f, client.NewGVR("dir"))
return &a
}
var yamlRX = regexp.MustCompile(`.*\.(yml|yaml)`)
// List returns a collection of aliases.
func (a *Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {
dir, ok := ctx.Value(internal.KeyPath).(string)
if !ok {
return nil, errors.New("No dir in context")
}
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
oo := make([]runtime.Object, 0, len(files))
for _, f := range files {
if strings.HasPrefix(f.Name(), ".") || !f.IsDir() && !yamlRX.MatchString(f.Name()) {
continue
}
oo = append(oo, render.DirRes{
Path: filepath.Join(dir, f.Name()),
Info: f,
})
}
return oo, err
}
// Get fetch a resource.
func (a *Dir) Get(_ context.Context, _ string) (runtime.Object, error) {
return nil, errors.New("NYI!!")
}

View File

@ -51,6 +51,7 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) {
client.NewGVR("popeye"): &Popeye{},
client.NewGVR("sanitizer"): &Popeye{},
client.NewGVR("helm"): &Helm{},
client.NewGVR("dir"): &Dir{},
}
r, ok := m[gvr]
@ -152,6 +153,12 @@ func loadK9s(m ResourceMetas) {
ShortNames: []string{"hz", "pu"},
Categories: []string{"k9s"},
}
m[client.NewGVR("dir")] = metav1.APIResource{
Name: "dir",
Kind: "Dir",
SingularName: "dir",
Categories: []string{"k9s"},
}
m[client.NewGVR("xrays")] = metav1.APIResource{
Name: "xray",
Kind: "XRays",
@ -176,7 +183,7 @@ func loadK9s(m ResourceMetas) {
Name: "popeye",
Kind: "Popeye",
SingularName: "popeye",
Namespaced: true,
Namespaced: true,
Verbs: []string{},
Categories: []string{"k9s"},
}

View File

@ -14,6 +14,10 @@ var Registry = map[string]ResourceMeta{
DAO: &dao.Reference{},
Renderer: &render.Reference{},
},
"dir": {
DAO: &dao.Dir{},
Renderer: &render.Dir{},
},
"helm": {
DAO: &dao.Helm{},
Renderer: &render.Helm{},

64
internal/render/dir.go Normal file
View File

@ -0,0 +1,64 @@
package render
import (
"fmt"
"os"
"github.com/gdamore/tcell"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Dir renders a directory entry to screen.
type Dir struct{}
// ColorerFunc colors a resource row.
func (Dir) ColorerFunc() ColorerFunc {
return func(ns string, _ Header, re RowEvent) tcell.Color {
return tcell.ColorCadetBlue
}
}
// Header returns a header row.
func (Dir) Header(ns string) Header {
return Header{
HeaderColumn{Name: "NAME"},
}
}
// Render renders a K8s resource to screen.
// BOZO!! Pass in a row with pre-alloc fields??
func (Dir) Render(o interface{}, ns string, r *Row) error {
d, ok := o.(DirRes)
if !ok {
return fmt.Errorf("expected DirRes, but got %T", o)
}
name := "🦄 "
if d.Info.IsDir() {
name = "📁 "
}
name += d.Info.Name()
r.ID, r.Fields = d.Path, append(r.Fields, name)
return nil
}
// ----------------------------------------------------------------------------
// Helpers...
// DirRes represents an alias resource.
type DirRes struct {
Info os.FileInfo
Path string
}
// GetObjectKind returns a schema object.
func (DirRes) GetObjectKind() schema.ObjectKind {
return nil
}
// DeepCopyObject returns a container copy.
func (d DirRes) DeepCopyObject() runtime.Object {
return d
}

View File

@ -58,6 +58,10 @@ func (a *Alias) bindKeys(aa ui.KeyActions) {
}
func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.GetTable().CmdBuff().IsActive() {
return a.GetTable().activateCmd(evt)
}
r, _ := a.GetTable().GetSelection()
if r != 0 {
s := ui.TrimCell(a.GetTable().SelectTable, r, 1)
@ -68,8 +72,5 @@ func (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
return nil
}
if a.GetTable().CmdBuff().IsActive() {
return a.GetTable().activateCmd(evt)
}
return evt
}

View File

@ -523,6 +523,24 @@ func (a *App) meowCmd(msg string) {
}
}
func (a *App) dirCmd(path string) error {
log.Debug().Msgf("DIR PATH %q", path)
_, err := os.Stat(path)
if err != nil {
return err
}
if path == "." {
dir, err := os.Getwd()
if err == nil {
path = dir
}
}
a.Content.Stack.Clear()
a.cmdHistory.Push("dir " + path)
return a.inject(NewDir(path))
}
func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey {
if a.CmdBuff().InCmdMode() {
return evt

View File

@ -124,6 +124,11 @@ func (c *Command) run(cmd, path string, clearStack bool) error {
return useContext(c.app, cmds[1])
}
return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack)
case "dir":
if len(cmds) != 2 {
return errors.New("You must specify a directory")
}
return c.app.dirCmd(cmds[1])
default:
// checks if Command includes a namespace
ns := c.app.Config.ActiveNamespace()

203
internal/view/dir.go Normal file
View File

@ -0,0 +1,203 @@
package view
import (
"context"
"errors"
"fmt"
"io/ioutil"
"path"
"github.com/derailed/k9s/internal"
"github.com/derailed/k9s/internal/client"
"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"
)
// Dir represents a command directory view.
type Dir struct {
ResourceViewer
path string
}
func NewDir(path string) ResourceViewer {
d := Dir{
ResourceViewer: NewBrowser(client.NewGVR("dir")),
path: path,
}
d.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue)
d.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone)
d.SetBindKeysFn(d.bindKeys)
d.SetContextFn(d.dirContext)
d.GetTable().SetColorerFn(render.Dir{}.ColorerFunc())
return &d
}
// Init initializes the view.
func (d *Dir) Init(ctx context.Context) error {
if err := d.ResourceViewer.Init(ctx); err != nil {
return err
}
return nil
}
func (d *Dir) dirContext(ctx context.Context) context.Context {
return context.WithValue(ctx, internal.KeyPath, d.path)
}
func (d *Dir) bindKeys(aa ui.KeyActions) {
aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)
aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ)
aa.Add(ui.KeyActions{
ui.KeyA: ui.NewKeyAction("Apply", d.applyCmd, true),
ui.KeyD: ui.NewKeyAction("Delete", d.delCmd, true),
ui.KeyE: ui.NewKeyAction("Edit", d.editCmd, true),
ui.KeyY: ui.NewKeyAction("YAML", d.viewCmd, true),
tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true),
})
}
func (d *Dir) viewCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := d.GetTable().GetSelectedItem()
if sel == "" {
return evt
}
if path.Ext(sel) == "" {
return nil
}
yaml, err := ioutil.ReadFile(sel)
if err != nil {
d.App().Flash().Err(err)
return nil
}
details := NewDetails(d.App(), "YAML", sel, true).Update(string(yaml))
if err := d.App().inject(details); err != nil {
d.App().Flash().Err(err)
}
return nil
}
func isManifest(s string) bool {
ext := path.Ext(s)
return ext == ".yml" || ext == ".yaml"
}
func (d *Dir) editCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := d.GetTable().GetSelectedItem()
if sel == "" {
return evt
}
log.Debug().Msgf("Selected %q", sel)
if !isManifest(sel) {
d.App().Flash().Errf("you must select a manifest")
return nil
}
d.Stop()
defer d.Start()
if !edit(d.App(), shellOpts{clear: true, args: []string{sel}}) {
d.App().Flash().Err(errors.New("Failed to launch editor"))
}
return nil
}
func (d *Dir) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {
if d.GetTable().CmdBuff().IsActive() {
return d.GetTable().activateCmd(evt)
}
sel := d.GetTable().GetSelectedItem()
if sel == "" {
return evt
}
if isManifest(sel) {
d.App().Flash().Errf("you must select a directory")
return nil
}
v := NewDir(sel)
if err := d.App().inject(v); err != nil {
d.App().Flash().Err(err)
}
return evt
}
func (d *Dir) applyCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := d.GetTable().GetSelectedItem()
if sel == "" {
return evt
}
if !isManifest(sel) {
d.App().Flash().Errf("you must select a manifest")
return nil
}
d.Stop()
defer d.Start()
{
args := make([]string, 0, 10)
args = append(args, "apply")
args = append(args, "-f")
args = append(args, sel)
res, err := runKu(d.App(), shellOpts{clear: false, args: args})
if err != nil {
res = "error:\n " + err.Error()
} else {
res = "message:\n " + res
}
details := NewDetails(d.App(), "Applied Manifest", sel, true).Update(res)
if err := d.App().inject(details); err != nil {
d.App().Flash().Err(err)
}
}
return nil
}
func (d *Dir) delCmd(evt *tcell.EventKey) *tcell.EventKey {
sel := d.GetTable().GetSelectedItem()
if sel == "" {
return evt
}
if !isManifest(sel) {
d.App().Flash().Errf("you must select a manifest")
return nil
}
d.Stop()
defer d.Start()
msg := fmt.Sprintf("Delete resource(s) in manifest %s", sel)
dialog.ShowConfirm(d.App().Content.Pages, "Confirm Delete", msg, func() {
args := make([]string, 0, 10)
args = append(args, "delete")
args = append(args, "-f")
args = append(args, sel)
res, err := runKu(d.App(), shellOpts{clear: false, args: args})
if err != nil {
res = "error:\n " + err.Error() + "\nmessage:\n " + res
} else {
res = "message:\n " + res
}
details := NewDetails(d.App(), "Deleted Manifest", sel, true).Update(res)
if err := d.App().inject(details); err != nil {
d.App().Flash().Err(err)
}
}, func() {})
return nil
}

View File

@ -1,6 +1,7 @@
package view
import (
"bytes"
"context"
"errors"
"fmt"
@ -123,6 +124,49 @@ func execute(opts shellOpts) error {
}
}
func runKu(a *App, opts shellOpts) (string, error) {
bin, err := exec.LookPath("kubectl")
if err != nil {
log.Error().Err(err).Msgf("kubectl command is not in your path")
return "", err
}
var args []string
if u, err := a.Conn().Config().ImpersonateUser(); err == nil {
args = append(args, "--as", u)
}
if g, err := a.Conn().Config().ImpersonateGroups(); err == nil {
args = append(args, "--as-group", g)
}
args = append(args, "--context", a.Config.K9s.CurrentContext)
if cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" {
args = append(args, "--kubeconfig", *cfg)
}
if len(args) > 0 {
opts.args = append(args, opts.args...)
}
opts.binary, opts.background = bin, false
return oneShoot(opts)
}
func oneShoot(opts shellOpts) (string, error) {
if opts.clear {
clearScreen()
}
log.Debug().Msgf("Running command> %s %s", opts.binary, strings.Join(opts.args, " "))
cmd := exec.Command(opts.binary, opts.args...)
var err error
buff := bytes.NewBufferString("")
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, buff, buff
_, _ = cmd.Stdout.Write([]byte(opts.banner))
err = cmd.Run()
log.Debug().Msgf("RES %q", buff)
return strings.Trim(buff.String(), "\n"), err
}
func clearScreen() {
fmt.Print("\033[H\033[2J")
}

View File

@ -239,7 +239,7 @@ func (h *Help) showGeneral() model.MenuHints {
Description: "Toggle Header",
},
{
Mnemonic: "q",
Mnemonic: ":q",
Description: "Quit",
},
{

View File

@ -124,7 +124,7 @@ func (m *Meow) ExtraHints() map[string]string {
}
func (m *Meow) updateTitle() {
m.SetTitle(" Meow! ")
m.SetTitle(" Error ")
}
var cow = []string{

View File

@ -90,7 +90,6 @@ func miscViewers(vv MetaViewers) {
vv[client.NewGVR("sanitizer")] = MetaViewer{
viewerFn: NewSanitizer,
}
}
func appsViewers(vv MetaViewers) {

View File

@ -103,8 +103,8 @@ func (f *Factory) HasSynced(gvr, ns string) (bool, error) {
}
// Get retrieves a given resource.
func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) {
ns, n := namespaced(path)
func (f *Factory) Get(gvr, fqn string, wait bool, sel labels.Selector) (runtime.Object, error) {
ns, n := namespaced(fqn)
inf, err := f.CanForResource(ns, gvr, []string{client.GetVerb})
if err != nil {
return nil, err