github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/rawexec/driver_test.go (about) 1 package rawexec 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "runtime" 10 "strconv" 11 "sync" 12 "syscall" 13 "testing" 14 "time" 15 16 "github.com/hashicorp/nomad/ci" 17 "github.com/hashicorp/nomad/client/lib/cgutil" 18 ctestutil "github.com/hashicorp/nomad/client/testutil" 19 "github.com/hashicorp/nomad/helper/pluginutils/hclutils" 20 "github.com/hashicorp/nomad/helper/testlog" 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 pstructs "github.com/hashicorp/nomad/plugins/shared/structs" 27 "github.com/hashicorp/nomad/testutil" 28 "github.com/stretchr/testify/require" 29 ) 30 31 // defaultEnv creates the default environment for raw exec tasks 32 func defaultEnv() map[string]string { 33 m := make(map[string]string) 34 if cgutil.UseV2 { 35 // normally the taskenv.Builder will set this automatically from the 36 // Node object which gets created via Client configuration, but none of 37 // that exists in the test harness so just set it here. 38 m["NOMAD_PARENT_CGROUP"] = "nomad.slice" 39 } 40 return m 41 } 42 43 func TestMain(m *testing.M) { 44 if !testtask.Run() { 45 os.Exit(m.Run()) 46 } 47 } 48 49 func newEnabledRawExecDriver(t *testing.T) *Driver { 50 ctx, cancel := context.WithCancel(context.Background()) 51 t.Cleanup(cancel) 52 53 logger := testlog.HCLogger(t) 54 d := NewRawExecDriver(ctx, logger).(*Driver) 55 d.config.Enabled = true 56 57 return d 58 } 59 60 func TestRawExecDriver_SetConfig(t *testing.T) { 61 ci.Parallel(t) 62 require := require.New(t) 63 64 ctx, cancel := context.WithCancel(context.Background()) 65 defer cancel() 66 67 logger := testlog.HCLogger(t) 68 69 d := NewRawExecDriver(ctx, logger) 70 harness := dtestutil.NewDriverHarness(t, d) 71 defer harness.Kill() 72 73 var ( 74 bconfig = new(basePlug.Config) 75 config = new(Config) 76 data = make([]byte, 0) 77 ) 78 79 // Default is raw_exec is disabled. 80 require.NoError(basePlug.MsgPackEncode(&data, config)) 81 bconfig.PluginConfig = data 82 require.NoError(harness.SetConfig(bconfig)) 83 require.Exactly(config, d.(*Driver).config) 84 85 // Enable raw_exec, but disable cgroups. 86 config.Enabled = true 87 config.NoCgroups = true 88 data = []byte{} 89 require.NoError(basePlug.MsgPackEncode(&data, config)) 90 bconfig.PluginConfig = data 91 require.NoError(harness.SetConfig(bconfig)) 92 require.Exactly(config, d.(*Driver).config) 93 94 // Enable raw_exec, enable cgroups. 95 config.NoCgroups = false 96 data = []byte{} 97 require.NoError(basePlug.MsgPackEncode(&data, config)) 98 bconfig.PluginConfig = data 99 require.NoError(harness.SetConfig(bconfig)) 100 require.Exactly(config, d.(*Driver).config) 101 } 102 103 func TestRawExecDriver_Fingerprint(t *testing.T) { 104 ci.Parallel(t) 105 106 fingerprintTest := func(config *Config, expected *drivers.Fingerprint) func(t *testing.T) { 107 return func(t *testing.T) { 108 require := require.New(t) 109 d := newEnabledRawExecDriver(t) 110 harness := dtestutil.NewDriverHarness(t, d) 111 defer harness.Kill() 112 113 var data []byte 114 require.NoError(basePlug.MsgPackEncode(&data, config)) 115 bconfig := &basePlug.Config{ 116 PluginConfig: data, 117 } 118 require.NoError(harness.SetConfig(bconfig)) 119 120 fingerCh, err := harness.Fingerprint(context.Background()) 121 require.NoError(err) 122 select { 123 case result := <-fingerCh: 124 require.Equal(expected, result) 125 case <-time.After(time.Duration(testutil.TestMultiplier()) * time.Second): 126 require.Fail("timeout receiving fingerprint") 127 } 128 } 129 } 130 131 cases := []struct { 132 Name string 133 Conf Config 134 Expected drivers.Fingerprint 135 }{ 136 { 137 Name: "Disabled", 138 Conf: Config{ 139 Enabled: false, 140 }, 141 Expected: drivers.Fingerprint{ 142 Attributes: nil, 143 Health: drivers.HealthStateUndetected, 144 HealthDescription: "disabled", 145 }, 146 }, 147 { 148 Name: "Enabled", 149 Conf: Config{ 150 Enabled: true, 151 }, 152 Expected: drivers.Fingerprint{ 153 Attributes: map[string]*pstructs.Attribute{"driver.raw_exec": pstructs.NewBoolAttribute(true)}, 154 Health: drivers.HealthStateHealthy, 155 HealthDescription: drivers.DriverHealthy, 156 }, 157 }, 158 } 159 160 for _, tc := range cases { 161 t.Run(tc.Name, fingerprintTest(&tc.Conf, &tc.Expected)) 162 } 163 } 164 165 func TestRawExecDriver_StartWait(t *testing.T) { 166 ci.Parallel(t) 167 require := require.New(t) 168 169 d := newEnabledRawExecDriver(t) 170 harness := dtestutil.NewDriverHarness(t, d) 171 defer harness.Kill() 172 task := &drivers.TaskConfig{ 173 AllocID: uuid.Generate(), 174 ID: uuid.Generate(), 175 Name: "test", 176 Env: defaultEnv(), 177 } 178 179 tc := &TaskConfig{ 180 Command: testtask.Path(), 181 Args: []string{"sleep", "10ms"}, 182 } 183 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 184 testtask.SetTaskConfigEnv(task) 185 186 cleanup := harness.MkAllocDir(task, false) 187 defer cleanup() 188 189 handle, _, err := harness.StartTask(task) 190 require.NoError(err) 191 192 ch, err := harness.WaitTask(context.Background(), handle.Config.ID) 193 require.NoError(err) 194 195 var result *drivers.ExitResult 196 select { 197 case result = <-ch: 198 case <-time.After(5 * time.Second): 199 t.Fatal("timed out") 200 } 201 202 require.Zero(result.ExitCode) 203 require.Zero(result.Signal) 204 require.False(result.OOMKilled) 205 require.NoError(result.Err) 206 require.NoError(harness.DestroyTask(task.ID, true)) 207 } 208 209 func TestRawExecDriver_StartWaitRecoverWaitStop(t *testing.T) { 210 ci.Parallel(t) 211 require := require.New(t) 212 213 d := newEnabledRawExecDriver(t) 214 harness := dtestutil.NewDriverHarness(t, d) 215 defer harness.Kill() 216 217 // Disable cgroups so test works without root 218 config := &Config{NoCgroups: true, Enabled: true} 219 var data []byte 220 require.NoError(basePlug.MsgPackEncode(&data, config)) 221 bconfig := &basePlug.Config{PluginConfig: data} 222 require.NoError(harness.SetConfig(bconfig)) 223 224 task := &drivers.TaskConfig{ 225 AllocID: uuid.Generate(), 226 ID: uuid.Generate(), 227 Name: "sleep", 228 Env: defaultEnv(), 229 } 230 tc := &TaskConfig{ 231 Command: testtask.Path(), 232 Args: []string{"sleep", "100s"}, 233 } 234 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 235 236 testtask.SetTaskConfigEnv(task) 237 cleanup := harness.MkAllocDir(task, false) 238 defer cleanup() 239 240 handle, _, err := harness.StartTask(task) 241 require.NoError(err) 242 243 ch, err := harness.WaitTask(context.Background(), task.ID) 244 require.NoError(err) 245 246 var waitDone bool 247 var wg sync.WaitGroup 248 wg.Add(1) 249 go func() { 250 defer wg.Done() 251 result := <-ch 252 require.Error(result.Err) 253 waitDone = true 254 }() 255 256 originalStatus, err := d.InspectTask(task.ID) 257 require.NoError(err) 258 259 d.tasks.Delete(task.ID) 260 261 wg.Wait() 262 require.True(waitDone) 263 _, err = d.InspectTask(task.ID) 264 require.Equal(drivers.ErrTaskNotFound, err) 265 266 err = d.RecoverTask(handle) 267 require.NoError(err) 268 269 status, err := d.InspectTask(task.ID) 270 require.NoError(err) 271 require.Exactly(originalStatus, status) 272 273 ch, err = harness.WaitTask(context.Background(), task.ID) 274 require.NoError(err) 275 276 wg.Add(1) 277 waitDone = false 278 go func() { 279 defer wg.Done() 280 result := <-ch 281 require.NoError(result.Err) 282 require.NotZero(result.ExitCode) 283 require.Equal(9, result.Signal) 284 waitDone = true 285 }() 286 287 time.Sleep(300 * time.Millisecond) 288 require.NoError(d.StopTask(task.ID, 0, "SIGKILL")) 289 wg.Wait() 290 require.NoError(d.DestroyTask(task.ID, false)) 291 require.True(waitDone) 292 } 293 294 func TestRawExecDriver_Start_Wait_AllocDir(t *testing.T) { 295 ci.Parallel(t) 296 require := require.New(t) 297 298 d := newEnabledRawExecDriver(t) 299 harness := dtestutil.NewDriverHarness(t, d) 300 defer harness.Kill() 301 302 task := &drivers.TaskConfig{ 303 AllocID: uuid.Generate(), 304 ID: uuid.Generate(), 305 Name: "sleep", 306 Env: defaultEnv(), 307 } 308 309 cleanup := harness.MkAllocDir(task, false) 310 defer cleanup() 311 312 exp := []byte("win") 313 file := "output.txt" 314 outPath := fmt.Sprintf(`%s/%s`, task.TaskDir().SharedAllocDir, file) 315 316 tc := &TaskConfig{ 317 Command: testtask.Path(), 318 Args: []string{"sleep", "1s", "write", string(exp), outPath}, 319 } 320 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 321 testtask.SetTaskConfigEnv(task) 322 323 _, _, err := harness.StartTask(task) 324 require.NoError(err) 325 326 // Task should terminate quickly 327 waitCh, err := harness.WaitTask(context.Background(), task.ID) 328 require.NoError(err) 329 330 select { 331 case res := <-waitCh: 332 require.NoError(res.Err) 333 require.True(res.Successful()) 334 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 335 require.Fail("WaitTask timeout") 336 } 337 338 // Check that data was written to the shared alloc directory. 339 outputFile := filepath.Join(task.TaskDir().SharedAllocDir, file) 340 act, err := ioutil.ReadFile(outputFile) 341 require.NoError(err) 342 require.Exactly(exp, act) 343 require.NoError(harness.DestroyTask(task.ID, true)) 344 } 345 346 // This test creates a process tree such that without cgroups tracking the 347 // processes cleanup of the children would not be possible. Thus the test 348 // asserts that the processes get killed properly when using cgroups. 349 func TestRawExecDriver_Start_Kill_Wait_Cgroup(t *testing.T) { 350 ci.Parallel(t) 351 ctestutil.ExecCompatible(t) 352 353 require := require.New(t) 354 pidFile := "pid" 355 356 d := newEnabledRawExecDriver(t) 357 harness := dtestutil.NewDriverHarness(t, d) 358 defer harness.Kill() 359 360 task := &drivers.TaskConfig{ 361 AllocID: uuid.Generate(), 362 ID: uuid.Generate(), 363 Name: "sleep", 364 User: "root", 365 Env: defaultEnv(), 366 } 367 368 cleanup := harness.MkAllocDir(task, false) 369 defer cleanup() 370 371 tc := &TaskConfig{ 372 Command: testtask.Path(), 373 Args: []string{"fork/exec", pidFile, "pgrp", "0", "sleep", "20s"}, 374 } 375 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 376 testtask.SetTaskConfigEnv(task) 377 378 _, _, err := harness.StartTask(task) 379 require.NoError(err) 380 381 // Find the process 382 var pidData []byte 383 testutil.WaitForResult(func() (bool, error) { 384 var err error 385 pidData, err = ioutil.ReadFile(filepath.Join(task.TaskDir().Dir, pidFile)) 386 if err != nil { 387 return false, err 388 } 389 390 if len(pidData) == 0 { 391 return false, fmt.Errorf("pidFile empty") 392 } 393 394 return true, nil 395 }, func(err error) { 396 require.NoError(err) 397 }) 398 399 pid, err := strconv.Atoi(string(pidData)) 400 require.NoError(err, "failed to read pidData: %s", string(pidData)) 401 402 // Check the pid is up 403 process, err := os.FindProcess(pid) 404 require.NoError(err) 405 require.NoError(process.Signal(syscall.Signal(0))) 406 407 var wg sync.WaitGroup 408 wg.Add(1) 409 go func() { 410 defer wg.Done() 411 time.Sleep(1 * time.Second) 412 err := harness.StopTask(task.ID, 0, "") 413 414 // Can't rely on the ordering between wait and kill on CI/travis... 415 if !testutil.IsCI() { 416 require.NoError(err) 417 } 418 }() 419 420 // Task should terminate quickly 421 waitCh, err := harness.WaitTask(context.Background(), task.ID) 422 require.NoError(err) 423 select { 424 case res := <-waitCh: 425 require.False(res.Successful()) 426 case <-time.After(time.Duration(testutil.TestMultiplier()*5) * time.Second): 427 require.Fail("WaitTask timeout") 428 } 429 430 testutil.WaitForResult(func() (bool, error) { 431 if err := process.Signal(syscall.Signal(0)); err == nil { 432 return false, fmt.Errorf("process should not exist: %v", pid) 433 } 434 435 return true, nil 436 }, func(err error) { 437 require.NoError(err) 438 }) 439 440 wg.Wait() 441 require.NoError(harness.DestroyTask(task.ID, true)) 442 } 443 444 func TestRawExecDriver_ParentCgroup(t *testing.T) { 445 ci.Parallel(t) 446 ctestutil.ExecCompatible(t) 447 ctestutil.CgroupsCompatibleV2(t) 448 449 d := newEnabledRawExecDriver(t) 450 harness := dtestutil.NewDriverHarness(t, d) 451 defer harness.Kill() 452 453 task := &drivers.TaskConfig{ 454 AllocID: uuid.Generate(), 455 ID: uuid.Generate(), 456 Name: "sleep", 457 Env: map[string]string{ 458 "NOMAD_PARENT_CGROUP": "custom.slice", 459 }, 460 } 461 462 cleanup := harness.MkAllocDir(task, false) 463 defer cleanup() 464 465 // run sleep task 466 tc := &TaskConfig{ 467 Command: testtask.Path(), 468 Args: []string{"sleep", "9000s"}, 469 } 470 require.NoError(t, task.EncodeConcreteDriverConfig(&tc)) 471 testtask.SetTaskConfigEnv(task) 472 _, _, err := harness.StartTask(task) 473 require.NoError(t, err) 474 475 // inspect environment variable 476 res, execErr := harness.ExecTask(task.ID, []string{"/usr/bin/env"}, 1*time.Second) 477 require.NoError(t, execErr) 478 require.True(t, res.ExitResult.Successful()) 479 require.Contains(t, string(res.Stdout), "custom.slice") 480 481 // inspect /proc/self/cgroup 482 res2, execErr2 := harness.ExecTask(task.ID, []string{"cat", "/proc/self/cgroup"}, 1*time.Second) 483 require.NoError(t, execErr2) 484 require.True(t, res2.ExitResult.Successful()) 485 require.Contains(t, string(res2.Stdout), "custom.slice") 486 487 // kill the sleep task 488 require.NoError(t, harness.DestroyTask(task.ID, true)) 489 } 490 491 func TestRawExecDriver_Exec(t *testing.T) { 492 ci.Parallel(t) 493 ctestutil.ExecCompatible(t) 494 495 require := require.New(t) 496 497 d := newEnabledRawExecDriver(t) 498 harness := dtestutil.NewDriverHarness(t, d) 499 defer harness.Kill() 500 501 task := &drivers.TaskConfig{ 502 AllocID: uuid.Generate(), 503 ID: uuid.Generate(), 504 Name: "sleep", 505 Env: defaultEnv(), 506 } 507 508 cleanup := harness.MkAllocDir(task, false) 509 defer cleanup() 510 511 tc := &TaskConfig{ 512 Command: testtask.Path(), 513 Args: []string{"sleep", "9000s"}, 514 } 515 require.NoError(task.EncodeConcreteDriverConfig(&tc)) 516 testtask.SetTaskConfigEnv(task) 517 518 _, _, err := harness.StartTask(task) 519 require.NoError(err) 520 521 if runtime.GOOS == "windows" { 522 // Exec a command that should work 523 res, err := harness.ExecTask(task.ID, []string{"cmd.exe", "/c", "echo", "hello"}, 1*time.Second) 524 require.NoError(err) 525 require.True(res.ExitResult.Successful()) 526 require.Equal(string(res.Stdout), "hello\r\n") 527 528 // Exec a command that should fail 529 res, err = harness.ExecTask(task.ID, []string{"cmd.exe", "/c", "stat", "notarealfile123abc"}, 1*time.Second) 530 require.NoError(err) 531 require.False(res.ExitResult.Successful()) 532 require.Contains(string(res.Stdout), "not recognized") 533 } else { 534 // Exec a command that should work 535 res, err := harness.ExecTask(task.ID, []string{"/usr/bin/stat", "/tmp"}, 1*time.Second) 536 require.NoError(err) 537 require.True(res.ExitResult.Successful()) 538 require.True(len(res.Stdout) > 100) 539 540 // Exec a command that should fail 541 res, err = harness.ExecTask(task.ID, []string{"/usr/bin/stat", "notarealfile123abc"}, 1*time.Second) 542 require.NoError(err) 543 require.False(res.ExitResult.Successful()) 544 require.Contains(string(res.Stdout), "No such file or directory") 545 } 546 547 require.NoError(harness.DestroyTask(task.ID, true)) 548 } 549 550 func TestConfig_ParseAllHCL(t *testing.T) { 551 ci.Parallel(t) 552 553 cfgStr := ` 554 config { 555 command = "/bin/bash" 556 args = ["-c", "echo hello"] 557 }` 558 559 expected := &TaskConfig{ 560 Command: "/bin/bash", 561 Args: []string{"-c", "echo hello"}, 562 } 563 564 var tc *TaskConfig 565 hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc) 566 567 require.EqualValues(t, expected, tc) 568 } 569 570 func TestRawExecDriver_Disabled(t *testing.T) { 571 ci.Parallel(t) 572 require := require.New(t) 573 574 d := newEnabledRawExecDriver(t) 575 d.config.Enabled = false 576 577 harness := dtestutil.NewDriverHarness(t, d) 578 defer harness.Kill() 579 task := &drivers.TaskConfig{ 580 AllocID: uuid.Generate(), 581 ID: uuid.Generate(), 582 Name: "test", 583 Env: defaultEnv(), 584 } 585 586 handle, _, err := harness.StartTask(task) 587 require.Error(err) 588 require.Contains(err.Error(), errDisabledDriver.Error()) 589 require.Nil(handle) 590 }