parent
994a6dba65
commit
a637d494bf
2
go.mod
2
go.mod
|
|
@ -157,7 +157,7 @@ require (
|
|||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -97,8 +97,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H
|
|||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/adrg/xdg v0.3.0 h1:BO+k4wFj0IoTolBF1Apn8oZrX3LQrEbBA8+/9vyW9J4=
|
||||
github.com/adrg/xdg v0.3.0/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
|
||||
github.com/adrg/xdg v0.3.4 h1:0BivHfQ0LSGQrFTaEZ0hyQLm/HAidci7m+1cT6wKKdA=
|
||||
github.com/adrg/xdg v0.3.4/go.mod h1:61xAR2VZcggl2St4O9ohF5qCKe08+JDmE4VNzPFQvOQ=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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("<PortForward on %s>", path), f)
|
||||
|
||||
if len(ports) != 0 {
|
||||
modal.SetText("Exposed Ports: " + strings.Join(ports, ","))
|
||||
modal := tview.NewModalForm("<PortForward>", 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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue