github.com/tilt-dev/tilt@v0.36.0/internal/k8s/portforward.go (about)

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"k8s.io/apimachinery/pkg/util/httpstream"
    13  	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp" // registers gcp auth provider
    14  	"k8s.io/client-go/transport/spdy"
    15  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    16  
    17  	"github.com/tilt-dev/tilt/internal/k8s/portforward"
    18  	"github.com/tilt-dev/tilt/pkg/logger"
    19  
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  type PortForwardClient interface {
    24  	// Creates a new port-forwarder that's bound to the given context's lifecycle.
    25  	// When the context is canceled, the port-forwarder will close.
    26  	CreatePortForwarder(ctx context.Context, namespace Namespace, podID PodID, localPort int, remotePort int, host string) (PortForwarder, error)
    27  }
    28  
    29  type PortForwarder interface {
    30  	// The local port we're listening on.
    31  	LocalPort() int
    32  
    33  	// Addresses that we're listening on.
    34  	Addresses() []string
    35  
    36  	// ReadyCh will be closed by ForwardPorts once the forwarding is successfully set up.
    37  	//
    38  	// ForwardPorts might return an error during initialization before forwarding is successfully set up, in which
    39  	// case this channel will NOT be closed.
    40  	ReadyCh() <-chan struct{}
    41  
    42  	// Listens on the configured port and forward all traffic to the container.
    43  	// Returns when the port-forwarder sees an unrecoverable error or
    44  	// when the context passed at creation is canceled.
    45  	ForwardPorts() error
    46  
    47  	// TODO(nick): If the port forwarder has any problems connecting to the pod,
    48  	// it just logs those as debug logs. I'm not sure that logs are the right API
    49  	// for this -- there are lots of cases (e.g., where you're deliberately
    50  	// restarting the pod) where it's ok if it drops the connection.
    51  	//
    52  	// I suspect what we actually need is a healthcheck/status field for the
    53  	// portforwarder that's exposed as part of the engine.
    54  }
    55  
    56  type portForwarder struct {
    57  	*portforward.PortForwarder
    58  	localPort int
    59  }
    60  
    61  var _ PortForwarder = portForwarder{}
    62  
    63  func (pf portForwarder) LocalPort() int {
    64  	return pf.localPort
    65  }
    66  
    67  func (pf portForwarder) ReadyCh() <-chan struct{} {
    68  	return pf.Ready
    69  }
    70  
    71  func (k *K8sClient) CreatePortForwarder(ctx context.Context, namespace Namespace, podID PodID, optionalLocalPort, remotePort int, host string) (PortForwarder, error) {
    72  	localPort := optionalLocalPort
    73  	if localPort == 0 {
    74  		// preferably, we'd set the localport to 0, and let the underlying function pick a port for us,
    75  		// to avoid the race condition potential of something else grabbing this port between
    76  		// the call to `getAvailablePort` and whenever `portForwarder` actually binds the port.
    77  		// the k8s client supports a local port of 0, and stores the actual local port assigned in a field,
    78  		// but unfortunately does not export that field, so there is no way for the caller to know which
    79  		// local port to talk to.
    80  		var err error
    81  		localPort, err = getAvailablePort()
    82  		if err != nil {
    83  			return nil, errors.Wrap(err, "failed to find an available local port")
    84  		}
    85  	}
    86  
    87  	return k.portForwardClient.CreatePortForwarder(ctx, namespace, podID, localPort, remotePort, host)
    88  }
    89  
    90  type newPodDialerFn func(namespace Namespace, podID PodID) (httpstream.Dialer, error)
    91  
    92  type portForwardClient struct {
    93  	newPodDialer newPodDialerFn
    94  }
    95  
    96  func ProvidePortForwardClient(
    97  	maybeRESTConfig RESTConfigOrError,
    98  	maybeClientset ClientsetOrError) PortForwardClient {
    99  	if maybeRESTConfig.Error != nil {
   100  		return explodingPortForwardClient{error: maybeRESTConfig.Error}
   101  	}
   102  	if maybeClientset.Error != nil {
   103  		return explodingPortForwardClient{error: maybeClientset.Error}
   104  	}
   105  
   106  	config := maybeRESTConfig.Config
   107  	core := maybeClientset.Clientset.CoreV1()
   108  	newPodDialer := newPodDialerFn(func(namespace Namespace, podID PodID) (httpstream.Dialer, error) {
   109  		transport, upgrader, err := spdy.RoundTripperFor(config)
   110  		if err != nil {
   111  			return nil, errors.Wrap(err, "error getting roundtripper")
   112  		}
   113  
   114  		req := core.RESTClient().Post().
   115  			Resource("pods").
   116  			Namespace(namespace.String()).
   117  			Name(podID.String()).
   118  			SubResource("portforward")
   119  
   120  		dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", req.URL())
   121  		if !cmdutil.PortForwardWebsockets.IsDisabled() {
   122  			tunnelingDialer, err := portforward.NewSPDYOverWebsocketDialer(req.URL(), config)
   123  			if err != nil {
   124  				return nil, errors.Wrap(err, "unable to setup a new websocket dialer")
   125  			}
   126  
   127  			dialer = portforward.NewFallbackDialer(tunnelingDialer, dialer, func(err error) bool {
   128  				return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
   129  			})
   130  		}
   131  		return dialer, nil
   132  	})
   133  
   134  	return portForwardClient{
   135  		newPodDialer: newPodDialer,
   136  	}
   137  }
   138  
   139  func (c portForwardClient) CreatePortForwarder(ctx context.Context, namespace Namespace, podID PodID, localPort int, remotePort int, host string) (PortForwarder, error) {
   140  	dialer, err := c.newPodDialer(namespace, podID)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	readyChan := make(chan struct{}, 1)
   145  
   146  	ports := []string{fmt.Sprintf("%d:%d", localPort, remotePort)}
   147  
   148  	var pf *portforward.PortForwarder
   149  
   150  	// The tiltfile evaluator always defaults the empty string.
   151  	//
   152  	// If it's defaulting to localhost, use the default kubernetse logic
   153  	// for binding the portforward.
   154  	w := logger.NewMutexWriter(logger.Get(ctx).Writer(logger.DebugLvl))
   155  	if host == "" || host == "localhost" {
   156  		pf, err = portforward.New(
   157  			dialer,
   158  			ports,
   159  			ctx.Done(),
   160  			readyChan, w, w)
   161  	} else {
   162  		var addresses []string
   163  		addresses, err = getListenableAddresses(host)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  		pf, err = portforward.NewOnAddresses(
   168  			dialer,
   169  			addresses,
   170  			ports,
   171  			ctx.Done(),
   172  			readyChan, w, w)
   173  	}
   174  	if err != nil {
   175  		return nil, errors.Wrap(err, "error forwarding port")
   176  	}
   177  
   178  	return portForwarder{
   179  		PortForwarder: pf,
   180  		localPort:     localPort,
   181  	}, nil
   182  }
   183  
   184  func getListenableAddresses(host string) ([]string, error) {
   185  	// handle IPv6 literals like `[::1]`
   186  	url, err := url.Parse(fmt.Sprintf("http://%s/", host))
   187  	if err != nil {
   188  		return nil, errors.Wrap(err, fmt.Sprintf("invalid host %s", host))
   189  	}
   190  	addresses, err := net.LookupHost(url.Hostname())
   191  	if err != nil {
   192  		return nil, errors.Wrap(err, fmt.Sprintf("failed to look up address for %s", host))
   193  	}
   194  	listenable := make([]string, 0)
   195  	for _, addr := range addresses {
   196  		var l net.Listener
   197  		if ipv6 := strings.Contains(addr, ":"); ipv6 {
   198  			// skip ipv6 addresses that include a zone index
   199  			// see: https://github.com/tilt-dev/tilt/issues/5981
   200  			if hasZoneIndex := strings.Contains(addr, "%"); hasZoneIndex {
   201  				continue
   202  			}
   203  
   204  			l, err = net.Listen("tcp6", fmt.Sprintf("[%s]:0", addr))
   205  		} else {
   206  			l, err = net.Listen("tcp4", fmt.Sprintf("%s:0", addr))
   207  		}
   208  		if err == nil {
   209  			l.Close()
   210  			listenable = append(listenable, addr)
   211  		}
   212  	}
   213  	if len(listenable) == 0 {
   214  		return nil, errors.Errorf("host %s: cannot listen on any resolved addresses: %v", host, addresses)
   215  	}
   216  	return listenable, nil
   217  }
   218  
   219  func getAvailablePort() (int, error) {
   220  	l, err := net.Listen("tcp", ":0")
   221  	if err != nil {
   222  		return 0, err
   223  	}
   224  	defer func() {
   225  		e := l.Close()
   226  		if err == nil {
   227  			err = e
   228  		}
   229  	}()
   230  
   231  	_, p, err := net.SplitHostPort(l.Addr().String())
   232  	if err != nil {
   233  		return 0, err
   234  	}
   235  	port, err := strconv.Atoi(p)
   236  	if err != nil {
   237  		return 0, err
   238  	}
   239  	return port, err
   240  }
   241  
   242  type explodingPortForwardClient struct {
   243  	error error
   244  }
   245  
   246  func (c explodingPortForwardClient) CreatePortForwarder(ctx context.Context, namespace Namespace, podID PodID, localPort int, remotePort int, host string) (PortForwarder, error) {
   247  	return nil, c.error
   248  }