istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/kube/portforwarder.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package kube 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "net" 22 "net/http" 23 "os" 24 "strconv" 25 26 v1 "k8s.io/api/core/v1" 27 "k8s.io/client-go/rest" 28 "k8s.io/client-go/tools/portforward" 29 "k8s.io/client-go/transport/spdy" 30 31 "istio.io/istio/pkg/log" 32 ) 33 34 // PortForwarder manages the forwarding of a single port. 35 type PortForwarder interface { 36 // Start runs this forwarder. 37 Start() error 38 39 // Address returns the local forwarded address. Only valid while the forwarder is running. 40 Address() string 41 42 // Close this forwarder and release an resources. 43 Close() 44 45 // ErrChan returns a channel that returns an error when one is encountered. While Start() may return an initial error, 46 // the port-forward connection may be lost at anytime. The ErrChan can be read to determine if/when the port-forwarding terminates. 47 // This can return nil if the port forwarding stops gracefully. 48 ErrChan() <-chan error 49 50 // WaitForStop blocks until connection closed (e.g. control-C interrupt) 51 WaitForStop() 52 } 53 54 var _ PortForwarder = &forwarder{} 55 56 type forwarder struct { 57 stopCh chan struct{} 58 restConfig *rest.Config 59 podName string 60 ns string 61 localAddress string 62 localPort int 63 podPort int 64 errCh chan error 65 } 66 67 func (f *forwarder) Start() error { 68 f.errCh = make(chan error, 1) 69 readyCh := make(chan struct{}, 1) 70 71 var fw *portforward.PortForwarder 72 go func() { 73 for { 74 select { 75 case <-f.stopCh: 76 return 77 default: 78 } 79 var err error 80 // Build a new port forwarder. 81 fw, err = f.buildK8sPortForwarder(readyCh) 82 if err != nil { 83 f.errCh <- fmt.Errorf("building port forwarded: %v", err) 84 return 85 } 86 if err = fw.ForwardPorts(); err != nil { 87 log.Errorf("port forward failed: %v", err) 88 f.errCh <- fmt.Errorf("port forward: %v", err) 89 return 90 } 91 log.Infof("port forward completed without error") 92 f.errCh <- nil 93 // At this point, either the stopCh has been closed, or port forwarder connection is broken. 94 // the port forwarder should have already been ready before. 95 // No need to notify the ready channel anymore when forwarding again. 96 readyCh = nil 97 } 98 }() 99 100 // We want to block Start() until we have either gotten an error or have started 101 // We may later get an error, but that is handled async. 102 select { 103 case err := <-f.errCh: 104 return fmt.Errorf("failure running port forward process: %v", err) 105 case <-readyCh: 106 p, err := fw.GetPorts() 107 if err != nil { 108 return fmt.Errorf("failed to get ports: %v", err) 109 } 110 if len(p) == 0 { 111 return fmt.Errorf("got no ports") 112 } 113 // Set local port now, as it may have been 0 as input 114 f.localPort = int(p[0].Local) 115 log.Debugf("Port forward established %v -> %v.%v:%v", f.Address(), f.podName, f.podName, f.podPort) 116 // The forwarder is now ready. 117 return nil 118 } 119 } 120 121 func (f *forwarder) Address() string { 122 return net.JoinHostPort(f.localAddress, strconv.Itoa(f.localPort)) 123 } 124 125 func (f *forwarder) Close() { 126 close(f.stopCh) 127 // Closing the stop channel should close anything 128 // opened by f.forwarder.ForwardPorts() 129 } 130 131 func (f *forwarder) ErrChan() <-chan error { 132 return f.errCh 133 } 134 135 func (f *forwarder) WaitForStop() { 136 <-f.stopCh 137 } 138 139 func newPortForwarder(c *client, podName, ns, localAddress string, localPort, podPort int) (PortForwarder, error) { 140 if localAddress == "" { 141 localAddress = defaultLocalAddress 142 } 143 f := &forwarder{ 144 stopCh: make(chan struct{}), 145 restConfig: c.config, 146 podName: podName, 147 ns: ns, 148 localAddress: localAddress, 149 localPort: localPort, 150 podPort: podPort, 151 } 152 153 return f, nil 154 } 155 156 func (f *forwarder) buildK8sPortForwarder(readyCh chan struct{}) (*portforward.PortForwarder, error) { 157 restClient, err := rest.RESTClientFor(f.restConfig) 158 if err != nil { 159 return nil, err 160 } 161 162 req := restClient.Post().Resource("pods").Namespace(f.ns).Name(f.podName).SubResource("portforward") 163 serverURL := req.URL() 164 165 roundTripper, upgrader, err := roundTripperFor(f.restConfig) 166 if err != nil { 167 return nil, fmt.Errorf("failure creating roundtripper: %v", err) 168 } 169 170 dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, serverURL) 171 172 fw, err := portforward.NewOnAddresses(dialer, 173 []string{f.localAddress}, 174 []string{fmt.Sprintf("%d:%d", f.localPort, f.podPort)}, 175 f.stopCh, 176 readyCh, 177 io.Discard, 178 os.Stderr) 179 if err != nil { 180 return nil, fmt.Errorf("failed establishing port-forward: %v", err) 181 } 182 183 // Run the same check as k8s.io/kubectl/pkg/cmd/portforward/portforward.go 184 // so that we will fail early if there is a problem contacting API server. 185 podGet := restClient.Get().Resource("pods").Namespace(f.ns).Name(f.podName) 186 obj, err := podGet.Do(context.TODO()).Get() 187 if err != nil { 188 return nil, fmt.Errorf("failed retrieving: %v in the %q namespace", err, f.ns) 189 } 190 pod, ok := obj.(*v1.Pod) 191 if !ok { 192 return nil, fmt.Errorf("failed getting pod, object type is %T", obj) 193 } 194 if pod.Status.Phase != v1.PodRunning { 195 return nil, fmt.Errorf("pod is not running. Status=%v", pod.Status.Phase) 196 } 197 198 return fw, nil 199 }