Skip to content

Commit 946d4d1

Browse files
committed
add first draft of smart port forwarding
1 parent 870a8a6 commit 946d4d1

4 files changed

Lines changed: 185 additions & 1 deletion

File tree

cmd/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/revolyssup/k8sdebug/pkg"
99
"github.com/revolyssup/k8sdebug/pkg/logs"
10+
"github.com/revolyssup/k8sdebug/pkg/portforward"
1011
"github.com/spf13/cobra"
1112
)
1213

@@ -30,5 +31,6 @@ func main() {
3031
},
3132
}
3233
rootCmd.AddCommand(logs.NewCommand())
34+
rootCmd.AddCommand(portforward.NewCommand())
3335
rootCmd.Execute()
3436
}

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.23.0
55
toolchain go1.23.8
66

77
require (
8+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
89
github.com/spf13/cobra v1.9.1
910
k8s.io/api v0.32.3
1011
k8s.io/apimachinery v0.32.3
@@ -26,15 +27,17 @@ require (
2627
github.com/google/go-cmp v0.6.0 // indirect
2728
github.com/google/gofuzz v1.2.0 // indirect
2829
github.com/google/uuid v1.6.0 // indirect
30+
github.com/gorilla/websocket v1.5.0 // indirect
2931
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3032
github.com/josharian/intern v1.0.0 // indirect
3133
github.com/json-iterator/go v1.1.12 // indirect
3234
github.com/mailru/easyjson v0.7.7 // indirect
35+
github.com/moby/spdystream v0.5.0 // indirect
3336
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
3437
github.com/modern-go/reflect2 v1.0.2 // indirect
3538
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
39+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
3640
github.com/pkg/errors v0.9.1 // indirect
37-
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3841
github.com/spf13/pflag v1.0.6 // indirect
3942
github.com/x448/float16 v0.8.4 // indirect
4043
golang.org/x/net v0.30.0 // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
2+
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
13
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
24
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -33,6 +35,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY
3335
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
3436
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
3537
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
38+
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
39+
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
3640
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3741
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
3842
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -47,13 +51,17 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
4751
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
4852
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
4953
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
54+
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
55+
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
5056
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
5157
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
5258
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
5359
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
5460
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
5561
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
5662
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
63+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
64+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
5765
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
5866
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
5967
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=

pkg/portforward/portforward.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package portforward
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net"
8+
"net/http"
9+
"os"
10+
"os/signal"
11+
"strconv"
12+
"sync"
13+
14+
"github.com/revolyssup/k8sdebug/pkg"
15+
"github.com/spf13/cobra"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/client-go/kubernetes"
18+
"k8s.io/client-go/tools/clientcmd"
19+
"k8s.io/client-go/tools/portforward"
20+
"k8s.io/client-go/transport/spdy"
21+
)
22+
23+
var (
24+
namespace string
25+
typ string
26+
hostport string
27+
)
28+
var labels string
29+
30+
func forwardToPod(hostConn net.Conn, podCon net.Conn) {
31+
//Copy data bidirectionally
32+
go io.Copy(hostConn, podCon)
33+
io.Copy(podCon, hostConn)
34+
}
35+
36+
var connPool []string
37+
38+
var policy string
39+
40+
func getPodConnection(fw forwarder) (net.Conn, error) {
41+
podConn, err := net.Dial("tcp", fmt.Sprintf(":%d", fw.Port()))
42+
if err != nil {
43+
return nil, err
44+
}
45+
return podConn, nil
46+
}
47+
48+
type forwarder interface {
49+
Port() int
50+
}
51+
52+
type roundRobin struct {
53+
connNumber int
54+
}
55+
56+
func (rr *roundRobin) Port() int {
57+
portNum := 8080 + (rr.connNumber % len(connPool))
58+
rr.connNumber++
59+
return portNum
60+
}
61+
func getForwarder(policy string) forwarder {
62+
switch policy {
63+
case "round-robin":
64+
return &roundRobin{}
65+
}
66+
return nil
67+
}
68+
func NewCommand() *cobra.Command {
69+
cmd := &cobra.Command{
70+
Use: "port-forward",
71+
Run: func(cmd *cobra.Command, args []string) {
72+
fw := getForwarder(policy)
73+
if fw == nil {
74+
fmt.Errorf("invalid policy for forwarding traffic\n")
75+
return
76+
}
77+
listener, err := net.Listen("tcp", fmt.Sprintf(":%s", hostport))
78+
if err != nil {
79+
panic(err)
80+
}
81+
defer listener.Close()
82+
ctx, cancel := context.WithCancel(context.Background())
83+
defer cancel()
84+
go func() {
85+
for {
86+
select {
87+
case <-ctx.Done():
88+
return
89+
default:
90+
hostConn, err := listener.Accept()
91+
if err != nil {
92+
fmt.Printf("Accept error: %v", err)
93+
continue
94+
}
95+
podConn, err := getPodConnection(fw)
96+
if err != nil {
97+
fmt.Printf("Pod connection error: %v", err)
98+
continue
99+
}
100+
forwardToPod(hostConn, podConn)
101+
}
102+
103+
}
104+
}()
105+
kubeconfig := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename()
106+
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
107+
if err != nil {
108+
panic(err.Error())
109+
}
110+
cs := kubernetes.NewForConfigOrDie(config)
111+
opts := metav1.ListOptions{}
112+
if labels != "" {
113+
opts.LabelSelector = labels
114+
}
115+
initialList, err := cs.CoreV1().Pods(namespace).List(context.TODO(), opts)
116+
if err != nil {
117+
panic("Exiting runner..." + err.Error())
118+
}
119+
connPool = make([]string, 0)
120+
pods := initialList.Items
121+
fmt.Printf(pkg.ColorLine(fmt.Sprintf("listening on %s using %s policy across %d pods\n", hostport, policy, len(pods)), pkg.ColorGreen))
122+
for i, pod := range pods {
123+
req := cs.CoreV1().RESTClient().Post().
124+
Resource("pods").
125+
Namespace(namespace).
126+
Name(pod.Name).
127+
SubResource("portforward")
128+
transporter, upgrader, err := spdy.RoundTripperFor(config)
129+
if err != nil {
130+
fmt.Println("coudnot open connection for pod", pod.Name)
131+
continue
132+
}
133+
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transporter}, "POST", req.URL())
134+
stopChan := make(chan struct{})
135+
readyChan := make(chan struct{})
136+
hostPort := 8080 + i
137+
hostPortStr := fmt.Sprintf("%s:80", strconv.Itoa(hostPort))
138+
connPool = append(connPool, hostPortStr)
139+
forwarder, err := portforward.New(dialer, []string{hostPortStr}, stopChan, readyChan, os.Stdout, os.Stderr)
140+
if err != nil {
141+
fmt.Println("coudnot forward connection for pod", pod.Name)
142+
continue
143+
}
144+
go func() {
145+
if err := forwarder.ForwardPorts(); err != nil {
146+
// errChan <- fmt.Errorf("port forwarding failed: %v", err)
147+
fmt.Println("coudnot forward connection for pod", pod.Name)
148+
}
149+
}()
150+
}
151+
var wg sync.WaitGroup
152+
sigchan := make(chan os.Signal)
153+
signal.Notify(sigchan, os.Interrupt)
154+
wg.Add(1)
155+
go func() {
156+
<-sigchan
157+
wg.Done()
158+
listener.Close()
159+
}()
160+
wg.Wait()
161+
fmt.Println("Stopping watcher...")
162+
},
163+
}
164+
165+
cmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "Name of the pod")
166+
cmd.PersistentFlags().StringVarP(&typ, "type", "t", "pod", "Name of the pod")
167+
cmd.PersistentFlags().StringVar(&policy, "policy", "round-robin", "policy to use while sending requests")
168+
cmd.Flags().StringVarP(&labels, "labels", "l", "", "list of key value pairs to use as labels while filtering pods.")
169+
cmd.Flags().StringVar(&hostport, "hostport", "3000", "host port on which requests will be sent")
170+
return cmd
171+
}

0 commit comments

Comments
 (0)