github.com/terraform-modules-krish/terratest@v0.29.0/modules/k8s/tunnel.go (about) 1 package k8s 2 3 // The following code is a fork of the Helm client. The main differences are: 4 // - Support testing context for better logging 5 // - Support resources other than pods 6 // See: https://github.com/helm/helm/blob/master/pkg/kube/tunnel.go 7 8 import ( 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net" 13 "net/http" 14 "strconv" 15 "strings" 16 "sync" 17 18 "github.com/stretchr/testify/require" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 "k8s.io/client-go/tools/portforward" 21 "k8s.io/client-go/transport/spdy" 22 23 "github.com/terraform-modules-krish/terratest/modules/logger" 24 "github.com/terraform-modules-krish/terratest/modules/testing" 25 ) 26 27 // Global lock to synchronize port selections 28 var globalMutex sync.Mutex 29 30 // KubeResourceType is an enum representing known resource types that can support port forwarding 31 type KubeResourceType int 32 33 const ( 34 // ResourceTypePod is a k8s pod kind identifier 35 ResourceTypePod KubeResourceType = iota 36 // ResourceTypeService is a k8s service kind identifier 37 ResourceTypeService 38 ) 39 40 func (resourceType KubeResourceType) String() string { 41 switch resourceType { 42 case ResourceTypePod: 43 return "pod" 44 case ResourceTypeService: 45 return "svc" 46 default: 47 // This should not happen 48 return "UNKNOWN_RESOURCE_TYPE" 49 } 50 } 51 52 // makeLabels is a helper to format a map of label key and value pairs into a single string for use as a selector. 53 func makeLabels(labels map[string]string) string { 54 out := []string{} 55 for key, value := range labels { 56 out = append(out, fmt.Sprintf("%s=%s", key, value)) 57 } 58 return strings.Join(out, ",") 59 } 60 61 // Tunnel is the main struct that configures and manages port forwading tunnels to Kubernetes resources. 62 type Tunnel struct { 63 out io.Writer 64 localPort int 65 remotePort int 66 kubectlOptions *KubectlOptions 67 resourceType KubeResourceType 68 resourceName string 69 stopChan chan struct{} 70 readyChan chan struct{} 71 } 72 73 // NewTunnel will create a new Tunnel struct. Note that if you use 0 for the local port, an open port on the host system 74 // will be selected automatically, and the Tunnel struct will be updated with the selected port. 75 func NewTunnel(kubectlOptions *KubectlOptions, resourceType KubeResourceType, resourceName string, local int, remote int) *Tunnel { 76 return &Tunnel{ 77 out: ioutil.Discard, 78 localPort: local, 79 remotePort: remote, 80 kubectlOptions: kubectlOptions, 81 resourceType: resourceType, 82 resourceName: resourceName, 83 stopChan: make(chan struct{}, 1), 84 readyChan: make(chan struct{}, 1), 85 } 86 } 87 88 // Endpoint returns the tunnel endpoint 89 func (tunnel *Tunnel) Endpoint() string { 90 return fmt.Sprintf("localhost:%d", tunnel.localPort) 91 } 92 93 // Close disconnects a tunnel connection by closing the StopChan, thereby stopping the goroutine. 94 func (tunnel *Tunnel) Close() { 95 close(tunnel.stopChan) 96 } 97 98 // getAttachablePodForResource will find a pod that can be port forwarded to given the provided resource type and return 99 // the name. 100 func (tunnel *Tunnel) getAttachablePodForResourceE(t testing.TestingT) (string, error) { 101 switch tunnel.resourceType { 102 case ResourceTypePod: 103 return tunnel.resourceName, nil 104 case ResourceTypeService: 105 return tunnel.getAttachablePodForServiceE(t) 106 default: 107 return "", UnknownKubeResourceType{tunnel.resourceType} 108 } 109 } 110 111 // getAttachablePodForServiceE will find an active pod associated with the Service and return the pod name. 112 func (tunnel *Tunnel) getAttachablePodForServiceE(t testing.TestingT) (string, error) { 113 service, err := GetServiceE(t, tunnel.kubectlOptions, tunnel.resourceName) 114 if err != nil { 115 return "", err 116 } 117 selectorLabelsOfPods := makeLabels(service.Spec.Selector) 118 servicePods, err := ListPodsE(t, tunnel.kubectlOptions, metav1.ListOptions{LabelSelector: selectorLabelsOfPods}) 119 if err != nil { 120 return "", err 121 } 122 for _, pod := range servicePods { 123 if IsPodAvailable(&pod) { 124 return pod.Name, nil 125 } 126 } 127 return "", ServiceNotAvailable{service} 128 } 129 130 // ForwardPort opens a tunnel to a kubernetes resource, as specified by the provided tunnel struct. This will fail the 131 // test if there is an error attempting to open the port. 132 func (tunnel *Tunnel) ForwardPort(t testing.TestingT) { 133 require.NoError(t, tunnel.ForwardPortE(t)) 134 } 135 136 // ForwardPortE opens a tunnel to a kubernetes resource, as specified by the provided tunnel struct. 137 func (tunnel *Tunnel) ForwardPortE(t testing.TestingT) error { 138 logger.Logf( 139 t, 140 "Creating a port forwarding tunnel for resource %s/%s routing local port %d to remote port %d", 141 tunnel.resourceType.String(), 142 tunnel.resourceName, 143 tunnel.localPort, 144 tunnel.remotePort, 145 ) 146 147 // Prepare a kubernetes client for the client-go library 148 clientset, err := GetKubernetesClientFromOptionsE(t, tunnel.kubectlOptions) 149 if err != nil { 150 logger.Logf(t, "Error creating a new Kubernetes client: %s", err) 151 return err 152 } 153 kubeConfigPath, err := tunnel.kubectlOptions.GetConfigPath(t) 154 if err != nil { 155 logger.Logf(t, "Error getting kube config path: %s", err) 156 return err 157 } 158 config, err := LoadApiClientConfigE(kubeConfigPath, tunnel.kubectlOptions.ContextName) 159 if err != nil { 160 logger.Logf(t, "Error loading Kubernetes config: %s", err) 161 return err 162 } 163 164 // Find the pod to port forward to 165 podName, err := tunnel.getAttachablePodForResourceE(t) 166 if err != nil { 167 logger.Logf(t, "Error finding available pod: %s", err) 168 return err 169 } 170 logger.Logf(t, "Selected pod %s to open port forward to", podName) 171 172 // Build a url to the portforward endpoint 173 // example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward 174 postEndpoint := clientset.CoreV1().RESTClient().Post() 175 namespace := tunnel.kubectlOptions.Namespace 176 portForwardCreateURL := postEndpoint. 177 Resource("pods"). 178 Namespace(namespace). 179 Name(podName). 180 SubResource("portforward"). 181 URL() 182 183 logger.Logf(t, "Using URL %s to create portforward", portForwardCreateURL) 184 185 // Construct the spdy client required by the client-go portforward library 186 transport, upgrader, err := spdy.RoundTripperFor(config) 187 if err != nil { 188 logger.Logf(t, "Error creating http client: %s", err) 189 return err 190 } 191 dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", portForwardCreateURL) 192 193 // If the localport is 0, get an available port before continuing. We do this here instead of relying on the 194 // underlying portforwarder library, because the portforwarder library does not expose the selected local port in a 195 // machine readable manner. 196 // Synchronize on the global lock to avoid race conditions with concurrently selecting the same available port, 197 // since there is a brief moment between `GetAvailablePort` and `portforwader.ForwardPorts` where the selected port 198 // is available for selection again. 199 if tunnel.localPort == 0 { 200 logger.Log(t, "Requested local port is 0. Selecting an open port on host system") 201 tunnel.localPort, err = GetAvailablePortE(t) 202 if err != nil { 203 logger.Logf(t, "Error getting available port: %s", err) 204 return err 205 } 206 logger.Logf(t, "Selected port %d", tunnel.localPort) 207 globalMutex.Lock() 208 defer globalMutex.Unlock() 209 } 210 211 // Construct a new PortForwarder struct that manages the instructed port forward tunnel 212 ports := []string{fmt.Sprintf("%d:%d", tunnel.localPort, tunnel.remotePort)} 213 portforwarder, err := portforward.New(dialer, ports, tunnel.stopChan, tunnel.readyChan, tunnel.out, tunnel.out) 214 if err != nil { 215 logger.Logf(t, "Error creating port forwarding tunnel: %s", err) 216 return err 217 } 218 219 // Open the tunnel in a goroutine so that it is available in the background. Report errors to the main goroutine via 220 // a new channel. 221 errChan := make(chan error) 222 go func() { 223 errChan <- portforwarder.ForwardPorts() 224 }() 225 226 // Wait for an error or the tunnel to be ready 227 select { 228 case err = <-errChan: 229 logger.Logf(t, "Error starting port forwarding tunnel: %s", err) 230 return err 231 case <-portforwarder.Ready: 232 logger.Logf(t, "Successfully created port forwarding tunnel") 233 return nil 234 } 235 } 236 237 // GetAvailablePort retrieves an available port on the host machine. This delegates the port selection to the golang net 238 // library by starting a server and then checking the port that the server is using. This will fail the test if it could 239 // not find an available port. 240 func GetAvailablePort(t testing.TestingT) int { 241 port, err := GetAvailablePortE(t) 242 require.NoError(t, err) 243 return port 244 } 245 246 // GetAvailablePortE retrieves an available port on the host machine. This delegates the port selection to the golang net 247 // library by starting a server and then checking the port that the server is using. 248 func GetAvailablePortE(t testing.TestingT) (int, error) { 249 l, err := net.Listen("tcp", ":0") 250 if err != nil { 251 return 0, err 252 } 253 defer l.Close() 254 255 _, p, err := net.SplitHostPort(l.Addr().String()) 256 if err != nil { 257 return 0, err 258 } 259 port, err := strconv.Atoi(p) 260 if err != nil { 261 return 0, err 262 } 263 return port, err 264 }