From f1111174aa8df815e25b0a029ba1465d3a855b49 Mon Sep 17 00:00:00 2001 From: derailed Date: Mon, 29 Jun 2020 18:47:02 -0600 Subject: [PATCH] add pf indicator --- change_logs/release_v0.20.6.md | 34 +++++++++++++++++++++++ internal/client/client.go | 25 ++++++++--------- internal/client/config.go | 29 ++++++++++++++++++-- internal/config/alias.go | 2 +- internal/config/styles.go | 6 ++--- internal/dao/benchmark.go | 15 ++++++++--- internal/dao/cronjob.go | 9 ++----- internal/dao/generic.go | 7 ++--- internal/dao/pod.go | 2 +- internal/dao/port_forward.go | 12 ++++++--- internal/dao/port_forwarder.go | 12 ++++++--- internal/model/cluster_info.go | 2 +- internal/model/cmd_buff.go | 9 ++++--- internal/model/cmd_buff_test.go | 2 +- internal/model/table_int_test.go | 4 +-- internal/model/table_test.go | 2 +- internal/perf/benchmark.go | 2 -- internal/render/alias.go | 2 +- internal/render/alias_test.go | 6 ++--- internal/render/benchmark.go | 2 +- internal/render/container.go | 2 ++ internal/render/container_test.go | 1 + internal/render/generic.go | 6 ++++- internal/render/pod.go | 2 ++ internal/render/pod_test.go | 8 +++--- internal/render/table_data.go | 5 ++++ internal/render/types.go | 3 +++ internal/ui/app.go | 4 +-- internal/ui/prompt.go | 6 ++--- internal/ui/table.go | 2 +- internal/view/alias.go | 4 +-- internal/view/app.go | 5 ++-- internal/view/browser.go | 6 +++-- internal/view/container.go | 45 +++++++++++++++++++++++++++++-- internal/view/container_test.go | 2 +- internal/view/help_test.go | 2 +- internal/view/node.go | 2 +- internal/view/pf.go | 14 +++++++++- internal/view/pf_dialog.go | 39 ++++++++++++++++++++++----- internal/view/pf_extender.go | 20 +++++++++----- internal/view/pod.go | 41 +++++++++++++++++++++++++++- internal/view/pod_test.go | 2 +- internal/view/restart_extender.go | 3 +-- internal/view/scale_extender.go | 3 +-- internal/watch/factory.go | 21 ++++++++++++++- internal/watch/forwarders.go | 20 +++++++++++++- 46 files changed, 353 insertions(+), 99 deletions(-) create mode 100644 change_logs/release_v0.20.6.md diff --git a/change_logs/release_v0.20.6.md b/change_logs/release_v0.20.6.md new file mode 100644 index 00000000..017e2aa7 --- /dev/null +++ b/change_logs/release_v0.20.6.md @@ -0,0 +1,34 @@ + + +# Release v0.20.6 + +## Notes + +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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## First A Word From Our Sponsors... + +First off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project! + +* [Remo Eichenberger](https://github.com/remoe) +* [Ken Ahrens](https://github.com/kenahrens) + +Maintenance Release! + +## Resolved Bugs/Features/PRs + +* [Issue #778](https://github.com/derailed/k9s/issues/778) +* [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) + +--- + + © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/internal/client/client.go b/internal/client/client.go index 3aa7e548..3728ef7f 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -24,14 +24,10 @@ import ( ) const ( - cacheSize = 100 - cacheExpiry = 5 * time.Minute - cacheMXKey = "metrics" - cacheMXAPIKey = "metricsAPI" - checkConnTimeout = 3 * time.Second - - // CallTimeout represents default api call timeout. - CallTimeout = 5 * time.Second + cacheSize = 100 + cacheExpiry = 5 * time.Minute + cacheMXKey = "metrics" + cacheMXAPIKey = "metricsAPI" ) var supportedMetricsAPIVersions = []string{"v1beta1"} @@ -157,7 +153,7 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) } client, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr) - ctx, cancel := context.WithTimeout(context.Background(), CallTimeout) + ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout()) defer cancel() for _, v := range verbs { sar.Spec.ResourceAttributes.Verb = v @@ -203,7 +199,7 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { if err != nil { return nil, err } - ctx, cancel := context.WithTimeout(context.Background(), CallTimeout) + ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout()) defer cancel() nn, err := dial.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) if err != nil { @@ -235,7 +231,6 @@ func (a *APIClient) CheckConnectivity() bool { a.connOK = false return a.connOK } - cfg.Timeout = checkConnTimeout client, err := kubernetes.NewForConfig(cfg) if err != nil { log.Error().Err(err).Msgf("Unable to connect to api server") @@ -280,7 +275,13 @@ func (a *APIClient) HasMetrics() bool { a.cache.Add(cacheMXKey, flag, cacheExpiry) return flag } - ctx, cancel := context.WithTimeout(context.Background(), CallTimeout) + + timeout, err := time.ParseDuration(*a.config.flags.Timeout) + if err != nil { + log.Error().Err(err).Msgf("parsing duration") + return false + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() if _, err := dial.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{Limit: 1}); err == nil { flag = true diff --git a/internal/client/config.go b/internal/client/config.go index 8c5c4f41..ed6b9b50 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" "sync" + "time" v1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -14,8 +15,10 @@ import ( ) const ( - defaultQPS = 50 - defaultBurst = 50 + defaultQPS = 50 + defaultBurst = 50 + defaultCallTimeoutDuration time.Duration = 5 * time.Second + defaultCallTimeout = "5s" ) // Config tracks a kubernetes configuration. @@ -29,12 +32,34 @@ type Config struct { // NewConfig returns a new k8s config or an error if the flags are invalid. func NewConfig(f *genericclioptions.ConfigFlags) *Config { + timeout := defaultCallTimeout + if f.Timeout == nil { + f.Timeout = &timeout + } else { + _, err := time.ParseDuration(*f.Timeout) + if err != nil { + f.Timeout = &timeout + } + } return &Config{ flags: f, mutex: &sync.RWMutex{}, } } +// CallTimeout returns the call timeout if set or the default if not set. +func (c *Config) CallTimeout() time.Duration { + if c.flags.Timeout == nil { + return defaultCallTimeoutDuration + } + dur, err := time.ParseDuration(*c.flags.Timeout) + if err != nil { + return defaultCallTimeoutDuration + } + + return dur +} + // Flags returns configuration flags. func (c *Config) Flags() *genericclioptions.ConfigFlags { return c.flags diff --git a/internal/config/alias.go b/internal/config/alias.go index d7cab9d7..04797e3e 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -148,7 +148,7 @@ func (a *Aliases) loadDefaultAliases() { a.declare("users", "user", "usr") a.declare("groups", "group", "grp") a.declare("portforwards", "portforward", "pf") - a.declare("benchmarks", "benchmark", "be") + a.declare("benchmarks", "bench", "benchmark", "be") a.declare("screendumps", "screendump", "sd") a.declare("pulses", "pulse", "pu", "hz") a.declare("xrays", "xray", "x") diff --git a/internal/config/styles.go b/internal/config/styles.go index 067c1876..78841dfe 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -215,9 +215,9 @@ func newStyle() Style { func newCharts() Charts { return Charts{ - BgColor: "default", - DialBgColor: "default", - ChartBgColor: "default", + BgColor: "black", + DialBgColor: "black", + ChartBgColor: "black", DefaultDialColors: Colors{Color("palegreen"), Color("orangered")}, DefaultChartColors: Colors{Color("palegreen"), Color("orangered")}, ResourceColors: map[string]Colors{ diff --git a/internal/dao/benchmark.go b/internal/dao/benchmark.go index 12efddf8..e5a863a8 100644 --- a/internal/dao/benchmark.go +++ b/internal/dao/benchmark.go @@ -6,9 +6,11 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/runtime" ) @@ -38,16 +40,23 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error if !ok { return nil, errors.New("no benchmark dir found in context") } + path, _ := ctx.Value(internal.KeyPath).(string) ff, err := ioutil.ReadDir(dir) if err != nil { return nil, err } - oo := make([]runtime.Object, len(ff)) - for i, f := range ff { - oo[i] = render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())} + oo := make([]runtime.Object, 0, len(ff)) + for _, f := range ff { + log.Debug().Msgf("BENCH-LIST %q::%q", strings.Replace(path, "/", "_", 1), f.Name()) + if path != "" && !strings.HasPrefix(f.Name(), strings.Replace(path, "/", "_", 1)) { + log.Debug().Msgf(" SKIP...") + continue + } + oo = append(oo, render.BenchInfo{File: f, Path: filepath.Join(dir, f.Name())}) } + log.Debug().Msgf("BENCH-FILES %#v", oo) return oo, nil } diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go index 81a1a54b..4a5829c8 100644 --- a/internal/dao/cronjob.go +++ b/internal/dao/cronjob.go @@ -39,26 +39,19 @@ func (c *CronJob) Run(path string) error { return fmt.Errorf("user is not authorize to run cronjobs") } - // BOZO!! Factory resource?? - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) - defer cancel() o, err := c.Factory.Get("batch/v1beta1/cronjobs", path, true, labels.Everything()) if err != nil { - return err } - var cj batchv1beta1.CronJob err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj) if err != nil { return errors.New("expecting CronJob resource") } - var jobName = cj.Name if len(cj.Name) >= maxJobNameSize { jobName = cj.Name[0:maxJobNameSize] } - job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: jobName + "-manual-" + rand.String(3), @@ -71,6 +64,8 @@ func (c *CronJob) Run(path string) error { if err != nil { return err } + ctx, cancel := context.WithTimeout(context.Background(), c.Client().Config().CallTimeout()) + defer cancel() _, err = dial.BatchV1().Jobs(ns).Create(ctx, job, metav1.CreateOptions{}) return err diff --git a/internal/dao/generic.go b/internal/dao/generic.go index e04e60f8..1fb96878 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -118,14 +118,15 @@ func (g *Generic) Delete(path string, cascade, force bool) error { PropagationPolicy: &p, GracePeriodSeconds: grace, } - // BOZO!! Move to caller! - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) - defer cancel() dial, err := g.dynClient() if err != nil { return err } + // BOZO!! Move to caller! + ctx, cancel := context.WithTimeout(context.Background(), g.Client().Config().CallTimeout()) + defer cancel() + if client.IsClusterScoped(ns) { return dial.Delete(ctx, n, opts) } diff --git a/internal/dao/pod.go b/internal/dao/pod.go index a68bf580..79511b65 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -201,7 +201,7 @@ func (p *Pod) TailLogs(ctx context.Context, c LogChan, opts LogOptions) error { for _, co := range po.Spec.InitContainers { log.Debug().Msgf("Tailing INIT-CO %q", co.Name) opts.Container = co.Name - if err := p.TailLogs(ctx, c, opts); err != nil { + if err := tailLogs(ctx, p, c, opts); err != nil { return err } tailed = true diff --git a/internal/dao/port_forward.go b/internal/dao/port_forward.go index e88f7969..07d13af6 100644 --- a/internal/dao/port_forward.go +++ b/internal/dao/port_forward.go @@ -40,21 +40,25 @@ func (p *PortForward) Delete(path string, cascade, force bool) error { return nil } -// List returns a collection of screen dumps. +// List returns a collection of port forwards. func (p *PortForward) List(ctx context.Context, _ string) ([]runtime.Object, error) { benchFile, ok := ctx.Value(internal.KeyBenchCfg).(string) if !ok { return nil, fmt.Errorf("no bench file found in context") } + path, _ := ctx.Value(internal.KeyPath).(string) config, err := config.NewBench(benchFile) if err != nil { log.Debug().Msgf("No custom benchmark config file found") } - cc := config.Benchmarks.Containers - oo := make([]runtime.Object, 0, len(p.Factory.Forwarders())) - for k, f := range p.Factory.Forwarders() { + ff, cc := p.Factory.Forwarders(), config.Benchmarks.Containers + oo := make([]runtime.Object, 0, len(ff)) + for k, f := range ff { + if !strings.HasPrefix(k, path) { + continue + } cfg := render.BenchCfg{ C: config.Benchmarks.Defaults.C, N: config.Benchmarks.Defaults.N, diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index 12316cea..4fc12fc1 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -103,8 +103,14 @@ func (p *PortForwarder) HasPortMapping(m string) bool { } // Start initiates a port forward session for a given pod and ports. -func (p *PortForwarder) Start(path, co string, t client.PortTunnel) (*portforward.PortForwarder, error) { - fwds := []string{t.PortMap()} +func (p *PortForwarder) Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error) { + if len(tt) == 0 { + return nil, fmt.Errorf("no ports assigned") + } + fwds := make([]string, 0, len(tt)) + for _, t := range tt { + fwds = append(fwds, t.PortMap()) + } p.path, p.container, p.ports, p.age = path, co, fwds, time.Now() ns, n := client.Namespaced(path) @@ -152,7 +158,7 @@ func (p *PortForwarder) Start(path, co string, t client.PortTunnel) (*portforwar Name(n). SubResource("portforward") - return p.forwardPorts("POST", req.URL(), t.Address, fwds) + return p.forwardPorts("POST", req.URL(), tt[0].Address, fwds) } func (p *PortForwarder) forwardPorts(method string, url *url.URL, address string, ports []string) (*portforward.PortForwarder, error) { diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index f654a2f3..b013b322 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -92,7 +92,7 @@ func (c *ClusterInfo) Refresh() { data.K9sVer = c.version data.K8sVer = c.cluster.Version() - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + ctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout()) defer cancel() var mx client.ClusterMetrics if err := c.cluster.Metrics(ctx, &mx); err == nil { diff --git a/internal/model/cmd_buff.go b/internal/model/cmd_buff.go index f2288eb8..4ab8011c 100644 --- a/internal/model/cmd_buff.go +++ b/internal/model/cmd_buff.go @@ -83,15 +83,16 @@ func (c *CmdBuff) Delete() { } // ClearText clears out command buffer. -func (c *CmdBuff) ClearText() { +func (c *CmdBuff) ClearText(fire bool) { c.buff = make([]rune, 0, maxBuff) - c.fireBufferChanged() + if fire { + c.fireBufferChanged() + } } // Reset clears out the command buffer and deactivates it. func (c *CmdBuff) Reset() { - c.ClearText() - c.fireBufferChanged() + c.ClearText(true) c.SetActive(false) } diff --git a/internal/model/cmd_buff_test.go b/internal/model/cmd_buff_test.go index 34719773..19c3874f 100644 --- a/internal/model/cmd_buff_test.go +++ b/internal/model/cmd_buff_test.go @@ -62,7 +62,7 @@ func TestCmdBuffChanged(t *testing.T) { assert.Equal(t, "", b.GetText()) b.Add('c') - b.ClearText() + b.ClearText(true) assert.Equal(t, 0, l.act) assert.Equal(t, 0, l.inact) assert.Equal(t, "", l.text) diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index 78adfcf5..369538ae 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -33,7 +33,7 @@ func TestTableReconcile(t *testing.T) { err := ta.reconcile(ctx) assert.Nil(t, err) data := ta.Peek() - assert.Equal(t, 17, len(data.Header)) + assert.Equal(t, 18, len(data.Header)) assert.Equal(t, 1, len(data.RowEvents)) assert.Equal(t, client.NamespaceAll, data.Namespace) } @@ -106,7 +106,7 @@ func TestTableHydrate(t *testing.T) { assert.Nil(t, hydrate("blee", oo, rr, render.Pod{})) assert.Equal(t, 1, len(rr)) - assert.Equal(t, 17, len(rr[0].Fields)) + assert.Equal(t, 18, len(rr[0].Fields)) } func TestTableGenericHydrate(t *testing.T) { diff --git a/internal/model/table_test.go b/internal/model/table_test.go index a144f684..8e889689 100644 --- a/internal/model/table_test.go +++ b/internal/model/table_test.go @@ -33,7 +33,7 @@ func TestTableRefresh(t *testing.T) { ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) ta.Refresh(ctx) data := ta.Peek() - assert.Equal(t, 17, len(data.Header)) + assert.Equal(t, 18, len(data.Header)) assert.Equal(t, 1, len(data.RowEvents)) assert.Equal(t, client.NamespaceAll, data.Namespace) assert.Equal(t, 1, l.count) diff --git a/internal/perf/benchmark.go b/internal/perf/benchmark.go index 204e5c9f..ed9b8685 100644 --- a/internal/perf/benchmark.go +++ b/internal/perf/benchmark.go @@ -47,7 +47,6 @@ func NewBenchmark(base, version string, cfg config.BenchConfig) (*Benchmark, err } func (b *Benchmark) init(base, version string) error { - log.Debug().Msgf("BENCH-INIT") req, err := http.NewRequest(b.config.HTTP.Method, base, nil) if err != nil { return err @@ -111,7 +110,6 @@ func (b *Benchmark) Canceled() bool { // Run starts a benchmark, func (b *Benchmark) Run(cluster string, done func()) { log.Debug().Msgf("Running benchmark on cluster %s", cluster) - log.Debug().Msgf("BENCH-CFG %#v", b.worker) buff := new(bytes.Buffer) b.worker.Writer = buff // this call will block until the benchmark is complete or timesout. diff --git a/internal/render/alias.go b/internal/render/alias.go index 1a9f0df5..0d488b8a 100644 --- a/internal/render/alias.go +++ b/internal/render/alias.go @@ -16,7 +16,7 @@ type Alias struct{} // ColorerFunc colors a resource row. func (Alias) ColorerFunc() ColorerFunc { return func(ns string, _ Header, re RowEvent) tcell.Color { - return tcell.ColorMediumSpringGreen + return tcell.ColorAliceBlue } } diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go index 5cc99c8e..8d3e0adc 100644 --- a/internal/render/alias_test.go +++ b/internal/render/alias_test.go @@ -25,15 +25,15 @@ func TestAliasColorer(t *testing.T) { "addAll": { ns: client.AllNamespaces, re: render.RowEvent{Kind: render.EventAdd, Row: r}, - e: tcell.ColorMediumSpringGreen}, + e: tcell.ColorAliceBlue}, "deleteAll": { ns: client.AllNamespaces, re: render.RowEvent{Kind: render.EventDelete, Row: r}, - e: tcell.ColorMediumSpringGreen}, + e: tcell.ColorAliceBlue}, "updateAll": { ns: client.AllNamespaces, re: render.RowEvent{Kind: render.EventUpdate, Row: r}, - e: tcell.ColorMediumSpringGreen, + e: tcell.ColorAliceBlue, }, } diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go index 6c884714..ae82684e 100644 --- a/internal/render/benchmark.go +++ b/internal/render/benchmark.go @@ -57,7 +57,7 @@ func (Benchmark) Header(ns string) Header { func (b Benchmark) Render(o interface{}, ns string, r *Row) error { bench, ok := o.(BenchInfo) if !ok { - return fmt.Errorf("expecting benchinfo but got `%T", o) + return fmt.Errorf("No benchmarks available %T", o) } data, err := b.readFile(bench.Path) diff --git a/internal/render/container.go b/internal/render/container.go index c9b0ccc7..2754738b 100644 --- a/internal/render/container.go +++ b/internal/render/container.go @@ -69,6 +69,7 @@ func (c Container) ColorerFunc() ColorerFunc { func (Container) Header(ns string) Header { return Header{ HeaderColumn{Name: "NAME"}, + HeaderColumn{Name: "PF"}, HeaderColumn{Name: "IMAGE"}, HeaderColumn{Name: "READY"}, HeaderColumn{Name: "STATE"}, @@ -103,6 +104,7 @@ func (c Container) Render(o interface{}, name string, r *Row) error { r.ID = co.Container.Name r.Fields = Fields{ co.Container.Name, + "●", co.Container.Image, ready, state, diff --git a/internal/render/container_test.go b/internal/render/container_test.go index 1d14321a..a15e9d38 100644 --- a/internal/render/container_test.go +++ b/internal/render/container_test.go @@ -28,6 +28,7 @@ func TestContainer(t *testing.T) { assert.Equal(t, "fred", r.ID) assert.Equal(t, render.Fields{ "fred", + "●", "img", "false", "Running", diff --git a/internal/render/generic.go b/internal/render/generic.go index eeb35ee2..be9c8702 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -19,7 +19,7 @@ type Generic struct { ageIndex int } -// Happy returns true if resoure is happy, false otherwise +// Happy returns true if resource is happy, false otherwise func (Generic) Happy(ns string, r Row) bool { return true } @@ -79,6 +79,10 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { ageCell = c continue } + if c == nil { + r.Fields = append(r.Fields, Blank) + continue + } r.Fields = append(r.Fields, fmt.Sprintf("%v", c)) } if ageCell != nil { diff --git a/internal/render/pod.go b/internal/render/pod.go index 00a45ba8..98d46a42 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -59,6 +59,7 @@ func (Pod) Header(ns string) Header { return Header{ HeaderColumn{Name: "NAMESPACE"}, HeaderColumn{Name: "NAME"}, + HeaderColumn{Name: "PF"}, HeaderColumn{Name: "READY"}, HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, HeaderColumn{Name: "STATUS"}, @@ -97,6 +98,7 @@ func (p Pod) Render(o interface{}, ns string, r *Row) error { r.Fields = Fields{ po.Namespace, po.ObjectMeta.Name, + "●", strconv.Itoa(cr) + "/" + strconv.Itoa(len(ss)), strconv.Itoa(rc), phase, diff --git a/internal/render/pod_test.go b/internal/render/pod_test.go index 6be5b41f..a9be45c1 100644 --- a/internal/render/pod_test.go +++ b/internal/render/pod_test.go @@ -159,8 +159,8 @@ func TestPodRender(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := render.Fields{"default", "nginx", "1/1", "0", "Running", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"} - assert.Equal(t, e, r.Fields[:14]) + e := render.Fields{"default", "nginx", "●", "1/1", "0", "Running", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"} + assert.Equal(t, e, r.Fields[:15]) } func BenchmarkPodRender(b *testing.B) { @@ -190,8 +190,8 @@ func TestPodInitRender(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := render.Fields{"default", "nginx", "1/1", "0", "Init:0/1", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"} - assert.Equal(t, e, r.Fields[:14]) + e := render.Fields{"default", "nginx", "●", "1/1", "0", "Init:0/1", "10", "10", "10", "14", render.NAValue, "5", "172.17.0.6", "minikube", "BE"} + assert.Equal(t, e, r.Fields[:15]) } // ---------------------------------------------------------------------------- diff --git a/internal/render/table_data.go b/internal/render/table_data.go index b42d537a..e4858fc7 100644 --- a/internal/render/table_data.go +++ b/internal/render/table_data.go @@ -14,6 +14,11 @@ func NewTableData() *TableData { return &TableData{} } +// IndexOfHeader return the index of the header. +func (t *TableData) IndexOfHeader(h string) int { + return t.Header.IndexOf(h, false) +} + // Labelize prints out specific label columns func (t *TableData) Labelize(labels []string) TableData { labelCol := t.Header.IndexOf("LABELS", true) diff --git a/internal/render/types.go b/internal/render/types.go index ef50fc7b..b757a83c 100644 --- a/internal/render/types.go +++ b/internal/render/types.go @@ -26,6 +26,9 @@ const ( // Pending represents a pod pending status. Pending = "Pending" + + // Blank represents no value. + Blank = "" ) const ( diff --git a/internal/ui/app.go b/internal/ui/app.go index 4435ed96..d76bba25 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -188,7 +188,7 @@ func (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey { if !a.CmdBuff().IsActive() { return evt } - a.CmdBuff().ClearText() + a.CmdBuff().ClearText(true) return nil } @@ -198,7 +198,7 @@ func (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } a.ResetPrompt(a.cmdModel) - a.cmdModel.ClearText() + a.cmdModel.ClearText(true) return nil } diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index 29ed48cd..4851447b 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -43,7 +43,7 @@ type PromptModel interface { GetText() string // ClearText clears out model text. - ClearText() + ClearText(fire bool) // Notify notifies all listener of current suggestions. Notify() @@ -135,13 +135,13 @@ func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey { case tcell.KeyRune: p.model.Add(evt.Rune()) case tcell.KeyEscape: - p.model.ClearText() + p.model.ClearText(true) p.model.SetActive(false) case tcell.KeyEnter, tcell.KeyCtrlE: p.model.SetText(p.model.GetText()) p.model.SetActive(false) case tcell.KeyCtrlW, tcell.KeyCtrlU: - p.model.ClearText() + p.model.ClearText(true) case tcell.KeyUp: if s, ok := m.NextSuggestion(); ok { p.suggest(p.model.GetText(), s) diff --git a/internal/ui/table.go b/internal/ui/table.go index fabedc87..62d83f32 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -382,7 +382,7 @@ func (t *Table) filtered(data render.TableData) render.TableData { filtered, err := rxFilter(t.cmdBuff.GetText(), filtered) if err != nil { log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp") - t.cmdBuff.ClearText() + t.cmdBuff.ClearText(true) } return filtered diff --git a/internal/view/alias.go b/internal/view/alias.go index 0fce9e19..6b5b08a3 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -24,8 +24,8 @@ func NewAlias(gvr client.GVR) ResourceViewer { ResourceViewer: NewBrowser(gvr), } a.GetTable().SetColorerFn(render.Alias{}.ColorerFunc()) - a.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) - a.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorMediumSpringGreen, tcell.AttrNone) + a.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue) + a.GetTable().SetSelectedStyle(tcell.ColorWhite, tcell.ColorAliceBlue, tcell.AttrNone) a.SetBindKeysFn(a.bindKeys) a.SetContextFn(a.aliasContext) diff --git a/internal/view/app.go b/internal/view/app.go index a337f088..65cfbf2d 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -29,7 +29,7 @@ var ExitStatus = "" const ( splashDelay = 1 * time.Second - clusterRefresh = 5 * time.Second + clusterRefresh = 15 * time.Second maxConRetry = 15 clusterInfoWidth = 50 clusterInfoPad = 15 @@ -284,6 +284,7 @@ func (a *App) refreshCluster() { } else { a.ClearStatus(true) } + a.factory.ValidatePortForwards() } else { atomic.AddInt32(&a.conRetry, 1) if c != nil { @@ -337,7 +338,7 @@ func (a *App) isValidNS(ns string) (bool, error) { return true, nil } - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + ctx, cancel := context.WithTimeout(context.Background(), a.Conn().Config().CallTimeout()) defer cancel() dial, err := a.Conn().Dial() if err != nil { diff --git a/internal/view/browser.go b/internal/view/browser.go index 3102e833..699e2a39 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -130,7 +130,9 @@ func (b *Browser) Start() { // Stop terminates browser updates. func (b *Browser) Stop() { + log.Debug().Msgf("BRO-STOP %v", b.GVR()) if b.cancelFn != nil { + log.Debug().Msgf("Canceling!!") b.cancelFn() b.cancelFn = nil } @@ -153,7 +155,7 @@ func (b *Browser) BufferActive(state bool, k model.BufferKind) { if state { return } - b.GetModel().Refresh(b.defaultContext()) + b.GetModel().Refresh(b.prepareContext()) b.app.QueueUpdateDraw(func() { b.Update(b.GetModel().Peek()) if b.GetRowCount() > 1 { @@ -242,7 +244,7 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { if !b.CmdBuff().InCmdMode() { - b.CmdBuff().Reset() + b.CmdBuff().ClearText(false) return b.App().PrevCmd(evt) } diff --git a/internal/view/container.go b/internal/view/container.go index d1b51e48..2c9493ec 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -1,10 +1,12 @@ package view import ( + "context" "errors" "fmt" "strings" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" @@ -28,10 +30,24 @@ func NewContainer(gvr client.GVR) ResourceViewer { c.GetTable().SetEnterFn(c.viewLogs) c.GetTable().SetColorerFn(render.Container{}.ColorerFunc()) c.SetBindKeysFn(c.bindKeys) + c.GetTable().SetDecorateFn(c.portForwardIndicator) return &c } +func (c *Container) portForwardIndicator(data render.TableData) render.TableData { + ff := c.App().factory.Forwarders() + + col := data.IndexOfHeader("PF") + for _, re := range data.RowEvents { + if ff.IsContainerForwarded(c.GetTable().Path, re.Row.ID) { + re.Row.Fields[col] = "[orange::b]Ⓕ" + } + } + + return data +} + // Name returns the component name. func (c *Container) Name() string { return containerTitle } @@ -50,6 +66,7 @@ func (c *Container) bindKeys(aa ui.KeyActions) { } aa.Add(ui.KeyActions{ + ui.KeyF: ui.NewKeyAction("Show PortForward", c.showPFCmd, true), ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false), }) @@ -60,7 +77,7 @@ func (c *Container) k9sEnv() Env { path := c.GetTable().GetSelectedItem() row, ok := c.GetTable().GetSelectedRow(path) if !ok { - log.Error().Msgf("unable to locate seleted row for %q", path) + log.Error().Msgf("unable to locate selected row for %q", path) } env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header, row) env["NAMESPACE"], env["POD"] = client.Namespaced(c.GetTable().Path) @@ -79,6 +96,30 @@ func (c *Container) viewLogs(app *App, model ui.Tabular, gvr, path string) { // Handlers... +func (c *Container) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { + path := c.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + if !c.App().factory.Forwarders().IsContainerForwarded(c.GetTable().Path, path) { + c.App().Flash().Errf("no portforwards defined") + return nil + } + pf := NewPortForward(client.NewGVR("portforwards")) + pf.SetContextFn(c.portForwardContext) + if err := c.App().inject(pf); err != nil { + c.App().Flash().Err(err) + } + + return nil +} + +func (c *Container) portForwardContext(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeyBenchCfg, c.App().BenchFile) + return context.WithValue(ctx, internal.KeyPath, c.GetTable().Path) +} + func (c *Container) shellCmd(evt *tcell.EventKey) *tcell.EventKey { sel := c.GetTable().GetSelectedItem() if sel == "" { @@ -151,7 +192,7 @@ func (c *Container) isForwardable(path string) ([]string, bool) { } } if cs == nil { - log.Error().Err(fmt.Errorf("unable to locate container status named %q", path)) + log.Error().Err(fmt.Errorf("unable to locate container status for %q", path)) return nil, false } diff --git a/internal/view/container_test.go b/internal/view/container_test.go index b5027259..161f4cec 100644 --- a/internal/view/container_test.go +++ b/internal/view/container_test.go @@ -13,5 +13,5 @@ func TestContainerNew(t *testing.T) { assert.Nil(t, c.Init(makeCtx())) assert.Equal(t, "Containers", c.Name()) - assert.Equal(t, 17, len(c.Hints())) + assert.Equal(t, 18, len(c.Hints())) } diff --git a/internal/view/help_test.go b/internal/view/help_test.go index e02ac736..dcb07bf1 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -21,7 +21,7 @@ func TestHelp(t *testing.T) { v := view.NewHelp() assert.Nil(t, v.Init(ctx)) - assert.Equal(t, 23, v.GetRowCount()) + assert.Equal(t, 24, 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/node.go b/internal/view/node.go index 0aa0fc44..7f35a58e 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -155,7 +155,7 @@ func (n *Node) yamlCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + ctx, cancel := context.WithTimeout(context.Background(), n.App().Conn().Config().CallTimeout()) defer cancel() sel := n.GetTable().GetSelectedItem() diff --git a/internal/view/pf.go b/internal/view/pf.go index cc89788a..dabe3ccc 100644 --- a/internal/view/pf.go +++ b/internal/view/pf.go @@ -56,13 +56,25 @@ func (p *PortForward) bindKeys(aa ui.KeyActions) { } func (p *PortForward) showBenchCmd(evt *tcell.EventKey) *tcell.EventKey { - if err := p.App().inject(NewBenchmark(client.NewGVR("benchmarks"))); err != nil { + b := NewBenchmark(client.NewGVR("benchmarks")) + b.SetContextFn(p.getContext) + if err := p.App().inject(b); err != nil { p.App().Flash().Err(err) } return nil } +func (p *PortForward) getContext(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeyDir, benchDir(p.App().Config)) + path := p.GetTable().GetSelectedItem() + if path == "" { + return ctx + } + + return context.WithValue(ctx, internal.KeyPath, path) +} + func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { if p.bench != nil { p.App().Status(model.FlashErr, "Benchmark Canceled!") diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index 5d4b3b01..1cc9bf3f 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -13,7 +13,7 @@ import ( const portForwardKey = "portforward" // PortForwardCB represents a port-forward callback function. -type PortForwardCB func(v ResourceViewer, path, co string, mapper client.PortTunnel) +type PortForwardCB func(v ResourceViewer, path, co string, mapper []client.PortTunnel) // ShowPortForwards pops a port forwarding configuration dialog. func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortForwardCB) { @@ -42,12 +42,29 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortFo pages := v.App().Content.Pages f.AddButton("OK", func() { - tunnel := client.PortTunnel{ - Address: address, - LocalPort: p2, - ContainerPort: extractPort(p1), + pp1 := strings.Split(p1, ",") + pp2 := strings.Split(p2, ",") + if len(pp1) == 0 || len(pp1) != len(pp2) { + v.App().Flash().Err(fmt.Errorf("container to local port mismatch")) + return } - okFn(v, path, extractContainer(p1), tunnel) + + for _, p := range pp1 { + if !hasPort(p, ports) { + v.App().Flash().Err(fmt.Errorf("container port must match exposed ports")) + return + } + } + + var tt []client.PortTunnel + for i := range pp1 { + tt = append(tt, client.PortTunnel{ + Address: address, + LocalPort: pp2[i], + ContainerPort: extractPort(pp1[i]), + }) + } + okFn(v, path, extractContainer(pp1[0]), tt) }) f.AddButton("Cancel", func() { DismissPortForwards(v, pages) @@ -73,6 +90,16 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) { // ---------------------------------------------------------------------------- // Helpers... +func hasPort(port string, pp []string) bool { + for _, p := range pp { + if p != port { + return false + } + } + + return true +} + func extractPort(p string) string { rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(╱UDP)?\z`) mm := rx.FindStringSubmatch(p) diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index ad548a66..d4557bae 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -49,6 +49,10 @@ func (p *PortForwardExtender) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { p.App().Flash().Err(err) return nil } + if p.App().factory.Forwarders().IsPodForwarded(pod) { + p.App().Flash().Errf("A PortForward already exist for pod %s", pod) + return nil + } if err := showFwdDialog(p, pod, startFwdCB); err != nil { p.App().Flash().Err(err) } @@ -100,11 +104,13 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward }) } -func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) { - err := tryListenPort(t.Address, t.LocalPort) - if err != nil { - v.App().Flash().Err(err) - return +func startFwdCB(v ResourceViewer, path, co string, tt []client.PortTunnel) { + for _, t := range tt { + err := tryListenPort(t.Address, t.LocalPort) + if err != nil { + v.App().Flash().Err(err) + return + } } if _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, co)); ok { @@ -113,13 +119,13 @@ func startFwdCB(v ResourceViewer, path, co string, t client.PortTunnel) { } pf := dao.NewPortForwarder(v.App().factory) - fwd, err := pf.Start(path, co, t) + fwd, err := pf.Start(path, co, tt) if err != nil { v.App().Flash().Err(err) return } - log.Debug().Msgf(">>> Starting port forward %q %#v", path, t) + log.Debug().Msgf(">>> Starting port forward %q %#v", path, tt) go runForward(v, pf, fwd) } diff --git a/internal/view/pod.go b/internal/view/pod.go index 00146ca0..e8f6f5b2 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -34,10 +34,24 @@ func NewPod(gvr client.GVR) ResourceViewer { p.SetBindKeysFn(p.bindKeys) p.GetTable().SetEnterFn(p.showContainers) p.GetTable().SetColorerFn(render.Pod{}.ColorerFunc()) + p.GetTable().SetDecorateFn(p.portForwardIndicator) return &p } +func (p *Pod) portForwardIndicator(data render.TableData) render.TableData { + ff := p.App().factory.Forwarders() + + col := data.IndexOfHeader("PF") + for _, re := range data.RowEvents { + if ff.IsPodForwarded(re.Row.ID) { + re.Row.Fields[col] = "[orange::b]Ⓕ" + } + } + + return data +} + func (p *Pod) bindDangerousKeys(aa ui.KeyActions) { aa.Add(ui.KeyActions{ tcell.KeyCtrlK: ui.NewKeyAction("Kill", p.killCmd, true), @@ -52,6 +66,7 @@ func (p *Pod) bindKeys(aa ui.KeyActions) { } aa.Add(ui.KeyActions{ + ui.KeyF: ui.NewKeyAction("Show PortForward", p.showPFCmd, true), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(readyCol, true), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd("RESTARTS", false), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(statusCol, true), false), @@ -90,7 +105,31 @@ func (p *Pod) coContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem()) } -// Commands... +// Handlers... + +func (p *Pod) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { + path := p.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + if !p.App().factory.Forwarders().IsPodForwarded(path) { + p.App().Flash().Errf("no portforwards defined") + return nil + } + pf := NewPortForward(client.NewGVR("portforwards")) + pf.SetContextFn(p.portForwardContext) + if err := p.App().inject(pf); err != nil { + p.App().Flash().Err(err) + } + + return nil +} + +func (p *Pod) portForwardContext(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) + return context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem()) +} func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey { sels := p.GetTable().GetSelectedItems() diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index cc1fe784..f33f312b 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -16,7 +16,7 @@ func TestPodNew(t *testing.T) { assert.Nil(t, po.Init(makeCtx())) assert.Equal(t, "Pods", po.Name()) - assert.Equal(t, 22, len(po.Hints())) + assert.Equal(t, 23, len(po.Hints())) } // Helpers... diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index 10f36e1c..0e6a6e42 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" - "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -45,7 +44,7 @@ func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { msg = fmt.Sprintf("Restart %d deployments?", len(paths)) } dialog.ShowConfirm(r.App().Content.Pages, "Confirm Restart", msg, func() { - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + ctx, cancel := context.WithTimeout(context.Background(), r.App().Conn().Config().CallTimeout()) defer cancel() for _, path := range paths { if err := r.restartRollout(ctx, path); err != nil { diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index 1127b3f4..4b1fed87 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" - "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" @@ -75,7 +74,7 @@ func (s *ScaleExtender) makeScaleForm(sel string) *tview.Form { s.App().Flash().Err(err) return } - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + ctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout()) defer cancel() if err := s.scale(ctx, sel, count); err != nil { log.Error().Err(err).Msgf("DP %s scaling failed", sel) diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 58d5f644..53eb8510 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -2,6 +2,7 @@ package watch import ( "fmt" + "strings" "sync" "time" @@ -243,8 +244,11 @@ func (f *Factory) ensureFactory(ns string) (di.DynamicSharedInformerFactory, err func (f *Factory) AddForwarder(pf Forwarder) { f.mx.Lock() defer f.mx.Unlock() - f.forwarders[pf.Path()] = pf + + for k, v := range f.forwarders { + log.Debug().Msgf("%q -- %#v", k, v) + } } // DeleteForwarder deletes portforward for a given container. @@ -266,6 +270,21 @@ func (f *Factory) ForwarderFor(path string) (Forwarder, bool) { f.mx.RLock() defer f.mx.RUnlock() + for k := range f.forwarders { + log.Debug().Msgf("KEY %q::%q", k, path) + } fwd, ok := f.forwarders[path] return fwd, ok } + +// Validate check if pods are still around for portforwards. +func (f *Factory) ValidatePortForwards() { + for k, fwd := range f.forwarders { + tokens := strings.Split(k, ":") + _, err := f.Get("v1/pods", tokens[0], false, labels.Everything()) + if err != nil { + fwd.Stop() + delete(f.forwarders, k) + } + } +} diff --git a/internal/watch/forwarders.go b/internal/watch/forwarders.go index 4ec6dd33..8e6b232d 100644 --- a/internal/watch/forwarders.go +++ b/internal/watch/forwarders.go @@ -11,7 +11,7 @@ import ( // Forwarder represents a port forwarder. type Forwarder interface { // Start starts a port-forward. - Start(path, co string, t client.PortTunnel) (*portforward.PortForwarder, error) + Start(path, co string, tt []client.PortTunnel) (*portforward.PortForwarder, error) // Stop terminates a port forward. Stop() @@ -49,6 +49,24 @@ func NewForwarders() Forwarders { return make(map[string]Forwarder) } +// IsForwarded checks if pod has a forward +func (ff Forwarders) IsPodForwarded(path string) bool { + for k := range ff { + fqn := strings.Split(k, ":") + if fqn[0] == path { + return true + } + } + return false +} + +// IsForwarded checks if pod has a forward +func (ff Forwarders) IsContainerForwarded(path, co string) bool { + _, ok := ff[path+":"+co] + + return ok +} + // DeleteAll stops and delete all port-forwards. func (ff Forwarders) DeleteAll() { for k, f := range ff {