blob: 051289372af643f600ef4b90744d53bce4e191ec [file] [log] [blame]
// Package revportforward establishes a reverse port-forward from a kubernetes
// pod to localhost.
//
// Setting up a port-forward from a kubernetes pod is simple:
//
// $ kubectl port-forward mypod 8888:5000
//
// The above will setup a port-forward, i.e. it will listen on port 8888
// locally, forwarding the traffic to 5000 in the pod named "mypod".
//
// What is more involved is setting up a port-forward in the reverse direction,
// which this code does.
//
// Note that for this to work, netcat (nc) must be installed in the pod.
//
// The code works by running netcat (nc) in the pod in listen mode and then
// connects the exec streams to local target address.
//
// This also support having ncrev installed on the pod, a safer version of nc in
// that it checks that there are no other listeners on the given port before
// starting.
package revportforward
import (
"context"
"fmt"
"net"
"os"
"go.skia.org/infra/go/skerr"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/kubectl/pkg/scheme"
)
// ReversePortForward creates reverse port-forwards to pods running in a
// kubernetes cluster.
type ReversePortForward struct {
config *rest.Config
localaddress string
useNcRev bool
}
// New returns a new RevPortForward instance.
//
// kubeconfig - The contents of the kubeconfig file.
// podName - The name of the pod found in the cluster pointed to by the kubeconfig file.
// podPort - The port to forward from within the pod.
// localaddress - The address we want the incoming connection to be forwarded
// to, something like "localhost:22"
func New(kubeconfig []byte, localaddress string, useNcRev bool) (*ReversePortForward, error) {
var kubeConfigGetter clientcmd.KubeconfigGetter = func() (*api.Config, error) {
return clientcmd.Load(kubeconfig)
}
config, err := clientcmd.BuildConfigFromKubeconfigGetter("", kubeConfigGetter)
if err != nil {
return nil, skerr.Wrapf(err, "Failed to initialize from kubeconfig: %s", string(kubeconfig))
}
return &ReversePortForward{
config: config,
localaddress: localaddress,
useNcRev: useNcRev,
}, nil
}
// Start a reverse port-forward. This function does not return as long as an
// active connection exists.
//
// Note that as connections are made and then closed this function may return,
// so it should be called from within a loop, e.g.:
//
// for {
// if err := rpf.Start(ctx); err != nil {
// sklog.Error(err)
// }
// }
func (r *ReversePortForward) Start(ctx context.Context, podName string, podPort int) error {
fmt.Println("Begin")
// First start a connection to the localaddress.
var d net.Dialer
// If the Context is cancelled then this connection should close, which
// should cause exec.Stream() below to exit, in theory, but it probably
// won't: https://github.com/kubernetes/client-go/issues/554
conn, err := d.DialContext(ctx, "tcp", r.localaddress)
if err != nil {
return skerr.Wrapf(err, "Failed to connect to localaddress: %s", r.localaddress)
}
// Then execute netcat (nc) on the pod.
clientset, err := kubernetes.NewForConfig(r.config)
if err != nil {
panic(err)
}
cmd := []string{
"sh",
"-c",
fmt.Sprintf("nc -vv -l -p %d", podPort),
}
if r.useNcRev {
hostname, _ := os.Hostname() // If hostname errs then use the empty string.
cmd = []string{
"sh",
"-c",
fmt.Sprintf("ncrev --port=:%d --machine=%s", podPort, hostname),
}
}
req := clientset.CoreV1().RESTClient().Post().Resource("pods").Name(podName).Namespace("default").SubResource("exec")
option := &v1.PodExecOptions{
Command: cmd,
Stdin: true,
Stdout: true,
Stderr: false,
TTY: false,
}
req.VersionedParams(
option,
scheme.ParameterCodec,
)
exec, err := remotecommand.NewSPDYExecutor(r.config, "POST", req.URL())
if err != nil {
return skerr.Wrapf(err, "Failed to run netcat on the pod: %q", podName)
}
// exec.Stream will not return until the connection is broken.
err = exec.Stream(remotecommand.StreamOptions{
Stdin: conn,
Stdout: conn,
})
if err != nil {
return skerr.Wrapf(err, "Stream connection failed.")
}
return nil
}