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 }