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