Programmatically Kubernetes port forward in Go

05 Dec 2019 · Five minute read · on Gianluca's blog

Along the way I saw at least two different ways to manage Kubernetes clusters from a networking prospective. Some companies configure a VPN inside the Kubernetes Cluster, in this way a developer connected in the VPN can reach pods and services.

It is not mandatory but suggested having a good network segmentation in order to be able to manage what a person connected in the VPN can touch and see. Achieving this level of control is not easy in Kubernetes a lot of the open source CNI plugin does not have this feature at all and I understand why in operations this is evaluated as a safe approach. It is very convenient if close an eye because pods and services are just IPs that you can reach from your laptop and if you configure the VPN to push the Kubernetes DNS you can also resolve them as DNS lookup.

The alternative I saw is to lock everybody out leaves as unique way to interact with a service or a pod the command kubectl port-forward. In this way the authentication and authorization method in Kubernetes allows you to decide who can do port-forwarding on what based on namespace for example. Or at least you can use Kubernetes Audit logs to figure out who did port forwarding if something bad happens.

We tried both ways, I was to one pushing for the first, but we never achieved a good segmentation and at some point I got locked down, sadly as it sound. Anyway I like to automate things and I had to figure out a way to make my scripts to work with this new approach.

I started to dig in the kubectl code because we all know that it is capable of doing the port forwarding. I had some trouble figuring out the right parameters and to make them to work but at the end I did it! So here we are! If I can do it you can do it as well!

The main repository with the code and an example is in github.com/gianarb/kube-port-forward, you can run it there. I am gonna explain it a bit here.

It is a simple CLI that mocks what kubectl port-forward already does but I extrapolated the code needed to do and control a port forwarding. I will write here as soon as the reason about why I did that is open source, I am telling it to you right now STAY TUNED! It will be great!

First of all I used the k8s.io/cli-runtime/pkg/genericclioptions library to configure a stream, we already used in the blog post about writing a CLI that uses the same flags as the kubectl. A stream is a struct used by different kubernetes service when they need to get or print information from a stream, in this case I am using os.Stdout, os.Stdin, os.Stderr for simplicity, but where I do not need to print out the output I use a bytes.Stream like this:

var berr, bout bytes.Buffer
buffErr := bufio.NewWriter(&berr)
buffOut := bufio.NewWriter(&bout)

In order to make this code easy to read I had a structure to request the port forwarding for a pod:

type PortForwardAPodRequest struct {
	// RestConfig is the kubernetes config
	RestConfig *rest.Config
	// Pod is the selected pod for this port forwarding
	Pod v1.Pod
	// LocalPort is the local port that will be selected to expose the PodPort
	LocalPort int
	// PodPort is the target port for the pod
	PodPort int
	// Steams configures where to write or read input from
	Streams genericclioptions.IOStreams
	// StopCh is the channel used to manage the port forward lifecycle
	StopCh <-chan struct{}
	// ReadyCh communicates when the tunnel is ready to receive traffic
	ReadyCh chan struct{}
}

And I wrote the function that actually does the port forward:

func PortForwardAPod(req PortForwardAPodRequest) error {
	path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward",
		req.Pod.Namespace, req.Pod.Name)
	hostIP := strings.TrimLeft(req.RestConfig.Host, "htps:/")

	transport, upgrader, err := spdy.RoundTripperFor(req.RestConfig)
	if err != nil {
		return err
	}

	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, &url.URL{Scheme: "https", Path: path, Host: hostIP})
	fw, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", req.LocalPort, req.PodPort)}, req.StopCh, req.ReadyCh, req.Streams.Out, req.Streams.ErrOut)
	if err != nil {
		return err
	}
	return fw.ForwardPorts()
}

An exercise that I can leave for you is to add Service support to this function, you can open a PR if you like on github.com/gianarb/kube-port-forward.

The Stop and Ready channels are crucial to manage the port forward because as you see in the example it is a blocking operation it means that it will luckily always run inside a goroutine. Those two channels gives you what you need to understand when the port forward is ready to get traffic ReadyCh and you have the capabilities to stop it StopCh.

My example is basic, I am closing the port forwarding when the SIGTERM signal gets notified:

sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
    <-sigs
    fmt.Println("Bye...")
    close(stopCh)
    wg.Done()
}()

I just wait until the readyCh tells me that the connection is up and running

select {
case <-readyCh:
    break
}
println("Port forwarding is ready to get traffic. have fun!")

As soon as I coded this feature I saw that it was gonna be an easy but useful post. I wrote a report with O’Reilly about how to extend Kubernetes, you can find more about Go and Kube there. It is a free PDF.

I hope you enjoyed it and let me know what cool things you are gonna do port-forwarding the universe!

Something weird with this website? Let me know.