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  }