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  }