github.com/pachyderm/pachyderm@v1.13.4/src/client/portforwarder.go (about)

     1  package client
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"math/rand"
     8  	"net/http"
     9  	"sync"
    10  
    11  	"github.com/pachyderm/pachyderm/src/client/pkg/config"
    12  
    13  	"github.com/pachyderm/pachyderm/src/client/pkg/errors"
    14  	log "github.com/sirupsen/logrus"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/client-go/kubernetes"
    17  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    18  	_ "k8s.io/client-go/plugin/pkg/client/auth" // enables support for configs with auth
    19  	"k8s.io/client-go/rest"
    20  	"k8s.io/client-go/tools/portforward"
    21  	"k8s.io/client-go/transport/spdy"
    22  )
    23  
    24  // PortForwarder handles proxying local traffic to a kubernetes pod
    25  type PortForwarder struct {
    26  	core          corev1.CoreV1Interface
    27  	client        rest.Interface
    28  	config        *rest.Config
    29  	namespace     string
    30  	logger        *io.PipeWriter
    31  	stopChansLock *sync.Mutex
    32  	stopChans     []chan struct{}
    33  	shutdown      bool
    34  }
    35  
    36  // NewPortForwarder creates a new port forwarder
    37  func NewPortForwarder(context *config.Context, namespace string) (*PortForwarder, error) {
    38  	if namespace == "" {
    39  		namespace = context.Namespace
    40  	}
    41  	if namespace == "" {
    42  		namespace = "default"
    43  	}
    44  
    45  	kubeConfig := config.KubeConfig(context)
    46  
    47  	kubeClientConfig, err := kubeConfig.ClientConfig()
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	client, err := kubernetes.NewForConfig(kubeClientConfig)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	core := client.CoreV1()
    58  
    59  	return &PortForwarder{
    60  		core:          core,
    61  		client:        core.RESTClient(),
    62  		config:        kubeClientConfig,
    63  		namespace:     namespace,
    64  		logger:        log.StandardLogger().Writer(),
    65  		stopChansLock: &sync.Mutex{},
    66  		stopChans:     []chan struct{}{},
    67  		shutdown:      false,
    68  	}, nil
    69  }
    70  
    71  // Run starts the port forwarder. Returns after initialization is begun with
    72  // the locally bound port and any initialization errors.
    73  func (f *PortForwarder) Run(appName string, localPort, remotePort uint16) (uint16, error) {
    74  	podNameSelector := map[string]string{
    75  		"suite": "pachyderm",
    76  		"app":   appName,
    77  	}
    78  
    79  	podList, err := f.core.Pods(f.namespace).List(metav1.ListOptions{
    80  		LabelSelector: metav1.FormatLabelSelector(metav1.SetAsLabelSelector(podNameSelector)),
    81  		TypeMeta: metav1.TypeMeta{
    82  			Kind:       "ListOptions",
    83  			APIVersion: "v1",
    84  		},
    85  	})
    86  	if err != nil {
    87  		return 0, err
    88  	}
    89  	if len(podList.Items) == 0 {
    90  		return 0, errors.Errorf("no pods found for app %s", appName)
    91  	}
    92  
    93  	// Choose a random pod
    94  	podName := podList.Items[rand.Intn(len(podList.Items))].Name
    95  
    96  	url := f.client.Post().
    97  		Resource("pods").
    98  		Namespace(f.namespace).
    99  		Name(podName).
   100  		SubResource("portforward").
   101  		URL()
   102  
   103  	transport, upgrader, err := spdy.RoundTripperFor(f.config)
   104  	if err != nil {
   105  		return 0, err
   106  	}
   107  
   108  	dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", url)
   109  	ports := []string{fmt.Sprintf("%d:%d", localPort, remotePort)}
   110  	readyChan := make(chan struct{}, 1)
   111  	stopChan := make(chan struct{}, 1)
   112  
   113  	// Ensure that the port forwarder isn't already shutdown, and append the
   114  	// shutdown channel so this forwarder can be closed
   115  	f.stopChansLock.Lock()
   116  	if f.shutdown {
   117  		f.stopChansLock.Unlock()
   118  		return 0, errors.Errorf("port forwarder is shutdown")
   119  	}
   120  	f.stopChans = append(f.stopChans, stopChan)
   121  	f.stopChansLock.Unlock()
   122  
   123  	fw, err := portforward.New(dialer, ports, stopChan, readyChan, ioutil.Discard, f.logger)
   124  	if err != nil {
   125  		return 0, err
   126  	}
   127  
   128  	errChan := make(chan error, 1)
   129  	go func() { errChan <- fw.ForwardPorts() }()
   130  
   131  	select {
   132  	case err = <-errChan:
   133  		return 0, errors.Wrap(err, "port forwarding failed")
   134  	case <-fw.Ready:
   135  	}
   136  
   137  	// don't discover the locally bound port if we already know what it is
   138  	if localPort != 0 {
   139  		return localPort, nil
   140  	}
   141  
   142  	// discover the locally bound port if we don't know what it is
   143  	bindings, err := fw.GetPorts()
   144  	if err != nil {
   145  		return 0, errors.Wrap(err, "failed to fetch local bound ports")
   146  	}
   147  
   148  	for _, binding := range bindings {
   149  		if binding.Remote == remotePort {
   150  			return binding.Local, nil
   151  		}
   152  	}
   153  
   154  	return 0, errors.New("failed to discover local bound port")
   155  }
   156  
   157  // RunForDaemon creates a port forwarder for the pachd daemon.
   158  func (f *PortForwarder) RunForDaemon(localPort, remotePort uint16) (uint16, error) {
   159  	return f.Run("pachd", localPort, remotePort)
   160  }
   161  
   162  // RunForSAMLACS creates a port forwarder for SAML ACS.
   163  func (f *PortForwarder) RunForSAMLACS(localPort, remotePort uint16) (uint16, error) {
   164  	return f.Run("pachd", localPort, remotePort)
   165  }
   166  
   167  // RunForOIDCACS creates a port forwarder for OIDC ACS.
   168  func (f *PortForwarder) RunForOIDCACS(localPort, remotePort uint16) (uint16, error) {
   169  	return f.Run("pachd", localPort, remotePort)
   170  }
   171  
   172  // RunForDashUI creates a port forwarder for the dash UI.
   173  func (f *PortForwarder) RunForDashUI(localPort uint16) (uint16, error) {
   174  	return f.Run("dash", localPort, 8080)
   175  }
   176  
   177  // RunForDashWebSocket creates a port forwarder for the dash websocket.
   178  func (f *PortForwarder) RunForDashWebSocket(localPort uint16) (uint16, error) {
   179  	return f.Run("dash", localPort, 8081)
   180  }
   181  
   182  // RunForPFS creates a port forwarder for PFS over HTTP.
   183  func (f *PortForwarder) RunForPFS(localPort uint16) (uint16, error) {
   184  	return f.Run("pachd", localPort, 30652)
   185  }
   186  
   187  // RunForS3Gateway creates a port forwarder for the s3gateway.
   188  func (f *PortForwarder) RunForS3Gateway(localPort uint16) (uint16, error) {
   189  	return f.Run("pachd", localPort, 600)
   190  }
   191  
   192  // Close shuts down port forwarding.
   193  func (f *PortForwarder) Close() {
   194  	defer f.logger.Close()
   195  
   196  	f.stopChansLock.Lock()
   197  	defer f.stopChansLock.Unlock()
   198  
   199  	if f.shutdown {
   200  		panic("port forwarder already shutdown")
   201  	}
   202  
   203  	f.shutdown = true
   204  
   205  	for _, stopChan := range f.stopChans {
   206  		close(stopChan)
   207  	}
   208  }