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 }