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