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 }