diff --git a/internal/view/container.go b/internal/view/container.go index b51bd55a..33023d46 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -182,7 +182,7 @@ func (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { if !ok { return nil } - ShowPortForwards(c, c.GetTable().Path, ports, startFwdCB) + ShowPortForwards(c, c.GetTable().Path, ports, "", startFwdCB) return nil } @@ -193,8 +193,8 @@ func (c *Container) isForwardable(path string) ([]string, bool) { return nil, false } - cc := po.Spec.Containers var co *v1.Container + cc := po.Spec.Containers for i := range cc { if cc[i].Name == path { co = &cc[i] @@ -230,6 +230,14 @@ func (c *Container) isForwardable(path string) ([]string, bool) { } pp := make([]string, 0, len(ports)) + container, port, ok := parsePFAnn(po.Annotations[AnnDefaultPF]) + if ok && container == path { + if index := indexOfPort(ports, port); index != -1 { + pp = append(pp, path+"/"+port) + ports = append(ports[:index], ports[index+1:]...) + } + } + for _, p := range ports { if !isTCPPort(p) { continue @@ -243,3 +251,16 @@ func (c *Container) isForwardable(path string) ([]string, bool) { return pp, true } + +func indexOfPort(pp []string, port string) int { + for i, p := range pp { + tokens := strings.Split(p, ":") + if len(tokens) == 2 { + if tokens[0] == port || tokens[1] == port { + return i + } + } + } + + return -1 +} diff --git a/internal/view/helpers.go b/internal/view/helpers.go index fcd5c7c1..42a972ad 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -17,6 +17,15 @@ import ( "github.com/rs/zerolog/log" ) +func parsePFAnn(s string) (string, string, bool) { + tokens := strings.Split(s, ":") + if len(tokens) != 2 { + return "", "", false + } + + return tokens[0], tokens[1], true +} + func k8sEnv(c *client.Config) Env { ctx, err := c.CurrentContextName() if err != nil { diff --git a/internal/view/helpers_test.go b/internal/view/helpers_test.go index 26aa6aa1..4827e418 100644 --- a/internal/view/helpers_test.go +++ b/internal/view/helpers_test.go @@ -19,6 +19,41 @@ func init() { zerolog.SetGlobalLevel(zerolog.Disabled) } +func TestParsePFAnn(t *testing.T) { + uu := map[string]struct { + ann, co, port string + ok bool + }{ + "named-port": { + ann: "fred:blee", + co: "fred", + port: "blee", + ok: true, + }, + "port-num": { + ann: "fred:1234", + co: "fred", + port: "1234", + ok: true, + }, + "toast": { + ann: "zorg", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + co, port, ok := parsePFAnn(u.ann) + if u.ok { + assert.Equal(t, u.co, co) + assert.Equal(t, u.port, port) + assert.Equal(t, u.ok, ok) + } + }) + } +} + func TestExtractApp(t *testing.T) { app := NewApp(config.NewConfig(nil)) diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index ae00dc9a..e84e0aee 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -2,6 +2,7 @@ package view import ( "fmt" + "math" "regexp" "strings" @@ -17,7 +18,7 @@ const portForwardKey = "portforward" 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) { +func ShowPortForwards(v ResourceViewer, path string, ports []string, ann string, okFn PortForwardCB) { styles := v.App().Styles.Dialog() f := tview.NewForm() @@ -34,15 +35,35 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortFo var p1, p2 string if len(ports) > 0 { p1, p2 = ports[0], extractPort(ports[0]) + if len(ann) != 0 { + container, port, ok := parsePFAnn(ann) + if ok { + for _, p := range ports { + co, po, portNum := parsePort(p) + if co == container && port == po || port == portNum { + p1, p2 = p, extractPort(p) + break + } + } + } + } } - - f.AddInputField("Container Port:", p1, 30, nil, func(p string) { + fieldLen := int(math.Max(30, float64(len(p1)))) + f.AddInputField("Container Port:", p1, fieldLen, nil, func(p string) { p1 = p }) - f.AddInputField("Local Port:", p2, 30, nil, func(p string) { + field := f.GetFormItemByLabel("Container Port:").(*tview.InputField) + if field.GetText() == "" { + field.SetPlaceholder("Enter a container name/port") + } + f.AddInputField("Local Port:", p2, fieldLen, nil, func(p string) { p2 = p }) - f.AddInputField("Address:", address, 30, nil, func(h string) { + field = f.GetFormItemByLabel("Local Port:").(*tview.InputField) + if field.GetText() == "" { + field.SetPlaceholder("Enter a local port") + } + f.AddInputField("Address:", address, fieldLen, nil, func(h string) { address = h }) for i := 0; i < 3; i++ { @@ -84,12 +105,12 @@ func ShowPortForwards(v ResourceViewer, path string, ports []string, okFn PortFo b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) } - modal := tview.NewModalForm(fmt.Sprintf("", path), f) - - if len(ports) != 0 { - modal.SetText("Exposed Ports: " + strings.Join(ports, ",")) + modal := tview.NewModalForm("", f) + msg := path + if len(ports) > 1 { + msg += "\n\nExposed Ports:\n" + strings.Join(ports, "\n") } - + modal.SetText(msg) modal.SetTextColor(styles.FgColor.Color()) modal.SetBackgroundColor(styles.BgColor.Color()) modal.SetDoneFunc(func(_ int, b string) { @@ -110,6 +131,16 @@ func DismissPortForwards(v ResourceViewer, p *ui.Pages) { // ---------------------------------------------------------------------------- // Helpers... +func parsePort(p string) (string, string, string) { + rx := regexp.MustCompile(`\A([\w|-]+)/?([\w|-]+)?:?(\d+)?(╱UDP)?\z`) + mm := rx.FindStringSubmatch(p) + if len(mm) != 5 { + return "", "", "" + } + + return mm[1], mm[2], mm[3] +} + 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 111910ac..3740d585 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -19,6 +19,8 @@ import ( "k8s.io/client-go/tools/portforward" ) +const AnnDefaultPF = "k9s.imhotep.io/default-portforward-container" + // PortForwardExtender adds port-forward extensions. type PortForwardExtender struct { ResourceViewer @@ -130,7 +132,7 @@ func startFwdCB(v ResourceViewer, path, co string, tt []client.PortTunnel) { } func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { - mm, err := fetchPodPorts(v.App().factory, path) + mm, coPort, err := fetchPodPorts(v.App().factory, path) if err != nil { return err } @@ -143,22 +145,22 @@ func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { ports = append(ports, client.FQN(co, p.Name)+":"+strconv.Itoa(int(p.ContainerPort))) } } - ShowPortForwards(v, path, ports, cb) + ShowPortForwards(v, path, ports, coPort, cb) return nil } -func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, error) { +func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort, string, error) { log.Debug().Msgf("Fetching ports on pod %q", path) o, err := f.Get("v1/pods", path, true, labels.Everything()) if err != nil { - return nil, err + return nil, "", err } var pod v1.Pod err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod) if err != nil { - return nil, err + return nil, "", err } pp := make(map[string][]v1.ContainerPort) @@ -166,5 +168,5 @@ func fetchPodPorts(f *watch.Factory, path string) (map[string][]v1.ContainerPort pp[co.Name] = co.Ports } - return pp, nil + return pp, pod.Annotations[AnnDefaultPF], nil }