github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/allocrunner/consul_grpc_sock_hook_test.go (about)

     1  package allocrunner
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"net"
     8  	"path/filepath"
     9  	"sync"
    10  	"testing"
    11  
    12  	"github.com/hashicorp/nomad/ci"
    13  	"github.com/hashicorp/nomad/client/allocdir"
    14  	"github.com/hashicorp/nomad/client/allocrunner/interfaces"
    15  	"github.com/hashicorp/nomad/helper/testlog"
    16  	"github.com/hashicorp/nomad/nomad/mock"
    17  	"github.com/hashicorp/nomad/nomad/structs/config"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  // TestConsulGRPCSocketHook_PrerunPostrun_Ok asserts that a proxy is started when the
    23  // Consul unix socket hook's Prerun method is called and stopped with the
    24  // Postrun method is called.
    25  func TestConsulGRPCSocketHook_PrerunPostrun_Ok(t *testing.T) {
    26  	ci.Parallel(t)
    27  
    28  	// As of Consul 1.6.0 the test server does not support the gRPC
    29  	// endpoint so we have to fake it.
    30  	fakeConsul, err := net.Listen("tcp", "127.0.0.1:0")
    31  	require.NoError(t, err)
    32  	defer fakeConsul.Close()
    33  	consulConfig := &config.ConsulConfig{
    34  		GRPCAddr: fakeConsul.Addr().String(),
    35  	}
    36  
    37  	alloc := mock.ConnectAlloc()
    38  
    39  	logger := testlog.HCLogger(t)
    40  
    41  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID)
    42  	defer cleanup()
    43  
    44  	// Start the unix socket proxy
    45  	h := newConsulGRPCSocketHook(logger, alloc, allocDir, consulConfig, map[string]string{})
    46  	require.NoError(t, h.Prerun())
    47  
    48  	gRPCSock := filepath.Join(allocDir.AllocDir, allocdir.AllocGRPCSocket)
    49  	envoyConn, err := net.Dial("unix", gRPCSock)
    50  	require.NoError(t, err)
    51  
    52  	// Write to Consul to ensure data is proxied out of the netns
    53  	input := bytes.Repeat([]byte{'X'}, 5*1024)
    54  	errCh := make(chan error, 1)
    55  	go func() {
    56  		_, err := envoyConn.Write(input)
    57  		errCh <- err
    58  	}()
    59  
    60  	// Accept the connection from the netns
    61  	consulConn, err := fakeConsul.Accept()
    62  	require.NoError(t, err)
    63  	defer consulConn.Close()
    64  
    65  	output := make([]byte, len(input))
    66  	_, err = consulConn.Read(output)
    67  	require.NoError(t, err)
    68  	require.NoError(t, <-errCh)
    69  	require.Equal(t, input, output)
    70  
    71  	// Read from Consul to ensure data is proxied into the netns
    72  	input = bytes.Repeat([]byte{'Y'}, 5*1024)
    73  	go func() {
    74  		_, err := consulConn.Write(input)
    75  		errCh <- err
    76  	}()
    77  
    78  	_, err = envoyConn.Read(output)
    79  	require.NoError(t, err)
    80  	require.NoError(t, <-errCh)
    81  	require.Equal(t, input, output)
    82  
    83  	// Stop the unix socket proxy
    84  	require.NoError(t, h.Postrun())
    85  
    86  	// Consul reads should error
    87  	n, err := consulConn.Read(output)
    88  	require.Error(t, err)
    89  	require.Zero(t, n)
    90  
    91  	// Envoy reads and writes should error
    92  	n, err = envoyConn.Write(input)
    93  	require.Error(t, err)
    94  	require.Zero(t, n)
    95  	n, err = envoyConn.Read(output)
    96  	require.Error(t, err)
    97  	require.Zero(t, n)
    98  }
    99  
   100  // TestConsulGRPCSocketHook_Prerun_Error asserts that invalid Consul addresses cause
   101  // Prerun to return an error if the alloc requires a grpc proxy.
   102  func TestConsulGRPCSocketHook_Prerun_Error(t *testing.T) {
   103  	ci.Parallel(t)
   104  
   105  	logger := testlog.HCLogger(t)
   106  
   107  	// A config without an Addr or GRPCAddr is invalid.
   108  	consulConfig := &config.ConsulConfig{}
   109  
   110  	alloc := mock.Alloc()
   111  	connectAlloc := mock.ConnectAlloc()
   112  
   113  	allocDir, cleanup := allocdir.TestAllocDir(t, logger, "EnvoyBootstrap", alloc.ID)
   114  	defer cleanup()
   115  
   116  	{
   117  		// An alloc without a Connect proxy sidecar should not return
   118  		// an error.
   119  		h := newConsulGRPCSocketHook(logger, alloc, allocDir, consulConfig, map[string]string{})
   120  		require.NoError(t, h.Prerun())
   121  
   122  		// Postrun should be a noop
   123  		require.NoError(t, h.Postrun())
   124  	}
   125  
   126  	{
   127  		// An alloc *with* a Connect proxy sidecar *should* return an error
   128  		// when Consul is not configured.
   129  		h := newConsulGRPCSocketHook(logger, connectAlloc, allocDir, consulConfig, map[string]string{})
   130  		require.EqualError(t, h.Prerun(), "consul address must be set on nomad client")
   131  
   132  		// Postrun should be a noop
   133  		require.NoError(t, h.Postrun())
   134  	}
   135  
   136  	{
   137  		// Updating an alloc without a sidecar to have a sidecar should
   138  		// error when the sidecar is added.
   139  		h := newConsulGRPCSocketHook(logger, alloc, allocDir, consulConfig, map[string]string{})
   140  		require.NoError(t, h.Prerun())
   141  
   142  		req := &interfaces.RunnerUpdateRequest{
   143  			Alloc: connectAlloc,
   144  		}
   145  		require.EqualError(t, h.Update(req), "consul address must be set on nomad client")
   146  
   147  		// Postrun should be a noop
   148  		require.NoError(t, h.Postrun())
   149  	}
   150  }
   151  
   152  // TestConsulGRPCSocketHook_proxy_Unix asserts that the destination can be a unix
   153  // socket path.
   154  func TestConsulGRPCSocketHook_proxy_Unix(t *testing.T) {
   155  	ci.Parallel(t)
   156  
   157  	dir := t.TempDir()
   158  
   159  	// Setup fake listener that would be inside the netns (normally a unix
   160  	// socket, but it doesn't matter for this test).
   161  	src, err := net.Listen("tcp", "127.0.0.1:0")
   162  	require.NoError(t, err)
   163  	defer src.Close()
   164  
   165  	// Setup fake listener that would be Consul outside the netns. Use a
   166  	// socket as Consul may be configured to listen on a unix socket.
   167  	destFn := filepath.Join(dir, "fakeconsul.sock")
   168  	dest, err := net.Listen("unix", destFn)
   169  	require.NoError(t, err)
   170  	defer dest.Close()
   171  
   172  	// Collect errors (must have len > goroutines)
   173  	errCh := make(chan error, 10)
   174  
   175  	// Block until completion
   176  	wg := sync.WaitGroup{}
   177  
   178  	ctx, cancel := context.WithCancel(context.Background())
   179  	defer cancel()
   180  
   181  	wg.Add(1)
   182  	go func() {
   183  		defer wg.Done()
   184  		proxy(ctx, testlog.HCLogger(t), "unix://"+destFn, src)
   185  	}()
   186  
   187  	// Fake Envoy
   188  	// Connect and write to the src (netns) side of the proxy; then read
   189  	// and exit.
   190  	wg.Add(1)
   191  	go func() {
   192  		defer func() {
   193  			// Cancel after final read has completed (or an error
   194  			// has occurred)
   195  			cancel()
   196  
   197  			wg.Done()
   198  		}()
   199  
   200  		addr := src.Addr()
   201  		conn, err := net.Dial(addr.Network(), addr.String())
   202  		if err != nil {
   203  			errCh <- err
   204  			return
   205  		}
   206  
   207  		defer conn.Close()
   208  
   209  		if _, err := conn.Write([]byte{'X'}); err != nil {
   210  			errCh <- err
   211  			return
   212  		}
   213  
   214  		recv := make([]byte, 1)
   215  		if _, err := conn.Read(recv); err != nil {
   216  			errCh <- err
   217  			return
   218  		}
   219  
   220  		if expected := byte('Y'); recv[0] != expected {
   221  			errCh <- fmt.Errorf("expected %q but received: %q", expected, recv[0])
   222  			return
   223  		}
   224  	}()
   225  
   226  	// Fake Consul on a unix socket
   227  	// Listen, receive 1 byte, write a response, and exit
   228  	wg.Add(1)
   229  	go func() {
   230  		defer wg.Done()
   231  
   232  		conn, err := dest.Accept()
   233  		if err != nil {
   234  			errCh <- err
   235  			return
   236  		}
   237  
   238  		// Close listener now. No more connections expected.
   239  		if err := dest.Close(); err != nil {
   240  			errCh <- err
   241  			return
   242  		}
   243  
   244  		defer conn.Close()
   245  
   246  		recv := make([]byte, 1)
   247  		if _, err := conn.Read(recv); err != nil {
   248  			errCh <- err
   249  			return
   250  		}
   251  
   252  		if expected := byte('X'); recv[0] != expected {
   253  			errCh <- fmt.Errorf("expected %q but received: %q", expected, recv[0])
   254  			return
   255  		}
   256  
   257  		if _, err := conn.Write([]byte{'Y'}); err != nil {
   258  			errCh <- err
   259  			return
   260  		}
   261  	}()
   262  
   263  	// Wait for goroutines to complete
   264  	wg.Wait()
   265  
   266  	// Make sure no errors occurred
   267  	for len(errCh) > 0 {
   268  		assert.NoError(t, <-errCh)
   269  	}
   270  }