| // 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 |
| } |