github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/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 "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) (exitCode int, stdout, stderr string) { 212 r := execTask(t, driver, taskID, cmd, tty, stdin) 213 return r.exitCode, r.stdout, r.stderr 214 } 215 216 func execTask(t *testing.T, driver *DriverHarness, taskID string, cmd string, tty bool, stdin string) execResult { 217 stream := newTestExecStream(t, tty, stdin) 218 219 ctx, cancelFn := context.WithTimeout(context.Background(), 30*time.Second) 220 defer cancelFn() 221 222 command := []string{"/bin/sh", "-c", cmd} 223 224 isRaw := false 225 exitCode := -2 226 if raw, ok := driver.impl.(drivers.ExecTaskStreamingRawDriver); ok { 227 isRaw = true 228 err := raw.ExecTaskStreamingRaw(ctx, taskID, 229 command, tty, stream) 230 require.NoError(t, err) 231 } else if d, ok := driver.impl.(drivers.ExecTaskStreamingDriver); ok { 232 execOpts, errCh := drivers.StreamToExecOptions(ctx, command, tty, stream) 233 234 r, err := d.ExecTaskStreaming(ctx, taskID, execOpts) 235 require.NoError(t, err) 236 237 select { 238 case err := <-errCh: 239 require.NoError(t, err) 240 default: 241 // all good 242 } 243 244 exitCode = r.ExitCode 245 } else { 246 require.Fail(t, "driver does not support exec") 247 } 248 249 result := stream.currentResult() 250 require.NoError(t, result.err) 251 252 if !isRaw { 253 result.exitCode = exitCode 254 } 255 256 return result 257 } 258 259 type execResult struct { 260 exitCode int 261 stdout string 262 stderr string 263 264 err error 265 } 266 267 func newTestExecStream(t *testing.T, tty bool, stdin string) *testExecStream { 268 269 return &testExecStream{ 270 t: t, 271 input: newInputStream(tty, stdin), 272 result: &execResult{exitCode: -2}, 273 } 274 } 275 276 func newInputStream(tty bool, stdin string) []*drivers.ExecTaskStreamingRequestMsg { 277 input := []*drivers.ExecTaskStreamingRequestMsg{} 278 if tty { 279 // emit two resize to ensure we honor latest 280 input = append(input, &drivers.ExecTaskStreamingRequestMsg{ 281 TtySize: &dproto.ExecTaskStreamingRequest_TerminalSize{ 282 Height: 50, 283 Width: 40, 284 }}) 285 input = append(input, &drivers.ExecTaskStreamingRequestMsg{ 286 TtySize: &dproto.ExecTaskStreamingRequest_TerminalSize{ 287 Height: 100, 288 Width: 100, 289 }}) 290 291 } 292 293 input = append(input, &drivers.ExecTaskStreamingRequestMsg{ 294 Stdin: &dproto.ExecTaskStreamingIOOperation{ 295 Data: []byte(stdin), 296 }, 297 }) 298 299 if !tty { 300 // don't close stream in interactive session and risk closing tty prematurely 301 input = append(input, &drivers.ExecTaskStreamingRequestMsg{ 302 Stdin: &dproto.ExecTaskStreamingIOOperation{ 303 Close: true, 304 }, 305 }) 306 } 307 308 return input 309 } 310 311 var _ drivers.ExecTaskStream = (*testExecStream)(nil) 312 313 type testExecStream struct { 314 t *testing.T 315 316 // input 317 input []*drivers.ExecTaskStreamingRequestMsg 318 recvCalled int 319 320 // result so far 321 resultLock sync.Mutex 322 result *execResult 323 } 324 325 func (s *testExecStream) currentResult() execResult { 326 s.resultLock.Lock() 327 defer s.resultLock.Unlock() 328 329 // make a copy 330 return *s.result 331 } 332 333 func (s *testExecStream) Recv() (*drivers.ExecTaskStreamingRequestMsg, error) { 334 if s.recvCalled >= len(s.input) { 335 return nil, io.EOF 336 } 337 338 i := s.input[s.recvCalled] 339 s.recvCalled++ 340 return i, nil 341 } 342 343 func (s *testExecStream) Send(m *drivers.ExecTaskStreamingResponseMsg) error { 344 s.resultLock.Lock() 345 defer s.resultLock.Unlock() 346 347 switch { 348 case m.Stdout != nil && m.Stdout.Data != nil: 349 s.t.Logf("received stdout: %s", string(m.Stdout.Data)) 350 s.result.stdout += string(m.Stdout.Data) 351 case m.Stderr != nil && m.Stderr.Data != nil: 352 s.t.Logf("received stderr: %s", string(m.Stderr.Data)) 353 s.result.stderr += string(m.Stderr.Data) 354 case m.Exited && m.Result != nil: 355 s.result.exitCode = int(m.Result.ExitCode) 356 } 357 358 return nil 359 }