github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/plugins/drivers/testutils/exec_testing.go (about) 1 package testutils 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "reflect" 10 "regexp" 11 "runtime" 12 "strings" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/hashicorp/nomad/plugins/drivers" 18 dproto "github.com/hashicorp/nomad/plugins/drivers/proto" 19 "github.com/stretchr/testify/require" 20 ) 21 22 func ExecTaskStreamingConformanceTests(t *testing.T, driver *DriverHarness, taskID string) { 23 t.Helper() 24 25 if runtime.GOOS == "windows" { 26 // tests assume unix-ism now 27 t.Skip("test assume unix tasks") 28 } 29 30 TestExecTaskStreamingBasicResponses(t, driver, taskID) 31 TestExecFSIsolation(t, driver, taskID) 32 } 33 34 var ExecTaskStreamingBasicCases = []struct { 35 Name string 36 Command string 37 Tty bool 38 Stdin string 39 Stdout interface{} 40 Stderr interface{} 41 ExitCode int 42 }{ 43 { 44 Name: "notty: basic", 45 Command: "echo hello stdout; echo hello stderr >&2; exit 43", 46 Tty: false, 47 Stdout: "hello stdout\n", 48 Stderr: "hello stderr\n", 49 ExitCode: 43, 50 }, 51 { 52 Name: "notty: streaming", 53 Command: "for n in 1 2 3; do echo $n; sleep 1; done", 54 Tty: false, 55 Stdout: "1\n2\n3\n", 56 ExitCode: 0, 57 }, 58 { 59 Name: "notty: stty check", 60 Command: "stty size", 61 Tty: false, 62 Stderr: regexp.MustCompile("stty: .?standard input.?: Inappropriate ioctl for device\n"), 63 ExitCode: 1, 64 }, 65 { 66 Name: "notty: stdin passing", 67 Command: "echo hello from command; head -n1", 68 Tty: false, 69 Stdin: "hello from stdin\n", 70 Stdout: "hello from command\nhello from stdin\n", 71 ExitCode: 0, 72 }, 73 { 74 Name: "notty: children processes", 75 Command: "(( sleep 3; echo from background ) & ); echo from main; exec sleep 1", 76 Tty: false, 77 // when not using tty; wait for all processes to exit matching behavior of `docker exec` 78 Stdout: "from main\nfrom background\n", 79 ExitCode: 0, 80 }, 81 82 // TTY cases - difference is new lines add `\r` and child process waiting is different 83 { 84 Name: "tty: basic", 85 Command: "echo hello stdout; echo hello stderr >&2; exit 43", 86 Tty: true, 87 Stdout: "hello stdout\r\nhello stderr\r\n", 88 ExitCode: 43, 89 }, 90 { 91 Name: "tty: streaming", 92 Command: "for n in 1 2 3; do echo $n; sleep 1; done", 93 Tty: true, 94 Stdout: "1\r\n2\r\n3\r\n", 95 ExitCode: 0, 96 }, 97 { 98 Name: "tty: stty check", 99 Command: "sleep 1; stty size", 100 Tty: true, 101 Stdout: "100 100\r\n", 102 ExitCode: 0, 103 }, 104 { 105 Name: "tty: stdin passing", 106 Command: "head -n1", 107 Tty: true, 108 Stdin: "hello from stdin\n", 109 // in tty mode, we emit line twice: once for tty echoing and one for the actual head output 110 Stdout: "hello from stdin\r\nhello from stdin\r\n", 111 ExitCode: 0, 112 }, 113 { 114 Name: "tty: children processes", 115 Command: "(( sleep 3; echo from background ) & ); echo from main; exec sleep 1", 116 Tty: true, 117 // when using tty; wait for lead process only, like `docker exec -it` 118 Stdout: "from main\r\n", 119 ExitCode: 0, 120 }, 121 } 122 123 func TestExecTaskStreamingBasicResponses(t *testing.T, driver *DriverHarness, taskID string) { 124 for _, c := range ExecTaskStreamingBasicCases { 125 t.Run("basic: "+c.Name, func(t *testing.T) { 126 127 result := execTask(t, driver, taskID, c.Command, c.Tty, c.Stdin) 128 129 require.Equal(t, c.ExitCode, result.exitCode) 130 131 switch s := c.Stdout.(type) { 132 case string: 133 require.Equal(t, s, result.stdout) 134 case *regexp.Regexp: 135 require.Regexp(t, s, result.stdout) 136 case nil: 137 require.Empty(t, result.stdout) 138 default: 139 require.Fail(t, "unexpected stdout type", "found %v (%v), but expected string or regexp", s, reflect.TypeOf(s)) 140 } 141 142 switch s := c.Stderr.(type) { 143 case string: 144 require.Equal(t, s, result.stderr) 145 case *regexp.Regexp: 146 require.Regexp(t, s, result.stderr) 147 case nil: 148 require.Empty(t, result.stderr) 149 default: 150 require.Fail(t, "unexpected stderr type", "found %v (%v), but expected string or regexp", s, reflect.TypeOf(s)) 151 } 152 153 }) 154 } 155 } 156 157 // TestExecFSIsolation asserts that exec occurs inside chroot/isolation environment rather than 158 // on host 159 func TestExecFSIsolation(t *testing.T, driver *DriverHarness, taskID string) { 160 t.Run("isolation", func(t *testing.T) { 161 caps, err := driver.Capabilities() 162 require.NoError(t, err) 163 164 isolated := (caps.FSIsolation != drivers.FSIsolationNone) 165 166 text := "hello from the other side" 167 168 // write to a file and check it presence in host 169 w := execTask(t, driver, taskID, 170 fmt.Sprintf(`FILE=$(mktemp); echo "$FILE"; echo %q >> "${FILE}"`, text), 171 false, "") 172 require.Zero(t, w.exitCode) 173 174 tempfile := strings.TrimSpace(w.stdout) 175 if !isolated { 176 defer os.Remove(tempfile) 177 } 178 179 t.Logf("created file in task: %v", tempfile) 180 181 // read from host 182 b, err := ioutil.ReadFile(tempfile) 183 if !isolated { 184 require.NoError(t, err) 185 require.Equal(t, text, strings.TrimSpace(string(b))) 186 } else { 187 require.Error(t, err) 188 require.True(t, os.IsNotExist(err)) 189 } 190 191 // read should succeed from task again 192 r := execTask(t, driver, taskID, 193 fmt.Sprintf("cat %q", tempfile), 194 false, "") 195 require.Zero(t, r.exitCode) 196 require.Equal(t, text, strings.TrimSpace(r.stdout)) 197 198 // we always run in a cgroup - testing freezer cgroup 199 r = execTask(t, driver, taskID, 200 fmt.Sprintf("cat /proc/self/cgroup"), 201 false, "") 202 require.Zero(t, r.exitCode) 203 204 if !strings.Contains(r.stdout, ":freezer:/nomad") && !strings.Contains(r.stdout, "freezer:/docker") { 205 require.Fail(t, "unexpected freezer cgroup", "expected freezer to be /nomad/ or /docker/, but found:\n%s", r.stdout) 206 207 } 208 }) 209 } 210 211 func execTask(t *testing.T, driver *DriverHarness, taskID string, cmd string, tty bool, stdin string) execResult { 212 stream := newTestExecStream(t, tty, stdin) 213 214 ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second) 215 defer cancelFn() 216 217 command := []string{"/bin/sh", "-c", cmd} 218 219 isRaw := false 220 exitCode := -2 221 if raw, ok := driver.impl.(drivers.ExecTaskStreamingRawDriver); ok { 222 isRaw = true 223 err := raw.ExecTaskStreamingRaw(ctx, taskID, 224 command, tty, stream) 225 require.NoError(t, err) 226 } else if d, ok := driver.impl.(drivers.ExecTaskStreamingDriver); ok { 227 execOpts, errCh := drivers.StreamToExecOptions(ctx, command, tty, stream) 228 229 r, err := d.ExecTaskStreaming(ctx, taskID, execOpts) 230 require.NoError(t, err) 231 232 select { 233 case err := <-errCh: 234 require.NoError(t, err) 235 default: 236 // all good 237 } 238 239 exitCode = r.ExitCode 240 } else { 241 require.Fail(t, "driver does not support exec") 242 } 243 244 result := stream.currentResult() 245 require.NoError(t, result.err) 246 247 if !isRaw { 248 result.exitCode = exitCode 249 } 250 251 return result 252 } 253 254 type execResult struct { 255 exitCode int 256 stdout string 257 stderr string 258 259 err error 260 } 261 262 func newTestExecStream(t *testing.T, tty bool, stdin string) *testExecStream { 263 264 return &testExecStream{ 265 t: t, 266 input: newInputStream(tty, stdin), 267 result: &execResult{exitCode: -2}, 268 } 269 } 270 271 func newInputStream(tty bool, stdin string) []*drivers.ExecTaskStreamingRequestMsg { 272 input := []*drivers.ExecTaskStreamingRequestMsg{} 273 if tty { 274 // emit two resize to ensure we honor latest 275 input = append(input, &drivers.ExecTaskStreamingRequestMsg{ 276 TtySize: &dproto.ExecTaskStreamingRequest_TerminalSize{ 277 Height: 50, 278 Width: 40, 279 }}) 280 input = append(input, &drivers.ExecTaskStreamingRequestMsg{ 281 TtySize: &dproto.ExecTaskStreamingRequest_TerminalSize{ 282 Height: 100, 283 Width: 100, 284 }}) 285 286 } 287 288 input = append(input, &drivers.ExecTaskStreamingRequestMsg{ 289 Stdin: &dproto.ExecTaskStreamingIOOperation{ 290 Data: []byte(stdin), 291 }, 292 }) 293 294 if !tty { 295 // don't close stream in interactive session and risk closing tty prematurely 296 input = append(input, &drivers.ExecTaskStreamingRequestMsg{ 297 Stdin: &dproto.ExecTaskStreamingIOOperation{ 298 Close: true, 299 }, 300 }) 301 } 302 303 return input 304 } 305 306 var _ drivers.ExecTaskStream = (*testExecStream)(nil) 307 308 type testExecStream struct { 309 t *testing.T 310 311 // input 312 input []*drivers.ExecTaskStreamingRequestMsg 313 recvCalled int 314 315 // result so far 316 resultLock sync.Mutex 317 result *execResult 318 } 319 320 func (s *testExecStream) currentResult() execResult { 321 s.resultLock.Lock() 322 defer s.resultLock.Unlock() 323 324 // make a copy 325 return *s.result 326 } 327 328 func (s *testExecStream) Recv() (*drivers.ExecTaskStreamingRequestMsg, error) { 329 if s.recvCalled >= len(s.input) { 330 return nil, io.EOF 331 } 332 333 i := s.input[s.recvCalled] 334 s.recvCalled++ 335 return i, nil 336 } 337 338 func (s *testExecStream) Send(m *drivers.ExecTaskStreamingResponseMsg) error { 339 s.resultLock.Lock() 340 defer s.resultLock.Unlock() 341 342 switch { 343 case m.Stdout != nil && m.Stdout.Data != nil: 344 s.t.Logf("received stdout: %s", string(m.Stdout.Data)) 345 s.result.stdout += string(m.Stdout.Data) 346 case m.Stderr != nil && m.Stderr.Data != nil: 347 s.t.Logf("received stderr: %s", string(m.Stderr.Data)) 348 s.result.stderr += string(m.Stderr.Data) 349 case m.Exited && m.Result != nil: 350 s.result.exitCode = int(m.Result.ExitCode) 351 } 352 353 return nil 354 }