From 6eca7966fc275d59c08afbfccdd3794133675a71 Mon Sep 17 00:00:00 2001 From: Jarkko Sonninen Date: Wed, 8 Jan 2025 19:29:03 +0200 Subject: [PATCH 1/3] Add file browser --- internal/dao/dir_local.go | 58 ++++++++ internal/dao/dir_remote.go | 87 +++++++++++ internal/dao/registry.go | 14 ++ internal/keys.go | 1 + internal/model/registry.go | 8 ++ internal/render/dir_remote.go | 75 ++++++++++ internal/ui/dialog/transfer.go | 12 +- internal/view/dir_local.go | 167 +++++++++++++++++++++ internal/view/dir_remote.go | 255 +++++++++++++++++++++++++++++++++ internal/view/pod.go | 92 ++++++++++++ 10 files changed, 766 insertions(+), 3 deletions(-) create mode 100644 internal/dao/dir_local.go create mode 100644 internal/dao/dir_remote.go create mode 100644 internal/render/dir_remote.go create mode 100644 internal/view/dir_local.go create mode 100644 internal/view/dir_remote.go diff --git a/internal/dao/dir_local.go b/internal/dao/dir_local.go new file mode 100644 index 0000000000..2b831daa83 --- /dev/null +++ b/internal/dao/dir_local.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "context" + "errors" + "os" + "path/filepath" + + "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) + +// DirLocal tracks standard and custom command aliases. +type DirLocal struct { + NonResource +} + +// NewDirLocal returns a new set of aliases. +func NewDirLocal(f Factory) *Dir { + var a Dir + a.Init(f, client.NewGVR("dirlocal")) + return &a +} + +// List returns a collection of aliases. +func (a *DirLocal) 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 := os.ReadDir(dir) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(files)) + for _, f := range files { + oo = append(oo, render.DirRes{ + Path: filepath.Join(dir, f.Name()), + Entry: f, + }) + } + + return oo, err +} + +// Get fetch a resource. +func (a *DirLocal) Get(_ context.Context, _ string) (runtime.Object, error) { + return nil, errors.New("nyi") +} diff --git a/internal/dao/dir_remote.go b/internal/dao/dir_remote.go new file mode 100644 index 0000000000..ce5f6d22d3 --- /dev/null +++ b/internal/dao/dir_remote.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "context" + "errors" + "path/filepath" + "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 = (*DirRemote)(nil) + +// Dir tracks standard and custom command aliases. +type DirRemote struct { + NonResource +} + +// NewDirRemote returns a new set of aliases. +func NewDirRemote(f Factory) *DirRemote { + var a DirRemote + a.Init(f, client.NewGVR("dirremote")) + return &a +} + +// List returns a collection of aliases. +func (a *DirRemote) 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") + } + + // It would be better to list files here ? + // No access to view/exec.go, though + txt, ok := ctx.Value(internal.KeyContents).(string) + if !ok { + return nil, errors.New("no contents in context") + } + + var err error = nil + lines := strings.Split(txt, "\n") + oo := make([]runtime.Object, 0, len(lines)) + for _, name := range lines { + if len(name) == 0 { + continue + } + name = strings.TrimSuffix(name, "\n") + name = strings.TrimSuffix(name, "\r") + if name == "./" { + continue + } + if name == "../" && dir == "/" { + continue + } + //if strings.HasSuffix(name, "/") { // directory + // do not strip the trailing slash + //name = strings.TrimSuffix(name, "/") + //} + name = strings.TrimSuffix(name, "*") // executable + if strings.HasSuffix(name, "@") { // symlink + continue // kubectl cp ignores symlinks + } + if strings.HasSuffix(name, "|") { // pipe + continue + } + if strings.HasSuffix(name, "=") { // socket + continue + } + oo = append(oo, render.DirRemoteRes{ + Path: filepath.Join(dir, name), + Name: name, + }) + } + + return oo, err +} + +// Get fetch a resource. +func (a *DirRemote) Get(_ context.Context, _ string) (runtime.Object, error) { + return nil, errors.New("nyi") +} diff --git a/internal/dao/registry.go b/internal/dao/registry.go index ac092c88fc..9fd8b53c56 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -73,6 +73,8 @@ func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { client.NewGVR("benchmarks"): &Benchmark{}, client.NewGVR("portforwards"): &PortForward{}, client.NewGVR("dir"): &Dir{}, + client.NewGVR("dirlocal"): &DirLocal{}, + client.NewGVR("dirremote"): &DirRemote{}, client.NewGVR("v1/services"): &Service{}, client.NewGVR("v1/pods"): &Pod{}, client.NewGVR("v1/nodes"): &Node{}, @@ -227,6 +229,18 @@ func loadK9s(m ResourceMetas) { SingularName: "dir", Categories: []string{k9sCat}, } + m[client.NewGVR("dirlocal")] = metav1.APIResource{ + Name: "dirlocal", + Kind: "DirLocal", + SingularName: "dirlocal", + Categories: []string{k9sCat}, + } + m[client.NewGVR("dirremote")] = metav1.APIResource{ + Name: "dirremote", + Kind: "DirRemote", + SingularName: "dirremote", + Categories: []string{k9sCat}, + } m[client.NewGVR("xrays")] = metav1.APIResource{ Name: "xray", Kind: "XRays", diff --git a/internal/keys.go b/internal/keys.go index d18bc11d36..75c8eb31fc 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -36,4 +36,5 @@ const ( KeyWait ContextKey = "wait" KeyPodCounting ContextKey = "podCounting" KeyEnableImgScan ContextKey = "vulScan" + KeyContents ContextKey = "contents" ) diff --git a/internal/model/registry.go b/internal/model/registry.go index e2129c3bff..1f1465974a 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -26,6 +26,14 @@ var Registry = map[string]ResourceMeta{ DAO: &dao.Dir{}, Renderer: &render.Dir{}, }, + "dirlocal": { + DAO: &dao.DirLocal{}, + Renderer: &render.Dir{}, + }, + "dirremote": { + DAO: &dao.DirRemote{}, + Renderer: &render.DirRemote{}, + }, "pulses": { DAO: &dao.Pulse{}, }, diff --git a/internal/render/dir_remote.go b/internal/render/dir_remote.go new file mode 100644 index 0000000000..f24790efea --- /dev/null +++ b/internal/render/dir_remote.go @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "fmt" + "strings" + + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/tcell/v2" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// DirRemote renders a directory entry to screen. +type DirRemote struct{} + +// IsGeneric identifies a generic handler. +func (DirRemote) IsGeneric() bool { + return false +} + +// ColorerFunc colors a resource row. +func (DirRemote) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { + return tcell.ColorCadetBlue + } +} + +// Header returns a header row. +func (DirRemote) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + } +} + +// Render renders a K8s resource to screen. +// BOZO!! Pass in a row with pre-alloc fields?? +func (DirRemote) Render(o interface{}, ns string, r *model1.Row) error { + d, ok := o.(DirRemoteRes) + if !ok { + return fmt.Errorf("expected DirRemoteRes, but got %T", o) + } + var name string + var path = d.Path + if strings.HasSuffix(d.Name, "/") { // directory + name = "📁 " + strings.TrimSuffix(d.Name, "/") + path = path + "/" // was stripped by filepath + } else { + name = "📄 " + d.Name + } + r.ID, r.Fields = path, append(r.Fields, name) + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// DirRes represents an alias resource. +type DirRemoteRes struct { + Name string + Path string +} + +// GetObjectKind returns a schema object. +func (DirRemoteRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (d DirRemoteRes) DeepCopyObject() runtime.Object { + return d +} diff --git a/internal/ui/dialog/transfer.go b/internal/ui/dialog/transfer.go index 3141a8943c..a2eb0ba249 100644 --- a/internal/ui/dialog/transfer.go +++ b/internal/ui/dialog/transfer.go @@ -26,6 +26,8 @@ type TransferDialogOpts struct { Containers []string Pod string Title, Message string + File string + Download bool Retries int Ack TransferFn Cancel cancelFunc @@ -47,11 +49,15 @@ func ShowUploads(styles config.Dialog, pages *ui.Pages, opts TransferDialogOpts) modal := tview.NewModalForm("<"+opts.Title+">", f) args := TransferArgs{ - From: opts.Pod, - Retries: opts.Retries, + Download: opts.Download, + Retries: opts.Retries, + } + if opts.Download { + args.From, args.To = opts.Pod, opts.File + } else { + args.From, args.To = opts.File, opts.Pod } var fromField, toField *tview.InputField - args.Download = true f.AddCheckbox("Download:", args.Download, func(_ string, flag bool) { if flag { modal.SetText(strings.Replace(opts.Message, "Upload", "Download", 1)) diff --git a/internal/view/dir_local.go b/internal/view/dir_local.go new file mode 100644 index 0000000000..7a0e965b38 --- /dev/null +++ b/internal/view/dir_local.go @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "path" + "path/filepath" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/derailed/tcell/v2" +) + +// Dir represents a command directory view. +type DirLocal struct { + ResourceViewer + path string + fqn string + remote_dir string +} + +// NewDirLocal returns a new instance. +func NewDirLocal(path string, fqn string, remote_dir string) ResourceViewer { + d := DirLocal{ + ResourceViewer: NewBrowser(client.NewGVR("dirlocal")), + path: path, + fqn: fqn, + remote_dir: remote_dir, + } + d.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue) + d.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorAliceBlue).Attributes(tcell.AttrNone)) + d.AddBindKeysFn(d.bindKeys) + d.SetContextFn(d.dirContext) + + return &d +} + +// Init initializes the view. +func (d *DirLocal) Init(ctx context.Context) error { + if err := d.ResourceViewer.Init(ctx); err != nil { + return err + } + + return nil +} + +func (d *DirLocal) dirContext(ctx context.Context) context.Context { + return context.WithValue(ctx, internal.KeyPath, d.path) +} + +func (d *DirLocal) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyT: ui.NewKeyActionWithOpts("Transfer", d.transferCmd, ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + }) +} + +func (d *DirLocal) bindKeys(aa *ui.KeyActions) { + // !!BOZO!! Lame! + aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ) + if !d.App().Config.K9s.IsReadOnly() { + d.bindDangerousKeys(aa) + } + aa.Bulk(ui.KeyMap{ + ui.KeyV: ui.NewKeyAction("View", d.viewCmd, true), + tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true), + }) +} + +func (d *DirLocal) viewCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := d.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + if path.Ext(sel) == "" { + return nil + } + + yaml, err := os.ReadFile(sel) + if err != nil { + d.App().Flash().Err(err) + return nil + } + + details := NewDetails(d.App(), yamlAction, sel, contentYAML, true).Update(string(yaml)) + if err := d.App().inject(details, false); err != nil { + d.App().Flash().Err(err) + } + + return nil +} + +func (d *DirLocal) transferCmd(evt *tcell.EventKey) *tcell.EventKey { + sel := d.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + path := d.fqn + ns, n := client.Namespaced(path) + file := d.GetTable().GetSelectedItem() + + pod, err := fetchPod(d.App().factory, path) + if err != nil { + d.App().Flash().Err(err) + return nil + } + + opts := dialog.TransferDialogOpts{ + Title: "Transfer", + Containers: fetchContainers(pod.ObjectMeta, pod.Spec, false), + Message: "Upload Files", + Pod: fmt.Sprintf("%s/%s:%s/%s", ns, n, d.remote_dir, filepath.Base(file)), + File: file, + Ack: makeTransferAck(d.App(), path), + Download: false, + Retries: defaultTxRetries, + Cancel: func() {}, + } + dialog.ShowUploads(d.App().Styles.Dialog(), d.App().Content.Pages, opts) + + return nil +} + +func (d *DirLocal) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { + if d.GetTable().CmdBuff().IsActive() { + return d.GetTable().activateCmd(evt) + } + + sel := d.GetTable().GetSelectedItem() + if sel == "" { + return evt + } + + s, err := os.Stat(sel) + if errors.Is(err, fs.ErrNotExist) { + d.App().Flash().Err(err) + return nil + } + + if !s.IsDir() { // file + return d.transferCmd(evt) + } + + /* + v := NewDirLocal(sel, d.fqn, d.remote_dir) + if err := d.App().inject(v, false); err != nil { + d.App().Flash().Err(err) + } + */ + d.path = sel + d.Start() + + return evt +} diff --git a/internal/view/dir_remote.go b/internal/view/dir_remote.go new file mode 100644 index 0000000000..cae25cce9c --- /dev/null +++ b/internal/view/dir_remote.go @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/derailed/tcell/v2" + "github.com/rs/zerolog/log" +) + +// Dir represents a command directory view. +type DirRemote struct { + ResourceViewer + fqn string + co string + os string + dir string + text string +} + +// NewDirRemote returns a new instance. +func NewDirRemote(fqn, co string, os string, dir string, txt string) ResourceViewer { + + d := DirRemote{ + ResourceViewer: NewBrowser(client.NewGVR("dirremote")), + fqn: fqn, + co: co, + os: os, + dir: dir, + text: txt, + } + d.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue) + d.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorAliceBlue).Attributes(tcell.AttrNone)) + d.AddBindKeysFn(d.bindKeys) + d.SetContextFn(d.dirContext) + + return &d +} + +// Init initializes the view. +func (d *DirRemote) Init(ctx context.Context) error { + if err := d.ResourceViewer.Init(ctx); err != nil { + return err + } + return nil +} + +func (d *DirRemote) dirContext(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeyContents, d.text) + return context.WithValue(ctx, internal.KeyPath, d.dir) +} + +func (d *DirRemote) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyT: ui.NewKeyActionWithOpts("Transfer", d.transferCmd, ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + }) +} + +func (d *DirRemote) bindKeys(aa *ui.KeyActions) { + // !!BOZO!! Lame! + aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) + aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ) + if !d.App().Config.K9s.IsReadOnly() { + d.bindDangerousKeys(aa) + } + // TODO CD command + aa.Bulk(ui.KeyMap{ + tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true), + ui.KeyL: ui.NewKeyAction("Local", d.localCmd, true), + }) +} + +func getCWD(a *App, fqn, co string, os string) (string, error) { + + args := buildShellArgs("exec", fqn, co, a.Conn().Config().Flags().KubeConfig) + if os == windowsOS { + // FIXME implement + // args = append(args, "--", powerShell) + } + + // TODO make configurable + args = append(args, "--", "sh", "-c", "echo $PWD") + res, err := runKu(a, shellOpts{args: args}) + if err != nil { + a.Flash().Errf("Shell exec '%s' failed: %s", strings.Join(args, " "), err) + } + + return res, err +} + +func listRemoteFiles(a *App, fqn, co string, os string, dir string) (string, error) { + + args := buildShellArgs("exec", fqn, co, a.Conn().Config().Flags().KubeConfig) + if os == windowsOS { + // FIXME implement + // args = append(args, "--", powerShell) + } + + // TODO make configurable + args = append(args, "--", "ls", "-1aF", "--color=never") + if dir != "" { + args = append(args, "--", dir) + } + + res, err := runKu(a, shellOpts{args: args}) + if err != nil { + a.Flash().Errf("Shell exec '%s' failed: %s", strings.Join(args, " "), err) + } + + return res, err +} + +func (d *DirRemote) 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 !strings.HasSuffix(sel, "/") { // file + return d.transferCmd(evt) + } + dir := strings.TrimSuffix(sel, "/") + + str, err := listRemoteFiles(d.App(), d.fqn, d.co, d.os, dir) + if err != nil { + //d.App().Flash().Err(err) + return evt + } + + /* + v := NewDirRemote(d.fqn, d.co, d.os, dir, str) + if err := d.App().inject(v, false); err != nil { + d.App().Flash().Err(err) + } + */ + d.text = str + d.dir = dir + d.Start() + + return evt +} + +func makeTransferAck(a *App, path string) func(args dialog.TransferArgs) bool { + return func(args dialog.TransferArgs) bool { + local := args.To + if !args.Download { + local = args.From + } + if _, err := os.Stat(local); !args.Download && errors.Is(err, fs.ErrNotExist) { + a.Flash().Err(err) + return false + } + + opts := make([]string, 0, 10) + opts = append(opts, "cp") + opts = append(opts, strings.TrimSpace(args.From)) + opts = append(opts, strings.TrimSpace(args.To)) + opts = append(opts, fmt.Sprintf("--no-preserve=%t", args.NoPreserve)) + opts = append(opts, fmt.Sprintf("--retries=%d", args.Retries)) + if args.CO != "" { + opts = append(opts, "-c="+args.CO) + } + opts = append(opts, fmt.Sprintf("--retries=%d", args.Retries)) + + cliOpts := shellOpts{ + background: true, + args: opts, + } + op := trUpload + if args.Download { + op = trDownload + } + + fqn := path + ":" + args.CO + if err := runK(a, cliOpts); err != nil { + a.cowCmd(err.Error()) + } else { + a.Flash().Infof("%s successful on %s!", op, fqn) + } + return true + } +} + +func (d *DirRemote) transferCmd(evt *tcell.EventKey) *tcell.EventKey { + path := d.fqn + ns, n := client.Namespaced(path) + file := d.GetTable().GetSelectedItem() + pod, err := fetchPod(d.App().factory, path) + if err != nil { + d.App().Flash().Err(err) + return nil + } + + opts := dialog.TransferDialogOpts{ + Title: "Transfer", + Containers: fetchContainers(pod.ObjectMeta, pod.Spec, false), + Message: "Download Files", + Pod: fmt.Sprintf("%s/%s:%s", ns, n, file), + File: filepath.Base(file), + Ack: makeTransferAck(d.App(), path), + Download: true, + Retries: defaultTxRetries, + Cancel: func() {}, + } + dialog.ShowUploads(d.App().Styles.Dialog(), d.App().Content.Pages, opts) + + return nil +} + +func (d *DirRemote) localCmd(evt *tcell.EventKey) *tcell.EventKey { + path := "." + + log.Debug().Msgf("DIR PATH %q", path) + _, err := os.Stat(path) + if err != nil { + d.App().cowCmd(err.Error()) + return nil + } + if path == "." { + dir, err := os.Getwd() + if err == nil { + path = dir + } + } + d.App().cmdHistory.Push("dir " + path) + + if d.GetTable().CmdBuff().IsActive() { + return d.GetTable().activateCmd(evt) + } + + v := NewDirLocal(path, d.fqn, d.dir) + if err := d.App().inject(v, false); err != nil { + d.App().Flash().Err(err) + } + + return evt +} diff --git a/internal/view/pod.go b/internal/view/pod.go index 879bd6b487..882ea01e86 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -112,6 +112,13 @@ func (p *Pod) bindDangerousKeys(aa *ui.KeyActions) { Visible: true, Dangerous: true, }), + ui.KeyB: ui.NewKeyActionWithOpts( + "Browse", + p.browseCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), ui.KeyZ: ui.NewKeyActionWithOpts( "Sanitize", p.sanitizeCmd, @@ -347,6 +354,7 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { Message: "Download Files", Pod: fmt.Sprintf("%s/%s:", ns, n), Ack: ack, + Download: true, Retries: defaultTxRetries, Cancel: func() {}, } @@ -355,9 +363,93 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } +func (p *Pod) browseCmd(evt *tcell.EventKey) *tcell.EventKey { + path := p.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + if !podIsRunning(p.App().factory, path) { + p.App().Flash().Errf("%s is not in a running state", path) + return nil + } + + if err := containerListFiles(p.App(), p, path, ""); err != nil { + p.App().Flash().Err(err) + } + + return nil +} + // ---------------------------------------------------------------------------- // Helpers... +func containerListFiles(a *App, comp model.Component, path, co string) error { + dir := "" + + if co != "" { + dirRemoteIn(a, comp, path, co, dir) + return nil + } + + pod, err := fetchPod(a.factory, path) + if err != nil { + return err + } + cc := fetchContainers(pod.ObjectMeta, pod.Spec, false) + if len(cc) == 1 { + dirRemoteIn(a, comp, path, cc[0], dir) + return nil + } + + /* + v := NewLiveView(a, "Browse", nil) + if err := a.inject(v, false); err != nil { + a.Flash().Err(err) + } + return err + */ + picker := NewPicker() + picker.populate(cc) + + picker.SetSelectedFunc(func(_ int, co, _ string, _ rune) { + dirRemoteIn(a, comp, path, co, dir) + }) + + return a.inject(picker, false) +} + +func dirRemoteIn(a *App, c model.Component, fqn string, co string, dir string) { + c.Stop() + defer c.Start() + + os, err := getPodOS(a.factory, fqn) + if err != nil { + log.Warn().Err(err).Msgf("os detect failed") + } + + if dir == "" { + dir, err = getCWD(a, fqn, co, os) + if err != nil { + log.Warn().Err(err).Msgf("dir detect failed") + dir = "" + } else { + dir = strings.TrimSpace(dir) + } + } + + str, err := listRemoteFiles(a, fqn, co, os, dir) + if err != nil { + return + } + + details := NewDirRemote(fqn, co, os, dir, str) + err = a.inject(details, false) + if err != nil { + a.Flash().Errf("Remote dir failed: %s", err) + } +} + func containerShellIn(a *App, comp model.Component, path, co string) error { if co != "" { resumeShellIn(a, comp, path, co) From b7212dafbf4395b652a71c05b8b42e49615c2a2d Mon Sep 17 00:00:00 2001 From: Jarkko Sonninen Date: Wed, 8 Jan 2025 19:29:03 +0200 Subject: [PATCH 2/3] Add file browser --- internal/view/container.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/view/container.go b/internal/view/container.go index babcb2b8db..8274713dbd 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -77,6 +77,13 @@ func (c *Container) bindDangerousKeys(aa *ui.KeyActions) { Visible: true, Dangerous: true, }), + ui.KeyB: ui.NewKeyActionWithOpts( + "Browse", + c.browseCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), }) } @@ -204,6 +211,15 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } +func (c *Container) browseCmd(evt *tcell.EventKey) *tcell.EventKey { + co := c.GetTable().GetSelectedItem() + if co == "" { + return evt + } + dirRemoteIn(c.App(), c, c.GetTable().Path, co, "") + return nil +} + func checkRunningStatus(co string, ss []v1.ContainerStatus) error { var cs *v1.ContainerStatus for i := range ss { From 106f551f336c79a839e5cf290097b6d6aaf8a381 Mon Sep 17 00:00:00 2001 From: Jarkko Sonninen Date: Mon, 13 Jan 2025 14:49:46 +0200 Subject: [PATCH 3/3] Add file browser --- internal/view/container_test.go | 2 +- internal/view/help_test.go | 2 +- internal/view/pod_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/view/container_test.go b/internal/view/container_test.go index 84787f6d1b..a96ed7bc92 100644 --- a/internal/view/container_test.go +++ b/internal/view/container_test.go @@ -16,5 +16,5 @@ func TestContainerNew(t *testing.T) { assert.Nil(t, c.Init(makeCtx())) assert.Equal(t, "Containers", c.Name()) - assert.Equal(t, 19, len(c.Hints())) + assert.Equal(t, 20, len(c.Hints())) } diff --git a/internal/view/help_test.go b/internal/view/help_test.go index b6f19c7831..c5ad1aa4df 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -24,7 +24,7 @@ func TestHelp(t *testing.T) { v := view.NewHelp(app) assert.Nil(t, v.Init(ctx)) - assert.Equal(t, 29, v.GetRowCount()) + assert.Equal(t, 30, v.GetRowCount()) assert.Equal(t, 8, v.GetColumnCount()) assert.Equal(t, "", strings.TrimSpace(v.GetCell(1, 0).Text)) assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text)) diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index 23bdebaf74..1414da4d48 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -19,7 +19,7 @@ func TestPodNew(t *testing.T) { assert.Nil(t, po.Init(makeCtx())) assert.Equal(t, "Pods", po.Name()) - assert.Equal(t, 28, len(po.Hints())) + assert.Equal(t, 29, len(po.Hints())) } // Helpers...