github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/rawexec/driver_unix_test.go (about) 1 //go:build !windows 2 3 package rawexec 4 5 import ( 6 "context" 7 "fmt" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "regexp" 12 "runtime" 13 "strconv" 14 "strings" 15 "syscall" 16 "testing" 17 "time" 18 19 "github.com/hashicorp/nomad/ci" 20 clienttestutil "github.com/hashicorp/nomad/client/testutil" 21 "github.com/hashicorp/nomad/helper/testtask" 22 "github.com/hashicorp/nomad/helper/uuid" 23 basePlug "github.com/hashicorp/nomad/plugins/base" 24 "github.com/hashicorp/nomad/plugins/drivers" 25 dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils" 26 "github.com/hashicorp/nomad/testutil" 27 "github.com/stretchr/testify/require" 28 "golang.org/x/sys/unix" 29 ) 30 31 func TestRawExecDriver_User(t *testing.T) { 32 ci.Parallel(t) 33 clienttestutil.RequireLinux(t) 34 require := require.New(t) 35 36 d := newEnabledRawExecDriver(t) 37 harness := dtestutil.NewDriverHarness(t, d) 38 39 task := &drivers.TaskConfig{ 40 ID: uuid.Generate(), 41 Name: "sleep", 42 User: "alice", 43 } 44 45 cleanup := harness.MkAllocDir(task, false) 46 defer cleanup() 47 48 tc := &TaskConfig{ 49 Command: testtask.Path(), 50 Args: []string{"sleep", "45s"}, 51 } 52 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 53 testtask.SetTaskConfigEnv(task) 54 55 _, _, err := harness.StartTask(task) 56 require.Error(err) 57 msg := "unknown user alice" 58 require.Contains(err.Error(), msg) 59 } 60 61 func TestRawExecDriver_Signal(t *testing.T) { 62 ci.Parallel(t) 63 clienttestutil.RequireLinux(t) 64 65 require := require.New(t) 66 67 d := newEnabledRawExecDriver(t) 68 harness := dtestutil.NewDriverHarness(t, d) 69 70 task := &drivers.TaskConfig{ 71 AllocID: uuid.Generate(), 72 ID: uuid.Generate(), 73 Name: "signal", 74 Env: defaultEnv(), 75 } 76 77 cleanup := harness.MkAllocDir(task, true) 78 defer cleanup() 79 80 tc := &TaskConfig{ 81 Command: "/bin/bash", 82 Args: []string{"test.sh"}, 83 } 84 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 85 testtask.SetTaskConfigEnv(task) 86 87 testFile := filepath.Join(task.TaskDir().Dir, "test.sh") 88 testData := []byte(` 89 at_term() { 90 echo 'Terminated.' 91 exit 3 92 } 93 trap at_term USR1 94 while true; do 95 sleep 1 96 done 97 `) 98 require.NoError(ioutil.WriteFile(testFile, testData, 0777)) 99 100 _, _, err := harness.StartTask(task) 101 require.NoError(err) 102 103 go func() { 104 time.Sleep(100 * time.Millisecond) 105 require.NoError(harness.SignalTask(task.ID, "SIGUSR1")) 106 }() 107 108 // Task should terminate quickly 109 waitCh, err := harness.WaitTask(context.Background(), task.ID) 110 require.NoError(err) 111 select { 112 case res := <-waitCh: 113 require.False(res.Successful()) 114 require.Equal(3, res.ExitCode) 115 case <-time.After(time.Duration(testutil.TestMultiplier()*6) * time.Second): 116 require.Fail("WaitTask timeout") 117 } 118 119 // Check the log file to see it exited because of the signal 120 outputFile := filepath.Join(task.TaskDir().LogDir, "signal.stdout.0") 121 exp := "Terminated." 122 testutil.WaitForResult(func() (bool, error) { 123 act, err := ioutil.ReadFile(outputFile) 124 if err != nil { 125 return false, fmt.Errorf("Couldn't read expected output: %v", err) 126 } 127 128 if strings.TrimSpace(string(act)) != exp { 129 t.Logf("Read from %v", outputFile) 130 return false, fmt.Errorf("Command outputted %v; want %v", act, exp) 131 } 132 return true, nil 133 }, func(err error) { require.NoError(err) }) 134 } 135 136 func TestRawExecDriver_StartWaitStop(t *testing.T) { 137 ci.Parallel(t) 138 require := require.New(t) 139 140 d := newEnabledRawExecDriver(t) 141 harness := dtestutil.NewDriverHarness(t, d) 142 defer harness.Kill() 143 144 // Disable cgroups so test works without root 145 config := &Config{NoCgroups: true, Enabled: true} 146 var data []byte 147 require.NoError(basePlug.MsgPackEncode(&data, config)) 148 bconfig := &basePlug.Config{PluginConfig: data} 149 require.NoError(harness.SetConfig(bconfig)) 150 151 task := &drivers.TaskConfig{ 152 ID: uuid.Generate(), 153 Name: "test", 154 } 155 156 taskConfig := map[string]interface{}{} 157 taskConfig["command"] = testtask.Path() 158 taskConfig["args"] = []string{"sleep", "100s"} 159 160 require.NoError(task.EncodeConcreteDriverConfig(&taskConfig)) 161 162 cleanup := harness.MkAllocDir(task, false) 163 defer cleanup() 164 165 handle, _, err := harness.StartTask(task) 166 require.NoError(err) 167 168 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 169 require.NoError(err) 170 171 require.NoError(harness.WaitUntilStarted(task.ID, 1*time.Second)) 172 173 go func() { 174 harness.StopTask(task.ID, 2*time.Second, "SIGINT") 175 }() 176 177 select { 178 case result := <-ch: 179 require.Equal(int(unix.SIGINT), result.Signal) 180 case <-time.After(10 * time.Second): 181 require.Fail("timeout waiting for task to shutdown") 182 } 183 184 // Ensure that the task is marked as dead, but account 185 // for WaitTask() closing channel before internal state is updated 186 testutil.WaitForResult(func() (bool, error) { 187 status, err := harness.InspectTask(task.ID) 188 if err != nil { 189 return false, fmt.Errorf("inspecting task failed: %v", err) 190 } 191 if status.State != drivers.TaskStateExited { 192 return false, fmt.Errorf("task hasn't exited yet; status: %v", status.State) 193 } 194 195 return true, nil 196 }, func(err error) { 197 require.NoError(err) 198 }) 199 200 require.NoError(harness.DestroyTask(task.ID, true)) 201 } 202 203 // TestRawExecDriver_DestroyKillsAll asserts that when TaskDestroy is called all 204 // task processes are cleaned up. 205 func TestRawExecDriver_DestroyKillsAll(t *testing.T) { 206 ci.Parallel(t) 207 clienttestutil.RequireLinux(t) 208 209 d := newEnabledRawExecDriver(t) 210 harness := dtestutil.NewDriverHarness(t, d) 211 defer harness.Kill() 212 213 task := &drivers.TaskConfig{ 214 AllocID: uuid.Generate(), 215 ID: uuid.Generate(), 216 Name: "test", 217 Env: defaultEnv(), 218 } 219 220 cleanup := harness.MkAllocDir(task, true) 221 defer cleanup() 222 223 taskConfig := map[string]interface{}{} 224 taskConfig["command"] = "/bin/sh" 225 taskConfig["args"] = []string{"-c", fmt.Sprintf(`sleep 3600 & echo "SLEEP_PID=$!"`)} 226 227 require.NoError(t, task.EncodeConcreteDriverConfig(&taskConfig)) 228 229 handle, _, err := harness.StartTask(task) 230 require.NoError(t, err) 231 defer harness.DestroyTask(task.ID, true) 232 233 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 234 require.NoError(t, err) 235 236 select { 237 case result := <-ch: 238 require.True(t, result.Successful(), "command failed: %#v", result) 239 case <-time.After(10 * time.Second): 240 require.Fail(t, "timeout waiting for task to shutdown") 241 } 242 243 sleepPid := 0 244 245 // Ensure that the task is marked as dead, but account 246 // for WaitTask() closing channel before internal state is updated 247 testutil.WaitForResult(func() (bool, error) { 248 stdout, err := ioutil.ReadFile(filepath.Join(task.TaskDir().LogDir, "test.stdout.0")) 249 if err != nil { 250 return false, fmt.Errorf("failed to output pid file: %v", err) 251 } 252 253 pidMatch := regexp.MustCompile(`SLEEP_PID=(\d+)`).FindStringSubmatch(string(stdout)) 254 if len(pidMatch) != 2 { 255 return false, fmt.Errorf("failed to find pid in %s", string(stdout)) 256 } 257 258 pid, err := strconv.Atoi(pidMatch[1]) 259 if err != nil { 260 return false, fmt.Errorf("pid parts aren't int: %s", pidMatch[1]) 261 } 262 263 sleepPid = pid 264 return true, nil 265 }, func(err error) { 266 require.NoError(t, err) 267 }) 268 269 // isProcessRunning returns an error if process is not running 270 isProcessRunning := func(pid int) error { 271 process, err := os.FindProcess(pid) 272 if err != nil { 273 return fmt.Errorf("failed to find process: %s", err) 274 } 275 276 err = process.Signal(syscall.Signal(0)) 277 if err != nil { 278 return fmt.Errorf("failed to signal process: %s", err) 279 } 280 281 return nil 282 } 283 284 require.NoError(t, isProcessRunning(sleepPid)) 285 286 require.NoError(t, harness.DestroyTask(task.ID, true)) 287 288 testutil.WaitForResult(func() (bool, error) { 289 err := isProcessRunning(sleepPid) 290 if err == nil { 291 return false, fmt.Errorf("child process is still running") 292 } 293 294 if !strings.Contains(err.Error(), "failed to signal process") { 295 return false, fmt.Errorf("unexpected error: %v", err) 296 } 297 298 return true, nil 299 }, func(err error) { 300 require.NoError(t, err) 301 }) 302 } 303 304 func TestRawExec_ExecTaskStreaming(t *testing.T) { 305 ci.Parallel(t) 306 if runtime.GOOS == "darwin" { 307 t.Skip("skip running exec tasks on darwin as darwin has restrictions on starting tty shells") 308 } 309 require := require.New(t) 310 311 d := newEnabledRawExecDriver(t) 312 harness := dtestutil.NewDriverHarness(t, d) 313 defer harness.Kill() 314 315 task := &drivers.TaskConfig{ 316 AllocID: uuid.Generate(), 317 ID: uuid.Generate(), 318 Name: "sleep", 319 Env: defaultEnv(), 320 } 321 322 cleanup := harness.MkAllocDir(task, false) 323 defer cleanup() 324 325 tc := &TaskConfig{ 326 Command: testtask.Path(), 327 Args: []string{"sleep", "9000s"}, 328 } 329 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 330 testtask.SetTaskConfigEnv(task) 331 332 _, _, err := harness.StartTask(task) 333 require.NoError(err) 334 defer d.DestroyTask(task.ID, true) 335 336 dtestutil.ExecTaskStreamingConformanceTests(t, harness, task.ID) 337 338 } 339 340 func TestRawExec_ExecTaskStreaming_User(t *testing.T) { 341 ci.Parallel(t) 342 clienttestutil.RequireLinux(t) 343 344 d := newEnabledRawExecDriver(t) 345 346 // because we cannot set AllocID, see below 347 d.config.NoCgroups = true 348 349 harness := dtestutil.NewDriverHarness(t, d) 350 defer harness.Kill() 351 352 task := &drivers.TaskConfig{ 353 // todo(shoenig) - Setting AllocID causes test to fail - with or without 354 // cgroups, and with or without chroot. It has to do with MkAllocDir 355 // creating the directory structure, but the actual root cause is still 356 // TBD. The symptom is that any command you try to execute will result 357 // in "permission denied" coming from os/exec. This test is imperfect, 358 // the actual feature of running commands as another user works fine. 359 // AllocID: uuid.Generate() 360 ID: uuid.Generate(), 361 Name: "sleep", 362 User: "nobody", 363 } 364 365 cleanup := harness.MkAllocDir(task, false) 366 defer cleanup() 367 368 err := os.Chmod(task.AllocDir, 0777) 369 require.NoError(t, err) 370 371 tc := &TaskConfig{ 372 Command: "/bin/sleep", 373 Args: []string{"9000"}, 374 } 375 require.NoError(t, task.EncodeConcreteDriverConfig(&tc)) 376 testtask.SetTaskConfigEnv(task) 377 378 _, _, err = harness.StartTask(task) 379 require.NoError(t, err) 380 defer d.DestroyTask(task.ID, true) 381 382 code, stdout, stderr := dtestutil.ExecTask(t, harness, task.ID, "whoami", false, "") 383 require.Zero(t, code) 384 require.Empty(t, stderr) 385 require.Contains(t, stdout, "nobody") 386 } 387 388 func TestRawExecDriver_NoCgroup(t *testing.T) { 389 ci.Parallel(t) 390 clienttestutil.RequireLinux(t) 391 392 expectedBytes, err := ioutil.ReadFile("/proc/self/cgroup") 393 require.NoError(t, err) 394 expected := strings.TrimSpace(string(expectedBytes)) 395 396 d := newEnabledRawExecDriver(t) 397 d.config.NoCgroups = true 398 harness := dtestutil.NewDriverHarness(t, d) 399 400 task := &drivers.TaskConfig{ 401 AllocID: uuid.Generate(), 402 ID: uuid.Generate(), 403 Name: "nocgroup", 404 } 405 406 cleanup := harness.MkAllocDir(task, true) 407 defer cleanup() 408 409 tc := &TaskConfig{ 410 Command: "/bin/cat", 411 Args: []string{"/proc/self/cgroup"}, 412 } 413 require.NoError(t, task.EncodeConcreteDriverConfig(&tc)) 414 testtask.SetTaskConfigEnv(task) 415 416 _, _, err = harness.StartTask(task) 417 require.NoError(t, err) 418 419 // Task should terminate quickly 420 waitCh, err := harness.WaitTask(context.Background(), task.ID) 421 require.NoError(t, err) 422 select { 423 case res := <-waitCh: 424 require.True(t, res.Successful()) 425 require.Zero(t, res.ExitCode) 426 case <-time.After(time.Duration(testutil.TestMultiplier()*6) * time.Second): 427 require.Fail(t, "WaitTask timeout") 428 } 429 430 // Check the log file to see it exited because of the signal 431 outputFile := filepath.Join(task.TaskDir().LogDir, "nocgroup.stdout.0") 432 testutil.WaitForResult(func() (bool, error) { 433 act, err := ioutil.ReadFile(outputFile) 434 if err != nil { 435 return false, fmt.Errorf("Couldn't read expected output: %v", err) 436 } 437 438 if strings.TrimSpace(string(act)) != expected { 439 t.Logf("Read from %v", outputFile) 440 return false, fmt.Errorf("Command outputted\n%v; want\n%v", string(act), expected) 441 } 442 return true, nil 443 }, func(err error) { require.NoError(t, err) }) 444 }